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 <cursoragent@cursor.com>
This commit is contained in:
@@ -263,6 +263,18 @@
|
||||
<label>รูปแผนที่ <input type="file" id="bg-image" accept="image/*"></label>
|
||||
<button type="button" id="btn-clear-bg">ลบรูปพื้นหลัง</button>
|
||||
</div>
|
||||
<div id="editor-stack-tower-cam-block" class="editor-bg-scroll-block" hidden>
|
||||
<p class="legend" style="margin:0.5rem 0 0.35rem;line-height:1.45"><strong>กล้องตามความสูงหอ</strong> (เฉพาะฉากรหัส <code>mnn93hpi</code> · Stack Tower) — เลื่อนมุมมองขึ้นเมื่อสแต็กสูงขึ้น · <em>English:</em> Camera pans up as blocks stack; saved in map JSON.</p>
|
||||
<div class="editor-card__row editor-card__row--wrap" style="align-items:center;gap:0.75rem 1rem">
|
||||
<label class="editor-bg-scroll-check"><input type="checkbox" id="editor-stack-tower-cam-enabled" checked> เปิดตามชั้น / Follow stack height</label>
|
||||
<label title="พิกเซลโลกต่อ 1 ชั้นที่สแต็กแล้ว — ยิ่งมากยิ่งเลื่อนเร็ว">px ต่อชั้น
|
||||
<input type="number" id="editor-stack-tower-cam-px-layer" min="0" max="80" step="1" value="12" style="width:4.2rem">
|
||||
</label>
|
||||
<label title="จำกัดการเลื่อนสูงสุด (พิกเซลโลก)">เลื่อนสูงสุด (px)
|
||||
<input type="number" id="editor-stack-tower-cam-max-px" min="0" max="800" step="4" value="260" style="width:4.5rem">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="editor-bg-scroll-block" class="editor-bg-scroll-block" hidden>
|
||||
<p class="legend" style="margin:0.5rem 0 0.35rem;line-height:1.45"><strong>เลื่อนพื้นหลังแนวตั้ง</strong> (เฉพาะฉากรหัส <code>mnpz6rkp</code>) — รูป intro ต่อด้วย loop · บันทึกฉากแล้วหน้าเล่นใช้ค่าเดียวกัน · <em>English:</em> Intro + looping strip; saved in map JSON.</p>
|
||||
<div class="editor-card__row editor-card__row--wrap" style="align-items:center;gap:0.75rem 1rem">
|
||||
@@ -307,7 +319,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<script src="js/version.js?v=0.0169"></script>
|
||||
<script src="js/editor.js?v=20260502-towerblock-ftp-hint"></script>
|
||||
<script src="js/editor.js?v=20260427-stack-tower-cam"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -2009,7 +2009,7 @@
|
||||
</div>
|
||||
<script src="/Game/socket.io/socket.io.js"></script>
|
||||
<script src="js/version.js?v=0.0184"></script>
|
||||
<script src="js/play.js?v=0.258"></script>
|
||||
<script src="js/play.js?v=0.266"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user