Game: mnn93hpi stack tower intro+loop scrolling BG (stackTowerBgScroll)

Bundled assets under img/editor-bg-mnn93hpi; editor controls + JSON save;
play.js mirrors space-shooter vertical strip; tick during stack + pregame.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-03 04:43:33 +00:00
parent b005d5f56e
commit 6f401d4877
7 changed files with 455 additions and 3 deletions
+17 -1
View File
@@ -274,6 +274,22 @@
<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>
@@ -319,7 +335,7 @@
</div>
</div>
<script src="js/version.js?v=0.0169"></script>
<script src="js/editor.js?v=20260427-stack-tower-cam"></script>
<script src="js/editor.js?v=20260503-stack-tower-bg-intro-loop"></script>
<div class="version-tag">v —</div>
</body>
</html>
@@ -0,0 +1,3 @@
Tower Stack mission map (editor id mnn93hpi) — default vertical BG strip.
intro.png = ground/rooftop frame · loop.png = tiling sky (seam for scroll).
Editor: "พื้นหลัง intro + loop" / map JSON field stackTowerBgScroll.
Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+292 -1
View File
@@ -247,12 +247,222 @@
function syncStackTowerCamBlockVisibilityOnly() {
const block = document.getElementById('editor-stack-tower-cam-block');
if (!block) return;
block.hidden = !isEditorStackTowerMissionMap();
const show = isEditorStackTowerMissionMap();
if (!show) stopStackTowerBgScrollRaf();
block.hidden = !show;
}
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;
}
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();
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');
@@ -1464,6 +1674,8 @@
if (gtDraw === 'balloon_boss') ensureBalloonBossPlayerSlots();
if (isEditorScrollBgDrawing()) {
drawEditorScrollingBackground(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 {
@@ -2389,10 +2601,87 @@
if (isEditorScrollBgMap()) applyEditorBgScrollFromMap({});
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();
});
});
}
})();
document.getElementById('btn-new').addEventListener('click', initGrid);
document.getElementById('btn-spawn').addEventListener('click', () => {
isSpawnMode = true;
@@ -2615,6 +2904,8 @@
if (ebsSave) body.editorBgScroll = ebsSave;
const stcSave = readStackTowerCameraFollowForSave();
if (stcSave !== undefined) body.stackTowerCameraFollow = stcSave;
const stBgSave = readStackTowerBgScrollForSave();
if (stBgSave !== undefined) body.stackTowerBgScroll = stBgSave;
body.lanes = gameType === 'frogger' ? lanes : [];
sanitizeGauntletPlayerSpawns();
body.gauntletPlayerSpawns = gameType === 'gauntlet'
+142
View File
@@ -175,6 +175,7 @@
/** Tower / Stack — เชือกเหวี่ยงจาก `img/TowerBlock/sling.png` */
let stackTowerSlingImg = null;
let lastStackTickMs = performance.now();
let lastStackTowerScrollBgTickMs = performance.now();
let mapBackgroundImg = null;
/** space_shooter + แมป mnpz6rkp — พื้นหลังเลื่อนแนวตั้ง (ข้อมูลจาก editorBgScroll ในแมป) */
const PLAY_SCROLL_BG_MAP_ID = 'mnpz6rkp';
@@ -311,6 +312,137 @@
playScrollBgPx += (playScrollBgSpeedPxPerSec || 56) * dtSec;
}
/** stack + แมป mnn93hpi — พื้นหลัง intro+loop เลื่อนแนวตั้ง (stackTowerBgScroll ในแมป) */
const STACK_TOWER_SCROLL_BG_DEFAULT_INTRO = BASE + '/img/editor-bg-mnn93hpi/intro.png';
const STACK_TOWER_SCROLL_BG_DEFAULT_LOOP = BASE + '/img/editor-bg-mnn93hpi/loop.png';
let stackTowerScrollBgIntroImg = null;
let stackTowerScrollBgLoopImg = null;
let stackTowerScrollBgPx = 0;
let stackTowerScrollBgSpeedPxPerSec = 40;
let stackTowerScrollBgOff = false;
let stackTowerScrollBgFlowDown = false;
function stackTowerScrollBgMapEligible() {
return !!(mapData && mapData.gameType === 'stack' && (currentPlayMapId() || '').trim() === STACK_TOWER_MISSION_MAP_ID);
}
function stackTowerScrollBgDrawActive() {
if (!stackTowerScrollBgMapEligible() || stackTowerScrollBgOff) return false;
const a = stackTowerScrollBgIntroImg;
const b = stackTowerScrollBgLoopImg;
return !!(a && a.complete && a.naturalWidth && b && b.complete && b.naturalWidth);
}
function reloadStackTowerScrollBgFromMap() {
stackTowerScrollBgIntroImg = null;
stackTowerScrollBgLoopImg = null;
stackTowerScrollBgPx = 0;
if (!stackTowerScrollBgMapEligible()) return;
const raw = mapData.stackTowerBgScroll && typeof mapData.stackTowerBgScroll === 'object' ? mapData.stackTowerBgScroll : {};
stackTowerScrollBgOff = raw.enabled === false;
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');
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;
const bump = () => {
pending--;
if (pending <= 0) {
stackTowerScrollBgSyncInitialScrollToBottom();
try { draw(); } catch (e) { /* ignore */ }
}
};
const im1 = new Image();
im1.onload = () => { stackTowerScrollBgIntroImg = im1; bump(); };
im1.onerror = () => { bump(); };
im1.src = introSrc;
const im2 = new Image();
im2.onload = () => { stackTowerScrollBgLoopImg = im2; bump(); };
im2.onerror = () => { bump(); };
im2.src = loopSrc;
}
function stackTowerScrollBgSyncInitialScrollToBottom() {
const intro = stackTowerScrollBgIntroImg;
if (!canvas || !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 (stackTowerScrollBgFlowDown) {
stackTowerScrollBgPx = 0;
} else {
stackTowerScrollBgPx = Math.max(0, drawHIntro - chR);
}
}
function drawStackTowerScrollBgFullCanvas(cw, ch) {
const intro = stackTowerScrollBgIntroImg;
const loop = stackTowerScrollBgLoopImg;
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 = stackTowerScrollBgPx;
const vp0 = S;
const vp1 = S + chR;
const yLimit = vp1 + drawHLoop * 2;
const flowDown = stackTowerScrollBgFlowDown;
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 tickStackTowerScrollBg(dtSec) {
if (!stackTowerScrollBgDrawActive()) return;
stackTowerScrollBgPx += (stackTowerScrollBgSpeedPxPerSec || 40) * dtSec;
}
/** โหลดจาก mapData.gridImageLibrary — ดัชนีตรงกับ gridImageCells[y][x] */
let mapGridImageImgs = [];
let mapGridImageHeldImgs = [];
@@ -10337,6 +10469,7 @@
mapBackgroundImg.src = mapData.backgroundImage;
}
reloadPlayScrollBgFromMap();
reloadStackTowerScrollBgFromMap();
normalizeGridImageCellsOnMap(mapData);
loadMapGridImages();
var modeLabel = isFrogger() ? ' | โหมดกบข้ามถนน'
@@ -11091,6 +11224,7 @@
mapBackgroundImg.src = mapData.backgroundImage;
}
reloadPlayScrollBgFromMap();
reloadStackTowerScrollBgFromMap();
normalizeGridImageCellsOnMap(mapData);
loadMapGridImages();
if (mapData.gameType === 'gauntlet' && ev.peersSnap && Array.isArray(ev.peersSnap)) {
@@ -12772,6 +12906,10 @@
ctx.fillStyle = '#0a0e22';
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawPlayScrollBgFullCanvas(canvas.width, canvas.height);
} else if (stackTowerScrollBgDrawActive()) {
ctx.fillStyle = '#0a0e22';
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawStackTowerScrollBgFullCanvas(canvas.width, canvas.height);
} else if (mapBackgroundImg && mapBackgroundImg.complete && mapBackgroundImg.naturalWidth) {
/* กล้องเห็นพื้นที่นอก [0,map] ได้ — ห้ามสุ่มต้นทาง drawImage เกินขอบภาพ (จะเกิดซ้ำ/เส้นตัด/ซ้อนกันที่ขอบจอ) */
ctx.fillStyle = (isSpaceShooter() || isBalloonBoss()) ? '#0a0e22' : '#1a1b26';
@@ -13608,6 +13746,10 @@
function tick() {
if (!mapData) { requestAnimationFrame(tick); return; }
if (isStack()) {
const nowStBg = performance.now();
const dtStBg = Math.min(0.06, (nowStBg - lastStackTowerScrollBgTickMs) / 1000);
lastStackTowerScrollBgTickMs = nowStBg;
tickStackTowerScrollBg(dtStBg);
if (isStackTowerMissionPregameBlockingPlay()) {
me.isWalking = false;
draw();
+1 -1
View File
@@ -2009,7 +2009,7 @@
</div>
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0184"></script>
<script src="js/play.js?v=0.266"></script>
<script src="js/play.js?v=0.267"></script>
<div class="version-tag">v —</div>
</body>
</html>