建造系统: - 新增「经营」页签,农田/牧场归入经营分类 - 建筑默认左向(_l.png)与素材方向统一,图标/预览/落地三处一致 - 建筑底宽对齐占地格边缘 - 修复:中键平移、建造态右键取消选择、登录态2x2被后端覆盖为3x3 资源清理(均为未引用文件): - 删除 _ 临时脚本(_c/_check_inline/_chk/_del_assets/_mw_check/_proc_fence) - 删除原始生成稿(_raw/、backgrounds/samples/、preview_clean/、设计说明图) - 删除 default/ 下冗余待审稿(farmland_20260806/well_20260806/animals_3x3/style_reference) - 删除 spring/_preview 预览图、assets/1.png、Keep_the_vast 草图
286 lines
14 KiB
HTML
286 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
<title>微观世界 · Micro World</title>
|
|
<style>
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{background:#0a0e17;color:#e0e6f0;font-family:'Segoe UI',system-ui,sans-serif;display:flex;flex-direction:column;align-items:center;min-height:100vh;padding:20px}
|
|
h1{font-size:22px;font-weight:600;letter-spacing:2px;margin-bottom:12px;color:#7dd3fc;text-shadow:0 0 20px rgba(125,211,252,.3)}
|
|
.subtitle{font-size:13px;color:#94a3b8;margin-bottom:16px}
|
|
.frame{position:relative;border-radius:12px;overflow:hidden;box-shadow:0 0 40px rgba(0,0,0,.5),inset 0 0 1px rgba(255,255,255,.06)}
|
|
canvas{display:block;background:#111827}
|
|
.hud{position:absolute;top:10px;left:12px;font-size:12px;color:#cbd5e1;background:rgba(15,23,42,.75);padding:6px 10px;border-radius:6px;line-height:1.6;backdrop-filter:blur(4px);border:1px solid rgba(255,255,255,.06)}
|
|
.hud b{color:#7dd3fc}
|
|
.minimap{position:absolute;top:10px;right:12px;width:140px;height:93px;border-radius:6px;border:1px solid rgba(255,255,255,.08);background:rgba(15,23,42,.8);overflow:hidden}
|
|
.info{margin-top:14px;font-size:12px;color:#64748b;max-width:900px;line-height:1.7}
|
|
.legend{display:flex;gap:16px;margin-top:8px;flex-wrap:wrap}
|
|
.legend span{display:flex;align-items:center;gap:5px;font-size:11px;color:#94a3b8}
|
|
.legend i{display:inline-block;width:14px;height:14px;border-radius:3px;border:1px solid rgba(255,255,255,.15)}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>微观世界 · Micro World</h1>
|
|
<p class="subtitle">动漫风地表渲染演示 — WASD / 方向键移动,点击地面寻路</p>
|
|
<div class="frame">
|
|
<canvas id="world" width="900" height="560"></canvas>
|
|
<div class="hud" id="hud"><b>X</b> -- <b>Y</b><br/><b>T</b></div>
|
|
<canvas class="minimap" id="minimap" width="140" height="93"></canvas>
|
|
</div>
|
|
<div class="info">
|
|
<div class="legend">
|
|
<span><i style="background:#4ade80"></i>草地 Grass</span>
|
|
<span><i style="background:#d97706"></i>土地 Dirt</span>
|
|
<span><i style="background:#38bdf8"></i>水地 Water</span>
|
|
<span><i style="background:linear-gradient(135deg,#4ade80,#38bdf8)"></i>过渡 Transition</span>
|
|
</div>
|
|
<p style="margin-top:8px">地形素材:动漫手绘风格 | 地图 48×28 格 | Tile 32px | Atlas 184px/tile</p>
|
|
</div>
|
|
|
|
<script>
|
|
/* ================================================================
|
|
微观世界 Demo — 动漫风地表 Tilemap 渲染
|
|
================================================================ */
|
|
|
|
const ATLAS = {"tileSize":184,"groups":{"grass":["tile_00.png","tile_01.png","tile_05.png","tile_10.png","tile_15.png","tile_18.png"],"dirt":["tile_02.png","tile_06.png","tile_07.png","tile_11.png","tile_16.png"],"water":["tile_03.png","tile_04.png","tile_12.png","tile_14.png","tile_19.png"],"trans_gw":["tile_08.png","tile_09.png"],"trans_gd":["tile_13.png"],"trans_wd":["tile_17.png"]},"tiles":{"tile_00.png":{x:0,y:0,w:184,h:184,type:"grass"},"tile_01.png":{x:184,y:0,w:184,h:184,type:"grass"},"tile_02.png":{x:368,y:0,w:184,h:184,type:"dirt"},"tile_03.png":{x:552,y:0,w:184,h:184,type:"water"},"tile_04.png":{x:736,y:0,w:184,h:184,type:"water"},"tile_05.png":{x:920,y:0,w:184,h:184,type:"grass"},"tile_06.png":{x:1104,y:0,w:184,h:184,type:"dirt"},"tile_07.png":{x:1288,y:0,w:184,h:184,type:"dirt"},"tile_08.png":{x:0,y:184,w:184,h:184,type:"trans_gw"},"tile_09.png":{x:184,y:184,w:184,h:184,type:"trans_gw"},"tile_10.png":{x:368,y:184,w:184,h:184,type:"grass"},"tile_11.png":{x:552,y:184,w:184,h:184,type:"dirt"},"tile_12.png":{x:736,y:184,w:184,h:184,type:"water"},"tile_13.png":{x:920,y:184,w:184,h:184,type:"trans_gd"},"tile_14.png":{x:1104,y:184,w:184,h:184,type:"water"},"tile_15.png":{x:1288,y:184,w:184,h:184,type:"grass"},"tile_16.png":{x:0,y:368,w:184,h:184,type:"dirt"},"tile_17.png":{x:184,y:368,w:184,h:184,type:"trans_wd"},"tile_18.png":{x:368,y:368,w:184,h:184,type:"grass"},"tile_19.png":{x:552,y:368,w:184,h:184,type:"water"}}};
|
|
|
|
const TILE = 32;
|
|
const MAP_W = 48;
|
|
const MAP_H = 28; // 必须与 MAP_RAW 实际行数一致
|
|
const CANVAS_W = 900, CANVAS_H = 560;
|
|
|
|
const T = {G:0, D:1, W:2};
|
|
const MAP_RAW = [
|
|
"GGGGGGGGGGGDDDGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG",
|
|
"GGGGGGGGGGGDDDGGGGGDDDDDDGGGGGGGGGGGGGGGGGGGGGGG",
|
|
"GGGGGGGGGGGGDDDGGGDDDDDDDDGGGGWGGWWWWWWGGGGGGGGG",
|
|
"GGGGGGGGGGGGDDDGGGGDDDDDDDDGGWWWWWWWWWWWWWGGGGGG",
|
|
"GGGDDDGGGGGGDDDGGGGDDDDDDDGGGWWWWWWWWWWWWWWGGGGG",
|
|
"GDDDDDDDGGGGDDDGGGGGDDDDGGGWWWWWWWWWWWWWWWWWGGGG",
|
|
"GDDDDDDDGGGGDDDGGGGGGGGGGGWWWWWWWWWWWWWWWWWWGGGG",
|
|
"GGDDDDDDGGGGDDDGGGGGGGGGGGWWWWWWWWWWWWWWWWWWGGGG",
|
|
"GGGDDDGGGGGDDDGGGGGGGGGGGGWWWWWWWWWWWWWWWWWWWGGG",
|
|
"GGGGGGGGGGGDDDGGGGGGGGGGGGWWWWWWWWWWWWWWWWWWGGGG",
|
|
"GGGGGGGGGGGDDDGGGGGGGGGGWWWGWWWWWWWWWWWWWWWGGGGG",
|
|
"GGGGGGGGGGDDDGGGGGGGGGGWWWGGGGGWWWWWWWWWWWGGGGGG",
|
|
"GGGGGGGGGGDDDGGGGGGGGWWWGGGGGGWWWWWWWWGGGGGGGGGG",
|
|
"DDDGGGGGGGDDDGGGGGGWWWDDDGGGGGGGGGGGGGGGGGGGGGGG",
|
|
"DDDDDGGGGGDDDGGGGGWWWDDDDDDGGGGGGGGGGGGGGGGGGGGG",
|
|
"DDDDDDDGGGDDDGGGDDWWWDDDDDDDDGGGGGGGGGGGGGGGGGGG",
|
|
"GGGDDDDDDGDDDGDDDDWWWGGGGDDDDDDGGGGGGGGGGGGGGGGG",
|
|
"GGGGGDDDDDDDDDDDDDGWWWGGGGGDDDDDGGGGGGDDDDDGGGGG",
|
|
"GGGGGGGDDDDDDDDDGGGGWWWGGGGGGDDDGGGGDDDDDDDDGGGG",
|
|
"GGGGGGGGGDDDDDGGGGGGGWWWGGGGGGGDGGGGDDDDDDDDDGGG",
|
|
"GGGGGGWWGGGGDDDGGGGGGGWWWGGGGGGGGGGGDDDDDDDDDDGG",
|
|
"GGGGWWWWWWGGDDDGGGGGGGGWWWGGGGGGGGGGDDDDDDDDDDGG",
|
|
"GGGWWWWWWWWWDDDGGGGGGGGWWWGGGGGGGGGGGDDDDDDDDGGG",
|
|
"GGGWWWWWWWWWDDDGGGGGGGWWWGGGGGGGGGGGGGGDDDGGGGGG",
|
|
"GGGWWWWWWWWWDDDGGGGGGWWWGGGGGGGGGGGGGGGGGGDDDDDD",
|
|
"GGGGWWWWWWWDDDGGGGWWWWWGGGGGGGGGGGGGGGGGDDDDDDDD",
|
|
"GGGGGGGGWGGDDDGGWWWWWGGGGGGGGGGGGGGGGGGGDDDDDDDD",
|
|
"GGGGGGGGGGGDDDDWWWWWGGGGGGGGGGGGGGGGGGGGGDDDDDDD",
|
|
];
|
|
|
|
const MAP = MAP_RAW.map(row => row.split('').map(c => c==='G'?T.G : c==='D'?T.D : T.W));
|
|
|
|
function pickTileName(mx, my) {
|
|
const here = MAP[my][mx];
|
|
const n = my > 0 ? MAP[my-1][mx] : here;
|
|
const s = my < MAP_H-1 ? MAP[my+1][mx] : here;
|
|
const w = mx > 0 ? MAP[my][mx-1] : here;
|
|
const e = mx < MAP_W-1 ? MAP[my][mx+1] : here;
|
|
if (n !== here || s !== here || w !== here || e !== here) {
|
|
const neighbors = [[n,'n'],[s,'s'],[w,'w'],[e,'e']];
|
|
for (const [ntype] of neighbors) {
|
|
if (ntype === here) continue;
|
|
const pairKey = [here, ntype].sort().join('');
|
|
const transGroup = pairKey === '02' ? 'trans_gw' : pairKey === '01' ? 'trans_gd' : pairKey === '12' ? 'trans_wd' : null;
|
|
if (transGroup && ATLAS.groups[transGroup] && ATLAS.groups[transGroup].length > 0)
|
|
return ATLAS.groups[transGroup][Math.floor(Math.random() * ATLAS.groups[transGroup].length)];
|
|
}
|
|
}
|
|
const baseType = here === T.G ? 'grass' : here === T.D ? 'dirt' : 'water';
|
|
return ATLAS.groups[baseType][Math.floor(Math.random() * ATLAS.groups[baseType].length)];
|
|
}
|
|
|
|
const TILE_MAP = [];
|
|
for (let y = 0; y < MAP_H; y++) {
|
|
TILE_MAP[y] = [];
|
|
for (let x = 0; x < MAP_W; x++) TILE_MAP[y][x] = pickTileName(x, y);
|
|
}
|
|
|
|
// 纯色降级渲染用
|
|
const COLOR_MAP = {[T.G]:'#3b7d3c',[T.D]:'#8b6914',[T.W]:'#2a6f8e'};
|
|
const TRANS_COLOR = '#5a9a6b';
|
|
|
|
// ── Canvas & State ───────────────────────────────────────────
|
|
const canvas = document.getElementById('world');
|
|
const ctx = canvas.getContext('2d');
|
|
const mini = document.getElementById('minimap');
|
|
const mctx = mini.getContext('2d');
|
|
const hud = document.getElementById('hud');
|
|
|
|
let atlasImg = null;
|
|
let atlasReady = false;
|
|
|
|
const cam = { x: 0, y: 0 };
|
|
const player = { x: 12 * TILE + TILE/2, y: 16 * TILE + TILE/2, speed: 160 };
|
|
let clickTarget = null;
|
|
const keys = {};
|
|
|
|
// ── 加载 Atlas(多路径 + 超时降级)─────────────────────────────
|
|
atlasImg = new Image();
|
|
const _URLS = ['assets/tiles/terrain_atlas.png','tiles/terrain_atlas.png'];
|
|
let _ui = 0;
|
|
function _nextAtlas() {
|
|
if (_ui < _URLS.length) { atlasImg.src = _URLS[_ui++]; return; }
|
|
// 尝试 base64 js
|
|
const sc = document.createElement('script');
|
|
sc.src = 'assets/tiles/atlas_b64.js';
|
|
sc.onload = () => { if (window.ATLAS_B64) atlasImg.src = window.ATLAS_B64; };
|
|
sc.onerror = () => console.warn('atlas 加载失败,使用纯色降级');
|
|
document.head.appendChild(sc);
|
|
}
|
|
atlasImg.onload = () => { atlasReady = true; console.log('✅ atlas loaded:', atlasImg.width,'x',atlasImg.height); };
|
|
atlasImg.onerror = () => { console.warn('❌ atlas path failed:', _URLS[_ui-1]); _nextAtlas(); };
|
|
_nextAtlas();
|
|
|
|
// 3 秒超时强制启动(纯色降级)
|
|
setTimeout(() => { if (!atlasReady) { atlasReady = true; console.log('⏰ 降级为纯色渲染'); } }, 3000);
|
|
|
|
// ── 绘制 ───────────────────────────────────────────────────────
|
|
function draw() {
|
|
ctx.fillStyle = '#0f172a';
|
|
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
|
|
|
|
// 相机跟随
|
|
const tcx = player.x - CANVAS_W / 2, tcy = player.y - CANVAS_H / 2;
|
|
cam.x += (tcx - cam.x) * 0.12;
|
|
cam.y += (tcy - cam.y) * 0.12;
|
|
cam.x = Math.max(0, Math.min(cam.x, MAP_W * TILE - CANVAS_W));
|
|
cam.y = Math.max(0, Math.min(cam.y, MAP_H * TILE - CANVAS_H));
|
|
|
|
const sc = Math.max(0, Math.floor(cam.x / TILE));
|
|
const ec = Math.min(MAP_W, Math.ceil((cam.x + CANVAS_W) / TILE));
|
|
const sr = Math.max(0, Math.floor(cam.y / TILE));
|
|
const er = Math.min(MAP_H, Math.ceil((cam.y + CANVAS_H) / TILE));
|
|
const ox = -cam.x % TILE, oy = -cam.y % TILE;
|
|
|
|
const TS = ATLAS.tileSize;
|
|
const useImage = atlasReady && atlasImg && atlasImg.naturalWidth > 0;
|
|
|
|
if (useImage) {
|
|
ctx.imageSmoothingEnabled = true;
|
|
ctx.imageSmoothingQuality = 'high';
|
|
}
|
|
|
|
for (let r = sr; r < er; r++) {
|
|
for (let c = sc; c < ec; c++) {
|
|
const px = (c - sc) * TILE + ox;
|
|
const py = (r - sr) * TILE + oy;
|
|
if (useImage) {
|
|
const name = TILE_MAP[r][c];
|
|
const t = ATLAS.tiles[name];
|
|
if (t) ctx.drawImage(atlasImg, t.x, t.y, TS, TS, px, py, TILE, TILE);
|
|
} else {
|
|
// 纯色降级
|
|
const type = MAP[r][c];
|
|
let color = COLOR_MAP[type];
|
|
// 边缘检测:如果邻居不同,用过渡色
|
|
const n = r > 0 ? MAP[r-1][c] : type;
|
|
const s = r < MAP_H-1 ? MAP[r+1][c] : type;
|
|
const w = c > 0 ? MAP[r][c-1] : type;
|
|
const e = c < MAP_W-1 ? MAP[r][c+1] : type;
|
|
if (n !== type || s !== type || w !== type || e !== type) color = TRANS_COLOR;
|
|
ctx.fillStyle = color;
|
|
ctx.fillRect(px, py, TILE+1, TILE+1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 玩家
|
|
const sx = player.x - cam.x, sy = player.y - cam.y;
|
|
ctx.save();
|
|
ctx.shadowColor = 'rgba(125,211,252,.6)';
|
|
ctx.shadowBlur = 14;
|
|
ctx.beginPath(); ctx.arc(sx, sy, 7, 0, Math.PI*2);
|
|
ctx.fillStyle = '#7dd3fc'; ctx.fill();
|
|
ctx.shadowBlur = 0;
|
|
ctx.beginPath(); ctx.arc(sx, sy, 4, 0, Math.PI*2);
|
|
ctx.fillStyle = '#fff'; ctx.fill();
|
|
ctx.restore();
|
|
|
|
// 点击目标
|
|
if (clickTarget) {
|
|
const tx = clickTarget.x - cam.x, ty = clickTarget.y - cam.y;
|
|
ctx.strokeStyle = 'rgba(125,211,252,.5)'; ctx.lineWidth = 2;
|
|
ctx.beginPath(); ctx.arc(tx, ty, 10, 0, Math.PI*2); ctx.stroke();
|
|
ctx.beginPath(); ctx.moveTo(tx-6,ty); ctx.lineTo(tx+6,ty); ctx.moveTo(tx,ty-6); ctx.lineTo(tx,ty+6); ctx.stroke();
|
|
}
|
|
|
|
// HUD
|
|
const tgx = Math.floor(player.x / TILE), tgy = Math.floor(player.y / TILE);
|
|
const tt = MAP[tgy] && MAP[tgy][tgx] !== undefined ? (MAP[tgy][tgx]===T.G?'草地':MAP[tgy][tgx]===T.D?'土地':'水地') : '?';
|
|
const mode = useImage ? '🖼️ 素材' : '🎨 纯色';
|
|
hud.innerHTML = `<b>${tgx}</b>, <b>${tgy}</b><br/><b>${tt}</b><br/><span style="font-size:10px;color:#64748b">${mode}</span>`;
|
|
|
|
// Minimap
|
|
drawMinimap();
|
|
}
|
|
|
|
function drawMinimap() {
|
|
const ms = Math.max(1, Math.floor(mini.width / MAP_W));
|
|
mctx.fillStyle = '#0f172a'; mctx.fillRect(0, 0, mini.width, mini.height);
|
|
const colors = {[T.G]:'#4ade80',[T.D]:'#d97706',[T.W]:'#38bdf8'};
|
|
for (let y = 0; y < MAP_H; y++)
|
|
for (let x = 0; x < MAP_W; x++)
|
|
mctx.fillStyle = colors[MAP[y][x]], mctx.fillRect(x*ms, y*ms, ms+.5, ms+.5);
|
|
mctx.fillStyle = '#fff';
|
|
mctx.beginPath(); mctx.arc(player.x/TILE*ms, player.y/TILE*ms, 2.5, 0, Math.PI*2); mctx.fill();
|
|
const vx = cam.x/TILE*ms, vy = cam.y/TILE*ms, vw = CANVAS_W/TILE*ms, vh = CANVAS_H/TILE*ms;
|
|
mctx.strokeStyle = 'rgba(255,255,255,.4)'; mctx.lineWidth = 1;
|
|
mctx.strokeRect(vx, vy, vw, vh);
|
|
}
|
|
|
|
// ── 移动 ───────────────────────────────────────────────────────
|
|
function update(dt) {
|
|
let dx = 0, dy = 0;
|
|
if (keys['ArrowUp']||keys['KeyW']) dy -= 1;
|
|
if (keys['ArrowDown']||keys['KeyS']) dy += 1;
|
|
if (keys['ArrowLeft']||keys['KeyA']) dx -= 1;
|
|
if (keys['ArrowRight']||keys['KeyD']) dx += 1;
|
|
if (dx || dy) { const l=Math.sqrt(dx*dx+dy*dy)||1; dx/=l; dy/=l; clickTarget=null; }
|
|
if (clickTarget && !(dx||dy)) {
|
|
const tx=clickTarget.x-player.x, ty=clickTarget.y-player.y, d=Math.sqrt(tx*tx+ty*ty);
|
|
if (d > 4) { dx=tx/d; dy=ty/d; } else clickTarget = null;
|
|
}
|
|
if (dx || dy) {
|
|
player.x += dx * player.speed * dt;
|
|
player.y += dy * player.speed * dt;
|
|
player.x = Math.max(TILE/2, Math.min(MAP_W*TILE-TILE/2, player.x));
|
|
player.y = Math.max(TILE/2, Math.min(MAP_H*TILE-TILE/2, player.y));
|
|
}
|
|
}
|
|
|
|
// ── 主循环 ─────────────────────────────────────────────────────
|
|
let last = performance.now();
|
|
function loop(now) {
|
|
const dt = Math.min((now - last) / 1000, 0.05);
|
|
last = now;
|
|
update(dt);
|
|
draw();
|
|
requestAnimationFrame(loop);
|
|
}
|
|
requestAnimationFrame(loop);
|
|
|
|
window.addEventListener('keydown', e => { keys[e.code] = true; });
|
|
window.addEventListener('keyup', e => { keys[e.code] = false; });
|
|
canvas.addEventListener('click', e => {
|
|
const rect = canvas.getBoundingClientRect();
|
|
clickTarget = { x: (e.clientX - rect.left) + cam.x, y: (e.clientY - rect.top) + cam.y };
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|