fix on testcase 1.2

This commit is contained in:
2026-06-25 16:22:36 +00:00
parent bef0133a7a
commit 66c1801f3e
36 changed files with 8869 additions and 5170 deletions
+36 -15
View File
@@ -51,9 +51,9 @@
"providerUserId": "p_1775109142385_wq7wfy1p32j",
"notes": "auto: player-coins",
"blocked": false,
"coins": 1672,
"coins": 1918,
"createdAt": "2026-04-02T05:52:21+00:00",
"updatedAt": "2026-06-25T03:57:35+00:00",
"updatedAt": "2026-06-25T16:09:47+00:00",
"daily": {
"anchorMs": 1781197200000,
"claimedDays": [
@@ -67,21 +67,21 @@
],
"lockUntilMs": 0
},
"score": 1492,
"score": 1738,
"scoreByCase": {
"1": 16,
"10": 386,
"8": 288,
"10": 446,
"8": 296,
"11": 20,
"13": 176,
"13": 186,
"14": 30,
"9": 40,
"12": 282,
"12": 380,
"15": 20,
"4": 10,
"4": 20,
"5": 32,
"7": 6,
"3": 72,
"3": 132,
"6": 44,
"2": 70
},
@@ -91,7 +91,7 @@
"a1_first_deduction": 1,
"a5_truth_hunter": 3
},
"lobbyColorThemeIndex": 6,
"lobbyColorThemeIndex": 8,
"lobbySkinToneIndex": 1
},
{
@@ -651,21 +651,21 @@
"providerUserId": "p_1782016990238_0u37bv7pxlxs",
"notes": "auto: player-coins",
"blocked": false,
"coins": 142,
"coins": 150,
"createdAt": "2026-06-21T04:43:11+00:00",
"updatedAt": "2026-06-24T09:41:43+00:00",
"updatedAt": "2026-06-25T15:49:43+00:00",
"lobbyColorThemeIndex": 8,
"lobbySkinToneIndex": 1,
"score": 112,
"score": 120,
"scoreByCase": {
"13": 56,
"10": 38,
"10": 46,
"6": 8,
"8": 10
},
"lbName": "MONE",
"achievements": {
"d1_minigame_solver": 11
"d1_minigame_solver": 12
}
},
{
@@ -884,6 +884,27 @@
"achievements": {
"d1_minigame_solver": 3
}
},
{
"id": "12943530c9eb6ad35c66feb3",
"email": "",
"displayName": "MONE",
"loginType": "guest",
"providerUserId": "p_1782376007927_lpt9m4xge9",
"notes": "auto: player-coins",
"blocked": false,
"coins": 62,
"createdAt": "2026-06-25T08:26:49+00:00",
"updatedAt": "2026-06-25T14:34:19+00:00",
"score": 62,
"scoreByCase": {
"12": 42,
"8": 20
},
"lbName": "MONE",
"achievements": {
"d1_minigame_solver": 7
}
}
]
}
+3 -3
View File
@@ -74,7 +74,7 @@
"/Game/img/ViolentCrime/Meteo-3.png"
],
"spaceShooterAsteroidExplodeFrameMs": 60,
"spaceShooterAsteroidIntervalMs": 300,
"spaceShooterAsteroidIntervalMs": 200,
"spaceShooterShipDamageOverlayUrls": [
"/Game/img/ViolentCrime/Rocket-broke-1.png",
"/Game/img/ViolentCrime/Rocket-broke-2.png",
@@ -95,9 +95,9 @@
"balloonBossPlayerBalloonFallbackUrl": "/Game/img/MegaVirus/Artboard%209.png",
"balloonBossBalloonsPerPlayer": 3,
"forcedMinigameKeys": [
"quiz",
"gauntlet",
"stack"
"stack",
"balloon_boss"
],
"testSpecialCardByMap": {},
"troublesomeForceOffer": false,
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -254,6 +254,35 @@
}
}
/* มือถือแนวนอน (จอเตี้ย) — ไม่ stack แนวตั้งแบบ ≤960px (panel จะสูงล้นจอ) → ใช้เลย์เอาต์ "แนวนอน"
ตัวละครซ้าย + แผงขวา ขนาดอิงความสูงจอ ให้พอดีไม่ล้น · ต้องอยู่หลัง max-width:960px เพื่อ override */
@media (orientation: landscape) and (max-height: 540px) {
.customize-popup-content {
flex-direction: row;
align-items: center;
justify-content: center;
gap: calc(10px * var(--cc-scale));
transform: none;
}
.customize-popup-char-col {
width: min(340px, 30vw);
min-width: 0;
height: calc(100dvh - 26px);
padding-top: calc(8px * var(--cc-scale));
}
.customize-popup-coin-wrap {
left: calc(4px * var(--cc-scale));
}
.customize-popup-panel-shell {
width: min(
calc((100dvh - 26px) * (990 / 951)),
calc(100vw - 20px - min(340px, 30vw) - calc(10px * var(--cc-scale)))
);
max-height: calc(100dvh - 26px);
flex: 0 0 auto;
}
}
.customize-popup-theme-row {
position: relative;
display: block;
+28
View File
@@ -2012,3 +2012,31 @@ body.room-lobby-page .room-lobby-top-right-actions {
height: 42px !important;
}
}
/* ===== มือถือแนวนอน (จอเตี้ย) — ย่อ/จัด UI ห้องเกม (LobbyA) ให้พอดีไม่ล้น · อิงแนวทาง Main-Lobby · ไม่แตะ PC ===== */
@media (orientation: landscape) and (max-height: 540px) {
/* ปุ่มมุมขวาล่าง (ไมค์ + แว่น/ถ้วย) — ย่อตามความสูงจอ */
.room-lobby-br-fixed { --lobby-br-btn: clamp(46px, 11vh, 64px); gap: 7px; }
/* แผงแชท — เดิม 576x525 ล้นจอแนวนอน → ย่อให้พอดีความสูงจอ */
.room-lobby-chat-peers.chat-panel {
width: min(360px, 52vw);
height: min(248px, calc(100dvh - 80px));
bottom: max(0.5rem, env(safe-area-inset-bottom));
padding: 0.9rem 1rem;
}
/* ปุ่มแชท AI (collapsed) — ย่อลงอีกบนแนวนอน */
.room-lobby-chat-ai.chat-panel-collapsed .chat-close-btn,
.room-lobby-chat-ai.chat-panel-collapsed .chat-close-btn img {
width: 62px !important; height: 58px !important;
min-width: 62px !important; min-height: 58px !important;
max-width: 62px !important; max-height: 58px !important;
}
/* ปุ่ม ready — เตี้ยลงเล็กน้อย ไม่ชนปุ่มอื่น */
.room-lobby-ready-fixed .ready-label-img { height: clamp(38px, 9.5vh, 58px); }
/* ปุ่ม exit/howto มุมซ้ายบน — อิงความสูงจอ */
.room-lobby-exit-fixed .btn-exit-icon {
width: clamp(46px, 10vh, 62px);
height: clamp(46px, 10vh, 62px);
min-width: clamp(46px, 10vh, 62px);
}
}
+375 -46
View File
@@ -68,31 +68,44 @@
(+ หนวงเลกนอยให asset แรกโหลด) safety timeout นคาง. ไมโชวใน editor embed preview */
let playLoadingHidden = false;
let playLoadingHideScheduled = false;
const PLAY_LOADING_MIN_MS = 450; /* โชว์อย่างน้อยเท่านี้ กันแว่บหาย (ไม่ทันเห็น) */
let playLoadingShownAt = (typeof performance !== 'undefined' ? performance.now() : Date.now());
function nowMsPlay() { return (typeof performance !== 'undefined' ? performance.now() : Date.now()); }
function hidePlayLoadingOverlay() {
if (playLoadingHidden) return;
const elapsed = nowMsPlay() - playLoadingShownAt;
if (elapsed < PLAY_LOADING_MIN_MS) { setTimeout(hidePlayLoadingOverlay, PLAY_LOADING_MIN_MS - elapsed); return; }
playLoadingHidden = true;
try {
const el = document.getElementById('play-loading-overlay');
if (el) { el.style.opacity = '0'; setTimeout(function () { if (el && el.parentNode) el.parentNode.removeChild(el); }, 320); }
if (el) { el.style.opacity = '0'; el.style.pointerEvents = 'none'; setTimeout(function () { if (el && el.parentNode) el.parentNode.removeChild(el); }, 340); }
} catch (_e) { /* ignore */ }
}
(function showPlayLoadingOverlay() {
if (previewMode && editorEmbedReturn) { playLoadingHidden = true; return; }
/* editor embed preview: ไม่ต้องมี loading → เอา overlay static ออก */
if (previewMode && editorEmbedReturn) {
playLoadingHidden = true;
try { const pe = document.getElementById('play-loading-overlay'); if (pe && pe.parentNode) pe.parentNode.removeChild(pe); } catch (_e) {}
return;
}
try {
if (!document.body) return;
const css = document.createElement('style');
css.textContent = ''
+ '#play-loading-overlay{position:fixed;inset:0;z-index:99990;display:flex;flex-direction:column;align-items:center;'
+ 'justify-content:center;gap:18px;background:radial-gradient(circle at 50% 42%,#10203c,#060b16 75%);transition:opacity .32s;}'
+ '#play-loading-overlay .plo-spin{width:62px;height:62px;border-radius:50%;border:6px solid rgba(120,190,255,.18);'
+ 'border-top-color:#5fd0ff;animation:ploSpin .85s linear infinite;}'
+ '#play-loading-overlay .plo-text{color:#cfe8ff;font:600 19px system-ui,"Kanit",sans-serif;letter-spacing:.5px;text-shadow:0 2px 10px rgba(0,0,0,.5);}'
+ '@keyframes ploSpin{to{transform:rotate(360deg);}}';
document.head.appendChild(css);
const ov = document.createElement('div');
ov.id = 'play-loading-overlay';
ov.innerHTML = '<div class="plo-spin"></div><div class="plo-text">กำลังโหลดเกม…</div>';
document.body.appendChild(ov);
/* overlay เป็น static ใน play.html แล้ว — ถ้าไม่มี (html เก่า cache) ค่อย inject เสริม */
if (!document.getElementById('play-loading-overlay')) {
const css = document.createElement('style');
css.textContent = ''
+ '#play-loading-overlay{position:fixed;inset:0;z-index:99990;display:flex;flex-direction:column;align-items:center;'
+ 'justify-content:center;gap:18px;background:radial-gradient(circle at 50% 42%,#10203c,#060b16 75%);transition:opacity .32s;}'
+ '#play-loading-overlay .plo-spin{width:62px;height:62px;border-radius:50%;border:6px solid rgba(120,190,255,.18);'
+ 'border-top-color:#5fd0ff;animation:ploSpin .85s linear infinite;}'
+ '#play-loading-overlay .plo-text{color:#cfe8ff;font:600 19px system-ui,"Kanit",sans-serif;letter-spacing:.5px;text-shadow:0 2px 10px rgba(0,0,0,.5);}'
+ '@keyframes ploSpin{to{transform:rotate(360deg);}}';
document.head.appendChild(css);
const ov = document.createElement('div');
ov.id = 'play-loading-overlay';
ov.innerHTML = '<div class="plo-spin"></div><div class="plo-text">กำลังโหลดเกม…</div>';
document.body.insertBefore(ov, document.body.firstChild);
}
setTimeout(hidePlayLoadingOverlay, 8000); /* safety: กันค้างถ้าโหลดนาน/ติด */
} catch (_e) { playLoadingHidden = true; }
})();
@@ -551,9 +564,12 @@
var newN = awarded.length;
var ov = document.createElement('div');
ov.id = 'inscene-evidence-reveal';
ov.style.cssText = 'position:fixed;inset:0;z-index:99999;background:rgba(5,8,18,.9);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:22px;backdrop-filter:blur(3px)';
/* scroll container + inner margin:auto → มือถือแนวตั้งที่เนื้อหาสูงเกินจอ เลื่อนถึงปุ่มได้ (เดิม justify center ไม่มี scroll → ปุ่มหลุดล่างจอ) */
ov.style.cssText = 'position:fixed;inset:0;z-index:99999;background:rgba(5,8,18,.9);display:flex;overflow-y:auto;-webkit-overflow-scrolling:touch;backdrop-filter:blur(3px);padding:16px 12px;box-sizing:border-box';
var inner = document.createElement('div');
inner.style.cssText = 'margin:auto;display:flex;flex-direction:column;align-items:center;gap:clamp(12px,2.4vh,22px);width:100%;max-width:640px';
var title = document.createElement('div');
title.style.cssText = 'font:900 30px/1.2 Kanit,system-ui,sans-serif;color:#ffe7a3;text-shadow:0 0 18px rgba(255,214,102,.5);text-align:center';
title.style.cssText = 'font:900 clamp(20px,5.5vw,30px)/1.2 Kanit,system-ui,sans-serif;color:#ffe7a3;text-shadow:0 0 18px rgba(255,214,102,.5);text-align:center';
title.textContent = newN > 1 ? ('คุณได้รับหลักฐาน ' + newN + ' ใบ!') : 'คุณได้รับหลักฐาน!';
if (res.grade) {
var gradeEl = document.createElement('div');
@@ -580,7 +596,7 @@
var img = document.createElement('img');
img.src = c.imageUrl;
img.alt = EVIDENCE_RARITY_LABEL[rar] || 'หลักฐาน';
img.style.cssText = 'display:block;width:min(51vw,390px);height:auto;border-radius:14px;box-shadow:0 0 36px ' + glow + ',0 18px 50px rgba(0,0,0,.6);cursor:zoom-in';
img.style.cssText = 'display:block;width:auto;height:auto;max-width:min(72vw,360px);max-height:52vh;border-radius:14px;box-shadow:0 0 36px ' + glow + ',0 18px 50px rgba(0,0,0,.6);cursor:zoom-in';
img.title = 'แตะเพื่อดูใหญ่';
img.addEventListener('click', function () { openEvidenceCardLightboxPlay(c.imageUrl, EVIDENCE_RARITY_LABEL[rar] || '', rar); });
img.onerror = function () { this.onerror = null; this.style.width = '200px'; this.style.height = '300px'; this.style.background = '#1b2240'; this.removeAttribute('src'); };
@@ -595,14 +611,15 @@
var btn = document.createElement('button');
btn.type = 'button';
btn.textContent = 'คุณได้หลักฐาน · เก็บเข้าแฟ้ม';
btn.style.cssText = 'cursor:pointer;border:none;border-radius:999px;padding:14px 40px;font:800 18px/1 Kanit,system-ui,sans-serif;color:#0a0e1a;background:linear-gradient(180deg,#ffd666,#f0b429);box-shadow:0 10px 30px rgba(240,180,41,.45)';
btn.style.cssText = 'cursor:pointer;border:none;border-radius:999px;padding:clamp(11px,1.8vh,14px) clamp(26px,8vw,40px);font:800 clamp(15px,4.4vw,18px)/1 Kanit,system-ui,sans-serif;color:#0a0e1a;background:linear-gradient(180deg,#ffd666,#f0b429);box-shadow:0 10px 30px rgba(240,180,41,.45);flex:0 0 auto';
btn.addEventListener('click', function () {
ov.style.transition = 'opacity .25s'; ov.style.opacity = '0';
setTimeout(finish, 220);
});
ov.appendChild(title);
ov.appendChild(row);
ov.appendChild(btn);
inner.appendChild(title);
inner.appendChild(row);
inner.appendChild(btn);
ov.appendChild(inner);
document.body.appendChild(ov);
/* กันค้าง: ถ้าไม่กดภายใน 12 วิ ให้ไปต่อเอง */
setTimeout(function () { if (!done) { try { ov.remove(); } catch (e) {} finish(); } }, 12000);
@@ -700,7 +717,8 @@
try {
socket.emit('stack-state', {
idx: stackPreviewTurnIndex,
order: stackPreviewTurnOrder.map((e) => ({ kind: e.kind, seat: e.seat, id: e.id, botId: e.botId || null })),
/* theme ต่อที่นั่ง (host เป็นเจ้าของ) → ทุกจอลงสี block ตรงกัน (เดิม non-host คำนวณสีเองจาก playTint → ไม่ตรง 2 จอ) */
order: stackPreviewTurnOrder.map((e) => ({ kind: e.kind, seat: e.seat, id: e.id, botId: e.botId || null, theme: stackSeatColorThemePlay(e.seat) || null })),
});
} catch (e) { /* ignore */ }
}
@@ -784,10 +802,16 @@
const s = Math.floor(Number(seat)) || 0;
const entry = stackPreviewTurnOrder.find(function (e) { return e && e.seat === s; });
if (!entry) return null;
/* theme ที่ host broadcast มา (stack-state) — ตรงกันทุกจอ → ใช้ก่อน (เดิมคำนวณ local จาก playTint = ไม่ตรง 2 จอ) */
const bcTheme = parsePlayLobbyThemeIndex(entry.theme);
if (bcTheme) return bcTheme;
let ent = null;
if (entry.id != null) ent = (myId != null && String(entry.id) === String(myId)) ? me : (others.get(entry.id) || others.get(String(entry.id)));
if (!ent && entry.botId) ent = others.get(entry.botId) || others.get(String(entry.botId));
return ent ? playLobbyThemeIndexForEntityPlay(ent) : null;
if (!ent) return null;
/* ใช้ server theme index ก่อน (ตรงกันทุกจอ) — fallback ไป playTint ถ้าไม่มี (เช่น บอท) */
const srvIdx = parsePlayLobbyThemeIndex(ent.lobbyThemeIndexSrv);
return srvIdx || playLobbyThemeIndexForEntityPlay(ent);
}
function pickStackBlockHeavyVisual(seat) {
@@ -828,11 +852,12 @@
return out;
}
function resolveStackBlockSpriteRec(seat, heavy) {
function resolveStackBlockSpriteRec(seat, heavy, themeOverride) {
const tryHeavy = !!heavy;
/* #4 block = theme seat (Block-Code-1..8) resolve ( turn order)
fallback ไป admin slot URLs าไม theme (เกม stack /งไมลำดบตา) */
const theme = stackSeatColorThemePlay(seat);
fallback ไป admin slot URLs าไม theme (เกม stack /งไมลำดบตา)
themeOverride = "freeze" ไวบน layer ตอนวาง re-resolve เปลยนสเม turn order เปลยน ( block วางแลวคงท+ตรงทกจอ) */
const theme = (themeOverride >= 1 && themeOverride <= 8) ? themeOverride : stackSeatColorThemePlay(seat);
if (theme) {
const tn = stackThemeNormalUrlPlay(theme);
const th = stackThemeHeavyUrlPlay(theme);
@@ -860,7 +885,7 @@
*/
function drawStackBlockSpriteOrHue(ctx, sx, sy, drawW, drawH, seat, heavy, hueFallback, opts) {
const o = opts || {};
const rec = resolveStackBlockSpriteRec(seat, heavy);
const rec = resolveStackBlockSpriteRec(seat, heavy, o.themeOverride);
const img = rec && rec.img;
if (rec && rec.ready && img && img.complete && img.naturalWidth > 0 && img.naturalHeight > 0) {
const iw = img.naturalWidth;
@@ -1676,13 +1701,17 @@
function gauntletCrownRunwayScrollStripStopZoneReachedPlay() {
const g = gauntletCrownRunwayScrollGeometryPlay();
if (!g) return false;
if (g.scrollView < g.minScrollForFinishPass - 0.01) return false;
const alignWorldX = gauntletCrownRunwayFinishAlignMapWorldXPlay();
if (alignWorldX != null) {
/* world-based: " (world X)"
(เดมม gate g.scrollView >= minScrollForFinishPass cwWorld = canvas.width/zoom
จอกวางตอง scroll มากกวากอนหย = วละครวงเลยเสนชยบนจอท resolution างก) */
const finishMapX = gauntletCrownRunwayFinishLineMapWorldXPlay();
if (finishMapX == null) return false;
return finishMapX <= alignWorldX + 1.5;
}
/* fallback (ไม่มีตัวนำให้ align) — ใช้ gate scroll + สัดส่วนจอแบบเดิม */
if (g.scrollView < g.minScrollForFinishPass - 0.01) return false;
const screenFrac = gauntletCrownRunwayFinishLineScreenFracPlay(g);
if (screenFrac == null) return false;
return screenFrac <= gauntletCrownRunwayFinishStopViewportFracPlay() + 0.008;
@@ -2734,7 +2763,11 @@
function applyPlayTintFromLobbyIndices(peerId, themeIndex, skinIndex) {
const sid = peerId != null ? String(peerId) : '';
/* server-assigned theme index ( reassign ) "block MG3"
(เด self ใชจาก localStorage แต others ใช index จาก server block ไมตรง 2 จอ) */
const tiNum = parsePlayLobbyThemeIndex(themeIndex);
if (myId != null && sid === String(myId)) {
if (tiNum) me.lobbyThemeIndexSrv = tiNum;
/* : "" (localStorage) theme-index server
(สาเหตกระพร/สล: server sync index ไมตรงสเลอก แลวท me.playTint กคร) */
const meChosen = loadPlayTintFromLobbyPlayerStorage();
@@ -2743,6 +2776,8 @@
if (tintMe) me.playTint = tintMe;
return;
}
const oEarly = others.get(peerId) || others.get(sid);
if (oEarly && tiNum) oEarly.lobbyThemeIndexSrv = tiNum;
const tint = playTintFromLobbyThemeIndices(themeIndex, skinIndex);
if (!tint) return;
const o = others.get(peerId) || others.get(sid);
@@ -7329,9 +7364,15 @@
pushEnt(myId, (me.nickname || nick || 'คุณ').trim() || 'คุณ', me.characterId || getPlayCharacterId(), me.stackPreviewHumanPts || 0);
}
stackPreviewTurnOrder.forEach(function (entry) {
if (!entry || entry.kind !== 'bot' || !entry.botId) return;
const o = others.get(entry.botId);
pushEnt(entry.botId, o ? o.nickname : entry.botId, o ? o.characterId : null, o ? (o.stackBotScore || 0) : 0);
if (!entry) return;
if (entry.kind === 'bot' && entry.botId) {
const o = others.get(entry.botId);
pushEnt(entry.botId, o ? o.nickname : entry.botId, o ? o.characterId : null, o ? (o.stackBotScore || 0) : 0);
} else if (entry.kind === 'human' && entry.id != null && String(entry.id) !== String(myId)) {
/* คนจริงคนอื่น — ใส่ในสรุปด้วย ใช้คะแนนเฉพาะตัว (เดิมมีแต่ me+bots) */
const oh = others.get(entry.id) || others.get(String(entry.id));
pushEnt(String(entry.id), oh ? (oh.nickname || String(entry.id).slice(0, 6)) : String(entry.id).slice(0, 6), oh ? oh.characterId : null, oh ? (oh.stackPreviewHumanPts || 0) : 0);
}
});
} else {
if (myId != null) {
@@ -9527,6 +9568,10 @@
if (isQuiz()) {
if (o.quizCannotTrue != null) row.qct = !!o.quizCannotTrue;
if (o.quizCannotFalse != null) row.qcf = !!o.quizCannotFalse;
}
/* MG1 (quiz) + MG4 (quiz_carry) host
(เด MG4 ไม non-host sim บอทเอง วย RNG างก = คะแนนบอทไมตรง 2 จอ) */
if (isQuiz() || isQuizCarry()) {
const qsc = playLiveQuizScores[id];
if (qsc != null) row.qsc = Math.max(0, qsc | 0);
}
@@ -9769,12 +9814,15 @@
primary.classList.toggle('is-start-phase', humansReady);
primary.classList.toggle('is-read-only', !canPress);
primary.classList.toggle('is-pressed', meReady && !humansReady);
primary.classList.toggle('is-me-ready', meReady); /* เทาเมื่อยังไม่กด / สีเมื่อกด */
primary.disabled = !canPress;
primary.setAttribute('aria-pressed', humansReady ? 'false' : (meReady ? 'true' : 'false'));
primary.title = humansReady
? (isMePlayHost() ? 'START' : 'รอโฮสต์กด START')
: (meReady ? 'ยกเลิก READY' : 'READY');
}
/* โชว์หน้าคนกด ready ใต้ปุ่ม (เหมือน MG1/2/3/5/6/7) */
updateReadyAvatarsFloat(quizCarryLobbyReadyMap, primary);
}
function beginQuizCarryEmbedPregame() {
@@ -9791,6 +9839,7 @@
function endQuizCarryEmbedPregameAndStart() {
if (!quizCarryPregameActive) return;
quizCarryPregameActive = false;
hideReadyAvatarsFloat(); /* เริ่มเกม → ซ่อนแถวหน้าตัวละคร ready */
hideQuizCarryPregameOverlay();
quizCarryPickNextQuestion();
const nowEmb = Date.now();
@@ -10708,7 +10757,11 @@
ent.quizCarryHeld = null;
const ok = held === correct;
if (ok) {
playLiveQuizScores[actorId] = (playLiveQuizScores[actorId] || 0) + QUIZ_CARRY_POINTS_PER_CORRECT;
/* : delivery ; host (broadcast fill-bot-state qsc)
non-host บคะแนนบอทเอง (RNG างก) คะแนนบอทไมตรง 2 จอ */
if (actorId === myId || isMePlayHost()) {
playLiveQuizScores[actorId] = (playLiveQuizScores[actorId] || 0) + QUIZ_CARRY_POINTS_PER_CORRECT;
}
const lim = previewMode && isQuizCarry() ? quizCarrySessionLength : 0;
const willEnd = lim > 0 && (quizCarryRoundsCompleted + 1) >= lim;
const showMapFb = !opts.silent && actorId === myId;
@@ -11825,7 +11878,8 @@
const placedW = hit.placedW != null && Number.isFinite(hit.placedW)
? Math.max(0.85, Math.min(3.2, hit.placedW))
: stackMini.initialWidthTiles;
stackMini.layers.push({ w: placedW, cx: hit.placedCx, seat: seatLay, heavy: heavyLay });
/* freeze สี (theme) ตอนวาง → block ที่วางแล้วไม่เปลี่ยนสีแม้ turn order/theme resolve เปลี่ยนทีหลัง (ตรงทุกจอ) */
stackMini.layers.push({ w: placedW, cx: hit.placedCx, seat: seatLay, heavy: heavyLay, theme: stackSeatColorThemePlay(seatLay) || null });
const nAfter = stackMini.layers.length;
maybeQueueStackTowerBgScrollStepPlay(nAfter);
const perfectFull =
@@ -11859,7 +11913,14 @@
}
if (playBotsEnabled() && previewAttribution) {
if (previewAttribution.human) {
me.stackPreviewHumanPts = (me.stackPreviewHumanPts || 0) + hit.pts;
/* เข้าคนที่วางจริง (ตาม id) — เดิมเข้า me ทุกครั้ง → คนที่ไม่ได้วางก็มีคะแนน */
const hid = previewAttribution.id != null ? String(previewAttribution.id) : null;
if (!hid || (myId != null && hid === String(myId))) {
me.stackPreviewHumanPts = (me.stackPreviewHumanPts || 0) + hit.pts;
} else {
const oh = others.get(hid) || others.get(previewAttribution.id);
if (oh) oh.stackPreviewHumanPts = (oh.stackPreviewHumanPts || 0) + hit.pts;
}
} else if (previewAttribution.botId) {
const ob = others.get(previewAttribution.botId);
if (ob) ob.stackBotScore = (ob.stackBotScore || 0) + hit.pts;
@@ -12054,6 +12115,7 @@
: 0.16 * offN * (hit.perfect ? 0.1 : 1);
const isHumanPreview = !!(previewActor && previewActor.human);
const botIdPreview = previewActor && previewActor.botId ? previewActor.botId : null;
const humanIdPreview = (previewActor && previewActor.id != null) ? String(previewActor.id) : null;
const cableTopYRel = isStackTowerMissionUiMapPlay()
? (swingAttachY - tileSize * 6)
: Math.max(0, swingAttachY - tileSize * 6);
@@ -12071,6 +12133,7 @@
tiltMax,
sloppyN: offN,
previewHumanDrop: isHumanPreview,
previewHumanId: humanIdPreview,
previewBotId: botIdPreview,
releaseRopeAng,
releaseAttachWorldX: swingXW,
@@ -12097,7 +12160,7 @@
if (!stackHumanDropGateOkPlay()) return false;
ensureStackNextDropVisual();
const hit = computeStackDropResult(stackMini);
if (!startStackFallAnimation(hit, playBotsEnabled() ? { human: true } : null)) return false;
if (!startStackFallAnimation(hit, playBotsEnabled() ? { human: true, id: myId != null ? String(myId) : null } : null)) return false;
/* broadcast ผลการวางของเรา → ทุกเครื่องวางบล็อกเดียวกัน + เลื่อนตาพร้อมกัน */
if (playBotsEnabled()) broadcastStackDropPlay(hit, getStackDropActorSeat(), !!stackMini.pendingDropHeavy, myId, false);
if (playBotsEnabled()) {
@@ -12202,6 +12265,7 @@
if (t >= 1) {
const wasHuman = !!stackFall.previewHumanDrop;
const fallBotId = stackFall.previewBotId || null;
const fallHumanId = stackFall.previewHumanId || null;
const hit = stackFall.hit;
const stableMin = STACK_STABLE_SUPPORT_RATIO_MIN;
if (hit.miss) {
@@ -12226,6 +12290,7 @@
stripH,
delta: hit.delta || 0,
previewHumanDrop: wasHuman,
previewHumanId: fallHumanId,
previewBotId: fallBotId,
actorSeat: stackFall.actorSeat,
actorHeavy: stackFall.actorHeavy,
@@ -12234,7 +12299,7 @@
} else {
let previewAttr;
if (playBotsEnabled()) {
if (wasHuman) previewAttr = { human: true };
if (wasHuman) previewAttr = { human: true, id: fallHumanId };
else if (fallBotId) previewAttr = { botId: fallBotId };
}
applyStackHitFromDrop(hit, previewAttr, { seat: stackFall.actorSeat, heavy: stackFall.actorHeavy });
@@ -12301,6 +12366,7 @@
me.tx = cx;
me.ty = cy;
others.forEach((o, id) => {
if (o) o.stackPreviewHumanPts = 0; /* reset คะแนนเฉพาะตัวของคนจริงทุกคน (กันคะแนนค้างจากรอบก่อน) */
if (!isPreviewBotId(id)) return;
o.x = cx + (Math.random() - 0.5) * 0.15;
o.y = cy + (Math.random() - 0.5) * 0.15;
@@ -12343,7 +12409,7 @@
drawStackTowerPerfectYellowGlowBehindBlockPlay(ctx, sx, sy, drawW, drawH, ntGlow, L.perfectGlowUntil);
}
}
drawStackBlockSpriteOrHue(ctx, sx, sy, drawW, drawH, seatL, heavyL, hue, {});
drawStackBlockSpriteOrHue(ctx, sx, sy, drawW, drawH, seatL, heavyL, hue, { themeOverride: L.theme });
}
if (towerHudLive && m.perfectStackX2Fx) {
const fxX2 = m.perfectStackX2Fx;
@@ -13171,12 +13237,127 @@
btn.classList.toggle('is-read-only', locked);
btn.disabled = locked;
btn.setAttribute('aria-pressed', (!allReady && meReady) ? 'true' : 'false');
/* ปุ่มเทาเมื่อยังไม่กด / สีเดิมเมื่อกดแล้ว (#1) */
btn.classList.toggle('is-me-ready', meReady);
btn.title = allReady
? (isMePlayHost() ? 'START' : 'รอโฮสต์เริ่ม')
: (meReady ? 'ยกเลิก READY' : 'READY');
updateGauntletCrownReadyAvatars(); /* โชว์หน้าคนที่กด ready (#2) */
return allReady;
}
/** วาดหน้าตัวละครจริง (tinted) ในกรอบ player-pic-frame — ใช้ใต้ปุ่ม READY (โชว์คนที่กด ready) */
function renderReadyAvatarInto(el, id) {
var info = (typeof specialQuizResolveCharTint === 'function') ? specialQuizResolveCharTint(id) : { characterId: '', tint: null };
var frame = document.createElement('img');
frame.className = 'gra-frame';
frame.alt = '';
frame.src = SPECIAL_QUIZ_AV_FRAME_SRC;
var port = document.createElement('div');
port.className = 'gra-port';
var cv = document.createElement('canvas');
cv.width = 120;
cv.height = 120;
port.appendChild(cv);
el.appendChild(frame);
el.appendChild(port);
if (!info.characterId) {
var ctx0 = cv.getContext('2d');
ctx0.fillStyle = '#5eeaff';
ctx0.beginPath();
ctx0.arc(cv.width / 2, cv.height / 2, cv.width / 2, 0, Math.PI * 2);
ctx0.fill();
return;
}
try {
if (typeof ensurePlayLobbyIdleAvatarImage === 'function') ensurePlayLobbyIdleAvatarImage(info.characterId);
if (typeof preloadPlayTintForAvatar === 'function') preloadPlayTintForAvatar(info.characterId, info.tint);
} catch (e) { /* ignore */ }
var ctx = cv.getContext('2d');
var tries = 0;
function paint() {
if (!el.isConnected) return;
tries++;
var src = null;
try { src = resolvePlayDrawCharSource(info.characterId, 'down', (typeof performance !== 'undefined' ? performance.now() : Date.now()), false, info.tint, false); } catch (e) { src = null; }
var isCanvas = !!(src && (src instanceof HTMLCanvasElement));
var sw = src ? (src.naturalWidth || src.width || 0) : 0;
var sh = src ? (src.naturalHeight || src.height || 0) : 0;
var ready = src && sw > 0 && sh > 0 && (isCanvas || tries >= 18);
if (ready) {
var scale = Math.max(cv.width / sw, cv.height / sh);
var dw = sw * scale, dh = sh * scale;
ctx.clearRect(0, 0, cv.width, cv.height);
ctx.drawImage(src, (cv.width - dw) / 2, 0, dw, dh);
/* ถ้ายังเป็นรูปดิบ (ยังไม่ tint) ลองวาดซ้ำสักพักให้ upgrade เป็น canvas ที่ย้อมสีแล้ว */
if (!isCanvas && tries < 90) requestAnimationFrame(paint);
return;
}
if (tries < 150) { requestAnimationFrame(paint); return; }
/* โหลดตัวละครไม่ทัน (เช่นตอน howto) → วาดวงกลมสีธีมผู้เล่นแทน (กันเฟรมว่าง) */
ctx.clearRect(0, 0, cv.width, cv.height);
ctx.fillStyle = (info.tint && info.tint.body) ? info.tint.body : '#5eeaff';
ctx.beginPath();
ctx.arc(cv.width / 2, cv.height / 2, cv.width / 2, 0, Math.PI * 2);
ctx.fill();
}
paint();
}
/** อัปเดตแถวหน้าตัวละครคนที่กด READY แล้ว (บอท auto-ready + คนจริงที่กด) ใต้ปุ่ม READY */
/** กล่องลอย (body) วางใต้ปุ่ม READY ด้วย getBoundingClientRect — ตรงทุก layout (legacy/tf/mg2/quiz_carry) */
function ensureReadyAvatarsFloatBox() {
var box = document.getElementById('gch-ready-avatars-float');
if (!box) {
box = document.createElement('div');
box.id = 'gch-ready-avatars-float';
document.body.appendChild(box);
}
return box;
}
/** อัปเดตแถวหน้าตัวละครคนกด READY — readyMap = map ของเกมนั้น, btnEl = ปุ่ม READY ที่กำลังโชว์ */
function updateReadyAvatarsFloat(readyMap, btnEl) {
var box = ensureReadyAvatarsFloatBox();
var visible = btnEl && (btnEl.offsetWidth > 0 || btnEl.getClientRects().length > 0);
if (!visible) { box.style.display = 'none'; return; }
var ids = [];
if (playBotsEnabled()) {
others.forEach(function (o, id) { if (o && isPreviewBotId(id)) ids.push(String(id)); });
}
(typeof quizCarryPregameHumanIds === 'function' ? quizCarryPregameHumanIds() : []).forEach(function (id) {
if (readyMap && (readyMap[String(id)] || readyMap[id])) ids.push(String(id));
});
ids = ids.filter(function (v, i) { return ids.indexOf(v) === i; });
ids.sort(function (a, b) {
var am = a === String(myId) ? 0 : 1, bm = b === String(myId) ? 0 : 1;
if (am !== bm) return am - bm;
return a < b ? -1 : a > b ? 1 : 0;
});
var key = ids.join('|');
if (box._gchReadyKey !== key) {
box._gchReadyKey = key;
box.innerHTML = '';
ids.forEach(function (id) {
var cell = document.createElement('div');
cell.className = 'gch-ready-av' + (id === String(myId) ? ' gch-ready-av--me' : '');
cell.dataset.mid = id;
box.appendChild(cell);
renderReadyAvatarInto(cell, id);
});
}
if (!ids.length) { box.style.display = 'none'; return; }
/* วางใต้ปุ่มจริง (viewport coords — fixed บน body ไม่โดน transform ของ howto) */
var r = btnEl.getBoundingClientRect();
box.style.display = 'flex';
box.style.left = Math.round(r.left + r.width / 2) + 'px';
box.style.top = Math.round(r.bottom + 6) + 'px';
}
/** crown-lobby games (MG1,2,3,5,6,7) — ใช้ gauntletCrownLobbyReadyMap + ปุ่มของ layout ปัจจุบัน */
function updateGauntletCrownReadyAvatars() {
var btn = (typeof getGauntletCrownHowtoBtnEl === 'function') ? getGauntletCrownHowtoBtnEl() : document.getElementById('btn-gch-ready');
updateReadyAvatarsFloat(gauntletCrownLobbyReadyMap, btn);
}
/** Minigame-2 mno9kb07 — HOW TO PLAY ใช้ layout mock 01-howto (canvas = mg-bg) */
function gauntletCrownHowtoUsesMg2LayoutPlay() {
return !!isGauntletCrownHeistMapPlay();
@@ -13211,6 +13392,11 @@
clearGchMg2OverlayBoundsPlay(howto);
}
gauntletCrownHowtoVisible = false;
hideReadyAvatarsFloat(); /* ปิด howto → ซ่อนแถวหน้าตัวละคร ready */
}
function hideReadyAvatarsFloat() {
var fb = document.getElementById('gch-ready-avatars-float');
if (fb) { fb.style.display = 'none'; fb._gchReadyKey = '__hidden__'; }
}
/** ภารกิจคำถาม mng8a80o — Ready Status / ปุ่ม แบบเดียวกับ Mega Virus (gauntlet-crown-lobby-sync) */
@@ -13673,7 +13859,8 @@
if (isStackTowerMissionUiMapPlay()) return stackTowerMissionPhase === 'live';
if (isJumpSurviveMissionUiMapPlay()) return jumpSurviveMissionPhase === 'live';
if (isSpaceShooterMissionUiMapPlay()) return spaceShooterMissionPhase === 'live';
if (isMegaVirusMissionShellMapPlay()) return balloonBossSessionStartMs > 0 && !balloonBossGameEnded;
/* MG7: ต้องพ้น HOW TO/นับถอยหลังจริง (pregame phase==='live') ก่อน — เดิมเช็คแค่ sessionStartMs>0 ซึ่งถูกตั้งตั้งแต่ตอนโหลดแมป (HOW TO) ทำให้ไอคอนคำถามพิเศษ arm + โผล่ระหว่าง HOW TO */
if (isMegaVirusMissionShellMapPlay()) return gauntletCrownPregamePhase === 'live' && balloonBossSessionStartMs > 0 && !balloonBossGameEnded;
if (isQuizCarry() && quizCarryEmbedMissionFlowActive()) return !quizCarryPregameActive;
return false;
}
@@ -13986,9 +14173,110 @@
const el = document.createElement('div');
el.className = 'sq-ov-av' + (isMe ? ' sq-ov-av--me' : '');
el.dataset.mid = String(id);
el.style.background = SPECIAL_QUIZ_AV_COLORS[(idx >= 0 ? idx : 0) % SPECIAL_QUIZ_AV_COLORS.length];
el.textContent = idx >= 0 ? String(idx + 1) : '?';
boxAv.appendChild(el);
renderSpecialQuizAvatarInto(el, id, idx);
}
var SPECIAL_QUIZ_AV_FRAME_SRC = BASE + '/img/special-quiz/mock/player-pic-frame.png';
/** หา characterId + tint จริงของสมาชิก special quiz (me / others / preview bot) */
function specialQuizResolveCharTint(id) {
var sid = String(id);
var characterId = '';
var tint = null;
if (sid === String(specialQuizSelfId())) {
characterId = me.characterId || getPlayCharacterId() || '';
tint = me.playTint || resolvePlayTintForPeer(sid);
} else {
var o = others.get(id) || others.get(sid);
if (o) {
characterId = o.characterId || '';
tint = o.playTint || resolvePlayTintForPeer(sid, o);
}
if (isPreviewBotId(sid)) {
if (!characterId) {
var bi = parseInt(sid.slice(PREVIEW_BOT_PREFIX.length), 10);
characterId = pickPreviewBotCharacterId(Number.isFinite(bi) ? bi : 0);
}
if (!tint) tint = playTintForPreviewBotId(sid) || resolvePlayTintForPeer(sid);
}
/* บอทใน roster คำถามพิเศษใช้ id 'sqbot-N' (ไม่ใช่ __pv_bot_N) → map ไปบอทในโลกตัวที่ N ให้ได้ตัวละคร+สีตรงกัน */
if (!characterId) {
var sqm = sid.match(/^sqbot-(\d+)$/);
if (sqm) {
var sbi = parseInt(sqm[1], 10) || 0;
var worldId = PREVIEW_BOT_PREFIX + sbi;
var wo2 = others.get(worldId);
characterId = (wo2 && wo2.characterId) || pickPreviewBotCharacterId(sbi);
tint = (wo2 && wo2.playTint) || playTintForPreviewBotId(worldId) || resolvePlayTintForPeer(worldId);
}
}
}
return { characterId: characterId, tint: tint };
}
/** avatar (gen ) player-pic-frame /
fallback เปนวงกลมส+เลขถาไม characterId/โหลด sprite ไมได */
function renderSpecialQuizAvatarInto(el, id, idx) {
var info = specialQuizResolveCharTint(id);
if (!info.characterId) {
el.classList.add('sq-ov-av--badge');
el.style.background = SPECIAL_QUIZ_AV_COLORS[(idx >= 0 ? idx : 0) % SPECIAL_QUIZ_AV_COLORS.length];
el.textContent = idx >= 0 ? String(idx + 1) : '?';
return;
}
var frame = document.createElement('img');
frame.className = 'sqav-frame';
frame.alt = '';
frame.src = SPECIAL_QUIZ_AV_FRAME_SRC;
var port = document.createElement('div');
port.className = 'sqav-portrait';
var cv = document.createElement('canvas');
cv.width = 120;
cv.height = 120;
port.appendChild(cv);
el.appendChild(frame);
el.appendChild(port);
try {
if (typeof ensurePlayLobbyIdleAvatarImage === 'function') ensurePlayLobbyIdleAvatarImage(info.characterId);
if (typeof preloadPlayTintForAvatar === 'function') preloadPlayTintForAvatar(info.characterId, info.tint);
} catch (e) { /* ignore */ }
var ctx = cv.getContext('2d');
var tries = 0;
function paint() {
if (!el.isConnected) return; /* overlay ปิดแล้ว — เลิก retry */
tries++;
var src = null;
try {
src = resolvePlayDrawCharSource(info.characterId, 'down', (typeof performance !== 'undefined' ? performance.now() : Date.now()), false, info.tint, false);
} catch (e) { src = null; }
var isCanvas = !!(src && (src instanceof HTMLCanvasElement));
var sw = src ? (src.naturalWidth || src.width || 0) : 0;
var sh = src ? (src.naturalHeight || src.height || 0) : 0;
/* รอ canvas ที่ย้อมสีแล้ว; ถ้าเกิน 18 เฟรมยังเป็นรูปดิบ (ตัวละครไม่มีเลเยอร์สี) ก็วาดรูปดิบไปก่อน */
var ready = src && sw > 0 && sh > 0 && (isCanvas || tries >= 18);
if (ready) {
var scale = Math.max(cv.width / sw, cv.height / sh);
var dw = sw * scale;
var dh = sh * scale;
ctx.clearRect(0, 0, cv.width, cv.height);
ctx.drawImage(src, (cv.width - dw) / 2, 0, dw, dh); /* cover + ชิดบน (โชว์หัว) */
return;
}
if (tries < 40) { requestAnimationFrame(paint); return; }
/* fallback: วงกลมสี+เลข */
ctx.clearRect(0, 0, cv.width, cv.height);
ctx.fillStyle = SPECIAL_QUIZ_AV_COLORS[(idx >= 0 ? idx : 0) % SPECIAL_QUIZ_AV_COLORS.length];
ctx.beginPath();
ctx.arc(cv.width / 2, cv.height / 2, cv.width / 2, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.font = '800 64px Kanit, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(idx >= 0 ? String(idx + 1) : '?', cv.width / 2, cv.height / 2);
}
paint();
}
function openSpecialQuizQuestion(q) {
@@ -19954,7 +20242,7 @@
}
});
if (tierChangedFb) relabelPreviewBotsPlay(); /* tier จาก host เปลี่ยน → อัปเดตป้าย "บอท N (tier)" ให้ตรง host */
if (quizBotScoreChanged && isQuiz()) renderPlayQuizScoreboard(playLiveQuizScores);
if (quizBotScoreChanged && (isQuiz() || isQuizCarry())) renderPlayQuizScoreboard(playLiveQuizScores);
});
/* MG3 stack (shared tower): รับผลการวางจากเจ้าของตา → เข้าคิว แล้วทยอย apply ทีละอัน (กันทิ้งดรอปตอนบล็อกกำลังร่วง = ตึก/% ต่างกัน) */
@@ -19970,7 +20258,8 @@
};
stackRemoteDropQueue.push({
hit: hit, seat: Number(d.seat) || 1, heavy: !!d.heavy,
actor: d.isBot ? { botId: String(d.actorId || '') } : { human: true },
/* เก็บ id ของคนวาง (ไม่ใช่แค่ human: true) → คะแนนเข้าคนที่วางจริง ไม่เข้า me ทุกครั้ง */
actor: d.isBot ? { botId: String(d.actorId || '') } : { human: true, id: d.actorId != null ? String(d.actorId) : null },
});
});
@@ -19991,6 +20280,7 @@
kind: e.kind, seat: e.seat,
id: e.id != null ? String(e.id) : null,
botId: e.botId != null ? String(e.botId) : null,
theme: parsePlayLobbyThemeIndex(e.theme) || null,
}));
}
if (stackPreviewTurnOrder && stackPreviewTurnOrder.length && Number.isFinite(Number(d.idx))) {
@@ -20190,6 +20480,11 @@
if (mapData && isQuizCarry() && data.quizCarryHeld !== undefined) {
o.quizCarryHeld = (data.quizCarryHeld == null ? null : data.quizCarryHeld);
}
if (mapData && isQuizCarry() && data.quizCarryScore !== undefined && (myId == null || data.id !== myId)) {
/* คะแนน carry ของคนจริงคนอื่น → ใช้ค่าจากเจ้าตัว (authoritative) ให้ทุกจอตรงกัน (ของเราใช้ค่า local) */
const qcs = Math.max(0, Number(data.quizCarryScore) || 0);
playLiveQuizScores[data.id] = qcs;
}
if (mapData && mapData.gameType === 'space_shooter') {
if (data.spaceShooterScore != null) {
const s = Number(data.spaceShooterScore);
@@ -20270,8 +20565,10 @@
}
socket.on('peer-disconnect-grace', (d) => { if (!d) return; showDisconnectGraceBanner(d.nickname || 'ผู้เล่น', d.seconds || 10); });
socket.on('peer-grace-cancel', () => { hideDisconnectGraceBanner(); });
socket.on('peer-bot-takeover', () => {
socket.on('peer-bot-takeover', (d) => {
hideDisconnectGraceBanner();
/* คนที่หลุด "ตาย/ตกรอบไปแล้ว" → ไม่ต้องเติมบอทมาเล่นแทน (เดิมเติมบอท alive = คนตายฟื้นกลับมา) */
if (d && d.eliminated) return;
/* host เติมบอท 1 ตัวเล่นแทนคนที่หลุด (ถ้ายังไม่เต็ม 6) — ใช้ rebalancePreviewBots เดิม */
try {
if (isMePlayHost()) {
@@ -23585,7 +23882,9 @@
playTint: o.playTint || resolvePlayTintForPeer(id),
score: (isQuizCarry() || quizMissionHud) ? cyberHudQuizCarryPeerScore(id)
: (stackMissionHud
? (playBotsEnabled() && isPreviewBotId(id) ? (o.stackBotScore || 0) : stackTeamPts)
? (playBotsEnabled()
? (isPreviewBotId(id) ? (o.stackBotScore || 0) : (o.stackPreviewHumanPts || 0))
: stackTeamPts)
: (isGauntlet() ? (o.gauntletScore || 0)
: (isSpaceShooter() ? (o.spaceShooterScore || 0)
: (isBalloonBoss() ? (o.balloonBossScore || 0)
@@ -23898,6 +24197,18 @@
ctx.fillStyle = '#0a0e22';
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawStackTowerScrollBgFullCanvas(canvas.width, canvas.height, zDraw);
} else if (isBalloonBoss() && mapBackgroundImg && mapBackgroundImg.complete && mapBackgroundImg.naturalWidth) {
/* MG7 (balloon_boss): BG 16:9 (1920×1080) cover (, crop , )
เด map เข world rect 4:3 (640×480) ทำใหภาพถกบ + เหลอขอบดำซาย-ขวา = "ฉากเล็ก"
วาดแยกจาก world movement / area / กลองของเกมคงเดมทกอยาง เปลยนเฉพาะพนหลงใหเตมจอ */
ctx.fillStyle = '#0a0e22';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const _bbNatW = mapBackgroundImg.naturalWidth;
const _bbNatH = mapBackgroundImg.naturalHeight;
const _bbScale = Math.max(canvas.width / _bbNatW, canvas.height / _bbNatH);
const _bbDw = _bbNatW * _bbScale;
const _bbDh = _bbNatH * _bbScale;
ctx.drawImage(mapBackgroundImg, (canvas.width - _bbDw) / 2, (canvas.height - _bbDh) / 2, _bbDw, _bbDh);
} else if (mapBackgroundImg && mapBackgroundImg.complete && mapBackgroundImg.naturalWidth) {
/* กล้องเห็นพื้นที่นอก [0,map] ได้ — ห้ามสุ่มต้นทาง drawImage เกินขอบภาพ (จะเกิดซ้ำ/เส้นตัด/ซ้อนกันที่ขอบจอ) */
ctx.fillStyle = (isSpaceShooter() || isBalloonBoss()) ? '#0a0e22' : '#1a1b26';
@@ -24437,6 +24748,20 @@
const cellSpan = Math.max(cw, ch);
const boxW = tileSize * zDraw * Math.max(cw, 1) * 1.15;
const boxH = tileSize * zDraw * Math.max(ch, 1) * 1.35;
/* MG2 กระโดด: เงาฝังในสไปรต์ลอยขึ้นตาม → วาดเงาพื้นแยกที่ตำแหน่งพื้นจริง (ไม่ lift) ให้ดูเหมือนกระโดดพ้นพื้น */
if (isGauntlet() && air > 0) {
const [gsx, gsy] = worldToScreen(cxWorld, (ay + ch) * tileSize);
const shW = boxW * 0.40;
const shH = shW * 0.32;
const liftFrac = Math.max(0, Math.min(1, liftWorldY / (tileSize * 0.9)));
ctx.save();
ctx.globalAlpha = 0.34 * (1 - 0.55 * liftFrac);
ctx.fillStyle = '#05070f';
ctx.beginPath();
ctx.ellipse(gsx, gsy - 2, shW * (1 - 0.28 * liftFrac), shH * (1 - 0.28 * liftFrac), 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
const r = Math.max(14, (tileSize * zDraw * cellSpan) / 2 - 2);
const dir = direction || 'down';
const charImg = resolvePlayDrawCharSource(characterId, dir, timeMs, isWalking, playTint, !!isMe);
@@ -24932,7 +25257,11 @@
/* payload move มาตรฐาน — quiz_carry แนบ quizCarryHeld เพื่อให้คนอื่นเห็นว่าเราถืออะไร */
function buildPlayMovePayload() {
const mv = { x: me.x, y: me.y, direction: me.direction };
if (isQuizCarry()) mv.quizCarryHeld = (me.quizCarryHeld == null ? null : me.quizCarryHeld);
if (isQuizCarry()) {
mv.quizCarryHeld = (me.quizCarryHeld == null ? null : me.quizCarryHeld);
/* แนบคะแนนของเรา → คนอื่นเห็นคะแนนคนจริงตรงกัน (เดิมคนอื่นเห็นคะแนนเรา = 0/ค้าง) */
mv.quizCarryScore = Math.max(0, Number(playLiveQuizScores[myId]) || 0);
}
return mv;
}
function tick() {
+78 -9
View File
@@ -486,9 +486,14 @@
/* ===== Preload + Loading overlay ก่อนเข้า room-lobby (กันสีตัวละครกระตุก) ===== */
var roomJoinReady = false, roomPreloadReady = false, roomLoadingHidden = false;
var ROOM_LOADING_MIN_MS = 450; /* โชว์ loading อย่างน้อยเท่านี้ กันแว่บหาย (ไม่ทันเห็น) */
var roomLoadingShownAt = Date.now();
function injectRoomLoadingOverlay() {
if (document.getElementById('room-loading-overlay')) return;
if (document.getElementById('room-loading-overlay')) {
setTimeout(hideRoomLoading, 12000); /* overlay เป็น static ใน html แล้ว — ตั้ง safety กันค้างถาวรถ้า preload แฮงค์ */
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}';
document.head.appendChild(style);
@@ -502,6 +507,8 @@
function hideRoomLoading() {
if (roomLoadingHidden) return;
var elapsed = Date.now() - roomLoadingShownAt;
if (elapsed < ROOM_LOADING_MIN_MS) { setTimeout(hideRoomLoading, ROOM_LOADING_MIN_MS - elapsed); return; }
roomLoadingHidden = true;
var ov = document.getElementById('room-loading-overlay');
if (ov) { ov.classList.add('is-hidden'); setTimeout(function () { if (ov.parentNode) ov.parentNode.removeChild(ov); }, 600); }
@@ -513,6 +520,7 @@
opts.persist=true ไมองต safety auto-hide (ใชอน navigate ออกจากหน) */
function showRoomLoadingOverlay(text, opts) {
roomLoadingHidden = false;
roomLoadingShownAt = Date.now();
var ov = document.getElementById('room-loading-overlay');
if (!ov) { injectRoomLoadingOverlay(); ov = document.getElementById('room-loading-overlay'); }
if (ov) { ov.classList.remove('is-hidden'); ov.style.opacity = ''; }
@@ -760,6 +768,10 @@
function applyLobbyTintSyncPayload(data) {
if (!data) return;
if (Array.isArray(data.lobbyBotThemes)) lobbyBotThemesFromServer = data.lobbyBotThemes;
/* จำนวนบอทอาจเพิ่ม (คนออก → เติมบอทแทน) — อัปเดตก่อน syncLobbyCaseBots ให้บอทใหม่โผล่ทันทีโดยไม่ต้อง re-join */
if (data.botSlotCount != null) {
lobbyBotSlotCount = Math.max(0, parseInt(data.botSlotCount, 10) || 0);
}
try { sessionStorage.removeItem(lobbyBotTintsStorageKey()); } catch (eClr) { /* ignore */ }
(data.peers || []).forEach(function (row) {
if (!row || !row.id) return;
@@ -1508,7 +1520,8 @@
}
const nameText = (p.nickname || id.slice(0, 6));
const isHost = id === hostId;
const nameY = labelY + (tileSize * 0.125);
/* ชื่อ+มงกุฎลงมาใกล้หัว (เดิม 0.125 ทำให้ลอยขึ้นไปทับไมค์/บับเบิล) — ไมค์/บับเบิล/ready อยู่ด้านบน */
const nameY = labelY + (tileSize * 0.5);
const voiceIconY = labelY - tileSize * 0.12;
if (p.voiceMicOn === false) {
const iconSize = Math.max(10, Math.round(tileSize * 0.35));
@@ -1525,7 +1538,8 @@
if (nwI > 0 && nhI > 0) {
var rih = 82;
var riw = 89;
var iconBottom = Math.round(nameY - nameFontSize * 1.0);
/* ready (✓) ขึ้นไปอยู่บนสุด เหนือไอคอนไมค์ — ไม่ทับมงกุฎ/ชื่อที่เลื่อนลงมา (เดิมผูกกับ nameY จึงตามชื่อลงมาทับ) */
var iconBottom = Math.round(voiceIconY - tileSize * 0.28);
var iconTop = iconBottom - rih;
var iconLeft = Math.round(cx - riw / 2);
ctx.save();
@@ -1548,7 +1562,9 @@
const textW = ctx.measureText(nameText).width;
const totalW = iconW + gap + textW;
const startX = cx - totalW / 2;
ctx.drawImage(hostIconImg, 0, 0, hostIconImg.naturalWidth, hostIconImg.naturalHeight, startX, labelY - iconH / 2, iconW, iconH);
/* มงกุฎ host จัดให้อยู่บรรทัดเดียวกับชื่อ (ตามชื่อที่ขยับลงมา) */
const hostIconY = nameY - nameFontSize * 0.72 - iconH / 2;
ctx.drawImage(hostIconImg, 0, 0, hostIconImg.naturalWidth, hostIconImg.naturalHeight, startX, hostIconY, iconW, iconH);
ctx.textAlign = 'left';
ctx.strokeText(nameText, startX + iconW + gap, nameY);
ctx.fillText(nameText, startX + iconW + gap, nameY);
@@ -4044,9 +4060,15 @@
socket.emit('troublesome-eligible');
}
/* ขอสิทธิ์ไมค์ "ครั้งเดียวพอ": เบราว์เซอร์จำสิทธิ์ต่อโดเมนอยู่แล้ว ไม่ต้องเด้งทุกครั้งที่เข้า Lobby */
var MIC_PROMPT_LS = 'justiceMicPromptHandled';
function micPromptHandled() { try { return localStorage.getItem(MIC_PROMPT_LS) === '1'; } catch (e) { return false; } }
function markMicPromptHandled() { try { localStorage.setItem(MIC_PROMPT_LS, '1'); } catch (e) {} }
function hideMicPermissionOverlay() {
var el = document.getElementById('mic-permission-overlay');
if (el) el.classList.add('hidden');
markMicPromptHandled();
tryTroublesomeLobbyGate();
}
@@ -4071,6 +4093,25 @@
var micPermissionOverlay = document.getElementById('mic-permission-overlay');
var micPermissionAllow = document.getElementById('mic-permission-allow');
var micPermissionClose = document.getElementById('mic-permission-close');
/* overlay (permission 'prompt' /)
- granted แล อนเลย (เบราวเซอรใหทธอยแล)
- denied อน (เดงไปกขอไมได ใช 🎤 ขอสทธไมค ในหองแทน)
- เคยจดการแล (localStorage) อน */
(function gateMicPermissionOverlay() {
if (!micPermissionOverlay) return;
var hideNow = function () { micPermissionOverlay.classList.add('hidden'); tryTroublesomeLobbyGate(); };
if (micPromptHandled()) hideNow();
if (navigator.permissions && navigator.permissions.query) {
try {
navigator.permissions.query({ name: 'microphone' }).then(function (st) {
if (st.state === 'granted') { markMicPromptHandled(); hideNow(); }
else if (st.state === 'denied') { hideNow(); }
/* 'prompt' & ยังไม่เคยจัดการ → ปล่อยให้เด้ง (ครั้งแรกจริง) */
st.onchange = function () { if (st.state === 'granted') { markMicPromptHandled(); micPermissionOverlay.classList.add('hidden'); } };
}).catch(function () {});
} catch (e) {}
}
})();
if (micPermissionAllow) {
micPermissionAllow.addEventListener('click', async function () {
unlockAudio();
@@ -5355,10 +5396,12 @@
'#trial-final-txt.final-txt-congrats{top:auto;bottom:21%;width:420px;filter:drop-shadow(0 2px 10px rgba(0,0,0,.7))}' +
'#trial-final-home{position:absolute;left:50%;bottom:5%;transform:translateX(-50%);width:300px;height:104px;max-width:70vw;background:url(' + R + 'btn-gohome.png) center/contain no-repeat;border:none;cursor:pointer;font-size:0;color:transparent;z-index:5;filter:drop-shadow(0 4px 14px rgba(0,0,0,.6))}' +
/* ===== หน้า2 (ชนะ): โพเดียมผู้ชนะ win-bg + ตัวละคร + มงกุฎ ===== */
'#trial-final.trial-final--podium{background-image:url(' + R + 'win-bg.png)!important}' +
/* ปักพื้นหลังไว้ขอบล่าง → พื้นเวทีติดขอบล่างจอเสมอ (เดิม center/cover crop พื้นหาย ตัวละครเลยลอย) */
'#trial-final.trial-final--podium{background-image:url(' + R + 'win-bg.png)!important;background-position:center bottom!important;background-size:cover!important}' +
'#trial-final-congrats{position:absolute;left:50%;top:5%;transform:translateX(-50%);width:560px;max-width:82vw;height:auto;z-index:4;display:none;filter:drop-shadow(0 3px 14px rgba(0,0,0,.6))}' +
'#trial-final.trial-final--podium #trial-final-congrats{display:block}' +
'#trial-final-podium{position:absolute;left:0;right:0;bottom:9%;height:58%;display:none;z-index:3;pointer-events:none}' +
/* bottom ต่ำลง → ตัวละครยืนบนพื้นเวที (เดิม 9% ลอยเหนือพื้น) */
'#trial-final-podium{position:absolute;left:0;right:0;bottom:2%;height:58%;display:none;z-index:3;pointer-events:none}' +
'#trial-final.trial-final--podium #trial-final-podium{display:block}' +
'.tf-pod-side{position:absolute;bottom:0;display:flex;align-items:flex-end;justify-content:center;gap:min(3vw,52px)}' +
'.tf-pod-left{left:2%;right:56%}' +
@@ -6616,13 +6659,13 @@
"#pick-vote-overlay .pvx-result-title{display:none;margin:2vh auto 2.2vh;height:clamp(48px,5vw,72px);width:auto}" +
"#pick-vote-overlay .pvx-bar{position:relative;width:min(560px,52vw);height:clamp(26px,3vh,38px);margin:2.2vh auto 3vh}" +
"#pick-vote-overlay .pvx-bar .frame{position:absolute;inset:0;width:100%;height:100%;object-fit:fill}" +
"#pick-vote-overlay .pvx-bar .fill{position:absolute;top:16%;left:1.5%;height:68%;width:100%;border-radius:30px;background:linear-gradient(90deg,#ff2e54,#ff6b81);box-shadow:0 0 14px rgba(255,70,100,.8)}" +
"#pick-vote-overlay .pvx-bar .fill{position:absolute;top:26%;left:1.5%;height:49%;width:100%;border-radius:30px;background:linear-gradient(90deg,#ff2e54,#ff6b81);box-shadow:0 0 14px rgba(255,70,100,.8)}" +
"#pick-vote-overlay .pvx-players{display:flex;justify-content:center;align-items:flex-start;gap:clamp(12px,3vw,50px);flex-wrap:nowrap;margin:0 auto}" +
"#pick-vote-overlay .pvx-pl{display:flex;flex-direction:column;align-items:center;gap:8px;cursor:default}" +
"#pick-vote-overlay:not(.is-result) .pvx-pl{cursor:pointer}" +
"#pick-vote-overlay .pvx-ava{position:relative;width:clamp(96px,9vw,142px);height:clamp(96px,9vw,142px);overflow:hidden;border-radius:18px}" +
"#pick-vote-overlay .pvx-ava .pvx-bg{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;z-index:1}" +
"#pick-vote-overlay .pvx-ava .pvx-char{position:absolute;left:50%;top:14%;width:120%;height:auto;transform:translateX(-50%);z-index:2}" +
"#pick-vote-overlay .pvx-ava .pvx-char{position:absolute;left:11%;top:11%;width:78%;height:80%;object-fit:cover;object-position:top center;border-radius:14%;z-index:2}" +
"#pick-vote-overlay .pvx-ava .pvx-frame{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;z-index:3}" +
"#pick-vote-overlay .pvx-ava .pvx-frame.sel{display:none}" +
"#pick-vote-overlay .pvx-pl.sel .pvx-ava .pvx-frame.norm{display:none}" +
@@ -6729,6 +6772,14 @@
clearTimeout(pickVoteState.closeTimer);
var ov = document.getElementById('pick-vote-overlay');
if (ov) ov.classList.remove('is-open');
/* คืนฉากคดีที่ซ่อนไว้ตอนเปิดโหวต (ถ้า flow ถัดไปไม่ได้พาออกไปหน้าอื่น) */
if (pickVoteState.hiddenScenes && pickVoteState.hiddenScenes.length) {
pickVoteState.hiddenScenes.forEach(function (sid2) {
var sEl = document.getElementById(sid2);
if (sEl) sEl.classList.remove('is-hidden');
});
}
pickVoteState.hiddenScenes = [];
}
socket.on('player-pick-vote-open', (data) => {
@@ -6740,6 +6791,12 @@
pickVoteState.confirmed = false;
pickVoteState.meta = meta;
clearTimeout(pickVoteState.closeTimer);
/* ปิดฉากคดี (suspect-pick) ระหว่างโหวต → โชว์โหวตบน LobbyB สะอาด (จำไว้เพื่อ restore ตอนปิด) */
pickVoteState.hiddenScenes = [];
['suspect-pick-overlay'].forEach(function (sid2) {
var sEl = document.getElementById(sid2);
if (sEl && !sEl.classList.contains('is-hidden')) { sEl.classList.add('is-hidden'); pickVoteState.hiddenScenes.push(sid2); }
});
ov.classList.remove('is-result');
document.getElementById('pvx-ic').src = pvAsset(meta.dir, meta.icon);
document.getElementById('pvx-label').textContent = meta.label;
@@ -6966,7 +7023,19 @@
/* Cutscene เรื่องราวคดี ก่อนเข้า LobbyB — โหลด LobbyB ใต้ overlay พร้อมกัน */
var cutsceneCid = (data && data.caseId != null) ? data.caseId : null;
if (cutsceneCid) {
ensureCaseMediaLoaded().then(function () { showCaseCutscene(cutsceneCid, function () {}); });
/* input () cutscene media LobbyA
(เดมขาม overlay ระหวาง ensureCaseMediaLoaded async หน LobbyA งกดได) */
showRoomLoadingOverlay('กำลังโหลด…', { persist: true });
var enterLobbyBAfterCutscene = function () {
/* "" cutscene loading LobbyB ( LobbyA )
LobbyB fetch+swap ขนานไปแล วาดอกท + loading (min-display นแวบหาย) */
showRoomLoadingOverlay('กำลังเข้า LobbyB…');
if (typeof resizeAndDraw === 'function') resizeAndDraw();
setTimeout(function () { if (typeof resizeAndDraw === 'function') resizeAndDraw(); hideRoomLoading(); }, 250);
};
ensureCaseMediaLoaded()
.then(function () { hideRoomLoading(); showCaseCutscene(cutsceneCid, enterLobbyBAfterCutscene); })
.catch(function () { hideRoomLoading(); });
} else {
/* #5 ไม่มี cutscene → โชว์หน้าโหลดเต็มจอระหว่าง fetch+swap ฉาก LobbyB (กันเห็นฉาก LobbyA เก่า) */
showRoomLoadingOverlay('กำลังโหลดฉาก LobbyB…');
+42 -6
View File
@@ -1603,6 +1603,26 @@
cursor: not-allowed;
}
.btn-gch-ready:focus-visible { outline: 2px solid #7af8ff; outline-offset: 3px; }
/* ปุ่ม READY: ยังไม่กด = ย้อมเทา, กดแล้ว = สีเดิม (กดอีกที = เทา) — ID-scoped ชนะ specificity ทุก layout + ครอบ img (mg2) */
#gauntlet-crown-howto-overlay .btn-gch-ready:not(.is-me-ready):not(.is-start-phase),
#gauntlet-crown-howto-overlay .btn-gch-ready:not(.is-me-ready):not(.is-start-phase) img { filter: grayscale(1) brightness(0.6) !important; }
#gauntlet-crown-howto-overlay .btn-gch-ready.is-me-ready,
#gauntlet-crown-howto-overlay .btn-gch-ready.is-me-ready img { filter: none !important; }
/* MG4 (quiz_carry) ปุ่ม ready แยก element — เทาเหมือนกัน */
#quiz-carry-pregame-primary:not(.is-me-ready):not(.is-start-phase),
#quiz-carry-pregame-primary:not(.is-me-ready):not(.is-start-phase) img { filter: grayscale(1) brightness(0.6) !important; }
#quiz-carry-pregame-primary.is-me-ready,
#quiz-carry-pregame-primary.is-me-ready img { filter: none !important; }
/* กล่องลอยแถวหน้าตัวละครคนกด READY — วางใต้ปุ่มด้วย JS getBoundingClientRect (ตรงทุก layout) */
#gch-ready-avatars-float{position:fixed;z-index:10260;display:none;justify-content:center;align-items:flex-start;gap:6px;flex-wrap:wrap;pointer-events:none;transform:translateX(-50%)}
/* แถวหน้าตัวละครคนที่กด READY แล้ว (โชว์ใต้ปุ่ม) */
.gch-ready-avatars { display: flex; justify-content: center; align-items: center; gap: clamp(4px, 1vw, 10px); flex-wrap: wrap; margin: clamp(6px, 1.4vw, 12px) auto 0; min-height: 1px; pointer-events: none; }
.gch-ready-avatars:empty { display: none; }
.gch-ready-av { position: relative; width: clamp(34px, 5vw, 52px); height: clamp(34px, 5vw, 52px); flex: none; }
.gch-ready-av .gra-frame { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; z-index: 2; }
.gch-ready-av .gra-port { position: absolute; left: 23%; right: 20%; top: 22%; bottom: 22%; z-index: 3; overflow: hidden; border-radius: 6px; display: flex; align-items: flex-start; justify-content: center; }
.gch-ready-av .gra-port > canvas { width: 100%; height: 100%; display: block; }
.gch-ready-av--me .gra-frame { filter: drop-shadow(0 0 5px rgba(255,233,90,0.95)); }
.gch-status {
margin: 0;
text-align: center;
@@ -4880,6 +4900,15 @@
</style>
</head>
<body>
<!-- Loading overlay แบบ static (อยู่ใน HTML ตั้งแต่แรก) — กันเห็นฉากเก่าแว่บก่อน JS รัน -->
<style>
#play-loading-overlay{position:fixed;inset:0;z-index:99990;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;background:radial-gradient(circle at 50% 42%,#10203c,#060b16 75%);transition:opacity .32s;}
#play-loading-overlay.is-hidden{opacity:0;pointer-events:none;}
#play-loading-overlay .plo-spin{width:62px;height:62px;border-radius:50%;border:6px solid rgba(120,190,255,.18);border-top-color:#5fd0ff;animation:ploSpin .85s linear infinite;}
#play-loading-overlay .plo-text{color:#cfe8ff;font:600 19px system-ui,"Kanit",sans-serif;letter-spacing:.5px;text-shadow:0 2px 10px rgba(0,0,0,.5);}
@keyframes ploSpin{to{transform:rotate(360deg);}}
</style>
<div id="play-loading-overlay"><div class="plo-spin"></div><div class="plo-text">กำลังโหลดเกม…</div></div>
<div class="game-wrap">
<div class="game-header">
<span id="room-id"></span>
@@ -5153,6 +5182,7 @@
<div class="gch-art-footer">
<p id="gauntlet-crown-howto-status" class="gch-status is-hidden" aria-live="polite"></p>
<button type="button" class="btn-gch-ready gch-legacy-only" id="btn-gch-ready" title="READY"><span class="sr-only">READY</span></button>
<div id="gch-ready-avatars" class="gch-ready-avatars" aria-hidden="true"></div>
</div>
</div>
<!-- Minigame-2 mno9kb07 — mock 01-howto (canvas = mg-bg อยู่ใต้ stack) -->
@@ -5167,6 +5197,7 @@
<button type="button" class="layer howto-btn btn btn-gch-ready" id="btn-gch-ready-mg2" title="READY">
<img id="btn-gch-ready-mg2-img" src="/Game/img/gauntlet-assets/btn-ready.png" alt="READY" width="299" height="120" decoding="async" />
</button>
<div id="gch-ready-avatars-mg2" class="gch-ready-avatars layer" aria-hidden="true"></div>
</div>
</div>
</div>
@@ -5243,18 +5274,18 @@
</div>
</div>
<style id="special-quiz-style">
#special-quiz-overlay { position: fixed; inset: 0; z-index: 1200; display: flex; align-items: center; justify-content: center; padding: 24px; }
#special-quiz-overlay { position: fixed; inset: 0; z-index: 10300; display: flex; align-items: center; justify-content: center; padding: 24px; }
#special-quiz-overlay.is-hidden { display: none; }
#special-quiz-overlay .sq-ov-backdrop { position: absolute; inset: 0; background: radial-gradient(ellipse at center, rgba(16,22,46,0.82), rgba(8,10,22,0.94)); backdrop-filter: blur(3px); }
/* ===== Special Quiz — ดีไซน์ตาม mock 09-Quiz (กรอบ neon + ตัวเลือกเต็มแถว) ===== */
#special-quiz-overlay .sq-ov-panel { position: relative; width: min(760px, 92vw, calc(94vh * 1305 / 1240)); aspect-ratio: 1305 / 1240; max-height: 94vh; overflow: auto; scrollbar-width: none; box-sizing: border-box; background: url('/Game/img/special-quiz/mock/special-quiz-tall.png') center/100% 100% no-repeat; border: none; border-radius: 0; box-shadow: none; padding: clamp(86px, 14%, 150px) 9% clamp(54px, 8%, 96px); color: #e7ecff; }
#special-quiz-overlay .sq-ov-panel { position: relative; width: min(1080px, 94vw, calc(96vh * 1305 / 1240)); aspect-ratio: 1305 / 1240; max-height: 96vh; overflow: auto; scrollbar-width: none; box-sizing: border-box; background: url('/Game/img/special-quiz/mock/special-quiz-tall.png') center/100% 100% no-repeat; border: none; border-radius: 0; box-shadow: none; padding: clamp(86px, 14%, 150px) 9% clamp(54px, 8%, 96px); color: #e7ecff; }
#special-quiz-overlay .sq-ov-panel::-webkit-scrollbar { width: 0; height: 0; }
/* หัวเก่า (ไอคอน/kicker/ข้อ) ซ่อน — ชื่อ "Special Quiz" อยู่ในรูปกรอบแล้ว */
#special-quiz-overlay .sq-ov-head { display: block; }
#special-quiz-overlay .sq-ov-icon,
#special-quiz-overlay .sq-ov-head-text { display: none; }
/* นาฬิกานับถอยหลัง — วงกลมกลางล่างของกรอบ */
#special-quiz-overlay .sq-ov-timer { position: absolute; left: 50%; bottom: 3.4%; transform: translateX(-50%); width: clamp(44px, 8%, 68px); aspect-ratio: 1; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: clamp(18px, 2.4vw, 26px); font-weight: 800; color: #eafcff; border: 3px solid rgba(94,234,255,0.85); background: rgba(8,16,30,0.85); box-shadow: 0 0 14px rgba(94,234,255,0.4); z-index: 3; }
#special-quiz-overlay .sq-ov-timer { position: absolute; left: 50%; bottom: 2.4%; transform: translateX(-50%); width: clamp(44px, 8%, 68px); aspect-ratio: 1; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: clamp(18px, 2.4vw, 26px); font-weight: 800; color: #eafcff; border: 3px solid rgba(94,234,255,0.85); background: rgba(8,16,30,0.85); box-shadow: 0 0 14px rgba(94,234,255,0.4); z-index: 3; }
#special-quiz-overlay .sq-ov-timer.sq-ov-timer--low { color: #ffd6e0; border-color: rgba(247,118,142,0.9); box-shadow: 0 0 14px rgba(247,118,142,0.5); }
#special-quiz-overlay .sq-ov-question { margin: 0 2px 22px; font-size: clamp(17px, 2.2vw, 25px); line-height: 1.4; font-weight: 800; text-align: center; color: #ffffff; text-shadow: 0 1px 6px rgba(0,0,0,0.5); }
#special-quiz-overlay .sq-ov-choices { display: flex; flex-direction: column; gap: clamp(16px, 2.8vh, 30px); }
@@ -5276,8 +5307,13 @@
/* แถว avatar ผู้เล่นที่เลือกคำตอบนี้ — เกาะขอบล่างของแถบ */
#special-quiz-overlay .sq-ov-choice-avatars { position: absolute; left: 50%; bottom: 0; transform: translate(-50%, 29%); display: flex; align-items: center; justify-content: center; gap: 3px; pointer-events: none; z-index: 4; min-height: 1px; }
#special-quiz-overlay .sq-ov-choice-avatars:empty { display: none; }
#special-quiz-overlay .sq-ov-av { width: clamp(26px, 3.4vw, 38px); height: clamp(26px, 3.4vw, 38px); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: clamp(12px, 1.5vw, 16px); font-weight: 800; color: #fff; border: 2px solid #eafcff; box-shadow: 0 2px 6px rgba(0,0,0,0.5); background: #5eeaff; }
#special-quiz-overlay .sq-ov-av--me { border-color: #ffe95a; box-shadow: 0 0 8px rgba(255,233,90,0.7); }
#special-quiz-overlay .sq-ov-av { position: relative; width: clamp(30px, 3.8vw, 42px); height: clamp(30px, 3.8vw, 42px); flex: none; display: flex; align-items: center; justify-content: center; }
#special-quiz-overlay .sq-ov-av.sq-ov-av--badge { border-radius: 50%; font-size: clamp(12px, 1.5vw, 16px); font-weight: 800; color: #fff; border: 2px solid #eafcff; box-shadow: 0 2px 6px rgba(0,0,0,0.5); }
#special-quiz-overlay .sq-ov-av .sqav-frame { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; z-index: 2; pointer-events: none; }
#special-quiz-overlay .sq-ov-av .sqav-portrait { position: absolute; left: 23%; right: 20%; top: 22%; bottom: 22%; z-index: 3; overflow: hidden; border-radius: 6px; display: flex; align-items: flex-start; justify-content: center; }
#special-quiz-overlay .sq-ov-av .sqav-portrait > canvas { width: 100%; height: 100%; display: block; }
#special-quiz-overlay .sq-ov-av--me .sqav-frame { filter: drop-shadow(0 0 5px rgba(255,233,90,0.95)); }
#special-quiz-overlay .sq-ov-av--me.sq-ov-av--badge { border-color: #ffe95a; box-shadow: 0 0 8px rgba(255,233,90,0.7); }
/* ปุ่ม "ถัดไป" — โผล่ตอนเฉลย แทนวงเวลา */
#special-quiz-overlay .sq-ov-next-btn { position: absolute; left: 50%; bottom: 0; transform: translate(-50%, 40%); width: clamp(150px, 22%, 210px); aspect-ratio: 354 / 132; border: none; background: url('/Game/img/special-quiz/mock/btn-next_1.png') center/100% 100% no-repeat; cursor: pointer; z-index: 5; transition: transform .08s, filter .12s; }
#special-quiz-overlay .sq-ov-next-btn:hover { filter: brightness(1.1); transform: translate(-50%, 40%) scale(1.04); }
@@ -5376,7 +5412,7 @@
<script src="/app-base.js?v=2"></script>
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0306"></script>
<script src="js/play.js?v=0.006250400"></script>
<script src="js/play.js?v=0.006250415"></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)
+24 -3
View File
@@ -7,9 +7,9 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Kanit:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/room-lobby.css?v=0.0176">
<link rel="stylesheet" href="css/room-lobby.css?v=0.0177">
<link rel="stylesheet" href="css/profile-popup.css?v=7">
<link rel="stylesheet" href="css/customize-popup.css?v=42">
<link rel="stylesheet" href="css/customize-popup.css?v=43">
<link rel="stylesheet" href="css/leaderboard-popup.css?v=2">
<link rel="stylesheet" href="css/testimony-overlay.css?v=18">
<link rel="stylesheet" href="css/evidence-view-overlay.css?v=13">
@@ -1108,6 +1108,15 @@
</style>
</head>
<body class="room-lobby-page">
<!-- Loading overlay แบบ static (อยู่ใน HTML ตั้งแต่แรก) — กันเห็นฉากเก่าแว่บก่อน JS รัน (โดยเฉพาะตอนเกม→LobbyB) -->
<style>
#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>
<div id="room-loading-overlay"><div id="room-loading-spinner"></div><div class="rl-txt">กำลังโหลดตัวละคร…</div></div>
<div id="room-howto-overlay" class="hidden" role="dialog" aria-modal="true" aria-labelledby="room-howto-title">
<div class="room-howto-inner">
<h2 id="room-howto-title" class="visually-hidden" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);">วิธีเล่นเบื้องต้น</h2>
@@ -1295,6 +1304,18 @@
<button type="button" class="mic-permission-btn-allow" id="mic-permission-allow" title="อนุญาต">อนุญาต</button>
</div>
</div>
<script>
/* กันเด้งซ้ำ + กันแฟลช: ถ้าผู้ใช้เคยอนุญาต/ปิดไปแล้ว ซ่อน overlay ทันทีก่อน paint
(เบราว์เซอร์จำสิทธิ์ไมค์ต่อโดเมนอยู่แล้ว ไม่ต้องเด้งทุกครั้งที่เข้า Lobby) */
(function () {
try {
if (localStorage.getItem('justiceMicPromptHandled') === '1') {
var el = document.getElementById('mic-permission-overlay');
if (el) el.classList.add('hidden');
}
} catch (e) {}
})();
</script>
<div id="quiz-game-overlay" class="is-hidden" role="status" aria-live="polite" aria-atomic="true">
<div class="quiz-hud-root">
<aside class="quiz-hud-score-col" aria-label="คะแนน">
@@ -1627,7 +1648,7 @@
<script src="js/version.js?v=0.0122"></script>
<script src="js/customize-popup.js?v=40" data-customize-triggers="" data-customize-asset-base="img/03-5-Customize"></script>
<script src="js/achievements.js?v=0.002" data-asset-base="img/03-6-Profile"></script>
<script src="js/room-lobby.js?v=0.0311"></script>
<script src="js/room-lobby.js?v=0.0323"></script>
<div class="version-tag">v —</div>
</body>
</html>
+59 -6
View File
@@ -121,6 +121,8 @@ function emitLobbyTintSync(spaceId, space) {
io.to(spaceId).emit('lobby-tint-sync', {
peers: peersTint,
lobbyBotThemes: syncLobbyBotThemeSlots(space),
/* ส่งจำนวนบอทมาด้วย — ให้ client ที่นั่งอยู่ใน LobbyB อยู่แล้ว (ไม่ได้ re-join) เห็นบอทที่มาแทนคนออกทันที */
botSlotCount: effectiveBotSlotCount(space),
});
}
@@ -1824,8 +1826,16 @@ function computeRunGradeForEvidence(space) {
const peers = [...space.peers.values()];
if (!peers.length) return 'C';
const raw = peers.map((p) => evidencePlayerRawScore(space, p, gameType));
const max = Math.max(1, ...raw);
const avgPct = raw.reduce((s, v) => s + (v / max) * 100, 0) / raw.length;
let avgPct;
if (gameType === 'balloon_boss') {
/* MG7: = (0-100) normalize
(เด normalize ทำใหคะแนนต เช 15 เฟอเปนเกรดส ไดการดชคนรายผ) */
const avgRaw = raw.reduce((s, v) => s + v, 0) / raw.length;
avgPct = Math.min(100, Math.max(0, avgRaw));
} else {
const max = Math.max(1, ...raw);
avgPct = raw.reduce((s, v) => s + (v / max) * 100, 0) / raw.length;
}
return evidenceGradeFromPct(avgPct);
} catch (e) { return 'C'; }
}
@@ -8830,6 +8840,16 @@ io.on('connection', (socket) => {
const leftPeerInfo = space.peers.get(socket.id) || {};
const leftPlayerKey = (typeof leftPeerInfo.playerKey === 'string') ? leftPeerInfo.playerKey : '';
const leftNick = (leftPeerInfo.nickname || '').trim() || 'ผู้เล่น';
/* คนที่หลุดตาย/ตกรอบไปแล้วหรือยัง (ตอนหลุด) — ใช้ตัดสินว่า bot ที่มาแทนควร spawn ใหม่ไหม (คนตายแล้วไม่ควรฟื้น) */
let leftWasEliminated = false;
try {
const md8 = (space.mapId && maps.get(space.mapId)) || space.mapData;
const gt8 = md8 && md8.gameType;
leftWasEliminated = minigamePlayerScoreSurvived(space, leftPeerInfo, gt8).survived === false;
} catch (e) { /* ignore */ }
/* สี (ธีม/สกิน) ของคนที่ออก — ใช้ให้ bot ที่มาแทนใช้สีเดียวกัน (b) */
const leftThemeIndex = parseInt(leftPeerInfo.lobbyColorThemeIndex, 10);
const leftSkinIndex = parseInt(leftPeerInfo.lobbySkinToneIndex, 10);
if (space.voiceInits) delete space.voiceInits[socket.id];
if (space.troublesomeEligible) space.troublesomeEligible.delete(socket.id);
if (space.quizSession && space.quizSession.players) delete space.quizSession.players[socket.id];
@@ -8898,9 +8918,14 @@ io.on('connection', (socket) => {
});
}
}
/* "" ( LobbyB) reconnect 10 + countdown banner
ครบเวลายกล broadcast bot-takeover ให host มบอทเลแทน */
if (space.detectiveMinigameActive && leftPlayerKey) {
/* "" 3 :
(1) ระหวงมเกม (2) ระหวาง LobbyB (3) วงเปลยนหน minigameLobbyB (detectiveMinigameActive งเป false)
งหมดใหเวลา reconnect 10 (นเปลยนหน lobbyBplaylobbyB socket หลดชวคราว)
ครบเวลายงไมกล ลบออกจาก roster คด + เตมบอทสตามคนทออก (ไม LobbyB าง "รอผู้เล่น x/y" เรมเกมตอไมได) */
const inCaseFlowForBot = space.detectiveMinigameActive
|| serverMapIsPostCaseLobbyB(space)
|| (space.joinLocked && space.caseParticipantNicknames && space.caseParticipantNicknames.size > 0);
if (inCaseFlowForBot && leftPlayerKey) {
io.to(sid).emit('peer-disconnect-grace', { id: socket.id, nickname: leftNick, seconds: DISCONNECT_GRACE_SEC });
if (!space.graceReconnect || typeof space.graceReconnect !== 'object') space.graceReconnect = {};
if (!space.graceTimers || typeof space.graceTimers !== 'object') space.graceTimers = {};
@@ -8915,7 +8940,28 @@ io.on('connection', (socket) => {
if (sp.peers) sp.peers.forEach((p) => { if (p && p.playerKey === leftPlayerKey) back = true; });
if (sp.graceReconnect) delete sp.graceReconnect[leftPlayerKey];
if (back) return;
io.to(sid).emit('peer-bot-takeover', { nickname: leftNick });
/* คนนี้กลายเป็นบอทถาวร → เอาออกจาก roster คดี เพื่อ LobbyB ไม่รอคนนี้อีก (เดิมยังนับ → ค้าง "รอผู้เล่น (x/y)") */
if (sp.caseParticipantNicknames && leftNick) {
try { sp.caseParticipantNicknames.delete(normalizeLobbyNickname(leftNick)); } catch (e) { /* ignore */ }
emitDetectiveLobbyBPresence(sid, sp);
}
/* (c) persist + (b) : 1 (: LobbyB vote)
แล pin "สีของคนที่ออก" ให slot บอทใหม บอททมาแทนสตรงคนเด + งสใหกเครองตรงก */
try {
const oldBotCount = effectiveBotSlotCount(sp);
if (oldBotCount < LOBBY_SLOT_TOTAL) {
sp.botSlotCount = oldBotCount + 1;
if (leftThemeIndex >= 1 && leftThemeIndex <= LOBBY_THEME_COUNT) {
if (!sp.lobbyBotThemeBySlot || typeof sp.lobbyBotThemeBySlot !== 'object') sp.lobbyBotThemeBySlot = {};
sp.lobbyBotThemeBySlot[oldBotCount] = {
themeIndex: leftThemeIndex,
skinToneIndex: (leftSkinIndex >= 1 && leftSkinIndex <= LOBBY_SKIN_COUNT) ? leftSkinIndex : pickLobbySkinIndexForSlot(oldBotCount),
};
}
emitLobbyTintSync(sid, sp);
}
} catch (e) { /* ignore */ }
io.to(sid).emit('peer-bot-takeover', { nickname: leftNick, eliminated: leftWasEliminated });
}, DISCONNECT_GRACE_SEC * 1000);
}
}
@@ -9251,6 +9297,12 @@ io.on('connection', (socket) => {
if (newHeld !== prevHeld) moveForceEmit = true;
p.quizCarryHeld = newHeld;
}
if (p && md && md.gameType === 'quiz_carry' && data && data.quizCarryScore !== undefined) {
/* คะแนน carry ของผู้เล่น (มนุษย์) → relay ให้ทุกเครื่องเห็นคะแนนตรงกัน */
const sc = Math.max(0, Math.floor(Number(data.quizCarryScore) || 0));
if (sc !== (p.quizCarryScore | 0)) moveForceEmit = true;
p.quizCarryScore = sc;
}
if (p) {
const prevX = Number(p.x);
const prevY = Number(p.y);
@@ -9277,6 +9329,7 @@ io.on('connection', (socket) => {
}
if (md && md.gameType === 'quiz_carry') {
out.quizCarryHeld = (p.quizCarryHeld === undefined ? null : p.quizCarryHeld);
out.quizCarryScore = Math.max(0, p.quizCarryScore | 0);
}
if (md && md.gameType === 'jump_survive') {
out.jumpSurviveEliminated = !!p.jumpSurviveEliminated;
+1 -1
View File
@@ -9,7 +9,7 @@
<link href="https://fonts.googleapis.com/css2?family=Kanit:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css?v=0.0190">
<link rel="stylesheet" href="daily-popup.css?v=8">
<link rel="stylesheet" href="../Game/css/customize-popup.css?v=42">
<link rel="stylesheet" href="../Game/css/customize-popup.css?v=43">
<link rel="stylesheet" href="../Game/css/profile-popup.css?v=7">
</head>
<body class="lobby-page">
Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

@@ -138,6 +138,20 @@
<span class="badge">เข้าหน้านี้ →</span>
</a>
<a class="card" href="special-quiz-mockup.html">
<div class="thumb" style="background-image:url('Mockup/09-Quiz.png');"></div>
<div class="name">Special Quiz · อีเวนต์พิเศษ</div>
<div class="sub">ป๊อปอัปคำถามพิเศษ · choice frame · ปุ่มถัดไป</div>
<span class="badge">เข้าหน้านี้ →</span>
</a>
<a class="card" href="silence-vote-mockup.html">
<div class="thumb" style="background-image:url('Mockup/11-1-2-ห้ามโหวต.png');"></div>
<div class="name">Silence Vote · ป๊อปอัปห้ามโหวต</div>
<div class="sub">โหวตปิดปาก/ห้ามโหวต · Popup design</div>
<span class="badge">เข้าหน้านี้ →</span>
</a>
<a class="card" href="Minigame-1/index.html" style="grid-column: 1 / -1; background: linear-gradient(135deg, #2a1450 0%, #0c1230 60%); border-color: rgba(255, 110, 220, 0.45);">
<div class="thumb" style="background-image:url('Minigame-1/Mockup/02.png'); aspect-ratio: 21/9;"></div>
<div class="name">Minigame 1 · "จริงหรือไม่" (6 หน้า)</div>
@@ -29,7 +29,7 @@
/* progress */
.bar{position:relative;width:min(560px,52vw);height:clamp(26px,3vh,38px);margin:2.2vh auto 3vh}
.bar .frame{position:absolute;inset:0;width:100%;height:100%;object-fit:fill}
.bar .fill{position:absolute;top:16%;left:1.5%;height:68%;width:62%;border-radius:30px;background:linear-gradient(90deg,#ff2e54,#ff6b81);box-shadow:0 0 14px rgba(255,70,100,.8)}
.bar .fill{position:absolute;top:26%;left:1.5%;height:49%;width:62%;border-radius:30px;background:linear-gradient(90deg,#ff2e54,#ff6b81);box-shadow:0 0 14px rgba(255,70,100,.8)}
/* players row */
.players{display:flex;justify-content:center;align-items:flex-start;gap:clamp(16px,3.4vw,56px);flex-wrap:nowrap;margin:0 auto}
@@ -40,13 +40,9 @@
.ava .frame.sel{display:none}
.pl.sel .ava .frame.norm{display:none}
.pl.sel .ava .frame.sel{display:block}
/* chibi placeholder เต็มกรอบ (จริงในเกม = ตัวละคร+สีของผู้เล่น) */
.ava .chibi{position:absolute;z-index:2;left:11%;right:11%;top:13%;bottom:6%}
.ava .chibi .hair{position:absolute;top:0;left:8%;right:8%;height:46%;border-radius:48% 48% 30% 30%}
.ava .chibi .face{position:absolute;top:24%;left:20%;right:20%;height:54%;border-radius:42% 42% 50% 50%;background:#ffe2cc}
.ava .chibi .eye{position:absolute;top:46%;width:11%;height:13%;background:#3a2a22;border-radius:50%}
.ava .chibi .eye.l{left:30%}.ava .chibi .eye.r{right:30%}
.ava .chibi .body{position:absolute;bottom:0;left:14%;right:14%;height:30%;border-radius:30% 30% 12% 12%;opacity:.92}
/* avatar ตัวละครจริง gen สีตามผู้เล่น (วาง portrait ในช่องกรอบ) */
.ava .real-av{position:absolute;z-index:2;left:11%;right:11%;top:11%;bottom:9%;overflow:hidden;border-radius:14%;display:flex;align-items:flex-start;justify-content:center}
.ava .real-av img{width:100%;height:100%;object-fit:cover;object-position:top center;display:block}
.name{font:800 clamp(15px,1.5vw,20px) Kanit;letter-spacing:.02em}
.pl.sel .name{color:#ff6b81;text-shadow:0 0 10px rgba(255,80,110,.6)}
.count{font:900 clamp(28px,3vw,40px) Kanit;line-height:1;margin-top:2px}
@@ -116,7 +112,7 @@
<img class="cta" src="cut/11-1-2-ห้ามโหวต/btn-gotovote.png" alt="ไปชี้ตัวคนร้าย">
</div>
<div class="note">Mockup · ตัวละคร+พื้นหลัง = placeholder (ในเกมจริงใช้ตัวละคร/สีของผู้เล่นจริง) · กดแท็บสลับ 3 สถานะ</div>
<div class="note">Mockup · ตัวละคร = sprite จริงในเกม gen สีตามผู้เล่น (tint สูตรเดียวกับเกม) · พื้นหลัง = placeholder · กดแท็บสลับ 3 สถานะ</div>
<script>
var A = 'cut/11-1-2-ห้ามโหวต/';
@@ -126,14 +122,15 @@
{n:'RAINY', hair:'#ff5a5a', sel:true},
{n:'MICKY', hair:'#5a86ff', sel:false},
{n:'JAME', hair:'#5ad65f', sel:false},
{n:'TONY', hair:'#b07cff', sel:false},
];
var COUNTS = { silenced:['X1','X0','X3','X1','X0'], safe:['X2','X0','X2','X1','X0'] };
var COUNTS = { silenced:['X1','X0','X3','X1','X0','X1'], safe:['X2','X0','X2','X1','X0','X1'] };
var SEL = { pick:2, silenced:2, safe:-1 };
function chibi(hair){
return '<div class="chibi"><div class="hair" style="background:'+hair+'"></div><div class="face"></div>'
+'<div class="eye l"></div><div class="eye r"></div>'
+'<div class="body" style="background:'+hair+'"></div></div>';
/* avatar ตัวละครจริง gen สีตามผู้เล่น — PNG ที่ composite+tint ไว้ล่วงหน้า (สูตรเดียวกับเกม)
ใน cut/09-Quiz/avatar-gen/sv-player-1..5.png (สีตรงกับ PLAYERS) */
function realAv(i){
return '<div class="real-av"><img src="cut/09-Quiz/avatar-gen/sv-player-'+(i+1)+'.png" alt=""></div>';
}
function buildRow(mode){
var selIdx = SEL[mode];
@@ -144,7 +141,7 @@
return '<div class="pl'+(isSel?' sel':'')+'">'
+'<div class="ava">'
+'<img class="bg" src="'+A+'player-icon-bg.png" alt="">'
+ chibi(p.hair)
+ realAv(i)
+'<img class="frame norm" src="'+A+'player-icon-frame.png" alt="">'
+'<img class="frame sel" src="'+A+'player-icon-frame-select.png" alt="">'
+'</div>'
@@ -15,7 +15,7 @@
.ov::before{content:"";position:absolute;inset:0;background:radial-gradient(ellipse at 50% 45%,rgba(10,16,34,.3),rgba(6,9,18,.8));z-index:-1}
/* ===== Q/Answer panel (09-Quiz) ===== */
.sq-panel{position:relative;width:min(760px,92vw,calc(94vh * 1305 / 1240));aspect-ratio:1305 / 1240;max-height:94vh;overflow:auto;scrollbar-width:none;box-sizing:border-box;
.sq-panel{position:relative;width:min(1080px,94vw,calc(96vh * 1305 / 1240));aspect-ratio:1305 / 1240;max-height:96vh;overflow:auto;scrollbar-width:none;box-sizing:border-box;
background:url('cut/09-Quiz/special-quiz-tall.png') center/100% 100% no-repeat;
padding:clamp(86px,14%,150px) 9% clamp(54px,8%,96px)}
.sq-panel::-webkit-scrollbar{width:0;height:0}
@@ -29,8 +29,9 @@
.sq-avs{position:absolute;left:50%;bottom:0;transform:translate(-50%,29%);display:flex;gap:4px;z-index:4}
.sq-av{position:relative;width:clamp(30px,3.6vw,42px);height:clamp(30px,3.6vw,42px);display:flex;align-items:center;justify-content:center}
.sq-av .frame{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;z-index:2}
.sq-av .ch{position:absolute;left:18%;right:18%;top:14%;bottom:10%;z-index:1;border-radius:50% 50% 40% 40%}
.sq-timer{position:absolute;left:50%;bottom:3.4%;transform:translateX(-50%);width:clamp(44px,8%,68px);aspect-ratio:1;border-radius:50%;display:flex;align-items:center;justify-content:center;font:800 clamp(18px,2.4vw,26px) Kanit;color:#eafcff;border:3px solid rgba(94,234,255,.85);background:rgba(8,16,30,.85);box-shadow:0 0 14px rgba(94,234,255,.4)}
.sq-av .ch{position:absolute;left:23%;right:20%;top:22%;bottom:22%;z-index:3;display:flex;align-items:flex-start;justify-content:center;overflow:hidden;border-radius:6px}
.sq-av .ch img.real-av{width:100%;height:100%;object-fit:cover;object-position:top center;display:block}
.sq-timer{position:absolute;left:50%;bottom:2.4%;transform:translateX(-50%);width:clamp(44px,8%,68px);aspect-ratio:1;border-radius:50%;display:flex;align-items:center;justify-content:center;font:800 clamp(18px,2.4vw,26px) Kanit;color:#eafcff;border:3px solid rgba(94,234,255,.85);background:rgba(8,16,30,.85);box-shadow:0 0 14px rgba(94,234,255,.4)}
.sq-next{position:absolute;left:50%;bottom:0;transform:translate(-50%,-12%);width:clamp(150px,22%,210px);aspect-ratio:354/132;background:url('cut/09-Quiz/btn-next_1.png') center/100% 100% no-repeat;cursor:pointer}
/* ===== Card reward (10-Game-1-result) ===== */
@@ -72,6 +73,7 @@
<div class="sq-choice">A : การเจาะระบบเข้าสู่ข้อมูลส่วนบุคคล<div class="sq-avs" id="avs-q"></div></div>
<div class="sq-choice">B : การใช้ WI-FI สาธารณะเพื่อดูข่าว</div>
<div class="sq-choice">C : การอัปเดตซอฟต์แวร์เพื่อความปลอดภัย</div>
<div class="sq-choice">D : การเปลี่ยนรหัสผ่านให้คาดเดายากเป็นประจำ</div>
</div>
<div class="sq-timer">10</div>
</div>
@@ -85,6 +87,7 @@
<div class="sq-choice correct">A : การเจาะระบบเข้าสู่ข้อมูลส่วนบุคคล<div class="sq-avs" id="avs-a"></div></div>
<div class="sq-choice">B : การใช้ WI-FI สาธารณะเพื่อดูข่าว</div>
<div class="sq-choice">C : การอัปเดตซอฟต์แวร์เพื่อความปลอดภัย</div>
<div class="sq-choice">D : การเปลี่ยนรหัสผ่านให้คาดเดายากเป็นประจำ</div>
</div>
<div class="sq-next"></div>
</div>
@@ -102,17 +105,20 @@
</div>
</div>
<div class="note">Special Quiz mockup · ตัวละคร = placeholder (ในเกมจริงใช้ตัวละครผู้เล่น) · กดแท็บสลับ 3 หน้า</div>
<div class="note">Special Quiz mockup · ตัวละคร = sprite จริงในเกม gen สีตามธีมผู้เล่น (tint สูตรเดียวกับเกม) · กดแท็บสลับ 3 หน้า</div>
<script>
var HAIR=['#e8b53a','#ff7ec8','#5a86ff','#ff5a5a','#5ad65f'];
function chibiSmall(hair){
/* ตัวละครจริงในเกม (char-1777017632279) — gen สีตามธีมผู้เล่น ด้วยสูตร tint เดียวกับเกม
(composite 8 layer: shadow/body/head/hair + stroke + face, ย้อม body=head=hair ตามธีม)
สร้างเป็น PNG ล่วงหน้าใน cut/09-Quiz/avatar-gen/player-1..5.png */
var PLAYER_AV=['player-1.png','player-2.png','player-3.png','player-4.png','player-5.png','player-6.png'];
function realAv(i){
return '<img class="frame" src="cut/09-Quiz/player-pic-frame.png" alt="">'
+'<div class="ch" style="background:'+hair+'"></div>';
+'<div class="ch"><img class="real-av" src="cut/09-Quiz/avatar-gen/'+PLAYER_AV[i]+'" alt=""></div>';
}
['avs-q','avs-a'].forEach(function(id){
var el=document.getElementById(id); if(!el) return;
el.innerHTML=HAIR.map(function(h){ return '<div class="sq-av">'+chibiSmall(h)+'</div>'; }).join('');
el.innerHTML=PLAYER_AV.map(function(_,i){ return '<div class="sq-av">'+realAv(i)+'</div>'; }).join('');
});
/* หน้ารับการ์ด: ไม่ใส่ตัวละคร placeholder — ในเกมจริงจะวางตัวละครผู้เล่นล้อมการ์ดเอง */
var tabs=document.querySelectorAll('.tabs button');
Executable → Regular
+721 -604
View File
File diff suppressed because it is too large Load Diff
+623
View File
@@ -0,0 +1,623 @@
[
{
"n": 1,
"name": "แต่งตัวไม่ได้",
"detail": "แต่งตัวยังไม่ได้ มีพี่คิวทำได้คนเดียว",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 2,
"name": "ตัวละครไม่แสดง",
"detail": "หน้าเลือกเกม Quiz battle ภาพตัวละครไม่แสดง",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 3,
"name": "ระบบ join เกมมีปัญหา",
"detail": "เมื่อเข้ามินิเกม : บางคนเล่นเกมกะบอท บางคนเล่นกะเพื่อน แต่ละคนเข้ามินิเกมแล้วเจอกันบ้าง ไม่เจอกันบ้าง",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 4,
"name": "ระบบ Host มีปัญหา - 1",
"detail": "เป็น host ในเกม ตอนที่เลือกผู้ต้องสงสัย รอบที่ 2 ระบบบอกว่าให้รอ host ไม่สามารถเลือกการ์ดและกดเริ่มสืบสวนได้",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 5,
"name": "ระบบ Host มีปัญหา - 2",
"detail": "เมื่อกดออกจากเกม Host เปลี่ยนจากพี่ต้อมเป็นพี่คิวอัติโนมัติ",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 6,
"name": "ระบบ Host มีปัญหา - 3",
"detail": "เมื่อเข้ามินิเกม ทุกคนเห็นตัวเองเป็น host หมดเลย แต่กดเริ่มเกมไม่ได้",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 7,
"name": "ระบบสีตัวละคร ยังใช้ไม่ได้",
"detail": "สีตัวละครเปลี่ยนไปเรื่อย ไม่รุ้ว่าเราสีอะไรแน่ งงไปหมด",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 8,
"name": "หน้าเข้าร่วมเกมส์",
"detail": "ห้องไหนที่เริ่มเกมส์ไปแล้ว เมื่อกดเข้าร่วมแสดง popup alert error ปรับดีไซต์ให้สวยงามกว่านี้",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 9,
"name": "เกมวิ่งขึ้นบันได ผิด flow",
"detail": "ปรับ flow ให้ถูกต้อง คือ ใครตกจอตาย แต่ถ้าขึ้นไปอยู่บนสุดแล้วจะกระโดดไม่ไปต่อเฉยๆ แต่ไม่ตาย",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 10,
"name": "เกมวิ่งขึ้นบันได",
"detail": "เวลาหมดแล้ว ให้เล่นรอบ 2 auto / รอบ 3 มี popup ให้กดเล่นต่อ",
"prio": "",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 11,
"name": "เกมยิงลูกโป่ง",
"detail": "ฉากเกมยิงลูกโป่ง (MG7 Mega Virus) เล็กไป — ต้องการให้ภาพพื้นหลัง (BG) เต็มจอ แต่การเดิน (movement) และพื้นที่เล่น (area) ต่างๆ คงเดิม",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "แก้แล้ว (deploy prod play.js?v=0.006250401): BG ของ MG7 เป็นภาพ 16:9 (1920x1080) แต่โค้ดเดิม map ภาพเข้า world rect 4:3 (640x480) ทำให้ภาพถูกบีบ + เหลือขอบดำซ้าย-ขวา = ฉากดูเล็ก. เปลี่ยนการวาด BG ของ balloon_boss เป็นแบบ cover เต็มจอ (รักษาสัดส่วน 16:9, crop ส่วนเกิน, จัดกึ่งกลาง) วาดแยกจาก world layer → กล้อง/movement/area ของเกมคงเดิมทุกอย่าง เปลี่ยนเฉพาะพื้นหลังให้เต็มจอ. ไฟล์: Game/public/js/play.js (draw loop, สาขา isBalloonBoss cover-fill)."
},
{
"n": 12,
"name": "เกมต่อตึก ผิด flow",
"detail": "ปรับ flow ให้ถูกต้อง คือ เล่นทีละคน เรียงตามคิวไปเรื่อย โดยที่ blog ตามสีผู้เล่นด้วย",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 13,
"name": "เข้าร่วมห้องไม่ได้",
"detail": "สร้างห้องส่วนตัวเเล้ว เเต่เพื่อนจอยเข้าไปไม่ได้ ทั้งที่กรอกข้อมูลครบ",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 14,
"name": "Room ID",
"detail": "เวลาจอยห้องให้กรอกเเค่ ID กับรหัสที่สร้าง",
"prio": "ต่ำ",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 15,
"name": "จอยห้องคนอื่น",
"detail": "ในกรณีที่จอยห้องคนอื่น ไปหน้าเข้าร่วม เเละกดปุ่มเข้าร่วมไปเเล้ว จากนั้นกดปุ่ม exit จะพาไปหน้าสร้างห้อง สิ่งที่ถูกต้อง มันควรมาหน้า ห้องเข้าร่วมเกม",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 16,
"name": "grid กับ ฉาก ไม่ตรงกัน",
"detail": "เกม Quiz battle เกมที่ 1 ฉากกับกริดไม่ตรงกัน ทำให้เดินทะลุ หรือเดินติดในจุดที่ไม่ควรติด",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 17,
"name": "ตัวละครวิ่งเร็วเกินไป",
"detail": "เกม Quiz battle เกมที่ 1 ตัวละครผู้เล่นเร็วเกินไป",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 18,
"name": "Oauth Social log-in error",
"detail": "ไม่สามารถ log-inด้วย facebook / Google ได้ path: /oauth-public.php",
"prio": "ต่ำ",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 19,
"name": "หน้า login",
"detail": "นโยบายความเป็นส่วนตัวกดไม่ได้",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 20,
"name": "แสดงเลข version ตรงมุมจอ",
"detail": "แสดงเลข version ตรงมุมจอขวาบนหรือขวาล่างก็ได้",
"prio": "ต่ำ",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — badge มุมขวาล่าง อ่าน ?v= อัตโนมัติ"
},
{
"n": 21,
"name": "ลดขนาดตัวละคร",
"detail": "ลดขนาดตัวละครในห้อง lobby ลงอีก 2เท่า",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 22,
"name": "เปลี่ยน ui popup ตอนพูดบนหัว",
"detail": "แก้ png seq บนหัวใหม่ เป็นแบบอื่น",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 23,
"name": "ชื่่อตัวละครทับลำโพง/ทับ bubble",
"detail": "ใน lobby ขยับชื่อลงมาอย่าให้ทับ",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 24,
"name": "UI แต่งตัว : การแจ้งเตือนสีตัวละครถูกใช้แล้ว",
"detail": "ตอนเลือกสีให้มีคำแจ้งเตือน used",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 25,
"name": "เพิ่ม ข้อความแจ้งเตือนอัพเดทกลางจอ",
"detail": "ถ้ามีการอัพเดทซีน อัพเดทสีตัวละครให้มีป็อปอัพเด้งมากลางหน้าจอ ว่ามีการอัพเดทและให้รอโหลด",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 26,
"name": "เอาเงาตัวละครออก 1 อัน",
"detail": "ตอนนี้ตัวละครมีเงา 2 ชั้น เอาออกไป 1 ชั้น",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — ปิด default-shadow overlay เหลือเงาในสไปรต์"
},
{
"n": 27,
"name": "optimize ตัวละครให้เล็กลงอีก มันโหลดช้า",
"detail": "",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 28,
"name": "ปุ่ม start ใน lobby",
"detail": "ทำให้ดูโต้ตอบหน่อย เช่นพอเดินเข้าไปใกล้ให้มัน glow ขึ้นมา",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 29,
"name": "เกมต่อตึก ผิด flow",
"detail": "สี blog กับคนเล่นยังไม่สัมพันกัน ต้องเรียงตามสี และตามคิว",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 30,
"name": "เกมต่อตึก บั๊กคนที่ 5",
"detail": "คนที่ 5 ไม่สามารถเล่นได้",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 31,
"name": "เอา mock การ์ดผู้ต้องหาเก่าออกไป",
"detail": "มันแว่บๆมา",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.006242530 (รอเทส) — ซ่อนรูปจนรูปคดีจริงโหลด + spinner"
},
{
"n": 32,
"name": "เอา popup ขออนุญาตใช้ไมค์ออกไปเลย น่ารำคาญ",
"detail": "",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 33,
"name": "หน้า howto เปลี่ยนสีปุ่ม ready ด้วยตอน กดready แล้ว",
"detail": "เปลี่ยนเป้นสีเขียวพร้อมเครื่องหมายถูกตามดีไซน์",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 34,
"name": "เกม ใช่ หรือไม่ : ตอนจบบั๊ค",
"detail": "ตอนสรุปคะแนน ยังนับเวลาอยุ่ และขึ้น gameover ทับหน้าสรุปคะแนน",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG1 ใช่/ไม่: capture mg1AnyScore ก่อนล้าง (gameover) + guard quiz-phase หลังจบ (timer)"
},
{
"n": 35,
"name": "เกมวิ่ง : กดปุ่มไม่กระโดด",
"detail": "ปุ่ม jump กดแล้วไม่กระโดด",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 36,
"name": "เกมวิ่ง : ตอนจบบั๊ค วิ่งเลยฉาก",
"detail": "ตอนจบบั๊ค วิ่งเลยฉาก (ในบางรอบ)",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG2 วิ่งเลยฉาก: walk-to-finish ค่อยๆเดินเข้าเส้น + clamp ไม่เลยเส้น"
},
{
"n": 37,
"name": "popup รับหลักฐาน ทับไมค์",
"detail": "กดไมค์ไม่ได้ ตอนรับหลักฐาน ไปดูเรื่องเลเยอร์อีกที",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 38,
"name": "หน้าส่งหลักฐาน ปุ่มส่งหลักฐานหลุดจอ (เครื่องของเนส)",
"detail": "เป้นเครื่องเนสคนเดียว",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 39,
"name": "ห้องส่งหลักฐาน : เมื่อมีคนหลุด 1 คน จะไปต่อไม่ได้เลย",
"detail": "",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — testimony: disconnect ลบ submit/ready + client ใช้ totalPlayers authoritative"
},
{
"n": 40,
"name": "จำนวนคน ให้เล่นได้ตั้งแต่ 4-6 คน",
"detail": "กรณีเลือกผู้เล่นตั้งแต่ 4 คนขึ้นไปไม่ต้องมี bot",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 41,
"name": "เกม หยิบแผ่นป้าย",
"detail": "ต้องเล่นให้ครบ 10 ข้อก่อน ค่อยจบเกม (ตอนนี้เล่นถูก 1 ข้อ จบเกมเลย)",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG4 หยิบป้าย: ใช้ map จริง (currentPlayMapId) เปิด mission flow + default 10 ข้อ"
},
{
"n": 42,
"name": "เกม กระโดด",
"detail": "ท่ากระโดดแปลก ยืนซอยเท้าทำไม",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 43,
"name": "เกม กระโดด",
"detail": "ขั้นบันไดมันถี่ไป บางทีกระโดดแล้วเบิ้ลขั้นเลย ปรับให้ระยะห่างแต่ละขั้นมันเท่ากัน",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 44,
"name": "เกมกระโดด",
"detail": "อีเวนท์พิเศษ (ทนาย/เทพความรู้) หายไป",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG5 อีเวนต์พิเศษ: เพิ่ม branch jump_survive วาง icon บน platform"
},
{
"n": 45,
"name": "เกมไม่รองรับ 4K",
"detail": "หน้ารับหลักฐาน (หลังจากเล่นเกมจบ)",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 46,
"name": "เกมยิิงอุกาบาด",
"detail": "สีตัวละครยังไม่ตรงกับที่เลือกมา",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG6 สี: ยาน Rocket-1..8 ตาม lobby theme ของผู้เล่น"
},
{
"n": 47,
"name": "เกมยิิงอุกาบาด",
"detail": "อุกาบาดให้พุ่งเร็วกว่านี้ และลูกเล็กกว่านี้เกมง่ายไป",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG6: อุกาบาตเร็วขึ้น (+~50%) + เล็กลง (48->34/11->8)"
},
{
"n": 48,
"name": "เพิ่ม popup เมื่อเกมค้าง เล่นต้่อไม่ได้ ให้มีปุ่มกดออกไปหน้าแรกสุด",
"detail": "สาเหตุเกิดจากมีคนหลุดระหว่างรอ host กด",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 49,
"name": "กรณีผู้เล่นหลุดออกจากเกม",
"detail": "ถ้ามีคนหลุด ให้กลายเป้นบอทแทน",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 50,
"name": "ผู้เล่น 6 คน: บางเกมเล่นได้แค่ 5 (คนที่ 6 หายจากสรุป)",
"detail": "สรุปคะแนนฝั่ง client slice(0,5) ตัดคนที่ 6 — แก้เป็น slice(0,6) ทุกมินิเกม (MG1/3/4/5/6/7)",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส)"
},
{
"n": 51,
"name": "ผู้เล่น 6 คน: คะแนนตอนจบไม่ตรงกันแต่ละจอ",
"detail": "MG1 merge คะแนน server / MG6,7 merge คะแนน host -> ครบ 6 ตรงกัน (MG3 ยัง local)",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส)"
},
{
"n": 52,
"name": "หน้าไต่สวน/สรุปหลักฐาน: กรอบปลิ้นตอน 6 คน",
"detail": "member row grid repeat(5)->repeat(6) ใน testimony-overlay.css (คนที่6 ตกแถว2)",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.006242530 (รอเทส)"
},
{
"n": 53,
"name": "เพิ่มหน้าโหลด (loading) ตอนเข้าเกม",
"detail": "overlay spinner กันเห็นจอเปล่า/asset แว่บ ซ่อนเมื่อ draw เฟรมแรก + safety 8s",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.006242530 (รอเทส)"
},
{
"n": 54,
"name": "host โหลดนานมากตอนเข้าเกม",
"detail": "optimize asset/char (เกี่ยว row 28) — loading overlay ช่วยซ่อน แต่ยังโหลดช้าจริง",
"prio": "สูง",
"status": "เปิด",
"updated": "2026-06-25",
"note": "loading overlay บังอาการแล้ว · root = canvas tint ภาพตัวละครใหญ่ (source ไม่อยู่ใน repo) → ต้อง optimize ภาพแยก"
},
{
"n": 55,
"name": "หลังออกจาก LobbyA ไม่ให้ใครเข้า join",
"detail": "server มี joinLocked (set ตอน LobbyA->case ~8656, check ~7585 whitelist nickname) — ต้อง repro ว่าใครเข้าได้ยังไง (room list ยังโชว์? ชื่อซ้ำ whitelist?)",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 — กรองห้อง joinLocked ออกจาก /api/spaces list (ห้องเริ่มคดีแล้วหาย) + server reject อยู่แล้ว"
},
{
"n": 56,
"name": "ตอนกลับ LobbyB โชว์รูปแรกก่อนแล้วค่อยถูก",
"detail": "เหมือน #1 การ์ด — ต้องใส่ loading state ตอน return LobbyB ด้วย (มี room-loading-overlay อยู่แต่ซ่อนเร็วไป)",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.0308 — การ์ดมี spinner (#1) + room-loading overlay ใส่ % + รอ char preload เสร็จก่อนซ่อน + safety 12s"
},
{
"n": 57,
"name": "เล่นมินิเกมเกิน 3 ครั้งต่อผู้ต้องสงสัย (+การ์ดพิเศษโผล่รอบที่4)",
"detail": "server cap suspect-pick-start (max หลักฐาน>=3 reject) + award cap 3 + client canInvestigateSuspect<3 (เดิม stub return true). UI messaging มีอยู่แล้ว",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.0309/server (รอเทส)"
},
{
"n": 58,
"name": "ปุ่ม \"เล่นเกมต่อ\" ไม่รู้ว่าค้างหรือรอคนอื่น",
"detail": "text \"รอผู้เล่นอื่น (X/Y)\" ถูก set อยู่แล้วแต่ CSS font-size:0 ซ่อน → เพิ่ม .is-waiting CSS โชว์ข้อความ+spinner แทนรูปปุ่ม",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.0309/server (รอเทส)"
},
{
"n": 59,
"name": "loading ไม่คลุม scene transition (เห็นฉากเก่าก่อนเปลี่ยน)",
"detail": "LobbyA->B (in-page swap) โชว์ room-loading ระหว่าง fetch+swap · LobbyB->เกม โชว์ overlay ก่อน location.href · play.html มี overlay รับช่วง",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.0309/server (รอเทส)"
},
{
"n": 60,
"name": "ทำหน้า testcase บนเซิร์ฟเวอร์",
"detail": "srv1361159.hstgr.cloud/testcase — bug list + filter/search/นับสถานะ generate จาก xlsx",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.0309/server (รอเทส)"
},
{
"n": 61,
"name": "MG3 สี block ไม่ตรง user (เกม+admin)",
"detail": "PNG Block-Code-1..8 deploy ครบแล้ว · ต้อง wire สี block = lobby theme ของผู้วาง (seat=theme refactor, ต้องเทส 2 จอ) + admin 8-slot config",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.006250200 — block สีตาม theme ผู้เล่นที่ seat (Block-Code-1..8) ตอนวาด ไม่แตะ turn/sync"
},
{
"n": 62,
"name": "MG1/MG4 user/bot ติดกัน (เดินไม่ผ่าน)",
"detail": "เพิ่ม anti-stuck: ดันชนเพื่อน → เขย่า (shake) + ปลดล็อกเดินผ่านกันชั่วคราว คงกำแพง/area/quiz-lock; ทั้งผู้เล่นและบอท",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.006250100 (รอเทส)"
},
{
"n": 63,
"name": "LobbyA มือถือ กดเดินไม่ได้",
"detail": "lobby canvas มีแค่ dblclick (ไม่มี touch) → มือถือแตะเดินไม่ได้. แก้: เพิ่ม tap-to-walk (เฉพาะ touch, PC ยัง dblclick) + DPI-safe coord + touch-action:none + zoom-out default บนจอ<600px (PC ย่อส่วน)",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 room-lobby.js?v=0.0310 (รอเทสมือถือ) · PC ไม่กระทบ"
},
{
"n": 64,
"name": "มือถือ: ปุ่มแชท AI ใหญ่ไป + ปุ่ม F (host) ไม่มี",
"detail": "AI chat collapsed btn 213x201px → ใหญ่บนมือถือ. + การกด F (interact: host เลือกระดับ/คดี, ห้องแต่งตัว) ไม่มีบนมือถือ",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 (css 0.0176/js 0.0311): media<600px ย่อปุ่ม AI (92px) + ปุ่มลอย \"โต้ตอบ(F)\" บน touch-primary device (dispatch synthetic KeyF). PC ไม่เห็น"
},
{
"n": 65,
"name": "อีเวนต์พิเศษ (ทนาย/เทพ) = 1 ใน 3 (ตามดีไซน์)",
"detail": "ดีไซน์: 3 เกม/LobbyB เล่นได้เกมละ 3 ครั้ง · อีเวนต์พิเศษ 1/3 ต่อครั้ง (1 ใบ) · Test Mode บังคับ. revert การแก้ \">1 ต่อรอบ\" กลับเป็น 1 ใบ/ครั้งที่ติด",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ revert 2026-06-25 — กลับเป็น 1/3 (1 ใบ/ครั้งที่ติด) ตามดีไซน์ · ยืนยันโครงสร้าง 3เกม×3ครั้ง + disruptor toggle (>4คน/Test Mode) ถูกต้อง"
},
{
"n": 66,
"name": "host หลุด → ย้าย host ให้คนอยู่นานสุด",
"detail": "pickNextHostPeer เลือก spawnJoinOrder ต่ำสุด (เสถียรข้าม reconnect) แทน Map-first + host จริง reclaim ได้",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 (server) — Part 1 host migration"
},
{
"n": 67,
"name": "คนหลุดกลางเกม → 10 วิ reconnect + บอทเล่นแทน",
"detail": "peer-disconnect-grace แบนเนอร์นับ 10 วิ · กลับทัน=cancel เล่นต่อ · ครบเวลา=peer-bot-takeover host เติมบอท 1 ตัว (rebalance, cap≤6). บอทไม่สืบทอดตำแหน่ง/คะแนน",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 (server+play.js v0.006250400) — Part 2 แบบ A · seamless(host คุม entity เดิม)=ค้าง(เสี่ยง)"
},
{
"n": 68,
"name": "ห้องแต่งตัว (customize) มือถือแนวนอนใช้ไม่ได้",
"detail": "@media max-width:960px stack แนวตั้ง → panel สูงตาม aspect ล้นจอแนวนอน. แก้: เพิ่ม media (landscape + max-height:540px) ใช้เลย์เอาต์แนวนอน + ขนาดอิงความสูงจอ",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 customize-popup.css?v=43 (Main-Lobby + room-lobby) — รอเทสมือถือแนวนอน"
},
{
"n": 69,
"name": "UI ห้องเกม (LobbyA) มือถือแนวนอน — ย่อให้พอดี",
"detail": "room-lobby ไม่มี landscape media query → chat panel(576x525), ปุ่ม voice/แว่น/ถ้วย(80px), AI chat ล้น/ใหญ่บนแนวนอน. เพิ่ม @media(landscape+max-height:540px) ย่อตามความสูงจอ (อิง Main-Lobby pattern)",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 room-lobby.css?v=0.0177 — chat/BR buttons/AI/ready/exit · overlay หลักฐาน-อันดับ (inline/JS) ยังต้องทำแยก"
}
]
+650
View File
@@ -0,0 +1,650 @@
[
{
"n": 1,
"name": "แต่งตัวไม่ได้",
"detail": "แต่งตัวยังไม่ได้ มีพี่คิวทำได้คนเดียว",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 2,
"name": "ตัวละครไม่แสดง",
"detail": "หน้าเลือกเกม Quiz battle ภาพตัวละครไม่แสดง",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 3,
"name": "ระบบ join เกมมีปัญหา",
"detail": "เมื่อเข้ามินิเกม : บางคนเล่นเกมกะบอท บางคนเล่นกะเพื่อน แต่ละคนเข้ามินิเกมแล้วเจอกันบ้าง ไม่เจอกันบ้าง",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 4,
"name": "ระบบ Host มีปัญหา - 1",
"detail": "เป็น host ในเกม ตอนที่เลือกผู้ต้องสงสัย รอบที่ 2 ระบบบอกว่าให้รอ host ไม่สามารถเลือกการ์ดและกดเริ่มสืบสวนได้",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 5,
"name": "ระบบ Host มีปัญหา - 2",
"detail": "เมื่อกดออกจากเกม Host เปลี่ยนจากพี่ต้อมเป็นพี่คิวอัติโนมัติ",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 6,
"name": "ระบบ Host มีปัญหา - 3",
"detail": "เมื่อเข้ามินิเกม ทุกคนเห็นตัวเองเป็น host หมดเลย แต่กดเริ่มเกมไม่ได้",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 7,
"name": "ระบบสีตัวละคร ยังใช้ไม่ได้",
"detail": "สีตัวละครเปลี่ยนไปเรื่อย ไม่รุ้ว่าเราสีอะไรแน่ งงไปหมด",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 8,
"name": "หน้าเข้าร่วมเกมส์",
"detail": "ห้องไหนที่เริ่มเกมส์ไปแล้ว เมื่อกดเข้าร่วมแสดง popup alert error ปรับดีไซต์ให้สวยงามกว่านี้",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 9,
"name": "เกมวิ่งขึ้นบันได ผิด flow",
"detail": "ปรับ flow ให้ถูกต้อง คือ ใครตกจอตาย แต่ถ้าขึ้นไปอยู่บนสุดแล้วจะกระโดดไม่ไปต่อเฉยๆ แต่ไม่ตาย",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 10,
"name": "เกมวิ่งขึ้นบันได",
"detail": "เวลาหมดแล้ว ให้เล่นรอบ 2 auto / รอบ 3 มี popup ให้กดเล่นต่อ",
"prio": "",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 11,
"name": "เกมยิงลูกโป่ง",
"detail": "ฉากเกมยิงลูกโป่ง (MG7 Mega Virus) เล็กไป — ต้องการให้ภาพพื้นหลัง (BG) เต็มจอ แต่การเดิน (movement) และพื้นที่เล่น (area) ต่างๆ คงเดิม",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "แก้แล้ว (deploy prod play.js?v=0.006250401): BG ของ MG7 เป็นภาพ 16:9 (1920x1080) แต่โค้ดเดิม map ภาพเข้า world rect 4:3 (640x480) ทำให้ภาพถูกบีบ + เหลือขอบดำซ้าย-ขวา = ฉากดูเล็ก. เปลี่ยนการวาด BG ของ balloon_boss เป็นแบบ cover เต็มจอ (รักษาสัดส่วน 16:9, crop ส่วนเกิน, จัดกึ่งกลาง) วาดแยกจาก world layer → กล้อง/movement/area ของเกมคงเดิมทุกอย่าง เปลี่ยนเฉพาะพื้นหลังให้เต็มจอ. ไฟล์: Game/public/js/play.js (draw loop, สาขา isBalloonBoss cover-fill)."
},
{
"n": 12,
"name": "เกมต่อตึก ผิด flow",
"detail": "ปรับ flow ให้ถูกต้อง คือ เล่นทีละคน เรียงตามคิวไปเรื่อย โดยที่ blog ตามสีผู้เล่นด้วย",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 13,
"name": "เข้าร่วมห้องไม่ได้",
"detail": "สร้างห้องส่วนตัวเเล้ว เเต่เพื่อนจอยเข้าไปไม่ได้ ทั้งที่กรอกข้อมูลครบ",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 14,
"name": "Room ID",
"detail": "เวลาจอยห้องให้กรอกเเค่ ID กับรหัสที่สร้าง",
"prio": "ต่ำ",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 15,
"name": "จอยห้องคนอื่น",
"detail": "ในกรณีที่จอยห้องคนอื่น ไปหน้าเข้าร่วม เเละกดปุ่มเข้าร่วมไปเเล้ว จากนั้นกดปุ่ม exit จะพาไปหน้าสร้างห้อง สิ่งที่ถูกต้อง มันควรมาหน้า ห้องเข้าร่วมเกม",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 16,
"name": "grid กับ ฉาก ไม่ตรงกัน",
"detail": "เกม Quiz battle เกมที่ 1 ฉากกับกริดไม่ตรงกัน ทำให้เดินทะลุ หรือเดินติดในจุดที่ไม่ควรติด",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 17,
"name": "ตัวละครวิ่งเร็วเกินไป",
"detail": "เกม Quiz battle เกมที่ 1 ตัวละครผู้เล่นเร็วเกินไป",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 18,
"name": "Oauth Social log-in error",
"detail": "ไม่สามารถ log-inด้วย facebook / Google ได้ path: /oauth-public.php",
"prio": "ต่ำ",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 19,
"name": "หน้า login",
"detail": "นโยบายความเป็นส่วนตัวกดไม่ได้",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 20,
"name": "แสดงเลข version ตรงมุมจอ",
"detail": "แสดงเลข version ตรงมุมจอขวาบนหรือขวาล่างก็ได้",
"prio": "ต่ำ",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — badge มุมขวาล่าง อ่าน ?v= อัตโนมัติ"
},
{
"n": 21,
"name": "ลดขนาดตัวละคร",
"detail": "ลดขนาดตัวละครในห้อง lobby ลงอีก 2เท่า",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 22,
"name": "เปลี่ยน ui popup ตอนพูดบนหัว",
"detail": "แก้ png seq บนหัวใหม่ เป็นแบบอื่น",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 23,
"name": "ชื่่อตัวละครทับลำโพง/ทับ bubble",
"detail": "ใน lobby ขยับชื่อลงมาอย่าให้ทับ",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 24,
"name": "UI แต่งตัว : การแจ้งเตือนสีตัวละครถูกใช้แล้ว",
"detail": "ตอนเลือกสีให้มีคำแจ้งเตือน used",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 25,
"name": "เพิ่ม ข้อความแจ้งเตือนอัพเดทกลางจอ",
"detail": "ถ้ามีการอัพเดทซีน อัพเดทสีตัวละครให้มีป็อปอัพเด้งมากลางหน้าจอ ว่ามีการอัพเดทและให้รอโหลด",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 26,
"name": "เอาเงาตัวละครออก 1 อัน",
"detail": "ตอนนี้ตัวละครมีเงา 2 ชั้น เอาออกไป 1 ชั้น",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — ปิด default-shadow overlay เหลือเงาในสไปรต์"
},
{
"n": 27,
"name": "optimize ตัวละครให้เล็กลงอีก มันโหลดช้า",
"detail": "",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 28,
"name": "ปุ่ม start ใน lobby",
"detail": "ทำให้ดูโต้ตอบหน่อย เช่นพอเดินเข้าไปใกล้ให้มัน glow ขึ้นมา",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 29,
"name": "เกมต่อตึก ผิด flow",
"detail": "สี blog กับคนเล่นยังไม่สัมพันกัน ต้องเรียงตามสี และตามคิว",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 30,
"name": "เกมต่อตึก บั๊กคนที่ 5",
"detail": "คนที่ 5 ไม่สามารถเล่นได้",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 31,
"name": "เอา mock การ์ดผู้ต้องหาเก่าออกไป",
"detail": "มันแว่บๆมา",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.006242530 (รอเทส) — ซ่อนรูปจนรูปคดีจริงโหลด + spinner"
},
{
"n": 32,
"name": "เอา popup ขออนุญาตใช้ไมค์ออกไปเลย น่ารำคาญ",
"detail": "",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 33,
"name": "หน้า howto เปลี่ยนสีปุ่ม ready ด้วยตอน กดready แล้ว",
"detail": "เปลี่ยนเป้นสีเขียวพร้อมเครื่องหมายถูกตามดีไซน์",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 34,
"name": "เกม ใช่ หรือไม่ : ตอนจบบั๊ค",
"detail": "ตอนสรุปคะแนน ยังนับเวลาอยุ่ และขึ้น gameover ทับหน้าสรุปคะแนน",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG1 ใช่/ไม่: capture mg1AnyScore ก่อนล้าง (gameover) + guard quiz-phase หลังจบ (timer)"
},
{
"n": 35,
"name": "เกมวิ่ง : กดปุ่มไม่กระโดด",
"detail": "ปุ่ม jump กดแล้วไม่กระโดด",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 36,
"name": "เกมวิ่ง : ตอนจบบั๊ค วิ่งเลยฉาก",
"detail": "ตอนจบบั๊ค วิ่งเลยฉาก (ในบางรอบ)",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG2 วิ่งเลยฉาก: walk-to-finish ค่อยๆเดินเข้าเส้น + clamp ไม่เลยเส้น"
},
{
"n": 37,
"name": "popup รับหลักฐาน ทับไมค์",
"detail": "กดไมค์ไม่ได้ ตอนรับหลักฐาน ไปดูเรื่องเลเยอร์อีกที",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 38,
"name": "หน้าส่งหลักฐาน ปุ่มส่งหลักฐานหลุดจอ (เครื่องของเนส)",
"detail": "เป้นเครื่องเนสคนเดียว",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 39,
"name": "ห้องส่งหลักฐาน : เมื่อมีคนหลุด 1 คน จะไปต่อไม่ได้เลย",
"detail": "",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — testimony: disconnect ลบ submit/ready + client ใช้ totalPlayers authoritative"
},
{
"n": 40,
"name": "จำนวนคน ให้เล่นได้ตั้งแต่ 4-6 คน",
"detail": "กรณีเลือกผู้เล่นตั้งแต่ 4 คนขึ้นไปไม่ต้องมี bot",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 41,
"name": "เกม หยิบแผ่นป้าย",
"detail": "ต้องเล่นให้ครบ 10 ข้อก่อน ค่อยจบเกม (ตอนนี้เล่นถูก 1 ข้อ จบเกมเลย)",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG4 หยิบป้าย: ใช้ map จริง (currentPlayMapId) เปิด mission flow + default 10 ข้อ"
},
{
"n": 42,
"name": "เกม กระโดด",
"detail": "ท่ากระโดดแปลก ยืนซอยเท้าทำไม",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 43,
"name": "เกม กระโดด",
"detail": "ขั้นบันไดมันถี่ไป บางทีกระโดดแล้วเบิ้ลขั้นเลย ปรับให้ระยะห่างแต่ละขั้นมันเท่ากัน",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 44,
"name": "เกมกระโดด",
"detail": "อีเวนท์พิเศษ (ทนาย/เทพความรู้) หายไป",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG5 อีเวนต์พิเศษ: เพิ่ม branch jump_survive วาง icon บน platform"
},
{
"n": 45,
"name": "เกมไม่รองรับ 4K",
"detail": "หน้ารับหลักฐาน (หลังจากเล่นเกมจบ)",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 46,
"name": "เกมยิิงอุกาบาด",
"detail": "สีตัวละครยังไม่ตรงกับที่เลือกมา",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG6 สี: ยาน Rocket-1..8 ตาม lobby theme ของผู้เล่น"
},
{
"n": 47,
"name": "เกมยิิงอุกาบาด",
"detail": "อุกาบาดให้พุ่งเร็วกว่านี้ และลูกเล็กกว่านี้เกมง่ายไป",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG6: อุกาบาตเร็วขึ้น (+~50%) + เล็กลง (48->34/11->8)"
},
{
"n": 48,
"name": "เพิ่ม popup เมื่อเกมค้าง เล่นต้่อไม่ได้ ให้มีปุ่มกดออกไปหน้าแรกสุด",
"detail": "สาเหตุเกิดจากมีคนหลุดระหว่างรอ host กด",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 49,
"name": "กรณีผู้เล่นหลุดออกจากเกม",
"detail": "ถ้ามีคนหลุด ให้กลายเป้นบอทแทน",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 50,
"name": "ผู้เล่น 6 คน: บางเกมเล่นได้แค่ 5 (คนที่ 6 หายจากสรุป)",
"detail": "สรุปคะแนนฝั่ง client slice(0,5) ตัดคนที่ 6 — แก้เป็น slice(0,6) ทุกมินิเกม (MG1/3/4/5/6/7)",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส)"
},
{
"n": 51,
"name": "ผู้เล่น 6 คน: คะแนนตอนจบไม่ตรงกันแต่ละจอ",
"detail": "MG1 merge คะแนน server / MG6,7 merge คะแนน host -> ครบ 6 ตรงกัน (MG3 ยัง local)",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส)"
},
{
"n": 52,
"name": "หน้าไต่สวน/สรุปหลักฐาน: กรอบปลิ้นตอน 6 คน",
"detail": "member row grid repeat(5)->repeat(6) ใน testimony-overlay.css (คนที่6 ตกแถว2)",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.006242530 (รอเทส)"
},
{
"n": 53,
"name": "เพิ่มหน้าโหลด (loading) ตอนเข้าเกม",
"detail": "overlay spinner กันเห็นจอเปล่า/asset แว่บ ซ่อนเมื่อ draw เฟรมแรก + safety 8s",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.006242530 (รอเทส)"
},
{
"n": 54,
"name": "host โหลดนานมากตอนเข้าเกม",
"detail": "optimize asset/char (เกี่ยว row 28) — loading overlay ช่วยซ่อน แต่ยังโหลดช้าจริง",
"prio": "สูง",
"status": "เปิด",
"updated": "2026-06-25",
"note": "loading overlay บังอาการแล้ว · root = canvas tint ภาพตัวละครใหญ่ (source ไม่อยู่ใน repo) → ต้อง optimize ภาพแยก"
},
{
"n": 55,
"name": "หลังออกจาก LobbyA ไม่ให้ใครเข้า join",
"detail": "server มี joinLocked (set ตอน LobbyA->case ~8656, check ~7585 whitelist nickname) — ต้อง repro ว่าใครเข้าได้ยังไง (room list ยังโชว์? ชื่อซ้ำ whitelist?)",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 — กรองห้อง joinLocked ออกจาก /api/spaces list (ห้องเริ่มคดีแล้วหาย) + server reject อยู่แล้ว"
},
{
"n": 56,
"name": "ตอนกลับ LobbyB โชว์รูปแรกก่อนแล้วค่อยถูก",
"detail": "เหมือน #1 การ์ด — ต้องใส่ loading state ตอน return LobbyB ด้วย (มี room-loading-overlay อยู่แต่ซ่อนเร็วไป)",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.0308 — การ์ดมี spinner (#1) + room-loading overlay ใส่ % + รอ char preload เสร็จก่อนซ่อน + safety 12s"
},
{
"n": 57,
"name": "เล่นมินิเกมเกิน 3 ครั้งต่อผู้ต้องสงสัย (+การ์ดพิเศษโผล่รอบที่4)",
"detail": "server cap suspect-pick-start (max หลักฐาน>=3 reject) + award cap 3 + client canInvestigateSuspect<3 (เดิม stub return true). UI messaging มีอยู่แล้ว",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.0309/server (รอเทส)"
},
{
"n": 58,
"name": "ปุ่ม \"เล่นเกมต่อ\" ไม่รู้ว่าค้างหรือรอคนอื่น",
"detail": "text \"รอผู้เล่นอื่น (X/Y)\" ถูก set อยู่แล้วแต่ CSS font-size:0 ซ่อน → เพิ่ม .is-waiting CSS โชว์ข้อความ+spinner แทนรูปปุ่ม",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.0309/server (รอเทส)"
},
{
"n": 59,
"name": "loading ไม่คลุม scene transition (เห็นฉากเก่าก่อนเปลี่ยน)",
"detail": "LobbyA->B (in-page swap) โชว์ room-loading ระหว่าง fetch+swap · LobbyB->เกม โชว์ overlay ก่อน location.href · play.html มี overlay รับช่วง",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.0309/server (รอเทส)"
},
{
"n": 60,
"name": "ทำหน้า testcase บนเซิร์ฟเวอร์",
"detail": "srv1361159.hstgr.cloud/testcase — bug list + filter/search/นับสถานะ generate จาก xlsx",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.0309/server (รอเทส)"
},
{
"n": 61,
"name": "MG3 สี block ไม่ตรง user (เกม+admin)",
"detail": "PNG Block-Code-1..8 deploy ครบแล้ว · ต้อง wire สี block = lobby theme ของผู้วาง (seat=theme refactor, ต้องเทส 2 จอ) + admin 8-slot config",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.006250200 — block สีตาม theme ผู้เล่นที่ seat (Block-Code-1..8) ตอนวาด ไม่แตะ turn/sync"
},
{
"n": 62,
"name": "MG1/MG4 user/bot ติดกัน (เดินไม่ผ่าน)",
"detail": "เพิ่ม anti-stuck: ดันชนเพื่อน → เขย่า (shake) + ปลดล็อกเดินผ่านกันชั่วคราว คงกำแพง/area/quiz-lock; ทั้งผู้เล่นและบอท",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.006250100 (รอเทส)"
},
{
"n": 63,
"name": "LobbyA มือถือ กดเดินไม่ได้",
"detail": "lobby canvas มีแค่ dblclick (ไม่มี touch) → มือถือแตะเดินไม่ได้. แก้: เพิ่ม tap-to-walk (เฉพาะ touch, PC ยัง dblclick) + DPI-safe coord + touch-action:none + zoom-out default บนจอ<600px (PC ย่อส่วน)",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 room-lobby.js?v=0.0310 (รอเทสมือถือ) · PC ไม่กระทบ"
},
{
"n": 64,
"name": "มือถือ: ปุ่มแชท AI ใหญ่ไป + ปุ่ม F (host) ไม่มี",
"detail": "AI chat collapsed btn 213x201px → ใหญ่บนมือถือ. + การกด F (interact: host เลือกระดับ/คดี, ห้องแต่งตัว) ไม่มีบนมือถือ",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 (css 0.0176/js 0.0311): media<600px ย่อปุ่ม AI (92px) + ปุ่มลอย \"โต้ตอบ(F)\" บน touch-primary device (dispatch synthetic KeyF). PC ไม่เห็น"
},
{
"n": 65,
"name": "อีเวนต์พิเศษ (ทนาย/เทพ) = 1 ใน 3 (ตามดีไซน์)",
"detail": "ดีไซน์: 3 เกม/LobbyB เล่นได้เกมละ 3 ครั้ง · อีเวนต์พิเศษ 1/3 ต่อครั้ง (1 ใบ) · Test Mode บังคับ. revert การแก้ \">1 ต่อรอบ\" กลับเป็น 1 ใบ/ครั้งที่ติด",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ revert 2026-06-25 — กลับเป็น 1/3 (1 ใบ/ครั้งที่ติด) ตามดีไซน์ · ยืนยันโครงสร้าง 3เกม×3ครั้ง + disruptor toggle (>4คน/Test Mode) ถูกต้อง"
},
{
"n": 66,
"name": "host หลุด → ย้าย host ให้คนอยู่นานสุด",
"detail": "pickNextHostPeer เลือก spawnJoinOrder ต่ำสุด (เสถียรข้าม reconnect) แทน Map-first + host จริง reclaim ได้",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 (server) — Part 1 host migration"
},
{
"n": 67,
"name": "คนหลุดกลางเกม → 10 วิ reconnect + บอทเล่นแทน",
"detail": "peer-disconnect-grace แบนเนอร์นับ 10 วิ · กลับทัน=cancel เล่นต่อ · ครบเวลา=peer-bot-takeover host เติมบอท 1 ตัว (rebalance, cap≤6). บอทไม่สืบทอดตำแหน่ง/คะแนน",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 (server+play.js v0.006250400) — Part 2 แบบ A · seamless(host คุม entity เดิม)=ค้าง(เสี่ยง)"
},
{
"n": 68,
"name": "ห้องแต่งตัว (customize) มือถือแนวนอนใช้ไม่ได้",
"detail": "@media max-width:960px stack แนวตั้ง → panel สูงตาม aspect ล้นจอแนวนอน. แก้: เพิ่ม media (landscape + max-height:540px) ใช้เลย์เอาต์แนวนอน + ขนาดอิงความสูงจอ",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 customize-popup.css?v=43 (Main-Lobby + room-lobby) — รอเทสมือถือแนวนอน"
},
{
"n": 69,
"name": "UI ห้องเกม (LobbyA) มือถือแนวนอน — ย่อให้พอดี",
"detail": "room-lobby ไม่มี landscape media query → chat panel(576x525), ปุ่ม voice/แว่น/ถ้วย(80px), AI chat ล้น/ใหญ่บนแนวนอน. เพิ่ม @media(landscape+max-height:540px) ย่อตามความสูงจอ (อิง Main-Lobby pattern)",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 room-lobby.css?v=0.0177 — chat/BR buttons/AI/ready/exit · overlay หลักฐาน-อันดับ (inline/JS) ยังต้องทำแยก"
},
{
"n": 70,
"name": "การ์ดรางวัลหลักฐาน มือถือใหญ่เกิน กดปุ่มไม่ได้",
"detail": "หน้าโชว์การ์ดรางวัล \"คุณได้รับหลักฐาน!\" (หลังจบมินิเกมสืบสวน) บนมือถือการ์ดใหญ่มากจนเนื้อหา+ปุ่ม \"เก็บเข้าแฟ้ม\" หลุดล่างจอ กดปิดไม่ได้",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "แก้แล้ว (deploy prod play.js?v=0.006250402): ทำ overlay เป็น scroll container + inner margin:auto (กัน flexbox clip ด้านบนเวลาเนื้อหาสูงเกินจอ) + คุมขนาดการ์ด/หัวข้อ/ปุ่มด้วย clamp ตาม viewport (การ์ด max-width min(72vw,360px) + max-height 52vh, ปุ่ม font/padding clamp). มือถือแนวตั้งเลื่อนถึงปุ่มได้เสมอ. ไฟล์: Game/public/js/play.js (showInSceneEvidenceReveal)."
},
{
"n": 71,
"name": "หน้าผลชนะ (โพเดียม) ตัวละครลอย ไม่ยืนบนพื้น",
"detail": "หน้า \"ยินดีด้วย ทีมนักสืบ ชนะ!\" ตัวละครผู้ชนะลอยอยู่กลางอากาศ ไม่ยืนบนพื้นเวที",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "แก้แล้ว (deploy prod room-lobby.js?v=0.0312): พื้นหลัง win-bg เดิม center/cover ทำให้พื้นเวทีถูก crop เลื่อน + podium ตั้ง bottom:9% ตัวละครเลยลอย. แก้เป็น (1) ปักพื้นหลังไว้ขอบล่าง background-position:center bottom -> พื้นเวทีติดขอบล่างจอเสมอทุกสัดส่วน (2) ลด podium bottom 9% -> 2% ให้ตัวละครยืนบนพื้น. ไฟล์: Game/public/js/room-lobby.js (injectFinalResultStyle, #trial-final-podium)."
},
{
"n": 72,
"name": "ป๊อปอัปขอสิทธิ์ไมค์ เด้งทุกครั้งที่เข้า Lobby",
"detail": "หน้าขอสิทธิ์ไมโครโฟน (mic-permission-overlay) เด้งขึ้นทุกครั้งที่เข้า LobbyA/LobbyB น่ารำคาญ ทั้งที่เบราว์เซอร์จำสิทธิ์ให้แล้ว",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "แก้แล้ว (deploy prod room-lobby.js?v=0.0313): เดิม overlay เป็น display:flex default จึงเด้งทุกครั้งที่โหลดหน้า. เพิ่มตรวจสิทธิ์ผ่าน navigator.permissions.query(microphone) + จำสถานะใน localStorage (justiceMicPromptHandled). แสดงเฉพาะครั้งแรกที่ยังไม่เคยตัดสินใจ (permission=prompt และยังไม่เคยกดอนุญาต/ปิด); ถ้า granted แล้ว/เคยจัดการแล้ว → ซ่อนทันที (มี inline script กันแฟลชก่อน paint). ปุ่ม 🎤 ขอสิทธิ์ไมค์ ในห้องยังใช้ขอใหม่ได้. ไฟล์: room-lobby.html (inline gate) + room-lobby.js (gateMicPermissionOverlay, hideMicPermissionOverlay)."
}
]
+623
View File
@@ -0,0 +1,623 @@
[
{
"n": 1,
"name": "แต่งตัวไม่ได้",
"detail": "แต่งตัวยังไม่ได้ มีพี่คิวทำได้คนเดียว",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 2,
"name": "ตัวละครไม่แสดง",
"detail": "หน้าเลือกเกม Quiz battle ภาพตัวละครไม่แสดง",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 3,
"name": "ระบบ join เกมมีปัญหา",
"detail": "เมื่อเข้ามินิเกม : บางคนเล่นเกมกะบอท บางคนเล่นกะเพื่อน แต่ละคนเข้ามินิเกมแล้วเจอกันบ้าง ไม่เจอกันบ้าง",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 4,
"name": "ระบบ Host มีปัญหา - 1",
"detail": "เป็น host ในเกม ตอนที่เลือกผู้ต้องสงสัย รอบที่ 2 ระบบบอกว่าให้รอ host ไม่สามารถเลือกการ์ดและกดเริ่มสืบสวนได้",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 5,
"name": "ระบบ Host มีปัญหา - 2",
"detail": "เมื่อกดออกจากเกม Host เปลี่ยนจากพี่ต้อมเป็นพี่คิวอัติโนมัติ",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 6,
"name": "ระบบ Host มีปัญหา - 3",
"detail": "เมื่อเข้ามินิเกม ทุกคนเห็นตัวเองเป็น host หมดเลย แต่กดเริ่มเกมไม่ได้",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 7,
"name": "ระบบสีตัวละคร ยังใช้ไม่ได้",
"detail": "สีตัวละครเปลี่ยนไปเรื่อย ไม่รุ้ว่าเราสีอะไรแน่ งงไปหมด",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 8,
"name": "หน้าเข้าร่วมเกมส์",
"detail": "ห้องไหนที่เริ่มเกมส์ไปแล้ว เมื่อกดเข้าร่วมแสดง popup alert error ปรับดีไซต์ให้สวยงามกว่านี้",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 9,
"name": "เกมวิ่งขึ้นบันได ผิด flow",
"detail": "ปรับ flow ให้ถูกต้อง คือ ใครตกจอตาย แต่ถ้าขึ้นไปอยู่บนสุดแล้วจะกระโดดไม่ไปต่อเฉยๆ แต่ไม่ตาย",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 10,
"name": "เกมวิ่งขึ้นบันได",
"detail": "เวลาหมดแล้ว ให้เล่นรอบ 2 auto / รอบ 3 มี popup ให้กดเล่นต่อ",
"prio": "",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 11,
"name": "เกมยิงลูกโป่ง",
"detail": "ฉากเกมยิงลูกโป่งเล็กไป",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 12,
"name": "เกมต่อตึก ผิด flow",
"detail": "ปรับ flow ให้ถูกต้อง คือ เล่นทีละคน เรียงตามคิวไปเรื่อย โดยที่ blog ตามสีผู้เล่นด้วย",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 13,
"name": "เข้าร่วมห้องไม่ได้",
"detail": "สร้างห้องส่วนตัวเเล้ว เเต่เพื่อนจอยเข้าไปไม่ได้ ทั้งที่กรอกข้อมูลครบ",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 14,
"name": "Room ID",
"detail": "เวลาจอยห้องให้กรอกเเค่ ID กับรหัสที่สร้าง",
"prio": "ต่ำ",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 15,
"name": "จอยห้องคนอื่น",
"detail": "ในกรณีที่จอยห้องคนอื่น ไปหน้าเข้าร่วม เเละกดปุ่มเข้าร่วมไปเเล้ว จากนั้นกดปุ่ม exit จะพาไปหน้าสร้างห้อง สิ่งที่ถูกต้อง มันควรมาหน้า ห้องเข้าร่วมเกม",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 16,
"name": "grid กับ ฉาก ไม่ตรงกัน",
"detail": "เกม Quiz battle เกมที่ 1 ฉากกับกริดไม่ตรงกัน ทำให้เดินทะลุ หรือเดินติดในจุดที่ไม่ควรติด",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 17,
"name": "ตัวละครวิ่งเร็วเกินไป",
"detail": "เกม Quiz battle เกมที่ 1 ตัวละครผู้เล่นเร็วเกินไป",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 18,
"name": "Oauth Social log-in error",
"detail": "ไม่สามารถ log-inด้วย facebook / Google ได้ path: /oauth-public.php",
"prio": "ต่ำ",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 19,
"name": "หน้า login",
"detail": "นโยบายความเป็นส่วนตัวกดไม่ได้",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 20,
"name": "แสดงเลข version ตรงมุมจอ",
"detail": "แสดงเลข version ตรงมุมจอขวาบนหรือขวาล่างก็ได้",
"prio": "ต่ำ",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — badge มุมขวาล่าง อ่าน ?v= อัตโนมัติ"
},
{
"n": 21,
"name": "ลดขนาดตัวละคร",
"detail": "ลดขนาดตัวละครในห้อง lobby ลงอีก 2เท่า",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 22,
"name": "เปลี่ยน ui popup ตอนพูดบนหัว",
"detail": "แก้ png seq บนหัวใหม่ เป็นแบบอื่น",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 23,
"name": "ชื่่อตัวละครทับลำโพง/ทับ bubble",
"detail": "ใน lobby ขยับชื่อลงมาอย่าให้ทับ",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 24,
"name": "UI แต่งตัว : การแจ้งเตือนสีตัวละครถูกใช้แล้ว",
"detail": "ตอนเลือกสีให้มีคำแจ้งเตือน used",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 25,
"name": "เพิ่ม ข้อความแจ้งเตือนอัพเดทกลางจอ",
"detail": "ถ้ามีการอัพเดทซีน อัพเดทสีตัวละครให้มีป็อปอัพเด้งมากลางหน้าจอ ว่ามีการอัพเดทและให้รอโหลด",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 26,
"name": "เอาเงาตัวละครออก 1 อัน",
"detail": "ตอนนี้ตัวละครมีเงา 2 ชั้น เอาออกไป 1 ชั้น",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — ปิด default-shadow overlay เหลือเงาในสไปรต์"
},
{
"n": 27,
"name": "optimize ตัวละครให้เล็กลงอีก มันโหลดช้า",
"detail": "",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 28,
"name": "ปุ่ม start ใน lobby",
"detail": "ทำให้ดูโต้ตอบหน่อย เช่นพอเดินเข้าไปใกล้ให้มัน glow ขึ้นมา",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 29,
"name": "เกมต่อตึก ผิด flow",
"detail": "สี blog กับคนเล่นยังไม่สัมพันกัน ต้องเรียงตามสี และตามคิว",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 30,
"name": "เกมต่อตึก บั๊กคนที่ 5",
"detail": "คนที่ 5 ไม่สามารถเล่นได้",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 31,
"name": "เอา mock การ์ดผู้ต้องหาเก่าออกไป",
"detail": "มันแว่บๆมา",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.006242530 (รอเทส) — ซ่อนรูปจนรูปคดีจริงโหลด + spinner"
},
{
"n": 32,
"name": "เอา popup ขออนุญาตใช้ไมค์ออกไปเลย น่ารำคาญ",
"detail": "",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 33,
"name": "หน้า howto เปลี่ยนสีปุ่ม ready ด้วยตอน กดready แล้ว",
"detail": "เปลี่ยนเป้นสีเขียวพร้อมเครื่องหมายถูกตามดีไซน์",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 34,
"name": "เกม ใช่ หรือไม่ : ตอนจบบั๊ค",
"detail": "ตอนสรุปคะแนน ยังนับเวลาอยุ่ และขึ้น gameover ทับหน้าสรุปคะแนน",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG1 ใช่/ไม่: capture mg1AnyScore ก่อนล้าง (gameover) + guard quiz-phase หลังจบ (timer)"
},
{
"n": 35,
"name": "เกมวิ่ง : กดปุ่มไม่กระโดด",
"detail": "ปุ่ม jump กดแล้วไม่กระโดด",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 36,
"name": "เกมวิ่ง : ตอนจบบั๊ค วิ่งเลยฉาก",
"detail": "ตอนจบบั๊ค วิ่งเลยฉาก (ในบางรอบ)",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG2 วิ่งเลยฉาก: walk-to-finish ค่อยๆเดินเข้าเส้น + clamp ไม่เลยเส้น"
},
{
"n": 37,
"name": "popup รับหลักฐาน ทับไมค์",
"detail": "กดไมค์ไม่ได้ ตอนรับหลักฐาน ไปดูเรื่องเลเยอร์อีกที",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 38,
"name": "หน้าส่งหลักฐาน ปุ่มส่งหลักฐานหลุดจอ (เครื่องของเนส)",
"detail": "เป้นเครื่องเนสคนเดียว",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 39,
"name": "ห้องส่งหลักฐาน : เมื่อมีคนหลุด 1 คน จะไปต่อไม่ได้เลย",
"detail": "",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — testimony: disconnect ลบ submit/ready + client ใช้ totalPlayers authoritative"
},
{
"n": 40,
"name": "จำนวนคน ให้เล่นได้ตั้งแต่ 4-6 คน",
"detail": "กรณีเลือกผู้เล่นตั้งแต่ 4 คนขึ้นไปไม่ต้องมี bot",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 41,
"name": "เกม หยิบแผ่นป้าย",
"detail": "ต้องเล่นให้ครบ 10 ข้อก่อน ค่อยจบเกม (ตอนนี้เล่นถูก 1 ข้อ จบเกมเลย)",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG4 หยิบป้าย: ใช้ map จริง (currentPlayMapId) เปิด mission flow + default 10 ข้อ"
},
{
"n": 42,
"name": "เกม กระโดด",
"detail": "ท่ากระโดดแปลก ยืนซอยเท้าทำไม",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 43,
"name": "เกม กระโดด",
"detail": "ขั้นบันไดมันถี่ไป บางทีกระโดดแล้วเบิ้ลขั้นเลย ปรับให้ระยะห่างแต่ละขั้นมันเท่ากัน",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 44,
"name": "เกมกระโดด",
"detail": "อีเวนท์พิเศษ (ทนาย/เทพความรู้) หายไป",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG5 อีเวนต์พิเศษ: เพิ่ม branch jump_survive วาง icon บน platform"
},
{
"n": 45,
"name": "เกมไม่รองรับ 4K",
"detail": "หน้ารับหลักฐาน (หลังจากเล่นเกมจบ)",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 46,
"name": "เกมยิิงอุกาบาด",
"detail": "สีตัวละครยังไม่ตรงกับที่เลือกมา",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG6 สี: ยาน Rocket-1..8 ตาม lobby theme ของผู้เล่น"
},
{
"n": 47,
"name": "เกมยิิงอุกาบาด",
"detail": "อุกาบาดให้พุ่งเร็วกว่านี้ และลูกเล็กกว่านี้เกมง่ายไป",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG6: อุกาบาตเร็วขึ้น (+~50%) + เล็กลง (48->34/11->8)"
},
{
"n": 48,
"name": "เพิ่ม popup เมื่อเกมค้าง เล่นต้่อไม่ได้ ให้มีปุ่มกดออกไปหน้าแรกสุด",
"detail": "สาเหตุเกิดจากมีคนหลุดระหว่างรอ host กด",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 49,
"name": "กรณีผู้เล่นหลุดออกจากเกม",
"detail": "ถ้ามีคนหลุด ให้กลายเป้นบอทแทน",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 50,
"name": "ผู้เล่น 6 คน: บางเกมเล่นได้แค่ 5 (คนที่ 6 หายจากสรุป)",
"detail": "สรุปคะแนนฝั่ง client slice(0,5) ตัดคนที่ 6 — แก้เป็น slice(0,6) ทุกมินิเกม (MG1/3/4/5/6/7)",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส)"
},
{
"n": 51,
"name": "ผู้เล่น 6 คน: คะแนนตอนจบไม่ตรงกันแต่ละจอ",
"detail": "MG1 merge คะแนน server / MG6,7 merge คะแนน host -> ครบ 6 ตรงกัน (MG3 ยัง local)",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส)"
},
{
"n": 52,
"name": "หน้าไต่สวน/สรุปหลักฐาน: กรอบปลิ้นตอน 6 คน",
"detail": "member row grid repeat(5)->repeat(6) ใน testimony-overlay.css (คนที่6 ตกแถว2)",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.006242530 (รอเทส)"
},
{
"n": 53,
"name": "เพิ่มหน้าโหลด (loading) ตอนเข้าเกม",
"detail": "overlay spinner กันเห็นจอเปล่า/asset แว่บ ซ่อนเมื่อ draw เฟรมแรก + safety 8s",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.006242530 (รอเทส)"
},
{
"n": 54,
"name": "host โหลดนานมากตอนเข้าเกม",
"detail": "optimize asset/char (เกี่ยว row 28) — loading overlay ช่วยซ่อน แต่ยังโหลดช้าจริง",
"prio": "สูง",
"status": "เปิด",
"updated": "2026-06-25",
"note": "loading overlay บังอาการแล้ว · root = canvas tint ภาพตัวละครใหญ่ (source ไม่อยู่ใน repo) → ต้อง optimize ภาพแยก"
},
{
"n": 55,
"name": "หลังออกจาก LobbyA ไม่ให้ใครเข้า join",
"detail": "server มี joinLocked (set ตอน LobbyA->case ~8656, check ~7585 whitelist nickname) — ต้อง repro ว่าใครเข้าได้ยังไง (room list ยังโชว์? ชื่อซ้ำ whitelist?)",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 — กรองห้อง joinLocked ออกจาก /api/spaces list (ห้องเริ่มคดีแล้วหาย) + server reject อยู่แล้ว"
},
{
"n": 56,
"name": "ตอนกลับ LobbyB โชว์รูปแรกก่อนแล้วค่อยถูก",
"detail": "เหมือน #1 การ์ด — ต้องใส่ loading state ตอน return LobbyB ด้วย (มี room-loading-overlay อยู่แต่ซ่อนเร็วไป)",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.0308 — การ์ดมี spinner (#1) + room-loading overlay ใส่ % + รอ char preload เสร็จก่อนซ่อน + safety 12s"
},
{
"n": 57,
"name": "เล่นมินิเกมเกิน 3 ครั้งต่อผู้ต้องสงสัย (+การ์ดพิเศษโผล่รอบที่4)",
"detail": "server cap suspect-pick-start (max หลักฐาน>=3 reject) + award cap 3 + client canInvestigateSuspect<3 (เดิม stub return true). UI messaging มีอยู่แล้ว",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.0309/server (รอเทส)"
},
{
"n": 58,
"name": "ปุ่ม \"เล่นเกมต่อ\" ไม่รู้ว่าค้างหรือรอคนอื่น",
"detail": "text \"รอผู้เล่นอื่น (X/Y)\" ถูก set อยู่แล้วแต่ CSS font-size:0 ซ่อน → เพิ่ม .is-waiting CSS โชว์ข้อความ+spinner แทนรูปปุ่ม",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.0309/server (รอเทส)"
},
{
"n": 59,
"name": "loading ไม่คลุม scene transition (เห็นฉากเก่าก่อนเปลี่ยน)",
"detail": "LobbyA->B (in-page swap) โชว์ room-loading ระหว่าง fetch+swap · LobbyB->เกม โชว์ overlay ก่อน location.href · play.html มี overlay รับช่วง",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.0309/server (รอเทส)"
},
{
"n": 60,
"name": "ทำหน้า testcase บนเซิร์ฟเวอร์",
"detail": "srv1361159.hstgr.cloud/testcase — bug list + filter/search/นับสถานะ generate จาก xlsx",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.0309/server (รอเทส)"
},
{
"n": 61,
"name": "MG3 สี block ไม่ตรง user (เกม+admin)",
"detail": "PNG Block-Code-1..8 deploy ครบแล้ว · ต้อง wire สี block = lobby theme ของผู้วาง (seat=theme refactor, ต้องเทส 2 จอ) + admin 8-slot config",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.006250200 — block สีตาม theme ผู้เล่นที่ seat (Block-Code-1..8) ตอนวาด ไม่แตะ turn/sync"
},
{
"n": 62,
"name": "MG1/MG4 user/bot ติดกัน (เดินไม่ผ่าน)",
"detail": "เพิ่ม anti-stuck: ดันชนเพื่อน → เขย่า (shake) + ปลดล็อกเดินผ่านกันชั่วคราว คงกำแพง/area/quiz-lock; ทั้งผู้เล่นและบอท",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.006250100 (รอเทส)"
},
{
"n": 63,
"name": "LobbyA มือถือ กดเดินไม่ได้",
"detail": "lobby canvas มีแค่ dblclick (ไม่มี touch) → มือถือแตะเดินไม่ได้. แก้: เพิ่ม tap-to-walk (เฉพาะ touch, PC ยัง dblclick) + DPI-safe coord + touch-action:none + zoom-out default บนจอ<600px (PC ย่อส่วน)",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 room-lobby.js?v=0.0310 (รอเทสมือถือ) · PC ไม่กระทบ"
},
{
"n": 64,
"name": "มือถือ: ปุ่มแชท AI ใหญ่ไป + ปุ่ม F (host) ไม่มี",
"detail": "AI chat collapsed btn 213x201px → ใหญ่บนมือถือ. + การกด F (interact: host เลือกระดับ/คดี, ห้องแต่งตัว) ไม่มีบนมือถือ",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 (css 0.0176/js 0.0311): media<600px ย่อปุ่ม AI (92px) + ปุ่มลอย \"โต้ตอบ(F)\" บน touch-primary device (dispatch synthetic KeyF). PC ไม่เห็น"
},
{
"n": 65,
"name": "อีเวนต์พิเศษ (ทนาย/เทพ) = 1 ใน 3 (ตามดีไซน์)",
"detail": "ดีไซน์: 3 เกม/LobbyB เล่นได้เกมละ 3 ครั้ง · อีเวนต์พิเศษ 1/3 ต่อครั้ง (1 ใบ) · Test Mode บังคับ. revert การแก้ \">1 ต่อรอบ\" กลับเป็น 1 ใบ/ครั้งที่ติด",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ revert 2026-06-25 — กลับเป็น 1/3 (1 ใบ/ครั้งที่ติด) ตามดีไซน์ · ยืนยันโครงสร้าง 3เกม×3ครั้ง + disruptor toggle (>4คน/Test Mode) ถูกต้อง"
},
{
"n": 66,
"name": "host หลุด → ย้าย host ให้คนอยู่นานสุด",
"detail": "pickNextHostPeer เลือก spawnJoinOrder ต่ำสุด (เสถียรข้าม reconnect) แทน Map-first + host จริง reclaim ได้",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 (server) — Part 1 host migration"
},
{
"n": 67,
"name": "คนหลุดกลางเกม → 10 วิ reconnect + บอทเล่นแทน",
"detail": "peer-disconnect-grace แบนเนอร์นับ 10 วิ · กลับทัน=cancel เล่นต่อ · ครบเวลา=peer-bot-takeover host เติมบอท 1 ตัว (rebalance, cap≤6). บอทไม่สืบทอดตำแหน่ง/คะแนน",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 (server+play.js v0.006250400) — Part 2 แบบ A · seamless(host คุม entity เดิม)=ค้าง(เสี่ยง)"
},
{
"n": 68,
"name": "ห้องแต่งตัว (customize) มือถือแนวนอนใช้ไม่ได้",
"detail": "@media max-width:960px stack แนวตั้ง → panel สูงตาม aspect ล้นจอแนวนอน. แก้: เพิ่ม media (landscape + max-height:540px) ใช้เลย์เอาต์แนวนอน + ขนาดอิงความสูงจอ",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 customize-popup.css?v=43 (Main-Lobby + room-lobby) — รอเทสมือถือแนวนอน"
},
{
"n": 69,
"name": "UI ห้องเกม (LobbyA) มือถือแนวนอน — ย่อให้พอดี",
"detail": "room-lobby ไม่มี landscape media query → chat panel(576x525), ปุ่ม voice/แว่น/ถ้วย(80px), AI chat ล้น/ใหญ่บนแนวนอน. เพิ่ม @media(landscape+max-height:540px) ย่อตามความสูงจอ (อิง Main-Lobby pattern)",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 room-lobby.css?v=0.0177 — chat/BR buttons/AI/ready/exit · overlay หลักฐาน-อันดับ (inline/JS) ยังต้องทำแยก"
}
]
+641
View File
@@ -0,0 +1,641 @@
[
{
"n": 1,
"name": "แต่งตัวไม่ได้",
"detail": "แต่งตัวยังไม่ได้ มีพี่คิวทำได้คนเดียว",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 2,
"name": "ตัวละครไม่แสดง",
"detail": "หน้าเลือกเกม Quiz battle ภาพตัวละครไม่แสดง",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 3,
"name": "ระบบ join เกมมีปัญหา",
"detail": "เมื่อเข้ามินิเกม : บางคนเล่นเกมกะบอท บางคนเล่นกะเพื่อน แต่ละคนเข้ามินิเกมแล้วเจอกันบ้าง ไม่เจอกันบ้าง",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 4,
"name": "ระบบ Host มีปัญหา - 1",
"detail": "เป็น host ในเกม ตอนที่เลือกผู้ต้องสงสัย รอบที่ 2 ระบบบอกว่าให้รอ host ไม่สามารถเลือกการ์ดและกดเริ่มสืบสวนได้",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 5,
"name": "ระบบ Host มีปัญหา - 2",
"detail": "เมื่อกดออกจากเกม Host เปลี่ยนจากพี่ต้อมเป็นพี่คิวอัติโนมัติ",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 6,
"name": "ระบบ Host มีปัญหา - 3",
"detail": "เมื่อเข้ามินิเกม ทุกคนเห็นตัวเองเป็น host หมดเลย แต่กดเริ่มเกมไม่ได้",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 7,
"name": "ระบบสีตัวละคร ยังใช้ไม่ได้",
"detail": "สีตัวละครเปลี่ยนไปเรื่อย ไม่รุ้ว่าเราสีอะไรแน่ งงไปหมด",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 8,
"name": "หน้าเข้าร่วมเกมส์",
"detail": "ห้องไหนที่เริ่มเกมส์ไปแล้ว เมื่อกดเข้าร่วมแสดง popup alert error ปรับดีไซต์ให้สวยงามกว่านี้",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 9,
"name": "เกมวิ่งขึ้นบันได ผิด flow",
"detail": "ปรับ flow ให้ถูกต้อง คือ ใครตกจอตาย แต่ถ้าขึ้นไปอยู่บนสุดแล้วจะกระโดดไม่ไปต่อเฉยๆ แต่ไม่ตาย",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 10,
"name": "เกมวิ่งขึ้นบันได",
"detail": "เวลาหมดแล้ว ให้เล่นรอบ 2 auto / รอบ 3 มี popup ให้กดเล่นต่อ",
"prio": "",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 11,
"name": "เกมยิงลูกโป่ง",
"detail": "ฉากเกมยิงลูกโป่ง (MG7 Mega Virus) เล็กไป — ต้องการให้ภาพพื้นหลัง (BG) เต็มจอ แต่การเดิน (movement) และพื้นที่เล่น (area) ต่างๆ คงเดิม",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "แก้แล้ว (deploy prod play.js?v=0.006250401): BG ของ MG7 เป็นภาพ 16:9 (1920x1080) แต่โค้ดเดิม map ภาพเข้า world rect 4:3 (640x480) ทำให้ภาพถูกบีบ + เหลือขอบดำซ้าย-ขวา = ฉากดูเล็ก. เปลี่ยนการวาด BG ของ balloon_boss เป็นแบบ cover เต็มจอ (รักษาสัดส่วน 16:9, crop ส่วนเกิน, จัดกึ่งกลาง) วาดแยกจาก world layer → กล้อง/movement/area ของเกมคงเดิมทุกอย่าง เปลี่ยนเฉพาะพื้นหลังให้เต็มจอ. ไฟล์: Game/public/js/play.js (draw loop, สาขา isBalloonBoss cover-fill)."
},
{
"n": 12,
"name": "เกมต่อตึก ผิด flow",
"detail": "ปรับ flow ให้ถูกต้อง คือ เล่นทีละคน เรียงตามคิวไปเรื่อย โดยที่ blog ตามสีผู้เล่นด้วย",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 13,
"name": "เข้าร่วมห้องไม่ได้",
"detail": "สร้างห้องส่วนตัวเเล้ว เเต่เพื่อนจอยเข้าไปไม่ได้ ทั้งที่กรอกข้อมูลครบ",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 14,
"name": "Room ID",
"detail": "เวลาจอยห้องให้กรอกเเค่ ID กับรหัสที่สร้าง",
"prio": "ต่ำ",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 15,
"name": "จอยห้องคนอื่น",
"detail": "ในกรณีที่จอยห้องคนอื่น ไปหน้าเข้าร่วม เเละกดปุ่มเข้าร่วมไปเเล้ว จากนั้นกดปุ่ม exit จะพาไปหน้าสร้างห้อง สิ่งที่ถูกต้อง มันควรมาหน้า ห้องเข้าร่วมเกม",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "",
"note": ""
},
{
"n": 16,
"name": "grid กับ ฉาก ไม่ตรงกัน",
"detail": "เกม Quiz battle เกมที่ 1 ฉากกับกริดไม่ตรงกัน ทำให้เดินทะลุ หรือเดินติดในจุดที่ไม่ควรติด",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 17,
"name": "ตัวละครวิ่งเร็วเกินไป",
"detail": "เกม Quiz battle เกมที่ 1 ตัวละครผู้เล่นเร็วเกินไป",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 18,
"name": "Oauth Social log-in error",
"detail": "ไม่สามารถ log-inด้วย facebook / Google ได้ path: /oauth-public.php",
"prio": "ต่ำ",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 19,
"name": "หน้า login",
"detail": "นโยบายความเป็นส่วนตัวกดไม่ได้",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 20,
"name": "แสดงเลข version ตรงมุมจอ",
"detail": "แสดงเลข version ตรงมุมจอขวาบนหรือขวาล่างก็ได้",
"prio": "ต่ำ",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — badge มุมขวาล่าง อ่าน ?v= อัตโนมัติ"
},
{
"n": 21,
"name": "ลดขนาดตัวละคร",
"detail": "ลดขนาดตัวละครในห้อง lobby ลงอีก 2เท่า",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 22,
"name": "เปลี่ยน ui popup ตอนพูดบนหัว",
"detail": "แก้ png seq บนหัวใหม่ เป็นแบบอื่น",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 23,
"name": "ชื่่อตัวละครทับลำโพง/ทับ bubble",
"detail": "ใน lobby ขยับชื่อลงมาอย่าให้ทับ",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 24,
"name": "UI แต่งตัว : การแจ้งเตือนสีตัวละครถูกใช้แล้ว",
"detail": "ตอนเลือกสีให้มีคำแจ้งเตือน used",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 25,
"name": "เพิ่ม ข้อความแจ้งเตือนอัพเดทกลางจอ",
"detail": "ถ้ามีการอัพเดทซีน อัพเดทสีตัวละครให้มีป็อปอัพเด้งมากลางหน้าจอ ว่ามีการอัพเดทและให้รอโหลด",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 26,
"name": "เอาเงาตัวละครออก 1 อัน",
"detail": "ตอนนี้ตัวละครมีเงา 2 ชั้น เอาออกไป 1 ชั้น",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — ปิด default-shadow overlay เหลือเงาในสไปรต์"
},
{
"n": 27,
"name": "optimize ตัวละครให้เล็กลงอีก มันโหลดช้า",
"detail": "",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 28,
"name": "ปุ่ม start ใน lobby",
"detail": "ทำให้ดูโต้ตอบหน่อย เช่นพอเดินเข้าไปใกล้ให้มัน glow ขึ้นมา",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 29,
"name": "เกมต่อตึก ผิด flow",
"detail": "สี blog กับคนเล่นยังไม่สัมพันกัน ต้องเรียงตามสี และตามคิว",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 30,
"name": "เกมต่อตึก บั๊กคนที่ 5",
"detail": "คนที่ 5 ไม่สามารถเล่นได้",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 31,
"name": "เอา mock การ์ดผู้ต้องหาเก่าออกไป",
"detail": "มันแว่บๆมา",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.006242530 (รอเทส) — ซ่อนรูปจนรูปคดีจริงโหลด + spinner"
},
{
"n": 32,
"name": "เอา popup ขออนุญาตใช้ไมค์ออกไปเลย น่ารำคาญ",
"detail": "",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 33,
"name": "หน้า howto เปลี่ยนสีปุ่ม ready ด้วยตอน กดready แล้ว",
"detail": "เปลี่ยนเป้นสีเขียวพร้อมเครื่องหมายถูกตามดีไซน์",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 34,
"name": "เกม ใช่ หรือไม่ : ตอนจบบั๊ค",
"detail": "ตอนสรุปคะแนน ยังนับเวลาอยุ่ และขึ้น gameover ทับหน้าสรุปคะแนน",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG1 ใช่/ไม่: capture mg1AnyScore ก่อนล้าง (gameover) + guard quiz-phase หลังจบ (timer)"
},
{
"n": 35,
"name": "เกมวิ่ง : กดปุ่มไม่กระโดด",
"detail": "ปุ่ม jump กดแล้วไม่กระโดด",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 36,
"name": "เกมวิ่ง : ตอนจบบั๊ค วิ่งเลยฉาก",
"detail": "ตอนจบบั๊ค วิ่งเลยฉาก (ในบางรอบ)",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG2 วิ่งเลยฉาก: walk-to-finish ค่อยๆเดินเข้าเส้น + clamp ไม่เลยเส้น"
},
{
"n": 37,
"name": "popup รับหลักฐาน ทับไมค์",
"detail": "กดไมค์ไม่ได้ ตอนรับหลักฐาน ไปดูเรื่องเลเยอร์อีกที",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 38,
"name": "หน้าส่งหลักฐาน ปุ่มส่งหลักฐานหลุดจอ (เครื่องของเนส)",
"detail": "เป้นเครื่องเนสคนเดียว",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 39,
"name": "ห้องส่งหลักฐาน : เมื่อมีคนหลุด 1 คน จะไปต่อไม่ได้เลย",
"detail": "",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — testimony: disconnect ลบ submit/ready + client ใช้ totalPlayers authoritative"
},
{
"n": 40,
"name": "จำนวนคน ให้เล่นได้ตั้งแต่ 4-6 คน",
"detail": "กรณีเลือกผู้เล่นตั้งแต่ 4 คนขึ้นไปไม่ต้องมี bot",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 41,
"name": "เกม หยิบแผ่นป้าย",
"detail": "ต้องเล่นให้ครบ 10 ข้อก่อน ค่อยจบเกม (ตอนนี้เล่นถูก 1 ข้อ จบเกมเลย)",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG4 หยิบป้าย: ใช้ map จริง (currentPlayMapId) เปิด mission flow + default 10 ข้อ"
},
{
"n": 42,
"name": "เกม กระโดด",
"detail": "ท่ากระโดดแปลก ยืนซอยเท้าทำไม",
"prio": "กลาง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 43,
"name": "เกม กระโดด",
"detail": "ขั้นบันไดมันถี่ไป บางทีกระโดดแล้วเบิ้ลขั้นเลย ปรับให้ระยะห่างแต่ละขั้นมันเท่ากัน",
"prio": "สูง",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 44,
"name": "เกมกระโดด",
"detail": "อีเวนท์พิเศษ (ทนาย/เทพความรู้) หายไป",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG5 อีเวนต์พิเศษ: เพิ่ม branch jump_survive วาง icon บน platform"
},
{
"n": 45,
"name": "เกมไม่รองรับ 4K",
"detail": "หน้ารับหลักฐาน (หลังจากเล่นเกมจบ)",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 46,
"name": "เกมยิิงอุกาบาด",
"detail": "สีตัวละครยังไม่ตรงกับที่เลือกมา",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG6 สี: ยาน Rocket-1..8 ตาม lobby theme ของผู้เล่น"
},
{
"n": 47,
"name": "เกมยิิงอุกาบาด",
"detail": "อุกาบาดให้พุ่งเร็วกว่านี้ และลูกเล็กกว่านี้เกมง่ายไป",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG6: อุกาบาตเร็วขึ้น (+~50%) + เล็กลง (48->34/11->8)"
},
{
"n": 48,
"name": "เพิ่ม popup เมื่อเกมค้าง เล่นต้่อไม่ได้ ให้มีปุ่มกดออกไปหน้าแรกสุด",
"detail": "สาเหตุเกิดจากมีคนหลุดระหว่างรอ host กด",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 49,
"name": "กรณีผู้เล่นหลุดออกจากเกม",
"detail": "ถ้ามีคนหลุด ให้กลายเป้นบอทแทน",
"prio": "วิกฤต",
"status": "เปิด",
"updated": "",
"note": ""
},
{
"n": 50,
"name": "ผู้เล่น 6 คน: บางเกมเล่นได้แค่ 5 (คนที่ 6 หายจากสรุป)",
"detail": "สรุปคะแนนฝั่ง client slice(0,5) ตัดคนที่ 6 — แก้เป็น slice(0,6) ทุกมินิเกม (MG1/3/4/5/6/7)",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส)"
},
{
"n": 51,
"name": "ผู้เล่น 6 คน: คะแนนตอนจบไม่ตรงกันแต่ละจอ",
"detail": "MG1 merge คะแนน server / MG6,7 merge คะแนน host -> ครบ 6 ตรงกัน (MG3 ยัง local)",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-24",
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส)"
},
{
"n": 52,
"name": "หน้าไต่สวน/สรุปหลักฐาน: กรอบปลิ้นตอน 6 คน",
"detail": "member row grid repeat(5)->repeat(6) ใน testimony-overlay.css (คนที่6 ตกแถว2)",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.006242530 (รอเทส)"
},
{
"n": 53,
"name": "เพิ่มหน้าโหลด (loading) ตอนเข้าเกม",
"detail": "overlay spinner กันเห็นจอเปล่า/asset แว่บ ซ่อนเมื่อ draw เฟรมแรก + safety 8s",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.006242530 (รอเทส)"
},
{
"n": 54,
"name": "host โหลดนานมากตอนเข้าเกม",
"detail": "optimize asset/char (เกี่ยว row 28) — loading overlay ช่วยซ่อน แต่ยังโหลดช้าจริง",
"prio": "สูง",
"status": "เปิด",
"updated": "2026-06-25",
"note": "loading overlay บังอาการแล้ว · root = canvas tint ภาพตัวละครใหญ่ (source ไม่อยู่ใน repo) → ต้อง optimize ภาพแยก"
},
{
"n": 55,
"name": "หลังออกจาก LobbyA ไม่ให้ใครเข้า join",
"detail": "server มี joinLocked (set ตอน LobbyA->case ~8656, check ~7585 whitelist nickname) — ต้อง repro ว่าใครเข้าได้ยังไง (room list ยังโชว์? ชื่อซ้ำ whitelist?)",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 — กรองห้อง joinLocked ออกจาก /api/spaces list (ห้องเริ่มคดีแล้วหาย) + server reject อยู่แล้ว"
},
{
"n": 56,
"name": "ตอนกลับ LobbyB โชว์รูปแรกก่อนแล้วค่อยถูก",
"detail": "เหมือน #1 การ์ด — ต้องใส่ loading state ตอน return LobbyB ด้วย (มี room-loading-overlay อยู่แต่ซ่อนเร็วไป)",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.0308 — การ์ดมี spinner (#1) + room-loading overlay ใส่ % + รอ char preload เสร็จก่อนซ่อน + safety 12s"
},
{
"n": 57,
"name": "เล่นมินิเกมเกิน 3 ครั้งต่อผู้ต้องสงสัย (+การ์ดพิเศษโผล่รอบที่4)",
"detail": "server cap suspect-pick-start (max หลักฐาน>=3 reject) + award cap 3 + client canInvestigateSuspect<3 (เดิม stub return true). UI messaging มีอยู่แล้ว",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.0309/server (รอเทส)"
},
{
"n": 58,
"name": "ปุ่ม \"เล่นเกมต่อ\" ไม่รู้ว่าค้างหรือรอคนอื่น",
"detail": "text \"รอผู้เล่นอื่น (X/Y)\" ถูก set อยู่แล้วแต่ CSS font-size:0 ซ่อน → เพิ่ม .is-waiting CSS โชว์ข้อความ+spinner แทนรูปปุ่ม",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.0309/server (รอเทส)"
},
{
"n": 59,
"name": "loading ไม่คลุม scene transition (เห็นฉากเก่าก่อนเปลี่ยน)",
"detail": "LobbyA->B (in-page swap) โชว์ room-loading ระหว่าง fetch+swap · LobbyB->เกม โชว์ overlay ก่อน location.href · play.html มี overlay รับช่วง",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.0309/server (รอเทส)"
},
{
"n": 60,
"name": "ทำหน้า testcase บนเซิร์ฟเวอร์",
"detail": "srv1361159.hstgr.cloud/testcase — bug list + filter/search/นับสถานะ generate จาก xlsx",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.0309/server (รอเทส)"
},
{
"n": 61,
"name": "MG3 สี block ไม่ตรง user (เกม+admin)",
"detail": "PNG Block-Code-1..8 deploy ครบแล้ว · ต้อง wire สี block = lobby theme ของผู้วาง (seat=theme refactor, ต้องเทส 2 จอ) + admin 8-slot config",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.006250200 — block สีตาม theme ผู้เล่นที่ seat (Block-Code-1..8) ตอนวาด ไม่แตะ turn/sync"
},
{
"n": 62,
"name": "MG1/MG4 user/bot ติดกัน (เดินไม่ผ่าน)",
"detail": "เพิ่ม anti-stuck: ดันชนเพื่อน → เขย่า (shake) + ปลดล็อกเดินผ่านกันชั่วคราว คงกำแพง/area/quiz-lock; ทั้งผู้เล่นและบอท",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 v0.006250100 (รอเทส)"
},
{
"n": 63,
"name": "LobbyA มือถือ กดเดินไม่ได้",
"detail": "lobby canvas มีแค่ dblclick (ไม่มี touch) → มือถือแตะเดินไม่ได้. แก้: เพิ่ม tap-to-walk (เฉพาะ touch, PC ยัง dblclick) + DPI-safe coord + touch-action:none + zoom-out default บนจอ<600px (PC ย่อส่วน)",
"prio": "วิกฤต",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 room-lobby.js?v=0.0310 (รอเทสมือถือ) · PC ไม่กระทบ"
},
{
"n": 64,
"name": "มือถือ: ปุ่มแชท AI ใหญ่ไป + ปุ่ม F (host) ไม่มี",
"detail": "AI chat collapsed btn 213x201px → ใหญ่บนมือถือ. + การกด F (interact: host เลือกระดับ/คดี, ห้องแต่งตัว) ไม่มีบนมือถือ",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 (css 0.0176/js 0.0311): media<600px ย่อปุ่ม AI (92px) + ปุ่มลอย \"โต้ตอบ(F)\" บน touch-primary device (dispatch synthetic KeyF). PC ไม่เห็น"
},
{
"n": 65,
"name": "อีเวนต์พิเศษ (ทนาย/เทพ) = 1 ใน 3 (ตามดีไซน์)",
"detail": "ดีไซน์: 3 เกม/LobbyB เล่นได้เกมละ 3 ครั้ง · อีเวนต์พิเศษ 1/3 ต่อครั้ง (1 ใบ) · Test Mode บังคับ. revert การแก้ \">1 ต่อรอบ\" กลับเป็น 1 ใบ/ครั้งที่ติด",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ revert 2026-06-25 — กลับเป็น 1/3 (1 ใบ/ครั้งที่ติด) ตามดีไซน์ · ยืนยันโครงสร้าง 3เกม×3ครั้ง + disruptor toggle (>4คน/Test Mode) ถูกต้อง"
},
{
"n": 66,
"name": "host หลุด → ย้าย host ให้คนอยู่นานสุด",
"detail": "pickNextHostPeer เลือก spawnJoinOrder ต่ำสุด (เสถียรข้าม reconnect) แทน Map-first + host จริง reclaim ได้",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 (server) — Part 1 host migration"
},
{
"n": 67,
"name": "คนหลุดกลางเกม → 10 วิ reconnect + บอทเล่นแทน",
"detail": "peer-disconnect-grace แบนเนอร์นับ 10 วิ · กลับทัน=cancel เล่นต่อ · ครบเวลา=peer-bot-takeover host เติมบอท 1 ตัว (rebalance, cap≤6). บอทไม่สืบทอดตำแหน่ง/คะแนน",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 (server+play.js v0.006250400) — Part 2 แบบ A · seamless(host คุม entity เดิม)=ค้าง(เสี่ยง)"
},
{
"n": 68,
"name": "ห้องแต่งตัว (customize) มือถือแนวนอนใช้ไม่ได้",
"detail": "@media max-width:960px stack แนวตั้ง → panel สูงตาม aspect ล้นจอแนวนอน. แก้: เพิ่ม media (landscape + max-height:540px) ใช้เลย์เอาต์แนวนอน + ขนาดอิงความสูงจอ",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 customize-popup.css?v=43 (Main-Lobby + room-lobby) — รอเทสมือถือแนวนอน"
},
{
"n": 69,
"name": "UI ห้องเกม (LobbyA) มือถือแนวนอน — ย่อให้พอดี",
"detail": "room-lobby ไม่มี landscape media query → chat panel(576x525), ปุ่ม voice/แว่น/ถ้วย(80px), AI chat ล้น/ใหญ่บนแนวนอน. เพิ่ม @media(landscape+max-height:540px) ย่อตามความสูงจอ (อิง Main-Lobby pattern)",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "✅ แก้แล้ว 2026-06-25 room-lobby.css?v=0.0177 — chat/BR buttons/AI/ready/exit · overlay หลักฐาน-อันดับ (inline/JS) ยังต้องทำแยก"
},
{
"n": 70,
"name": "การ์ดรางวัลหลักฐาน มือถือใหญ่เกิน กดปุ่มไม่ได้",
"detail": "หน้าโชว์การ์ดรางวัล \"คุณได้รับหลักฐาน!\" (หลังจบมินิเกมสืบสวน) บนมือถือการ์ดใหญ่มากจนเนื้อหา+ปุ่ม \"เก็บเข้าแฟ้ม\" หลุดล่างจอ กดปิดไม่ได้",
"prio": "สูง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "แก้แล้ว (deploy prod play.js?v=0.006250402): ทำ overlay เป็น scroll container + inner margin:auto (กัน flexbox clip ด้านบนเวลาเนื้อหาสูงเกินจอ) + คุมขนาดการ์ด/หัวข้อ/ปุ่มด้วย clamp ตาม viewport (การ์ด max-width min(72vw,360px) + max-height 52vh, ปุ่ม font/padding clamp). มือถือแนวตั้งเลื่อนถึงปุ่มได้เสมอ. ไฟล์: Game/public/js/play.js (showInSceneEvidenceReveal)."
},
{
"n": 71,
"name": "หน้าผลชนะ (โพเดียม) ตัวละครลอย ไม่ยืนบนพื้น",
"detail": "หน้า \"ยินดีด้วย ทีมนักสืบ ชนะ!\" ตัวละครผู้ชนะลอยอยู่กลางอากาศ ไม่ยืนบนพื้นเวที",
"prio": "กลาง",
"status": "แก้ไขแล้ว",
"updated": "2026-06-25",
"note": "แก้แล้ว (deploy prod room-lobby.js?v=0.0312): พื้นหลัง win-bg เดิม center/cover ทำให้พื้นเวทีถูก crop เลื่อน + podium ตั้ง bottom:9% ตัวละครเลยลอย. แก้เป็น (1) ปักพื้นหลังไว้ขอบล่าง background-position:center bottom -> พื้นเวทีติดขอบล่างจอเสมอทุกสัดส่วน (2) ลด podium bottom 9% -> 2% ให้ตัวละครยืนบนพื้น. ไฟล์: Game/public/js/room-lobby.js (injectFinalResultStyle, #trial-final-podium)."
}
]
+13
View File
@@ -0,0 +1,13 @@
[
"LobbyA",
"LobbyB",
"ระบบ disconnect",
"Minigame 1",
"Minigame 2",
"Minigame 3",
"Minigame 4",
"Minigame 5",
"Minigame 6",
"Minigame 7",
"อื่นๆ"
]
+246 -6
View File
@@ -78,10 +78,10 @@
"at": "2026-06-25 07:06"
},
"15": {
"result": "fail",
"comment": "❌ ยังไม่ผ่าน — กด exit จาก lobby ที่ join มา กลับไปหน้า \"สร้างห้องเกม\" แทนหน้า \"เข้าร่วมเกม\" bug ยังอยู่ อัปเดต 2026-06-25",
"result": "pass",
"comment": "ผ่านแล้ว ใครเป็น host ตอนออก ออกหน้านึง client ออกอีกหน้า",
"images": [],
"at": "2026-06-25 07:06"
"at": "2026-06-25 08:31"
},
"53": {
"result": "pass",
@@ -90,9 +90,249 @@
"at": "2026-06-25 07:17"
},
"60": {
"result": "none",
"comment": "✅ ผ่าน — หน้า testcase บนเซิร์ฟเวอร์ใช้งานได้ที่ \/testcase\/ อัปเดต 2026-06-25",
"result": "pass",
"comment": "✅ ผ่าน — หน้า testcase ทำงานบนเซิร์ฟเวอร์ที่ srv1361159.hstgr.cloud\/testcase\/ แล้ว อัปเดต 2026-06-25",
"images": [],
"at": "2026-06-25 07:17"
"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": "none",
"comment": "⚠️ ไม่สามารถ runtime test ได้ (canvas freeze CDP) — ต้องทดสอบ manual: MG3 ต่อตึก เล่นทีละคนตามคิว",
"images": [],
"at": "2026-06-25 08:45"
},
"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"
}
}
+235 -46
View File
@@ -1,49 +1,120 @@
<?php
// Justice — Test Case / Bug List tracker (QA verification + admin)
// Justice — Test Case / Bug List tracker (QA verification + admin + categories + dev edit)
$DATA_DIR = __DIR__ . '/data';
$BUGS = $DATA_DIR . '/bugs.json';
$VERIFY = $DATA_DIR . '/verify.json';
$CATS = $DATA_DIR . '/cats.json';
$UPLOADS = __DIR__ . '/uploads';
$ADMIN_PASS = 'justice2026'; // รหัสโหมดแอดมิน
function tc_load($f){ return is_file($f) ? (json_decode(file_get_contents($f), true) ?: []) : []; }
function tc_save($f,$d){ file_put_contents($f, json_encode($d, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX); }
// ---------- API: บันทึกผลตรวจ (POST) ----------
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (($_POST['api'] ?? '') === 'save')) {
// หมวดเริ่มต้น (แก้ไข/เพิ่ม/ลบได้ผ่านโหมดแอดมิน)
$DEFAULT_CATS = ['LobbyA','LobbyB','ระบบ disconnect','Minigame 1','Minigame 2','Minigame 3','Minigame 4','Minigame 5','Minigame 6','Minigame 7','อื่นๆ'];
function tc_cats($CATS,$DEFAULT_CATS){
$c = tc_load($CATS);
if (!is_array($c) || !count($c)) { $c = $DEFAULT_CATS; tc_save($CATS,$c); }
return array_values(array_filter(array_map('strval',$c), function($x){ return trim($x)!==''; }));
}
$is_admin = (($_POST['pass'] ?? '') === $ADMIN_PASS);
// ---------- API ----------
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['api'])) {
header('Content-Type: application/json; charset=utf-8');
if (($_POST['pass'] ?? '') !== $ADMIN_PASS) { http_response_code(403); echo json_encode(['ok'=>false,'error'=>'รหัสแอดมินไม่ถูกต้อง'], JSON_UNESCAPED_UNICODE); exit; }
$id = preg_replace('/[^0-9]/', '', (string)($_POST['id'] ?? ''));
if ($id === '') { echo json_encode(['ok'=>false,'error'=>'no id'], JSON_UNESCAPED_UNICODE); exit; }
if (!is_dir($DATA_DIR)) @mkdir($DATA_DIR, 0775, true);
$verify = tc_load($VERIFY);
$rec = $verify[$id] ?? ['result'=>'none','comment'=>'','images'=>[]];
if (!isset($rec['images']) || !is_array($rec['images'])) $rec['images'] = [];
$res = $_POST['result'] ?? '';
if (in_array($res, ['pass','fail','none'], true)) $rec['result'] = $res;
if (isset($_POST['comment'])) $rec['comment'] = mb_substr(trim((string)$_POST['comment']), 0, 2000);
// ลบรูป
if (($_POST['removeImage'] ?? '') !== '') {
$rm = basename((string)$_POST['removeImage']);
$rec['images'] = array_values(array_filter($rec['images'], function($x) use ($rm){ return $x !== $rm; }));
@unlink($UPLOADS . '/' . $rm);
}
// อัปโหลดรูป
if (!empty($_FILES['image']['tmp_name']) && is_uploaded_file($_FILES['image']['tmp_name'])) {
$ext = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION));
if (in_array($ext, ['jpg','jpeg','png','gif','webp'], true) && $_FILES['image']['size'] < 8*1024*1024) {
if (!is_dir($UPLOADS)) @mkdir($UPLOADS, 0775, true);
$fn = $id . '_' . time() . '_' . bin2hex(random_bytes(3)) . '.' . $ext;
if (move_uploaded_file($_FILES['image']['tmp_name'], $UPLOADS . '/' . $fn)) $rec['images'][] = $fn;
$api = $_POST['api'];
// QA save (เดิม) — ใช้รหัสแอดมิน
if ($api === 'save') {
if (!$is_admin) { http_response_code(403); echo json_encode(['ok'=>false,'error'=>'รหัสแอดมินไม่ถูกต้อง'], JSON_UNESCAPED_UNICODE); exit; }
$id = preg_replace('/[^0-9]/', '', (string)($_POST['id'] ?? ''));
if ($id === '') { echo json_encode(['ok'=>false,'error'=>'no id'], JSON_UNESCAPED_UNICODE); exit; }
if (!is_dir($DATA_DIR)) @mkdir($DATA_DIR, 0775, true);
$verify = tc_load($VERIFY);
$rec = $verify[$id] ?? ['result'=>'none','comment'=>'','images'=>[]];
if (!isset($rec['images']) || !is_array($rec['images'])) $rec['images'] = [];
$res = $_POST['result'] ?? '';
if (in_array($res, ['pass','fail','none'], true)) $rec['result'] = $res;
if (isset($_POST['comment'])) $rec['comment'] = mb_substr(trim((string)$_POST['comment']), 0, 2000);
if (($_POST['removeImage'] ?? '') !== '') {
$rm = basename((string)$_POST['removeImage']);
$rec['images'] = array_values(array_filter($rec['images'], function($x) use ($rm){ return $x !== $rm; }));
@unlink($UPLOADS . '/' . $rm);
}
if (!empty($_FILES['image']['tmp_name']) && is_uploaded_file($_FILES['image']['tmp_name'])) {
$ext = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION));
if (in_array($ext, ['jpg','jpeg','png','gif','webp'], true) && $_FILES['image']['size'] < 8*1024*1024) {
if (!is_dir($UPLOADS)) @mkdir($UPLOADS, 0775, true);
$fn = $id . '_' . time() . '_' . bin2hex(random_bytes(3)) . '.' . $ext;
if (move_uploaded_file($_FILES['image']['tmp_name'], $UPLOADS . '/' . $fn)) $rec['images'][] = $fn;
}
}
$rec['at'] = date('Y-m-d H:i');
$verify[$id] = $rec;
tc_save($VERIFY, $verify);
echo json_encode(['ok'=>true,'rec'=>$rec], JSON_UNESCAPED_UNICODE); exit;
}
$rec['at'] = date('Y-m-d H:i');
$verify[$id] = $rec;
file_put_contents($VERIFY, json_encode($verify, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX);
echo json_encode(['ok'=>true,'rec'=>$rec], JSON_UNESCAPED_UNICODE); exit;
// ทุก API ด้านล่างต้องเป็นแอดมิน
if (!$is_admin) { http_response_code(403); echo json_encode(['ok'=>false,'error'=>'ต้องใส่รหัสแอดมิน'], JSON_UNESCAPED_UNICODE); exit; }
if (!is_dir($DATA_DIR)) @mkdir($DATA_DIR, 0775, true);
// เพิ่ม/แก้ไขบั๊ก (Dev) — id ว่าง = เพิ่มใหม่
if ($api === 'bugsave') {
$bugs = tc_load($BUGS);
$id = preg_replace('/[^0-9]/', '', (string)($_POST['id'] ?? ''));
$row = [
'name' => mb_substr(trim((string)($_POST['name'] ?? '')), 0, 300),
'detail' => mb_substr(trim((string)($_POST['detail'] ?? '')), 0, 4000),
'prio' => in_array(($_POST['prio'] ?? ''), ['วิกฤต','สูง','กลาง','ต่ำ',''], true) ? ($_POST['prio'] ?? '') : '',
'status' => in_array(($_POST['status'] ?? ''), ['เปิด','แก้ไขแล้ว'], true) ? $_POST['status'] : 'เปิด',
'note' => mb_substr(trim((string)($_POST['note'] ?? '')), 0, 4000),
'cat' => mb_substr(trim((string)($_POST['cat'] ?? 'อื่นๆ')), 0, 60),
'updated'=> date('Y-m-d'),
];
if ($row['name'] === '') { echo json_encode(['ok'=>false,'error'=>'ใส่ชื่อปัญหาก่อน'], JSON_UNESCAPED_UNICODE); exit; }
if ($id === '') {
$max = 0; foreach ($bugs as $b) { if (($b['n'] ?? 0) > $max) $max = $b['n']; }
$row['n'] = $max + 1;
$bugs[] = $row;
} else {
$found = false;
foreach ($bugs as &$b) { if ((string)($b['n'] ?? '') === $id) { $b = array_merge($b, $row); $b['n'] = (int)$id; $found = true; break; } }
unset($b);
if (!$found) { echo json_encode(['ok'=>false,'error'=>'ไม่พบบั๊ก #'.$id], JSON_UNESCAPED_UNICODE); exit; }
}
tc_save($BUGS, $bugs);
echo json_encode(['ok'=>true,'bugs'=>$bugs], JSON_UNESCAPED_UNICODE); exit;
}
// ลบบั๊ก
if ($api === 'bugdel') {
$id = preg_replace('/[^0-9]/', '', (string)($_POST['id'] ?? ''));
if ($id === '') { echo json_encode(['ok'=>false,'error'=>'no id'], JSON_UNESCAPED_UNICODE); exit; }
$bugs = tc_load($BUGS);
$bugs = array_values(array_filter($bugs, function($b) use ($id){ return (string)($b['n'] ?? '') !== $id; }));
tc_save($BUGS, $bugs);
echo json_encode(['ok'=>true,'bugs'=>$bugs], JSON_UNESCAPED_UNICODE); exit;
}
// จัดการหมวด — รับ list ทั้งชุด (เพิ่ม/ลบ/แก้/เรียง)
if ($api === 'catsave') {
$list = json_decode((string)($_POST['cats'] ?? '[]'), true);
if (!is_array($list)) { echo json_encode(['ok'=>false,'error'=>'รูปแบบหมวดไม่ถูกต้อง'], JSON_UNESCAPED_UNICODE); exit; }
$clean = [];
foreach ($list as $c) { $c = mb_substr(trim((string)$c), 0, 60); if ($c !== '' && !in_array($c, $clean, true)) $clean[] = $c; }
if (!count($clean)) { echo json_encode(['ok'=>false,'error'=>'ต้องมีอย่างน้อย 1 หมวด'], JSON_UNESCAPED_UNICODE); exit; }
tc_save($CATS, $clean);
echo json_encode(['ok'=>true,'cats'=>$clean], JSON_UNESCAPED_UNICODE); exit;
}
echo json_encode(['ok'=>false,'error'=>'unknown api'], JSON_UNESCAPED_UNICODE); exit;
}
$bugs = tc_load($BUGS);
$verify = tc_load($VERIFY);
$cats = tc_cats($CATS, $DEFAULT_CATS);
?><!DOCTYPE html>
<html lang="th"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
@@ -51,7 +122,7 @@ $verify = tc_load($VERIFY);
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Kanit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root{--bg:#0a0f1e;--panel:#121a30;--panel2:#0e1526;--line:#22304f;--txt:#dce6ff;--mut:#8aa0c8;--cyan:#22d3ee;--green:#22c55e;--red:#f87171;--amber:#fbbf24;}
:root{--bg:#0a0f1e;--panel:#121a30;--panel2:#0e1526;--line:#22304f;--txt:#dce6ff;--mut:#8aa0c8;--cyan:#22d3ee;--green:#22c55e;--red:#f87171;--amber:#fbbf24;--violet:#a78bfa;}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:Kanit,system-ui,sans-serif;background:radial-gradient(circle at 50% -10%,#13213f,#070b16 70%);color:var(--txt);min-height:100vh;padding:24px 16px 80px}
.wrap{max-width:1240px;margin:0 auto}
@@ -68,19 +139,30 @@ h1 .sub{color:var(--mut);font-size:14px;font-weight:300;display:block;margin-top
.chip{background:var(--panel2);border:1px solid var(--line);border-radius:20px;color:var(--mut);padding:7px 15px;font-size:13px;cursor:pointer;font-family:inherit}
.chip:hover{border-color:var(--cyan);color:var(--txt)}
.chip.on{background:var(--cyan);border-color:var(--cyan);color:#04121a;font-weight:600}
.catbar{display:flex;flex-wrap:wrap;gap:7px;align-items:center;margin:6px 0 12px}
.catbar .lbl{color:var(--mut);font-size:12px;margin-right:2px}
.catchip{background:var(--panel2);border:1px solid var(--line);border-radius:18px;color:var(--mut);padding:6px 13px;font-size:12.5px;cursor:pointer;font-family:inherit}
.catchip:hover{border-color:var(--violet);color:var(--txt)}
.catchip.on{background:var(--violet);border-color:var(--violet);color:#160a2e;font-weight:600}
.catchip .cnt{opacity:.7;font-size:11px;margin-left:3px}
.adminbox{margin-left:auto;display:flex;gap:8px;align-items:center}
.adminbox .ok{color:var(--green);font-size:13px}
.lockhint{font-size:13px;color:var(--amber);white-space:nowrap}
.qabadge{cursor:pointer}
.qabadge:hover{filter:brightness(1.15);text-decoration:underline}
.qabadge{cursor:pointer}.qabadge:hover{filter:brightness(1.15);text-decoration:underline}
@keyframes apFlash{0%,100%{box-shadow:none}30%,70%{box-shadow:0 0 0 3px rgba(248,113,113,.55)}}
#adminpass.flash{animation:apFlash .7s ease}
.toolbar{display:flex;gap:8px;flex-wrap:wrap;margin:0 0 12px}
.btn{border:1px solid var(--line);background:var(--panel2);color:var(--txt);border-radius:9px;padding:7px 14px;font-size:13px;cursor:pointer;font-family:inherit}
.btn:hover{border-color:var(--cyan)}
.btn.primary{background:var(--green);color:#042c14;border-color:var(--green);font-weight:600}
.btn.v{background:rgba(167,139,250,.16);border-color:var(--violet);color:var(--violet)}
table{width:100%;border-collapse:collapse;background:var(--panel);border:1px solid var(--line);border-radius:12px;overflow:hidden}
th{text-align:left;font-size:12px;color:var(--mut);font-weight:500;padding:11px 12px;background:var(--panel2);border-bottom:1px solid var(--line)}
td{padding:11px 12px;border-bottom:1px solid var(--line);font-size:13.5px;vertical-align:top;line-height:1.45}
tr:last-child td{border-bottom:none}
.n{color:var(--mut);font-size:12px}.nm{font-weight:500}.dt{color:var(--mut);font-size:12.5px;margin-top:3px}.nt{color:#7fd4a8;font-size:12px;margin-top:4px}
.b{display:inline-block;border-radius:6px;padding:3px 9px;font-size:11.5px;font-weight:500;white-space:nowrap}
.cattag{display:inline-block;border-radius:6px;padding:2px 8px;font-size:11px;background:rgba(167,139,250,.16);color:var(--violet);margin-top:5px}
.s-open{background:rgba(251,191,36,.16);color:var(--amber)}.s-fix{background:rgba(34,197,94,.16);color:var(--green)}.s-other{background:rgba(138,160,200,.16);color:var(--mut)}
.p-crit{background:rgba(248,113,113,.16);color:var(--red)}.p-high{background:rgba(251,146,60,.16);color:#fb923c}.p-mid{background:rgba(251,191,36,.14);color:var(--amber)}.p-low{background:rgba(138,160,200,.14);color:var(--mut)}
.qa{min-width:240px}
@@ -97,17 +179,31 @@ tr:last-child td{border-bottom:none}
.edit .rb button.sel-pass{background:var(--cyan);color:#04121a;border-color:var(--cyan)}
.edit .rb button.sel-fail{background:var(--red);color:#fff;border-color:var(--red)}
.edit .rb button.sel-none{background:#33415c;color:#fff}
.edit textarea{width:100%;background:var(--panel2);border:1px solid var(--line);border-radius:8px;color:var(--txt);font-family:inherit;font-size:13px;padding:8px;resize:vertical;min-height:46px}
.edit textarea,.edit input[type=text],.edit select{width:100%;background:var(--panel2);border:1px solid var(--line);border-radius:8px;color:var(--txt);font-family:inherit;font-size:13px;padding:8px;resize:vertical}
.edit textarea{min-height:46px}
.edit .erow{display:flex;gap:8px;align-items:center;margin-top:6px;flex-wrap:wrap}
.edit .save{background:var(--green);color:#042c14;border:none;border-radius:8px;padding:7px 16px;font-weight:600;cursor:pointer;font-family:inherit}
.edit .msg{font-size:12px}
.editbtn{margin-top:7px;background:none;border:1px dashed var(--line);color:var(--mut);border-radius:8px;padding:4px 11px;font-size:12px;cursor:pointer;font-family:inherit}
.editbtn{margin-top:7px;margin-right:6px;background:none;border:1px dashed var(--line);color:var(--mut);border-radius:8px;padding:4px 11px;font-size:12px;cursor:pointer;font-family:inherit}
.editbtn.dev{border-color:var(--amber);color:var(--amber)}
.editbtn.del{border-color:var(--red);color:var(--red)}
.devform{margin-top:8px;display:none;border-top:1px dashed var(--amber);padding-top:8px}
.devform.show{display:block}
.devform .g{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:7px}
.devform label{font-size:11px;color:var(--mut);display:block;margin-bottom:3px}
.foot{color:var(--mut);font-size:12px;margin-top:16px;text-align:center}
.hidden{display:none}
@media(max-width:760px){.hide-sm{display:none}td,th{padding:9px 7px}}
.modal{position:fixed;inset:0;background:rgba(4,8,18,.78);display:none;align-items:center;justify-content:center;z-index:50;padding:16px}
.modal.show{display:flex}
.modal .box{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:20px;max-width:440px;width:100%;max-height:88vh;overflow:auto}
.modal h3{font-weight:600;font-size:17px;margin-bottom:12px}
.catrow{display:flex;gap:7px;align-items:center;margin-bottom:7px}
.catrow input{flex:1;background:var(--panel2);border:1px solid var(--line);border-radius:8px;color:var(--txt);font-family:inherit;font-size:13px;padding:7px 10px}
.catrow .x{background:var(--red);color:#fff;border:none;border-radius:7px;width:30px;height:30px;cursor:pointer;font-size:15px}
@media(max-width:760px){.hide-sm{display:none}td,th{padding:9px 7px}.devform .g{grid-template-columns:1fr}}
</style></head>
<body><div class="wrap">
<h1>Justice Test Case / Bug List<span class="sub">รายการบัก + ผลตรวจ QA · srv1361159.hstgr.cloud/testcase</span></h1>
<h1>Justice Test Case / Bug List<span class="sub">รายการบัก + ผลตรวจ QA · จัดหมวด · srv1361159.hstgr.cloud/testcase</span></h1>
<div class="cards" id="cards"></div>
<div class="bar">
<input type="text" id="q" placeholder="ค้นหา… (ชื่อ / รายละเอียด / คำอธิบาย)">
@@ -118,30 +214,53 @@ tr:last-child td{border-bottom:none}
<button class="chip" data-f="fail">ยังไม่ผ่าน</button>
<button class="chip" data-f="untested">ยังไม่ตรวจ</button>
<span class="adminbox">
<span class="lockhint" id="lockhint">🔒 แก้ผลตรวจ ใส่รหัส:</span>
<span class="lockhint" id="lockhint">🔒 แก้ไข ใส่รหัส:</span>
<input type="password" id="adminpass" placeholder="รหัสแอดมิน" autocomplete="off">
<span id="adminok" class="ok hidden">โหมดแก้ไข </span>
</span>
</div>
<div class="catbar" id="catbar"></div>
<div class="toolbar hidden" id="toolbar">
<button class="btn primary" id="addBug">+ เพิ่มบั๊ก</button>
<button class="btn v" id="mgCats"> จัดการหมวด</button>
</div>
<table><thead><tr><th style="width:30px">#</th><th>ปัญหา</th><th style="width:78px">สำคัญ</th><th style="width:88px">Dev</th><th class="qa">ผลตรวจ QA</th></tr></thead>
<tbody id="tb"></tbody></table>
<div class="foot">Justice bug tracker · ผลตรวจบันทึกบนเซิร์ฟเวอร์ (แก้ไขได้เมื่อใส่รหัสแอดมิน) · <?php echo count($bugs); ?> รายการ</div>
<div class="foot">Justice bug tracker · ผลตรวจ+Dev แก้ไขได้เมื่อใส่รหัสแอดมิน · <span id="cnt"><?php echo count($bugs); ?></span> รายการ</div>
</div>
<div class="modal" id="catModal"><div class="box">
<h3>จัดการหมวด</h3>
<div id="catList"></div>
<button class="btn" id="catAdd">+ เพิ่มหมวด</button>
<div class="erow" style="margin-top:14px;display:flex;gap:8px"><button class="btn primary" id="catSave">บันทึก</button><button class="btn" id="catClose">ปิด</button><span class="msg" id="catMsg" style="font-size:12px"></span></div>
</div></div>
<script>
var BUGS = <?php echo json_encode($bugs, JSON_UNESCAPED_UNICODE); ?>;
var VERIFY = <?php echo json_encode((object)$verify, JSON_UNESCAPED_UNICODE); ?>;
var filt="all",q="",adminPass="";
var CATS = <?php echo json_encode($cats, JSON_UNESCAPED_UNICODE); ?>;
var filt="all",q="",adminPass="",catFilt="";
function esc(s){return (s||"").replace(/[&<>"]/g,function(c){return{"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;"}[c]})}
function sCls(s){return s.indexOf("แก้")>=0?"s-fix":s==="เปิด"?"s-open":"s-other"}
function pCls(p){return p.indexOf("วิกฤต")>=0?"p-crit":p.indexOf("สูง")>=0?"p-high":p.indexOf("กลาง")>=0?"p-mid":p?"p-low":""}
function vof(n){return VERIFY[n]||{result:"none",comment:"",images:[]}}
function bcat(b){return (b.cat&&(""+b.cat).trim())?(""+b.cat).trim():"อื่นๆ"}
function rLabel(r){return r==="pass"?"✅ เช็คแล้ว (ผ่าน)":r==="fail"?"❌ ยังไม่ผ่าน":"⬜ ยังไม่ตรวจ"}
function rCls(r){return r==="pass"?"r-pass":r==="fail"?"r-fail":"r-none"}
function counts(){
var o=0,f=0,p=0,fa=0,un=0;
BUGS.forEach(function(b){if(b.status==="เปิด")o++;if(b.status.indexOf("แก้")>=0)f++;var v=vof(b.n);if(v.result==="pass")p++;else if(v.result==="fail")fa++;else un++;});
BUGS.forEach(function(b){if(b.status==="เปิด")o++;if((b.status||"").indexOf("แก้")>=0)f++;var v=vof(b.n);if(v.result==="pass")p++;else if(v.result==="fail")fa++;else un++;});
return {o:o,f:f,p:p,fa:fa,un:un};
}
function catBar(){
var byCat={}; BUGS.forEach(function(b){var c=bcat(b);byCat[c]=(byCat[c]||0)+1;});
var html='<span class="lbl">หมวด:</span><button class="catchip'+(catFilt===""?" on":"")+'" data-cat="">ทั้งหมด <span class="cnt">'+BUGS.length+'</span></button>';
CATS.forEach(function(c){html+='<button class="catchip'+(catFilt===c?" on":"")+'" data-cat="'+esc(c)+'">'+esc(c)+' <span class="cnt">'+(byCat[c]||0)+'</span></button>';});
// หมวดที่บั๊กใช้แต่ไม่อยู่ในรายการ (กันตกหล่น)
Object.keys(byCat).forEach(function(c){if(CATS.indexOf(c)<0){html+='<button class="catchip'+(catFilt===c?" on":"")+'" data-cat="'+esc(c)+'" style="opacity:.7">'+esc(c)+' <span class="cnt">'+byCat[c]+'</span></button>';}});
document.getElementById("catbar").innerHTML=html;
}
function render(){
var c=counts();
document.getElementById("cards").innerHTML=
@@ -151,14 +270,18 @@ function render(){
'<div class="card pass"><div class="v">'+c.p+'</div><div class="l">QA: ผ่าน</div></div>'+
'<div class="card fail"><div class="v">'+c.fa+'</div><div class="l">QA: ยังไม่ผ่าน</div></div>'+
'<div class="card"><div class="v">'+c.un+'</div><div class="l">QA: ยังไม่ตรวจ</div></div>';
catBar();
document.getElementById("toolbar").classList.toggle("hidden",!adminPass);
document.getElementById("cnt").textContent=BUGS.length;
var list=BUGS.filter(function(b){
var v=vof(b.n);
if(catFilt&&bcat(b)!==catFilt)return false;
if(filt==="open"&&b.status!=="เปิด")return false;
if(filt==="fixed"&&b.status.indexOf("แก้")<0)return false;
if(filt==="fixed"&&(b.status||"").indexOf("แก้")<0)return false;
if(filt==="pass"&&v.result!=="pass")return false;
if(filt==="fail"&&v.result!=="fail")return false;
if(filt==="untested"&&v.result&&v.result!=="none")return false;
if(q){var h=(b.name+" "+b.detail+" "+b.note+" "+(v.comment||"")).toLowerCase();if(h.indexOf(q)<0)return false}
if(q){var h=(b.name+" "+b.detail+" "+b.note+" "+bcat(b)+" "+(v.comment||"")).toLowerCase();if(h.indexOf(q)<0)return false}
return true;});
document.getElementById("tb").innerHTML=list.map(rowHtml).join("")||'<tr><td colspan="5" style="text-align:center;color:var(--mut);padding:30px">ไม่พบรายการ</td></tr>';
}
@@ -168,6 +291,7 @@ function thumbsHtml(v,n,admin){
return '<a href="uploads/'+encodeURIComponent(im)+'" target="_blank" style="background-image:url(uploads/'+encodeURIComponent(im)+')">'+rm+'</a>';
}).join("");
}
function catOptions(sel){var o="";CATS.forEach(function(c){o+='<option'+(c===sel?" selected":"")+'>'+esc(c)+'</option>';});if(CATS.indexOf(sel)<0&&sel){o+='<option selected>'+esc(sel)+'</option>';}return o;}
function rowHtml(b){
var v=vof(b.n),admin=!!adminPass;
var qa='<span class="b '+rCls(v.result)+' qabadge" data-qa="'+b.n+'" title="'+(admin?'คลิกเพื่อแก้ผลตรวจ':'ใส่รหัสแอดมินมุมขวาบนเพื่อแก้')+'">'+rLabel(v.result)+'</span>'+
@@ -175,7 +299,9 @@ function rowHtml(b){
(v.at?'<div class="qat">อัปเดต '+esc(v.at)+'</div>':'')+
'<div class="thumbs">'+thumbsHtml(v,b.n,admin)+'</div>';
if(admin){
qa+='<button class="editbtn" data-edit="'+b.n+'">✎ แก้ผลตรวจ</button>'+
qa+='<button class="editbtn" data-edit="'+b.n+'">✎ ผลตรวจ</button>'+
'<button class="editbtn dev" data-dev="'+b.n+'">🛠 Dev/หมวด</button>'+
'<button class="editbtn del" data-del="'+b.n+'">🗑 ลบ</button>'+
'<div class="edit" id="edit-'+b.n+'">'+
'<div class="rb" data-rbid="'+b.n+'">'+
'<button data-r="pass" class="'+(v.result==="pass"?"sel-pass":"")+'">ผ่าน</button>'+
@@ -186,23 +312,37 @@ function rowHtml(b){
'<div class="erow"><input type="file" id="img-'+b.n+'" accept="image/*">'+
'<button class="save" data-save="'+b.n+'">บันทึก</button>'+
'<span class="msg" id="msg-'+b.n+'"></span></div>'+
'</div>'+
'<div class="devform" id="dev-'+b.n+'">'+
'<div class="g">'+
'<div><label>หมวด</label><select id="d-cat-'+b.n+'">'+catOptions(bcat(b))+'</select></div>'+
'<div><label>Dev สถานะ</label><select id="d-status-'+b.n+'"><option'+(b.status==="เปิด"?" selected":"")+'>เปิด</option><option'+((b.status||"").indexOf("แก้")>=0?" selected":"")+'>แก้ไขแล้ว</option></select></div>'+
'<div><label>ความสำคัญ</label><select id="d-prio-'+b.n+'"><option value="">-</option>'+["วิกฤต","สูง","กลาง","ต่ำ"].map(function(p){return '<option'+(b.prio===p?" selected":"")+'>'+p+'</option>';}).join("")+'</select></div>'+
'<div><label>ชื่อปัญหา</label><input type="text" id="d-name-'+b.n+'" value="'+esc(b.name)+'"></div>'+
'</div>'+
'<label>รายละเอียด</label><textarea id="d-detail-'+b.n+'">'+esc(b.detail||"")+'</textarea>'+
'<label style="margin-top:6px">หมายเหตุ Dev (วิธีแก้)</label><textarea id="d-note-'+b.n+'">'+esc(b.note||"")+'</textarea>'+
'<div class="erow"><button class="save" data-bugsave="'+b.n+'">บันทึก Dev</button><span class="msg" id="dmsg-'+b.n+'"></span></div>'+
'</div>';
}
return '<tr><td class="n">'+b.n+'</td><td><div class="nm">'+esc(b.name)+'</div>'+
(b.detail?'<div class="dt">'+esc(b.detail)+'</div>':'')+
(b.note?'<div class="nt">'+esc(b.note)+'</div>':'')+'</td>'+
(b.note?'<div class="nt">'+esc(b.note)+'</div>':'')+
'<div class="cattag">'+esc(bcat(b))+'</div></td>'+
'<td>'+(b.prio?'<span class="b '+pCls(b.prio)+'">'+esc(b.prio)+'</span>':'')+'</td>'+
'<td><span class="b '+sCls(b.status)+'">'+esc(b.status)+'</span></td>'+
'<td class="qa">'+qa+'</td></tr>';
}
// events (delegate)
function flashAdmin(){var a=document.getElementById("adminpass");a.classList.remove("flash");void a.offsetWidth;a.classList.add("flash");a.focus();}
document.getElementById("tb").addEventListener("click",function(e){
var t=e.target;
if(t.dataset.qa){ if(!adminPass){ flashAdmin(); } else { var ee=document.getElementById("edit-"+t.dataset.qa); if(ee) ee.classList.toggle("show"); } return; }
if(t.dataset.edit){var ed=document.getElementById("edit-"+t.dataset.edit);if(ed)ed.classList.toggle("show");return;}
if(t.dataset.dev){var df=document.getElementById("dev-"+t.dataset.dev);if(df)df.classList.toggle("show");return;}
if(t.dataset.del){doDelBug(t.dataset.del);return;}
if(t.dataset.r){var rb=t.parentNode;[].forEach.call(rb.children,function(x){x.className=""});t.className="sel-"+t.dataset.r;rb.dataset.sel=t.dataset.r;return;}
if(t.dataset.save){doSave(t.dataset.save);return;}
if(t.dataset.bugsave){doBugSave(t.dataset.bugsave);return;}
if(t.dataset.rm){doRemoveImg(t.dataset.id,t.dataset.rm);return;}
});
function curResult(n){var rb=document.querySelector('.rb[data-rbid="'+n+'"]');if(rb&&rb.dataset.sel)return rb.dataset.sel;var v=vof(n);return v.result||"none";}
@@ -216,11 +356,59 @@ function doSave(n){
else{msg.textContent=j.error||"ผิดพลาด";msg.style.color="var(--red)";}
}).catch(function(){msg.textContent="เชื่อมต่อไม่ได้";msg.style.color="var(--red)";});
}
function doBugSave(n){
var fd=new FormData();fd.append("api","bugsave");fd.append("pass",adminPass);fd.append("id",n);
fd.append("name",document.getElementById("d-name-"+n).value);
fd.append("detail",document.getElementById("d-detail-"+n).value);
fd.append("note",document.getElementById("d-note-"+n).value);
fd.append("cat",document.getElementById("d-cat-"+n).value);
fd.append("status",document.getElementById("d-status-"+n).value);
fd.append("prio",document.getElementById("d-prio-"+n).value);
var msg=document.getElementById("dmsg-"+n);msg.textContent="กำลังบันทึก…";msg.style.color="var(--mut)";
fetch(location.pathname,{method:"POST",body:fd}).then(function(r){return r.json()}).then(function(j){
if(j.ok){BUGS=j.bugs;msg.textContent="บันทึกแล้ว ✓";msg.style.color="var(--green)";setTimeout(render,400);}
else{msg.textContent=j.error||"ผิดพลาด";msg.style.color="var(--red)";}
}).catch(function(){msg.textContent="เชื่อมต่อไม่ได้";msg.style.color="var(--red)";});
}
function doDelBug(n){
if(!confirm("ลบบั๊ก #"+n+" ?"))return;
var fd=new FormData();fd.append("api","bugdel");fd.append("pass",adminPass);fd.append("id",n);
fetch(location.pathname,{method:"POST",body:fd}).then(function(r){return r.json()}).then(function(j){if(j.ok){BUGS=j.bugs;render();}else alert(j.error||"ลบไม่ได้");});
}
function doRemoveImg(n,im){
if(!confirm("ลบรูปนี้?"))return;
var fd=new FormData();fd.append("api","save");fd.append("pass",adminPass);fd.append("id",n);fd.append("removeImage",im);
fetch(location.pathname,{method:"POST",body:fd}).then(function(r){return r.json()}).then(function(j){if(j.ok){VERIFY[n]=j.rec;render();}});
}
// เพิ่มบั๊กใหม่
document.getElementById("addBug").addEventListener("click",function(){
var name=prompt("ชื่อปัญหา (บั๊กใหม่):");if(!name||!name.trim())return;
var cat=prompt("หมวด (เช่น "+CATS.slice(0,3).join(" / ")+"):",catFilt||"อื่นๆ")||"อื่นๆ";
var fd=new FormData();fd.append("api","bugsave");fd.append("pass",adminPass);fd.append("id","");
fd.append("name",name.trim());fd.append("cat",cat.trim());fd.append("status","เปิด");fd.append("prio","กลาง");
fetch(location.pathname,{method:"POST",body:fd}).then(function(r){return r.json()}).then(function(j){if(j.ok){BUGS=j.bugs;render();}else alert(j.error||"เพิ่มไม่ได้");});
});
// จัดการหมวด
var catModal=document.getElementById("catModal");
function renderCatList(){
document.getElementById("catList").innerHTML=CATS.map(function(c,i){
return '<div class="catrow"><input type="text" value="'+esc(c)+'" data-ci="'+i+'"><button class="x" data-cx="'+i+'">×</button></div>';
}).join("");
}
document.getElementById("mgCats").addEventListener("click",function(){renderCatList();document.getElementById("catMsg").textContent="";catModal.classList.add("show");});
document.getElementById("catClose").addEventListener("click",function(){catModal.classList.remove("show");});
document.getElementById("catAdd").addEventListener("click",function(){CATS=collectCats();CATS.push("หมวดใหม่");renderCatList();});
document.getElementById("catList").addEventListener("click",function(e){if(e.target.dataset.cx!=null){CATS=collectCats();CATS.splice(+e.target.dataset.cx,1);renderCatList();}});
function collectCats(){var out=[];[].forEach.call(document.querySelectorAll("#catList input"),function(inp){var v=inp.value.trim();if(v&&out.indexOf(v)<0)out.push(v);});return out;}
document.getElementById("catSave").addEventListener("click",function(){
var list=collectCats();if(!list.length){document.getElementById("catMsg").textContent="ต้องมีอย่างน้อย 1 หมวด";document.getElementById("catMsg").style.color="var(--red)";return;}
var fd=new FormData();fd.append("api","catsave");fd.append("pass",adminPass);fd.append("cats",JSON.stringify(list));
var msg=document.getElementById("catMsg");msg.textContent="กำลังบันทึก…";msg.style.color="var(--mut)";
fetch(location.pathname,{method:"POST",body:fd}).then(function(r){return r.json()}).then(function(j){
if(j.ok){CATS=j.cats;msg.textContent="บันทึกแล้ว ✓";msg.style.color="var(--green)";render();setTimeout(function(){catModal.classList.remove("show");},600);}
else{msg.textContent=j.error||"ผิดพลาด";msg.style.color="var(--red)";}
}).catch(function(){msg.textContent="เชื่อมต่อไม่ได้";msg.style.color="var(--red)";});
});
// admin pass
var ap=document.getElementById("adminpass");
ap.value=localStorage.getItem("tcAdminPass")||"";
@@ -232,5 +420,6 @@ document.getElementById("q").addEventListener("input",function(e){q=e.target.val
[].forEach.call(document.querySelectorAll(".chip"),function(b){b.addEventListener("click",function(){
[].forEach.call(document.querySelectorAll(".chip"),function(x){x.classList.remove("on")});
b.classList.add("on");filt=b.getAttribute("data-f");render();})});
document.getElementById("catbar").addEventListener("click",function(e){var t=e.target.closest(".catchip");if(!t)return;catFilt=t.getAttribute("data-cat")||"";render();});
render();
</script></body></html>