From b005d5f56e43ddb33487c0734ed59dd9e87bc405 Mon Sep 17 00:00:00 2001 From: giteaadmin Date: Sun, 3 May 2026 04:37:08 +0000 Subject: [PATCH] Game: stack tower camera follow (mnn93hpi) + editor controls Persist stackTowerCameraFollow on map JSON; pan camera up with stack height in play.js. Editor UI only for map id mnn93hpi + gameType stack. Bump play/editor script cache versions. Co-authored-by: Cursor --- www/html/Game/public/editor.html | 14 +++++++- www/html/Game/public/js/editor.js | 48 ++++++++++++++++++++++++++ www/html/Game/public/js/play.js | 57 ++++++++++++++++++++++++++----- www/html/Game/public/play.html | 2 +- 4 files changed, 110 insertions(+), 11 deletions(-) diff --git a/www/html/Game/public/editor.html b/www/html/Game/public/editor.html index aec4988..2537aa6 100644 --- a/www/html/Game/public/editor.html +++ b/www/html/Game/public/editor.html @@ -263,6 +263,18 @@ + - +
v —
diff --git a/www/html/Game/public/js/editor.js b/www/html/Game/public/js/editor.js index d8d3eda..f638252 100644 --- a/www/html/Game/public/js/editor.js +++ b/www/html/Game/public/js/editor.js @@ -236,6 +236,45 @@ if (dir) dir.value = editorBgScrollConfig.scrollDirection === 'down' ? 'down' : 'up'; } + /** กล้องตามความสูงหอ — เฉพาะแมป Tower mission mnn93hpi (Editor + map JSON) */ + const EDITOR_STACK_TOWER_CAM_MAP_ID = 'mnn93hpi'; + + function isEditorStackTowerMissionMap() { + const gt = gameTypeEl ? gameTypeEl.value : gameType; + return mapId === EDITOR_STACK_TOWER_CAM_MAP_ID && gt === 'stack'; + } + + function syncStackTowerCamBlockVisibilityOnly() { + const block = document.getElementById('editor-stack-tower-cam-block'); + if (!block) return; + block.hidden = !isEditorStackTowerMissionMap(); + } + + function applyStackTowerCameraFollowFromMap(m) { + const block = document.getElementById('editor-stack-tower-cam-block'); + if (block) block.hidden = !isEditorStackTowerMissionMap(); + if (!isEditorStackTowerMissionMap()) return; + const raw = m && m.stackTowerCameraFollow && typeof m.stackTowerCameraFollow === 'object' ? m.stackTowerCameraFollow : {}; + const en = document.getElementById('editor-stack-tower-cam-enabled'); + const pl = document.getElementById('editor-stack-tower-cam-px-layer'); + const mx = document.getElementById('editor-stack-tower-cam-max-px'); + if (en) en.checked = raw.enabled !== false; + const ppl = Math.max(0, Math.min(80, Math.floor(Number(raw.pxPerLayer)) || 12)); + const mpx = Math.max(0, Math.min(800, Math.floor(Number(raw.maxPx)) || 260)); + if (pl) pl.value = String(ppl); + if (mx) mx.value = String(mpx); + } + + function readStackTowerCameraFollowForSave() { + if (!isEditorStackTowerMissionMap()) return undefined; + const en = document.getElementById('editor-stack-tower-cam-enabled'); + const pl = document.getElementById('editor-stack-tower-cam-px-layer'); + const mx = document.getElementById('editor-stack-tower-cam-max-px'); + const pxPerLayer = Math.max(0, Math.min(80, Math.floor(Number(pl && pl.value)) || 12)); + const maxPx = Math.max(0, Math.min(800, Math.floor(Number(mx && mx.value)) || 260)); + return { enabled: !!(en && en.checked), pxPerLayer, maxPx }; + } + function readEditorBgScrollConfigForSave() { if (!isEditorScrollBgMap()) return null; const en = document.getElementById('editor-bg-scroll-enabled'); @@ -1069,6 +1108,7 @@ } function toggleFroggerUI() { + syncStackTowerCamBlockVisibilityOnly(); const froggerWrap = document.getElementById('frogger-lanes-wrap'); const hint = document.getElementById('game-type-hint'); const legEl = document.getElementById('frogger-lanes-legend'); @@ -2347,6 +2387,11 @@ } })(); if (isEditorScrollBgMap()) applyEditorBgScrollFromMap({}); + if (isEditorStackTowerMissionMap()) applyStackTowerCameraFollowFromMap({}); + else { + const stBlock0 = document.getElementById('editor-stack-tower-cam-block'); + if (stBlock0) stBlock0.hidden = true; + } document.getElementById('btn-new').addEventListener('click', initGrid); document.getElementById('btn-spawn').addEventListener('click', () => { @@ -2568,6 +2613,8 @@ if (backgroundImage) body.backgroundImage = backgroundImage; const ebsSave = readEditorBgScrollConfigForSave(); if (ebsSave) body.editorBgScroll = ebsSave; + const stcSave = readStackTowerCameraFollowForSave(); + if (stcSave !== undefined) body.stackTowerCameraFollow = stcSave; body.lanes = gameType === 'frogger' ? lanes : []; sanitizeGauntletPlayerSpawns(); body.gauntletPlayerSpawns = gameType === 'gauntlet' @@ -2882,6 +2929,7 @@ } gameType = m.gameType || 'zep'; if (gameTypeEl) gameTypeEl.value = gameType; + applyStackTowerCameraFollowFromMap(m); lanes = (m.lanes && m.lanes.length) ? m.lanes.map(l => ({ ...l })) : []; if (gameType === 'gauntlet' || gameType === 'stack' || gameType === 'quiz_carry' || gameType === 'quiz_battle' || gameType === 'jump_survive' || gameType === 'space_shooter' || gameType === 'balloon_boss') lanes = []; if (gameType === 'frogger') { diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js index 73cdb97..a636ffe 100644 --- a/www/html/Game/public/js/play.js +++ b/www/html/Game/public/js/play.js @@ -6409,9 +6409,22 @@ }; } + function getStackTowerCameraFollowPlayConfig() { + const raw = mapData && mapData.stackTowerCameraFollow && typeof mapData.stackTowerCameraFollow === 'object' + ? mapData.stackTowerCameraFollow + : {}; + if (raw.enabled === false) { + return { enabled: false, pxPerLayer: 12, maxPx: 260 }; + } + const pxPerLayer = Math.max(0, Math.min(80, Math.floor(Number(raw.pxPerLayer)) || 12)); + const maxPx = Math.max(0, Math.min(800, Math.floor(Number(raw.maxPx)) || 260)); + return { enabled: raw.enabled !== false, pxPerLayer, maxPx }; + } + function getStackCameraCentersPx() { const w = mapData.width || 20, h = mapData.height || 15; const ts = tileSize; + const mapHpx = h * ts; const land = getStackAreaBoundsPlay(mapData.stackLandArea, w, h); const rel = getStackAreaBoundsPlay(mapData.stackReleaseArea, w, h); let cx = (w / 2) * ts; @@ -6426,6 +6439,20 @@ cx = rel.cx * ts; cy = rel.cy * ts; } + if (isStackTowerMissionUiMapPlay() && stackMini) { + const cfg = getStackTowerCameraFollowPlayConfig(); + if (cfg.enabled && cfg.pxPerLayer > 0) { + const n = stackMini.layers ? stackMini.layers.length : 0; + let layerCount = n; + if (stackFall) { + const dur = Math.max(1, stackFall.dur || 400); + const u = Math.min(1, Math.max(0, (performance.now() - stackFall.t0) / dur)); + layerCount = n + u * 0.9; + } + const shift = Math.min(cfg.maxPx, layerCount * cfg.pxPerLayer); + cy = Math.max(ts * 1.5, Math.min(mapHpx - ts * 2, cy - shift)); + } + } return { px: cx, py: cy }; } @@ -6710,6 +6737,9 @@ : 0.16 * offN * (hit.perfect ? 0.1 : 1); const isHumanPreview = !!(previewActor && previewActor.human); const botIdPreview = previewActor && previewActor.botId ? previewActor.botId : null; + const cableTopYRel = Math.max(0, m.swingWorldY - tileSize * 6); + const craneXRel = m.craneWorldX != null ? m.craneWorldX : m.topCenterX * tileSize; + const releaseRopeAng = Math.atan2(m.swingWorldY - cableTopYRel, swingXW - craneXRel); stackFall = { t0: performance.now(), dur, @@ -6723,6 +6753,9 @@ sloppyN: offN, previewHumanDrop: isHumanPreview, previewBotId: botIdPreview, + releaseRopeAng, + releaseAttachWorldX: swingXW, + releaseAttachWorldY: m.swingWorldY, }; return true; } @@ -6964,21 +6997,27 @@ const cxMid = blockLeftWorld + twWorld / 2; const [tcx, tcy] = worldToScreen(craneX, cableTopY); const [bcx, bcy] = worldToScreen(cxMid, blockTopWorld); - drawStackSlingSegmentPlay(ctx, tcx, tcy, bcx, bcy, zoom); - const slingAng = Math.atan2(bcy - tcy, bcx - tcx); - const swingTiltRad = Math.PI / 2 - slingAng; + let slingBx = bcx; + let slingBy = bcy; + if (stackFall && stackFall.releaseAttachWorldX != null) { + const p = worldToScreen(stackFall.releaseAttachWorldX, stackFall.releaseAttachWorldY); + slingBx = p[0]; + slingBy = p[1]; + } + drawStackSlingSegmentPlay(ctx, tcx, tcy, slingBx, slingBy, zoom); + const ropeAng = stackFall && stackFall.releaseRopeAng != null + ? stackFall.releaseRopeAng + : Math.atan2(bcy - tcy, bcx - tcx); + const swingTiltRad = ropeAng - Math.PI / 2; const hitMissTint = stackFall && stackFall.hit && stackFall.hit.miss; ctx.fillStyle = hitMissTint ? 'rgba(247, 118, 190, 0.88)' : 'rgba(125, 207, 255, 0.95)'; ctx.strokeStyle = hitMissTint ? 'rgba(255, 180, 210, 0.98)' : 'rgba(192, 202, 245, 0.98)'; - const cxW = blockLeftWorld + twWorld / 2; - const cyW = blockTopWorld + stripH * 0.5; - const [cxs, cys] = worldToScreen(cxW, cyW); ctx.save(); - ctx.translate(cxs, cys); + ctx.translate(bcx, bcy); ctx.rotate(fallTiltRad + swingTiltRad); - ctx.fillRect(-twWorld * zoom / 2, -drawStripPx / 2, twWorld * zoom, drawStripPx); + ctx.fillRect(-twWorld * zoom / 2, 0, twWorld * zoom, drawStripPx); ctx.lineWidth = 2; - ctx.strokeRect(-twWorld * zoom / 2, -drawStripPx / 2, twWorld * zoom, drawStripPx); + ctx.strokeRect(-twWorld * zoom / 2, 0, twWorld * zoom, drawStripPx); ctx.lineWidth = 1; ctx.restore(); } diff --git a/www/html/Game/public/play.html b/www/html/Game/public/play.html index 70057f7..7dfa18b 100644 --- a/www/html/Game/public/play.html +++ b/www/html/Game/public/play.html @@ -2009,7 +2009,7 @@ - +
v —