tower babel 1.5
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 (stripTop−S): 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);
|
||||
|
||||
@@ -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
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user