game/website/demo.html

1087 lines
42 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇森 · 世界沙盘</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sans-serif; color: #e0e0e0; }
#gameCanvas { display: block; cursor: default; }
#gameCanvas.dragging { cursor: grabbing; }
#gameCanvas.canDrag { cursor: grab; }
#topBar {
position: fixed; top: 0; left: 0; right: 0; height: 44px;
background: linear-gradient(180deg, rgba(10,14,23,0.95) 0%, rgba(10,14,23,0.7) 100%);
display: flex; align-items: center; padding: 0 20px;
border-bottom: 1px solid rgba(100,200,255,0.15);
z-index: 100; backdrop-filter: blur(8px);
}
#topBar .title {
font-size: 15px; font-weight: bold;
background: linear-gradient(90deg, #4af, #a8e6cf);
-webkit-background-clip: text; -webkit-text-fill-color:transparent;
text-decoration:none;
}
#topBar .nav-links { display:flex; gap:16px; margin-left:24px; }
#topBar .nav-links a { color:#667788; text-decoration:none; font-size:12px; transition:color 0.2s; }
#topBar .nav-links a:hover { color:#4af; }
#topBar .coords { margin-left:8px; font-size:12px; color:#8899aa; }
/* 微观世界视图 */
#microView {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: #0a0e17; z-index: 200;
display: none; flex-direction: column;
}
#microView.active { display: flex; }
#microHeader {
height: 50px; display: flex; align-items: center; padding: 0 16px;
background: linear-gradient(90deg, rgba(10,14,23,0.98), rgba(20,30,50,0.98));
border-bottom: 1px solid rgba(100,200,255,0.2);
z-index: 10; flex-shrink: 0;
pointer-events: auto;
}
#microHeader .backBtn {
background: rgba(100,200,255,0.15); border: 1px solid rgba(100,200,255,0.3);
color: #4af; padding: 6px 16px; border-radius: 6px; cursor: pointer;
font-size: 13px; transition: all 0.2s;
}
#microHeader .backBtn:hover { background: rgba(100,200,255,0.25); }
#microHeader .microTitle { margin-left: 16px; font-size: 16px; font-weight: bold; color: #e0e0e0; }
#microHeader .microSubtitle { margin-left: 12px; font-size: 12px; color: #8899aa; }
#microBody { flex: 1; display: flex; position: relative; overflow: hidden; }
#microCanvas { flex: 1; display: block; }
/* 微观世界右侧面板 */
#microSidebar {
width: 280px; background: linear-gradient(270deg, rgba(10,14,23,0.98) 0%, rgba(10,14,23,0.85) 100%);
border-left: 1px solid rgba(100,200,255,0.15);
padding: 16px; overflow-y: auto; flex-shrink: 0;
pointer-events: auto;
}
#microSidebar::-webkit-scrollbar { width: 4px; }
#microSidebar::-webkit-scrollbar-thumb { background: rgba(100,200,255,0.3); border-radius: 2px; }
.panel-section { margin-bottom: 20px; }
.panel-label { font-size: 11px; color: #667788; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
.panel-value { font-size: 14px; color: #e0e0e0; margin-bottom: 4px; }
.resource-bar { height: 18px; background: rgba(255,255,255,0.08); border-radius: 9px; overflow: hidden; margin: 6px 0; position: relative; }
.resource-fill { height: 100%; border-radius: 9px; transition: width 0.5s ease; position: relative; }
.resource-fill::after {
content: ''; position: absolute; top: 0; left: 0; right: 0; height: 50%;
background: linear-gradient(180deg, rgba(255,255,255,0.2), transparent);
border-radius: 9px 9px 0 0;
}
.unit-card {
background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1);
border-radius: 8px; padding: 10px; margin-bottom: 8px;
cursor: pointer; transition: all 0.2s;
}
.unit-card:hover { background: rgba(100,200,255,0.1); border-color: rgba(100,200,255,0.3); }
.unit-card .unit-name { font-size: 13px; font-weight: bold; color: #e0e0e0; }
.unit-card .unit-role { font-size: 11px; color: #8899aa; }
.unit-card .unit-status { font-size: 11px; margin-top: 4px; }
.status-working { color: #8bc34a; }
.status-resting { color: #ff9800; }
.status-moving { color: #4af; }
/* 飞入过渡遮罩 */
#flyInOverlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: #0a0e17; z-index: 300;
display: none; justify-content: center; align-items: center; flex-direction: column;
}
#flyInOverlay.active { display: flex; }
.fly-circle {
width: 60px; height: 60px; border: 2px solid rgba(100,200,255,0.5);
border-top-color: #4af; border-radius: 50%;
animation: flySpin 1s linear infinite;
}
#flyInOverlay .flyText {
font-size: 18px; color: #4af; margin-top: 20px;
animation: flyPulse 1s ease-in-out infinite;
}
@keyframes flySpin { to { transform: rotate(360deg); } }
@keyframes flyPulse { 0%, 100% { opacity: 0.5; } 50% { opacity: 1; } }
/* 提示 */
#tip {
position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%);
background: rgba(10,14,23,0.9); border: 1px solid rgba(100,200,255,0.3);
padding: 8px 20px; border-radius: 20px; font-size: 12px; color: #8899aa;
z-index: 100; pointer-events: none; transition: opacity 0.3s;
}
/* 加载界面 */
#loadingScreen {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: #0a0e17; z-index: 500;
display: flex; flex-direction: column; justify-content: center; align-items: center;
}
#loadingScreen .loadTitle {
font-size: 24px; font-weight: bold; margin-bottom: 20px;
background: linear-gradient(90deg, #4af, #a8e6cf);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
#loadingScreen .loadBar { width: 200px; height: 3px; background: rgba(255,255,255,0.1); border-radius: 2px; overflow: hidden; }
#loadingScreen .loadFill { height: 100%; width: 0%; background: linear-gradient(90deg, #4af, #a8e6cf); border-radius: 2px; transition: width 0.3s; }
#loadingScreen .loadText { font-size: 12px; color: #667788; margin-top: 12px; }
/* 单位详情弹窗 */
#unitModal {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.7); z-index: 400; display: none;
justify-content: center; align-items: center;
}
#unitModal.active { display: flex; }
#unitModal .modalContent {
background: #1a2030; border: 1px solid rgba(100,200,255,0.3); border-radius: 12px;
padding: 24px; max-width: 340px; width: 90%; pointer-events: auto;
}
#unitModal .modalTitle { font-size: 16px; font-weight: bold; color: #e0e0e0; margin-bottom: 16px; }
#unitModal .statRow { display: flex; justify-content: space-between; align-items: center; margin: 6px 0; font-size: 12px; }
#unitModal .statLabel { color: #8899aa; }
#unitModal .statValue { color: #e0e0e0; }
#unitModal .statBar { flex: 1; height: 8px; background: rgba(255,255,255,0.08); border-radius: 4px; margin: 0 8px; overflow: hidden; }
#unitModal .statFill { height: 100%; border-radius: 4px; }
#unitModal .cmdBtn {
display: block; width: 100%; margin-top: 8px; padding: 8px;
background: rgba(100,200,255,0.1); border: 1px solid rgba(100,200,255,0.25);
color: #cce; border-radius: 6px; cursor: pointer; font-size: 12px; text-align: center;
transition: all 0.2s;
}
#unitModal .cmdBtn:hover { background: rgba(100,200,255,0.2); }
#unitModal .closeBtn {
margin-top: 16px; width: 100%; padding: 8px; background: rgba(255,80,80,0.15);
border: 1px solid rgba(255,80,80,0.3); color: #f88; border-radius: 6px;
cursor: pointer; font-size: 13px;
}
</style>
<link rel="stylesheet" href="auth.css">
</head>
<body>
<!-- 加载界面 -->
<div id="loadingScreen">
<div class="loadTitle">元宇宙世界</div>
<div class="loadBar"><div class="loadFill" id="loadFill"></div></div>
<div class="loadText" id="loadText">正在生成世界...</div>
</div>
<!-- 主视图 -->
<canvas id="gameCanvas"></canvas>
<div id="topBar">
<a href="index.html" class="title">✦ 宇森</a>
<div class="nav-links">
<a href="intro.html">介绍</a>
<a href="platform.html">架构</a>
<a href="world.html">世界</a>
<a href="races.html">种族</a>
</div>
<div id="auth-area" style="margin-left:auto; margin-right:8px;"></div>
<span class="coords" id="coords">坐标: (0, 0)</span>
</div>
<div id="tip">🖱️ 滚轮缩放 · 拖拽移动 · 点击格子进入微观世界</div>
<!-- 飞入过渡 -->
<div id="flyInOverlay">
<div class="fly-circle"></div>
<div class="flyText">正在进入微观世界...</div>
</div>
<!-- 微观世界视图 -->
<div id="microView">
<div id="microHeader">
<button class="backBtn" id="backBtn">← 返回世界</button>
<span class="microTitle" id="microTitle">平原村落</span>
<span class="microSubtitle" id="microSubtitle">微观世界视角</span>
</div>
<div id="microBody">
<canvas id="microCanvas"></canvas>
<div id="microSidebar">
<div class="panel-section">
<div class="panel-label">地块信息</div>
<div class="panel-value" id="detailTerrain">🟢 平原</div>
<div class="panel-value" id="detailCoords">坐标: (0, 0)</div>
</div>
<div class="panel-section">
<div class="panel-label">建筑</div>
<div id="detailBuilding"></div>
</div>
<div class="panel-section">
<div class="panel-label">资源产出</div>
<div id="detailResources"></div>
</div>
<div class="panel-section">
<div class="panel-label">人口</div>
<div id="detailPopulation"></div>
</div>
</div>
</div>
</div>
<!-- 单位详情弹窗 -->
<div id="unitModal">
<div class="modalContent">
<div class="modalTitle" id="modalTitle">👤 单位详情</div>
<div id="modalBody"></div>
<button class="closeBtn" onclick="closeUnitModal()">关闭</button>
</div>
</div>
<script>
// ============ 配置 ============
const MAP_RADIUS = 4;
const POLY_SIZE = 52;
const SIDES = 6; // 正六边形
let currentLayer = 'surface';
// ============ 地形 ============
const TERRAIN = {
plain: { name: '平原', color: '#4a7c4a', microBg: '#3d6b3d', groundColor: '#5a8a4a', scene: 'village' },
forest: { name: '森林', color: '#2d5a2d', microBg: '#1e4a1e', groundColor: '#2a5a2a', scene: 'forest' },
mountain: { name: '山脉', color: '#6b6b6b', microBg: '#5a5a5a', groundColor: '#7a7a7a', scene: 'mountain' },
water: { name: '水域', color: '#1a4a7a', microBg: '#0e3a6a', groundColor: '#1a5a8a', scene: 'water' },
desert: { name: '荒漠', color: '#b8944a', microBg: '#a07e3a', groundColor: '#c8a85a', scene: 'desert' },
snow: { name: '雪地', color: '#8aaaaa', microBg: '#7a9aaa', groundColor: '#c8d8e8', scene: 'snow' },
wetland: { name: '湿地', color: '#3a6a5a', microBg: '#2a5a4a', groundColor: '#4a7a6a', scene: 'wetland' }
};
// ============ 建筑 ============
const BUILDINGS = {
farm: { name: '农场', icon: '🌾', color: '#8bc34a', yield: { food: 8 } },
lumber: { name: '伐木场', icon: '🪓', color: '#795548', yield: { wood: 6 } },
mine: { name: '矿场', icon: '⛏', color: '#ff9800', yield: { metal: 5 } },
house: { name: '民居', icon: '🏠', color: '#64b5f6', yield: { pop: 4 } },
market: { name: '集市', icon: '🏪', color: '#4dd0e1', yield: { gold: 6 } },
temple: { name: '神庙', icon: '⛩', color: '#fff176', yield: { culture: 3 } },
barracks: { name: '兵营', icon: '⚔', color: '#ef5350', yield: { military: 8 } },
library: { name: '图书馆', icon: '📚', color: '#ab47bc', yield: { research: 4 } }
};
// ============ 单位 ============
const UNITS = {
farmer: { name: '农夫', icon: '👨🌾', role: 'farm' },
lumberjack:{ name: '伐木工', icon: '🪓', role: 'lumber' },
miner: { name: '矿工', icon: '⛏️', role: 'mine' },
merchant: { name: '商人', icon: '🧑💼', role: 'trade' },
guard: { name: '守卫', icon: '💂', role: 'defense' },
scholar: { name: '学者', icon: '📖', role: 'research' },
priest: { name: '祭司', icon: '🧙', role: 'culture' },
child: { name: '孩童', icon: '👶', role: 'none' },
fisher: { name: '渔民', icon: '🎣', role: 'fish' }
};
// ============ 全局状态 ============
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let W, H;
let camera = { x: 0, y: 0, zoom: 1.6 };
let hoveredTile = null;
let selectedTile = null;
// 拖拽状态(修复版)
let isMouseDown = false;
let hasDragged = false;
let mouseDownX = 0;
let mouseDownY = 0;
let dragCamStartX = 0;
let dragCamStartY = 0;
let tiles = [];
let animTime = 0;
let stars = [];
// 微观世界
let microState = null;
let microCanvas, microCtx;
let microAnimId = null;
// ============ 地图生成 ============
function generateMap() {
tiles = [];
for (let q = -MAP_RADIUS; q <= MAP_RADIUS; q++) {
for (let r = -MAP_RADIUS; r <= MAP_RADIUS; r++) {
const s = -q - r;
if (Math.abs(s) > MAP_RADIUS) continue;
const dist = Math.sqrt(q * q + r * r) / MAP_RADIUS;
let terrain;
if (dist < 0.15) terrain = 'plain';
else if (dist < 0.3) terrain = Math.random() > 0.4 ? 'forest' : 'plain';
else if (dist < 0.5) terrain = Math.random() > 0.5 ? 'mountain' : 'plain';
else if (Math.random() > 0.65) terrain = 'desert';
else if (Math.random() > 0.8) terrain = 'snow';
else if (Math.random() > 0.85) terrain = 'water';
else terrain = 'plain';
const hasBuilding = Math.random() > 0.7;
const buildingType = hasBuilding ? Object.keys(BUILDINGS)[Math.floor(Math.random() * Object.keys(BUILDINGS).length)] : null;
const unitCount = hasBuilding ? Math.floor(Math.random() * 4) + 1 : (Math.random() > 0.55 ? Math.floor(Math.random() * 2) + 1 : 0);
const units = [];
for (let i = 0; i < unitCount; i++) {
const uKeys = Object.keys(UNITS);
units.push({
...UNITS[uKeys[Math.floor(Math.random() * uKeys.length)]],
id: Math.random().toString(36).substr(2, 6),
status: Math.random() > 0.3 ? 'working' : 'resting',
hp: 80 + Math.floor(Math.random() * 40),
energy: 60 + Math.floor(Math.random() * 40),
mood: ['😊','😐','😄'][Math.floor(Math.random()*3)],
level: Math.floor(Math.random() * 3) + 1
});
}
tiles.push({ q, r, s, terrain, layer: 'surface', hasBuilding, buildingType,
buildingLevel: hasBuilding ? Math.floor(Math.random() * 3) + 1 : 0,
units, unitCount, explored: Math.random() > 0.2
});
}
}
}
// ============ 坐标工具 ============
function hexToPixel(q, r) {
const x = POLY_SIZE * (Math.sqrt(3) * q + Math.sqrt(3) / 2 * r);
const y = POLY_SIZE * (3 / 2 * r);
return { x: x * camera.zoom + W / 2 + camera.x, y: y * camera.zoom + H / 2 + camera.y };
}
function pixelToHex(px, py) {
const size = POLY_SIZE * camera.zoom;
const x = (px - W / 2 - camera.x) / size;
const y = (py - H / 2 - camera.y) / size;
let q = (Math.sqrt(3) / 3 * x - 1 / 3 * y);
let r = (2 / 3 * y);
const fq = Math.round(q), fr = Math.round(r);
return { q: fq, r: fr, s: -fq - fr };
}
function getTile(q, r) { return tiles.find(t => t.q === q && t.r === r && t.layer === currentLayer); }
// ============ 绘制12边形 ============
function drawPoly(x, y, size, fillColor, strokeColor, lineWidth) {
ctx.beginPath();
for (let i = 0; i < SIDES; i++) {
const angle = (Math.PI * 2 / SIDES) * i - Math.PI / 2;
const px = x + size * Math.cos(angle);
const py = y + size * Math.sin(angle);
if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.closePath();
if (fillColor) { ctx.fillStyle = fillColor; ctx.fill(); }
if (strokeColor) { ctx.strokeStyle = strokeColor; ctx.lineWidth = lineWidth || 1; ctx.stroke(); }
}
// ============ 渲染 ============
function render() {
animTime += 0.016;
ctx.clearRect(0, 0, W, H);
drawStarfield();
const size = POLY_SIZE * camera.zoom;
const sortedTiles = tiles.filter(t => t.layer === currentLayer).sort((a, b) => {
return (a.r + a.q * 0.5) - (b.r + b.q * 0.5);
});
sortedTiles.forEach(tile => {
const { x, y } = hexToPixel(tile.q, tile.r);
if (x < -size * 2 || x > W + size * 2 || y < -size * 2 || y > H + size * 2) return;
const terrain = TERRAIN[tile.terrain];
const isHovered = hoveredTile && hoveredTile.q === tile.q && hoveredTile.r === tile.r;
const isSelected = selectedTile && selectedTile.q === tile.q && selectedTile.r === tile.r;
let fillColor = terrain.color;
let strokeColor = 'rgba(255,255,255,0.12)';
let lineWidth = 1;
if (isSelected) { strokeColor = '#4af'; lineWidth = 2.5; }
else if (isHovered) { strokeColor = 'rgba(100,200,255,0.6)'; lineWidth = 1.5; }
drawPoly(x, y, size * 0.99, fillColor, strokeColor, lineWidth);
// 悬停光晕
if (isHovered) {
ctx.save();
ctx.shadowColor = '#4af'; ctx.shadowBlur = 12;
drawPoly(x, y, size * 0.99, 'rgba(100,200,255,0.08)', '#4af', 1.5);
ctx.restore();
}
// 地形图标
if (camera.zoom > 0.3) {
ctx.font = `${13 * camera.zoom}px sans-serif`;
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
const icons = { plain:'🌿', forest:'🌲', mountain:'⛰', water:'💧', desert:'🏜', snow:'❄', wetland:'🌾' };
ctx.fillText(icons[tile.terrain] || '', x, y - 5 * camera.zoom);
}
// 建筑
if (tile.hasBuilding && camera.zoom > 0.25) {
const b = BUILDINGS[tile.buildingType];
const bSize = 15 * camera.zoom;
const pulse = 0.7 + 0.3 * Math.sin(animTime * 3 + tile.q * 0.7 + tile.r * 0.7);
ctx.save();
ctx.shadowColor = b.color; ctx.shadowBlur = 8 * pulse;
ctx.fillStyle = b.color; ctx.globalAlpha = 0.2 * pulse;
ctx.beginPath(); ctx.arc(x, y + 7 * camera.zoom, bSize * 0.7, 0, Math.PI * 2); ctx.fill();
ctx.restore();
ctx.font = `${14 * camera.zoom}px sans-serif`;
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(b.icon, x, y + 12 * camera.zoom);
// 等级
if (tile.buildingLevel > 1 && camera.zoom > 0.45) {
for (let i = 0; i < Math.min(tile.buildingLevel, 3); i++) {
ctx.fillStyle = '#ffeb3b';
ctx.beginPath();
ctx.arc(x - 7 * camera.zoom + i * 7 * camera.zoom, y - 12 * camera.zoom, 2 * camera.zoom, 0, Math.PI * 2);
ctx.fill();
}
}
}
// 单位指示
if (tile.unitCount > 0 && camera.zoom > 0.4) {
ctx.font = `${8 * camera.zoom}px sans-serif`;
ctx.textAlign = 'center';
ctx.fillText('👥 ' + tile.unitCount, x + 14 * camera.zoom, y + 14 * camera.zoom);
}
// 选中虚线动画
if (isSelected) {
const p = 0.5 + 0.5 * Math.sin(animTime * 4);
ctx.save();
ctx.strokeStyle = `rgba(100,200,255,${0.3 + 0.3 * p})`;
ctx.lineWidth = 2; ctx.setLineDash([4 * camera.zoom, 4 * camera.zoom]);
drawPoly(x, y, size * 0.99, null, `rgba(100,200,255,${0.3 + 0.3 * p})`, 2);
ctx.setLineDash([]);
ctx.restore();
}
});
requestAnimationFrame(render);
}
// ============ 星空背景 ============
function initStars() {
stars = [];
for (let i = 0; i < 100; i++) {
stars.push({ x: Math.random() * 2000, y: Math.random() * 2000,
r: Math.random() * 1.2 + 0.3, speed: Math.random() * 0.3 + 0.1,
alpha: Math.random() * 0.5 + 0.2 });
}
}
function drawStarfield() {
ctx.fillStyle = '#0a0e17'; ctx.fillRect(0, 0, W, H);
stars.forEach(s => {
s.y += s.speed * 0.3;
if (s.y > H + 10) { s.y = -10; s.x = Math.random() * W; }
const twinkle = s.alpha + 0.15 * Math.sin(animTime * 2 + s.x);
ctx.fillStyle = `rgba(180,200,255,${twinkle})`;
ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2); ctx.fill();
});
}
// ============ 进入微观世界 ============
function enterMicroWorld(tile) {
if (!tile) return;
const overlay = document.getElementById('flyInOverlay');
overlay.classList.add('active');
setTimeout(() => {
overlay.classList.remove('active');
microState = {
tile,
buildings: tile.hasBuilding ? [{ ...BUILDINGS[tile.buildingType], type: tile.buildingType, level: tile.buildingLevel, hp: 80 + tile.buildingLevel * 10 }] : [],
units: tile.units || [],
resources: calcResources(tile),
animTime: 0
};
document.getElementById('microView').classList.add('active');
document.getElementById('microTitle').textContent = `${tile.terrain === 'plain' ? '🌿' : TERRAIN[tile.terrain].name} ${TERRAIN[tile.terrain].name}村落`;
document.getElementById('microSubtitle').textContent = `坐标(${tile.q}, ${tile.r}) · ${microState.units.length}位居民`;
updateSidebar();
initMicroCanvas();
startMicroRender();
}, 500);
}
function calcResources(tile) {
const res = {};
if (tile.hasBuilding && BUILDINGS[tile.buildingType]) {
for (const [k, v] of Object.entries(BUILDINGS[tile.buildingType].yield)) res[k] = v * tile.buildingLevel;
}
return res;
}
function updateSidebar() {
const tile = microState.tile;
document.getElementById('detailTerrain').textContent = `${TERRAIN[tile.terrain].name} · 坐标(${tile.q}, ${tile.r})`;
const buildingDiv = document.getElementById('detailBuilding');
if (tile.hasBuilding) {
const b = BUILDINGS[tile.buildingType];
buildingDiv.innerHTML = `<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
<span style="font-size:22px;">${b.icon}</span>
<div><div style="font-size:14px;font-weight:bold;color:#e0e0e0;">${b.name}</div>
<div style="font-size:11px;color:#8899aa;">Lv.${tile.buildingLevel}</div></div></div>`;
} else { buildingDiv.innerHTML = '<div style="color:#667788;font-size:12px;">可建造建筑</div>'; }
const resDiv = document.getElementById('detailResources');
const res = microState.resources;
if (Object.keys(res).length > 0) {
const resNames = { food:'🍖 食物', wood:'🪵 木材', metal:'⛏ 金属', gold:'💰 金币', culture:'🏛 文化', research:'🔬 科研', defense:'🛡 防御', military:'⚔ 军事', pop:'👥 人口' };
resDiv.innerHTML = Object.entries(res).map(([k,v]) => `<div style="display:flex;justify-content:space-between;align-items:center;margin:4px 0;">
<span style="font-size:12px;color:#ccd;">${resNames[k]||k}</span>
<div class="resource-bar" style="width:100px;">
<div class="resource-fill" style="width:${Math.min(v*12,100)}%;background:linear-gradient(90deg,#4af,#a8e6cf);"></div></div>
<span style="font-size:12px;color:#8bc34a;">+${v}</span></div>`).join('');
} else { resDiv.innerHTML = '<div style="color:#667788;font-size:12px;">无产出</div>'; }
const popDiv = document.getElementById('detailPopulation');
if (microState.units.length > 0) {
popDiv.innerHTML = microState.units.map(u => `<div class="unit-card" onclick="showUnitModal('${u.id}')">
<div style="display:flex;align-items:center;gap:8px;">
<span style="font-size:18px;">${u.icon}</span>
<div><div class="unit-name">${u.name}</div><div class="unit-role">${u.status === 'working' ? '🟢' : u.status === 'resting' ? '🟡' : '🔵'} ${u.status}</div></div>
</div></div>`).join('');
} else { popDiv.innerHTML = '<div style="color:#667788;font-size:12px;">暂无居民</div>'; }
}
// ============ 微观世界 Canvas 渲染 ============
function initMicroCanvas() {
microCanvas = document.getElementById('microCanvas');
const body = document.getElementById('microBody');
microCanvas.width = body.clientWidth - 280;
microCanvas.height = body.clientHeight;
microCtx = microCanvas.getContext('2d');
}
function startMicroRender() {
if (!microState) return;
if (microAnimId) cancelAnimationFrame(microAnimId);
function loop() {
if (!microState) return;
microAnimId = requestAnimationFrame(loop);
microState.animTime += 0.016;
const mW = microCanvas.width, mH = microCanvas.height;
const tile = microState.tile;
const terrain = TERRAIN[tile.terrain];
// 背景天空
const skyGrad = microCtx.createLinearGradient(0, 0, 0, mH * 0.55);
if (tile.terrain === 'snow') { skyGrad.addColorStop(0, '#8aaaaa'); skyGrad.addColorStop(1, '#c8d8e8'); }
else if (tile.terrain === 'desert') { skyGrad.addColorStop(0, '#e8c878'); skyGrad.addColorStop(1, '#c8a85a'); }
else { skyGrad.addColorStop(0, '#4a7aaa'); skyGrad.addColorStop(1, terrain.groundColor + 'cc'); }
microCtx.fillStyle = skyGrad;
microCtx.fillRect(0, 0, mW, mH);
// 太阳/月亮
drawSkyDecor(microCtx, mW, mH, tile.terrain);
// 远处山/树林(剪影)
drawDistantScenery(microCtx, mW, mH, tile.terrain);
// 地面
const groundY = mH * 0.55;
drawGround(microCtx, mW, mH, groundY, tile);
// 场景元素(农田/建筑/街道)
drawSceneElements(microCtx, mW, mH, groundY, microState);
// 单位
microState.units.forEach((u, i) => drawMicroUnit(microCtx, mW, mH, groundY, u, i, microState.animTime));
// 粒子
drawParticles(microCtx, mW, mH);
// 提示文字
microCtx.fillStyle = 'rgba(255,255,255,0.35)';
microCtx.font = '12px sans-serif';
microCtx.textAlign = 'left';
microCtx.fillText('🖱️ 滚动缩放微观场景 · 点击单位卡片查看详情', 16, mH - 16);
}
loop();
}
// ============ 微观世界场景绘制 ============
function drawSkyDecor(c, mW, mH, terrain) {
// 太阳或月亮
if (terrain === 'snow' || terrain === 'mountain') {
// 月亮
c.fillStyle = 'rgba(200,220,255,0.8)';
c.beginPath(); c.arc(mW * 0.8, 60, 25, 0, Math.PI * 2); c.fill();
c.fillStyle = skyGrad || '#8aaaaa';
c.beginPath(); c.arc(mW * 0.8 + 8, 55, 22, 0, Math.PI * 2); c.fill();
} else {
// 太阳
const sunGlow = c.createRadialGradient(mW * 0.75, 50, 0, mW * 0.75, 50, 60);
sunGlow.addColorStop(0, 'rgba(255,230,100,0.6)');
sunGlow.addColorStop(1, 'rgba(255,230,100,0)');
c.fillStyle = sunGlow;
c.fillRect(0, 0, mW, mH * 0.55);
c.fillStyle = '#ffe664';
c.beginPath(); c.arc(mW * 0.75, 50, 18, 0, Math.PI * 2); c.fill();
}
// 云
for (let i = 0; i < 4; i++) {
const cx = (microState.animTime * 8 + i * mW * 0.3) % (mW + 200) - 100;
const cy = 30 + i * 25;
c.fillStyle = 'rgba(255,255,255,0.12)';
c.beginPath(); c.ellipse(cx, cy, 50 + i * 10, 14, 0, 0, Math.PI * 2); c.fill();
c.beginPath(); c.ellipse(cx - 20, cy + 5, 30, 10, 0, 0, Math.PI * 2); c.fill();
c.beginPath(); c.ellipse(cx + 20, cy + 3, 35, 12, 0, 0, Math.PI * 2); c.fill();
}
}
function drawDistantScenery(c, mW, mH, terrain) {
const groundY = mH * 0.55;
// 远山剪影
c.fillStyle = 'rgba(0,0,0,0.15)';
c.beginPath(); c.moveTo(0, groundY);
for (let x = 0; x <= mW; x += 40) {
const h = Math.sin(x * 0.008) * 40 + Math.sin(x * 0.003) * 60 + 20;
c.lineTo(x, groundY - h);
}
c.lineTo(mW, groundY); c.fill();
// 远景树林剪影
if (terrain === 'forest' || terrain === 'plain') {
c.fillStyle = 'rgba(0,0,0,0.1)';
for (let i = 0; i < 12; i++) {
const tx = (i / 12) * mW + Math.sin(i * 3) * 20;
const th = 30 + Math.sin(i * 2.7) * 15;
drawTreeSilhouette(c, tx, groundY - th + 10, th * 0.6, 12);
}
}
}
function drawTreeSilhouette(c, x, y, h, size) {
// 树干
c.fillStyle = 'rgba(80,50,20,0.3)';
c.fillRect(x - 2, y, 4, h * 0.4);
// 树冠
c.fillStyle = 'rgba(20,60,20,0.25)';
c.beginPath(); c.arc(x, y - h * 0.2, size, 0, Math.PI * 2); c.fill();
c.beginPath(); c.arc(x - size * 0.5, y, size * 0.8, 0, Math.PI * 2); c.fill();
c.beginPath(); c.arc(x + size * 0.4, y + 2, size * 0.7, 0, Math.PI * 2); c.fill();
}
function drawGround(c, mW, mH, groundY, tile) {
const terrain = TERRAIN[tile.terrain];
// 地面主体
c.fillStyle = terrain.groundColor;
c.fillRect(0, groundY, mW, mH - groundY);
// 地面纹理/草地
if (tile.terrain === 'plain' || tile.terrain === 'forest') {
for (let i = 0; i < 30; i++) {
const gx = (Math.sin(i * 7.3) * 0.5 + 0.5) * mW;
const gy = groundY + (Math.cos(i * 5.7) * 0.5 + 0.5) * (mH - groundY) * 0.7;
c.fillStyle = `rgba(100,180,100,${0.15 + 0.1 * Math.sin(microState.animTime * 2 + i)})`;
c.font = '10px sans-serif';
c.fillText(['🌱','🌿','🍀'][i % 3], gx, gy);
}
// 小路
c.strokeStyle = 'rgba(160,130,80,0.4)';
c.lineWidth = 18; c.lineCap = 'round';
c.beginPath(); c.moveTo(mW * 0.1, groundY + 20); c.quadraticCurveTo(mW * 0.5, groundY + 60, mW * 0.9, groundY + 30); c.stroke();
} else if (tile.terrain === 'desert') {
for (let i = 0; i < 15; i++) {
const gx = (Math.sin(i * 9.3) * 0.5 + 0.5) * mW;
const gy = groundY + (Math.cos(i * 6.7) * 0.5 + 0.5) * (mH - groundY) * 0.6;
c.fillStyle = `rgba(180,160,100,${0.08 + 0.05 * Math.sin(microState.animTime + i)})`;
c.font = '10px sans-serif'; c.fillText('·', gx, gy);
}
} else if (tile.terrain === 'snow') {
// 雪地雪花
for (let i = 0; i < 20; i++) {
const gx = Math.random() * mW;
const gy = groundY + Math.random() * (mH - groundY);
c.fillStyle = `rgba(255,255,255,${0.1 + 0.1 * Math.sin(microState.animTime + i)})`;
c.font = '8px sans-serif'; c.fillText('❄', gx, gy);
}
}
}
function drawSceneElements(c, mW, mH, groundY, state) {
const tile = state.tile;
const hasBuilding = tile.hasBuilding;
if (hasBuilding) {
// 绘制建筑
state.buildings.forEach((b, bi) => {
const bx = mW * 0.35 + bi * 80;
const by = groundY + 30;
drawBuilding(c, bx, by, b, state.animTime, mW, mH);
});
// 如果有农场,画农田
if (tile.buildingType === 'farm') {
drawFarmland(c, mW, mH, groundY);
}
} else {
// 无建筑时画几间小屋
for (let i = 0; i < 2; i++) {
drawSmallHouse(c, mW * 0.3 + i * 100, groundY + 25, state.animTime);
}
}
// 街道/路径(连接建筑)
if (hasBuilding) {
c.strokeStyle = 'rgba(180,160,120,0.3)';
c.lineWidth = 12; c.lineCap = 'round';
c.beginPath();
c.moveTo(mW * 0.15, groundY + 50);
c.lineTo(mW * 0.85, groundY + 40);
c.stroke();
// 石头纹理
for (let i = 0; i < 8; i++) {
const sx = mW * 0.15 + (mW * 0.7 / 8) * i + Math.sin(i) * 5;
const sy = groundY + 45 + Math.cos(i * 2) * 5;
c.fillStyle = 'rgba(180,170,150,0.25)';
c.beginPath(); c.arc(sx, sy, 4, 0, Math.PI * 2); c.fill();
}
}
}
function drawBuilding(c, x, y, building, time, mW, mH) {
const pulse = 0.8 + 0.2 * Math.sin(time * 3);
const w = 44, h = 50;
// 阴影
c.fillStyle = 'rgba(0,0,0,0.3)';
c.beginPath(); c.ellipse(x, y + h * 0.45, w * 0.6, 8, 0, 0, Math.PI * 2); c.fill();
// 建筑主体
c.fillStyle = building.color + 'dd';
c.strokeStyle = building.color;
c.lineWidth = 2;
// 自定义圆角矩形兼容非Chrome浏览器
const r = 4;
c.beginPath();
c.moveTo(x - w/2 + r, y - h/2);
c.lineTo(x + w/2 - r, y - h/2);
c.quadraticCurveTo(x + w/2, y - h/2, x + w/2, y - h/2 + r);
c.lineTo(x + w/2, y + h/2 - r);
c.quadraticCurveTo(x + w/2, y + h/2, x + w/2 - r, y + h/2);
c.lineTo(x - w/2 + r, y + h/2);
c.quadraticCurveTo(x - w/2, y + h/2, x - w/2, y + h/2 - r);
c.lineTo(x - w/2, y - h/2 + r);
c.quadraticCurveTo(x - w/2, y - h/2, x - w/2 + r, y - h/2);
c.closePath();
c.fill(); c.stroke();
// 屋顶
c.fillStyle = shadeColor(building.color, -30);
c.beginPath();
c.moveTo(x - w/2 - 6, y - h/2);
c.lineTo(x, y - h/2 - 16);
c.lineTo(x + w/2 + 6, y - h/2);
c.closePath(); c.fill();
// 门
c.fillStyle = shadeColor(building.color, -40);
c.fillRect(x - 5, y + h/2 - 18, 10, 18);
// 门把手
c.fillStyle = '#ffeb3b';
c.beginPath(); c.arc(x + 3, y + h/2 - 9, 1.5, 0, Math.PI * 2); c.fill();
// 窗户(亮灯动画)
const windowLit = Math.sin(time * 2) > -0.3;
c.fillStyle = windowLit ? 'rgba(255,230,100,0.7)' : 'rgba(80,80,80,0.5)';
c.fillRect(x - 12, y - h/2 + 8, 8, 7);
c.fillRect(x + 4, y - h/2 + 8, 8, 7);
// 建筑图标
c.font = '22px sans-serif';
c.textAlign = 'center'; c.textBaseline = 'middle';
c.fillText(building.icon, x, y - 2);
// 名称
c.fillStyle = '#e0e0e0'; c.font = '10px sans-serif';
c.textAlign = 'center';
c.fillText(building.name, x, y + h/2 + 14);
// 工作动画
if (building.type === 'farm' || building.type === 'mine') {
const workAnim = Math.sin(time * 4) > 0;
if (workAnim) {
c.font = '12px sans-serif';
c.fillText(building.type === 'farm' ? '💪' : '⚒', x + 22, y - 8 + Math.sin(time * 5) * 4);
}
}
}
function drawSmallHouse(c, x, y, time) {
// 小屋
c.fillStyle = '#8b7355cc';
const r = 3;
c.beginPath();
c.moveTo(x - 16 + r, y - 14);
c.lineTo(x + 16 - r, y - 14);
c.quadraticCurveTo(x + 16, y - 14, x + 16, y - 14 + r);
c.lineTo(x + 16, y + 14 - r);
c.quadraticCurveTo(x + 16, y + 14, x + 16 - r, y + 14);
c.lineTo(x - 16 + r, y + 14);
c.quadraticCurveTo(x - 16, y + 14, x - 16, y + 14 - r);
c.lineTo(x - 16, y - 14 + r);
c.quadraticCurveTo(x - 16, y - 14, x - 16 + r, y - 14);
c.closePath(); c.fill();
// 屋顶
c.fillStyle = '#a0522d';
c.beginPath(); c.moveTo(x - 20, y - 14); c.lineTo(x, y - 28); c.lineTo(x + 20, y - 14); c.closePath(); c.fill();
// 门
c.fillStyle = '#5a3a1a'; c.fillRect(x - 4, y + 2, 8, 12);
// 窗
const lit = Math.sin(time * 1.5) > 0;
c.fillStyle = lit ? 'rgba(255,230,100,0.6)' : 'rgba(80,80,80,0.4)';
c.fillRect(x - 12, y - 8, 6, 5);
}
function drawFarmland(c, mW, mH, groundY) {
// 农田行
const farmY = groundY + 70;
for (let row = 0; row < 3; row++) {
for (let col = 0; col < 5; col++) {
const fx = mW * 0.55 + col * 24;
const fy = farmY + row * 22;
// 田垄
c.fillStyle = 'rgba(120,180,80,0.3)';
c.fillRect(fx - 8, fy - 3, 16, 6);
// 作物
c.fillStyle = 'rgba(80,140,40,0.5)';
c.fillRect(fx - 1, fy - 7, 2, 7);
// 生长动画
const grow = 0.8 + 0.2 * Math.sin(microState.animTime * 2 + row + col);
c.fillStyle = `rgba(100,180,60,${0.3 * grow})`;
c.fillRect(fx - 1, fy - 9, 2, 9 * grow);
}
}
}
function drawMicroUnit(c, mW, mH, groundY, unit, index, time) {
// 围绕建筑散布
const angle = (index / Math.max(microState.units.length, 1)) * Math.PI * 2 + time * 0.3;
const radius = 60 + index * 18;
const ux = mW * 0.65 + Math.cos(angle) * radius * 0.5;
const uy = groundY + 40 + Math.sin(angle) * radius * 0.3;
const bob = Math.sin(time * 2 + index * 1.5) * 3;
// 阴影
c.fillStyle = 'rgba(0,0,0,0.2)';
c.beginPath(); c.ellipse(ux, uy + 22, 10, 4, 0, 0, Math.PI * 2); c.fill();
// 身体(小人)
const bodyColor = unit.status === 'working' ? '#8bc34a' : unit.status === 'resting' ? '#ff9800' : '#4af';
c.fillStyle = bodyColor + '60';
c.beginPath(); c.arc(ux, uy + bob, 14, 0, Math.PI * 2); c.fill();
c.strokeStyle = bodyColor; c.lineWidth = 1.5;
c.stroke();
// 图标
c.font = '20px sans-serif';
c.textAlign = 'center'; c.textBaseline = 'middle';
c.fillText(unit.icon, ux, uy + bob);
// 名字
c.fillStyle = '#ccd'; c.font = '9px sans-serif';
c.textAlign = 'center';
c.fillText(unit.name, ux, uy + bob + 24);
// 状态灯
const dotColor = unit.status === 'working' ? '#8bc34a' : unit.status === 'resting' ? '#ff9800' : '#4af';
c.fillStyle = dotColor;
c.beginPath(); c.arc(ux + 12, uy + bob - 12, 3, 0, Math.PI * 2); c.fill();
}
function drawParticles(c, mW, mH) {
// 初始化粒子
if (!microState._particles) {
microState._particles = [];
for (let i = 0; i < 12; i++) {
microState._particles.push({
x: Math.random() * mW, y: mH + Math.random() * 50,
vx: (Math.random() - 0.5) * 0.4, vy: -(Math.random() * 0.6 + 0.2),
life: Math.random() * 120 + 60, maxLife: Math.random() * 120 + 60,
text: ['✨','🌟','💫','+1'][Math.floor(Math.random()*4)],
size: 10 + Math.random() * 6
});
}
}
microState._particles.forEach(p => {
p.x += p.vx; p.y += p.vy; p.life--;
if (p.life <= 0 || p.y < -20) {
p.x = Math.random() * mW; p.y = mH + 10; p.life = p.maxLife;
}
const alpha = p.life / p.maxLife * 0.5;
c.globalAlpha = alpha;
c.font = `${p.size}px sans-serif`; c.textAlign = 'center';
c.fillText(p.text, p.x, p.y);
c.globalAlpha = 1;
});
}
// ============ 单位详情弹窗 ============
function showUnitModal(unitId) {
if (!microState) return;
const unit = microState.units.find(u => u.id === unitId);
if (!unit) return;
const modal = document.getElementById('unitModal');
document.getElementById('modalTitle').textContent = `${unit.icon} ${unit.name}`;
const hpColor = unit.hp > 70 ? '#8bc34a' : unit.hp > 40 ? '#ff9800' : '#f44336';
const enColor = unit.energy > 70 ? '#4af' : unit.energy > 40 ? '#ff9800' : '#f44336';
document.getElementById('modalBody').innerHTML = `
<div class="statRow"><span class="statLabel">状态</span><span class="statValue" style="color:${unit.status==='working'?'#8bc34a':unit.status==='resting'?'#ff9800':'#4af'}">${unit.status==='working'?'🟢 工作中':unit.status==='resting'?'🟡 休息中':'🔵 移动中'}</span></div>
<div class="statRow"><span class="statLabel">生命值</span><div class="statBar"><div class="statFill" style="width:${unit.hp}%;background:${hpColor};"></div></div><span class="statValue">${unit.hp}</span></div>
<div class="statRow"><span class="statLabel">能量</span><div class="statBar"><div class="statFill" style="width:${unit.energy}%;background:${enColor};"></div></div><span class="statValue">${unit.energy}</span></div>
<div class="statRow"><span class="statLabel">等级</span><span class="statValue">Lv.${unit.level} ${'★'.repeat(unit.level)}${'☆'.repeat(3-unit.level)}</span></div>
<div class="statRow"><span class="statLabel">心情</span><span class="statValue">${unit.mood}</span></div>
<div style="margin-top:16px;"><div class="panel-label">操作指令</div>
<div class="cmdBtn" onclick="alert('已派遣工作')">📋 派遣工作</div>
<div class="cmdBtn" onclick="alert('已令其休息')">😴 令其休息</div>
<div class="cmdBtn" onclick="alert('已下达移动指令')">🚶 移动到指定位置</div>
<div class="cmdBtn" onclick="alert('训练中...')">⬆️ 训练升级</div>
</div>
`;
modal.classList.add('active');
}
function closeUnitModal() { document.getElementById('unitModal').classList.remove('active'); }
// ============ 工具函数 ============
function shadeColor(color, percent) {
const num = parseInt(color.replace('#', ''), 16);
const r = Math.min(255, Math.max(0, (num >> 16) + percent));
const g = Math.min(255, Math.max(0, ((num >> 8) & 0x00FF) + percent));
const b = Math.min(255, Math.max(0, (num & 0x0000FF) + percent));
return `rgb(${r},${g},${b})`;
}
// ============ 事件(修复版)============
canvas.addEventListener('wheel', e => {
e.preventDefault();
const zoomDelta = e.deltaY > 0 ? 0.9 : 1.1;
camera.zoom = Math.max(0.4, Math.min(4, camera.zoom * zoomDelta));
// 更新光标状态
canvas.classList.add('canDrag');
}, { passive: false });
canvas.addEventListener('mousedown', e => {
if (e.button !== 0) return;
isMouseDown = true;
hasDragged = false;
mouseDownX = e.clientX;
mouseDownY = e.clientY;
dragCamStartX = camera.x;
dragCamStartY = camera.y;
canvas.classList.add('dragging');
});
canvas.addEventListener('mousemove', e => {
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
const hex = pixelToHex(mx, my);
document.getElementById('coords').textContent = `坐标: (${hex.q}, ${hex.r})`;
if (isMouseDown) {
const dx = e.clientX - mouseDownX;
const dy = e.clientY - mouseDownY;
if (Math.abs(dx) > 8 || Math.abs(dy) > 8) {
hasDragged = true;
}
if (hasDragged) {
camera.x = dragCamStartX + (e.clientX - mouseDownX);
camera.y = dragCamStartY + (e.clientY - mouseDownY);
}
} else {
// 只有没按下鼠标时才检测悬停
hoveredTile = getTile(hex.q, hex.r);
}
});
canvas.addEventListener('mouseup', e => {
if (!isMouseDown) return;
isMouseDown = false;
canvas.classList.remove('dragging');
if (!hasDragged) {
// 这是点击,不是拖拽
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
const hex = pixelToHex(mx, my);
const tile = getTile(hex.q, hex.r);
if (tile) {
selectedTile = tile;
enterMicroWorld(tile);
}
}
});
canvas.addEventListener('mouseleave', () => {
isMouseDown = false;
hasDragged = false;
canvas.classList.remove('dragging');
hoveredTile = null;
});
// ESC退出微观世界
document.addEventListener('keydown', e => { if (e.key === 'Escape') exitMicroWorld(); });
document.getElementById('backBtn').addEventListener('click', exitMicroWorld);
document.getElementById('unitModal').addEventListener('click', e => { if (e.target === document.getElementById('unitModal')) closeUnitModal(); });
function exitMicroWorld() {
document.getElementById('microView').classList.remove('active');
if (microAnimId) { cancelAnimationFrame(microAnimId); microAnimId = null; }
microState = null;
}
// ============ 初始化 ============
function init() {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
canvas.classList.add('canDrag');
initStars();
// 加载动画
const loadFill = document.getElementById('loadFill');
const loadText = document.getElementById('loadText');
let progress = 0;
const loadInterval = setInterval(() => {
progress += Math.random() * 18 + 7;
if (progress >= 100) {
progress = 100; clearInterval(loadInterval);
setTimeout(() => {
document.getElementById('loadingScreen').style.display = 'none';
requestAnimationFrame(render);
}, 300);
}
loadFill.style.width = progress + '%';
const msgs = ['正在生成世界...','种植森林...','建造村落...','召唤居民...','世界已就绪!'];
loadText.textContent = msgs[Math.min(Math.floor(progress / 25), msgs.length - 1)];
}, 120);
}
window.addEventListener('resize', () => {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
});
generateMap();
init();
</script>
<script src="auth.js"></script>
<script>YuSenAuth.init();</script>
</body>
</html>