minigame 3add more design 1.5

This commit is contained in:
2026-05-04 17:30:21 +00:00
parent 95c7f04d58
commit b95c54f57e
8 changed files with 620 additions and 145 deletions
+38
View File
@@ -2464,3 +2464,41 @@ code {
flex: 0 0 2.25rem;
font-size: 0.82rem;
}
/* Stack minigame (Tower block) — คอลัมน์ปกติ/ใหญ่เท่ากัน + พรีวิวสัดส่วนตามรูปอ้างอิง (ปกติ 314×103 · ใหญ่ 382×72) */
.space-shooter-ship-row.stack-block-visual-row {
display: grid;
grid-template-columns:
1.75rem
minmax(0, 1fr)
min(42vw, 168px)
auto
minmax(0, 1fr)
min(48vw, 200px)
auto;
align-items: start;
gap: 0.45rem 0.55rem;
}
.stack-block-visual-row .space-shooter-ship-url-label {
flex: unset;
min-width: 0;
max-width: 100%;
}
.stack-block-visual-row .space-shooter-ship-url-label input[type='text'] {
max-width: 100%;
}
.stack-block-visual-row .space-shooter-ship-prev.stack-block-prev {
flex: unset;
width: 100%;
height: auto;
display: block;
box-sizing: border-box;
}
.stack-block-visual-row .space-shooter-ship-prev.stack-block-prev--normal {
aspect-ratio: 314 / 103;
max-width: 168px;
}
.stack-block-visual-row .space-shooter-ship-prev.stack-block-prev--heavy {
aspect-ratio: 382 / 72;
max-width: 200px;
}
+7 -7
View File
@@ -10,7 +10,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Kanit:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="admin.css?v=30">
<link rel="stylesheet" href="admin.css?v=31">
</head>
<body>
<a class="skip-link" href="#admin-main">ข้ามไปเนื้อหา</a>
@@ -784,12 +784,12 @@
</label>
</div>
<div class="space-shooter-ship-grid" role="group" aria-label="Stack block images per seat">
<div class="space-shooter-ship-row stack-block-visual-row"><span class="space-shooter-ship-slot" title="ที่นั่ง 1">P1</span><label class="space-shooter-ship-url-label">ปกติ <input type="text" id="stack-block-normal-url-1" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="stack-block-normal-prev-1" alt="" width="56" height="28" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-normal-clear-1">ล้าง</button><label class="space-shooter-ship-url-label">ใหญ่ <input type="text" id="stack-block-heavy-url-1" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="stack-block-heavy-prev-1" alt="" width="56" height="28" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-heavy-clear-1">ล้าง</button></div>
<div class="space-shooter-ship-row stack-block-visual-row"><span class="space-shooter-ship-slot" title="ที่นั่ง 2">P2</span><label class="space-shooter-ship-url-label">ปกติ <input type="text" id="stack-block-normal-url-2" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="stack-block-normal-prev-2" alt="" width="56" height="28" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-normal-clear-2">ล้าง</button><label class="space-shooter-ship-url-label">ใหญ่ <input type="text" id="stack-block-heavy-url-2" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="stack-block-heavy-prev-2" alt="" width="56" height="28" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-heavy-clear-2">ล้าง</button></div>
<div class="space-shooter-ship-row stack-block-visual-row"><span class="space-shooter-ship-slot" title="ที่นั่ง 3">P3</span><label class="space-shooter-ship-url-label">ปกติ <input type="text" id="stack-block-normal-url-3" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="stack-block-normal-prev-3" alt="" width="56" height="28" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-normal-clear-3">ล้าง</button><label class="space-shooter-ship-url-label">ใหญ่ <input type="text" id="stack-block-heavy-url-3" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="stack-block-heavy-prev-3" alt="" width="56" height="28" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-heavy-clear-3">ล้าง</button></div>
<div class="space-shooter-ship-row stack-block-visual-row"><span class="space-shooter-ship-slot" title="ที่นั่ง 4">P4</span><label class="space-shooter-ship-url-label">ปกติ <input type="text" id="stack-block-normal-url-4" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="stack-block-normal-prev-4" alt="" width="56" height="28" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-normal-clear-4">ล้าง</button><label class="space-shooter-ship-url-label">ใหญ่ <input type="text" id="stack-block-heavy-url-4" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="stack-block-heavy-prev-4" alt="" width="56" height="28" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-heavy-clear-4">ล้าง</button></div>
<div class="space-shooter-ship-row stack-block-visual-row"><span class="space-shooter-ship-slot" title="ที่นั่ง 5">P5</span><label class="space-shooter-ship-url-label">ปกติ <input type="text" id="stack-block-normal-url-5" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="stack-block-normal-prev-5" alt="" width="56" height="28" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-normal-clear-5">ล้าง</button><label class="space-shooter-ship-url-label">ใหญ่ <input type="text" id="stack-block-heavy-url-5" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="stack-block-heavy-prev-5" alt="" width="56" height="28" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-heavy-clear-5">ล้าง</button></div>
<div class="space-shooter-ship-row stack-block-visual-row"><span class="space-shooter-ship-slot" title="ที่นั่ง 6">P6</span><label class="space-shooter-ship-url-label">ปกติ <input type="text" id="stack-block-normal-url-6" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="stack-block-normal-prev-6" alt="" width="56" height="28" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-normal-clear-6">ล้าง</button><label class="space-shooter-ship-url-label">ใหญ่ <input type="text" id="stack-block-heavy-url-6" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="stack-block-heavy-prev-6" alt="" width="56" height="28" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-heavy-clear-6">ล้าง</button></div>
<div class="space-shooter-ship-row stack-block-visual-row"><span class="space-shooter-ship-slot" title="ที่นั่ง 1">P1</span><label class="space-shooter-ship-url-label">ปกติ <input type="text" id="stack-block-normal-url-1" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev stack-block-prev stack-block-prev--normal" id="stack-block-normal-prev-1" alt="" width="314" height="103" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-normal-clear-1">ล้าง</button><label class="space-shooter-ship-url-label">ใหญ่ <input type="text" id="stack-block-heavy-url-1" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev stack-block-prev stack-block-prev--heavy" id="stack-block-heavy-prev-1" alt="" width="382" height="72" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-heavy-clear-1">ล้าง</button></div>
<div class="space-shooter-ship-row stack-block-visual-row"><span class="space-shooter-ship-slot" title="ที่นั่ง 2">P2</span><label class="space-shooter-ship-url-label">ปกติ <input type="text" id="stack-block-normal-url-2" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev stack-block-prev stack-block-prev--normal" id="stack-block-normal-prev-2" alt="" width="314" height="103" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-normal-clear-2">ล้าง</button><label class="space-shooter-ship-url-label">ใหญ่ <input type="text" id="stack-block-heavy-url-2" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev stack-block-prev stack-block-prev--heavy" id="stack-block-heavy-prev-2" alt="" width="382" height="72" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-heavy-clear-2">ล้าง</button></div>
<div class="space-shooter-ship-row stack-block-visual-row"><span class="space-shooter-ship-slot" title="ที่นั่ง 3">P3</span><label class="space-shooter-ship-url-label">ปกติ <input type="text" id="stack-block-normal-url-3" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev stack-block-prev stack-block-prev--normal" id="stack-block-normal-prev-3" alt="" width="314" height="103" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-normal-clear-3">ล้าง</button><label class="space-shooter-ship-url-label">ใหญ่ <input type="text" id="stack-block-heavy-url-3" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev stack-block-prev stack-block-prev--heavy" id="stack-block-heavy-prev-3" alt="" width="382" height="72" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-heavy-clear-3">ล้าง</button></div>
<div class="space-shooter-ship-row stack-block-visual-row"><span class="space-shooter-ship-slot" title="ที่นั่ง 4">P4</span><label class="space-shooter-ship-url-label">ปกติ <input type="text" id="stack-block-normal-url-4" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev stack-block-prev stack-block-prev--normal" id="stack-block-normal-prev-4" alt="" width="314" height="103" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-normal-clear-4">ล้าง</button><label class="space-shooter-ship-url-label">ใหญ่ <input type="text" id="stack-block-heavy-url-4" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev stack-block-prev stack-block-prev--heavy" id="stack-block-heavy-prev-4" alt="" width="382" height="72" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-heavy-clear-4">ล้าง</button></div>
<div class="space-shooter-ship-row stack-block-visual-row"><span class="space-shooter-ship-slot" title="ที่นั่ง 5">P5</span><label class="space-shooter-ship-url-label">ปกติ <input type="text" id="stack-block-normal-url-5" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev stack-block-prev stack-block-prev--normal" id="stack-block-normal-prev-5" alt="" width="314" height="103" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-normal-clear-5">ล้าง</button><label class="space-shooter-ship-url-label">ใหญ่ <input type="text" id="stack-block-heavy-url-5" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev stack-block-prev stack-block-prev--heavy" id="stack-block-heavy-prev-5" alt="" width="382" height="72" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-heavy-clear-5">ล้าง</button></div>
<div class="space-shooter-ship-row stack-block-visual-row"><span class="space-shooter-ship-slot" title="ที่นั่ง 6">P6</span><label class="space-shooter-ship-url-label">ปกติ <input type="text" id="stack-block-normal-url-6" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev stack-block-prev stack-block-prev--normal" id="stack-block-normal-prev-6" alt="" width="314" height="103" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-normal-clear-6">ล้าง</button><label class="space-shooter-ship-url-label">ใหญ่ <input type="text" id="stack-block-heavy-url-6" maxlength="500" spellcheck="false" placeholder="/Game/img/....png" autocomplete="off"></label><img class="space-shooter-ship-prev stack-block-prev stack-block-prev--heavy" id="stack-block-heavy-prev-6" alt="" width="382" height="72" decoding="async"><button type="button" class="btn btn-ghost" id="btn-stack-block-heavy-clear-6">ล้าง</button></div>
</div>
</fieldset>
<div class="quiz-admin-actions">
+2 -2
View File
@@ -358,8 +358,8 @@
<p class="legend" id="editor-status">บันทึกแล้วจะได้รหัสฉาก ใช้รหัสนี้ตอนสร้างห้องในล็อบบี้</p>
</div>
</div>
<script src="js/version.js?v=0.0253"></script>
<script src="js/editor.js?v=0.0253"></script>
<script src="js/version.js?v=0.0258"></script>
<script src="js/editor.js?v=0.0258"></script>
<div class="version-tag">v —</div>
</body>
</html>
+10 -1
View File
@@ -23,5 +23,14 @@ English: Upload Stack / tower assets under **`TowerBlock`** (no spaces). **`Towe
| `popup-result.png` | แผงสรุปภารกิจ (GCM) |
| `result-check.png` (ถ้ามี) | ไอคอนติ๊ก «ภารกิจสำเร็จ» บนสรุป — ไม่มีจะใช้ของ gauntlet |
| `sling.png` | เชือกเหวี่ยงบล็อกในเกม (ยืดจากจุดแขวนถึงบล็อก) — แนวตั้งในไฟล์ = แนวสาย; ไม่มีไฟล์จะใช้เส้นประแทน |
| `heart-1.png` | โชว์กลางเหนือกองเมื่อ **พลาด** (Tower live) — ไม่มีจะ fallback ข้อความ `♥ 1` |
| `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 เต็ม) จะโชว์บนชั้นบน **และ** ชั้นก่อนหน้า |
| `score-light.png` | คู่กับ **+20 / perfect full** — แสงข้างชั้นบน (mock รูป2) |
| `scorex2.png` | กราฟิก **×2** ข้างชั้นบน — ใช้คู่ `score-light.png` |
| `result-timeup.png` | หมดเวลาก่อน decrypt ครบ 100% → แฟลชเต็มจอ **5 วิ** ก่อนหน้า GCM |
| `result-gameover.png` | พลาดจนหมด **team misses** (เช่น เกิน 3 ครั้งตามตั้งค่า) → แฟลช 5 วิ ก่อน GCM |
| `result-complete.png` | decrypt ถึง **100%** ภายในเวลา → แฟลช 5 วิ ก่อน GCM |
English: Mission map **mnn93hpi** loads **`popup-Howto.png`**, **`popup-result.png`**, optional **`result-check.png`**, **`sling.png`** (crane rope; dashed fallback if missing) from this folder.
English: **`score+10.png`** / **`score+20.png`** — score popups above blocks; dual **+20** on perfect “full support” pair. **`result-timeup.png`**, **`result-gameover.png`**, **`result-complete.png`** — 5s fullscreen flash before the mission summary (GCM). Missing images fall back to opening GCM immediately for the flash layer.
+468 -83
View File
@@ -46,6 +46,28 @@
const STACK_MAX_CENTER_DRIFT_NO_LAND_MULT = 1.0;
/** overlap/fallW ต่ำกว่านี้ → เข้า settling (แท่งไม่นิ่ง) */
const STACK_STABLE_SUPPORT_RATIO_MIN = 0.62;
/** Stack ทั่วไป (ไม่ใช่ Tower post-50%): บล็อก heavy กว้างกว่าปกติ (~382×72 vs 314×103) */
const STACK_BLOCK_HEAVY_WIDTH_MULT = 382 / 314;
/** Tower live + progress ≥50%: ลดความกว้างบล็อก 40% (เหลือ 60%) — ความสูงชั้น `layerWorldH` เท่าเดิม */
const STACK_TOWER_POST50_WIDTH_MULT = 0.6;
function stackBlockWidthTilesForPlay(baseTiles, heavy) {
const b = Number(baseTiles);
const base = Number.isFinite(b) ? Math.max(0.85, Math.min(3.2, b)) : 2;
const towerPost50 =
isStackTowerMissionUiMapPlay() &&
stackTowerMissionPhase === 'live' &&
stackMini &&
Number.isFinite(Number(stackMini.progressPct)) &&
Number(stackMini.progressPct) >= 50;
if (towerPost50) {
const w = base * STACK_TOWER_POST50_WIDTH_MULT;
return Math.max(0.85, Math.min(3.2, Math.round(w * 200) / 200));
}
if (!heavy) return base;
const w = base * STACK_BLOCK_HEAVY_WIDTH_MULT;
return Math.min(3.2, Math.round(w * 200) / 200);
}
/** Violent Crime mission: asteroid strikes ship → invuln → 3 strikes = eliminated */
const SPACE_SHOOTER_MISSION_MAX_ASTEROID_HITS = 3;
const SPACE_SHOOTER_MISSION_HIT_INVULN_MS = 1400;
@@ -198,11 +220,11 @@
function pickStackBlockHeavyVisual(seat) {
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 */
/** 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;
if (!Number.isFinite(p) || p < 50) return false;
return !!hU;
}
const pct = Math.max(0, Math.min(100, Math.floor(Number(playStackHeavyBlockPercent) || 0)));
@@ -216,6 +238,7 @@
if (!stackMini || !stackMini.nextDropVisualDirty) return;
stackMini.pendingDropSeat = getStackDropActorSeat();
stackMini.pendingDropHeavy = pickStackBlockHeavyVisual(stackMini.pendingDropSeat);
stackMini.widthTiles = stackBlockWidthTilesForPlay(stackMini.initialWidthTiles, !!stackMini.pendingDropHeavy);
stackMini.nextDropVisualDirty = false;
}
@@ -243,7 +266,11 @@
return null;
}
/** @returns {boolean} true = วาดสไปรต์แล้ว (ไม่ยืด — fit ในกล่องแบบ contain) · false = วาดสี่เหลี่ยมสี fallback */
/**
* @returns {boolean} true = วาดสไปรตแล · false = วาดสเหลยมส fallback
* ใช scale แบบ cover + clip ใหขอบภาพตรงกลองฟกส (drawW×drawH) ไมเวนขอบซายขวาแบบ contain
* เพอใหสายตาตรงก overlap / miss ใน computeStackDropResult
*/
function drawStackBlockSpriteOrHue(ctx, sx, sy, drawW, drawH, seat, heavy, hueFallback, opts) {
const o = opts || {};
const rec = resolveStackBlockSpriteRec(seat, heavy);
@@ -252,21 +279,26 @@
const iw = img.naturalWidth;
const ih = img.naturalHeight;
if (drawW > 0 && drawH > 0) {
const scale = Math.min(drawW / iw, drawH / ih);
const scale = Math.max(drawW / iw, drawH / ih);
const dw = iw * scale;
const dh = ih * scale;
const dx = sx + (drawW - dw) * 0.5;
const dy = sy + (drawH - dh) * 0.5;
const prevSmooth = ctx.imageSmoothingEnabled;
ctx.save();
ctx.beginPath();
ctx.rect(sx, sy, drawW, drawH);
ctx.clip();
try {
ctx.imageSmoothingEnabled = false;
ctx.drawImage(img, 0, 0, iw, ih, dx, dy, dw, dh);
} finally {
ctx.imageSmoothingEnabled = prevSmooth;
}
ctx.restore();
if (o.missOverlay) {
ctx.fillStyle = 'rgba(247, 118, 190, 0.42)';
ctx.fillRect(dx, dy, dw, dh);
ctx.fillRect(sx, sy, drawW, drawH);
}
return true;
}
@@ -287,10 +319,20 @@
let stackFall = null;
/** Tower / Stack — เชือกเหวี่ยงจาก `img/TowerBlock/sling.png` */
let stackTowerSlingImg = null;
/** Tower mnn93hpi — perfect stack FX (`img/TowerBlock/score-light.png`, `scorex2.png`) */
/** Tower mnn93hpi — +คะแนน (`score+10.png` / `score+20.png`) + เอฟเฟกต์ ×2 (`score-light.png`, `scorex2.png`) */
let stackTowerScorePlus10Img = null;
let stackTowerScorePlus20Img = null;
let stackTowerScoreLightImg = null;
let stackTowerScoreX2Img = null;
/** พลาดแล้วโชว์ heart-1.png กลางเหนือกอง (mock Tower) */
let stackTowerHeartMinusImg = null;
/** @type {{ until: number } | null} */
let stackTowerHeartMinusFx = null;
const STACK_TOWER_HEART_MINUS_FX_MS = 1500;
const STACK_TOWER_PERFECT_GLOW_MS = 1500;
/** ป๊อปอัป +10 / +20 เหนือบล็อก */
const STACK_TOWER_SCORE_POPUP_MS = 2000;
/** ระยะโชว์ score-light + scorex2 ข้างชั้นบน (คู่กับ +20 / perfect full) */
const STACK_TOWER_PERFECT_X2_MS = 2200;
/** ทับบล็อกก่อนหน้า + ชั้นใหม่: support เต็มแทบไม่มีเพี้ยน */
const STACK_TOWER_PERFECT_SUPPORT_MIN = 0.998;
@@ -1863,6 +1905,9 @@
let stackTowerMissionDeferredPhase = null;
let stackTowerSessionStartAt = 0;
let stackTowerMissionEndedOnce = false;
/** แฟลชรูปผล Tower (timeup / gameover / complete) ก่อน GCM 5 วิ */
let stackTowerResultFlashTimer = null;
const STACK_TOWER_RESULT_FLASH_MS = 5000;
function spaceShooterSpawnAsteroidExplosion(worldX, worldY, r) {
const urls = playSpaceShooterAsteroidSpriteUrls;
@@ -3782,6 +3827,14 @@
gchJumpTeardown.setAttribute('aria-hidden', 'true');
gchJumpTeardown.style.pointerEvents = '';
}
const stackDropTeardown = document.getElementById('stack-tower-drop-btn');
if (stackDropTeardown) {
stackDropTeardown.classList.add('is-hidden');
stackDropTeardown.setAttribute('aria-hidden', 'true');
stackDropTeardown.style.pointerEvents = '';
stackDropTeardown.classList.remove('stack-tower-drop-btn--active');
}
hideStackTowerResultFlashDomOnlyPlay();
}
function setupPlayQuizUi() {
@@ -4013,6 +4066,20 @@
return im && im.complete && im.naturalWidth > 0 ? im : null;
}
function ensureStackTowerScorePopupImagesPlay() {
if (!stackTowerScorePlus10Img) {
stackTowerScorePlus10Img = new Image();
stackTowerScorePlus10Img.decoding = 'async';
stackTowerScorePlus10Img.src = stackTowerAssetUrl('score+10.png');
}
if (!stackTowerScorePlus20Img) {
stackTowerScorePlus20Img = new Image();
stackTowerScorePlus20Img.decoding = 'async';
stackTowerScorePlus20Img.src = stackTowerAssetUrl('score+20.png');
}
return { p10: stackTowerScorePlus10Img, p20: stackTowerScorePlus20Img };
}
function ensureStackTowerPerfectFxImagesPlay() {
if (!stackTowerScoreLightImg) {
stackTowerScoreLightImg = new Image();
@@ -4027,29 +4094,8 @@
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) {
/** score-light หลัง scorex2 — วาดเป็นคู่ข้างชั้นบน (mock รูป2) */
function drawStackTowerPerfectX2ClusterPlay(ctx, lightImg, x2Img, sx, sy, drawW, drawH, nowT, fxUntil) {
if (!x2Img || !x2Img.complete || !x2Img.naturalWidth || drawW <= 0 || drawH <= 0) return;
const iw = x2Img.naturalWidth;
const ih = x2Img.naturalHeight;
@@ -4059,14 +4105,157 @@
const dh = ih * scale;
const cx = sx + drawW + drawW * 0.06;
const cy = sy + drawH * 0.5;
const life = fxUntil != null ? Math.max(0, Math.min(1, (fxUntil - nowT) / STACK_TOWER_PERFECT_X2_MS)) : 1;
const pulse = 0.4 + 0.48 * Math.sin((1 - life) * Math.PI);
if (lightImg && lightImg.complete && lightImg.naturalWidth) {
const lp = 1.06;
const lw = dw * lp;
const lh = dh * lp;
const lnx = lightImg.naturalWidth;
const lny = lightImg.naturalHeight;
ctx.save();
ctx.globalAlpha = Math.min(0.9, pulse + 0.12);
ctx.globalCompositeOperation = 'screen';
try {
ctx.drawImage(lightImg, 0, 0, lnx, lny, cx - lw * 0.5, cy - lh * 0.5, lw, lh);
} catch (e) { /* ignore */ }
ctx.restore();
}
ctx.save();
ctx.globalAlpha = 0.98;
ctx.globalCompositeOperation = 'source-over';
try {
ctx.drawImage(x2Img, 0, 0, iw, ih, cx - dw * 0.48, cy - dh * 0.5, dw, dh);
} catch (e) { /* ignore */ }
ctx.restore();
}
function drawStackTowerScorePopupsPlay(ctx, worldToScreen, zoom, m, layerWorldH, floorY) {
if (!m.scorePopups || !m.scorePopups.length) return;
const now = performance.now();
const dur = STACK_TOWER_SCORE_POPUP_MS;
const imgs = ensureStackTowerScorePopupImagesPlay();
m.scorePopups = m.scorePopups.filter((p) => p.until > now);
for (let pi = 0; pi < m.scorePopups.length; pi++) {
const p = m.scorePopups[pi];
const li = Math.floor(Number(p.layerIndex)) || 0;
if (li < 0 || li >= m.layers.length) continue;
const L = m.layers[li];
const lw = (L.w != null ? L.w : m.initialWidthTiles) * tileSize;
const cxW = L.cx * tileSize;
const yb = floorY - (li + 1) * layerWorldH;
const xl = cxW - lw / 2;
const [sx, sy] = worldToScreen(xl, yb);
const drawW = lw * zoom;
const drawH = layerWorldH * zoom;
const t0 = p.until - dur;
const t = Math.max(0, Math.min(1, (now - t0) / dur));
const alpha = 0.2 + 0.78 * Math.sin(Math.PI * t);
const rise = Math.sin((t * Math.PI) / 2) * 20 * zoom;
const im = p.kind === '20' ? imgs.p20 : imgs.p10;
const targetW = Math.min(drawW * 0.92, Math.max(52, 64 * zoom));
let iw = targetW;
let ih = targetW * 0.38;
if (im && im.complete && im.naturalWidth > 0 && im.naturalHeight > 0) {
ih = (targetW * im.naturalHeight) / im.naturalWidth;
}
const cx = sx + drawW / 2;
const cy = sy - Math.max(8, 6 * zoom) - rise - ih * 0.5;
ctx.save();
ctx.globalAlpha = Math.min(0.98, Math.max(0.08, alpha));
if (im && im.complete && im.naturalWidth > 0) {
try {
ctx.drawImage(im, 0, 0, im.naturalWidth, im.naturalHeight, cx - iw / 2, cy - ih / 2, iw, ih);
} catch (e) { /* ignore */ }
} else {
ctx.font = 'bold ' + Math.round(Math.max(12, 15 * zoom)) + 'px ui-monospace, monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = p.kind === '20' ? '#7af8ff' : '#b4f9cd';
ctx.fillText(p.kind === '20' ? '+20' : '+10', cx, cy);
}
ctx.restore();
}
}
function ensureStackTowerHeartMinusImgPlay() {
if (!stackTowerHeartMinusImg) {
stackTowerHeartMinusImg = new Image();
stackTowerHeartMinusImg.decoding = 'async';
stackTowerHeartMinusImg.src = stackTowerAssetUrl('heart-1.png');
}
const im = stackTowerHeartMinusImg;
return im && im.complete && im.naturalWidth > 0 ? im : null;
}
function triggerStackTowerHeartMinusFxPlay() {
if (!isStackTowerMissionHudActivePlay()) return;
stackTowerHeartMinusFx = { until: performance.now() + STACK_TOWER_HEART_MINUS_FX_MS };
}
function drawStackTowerHeartMinusFxPlay(ctx, worldToScreen, zoom, m, layerWorldH, floorY, nLay) {
if (!stackTowerHeartMinusFx) return;
const nt = performance.now();
if (nt >= stackTowerHeartMinusFx.until) {
stackTowerHeartMinusFx = null;
return;
}
const dur = STACK_TOWER_HEART_MINUS_FX_MS;
const t0 = stackTowerHeartMinusFx.until - dur;
const u = Math.max(0, Math.min(1, (nt - t0) / dur));
const alpha = 1 - u * u;
const worldCx = m.topCenterX * tileSize;
const stackTopY = floorY - nLay * layerWorldH;
const worldCy = stackTopY - tileSize * 1.55 - u * tileSize * 0.5;
const [sx, sy] = worldToScreen(worldCx, worldCy);
const im = ensureStackTowerHeartMinusImgPlay();
const maxW = Math.max(72, Math.min(240, 6.8 * tileSize * zoom));
let dw = maxW;
let dh = maxW * 0.52;
if (im && im.naturalWidth > 0 && im.naturalHeight > 0) {
dh = (maxW * im.naturalHeight) / im.naturalWidth;
}
ctx.save();
ctx.globalAlpha = Math.max(0, Math.min(0.98, alpha));
if (im) {
ctx.drawImage(im, sx - dw / 2, sy - dh / 2, dw, dh);
} else {
ctx.fillStyle = '#f7768e';
ctx.font = 'bold ' + Math.round(Math.max(14, 18 * zoom)) + 'px ui-monospace, monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('♥ 1', sx, sy);
}
ctx.restore();
}
/** Glow สี่เหลี่ยมสีเหลือง — ขอบเขตเท่ากล่องบล็อก (drawW×drawH) ไม่ล้ำออกนอกสไปรต์ */
function drawStackTowerPerfectYellowGlowBehindBlockPlay(ctx, sx, sy, drawW, drawH, nowT, untilT) {
if (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.48 * Math.sin((1 - life) * Math.PI);
ctx.save();
ctx.beginPath();
ctx.rect(sx, sy, drawW, drawH);
ctx.clip();
ctx.globalCompositeOperation = 'source-over';
ctx.shadowColor = 'rgba(255, 210, 40, 0.9)';
ctx.shadowBlur = Math.max(3, Math.min(10, 4 + pulse * 5));
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.globalAlpha = Math.min(0.85, 0.4 + pulse * 0.34);
ctx.fillStyle = 'rgba(255, 228, 72, 0.52)';
ctx.fillRect(sx, sy, drawW, drawH);
ctx.shadowBlur = 0;
ctx.shadowColor = 'transparent';
ctx.globalAlpha = Math.min(0.72, 0.26 + pulse * 0.32);
ctx.strokeStyle = 'rgba(255, 250, 200, 0.78)';
const sw = Math.max(1.2, Math.min(2.8, drawW * 0.022));
ctx.lineWidth = sw;
ctx.strokeRect(sx + sw * 0.5, sy + sw * 0.5, drawW - sw, drawH - sw);
ctx.restore();
}
/**
* เชอกจากจดหนบบน (จอ) ลงจดยดบลอก sling แนวต (แกน Y = สาย) ดตามความยาว
*/
@@ -4118,11 +4307,7 @@
}
function hideStackTowerHowtoRulesDom() {
const rulesEl = document.getElementById('stack-tower-howto-rules');
if (rulesEl) {
rulesEl.classList.add('is-hidden');
rulesEl.setAttribute('aria-hidden', 'true');
}
/* กล่องกติกา Stack Tower ถูกถอนออกจาก DOM — เก็บชื่อฟังก์ชันเพื่อไม่ให้จุดเรียกพัง */
}
function teardownStackTowerMissionUiPlay() {
@@ -4136,6 +4321,59 @@
clearTimeout(stackTowerMissionCountdownTimer);
stackTowerMissionCountdownTimer = null;
}
hideStackTowerResultFlashDomOnlyPlay();
}
function hideStackTowerResultFlashDomOnlyPlay() {
if (stackTowerResultFlashTimer) {
clearTimeout(stackTowerResultFlashTimer);
stackTowerResultFlashTimer = null;
}
const ov = document.getElementById('stack-tower-result-flash');
const img = document.getElementById('stack-tower-result-flash-img');
if (ov) {
ov.classList.add('is-hidden');
ov.setAttribute('aria-hidden', 'true');
}
if (img) {
img.onerror = null;
img.removeAttribute('src');
}
}
/** โชว์รูปผล Tower จาก TowerBlock 5 วิ แล้วเปิด GCM สรุปเดิม */
function beginStackTowerResultFlashThenGcm(mission) {
if (!mission || mission.uiSkin !== 'stack_tower') {
showGauntletCrownMissionOverlay(mission);
return;
}
hideStackTowerResultFlashDomOnlyPlay();
const ov = document.getElementById('stack-tower-result-flash');
const imgEl = document.getElementById('stack-tower-result-flash-img');
if (!ov || !imgEl) {
showGauntletCrownMissionOverlay(mission);
return;
}
const outc = String(mission.stackTowerOutcome || 'time_up');
let file = 'result-timeup.png';
if (outc === 'decrypt_complete') file = 'result-complete.png';
else if (outc === 'misses_exhausted') file = 'result-gameover.png';
const url = stackTowerAssetUrl(file);
const finishToGcm = function () {
hideStackTowerResultFlashDomOnlyPlay();
showGauntletCrownMissionOverlay(mission);
};
imgEl.onerror = function () {
imgEl.onerror = null;
finishToGcm();
};
imgEl.src = url;
ov.classList.remove('is-hidden');
ov.setAttribute('aria-hidden', 'false');
stackTowerResultFlashTimer = setTimeout(function () {
stackTowerResultFlashTimer = null;
finishToGcm();
}, STACK_TOWER_RESULT_FLASH_MS);
}
function stackTowerMissionTimeLimitSecPlay() {
@@ -4204,24 +4442,52 @@
ov.classList.remove('is-hidden');
setGauntletCrownHowtoStackTowerArtMaskPlay(true);
const st = document.getElementById('gauntlet-crown-howto-status');
if (st) {
st.textContent = '';
st.classList.remove('gch-status--jumper');
st.classList.add('is-hidden');
}
if (st) st.classList.remove('gch-status--jumper');
const btn = document.getElementById('btn-gch-ready');
if (btn) {
const humans = quizCarryPregameHumanIds();
const soleHuman = humans.length === 1;
const canGo = soleHuman || isMePlayHost();
btn.classList.remove('is-start-phase');
btn.classList.toggle('is-read-only', !canGo);
btn.disabled = !canGo;
btn.title = canGo ? 'READY — เริ่มนับถอยหลัง' : 'รอโฮสต์ · Wait for host';
btn.setAttribute('aria-pressed', 'false');
const humans = quizCarryPregameHumanIds();
const totPlayers = Math.max(1, quizCarryPregameTotalPlayers());
const soleParticipant = totPlayers === 1;
if (soleParticipant) {
if (st) {
st.textContent = '';
st.classList.add('is-hidden');
}
if (btn) {
const canGo = isMePlayHost() || humans.length === 1;
btn.classList.remove('is-start-phase');
btn.classList.toggle('is-read-only', !canGo);
btn.disabled = !canGo;
btn.title = canGo ? 'READY — เริ่มนับถอยหลัง' : 'รอโฮสต์ · Wait for host';
btn.setAttribute('aria-pressed', 'false');
}
} else {
if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-sync-request');
gauntletCrownSyncGuestReadyIfNeeded();
updateStackTowerMissionHowtoHud();
}
}
/** Stack Tower mnn93hpi — Ready Status / ปุ่ม แบบ quiz mission (mng8a80o) */
function updateStackTowerMissionHowtoHud() {
if (!isStackTowerMissionUiMapPlay() || stackTowerMissionPhase !== 'howto') return;
const st = document.getElementById('gauntlet-crown-howto-status');
const btn = document.getElementById('btn-gch-ready');
if (!st || !btn) return;
const humans = quizCarryPregameHumanIds();
const tot = Math.max(1, quizCarryPregameTotalPlayers());
const num = gauntletCrownPregameReadyNumerator();
st.classList.remove('is-hidden');
st.textContent = 'Ready Status : ' + num + '/' + tot;
const humansReady = humans.length > 0 && humans.every((id) => !!gauntletCrownLobbyReadyMap[id]);
btn.classList.toggle('is-start-phase', humansReady);
btn.classList.toggle('is-read-only', !isMePlayHost());
btn.disabled = !isMePlayHost();
btn.setAttribute('aria-pressed', humansReady ? 'false' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'true' : 'false'));
btn.title = isMePlayHost()
? (humansReady ? 'START' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'ยกเลิก READY' : 'READY'))
: (humansReady ? 'START (โฮสต์เท่านั้น)' : 'READY (โฮสต์เท่านั้น)');
}
function beginStackTowerMissionCountdownThenRun() {
if (!isStackTowerMissionUiMapPlay()) return;
if (stackTowerMissionCountdownTimer) {
@@ -4451,7 +4717,7 @@
let outcome = 'time_up';
if (pct >= 100) outcome = 'decrypt_complete';
else if (missesOut) outcome = 'misses_exhausted';
showGauntletCrownMissionOverlay(stackTowerMissionBuildPayload({ outcome: outcome }));
beginStackTowerResultFlashThenGcm(stackTowerMissionBuildPayload({ outcome: outcome }));
}
function isJumpSurviveMissionUiMapPlay() {
@@ -7147,6 +7413,30 @@
btn.style.pointerEvents = '';
}
function syncStackTowerDropButtonPlay() {
const btn = document.getElementById('stack-tower-drop-btn');
if (!btn) return;
if (!mapData || !isStack() || !stackMini) {
btn.classList.add('is-hidden');
btn.setAttribute('aria-hidden', 'true');
btn.style.pointerEvents = 'none';
btn.classList.remove('stack-tower-drop-btn--active');
return;
}
if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase !== 'live') {
btn.classList.add('is-hidden');
btn.setAttribute('aria-hidden', 'true');
btn.style.pointerEvents = 'none';
btn.classList.remove('stack-tower-drop-btn--active');
return;
}
btn.classList.remove('is-hidden');
btn.setAttribute('aria-hidden', 'false');
const ok = stackHumanDropGateOkPlay();
btn.classList.toggle('stack-tower-drop-btn--active', ok);
btn.style.pointerEvents = ok ? '' : 'none';
}
function resetQuizCarryPlayState() {
quizCarryPregameActive = false;
hideQuizCarryPregameOverlay();
@@ -7635,10 +7925,12 @@
: autoW;
stackMini.initialWidthTiles = iw;
stackMini.widthTiles = iw;
markStackNextDropVisualDirty();
}
function resetStackMinigameState() {
stackMini = null;
stackTowerHeartMinusFx = null;
if (!mapData || !isStack()) return;
const w = mapData.width || 20, h = mapData.height || 15;
const land = getStackAreaBoundsPlay(mapData.stackLandArea, w, h);
@@ -7689,6 +7981,7 @@
nextDropVisualDirty: true,
pendingDropSeat: 1,
pendingDropHeavy: false,
scorePopups: [],
perfectStackX2Fx: null,
};
if (previewFillBots) {
@@ -7719,6 +8012,7 @@
const towerLive = isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'live';
if (hit.miss) {
stackMini.combo = 0;
stackMini.scorePopups = [];
stackMini.perfectStackX2Fx = null;
if (stackMini.layers && stackMini.layers.length) {
for (let gi = 0; gi < stackMini.layers.length; gi++) {
@@ -7734,6 +8028,7 @@
stackMini.score = Math.max(0, stackMini.score - 2);
stackMini.lives = 3;
stackMini.layers = [];
stackMini.scorePopups = [];
stackMini.perfectStackX2Fx = null;
stackMini.widthTiles = stackMini.initialWidthTiles;
const lb = getStackAreaBoundsPlay(mapData.stackLandArea, mapData.width || 20, mapData.height || 15);
@@ -7743,11 +8038,14 @@
stackMini.stackTiltVel = 0;
}
}
if (towerLive) triggerStackTowerHeartMinusFxPlay();
} else {
const W = stackMini.initialWidthTiles;
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 placedW = hit.placedW != null && Number.isFinite(hit.placedW)
? Math.max(0.85, Math.min(3.2, hit.placedW))
: stackMini.initialWidthTiles;
stackMini.layers.push({ w: placedW, cx: hit.placedCx, seat: seatLay, heavy: heavyLay });
const nAfter = stackMini.layers.length;
const perfectFull =
towerLive &&
@@ -7760,6 +8058,8 @@
const gUntil = performance.now() + STACK_TOWER_PERFECT_GLOW_MS;
stackMini.layers[nAfter - 1].perfectGlowUntil = gUntil;
stackMini.layers[nAfter - 2].perfectGlowUntil = gUntil;
}
if (towerLive && (perfectFull || hit.pts >= 20)) {
ensureStackTowerPerfectFxImagesPlay();
stackMini.perfectStackX2Fx = {
until: performance.now() + STACK_TOWER_PERFECT_X2_MS,
@@ -7774,6 +8074,7 @@
np = Math.min(100, Math.max(0, np));
if (np > 99.999) np = 100;
stackMini.progressPct = np;
markStackNextDropVisualDirty();
}
if (previewFillBots && previewAttribution) {
if (previewAttribution.human) {
@@ -7784,8 +8085,19 @@
}
}
stackMini.combo = hit.perfect ? stackMini.combo + 1 : 0;
stackMini.widthTiles = W;
stackMini.topCenterX = hit.placedCx;
if (towerLive && !hit.miss && hit.pts > 0) {
const pNow = Number(stackMini.progressPct);
const prog100 = Number.isFinite(pNow) && pNow >= 99.5;
const kind20 = hit.pts >= 20 || prog100;
if (!stackMini.scorePopups) stackMini.scorePopups = [];
const until = performance.now() + STACK_TOWER_SCORE_POPUP_MS;
stackMini.scorePopups.push({ until, layerIndex: nAfter - 1, kind: kind20 ? '20' : '10' });
if (kind20 && perfectFull && nAfter >= 2) {
stackMini.scorePopups.push({ until, layerIndex: nAfter - 2, kind: '20' });
}
while (stackMini.scorePopups.length > 12) stackMini.scorePopups.shift();
}
}
}
@@ -7797,7 +8109,8 @@
const wMap = mapData.width || 20;
const hMap = mapData.height || 15;
const land = getStackAreaBoundsPlay(mapData.stackLandArea, wMap, hMap);
const fallW = m.initialWidthTiles;
const fallWRaw = m.widthTiles != null && Number.isFinite(m.widthTiles) ? m.widthTiles : m.initialWidthTiles;
const fallW = Math.max(0.85, Math.min(3.2, fallWRaw));
let supportCx;
let supportW;
if (m.layers.length === 0) {
@@ -7806,7 +8119,8 @@
} else {
const sup = m.layers[m.layers.length - 1];
supportCx = sup.cx;
supportW = fallW;
const swR = sup.w != null && Number.isFinite(sup.w) ? sup.w : m.initialWidthTiles;
supportW = Math.max(0.85, Math.min(3.2, swR));
}
const tier = botPeer && botPeer.botTier === 'sharp' ? 'sharp' : botPeer && botPeer.botTier === 'weak' ? 'weak' : 'avg';
const layerTight = 1 / Math.sqrt(1 + (m.layers.length || 0) * 1.05);
@@ -7849,15 +8163,16 @@
}
/**
* บลอกทนหนกวางเทาก (initialWidthTiles) · พลาดเมอทบแทนไมพอ หรอจดกลางหลดขอบฐาน (STACK_MAX_*)
* วาดชนเปนสเหลยมแนวนอน (ไมนตอช) เพอกองตรง ไมเปนกองไม
* นรองรบใความกวงจรงตอช (L.w) · บลอกทกำลงตกใช m.widthTiles (ปกต vs heavy กวางตางก)
* พลาดเมอทบแทนไพอ หรอจดกลางหลขอบฐาน (STACK_MAX_*)
*/
function computeStackDropResult(m) {
const raw = m.swingAmp * Math.sin(m.phase);
const wMap = mapData.width || 20;
const hMap = mapData.height || 15;
const land = getStackAreaBoundsPlay(mapData.stackLandArea, wMap, hMap);
const fallW = m.initialWidthTiles;
const fallWRaw = m.widthTiles != null && Number.isFinite(m.widthTiles) ? m.widthTiles : m.initialWidthTiles;
const fallW = Math.max(0.85, Math.min(3.2, fallWRaw));
let supportCx;
let supportW;
if (m.layers.length === 0) {
@@ -7866,7 +8181,8 @@
} else {
const sup = m.layers[m.layers.length - 1];
supportCx = sup.cx;
supportW = fallW;
const swR = sup.w != null && Number.isFinite(sup.w) ? sup.w : m.initialWidthTiles;
supportW = Math.max(0.85, Math.min(3.2, swR));
}
const c1 = m.topCenterX + raw;
const half1 = fallW * 0.5;
@@ -7981,8 +8297,7 @@
return true;
}
/** @returns {boolean} เริ่มแอนิเมชันร่วงแล้วหรือไม่ */
function tryHumanStackDrop() {
function stackHumanDropGateOkPlay() {
if (!stackMini || !isStack() || stackFall || stackMini.settling) return false;
if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase !== 'live') return false;
if (previewFillBots && mapData && mapData.gameType === 'stack' &&
@@ -7990,8 +8305,14 @@
rebuildStackPreviewTurnOrder();
}
if (previewFillBots && !isStackPreviewHumanTurn()) return false;
const now = Date.now();
if (now - stackMini.lastDropAt < 480) return false;
if (Date.now() - stackMini.lastDropAt < 480) return false;
return true;
}
/** @returns {boolean} เริ่มแอนิเมชันร่วงแล้วหรือไม่ */
function tryHumanStackDrop() {
if (!stackHumanDropGateOkPlay()) return false;
ensureStackNextDropVisual();
const hit = computeStackDropResult(stackMini);
if (!startStackFallAnimation(hit, previewFillBots ? { human: true } : null)) return false;
if (previewFillBots) {
@@ -8004,6 +8325,7 @@
/** บอท preview: ปล่อยแบบมีแอนิเมชันร่วงเหมือนผู้เล่น — apply หลังแอนิเมชันจบ */
function simulatePreviewBotStackDrop(botId, o) {
if (!stackMini || stackFall || stackMini.settling) return;
ensureStackNextDropVisual();
previewBotAlignStackSwingPhasePlay(stackMini, o);
const hit = computeStackDropResult(stackMini);
if (!startStackFallAnimation(hit, previewFillBots ? { botId } : null)) return;
@@ -8179,16 +8501,13 @@
const hue = (i * 47) % 360;
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);
}
drawStackTowerPerfectYellowGlowBehindBlockPlay(ctx, sx, sy, drawW, drawH, ntGlow, L.perfectGlowUntil);
}
}
drawStackBlockSpriteOrHue(ctx, sx, sy, drawW, drawH, seatL, heavyL, hue, {});
}
if (towerHudLive && m.perfectStackX2Fx) {
const fxX2 = m.perfectStackX2Fx;
@@ -8207,10 +8526,15 @@
const drawWT = lwT * zoom;
const drawHT = layerWorldH * zoom;
const fxImX = ensureStackTowerPerfectFxImagesPlay();
if (fxImX.x2) drawStackTowerPerfectX2OverlayPlay(ctx, fxImX.x2, sxT, syT, drawWT, drawHT);
if (fxImX.x2) {
drawStackTowerPerfectX2ClusterPlay(ctx, fxImX.light, fxImX.x2, sxT, syT, drawWT, drawHT, ntX, fxX2.until);
}
}
}
}
if (towerHudLive) {
drawStackTowerScorePopupsPlay(ctx, worldToScreen, zoom, m, layerWorldH, floorY);
}
if (m.settling) {
const s = m.settling;
const u = Math.min(1, (performance.now() - s.t0) / s.dur);
@@ -8282,6 +8606,9 @@
ctx.lineWidth = 1;
ctx.restore();
}
if (towerHudLive) {
drawStackTowerHeartMinusFxPlay(ctx, worldToScreen, zoom, m, layerWorldH, floorY, nLay);
}
}
function applyGauntletTimingFromServer(payload) {
@@ -8786,7 +9113,8 @@
function gauntletCrownSyncGuestReadyIfNeeded() {
const inQuizQuestionHowto = isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'howto';
if (gauntletCrownPregamePhase !== 'howto' && !inQuizQuestionHowto) return;
const inStackTowerHowto = isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'howto';
if (gauntletCrownPregamePhase !== 'howto' && !inQuizQuestionHowto && !inStackTowerHowto) return;
if (myId == null || isMePlayHost()) return;
const sid = String(myId);
if (gauntletCrownLobbyReadyMap[sid]) return;
@@ -12330,11 +12658,13 @@
if (previewFillBots) rebalancePreviewBots();
if (quizCarryPregameActive && isQuizCarry()) updateQuizCarryPregameHud();
if (isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'howto') updateQuizQuestionMissionHowtoHud();
if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'howto') updateStackTowerMissionHowtoHud();
});
socket.on('host-changed', (d) => {
if (d && d.hostId != null) playHostId = d.hostId;
if (quizCarryPregameActive && isQuizCarry()) updateQuizCarryPregameHud();
if (isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'howto') updateQuizQuestionMissionHowtoHud();
if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'howto') updateStackTowerMissionHowtoHud();
});
socket.on('quiz-carry-lobby-sync', (d) => {
if (!d || typeof d.readyMap !== 'object') return;
@@ -12357,6 +12687,9 @@
} else if (isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'howto') {
gauntletCrownSyncGuestReadyIfNeeded();
updateQuizQuestionMissionHowtoHud();
} else if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'howto') {
gauntletCrownSyncGuestReadyIfNeeded();
updateStackTowerMissionHowtoHud();
}
});
socket.on('gauntlet-crown-lobby-started', () => {
@@ -12364,6 +12697,10 @@
beginQuizQuestionMissionCountdownThenRun();
return;
}
if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'howto') {
beginStackTowerMissionCountdownThenRun();
return;
}
if (!usesCrownLobbyShellPlay()) return;
beginGauntletCrownCountdownThenRun();
});
@@ -12471,6 +12808,7 @@
others.delete(data.id);
if (previewFillBots) rebalancePreviewBots();
if (isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'howto') updateQuizQuestionMissionHowtoHud();
if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'howto') updateStackTowerMissionHowtoHud();
});
socket.on('chat', (data) => {
const box = document.getElementById('chat-messages');
@@ -13954,7 +14292,11 @@
ctx.fillText('TEAM ' + teamScore + ' · COMBO x' + combo, rightX + rightW / 2, topY + bigAv + 96);
const termW = Math.min(380, Math.max(220, cw * 0.42));
const termH = 86;
const logLineH = 12;
const logMaxLines = 5;
const logBlockH = logMaxLines * logLineH;
/** หัวข้อ + บล็อกล็อก + ช่องว่าง + ป้าย TOWER_LINK + แถบ — อย่าให้ทับกัน (เดิม termH=86 แคบเกิน) */
const termH = Math.max(118, 22 + logBlockH + 10 + 14 + 8 + 10);
const termY = Math.max(boardH + 24, ch - termH - 52);
ctx.fillStyle = 'rgba(18, 8, 32, 0.88)';
roundRectPath(pad, termY, termW, termH, 6);
@@ -13971,27 +14313,34 @@
ctx.fillText('// NODE_LOG', pad + 10, termY + 8);
ctx.fillStyle = '#7dcfff';
ctx.font = monoSm;
let ly = termY + 26;
const logTop = termY + 24;
let ly = logTop;
const logLines = stackPreviewHudLog.length
? stackPreviewHudLog.slice(-5)
? stackPreviewHudLog.slice(-logMaxLines)
: ['>>> Awaiting stack sync…', '>>> Space / click = release (your turn only)'];
logLines.forEach((ln) => {
ctx.fillText(ln, pad + 10, ly);
ly += 12;
ly += logLineH;
});
const decryptPct = stackTowerDecryptPctPlay();
const barY = termY + termH - 18;
const barW = termW - 20;
const barPadX = 10;
const barH = 8;
const barW = termW - barPadX * 2;
const barY = termY + termH - barH - 10;
const labelY = barY - 14;
if (labelY >= logTop + 4) {
ctx.fillStyle = '#94e2d5';
ctx.font = monoSm;
ctx.textBaseline = 'top';
ctx.fillText('TOWER_LINK ' + decryptPct + '%', pad + barPadX, labelY);
}
ctx.fillStyle = 'rgba(0,0,0,0.35)';
ctx.fillRect(pad + 10, barY, barW, 8);
ctx.fillRect(pad + barPadX, barY, barW, barH);
ctx.fillStyle = 'rgba(0, 255, 200, 0.65)';
ctx.fillRect(pad + 10, barY, Math.max(4, barW * decryptPct / 100), 8);
ctx.fillRect(pad + barPadX, barY, Math.max(4, barW * decryptPct / 100), barH);
ctx.strokeStyle = 'rgba(0, 234, 255, 0.4)';
ctx.strokeRect(pad + 10, barY, barW, 8);
ctx.fillStyle = '#94e2d5';
ctx.font = monoSm;
ctx.fillText('TOWER_LINK ' + decryptPct + '%', pad + 10, barY - 14);
ctx.strokeRect(pad + barPadX, barY, barW, barH);
ctx.restore();
}
@@ -15366,6 +15715,7 @@
syncPlayCyberHud();
syncQuizCarryGrabButton();
syncGauntletCrownJumpButton();
syncStackTowerDropButtonPlay();
}
document.addEventListener('keydown', (e) => {
@@ -15756,6 +16106,17 @@
});
})();
(function wireStackTowerDropButton() {
const btn = document.getElementById('stack-tower-drop-btn');
if (!btn) return;
btn.addEventListener('click', (ev) => {
ev.preventDefault();
if (!mapData || !isStack() || isChatFocused()) return;
if (!btn.classList.contains('stack-tower-drop-btn--active')) return;
tryHumanStackDrop();
});
})();
(function wireGauntletCrownHowtoPrimary() {
const btn = document.getElementById('btn-gch-ready');
if (!btn) return;
@@ -15807,8 +16168,32 @@
if (isStackTowerMissionUiMapPlay()) {
if (stackTowerMissionPhase !== 'howto') return;
const humansSt = quizCarryPregameHumanIds();
if (!(humansSt.length === 1 || isMePlayHost())) return;
beginStackTowerMissionCountdownThenRun();
const totPlayersSt = Math.max(1, quizCarryPregameTotalPlayers());
if (totPlayersSt === 1) {
if (!(humansSt.length === 1 || isMePlayHost())) return;
beginStackTowerMissionCountdownThenRun();
return;
}
if (!isMePlayHost()) {
if (myId == null) return;
const sidSt = String(myId);
const nextSt = !gauntletCrownLobbyReadyMap[sidSt];
gauntletCrownLobbyReadyMap[sidSt] = nextSt;
updateStackTowerMissionHowtoHud();
if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-ready', { ready: nextSt });
return;
}
if (myId == null) return;
const humansReadySt = humansSt.length > 0 && humansSt.every((id) => !!gauntletCrownLobbyReadyMap[id]);
if (humansReadySt) {
socket.emit('gauntlet-crown-lobby-start', {}, (r) => { if (!r || !r.ok) { /* ignore */ } });
return;
}
const sidHostSt = String(myId);
const nextHostSt = !gauntletCrownLobbyReadyMap[sidHostSt];
gauntletCrownLobbyReadyMap[sidHostSt] = nextHostSt;
updateStackTowerMissionHowtoHud();
if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-ready', { ready: nextHostSt });
return;
}
if (!usesCrownLobbyShellPlay()) return;
+1 -1
View File
@@ -1,6 +1,6 @@
// ทุกครั้งที่มีการเพิ่มหรือเปลี่ยน ให้เพิ่ม v +0.0001
// หลังแก้ค่าแล้วต้อง copy ไป path ที่ Nginx ชี้ (หรือรัน copy-frogger-files-only.sh) ถึงจะเห็นบนเว็บ
window.APP_VERSION = '0.0253';
window.APP_VERSION = '0.0258';
document.addEventListener('DOMContentLoaded', function () {
var t = document.querySelector('.version-tag');
if (t) t.textContent = 'v ' + window.APP_VERSION;
+81 -46
View File
@@ -188,6 +188,45 @@
pointer-events: none;
user-select: none;
}
/* Stack Tower (mnn93hpi) — ปุ่มปล่อยบล็อก มุมขวาล่าง · เทียบ Space / Enter */
#stack-tower-drop-btn {
position: absolute;
right: max(10px, env(safe-area-inset-right, 0px));
bottom: max(10px, env(safe-area-inset-bottom, 0px));
z-index: 60;
width: min(14vh, 16vw, 200px);
height: min(14vh, 16vw, 200px);
padding: 0;
margin: 0;
border: none;
border-radius: 50%;
background: transparent;
cursor: pointer;
opacity: 0.4;
transition: transform 0.15s ease, opacity 0.2s ease, filter 0.15s ease;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
box-shadow: none;
filter: drop-shadow(0 0 14px rgba(187, 100, 255, 0.5));
}
#stack-tower-drop-btn.is-hidden {
display: none !important;
}
#stack-tower-drop-btn.stack-tower-drop-btn--active {
opacity: 1;
cursor: pointer;
}
#stack-tower-drop-btn.stack-tower-drop-btn--active:active {
transform: scale(0.96);
}
#stack-tower-drop-btn img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
pointer-events: none;
user-select: none;
}
#quiz-game-overlay {
position: fixed;
left: 50%;
@@ -1040,9 +1079,41 @@
backdrop-filter: blur(3px);
}
#gauntlet-crown-mission-overlay { z-index: 10150; }
/* Stack Tower (mnn93hpi) — แฟลชผลจบ 5 วิ ก่อน GCM */
#stack-tower-result-flash {
position: fixed;
inset: 0;
z-index: 10160;
display: flex;
align-items: center;
justify-content: center;
padding: max(10px, env(safe-area-inset-top)) max(10px, env(safe-area-inset-right)) max(10px, env(safe-area-inset-bottom)) max(10px, env(safe-area-inset-left));
box-sizing: border-box;
pointer-events: none;
}
#stack-tower-result-flash.is-hidden {
display: none !important;
}
.stack-tower-result-flash-backdrop {
position: absolute;
inset: 0;
background: rgba(4, 8, 22, 0.58);
backdrop-filter: blur(4px);
}
#stack-tower-result-flash-img {
position: relative;
z-index: 1;
max-width: min(94vw, 980px);
max-height: min(82vh, 640px);
width: auto;
height: auto;
object-fit: contain;
filter: drop-shadow(0 0 28px rgba(0, 0, 0, 0.55));
}
#gauntlet-crown-howto-overlay.is-hidden,
#gauntlet-crown-mission-overlay.is-hidden,
#gauntlet-crown-countdown.is-hidden { display: none !important; }
#gauntlet-crown-countdown.is-hidden,
#stack-tower-result-flash.is-hidden { display: none !important; }
.gcc-panel {
text-align: center;
padding: 0;
@@ -1154,37 +1225,6 @@
flex-shrink: 0;
margin: 0;
}
#stack-tower-howto-rules.stack-tower-howto-rules {
pointer-events: none;
max-height: none;
overflow: visible;
width: min(98%, 440px);
margin: 0 auto 6px;
padding: 8px 10px;
text-align: left;
font-size: clamp(0.62rem, 1.45vw, 0.74rem);
line-height: 1.42;
color: #d5e6ff;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.92);
background: rgba(6, 10, 24, 0.88);
border: 1px solid rgba(0, 234, 255, 0.38);
border-radius: 8px;
box-sizing: border-box;
display: none;
visibility: hidden;
}
.stack-tower-howto-rules h4 {
margin: 8px 0 4px;
font-size: 0.92em;
color: #7af8ff;
font-weight: 800;
letter-spacing: 0.04em;
}
.stack-tower-howto-rules h4:first-child { margin-top: 0; }
.stack-tower-howto-rules ul { margin: 0; padding-left: 1.15em; }
.stack-tower-howto-rules li { margin-bottom: 3px; }
.stack-tower-howto-rules .warn { color: #f6e08a; font-weight: 700; }
.stack-tower-howto-rules.is-hidden { display: none !important; }
.gch-title {
margin: 0 0 4px;
font-size: clamp(1rem, 3.2vw, 1.35rem);
@@ -2510,6 +2550,9 @@
<button type="button" id="gauntlet-crown-jump-btn" class="is-hidden" aria-label="กระโดด (Jump)" title="กระโดด — เหมือน Space / W / ↑" aria-hidden="true">
<img src="/Game/img/gauntlet-assets/btn-jump.png" alt="" width="256" height="256" decoding="async" />
</button>
<button type="button" id="stack-tower-drop-btn" class="is-hidden" aria-label="ปล่อยบล็อก (Drop)" title="ปล่อยบล็อก — เหมือน Space / Enter" aria-hidden="true">
<img src="/Game/img/TowerBlock/btn-drop.png" alt="" width="256" height="256" decoding="async" />
</button>
<div id="quiz-map-question-panel" class="is-hidden" aria-hidden="true">
<img id="quiz-map-q-feedback-icon" class="quiz-map-q-feedback-icon is-hidden" alt="" width="64" height="64" decoding="async" />
<div id="quiz-map-question-kicker" class="quiz-map-question-kicker is-hidden" aria-hidden="true"></div>
@@ -2630,18 +2673,6 @@
<img class="gch-bg" src="/Game/img/gauntlet-assets/popup-Howto.png" alt="" width="920" height="520" decoding="async" />
<div class="gch-inner gch-inner--art">
<div class="gch-art-footer">
<div id="stack-tower-howto-rules" class="stack-tower-howto-rules is-hidden" aria-hidden="true">
<h4>กติกา / How to</h4>
<ul>
<li>วางชิ้นส่วนให้ตรงตำแหน่งมากที่สุด</li>
<li>แตะ / ปล่อย — <strong>Space · Enter · คลิก</strong> เพื่อวางชิ้นส่วนลงบนฐาน</li>
<li>วางตรง = ได้คะแนน + ไปต่อ · วางแม่นต่อเนื่อง = <strong>คอมโบ ×2</strong> (คะแนน + Progress)</li>
</ul>
<h4>เวลา &amp; โอกาส</h4>
<ul>
<li class="warn">พลาดครบหรือหมดเวลา = เกมจบ</li>
</ul>
</div>
<p id="gauntlet-crown-howto-status" class="gch-status is-hidden" aria-live="polite"></p>
<button type="button" class="btn-gch-ready" id="btn-gch-ready" title="READY"><span class="sr-only">READY</span></button>
</div>
@@ -2653,6 +2684,10 @@
<img id="gauntlet-crown-countdown-num" class="gcc-num" src="/Game/img/QUESTION/3.png" width="240" height="240" alt="" decoding="async" />
</div>
</div>
<div id="stack-tower-result-flash" class="is-hidden" role="dialog" aria-modal="true" aria-hidden="true" aria-label="ผลภารกิจ Tower">
<div class="stack-tower-result-flash-backdrop" aria-hidden="true"></div>
<img id="stack-tower-result-flash-img" src="" alt="" width="920" height="520" decoding="async" />
</div>
<div id="gauntlet-crown-mission-overlay" class="is-hidden" role="dialog" aria-modal="true" aria-labelledby="gcm-heading">
<div class="gcm-backdrop" aria-hidden="true"></div>
<div class="gcm-shell">
@@ -2713,8 +2748,8 @@
</div>
</div>
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0253"></script>
<script src="js/play.js?v=0.0253"></script>
<script src="js/version.js?v=0.0258"></script>
<script src="js/play.js?v=0.0258"></script>
<div class="version-tag">v —</div>
</body>
</html>
+13 -5
View File
@@ -13,6 +13,8 @@ const adminTokens = new Set();
const POST_CASE_LOBBY_SPACE_ID = 'mn8nx46h';
/** หลัง host เลือกผู้ต้องสงสัยแล้วกด «เริ่มสืบสวน» — ย้ายทุกคนไปฉากควิซนี้และเริ่มเกม */
const SUSPECT_INVESTIGATION_QUIZ_MAP_ID = 'mng8a80o';
/** Stack Tower ภารกิจ — lobby READY / START เดียวกับ quiz mission (mng8a80o) */
const STACK_TOWER_MISSION_MAP_ID = 'mnn93hpi';
/** ห้องสาธารณะที่สร้างแล้วไม่มีคนเข้าเกินนี้ → ลบออกจาก memory (Join Room ไม่โชว์ห้องผี) */
const SPACE_EMPTY_TTL_MS = 45000;
// Chat completion models from https://developers.openai.com/api/docs/models (Feb 2025)
@@ -2915,8 +2917,14 @@ function isQuizQuestionMissionShellSpace(space) {
return !!(md && md.gameType === 'quiz');
}
function isStackTowerMissionShellSpace(space) {
if (!space || !space.mapId || String(space.mapId) !== STACK_TOWER_MISSION_MAP_ID) return false;
const md = (space.mapId && maps.get(space.mapId)) || space.mapData;
return !!(md && md.gameType === 'stack');
}
function isCrownLobbyShellSpace(space) {
return isGauntletCrownHeistMapBySpace(space) || isBalloonBossMissionShellSpace(space) || isQuizQuestionMissionShellSpace(space);
return isGauntletCrownHeistMapBySpace(space) || isBalloonBossMissionShellSpace(space) || isQuizQuestionMissionShellSpace(space) || isStackTowerMissionShellSpace(space);
}
function newBalloonBossShellGauntletRunState() {
@@ -3710,7 +3718,7 @@ io.on('connection', (socket) => {
io.to(spaceId).emit('gauntlet-crown-lobby-sync', { readyMap: { ...space.gauntletCrownLobbyReady } });
}
startGauntletTicker(spaceId, space);
} else if (isBalloonBossMissionShellSpace(space) || isQuizQuestionMissionShellSpace(space)) {
} else if (isBalloonBossMissionShellSpace(space) || isQuizQuestionMissionShellSpace(space) || isStackTowerMissionShellSpace(space)) {
if (!space.gauntletCrownLobbyReady || typeof space.gauntletCrownLobbyReady !== 'object') space.gauntletCrownLobbyReady = {};
space.gauntletCrownLobbyReady[socket.id] = false;
if (!space.gauntletRun) space.gauntletRun = newBalloonBossShellGauntletRunState();
@@ -3759,7 +3767,7 @@ io.on('connection', (socket) => {
if (mdJoin.gameType === 'gauntlet' && space.gauntletRun) {
joinCb.gauntletEndsAt = space.gauntletRun.endsAt != null ? space.gauntletRun.endsAt : null;
}
if ((isBalloonBossMissionShellSpace(space) || isQuizQuestionMissionShellSpace(space)) && space.gauntletRun) {
if ((isBalloonBossMissionShellSpace(space) || isQuizQuestionMissionShellSpace(space) || isStackTowerMissionShellSpace(space)) && space.gauntletRun) {
joinCb.gauntletCrownRunHeld = !!space.gauntletRun.crownRunHeld;
}
if (typeof cb === 'function') cb(joinCb);
@@ -4024,7 +4032,7 @@ io.on('connection', (socket) => {
gauntletEliminated: !!p.gauntletEliminated,
}));
}
if (md && (isBalloonBossMissionShellSpace(space) || isQuizQuestionMissionShellSpace(space))) {
if (md && (isBalloonBossMissionShellSpace(space) || isQuizQuestionMissionShellSpace(space) || isStackTowerMissionShellSpace(space))) {
if (!space.gauntletCrownLobbyReady || typeof space.gauntletCrownLobbyReady !== 'object') space.gauntletCrownLobbyReady = {};
space.peers.forEach((_p, id) => {
space.gauntletCrownLobbyReady[id] = false;
@@ -4039,7 +4047,7 @@ io.on('connection', (socket) => {
peersSnap: peersSnap || undefined,
};
if (peersSnap) gameStartPayload.gauntletEndsAt = gauntletEndsAtEmit != null ? gauntletEndsAtEmit : null;
if (mapId && maps.has(mapId) && (isBalloonBossMissionShellSpace(space) || isQuizQuestionMissionShellSpace(space))) {
if (mapId && maps.has(mapId) && (isBalloonBossMissionShellSpace(space) || isQuizQuestionMissionShellSpace(space) || isStackTowerMissionShellSpace(space))) {
const grs = space.gauntletRun;
gameStartPayload.gauntletCrownRunHeld = !!(grs && grs.crownRunHeld);
}