game 50 users

This commit is contained in:
2026-06-07 04:19:51 +00:00
parent 76d31a224a
commit 9119c7068c
8 changed files with 728 additions and 64 deletions
File diff suppressed because one or more lines are too long
+32
View File
@@ -512,6 +512,38 @@ body.editor-embed-admin .grid-cell-image-gallery-item img {
image-rendering: pixelated;
image-rendering: crisp-edges;
}
.editor-map-zoom-bar {
display: flex;
align-items: center;
gap: 5px;
padding: 6px 10px;
border-radius: 999px;
background: rgba(8, 12, 28, 0.88);
border: 1px solid #2a3357;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.45);
}
.editor-map-zoom-btn {
border: 1px solid #2a3357;
background: #0d1430;
color: #eef2ff;
border-radius: 8px;
min-width: 34px;
height: 32px;
padding: 0 8px;
cursor: pointer;
font: inherit;
font-weight: 700;
line-height: 1;
}
.editor-map-zoom-btn:hover { filter: brightness(1.2); }
.editor-map-zoom-label {
min-width: 46px;
text-align: center;
font-size: 12px;
font-weight: 700;
color: #c7d2fe;
user-select: none;
}
.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; }
+15 -1
View File
@@ -361,6 +361,13 @@
</section>
</div>
<div class="editor-workspace">
<div class="editor-map-zoom-bar" id="editor-map-zoom-bar" aria-label="ซูมแผนที่">
<button type="button" class="editor-map-zoom-btn" id="editor-zoom-out" title="ซูมออก ()"></button>
<span class="editor-map-zoom-label" id="editor-zoom-label">100%</span>
<button type="button" class="editor-map-zoom-btn" id="editor-zoom-in" title="ซูมเข้า (+)">+</button>
<button type="button" class="editor-map-zoom-btn" id="editor-zoom-reset" title="ขนาดจริง 1:1">1:1</button>
<button type="button" class="editor-map-zoom-btn" id="editor-zoom-fit" title="พอดีหน้าจอ">Fit</button>
</div>
<div class="canvas-wrap">
<canvas id="editor-canvas"></canvas>
<div id="editor-stack-hud-mock" class="is-hidden" aria-hidden="true">
@@ -400,7 +407,7 @@
</div>
</div>
<script src="js/version.js?v=0.0258"></script>
<script src="js/editor.js?v=0.0335"></script>
<script src="js/editor.js?v=0.0336"></script>
<div class="version-tag">v —</div>
<!-- ===== QB overlay layout: map เต็มจอ + แถบเครื่องมือลอย ดัก/พับ/ปิดเปิดได้ (เพิ่มทับ ไม่แตะ editor.js) ===== -->
@@ -411,6 +418,13 @@
.editor-workspace { position:fixed !important; inset:0 !important; margin:0 !important; padding:16px !important;
overflow:auto !important; z-index:1; text-align:center; }
.editor-workspace .canvas-wrap { display:inline-block; margin:0 auto; box-shadow:0 12px 50px rgba(0,0,0,.6); }
.editor-map-zoom-bar { position:fixed; right:14px; bottom:38px; z-index:6; display:flex; align-items:center; gap:5px;
padding:6px 10px; border-radius:999px; background:rgba(8,12,28,.88); border:1px solid #2a3357;
box-shadow:0 6px 24px rgba(0,0,0,.45); -webkit-backdrop-filter:blur(8px); backdrop-filter:blur(8px); }
.editor-map-zoom-btn { border:1px solid #2a3357; background:#0d1430; color:#eef2ff; border-radius:8px; min-width:34px; height:32px;
padding:0 8px; cursor:pointer; font:inherit; font-weight:700; line-height:1; }
.editor-map-zoom-btn:hover { filter:brightness(1.2); }
.editor-map-zoom-label { min-width:46px; text-align:center; font-size:12px; font-weight:700; color:#c7d2fe; user-select:none; }
/* ซ่อน mock HUD ของ Tower Stack ที่ไม่เกี่ยวกับการ set ฉาก */
#editor-stack-hud-mock { display:none !important; }
.editor-workspace #editor-status { position:fixed; left:50%; bottom:6px; transform:translateX(-50%); margin:0; z-index:5;
+82 -2
View File
@@ -51,6 +51,12 @@
}
let tileSize = 32, width = 20, height = 15, characterCellsW = 1, characterCellsH = 1;
const MAP_ZOOM_MIN = 0.25;
const MAP_ZOOM_MAX = 3;
const MAP_ZOOM_STEP = 0.1;
let mapZoom = 1;
const editorZoomLabelEl = document.getElementById('editor-zoom-label');
const editorWorkspaceEl = document.querySelector('.editor-workspace');
function clampCharacterFootprint(n) {
const v = parseInt(n, 10);
@@ -2219,6 +2225,55 @@
toggleFroggerUI();
}
function clampMapZoom(z) {
const v = Number(z);
if (!Number.isFinite(v)) return 1;
return Math.max(MAP_ZOOM_MIN, Math.min(MAP_ZOOM_MAX, v));
}
function updateMapZoomLabel() {
if (editorZoomLabelEl) editorZoomLabelEl.textContent = Math.round(mapZoom * 100) + '%';
}
function applyMapZoom() {
mapZoom = clampMapZoom(mapZoom);
if (canvas.width > 0 && canvas.height > 0) {
canvas.style.width = Math.round(canvas.width * mapZoom) + 'px';
canvas.style.height = Math.round(canvas.height * mapZoom) + 'px';
}
updateMapZoomLabel();
try { localStorage.setItem('editorMapZoom', String(mapZoom)); } catch (e) { /* ignore */ }
}
function setMapZoom(next, anchorEvent) {
const prev = mapZoom;
mapZoom = clampMapZoom(next);
if (anchorEvent && editorWorkspaceEl && prev !== mapZoom) {
const ws = editorWorkspaceEl;
const r = canvas.getBoundingClientRect();
const ratioX = (anchorEvent.clientX - r.left) / Math.max(1, r.width);
const ratioY = (anchorEvent.clientY - r.top) / Math.max(1, r.height);
applyMapZoom();
const r2 = canvas.getBoundingClientRect();
ws.scrollLeft += (ratioX * r2.width + r2.left) - (ratioX * r.width + r.left);
ws.scrollTop += (ratioY * r2.height + r2.top) - (ratioY * r.height + r.top);
} else {
applyMapZoom();
}
}
function zoomMapBy(delta, anchorEvent) {
setMapZoom(mapZoom + delta, anchorEvent);
}
function zoomMapFit() {
if (!editorWorkspaceEl || !canvas.width || !canvas.height) return;
const pad = 28;
const availW = Math.max(120, editorWorkspaceEl.clientWidth - pad);
const availH = Math.max(120, editorWorkspaceEl.clientHeight - pad);
setMapZoom(Math.min(availW / canvas.width, availH / canvas.height, MAP_ZOOM_MAX));
}
function resize() {
canvas.width = width * tileSize;
canvas.height = height * tileSize;
@@ -2228,13 +2283,17 @@
ensureBalloonBossPlayerSlots();
sanitizeSpritesInPlace();
syncGridImageBrushInputCaps();
applyMapZoom();
draw();
}
function getCell(e) {
const r = canvas.getBoundingClientRect();
const x = Math.floor((e.clientX - r.left) / tileSize);
const y = Math.floor((e.clientY - r.top) / tileSize);
if (!r.width || !r.height) return { x: 0, y: 0 };
const relX = (e.clientX - r.left) / r.width;
const relY = (e.clientY - r.top) / r.height;
const x = Math.floor(relX * width);
const y = Math.floor(relY * height);
return { x: Math.max(0, Math.min(width - 1, x)), y: Math.max(0, Math.min(height - 1, y)) };
}
@@ -3950,6 +4009,27 @@
mapW.addEventListener('change', resize);
mapH.addEventListener('change', resize);
tileSizeEl.addEventListener('change', resize);
try {
const savedZoom = parseFloat(localStorage.getItem('editorMapZoom'));
if (Number.isFinite(savedZoom)) mapZoom = clampMapZoom(savedZoom);
} catch (e) { /* ignore */ }
const btnZoomIn = document.getElementById('editor-zoom-in');
const btnZoomOut = document.getElementById('editor-zoom-out');
const btnZoomReset = document.getElementById('editor-zoom-reset');
const btnZoomFit = document.getElementById('editor-zoom-fit');
if (btnZoomIn) btnZoomIn.addEventListener('click', () => zoomMapBy(MAP_ZOOM_STEP));
if (btnZoomOut) btnZoomOut.addEventListener('click', () => zoomMapBy(-MAP_ZOOM_STEP));
if (btnZoomReset) btnZoomReset.addEventListener('click', () => setMapZoom(1));
if (btnZoomFit) btnZoomFit.addEventListener('click', () => zoomMapFit());
if (editorWorkspaceEl) {
editorWorkspaceEl.addEventListener('wheel', (e) => {
if (!e.ctrlKey && !e.metaKey) return;
e.preventDefault();
const delta = e.deltaY < 0 ? MAP_ZOOM_STEP : -MAP_ZOOM_STEP;
zoomMapBy(delta, e);
}, { passive: false });
}
if (characterCellsWEl) characterCellsWEl.addEventListener('input', () => {
const fp = readCharacterFootprintInputs();
applyCharacterFootprintInputs(fp.cw, fp.ch);
+373 -45
View File
@@ -4346,9 +4346,159 @@
}
}
/** จุดเกิด Quiz Battle — ต้องอยู่บนเส้นทาง (กล่องสไปรต์) ไม่ใช่แค่ snap จาก spawnArea นอกเลน */
function collectQuizBattleValidSpawnCentersPlay(md) {
const out = [];
if (!md || md.gameType !== 'quiz_battle' || !quizBattlePathModeActive(md)) return out;
const w = md.width || 20, h = md.height || 15;
const g = md.quizBattlePathArea;
const seen = new Set();
const prevMap = mapData;
mapData = md;
try {
for (let ty = 0; ty < h; ty++) {
for (let tx = 0; tx < w; tx++) {
if (!g[ty] || g[ty][tx] !== 1) continue;
for (const [nx, ny] of [[tx + 0.5, ty + 0.5], [tx + 0.01, ty + 0.01]]) {
const key = nx.toFixed(3) + ',' + ny.toFixed(3);
if (seen.has(key)) continue;
if (!quizBattleSnapTargetValidPlay(nx, ny)) continue;
seen.add(key);
out.push({ tx, ty, x: nx, y: ny });
}
}
}
} finally {
mapData = prevMap;
}
out.sort((a, b) => a.ty - b.ty || a.tx - b.tx || a.x - b.x);
return out;
}
/** จุดโลกใกล้ pref — ลองกลางช่องก่อน แล้วหาใน spawnArea (ฟ้า) ที่วางตัวได้ */
function quizBattleNearestValidSpawnWorldPlay(md, prefTx, prefTy) {
const ptx = Math.floor(Number(prefTx)) || 0;
const pty = Math.floor(Number(prefTy)) || 0;
const prevMap = mapData;
mapData = md;
try {
for (const [nx, ny] of [[ptx + 0.5, pty + 0.5], [ptx + 0.01, pty + 0.01]]) {
if (quizBattleSpawnWallClearPlay(md, nx, ny)) return { x: nx, y: ny };
}
const pool = collectQuizBattleSpawnPoolPlay(md);
if (pool.length) {
let best = pool[0], bestD = Infinity;
for (const c of pool) {
const d = Math.abs(c.tx - ptx) + Math.abs(c.ty - pty);
if (d < bestD) { bestD = d; best = c; }
}
return { x: best.x, y: best.y };
}
const valid = collectQuizBattleValidSpawnCentersPlay(md);
if (!valid.length) return { x: ptx + 0.5, y: pty + 0.5 };
let best = valid[0], bestD = Infinity;
for (const c of valid) {
const d = Math.abs(c.tx - ptx) + Math.abs(c.ty - pty);
if (d < bestD) { bestD = d; best = c; }
}
return { x: best.x, y: best.y };
} finally {
mapData = prevMap;
}
}
/** ช่อง spawnArea (ฟ้า) ที่วางตัวได้ — สุ่มในพื้นที่ฟ้า ไม่บังคับอยู่บนเลนม่วง */
function collectQuizBattleSpawnPoolPlay(md) {
const out = [];
if (!md) return out;
const prevMap = mapData;
mapData = md;
try {
const w = md.width || 20, h = md.height || 15;
const grid = md.spawnArea;
const seen = new Set();
const cells = [];
if (grid && Array.isArray(grid)) {
for (let ty = 0; ty < h; ty++) {
const row = grid[ty];
if (!row) continue;
for (let tx = 0; tx < w; tx++) {
if (Number(row[tx]) === 1) cells.push({ tx, ty });
}
}
}
if (!cells.length && md.spawn) {
cells.push({
tx: Math.max(0, Math.min(w - 1, Math.floor(Number(md.spawn.x)) || 1)),
ty: Math.max(0, Math.min(h - 1, Math.floor(Number(md.spawn.y)) || 1)),
});
}
for (const { tx, ty } of cells) {
for (const [nx, ny] of [[tx + 0.5, ty + 0.5], [tx + 0.01, ty + 0.01]]) {
const key = nx.toFixed(3) + ',' + ny.toFixed(3);
if (seen.has(key)) continue;
if (quizBattlePathModeActive(md)) {
if (!quizBattleSpawnWallClearPlay(md, nx, ny)) continue;
} else if (!spawnFootprintFitsPlay(md, tx, ty)) {
continue;
}
seen.add(key);
out.push({ tx, ty, x: nx, y: ny });
}
}
if (!out.length && quizBattlePathModeActive(md)) {
return collectQuizBattleValidSpawnCentersPlay(md);
}
} finally {
mapData = prevMap;
}
return out;
}
function pickQuizBattleRandomSpawnWorldPlay(md) {
const pool = collectQuizBattleSpawnPoolPlay(md);
const fb = md.spawn || { x: 1, y: 1 };
if (!pool.length) {
return quizBattleNearestValidSpawnWorldPlay(md, fb.x, fb.y);
}
const u = new Uint32Array(1);
if (typeof crypto !== 'undefined' && crypto.getRandomValues) crypto.getRandomValues(u);
else u[0] = (Math.floor(Math.random() * 0xffffffff) >>> 0);
const pick = pool[u[0] % pool.length];
return { x: pick.x, y: pick.y };
}
function quizBattleSpawnWorldFromJoinOrderPlay(md, joinOrderIndex) {
if (!md) return { x: 1.5, y: 1.5 };
const ord = joinOrderIndex | 0;
const mode = md.lobbySpawnMode;
if (mode === 'slots6') {
const slots = parseLobbyPlayerSpawnsFromMapPlay(md);
const j = Math.min(Math.max(0, ord), 5);
const slot = slots[j];
if (slot) return quizBattleNearestValidSpawnWorldPlay(md, slot.x, slot.y);
return pickQuizBattleRandomSpawnWorldPlay(md);
}
if (mode === 'fixed' && md.spawn) {
const sx = Number(md.spawn.x) || 1, sy = Number(md.spawn.y) || 1;
return quizBattleNearestValidSpawnWorldPlay(md, sx, sy);
}
/* random (ค่าเริ่มต้นใน editor) — สุ่มในพื้นที่ฟ้า ไม่เรียงตาม join order */
return pickQuizBattleRandomSpawnWorldPlay(md);
}
function pickQuizBattleSpawnFromMapPlay(md, joinOrderIndex) {
const world = quizBattleSpawnWorldFromJoinOrderPlay(md, joinOrderIndex);
return { x: Math.floor(world.x), y: Math.floor(world.y) };
}
/** สอดคล้องกับ server pickSpawnForJoin — ใช้พรีวิวบอท / ทดสอบ */
function pickSpawnForJoinPlay(md, joinOrderIndex) {
if (!md) return { x: 1, y: 1 };
if (md.gameType === 'quiz_battle' && quizBattlePathModeActive(md)) {
const qb = pickQuizBattleSpawnFromMapPlay(md, joinOrderIndex);
if (qb) return qb;
}
const mode = md.lobbySpawnMode;
const ord = joinOrderIndex | 0;
if (mode === 'slots6' && ord >= 6) return pickRandomSpawnFromMapPlay(md);
@@ -4561,6 +4711,10 @@
const pos = jumpSurviveSpawnWorldFromJoinOrderPlay(mapData, joinIdx);
x = pos.x;
y = pos.y;
} else if (mapData.gameType === 'quiz_battle' && quizBattlePathModeActive(mapData)) {
const pos = quizBattleSpawnWorldFromJoinOrderPlay(mapData, joinIdx);
x = pos.x;
y = pos.y;
} else {
const sp = pickSpawnForJoinPlay(mapData, joinIdx);
const jx = (Math.random() - 0.5) * 0.4;
@@ -4568,11 +4722,6 @@
x = sp.x + 0.5 + jx;
y = sp.y + 0.5 + jy;
}
if (mapData.gameType === 'quiz_battle' && quizBattlePathModeActive(mapData)) {
const pathSnap = snapPositionOntoQuizBattlePathIfNeeded(x, y);
x = pathSnap.x;
y = pathSnap.y;
}
const tierRoll = Math.random();
const stackPrev = mapData && mapData.gameType === 'stack';
const botTier = stackPrev
@@ -12417,12 +12566,13 @@
return false;
}
/** ทุกช่องที่สไปรต์กินพื้นที่ต้องอยู่บนเลน — กันหลุดไปพื้นเปิด (path=0) หรือทับกำแพง (objects=1) */
function quizBattleFootprintFullyOnPath(md, px, py) {
if (!md || !quizBattlePathModeActive(md)) return true;
if (typeof px !== 'number' || typeof py !== 'number' || !Number.isFinite(px) || !Number.isFinite(py)) return false;
const tiles = quizTilesFootprintPlay(px, py);
if (tiles.size === 0) return false;
const g = md.quizBattlePathArea;
const tiles = quizBattleSpriteBoundsTilesPlay(md, px, py);
if (tiles.size === 0) return false;
for (const k of tiles) {
const p = k.split(',');
const tx = +p[0], ty = +p[1];
@@ -12431,6 +12581,61 @@
return true;
}
/** ยืน/เดินได้บนเลนม่วง หรือทุกช่องสไปรต์อยู่ใน spawnArea (ฟ้า) — โซนรอก่อนลงเลน */
function quizBattlePositionAllowedPlay(md, px, py) {
if (!md || !quizBattlePathModeActive(md)) return true;
if (typeof px !== 'number' || typeof py !== 'number' || !Number.isFinite(px) || !Number.isFinite(py)) return false;
if (quizBattleFootprintFullyOnPath(md, px, py)) return true;
const sa = md.spawnArea;
if (!sa || !Array.isArray(sa)) return false;
const tiles = quizBattleSpriteBoundsTilesPlay(md, px, py);
if (tiles.size === 0) return false;
for (const k of tiles) {
const p = k.split(',');
const tx = +p[0], ty = +p[1];
if (!sa[ty] || sa[ty][tx] !== 1) return false;
}
return true;
}
/** สปอว์นในพื้นที่ฟ้า — เช็คแค่ไม่ทับกำแพง ไม่บังคับอยู่บนเลน */
function quizBattleSpawnWallClearPlay(md, nx, ny) {
if (!md || !md.objects) return false;
if (typeof nx !== 'number' || typeof ny !== 'number' || !Number.isFinite(nx) || !Number.isFinite(ny)) return false;
const w = md.width || 20, h = md.height || 15;
const prevMap = mapData;
mapData = md;
try {
for (const k of quizTilesWallCollisionFootprintPlay(nx, ny)) {
const p = k.split(',');
const tx = +p[0], ty = +p[1];
if (tx < 0 || tx >= w || ty < 0 || ty >= h) return false;
const row = md.objects[ty];
if (!row || row[tx] === 1) return false;
}
return true;
} finally {
mapData = prevMap;
}
}
/** เดินทีละย่อย — เลนบาง 1 ช่อง + กล่องสไปรต์ใหญ่กว่าเท้า ยังเดินได้โดยไม่หลุดเลน */
function quizBattleTryStepMovePlay(fx, fy, tx, ty, canWalkFn) {
const SUB = 8;
let x = fx;
let y = fy;
for (let i = 1; i <= SUB; i++) {
const t = i / SUB;
const nx = fx + (tx - fx) * t;
const ny = fy + (ty - fy) * t;
if (canWalkFn(nx, ny, x, y)) {
x = nx;
y = ny;
}
}
return { x, y };
}
/** สแน็ปบนเลน — เช็คแค่กำแพง + อยู่บน path (ให้ตรงกับ server) ไม่ใช้ blockPlayer/ผู้เล่นคนอื่น เพราะจะทำให้หาจุดสแน็ปไม่ได้แล้วค้างนอกเลน */
function quizBattleSnapTargetValidPlay(x, y) {
if (!mapData || !mapData.objects) return false;
@@ -12443,7 +12648,7 @@
const row = mapData.objects[ty];
if (!row || row[tx] === 1) return false;
}
if (quizBattlePathModeActive(mapData) && !quizBattleFootprintFullyOnPath(mapData, x, y)) return false;
if (quizBattlePathModeActive(mapData) && !quizBattlePositionAllowedPlay(mapData, x, y)) return false;
return true;
}
@@ -12464,18 +12669,34 @@
}
}
if (bestX != null) return { x: bestX, y: bestY };
/* footprint ใหญ่กว่าเลน (เช่น 2×2 บนทางแคบ 1 ช่อง): หาช่อง path ใกล้สุดที่เดินได้อย่างน้อยที่เซ็นเตอร์ — ดีกว่าปล่อยค้างนอกเลน */
/* footprint ใหญ่กว่าเลน: ลองจุดกลางช่อง — ต้องผ่านกล่องสไปรต์+path เหมือนก้อนบน (เดิมเช็คแค่ไม่ทับกำแพง → snap หลุดเลน) */
for (let ty = 0; ty < h; ty++) {
for (let tx = 0; tx < w; tx++) {
if (!g[ty] || g[ty][tx] !== 1) continue;
if (!spawnTileWalkablePlay(mapData, tx, ty)) continue;
const nx = tx + 0.5;
const ny = ty + 0.5;
if (!quizBattleSnapTargetValidPlay(nx, ny)) continue;
const d = Math.abs(nx - px) + Math.abs(ny - py);
if (d < bestD) { bestD = d; bestX = nx; bestY = ny; }
}
}
if (bestX != null) return { x: bestX, y: bestY };
const sa = mapData.spawnArea;
if (sa && Array.isArray(sa)) {
for (let ty = 0; ty < h; ty++) {
const row = sa[ty];
if (!row) continue;
for (let tx = 0; tx < w; tx++) {
if (row[tx] !== 1) continue;
for (const [nx, ny] of [[tx + 0.5, ty + 0.5], [tx + 0.01, ty + 0.01]]) {
if (!quizBattleSnapTargetValidPlay(nx, ny)) continue;
const d = Math.abs(nx - px) + Math.abs(ny - py);
if (d < bestD) { bestD = d; bestX = nx; bestY = ny; }
}
}
}
}
if (bestX != null) return { x: bestX, y: bestY };
return { x: px, y: py };
}
@@ -12488,6 +12709,20 @@
me.y = snapped.y;
}
/** สแน็ป peer / บอทหลัง lerp — กันตัวละครล้ำกำแพงหรือหลุดเลนระหว่างเฟรม */
function enforceQuizBattleLaneOnPeersPlay() {
if (!mapData || !isQuizBattle() || !quizBattlePathModeActive(mapData)) return;
others.forEach((o) => {
if (!o) return;
if (quizBattleSnapTargetValidPlay(o.x, o.y)) return;
const sn = snapPositionOntoQuizBattlePathIfNeeded(o.x, o.y);
o.x = sn.x;
o.y = sn.y;
o.tx = sn.x;
o.ty = sn.y;
});
}
function hideQuizBattleMcqModal() {
const ov = document.getElementById('quiz-battle-mcq-overlay');
if (ov) {
@@ -14578,6 +14813,45 @@
}
/** ทดสอบจากเอดิเตอร์: วางคน+บอทบนแพลตฟอร์ม (ไม่ให้เกิดบน spawnArea บนฟ้าแล้วลอย) */
/** Quiz Battle พรีวิว: บอท spawn ตามโหมดแมป — ผู้เล่นจริงใช้ตำแหน่งจาก server แล้ว snap เท่าที่จำเป็น */
function applyQuizBattlePreviewSpawnLayout(onlyBots) {
if (!mapData || mapData.gameType !== 'quiz_battle' || !quizBattlePathModeActive(mapData)) return;
function stampQuizEnt(ent, joinOrd) {
if (!ent) return;
const jo = Number.isFinite(Number(joinOrd)) ? Math.max(0, Math.floor(Number(joinOrd))) : 0;
const pos = quizBattleSpawnWorldFromJoinOrderPlay(mapData, jo);
ent.x = pos.x;
ent.y = pos.y;
ent.tx = pos.x;
ent.ty = pos.y;
}
if (!onlyBots) {
const sn = snapPositionOntoQuizBattlePathIfNeeded(me.x, me.y);
me.x = sn.x;
me.y = sn.y;
me.tx = sn.x;
me.ty = sn.y;
}
const realIds = [...others.keys()].filter((id) => !isPreviewBotId(id)).sort();
const botIds = [...others.keys()].filter(isPreviewBotId).sort();
if (!onlyBots) {
realIds.forEach((rid) => {
const o = others.get(rid);
if (!o) return;
const jo = Number.isFinite(Number(o.spawnJoinOrder)) ? o.spawnJoinOrder : realIds.indexOf(rid);
stampQuizEnt(o, jo);
});
}
botIds.forEach((bid, bi) => {
const o = others.get(bid);
if (!o) return;
const jo = Number.isFinite(Number(o.spawnJoinOrder)) ? o.spawnJoinOrder : countPlayHumans() + bi;
stampQuizEnt(o, jo);
});
}
function applyJumpSurvivePreviewSpawnLayout(onlyBots) {
if (!mapData || mapData.gameType !== 'jump_survive') return;
@@ -15588,6 +15862,10 @@
gauntletEndsAtMs = null;
}
rebalancePreviewBots();
if (mapData.gameType === 'quiz_battle' && quizBattlePathModeActive(mapData)) {
applyQuizBattlePreviewSpawnLayout(false);
try { socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); } catch (eSync) { /* ignore */ }
}
if (mapData.gameType === 'space_shooter') {
applySpaceShooterSpawnLayoutPlay();
}
@@ -16890,6 +17168,33 @@
return { cw: leg, ch: leg };
}
/** Quiz Battle: กล่องชน/เส้นทางตามสไปรต์ที่วาดจริง (drawAvatar ใช้ cw×1.15, ch×1.35 จากเท้า) */
const QUIZ_BATTLE_SPRITE_COL_SCALE_W = 1.15;
const QUIZ_BATTLE_SPRITE_COL_SCALE_H = 1.35;
function quizBattleSpriteBoundsTilesPlay(md, px, py) {
const s = new Set();
if (!md || md.gameType !== 'quiz_battle') return s;
if (typeof px !== 'number' || typeof py !== 'number' || !Number.isFinite(px) || !Number.isFinite(py)) return s;
const w = md.width || 20, h = md.height || 15;
const { cw, ch } = getCharacterFootprintWH(md);
const cx = px + cw * 0.5;
const feetY = py + ch;
const left = cx - (cw * QUIZ_BATTLE_SPRITE_COL_SCALE_W) / 2;
const right = cx + (cw * QUIZ_BATTLE_SPRITE_COL_SCALE_W) / 2;
const top = feetY - ch * QUIZ_BATTLE_SPRITE_COL_SCALE_H;
const bottom = feetY;
if (left < 0 || right > w || top < 0 || bottom > h) return s;
const minTx = Math.floor(left);
const maxTx = Math.min(w - 1, Math.floor(right - 1e-6));
const minTy = Math.floor(top);
const maxTy = Math.min(h - 1, Math.floor(bottom - 1e-6));
for (let ty = minTy; ty <= maxTy; ty++) {
for (let tx = minTx; tx <= maxTx; tx++) s.add(tx + ',' + ty);
}
return s;
}
/**
* ขนาดชองทใชชนกำแพง (objects=1) เทาน ดเท (าง) + กลางแนวนอน · เลกกว footprint = วนบนไมดกำแพง
*/
@@ -17040,6 +17345,9 @@
/** Footprint ชนกำแพง (objects=1) เท่านั้น — hub / interactive / blockPlayer / quiz ใช้ quizTilesFootprintPlay = เต็ม characterCells */
function quizTilesWallCollisionFootprintPlay(px, py) {
if (mapData && mapData.gameType === 'quiz_battle') {
return quizBattleSpriteBoundsTilesPlay(mapData, px, py);
}
const s = new Set();
if (!mapData) return s;
if (typeof px !== 'number' || typeof py !== 'number' || !Number.isFinite(px) || !Number.isFinite(py)) return s;
@@ -17133,6 +17441,7 @@
if (typeof x !== 'number' || typeof y !== 'number' || !Number.isFinite(x) || !Number.isFinite(y)) return false;
const w = mapData.width || 20, h = mapData.height || 15;
const wallTiles = quizTilesWallCollisionFootprintPlay(x, y);
if (wallTiles.size === 0) return false;
for (const k of wallTiles) {
const p = k.split(',');
const tx = +p[0], ty = +p[1];
@@ -17140,7 +17449,10 @@
const row = mapData.objects[ty];
if (!row || row[tx] === 1) return false;
}
if (wallTiles.size === 0) return false;
if (!isQuizBattle()) {
const { colW, colH } = getCharacterCollisionFootprintWH(mapData);
if (wallTiles.size < colW * colH) return false;
}
const bp = mapData.blockPlayer;
if (bp) {
for (const k of quizTilesFootprintPlay(x, y)) {
@@ -17162,7 +17474,7 @@
}
if (isQuizCarry() && quizCarryFootprintOverlapsHub(x, y)) return false;
if (isQuizBattle() && quizBattlePathModeActive(mapData)) {
if (!quizBattleFootprintFullyOnPath(mapData, x, y)) return false;
if (!quizBattlePositionAllowedPlay(mapData, x, y)) return false;
}
return true;
}
@@ -17180,6 +17492,10 @@
if (!row || row[tx] === 1) return false;
}
if (wallTilesB.size === 0) return false;
if (!isQuizBattle()) {
const { colW, colH } = getCharacterCollisionFootprintWH(mapData);
if (wallTilesB.size < colW * colH) return false;
}
const bp = mapData.blockPlayer;
if (bp) {
for (const k of quizTilesFootprintPlay(x, y)) {
@@ -17206,7 +17522,7 @@
}
if (isQuizCarry() && quizCarryFootprintOverlapsHub(x, y)) return false;
if (isQuizBattle() && quizBattlePathModeActive(mapData)) {
if (!quizBattleFootprintFullyOnPath(mapData, x, y)) return false;
if (!quizBattlePositionAllowedPlay(mapData, x, y)) return false;
}
return true;
}
@@ -17263,10 +17579,14 @@
else if (Math.abs(dx) > 1e-6) o.direction = dx > 0 ? 'right' : 'left';
const ox = o.x, oy = o.y;
const pathStrict = isQuizBattle() && quizBattlePathModeActive(mapData);
if (canWalkLikeLobbyForBot(nx, ny, o.x, o.y, o)) {
if (pathStrict) {
const moved = quizBattleTryStepMovePlay(o.x, o.y, nx, ny, (px, py, fx, fy) => canWalkLikeLobbyForBot(px, py, fx, fy, o));
o.x = moved.x;
o.y = moved.y;
} else if (canWalkLikeLobbyForBot(nx, ny, o.x, o.y, o)) {
o.x = nx;
o.y = ny;
} else if (!pathStrict) {
} else {
if (canWalkLikeLobbyForBot(nx, o.y, o.x, o.y, o)) {
o.x = nx;
} else if (canWalkLikeLobbyForBot(o.x, ny, o.x, o.y, o)) {
@@ -17360,29 +17680,34 @@
if (Math.abs(accY) > Math.abs(accX)) o.direction = accY > 0 ? 'down' : 'up';
else if (accX !== 0) o.direction = accX > 0 ? 'right' : 'left';
const step = MOVE_SPEED;
const nx = o.x + accX * step;
const ny = o.y + accY * step;
const tox = o.x + accX * step;
const toy = o.y + accY * step;
const ox = o.x, oy = o.y;
const pathStrictW = isQuizBattle() && quizBattlePathModeActive(mapData);
if (canWalkLikeLobbyForBot(nx, ny, o.x, o.y, o)) {
o.x = nx;
o.y = ny;
} else if (!pathStrictW) {
if (canWalkLikeLobbyForBot(nx, o.y, o.x, o.y, o)) {
o.x = nx;
} else if (canWalkLikeLobbyForBot(o.x, ny, o.x, o.y, o)) {
o.y = ny;
if (pathStrictW) {
const moved = quizBattleTryStepMovePlay(o.x, o.y, tox, toy, (nx, ny, fx, fy) => canWalkLikeLobbyForBot(nx, ny, fx, fy, o));
o.x = moved.x;
o.y = moved.y;
if (Math.abs(o.x - ox) < 1e-5 && Math.abs(o.y - oy) < 1e-5) {
const d = pickRandomPreviewBotWanderDir();
o.botWanderDx = d[0];
o.botWanderDy = d[1];
o.botWanderNextTurn = now + 200 + Math.floor(Math.random() * 600);
}
} else if (canWalkLikeLobbyForBot(tox, toy, o.x, o.y, o)) {
o.x = tox;
o.y = toy;
} else {
if (canWalkLikeLobbyForBot(tox, o.y, o.x, o.y, o)) {
o.x = tox;
} else if (canWalkLikeLobbyForBot(o.x, toy, o.x, o.y, o)) {
o.y = toy;
} else {
const d = pickRandomPreviewBotWanderDir();
o.botWanderDx = d[0];
o.botWanderDy = d[1];
o.botWanderNextTurn = now + 200 + Math.floor(Math.random() * 600);
}
} else {
const d = pickRandomPreviewBotWanderDir();
o.botWanderDx = d[0];
o.botWanderDy = d[1];
o.botWanderNextTurn = now + 200 + Math.floor(Math.random() * 600);
}
if (!Number.isFinite(o.x)) o.x = 0.5;
if (!Number.isFinite(o.y)) o.y = 0.5;
@@ -20062,6 +20387,7 @@
if (isChatFocused()) {
me.isWalking = false;
enforceQuizBattleLaneOnMePlay();
enforceQuizBattleLaneOnPeersPlay();
draw();
requestAnimationFrame(tick);
return;
@@ -20094,8 +20420,7 @@
}
if (playPath.length === 0) {
me.isWalking = false;
me.x = Math.max(0, Math.min(w - 0.01, me.x));
me.y = Math.max(0, Math.min(h - 0.01, me.y));
clampPlayEntityFootprintToMap(me, mapData);
enforceQuizBattleLaneOnMePlay();
const t = Date.now();
if (t - lastSend > 80) { lastSend = t; socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); }
@@ -20141,24 +20466,27 @@
if (accX !== 0 || accY !== 0) {
const len = Math.sqrt(accX * accX + accY * accY) || 1;
const step = Math.min(moveSpeedTilesThisFrameForWalk(), len);
const nx = me.x + (accX / len) * step;
const ny = me.y + (accY / len) * step;
/** Quiz Battle + เส้นทาง: ห้ามเลื่อนแกนเดียว (slide) — ไม่งั้นเดินทแยงหลุดออกนอกเลนได้ */
const tox = me.x + (accX / len) * step;
const toy = me.y + (accY / len) * step;
const pathStrict = isQuizBattle() && quizBattlePathModeActive(mapData);
if (canWalkLikeLobby(nx, ny, me.x, me.y)) {
me.x = nx;
me.y = ny;
} else if (!pathStrict) {
if (canWalkLikeLobby(nx, me.y, me.x, me.y)) {
me.x = nx;
} else if (canWalkLikeLobby(me.x, ny, me.x, me.y)) {
me.y = ny;
if (pathStrict) {
const moved = quizBattleTryStepMovePlay(me.x, me.y, tox, toy, (nx, ny, ox, oy) => canWalkLikeLobby(nx, ny, ox, oy));
me.x = moved.x;
me.y = moved.y;
} else if (canWalkLikeLobby(tox, toy, me.x, me.y)) {
me.x = tox;
me.y = toy;
} else {
if (canWalkLikeLobby(tox, me.y, me.x, me.y)) {
me.x = tox;
} else if (canWalkLikeLobby(me.x, toy, me.x, me.y)) {
me.y = toy;
}
}
}
me.x = Math.max(0, Math.min(w - 0.01, me.x));
me.y = Math.max(0, Math.min(h - 0.01, me.y));
clampPlayEntityFootprintToMap(me, mapData);
enforceQuizBattleLaneOnMePlay();
enforceQuizBattleLaneOnPeersPlay();
const movedThisTick = Math.abs(me.x - preWalkX) > 1e-5 || Math.abs(me.y - preWalkY) > 1e-5;
me.isWalking = !!(accX !== 0 || accY !== 0) || playPath.length > 0 || movedThisTick;
const now = Date.now();
+1 -1
View File
@@ -3974,7 +3974,7 @@
<script src="/app-base.js?v=2"></script>
<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.0512"></script>
<script src="js/play.js?v=0.0520"></script>
<div class="version-tag">v —</div>
</body>
</html>
+9 -2
View File
@@ -6286,16 +6286,23 @@
const tx = +p[0], ty = +p[1];
if (!g[ty] || g[ty][tx] !== 1) return false;
}
/* กันเดินลงเกิน 1 ช่อง: เท้า (จุดล่างของตัว ≈ py+0.5) ต้องอยู่บนเส้นทางด้วย → หยุดในครึ่งบนของช่องล่างสุด ไม่ล้ำลงช่องกำแพง */
/* กันเดินลง/ข้ามขอบเลน: เท้า (≈ py+0.5 / px+0.5) ต้องอยู่บนเส้นทาง — หยุดก่อนล้ำช่องถัดไปที่ไม่ใช่ path */
const h = md.height || 15, w = md.width || 20;
const { cw, ch } = getCharacterFootprintWH(md);
const fy = Math.floor(py + 0.5);
if (fy >= 0 && fy < h) {
const { cw } = getCharacterFootprintWH(md);
const minTx = Math.floor(px), maxTx = Math.min(w - 1, minTx + cw - 1);
for (let tx = Math.max(0, minTx); tx <= maxTx; tx++) {
if (!g[fy] || g[fy][tx] !== 1) return false;
}
}
const fx = Math.floor(px + 0.5);
if (fx >= 0 && fx < w) {
const minTy = Math.floor(py), maxTy = Math.min(h - 1, minTy + ch - 1);
for (let ty = Math.max(0, minTy); ty <= maxTy; ty++) {
if (!g[ty] || g[ty][fx] !== 1) return false;
}
}
return true;
}
+215 -12
View File
@@ -2426,7 +2426,35 @@ function getCharacterCollisionFootprintWHForMove(md) {
return { cw, ch, colW, colH };
}
const QUIZ_BATTLE_SPRITE_COL_SCALE_W = 1.15;
const QUIZ_BATTLE_SPRITE_COL_SCALE_H = 1.35;
function quizBattleSpriteBoundsTileKeys(md, px, py) {
const out = [];
if (!md || md.gameType !== 'quiz_battle') return out;
const x = Number(px), y = Number(py);
if (!Number.isFinite(x) || !Number.isFinite(y)) return out;
const w = md.width || 20, h = md.height || 15;
const { cw, ch } = getCharacterFootprintWHForMove(md);
const cx = x + cw * 0.5;
const feetY = y + ch;
const left = cx - (cw * QUIZ_BATTLE_SPRITE_COL_SCALE_W) / 2;
const right = cx + (cw * QUIZ_BATTLE_SPRITE_COL_SCALE_W) / 2;
const top = feetY - ch * QUIZ_BATTLE_SPRITE_COL_SCALE_H;
const bottom = feetY;
if (left < 0 || right > w || top < 0 || bottom > h) return out;
const minTx = Math.floor(left);
const maxTx = Math.min(w - 1, Math.floor(right - 1e-6));
const minTy = Math.floor(top);
const maxTy = Math.min(h - 1, Math.floor(bottom - 1e-6));
for (let ty = minTy; ty <= maxTy; ty++) {
for (let tx = minTx; tx <= maxTx; tx++) out.push(`${tx},${ty}`);
}
return out;
}
function serverWallCollisionTileKeys(md, px, py) {
if (md && md.gameType === 'quiz_battle') return quizBattleSpriteBoundsTileKeys(md, px, py);
const w = md.width || 20;
const h = md.height || 15;
const { cw, ch, colW, colH } = getCharacterCollisionFootprintWHForMove(md);
@@ -3707,16 +3735,36 @@ function quizBattlePathModeActiveServer(m) {
function quizBattleFootprintFullyOnPathServer(m, px, py) {
if (!quizBattlePathModeActiveServer(m)) return true;
const g = m.quizBattlePathArea;
for (const k of quizCarryFootprintTileKeys(m, px, py)) {
const keys = quizBattleSpriteBoundsTileKeys(m, px, py);
if (!keys.length) return false;
for (const k of keys) {
const [tx, ty] = k.split(',').map(Number);
if (!g[ty] || g[ty][tx] !== 1) return false;
}
return true;
}
/** ยืน/เดินได้บนเลนม่วง หรือทุกช่องสไปรต์อยู่ใน spawnArea (ฟ้า) */
function quizBattlePositionAllowedServer(m, px, py) {
if (!quizBattlePathModeActiveServer(m)) return true;
if (typeof px !== 'number' || typeof py !== 'number' || !Number.isFinite(px) || !Number.isFinite(py)) return false;
if (quizBattleFootprintFullyOnPathServer(m, px, py)) return true;
const sa = m.spawnArea;
if (!sa || !Array.isArray(sa)) return false;
const keys = quizBattleSpriteBoundsTileKeys(m, px, py);
if (!keys.length) return false;
for (const k of keys) {
const [tx, ty] = k.split(',').map(Number);
if (!sa[ty] || sa[ty][tx] !== 1) return false;
}
return true;
}
function serverFootprintClearOfWalls(md, px, py) {
const w = md.width || 20, h = md.height || 15;
for (const k of serverWallCollisionTileKeys(md, px, py)) {
const keys = serverWallCollisionTileKeys(md, px, py);
if (!keys.length) return false;
for (const k of keys) {
const [tx, ty] = k.split(',').map(Number);
if (tx < 0 || tx >= w || ty < 0 || ty >= h) return false;
const row = md.objects && md.objects[ty];
@@ -3728,7 +3776,7 @@ function serverFootprintClearOfWalls(md, px, py) {
/** จุดใกล้สุดบนเส้นทาง ( footprint อยู่ใน path + ไม่ทับกำแพง ) — ใช้ตอน join / เริ่มเกม */
function snapPositionOntoQuizBattlePathServer(md, px, py) {
if (!md || md.gameType !== 'quiz_battle' || !quizBattlePathModeActiveServer(md)) return { x: px, y: py };
if (quizBattleFootprintFullyOnPathServer(md, px, py) && serverFootprintClearOfWalls(md, px, py)) return { x: px, y: py };
if (quizBattleSnapTargetValidServer(md, px, py)) return { x: px, y: py };
const w = md.width || 20, h = md.height || 15;
const g = md.quizBattlePathArea;
let bestX = null, bestY = null, bestD = Infinity;
@@ -3747,18 +3795,167 @@ function snapPositionOntoQuizBattlePathServer(md, px, py) {
for (let ty = 0; ty < h; ty++) {
for (let tx = 0; tx < w; tx++) {
if (!g[ty] || g[ty][tx] !== 1) continue;
const row = md.objects && md.objects[ty];
if (!row || row[tx] === 1) continue;
const nx = tx + 0.5;
const ny = ty + 0.5;
if (!quizBattleFootprintFullyOnPathServer(md, nx, ny)) continue;
if (!serverFootprintClearOfWalls(md, nx, ny)) continue;
const d = Math.abs(nx - px) + Math.abs(ny - py);
if (d < bestD) { bestD = d; bestX = nx; bestY = ny; }
}
}
if (bestX != null) return { x: bestX, y: bestY };
const sa = md.spawnArea;
if (sa && Array.isArray(sa)) {
for (let ty = 0; ty < h; ty++) {
const row = sa[ty];
if (!row) continue;
for (let tx = 0; tx < w; tx++) {
if (row[tx] !== 1) continue;
for (const [nx, ny] of [[tx + 0.5, ty + 0.5], [tx + 0.01, ty + 0.01]]) {
if (!quizBattleSnapTargetValidServer(md, nx, ny)) continue;
const d = Math.abs(nx - px) + Math.abs(ny - py);
if (d < bestD) { bestD = d; bestX = nx; bestY = ny; }
}
}
}
}
if (bestX != null) return { x: bestX, y: bestY };
return { x: px, y: py };
}
function collectQuizBattleValidSpawnCentersServer(md) {
const out = [];
if (!md || md.gameType !== 'quiz_battle' || !quizBattlePathModeActiveServer(md)) return out;
const w = md.width || 20, h = md.height || 15;
const g = md.quizBattlePathArea;
const seen = new Set();
for (let ty = 0; ty < h; ty++) {
for (let tx = 0; tx < w; tx++) {
if (!g[ty] || g[ty][tx] !== 1) continue;
for (const [nx, ny] of [[tx + 0.5, ty + 0.5], [tx + 0.01, ty + 0.01]]) {
const key = nx.toFixed(3) + ',' + ny.toFixed(3);
if (seen.has(key)) continue;
if (!quizBattleFootprintFullyOnPathServer(md, nx, ny)) continue;
if (!serverFootprintClearOfWalls(md, nx, ny)) continue;
seen.add(key);
out.push({ tx, ty, x: nx, y: ny });
}
}
}
out.sort((a, b) => a.ty - b.ty || a.tx - b.tx || a.x - b.x);
return out;
}
function quizBattleSnapTargetValidServer(md, x, y) {
if (!md || !md.objects) return false;
if (typeof x !== 'number' || typeof y !== 'number' || !Number.isFinite(x) || !Number.isFinite(y)) return false;
if (!serverFootprintClearOfWalls(md, x, y)) return false;
if (quizBattlePathModeActiveServer(md) && !quizBattlePositionAllowedServer(md, x, y)) return false;
return true;
}
function quizBattleNearestValidSpawnWorldServer(md, prefTx, prefTy) {
const ptx = Math.floor(Number(prefTx)) || 0;
const pty = Math.floor(Number(prefTy)) || 0;
for (const [nx, ny] of [[ptx + 0.5, pty + 0.5], [ptx + 0.01, pty + 0.01]]) {
if (serverFootprintClearOfWalls(md, nx, ny)) return { x: nx, y: ny };
}
const pool = collectQuizBattleSpawnPoolServer(md);
if (pool.length) {
let best = pool[0], bestD = Infinity;
for (const c of pool) {
const d = Math.abs(c.tx - ptx) + Math.abs(c.ty - pty);
if (d < bestD) { bestD = d; best = c; }
}
return { x: best.x, y: best.y };
}
const valid = collectQuizBattleValidSpawnCentersServer(md);
if (!valid.length) return { x: ptx + 0.5, y: pty + 0.5 };
let best = valid[0], bestD = Infinity;
for (const c of valid) {
const d = Math.abs(c.tx - ptx) + Math.abs(c.ty - pty);
if (d < bestD) { bestD = d; best = c; }
}
return { x: best.x, y: best.y };
}
function collectQuizBattleSpawnPoolServer(md) {
const out = [];
if (!md) return out;
const w = md.width || 20, h = md.height || 15;
const grid = md.spawnArea;
const seen = new Set();
const cells = [];
if (grid && Array.isArray(grid)) {
for (let ty = 0; ty < h; ty++) {
const row = grid[ty];
if (!row) continue;
for (let tx = 0; tx < w; tx++) {
if (Number(row[tx]) === 1) cells.push({ tx, ty });
}
}
}
if (!cells.length && md.spawn) {
cells.push({
tx: Math.max(0, Math.min(w - 1, Math.floor(Number(md.spawn.x)) || 1)),
ty: Math.max(0, Math.min(h - 1, Math.floor(Number(md.spawn.y)) || 1)),
});
}
for (const { tx, ty } of cells) {
for (const [nx, ny] of [[tx + 0.5, ty + 0.5], [tx + 0.01, ty + 0.01]]) {
const key = nx.toFixed(3) + ',' + ny.toFixed(3);
if (seen.has(key)) continue;
if (quizBattlePathModeActiveServer(md)) {
if (!serverFootprintClearOfWalls(md, nx, ny)) continue;
} else if (!isMapTileWalkableForSpawn(md, tx, ty)) {
continue;
}
seen.add(key);
out.push({ tx, ty, x: nx, y: ny });
}
}
if (!out.length && quizBattlePathModeActiveServer(md)) {
return collectQuizBattleValidSpawnCentersServer(md);
}
return out;
}
function pickQuizBattleRandomSpawnWorldServer(md) {
const pool = collectQuizBattleSpawnPoolServer(md);
const fb = md.spawn || { x: 1, y: 1 };
if (!pool.length) {
return quizBattleNearestValidSpawnWorldServer(md, fb.x, fb.y);
}
const idx = typeof crypto.randomInt === 'function'
? crypto.randomInt(0, pool.length)
: Math.floor(Math.random() * pool.length);
const pick = pool[idx];
return { x: pick.x, y: pick.y };
}
function quizBattleSpawnWorldFromJoinOrderServer(md, joinOrderIndex) {
if (!md) return { x: 1.5, y: 1.5 };
const ord = joinOrderIndex | 0;
const mode = md.lobbySpawnMode;
if (mode === 'slots6') {
const slots = parseLobbyPlayerSpawnsFromMap(md);
const j = Math.min(Math.max(0, ord), 5);
const slot = slots[j];
if (slot) return quizBattleNearestValidSpawnWorldServer(md, slot.x, slot.y);
return pickQuizBattleRandomSpawnWorldServer(md);
}
if (mode === 'fixed' && md.spawn) {
const sx = Number(md.spawn.x) || 1, sy = Number(md.spawn.y) || 1;
return quizBattleNearestValidSpawnWorldServer(md, sx, sy);
}
return pickQuizBattleRandomSpawnWorldServer(md);
}
function pickQuizBattleSpawnFromMap(md, joinOrderIndex) {
const world = quizBattleSpawnWorldFromJoinOrderServer(md, joinOrderIndex);
return { x: Math.floor(world.x), y: Math.floor(world.y) };
}
function normalizeJumpSurvivePlatformAreaOnMap(m) {
if (!m || m.gameType !== 'jump_survive') return;
const w = m.width || 20, h = m.height || 15;
@@ -5760,6 +5957,10 @@ function augmentLobbySlotsFromShooterPaintJumpSurvive(md, slots6) {
*/
function pickSpawnForJoin(md, joinOrderIndex) {
if (!md) return { x: 1, y: 1 };
if (md.gameType === 'quiz_battle' && quizBattlePathModeActiveServer(md)) {
const qb = pickQuizBattleSpawnFromMap(md, joinOrderIndex);
if (qb) return qb;
}
const mode = md.lobbySpawnMode;
const ord = joinOrderIndex | 0;
if (mode === 'slots6' && ord >= 6) return pickRandomSpawnFromMap(md);
@@ -6287,7 +6488,7 @@ io.on('connection', (socket) => {
balloonBossScore: 0, balloonBossBossDmg: 0, balloonBossBalloons: mdJoin.gameType === 'balloon_boss' ? bbStartBalloons : 5, balloonBossEliminated: false,
};
if (mdJoin.gameType === 'quiz_battle' && quizBattlePathModeActiveServer(mdJoin)) {
const sn = snapPositionOntoQuizBattlePathServer(mdJoin, Number(peer.x) + 0.5, Number(peer.y) + 0.5);
const sn = quizBattleSpawnWorldFromJoinOrderServer(mdJoin, spawnJoinOrder);
peer.x = sn.x;
peer.y = sn.y;
}
@@ -7309,19 +7510,21 @@ io.on('connection', (socket) => {
ny = p.y;
}
}
if (p && md && md.gameType === 'quiz_battle' && quizBattlePathModeActiveServer(md)) {
if (p && md && md.gameType === 'quiz_battle') {
const txN = Number(nx);
const tyN = Number(ny);
if (!Number.isFinite(txN) || !Number.isFinite(tyN)) {
if (!Number.isFinite(txN) || !Number.isFinite(tyN) || !serverFootprintClearOfWalls(md, txN, tyN)) {
nx = p.x;
ny = p.y;
} else if (!quizBattleFootprintFullyOnPathServer(md, txN, tyN)) {
} else if (quizBattlePathModeActiveServer(md) && !quizBattlePositionAllowedServer(md, txN, tyN)) {
nx = p.x;
ny = p.y;
}
const sn = snapPositionOntoQuizBattlePathServer(md, Number(nx), Number(ny));
nx = sn.x;
ny = sn.y;
if (quizBattlePathModeActiveServer(md)) {
const sn = snapPositionOntoQuizBattlePathServer(md, Number(nx), Number(ny));
nx = sn.x;
ny = sn.y;
}
}
if (p && md && md.gameType === 'space_shooter' && data && data.spaceShooterScore != null) {
const ns = Math.floor(Number(data.spaceShooterScore));