game 50 users
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user