diff --git a/www/html/Admin/index.html b/www/html/Admin/index.html index e21fce3..fa3861e 100644 --- a/www/html/Admin/index.html +++ b/www/html/Admin/index.html @@ -53,6 +53,8 @@ อัป Git (justice) 📊 Game Log 🐞 Test Case + + 👥 … @@ -1288,5 +1290,58 @@ + diff --git a/www/html/Admin/private/store.json b/www/html/Admin/private/store.json index d55ba6a..8a9206f 100644 --- a/www/html/Admin/private/store.json +++ b/www/html/Admin/private/store.json @@ -51,9 +51,9 @@ "providerUserId": "p_1775109142385_wq7wfy1p32j", "notes": "auto: player-coins", "blocked": false, - "coins": 2140, + "coins": 2256, "createdAt": "2026-04-02T05:52:21+00:00", - "updatedAt": "2026-06-26T10:25:41+00:00", + "updatedAt": "2026-06-26T18:19:28+00:00", "daily": { "anchorMs": 1781197200000, "claimedDays": [ @@ -67,13 +67,13 @@ ], "lockUntilMs": 0 }, - "score": 86, + "score": 192, "scoreByCase": { - "1": 16, - "10": 562, + "1": 26, + "10": 616, "8": 296, "11": 20, - "13": 234, + "13": 268, "14": 30, "9": 40, "12": 408, @@ -83,7 +83,7 @@ "7": 6, "3": 142, "6": 44, - "2": 90 + "2": 98 }, "lbName": "Q", "achievements": { @@ -102,33 +102,36 @@ "providerUserId": "p_1775128666204_xzgg2n6x8h", "notes": "auto: player-coins", "blocked": false, - "coins": 86, + "coins": 165, "createdAt": "2026-04-02T11:17:46+00:00", - "updatedAt": "2026-06-24T12:38:24+00:00", - "lobbyColorThemeIndex": 4, - "lobbySkinToneIndex": 2, + "updatedAt": "2026-06-26T12:12:46+00:00", + "lobbyColorThemeIndex": 1, + "lobbySkinToneIndex": 3, "daily": { "anchorMs": 1782234000000, "claimedDays": [ true, - false, + true, false, false, false, false, false ], - "lockUntilMs": 1782320400000 + "lockUntilMs": 1782493200000 }, - "score": 76, + "score": 140, "scoreByCase": { - "6": 18, + "6": 30, "13": 50, - "5": 8 + "5": 8, + "12": 6, + "10": 36, + "3": 10 }, "lbName": "OHM", "achievements": { - "d1_minigame_solver": 10, + "d1_minigame_solver": 19, "a1_first_deduction": 1, "a5_truth_hunter": 1 } @@ -226,9 +229,9 @@ "providerUserId": "p_1780400804441_lsc3lqqvs0i", "notes": "auto: player-coins", "blocked": false, - "coins": 135, + "coins": 193, "createdAt": "2026-06-02T11:46:46+00:00", - "updatedAt": "2026-06-24T12:38:36+00:00", + "updatedAt": "2026-06-26T12:12:46+00:00", "daily": { "anchorMs": 1781802000000, "claimedDays": [ @@ -240,24 +243,26 @@ false, false ], - "lockUntilMs": 1782320400000 + "lockUntilMs": 0 }, - "score": 90, + "score": 148, "scoreByCase": { "1": 4, "7": 2, - "10": 26, - "6": 20, + "10": 52, + "6": 24, "13": 32, - "5": 6 + "5": 6, + "12": 10, + "3": 18 }, "lbName": "Pear", "achievements": { - "d1_minigame_solver": 17, + "d1_minigame_solver": 20, "a1_first_deduction": 1, "a5_truth_hunter": 2 }, - "lobbyColorThemeIndex": 6, + "lobbyColorThemeIndex": 8, "lobbySkinToneIndex": 3 }, { @@ -295,17 +300,22 @@ "providerUserId": "p_1781849622720_33zppelly7r", "notes": "auto: player-coins", "blocked": false, - "coins": 12, + "coins": 40, "createdAt": "2026-06-19T06:13:44+00:00", - "updatedAt": "2026-06-19T07:10:26+00:00", - "score": 12, + "updatedAt": "2026-06-26T12:12:46+00:00", + "score": 40, "scoreByCase": { - "1": 12 + "1": 12, + "10": 6, + "6": 10, + "3": 12 }, "lbName": "Nutty", "achievements": { - "d1_minigame_solver": 3 - } + "d1_minigame_solver": 10 + }, + "lobbyColorThemeIndex": 7, + "lobbySkinToneIndex": 2 }, { "id": "71f99e98ffa2e824b225af7c", @@ -315,9 +325,9 @@ "providerUserId": "p_1781849642042_x2vt68wscm", "notes": "auto: player-coins", "blocked": false, - "coins": 136, + "coins": 148, "createdAt": "2026-06-19T06:14:02+00:00", - "updatedAt": "2026-06-19T07:49:00+00:00", + "updatedAt": "2026-06-26T12:12:46+00:00", "daily": { "anchorMs": 1781802000000, "claimedDays": [ @@ -329,18 +339,20 @@ false, false ], - "lockUntilMs": 1781888400000 + "lockUntilMs": 0 }, - "score": 126, + "score": 138, "scoreByCase": { "1": 66, "7": 10, "5": 10, - "2": 40 + "2": 40, + "6": 10, + "3": 2 }, "lbName": "tomato", "achievements": { - "d1_minigame_solver": 14 + "d1_minigame_solver": 18 } }, { @@ -651,20 +663,21 @@ "providerUserId": "p_1782016990238_0u37bv7pxlxs", "notes": "auto: player-coins", "blocked": false, - "coins": 368, + "coins": 510, "createdAt": "2026-06-21T04:43:11+00:00", - "updatedAt": "2026-06-26T09:44:40+00:00", - "lobbyColorThemeIndex": 8, + "updatedAt": "2026-06-26T18:36:24+00:00", + "lobbyColorThemeIndex": 5, "lobbySkinToneIndex": 1, - "score": 338, + "score": 470, "scoreByCase": { - "13": 82, - "10": 172, + "13": 130, + "10": 238, "6": 8, "8": 20, - "2": 36, + "2": 46, "3": 10, - "7": 10 + "7": 10, + "1": 8 }, "lbName": "MONE", "achievements": { @@ -824,22 +837,37 @@ "providerUserId": "p_1782300287206_l5omneu1yeo", "notes": "auto: player-coins", "blocked": false, - "coins": 56, + "coins": 104, "createdAt": "2026-06-24T11:24:48+00:00", - "updatedAt": "2026-06-24T12:38:36+00:00", - "lobbyColorThemeIndex": 7, + "updatedAt": "2026-06-26T12:12:46+00:00", + "lobbyColorThemeIndex": 3, "lobbySkinToneIndex": 2, - "score": 56, + "score": 94, "scoreByCase": { - "6": 14, + "6": 24, "13": 26, - "5": 16 + "5": 16, + "12": 8, + "3": 20 }, "lbName": "Nam", "achievements": { - "d1_minigame_solver": 11, + "d1_minigame_solver": 17, "a1_first_deduction": 1, "a5_truth_hunter": 1 + }, + "daily": { + "anchorMs": 1782406800000, + "claimedDays": [ + true, + false, + false, + false, + false, + false, + false + ], + "lockUntilMs": 1782493200000 } }, { @@ -909,6 +937,54 @@ "achievements": { "d1_minigame_solver": 9 } + }, + { + "id": "e703a22c23c4152df9c12bdb", + "email": "", + "displayName": "Guest", + "loginType": "guest", + "providerUserId": "p_1782472333150_uuc91mrf9r8", + "notes": "auto: player-coins", + "blocked": false, + "coins": 0, + "createdAt": "2026-06-26T11:12:13+00:00", + "updatedAt": "2026-06-26T11:12:13+00:00" + }, + { + "id": "bc0df0c0af13306a95e5359b", + "email": "", + "displayName": "MOOKIZ", + "loginType": "guest", + "providerUserId": "p_1782472374020_hlha51h8q2m", + "notes": "auto: player-coins", + "blocked": false, + "coins": 56, + "createdAt": "2026-06-26T11:12:52+00:00", + "updatedAt": "2026-06-26T12:12:46+00:00", + "lobbyColorThemeIndex": 6, + "lobbySkinToneIndex": 3, + "score": 56, + "scoreByCase": { + "10": 32, + "6": 8, + "3": 16 + }, + "lbName": "MOOKIZ", + "achievements": { + "d1_minigame_solver": 7 + } + }, + { + "id": "fcb97fd6e205c5ef88b7376c", + "email": "", + "displayName": "Guest", + "loginType": "guest", + "providerUserId": "p_1782472855815_uzyehie0hmf", + "notes": "auto: player-coins", + "blocked": false, + "coins": 0, + "createdAt": "2026-06-26T11:20:55+00:00", + "updatedAt": "2026-06-26T11:20:55+00:00" } ] } \ No newline at end of file diff --git a/www/html/Game/data/game-timing.json b/www/html/Game/data/game-timing.json index 9f6aceb..06dea22 100644 --- a/www/html/Game/data/game-timing.json +++ b/www/html/Game/data/game-timing.json @@ -2,12 +2,7 @@ "gauntletTickMs": 220, "gauntletJumpTicks": 16, "gauntletTimeLimitSec": 0, - "gauntletLaneImageUrls": [ - "/Game/img/gauntlet-assets/obstacles-2.png", - "/Game/img/gauntlet-assets/obstacles-3.png", - "/Game/img/gauntlet-assets/obstacles-4.png", - "/Game/img/gauntlet-assets/obstacles-5.png" - ], + "gauntletLaneImageUrls": [], "gauntletLaserTopUrl": "", "gauntletLaserBottomUrl": "", "gauntletLaserLineUrl": "", @@ -74,7 +69,7 @@ "/Game/img/ViolentCrime/Meteo-3.png" ], "spaceShooterAsteroidExplodeFrameMs": 60, - "spaceShooterAsteroidIntervalMs": 200, + "spaceShooterAsteroidIntervalMs": 300, "spaceShooterShipDamageOverlayUrls": [ "/Game/img/ViolentCrime/Rocket-broke-1.png", "/Game/img/ViolentCrime/Rocket-broke-2.png", @@ -95,13 +90,15 @@ "balloonBossPlayerBalloonFallbackUrl": "/Game/img/MegaVirus/Artboard%209.png", "balloonBossBalloonsPerPlayer": 3, "forcedMinigameKeys": [ - "stack", - "jump_survive", - "space_shooter" + "quiz", + "gauntlet", + "balloon_boss" ], "testSpecialCardByMap": {}, "troublesomeForceOffer": false, "specialQuizIconExpireSec": 20, "pickVoteSec": 25, - "trialVoteSec": 30 + "trialVoteSec": 30, + "lobbyDebugHud": true, + "lobbyDebugHudNames": [] } \ No newline at end of file diff --git a/www/html/Game/public/img/QUESTION/btn-ready-active.png b/www/html/Game/public/img/QUESTION/btn-ready-active.png new file mode 100644 index 0000000..a54a8cb Binary files /dev/null and b/www/html/Game/public/img/QUESTION/btn-ready-active.png differ diff --git a/www/html/Game/public/img/QUESTION/popup-Howtominigame1.png b/www/html/Game/public/img/QUESTION/popup-Howtominigame1.png new file mode 100644 index 0000000..02fc091 Binary files /dev/null and b/www/html/Game/public/img/QUESTION/popup-Howtominigame1.png differ diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js index 7b2801d..e04e823 100644 --- a/www/html/Game/public/js/play.js +++ b/www/html/Game/public/js/play.js @@ -3483,6 +3483,10 @@ return pool[idx % pool.length] || humanId || roster[0]; } const MOVE_SPEED = 0.15; + /* time-based movement: เดิม step=MOVE_SPEED ต่อเฟรม → จอ 120fps เดินเร็ว 2 เท่า (คน/บอทวิ่งเร็วช้าไม่เท่ากัน) + playDtMul = dt*60 (clamp ≤3 กัน tunneling) คูณเข้ากับ step ทุกที่ → ความเร็วเท่ากันทุก FPS */ + let playDtMul = 1; + let playTickLastTs = 0; /** Anti-stuck (MG1/MG4): ดันชน "เพื่อน" กี่เฟรมก่อนปลดล็อกเดินผ่าน + ช่วงเวลาที่ผ่านได้ต่อเนื่อง */ const ANTI_STUCK_PEER_TICKS = 13; const ANTI_STUCK_WINDOW_MS = 650; @@ -3519,7 +3523,7 @@ } function moveSpeedTilesThisFrameForWalk() { - return MOVE_SPEED * (isQuizCarry() ? quizCarryWalkSpeedMultActive : 1); + return MOVE_SPEED * (isQuizCarry() ? quizCarryWalkSpeedMultActive : 1) * playDtMul; } function isMovementKey(code) { @@ -3878,7 +3882,9 @@ const md = mapData; const { cw, ch } = getCharacterFootprintWH(md); const cxWorld = (axTile + cw * 0.5) * tileSize; - const cyBottomWorld = (ayTile + ch) * tileSize; + /* ยกผีขึ้น: anchor ที่ "หัว tile" (ayTile) ไม่ใช่เท้า (ayTile+ch) → ภาพผีตัวใหญ่ลอยอยู่ในโซนผี + ไม่ห้อยลงทับโซนคำตอบ SAFE/SCAM (ตำแหน่งจริงอยู่ในโซน y4-6 แต่เดิมภาพห้อยถึง y7-8) */ + const cyBottomWorld = ayTile * tileSize; const [sx, sy] = w2s(cxWorld, cyBottomWorld); /* ความสูง = สูง avatar บนจอ (สอดคล้องกับ drawAvatar: boxH = tileSize * zDraw * ch * 1.35) */ const sizeH = tileSize * zDrawIn * Math.max(ch, 1) * 1.35; @@ -5921,6 +5927,7 @@ /** ช่วงตอบ: บอทสุ่มเดินไปโซนถูก/ผิด/เดินมั่ว ตามระดับ (ฉลาด/กลาง/พลาดบ่อย) */ function previewBotsPrepareAnswerRound() { if (!playBotsEnabled() || !mapData || !isQuiz() || !previewQuizCurrent) return; + if (detectiveCaseFillBots) return; /* เกมจริง: server ตัดสินคำตอบบอทเอง */ const correctTrue = !!previewQuizCurrent.answerTrue; const qt = mapData.quizTrueArea; const qf = mapData.quizFalseArea; @@ -8680,7 +8687,8 @@ } const howtoBg = document.querySelector('#gauntlet-crown-howto-overlay .gch-bg'); if (howtoBg) { - howtoBg.src = questionMissionAssetUrl('popup-Howto.png'); + /* MG1 ใช้ popup-Howtominigame1.png (รูป SAFE/SCAM) — popup-Howto.png เดิมเป็นดีไซน์ MG4 ใส่ผิดที่ */ + howtoBg.src = questionMissionAssetUrl('popup-Howtominigame1.png'); howtoBg.onerror = function () { this.onerror = null; this.src = BASE + '/img/gauntlet-assets/popup-Howto.png'; @@ -9689,6 +9697,7 @@ function fbNum(v) { return (typeof v === 'number' && Number.isFinite(v)) ? Math.round(v * 100) / 100 : undefined; } function broadcastFillBotState() { if (!playBotsEnabled() || !isMePlayHost() || !mapData) return; + if (isQuiz() && detectiveCaseFillBots) return; /* quiz เกมจริง: server เป็นเจ้าของบอท (quiz-bot-move) → ห้าม host ส่งทับ = บอทวาป/ผีหลุด area */ const arr = []; others.forEach((o, id) => { if (!isPreviewBotId(id) || !o) return; @@ -20343,6 +20352,8 @@ } if (isMePlayHost()) return; if (!data || !Array.isArray(data.bots)) return; + /* quiz เกมจริง: server เป็นเจ้าของบอท (quiz-bot-move) + roster (play-roster) → ไม่รับ fill-bot-state ทับ (กันบอทวาป/ผีหลุด) */ + if (isQuiz() && detectiveCaseFillBots) return; /* force รายชื่อ "คนจริง" ให้ตรง host — ลบผี (peer ที่ host ไม่มี แต่ค้างบน client จาก reconnect/snapshot) id-based (ไม่ชนชื่อ default). debounce 3 ครั้งติด กัน peer ที่ host ยังไม่เห็นชั่วคราวถูกลบผิด (self-heal) */ if (Array.isArray(data.humans)) { @@ -20758,6 +20769,51 @@ if (isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase === 'howto') updateSpaceShooterMissionHowtoHud(); }); + /* ฆ่าผี (avatar 2 อันของคนเดียว): server ส่ง roster จริงทุก 1.5s ระหว่างเล่น → ตัด peer ที่ไม่อยู่ใน roster + (เดิม others เป็น add-only ไม่เคย prune — user-left หลุด/มาไม่ถึง = ตัวเก่าค้างเป็นผีขยับไม่ได้) + ข้ามบอท (ไม่อยู่ใน server roster) + ข้ามตัวเราเอง */ + socket.on('play-roster', (d) => { + if (!d || !Array.isArray(d.ids)) return; + const live = new Set(d.ids.map(String)); + let removed = 0; + [...others.keys()].forEach((id) => { + if (isPreviewBotId(id)) return; + if (myId != null && String(id) === String(myId)) return; + if (!live.has(String(id))) { others.delete(id); removed++; } + }); + if (removed > 0 && typeof renderPlayQuizScoreboard === 'function' && isQuiz()) { + try { renderPlayQuizScoreboard(playLiveQuizScores); } catch (e) { /* ignore */ } + } + }); + + /* บอท MG1 quiz: server เป็นเจ้าของการเดิน (host หลุดก็ไม่หยุด) → รับตำแหน่ง แล้ว lerp ใน tick (o.tx/o.ty) */ + socket.on('quiz-bot-move', (d) => { + if (!d || !Array.isArray(d.bots)) return; + if (!(isQuiz() && detectiveCaseFillBots)) return; + d.bots.forEach((rb) => { + if (!rb || typeof rb.id !== 'string') return; + const o = others.get(rb.id); + if (!o) return; /* บอทฝั่ง client ยังไม่ถูกสร้าง — รอ bot sync สร้างก่อน */ + if (rb.elim) { + /* ผี (eliminated): snap ตอน "warp" (กระโดดไกล >1.5 tile เข้าโซนผี) ไม่ให้ไหลผ่านสนาม; + แต่ lerp การเดินเล็กๆ ในโซนผี (กันกระตุก) */ + var jx = Number.isFinite(rb.x) ? rb.x - o.x : 0; + var jy = Number.isFinite(rb.y) ? rb.y - o.y : 0; + if (Math.hypot(jx, jy) > 1.5) { + if (Number.isFinite(rb.x)) o.x = rb.x; + if (Number.isFinite(rb.y)) o.y = rb.y; + } + if (Number.isFinite(rb.x)) o.tx = rb.x; + if (Number.isFinite(rb.y)) o.ty = rb.y; + } else { + if (Number.isFinite(rb.x)) o.tx = rb.x; + if (Number.isFinite(rb.y)) o.ty = rb.y; + } + if (rb.direction) o.direction = rb.direction; + o.botIsWalking = !!rb.isWalking; + }); + }); + /* ===== Disconnect grace + bot takeover (10 วิ รอ reconnect) ===== */ let _playGraceCountdownTimer = null; function showDisconnectGraceBanner(nickname, seconds) { @@ -21124,7 +21180,9 @@ } /** เกมจริง: bot ไม่อยู่ในผล server → HOST ตัดสินบอทคนเดียว (เช็คโซน/respawn/คะแนน) แล้ว broadcast ผ่าน fill-bot-state (เดิมทุก client ตัดสินเอง + respawn ด้วย Math.random แยกกัน → ตำแหน่ง/ล็อก/คะแนนบอทคนละแบบทุกจอ) */ - if (isQuiz() && Array.isArray(r.results) && playBotsEnabled() && isMePlayHost()) { + if (isQuiz() && Array.isArray(r.results) && playBotsEnabled() && isMePlayHost() && !detectiveCaseFillBots) { + /* เกมจริง (detectiveCaseFillBots): server เป็นเจ้าของบอท (คะแนน/คัดออกใน r.scores/r.results แล้ว) → ข้าม host scoring + คงไว้เฉพาะ editor preview (previewFillBots) ที่ไม่มี server session */ const correctTrueBot = !!r.correctTrue; others.forEach(function (o, id) { if (!isPreviewBotId(id) || !o) return; @@ -22436,7 +22494,7 @@ const pathStepMul = (previewFillBots && editorEmbedReturn) ? (isQuizCarry() ? baseEmbedCarryPathMul * (quizCarryWalkSpeedMultActive / QUIZ_CARRY_WALK_SPEED_MULT) : 0.56) : 1; - const step = Math.min(MOVE_SPEED * 1.28 * pathStepMul * carryWalk, len); + const step = Math.min(MOVE_SPEED * 1.28 * pathStepMul * carryWalk * playDtMul, len); /* time-based */ const nx = o.x + (dx / len) * step; const ny = o.y + (dy / len) * step; if (Math.abs(dy) > Math.abs(dx)) o.direction = dy > 0 ? 'down' : 'up'; @@ -22509,6 +22567,7 @@ function stepPreviewBots() { if (!playBotsEnabled() || !isMePlayHost() || !mapData || isFrogger() || isGauntlet() || isStack() || isJumpSurvive() || isSpaceShooter() || isBalloonBoss()) return; + if (isQuiz() && detectiveCaseFillBots) return; /* เกมจริง: server เดินบอท quiz เอง (host หลุดก็ไม่หยุด) */ if (isQuizCarry()) { stepQuizCarryPreviewBots(); return; @@ -22563,7 +22622,7 @@ dxq /= distq; dyq /= distq; if (Math.abs(dyq) > Math.abs(dxq)) o.direction = dyq > 0 ? 'down' : 'up'; else o.direction = dxq > 0 ? 'right' : 'left'; - const step = MOVE_SPEED; + const step = MOVE_SPEED * playDtMul; /* time-based: บอทเร็วเท่ากันทุก FPS ของ host */ const tox = o.x + dxq * step; const toy = o.y + dyq * step; const pathStrictW = isQuizBattle() && quizBattlePathModeActive(mapData); @@ -25483,7 +25542,16 @@ return mv; } function tick() { - if (!mapData) { requestAnimationFrame(tick); return; } + if (!mapData) { playTickLastTs = 0; requestAnimationFrame(tick); return; } + /* dt frame (วินาที) → playDtMul = dt*60 (clamp 0.05s = step ≤0.45 tile กัน tunneling) ใช้ทำ movement time-based */ + { + const _pnow = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now(); + let _pdt = playTickLastTs ? (_pnow - playTickLastTs) / 1000 : (1 / 60); + if (!(_pdt > 0)) _pdt = 1 / 60; + if (_pdt > 0.05) _pdt = 0.05; + playTickLastTs = _pnow; + playDtMul = _pdt * 60; + } /* คำถามพิเศษเปิดอยู่ — หยุดเกมทุกอย่าง (input/sim/timer) เห็นแต่ภาพค้างไว้ ดันเวลาทุกตัวไปข้างหน้าเท่า delta ทุกเฟรม → countdown ที่แสดงหยุดนิ่งจริง */ if (specialQuizFreeze) { @@ -25604,14 +25672,42 @@ } /* บอท preview ขยับที่ o.x/o.y โดยตรง — ห้าม LERP กับ tx/ty เพราะ stepPreviewBots เคยตั้ง tx=ก่อนเดิน ทำให้โดนดึงถอย ~20%/เฟรม กระตุกและแทบไม่ไป */ others.forEach((o, id) => { - /* host เดินบอทเองตรงๆ (o.x/o.y) → ข้าม lerp; แต่ client อื่นต้อง lerp ตามตำแหน่งที่ host ส่งมา (o.tx/o.ty) */ - if (playBotsEnabled() && isPreviewBotId(id) && isMePlayHost()) return; + /* host เดินบอทเองตรงๆ (o.x/o.y) → ข้าม lerp; แต่ client อื่นต้อง lerp ตามตำแหน่งที่ host ส่งมา (o.tx/o.ty) + ยกเว้น quiz เกมจริง: server เดินบอท → ทุกคน (รวม host) lerp ตาม server */ + if (playBotsEnabled() && isPreviewBotId(id) && isMePlayHost() && !(isQuiz() && detectiveCaseFillBots)) return; /* MG4 (quiz_carry) + MG1 (quiz mng8a80o) บอทเดินต่อเนื่องเร็ว → non-host lerp 0.2 จะตามหลัง host ~1-2 ช่อง ("เดินไม่ตรงกัน") เร่ง lerp เฉพาะบอท quiz/quiz_carry ให้ตามตำแหน่ง host ทันขึ้น (ไม่แตะเกมอื่น/ผู้เล่นจริง) */ - const lerpF = (isPreviewBotId(id) && (isQuizCarry() || isQuiz())) ? 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; + /* บอท MG1 server-driven: server ส่งทุก 60ms → lerp นุ่ม 0.22 (เดิม 0.5 = ถึงเป้าใน 1-2 เฟรมแล้วค้าง = กระตุก) */ + const serverDrivenQuizBot = isPreviewBotId(id) && isQuiz() && detectiveCaseFillBots; + const lerpF = serverDrivenQuizBot ? 0.22 : ((isPreviewBotId(id) && (isQuizCarry() || isQuiz())) ? 0.5 : LERP); + const lerpFN = 1 - Math.pow(1 - lerpF, playDtMul); /* frame-rate-normalized → นุ่มเท่ากันทุก FPS */ + if (o.tx != null) o.x += (o.tx - o.x) * lerpFN; + if (o.ty != null) o.y += (o.ty - o.y) * lerpFN; }); + /* client guard: ผีบอท (eliminated) ที่ "ช่องที่ยืน" หลุดนอกโซนผี → snap ไปช่องผีใกล้สุดครั้งเดียว + ใช้ single-tile check (ตรงกับ server) — ถ้าอยู่ในช่องผีแล้วปล่อยให้ลื่นตาม server (กันวาปวนจากเดิมที่ใช้ footprint+สุ่ม) + (เดิม confine อยู่ใน stepPreviewBots ที่ปิดไปสำหรับ quiz → ย้ายมาเป็น guard อิสระ) */ + if (isQuiz() && detectiveCaseFillBots) { + const _gg = quizGhostAreaGridPlay(); + if (_gg) { + others.forEach((o, id) => { + if (!isPreviewBotId(id) || !o) return; + const isGhostBot = !!playQuizEverWrong[String(id)] || (o.quizCannotTrue && o.quizCannotFalse); + if (!isGhostBot) return; + const tx = Math.floor(o.x), ty = Math.floor(o.y); + if (_gg[ty] && _gg[ty][tx] === 1) return; /* ช่องที่ยืนเป็นช่องผีแล้ว → ปล่อย (ตรงกับ server) */ + /* หลุดจริง → หาช่องผีใกล้สุด snap (deterministic ไม่สุ่ม) */ + let best = null, bd = Infinity; + for (let y = 0; y < _gg.length; y++) { + const row = _gg[y] || []; + for (let x = 0; x < row.length; x++) { + if (row[x] === 1) { const d = Math.abs(x - o.x) + Math.abs(y - o.y); if (d < bd) { bd = d; best = { x: x, y: y }; } } + } + } + if (best) { o.x = best.x + 0.5; o.y = best.y + 0.5; o.tx = o.x; o.ty = o.y; } + }); + } + } tickQuizCarryEmbedCountdown(); tickQuizCarryRoundTimers(); updateQuizCarryCarryPhaseHud(); diff --git a/www/html/Game/public/js/room-lobby.js b/www/html/Game/public/js/room-lobby.js index f324115..539993c 100644 --- a/www/html/Game/public/js/room-lobby.js +++ b/www/html/Game/public/js/room-lobby.js @@ -25,6 +25,79 @@ } catch (e) { /* ignore */ } } lobbyLog('lobby-open', 'space=' + spaceId); + + /* ===== DEV HUD แบบ Iron Man: โชว์ real-time ว่า LobbyA โหลดอะไร/ขั้นไหน/กี่% — เปิดได้ 3 ทาง: + (1) ?dbg=1 (2) localStorage.lobbyDbg='1' (3) admin: game-timing.json {lobbyDebugHud:true, lobbyDebugHudNames:[ชื่อ...]} + default ปิด (ผู้เล่นจริงไม่เห็น). โหลดเสร็จ → fade หาย ===== */ + var _dbgForced = (function () { try { return new URLSearchParams(location.search).get('dbg') === '1' || localStorage.getItem('lobbyDbg') === '1'; } catch (e) { return false; } })(); + var _dbgAdmin = false; + function dbgOn() { return _dbgForced || _dbgAdmin; } + var _dbgState = { steps: [ + { k: 'manifest', label: 'MANIFEST', pct: 0, st: 'wait' }, + { k: 'colors', label: 'RESOLVE COLOR', pct: 0, st: 'wait' }, + { k: 'join', label: 'JOIN ROOM', pct: 0, st: 'wait' }, + { k: 'mychar', label: 'MY CHARACTER', pct: 0, st: 'wait' }, + { k: 'occ', label: 'OCCUPANTS', pct: 0, st: 'wait', sub: '' }, + ], t0: Date.now() }; + function _dbgStep(k) { for (var i = 0; i < _dbgState.steps.length; i++) if (_dbgState.steps[i].k === k) return _dbgState.steps[i]; return null; } + function dbgSet(k, pct, st, sub) { + var s = _dbgStep(k); if (!s) return; + if (pct != null) s.pct = Math.max(0, Math.min(100, Math.round(pct))); + if (st) s.st = st; if (sub != null) s.sub = sub; + if (dbgOn()) { dbgEnsure(); dbgRender(); } + } + function dbgEnsure() { + if (!dbgOn() || document.getElementById('lobby-dbg-hud')) return; + if (!document.getElementById('lobby-dbg-css')) { + var st = document.createElement('style'); st.id = 'lobby-dbg-css'; + st.textContent = '#lobby-dbg-hud{position:fixed;top:14px;left:14px;z-index:100000;min-width:248px;padding:12px 14px 11px;' + + 'font:600 11px/1.5 "Share Tech Mono",ui-monospace,monospace;color:#7ff3ff;background:linear-gradient(160deg,rgba(6,20,34,.92),rgba(4,12,22,.92));' + + 'border:1px solid rgba(64,224,255,.5);border-radius:10px;box-shadow:0 0 22px rgba(40,200,255,.25),inset 0 0 18px rgba(40,200,255,.08);' + + 'backdrop-filter:blur(2px);letter-spacing:.5px;transition:opacity .4s ease;pointer-events:none;text-shadow:0 0 6px rgba(64,224,255,.5)}' + + '#lobby-dbg-hud .ttl{display:flex;justify-content:space-between;color:#ffb152;font-size:12px;border-bottom:1px solid rgba(255,177,82,.3);padding-bottom:5px;margin-bottom:7px;text-shadow:0 0 6px rgba(255,160,60,.5)}' + + '#lobby-dbg-hud .row{display:flex;align-items:center;gap:7px;margin:3px 0}' + + '#lobby-dbg-hud .ic{width:13px;text-align:center}' + + '#lobby-dbg-hud .lb{flex:0 0 96px;opacity:.92}' + + '#lobby-dbg-hud .br{flex:1;height:7px;border-radius:4px;background:rgba(64,224,255,.12);overflow:hidden;box-shadow:inset 0 0 4px rgba(0,0,0,.5)}' + + '#lobby-dbg-hud .bf{height:100%;width:0;background:linear-gradient(90deg,#1fbfff,#7ff3ff);box-shadow:0 0 8px rgba(80,220,255,.8);transition:width .15s}' + + '#lobby-dbg-hud .pc{flex:0 0 34px;text-align:right;opacity:.85}' + + '#lobby-dbg-hud .sub{font-size:10px;color:#9fe8ff;opacity:.7;margin:1px 0 2px 20px}' + + '#lobby-dbg-hud .ok{color:#5effa6}#lobby-dbg-hud .run .ic{color:#ffd166;animation:dbgpulse .8s infinite}#lobby-dbg-hud .wait{opacity:.4}' + + '@keyframes dbgpulse{50%{opacity:.3}}'; + document.head.appendChild(st); + } + var el = document.createElement('div'); el.id = 'lobby-dbg-hud'; + document.body.appendChild(el); dbgRender(); + } + function dbgRender() { + var el = document.getElementById('lobby-dbg-hud'); if (!el) return; + var ms = Date.now() - _dbgState.t0; + var h = '
◢ LOBBY LOAD' + (ms / 1000).toFixed(1) + 's
'; + _dbgState.steps.forEach(function (s) { + var ic = s.st === 'done' ? '✓' : (s.st === 'run' ? '⟳' : '·'); + h += '
' + ic + '' + s.label + '' + + '' + s.pct + '%
'; + if (s.sub) h += '
▸ ' + s.sub + '
'; + }); + el.innerHTML = h; + } + function dbgFinish() { + var el = document.getElementById('lobby-dbg-hud'); if (!el) return; + _dbgState.steps.forEach(function (s) { s.st = 'done'; s.pct = 100; }); + dbgRender(); + setTimeout(function () { el.style.opacity = '0'; setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 450); }, 600); + } + /* อ่าน flag จาก admin (game-timing.json) — เปิด HUD ให้ทุกคน หรือเฉพาะชื่อที่ระบุ */ + try { + fetch(BASE + '/data/game-timing.json?_=' + Date.now()).then(function (r) { return r.json(); }).then(function (g) { + if (!g || !g.lobbyDebugHud) return; + var names = Array.isArray(g.lobbyDebugHudNames) ? g.lobbyDebugHudNames.map(function (s) { return String(s).trim().toLowerCase(); }).filter(Boolean) : []; + var me = ''; try { me = (getProfileDisplayName() || '').trim().toLowerCase(); } catch (e) { /* ignore */ } + _dbgAdmin = (names.length === 0) || names.indexOf(me) >= 0; + if (dbgOn()) { dbgEnsure(); dbgRender(); } + }).catch(function () {}); + } catch (e) { /* ignore */ } + if (_dbgForced) { dbgEnsure(); } const DESIGN_MENU = typeof designMainMenuHref === 'function' ? designMainMenuHref() : '/Main-Menu/'; if (!spaceId) { location.href = DESIGN_MENU; return; } const roomIdValueEl = document.getElementById('room-id-value'); @@ -506,18 +579,18 @@ function injectRoomLoadingOverlay() { if (document.getElementById('room-loading-overlay')) { - setTimeout(hideRoomLoading, 12000); /* overlay เป็น static ใน html แล้ว — ตั้ง safety กันค้างถาวรถ้า preload แฮงค์ */ + setTimeout(hideRoomLoading, 30000); /* overlay เป็น static ใน html แล้ว — ตั้ง safety กันค้างถาวรถ้า preload แฮงค์ (occupants นานขึ้น) */ return; } var style = document.createElement('style'); - style.textContent = '#room-loading-overlay{position:fixed;inset:0;z-index:99999;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:18px;background:radial-gradient(circle at 50% 38%, #0b1430, #060818);color:#cfe9ff;font-family:Kanit,Sarabun,system-ui,sans-serif;transition:opacity .45s ease}#room-loading-overlay.is-hidden{opacity:0;pointer-events:none}#room-loading-spinner{width:56px;height:56px;border:5px solid rgba(34,211,238,.22);border-top-color:#22d3ee;border-radius:50%;animation:rlspin 1s linear infinite}@keyframes rlspin{to{transform:rotate(360deg)}}#room-loading-overlay .rl-txt{font-size:1.1rem;letter-spacing:.04em;opacity:.9}'; + style.textContent = '#room-loading-overlay{position:fixed;inset:0;z-index:99999;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:18px;background:rgba(0,0,0,0.9);color:#cfe9ff;font-family:Kanit,Sarabun,system-ui,sans-serif;transition:opacity .45s ease}#room-loading-overlay.is-hidden{opacity:0;pointer-events:none}#room-loading-spinner{width:56px;height:56px;border:5px solid rgba(34,211,238,.22);border-top-color:#22d3ee;border-radius:50%;animation:rlspin 1s linear infinite}@keyframes rlspin{to{transform:rotate(360deg)}}#room-loading-overlay .rl-txt{font-size:1.1rem;letter-spacing:.04em;opacity:.9}'; document.head.appendChild(style); var ov = document.createElement('div'); ov.id = 'room-loading-overlay'; ov.innerHTML = '
กำลังโหลดตัวละคร…
'; (document.body || document.documentElement).appendChild(ov); - /* safety: กัน overlay ค้างถาวรถ้า preload/manifest แฮงค์ (ไม่มี roomPreloadReady) */ - setTimeout(hideRoomLoading, 12000); + /* safety: กัน overlay ค้างถาวรถ้า preload/manifest แฮงค์ (ไม่มี roomPreloadReady) — occupants นานขึ้น */ + setTimeout(hideRoomLoading, 30000); } function hideRoomLoading() { @@ -542,7 +615,7 @@ if (ov) { ov.classList.remove('is-hidden'); ov.style.opacity = ''; } var t = ov && ov.querySelector('.rl-txt'); if (t) t.textContent = text || 'กำลังโหลด…'; - if (!(opts && opts.persist)) setTimeout(hideRoomLoading, 12000); + if (!(opts && opts.persist)) setTimeout(hideRoomLoading, 30000); } /* แสดง % บนหน้าโหลด LobbyB (ตอนกลับจากมินิเกม) — ตามความคืบหน้า preload ตัวละคร */ @@ -554,7 +627,7 @@ el.textContent = 'กำลังโหลดตัวละคร… ' + p + '%'; } - function preloadLobbyTintedCharacter(id, theme, skin, cb) { + function preloadLobbyTintedCharacter(id, theme, skin, cb, onFrame) { if (!id || (!theme && !skin)) { if (cb) cb(); return; } var dirs = ['down', 'up', 'left', 'right']; var suffixes = ['idle', '0', '1', '2', '3']; @@ -563,6 +636,7 @@ var finished = false; function one() { doneCount++; + if (onFrame) { try { onFrame(doneCount, total, id); } catch (e) { /* ignore */ } } if (!finished && doneCount >= total) { finished = true; if (cb) cb(); @@ -589,8 +663,8 @@ }); } - function preloadMyTintedCharacter(cb) { - preloadLobbyTintedCharacter(getStoredCharacterId(), myTintTheme, myTintSkin, cb); + function preloadMyTintedCharacter(cb, onFrame) { + preloadLobbyTintedCharacter(getStoredCharacterId(), myTintTheme, myTintSkin, cb, onFrame); } /** อุ่น tint บอท + peer ที่มีสี — กันกระตุกตอนบอทเดิน/idle ครั้งแรก */ @@ -616,8 +690,10 @@ tasks.forEach(function (t) { preloadLobbyTintedCharacter(t[0], t[1], t[2], function () { done++; - if (onProgress) onProgress(done, total); + if (onProgress) onProgress(done, total, t[0], 1, 1); if (done >= total && cb) cb(); + }, function (fd, ft, cid) { + if (onProgress) onProgress(done, total, cid, fd, ft); }); }); } @@ -1170,6 +1246,10 @@ const keys = {}; const MOVE_SPEED = 0.15; + /* L2 time-based movement: เดิม step=MOVE_SPEED ต่อเฟรม → จอ 120fps เดินเร็วเป็น 2 เท่า (คนวิ่งเร็วช้าไม่เท่ากัน) + เปลี่ยนเป็น units/วินาที (0.15/เฟรม × 60fps = 9 u/s) แล้วคูณ dt → ทุกเครื่องเร็วเท่ากันไม่ขึ้นกับ FPS */ + const MOVE_SPEED_PER_SEC = MOVE_SPEED * 60; + let lobbyLastTickTs = 0; /* timestamp เฟรมก่อน (ms) ใช้คำนวณ dt */ let lastMoveSend = 0; const moveCodes = ['KeyW', 'KeyA', 'KeyS', 'KeyD', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']; let lobbyInteractPulse = null; @@ -2398,29 +2478,36 @@ if (arr.length) socket.emit('lobby-bot-move', { bots: arr }); } - function lerpLobbyBots() { + function lerpLobbyBots(factor) { + const f = (typeof factor === 'number' && factor > 0) ? factor : LERP; lobbyBots.forEach((b) => { - if (typeof b.botSyncTX === 'number') b.x += (b.botSyncTX - b.x) * LERP; - if (typeof b.botSyncTY === 'number') b.y += (b.botSyncTY - b.y) * LERP; + if (typeof b.botSyncTX === 'number') b.x += (b.botSyncTX - b.x) * f; + if (typeof b.botSyncTY === 'number') b.y += (b.botSyncTY - b.y) * f; }); } function lobbyTick() { const me = peers.get(socket.id); - if (!mapData || !me) { requestAnimationFrame(lobbyTick); return; } + if (!mapData || !me) { lobbyLastTickTs = 0; requestAnimationFrame(lobbyTick); return; } + /* dt = เวลาจริงตั้งแต่เฟรมก่อน (วินาที) — clamp 0.05s กันกระโดดไกลตอนสลับแท็บ/เฟรมหลุด + เฟรมแรก (ts0===0) ใช้ค่า 60fps มาตรฐาน */ + const _now = Date.now(); + let dt = lobbyLastTickTs ? (_now - lobbyLastTickTs) / 1000 : (1 / 60); + if (!(dt > 0)) dt = 1 / 60; + if (dt > 0.05) dt = 0.05; + lobbyLastTickTs = _now; + /* LERP remote avatar แบบ frame-rate-normalized → ความนุ่มเท่ากันทุก FPS (เดิม 0.2/เฟรม = 120fps ลู่เร็วเกิน) */ + const lerpA = 1 - Math.pow(1 - LERP, dt * 60); peers.forEach((p, id) => { if (id !== socket.id && (p.tx != null || p.ty != null)) { - if (p.tx != null) p.x += (p.tx - p.x) * LERP; - if (p.ty != null) p.y += (p.ty - p.y) * LERP; + if (p.tx != null) p.x += (p.tx - p.x) * lerpA; + if (p.ty != null) p.y += (p.ty - p.y) * lerpA; } }); if (lobbyBotSlotCount > 0) { - if (hostId === socket.id) { - if (!suspectPickOverlayOpen) stepLobbyCaseBots(); - broadcastLobbyBotPositions(); /* host เดินจริง → ส่งตำแหน่งให้ทุกคน */ - } else { - lerpLobbyBots(); /* client อื่น: ตามตำแหน่งจาก host (ไม่สุ่มเดินเอง) */ - } + /* L1: server เป็นเจ้าของบอท lobby แล้ว — ทุก client (รวม host) แค่ lerp ตามตำแหน่งจาก server + (เดิม host เดินเอง frame-based → บอทวิ่งเร็วช้าต่างกันตาม FPS/host) */ + lerpLobbyBots(lerpA); } const w = mapData.width || 20, h = mapData.height || 15; const preWalkX = me.x, preWalkY = me.y; @@ -2464,7 +2551,7 @@ } if (accX !== 0 || accY !== 0) { const len = Math.sqrt(accX * accX + accY * accY) || 1; - const step = Math.min(MOVE_SPEED, len); + const step = Math.min(MOVE_SPEED_PER_SEC * dt, len); /* time-based: เร็วเท่ากันทุก FPS */ const nx = me.x + (accX / len) * step; const ny = me.y + (accY / len) * step; if (canWalkLobby(nx, ny, me.x, me.y)) { @@ -3232,6 +3319,7 @@ socket.on('connect', () => { lobbyLog('socket-connect', ''); + dbgSet('join', 40, 'run', 'socket connected · waiting server…'); /* ส่งค่าสีที่เลือกใน Main-Lobby ติดไปด้วย — server จะใช้ค่านี้ถ้ายังว่าง (กันโชว์สีแดง default ตอนเข้าใหม่) */ var savedThemeIdx = null; var savedSkinIdx = null; @@ -3264,6 +3352,7 @@ roomJoinReady = true; clientLobbyMapId = res.mapId != null ? res.mapId : null; lobbyLog('join-ok', 'peers=' + (res.peers || []).length + ' host=' + (res.hostId === socket.id ? 'me' : 'other') + ' map=' + res.mapId + ' bots=' + (res.botSlotCount != null ? res.botSlotCount : '?')); + dbgSet('join', 100, 'done', 'peers ' + (res.peers || []).length + ' · bots ' + (res.botSlotCount != null ? res.botSlotCount : '?')); /* ไอคอนกุญแจ 🔒 โชว์เฉพาะห้อง private (จาก res.isPrivate) — public ไม่โชว์ */ try { const lockEl = document.getElementById('room-id-lock'); @@ -4042,9 +4131,9 @@ } catch (err) { console.warn('WebRTC signal error', err); } }); - /* รับตำแหน่งบอทจาก host (เฉพาะ client ที่ไม่ใช่ host) → เก็บเป็นเป้า lerp ใน lobbyTick */ + /* รับตำแหน่งบอทจาก server (L1: server เป็นเจ้าของบอท lobby) → เก็บเป็นเป้า lerp ใน lobbyTick + ทุก client รับเหมือนกันรวม host (เดิม host เดินเอง — ถูกย้ายมา server แล้ว) */ socket.on('lobby-bot-move', (data) => { - if (hostId === socket.id) return; /* host เป็นเจ้าของเอง ไม่ต้องรับ */ if (!data || !Array.isArray(data.bots)) return; data.bots.forEach((rb) => { if (!rb || typeof rb.id !== 'string') return; @@ -4057,6 +4146,27 @@ }); }); + /* L3 ฆ่าผี: server ส่ง roster จริงทั้งหมดเป็นระยะ → ตัด peer ที่ไม่อยู่ในนั้นทิ้ง + (เดิมพึ่ง user-left อย่างเดียว — ถ้า event หลุด/มาไม่ถึง = ผีค้างถาวรใน LobbyA/B) */ + socket.on('lobby-roster', (data) => { + if (!data || !Array.isArray(data.ids)) return; + const live = new Set(data.ids.map(String)); + let removed = 0; + [...peers.keys()].forEach((id) => { + if (String(id) === String(socket.id)) return; /* ตัวเราเองไม่ตัด */ + if (!live.has(String(id))) { + if (typeof closePeer === 'function') closePeer(id); + peers.delete(id); + removed++; + } + }); + if (removed > 0) { + updatePlayersHud(); + renderPeers(); + redrawLobbyMap(); + } + }); + socket.on('host-changed', (data) => { hostId = data.hostId || null; const isHost = hostId === socket.id; @@ -7605,24 +7715,33 @@ updateLobbyProfileAvatar(); setupRoomCustomize(); lobbyLog('preload-start', ''); + dbgSet('manifest', 0, 'run'); rlEnsureManifest(function () { lobbyLog('manifest-done', ''); + dbgSet('manifest', 100, 'done'); dbgSet('colors', 0, 'run'); rlResolveMyColors(function () { lobbyLog('colors-resolved', ''); + dbgSet('colors', 100, 'done'); dbgSet('mychar', 0, 'run'); updateRoomProfileAvatarTinted(); preloadMyTintedCharacter(function () { lobbyLog('mychar-tinted', 'roomJoinReady=' + roomJoinReady); - /* พอ tint "ตัวเรา" เสร็จ → เปิดให้เล่นได้เลย (ไม่รอ tint คนอื่นครบ = เดิมกิน ~12 วิ คอขวด) - tint คนอื่นโหลด background → avatar คนอื่นโผล่สีตามทีหลังนิดหน่อย (ยอมรับได้) */ - roomPreloadReady = true; - setRoomLoadingPct(100); - lobbyLog('preload-ready(mychar)', ''); - maybeHideRoomLoading(); + dbgSet('mychar', 100, 'done'); dbgSet('occ', 0, 'run'); + /* overlay (ดำ 0.9) ค้างต่อจน occupants = ย้อมสีตัวละคร "ทุกคนในห้อง" (คนอื่น+บอท) เสร็จครบ — โชว์ % + กันเห็น avatar คนอื่นโผล่ขึ้นสีทีหลัง (เดิมซ่อน overlay หลังตัวเรา → occupants โหลด background ไม่มี loading) */ + setRoomLoadingPct(0); preloadAllLobbyTintedOccupants(function () { - lobbyLog('preload-occupants-done(bg)', ''); - }, function (d, t) { + lobbyLog('preload-occupants-done', ''); + dbgSet('occ', 100, 'done'); dbgFinish(); + roomPreloadReady = true; + setRoomLoadingPct(100); + lobbyLog('preload-ready(occupants)', ''); + maybeHideRoomLoading(); + }, function (d, t, cid, fd, ft) { setRoomLoadingPct(t ? (d / t) * 100 : 100); + dbgSet('occ', t ? (d / t) * 100 : 100, 'run', 'char ' + d + '/' + t + (cid ? ' · ' + String(cid).slice(0, 12) + (ft ? ' ' + Math.round((fd / ft) * 100) + '%' : '') : '')); }); + }, function (fd, ft) { + dbgSet('mychar', ft ? (fd / ft) * 100 : 0, 'run', 'frame ' + fd + '/' + ft); }); }); }); diff --git a/www/html/Game/public/play.html b/www/html/Game/public/play.html index 7f880f1..cff154c 100644 --- a/www/html/Game/public/play.html +++ b/www/html/Game/public/play.html @@ -3926,6 +3926,20 @@ #gauntlet-crown-howto-overlay.gch-quiz-tf-mock .gch-inner.gch-inner--art .btn-gch-ready:active { transform: translateY(2px); } + /* MG1 quiz — ปุ่ม READY 2 สถานะ: + ยังไม่กด = cyan เต็ม (รูป2 btn-ready.png · override greyscale global) · กดแล้ว = เขียว+✓ (รูป3 btn-ready-on.png) */ + #gauntlet-crown-howto-overlay.gch-quiz-tf-mock .gch-inner.gch-inner--art .btn-gch-ready:not(.is-me-ready):not(.is-start-phase), + #gauntlet-crown-howto-overlay.gch-quiz-tf-mock .gch-inner.gch-inner--art .btn-gch-ready:not(.is-me-ready):not(.is-start-phase) img { + filter: drop-shadow(0 0 18px rgba(120, 220, 255, 0.65)) !important; /* cyan เต็ม ไม่ย้อมเทา */ + } + #gauntlet-crown-howto-overlay.gch-quiz-tf-mock .gch-inner.gch-inner--art .btn-gch-ready.is-me-ready:not(.is-start-phase) { + background-image: url('/Game/img/QUESTION/btn-ready-active.png') !important; /* รูปเขียว READY ✓ (มี check ในรูปแล้ว) */ + filter: drop-shadow(0 0 16px rgba(74, 222, 128, 0.7)) !important; + } + #gauntlet-crown-howto-overlay.gch-quiz-tf-mock .gch-inner.gch-inner--art .btn-gch-ready.is-me-ready:not(.is-start-phase):hover { + transform: translateY(-3px); + filter: drop-shadow(0 0 26px rgba(74, 222, 128, 0.95)) !important; + } /* Minigame-2 mno9kb07 — HOW TO PLAY บน canvas (แทน mg-bg ใน mock 01-howto) */ #gauntlet-crown-howto-overlay.gch-mg2-mock { display: block; @@ -5412,7 +5426,7 @@ - +
v —