minigame 3add more design 1.6
This commit is contained in:
@@ -24,6 +24,9 @@ English: Upload Stack / tower assets under **`TowerBlock`** (no spaces). **`Towe
|
||||
| `result-check.png` (ถ้ามี) | ไอคอนติ๊ก «ภารกิจสำเร็จ» บนสรุป — ไม่มีจะใช้ของ gauntlet |
|
||||
| `sling.png` | เชือกเหวี่ยงบล็อกในเกม (ยืดจากจุดแขวนถึงบล็อก) — แนวตั้งในไฟล์ = แนวสาย; ไม่มีไฟล์จะใช้เส้นประแทน |
|
||||
| `heart-1.png` | โชว์กลางเหนือกองเมื่อ **พลาด** (Tower live) — ไม่มีจะ fallback ข้อความ `♥ −1` |
|
||||
| `life-full.png` | HUD มุมขวา: หัวใจ **เต็ม** ในแถบชีวิต (คู่ `life-hit.png`) — ไม่มีจะ fallback เป็น `heart-1.png` |
|
||||
| `life-bar.png` | HUD: สไตล์ข้อความ **SYSTEM INTEGRITY** + วงเล็บ — วาง **ซ้ายของอวตาร** ในแผง cyan (mock รูป2) |
|
||||
| `life-hit.png` | หัวใจ **หาย / โดนตี** ในแถบชีวิต (คู่ `life-full.png`) |
|
||||
| `btn-drop.png` | ปุ่ม **DROP** มุมขวาล่างใน `play.html` (เทียบ Space / Enter) |
|
||||
| `score+10.png` | Tower live: โชว์เหนือบล็อกเมื่อได้ **+10** |
|
||||
| `score+20.png` | Tower live: เหนือบล็อกเมื่อ **×2 (+20)** หรือ **progress ถึง ~100%**; ถ้า perfect คู่ (support เต็ม) จะโชว์บนชั้นบน **และ** ชั้นก่อนหน้า |
|
||||
|
||||
+148
-34
@@ -324,8 +324,12 @@
|
||||
let stackTowerScorePlus20Img = null;
|
||||
let stackTowerScoreLightImg = null;
|
||||
let stackTowerScoreX2Img = null;
|
||||
/** พลาดแล้วโชว์ heart-1.png กลางเหนือกอง (mock Tower) */
|
||||
/** พลาดแล้วโชว์ heart-1.png กลางเหนือกอง (mock Tower) — heart เต็มใน HUD ใช้ไฟล์เดียวกัน */
|
||||
let stackTowerHeartMinusImg = null;
|
||||
/** HUD มุมขวา: `life-bar.png` + `life-full.png` / `life-hit.png` (mock รูป2) */
|
||||
let stackTowerLifeBarImg = null;
|
||||
let stackTowerLifeHitImg = null;
|
||||
let stackTowerLifeFullImg = null;
|
||||
/** @type {{ until: number } | null} */
|
||||
let stackTowerHeartMinusFx = null;
|
||||
const STACK_TOWER_HEART_MINUS_FX_MS = 1500;
|
||||
@@ -4188,6 +4192,102 @@
|
||||
return im && im.complete && im.naturalWidth > 0 ? im : null;
|
||||
}
|
||||
|
||||
function ensureStackTowerLifeHudImagesPlay() {
|
||||
if (!stackTowerLifeBarImg) {
|
||||
stackTowerLifeBarImg = new Image();
|
||||
stackTowerLifeBarImg.decoding = 'async';
|
||||
stackTowerLifeBarImg.src = stackTowerAssetUrl('life-bar.png');
|
||||
}
|
||||
if (!stackTowerLifeHitImg) {
|
||||
stackTowerLifeHitImg = new Image();
|
||||
stackTowerLifeHitImg.decoding = 'async';
|
||||
stackTowerLifeHitImg.src = stackTowerAssetUrl('life-hit.png');
|
||||
}
|
||||
if (!stackTowerLifeFullImg) {
|
||||
stackTowerLifeFullImg = new Image();
|
||||
stackTowerLifeFullImg.decoding = 'async';
|
||||
stackTowerLifeFullImg.src = stackTowerAssetUrl('life-full.png');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* พื้นที่ระหว่างวงเล็บบน life-bar.png (normalized 0–1) — ปรับถ้า asset เปลี่ยนเลย์เอาต์
|
||||
* UV rect on life-bar.png for heart row between brackets (tune if art changes).
|
||||
*/
|
||||
const STACK_TOWER_LIFE_BAR_HEART_SLOT_UV = { u0: 0.11, u1: 0.89, v0: 0.40, v1: 0.86 };
|
||||
|
||||
/**
|
||||
* วาด life-bar + หัวใจ life-full (เต็ม) / life-hit (หาย) ในกรอบ dest — คืนความสูงจริงที่วาด
|
||||
*/
|
||||
function drawStackTowerLifeIntegrityBarPlay(ctx, destX, destY, maxW, maxH, lifeSlots, livesRem) {
|
||||
ensureStackTowerLifeHudImagesPlay();
|
||||
const bar = stackTowerLifeBarImg;
|
||||
const fullIm = (stackTowerLifeFullImg && stackTowerLifeFullImg.complete && stackTowerLifeFullImg.naturalWidth > 0)
|
||||
? stackTowerLifeFullImg
|
||||
: ensureStackTowerHeartMinusImgPlay();
|
||||
const hitIm = stackTowerLifeHitImg && stackTowerLifeHitImg.complete && stackTowerLifeHitImg.naturalWidth > 0
|
||||
? stackTowerLifeHitImg
|
||||
: null;
|
||||
const slots = Math.max(1, Math.min(20, Math.floor(lifeSlots) || 1));
|
||||
const lives = Math.max(0, Math.min(slots, Math.floor(livesRem) || 0));
|
||||
|
||||
if (!bar || !bar.complete || !bar.naturalWidth || maxW <= 4 || maxH <= 4) {
|
||||
ctx.save();
|
||||
ctx.fillStyle = '#bb9af7';
|
||||
ctx.font = '600 9px ui-sans-serif, system-ui, sans-serif';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'middle';
|
||||
let heartStr = '';
|
||||
for (let i = 0; i < slots; i++) heartStr += i < lives ? '♥ ' : '♡ ';
|
||||
ctx.fillText('SYSTEM INTEGRITY ' + heartStr.trim(), destX, destY + maxH * 0.5);
|
||||
ctx.restore();
|
||||
return Math.min(maxH, 28);
|
||||
}
|
||||
|
||||
const bw0 = bar.naturalWidth;
|
||||
const bh0 = bar.naturalHeight;
|
||||
const scale = Math.min(maxW / bw0, maxH / bh0, 1.35);
|
||||
const bw = bw0 * scale;
|
||||
const bh = bh0 * scale;
|
||||
const bx = destX;
|
||||
const by = destY + Math.max(0, (maxH - bh) * 0.5);
|
||||
|
||||
ctx.save();
|
||||
try {
|
||||
ctx.drawImage(bar, 0, 0, bw0, bh0, bx, by, bw, bh);
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
const uv = STACK_TOWER_LIFE_BAR_HEART_SLOT_UV;
|
||||
const ix = bx + uv.u0 * bw;
|
||||
const iy = by + uv.v0 * bh;
|
||||
const iwSlot = Math.max(4, (uv.u1 - uv.u0) * bw);
|
||||
const ihSlot = Math.max(4, (uv.v1 - uv.v0) * bh);
|
||||
const slotW = iwSlot / slots;
|
||||
const iconMax = Math.min(slotW * 0.82, ihSlot * 0.88, 36);
|
||||
|
||||
for (let i = 0; i < slots; i++) {
|
||||
const cx = ix + slotW * (i + 0.5);
|
||||
const cy = iy + ihSlot * 0.5;
|
||||
const useFull = i < lives;
|
||||
const im = useFull ? fullIm : hitIm;
|
||||
if (im && im.complete && im.naturalWidth > 0 && im.naturalHeight > 0) {
|
||||
const ih = iconMax;
|
||||
const iw = (ih * im.naturalWidth) / im.naturalHeight;
|
||||
try {
|
||||
ctx.drawImage(im, 0, 0, im.naturalWidth, im.naturalHeight, cx - iw / 2, cy - ih / 2, iw, ih);
|
||||
} catch (e2) { /* ignore */ }
|
||||
} else {
|
||||
ctx.fillStyle = useFull ? '#9ece6a' : '#565f89';
|
||||
ctx.font = 'bold ' + Math.round(Math.max(10, iconMax * 0.72)) + 'px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(useFull ? '♥' : '♡', cx, cy);
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
return bh;
|
||||
}
|
||||
|
||||
function triggerStackTowerHeartMinusFxPlay() {
|
||||
if (!isStackTowerMissionHudActivePlay()) return;
|
||||
stackTowerHeartMinusFx = { until: performance.now() + STACK_TOWER_HEART_MINUS_FX_MS };
|
||||
@@ -7989,6 +8089,10 @@
|
||||
stackPreviewHudLog = [];
|
||||
}
|
||||
ensureStackTowerSlingImagePlay();
|
||||
if (towerMission || previewFillBots) {
|
||||
ensureStackTowerLifeHudImagesPlay();
|
||||
ensureStackTowerHeartMinusImgPlay();
|
||||
}
|
||||
}
|
||||
|
||||
function stackEaseDrop(t) {
|
||||
@@ -14236,18 +14340,29 @@
|
||||
drawRows(rows);
|
||||
}
|
||||
|
||||
const rightW = 136;
|
||||
const framePad = 10;
|
||||
const bigAv = Math.min(72, Math.max(52, ch * 0.10));
|
||||
const avBox = bigAv + 10;
|
||||
const integrityMinW = 120;
|
||||
const rightW = Math.min(300, Math.max(236, Math.round(cw * 0.34)));
|
||||
let rightX = cw - pad - rightW;
|
||||
const topY = 8;
|
||||
if (rightX < pad + boardW + 14) {
|
||||
rightX = Math.max(pad, cw - pad - rightW);
|
||||
}
|
||||
const bigAv = Math.min(78, Math.max(56, ch * 0.11));
|
||||
const turnPanelH = bigAv + 118;
|
||||
const integrityW = Math.max(integrityMinW, rightW - framePad * 2 - avBox - 10);
|
||||
const innerTop = topY + framePad;
|
||||
const integrityX = rightX + framePad;
|
||||
const integrityH = avBox + 2;
|
||||
const avFrameX = integrityX + integrityW + 10;
|
||||
const avFrameY = innerTop;
|
||||
const footerH = 26;
|
||||
const turnPanelH = framePad + integrityH + 10 + footerH + framePad;
|
||||
|
||||
ctx.fillStyle = 'rgba(8, 12, 28, 0.9)';
|
||||
roundRectPath(rightX, topY, rightW, turnPanelH, 8);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = 'rgba(187, 154, 247, 0.55)';
|
||||
ctx.strokeStyle = 'rgba(0, 230, 255, 0.55)';
|
||||
ctx.lineWidth = 1.5;
|
||||
roundRectPath(rightX, topY, rightW, turnPanelH, 8);
|
||||
ctx.stroke();
|
||||
@@ -14257,39 +14372,34 @@
|
||||
curEntry = order.find((e) => e.seat === currentSeat) || null;
|
||||
}
|
||||
const curV = curEntry ? resolveEntryVisual(curEntry) : resolveEntryVisual({ kind: 'human', seat: 1 });
|
||||
const framePad = 8;
|
||||
const avCx = rightX + rightW / 2;
|
||||
const bigFrameTop = topY + framePad - 2;
|
||||
const bigFrameH = bigAv + 8;
|
||||
ctx.strokeStyle = 'rgba(255, 120, 200, 0.75)';
|
||||
ctx.lineWidth = 2;
|
||||
roundRectPath(rightX + framePad - 2, topY + framePad - 2, rightW - (framePad - 2) * 2, bigAv + 8, 6);
|
||||
ctx.stroke();
|
||||
drawHudAvatar(avCx, bigFrameTop + bigFrameH - 4, bigAv, curV.characterId, curV.playTint);
|
||||
|
||||
ctx.fillStyle = '#f5c2e7';
|
||||
ctx.font = 'bold 10px ui-sans-serif, system-ui, sans-serif';
|
||||
drawStackTowerLifeIntegrityBarPlay(ctx, integrityX, innerTop - 2, integrityW, integrityH + 4, lifeSlots, livesRem);
|
||||
|
||||
const avCx = avFrameX + avBox / 2;
|
||||
ctx.save();
|
||||
ctx.shadowColor = 'rgba(0, 234, 255, 0.5)';
|
||||
ctx.shadowBlur = 12;
|
||||
ctx.strokeStyle = 'rgba(0, 234, 255, 0.92)';
|
||||
ctx.lineWidth = 2;
|
||||
roundRectPath(avFrameX - 1, avFrameY - 1, avBox + 2, avBox + 2, 6);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
ctx.strokeStyle = 'rgba(0, 234, 255, 0.45)';
|
||||
ctx.lineWidth = 1;
|
||||
roundRectPath(avFrameX - 1, avFrameY - 1, avBox + 2, avBox + 2, 6);
|
||||
ctx.stroke();
|
||||
drawHudAvatar(avCx, avFrameY + avBox - 5, bigAv, curV.characterId, curV.playTint);
|
||||
|
||||
const footY = innerTop + integrityH + 12;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText('ACTIVE NODE', rightX + rightW / 2, topY + bigAv + 22);
|
||||
ctx.fillStyle = '#7dfcff';
|
||||
ctx.font = 'bold 11px ' + mono;
|
||||
ctx.fillText('P' + currentSeat + ' · ' + curV.name.slice(0, 10), rightX + rightW / 2, topY + bigAv + 38);
|
||||
|
||||
ctx.fillStyle = '#bb9af7';
|
||||
ctx.font = '600 9px ui-sans-serif, system-ui, sans-serif';
|
||||
ctx.fillText('SYSTEM INTEGRITY', rightX + rightW / 2, topY + bigAv + 58);
|
||||
ctx.font = '14px sans-serif';
|
||||
let heartStr = '';
|
||||
for (let i = 0; i < lifeSlots; i++) {
|
||||
heartStr += i < livesRem ? '♥ ' : '♡ ';
|
||||
}
|
||||
ctx.fillStyle = livesRem <= 1 ? '#f7768e' : '#9ece6a';
|
||||
ctx.fillText(heartStr.trim(), rightX + rightW / 2, topY + bigAv + 74);
|
||||
|
||||
ctx.fillStyle = 'rgba(125, 252, 255, 0.85)';
|
||||
ctx.fillStyle = livesRem <= 1 ? '#f7768e' : 'rgba(125, 252, 255, 0.88)';
|
||||
ctx.font = monoSm;
|
||||
ctx.fillText('TEAM ' + teamScore + ' · COMBO x' + combo, rightX + rightW / 2, topY + bigAv + 96);
|
||||
ctx.fillText(
|
||||
'P' + currentSeat + ' · ' + curV.name.slice(0, 9) + ' · TEAM ' + teamScore + ' · COMBO x' + combo,
|
||||
rightX + rightW / 2,
|
||||
footY,
|
||||
);
|
||||
|
||||
const termW = Math.min(380, Math.max(220, cw * 0.42));
|
||||
const logLineH = 12;
|
||||
@@ -14503,6 +14613,10 @@
|
||||
'play-cyber-hud--question-mission',
|
||||
!!(isQuizQuestionMissionHudActivePlay() || isStackTowerMissionHudActivePlay()),
|
||||
);
|
||||
root.classList.toggle(
|
||||
'play-cyber-hud--stack-tower-canvas-hud',
|
||||
!!(previewFillBots && stackMini && isStack() && isStackTowerMissionUiMapPlay()),
|
||||
);
|
||||
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' : '';
|
||||
|
||||
@@ -1674,6 +1674,10 @@
|
||||
#play-cyber-hud.play-cyber-hud--question-mission .play-cyber-preview-line {
|
||||
display: none !important;
|
||||
}
|
||||
/* Stack Tower: แผงโปรไฟล์วาดบน canvas — ซ่อนกล่อง portrait DOM ขวา ไม่ให้ซ้ำกับอวตารในแผง SYSTEM INTEGRITY */
|
||||
#play-cyber-hud.play-cyber-hud--stack-tower-canvas-hud .play-cyber-self {
|
||||
display: none !important;
|
||||
}
|
||||
/* Last Light (mno9kb07): แถบคะแนนแนวนอน — score.png แล้วตามด้วยสูงสุด 6 ช่อง (อวาตาร์ | ชื่อบน / คะแนนล่าง) */
|
||||
#play-cyber-hud.play-cyber-hud--gauntlet-crown-strip .play-cyber-hint {
|
||||
display: none !important;
|
||||
@@ -2749,7 +2753,7 @@
|
||||
</div>
|
||||
<script src="/Game/socket.io/socket.io.js"></script>
|
||||
<script src="js/version.js?v=0.0258"></script>
|
||||
<script src="js/play.js?v=0.0258"></script>
|
||||
<script src="js/play.js?v=0.0259"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user