fix on testcase 1.2
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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…');
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) ช่วงเปลี่ยนหน้า minigame→LobbyB (detectiveMinigameActive เพิ่งเป็น false)
|
||||
ทั้งหมดให้เวลา reconnect 10 วิ (กันเปลี่ยนหน้า lobbyB→play→lobbyB ที่ 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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
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');
|
||||
|
||||
@@ -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) ยังต้องทำแยก"
|
||||
}
|
||||
]
|
||||
@@ -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)."
|
||||
}
|
||||
]
|
||||
@@ -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) ยังต้องทำแยก"
|
||||
}
|
||||
]
|
||||
@@ -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)."
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
[
|
||||
"LobbyA",
|
||||
"LobbyB",
|
||||
"ระบบ disconnect",
|
||||
"Minigame 1",
|
||||
"Minigame 2",
|
||||
"Minigame 3",
|
||||
"Minigame 4",
|
||||
"Minigame 5",
|
||||
"Minigame 6",
|
||||
"Minigame 7",
|
||||
"อื่นๆ"
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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{"&":"&","<":"<",">":">","\"":"""}[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>
|
||||
|
||||