tower babel 1.5

This commit is contained in:
2026-05-07 15:39:55 +00:00
parent 0f600e3b9b
commit c826d5a216
6 changed files with 548 additions and 20 deletions
File diff suppressed because one or more lines are too long
+27 -1
View File
@@ -270,6 +270,32 @@
<label>รูปแผนที่ <input type="file" id="bg-image" accept="image/*"></label>
<button type="button" id="btn-clear-bg">ลบรูปพื้นหลัง</button>
</div>
<div id="editor-stack-tower-vertical-bg-block" class="editor-bg-scroll-block" hidden>
<p class="legend" style="margin:0.45rem 0 0.35rem;line-height:1.45"><strong>พื้นหลังแนวตั้ง (Stack Tower)</strong> ฉาก <code>mnn93hpi</code><strong>รูปแรก</strong> = ช่วงเริ่ม (intro) · <strong>รูปที่สอง</strong> = ไทล์วนลูป (loop) · เปิดแล้วใช้แทนรูปแผนที่เดียวในเกม · ความเร็ว 0 = ไม่เลื่อนอัตโนมัติ — ตั้ง <strong>ทุกกี่ชั้น</strong> แล้วเลื่อนแถบขึ้นทีละขั้น (แอนิเมชัน) · <em>English:</em> Speed 0 = no auto-scroll; optional step scroll every N placed blocks.</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-vbg-enabled"> ใช้ intro + loop แนวตั้ง / Use vertical strip BG</label>
<label title="0 = นิ่ง (ไม่เลื่อนอัตโนมัติ)">ความเร็ว px/s <input type="number" id="editor-stack-tower-vbg-speed" min="0" max="400" step="4" value="0" style="width:5rem"></label>
<label>ทิศทาง
<select id="editor-stack-tower-vbg-direction">
<option value="down" selected>บน → ล่าง</option>
<option value="up">ล่าง → บน</option>
</select>
</label>
</div>
<div class="editor-card__row editor-card__row--wrap" style="align-items:center;gap:0.75rem 1rem;margin-top:0.35rem">
<label title="0 = ปิดการเลื่อนเป็นขั้น">เลื่อนทุก N ชั้น / Step every N layers <input type="number" id="editor-stack-tower-vbg-step-layers" min="0" max="200" step="1" value="0" style="width:4rem"></label>
<label title="0 = ใช้ความสูงไทล์ loop บนจอ">ขั้น px (0=สูง loop) / Step px <input type="number" id="editor-stack-tower-vbg-step-px" min="0" max="8000" step="8" value="0" style="width:4.5rem"></label>
<label>ระยะเวลาแอนิเมชัน ms <input type="number" id="editor-stack-tower-vbg-step-anim-ms" min="120" max="4000" step="20" value="520" style="width:4.5rem"></label>
</div>
<div class="editor-card__row editor-card__row--wrap" style="align-items:center;gap:0.75rem 1rem;margin-top:0.2rem">
<label title="เว้นว่าง = ใช้จุดปล่อยจากโซนวาดบนแผนที่ · ตัวเลข = ระยะห่างจุดยึดเชือกเหนือขอบบนของบล็อกบนสุด (world px)">ช่องว่างปล่อยเหนือบล็อกบนสุด (world px, ว่าง=ค่าแมป) <input type="number" id="editor-stack-tower-vbg-release-gap" min="0" max="800" step="2" value="" placeholder="—" style="width:4.2rem" title="Release rope gap above top block"></label>
</div>
<div class="editor-card__row editor-card__row--wrap" style="margin-top:0.35rem">
<label>รูปเริ่ม (intro) <input type="file" id="editor-stack-tower-vbg-intro-file" accept="image/*"></label>
<label>รูปลูป (loop) <input type="file" id="editor-stack-tower-vbg-loop-file" accept="image/*"></label>
<button type="button" id="editor-stack-tower-vbg-reset-assets">คืนรูปเริ่มจากเซิร์ฟ / Reset to bundled</button>
</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">
@@ -372,7 +398,7 @@
</div>
</div>
<script src="js/version.js?v=0.0258"></script>
<script src="js/editor.js?v=0.0331"></script>
<script src="js/editor.js?v=0.0333"></script>
<div class="version-tag">v —</div>
</body>
</html>
+380 -10
View File
@@ -248,25 +248,290 @@
if (dir) dir.value = editorBgScrollConfig.scrollDirection === 'down' ? 'down' : 'up';
}
/** แมป Stack Tower ภารกิจ (mnn93hpi) — ใช้จับ HUD embed; ไม่มี UI เลื่อนกล้อง/แถบ BG ในเอดิเตอร์แล้ว · บันทึกฉากบังคับปิด scroll/cam ใน JSON */
/** Stack Tower ภารกิจ (mnn93hpi) — HUD embed + พื้นหลังแนวตั้ง intro/loop */
const EDITOR_STACK_TOWER_CAM_MAP_ID = 'mnn93hpi';
const EDITOR_STACK_TOWER_BG_DEFAULT_INTRO = BASE + '/img/editor-bg-mnn93hpi/intro.png';
const EDITOR_STACK_TOWER_BG_DEFAULT_LOOP = BASE + '/img/editor-bg-mnn93hpi/loop.png';
let stackTowerBgScrollConfig = {
enabled: false,
speedPxPerSec: 0,
scrollDirection: 'down',
introImage: null,
loopImage: null,
stepEveryLayers: 0,
stepScrollPx: 0,
stepAnimMs: 520,
releaseGapWorldPx: null,
};
let stackTowerBgScrollIntroImg = null;
let stackTowerBgScrollLoopImg = null;
let stackTowerBgScrollPx = 0;
function isEditorStackTowerMissionMap() {
const gt = gameTypeEl ? gameTypeEl.value : gameType;
return mapId === EDITOR_STACK_TOWER_CAM_MAP_ID && gt === 'stack';
}
function syncEditorStackTowerVerticalBgBlockVisibility() {
const block = document.getElementById('editor-stack-tower-vertical-bg-block');
if (!block) return;
block.hidden = !isEditorStackTowerMissionMap();
}
function syncStackTowerCamBlockVisibilityOnly() {
/* บล็อก UI ถูกลบแล้ว */
syncEditorStackTowerVerticalBgBlockVisibility();
if (!isEditorStackTowerMissionMap()) stopStackTowerBgScrollRaf();
}
function stopStackTowerBgScrollRaf() {
/* no-op — เก็บชื่อให้จุดเรียกเดิมไม่พัง */
/* เคยใช้ RAF — เก็บชื่อให้จุดเรียกเดิมไม่พัง */
}
function applyStackTowerCameraFollowFromMap(_m) {
function isEditorStackTowerBgDrawing() {
if (!isEditorStackTowerMissionMap() || !stackTowerBgScrollConfig.enabled) return false;
const a = stackTowerBgScrollIntroImg;
const b = stackTowerBgScrollLoopImg;
return !!(a && a.complete && a.naturalWidth && b && b.complete && b.naturalWidth);
}
function loadStackTowerBgScrollImagePair(introSrc, loopSrc, onDone) {
stopStackTowerBgScrollRaf();
if (!isEditorStackTowerMissionMap()) return;
stackTowerBgScrollIntroImg = null;
stackTowerBgScrollLoopImg = null;
let pending = 2;
const doneOne = () => {
pending--;
if (pending <= 0 && typeof onDone === 'function') onDone();
};
const im1 = new Image();
im1.onload = () => {
stackTowerBgScrollIntroImg = im1;
doneOne();
};
im1.onerror = () => { doneOne(); };
im1.src = introSrc;
const im2 = new Image();
im2.onload = () => {
stackTowerBgScrollLoopImg = im2;
doneOne();
};
im2.onerror = () => { doneOne(); };
im2.src = loopSrc;
}
function syncStackTowerBgScrollUiFromConfig() {
const en = document.getElementById('editor-stack-tower-vbg-enabled');
const sp = document.getElementById('editor-stack-tower-vbg-speed');
const dir = document.getElementById('editor-stack-tower-vbg-direction');
const stL = document.getElementById('editor-stack-tower-vbg-step-layers');
const stPx = document.getElementById('editor-stack-tower-vbg-step-px');
const stMs = document.getElementById('editor-stack-tower-vbg-step-anim-ms');
const relG = document.getElementById('editor-stack-tower-vbg-release-gap');
if (en) en.checked = !!stackTowerBgScrollConfig.enabled;
if (sp) sp.value = String(stackTowerBgScrollConfig.speedPxPerSec != null ? stackTowerBgScrollConfig.speedPxPerSec : 0);
if (dir) dir.value = stackTowerBgScrollConfig.scrollDirection === 'down' ? 'down' : 'up';
if (stL) stL.value = String(stackTowerBgScrollConfig.stepEveryLayers != null ? stackTowerBgScrollConfig.stepEveryLayers : 0);
if (stPx) stPx.value = String(stackTowerBgScrollConfig.stepScrollPx != null ? stackTowerBgScrollConfig.stepScrollPx : 0);
if (stMs) stMs.value = String(stackTowerBgScrollConfig.stepAnimMs != null ? stackTowerBgScrollConfig.stepAnimMs : 520);
if (relG) {
const g = stackTowerBgScrollConfig.releaseGapWorldPx;
relG.value = g != null && Number.isFinite(Number(g)) ? String(Math.floor(Number(g))) : '';
}
}
function syncStackTowerBgScrollInitialToBottom() {
const intro = stackTowerBgScrollIntroImg;
if (!intro || !intro.complete || !intro.naturalWidth) return;
const cwR = Math.max(1, Math.round(canvas.width));
const chR = Math.max(1, Math.round(canvas.height));
const drawHIntro = Math.round(intro.naturalHeight * (cwR / intro.naturalWidth));
const rawDownLike = stackTowerBgScrollConfig.scrollDirection === 'down';
const flowDown = !rawDownLike;
if (flowDown) {
stackTowerBgScrollPx = 0;
} else {
stackTowerBgScrollPx = Math.max(0, drawHIntro - chR);
}
}
function foldStackTowerBgScrollSViewEditor(S, drawHIntro, drawHLoop) {
const dH = Math.max(1, Math.round(Number(drawHLoop) || 1));
const dI = Math.max(0, Math.round(Number(drawHIntro) || 0));
const s = Number(S) || 0;
if (s < dI) return s;
return s - Math.floor((s - dI) / dH) * dH;
}
function drawStackTowerScrollingBackground(cw, ch) {
const intro = stackTowerBgScrollIntroImg;
const loop = stackTowerBgScrollLoopImg;
if (!intro || !intro.complete || !loop || !loop.complete) return;
const cwR = Math.max(1, Math.round(cw));
const chR = Math.max(1, Math.round(ch));
const scaleI = cwR / intro.naturalWidth;
const drawHIntro = Math.round(intro.naturalHeight * scaleI);
const scaleL = cwR / loop.naturalWidth;
const drawHLoop = Math.max(1, Math.round(loop.naturalHeight * scaleL));
const S = foldStackTowerBgScrollSViewEditor(stackTowerBgScrollPx, drawHIntro, drawHLoop);
const vp0 = S;
const vp1 = S + chR;
const yLimit = vp1 + drawHLoop * 96;
const rawDownLike = stackTowerBgScrollConfig.scrollDirection === 'down';
const flowDown = !rawDownLike;
function stripTopToDestY(stripTop, drawH) {
if (!flowDown) return stripTop - S;
return S + chR - (stripTop + drawH);
}
ctx.save();
ctx.imageSmoothingEnabled = false;
ctx.beginPath();
ctx.rect(0, 0, cwR, chR);
ctx.clip();
if (vp0 < 0) {
let k = 1;
if (vp0 < -drawHLoop * 4) {
k = Math.max(1, Math.floor(-vp0 / drawHLoop) - 2);
}
for (; k < 50000; k++) {
const y0 = -k * drawHLoop;
if (y0 >= vp1 + drawHLoop) break;
if (y0 + drawHLoop <= vp0) continue;
const dy = Math.round(stripTopToDestY(y0, drawHLoop));
ctx.drawImage(loop, 0, 0, loop.naturalWidth, loop.naturalHeight, 0, dy, cwR, drawHLoop);
}
}
if (drawHIntro > vp0 && vp1 > 0) {
const dy = Math.round(stripTopToDestY(0, drawHIntro));
ctx.drawImage(intro, 0, 0, intro.naturalWidth, intro.naturalHeight, 0, dy, cwR, drawHIntro);
}
let y = drawHIntro;
if (y < yLimit && vp1 > drawHIntro) {
if (vp0 > drawHIntro) {
const n = Math.floor((vp0 - drawHIntro) / drawHLoop);
y = drawHIntro + n * drawHLoop;
}
while (y < yLimit) {
const dy = Math.round(stripTopToDestY(y, drawHLoop));
ctx.drawImage(loop, 0, 0, loop.naturalWidth, loop.naturalHeight, 0, dy, cwR, drawHLoop);
y += drawHLoop;
}
}
ctx.restore();
}
function applyStackTowerBgScrollFromMap(m) {
stopStackTowerBgScrollRaf();
stackTowerBgScrollIntroImg = null;
stackTowerBgScrollLoopImg = null;
stackTowerBgScrollPx = 0;
if (!isEditorStackTowerMissionMap()) {
stackTowerBgScrollConfig = {
enabled: false,
speedPxPerSec: 0,
scrollDirection: 'down',
introImage: null,
loopImage: null,
stepEveryLayers: 0,
stepScrollPx: 0,
stepAnimMs: 520,
releaseGapWorldPx: null,
};
return;
}
const raw = m && m.stackTowerBgScroll && typeof m.stackTowerBgScroll === 'object' ? m.stackTowerBgScroll : {};
const spNum = Number(raw.speedPxPerSec);
const sp = Number.isFinite(spNum) ? Math.max(0, Math.min(400, Math.floor(spNum))) : 0;
const sd = String(raw.scrollDirection || raw.direction || 'down').toLowerCase();
const sel = Math.floor(Number(raw.stepEveryLayers));
const stepEveryLayers = Number.isFinite(sel) ? Math.max(0, Math.min(200, sel)) : 0;
const ssp = Math.floor(Number(raw.stepScrollPx));
const stepScrollPx = Number.isFinite(ssp) ? Math.max(0, Math.min(8000, ssp)) : 0;
const sam = Math.floor(Number(raw.stepAnimMs));
const stepAnimMs = Number.isFinite(sam) ? Math.max(120, Math.min(4000, sam)) : 520;
const rg = Number(raw.releaseGapWorldPx);
const releaseGapWorldPx = Number.isFinite(rg) && rg >= 0 ? Math.min(800, Math.floor(rg)) : null;
stackTowerBgScrollConfig = {
enabled: raw.enabled === true,
speedPxPerSec: sp,
scrollDirection: (sd === 'down' || sd === 'top' || sd === 'toptobottom') ? 'down' : 'up',
introImage: typeof raw.introImage === 'string' && raw.introImage.length ? raw.introImage : null,
loopImage: typeof raw.loopImage === 'string' && raw.loopImage.length ? raw.loopImage : null,
stepEveryLayers,
stepScrollPx,
stepAnimMs,
releaseGapWorldPx,
};
const introSrc = stackTowerBgScrollConfig.introImage || EDITOR_STACK_TOWER_BG_DEFAULT_INTRO;
const loopSrc = stackTowerBgScrollConfig.loopImage || EDITOR_STACK_TOWER_BG_DEFAULT_LOOP;
loadStackTowerBgScrollImagePair(introSrc, loopSrc, () => {
syncStackTowerBgScrollUiFromConfig();
syncStackTowerBgScrollInitialToBottom();
draw();
});
}
function readStackTowerBgScrollForSave() {
if (!isEditorStackTowerMissionMap()) return undefined;
const en = document.getElementById('editor-stack-tower-vbg-enabled');
const spEl = document.getElementById('editor-stack-tower-vbg-speed');
const spNum = Number(spEl && spEl.value);
const sp = Number.isFinite(spNum) ? Math.max(0, Math.min(400, Math.floor(spNum))) : 0;
const dirEl = document.getElementById('editor-stack-tower-vbg-direction');
const stLEl = document.getElementById('editor-stack-tower-vbg-step-layers');
const stPxEl = document.getElementById('editor-stack-tower-vbg-step-px');
const stMsEl = document.getElementById('editor-stack-tower-vbg-step-anim-ms');
const relEl = document.getElementById('editor-stack-tower-vbg-release-gap');
const selRaw = Number(stLEl && stLEl.value);
const stepEveryLayers = Number.isFinite(selRaw) ? Math.max(0, Math.min(200, Math.floor(selRaw))) : 0;
const sspRaw = Number(stPxEl && stPxEl.value);
const stepScrollPx = Number.isFinite(sspRaw) ? Math.max(0, Math.min(8000, Math.floor(sspRaw))) : 0;
const samRaw = Number(stMsEl && stMsEl.value);
const stepAnimMs = Number.isFinite(samRaw) ? Math.max(120, Math.min(4000, Math.floor(samRaw))) : 520;
const relStr = relEl && String(relEl.value).trim();
const relNum = relStr === '' ? null : Number(relStr);
const releaseGapWorldPx = relNum != null && Number.isFinite(relNum) && relNum >= 0
? Math.min(800, Math.floor(relNum))
: null;
const out = {
enabled: !!(en && en.checked),
speedPxPerSec: sp,
scrollDirection: dirEl && dirEl.value === 'down' ? 'down' : 'up',
stepEveryLayers,
stepScrollPx,
stepAnimMs,
};
if (releaseGapWorldPx != null) out.releaseGapWorldPx = releaseGapWorldPx;
if (stackTowerBgScrollConfig.introImage) out.introImage = stackTowerBgScrollConfig.introImage;
if (stackTowerBgScrollConfig.loopImage) out.loopImage = stackTowerBgScrollConfig.loopImage;
return out;
}
function applyStackTowerCameraFollowFromMap(m) {
stopStackTowerBgScrollRaf();
syncEditorStackTowerVerticalBgBlockVisibility();
if (!isEditorStackTowerMissionMap()) {
stackTowerBgScrollConfig = {
enabled: false,
speedPxPerSec: 0,
scrollDirection: 'down',
introImage: null,
loopImage: null,
stepEveryLayers: 0,
stepScrollPx: 0,
stepAnimMs: 520,
releaseGapWorldPx: null,
};
stackTowerBgScrollIntroImg = null;
stackTowerBgScrollLoopImg = null;
stackTowerBgScrollPx = 0;
return;
}
applyStackTowerBgScrollFromMap(m || {});
}
function readStackTowerCameraFollowForSave() {
@@ -274,11 +539,6 @@
return { enabled: false, pxPerLayer: 12, maxPx: 260 };
}
function readStackTowerBgScrollForSave() {
if (!isEditorStackTowerMissionMap()) return undefined;
return { enabled: false, speedPxPerSec: 40, scrollDirection: 'down' };
}
/** พื้นหลังแนวนอน start + วน 2–4 + finish — เฉพาะแมป Last Light mno9kb07 (Editor + map JSON: gauntletCrownRunwayBg) */
const EDITOR_GAUNTLET_RUNWAY_BG_MAP_ID = 'mno9kb07';
const EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_START = BASE + '/img/editor-bg-mno9kb07/start.png';
@@ -1996,6 +2256,8 @@
drawEditorScrollingBackground(canvas.width, canvas.height);
} else if (isEditorGauntletRunwayBgDrawing()) {
drawEditorGauntletRunwayScrollingBackground(canvas.width, canvas.height);
} else if (isEditorStackTowerBgDrawing()) {
drawStackTowerScrollingBackground(canvas.width, canvas.height);
} else if (backgroundImageImg && backgroundImageImg.complete && backgroundImageImg.naturalWidth) {
ctx.drawImage(backgroundImageImg, 0, 0, canvas.width, canvas.height);
} else {
@@ -3080,6 +3342,112 @@
if (isEditorStackTowerMissionMap()) applyStackTowerCameraFollowFromMap({});
else stopStackTowerBgScrollRaf();
(function wireStackTowerVerticalBgUi() {
const en = document.getElementById('editor-stack-tower-vbg-enabled');
const sp = document.getElementById('editor-stack-tower-vbg-speed');
if (en) {
en.addEventListener('change', () => {
stackTowerBgScrollConfig.enabled = !!en.checked;
if (stackTowerBgScrollConfig.enabled) syncStackTowerBgScrollInitialToBottom();
draw();
});
}
if (sp) {
sp.addEventListener('change', () => {
const n = Number(sp.value);
stackTowerBgScrollConfig.speedPxPerSec = Number.isFinite(n) ? Math.max(0, Math.min(400, Math.floor(n))) : 0;
});
}
const dirSel = document.getElementById('editor-stack-tower-vbg-direction');
if (dirSel) {
dirSel.addEventListener('change', () => {
stackTowerBgScrollConfig.scrollDirection = dirSel.value === 'down' ? 'down' : 'up';
syncStackTowerBgScrollInitialToBottom();
draw();
});
}
const stL = document.getElementById('editor-stack-tower-vbg-step-layers');
if (stL) {
stL.addEventListener('change', () => {
const n = Number(stL.value);
stackTowerBgScrollConfig.stepEveryLayers = Number.isFinite(n) ? Math.max(0, Math.min(200, Math.floor(n))) : 0;
});
}
const stPx = document.getElementById('editor-stack-tower-vbg-step-px');
if (stPx) {
stPx.addEventListener('change', () => {
const n = Number(stPx.value);
stackTowerBgScrollConfig.stepScrollPx = Number.isFinite(n) ? Math.max(0, Math.min(8000, Math.floor(n))) : 0;
});
}
const stMs = document.getElementById('editor-stack-tower-vbg-step-anim-ms');
if (stMs) {
stMs.addEventListener('change', () => {
const n = Number(stMs.value);
stackTowerBgScrollConfig.stepAnimMs = Number.isFinite(n) ? Math.max(120, Math.min(4000, Math.floor(n))) : 520;
});
}
const relG = document.getElementById('editor-stack-tower-vbg-release-gap');
if (relG) {
relG.addEventListener('change', () => {
const s = String(relG.value).trim();
if (s === '') stackTowerBgScrollConfig.releaseGapWorldPx = null;
else {
const n = Number(s);
stackTowerBgScrollConfig.releaseGapWorldPx = Number.isFinite(n) && n >= 0
? Math.min(800, Math.floor(n))
: null;
}
});
}
const introF = document.getElementById('editor-stack-tower-vbg-intro-file');
if (introF) {
introF.addEventListener('change', (e) => {
const file = e.target.files && e.target.files[0];
if (!file) return;
const r = new FileReader();
r.onload = () => {
stackTowerBgScrollConfig.introImage = r.result;
loadStackTowerBgScrollImagePair(stackTowerBgScrollConfig.introImage, stackTowerBgScrollConfig.loopImage || EDITOR_STACK_TOWER_BG_DEFAULT_LOOP, () => {
syncStackTowerBgScrollInitialToBottom();
draw();
});
};
r.readAsDataURL(file);
e.target.value = '';
});
}
const loopF = document.getElementById('editor-stack-tower-vbg-loop-file');
if (loopF) {
loopF.addEventListener('change', (e) => {
const file = e.target.files && e.target.files[0];
if (!file) return;
const r = new FileReader();
r.onload = () => {
stackTowerBgScrollConfig.loopImage = r.result;
loadStackTowerBgScrollImagePair(stackTowerBgScrollConfig.introImage || EDITOR_STACK_TOWER_BG_DEFAULT_INTRO, stackTowerBgScrollConfig.loopImage, () => {
syncStackTowerBgScrollInitialToBottom();
draw();
});
};
r.readAsDataURL(file);
e.target.value = '';
});
}
const btnR = document.getElementById('editor-stack-tower-vbg-reset-assets');
if (btnR) {
btnR.addEventListener('click', () => {
stackTowerBgScrollConfig.introImage = null;
stackTowerBgScrollConfig.loopImage = null;
loadStackTowerBgScrollImagePair(EDITOR_STACK_TOWER_BG_DEFAULT_INTRO, EDITOR_STACK_TOWER_BG_DEFAULT_LOOP, () => {
syncStackTowerBgScrollUiFromConfig();
syncStackTowerBgScrollInitialToBottom();
draw();
});
});
}
})();
document.getElementById('btn-new').addEventListener('click', initGrid);
document.getElementById('btn-spawn').addEventListener('click', () => {
isSpawnMode = true;
@@ -3088,6 +3456,8 @@
if (gameTypeEl) gameTypeEl.addEventListener('change', () => {
gameType = gameTypeEl.value;
syncEditorGauntletRunwayBgBlockVisibility();
syncEditorStackTowerVerticalBgBlockVisibility();
if (!isEditorStackTowerMissionMap()) applyStackTowerCameraFollowFromMap({});
if (mapId === EDITOR_GAUNTLET_RUNWAY_BG_MAP_ID) {
stopEditorGauntletRunwayBgRaf();
if (isEditorGauntletRunwayBgMap() && editorGauntletRunwayBgConfig.enabled) startEditorGauntletRunwayBgRaf();
+112 -2
View File
@@ -1012,6 +1012,11 @@
let stackTowerScrollBgFlowDown = false;
/** Sraw = px+boost+post ครั้งแรกที่คำนวณ world offset — ใช้หา delta แบบคาบ loop (อย่าใช้ fold(Sraw) ลบตรงๆ จะดันโลกขึ้นหลายร้อย px) */
let stackTowerScrollBgSrawWorldBaselinePlay = null;
/** ความสูงวาดของไทล์ loop (พิกเซลจอ) — ใช้ขั้นเลื่อนเมื่อ stepScrollPx = 0 */
let stackTowerScrollBgLoopDrawHPlay = 0;
/** { from, to, t0, dur } — เลื่อนแถบ BG เป็นขั้นตามจำนวนชั้น */
let stackTowerBgScrollStepAnimPlay = null;
let stackTowerBgScrollPendingStepDeltaPlay = 0;
function stackTowerScrollBgMapEligible() {
return !!(mapData && mapData.gameType === 'stack' && (currentPlayMapId() || '').trim() === STACK_TOWER_MISSION_MAP_ID);
@@ -1034,10 +1039,18 @@
stackTowerScrollBgLoopImg = null;
stackTowerScrollBgPx = 0;
stackTowerScrollBgSrawWorldBaselinePlay = null;
stackTowerScrollBgLoopDrawHPlay = 0;
stackTowerBgScrollStepAnimPlay = null;
stackTowerBgScrollPendingStepDeltaPlay = 0;
if (!stackTowerScrollBgMapEligible()) return;
const raw = mapData.stackTowerBgScroll && typeof mapData.stackTowerBgScroll === 'object' ? mapData.stackTowerBgScroll : {};
stackTowerScrollBgOff = raw.enabled !== true;
stackTowerScrollBgSpeedPxPerSec = Math.max(8, Math.min(400, Math.floor(Number(raw.speedPxPerSec)) || 40));
{
const spNum = Number(raw.speedPxPerSec);
stackTowerScrollBgSpeedPxPerSec = Number.isFinite(spNum)
? Math.max(0, Math.min(400, Math.floor(spNum)))
: 0;
}
/* คีย์เวิร์ด down/top = ตัวเลือก "บน→ล่าง" ในเอดิเตอร์ — ใช้สูตรเลื่อนแบบ stripTop−S (ฉากไหลขึ้น) ไม่ใช่ S+chR−… (ไหลลง) เพราะผู้เล่นคาดว่าหอสูง = มองขึ้น */
const dirRaw = String(raw.scrollDirection || raw.direction || 'down').toLowerCase();
const rawIsDownKeyword = (dirRaw === 'down' || dirRaw === 'top' || dirRaw === 'toptobottom');
@@ -1049,6 +1062,12 @@
const bump = () => {
pending--;
if (pending <= 0) {
const intro0 = stackTowerScrollBgIntroImg;
const loop0 = stackTowerScrollBgLoopImg;
if (loop0 && loop0.complete && intro0 && intro0.complete && canvas && canvas.width) {
const cwR0 = Math.max(1, Math.round(canvas.width));
stackTowerScrollBgLoopDrawHPlay = Math.max(1, Math.round(loop0.naturalHeight * (cwR0 / loop0.naturalWidth)));
}
stackTowerScrollBgSyncInitialScrollToBottom();
try { draw(); } catch (e) { /* ignore */ }
}
@@ -1197,10 +1216,97 @@
ctx.restore();
}
function tickStackTowerBgScrollStepAnimPlay() {
const q = stackTowerBgScrollStepAnimPlay;
if (!q) return;
const t = Math.min(1, (performance.now() - q.t0) / Math.max(1, q.dur));
const e = 1 - Math.pow(1 - t, 3);
stackTowerScrollBgPx = q.from + (q.to - q.from) * e;
if (t >= 1) {
stackTowerScrollBgPx = q.to;
stackTowerBgScrollStepAnimPlay = null;
stackTowerScrollBgSrawWorldBaselinePlay = null;
if (stackTowerBgScrollPendingStepDeltaPlay !== 0) {
const d = stackTowerBgScrollPendingStepDeltaPlay;
stackTowerBgScrollPendingStepDeltaPlay = 0;
startStackTowerBgScrollStepAnimPlay(d);
}
}
}
function getStackTowerBgScrollStepEveryLayersPlay() {
const raw = mapData && mapData.stackTowerBgScroll;
if (!raw || typeof raw !== 'object') return 0;
const n = Math.floor(Number(raw.stepEveryLayers));
return Number.isFinite(n) && n > 0 ? Math.min(200, n) : 0;
}
function getStackTowerBgScrollStepAnimMsPlay() {
const raw = mapData && mapData.stackTowerBgScroll;
if (!raw || typeof raw !== 'object') return 520;
const t = Math.floor(Number(raw.stepAnimMs));
return Number.isFinite(t) ? Math.max(120, Math.min(4000, t)) : 520;
}
function getStackTowerBgScrollStepScrollPxPlay() {
const raw = mapData && mapData.stackTowerBgScroll;
if (!raw || typeof raw !== 'object') return 0;
const d = Math.floor(Number(raw.stepScrollPx));
return Number.isFinite(d) ? Math.max(0, Math.min(8000, d)) : 0;
}
function startStackTowerBgScrollStepAnimPlay(deltaPx) {
if (!stackTowerScrollBgDrawActive() || !deltaPx) return;
if (stackTowerBgScrollStepAnimPlay) {
stackTowerBgScrollPendingStepDeltaPlay += deltaPx;
return;
}
const dur = getStackTowerBgScrollStepAnimMsPlay();
stackTowerBgScrollStepAnimPlay = {
from: stackTowerScrollBgPx,
to: stackTowerScrollBgPx + deltaPx,
t0: performance.now(),
dur,
};
}
function maybeQueueStackTowerBgScrollStepPlay(layerCountAfter) {
if (!stackTowerScrollBgDrawActive()) return;
const every = getStackTowerBgScrollStepEveryLayersPlay();
if (every <= 0 || layerCountAfter <= 0 || layerCountAfter % every !== 0) return;
let delta = getStackTowerBgScrollStepScrollPxPlay();
if (!delta) delta = Math.max(8, stackTowerScrollBgLoopDrawHPlay || 200);
delta = Math.min(8000, Math.max(8, delta));
startStackTowerBgScrollStepAnimPlay(delta);
}
function getStackTowerStripReleaseGapWorldPxPlay() {
const raw = mapData && mapData.stackTowerBgScroll;
if (!isStackTowerMissionUiMapPlay() || !raw || typeof raw !== 'object') return null;
const g = Number(raw.releaseGapWorldPx);
if (!Number.isFinite(g) || g < 0) return null;
return Math.min(800, g);
}
function updateStackTowerSwingYForStripGapPlay() {
if (!stackMini || !isStackTowerMissionUiMapPlay()) return;
const gap = getStackTowerStripReleaseGapWorldPxPlay();
if (gap == null) return;
const m = stackMini;
const lh = m.layerWorldH || Math.max(14, tileSize * 0.3);
const nLay = m.layers ? m.layers.length : 0;
const topBlockTopY = m.floorWorldY - nLay * lh;
const swingLift = getStackTowerSwingLiftWorldPx(nLay, lh);
m.swingWorldY = topBlockTopY - gap + swingLift;
}
function tickStackTowerScrollBg(dtSec) {
if (!stackTowerScrollBgMapEligible() || stackTowerScrollBgOff) return;
if (!stackTowerScrollBgImagesReady()) return;
const spd = (stackTowerScrollBgSpeedPxPerSec || 40) * dtSec;
tickStackTowerBgScrollStepAnimPlay();
if (stackTowerBgScrollStepAnimPlay) return;
const spd = Math.max(0, Number(stackTowerScrollBgSpeedPxPerSec) || 0) * dtSec;
if (spd <= 0) return;
/* !flowDown (stripTopS): S เพิ่ม → dy ลด → ฉากไหลขึ้น — ห้ามใช้ −= (จะลด S แล้วศิลป์ไหลลง) */
stackTowerScrollBgPx += spd;
}
@@ -8844,6 +8950,7 @@
ensureStackTowerLifeHudImagesPlay();
ensureStackTowerHeartMinusImgPlay();
}
updateStackTowerSwingYForStripGapPlay();
}
function stackEaseDrop(t) {
@@ -8902,6 +9009,7 @@
: stackMini.initialWidthTiles;
stackMini.layers.push({ w: placedW, cx: hit.placedCx, seat: seatLay, heavy: heavyLay });
const nAfter = stackMini.layers.length;
maybeQueueStackTowerBgScrollStepPlay(nAfter);
const perfectFull =
towerLive &&
hit.perfect &&
@@ -17425,12 +17533,14 @@
lastStackTowerScrollBgTickMs = nowStBg;
tickStackTowerScrollBg(dtStBg);
if (isStackTowerMissionPregameBlockingPlay()) {
updateStackTowerSwingYForStripGapPlay();
me.isWalking = false;
draw();
requestAnimationFrame(tick);
return;
}
stackTickFrame();
updateStackTowerSwingYForStripGapPlay();
me.isWalking = false;
draw();
requestAnimationFrame(tick);
+1 -1
View File
@@ -3203,7 +3203,7 @@
</div>
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0306"></script>
<script src="js/play.js?v=0.0406"></script>
<script src="js/play.js?v=0.0408"></script>
<div class="version-tag">v —</div>
</body>
</html>
+27 -5
View File
@@ -1966,14 +1966,36 @@ function normalizeJumpSurviveHazardAreaOnMap(m) {
}
/**
* แมปภารกิจ Stack Tower (`mnn93hpi`): ปิดเลื่อนแนวตั้งทั้งแถบ BG และ offset กล้องตามหอใน JSON ที่ส่งให้ไคลเอนต์
* (เดิมบังคับ stackTowerCameraFollow.enabled=true ทำให้ play.js เลื่อนโลกทุกชั้น + อาการครึ่งจอดำ — แก้ให้ปิดเสมอ)
* แมปภารกิจ Stack Tower (`mnn93hpi`): ปิดกล้องตามหอเสมอ · คลีน/จำกัดค่า stackTowerBgScroll (intro+loop แนวตั้ง) จาก JSON
*/
function applyStackTowerMissionMapPlayPolicy(safeId, m) {
if (safeId !== STACK_TOWER_MISSION_MAP_ID || !m || m.gameType !== 'stack') return;
const dirRaw = m.stackTowerBgScroll && typeof m.stackTowerBgScroll === 'object' ? m.stackTowerBgScroll.scrollDirection : 'down';
const dir = typeof dirRaw === 'string' && dirRaw.trim() ? String(dirRaw).trim() : 'down';
m.stackTowerBgScroll = { enabled: false, speedPxPerSec: 0, scrollDirection: dir };
const raw = m.stackTowerBgScroll && typeof m.stackTowerBgScroll === 'object' ? m.stackTowerBgScroll : {};
const dirRaw = String(raw.scrollDirection || raw.direction || 'down').toLowerCase();
const scrollDirection = (dirRaw === 'down' || dirRaw === 'top' || dirRaw === 'toptobottom') ? 'down' : 'up';
const spNum = Number(raw.speedPxPerSec);
const speedPxPerSec = Number.isFinite(spNum)
? Math.max(0, Math.min(400, Math.floor(spNum)))
: 0;
const sel = Math.floor(Number(raw.stepEveryLayers));
const stepEveryLayers = Number.isFinite(sel) ? Math.max(0, Math.min(200, sel)) : 0;
const ssp = Math.floor(Number(raw.stepScrollPx));
const stepScrollPx = Number.isFinite(ssp) ? Math.max(0, Math.min(8000, ssp)) : 0;
const sam = Math.floor(Number(raw.stepAnimMs));
const stepAnimMs = Number.isFinite(sam) ? Math.max(120, Math.min(4000, sam)) : 520;
const st = {
enabled: raw.enabled === true,
speedPxPerSec,
scrollDirection,
stepEveryLayers,
stepScrollPx,
stepAnimMs,
};
if (typeof raw.introImage === 'string' && raw.introImage.length) st.introImage = raw.introImage;
if (typeof raw.loopImage === 'string' && raw.loopImage.length) st.loopImage = raw.loopImage;
const rg = Number(raw.releaseGapWorldPx);
if (Number.isFinite(rg) && rg >= 0) st.releaseGapWorldPx = Math.min(800, Math.floor(rg));
m.stackTowerBgScroll = st;
const rawCf = m.stackTowerCameraFollow && typeof m.stackTowerCameraFollow === 'object' ? m.stackTowerCameraFollow : {};
const pxPerLayer = Math.max(0, Math.min(80, Math.floor(Number(rawCf.pxPerLayer)) || 12));
const maxPx = Math.max(0, Math.min(800, Math.floor(Number(rawCf.maxPx)) || 260));