diff --git a/world.html b/world.html
index d4da67f..bb1f115 100644
--- a/world.html
+++ b/world.html
@@ -36,25 +36,25 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
font-size: 15px; font-weight: 600; color: #cfe1f3; -webkit-text-fill-color: #cfe1f3;
margin-left: 10px; padding-left: 10px; border-left: 1px solid rgba(255,255,255,.28);
}
+#topBar .topRight { margin-left:auto; display:flex; align-items:center; gap:14px; }
#topBar .coords { font-size: 14px; color: #a8b8cc; }
/* ===== 顶部右侧用户区(融合进 #topBar,恒在最右侧) ===== */
-#topBar .userArea{position:relative;margin-left:auto;pointer-events:auto;}
+#topBar .userArea{position:relative;pointer-events:auto;}
#topBar .ua-trigger{display:flex;align-items:center;gap:8px;background:rgba(139,92,246,.18);
border:1px solid rgba(139,92,246,.5);color:#e9edf6;padding:7px 14px;border-radius:999px;
- font:inherit;font-size:14px;font-weight:500;cursor:pointer;transition:.2s;}
-#topBar .ua-trigger:hover{background:rgba(139,92,246,.32);border-color:#a78bfa;}
+ font:inherit;font-size:14px;font-weight:500;cursor:pointer;}
#topBar .ua-avatar{width:22px;height:22px;border-radius:50%;
background:linear-gradient(135deg,#c77dff,#4af);display:inline-flex;align-items:center;justify-content:center;
font-size:11px;color:#fff;font-weight:700;}
#topBar .ua-name{font-weight:600;color:#c77dff;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
-#topBar .ua-caret{font-size:10px;color:#9aa6c4;transition:transform .2s;}
+#topBar .ua-caret{font-size:10px;color:#9aa6c4;}
#topBar .ua-trigger[aria-expanded="true"] .ua-caret{transform:rotate(180deg);}
#topBar .ua-menu{position:absolute;top:calc(100% + 8px);right:0;min-width:200px;
background:#0e1428;border:1px solid rgba(139,92,246,.45);border-radius:12px;padding:6px;
- box-shadow:0 10px 28px rgba(0,0,0,.55);display:none;z-index:120;}
-#topBar .ua-menu.open{display:block;animation:uaSlide .18s ease-out;}
-@keyframes uaSlide{from{opacity:0;transform:translateY(-6px);}to{opacity:1;transform:translateY(0);}}
+ box-shadow:0 4px 12px rgba(0,0,0,.5);z-index:120;
+ visibility:hidden;opacity:0;pointer-events:none;}
+#topBar .ua-menu.open{visibility:visible;opacity:1;pointer-events:auto;}
#topBar .ua-item{display:flex;align-items:center;justify-content:space-between;width:100%;
background:transparent;border:none;color:#cfd5ea;padding:9px 12px;border-radius:8px;
font:inherit;font-size:14px;cursor:pointer;text-align:left;transition:.15s;}
@@ -118,7 +118,7 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
}
#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; }
+#microHeader .microSubtitle { display: none; }
#microBody { flex: 1; display: flex; flex-direction: column; position: relative; overflow: hidden; }
#microCanvas { flex: 1; display: block; }
@@ -346,7 +346,7 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
border:1px solid rgba(255,255,255,.22);cursor:default;user-select:none;transition:background-color .9s ease,border-color .2s;
box-shadow:0 6px 22px rgba(0,0,0,.6);}
#clockWidget svg{display:block;}
-#clockWidget #clockLabel{font-size:15px;font-weight:700;color:#ffffff;font-variant-numeric:tabular-nums;white-space:nowrap;text-shadow:0 1px 4px rgba(0,0,0,.95),0 0 2px rgba(0,0,0,.8);}
+#clockWidget #clockLabel{font-size:18px;font-weight:700;color:#ffffff;font-variant-numeric:tabular-nums;white-space:nowrap;text-shadow:0 1px 4px rgba(0,0,0,.95),0 0 2px rgba(0,0,0,.8);}
#clockWidget #calendarLabel{font-size:13px;font-weight:700;color:#ffffff;font-variant-numeric:tabular-nums;white-space:nowrap;letter-spacing:.4px;text-shadow:0 1px 4px rgba(0,0,0,.95),0 0 2px rgba(0,0,0,.8);}
#clockWidget:hover{border-color:rgba(139,92,246,.5);}
#sceneImage{position:fixed;inset:0;width:100%;height:100%;object-fit:cover;z-index:-1;}
@@ -374,6 +374,29 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
transition:opacity .2s ease,transform .2s ease;}
#worldNav .wn-item:hover .wn-text{opacity:1;transform:translateY(-50%) translateX(0);}
+/* 右下角导航控件:方向 + 缩放(白色半透、大气精致) */
+#navControls{position:fixed;bottom:120px;right:22px;z-index:50;
+ display:grid;grid-template-columns:repeat(3,68px);grid-template-rows:repeat(3,68px);gap:7px;}
+#navControls button{width:68px;height:68px;border:none;border-radius:17px;
+ background:rgba(255,255,255,0.18);color:#fff;font-size:28px;cursor:pointer;
+ display:flex;align-items:center;justify-content:center;
+ backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);
+ border:1px solid rgba(255,255,255,0.3);
+ box-shadow:0 4px 16px rgba(0,0,0,0.25),inset 0 1px 0 rgba(255,255,255,0.2);
+ transition:background .15s,transform .12s;user-select:none;}
+#navControls button:hover{background:rgba(255,255,255,0.32);transform:scale(1.08);}
+#navControls button:active{background:rgba(255,255,255,0.44);transform:scale(0.94);}
+#navControls .nc-up{grid-column:2;grid-row:1;}
+#navControls .nc-nw{grid-column:1;grid-row:1;}
+#navControls .nc-ne{grid-column:3;grid-row:1;}
+#navControls .nc-left{grid-column:1;grid-row:2;}
+#navControls .nc-center{grid-column:2;grid-row:2;background:rgba(255,255,255,0.06)!important;
+ cursor:default;border-color:transparent;box-shadow:none;}
+#navControls .nc-right{grid-column:3;grid-row:2;}
+#navControls .nc-sw{grid-column:1;grid-row:3;}
+#navControls .nc-down{grid-column:2;grid-row:3;}
+#navControls .nc-se{grid-column:3;grid-row:3;}
+
/* ===== 登录闸门 ===== */
#authSplash{position:fixed;inset:0;z-index:9999;display:flex;align-items:center;justify-content:center;
background:linear-gradient(160deg,#080c1a,#0e1730 48%,#15213f);color:#9aa6c4;
@@ -400,6 +423,7 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
@@ -770,10 +794,10 @@ const TIME_KEYFRAMES = [
// 四个时间节点的代表色(与 TIME_KEYFRAMES 顺序一致:清晨/正午/黄昏/黑夜)
// 用于时钟背景与表盘外环着色,让玩家一眼看出当前处于哪个时间节点
const NODE_RGB = [
- [255, 158, 94], // 清晨:暖橙
- [90, 176, 255], // 正午:天蓝
- [255, 122, 77], // 黄昏:红橙
- [58, 79, 138], // 黑夜:深蓝
+ [255, 179, 0], // 清晨:琥珀金 #FFB300
+ [0, 229, 255], // 正午:亮青 #00E5FF
+ [255, 61, 0], // 黄昏:烈焰红 #FF3D00
+ [61, 61, 74], // 黑夜:深灰 #3D3D4A
];
// ============ 游戏日历(纪 / 年 / 季节) ============
@@ -1141,7 +1165,7 @@ const BOTTOM_BAR_H = 72; // 主沙盘底部功能栏高度(与 #bottomBar CS
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let W, H;
-let camera = { x: 0, y: 0, zoom: 1.6 };
+let camera = { x: 0, y: 0, zoom: 1.17 }; // 主世界默认缩放;原 1.6 偏近,按三档滚轮缩小(×0.9³≈0.729)降到 1.17,进入时沙盘更开阔
let hoveredTile = null;
let selectedTile = null;
@@ -1157,6 +1181,11 @@ let _camIntT = null; // 缩放交互的 debounce 定时器
let _cachedSorted = null; // 按 currentLayer 缓存的绘制顺序地块(交互期复用,静止帧每帧重建)
let _cachedSortedLayer = null;
let shadowFade = 1; // 阴影投影强度淡入淡出:交互中→0(关),松手/停滚→1(渐入),避免明暗硬跳变
+let worldCache = null, worldCacheCtx = null; // 交互期离屏世界快照:静止帧把世界烘焙进此画布,拖动/缩放时仅 blit(O(1)),纹理不丢、不掉帧
+let worldCacheValid = false;
+const _cachedCam = { x: 0, y: 0, zoom: 1 }; // 快照对应的相机状态,交互期按相机增量平移/缩放 blit
+let releaseBlend = 0; // 松手过渡淡出:0=不混合,>0 时在静止帧上方叠快照(globalAlpha 衰减),平滑 blit→全量的亚像素跳
+const MIN_ZOOM = 0.7, MAX_ZOOM = 7; // 缩放下限抬高(别太小)+ 上限 +3 单位(原 0.4 / 4)
let tiles = [];
@@ -1404,31 +1433,8 @@ function drawHexPrism(cx, cyTop, topR, wallH, topColor, hover, tile) {
hover ? 'rgba(120,210,255,0.55)' : null, hover ? 1.5 : 0);
return;
}
- // 交互期轻量路径:跳过 paintWallTexture(6面贴图)/paintHexTexture(顶面)/getTileFast(邻居)/径向渐变,
- // 用纯色兜底最大化每帧吞吐;camInteracting 由松手 200ms 后自动置 false,下帧恢复完整绘制
- if (camInteracting && tile) {
- const top = prismTopPoints(cx, cyTop, topR);
- ctx.save();
- // 6 面合并为一个 path,纯色 fill
- ctx.fillStyle = shadeColor(base, Math.max(-20, dim * 0.7 - 6));
- ctx.beginPath();
- ctx.moveTo(top[0].x, top[0].y);
- for (let i = 0; i < SIDES; i++) {
- const a = top[i], b = top[(i + 1) % SIDES];
- ctx.lineTo(b.x, b.y);
- ctx.lineTo(b.x, b.y + wallH);
- }
- ctx.closePath();
- ctx.fill();
- // 顶面覆盖(单色,比 createRadialGradient 轻得多)
- ctx.fillStyle = shadeColor(base, Math.max(0, dim * 0.5) + 3);
- ctx.beginPath();
- for (let i = 0; i < SIDES; i++) ctx.lineTo(top[i].x, top[i].y);
- ctx.closePath();
- ctx.fill();
- ctx.restore();
- return;
- }
+ // 注:交互期不再走"纯色兜底"分支——改由 render() 把世界烘焙进离屏快照,拖动/缩放只 blit 快照,
+ // 纹理全程可见且不掉帧(见 render() 的交互快速路径)。完整绘制始终走下方路径。
const sun = currentPalette.sun;
const top = prismTopPoints(cx, cyTop, topR);
// 高度阴影:整块投下柔和投影,强化起伏层次(夜/低光下投影略重,但不超 0.30)
@@ -1504,22 +1510,58 @@ function drawHexPrism(cx, cyTop, topR, wallH, topColor, hover, tile) {
}
// ============ 渲染 ============
+// 把当前主画布(已全细节渲染的世界)烘焙进离屏快照,供交互期 blit(O(1) 单图绘制,纹理不丢、不掉帧)
+function snapshotWorld() {
+ if (!worldCache) { worldCache = document.createElement('canvas'); worldCacheCtx = worldCache.getContext('2d'); }
+ if (worldCache.width !== W || worldCache.height !== H) { worldCache.width = W; worldCache.height = H; }
+ worldCacheCtx.setTransform(1, 0, 0, 1, 0, 0);
+ worldCacheCtx.clearRect(0, 0, W, H);
+ worldCacheCtx.drawImage(canvas, 0, 0);
+ _cachedCam.x = camera.x; _cachedCam.y = camera.y; _cachedCam.zoom = camera.zoom;
+}
function render() {
if (microViewOpen) { requestAnimationFrame(render); return; } // 微观视图打开时主世界暂停重绘,把 CPU 让给微观渲染循环
- const interacting = camInteracting; // 拖动/缩放进行中:关阴影+复用地块缓存,松手后自动恢复
- // 阴影投影强度平滑过渡:交互中朝 0 收敛(关),松手/停滚后朝 1 渐入(~12 帧到位),消除松开瞬间的明暗硬跳变
- const targetShadow = interacting ? 0 : 1;
+ const interacting = camInteracting; // 拖动/缩放进行中:复用世界快照,松手后自动恢复
+ // 阴影投影常驻 1:交互期走快照 blit(不再每帧重绘带模糊卷积),固定 1,松手帧与拖动帧像素级一致。
+ const targetShadow = 1;
shadowFade += (targetShadow - shadowFade) * 0.14;
if (shadowFade < 0.003) shadowFade = 0;
+
+ // ---- 交互快速路径:只 blit 快照,跳过时间/UI/调色板/全量重绘 ----
+ // 交互期 timeOfDay 已冻结,syncTimeUI/refreshWorldVideo/getSky 的结果不变,跑了纯空转;
+ // 鼠标快速移动时每帧只做一次 drawImage → 跟手不卡。
+ if (interacting) {
+ if (!worldCacheValid) { snapshotWorld(); worldCacheValid = true; }
+ const sc = camera.zoom / (_cachedCam.zoom || 1);
+ const tx = (W / 2 + camera.x) - sc * (W / 2 + _cachedCam.x);
+ const ty = (H / 2 + camera.y) - sc * (H / 2 + _cachedCam.y);
+ // 拖动期保持画布透明(与静止帧完全一致),底层 CSS 场景图无缝透出、固定不跳;
+ // 非场景图层无 CSS 底图,补调色板渐变兜底(用上一次 currentPalette,时间冻结所以不变)。
+ if (useImageBg) {
+ ctx.clearRect(0, 0, W, H);
+ } else {
+ const _pal = currentPalette;
+ const _g = ctx.createLinearGradient(0, 0, 0, H);
+ _g.addColorStop(0, _pal.top); _g.addColorStop(0.55, _pal.mid); _g.addColorStop(1, _pal.bot);
+ ctx.fillStyle = _g; ctx.fillRect(0, 0, W, H);
+ }
+ ctx.drawImage(worldCache, 0, 0, W, H, tx, ty, W * sc, H * sc);
+ requestAnimationFrame(render);
+ return;
+ }
+
+ // ---- 静止帧:时间推进 + UI 刷新 + 调色板 + 全量重绘 ----
animTime += 0.016;
if (timePlaying) {
const prevT = timeOfDay;
- timeOfDay = nowHours(); // 每帧同步系统真实时间:时钟=本地真实时刻,与墙钟 100% 一致
+ timeOfDay = nowHours(); // 静止帧同步系统真实时间
if (timeOfDay < prevT) gameDay++; // 跨过午夜,游戏天数 +1(驱动日历/季节)
}
- syncTimeUI(); // 每帧刷新时钟(即便暂停也刷);秒针由真实时间驱动持续扫动,体现时间在走
+ syncTimeUI(); // 每帧刷新时钟(即便暂停也刷);秒针由真实时间驱动持续扫动
refreshWorldVideo(); // 时间节点变化则切换背景视频(其余帧直接跳过)
currentPalette = getSky(currentLayer, timeOfDay);
+ worldCacheValid = false; // 静止帧:下次交互重新烘焙快照(世界可能已变化)
+
ctx.clearRect(0, 0, W, H);
drawBackground(currentLayer);
// 时间色调叠加:仅大地/天空随昼夜变色调(星空/地下/地心不染时间色),不切换视频本身
@@ -1616,6 +1658,20 @@ function render() {
}
}
+ // 松手过渡淡出:在上方叠放拖动的快照(同 camera 变换 + 衰减 alpha),平滑 blit→全量 的亚像素/插值差异,
+ // 2-3 帧消隐,过渡无跳。仅场景图层(useImageBg=透明画布)做过渡,非场景图层直接切。
+ if (releaseBlend > 0.003) {
+ ctx.save();
+ ctx.globalAlpha = Math.min(releaseBlend, 1);
+ const sc = camera.zoom / (_cachedCam.zoom || 1);
+ const tx = (W / 2 + camera.x) - sc * (W / 2 + _cachedCam.x);
+ const ty = (H / 2 + camera.y) - sc * (H / 2 + _cachedCam.y);
+ ctx.drawImage(worldCache, 0, 0, W, H, tx, ty, W * sc, H * sc);
+ ctx.restore();
+ releaseBlend *= 0.4; // 快速衰减:1→0.4→0.16→0.06→0.03(4 帧 ~67ms,基本不可感知)
+ if (releaseBlend < 0.004) releaseBlend = 0;
+ }
+
requestAnimationFrame(render);
}
@@ -1873,7 +1929,7 @@ function startMicroRender() {
microSwipe = null; // 回弹到位,清除预览
}
}
- if (microSwipe) microCtx.translate(microSwipe.x - microSwipe.x0, microSwipe.y - microSwipe.y0);
+ if (microSwipe) microCtx.translate(Math.round(microSwipe.x - microSwipe.x0), Math.round(microSwipe.y - microSwipe.y0));
}
// 背景 + 地形场为静态缓存;逐帧只贴图(+ 滑动切换过渡,带方向性平移)
@@ -1885,8 +1941,8 @@ function startMicroRender() {
// 旧图:从松手预览偏移继续朝滑动方向滑出(消除"弹回原位再滑"的跳变)
if (tr.from) {
microCtx.save();
- microCtx.globalAlpha = 1 - e * 0.35;
- microCtx.translate(tr.sx * (1 - e) + tr.ux * e * D, tr.sy * (1 - e) + tr.uy * e * D);
+ microCtx.globalAlpha = 1 - e; // 旧图平滑淡出到 0,过渡收尾与下一帧纯新图无缝衔接,消除两侧阴影闪烁
+ microCtx.translate(Math.round(tr.sx * (1 - e) + tr.ux * e * D), Math.round(tr.sy * (1 - e) + tr.uy * e * D));
microCtx.drawImage(tr.from, 0, 0);
microCtx.restore();
}
@@ -1894,7 +1950,7 @@ function startMicroRender() {
if (microGridCache) {
microCtx.save();
microCtx.globalAlpha = Math.min(1, e * 1.6);
- microCtx.translate(-tr.sx * (1 - e) + tr.ux * (e - 1) * D, -tr.sy * (1 - e) + tr.uy * (e - 1) * D);
+ microCtx.translate(Math.round(-tr.sx * (1 - e) + tr.ux * (e - 1) * D), Math.round(-tr.sy * (1 - e) + tr.uy * (e - 1) * D));
microCtx.drawImage(microGridCache, 0, 0);
microCtx.restore();
}
@@ -2050,10 +2106,20 @@ function onMicroMove(e) {
}
if (!microSwipe || !microSwipe.active) return;
const p = getMicroPoint(e);
- // 边界约束:跟手最大 ±320px(足以判断切地块方向),防止画面被拖到浮岛外
+ // 跟手最大 ±320px(足以判断切地块方向),防止画面被拖太远
const SWIPE_MAX = 320;
- microSwipe.x = Math.max(microSwipe.x0 - SWIPE_MAX, Math.min(microSwipe.x0 + SWIPE_MAX, p.x));
- microSwipe.y = Math.max(microSwipe.y0 - SWIPE_MAX, Math.min(microSwipe.y0 + SWIPE_MAX, p.y));
+ let ox = Math.max(-SWIPE_MAX, Math.min(SWIPE_MAX, p.x - microSwipe.x0));
+ let oy = Math.max(-SWIPE_MAX, Math.min(SWIPE_MAX, p.y - microSwipe.y0));
+ // 边缘即墙:当前拖动方向没有相邻地块 → 橡皮筋阻尼,岛不真正移动(不循环/不越界)
+ const t = microState.tile;
+ const [dq, dr] = MICRO_DIRS[microHexDirOfVector(ox, oy)];
+ if (!getTile(t.q + dq, t.r + dr)) {
+ const RUBBER = 0.18, RUBBER_MAX = 46;
+ ox = Math.max(-RUBBER_MAX, Math.min(RUBBER_MAX, ox * RUBBER));
+ oy = Math.max(-RUBBER_MAX, Math.min(RUBBER_MAX, oy * RUBBER));
+ }
+ microSwipe.x = microSwipe.x0 + ox;
+ microSwipe.y = microSwipe.y0 + oy;
}
function onMicroUp(e) {
if (!microState) return;
@@ -2067,32 +2133,43 @@ function onMicroUp(e) {
// 左键:判断是切地块还是点击
if (!microSwipe) return;
const dx = microSwipe.x - microSwipe.x0, dy = microSwipe.y - microSwipe.y0;
- const active = microSwipe.active; microSwipe.active = false;
+ microSwipe.active = false;
microCanvas.style.cursor = '';
- if (!active) return;
if (Math.hypot(dx, dy) < MICRO_SWIPE_MIN) {
- // 未达阈值:保留 microSwipe(active=false),由 loop 弹性回弹,避免生硬瞬回
+ // 未达阈值:保留 microSwipe,由 loop 弹性回弹,避免生硬瞬回
return;
}
- microSwipe = null; // 超阈值走 transition,起点偏移已存入 tr.sx/sy,预览可清
- navigateMicro(dx, dy);
+ // 超阈值:尝试切相邻地块;若边缘即墙(无相邻地块)navigateMicro 返回 false,
+ // 仍保留 microSwipe 由 loop 弹性回弹——避免"瞬回原位"造成的闪烁/循环感
+ const moved = navigateMicro(dx, dy);
+ if (moved) microSwipe = null; // 已切换,预览可清(起点偏移已存入 tr.sx/sy)
}
-// 选滑动方向夹角最接近的相邻六边形地块(空间一致,像在沙盘里平移显微镜)
+// 六边形六个相邻方向的轴向偏移(axial),与 hexToPixel 的线性映射一致
+const MICRO_DIRS = [[1, 0], [1, -1], [0, -1], [-1, 0], [-1, 1], [0, 1]];
+// 由拖动向量求"最匹配"的六边形方向角标(只看方向,去掉缩放/居中偏移)
+function microHexDirOfVector(dx, dy) {
+ const dirs = MICRO_DIRS.map(([dq, dr]) => ({
+ dq, dr,
+ vx: Math.sqrt(3) * dq + Math.sqrt(3) / 2 * dr, // 与 hexToPixel 线性部分一致
+ vy: 3 / 2 * dr
+ }));
+ const sl = Math.hypot(dx, dy) || 1;
+ let bestI = 0, bestDot = -Infinity;
+ for (let i = 0; i < dirs.length; i++) {
+ const d = dirs[i], vl = Math.hypot(d.vx, d.vy) || 1;
+ const dot = (dx * d.vx + dy * d.vy) / sl / vl; // 余弦相似度
+ if (dot > bestDot) { bestDot = dot; bestI = i; }
+ }
+ return bestI;
+}
+// 朝某方向拖动 → 若该方向有相邻地块则切入它(像平移显微镜);否则不切换(边缘即墙)
function navigateMicro(dx, dy) {
const t = microState.tile;
- const o = hexToPixel(t.q, t.r);
- const DIRS = [[1, 0], [1, -1], [0, -1], [-1, 0], [-1, 1], [0, 1]];
- let best = null, bestDot = -Infinity;
- const sl = Math.hypot(dx, dy) || 1;
- for (const [dq, dr] of DIRS) {
- const nb = getTile(t.q + dq, t.r + dr);
- if (!nb) continue;
- const p = hexToPixel(t.q + dq, t.r + dr);
- const vx = p.x - o.x, vy = p.y - o.y, vl = Math.hypot(vx, vy) || 1;
- const dot = (dx * vx + dy * vy) / sl / vl; // 余弦相似度
- if (dot > bestDot) { bestDot = dot; best = nb; }
- }
- if (best) transitionMicroTo(best, dx, dy);
+ const di = microHexDirOfVector(dx, dy);
+ const [dq, dr] = MICRO_DIRS[di];
+ const nb = getTile(t.q + dq, t.r + dr);
+ if (nb) { transitionMicroTo(nb, dx, dy); return true; }
+ return false; // 边缘即墙:不切换,由 onMicroUp 保留 microSwipe 让 loop 弹性回弹
}
// 切换当前微观地块(更新状态 + 侧栏 + 标题)
function setMicroTile(tile) {
@@ -2545,22 +2622,15 @@ function shadeColor(color, percent) {
// ============ 事件(修复版)============
canvas.addEventListener('wheel', e => {
- // 建造 · 捏高度工具:滚轮微调悬停格,不触发相机缩放
+ // 建造 · 捏高度工具:滚轮微调悬停格
if (buildMode && buildTool === 'height' && currentLayer === 'earth' && hoveredTile) {
e.preventDefault();
- applyHeight(hoveredTile, e.deltaY < 0 ? 1 : -1, true); // 滚轮离散事件:跳过笔触去重守卫,可连续升降到 0~12 全程
+ applyHeight(hoveredTile, e.deltaY < 0 ? 1 : -1, true);
return;
}
e.preventDefault();
const zoomDelta = e.deltaY > 0 ? 0.9 : 1.1;
- camera.zoom = Math.max(0.4, Math.min(4, camera.zoom * zoomDelta));
- // 缩放交互标志:连续滚轮期间保持真(关阴影+缓存复用),最后一次滚动 200ms 后复位
- camInteracting = true;
- clearTimeout(_camIntT);
- _camIntT = setTimeout(() => { camInteracting = false; }, 200);
- scheduleSettingsSave(); // 相机缩放后防抖存后端
- // 更新光标状态
- canvas.classList.add('canDrag');
+ camera.zoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, camera.zoom * zoomDelta));
}, { passive: false });
canvas.addEventListener('contextmenu', e => e.preventDefault());
@@ -2576,16 +2646,10 @@ canvas.addEventListener('mousedown', e => {
const hex = pixelToHex(e.clientX - rect.left, e.clientY - rect.top);
const t = getTile(hex.q, hex.r);
if (t) applyHeight(t, heightBrush);
- canvas.classList.add('dragging');
return;
}
if (e.button !== 0) return;
isMouseDown = true;
- hasDragged = false;
- mouseDownX = e.clientX;
- mouseDownY = e.clientY;
- dragCamStartX = camera.x;
- dragCamStartY = camera.y;
// 建造 · 移动工具:按住即进入"搬运交换",不平移相机
if (buildMode && buildTool === 'move' && currentLayer === 'earth') {
const rect = canvas.getBoundingClientRect();
@@ -2593,7 +2657,6 @@ canvas.addEventListener('mousedown', e => {
dragSwapFrom = getTile(hex.q, hex.r);
draggingSwap = !!dragSwapFrom;
}
- canvas.classList.add('dragging');
});
canvas.addEventListener('mousemove', e => {
@@ -2610,23 +2673,11 @@ canvas.addEventListener('mousemove', e => {
if (editingHeight) {
const t = getTile(hex.q, hex.r);
if (t) applyHeight(t, heightBrush);
- return; // 捏高度笔触中:只刷高度,不平移相机
+ return; // 捏高度笔触中:只刷高度
}
if (draggingSwap) {
- const rect = canvas.getBoundingClientRect();
- const hx = pixelToHex(e.clientX - rect.left, e.clientY - rect.top);
- hoveredTile = getTile(hx.q, hx.r);
- return; // 搬运中:只更新目标高亮,不平移相机
- }
- const dx = e.clientX - mouseDownX;
- const dy = e.clientY - mouseDownY;
- if (Math.abs(dx) > 4 || Math.abs(dy) > 4) {
- hasDragged = true;
- camInteracting = true; // 平移拖动开始:触发关阴影+缓存复用,提升拖动流畅度
- }
- if (hasDragged) {
- camera.x = dragCamStartX + (e.clientX - mouseDownX);
- camera.y = dragCamStartY + (e.clientY - mouseDownY);
+ hoveredTile = getTile(hex.q, hex.r);
+ return; // 搬运中:只更新目标高亮
}
} else {
// 只有没按下鼠标时才检测悬停
@@ -2644,9 +2695,6 @@ canvas.addEventListener('mousemove', e => {
canvas.addEventListener('mouseup', e => {
if (!isMouseDown) return;
isMouseDown = false;
- camInteracting = false; // 松手:恢复阴影投影与每帧重建(静止帧细节回到位)
- if (hasDragged) scheduleSettingsSave(); // 平移过则防抖存相机位
- canvas.classList.remove('dragging');
// 建造模式 · 移动工具:拖拽/点击交换板块
if (draggingSwap) {
@@ -2907,7 +2955,7 @@ function applyWorldSettings(s) {
const c = s.camera;
if (typeof c.x === 'number') camera.x = c.x;
if (typeof c.y === 'number') camera.y = c.y;
- if (typeof c.zoom === 'number') camera.zoom = c.zoom;
+ if (typeof c.zoom === 'number') camera.zoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, c.zoom));
}
function scheduleSettingsSave() {
if (settingsTimer) clearTimeout(settingsTimer);
@@ -3030,7 +3078,7 @@ function syncTimeUI() {
const ni = currentNodeIndex(timeOfDay);
const col = NODE_RGB[ni];
const cw = document.getElementById('clockWidget');
- if (cw) cw.style.background = `rgba(${col[0]},${col[1]},${col[2]},.42)`;
+ if (cw) cw.style.background = `rgba(14,20,36,.85)`; // 统一深底,不额外着色;四段弧的颜色已足够区分阶段
// 当前阶段的彩色弧高亮(不透明度 .55 → 1)、其他段压暗,其它 stage 名变淡
const segIds = ['segDawn', 'segDay', 'segDusk', 'segNight'];
for (let i = 0; i < 4; i++) {
@@ -3165,23 +3213,24 @@ function init() {
ln.setAttribute('stroke-width', isMajor ? 2.0 : 1.2);
ticks.appendChild(ln);
}
- // 时钟外环:4 段时间彩色弧(按 TIME_KEYFRAMES 的小时边界分割 24h 圆周)
+ // 时钟外环:4 段填充扇形(铺满整圆,从圆心到外弧的 pie wedge)
(function buildClockRing() {
const segs = TIME_KEYFRAMES; // [{h:5},{h:12},{h:18},{h:21}]
const ids = ['segDawn', 'segDay', 'segDusk', 'segNight'];
- const R = 34; // 弧半径(与 stroke-width=18 配合,弧再向内扩+加粗,更内敛醒目)
+ const R = 44; // 外半径(铺满整个表盘区域)
const cx = 50, cy = 50;
- const pt = (h) => { // 小时 → 圆周坐标(0h 在 12 点钟,24h 顺时针回到顶部)
+ const pt = (h) => { // 小时 → 圆周坐标(0h 在 12 点钟)
const rad = (h / 24) * Math.PI * 2 - Math.PI / 2;
return { x: cx + R * Math.cos(rad), y: cy + R * Math.sin(rad) };
};
for (let i = 0; i < 4; i++) {
- const h0 = segs[i].h; // 段起(含首段从 0 起、末段绕回 24)
+ const h0 = segs[i].h;
const h1 = (i === 3) ? 24 : segs[i + 1].h;
const p0 = pt(h0), p1 = pt(h1);
const span = h1 - h0;
- const large = span > 12 ? 1 : 0; // 跨 >180° 用大弧
- const d = `M ${p0.x} ${p0.y} A ${R} ${R} 0 ${large} 1 ${p1.x} ${p1.y}`;
+ const large = span > 12 ? 1 : 0;
+ // 扇形路径:圆心 → 弧起点 → 弧(顺时针) → 弧终点 → 回圆心闭合
+ const d = `M ${cx} ${cy} L ${p0.x} ${p0.y} A ${R} ${R} 0 ${large} 1 ${p1.x} ${p1.y} Z`;
const el = document.getElementById(ids[i]);
if (el) el.setAttribute('d', d);
}
@@ -3253,12 +3302,43 @@ function init() {
window.addEventListener('resize', () => {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight - BOTTOM_BAR_H;
+ worldCacheValid = false; // 画布尺寸变化,交互快照失效,下次交互重新烘焙
});
loadSavedRatio(); // 应用上次保存的地表比例
generateMap();
// 注意:地表改动不再走 localStorage,统一由登录闸门拉取后端存档 applyWorldState 覆盖
init();
+
+
+
+
+
+
+
+
+
+
+
+
+
+