game flow1.1

This commit is contained in:
2026-05-21 06:06:33 +00:00
parent 21cbea0fad
commit e46cd54645
10 changed files with 1124 additions and 229 deletions
+1
View File
@@ -171,6 +171,7 @@
name: name,
isPrivate: isPrivate,
maxPlayers: maxPlayersForSlot(selectedSlot),
botSlotCount: botCountForSlot(selectedSlot),
};
fetch(SERVER + '/api/spaces', {
method: 'POST',
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -398,7 +398,7 @@
</div>
</div>
<script src="js/version.js?v=0.0258"></script>
<script src="js/editor.js?v=0.0333"></script>
<script src="js/editor.js?v=0.0334"></script>
<div class="version-tag">v —</div>
</body>
</html>
+34 -3
View File
@@ -1469,6 +1469,18 @@
lobbyPlayerSpawns.length = 6;
}
function lobbySpawnFootprintFitsInEditor(x, y) {
const fp = readCharacterFootprintInputs();
const effW = Math.max(1, Math.min(fp.cw, width - x));
const effH = Math.max(1, Math.min(fp.ch, height - y));
for (let ty = y; ty < y + effH; ty++) {
for (let tx = x; tx < x + effW; tx++) {
if (objects[ty] && objects[ty][tx] === 1) return false;
}
}
return true;
}
function sanitizeLobbyPlayerSpawnsInEditor() {
ensureLobbyPlayerSpawnsLength();
for (let i = 0; i < 6; i++) {
@@ -1477,7 +1489,7 @@
const x = Math.floor(Number(s.x));
const y = Math.floor(Number(s.y));
if (!Number.isFinite(x) || !Number.isFinite(y) || x < 0 || x >= width || y < 0 || y >= height
|| (objects[y] && objects[y][x] === 1)) {
|| !lobbySpawnFootprintFitsInEditor(x, y)) {
lobbyPlayerSpawns[i] = null;
} else {
lobbyPlayerSpawns[i] = { x, y };
@@ -2745,8 +2757,20 @@
const drawLobbyPNum = (s, num) => {
if (!s || s.x < 0 || s.x >= width || s.y < 0 || s.y >= height) return;
if (objects[s.y][s.x] === 1) return;
const cx = s.x * tileSize + tileSize / 2;
const cy = s.y * tileSize + tileSize / 2 + 4;
const fpP = readCharacterFootprintInputs();
const effW = Math.max(1, Math.min(fpP.cw, width - s.x));
const effH = Math.max(1, Math.min(fpP.ch, height - s.y));
const tx0 = s.x * tileSize;
const ty0 = s.y * tileSize;
const rw = effW * tileSize;
const rh = effH * tileSize;
ctx.fillStyle = 'rgba(0, 120, 180, 0.28)';
ctx.fillRect(tx0 + 1, ty0 + 1, rw - 2, rh - 2);
ctx.strokeStyle = '#7dcfff';
ctx.lineWidth = 2;
ctx.strokeRect(tx0 + 1, ty0 + 1, rw - 2, rh - 2);
const cx = tx0 + rw / 2;
const cy = ty0 + rh - tileSize * 0.35;
ctx.fillStyle = 'rgba(0,80,120,0.55)';
ctx.beginPath();
ctx.arc(cx, cy - 3, 12, 0, Math.PI * 2);
@@ -3624,6 +3648,13 @@
blockPlayerSeparationH: bpSepSave.h,
ground, objects, blockPlayer, interactive, startGameArea, spawnArea, spawn, cellColors, showMapInGame,
lobbySpawnMode: (() => {
ensureLobbyPlayerSpawnsLength();
sanitizeLobbyPlayerSpawnsInEditor();
let hasP = false;
for (let pi = 0; pi < 6; pi++) {
if (lobbyPlayerSpawns[pi]) { hasP = true; break; }
}
if (hasP) return 'slots6';
const el = document.getElementById('lobby-spawn-mode');
const v = el && el.value;
return (v === 'fixed' || v === 'slots6') ? v : 'random';
+237 -89
View File
@@ -241,7 +241,7 @@
})
.finally(() => {
try {
if (previewFillBots && mapData) rebalancePreviewBots();
if (playBotsEnabled() && mapData) rebalancePreviewBots();
} catch (e) { /* map ยังไม่พร้อม */ }
});
const lobbyLevelParam = params.get('lobbyLevel');
@@ -251,8 +251,88 @@
window.__detectiveLobbyMeta = { level: lobbyLevelParam, caseId: lobbyCaseParam };
} catch (e) { /* ignore */ }
}
const POST_CASE_LOBBY_MAP_ID = 'mn8nx46h';
function isDetectiveMinigamePlay() {
if (params.get('detectiveReturn') === '1') return true;
try {
return sessionStorage.getItem('detectiveMinigameReturn') === '1';
} catch (e) {
return false;
}
}
function buildRoomLobbyReturnHref() {
let h = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick);
h += '&map=' + encodeURIComponent(POST_CASE_LOBBY_MAP_ID);
const drParam = (params.get('displayRoom') || '').trim();
if (drParam) {
h += '&displayRoom=' + encodeURIComponent(drParam);
} else {
try {
const dr = localStorage.getItem('lastCreatedSpaceName');
if (dr) h += '&displayRoom=' + encodeURIComponent(dr);
} catch (e) { /* ignore */ }
}
try {
const meta = window.__detectiveLobbyMeta;
if (meta && meta.level != null && String(meta.level).trim()) {
h += '&lobbyLevel=' + encodeURIComponent(String(meta.level).trim());
}
if (meta && meta.caseId != null && String(meta.caseId).trim()) {
h += '&caseId=' + encodeURIComponent(String(meta.caseId).trim());
}
} catch (e2) { /* ignore */ }
try {
if (sessionStorage.getItem('detectiveMinigameReturn') === '1') {
h += '&detectiveReturn=1';
sessionStorage.removeItem('detectiveMinigameReturn');
}
} catch (e3) { /* ignore */ }
if (params.get('detectiveReturn') === '1' && h.indexOf('detectiveReturn') < 0) {
h += '&detectiveReturn=1';
}
return h;
}
function finishDetectiveMinigameAndReturnLobby() {
quizCarrySessionEnded = true;
cancelQuizCarryAnswerRoundTimeupAuto();
quizCarryAnswerTimeupAwaitNext = false;
hideQuizCarryTimeupOnDeskLayer();
try { sessionStorage.setItem('detectiveMinigameReturn', '1'); } catch (e) { /* ignore */ }
const go = function () {
window.location.href = buildRoomLobbyReturnHref();
};
if (socket && socket.connected) {
socket.emit('detective-minigame-finished', {}, function (res) {
go();
});
} else {
go();
}
}
/** จบมินิเกมโหมดสืบสวน → LobbyB; คืน true ถ้า redirect แล้ว */
function tryFinishDetectiveMinigameAndReturnLobby() {
if (!isDetectiveMinigamePlay()) return false;
finishDetectiveMinigameAndReturnLobby();
return true;
}
/** ทดสอบจากเอดิเตอร์: เติมบอทให้ครบจำนวน (รวมผู้เล่นจริง) — ?fillTotal=6 (ค่าเริ่ม 6) */
const previewFillBots = previewMode && editorEmbedReturn;
let detectiveCaseFillBots = false;
let detectiveBotSlotCount = 0;
function playBotsEnabled() {
return previewFillBots || detectiveCaseFillBots;
}
function playBotTargetHeadcount() {
if (previewFillBots) return previewTargetHeadcount;
if (detectiveCaseFillBots) return countPlayHumans() + detectiveBotSlotCount;
return countPlayHumans();
}
/** พรีวิวใน iframe เอดิเตอร์: ซูมมุมมองได้ (เดิมไม่มี — zDraw จาก zoom คงที่) */
const PLAY_EMBED_USER_ZOOM_MIN = 0.5;
const PLAY_EMBED_USER_ZOOM_MAX = 3;
@@ -277,7 +357,7 @@
let stackPreviewBotThinkUntil = 0;
function rebuildStackPreviewTurnOrder() {
if (!previewFillBots || !mapData || mapData.gameType !== 'stack') {
if (!playBotsEnabled() || !mapData || mapData.gameType !== 'stack') {
stackPreviewTurnOrder = null;
return;
}
@@ -292,14 +372,14 @@
}
function advanceStackPreviewTurn() {
if (!previewFillBots || !stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT) return;
if (!playBotsEnabled() || !stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT) return;
stackPreviewTurnIndex = (stackPreviewTurnIndex + 1) % STACK_PREVIEW_TURN_COUNT;
stackPreviewBotThinkUntil = 0;
markStackNextDropVisualDirty();
}
function isStackPreviewHumanTurn() {
if (!previewFillBots || !stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT) return true;
if (!playBotsEnabled() || !stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT) return true;
const cur = stackPreviewTurnOrder[stackPreviewTurnIndex];
return !!(cur && cur.kind === 'human');
}
@@ -311,7 +391,7 @@
}
function getStackDropActorSeat() {
if (previewFillBots && stackPreviewTurnOrder && stackPreviewTurnOrder.length === STACK_PREVIEW_TURN_COUNT) {
if (playBotsEnabled() && stackPreviewTurnOrder && stackPreviewTurnOrder.length === STACK_PREVIEW_TURN_COUNT) {
return getStackPreviewCurrentSeat();
}
return 1;
@@ -1005,7 +1085,7 @@
if (Date.now() - gauntletCrownRunwayBgStripFreezeSinceMs >= GAUNTLET_CROWN_RUNWAY_POST_STOP_MS) {
gauntletCrownRunwayBgFinishLatched = true;
gauntletCrownRunwayBgStripFreezeSinceMs = 0;
if (previewFillBots && gauntletCrownRunwayBgMapEligiblePlay() && !gauntletCrownRunwayClientMissionShown) {
if (playBotsEnabled() && gauntletCrownRunwayBgMapEligiblePlay() && !gauntletCrownRunwayClientMissionShown) {
gauntletCrownRunwayClientMissionShown = true;
window.setTimeout(function () {
try {
@@ -1398,12 +1478,12 @@
const STACK_PREVIEW_HUD_LOG_MAX = 8;
let stackPreviewHudLog = [];
function stackPreviewPushHudLog(line) {
if (!previewFillBots) return;
if (!playBotsEnabled()) return;
stackPreviewHudLog.push(String(line || '').slice(0, 96));
while (stackPreviewHudLog.length > STACK_PREVIEW_HUD_LOG_MAX) stackPreviewHudLog.shift();
}
function stackPreviewLogStackDrop(hit, actor) {
if (!previewFillBots || !hit) return;
if (!playBotsEnabled() || !hit) return;
const bid = actor && actor.botId;
let nm = 'NODE';
if (actor && actor.human) nm = (me.nickname || 'YOU').toUpperCase().replace(/\s+/g, '_').slice(0, 12);
@@ -2466,7 +2546,7 @@
}
if (!mine) return;
el.classList.remove('is-hidden');
const botExtra = previewFillBots && (botRight + botWrong > 0)
const botExtra = playBotsEnabled() && (botRight + botWrong > 0)
? ' · บอท ถูก ' + botRight + ' / ผิด ' + botWrong + ' (Bots: ' + botRight + ' right / ' + botWrong + ' wrong)'
: '';
if (mine.right) {
@@ -3502,7 +3582,7 @@
}
function clearPreviewBotAnswerPaths() {
if (!previewFillBots) return;
if (!playBotsEnabled()) return;
others.forEach((o, id) => {
if (!isPreviewBotId(id)) return;
o.botPath = [];
@@ -3511,7 +3591,7 @@
}
function resetPreviewBotsQuizState() {
if (!previewFillBots) return;
if (!playBotsEnabled()) return;
others.forEach((o, id) => {
if (!isPreviewBotId(id)) return;
o.quizCannotTrue = false;
@@ -3582,6 +3662,22 @@
return true;
}
function spawnFootprintFitsPlay(md, anchorX, anchorY) {
if (!spawnTileWalkablePlay(md, anchorX, anchorY)) return false;
const cellsW = Math.max(1, Math.min(4, Math.floor(Number(md.characterCellsW) || Number(md.characterCells) || 1)));
const cellsH = Math.max(1, Math.min(4, Math.floor(Number(md.characterCellsH) || Number(md.characterCells) || 1)));
const w = md.width || 20;
const h = md.height || 15;
const maxX = Math.min(w, anchorX + cellsW);
const maxY = Math.min(h, anchorY + cellsH);
for (let ty = anchorY; ty < maxY; ty++) {
for (let tx = anchorX; tx < maxX; tx++) {
if (!spawnTileWalkablePlay(md, tx, ty)) return false;
}
}
return true;
}
/** เหมือน server pickRandomSpawnFromMap — สุ่มด้วย crypto.getRandomValues */
function pickRandomSpawnFromMapPlay(md) {
const fallback = md.spawn || { x: 1, y: 1 };
@@ -3596,7 +3692,7 @@
const row = grid[y];
if (!row) continue;
for (let x = 0; x < w; x++) {
if (Number(row[x]) === 1 && spawnTileWalkablePlay(md, x, y)) pool.push({ x, y });
if (Number(row[x]) === 1 && spawnFootprintFitsPlay(md, x, y)) pool.push({ x, y });
}
}
if (!pool.length) return { x: fx, y: fy };
@@ -3664,14 +3760,14 @@
const h = md.height || 15;
const x = Math.max(0, Math.min(w - 1, fx));
const y = Math.max(0, Math.min(h - 1, fy));
if (spawnTileWalkablePlay(md, x, y)) return { x, y };
if (spawnFootprintFitsPlay(md, x, y)) return { x, y };
return pickRandomSpawnFromMapPlay(md);
}
if (mode === 'slots6') {
const slots = parseLobbyPlayerSpawnsFromMapPlay(md);
augmentLobbySlotsFromShooterPaintJumpSurvivePlay(md, slots);
const pick = slots[j];
if (pick && spawnTileWalkablePlay(md, pick.x, pick.y)) return { x: pick.x, y: pick.y };
if (pick && spawnFootprintFitsPlay(md, pick.x, pick.y)) return { x: pick.x, y: pick.y };
return pickRandomSpawnFromMapPlay(md);
}
return pickRandomSpawnFromMapPlay(md);
@@ -3813,9 +3909,9 @@
}
function rebalancePreviewBots() {
if (!previewFillBots || !mapData) return;
if (!playBotsEnabled() || !mapData) return;
const human = countPlayHumans();
const wantBots = Math.max(0, previewTargetHeadcount - human);
const wantBots = Math.max(0, playBotTargetHeadcount() - human);
[...others.keys()].filter(isPreviewBotId).forEach((bid) => {
const o = others.get(bid);
if (!o) return;
@@ -3981,7 +4077,7 @@
/** ช่วงตอบ: บอทสุ่มเดินไปโซนถูก/ผิด/เดินมั่ว ตามระดับ (ฉลาด/กลาง/พลาดบ่อย) */
function previewBotsPrepareAnswerRound() {
if (!previewFillBots || !mapData || !isQuiz() || !previewQuizCurrent) return;
if (!playBotsEnabled() || !mapData || !isQuiz() || !previewQuizCurrent) return;
const correctTrue = !!previewQuizCurrent.answerTrue;
const qt = mapData.quizTrueArea;
const qf = mapData.quizFalseArea;
@@ -4117,7 +4213,7 @@
choice,
score: playLiveQuizScores[myId] != null ? playLiveQuizScores[myId] : 0,
});
if (previewFillBots) {
if (playBotsEnabled()) {
others.forEach((o, id) => {
if (!isPreviewBotId(id)) return;
const br = quizResolveChoiceFromStanding(o.x, o.y, correctTrue);
@@ -4540,7 +4636,7 @@
}
others.forEach((o, id) => {
if (!o || o.gauntletEliminated) return;
if (!previewFillBots && isPreviewBotId(id)) return;
if (!playBotsEnabled() && isPreviewBotId(id)) return;
if (!Number.isFinite(o.x) || !Number.isFinite(o.y)) return;
ents.push({ cx: (o.x + cw * 0.5) * tileSizeRef, cy: (o.y + ch * 0.5) * tileSizeRef });
});
@@ -4576,7 +4672,7 @@
/** Preview bots only — full tail-off loss needs server support for real humans */
const GAUNTLET_CROWN_TAIL_OFFSCREEN_MARGIN_PX = 40;
function maybeEliminateGauntletPreviewBotsTailOffCameraPlay(camX, camY, zDrawVal) {
if (!previewFillBots || !isGauntletCrownHeistMapPlay() || !mapData) return;
if (!playBotsEnabled() || !isGauntletCrownHeistMapPlay() || !mapData) return;
if (gauntletCrownPregamePhase !== 'live') return;
const halfW = canvas.width / (2 * zDrawVal);
const worldMinX = camX - halfW;
@@ -5438,7 +5534,7 @@
finalScore: Math.max(0, Number(score) || 0),
});
}
if (previewFillBots && stackPreviewTurnOrder && stackPreviewTurnOrder.length === STACK_PREVIEW_TURN_COUNT) {
if (playBotsEnabled() && stackPreviewTurnOrder && stackPreviewTurnOrder.length === STACK_PREVIEW_TURN_COUNT) {
if (myId != null) {
pushEnt(myId, (me.nickname || nick || 'คุณ').trim() || 'คุณ', me.characterId || getPlayCharacterId(), me.stackPreviewHumanPts || 0);
}
@@ -5453,7 +5549,7 @@
}
others.forEach(function (o, id) {
if (!o) return;
if (previewFillBots && isPreviewBotId(id)) return;
if (playBotsEnabled() && isPreviewBotId(id)) return;
pushEnt(id, o.nickname, o.characterId, teamPts);
});
}
@@ -5507,7 +5603,7 @@
}
function stackTowerMissionMergePreviewBotsMission(mission) {
if (!mission || mission.uiSkin !== 'stack_tower' || !previewFillBots || !others || typeof others.forEach !== 'function') return mission;
if (!mission || mission.uiSkin !== 'stack_tower' || !playBotsEnabled() || !others || typeof others.forEach !== 'function') return mission;
const seen = new Set();
(mission.ranked || []).forEach(function (r) {
if (r && r.id != null) seen.add(String(r.id));
@@ -5867,7 +5963,7 @@
}
function jumpSurviveMergePreviewBotsMission(mission) {
if (!mission || mission.uiSkin !== 'jumper' || !previewFillBots || !others || typeof others.forEach !== 'function') return mission;
if (!mission || mission.uiSkin !== 'jumper' || !playBotsEnabled() || !others || typeof others.forEach !== 'function') return mission;
const seen = new Set();
(mission.ranked || []).forEach(function (r) {
if (r && r.id != null) seen.add(String(r.id));
@@ -6169,7 +6265,7 @@
}
function spaceShooterMergePreviewBotsMission(mission) {
if (!mission || mission.uiSkin !== 'violent_crime' || !previewFillBots || !others || typeof others.forEach !== 'function') return mission;
if (!mission || mission.uiSkin !== 'violent_crime' || !playBotsEnabled() || !others || typeof others.forEach !== 'function') return mission;
const seen = new Set();
(mission.ranked || []).forEach(function (r) {
if (r && r.id != null) seen.add(String(r.id));
@@ -6242,7 +6338,7 @@
}
function balloonBossMergePreviewBotsMission(mission) {
if (!mission || mission.uiSkin !== 'mega_virus' || !previewFillBots || !others || typeof others.forEach !== 'function') return mission;
if (!mission || mission.uiSkin !== 'mega_virus' || !playBotsEnabled() || !others || typeof others.forEach !== 'function') return mission;
const seen = new Set();
(mission.ranked || []).forEach(function (r) {
if (r && r.id != null) seen.add(String(r.id));
@@ -6615,7 +6711,7 @@
}
function quizQuestionMissionMergePreviewBotsMission(mission) {
if (!mission || mission.uiSkin !== 'question_mission' || !previewFillBots || !others || typeof others.forEach !== 'function') return mission;
if (!mission || mission.uiSkin !== 'question_mission' || !playBotsEnabled() || !others || typeof others.forEach !== 'function') return mission;
const seen = new Set();
(mission.ranked || []).forEach(function (r) {
if (r && r.id != null) seen.add(String(r.id));
@@ -7518,6 +7614,7 @@
if (quizCarrySessionEnded) return;
if (quizCarryPregameActive) return;
if (!isQuizCarry() || !quizCarryCurrent) return;
if (isDetectiveMinigamePlay()) return;
const phaseEl = document.getElementById('quiz-game-phase-label');
if (!phaseEl) return;
if (quizCarryAnswerTimeupAwaitNext && quizCarryAnswerRoundTimeupAdvanceAt > 0) {
@@ -7563,6 +7660,10 @@
o.botPath = [];
});
renderPlayQuizScoreboard(playLiveQuizScores);
if (isDetectiveMinigamePlay()) {
quizCarryAfterRoundResolved();
return;
}
quizCarryAnswerTimeupAwaitNext = true;
showQuizCarryAnswerRoundTimeupOverlay();
updateQuizCarryCarryPhaseHud();
@@ -7742,6 +7843,10 @@
/** หลังจบข้อหนึ่งข้อ (ถูกหรือหมดเวลา): พรีวิวนับรอบจนครบ carrySessionLength แล้วจบเซสชัน */
function quizCarryAfterRoundResolved() {
if (!isQuizCarry()) return;
if (isDetectiveMinigamePlay()) {
finishDetectiveMinigameAndReturnLobby();
return;
}
if (!previewMode) {
quizCarryPickNextQuestion();
return;
@@ -8260,7 +8365,8 @@
});
ov.classList.remove('is-hidden');
function goLobby() {
window.location.href = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick);
if (tryFinishDetectiveMinigameAndReturnLobby()) return;
window.location.href = buildRoomLobbyReturnHref();
}
if (btn) {
btn.onclick = () => {
@@ -8600,9 +8706,15 @@
s = await mergeQuizCarrySettingsFromDisk(s);
} catch (e) { /* ignore */ }
if (snap) {
if (s.carryReadMs == null && snap.carryReadMs != null) s.carryReadMs = snap.carryReadMs;
if (s.carryAnswerMs == null && snap.carryAnswerMs != null) s.carryAnswerMs = snap.carryAnswerMs;
if (s.carrySessionLength == null && snap.carrySessionLength != null) s.carrySessionLength = snap.carrySessionLength;
if (isDetectiveMinigamePlay()) {
if (snap.carryReadMs != null) s.carryReadMs = snap.carryReadMs;
if (snap.carryAnswerMs != null) s.carryAnswerMs = snap.carryAnswerMs;
s.carrySessionLength = 1;
} else {
if (s.carryReadMs == null && snap.carryReadMs != null) s.carryReadMs = snap.carryReadMs;
if (s.carryAnswerMs == null && snap.carryAnswerMs != null) s.carryAnswerMs = snap.carryAnswerMs;
if (s.carrySessionLength == null && snap.carrySessionLength != null) s.carrySessionLength = snap.carrySessionLength;
}
}
if (snap && snap.carryMapPanelTheme && typeof snap.carryMapPanelTheme === 'object') {
s.carryMapPanelTheme = snap.carryMapPanelTheme;
@@ -8678,6 +8790,8 @@
}
if (previewMode && editorEmbedReturn) {
beginQuizCarryEmbedPregame();
} else if (isDetectiveMinigamePlay()) {
quizCarryPickNextQuestion();
} else {
quizCarryPickNextQuestion();
}
@@ -8699,22 +8813,29 @@
}
const ov = document.getElementById('quiz-game-overlay');
if (ov) {
if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden');
else ov.classList.remove('is-hidden');
if (isDetectiveMinigamePlay() || (previewMode && editorEmbedReturn)) {
ov.classList.add('is-hidden');
ov.setAttribute('aria-hidden', 'true');
} else {
ov.classList.remove('is-hidden');
ov.setAttribute('aria-hidden', 'false');
}
}
const tEl = document.getElementById('quiz-game-timer');
if (tEl) tEl.textContent = '';
const leg = document.getElementById('quiz-play-legend');
if (leg) {
leg.textContent = quizCarryMapHasAnswerInteractive(mapData)
? 'แต่ละตัวเลือกมีคนถือได้คนเดียว — ส่งคำตอบที่โซน interactive (เขียว) · โซนกลางม่วงเป็นกำแพง · กด F หรือปุ่ม GRAB มุมขวาล่าง'
: 'แต่ละตัวเลือกมีคนถือได้คนเดียว — โซนกลางเป็นกำแพง · ยืนชิดขอบแล้วกด F หรือปุ่ม GRAB ส่งเมื่อถือป้าย';
leg.textContent = isDetectiveMinigamePlay()
? ''
: (quizCarryMapHasAnswerInteractive(mapData)
? 'แต่ละตัวเลือกมีคนถือได้คนเดียว — ส่งคำตอบที่โซน interactive (เขียว) · โซนกลางม่วงเป็นกำแพง · กด F หรือปุ่ม GRAB มุมขวาล่าง'
: 'แต่ละตัวเลือกมีคนถือได้คนเดียว — โซนกลางเป็นกำแพง · ยืนชิดขอบแล้วกด F หรือปุ่ม GRAB ส่งเมื่อถือป้าย');
}
resetQuizCarryPlayState();
if (previewMode) {
loadQuizCarryPoolAndStart();
} else {
playQuizText = 'เข้าห้องแล้ว — คำถามจากแมป (เล่นจริงแนะนำซิงก์ผ่านโฮสต์ในอนาคต)';
playQuizText = isDetectiveMinigamePlay() ? '' : 'เข้าห้องแล้ว — คำถามจากแมป (เล่นจริงแนะนำซิงก์ผ่านโฮสต์ในอนาคต)';
loadQuizCarryPoolAndStart();
}
renderPlayQuizScoreboard(playLiveQuizScores);
@@ -8829,7 +8950,7 @@
function stepQuizCarryPreviewBots() {
if (quizCarrySessionEnded) return;
if (quizCarryPregameActive) return;
if (!previewFillBots || !isQuizCarry() || !mapData) return;
if (!playBotsEnabled() || !isQuizCarry() || !mapData) return;
const w = mapData.width || 20, h = mapData.height || 15;
const nowPf = performance.now();
const pathfindMinGap = editorEmbedReturn ? 280 : 140;
@@ -9064,12 +9185,12 @@
scorePopups: [],
perfectStackX2Fx: null,
};
if (previewFillBots) {
if (playBotsEnabled()) {
me.stackPreviewHumanPts = 0;
stackPreviewHudLog = [];
}
ensureStackTowerSlingImagePlay();
if (towerMission || previewFillBots) {
if (towerMission || playBotsEnabled()) {
ensureStackTowerLifeHudImagesPlay();
ensureStackTowerHeartMinusImgPlay();
}
@@ -9162,7 +9283,7 @@
stackMini.progressPct = np;
markStackNextDropVisualDirty();
}
if (previewFillBots && previewAttribution) {
if (playBotsEnabled() && previewAttribution) {
if (previewAttribution.human) {
me.stackPreviewHumanPts = (me.stackPreviewHumanPts || 0) + hit.pts;
} else if (previewAttribution.botId) {
@@ -9191,7 +9312,7 @@
* Editor embed เทาน: ปร `stackMini.phase` ให sin·swingAmp ใกลดรองรบกอนปลอย บอทฉลาด/กลาง/พลาดบอย างค aim error
*/
function previewBotAlignStackSwingPhasePlay(m, botPeer) {
if (!previewFillBots || !m || !mapData) return;
if (!playBotsEnabled() || !m || !mapData) return;
const wMap = mapData.width || 20;
const hMap = mapData.height || 15;
const land = getStackAreaBoundsPlay(mapData.stackLandArea, wMap, hMap);
@@ -9388,11 +9509,11 @@
function stackHumanDropGateOkPlay() {
if (!stackMini || !isStack() || stackFall || stackMini.settling) return false;
if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase !== 'live') return false;
if (previewFillBots && mapData && mapData.gameType === 'stack' &&
if (playBotsEnabled() && mapData && mapData.gameType === 'stack' &&
(!stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT)) {
rebuildStackPreviewTurnOrder();
}
if (previewFillBots && !isStackPreviewHumanTurn()) return false;
if (playBotsEnabled() && !isStackPreviewHumanTurn()) return false;
if (Date.now() - stackMini.lastDropAt < 480) return false;
return true;
}
@@ -9402,8 +9523,8 @@
if (!stackHumanDropGateOkPlay()) return false;
ensureStackNextDropVisual();
const hit = computeStackDropResult(stackMini);
if (!startStackFallAnimation(hit, previewFillBots ? { human: true } : null)) return false;
if (previewFillBots) {
if (!startStackFallAnimation(hit, playBotsEnabled() ? { human: true } : null)) return false;
if (playBotsEnabled()) {
lastStackPreviewActorId = '__human__';
lastStackPreviewActorUntil = Date.now() + 900;
}
@@ -9416,7 +9537,7 @@
ensureStackNextDropVisual();
previewBotAlignStackSwingPhasePlay(stackMini, o);
const hit = computeStackDropResult(stackMini);
if (!startStackFallAnimation(hit, previewFillBots ? { botId } : null)) return;
if (!startStackFallAnimation(hit, playBotsEnabled() ? { botId } : null)) return;
o.stackBotGlowUntil = Date.now() + Math.max(900, 400 + (stackFall && stackFall.dur ? stackFall.dur : 800));
lastStackPreviewActorId = botId;
lastStackPreviewActorUntil = Date.now() + Math.max(1000, 500 + (stackFall && stackFall.dur ? stackFall.dur : 800));
@@ -9424,7 +9545,7 @@
/** Stack preview: สลับตา — เฉพาะบอทที่ถึงตาถึงจะจำลองปล่อยหนึ่งครั้งแล้วเลื่อนตา */
function stepPreviewStackTurnBased(nowMs) {
if (!previewFillBots || !isStack() || !stackMini || stackFall || stackMini.settling) return;
if (!playBotsEnabled() || !isStack() || !stackMini || stackFall || stackMini.settling) return;
if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase !== 'live') return;
if (mapData && mapData.gameType === 'stack' &&
(!stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT)) {
@@ -9467,13 +9588,13 @@
const wasHuman = !!s.previewHumanDrop;
const settleBotId = s.previewBotId || null;
applyStackHitFromDrop({ miss: true, perfect: false, pts: 0, landOffsetTiles: 0 }, undefined);
if (previewFillBots && (wasHuman || settleBotId)) {
if (playBotsEnabled() && (wasHuman || settleBotId)) {
stackPreviewLogStackDrop({ miss: true, perfect: false, pts: 0 }, { human: wasHuman, botId: settleBotId });
}
stackMini.settling = null;
stackMini.lastDropAt = Date.now();
markStackNextDropVisualDirty();
if (previewFillBots && (wasHuman || settleBotId)) advanceStackPreviewTurn();
if (playBotsEnabled() && (wasHuman || settleBotId)) advanceStackPreviewTurn();
}
} else if (stackFall) {
const t = (now - stackFall.t0) / stackFall.dur;
@@ -9484,13 +9605,13 @@
const stableMin = STACK_STABLE_SUPPORT_RATIO_MIN;
if (hit.miss) {
applyStackHitFromDrop(hit);
if (previewFillBots && (wasHuman || fallBotId)) {
if (playBotsEnabled() && (wasHuman || fallBotId)) {
stackPreviewLogStackDrop(hit, { human: wasHuman, botId: fallBotId });
}
stackMini.lastDropAt = Date.now();
stackFall = null;
markStackNextDropVisualDirty();
if (previewFillBots && (wasHuman || fallBotId)) advanceStackPreviewTurn();
if (playBotsEnabled() && (wasHuman || fallBotId)) advanceStackPreviewTurn();
} else if (hit.supportRatio != null && hit.supportRatio < stableMin) {
const stripH = stackMini.blockStripH || Math.max(16, tileSize * 0.32);
stackMini.settling = {
@@ -9510,18 +9631,18 @@
stackFall = null;
} else {
let previewAttr;
if (previewFillBots) {
if (playBotsEnabled()) {
if (wasHuman) previewAttr = { human: true };
else if (fallBotId) previewAttr = { botId: fallBotId };
}
applyStackHitFromDrop(hit, previewAttr, { seat: stackFall.actorSeat, heavy: stackFall.actorHeavy });
if (previewFillBots && (wasHuman || fallBotId)) {
if (playBotsEnabled() && (wasHuman || fallBotId)) {
stackPreviewLogStackDrop(hit, { human: wasHuman, botId: fallBotId });
}
stackMini.lastDropAt = Date.now();
stackFall = null;
markStackNextDropVisualDirty();
if (previewFillBots && (wasHuman || fallBotId)) advanceStackPreviewTurn();
if (playBotsEnabled() && (wasHuman || fallBotId)) advanceStackPreviewTurn();
}
}
} else if (stackMini) {
@@ -10177,7 +10298,7 @@
}
function applyGauntletPreviewBotsAfterSync() {
if (!previewFillBots || !mapData || mapData.gameType !== 'gauntlet') return;
if (!playBotsEnabled() || !mapData || mapData.gameType !== 'gauntlet') return;
if (isGauntletCrownHeistMapPlay() && isGauntletCrownPregameBlockingPlay()) return;
const w = mapData.width || 20;
const h = mapData.height || 15;
@@ -10190,7 +10311,7 @@
/** แจ้งเซิร์ฟเวอร์แถว y ที่มีคุณ+บอททดสอบ — lane spawn ใช้เฉพาะแถวที่มี peer; บอทไม่ใช่ peer */
function emitGauntletPreviewRowsToServer() {
if (!previewFillBots || !mapData || mapData.gameType !== 'gauntlet') return;
if (!playBotsEnabled() || !mapData || mapData.gameType !== 'gauntlet') return;
if (isGauntletCrownHeistMapPlay() && isGauntletCrownPregameBlockingPlay()) return;
const h = mapData.height || 15;
const ysSet = new Set();
@@ -10310,7 +10431,7 @@
gauntletCrownCountdownTimer = null;
}
gauntletCrownPregamePhase = 'countdown';
if (previewFillBots && mapData && mapData.gameType === 'gauntlet') {
if (playBotsEnabled() && mapData && mapData.gameType === 'gauntlet') {
applyGauntletPreviewSpawnLayout(false);
}
trySnapGauntletCrownRunwayScrollToSpawnsPlay();
@@ -10520,7 +10641,7 @@
function gauntletCrownBuildDisplayMission(mission) {
if (!mission || !Array.isArray(mission.ranked)) return mission;
if (mission.uiSkin === 'jumper' || mission.uiSkin === 'violent_crime' || mission.uiSkin === 'question_mission' || mission.uiSkin === 'stack_tower' || mission.uiSkin === 'mega_virus') return mission;
if (!(previewFillBots && isGauntletCrownHeistMapPlay() && others && typeof others.forEach === 'function')) return mission;
if (!(playBotsEnabled() && isGauntletCrownHeistMapPlay() && others && typeof others.forEach === 'function')) return mission;
const seen = new Set();
const baseRows = [];
@@ -10678,10 +10799,10 @@
const sc = document.createElement('div');
sc.className = 'gcm-sc';
const jumpPlain = mission && (mission.uiSkin === 'jumper' || mission.uiSkin === 'violent_crime' || mission.uiSkin === 'question_mission' || mission.uiSkin === 'stack_tower' || mission.uiSkin === 'mega_virus');
const botScorePlain = (previewFillBots && isGauntletCrownHeistMapPlay() && isPreviewBotId(r.id))
|| (previewFillBots && mission && mission.uiSkin === 'question_mission' && isPreviewBotId(r.id))
|| (previewFillBots && mission && mission.uiSkin === 'stack_tower' && isPreviewBotId(r.id))
|| (previewFillBots && mission && mission.uiSkin === 'mega_virus' && isPreviewBotId(r.id));
const botScorePlain = (playBotsEnabled() && isGauntletCrownHeistMapPlay() && isPreviewBotId(r.id))
|| (playBotsEnabled() && mission && mission.uiSkin === 'question_mission' && isPreviewBotId(r.id))
|| (playBotsEnabled() && mission && mission.uiSkin === 'stack_tower' && isPreviewBotId(r.id))
|| (playBotsEnabled() && mission && mission.uiSkin === 'mega_virus' && isPreviewBotId(r.id));
sc.textContent = (jumpPlain || botScorePlain)
? String(Math.max(0, Number(r.baseScore) || 0))
: String(r.baseScore || 0) + '(+' + String(r.rankBonus || 0) + ')';
@@ -10778,7 +10899,8 @@
function goLobby() {
ov.classList.add('is-hidden');
if (gcmHead) gcmHead.classList.remove('sr-only');
window.location.href = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick);
if (tryFinishDetectiveMinigameAndReturnLobby()) return;
window.location.href = buildRoomLobbyReturnHref();
}
if (btn) {
btn.onclick = function () {
@@ -11505,7 +11627,7 @@
}
function stepSpaceShooterPreviewBots(dt) {
if (spaceShooterGameEnded || !previewFillBots || !mapData || mapData.gameType !== 'space_shooter') return;
if (spaceShooterGameEnded || !playBotsEnabled() || !mapData || mapData.gameType !== 'space_shooter') return;
if (isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase !== 'live') return;
const ts = tileSize;
const mw = (mapData.width || 20) * ts;
@@ -12228,7 +12350,7 @@
}
function stepBalloonBossPreviewBots(dt, mw, mh, bossCx, bossCy, ts) {
if (!previewFillBots || !mapData || mapData.gameType !== 'balloon_boss') return;
if (!playBotsEnabled() || !mapData || mapData.gameType !== 'balloon_boss') return;
[...others.keys()].filter(isPreviewBotId).forEach((bid) => {
const o = others.get(bid);
if (!o || o.balloonBossEliminated || o.balloonBossCx == null) return;
@@ -13542,7 +13664,7 @@
}
function stepJumpSurvivePreviewBots(dt, halfViewH) {
if (!previewFillBots || !mapData || !isJumpSurvive()) return;
if (!playBotsEnabled() || !mapData || !isJumpSurvive()) return;
const md = mapData;
const { cw, ch } = getCharacterFootprintWH(md);
const ts = tileSize;
@@ -14332,19 +14454,36 @@
socket.on('connect', () => {
firstCharacterDefaultPromise.then(() => {
socket.emit('join-space', { spaceId, nickname: nick, characterId: getPlayCharacterId() }, (res) => {
const joinPlayMapId = (params.get('map') || '').trim();
socket.emit('join-space', {
spaceId,
nickname: nick,
characterId: getPlayCharacterId(),
playMapId: joinPlayMapId || undefined,
}, (res) => {
if (!res || !res.ok) {
const errMsg = (res && res.error) || 'เข้าร่วมไม่ได้';
alert(errMsg);
const caseLocked = /เริ่มคดี|ไม่รับผู้เล่น/.test(errMsg);
if (caseLocked) {
window.location.replace('room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick));
window.location.replace(buildRoomLobbyReturnHref());
} else {
window.location.replace(BASE + '/lobby.html');
}
return;
}
myId = socket.id;
let botSlots = parseInt(res.botSlotCount, 10);
if ((!botSlots || botSlots < 1) && res.maxPlayers > 0 && res.maxPlayers < 6) {
botSlots = 6 - res.maxPlayers;
}
if (botSlots > 0) {
detectiveCaseFillBots = true;
detectiveBotSlotCount = Math.min(6, Math.max(0, botSlots));
}
if (isDetectiveMinigamePlay()) {
try { document.documentElement.classList.add('play-detective-minigame'); } catch (eD) { /* ignore */ }
}
if (res.quizSettingsSnap && typeof res.quizSettingsSnap === 'object') {
if (res.quizSettingsSnap.quizMapPanelTheme && typeof res.quizSettingsSnap.quizMapPanelTheme === 'object') {
setQuizMapPanelThemeFromApi({ quizMapPanelTheme: res.quizSettingsSnap.quizMapPanelTheme });
@@ -14425,7 +14564,7 @@
balloonBossEliminated: !!data.balloonBossEliminated,
quizCarryHeld: null,
});
if (previewFillBots) rebalancePreviewBots();
if (playBotsEnabled()) rebalancePreviewBots();
if (quizCarryPregameActive && isQuizCarry()) updateQuizCarryPregameHud();
if (isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'howto') updateQuizQuestionMissionHowtoHud();
if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'howto') updateStackTowerMissionHowtoHud();
@@ -14602,7 +14741,7 @@
socket.on('user-left', (data) => {
if (!data || isPreviewBotId(data.id)) return;
others.delete(data.id);
if (previewFillBots) rebalancePreviewBots();
if (playBotsEnabled()) rebalancePreviewBots();
if (isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'howto') updateQuizQuestionMissionHowtoHud();
if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'howto') updateStackTowerMissionHowtoHud();
if (isJumpSurviveMissionUiMapPlay() && jumpSurviveMissionPhase === 'howto') updateJumpSurviveMissionHowtoHud();
@@ -14746,7 +14885,7 @@
}
}
});
if (previewFillBots) {
if (playBotsEnabled()) {
applyGauntletPreviewBotsAfterSync();
emitGauntletPreviewRowsToServer();
}
@@ -14808,6 +14947,10 @@
socket.on('game-start', (ev) => {
if (!ev || !ev.mapId) return;
const urlMap = (params.get('map') || '').trim();
if (urlMap && ev.stayInRoomLobby && String(ev.mapId).trim() !== urlMap) {
return;
}
const applySnap = (md) => {
mapData = md;
playEmbedUserZoomMul = 1;
@@ -15163,17 +15306,17 @@
if (mapData.gameType === 'balloon_boss') {
applyBalloonBossSpawnLayoutPlay();
}
if (previewFillBots && mapData.gameType === 'gauntlet') {
if (playBotsEnabled() && mapData.gameType === 'gauntlet') {
applyGauntletPreviewSpawnLayout(true);
emitGauntletPreviewRowsToServer();
}
if (previewFillBots && mapData.gameType === 'jump_survive') {
if (playBotsEnabled() && mapData.gameType === 'jump_survive') {
applyJumpSurvivePreviewSpawnLayout(true);
}
if (previewFillBots && mapData.gameType === 'space_shooter') {
if (playBotsEnabled() && mapData.gameType === 'space_shooter') {
applySpaceShooterSpawnLayoutPlay();
}
if (previewFillBots && mapData.gameType === 'balloon_boss') {
if (playBotsEnabled() && mapData.gameType === 'balloon_boss') {
applyBalloonBossSpawnLayoutPlay();
}
};
@@ -15340,7 +15483,7 @@
/** แยกบอท preview ที่ทับกันบนจุดเดียว (มักชนกันที่ขอบแมป) */
function separateClumpedPreviewBots() {
if (!previewFillBots || !mapData) return;
if (!playBotsEnabled() || !mapData) return;
const ids = [...others.keys()].filter(isPreviewBotId);
const minD = 0.52;
const pushAmt = MOVE_SPEED * 0.5;
@@ -15707,7 +15850,7 @@
if ((o.botPathStuckTicks | 0) >= 20) {
o.botPathStuckTicks = 0;
if (path.length) path.shift();
if (isQuizCarry() && previewFillBots) o.botQuizCarryPathfindAfter = 0;
if (isQuizCarry() && playBotsEnabled()) o.botQuizCarryPathfindAfter = 0;
}
}
}
@@ -15718,7 +15861,7 @@
}
function stepPreviewBots() {
if (!previewFillBots || !mapData || isFrogger() || isGauntlet() || isStack() || isJumpSurvive() || isSpaceShooter() || isBalloonBoss()) return;
if (!playBotsEnabled() || !mapData || isFrogger() || isGauntlet() || isStack() || isJumpSurvive() || isSpaceShooter() || isBalloonBoss()) return;
if (isQuizCarry()) {
stepQuizCarryPreviewBots();
return;
@@ -15947,7 +16090,7 @@
/** Stack preview: HUD แนว cyberdeck — SCORE ซ้าย, ตาปัจจุบันขวา, เทอร์มินัลล่างซ้าย */
function drawStackPreviewCyberHud(ctx, cw, ch) {
if (!previewFillBots || !stackMini || !mapData || !isStack()) return;
if (!playBotsEnabled() || !stackMini || !mapData || !isStack()) return;
const now = Date.now();
const pad = 12;
const currentSeat = getStackPreviewCurrentSeat();
@@ -16473,7 +16616,7 @@
);
root.classList.toggle(
'play-cyber-hud--stack-tower-canvas-hud',
!!(previewFillBots && stackMini && isStack() && isStackTowerMissionUiMapPlay()),
!!(playBotsEnabled() && stackMini && isStack() && isStackTowerMissionUiMapPlay()),
);
root.classList.toggle('play-cyber-hud--ss-self-integrity-left', !!isSpaceShooterMissionHudActivePlay());
/** แถบ SCORE แนวนอน (mno9kb07) — ใช้รูปแบบเดียวกับ quiz_carry / พรีวิว editor */
@@ -16746,10 +16889,12 @@
}
}
if (previewEl) {
if (previewFillBots && mapData) {
if (playBotsEnabled() && mapData) {
const human = countPlayHumans();
const bots = [...others.keys()].filter(isPreviewBotId).length;
previewEl.textContent = `SIM · humans ${human} + bots ${bots} · target ${previewTargetHeadcount}`;
const botTarget = playBotTargetHeadcount();
const simLabel = detectiveCaseFillBots ? 'CASE' : 'SIM';
previewEl.textContent = `${simLabel} · humans ${human} + bots ${bots} · target ${botTarget}`;
previewEl.classList.remove('is-hidden');
} else {
previewEl.textContent = '';
@@ -16815,7 +16960,7 @@
characterId: me.characterId || getPlayCharacterId(),
score: (isQuizCarry() || quizMissionHud) ? cyberHudQuizCarryPeerScore(myId)
: (stackMissionHud
? (previewFillBots ? (me.stackPreviewHumanPts || 0) : stackTeamPts)
? (playBotsEnabled() ? (me.stackPreviewHumanPts || 0) : stackTeamPts)
: (isGauntlet() ? (me.gauntletScore || 0)
: (isSpaceShooter() ? (me.spaceShooterScore || 0)
: (isBalloonBoss() ? (me.balloonBossScore || 0)
@@ -16832,7 +16977,7 @@
characterId: o.characterId,
score: (isQuizCarry() || quizMissionHud) ? cyberHudQuizCarryPeerScore(id)
: (stackMissionHud
? (previewFillBots && isPreviewBotId(id) ? (o.stackBotScore || 0) : stackTeamPts)
? (playBotsEnabled() && isPreviewBotId(id) ? (o.stackBotScore || 0) : stackTeamPts)
: (isGauntlet() ? (o.gauntletScore || 0)
: (isSpaceShooter() ? (o.spaceShooterScore || 0)
: (isBalloonBoss() ? (o.balloonBossScore || 0)
@@ -17800,7 +17945,7 @@
}
}
if (isStack() && stackMini) {
if (previewFillBots) {
if (playBotsEnabled()) {
ctx.fillStyle = 'rgba(125, 220, 255, 0.9)';
ctx.font = '600 12px ui-sans-serif, system-ui, sans-serif';
ctx.textAlign = 'left';
@@ -17822,7 +17967,7 @@
}
/* timeup ต้องซิงก์ทุกเฟรมแม้จบเซสชัน — อย่าให้อยู่ในเงื่อนไขด้านบนอย่างเดียว */
syncQuizCarryTimeupDeskLayerPosition();
if (previewFillBots && mapData && !useCyberPlayHud()) {
if (playBotsEnabled() && mapData && !useCyberPlayHud()) {
const human = countPlayHumans();
const bots = [...others.keys()].filter(isPreviewBotId).length;
ctx.save();
@@ -17833,7 +17978,10 @@
ctx.fillStyle = '#a9b1d6';
ctx.font = '12px sans-serif';
ctx.textAlign = 'left';
let line = 'ทดสอบ: คนจริง ' + human + ' + บอท ' + bots + ' (เป้า ' + previewTargetHeadcount + ' คน)';
const botTarget = playBotTargetHeadcount();
let line = detectiveCaseFillBots
? ('คดีสืบสวน: คนจริง ' + human + ' + บอท ' + bots + ' (เป้า ' + botTarget + ' คน)')
: ('ทดสอบ: คนจริง ' + human + ' + บอท ' + bots + ' (เป้า ' + botTarget + ' คน)');
if (isStack()) {
line += ' · ตึกร่วม + HUD ซ้าย/ขวา';
}
@@ -18108,7 +18256,7 @@
}
/* บอท preview ขยับที่ o.x/o.y โดยตรง — ห้าม LERP กับ tx/ty เพราะ stepPreviewBots เคยตั้ง tx=ก่อนเดิน ทำให้โดนดึงถอย ~20%/เฟรม กระตุกและแทบไม่ไป */
others.forEach((o, id) => {
if (previewFillBots && isPreviewBotId(id)) return;
if (playBotsEnabled() && isPreviewBotId(id)) return;
if (o.tx != null) o.x += (o.tx - o.x) * LERP;
if (o.ty != null) o.y += (o.ty - o.y) * LERP;
});
+380 -37
View File
@@ -12,7 +12,13 @@
let profileDisplayName = nick;
if (!spaceId) { location.href = BASE + '/lobby.html'; return; }
const roomIdValueEl = document.getElementById('room-id-value');
if (roomIdValueEl) roomIdValueEl.textContent = String(spaceId);
const displayRoomParam = (params.get('displayRoom') || '').trim();
if (displayRoomParam) {
try { localStorage.setItem('lastCreatedSpaceName', displayRoomParam); } catch (e) { /* ignore */ }
if (roomIdValueEl) roomIdValueEl.textContent = displayRoomParam;
} else if (roomIdValueEl) {
roomIdValueEl.textContent = String(spaceId);
}
const CREATE_ROOM_URL = typeof appPath === 'function' ? appPath('/Create%20Room/') : '/Create%20Room/';
@@ -125,10 +131,15 @@
let hostId = null;
let spaceName = '';
let maxPlayers = 10;
let lobbyBotSlotCount = 0;
const LOBBY_BOT_PREFIX = '__lobby_bot_';
const lobbyBots = new Map();
let suspectPickOverlayOpen = false;
let suspectSelectedIndex = 0;
/** ตรงกับเซิร์ฟ — เฟสเลือกผู้ต้องสงสัยยังไม่จบ (ปิด overlay แล้วยังเปิดได้จากปุ่มโถง) */
let serverSuspectPhaseActive = false;
/** มินิเกม 3 แบบที่สุ่มแล้ว — ล็อกกับการ์ด 0–2 จนกว่าสร้างห้องใหม่ */
let suspectCardMinigames = [];
let mapBackgroundImg = null;
let hostIconImg = null;
const peers = new Map();
@@ -559,13 +570,14 @@
drawQuizZoneLabel(mapData.quizFalseArea, 'SCAM', 'อันตราย', '✕', 'rgba(255,130,200,0.95)', 'rgba(255,210,225,0.95)');
}
function peerVisualOffset(id) {
if (mapData && mapData.lobbySpawnMode === 'slots6') return { ax: 0, ay: 0 };
let h = 0;
for (let i = 0; i < (id || '').length; i++) h = (h * 31 + id.charCodeAt(i)) >>> 0;
const ax = ((h % 5) - 2) * 0.1;
const ay = ((Math.floor(h / 5) % 5) - 2) * 0.1;
return { ax, ay };
}
const peerList = [...peers.entries()].sort(function (a, b) {
const peerList = [...peers.entries(), ...lobbyBots.entries()].sort(function (a, b) {
const pa = a[1], pb = b[1];
const ya = pa.y != null ? pa.y : 1, yb = pb.y != null ? pb.y : 1;
if (Math.abs(ya - yb) > 0.01) return ya - yb;
@@ -809,6 +821,26 @@
return false;
}
function canWalkLobbyBot(x, y, fromX, fromY, botId) {
if (!mapData) return false;
const w = mapData.width || 20, h = mapData.height || 15;
const tx = Math.floor(x), ty = Math.floor(y);
if (tx < 0 || tx >= w || ty < 0 || ty >= h) return false;
const ob = mapData.objects?.[ty]?.[tx] ?? 0;
if (ob === 1) return false;
const bp = mapData.blockPlayer;
if (bp && bp[ty] && bp[ty][tx] === 1) {
for (const [, p] of peers) {
if (Math.floor(p.x) === tx && Math.floor(p.y) === ty) return false;
}
for (const [id, b] of lobbyBots) {
if (id === botId) continue;
if (Math.floor(b.x) === tx && Math.floor(b.y) === ty) return false;
}
}
return true;
}
function canWalkLobby(x, y, fromX, fromY) {
if (!mapData) return false;
const w = mapData.width || 20, h = mapData.height || 15;
@@ -896,12 +928,41 @@
return false;
}
function hostStandingInStartGameArea() {
var me = peers.get(socket.id);
/** ช่องกริดที่ตัวละครครอบคลุม (สูง×กว้าง จากแมป) — ใช้ตรวจพื้นที่ส้ม/เขียว ไม่ใช่แค่มุม anchor */
function lobbyCharacterFootprintTiles(px, py) {
const tiles = new Set();
if (!mapData) return tiles;
const w = mapData.width || 20;
const h = mapData.height || 15;
const cellsW = Math.max(1, Math.min(4, mapData.characterCellsW || mapData.characterCells || 1));
const cellsH = Math.max(1, Math.min(4, mapData.characterCellsH || mapData.characterCells || 1));
const minTx = Math.floor(px);
const minTy = Math.floor(py);
const maxTx = Math.min(w - 1, minTx + cellsW - 1);
const maxTy = Math.min(h - 1, minTy + cellsH - 1);
for (let ty = minTy; ty <= maxTy; ty++) {
for (let tx = minTx; tx <= maxTx; tx++) {
if (tx >= 0 && ty >= 0) tiles.add(tx + ',' + ty);
}
}
return tiles;
}
function peerStandingInStartGameArea(me) {
if (!me || !mapData) return false;
var tx = Math.floor(me.x), ty = Math.floor(me.y);
var row = mapData.startGameArea && mapData.startGameArea[ty];
return !!(row && row[tx] === 1);
const area = mapData.startGameArea;
if (!area || !area.length) return false;
for (const key of lobbyCharacterFootprintTiles(me.x, me.y)) {
const parts = key.split(',');
const tx = +parts[0];
const ty = +parts[1];
if (area[ty] && area[ty][tx] === 1) return true;
}
return false;
}
function hostStandingInStartGameArea() {
return peerStandingInStartGameArea(peers.get(socket.id));
}
function updateHostStartGameButton() {
@@ -917,11 +978,11 @@
var reqA = lobbyMapRequiresStartGameArea();
var okA = !reqA || hostStandingInStartGameArea();
if (reqA && !okA) {
hint.textContent = 'LobbyA: ยืนในพื้นที่สีส้ม แล้วกด F หรือแป้นไทย ด เพื่อเลือกระดับและคดี';
hint.textContent = 'LobbyA: กดพร้อมก่อน · ยืนพื้นส้ม แล้วกด F / ด เพื่อเลือกระดับและคดี';
} else if (reqA) {
hint.textContent = 'LobbyA: ในพื้นที่สีส้ม กด F หรือแป้นไทย ด เพื่อเลือกระดับและคดี';
hint.textContent = 'LobbyA: กดพร้อมก่อน · ในพื้นส้ม กด F / ด เพื่อเลือกระดับและคดี';
} else {
hint.textContent = 'LobbyA: กด F หรือแป้นไทย ด เพื่อเลือกระดับและคดี';
hint.textContent = 'LobbyA: กดพร้อมก่อน · กด F / ด เพื่อเลือกระดับและคดี';
}
}
return;
@@ -943,14 +1004,19 @@
}
}
/** โถง LobbyA — host กดเริ่มแล้วให้เลือกระดับแล้วคดีก่อนเข้า play */
/** โถง LobbyA — host กดเริ่มแล้วให้เลือกระดับแล้วคดีก่อนเข้า play (ตรง Create Room → mlsbbxfe) */
const LOBBY_A_MAP_NAME = 'LobbyA';
const LOBBY_A_MAP_ID = 'mlsbbxfe';
function isCurrentRoomLobbyA() {
if (!mapData) return false;
return String(mapData.name || '').trim().toLowerCase() === LOBBY_A_MAP_NAME.toLowerCase();
if (String(mapData.name || '').trim().toLowerCase() === LOBBY_A_MAP_NAME.toLowerCase()) return true;
if (clientLobbyMapId === LOBBY_A_MAP_ID) return true;
if (String(mapData.id || '').trim() === LOBBY_A_MAP_ID) return true;
return false;
}
/** Host บน LobbyA — หลังกดพร้อมแล้ว กด F ในพื้นที่ส้ม (หรือทั้งแมปถ้าไม่มีส้ม) เพื่อเลือกระดับ/คดี */
function hostCanOpenLobbyAPreplayWithF() {
if (!isCurrentRoomLobbyA() || hostId !== socket.id) return false;
var req = lobbyMapRequiresStartGameArea();
@@ -1093,6 +1159,54 @@
}
const PATH_ARRIVE_THRESH = 0.15;
const LOBBY_BOT_WANDER_DIRS = [[0, -1], [0, 1], [-1, 0], [1, 0]];
function stepLobbyCaseBots() {
if (!lobbyBotSlotCount || !mapData || lobbyBots.size === 0) return;
const w = mapData.width || 20, h = mapData.height || 15;
const now = Date.now();
lobbyBots.forEach((b, id) => {
if (typeof b.botWanderDx !== 'number' || typeof b.botWanderDy !== 'number' || (b.botWanderDx === 0 && b.botWanderDy === 0)) {
const d = LOBBY_BOT_WANDER_DIRS[Math.floor(Math.random() * LOBBY_BOT_WANDER_DIRS.length)];
b.botWanderDx = d[0];
b.botWanderDy = d[1];
}
if (typeof b.botWanderNextTurn !== 'number') b.botWanderNextTurn = now + 600;
if (now >= b.botWanderNextTurn) {
b.botWanderNextTurn = now + 650 + Math.floor(Math.random() * 2200);
if (Math.random() < 0.55) {
const d = LOBBY_BOT_WANDER_DIRS[Math.floor(Math.random() * LOBBY_BOT_WANDER_DIRS.length)];
b.botWanderDx = d[0];
b.botWanderDy = d[1];
}
}
const accX = b.botWanderDx;
const accY = b.botWanderDy;
if (Math.abs(accY) > Math.abs(accX)) b.direction = accY > 0 ? 'down' : 'up';
else if (accX !== 0) b.direction = accX > 0 ? 'right' : 'left';
const ox = b.x, oy = b.y;
const step = MOVE_SPEED;
const nx = b.x + accX * step;
const ny = b.y + accY * step;
if (canWalkLobbyBot(nx, ny, b.x, b.y, id)) {
b.x = nx;
b.y = ny;
} else if (canWalkLobbyBot(nx, b.y, b.x, b.y, id)) {
b.x = nx;
} else if (canWalkLobbyBot(b.x, ny, b.x, b.y, id)) {
b.y = ny;
} else {
const d = LOBBY_BOT_WANDER_DIRS[Math.floor(Math.random() * LOBBY_BOT_WANDER_DIRS.length)];
b.botWanderDx = d[0];
b.botWanderDy = d[1];
b.botWanderNextTurn = now + 200 + Math.floor(Math.random() * 600);
}
b.x = Math.max(0, Math.min(w - 0.01, b.x));
b.y = Math.max(0, Math.min(h - 0.01, b.y));
b.isWalking = Math.abs(b.x - ox) > 1e-5 || Math.abs(b.y - oy) > 1e-5;
});
}
function lobbyTick() {
const me = peers.get(socket.id);
if (!mapData || !me) { requestAnimationFrame(lobbyTick); return; }
@@ -1102,6 +1216,7 @@
if (p.ty != null) p.y += (p.ty - p.y) * LERP;
}
});
if (!suspectPickOverlayOpen && lobbyBotSlotCount > 0) stepLobbyCaseBots();
const w = mapData.width || 20, h = mapData.height || 15;
const preWalkX = me.x, preWalkY = me.y;
let accX = 0, accY = 0;
@@ -1171,10 +1286,144 @@
requestAnimationFrame(lobbyTick);
}
function lobbyOccupantCount() {
return peers.size + lobbyBots.size;
}
function lobbyOccupantSlots() {
return maxPlayers + lobbyBotSlotCount;
}
function lobbySpawnTileWalkable(tx, ty) {
if (!mapData) return false;
const w = mapData.width || 20;
const h = mapData.height || 15;
if (tx < 0 || tx >= w || ty < 0 || ty >= h) return false;
const row = mapData.objects && mapData.objects[ty];
return !(row && row[tx] === 1);
}
function lobbySpawnFootprintFits(anchorX, anchorY) {
if (!mapData) return false;
const cellsW = Math.max(1, Math.min(4, Math.floor(Number(mapData.characterCellsW) || Number(mapData.characterCells) || 1)));
const cellsH = Math.max(1, Math.min(4, Math.floor(Number(mapData.characterCellsH) || Number(mapData.characterCells) || 1)));
const w = mapData.width || 20;
const h = mapData.height || 15;
const maxX = Math.min(w, anchorX + cellsW);
const maxY = Math.min(h, anchorY + cellsH);
for (let ty = anchorY; ty < maxY; ty++) {
for (let tx = anchorX; tx < maxX; tx++) {
if (!lobbySpawnTileWalkable(tx, ty)) return false;
}
}
return true;
}
function parseLobbyPlayerSpawnsFromMapLobby(md) {
const w = md.width || 20;
const h = md.height || 15;
const out = [null, null, null, null, null, null];
const raw = md && md.lobbyPlayerSpawns;
if (!Array.isArray(raw)) return out;
for (let i = 0; i < 6 && i < raw.length; i++) {
const cell = raw[i];
if (!cell || typeof cell !== 'object') continue;
const x = Math.floor(Number(cell.x));
const y = Math.floor(Number(cell.y));
if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
if (x < 0 || x >= w || y < 0 || y >= h) continue;
out[i] = { x, y };
}
return out;
}
function pickRandomLobbySpawnFromMap() {
const fb = mapData.spawn || { x: 1, y: 1 };
const fx = Number.isFinite(Number(fb.x)) ? Number(fb.x) : 1;
const fy = Number.isFinite(Number(fb.y)) ? Number(fb.y) : 1;
const grid = mapData.spawnArea;
if (!grid || !Array.isArray(grid)) return { x: fx, y: fy };
const w = mapData.width || 20;
const h = mapData.height || 15;
const pool = [];
for (let yy = 0; yy < h; yy++) {
const row = grid[yy];
if (!row) continue;
for (let xx = 0; xx < w; xx++) {
if (Number(row[xx]) === 1 && lobbySpawnFootprintFits(xx, yy)) pool.push({ x: xx, y: yy });
}
}
if (!pool.length) return { x: fx, y: fy };
const pick = pool[Math.floor(Math.random() * pool.length)];
return { x: pick.x, y: pick.y };
}
/** สอดคล้อง server pickSpawnForJoin — P1…P6 ตามลำดับเข้า */
function pickLobbySpawnForJoin(joinOrderIndex) {
if (!mapData) return { x: 1, y: 1 };
const mode = mapData.lobbySpawnMode;
const ord = joinOrderIndex | 0;
if (mode === 'slots6' && ord >= 6) return pickRandomLobbySpawnFromMap();
const j = Math.min(Math.max(0, ord), 5);
if (mode === 'fixed' && mapData.spawn) {
const fx = Number.isFinite(Number(mapData.spawn.x)) ? Math.floor(Number(mapData.spawn.x)) : 1;
const fy = Number.isFinite(Number(mapData.spawn.y)) ? Math.floor(Number(mapData.spawn.y)) : 1;
const w = mapData.width || 20;
const h = mapData.height || 15;
const x = Math.max(0, Math.min(w - 1, fx));
const y = Math.max(0, Math.min(h - 1, fy));
if (lobbySpawnFootprintFits(x, y)) return { x, y };
return pickRandomLobbySpawnFromMap();
}
if (mode === 'slots6') {
const slots = parseLobbyPlayerSpawnsFromMapLobby(mapData);
const pick = slots[j];
if (pick && lobbySpawnFootprintFits(pick.x, pick.y)) return { x: pick.x, y: pick.y };
return pickRandomLobbySpawnFromMap();
}
return pickRandomLobbySpawnFromMap();
}
function pickLobbyBotSpawn(index) {
const sp = pickLobbySpawnForJoin(index);
return { x: sp.x, y: sp.y };
}
function syncLobbyCaseBots() {
if (!lobbyBotSlotCount || !mapData) {
lobbyBots.clear();
return;
}
while (lobbyBots.size < lobbyBotSlotCount) {
const i = lobbyBots.size;
const id = LOBBY_BOT_PREFIX + i;
const sp = pickLobbyBotSpawn(peers.size + i);
const wanderDirs = [[0, -1], [0, 1], [-1, 0], [1, 0]];
const wd = wanderDirs[Math.floor(Math.random() * wanderDirs.length)];
lobbyBots.set(id, {
x: sp.x,
y: sp.y,
direction: 'down',
nickname: 'บอท ' + (i + 1),
ready: true,
characterId: getStoredCharacterId(),
isWalking: false,
botWanderDx: wd[0],
botWanderDy: wd[1],
botWanderNextTurn: Date.now() + 400 + Math.floor(Math.random() * 900),
});
}
while (lobbyBots.size > lobbyBotSlotCount) {
const keys = [...lobbyBots.keys()];
lobbyBots.delete(keys[keys.length - 1]);
}
}
function updatePlayersHud() {
if (roomPlayersHud) roomPlayersHud.textContent = 'PLAYERS : ' + peers.size + '/' + maxPlayers;
const hudText = 'PLAYERS : ' + lobbyOccupantCount() + '/' + lobbyOccupantSlots();
if (roomPlayersHud) roomPlayersHud.textContent = hudText;
const sph = document.getElementById('suspect-players-hud');
if (sph) sph.textContent = 'PLAYERS : ' + peers.size + '/' + maxPlayers;
if (sph) sph.textContent = hudText;
}
function renderPeers() {
@@ -1675,7 +1924,13 @@
if (!mapData.quizQuestionArea) mapData.quizQuestionArea = [];
maxPlayers = res.maxPlayers != null ? res.maxPlayers : 10;
lobbyBotSlotCount = res.botSlotCount != null ? Math.max(0, parseInt(res.botSlotCount, 10) || 0) : 0;
if (lobbyBotSlotCount === 0 && maxPlayers > 0 && maxPlayers < 6) {
lobbyBotSlotCount = 6 - maxPlayers;
}
if (lobbyBotSlotCount > 0) syncLobbyCaseBots();
updatePlayersHud();
syncLobbyBUiChrome();
if (wasPageReload() && (hostId === socket.id || peers.size <= 1)) {
window.location.replace(CREATE_ROOM_URL);
@@ -1691,12 +1946,20 @@
}
ensureReadyControlEnabled();
if (res.cardMinigames) setSuspectCardMinigames(res.cardMinigames);
serverSuspectPhaseActive = !!res.suspectPhaseActive;
if (res.suspectPhaseActive) {
openSuspectOverlay(res.suspectPickIndex != null ? res.suspectPickIndex : 0);
} else {
updateSuspectFloatingOpenBtn();
}
if (clientLobbyMapId === POST_CASE_LOBBY_SPACE_ID || res.suspectPhaseActive) {
try {
var uLobbyB = new URL(window.location.href);
uLobbyB.searchParams.set('map', POST_CASE_LOBBY_SPACE_ID);
history.replaceState({}, '', uLobbyB.pathname + uLobbyB.search);
} catch (eMap) { /* ignore */ }
}
const isHost = hostId === socket.id;
if (hostOnly) hostOnly.style.display = isHost ? 'flex' : 'none';
@@ -1999,9 +2262,17 @@
try { document.body.classList.remove('room-lobby--quiz-active'); } catch (e) { /* ignore */ }
hideQuizOverlay();
appendLobbySystemChat('— ' + (d && d.message ? d.message : 'จบเกมตอบคำถาม'));
if (d && d.returnToLobbyB) {
serverSuspectPhaseActive = true;
updateSuspectFloatingOpenBtn();
}
redrawLobbyMap();
});
socket.on('detective-minigame-ended', function (data) {
applyDetectiveReturnToLobbyB(data || {});
});
socket.on('lobby-interact', (data) => {
if (!data || data.x == null || data.y == null) return;
lobbyInteractPulse = { x: data.x, y: data.y, until: Date.now() + 700 };
@@ -2924,6 +3195,11 @@
else el.textContent = 'คดีโจรกรรมไซเบอร์';
}
function setSuspectCardMinigames(cards) {
if (!cards || !cards.length) return;
suspectCardMinigames = cards;
}
function applySuspectSelectionVisual(idx) {
let i = Math.floor(Number(idx));
if (Number.isNaN(i) || i < 0 || i > 2) i = 0;
@@ -2934,6 +3210,37 @@
});
}
function applyDetectiveReturnToLobbyB(data) {
quizModeActive = false;
quizPhaseLocal = null;
quizPlayerLocal = { cannotTrue: false, cannotFalse: false, eliminated: false, score: 0 };
quizPeersLocked = {};
try { document.body.classList.remove('room-lobby--quiz-active'); } catch (e) { /* ignore */ }
hideQuizOverlay();
const mid = (data && data.mapId) ? String(data.mapId).trim() : POST_CASE_LOBBY_SPACE_ID;
const reopenSuspect = !!(data && data.suspectPhaseActive);
const pickIdx = (data && typeof data.suspectPickIndex === 'number') ? data.suspectPickIndex : suspectSelectedIndex;
if (data && data.cardMinigames) setSuspectCardMinigames(data.cardMinigames);
applyRoomLobbyBTransition({
mapId: mid,
peersSnap: (data && data.peersSnap) ? data.peersSnap : [],
lobbyLevel: (window.__detectiveLobbyMeta && window.__detectiveLobbyMeta.level) || null,
caseId: (window.__detectiveLobbyMeta && window.__detectiveLobbyMeta.caseId) || null,
});
if (reopenSuspect) {
serverSuspectPhaseActive = true;
setTimeout(function () {
openSuspectOverlay(pickIdx);
updateSuspectFloatingOpenBtn();
syncLobbyBUiChrome();
updatePlayersHud();
}, 500);
}
syncLobbyBUiChrome();
updatePlayersHud();
appendLobbySystemChat('— จบมินิเกม · กลับ LobbyB แล้ว');
}
const SUSPECT_DESIGN_WIDTH = 1200;
let suspectPickScaleListenersBound = false;
@@ -3026,6 +3333,13 @@
const isHost = hostId === socket.id;
const actions = document.getElementById('suspect-pick-actions');
const accuseBtn = document.getElementById('suspect-btn-accuse');
const hint = document.getElementById('suspect-pick-hint');
if (hint) {
hint.textContent = isHost
? 'เลือกการ์ดผู้ต้องสงสัยก่อน แล้วกดเริ่มสืบสวน'
: 'รอ Host เลือกการ์ดและกดเริ่มสืบสวน';
hint.classList.toggle('is-hidden', false);
}
if (actions) {
actions.classList.toggle('suspect-pick-actions--visible', isHost);
actions.setAttribute('aria-hidden', isHost ? 'false' : 'true');
@@ -3118,6 +3432,7 @@
socket.on('suspect-phase-open', (data) => {
serverSuspectPhaseActive = true;
if (data && data.hostId != null) hostId = data.hostId;
if (data && data.cardMinigames) setSuspectCardMinigames(data.cardMinigames);
const idx = (data && typeof data.selectedIndex === 'number') ? data.selectedIndex : 0;
openSuspectOverlay(idx);
});
@@ -3131,36 +3446,51 @@
serverSuspectPhaseActive = false;
closeSuspectOverlay();
const n = (data && typeof data.selectedIndex === 'number') ? (data.selectedIndex + 1) : (suspectSelectedIndex + 1);
appendLobbySystemChat('— เริ่มสืบสวน · ผู้ต้องสงสัยหมายเลข ' + n);
const mgLabel = (data && data.minigameLabel) ? String(data.minigameLabel) : '';
appendLobbySystemChat('— เริ่มสืบสวน · ผู้ต้องสงสัยหมายเลข ' + n + (mgLabel ? ' · ' + mgLabel : ''));
});
/** เลือกการ์ดผู้ต้องสงสัยเท่านั้น — ยังไม่เริ่มมินิเกม */
function selectSuspectCard(idx) {
if (!suspectPickOverlayOpen || hostId !== socket.id) return;
if (idx < 0 || idx > 2) return;
const prevIdx = suspectSelectedIndex;
applySuspectSelectionVisual(idx);
socket.emit('suspect-pick-select', { index: idx });
if (prevIdx !== idx) {
appendLobbySystemChat('— เลือกผู้ต้องสงสัยหมายเลข ' + (idx + 1));
}
}
/** เริ่มมินิเกมตามการ์ดที่เลือกแล้ว — กดปุ่มเริ่มสืบสวน / ชี้ตัวคนร้าย */
function beginSuspectInvestigation(sourceLabel) {
if (!suspectPickOverlayOpen || hostId !== socket.id) return;
const idx = suspectSelectedIndex;
if (idx < 0 || idx > 2) return;
socket.emit('suspect-pick-start', {}, function (res) {
if (res && res.ok) return;
const err = (res && res.error) ? String(res.error) : (sourceLabel || 'เริ่มสืบสวนไม่สำเร็จ');
appendLobbySystemChat('— ' + err);
try { console.warn('suspect-pick-start', res); } catch (e2) { /* ignore */ }
});
}
document.getElementById('suspect-cards-row')?.addEventListener('click', (ev) => {
const card = ev.target.closest('.suspect-card');
if (!card || !suspectPickOverlayOpen || hostId !== socket.id) return;
const idx = parseInt(card.getAttribute('data-index'), 10);
if (idx >= 0 && idx <= 2) socket.emit('suspect-pick-select', { index: idx });
if (idx >= 0 && idx <= 2) selectSuspectCard(idx);
});
document.getElementById('suspect-btn-start')?.addEventListener('click', () => {
if (!suspectPickOverlayOpen || hostId !== socket.id) return;
socket.emit('suspect-pick-start', {}, (res) => {
if (res && res.ok) return;
const err = (res && res.error) ? String(res.error) : 'เริ่มสืบสวนไม่สำเร็จ';
appendLobbySystemChat('— ' + err);
try { console.warn('suspect-pick-start', res); } catch (e2) { /* ignore */ }
});
beginSuspectInvestigation('เริ่มสืบสวนไม่สำเร็จ');
});
document.getElementById('suspect-btn-accuse')?.addEventListener('click', () => {
if (!suspectPickOverlayOpen || hostId !== socket.id) return;
const pickNumber = Number.isFinite(suspectSelectedIndex) ? (suspectSelectedIndex + 1) : 1;
appendLobbySystemChat('— Host ชี้ตัวคนร้ายหมายเลข ' + pickNumber);
socket.emit('suspect-pick-start', {}, (res) => {
if (res && res.ok) return;
const err = (res && res.error) ? String(res.error) : 'ชี้ตัวคนร้ายไม่สำเร็จ';
appendLobbySystemChat('— ' + err);
try { console.warn('suspect-btn-accuse', res); } catch (e2) { /* ignore */ }
});
beginSuspectInvestigation('ชี้ตัวคนร้ายไม่สำเร็จ');
});
document.getElementById('suspect-pick-close')?.addEventListener('click', () => {
@@ -3227,10 +3557,13 @@
updateHostStartGameButton();
renderPeers();
ensureReadyControlEnabled();
if (data.cardMinigames) setSuspectCardMinigames(data.cardMinigames);
if (lobbyBotSlotCount > 0) syncLobbyCaseBots();
appendLobbySystemChat('— ย้ายไป LobbyB แล้ว · ห้องนี้ไม่รับผู้เล่นใหม่');
tryTroublesomeLobbyGate();
updateSuspectFloatingOpenBtn();
syncLobbyBUiChrome();
updatePlayersHud();
try {
if (String(mid) === POST_CASE_LOBBY_SPACE_ID) {
var u = new URL(window.location.href);
@@ -3288,10 +3621,15 @@
}
socket.on('game-start', (data) => {
if (data && data.detectiveReturn && data.stayInRoomLobby) {
applyDetectiveReturnToLobbyB(data);
return;
}
serverSuspectPhaseActive = false;
closeTroublesomeOverlay();
closeSuspectOverlay();
closeLobbyPreplayWizard();
if (data && data.cardMinigames) setSuspectCardMinigames(data.cardMinigames);
if (data && data.quizMode && data.stayInRoomLobby) {
const qMid = (data.mapId != null) ? String(data.mapId).trim() : '';
applyLobbyPeersSnapFromServer(data.peersSnap);
@@ -3354,10 +3692,14 @@
});
return;
}
if (data && data.detectiveMinigame) {
try { sessionStorage.setItem('detectiveMinigameReturn', '1'); } catch (e) { /* ignore */ }
}
let q = 'play.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick);
if (mid) q += '&map=' + encodeURIComponent(mid);
if (lobbyLevelStr) q += '&lobbyLevel=' + encodeURIComponent(lobbyLevelStr);
if (caseIdStr) q += '&case=' + encodeURIComponent(caseIdStr);
if (data && data.detectiveMinigame) q += '&detectiveReturn=1';
location.href = q;
});
@@ -3416,20 +3758,21 @@
}
const me = peers.get(socket.id);
if (!mapData || !me) return;
e.preventDefault();
/* ทุก F ในโถง — ต้องกดพร้อมก่อน (รวม Host เลือกระดับ/คดี และช่องเขียว) */
if (!me.ready) {
appendLobbySystemChat('— กดพร้อมก่อน แล้วค่อยกด F');
return;
}
if (hostCanOpenLobbyAPreplayWithF()) {
e.preventDefault();
openLobbyPreplayWizard();
return;
}
const target = getLobbyInteractTarget(me);
if (!target) {
e.preventDefault();
return;
}
/* ทุกแบบแผนที่ในโถง (รวม quiz) — ต้องพร้อมก่อน F โต้ตอบช่องเขียว */
if (!me.ready) {
e.preventDefault();
appendLobbySystemChat('— กดพร้อมก่อน แล้วค่อยกด F ที่ช่องสีเขียว');
if (isCurrentRoomLobbyA() && hostId !== socket.id) {
appendLobbySystemChat('— เฉพาะ Host กด F เพื่อเลือกระดับและคดี');
}
return;
}
e.preventDefault();
+1 -1
View File
@@ -3203,7 +3203,7 @@
</div>
<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.0432"></script>
<script src="js/play.js?v=0.0436"></script>
<div class="version-tag">v —</div>
</body>
</html>
+142 -46
View File
@@ -45,28 +45,95 @@
window.__detectiveLobbyMeta = { level: lobbyLevelParam, caseId: lobbyCaseParam };
} catch (e) { /* ignore */ }
}
const POST_CASE_LOBBY_MAP_ID = 'mn8nx46h';
function isDetectiveMinigamePlay() {
if (params.get('detectiveReturn') === '1') return true;
try {
return sessionStorage.getItem('detectiveMinigameReturn') === '1';
} catch (e) {
return false;
}
}
function buildRoomLobbyReturnHref() {
let h = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick);
h += '&map=' + encodeURIComponent(POST_CASE_LOBBY_MAP_ID);
try {
const dr = localStorage.getItem('lastCreatedSpaceName');
if (dr) h += '&displayRoom=' + encodeURIComponent(dr);
} catch (e) { /* ignore */ }
try {
const meta = window.__detectiveLobbyMeta;
if (meta && meta.level != null && String(meta.level).trim()) {
h += '&lobbyLevel=' + encodeURIComponent(String(meta.level).trim());
}
if (meta && meta.caseId != null && String(meta.caseId).trim()) {
h += '&caseId=' + encodeURIComponent(String(meta.caseId).trim());
}
} catch (e2) { /* ignore */ }
try {
if (sessionStorage.getItem('detectiveMinigameReturn') === '1') {
h += '&detectiveReturn=1';
sessionStorage.removeItem('detectiveMinigameReturn');
}
} catch (e3) { /* ignore */ }
if (params.get('detectiveReturn') === '1' && h.indexOf('detectiveReturn') < 0) {
h += '&detectiveReturn=1';
}
return h;
}
function finishDetectiveMinigameAndReturnLobby() {
quizCarrySessionEnded = true;
try { sessionStorage.setItem('detectiveMinigameReturn', '1'); } catch (e) { /* ignore */ }
const go = function () {
window.location.href = buildRoomLobbyReturnHref();
};
if (socket && socket.connected) {
socket.emit('detective-minigame-finished', {}, function (res) {
if (res && res.ok) go();
else go();
});
} else {
go();
}
}
/** ทดสอบจากเอดิเตอร์: เติมบอทให้ครบจำนวน (รวมผู้เล่นจริง) — ?fillTotal=6 (ค่าเริ่ม 6) */
const previewFillBots = previewMode && editorEmbedReturn;
const previewTargetHeadcount = Math.min(24, Math.max(1, parseInt(params.get('fillTotal'), 10) || 6));
let detectiveCaseFillBots = false;
let detectiveBotSlotCount = 0;
function playBotsEnabled() {
return previewFillBots || detectiveCaseFillBots;
}
function playBotTargetHeadcount() {
if (previewFillBots) return previewTargetHeadcount;
if (detectiveCaseFillBots) return countPlayHumans() + detectiveBotSlotCount;
return countPlayHumans();
}
const PREVIEW_BOT_PREFIX = '__pv_bot_';
let previewBotSeq = 0;
/** Stack preview: บอทไม่วาบบนแผนที่ — ใช้ไฮไลต์แถบแข่งแทน */
let lastStackPreviewActorId = null;
let lastStackPreviewActorUntil = 0;
/** Stack preview: สลับตา Player 1 = มนุษย์, 2–6 = บอท (เฉพาะตาปัจจุบันเล่นได้) */
const STACK_PREVIEW_TURN_COUNT = 6;
let stackTurnCount = 6;
let stackPreviewTurnOrder = null;
let stackPreviewTurnIndex = 0;
let stackPreviewBotThinkUntil = 0;
function rebuildStackPreviewTurnOrder() {
if (!previewFillBots || !mapData || mapData.gameType !== 'stack') {
if (!playBotsEnabled() || !mapData || mapData.gameType !== 'stack') {
stackPreviewTurnOrder = null;
stackTurnCount = 1;
return;
}
const bots = [...others.keys()].filter(isPreviewBotId).sort();
stackTurnCount = Math.max(1, Math.min(12, playBotTargetHeadcount()));
stackPreviewTurnOrder = [{ kind: 'human', seat: 1 }];
for (let i = 0; i < STACK_PREVIEW_TURN_COUNT - 1; i++) {
for (let i = 0; i < stackTurnCount - 1; i++) {
stackPreviewTurnOrder.push({ kind: 'bot', seat: i + 2, botId: bots[i] || null });
}
stackPreviewTurnIndex = 0;
@@ -74,19 +141,19 @@
}
function advanceStackPreviewTurn() {
if (!previewFillBots || !stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT) return;
stackPreviewTurnIndex = (stackPreviewTurnIndex + 1) % STACK_PREVIEW_TURN_COUNT;
if (!playBotsEnabled() || !stackPreviewTurnOrder || stackPreviewTurnOrder.length !== stackTurnCount) return;
stackPreviewTurnIndex = (stackPreviewTurnIndex + 1) % stackTurnCount;
stackPreviewBotThinkUntil = 0;
}
function isStackPreviewHumanTurn() {
if (!previewFillBots || !stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT) return true;
if (!playBotsEnabled() || !stackPreviewTurnOrder || stackPreviewTurnOrder.length !== stackTurnCount) return true;
const cur = stackPreviewTurnOrder[stackPreviewTurnIndex];
return !!(cur && cur.kind === 'human');
}
function getStackPreviewCurrentSeat() {
if (!stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT) return 1;
if (!stackPreviewTurnOrder || stackPreviewTurnOrder.length !== stackTurnCount) return 1;
const cur = stackPreviewTurnOrder[stackPreviewTurnIndex];
return cur && cur.seat ? cur.seat : stackPreviewTurnIndex + 1;
}
@@ -2030,9 +2097,12 @@
}
function rebalancePreviewBots() {
if (!previewFillBots || !mapData) return;
if (!playBotsEnabled() || !mapData) return;
if (isDetectiveMinigamePlay() && isQuizCarry()) {
/* บอทคดีสืบสวน — ใช้ stepQuizCarryPreviewBots */
}
const human = countPlayHumans();
const wantBots = Math.max(0, previewTargetHeadcount - human);
const wantBots = Math.max(0, playBotTargetHeadcount() - human);
[...others.keys()].filter(isPreviewBotId).forEach((bid) => {
const o = others.get(bid);
if (!o) return;
@@ -3801,6 +3871,10 @@
/** หลังจบข้อหนึ่งข้อ (ถูกหรือหมดเวลา): พรีวิวนับรอบจนครบ carrySessionLength แล้วจบเซสชัน */
function quizCarryAfterRoundResolved() {
if (!isQuizCarry()) return;
if (isDetectiveMinigamePlay()) {
finishDetectiveMinigameAndReturnLobby();
return;
}
if (!previewMode) {
quizCarryPickNextQuestion();
return;
@@ -3932,7 +4006,7 @@
if (!(previewMode && editorEmbedReturn)) return;
embedPreviewLobbyReturnTimer = setTimeout(function () {
embedPreviewLobbyReturnTimer = null;
window.location.href = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick);
window.location.href = buildRoomLobbyReturnHref();
}, 5000);
}
@@ -4144,7 +4218,7 @@
if (previewMode && editorEmbedReturn) {
showQuizCarryTimeupOnDeskLayer();
} else {
window.location.href = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick);
window.location.href = buildRoomLobbyReturnHref();
}
};
}
@@ -4216,7 +4290,7 @@
});
ov.classList.remove('is-hidden');
function goLobby() {
window.location.href = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick);
window.location.href = buildRoomLobbyReturnHref();
}
if (btn) {
btn.onclick = () => {
@@ -4557,6 +4631,8 @@
}
if (previewMode && editorEmbedReturn) {
beginQuizCarryEmbedPregame();
} else if (isDetectiveMinigamePlay()) {
quizCarryPickNextQuestion();
} else {
quizCarryPickNextQuestion();
}
@@ -4576,16 +4652,23 @@
}
const ov = document.getElementById('quiz-game-overlay');
if (ov) {
if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden');
else ov.classList.remove('is-hidden');
if (isDetectiveMinigamePlay() || (previewMode && editorEmbedReturn)) {
ov.classList.add('is-hidden');
ov.setAttribute('aria-hidden', 'true');
} else {
ov.classList.remove('is-hidden');
ov.setAttribute('aria-hidden', 'false');
}
}
const tEl = document.getElementById('quiz-game-timer');
if (tEl) tEl.textContent = '';
const leg = document.getElementById('quiz-play-legend');
if (leg) {
leg.textContent = quizCarryMapHasAnswerInteractive(mapData)
? 'แต่ละตัวเลือกมีคนถือได้คนเดียว — ส่งคำตอบที่โซน interactive (เขียว) · โซนกลางม่วงเป็นกำแพง · กด F หรือปุ่ม GRAB มุมขวาล่าง'
: 'แต่ละตัวเลือกมีคนถือได้คนเดียว — โซนกลางเป็นกำแพง · ยืนชิดขอบแล้วกด F หรือปุ่ม GRAB ส่งเมื่อถือป้าย';
leg.textContent = isDetectiveMinigamePlay()
? ''
: (quizCarryMapHasAnswerInteractive(mapData)
? 'แต่ละตัวเลือกมีคนถือได้คนเดียว — ส่งคำตอบที่โซน interactive (เขียว) · โซนกลางม่วงเป็นกำแพง · กด F หรือปุ่ม GRAB มุมขวาล่าง'
: 'แต่ละตัวเลือกมีคนถือได้คนเดียว — โซนกลางเป็นกำแพง · ยืนชิดขอบแล้วกด F หรือปุ่ม GRAB ส่งเมื่อถือป้าย');
}
resetQuizCarryPlayState();
if (previewMode) {
@@ -4706,7 +4789,7 @@
function stepQuizCarryPreviewBots() {
if (quizCarrySessionEnded) return;
if (quizCarryPregameActive) return;
if (!previewFillBots || !isQuizCarry() || !mapData) return;
if (!playBotsEnabled() || !isQuizCarry() || !mapData) return;
const w = mapData.width || 20, h = mapData.height || 15;
const nowPf = performance.now();
const pathfindMinGap = editorEmbedReturn ? 280 : 140;
@@ -5032,16 +5115,16 @@
/** @returns {boolean} เริ่มแอนิเมชันร่วงแล้วหรือไม่ */
function tryHumanStackDrop() {
if (!stackMini || !isStack() || stackFall || stackMini.settling) return false;
if (previewFillBots && mapData && mapData.gameType === 'stack' &&
(!stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT)) {
if (playBotsEnabled() && mapData && mapData.gameType === 'stack' &&
(!stackPreviewTurnOrder || stackPreviewTurnOrder.length !== stackTurnCount)) {
rebuildStackPreviewTurnOrder();
}
if (previewFillBots && !isStackPreviewHumanTurn()) return false;
if (playBotsEnabled() && !isStackPreviewHumanTurn()) return false;
const now = Date.now();
if (now - stackMini.lastDropAt < 480) return false;
const hit = computeStackDropResult(stackMini);
if (!startStackFallAnimation(hit, previewFillBots ? { human: true } : null)) return false;
if (previewFillBots) {
if (!startStackFallAnimation(hit, playBotsEnabled() ? { human: true } : null)) return false;
if (playBotsEnabled()) {
lastStackPreviewActorId = '__human__';
lastStackPreviewActorUntil = Date.now() + 900;
}
@@ -5052,7 +5135,7 @@
function simulatePreviewBotStackDrop(botId, o) {
if (!stackMini || stackFall || stackMini.settling) return;
const hit = computeStackDropResult(stackMini);
if (!startStackFallAnimation(hit, previewFillBots ? { botId } : null)) return;
if (!startStackFallAnimation(hit, playBotsEnabled() ? { botId } : null)) return;
o.stackBotGlowUntil = Date.now() + Math.max(900, 400 + (stackFall && stackFall.dur ? stackFall.dur : 800));
lastStackPreviewActorId = botId;
lastStackPreviewActorUntil = Date.now() + Math.max(1000, 500 + (stackFall && stackFall.dur ? stackFall.dur : 800));
@@ -5060,13 +5143,13 @@
/** Stack preview: สลับตา — เฉพาะบอทที่ถึงตาถึงจะจำลองปล่อยหนึ่งครั้งแล้วเลื่อนตา */
function stepPreviewStackTurnBased(nowMs) {
if (!previewFillBots || !isStack() || !stackMini || stackFall || stackMini.settling) return;
if (!playBotsEnabled() || !isStack() || !stackMini || stackFall || stackMini.settling) return;
if (mapData && mapData.gameType === 'stack' &&
(!stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT)) {
(!stackPreviewTurnOrder || stackPreviewTurnOrder.length !== stackTurnCount)) {
rebuildStackPreviewTurnOrder();
}
const order = stackPreviewTurnOrder;
if (!order || order.length !== STACK_PREVIEW_TURN_COUNT) return;
if (!order || order.length !== stackTurnCount) return;
const cur = order[stackPreviewTurnIndex];
if (!cur || cur.kind !== 'bot') return;
if (!cur.botId) {
@@ -5098,12 +5181,12 @@
const wasHuman = !!s.previewHumanDrop;
const settleBotId = s.previewBotId || null;
applyStackHitFromDrop({ miss: true, perfect: false, pts: 0, landOffsetTiles: 0 }, undefined);
if (previewFillBots && (wasHuman || settleBotId)) {
if (playBotsEnabled() && (wasHuman || settleBotId)) {
stackPreviewLogStackDrop({ miss: true, perfect: false, pts: 0 }, { human: wasHuman, botId: settleBotId });
}
stackMini.settling = null;
stackMini.lastDropAt = Date.now();
if (previewFillBots && (wasHuman || settleBotId)) advanceStackPreviewTurn();
if (playBotsEnabled() && (wasHuman || settleBotId)) advanceStackPreviewTurn();
}
} else if (stackFall) {
const t = (now - stackFall.t0) / stackFall.dur;
@@ -5114,12 +5197,12 @@
const stableMin = 0.52;
if (hit.miss) {
applyStackHitFromDrop(hit);
if (previewFillBots && (wasHuman || fallBotId)) {
if (playBotsEnabled() && (wasHuman || fallBotId)) {
stackPreviewLogStackDrop(hit, { human: wasHuman, botId: fallBotId });
}
stackMini.lastDropAt = Date.now();
stackFall = null;
if (previewFillBots && (wasHuman || fallBotId)) advanceStackPreviewTurn();
if (playBotsEnabled() && (wasHuman || fallBotId)) advanceStackPreviewTurn();
} else if (hit.supportRatio != null && hit.supportRatio < stableMin) {
const stripH = stackMini.blockStripH || Math.max(16, tileSize * 0.32);
stackMini.settling = {
@@ -5137,17 +5220,17 @@
stackFall = null;
} else {
let previewAttr;
if (previewFillBots) {
if (playBotsEnabled()) {
if (wasHuman) previewAttr = { human: true };
else if (fallBotId) previewAttr = { botId: fallBotId };
}
applyStackHitFromDrop(hit, previewAttr);
if (previewFillBots && (wasHuman || fallBotId)) {
if (playBotsEnabled() && (wasHuman || fallBotId)) {
stackPreviewLogStackDrop(hit, { human: wasHuman, botId: fallBotId });
}
stackMini.lastDropAt = Date.now();
stackFall = null;
if (previewFillBots && (wasHuman || fallBotId)) advanceStackPreviewTurn();
if (playBotsEnabled() && (wasHuman || fallBotId)) advanceStackPreviewTurn();
}
}
} else if (stackMini) {
@@ -6008,7 +6091,7 @@
ov.classList.remove('is-hidden');
function goLobby() {
ov.classList.add('is-hidden');
window.location.href = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick);
window.location.href = buildRoomLobbyReturnHref();
}
if (btn) {
btn.onclick = function () {
@@ -6614,7 +6697,7 @@
});
ov.classList.remove('is-hidden');
function goLobby() {
window.location.href = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick);
window.location.href = buildRoomLobbyReturnHref();
}
if (btn) {
btn.onclick = () => {
@@ -7117,7 +7200,7 @@
});
ov.classList.remove('is-hidden');
function goLobby() {
window.location.href = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick);
window.location.href = buildRoomLobbyReturnHref();
}
if (btn) {
btn.onclick = () => {
@@ -8561,14 +8644,27 @@
const errMsg = (res && res.error) || 'เข้าร่วมไม่ได้';
alert(errMsg);
const caseLocked = /เริ่มคดี|ไม่รับผู้เล่น/.test(errMsg);
if (caseLocked) {
window.location.replace('room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick));
const detectiveReturn = params.get('detectiveReturn') === '1'
|| (function () { try { return sessionStorage.getItem('detectiveMinigameReturn') === '1'; } catch (e) { return false; } })();
if (caseLocked || detectiveReturn) {
window.location.replace(buildRoomLobbyReturnHref());
} else {
window.location.replace(BASE + '/lobby.html');
}
return;
}
myId = socket.id;
let botSlots = parseInt(res.botSlotCount, 10);
if ((!botSlots || botSlots < 1) && res.maxPlayers > 0 && res.maxPlayers < 6) {
botSlots = 6 - res.maxPlayers;
}
if (botSlots > 0) {
detectiveCaseFillBots = true;
detectiveBotSlotCount = Math.min(6, Math.max(0, botSlots));
}
if (isDetectiveMinigamePlay()) {
try { document.documentElement.classList.add('play-detective-minigame'); } catch (eD) { /* ignore */ }
}
if (res.quizCarrySettingsSnap && typeof res.quizCarrySettingsSnap === 'object') {
quizCarryJoinSettingsSnap = res.quizCarrySettingsSnap;
if (res.quizCarrySettingsSnap.carryMapPanelTheme && typeof res.quizCarrySettingsSnap.carryMapPanelTheme === 'object') {
@@ -8935,7 +9031,7 @@
});
ov.classList.remove('is-hidden');
function goLobby() {
window.location.href = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick);
window.location.href = buildRoomLobbyReturnHref();
}
if (btn) {
btn.onclick = () => {
@@ -9662,7 +9758,7 @@
}
function stepPreviewBots() {
if (!previewFillBots || !mapData || isFrogger() || isGauntlet() || isStack() || isJumpSurvive() || isSpaceShooter() || isBalloonBoss()) return;
if (!playBotsEnabled() || !mapData || isFrogger() || isGauntlet() || isStack() || isJumpSurvive() || isSpaceShooter() || isBalloonBoss()) return;
if (isQuizCarry()) {
stepQuizCarryPreviewBots();
return;
@@ -9887,7 +9983,7 @@
/** Stack preview: HUD แนว cyberdeck — SCORE ซ้าย, ตาปัจจุบันขวา, เทอร์มินัลล่างซ้าย */
function drawStackPreviewCyberHud(ctx, cw, ch) {
if (!previewFillBots || !stackMini || !mapData || !isStack()) return;
if (!playBotsEnabled() || !stackMini || !mapData || !isStack()) return;
const now = Date.now();
const pad = 12;
const currentSeat = getStackPreviewCurrentSeat();
@@ -9968,7 +10064,7 @@
const boardW = Math.min(248, Math.max(200, cw * 0.36));
const rowH = 42;
const headH = 38;
const boardH = headH + STACK_PREVIEW_TURN_COUNT * rowH + 10;
const boardH = headH + stackTurnCount * rowH + 10;
ctx.fillStyle = 'rgba(8, 12, 28, 0.9)';
roundRectPath(pad, 8, boardW, boardH, 8);
@@ -10031,12 +10127,12 @@
});
};
if (order && order.length === STACK_PREVIEW_TURN_COUNT) {
if (order && order.length === stackTurnCount) {
drawRows(order);
} else {
const bots = [...others.keys()].filter(isPreviewBotId).sort();
const rows = [{ kind: 'human', seat: 1 }];
for (let i = 0; i < STACK_PREVIEW_TURN_COUNT - 1; i++) {
for (let i = 0; i < stackTurnCount - 1; i++) {
rows.push({ kind: 'bot', seat: i + 2, botId: bots[i] || null });
}
drawRows(rows);
@@ -10059,7 +10155,7 @@
ctx.stroke();
let curEntry = null;
if (order && order.length === STACK_PREVIEW_TURN_COUNT) {
if (order && order.length === stackTurnCount) {
curEntry = order.find((e) => e.seat === currentSeat) || null;
}
const curV = curEntry ? resolveEntryVisual(curEntry) : resolveEntryVisual({ kind: 'human', seat: 1 });
+12 -2
View File
@@ -290,6 +290,15 @@
height: 61px;
object-fit: contain;
}
.suspect-pick-hint {
margin: 8px 0 0;
width: 100%;
text-align: center;
font: 600 15px/1.35 system-ui, "Segoe UI", "Kanit", sans-serif;
color: rgba(224, 230, 255, 0.92);
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.55);
}
.suspect-pick-hint.is-hidden { display: none !important; }
.suspect-pick-actions {
display: none;
width: 100%;
@@ -1115,8 +1124,9 @@
</div>
</button>
</div>
<p id="suspect-pick-hint" class="suspect-pick-hint is-hidden" aria-live="polite">เลือกการ์ดผู้ต้องสงสัยก่อน แล้วกดเริ่มสืบสวน</p>
<div class="suspect-pick-actions" id="suspect-pick-actions" aria-hidden="true">
<button type="button" class="suspect-btn-start" id="suspect-btn-start" title="เริ่มสืบสวน" aria-label="เริ่มสืบสวน">
<button type="button" class="suspect-btn-start" id="suspect-btn-start" title="เริ่มสืบสวน (หลังเลือกการ์ดแล้ว)" aria-label="เริ่มสืบสวน">
<img src="/Main-Lobby/IMAGE/Choose%20a%20suspect/suspect-start.png" alt="เริ่มสืบสวน" width="253" height="116" decoding="async">
</button>
</div>
@@ -1523,7 +1533,7 @@
<script src="/app-base.js?v=1"></script>
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0122"></script>
<script src="js/room-lobby.js?v=0.0192"></script>
<script src="js/room-lobby.js?v=0.0202"></script>
<div class="version-tag">v —</div>
</body>
</html>
+315 -49
View File
@@ -11,8 +11,32 @@ const ADMIN_PASSWORD = process.env.GAME_AI_ADMIN_PASSWORD || 'game-ai-admin';
const adminTokens = new Set();
/** Lobby หลังคดี — ไม่ลบออกจาก spaces ตอน prune รายการห้องว่าง */
const POST_CASE_LOBBY_SPACE_ID = 'mn8nx46h';
/** หลัง host เลือกผู้ต้องสงสัยแล้วกด «เริ่มสืบสวน» — ย้ายทุกคนไปฉากควิซนี้และเริ่มเกม */
/** หลัง host เลือกผู้ต้องสงสัยแล้วกด «เริ่มสืบสวน» — ฉากควิซเดิม (fallback) */
const SUSPECT_INVESTIGATION_QUIZ_MAP_ID = 'mng8a80o';
/** 7 Minigame จาก Admin — สุ่ม 3 แบบไม่ซ้ำผูกการ์ดผู้ต้องสงสัย (ล็อกจนกว่าสร้างห้องใหม่) */
/** ตรงแท็บ Admin Minigame 17 (ไม่รวม Quiz Battle / กบ frogger เก่า) */
const DETECTIVE_MINIGAME_POOL = [
{ key: 'quiz', mapId: 'mng8a80o', labelTh: 'Minigame-1 จริงหรือไม่' },
{ key: 'gauntlet', mapId: 'mno9kb07', labelTh: 'Minigame-2 วิ่งหลบสิ่งกีดขวาง' },
{ key: 'stack', mapId: 'mnn93hpi', labelTh: 'Minigame-3 Tower block' },
{ key: 'quiz_carry', mapId: 'mnorwqx1', labelTh: 'Minigame-4 คำศัพท์ในกระบวนการยุติธรรม' },
{ key: 'jump_survive', mapId: 'mnptfts2', labelTh: 'Minigame-5 กระโดดขึ้นแท่น' },
{ key: 'space_shooter', mapId: 'mnpz6rkp', labelTh: 'Minigame-6 ยิงอุกาบาต Violent Crime' },
{ key: 'balloon_boss', mapId: 'mnq1eml7', labelTh: 'Minigame-7 ยิง Mega Virus' },
];
const LOBBY_SLOT_TOTAL = 6;
/** จำนวน Bot จาก Create Room — ถ้าไม่เคยบันทึก ใช้ 6 − maxPlayers (เช่น 3 คน → 3 Bot) */
function effectiveBotSlotCount(space) {
let n = parseInt(space && space.botSlotCount, 10);
if (isNaN(n) || n < 0) n = 0;
const maxP = space && space.maxPlayers ? parseInt(space.maxPlayers, 10) : 0;
if (n === 0 && maxP > 0 && maxP < LOBBY_SLOT_TOTAL) {
n = LOBBY_SLOT_TOTAL - maxP;
if (space) space.botSlotCount = n;
}
return Math.min(LOBBY_SLOT_TOTAL, Math.max(0, n));
}
/** Stack Tower ภารกิจ — lobby READY / START เดียวกับ quiz mission (mng8a80o) */
const STACK_TOWER_MISSION_MAP_ID = 'mnn93hpi';
/** Jump Survival ภารกิจ Jumper — lobby READY / START เดียวกับ Stack Tower */
@@ -1585,9 +1609,207 @@ function shuffleQuizQuestions(arr) {
return a;
}
function buildPeersSnapForSpace(space, extraFields) {
return [...space.peers.values()].map((p) => {
const row = {
id: p.id,
x: p.x,
y: p.y,
direction: p.direction || 'down',
nickname: p.nickname,
ready: !!p.ready,
characterId: p.characterId ?? null,
};
if (extraFields && extraFields.gauntlet) {
row.gauntletJumpTicks = p.gauntletJumpTicks || 0;
row.gauntletScore = p.gauntletScore || 0;
row.gauntletEliminated = !!p.gauntletEliminated;
}
return row;
});
}
/** สุ่ม 3 minigame จาก pool 7 แบบ — เรียกครั้งเดียวตอนเข้า LobbyB (ล็อกบน space) */
/** แมปเก่า (กบ frogger) → Minigame-2 Gauntlet ตาม Admin */
function normalizeDetectiveCardEntry(entry) {
if (!entry) return entry;
const mid = entry.mapId != null ? String(entry.mapId).trim() : '';
if (mid === 'mlpecp2j' || entry.key === 'frogger') {
const gaunt = DETECTIVE_MINIGAME_POOL.find((e) => e.key === 'gauntlet');
if (gaunt && maps.has(gaunt.mapId)) {
const md = maps.get(gaunt.mapId);
return {
...entry,
key: gaunt.key,
mapId: gaunt.mapId,
labelTh: gaunt.labelTh,
mapName: (md && md.name) || gaunt.mapId,
gameType: (md && md.gameType) || 'gauntlet',
};
}
}
return entry;
}
function assignDetectiveSuspectCardMinigames(space) {
const available = DETECTIVE_MINIGAME_POOL.filter((e) => maps.has(e.mapId));
const shuffled = shuffleQuizQuestions(available.length ? available : [{ key: 'quiz', mapId: SUSPECT_INVESTIGATION_QUIZ_MAP_ID, labelTh: 'QuestionGame' }]);
space.suspectCardMinigames = shuffled.slice(0, 3).map((entry, i) => {
const md = maps.get(entry.mapId);
return {
cardIndex: i,
key: entry.key,
mapId: entry.mapId,
labelTh: entry.labelTh,
mapName: (md && md.name) || entry.mapId,
gameType: (md && md.gameType) || '',
};
});
}
/** เริ่มมินิเกมตามการ์ดที่เลือก (หลังกดเริ่มสืบสวน) */
function beginDetectiveSuspectMinigame(sid, space, cardEntry, selectedIndex) {
const mapId = cardEntry && cardEntry.mapId ? String(cardEntry.mapId).trim() : '';
const md = mapId && maps.has(mapId) ? maps.get(mapId) : null;
if (!md) return { ok: false, error: 'ไม่พบฉากเกมบนเซิร์ฟเวอร์' };
space.suspectPhaseActive = false;
space.detectiveMinigameActive = true;
space.detectiveMinigameCardIndex = cardEntry.cardIndex;
space.gauntletReturnMapId = POST_CASE_LOBBY_SPACE_ID;
clearSpaceQuizTimers(space);
if (space.quizSession) space.quizSession.active = false;
space.quizSession = null;
space.mapId = mapId;
space.mapData = md;
let si = 0;
space.peers.forEach((p) => {
const sp = pickSpawnForJoin(md, si++);
p.x = sp.x;
p.y = sp.y;
p.direction = 'down';
p.gauntletJumpTicks = 0;
p.gauntletScore = 0;
p.gauntletJumpPending = false;
p.gauntletEliminated = false;
});
const peersSnap = buildPeersSnapForSpace(space, { gauntlet: md.gameType === 'gauntlet' });
const invPayload = {
selectedIndex,
cardIndex: cardEntry.cardIndex,
minigameKey: cardEntry.key,
minigameLabel: cardEntry.labelTh,
mapId,
};
io.to(sid).emit('suspect-investigation-start', invPayload);
if (md.gameType === 'quiz') {
const qErr = validateQuizMap(md);
if (qErr) {
space.detectiveMinigameActive = false;
space.gauntletReturnMapId = null;
return { ok: false, error: qErr };
}
io.to(sid).emit('game-start', {
quizMode: true,
stayInRoomLobby: true,
mapId,
peersSnap,
detectiveMinigame: true,
minigameLabel: cardEntry.labelTh,
});
setTimeout(() => {
const spNow = spaces.get(sid);
if (!spNow || !spNow.peers.size) return;
const mdLive = maps.get(mapId);
if (!mdLive || mdLive.gameType !== 'quiz') return;
startQuizGame(sid, spNow, mdLive);
}, 900);
return { ok: true, mapId, gameType: md.gameType };
}
stopGauntletTicker(space);
space.gauntletRun = null;
if (md.gameType === 'gauntlet') {
initGauntletPeersAll(space, md);
ensureGauntletEndsAtIfNeeded(space);
startGauntletTicker(sid, space);
} else if (isBalloonBossMissionShellSpace(space) || isQuizQuestionMissionShellSpace(space) || isStackTowerMissionShellSpace(space) || isJumpSurviveMissionShellSpace(space) || isSpaceShooterMissionShellSpace(space) || md.gameType === 'quiz_carry') {
if (!space.gauntletCrownLobbyReady || typeof space.gauntletCrownLobbyReady !== 'object') space.gauntletCrownLobbyReady = {};
space.peers.forEach((_p, id) => { space.gauntletCrownLobbyReady[id] = false; });
space.gauntletRun = newBalloonBossShellGauntletRunState();
}
const gameStartPayload = {
mapId,
peersSnap,
detectiveMinigame: true,
detectiveReturnLobbyB: true,
minigameLabel: cardEntry.labelTh,
};
if (md.gameType === 'gauntlet' && space.gauntletRun) {
gameStartPayload.gauntletEndsAt = space.gauntletRun.endsAt != null ? space.gauntletRun.endsAt : null;
}
if (space.gauntletRun && space.gauntletRun.crownRunHeld) {
gameStartPayload.gauntletCrownRunHeld = true;
}
io.to(sid).emit('game-start', gameStartPayload);
return { ok: true, mapId, gameType: md.gameType };
}
function returnDetectiveSpaceToLobbyB(sid, space, message) {
ensurePostCaseLobbyMapLoaded();
clearSpaceQuizTimers(space);
if (space.quizSession) space.quizSession.active = false;
space.quizSession = null;
space.detectiveMinigameActive = false;
space.gauntletReturnMapId = null;
stopGauntletTicker(space);
space.gauntletRun = null;
const lb = maps.get(POST_CASE_LOBBY_SPACE_ID);
if (!lb) {
io.to(sid).emit('quiz-ended', { message: message || 'จบมินิเกม — ไม่พบ LobbyB', returnToLobbyB: true });
return;
}
space.mapId = POST_CASE_LOBBY_SPACE_ID;
space.mapData = lb;
space.suspectPhaseActive = true;
let gi = 0;
space.peers.forEach((p) => {
const sp = pickSpawnForJoin(lb, gi++);
p.x = sp.x;
p.y = sp.y;
p.direction = 'down';
p.gauntletJumpTicks = 0;
p.gauntletScore = 0;
p.gauntletJumpPending = false;
p.gauntletEliminated = false;
});
const peersSnap = buildPeersSnapForSpace(space);
const payload = {
stayInRoomLobby: true,
mapId: POST_CASE_LOBBY_SPACE_ID,
peersSnap,
suspectPhaseActive: true,
suspectPickIndex: typeof space.suspectPickIndex === 'number' ? space.suspectPickIndex : 0,
cardMinigames: space.suspectCardMinigames || [],
detectiveReturn: true,
};
io.to(sid).emit('quiz-ended', { message: message || 'จบมินิเกม — กลับ LobbyB', returnToLobbyB: true });
io.to(sid).emit('detective-minigame-ended', payload);
io.to(sid).emit('game-start', payload);
}
function endQuizGame(sid, space, message) {
clearSpaceQuizTimers(space);
if (space.quizSession) space.quizSession.active = false;
if (space.detectiveMinigameActive) {
returnDetectiveSpaceToLobbyB(sid, space, message);
space.quizSession = null;
return;
}
io.to(sid).emit('quiz-ended', { message: message || 'จบเกม' });
space.quizSession = null;
}
@@ -2837,9 +3059,17 @@ const server = http.createServer((req, res) => {
if (mapData.gameType === 'gauntlet' || mapData.gameType === 'space_shooter' || mapData.gameType === 'balloon_boss') maxPlayers = Math.min(maxPlayers, 6);
const mapId = d.mapId ? String(d.mapId).trim() : null;
const previewEditorTest = isPrivate && String(spaceName).toLowerCase() === 'preview';
let botSlotCount = parseInt(d.botSlotCount, 10);
if (isNaN(botSlotCount) || botSlotCount < 0) botSlotCount = 0;
botSlotCount = effectiveBotSlotCount({ botSlotCount, maxPlayers });
spaces.set(sid, {
mapData, mapId, peers: new Map(), spaceName, hostId: null, isPrivate, maxPlayers, createdAt: Date.now(),
previewEditorTest,
botSlotCount,
suspectCardMinigames: null,
suspectPhaseActive: false,
suspectPickIndex: 0,
detectiveMinigameActive: false,
});
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true, spaceId: sid, roomCode: isPrivate ? sid : null }));
@@ -3419,6 +3649,11 @@ function findFallbackLobbyMapForGauntletEnd() {
function endGauntletGame(sid, space, reason) {
const md = (space.mapId && maps.get(space.mapId)) || space.mapData;
if (!md || md.gameType !== 'gauntlet') return;
if (space.detectiveMinigameActive) {
const msg = reason === 'time' ? 'หมดเวลา — จบมินิเกม' : 'จบมินิเกม';
returnDetectiveSpaceToLobbyB(sid, space, msg);
return;
}
stopGauntletTicker(space);
const wasCrown = isGauntletCrownHeistMapBySpace(space);
const crownMission = wasCrown ? buildGauntletCrownMissionPayload(space) : null;
@@ -3724,6 +3959,28 @@ function isMapTileWalkableForSpawn(md, x, y) {
return true;
}
function mapCharacterFootprintWH(md) {
const cw = Math.max(1, Math.min(4, Math.floor(Number(md && md.characterCellsW) || Number(md && md.characterCells) || 1)));
const ch = Math.max(1, Math.min(4, Math.floor(Number(md && md.characterCellsH) || Number(md && md.characterCells) || 1)));
return { cw, ch };
}
/** จุดเกิด = มุมซ้ายบนของ footprint — ต้องเดินได้ทั้งกล่อง (ตรง editor / room-lobby) */
function isMapSpawnAnchorWalkable(md, x, y) {
if (!isMapTileWalkableForSpawn(md, x, y)) return false;
const { cw, ch } = mapCharacterFootprintWH(md);
const w = md.width || 20;
const h = md.height || 15;
const maxX = Math.min(w, x + cw);
const maxY = Math.min(h, y + ch);
for (let ty = y; ty < maxY; ty++) {
for (let tx = x; tx < maxX; tx++) {
if (!isMapTileWalkableForSpawn(md, tx, ty)) return false;
}
}
return true;
}
/** แปลง lobbyPlayerSpawns จากแมปเป็นอาร์เรย์ยาว 6 ช่อง (null หรือ {x,y}) */
function parseLobbyPlayerSpawnsFromMap(md) {
const w = md.width || 20;
@@ -3784,14 +4041,14 @@ function pickSpawnForJoin(md, joinOrderIndex) {
const h = md.height || 15;
const x = Math.max(0, Math.min(w - 1, fx));
const y = Math.max(0, Math.min(h - 1, fy));
if (isMapTileWalkableForSpawn(md, x, y)) return { x, y };
if (isMapSpawnAnchorWalkable(md, x, y)) return { x, y };
return pickRandomSpawnFromMap(md);
}
if (mode === 'slots6') {
const slots = parseLobbyPlayerSpawnsFromMap(md);
augmentLobbySlotsFromShooterPaintJumpSurvive(md, slots);
const pick = slots[j];
if (pick && isMapTileWalkableForSpawn(md, pick.x, pick.y)) return { x: pick.x, y: pick.y };
if (pick && isMapSpawnAnchorWalkable(md, pick.x, pick.y)) return { x: pick.x, y: pick.y };
return pickRandomSpawnFromMap(md);
}
return pickRandomSpawnFromMap(md);
@@ -3882,7 +4139,7 @@ function spaceAllowsQuizCarryLobbyRelaxed(space) {
}
io.on('connection', (socket) => {
socket.on('join-space', ({ spaceId, nickname, characterId }, cb) => {
socket.on('join-space', ({ spaceId, nickname, characterId, playMapId }, cb) => {
const space = spaces.get(spaceId);
if (!space || !space.mapData) return cb && cb({ ok: false, error: 'ไม่พบห้อง' });
const maxPlayers = space.maxPlayers || 10;
@@ -3928,6 +4185,14 @@ io.on('connection', (socket) => {
peer.y = sn.y;
}
space.peers.set(socket.id, peer);
/* กลับ LobbyB เฉพาะเมื่อไม่ได้เข้า play ฉากมินิเกมที่กำลังเล่น (เช่น refresh room-lobby) */
if (space.detectiveMinigameActive) {
const clientMap = playMapId != null ? String(playMapId).trim() : '';
const activeMap = space.mapId != null ? String(space.mapId).trim() : '';
if (!clientMap || !activeMap || clientMap !== activeMap) {
returnDetectiveSpaceToLobbyB(spaceId, space, 'กลับ LobbyB');
}
}
if (spaceAllowsQuizCarryLobbyRelaxed(space)) {
if (!space.quizCarryLobbyReady || typeof space.quizCarryLobbyReady !== 'object') space.quizCarryLobbyReady = {};
space.quizCarryLobbyReady[socket.id] = false;
@@ -3955,10 +4220,10 @@ io.on('connection', (socket) => {
io.to(spaceId).emit('gauntlet-crown-lobby-sync', { readyMap: { ...space.gauntletCrownLobbyReady } });
}
const peersList = [...space.peers.values()];
const mapData = mdJoin;
const mapDataOut = (space.mapId && maps.get(space.mapId)) || space.mapData || mdJoin;
const joinCb = {
ok: true,
mapData,
mapData: mapDataOut,
mapId: space.mapId || null,
peers: peersList,
hostId: space.hostId,
@@ -3968,6 +4233,9 @@ io.on('connection', (socket) => {
maxPlayers: space.maxPlayers || 10,
suspectPhaseActive: !!space.suspectPhaseActive && serverMapIsPostCaseLobbyB(space),
suspectPickIndex: typeof space.suspectPickIndex === 'number' ? space.suspectPickIndex : 0,
cardMinigames: space.suspectCardMinigames || [],
botSlotCount: effectiveBotSlotCount(space),
joinLocked: !!space.joinLocked,
};
if (mdJoin.gameType === 'quiz_carry' || spaceAllowsQuizCarryLobbyRelaxed(space)) {
try {
@@ -4044,11 +4312,15 @@ io.on('connection', (socket) => {
let idx = Math.floor(Number(space.suspectPickIndex));
if (Number.isNaN(idx) || idx < 0 || idx > 2) idx = 0;
space.suspectPickIndex = idx;
if (!space.suspectCardMinigames || space.suspectCardMinigames.length < 3) {
assignDetectiveSuspectCardMinigames(space);
}
space.suspectPhaseActive = true;
io.to(sid).emit('suspect-phase-open', {
hostId: space.hostId,
selectedIndex: space.suspectPickIndex,
disruptorAccepted: accept
disruptorAccepted: accept,
cardMinigames: space.suspectCardMinigames || [],
});
socket.emit('troublesome-ack', { ok: true, accept });
});
@@ -4068,6 +4340,9 @@ io.on('connection', (socket) => {
const reply = typeof cb === 'function' ? cb : () => {};
const sid = socket.data.spaceId;
const space = sid ? spaces.get(sid) : null;
if (space && Array.isArray(space.suspectCardMinigames)) {
space.suspectCardMinigames = space.suspectCardMinigames.map(normalizeDetectiveCardEntry);
}
if (!space || !serverMapIsPostCaseLobbyB(space)) {
return reply({ ok: false, error: 'ไม่พร้อม — ต้องอยู่โถง LobbyB' });
}
@@ -4077,49 +4352,30 @@ io.on('connection', (socket) => {
if (space.hostId !== socket.id) {
return reply({ ok: false, error: 'เฉพาะ host กดเริ่มสืบสวนได้' });
}
const md = maps.get(SUSPECT_INVESTIGATION_QUIZ_MAP_ID);
if (!md) {
return reply({ ok: false, error: 'ไม่พบฉากเกม (mng8a80o) บนเซิร์ฟเวอร์' });
if (!space.suspectCardMinigames || space.suspectCardMinigames.length < 3) {
assignDetectiveSuspectCardMinigames(space);
}
const qErr = validateQuizMap(md);
if (qErr) {
return reply({ ok: false, error: qErr });
}
space.suspectPhaseActive = false;
clearSpaceQuizTimers(space);
space.mapId = SUSPECT_INVESTIGATION_QUIZ_MAP_ID;
space.mapData = md;
let si = 0;
space.peers.forEach((p) => {
const sp = pickSpawnForJoin(md, si++);
p.x = sp.x;
p.y = sp.y;
p.direction = 'down';
});
const peersSnap = [...space.peers.values()].map((p) => ({
id: p.id,
x: p.x,
y: p.y,
direction: p.direction || 'down',
nickname: p.nickname,
ready: !!p.ready,
characterId: p.characterId ?? null
}));
const selectedIndex = typeof space.suspectPickIndex === 'number' ? space.suspectPickIndex : 0;
io.to(sid).emit('suspect-investigation-start', { selectedIndex });
io.to(sid).emit('game-start', {
quizMode: true,
stayInRoomLobby: true,
mapId: SUSPECT_INVESTIGATION_QUIZ_MAP_ID,
peersSnap
});
setTimeout(() => {
const spNow = spaces.get(sid);
if (!spNow || !spNow.peers.has(socket.id)) return;
const mdLive = maps.get(SUSPECT_INVESTIGATION_QUIZ_MAP_ID);
if (!mdLive) return;
startQuizGame(sid, spNow, mdLive);
}, 900);
const cardEntry = space.suspectCardMinigames[selectedIndex];
if (!cardEntry || !cardEntry.mapId) {
return reply({ ok: false, error: 'ยังไม่ได้สุ่มมินิเกมสำหรับการ์ดนี้' });
}
const started = beginDetectiveSuspectMinigame(sid, space, cardEntry, selectedIndex);
reply(started);
});
socket.on('detective-minigame-finished', (_data, cb) => {
const reply = typeof cb === 'function' ? cb : () => {};
const sid = socket.data.spaceId;
const space = sid ? spaces.get(sid) : null;
if (!space || !space.peers.has(socket.id)) {
return reply({ ok: false, error: 'ไม่อยู่ในห้อง' });
}
if (!space.detectiveMinigameActive) {
if (serverMapIsPostCaseLobbyB(space)) return reply({ ok: true, alreadyOnLobbyB: true });
return reply({ ok: false, error: 'ไม่ได้อยู่ในมินิเกมสืบสวน' });
}
returnDetectiveSpaceToLobbyB(sid, space, 'จบมินิเกม — กลับ LobbyB');
reply({ ok: true });
});
@@ -4175,6 +4431,7 @@ io.on('connection', (socket) => {
return reply({ ok: false, error: 'ยืนในพื้นที่เริ่มเกม (สีส้มในเอดิเตอร์) ก่อนกดเริ่ม' });
}
if (isLobbyADetectiveStart) {
assignDetectiveSuspectCardMinigames(space);
space.mapId = POST_CASE_LOBBY_SPACE_ID;
space.mapData = maps.get(POST_CASE_LOBBY_SPACE_ID);
const caseNickWl = new Set();
@@ -4216,7 +4473,8 @@ io.on('connection', (socket) => {
mapId: POST_CASE_LOBBY_SPACE_ID,
lobbyLevel,
caseId,
peersSnap
peersSnap,
cardMinigames: space.suspectCardMinigames || [],
});
return reply({ ok: true });
}
@@ -4319,6 +4577,13 @@ io.on('connection', (socket) => {
stopGauntletTicker(space);
clearSpaceQuizTimers(space);
space.quizSession = null;
/* ระหว่างมินิเกมสืบสวน / คดีล็อก — อย่าลบห้องเมื่อเปลี่ยนหน้า room-lobby → play.html (รอ join กลับ) */
const keepCaseSpace = !!(space.detectiveMinigameActive
|| (space.joinLocked && space.caseParticipantNicknames && space.caseParticipantNicknames.size > 0));
if (keepCaseSpace) {
space.hostId = null;
return;
}
spaces.delete(sid);
} else {
if (wasHost) {
@@ -4607,6 +4872,7 @@ io.on('connection', (socket) => {
if (px + dx === tx && py + dy === ty) { near = true; break; }
}
if (!near) return reply({ ok: false, error: 'เข้าใกล้จุดสีเขียว (ช่องโต้ตอบ) ก่อน' });
if (!p.ready) return reply({ ok: false, error: 'กดพร้อมก่อน แล้วค่อยกด F ที่ช่องสีเขียว' });
const nick = (p.nickname || '').trim() || 'ผู้เล่น';
io.to(sid).emit('lobby-interact', { id: socket.id, nickname: nick, x: tx, y: ty });
return reply({ ok: true });