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:
@@ -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 |
@@ -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'
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user