minigame 6 rocker 1.7
This commit is contained in:
@@ -505,7 +505,13 @@ body.editor-embed-admin .grid-cell-image-gallery-item img {
|
||||
}
|
||||
.canvas-wrap { margin: 1rem 0; }
|
||||
.container { padding-left: 1rem; padding-right: 1rem; }
|
||||
#editor-canvas { background: #1a1b26; display: block; touch-action: none; }
|
||||
#editor-canvas {
|
||||
background: #1a1b26;
|
||||
display: block;
|
||||
touch-action: none;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
.game-wrap { display: flex; flex-direction: column; height: 100vh; height: 100dvh; min-height: 0; }
|
||||
.game-header { flex-shrink: 0; background: #24283b; padding: 0.5rem 1rem; display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; }
|
||||
.game-header span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
@@ -517,6 +523,8 @@ body.editor-embed-admin .grid-cell-image-gallery-item img {
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
touch-action: none;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
.game-wrap .chat-panel { flex-shrink: 0; }
|
||||
.chat-panel { height: 160px; border-top: 1px solid #414868; display: flex; flex-direction: column; }
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<title>Editor ฉาก — Game</title>
|
||||
<link rel="stylesheet" href="css/style.css?v=20260430-footprint-groups">
|
||||
<link rel="stylesheet" href="css/style.css?v=20260427-canvas-pixel">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container editor-page">
|
||||
@@ -263,6 +263,24 @@
|
||||
<label>รูปแผนที่ <input type="file" id="bg-image" accept="image/*"></label>
|
||||
<button type="button" id="btn-clear-bg">ลบรูปพื้นหลัง</button>
|
||||
</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">
|
||||
<label class="editor-bg-scroll-check"><input type="checkbox" id="editor-bg-scroll-enabled"> เปิดเลื่อน / Enable scroll</label>
|
||||
<label>ความเร็ว px/s <input type="number" id="editor-bg-scroll-speed" min="8" max="400" step="4" value="56" style="width:5rem" title="ความเร็วการเลื่อนแนวตั้ง"></label>
|
||||
<label>ทิศทาง
|
||||
<select id="editor-bg-scroll-direction" title="เริ่มชิดล่าง intro แล้วเลื่อนเข้า loop ใต้ intro — ล่าง→บน = พิกเซลไหลขึ้น · บน→ล่าง = พลิกมุมมองในกรอบจอให้ไหลลง (strip เดิม)">
|
||||
<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-bg-scroll-intro-file" accept="image/*"></label>
|
||||
<label>รูปลูป (loop) <input type="file" id="editor-bg-scroll-loop-file" accept="image/*"></label>
|
||||
<button type="button" id="editor-bg-scroll-reset-assets">คืนรูปเริ่มจากเซิร์ฟ / Reset to bundled</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="editor-card editor-card--help" aria-labelledby="editor-card-help-label">
|
||||
@@ -289,7 +307,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<script src="js/version.js?v=0.0169"></script>
|
||||
<script src="js/editor.js?v=20260427-violent-crime-howto"></script>
|
||||
<script src="js/editor.js?v=20260427-bg-down-start"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 151 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
@@ -121,6 +121,216 @@
|
||||
let showMapInGame = true;
|
||||
let backgroundImage = null;
|
||||
let backgroundImageImg = null;
|
||||
/** พื้นหลังเลื่อนแนวตั้ง — เฉพาะแมป mnpz6rkp (Editor) */
|
||||
const EDITOR_SCROLL_BG_MAP_ID = 'mnpz6rkp';
|
||||
const EDITOR_SCROLL_BG_DEFAULT_INTRO = BASE + '/img/editor-bg-mnpz6rkp/intro.png';
|
||||
const EDITOR_SCROLL_BG_DEFAULT_LOOP = BASE + '/img/editor-bg-mnpz6rkp/loop.png';
|
||||
let editorBgScrollConfig = {
|
||||
enabled: false,
|
||||
speedPxPerSec: 56,
|
||||
/** 'up' = ล่าง→บน · 'down' = บน→ล่าง */
|
||||
scrollDirection: 'up',
|
||||
introImage: null,
|
||||
loopImage: null,
|
||||
};
|
||||
let editorBgScrollIntroImg = null;
|
||||
let editorBgScrollLoopImg = null;
|
||||
/** viewport top ใน strip (>=0) — เพิ่ม = เลื่อนขึ้น (ล่าง→บน) */
|
||||
let editorBgScrollPx = 0;
|
||||
let editorBgScrollRaf = null;
|
||||
let editorBgScrollLastTs = 0;
|
||||
|
||||
function stopEditorBgScrollRaf() {
|
||||
if (editorBgScrollRaf != null) {
|
||||
cancelAnimationFrame(editorBgScrollRaf);
|
||||
editorBgScrollRaf = null;
|
||||
}
|
||||
editorBgScrollLastTs = 0;
|
||||
}
|
||||
|
||||
function isEditorScrollBgMap() {
|
||||
return mapId === EDITOR_SCROLL_BG_MAP_ID;
|
||||
}
|
||||
|
||||
function isEditorScrollBgDrawing() {
|
||||
if (!isEditorScrollBgMap() || !editorBgScrollConfig.enabled) return false;
|
||||
const a = editorBgScrollIntroImg;
|
||||
const b = editorBgScrollLoopImg;
|
||||
return !!(a && a.complete && a.naturalWidth && b && b.complete && b.naturalWidth);
|
||||
}
|
||||
|
||||
function loadEditorBgScrollImagePair(introSrc, loopSrc, onDone) {
|
||||
stopEditorBgScrollRaf();
|
||||
editorBgScrollIntroImg = null;
|
||||
editorBgScrollLoopImg = null;
|
||||
let pending = 2;
|
||||
const doneOne = () => {
|
||||
pending--;
|
||||
if (pending <= 0 && typeof onDone === 'function') onDone();
|
||||
};
|
||||
const im1 = new Image();
|
||||
im1.onload = () => {
|
||||
editorBgScrollIntroImg = im1;
|
||||
doneOne();
|
||||
};
|
||||
im1.onerror = () => { doneOne(); };
|
||||
im1.src = introSrc;
|
||||
const im2 = new Image();
|
||||
im2.onload = () => {
|
||||
editorBgScrollLoopImg = im2;
|
||||
doneOne();
|
||||
};
|
||||
im2.onerror = () => { doneOne(); };
|
||||
im2.src = loopSrc;
|
||||
}
|
||||
|
||||
function applyEditorBgScrollFromMap(m) {
|
||||
if (!isEditorScrollBgMap()) {
|
||||
editorBgScrollConfig = { enabled: false, speedPxPerSec: 56, scrollDirection: 'up', introImage: null, loopImage: null };
|
||||
stopEditorBgScrollRaf();
|
||||
editorBgScrollIntroImg = null;
|
||||
editorBgScrollLoopImg = null;
|
||||
return;
|
||||
}
|
||||
const raw = m && m.editorBgScroll && typeof m.editorBgScroll === 'object' ? m.editorBgScroll : {};
|
||||
const sp = Math.max(8, Math.min(400, Math.floor(Number(raw.speedPxPerSec)) || 56));
|
||||
const sd = String(raw.scrollDirection || raw.direction || 'up').toLowerCase();
|
||||
editorBgScrollConfig = {
|
||||
enabled: !!raw.enabled,
|
||||
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 = editorBgScrollConfig.introImage || EDITOR_SCROLL_BG_DEFAULT_INTRO;
|
||||
const loopSrc = editorBgScrollConfig.loopImage || EDITOR_SCROLL_BG_DEFAULT_LOOP;
|
||||
loadEditorBgScrollImagePair(introSrc, loopSrc, () => {
|
||||
syncEditorBgScrollUiFromConfig();
|
||||
syncEditorBgScrollInitialToBottom();
|
||||
draw();
|
||||
if (editorBgScrollConfig.enabled) startEditorBgScrollRaf();
|
||||
});
|
||||
}
|
||||
|
||||
function syncEditorBgScrollInitialToBottom() {
|
||||
const intro = editorBgScrollIntroImg;
|
||||
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 (editorBgScrollConfig.scrollDirection === 'down') {
|
||||
editorBgScrollPx = 0;
|
||||
} else {
|
||||
editorBgScrollPx = Math.max(0, drawHIntro - chR);
|
||||
}
|
||||
}
|
||||
|
||||
function syncEditorBgScrollUiFromConfig() {
|
||||
const block = document.getElementById('editor-bg-scroll-block');
|
||||
if (block) block.hidden = !isEditorScrollBgMap();
|
||||
const en = document.getElementById('editor-bg-scroll-enabled');
|
||||
if (en) en.checked = !!editorBgScrollConfig.enabled;
|
||||
const sp = document.getElementById('editor-bg-scroll-speed');
|
||||
if (sp) sp.value = String(editorBgScrollConfig.speedPxPerSec || 56);
|
||||
const dir = document.getElementById('editor-bg-scroll-direction');
|
||||
if (dir) dir.value = editorBgScrollConfig.scrollDirection === 'down' ? 'down' : 'up';
|
||||
}
|
||||
|
||||
function readEditorBgScrollConfigForSave() {
|
||||
if (!isEditorScrollBgMap()) return null;
|
||||
const en = document.getElementById('editor-bg-scroll-enabled');
|
||||
const spEl = document.getElementById('editor-bg-scroll-speed');
|
||||
const sp = Math.max(8, Math.min(400, Math.floor(Number(spEl && spEl.value)) || 56));
|
||||
const dirEl = document.getElementById('editor-bg-scroll-direction');
|
||||
const out = {
|
||||
enabled: !!(en && en.checked),
|
||||
speedPxPerSec: sp,
|
||||
scrollDirection: dirEl && dirEl.value === 'down' ? 'down' : 'up',
|
||||
};
|
||||
if (editorBgScrollConfig.introImage) out.introImage = editorBgScrollConfig.introImage;
|
||||
if (editorBgScrollConfig.loopImage) out.loopImage = editorBgScrollConfig.loopImage;
|
||||
return out;
|
||||
}
|
||||
|
||||
function drawEditorScrollingBackground(cw, ch) {
|
||||
const intro = editorBgScrollIntroImg;
|
||||
const loop = editorBgScrollLoopImg;
|
||||
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 = editorBgScrollPx;
|
||||
const vp0 = S;
|
||||
const vp1 = S + chR;
|
||||
const yLimit = vp1 + drawHLoop * 2;
|
||||
const flowDown = editorBgScrollConfig.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 editorBgScrollTick(ts) {
|
||||
editorBgScrollRaf = null;
|
||||
if (!isEditorScrollBgDrawing()) return;
|
||||
if (!editorBgScrollLastTs) editorBgScrollLastTs = ts;
|
||||
const dt = Math.min(64, Math.max(0, ts - editorBgScrollLastTs));
|
||||
editorBgScrollLastTs = ts;
|
||||
editorBgScrollPx += (editorBgScrollConfig.speedPxPerSec || 56) * (dt / 1000);
|
||||
draw();
|
||||
editorBgScrollRaf = requestAnimationFrame(editorBgScrollTick);
|
||||
}
|
||||
|
||||
function startEditorBgScrollRaf() {
|
||||
if (!isEditorScrollBgDrawing()) return;
|
||||
stopEditorBgScrollRaf();
|
||||
editorBgScrollLastTs = 0;
|
||||
editorBgScrollRaf = requestAnimationFrame(editorBgScrollTick);
|
||||
}
|
||||
|
||||
/** คลังรูป + สไปรต์ { i, x, y, w, h } — บันทึก gridImageSprites + gridImageCells (สำหรับรุ่นเก่า) */
|
||||
let gridImageLibrary = [];
|
||||
let gridImageSprites = [];
|
||||
@@ -1212,7 +1422,9 @@
|
||||
if (gtDraw === 'jump_survive') ensureJumpSurvivePlatformArea();
|
||||
if (gtDraw === 'space_shooter') ensureShooterSpawnSlots();
|
||||
if (gtDraw === 'balloon_boss') ensureBalloonBossPlayerSlots();
|
||||
if (backgroundImageImg && backgroundImageImg.complete && backgroundImageImg.naturalWidth) {
|
||||
if (isEditorScrollBgDrawing()) {
|
||||
drawEditorScrollingBackground(canvas.width, canvas.height);
|
||||
} else if (backgroundImageImg && backgroundImageImg.complete && backgroundImageImg.naturalWidth) {
|
||||
ctx.drawImage(backgroundImageImg, 0, 0, canvas.width, canvas.height);
|
||||
} else {
|
||||
ctx.fillStyle = '#1a1b26';
|
||||
@@ -2057,6 +2269,85 @@
|
||||
draw();
|
||||
});
|
||||
|
||||
(function wireEditorBgScrollUi() {
|
||||
if (!isEditorScrollBgMap()) return;
|
||||
syncEditorBgScrollUiFromConfig();
|
||||
const en = document.getElementById('editor-bg-scroll-enabled');
|
||||
const sp = document.getElementById('editor-bg-scroll-speed');
|
||||
if (en) {
|
||||
en.addEventListener('change', () => {
|
||||
editorBgScrollConfig.enabled = !!en.checked;
|
||||
if (editorBgScrollConfig.enabled) startEditorBgScrollRaf();
|
||||
else stopEditorBgScrollRaf();
|
||||
draw();
|
||||
});
|
||||
}
|
||||
if (sp) {
|
||||
sp.addEventListener('change', () => {
|
||||
editorBgScrollConfig.speedPxPerSec = Math.max(8, Math.min(400, Math.floor(Number(sp.value)) || 56));
|
||||
});
|
||||
}
|
||||
const dirSel = document.getElementById('editor-bg-scroll-direction');
|
||||
if (dirSel) {
|
||||
dirSel.addEventListener('change', () => {
|
||||
editorBgScrollConfig.scrollDirection = dirSel.value === 'down' ? 'down' : 'up';
|
||||
syncEditorBgScrollInitialToBottom();
|
||||
draw();
|
||||
if (editorBgScrollConfig.enabled) startEditorBgScrollRaf();
|
||||
});
|
||||
}
|
||||
const introF = document.getElementById('editor-bg-scroll-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 = () => {
|
||||
editorBgScrollConfig.introImage = r.result;
|
||||
loadEditorBgScrollImagePair(editorBgScrollConfig.introImage, editorBgScrollConfig.loopImage || EDITOR_SCROLL_BG_DEFAULT_LOOP, () => {
|
||||
syncEditorBgScrollInitialToBottom();
|
||||
draw();
|
||||
if (editorBgScrollConfig.enabled) startEditorBgScrollRaf();
|
||||
});
|
||||
};
|
||||
r.readAsDataURL(file);
|
||||
e.target.value = '';
|
||||
});
|
||||
}
|
||||
const loopF = document.getElementById('editor-bg-scroll-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 = () => {
|
||||
editorBgScrollConfig.loopImage = r.result;
|
||||
loadEditorBgScrollImagePair(editorBgScrollConfig.introImage || EDITOR_SCROLL_BG_DEFAULT_INTRO, editorBgScrollConfig.loopImage, () => {
|
||||
syncEditorBgScrollInitialToBottom();
|
||||
draw();
|
||||
if (editorBgScrollConfig.enabled) startEditorBgScrollRaf();
|
||||
});
|
||||
};
|
||||
r.readAsDataURL(file);
|
||||
e.target.value = '';
|
||||
});
|
||||
}
|
||||
const btnR = document.getElementById('editor-bg-scroll-reset-assets');
|
||||
if (btnR) {
|
||||
btnR.addEventListener('click', () => {
|
||||
editorBgScrollConfig.introImage = null;
|
||||
editorBgScrollConfig.loopImage = null;
|
||||
loadEditorBgScrollImagePair(EDITOR_SCROLL_BG_DEFAULT_INTRO, EDITOR_SCROLL_BG_DEFAULT_LOOP, () => {
|
||||
syncEditorBgScrollUiFromConfig();
|
||||
syncEditorBgScrollInitialToBottom();
|
||||
draw();
|
||||
if (editorBgScrollConfig.enabled) startEditorBgScrollRaf();
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
if (isEditorScrollBgMap()) applyEditorBgScrollFromMap({});
|
||||
|
||||
document.getElementById('btn-new').addEventListener('click', initGrid);
|
||||
document.getElementById('btn-spawn').addEventListener('click', () => {
|
||||
isSpawnMode = true;
|
||||
@@ -2275,6 +2566,8 @@
|
||||
gridImageCells: deriveGridImageCellsFromSprites(),
|
||||
};
|
||||
if (backgroundImage) body.backgroundImage = backgroundImage;
|
||||
const ebsSave = readEditorBgScrollConfigForSave();
|
||||
if (ebsSave) body.editorBgScroll = ebsSave;
|
||||
body.lanes = gameType === 'frogger' ? lanes : [];
|
||||
sanitizeGauntletPlayerSpawns();
|
||||
body.gauntletPlayerSpawns = gameType === 'gauntlet'
|
||||
@@ -2556,6 +2849,7 @@
|
||||
backgroundImageImg.onload = () => { draw(); };
|
||||
backgroundImageImg.src = backgroundImage;
|
||||
} else backgroundImageImg = null;
|
||||
applyEditorBgScrollFromMap(m);
|
||||
spawn = m.spawn || { x: 1, y: 1 };
|
||||
gauntletPlayerSpawns = Array.isArray(m.gauntletPlayerSpawns)
|
||||
? m.gauntletPlayerSpawns.map((s) => ({ x: Math.floor(Number(s.x)), y: Math.floor(Number(s.y)) }))
|
||||
|
||||
+154
-12
@@ -156,6 +156,141 @@
|
||||
let stackFall = null;
|
||||
let lastStackTickMs = performance.now();
|
||||
let mapBackgroundImg = null;
|
||||
/** space_shooter + แมป mnpz6rkp — พื้นหลังเลื่อนแนวตั้ง (ข้อมูลจาก editorBgScroll ในแมป) */
|
||||
const PLAY_SCROLL_BG_MAP_ID = 'mnpz6rkp';
|
||||
const PLAY_SCROLL_BG_DEFAULT_INTRO = BASE + '/img/editor-bg-mnpz6rkp/intro.png';
|
||||
const PLAY_SCROLL_BG_DEFAULT_LOOP = BASE + '/img/editor-bg-mnpz6rkp/loop.png';
|
||||
let playScrollBgIntroImg = null;
|
||||
let playScrollBgLoopImg = null;
|
||||
/** ขอบบนของ viewport ใน strip — เพิ่มค่า = เลื่อนลง strip (เข้า loop ใต้ intro) */
|
||||
let playScrollBgPx = 0;
|
||||
let playScrollBgSpeedPxPerSec = 56;
|
||||
let playScrollBgOff = false;
|
||||
/** true = บน→ล่าง: พลิกมุมมองใน viewport (ยังเลื่อน S+ เข้า loop ใต้ intro) */
|
||||
let playScrollBgFlowDown = false;
|
||||
|
||||
function playScrollBgMapEligible() {
|
||||
return !!(mapData && mapData.gameType === 'space_shooter' && (currentPlayMapId() || '').trim() === PLAY_SCROLL_BG_MAP_ID);
|
||||
}
|
||||
|
||||
function playScrollBgDrawActive() {
|
||||
if (!playScrollBgMapEligible() || playScrollBgOff) return false;
|
||||
const a = playScrollBgIntroImg;
|
||||
const b = playScrollBgLoopImg;
|
||||
return !!(a && a.complete && a.naturalWidth && b && b.complete && b.naturalWidth);
|
||||
}
|
||||
|
||||
function reloadPlayScrollBgFromMap() {
|
||||
playScrollBgIntroImg = null;
|
||||
playScrollBgLoopImg = null;
|
||||
playScrollBgPx = 0;
|
||||
if (!playScrollBgMapEligible()) return;
|
||||
const raw = mapData.editorBgScroll && typeof mapData.editorBgScroll === 'object' ? mapData.editorBgScroll : {};
|
||||
playScrollBgOff = raw.enabled === false;
|
||||
playScrollBgSpeedPxPerSec = Math.max(8, Math.min(400, Math.floor(Number(raw.speedPxPerSec)) || 56));
|
||||
const dirRaw = String(raw.scrollDirection || raw.direction || 'up').toLowerCase();
|
||||
playScrollBgFlowDown = (dirRaw === 'down' || dirRaw === 'top' || dirRaw === 'toptobottom');
|
||||
if (playScrollBgOff) return;
|
||||
const introSrc = (typeof raw.introImage === 'string' && raw.introImage.length) ? raw.introImage : PLAY_SCROLL_BG_DEFAULT_INTRO;
|
||||
const loopSrc = (typeof raw.loopImage === 'string' && raw.loopImage.length) ? raw.loopImage : PLAY_SCROLL_BG_DEFAULT_LOOP;
|
||||
let pending = 2;
|
||||
const bump = () => {
|
||||
pending--;
|
||||
if (pending <= 0) {
|
||||
playScrollBgSyncInitialScrollToBottom();
|
||||
try { draw(); } catch (e) { /* ignore */ }
|
||||
}
|
||||
};
|
||||
const im1 = new Image();
|
||||
im1.onload = () => { playScrollBgIntroImg = im1; bump(); };
|
||||
im1.onerror = () => { bump(); };
|
||||
im1.src = introSrc;
|
||||
const im2 = new Image();
|
||||
im2.onload = () => { playScrollBgLoopImg = im2; bump(); };
|
||||
im2.onerror = () => { bump(); };
|
||||
im2.src = loopSrc;
|
||||
}
|
||||
|
||||
function playScrollBgSyncInitialScrollToBottom() {
|
||||
const intro = playScrollBgIntroImg;
|
||||
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));
|
||||
/* ล่าง→บน: S = HI−ch ให้ขอบล่าง intro ชิดล่างจอ · บน→ล่าง (พลิก dest): S=0 ถึงได้ขอบล่าง intro ชิดล่างจอ */
|
||||
if (playScrollBgFlowDown) {
|
||||
playScrollBgPx = 0;
|
||||
} else {
|
||||
playScrollBgPx = Math.max(0, drawHIntro - chR);
|
||||
}
|
||||
}
|
||||
|
||||
function drawPlayScrollBgFullCanvas(cw, ch) {
|
||||
const intro = playScrollBgIntroImg;
|
||||
const loop = playScrollBgLoopImg;
|
||||
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 = playScrollBgPx;
|
||||
const vp0 = S;
|
||||
const vp1 = S + chR;
|
||||
const yLimit = vp1 + drawHLoop * 2;
|
||||
const flowDown = playScrollBgFlowDown;
|
||||
|
||||
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 tickPlayScrollBg(dtSec) {
|
||||
if (!playScrollBgDrawActive()) return;
|
||||
playScrollBgPx += (playScrollBgSpeedPxPerSec || 56) * dtSec;
|
||||
}
|
||||
|
||||
/** โหลดจาก mapData.gridImageLibrary — ดัชนีตรงกับ gridImageCells[y][x] */
|
||||
let mapGridImageImgs = [];
|
||||
let mapGridImageHeldImgs = [];
|
||||
@@ -7217,6 +7352,7 @@
|
||||
const now = performance.now();
|
||||
const dt = Math.min(0.055, spaceShooterLastTickMs ? (now - spaceShooterLastTickMs) / 1000 : 0.016);
|
||||
spaceShooterLastTickMs = now;
|
||||
tickPlayScrollBg(dt);
|
||||
spaceShooterTickAsteroidExplosions(dt);
|
||||
|
||||
if (spaceShooterGameEnded) {
|
||||
@@ -7308,20 +7444,20 @@
|
||||
const a = spaceShooterAsteroids[hit];
|
||||
a.hp = (a.hp || 1) - 1;
|
||||
spaceShooterBullets.splice(bi, 1);
|
||||
const add = 5;
|
||||
if (b.ownerId === myId) {
|
||||
me.spaceShooterScore = Math.max(0, (me.spaceShooterScore || 0) + add);
|
||||
spaceShooterPopups.push({ x: me.spaceShooterCx, y: me.spaceShooterCy - 30, text: '+5', until: Date.now() + 700 });
|
||||
} else {
|
||||
const o = others.get(b.ownerId);
|
||||
if (o) {
|
||||
o.spaceShooterScore = Math.max(0, (o.spaceShooterScore || 0) + add);
|
||||
spaceShooterPopups.push({ x: o.spaceShooterCx, y: o.spaceShooterCy - 30, text: '+5', until: Date.now() + 700 });
|
||||
}
|
||||
}
|
||||
if (a.hp <= 0) {
|
||||
spaceShooterSpawnAsteroidExplosion(a.x, a.y, a.r);
|
||||
spaceShooterAsteroids.splice(hit, 1);
|
||||
const add = 5;
|
||||
if (b.ownerId === myId) {
|
||||
me.spaceShooterScore = Math.max(0, (me.spaceShooterScore || 0) + add);
|
||||
spaceShooterPopups.push({ x: me.spaceShooterCx, y: me.spaceShooterCy - 30, text: '+5', until: Date.now() + 700 });
|
||||
} else {
|
||||
const o = others.get(b.ownerId);
|
||||
if (o) {
|
||||
o.spaceShooterScore = Math.max(0, (o.spaceShooterScore || 0) + add);
|
||||
spaceShooterPopups.push({ x: o.spaceShooterCx, y: o.spaceShooterCy - 30, text: '+5', until: Date.now() + 700 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8963,6 +9099,7 @@
|
||||
mapBackgroundImg = new Image();
|
||||
mapBackgroundImg.src = mapData.backgroundImage;
|
||||
}
|
||||
reloadPlayScrollBgFromMap();
|
||||
normalizeGridImageCellsOnMap(mapData);
|
||||
loadMapGridImages();
|
||||
var modeLabel = isFrogger() ? ' | โหมดกบข้ามถนน'
|
||||
@@ -9628,6 +9765,7 @@
|
||||
mapBackgroundImg = new Image();
|
||||
mapBackgroundImg.src = mapData.backgroundImage;
|
||||
}
|
||||
reloadPlayScrollBgFromMap();
|
||||
normalizeGridImageCellsOnMap(mapData);
|
||||
loadMapGridImages();
|
||||
if (mapData.gameType === 'gauntlet' && ev.peersSnap && Array.isArray(ev.peersSnap)) {
|
||||
@@ -11160,7 +11298,11 @@
|
||||
const showGrid = mapData.showMapInGame !== false && mapData.showMapInGame !== 'false';
|
||||
const timeMs = Date.now();
|
||||
|
||||
if (mapBackgroundImg && mapBackgroundImg.complete && mapBackgroundImg.naturalWidth) {
|
||||
if (playScrollBgDrawActive()) {
|
||||
ctx.fillStyle = '#0a0e22';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
drawPlayScrollBgFullCanvas(canvas.width, canvas.height);
|
||||
} else if (mapBackgroundImg && mapBackgroundImg.complete && mapBackgroundImg.naturalWidth) {
|
||||
/* กล้องเห็นพื้นที่นอก [0,map] ได้ — ห้ามสุ่มต้นทาง drawImage เกินขอบภาพ (จะเกิดซ้ำ/เส้นตัด/ซ้อนกันที่ขอบจอ) */
|
||||
ctx.fillStyle = (isSpaceShooter() || isBalloonBoss()) ? '#0a0e22' : '#1a1b26';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
@@ -8,7 +8,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">
|
||||
<link rel="stylesheet" href="css/style.css?v=20260427-canvas-pixel">
|
||||
<style>
|
||||
html, body { height: 100%; margin: 0; }
|
||||
body { min-height: 100dvh; }
|
||||
@@ -37,6 +37,8 @@
|
||||
width: 100%;
|
||||
display: block;
|
||||
touch-action: none;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
/* หลังกดรับหลักฐานในหน้าสรุป — BG ดำ alpha ทั้งจอ + timeup ติดโซนโต๊ะเหมือน #quiz-map-question-panel */
|
||||
#quiz-carry-timeup-desk-layer {
|
||||
@@ -1897,7 +1899,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.224"></script>
|
||||
<script src="js/play.js?v=0.231"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user