diff --git a/www/html/Game/public/editor.html b/www/html/Game/public/editor.html index 6c5cc0e..728ad80 100644 --- a/www/html/Game/public/editor.html +++ b/www/html/Game/public/editor.html @@ -358,8 +358,8 @@

บันทึกแล้วจะได้รหัสฉาก ใช้รหัสนี้ตอนสร้างห้องในล็อบบี้

- - + +
v —
diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js index 036b5cf..d0314c2 100644 --- a/www/html/Game/public/js/play.js +++ b/www/html/Game/public/js/play.js @@ -196,10 +196,17 @@ } function pickStackBlockHeavyVisual(seat) { - const pct = Math.max(0, Math.min(100, Math.floor(Number(playStackHeavyBlockPercent) || 0))); - if (pct <= 0) return false; const slot = Math.max(0, Math.min(5, (Math.floor(Number(seat)) || 1) - 1)); const hU = normalizeGauntletAssetUrlForPlay(playStackBlockHeavyUrls[slot] || ''); + /** Tower mnn93hpi: เกิน 50% ของ Progress 100% (ตาม stackTowerProgressBlocks) → ใช้รูป heavy จาก admin — นอก live ไม่สุ่ม heavy */ + if (isStackTowerMissionUiMapPlay()) { + if (!(stackTowerMissionPhase === 'live' && stackMini)) return false; + const p = Number(stackMini.progressPct); + if (!Number.isFinite(p) || p <= 50) return false; + return !!hU; + } + const pct = Math.max(0, Math.min(100, Math.floor(Number(playStackHeavyBlockPercent) || 0))); + if (pct <= 0) return false; if (!hU) return false; if (Math.random() * 100 >= pct) return false; return true; @@ -280,6 +287,13 @@ let stackFall = null; /** Tower / Stack — เชือกเหวี่ยงจาก `img/TowerBlock/sling.png` */ let stackTowerSlingImg = null; + /** Tower mnn93hpi — perfect stack FX (`img/TowerBlock/score-light.png`, `scorex2.png`) */ + let stackTowerScoreLightImg = null; + let stackTowerScoreX2Img = null; + const STACK_TOWER_PERFECT_GLOW_MS = 1500; + const STACK_TOWER_PERFECT_X2_MS = 2200; + /** ทับบล็อกก่อนหน้า + ชั้นใหม่: support เต็มแทบไม่มีเพี้ยน */ + const STACK_TOWER_PERFECT_SUPPORT_MIN = 0.998; let lastStackTickMs = performance.now(); let lastStackTowerScrollBgTickMs = performance.now(); let mapBackgroundImg = null; @@ -3999,6 +4013,60 @@ return im && im.complete && im.naturalWidth > 0 ? im : null; } + function ensureStackTowerPerfectFxImagesPlay() { + if (!stackTowerScoreLightImg) { + stackTowerScoreLightImg = new Image(); + stackTowerScoreLightImg.decoding = 'async'; + stackTowerScoreLightImg.src = stackTowerAssetUrl('score-light.png'); + } + if (!stackTowerScoreX2Img) { + stackTowerScoreX2Img = new Image(); + stackTowerScoreX2Img.decoding = 'async'; + stackTowerScoreX2Img.src = stackTowerAssetUrl('scorex2.png'); + } + return { light: stackTowerScoreLightImg, x2: stackTowerScoreX2Img }; + } + + /** วาด score-light ทับขอบบล็อก (หลังสไปรต์ชั้น) */ + function drawStackTowerPerfectGlowOverlayPlay(ctx, sx, sy, drawW, drawH, lightImg, nowT, untilT) { + if (!lightImg || !lightImg.complete || !lightImg.naturalWidth || drawW <= 0 || drawH <= 0) return; + const life = Math.max(0, Math.min(1, (untilT - nowT) / STACK_TOWER_PERFECT_GLOW_MS)); + const pulse = 0.38 + 0.52 * Math.sin((1 - life) * Math.PI); + const nw = lightImg.naturalWidth; + const nh = lightImg.naturalHeight; + const pad = 1.14 + (1 - life) * 0.06; + const gw = drawW * pad; + const gh = drawH * pad; + const gx = sx + (drawW - gw) * 0.5; + const gy = sy + (drawH - gh) * 0.5; + ctx.save(); + ctx.globalAlpha = Math.min(0.94, pulse); + ctx.globalCompositeOperation = 'screen'; + try { + ctx.drawImage(lightImg, 0, 0, nw, nh, gx, gy, gw, gh); + } catch (e) { /* ignore */ } + ctx.restore(); + } + + /** x2 ขวาชั้นบน — ทับจุดเดียวกับ mock (แสง + ไอคอน) */ + function drawStackTowerPerfectX2OverlayPlay(ctx, x2Img, sx, sy, drawW, drawH) { + if (!x2Img || !x2Img.complete || !x2Img.naturalWidth || drawW <= 0 || drawH <= 0) return; + const iw = x2Img.naturalWidth; + const ih = x2Img.naturalHeight; + const targetH = Math.max(drawH * 0.92, Math.min(drawH * 1.35, 72)); + const scale = targetH / ih; + const dw = iw * scale; + const dh = ih * scale; + const cx = sx + drawW + drawW * 0.06; + const cy = sy + drawH * 0.5; + ctx.save(); + ctx.globalAlpha = 0.98; + try { + ctx.drawImage(x2Img, 0, 0, iw, ih, cx - dw * 0.48, cy - dh * 0.5, dw, dh); + } catch (e) { /* ignore */ } + ctx.restore(); + } + /** * เชือกจากจุดหนีบบน (จอ) ลงจุดยึดบล็อก — รูป sling แนวตั้ง (แกน Y = สาย) ยืดตามความยาว */ @@ -7621,6 +7689,7 @@ nextDropVisualDirty: true, pendingDropSeat: 1, pendingDropHeavy: false, + perfectStackX2Fx: null, }; if (previewFillBots) { me.stackPreviewHumanPts = 0; @@ -7650,6 +7719,13 @@ const towerLive = isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'live'; if (hit.miss) { stackMini.combo = 0; + stackMini.perfectStackX2Fx = null; + if (stackMini.layers && stackMini.layers.length) { + for (let gi = 0; gi < stackMini.layers.length; gi++) { + const lay = stackMini.layers[gi]; + if (lay && lay.perfectGlowUntil != null) delete lay.perfectGlowUntil; + } + } if (towerLive && stackMini.teamMissesLeft != null) { stackMini.teamMissesLeft = Math.max(0, stackMini.teamMissesLeft - 1); } else { @@ -7658,6 +7734,7 @@ stackMini.score = Math.max(0, stackMini.score - 2); stackMini.lives = 3; stackMini.layers = []; + stackMini.perfectStackX2Fx = null; stackMini.widthTiles = stackMini.initialWidthTiles; const lb = getStackAreaBoundsPlay(mapData.stackLandArea, mapData.width || 20, mapData.height || 15); stackMini.topCenterX = lb ? lb.cx : (mapData.width || 20) * 0.5; @@ -7671,6 +7748,26 @@ const seatLay = layerVisualMeta && layerVisualMeta.seat != null ? layerVisualMeta.seat : getStackDropActorSeat(); const heavyLay = !!(layerVisualMeta && layerVisualMeta.heavy); stackMini.layers.push({ w: W, cx: hit.placedCx, seat: seatLay, heavy: heavyLay }); + const nAfter = stackMini.layers.length; + const perfectFull = + towerLive && + hit.perfect && + !hit.miss && + nAfter >= 2 && + hit.supportRatio != null && + hit.supportRatio >= STACK_TOWER_PERFECT_SUPPORT_MIN; + if (perfectFull) { + const gUntil = performance.now() + STACK_TOWER_PERFECT_GLOW_MS; + stackMini.layers[nAfter - 1].perfectGlowUntil = gUntil; + stackMini.layers[nAfter - 2].perfectGlowUntil = gUntil; + ensureStackTowerPerfectFxImagesPlay(); + stackMini.perfectStackX2Fx = { + until: performance.now() + STACK_TOWER_PERFECT_X2_MS, + topLayerIndex: nAfter - 1, + }; + } else if (towerLive) { + stackMini.perfectStackX2Fx = null; + } stackMini.score += hit.pts; if (towerLive && Number.isFinite(Number(hit.progressDelta)) && (hit.progressDelta || 0) > 0) { let np = Number(stackMini.progressPct) + Number(hit.progressDelta); @@ -8047,6 +8144,7 @@ function drawStackMinigame(ctx, worldToScreen, zoom) { if (!stackMini) return; const m = stackMini; + const towerHudLive = isStackTowerMissionHudActivePlay(); ensureStackNextDropVisual(); const layerWorldH = m.layerWorldH || Math.max(14, tileSize * 0.3); const stripH = m.blockStripH || Math.max(16, tileSize * 0.32); @@ -8082,6 +8180,36 @@ const seatL = L.seat != null ? L.seat : 1; const heavyL = !!L.heavy; drawStackBlockSpriteOrHue(ctx, sx, sy, drawW, drawH, seatL, heavyL, hue, {}); + if (towerHudLive && L.perfectGlowUntil != null) { + const ntGlow = performance.now(); + if (ntGlow < L.perfectGlowUntil) { + const fxImG = ensureStackTowerPerfectFxImagesPlay(); + if (fxImG.light) { + drawStackTowerPerfectGlowOverlayPlay(ctx, sx, sy, drawW, drawH, fxImG.light, ntGlow, L.perfectGlowUntil); + } + } + } + } + if (towerHudLive && m.perfectStackX2Fx) { + const fxX2 = m.perfectStackX2Fx; + const ntX = performance.now(); + if (ntX >= fxX2.until) { + m.perfectStackX2Fx = null; + } else { + const ti = fxX2.topLayerIndex; + if (ti >= 0 && ti < m.layers.length) { + const Ltop = m.layers[ti]; + const lwT = (Ltop.w != null ? Ltop.w : m.initialWidthTiles) * tileSize; + const cxWT = Ltop.cx * tileSize; + const ybT = floorY - (ti + 1) * layerWorldH; + const xlT = cxWT - lwT / 2; + const [sxT, syT] = worldToScreen(xlT, ybT); + const drawWT = lwT * zoom; + const drawHT = layerWorldH * zoom; + const fxImX = ensureStackTowerPerfectFxImagesPlay(); + if (fxImX.x2) drawStackTowerPerfectX2OverlayPlay(ctx, fxImX.x2, sxT, syT, drawWT, drawHT); + } + } } if (m.settling) { const s = m.settling; @@ -8154,15 +8282,6 @@ ctx.lineWidth = 1; ctx.restore(); } - const [fsx, fsy] = worldToScreen(0, floorY); - const [fex, fey] = worldToScreen(mapData.width * tileSize, floorY); - ctx.strokeStyle = 'rgba(247, 118, 190, 0.65)'; - ctx.lineWidth = 3; - ctx.beginPath(); - ctx.moveTo(fsx, fsy); - ctx.lineTo(fex, fey); - ctx.stroke(); - ctx.lineWidth = 1; } function applyGauntletTimingFromServer(payload) { @@ -13895,7 +14014,7 @@ const qText = document.getElementById('play-cyber-quiz-mission-q-text'); const qPlaque = document.getElementById('play-cyber-quiz-mission-q-plaque'); const tb = root ? root.querySelector('.play-cyber-time-block') : null; - const on = isQuizQuestionMissionHudActivePlay(); + const on = isQuizQuestionMissionHudActivePlay() || isStackTowerMissionHudActivePlay(); if (isQuizQuestionMissionUiMapPlay()) { if (qBand) { qBand.classList.add('is-hidden'); @@ -14031,7 +14150,10 @@ const gw = document.querySelector('.game-wrap'); if (gw) gw.classList.toggle('play-cyber-active', !!on); if (!on || !root) return; - root.classList.toggle('play-cyber-hud--question-mission', !!isQuizQuestionMissionHudActivePlay()); + root.classList.toggle( + 'play-cyber-hud--question-mission', + !!(isQuizQuestionMissionHudActivePlay() || isStackTowerMissionHudActivePlay()), + ); root.classList.toggle('play-cyber-hud--gauntlet-crown-strip', !!(on && isGauntletCrownHeistMapPlay())); const timeLabelEl = root.querySelector('.play-cyber-time-label'); if (timeLabelEl) timeLabelEl.style.display = isQuizCarry() ? 'none' : ''; @@ -14289,6 +14411,8 @@ const jumpMissionHud = isJumpSurvive() && isJumpSurviveMissionUiMapPlay(); const quizMissionHud = isQuizQuestionMissionHudActivePlay(); const stackMissionHud = isStackTowerMissionHudActivePlay(); + /** แถบ SCORE แบบ mock mng8a80o (แนวนอน อวาตาร์+ชื่อ | คะแนน) — ใช้กับภารกิจคำถาม + Stack Tower mnn93hpi */ + const cyberQmMockHud = quizMissionHud || stackMissionHud; const stackTeamPts = Math.max(0, Number(stackMini && stackMini.score) || 0); const rows = []; rows.push({ @@ -14368,22 +14492,22 @@ const row = hudRows[r]; const li = document.createElement('li'); li.className = 'play-cyber-score-row' + (row.isMe ? ' is-me' : '') + (row.eliminated ? ' is-out' : '') - + (quizMissionHud ? ' play-cyber-score-row--qm-mock' : '') + + (cyberQmMockHud ? ' play-cyber-score-row--qm-mock' : '') + (crownStripHud ? ' play-cyber-score-row--crown-strip' : ''); const av = document.createElement('img'); - av.className = 'play-cyber-score-av' + (quizMissionHud ? ' play-cyber-score-av--qm' : '') + av.className = 'play-cyber-score-av' + (cyberQmMockHud ? ' play-cyber-score-av--qm' : '') + (crownStripHud ? ' play-cyber-score-av--crown-strip' : ''); av.alt = ''; setCyberHudScoreAvatarImg(av, row); const sc = document.createElement('span'); - sc.className = 'play-cyber-score-val' + (quizMissionHud ? ' play-cyber-qm-score' : '') + sc.className = 'play-cyber-score-val' + (cyberQmMockHud ? ' play-cyber-qm-score' : '') + (crownStripHud ? ' play-cyber-crown-strip-val' : ''); if (isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry() || jumpMissionHud || quizMissionHud || stackMissionHud) { sc.textContent = String(row.score); } else { sc.textContent = row.eliminated ? '—' : String(r + 1); } - if (quizMissionHud) { + if (cyberQmMockHud) { if (row.id === leaderId && leaderId != null) { const crown = document.createElement('span'); crown.className = 'play-cyber-lead-crown'; diff --git a/www/html/Game/public/js/version.js b/www/html/Game/public/js/version.js index 4dcc741..e63d4e7 100644 --- a/www/html/Game/public/js/version.js +++ b/www/html/Game/public/js/version.js @@ -1,6 +1,6 @@ // ทุกครั้งที่มีการเพิ่มหรือเปลี่ยน ให้เพิ่ม v +0.0001 // หลังแก้ค่าแล้วต้อง copy ไป path ที่ Nginx ชี้ (หรือรัน copy-frogger-files-only.sh) ถึงจะเห็นบนเว็บ -window.APP_VERSION = '0.0249'; +window.APP_VERSION = '0.0253'; document.addEventListener('DOMContentLoaded', function () { var t = document.querySelector('.version-tag'); if (t) t.textContent = 'v ' + window.APP_VERSION; diff --git a/www/html/Game/public/play.html b/www/html/Game/public/play.html index 20aed14..2bd5686 100644 --- a/www/html/Game/public/play.html +++ b/www/html/Game/public/play.html @@ -2713,8 +2713,8 @@ - - + +
v —