From 8efb7adedf9affa6f1806fd96878478ba834ad6f Mon Sep 17 00:00:00 2001 From: giteaadmin Date: Sun, 28 Jun 2026 15:04:41 +0000 Subject: [PATCH] update all game still have issue on mg 7 6 a bit --- www/html/Admin/private/store.json | 21 +- www/html/Game/data/game-timing.json | 4 +- www/html/Game/public/js/play.js | 257 ++++- www/html/Game/public/play.html | 2 +- www/html/Game/server.js | 272 ++++- www/html/gamelog/data/log.jsonl | 257 +++++ www/html/testcase/data/bugs.json | 1530 +++++++++++---------------- 7 files changed, 1349 insertions(+), 994 deletions(-) diff --git a/www/html/Admin/private/store.json b/www/html/Admin/private/store.json index a2ec39c..c9ea4ab 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": 2382, + "coins": 2392, "createdAt": "2026-04-02T05:52:21+00:00", - "updatedAt": "2026-06-27T18:23:36+00:00", + "updatedAt": "2026-06-28T14:47:24+00:00", "daily": { "anchorMs": 1781197200000, "claimedDays": [ @@ -67,7 +67,7 @@ ], "lockUntilMs": 0 }, - "score": 308, + "score": 318, "scoreByCase": { "1": 26, "10": 676, @@ -76,7 +76,7 @@ "13": 324, "14": 30, "9": 40, - "12": 408, + "12": 418, "15": 20, "4": 20, "5": 32, @@ -924,18 +924,19 @@ "providerUserId": "p_1782376007927_lpt9m4xge9", "notes": "auto: player-coins", "blocked": false, - "coins": 82, + "coins": 100, "createdAt": "2026-06-25T08:26:49+00:00", - "updatedAt": "2026-06-26T04:11:43+00:00", - "score": 82, + "updatedAt": "2026-06-28T14:53:20+00:00", + "score": 100, "scoreByCase": { - "12": 52, + "12": 60, "8": 20, - "3": 10 + "3": 10, + "2": 10 }, "lbName": "MONE", "achievements": { - "d1_minigame_solver": 9 + "d1_minigame_solver": 10 } }, { diff --git a/www/html/Game/data/game-timing.json b/www/html/Game/data/game-timing.json index 11a05ad..1524c84 100644 --- a/www/html/Game/data/game-timing.json +++ b/www/html/Game/data/game-timing.json @@ -95,9 +95,9 @@ "balloonBossPlayerBalloonFallbackUrl": "/Game/img/MegaVirus/Artboard%209.png", "balloonBossBalloonsPerPlayer": 3, "forcedMinigameKeys": [ - "gauntlet", + "jump_survive", "space_shooter", - "stack" + "balloon_boss" ], "testSpecialCardByMap": {}, "troublesomeForceOffer": false, diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js index 00b0df1..231977d 100644 --- a/www/html/Game/public/js/play.js +++ b/www/html/Game/public/js/play.js @@ -694,6 +694,9 @@ /* MG3 server-authoritative: เปิดเมื่อได้รับ stack-state ที่ srv:true → client เลิก host-sim (ลำดับตา/แกว่ง/ดรอปบอท/จบเกม = server เป็นเจ้าของ; ผู้เล่นส่งเจตนา stack-drop-req) */ let stackServerAuthPlay = false; + /* server-auth swing: extrapolate phase จาก sample ล่าสุดของ server (กัน drift → บล็อกวาปตอนปล่อย) */ + let stackSrvPhaseAnchor = 0; + let stackSrvPhaseAnchorAt = 0; /** ลำดับตา MG3 = ผู้เล่นจริงทุกคน + บอท เรียงตาม id แบบคงที่ → ทุกเครื่องได้ลำดับ/ที่นั่งเดียวกัน (shared tower) (เดิม [human=ตัวเอง seat1 + bots] ต่อเครื่อง → แต่ละเครื่องมองตัวเองเป็นคนเดียว เลยแยกตึกกัน) */ @@ -3596,6 +3599,14 @@ let meGauntletJumpTicks = 0; /** ค่าที่ใช้วาดการยกตัว (เลอร์ปจาก meGauntletJumpTicks ให้โค้งกระโดดไม่กระตุก) */ let meGauntletJumpVis = 0; + /** โค้งกระโดด local ของ "เรา" — เล่นเต็มอาร์ค (ขึ้น-ลง) เสมอเมื่อกดกระโดด + (server เซ็ต jumpTicks=0 ทันทีตอนข้ามสำเร็จ = ตัดอาร์ค + สัญญาณลอยหายพร้อมตอนดัน +1 → ดูเป็น "วาปข้าม") + ระหว่างอาร์คนี้ vis คุมเอง + glide แนวนอนช้า → เห็นเป็นกระโดดข้ามจริง ไม่ใช่วาป */ + let meGauntletJumpAnimStart = 0; + let meGauntletJumpAnimDur = 0; + function isMeGauntletJumpAnimActivePlay() { + return meGauntletJumpAnimStart > 0 && (Date.now() - meGauntletJumpAnimStart) < meGauntletJumpAnimDur; + } /** ซิงก์จากเซิร์ฟเวอร์ (gauntlet-sync / GET /api/game-timing) */ let gauntletRuntimeTickMs = 220; let gauntletRuntimeJumpTicks = 6; @@ -3990,6 +4001,11 @@ /** quiz_carry: epoch ให้หยิบตัวเลือกได้ · epoch ปิดรอบตอบ (ตั้งที่ Admin แท็บหยิบมาวาง) */ let quizCarryOptionRevealAt = 0; let quizCarryAnswerCloseAt = 0; + /** server-auth (เกมจริง): epoch สิ้นสุดนับ 3-2-1 บนโต๊ะ ช่วง read phase (>0 = กำลังโชว์ countdown) */ + let quizCarrySrvReadCountdownEndAt = 0; + /** MG4 server-auth: ไอคอน ✓/✗ ลอยเหนือหัวตอนตอบ (key = entity id → { ok, until }) ทุกจอเห็นพร้อมกัน */ + const quizCarryEntityFeedbackFx = Object.create(null); + const QUIZ_CARRY_FEEDBACK_FX_MS = 1300; /** หมดเวลาตอบรอบนี้แล้ว — โชว์ TIME'S UP ~3s แล้วเรียก quizCarryAfterRoundResolved */ let quizCarryAnswerTimeupAwaitNext = false; let quizCarryAnswerRoundTimeupAutoT = null; @@ -3999,6 +4015,8 @@ let quizCarrySessionLength = 0; let quizCarryRoundsCompleted = 0; let quizCarrySessionEnded = false; + /** MG4 server-auth (detective): กัน schedule กลับ LobbyB ซ้ำเมื่อ quiz-carry-ended มาหลายครั้ง */ + let quizCarryDetectiveReturnNavDone = false; /** หลัง timeup บนโต๊ะ (embed): รอ 5s แล้วโชว์ result-complete / result-gameover */ let quizCarryResultEndTimer = null; /** หลังโชว์ result-end (embed preview): รอ 5s แล้วกลับ room-lobby — ใช้ร่วม quiz_carry + crown */ @@ -9833,7 +9851,7 @@ if (o.balloonBossBossDmg != null) row.bbDmg = o.balloonBossBossDmg; if (o.balloonBossBalloons != null) row.bbBalloons = o.balloonBossBalloons; if (o.balloonBossEliminated != null) row.bbElim = !!o.balloonBossEliminated; - if (o.quizCarryHeld !== undefined) row.qcHeld = o.quizCarryHeld; + if (o.quizCarryHeld !== undefined && !(isQuizCarry() && quizCarryServerAuthActive())) row.qcHeld = o.quizCarryHeld; /* MG1 quiz: สถานะล็อก (ตอบผิด=ผี) + คะแนนบอท → host เป็นเจ้าของ ส่งให้ทุกเครื่องตรงกัน (เดิมไม่ส่งเลย → non-host เห็นบอทล็อก/respawn/คะแนนคนละแบบ) */ if (isQuiz()) { @@ -10897,6 +10915,27 @@ } } + /** MG4 server-auth (detective): จบเซสชัน — flow เหมือน MG1 (result flash 5s → GCM สรุป → Done → LobbyB + การ์ด) */ + function beginQuizCarryServerAuthMissionEndFlow() { + if (quizCarrySessionEnded) return; + quizCarrySessionEnded = true; + quizCarryPregameActive = false; + hideQuizCarryPregameOverlay(); + quizCarryCurrent = null; + quizCarryEmbedPendingQuestion = null; + hideQuizCarryEmbedCountdownOverlay(); + me.quizCarryHeld = null; + others.forEach((o) => { + if (!o) return; + o.quizCarryHeld = null; + o.botPath = []; + }); + hidePlayQuizHud(); + const mission = quizCarryBuildGcmMissionPayload({}); + const anyScore = quizCarryAnyPlayerHasCorrectAnswer(); + beginSimpleResultFlashThenGcmPlay(mission, anyScore ? 'result-complete.png' : 'result-gameover.png'); + } + function showQuizCarrySessionCompleteOverlay() { if (quizCarrySessionEnded) return; quizCarrySessionEnded = true; @@ -11026,7 +11065,7 @@ showQuizCarryToast(ok ? ('ถูกต้อง +' + QUIZ_CARRY_POINTS_PER_CORRECT + ' แต้ม') : 'ผิด — คืนป้ายที่โซนตัวเลือกแล้ว กด F หยิบใหม่ได้', ok); } } catch (e) { /* ignore */ } - socket.emit('quiz-carry-grab'); + socket.emit('quiz-carry-grab', { x: me.x, y: me.y }); } return false; } @@ -11198,8 +11237,15 @@ if (isGauntletCrownHeistMapPlay() && me.gauntletEliminated) return false; const now = Date.now(); if (now - lastGauntletJumpKey < 200) return false; + /* ยังไม่อยู่ที่พื้น (server บอกลอยอยู่ หรือ arc local ยังเล่น) → กระโดดซ้ำไม่ได้ + (server ก็กันด้วย if jumpTicks>0 return แต่ gate ที่ client กันการเริ่ม arc ใหม่ = ไม่เห็นกระโดดต่อเนื่อง) */ + if ((meGauntletJumpTicks || 0) > 0 || isMeGauntletJumpAnimActivePlay()) return false; lastGauntletJumpKey = now; socket.emit('gauntlet-jump'); + /* เริ่มอาร์คกระโดด local ทันที (instant feedback) — ยาวเท่าจังหวะกระโดดของ server (jumpTicks×tickMs) + เพื่อใ��้เห็นโค้งขึ้น-ลงเต็ม แม้ server จะตัด jumpTicks=0 กลางคันตอนข้ามสำเร็จ */ + meGauntletJumpAnimDur = Math.max(360, (gauntletRuntimeJumpTicks || 6) * (gauntletRuntimeTickMs || 220)); + meGauntletJumpAnimStart = now; return true; } @@ -12047,6 +12093,8 @@ stackTowerPost50AnimStartMs = null; stackTowerHeartMinusFx = null; stackServerAuthPlay = false; /* re-arm: server จะเปิดใหม่ผ่าน stack-state(srv) เมื่อรอบใหม่เริ่ม */ + stackSrvPhaseAnchor = 0; + stackSrvPhaseAnchorAt = 0; if (!mapData || !isStack()) return; const w = mapData.width || 20, h = mapData.height || 15; const land = getStackAreaBoundsPlay(mapData.stackLandArea, w, h); @@ -12145,7 +12193,11 @@ } } if (towerLive && stackMini.teamMissesLeft != null) { - stackMini.teamMissesLeft = Math.max(0, stackMini.teamMissesLeft - 1); + if (stackServerAuthPlay && hit.teamMissesLeft != null) { + stackMini.teamMissesLeft = Math.max(0, Math.floor(Number(hit.teamMissesLeft))); + } else { + stackMini.teamMissesLeft = Math.max(0, stackMini.teamMissesLeft - 1); + } } else { stackMini.lives = Math.max(0, stackMini.lives - 1); if (stackMini.lives <= 0) { @@ -12166,6 +12218,13 @@ triggerStackTowerHeartMinusFxPlay(); const remain = stackMini.teamMissesLeft != null ? stackMini.teamMissesLeft : Math.max(0, (stackMini.lives || 0)); mg3TermOnMiss(Number(remain) || 0); + if (stackServerAuthPlay && stackMini.teamMissesLeft != null && stackMini.teamMissesLeft <= 0 && !stackTowerMissionEndedOnce) { + setTimeout(() => { + if (!stackTowerMissionEndedOnce && stackTowerMissionPhase === 'live' && stackMini && stackMini.teamMissesLeft <= 0) { + finishStackTowerMissionLocalPlay('misses_exhausted'); + } + }, 900); + } } } else { const seatLay = layerVisualMeta && layerVisualMeta.seat != null ? layerVisualMeta.seat : getStackDropActorSeat(); @@ -12544,8 +12603,9 @@ stackMini.pendingDropHeavy = !!item.heavy; stackMini.pendingDropTheme = item.theme || null; /* สีจาก host สำหรับบล็อกนี้ (remote drop) */ stackMini.widthTiles = stackBlockWidthTilesForPlay(stackMini.initialWidthTiles, !!item.heavy); - /* snap เฟสเชือกให้ตรงจุดปล่อยของ server → เชือก+บล็อกที่ร่วงตรงกัน (กันบล็อกไม่ตรงเชือก) */ - if (Number.isFinite(item.phase)) stackMini.phase = item.phase; + /* server-auth: ไม่ snap phase — startStackFallAnimation ใช้ landOffsetTiles จาก server เป็นจุดปล่อย + (snap phase ทำให้บล็อกเด้งไปทิศอื่นก่อนร่วง เพราะ phase ฝั่ง client อาจคลาดจาก server) */ + if (!stackServerAuthPlay && Number.isFinite(item.phase)) stackMini.phase = item.phase; return startStackFallAnimation(item.hit, item.actor || { human: true }); } @@ -12630,14 +12690,18 @@ } } else if (stackMini && stackRemoteDropQueue.length && applyNextRemoteStackDropPlay()) { /* ว่าง (ไม่มีบล็อกร่วง/settle) + มีดรอปในคิวจาก host/คนอื่น → apply ทีละอัน (ไม่แกว่งเฟรมนี้) */ - } else if (stackMini) { + } else if (stackMini && (!isStackTowerMissionUiMapPlay() || stackTowerMissionPhase === 'live')) { /* เล่นจริง: แกว่งเฉพาะตาคนที่คุม (เรา หรือ host คุมบอท) แล้ว broadcast เฟสให้คนอื่น mirror; editor preview / เล่นเดี่ยว: แกว่งเองตลอดเหมือนเดิม */ let advancePhase = true; let emitSwing = false; if (stackServerAuthPlay) { - advancePhase = true; /* server-auth: เดินเฟส local ให้ลื่น → reconcile ตอนรับ stack-swing */ - emitSwing = false; /* แต่ไม่ emit — server เป็นเจ้าของเฟส */ + if (!stackFall && !stackMini.settling && stackSrvPhaseAnchorAt > 0) { + const elapsed = (Date.now() - stackSrvPhaseAnchorAt) / 1000; + stackMini.phase = stackSrvPhaseAnchor + elapsed * stackMini.phaseSpeed * Math.PI * 2; + } + advancePhase = false; + emitSwing = false; } else if (detectiveCaseFillBots) { advancePhase = isMyStackTurn() || (isMePlayHost() && isStackCurrentBotTurnPlay()); emitSwing = advancePhase; @@ -13105,6 +13169,8 @@ /** เลอร์ปตำแหน่งตัวละครต่อเฟรม (ยิ่งสูงยิ่งตามเป้าเร็ว) */ const GAUNTLET_VIS_LERP = 0.42; + /** ระหว่างกระโดด (อยู่กลางอากาศ) glide แนวนอนช้าลง — ให้การเลื่อนไปข้างหน้า +1 ช่อง กระจายทั่วโค้งกระโดด ไม่วาปไปข้างหน้าทันที */ + const GAUNTLET_AIR_X_LERP = 0.13; function gauntletObsSmoothstep(t) { t = Math.max(0, Math.min(1, t)); @@ -13274,11 +13340,12 @@ gauntletObsBlendT0 = now; } - function lerpGauntletEntityPos(x, y, tx, ty) { + function lerpGauntletEntityPos(x, y, tx, ty, xLerp) { let nx = x; let ny = y; + const xr = Number.isFinite(xLerp) ? xLerp : GAUNTLET_VIS_LERP; if (tx != null && Number.isFinite(tx)) { - nx += (tx - nx) * GAUNTLET_VIS_LERP; + nx += (tx - nx) * xr; if (Math.abs(tx - nx) < 0.02) nx = tx; } if (ty != null && Number.isFinite(ty)) { @@ -20495,6 +20562,38 @@ if (!Number.isFinite(n) || n <= 0) return 0; return n - (serverNow() - Date.now()); } + /* server-auth: โชว์รูป TIME'S UP บนโต๊ะ (รูปเดียวกับ embed) — ซ่อนเองเมื่อ phase ถัดไปมา (applyQuizCarryPhaseFromServer) */ + function showQuizCarrySrvTimeupOnDesk() { + if (!isQuizCarry()) return; + const el = document.getElementById('quiz-carry-timeup-desk-layer'); + if (!el) return; + const img = document.getElementById('quiz-carry-timeup-txt-img'); + if (img) { img.onerror = null; img.src = BASE + '/img/quiz-carry/timeup-txt.png?v=' + String(Date.now()); } + el.classList.remove('qc-timeup-desk--interactive'); + el.classList.remove('is-hidden'); + el.setAttribute('aria-hidden', 'false'); + try { syncQuizCarryTimeupDeskLayerPosition(); } catch (e) { /* ignore */ } + } + /* server-auth: นับ 3-2-1 บนโต๊ะช่วง read phase (ก่อนเปิดป้ายตัวเลือก) — ใช้ overlay เดียวกับ embed + ขับทุกเฟรมจาก render loop; ซ่อนเองเมื่อถึง optionRevealAt (เข้าเฟส answer) */ + function tickQuizCarrySrvReadCountdown() { + if (!quizCarryServerAuthActive()) { if (quizCarrySrvReadCountdownEndAt > 0) { quizCarrySrvReadCountdownEndAt = 0; try { hideQuizCarryEmbedCountdownOverlay(); } catch (e) { /* ignore */ } } return; } + const ov = document.getElementById('quiz-carry-embed-countdown'); + const numEl = document.getElementById('quiz-carry-embed-countdown-num'); + if (!ov || !numEl) return; + const now = Date.now(); + if (quizCarrySrvReadCountdownEndAt > now) { + const n = Math.max(1, Math.min(3, Math.ceil((quizCarrySrvReadCountdownEndAt - now) / 1000))); + setCountdown321QuestionAssetGraphic(numEl, n); + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + try { syncQuizCarryEmbedCountdownLayout(); } catch (e) { /* ignore */ } + try { showQuizCarryEmbedCountdownDigitsOnlyChrome(); } catch (e) { /* ignore */ } + } else if (quizCarrySrvReadCountdownEndAt > 0) { + quizCarrySrvReadCountdownEndAt = 0; + try { hideQuizCarryEmbedCountdownOverlay(); } catch (e) { /* ignore */ } + } + } function applyQuizCarryPhaseFromServer(p) { if (!p || !isQuizCarry()) return; quizCarryServerAuthPlay = true; @@ -20523,10 +20622,12 @@ if (p.phase === 'answer') { quizCarryOptionRevealAt = quizCarryServerEpochToLocal(p.optionRevealAt) || Date.now(); quizCarryAnswerCloseAt = quizCarryServerEpochToLocal(p.endsAt); + quizCarrySrvReadCountdownEndAt = 0; /* เข้าเฟสตอบ → จบ countdown โต๊ะ */ } else { - /* read: optionRevealAt อยู่อนาคต → ยังหยิบไม่ได้ จนกว่าเฟส answer */ + /* read: optionRevealAt อยู่อนาคต → ยังหยิบไม่ได้ จนกว่าเฟส answer; โชว์ 3-2-1 บนโต๊ะจนถึง optionRevealAt */ quizCarryOptionRevealAt = quizCarryServerEpochToLocal(p.optionRevealAt); quizCarryAnswerCloseAt = 0; + quizCarrySrvReadCountdownEndAt = (quizCarryOptionRevealAt && quizCarryOptionRevealAt > Date.now()) ? quizCarryOptionRevealAt : 0; } me.quizCarryHeld = null; others.forEach((o) => { if (o) { o.quizCarryHeld = null; o.botPath = []; } }); @@ -20539,6 +20640,17 @@ if (!p || !p.srv || !isQuizCarry()) return; applyQuizCarryPhaseFromServer(p); }); + /* ผลถูก/ผิดของ "คนที่ส่งคำตอบ" → โชว์ไอคอน + (ถ้าถูก) score+10 บนแผงคำถาม ให้เจ้าตัวเห็น (server-auth) */ + socket.on('quiz-carry-answer-feedback', (d) => { + if (!d || d.id == null || !isQuizCarry()) return; + const ok = !!d.correct; + /* ทุกคน (คนจริง+บอท) → ลอยไอคอน ✓/✗ เหนือหัว ให้ทุกจอเห็นว่าใครตอบถูก/ผิด */ + quizCarryEntityFeedbackFx[String(d.id)] = { ok, until: Date.now() + QUIZ_CARRY_FEEDBACK_FX_MS }; + /* ตัวเองเพิ่มแผง feedback กลางจอ (ตอบถูก/ผิดของเรา) เหมือนเดิม */ + if (String(d.id) === String(myId)) { + try { showQuizMapQuestionCarryFeedback(ok); } catch (e) { /* ignore */ } + } + }); /* บอท carry: server เป็นเจ้าของการเดิน/หยิบ (BFS) → รับตำแหน่ง+held แล้ว lerp ใน tick (o.tx/o.ty) */ socket.on('quiz-carry-bot-move', (d) => { if (!d || !Array.isArray(d.bots) || !isQuizCarry()) return; @@ -20577,6 +20689,10 @@ if (d.winnerId != null && String(d.winnerId) === String(myId)) { try { showQuizMapQuestionCarryFeedback(true); } catch (e) { /* ignore */ } } + /* หมดเวลา (ไม่มีใครตอบถูก) → โชว์รูป TIME'S UP บนโต๊ะ ~จนข้อถัดไปมา (applyQuizCarryPhaseFrom... ซ่อนให้) */ + if (d.winnerId == null) { + try { showQuizCarrySrvTimeupOnDesk(); } catch (e) { /* ignore */ } + } try { renderPlayQuizScoreboard(playLiveQuizScores); } catch (e) { /* ignore */ } try { syncQuizCarryGrabButton(); } catch (e) { /* ignore */ } }); @@ -20584,10 +20700,19 @@ socket.on('quiz-carry-ended', (d) => { if (!isQuizCarry()) return; quizCarryServerAuthPlay = true; - quizCarrySessionEnded = true; if (d && d.scores && typeof d.scores === 'object') { playLiveQuizScores = {}; for (const id in d.scores) playLiveQuizScores[id] = Math.max(0, Number(d.scores[id]) || 0); + } + /* จบเซสชัน detective: flow เหมือน MG1 — result-complete/gameover → GCM สรุปภารกิจ → กด Done → กลับ LobbyB + การ์ด + (ห้าม finishDetectiveMinigameAndReturnLobby ทันที — จะข้าม flash + สรุปคะแนน) */ + if (isDetectiveMinigamePlay() && !quizCarryDetectiveReturnNavDone) { + quizCarryDetectiveReturnNavDone = true; + try { beginQuizCarryServerAuthMissionEndFlow(); } catch (e) { /* ignore */ } + return; + } + quizCarrySessionEnded = true; + if (d && d.scores && typeof d.scores === 'object') { try { renderPlayQuizScoreboard(playLiveQuizScores); } catch (e) { /* ignore */ } } }); @@ -20753,7 +20878,7 @@ if (rb.bbDmg != null) o.balloonBossBossDmg = rb.bbDmg; if (rb.bbBalloons != null) o.balloonBossBalloons = rb.bbBalloons; if (rb.bbElim != null) o.balloonBossEliminated = !!rb.bbElim; - if (rb.qcHeld !== undefined) o.quizCarryHeld = rb.qcHeld; + if (rb.qcHeld !== undefined && !(isQuizCarry() && quizCarryServerAuthActive())) o.quizCarryHeld = rb.qcHeld; /* MG1 quiz: รับสถานะล็อก + คะแนนบอทจาก host (single source of truth) */ if (rb.qct != null) o.quizCannotTrue = !!rb.qct; if (rb.qcf != null) o.quizCannotFalse = !!rb.qcf; @@ -20780,7 +20905,11 @@ overlap: Number(d.overlap), supportRatio: (d.supportRatio == null ? null : Number(d.supportRatio)), delta: Number(d.delta) || 0, + teamMissesLeft: d.teamMissesLeft != null ? Math.max(0, Math.floor(Number(d.teamMissesLeft))) : null, }; + if (stackServerAuthPlay && d.teamMissesLeft != null && stackMini.teamMissesLeft != null) { + stackMini.teamMissesLeft = Math.max(0, Math.floor(Number(d.teamMissesLeft))); + } stackRemoteDropQueue.push({ hit: hit, seat: Number(d.seat) || 1, heavy: !!d.heavy, theme: parsePlayLobbyThemeIndex(d.theme) || null, /* สีจาก host → freeze บนบล็อกที่วาง (ตรงทุกจอ) */ @@ -20810,12 +20939,9 @@ const ph = Number(d.phase); if (Number.isFinite(ph)) { if (stackServerAuthPlay && !stackFall && !stackMini.settling) { - /* server-auth: ปล่อยเฟส local เดินเองที่ความเร็ว server (ลื่น 60fps) — "ไม่" ดึงกลับเข้าหา sample ที่มาช้า - เพราะ sample จาก server เป็นค่าของอดีต (network latency) → local เดินนำหน้าเสมอ การ chase กลับ = บล็อกสะดุด/ถอยหลังทุก ~60ms (อาการกระตุก) - จึง snap เฉพาะตอน drift ใหญ่จริง (เปลี่ยนตา/ดรอป/หลุด sync) นอกนั้นเดิน local ล้วน */ - let diff = ph - stackMini.phase; - diff = ((diff + Math.PI) % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2) - Math.PI; - if (Math.abs(diff) > Math.PI * 0.6) stackMini.phase = ph; + stackSrvPhaseAnchor = ph; + stackSrvPhaseAnchorAt = Date.now(); + stackMini.phase = ph; } else { stackMini.phase = ph; } @@ -21054,10 +21180,10 @@ (MG6 ยิงอุกาบาต/MG7 ลูกโป่ง ไม่มี sync สีต่อ tick เหมือน gauntlet → เคยตกไป hash) */ const moveTint = playTintFromJoinPeer(data); if (moveTint) o.playTint = moveTint; - if (mapData && isQuizCarry() && data.quizCarryHeld !== undefined) { + if (mapData && isQuizCarry() && data.quizCarryHeld !== undefined && !quizCarryServerAuthActive()) { o.quizCarryHeld = (data.quizCarryHeld == null ? null : data.quizCarryHeld); } - if (mapData && isQuizCarry() && data.quizCarryScore !== undefined && (myId == null || data.id !== myId)) { + if (mapData && isQuizCarry() && data.quizCarryScore !== undefined && !quizCarryServerAuthActive() && (myId == null || data.id !== myId)) { /* คะแนน carry ของคนจริงคนอื่น → ใช้ค่าจากเจ้าตัว (authoritative) ให้ทุกจอตรงกัน (ของเราใช้ค่า local) */ const qcs = Math.max(0, Number(data.quizCarryScore) || 0); playLiveQuizScores[data.id] = qcs; @@ -21124,7 +21250,8 @@ [...others.keys()].forEach((id) => { if (isPreviewBotId(id)) return; const oo = others.get(id); - if (oo && (oo.isQuizTakeoverBot || oo.isGauntletTakeoverBot)) return; /* บอทที่แปลงจากคนหลุด — ไม่อยู่ใน peers แล้ว แต่ห้าม prune */ + /* บอทที่แปลงจากคนหลุด (ทุก MG) — ไม่อยู่ใน peers แล้ว แต่ห้าม prune ไม่งั้น avatar บอท takeover หาย ~1.5s หลังคนออก */ + if (oo && (oo.isQuizTakeoverBot || oo.isGauntletTakeoverBot || oo.isStackTakeoverBot || oo.isQuizCarryTakeoverBot || oo.isJumpSurviveTakeoverBot || oo.isSpaceShooterTakeoverBot || oo.isBalloonBossTakeoverBot)) return; if (myId != null && String(id) === String(myId)) return; if (!live.has(String(id))) { others.delete(id); removed++; } }); @@ -21833,7 +21960,8 @@ if (p.direction) me.direction = p.direction; const jt = Number(p.gauntletJumpTicks); meGauntletJumpTicks = Number.isFinite(jt) ? jt : 0; - meGauntletJumpVis = meGauntletJumpTicks; + /* อย่าทับ vis ระหว่างอาร์ค local — ไม่งั้น server ที่เซ็ต jumpTicks=0 ตอนข้ามจะตัดโค้งกระโดด (วาป) */ + if (!isMeGauntletJumpAnimActivePlay()) meGauntletJumpVis = meGauntletJumpTicks; const sc = Number(p.gauntletScore); const newSc = Number.isFinite(sc) ? Math.max(0, sc) : 0; me.gauntletScore = newSc; @@ -22118,7 +22246,8 @@ me.direction = p.direction || me.direction; const jt = Number(p.gauntletJumpTicks); meGauntletJumpTicks = Number.isFinite(jt) ? jt : 0; - meGauntletJumpVis = meGauntletJumpTicks; + /* อย่าทับ vis ระหว่างอาร์ค local — กันการตัดโค้งกระโดดตอน server ดัน +1 พร้อมเซ็ต jumpTicks=0 */ + if (!isMeGauntletJumpAnimActivePlay()) meGauntletJumpVis = meGauntletJumpTicks; const sc = Number(p.gauntletScore); me.gauntletScore = Number.isFinite(sc) ? Math.max(0, sc) : 0; me.gauntletEliminated = !!p.gauntletEliminated; @@ -25491,6 +25620,41 @@ ctx.restore(); } + /** MG4: ไอคอน ✓ (เขียว) / ✗ (แดง) ลอยเหนือหัวตอนตอบ — float ขึ้น + จางหายช่วงท้าย */ + function drawQuizCarryAnswerBadge(cx, topY, ok, ageMs) { + const remain = QUIZ_CARRY_FEEDBACK_FX_MS - ageMs; + const rise = Math.min(1, ageMs / QUIZ_CARRY_FEEDBACK_FX_MS) * 14; + const rr = Math.max(11, tileSize * zDraw * 0.42); + const cy = topY - rr - 8 - rise; + const alpha = remain < 300 ? Math.max(0, remain / 300) : 1; + ctx.save(); + ctx.globalAlpha = alpha; + ctx.beginPath(); + ctx.arc(cx, cy, rr, 0, Math.PI * 2); + ctx.fillStyle = ok ? 'rgba(34,197,94,0.96)' : 'rgba(239,68,68,0.96)'; + ctx.fill(); + ctx.lineWidth = Math.max(2, rr * 0.14); + ctx.strokeStyle = 'rgba(255,255,255,0.95)'; + ctx.stroke(); + ctx.lineWidth = Math.max(2.2, rr * 0.18); + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.strokeStyle = '#ffffff'; + ctx.beginPath(); + if (ok) { + ctx.moveTo(cx - rr * 0.42, cy + rr * 0.04); + ctx.lineTo(cx - rr * 0.10, cy + rr * 0.36); + ctx.lineTo(cx + rr * 0.46, cy - rr * 0.34); + } else { + ctx.moveTo(cx - rr * 0.32, cy - rr * 0.32); + ctx.lineTo(cx + rr * 0.32, cy + rr * 0.32); + ctx.moveTo(cx + rr * 0.32, cy - rr * 0.32); + ctx.lineTo(cx - rr * 0.32, cy + rr * 0.32); + } + ctx.stroke(); + ctx.restore(); + } + /** ป้ายติดตัว — กึ่งกลางลำตัว (ทับอก/เอว) แบบเดียวกับป้ายบนพื้น ไม่มีเส้นเชื่อม */ function drawQuizCarryHeldSignBoard(sxFeet, feetSy, halfSpriteW, spriteTopY, signText, _faceDir, choiceIndex, imageUrl, imageElementOpt) { const imgUrlSan = sanitizeQuizCarryImageUrlClient(imageUrl); @@ -25562,7 +25726,7 @@ ctx.restore(); } - function drawAvatar(ax, ay, isMe, name, characterId, direction, isWalking, playTint, gauntletAirTicks, gauntletScoreLabel, quizCarrySignPayload, crownPenaltyFxUntil) { + function drawAvatar(ax, ay, isMe, name, characterId, direction, isWalking, playTint, gauntletAirTicks, gauntletScoreLabel, quizCarrySignPayload, crownPenaltyFxUntil, entIdForFx) { const { cw, ch } = getCharacterFootprintWH(mapData); const air = Number(gauntletAirTicks) || 0; let liftWorldY = 0; @@ -25664,6 +25828,14 @@ quizCarrySignPayload.imageElement, ); } + if (isQuizCarry() && entIdForFx != null) { + const fx = quizCarryEntityFeedbackFx[String(entIdForFx)]; + if (fx) { + const age = Date.now() - (fx.until - QUIZ_CARRY_FEEDBACK_FX_MS); + if (age >= 0 && age < QUIZ_CARRY_FEEDBACK_FX_MS) drawQuizCarryAnswerBadge(sx, spriteTopY, fx.ok, age); + else delete quizCarryEntityFeedbackFx[String(entIdForFx)]; + } + } } function quizCarrySignForEntity(ent) { if (!isQuizCarry() || !quizCarryCurrent || !ent || ent.quizCarryHeld == null) return null; @@ -25782,7 +25954,8 @@ drawAvatar(axO, ayO, false, peerName, o.characterId, faceDirOther, otherWalk, ot, (o.gauntletJumpVis != null ? o.gauntletJumpVis : o.gauntletJumpTicks) || 0, (isGauntletCrownHeistMapPlay() && gauntletCrownPregamePhase === 'live') ? (o.gauntletScore || 0) : undefined, quizCarrySignForEntity(o), - (isGauntletCrownHeistMapPlay() && gauntletCrownPregamePhase === 'live') ? (o.gauntletCrownPenaltyFxUntil || 0) : 0); + (isGauntletCrownHeistMapPlay() && gauntletCrownPregamePhase === 'live') ? (o.gauntletCrownPenaltyFxUntil || 0) : 0, + id); } if (botOut) ctx.restore(); } else { @@ -25800,7 +25973,8 @@ drawAvatar(axMe, ayMe, true, me.nickname + meTag, me.characterId, faceDirMe, meWalking, mt, meGauntletJumpVis, (isGauntletCrownHeistMapPlay() && gauntletCrownPregamePhase === 'live') ? (me.gauntletScore || 0) : undefined, quizCarrySignForEntity(me), - (isGauntletCrownHeistMapPlay() && gauntletCrownPregamePhase === 'live') ? (me.gauntletCrownPenaltyFxUntil || 0) : 0); + (isGauntletCrownHeistMapPlay() && gauntletCrownPregamePhase === 'live') ? (me.gauntletCrownPenaltyFxUntil || 0) : 0, + myId); } } }); @@ -26089,9 +26263,12 @@ function buildPlayMovePayload() { const mv = { x: me.x, y: me.y, direction: me.direction }; if (isQuizCarry()) { - mv.quizCarryHeld = (me.quizCarryHeld == null ? null : me.quizCarryHeld); - /* แนบคะแนนของเรา → คนอื่นเห็นคะแนนคนจริงตรงกัน (เดิมคนอื่นเห็นคะแนนเรา = 0/ค้าง) */ - mv.quizCarryScore = Math.max(0, Number(playLiveQuizScores[myId]) || 0); + /* server-auth: held/คะแนน มาจาก quiz-carry-held / quiz-carry-result เท่านั้น — ห้ามแนบใน move + (เดิมแนบ quizCarryHeld local → user-move ทับ state ที่ server broadcast แล้ว = คนอื่นเห็นหยิบแล้วเด้งกลับ) */ + if (!quizCarryServerAuthActive()) { + mv.quizCarryHeld = (me.quizCarryHeld == null ? null : me.quizCarryHeld); + mv.quizCarryScore = Math.max(0, Number(playLiveQuizScores[myId]) || 0); + } } return mv; } @@ -26207,13 +26384,24 @@ }; /* เฟสเดินเข้าเส้นชัย/freeze → บังคับไม่มีท่ากระโดด (ทุกคนเดินเข้าเส้น ไม่ค้างลอย) */ const gauntletFinishWalkPose = gauntletCrownRunwayWalkActive || gauntletCrownRunwayBgFinishLatched; - if (gauntletFinishWalkPose) { meGauntletJumpTicks = 0; meGauntletJumpVis = 0; } - else meGauntletJumpVis = stepGauntletJumpVis(meGauntletJumpVis, meGauntletJumpTicks); - const mp = lerpGauntletEntityPos(me.x, me.y, me.tx, me.ty); + if (gauntletFinishWalkPose) { meGauntletJumpTicks = 0; meGauntletJumpVis = 0; meGauntletJumpAnimStart = 0; } + else if (isMeGauntletJumpAnimActivePlay()) { + /* อาร์ค local: prog 0→1 ; vis = max·(1−prog) → lift = sin(π·prog) ขึ้น-ลงเต็มโค้งเสมอ (ไม่ถูกตัดตอนข้ามสำเร็จ) */ + const prog = Math.max(0, Math.min(1, (Date.now() - meGauntletJumpAnimStart) / meGauntletJumpAnimDur)); + meGauntletJumpVis = (gauntletRuntimeJumpTicks || 6) * (1 - prog); + } else { + if (meGauntletJumpAnimStart > 0) meGauntletJumpAnimStart = 0; + meGauntletJumpVis = stepGauntletJumpVis(meGauntletJumpVis, meGauntletJumpTicks); + } + /* กระโดดข้าม: server ดันไปข้างหน้า +1 ช่องทันที — ถ้า lerp x เร็ว (0.42) จะวิ่งไปข้างหน้าหมดใน ~80ms + แล้วลอยค้างต่ออีกตลอด arc = ดู "วาปข้าม" ไม่เหมือนกระโดด → ตอนลอยอยู่ glide x ช้าลง ให้ค่อยๆ ไปข้างหน้าพร้อมโค้งกระโดด */ + const meAir = (meGauntletJumpVis || 0) > 0.08; + const mp = lerpGauntletEntityPos(me.x, me.y, me.tx, me.ty, meAir ? GAUNTLET_AIR_X_LERP : GAUNTLET_VIS_LERP); me.x = mp.nx; me.y = mp.ny; others.forEach((o) => { - const op = lerpGauntletEntityPos(o.x, o.y, o.tx, o.ty); + const oAir = (o.gauntletJumpVis != null ? o.gauntletJumpVis : o.gauntletJumpTicks || 0) > 0.08; + const op = lerpGauntletEntityPos(o.x, o.y, o.tx, o.ty, oAir ? GAUNTLET_AIR_X_LERP : GAUNTLET_VIS_LERP); o.x = op.nx; o.y = op.ny; if (gauntletFinishWalkPose) { o.gauntletJumpTicks = 0; o.gauntletJumpVis = 0; return; } @@ -26271,6 +26459,7 @@ } } tickQuizCarryEmbedCountdown(); + tickQuizCarrySrvReadCountdown(); tickQuizCarryRoundTimers(); updateQuizCarryCarryPhaseHud(); stepPreviewBots(); diff --git a/www/html/Game/public/play.html b/www/html/Game/public/play.html index f26fccb..2c7b6fe 100644 --- a/www/html/Game/public/play.html +++ b/www/html/Game/public/play.html @@ -5429,7 +5429,7 @@ - +
v —