@@ -383,8 +383,15 @@
const go = function ( ) {
window . location . href = buildRoomLobbyReturnHref ( ) ;
} ;
/ * ร า ย ง า น s c o r e / s u r v i v e d ข อ ง เ ก ม ท ี ่ c l i e n t น ั บ เ อ ง ( s t a c k / j u m p _ s u r v i v e ) → s e r v e r ใ ช ้ จ ั ด อ ั น ด ั บ เ ห ร ี ย ญ
( เด ิ มส ่ ง { } เปล ่ า → 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' ;
}
/ * แ จ ้ ง h o s t ว ่ า ท ำ ไ ม S T A R T ย ั ง ก ด เ ร ิ ่ ม ไ ม ่ ไ ด ้ ( เ ด ิ ม r e p l y ถ ู ก i g n o r e เ ง ี ย บ → ป ุ ่ ม เ ห ม ื อ น เ ส ี ย )
ใช ้ ร ่ วม 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 ;
/ * * ล ำ ด ั บ ต า M G 3 = ผ ู ้ เ ล ่ น จ ร ิ ง ท ุ ก ค น + บ อ ท เ ร ี ย ง ต า ม i d แ บ บ ค ง ท ี ่ → ท ุ ก เ ค ร ื ่ อ ง ไ ด ้ ล ำ ด ั บ / ท ี ่ น ั ่ ง เ ด ี ย ว ก ั น ( s h a r e d t o w e r )
( เด ิ ม [ 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 ) ;
/ * ท ุ ก ค น ต อ บ ผ ิ ด ห ม ด → h o s t ข อ ใ ห ้ s e r v e r จ บ เ ก ม → s e r v e r ส ่ ง q u i z - e n d e d → โ ช ว ์ ผ ล / ร อ ก ด ร ั บ ก า ร ์ ด ( เ ห ม ื อ น เ ก ม อ ื ่ น )
เด ิ ม : 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 ;
/ * M G 7 h o s t - a u t h o r i t a t i v e b o s s H P : h o s t b r o a d c a s t ด า เ ม จ ร ว ม เ ป ็ น ร ะ ย ะ ( b a l l o o n - b o s s - s y n c )
→ 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 ต า ม ผ ู ้ เ ล ่ น เ ค ร ื ่ อ ง ต ั ว เ อ ง ( ไ ม ่ เ ฉ ล ี ่ ย ท ั ้ ง ก ล ุ ่ ม ) → เ ล น ข อ ง เ ร า ค ง ท ี ่ ใ ต ้ ก ล ้ อ ง , h o s t / ค น อ ื ่ น อ ย ู ่ เ ล น ข อ ง เ ข า เ อ ง
( เด ิ มเฉล ี ่ ย 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 ;
/ * M G 1 q u i z : ส ถ า น ะ ล ็ อ ก ( ต อ บ ผ ิ ด = ผ ี ) + ค ะ แ น น บ อ ท → h o s t เ ป ็ น เ จ ้ า ข อ ง ส ่ ง ใ ห ้ ท ุ ก เ ค ร ื ่ อ ง ต ร ง ก ั น
( เด ิ มไม ่ ส ่ งเลย → 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 ) ;
}
/ * ส ี บ อ ท ( h o s t เ ป ็ น เ จ ้ า ข อ ง ) — แ น บ ไ ป ด ้ ว ย เ พ ื ่ อ ใ ห ้ ท ุ ก c l i e n t แ ส ด ง ส ี ต ร ง ก ั น
( เด ิ มแต ่ ละเคร ื ่ องคำนวณส ี เองจาก 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 ;
/ * เ ล ่ น จ ร ิ ง : แ ก ว ่ ง เ ฉ พ า ะ ต า ค น ท ี ่ ค ุ ม ( เ ร า ห ร ื อ h o s t ค ุ ม บ อ ท ) แ ล ้ ว b r o a d c a s t เ ฟ ส ใ ห ้ ค น อ ื ่ น m i r r o r ;
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 ต่อครั้ง) */
/ * * ร ว ม ด า เ ม จ ท ี ่ ท ำ ก ั บ บ อ ส ( ไ ม ่ เ ท ่ า ก ั บ ค ะ แ น น — ค ะ แ น น + 1 0 ต ่ อ ค ร ั ้ ง , H P บ อ ส - 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 ) ;
/ * ก า ร ช น ก ร ะ ส ุ น บ อ ส : เ จ ้ า ข อ ง ต ั ด ส ิ น ย า น ต ั ว เ อ ง เ ท ่ า น ั ้ น ( + บ อ ท พ ร ี ว ิ ว = h o s t ) — เ ห ม ื อ น M G 6
คนอ ื ่ นรอสถานะล ู กโป ่ ง / ตายจาก 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 ,
} ) ;
}
/ * M G 7 h o s t - a u t h o r i t a t i v e b o s s H P : h o s t b r o a d c a s t ด า เ ม จ ร ว ม + ส ถ า น ะ ผ ู ้ เ ล ่ น ค น อ ื ่ น ~ 7 H z
→ ท ุ กเคร ื ่ องแสดง 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 ;
/ * g h o s t o v e r l a y ( ว า ด ผ ี ) อ ่ า น จ า ก p l a y Q u i z E v e r W r o n g — n o n - h o s t ไ ม ่ ไ ด ้ ร ั น บ ล ็ อ ก ต ั ด ส ิ น บ อ ท ( h o s t - o n l y )
จ ึ งต ้ อง 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 ) ;
} ) ;
/ * M G 7 b a l l o o n _ b o s s — ร ั บ ก ร ะ ส ุ น ข อ ง ผ ู ้ เ ล ่ น / บ อ ท ค น อ ื ่ น แ ล ้ ว เ พ ิ ่ ม เ ข ้ า a r r a y ใ ห ้ เ ห ็ น ต ร ง ก ั น ท ุ ก เ ค ร ื ่ อ ง
@@ -19194,6 +19480,26 @@
endBalloonBossGame ( reason ) ;
} ) ;
/ * M G 7 — h o s t เ ป ็ น เ จ ้ า ข อ ง H P บ อ ส : n o n - h o s t ร ั บ ด า เ ม จ ร ว ม + ส ถ า น ะ ผ ู ้ เ ล ่ น ค น อ ื ่ น ( ล ู ก โ ป ่ ง / ต า ย ) ม า ต ร ง ๆ
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 ;
/ * q u i z ( ย ื น บ น ช ่ อ ง ค ง ท ี ่ + m o v e ม า ห ่ า ง ) → ส แ น ป ท ั น ท ี ก ั น h o s t ค ้ า ง / ห า ย
quiz _carry → ปล ่ อยให ้ lerp ( move ถ ี ่ ท ุ ก 80 ms ) เพ ื ่ อให ้ "ตัวละครคนอื่นมี animation เดิน" ( เด ิ มสแนป = ไม ่ ม ี delta = ย ื นน ิ ่ ง ) * /
/ * q u i z : ป ล ่ อ ย ใ ห ้ l e r p ( 2 4 0 2 5 ) เ ล ื ่ อ น o . x → o . t x เ อ ง → ม ี d e l t a = " ต ั ว ล ะ ค ร ค น อ ื ่ น เ ด ิ น ม ี a n i m a t i o n "
( เด ิ มสแนปท ั นท ี = 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 ( ) ) {
/ * * เ ก ม จ ร ิ ง : b o t ไ ม ่ อ ย ู ่ ใ น ผ ล s e r v e r → H O S T ต ั ด ส ิ น บ อ ท ค น เ ด ี ย ว ( เ ช ็ ค โ ซ น / r e s p a w n / ค ะ แ น น ) แ ล ้ ว b r o a d c a s t ผ ่ า น f i l l - b o t - s t a t e
( เด ิ มท ุ ก 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 ;
/ * M G 4 ( q u i z _ c a r r y ) บ อ ท เ ด ิ น ต ่ อ เ น ื ่ อ ง เ ร ็ ว → n o n - h o s t ต า ม ด ้ ว ย l e r p 0 . 2 จ ะ ต า ม ห ล ั ง h o s t ~ 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 ;
}