1088 lines
42 KiB
HTML
1088 lines
42 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<base href="https://yt.pengyuthon.cn/">
|
||
<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>
|