diff --git a/www/html/Game/public/css/testimony-overlay.css b/www/html/Game/public/css/testimony-overlay.css index 325d69c..e46ccef 100644 --- a/www/html/Game/public/css/testimony-overlay.css +++ b/www/html/Game/public/css/testimony-overlay.css @@ -358,18 +358,24 @@ #testimony-overlay .es3-cards { left: calc(200 / var(--es3-w) * 100%); top: calc(635 / var(--es3-h) * 100%); - width: calc(800 / var(--es3-w) * 100%); - height: calc(380 / var(--es3-h) * 100%); + /* ขยายให้พอ 3 ใบเรียงพอดี = 252*3 + 22*2 = 800px stage */ + width: 800px; + height: 380px; display: flex; - gap: calc(22 / var(--es3-w) * 100%); + justify-content: flex-start; + align-items: stretch; + gap: 22px; z-index: 3; } #testimony-overlay .es3-card { position: relative; - flex: 1 1 0; - min-width: 0; - height: 100%; + /* คงสัดส่วน portrait — ไม่ยืดเต็มจอเมื่อเก็บได้แค่ใบเดียว; 252×380 stage px ตรงตามดีไซน์ */ + flex: 0 0 252px; + width: 252px; + min-width: 252px; + max-width: 252px; + height: 380px; border-radius: 12px; background: linear-gradient(180deg, #cfd5dd 0%, #9ea7b4 100%); box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45); @@ -718,16 +724,18 @@ } #testimony-overlay .es-reveal-wrap .es-players { - left: 120px; - top: 300px; - width: 1680px; - height: 650px; - display: grid; - grid-template-columns: repeat(5, 1fr); - column-gap: 24px; - padding: 0 70px; + left: 200px; + top: 290px; + width: 1520px; + height: 720px; + display: flex; + flex-wrap: nowrap; + justify-content: flex-start; + align-items: flex-start; + column-gap: 32px; + row-gap: 18px; + padding: 0; box-sizing: border-box; - align-items: start; z-index: 6; } @@ -771,19 +779,19 @@ #testimony-overlay .es-reveal-wrap .es-card { position: relative; - width: 220px; - max-width: 220px; - height: 275px; + width: 252px; + min-width: 252px; + max-width: 252px; + height: 320px; flex-shrink: 0; - border-radius: 10px; - background: linear-gradient(180deg, #cfd5dd 0%, #b2bac4 100%); - box-shadow: 0 6px 16px rgba(0, 0, 0, 0.45); + border-radius: 12px; + background: linear-gradient(180deg, #cfd5dd 0%, #9ea7b4 100%); + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45); display: flex; flex-direction: column; overflow: hidden; color: #1b2030; border: 3px solid rgba(150, 200, 255, 0.7); - flex-shrink: 0; } #testimony-overlay .es-reveal-wrap .es-card.rarity-common { diff --git a/www/html/Game/public/img/QUESTION/ghost/ghost-front.png b/www/html/Game/public/img/QUESTION/ghost/ghost-front.png new file mode 100644 index 0000000..4090b2e Binary files /dev/null and b/www/html/Game/public/img/QUESTION/ghost/ghost-front.png differ diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js index 15ab2f5..7a323e9 100644 --- a/www/html/Game/public/js/play.js +++ b/www/html/Game/public/js/play.js @@ -813,8 +813,8 @@ const raw = mapData.gauntletCrownRunwayBg && typeof mapData.gauntletCrownRunwayBg === 'object' ? mapData.gauntletCrownRunwayBg : {}; let lineInArt = Number(raw.finishLineInFinishArtFrac); if (!Number.isFinite(lineInArt)) { - /* mno9kb07: จบที่ประตูตู้เซฟ (ขวาของ finish art) ไม่ใช่แถบ FINISH ด้านซ้าย */ - lineInArt = gauntletCrownRunwayBgMapEligiblePlay() ? 0.72 : 0.12; + /* mno9kb07: จบที่ "แถบ FINISH" สีเหลือง (กลาง-ซ้ายของ finish art) — ไม่ใช่ตู้เซฟ */ + lineInArt = gauntletCrownRunwayBgMapEligiblePlay() ? 0.22 : 0.12; } lineInArt = Math.max(0, Math.min(0.92, lineInArt)); const finishLineWorldX = g.finishStart + g.w4Safe * lineInArt; @@ -831,13 +831,15 @@ legacy = Math.max(0.08, Math.min(0.96, legacy)); return Math.max(0.12, Math.min(0.62, 0.14 + legacy * 0.42)); } - return gauntletCrownRunwayBgMapEligiblePlay() ? 0.5 : 0.34; + /* mno9kb07: หยุดเลื่อนที่ ~0.42 (เส้น FINISH เลื่อนมาอยู่กลางจอ) */ + return gauntletCrownRunwayBgMapEligiblePlay() ? 0.42 : 0.34; } function gauntletCrownRunwayObstacleFadeStartViewportFracPlay() { const raw = mapData && mapData.gauntletCrownRunwayBg && typeof mapData.gauntletCrownRunwayBg === 'object' ? mapData.gauntletCrownRunwayBg : {}; let v = Number(raw.obstacleFadeStartViewportFrac); - if (!Number.isFinite(v)) v = 0.9; + /* mno9kb07: เริ่มจางอุปสรรคให้เร็วขึ้น (1.0 = ตอนเส้น FINISH ยังอยู่ขอบขวา) เพื่อให้อุปสรรคหายก่อนถึงเส้นชัย */ + if (!Number.isFinite(v)) v = gauntletCrownRunwayBgMapEligiblePlay() ? 1.05 : 0.9; return Math.max(0.45, Math.min(1.15, v)); } @@ -2695,6 +2697,41 @@ let playLiveQuizScores = {}; /** เกมถูก/ผิด (multiplayer): ใครเคยตอบผิดในเซสชันนี้ — ใช้โชว์แสตมป์ในสรุปภารกิจ mng8a80o */ let playQuizEverWrong = {}; + /** ผีตามตัวเมื่อตอบผิด — ใช้เฉพาะแมป mng8a80o (เป็นสัญลักษณ์/บทลงโทษเชิงสนุก) */ + const QUIZ_WRONG_GHOST_URL = BASE + '/img/QUESTION/ghost/ghost-front.png'; + let quizWrongGhostImg = null; + function getQuizWrongGhostImg() { + if (quizWrongGhostImg) return quizWrongGhostImg; + const img = new Image(); + img.decoding = 'async'; + img.src = QUIZ_WRONG_GHOST_URL; + quizWrongGhostImg = img; + return img; + } + /** วาด "ผี" คุมหัวผู้เล่นในแมป mng8a80o เมื่อตอบผิด (ใช้กับ me/peers) */ + function drawQuizWrongGhostOverlayPlay(ax, ay, entId) { + if (!isQuizQuestionMissionUiMapPlay()) return; + const sid = String(entId == null ? '' : entId); + const isMeEnt = (myId != null && sid === String(myId)); + const wrong = isMeEnt + ? !!(playQuizPlayerLocal && (playQuizPlayerLocal.cannotTrue || playQuizPlayerLocal.cannotFalse)) + : !!playQuizEverWrong[sid]; + if (!wrong) return; + const img = getQuizWrongGhostImg(); + if (!img || !img.complete || !img.naturalWidth) return; + /* ขนาด: ~1.6 tile (ใหญ่กว่าหัวเล็กน้อย แต่ไม่บัง avatar ทั้งตัว); ลอยเหนือหัว */ + const tile = (typeof tileSize === 'number' && tileSize > 0) ? tileSize : 32; + const wPx = tile * 1.6; + const ratio = img.naturalHeight / Math.max(1, img.naturalWidth); + const hPx = wPx * ratio; + const tBob = Math.sin(performance.now() / 280) * (tile * 0.06); + const drawX = ax + (tile - wPx) / 2; + const drawY = ay - hPx * 0.78 + tBob; + ctx.save(); + ctx.globalAlpha = 0.92; + ctx.drawImage(img, drawX, drawY, wPx, hPx); + ctx.restore(); + } /** เกมถูก/ผิด — ตรงกับ server QUIZ_TF_POINTS_PER_CORRECT */ const QUIZ_TF_POINTS_PER_CORRECT = 10; const QUIZ_TF_SCORE_PLUS_URL = BASE + '/img/QUESTION/score+.png'; @@ -8340,6 +8377,7 @@ return; } const tint = getPlayTintForMissionAvatar(peerId != null ? peerId : ((myId != null && myId !== '') ? myId : 'local')); + const isMePeer = (peerId != null && myId != null && String(peerId) === String(myId)); const out = resolvePlayDrawCharSource(characterId, 'down', nowT, false, tint, isMePeer); if (out && out.tagName === 'CANVAS' && out.width > 0) { try { @@ -18435,6 +18473,7 @@ if (botOut) ctx.save(); if (botOut) ctx.globalAlpha = 0.4; 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); + drawQuizWrongGhostOverlayPlay(axO, ayO, id); if (botOut) ctx.restore(); } else { if (shouldGauntletCrownHeistSkipAvatarDrawPlay(me, true, 0, 0)) return; @@ -18445,6 +18484,7 @@ if (isJumpSurvive() && jumpSurviveEliminated) ctx.globalAlpha = 0.4; const faceDirMe = isGauntletFaceRightMapMno9kb07() ? 'right' : me.direction; drawAvatar(axMe, ayMe, true, me.nickname + meTag, me.characterId, faceDirMe, meWalking, mt, meGauntletJumpVis, me.gauntletScore || 0, quizCarrySignForEntity(me), isGauntletCrownHeistMapPlay() ? (me.gauntletCrownPenaltyFxUntil || 0) : 0); + drawQuizWrongGhostOverlayPlay(axMe, ayMe, myId); if (isJumpSurvive() && jumpSurviveEliminated) ctx.restore(); } }); diff --git a/www/html/Game/public/js/room-lobby.js b/www/html/Game/public/js/room-lobby.js index 665a002..94b35d8 100644 --- a/www/html/Game/public/js/room-lobby.js +++ b/www/html/Game/public/js/room-lobby.js @@ -4600,7 +4600,20 @@ } /* ===== ห้องสรุปหลักฐาน — การไต่สวน (เลือกหลักฐาน 2 ใบ/ปากคำ ก่อนโหวต) ===== */ - var tmState = { round: 0, members: [], picks: [], submitted: false, mode: 'select', submittedPicksById: {} }; + var tmState = { round: 0, members: [], picks: [], submitted: false, mode: 'select', submittedPicksById: {}, ownedSlots: [], requiredPicks: 2 }; + + /** สล็อตหลักฐานที่ผู้เล่นเก็บได้สำหรับ suspect ตามรอบ (เช่น [0,2]) */ + function tmOwnedSlotsForRound(round) { + var i = Math.floor(Number(round)); + if (Number.isNaN(i) || i < 0 || i > 2) i = 0; + var row = (myPlayerEvidence && myPlayerEvidence[i]) || []; + var out = []; + row.forEach(function (s) { + var v = Math.floor(Number(s)); + if (v >= 0 && v <= 2 && out.indexOf(v) === -1) out.push(v); + }); + return out; + } var SUSPECT_NAMES_EN = ['SOMCHAI', 'ARIN', 'JIRAPHA']; function ensureTestimonyStyle() { @@ -4739,31 +4752,49 @@ var card = e.target.closest('.es3-card'); if (!card || tmState.submitted) return; var idx = parseInt(card.getAttribute('data-card-index'), 10); if (!(idx >= 0)) return; + var maxPicks = Math.max(1, Math.min(2, tmState.requiredPicks || 2)); var pos = tmState.picks.indexOf(idx); if (pos >= 0) { tmState.picks.splice(pos, 1); card.classList.remove('is-selected'); } - else { if (tmState.picks.length >= 2) return; tmState.picks.push(idx); card.classList.add('is-selected'); } + else { if (tmState.picks.length >= maxPicks) return; tmState.picks.push(idx); card.classList.add('is-selected'); } es3UpdateCount(); }); var submitEl = ov.querySelector('#es3BtnSubmit'); if (submitEl) submitEl.addEventListener('click', function () { - if (tmState.picks.length !== 2 || tmState.submitted) return; - socket.emit('testimony-submit', { picks: tmState.picks.slice(0, 2) }, function (res) { + var req = Math.max(1, Math.min(2, tmState.requiredPicks || 2)); + if (tmState.picks.length !== req || tmState.submitted) return; + socket.emit('testimony-submit', { picks: tmState.picks.slice(0, req) }, function (res) { if (res && res.ok) { tmState.submitted = true; if (!tmState.submittedPicksById) tmState.submittedPicksById = {}; - tmState.submittedPicksById[socket.id] = tmState.picks.slice(0, 2); + tmState.submittedPicksById[socket.id] = tmState.picks.slice(0, req); es3SetStatus('ส่งหลักฐานแล้ว — รอผู้เล่นอื่น...'); var sub = document.getElementById('es3BtnSubmit'); if (sub) sub.disabled = true; } else appendLobbySystemChat('— ส่งหลักฐานไม่สำเร็จ' + (res && res.error ? (' · ' + res.error) : '')); }); }); var openEl = ov.querySelector('#es3BtnOpen'); - if (openEl) { - openEl.addEventListener('click', function () { + function autoSubmitThenReveal() { + var req = Math.max(1, Math.min(2, tmState.requiredPicks || 2)); + var doReveal = function () { socket.emit('testimony-reveal', {}, function (res) { if (!(res && res.ok)) appendLobbySystemChat('— เปิดเผยไม่สำเร็จ' + (res && res.error ? (' · ' + res.error) : '')); }); - }); + }; + /* ถ้ายังไม่ submit แต่เลือกครบตาม required แล้ว — submit ให้ก่อน reveal */ + if (!tmState.submitted && tmState.ownedSlots.length > 0 && tmState.picks.length === req) { + socket.emit('testimony-submit', { picks: tmState.picks.slice(0, req) }, function (res) { + if (res && res.ok) { + tmState.submitted = true; + tmState.submittedPicksById[socket.id] = tmState.picks.slice(0, req); + } + doReveal(); + }); + } else { + doReveal(); + } + } + if (openEl) { + openEl.addEventListener('click', autoSubmitThenReveal); openEl.addEventListener('keydown', function (e) { - if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.click(); } + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); autoSubmitThenReveal(); } }); } var readyEl = ov.querySelector('#esBtnReady'); @@ -4788,8 +4819,9 @@ } function es3UpdateCount() { - var el = document.getElementById('es3-count'); if (el) el.textContent = 'เลือกหลักฐาน ' + tmState.picks.length + '/2'; - var sub = document.getElementById('es3BtnSubmit'); if (sub) sub.disabled = (tmState.picks.length !== 2 || tmState.submitted); + var req = Math.max(1, Math.min(2, tmState.requiredPicks || 2)); + var el = document.getElementById('es3-count'); if (el) el.textContent = 'เลือกหลักฐาน ' + tmState.picks.length + '/' + req; + var sub = document.getElementById('es3BtnSubmit'); if (sub) sub.disabled = (tmState.picks.length !== req || tmState.submitted); } function esRevealCardEl(c, linkName, cardIdx) { @@ -4924,6 +4956,8 @@ function openTestimony(data) { if (typeof closeSuspectOverlay === 'function') closeSuspectOverlay(); + /* ซิงค์หลักฐานของตัวเองจาก server ก่อน (กันรอบที่ play จบใหม่ๆ แล้ว state ใน room-lobby ยังไม่อัปเดต) */ + if (data && Array.isArray(data.myPlayerEvidence)) applyMyPlayerEvidence(data.myPlayerEvidence); var ov = ensureTestimonyOverlay(); tmState.round = (data && typeof data.round === 'number') ? data.round : 0; tmState.members = (data && Array.isArray(data.members)) ? data.members : []; @@ -4931,6 +4965,9 @@ tmState.submitted = false; tmState.submittedPicksById = {}; tmState.mode = 'select'; + tmState.ownedSlots = tmOwnedSlotsForRound(tmState.round); + /* ต้องเลือกหลักฐาน = min(2, จำนวนที่เก็บได้) — ถ้าได้ 1 ใบ ส่ง 1 ใบพอ */ + tmState.requiredPicks = Math.max(1, Math.min(2, tmState.ownedSlots.length)); var SC = '/Main-Lobby/IMAGE/Showcard/'; var CS = '/Main-Lobby/IMAGE/Choose%20a%20suspect/'; var sd = tmSuspectData(tmState.round); @@ -4953,7 +4990,26 @@ } es3SetStatus('กำลังรอ..ผู้เล่นเลือกหลักฐาน'); var cardsRoot = document.getElementById('es3-cards'); cardsRoot.textContent = ''; - (sd.cards || []).forEach(function (c, i) { cardsRoot.appendChild(es3EvidenceCardEl(c, sd.linkName, i, true)); }); + /* แสดงเฉพาะการ์ดที่ผู้เล่นเก็บได้จริง (ตามสล็อตจาก myPlayerEvidence) */ + if (tmState.ownedSlots.length === 0) { + var empty = document.createElement('div'); + empty.className = 'es3-empty-hint'; + empty.textContent = 'ยังไม่มีหลักฐานของ ' + (sd.linkName || ('ผู้ต้องสงสัย ' + (tmState.round + 1))) + ' — รอเพื่อนเลือกหลักฐาน'; + cardsRoot.appendChild(empty); + /* auto-submit ว่างเพื่อไม่บล็อกรอบ */ + socket.emit('testimony-submit', { picks: [] }, function (res) { + if (res && res.ok) { + tmState.submitted = true; + tmState.submittedPicksById[socket.id] = []; + es3SetStatus('ไม่มีหลักฐาน — ข้ามอัตโนมัติ'); + } + }); + } else { + tmState.ownedSlots.forEach(function (slot) { + var c = (sd.cards || [])[slot]; + if (c) cardsRoot.appendChild(es3EvidenceCardEl(c, sd.linkName, slot, true)); + }); + } es3UpdateCount(); es3RenderMembers([], [], false); ov.classList.remove('is-hidden'); diff --git a/www/html/Game/public/play.html b/www/html/Game/public/play.html index 4c02b02..fe93ba6 100644 --- a/www/html/Game/public/play.html +++ b/www/html/Game/public/play.html @@ -3214,7 +3214,7 @@ - +