minigame 3add more design 1.5
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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,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;
|
||||
|
||||
@@ -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>เวลา & โอกาส</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
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user