fix on testcase
This commit is contained in:
+13
-7
@@ -3023,18 +3023,24 @@
|
||||
}
|
||||
var normArr = Array.isArray(data.stackBlockNormalImageUrls) ? data.stackBlockNormalImageUrls : [];
|
||||
var heavyArr = Array.isArray(data.stackBlockHeavyImageUrls) ? data.stackBlockHeavyImageUrls : [];
|
||||
for (var si = 1; si <= 6; si++) {
|
||||
for (var si = 1; si <= 8; si++) {
|
||||
var nin = el('stack-block-normal-url-' + si);
|
||||
var hin = el('stack-block-heavy-url-' + si);
|
||||
if (nin) nin.value = normArr[si - 1] != null ? String(normArr[si - 1]) : '';
|
||||
if (hin) hin.value = heavyArr[si - 1] != null ? String(heavyArr[si - 1]) : '';
|
||||
var nv = normArr[si - 1] != null && String(normArr[si - 1]) !== '' ? String(normArr[si - 1]) : STACK_BLOCK_DEFAULT_NORMAL[si - 1];
|
||||
var hv = heavyArr[si - 1] != null && String(heavyArr[si - 1]) !== '' ? String(heavyArr[si - 1]) : STACK_BLOCK_DEFAULT_HEAVY[si - 1];
|
||||
if (nin) nin.value = nv || '';
|
||||
if (hin) hin.value = hv || '';
|
||||
updateStackBlockSeatPreview(si);
|
||||
}
|
||||
}
|
||||
/* #4 MG3 8 สีตาม lobby theme — ค่าเริ่มต้น Block-Code-1..8 (ตรง PLAY_LOBBY_THEME_HEX) */
|
||||
var STACK_BLOCK_THEME_NAMES = ['1-red', '2-orange', '3-Yellow', '4-Green', '5-Blue', '6-Navy', '7-Violet', '8-Pink'];
|
||||
var STACK_BLOCK_DEFAULT_NORMAL = STACK_BLOCK_THEME_NAMES.map(function (s) { return '/Game/img/TowerBlock/Block-Code-' + s + '.png'; });
|
||||
var STACK_BLOCK_DEFAULT_HEAVY = STACK_BLOCK_THEME_NAMES.map(function (s) { return '/Game/img/TowerBlock/Block-Code-' + s + '-2.png'; });
|
||||
|
||||
function readStackBlockNormalUrlsFromForm() {
|
||||
var out = [];
|
||||
for (var i = 1; i <= 6; i++) {
|
||||
for (var i = 1; i <= 8; i++) {
|
||||
var inp = el('stack-block-normal-url-' + i);
|
||||
out.push(inp && inp.value ? String(inp.value).trim() : '');
|
||||
}
|
||||
@@ -3043,7 +3049,7 @@
|
||||
|
||||
function readStackBlockHeavyUrlsFromForm() {
|
||||
var out = [];
|
||||
for (var j = 1; j <= 6; j++) {
|
||||
for (var j = 1; j <= 8; j++) {
|
||||
var inp = el('stack-block-heavy-url-' + j);
|
||||
out.push(inp && inp.value ? String(inp.value).trim() : '');
|
||||
}
|
||||
@@ -3068,7 +3074,7 @@
|
||||
}
|
||||
|
||||
function bindStackBlockVisualInputs() {
|
||||
for (var s = 1; s <= 6; s++) {
|
||||
for (var s = 1; s <= 8; s++) {
|
||||
(function (seat) {
|
||||
var nIn = el('stack-block-normal-url-' + seat);
|
||||
var hIn = el('stack-block-heavy-url-' + seat);
|
||||
@@ -4130,7 +4136,7 @@
|
||||
})
|
||||
.then(function (res) {
|
||||
if (res && typeof res === 'object') chk.checked = !!res.troublesomeForceOffer;
|
||||
setMsg('troublesome-force-msg', on ? 'บันทึกแล้ว — บทตัวป่วนจะขึ้นเสมอ (เริ่มคดีใหม่เพื่อเทสต์)' : 'ปิดแล้ว — กลับเป็นปกติ (ขึ้นเฉพาะคนจริง > 4)', 'ok');
|
||||
setMsg('troublesome-force-msg', on ? 'บันทึกแล้ว — บทตัวป่วนจะขึ้นเสมอ (เริ่มคดีใหม่เพื่อเทสต์)' : 'ปิดแล้ว — กลับเป็นปกติ (ขึ้นเฉพาะคนจริง ≥ 4)', 'ok');
|
||||
})
|
||||
.catch(function (e) { setMsg('troublesome-force-msg', e.message || 'บันทึกไม่ได้', 'error'); })
|
||||
.then(function () { if (btn) btn.disabled = false; });
|
||||
|
||||
@@ -976,6 +976,8 @@
|
||||
<div class="space-shooter-ship-row stack-block-visual-row"><span class="space-shooter-ship-slot" title="ที่นั่ง 4">P4</span><label class="space-shooter-ship-url-label">ปกติ <input type="text" id="stack-block-normal-url-4" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev stack-block-prev stack-block-prev--normal" id="stack-block-normal-prev-4" alt="" width="314" height="103" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-normal-clear-4">ล้าง</button><label class="space-shooter-ship-url-label">ใหญ่ <input type="text" id="stack-block-heavy-url-4" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev stack-block-prev stack-block-prev--heavy" id="stack-block-heavy-prev-4" alt="" width="382" height="72" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-heavy-clear-4">ล้าง</button></div>
|
||||
<div class="space-shooter-ship-row stack-block-visual-row"><span class="space-shooter-ship-slot" title="ที่นั่ง 5">P5</span><label class="space-shooter-ship-url-label">ปกติ <input type="text" id="stack-block-normal-url-5" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev stack-block-prev stack-block-prev--normal" id="stack-block-normal-prev-5" alt="" width="314" height="103" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-normal-clear-5">ล้าง</button><label class="space-shooter-ship-url-label">ใหญ่ <input type="text" id="stack-block-heavy-url-5" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev stack-block-prev stack-block-prev--heavy" id="stack-block-heavy-prev-5" alt="" width="382" height="72" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-heavy-clear-5">ล้าง</button></div>
|
||||
<div class="space-shooter-ship-row stack-block-visual-row"><span class="space-shooter-ship-slot" title="ที่นั่ง 6">P6</span><label class="space-shooter-ship-url-label">ปกติ <input type="text" id="stack-block-normal-url-6" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev stack-block-prev stack-block-prev--normal" id="stack-block-normal-prev-6" alt="" width="314" height="103" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-normal-clear-6">ล้าง</button><label class="space-shooter-ship-url-label">ใหญ่ <input type="text" id="stack-block-heavy-url-6" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev stack-block-prev stack-block-prev--heavy" id="stack-block-heavy-prev-6" alt="" width="382" height="72" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-heavy-clear-6">ล้าง</button></div>
|
||||
<div class="space-shooter-ship-row stack-block-visual-row"><span class="space-shooter-ship-slot" title="ที่นั่ง 7">P7</span><label class="space-shooter-ship-url-label">ปกติ <input type="text" id="stack-block-normal-url-7" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev stack-block-prev stack-block-prev--normal" id="stack-block-normal-prev-7" alt="" width="314" height="103" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-normal-clear-7">ล้าง</button><label class="space-shooter-ship-url-label">ใหญ่ <input type="text" id="stack-block-heavy-url-7" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev stack-block-prev stack-block-prev--heavy" id="stack-block-heavy-prev-7" alt="" width="382" height="72" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-heavy-clear-7">ล้าง</button></div>
|
||||
<div class="space-shooter-ship-row stack-block-visual-row"><span class="space-shooter-ship-slot" title="ที่นั่ง 8">P8</span><label class="space-shooter-ship-url-label">ปกติ <input type="text" id="stack-block-normal-url-8" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev stack-block-prev stack-block-prev--normal" id="stack-block-normal-prev-8" alt="" width="314" height="103" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-normal-clear-8">ล้าง</button><label class="space-shooter-ship-url-label">ใหญ่ <input type="text" id="stack-block-heavy-url-8" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev stack-block-prev stack-block-prev--heavy" id="stack-block-heavy-prev-8" alt="" width="382" height="72" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-heavy-clear-8">ล้าง</button></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="quiz-admin-actions">
|
||||
@@ -1271,10 +1273,10 @@
|
||||
<p id="special-card-by-map-msg" class="msg" role="status"></p>
|
||||
|
||||
<h3 class="admin-subheading" style="margin-top:28px;">บังคับเสนอบท "ตัวป่วน" (เทสต์)</h3>
|
||||
<p class="muted">ปกติบทตัวป่วนจะเสนอเฉพาะเมื่อมี<strong>คนจริงเกิน 4 คน</strong> · เปิดอันนี้เพื่อ<strong>บังคับให้ขึ้นเสมอ</strong>แม้เล่นคนเดียว (ไว้เทสต์)</p>
|
||||
<p class="muted">ปกติบทตัวป่วนจะเสนอเฉพาะเมื่อมี<strong>คนจริงตั้งแต่ 4 คนขึ้นไป</strong> · เปิดอันนี้เพื่อ<strong>บังคับให้ขึ้นเสมอ</strong>แม้เล่นคนเดียว (ไว้เทสต์)</p>
|
||||
<label style="display:flex;align-items:center;gap:10px;color:#cfd6ea;font-size:14px;cursor:pointer;">
|
||||
<input type="checkbox" id="troublesome-force-offer" style="width:18px;height:18px;cursor:pointer;">
|
||||
บังคับเสนอบทตัวป่วน (ข้ามเงื่อนไข > 4 คน)
|
||||
บังคับเสนอบทตัวป่วน (ข้ามเงื่อนไข ≥ 4 คน)
|
||||
</label>
|
||||
<div style="margin-top:12px;"><button type="button" class="btn" id="btn-troublesome-force-save">บันทึก</button></div>
|
||||
<p id="troublesome-force-msg" class="msg" role="status"></p>
|
||||
@@ -1283,6 +1285,6 @@
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<script src="admin.js?v=85"></script>
|
||||
<script src="admin.js?v=87"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -51,9 +51,9 @@
|
||||
"providerUserId": "p_1775109142385_wq7wfy1p32j",
|
||||
"notes": "auto: player-coins",
|
||||
"blocked": false,
|
||||
"coins": 1422,
|
||||
"coins": 1672,
|
||||
"createdAt": "2026-04-02T05:52:21+00:00",
|
||||
"updatedAt": "2026-06-24T09:36:08+00:00",
|
||||
"updatedAt": "2026-06-25T03:57:35+00:00",
|
||||
"daily": {
|
||||
"anchorMs": 1781197200000,
|
||||
"claimedDays": [
|
||||
@@ -65,45 +65,73 @@
|
||||
false,
|
||||
false
|
||||
],
|
||||
"lockUntilMs": 1782320400000
|
||||
"lockUntilMs": 0
|
||||
},
|
||||
"score": 1252,
|
||||
"score": 1492,
|
||||
"scoreByCase": {
|
||||
"1": 16,
|
||||
"10": 358,
|
||||
"8": 238,
|
||||
"10": 386,
|
||||
"8": 288,
|
||||
"11": 20,
|
||||
"13": 140,
|
||||
"14": 20,
|
||||
"13": 176,
|
||||
"14": 30,
|
||||
"9": 40,
|
||||
"12": 272,
|
||||
"12": 282,
|
||||
"15": 20,
|
||||
"4": 10,
|
||||
"5": 20,
|
||||
"5": 32,
|
||||
"7": 6,
|
||||
"3": 72,
|
||||
"6": 20
|
||||
"6": 44,
|
||||
"2": 70
|
||||
},
|
||||
"lbName": "Q",
|
||||
"achievements": {
|
||||
"d1_minigame_solver": 20,
|
||||
"a1_first_deduction": 1,
|
||||
"a5_truth_hunter": 2
|
||||
"a5_truth_hunter": 3
|
||||
},
|
||||
"lobbyColorThemeIndex": 5,
|
||||
"lobbyColorThemeIndex": 6,
|
||||
"lobbySkinToneIndex": 1
|
||||
},
|
||||
{
|
||||
"id": "1d64c56fadb64a93eae68a1d",
|
||||
"email": "",
|
||||
"displayName": "Guest",
|
||||
"displayName": "OHM",
|
||||
"loginType": "guest",
|
||||
"providerUserId": "p_1775128666204_xzgg2n6x8h",
|
||||
"notes": "auto: player-coins",
|
||||
"blocked": false,
|
||||
"coins": 0,
|
||||
"coins": 86,
|
||||
"createdAt": "2026-04-02T11:17:46+00:00",
|
||||
"updatedAt": "2026-04-02T11:17:46+00:00"
|
||||
"updatedAt": "2026-06-24T12:38:24+00:00",
|
||||
"lobbyColorThemeIndex": 4,
|
||||
"lobbySkinToneIndex": 2,
|
||||
"daily": {
|
||||
"anchorMs": 1782234000000,
|
||||
"claimedDays": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"lockUntilMs": 1782320400000
|
||||
},
|
||||
"score": 76,
|
||||
"scoreByCase": {
|
||||
"6": 18,
|
||||
"13": 50,
|
||||
"5": 8
|
||||
},
|
||||
"lbName": "OHM",
|
||||
"achievements": {
|
||||
"d1_minigame_solver": 10,
|
||||
"a1_first_deduction": 1,
|
||||
"a5_truth_hunter": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "d1d5c39d42a2dce9a4949d5a",
|
||||
@@ -198,36 +226,39 @@
|
||||
"providerUserId": "p_1780400804441_lsc3lqqvs0i",
|
||||
"notes": "auto: player-coins",
|
||||
"blocked": false,
|
||||
"coins": 57,
|
||||
"coins": 135,
|
||||
"createdAt": "2026-06-02T11:46:46+00:00",
|
||||
"updatedAt": "2026-06-23T06:29:36+00:00",
|
||||
"updatedAt": "2026-06-24T12:38:36+00:00",
|
||||
"daily": {
|
||||
"anchorMs": 1781802000000,
|
||||
"claimedDays": [
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"lockUntilMs": 1782234000000
|
||||
"lockUntilMs": 1782320400000
|
||||
},
|
||||
"score": 32,
|
||||
"score": 90,
|
||||
"scoreByCase": {
|
||||
"1": 4,
|
||||
"7": 2,
|
||||
"10": 26
|
||||
"10": 26,
|
||||
"6": 20,
|
||||
"13": 32,
|
||||
"5": 6
|
||||
},
|
||||
"lbName": "Pear",
|
||||
"achievements": {
|
||||
"d1_minigame_solver": 5,
|
||||
"d1_minigame_solver": 17,
|
||||
"a1_first_deduction": 1,
|
||||
"a5_truth_hunter": 1
|
||||
"a5_truth_hunter": 2
|
||||
},
|
||||
"lobbyColorThemeIndex": 8,
|
||||
"lobbySkinToneIndex": 1
|
||||
"lobbyColorThemeIndex": 6,
|
||||
"lobbySkinToneIndex": 3
|
||||
},
|
||||
{
|
||||
"id": "9404ea306bffed187b43a490",
|
||||
@@ -597,18 +628,19 @@
|
||||
"providerUserId": "p_1781938677448_8j2wnr5ha1b",
|
||||
"notes": "auto: player-coins",
|
||||
"blocked": false,
|
||||
"coins": 28,
|
||||
"coins": 36,
|
||||
"createdAt": "2026-06-20T06:57:57+00:00",
|
||||
"updatedAt": "2026-06-22T07:38:20+00:00",
|
||||
"updatedAt": "2026-06-24T16:52:31+00:00",
|
||||
"lobbyColorThemeIndex": 7,
|
||||
"lobbySkinToneIndex": 1,
|
||||
"achievements": {
|
||||
"d1_minigame_solver": 3
|
||||
"d1_minigame_solver": 4
|
||||
},
|
||||
"lbName": "MONE",
|
||||
"score": 18,
|
||||
"score": 26,
|
||||
"scoreByCase": {
|
||||
"13": 18
|
||||
"13": 18,
|
||||
"2": 8
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -619,21 +651,21 @@
|
||||
"providerUserId": "p_1782016990238_0u37bv7pxlxs",
|
||||
"notes": "auto: player-coins",
|
||||
"blocked": false,
|
||||
"coins": 124,
|
||||
"coins": 142,
|
||||
"createdAt": "2026-06-21T04:43:11+00:00",
|
||||
"updatedAt": "2026-06-24T09:36:08+00:00",
|
||||
"updatedAt": "2026-06-24T09:41:43+00:00",
|
||||
"lobbyColorThemeIndex": 8,
|
||||
"lobbySkinToneIndex": 1,
|
||||
"score": 94,
|
||||
"score": 112,
|
||||
"scoreByCase": {
|
||||
"13": 56,
|
||||
"10": 20,
|
||||
"10": 38,
|
||||
"6": 8,
|
||||
"8": 10
|
||||
},
|
||||
"lbName": "MONE",
|
||||
"achievements": {
|
||||
"d1_minigame_solver": 9
|
||||
"d1_minigame_solver": 11
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -780,6 +812,78 @@
|
||||
"achievements": {
|
||||
"d1_minigame_solver": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "434908003822311d8042718a",
|
||||
"email": "",
|
||||
"displayName": "Nam",
|
||||
"loginType": "guest",
|
||||
"providerUserId": "p_1782300287206_l5omneu1yeo",
|
||||
"notes": "auto: player-coins",
|
||||
"blocked": false,
|
||||
"coins": 56,
|
||||
"createdAt": "2026-06-24T11:24:48+00:00",
|
||||
"updatedAt": "2026-06-24T12:38:36+00:00",
|
||||
"lobbyColorThemeIndex": 7,
|
||||
"lobbySkinToneIndex": 2,
|
||||
"score": 56,
|
||||
"scoreByCase": {
|
||||
"6": 14,
|
||||
"13": 26,
|
||||
"5": 16
|
||||
},
|
||||
"lbName": "Nam",
|
||||
"achievements": {
|
||||
"d1_minigame_solver": 11,
|
||||
"a1_first_deduction": 1,
|
||||
"a5_truth_hunter": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fac53fca91f1f05e85e7aee7",
|
||||
"email": "",
|
||||
"displayName": "MONE",
|
||||
"loginType": "guest",
|
||||
"providerUserId": "p_1782300890577_kh50k083l2",
|
||||
"notes": "auto: player-coins",
|
||||
"blocked": false,
|
||||
"coins": 108,
|
||||
"createdAt": "2026-06-24T11:34:50+00:00",
|
||||
"updatedAt": "2026-06-24T12:38:36+00:00",
|
||||
"score": 108,
|
||||
"scoreByCase": {
|
||||
"6": 32,
|
||||
"13": 60,
|
||||
"5": 16
|
||||
},
|
||||
"lbName": "MONE",
|
||||
"achievements": {
|
||||
"d1_minigame_solver": 13,
|
||||
"a1_first_deduction": 1,
|
||||
"a5_truth_hunter": 1,
|
||||
"e2_the_impostor": 1,
|
||||
"e4_agent_of_chaos": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "1605fc842806c0f7aa67cc47",
|
||||
"email": "",
|
||||
"displayName": "ผู้เล่น",
|
||||
"loginType": "guest",
|
||||
"providerUserId": "p_1782327780535_twosoa33hsr",
|
||||
"notes": "auto: player-coins",
|
||||
"blocked": false,
|
||||
"coins": 30,
|
||||
"createdAt": "2026-06-24T19:03:00+00:00",
|
||||
"updatedAt": "2026-06-24T19:12:57+00:00",
|
||||
"score": 30,
|
||||
"scoreByCase": {
|
||||
"10": 30
|
||||
},
|
||||
"lbName": "ผู้เล่น",
|
||||
"achievements": {
|
||||
"d1_minigame_solver": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -25,7 +25,9 @@
|
||||
"/Game/img/TowerBlock/Block-Code-3-Yellow.png",
|
||||
"/Game/img/TowerBlock/Block-Code-4-Green.png",
|
||||
"/Game/img/TowerBlock/Block-Code-5-Blue.png",
|
||||
"/Game/img/TowerBlock/Block-Code-6-Navy.png"
|
||||
"/Game/img/TowerBlock/Block-Code-6-Navy.png",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"stackBlockHeavyImageUrls": [
|
||||
"/Game/img/TowerBlock/Block-Code-1-red-2.png",
|
||||
@@ -33,7 +35,9 @@
|
||||
"/Game/img/TowerBlock/Block-Code-3-Yellow-2.png",
|
||||
"/Game/img/TowerBlock/Block-Code-4-Green-2.png",
|
||||
"/Game/img/TowerBlock/Block-Code-5-Blue-2.png",
|
||||
"/Game/img/TowerBlock/Block-Code-6-Navy-2.png"
|
||||
"/Game/img/TowerBlock/Block-Code-6-Navy-2.png",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"stackHeavyBlockPercent": 35,
|
||||
"jumpSurviveJumpHeightMult": 1.5,
|
||||
@@ -84,7 +88,9 @@
|
||||
"/Game/img/MegaVirus/balloon-3.png",
|
||||
"/Game/img/MegaVirus/balloon-4.png",
|
||||
"/Game/img/MegaVirus/balloon-5.png",
|
||||
"/Game/img/MegaVirus/balloon-6.png"
|
||||
"/Game/img/MegaVirus/balloon-6.png",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"balloonBossPlayerBalloonFallbackUrl": "/Game/img/MegaVirus/Artboard%209.png",
|
||||
"balloonBossBalloonsPerPlayer": 3,
|
||||
|
||||
@@ -1994,3 +1994,21 @@ body.room-lobby-page .room-lobby-top-right-actions {
|
||||
color: #a9c7da;
|
||||
min-height: 1.2em;
|
||||
}
|
||||
|
||||
/* ===== มือถือ: ย่อปุ่ม AI chat ที่ใหญ่เกิน (213x201) + ปุ่ม interact (F) ให้พอดีจอ ===== */
|
||||
@media (max-width: 600px) {
|
||||
.room-lobby-chat-ai.chat-panel-collapsed .chat-close-btn,
|
||||
.room-lobby-chat-ai.chat-panel-collapsed .chat-close-btn img {
|
||||
width: 92px !important;
|
||||
height: 87px !important;
|
||||
min-width: 92px !important;
|
||||
min-height: 87px !important;
|
||||
max-width: 92px !important;
|
||||
max-height: 87px !important;
|
||||
}
|
||||
.room-lobby-chat-ai.chat-panel .chat-close-btn,
|
||||
.room-lobby-chat-ai.chat-panel .chat-close-btn img {
|
||||
width: 42px !important;
|
||||
height: 42px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,8 +214,8 @@
|
||||
width: 1500px;
|
||||
height: 170px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
column-gap: 24px;
|
||||
grid-template-columns: repeat(6, 1fr); /* #4 รองรับ 6 คน — เดิม 5 → คนที่ 6 ตกแถว 2 ล้นกรอบ (ปลิ้น) */
|
||||
column-gap: 18px;
|
||||
padding: 0 30px;
|
||||
box-sizing: border-box;
|
||||
z-index: 5;
|
||||
|
||||
+332
-37
@@ -63,12 +63,47 @@
|
||||
if (previewMode && editorEmbedReturn) {
|
||||
try { document.documentElement.classList.add('play-preview-editor-embed'); } catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
/* #5 หน้าโหลดตอนเข้าเกม — กันเห็นจอเปล่า/asset แว่บก่อนฉากพร้อม. โชว์ทันที ซ่อนเมื่อ draw() เฟรมแรกมี mapData
|
||||
(+ หน่วงเล็กน้อยให้ asset แรกโหลด) มี safety timeout กันค้าง. ไม่โชว์ใน editor embed preview */
|
||||
let playLoadingHidden = false;
|
||||
let playLoadingHideScheduled = false;
|
||||
function hidePlayLoadingOverlay() {
|
||||
if (playLoadingHidden) return;
|
||||
playLoadingHidden = true;
|
||||
try {
|
||||
const el = document.getElementById('play-loading-overlay');
|
||||
if (el) { el.style.opacity = '0'; setTimeout(function () { if (el && el.parentNode) el.parentNode.removeChild(el); }, 320); }
|
||||
} catch (_e) { /* ignore */ }
|
||||
}
|
||||
(function showPlayLoadingOverlay() {
|
||||
if (previewMode && editorEmbedReturn) { playLoadingHidden = true; return; }
|
||||
try {
|
||||
if (!document.body) return;
|
||||
const css = document.createElement('style');
|
||||
css.textContent = ''
|
||||
+ '#play-loading-overlay{position:fixed;inset:0;z-index:99990;display:flex;flex-direction:column;align-items:center;'
|
||||
+ 'justify-content:center;gap:18px;background:radial-gradient(circle at 50% 42%,#10203c,#060b16 75%);transition:opacity .32s;}'
|
||||
+ '#play-loading-overlay .plo-spin{width:62px;height:62px;border-radius:50%;border:6px solid rgba(120,190,255,.18);'
|
||||
+ 'border-top-color:#5fd0ff;animation:ploSpin .85s linear infinite;}'
|
||||
+ '#play-loading-overlay .plo-text{color:#cfe8ff;font:600 19px system-ui,"Kanit",sans-serif;letter-spacing:.5px;text-shadow:0 2px 10px rgba(0,0,0,.5);}'
|
||||
+ '@keyframes ploSpin{to{transform:rotate(360deg);}}';
|
||||
document.head.appendChild(css);
|
||||
const ov = document.createElement('div');
|
||||
ov.id = 'play-loading-overlay';
|
||||
ov.innerHTML = '<div class="plo-spin"></div><div class="plo-text">กำลังโหลดเกม…</div>';
|
||||
document.body.appendChild(ov);
|
||||
setTimeout(hidePlayLoadingOverlay, 8000); /* safety: กันค้างถ้าโหลดนาน/ติด */
|
||||
} catch (_e) { playLoadingHidden = true; }
|
||||
})();
|
||||
const playMapIdFromQuery = (params.get('map') || '').trim();
|
||||
/** mapId จาก join / game-start — ใช้จับฉาก crown เมื่อ URL ไม่มี ?map= */
|
||||
let playSessionMapId = playMapIdFromQuery;
|
||||
/** สรุปภารกิจแบบ mock (popup-result ฯลฯ) — ใช้เฉพาะฉากนี้จาก editor (id ใน URL เช่น editor.html?id=mnorwqx1) */
|
||||
const quizCarryMissionSummaryMapId = 'mnorwqx1';
|
||||
const quizCarryUseMissionSummaryOverlay = playMapIdFromQuery === quizCarryMissionSummaryMapId;
|
||||
/* ใช้ map จริง (currentPlayMapId) ไม่ใช่ ?map= ใน URL — live play ไม่มี ?map= → เดิม false →
|
||||
mission flow ไม่ทำงาน → MG4 จบหลังตอบ 1 ข้อ (bug #41). เป็นฟังก์ชันเพราะ map รู้ตอน join */
|
||||
function quizCarryUseMissionSummaryOverlay() { return currentPlayMapId() === quizCarryMissionSummaryMapId; }
|
||||
/** Gauntlet พรมแดง — ฉากนี้จาก editor (?map=mno9kb07): วาดตัวหันขวา + ใช้รูป lane/laser จาก game-timing */
|
||||
const GAUNTLET_FACE_RIGHT_MAP_ID = 'mno9kb07';
|
||||
/** Jump Survival ฉาก Jumper — UI สรุปภารกิจแบบ crown + รูปใน /img/Jumper/ (editor ?map=mnptfts2) */
|
||||
@@ -142,8 +177,8 @@
|
||||
const SPACE_SHOOTER_MISSION_SHIP_HIT_RADIUS = 12;
|
||||
/** อุกาบาต: HP สุ่ม 1–5 ต่อก้อน (ไม่วาดหัวใจบนก้อน) — ขนาดตาม HP เทียบสเกล 5 */
|
||||
const SPACE_SHOOTER_ASTEROID_MAX_HP = 5;
|
||||
const SPACE_SHOOTER_ASTEROID_RADIUS_AT_FULL = 48;
|
||||
const SPACE_SHOOTER_ASTEROID_RADIUS_AT_MIN_HP = 11;
|
||||
const SPACE_SHOOTER_ASTEROID_RADIUS_AT_FULL = 34;
|
||||
const SPACE_SHOOTER_ASTEROID_RADIUS_AT_MIN_HP = 8;
|
||||
|
||||
/* PRNG แบบ seed ได้ (mulberry32) — ใช้สุ่มสิ่งแวดล้อมมินิเกมแบบ deterministic
|
||||
seed เดียวกัน (อิง liveBeginsAt ของ server) → ทุกเครื่องได้ลำดับสุ่มเดียวกัน = อุกาบาต/บอลลูนชุดเดียวกัน */
|
||||
@@ -737,9 +772,30 @@
|
||||
if (stackMini) stackMini.nextDropVisualDirty = true;
|
||||
}
|
||||
|
||||
/* #4 MG3 block สีตาม user — Block-Code-1..8 ตรง lobby theme (PLAY_LOBBY_THEME_HEX) + heavy '-2' */
|
||||
const STACK_BLOCK_THEME_NAMES = ['1-red', '2-orange', '3-Yellow', '4-Green', '5-Blue', '6-Navy', '7-Violet', '8-Pink'];
|
||||
const STACK_BLOCK_THEME_NORMAL = STACK_BLOCK_THEME_NAMES.map(function (s) { return BASE + '/img/TowerBlock/Block-Code-' + s + '.png'; });
|
||||
const STACK_BLOCK_THEME_HEAVY = STACK_BLOCK_THEME_NAMES.map(function (s) { return BASE + '/img/TowerBlock/Block-Code-' + s + '-2.png'; });
|
||||
function stackThemeNormalUrlPlay(theme) { return (theme >= 1 && theme <= 8) ? STACK_BLOCK_THEME_NORMAL[theme - 1] : ''; }
|
||||
function stackThemeHeavyUrlPlay(theme) { return (theme >= 1 && theme <= 8) ? STACK_BLOCK_THEME_HEAVY[theme - 1] : ''; }
|
||||
/** theme (1-8) ของผู้เล่นที่ครอบครอง seat นั้นในลำดับตา — ใช้ลงสี block ตามผู้วาง (ไม่แตะ seat/turn logic) */
|
||||
function stackSeatColorThemePlay(seat) {
|
||||
if (!isStack() || !stackTurnOrderReadyPlay()) return null;
|
||||
const s = Math.floor(Number(seat)) || 0;
|
||||
const entry = stackPreviewTurnOrder.find(function (e) { return e && e.seat === s; });
|
||||
if (!entry) return null;
|
||||
let ent = null;
|
||||
if (entry.id != null) ent = (myId != null && String(entry.id) === String(myId)) ? me : (others.get(entry.id) || others.get(String(entry.id)));
|
||||
if (!ent && entry.botId) ent = others.get(entry.botId) || others.get(String(entry.botId));
|
||||
return ent ? playLobbyThemeIndexForEntityPlay(ent) : null;
|
||||
}
|
||||
|
||||
function pickStackBlockHeavyVisual(seat) {
|
||||
const slot = Math.max(0, Math.min(5, (Math.floor(Number(seat)) || 1) - 1));
|
||||
const hU = normalizeGauntletAssetUrlForPlay(playStackBlockHeavyUrls[slot] || '');
|
||||
const slot = Math.max(0, Math.min(7, (Math.floor(Number(seat)) || 1) - 1));
|
||||
let hU = normalizeGauntletAssetUrlForPlay(playStackBlockHeavyUrls[slot] || '');
|
||||
/* ถ้าใช้สี theme (Block-Code) — heavy variant '-2' มีครบ ใช้แทน admin slot */
|
||||
const _theme = stackSeatColorThemePlay(seat);
|
||||
if (_theme && stackThemeHeavyUrlPlay(_theme)) hU = stackThemeHeavyUrlPlay(_theme);
|
||||
/** Tower mnn93hpi: ตั้งแต่เกณฑ์ progress (ตาม stackTowerProgressBlocks) → ใช้รูป heavy จาก admin — นอก live ไม่สุ่ม heavy */
|
||||
if (isStackTowerMissionUiMapPlay()) {
|
||||
if (!(stackTowerMissionPhase === 'live' && stackMini)) return false;
|
||||
@@ -763,9 +819,9 @@
|
||||
}
|
||||
|
||||
function normalizePlayStackSixUrlsFromTiming(arr) {
|
||||
const out = ['', '', '', '', '', ''];
|
||||
const out = ['', '', '', '', '', '', '', '']; /* 8 slot ตาม lobby theme (#4) */
|
||||
if (!Array.isArray(arr)) return out;
|
||||
for (let i = 0; i < 6; i++) {
|
||||
for (let i = 0; i < 8; i++) {
|
||||
out[i] = normalizeGauntletAssetUrlForPlay(typeof arr[i] === 'string' ? arr[i] : '');
|
||||
if (out[i]) ensureGauntletAssetImage(out[i]);
|
||||
}
|
||||
@@ -773,8 +829,19 @@
|
||||
}
|
||||
|
||||
function resolveStackBlockSpriteRec(seat, heavy) {
|
||||
const slot = Math.max(0, Math.min(5, (Math.floor(Number(seat)) || 1) - 1));
|
||||
const tryHeavy = !!heavy;
|
||||
/* #4 สี block = theme ของผู้เล่นที่ seat นั้น (Block-Code-1..8) ถ้า resolve ได้ (มี turn order)
|
||||
— fallback ไป admin slot URLs ถ้าไม่มี theme (เกม stack อื่น/ยังไม่มีลำดับตา) */
|
||||
const theme = stackSeatColorThemePlay(seat);
|
||||
if (theme) {
|
||||
const tn = stackThemeNormalUrlPlay(theme);
|
||||
const th = stackThemeHeavyUrlPlay(theme);
|
||||
const torder = tryHeavy ? [th, tn] : [tn, th];
|
||||
for (let ti = 0; ti < torder.length; ti++) {
|
||||
if (torder[ti]) return ensureGauntletAssetImage(torder[ti]);
|
||||
}
|
||||
}
|
||||
const slot = Math.max(0, Math.min(7, (Math.floor(Number(seat)) || 1) - 1));
|
||||
const order = tryHeavy
|
||||
? [playStackBlockHeavyUrls[slot], playStackBlockNormalUrls[slot]]
|
||||
: [playStackBlockNormalUrls[slot], playStackBlockHeavyUrls[slot]];
|
||||
@@ -3237,6 +3304,9 @@
|
||||
return pool[idx % pool.length] || humanId || roster[0];
|
||||
}
|
||||
const MOVE_SPEED = 0.15;
|
||||
/** Anti-stuck (MG1/MG4): ดันชน "เพื่อน" กี่เฟรมก่อนปลดล็อกเดินผ่าน + ช่วงเวลาที่ผ่านได้ต่อเนื่อง */
|
||||
const ANTI_STUCK_PEER_TICKS = 13;
|
||||
const ANTI_STUCK_WINDOW_MS = 650;
|
||||
/** quiz_carry — ค่าเริ่มเมื่อไม่มี override จาก Admin (รหัสฉากตรงกันใน quiz-settings.json) */
|
||||
const QUIZ_CARRY_WALK_SPEED_MULT = 1.42;
|
||||
/** ค่าที่ใช้จริงต่อเฟรม — อัปเดตจาก /api/quiz-settings + join snap */
|
||||
@@ -3304,8 +3374,8 @@
|
||||
/** ภารกิจ Tower: ชั้นสำเร็จกี่ชั้น (ฐาน) = Progress 100% — แต่ละชั้น +100/N %; คอมโบ ×2 ของชั้นนั้น */
|
||||
let playStackTowerProgressBlocks = 50;
|
||||
/** รูปบล็อก Stack ต่อที่นั่ง 1–6 (game-timing) — ว่าง = วาดสีเดิม */
|
||||
let playStackBlockNormalUrls = ['', '', '', '', '', ''];
|
||||
let playStackBlockHeavyUrls = ['', '', '', '', '', ''];
|
||||
let playStackBlockNormalUrls = ['', '', '', '', '', '', '', ''];
|
||||
let playStackBlockHeavyUrls = ['', '', '', '', '', '', '', ''];
|
||||
/** 0–100: โอกาสใช้สไปรต์ “ใหญ่” ต่อการปล่อย (ถ้ามี URL ใหญ่ช่องนั้น) */
|
||||
let playStackHeavyBlockPercent = 35;
|
||||
/** กระโดดให้รอด: ความสูงกระโดด = ทวีคูณ × ความสูงตัว (ch×tile) — จาก GET /api/game-timing */
|
||||
@@ -3851,6 +3921,15 @@
|
||||
/** ซูมที่ผู้เล่นปรับเองในฉาก quiz_battle (ล้อเมาส์ / ปุ่ม +−) */
|
||||
let quizBattleUserZoomMul = 1;
|
||||
const SPACE_SHOOTER_SHIP_COLORS = ['#50fa7b', '#ff79c6', '#ff5555', '#f1fa8c', '#bd93f9', '#8be9fd'];
|
||||
/** ยานยิงอุกาบาต (MG6) สีตาม lobby theme 1-8 — Rocket-1..8 ตรงกับ PLAY_LOBBY_THEME_HEX */
|
||||
const SPACE_SHOOTER_ROCKET_THEME_URLS = [1, 2, 3, 4, 5, 6, 7, 8].map(function (n) { return BASE + '/img/ViolentCrime/Rocket-' + n + '.png'; });
|
||||
/** theme index (1-8) ของ entity จากสี body ที่ย้อม (playTint) — ใช้แมป asset สีตามผู้เล่น */
|
||||
function playLobbyThemeIndexForEntityPlay(ent) {
|
||||
const tint = ent && ent.playTint;
|
||||
const body = (tint && typeof tint.body === 'string') ? tint.body : null;
|
||||
const idx = body ? nearestIndexFromHexPlay(body, PLAY_LOBBY_THEME_HEX) : null;
|
||||
return (idx >= 1 && idx <= 8) ? idx : null;
|
||||
}
|
||||
|
||||
document.getElementById('room-id').textContent = spaceId;
|
||||
|
||||
@@ -5483,6 +5562,7 @@
|
||||
if (!playBotsEnabled() || !mapData) return;
|
||||
const human = countPlayHumans();
|
||||
let wantBots = Math.max(0, playBotTargetHeadcount() - human);
|
||||
wantBots = Math.min(wantBots, Math.max(0, 6 - human)); /* total ≤ 6 เสมอ (กัน over-count เช่น reconnect หลัง bot-takeover) */
|
||||
/* Card 3 Ban — บอทที่ถูกแบนรอบนี้ (slot N) ไม่ลงเล่น: ลบทิ้ง + ลดเป้าจำนวนลง 1 + ข้าม slot นั้นตอนสร้าง */
|
||||
if (playBannedBotSlot >= 0) {
|
||||
others.delete(PREVIEW_BOT_PREFIX + playBannedBotSlot);
|
||||
@@ -7275,7 +7355,7 @@
|
||||
if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore;
|
||||
return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id));
|
||||
});
|
||||
const top = uniq.slice(0, 5);
|
||||
const top = uniq.slice(0, 6);
|
||||
const ranked = top.map(function (row, idx) {
|
||||
const pos = idx + 1;
|
||||
return {
|
||||
@@ -7350,7 +7430,7 @@
|
||||
if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore;
|
||||
return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id));
|
||||
});
|
||||
const top = baseRows.slice(0, 5);
|
||||
const top = baseRows.slice(0, 6);
|
||||
const ranked = top.map(function (row, idx) {
|
||||
const pos = idx + 1;
|
||||
const bs = Math.max(0, Number(row.baseScore) || 0);
|
||||
@@ -7722,7 +7802,7 @@
|
||||
}
|
||||
const participantCount = baseRows.length;
|
||||
const grade = jumpSurviveGradeFromSurvivorCount(survivorCount);
|
||||
const top = baseRows.slice(0, 5);
|
||||
const top = baseRows.slice(0, 6);
|
||||
const ranked = top.map(function (row, idx) {
|
||||
const pos = idx + 1;
|
||||
const bs = Math.max(0, Number(row.baseScore) || 0);
|
||||
@@ -7975,7 +8055,7 @@
|
||||
if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore;
|
||||
return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id));
|
||||
});
|
||||
const top = rows.slice(0, 5);
|
||||
const top = rows.slice(0, 6);
|
||||
const ranked = top.map(function (row, idx) {
|
||||
const pos = idx + 1;
|
||||
/* โบนัสอยู่รอดจนจบเกม = +20 (เฉพาะคนไม่ตาย) — รวมใน finalScore เหมือน rankBonus ของ MG2 */
|
||||
@@ -8049,7 +8129,7 @@
|
||||
if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore;
|
||||
return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id));
|
||||
});
|
||||
const top = baseRows.slice(0, 5);
|
||||
const top = baseRows.slice(0, 6);
|
||||
const ranked = top.map(function (row, idx) {
|
||||
const pos = idx + 1;
|
||||
const bs = Math.max(0, Number(row.baseScore) || 0);
|
||||
@@ -8122,7 +8202,7 @@
|
||||
if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore;
|
||||
return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id));
|
||||
});
|
||||
const top = baseRows.slice(0, 5);
|
||||
const top = baseRows.slice(0, 6);
|
||||
/* โบนัสจบเกม: ฆ่าบอส(+100)→ทุกคนที่ร่วมเล่น · รอดชีวิต(+10)→เฉพาะคนไม่ตาย (รวมใน finalScore, clamp ≥0) */
|
||||
const endRBonus = mission && mission.balloonBossEndReason;
|
||||
const killBonusAll = endRBonus === 'victory' ? 100 : 0;
|
||||
@@ -8197,7 +8277,7 @@
|
||||
rows.sort((a, b) => b.baseScore - a.baseScore
|
||||
|| String(a.nickname).localeCompare(String(b.nickname), 'th')
|
||||
|| String(a.id).localeCompare(String(b.id)));
|
||||
const top = rows.slice(0, 5);
|
||||
const top = rows.slice(0, 6);
|
||||
/* โบนัสจบเกม: ฆ่าบอส(+100)→ทุกคนที่ร่วมเล่น · รอดชีวิต(+10)→เฉพาะคนไม่ตาย (รวมใน finalScore, clamp ≥0) */
|
||||
const killBonusAll = reason === 'victory' ? 100 : 0;
|
||||
const ranked = top.map((row, idx) => {
|
||||
@@ -8545,7 +8625,7 @@
|
||||
if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore;
|
||||
return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id));
|
||||
});
|
||||
const top = rows.slice(0, 5);
|
||||
const top = rows.slice(0, 6);
|
||||
const ranked = top.map(function (row, idx) {
|
||||
const pos = idx + 1;
|
||||
return {
|
||||
@@ -8622,7 +8702,7 @@
|
||||
if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore;
|
||||
return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id));
|
||||
});
|
||||
const top = baseRows.slice(0, 5);
|
||||
const top = baseRows.slice(0, 6);
|
||||
const ranked = top.map(function (row, idx) {
|
||||
const pos = idx + 1;
|
||||
const bs = Math.max(0, Number(row.baseScore) || 0);
|
||||
@@ -8666,7 +8746,7 @@
|
||||
function quizCarryEmbedMissionFlowActive() {
|
||||
if (!isQuizCarry()) return false;
|
||||
if (previewMode && editorEmbedReturn) return true;
|
||||
return !!(isDetectiveMinigamePlay() && quizCarryUseMissionSummaryOverlay);
|
||||
return !!(isDetectiveMinigamePlay() && quizCarryUseMissionSummaryOverlay());
|
||||
}
|
||||
/** quiz_carry: จุดกลางแมป (world px) — กล้องไม่ตามตัวละคร; ใช้ร่วม draw + overlay DOM */
|
||||
function getQuizCarryMapCameraWorldCenterPxPlay() {
|
||||
@@ -9504,6 +9584,115 @@
|
||||
}, 12000);
|
||||
}
|
||||
|
||||
/* ───────────────────────────────────────────────────────────────────────
|
||||
ตัวบอกสถานะเน็ต (เพิ่มล้วน — ไม่แตะ game logic): ป้าย "เน็ตหน่วง"/"กำลังเชื่อมต่อใหม่"
|
||||
แสดงมุมขวาบน. วัด RTT เองด้วย time-sync (server มี handler อยู่แล้ว ไม่ต้องแก้ server)
|
||||
───────────────────────────────────────────────────────────────────────── */
|
||||
const NET_RTT_WARN_MS = 320; /* เกินนี้ = เน็ตหน่วง (เหลือง) */
|
||||
const NET_RTT_BAD_MS = 700; /* เกินนี้ = แย่มาก (แดง) */
|
||||
let netStatusEl = null;
|
||||
let netLastRtt = null;
|
||||
let netReconnecting = false;
|
||||
let netPingTimer = null;
|
||||
let netOkHideTimer = null;
|
||||
function ensureNetStatusEl() {
|
||||
if (netStatusEl) return netStatusEl;
|
||||
const css = document.createElement('style');
|
||||
css.textContent = ''
|
||||
+ '#play-net-status{position:fixed;top:10px;right:12px;z-index:9400;display:none;align-items:center;gap:7px;'
|
||||
+ 'padding:7px 13px;border-radius:11px;font-family:inherit;font-weight:700;font-size:14px;line-height:1;'
|
||||
+ 'color:#fff;box-shadow:0 4px 16px rgba(0,0,0,.42);pointer-events:none;backdrop-filter:blur(2px);'
|
||||
+ 'transition:opacity .25s;letter-spacing:.2px;}'
|
||||
+ '#play-net-status.is-warn{background:linear-gradient(135deg,#c9831a,#e0a52b);}'
|
||||
+ '#play-net-status.is-bad{background:linear-gradient(135deg,#b3231d,#d84038);}'
|
||||
+ '#play-net-status.is-ok{background:linear-gradient(135deg,#1b9e4b,#13c06a);}'
|
||||
+ '#play-net-status .pns-dot{width:9px;height:9px;border-radius:50%;background:#fff;opacity:.92;}'
|
||||
+ '#play-net-status.is-bad .pns-spin{width:13px;height:13px;border:2px solid rgba(255,255,255,.4);'
|
||||
+ 'border-top-color:#fff;border-radius:50%;animation:pnsSpin .8s linear infinite;}'
|
||||
+ '@keyframes pnsSpin{to{transform:rotate(360deg);}}';
|
||||
document.head.appendChild(css);
|
||||
const el = document.createElement('div');
|
||||
el.id = 'play-net-status';
|
||||
el.innerHTML = '<span class="pns-icon"></span><span class="pns-text"></span>';
|
||||
document.body.appendChild(el);
|
||||
netStatusEl = el;
|
||||
return el;
|
||||
}
|
||||
function setNetStatus(kind, text, withSpin) {
|
||||
const el = ensureNetStatusEl();
|
||||
if (netOkHideTimer) { clearTimeout(netOkHideTimer); netOkHideTimer = null; }
|
||||
if (kind === 'hidden') { el.style.display = 'none'; return; }
|
||||
el.className = 'is-' + kind;
|
||||
const icon = el.querySelector('.pns-icon');
|
||||
if (icon) icon.innerHTML = withSpin ? '<span class="pns-spin"></span>' : '<span class="pns-dot"></span>';
|
||||
const t = el.querySelector('.pns-text');
|
||||
if (t) t.textContent = text;
|
||||
el.style.display = 'flex';
|
||||
el.style.opacity = '1';
|
||||
if (kind === 'ok') {
|
||||
netOkHideTimer = setTimeout(function () { el.style.opacity = '0'; setTimeout(function () { if (netStatusEl) netStatusEl.style.display = 'none'; }, 280); }, 1600);
|
||||
}
|
||||
}
|
||||
function refreshNetStatus() {
|
||||
if (netReconnecting || (socket && socket.connected === false)) {
|
||||
setNetStatus('bad', 'กำลังเชื่อมต่อใหม่…', true);
|
||||
return;
|
||||
}
|
||||
if (netLastRtt != null && netLastRtt >= NET_RTT_BAD_MS) {
|
||||
setNetStatus('bad', 'เน็ตหน่วงมาก (' + Math.round(netLastRtt) + ' ms)', false);
|
||||
} else if (netLastRtt != null && netLastRtt >= NET_RTT_WARN_MS) {
|
||||
setNetStatus('warn', 'เน็ตหน่วง (' + Math.round(netLastRtt) + ' ms)', false);
|
||||
} else {
|
||||
setNetStatus('hidden');
|
||||
}
|
||||
}
|
||||
function netPingOnce() {
|
||||
if (!socket || !socket.connected) return;
|
||||
const t0 = Date.now();
|
||||
let done = false;
|
||||
try {
|
||||
socket.emit('time-sync', function () {
|
||||
if (done) return; done = true;
|
||||
netLastRtt = Date.now() - t0;
|
||||
refreshNetStatus();
|
||||
});
|
||||
} catch (_e) { /* ignore */ }
|
||||
setTimeout(function () { if (!done) { done = true; netLastRtt = Math.max(netLastRtt || 0, NET_RTT_BAD_MS); refreshNetStatus(); } }, 2500);
|
||||
}
|
||||
function setupNetStatusIndicator() {
|
||||
if (netPingTimer) return;
|
||||
try {
|
||||
socket.on('disconnect', function () { netReconnecting = true; refreshNetStatus(); });
|
||||
socket.on('connect', function () {
|
||||
if (netReconnecting) { netReconnecting = false; setNetStatus('ok', 'เชื่อมต่อแล้ว', false); }
|
||||
else { netReconnecting = false; }
|
||||
netPingOnce();
|
||||
});
|
||||
if (socket.io && socket.io.on) {
|
||||
socket.io.on('reconnect_attempt', function () { netReconnecting = true; refreshNetStatus(); });
|
||||
socket.io.on('reconnect', function () { netReconnecting = false; setNetStatus('ok', 'เชื่อมต่อแล้ว', false); });
|
||||
}
|
||||
} catch (_e) { /* ignore */ }
|
||||
netPingTimer = setInterval(netPingOnce, 4000);
|
||||
netPingOnce();
|
||||
}
|
||||
|
||||
/* #20 แสดงเลข version มุมขวาล่าง — อ่านจาก ?v= ของ <script play.js> อัตโนมัติ (sync เองทุก deploy) */
|
||||
(function showPlayBuildVersion() {
|
||||
try {
|
||||
let ver = '';
|
||||
const sc = document.querySelector('script[src*="play.js?v="]');
|
||||
if (sc && sc.src) { const m = sc.src.match(/[?&]v=([^&]+)/); if (m) ver = decodeURIComponent(m[1]); }
|
||||
if (!ver) return;
|
||||
const el = document.createElement('div');
|
||||
el.id = 'play-build-version';
|
||||
el.textContent = 'v' + ver;
|
||||
el.style.cssText = 'position:fixed;right:6px;bottom:5px;z-index:9300;font:600 11px system-ui,sans-serif;'
|
||||
+ 'color:rgba(255,255,255,.5);background:rgba(0,0,0,.32);padding:2px 7px;border-radius:7px;pointer-events:none;letter-spacing:.3px;';
|
||||
document.body.appendChild(el);
|
||||
} catch (_e) { /* ignore */ }
|
||||
})();
|
||||
|
||||
let playMissionLiveBeginsAt = 0; /* server epoch ms ที่เกมเริ่มจริงพร้อมกันทุกคน (0 = ยังไม่รู้) */
|
||||
function setPlayMissionLiveBeginsAt(v) {
|
||||
const n = Number(v);
|
||||
@@ -10014,7 +10203,7 @@
|
||||
});
|
||||
});
|
||||
ranks.sort((a, b) => b.score - a.score || String(a.nickname).localeCompare(String(b.nickname), 'th'));
|
||||
return ranks.slice(0, 5);
|
||||
return ranks.slice(0, 6);
|
||||
}
|
||||
|
||||
/** payload สำหรับ overlay สรุปผลกลาง (gauntlet-crown-mission-overlay) — ให้ MG4 ใช้หน้าจบแบบเดียวกับเกมอื่น */
|
||||
@@ -10411,7 +10600,7 @@
|
||||
const phaseEl = document.getElementById('quiz-game-phase-label');
|
||||
if (phaseEl) phaseEl.textContent = 'ครบ ' + String(quizCarryRoundsCompleted) + ' ข้อ';
|
||||
const missionOv = document.getElementById('quiz-carry-mission-overlay');
|
||||
if (missionOv && quizCarryUseMissionSummaryOverlay) {
|
||||
if (missionOv && quizCarryUseMissionSummaryOverlay()) {
|
||||
cancelQuizCarrySessionCompleteResultToSummary();
|
||||
hideQuizCarryResultEndLayer();
|
||||
const summaryOpts = { forceGradeF: !!(playQuizPlayerLocal && playQuizPlayerLocal.eliminated) };
|
||||
@@ -11133,7 +11322,8 @@
|
||||
carryReadMs: clampPreviewMs(s.carryReadMs, 3000, 0, 120000),
|
||||
carryAnswerMs: clampPreviewMs(s.carryAnswerMs, 5000, 1000, 300000),
|
||||
};
|
||||
quizCarrySessionLength = clampCarrySessionLen(s.carrySessionLength, 0);
|
||||
/* default 10 ข้อ (MG4 detective ต้องจบเมื่อครบจำนวน — 0 = เล่นไม่จบ) bug #41 */
|
||||
{ const _csl = clampCarrySessionLen(s.carrySessionLength, 10); quizCarrySessionLength = _csl > 0 ? _csl : 10; }
|
||||
setQuizCarryMapPanelThemeFromApi(s);
|
||||
setQuizCarryEmbedCountdownThemeFromApi(s);
|
||||
setQuizCarryChoicePlaqueThemeFromApi(s);
|
||||
@@ -16250,7 +16440,7 @@
|
||||
const maxHp = 1 + Math.floor(rng() * SPACE_SHOOTER_ASTEROID_MAX_HP);
|
||||
const ax = 36 + rng() * (mw - 72);
|
||||
const baseY = -45 - rng() * 90;
|
||||
const avy = 58 + rng() * 62;
|
||||
const avy = 92 + rng() * 84; /* #47 เร็วขึ้น (เดิม 58+62) */
|
||||
const spawnTimeMs = spaceShooterLiveAnchorServerMs + k * interval;
|
||||
const elapsedSec = Math.max(0, (serverNow() - spawnTimeMs) / 1000);
|
||||
spaceShooterAsteroids.push({
|
||||
@@ -16273,7 +16463,7 @@
|
||||
x: 36 + Math.random() * (mw - 72),
|
||||
y: -45 - Math.random() * 90,
|
||||
r: spaceShooterAsteroidRadiusFromHpPlay(hp0),
|
||||
vy: 58 + Math.random() * 62,
|
||||
vy: 92 + Math.random() * 84, /* #47 เร็วขึ้น (เดิม 58+62) */
|
||||
maxHp: maxHp,
|
||||
hp: hp0,
|
||||
});
|
||||
@@ -16491,7 +16681,10 @@
|
||||
(เดิมใช้ ent.spaceShooterSlot ที่ freeze ไว้ → เพี้ยนถ้า others ยังไม่ครบตอน layout) */
|
||||
const slot = (idx % 6) + 1;
|
||||
const col = SPACE_SHOOTER_SHIP_COLORS[(slot - 1) % SPACE_SHOOTER_SHIP_COLORS.length];
|
||||
const shipUrl = (playSpaceShooterShipImageUrls[slot - 1] || '').trim();
|
||||
/* สียานตาม lobby theme ของผู้เล่น (Rocket-1..8) ก่อน — fallback admin config ตาม slot (bug #3 สีตามuser) */
|
||||
const shipThemeIdx = playLobbyThemeIndexForEntityPlay(ent);
|
||||
const shipUrl = (shipThemeIdx ? SPACE_SHOOTER_ROCKET_THEME_URLS[shipThemeIdx - 1] : '')
|
||||
|| (playSpaceShooterShipImageUrls[slot - 1] || '').trim();
|
||||
const shipRec = shipUrl ? ensureGauntletAssetImage(shipUrl) : null;
|
||||
const shipImg = shipRec && shipRec.ready && shipRec.img && shipRec.img.naturalWidth > 0 ? shipRec.img : null;
|
||||
let shipDrawW = bodyW * 2.2;
|
||||
@@ -19343,6 +19536,7 @@
|
||||
desiredLobbySkinToneIndex: __chosenColors.skinIndex || undefined,
|
||||
}, (res) => {
|
||||
if (!res || !res.ok) {
|
||||
hidePlayLoadingOverlay(); /* #5 ซ่อนหน้าโหลดก่อนโชว์ alert (ไม่งั้น alert โดนบัง) */
|
||||
const errMsg = (res && res.error) || 'เข้าร่วมไม่ได้';
|
||||
const caseLocked = /เริ่มคดี|ไม่รับผู้เล่น/.test(errMsg);
|
||||
showPlayAlert(errMsg, {
|
||||
@@ -19361,6 +19555,7 @@
|
||||
myId = socket.id;
|
||||
try { syncPlayClock(); } catch (_sc) { /* ignore */ }
|
||||
try { startPeriodicClockSync(); } catch (_sci) { /* ignore */ }
|
||||
try { setupNetStatusIndicator(); } catch (_nsi) { /* ignore */ }
|
||||
if (res && res.missionLiveBeginsAt != null) setPlayMissionLiveBeginsAt(res.missionLiveBeginsAt);
|
||||
if (playBannedSpectator) setupBannedSpectatorUi();
|
||||
if (specialQuizForceDebug) {
|
||||
@@ -19988,6 +20183,10 @@
|
||||
}
|
||||
o.direction = data.direction || o.direction;
|
||||
if (data.characterId != null) o.characterId = data.characterId;
|
||||
/* รีเฟรชสีจาก lobby index ที่ server พ่วงมาใน user-move (ทุกเกม) — กัน peer ได้สี hash สุ่มไม่ตรง lobbyB
|
||||
(MG6 ยิงอุกาบาต/MG7 ลูกโป่ง ไม่มี sync สีต่อ tick เหมือน gauntlet → เคยตกไป hash) */
|
||||
const moveTint = playTintFromJoinPeer(data);
|
||||
if (moveTint) o.playTint = moveTint;
|
||||
if (mapData && isQuizCarry() && data.quizCarryHeld !== undefined) {
|
||||
o.quizCarryHeld = (data.quizCarryHeld == null ? null : data.quizCarryHeld);
|
||||
}
|
||||
@@ -20040,6 +20239,50 @@
|
||||
if (isJumpSurviveMissionUiMapPlay() && jumpSurviveMissionPhase === 'howto') updateJumpSurviveMissionHowtoHud();
|
||||
if (isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase === 'howto') updateSpaceShooterMissionHowtoHud();
|
||||
});
|
||||
|
||||
/* ===== Disconnect grace + bot takeover (10 วิ รอ reconnect) ===== */
|
||||
let _playGraceCountdownTimer = null;
|
||||
function showDisconnectGraceBanner(nickname, seconds) {
|
||||
let el = document.getElementById('play-grace-banner');
|
||||
if (!el) {
|
||||
el = document.createElement('div');
|
||||
el.id = 'play-grace-banner';
|
||||
el.style.cssText = 'position:fixed;left:50%;top:12px;transform:translateX(-50%);z-index:9500;'
|
||||
+ 'padding:9px 18px;border-radius:12px;background:rgba(176,55,40,.93);color:#fff;'
|
||||
+ 'font:600 15px system-ui,"Kanit",sans-serif;box-shadow:0 4px 16px rgba(0,0,0,.42);pointer-events:none;letter-spacing:.2px;';
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
let remain = Math.max(1, Math.floor(Number(seconds) || 10));
|
||||
const render = function () { el.textContent = '🔌 ' + nickname + ' หลุด — รอกลับมา ' + remain + ' วิ'; };
|
||||
render();
|
||||
el.style.display = 'block';
|
||||
if (_playGraceCountdownTimer) clearInterval(_playGraceCountdownTimer);
|
||||
_playGraceCountdownTimer = setInterval(function () {
|
||||
remain -= 1;
|
||||
if (remain <= 0) { hideDisconnectGraceBanner(); return; }
|
||||
render();
|
||||
}, 1000);
|
||||
}
|
||||
function hideDisconnectGraceBanner() {
|
||||
if (_playGraceCountdownTimer) { clearInterval(_playGraceCountdownTimer); _playGraceCountdownTimer = null; }
|
||||
const el = document.getElementById('play-grace-banner');
|
||||
if (el) el.style.display = 'none';
|
||||
}
|
||||
socket.on('peer-disconnect-grace', (d) => { if (!d) return; showDisconnectGraceBanner(d.nickname || 'ผู้เล่น', d.seconds || 10); });
|
||||
socket.on('peer-grace-cancel', () => { hideDisconnectGraceBanner(); });
|
||||
socket.on('peer-bot-takeover', () => {
|
||||
hideDisconnectGraceBanner();
|
||||
/* host เติมบอท 1 ตัวเล่นแทนคนที่หลุด (ถ้ายังไม่เต็ม 6) — ใช้ rebalancePreviewBots เดิม */
|
||||
try {
|
||||
if (isMePlayHost()) {
|
||||
detectiveCaseFillBots = true;
|
||||
if (countPlayHumans() + detectiveBotSlotCount < 6) detectiveBotSlotCount += 1;
|
||||
rebalancePreviewBots();
|
||||
if (typeof broadcastFillBotState === 'function') broadcastFillBotState();
|
||||
}
|
||||
} catch (_e) { /* ignore */ }
|
||||
});
|
||||
|
||||
socket.on('chat', (data) => {
|
||||
const box = document.getElementById('chat-messages');
|
||||
if (!box) return;
|
||||
@@ -20339,6 +20582,8 @@
|
||||
socket.on('quiz-phase', (p) => {
|
||||
if (previewMode) return;
|
||||
if (!p || !mapData || !isQuiz()) return;
|
||||
/* จบเกมแล้ว — เพิกเฉย quiz-phase ที่มาช้า ไม่งั้น applyQuizPhaseFromServer restart timer → "TIME" เดินต่อทับสรุป (bug #34) */
|
||||
if (isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'ended') return;
|
||||
if (isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase !== 'live' && quizQuestionMissionPhase !== 'ended') {
|
||||
quizQuestionMissionDeferredPhase = p;
|
||||
return;
|
||||
@@ -20439,6 +20684,9 @@
|
||||
}
|
||||
playQuizText = '';
|
||||
playQuizPhaseEndsAt = 0;
|
||||
/* เก็บ "มีใครได้คะแนน ≥1" ก่อนล้าง playLiveQuizScores — เดิมล้างก่อนเช็ค → mg1AnyScore=false เสมอ
|
||||
→ ขึ้น result-gameover ทับสรุปทุกครั้งแม้มีคนตอบถูก (bug #34) */
|
||||
const mg1AnyScore = Object.keys(playLiveQuizScores).some(function (k) { return (playLiveQuizScores[k] | 0) > 0; });
|
||||
playLiveQuizScores = {};
|
||||
hidePlayQuizHud();
|
||||
if (questionMissionEnd) {
|
||||
@@ -20447,8 +20695,8 @@
|
||||
applyQuizQuestionMissionPanelImages();
|
||||
/* ตามกฎใหม่: มีใครตอบถูก ≥1 = result-complete · ไม่มีใครได้เลย = result-gameover (ไม่มี timeup)
|
||||
ตัดสินจาก playLiveQuizScores ตรงๆ (มีครบทุกคน + merge authoritative แล้ว) — ไม่ใช่ ranked
|
||||
(ranked สร้างจาก me+others เท่านั้น → ถ้าคนตอบถูกไม่อยู่ใน others ของจอนี้ คะแนนหาย = ผลเพี้ยน/2 จอไม่ตรง) */
|
||||
const mg1AnyScore = Object.keys(playLiveQuizScores).some(function (k) { return (playLiveQuizScores[k] | 0) > 0; });
|
||||
(ranked สร้างจาก me+others เท่านั้น → ถ้าคนตอบถูกไม่อยู่ใน others ของจอนี้ คะแนนหาย = ผลเพี้ยน/2 จอไม่ตรง)
|
||||
mg1AnyScore เก็บไว้ก่อนล้าง playLiveQuizScores แล้ว (ดูด้านบน) */
|
||||
beginSimpleResultFlashThenGcmPlay(questionMissionEnd, mg1AnyScore ? 'result-complete.png' : 'result-gameover.png');
|
||||
}
|
||||
});
|
||||
@@ -21531,7 +21779,9 @@
|
||||
}
|
||||
|
||||
/** Same walkability as room-lobby `canWalkLobby` (LobbyA / hall). */
|
||||
function canWalkLikeLobby(x, y, fromX, fromY) {
|
||||
/* softPeer=true → ข้ามเฉพาะการชน "ผู้เล่น/บอทอื่น" (blockPlayer zone) — ใช้ตอน anti-stuck
|
||||
ปลดล็อกเดินผ่านกันชั่วคราว แต่ยังคงกำแพง/ขอบ/area quiz/hub ทุกอย่าง */
|
||||
function canWalkLikeLobby(x, y, fromX, fromY, softPeer) {
|
||||
if (!mapData || !mapData.objects) return false;
|
||||
if (typeof x !== 'number' || typeof y !== 'number' || !Number.isFinite(x) || !Number.isFinite(y)) return false;
|
||||
const w = mapData.width || 20, h = mapData.height || 15;
|
||||
@@ -21549,7 +21799,7 @@
|
||||
if (wallTiles.size < colW * colH) return false;
|
||||
}
|
||||
const bp = mapData.blockPlayer;
|
||||
if (bp) {
|
||||
if (bp && !softPeer) {
|
||||
for (const k of quizTilesFootprintPlay(x, y)) {
|
||||
const p = k.split(',');
|
||||
const tx = +p[0], ty = +p[1];
|
||||
@@ -21723,6 +21973,15 @@
|
||||
o.botPathStuckTicks = 0;
|
||||
} else {
|
||||
o.botPathStuckTicks = (o.botPathStuckTicks | 0) + 1;
|
||||
/* anti-stuck (MG1/MG4): ติดเพื่อนนานพอ → เขย่า + ลองเดินผ่าน peer (ignorePeers) คงกำแพง/area เหมือนผู้เล่น */
|
||||
if ((isQuiz() || isQuizCarry()) && (o.botPathStuckTicks | 0) >= ANTI_STUCK_PEER_TICKS) {
|
||||
o._stuckShakeUntil = Date.now() + 200;
|
||||
if (canWalkLikeLobbyForBot(nx, ny, o.x, o.y, o, true)) { o.x = nx; o.y = ny; }
|
||||
else if (canWalkLikeLobbyForBot(nx, o.y, o.x, o.y, o, true)) { o.x = nx; }
|
||||
else if (canWalkLikeLobbyForBot(o.x, ny, o.x, o.y, o, true)) { o.y = ny; }
|
||||
clampPlayEntityFootprintToMap(o, mapData);
|
||||
if (Math.abs(o.x - ox) > 1e-5 || Math.abs(o.y - oy) > 1e-5) { o.botIsWalking = true; o.botPathStuckTicks = 0; }
|
||||
}
|
||||
if ((o.botPathStuckTicks | 0) >= 20) {
|
||||
o.botPathStuckTicks = 0;
|
||||
if (path.length) path.shift();
|
||||
@@ -23560,6 +23819,11 @@
|
||||
}
|
||||
document.documentElement.classList.toggle('play-stack-tower-pixel-canvas', isStackTowerMissionUiMapPlay());
|
||||
} catch (e) { /* ignore */ }
|
||||
/* #5 ฉากเริ่มเรนเดอร์แล้ว (มี mapData) → ซ่อนหน้าโหลด หลังหน่วงสั้น ๆ ให้ asset แรกขึ้น */
|
||||
if (!playLoadingHidden && !playLoadingHideScheduled) {
|
||||
playLoadingHideScheduled = true;
|
||||
setTimeout(hidePlayLoadingOverlay, 320);
|
||||
}
|
||||
if (isGauntletCrownHeistMapPlay()) ensureGauntletCrownScorePenaltyImgPlay();
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
const w = mapData.width, h = mapData.height;
|
||||
@@ -24266,6 +24530,13 @@
|
||||
for (let i = 0; i < (id || '').length; i++) h = (h * 31 + (id || '').charCodeAt(i)) >>> 0;
|
||||
return { ax: ((h % 5) - 2) * 0.1, ay: ((Math.floor(h / 5) % 5) - 2) * 0.1 };
|
||||
}
|
||||
/** Anti-stuck (MG1/MG4): เขย่าแนวนอนเล็กๆ ตอนดันชนเพื่อน — แสดง "กำลังเบียด" ก่อนเดินผ่าน */
|
||||
function antiStuckShakeXPlay(ent) {
|
||||
if (!ent || !ent._stuckShakeUntil) return 0;
|
||||
const now = Date.now();
|
||||
if (now >= ent._stuckShakeUntil) return 0;
|
||||
return Math.sin(now * 0.045 + (ent._stuckShakeSeed || 0)) * 0.07; /* ±0.07 ช่อง */
|
||||
}
|
||||
function shouldGauntletCrownHeistSkipAvatarDrawPlay(ent, isMeFlag, offAx, offAy) {
|
||||
if (!isGauntletCrownHeistMapPlay() || gauntletCrownPregamePhase !== 'live') return false;
|
||||
if (!ent || ent.gauntletEliminated) return true;
|
||||
@@ -24340,7 +24611,7 @@
|
||||
const peerName = botOut ? (o.nickname + ' (ตกรอบ)') : o.nickname;
|
||||
if (shouldGauntletCrownHeistSkipAvatarDrawPlay(o, false, off.ax, off.ay)) return;
|
||||
if (isJumpSurvive() && o.jumpSurviveEliminated) return; /* MG5 ตายแล้ว → ตัวละครหายไป */
|
||||
const axO = safeX(o.x) + off.ax;
|
||||
const axO = safeX(o.x) + off.ax + antiStuckShakeXPlay(o);
|
||||
const ayO = safeY(o.y) + off.ay;
|
||||
/* ผีต้องโชว์เสมอ แม้ทับโซนคำตอบ (เดินไปไหนก็ได้) */
|
||||
const ghostActiveO = isQuizWrongGhostActiveForEnt(id);
|
||||
@@ -24361,7 +24632,7 @@
|
||||
} else {
|
||||
if (shouldGauntletCrownHeistSkipAvatarDrawPlay(me, true, 0, 0)) return;
|
||||
if (isJumpSurvive() && jumpSurviveEliminated) return; /* MG5 ตายแล้ว → ตัวละครหายไป */
|
||||
const axMe = safeX(me.x);
|
||||
const axMe = safeX(me.x) + antiStuckShakeXPlay(me);
|
||||
const ayMe = safeY(me.y);
|
||||
const ghostActiveMe = isQuizWrongGhostActiveForEnt(myId);
|
||||
if (!ghostActiveMe && playQuizAvatarHideWhileOverlappingAnswerZone(me, myId, axMe, ayMe)) return;
|
||||
@@ -24884,20 +25155,44 @@
|
||||
const tox = me.x + (accX / len) * step;
|
||||
const toy = me.y + (accY / len) * step;
|
||||
const pathStrict = isQuizBattle() && quizBattlePathModeActive(mapData);
|
||||
const antiStuckGame = !pathStrict && (isQuiz() || isQuizCarry());
|
||||
let normalMoved = false;
|
||||
if (pathStrict) {
|
||||
const moved = quizBattleTryStepMovePlay(me.x, me.y, tox, toy, (nx, ny, ox, oy) => canWalkLikeLobby(nx, ny, ox, oy));
|
||||
me.x = moved.x;
|
||||
me.y = moved.y;
|
||||
normalMoved = true;
|
||||
} else if (canWalkLikeLobby(tox, toy, me.x, me.y)) {
|
||||
me.x = tox;
|
||||
me.y = toy;
|
||||
} else {
|
||||
if (canWalkLikeLobby(tox, me.y, me.x, me.y)) {
|
||||
me.x = tox;
|
||||
} else if (canWalkLikeLobby(me.x, toy, me.x, me.y)) {
|
||||
me.y = toy;
|
||||
normalMoved = true;
|
||||
} else if (canWalkLikeLobby(tox, me.y, me.x, me.y)) {
|
||||
me.x = tox;
|
||||
normalMoved = true;
|
||||
} else if (canWalkLikeLobby(me.x, toy, me.x, me.y)) {
|
||||
me.y = toy;
|
||||
normalMoved = true;
|
||||
} else if (antiStuckGame) {
|
||||
/* ปกติเดินไม่ได้ → เช็คว่าติด "เพื่อน" (soft ผ่านได้ แต่ยังติดกำแพง/area) หรือ "กำแพง"
|
||||
ติดเพื่อน: สั่น (แสดงการดัน) + นับเฟรม → ดันครบ ANTI_STUCK_PEER_TICKS แล้วปลดล็อกเดินผ่านชั่วคราว */
|
||||
const sF = canWalkLikeLobby(tox, toy, me.x, me.y, true);
|
||||
const sX = canWalkLikeLobby(tox, me.y, me.x, me.y, true);
|
||||
const sY = canWalkLikeLobby(me.x, toy, me.x, me.y, true);
|
||||
if (sF || sX || sY) {
|
||||
me._stuckPeerTicks = (me._stuckPeerTicks || 0) + 1;
|
||||
me._stuckShakeUntil = Date.now() + 200;
|
||||
const winOpen = me._antiStuckUntil && Date.now() < me._antiStuckUntil;
|
||||
if (me._stuckPeerTicks >= ANTI_STUCK_PEER_TICKS || winOpen) {
|
||||
if (sF) { me.x = tox; me.y = toy; } else if (sX) { me.x = tox; } else if (sY) { me.y = toy; }
|
||||
me._antiStuckUntil = Date.now() + ANTI_STUCK_WINDOW_MS;
|
||||
}
|
||||
} else {
|
||||
me._stuckPeerTicks = 0; /* ติดกำแพง/area ไม่ใช่เพื่อน — ไม่ปลดล็อก */
|
||||
}
|
||||
}
|
||||
if (normalMoved) me._stuckPeerTicks = 0;
|
||||
} else {
|
||||
me._stuckPeerTicks = 0;
|
||||
}
|
||||
clampPlayEntityFootprintToMap(me, mapData);
|
||||
enforceQuizBattleLaneOnMePlay();
|
||||
|
||||
@@ -496,6 +496,8 @@
|
||||
ov.id = 'room-loading-overlay';
|
||||
ov.innerHTML = '<div id="room-loading-spinner"></div><div class="rl-txt">กำลังโหลดตัวละคร…</div>';
|
||||
(document.body || document.documentElement).appendChild(ov);
|
||||
/* safety: กัน overlay ค้างถาวรถ้า preload/manifest แฮงค์ (ไม่มี roomPreloadReady) */
|
||||
setTimeout(hideRoomLoading, 12000);
|
||||
}
|
||||
|
||||
function hideRoomLoading() {
|
||||
@@ -507,6 +509,27 @@
|
||||
|
||||
function maybeHideRoomLoading() { if (roomJoinReady && roomPreloadReady) hideRoomLoading(); }
|
||||
|
||||
/* #5 เปิดหน้าโหลดเต็มจอ "อีกครั้ง" (ตอนเปลี่ยน scene) — กันเห็นฉากเก่าก่อนฉากใหม่พร้อม
|
||||
opts.persist=true → ไม่ต้องตั้ง safety auto-hide (ใช้ก่อน navigate ออกจากหน้า) */
|
||||
function showRoomLoadingOverlay(text, opts) {
|
||||
roomLoadingHidden = false;
|
||||
var ov = document.getElementById('room-loading-overlay');
|
||||
if (!ov) { injectRoomLoadingOverlay(); ov = document.getElementById('room-loading-overlay'); }
|
||||
if (ov) { ov.classList.remove('is-hidden'); ov.style.opacity = ''; }
|
||||
var t = ov && ov.querySelector('.rl-txt');
|
||||
if (t) t.textContent = text || 'กำลังโหลด…';
|
||||
if (!(opts && opts.persist)) setTimeout(hideRoomLoading, 12000);
|
||||
}
|
||||
|
||||
/* แสดง % บนหน้าโหลด LobbyB (ตอนกลับจากมินิเกม) — ตามความคืบหน้า preload ตัวละคร */
|
||||
function setRoomLoadingPct(pct) {
|
||||
if (roomLoadingHidden) return;
|
||||
var el = document.querySelector('#room-loading-overlay .rl-txt');
|
||||
if (!el) return;
|
||||
var p = Math.round(Math.max(0, Math.min(100, Number(pct) || 0)));
|
||||
el.textContent = 'กำลังโหลดตัวละคร… ' + p + '%';
|
||||
}
|
||||
|
||||
function preloadLobbyTintedCharacter(id, theme, skin, cb) {
|
||||
if (!id || (!theme && !skin)) { if (cb) cb(); return; }
|
||||
var dirs = ['down', 'up', 'left', 'right'];
|
||||
@@ -547,31 +570,32 @@
|
||||
}
|
||||
|
||||
/** อุ่น tint บอท + peer ที่มีสี — กันกระตุกตอนบอทเดิน/idle ครั้งแรก */
|
||||
function preloadAllLobbyTintedOccupants(cb) {
|
||||
var pending = 1;
|
||||
function doneOne() {
|
||||
pending--;
|
||||
if (pending <= 0 && cb) cb();
|
||||
}
|
||||
function preloadAllLobbyTintedOccupants(cb, onProgress) {
|
||||
var tasks = [];
|
||||
var pid = getStoredCharacterId();
|
||||
if (pid && (myTintTheme || myTintSkin)) {
|
||||
pending++;
|
||||
preloadLobbyTintedCharacter(pid, myTintTheme, myTintSkin, doneOne);
|
||||
}
|
||||
if (pid && (myTintTheme || myTintSkin)) tasks.push([pid, myTintTheme, myTintSkin]);
|
||||
lobbyBots.forEach(function (b, id) {
|
||||
if (!b || !b.characterId) return;
|
||||
if (!b.colorTheme && !b.colorSkin) return;
|
||||
saveLobbyBotTint(id, b); /* sync สีบอท lobby → เกม (เกมอ่าน sessionStorage นี้ ให้สีตรงกัน) */
|
||||
pending++;
|
||||
preloadLobbyTintedCharacter(b.characterId, b.colorTheme, b.colorSkin, doneOne);
|
||||
tasks.push([b.characterId, b.colorTheme, b.colorSkin]);
|
||||
});
|
||||
peers.forEach(function (p) {
|
||||
if (!p || !p.characterId) return;
|
||||
if (!p.colorTheme && !p.colorSkin) return;
|
||||
pending++;
|
||||
preloadLobbyTintedCharacter(p.characterId, p.colorTheme, p.colorSkin, doneOne);
|
||||
tasks.push([p.characterId, p.colorTheme, p.colorSkin]);
|
||||
});
|
||||
var total = tasks.length;
|
||||
var done = 0;
|
||||
if (!total) { if (onProgress) onProgress(1, 1); if (cb) cb(); return; }
|
||||
if (onProgress) onProgress(0, total);
|
||||
tasks.forEach(function (t) {
|
||||
preloadLobbyTintedCharacter(t[0], t[1], t[2], function () {
|
||||
done++;
|
||||
if (onProgress) onProgress(done, total);
|
||||
if (done >= total && cb) cb();
|
||||
});
|
||||
});
|
||||
doneOne();
|
||||
}
|
||||
|
||||
function readLobbyCharImgSize(img) {
|
||||
@@ -1055,7 +1079,8 @@
|
||||
let lastMoveSend = 0;
|
||||
const moveCodes = ['KeyW', 'KeyA', 'KeyS', 'KeyD', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
|
||||
let lobbyInteractPulse = null;
|
||||
let lobbyZoom = 1.2;
|
||||
/* มือถือจอแคบ → zoom ต่ำลง ให้เห็นพื้นที่ lobby กว้างขึ้น (PC ย่อส่วน) · PC คงเดิม 1.2 */
|
||||
let lobbyZoom = (typeof window !== 'undefined' && (window.innerWidth || 0) > 0 && window.innerWidth < 600) ? 0.85 : 1.2;
|
||||
const LOBBY_ZOOM_MIN = 0.4;
|
||||
const LOBBY_ZOOM_MAX = 2.5;
|
||||
let mapTransform = { offsetX: 0, offsetY: 0, ts: 32, w: 20, h: 15 };
|
||||
@@ -1423,7 +1448,10 @@
|
||||
const drawW = imgScale > 0 ? iw * imgScale : 0;
|
||||
const drawH = imgScale > 0 ? ih * imgScale : 0;
|
||||
const drawY = cellBottom - drawH;
|
||||
const shadowImg = getDefaultShadowImg(dir);
|
||||
/* #26 เอาเงาออก 1 ชั้น — เดิมวาด default-shadow overlay ทับเงาที่มีในสไปรต์ (เลเยอร์ 'shadow') = เงา 2 ชั้น
|
||||
ปิด overlay นี้ เหลือเงาในสไปรต์ชั้นเดียว (ตั้ง RL_LOBBY_DRAW_DEFAULT_SHADOW=true เพื่อกลับมาวาด) */
|
||||
const RL_LOBBY_DRAW_DEFAULT_SHADOW = false;
|
||||
const shadowImg = RL_LOBBY_DRAW_DEFAULT_SHADOW ? getDefaultShadowImg(dir) : null;
|
||||
if (shadowImg) {
|
||||
const sw = shadowImg.naturalWidth;
|
||||
const sh = shadowImg.naturalHeight;
|
||||
@@ -1968,12 +1996,20 @@
|
||||
function applySuspectPickImages() {
|
||||
var cards = document.querySelectorAll('#suspect-cards-row .suspect-card');
|
||||
if (!cards.length) return;
|
||||
/* ทันที: เข้าสถานะ loading (ซ่อนรูป generic/เก่าใน HTML + spinner) ก่อนรอ case media — กันรูปแว่บ (#1) */
|
||||
cards.forEach(function (card) { card.classList.add('is-img-loading'); });
|
||||
function paint() {
|
||||
cards.forEach(function (card) {
|
||||
var idx = parseInt(card.getAttribute('data-index'), 10);
|
||||
if (Number.isNaN(idx) || idx < 0 || idx > 2) return;
|
||||
var img = card.querySelector(':scope > img');
|
||||
if (img) img.src = getSuspectPickImageUrl(idx);
|
||||
if (img) {
|
||||
var reveal = function () { card.classList.remove('is-img-loading'); };
|
||||
img.onload = reveal;
|
||||
img.onerror = reveal; /* กัน loading ค้างถ้ารูปเสีย */
|
||||
img.src = getSuspectPickImageUrl(idx);
|
||||
if (img.complete && img.naturalWidth > 0) reveal(); /* cached แล้ว */
|
||||
}
|
||||
/* ปุ่มแว่นขยาย — ดูการ์ดเต็ม + ปิดได้ (ไม่ชนการเลือก/โหวต) */
|
||||
if (!card.querySelector('.suspect-zoom-btn')) {
|
||||
if (getComputedStyle(card).position === 'static') card.style.position = 'relative';
|
||||
@@ -4366,7 +4402,8 @@
|
||||
}
|
||||
|
||||
function canInvestigateSuspect(si) {
|
||||
return true; // เก็บได้ไม่จำกัด
|
||||
/* #1 สืบผู้ต้องสงสัยละไม่เกิน 3 ครั้ง — เต็ม 3 หลักฐานแล้วเลือกสืบซ้ำไม่ได้ (server ก็ reject) */
|
||||
return getSuspectEvidenceCount(si) < 3;
|
||||
}
|
||||
|
||||
function persistMyPlayerEvidenceSession() {
|
||||
@@ -5779,7 +5816,9 @@
|
||||
if (!btn || !img) return;
|
||||
var SC = '/Main-Lobby/IMAGE/Showcard/';
|
||||
var req = Math.max(1, Math.min(2, tmState.requiredPicks || 2));
|
||||
var totalMembers = (tmState.members || []).length;
|
||||
/* ใช้จำนวน authoritative จาก server (ลดเมื่อคนหลุด) ถ้ามี — ไม่งั้น fallback members.length */
|
||||
var totalMembers = (Number.isFinite(tmState.totalPlayers) && tmState.totalPlayers > 0)
|
||||
? tmState.totalPlayers : (tmState.members || []).length;
|
||||
var submittedCount = (tmState.submittedIds || []).length;
|
||||
var allDone = totalMembers > 0 && submittedCount >= totalMembers;
|
||||
var isHost = !!(tmState.hostId && tmState.hostId === socket.id);
|
||||
@@ -6010,6 +6049,8 @@
|
||||
var ov = ensureTestimonyOverlay();
|
||||
tmState.round = (data && typeof data.round === 'number') ? data.round : 0;
|
||||
tmState.members = (data && Array.isArray(data.members)) ? data.members : [];
|
||||
tmState.totalPlayers = (data && Number.isFinite(data.totalPlayers) && data.totalPlayers > 0)
|
||||
? data.totalPlayers : tmState.members.length;
|
||||
tmState.picks = [];
|
||||
tmState.submitted = false;
|
||||
tmState.submittedPicksById = {};
|
||||
@@ -6071,6 +6112,9 @@
|
||||
if (!data) return;
|
||||
tmState.submittedIds = Array.isArray(data.submitted) ? data.submitted.slice() : [];
|
||||
if (data.hostId) tmState.hostId = data.hostId;
|
||||
/* จำนวนผู้ร่วม authoritative จาก server (ลดเมื่อมีคนหลุด) — ใช้คุมปุ่ม host แทน members.length
|
||||
ที่ไม่ลดตอนคนหลุด (กันค้าง "รอคนที่ออกไปแล้ว" → host เปิดหลักฐานไม่ได้) */
|
||||
if (Number.isFinite(data.totalPlayers) && data.totalPlayers > 0) tmState.totalPlayers = data.totalPlayers;
|
||||
es3RenderMembers(tmState.submittedIds, [], false);
|
||||
es3UpdateActionButton();
|
||||
/* อัปเดต status text */
|
||||
@@ -6923,6 +6967,9 @@
|
||||
var cutsceneCid = (data && data.caseId != null) ? data.caseId : null;
|
||||
if (cutsceneCid) {
|
||||
ensureCaseMediaLoaded().then(function () { showCaseCutscene(cutsceneCid, function () {}); });
|
||||
} else {
|
||||
/* #5 ไม่มี cutscene → โชว์หน้าโหลดเต็มจอระหว่าง fetch+swap ฉาก LobbyB (กันเห็นฉาก LobbyA เก่า) */
|
||||
showRoomLoadingOverlay('กำลังโหลดฉาก LobbyB…');
|
||||
}
|
||||
fetch(SERVER + '/api/maps/' + encodeURIComponent(mid))
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
@@ -6980,6 +7027,7 @@
|
||||
resizeAndDraw();
|
||||
updateHostStartGameButton();
|
||||
renderPeers();
|
||||
if (!cutsceneCid) setTimeout(hideRoomLoading, 250); /* #5 ฉาก LobbyB วาดแล้ว → ปิดหน้าโหลด */
|
||||
ensureReadyControlEnabled();
|
||||
if (data.cardMinigames) setSuspectCardMinigames(data.cardMinigames);
|
||||
applyPersonalSuspectState(data);
|
||||
@@ -6999,6 +7047,7 @@
|
||||
})
|
||||
.catch(function () {
|
||||
appendLobbySystemChat('— โหลดแผนที่ LobbyB ไม่สำเร็จ');
|
||||
hideRoomLoading(); /* #5 กัน overlay ค้างถ้าโหลดแมปไม่สำเร็จ */
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7147,6 +7196,9 @@
|
||||
appendLobbySystemChat('— บอทถูกแบนรอบนี้ (Ban Card) · ไม่ได้ลงเล่นมินิเกมรอบนี้');
|
||||
}
|
||||
}
|
||||
/* #5 โชว์หน้าโหลดเต็มจอก่อน navigate เข้าเกม — กันเห็นฉาก LobbyB เก่าระหว่างโหลด play.html
|
||||
(persist: ไม่ auto-hide เพราะกำลังออกจากหน้า → play.html มี play-loading-overlay รับช่วงต่อ) */
|
||||
try { showRoomLoadingOverlay('กำลังเข้าเกม…', { persist: true }); } catch (e) { /* ignore */ }
|
||||
location.href = q;
|
||||
});
|
||||
|
||||
@@ -7275,8 +7327,44 @@
|
||||
lobbyZoomPctHideTimer = null;
|
||||
}, 1500);
|
||||
}
|
||||
/** เดินไปยังจุดที่กด/แตะบน canvas (ใช้ร่วมกัน dblclick (PC) + tap (มือถือ)) — DPI-safe */
|
||||
function lobbyWalkToClientXY(clientX, clientY) {
|
||||
if (!mapData || !canvas) return;
|
||||
const me = peers.get(socket.id);
|
||||
if (!me) return;
|
||||
const r = canvas.getBoundingClientRect();
|
||||
if (!r.width || !r.height) return;
|
||||
/* สเกลด้วย canvas.width/r.width — canvas อาจมีความละเอียดต่างจากขนาดแสดง (มือถือ DPI) */
|
||||
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, w, h } = mapTransform;
|
||||
const gx = (sx - cw / 2) / (z * t) + meX;
|
||||
const gy = (sy - ch / 2) / (z * t) + meY;
|
||||
const tx = Math.floor(gx);
|
||||
const ty = Math.floor(gy);
|
||||
if (tx < 0 || tx >= w || ty < 0 || ty >= h) return;
|
||||
if (!canWalkLobby(tx + 0.5, ty + 0.5)) return;
|
||||
const path = pathfindLobby(me.x, me.y, tx + 0.5, ty + 0.5);
|
||||
if (path.length <= 1) return;
|
||||
lobbyPath = path.slice(1);
|
||||
}
|
||||
if (canvas) {
|
||||
canvas.addEventListener('pointerdown', () => { focusLobbyMapCanvas(); });
|
||||
try { canvas.style.touchAction = 'none'; } catch (_e) { /* กัน browser zoom/pan รบกวนการแตะเดิน */ }
|
||||
let _lobbyTapStart = null;
|
||||
canvas.addEventListener('pointerdown', (e) => {
|
||||
focusLobbyMapCanvas();
|
||||
/* มือถือ: จำจุดเริ่มแตะ → แตะสั้นๆ = เดิน (PC ใช้ dblclick เหมือนเดิม ไม่เพี้ยน) */
|
||||
if (e.pointerType === 'touch') _lobbyTapStart = { x: e.clientX, y: e.clientY, t: Date.now() };
|
||||
});
|
||||
canvas.addEventListener('pointerup', (e) => {
|
||||
if (e.pointerType !== 'touch' || !_lobbyTapStart) return;
|
||||
const dx = e.clientX - _lobbyTapStart.x, dy = e.clientY - _lobbyTapStart.y, dt = Date.now() - _lobbyTapStart.t;
|
||||
_lobbyTapStart = null;
|
||||
/* แตะสั้นๆ ไม่ลาก = เดิน · ลาก/กดค้าง = ข้าม (กันชนกับการ scroll/zoom) */
|
||||
if (dx * dx + dy * dy > 24 * 24 || dt > 700) return;
|
||||
lobbyWalkToClientXY(e.clientX, e.clientY);
|
||||
});
|
||||
canvas.addEventListener('pointercancel', () => { _lobbyTapStart = null; });
|
||||
canvas.addEventListener('wheel', (e) => {
|
||||
e.preventDefault();
|
||||
lobbyZoom *= e.deltaY > 0 ? 0.9 : 1.1;
|
||||
@@ -7284,27 +7372,38 @@
|
||||
redrawLobbyMap();
|
||||
showZoomPct();
|
||||
}, { passive: false });
|
||||
canvas.addEventListener('dblclick', (e) => {
|
||||
if (!mapData) return;
|
||||
const me = peers.get(socket.id);
|
||||
if (!me) return;
|
||||
const r = canvas.getBoundingClientRect();
|
||||
const sx = e.clientX - r.left;
|
||||
const sy = e.clientY - r.top;
|
||||
const { cw, ch, lobbyZoom: z, tileSize: t, meX, meY, w, h } = mapTransform;
|
||||
const gx = (sx - cw / 2) / (z * t) + meX;
|
||||
const gy = (sy - ch / 2) / (z * t) + meY;
|
||||
const tx = Math.floor(gx);
|
||||
const ty = Math.floor(gy);
|
||||
if (tx < 0 || tx >= w || ty < 0 || ty >= h) return;
|
||||
if (!canWalkLobby(tx + 0.5, ty + 0.5)) return;
|
||||
const targX = tx + 0.5, targY = ty + 0.5;
|
||||
const path = pathfindLobby(me.x, me.y, targX, targY);
|
||||
if (path.length <= 1) return;
|
||||
lobbyPath = path.slice(1);
|
||||
});
|
||||
canvas.addEventListener('dblclick', (e) => { lobbyWalkToClientXY(e.clientX, e.clientY); });
|
||||
}
|
||||
|
||||
/* มือถือ (touch-primary) ไม่มีคีย์บอร์ด → เพิ่มปุ่ม "โต้ตอบ" แทนปุ่ม F (เดินไปยืนใกล้จุด แล้วกดปุ่มนี้)
|
||||
dispatch synthetic KeyF → ใช้ handler interaction เดิม (host เลือกระดับ/คดี, ห้องแต่งตัว ฯลฯ) — PC ไม่เห็นปุ่มนี้ */
|
||||
(function setupMobileLobbyInteractBtn() {
|
||||
try {
|
||||
var coarse = window.matchMedia && window.matchMedia('(pointer: coarse)').matches;
|
||||
if (!coarse) return;
|
||||
if (document.getElementById('lobby-mobile-interact-btn')) return;
|
||||
var btn = document.createElement('button');
|
||||
btn.id = 'lobby-mobile-interact-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;';
|
||||
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)}';
|
||||
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 */ }
|
||||
});
|
||||
document.body.appendChild(btn);
|
||||
} catch (_e) { /* ignore */ }
|
||||
})();
|
||||
|
||||
if (readyCheck) {
|
||||
ensureReadyControlEnabled();
|
||||
updateReadyLabelVisual();
|
||||
@@ -7354,7 +7453,10 @@
|
||||
preloadMyTintedCharacter(function () {
|
||||
preloadAllLobbyTintedOccupants(function () {
|
||||
roomPreloadReady = true;
|
||||
setRoomLoadingPct(100);
|
||||
maybeHideRoomLoading();
|
||||
}, function (d, t) {
|
||||
setRoomLoadingPct(t ? (d / t) * 100 : 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -7370,4 +7472,20 @@
|
||||
window.addEventListener('storage', function (e) {
|
||||
if (e.key == null || e.key === 'gameCharacterId') syncLobbyAvatarFromStorage();
|
||||
});
|
||||
|
||||
/* #20 แสดงเลข version มุมขวาล่าง — อ่านจาก ?v= ของ <script room-lobby.js> อัตโนมัติ */
|
||||
(function showLobbyBuildVersion() {
|
||||
try {
|
||||
var ver = '';
|
||||
var sc = document.querySelector('script[src*="room-lobby.js?v="]');
|
||||
if (sc && sc.src) { var m = sc.src.match(/[?&]v=([^&]+)/); if (m) ver = decodeURIComponent(m[1]); }
|
||||
if (!ver) return;
|
||||
var el = document.createElement('div');
|
||||
el.id = 'lobby-build-version';
|
||||
el.textContent = 'v' + ver;
|
||||
el.style.cssText = 'position:fixed;right:6px;bottom:5px;z-index:9300;font:600 11px system-ui,sans-serif;'
|
||||
+ 'color:rgba(255,255,255,.5);background:rgba(0,0,0,.32);padding:2px 7px;border-radius:7px;pointer-events:none;letter-spacing:.3px;';
|
||||
document.body.appendChild(el);
|
||||
} catch (_e) { /* ignore */ }
|
||||
})();
|
||||
})();
|
||||
|
||||
@@ -5304,6 +5304,11 @@
|
||||
#special-quiz-overlay .sq-ov-award-msg:empty { display: none; }
|
||||
#special-quiz-overlay .sq-ov-award-btn { margin-top: 4px; cursor: pointer; border: none; background: url('/Game/img/special-quiz/reward/btn-continue-play.png') center/100% 100% no-repeat; width: clamp(220px, 26vw, 320px); aspect-ratio: 354 / 120; font-size: 0; color: transparent; transition: filter .12s, transform .08s; }
|
||||
#special-quiz-overlay .sq-ov-award-btn:hover { filter: brightness(1.1); transform: translateY(-2px); }
|
||||
/* #3 ตอนรอผู้เล่นอื่น (หลังกดเล่นต่อ) — แทนรูปปุ่มด้วยข้อความ "รอผู้เล่นอื่น… (X/Y)" ให้รู้ว่าไม่ค้าง */
|
||||
#special-quiz-overlay .sq-ov-award-btn.is-waiting { background: rgba(18,26,46,0.92); background-image: none; border: 1px solid rgba(122,162,247,0.45); border-radius: 12px; font-size: clamp(15px,1.7vw,19px); color: #bcd2ff; font-weight: 700; letter-spacing: .02em; display: flex; align-items: center; justify-content: center; gap: 8px; cursor: progress; opacity: .95; }
|
||||
#special-quiz-overlay .sq-ov-award-btn.is-waiting::before { content: ''; width: 16px; height: 16px; border-radius: 50%; border: 2px solid rgba(122,162,247,.35); border-top-color: #7aa2f7; animation: sqWaitSpin .8s linear infinite; }
|
||||
#special-quiz-overlay .sq-ov-award-btn.is-waiting:hover { filter: none; transform: none; }
|
||||
@keyframes sqWaitSpin { to { transform: rotate(360deg); } }
|
||||
#special-quiz-overlay .sq-ov-members-head { margin-top: 16px; font-size: 12px; letter-spacing: .04em; text-transform: uppercase; color: #7aa2f7; font-weight: 700; }
|
||||
#special-quiz-overlay .sq-ov-members { margin-top: 8px; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
@media (max-width: 560px) { #special-quiz-overlay .sq-ov-members { grid-template-columns: 1fr; } }
|
||||
@@ -5371,7 +5376,7 @@
|
||||
<script src="/app-base.js?v=2"></script>
|
||||
<script src="/Game/socket.io/socket.io.js"></script>
|
||||
<script src="js/version.js?v=0.0306"></script>
|
||||
<script src="js/play.js?v=0.006242301"></script>
|
||||
<script src="js/play.js?v=0.006250400"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
<style id="qc-howto-mg2-fix">
|
||||
/* HOW TO PLAY ของ quiz_carry (Minigame-4) -> ให้เหมือน Minigame-2 (gch-mg2-mock)
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Kanit:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/room-lobby.css?v=0.0175">
|
||||
<link rel="stylesheet" href="css/room-lobby.css?v=0.0176">
|
||||
<link rel="stylesheet" href="css/profile-popup.css?v=7">
|
||||
<link rel="stylesheet" href="css/customize-popup.css?v=42">
|
||||
<link rel="stylesheet" href="css/leaderboard-popup.css?v=2">
|
||||
<link rel="stylesheet" href="css/testimony-overlay.css?v=17">
|
||||
<link rel="stylesheet" href="css/testimony-overlay.css?v=18">
|
||||
<link rel="stylesheet" href="css/evidence-view-overlay.css?v=13">
|
||||
<style>
|
||||
html, body.room-lobby-page { margin: 0; padding: 0; overflow: hidden; width: 100%; height: 100%; min-height: 100%; }
|
||||
@@ -224,7 +224,17 @@
|
||||
width: 405px;
|
||||
height: 625px;
|
||||
border-radius: 10px;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
/* #1 กันรูปการ์ดเก่า/mock แว่บก่อนรูปจริงโหลด — ซ่อนรูปไว้ + spinner จนรูปคดีจริงโหลดเสร็จ */
|
||||
.suspect-card.is-img-loading > img { opacity: 0; }
|
||||
.suspect-card.is-img-loading::after {
|
||||
content: ''; position: absolute; left: 50%; top: 42%;
|
||||
width: 56px; height: 56px; margin: -28px 0 0 -28px; border-radius: 50%;
|
||||
border: 5px solid rgba(159, 233, 255, 0.22); border-top-color: #9fe9ff;
|
||||
animation: suspectCardSpin 0.9s linear infinite; z-index: 6; pointer-events: none;
|
||||
}
|
||||
@keyframes suspectCardSpin { to { transform: rotate(360deg); } }
|
||||
.suspect-card--host {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1617,7 +1627,7 @@
|
||||
<script src="js/version.js?v=0.0122"></script>
|
||||
<script src="js/customize-popup.js?v=40" data-customize-triggers="" data-customize-asset-base="img/03-5-Customize"></script>
|
||||
<script src="js/achievements.js?v=0.002" data-asset-base="img/03-6-Profile"></script>
|
||||
<script src="js/room-lobby.js?v=0.0304"></script>
|
||||
<script src="js/room-lobby.js?v=0.0311"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+144
-14
@@ -532,6 +532,8 @@ const SPECIAL_QUIZ_ICON_TYPES = ['lawyer', 'police'];
|
||||
/** โอกาสโผล่ = 1 ใน 3 ของการเล่นแต่ละรอบ */
|
||||
/** โอกาสขึ้นคำถามพิเศษต่อรอบ (เล่นจริง) — 1/3 · โหมดเทสต์บังคับการ์ดต่อเกมจะ force ขึ้นเสมอแยกต่างหาก */
|
||||
const SPECIAL_QUIZ_SPAWN_CHANCE = 1 / 3;
|
||||
/** หลุดกลางมินิเกม → ให้เวลา reconnect (วิ) ก่อน bot เข้าเล่นแทน */
|
||||
const DISCONNECT_GRACE_SEC = 10;
|
||||
|
||||
/** โหมดเทสต์ (admin): บังคับ "การ์ดพิเศษใบไหน → เกมไหน" — { mapId: cardId(1-7) } · เกมที่ตั้งไว้จะ force ขึ้นเสมอด้วยการ์ดนั้น */
|
||||
function normalizeTestSpecialCardByMap(v) {
|
||||
@@ -710,6 +712,29 @@ function pickSpecialQuizSpawnTile(md, occupiedLaneCount) {
|
||||
return { x: p.x + 0.5, y: p.y + 0.5 };
|
||||
}
|
||||
}
|
||||
/* jump_survive (กระโดด mnptfts2): ผู้เล่นยืนบน "แพลตฟอร์ม" — พื้นที่ว่างนับ walkable ผิด (ลอยกลางอากาศเก็บไม่ได้)
|
||||
ไอคอนคลิก/แตะเก็บได้ทุกมินิเกม (trySpecialQuizClickAt) → วางบนแพลตฟอร์มใกล้จุดเกิด (อยู่ในจอตั้งแต่ต้น คลิกได้)
|
||||
= แก้ #44 อีเวนต์พิเศษ (ทนาย/เทพ) ไม่ขึ้นเพราะ BFS หา walkable ไม่เจอ → return null */
|
||||
if (md.gameType === 'jump_survive' && Array.isArray(md.jumpSurvivePlatformArea) && md.jumpSurvivePlatformArea.length) {
|
||||
const sp = md.spawn || { x: Math.floor(w / 2), y: Math.floor(h / 2) };
|
||||
const bx = Math.floor(Number(sp.x));
|
||||
const by = Math.floor(Number(sp.y));
|
||||
const plats = [];
|
||||
for (let y = 0; y < h; y++) {
|
||||
const row = md.jumpSurvivePlatformArea[y];
|
||||
if (!row) continue;
|
||||
for (let x = 0; x < w; x++) {
|
||||
if (row[x]) plats.push({ x, y });
|
||||
}
|
||||
}
|
||||
if (plats.length) {
|
||||
plats.sort((a, b) => (Math.abs(a.x - bx) + Math.abs(a.y - by)) - (Math.abs(b.x - bx) + Math.abs(b.y - by)));
|
||||
const near = plats.slice(0, Math.min(8, plats.length));
|
||||
const p = near[Math.floor(Math.random() * near.length)];
|
||||
const iy = (p.y - 1 >= 0) ? p.y - 1 : p.y; /* เหนือแพลตฟอร์ม 1 ช่อง = ระดับตัวผู้เล่น (ไม่จมในแพลตฟอร์ม) */
|
||||
return { x: p.x + 0.5, y: iy + 0.5 };
|
||||
}
|
||||
}
|
||||
/* seed BFS จากพื้นที่ที่ผู้เล่นเกิดจริง (spawnArea) เพื่อรับประกันว่าไอคอนเดินไปถึงได้
|
||||
fallback: md.spawn (ถ้าเดินได้) แล้วค่อยสแกนหา walkable tile แรก */
|
||||
let sx = -1;
|
||||
@@ -803,7 +828,8 @@ function specialQuizIconPayload(space) {
|
||||
return { iconType: sq.iconType, x: sq.x, y: sq.y, expiresAt: sq.expiresAt || 0 };
|
||||
}
|
||||
|
||||
/** เริ่มมินิเกมสืบสวน: สุ่ม 1/3 ว่ารอบนี้จะมีไอคอนพิเศษไหม + เลือกตำแหน่ง */
|
||||
/** เริ่มมินิเกมสืบสวน: สุ่ม 1/3 ว่ารอบนี้จะมีไอคอนพิเศษไหม + เลือกตำแหน่ง (1 ใบต่อครั้งที่ติด — ดีไซน์ "1 ใน 3")
|
||||
Test Mode (testSpecialCardByMap / forceSpecialQuiz) → ข้าม roll ขึ้นเสมอ */
|
||||
function maybeSpawnSpecialQuizForRun(space) {
|
||||
clearSpecialQuizTimers(space);
|
||||
space.specialQuiz = null;
|
||||
@@ -2062,8 +2088,8 @@ function defaultGameTiming() {
|
||||
stackTeamMissesMax: 3,
|
||||
/** Stack ภารกิจ Tower: จำนวนชั้นสำเร็จ (ไม่คิดคอมโบ) ที่รวมแล้วได้ Progress 100% — แต่ละชั้น +100/N %; คอมโบ ×2 ของชั้นนั้น */
|
||||
stackTowerProgressBlocks: 50,
|
||||
stackBlockNormalImageUrls: ['', '', '', '', '', ''],
|
||||
stackBlockHeavyImageUrls: ['', '', '', '', '', ''],
|
||||
stackBlockNormalImageUrls: ['', '', '', '', '', '', '', ''],
|
||||
stackBlockHeavyImageUrls: ['', '', '', '', '', '', '', ''],
|
||||
/** โอกาสใช้รูป “ใหญ่” ต่อการปล่อยหนึ่งครั้ง (0–100) — ถ้าไม่มี URL ใหญ่ช่องนั้น = ใช้ปกติ */
|
||||
stackHeavyBlockPercent: 35,
|
||||
/** ยิงยานอวกาศ / space_shooter: จำกัดเวลารอบ (วินาที) — 0 = ไคลเอนต์ใช้ค่าเริ่ม 90; แมป spaceShooterTimeSec > 0 ทับ */
|
||||
@@ -2086,7 +2112,7 @@ function defaultGameTiming() {
|
||||
forcedMinigameKeys: ['', '', ''],
|
||||
/** โหมดเทสต์: บังคับการ์ดพิเศษต่อเกม { mapId: cardId } */
|
||||
testSpecialCardByMap: {},
|
||||
/** โหมดเทสต์: บังคับเสนอบทตัวป่วน (ปกติเสนอเฉพาะเมื่อมีคนจริง > 4) */
|
||||
/** โหมดเทสต์: บังคับเสนอบทตัวป่วน (ปกติเสนอเฉพาะเมื่อมีคนจริง ≥ 4) */
|
||||
troublesomeForceOffer: false,
|
||||
/** เวลาที่ไอคอนคำถามพิเศษอยู่บนฉากก่อนหายไป (วินาที) — 0 = ไม่หาย */
|
||||
specialQuizIconExpireSec: 10,
|
||||
@@ -2149,7 +2175,7 @@ function normalizeStackBlockSixImageUrls(val) {
|
||||
}
|
||||
if (!Array.isArray(arr)) arr = [];
|
||||
const out = [];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
for (let i = 0; i < 8; i++) { /* 8 slot ตาม lobby theme (#4) — เดิม 6 */
|
||||
out.push(sanitizeGauntletAssetUrl(arr[i]));
|
||||
}
|
||||
return out;
|
||||
@@ -3428,6 +3454,12 @@ function awardRandomEvidenceCardToPlayer(space, playerId, suspectIndex, grade) {
|
||||
: [[], [], []];
|
||||
}
|
||||
const list = normalizeEvidenceSlotList(space.playerEvidence[playerId][suspectIndex]);
|
||||
/* กันเกิน 3 ใบ/ผู้ต้องสงสัย (#1) — defensive ถ้ามีรอบที่ 4 หลุดมา */
|
||||
if (list.length >= 3) {
|
||||
space.playerEvidence[playerId][suspectIndex] = list;
|
||||
syncEvidenceByNick(space, playerId);
|
||||
return null;
|
||||
}
|
||||
const card = pickEvidenceCardForSuspect(space, suspectIndex, grade);
|
||||
if (!card) {
|
||||
space.playerEvidence[playerId][suspectIndex] = list;
|
||||
@@ -3539,6 +3571,8 @@ function beginDetectiveSuspectMinigame(sid, space, cardEntry, selectedIndex) {
|
||||
space.coinAwardDoneForRun = false;
|
||||
/* รีเซ็ตคะแนนที่ client รายงาน (stack/quiz_carry) ของรอบก่อน */
|
||||
space.peers.forEach((p) => { p.reportedMiniScore = 0; p.reportedMiniSurvived = true; });
|
||||
/* เริ่มรอบใหม่ → ล้าง over event เก่า (กัน resume ส่ง over ของรอบก่อนให้คน reconnect) */
|
||||
space.lastMinigameOver = null;
|
||||
/* Card 3 Ban — ผู้ที่ถูกแบนไม่ได้เล่นมินิเกมรอบนี้ (1 รอบ) */
|
||||
space.bannedThisRunPlayerId = space.bannedPlayerId || null;
|
||||
console.log('[ban-debug] startMinigame: bannedThisRunPlayerId=', space.bannedThisRunPlayerId);
|
||||
@@ -5926,6 +5960,7 @@ const server = http.createServer((req, res) => {
|
||||
const list = [...spaces]
|
||||
.filter(([, s]) => !s.isPrivate)
|
||||
.filter(([, s]) => (s.peers && s.peers.size > 0))
|
||||
.filter(([, s]) => !s.joinLocked) /* ห้องที่เริ่มคดีแล้ว (ออกจาก LobbyA) → ซ่อนจาก Join Room list ไม่ให้ใครเข้า */
|
||||
.map(([id, s]) => ({
|
||||
spaceId: id,
|
||||
spaceName: s.spaceName || id,
|
||||
@@ -7024,6 +7059,23 @@ function pickSpawnForJoin(md, joinOrderIndex) {
|
||||
|
||||
/* spawnJoinOrder เสถียรข้ามการเปลี่ยนหน้า (lobbyA→lobbyB→minigame): จำลำดับด้วย playerKey (คงที่ต่อ browser)
|
||||
ไม่ใช่ peers.size ที่เปลี่ยนตามลำดับ reconnect → ตำแหน่งเกิด / สีผิว / slot ของแต่ละคนสลับกันมั่ว */
|
||||
/** เลือก host คนถัดไปเมื่อ host ปัจจุบันหลุด — คนที่อยู่ในห้องนานสุด (spawnJoinOrder ต่ำสุด)
|
||||
เสถียรข้ามการ reconnect (spawnJoinOrder ผูกกับ playerKey) — แทน Map-first ที่สลับได้ */
|
||||
function pickNextHostPeer(space) {
|
||||
if (!space || !space.peers || space.peers.size === 0) return null;
|
||||
let best = null;
|
||||
let bestOrd = Infinity;
|
||||
space.peers.forEach((p, id) => {
|
||||
if (!p) return;
|
||||
const ord = (typeof p.spawnJoinOrder === 'number' && p.spawnJoinOrder >= 0) ? p.spawnJoinOrder : 9999;
|
||||
if (ord < bestOrd || (ord === bestOrd && (best == null || String(id) < String(best.id)))) {
|
||||
bestOrd = ord;
|
||||
best = p;
|
||||
}
|
||||
});
|
||||
return best;
|
||||
}
|
||||
|
||||
function resolveSpawnJoinOrder(space, playerKey) {
|
||||
if (!space.spawnOrderByKey || typeof space.spawnOrderByKey !== 'object') space.spawnOrderByKey = {};
|
||||
const key = (typeof playerKey === 'string' && /^[a-zA-Z0-9_-]{8,128}$/.test(playerKey)) ? playerKey : '';
|
||||
@@ -7081,7 +7133,7 @@ function initTroublesomeState(space) {
|
||||
if (space.suspectPhaseActive == null) space.suspectPhaseActive = false;
|
||||
}
|
||||
|
||||
/** เปิดเฟส "เลือกผู้ต้องสงสัย" ให้ทั้งห้อง — เรียกหลังตอบข้อเสนอตัวป่วน หรือเมื่อข้ามการเสนอ (คน ≤ 4) */
|
||||
/** เปิดเฟส "เลือกผู้ต้องสงสัย" ให้ทั้งห้อง — เรียกหลังตอบข้อเสนอตัวป่วน หรือเมื่อข้ามการเสนอ (คน < 4) */
|
||||
function openSuspectPhaseForRoom(sid, space, disruptorAccepted) {
|
||||
if (!space) return;
|
||||
let idx = Math.floor(Number(space.suspectPickIndex));
|
||||
@@ -7114,10 +7166,10 @@ function attemptTroublesomeRoll(sid, force) {
|
||||
const peerIds = [...space.peers.keys()];
|
||||
if (peerIds.length === 0) return;
|
||||
if (!force && space.troublesomeEligible.size < peerIds.length) return;
|
||||
/* เสนอบทตัวป่วนเฉพาะเมื่อมี "คนจริง > 4 คน" — ยกเว้น admin บังคับ (troublesomeForceOffer) */
|
||||
/* เสนอบทตัวป่วนเฉพาะเมื่อมี "คนจริง ≥ 4 คน" — ยกเว้น admin บังคับ (troublesomeForceOffer) */
|
||||
const forceOffer = !!(runtimeGameTiming && runtimeGameTiming.troublesomeForceOffer);
|
||||
if (peerIds.length <= 4 && !forceOffer) {
|
||||
/* ไม่เสนอบทตัวป่วน (คน ≤ 4) → ข้ามไปเปิดหน้าเลือกผู้ต้องสงสัยเลย (ไม่มีตัวป่วน) */
|
||||
if (peerIds.length < 4 && !forceOffer) {
|
||||
/* ไม่เสนอบทตัวป่วน (คน < 4) → ข้ามไปเปิดหน้าเลือกผู้ต้องสงสัยเลย (ไม่มีตัวป่วน) */
|
||||
space.troublesomeOfferSent = true;
|
||||
space.troublesomeResponseReceived = true;
|
||||
space.troublesomeAccept = false;
|
||||
@@ -7593,6 +7645,12 @@ io.on('connection', (socket) => {
|
||||
if (space.gauntletCrownLobbyReady) delete space.gauntletCrownLobbyReady[pid];
|
||||
io.to(spaceId).emit('user-left', { id: pid });
|
||||
});
|
||||
/* กลับมาทัน grace 10 วิ → ยกเลิก bot takeover + ปิด banner countdown */
|
||||
if (space.graceReconnect && space.graceReconnect[joinPlayerKey]) {
|
||||
delete space.graceReconnect[joinPlayerKey];
|
||||
if (space.graceTimers && space.graceTimers[joinPlayerKey]) { clearTimeout(space.graceTimers[joinPlayerKey]); delete space.graceTimers[joinPlayerKey]; }
|
||||
io.to(spaceId).emit('peer-grace-cancel', { nickname: nickname || 'ผู้เล่น' });
|
||||
}
|
||||
}
|
||||
/* reconnect dedup ชั้น 2: playerKey ไม่ match (เก่าว่าง/คนละค่า — เช่น peer เก่าจาก lobbyB ไม่มี playerKey)
|
||||
→ คนเดิมกลับเข้ามากลายเป็นตัวซ้ำ (ghost) ที่ playerKey block จับไม่ได้ → เห็น "ตัวละครซ้ำ 2 ตัว" บน "ทุกจอรวม host"
|
||||
@@ -7763,6 +7821,22 @@ io.on('connection', (socket) => {
|
||||
const sqIcon = specialQuizIconPayload(space);
|
||||
if (sqIcon) joinCb.specialQuizIcon = sqIcon;
|
||||
if (typeof cb === 'function') cb(joinCb);
|
||||
/* กันค้างตอน reconnect: event "จบมินิเกม" (balloon-boss-over/stack-over/space-shooter-over)
|
||||
host ยิงครั้งเดียวผ่าน socket.to — ถ้า client หลุดตอนนั้นจะค้างถาวร (ไม่มี replay).
|
||||
เก็บ over ล่าสุดไว้ → resend ให้ socket ที่เพิ่ง (re)join ภายใน 45s ถ้ายังอยู่ในแมปมินิเกม
|
||||
(gameType !== 'lobby' = ยังไม่ทรานซิชันกลับ). delay เล็กน้อยให้ client applyMapAndStart เสร็จก่อน */
|
||||
const lmo = space.lastMinigameOver;
|
||||
if (lmo && lmo.event && (Date.now() - lmo.atMs) < 45000 && mapDataOut && mapDataOut.gameType !== 'lobby') {
|
||||
const targetSid = socket.id;
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const sp = spaces.get(spaceId);
|
||||
if (sp && sp.lastMinigameOver === lmo && io.sockets.sockets.get(targetSid)) {
|
||||
io.to(targetSid).emit(lmo.event, lmo.payload);
|
||||
}
|
||||
} catch (_e) { /* ignore */ }
|
||||
}, 400);
|
||||
}
|
||||
if (space.hostId !== prevHostId) io.to(spaceId).emit('host-changed', { hostId: space.hostId }); /* host เปลี่ยน (เช่น host ตัวจริงกลับเข้ามา) → sync ทุกคน */
|
||||
emitLobbyTintSync(spaceId, space);
|
||||
socket.to(spaceId).emit('user-joined', peer);
|
||||
@@ -7994,7 +8068,10 @@ io.on('connection', (socket) => {
|
||||
const scores = Array.isArray(data.scores)
|
||||
? data.scores.filter((s) => s && s.id != null).map((s) => ({ id: s.id, score: Math.max(0, Number(s.score) || 0), eliminated: !!s.eliminated }))
|
||||
: [];
|
||||
socket.to(sid).emit('balloon-boss-over', { reason, scores });
|
||||
const overPayload = { reason, scores };
|
||||
socket.to(sid).emit('balloon-boss-over', overPayload);
|
||||
/* เก็บไว้ resend ให้คน reconnect (host ยิงครั้งเดียว — หลุดตอนนั้นจะค้างถาวร) */
|
||||
space.lastMinigameOver = { event: 'balloon-boss-over', payload: overPayload, atMs: Date.now() };
|
||||
});
|
||||
|
||||
/* MG7 balloon_boss — host เป็นเจ้าของ HP บอส: relay ดาเมจรวม + สถานะผู้เล่น (ลูกโป่ง/ตาย) เป็นระยะ
|
||||
@@ -8076,7 +8153,9 @@ io.on('connection', (socket) => {
|
||||
const space = sid ? spaces.get(sid) : null;
|
||||
if (!space) return;
|
||||
const outcome = (data && typeof data.outcome === 'string') ? data.outcome.slice(0, 32) : 'time_up';
|
||||
socket.to(sid).emit('stack-over', { outcome });
|
||||
const overPayload = { outcome };
|
||||
socket.to(sid).emit('stack-over', overPayload);
|
||||
space.lastMinigameOver = { event: 'stack-over', payload: overPayload, atMs: Date.now() };
|
||||
});
|
||||
|
||||
/* MG6 space_shooter — host ตัดสินจบเกม + แนบคะแนนสุดท้าย → ทุกคนจบ/อันดับตรงกัน */
|
||||
@@ -8088,7 +8167,9 @@ io.on('connection', (socket) => {
|
||||
const scores = Array.isArray(data.scores)
|
||||
? data.scores.filter((s) => s && s.id != null).map((s) => ({ id: s.id, score: Math.max(0, Number(s.score) || 0), hits: Math.max(0, Number(s.hits) || 0), eliminated: !!s.eliminated }))
|
||||
: [];
|
||||
socket.to(sid).emit('space-shooter-over', { kind, scores });
|
||||
const overPayload = { kind, scores };
|
||||
socket.to(sid).emit('space-shooter-over', overPayload);
|
||||
space.lastMinigameOver = { event: 'space-shooter-over', payload: overPayload, atMs: Date.now() };
|
||||
});
|
||||
|
||||
/* MG6 space_shooter — relay กระสุนผู้เล่น/บอท ให้ทุกคนเห็นตรงกัน (host เห็นยังไง client เห็นแบบนั้น) */
|
||||
@@ -8225,6 +8306,17 @@ io.on('connection', (socket) => {
|
||||
if (!cardEntry || !cardEntry.mapId) {
|
||||
return reply({ ok: false, error: 'ยังไม่ได้สุ่มมินิเกมสำหรับการ์ดนี้' });
|
||||
}
|
||||
/* จำกัดสืบผู้ต้องสงสัยละไม่เกิน 3 ครั้ง (#1) — นับจาก max หลักฐานของผู้ต้องสงสัยนี้ข้ามผู้เล่น
|
||||
(แต่ละรอบทุกคนได้ +1) → กันเล่นครั้งที่ 4 + กันการ์ดพิเศษโผล่รอบที่ 4 (#2) */
|
||||
let timesPlayedThisSuspect = 0;
|
||||
space.peers.forEach((_p, pid) => {
|
||||
const prog = playerSuspectProgressFromEvidence(space, pid);
|
||||
const n = Array.isArray(prog) ? (prog[selectedIndex] | 0) : 0;
|
||||
if (n > timesPlayedThisSuspect) timesPlayedThisSuspect = n;
|
||||
});
|
||||
if (timesPlayedThisSuspect >= 3) {
|
||||
return reply({ ok: false, error: 'สืบผู้ต้องสงสัยคนนี้ครบ 3 ครั้งแล้ว — เลือกคนอื่น' });
|
||||
}
|
||||
/* Card 3 Ban — ก่อนเริ่มมินิเกมรอบหน้า โหวตเลือกผู้เล่น 1 คนที่ห้ามเล่น */
|
||||
const pend = findPendingSpecialCard(space, 3);
|
||||
if (pend && !space.pickVote) {
|
||||
@@ -8734,6 +8826,10 @@ io.on('connection', (socket) => {
|
||||
}
|
||||
if (!space || !space.peers.has(socket.id)) return;
|
||||
const wasHost = space.hostId === socket.id;
|
||||
/* เก็บข้อมูลก่อนลบ peer — ใช้ทำ grace 10 วิ (รอ reconnect) + bot takeover ระหว่างมินิเกม */
|
||||
const leftPeerInfo = space.peers.get(socket.id) || {};
|
||||
const leftPlayerKey = (typeof leftPeerInfo.playerKey === 'string') ? leftPeerInfo.playerKey : '';
|
||||
const leftNick = (leftPeerInfo.nickname || '').trim() || 'ผู้เล่น';
|
||||
if (space.voiceInits) delete space.voiceInits[socket.id];
|
||||
if (space.troublesomeEligible) space.troublesomeEligible.delete(socket.id);
|
||||
if (space.quizSession && space.quizSession.players) delete space.quizSession.players[socket.id];
|
||||
@@ -8770,7 +8866,7 @@ io.on('connection', (socket) => {
|
||||
spaces.delete(sid);
|
||||
} else {
|
||||
if (wasHost) {
|
||||
const firstPeer = space.peers.values().next().value;
|
||||
const firstPeer = pickNextHostPeer(space); /* คนอยู่นานสุด (spawnJoinOrder ต่ำสุด) — แทน Map-first */
|
||||
if (firstPeer) {
|
||||
space.hostId = firstPeer.id; /* ย้าย pointer host ชั่วคราวให้คนที่เหลือ (liveness) */
|
||||
/* อย่าเขียนทับ hostPlayerKey ถ้ามีอยู่แล้ว — host ตัวจริงต้อง reclaim ได้เมื่อ rejoin จาก play.html
|
||||
@@ -8788,6 +8884,40 @@ io.on('connection', (socket) => {
|
||||
if (space.specialQuizContinueAcks) space.specialQuizContinueAcks.delete(socket.id);
|
||||
maybeFinishSpecialQuizContinue(sid, space);
|
||||
}
|
||||
/* ห้องส่งหลักฐาน (testimony) — คนหลุดระหว่างเฟส → ลบ submit/ready ที่ค้าง แล้ว re-emit สถานะ
|
||||
เพื่อให้ "จำนวนที่ต้องส่ง/ready" ลดตามจริง (peers.delete ไปแล้วด้านบน) ไม่งั้น host รอคนที่ไม่อยู่ = ไปต่อไม่ได้ */
|
||||
if (space.testimonyActive) {
|
||||
if (space.testimonySubmitted) delete space.testimonySubmitted[socket.id];
|
||||
if (space.testimonyReadyNext) delete space.testimonyReadyNext[socket.id];
|
||||
emitTestimonyStatus(sid, space);
|
||||
if (space.testimonyRevealed) {
|
||||
io.to(sid).emit('testimony-ready-update', {
|
||||
ready: Object.keys(space.testimonyReadyNext || {}),
|
||||
totalPlayers: testimonyTotalParticipants(space),
|
||||
hostId: space.hostId,
|
||||
});
|
||||
}
|
||||
}
|
||||
/* หลุดกลาง "มินิเกม" (ไม่ใช่ LobbyB) → ให้เวลา reconnect 10 วิ + countdown banner
|
||||
ครบเวลายังไม่กลับ → broadcast bot-takeover ให้ host เติมบอทเล่นแทน */
|
||||
if (space.detectiveMinigameActive && leftPlayerKey) {
|
||||
io.to(sid).emit('peer-disconnect-grace', { id: socket.id, nickname: leftNick, seconds: DISCONNECT_GRACE_SEC });
|
||||
if (!space.graceReconnect || typeof space.graceReconnect !== 'object') space.graceReconnect = {};
|
||||
if (!space.graceTimers || typeof space.graceTimers !== 'object') space.graceTimers = {};
|
||||
if (space.graceTimers[leftPlayerKey]) clearTimeout(space.graceTimers[leftPlayerKey]);
|
||||
space.graceReconnect[leftPlayerKey] = Date.now();
|
||||
space.graceTimers[leftPlayerKey] = setTimeout(() => {
|
||||
const sp = spaces.get(sid);
|
||||
if (!sp) return;
|
||||
if (sp.graceTimers) delete sp.graceTimers[leftPlayerKey];
|
||||
/* กลับมาแล้ว (playerKey อยู่ใน peers) → ไม่ต้อง takeover */
|
||||
let back = false;
|
||||
if (sp.peers) sp.peers.forEach((p) => { if (p && p.playerKey === leftPlayerKey) back = true; });
|
||||
if (sp.graceReconnect) delete sp.graceReconnect[leftPlayerKey];
|
||||
if (back) return;
|
||||
io.to(sid).emit('peer-bot-takeover', { nickname: leftNick });
|
||||
}, DISCONNECT_GRACE_SEC * 1000);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -9130,7 +9260,7 @@ io.on('connection', (socket) => {
|
||||
p.direction = data.direction || p.direction;
|
||||
const posUnchanged = Math.abs(nx - prevX) < 1e-5 && Math.abs(ny - prevY) < 1e-5 && p.direction === prevDir;
|
||||
if (!posUnchanged || moveForceEmit) {
|
||||
const out = { id: socket.id, x: nx, y: ny, direction: p.direction, characterId: p.characterId };
|
||||
const out = { id: socket.id, x: nx, y: ny, direction: p.direction, characterId: p.characterId, lobbyColorThemeIndex: p.lobbyColorThemeIndex || null, lobbySkinToneIndex: p.lobbySkinToneIndex || null };
|
||||
if (md && md.gameType === 'space_shooter') {
|
||||
out.spaceShooterScore = Math.max(0, p.spaceShooterScore | 0);
|
||||
out.spaceShooterHits = Math.max(0, p.spaceShooterHits | 0);
|
||||
|
||||
Executable
+605
@@ -0,0 +1,605 @@
|
||||
[
|
||||
{
|
||||
"n": 1,
|
||||
"name": "แต่งตัวไม่ได้",
|
||||
"detail": "แต่งตัวยังไม่ได้ มีพี่คิวทำได้คนเดียว",
|
||||
"prio": "วิกฤต",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 2,
|
||||
"name": "ตัวละครไม่แสดง",
|
||||
"detail": "หน้าเลือกเกม Quiz battle ภาพตัวละครไม่แสดง",
|
||||
"prio": "กลาง",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 3,
|
||||
"name": "ระบบ join เกมมีปัญหา",
|
||||
"detail": "เมื่อเข้ามินิเกม : บางคนเล่นเกมกะบอท บางคนเล่นกะเพื่อน แต่ละคนเข้ามินิเกมแล้วเจอกันบ้าง ไม่เจอกันบ้าง",
|
||||
"prio": "วิกฤต",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 4,
|
||||
"name": "ระบบ Host มีปัญหา - 1",
|
||||
"detail": "เป็น host ในเกม ตอนที่เลือกผู้ต้องสงสัย รอบที่ 2 ระบบบอกว่าให้รอ host ไม่สามารถเลือกการ์ดและกดเริ่มสืบสวนได้",
|
||||
"prio": "วิกฤต",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 5,
|
||||
"name": "ระบบ Host มีปัญหา - 2",
|
||||
"detail": "เมื่อกดออกจากเกม Host เปลี่ยนจากพี่ต้อมเป็นพี่คิวอัติโนมัติ",
|
||||
"prio": "วิกฤต",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 6,
|
||||
"name": "ระบบ Host มีปัญหา - 3",
|
||||
"detail": "เมื่อเข้ามินิเกม ทุกคนเห็นตัวเองเป็น host หมดเลย แต่กดเริ่มเกมไม่ได้",
|
||||
"prio": "วิกฤต",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 7,
|
||||
"name": "ระบบสีตัวละคร ยังใช้ไม่ได้",
|
||||
"detail": "สีตัวละครเปลี่ยนไปเรื่อย ไม่รุ้ว่าเราสีอะไรแน่ งงไปหมด",
|
||||
"prio": "วิกฤต",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 8,
|
||||
"name": "หน้าเข้าร่วมเกมส์",
|
||||
"detail": "ห้องไหนที่เริ่มเกมส์ไปแล้ว เมื่อกดเข้าร่วมแสดง popup alert error ปรับดีไซต์ให้สวยงามกว่านี้",
|
||||
"prio": "กลาง",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 9,
|
||||
"name": "เกมวิ่งขึ้นบันได ผิด flow",
|
||||
"detail": "ปรับ flow ให้ถูกต้อง คือ ใครตกจอตาย แต่ถ้าขึ้นไปอยู่บนสุดแล้วจะกระโดดไม่ไปต่อเฉยๆ แต่ไม่ตาย",
|
||||
"prio": "วิกฤต",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 10,
|
||||
"name": "เกมวิ่งขึ้นบันได",
|
||||
"detail": "เวลาหมดแล้ว ให้เล่นรอบ 2 auto / รอบ 3 มี popup ให้กดเล่นต่อ",
|
||||
"prio": "",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 11,
|
||||
"name": "เกมยิงลูกโป่ง",
|
||||
"detail": "ฉากเกมยิงลูกโป่งเล็กไป",
|
||||
"prio": "กลาง",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 12,
|
||||
"name": "เกมต่อตึก ผิด flow",
|
||||
"detail": "ปรับ flow ให้ถูกต้อง คือ เล่นทีละคน เรียงตามคิวไปเรื่อย โดยที่ blog ตามสีผู้เล่นด้วย",
|
||||
"prio": "วิกฤต",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 13,
|
||||
"name": "เข้าร่วมห้องไม่ได้",
|
||||
"detail": "สร้างห้องส่วนตัวเเล้ว เเต่เพื่อนจอยเข้าไปไม่ได้ ทั้งที่กรอกข้อมูลครบ",
|
||||
"prio": "วิกฤต",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 14,
|
||||
"name": "Room ID",
|
||||
"detail": "เวลาจอยห้องให้กรอกเเค่ ID กับรหัสที่สร้าง",
|
||||
"prio": "ต่ำ",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 15,
|
||||
"name": "จอยห้องคนอื่น",
|
||||
"detail": "ในกรณีที่จอยห้องคนอื่น ไปหน้าเข้าร่วม เเละกดปุ่มเข้าร่วมไปเเล้ว จากนั้นกดปุ่ม exit จะพาไปหน้าสร้างห้อง สิ่งที่ถูกต้อง มันควรมาหน้า ห้องเข้าร่วมเกม",
|
||||
"prio": "กลาง",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 16,
|
||||
"name": "grid กับ ฉาก ไม่ตรงกัน",
|
||||
"detail": "เกม Quiz battle เกมที่ 1 ฉากกับกริดไม่ตรงกัน ทำให้เดินทะลุ หรือเดินติดในจุดที่ไม่ควรติด",
|
||||
"prio": "สูง",
|
||||
"status": "เปิด",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 17,
|
||||
"name": "ตัวละครวิ่งเร็วเกินไป",
|
||||
"detail": "เกม Quiz battle เกมที่ 1 ตัวละครผู้เล่นเร็วเกินไป",
|
||||
"prio": "กลาง",
|
||||
"status": "เปิด",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 18,
|
||||
"name": "Oauth Social log-in error",
|
||||
"detail": "ไม่สามารถ log-inด้วย facebook / Google ได้ path: /oauth-public.php",
|
||||
"prio": "ต่ำ",
|
||||
"status": "เปิด",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 19,
|
||||
"name": "หน้า login",
|
||||
"detail": "นโยบายความเป็นส่วนตัวกดไม่ได้",
|
||||
"prio": "กลาง",
|
||||
"status": "เปิด",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 20,
|
||||
"name": "แสดงเลข version ตรงมุมจอ",
|
||||
"detail": "แสดงเลข version ตรงมุมจอขวาบนหรือขวาล่างก็ได้",
|
||||
"prio": "ต่ำ",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-24",
|
||||
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — badge มุมขวาล่าง อ่าน ?v= อัตโนมัติ"
|
||||
},
|
||||
{
|
||||
"n": 21,
|
||||
"name": "ลดขนาดตัวละคร",
|
||||
"detail": "ลดขนาดตัวละครในห้อง lobby ลงอีก 2เท่า",
|
||||
"prio": "สูง",
|
||||
"status": "เปิด",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 22,
|
||||
"name": "เปลี่ยน ui popup ตอนพูดบนหัว",
|
||||
"detail": "แก้ png seq บนหัวใหม่ เป็นแบบอื่น",
|
||||
"prio": "กลาง",
|
||||
"status": "เปิด",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 23,
|
||||
"name": "ชื่่อตัวละครทับลำโพง/ทับ bubble",
|
||||
"detail": "ใน lobby ขยับชื่อลงมาอย่าให้ทับ",
|
||||
"prio": "กลาง",
|
||||
"status": "เปิด",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 24,
|
||||
"name": "UI แต่งตัว : การแจ้งเตือนสีตัวละครถูกใช้แล้ว",
|
||||
"detail": "ตอนเลือกสีให้มีคำแจ้งเตือน used",
|
||||
"prio": "กลาง",
|
||||
"status": "เปิด",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 25,
|
||||
"name": "เพิ่ม ข้อความแจ้งเตือนอัพเดทกลางจอ",
|
||||
"detail": "ถ้ามีการอัพเดทซีน อัพเดทสีตัวละครให้มีป็อปอัพเด้งมากลางหน้าจอ ว่ามีการอัพเดทและให้รอโหลด",
|
||||
"prio": "สูง",
|
||||
"status": "เปิด",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 26,
|
||||
"name": "เอาเงาตัวละครออก 1 อัน",
|
||||
"detail": "ตอนนี้ตัวละครมีเงา 2 ชั้น เอาออกไป 1 ชั้น",
|
||||
"prio": "กลาง",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-24",
|
||||
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — ปิด default-shadow overlay เหลือเงาในสไปรต์"
|
||||
},
|
||||
{
|
||||
"n": 27,
|
||||
"name": "optimize ตัวละครให้เล็กลงอีก มันโหลดช้า",
|
||||
"detail": "",
|
||||
"prio": "สูง",
|
||||
"status": "เปิด",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 28,
|
||||
"name": "ปุ่ม start ใน lobby",
|
||||
"detail": "ทำให้ดูโต้ตอบหน่อย เช่นพอเดินเข้าไปใกล้ให้มัน glow ขึ้นมา",
|
||||
"prio": "กลาง",
|
||||
"status": "เปิด",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 29,
|
||||
"name": "เกมต่อตึก ผิด flow",
|
||||
"detail": "สี blog กับคนเล่นยังไม่สัมพันกัน ต้องเรียงตามสี และตามคิว",
|
||||
"prio": "วิกฤต",
|
||||
"status": "เปิด",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 30,
|
||||
"name": "เกมต่อตึก บั๊กคนที่ 5",
|
||||
"detail": "คนที่ 5 ไม่สามารถเล่นได้",
|
||||
"prio": "วิกฤต",
|
||||
"status": "เปิด",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 31,
|
||||
"name": "เอา mock การ์ดผู้ต้องหาเก่าออกไป",
|
||||
"detail": "มันแว่บๆมา",
|
||||
"prio": "กลาง",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-25",
|
||||
"note": "✅ แก้แล้ว 2026-06-25 v0.006242530 (รอเทส) — ซ่อนรูปจนรูปคดีจริงโหลด + spinner"
|
||||
},
|
||||
{
|
||||
"n": 32,
|
||||
"name": "เอา popup ขออนุญาตใช้ไมค์ออกไปเลย น่ารำคาญ",
|
||||
"detail": "",
|
||||
"prio": "สูง",
|
||||
"status": "เปิด",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 33,
|
||||
"name": "หน้า howto เปลี่ยนสีปุ่ม ready ด้วยตอน กดready แล้ว",
|
||||
"detail": "เปลี่ยนเป้นสีเขียวพร้อมเครื่องหมายถูกตามดีไซน์",
|
||||
"prio": "สูง",
|
||||
"status": "เปิด",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 34,
|
||||
"name": "เกม ใช่ หรือไม่ : ตอนจบบั๊ค",
|
||||
"detail": "ตอนสรุปคะแนน ยังนับเวลาอยุ่ และขึ้น gameover ทับหน้าสรุปคะแนน",
|
||||
"prio": "วิกฤต",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-24",
|
||||
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG1 ใช่/ไม่: capture mg1AnyScore ก่อนล้าง (gameover) + guard quiz-phase หลังจบ (timer)"
|
||||
},
|
||||
{
|
||||
"n": 35,
|
||||
"name": "เกมวิ่ง : กดปุ่มไม่กระโดด",
|
||||
"detail": "ปุ่ม jump กดแล้วไม่กระโดด",
|
||||
"prio": "วิกฤต",
|
||||
"status": "เปิด",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 36,
|
||||
"name": "เกมวิ่ง : ตอนจบบั๊ค วิ่งเลยฉาก",
|
||||
"detail": "ตอนจบบั๊ค วิ่งเลยฉาก (ในบางรอบ)",
|
||||
"prio": "วิกฤต",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-24",
|
||||
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG2 วิ่งเลยฉาก: walk-to-finish ค่อยๆเดินเข้าเส้น + clamp ไม่เลยเส้น"
|
||||
},
|
||||
{
|
||||
"n": 37,
|
||||
"name": "popup รับหลักฐาน ทับไมค์",
|
||||
"detail": "กดไมค์ไม่ได้ ตอนรับหลักฐาน ไปดูเรื่องเลเยอร์อีกที",
|
||||
"prio": "กลาง",
|
||||
"status": "เปิด",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 38,
|
||||
"name": "หน้าส่งหลักฐาน ปุ่มส่งหลักฐานหลุดจอ (เครื่องของเนส)",
|
||||
"detail": "เป้นเครื่องเนสคนเดียว",
|
||||
"prio": "วิกฤต",
|
||||
"status": "เปิด",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 39,
|
||||
"name": "ห้องส่งหลักฐาน : เมื่อมีคนหลุด 1 คน จะไปต่อไม่ได้เลย",
|
||||
"detail": "",
|
||||
"prio": "วิกฤต",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-24",
|
||||
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — testimony: disconnect ลบ submit/ready + client ใช้ totalPlayers authoritative"
|
||||
},
|
||||
{
|
||||
"n": 40,
|
||||
"name": "จำนวนคน ให้เล่นได้ตั้งแต่ 4-6 คน",
|
||||
"detail": "กรณีเลือกผู้เล่นตั้งแต่ 4 คนขึ้นไปไม่ต้องมี bot",
|
||||
"prio": "วิกฤต",
|
||||
"status": "เปิด",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 41,
|
||||
"name": "เกม หยิบแผ่นป้าย",
|
||||
"detail": "ต้องเล่นให้ครบ 10 ข้อก่อน ค่อยจบเกม (ตอนนี้เล่นถูก 1 ข้อ จบเกมเลย)",
|
||||
"prio": "วิกฤต",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-24",
|
||||
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG4 หยิบป้าย: ใช้ map จริง (currentPlayMapId) เปิด mission flow + default 10 ข้อ"
|
||||
},
|
||||
{
|
||||
"n": 42,
|
||||
"name": "เกม กระโดด",
|
||||
"detail": "ท่ากระโดดแปลก ยืนซอยเท้าทำไม",
|
||||
"prio": "กลาง",
|
||||
"status": "เปิด",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 43,
|
||||
"name": "เกม กระโดด",
|
||||
"detail": "ขั้นบันไดมันถี่ไป บางทีกระโดดแล้วเบิ้ลขั้นเลย ปรับให้ระยะห่างแต่ละขั้นมันเท่ากัน",
|
||||
"prio": "สูง",
|
||||
"status": "เปิด",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 44,
|
||||
"name": "เกมกระโดด",
|
||||
"detail": "อีเวนท์พิเศษ (ทนาย/เทพความรู้) หายไป",
|
||||
"prio": "วิกฤต",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-24",
|
||||
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG5 อีเวนต์พิเศษ: เพิ่ม branch jump_survive วาง icon บน platform"
|
||||
},
|
||||
{
|
||||
"n": 45,
|
||||
"name": "เกมไม่รองรับ 4K",
|
||||
"detail": "หน้ารับหลักฐาน (หลังจากเล่นเกมจบ)",
|
||||
"prio": "วิกฤต",
|
||||
"status": "เปิด",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 46,
|
||||
"name": "เกมยิิงอุกาบาด",
|
||||
"detail": "สีตัวละครยังไม่ตรงกับที่เลือกมา",
|
||||
"prio": "วิกฤต",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-24",
|
||||
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG6 สี: ยาน Rocket-1..8 ตาม lobby theme ของผู้เล่น"
|
||||
},
|
||||
{
|
||||
"n": 47,
|
||||
"name": "เกมยิิงอุกาบาด",
|
||||
"detail": "อุกาบาดให้พุ่งเร็วกว่านี้ และลูกเล็กกว่านี้เกมง่ายไป",
|
||||
"prio": "สูง",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-24",
|
||||
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส) — MG6: อุกาบาตเร็วขึ้น (+~50%) + เล็กลง (48->34/11->8)"
|
||||
},
|
||||
{
|
||||
"n": 48,
|
||||
"name": "เพิ่ม popup เมื่อเกมค้าง เล่นต้่อไม่ได้ ให้มีปุ่มกดออกไปหน้าแรกสุด",
|
||||
"detail": "สาเหตุเกิดจากมีคนหลุดระหว่างรอ host กด",
|
||||
"prio": "วิกฤต",
|
||||
"status": "เปิด",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 49,
|
||||
"name": "กรณีผู้เล่นหลุดออกจากเกม",
|
||||
"detail": "ถ้ามีคนหลุด ให้กลายเป้นบอทแทน",
|
||||
"prio": "วิกฤต",
|
||||
"status": "เปิด",
|
||||
"updated": "",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"n": 50,
|
||||
"name": "ผู้เล่น 6 คน: บางเกมเล่นได้แค่ 5 (คนที่ 6 หายจากสรุป)",
|
||||
"detail": "สรุปคะแนนฝั่ง client slice(0,5) ตัดคนที่ 6 — แก้เป็น slice(0,6) ทุกมินิเกม (MG1/3/4/5/6/7)",
|
||||
"prio": "วิกฤต",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-24",
|
||||
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส)"
|
||||
},
|
||||
{
|
||||
"n": 51,
|
||||
"name": "ผู้เล่น 6 คน: คะแนนตอนจบไม่ตรงกันแต่ละจอ",
|
||||
"detail": "MG1 merge คะแนน server / MG6,7 merge คะแนน host -> ครบ 6 ตรงกัน (MG3 ยัง local)",
|
||||
"prio": "วิกฤต",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-24",
|
||||
"note": "✅ แก้แล้ว 2026-06-24 v0.006242500 (รอเทส)"
|
||||
},
|
||||
{
|
||||
"n": 52,
|
||||
"name": "หน้าไต่สวน/สรุปหลักฐาน: กรอบปลิ้นตอน 6 คน",
|
||||
"detail": "member row grid repeat(5)->repeat(6) ใน testimony-overlay.css (คนที่6 ตกแถว2)",
|
||||
"prio": "สูง",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-25",
|
||||
"note": "✅ แก้แล้ว 2026-06-25 v0.006242530 (รอเทส)"
|
||||
},
|
||||
{
|
||||
"n": 53,
|
||||
"name": "เพิ่มหน้าโหลด (loading) ตอนเข้าเกม",
|
||||
"detail": "overlay spinner กันเห็นจอเปล่า/asset แว่บ ซ่อนเมื่อ draw เฟรมแรก + safety 8s",
|
||||
"prio": "สูง",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-25",
|
||||
"note": "✅ แก้แล้ว 2026-06-25 v0.006242530 (รอเทส)"
|
||||
},
|
||||
{
|
||||
"n": 54,
|
||||
"name": "host โหลดนานมากตอนเข้าเกม",
|
||||
"detail": "optimize asset/char (เกี่ยว row 28) — loading overlay ช่วยซ่อน แต่ยังโหลดช้าจริง",
|
||||
"prio": "สูง",
|
||||
"status": "เปิด",
|
||||
"updated": "2026-06-25",
|
||||
"note": "loading overlay บังอาการแล้ว · root = canvas tint ภาพตัวละครใหญ่ (source ไม่อยู่ใน repo) → ต้อง optimize ภาพแยก"
|
||||
},
|
||||
{
|
||||
"n": 55,
|
||||
"name": "หลังออกจาก LobbyA ไม่ให้ใครเข้า join",
|
||||
"detail": "server มี joinLocked (set ตอน LobbyA->case ~8656, check ~7585 whitelist nickname) — ต้อง repro ว่าใครเข้าได้ยังไง (room list ยังโชว์? ชื่อซ้ำ whitelist?)",
|
||||
"prio": "สูง",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-25",
|
||||
"note": "✅ แก้แล้ว 2026-06-25 — กรองห้อง joinLocked ออกจาก /api/spaces list (ห้องเริ่มคดีแล้วหาย) + server reject อยู่แล้ว"
|
||||
},
|
||||
{
|
||||
"n": 56,
|
||||
"name": "ตอนกลับ LobbyB โชว์รูปแรกก่อนแล้วค่อยถูก",
|
||||
"detail": "เหมือน #1 การ์ด — ต้องใส่ loading state ตอน return LobbyB ด้วย (มี room-loading-overlay อยู่แต่ซ่อนเร็วไป)",
|
||||
"prio": "สูง",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-25",
|
||||
"note": "✅ แก้แล้ว 2026-06-25 v0.0308 — การ์ดมี spinner (#1) + room-loading overlay ใส่ % + รอ char preload เสร็จก่อนซ่อน + safety 12s"
|
||||
},
|
||||
{
|
||||
"n": 57,
|
||||
"name": "เล่นมินิเกมเกิน 3 ครั้งต่อผู้ต้องสงสัย (+การ์ดพิเศษโผล่รอบที่4)",
|
||||
"detail": "server cap suspect-pick-start (max หลักฐาน>=3 reject) + award cap 3 + client canInvestigateSuspect<3 (เดิม stub return true). UI messaging มีอยู่แล้ว",
|
||||
"prio": "วิกฤต",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-25",
|
||||
"note": "✅ แก้แล้ว 2026-06-25 v0.0309/server (รอเทส)"
|
||||
},
|
||||
{
|
||||
"n": 58,
|
||||
"name": "ปุ่ม \"เล่นเกมต่อ\" ไม่รู้ว่าค้างหรือรอคนอื่น",
|
||||
"detail": "text \"รอผู้เล่นอื่น (X/Y)\" ถูก set อยู่แล้วแต่ CSS font-size:0 ซ่อน → เพิ่ม .is-waiting CSS โชว์ข้อความ+spinner แทนรูปปุ่ม",
|
||||
"prio": "สูง",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-25",
|
||||
"note": "✅ แก้แล้ว 2026-06-25 v0.0309/server (รอเทส)"
|
||||
},
|
||||
{
|
||||
"n": 59,
|
||||
"name": "loading ไม่คลุม scene transition (เห็นฉากเก่าก่อนเปลี่ยน)",
|
||||
"detail": "LobbyA->B (in-page swap) โชว์ room-loading ระหว่าง fetch+swap · LobbyB->เกม โชว์ overlay ก่อน location.href · play.html มี overlay รับช่วง",
|
||||
"prio": "สูง",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-25",
|
||||
"note": "✅ แก้แล้ว 2026-06-25 v0.0309/server (รอเทส)"
|
||||
},
|
||||
{
|
||||
"n": 60,
|
||||
"name": "ทำหน้า testcase บนเซิร์ฟเวอร์",
|
||||
"detail": "srv1361159.hstgr.cloud/testcase — bug list + filter/search/นับสถานะ generate จาก xlsx",
|
||||
"prio": "กลาง",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-25",
|
||||
"note": "✅ แก้แล้ว 2026-06-25 v0.0309/server (รอเทส)"
|
||||
},
|
||||
{
|
||||
"n": 61,
|
||||
"name": "MG3 สี block ไม่ตรง user (เกม+admin)",
|
||||
"detail": "PNG Block-Code-1..8 deploy ครบแล้ว · ต้อง wire สี block = lobby theme ของผู้วาง (seat=theme refactor, ต้องเทส 2 จอ) + admin 8-slot config",
|
||||
"prio": "วิกฤต",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-25",
|
||||
"note": "✅ แก้แล้ว 2026-06-25 v0.006250200 — block สีตาม theme ผู้เล่นที่ seat (Block-Code-1..8) ตอนวาด ไม่แตะ turn/sync"
|
||||
},
|
||||
{
|
||||
"n": 62,
|
||||
"name": "MG1/MG4 user/bot ติดกัน (เดินไม่ผ่าน)",
|
||||
"detail": "เพิ่ม anti-stuck: ดันชนเพื่อน → เขย่า (shake) + ปลดล็อกเดินผ่านกันชั่วคราว คงกำแพง/area/quiz-lock; ทั้งผู้เล่นและบอท",
|
||||
"prio": "สูง",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-25",
|
||||
"note": "✅ แก้แล้ว 2026-06-25 v0.006250100 (รอเทส)"
|
||||
},
|
||||
{
|
||||
"n": 63,
|
||||
"name": "LobbyA มือถือ กดเดินไม่ได้",
|
||||
"detail": "lobby canvas มีแค่ dblclick (ไม่มี touch) → มือถือแตะเดินไม่ได้. แก้: เพิ่ม tap-to-walk (เฉพาะ touch, PC ยัง dblclick) + DPI-safe coord + touch-action:none + zoom-out default บนจอ<600px (PC ย่อส่วน)",
|
||||
"prio": "วิกฤต",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-25",
|
||||
"note": "✅ แก้แล้ว 2026-06-25 room-lobby.js?v=0.0310 (รอเทสมือถือ) · PC ไม่กระทบ"
|
||||
},
|
||||
{
|
||||
"n": 64,
|
||||
"name": "มือถือ: ปุ่มแชท AI ใหญ่ไป + ปุ่ม F (host) ไม่มี",
|
||||
"detail": "AI chat collapsed btn 213x201px → ใหญ่บนมือถือ. + การกด F (interact: host เลือกระดับ/คดี, ห้องแต่งตัว) ไม่มีบนมือถือ",
|
||||
"prio": "สูง",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-25",
|
||||
"note": "✅ แก้แล้ว 2026-06-25 (css 0.0176/js 0.0311): media<600px ย่อปุ่ม AI (92px) + ปุ่มลอย \"โต้ตอบ(F)\" บน touch-primary device (dispatch synthetic KeyF). PC ไม่เห็น"
|
||||
},
|
||||
{
|
||||
"n": 65,
|
||||
"name": "อีเวนต์พิเศษ (ทนาย/เทพ) = 1 ใน 3 (ตามดีไซน์)",
|
||||
"detail": "ดีไซน์: 3 เกม/LobbyB เล่นได้เกมละ 3 ครั้ง · อีเวนต์พิเศษ 1/3 ต่อครั้ง (1 ใบ) · Test Mode บังคับ. revert การแก้ \">1 ต่อรอบ\" กลับเป็น 1 ใบ/ครั้งที่ติด",
|
||||
"prio": "กลาง",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-25",
|
||||
"note": "✅ revert 2026-06-25 — กลับเป็น 1/3 (1 ใบ/ครั้งที่ติด) ตามดีไซน์ · ยืนยันโครงสร้าง 3เกม×3ครั้ง + disruptor toggle (>4คน/Test Mode) ถูกต้อง"
|
||||
},
|
||||
{
|
||||
"n": 66,
|
||||
"name": "host หลุด → ย้าย host ให้คนอยู่นานสุด",
|
||||
"detail": "pickNextHostPeer เลือก spawnJoinOrder ต่ำสุด (เสถียรข้าม reconnect) แทน Map-first + host จริง reclaim ได้",
|
||||
"prio": "สูง",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-25",
|
||||
"note": "✅ แก้แล้ว 2026-06-25 (server) — Part 1 host migration"
|
||||
},
|
||||
{
|
||||
"n": 67,
|
||||
"name": "คนหลุดกลางเกม → 10 วิ reconnect + บอทเล่นแทน",
|
||||
"detail": "peer-disconnect-grace แบนเนอร์นับ 10 วิ · กลับทัน=cancel เล่นต่อ · ครบเวลา=peer-bot-takeover host เติมบอท 1 ตัว (rebalance, cap≤6). บอทไม่สืบทอดตำแหน่ง/คะแนน",
|
||||
"prio": "สูง",
|
||||
"status": "แก้ไขแล้ว",
|
||||
"updated": "2026-06-25",
|
||||
"note": "✅ แก้แล้ว 2026-06-25 (server+play.js v0.006250400) — Part 2 แบบ A · seamless(host คุม entity เดิม)=ค้าง(เสี่ยง)"
|
||||
}
|
||||
]
|
||||
Executable
+98
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"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": "fail",
|
||||
"comment": "❌ ยังไม่ผ่าน — กด exit จาก lobby ที่ join มา กลับไปหน้า \"สร้างห้องเกม\" แทนหน้า \"เข้าร่วมเกม\" bug ยังอยู่ อัปเดต 2026-06-25",
|
||||
"images": [],
|
||||
"at": "2026-06-25 07:06"
|
||||
},
|
||||
"53": {
|
||||
"result": "pass",
|
||||
"comment": "✅ ผ่าน — หน้าโหลด \"กำลังเข้าสู่การสืบสวน\" มีอยู่และแสดงผลก่อน redirect อัปเดต 2026-06-25",
|
||||
"images": [],
|
||||
"at": "2026-06-25 07:17"
|
||||
},
|
||||
"60": {
|
||||
"result": "none",
|
||||
"comment": "✅ ผ่าน — หน้า testcase บนเซิร์ฟเวอร์ใช้งานได้ที่ \/testcase\/ อัปเดต 2026-06-25",
|
||||
"images": [],
|
||||
"at": "2026-06-25 07:17"
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,236 @@
|
||||
<?php
|
||||
// Justice — Test Case / Bug List tracker (QA verification + admin)
|
||||
$DATA_DIR = __DIR__ . '/data';
|
||||
$BUGS = $DATA_DIR . '/bugs.json';
|
||||
$VERIFY = $DATA_DIR . '/verify.json';
|
||||
$UPLOADS = __DIR__ . '/uploads';
|
||||
$ADMIN_PASS = 'justice2026'; // รหัสโหมดแอดมิน
|
||||
|
||||
function tc_load($f){ return is_file($f) ? (json_decode(file_get_contents($f), true) ?: []) : []; }
|
||||
|
||||
// ---------- API: บันทึกผลตรวจ (POST) ----------
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (($_POST['api'] ?? '') === 'save')) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
if (($_POST['pass'] ?? '') !== $ADMIN_PASS) { http_response_code(403); echo json_encode(['ok'=>false,'error'=>'รหัสแอดมินไม่ถูกต้อง'], JSON_UNESCAPED_UNICODE); exit; }
|
||||
$id = preg_replace('/[^0-9]/', '', (string)($_POST['id'] ?? ''));
|
||||
if ($id === '') { echo json_encode(['ok'=>false,'error'=>'no id'], JSON_UNESCAPED_UNICODE); exit; }
|
||||
if (!is_dir($DATA_DIR)) @mkdir($DATA_DIR, 0775, true);
|
||||
$verify = tc_load($VERIFY);
|
||||
$rec = $verify[$id] ?? ['result'=>'none','comment'=>'','images'=>[]];
|
||||
if (!isset($rec['images']) || !is_array($rec['images'])) $rec['images'] = [];
|
||||
$res = $_POST['result'] ?? '';
|
||||
if (in_array($res, ['pass','fail','none'], true)) $rec['result'] = $res;
|
||||
if (isset($_POST['comment'])) $rec['comment'] = mb_substr(trim((string)$_POST['comment']), 0, 2000);
|
||||
// ลบรูป
|
||||
if (($_POST['removeImage'] ?? '') !== '') {
|
||||
$rm = basename((string)$_POST['removeImage']);
|
||||
$rec['images'] = array_values(array_filter($rec['images'], function($x) use ($rm){ return $x !== $rm; }));
|
||||
@unlink($UPLOADS . '/' . $rm);
|
||||
}
|
||||
// อัปโหลดรูป
|
||||
if (!empty($_FILES['image']['tmp_name']) && is_uploaded_file($_FILES['image']['tmp_name'])) {
|
||||
$ext = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION));
|
||||
if (in_array($ext, ['jpg','jpeg','png','gif','webp'], true) && $_FILES['image']['size'] < 8*1024*1024) {
|
||||
if (!is_dir($UPLOADS)) @mkdir($UPLOADS, 0775, true);
|
||||
$fn = $id . '_' . time() . '_' . bin2hex(random_bytes(3)) . '.' . $ext;
|
||||
if (move_uploaded_file($_FILES['image']['tmp_name'], $UPLOADS . '/' . $fn)) $rec['images'][] = $fn;
|
||||
}
|
||||
}
|
||||
$rec['at'] = date('Y-m-d H:i');
|
||||
$verify[$id] = $rec;
|
||||
file_put_contents($VERIFY, json_encode($verify, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX);
|
||||
echo json_encode(['ok'=>true,'rec'=>$rec], JSON_UNESCAPED_UNICODE); exit;
|
||||
}
|
||||
|
||||
$bugs = tc_load($BUGS);
|
||||
$verify = tc_load($VERIFY);
|
||||
?><!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;}
|
||||
*{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}
|
||||
.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}
|
||||
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}
|
||||
.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{width:100%;background:var(--panel2);border:1px solid var(--line);border-radius:8px;color:var(--txt);font-family:inherit;font-size:13px;padding:8px;resize:vertical;min-height:46px}
|
||||
.edit .erow{display:flex;gap:8px;align-items:center;margin-top:6px;flex-wrap:wrap}
|
||||
.edit .save{background:var(--green);color:#042c14;border:none;border-radius:8px;padding:7px 16px;font-weight:600;cursor:pointer;font-family:inherit}
|
||||
.edit .msg{font-size:12px}
|
||||
.editbtn{margin-top:7px;background:none;border:1px dashed var(--line);color:var(--mut);border-radius:8px;padding:4px 11px;font-size:12px;cursor:pointer;font-family:inherit}
|
||||
.foot{color:var(--mut);font-size:12px;margin-top:16px;text-align:center}
|
||||
.hidden{display:none}
|
||||
@media(max-width:760px){.hide-sm{display:none}td,th{padding:9px 7px}}
|
||||
</style></head>
|
||||
<body><div class="wrap">
|
||||
<h1>Justice — Test Case / Bug List<span class="sub">รายการบัก + ผลตรวจ QA · srv1361159.hstgr.cloud/testcase</span></h1>
|
||||
<div class="cards" id="cards"></div>
|
||||
<div class="bar">
|
||||
<input type="text" id="q" placeholder="ค้นหา… (ชื่อ / รายละเอียด / คำอธิบาย)">
|
||||
<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>
|
||||
<table><thead><tr><th style="width:30px">#</th><th>ปัญหา</th><th style="width:78px">สำคัญ</th><th style="width:88px">Dev</th><th class="qa">ผลตรวจ QA</th></tr></thead>
|
||||
<tbody id="tb"></tbody></table>
|
||||
<div class="foot">Justice bug tracker · ผลตรวจบันทึกบนเซิร์ฟเวอร์ (แก้ไขได้เมื่อใส่รหัสแอดมิน) · <?php echo count($bugs); ?> รายการ</div>
|
||||
</div>
|
||||
<script>
|
||||
var BUGS = <?php echo json_encode($bugs, JSON_UNESCAPED_UNICODE); ?>;
|
||||
var VERIFY = <?php echo json_encode((object)$verify, JSON_UNESCAPED_UNICODE); ?>;
|
||||
var filt="all",q="",adminPass="";
|
||||
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 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 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>';
|
||||
var list=BUGS.filter(function(b){
|
||||
var v=vof(b.n);
|
||||
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+" "+(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 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>'+
|
||||
'<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>';
|
||||
}
|
||||
return '<tr><td class="n">'+b.n+'</td><td><div class="nm">'+esc(b.name)+'</div>'+
|
||||
(b.detail?'<div class="dt">'+esc(b.detail)+'</div>':'')+
|
||||
(b.note?'<div class="nt">'+esc(b.note)+'</div>':'')+'</td>'+
|
||||
'<td>'+(b.prio?'<span class="b '+pCls(b.prio)+'">'+esc(b.prio)+'</span>':'')+'</td>'+
|
||||
'<td><span class="b '+sCls(b.status)+'">'+esc(b.status)+'</span></td>'+
|
||||
'<td class="qa">'+qa+'</td></tr>';
|
||||
}
|
||||
// events (delegate)
|
||||
function flashAdmin(){var a=document.getElementById("adminpass");a.classList.remove("flash");void a.offsetWidth;a.classList.add("flash");a.focus();}
|
||||
document.getElementById("tb").addEventListener("click",function(e){
|
||||
var t=e.target;
|
||||
if(t.dataset.qa){ if(!adminPass){ flashAdmin(); } else { var ee=document.getElementById("edit-"+t.dataset.qa); if(ee) ee.classList.toggle("show"); } return; }
|
||||
if(t.dataset.edit){var ed=document.getElementById("edit-"+t.dataset.edit);if(ed)ed.classList.toggle("show");return;}
|
||||
if(t.dataset.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.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 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();}});
|
||||
}
|
||||
// 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();})});
|
||||
render();
|
||||
</script></body></html>
|
||||
Reference in New Issue
Block a user