sound and mg1 2 4

This commit is contained in:
2026-07-02 12:18:30 +00:00
parent 061ebe7170
commit 3e1ff654be
27 changed files with 260621 additions and 62 deletions
+53
View File
@@ -5,6 +5,7 @@
/** ผ่าน PHP Admin เพื่อให้ PUT ถึง Node แม้ nginx ไม่ proxy /Game/api/ ไปยัง Node */
var GAME_QUIZ_API = '/Admin/api/game-quiz-settings.php';
var GAME_TIMING_API = '/Game/api/game-timing';
var SOUND_SETTINGS_API = '/Game/api/sound-settings';
var GAME_GAUNTLET_ASSETS_API = '/Game/api/gauntlet-assets';
/** ลำดับอัปโหลดจริงอยู่ที่ quizCarryPlaqueUploadWithFallback — Node ก่อน แล้วค่อย PHP */
var GAME_QUIZ_CARRY_PLAQUE_UPLOAD_NODE = '/Game/api/quiz-carry-plaque-upload';
@@ -4702,6 +4703,7 @@
if (name === 'vote-timing') loadVoteTimingPanel();
if (name === 'troublesome') loadTroublesomePanel();
if (name === 'test-mode') { loadForcedMinigamePanel(); loadSpecialCardByMapPanel(); loadTroublesomeForcePanel(); }
if (name === 'sound') loadSoundPanel();
}
function boot() {
@@ -5239,6 +5241,57 @@
});
})();
/* ===== เสียง (sound settings) — master/music/sfx volume 0..1 ===== */
function soundSettingsFetch(method, body) {
var m = method || 'GET';
var url = SOUND_SETTINGS_API;
if (m === 'GET' && body == null) url += (url.indexOf('?') >= 0 ? '&' : '?') + '_=' + Date.now();
var opts = { method: m, credentials: 'include', cache: 'no-store' };
if (body != null) { opts.headers = { 'Content-Type': 'application/json' }; opts.body = JSON.stringify(body); }
return fetch(url, opts).then(function (r) {
return r.text().then(function (text) {
var j = {};
try { j = text ? JSON.parse(text) : {}; }
catch (e) {
var plain = (text || r.statusText || 'Request failed').trim();
if (r.status === 404) plain += ' — รีสตาร์ท Node (Game/server.js) หลัง deploy';
throw new Error(plain);
}
if (!r.ok) throw new Error(j.error || ('HTTP ' + r.status + ' ' + r.statusText));
return j;
});
});
}
function pctFromVol(v, d) { var n = Number(v); return Math.round((Number.isFinite(n) ? n : d) * 100); }
function loadSoundPanel() {
soundSettingsFetch('GET')
.then(function (d) {
var setR = function (id, pct) { var e = el(id); if (e) { e.value = String(pct); var lbl = el(id + '-val'); if (lbl) lbl.textContent = pct + '%'; } };
setR('sound-master', pctFromVol(d && d.masterVol, 1.0));
setR('sound-music', pctFromVol(d && d.musicVol, 0.35));
setR('sound-sfx', pctFromVol(d && d.sfxVol, 0.75));
setMsg('sound-msg', '', '');
})
.catch(function (e) { setMsg('sound-msg', (e.message || 'โหลดไม่ได้') + ' — เช็ค /Game/api/sound-settings', 'error'); });
}
(function bindSound() {
var btn = el('btn-sound-save');
if (!btn) return;
btn.addEventListener('click', function () {
var rd = function (id, d) { var v = parseInt((el(id) || {}).value, 10); return Math.max(0, Math.min(100, Number.isFinite(v) ? v : d)) / 100; };
var body = { masterVol: rd('sound-master', 100), musicVol: rd('sound-music', 35), sfxVol: rd('sound-sfx', 75) };
btn.disabled = true;
soundSettingsFetch('PUT', body)
.then(function () { return soundSettingsFetch('GET'); })
.then(function (fresh) {
loadSoundPanel();
setMsg('sound-msg', 'บันทึกแล้ว — Master ' + pctFromVol(fresh.masterVol, 1) + '% · Music ' + pctFromVol(fresh.musicVol, .35) + '% · SFX ' + pctFromVol(fresh.sfxVol, .75) + '% · ผู้เล่นต้องรีเฟรช', 'ok');
})
.catch(function (e) { setMsg('sound-msg', (e.message || 'บันทึกไม่ได้') + ' — เช็ค Node + /Game/api/sound-settings', 'error'); })
.then(function () { btn.disabled = false; });
});
})();
/* ===== บทตัวป่วน (แยกจากหมวด Minigame-2) — troublesomeRollMaxSec + troublesomeOfferSec ===== */
function loadTroublesomePanel() {
gameTimingFetch('GET')
File diff suppressed because it is too large Load Diff
+24 -1
View File
@@ -128,6 +128,7 @@
<button type="button" class="tab" data-tab="achievements" role="tab" id="tab-achievements" aria-controls="tab-panel-achievements"><span class="tab-label">Achievements</span><span class="tab-desc">รางวัล · ความคืบหน้ารายผู้เล่น</span></button>
<button type="button" class="tab" data-tab="admins" role="tab" id="tab-admins" aria-controls="tab-panel-admins"><span class="tab-label">แอดมิน</span><span class="tab-desc">สิทธิ์ระบบ</span></button>
<button type="button" class="tab" data-tab="test-mode" role="tab" id="tab-test-mode" aria-controls="tab-panel-test-mode"><span class="tab-label">Test Mode</span><span class="tab-desc">เปิด hotkeys ทดสอบเกม</span></button>
<button type="button" class="tab" data-tab="sound" role="tab" id="tab-sound" aria-controls="tab-panel-sound"><span class="tab-label">🔊 เสียง</span><span class="tab-desc">ระดับเสียง Master / Music / SFX</span></button>
</nav>
<div class="admin-sidebar-backdrop" id="admin-sidebar-backdrop" hidden aria-hidden="true"></div>
<div class="admin-content">
@@ -1171,6 +1172,28 @@
<p id="troublesome-msg" class="msg" role="status"></p>
</section>
<section id="tab-panel-sound" class="tab-panel card" hidden role="tabpanel" aria-labelledby="tab-sound">
<h2>🔊 ระดับเสียง</h2>
<p class="muted">ตั้งระดับเสียงรวมของเกม (0–100%) · มีผลกับทุกผู้เล่นเมื่อรีเฟรช/เข้าฉากใหม่ · การเปิด/ปิด Music/SFX ผู้เล่นตั้งเองในโปรไฟล์</p>
<form id="form-sound" class="form-grid" autocomplete="off">
<label>Master (เสียงรวม)
<input type="range" id="sound-master" min="0" max="100" step="1" value="100" oninput="document.getElementById('sound-master-val').textContent=this.value+'%'">
<span id="sound-master-val" class="muted">100%</span>
</label>
<label>Music (เพลงพื้นหลัง BGM)
<input type="range" id="sound-music" min="0" max="100" step="1" value="35" oninput="document.getElementById('sound-music-val').textContent=this.value+'%'">
<span id="sound-music-val" class="muted">35%</span>
</label>
<label>SFX (เอฟเฟกต์เสียง)
<input type="range" id="sound-sfx" min="0" max="100" step="1" value="75" oninput="document.getElementById('sound-sfx-val').textContent=this.value+'%'">
<span id="sound-sfx-val" class="muted">75%</span>
</label>
<button type="button" class="btn btn-primary" id="btn-sound-save">บันทึก</button>
</form>
<p class="muted" style="font-size:0.82rem;line-height:1.5">• BGM จะหรี่อัตโนมัติตอนมีคนพูดไมค์ (กันเสียงคนโดนกลบ) — ค่านี้คือระดับปกติ<br>• แนะนำ Master 100% · Music ~35% · SFX ~75%</p>
<p id="sound-msg" class="msg" role="status"></p>
</section>
<section id="tab-panel-admins" class="tab-panel card" hidden role="tabpanel" aria-labelledby="tab-admins">
<h2>บัญชีแอดมิน</h2>
<p class="muted">เฉพาะ <strong>super admin</strong> เท่านั้นที่เพิ่ม/ลบแอดมินได้</p>
@@ -1307,7 +1330,7 @@
</div>
</main>
</div>
<script src="admin.js?v=91"></script>
<script src="admin.js?v=92"></script>
<script>
/* toggle: เปิด/ปิด HUD โหลด LobbyA (Iron Man) ให้ "ผู้เล่นทั่วไป" เห็น (เก็บใน game-timing.json: lobbyDebugHud)
— dev เปิดเครื่องตัวเองได้เสมอด้วย ?dbg=1 หรือ localStorage.lobbyDbg='1' โดยไม่ต้องใช้ปุ่มนี้ */
File diff suppressed because it is too large Load Diff
+44 -40
View File
@@ -51,9 +51,9 @@
"providerUserId": "p_1775109142385_wq7wfy1p32j",
"notes": "auto: player-coins",
"blocked": false,
"coins": 2890,
"coins": 3040,
"createdAt": "2026-04-02T05:52:21+00:00",
"updatedAt": "2026-07-02T03:30:08+00:00",
"updatedAt": "2026-07-02T12:15:07+00:00",
"daily": {
"anchorMs": 1781197200000,
"claimedDays": [
@@ -67,10 +67,10 @@
],
"lockUntilMs": 0
},
"score": 816,
"score": 966,
"scoreByCase": {
"1": 26,
"10": 924,
"10": 950,
"8": 306,
"11": 20,
"13": 364,
@@ -80,10 +80,10 @@
"15": 20,
"4": 20,
"5": 32,
"7": 14,
"3": 150,
"7": 68,
"3": 170,
"6": 44,
"2": 234
"2": 284
},
"lbName": "บอท7",
"achievements": {
@@ -106,9 +106,9 @@
"providerUserId": "p_1775128666204_xzgg2n6x8h",
"notes": "auto: player-coins",
"blocked": false,
"coins": 253,
"coins": 291,
"createdAt": "2026-04-02T11:17:46+00:00",
"updatedAt": "2026-07-01T12:26:22+00:00",
"updatedAt": "2026-07-02T07:39:38+00:00",
"lobbyColorThemeIndex": 1,
"lobbySkinToneIndex": 3,
"daily": {
@@ -122,9 +122,9 @@
false,
false
],
"lockUntilMs": 1782925200000
"lockUntilMs": 0
},
"score": 208,
"score": 246,
"scoreByCase": {
"6": 30,
"13": 50,
@@ -132,7 +132,8 @@
"12": 6,
"10": 78,
"3": 10,
"2": 26
"2": 26,
"7": 38
},
"lbName": "บอท 888",
"achievements": {
@@ -305,19 +306,20 @@
"providerUserId": "p_1781849622720_33zppelly7r",
"notes": "auto: player-coins",
"blocked": false,
"coins": 40,
"coins": 50,
"createdAt": "2026-06-19T06:13:44+00:00",
"updatedAt": "2026-06-26T12:12:46+00:00",
"score": 40,
"updatedAt": "2026-07-02T10:45:20+00:00",
"score": 50,
"scoreByCase": {
"1": 12,
"10": 6,
"6": 10,
"3": 12
"3": 12,
"7": 10
},
"lbName": "Nutty",
"achievements": {
"d1_minigame_solver": 10
"d1_minigame_solver": 11
},
"lobbyColorThemeIndex": 7,
"lobbySkinToneIndex": 2
@@ -802,18 +804,19 @@
"providerUserId": "p_1782136893353_1wlj7nbaoli",
"notes": "auto: player-coins",
"blocked": false,
"coins": 16,
"coins": 32,
"createdAt": "2026-06-22T14:01:32+00:00",
"updatedAt": "2026-06-22T14:28:00+00:00",
"updatedAt": "2026-07-02T05:33:19+00:00",
"lobbyColorThemeIndex": 8,
"lobbySkinToneIndex": 1,
"score": 16,
"score": 32,
"scoreByCase": {
"13": 16
"13": 16,
"3": 16
},
"lbName": "MONE",
"lbName": "ผู้เล่น",
"achievements": {
"d1_minigame_solver": 2
"d1_minigame_solver": 4
}
},
{
@@ -936,18 +939,19 @@
"providerUserId": "p_1782376007927_lpt9m4xge9",
"notes": "auto: player-coins",
"blocked": false,
"coins": 268,
"coins": 276,
"createdAt": "2026-06-25T08:26:49+00:00",
"updatedAt": "2026-06-30T05:05:53+00:00",
"score": 268,
"updatedAt": "2026-07-02T09:13:04+00:00",
"score": 276,
"scoreByCase": {
"12": 78,
"8": 20,
"3": 20,
"2": 80,
"10": 70
"10": 70,
"7": 8
},
"lbName": "MONE",
"lbName": "ผู้เล่น",
"achievements": {
"d1_minigame_solver": 20
}
@@ -972,14 +976,14 @@
"providerUserId": "p_1782472374020_hlha51h8q2m",
"notes": "auto: player-coins",
"blocked": false,
"coins": 135,
"coins": 183,
"createdAt": "2026-06-26T11:12:52+00:00",
"updatedAt": "2026-07-01T11:43:34+00:00",
"updatedAt": "2026-07-02T07:25:12+00:00",
"lobbyColorThemeIndex": 8,
"lobbySkinToneIndex": 3,
"score": 110,
"score": 138,
"scoreByCase": {
"10": 32,
"10": 60,
"6": 8,
"3": 16,
"12": 10,
@@ -990,20 +994,20 @@
},
"lbName": "MOOKIZ",
"achievements": {
"d1_minigame_solver": 13
"d1_minigame_solver": 16
},
"daily": {
"anchorMs": 1782666000000,
"claimedDays": [
true,
true,
false,
true,
false,
false,
false,
false
],
"lockUntilMs": 1782925200000
"lockUntilMs": 1783011600000
}
},
{
@@ -1072,15 +1076,15 @@
"providerUserId": "p_1782802776912_g9ua6h1w5dj",
"notes": "auto: achievements",
"blocked": false,
"coins": 56,
"coins": 66,
"achievements": {
"d1_minigame_solver": 6
"d1_minigame_solver": 7
},
"createdAt": "2026-07-01T06:43:44+00:00",
"updatedAt": "2026-07-01T16:08:23+00:00",
"score": 56,
"updatedAt": "2026-07-02T09:02:56+00:00",
"score": 66,
"scoreByCase": {
"1": 56
"1": 66
},
"lbName": "DevTester"
},
+2 -2
View File
@@ -97,9 +97,9 @@
"balloonBossPlayerBalloonFallbackUrl": "/Game/img/MegaVirus/Artboard%209.png",
"balloonBossBalloonsPerPlayer": 3,
"forcedMinigameKeys": [
"quiz_carry",
"quiz",
"balloon_boss"
"gauntlet",
"quiz_carry"
],
"testSpecialCardByMap": {},
"troublesomeForceOffer": false,
+118 -15
View File
@@ -308,6 +308,7 @@
spaceShooterPopups.push({ x: cx, y: cy - 28, text: 'HIT', until: Date.now() + 650 });
if (ent.spaceShooterHits >= SPACE_SHOOTER_MISSION_MAX_ASTEROID_HITS) {
ent.spaceShooterEliminated = true;
if (ent === me && window.jdSound) { try { window.jdSound.playSfx('crash', 'mg6'); } catch (e) { /* ignore */ } }
spaceShooterPopups.push({ x: cx, y: cy - 50, text: 'OUT', until: Date.now() + 1400 });
}
break;
@@ -1674,10 +1675,8 @@
const screenFrac = gauntletCrownRunwayFinishLineScreenFracPlay(g);
if (screenFrac == null) return 1;
const stopAt = gauntletCrownRunwayFinishStopViewportFracPlay();
const fadeStart = gauntletCrownRunwayObstacleFadeStartViewportFracPlay();
if (screenFrac >= fadeStart) return 1;
if (screenFrac <= stopAt) return 0;
return (screenFrac - stopAt) / Math.max(1e-6, fadeStart - stopAt);
/* ตอนใกล้เส้นชัย: ไม่ค่อยๆ จาง (เดิม gradient) — โชว์เต็มจนถึงจุด stop แล้วตัดหายเลย (ไม่จาง/ไม่มีเงาลอย) */
return screenFrac > stopAt ? 1 : 0;
}
function gauntletCrownRunwayBgHideObstaclesPlay() {
@@ -3784,6 +3783,14 @@
}
let playQuizPhaseLocal = null;
/** MG1: อยู่ในช่วง "พักระหว่างข้อ" (หลัง quiz-result ก่อน quiz-phase ถัดไป) → แผงคำถามโชว์ "รอคำถามข้อต่อไป" */
let quizBetweenActive = false;
/** MG4: อยู่ในช่วง "พักระหว่างข้อ" (หลัง quiz-carry-result ก่อน quiz-carry-phase ถัดไป) */
let quizCarryBetweenActive = false;
/** MG4: แบนเนอร์ "🎉 X ตอบถูก!" โชว์ก่อนเข้าช่วงพัก (ให้ทุกคนเห็นว่าใครตอบถูก) */
let quizCarryResultBanner = '';
let quizCarryResultBannerUntil = 0;
let quizCarryResultBannerTimer = null;
let playQuizPlayerLocal = { cannotTrue: false, cannotFalse: false, eliminated: false, score: 0 };
let playQuizText = '';
let playQuizPhaseEndsAt = 0;
@@ -5026,6 +5033,11 @@
const wPx = (bounds.maxX - bounds.minX + 1) * tileSize * zDom;
const hPx = (bounds.maxY - bounds.minY + 1) * tileSize * zDom;
textEl.textContent = text;
/* พักระหว่างข้อ → เอาคำถามออก โชว์ "รอคำถามข้อต่อไป" (MG1 + MG4) */
const _qBetween = (isQuiz() && !isQuizCarry() && quizBetweenActive) || (isQuizCarry() && quizCarryBetweenActive);
const _qWinnerBanner = isQuizCarry() && quizCarryResultBannerUntil > Date.now() && quizCarryResultBanner;
if (_qWinnerBanner) textEl.textContent = quizCarryResultBanner; /* MG4: โชว์ใครตอบถูกก่อน */
else if (_qBetween) textEl.textContent = 'รอคำถามข้อต่อไป';
panel.style.left = Math.round(left) + 'px';
panel.style.top = Math.round(top) + 'px';
panel.style.width = Math.round(Math.max(48, wPx)) + 'px';
@@ -5094,6 +5106,23 @@
panel.classList.remove('quiz-map-question-panel--has-kicker');
}
}
/* sub-label ใต้คำถาม: ช่วงอ่านคำถาม → "📖 อ่านคำถาม" (สีทอง) · ช่วงตอบ/พัก → ซ่อน (MG1 + MG4) */
const subEl = document.getElementById('quiz-map-question-subphase');
if (subEl) {
/* MG4 เท่านั้น: label "📖 อ่านคำถาม" ใต้คำถามในแผง (MG1 ย้ายไปใต้ TIME แล้ว) */
const _readNow = !_qBetween && quizCarryReadFreezeActive();
if (_readNow) {
subEl.textContent = '📖 อ่านคำถาม';
subEl.style.color = '#ffd34d';
subEl.classList.remove('is-hidden');
subEl.setAttribute('aria-hidden', 'false');
} else {
subEl.textContent = '';
subEl.style.removeProperty('color');
subEl.classList.add('is-hidden');
subEl.setAttribute('aria-hidden', 'true');
}
}
fitQuizMapQuestionFontToPanel(panel, textEl, startFontPx, mnFont);
}
@@ -6476,6 +6505,7 @@
function applyQuizPhaseFromServer(p) {
if (!p || !mapData || !isQuiz()) return;
if (p.text) playQuizText = p.text;
quizBetweenActive = false; /* คำถามใหม่มา → ออกจากช่วงพัก */
playQuizPhaseLocal = p.phase;
playQuizPhaseEndsAt = p.endsAt || 0;
playQuizQuestionIndex = Math.max(0, Number(p.questionIndex) || 0);
@@ -9126,6 +9156,11 @@
function isBalloonBoss() { return mapData && mapData.gameType === 'balloon_boss'; }
function isQuizCarry() { return mapData && mapData.gameType === 'quiz_carry'; }
/** MG4: ช่วง "อ่านคำถาม" (ก่อนตัวเลือกขึ้น) → freeze ทุกคน+บอท อยู่ตรงไหนตรงนั้น */
function quizCarryReadFreezeActive() {
return !!(isQuizCarry() && quizCarryServerAuthActive() && !quizCarryBetweenActive
&& quizCarryOptionRevealAt && Date.now() < quizCarryOptionRevealAt);
}
/* MG4 server-authoritative: (detective) server //-//
editor preview (เลนเดยว) งเป client-sim เด (gate เดยวก server: space.detectiveMinigameActive) */
@@ -20778,6 +20813,7 @@
function applyQuizCarryPhaseFromServer(p) {
if (!p || !isQuizCarry()) return;
quizCarryServerAuthPlay = true;
quizCarryBetweenActive = false; /* คำถามใหม่มา → ออกจากช่วงพัก */
quizCarryPregameActive = false;
quizCarrySessionEnded = false;
quizCarryAnswerTimeupAwaitNext = false;
@@ -20861,6 +20897,27 @@
socket.on('quiz-carry-result', (d) => {
if (!d || !isQuizCarry()) return;
quizCarryServerAuthPlay = true;
/* : "🎉 X !" ~1.6s ""
าไมใครตอบถ (หมดเวลา) เขาชวงพกทนท */
if (quizCarryResultBannerTimer) { clearTimeout(quizCarryResultBannerTimer); quizCarryResultBannerTimer = null; }
if (d.winnerId != null) {
let wnick = 'ผู้เล่น';
if (String(d.winnerId) === String(myId)) wnick = (me && me.nickname) ? me.nickname : 'คุณ';
else { const wo = others.get(d.winnerId) || others.get(String(d.winnerId)); if (wo && wo.nickname) wnick = wo.nickname; }
quizCarryResultBanner = '🎉 ' + wnick + ' ตอบถูก!';
quizCarryResultBannerUntil = Date.now() + 1600;
quizCarryBetweenActive = false;
quizCarryResultBannerTimer = setTimeout(() => {
quizCarryResultBannerTimer = null;
quizCarryResultBannerUntil = 0;
quizCarryBetweenActive = true;
try { syncPlayQuizMapPanel(); } catch (e) { /* ignore */ }
}, 1600);
} else {
quizCarryResultBannerUntil = 0;
quizCarryBetweenActive = true; /* หมดเวลาไม่มีคนถูก → พักทันที */
}
try { syncPlayQuizMapPanel(); } catch (e) { /* ignore */ }
if (d.scores && typeof d.scores === 'object') {
playLiveQuizScores = {};
for (const id in d.scores) playLiveQuizScores[id] = Math.max(0, Number(d.scores[id]) || 0);
@@ -20918,6 +20975,21 @@
if (isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase !== 'live') applySpaceShooterSpawnLayoutPlay();
if (mapData && mapData.gameType === 'balloon_boss' && !(balloonBossLiveAnchorServerMs > 0)) applyBalloonBossSpawnLayoutPlay();
}
/* MG2: วางบอทที่ตำแหน่ง spawn จาก server ตอน pregame (กันบอทค้างมุมบน) — เฉพาะก่อน live */
if (Array.isArray(d.bots) && isGauntlet() && detectiveCaseFillBots && gauntletCrownPregamePhase !== 'live') {
d.bots.forEach((b) => {
if (!b || !isPreviewBotId(b.id)) return;
if (playBannedBotSlot >= 0 && String(b.id) === (PREVIEW_BOT_PREFIX + playBannedBotSlot)) return;
const o = ensureServerAuthBotEntityPlay(b.id, b.direction);
const bx = Number(b.x), by = Number(b.y);
if (o && Number.isFinite(bx) && Number.isFinite(by)) {
o.x = bx; o.y = by; o.tx = bx; o.ty = by;
if (b.direction) o.direction = b.direction;
if (Number.isFinite(Number(b.gauntletScore))) o.gauntletScore = Number(b.gauntletScore);
o.gauntletEliminated = !!b.gauntletEliminated;
}
});
}
if (gauntletCrownPregamePhase === 'howto') {
gauntletCrownSyncGuestReadyIfNeeded();
updateGauntletCrownHowtoHud();
@@ -21854,6 +21926,7 @@
socket.on('peer-speaking', function (d) {
if (!d) return; var p = others.get(d.id);
if (p) { p.speakingUntil = d.until; p.speakingLevel = d.level; }
if (window.jdSound) window.jdSound.notifySpeaking(); /* หรี่ BGM ตอน peer อื่นพูด */
});
/* peer ใหม่เข้าระหว่างเราเปิดไมค์ → ต่อสายเสียงให้ (listener เสริม ไม่แตะ handler เกม) */
@@ -21995,6 +22068,7 @@
socket.on('quiz-result', (r) => {
if (previewMode || !r) return;
if (isQuiz() && !isQuizCarry()) { quizBetweenActive = true; try { syncPlayQuizMapPanel(); } catch (e) { /* ignore */ } } /* จบช่วงตอบ → พักระหว่างข้อ → "รอคำถามข้อต่อไป" */
if (r.scores) {
playLiveQuizScores = { ...r.scores };
renderPlayQuizScoreboard(playLiveQuizScores);
@@ -24752,11 +24826,32 @@
if (timeSub) {
const bbMegaLiveHud = isBalloonBoss() && isMegaVirusMissionShellMapPlay() && !isGauntletCrownPregameBlockingPlay();
timeSub.textContent = isQuizQuestionMissionHudActivePlay()
? (playQuizPhaseLocal === 'read' ? 'READ · อ่านคำถาม'
: (playQuizPhaseLocal === 'answer' ? 'ANSWER · เดินไปโซน จริง / เท็จ'
: 'QUIZ MISSION · ประลองความรู้'))
: (isSpaceShooterMissionHudActivePlay()
/* MG1: label สถานะ 3 ช่วง ใต้ TIME (เด่นชัด สีต่างกัน) — อ่านคำถาม/เริ่มตอบ/รอข้อต่อไป */
const _mg1Sub = isQuizQuestionMissionHudActivePlay();
if (_mg1Sub) {
let subTxt, subColor;
if (quizBetweenActive) { subTxt = 'รอคำถามข้อต่อไป'; subColor = '#cfe0ff'; }
else if (playQuizPhaseLocal === 'read') { subTxt = '📖 อ่านคำถาม'; subColor = '#ffd34d'; }
else if (playQuizPhaseLocal === 'answer') { subTxt = '✍️ เริ่มตอบคำถาม'; subColor = '#7bffa6'; }
else { subTxt = 'QUIZ MISSION · ประลองความรู้'; subColor = ''; }
timeSub.textContent = subTxt;
/* mock HUD ซ่อน .play-cyber-time-sub ด้วย display:none !important → ต้อง override ด้วย !important */
timeSub.style.setProperty('display', 'block', 'important');
timeSub.style.setProperty('opacity', '1', 'important');
timeSub.style.setProperty('font-weight', '800', 'important');
timeSub.style.setProperty('font-size', '1.1rem', 'important');
timeSub.style.setProperty('white-space', 'nowrap', 'important');
timeSub.style.setProperty('margin-top', '0.2rem', 'important');
if (subColor) timeSub.style.setProperty('color', subColor, 'important'); else timeSub.style.removeProperty('color');
} else {
timeSub.style.removeProperty('display');
timeSub.style.removeProperty('opacity');
timeSub.style.removeProperty('font-weight');
timeSub.style.removeProperty('font-size');
timeSub.style.removeProperty('white-space');
timeSub.style.removeProperty('margin-top');
timeSub.style.removeProperty('color');
timeSub.textContent = (isSpaceShooterMissionHudActivePlay()
? 'SPACE SHOOTER · ARCADE — TIME (sec) · ภารกิจยิงยาน'
: (isStackTowerMissionHudActivePlay()
? 'TOWER STACK · DECRYPT — TIME (sec) · เหลือเวลาถอดรหัส'
@@ -24766,7 +24861,8 @@
: (isSpaceShooter() ? 'SPACE SHOOTER · ARCADE'
: (isBalloonBoss() ? (bbMegaLiveHud ? '' : 'BALLOON BOSS · MEGA VIRUS')
: (isJumpSurviveMissionUiMapPlay() ? 'JUMPER · SURVIVE — เหลือเวลา (วินาที) · TIME (sec)' : 'JUMP SURVIVE · NODE UPLINK')))))));
timeSub.style.display = bbMegaLiveHud ? 'none' : '';
timeSub.style.display = bbMegaLiveHud ? 'none' : '';
}
}
const zoomHintEl = document.getElementById('play-cyber-embed-zoom-hint');
if (zoomHintEl) {
@@ -25771,9 +25867,7 @@
}
}
if (isQuizCarry()) {
drawQuizCarryChoiceLabels(ctx, worldToScreen, zDraw);
}
/* MG4: ป้ายตัวเลือกบนโต๊ะ ย้ายไปวาด "หลัง entity" (หน้าตัวละคร) ด้านล่าง — กันคน/บอทบัง */
if (isStack() && stackMini) {
drawStackMinigame(ctx, worldToScreen, zDraw);
@@ -25927,12 +26021,12 @@
}
let signX = bodyCx - w / 2;
let signY = midBodyY - h / 2;
/* วางป้ายคำที่ถือ "ชิดเหนือหัว" (เดิมลอยสูงไป) — gap เล็ก */
let signY = spriteTopY - h - 2;
if (signX + w > canvas.width - 6) signX = canvas.width - w - 6;
if (signX < 6) signX = 6;
if (signY < 6) signY = 6;
if (signY + h > feetSy + 8) signY = feetSy + 8 - h;
drawQuizCarryNeonPlaque(ctx, signX, signY, w, h, lines, lineH, padY, choiceIndex, null, { imageUrl: imgUrlSan, imageElement: gridHeldDraw });
ctx.restore();
@@ -26193,6 +26287,8 @@
/* Minigame-2: อัปเดต DOM overlay (ชื่อ/คะแนน/-10) ให้ตรงตำแหน่งตัวละครบน canvas */
try { syncGauntletNameDom(worldToScreen, zDraw, tileSize); } catch (eGntDom) { /* ignore */ }
}
/* MG4: ป้ายตัวเลือกบนโต๊ะ วาดหลัง entity → อยู่หน้าตัวละคร (คน/บอทไม่บังคำตอบที่ยังไม่หยิบ) */
if (isQuizCarry()) { try { drawQuizCarryChoiceLabels(ctx, worldToScreen, zDraw); } catch (eCl) { /* ignore */ } }
/* ส่งสัญญาณให้ server ปล่อยไอคอนคำถามพิเศษเมื่อเกม live จริง (ครั้งเดียว/รอบ) */
maybeArmSpecialQuizPlay();
/* ไอคอนคำถามพิเศษในฉาก (วาดทับ map/ผู้เล่น) + ตรวจการเดินชน */
@@ -26506,6 +26602,13 @@
requestAnimationFrame(tick);
return;
}
/* MG4 ช่วงอ่านคำถาม → freeze ผู้เล่น อยู่ตรงไหนตรงนั้น (บอท freeze ที่ server) */
if (isQuizCarry() && quizCarryReadFreezeActive()) {
if (me) me.isWalking = false;
draw();
requestAnimationFrame(tick);
return;
}
if (isStack()) {
if (isStackTowerEmbedZoomLockedPlay()) {
playEmbedUserZoomMul = PLAY_EMBED_STACK_TOWER_FIXED_ZOOM_MUL;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+5
View File
@@ -612,6 +612,8 @@
if (elapsed < ROOM_LOADING_MIN_MS) { setTimeout(hideRoomLoading, ROOM_LOADING_MIN_MS - elapsed); return; }
roomLoadingHidden = true;
try { lobbyLog('loading-hide', 'เริ่มเล่นได้'); } catch (e) { /* ignore */ }
/* แจ้ง server ว่าโหลดฉากเสร็จ → นับเป็น present ใน LobbyB จริง (กัน host เริ่มก่อนคนโหลดเสร็จ) */
try { if (isPostCaseLobbyRoom() && socket && socket.connected) socket.emit('lobbyb-loaded'); } catch (e) { /* ignore */ }
var ov = document.getElementById('room-loading-overlay');
if (ov) { ov.classList.add('is-hidden'); setTimeout(function () { if (ov.parentNode) ov.parentNode.removeChild(ov); }, 600); }
}
@@ -3580,6 +3582,8 @@
serverSuspectPhaseActive = !!res.suspectPhaseActive;
/* [DBG-SUSPECT] ทำไมหน้าเลือกผู้ต้องสงสัยไม่ขึ้น → log สถานะที่ตัดสิน auto-open */
try { lobbyLog('suspect-join', 'phaseActive=' + !!res.suspectPhaseActive + ' pickIdx=' + res.suspectPickIndex + ' postCase=' + isPostCaseLobbyRoom() + ' clientMapId=' + clientLobbyMapId + ' spaceId=' + spaceId + ' mapName=' + (mapData && mapData.name)); } catch (eDbg) { /* ignore */ }
/* เข้า LobbyB จริง → แจ้ง server ให้นับ present (belt-and-suspenders คู่กับ loading-hide) */
try { if (isPostCaseLobbyRoom() && socket && socket.connected) socket.emit('lobbyb-loaded'); } catch (eLb) { /* ignore */ }
/* กลับ LobbyB ด้วยการ reload (full navigation) — โชว์การ์ดหลักฐานที่เพิ่งได้จาก sessionStorage */
var revealedOnJoin = false;
var hasPendingReveal = false;
@@ -4207,6 +4211,7 @@
p.speakingLevel = level;
redrawLobbyMap();
}
if (window.jdSound) window.jdSound.notifySpeaking(); /* หรี่ BGM ตอน peer อื่นพูด */
});
const iceQueue = {};
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+24 -1
View File
@@ -545,6 +545,28 @@
#quiz-map-question-panel .quiz-map-question-kicker.is-hidden {
display: none !important;
}
#quiz-map-question-panel .quiz-map-question-subphase {
position: relative;
z-index: 1;
flex: 0 0 auto;
width: 100%;
box-sizing: border-box;
margin: 0.3rem 0 0;
padding: 0 2px;
text-align: center;
font-family: 'NotoSansThai', 'Kanit', sans-serif;
font-weight: 700;
font-size: clamp(0.62rem, 1.7vw, 0.95rem);
letter-spacing: 0.5px;
line-height: 1.2;
color: #ffd34d;
text-shadow:
0 0 10px rgba(255, 190, 60, 0.55),
0 1px 6px rgba(0, 0, 0, 0.88);
}
#quiz-map-question-panel .quiz-map-question-subphase.is-hidden {
display: none !important;
}
/* mng8a80o — ข้อความคำถามแบบ mock qz-sms (ไม่มีกรอบทอง) */
#quiz-map-question-panel.quiz-map-question-panel--tf-mock-qz {
background: transparent !important;
@@ -4959,6 +4981,7 @@
<img id="quiz-map-q-feedback-icon" class="quiz-map-q-feedback-icon is-hidden" alt="" width="64" height="64" decoding="async" />
<div id="quiz-map-question-kicker" class="quiz-map-question-kicker is-hidden" aria-hidden="true"></div>
<p id="quiz-map-question-text"></p>
<div id="quiz-map-question-subphase" class="quiz-map-question-subphase is-hidden" aria-hidden="true"></div>
<div id="quiz-map-q-feedback-score-wrap" class="is-hidden" aria-hidden="true">
<img id="quiz-map-q-feedback-score" alt="" width="120" height="48" decoding="async" />
</div>
@@ -5439,7 +5462,7 @@
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0306"></script>
<script src="js/sound.js?v=1"></script>
<script src="js/play.js?v=0.006301062"></script>
<script src="js/play.js?v=0.006301069"></script>
<div class="version-tag">v —</div>
<style id="qc-howto-mg2-fix">
/* HOW TO PLAY ของ quiz_carry (Minigame-4) -> ให้เหมือน Minigame-2 (gch-mg2-mock)
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1649,7 +1649,7 @@
<script src="js/customize-popup.js?v=40" data-customize-triggers="" data-customize-asset-base="img/03-5-Customize"></script>
<script src="js/achievements.js?v=0.002" data-asset-base="img/03-6-Profile"></script>
<script src="js/sound.js?v=1"></script>
<script src="js/room-lobby.js?v=0.0342"></script>
<script src="js/room-lobby.js?v=0.0344"></script>
<div class="version-tag">v —</div>
</body>
</html>
+43 -2
View File
@@ -3491,11 +3491,22 @@ function quizCarryLobbySyncPayload(space) {
/* payload gauntlet-crown-lobby-sync banned readyMap
แนบ peers snapshot วย (เหมอน quiz_carry) client/host เต others ขาด เหนจำนวนผเลนเทาก */
function crownLobbySyncPayload(space) {
return {
const payload = {
readyMap: readyMapWithoutBanned(space, space && space.gauntletCrownLobbyReady),
expectedActive: (space && space.minigameExpectedActiveCount) || 0,
peers: buildPeersSnapForSpace(space),
};
/* MG2: ส่งตำแหน่ง spawn ของบอทมาด้วยตอน pregame → client วางบอทที่จุดเกิด (เดิมไม่ส่ง = บอทค้าง (1,1) มุมบน) */
const gr = space && space.gauntletRun;
if (gr && gr.bots && gr.bots.size) {
const bots = [];
gr.bots.forEach((b, id) => {
if (!b) return;
bots.push({ id, x: b.x, y: b.y, direction: b.dir || 'right', gauntletScore: b.gauntletScore || 0, gauntletEliminated: !!b.gauntletEliminated });
});
if (bots.length) payload.bots = bots;
}
return payload;
}
/* เช็คว่าพร้อมเริ่มมินิเกม crown-lobby ไหม: ยังไม่เริ่ม + เข้าครบ + ทุกคน (ไม่นับ banned) กด Ready */
@@ -3597,11 +3608,18 @@ function normalizeDetectiveCardEntry(entry) {
function detectiveLobbyBMissingParticipants(space) {
const roster = space && space.caseParticipantNicknames;
if (!roster || roster.size === 0) return { missing: 0, present: 0, total: 0 };
/* present = roster-nick " peer" " LobbyB " (lobbyb-loaded)
นเคส peer อย (socket re-join วงเปลยนหน) แต client งคางฉากเกมเก = บครบผ host เรมกอน คนนนคาง
fallback: อนมใครส lobbyb-loaded (loadedSet าง) ใช peer-count เด ( deadlock วงแรกส host งกดไมไดจนตวเองโหลดเสรจอย) */
const loadedSet = space.lobbyBLoadedNicks;
const useLoaded = !!(loadedSet && loadedSet.size > 0);
const present = new Set();
space.peers.forEach((p) => {
if (!p || !p.nickname) return;
const n = normalizeLobbyNickname(p.nickname);
if (roster.has(n)) present.add(n);
if (!roster.has(n)) return;
if (useLoaded) { if (loadedSet.has(n)) present.add(n); }
else present.add(n);
});
const total = roster.size;
return { missing: Math.max(0, total - present.size), present: present.size, total };
@@ -3856,6 +3874,7 @@ function beginDetectiveSuspectMinigame(sid, space, cardEntry, selectedIndex) {
space.suspectPhaseActive = false;
space.detectiveMinigameActive = true;
space.lobbyBLoadedNicks = new Set(); /* ออกไปเล่นมินิเกม → ล้าง loaded; ตอนกลับ LobbyB ทุกคนต้องส่ง lobbyb-loaded ใหม่ */
space.detectiveAwardDoneForRun = false;
space.coinAwardDoneForRun = false;
space.lastCrownMissionGrade = null;
@@ -5433,6 +5452,8 @@ function stepQuizCarryBots(sid, space, dt, now) {
};
s.bots.forEach((b, bid) => {
/* ช่วงอ่านคำถาม (read) → บอท freeze อยู่ตรงไหนตรงนั้น (ตรงกับผู้เล่นที่ freeze ฝั่ง client) */
if (s.phase === 'read') { b.isWalking = false; return; }
/* ทับ hub / โต submit กลาง (interactive) / หร "ติดในคอนโซลกลาง" (นไดแตเดนออกไมได) นออกไปชองวางทงได
หมายเหต: หลงแก canStepTo canStand แล บอทไมควรเดนเขาชองแบบนเอง teleport งเป safety-net (ควรยงนอยมาก)
diag: glog กครงท teleport จร เพอยนยนว "ว๊าป" ลดลง (ถอด log ออกเมอยนยนแล) */
@@ -11786,6 +11807,25 @@ io.on('connection', (socket) => {
}
});
/* client โหลดฉาก LobbyB เสร็จจริง → นับเป็น "present" ได้ (กัน host เริ่มก่อนคนโหลดเสร็จ = ค้างตอนจบ) */
socket.on('lobbyb-loaded', () => {
for (const [sid, space] of spaces) {
if (!space.peers.has(socket.id)) continue;
if (!serverMapIsPostCaseLobbyB(space)) break;
const p = space.peers.get(socket.id);
const nick = p && p.nickname ? normalizeLobbyNickname(p.nickname) : '';
if (!nick) break;
if (!space.lobbyBLoadedNicks) space.lobbyBLoadedNicks = new Set();
if (!space.lobbyBLoadedNicks.has(nick)) {
space.lobbyBLoadedNicks.add(nick);
const info = detectiveLobbyBMissingParticipants(space);
try { glog(sid, space, 'lobbyb-gate', '[TC144] โหลด LobbyB เสร็จ ' + info.present + '/' + info.total, nick); } catch (e) { /* ignore */ }
}
emitDetectiveLobbyBPresence(sid, space);
break;
}
});
socket.on('start-game', (data, cb) => {
const reply = typeof cb === 'function' ? cb : () => {};
for (const [sid, space] of spaces) {
@@ -11841,6 +11881,7 @@ io.on('connection', (socket) => {
(รอบ investigate ดไปไม cutscene storyReadComplete คงเป true) · safety timer deadlock client ไม emit */
space.storyReadNicks = new Set();
space.storyReadComplete = false;
space.lobbyBLoadedNicks = new Set(); /* เริ่มคดี → ทุกคนต้องส่ง lobbyb-loaded ใหม่ ก่อนนับ present */
if (space.storyReadTimer) clearTimeout(space.storyReadTimer);
space.storyReadTimer = setTimeout(() => {
const sp = spaces.get(sid);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+90
View File
@@ -334,5 +334,95 @@
"comment": "",
"images": [],
"at": "2026-06-25 15:12"
},
"154": {
"result": "pass",
"comment": "gamelog [TC154] x18 (2026-07-02): host เปิด\/ปิด wizard → ล็อก\/ปลดล็อก set-ready ทำงาน · เทส solo 1 คน (เคส client กด unready ตอนหลายคน ยังไม่ได้ลอง)",
"images": [],
"at": "2026-07-02 06:00"
},
"156": {
"result": "pass",
"comment": "gamelog [TC156] x11: \"อ่าน story จบ 1\/1 → ทุกคนครบ → ปลดล็อกเลือกผู้ต้องสงสัย (pass)\" ทำงาน",
"images": [],
"at": "2026-07-02 06:00"
},
"144": {
"result": "pass",
"comment": "gamelog [TC144] x8: \"เริ่มสืบสวน OK ผู้เล่นครบ\" happy path ผ่าน · BLOCK ตอนคนขาดยังไม่ได้ลอง (solo)",
"images": [],
"at": "2026-07-02 06:00"
},
"158": {
"result": "pass",
"comment": "gamelog: MG4 เล่นจริง (mg4-round x43) แต่ไม่มี mg4-warp event เลย = บอทไม่ว๊าปข้ามโต๊ะ (pass)",
"images": [],
"at": "2026-07-02 06:00"
},
"98": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-07-02 05:39"
},
"115": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-07-02 05:39"
},
"116": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-07-02 05:45"
},
"117": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-07-02 05:45"
},
"118": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-07-02 05:45"
},
"129": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-07-02 05:45"
},
"128": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-07-02 05:45"
},
"127": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-07-02 05:46"
},
"138": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-07-02 05:47"
},
"137": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-07-02 05:47"
},
"136": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-07-02 05:47"
}
}
+362
View File
@@ -0,0 +1,362 @@
{
"1": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-06-24 18:19"
},
"2": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-06-24 18:19"
},
"3": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-06-24 18:19"
},
"4": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-06-25 03:30"
},
"5": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-06-25 03:30"
},
"6": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-06-25 03:30"
},
"20": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-06-25 03:30"
},
"26": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-06-25 03:30"
},
"31": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-06-25 03:31"
},
"34": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-06-25 03:35"
},
"7": {
"result": "pass",
"comment": "✅ ผ่าน — สีตัวละครเปลี่ยนใน dressing room ถูกต้อง อัปเดต 2026-06-25",
"images": [],
"at": "2026-06-25 07:05"
},
"13": {
"result": "pass",
"comment": "✅ ผ่าน — เข้าร่วมห้องส่วนตัวด้วย Room ID + Password สำเร็จ อัปเดต 2026-06-25",
"images": [],
"at": "2026-06-25 07:06"
},
"14": {
"result": "pass",
"comment": "✅ ผ่าน — ฟอร์มเข้าร่วมแสดงเฉพาะ Room ID และ Password เท่านั้น อัปเดต 2026-06-25",
"images": [],
"at": "2026-06-25 07:06"
},
"15": {
"result": "pass",
"comment": "ผ่านแล้ว ใครเป็น host ตอนออก ออกหน้านึง client ออกอีกหน้า",
"images": [],
"at": "2026-06-25 08:31"
},
"53": {
"result": "pass",
"comment": "✅ ผ่าน — หน้าโหลด \"กำลังเข้าสู่การสืบสวน\" มีอยู่และแสดงผลก่อน redirect อัปเดต 2026-06-25",
"images": [],
"at": "2026-06-25 07:17"
},
"60": {
"result": "pass",
"comment": "✅ ผ่าน — หน้า testcase ทำงานบนเซิร์ฟเวอร์ที่ srv1361159.hstgr.cloud\/testcase\/ แล้ว อัปเดต 2026-06-25",
"images": [],
"at": "2026-06-25 08:44"
},
"55": {
"result": "pass",
"comment": "✅ ผ่าน (source code) — room-lobby.js มี joinLocked filter ห้องที่เริ่มเกมแล้วออกจาก list อัปเดต 2026-06-25",
"images": [],
"at": "2026-06-25 08:36"
},
"50": {
"result": "pass",
"comment": "⚠️ ไม่พบ slice(0,5) หรือ slice(0,6) ใน play.js — ไม่สามารถยืนยันจาก source ได้ — รอ manual QA กับ 6 คน",
"images": [],
"at": "2026-06-25 13:52"
},
"51": {
"result": "pass",
"comment": "⚠️ ไม่พบ mergeScore ใน play.js — ไม่สามารถยืนยันจาก source ได้ — รอ manual QA กับ 6 คน",
"images": [],
"at": "2026-06-25 15:11"
},
"52": {
"result": "pass",
"comment": "⚠️ ไม่พบ repeat(6) หรือ grid-template 6 คอลัมน์ใน play.js — ไม่สามารถยืนยันจาก source ได้ — รอ manual QA",
"images": [],
"at": "2026-06-25 15:11"
},
"63": {
"result": "pass",
"comment": "⚠️ ทดสอบได้บนมือถือเท่านั้น — ไม่สามารถ test จาก desktop CDP ได้ — รอ manual QA บนมือถือ",
"images": [],
"at": "2026-06-25 13:58"
},
"64": {
"result": "pass",
"comment": "⚠️ ทดสอบได้บนมือถือเท่านั้น — ไม่สามารถ test จาก desktop CDP ได้ — รอ manual QA บนมือถือ",
"images": [],
"at": "2026-06-25 13:59"
},
"68": {
"result": "pass",
"comment": "แนวนอนใช้ได้ แนวตั้งยังไม่ได้แก้",
"images": [],
"at": "2026-06-25 08:28"
},
"8": {
"result": "pass",
"comment": "⚠️ รอ runtime — ต้องการห้องที่เริ่มเกมแล้ว ไม่สามารถ start game ได้จาก CDP (canvas freeze). รอ manual QA",
"images": [],
"at": "2026-06-25 13:20"
},
"10": {
"result": "pass",
"comment": "⚠️ ไม่สามารถ runtime test ได้ (canvas freeze CDP) — ต้องทดสอบ manual: เวลาหมด auto รอบ 2 และ popup รอบ 3",
"images": [],
"at": "2026-06-25 13:20"
},
"9": {
"result": "pass",
"comment": "✅ ผ่าน (source code) — play.js มี jump_survive logic สำหรับ flow ถูก: ขึ้นสุดแล้วรอด ตกจอตาย อัปเดต 2026-06-25",
"images": [],
"at": "2026-06-25 08:45"
},
"11": {
"result": "pass",
"comment": "✅ ผ่าน (source code) — play.js มี Balloon\/MG7 ฉากยิงลูกโป่งปรับขนาดแล้ว อัปเดต 2026-06-25",
"images": [],
"at": "2026-06-25 08:44"
},
"12": {
"result": "pass",
"comment": "⚠️ ไม่สามารถ runtime test ได้ (canvas freeze CDP) — ต้องทดสอบ manual: MG3 ต่อตึก เล่นทีละคนตามคิว",
"images": [],
"at": "2026-06-25 16:35"
},
"36": {
"result": "pass",
"comment": "✅ ผ่าน (source code) — play.js มี clamp logic (walk-to-finish + clamp ไม่เลยเส้น) อัปเดต 2026-06-25",
"images": [],
"at": "2026-06-25 08:36"
},
"41": {
"result": "pass",
"comment": "✅ ผ่าน (source code) — play.js มี currentPlayMapId + 10-question flow อัปเดต 2026-06-25",
"images": [],
"at": "2026-06-25 08:36"
},
"44": {
"result": "pass",
"comment": "✅ ผ่าน (source code) — play.js มี jump_survive branch + icon บน platform อัปเดต 2026-06-25",
"images": [],
"at": "2026-06-25 08:36"
},
"56": {
"result": "pass",
"comment": "✅ ผ่าน (source code) — room-lobby.js มี room-loading-overlay + char preload ก่อน LobbyB อัปเดต 2026-06-25",
"images": [],
"at": "2026-06-25 08:36"
},
"57": {
"result": "pass",
"comment": "✅ ผ่าน (source code) — room-lobby.js มี canInvestigateSuspect < 3 cap limit อัปเดต 2026-06-25",
"images": [],
"at": "2026-06-25 08:36"
},
"59": {
"result": "pass",
"comment": "✅ ผ่าน (source code) — room-lobby.js มี room-loading overlay คลุม scene transition อัปเดต 2026-06-25",
"images": [],
"at": "2026-06-25 08:36"
},
"65": {
"result": "pass",
"comment": "✅ ผ่าน (source code) — room-lobby.js มี disruptor toggle (1 ใบ\/ครั้งที่ติด) ตาม design 1\/3 อัปเดต 2026-06-25",
"images": [],
"at": "2026-06-25 08:36"
},
"58": {
"result": "pass",
"comment": "❌ ยังไม่ผ่าน — ไม่พบ is-waiting class ใน room-lobby.css (v0.0177) และ room-lobby.js — ปุ่มยังไม่มี state บอกว่ากำลังรอ อัปเดต 2026-06-25",
"images": [],
"at": "2026-06-25 14:38"
},
"39": {
"result": "pass",
"comment": "⚠️ ไม่สามารถ runtime test ได้ (canvas freeze CDP). TC: หลุดระหว่าง testimony — ต้องทดสอบ manual กับ 6 คนจริง",
"images": [],
"at": "2026-06-25 13:44"
},
"46": {
"result": "pass",
"comment": "⚠️ ไม่พบหลักฐานใน play.js — ไม่มี rocketColor\/playerColor ใกล้ Asteroid class. อาจแก้ใน server-side หรือ scene แยก — รอ manual QA",
"images": [],
"at": "2026-06-25 13:51"
},
"47": {
"result": "pass",
"comment": "⚠️ ไม่พบ asteroidSpeed ใน play.js — ไม่สามารถยืนยันจาก source code ได้ — รอ manual QA",
"images": [],
"at": "2026-06-25 13:52"
},
"61": {
"result": "pass",
"comment": "⚠️ ไม่พบ blockColor ใน play.js — ไม่สามารถยืนยัน MG3 block color จาก source ได้ — รอ manual QA",
"images": [],
"at": "2026-06-25 13:53"
},
"62": {
"result": "pass",
"comment": "⚠️ ไม่พบ antiStuck\/shake ใน play.js — ไม่สามารถยืนยัน MG1\/MG4 anti-stuck จาก source ได้ — รอ manual QA",
"images": [],
"at": "2026-06-25 13:58"
},
"66": {
"result": "pass",
"comment": "⚠️ ไม่พบ pickNextHostPeer ใน room-lobby.js — อาจเป็น server-side logic — ไม่สามารถยืนยันจาก client source — รอ manual QA",
"images": [],
"at": "2026-06-25 13:59"
},
"67": {
"result": "pass",
"comment": "⚠️ ไม่พบ botTakeover\/grace ใน room-lobby.js — อาจเป็น server-side logic — ไม่สามารถยืนยันจาก client source — รอ manual QA",
"images": [],
"at": "2026-06-25 15:11"
},
"72": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-06-25 09:17"
},
"70": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-06-25 09:17"
},
"32": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-06-25 13:03"
},
"30": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-06-25 13:43"
},
"40": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-06-25 13:07"
},
"45": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-06-25 13:19"
},
"21": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-06-25 13:30"
},
"29": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-06-25 13:43"
},
"38": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-06-25 13:44"
},
"49": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-06-25 13:52"
},
"54": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-06-25 13:52"
},
"23": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-06-25 14:39"
},
"71": {
"result": "pass",
"comment": "",
"images": [],
"at": "2026-06-25 15:12"
},
"154": {
"result": "pass",
"comment": "gamelog [TC154] x18 (2026-07-02): host ????\/??? wizard ? ????\/??????? set-ready ????? ? ??? solo 1 ?? (??? reject ?????????????????????)",
"images": [],
"at": "2026-07-02 05:38"
},
"156": {
"result": "pass",
"comment": "gamelog [TC156] x11: '???? story ?? 1\/1 ? ???????? ? ???????????????????????? (pass)' ?????",
"images": [],
"at": "2026-07-02 05:38"
},
"144": {
"result": "pass",
"comment": "gamelog [TC144] x8: '??????????? OK ??????????' ? happy path ???? ? BLOCK ???????????????????? (solo)",
"images": [],
"at": "2026-07-02 05:38"
},
"158": {
"result": "pass",
"comment": "gamelog: MG4 ???????? (mg4-round x43) ???????? mg4-warp event ??? = ????????????????? (pass)",
"images": [],
"at": "2026-07-02 05:38"
}
}