From d599c5c72d8b595c116cd34909f8ca813d39edc0 Mon Sep 17 00:00:00 2001 From: giteaadmin Date: Sat, 27 Jun 2026 11:28:34 +0000 Subject: [PATCH] on progress minigame 2 --- www/html/Admin/private/store.json | 18 +- www/html/Game/public/js/play.js | 111 ++- www/html/Game/public/js/room-lobby.js | 3 +- www/html/Game/public/play.html | 2 +- www/html/Game/public/room-lobby.html | 2 +- www/html/Game/server.js | 54 +- www/html/gamelog/data/log.jsonl | 537 +++++++++++ www/html/testcase/data/bugs.json | 60 ++ .../data/bugs.json.bak-20260627-disconnect | 872 ++++++++++++++++++ 9 files changed, 1634 insertions(+), 25 deletions(-) create mode 100644 www/html/testcase/data/bugs.json.bak-20260627-disconnect diff --git a/www/html/Admin/private/store.json b/www/html/Admin/private/store.json index 8a9206f..82937fa 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": 2256, + "coins": 2322, "createdAt": "2026-04-02T05:52:21+00:00", - "updatedAt": "2026-06-26T18:19:28+00:00", + "updatedAt": "2026-06-27T11:14:02+00:00", "daily": { "anchorMs": 1781197200000, "claimedDays": [ @@ -67,13 +67,13 @@ ], "lockUntilMs": 0 }, - "score": 192, + "score": 248, "scoreByCase": { "1": 26, "10": 616, "8": 296, "11": 20, - "13": 268, + "13": 324, "14": 30, "9": 40, "12": 408, @@ -663,15 +663,15 @@ "providerUserId": "p_1782016990238_0u37bv7pxlxs", "notes": "auto: player-coins", "blocked": false, - "coins": 510, + "coins": 572, "createdAt": "2026-06-21T04:43:11+00:00", - "updatedAt": "2026-06-26T18:36:24+00:00", + "updatedAt": "2026-06-27T11:14:02+00:00", "lobbyColorThemeIndex": 5, "lobbySkinToneIndex": 1, - "score": 470, + "score": 532, "scoreByCase": { - "13": 130, - "10": 238, + "13": 182, + "10": 248, "6": 8, "8": 20, "2": 46, diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js index e04e823..7aba128 100644 --- a/www/html/Game/public/js/play.js +++ b/www/html/Game/public/js/play.js @@ -1313,6 +1313,10 @@ let gauntletCrownRunwayBgOff = false; let gauntletCrownRunwayBgSpeedPxPerSec = 48; let gauntletCrownRunwayBgScrollPx = 0; + /* G1: scroll ผูกเวลา server (ทุกเครื่องตรงกัน ไม่ขึ้น resolution/frame-rate/host) — scroll = base + speed × (serverNow − anchor) */ + let gauntletCrownRunwayLiveAnchorServerMs = 0; /* เวลาเริ่ม live (server epoch) */ + let gauntletCrownRunwayServerLiveStartMs = 0; /* liveStartMs จาก server (gauntlet-sync) — anchor เดียวกันทุกเครื่อง */ + let gauntletCrownRunwayBaseScrollPx = 0; /* ค่า scroll ตอน snap (resolution-independent เท่ากันทุกเครื่อง) */ let gauntletCrownRunwayBgFinishLatched = false; /** ส่งสัญญาณ "ถึงเส้นชัย" ให้ server แล้วหรือยัง (กันส่งซ้ำ) — server จะหยุด obstacle/collision */ let gauntletFinishPhaseSignaled = false; @@ -1934,12 +1938,28 @@ const raw = mapData.gauntletCrownRunwayBg && typeof mapData.gauntletCrownRunwayBg === 'object' ? mapData.gauntletCrownRunwayBg : {}; const frac = Math.max(0.02, Math.min(0.95, Number(raw.runwaySpawnAlignFrac) || 0.32)); gauntletCrownRunwayBgScrollPx = frac * w0 - anchorWorldX; + /* G1: เก็บ base (ค่า snap นี้ — เท่ากันทุกเครื่อง) + anchor เวลาเริ่ม (server epoch) → scroll คำนวณจากเวลา server */ + gauntletCrownRunwayBaseScrollPx = gauntletCrownRunwayBgScrollPx; + /* anchor = liveStartMs จาก server (gauntlet-sync) — authoritative เดียวกันทุกเครื่อง + สดเสมอ + (เดิมใช้ playMissionLiveBeginsAt ที่ค้างจากเกมก่อน → scroll กระโดด/host-client ไม่ตรง) + fallback: liveBeginsAt ถ้าสด ไม่งั้น serverNow() */ + if (gauntletCrownRunwayServerLiveStartMs > 0) { + gauntletCrownRunwayLiveAnchorServerMs = gauntletCrownRunwayServerLiveStartMs; + } else { + const _lba = (typeof playMissionLiveBeginsAt !== 'undefined') ? Number(playMissionLiveBeginsAt) : 0; + const _nowS = serverNow(); + const _el = _nowS - _lba; + gauntletCrownRunwayLiveAnchorServerMs = (_lba > 0 && _el >= -2000 && _el < 10000) ? _lba : _nowS; + } gauntletCrownRunwaySpawnScrollSnapped = true; } function reloadGauntletCrownRunwayBgFromMap() { gauntletCrownRunwayBgImgs = [null, null, null, null, null]; gauntletCrownRunwayBgScrollPx = 0; + gauntletCrownRunwayLiveAnchorServerMs = 0; /* G1: รีเซ็ต anchor รอบใหม่ */ + gauntletCrownRunwayServerLiveStartMs = 0; + gauntletCrownRunwayBaseScrollPx = 0; gauntletCrownRunwayBgFinishLatched = false; gauntletFinishPhaseSignaled = false; gauntletCrownRunwayWalkActive = false; @@ -2044,17 +2064,27 @@ } catch (_d) { /* ignore */ } return; } - const gcrInc = (gauntletCrownRunwayBgSpeedPxPerSec || 48) * dtSec; - /* ถ้าก้าวถัดไป "จะถึง/เลย" จุดหยุด FINISH → หยุดเป๊ะตรงนั้น (ไม่วิ่งเลย) แล้วเริ่มจบเกมทันที */ + /* G1: scroll = base + speed × (serverNow − anchor) → ทุกเครื่องเลื่อนตรงกัน ไม่ขึ้น resolution/frame-rate/host + (เดิม scrollPx += speed×dt นับ local จากเวลาเริ่มของตัวเอง → เลื่อนไม่ตรงกัน + host หลุด/resize เพี้ยน) + fallback เป็นนับ local ถ้ายังไม่มี anchor (กันค้าง) */ + const gcrSpeed = gauntletCrownRunwayBgSpeedPxPerSec || 48; + let gcrTarget; + if (gauntletCrownRunwayLiveAnchorServerMs > 0) { + gcrTarget = gauntletCrownRunwayBaseScrollPx + gcrSpeed * Math.max(0, (serverNow() - gauntletCrownRunwayLiveAnchorServerMs)) / 1000; + } else { + gcrTarget = gauntletCrownRunwayBgScrollPx + gcrSpeed * dtSec; + } + /* ถ้า "จะถึง/เลย" จุดหยุด FINISH → หยุดเป๊ะตรงนั้น แล้วเริ่มจบเกมทันที */ const gcrLiveStopPx = gauntletCrownRunwayFinishStopScrollPxPlay(); if (gauntletCrownRunwayBgMapEligiblePlay() && gcrLiveStopPx != null && gcrLiveStopPx >= gauntletCrownRunwayBgScrollPx - 0.01 - && gauntletCrownRunwayBgScrollPx + gcrInc >= gcrLiveStopPx) { + && gcrTarget >= gcrLiveStopPx) { gauntletCrownRunwayBgScrollPx = gcrLiveStopPx; gauntletCrownRunwayBeginFinishStopPlay(); return; } - gauntletCrownRunwayBgScrollPx += gcrInc; + /* monotonic — เลื่อนหน้าเท่านั้น (กัน clock jitter ดึงถอย) */ + if (gcrTarget > gauntletCrownRunwayBgScrollPx) gauntletCrownRunwayBgScrollPx = gcrTarget; } /** stack + แมป mnn93hpi — พื้นหลัง intro+loop เลื่อนแนวตั้ง (stackTowerBgScroll ในแมป) */ @@ -3878,13 +3908,34 @@ } /** ช่วยแปลง tile coords → screen pos สำหรับ ghost (ใช้เฉพาะที่ caller ใน render scope) * รับ helpers ที่เป็น scope-local (worldToScreen, zDraw) เพราะถูกเรียกในรอบ draw เท่านั้น */ + /** row ล่างสุดของโซนผี (cache บน mapData) — ใช้ clamp ภาพผีให้อยู่ในโซน */ + function quizGhostZoneMaxRowPlay() { + if (!mapData) return null; + if (mapData._ghostMaxRow !== undefined) return mapData._ghostMaxRow; + const g = mapData.quizGhostArea; + let maxY = null; + if (Array.isArray(g)) { + for (let y = 0; y < g.length; y++) { + const row = g[y] || []; + for (let x = 0; x < row.length; x++) { if (Number(row[x]) === 1) maxY = y; } + } + } + mapData._ghostMaxRow = maxY; + return maxY; + } function _quizGhostScreenAnchorPlay(axTile, ayTile, w2s, zDrawIn) { const md = mapData; const { cw, ch } = getCharacterFootprintWH(md); const cxWorld = (axTile + cw * 0.5) * tileSize; - /* ยกผีขึ้น: anchor ที่ "หัว tile" (ayTile) ไม่ใช่เท้า (ayTile+ch) → ภาพผีตัวใหญ่ลอยอยู่ในโซนผี - ไม่ห้อยลงทับโซนคำตอบ SAFE/SCAM (ตำแหน่งจริงอยู่ในโซน y4-6 แต่เดิมภาพห้อยถึง y7-8) */ - const cyBottomWorld = ayTile * tileSize; + /* anchor เท้าผีตามปกติ (ayTile + ch) แต่ clamp ไม่ให้ต่ำกว่าขอบล่างโซนผี (maxRow + 0.6) + → ภาพผีอยู่ "ในโซนผี" พอดี: ไม่ลอยสูงเกิน (เคส ayTile) และไม่ห้อยลงทับ SAFE/SCAM (เคส ayTile+ch) */ + let bottomTile = ayTile + ch; + const maxRow = quizGhostZoneMaxRowPlay(); + if (maxRow != null) { + const clampBottom = maxRow + 0.6; + if (bottomTile > clampBottom) bottomTile = clampBottom; + } + const cyBottomWorld = bottomTile * tileSize; const [sx, sy] = w2s(cxWorld, cyBottomWorld); /* ความสูง = สูง avatar บนจอ (สอดคล้องกับ drawAvatar: boxH = tileSize * zDraw * ch * 1.35) */ const sizeH = tileSize * zDrawIn * Math.max(ch, 1) * 1.35; @@ -6465,6 +6516,12 @@ zDraw = Math.min(canvas.width / mwPx, canvas.height / mhPx) * 0.96; } else if (isQuizBattle()) { zDraw *= quizBattleUserZoomMul; + } else if (isGauntletCrownHeistMapPlay() && !(previewMode && editorEmbedReturn)) { + /* #1 map fit: gauntlet เกมจริง — fit "ความสูงแมป (ลู่ทั้งหมด)" ให้พอดีจอ resolution-adaptive + (เดิมใช้ zoom ดิบ ไม่ปรับตาม resolution = ไม่พอดี) · fit สูงอย่างเดียว เพราะ runway เลื่อนแนวนอน */ + const mhPx = h * tileSize; + const fitZ = (canvas.height / mhPx) * 0.96; + if (Number.isFinite(fitZ) && fitZ > 0) zDraw = fitZ; } if (isStackTowerEmbedZoomLockedPlay()) { playEmbedUserZoomMul = PLAY_EMBED_STACK_TOWER_FIXED_ZOOM_MUL; @@ -12705,6 +12762,10 @@ if (Number.isFinite(jt)) gauntletRuntimeJumpTicks = Math.max(4, Math.min(40, jt)); const tl = Number(payload.gauntletTimeLimitSec); if (Number.isFinite(tl)) gauntletRuntimeTimeLimitSec = Math.max(0, Math.min(7200, tl)); + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLiveStartMs')) { + const _ls = Number(payload.gauntletLiveStartMs); + gauntletCrownRunwayServerLiveStartMs = (Number.isFinite(_ls) && _ls > 0) ? _ls : 0; /* G1: anchor scroll จาก server (เดียวกันทุกเครื่อง) */ + } if (Object.prototype.hasOwnProperty.call(payload, 'gauntletEndsAt')) { if (payload.gauntletEndsAt == null) gauntletEndsAtMs = null; else { @@ -20778,6 +20839,8 @@ let removed = 0; [...others.keys()].forEach((id) => { if (isPreviewBotId(id)) return; + const oo = others.get(id); + if (oo && oo.isQuizTakeoverBot) return; /* บอทที่แปลงจากคนหลุด — ไม่อยู่ใน peers แล้ว แต่ห้าม prune */ if (myId != null && String(id) === String(myId)) return; if (!live.has(String(id))) { others.delete(id); removed++; } }); @@ -20786,10 +20849,23 @@ } }); - /* บอท MG1 quiz: server เป็นเจ้าของการเดิน (host หลุดก็ไม่หยุด) → รับตำแหน่ง แล้ว lerp ใน tick (o.tx/o.ty) */ + /* คนหลุด MG1 → server แปลงเป็นบอท "ในที่เดิม" (สี/ตำแหน่ง/สถานะเดิม) → client เก็บ avatar ไว้ (ไม่ลบ) แล้วให้ server ขับ */ + socket.on('quiz-player-to-bot', (d) => { + if (!d || d.id == null) return; + const o = others.get(d.id) || others.get(String(d.id)); + if (o) { + o.isQuizTakeoverBot = true; + o.tx = o.x; o.ty = o.y; /* หยุดนิ่งจนกว่า quiz-bot-move ตัวแรกจะมา */ + /* UX: เติม "(บอท)" หลังชื่อเดิม → รู้ว่าคนนี้กลายเป็นบอทแล้ว (กันสับสน) */ + if (o.nickname && String(o.nickname).indexOf('(บอท)') < 0) o.nickname = String(o.nickname).trim() + ' (บอท)'; + } + }); + + /* บอท MG1 quiz: server เป็นเจ้าของการเดิน (host หลุดก็ไม่หยุด) → รับตำแหน่ง แล้ว lerp ใน tick (o.tx/o.ty) + รับทุก quiz (ไม่ผูก detectiveCaseFillBots) → รองรับ takeover bot แม้เริ่มเกมไม่มีบอท */ socket.on('quiz-bot-move', (d) => { if (!d || !Array.isArray(d.bots)) return; - if (!(isQuiz() && detectiveCaseFillBots)) return; + if (!isQuiz()) return; d.bots.forEach((rb) => { if (!rb || typeof rb.id !== 'string') return; const o = others.get(rb.id); @@ -20821,7 +20897,7 @@ 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;' + el.style.cssText = 'position:fixed;left:50%;top:12px;transform:translateX(-50%);z-index:10500;' + '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); @@ -21400,6 +21476,21 @@ } } }); + /* G3 (#4): ตัดผีตัวซ้ำ — gauntlet-sync = roster จริง server-authoritative (ทุก ~80ms) → ลบ peer ที่หายจาก roster + 3 ครั้งติด (~240ms · debounce กัน blip คนเพิ่ง join) · ข้ามบอท + ตัวเราเอง = เร็วกว่า play-roster 1.5s */ + { + const syncLive = new Set(); + data.players.forEach((p) => { if (p && p.id != null) syncLive.add(String(p.id)); }); + [...others.keys()].forEach((id) => { + if (isPreviewBotId(id)) return; + const oo = others.get(id); + if (!oo || oo.isQuizTakeoverBot) return; + if (myId != null && String(id) === String(myId)) return; + if (syncLive.has(String(id))) { oo._notInGSync = 0; return; } + oo._notInGSync = (oo._notInGSync || 0) + 1; + if (oo._notInGSync >= 3) others.delete(id); + }); + } if (playBotsEnabled()) { applyGauntletPreviewBotsAfterSync(); emitGauntletPreviewRowsToServer(); diff --git a/www/html/Game/public/js/room-lobby.js b/www/html/Game/public/js/room-lobby.js index 539993c..4e40b37 100644 --- a/www/html/Game/public/js/room-lobby.js +++ b/www/html/Game/public/js/room-lobby.js @@ -708,7 +708,8 @@ el.id = 'lobby-color-loading'; el.style.cssText = 'position:fixed;inset:0;z-index:99999;display:none;flex-direction:column;align-items:center;' + 'justify-content:center;gap:16px;background:rgba(0,0,0,.9);opacity:0;transition:opacity .16s ease;' - + 'pointer-events:all;font-family:Kanit,system-ui,sans-serif'; + /* pointer-events:none → overlay สี (peer เข้า/เปลี่ยนสี) ไม่บังการกดปุ่ม (เดิม all = re-tint ทุกคนทุก join = กดไม่ได้) */ + + 'pointer-events:none;font-family:Kanit,system-ui,sans-serif'; el.innerHTML = '
กำลังโหลดสีตัวละคร...
' + '
0%
' diff --git a/www/html/Game/public/play.html b/www/html/Game/public/play.html index cff154c..5dd90ec 100644 --- a/www/html/Game/public/play.html +++ b/www/html/Game/public/play.html @@ -5426,7 +5426,7 @@ - +
v —