diff --git a/www/html/Admin/admin.js b/www/html/Admin/admin.js index 1867d8a..99c2427 100644 --- a/www/html/Admin/admin.js +++ b/www/html/Admin/admin.js @@ -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; }); diff --git a/www/html/Admin/index.html b/www/html/Admin/index.html index c9959a9..5906fcb 100644 --- a/www/html/Admin/index.html +++ b/www/html/Admin/index.html @@ -976,6 +976,8 @@
P4
P5
P6
+
P7
+
P8
@@ -1271,10 +1273,10 @@

บังคับเสนอบท "ตัวป่วน" (เทสต์)

-

ปกติบทตัวป่วนจะเสนอเฉพาะเมื่อมีคนจริงเกิน 4 คน · เปิดอันนี้เพื่อบังคับให้ขึ้นเสมอแม้เล่นคนเดียว (ไว้เทสต์)

+

ปกติบทตัวป่วนจะเสนอเฉพาะเมื่อมีคนจริงตั้งแต่ 4 คนขึ้นไป · เปิดอันนี้เพื่อบังคับให้ขึ้นเสมอแม้เล่นคนเดียว (ไว้เทสต์)

@@ -1283,6 +1285,6 @@
- + diff --git a/www/html/Admin/private/store.json b/www/html/Admin/private/store.json index 7c6101b..b0aca3a 100644 --- a/www/html/Admin/private/store.json +++ b/www/html/Admin/private/store.json @@ -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 + } } ] } \ No newline at end of file diff --git a/www/html/Game/data/game-timing.json b/www/html/Game/data/game-timing.json index 036d01c..659b4b8 100644 --- a/www/html/Game/data/game-timing.json +++ b/www/html/Game/data/game-timing.json @@ -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, diff --git a/www/html/Game/public/css/room-lobby.css b/www/html/Game/public/css/room-lobby.css index 74f3640..2dd3c98 100644 --- a/www/html/Game/public/css/room-lobby.css +++ b/www/html/Game/public/css/room-lobby.css @@ -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; + } +} diff --git a/www/html/Game/public/css/testimony-overlay.css b/www/html/Game/public/css/testimony-overlay.css index a3505b4..f004536 100644 --- a/www/html/Game/public/css/testimony-overlay.css +++ b/www/html/Game/public/css/testimony-overlay.css @@ -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; diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js index 40e8ff8..bd5e8c0 100644 --- a/www/html/Game/public/js/play.js +++ b/www/html/Game/public/js/play.js @@ -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 = '
กำลังโหลดเกม…
'; + 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 = ''; + 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 ? '' : ''; + 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= ของ - +
v —
+
+

Justice — Test Case / Bug Listรายการบัก/เทสเคส · อัปเดตจากการเทสจริง · srv1361159.hstgr.cloud/testcase

+
+
+ + + + +
+ +
#ปัญหาสำคัญสถานะอัปเดต
+
Justice bug tracker · generated from xlsx · 62 รายการ
+
+ \ No newline at end of file diff --git a/www/html/testcase/index.php b/www/html/testcase/index.php new file mode 100644 index 0000000..042e124 --- /dev/null +++ b/www/html/testcase/index.php @@ -0,0 +1,236 @@ +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); +?> + + +Justice — Test Case / Bug List + + + +
+

Justice — Test Case / Bug Listรายการบัก + ผลตรวจ QA · srv1361159.hstgr.cloud/testcase

+
+
+ + + + + + + + + 🔒 แก้ผลตรวจ — ใส่รหัส: + + + +
+ +
#ปัญหาสำคัญDevผลตรวจ QA
+
Justice bug tracker · ผลตรวจบันทึกบนเซิร์ฟเวอร์ (แก้ไขได้เมื่อใส่รหัสแอดมิน) · รายการ
+
+