update sync game 1-7

This commit is contained in:
2026-06-22 17:09:05 +00:00
parent cedfad8ff7
commit 35e42be4f4
6 changed files with 626 additions and 69 deletions
+128 -10
View File
@@ -51,9 +51,9 @@
"providerUserId": "p_1775109142385_wq7wfy1p32j",
"notes": "auto: player-coins",
"blocked": false,
"coins": 897,
"coins": 945,
"createdAt": "2026-04-02T05:52:21+00:00",
"updatedAt": "2026-06-19T09:42:28+00:00",
"updatedAt": "2026-06-22T16:52:22+00:00",
"daily": {
"anchorMs": 1781197200000,
"claimedDays": [
@@ -65,9 +65,9 @@
false,
false
],
"lockUntilMs": 1781888400000
"lockUntilMs": 0
},
"score": 792,
"score": 840,
"scoreByCase": {
"1": 16,
"10": 320,
@@ -76,7 +76,7 @@
"13": 40,
"14": 20,
"9": 40,
"12": 60,
"12": 108,
"15": 20,
"4": 10,
"5": 20,
@@ -145,14 +145,24 @@
{
"id": "0f14afd3ee469abd185d026c",
"email": "",
"displayName": "Guest",
"displayName": "MONE",
"loginType": "guest",
"providerUserId": "p_1778099867825_vd4pphrr5a",
"notes": "auto: player-coins",
"blocked": false,
"coins": 0,
"coins": 72,
"createdAt": "2026-05-06T20:37:46+00:00",
"updatedAt": "2026-05-06T20:37:46+00:00"
"updatedAt": "2026-06-22T17:04:52+00:00",
"lobbyColorThemeIndex": 5,
"lobbySkinToneIndex": 1,
"score": 72,
"scoreByCase": {
"12": 72
},
"lbName": "MONE",
"achievements": {
"d1_minigame_solver": 9
}
},
{
"id": "7c655e167f8dd9f927cd94b2",
@@ -364,7 +374,7 @@
"blocked": false,
"coins": 10,
"createdAt": "2026-06-19T06:23:24+00:00",
"updatedAt": "2026-06-19T06:24:28+00:00",
"updatedAt": "2026-06-22T13:54:04+00:00",
"daily": {
"anchorMs": 1781802000000,
"claimedDays": [
@@ -376,7 +386,7 @@
false,
false
],
"lockUntilMs": 1781888400000
"lockUntilMs": 0
}
},
{
@@ -615,6 +625,114 @@
"achievements": {
"d1_minigame_solver": 2
}
},
{
"id": "9940a0de4693908fab29c349",
"email": "",
"displayName": "MONE",
"loginType": "guest",
"providerUserId": "p_1782122326943_7p2fnvivscm",
"notes": "auto: player-coins",
"blocked": false,
"coins": 20,
"createdAt": "2026-06-22T09:58:46+00:00",
"updatedAt": "2026-06-22T10:04:04+00:00",
"lobbyColorThemeIndex": 3,
"lobbySkinToneIndex": 1,
"score": 20,
"scoreByCase": {
"10": 20
},
"lbName": "MONE",
"achievements": {
"d1_minigame_solver": 1
}
},
{
"id": "053be00ea728989cff210a11",
"email": "",
"displayName": "ผู้เล่น",
"loginType": "guest",
"providerUserId": "p_1782122328583_4mqrfx5txyo",
"notes": "auto: player-coins",
"blocked": false,
"coins": 26,
"createdAt": "2026-06-22T09:58:48+00:00",
"updatedAt": "2026-06-22T12:17:11+00:00",
"lobbyColorThemeIndex": 1,
"lobbySkinToneIndex": 1,
"score": 26,
"scoreByCase": {
"10": 8,
"8": 18
},
"lbName": "MONE",
"achievements": {
"d1_minigame_solver": 1
}
},
{
"id": "3d78f962ad8309e86cf2b4c4",
"email": "",
"displayName": "ผู้เล่น",
"loginType": "guest",
"providerUserId": "p_1782130171358_cwsd1t9lt2c",
"notes": "auto: player-coins",
"blocked": false,
"coins": 18,
"createdAt": "2026-06-22T12:09:30+00:00",
"updatedAt": "2026-06-22T12:17:11+00:00",
"score": 18,
"scoreByCase": {
"8": 18
},
"lbName": "ผู้เล่น",
"achievements": {
"d1_minigame_solver": 1
}
},
{
"id": "2a1635c13b08c05b62dbb53c",
"email": "",
"displayName": "ผู้เล่น",
"loginType": "guest",
"providerUserId": "p_1778097234255_htbbepc8x3m",
"notes": "auto: player-coins",
"blocked": false,
"coins": 50,
"createdAt": "2026-06-22T13:52:23+00:00",
"updatedAt": "2026-06-22T14:43:06+00:00",
"score": 50,
"scoreByCase": {
"12": 20,
"13": 30
},
"lbName": "ผู้เล่น",
"achievements": {
"d1_minigame_solver": 5
}
},
{
"id": "07361aa244ad0d88dc486d5f",
"email": "",
"displayName": "MONE",
"loginType": "guest",
"providerUserId": "p_1782136893353_1wlj7nbaoli",
"notes": "auto: player-coins",
"blocked": false,
"coins": 16,
"createdAt": "2026-06-22T14:01:32+00:00",
"updatedAt": "2026-06-22T14:28:00+00:00",
"lobbyColorThemeIndex": 8,
"lobbySkinToneIndex": 1,
"score": 16,
"scoreByCase": {
"13": 16
},
"lbName": "MONE",
"achievements": {
"d1_minigame_solver": 2
}
}
]
}
+2 -2
View File
@@ -89,9 +89,9 @@
"balloonBossPlayerBalloonFallbackUrl": "/Game/img/MegaVirus/Artboard%209.png",
"balloonBossBalloonsPerPlayer": 3,
"forcedMinigameKeys": [
"quiz",
"quiz_carry",
"space_shooter",
"balloon_boss"
"jump_survive"
],
"testSpecialCardByMap": {},
"troublesomeForceOffer": false,
+379 -52
View File
@@ -383,8 +383,15 @@
const go = function () {
window.location.href = buildRoomLobbyReturnHref();
};
/* score/survived client (stack/jump_survive) server
(เดมส {} เปล reportedMiniScore/Survived ไมกต coin ranking MG3/MG5 เพยน) */
let finishReport = {};
try {
if (isStack() && stackMini) finishReport = { score: Math.max(0, stackMini.score | 0), survived: true };
else if (isJumpSurvive()) finishReport = { score: 0, survived: !jumpSurviveEliminated };
} catch (eFR) { /* ignore */ }
if (socket && socket.connected) {
socket.emit('detective-minigame-finished', {}, function (res) {
socket.emit('detective-minigame-finished', finishReport, function (res) {
try {
if (res && Array.isArray(res.myPlayerEvidence)) {
var hasAny = res.myPlayerEvidence.some(function (r) { return Array.isArray(r) && r.length; });
@@ -425,6 +432,25 @@
ov.style.display = 'flex';
}
/* host START ( reply ignore )
ใชวม MG2/5/6/7 (crown lobby shell) โชว "รอผู้เล่น x/N" หร "ยังมีคนไม่ Ready" แลวซอนเอง */
function showCrownStartBlockedNoticePlay(r) {
if (!r) return;
let msg = r.error || 'ยังเริ่มไม่ได้';
if (r.waitingPlayers) msg = 'รอผู้เล่นเข้าเกมให้ครบก่อน (' + (r.present || 0) + '/' + (r.total || 0) + ')';
let ov = document.getElementById('crown-start-notice');
if (!ov) {
ov = document.createElement('div');
ov.id = 'crown-start-notice';
ov.style.cssText = 'position:fixed;left:50%;top:14%;transform:translateX(-50%);z-index:100001;padding:12px 22px;border-radius:14px;background:rgba(20,12,16,.92);border:1px solid rgba(255,120,120,.5);color:#ffd9d9;font:700 18px/1.3 Kanit,system-ui,sans-serif;text-align:center;box-shadow:0 6px 24px rgba(0,0,0,.5);pointer-events:none';
document.body.appendChild(ov);
}
ov.textContent = '⏳ ' + msg;
ov.style.display = 'block';
clearTimeout(ov._hideT);
ov._hideT = setTimeout(function () { ov.style.display = 'none'; }, 2600);
}
/* ===== หน้าโชว์การ์ดหลักฐานในฉากมินิเกม (ก่อนกลับ LobbyB) ===== */
var __evidenceConfigCache = null;
function ensureEvidenceConfigLoaded(cb) {
@@ -589,42 +615,119 @@
let stackPreviewTurnIndex = 0;
let stackPreviewBotThinkUntil = 0;
/** MG3 = + id / (shared tower)
(เด [human=วเอง seat1 + bots] อเครอง แตละเครองมองตวเองเปนคนเดยว เลยแยกตกก) */
function rebuildStackPreviewTurnOrder() {
if (!playBotsEnabled() || !mapData || mapData.gameType !== 'stack') {
stackPreviewTurnOrder = null;
return;
}
const bots = [...others.keys()].filter(isPreviewBotId).sort();
stackPreviewTurnOrder = [{ kind: 'human', seat: 1 }];
for (let i = 0; i < STACK_PREVIEW_TURN_COUNT - 1; i++) {
stackPreviewTurnOrder.push({ kind: 'bot', seat: i + 2, botId: bots[i] || null });
if (!detectiveCaseFillBots) {
/* editor preview (คนเดียว): คงพฤติกรรมเดิม [human ตัวเอง seat1 + บอท] */
const bots = [...others.keys()].filter(isPreviewBotId).sort();
stackPreviewTurnOrder = [{ kind: 'human', seat: 1, id: myId != null ? String(myId) : '__me__' }];
for (let i = 0; i < STACK_PREVIEW_TURN_COUNT - 1; i++) {
stackPreviewTurnOrder.push({ kind: 'bot', seat: i + 2, id: bots[i] || null, botId: bots[i] || null });
}
stackPreviewTurnIndex = 0;
stackPreviewBotThinkUntil = 0;
markStackNextDropVisualDirty();
return;
}
stackPreviewTurnIndex = 0;
/* เล่นจริง: non-host ไม่สร้าง order เอง — ใช้ของ host จาก stack-state (กัน order ไม่ตรง/client ตกหล่นช่วงโหลด) */
if (!isMePlayHost()) return;
/* host: ผู้เล่นจริงทุกคน (ตัด banned spectator กัน stall) + บอท เรียง id → ที่นั่งคงที่; preserve คนที่กำลังถึงตา */
const prevActorId = (stackCurrentTurnEntryPlay() && stackCurrentTurnEntryPlay().id) || null;
const parts = [];
if (myId != null && !playBannedSpectator) parts.push({ kind: 'human', id: String(myId) });
others.forEach((o, id) => {
if (isPreviewBotId(id)) { parts.push({ kind: 'bot', id: String(id), botId: String(id) }); return; }
if (o && o.bannedSpectator) return; /* คนถูกแบน = ดูเฉยๆ ไม่เข้าคิววาง */
parts.push({ kind: 'human', id: String(id) });
});
parts.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
const capped = parts.slice(0, STACK_PREVIEW_TURN_COUNT);
stackPreviewTurnOrder = capped.map((p, i) => ({
kind: p.kind, seat: i + 1, id: p.id, botId: p.kind === 'bot' ? p.botId : null,
}));
let ni = -1;
if (prevActorId != null) ni = stackPreviewTurnOrder.findIndex((e) => String(e.id) === String(prevActorId));
if (ni >= 0) stackPreviewTurnIndex = ni;
else if (stackPreviewTurnIndex >= stackPreviewTurnOrder.length) stackPreviewTurnIndex = 0;
stackPreviewBotThinkUntil = 0;
markStackNextDropVisualDirty();
broadcastStackStatePlay();
}
/** host broadcast ลำดับตา + index ปัจจุบัน → non-host ใช้ตรงกัน (single source of truth) */
function broadcastStackStatePlay() {
if (!detectiveCaseFillBots || !isMePlayHost() || !socket || myId == null || !stackTurnOrderReadyPlay()) return;
try {
socket.emit('stack-state', {
idx: stackPreviewTurnIndex,
order: stackPreviewTurnOrder.map((e) => ({ kind: e.kind, seat: e.seat, id: e.id, botId: e.botId || null })),
});
} catch (e) { /* ignore */ }
}
/** order พร้อมใช้ (มีอย่างน้อย 1 ที่นั่ง) */
function stackTurnOrderReadyPlay() {
return !!(stackPreviewTurnOrder && stackPreviewTurnOrder.length >= 1);
}
function stackCurrentTurnEntryPlay() {
if (!stackTurnOrderReadyPlay()) return null;
return stackPreviewTurnOrder[stackPreviewTurnIndex % stackPreviewTurnOrder.length] || null;
}
function isMyStackTurn() {
if (!playBotsEnabled()) return true; /* เล่นเดี่ยว → กดได้ */
if (detectiveCaseFillBots && !stackTurnOrderReadyPlay()) return false; /* เล่นจริง: รอ order จาก host ก่อน */
if (!stackTurnOrderReadyPlay()) return true;
const cur = stackCurrentTurnEntryPlay();
if (!cur || cur.kind !== 'human') return false;
if (!detectiveCaseFillBots) return true; /* editor preview: human = previewer คนเดียว */
return String(cur.id) === String(myId);
}
function isStackCurrentBotTurnPlay() {
const cur = stackCurrentTurnEntryPlay();
return !!(cur && cur.kind === 'bot');
}
/** เจ้าของตาที่วาง broadcast ผลการวาง → ทุกเครื่อง apply เหมือนกัน + เลื่อนตาพร้อมกัน */
function broadcastStackDropPlay(hit, seat, heavy, actorId, isBot) {
if (!detectiveCaseFillBots || !socket || myId == null || !hit) return; /* broadcast เฉพาะเล่นจริง */
try {
socket.emit('stack-drop', {
placedCx: hit.placedCx, placedW: hit.placedW, miss: !!hit.miss, perfect: !!hit.perfect,
pts: hit.pts || 0, progressDelta: hit.progressDelta || 0, landOffsetTiles: hit.landOffsetTiles || 0,
overlap: hit.overlap, supportRatio: hit.supportRatio, delta: hit.delta || 0,
seat: seat || 1, heavy: !!heavy, actorId: actorId != null ? String(actorId) : '', isBot: !!isBot,
});
} catch (e) { /* ignore */ }
}
function advanceStackPreviewTurn() {
if (!playBotsEnabled() || !stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT) return;
stackPreviewTurnIndex = (stackPreviewTurnIndex + 1) % STACK_PREVIEW_TURN_COUNT;
if (!playBotsEnabled() || !stackTurnOrderReadyPlay()) return;
/* เล่นจริง: index คุมโดย host เท่านั้น (non-host รับจาก stack-state) กันลำดับตาเหลื่อมกัน */
if (detectiveCaseFillBots && !isMePlayHost()) return;
stackPreviewTurnIndex = (stackPreviewTurnIndex + 1) % stackPreviewTurnOrder.length;
stackPreviewBotThinkUntil = 0;
markStackNextDropVisualDirty();
broadcastStackStatePlay();
}
function isStackPreviewHumanTurn() {
if (!playBotsEnabled() || !stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT) return true;
const cur = stackPreviewTurnOrder[stackPreviewTurnIndex];
if (!playBotsEnabled() || !stackTurnOrderReadyPlay()) return true;
const cur = stackCurrentTurnEntryPlay();
return !!(cur && cur.kind === 'human');
}
function getStackPreviewCurrentSeat() {
if (!stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT) return 1;
const cur = stackPreviewTurnOrder[stackPreviewTurnIndex];
if (!stackTurnOrderReadyPlay()) return 1;
const cur = stackCurrentTurnEntryPlay();
return cur && cur.seat ? cur.seat : stackPreviewTurnIndex + 1;
}
function getStackDropActorSeat() {
if (playBotsEnabled() && stackPreviewTurnOrder && stackPreviewTurnOrder.length === STACK_PREVIEW_TURN_COUNT) {
if (playBotsEnabled() && stackTurnOrderReadyPlay()) {
return getStackPreviewCurrentSeat();
}
return 1;
@@ -735,6 +838,9 @@
let mapData = null, tileSize = 32, myId = null;
let stackMini = null;
let stackFall = null;
let stackLastSwingEmit = 0; /* throttle broadcast เฟสแกว่ง MG3 (เจ้าของตา → คนอื่น mirror) */
let stackLastStateEmit = 0; /* throttle broadcast ลำดับตา+index MG3 (host → non-host adopt) */
let stackRemoteDropQueue = []; /* คิวดรอปจาก host/คนอื่น (MG3) — ทยอย apply ทีละอันกันทิ้ง */
/** เศษบล็อกที่พลาด/หล่น → ตกแบบ physics หลุดขอบจอ (visual เท่านั้น ไม่กระทบคะแนน/sync) */
let stackMissDebris = [];
let _stackDebrisLastT = 0;
@@ -2882,7 +2988,9 @@
}
function getAvatarImg(characterId, direction, now, isWalking) {
const img = characterId ? getCharacterFrame(characterId, direction, now, isWalking) : null;
/* 'Chatest' = placeholder ที่ไม่มีไฟล์ sprite จริง → อย่าโหลด (กัน 404 รัว) ใช้ default avatar generated แทน */
const validId = characterId && characterId !== LEGACY_PLACEHOLDER_CHARACTER_ID;
const img = validId ? getCharacterFrame(characterId, direction, now, isWalking) : null;
if (img) return img;
return defaultAvatarImg;
}
@@ -2935,6 +3043,8 @@
/** วาดตัวละคร — ทิศลง + useLobbyIdleComposite: composite เดียวกับล็อบบี้ (เฉพาะผู้เล่นจริง ตอนยืน); ตอนเดินใช้ strip เดิน down_0..3 */
function resolvePlayDrawCharSource(characterId, direction, now, isWalking, playTint, useLobbyIdleComposite) {
/* placeholder 'Chatest' ไม่มี sprite/เลเยอร์จริง → ปฏิบัติเป็นว่าง (ใช้ default avatar) กัน 404 รัวทุกเส้นทางวาด */
if (characterId === LEGACY_PLACEHOLDER_CHARACTER_ID) characterId = '';
const dir = direction || 'down';
const walk = !!isWalking;
if (dir === 'down' && useLobbyIdleComposite && !walk) {
@@ -3260,7 +3370,14 @@
});
if (anyAlive) return;
mg1AllDeadEndScheduled = true;
setTimeout(function () { try { tryFinishDetectiveMinigameAndReturnLobby(); } catch (e) { /* ignore */ } }, 1600);
/* host server server quiz-ended / ()
เด: auto tryFinish = บผล/การดเองทนท ไมรอกด */
setTimeout(function () {
try {
if (isMePlayHost() && socket && socket.connected) socket.emit('quiz-end-request');
/* non-host: ไม่ต้องทำอะไร รอ server ส่ง quiz-ended มาโชว์ผล */
} catch (e) { /* ignore */ }
}, 1600);
}
/** ผีตามตัวเมื่อตอบผิด — ใช้เฉพาะแมป mng8a80o (เป็นสัญลักษณ์/บทลงโทษเชิงสนุก) */
const QUIZ_WRONG_GHOST_URL = BASE + '/img/QUESTION/ghost/ghost-front.png';
@@ -3585,6 +3702,10 @@
/* anchor เวลา server + ตัวนับกระสุนบอส — deterministic boss fire ให้ทุกเครื่องยิงทิศ/เวลาเดียวกัน */
let balloonBossLiveAnchorServerMs = 0;
let balloonBossBossShotCount = 0;
/* MG7 host-authoritative boss HP: host broadcast (balloon-boss-sync)
non-host แสดง HP บอสตามค host เป (null = งไมได fallback ผลรวมทองถ) */
let balloonBossHostBossDmg = null;
let balloonBossLastSyncEmit = 0;
/** quiz_battle — โดม MCQ กด E (ข้อจาก battleQuizMcq) */
let quizBattleMcqPool = [];
let quizBattleAnsweredComps = new Set();
@@ -5994,7 +6115,12 @@
sumCy += e.cy;
}
let px = (minCx + maxCx) * 0.5;
/* Y () , host/
(เดมเฉล Y งกล กคนถกเลอนจากเลนจร ทำให host มาซอนเลนของ client) ; ตายแล fallback เฉลยกล */
let py = sumCy / ents.length;
if (!me.gauntletEliminated && Number.isFinite(me.y)) {
py = (me.y + ch * 0.5) * tileSizeRef;
}
const minCamX = halfW;
const maxCamX = Math.max(minCamX, mapWpx - halfW);
const minCamY = halfH;
@@ -7068,13 +7194,24 @@
const missesOut = stackMini && stackMini.teamMissesLeft != null && stackMini.teamMissesLeft <= 0;
const timeOut = elapsed >= timeLim;
if (pct < 100 && !timeOut && !missesOut) return;
stackTowerMissionEndedOnce = true;
stackTowerMissionPhase = 'ended';
applyStackTowerMissionPanelImages();
let outcome = 'time_up';
if (pct >= 100) outcome = 'decrypt_complete';
else if (missesOut) outcome = 'misses_exhausted';
beginStackTowerResultFlashThenGcm(stackTowerMissionBuildPayload({ outcome: outcome }));
/* เล่นจริง: host เป็นคนตัดสินจบ + broadcast → ทุกเครื่องจบพร้อมกัน (เดิมต่างคนต่างจับเวลา local → จบไม่พร้อม) */
if (detectiveCaseFillBots && !isMePlayHost()) return; /* non-host รอ stack-over จาก host */
if (detectiveCaseFillBots && socket && myId != null) {
try { socket.emit('stack-over', { outcome: outcome }); } catch (e) { /* ignore */ }
}
finishStackTowerMissionLocalPlay(outcome);
}
/** จบรอบ MG3 จริง (host เรียกเอง / non-host เรียกตอนรับ stack-over) */
function finishStackTowerMissionLocalPlay(outcome) {
if (stackTowerMissionEndedOnce || !isStackTowerMissionUiMapPlay()) return;
stackTowerMissionEndedOnce = true;
stackTowerMissionPhase = 'ended';
applyStackTowerMissionPanelImages();
beginStackTowerResultFlashThenGcm(stackTowerMissionBuildPayload({ outcome: outcome || 'time_up' }));
}
function isJumpSurviveMissionUiMapPlay() {
@@ -9097,6 +9234,14 @@
if (o.balloonBossBalloons != null) row.bbBalloons = o.balloonBossBalloons;
if (o.balloonBossEliminated != null) row.bbElim = !!o.balloonBossEliminated;
if (o.quizCarryHeld !== undefined) row.qcHeld = o.quizCarryHeld;
/* MG1 quiz: (=) + host
(เดมไมงเลย non-host เหนบอทลอก/respawn/คะแนนคนละแบบ) */
if (isQuiz()) {
if (o.quizCannotTrue != null) row.qct = !!o.quizCannotTrue;
if (o.quizCannotFalse != null) row.qcf = !!o.quizCannotFalse;
const qsc = playLiveQuizScores[id];
if (qsc != null) row.qsc = Math.max(0, qsc | 0);
}
/* (host ) client
(เดมแตละเครองคำนวณสเองจาก theme/sessionStorage บอทไมตรงกนเม sync มมาช) */
if (o.playTint && o.playTint.body) {
@@ -9562,7 +9707,7 @@
}
function playQuizCarryAvatarUrlForMission(characterId) {
if (!characterId) return '';
if (!characterId || characterId === LEGACY_PLACEHOLDER_CHARACTER_ID) return '';
return BASE + '/img/characters/' + encodeURIComponent(String(characterId)) + '_down.png';
}
@@ -11174,6 +11319,7 @@
? Math.max(0.85, Math.min(3.2, playStackBlockWidthTiles))
: autoW;
stackFall = null;
stackRemoteDropQueue = [];
const towerMission = isStackTowerMissionUiMapPlay();
const towerMul = towerMission ? STACK_TOWER_BLOCK_WORLD_SCALE : 1;
const towerLayerStripH = Math.max(14, tileSize * 0.31) * towerMul;
@@ -11534,11 +11680,10 @@
function stackHumanDropGateOkPlay() {
if (!stackMini || !isStack() || stackFall || stackMini.settling) return false;
if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase !== 'live') return false;
if (playBotsEnabled() && mapData && mapData.gameType === 'stack' &&
(!stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT)) {
if (playBotsEnabled() && mapData && mapData.gameType === 'stack' && !stackTurnOrderReadyPlay()) {
rebuildStackPreviewTurnOrder();
}
if (playBotsEnabled() && !isStackPreviewHumanTurn()) return false;
if (playBotsEnabled() && !isMyStackTurn()) return false; /* กดได้เฉพาะตาตัวเอง (shared tower) */
if (Date.now() - stackMini.lastDropAt < 480) return false;
return true;
}
@@ -11549,6 +11694,8 @@
ensureStackNextDropVisual();
const hit = computeStackDropResult(stackMini);
if (!startStackFallAnimation(hit, playBotsEnabled() ? { human: true } : null)) return false;
/* broadcast ผลการวางของเรา → ทุกเครื่องวางบล็อกเดียวกัน + เลื่อนตาพร้อมกัน */
if (playBotsEnabled()) broadcastStackDropPlay(hit, getStackDropActorSeat(), !!stackMini.pendingDropHeavy, myId, false);
if (playBotsEnabled()) {
lastStackPreviewActorId = '__human__';
lastStackPreviewActorUntil = Date.now() + 900;
@@ -11565,6 +11712,8 @@
previewBotAlignStackSwingPhasePlay(stackMini, o);
const hit = computeStackDropResult(stackMini);
if (!startStackFallAnimation(hit, playBotsEnabled() ? { botId } : null)) return;
/* host เป็นเจ้าของบอท → broadcast ผลการวางของบอทให้ทุกเครื่องวางตรงกัน */
broadcastStackDropPlay(hit, getStackDropActorSeat(), !!stackMini.pendingDropHeavy, botId, true);
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));
@@ -11575,14 +11724,15 @@
/** Stack preview: สลับตา — เฉพาะบอทที่ถึงตาถึงจะจำลองปล่อยหนึ่งครั้งแล้วเลื่อนตา */
function stepPreviewStackTurnBased(nowMs) {
if (!playBotsEnabled() || !isStack() || !stackMini || stackFall || stackMini.settling) return;
/* เล่นจริง: host เท่านั้นจำลองบอท (แล้ว broadcast) — non-host รอรับ stack-drop; editor preview (previewFillBots) จำลองเองได้ */
if (detectiveCaseFillBots && !isMePlayHost()) return;
if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase !== 'live') return;
if (mapData && mapData.gameType === 'stack' &&
(!stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT)) {
if (mapData && mapData.gameType === 'stack' && !stackTurnOrderReadyPlay()) {
rebuildStackPreviewTurnOrder();
}
const order = stackPreviewTurnOrder;
if (!order || order.length !== STACK_PREVIEW_TURN_COUNT) return;
const cur = order[stackPreviewTurnIndex];
if (!order || !order.length) return;
const cur = order[stackPreviewTurnIndex % order.length];
if (!cur || cur.kind !== 'bot') return;
if (!cur.botId) {
advanceStackPreviewTurn();
@@ -11605,11 +11755,29 @@
simulatePreviewBotStackDrop(cur.botId, o);
}
/** apply ดรอปถัดไปในคิว (จาก host/คนอื่น) — เริ่มแอนิเมชันร่วงเหมือนเดิม → settle apply ชั้น + เลื่อนตา */
function applyNextRemoteStackDropPlay() {
if (!stackMini || stackFall || stackMini.settling) return false;
const item = stackRemoteDropQueue.shift();
if (!item || !item.hit) return false;
ensureStackNextDropVisual();
stackMini.pendingDropSeat = item.seat || 1;
stackMini.pendingDropHeavy = !!item.heavy;
stackMini.widthTiles = stackBlockWidthTilesForPlay(stackMini.initialWidthTiles, !!item.heavy);
return startStackFallAnimation(item.hit, item.actor || { human: true });
}
function stackTickFrame() {
stackTowerMissionMaybeEndPlay();
const now = performance.now();
const dt = Math.min(0.06, (now - lastStackTickMs) / 1000);
lastStackTickMs = now;
/* เล่นจริง: host ส่งลำดับตา+index เป็นระยะ → non-host adopt (ครอบ late-join: client โหลดช้า host จะรวมเข้า order ให้) */
if (detectiveCaseFillBots && isMePlayHost()) {
if (!stackTurnOrderReadyPlay()) rebuildStackPreviewTurnOrder();
const ts = Date.now();
if (ts - stackLastStateEmit > 300) { stackLastStateEmit = ts; broadcastStackStatePlay(); }
}
if (stackMini && stackMini.settling) {
const s = stackMini.settling;
const u = (now - s.t0) / s.dur;
@@ -11675,8 +11843,25 @@
if (playBotsEnabled() && (wasHuman || fallBotId)) advanceStackPreviewTurn();
}
}
} else if (stackMini && stackRemoteDropQueue.length && applyNextRemoteStackDropPlay()) {
/* ว่าง (ไม่มีบล็อกร่วง/settle) + มีดรอปในคิวจาก host/คนอื่น → apply ทีละอัน (ไม่แกว่งเฟรมนี้) */
} else if (stackMini) {
stackMini.phase += stackMini.phaseSpeed * dt * Math.PI * 2;
/* : ( host ) broadcast mirror;
editor preview / เลนเดยว: แกวงเองตลอดเหมอนเด */
let advancePhase = true;
if (detectiveCaseFillBots) {
advancePhase = isMyStackTurn() || (isMePlayHost() && isStackCurrentBotTurnPlay());
}
if (advancePhase) {
stackMini.phase += stackMini.phaseSpeed * dt * Math.PI * 2;
if (detectiveCaseFillBots && socket && myId != null) {
const tnow = Date.now();
if (tnow - stackLastSwingEmit > 50) {
stackLastSwingEmit = tnow;
try { socket.emit('stack-swing', { phase: stackMini.phase }); } catch (e) { /* ignore */ }
}
}
}
}
stepPreviewStackTurnBased(Date.now());
}
@@ -12666,6 +12851,8 @@
balloonBossBossBullets = [];
balloonBossBossFireAcc = 0;
balloonBossScorePopups = [];
balloonBossHostBossDmg = null;
balloonBossLastSyncEmit = 0;
me.balloonBossScore = 0;
me.balloonBossBossDmg = 0;
me.balloonBossBalloons = balloonBossBalloonsStartPlay();
@@ -16389,8 +16576,12 @@
return s;
}
/** รวมดาเมจที่ทำกับบอส (ไม่เท่ากับคะแนน — คะแนน +10 ต่อครั้ง, HP บอส -2 ต่อครั้ง) */
/** ( +10 , HP -2 )
non-host: ใชาท host เปนเจาของ (single source of truth) าไดบแล HP บอสตรงกนทกเครอง */
function balloonBossTeamBossDamagePlay() {
if (!isMePlayHost() && balloonBossHostBossDmg != null) {
return Math.max(0, balloonBossHostBossDmg | 0);
}
let s = Math.max(0, me.balloonBossBossDmg | 0);
others.forEach((o) => { s += Math.max(0, o.balloonBossBossDmg | 0); });
return s;
@@ -16733,17 +16924,30 @@
continue;
}
let hit = false;
aliveRefs.forEach((entry) => {
if (hit) return;
const ent = entry.ref;
if (!ent || ent.balloonBossCx == null) return;
const cc = balloonBossBalloonClusterWorldPlay(ent, bossC.cx, bossC.cy);
/* : (+ = host) MG6
คนอนรอสถานะลกโป/ตายจาก move ของเจาต + balloon-boss-sync ของ host
(เดมทกเครองคดชนใหกยานดวยตำแหน interpolate างก กโปงเพยน/ตายไมตรงก) */
if (me && !me.balloonBossEliminated && me.balloonBossCx != null) {
const cc = balloonBossBalloonClusterWorldPlay(me, bossC.cx, bossC.cy);
const dx = b.x - cc.cx, dy = b.y - cc.cy;
if (dx * dx + dy * dy <= (36 * 36)) {
hit = true;
applyBalloonBossHitToEntity(ent);
applyBalloonBossHitToEntity(me);
}
});
}
if (!hit && isMePlayHost()) {
aliveRefs.forEach((entry) => {
if (hit || !isPreviewBotId(entry.id)) return;
const ent = entry.ref;
if (!ent || ent.balloonBossCx == null) return;
const cc = balloonBossBalloonClusterWorldPlay(ent, bossC.cx, bossC.cy);
const dx = b.x - cc.cx, dy = b.y - cc.cy;
if (dx * dx + dy * dy <= (36 * 36)) {
hit = true;
applyBalloonBossHitToEntity(ent);
}
});
}
if (hit) balloonBossBossBullets.splice(bi, 1);
}
@@ -16917,6 +17121,21 @@
balloonBossEliminated: !!me.balloonBossEliminated,
});
}
/* MG7 host-authoritative boss HP: host broadcast + ~7Hz
กเครองแสดง HP บอส/กโป/การตาย ตรงก host (single source of truth) */
if (isMePlayHost() && socket && myId != null && tEmit - balloonBossLastSyncEmit > 140) {
balloonBossLastSyncEmit = tEmit;
const syncRefs = buildBalloonBossParticipantRefsPlay();
socket.emit('balloon-boss-sync', {
bossDmg: Math.max(0, balloonBossTeamBossDamagePlay() | 0),
players: syncRefs.map((e) => ({
id: e.id,
d: Math.max(0, (e.ref && (e.ref.balloonBossBossDmg | 0)) || 0),
b: Math.max(0, (e.ref && (e.ref.balloonBossBalloons | 0)) || 0),
e: !!(e.ref && e.ref.balloonBossEliminated),
})),
});
}
}
function drawBalloonBossCombatLayer(ctx, worldToScreen, zDraw, timeMs) {
@@ -18135,6 +18354,8 @@
jumpSurviveVy = 0;
jumpSurviveSyncMeFromHitbox(r.pxc, r.feetY);
['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'KeyA', 'KeyW', 'KeyS', 'KeyD'].forEach((c) => { keys[c] = false; });
/* broadcast การตายทันที — เดิมพอตายแล้วบล็อก emit move (เงื่อนไข !eliminated) → คนอื่นไม่เคยรู้ว่าเราตาย = "ไม่ยอมตาย" */
try { if (socket && myId != null) socket.emit('move', { x: me.x, y: me.y, direction: me.direction, jumpSurviveEliminated: true }); } catch (eJ) { /* ignore */ }
} else {
jumpSurviveVy = r.vy;
jumpSurviveOnGround = r.onGround;
@@ -18966,6 +19187,8 @@
bannedSpectator: !!data.bannedSpectator,
});
if (playBotsEnabled()) rebalancePreviewBots();
/* MG3: มีคนเข้าใหม่ → host รวมเข้าลำดับตา (preserve คนที่กำลังถึงตา) แล้ว broadcast ใหม่ — กัน late-join ตกหล่น */
if (isStack() && detectiveCaseFillBots && isMePlayHost()) rebuildStackPreviewTurnOrder();
if (quizCarryPregameActive && isQuizCarry()) updateQuizCarryPregameHud();
if (isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'howto') updateQuizQuestionMissionHowtoHud();
if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'howto') updateStackTowerMissionHowtoHud();
@@ -19115,6 +19338,7 @@
[...others.keys()].filter(isPreviewBotId).forEach((bid) => {
if (!hostBotIds.has(String(bid))) others.delete(bid);
});
let quizBotScoreChanged = false;
data.bots.forEach((rb) => {
if (!rb || !isPreviewBotId(rb.id)) return;
let o = others.get(rb.id);
@@ -19136,8 +19360,9 @@
if (o.characterId) { try { preloadPlayTintForAvatar(o.characterId, o.playTint); } catch (_e) { /* ignore */ } }
}
if (rb.cid != null && o.characterId == null) o.characterId = rb.cid;
if (Number.isFinite(rb.x)) { o.tx = rb.x; if (snapPos || !Number.isFinite(o.x)) o.x = rb.x; }
if (Number.isFinite(rb.y)) { o.ty = rb.y; if (snapPos || !Number.isFinite(o.y)) o.y = rb.y; }
/* delta ใหญ่ = teleport/respawn (เช่น บอทควิซตอบผิดถูกดีดกลับจุดเกิด) → สแนปเลย ไม่งั้น lerp ไถลทะลุกำแพง */
if (Number.isFinite(rb.x)) { const bigX = Number.isFinite(o.x) && Math.abs(rb.x - o.x) > 3; o.tx = rb.x; if (snapPos || bigX || !Number.isFinite(o.x)) o.x = rb.x; }
if (Number.isFinite(rb.y)) { const bigY = Number.isFinite(o.y) && Math.abs(rb.y - o.y) > 3; o.ty = rb.y; if (snapPos || bigY || !Number.isFinite(o.y)) o.y = rb.y; }
if (rb.direction) o.direction = rb.direction;
/* สีบอทจาก host → ใช้ตามให้ตรงกันทุกเครื่อง (อัปเดตเฉพาะเมื่อเปลี่ยน กัน churn ทุกเฟรม) */
if (rb.tint && rb.tint.body) {
@@ -19164,7 +19389,68 @@
if (rb.bbBalloons != null) o.balloonBossBalloons = rb.bbBalloons;
if (rb.bbElim != null) o.balloonBossEliminated = !!rb.bbElim;
if (rb.qcHeld !== undefined) o.quizCarryHeld = rb.qcHeld;
/* MG1 quiz: รับสถานะล็อก + คะแนนบอทจาก host (single source of truth) */
if (rb.qct != null) o.quizCannotTrue = !!rb.qct;
if (rb.qcf != null) o.quizCannotFalse = !!rb.qcf;
/* ghost overlay () playQuizEverWrong non-host (host-only)
งตอง bridge จากสถานะลอกท host งมา ไม client ไมเหนบอทเปนผตอนตอบผ */
if (o.quizCannotTrue && o.quizCannotFalse) playQuizEverWrong[String(rb.id)] = true;
if (rb.qsc != null) {
const k = String(rb.id);
const nv = Math.max(0, rb.qsc | 0);
if (playLiveQuizScores[k] !== nv) { playLiveQuizScores[k] = nv; quizBotScoreChanged = true; }
}
});
if (quizBotScoreChanged && isQuiz()) renderPlayQuizScoreboard(playLiveQuizScores);
});
/* MG3 stack (shared tower): รับผลการวางจากเจ้าของตา → เข้าคิว แล้วทยอย apply ทีละอัน (กันทิ้งดรอปตอนบล็อกกำลังร่วง = ตึก/% ต่างกัน) */
socket.on('stack-drop', (d) => {
if (!d || !mapData || !isStack() || !stackMini || !detectiveCaseFillBots) return;
const hit = {
miss: !!d.miss, perfect: !!d.perfect, pts: Number(d.pts) || 0, progressDelta: Number(d.progressDelta) || 0,
landOffsetTiles: Number(d.landOffsetTiles) || 0,
placedW: Number(d.placedW), placedCx: Number(d.placedCx),
overlap: Number(d.overlap),
supportRatio: (d.supportRatio == null ? null : Number(d.supportRatio)),
delta: Number(d.delta) || 0,
};
stackRemoteDropQueue.push({
hit: hit, seat: Number(d.seat) || 1, heavy: !!d.heavy,
actor: d.isBot ? { botId: String(d.actorId || '') } : { human: true },
});
});
/* MG3 stack (shared tower): รับเฟสแกว่งจากเจ้าของตา → mirror บล็อกที่กำลังแกว่ง (เห็นเหมือนกันจนกว่าจะถึงตาตัวเอง) */
socket.on('stack-swing', (d) => {
if (!d || !mapData || !isStack() || !stackMini || !detectiveCaseFillBots) return;
if (isMyStackTurn()) return; /* ตาเรา → คุมเฟสเอง */
if (isMePlayHost() && isStackCurrentBotTurnPlay()) return; /* host คุมบอทเอง */
const ph = Number(d.phase);
if (Number.isFinite(ph)) stackMini.phase = ph;
});
/* MG3 stack — non-host รับลำดับตา+index จาก host (single source of truth) → เลิกแยกตึก/ตาเหลื่อม */
socket.on('stack-state', (d) => {
if (!d || !mapData || !isStack() || !detectiveCaseFillBots || isMePlayHost()) return;
if (Array.isArray(d.order)) {
stackPreviewTurnOrder = d.order.map((e) => ({
kind: e.kind, seat: e.seat,
id: e.id != null ? String(e.id) : null,
botId: e.botId != null ? String(e.botId) : null,
}));
}
if (stackPreviewTurnOrder && stackPreviewTurnOrder.length && Number.isFinite(Number(d.idx))) {
const n = stackPreviewTurnOrder.length;
stackPreviewTurnIndex = ((Math.floor(Number(d.idx)) % n) + n) % n;
}
});
/* MG3 stack — host ตัดสินจบรอบ → non-host จบพร้อมกันด้วย outcome เดียวกัน (กัน "จบไม่พร้อมกัน") */
socket.on('stack-over', (d) => {
if (!mapData || !isStack() || !detectiveCaseFillBots) return;
const outcome = (d && typeof d.outcome === 'string') ? d.outcome : 'time_up';
finishStackTowerMissionLocalPlay(outcome);
});
/* MG7 balloon_boss / array
@@ -19194,6 +19480,26 @@
endBalloonBossGame(reason);
});
/* MG7 host HP : non-host + (/)
host เพกเฉย (วเองคำนวณเอง) · วเราเองไม (เปนเจาของ balloon/dmg ของตวเอง broadcast าน move) */
socket.on('balloon-boss-sync', (d) => {
if (!d || !mapData || mapData.gameType !== 'balloon_boss' || balloonBossGameEnded) return;
if (isMePlayHost()) return;
const bd = Number(d.bossDmg);
if (Number.isFinite(bd)) balloonBossHostBossDmg = Math.max(0, bd);
if (Array.isArray(d.players)) {
d.players.forEach((p) => {
if (!p || p.id == null) return;
if (myId != null && String(p.id) === String(myId)) return; /* ตัวเองเป็นเจ้าของ ไม่ทับ */
const o = others.get(String(p.id)) || others.get(p.id);
if (!o) return;
const dd = Number(p.d); if (Number.isFinite(dd)) o.balloonBossBossDmg = Math.max(0, dd);
const bb = Number(p.b); if (Number.isFinite(bb)) o.balloonBossBalloons = Math.max(0, bb);
if (typeof p.e === 'boolean') o.balloonBossEliminated = p.e;
});
}
});
/* MG6 space_shooter — รับกระสุนผู้เล่น/บอทคนอื่น (ตรงดิ่งขึ้น vy คงที่) → ตำแหน่ง = y + vy*elapsed เป๊ะทุกเครื่อง */
socket.on('space-shooter-shot', (d) => {
if (!d || !mapData || mapData.gameType !== 'space_shooter') return;
@@ -19304,13 +19610,16 @@
} else {
o.tx = data.x;
o.ty = data.y;
/* quiz ( + move ) host /
quiz_carry ปลอยให lerp (move 80ms) เพอให "ตัวละครคนอื่นมี animation เดิน" (เดมสแนป = ไม delta = ) */
/* quiz: lerp (24025) o.xo.tx delta = " animation"
(เดมสแนปทนท = o.x==o.tx = ไม delta = นนงไมเด) ; สแนปเฉพาะ delta ใหญ (เทเลพอร/คลกไกล) ลากช/ */
if (mapData && isQuiz() && !isQuizCarry()) {
const px = Number(data.x);
const py = Number(data.y);
if (Number.isFinite(px)) o.x = px;
if (Number.isFinite(py)) o.y = py;
const bigJump = (Number.isFinite(px) && Math.abs(px - o.x) > 2.5) || (Number.isFinite(py) && Math.abs(py - o.y) > 2.5);
if (bigJump) {
if (Number.isFinite(px)) o.x = px;
if (Number.isFinite(py)) o.y = py;
}
}
}
o.direction = data.direction || o.direction;
@@ -19330,6 +19639,10 @@
/* ตาย/รอด มาจากเจ้าของยานเท่านั้น → ทุกเครื่องเห็นตรงกัน */
if (data.spaceShooterEliminated != null) o.spaceShooterEliminated = !!data.spaceShooterEliminated;
}
/* MG5: การตายของผู้เล่นมนุษย์มาจากเจ้าตัว (broadcast ผ่าน move) → ทุกเครื่องเห็นตรงกัน (เดิมไม่ส่ง = เพื่อน "ไม่ยอมตาย") */
if (mapData && mapData.gameType === 'jump_survive' && data.jumpSurviveEliminated != null) {
o.jumpSurviveEliminated = !!data.jumpSurviveEliminated;
}
if (mapData && mapData.gameType === 'balloon_boss') {
if (data.balloonBossScore != null) {
const s = Number(data.balloonBossScore);
@@ -19349,8 +19662,15 @@
});
socket.on('user-left', (data) => {
if (!data || isPreviewBotId(data.id)) return;
const leftWasCurrentStack = !!(isStack() && detectiveCaseFillBots && isMePlayHost()
&& stackCurrentTurnEntryPlay() && String(stackCurrentTurnEntryPlay().id) === String(data.id));
others.delete(data.id);
if (playBotsEnabled()) rebalancePreviewBots();
/* MG3: คนออก → host รื้อลำดับตาใหม่; ถ้าคนที่ออกกำลังถึงตา → เลื่อนตา (กันค้างเพราะไม่มีใครวางแทน) */
if (isStack() && detectiveCaseFillBots && isMePlayHost()) {
rebuildStackPreviewTurnOrder();
if (leftWasCurrentStack && !(stackFall || (stackMini && stackMini.settling))) advanceStackPreviewTurn();
}
if (isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'howto') updateQuizQuestionMissionHowtoHud();
if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'howto') updateStackTowerMissionHowtoHud();
if (isJumpSurviveMissionUiMapPlay() && jumpSurviveMissionPhase === 'howto') updateJumpSurviveMissionHowtoHud();
@@ -19395,8 +19715,9 @@
}
});
}
/** เกมจริง: bot ไม่อยู่ในผล server → เช็คโซนยืนเอง ผิด = เป็นผี (เหมือน path preview) */
if (isQuiz() && Array.isArray(r.results) && playBotsEnabled()) {
/** : bot server HOST (/respawn/) broadcast fill-bot-state
(เดมท client ดสนเอง + respawn วย Math.random แยกก ตำแหน/อก/คะแนนบอทคนละแบบทกจอ) */
if (isQuiz() && Array.isArray(r.results) && playBotsEnabled() && isMePlayHost()) {
const correctTrueBot = !!r.correctTrue;
others.forEach(function (o, id) {
if (!isPreviewBotId(id) || !o) return;
@@ -19951,6 +20272,8 @@
balloonBossPlayerFireCd = 0;
balloonBossLastMoveEmit = 0;
balloonBossBossFireAcc = 0;
balloonBossHostBossDmg = null;
balloonBossLastSyncEmit = 0;
me.balloonBossScore = 0;
me.balloonBossBossDmg = 0;
me.balloonBossBalloons = balloonBossBalloonsStartPlay();
@@ -21533,7 +21856,8 @@
/** แถบ Cyber SCORE + สรุปผล — หน้า idle down + tint ต่อผู้เล่น */
function setCyberHudScoreAvatarImg(avImg, row, skipReuse) {
const cid = row.characterId ? String(row.characterId) : '';
/* 'Chatest' = placeholder ไม่มี sprite จริง → ปฏิบัติเป็นว่าง ใช้ default (กัน 404 + retry วน ×16) */
const cid = (row.characterId && row.characterId !== LEGACY_PLACEHOLDER_CHARACTER_ID) ? String(row.characterId) : '';
if (!cid) {
avImg.src = defaultAvatarImg.src;
return;
@@ -23714,8 +24038,11 @@
others.forEach((o, id) => {
/* host เดินบอทเองตรงๆ (o.x/o.y) → ข้าม lerp; แต่ client อื่นต้อง lerp ตามตำแหน่งที่ host ส่งมา (o.tx/o.ty) */
if (playBotsEnabled() && isPreviewBotId(id) && isMePlayHost()) return;
if (o.tx != null) o.x += (o.tx - o.x) * LERP;
if (o.ty != null) o.y += (o.ty - o.y) * LERP;
/* MG4 (quiz_carry) non-host lerp 0.2 host ~1-2 ("")
เร lerp เฉพาะบอท quiz_carry ใหตามตำแหน host นข (ไมแตะเกมอ/เลนจร) */
const lerpF = (isQuizCarry() && isPreviewBotId(id)) ? 0.5 : LERP;
if (o.tx != null) o.x += (o.tx - o.x) * lerpF;
if (o.ty != null) o.y += (o.ty - o.y) * lerpF;
});
tickQuizCarryEmbedCountdown();
tickQuizCarryRoundTimers();
@@ -24033,7 +24360,7 @@
if (myId == null) return;
const humansReady = gauntletCrownAllReadyPlay();
if (humansReady) {
socket.emit('gauntlet-crown-lobby-start', {}, (r) => { if (!r || !r.ok) { /* ignore */ } });
socket.emit('gauntlet-crown-lobby-start', {}, (r) => { if (r && r.ok === false) showCrownStartBlockedNoticePlay(r); });
return;
}
const sidH = String(myId);
@@ -24065,7 +24392,7 @@
if (myId == null) return;
const humansReadySs = gauntletCrownAllReadyPlay();
if (humansReadySs) {
socket.emit('gauntlet-crown-lobby-start', {}, (r) => { if (!r || !r.ok) { /* ignore */ } });
socket.emit('gauntlet-crown-lobby-start', {}, (r) => { if (r && r.ok === false) showCrownStartBlockedNoticePlay(r); });
return;
}
const sidHostSs = String(myId);
@@ -24096,7 +24423,7 @@
if (myId == null) return;
const humansReady = gauntletCrownAllReadyPlay();
if (humansReady) {
socket.emit('gauntlet-crown-lobby-start', {}, (r) => { if (!r || !r.ok) { /* ignore */ } });
socket.emit('gauntlet-crown-lobby-start', {}, (r) => { if (r && r.ok === false) showCrownStartBlockedNoticePlay(r); });
return;
}
const sidH = String(myId);
@@ -24128,7 +24455,7 @@
if (myId == null) return;
const humansReadySt = gauntletCrownAllReadyPlay();
if (humansReadySt) {
socket.emit('gauntlet-crown-lobby-start', {}, (r) => { if (!r || !r.ok) { /* ignore */ } });
socket.emit('gauntlet-crown-lobby-start', {}, (r) => { if (r && r.ok === false) showCrownStartBlockedNoticePlay(r); });
return;
}
const sidHostSt = String(myId);
@@ -24160,7 +24487,7 @@
return;
}
socket.emit('gauntlet-crown-lobby-start', {}, (r) => {
if (!r || !r.ok) { /* ignore */ }
if (r && r.ok === false) showCrownStartBlockedNoticePlay(r);
});
return;
}
+115 -4
View File
@@ -1800,6 +1800,10 @@ function gameAwardSecret() {
/* เหรียญตามอันดับผู้รอดชีวิต: อันดับ 1→10, 2→8, 3→6, 4→4, 5→2, 6+→0 */
const MINIGAME_RANK_COINS = [10, 8, 6, 4, 2, 0];
/* client (stack/quiz_carry) sanity cap overflow//spoof
าจรงตอรอบอยหลกรอย- เพดานนกวางพอไมกระทบเลนจร (ไมใช anti-cheat สมบรณ คอมเมนต handler) */
const REPORTED_MINI_SCORE_MAX = 50000;
/* คะแนน + สถานะรอด/ตาย ของผู้เล่นต่อ gameType ตอนจบมินิเกม */
function minigamePlayerScoreSurvived(space, p, gameType) {
/* Card 3 Ban — ผู้ถูกแบนเข้ามาดูเฉยๆ ไม่ได้เล่น → ไม่นับเป็นผู้รอด/ไม่ได้คะแนน */
@@ -1810,8 +1814,11 @@ function minigamePlayerScoreSurvived(space, p, gameType) {
return { score: st ? Math.max(0, Number(st.score) | 0) : 0, survived: st ? !st.eliminated : true };
}
case 'gauntlet':
case 'jump_survive':
return { score: Math.max(0, p.gauntletScore | 0), survived: !p.gauntletEliminated };
case 'jump_survive':
/* jump_survive gauntlet ticker gauntletScore/Eliminated server 0/false
ใชาท client รายงานตอนจบแทน (reportedMiniSurvived = ไมตาย) ; เดมเข case gauntlet กคน survived เสมอ = coin */
return { score: Math.max(0, p.reportedMiniScore | 0), survived: p.reportedMiniSurvived !== false };
case 'space_shooter':
return { score: Math.max(0, p.spaceShooterScore | 0), survived: true };
case 'balloon_boss':
@@ -4143,8 +4150,12 @@ function endQuizGame(sid, space, message) {
clearSpaceQuizTimers(space);
if (space.quizSession) space.quizSession.active = false;
if (space.detectiveMinigameActive) {
returnDetectiveSpaceToLobbyB(sid, space, message, { allPlayers: true });
space.quizSession = null;
/* MG1: auto-return/auto-grant quiz-ended client ( )
เด returnDetectiveSpaceToLobbyB = server แจกการ+เดงกลบเองทนท = "auto รับผล/การ์ด"
การแจกการ/กล LobbyB จะเกดเม client กดป detective-minigame-finished (เสนทางเดยวก MG2-7) */
io.to(sid).emit('quiz-ended', { message: message || 'จบเกม', returnToLobbyB: true });
/* quizSession (active=false ) client finish returnDetectiveSpaceToLobbyB quiz
( null minigamePlayerScoreSurvived('quiz') = 0 หมด เหรยญเพยน) ; จะถกแทนเมอเรมมเกมรอบใหม */
return;
}
io.to(sid).emit('quiz-ended', { message: message || 'จบเกม' });
@@ -7886,6 +7897,87 @@ io.on('connection', (socket) => {
socket.to(sid).emit('balloon-boss-over', { reason, scores });
});
/* MG7 balloon_boss host HP : relay + (/)
กเครองแสดง HP บอสตรงก host เป (เดมแตละเครองรวม balloonBossBossDmg จาก move เองทยอยมา = bar เพยน) */
socket.on('balloon-boss-sync', (data) => {
const sid = socket.data.spaceId;
const space = sid ? spaces.get(sid) : null;
if (!space || space.hostId !== socket.id || !data) return; /* host เท่านั้น */
const bossDmg = Math.max(0, Number(data.bossDmg) || 0);
const players = Array.isArray(data.players)
? data.players.filter((p) => p && p.id != null).slice(0, 12).map((p) => ({
id: p.id,
d: Math.max(0, Number(p.d) || 0),
b: Math.max(0, Number(p.b) || 0),
e: !!p.e,
}))
: [];
socket.to(sid).emit('balloon-boss-sync', { bossDmg, players });
});
/* MG3 stack (shared tower) — relay ผลการวาง (เจ้าของตา/บอทของ host) ให้ทุกคนวางบล็อกเดียวกัน + เลื่อนตาพร้อมกัน */
socket.on('stack-drop', (data) => {
const sid = socket.data.spaceId;
const space = sid ? spaces.get(sid) : null;
if (!space || !data) return;
const num = (v) => (typeof v === 'number' && Number.isFinite(v)) ? v : 0;
socket.to(sid).emit('stack-drop', {
placedCx: num(data.placedCx), placedW: num(data.placedW),
miss: !!data.miss, perfect: !!data.perfect,
pts: num(data.pts), progressDelta: num(data.progressDelta), landOffsetTiles: num(data.landOffsetTiles),
overlap: num(data.overlap),
supportRatio: (data.supportRatio == null ? null : num(data.supportRatio)),
delta: num(data.delta),
seat: Math.max(1, Math.min(8, Math.floor(num(data.seat)) || 1)), heavy: !!data.heavy,
actorId: data.actorId != null ? String(data.actorId).slice(0, 64) : '', isBot: !!data.isBot,
});
});
/* MG1 quiz — host ขอจบเกมก่อนครบข้อ (ทุกคนตอบผิดหมด=ผีหมด) → endQuizGame โชว์ผล/รอกดรับการ์ด (ไม่ auto) */
socket.on('quiz-end-request', () => {
const sid = socket.data.spaceId;
const space = sid ? spaces.get(sid) : null;
if (!space || space.hostId !== socket.id) return; /* host เท่านั้น */
if (!space.detectiveMinigameActive || !space.quizSession || !space.quizSession.active) return;
const md = (space.mapId && maps.get(space.mapId)) || space.mapData;
if (!md || md.gameType !== 'quiz') return;
endQuizGame(sid, space, 'จบเกม — ทุกคนตอบผิดหมด');
});
/* MG3 stack — relay เฟสแกว่ง (เจ้าของตา → คนอื่น mirror บล็อกที่กำลังแกว่ง) */
socket.on('stack-swing', (data) => {
const sid = socket.data.spaceId;
const space = sid ? spaces.get(sid) : null;
if (!space || !data) return;
const ph = Number(data.phase);
if (!Number.isFinite(ph)) return;
socket.to(sid).emit('stack-swing', { phase: ph });
});
/* MG3 stack — host เป็นเจ้าของลำดับตา+index relay ให้ non-host (กัน host/client มองลำดับตาไม่ตรง) */
socket.on('stack-state', (data) => {
const sid = socket.data.spaceId;
const space = sid ? spaces.get(sid) : null;
if (!space || space.hostId !== socket.id || !data) return; /* host เท่านั้น */
const order = Array.isArray(data.order)
? data.order.filter((e) => e && (e.kind === 'human' || e.kind === 'bot')).slice(0, 8).map((e) => ({
kind: e.kind === 'bot' ? 'bot' : 'human',
seat: Math.max(1, Math.min(8, Math.floor(Number(e.seat)) || 1)),
id: e.id != null ? String(e.id).slice(0, 64) : null,
botId: e.botId != null ? String(e.botId).slice(0, 64) : null,
}))
: [];
const idx = Math.max(0, Math.floor(Number(data.idx)) || 0);
socket.to(sid).emit('stack-state', { order, idx });
});
/* MG3 stack — host ตัดสินจบรอบ relay ให้ทุกคนจบพร้อมกัน */
socket.on('stack-over', (data) => {
const sid = socket.data.spaceId;
const space = sid ? spaces.get(sid) : null;
if (!space) return;
const outcome = (data && typeof data.outcome === 'string') ? data.outcome.slice(0, 32) : 'time_up';
socket.to(sid).emit('stack-over', { outcome });
});
/* MG6 space_shooter — host ตัดสินจบเกม + แนบคะแนนสุดท้าย → ทุกคนจบ/อันดับตรงกัน */
socket.on('space-shooter-over', (data) => {
const sid = socket.data.spaceId;
@@ -8062,7 +8154,16 @@ io.on('connection', (socket) => {
/* client รายงานคะแนน/รอด ของตัวเอง (ใช้กับเกมที่ server ไม่ได้นับคะแนน เช่น stack/quiz_carry) */
const finPeer = space.peers.get(socket.id);
if (finPeer && data && typeof data === 'object') {
if (Number.isFinite(Number(data.score))) finPeer.reportedMiniScore = Math.max(0, Math.floor(Number(data.score)));
/* client (stack/quiz_carry) clamp sanity cap overflow//spoof
หมายเหต: งเป client-trusted ภายในเพดาน (anti-cheat สมบรณองจำลองเกมฝ server เกนสโคปตอนน) */
if (Number.isFinite(Number(data.score))) {
let rs = Math.max(0, Math.floor(Number(data.score)));
if (rs > REPORTED_MINI_SCORE_MAX) {
console.warn('[mini-score] clamp reportedMiniScore', socket.id, rs, '->', REPORTED_MINI_SCORE_MAX);
rs = REPORTED_MINI_SCORE_MAX;
}
finPeer.reportedMiniScore = rs;
}
if (data.survived === false) finPeer.reportedMiniSurvived = false;
}
if (!space.detectiveMinigameActive) {
@@ -8870,6 +8971,13 @@ io.on('connection', (socket) => {
moveForceEmit = true;
}
}
if (p && md && md.gameType === 'jump_survive' && data) {
/* MG5: การตายมาจากเจ้าตัว → relay แม้ตำแหน่งไม่เปลี่ยน (ตอนตายไม่ขยับ) ไม่งั้นคนอื่นไม่รู้ = "ไม่ยอมตาย" */
if (data.jumpSurviveEliminated && !p.jumpSurviveEliminated) {
p.jumpSurviveEliminated = true;
moveForceEmit = true;
}
}
if (p && md && md.gameType === 'balloon_boss' && data) {
const bbDefaultBalloons = balloonBossBalloonsForMap(md);
if (data.balloonBossScore != null) {
@@ -8934,6 +9042,9 @@ io.on('connection', (socket) => {
if (md && md.gameType === 'quiz_carry') {
out.quizCarryHeld = (p.quizCarryHeld === undefined ? null : p.quizCarryHeld);
}
if (md && md.gameType === 'jump_survive') {
out.jumpSurviveEliminated = !!p.jumpSurviveEliminated;
}
if (nx !== data.x || ny !== data.y) socket.emit('user-move', out);
socket.to(sid).emit('user-move', out);
}
+1 -1
View File
@@ -231,6 +231,6 @@
<script src="../Game/js/customize-popup.js?v=38" data-customize-triggers="#btn-cloth" data-customize-asset-base="/Game/img/03-5-Customize"></script>
<script src="../Game/js/achievements.js?v=0.002" data-asset-base="/Game/img/03-6-Profile"></script>
<script src="../Game/js/profile-popup.js?v=8" data-profile-trigger="#btn-myprofile" data-profile-asset-base="/Game/img/03-6-Profile"></script>
<script src="lobby.js?v=0.0193"></script>
<script src="lobby.js?v=0.0194"></script>
</body>
</html>
+1
View File
@@ -84,6 +84,7 @@
function normalizeCharacterAssetId(id) {
var s = String(id == null ? '' : id).trim();
if (!s) return '';
if (s === 'Chatest') return ''; // legacy placeholder — ไม่มี sprite จริง ถือเป็นว่าง (fallback ไปตัวจริงจาก API) กัน 404 รัว
if (s.length > 96) s = s.slice(0, 96);
if (!/^[a-zA-Z0-9._-]+$/.test(s)) return '';
return s;