minigame 3add more design 1.1

This commit is contained in:
2026-05-04 16:05:42 +00:00
parent 4be7be8be2
commit 95c7f04d58
4 changed files with 146 additions and 22 deletions
+2 -2
View File
@@ -358,8 +358,8 @@
<p class="legend" id="editor-status">บันทึกแล้วจะได้รหัสฉาก ใช้รหัสนี้ตอนสร้างห้องในล็อบบี้</p>
</div>
</div>
<script src="js/version.js?v=0.0249"></script>
<script src="js/editor.js?v=0.0249"></script>
<script src="js/version.js?v=0.0253"></script>
<script src="js/editor.js?v=0.0253"></script>
<div class="version-tag">v —</div>
</body>
</html>
+141 -17
View File
@@ -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';
+1 -1
View File
@@ -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;
+2 -2
View File
@@ -2713,8 +2713,8 @@
</div>
</div>
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0249"></script>
<script src="js/play.js?v=0.0249"></script>
<script src="js/version.js?v=0.0253"></script>
<script src="js/play.js?v=0.0253"></script>
<div class="version-tag">v —</div>
</body>
</html>