diff --git a/www/html/Admin/private/store.json b/www/html/Admin/private/store.json index 30055c3..d55ba6a 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": 2072, + "coins": 2140, "createdAt": "2026-04-02T05:52:21+00:00", - "updatedAt": "2026-06-26T07:58:33+00:00", + "updatedAt": "2026-06-26T10:25:41+00:00", "daily": { "anchorMs": 1781197200000, "claimedDays": [ @@ -67,13 +67,13 @@ ], "lockUntilMs": 0 }, - "score": 18, + "score": 86, "scoreByCase": { "1": 16, - "10": 514, + "10": 562, "8": 296, "11": 20, - "13": 224, + "13": 234, "14": 30, "9": 40, "12": 408, @@ -83,7 +83,7 @@ "7": 6, "3": 142, "6": 44, - "2": 80 + "2": 90 }, "lbName": "Q", "achievements": { @@ -651,19 +651,20 @@ "providerUserId": "p_1782016990238_0u37bv7pxlxs", "notes": "auto: player-coins", "blocked": false, - "coins": 288, + "coins": 368, "createdAt": "2026-06-21T04:43:11+00:00", - "updatedAt": "2026-06-26T08:07:51+00:00", + "updatedAt": "2026-06-26T09:44:40+00:00", "lobbyColorThemeIndex": 8, "lobbySkinToneIndex": 1, - "score": 258, + "score": 338, "scoreByCase": { "13": 82, - "10": 120, + "10": 172, "6": 8, "8": 20, - "2": 18, - "3": 10 + "2": 36, + "3": 10, + "7": 10 }, "lbName": "MONE", "achievements": { diff --git a/www/html/Game/data/game-timing.json b/www/html/Game/data/game-timing.json index 3cec9d9..9f6aceb 100644 --- a/www/html/Game/data/game-timing.json +++ b/www/html/Game/data/game-timing.json @@ -74,7 +74,7 @@ "/Game/img/ViolentCrime/Meteo-3.png" ], "spaceShooterAsteroidExplodeFrameMs": 60, - "spaceShooterAsteroidIntervalMs": 300, + "spaceShooterAsteroidIntervalMs": 200, "spaceShooterShipDamageOverlayUrls": [ "/Game/img/ViolentCrime/Rocket-broke-1.png", "/Game/img/ViolentCrime/Rocket-broke-2.png", diff --git a/www/html/Game/data/quiz-settings.json b/www/html/Game/data/quiz-settings.json index 71df438..5550e8c 100644 --- a/www/html/Game/data/quiz-settings.json +++ b/www/html/Game/data/quiz-settings.json @@ -4,7 +4,7 @@ "betweenMs": 3000, "carryReadMs": 3000, "carryAnswerMs": 15000, - "carrySessionLength": 2, + "carrySessionLength": 10, "carryMapPanelTheme": { "panelBg": "rgba(255, 0, 0, 0)", "panelBorder": "rgba(255, 255, 255, 0)", diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js index 777eaaa..7b2801d 100644 --- a/www/html/Game/public/js/play.js +++ b/www/html/Game/public/js/play.js @@ -106,7 +106,20 @@ ov.innerHTML = '
กำลังโหลดเกม…
'; document.body.insertBefore(ov, document.body.firstChild); } - setTimeout(hidePlayLoadingOverlay, 8000); /* safety: กันค้างถ้าโหลดนาน/ติด */ + /* safety กันค้าง — แต่ "อย่าซ่อนถ้าเกมยังไม่ render" (เน็ตช้า join นาน → ถ้าซ่อน 8 วิ = จอดำเปล่า ไม่รู้สถานะ) + เช็คทุก 1.5 วิ: เกมเริ่มวาดแล้ว (playLoadingHideScheduled) → ปล่อย path ปกติซ่อน · ยังไม่วาด+ช้า → โชว์ "เน็ตช้า (N วิ)" คง spinner · เกิน 30 วิ ค่อยซ่อนกันค้างถาวร */ + const _ploT0 = Date.now(); + const _ploCheck = function () { + if (playLoadingHidden || playLoadingHideScheduled) return; /* ซ่อนแล้ว/เกมเริ่มวาดแล้ว */ + const waited = Date.now() - _ploT0; + if (waited > 30000) { hidePlayLoadingOverlay(); return; } /* hard limit กันค้างถาวร */ + if (waited > 5000) { + const txt = document.querySelector('#play-loading-overlay .plo-text'); + if (txt) txt.textContent = 'เน็ตช้า… กำลังโหลด (' + Math.floor(waited / 1000) + ' วิ)'; + } + setTimeout(_ploCheck, 1500); + }; + setTimeout(_ploCheck, 1500); } catch (_e) { playLoadingHidden = true; } })(); const playMapIdFromQuery = (params.get('map') || '').trim(); @@ -730,7 +743,9 @@ socket.emit('stack-state', { idx: stackPreviewTurnIndex, /* theme ต่อที่นั่ง (host เป็นเจ้าของ) → ทุกจอลงสี block ตรงกัน (เดิม non-host คำนวณสีเองจาก playTint → ไม่ตรง 2 จอ) */ - order: stackPreviewTurnOrder.map((e) => ({ kind: e.kind, seat: e.seat, id: e.id, botId: e.botId || null, theme: stackSeatColorThemePlay(e.seat) || null })), + order: stackPreviewTurnOrder.map((e) => ({ kind: e.kind, seat: e.seat, id: e.id, botId: e.botId || null, theme: stackSeatThemeForBroadcastPlay(e.seat) })), + /* สีของ block ที่ "วางแล้ว" ทั้งหอ (host authoritative) → client sync เขียนทับ layer ที่เพี้ยนค้าง (จากจังหวะ dual-host ต้นเกม) */ + lthemes: (stackMini && Array.isArray(stackMini.layers)) ? stackMini.layers.map(function (l) { return (l && l.theme >= 1 && l.theme <= 8) ? l.theme : 0; }) : [], }); logStackSeatColors('host-bcast'); } catch (e) { /* ignore */ } @@ -740,6 +755,29 @@ function stackTurnOrderReadyPlay() { return !!(stackPreviewTurnOrder && stackPreviewTurnOrder.length >= 1); } + /** (host) คิวตาตรงกับผู้เล่น/บอทปัจจุบันไหม — ใช้ auto-heal late-join (client เข้าทีหลังต้องได้ seat ไม่งั้น "เล่นไม่ได้" + ไม่มีสี) + เทียบรายชื่อคน (me + others ที่ไม่ใช่บอท/ไม่ถูกแบน) + บอท กับ entry ในคิว; ต่างเมื่อไหร่ = ต้อง rebuild */ + let _stackHumansSig = ''; + function stackOrderNeedsRebuildPlay() { + if (!stackTurnOrderReadyPlay() || !Array.isArray(stackPreviewTurnOrder)) return true; + /* เช็คเฉพาะ "คน" (humans ≤ maxPlayers จึงลงคิวครบเสมอ ไม่โดน cap ตัด) — บอทจัดการใน rebuild เอง */ + const cur = []; + if (myId != null && !playBannedSpectator) cur.push(String(myId)); + others.forEach((o, id) => { + if (isPreviewBotId(id)) return; + if (o && o.bannedSpectator) return; + cur.push(String(id)); + }); + cur.sort(); + const sig = cur.join('|'); + if (sig === _stackHumansSig) return false; /* คนไม่เปลี่ยน → ไม่ต้องเช็ค (กัน rebuild ถี่) */ + const ord = []; + stackPreviewTurnOrder.forEach((e) => { if (e && e.kind !== 'bot' && !e.botId && e.id != null) ord.push(String(e.id)); }); + ord.sort(); + const need = (sig !== ord.join('|')); + if (!need) _stackHumansSig = sig; /* ตรงแล้ว → cache กันเช็คซ้ำทุกเฟรม */ + return need; + } function stackCurrentTurnEntryPlay() { if (!stackTurnOrderReadyPlay()) return null; return stackPreviewTurnOrder[stackPreviewTurnIndex % stackPreviewTurnOrder.length] || null; @@ -766,8 +804,8 @@ pts: hit.pts || 0, progressDelta: hit.progressDelta || 0, landOffsetTiles: hit.landOffsetTiles || 0, overlap: hit.overlap, supportRatio: hit.supportRatio, delta: hit.delta || 0, seat: seat || 1, heavy: !!heavy, actorId: actorId != null ? String(actorId) : '', isBot: !!isBot, - /* สี (theme) ที่ host/เจ้าของตา resolve ตอนวาง → ส่งไปด้วย ให้บล็อกที่วางบนทุกจอใช้สีเดียวกัน (กัน host/client คนละสี) */ - theme: stackSeatColorThemePlay(seat) || null, + /* สี (theme) ที่ host/เจ้าของตา resolve ตอนวาง → ส่งไปด้วย ให้บล็อกที่วางบนทุกจอใช้สีเดียวกัน (กัน host/client คนละสี) — never-null */ + theme: stackSeatThemeForBroadcastPlay(seat), }); } catch (e) { /* ignore */ } } @@ -843,6 +881,15 @@ return ent ? playLobbyThemeIndexForEntityPlay(ent) : null; } + /** สีที่ host จะ broadcast — "ไม่มีทาง null": resolve ปกติ ไม่ได้ค่อย default ตาม seat + → client (broadcast-first) ยึดค่า host ได้เสมอ ไม่ตกไปคำนวณเอง = สีตรง host ทุกจอแน่นอน */ + function stackSeatThemeForBroadcastPlay(seat) { + const t = stackSeatColorThemePlay(seat); + if (t >= 1 && t <= 8) return t; + const s = Math.floor(Number(seat)) || 1; + return ((s - 1) % 8) + 1; + } + /** ส่ง log ไป /gamelog (ดูรวมที่หน้าเว็บ) */ function gamelogPost(cat, ev, detail, who) { try { @@ -855,6 +902,7 @@ /** วินิจฉัยสี block MG3 ต่อ seat — เทียบ "สี avatar (playTint)" vs "สี block (resolved)"; ถ้าไม่ตรง → ส่งเข้า /gamelog เรียกเอง: window.__mg3ColorLog() หรือ auto throttle */ let _mg3ColorLogAt = 0; + let _mg3SnapAt = 0; let _mg3LastMismatchKey = ''; function logStackSeatColors(tag, force) { try { @@ -872,13 +920,34 @@ return { seat: e.seat, kind: e.kind, nick: (ent && ent.nickname) || String(e.id || e.botId || '').slice(0, 6), avatarIdx: avatarIdx, blockIdx: blockIdx, srv: (ent && ent.lobbyThemeIndexSrv) || null, bc: e.theme || null, ok: (avatarIdx && blockIdx) ? (avatarIdx === blockIdx) : null }; }); console.log('[mg3-color/' + (tag || '') + '] host=' + isMePlayHost(), JSON.stringify(rows)); - /* ส่งเฉพาะกรณีพบ mismatch (avatar≠block) → เห็นใน /gamelog (MG3) ทันที ไม่ต้องเปิด console */ + const role = isMePlayHost() ? 'host' : 'client'; + /* (A) snapshot ทุก ~5 วิ จาก "ทั้ง host และ client" → เทียบใน /gamelog ได้: block(=สีที่วาด) av(=สี avatar) srv bc */ + if (now - _mg3SnapAt > 5000) { + _mg3SnapAt = now; + /* รายชื่อ "คน" ใน others (เทียบกับคิว) — หา root "client หายจากคิว" */ + const othH = []; + if (myId != null) othH.push((me && me.nickname ? me.nickname : 'me') + '(self' + (playBannedSpectator ? ',BAN' : '') + ')'); + others.forEach(function (o, id) { + if (isPreviewBotId(id)) return; + othH.push((o && o.nickname ? o.nickname : String(id).slice(0, 5)) + (o && o.bannedSpectator ? '(BAN)' : '')); + }); + const ordSeats = rows.length; + /* block ที่ "วางแล้ว" (frozen layer.theme) — ตัวที่ user เห็นเพี้ยน; เทียบ host vs client ทีละ layer (seat:theme) */ + let lyr = ''; + if (stackMini && Array.isArray(stackMini.layers)) { + const L = stackMini.layers; + lyr = ' || layers(' + L.length + ')=[' + L.slice(-12).map(function (l) { return (l && l.seat != null ? l.seat : '?') + ':' + (l && l.theme != null ? l.theme : 'null'); }).join(',') + ']'; + } + const summary = 'order=' + ordSeats + 'seat othHumans=[' + othH.join(',') + '] :: ' + + rows.map(function (r) { return 's' + r.seat + ':' + (r.nick || '?').slice(0, 5) + ' blk=' + r.blockIdx + ' av=' + r.avatarIdx; }).join(' | ') + lyr; + gamelogPost('MG3', 'color-snap', summary, role); + } + /* (B) mismatch (avatar≠block บนจอเดียวกัน) → ส่งทันที (เปลี่ยนค่อยส่งซ้ำ) */ const bad = rows.filter(function (r) { return r.ok === false; }); if (bad.length) { - const key = (isMePlayHost() ? 'H' : 'C') + '|' + bad.map(function (r) { return r.seat + ':' + r.avatarIdx + '!=' + r.blockIdx; }).join(','); + const key = role + '|' + bad.map(function (r) { return r.seat + ':' + r.avatarIdx + '!=' + r.blockIdx; }).join(','); if (key !== _mg3LastMismatchKey) { _mg3LastMismatchKey = key; - const role = isMePlayHost() ? 'host' : 'client'; const detail = bad.map(function (r) { return r.nick + ' seat' + r.seat + ' avatar#' + r.avatarIdx + ' แต่ block#' + r.blockIdx + ' (srv' + r.srv + '/bc' + r.bc + ')'; }).join(' | '); gamelogPost('MG3', 'color-mismatch', detail, role); } @@ -12325,7 +12394,8 @@ lastStackTickMs = now; /* เล่นจริง: host ส่งลำดับตา+index เป็นระยะ → non-host adopt (ครอบ late-join: client โหลดช้า host จะรวมเข้า order ให้) */ if (detectiveCaseFillBots && isMePlayHost()) { - if (!stackTurnOrderReadyPlay()) rebuildStackPreviewTurnOrder(); + /* rebuild เมื่อ (ก) ยังไม่มีคิว หรือ (ข) คน/บอท เปลี่ยน (เช่น client เข้าทีหลัง = late-join) → ให้ทุกคนได้ seat + สี */ + if (!stackTurnOrderReadyPlay() || stackOrderNeedsRebuildPlay()) rebuildStackPreviewTurnOrder(); const ts = Date.now(); if (ts - stackLastStateEmit > 300) { stackLastStateEmit = ts; broadcastStackStatePlay(); } } @@ -12602,11 +12672,13 @@ ? stackFall.actorSeat : (m.pendingDropSeat != null ? m.pendingDropSeat : getStackDropActorSeat()); const heavyDraw = stackFall ? !!stackFall.actorHeavy : !!m.pendingDropHeavy; + /* remote drop → ใช้สีจาก host (actorTheme/pendingDropTheme) เหมือน block ที่วาง → block ห้อย/ตก สีตรง host */ + const themeOverrideDraw = stackFall ? (stackFall.actorTheme || null) : (m.pendingDropTheme || null); const hueHint = (m.layers.length * 47) % 360; ctx.save(); ctx.translate(bcx, bcy); ctx.rotate(fallTiltRad + swingTiltRad); - drawStackBlockSpriteOrHue(ctx, -twWorld * zoom / 2, 0, twWorld * zoom, drawStripPx, seatDraw, heavyDraw, hueHint, { missTint: !!hitMissTint, missOverlay: !!hitMissTint }); + drawStackBlockSpriteOrHue(ctx, -twWorld * zoom / 2, 0, twWorld * zoom, drawStripPx, seatDraw, heavyDraw, hueHint, { missTint: !!hitMissTint, missOverlay: !!hitMissTint, themeOverride: themeOverrideDraw }); ctx.lineWidth = 1; ctx.restore(); } @@ -20399,7 +20471,13 @@ /* MG3 stack — non-host รับลำดับตา+index จาก host (single source of truth) → เลิกแยกตึก/ตาเหลื่อม */ socket.on('stack-state', (d) => { - if (!d || !mapData || !isStack() || !detectiveCaseFillBots || isMePlayHost()) return; + if (!d || !mapData || !isStack() || !detectiveCaseFillBots) return; + /* ได้รับ stack-state = server relay จาก "host จริง" (socket.to ไม่ส่งกลับผู้ส่ง) → เรา "ไม่ใช่ host" + แก้ playHostId ที่ผิด (dual-host: เข้าใจผิดว่าเป็น host แล้ว sim เอง = ลำดับ/สี block หลุดกับ host) */ + if (d.hostId != null && myId != null && String(d.hostId) !== String(myId)) { + if (String(playHostId) !== String(d.hostId)) playHostId = d.hostId; + } + if (isMePlayHost()) return; if (Array.isArray(d.order)) { stackPreviewTurnOrder = d.order.map((e) => ({ kind: e.kind, seat: e.seat, @@ -20410,7 +20488,24 @@ } if (stackPreviewTurnOrder && stackPreviewTurnOrder.length && Number.isFinite(Number(d.idx))) { const n = stackPreviewTurnOrder.length; - stackPreviewTurnIndex = ((Math.floor(Number(d.idx)) % n) + n) % n; + const newIdx = ((Math.floor(Number(d.idx)) % n) + n) % n; + if (newIdx !== stackPreviewTurnIndex) { + stackPreviewTurnIndex = newIdx; + /* ตาเปลี่ยน → refresh "block ที่ห้อย" (pendingDropSeat/สี) ให้ตรงตาปัจจุบัน + เดิมไม่ mark dirty → block ห้อยค้างโชว์สีของตาที่แล้ว = ก่อนปล่อยสีไม่ตรง */ + markStackNextDropVisualDirty(); + } + } + /* sync สี block ที่ "วางแล้ว" จาก host → เขียนทับ layer ที่เพี้ยนค้าง (จากจังหวะ dual-host ต้นเกม) + sync เฉพาะเมื่อจำนวนชั้นตรงกัน (aligned) — กันเขียนผิดตำแหน่งช่วง drop คาบเกี่ยว */ + if (Array.isArray(d.lthemes) && stackMini && Array.isArray(stackMini.layers) + && d.lthemes.length === stackMini.layers.length) { + for (let i = 0; i < stackMini.layers.length; i++) { + const ht = Math.floor(Number(d.lthemes[i])); + if (ht >= 1 && ht <= 8 && stackMini.layers[i] && stackMini.layers[i].theme !== ht) { + stackMini.layers[i].theme = ht; + } + } } logStackSeatColors('client-recv'); }); diff --git a/www/html/Game/public/js/room-lobby.js b/www/html/Game/public/js/room-lobby.js index b36d2b7..f324115 100644 --- a/www/html/Game/public/js/room-lobby.js +++ b/www/html/Game/public/js/room-lobby.js @@ -10,6 +10,21 @@ const nick = (params.get('nick') || '').trim() || 'ผู้เล่น'; const DISPLAY_NAME_STORAGE_KEY = 'roomLobbyDisplayName'; let profileDisplayName = nick; + /* ===== log LobbyA/B → /gamelog (จับ timeline init: connect/join/preload/loading-hide หาเหตุ "กดไม่ได้ต้องรอ") ===== */ + const _lobbyT0 = Date.now(); + function lobbyLogCat() { + try { if (clientLobbyMapId === POST_CASE_LOBBY_SPACE_ID || serverSuspectPhaseActive) return 'LobbyB'; } catch (e) { /* ignore */ } + return 'LobbyA'; + } + function lobbyLog(ev, detail) { + try { + fetch('/gamelog/log.php', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, keepalive: true, + body: JSON.stringify({ cat: lobbyLogCat(), room: (typeof spaceId === 'string' ? spaceId : ''), who: (function () { try { return getProfileDisplayName(); } catch (e) { return nick; } })(), ev: ev || '', detail: (detail || '') + ' [+' + (Date.now() - _lobbyT0) + 'ms]', src: 'client' }), + }).catch(function () {}); + } catch (e) { /* ignore */ } + } + lobbyLog('lobby-open', 'space=' + spaceId); const DESIGN_MENU = typeof designMainMenuHref === 'function' ? designMainMenuHref() : '/Main-Menu/'; if (!spaceId) { location.href = DESIGN_MENU; return; } const roomIdValueEl = document.getElementById('room-id-value'); @@ -510,6 +525,7 @@ var elapsed = Date.now() - roomLoadingShownAt; if (elapsed < ROOM_LOADING_MIN_MS) { setTimeout(hideRoomLoading, ROOM_LOADING_MIN_MS - elapsed); return; } roomLoadingHidden = true; + try { lobbyLog('loading-hide', 'เริ่มเล่นได้'); } catch (e) { /* ignore */ } var ov = document.getElementById('room-loading-overlay'); if (ov) { ov.classList.add('is-hidden'); setTimeout(function () { if (ov.parentNode) ov.parentNode.removeChild(ov); }, 600); } } @@ -3215,6 +3231,7 @@ socket.on('peer-display-name', applyPeerDisplayNameUpdate); socket.on('connect', () => { + lobbyLog('socket-connect', ''); /* ส่งค่าสีที่เลือกใน Main-Lobby ติดไปด้วย — server จะใช้ค่านี้ถ้ายังว่าง (กันโชว์สีแดง default ตอนเข้าใหม่) */ var savedThemeIdx = null; var savedSkinIdx = null; @@ -3245,6 +3262,8 @@ } mapData = res.mapData; roomJoinReady = true; + clientLobbyMapId = res.mapId != null ? res.mapId : null; + lobbyLog('join-ok', 'peers=' + (res.peers || []).length + ' host=' + (res.hostId === socket.id ? 'me' : 'other') + ' map=' + res.mapId + ' bots=' + (res.botSlotCount != null ? res.botSlotCount : '?')); /* ไอคอนกุญแจ 🔒 โชว์เฉพาะห้อง private (จาก res.isPrivate) — public ไม่โชว์ */ try { const lockEl = document.getElementById('room-id-lock'); @@ -7585,14 +7604,22 @@ setTimeout(resizeAndDraw, 100); updateLobbyProfileAvatar(); setupRoomCustomize(); + lobbyLog('preload-start', ''); rlEnsureManifest(function () { + lobbyLog('manifest-done', ''); rlResolveMyColors(function () { + lobbyLog('colors-resolved', ''); updateRoomProfileAvatarTinted(); preloadMyTintedCharacter(function () { + lobbyLog('mychar-tinted', 'roomJoinReady=' + roomJoinReady); + /* พอ tint "ตัวเรา" เสร็จ → เปิดให้เล่นได้เลย (ไม่รอ tint คนอื่นครบ = เดิมกิน ~12 วิ คอขวด) + tint คนอื่นโหลด background → avatar คนอื่นโผล่สีตามทีหลังนิดหน่อย (ยอมรับได้) */ + roomPreloadReady = true; + setRoomLoadingPct(100); + lobbyLog('preload-ready(mychar)', ''); + maybeHideRoomLoading(); preloadAllLobbyTintedOccupants(function () { - roomPreloadReady = true; - setRoomLoadingPct(100); - maybeHideRoomLoading(); + lobbyLog('preload-occupants-done(bg)', ''); }, function (d, t) { setRoomLoadingPct(t ? (d / t) * 100 : 100); }); diff --git a/www/html/Game/public/play.html b/www/html/Game/public/play.html index db0a15c..7f880f1 100644 --- a/www/html/Game/public/play.html +++ b/www/html/Game/public/play.html @@ -5412,7 +5412,7 @@ - +
v —