update sync game 1-7
This commit is contained in:
@@ -394,14 +394,14 @@
|
||||
{
|
||||
"id": "0bf4718eeec4434c080d6057",
|
||||
"email": "",
|
||||
"displayName": "Guest",
|
||||
"displayName": "MONE",
|
||||
"loginType": "guest",
|
||||
"providerUserId": "p_1781851275186_szcmp5pu3s",
|
||||
"notes": "auto: player-coins",
|
||||
"blocked": false,
|
||||
"coins": 10,
|
||||
"coins": 158,
|
||||
"createdAt": "2026-06-19T06:41:15+00:00",
|
||||
"updatedAt": "2026-06-19T07:00:09+00:00",
|
||||
"updatedAt": "2026-06-21T17:18:58+00:00",
|
||||
"daily": {
|
||||
"anchorMs": 1781802000000,
|
||||
"claimedDays": [
|
||||
@@ -413,7 +413,20 @@
|
||||
false,
|
||||
false
|
||||
],
|
||||
"lockUntilMs": 1781888400000
|
||||
"lockUntilMs": 0
|
||||
},
|
||||
"score": 138,
|
||||
"scoreByCase": {
|
||||
"10": 20,
|
||||
"13": 36,
|
||||
"12": 54,
|
||||
"4": 28
|
||||
},
|
||||
"lbName": "MONE",
|
||||
"achievements": {
|
||||
"d1_minigame_solver": 13,
|
||||
"a1_first_deduction": 1,
|
||||
"a5_truth_hunter": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -541,33 +554,44 @@
|
||||
"providerUserId": "p_1781862203558_jhynnz90qrg",
|
||||
"notes": "auto: player-coins",
|
||||
"blocked": false,
|
||||
"coins": 18,
|
||||
"coins": 182,
|
||||
"createdAt": "2026-06-19T09:43:23+00:00",
|
||||
"updatedAt": "2026-06-21T09:15:13+00:00",
|
||||
"updatedAt": "2026-06-22T07:38:20+00:00",
|
||||
"lobbyColorThemeIndex": 6,
|
||||
"lobbySkinToneIndex": 1,
|
||||
"achievements": {
|
||||
"d1_minigame_solver": 2
|
||||
"d1_minigame_solver": 17
|
||||
},
|
||||
"score": 18,
|
||||
"score": 162,
|
||||
"scoreByCase": {
|
||||
"10": 18
|
||||
"10": 44,
|
||||
"13": 38,
|
||||
"12": 54,
|
||||
"4": 26
|
||||
},
|
||||
"lbName": "ผู้เล่น"
|
||||
},
|
||||
{
|
||||
"id": "fbb262e6c2aa47bd97aef554",
|
||||
"email": "",
|
||||
"displayName": "Guest",
|
||||
"displayName": "MONE",
|
||||
"loginType": "guest",
|
||||
"providerUserId": "p_1781938677448_8j2wnr5ha1b",
|
||||
"notes": "auto: player-coins",
|
||||
"blocked": false,
|
||||
"coins": 0,
|
||||
"coins": 28,
|
||||
"createdAt": "2026-06-20T06:57:57+00:00",
|
||||
"updatedAt": "2026-06-20T06:58:53+00:00",
|
||||
"updatedAt": "2026-06-22T07:38:20+00:00",
|
||||
"lobbyColorThemeIndex": 7,
|
||||
"lobbySkinToneIndex": 1
|
||||
"lobbySkinToneIndex": 1,
|
||||
"achievements": {
|
||||
"d1_minigame_solver": 3
|
||||
},
|
||||
"lbName": "MONE",
|
||||
"score": 18,
|
||||
"scoreByCase": {
|
||||
"13": 18
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "8f0741532038e900e759c5ef",
|
||||
|
||||
+531
-238
File diff suppressed because it is too large
Load Diff
@@ -174,6 +174,10 @@
|
||||
let suspectSelectedIndex = 0;
|
||||
/** ตรงกับเซิร์ฟ — เฟสเลือกผู้ต้องสงสัยยังไม่จบ (ปิด overlay แล้วยังเปิดได้จากปุ่มโถง) */
|
||||
let serverSuspectPhaseActive = false;
|
||||
/** จำนวนผู้เล่นที่กลับเข้า LobbyB แล้ว/ทั้งหมด — host เริ่มมินิเกมรอบถัดไปได้เมื่อครบ (allHere) */
|
||||
let lobbyBPresentCount = 0;
|
||||
let lobbyBTotalCount = 0;
|
||||
let lobbyBAllHere = true;
|
||||
/** มินิเกม 3 แบบที่สุ่มแล้ว — ล็อกกับการ์ด 0–2 จนกว่าสร้างห้องใหม่ */
|
||||
let suspectCardMinigames = [];
|
||||
/** หลักฐาน (ติกถูก) ที่เก็บได้ต่อผู้ต้องสงสัย 0–2 (เล่นมินิเกมจบ +1 ต่อช่อง สูงสุด 3) */
|
||||
@@ -6274,7 +6278,9 @@
|
||||
const accuseBtn = document.getElementById('suspect-btn-accuse');
|
||||
const startBtn = document.getElementById('suspect-btn-start');
|
||||
const hint = document.getElementById('suspect-pick-hint');
|
||||
const canStartInvestigation = isHost && canInvestigateSuspect(suspectSelectedIndex);
|
||||
/* ต้องรอผู้เล่นทุกคนกลับเข้า LobbyB ครบก่อน host ถึงเริ่มได้ (กันคนยังโหลดไม่เสร็จหลุดรอบ) */
|
||||
const waitingForPlayers = !lobbyBAllHere;
|
||||
const canStartInvestigation = isHost && canInvestigateSuspect(suspectSelectedIndex) && !waitingForPlayers;
|
||||
if (startBtn) {
|
||||
startBtn.disabled = !canStartInvestigation;
|
||||
startBtn.classList.toggle('suspect-btn-start--disabled', !canStartInvestigation);
|
||||
@@ -6283,6 +6289,8 @@
|
||||
if (hint) {
|
||||
if (!isHost) {
|
||||
hint.textContent = 'รอ Host เลือกการ์ดและกดเริ่มสืบสวน';
|
||||
} else if (waitingForPlayers) {
|
||||
hint.textContent = 'รอผู้เล่นกลับเข้าห้องให้ครบก่อน (' + lobbyBPresentCount + '/' + lobbyBTotalCount + ') แล้วจึงเริ่มได้';
|
||||
} else if (!canInvestigateSuspect(suspectSelectedIndex)) {
|
||||
hint.textContent = 'สืบผู้ต้องสงสัยคนนี้ครบ 3 หลักฐานแล้ว — เลือกคนอื่นเพื่อสืบต่อ';
|
||||
} else if (allSuspectsInvestigated()) {
|
||||
@@ -6409,6 +6417,15 @@
|
||||
applySuspectSelectionVisual(idx);
|
||||
});
|
||||
|
||||
/* เซิร์ฟแจ้งจำนวนผู้เล่นที่กลับเข้า LobbyB แล้ว — ใช้ล็อกปุ่มเริ่มจนกว่าจะครบทุกคน */
|
||||
socket.on('detective-lobbyb-presence', (data) => {
|
||||
if (!data) return;
|
||||
lobbyBPresentCount = Number(data.present) || 0;
|
||||
lobbyBTotalCount = Number(data.total) || 0;
|
||||
lobbyBAllHere = (data.allHere !== false) && (lobbyBTotalCount <= 0 || lobbyBPresentCount >= lobbyBTotalCount);
|
||||
if (suspectPickOverlayOpen) updateSuspectHostUi();
|
||||
});
|
||||
|
||||
// ===== ห้องสรุปหลักฐาน (ไต่สวน) =====
|
||||
socket.on('testimony-open', (data) => { openTestimony(data || {}); });
|
||||
socket.on('testimony-status', (data) => { testimonyStatusUpdate(data || {}); });
|
||||
|
||||
@@ -5205,7 +5205,7 @@
|
||||
<script src="/app-base.js?v=2"></script>
|
||||
<script src="/Game/socket.io/socket.io.js"></script>
|
||||
<script src="js/version.js?v=0.0306"></script>
|
||||
<script src="js/play.js?v=0.006131850"></script>
|
||||
<script src="js/play.js?v=0.006131863"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
<style id="qc-howto-mg2-fix">
|
||||
/* HOW TO PLAY ของ quiz_carry (Minigame-4) -> ให้เหมือน Minigame-2 (gch-mg2-mock)
|
||||
|
||||
@@ -1617,7 +1617,7 @@
|
||||
<script src="js/version.js?v=0.0122"></script>
|
||||
<script src="js/customize-popup.js?v=40" data-customize-triggers="" data-customize-asset-base="img/03-5-Customize"></script>
|
||||
<script src="js/achievements.js?v=0.002" data-asset-base="img/03-6-Profile"></script>
|
||||
<script src="js/room-lobby.js?v=0.0298"></script>
|
||||
<script src="js/room-lobby.js?v=0.0299"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+206
-19
@@ -3202,6 +3202,7 @@ function buildPeersSnapForSpace(space, extraFields) {
|
||||
spawnJoinOrder: typeof p.spawnJoinOrder === 'number' ? p.spawnJoinOrder : null,
|
||||
lobbyColorThemeIndex: p.lobbyColorThemeIndex || null,
|
||||
lobbySkinToneIndex: p.lobbySkinToneIndex || null,
|
||||
bannedSpectator: !!p.bannedSpectator,
|
||||
};
|
||||
if (extraFields && extraFields.gauntlet) {
|
||||
row.gauntletJumpTicks = p.gauntletJumpTicks || 0;
|
||||
@@ -3215,11 +3216,42 @@ function buildPeersSnapForSpace(space, extraFields) {
|
||||
/* payload ของ quiz-carry-lobby-sync — แนบ peers snapshot ไปด้วยเสมอ
|
||||
→ client (โดยเฉพาะ host) upsert others ที่ขาดหายให้ครบ กัน host ไม่เห็นตัวละคร client
|
||||
เมื่อ join/reconnect ไม่ทันรอบ (race ตอนเปลี่ยนหน้า lobbyB→play.html) */
|
||||
/* true ถ้า peer นี้คือผู้ถูกแบนรอบนี้ (Card 3 Ban) — เข้ามาดูเฉยๆ ไม่ต้อง Ready และไม่นับใน gate เริ่มเกม
|
||||
(เช็คทั้ง id เดิมตอนโหวต และ flag bannedSpectator ของ peer ที่ reconnect เข้า play.html) */
|
||||
function isBannedPeerForRun(space, id) {
|
||||
if (!space) return false;
|
||||
if (space.bannedThisRunPlayerId && id === space.bannedThisRunPlayerId) return true;
|
||||
const pp = space.peers && space.peers.get(id);
|
||||
return !!(pp && pp.bannedSpectator);
|
||||
}
|
||||
|
||||
/* สำเนา ready map โดยตัด id ผู้ถูกแบนออก — กัน client นับ banned เป็นผู้เล่นที่ต้องรอ Ready (host ค้าง N/N+1) */
|
||||
function readyMapWithoutBanned(space, srcMap) {
|
||||
const out = {};
|
||||
if (srcMap && typeof srcMap === 'object') {
|
||||
Object.keys(srcMap).forEach((id) => {
|
||||
if (!isBannedPeerForRun(space, id)) out[id] = srcMap[id];
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function quizCarryLobbySyncPayload(space) {
|
||||
const readyMap = (space && space.quizCarryLobbyReady && typeof space.quizCarryLobbyReady === 'object')
|
||||
? { ...space.quizCarryLobbyReady }
|
||||
: {};
|
||||
return { readyMap, peers: buildPeersSnapForSpace(space) };
|
||||
return {
|
||||
readyMap: readyMapWithoutBanned(space, space && space.quizCarryLobbyReady),
|
||||
peers: buildPeersSnapForSpace(space),
|
||||
expectedActive: (space && space.minigameExpectedActiveCount) || 0,
|
||||
};
|
||||
}
|
||||
|
||||
/* payload ของ gauntlet-crown-lobby-sync — ตัด banned ออกจาก readyMap เช่นกัน
|
||||
แนบ peers snapshot ด้วย (เหมือน quiz_carry) → client/host เติม others ที่ขาด เห็นจำนวนผู้เล่นเท่ากัน */
|
||||
function crownLobbySyncPayload(space) {
|
||||
return {
|
||||
readyMap: readyMapWithoutBanned(space, space && space.gauntletCrownLobbyReady),
|
||||
expectedActive: (space && space.minigameExpectedActiveCount) || 0,
|
||||
peers: buildPeersSnapForSpace(space),
|
||||
};
|
||||
}
|
||||
|
||||
/** สุ่ม 3 minigame จาก pool 7 แบบ — เรียกครั้งเดียวตอนเข้า LobbyB (ล็อกบน space) */
|
||||
@@ -3244,6 +3276,34 @@ function normalizeDetectiveCardEntry(entry) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
/* นับผู้เล่นในคดี (caseParticipantNicknames) ที่กลับเข้า LobbyB แล้วเทียบกับทั้งหมด
|
||||
ใช้ตรวจว่าครบหรือยังก่อนให้ host เริ่มมินิเกมรอบถัดไป */
|
||||
function detectiveLobbyBMissingParticipants(space) {
|
||||
const roster = space && space.caseParticipantNicknames;
|
||||
if (!roster || roster.size === 0) return { missing: 0, present: 0, total: 0 };
|
||||
const present = new Set();
|
||||
space.peers.forEach((p) => {
|
||||
if (!p || !p.nickname) return;
|
||||
const n = normalizeLobbyNickname(p.nickname);
|
||||
if (roster.has(n)) present.add(n);
|
||||
});
|
||||
const total = roster.size;
|
||||
return { missing: Math.max(0, total - present.size), present: present.size, total };
|
||||
}
|
||||
|
||||
/* แจ้งทุกคนใน LobbyB ว่ามีผู้เล่นกลับเข้ามาแล้วกี่คน/ทั้งหมดกี่คน → host ใช้เปิด/ปิดปุ่มเริ่ม + แสดงสถานะรอ */
|
||||
function emitDetectiveLobbyBPresence(sid, space) {
|
||||
if (!sid || !space) return;
|
||||
if (!serverMapIsPostCaseLobbyB(space)) return;
|
||||
const info = detectiveLobbyBMissingParticipants(space);
|
||||
if (info.total <= 0) return;
|
||||
io.to(sid).emit('detective-lobbyb-presence', {
|
||||
present: info.present,
|
||||
total: info.total,
|
||||
allHere: info.missing === 0,
|
||||
});
|
||||
}
|
||||
|
||||
function assignDetectiveSuspectCardMinigames(space) {
|
||||
const available = DETECTIVE_MINIGAME_POOL.filter((e) => maps.has(e.mapId));
|
||||
/** โหมดเทสต์ (admin game-timing): เลือกเกมต่อการ์ด 3 ใบ — ช่องที่ตั้ง key = เจาะจง, ช่องว่าง = สุ่ม */
|
||||
@@ -3458,6 +3518,9 @@ function beginDetectiveSuspectMinigame(sid, space, cardEntry, selectedIndex) {
|
||||
space.bannedThisRunPlayerId = space.bannedPlayerId || null;
|
||||
console.log('[ban-debug] startMinigame: bannedThisRunPlayerId=', space.bannedThisRunPlayerId);
|
||||
space.bannedPlayerId = null;
|
||||
/* จำนวนผู้เล่นที่ต้องเข้าเกม + กด Ready ให้ครบก่อน host เริ่มได้ (ไม่นับผู้ถูกแบน)
|
||||
สแนปจาก peers ใน LobbyB ตอนนี้ — กัน host เริ่มก่อน client โหลด/กด Ready ครบ (เกมไม่ sync/จบไม่พร้อมกัน) */
|
||||
space.minigameExpectedActiveCount = [...space.peers.keys()].filter((id) => !isBannedPeerForRun(space, id)).length;
|
||||
space.detectiveMinigameCardIndex = cardEntry.cardIndex;
|
||||
// จำว่ากำลังสืบผู้ต้องสงสัยคนไหน เพื่อเติมหลักฐาน (ติกถูก) ตอนเล่นจบ
|
||||
space.suspectActiveIndex = (typeof selectedIndex === 'number' && selectedIndex >= 0 && selectedIndex <= 2)
|
||||
@@ -7439,6 +7502,21 @@ io.on('connection', (socket) => {
|
||||
space.hostId = socket.id; /* ยังไม่มี host → คนนี้เป็น host (ชั่วคราวถ้ามี hostPlayerKey อยู่แล้ว) */
|
||||
if (!space.hostPlayerKey && joinPlayerKey) space.hostPlayerKey = joinPlayerKey;
|
||||
}
|
||||
/* reconnect/เปลี่ยนหน้า: ผู้เล่นคนเดิม (playerKey เดิม) ได้ socket.id ใหม่ แต่ peer เก่ายังค้างใน space.peers
|
||||
จนกว่า disconnect เก่าจะมา (อาจช้า) → ทุกคนเห็น "ตัวละครซ้ำ 2 ตัว" (ghost)
|
||||
แก้: ลบ ghost peer ที่ playerKey ตรงแต่ socket.id ต่าง ก่อนเพิ่มตัวใหม่ + แจ้ง user-left ให้ client ลบทิ้ง */
|
||||
if (joinPlayerKey) {
|
||||
const staleIds = [];
|
||||
space.peers.forEach((p, pid) => {
|
||||
if (pid !== socket.id && p && p.playerKey && p.playerKey === joinPlayerKey) staleIds.push(pid);
|
||||
});
|
||||
staleIds.forEach((pid) => {
|
||||
space.peers.delete(pid);
|
||||
if (space.quizCarryLobbyReady) delete space.quizCarryLobbyReady[pid];
|
||||
if (space.gauntletCrownLobbyReady) delete space.gauntletCrownLobbyReady[pid];
|
||||
io.to(spaceId).emit('user-left', { id: pid });
|
||||
});
|
||||
}
|
||||
if (serverMapIsPostCaseLobbyB(space)) initTroublesomeState(space);
|
||||
ensurePeerLobbyThemes(space);
|
||||
const mdJoin = (space.mapId && maps.get(space.mapId)) || space.mapData;
|
||||
@@ -7512,14 +7590,14 @@ io.on('connection', (socket) => {
|
||||
peer.gauntletEliminated = false;
|
||||
if (!space.gauntletCrownLobbyReady || typeof space.gauntletCrownLobbyReady !== 'object') space.gauntletCrownLobbyReady = {};
|
||||
space.gauntletCrownLobbyReady[socket.id] = false;
|
||||
io.to(spaceId).emit('gauntlet-crown-lobby-sync', { readyMap: { ...space.gauntletCrownLobbyReady } });
|
||||
io.to(spaceId).emit('gauntlet-crown-lobby-sync', crownLobbySyncPayload(space));
|
||||
}
|
||||
startGauntletTicker(spaceId, space);
|
||||
} else if (isBalloonBossMissionShellSpace(space) || isQuizQuestionMissionShellSpace(space) || isStackTowerMissionShellSpace(space) || isJumpSurviveMissionShellSpace(space) || isSpaceShooterMissionShellSpace(space)) {
|
||||
if (!space.gauntletCrownLobbyReady || typeof space.gauntletCrownLobbyReady !== 'object') space.gauntletCrownLobbyReady = {};
|
||||
space.gauntletCrownLobbyReady[socket.id] = false;
|
||||
if (!space.gauntletRun) space.gauntletRun = newBalloonBossShellGauntletRunState();
|
||||
io.to(spaceId).emit('gauntlet-crown-lobby-sync', { readyMap: { ...space.gauntletCrownLobbyReady } });
|
||||
io.to(spaceId).emit('gauntlet-crown-lobby-sync', crownLobbySyncPayload(space));
|
||||
}
|
||||
const peersList = [...space.peers.values()];
|
||||
const mapDataOut = (space.mapId && maps.get(space.mapId)) || space.mapData || mdJoin;
|
||||
@@ -7593,6 +7671,8 @@ io.on('connection', (socket) => {
|
||||
/* Card 3 Ban: ผู้เล่นกลับเข้า LobbyB หลังเกมจบ + มีการ์ด Ban ค้าง → เริ่มโหวตแบนทันที
|
||||
(คน rejoin คนแรกเป็นคนเปิดโหวต; คนที่ rejoin ทีหลังขณะโหวตเปิดอยู่ → ส่งสถานะให้เห็นด้วย) */
|
||||
if (serverMapIsPostCaseLobbyB(space)) {
|
||||
/* ผู้เล่นกลับเข้า LobbyB แล้ว → อัปเดตจำนวนคนที่พร้อมให้ทุกคน (host ใช้เปิดปุ่มเริ่มเมื่อครบ) */
|
||||
emitDetectiveLobbyBPresence(spaceId, space);
|
||||
console.log('[ban-debug] join-space rejoin: postLobbyB=true pendingBan=', !!space.pendingBanVoteCard, 'pickVote=', !!space.pickVote, 'by', socket.id);
|
||||
if (space.pendingBanVoteCard && !space.pickVote) {
|
||||
const banCard = space.pendingBanVoteCard;
|
||||
@@ -7780,6 +7860,57 @@ io.on('connection', (socket) => {
|
||||
socket.to(sid).emit('fill-bot-state', { bots: data.bots });
|
||||
});
|
||||
|
||||
/* MG7 balloon_boss — relay กระสุนของผู้เล่น/บอท ให้ทุกคนในห้องเห็นตรงกัน (ทุกคนยิงได้ ไม่จำกัด host)
|
||||
payload: { sx, sy, vx, vy, ownerId, t0 } — t0 = serverNow ตอนยิง ให้ผู้รับ fast-forward ตำแหน่งให้ตรง */
|
||||
socket.on('balloon-boss-shot', (data) => {
|
||||
const sid = socket.data.spaceId;
|
||||
const space = sid ? spaces.get(sid) : null;
|
||||
if (!space || !data) return;
|
||||
const ok = ['sx', 'sy', 'vx', 'vy'].every((k) => typeof data[k] === 'number' && Number.isFinite(data[k]));
|
||||
if (!ok) return;
|
||||
socket.to(sid).emit('balloon-boss-shot', {
|
||||
sx: data.sx, sy: data.sy, vx: data.vx, vy: data.vy,
|
||||
ownerId: data.ownerId, t0: (typeof data.t0 === 'number' ? data.t0 : Date.now()),
|
||||
});
|
||||
});
|
||||
|
||||
/* MG7 balloon_boss — host เป็นคนตัดสินจบเกม แล้ว relay ให้ทุกคนจบพร้อมกัน */
|
||||
socket.on('balloon-boss-over', (data) => {
|
||||
const sid = socket.data.spaceId;
|
||||
const space = sid ? spaces.get(sid) : null;
|
||||
if (!space || !data) return;
|
||||
const reason = (data.reason === 'victory' || data.reason === 'all_dead') ? data.reason : 'all_dead';
|
||||
const scores = Array.isArray(data.scores)
|
||||
? data.scores.filter((s) => s && s.id != null).map((s) => ({ id: s.id, score: Math.max(0, Number(s.score) || 0) }))
|
||||
: [];
|
||||
socket.to(sid).emit('balloon-boss-over', { reason, scores });
|
||||
});
|
||||
|
||||
/* MG6 space_shooter — host ตัดสินจบเกม + แนบคะแนนสุดท้าย → ทุกคนจบ/อันดับตรงกัน */
|
||||
socket.on('space-shooter-over', (data) => {
|
||||
const sid = socket.data.spaceId;
|
||||
const space = sid ? spaces.get(sid) : null;
|
||||
if (!space || !data) return;
|
||||
const kind = (data.kind === 'all_dead') ? 'all_dead' : 'time_up';
|
||||
const scores = Array.isArray(data.scores)
|
||||
? data.scores.filter((s) => s && s.id != null).map((s) => ({ id: s.id, score: Math.max(0, Number(s.score) || 0), hits: Math.max(0, Number(s.hits) || 0), eliminated: !!s.eliminated }))
|
||||
: [];
|
||||
socket.to(sid).emit('space-shooter-over', { kind, scores });
|
||||
});
|
||||
|
||||
/* MG6 space_shooter — relay กระสุนผู้เล่น/บอท ให้ทุกคนเห็นตรงกัน (host เห็นยังไง client เห็นแบบนั้น) */
|
||||
socket.on('space-shooter-shot', (data) => {
|
||||
const sid = socket.data.spaceId;
|
||||
const space = sid ? spaces.get(sid) : null;
|
||||
if (!space || !data) return;
|
||||
const ok = ['x', 'y', 'vy'].every((k) => typeof data[k] === 'number' && Number.isFinite(data[k]));
|
||||
if (!ok) return;
|
||||
socket.to(sid).emit('space-shooter-shot', {
|
||||
x: data.x, y: data.y, vy: data.vy,
|
||||
ownerId: data.ownerId, t0: (typeof data.t0 === 'number' ? data.t0 : Date.now()),
|
||||
});
|
||||
});
|
||||
|
||||
/** เปลี่ยนชื่อที่แสดงใน lobby / เกม (ซิงก์ทุก client ในห้อง) */
|
||||
socket.on('update-display-name', ({ nickname }, cb) => {
|
||||
const reply = typeof cb === 'function' ? cb : () => {};
|
||||
@@ -7881,6 +8012,18 @@ io.on('connection', (socket) => {
|
||||
if (space.hostId !== socket.id) {
|
||||
return reply({ ok: false, error: 'เฉพาะ host กดเริ่มสืบสวนได้' });
|
||||
}
|
||||
/* ต้องรอผู้เล่นทุกคนในคดีกลับเข้า LobbyB ให้ครบก่อน host ถึงเริ่มมินิเกมรอบถัดไปได้
|
||||
(เดิม: หลังจบเกม client บางคนยังโหลด LobbyB ไม่เสร็จ แต่ host กดเริ่มได้เลย → คนนั้นหลุดรอบ) */
|
||||
const waitInfo = detectiveLobbyBMissingParticipants(space);
|
||||
if (waitInfo.missing > 0) {
|
||||
return reply({
|
||||
ok: false,
|
||||
waitingPlayers: true,
|
||||
present: waitInfo.present,
|
||||
total: waitInfo.total,
|
||||
error: 'รอผู้เล่นกลับเข้าห้องให้ครบก่อน (' + waitInfo.present + '/' + waitInfo.total + ')',
|
||||
});
|
||||
}
|
||||
if (!space.suspectCardMinigames || space.suspectCardMinigames.length < 3) {
|
||||
assignDetectiveSuspectCardMinigames(space);
|
||||
}
|
||||
@@ -8402,7 +8545,7 @@ io.on('connection', (socket) => {
|
||||
if (space.gauntletCrownLobbyReady && typeof space.gauntletCrownLobbyReady === 'object') {
|
||||
delete space.gauntletCrownLobbyReady[socket.id];
|
||||
if (isCrownLobbyShellSpace(space)) {
|
||||
io.to(sid).emit('gauntlet-crown-lobby-sync', { readyMap: { ...space.gauntletCrownLobbyReady } });
|
||||
io.to(sid).emit('gauntlet-crown-lobby-sync', crownLobbySyncPayload(space));
|
||||
}
|
||||
}
|
||||
if (space.qbwSolved) space.qbwSolved.delete(socket.id); // Quiz Battle (ฉากเดิน) — เอาคะแนนคนออกแล้ว sync ใหม่ด้านล่าง
|
||||
@@ -8437,6 +8580,7 @@ io.on('connection', (socket) => {
|
||||
}
|
||||
io.to(sid).emit('user-left', { id: socket.id });
|
||||
if (space.qbwSolved) qbwBroadcastScores(sid, space); // Quiz Battle ฉากเดิน — อัปเดตอันดับหลังมีคนออก
|
||||
emitDetectiveLobbyBPresence(sid, space); /* คนออกจาก LobbyB → อัปเดตจำนวนที่พร้อมให้ host */
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8547,7 +8691,12 @@ io.on('connection', (socket) => {
|
||||
if (!spaceAllowsQuizCarryLobbyRelaxed(space)) return reply({ ok: false, error: 'ห้องนี้ไม่ใช่โหมด lobby หยิบมาวาง / พรีวิว' });
|
||||
if (space.hostId !== socket.id) return reply({ ok: false, error: 'เฉพาะโฮสต์กด START' });
|
||||
const rm = space.quizCarryLobbyReady || {};
|
||||
const humanIds = [...space.peers.keys()];
|
||||
const humanIds = [...space.peers.keys()].filter((id) => !isBannedPeerForRun(space, id));
|
||||
/* ต้องเข้าเกมครบก่อน (กัน host เริ่มก่อน client โหลดเสร็จ) */
|
||||
const expectedActive = space.minigameExpectedActiveCount || 0;
|
||||
if (expectedActive > 0 && humanIds.length < expectedActive) {
|
||||
return reply({ ok: false, waitingPlayers: true, present: humanIds.length, total: expectedActive, error: 'รอผู้เล่นเข้าเกมให้ครบก่อน (' + humanIds.length + '/' + expectedActive + ')' });
|
||||
}
|
||||
const allReady = humanIds.length > 0 && humanIds.every((id) => rm[id]);
|
||||
if (!allReady) return reply({ ok: false, error: 'ยังมีผู้เล่นที่ยังไม่ Ready' });
|
||||
const liveBeginsAt = Date.now() + LIVE_COUNTDOWN_MS;
|
||||
@@ -8590,7 +8739,7 @@ io.on('connection', (socket) => {
|
||||
for (const id of space.peers.keys()) {
|
||||
if (space.gauntletCrownLobbyReady[id] === undefined) space.gauntletCrownLobbyReady[id] = false;
|
||||
}
|
||||
socket.emit('gauntlet-crown-lobby-sync', { readyMap: { ...space.gauntletCrownLobbyReady } });
|
||||
socket.emit('gauntlet-crown-lobby-sync', crownLobbySyncPayload(space));
|
||||
});
|
||||
|
||||
socket.on('gauntlet-crown-lobby-ready', (data) => {
|
||||
@@ -8603,7 +8752,7 @@ io.on('connection', (socket) => {
|
||||
if (space.gauntletCrownLobbyReady[id] === undefined) space.gauntletCrownLobbyReady[id] = false;
|
||||
}
|
||||
space.gauntletCrownLobbyReady[socket.id] = !!(data && data.ready);
|
||||
io.to(sid).emit('gauntlet-crown-lobby-sync', { readyMap: { ...space.gauntletCrownLobbyReady } });
|
||||
io.to(sid).emit('gauntlet-crown-lobby-sync', crownLobbySyncPayload(space));
|
||||
});
|
||||
|
||||
socket.on('gauntlet-crown-lobby-start', (_data, cb) => {
|
||||
@@ -8617,7 +8766,12 @@ io.on('connection', (socket) => {
|
||||
const gr = space.gauntletRun;
|
||||
if (!gr || !gr.crownRunHeld) return reply({ ok: false, error: 'เริ่มแล้ว' });
|
||||
const rm = space.gauntletCrownLobbyReady || {};
|
||||
const humanIds = [...space.peers.keys()];
|
||||
const humanIds = [...space.peers.keys()].filter((id) => !isBannedPeerForRun(space, id));
|
||||
/* ต้องเข้าเกมครบก่อน (กัน host เริ่มก่อน client โหลดเสร็จ → เกมไม่ sync/จบไม่พร้อมกัน) */
|
||||
const expectedActive = space.minigameExpectedActiveCount || 0;
|
||||
if (expectedActive > 0 && humanIds.length < expectedActive) {
|
||||
return reply({ ok: false, waitingPlayers: true, present: humanIds.length, total: expectedActive, error: 'รอผู้เล่นเข้าเกมให้ครบก่อน (' + humanIds.length + '/' + expectedActive + ')' });
|
||||
}
|
||||
const allReady = humanIds.length > 0 && humanIds.every((id) => rm[id]);
|
||||
if (!allReady) return reply({ ok: false, error: 'ยังมีผู้เล่นที่ยังไม่ Ready' });
|
||||
/* เวลาเริ่มเล่นจริงร่วมกัน (server epoch) → ทุก client นับ 3-2-1 ไปหาจุดนี้ → GO พร้อมกัน */
|
||||
@@ -8693,11 +8847,27 @@ io.on('connection', (socket) => {
|
||||
ny = sn.y;
|
||||
}
|
||||
}
|
||||
if (p && md && md.gameType === 'space_shooter' && data && data.spaceShooterScore != null) {
|
||||
const ns = Math.floor(Number(data.spaceShooterScore));
|
||||
if (Number.isFinite(ns) && ns >= 0) {
|
||||
const prev = Math.max(0, p.spaceShooterScore | 0);
|
||||
if (ns <= prev + 35) p.spaceShooterScore = Math.max(prev, ns);
|
||||
let moveForceEmit = false;
|
||||
if (p && md && md.gameType === 'space_shooter' && data) {
|
||||
if (data.spaceShooterScore != null) {
|
||||
const ns = Math.floor(Number(data.spaceShooterScore));
|
||||
if (Number.isFinite(ns) && ns >= 0) {
|
||||
const prev = Math.max(0, p.spaceShooterScore | 0);
|
||||
if (ns <= prev + 35) p.spaceShooterScore = Math.max(prev, ns);
|
||||
}
|
||||
}
|
||||
if (data.spaceShooterHits != null) {
|
||||
const nh = Math.floor(Number(data.spaceShooterHits));
|
||||
if (Number.isFinite(nh) && nh >= 0) {
|
||||
const prevH = Math.max(0, p.spaceShooterHits | 0);
|
||||
if (nh > prevH) { p.spaceShooterHits = nh; moveForceEmit = true; }
|
||||
}
|
||||
}
|
||||
/* ตาย/รอด มาจากเจ้าของยาน → ต้อง relay แม้ตำแหน่งไม่เปลี่ยน (ยานหยุดตอนตาย)
|
||||
ไม่งั้นยานคนตายจะค้างนิ่งบนจออีกฝั่ง (ดูเหมือนขยับไม่ได้) + ตายไม่ตรงกัน */
|
||||
if (data.spaceShooterEliminated && !p.spaceShooterEliminated) {
|
||||
p.spaceShooterEliminated = true;
|
||||
moveForceEmit = true;
|
||||
}
|
||||
}
|
||||
if (p && md && md.gameType === 'balloon_boss' && data) {
|
||||
@@ -8725,7 +8895,17 @@ io.on('connection', (socket) => {
|
||||
if (nb <= prevB && nb >= Math.max(0, prevB - 2)) p.balloonBossBalloons = nb;
|
||||
}
|
||||
}
|
||||
if (data.balloonBossEliminated) p.balloonBossEliminated = true;
|
||||
if (data.balloonBossEliminated && !p.balloonBossEliminated) { p.balloonBossEliminated = true; moveForceEmit = true; }
|
||||
else if (data.balloonBossEliminated) p.balloonBossEliminated = true;
|
||||
}
|
||||
if (p && md && md.gameType === 'quiz_carry' && data && data.quizCarryHeld !== undefined) {
|
||||
/* relay ของที่ผู้เล่น (มนุษย์) ถืออยู่ → client อื่นเห็น "ใครถืออะไร"; force emit แม้ยืนนิ่ง (หยิบ/วางขณะไม่ขยับ) */
|
||||
const prevHeld = (p.quizCarryHeld === undefined ? null : p.quizCarryHeld);
|
||||
const rawHeld = data.quizCarryHeld;
|
||||
const nh = (rawHeld == null) ? null : Math.floor(Number(rawHeld));
|
||||
const newHeld = (nh == null || !Number.isFinite(nh)) ? null : nh;
|
||||
if (newHeld !== prevHeld) moveForceEmit = true;
|
||||
p.quizCarryHeld = newHeld;
|
||||
}
|
||||
if (p) {
|
||||
const prevX = Number(p.x);
|
||||
@@ -8735,9 +8915,13 @@ io.on('connection', (socket) => {
|
||||
p.y = ny;
|
||||
p.direction = data.direction || p.direction;
|
||||
const posUnchanged = Math.abs(nx - prevX) < 1e-5 && Math.abs(ny - prevY) < 1e-5 && p.direction === prevDir;
|
||||
if (!posUnchanged) {
|
||||
if (!posUnchanged || moveForceEmit) {
|
||||
const out = { id: socket.id, x: nx, y: ny, direction: p.direction, characterId: p.characterId };
|
||||
if (md && md.gameType === 'space_shooter') out.spaceShooterScore = Math.max(0, p.spaceShooterScore | 0);
|
||||
if (md && md.gameType === 'space_shooter') {
|
||||
out.spaceShooterScore = Math.max(0, p.spaceShooterScore | 0);
|
||||
out.spaceShooterHits = Math.max(0, p.spaceShooterHits | 0);
|
||||
out.spaceShooterEliminated = !!p.spaceShooterEliminated;
|
||||
}
|
||||
if (md && md.gameType === 'balloon_boss') {
|
||||
const bbDef = balloonBossBalloonsForMap(md);
|
||||
out.balloonBossScore = Math.max(0, p.balloonBossScore | 0);
|
||||
@@ -8747,6 +8931,9 @@ io.on('connection', (socket) => {
|
||||
: bbDef;
|
||||
out.balloonBossEliminated = !!p.balloonBossEliminated;
|
||||
}
|
||||
if (md && md.gameType === 'quiz_carry') {
|
||||
out.quizCarryHeld = (p.quizCarryHeld === undefined ? null : p.quizCarryHeld);
|
||||
}
|
||||
if (nx !== data.x || ny !== data.y) socket.emit('user-move', out);
|
||||
socket.to(sid).emit('user-move', out);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user