main web4.1
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1942,3 +1942,8 @@ body.room-lobby--quiz-active .lobby-b-extra-row {
|
||||
.quiz-q-remove {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* Stack Tower play — กัน #game-canvas ถูกกฎอื่นเป็น contain แล้วแถบดำรอบแคนวาส */
|
||||
html.play-stack-tower-pixel-canvas #play-canvas-stage #game-canvas {
|
||||
object-fit: cover !important;
|
||||
}
|
||||
|
||||
@@ -270,34 +270,6 @@
|
||||
<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>) — เลื่อนมุมมอง<strong>โลก</strong>ขึ้นเมื่อสแต็กสูง บล็อก+พื้นหลังแผนที่ไป<strong>ด้วยกัน</strong> · แถบ intro/loop ด้านล่างยังเลื่อนอัตโนมัติแยกต่างหาก · <em>English:</em> World camera pans up with stack height (blocks move with scene); saved as <code>stackTowerCameraFollow</code>.</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 (camera)</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>
|
||||
<p class="legend" style="margin:0.65rem 0 0.35rem;line-height:1.45"><strong>พื้นหลัง intro + loop</strong> (เลื่อนแนวตั้ง เหมือนฉากยิงยาน) — รูปแรกแล้วต่อลูป · <em>English:</em> Vertical strip: intro then tiling loop; saved as <code>stackTowerBgScroll</code> 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-bg-enabled" checked> เปิดเลื่อนพื้นหลัง / Enable BG scroll</label>
|
||||
<label>ความเร็ว px/s <input type="number" id="editor-stack-tower-bg-speed" min="8" max="400" step="4" value="40" style="width:5rem"></label>
|
||||
<label>ทิศทาง
|
||||
<select id="editor-stack-tower-bg-direction">
|
||||
<option value="up" selected>ล่าง → บน</option>
|
||||
<option value="down">บน → ล่าง</option>
|
||||
</select>
|
||||
</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-bg-intro-file" accept="image/*"></label>
|
||||
<label>รูปลูป (loop) <input type="file" id="editor-stack-tower-bg-loop-file" accept="image/*"></label>
|
||||
<button type="button" id="editor-stack-tower-bg-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">
|
||||
@@ -400,7 +372,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<script src="js/version.js?v=0.0258"></script>
|
||||
<script src="js/editor.js?v=0.0321"></script>
|
||||
<script src="js/editor.js?v=0.0331"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -248,7 +248,7 @@
|
||||
if (dir) dir.value = editorBgScrollConfig.scrollDirection === 'down' ? 'down' : 'up';
|
||||
}
|
||||
|
||||
/** กล้องตามความสูงหอ — เฉพาะแมป Tower mission mnn93hpi (Editor + map JSON) */
|
||||
/** แมป Stack Tower ภารกิจ (mnn93hpi) — ใช้จับ HUD embed; ไม่มี UI เลื่อนกล้อง/แถบ BG ในเอดิเตอร์แล้ว · บันทึกฉากบังคับปิด scroll/cam ใน JSON */
|
||||
const EDITOR_STACK_TOWER_CAM_MAP_ID = 'mnn93hpi';
|
||||
|
||||
function isEditorStackTowerMissionMap() {
|
||||
@@ -257,244 +257,26 @@
|
||||
}
|
||||
|
||||
function syncStackTowerCamBlockVisibilityOnly() {
|
||||
const block = document.getElementById('editor-stack-tower-cam-block');
|
||||
if (!block) return;
|
||||
const show = isEditorStackTowerMissionMap();
|
||||
if (!show) stopStackTowerBgScrollRaf();
|
||||
block.hidden = !show;
|
||||
/* บล็อก UI ถูกลบแล้ว */
|
||||
}
|
||||
|
||||
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: true,
|
||||
speedPxPerSec: 40,
|
||||
scrollDirection: 'up',
|
||||
introImage: null,
|
||||
loopImage: null,
|
||||
};
|
||||
let stackTowerBgScrollIntroImg = null;
|
||||
let stackTowerBgScrollLoopImg = null;
|
||||
let stackTowerBgScrollPx = 0;
|
||||
let stackTowerBgScrollRaf = null;
|
||||
let stackTowerBgScrollLastTs = 0;
|
||||
|
||||
function stopStackTowerBgScrollRaf() {
|
||||
if (stackTowerBgScrollRaf != null) {
|
||||
cancelAnimationFrame(stackTowerBgScrollRaf);
|
||||
stackTowerBgScrollRaf = null;
|
||||
}
|
||||
stackTowerBgScrollLastTs = 0;
|
||||
/* no-op — เก็บชื่อให้จุดเรียกเดิมไม่พัง */
|
||||
}
|
||||
|
||||
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) {
|
||||
function applyStackTowerCameraFollowFromMap(_m) {
|
||||
stopStackTowerBgScrollRaf();
|
||||
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-bg-enabled');
|
||||
const sp = document.getElementById('editor-stack-tower-bg-speed');
|
||||
const dir = document.getElementById('editor-stack-tower-bg-direction');
|
||||
if (en) en.checked = !!stackTowerBgScrollConfig.enabled;
|
||||
if (sp) sp.value = String(stackTowerBgScrollConfig.speedPxPerSec || 40);
|
||||
if (dir) dir.value = stackTowerBgScrollConfig.scrollDirection === 'down' ? 'down' : 'up';
|
||||
}
|
||||
|
||||
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));
|
||||
if (stackTowerBgScrollConfig.scrollDirection === 'down') {
|
||||
stackTowerBgScrollPx = 0;
|
||||
} else {
|
||||
stackTowerBgScrollPx = Math.max(0, drawHIntro - chR);
|
||||
}
|
||||
}
|
||||
|
||||
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 = stackTowerBgScrollPx;
|
||||
const vp0 = S;
|
||||
const vp1 = S + chR;
|
||||
const yLimit = vp1 + drawHLoop * 2;
|
||||
const flowDown = stackTowerBgScrollConfig.scrollDirection === 'down';
|
||||
|
||||
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 + Math.max(0, n - 1) * 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 stackTowerBgScrollTick(ts) {
|
||||
stackTowerBgScrollRaf = null;
|
||||
if (!isEditorStackTowerBgDrawing()) return;
|
||||
if (!stackTowerBgScrollLastTs) stackTowerBgScrollLastTs = ts;
|
||||
const dt = Math.min(64, Math.max(0, ts - stackTowerBgScrollLastTs));
|
||||
stackTowerBgScrollLastTs = ts;
|
||||
stackTowerBgScrollPx += (stackTowerBgScrollConfig.speedPxPerSec || 40) * (dt / 1000);
|
||||
draw();
|
||||
stackTowerBgScrollRaf = requestAnimationFrame(stackTowerBgScrollTick);
|
||||
}
|
||||
|
||||
function startStackTowerBgScrollRaf() {
|
||||
if (!isEditorStackTowerBgDrawing()) return;
|
||||
stopStackTowerBgScrollRaf();
|
||||
stackTowerBgScrollLastTs = 0;
|
||||
stackTowerBgScrollRaf = requestAnimationFrame(stackTowerBgScrollTick);
|
||||
}
|
||||
|
||||
function applyStackTowerBgScrollFromMap(m) {
|
||||
stopStackTowerBgScrollRaf();
|
||||
stackTowerBgScrollIntroImg = null;
|
||||
stackTowerBgScrollLoopImg = null;
|
||||
stackTowerBgScrollPx = 0;
|
||||
if (!isEditorStackTowerMissionMap()) {
|
||||
stackTowerBgScrollConfig = {
|
||||
enabled: true,
|
||||
speedPxPerSec: 40,
|
||||
scrollDirection: 'up',
|
||||
introImage: null,
|
||||
loopImage: null,
|
||||
};
|
||||
return;
|
||||
}
|
||||
const raw = m && m.stackTowerBgScroll && typeof m.stackTowerBgScroll === 'object' ? m.stackTowerBgScroll : {};
|
||||
const sp = Math.max(8, Math.min(400, Math.floor(Number(raw.speedPxPerSec)) || 40));
|
||||
const sd = String(raw.scrollDirection || raw.direction || 'up').toLowerCase();
|
||||
stackTowerBgScrollConfig = {
|
||||
enabled: raw.enabled !== false,
|
||||
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,
|
||||
};
|
||||
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();
|
||||
if (stackTowerBgScrollConfig.enabled) startStackTowerBgScrollRaf();
|
||||
});
|
||||
}
|
||||
|
||||
function readStackTowerBgScrollForSave() {
|
||||
if (!isEditorStackTowerMissionMap()) return undefined;
|
||||
const en = document.getElementById('editor-stack-tower-bg-enabled');
|
||||
const spEl = document.getElementById('editor-stack-tower-bg-speed');
|
||||
const sp = Math.max(8, Math.min(400, Math.floor(Number(spEl && spEl.value)) || 40));
|
||||
const dirEl = document.getElementById('editor-stack-tower-bg-direction');
|
||||
const out = {
|
||||
enabled: !!(en && en.checked),
|
||||
speedPxPerSec: sp,
|
||||
scrollDirection: dirEl && dirEl.value === 'down' ? 'down' : 'up',
|
||||
};
|
||||
if (stackTowerBgScrollConfig.introImage) out.introImage = stackTowerBgScrollConfig.introImage;
|
||||
if (stackTowerBgScrollConfig.loopImage) out.loopImage = stackTowerBgScrollConfig.loopImage;
|
||||
return out;
|
||||
}
|
||||
|
||||
function applyStackTowerCameraFollowFromMap(m) {
|
||||
const block = document.getElementById('editor-stack-tower-cam-block');
|
||||
if (block) block.hidden = !isEditorStackTowerMissionMap();
|
||||
applyStackTowerBgScrollFromMap(m);
|
||||
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 };
|
||||
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) */
|
||||
@@ -2214,8 +1996,6 @@
|
||||
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 {
|
||||
@@ -3298,87 +3078,7 @@
|
||||
if (isEditorScrollBgMap()) applyEditorBgScrollFromMap({});
|
||||
if (isEditorGauntletRunwayBgMap()) applyGauntletCrownRunwayBgFromMap({});
|
||||
if (isEditorStackTowerMissionMap()) applyStackTowerCameraFollowFromMap({});
|
||||
else {
|
||||
stopStackTowerBgScrollRaf();
|
||||
const stBlock0 = document.getElementById('editor-stack-tower-cam-block');
|
||||
if (stBlock0) stBlock0.hidden = true;
|
||||
}
|
||||
|
||||
(function wireStackTowerBgScrollUi() {
|
||||
const en = document.getElementById('editor-stack-tower-bg-enabled');
|
||||
const sp = document.getElementById('editor-stack-tower-bg-speed');
|
||||
if (en) {
|
||||
en.addEventListener('change', () => {
|
||||
stackTowerBgScrollConfig.enabled = !!en.checked;
|
||||
if (stackTowerBgScrollConfig.enabled) startStackTowerBgScrollRaf();
|
||||
else stopStackTowerBgScrollRaf();
|
||||
draw();
|
||||
});
|
||||
}
|
||||
if (sp) {
|
||||
sp.addEventListener('change', () => {
|
||||
stackTowerBgScrollConfig.speedPxPerSec = Math.max(8, Math.min(400, Math.floor(Number(sp.value)) || 40));
|
||||
});
|
||||
}
|
||||
const dirSel = document.getElementById('editor-stack-tower-bg-direction');
|
||||
if (dirSel) {
|
||||
dirSel.addEventListener('change', () => {
|
||||
stackTowerBgScrollConfig.scrollDirection = dirSel.value === 'down' ? 'down' : 'up';
|
||||
syncStackTowerBgScrollInitialToBottom();
|
||||
draw();
|
||||
if (stackTowerBgScrollConfig.enabled) startStackTowerBgScrollRaf();
|
||||
});
|
||||
}
|
||||
const introF = document.getElementById('editor-stack-tower-bg-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();
|
||||
if (stackTowerBgScrollConfig.enabled) startStackTowerBgScrollRaf();
|
||||
});
|
||||
};
|
||||
r.readAsDataURL(file);
|
||||
e.target.value = '';
|
||||
});
|
||||
}
|
||||
const loopF = document.getElementById('editor-stack-tower-bg-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();
|
||||
if (stackTowerBgScrollConfig.enabled) startStackTowerBgScrollRaf();
|
||||
});
|
||||
};
|
||||
r.readAsDataURL(file);
|
||||
e.target.value = '';
|
||||
});
|
||||
}
|
||||
const btnR = document.getElementById('editor-stack-tower-bg-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();
|
||||
if (stackTowerBgScrollConfig.enabled) startStackTowerBgScrollRaf();
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
else stopStackTowerBgScrollRaf();
|
||||
|
||||
document.getElementById('btn-new').addEventListener('click', initGrid);
|
||||
document.getElementById('btn-spawn').addEventListener('click', () => {
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
const base = Number.isFinite(b) ? Math.max(0.85, Math.min(3.2, b)) : 2;
|
||||
const towerPost50 =
|
||||
isStackTowerMissionUiMapPlay() &&
|
||||
getStackTowerCameraFollowPlayConfig().enabled &&
|
||||
stackTowerMissionPhase === 'live' &&
|
||||
stackMini &&
|
||||
Number.isFinite(Number(stackMini.progressPct)) &&
|
||||
@@ -1006,11 +1007,11 @@
|
||||
let stackTowerScrollBgIntroImg = null;
|
||||
let stackTowerScrollBgLoopImg = null;
|
||||
let stackTowerScrollBgPx = 0;
|
||||
/** ค่า zDraw ครั้งแรกที่วาด BG — ใช้ scale พื้นหลังให้ขยายตาม zoom เหมือนฉากโลก */
|
||||
let stackTowerScrollBgZoomRefPlay = 0;
|
||||
let stackTowerScrollBgSpeedPxPerSec = 40;
|
||||
let stackTowerScrollBgOff = false;
|
||||
let stackTowerScrollBgFlowDown = false;
|
||||
/** Sraw = px+boost+post ครั้งแรกที่คำนวณ world offset — ใช้หา delta แบบคาบ loop (อย่าใช้ fold(Sraw) ลบตรงๆ จะดันโลกขึ้นหลายร้อย px) */
|
||||
let stackTowerScrollBgSrawWorldBaselinePlay = null;
|
||||
|
||||
function stackTowerScrollBgMapEligible() {
|
||||
return !!(mapData && mapData.gameType === 'stack' && (currentPlayMapId() || '').trim() === STACK_TOWER_MISSION_MAP_ID);
|
||||
@@ -1023,21 +1024,25 @@
|
||||
return !!(a && a.complete && a.naturalWidth && b && b.complete && b.naturalWidth);
|
||||
}
|
||||
|
||||
/** แถบ intro+loop เต็มจอ — เปิดเฉพาะเมื่อแมปตั้ง <code>stackTowerBgScroll.enabled: true</code> และรูปโหลดครบ (ไม่โหลด strip เมื่อปิด → ใช้รูปแผนที่ backgroundImage) */
|
||||
function stackTowerScrollBgDrawActive() {
|
||||
return stackTowerScrollBgImagesReady();
|
||||
return stackTowerScrollBgImagesReady() && !stackTowerScrollBgOff;
|
||||
}
|
||||
|
||||
function reloadStackTowerScrollBgFromMap() {
|
||||
stackTowerScrollBgIntroImg = null;
|
||||
stackTowerScrollBgLoopImg = null;
|
||||
stackTowerScrollBgPx = 0;
|
||||
stackTowerScrollBgZoomRefPlay = 0;
|
||||
stackTowerScrollBgSrawWorldBaselinePlay = null;
|
||||
if (!stackTowerScrollBgMapEligible()) return;
|
||||
const raw = mapData.stackTowerBgScroll && typeof mapData.stackTowerBgScroll === 'object' ? mapData.stackTowerBgScroll : {};
|
||||
stackTowerScrollBgOff = raw.enabled === false;
|
||||
stackTowerScrollBgOff = raw.enabled !== true;
|
||||
stackTowerScrollBgSpeedPxPerSec = Math.max(8, Math.min(400, Math.floor(Number(raw.speedPxPerSec)) || 40));
|
||||
const dirRaw = String(raw.scrollDirection || raw.direction || 'up').toLowerCase();
|
||||
stackTowerScrollBgFlowDown = (dirRaw === 'down' || dirRaw === 'top' || dirRaw === 'toptobottom');
|
||||
/* คีย์เวิร์ด down/top = ตัวเลือก "บน→ล่าง" ในเอดิเตอร์ — ใช้สูตรเลื่อนแบบ stripTop−S (ฉากไหลขึ้น) ไม่ใช่ S+chR−… (ไหลลง) เพราะผู้เล่นคาดว่าหอสูง = มองขึ้น */
|
||||
const dirRaw = String(raw.scrollDirection || raw.direction || 'down').toLowerCase();
|
||||
const rawIsDownKeyword = (dirRaw === 'down' || dirRaw === 'top' || dirRaw === 'toptobottom');
|
||||
stackTowerScrollBgFlowDown = !rawIsDownKeyword;
|
||||
if (stackTowerScrollBgOff) return;
|
||||
const introSrc = (typeof raw.introImage === 'string' && raw.introImage.length) ? raw.introImage : STACK_TOWER_SCROLL_BG_DEFAULT_INTRO;
|
||||
const loopSrc = (typeof raw.loopImage === 'string' && raw.loopImage.length) ? raw.loopImage : STACK_TOWER_SCROLL_BG_DEFAULT_LOOP;
|
||||
let pending = 2;
|
||||
@@ -1067,22 +1072,44 @@
|
||||
if (stackTowerScrollBgFlowDown) {
|
||||
stackTowerScrollBgPx = 0;
|
||||
} else {
|
||||
/* ขอบล่างของ intro ติดขอบล่างแคนวาสตอนเริ่ม — แม้ intro ต่ำกว่าจอก็ไม่ pin จากด้านบน */
|
||||
/* ขอบล่าง intro ชิดขอบล่างแคนวาส — คู่กับ stripTop−S */
|
||||
stackTowerScrollBgPx = drawHIntro - chR;
|
||||
}
|
||||
}
|
||||
|
||||
/** zDrawPan = zoom ตอนวาด stack — ผูกเลื่อนแถบ BG กับกล้องโลก (px จอ = boostWorld * z) */
|
||||
/**
|
||||
* พับค่าเลื่อนแนวตั้ง S (หน่วยเดียวกับ drawStackTowerScrollBgFullCanvas) เข้าช่วงหนึ่งคาบของ loop
|
||||
* เมื่อ vp0 ≥ drawHIntro — รูปบนจอซ้ำทุก drawHLoop; พับค่า S ก่อนส่งเข้า worldToScreen กัน px โตไม่จำกัดดันโลกหลุดจอ
|
||||
*/
|
||||
function foldStackTowerScrollStripViewSForPhase(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;
|
||||
}
|
||||
|
||||
/**
|
||||
* offset แนวตั้งจอให้โลกซิงก์กับ draw ที่ใช้ S = fold(Sraw) — ใช้ความต่างเฟส fold(Sraw)−fold(Sraw0)
|
||||
* เวลาเลื่อนครบคาบ loop ค่า fold วนกลับ → offset ไม่โตไม่จำกัด · ห้ามคืน fold(Sraw) ล้วนเมื่อ Sraw0 ยังอยู่ intro (baseline ไม่เข้า loop ทำให้สูตร f0+mod ไม่ทำงานและเฟสหลุด)
|
||||
*/
|
||||
function stackTowerScrollWorldTotalYFromStripPlay(Sraw, drawHIntro, drawHLoop, Sraw0) {
|
||||
if (Sraw0 == null) return 0;
|
||||
const dH = Math.max(1, Math.round(Number(drawHLoop) || 1));
|
||||
const dI = Math.max(0, Math.round(Number(drawHIntro) || 0));
|
||||
const s = Number(Sraw) || 0;
|
||||
const s0 = Number(Sraw0) || 0;
|
||||
const fs = foldStackTowerScrollStripViewSForPhase(s, dI, dH);
|
||||
const fs0 = foldStackTowerScrollStripViewSForPhase(s0, dI, dH);
|
||||
return fs - fs0;
|
||||
}
|
||||
|
||||
/** zDrawPan ส่งต่อเพื่อคูณ boost กับ z — ไม่ scale แคนวาสทั้งแผง (เคย scale รอบกลางทำให้จอบางส่วนว่างเมื่อ z ≠ zRef) */
|
||||
function drawStackTowerScrollBgFullCanvas(cw, ch, zDrawPan) {
|
||||
const intro = stackTowerScrollBgIntroImg;
|
||||
const loop = stackTowerScrollBgLoopImg;
|
||||
if (!intro || !intro.complete || !loop || !loop.complete) return;
|
||||
const zPan = Number(zDrawPan) > 0 ? zDrawPan : zoom;
|
||||
if (!(stackTowerScrollBgZoomRefPlay > 0) && zPan > 0.01 && Number.isFinite(zPan)) {
|
||||
stackTowerScrollBgZoomRefPlay = zPan;
|
||||
}
|
||||
const zRef = stackTowerScrollBgZoomRefPlay > 0 ? stackTowerScrollBgZoomRefPlay : zPan;
|
||||
const bgZoomMul = Math.max(0.28, Math.min(4.25, zPan / zRef));
|
||||
const cwR = Math.max(1, Math.round(cw));
|
||||
const chR = Math.max(1, Math.round(ch));
|
||||
const scaleI = cwR / intro.naturalWidth;
|
||||
@@ -1091,10 +1118,11 @@
|
||||
const drawHLoop = Math.max(1, Math.round(loop.naturalHeight * scaleL));
|
||||
const camFollowScreen = isStackTowerMissionUiMapPlay() ? getStackTowerBgScrollHeightBoostPx() * zPan : 0;
|
||||
const post50Scroll = isStackTowerMissionUiMapPlay() ? getStackTowerPost50BgScrollExtraPxPlay(chR) : 0;
|
||||
const S = stackTowerScrollBgPx + camFollowScreen + post50Scroll;
|
||||
const Sraw = stackTowerScrollBgPx + camFollowScreen + post50Scroll;
|
||||
const S = foldStackTowerScrollStripViewSForPhase(Sraw, drawHIntro, drawHLoop);
|
||||
const vp0 = S;
|
||||
const vp1 = S + chR;
|
||||
const yLimit = vp1 + drawHLoop * 2;
|
||||
const yLimit = vp1 + drawHLoop * 96;
|
||||
const flowDown = stackTowerScrollBgFlowDown;
|
||||
|
||||
function stripTopToDestY(stripTop, drawH) {
|
||||
@@ -1107,9 +1135,6 @@
|
||||
ctx.beginPath();
|
||||
ctx.rect(0, 0, cwR, chR);
|
||||
ctx.clip();
|
||||
ctx.translate(cwR * 0.5, chR * 0.5);
|
||||
ctx.scale(bgZoomMul, bgZoomMul);
|
||||
ctx.translate(-cwR * 0.5, -chR * 0.5);
|
||||
|
||||
if (vp0 < 0) {
|
||||
let k = 1;
|
||||
@@ -1134,7 +1159,8 @@
|
||||
if (y < yLimit && vp1 > drawHIntro) {
|
||||
if (vp0 > drawHIntro) {
|
||||
const n = Math.floor((vp0 - drawHIntro) / drawHLoop);
|
||||
y = drawHIntro + Math.max(0, n - 1) * drawHLoop;
|
||||
/* ใช้ n ไม่ใช่ n−1 — เดิมทำให้แถบ loop เริ่มสูงกว่า vp0 หนึ่งความสูง จอล่างว่างดำเมื่อ S ใหญ่ */
|
||||
y = drawHIntro + n * drawHLoop;
|
||||
}
|
||||
while (y < yLimit) {
|
||||
const dy = Math.round(stripTopToDestY(y, drawHLoop));
|
||||
@@ -1155,7 +1181,8 @@
|
||||
const scaleL = cwR / loop.naturalWidth;
|
||||
const drawHLoop = Math.max(1, Math.round(loop.naturalHeight * scaleL));
|
||||
const skyScreenH = Math.min(chR, Math.ceil(-worldMinY * zDraw) + 4);
|
||||
const rawScroll = stackTowerScrollBgPx + getStackTowerBgScrollHeightBoostPx() * zDraw
|
||||
const stripPx = stackTowerScrollBgOff ? 0 : stackTowerScrollBgPx;
|
||||
const rawScroll = stripPx + getStackTowerBgScrollHeightBoostPx() * zDraw
|
||||
+ getStackTowerPost50BgScrollExtraPxPlay(chR);
|
||||
const scroll = ((rawScroll % drawHLoop) + drawHLoop) % drawHLoop;
|
||||
let y = -scroll;
|
||||
@@ -1173,26 +1200,45 @@
|
||||
function tickStackTowerScrollBg(dtSec) {
|
||||
if (!stackTowerScrollBgMapEligible() || stackTowerScrollBgOff) return;
|
||||
if (!stackTowerScrollBgImagesReady()) return;
|
||||
stackTowerScrollBgPx += (stackTowerScrollBgSpeedPxPerSec || 40) * dtSec;
|
||||
const spd = (stackTowerScrollBgSpeedPxPerSec || 40) * dtSec;
|
||||
/* !flowDown (stripTop−S): S เพิ่ม → dy ลด → ฉากไหลขึ้น — ห้ามใช้ −= (จะลด S แล้วศิลป์ไหลลง) */
|
||||
stackTowerScrollBgPx += spd;
|
||||
}
|
||||
|
||||
/**
|
||||
* ค่าลบจาก screen Y ใน worldToScreen — ซิงก์กับ drawStackTowerScrollBgFullCanvas (auto + boost ตามความสูงชั้น + post-50)
|
||||
* ค่าลบจาก screen Y ใน worldToScreen — ซิงก์กับ drawStackTowerScrollBgFullCanvas (auto + boost + post-50)
|
||||
* แถบ BG วาด 1:1 กับแคนวาส — ห้ามใช้ fold(Sraw) เป็นค่าลบตรงๆ (จะดันโลกขึ้นหลายร้อย px เหมือนครึ่งล่างจอว่าง)
|
||||
*/
|
||||
function getStackTowerWorldLayerScrollScreenOffsetYPlay(zPan) {
|
||||
if (!isStackTowerMissionUiMapPlay() || !stackMini) return 0;
|
||||
const zPn = Number(zPan) > 0 ? zPan : zoom;
|
||||
const zRef = stackTowerScrollBgZoomRefPlay > 0 ? stackTowerScrollBgZoomRefPlay : zPn;
|
||||
const bgZm = Math.max(0.28, Math.min(4.25, zPn / zRef));
|
||||
const chPx = Math.max(1, Math.round(canvas && canvas.height ? canvas.height : 720));
|
||||
const boostScreen = getStackTowerBgScrollHeightBoostPx() * zPn;
|
||||
const post50Screen = getStackTowerPost50BgScrollExtraPxPlay(chPx);
|
||||
let autoSigned = 0;
|
||||
if (stackTowerScrollBgDrawActive() && stackTowerScrollBgImagesReady() && !stackTowerScrollBgOff) {
|
||||
autoSigned = stackTowerScrollBgFlowDown ? -stackTowerScrollBgPx : stackTowerScrollBgPx;
|
||||
const intro = stackTowerScrollBgIntroImg;
|
||||
const loop = stackTowerScrollBgLoopImg;
|
||||
let totalScreen = boostScreen + post50Screen;
|
||||
if (stackTowerScrollBgDrawActive() && intro && intro.complete && intro.naturalWidth
|
||||
&& loop && loop.complete && loop.naturalWidth && canvas && canvas.width) {
|
||||
const cwR = Math.max(1, Math.round(canvas.width));
|
||||
const drawHIntro = Math.round(intro.naturalHeight * (cwR / intro.naturalWidth));
|
||||
const drawHLoop = Math.max(1, Math.round(loop.naturalHeight * (cwR / loop.naturalWidth)));
|
||||
const SrawDraw = stackTowerScrollBgPx + boostScreen + post50Screen;
|
||||
if (stackTowerScrollBgSrawWorldBaselinePlay == null) stackTowerScrollBgSrawWorldBaselinePlay = SrawDraw;
|
||||
const S0 = stackTowerScrollBgSrawWorldBaselinePlay;
|
||||
let stripY;
|
||||
if (stackTowerScrollBgFlowDown) {
|
||||
const Sview = foldStackTowerScrollStripViewSForPhase(SrawDraw, drawHIntro, drawHLoop);
|
||||
stripY = (boostScreen + post50Screen) * 2 - Sview;
|
||||
} else {
|
||||
stripY = stackTowerScrollWorldTotalYFromStripPlay(SrawDraw, drawHIntro, drawHLoop, S0);
|
||||
}
|
||||
totalScreen = stripY;
|
||||
} else if (stackTowerScrollBgDrawActive()) {
|
||||
const px = stackTowerScrollBgFlowDown ? -stackTowerScrollBgPx : stackTowerScrollBgPx;
|
||||
totalScreen = px + boostScreen + post50Screen;
|
||||
}
|
||||
const totalScreen = autoSigned + boostScreen + post50Screen;
|
||||
return totalScreen / bgZm;
|
||||
return totalScreen;
|
||||
}
|
||||
|
||||
/** โหลดจาก mapData.gridImageLibrary — ดัชนีตรงกับ gridImageCells[y][x] */
|
||||
@@ -4228,8 +4274,12 @@
|
||||
return x * x * (3 - 2 * x);
|
||||
}
|
||||
|
||||
/** Tower live + progress ≥50%: 0→1 ระหว่าง STACK_TOWER_POST50_ANIM_MS */
|
||||
/** Tower live + progress ≥50%: 0→1 ระหว่าง STACK_TOWER_POST50_ANIM_MS — ใช้เฉพาะเมื่อเปิดกล้องตามหอ (ไม่งั้นฉากธรรมดาไม่เลื่อน/ซูมหลัง 50%) */
|
||||
function getStackTowerPost50AnimEasePlay() {
|
||||
if (!getStackTowerCameraFollowPlayConfig().enabled) {
|
||||
stackTowerPost50AnimStartMs = null;
|
||||
return 0;
|
||||
}
|
||||
if (!isStackTowerMissionUiMapPlay() || stackTowerMissionPhase !== 'live' || !stackMini) return 0;
|
||||
const p = Number(stackMini.progressPct) || 0;
|
||||
if (p < STACK_TOWER_POST50_PROGRESS_THRESH) {
|
||||
@@ -8641,21 +8691,14 @@
|
||||
};
|
||||
}
|
||||
|
||||
/** ปิดการตามความสูงหอจาก JSON เสมอ — เซิร์ฟเคยบังคับ enabled:true ใน policy ทำให้ boost เลื่อนโลก */
|
||||
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 };
|
||||
return { enabled: false, pxPerLayer: 12, maxPx: 260 };
|
||||
}
|
||||
|
||||
/** ยกจุดยึดเชือกขึ้นเมื่อหอสูง (y ลดลง = สูงขึ้นในโลก) — เฉพาะ mnn93hpi */
|
||||
/** ยกจุดยึดเชือกขึ้นเมื่อหอสูง (y ลดลง = สูงขึ้นในโลก) — เฉพาะ mnn93hpi + เปิดกล้องตามหอ */
|
||||
function getStackTowerSwingLiftWorldPx(layerCount, layerWorldHPx) {
|
||||
if (!isStackTowerMissionUiMapPlay()) return 0;
|
||||
if (!isStackTowerMissionUiMapPlay() || !getStackTowerCameraFollowPlayConfig().enabled) return 0;
|
||||
const n = Math.floor(Number(layerCount) || 0);
|
||||
if (n < STACK_TOWER_SWING_LIFT_FROM_LAYER) return 0;
|
||||
const lh = Math.max(10, Number(layerWorldHPx) || Math.max(14, (tileSize || 32) * 0.3));
|
||||
@@ -14563,8 +14606,8 @@
|
||||
if (mapData && isStackTowerMissionUiMapPlay() && canvas) {
|
||||
canvas.width = Math.max(320, STACK_TOWER_FIXED_RENDER_W);
|
||||
canvas.height = Math.max(240, STACK_TOWER_FIXED_RENDER_H);
|
||||
/* รีคำนวน BG scroll + zoom ref หลังขนาดบัฟเฟอร์คงที่ — กันรูปโหลดเร็วตอนแคนวาสยังไม่ล็อกแล้วไม่ซิงก์ใหม่ */
|
||||
stackTowerScrollBgZoomRefPlay = 0;
|
||||
/* รีคำนวน BG scroll หลังขนาดบัฟเฟอร์คงที่ — กันรูปโหลดเร็วตอนแคนวาสยังไม่ล็อกแล้วไม่ซิงก์ใหม่ */
|
||||
stackTowerScrollBgSrawWorldBaselinePlay = null;
|
||||
stackTowerScrollBgSyncInitialScrollToBottom();
|
||||
syncQuizCarryEmbedCountdownLayout();
|
||||
return;
|
||||
@@ -16387,23 +16430,7 @@
|
||||
camX = gauntletGroupCam.px;
|
||||
camY = gauntletGroupCam.py;
|
||||
}
|
||||
if (isStackTowerMissionUiMapPlay() && stackMini && canvas && zDraw > 0) {
|
||||
const nLay0 = stackMini.layers ? stackMini.layers.length : 0;
|
||||
const hasMotion = !!(stackFall || stackMini.settling);
|
||||
if (nLay0 === 0 && !hasMotion) {
|
||||
const rawFrac = mapData && mapData.stackTowerFloorScreenFrac;
|
||||
const frac = Number(rawFrac);
|
||||
/** ค่าน้อย = พื้นขึ้นบนจอ (จุดเริ่มตรงฐานศิลป์) — แมป override ได้ที่ stackTowerFloorScreenFrac */
|
||||
const screenFrac = Number.isFinite(frac)
|
||||
? Math.max(0.38, Math.min(0.82, frac))
|
||||
: 0.56;
|
||||
const floorY0 = stackMini.floorWorldY;
|
||||
const stScrollFloor = getStackTowerWorldLayerScrollScreenOffsetYPlay(zDraw);
|
||||
const curScreen = (floorY0 - camY) * zDraw + canvas.height * 0.5 - stScrollFloor;
|
||||
const desired = canvas.height * screenFrac;
|
||||
camY += (curScreen - desired) / zDraw;
|
||||
}
|
||||
}
|
||||
/* Stack Tower (mnn93hpi): ไม่เลื่อนกล้องตาม floorWorldY — ใช้ cam จาก getStackCameraCentersPx() เหมือน Stack ทั่วไป (วางบล็อกแล้วจอไม่ไหล) */
|
||||
lastPlayZDrawForInput = zDraw;
|
||||
const stackTowerWorldScrollScreenY = getStackTowerWorldLayerScrollScreenOffsetYPlay(zDraw);
|
||||
const halfW = canvas.width / (2 * zDraw);
|
||||
@@ -16457,7 +16484,8 @@
|
||||
const srcW = ((bgMaxX - bgMinX) / mapWpx) * natW;
|
||||
const srcH = ((bgMaxY - bgMinY) / mapHpx) * natH;
|
||||
const destX = (bgMinX - camX) * zDraw + canvas.width / 2;
|
||||
const destY = (bgMinY - camY) * zDraw + canvas.height / 2;
|
||||
const destY = (bgMinY - camY) * zDraw + canvas.height / 2
|
||||
- (isStackTowerMissionUiMapPlay() ? stackTowerWorldScrollScreenY : 0);
|
||||
const destW = (bgMaxX - bgMinX) * zDraw;
|
||||
const destH = (bgMaxY - bgMinY) * zDraw;
|
||||
if (srcW > 0.25 && srcH > 0.25 && destW > 0.25 && destH > 0.25) {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;800&family=Share+Tech+Mono&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/style.css?v=20260427-canvas-pixel">
|
||||
<link rel="stylesheet" href="css/style.css?v=20260506-stack-tower-cover">
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
@@ -127,7 +127,7 @@
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
/* Stack Tower (mnn93hpi): เต็มพื้นที่ stage ยังคงสัดส่วนบัฟเฟอร์ 16:9 (object-fit) */
|
||||
/* Stack Tower (mnn93hpi): แคนวาสบัฟเฟอร์ 16:9 — object-fit: cover เติม stage ไม่ให้แถบดำ (contain เคยทำขอบว่าง) */
|
||||
html.play-stack-tower-pixel-canvas #play-canvas-stage.play-canvas-stage {
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
@@ -139,7 +139,8 @@
|
||||
height: 100% !important;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
object-fit: contain;
|
||||
/* contain ทำให้บัฟเฟอร์ 16:9 ถูกย่อกลาง stage — ขอบบน/ล่างหรือซ้าย/ขวาว่างดำเหมือนจอเสีย; cover เติมเต็ม stage (อาจ crop ขอบเล็กน้อย) */
|
||||
object-fit: cover;
|
||||
object-position: center center;
|
||||
touch-action: none;
|
||||
image-rendering: pixelated;
|
||||
@@ -3202,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.0381"></script>
|
||||
<script src="js/play.js?v=0.0406"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+48
-2
@@ -1965,6 +1965,21 @@ function normalizeJumpSurviveHazardAreaOnMap(m) {
|
||||
m.jumpSurviveHazardArea = rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* แมปภารกิจ Stack Tower (`mnn93hpi`): ปิดเลื่อนแนวตั้งทั้งแถบ BG และ offset กล้องตามหอใน JSON ที่ส่งให้ไคลเอนต์
|
||||
* (เดิมบังคับ stackTowerCameraFollow.enabled=true ทำให้ play.js เลื่อนโลกทุกชั้น + อาการครึ่งจอดำ — แก้ให้ปิดเสมอ)
|
||||
*/
|
||||
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 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));
|
||||
m.stackTowerCameraFollow = { enabled: false, pxPerLayer, maxPx };
|
||||
}
|
||||
|
||||
function loadMaps() {
|
||||
try {
|
||||
if (!fs.existsSync(path.join(__dirname, 'data'))) fs.mkdirSync(path.join(__dirname, 'data'), { recursive: true });
|
||||
@@ -1983,12 +1998,38 @@ function loadMaps() {
|
||||
normalizeJumpSurviveHazardAreaOnMap(data);
|
||||
normalizeQuizBattleDomeAreaOnMap(data);
|
||||
normalizeQuizBattlePathAreaOnMap(data);
|
||||
applyStackTowerMissionMapPlayPolicy(id, data);
|
||||
maps.set(id, data);
|
||||
} catch (e) { console.error('load map', f, e.message); }
|
||||
}
|
||||
} catch (e) { console.error('loadMaps', e.message); }
|
||||
}
|
||||
|
||||
/**
|
||||
* อ่านแมปหนึ่งไฟล์จากดิสก์เข้า `maps` — ใช้ตอน GET /api/maps/:id เพื่อให้ deploy แก้ JSON แล้วมีผลทันที
|
||||
* (เดิมโหลดครั้งเดียวตอนบูต · ถ้าแก้ไฟล์แล้วไม่รีสตาร์ท Node ลูกค้าได้ข้อมูลเก่า)
|
||||
*/
|
||||
function rehydrateSingleMapFromDisk(safeId) {
|
||||
if (!safeId) return false;
|
||||
const fp = path.join(MAPS_DIR, safeId + '.json');
|
||||
if (!fs.existsSync(fp)) return false;
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(fp, 'utf8'));
|
||||
normalizeQuizCarryLayersOnMap(data);
|
||||
normalizeJumpSurvivePlatformAreaOnMap(data);
|
||||
normalizeJumpSurvivePlatformVariantAreaOnMap(data);
|
||||
normalizeJumpSurviveHazardAreaOnMap(data);
|
||||
normalizeQuizBattleDomeAreaOnMap(data);
|
||||
normalizeQuizBattlePathAreaOnMap(data);
|
||||
applyStackTowerMissionMapPlayPolicy(safeId, data);
|
||||
maps.set(safeId, data);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('rehydrate map', safeId, e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** โหลด LobbyB จากไฟล์อีกครั้งถ้ายังไม่อยู่ใน memory (หลังรีสตาร์ท / ไฟล์เพิ่งวาง) */
|
||||
function ensurePostCaseLobbyMapLoaded() {
|
||||
if (maps.has(POST_CASE_LOBBY_SPACE_ID)) return true;
|
||||
@@ -2024,9 +2065,13 @@ const server = http.createServer((req, res) => {
|
||||
let url = req.url;
|
||||
if (url === BASE_PATH || url === BASE_PATH + '/') url = BASE_PATH + '/index.html';
|
||||
if (url.startsWith(BASE_PATH + '/api/maps')) {
|
||||
const id = url.replace(BASE_PATH + '/api/maps/', '').replace(BASE_PATH + '/api/maps', '').replace(/^\//, '').split('/')[0];
|
||||
let id = url.replace(BASE_PATH + '/api/maps/', '').replace(BASE_PATH + '/api/maps', '').replace(/^\//, '').split('/')[0];
|
||||
if (id.includes('?')) id = id.split('?')[0];
|
||||
if (req.method === 'GET' && id) {
|
||||
const m = maps.get(id);
|
||||
const sid = safeMapId(decodeURIComponent(id));
|
||||
if (!sid) return res.writeHead(404), res.end(JSON.stringify({ error: 'ไม่พบฉาก' }));
|
||||
rehydrateSingleMapFromDisk(sid);
|
||||
const m = maps.get(sid);
|
||||
if (!m) return res.writeHead(404), res.end(JSON.stringify({ error: 'ไม่พบฉาก' }));
|
||||
return res.writeHead(200, { 'Content-Type': 'application/json' }), res.end(JSON.stringify(m));
|
||||
}
|
||||
@@ -2113,6 +2158,7 @@ const server = http.createServer((req, res) => {
|
||||
normalizeJumpSurviveHazardAreaOnMap(m);
|
||||
normalizeQuizBattleDomeAreaOnMap(m);
|
||||
normalizeQuizBattlePathAreaOnMap(m);
|
||||
applyStackTowerMissionMapPlayPolicy(id, m);
|
||||
maps.set(id, m);
|
||||
saveMap(id, m);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
|
||||
Reference in New Issue
Block a user