From 8f94f6e54b1640e457b32d6b535319e55bcd1043 Mon Sep 17 00:00:00 2001 From: giteaadmin Date: Tue, 9 Jun 2026 09:05:48 +0000 Subject: [PATCH] minigame 2 update design --- .../Game/public/img/gauntlet-assets/score.png | Bin 5660 -> 4904 bytes www/html/Game/public/js/play.js | 228 +++++++++++++-- www/html/Game/public/play.html | 261 ++++++++++++++++-- 3 files changed, 437 insertions(+), 52 deletions(-) diff --git a/www/html/Game/public/img/gauntlet-assets/score.png b/www/html/Game/public/img/gauntlet-assets/score.png index 7d629c9afd7016e0e59ce70f3a6cd3278367d997..db271f31b54ea0e2c373c415c1e83d5e3ba5521c 100644 GIT binary patch delta 4891 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 diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js index 4a6509d..0311eff 100644 --- a/www/html/Game/public/js/play.js +++ b/www/html/Game/public/js/play.js @@ -960,6 +960,7 @@ return alignWorld; } + /** * คอลัมน์บนกริดที่เส้น FINISH ต้องแนบ (map world px) * gauntletCrownRunwayBg.finishAlignMapTileX | finishAlignMapTileFrac — ถ้าไม่ตั้ง ใช้ตำแหน่งตัวนำ @@ -9589,7 +9590,7 @@ if (mg2Jump) { const hud = document.getElementById('play-cyber-hud'); if (hud && hud.classList.contains('play-cyber-hud--quiz-tf-mock')) updateQuizTfMockScale(hud); - else syncMg2MockJumpButtonBoundsPlay(); + syncMg2MockJumpButtonBoundsPlay(); } else { btn.classList.remove('mg2-jump'); } @@ -11134,13 +11135,80 @@ const jm = Math.max(4, jumpTicksMax || 16); const a = Math.max(0, Math.min(jm, Number(air) || 0)); const t = a / jm; - const peakAt = 0.28; - if (t >= peakAt) { - const span = 1 - peakAt; - const u = span > 0 ? (t - peakAt) / span : 0; - return Math.max(0, 1 - u * u * 1.2); + /* อาร์คกระโดดนุ่มแบบ design (rn-jump): sine โค้งสมมาตร ease-in-out peak กลางอากาศ + เดิม: ขาขึ้น quadratic / ขาลง linear (อสมมาตร peak ~72% เวลา → ขาลงกระตุก) */ + const p = Math.max(0, Math.min(1, 1 - t)); /* 0 = เริ่มกระโดด, 1 = แตะพื้น */ + return Math.sin(Math.PI * p); + } + + /* ===== Minigame-2: ชื่อ+คะแนนใต้ตัวละคร + เลข -10 เป็น DOM overlay (sync world→screen) แทนวาดบน canvas ===== + เรียกทุกเฟรมจาก draw() (ส่ง worldToScreen/zDraw/tileSize เข้ามา) — container map buffer→display ด้วย transform:scale */ + const gauntletDomLabelPool = new Map(); /* id -> .gnt-label (pool ไว้ reuse) */ + const gauntletPenaltySeenById = new Map(); /* id -> ts penalty ล่าสุด (กัน spawn float ซ้ำ) */ + function syncGauntletNameDom(worldToScreenFn, zDrawVal, tileSizePx) { + const ov = document.getElementById('gauntlet-name-overlay'); + if (!ov) return; + const live = isGauntletCrownHeistMapPlay() && gauntletCrownPregamePhase === 'live'; + if (!live || typeof worldToScreenFn !== 'function') { + if (!ov.classList.contains('is-hidden')) { + ov.classList.add('is-hidden'); + gauntletDomLabelPool.forEach((el) => { if (el.parentNode) el.parentNode.removeChild(el); }); + gauntletDomLabelPool.clear(); + } + return; } - return Math.min(1, t / peakAt); + ov.classList.remove('is-hidden'); + const canvas = document.getElementById('game-canvas'); + const stage = document.getElementById('play-canvas-stage'); + if (!canvas || !stage) return; + const cr = canvas.getBoundingClientRect(); + const srect = stage.getBoundingClientRect(); + const dispScale = canvas.width > 0 ? cr.width / canvas.width : 1; + ov.style.left = Math.round(cr.left - srect.left) + 'px'; + ov.style.top = Math.round(cr.top - srect.top) + 'px'; + ov.style.width = canvas.width + 'px'; + ov.style.height = canvas.height + 'px'; + ov.style.transform = 'scale(' + dispScale + ')'; + const fp = (typeof getCharacterFootprintWH === 'function') ? getCharacterFootprintWH(mapData) : { cw: 1, ch: 1 }; + const cwv = fp.cw || 1, chv = fp.ch || 1; + function v(n) { const x = Number(n); return Number.isFinite(x) ? x : 0; } + const seen = new Set(); + function place(id, ent, isMe) { + if (!ent) return; + if (typeof shouldGauntletCrownHeistSkipAvatarDrawPlay === 'function' + && shouldGauntletCrownHeistSkipAvatarDrawPlay(ent, isMe, 0, 0)) return; + seen.add(id); + const air = isMe ? (meGauntletJumpVis || 0) : ((ent.gauntletJumpVis != null ? ent.gauntletJumpVis : ent.gauntletJumpTicks) || 0); + const liftWorldY = air > 0 ? gauntletLiftHeightNorm(air, gauntletRuntimeJumpTicks) * tileSizePx * 0.52 : 0; + const cxWorld = (v(ent.x) + cwv * 0.5) * tileSizePx; + const cyBottomWorld = (v(ent.y) + chv) * tileSizePx - liftWorldY; + const scr = worldToScreenFn(cxWorld, cyBottomWorld); + const sx = scr[0], sy = scr[1]; + /* ชื่อ/คะแนนใต้ตัวละคร: ตัดออกตาม mock — mock โชว์ชื่อ+คะแนนเฉพาะ "แถบคะแนนบน" เท่านั้น ไม่มีใต้ตัวละคร + (กันดูเหมือน 2 layer ทับกัน) — คงไว้เฉพาะ -10 penalty float ที่ mock มี */ + const pen = Number(ent.gauntletCrownPenaltyFxUntil) || 0; + /* -10 float ต้องอยู่ "ใต้แถบคะแนนบน" เท่านั้น — กันผู้เล่นที่จุดเริ่ม (project ใกล้แถบคะแนน เช่น AFK) ทำ float โผล่ทับ SCORE label */ + var sbEl = document.querySelector('.play-cyber-scoreboard'); + var sbBottomBuf = sbEl ? ((sbEl.getBoundingClientRect().bottom - cr.top) / (dispScale || 1) + 6) : 0; + if (pen > (gauntletPenaltySeenById.get(id) || 0) && (sy - 28) > sbBottomBuf) { + gauntletPenaltySeenById.set(id, pen); + /* -10 float = design asset score-.png (รูป "-10" สำเร็จรูป) ไม่ใช่ text สไตล์เอง */ + const pf = document.createElement('img'); + pf.className = 'gnt-pen'; + pf.src = GAUNTLET_CROWN_SCORE_POP_URL; + pf.alt = '-10'; + pf.decoding = 'async'; + pf.style.left = Math.round(sx) + 'px'; + pf.style.top = Math.round(sy - 28) + 'px'; + ov.appendChild(pf); + setTimeout(function () { if (pf.parentNode) pf.parentNode.removeChild(pf); }, 900); + } else if (pen > (gauntletPenaltySeenById.get(id) || 0)) { + /* อยู่บริเวณแถบคะแนน — บันทึกว่าเห็นแล้ว ไม่ต้องสร้าง float (กัน spawn ซ้ำตอนเลื่อนลงมา) */ + gauntletPenaltySeenById.set(id, pen); + } + } + if (others && typeof others.forEach === 'function') others.forEach((o, id) => place(id, o, false)); + place('__me', me, true); } function getGauntletObsDrawPositionsAt(nowMs) { @@ -11454,6 +11522,42 @@ if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-ready', { ready: true }); } + /** Minigame-2 mno9kb07 — HOW TO PLAY ใช้ layout mock 01-howto (canvas = mg-bg) */ + function gauntletCrownHowtoUsesMg2LayoutPlay() { + return !!isGauntletCrownHeistMapPlay(); + } + function getGauntletCrownHowtoStatusEl() { + if (gauntletCrownHowtoUsesMg2LayoutPlay()) { + return document.getElementById('gauntlet-crown-howto-status-mg2'); + } + return document.getElementById('gauntlet-crown-howto-status'); + } + function getGauntletCrownHowtoBtnEl() { + if (gauntletCrownHowtoUsesMg2LayoutPlay()) { + return document.getElementById('btn-gch-ready-mg2'); + } + return document.getElementById('btn-gch-ready'); + } + function setPlayMg2HowtoLivePlay(on) { + document.documentElement.classList.toggle('play-mg2-howto-live', !!on); + } + function clearGchMg2OverlayBoundsPlay(ov) { + ov = ov || document.getElementById('gauntlet-crown-howto-overlay'); + if (!ov) return; + ['position', 'left', 'top', 'width', 'height', 'right', 'bottom', 'transform'].forEach((p) => { + ov.style.removeProperty(p); + }); + } + function dismissGauntletCrownHowtoOverlayPlay() { + const howto = document.getElementById('gauntlet-crown-howto-overlay'); + if (howto) { + howto.classList.add('is-hidden'); + setPlayMg2HowtoLivePlay(false); + clearGchMg2OverlayBoundsPlay(howto); + } + gauntletCrownHowtoVisible = false; + } + /** ภารกิจคำถาม mng8a80o — Ready Status / ปุ่ม แบบเดียวกับ Mega Virus (gauntlet-crown-lobby-sync) */ function updateQuizQuestionMissionHowtoHud() { if (!isQuizQuestionMissionUiMapPlay() || quizQuestionMissionPhase !== 'howto') return; @@ -11476,8 +11580,8 @@ } function updateGauntletCrownHowtoHud() { - const st = document.getElementById('gauntlet-crown-howto-status'); - const btn = document.getElementById('btn-gch-ready'); + const st = getGauntletCrownHowtoStatusEl(); + const btn = getGauntletCrownHowtoBtnEl(); if (!btn || !usesCrownLobbyShellPlay() || gauntletCrownPregamePhase !== 'howto') return; const humans = quizCarryPregameHumanIds(); const tot = Math.max(1, quizCarryPregameTotalPlayers()); @@ -11488,6 +11592,15 @@ } const humansReady = humans.length > 0 && humans.every((id) => !!gauntletCrownLobbyReadyMap[id]); btn.classList.toggle('is-start-phase', humansReady); + if (gauntletCrownHowtoUsesMg2LayoutPlay()) { + const btnImg = document.getElementById('btn-gch-ready-mg2-img'); + if (btnImg) { + btnImg.src = humansReady + ? (missionMockHudAssetUrl('btn-start.png') || (BASE + '/img/gauntlet-assets/btn-start.png')) + : (missionMockHudAssetUrl('btn-ready.png') || (BASE + '/img/gauntlet-assets/btn-ready.png')); + btnImg.alt = humansReady ? 'START' : 'READY'; + } + } btn.classList.toggle('is-read-only', !isMePlayHost()); btn.disabled = !isMePlayHost(); btn.setAttribute('aria-pressed', humansReady ? 'false' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'true' : 'false')); @@ -11507,9 +11620,7 @@ applyGauntletPreviewSpawnLayout(false); } trySnapGauntletCrownRunwayScrollToSpawnsPlay(); - const howto = document.getElementById('gauntlet-crown-howto-overlay'); - if (howto) howto.classList.add('is-hidden'); - gauntletCrownHowtoVisible = false; + dismissGauntletCrownHowtoOverlayPlay(); const cd = document.getElementById('gauntlet-crown-countdown'); const numEl = document.getElementById('gauntlet-crown-countdown-num'); function applyMegaBalloonLiveStartPlay() { @@ -11600,6 +11711,11 @@ if (!ov) return; gauntletCrownHowtoVisible = true; ov.classList.remove('is-hidden'); + if (isGauntletCrownHeistMapPlay()) { + setPlayMg2HowtoLivePlay(true); + updateGchTfMockScale(ov); + trySnapGauntletCrownRunwayScrollToSpawnsPlay(); + } if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-sync-request'); gauntletCrownSyncGuestReadyIfNeeded(); updateGauntletCrownHowtoHud(); @@ -11683,7 +11799,7 @@ if (nick !== 0) return nick; return String(a.id).localeCompare(String(b.id)); }); - const top = baseRows.slice(0, 5); + const top = baseRows.slice(0, 6); /* Last Light รองรับ 6 ผู้เล่น — summary ต้องครบ 6 */ const ranked = top.map((row, idx) => { const pos = idx + 1; const rankBonus = gauntletCrownRankBonusLocal(pos); @@ -11757,6 +11873,17 @@ eliminated: !!o.gauntletEliminated, }); }); + /* รวมตัวเรา (human) ถ้ายังไม่อยู่ — บาง mission (preview/จาก server) มีแต่บอท ทำให้ตัวเราตกหายจาก summary */ + if (myId != null && !seen.has(String(myId))) { + seen.add(String(myId)); + baseRows.push({ + id: myId, + nickname: (me.nickname || nick || 'คุณ').trim() || 'คุณ', + characterId: me.characterId ?? null, + baseScore: me.gauntletEliminated ? 0 : Math.max(0, Number(me.gauntletScore) | 0), + eliminated: !!me.gauntletEliminated, + }); + } baseRows.sort((a, b) => { if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; @@ -11764,7 +11891,7 @@ if (nick !== 0) return nick; return String(a.id).localeCompare(String(b.id)); }); - const top = baseRows.slice(0, 5); + const top = baseRows.slice(0, 6); /* Last Light รองรับ 6 ผู้เล่น — summary ต้องครบ 6 รวมตัวเรา (เดิม 5 ตัดคนคะแนนต่ำสุด/ตายออก) */ const ranked = top.map((row, idx) => { const pos = idx + 1; const rankBonus = gauntletCrownRankBonusLocal(pos); @@ -19075,6 +19202,10 @@ /* ตัวละครที่ไม่มีไฟล์เลเยอร์สี (เช่น defaultChar) — compose คืน Image ดิบ → crop หน้าจากรูปดิบตรง ๆ กันช่องว่าง */ let drawSrc = srcCanvas; if (!drawSrc && raw && raw.complete && raw.naturalWidth > 0 && raw.naturalHeight > 0) { + /* ตัวที่มีไฟล์เลเยอร์สี (tintable) แต่ composite ยังไม่พร้อม หรือ API characters ยังโหลดไม่เสร็จ → + คืน '' ให้ applyCyberHudScoreAvatarImgWithRetry retry รอเลเยอร์โหลด (avatar bot จะไม่ถูกรับจนกว่าจะเป็น data URL ที่ย้อมสีแล้ว) + อย่า fallback raw ที่ "ไม่มีสี" ไม่งั้น avatar ทุกคนเหมือนกัน = "สีไม่ขึ้น"; ใช้ raw เฉพาะตัวที่ API ยืนยันว่าไม่มีเลเยอร์ (defaultChar) */ + if (playCharLayerApi.status !== 'done' || playCharLayerFromApi(cid) === true) return ''; drawSrc = raw; } if (!drawSrc) return ''; @@ -19336,6 +19467,7 @@ if (Math.abs(s - _quizTfScaleLast) < 0.0005 && root.dataset.qtfStackW === String(Math.round(cw)) && root.dataset.qtfStackH === String(Math.round(ch))) { + syncMg2MockJumpButtonBoundsPlay(r, s); return; } _quizTfScaleLast = s; @@ -19406,10 +19538,33 @@ const s = missionMockOverlayScalePlay(); if (s > 0 && isFinite(s)) ov.style.setProperty('--gcm-tf-scale', String(s)); } - /* popup HOW TO PLAY (01-howto) — scale stage 1920×1080 ตามสูตรเดียวกัน */ + /* popup HOW TO PLAY (01-howto) — scale stage 1920×1080 · MG2 ยึด #play-canvas-stack เหมือน HUD */ function updateGchTfMockScale(ov) { ov = ov || document.getElementById('gauntlet-crown-howto-overlay'); if (!ov) return; + const mg2 = ov.classList.contains('gch-mg2-mock'); + if (mg2) { + const stack = document.getElementById('play-canvas-stack'); + if (!stack) return; + const r = stack.getBoundingClientRect(); + const cw = Math.max(1, r.width || stack.clientWidth || 0); + const ch = Math.max(1, r.height || stack.clientHeight || 0); + const s = Math.min(cw / 1920, ch / 1080); + if (!(s > 0 && isFinite(s))) return; + ov.style.position = 'fixed'; + ov.style.left = Math.round(r.left) + 'px'; + ov.style.top = Math.round(r.top) + 'px'; + ov.style.width = Math.round(cw) + 'px'; + ov.style.height = Math.round(ch) + 'px'; + ov.style.right = 'auto'; + ov.style.bottom = 'auto'; + ov.style.transform = 'none'; + ov.style.setProperty('--gch-tf-scale', String(s)); + const stage = ov.querySelector('.gch-mg2-stage'); + if (stage) stage.setAttribute('aria-hidden', 'false'); + return; + } + clearGchMg2OverlayBoundsPlay(ov); const s = missionMockOverlayScalePlay(); if (s > 0 && isFinite(s)) ov.style.setProperty('--gch-tf-scale', String(s)); } @@ -19503,7 +19658,8 @@ /** แถบ SCORE แนวนอน (mno9kb07) — ใช้รูปแบบเดียวกับ quiz_carry / พรีวิว editor */ root.classList.toggle( 'play-cyber-hud--gauntlet-crown-strip', - !!(on && (isGauntletCrownHeistMapPlay() || isQuizCarry())), + !!(on && (isGauntletCrownHeistMapPlay() || isQuizCarry()) + && !(missionMockOn && getMissionMockHudSkinPlay() === 'gauntlet_crown')), ); root.classList.toggle('play-cyber-hud--score-flush-left', !!(on && isQuizCarry())); root.classList.toggle('play-cyber-hud--jump-mission-hide-score', !!isJumpSurviveMissionUiMapPlay()); @@ -19895,7 +20051,9 @@ return a.y - b.y; }); - const cap = stripScoreHud ? (isQuizCarry() ? 8 : 6) : 8; + /* Minigame-2 รองรับสูงสุด 6 ผู้เล่น (GAUNTLET_MAX_PLAYERS) — แถบคะแนนต้องโชว์ครบ 6 รวมตัวเรา + (เดิม cap 5 ตาม mockup ที่วาดตัวอย่าง 5 คน ทำให้ตัวเราคะแนนต่ำสุดถูกตัดออก) */ + const cap = mg2MockHud ? 6 : (stripScoreHud ? (isQuizCarry() ? 8 : 6) : 8); let hudRows = rows.slice(0, Math.min(cap, rows.length)); if (stripScoreHud) { const mi = hudRows.findIndex((row) => row.isMe); @@ -20719,11 +20877,17 @@ ctx.font = '10px sans-serif'; ctx.textAlign = 'center'; const nameBase = name || ''; + /* gauntlet (Last Light) live: ชื่อ/คะแนน/-10 ใช้ DOM overlay (syncGauntletNameDom) — ไม่วาดบน canvas */ + /* crown map (Minigame-2): ไม่วาดชื่อ/คะแนน/-10 ใต้ตัวละครบน canvas "ทุกเฟส" (ตรง mock — โชว์เฉพาะแถบคะแนนบน) + เดิมเช็คเฉพาะ phase==='live' → ตอน summary/howto canvas วาดชื่อกลับมา ทับแถบคะแนน (ดูเหมือน avatar ซ้อน) */ + const gntDomLabel = isGauntletCrownHeistMapPlay(); const withScore = isGauntlet() && typeof gauntletScoreLabel === 'number' && gauntletScoreLabel >= 0 ? (nameBase + ' · ' + gauntletScoreLabel) : nameBase; - ctx.fillText(withScore, sx, sy + 10); - if (isGauntletCrownHeistMapPlay() + if (!gntDomLabel) ctx.fillText(withScore, sx, sy + 10); + if (!gntDomLabel + && isGauntletCrownHeistMapPlay() + && gauntletCrownPregamePhase === 'live' && typeof crownPenaltyFxUntil === 'number' && crownPenaltyFxUntil > timeMs) { const penImg = ensureGauntletCrownScorePenaltyImgPlay(); @@ -20847,7 +21011,10 @@ const ga = _quizGhostScreenAnchorPlay(axO, ayO, worldToScreen, zDraw); drawQuizWrongGhostOverlayPlay(ga.sx, ga.sy, ga.sizeH, id, peerName); } else { - drawAvatar(axO, ayO, false, peerName, o.characterId, faceDirOther, otherWalk, ot, (o.gauntletJumpVis != null ? o.gauntletJumpVis : o.gauntletJumpTicks) || 0, o.gauntletScore || 0, quizCarrySignForEntity(o), isGauntletCrownHeistMapPlay() ? (o.gauntletCrownPenaltyFxUntil || 0) : 0); + 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); } if (botOut) ctx.restore(); } else { @@ -20863,11 +21030,16 @@ const gam = _quizGhostScreenAnchorPlay(axMe, ayMe, worldToScreen, zDraw); drawQuizWrongGhostOverlayPlay(gam.sx, gam.sy, gam.sizeH, myId, me.nickname + meTag); } else { - drawAvatar(axMe, ayMe, true, me.nickname + meTag, me.characterId, faceDirMe, meWalking, mt, meGauntletJumpVis, me.gauntletScore || 0, quizCarrySignForEntity(me), isGauntletCrownHeistMapPlay() ? (me.gauntletCrownPenaltyFxUntil || 0) : 0); + 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); } if (isJumpSurvive() && jumpSurviveEliminated) ctx.restore(); } }); + /* Minigame-2: อัปเดต DOM overlay (ชื่อ/คะแนน/-10) ให้ตรงตำแหน่งตัวละครบน canvas */ + try { syncGauntletNameDom(worldToScreen, zDraw, tileSize); } catch (eGntDom) { /* ignore */ } } /* ไอคอนคำถามพิเศษในฉาก (วาดทับ map/ผู้เล่น) + ตรวจการเดินชน */ if (specialQuizIcon) { @@ -20896,10 +21068,8 @@ ctx.font = 'bold 14px sans-serif'; ctx.textAlign = 'left'; if (isGauntletCrownHeistMapPlay()) { - ctx.fillText('พรมแดงสุดท้าย | คะแนน ' + (me.gauntletScore || 0) + ' · ชน -10 · ขอบซ้ายตาย · สูงสุด 6 คน', 10, 24); - ctx.font = '12px sans-serif'; - ctx.fillStyle = '#c0caf5'; - ctx.fillText('Space / W / ↑ = กระโดด · เลน + เลเซอร์ · จบเกมมีโบนัสอันดับ + เกรดทีม', 10, 42); + /* Minigame-2 (Last Light): ใช้ cyber HUD + DOM overlay แล้ว — ไม่วาด debug text (คะแนน · ชน -10 · ขอบซ้ายตาย) บน canvas + (เดิมโผล่มุมซ้ายบนทับ SCORE label เวลา useCyberPlayHud() = false ใน preview embed) */ } else { ctx.fillText('พรมแดงสุดท้าย | คะแนน: ' + (me.gauntletScore || 0) + ' (ข้าม 1 ชิ้น +1) · สูงสุด 6 คน', 10, 24); ctx.font = '12px sans-serif'; @@ -21508,9 +21678,7 @@ })(); (function wireGauntletCrownHowtoPrimary() { - const btn = document.getElementById('btn-gch-ready'); - if (!btn) return; - btn.addEventListener('click', () => { + function onHowtoPrimaryClick() { if (isJumpSurviveMissionUiMapPlay()) { if (jumpSurviveMissionPhase !== 'howto') return; const humans = quizCarryPregameHumanIds(); @@ -21654,6 +21822,10 @@ gauntletCrownLobbyReadyMap[sid] = next; updateGauntletCrownHowtoHud(); socket.emit('gauntlet-crown-lobby-ready', { ready: next }); + } + ['btn-gch-ready', 'btn-gch-ready-mg2'].forEach((id) => { + const btn = document.getElementById(id); + if (btn) btn.addEventListener('click', onHowtoPrimaryClick); }); })(); diff --git a/www/html/Game/public/play.html b/www/html/Game/public/play.html index c6e0ac5..b517701 100644 --- a/www/html/Game/public/play.html +++ b/www/html/Game/public/play.html @@ -21,6 +21,9 @@ + + +