game flow1.1
This commit is contained in:
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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 });
|
||||
|
||||
@@ -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
@@ -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 1–7 (ไม่รวม 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 });
|
||||
|
||||
Reference in New Issue
Block a user