update minigame 3 all etc
This commit is contained in:
@@ -5208,8 +5208,10 @@
|
||||
.then(function (data) {
|
||||
var pk = (data && data.pickVoteSec != null) ? Number(data.pickVoteSec) : 25;
|
||||
var tr = (data && data.trialVoteSec != null) ? Number(data.trialVoteSec) : 30;
|
||||
var rv = (data && data.trialResultRevealMs != null) ? Number(data.trialResultRevealMs) : 7000;
|
||||
if (el('vote-timing-pick')) el('vote-timing-pick').value = String(Number.isFinite(pk) ? pk : 25);
|
||||
if (el('vote-timing-trial')) el('vote-timing-trial').value = String(Number.isFinite(tr) ? tr : 30);
|
||||
if (el('vote-timing-reveal')) el('vote-timing-reveal').value = String(Number.isFinite(rv) ? rv : 7000);
|
||||
setMsg('vote-timing-msg', '', '');
|
||||
})
|
||||
.catch(function (e) { setMsg('vote-timing-msg', (e.message || 'โหลดไม่ได้') + ' — เช็ค /Game/api/game-timing', 'error'); });
|
||||
@@ -5221,20 +5223,24 @@
|
||||
btn.addEventListener('click', function () {
|
||||
var pk = Math.max(5, Math.min(120, parseInt((el('vote-timing-pick') || {}).value, 10) || 25));
|
||||
var tr = Math.max(5, Math.min(180, parseInt((el('vote-timing-trial') || {}).value, 10) || 30));
|
||||
var rv = Math.max(1500, Math.min(20000, parseInt((el('vote-timing-reveal') || {}).value, 10) || 7000));
|
||||
btn.disabled = true;
|
||||
gameTimingFetch('GET')
|
||||
.then(function (data) {
|
||||
data.pickVoteSec = pk;
|
||||
data.trialVoteSec = tr;
|
||||
data.trialResultRevealMs = rv;
|
||||
return gameTimingFetch('PUT', data);
|
||||
})
|
||||
.then(function () { return gameTimingFetch('GET'); })
|
||||
.then(function (fresh) {
|
||||
var p = (fresh && fresh.pickVoteSec != null) ? fresh.pickVoteSec : pk;
|
||||
var t = (fresh && fresh.trialVoteSec != null) ? fresh.trialVoteSec : tr;
|
||||
var r = (fresh && fresh.trialResultRevealMs != null) ? fresh.trialResultRevealMs : rv;
|
||||
if (el('vote-timing-pick')) el('vote-timing-pick').value = String(p);
|
||||
if (el('vote-timing-trial')) el('vote-timing-trial').value = String(t);
|
||||
setMsg('vote-timing-msg', 'บันทึกแล้ว — โหวตเลือกผู้เล่น ' + p + 's · ชี้ตัวคนร้าย ' + t + 's · ผู้เล่นต้องรีเฟรช/เข้าห้องใหม่', 'ok');
|
||||
if (el('vote-timing-reveal')) el('vote-timing-reveal').value = String(r);
|
||||
setMsg('vote-timing-msg', 'บันทึกแล้ว — โหวตเลือกผู้เล่น ' + p + 's · ชี้ตัวคนร้าย ' + t + 's · โชว์ผล ' + Math.round(r/1000) + 's · ผู้เล่นต้องรีเฟรช/เข้าห้องใหม่', 'ok');
|
||||
})
|
||||
.catch(function (e) { setMsg('vote-timing-msg', (e.message || 'บันทึกไม่ได้') + ' — เช็ค Node + /Game/api/game-timing', 'error'); })
|
||||
.then(function () { btn.disabled = false; });
|
||||
|
||||
@@ -1151,6 +1151,9 @@
|
||||
<label>โหวตชี้ตัวคนร้าย (พิจารณาคดี)
|
||||
<input type="number" id="vote-timing-trial" name="trialVoteSec" min="5" max="180" step="1" value="30"> วินาที
|
||||
</label>
|
||||
<label>โชว์ผลโหวต (คนร้ายถูก/ผิด) ก่อนไปหน้าผลเต็ม
|
||||
<input type="number" id="vote-timing-reveal" name="trialResultRevealMs" min="1500" max="20000" step="500" value="7000"> มิลลิวินาที (1000ms = 1 วิ)
|
||||
</label>
|
||||
<button type="button" class="btn btn-primary" id="btn-vote-timing-save">บันทึก</button>
|
||||
</form>
|
||||
<p id="vote-timing-msg" class="msg" role="status"></p>
|
||||
@@ -1330,7 +1333,7 @@
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<script src="admin.js?v=92"></script>
|
||||
<script src="admin.js?v=93"></script>
|
||||
<script>
|
||||
/* toggle: เปิด/ปิด HUD โหลด LobbyA (Iron Man) ให้ "ผู้เล่นทั่วไป" เห็น (เก็บใน game-timing.json: lobbyDebugHud)
|
||||
— dev เปิดเครื่องตัวเองได้เสมอด้วย ?dbg=1 หรือ localStorage.lobbyDbg='1' โดยไม่ต้องใช้ปุ่มนี้ */
|
||||
|
||||
@@ -51,9 +51,9 @@
|
||||
"providerUserId": "p_1775109142385_wq7wfy1p32j",
|
||||
"notes": "auto: player-coins",
|
||||
"blocked": false,
|
||||
"coins": 3090,
|
||||
"coins": 3156,
|
||||
"createdAt": "2026-04-02T05:52:21+00:00",
|
||||
"updatedAt": "2026-07-03T16:31:03+00:00",
|
||||
"updatedAt": "2026-07-04T13:18:23+00:00",
|
||||
"daily": {
|
||||
"anchorMs": 1781197200000,
|
||||
"claimedDays": [
|
||||
@@ -67,16 +67,16 @@
|
||||
],
|
||||
"lockUntilMs": 0
|
||||
},
|
||||
"score": 1016,
|
||||
"score": 1082,
|
||||
"scoreByCase": {
|
||||
"1": 26,
|
||||
"10": 978,
|
||||
"8": 306,
|
||||
"8": 324,
|
||||
"11": 20,
|
||||
"13": 368,
|
||||
"14": 30,
|
||||
"9": 40,
|
||||
"12": 476,
|
||||
"12": 524,
|
||||
"15": 20,
|
||||
"4": 20,
|
||||
"5": 32,
|
||||
@@ -157,7 +157,7 @@
|
||||
"blocked": false,
|
||||
"coins": 382,
|
||||
"createdAt": "2026-04-24T07:28:02+00:00",
|
||||
"updatedAt": "2026-07-03T16:38:57+00:00",
|
||||
"updatedAt": "2026-07-04T10:39:15+00:00",
|
||||
"score": 382,
|
||||
"scoreByCase": {
|
||||
"8": 220,
|
||||
@@ -169,7 +169,9 @@
|
||||
},
|
||||
"lbName": "Q",
|
||||
"achievements": {
|
||||
"d1_minigame_solver": 8
|
||||
"d1_minigame_solver": 8,
|
||||
"a1_first_deduction": 1,
|
||||
"a5_truth_hunter": 1
|
||||
},
|
||||
"lobbyColorThemeIndex": 6,
|
||||
"lobbySkinToneIndex": 1
|
||||
@@ -484,9 +486,9 @@
|
||||
"providerUserId": "p_1781851275186_szcmp5pu3s",
|
||||
"notes": "auto: player-coins",
|
||||
"blocked": false,
|
||||
"coins": 254,
|
||||
"coins": 290,
|
||||
"createdAt": "2026-06-19T06:41:15+00:00",
|
||||
"updatedAt": "2026-07-03T16:31:03+00:00",
|
||||
"updatedAt": "2026-07-04T13:18:23+00:00",
|
||||
"daily": {
|
||||
"anchorMs": 1781802000000,
|
||||
"claimedDays": [
|
||||
@@ -500,19 +502,24 @@
|
||||
],
|
||||
"lockUntilMs": 0
|
||||
},
|
||||
"score": 234,
|
||||
"score": 270,
|
||||
"scoreByCase": {
|
||||
"10": 70,
|
||||
"13": 36,
|
||||
"12": 82,
|
||||
"12": 108,
|
||||
"4": 28,
|
||||
"3": 18
|
||||
"3": 18,
|
||||
"8": 10
|
||||
},
|
||||
"lbName": "MONE",
|
||||
"achievements": {
|
||||
"d1_minigame_solver": 20,
|
||||
"a1_first_deduction": 1,
|
||||
"a5_truth_hunter": 1
|
||||
"a5_truth_hunter": 1,
|
||||
"e2_the_impostor": 1,
|
||||
"e4_agent_of_chaos": 1,
|
||||
"e3_master_of_doubt": 1,
|
||||
"e5_slippery_eel": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -717,21 +724,26 @@
|
||||
"providerUserId": "p_1782122326943_7p2fnvivscm",
|
||||
"notes": "auto: player-coins",
|
||||
"blocked": false,
|
||||
"coins": 216,
|
||||
"coins": 264,
|
||||
"createdAt": "2026-06-22T09:58:46+00:00",
|
||||
"updatedAt": "2026-07-03T16:31:03+00:00",
|
||||
"updatedAt": "2026-07-04T13:20:26+00:00",
|
||||
"lobbyColorThemeIndex": 3,
|
||||
"lobbySkinToneIndex": 1,
|
||||
"score": 206,
|
||||
"score": 254,
|
||||
"scoreByCase": {
|
||||
"10": 34,
|
||||
"12": 108,
|
||||
"12": 142,
|
||||
"3": 54,
|
||||
"13": 10
|
||||
"13": 10,
|
||||
"8": 14
|
||||
},
|
||||
"lbName": "MONE",
|
||||
"achievements": {
|
||||
"d1_minigame_solver": 20
|
||||
"d1_minigame_solver": 20,
|
||||
"e2_the_impostor": 1,
|
||||
"e4_agent_of_chaos": 1,
|
||||
"e3_master_of_doubt": 1,
|
||||
"e5_slippery_eel": 1
|
||||
},
|
||||
"daily": {
|
||||
"anchorMs": 1782147600000,
|
||||
@@ -755,20 +767,23 @@
|
||||
"providerUserId": "p_1782122328583_4mqrfx5txyo",
|
||||
"notes": "auto: player-coins",
|
||||
"blocked": false,
|
||||
"coins": 46,
|
||||
"coins": 92,
|
||||
"createdAt": "2026-06-22T09:58:48+00:00",
|
||||
"updatedAt": "2026-07-03T16:31:03+00:00",
|
||||
"updatedAt": "2026-07-04T13:20:26+00:00",
|
||||
"lobbyColorThemeIndex": 1,
|
||||
"lobbySkinToneIndex": 1,
|
||||
"score": 46,
|
||||
"score": 92,
|
||||
"scoreByCase": {
|
||||
"10": 20,
|
||||
"8": 18,
|
||||
"13": 8
|
||||
"8": 32,
|
||||
"13": 8,
|
||||
"12": 32
|
||||
},
|
||||
"lbName": "MONE",
|
||||
"achievements": {
|
||||
"d1_minigame_solver": 4
|
||||
"d1_minigame_solver": 12,
|
||||
"a1_first_deduction": 1,
|
||||
"a5_truth_hunter": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"gauntletTickMs": 220,
|
||||
"gauntletJumpTicks": 2,
|
||||
"gauntletJumpHeightMult": 1,
|
||||
"gauntletJumpTicks": 4,
|
||||
"gauntletJumpHeightMult": 0.52,
|
||||
"gauntletTimeLimitSec": 0,
|
||||
"troublesomeOfferSec": 15,
|
||||
"gauntletLaneImageUrls": [
|
||||
@@ -98,15 +98,16 @@
|
||||
"balloonBossBalloonsPerPlayer": 3,
|
||||
"forcedMinigameKeys": [
|
||||
"stack",
|
||||
"jump_survive",
|
||||
"balloon_boss"
|
||||
"gauntlet",
|
||||
"space_shooter"
|
||||
],
|
||||
"testSpecialCardByMap": {},
|
||||
"troublesomeForceOffer": false,
|
||||
"troublesomeRollMaxSec": 15,
|
||||
"troublesomeRollMaxSec": 8,
|
||||
"specialQuizIconExpireSec": 20,
|
||||
"pickVoteSec": 25,
|
||||
"trialVoteSec": 30,
|
||||
"trialResultRevealMs": 7000,
|
||||
"lobbyDebugHud": false,
|
||||
"lobbyDebugHudNames": []
|
||||
}
|
||||
@@ -926,6 +926,14 @@
|
||||
}).catch(function () {});
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
/** หมวด gamelog ตามเกมปัจจุบัน (MG1-MG7) */
|
||||
function gameLogCatForPlay() {
|
||||
try {
|
||||
var gt = mapData && mapData.gameType;
|
||||
var m = { quiz: 'MG1', gauntlet: 'MG2', stack: 'MG3', quiz_carry: 'MG4', jump_survive: 'MG5', space_shooter: 'MG6', balloon_boss: 'MG7' };
|
||||
return (gt && m[gt]) || 'อื่นๆ';
|
||||
} catch (e) { return 'อื่นๆ'; }
|
||||
}
|
||||
/** วินิจฉัยสี block MG3 ต่อ seat — เทียบ "สี avatar (playTint)" vs "สี block (resolved)"; ถ้าไม่ตรง → ส่งเข้า /gamelog
|
||||
เรียกเอง: window.__mg3ColorLog() หรือ auto throttle */
|
||||
let _mg3ColorLogAt = 0;
|
||||
@@ -3137,8 +3145,10 @@
|
||||
const diskSet = playCharDiskLayersSet(charId);
|
||||
if (!diskSet) {
|
||||
// manifest ยังไม่พร้อม — ลองใหม่อีกครู่ (กันยิง layer ที่ไม่มี = 404)
|
||||
// เครื่องช้า/เปิดหลายหน้าต่าง manifest มาช้า >3.6วิ → เดิมยอมแพ้ 8 ครั้ง = char ไม่ warm (avatar ก้อนเขียวค้าง)
|
||||
// ขยายเป็น ~15วิ (30×500ms) ให้เครื่องช้าโหลด avatar สำเร็จ
|
||||
const t = _tries || 0;
|
||||
if (t < 8) setTimeout(function () { preloadPlayCharacterLayers(charId, t + 1); }, 450);
|
||||
if (t < 30) setTimeout(function () { preloadPlayCharacterLayers(charId, t + 1); }, 500);
|
||||
return;
|
||||
}
|
||||
_playPreloadedChars.add(charId);
|
||||
@@ -7634,6 +7644,12 @@
|
||||
}
|
||||
|
||||
function stackTowerMissionElapsedSecPlay() {
|
||||
/* ยึดเวลาเริ่มจริงของ server (playMissionLiveBeginsAt) + serverNow() → ทุกจอเห็น TIME ตรงกัน
|
||||
แม้เข้า/reconnect ช้า (เดิมใช้ performance.now() local = จอที่เข้าทีหลังนับเวลาเริ่มใหม่ → TIME 90 vs 8) */
|
||||
if (playMissionLiveBeginsAt > 0) {
|
||||
const el = (serverNow() - playMissionLiveBeginsAt) / 1000;
|
||||
if (Number.isFinite(el) && el >= 0) return el;
|
||||
}
|
||||
if (stackTowerSessionStartAt <= 0) return 0;
|
||||
return (performance.now() - stackTowerSessionStartAt) / 1000;
|
||||
}
|
||||
@@ -10108,6 +10124,27 @@
|
||||
let netReconnecting = false;
|
||||
let netPingTimer = null;
|
||||
let netOkHideTimer = null;
|
||||
/* เน็ตแย่ = กด Ready มินิเกมไม่ได้ (กันเริ่มเกมตอน sync ยังไม่พร้อม) — มี safety กันค้าง */
|
||||
let netBadSinceMs = 0;
|
||||
const NET_READY_BLOCK_SAFETY_MS = 15000; /* เน็ตแย่ต่อเนื่องเกินนี้ → ปลดล็อกให้กดได้ (กัน soft-lock) */
|
||||
function isNetBadNowPlay() {
|
||||
return netReconnecting || (socket && socket.connected === false) || (netLastRtt != null && netLastRtt >= NET_RTT_BAD_MS);
|
||||
}
|
||||
/** true = เน็ตแย่จน "ยังไม่ควรให้กด Ready" (แต่ถ้าแย่ต่อเนื่องเกิน safety → คืน false ปลดล็อก) */
|
||||
function isNetTooBadToReadyPlay() {
|
||||
if (!isNetBadNowPlay()) return false;
|
||||
if (!netBadSinceMs) return true; /* เพิ่งแย่ */
|
||||
return (Date.now() - netBadSinceMs) < NET_READY_BLOCK_SAFETY_MS;
|
||||
}
|
||||
let _netWaitToastAt = 0;
|
||||
function showNetWaitToastPlay() {
|
||||
const now = Date.now();
|
||||
if (now - _netWaitToastAt < 1500) return; /* กันสแปม */
|
||||
_netWaitToastAt = now;
|
||||
try { showCrownStartBlockedNoticePlay({ error: 'กำลังรอสัญญาณให้เสถียร… อีกครู่กดพร้อมได้' }); } catch (_e) { /* ignore */ }
|
||||
try { setNetStatus('bad', 'กำลังปรับสัญญาณให้ลื่นขึ้น — รอสักครู่ค่อยกดพร้อม', true); } catch (_e2) { /* ignore */ }
|
||||
try { gamelogPost(gameLogCatForPlay(), 'net-ready-block', '[TC175] เน็ตแย่ → บล็อกกด Ready (block+safety 15วิ) · rtt=' + (netLastRtt == null ? '?' : Math.round(netLastRtt)) + ' reconn=' + netReconnecting); } catch (_e3) { /* ignore */ }
|
||||
}
|
||||
function ensureNetStatusEl() {
|
||||
if (netStatusEl) return netStatusEl;
|
||||
const css = document.createElement('style');
|
||||
@@ -10147,6 +10184,11 @@
|
||||
}
|
||||
}
|
||||
function refreshNetStatus() {
|
||||
/* จับ/ปล่อยเวลาเริ่มแย่ (ใช้คุม safety ปลดล็อกปุ่ม Ready) + refresh ปุ่ม how-to ให้ปลดล็อกเองเมื่อเน็ตกลับมา */
|
||||
const badNow = isNetBadNowPlay();
|
||||
if (badNow && !netBadSinceMs) netBadSinceMs = Date.now();
|
||||
else if (!badNow && netBadSinceMs) netBadSinceMs = 0;
|
||||
try { if (typeof syncGauntletCrownJumpButton === 'function') { /* no-op guard */ } if (typeof refreshGauntletCrownHowtoReadyButtonsPlay === 'function') refreshGauntletCrownHowtoReadyButtonsPlay(); } catch (_e) { /* ignore */ }
|
||||
if (netReconnecting || (socket && socket.connected === false)) {
|
||||
setNetStatus('bad', 'กำลังเชื่อมต่อใหม่…', true);
|
||||
return;
|
||||
@@ -13885,10 +13927,32 @@
|
||||
btn.title = allReady
|
||||
? (isMePlayHost() ? 'START' : 'รอโฮสต์เริ่ม')
|
||||
: (meReady ? 'ยกเลิก READY' : 'READY');
|
||||
/* เน็ตแย่ + ยังไม่กด Ready → ล็อกปุ่มไว้ก่อน (block+safety 15วิ) โชว์คำสุภาพ · เน็ตกลับมา/ครบ safety = ปลดเอง */
|
||||
if (!locked && !meReady && !allReady && isNetTooBadToReadyPlay()) {
|
||||
btn.disabled = true;
|
||||
btn.classList.add('is-net-blocked');
|
||||
btn.style.filter = 'grayscale(0.72) brightness(0.6)';
|
||||
btn.style.cursor = 'not-allowed';
|
||||
btn.title = 'กำลังรอสัญญาณให้เสถียร… อีกครู่กดพร้อมได้';
|
||||
} else {
|
||||
btn.classList.remove('is-net-blocked');
|
||||
btn.style.removeProperty('filter');
|
||||
btn.style.removeProperty('cursor');
|
||||
}
|
||||
updateGauntletCrownReadyAvatars(); /* โชว์หน้าคนที่กด ready (#2) */
|
||||
return allReady;
|
||||
}
|
||||
|
||||
/** refresh ปุ่ม Ready how-to (ใช้ตอนเน็ตกลับมา → ปลดล็อกเอง) — หยิบ st/btn ปัจจุบันแล้ว apply ซ้ำ */
|
||||
function refreshGauntletCrownHowtoReadyButtonsPlay() {
|
||||
try {
|
||||
const btn = (typeof getGauntletCrownHowtoBtnEl === 'function') ? getGauntletCrownHowtoBtnEl() : document.getElementById('btn-gch-ready');
|
||||
if (!btn || btn.classList.contains('is-hidden')) return;
|
||||
const st = (typeof getGauntletCrownHowtoStatusEl === 'function') ? getGauntletCrownHowtoStatusEl() : document.getElementById('gauntlet-crown-howto-status');
|
||||
applyGauntletCrownReadyButtonPlay(st, btn);
|
||||
} catch (_e) { /* ignore */ }
|
||||
}
|
||||
|
||||
/** วาดหน้าตัวละครจริง (tinted) ในกรอบ player-pic-frame — ใช้ใต้ปุ่ม READY (โชว์คนที่กด ready) */
|
||||
function renderReadyAvatarInto(el, id) {
|
||||
var info = (typeof specialQuizResolveCharTint === 'function') ? specialQuizResolveCharTint(id) : { characterId: '', tint: null };
|
||||
@@ -21325,6 +21389,18 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
/* full-sync หัวใจ + decrypt% (server-auth) → self-correct จอที่พลาด packet stack-drop
|
||||
(แก้ "หัวใจไม่ลดบางที / % ค้าง") · หัวใจปรับลงเท่านั้น, % เดินหน้าเท่านั้น (กัน flicker) · ไม่แตะช่วงกำลัง drop */
|
||||
if (stackMini && !stackFall && !stackMini.settling) {
|
||||
if (d.teamMissesLeft != null && stackMini.teamMissesLeft != null) {
|
||||
const hm = Math.max(0, Math.floor(Number(d.teamMissesLeft)));
|
||||
if (Number.isFinite(hm) && hm < stackMini.teamMissesLeft) stackMini.teamMissesLeft = hm;
|
||||
}
|
||||
if (d.progressPct != null) {
|
||||
const pp = Number(d.progressPct);
|
||||
if (Number.isFinite(pp) && pp >= 0) stackMini.progressPct = Math.max(stackMini.progressPct || 0, Math.min(100, pp));
|
||||
}
|
||||
}
|
||||
logStackSeatColors('client-recv');
|
||||
});
|
||||
|
||||
@@ -24710,35 +24786,20 @@
|
||||
}
|
||||
|
||||
function syncMg2MockJumpButtonBoundsPlay(stackRect, scale) {
|
||||
/* ปุ่ม action ทุกเกมใช้ตำแหน่ง/ขนาดมาตรฐานแบบ MG3 (มุมขวาล่าง, CSS base #gauntlet-crown-jump-btn)
|
||||
— เดิมวางปุ่มไว้ในภาพ mock-HUD (1648*s,818*s ขนาด 231*s) เฉพาะ gauntlet_crown/jumper/mega_virus
|
||||
ทำให้ตำแหน่ง/ขนาดไม่เท่ากันทุกเกม. ตอนนี้เคลียร์ inline เสมอ → ปุ่มกลับไปใช้ base (เท่า MG3 ทุกเกม) */
|
||||
const jumpBtn = document.getElementById('gauntlet-crown-jump-btn');
|
||||
const hud = document.getElementById('play-cyber-hud');
|
||||
const _jbSkin = hud ? hud.dataset.missionMockSkin : '';
|
||||
if (!jumpBtn || !hud || (_jbSkin !== 'gauntlet_crown' && _jbSkin !== 'jumper' && _jbSkin !== 'mega_virus')) {
|
||||
if (jumpBtn) {
|
||||
jumpBtn.style.removeProperty('left');
|
||||
jumpBtn.style.removeProperty('top');
|
||||
jumpBtn.style.removeProperty('width');
|
||||
jumpBtn.style.removeProperty('height');
|
||||
jumpBtn.style.removeProperty('right');
|
||||
jumpBtn.style.removeProperty('bottom');
|
||||
jumpBtn.style.removeProperty('transform');
|
||||
jumpBtn.style.removeProperty('filter');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (jumpBtn.classList.contains('is-hidden')) return;
|
||||
const r = stackRect || (document.getElementById('play-canvas-stack') || {}).getBoundingClientRect?.();
|
||||
const s = Number.isFinite(scale) ? scale : _quizTfScaleLast;
|
||||
if (!r || !(s > 0)) return;
|
||||
jumpBtn.style.position = 'fixed';
|
||||
jumpBtn.style.left = Math.round(r.left + 1648 * s) + 'px';
|
||||
jumpBtn.style.top = Math.round(r.top + 818 * s) + 'px';
|
||||
jumpBtn.style.width = Math.round(231 * s) + 'px';
|
||||
jumpBtn.style.height = Math.round(231 * s) + 'px';
|
||||
jumpBtn.style.right = 'auto';
|
||||
jumpBtn.style.bottom = 'auto';
|
||||
jumpBtn.style.transform = 'none';
|
||||
jumpBtn.style.filter = 'drop-shadow(0 0 ' + Math.max(8, Math.round(18 * s)) + 'px rgba(255, 110, 220, 0.45))';
|
||||
if (!jumpBtn) return;
|
||||
jumpBtn.style.removeProperty('left');
|
||||
jumpBtn.style.removeProperty('top');
|
||||
jumpBtn.style.removeProperty('width');
|
||||
jumpBtn.style.removeProperty('height');
|
||||
jumpBtn.style.removeProperty('right');
|
||||
jumpBtn.style.removeProperty('bottom');
|
||||
jumpBtn.style.removeProperty('transform');
|
||||
jumpBtn.style.removeProperty('filter');
|
||||
jumpBtn.style.removeProperty('position');
|
||||
}
|
||||
|
||||
function clearQuizTfMockHudBoundsPlay(root) {
|
||||
@@ -27180,6 +27241,11 @@
|
||||
|
||||
(function wireGauntletCrownHowtoPrimary() {
|
||||
function onHowtoPrimaryClick() {
|
||||
/* เน็ตแย่ + ยังไม่กด Ready → บล็อกการกดพร้อม (block+safety 15วิ) โชว์คำสุภาพ · ไม่บล็อกการยกเลิก/START */
|
||||
if (isNetTooBadToReadyPlay()) {
|
||||
const meReadyNow = !!(myId != null && gauntletCrownLobbyReadyMap[String(myId)]);
|
||||
if (!meReadyNow && !gauntletCrownAllReadyPlay()) { showNetWaitToastPlay(); return; }
|
||||
}
|
||||
if (isJumpSurviveMissionUiMapPlay()) {
|
||||
if (jumpSurviveMissionPhase !== 'howto') return;
|
||||
const humans = quizCarryPregameHumanIds();
|
||||
|
||||
@@ -87,10 +87,16 @@
|
||||
dbgRender();
|
||||
setTimeout(function () { el.style.opacity = '0'; setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 450); }, 600);
|
||||
}
|
||||
/* อ่าน flag จาก admin (game-timing.json) — เปิด HUD ให้ทุกคน หรือเฉพาะชื่อที่ระบุ */
|
||||
/* เวลาโชว์ผลโหวตคนร้าย (ถูก/ผิด) ก่อนตัดไปหน้าผลเต็ม — default 7000ms, admin override ผ่าน game-timing.json */
|
||||
var trialResultRevealMs = 7000;
|
||||
/* อ่าน flag จาก admin (game-timing.json) — เปิด HUD ให้ทุกคน หรือเฉพาะชื่อที่ระบุ + เวลาโชว์ผลโหวต */
|
||||
try {
|
||||
fetch(BASE + '/data/game-timing.json?_=' + Date.now()).then(function (r) { return r.json(); }).then(function (g) {
|
||||
if (!g || !g.lobbyDebugHud) return;
|
||||
if (!g) return;
|
||||
/* เวลาโชว์ผลโหวต (คนร้ายถูก/ผิด) ก่อนตัดไปหน้าผลเต็ม — admin ปรับได้ (default 7000ms) */
|
||||
var trm = Number(g.trialResultRevealMs);
|
||||
if (Number.isFinite(trm) && trm >= 1500 && trm <= 20000) trialResultRevealMs = Math.round(trm);
|
||||
if (!g.lobbyDebugHud) return;
|
||||
var names = Array.isArray(g.lobbyDebugHudNames) ? g.lobbyDebugHudNames.map(function (s) { return String(s).trim().toLowerCase(); }).filter(Boolean) : [];
|
||||
var me = ''; try { me = (getProfileDisplayName() || '').trim().toLowerCase(); } catch (e) { /* ignore */ }
|
||||
_dbgAdmin = (names.length === 0) || names.indexOf(me) >= 0;
|
||||
@@ -293,11 +299,15 @@
|
||||
let suspectCardMinigames = [];
|
||||
/** หลักฐาน (ติกถูก) ที่เก็บได้ต่อผู้ต้องสงสัย 0–2 (เล่นมินิเกมจบ +1 ต่อช่อง สูงสุด 3) */
|
||||
let suspectProgress = [0, 0, 0];
|
||||
/* #6: ความคืบหน้าการสืบต่อผู้ต้องสงสัย (shared จาก server — เท่ากันทุกจอ) = จำนวนมินิเกมที่เล่นให้คนนั้น 0-3
|
||||
ใช้โชว์ช่องหลักฐานใต้การ์ด suspect แทนหลักฐานส่วนตัว (myPlayerEvidence) ที่แต่ละคนเห็นไม่เท่ากัน */
|
||||
let suspectTeamProgress = [0, 0, 0];
|
||||
/** การ์ดหลักฐานที่ผู้เล่นคนนี้เก็บได้ — [suspectIdx][cardIdx 0–2] จากเซิร์ฟ */
|
||||
let myPlayerEvidence = [[], [], []];
|
||||
/** ขั้นพิจารณาคดี: null | 'voting' | 'revealed' */
|
||||
let trialMode = null;
|
||||
let myTrialVote = null;
|
||||
let trialVoteConfirmed = false; /* #7: กด "ยืนยันโหวต" แล้ว → ล็อก ห้ามเปลี่ยน + ส่งจริง (barrier รอทุกคน) */
|
||||
let trialExcludedSuspect = null; /* รอบจับผิดตัว: ผู้ต้องสงสัยที่โหวตผิดไปแล้ว — ห้ามเลือกซ้ำ */
|
||||
let trialSilencedMe = false; /* ฉันถูกปิดปาก (Silence) — ห้ามโหวตรอบนี้ */
|
||||
let trialVoteCounts = [0, 0, 0];
|
||||
@@ -1852,7 +1862,8 @@
|
||||
ctx.fillText('🔇', cx, voiceIconY);
|
||||
ctx.textBaseline = 'alphabetic';
|
||||
}
|
||||
if (p.ready && lobbyReadyIconImg && lobbyReadyIconImg.complete && lobbyReadyIconImg.naturalWidth) {
|
||||
/* LobbyB ไม่มีเฟส "กดพร้อมเริ่มเกม" แบบ LobbyA → ไม่โชว์ ✓ ready เหนือหัว (รกจอ ไม่มีความหมาย) */
|
||||
if (p.ready && !isPostCaseLobbyRoom() && lobbyReadyIconImg && lobbyReadyIconImg.complete && lobbyReadyIconImg.naturalWidth) {
|
||||
var nwI = lobbyReadyIconImg.naturalWidth;
|
||||
var nhI = lobbyReadyIconImg.naturalHeight;
|
||||
if (nwI > 0 && nhI > 0) {
|
||||
@@ -4903,6 +4914,13 @@
|
||||
else if (myPlayerEvidence && myPlayerEvidence.some(function (row) { return row && row.length; })) {
|
||||
setSuspectProgress([0, 1, 2].map(function (i) { return (myPlayerEvidence[i] || []).length; }));
|
||||
}
|
||||
/* #6: เก็บ team progress (shared) — ใช้เป็นตัวหลักในการวาดช่องหลักฐาน (เท่ากันทุกจอ) */
|
||||
if (data && Array.isArray(data.suspectTeamProgress)) {
|
||||
suspectTeamProgress = [0, 1, 2].map(function (i) {
|
||||
var v = Math.floor(Number(data.suspectTeamProgress[i]));
|
||||
return (Number.isFinite(v) && v > 0) ? Math.min(3, v) : 0;
|
||||
});
|
||||
}
|
||||
applySuspectProgressVisual();
|
||||
/* อัปเดต UI host เร็วทันที — ปุ่ม "ชี้ตัวคนร้าย" ต้องโผล่ทันทีเมื่อสืบครบ */
|
||||
if (suspectPickOverlayOpen) updateSuspectHostUi();
|
||||
@@ -5274,8 +5292,12 @@
|
||||
for (var i = 0; i <= 2; i++) {
|
||||
var card = document.querySelector('.suspect-card[data-index="' + i + '"]');
|
||||
if (!card) continue;
|
||||
var count = getSuspectEvidenceCount(i);
|
||||
if (!count && suspectProgress && suspectProgress[i] > 0) count = suspectProgress[i];
|
||||
/* #6: team progress (shared) เป็นหลัก → ทุกจอเห็นเท่ากัน · fallback หลักฐานส่วนตัวเฉพาะตอน server ยังไม่ส่ง */
|
||||
var count = (suspectTeamProgress && suspectTeamProgress[i] > 0) ? suspectTeamProgress[i] : 0;
|
||||
if (!count) {
|
||||
count = getSuspectEvidenceCount(i);
|
||||
if (!count && suspectProgress && suspectProgress[i] > 0) count = suspectProgress[i];
|
||||
}
|
||||
card.classList.toggle('suspect-card--evidence-full', count >= 3);
|
||||
var list = card.querySelector('.suspect-symbol-list');
|
||||
if (!list) continue;
|
||||
@@ -5350,7 +5372,12 @@
|
||||
'.suspect-card--innocent{filter:grayscale(1) brightness(.8)}' +
|
||||
'#trial-lose-txt{position:absolute;left:50%;top:4%;transform:translateX(-50%);width:712px;max-width:88vw;height:auto;z-index:61;display:none}' +
|
||||
'#trial-result-banner{position:absolute;left:50%;top:5%;transform:translateX(-50%);background:rgba(6,12,24,.96);border:2px solid #22d3ee;border-radius:16px;padding:14px 26px;color:#e7ecff;font:600 20px/1.5 Kanit,system-ui,sans-serif;text-align:center;z-index:60;max-width:82%;box-shadow:0 8px 40px rgba(0,0,0,.6)}' +
|
||||
'#trial-reveal-btn{position:absolute;left:50%;bottom:5%;transform:translateX(-50%);background:linear-gradient(180deg,#22d3ee,#0e7490);color:#06121f;border:none;border-radius:14px;padding:14px 30px;font:700 20px/1 Kanit,system-ui,sans-serif;cursor:pointer;z-index:60;box-shadow:0 6px 22px rgba(34,211,238,.5)}' +
|
||||
/* #7: reveal (host) ย้ายไปมุมขวาล่าง เลี่ยงทับปุ่มยืนยันกลางล่าง */
|
||||
'#trial-reveal-btn{position:absolute;right:4%;bottom:5%;left:auto;transform:none;background:linear-gradient(180deg,#22d3ee,#0e7490);color:#06121f;border:none;border-radius:14px;padding:12px 22px;font:700 17px/1 Kanit,system-ui,sans-serif;cursor:pointer;z-index:60;box-shadow:0 6px 22px rgba(34,211,238,.5)}' +
|
||||
/* #7: ปุ่มยืนยันโหวต (ทุกคน) กลางล่าง — เขียว, disabled=รอคนอื่น */
|
||||
'#trial-confirm-btn{position:absolute;left:50%;bottom:5%;transform:translateX(-50%);background:linear-gradient(180deg,#34d399,#059669);color:#04140d;border:none;border-radius:16px;padding:15px 40px;font:800 21px/1 Kanit,system-ui,sans-serif;cursor:pointer;z-index:61;box-shadow:0 8px 26px rgba(52,211,153,.55);display:none}' +
|
||||
'#trial-confirm-btn:disabled{filter:grayscale(.5) brightness(.8);cursor:default;font-weight:700;font-size:17px}' +
|
||||
'#trial-confirm-btn:not(:disabled):active{transform:translateX(-50%) scale(.96)}' +
|
||||
// รอบจับผิดตัว: ใบที่โหวตผิดไปแล้ว — หรี่ + กันคลิก + ป้าย "โหวตผิดไปแล้ว"
|
||||
'.suspect-card--excluded{filter:grayscale(1) brightness(.55);pointer-events:none;position:relative}' +
|
||||
'.suspect-card--excluded .trial-vote-badge{display:none!important}' +
|
||||
@@ -5489,8 +5516,10 @@
|
||||
if (sub) sub.src = (mode === 'result') ? (RV + 'roomvote-txt-3.png') : (RV + 'roomvote-txt-2.png');
|
||||
}
|
||||
|
||||
/* #7: คลิกการ์ด = "เลือก" อย่างเดียว (ยังไม่ส่ง) — เปลี่ยนใจได้จนกว่าจะกด "ยืนยันโหวต" */
|
||||
function castTrialVote(idx) {
|
||||
if (trialMode !== 'voting') return;
|
||||
if (trialVoteConfirmed) { appendLobbySystemChat('— ยืนยันโหวตไปแล้ว รอผู้เล่นอื่น'); return; }
|
||||
if (trialSilencedMe) { appendLobbySystemChat('— คุณถูกปิดปาก (Silence) · โหวตรอบนี้ไม่ได้'); return; }
|
||||
if (trialExcludedSuspect != null && idx === trialExcludedSuspect) {
|
||||
appendLobbySystemChat('— ผู้ต้องสงสัยหมายเลข ' + (idx + 1) + ' ถูกโหวตไปแล้ว (ผิด) · เลือกคนอื่น');
|
||||
@@ -5503,14 +5532,50 @@
|
||||
var b = c.querySelector('.trial-vote-badge');
|
||||
if (b && sel) b.textContent = '✓ เลือกแล้ว';
|
||||
});
|
||||
socket.emit('trial-vote', { index: idx }, function (res) {
|
||||
if (!(res && res.ok)) appendLobbySystemChat('— โหวตไม่สำเร็จ' + (res && res.error ? (' · ' + res.error) : ''));
|
||||
ensureTrialConfirmBtn(); /* โชว์ปุ่มยืนยันเมื่อเลือกแล้ว */
|
||||
}
|
||||
|
||||
/* #7: กดยืนยัน → ส่ง trial-vote จริง + ล็อกเปลี่ยนใจ · server รอทุกคน (บอท auto, banned ข้าม) ค่อย reveal */
|
||||
function confirmTrialVote() {
|
||||
if (trialMode !== 'voting' || trialVoteConfirmed || myTrialVote == null) return;
|
||||
trialVoteConfirmed = true;
|
||||
var cb = document.getElementById('trial-confirm-btn');
|
||||
if (cb) { cb.disabled = true; cb.textContent = 'รอผู้เล่นอื่นยืนยัน…'; }
|
||||
document.querySelectorAll('.suspect-card').forEach(function (c) { c.style.pointerEvents = 'none'; });
|
||||
socket.emit('trial-vote', { index: myTrialVote }, function (res) {
|
||||
if (!(res && res.ok)) {
|
||||
/* ส่งไม่สำเร็จ → ปลดล็อกให้เลือก/ยืนยันใหม่ */
|
||||
trialVoteConfirmed = false;
|
||||
document.querySelectorAll('.suspect-card').forEach(function (c) { c.style.pointerEvents = ''; });
|
||||
if (cb) { cb.disabled = false; cb.textContent = 'ยืนยันโหวต ✓'; }
|
||||
appendLobbySystemChat('— โหวตไม่สำเร็จ' + (res && res.error ? (' · ' + res.error) : ''));
|
||||
}
|
||||
});
|
||||
appendLobbySystemChat('— ยืนยันโหวตแล้ว · รอผู้เล่นอื่นยืนยันให้ครบ');
|
||||
try { lobbyLog('trial-confirm', '[TC180] กดยืนยันโหวต (แยก select/confirm) → ส่งจริง'); } catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function ensureTrialConfirmBtn() {
|
||||
var ov = document.getElementById('suspect-pick-overlay');
|
||||
var btn = document.getElementById('trial-confirm-btn');
|
||||
if (!btn && ov) {
|
||||
btn = document.createElement('button');
|
||||
btn.id = 'trial-confirm-btn';
|
||||
btn.type = 'button';
|
||||
btn.textContent = 'ยืนยันโหวต ✓';
|
||||
btn.addEventListener('click', confirmTrialVote);
|
||||
ov.appendChild(btn);
|
||||
}
|
||||
if (btn) {
|
||||
btn.style.display = (trialMode === 'voting' && myTrialVote != null && !trialVoteConfirmed) ? 'block' : 'none';
|
||||
if (!trialVoteConfirmed) { btn.disabled = false; btn.textContent = 'ยืนยันโหวต ✓'; }
|
||||
}
|
||||
}
|
||||
|
||||
function openTrialVoteUI(data) {
|
||||
trialMode = 'voting';
|
||||
myTrialVote = null;
|
||||
trialVoteConfirmed = false; /* #7: รอบโหวตใหม่ → ยังไม่ยืนยัน */
|
||||
trialExcludedSuspect = (data && data.revote && Number.isInteger(data.excludedSuspect)) ? data.excludedSuspect : null;
|
||||
trialSilencedMe = !!(data && data.silenced);
|
||||
trialVoteCounts = [0, 0, 0];
|
||||
@@ -5528,8 +5593,10 @@
|
||||
ov.setAttribute('aria-hidden', 'false');
|
||||
document.querySelectorAll('.suspect-card').forEach(function (c) {
|
||||
c.classList.remove('suspect-card--myvote', 'suspect-card--culprit', 'suspect-card--selected', 'suspect-card--innocent', 'suspect-card--excluded');
|
||||
c.style.pointerEvents = ''; /* #7: ปลดล็อกการคลิก (เผื่อรอบก่อนล็อกไว้ตอนยืนยัน) */
|
||||
var xb = c.querySelector('.trial-excluded-badge'); if (xb) xb.remove();
|
||||
});
|
||||
ensureTrialConfirmBtn(); /* #7: reset ปุ่มยืนยัน (ซ่อนจนกว่าจะเลือก) */
|
||||
/* รอบจับผิดตัว: ตีตราใบที่โหวตผิดไปแล้ว + ห้ามเลือกซ้ำ */
|
||||
if (trialExcludedSuspect != null) {
|
||||
document.querySelectorAll('.suspect-card').forEach(function (c) {
|
||||
@@ -5665,12 +5732,16 @@
|
||||
}
|
||||
var rb = document.getElementById('trial-reveal-btn');
|
||||
if (rb) rb.style.display = 'none';
|
||||
var cfb = document.getElementById('trial-confirm-btn'); /* #7: ซ่อนปุ่มยืนยันตอน reveal */
|
||||
if (cfb) cfb.style.display = 'none';
|
||||
document.querySelectorAll('.suspect-card').forEach(function (c) { c.style.pointerEvents = ''; });
|
||||
var hint = document.getElementById('suspect-pick-hint');
|
||||
if (hint) hint.textContent = 'กำลังสรุปผล...';
|
||||
appendLobbySystemChat('— เปิดเผยผล: คนร้ายคือผู้ต้องสงสัยหมายเลข ' + (culprit + 1) + ' · ผู้ตอบถูก: ' + names);
|
||||
// โชว์คะแนนโหวตสักครู่ แล้วไปหน้าผลเต็มเสมอ (ถูก = เรือนจำไซเบอร์ / ผิด/ไม่โหวต = ภารกิจล้มเหลว)
|
||||
try { lobbyLog('trial-reveal-dur', '[TC174] โชว์ผลโหวต ' + trialResultRevealMs + 'ms (admin ปรับได้) ก่อนไปหน้าผลเต็ม'); } catch (e) { /* ignore */ }
|
||||
if (trialFinalTimer) clearTimeout(trialFinalTimer);
|
||||
trialFinalTimer = setTimeout(function () { showFinalResult(correct, culprit, anyVotes); }, 2600);
|
||||
trialFinalTimer = setTimeout(function () { showFinalResult(correct, culprit, anyVotes); }, trialResultRevealMs);
|
||||
}
|
||||
|
||||
var trialFinalTimer = null;
|
||||
@@ -7048,10 +7119,17 @@
|
||||
});
|
||||
socket.on('trial-vote-update', (data) => {
|
||||
if (data && Array.isArray(data.counts)) trialVoteCounts = data.counts; // เก็บไว้โชว์ตอนเปิดเผยผล (ไม่โชว์ต่อใบตอนโหวต)
|
||||
if (trialMode === 'voting') setTrialCounter((data && data.voted) || 0, (data && data.totalPlayers) || 0);
|
||||
var voted = (data && data.voted) || 0;
|
||||
var total = (data && data.totalPlayers) || 0;
|
||||
if (trialMode === 'voting') setTrialCounter(voted, total);
|
||||
const hint = document.getElementById('suspect-pick-hint');
|
||||
if (hint && trialMode === 'voting') {
|
||||
hint.textContent = 'คลิกการ์ดที่คิดว่าเป็นคนร้าย';
|
||||
hint.textContent = trialVoteConfirmed ? ('รอผู้เล่นอื่นยืนยัน…') : 'คลิกการ์ดที่คิดว่าเป็นคนร้าย แล้วกด “ยืนยันโหวต”';
|
||||
}
|
||||
/* #7: หลังยืนยันแล้ว โชว์ความคืบหน้าบนปุ่ม (x/total ยืนยันแล้ว) */
|
||||
if (trialVoteConfirmed) {
|
||||
var cfb = document.getElementById('trial-confirm-btn');
|
||||
if (cfb && total > 0) cfb.textContent = 'รอผู้เล่นอื่น… (' + voted + '/' + total + ')';
|
||||
}
|
||||
});
|
||||
socket.on('trial-result', (data) => {
|
||||
@@ -7862,6 +7940,50 @@
|
||||
if (path.length <= 1) return;
|
||||
lobbyPath = path.slice(1);
|
||||
}
|
||||
/* #4 มือถือ: แตะ "ปุ่ม START GAME / วัตถุกลางแมป" ที่เรนเดอร์จริง → ทำ action เลย (แทนเดินไปยืน+กด F)
|
||||
คืน true ถ้าแตะโดนจุด interact (จะไม่เดินไปที่แตะ) */
|
||||
function lobbyTapWorldXYPlay(clientX, clientY) {
|
||||
if (!mapData || !canvas || !mapTransform) return null;
|
||||
const r = canvas.getBoundingClientRect();
|
||||
if (!r.width || !r.height) return null;
|
||||
const sx = (clientX - r.left) * (canvas.width / r.width);
|
||||
const sy = (clientY - r.top) * (canvas.height / r.height);
|
||||
const { cw, ch, lobbyZoom: z, tileSize: t, meX, meY } = mapTransform;
|
||||
if (!(z > 0) || !(t > 0)) return null;
|
||||
return { gx: (sx - cw / 2) / (z * t) + meX, gy: (sy - ch / 2) / (z * t) + meY };
|
||||
}
|
||||
function lobbyTapInteractPlay(clientX, clientY) {
|
||||
const wp = lobbyTapWorldXYPlay(clientX, clientY);
|
||||
if (!wp) return false;
|
||||
const gx = wp.gx, gy = wp.gy;
|
||||
/* 1) ปุ่มกลาง (START GAME / เปิดผู้ต้องสงสัย) = gridImageSprite[0] — แตะโดนกรอบปุ่ม (+เผื่อ 0.4 ช่อง) */
|
||||
const sp = lobbyButtonSprite();
|
||||
if (sp) {
|
||||
const bx = Math.floor(sp.x), by = Math.floor(sp.y);
|
||||
const bw = Math.max(1, Math.floor(sp.w) || 1), bh = Math.max(1, Math.floor(sp.h) || 1);
|
||||
if (gx >= bx - 0.4 && gx < bx + bw + 0.4 && gy >= by - 0.4 && gy < by + bh + 0.4) {
|
||||
try { lobbyLog('mobile-center-btn', '[TC177] แตะปุ่ม START/center โดยตรง (มือถือ ไม่ใช้ F)'); } catch (e) { /* ignore */ }
|
||||
if (typeof isPostCaseLobbyRoom === 'function' && isPostCaseLobbyRoom() && serverSuspectPhaseActive && !suspectPickOverlayOpen) {
|
||||
openSuspectOverlay(typeof suspectSelectedIndex === 'number' ? suspectSelectedIndex : 0); return true;
|
||||
}
|
||||
if (typeof isCurrentRoomLobbyA === 'function' && isCurrentRoomLobbyA()) {
|
||||
if (hostId !== socket.id) { appendLobbySystemChat('— เฉพาะ Host กดเริ่มเกม'); return true; }
|
||||
if (!lobbyAllHumansReady()) { appendLobbySystemChat('— รอให้ทุกคนกดพร้อมก่อน'); return true; }
|
||||
openLobbyPreplayWizard(); return true;
|
||||
}
|
||||
return true; /* แตะโดนปุ่มแล้วแม้ยังไม่ถึงเงื่อนไข — ไม่เดินไปที่ปุ่ม */
|
||||
}
|
||||
}
|
||||
/* 2) ช่องแต่งตัว (เขียว) / Host Console (ชมพู) — แตะใกล้ ~1.3 ช่อง (แทน F มือถือที่ถอดออก) */
|
||||
const near = function (spot) {
|
||||
if (!spot) return false;
|
||||
const ddx = gx - (spot.x + 0.5), ddy = gy - (spot.y + 0.5);
|
||||
return (ddx * ddx + ddy * ddy) <= 1.6;
|
||||
};
|
||||
if (near(ROOM_CZ_SPOT)) { openRoomCustomize(); return true; }
|
||||
if (near(ROOM_HC_SPOT)) { openHostConsoleIfHost(); return true; }
|
||||
return false;
|
||||
}
|
||||
if (canvas) {
|
||||
try { canvas.style.touchAction = 'none'; } catch (_e) { /* กัน browser zoom/pan รบกวนการแตะเดิน */ }
|
||||
let _lobbyTapStart = null;
|
||||
@@ -7876,6 +7998,8 @@
|
||||
_lobbyTapStart = null;
|
||||
/* แตะสั้นๆ ไม่ลาก = เดิน · ลาก/กดค้าง = ข้าม (กันชนกับการ scroll/zoom) */
|
||||
if (dx * dx + dy * dy > 24 * 24 || dt > 700) return;
|
||||
/* #4: แตะปุ่ม START GAME/interact ที่เรนเดอร์ → ทำ action เลย (ไม่เดินไปที่ปุ่ม) */
|
||||
if (lobbyTapInteractPlay(e.clientX, e.clientY)) return;
|
||||
lobbyWalkToClientXY(e.clientX, e.clientY);
|
||||
});
|
||||
canvas.addEventListener('pointercancel', () => { _lobbyTapStart = null; });
|
||||
@@ -7889,32 +8013,74 @@
|
||||
canvas.addEventListener('dblclick', (e) => { lobbyWalkToClientXY(e.clientX, e.clientY); });
|
||||
}
|
||||
|
||||
/* มือถือ (touch-primary) ไม่มีคีย์บอร์ด → เพิ่มปุ่ม "โต้ตอบ" แทนปุ่ม F (เดินไปยืนใกล้จุด แล้วกดปุ่มนี้)
|
||||
dispatch synthetic KeyF → ใช้ handler interaction เดิม (host เลือกระดับ/คดี, ห้องแต่งตัว ฯลฯ) — PC ไม่เห็นปุ่มนี้ */
|
||||
(function setupMobileLobbyInteractBtn() {
|
||||
try {
|
||||
/* ปุ่ม F มือถือ (มุมล่างซ้าย) ถอดออกแล้ว — มือถือใช้ "แตะปุ่ม/วัตถุกลางแมปโดยตรง" (lobbyTapInteractPlay) แทน
|
||||
เผื่อมีปุ่มค้างจาก build เก่า/cache → ลบทิ้ง */
|
||||
(function removeLegacyMobileLobbyInteractBtn() {
|
||||
try { var old = document.getElementById('lobby-mobile-interact-btn'); if (old && old.parentNode) old.parentNode.removeChild(old); } catch (_e) { /* ignore */ }
|
||||
})();
|
||||
|
||||
/* #4 (rework 2026-07-04): เลิกใช้ปุ่ม DOM กลางล่าง — user อยากแตะ "ปุ่ม START GAME ที่เรนเดอร์จริง" โดยตรง
|
||||
(ทำใน lobbyTapInteractPlay แล้ว) · เก็บฟังก์ชันไว้แต่ปิดการสร้างปุ่ม + ลบปุ่มค้างจาก build เก่า */
|
||||
(function setupMobileLobbyCenterActionBtn() {
|
||||
try { var oldC = document.getElementById('lobby-mobile-center-btn'); if (oldC && oldC.parentNode) oldC.parentNode.removeChild(oldC); } catch (_e) { /* ignore */ }
|
||||
return; // eslint-disable-line no-unreachable
|
||||
try { // eslint-disable-line no-unreachable
|
||||
var coarse = window.matchMedia && window.matchMedia('(pointer: coarse)').matches;
|
||||
if (!coarse) return;
|
||||
if (document.getElementById('lobby-mobile-interact-btn')) return;
|
||||
if (document.getElementById('lobby-mobile-center-btn')) return;
|
||||
var CS = '/Main-Lobby/IMAGE/case/btn-startcase.png';
|
||||
var SU = '/Main-Lobby/IMAGE/Choose%20a%20suspect/symbol-card.png';
|
||||
var btn = document.createElement('button');
|
||||
btn.id = 'lobby-mobile-interact-btn';
|
||||
btn.id = 'lobby-mobile-center-btn';
|
||||
btn.type = 'button';
|
||||
btn.setAttribute('aria-label', 'โต้ตอบ (F)');
|
||||
btn.innerHTML = '<span class="lmi-key">F</span><span class="lmi-txt">โต้ตอบ</span>';
|
||||
btn.style.cssText = 'position:fixed;left:max(1rem,env(safe-area-inset-left));bottom:max(1.1rem,env(safe-area-inset-bottom));'
|
||||
+ 'z-index:410;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;'
|
||||
+ 'width:64px;height:64px;border-radius:50%;border:2px solid #ffd166;background:rgba(20,28,48,.86);'
|
||||
+ 'color:#ffe9a8;font-family:inherit;cursor:pointer;box-shadow:0 4px 14px rgba(0,0,0,.45);touch-action:manipulation;';
|
||||
btn.style.cssText = 'position:fixed;left:50%;bottom:max(1.4rem,env(safe-area-inset-bottom));transform:translateX(-50%);'
|
||||
+ 'z-index:5400;display:none;align-items:center;gap:9px;padding:11px 22px 11px 14px;border-radius:44px;'
|
||||
+ 'border:2px solid #ffd166;background:rgba(16,22,40,.93);color:#ffe9a8;font:800 17px/1 Kanit,system-ui,sans-serif;'
|
||||
+ 'cursor:pointer;box-shadow:0 7px 22px rgba(0,0,0,.5);touch-action:manipulation;animation:lmcbPulse 1.8s ease-in-out infinite;';
|
||||
var img = document.createElement('img');
|
||||
img.alt = '';
|
||||
img.style.cssText = 'height:38px;width:auto;max-width:84px;object-fit:contain;display:block;';
|
||||
var label = document.createElement('span');
|
||||
btn.appendChild(img);
|
||||
btn.appendChild(label);
|
||||
var st = document.createElement('style');
|
||||
st.textContent = '#lobby-mobile-interact-btn .lmi-key{font-weight:800;font-size:20px;line-height:1}'
|
||||
+ '#lobby-mobile-interact-btn .lmi-txt{font-size:11px;line-height:1;opacity:.92}'
|
||||
+ '#lobby-mobile-interact-btn:active{transform:scale(.93);background:rgba(255,209,102,.25)}';
|
||||
st.textContent = '@keyframes lmcbPulse{0%,100%{box-shadow:0 7px 22px rgba(0,0,0,.5)}50%{box-shadow:0 7px 30px rgba(255,209,102,.6)}}'
|
||||
+ '#lobby-mobile-center-btn:active{transform:translateX(-50%) scale(.95)}';
|
||||
document.head.appendChild(st);
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
try { document.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyF', key: 'f', bubbles: true })); } catch (_e) { /* ignore */ }
|
||||
var mode = btn.dataset.mode || '';
|
||||
try { lobbyLog('mobile-center-btn', '[TC177] กดปุ่มกลางมือถือ (แทน F) mode=' + mode); } catch (e) { /* ignore */ }
|
||||
if (mode === 'start') {
|
||||
if (typeof lobbyAllHumansReady === 'function' && !lobbyAllHumansReady()) { appendLobbySystemChat('— รอให้ทุกคนกดพร้อมก่อน'); return; }
|
||||
if (typeof openLobbyPreplayWizard === 'function') openLobbyPreplayWizard();
|
||||
} else if (mode === 'suspect') {
|
||||
if (typeof openSuspectOverlay === 'function') openSuspectOverlay(typeof suspectSelectedIndex === 'number' ? suspectSelectedIndex : 0);
|
||||
}
|
||||
});
|
||||
document.body.appendChild(btn);
|
||||
function refreshCenterBtn() {
|
||||
var show = false, mode = '', src = '', txt = '';
|
||||
try {
|
||||
if (typeof isPostCaseLobbyRoom === 'function' && isPostCaseLobbyRoom()
|
||||
&& serverSuspectPhaseActive && !suspectPickOverlayOpen) {
|
||||
show = true; mode = 'suspect'; src = SU; txt = 'ผู้ต้องสงสัย';
|
||||
} else if (typeof isCurrentRoomLobbyA === 'function' && isCurrentRoomLobbyA()
|
||||
&& hostId === socket.id && typeof lobbyAllHumansReady === 'function' && lobbyAllHumansReady()) {
|
||||
show = true; mode = 'start'; src = CS; txt = 'เริ่มเกม';
|
||||
}
|
||||
} catch (_e) { show = false; }
|
||||
if (show) {
|
||||
btn.dataset.mode = mode;
|
||||
if (img.getAttribute('src') !== src) img.src = src;
|
||||
if (label.textContent !== txt) label.textContent = txt;
|
||||
if (btn.style.display !== 'flex') btn.style.display = 'flex';
|
||||
} else if (btn.style.display !== 'none') {
|
||||
btn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
setInterval(refreshCenterBtn, 500);
|
||||
refreshCenterBtn();
|
||||
} catch (_e) { /* ignore */ }
|
||||
})();
|
||||
|
||||
|
||||
@@ -4339,14 +4339,10 @@
|
||||
color: #ff8a8a;
|
||||
text-shadow: 0 0 8px rgba(255, 90, 90, 0.55);
|
||||
}
|
||||
/* ปุ่ม jump ทุกเกม (รวม MG2/MG5/MG7 mock-hud) ใช้ตำแหน่ง/ขนาดมาตรฐานแบบ MG3 (มุมขวาล่างวงกลม จาก base
|
||||
#gauntlet-crown-jump-btn) — ไม่ override position/ขนาดอีก · คง glow ธีมชมพูไว้เฉยๆ */
|
||||
html.play-mg2-mock-hud #gauntlet-crown-jump-btn.mg2-jump:not(.is-hidden) {
|
||||
position: fixed !important;
|
||||
right: auto !important;
|
||||
bottom: auto !important;
|
||||
z-index: 56;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
z-index: 59;
|
||||
filter: drop-shadow(0 0 18px rgba(255, 110, 220, 0.45));
|
||||
}
|
||||
html.play-mg2-mock-hud #gauntlet-crown-jump-btn.mg2-jump:not(.is-hidden):active {
|
||||
@@ -5462,7 +5458,7 @@
|
||||
<script src="/Game/socket.io/socket.io.js"></script>
|
||||
<script src="js/version.js?v=0.0306"></script>
|
||||
<script src="js/sound.js?v=1"></script>
|
||||
<script src="js/play.js?v=0.006301075"></script>
|
||||
<script src="js/play.js?v=0.006301080"></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)
|
||||
|
||||
@@ -1649,7 +1649,7 @@
|
||||
<script src="js/customize-popup.js?v=40" data-customize-triggers="" data-customize-asset-base="img/03-5-Customize"></script>
|
||||
<script src="js/achievements.js?v=0.002" data-asset-base="img/03-6-Profile"></script>
|
||||
<script src="js/sound.js?v=1"></script>
|
||||
<script src="js/room-lobby.js?v=0.0345"></script>
|
||||
<script src="js/room-lobby.js?v=0.0351"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+73
-9
@@ -2282,6 +2282,8 @@ function defaultGameTiming() {
|
||||
pickVoteSec: 25,
|
||||
/** เวลาโหวตชี้ตัวคนร้าย (พิจารณาคดี) วินาที */
|
||||
trialVoteSec: 30,
|
||||
/** เวลาโชว์ผลโหวตคนร้าย (ถูก/ผิด) ก่อนตัดไปหน้าผลเต็ม — มิลลิวินาที */
|
||||
trialResultRevealMs: 7000,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2707,6 +2709,9 @@ function loadGameTiming() {
|
||||
trialVoteSec: Object.prototype.hasOwnProperty.call(j, 'trialVoteSec')
|
||||
? Math.max(5, Math.min(180, Math.floor(Number(j.trialVoteSec)) || 30))
|
||||
: 30,
|
||||
trialResultRevealMs: Object.prototype.hasOwnProperty.call(j, 'trialResultRevealMs')
|
||||
? Math.max(1500, Math.min(20000, Math.floor(Number(j.trialResultRevealMs)) || 7000))
|
||||
: 7000,
|
||||
};
|
||||
}
|
||||
} catch (e) { console.error('loadGameTiming', e.message); }
|
||||
@@ -2937,6 +2942,9 @@ function saveGameTimingToFile(d) {
|
||||
trialVoteSec: Object.prototype.hasOwnProperty.call(d, 'trialVoteSec')
|
||||
? Math.max(5, Math.min(180, Math.floor(Number(d.trialVoteSec)) || 30))
|
||||
: (prev.trialVoteSec != null ? Math.max(5, Math.min(180, Math.floor(Number(prev.trialVoteSec)) || 30)) : 30),
|
||||
trialResultRevealMs: Object.prototype.hasOwnProperty.call(d, 'trialResultRevealMs')
|
||||
? Math.max(1500, Math.min(20000, Math.floor(Number(d.trialResultRevealMs)) || 7000))
|
||||
: (prev.trialResultRevealMs != null ? Math.max(1500, Math.min(20000, Math.floor(Number(prev.trialResultRevealMs)) || 7000)) : 7000),
|
||||
/* DEV: เปิด HUD โหลด LobbyA (Iron Man) — admin toggle. preserve ค่าเดิมถ้า request ไม่ส่งมา */
|
||||
lobbyDebugHud: Object.prototype.hasOwnProperty.call(d, 'lobbyDebugHud')
|
||||
? !!d.lobbyDebugHud
|
||||
@@ -3717,8 +3725,12 @@ function assignDetectiveSuspectCardMinigames(space) {
|
||||
gameType: (md && md.gameType) || '',
|
||||
};
|
||||
});
|
||||
// ชุดการ์ดใหม่ = ล้างหลักฐานที่ผู้เล่นเก็บได้
|
||||
// ชุดการ์ดใหม่ = ล้างหลักฐานที่ผู้เล่นเก็บได้ (รวม evidenceByNick fallback — กันหลักฐานคดีเก่า leak ข้ามคดี)
|
||||
space.playerEvidence = {};
|
||||
space.evidenceByNick = {};
|
||||
/* #6: ความคืบหน้าการสืบต่อผู้ต้องสงสัย (shared — เท่ากันทุกคน) = จำนวนมินิเกมที่เล่นให้คนนั้น (0-3)
|
||||
ใช้โชว์ช่องหลักฐานใต้การ์ด suspect แทนหลักฐานส่วนตัว (เดิมแต่ละคนเห็นไม่เท่ากัน) */
|
||||
space.suspectTeamProgress = [0, 0, 0];
|
||||
// สุ่มคนร้ายตัวจริง 1 ใน 3 ใบ (เก็บฝั่ง server ไม่ส่งให้ client จนกว่าจะเปิดเผย)
|
||||
space.culpritIndex = Math.floor(Math.random() * 3);
|
||||
space.trialPhase = null; // null | 'voting' | 'revealed'
|
||||
@@ -3743,11 +3755,15 @@ function normalizeEvidenceSlotList(cards) {
|
||||
}
|
||||
|
||||
/** ดึง nickname normalized ของผู้เล่นบน space (จากแถว peers) — ใช้คีย์เก็บหลักฐานข้ามการ reconnect */
|
||||
/* คีย์เก็บหลักฐานต่อผู้เล่น — ใช้ playerKey (unique ต่อ browser) เป็นหลัก กัน "ชื่อซ้ำ"
|
||||
(เช่น 2-3 คนชื่อ MONE) ใช้ evidence ก้อนเดียวกัน = ถูกแจกซ้ำ/เกิน. fallback ชื่อเฉพาะกรณีไม่มี key */
|
||||
function peerNickKey(space, playerId) {
|
||||
if (!space || !playerId) return '';
|
||||
const p = space.peers && space.peers.get ? space.peers.get(playerId) : null;
|
||||
if (!p || !p.nickname) return '';
|
||||
return normalizeLobbyNickname(p.nickname);
|
||||
if (!p) return '';
|
||||
if (typeof p.playerKey === 'string' && /^[a-zA-Z0-9_-]{8,128}$/.test(p.playerKey)) return 'k:' + p.playerKey;
|
||||
if (!p.nickname) return '';
|
||||
return 'n:' + normalizeLobbyNickname(p.nickname);
|
||||
}
|
||||
|
||||
/** สำเนาหลักฐาน — fallback ไป evidenceByNick ถ้าไม่มีตาม socket.id (กรณี reconnect socket ใหม่) */
|
||||
@@ -3812,10 +3828,13 @@ function awardRandomEvidenceCardToPlayer(space, playerId, suspectIndex, grade) {
|
||||
|
||||
function awardDetectiveEvidenceForMinigameEnd(space, suspectIndex, playerIds, grade) {
|
||||
const out = {};
|
||||
const grantedKeys = new Set(); /* แจก 1 การ์ด/playerKey ต่อเกม — กัน "หลายแท็บ/ชื่อซ้ำ" key เดียวได้เกิน */
|
||||
(playerIds || []).forEach((pid) => {
|
||||
if (!space.peers.has(pid)) return;
|
||||
const k = peerNickKey(space, pid);
|
||||
if (k && grantedKeys.has(k)) return; /* key นี้ได้การ์ดรอบนี้แล้ว (evidence เก็บก้อนเดียวกันตาม key) */
|
||||
const card = awardRandomEvidenceCardToPlayer(space, pid, suspectIndex, grade);
|
||||
if (card != null) out[pid] = card;
|
||||
if (card != null) { out[pid] = card; if (k) grantedKeys.add(k); }
|
||||
});
|
||||
return out;
|
||||
}
|
||||
@@ -3854,10 +3873,13 @@ function grantDetectiveEvidenceForCurrentRun(sid, space, playerIds) {
|
||||
const def = specialCardDef(7);
|
||||
freeEvidenceCount = (def && def.evidence) || 1;
|
||||
for (let n = 0; n < freeEvidenceCount; n++) {
|
||||
const freeKeys = new Set(); /* 1 ใบ/playerKey ต่อรอบ (กันชื่อซ้ำ/หลายแท็บได้เกิน) */
|
||||
ids.forEach((pid) => {
|
||||
if (!space.peers.has(pid)) return;
|
||||
const k = peerNickKey(space, pid);
|
||||
if (k && freeKeys.has(k)) return;
|
||||
const extra = awardRandomEvidenceCardToPlayer(space, pid, suspectIdx, grade);
|
||||
if (extra != null) { awardedList[pid].push(extra); if (awarded[pid] == null) awarded[pid] = extra; }
|
||||
if (extra != null) { awardedList[pid].push(extra); if (awarded[pid] == null) awarded[pid] = extra; if (k) freeKeys.add(k); }
|
||||
});
|
||||
}
|
||||
pend.consumed = true;
|
||||
@@ -3865,6 +3887,17 @@ function grantDetectiveEvidenceForCurrentRun(sid, space, playerIds) {
|
||||
space.detectiveAwardDoneForRun = true;
|
||||
space.lastDetectiveInvestigatedSuspect = suspectIdx;
|
||||
space.lastEvidenceGrant = { suspectIdx, awarded, awardedList, grade, freeEvidenceCount };
|
||||
/* [TC182] verify: จำนวนใบต่อ playerKey ที่แจกรอบนี้ — ควรได้ 1 ใบ/key/เกม (กันชื่อซ้ำแจกเกิน) */
|
||||
try {
|
||||
const keyCounts = {};
|
||||
Object.keys(awardedList).forEach((pid) => {
|
||||
const k = peerNickKey(space, pid) || String(pid);
|
||||
keyCounts[k] = (keyCounts[k] || 0) + (awardedList[pid] ? awardedList[pid].length : 0);
|
||||
});
|
||||
const vals = Object.values(keyCounts);
|
||||
const over = vals.some((n) => n > 1);
|
||||
glog(sid, space, 'evidence-grant', '[TC182] แจกหลักฐาน suspect#' + (suspectIdx + 1) + ' → ' + vals.length + ' key · ใบ/key=' + JSON.stringify(vals) + (over ? ' ⚠️เกิน' : ' (1/key · pass)'));
|
||||
} catch (e) { /* ignore */ }
|
||||
emitLobbyEvidenceSync(sid, space);
|
||||
return { suspectIdx, awarded, awardedList, grade, alreadyGranted: false, freeEvidenceCount };
|
||||
}
|
||||
@@ -4066,6 +4099,12 @@ function returnDetectiveSpaceToLobbyB(sid, space, message, awardOptions) {
|
||||
const suspectIdx = resolveDetectiveSuspectAwardIndex(space);
|
||||
if (suspectIdx >= 0) {
|
||||
space.lastDetectiveInvestigatedSuspect = suspectIdx;
|
||||
/* #6: นับความคืบหน้าการสืบ (shared) +1 ต่อการเล่นมินิเกม 1 รอบของผู้ต้องสงสัยนั้น (สูงสุด 3) */
|
||||
if (!Array.isArray(space.suspectTeamProgress)) space.suspectTeamProgress = [0, 0, 0];
|
||||
if (suspectIdx >= 0 && suspectIdx <= 2) {
|
||||
space.suspectTeamProgress[suspectIdx] = Math.min(3, (space.suspectTeamProgress[suspectIdx] || 0) + 1);
|
||||
try { glog(sid, space, 'suspect-progress', '[TC179] สืบผู้ต้องสงสัย #' + (suspectIdx + 1) + ' → team progress ' + space.suspectTeamProgress.join('/') + ' (shared เท่ากันทุกจอ · pass)'); } catch (e) { /* ignore */ }
|
||||
}
|
||||
let awardIds = [];
|
||||
if (awardOptions && awardOptions.allPlayers) {
|
||||
awardIds = [...space.peers.keys()];
|
||||
@@ -4116,6 +4155,7 @@ function returnDetectiveSpaceToLobbyB(sid, space, message, awardOptions) {
|
||||
suspectPhaseActive: true,
|
||||
suspectPickIndex: typeof space.suspectPickIndex === 'number' ? space.suspectPickIndex : 0,
|
||||
cardMinigames: space.suspectCardMinigames || [],
|
||||
suspectTeamProgress: Array.isArray(space.suspectTeamProgress) ? space.suspectTeamProgress.slice() : [0, 0, 0], /* #6 shared */
|
||||
detectiveReturn: true,
|
||||
};
|
||||
emitDetectiveLobbyReturnToPeers(sid, space, message, basePayload);
|
||||
@@ -8001,7 +8041,7 @@ const STACK_MAX_CENTER_DRIFT_FRAC_OF_LAND = 0.38;
|
||||
const STACK_MAX_CENTER_DRIFT_NO_LAND_MULT = 1.0;
|
||||
const STACK_TOWER_BLOCK_WORLD_SCALE = 1.5;
|
||||
const STACK_TICK_MS = 60;
|
||||
const STACK_DROP_BUSY_MS = 1500; /* รอแอนิเมชันบล็อกร่วง/นิ่งบน client ก่อนถึงตาถัดไป */
|
||||
const STACK_DROP_BUSY_MS = 1200; /* รอแอนิเมชันบล็อกร่วง/นิ่งบน client ก่อนถึงตาถัดไป (ลดจาก 1500 + swing เดินต่อระหว่าง busy → ไม่ค้าง) */
|
||||
const STACK_BOT_THINK_MIN_MS = 560;
|
||||
const STACK_BOT_THINK_MAX_MS = 1180;
|
||||
const STACK_SEATS = 6;
|
||||
@@ -8284,6 +8324,10 @@ function emitStackStateServer(sid, space) {
|
||||
idx: s.turnIndex,
|
||||
hostId: null,
|
||||
lthemes,
|
||||
/* full-sync หัวใจ + decrypt% → client ที่พลาด packet stack-drop (ช้า/churn) self-correct
|
||||
(เดิมส่งเฉพาะตอน drop → พลาดแล้ว "หัวใจไม่ลด/% ไม่ตรง" ค้าง) */
|
||||
teamMissesLeft: s.teamMissesLeft,
|
||||
progressPct: s.progressPct,
|
||||
});
|
||||
}
|
||||
function stackAdvanceTurnServer(s) {
|
||||
@@ -8317,6 +8361,15 @@ function stackSessionEnd(sid, space, outcome) {
|
||||
if (p) { p.reportedMiniScore = Math.max(0, s.humanPts[e.id] | 0); p.reportedMiniSurvived = true; }
|
||||
}
|
||||
});
|
||||
/* [TC183] verify คะแนน MG3: team score + คะแนนต่อคน (ดูว่าสูงผิดปกติ/ซ้ำจากชื่อซ้ำไหม) */
|
||||
try {
|
||||
const per = s.turnOrder.map((e) => {
|
||||
if (e.kind === 'bot' && e.botId) { const b = s.bots.get(e.botId); return 'bot=' + Math.max(0, (b && b.score) | 0); }
|
||||
if (e.id != null) { const p = space.peers.get(e.id); return ((p && p.nickname) || e.id).toString().slice(0, 6) + '=' + Math.max(0, s.humanPts[e.id] | 0); }
|
||||
return '?';
|
||||
});
|
||||
glog(sid, space, 'stack-score', '[TC183] จบ MG3 outcome=' + outcome + ' team=' + (s.score | 0) + ' layers=' + s.layers.length + ' progress=' + Math.floor(s.progressPct || 0) + '% · ต่อคน: ' + per.join(' '));
|
||||
} catch (e) { /* ignore */ }
|
||||
try { space.lastStackMissionGrade = stackSessionMissionGrade(space); } catch (_e) { space.lastStackMissionGrade = null; }
|
||||
const overPayload = { outcome: typeof outcome === 'string' ? outcome.slice(0, 32) : 'time_up' };
|
||||
io.to(sid).emit('stack-over', overPayload);
|
||||
@@ -8333,8 +8386,16 @@ function runStackTick(sid, space) {
|
||||
if (s.teamMissesLeft != null && s.teamMissesLeft <= 0) { stackSessionEnd(sid, space, 'misses_exhausted'); return; }
|
||||
if ((now - s.startMs) >= stackTimeLimitSecServer() * 1000) { stackSessionEnd(sid, space, 'time_up'); return; }
|
||||
if (!s.turnOrder.length) return;
|
||||
/* รอแอนิเมชันบล็อกก่อนหน้า (busy) → ไม่แกว่ง/ไม่ดรอป */
|
||||
if (now < s.busyUntil) return;
|
||||
/* บล็อกก่อนหน้ากำลังร่วง (busy) — ยังดรอปไม่ได้ (drop handler เช็ค busyUntil) แต่ให้ "บล็อกถัดไปแกว่งต่อ"
|
||||
(advance phase + emit swing) → ตาถัดไปไม่เห็นบล็อกห้อยนิ่ง = ไม่ค้าง · client มี guard (!stackFall) ไม่ให้กระตุกตอนยังร่วง */
|
||||
if (now < s.busyUntil) {
|
||||
s.phase += s.phaseSpeed * (STACK_TICK_MS / 1000) * Math.PI * 2;
|
||||
if (now - s.lastSwingEmit > 50) {
|
||||
s.lastSwingEmit = now;
|
||||
io.to(sid).emit('stack-swing', { phase: s.phase, speed: s.phaseSpeed, swingAmp: s.swingAmp, topCenterX: s.topCenterX, t: now });
|
||||
}
|
||||
return;
|
||||
}
|
||||
const entry = stackCurrentTurnEntry(s);
|
||||
if (!entry) return;
|
||||
if (entry.kind === 'bot' && entry.botId) {
|
||||
@@ -10907,6 +10968,7 @@ io.on('connection', (socket) => {
|
||||
cardMinigames: space.suspectCardMinigames || [],
|
||||
myPlayerEvidence: clonePlayerEvidence(space, socket.id),
|
||||
suspectProgress: playerSuspectProgressFromEvidence(space, socket.id),
|
||||
suspectTeamProgress: Array.isArray(space.suspectTeamProgress) ? space.suspectTeamProgress.slice() : [0, 0, 0], /* #6 shared */
|
||||
botSlotCount: effectiveBotSlotCount(space),
|
||||
lobbyBotThemes: syncLobbyBotThemeSlots(space),
|
||||
joinLocked: !!space.joinLocked,
|
||||
@@ -11791,7 +11853,9 @@ io.on('connection', (socket) => {
|
||||
space.trialVotes[socket.id] = idx;
|
||||
emitTrialVoteUpdate(sid, space);
|
||||
const totalTV = trialEligibleVoterTotal(space);
|
||||
if (Object.keys(space.trialVotes).length >= totalTV && totalTV > 0) {
|
||||
const votedNow = Object.keys(space.trialVotes).length;
|
||||
try { glog(sid, space, 'trial-vote', '[TC180] ยืนยันโหวต ' + votedNow + '/' + totalTV + (votedNow >= totalTV && totalTV > 0 ? ' — ครบ → reveal (pass)' : ' — รออีก')); } catch (e) { /* ignore */ }
|
||||
if (votedNow >= totalTV && totalTV > 0) {
|
||||
computeAndEmitTrialResult(sid, space);
|
||||
}
|
||||
reply({ ok: true, index: idx });
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1348,5 +1348,145 @@
|
||||
"status": "รอทดสอบ",
|
||||
"updated": "",
|
||||
"note": "ต้องเทส 2+ คน · log tag [TC173]"
|
||||
},
|
||||
{
|
||||
"n": 174,
|
||||
"cat": "LobbyB",
|
||||
"name": "Vote reveal โชว์ผลนานขึ้น (7วิ) + admin ปรับได้",
|
||||
"detail": "โหวตจบ → จอ \"คนร้ายตัวจริงคือ...\" โชว์ ~7วิ ก่อนไปหน้าผลเต็ม (เดิม 2.6วิ) · ปรับที่ admin→Vote timing→โชว์ผลโหวต · verify: gamelog ev=trial-reveal-dur [TC174]",
|
||||
"prio": "กลาง",
|
||||
"status": "รอทดสอบ",
|
||||
"updated": "",
|
||||
"note": "เทสด้วยตา+log · admin game-timing.trialResultRevealMs"
|
||||
},
|
||||
{
|
||||
"n": 175,
|
||||
"cat": "ระบบ disconnect",
|
||||
"name": "เน็ตแย่ → บล็อกกด Ready + safety 15วิ",
|
||||
"detail": "ในหน้า how-to มินิเกม ถ้าเน็ตแย่ (RTT≥700/reconnecting) ปุ่ม Ready เทากดไม่ได้ + toast \"กำลังรอสัญญาณให้เสถียร\" · เน็ตกลับ/ครบ15วิ ปลดล็อกเอง · verify: gamelog ev=net-ready-block [TC175]",
|
||||
"prio": "กลาง",
|
||||
"status": "รอทดสอบ",
|
||||
"updated": "",
|
||||
"note": "throttle เน็ต (Slow 3G) เทส · client log"
|
||||
},
|
||||
{
|
||||
"n": 176,
|
||||
"cat": "LobbyB",
|
||||
"name": "LobbyB ไม่โชว์ ✓ ready เหนือหัว",
|
||||
"detail": "LobbyB ไม่มีเฟสกดพร้อม → ไม่วาด ✓ เหนือหัวตัวละคร (LobbyA ยังมี) · เทสด้วยตา",
|
||||
"prio": "กลาง",
|
||||
"status": "รอทดสอบ",
|
||||
"updated": "",
|
||||
"note": "visual only"
|
||||
},
|
||||
{
|
||||
"n": 177,
|
||||
"cat": "LobbyB",
|
||||
"name": "มือถือ ปุ่มกลาง(รูป) แทนกด F",
|
||||
"detail": "coarse pointer: LobbyA host+พร้อมครบ=ปุ่ม เริ่มเกม กลางล่าง · LobbyB เฟสสืบสวน=ปุ่ม ผู้ต้องสงสัย · กดทำ action เลย ไม่ต้องเดินไปยืน+F · อยู่คู่ปุ่ม F ล่างซ้าย · PC ไม่เห็น · verify: gamelog ev=mobile-center-btn [TC177]",
|
||||
"prio": "กลาง",
|
||||
"status": "รอทดสอบ",
|
||||
"updated": "",
|
||||
"note": "เทสมือถือ + client log"
|
||||
},
|
||||
{
|
||||
"n": 178,
|
||||
"cat": "Minigame 2",
|
||||
"name": "ปุ่ม action ทุกเกม = แบบ MG3 (มุมขวาล่าง)",
|
||||
"detail": "MG2/5/7 ปุ่มกระโดด/ยิง ใช้ position/size เดียวกับ MG3 (#stack-tower-drop-btn มุมขวาล่างวงกลม) · เทสด้วยตา ไม่ทับ art HUD",
|
||||
"prio": "กลาง",
|
||||
"status": "รอทดสอบ",
|
||||
"updated": "",
|
||||
"note": "visual only"
|
||||
},
|
||||
{
|
||||
"n": 179,
|
||||
"cat": "LobbyB",
|
||||
"name": "การ์ดหลักฐาน LobbyB เท่ากันทุกจอ (team progress)",
|
||||
"detail": "ช่องหลักฐานใต้การ์ดผู้ต้องสงสัยใช้ team progress (shared server) ไม่ใช่หลักฐานส่วนตัว → ทุกจอเห็นเท่ากัน (เดิมจริง1 บางคนเห็น3) · verify: gamelog ev=suspect-progress [TC179]",
|
||||
"prio": "กลาง",
|
||||
"status": "รอทดสอบ",
|
||||
"updated": "",
|
||||
"note": "เทส 4 คน + log"
|
||||
},
|
||||
{
|
||||
"n": 180,
|
||||
"cat": "LobbyB",
|
||||
"name": "Vote ยืนยันก่อนส่ง (barrier รอทุกคน)",
|
||||
"detail": "เลือกการ์ด (เปลี่ยนใจได้) → กด ยืนยันโหวต ✓ → ล็อก · รอทุกคนยืนยันครบ (บอท auto/แบนข้าม) ค่อย reveal · verify: gamelog ev=trial-vote [TC180] + trial-confirm",
|
||||
"prio": "กลาง",
|
||||
"status": "รอทดสอบ",
|
||||
"updated": "",
|
||||
"note": "เทส 2+ คน + log"
|
||||
},
|
||||
{
|
||||
"n": 182,
|
||||
"cat": "ระบบ disconnect",
|
||||
"name": "หลักฐานแจกเกินตอนชื่อซ้ำ (MONE) — 1 การ์ด/คน",
|
||||
"detail": "ชื่อซ้ำหลายจอ → หลักฐานเก็บด้วย nickname ชนกันแจกเกิน · fix: key ด้วย playerKey + dedup 1/key/เกม · verify gamelog [TC182] ใบ/key=[1,1,1,1]",
|
||||
"prio": "กลาง",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 183,
|
||||
"cat": "Minigame 3",
|
||||
"name": "MG3 คะแนนต่อคนปกติ (ไม่ inflate)",
|
||||
"detail": "เช็คคะแนน MG3 ไม่สูงผิดปกติ/ไม่ซ้ำจากชื่อซ้ำ · verify gamelog [TC183] team + ต่อคน (เช่น 50/50/40/30 = ปกติ)",
|
||||
"prio": "กลาง",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 184,
|
||||
"cat": "Minigame 3",
|
||||
"name": "MG3 เปลี่ยนตาไม่ค้าง (turn-hang)",
|
||||
"detail": "พอไปตาคนต่อไปเดิมค้าง ~1.5วิ (swing แช่แข็ง) · fix: swing เดินต่อระหว่าง busy + ลด 1500→1200",
|
||||
"prio": "กลาง",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 185,
|
||||
"cat": "Minigame 3",
|
||||
"name": "MG3 หัวใจ/decrypt% sync ตรงทุกจอ",
|
||||
"detail": "เดิมหัวใจไม่ลดบางจอ (full-sync ไม่ส่ง teamMissesLeft) · fix: full-sync ส่งหัวใจ+% + client self-correct",
|
||||
"prio": "กลาง",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 186,
|
||||
"cat": "Minigame 3",
|
||||
"name": "MG3 TIME ตรงทุกจอ (ไม่ 90 vs 8)",
|
||||
"detail": "เดิมจอเข้าช้า TIME เริ่มใหม่ (local clock) · fix: ยึด server anchor (playMissionLiveBeginsAt)",
|
||||
"prio": "กลาง",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 187,
|
||||
"cat": "Minigame 3",
|
||||
"name": "MG3 avatar โหลดสำเร็จบนเครื่องช้า",
|
||||
"detail": "avatar ก้อนเขียวตอนเครื่องช้า (preload ยอมแพ้ 3.6วิ) · fix: retry ขยาย 15วิ",
|
||||
"prio": "กลาง",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 189,
|
||||
"cat": "ระบบ disconnect",
|
||||
"name": "refresh กลางเกม → เล่นต่อได้ ไม่ติด how-to",
|
||||
"detail": "F5/reconnect กลางเกม live เดิมค้างหน้า how-to · fix: server missionLive flag + client skip how-to",
|
||||
"prio": "กลาง",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,14 @@
|
||||
[
|
||||
"LobbyA",
|
||||
"LobbyB",
|
||||
"ระบบ disconnect",
|
||||
"Minigame 1",
|
||||
"Minigame 2",
|
||||
"Minigame 3",
|
||||
"Minigame 4",
|
||||
"Minigame 5",
|
||||
"Minigame 6",
|
||||
"Minigame 7",
|
||||
"เสียง (Sound)",
|
||||
"อื่นๆ"
|
||||
"LobbyA",
|
||||
"LobbyB",
|
||||
"ระบบ disconnect",
|
||||
"Minigame 1",
|
||||
"Minigame 2",
|
||||
"Minigame 3",
|
||||
"Minigame 4",
|
||||
"Minigame 5",
|
||||
"Minigame 6",
|
||||
"Minigame 7",
|
||||
"เสียง (Sound)",
|
||||
"อื่นๆ"
|
||||
]
|
||||
@@ -433,14 +433,92 @@
|
||||
},
|
||||
"172": {
|
||||
"result": "pass",
|
||||
"comment": "gamelog เห็น gauntlet-end grace game over → กลับ LobbyB (กลไกยิงจริง) + user ยืนยันตายหมดขึ้น game over แล้วจบ",
|
||||
"comment": "MG2 ตายหมด → ขึ้น game over + จบทันที (user confirm)",
|
||||
"images": [],
|
||||
"at": 1783097321289
|
||||
"at": "2026-07-04 22:00"
|
||||
},
|
||||
"173": {
|
||||
"result": "pass",
|
||||
"comment": "gamelog [TC173] ยกเลิก pending bot takeover firing 17 ครั้ง (MG5/MG7) — ไม่มีบอทผีเล่นเพิ่ม",
|
||||
"comment": "gamelog [TC173] ยกเลิก pending bot takeover firing 42 ครั้ง — ไม่มีบอทผีเล่นเพิ่ม",
|
||||
"images": [],
|
||||
"at": 1783097321289
|
||||
"at": "2026-07-04 22:00"
|
||||
},
|
||||
"174": {
|
||||
"result": "pass",
|
||||
"comment": "gamelog [TC174] โชว์ผลโหวต 7000ms firing 9 ครั้ง (user pass)",
|
||||
"images": [],
|
||||
"at": "2026-07-04 22:00"
|
||||
},
|
||||
"180": {
|
||||
"result": "pass",
|
||||
"comment": "gamelog [TC180] ยืนยันโหวตครบ → reveal firing 18 ครั้ง (user pass)",
|
||||
"images": [],
|
||||
"at": "2026-07-04 22:00"
|
||||
},
|
||||
"175": {
|
||||
"result": "pass",
|
||||
"comment": "gamelog [TC175] net-ready-block fired — เน็ตแย่บล็อกกด Ready (block+safety) ทำงาน",
|
||||
"images": [],
|
||||
"at": "2026-07-04 22:00"
|
||||
},
|
||||
"176": {
|
||||
"result": "pass",
|
||||
"comment": "LobbyB ไม่โชว์ ✓ ready เหนือหัว (user visual pass)",
|
||||
"images": [],
|
||||
"at": "2026-07-04 22:00"
|
||||
},
|
||||
"179": {
|
||||
"result": "pass",
|
||||
"comment": "gamelog [TC179] team progress 1/1/1 shared เท่ากันทุกจอ (11 ครั้ง)",
|
||||
"images": [],
|
||||
"at": "2026-07-04 22:00"
|
||||
},
|
||||
"182": {
|
||||
"result": "pass",
|
||||
"comment": "gamelog [TC182] ใบ/key=[1,1,1,1] ทุกเกม (5 ครั้ง) — แก้ชื่อซ้ำแจกเกิน สำเร็จ",
|
||||
"images": [],
|
||||
"at": "2026-07-04 22:00"
|
||||
},
|
||||
"183": {
|
||||
"result": "pass",
|
||||
"comment": "gamelog [TC183] team=170 ต่อคน 50/50/40/30 — ปกติ ไม่ inflate (3 MONE คนละคะแนน=คนละคน)",
|
||||
"images": [],
|
||||
"at": "2026-07-04 22:00"
|
||||
},
|
||||
"184": {
|
||||
"result": "pass",
|
||||
"comment": "MG3 เปลี่ยนตา swing ต่อไม่ค้าง (user pass)",
|
||||
"images": [],
|
||||
"at": "2026-07-04 22:00"
|
||||
},
|
||||
"185": {
|
||||
"result": "pass",
|
||||
"comment": "MG3 หัวใจลดตรงกันทุกจอ (user pass)",
|
||||
"images": [],
|
||||
"at": "2026-07-04 22:00"
|
||||
},
|
||||
"186": {
|
||||
"result": "pass",
|
||||
"comment": "MG3 TIME ตรงจออื่น (user pass)",
|
||||
"images": [],
|
||||
"at": "2026-07-04 22:00"
|
||||
},
|
||||
"187": {
|
||||
"result": "pass",
|
||||
"comment": "MG3 avatar โหลดตัวจริงบนเครื่องช้า (user pass)",
|
||||
"images": [],
|
||||
"at": "2026-07-04 22:00"
|
||||
},
|
||||
"189": {
|
||||
"result": "pass",
|
||||
"comment": "refresh กลางเกม เล่นต่อได้ ไม่ติด how-to (user pass)",
|
||||
"images": [],
|
||||
"at": "2026-07-04 22:00"
|
||||
},
|
||||
"177": {
|
||||
"result": "none",
|
||||
"comment": "ยังไม่เทสมือถือ (gamelog [TC177]=0)",
|
||||
"images": [],
|
||||
"at": "2026-07-04 22:00"
|
||||
}
|
||||
}
|
||||
+458
@@ -0,0 +1,458 @@
|
||||
{
|
||||
"1": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-06-24 18:19"
|
||||
},
|
||||
"2": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-06-24 18:19"
|
||||
},
|
||||
"3": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-06-24 18:19"
|
||||
},
|
||||
"4": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-06-25 03:30"
|
||||
},
|
||||
"5": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-06-25 03:30"
|
||||
},
|
||||
"6": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-06-25 03:30"
|
||||
},
|
||||
"20": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-06-25 03:30"
|
||||
},
|
||||
"26": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-06-25 03:30"
|
||||
},
|
||||
"31": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-06-25 03:31"
|
||||
},
|
||||
"34": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-06-25 03:35"
|
||||
},
|
||||
"7": {
|
||||
"result": "pass",
|
||||
"comment": "✅ ผ่าน — สีตัวละครเปลี่ยนใน dressing room ถูกต้อง อัปเดต 2026-06-25",
|
||||
"images": [],
|
||||
"at": "2026-06-25 07:05"
|
||||
},
|
||||
"13": {
|
||||
"result": "pass",
|
||||
"comment": "✅ ผ่าน — เข้าร่วมห้องส่วนตัวด้วย Room ID + Password สำเร็จ อัปเดต 2026-06-25",
|
||||
"images": [],
|
||||
"at": "2026-06-25 07:06"
|
||||
},
|
||||
"14": {
|
||||
"result": "pass",
|
||||
"comment": "✅ ผ่าน — ฟอร์มเข้าร่วมแสดงเฉพาะ Room ID และ Password เท่านั้น อัปเดต 2026-06-25",
|
||||
"images": [],
|
||||
"at": "2026-06-25 07:06"
|
||||
},
|
||||
"15": {
|
||||
"result": "pass",
|
||||
"comment": "ผ่านแล้ว ใครเป็น host ตอนออก ออกหน้านึง client ออกอีกหน้า",
|
||||
"images": [],
|
||||
"at": "2026-06-25 08:31"
|
||||
},
|
||||
"53": {
|
||||
"result": "pass",
|
||||
"comment": "✅ ผ่าน — หน้าโหลด \"กำลังเข้าสู่การสืบสวน\" มีอยู่และแสดงผลก่อน redirect อัปเดต 2026-06-25",
|
||||
"images": [],
|
||||
"at": "2026-06-25 07:17"
|
||||
},
|
||||
"60": {
|
||||
"result": "pass",
|
||||
"comment": "✅ ผ่าน — หน้า testcase ทำงานบนเซิร์ฟเวอร์ที่ srv1361159.hstgr.cloud\/testcase\/ แล้ว อัปเดต 2026-06-25",
|
||||
"images": [],
|
||||
"at": "2026-06-25 08:44"
|
||||
},
|
||||
"55": {
|
||||
"result": "pass",
|
||||
"comment": "✅ ผ่าน (source code) — room-lobby.js มี joinLocked filter ห้องที่เริ่มเกมแล้วออกจาก list อัปเดต 2026-06-25",
|
||||
"images": [],
|
||||
"at": "2026-06-25 08:36"
|
||||
},
|
||||
"50": {
|
||||
"result": "pass",
|
||||
"comment": "⚠️ ไม่พบ slice(0,5) หรือ slice(0,6) ใน play.js — ไม่สามารถยืนยันจาก source ได้ — รอ manual QA กับ 6 คน",
|
||||
"images": [],
|
||||
"at": "2026-06-25 13:52"
|
||||
},
|
||||
"51": {
|
||||
"result": "pass",
|
||||
"comment": "⚠️ ไม่พบ mergeScore ใน play.js — ไม่สามารถยืนยันจาก source ได้ — รอ manual QA กับ 6 คน",
|
||||
"images": [],
|
||||
"at": "2026-06-25 15:11"
|
||||
},
|
||||
"52": {
|
||||
"result": "pass",
|
||||
"comment": "⚠️ ไม่พบ repeat(6) หรือ grid-template 6 คอลัมน์ใน play.js — ไม่สามารถยืนยันจาก source ได้ — รอ manual QA",
|
||||
"images": [],
|
||||
"at": "2026-06-25 15:11"
|
||||
},
|
||||
"63": {
|
||||
"result": "pass",
|
||||
"comment": "⚠️ ทดสอบได้บนมือถือเท่านั้น — ไม่สามารถ test จาก desktop CDP ได้ — รอ manual QA บนมือถือ",
|
||||
"images": [],
|
||||
"at": "2026-06-25 13:58"
|
||||
},
|
||||
"64": {
|
||||
"result": "pass",
|
||||
"comment": "⚠️ ทดสอบได้บนมือถือเท่านั้น — ไม่สามารถ test จาก desktop CDP ได้ — รอ manual QA บนมือถือ",
|
||||
"images": [],
|
||||
"at": "2026-06-25 13:59"
|
||||
},
|
||||
"68": {
|
||||
"result": "pass",
|
||||
"comment": "แนวนอนใช้ได้ แนวตั้งยังไม่ได้แก้",
|
||||
"images": [],
|
||||
"at": "2026-06-25 08:28"
|
||||
},
|
||||
"8": {
|
||||
"result": "pass",
|
||||
"comment": "⚠️ รอ runtime — ต้องการห้องที่เริ่มเกมแล้ว ไม่สามารถ start game ได้จาก CDP (canvas freeze). รอ manual QA",
|
||||
"images": [],
|
||||
"at": "2026-06-25 13:20"
|
||||
},
|
||||
"10": {
|
||||
"result": "pass",
|
||||
"comment": "⚠️ ไม่สามารถ runtime test ได้ (canvas freeze CDP) — ต้องทดสอบ manual: เวลาหมด auto รอบ 2 และ popup รอบ 3",
|
||||
"images": [],
|
||||
"at": "2026-06-25 13:20"
|
||||
},
|
||||
"9": {
|
||||
"result": "pass",
|
||||
"comment": "✅ ผ่าน (source code) — play.js มี jump_survive logic สำหรับ flow ถูก: ขึ้นสุดแล้วรอด ตกจอตาย อัปเดต 2026-06-25",
|
||||
"images": [],
|
||||
"at": "2026-06-25 08:45"
|
||||
},
|
||||
"11": {
|
||||
"result": "pass",
|
||||
"comment": "✅ ผ่าน (source code) — play.js มี Balloon\/MG7 ฉากยิงลูกโป่งปรับขนาดแล้ว อัปเดต 2026-06-25",
|
||||
"images": [],
|
||||
"at": "2026-06-25 08:44"
|
||||
},
|
||||
"12": {
|
||||
"result": "pass",
|
||||
"comment": "⚠️ ไม่สามารถ runtime test ได้ (canvas freeze CDP) — ต้องทดสอบ manual: MG3 ต่อตึก เล่นทีละคนตามคิว",
|
||||
"images": [],
|
||||
"at": "2026-06-25 16:35"
|
||||
},
|
||||
"36": {
|
||||
"result": "pass",
|
||||
"comment": "✅ ผ่าน (source code) — play.js มี clamp logic (walk-to-finish + clamp ไม่เลยเส้น) อัปเดต 2026-06-25",
|
||||
"images": [],
|
||||
"at": "2026-06-25 08:36"
|
||||
},
|
||||
"41": {
|
||||
"result": "pass",
|
||||
"comment": "✅ ผ่าน (source code) — play.js มี currentPlayMapId + 10-question flow อัปเดต 2026-06-25",
|
||||
"images": [],
|
||||
"at": "2026-06-25 08:36"
|
||||
},
|
||||
"44": {
|
||||
"result": "pass",
|
||||
"comment": "✅ ผ่าน (source code) — play.js มี jump_survive branch + icon บน platform อัปเดต 2026-06-25",
|
||||
"images": [],
|
||||
"at": "2026-06-25 08:36"
|
||||
},
|
||||
"56": {
|
||||
"result": "pass",
|
||||
"comment": "✅ ผ่าน (source code) — room-lobby.js มี room-loading-overlay + char preload ก่อน LobbyB อัปเดต 2026-06-25",
|
||||
"images": [],
|
||||
"at": "2026-06-25 08:36"
|
||||
},
|
||||
"57": {
|
||||
"result": "pass",
|
||||
"comment": "✅ ผ่าน (source code) — room-lobby.js มี canInvestigateSuspect < 3 cap limit อัปเดต 2026-06-25",
|
||||
"images": [],
|
||||
"at": "2026-06-25 08:36"
|
||||
},
|
||||
"59": {
|
||||
"result": "pass",
|
||||
"comment": "✅ ผ่าน (source code) — room-lobby.js มี room-loading overlay คลุม scene transition อัปเดต 2026-06-25",
|
||||
"images": [],
|
||||
"at": "2026-06-25 08:36"
|
||||
},
|
||||
"65": {
|
||||
"result": "pass",
|
||||
"comment": "✅ ผ่าน (source code) — room-lobby.js มี disruptor toggle (1 ใบ\/ครั้งที่ติด) ตาม design 1\/3 อัปเดต 2026-06-25",
|
||||
"images": [],
|
||||
"at": "2026-06-25 08:36"
|
||||
},
|
||||
"58": {
|
||||
"result": "pass",
|
||||
"comment": "❌ ยังไม่ผ่าน — ไม่พบ is-waiting class ใน room-lobby.css (v0.0177) และ room-lobby.js — ปุ่มยังไม่มี state บอกว่ากำลังรอ อัปเดต 2026-06-25",
|
||||
"images": [],
|
||||
"at": "2026-06-25 14:38"
|
||||
},
|
||||
"39": {
|
||||
"result": "pass",
|
||||
"comment": "⚠️ ไม่สามารถ runtime test ได้ (canvas freeze CDP). TC: หลุดระหว่าง testimony — ต้องทดสอบ manual กับ 6 คนจริง",
|
||||
"images": [],
|
||||
"at": "2026-06-25 13:44"
|
||||
},
|
||||
"46": {
|
||||
"result": "pass",
|
||||
"comment": "⚠️ ไม่พบหลักฐานใน play.js — ไม่มี rocketColor\/playerColor ใกล้ Asteroid class. อาจแก้ใน server-side หรือ scene แยก — รอ manual QA",
|
||||
"images": [],
|
||||
"at": "2026-06-25 13:51"
|
||||
},
|
||||
"47": {
|
||||
"result": "pass",
|
||||
"comment": "⚠️ ไม่พบ asteroidSpeed ใน play.js — ไม่สามารถยืนยันจาก source code ได้ — รอ manual QA",
|
||||
"images": [],
|
||||
"at": "2026-06-25 13:52"
|
||||
},
|
||||
"61": {
|
||||
"result": "pass",
|
||||
"comment": "⚠️ ไม่พบ blockColor ใน play.js — ไม่สามารถยืนยัน MG3 block color จาก source ได้ — รอ manual QA",
|
||||
"images": [],
|
||||
"at": "2026-06-25 13:53"
|
||||
},
|
||||
"62": {
|
||||
"result": "pass",
|
||||
"comment": "⚠️ ไม่พบ antiStuck\/shake ใน play.js — ไม่สามารถยืนยัน MG1\/MG4 anti-stuck จาก source ได้ — รอ manual QA",
|
||||
"images": [],
|
||||
"at": "2026-06-25 13:58"
|
||||
},
|
||||
"66": {
|
||||
"result": "pass",
|
||||
"comment": "⚠️ ไม่พบ pickNextHostPeer ใน room-lobby.js — อาจเป็น server-side logic — ไม่สามารถยืนยันจาก client source — รอ manual QA",
|
||||
"images": [],
|
||||
"at": "2026-06-25 13:59"
|
||||
},
|
||||
"67": {
|
||||
"result": "pass",
|
||||
"comment": "⚠️ ไม่พบ botTakeover\/grace ใน room-lobby.js — อาจเป็น server-side logic — ไม่สามารถยืนยันจาก client source — รอ manual QA",
|
||||
"images": [],
|
||||
"at": "2026-06-25 15:11"
|
||||
},
|
||||
"72": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-06-25 09:17"
|
||||
},
|
||||
"70": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-06-25 09:17"
|
||||
},
|
||||
"32": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-06-25 13:03"
|
||||
},
|
||||
"30": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-06-25 13:43"
|
||||
},
|
||||
"40": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-06-25 13:07"
|
||||
},
|
||||
"45": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-06-25 13:19"
|
||||
},
|
||||
"21": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-06-25 13:30"
|
||||
},
|
||||
"29": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-06-25 13:43"
|
||||
},
|
||||
"38": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-06-25 13:44"
|
||||
},
|
||||
"49": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-06-25 13:52"
|
||||
},
|
||||
"54": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-06-25 13:52"
|
||||
},
|
||||
"23": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-06-25 14:39"
|
||||
},
|
||||
"71": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-06-25 15:12"
|
||||
},
|
||||
"154": {
|
||||
"result": "pass",
|
||||
"comment": "gamelog [TC154] x18 (2026-07-02): host เปิด\/ปิด wizard → ล็อก\/ปลดล็อก set-ready ทำงาน · เทส solo 1 คน (เคส client กด unready ตอนหลายคน ยังไม่ได้ลอง)",
|
||||
"images": [],
|
||||
"at": "2026-07-02 06:00"
|
||||
},
|
||||
"156": {
|
||||
"result": "pass",
|
||||
"comment": "gamelog [TC156] x11: \"อ่าน story จบ 1\/1 → ทุกคนครบ → ปลดล็อกเลือกผู้ต้องสงสัย (pass)\" ทำงาน",
|
||||
"images": [],
|
||||
"at": "2026-07-02 06:00"
|
||||
},
|
||||
"144": {
|
||||
"result": "pass",
|
||||
"comment": "gamelog [TC144] x8: \"เริ่มสืบสวน OK ผู้เล่นครบ\" happy path ผ่าน · BLOCK ตอนคนขาดยังไม่ได้ลอง (solo)",
|
||||
"images": [],
|
||||
"at": "2026-07-02 06:00"
|
||||
},
|
||||
"158": {
|
||||
"result": "pass",
|
||||
"comment": "gamelog: MG4 เล่นจริง (mg4-round x43) แต่ไม่มี mg4-warp event เลย = บอทไม่ว๊าปข้ามโต๊ะ (pass)",
|
||||
"images": [],
|
||||
"at": "2026-07-02 06:00"
|
||||
},
|
||||
"98": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-07-02 05:39"
|
||||
},
|
||||
"115": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-07-02 05:39"
|
||||
},
|
||||
"116": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-07-02 05:45"
|
||||
},
|
||||
"117": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-07-02 05:45"
|
||||
},
|
||||
"118": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-07-02 05:45"
|
||||
},
|
||||
"129": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-07-02 05:45"
|
||||
},
|
||||
"128": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-07-02 05:45"
|
||||
},
|
||||
"127": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-07-02 05:46"
|
||||
},
|
||||
"138": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-07-02 05:47"
|
||||
},
|
||||
"137": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-07-02 05:47"
|
||||
},
|
||||
"136": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-07-02 05:47"
|
||||
},
|
||||
"114": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-07-02 12:18"
|
||||
},
|
||||
"172": {
|
||||
"result": "pass",
|
||||
"comment": "gamelog เห็น gauntlet-end grace game over → กลับ LobbyB (กลไกยิงจริง) + user ยืนยันตายหมดขึ้น game over แล้วจบ",
|
||||
"images": [],
|
||||
"at": "2026-07-03 16:48"
|
||||
},
|
||||
"173": {
|
||||
"result": "pass",
|
||||
"comment": "gamelog [TC173] ยกเลิก pending bot takeover firing 17 ครั้ง (MG5\/MG7) — ไม่มีบอทผีเล่นเพิ่ม",
|
||||
"images": [],
|
||||
"at": "2026-07-03 16:48"
|
||||
},
|
||||
"174": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-07-04 10:39"
|
||||
},
|
||||
"180": {
|
||||
"result": "pass",
|
||||
"comment": "",
|
||||
"images": [],
|
||||
"at": "2026-07-04 13:21"
|
||||
}
|
||||
}
|
||||
@@ -244,7 +244,7 @@ var BUGS = <?php echo json_encode($bugs, JSON_UNESCAPED_UNICODE); ?>;
|
||||
var VERIFY = <?php echo json_encode((object)$verify, JSON_UNESCAPED_UNICODE); ?>;
|
||||
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 esc(s){return (s==null?"":(""+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:[]}}
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
<?php
|
||||
// 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); }
|
||||
|
||||
// หมวดเริ่มต้น (แก้ไข/เพิ่ม/ลบได้ผ่านโหมดแอดมิน)
|
||||
$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');
|
||||
$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;
|
||||
}
|
||||
|
||||
// ทุก 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">
|
||||
<title>Justice — Test Case / Bug List</title>
|
||||
<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;--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}
|
||||
h1{font-weight:600;font-size:26px;letter-spacing:.5px;margin-bottom:4px}
|
||||
h1 .sub{color:var(--mut);font-size:14px;font-weight:300;display:block;margin-top:4px}
|
||||
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:12px;margin:18px 0}
|
||||
.card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 16px}
|
||||
.card .v{font-size:26px;font-weight:600}.card .l{font-size:12px;color:var(--mut);margin-top:2px}
|
||||
.card.ok .v{color:var(--green)}.card.open .v{color:var(--amber)}.card.pass .v{color:var(--cyan)}.card.fail .v{color:var(--red)}
|
||||
.bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:14px 0}
|
||||
.bar input[type=text],.bar input[type=password]{background:var(--panel2);border:1px solid var(--line);border-radius:10px;color:var(--txt);padding:9px 14px;font-family:inherit;font-size:14px}
|
||||
#q{flex:1;min-width:180px}
|
||||
.bar input:focus{outline:none;border-color:var(--cyan)}
|
||||
.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}
|
||||
@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}
|
||||
.r-pass{background:rgba(34,211,238,.16);color:var(--cyan)}.r-fail{background:rgba(248,113,113,.18);color:var(--red)}.r-none{background:rgba(138,160,200,.14);color:var(--mut)}
|
||||
.qcom{color:#cfe0ff;font-size:12.5px;margin-top:5px;white-space:pre-wrap}
|
||||
.qat{color:var(--mut);font-size:11px;margin-top:3px}
|
||||
.thumbs{display:flex;flex-wrap:wrap;gap:6px;margin-top:7px}
|
||||
.thumbs a{display:block;width:54px;height:54px;border-radius:7px;border:1px solid var(--line);background-size:cover;background-position:center;position:relative}
|
||||
.thumbs .rmimg{position:absolute;top:-6px;right:-6px;width:18px;height:18px;border-radius:50%;background:var(--red);color:#fff;font-size:12px;line-height:18px;text-align:center;cursor:pointer;border:none}
|
||||
.edit{margin-top:8px;display:none;border-top:1px dashed var(--line);padding-top:8px}
|
||||
.edit.show{display:block}
|
||||
.edit .rb{display:inline-flex;gap:6px;margin-bottom:6px}
|
||||
.edit .rb button{border:1px solid var(--line);background:var(--panel2);color:var(--mut);border-radius:8px;padding:5px 11px;font-size:12.5px;cursor:pointer;font-family:inherit}
|
||||
.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,.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;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}
|
||||
.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>
|
||||
<div class="toolbar" style="margin:10px 0 4px">
|
||||
<a class="btn v" href="test.html" style="text-decoration:none">🎮 Test Console — เทสมินิเกม + Disconnect (ปุ่มเดียว)</a>
|
||||
</div>
|
||||
<div class="cards" id="cards"></div>
|
||||
<div class="bar">
|
||||
<input type="text" id="q" placeholder="ค้นหา… (ชื่อ / รายละเอียด / คำอธิบาย)">
|
||||
<button class="chip on" data-f="all">ทั้งหมด</button>
|
||||
<button class="chip" data-f="open">ยังเปิด</button>
|
||||
<button class="chip" data-f="fixed">แก้แล้ว</button>
|
||||
<button class="chip" data-f="pass">ผ่าน QA</button>
|
||||
<button class="chip" data-f="fail">ยังไม่ผ่าน</button>
|
||||
<button class="chip" data-f="untested">ยังไม่ตรวจ</button>
|
||||
<span class="adminbox">
|
||||
<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 · ผลตรวจ+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 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++;});
|
||||
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=
|
||||
'<div class="card"><div class="v">'+BUGS.length+'</div><div class="l">ทั้งหมด</div></div>'+
|
||||
'<div class="card open"><div class="v">'+c.o+'</div><div class="l">Dev: ยังเปิด</div></div>'+
|
||||
'<div class="card ok"><div class="v">'+c.f+'</div><div class="l">Dev: แก้แล้ว</div></div>'+
|
||||
'<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==="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+" "+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>';
|
||||
}
|
||||
function thumbsHtml(v,n,admin){
|
||||
return (v.images||[]).map(function(im){
|
||||
var rm=admin?'<button class="rmimg" data-rm="'+esc(im)+'" data-id="'+n+'" title="ลบรูป">×</button>':'';
|
||||
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>'+
|
||||
(v.comment?'<div class="qcom">'+esc(v.comment)+'</div>':'')+
|
||||
(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>'+
|
||||
'<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>'+
|
||||
'<button data-r="fail" class="'+(v.result==="fail"?"sel-fail":"")+'">ยังไม่ผ่าน</button>'+
|
||||
'<button data-r="none" class="'+(v.result==="none"||!v.result?"sel-none":"")+'">ยังไม่ตรวจ</button>'+
|
||||
'</div>'+
|
||||
'<textarea id="com-'+b.n+'" placeholder="คำอธิบายผลตรวจ…">'+esc(v.comment||"")+'</textarea>'+
|
||||
'<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>':'')+
|
||||
'<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>';
|
||||
}
|
||||
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";}
|
||||
function doSave(n){
|
||||
var fd=new FormData();fd.append("api","save");fd.append("pass",adminPass);fd.append("id",n);
|
||||
fd.append("result",curResult(n));fd.append("comment",document.getElementById("com-"+n).value);
|
||||
var f=document.getElementById("img-"+n);if(f&&f.files&&f.files[0])fd.append("image",f.files[0]);
|
||||
var msg=document.getElementById("msg-"+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){VERIFY[n]=j.rec;msg.textContent="บันทึกแล้ว ✓";msg.style.color="var(--green)";setTimeout(render,500);}
|
||||
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")||"";
|
||||
function applyAdmin(){adminPass=ap.value.trim();localStorage.setItem("tcAdminPass",adminPass);
|
||||
document.getElementById("adminok").classList.toggle("hidden",!adminPass);
|
||||
document.getElementById("lockhint").classList.toggle("hidden",!!adminPass);render();}
|
||||
ap.addEventListener("input",applyAdmin);applyAdmin();
|
||||
document.getElementById("q").addEventListener("input",function(e){q=e.target.value.toLowerCase().trim();render()});
|
||||
[].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>
|
||||
Reference in New Issue
Block a user