minigame 5 jumper 1.3

This commit is contained in:
2026-05-01 08:04:36 +00:00
parent a22f73ec5f
commit 06a16bb837
56 changed files with 558 additions and 40 deletions
+5
View File
@@ -38,6 +38,11 @@ JUMPER="$DST/Game/public/img/Jumper"
mkdir -p "$JUMPER"
chown www-data:www-data "$JUMPER"
chmod 2775 "$JUMPER"
# Space shooter / Violent Crime — ชื่อโฟลเดอร์ไม่มีช่องว่าง (FTP MKD "Violent Crime" มัก 550)
VIOLENT_CRIME="$DST/Game/public/img/ViolentCrime"
mkdir -p "$VIOLENT_CRIME"
chown www-data:www-data "$VIOLENT_CRIME"
chmod 2775 "$VIOLENT_CRIME"
GAME_SRV_HASH_AFTER="$(hash_game_server_js "$GAME_SERVER_JS")"
if [[ -n "$GAME_SRV_HASH_AFTER" && "$GAME_SRV_HASH_BEFORE" != "$GAME_SRV_HASH_AFTER" ]]; then
+39 -2
View File
@@ -2630,6 +2630,38 @@
});
}
function loadSpaceShooterTimingPanel() {
gameTimingFetch('GET')
.then(function (data) {
var inpT = el('space-shooter-mission-sec');
if (inpT) {
var ss = Number(data.spaceShooterMissionTimeSec);
inpT.value = String(Number.isFinite(ss) && ss > 0 ? Math.floor(ss) : 0);
}
setMsg('space-shooter-timing-msg', '', '');
})
.catch(function (e) {
setMsg('space-shooter-timing-msg', e.message || 'โหลดไม่ได้', 'error');
});
}
function saveSpaceShooterTimingPanel() {
var limSs = el('space-shooter-mission-sec') ? parseInt(String(el('space-shooter-mission-sec').value), 10) : 0;
if (Number.isNaN(limSs) || limSs < 0) limSs = 0;
limSs = limSs <= 0 ? 0 : Math.max(10, Math.min(7200, limSs));
gameTimingFetch('GET')
.then(function (data) {
data.spaceShooterMissionTimeSec = limSs;
return gameTimingFetch('PUT', data);
})
.then(function () {
setMsg('space-shooter-timing-msg', 'บันทึกแล้ว — ผู้เล่นยิงยานอวกาศได้ค่าใหม่เมื่อรีเฟรชหน้าเล่นหรือโหลด /api/game-timing', 'ok');
})
.catch(function (e) {
setMsg('space-shooter-timing-msg', e.message || 'บันทึกไม่ได้', 'error');
});
}
function saveJumpSurviveTimingPanel() {
var inp = el('jump-survive-height-mult');
var jumpSurvMult = inp ? parseFloat(String(inp.value)) : 1.5;
@@ -2681,7 +2713,7 @@
return gameTimingFetch('PUT', data);
})
.then(function () {
setMsg('game-timing-msg', 'บันทึกแล้ว — พรมแดง tick·กระโดด / รูปอุปสรรค์ · มินิเกมกระโดดขึ้นแท่นตั้งที่แท็บ Minigame-5 · ค่า Stack แก้ที่แท็บ Stack เท่านั้น · จำกัดเวลาพรมแดงใช้กับรอบถัดไปที่โฮสต์เริ่มเกม', 'ok');
setMsg('game-timing-msg', 'บันทึกแล้ว — พรมแดง tick·กระโดด / รูปอุปสรรค์ · Minigame-5 / Minigame-6 ตั้งแท็บของแต่ละเกม · ค่า Stack แก้ที่แท็บ Stack เท่านั้น · จำกัดเวลาพรมแดงใช้กับรอบถัดไปที่โฮสต์เริ่มเกม', 'ok');
})
.catch(function (e) {
setMsg('game-timing-msg', e.message || 'บันทึกไม่ได้', 'error');
@@ -2725,6 +2757,7 @@
if (name === 'quiz-carry') loadQuizCarryPanel();
if (name === 'quiz-battle') loadQbBattlePanel();
if (name === 'jump-survive') loadJumpSurviveTimingPanel();
if (name === 'space-shooter') loadSpaceShooterTimingPanel();
if (name === 'game-timing') loadGameTimingPanel();
if (name === 'stack-game') loadStackGamePanel();
}
@@ -2850,6 +2883,10 @@
if (btnJumpSurviveSave) {
btnJumpSurviveSave.addEventListener('click', saveJumpSurviveTimingPanel);
}
var btnSpaceShooterSave = el('btn-space-shooter-save');
if (btnSpaceShooterSave) {
btnSpaceShooterSave.addEventListener('click', saveSpaceShooterTimingPanel);
}
var btnStackGameSave = el('btn-stack-game-save');
if (btnStackGameSave) {
btnStackGameSave.addEventListener('click', saveStackGamePanel);
@@ -3160,7 +3197,7 @@
try {
want = new URL(window.location.href).searchParams.get('tab');
} catch (e) { /* ignore */ }
var allowed = { 'change-password': true, oauth: true, 'map-editor': true, quiz: true, 'quiz-carry': true, 'quiz-battle': true, 'jump-survive': true, 'game-timing': true, 'stack-game': true, characters: true, accounts: true, admins: true };
var allowed = { 'change-password': true, oauth: true, 'map-editor': true, quiz: true, 'quiz-carry': true, 'quiz-battle': true, 'jump-survive': true, 'space-shooter': true, 'game-timing': true, 'stack-game': true, characters: true, accounts: true, admins: true };
if (!want || !allowed[want]) return 'oauth';
if (want === 'admins' && (!admin || admin.role !== 'super')) return 'oauth';
return want;
+18 -1
View File
@@ -72,6 +72,7 @@
<button type="button" class="tab" data-tab="quiz-carry" role="tab" id="tab-quiz-carry" aria-controls="tab-panel-quiz-carry"><span class="tab-label">Minigame-4-คำศัพท์ในกระบวนการยุติธรรม</span><span class="tab-desc" aria-hidden="true"></span></button>
<button type="button" class="tab" data-tab="quiz-battle" role="tab" id="tab-quiz-battle" aria-controls="tab-panel-quiz-battle"><span class="tab-label">Quiz Battle</span><span class="tab-desc">โดม · A B C · หมวด</span></button>
<button type="button" class="tab" data-tab="jump-survive" role="tab" id="tab-jump-survive" aria-controls="tab-panel-jump-survive"><span class="tab-label">Minigame-5-กระโดดขึ้นแท่น</span><span class="tab-desc" aria-hidden="true"></span></button>
<button type="button" class="tab" data-tab="space-shooter" role="tab" id="tab-space-shooter" aria-controls="tab-panel-space-shooter"><span class="tab-label">Minigame-6-ยิงอุกาบาต Violent Crime</span><span class="tab-desc" aria-hidden="true"></span></button>
<button type="button" class="tab" data-tab="game-timing" role="tab" id="tab-game-timing" aria-controls="tab-panel-game-timing"><span class="tab-label">Minigame-2-วิ่งหลบสิ่งกีดขวาง</span><span class="tab-desc" aria-hidden="true"></span></button>
<button type="button" class="tab" data-tab="stack-game" role="tab" id="tab-stack-game" aria-controls="tab-panel-stack-game"><span class="tab-label">Stack</span><span class="tab-desc">สวิง · ความยาวแท่ง</span></button>
<button type="button" class="tab" data-tab="characters" role="tab" id="tab-characters" aria-controls="tab-panel-characters"><span class="tab-label">ตัวละคร</span><span class="tab-desc">อัปโหลด · เลือกใช้ในเกม</span></button>
@@ -464,9 +465,25 @@
<p id="jump-survive-timing-msg" class="msg" role="status"></p>
</section>
<section id="tab-panel-space-shooter" class="tab-panel card" hidden role="tabpanel" aria-labelledby="tab-space-shooter">
<h2>ยิงยานอวกาศ (Violent Crime / space_shooter) — ตั้งค่าเวลา</h2>
<p class="muted">เก็บที่ <code>/Game/data/game-timing.json</code> ฟิลด์ <code>spaceShooterMissionTimeSec</code> · ผู้เล่นได้ค่าใหม่เมื่อโหลดหน้าเล่น / <code>GET /api/game-timing</code> · รูปเกมอัปโหลดไปที่ <code>/Game/public/img/ViolentCrime/</code> (ชื่อโฟลเดอร์<strong>ไม่มีช่องว่าง</strong> — FTP หลายตัวจะ <code>550</code> ถ้าใช้ <code>Violent Crime</code>)</p>
<fieldset class="quiz-timing-fieldset">
<legend>จำกัดเวลารอบ (ยิงอุกาบาต)</legend>
<div class="form-grid form-inline quiz-timing-grid">
<label title="0 = ใช้ค่าเริ่ม 90 วินาทีในเกม · ตั้งอย่างน้อย 10 วินาทีถ้าต้องการกำหนดเอง">เวลารอบ (วินาที) <input type="number" id="space-shooter-mission-sec" min="0" max="7200" step="5" value="0"></label>
</div>
</fieldset>
<p class="muted" style="margin-top:-0.25rem;margin-bottom:0.65rem">นับจากเริ่มรอบในเกม · ถ้าในแมปตั้ง <code>spaceShooterTimeSec</code> &gt; 0 จะใช้ค่าบนแมปแทนทั้งหมด · ตั้ง <code>spaceShooterTimeSec</code> = 0 บนแมป = ไม่จับเวลา</p>
<div class="quiz-admin-actions">
<button type="button" class="btn btn-primary" id="btn-space-shooter-save">บันทึก</button>
</div>
<p id="space-shooter-timing-msg" class="msg" role="status"></p>
</section>
<section id="tab-panel-game-timing" class="tab-panel card" hidden role="tabpanel" aria-labelledby="tab-game-timing">
<h2>เวลาเกม — พรมแดงสุดท้าย (Gauntlet)</h2>
<p class="muted">เก็บที่เซิร์ฟเวอร์เกม <code>/Game/data/game-timing.json</code> · หลังบันทึกห้องที่กำลังเล่นจะปรับความถี่ tick ทันที · ค่าจะส่งไปยังผู้เล่นผ่าน <code>gauntlet-sync</code> · <strong>จำกัดเวลา</strong>นับจากตอนโฮสต์เริ่มเกมพรมแดง — หมดเวลาแล้วเกมจบและส่งทุกคนกลับฉากล็อบบี้บนเซิร์ฟเวอร์ · ถ้าขึ้น Not Found ให้<strong>รีสตาร์ท Node</strong>ที่รัน <code>Game/server.js</code> (เช่น <code>pm2 restart</code>) หลัง deploy · <strong>เวลามินิเกมกระโดดขึ้นแท่น</strong>ตั้งที่แท็บ Minigame-5</p>
<p class="muted">เก็บที่เซิร์ฟเวอร์เกม <code>/Game/data/game-timing.json</code> · หลังบันทึกห้องที่กำลังเล่นจะปรับความถี่ tick ทันที · ค่าจะส่งไปยังผู้เล่นผ่าน <code>gauntlet-sync</code> · <strong>จำกัดเวลา</strong>นับจากตอนโฮสต์เริ่มเกมพรมแดง — หมดเวลาแล้วเกมจบและส่งทุกคนกลับฉากล็อบบี้บนเซิร์ฟเวอร์ · ถ้าขึ้น Not Found ให้<strong>รีสตาร์ท Node</strong>ที่รัน <code>Game/server.js</code> (เช่น <code>pm2 restart</code>) หลัง deploy · <strong>เวลามินิเกมกระโดดขึ้นแท่น</strong>ตั้งที่แท็บ Minigame-5 · <strong>เวลายิงอุกาบาต</strong>ตั้งที่แท็บ Minigame-6</p>
<fieldset class="quiz-timing-fieldset">
<legend>พารามิเตอร์</legend>
<div class="form-grid form-inline quiz-timing-grid">
+2 -1
View File
@@ -17,5 +17,6 @@
"stackSwingHz": 0.1,
"stackBlockWidthTiles": 3.2,
"jumpSurviveJumpHeightMult": 1.5,
"jumpSurviveMissionTimeSec": 0
"jumpSurviveMissionTimeSec": 0,
"spaceShooterMissionTimeSec": 0
}
File diff suppressed because one or more lines are too long
@@ -44,6 +44,7 @@
<nav class="admin-tabs" role="tablist" aria-label="เมนูหลัก Admin">
<button type="button" role="tab" id="tab-jump" aria-selected="true" aria-controls="panel-jump" data-panel="panel-jump">กระโดดให้รอด</button>
<button type="button" role="tab" id="tab-shooter" aria-selected="false" aria-controls="panel-shooter" data-panel="panel-shooter">ยิงอุกาบาต · Violent Crime</button>
<button type="button" role="tab" id="tab-gauntlet" aria-selected="false" aria-controls="panel-gauntlet" data-panel="panel-gauntlet">พรมแดง · tick &amp; กระโดด</button>
<button type="button" role="tab" id="tab-stack" aria-selected="false" aria-controls="panel-stack" data-panel="panel-stack">Stack · สวิง</button>
</nav>
@@ -62,6 +63,15 @@
</div>
</div>
<div id="panel-shooter" class="panel" role="tabpanel" aria-labelledby="tab-shooter" hidden>
<h2>ยิงยานอวกาศ (space_shooter)</h2>
<div class="row">
<label for="spaceShooterMissionTimeSec">spaceShooterMissionTimeSec — เวลารอบ (วินาที · 0 = เกมใช้ค่าเริ่ม 90)</label>
<input type="number" id="spaceShooterMissionTimeSec" min="0" max="7200" step="5" value="0">
<p class="hint">ถ้าในแมปตั้ง <code>spaceShooterTimeSec</code> &gt; 0 จะใช้ค่าบนแมปแทน · ตั้งเป็น 0 บนแมป = ไม่จับเวลา</p>
</div>
</div>
<div id="panel-gauntlet" class="panel" role="tabpanel" aria-labelledby="tab-gauntlet" hidden>
<h2>พรมแดง (Gauntlet)</h2>
<div class="row">
@@ -102,6 +112,7 @@
const tabs = document.querySelectorAll('nav.admin-tabs [role="tab"]');
const panels = {
'panel-jump': document.getElementById('panel-jump'),
'panel-shooter': document.getElementById('panel-shooter'),
'panel-gauntlet': document.getElementById('panel-gauntlet'),
'panel-stack': document.getElementById('panel-stack'),
};
@@ -126,6 +137,8 @@
document.getElementById('jumpSurviveJumpHeightMult').value = timing.jumpSurviveJumpHeightMult != null ? timing.jumpSurviveJumpHeightMult : 1.5;
const jms = Number(timing.jumpSurviveMissionTimeSec);
document.getElementById('jumpSurviveMissionTimeSec').value = Number.isFinite(jms) && jms > 0 ? Math.floor(jms) : 0;
const sms = Number(timing.spaceShooterMissionTimeSec);
document.getElementById('spaceShooterMissionTimeSec').value = Number.isFinite(sms) && sms > 0 ? Math.floor(sms) : 0;
document.getElementById('gauntletTickMs').value = timing.gauntletTickMs != null ? timing.gauntletTickMs : 150;
document.getElementById('gauntletJumpTicks').value = timing.gauntletJumpTicks != null ? timing.gauntletJumpTicks : 5;
document.getElementById('gauntletTimeLimitSec').value = timing.gauntletTimeLimitSec != null ? timing.gauntletTimeLimitSec : 0;
@@ -139,10 +152,14 @@
let jmt = parseInt(document.getElementById('jumpSurviveMissionTimeSec').value, 10);
if (Number.isNaN(jmt) || jmt < 0) jmt = 0;
jmt = jmt <= 0 ? 0 : Math.max(10, Math.min(7200, jmt));
let sst = parseInt(document.getElementById('spaceShooterMissionTimeSec').value, 10);
if (Number.isNaN(sst) || sst < 0) sst = 0;
sst = sst <= 0 ? 0 : Math.max(10, Math.min(7200, sst));
return {
...timing,
jumpSurviveJumpHeightMult: Number.isFinite(jm) ? jm : 1.5,
jumpSurviveMissionTimeSec: jmt,
spaceShooterMissionTimeSec: sst,
gauntletTickMs: parseInt(document.getElementById('gauntletTickMs').value, 10),
gauntletJumpTicks: parseInt(document.getElementById('gauntletJumpTicks').value, 10),
gauntletTimeLimitSec: parseInt(document.getElementById('gauntletTimeLimitSec').value, 10),
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

@@ -0,0 +1,19 @@
Violent Crime / space_shooter — รูปเกม
========================================
ใช้โฟลเดอร์นี้ชื่อ ViolentCrime (camelCase, ไม่มีช่องว่าง)
เหตุผล: คำสั่ง FTP MKD สำหรับโฟลเดอร์ที่มีช่องว่าง เช่น "Violent Crime"
มักได้รหัส 550 Create directory operation failed บนเซิร์ฟเวอร์หลายแบบ
อัปโหลดรูปผ่าน FTP/SFTP มาที่:
/Game/public/img/ViolentCrime/
English: Use this folder name without spaces; many FTP servers reject MKD with spaces in the path.
ไฟล์ที่ client อ้างอิง (ชื่อตรงนี้):
popup-Howto.png — overlay วิธีเล่น + READY
popup-result.png — พื้นหลังป๊อปอัปสรุปภารกิจ
result-timeup.png — (embed) ข้อความ TIME'S UP บนโต๊ะ หลังสรุป — ถ้าไม่มีจะ fallback quiz-carry
result-complete.png — (embed) หลัง timeup ถ้าผ่านเกณฑ์ — fallback quiz-carry
result-gameover.png — (embed) หลัง timeup ถ้าไม่ผ่าน — fallback quiz-carry
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

+418 -23
View File
@@ -23,6 +23,8 @@
const GAUNTLET_FACE_RIGHT_MAP_ID = 'mno9kb07';
/** Jump Survival ฉาก Jumper — UI สรุปภารกิจแบบ crown + รูปใน /img/Jumper/ (editor ?map=mnptfts2) */
const JUMP_SURVIVE_MISSION_MAP_ID = 'mnptfts2';
/** Space shooter ฉาก Violent Crime — flow เดียวกับ crown/howto (รูปใน /img/ViolentCrime/) */
const SPACE_SHOOTER_MISSION_MAP_ID = 'mnpz6rkp';
/** จนกว่าโหลด /api/characters — placeholder ชั่วคราวก่อน join */
const LEGACY_PLACEHOLDER_CHARACTER_ID = 'Chatest';
let firstCharacterDefaultResolved = null;
@@ -692,6 +694,8 @@
let playJumpSurviveJumpHeightMult = 1.5;
/** จำกัดเวลารอบภารกิจกระโดดขึ้นแท่น (วินาที) — จาก GET /api/game-timing; 0 = ใช้ค่าเริ่ม 60 ในเกม */
let playJumpSurviveMissionTimeSec = 0;
/** ยิงยานอวกาศ (space_shooter): เวลารอบจาก game-timing; 0 = ใช้ค่าเริ่ม 90 ในเกมถ้าแมปไม่ทับ */
let playSpaceShooterMissionTimeSec = 0;
/** 0 = ไม่จำกัด — จาก game-timing / gauntlet-sync (พรมแดง Gauntlet เท่านั้น) */
let gauntletRuntimeTimeLimitSec = 0;
/** เวลาสิ้นสุดรอบ (epoch ms) จากเซิร์ฟเวอร์ — null = ไม่จับเวลา */
@@ -837,6 +841,8 @@
let spaceShooterSessionStartMs = 0;
let spaceShooterLastMoveEmit = 0;
let spaceShooterGameEnded = false;
let spaceShooterMissionPhase = null;
let spaceShooterMissionCountdownTimer = null;
/** balloon_boss — ลูกโป้งยิงบอส (Mega Virus) */
let balloonBossPendingShots = [];
@@ -2832,6 +2838,272 @@
showGauntletCrownMissionOverlay(mission);
}
function isSpaceShooter() { return mapData && mapData.gameType === 'space_shooter'; }
function isSpaceShooterMissionUiMapPlay() {
return isSpaceShooter() && currentPlayMapId() === SPACE_SHOOTER_MISSION_MAP_ID;
}
function isSpaceShooterMissionPregameBlockingPlay() {
if (!isSpaceShooterMissionUiMapPlay()) return false;
return spaceShooterMissionPhase != null && spaceShooterMissionPhase !== 'live' && spaceShooterMissionPhase !== 'ended';
}
function violentCrimeAssetUrl(file) {
return BASE + '/img/ViolentCrime/' + String(file || '').replace(/^\//, '');
}
function applySpaceShooterMissionPanelImages() {
const howtoBg = document.querySelector('#gauntlet-crown-howto-overlay .gch-bg');
if (howtoBg) {
howtoBg.src = violentCrimeAssetUrl('popup-Howto.png');
howtoBg.onerror = function () {
this.onerror = null;
this.src = BASE + '/img/gauntlet-assets/popup-Howto.png';
};
}
const resBg = document.querySelector('#gauntlet-crown-mission-overlay .gcm-bg');
if (resBg) {
resBg.src = violentCrimeAssetUrl('popup-result.png');
resBg.onerror = function () {
this.onerror = null;
this.src = BASE + '/img/gauntlet-assets/popup-result.png';
};
}
}
function showSpaceShooterMissionHowtoOverlay() {
if (!isSpaceShooterMissionUiMapPlay()) return;
hideConflictingOverlaysForGauntletCrown();
applySpaceShooterMissionPanelImages();
if (spaceShooterMissionCountdownTimer) {
clearTimeout(spaceShooterMissionCountdownTimer);
spaceShooterMissionCountdownTimer = null;
}
const cd = document.getElementById('gauntlet-crown-countdown');
if (cd) cd.classList.add('is-hidden');
spaceShooterMissionPhase = 'howto';
const ov = document.getElementById('gauntlet-crown-howto-overlay');
if (!ov) return;
gauntletCrownHowtoVisible = true;
ov.classList.remove('is-hidden');
const st = document.getElementById('gauntlet-crown-howto-status');
if (st) {
st.textContent = '';
st.classList.remove('gch-status--jumper');
st.classList.add('is-hidden');
}
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');
}
}
function beginSpaceShooterMissionCountdownThenRun() {
if (!isSpaceShooterMissionUiMapPlay()) return;
if (spaceShooterMissionCountdownTimer) {
clearTimeout(spaceShooterMissionCountdownTimer);
spaceShooterMissionCountdownTimer = null;
}
spaceShooterMissionPhase = 'countdown';
const howto = document.getElementById('gauntlet-crown-howto-overlay');
if (howto) howto.classList.add('is-hidden');
gauntletCrownHowtoVisible = false;
const st = document.getElementById('gauntlet-crown-howto-status');
if (st) {
st.classList.add('is-hidden');
st.classList.remove('gch-status--jumper');
}
const cd = document.getElementById('gauntlet-crown-countdown');
const numEl = document.getElementById('gauntlet-crown-countdown-num');
const runFinish = function () {
if (cd) cd.classList.add('is-hidden');
if (spaceShooterMissionCountdownTimer) {
clearTimeout(spaceShooterMissionCountdownTimer);
spaceShooterMissionCountdownTimer = null;
}
spaceShooterMissionPhase = 'live';
spaceShooterGameEnded = false;
spaceShooterBullets = [];
spaceShooterAsteroids = [];
spaceShooterPopups = [];
spaceShooterLastTickMs = performance.now();
spaceShooterSpawnAccMs = 0;
spaceShooterFireCd = 0;
spaceShooterSessionStartMs = performance.now();
spaceShooterLastMoveEmit = 0;
if (myId != null) me.spaceShooterScore = 0;
others.forEach(function (o) {
if (o) o.spaceShooterScore = 0;
});
if (mapData) applySpaceShooterSpawnLayoutPlay();
};
if (!cd || !numEl) {
runFinish();
return;
}
cd.classList.remove('is-hidden');
let n = 3;
numEl.textContent = String(n);
const step = function () {
n--;
if (n > 0) {
numEl.textContent = String(n);
spaceShooterMissionCountdownTimer = setTimeout(step, 1000);
} else {
spaceShooterMissionCountdownTimer = null;
runFinish();
}
};
spaceShooterMissionCountdownTimer = setTimeout(step, 1000);
}
function spaceShooterBuildMissionPayload() {
const rows = [];
function pushEnt(id, nickname, characterId, score) {
rows.push({
id: id,
nickname: (nickname && String(nickname).trim()) ? String(nickname).trim() : String(id),
characterId: characterId ?? null,
baseScore: Math.max(0, Number(score) || 0),
eliminated: false,
rankBonus: 0,
finalScore: Math.max(0, Number(score) || 0),
});
}
if (myId != null) {
pushEnt(myId, (me.nickname || nick || 'คุณ').trim() || 'คุณ', me.characterId || getPlayCharacterId(), me.spaceShooterScore);
}
others.forEach(function (o, id) {
if (!o) return;
pushEnt(id, o.nickname, o.characterId, o.spaceShooterScore);
});
rows.sort(function (a, b) {
if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore;
return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id));
});
const top = rows.slice(0, 5);
const ranked = top.map(function (row, idx) {
const pos = idx + 1;
return {
id: row.id,
nickname: row.nickname,
characterId: row.characterId,
baseScore: row.baseScore,
eliminated: false,
rank: pos,
rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos),
rankBonus: 0,
finalScore: row.baseScore,
};
});
const totalParts = ranked.map(function (r) { return r.baseScore; });
const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0);
const n = ranked.length || 1;
const averageScore = Math.floor(totalSum / n);
const grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C';
const rewardCard = gauntletCrownRollRewardCardLocal(grade);
return {
ranked: ranked,
totalSum: totalSum,
averageScore: averageScore,
grade: grade,
rewardCard: rewardCard,
totalParts: totalParts,
uiSkin: 'violent_crime',
survivorCount: ranked.length,
participantCount: rows.length,
};
}
function spaceShooterMergePreviewBotsMission(mission) {
if (!mission || mission.uiSkin !== 'violent_crime' || !previewFillBots || !others || typeof others.forEach !== 'function') return mission;
const seen = new Set();
(mission.ranked || []).forEach(function (r) {
if (r && r.id != null) seen.add(String(r.id));
});
const baseRows = (mission.ranked || []).map(function (r) {
return {
id: r.id,
nickname: r.nickname,
characterId: r.characterId,
baseScore: Math.max(0, Number(r.baseScore) || 0),
eliminated: false,
rankBonus: 0,
finalScore: Math.max(0, Number(r.finalScore != null ? r.finalScore : r.baseScore) || 0),
};
});
others.forEach(function (o, id) {
if (!o || !isPreviewBotId(id)) return;
const sid = String(id);
if (seen.has(sid)) return;
seen.add(sid);
baseRows.push({
id: id,
nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : sid,
characterId: o.characterId ?? null,
baseScore: Math.max(0, Number(o.spaceShooterScore) || 0),
eliminated: false,
rankBonus: 0,
finalScore: Math.max(0, Number(o.spaceShooterScore) || 0),
});
});
baseRows.sort(function (a, b) {
if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore;
return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id));
});
const top = baseRows.slice(0, 5);
const ranked = top.map(function (row, idx) {
const pos = idx + 1;
const bs = Math.max(0, Number(row.baseScore) || 0);
return {
id: row.id,
nickname: row.nickname,
characterId: row.characterId,
baseScore: bs,
eliminated: false,
rank: pos,
rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos),
rankBonus: 0,
finalScore: bs,
};
});
const totalParts = ranked.map(function (r) { return r.baseScore; });
const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0);
const n2 = ranked.length || 1;
const averageScore = Math.floor(totalSum / n2);
const grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C';
return {
ranked: ranked,
totalSum: totalSum,
averageScore: averageScore,
grade: grade,
rewardCard: gauntletCrownRollRewardCardLocal(grade),
totalParts: totalParts,
uiSkin: 'violent_crime',
survivorCount: ranked.length,
participantCount: baseRows.length,
};
}
function endSpaceShooterMissionRound() {
if (!isSpaceShooterMissionUiMapPlay() || spaceShooterGameEnded) return;
spaceShooterGameEnded = true;
spaceShooterMissionPhase = 'ended';
applySpaceShooterMissionPanelImages();
spaceShooterBullets = [];
spaceShooterAsteroids = [];
spaceShooterSpawnAccMs = 0;
spaceShooterPopups = [];
showGauntletCrownMissionOverlay(spaceShooterBuildMissionPayload());
}
function isBalloonBoss() { return mapData && mapData.gameType === 'balloon_boss'; }
function isQuizCarry() { return mapData && mapData.gameType === 'quiz_carry'; }
function isQuizBattle() { return mapData && mapData.gameType === 'quiz_battle'; }
@@ -3957,7 +4229,16 @@
const img = document.getElementById('quiz-carry-result-end-img');
if (!el || !img) return;
const fname = useComplete ? 'result-complete.png' : 'result-gameover.png';
img.src = BASE + '/img/quiz-carry/' + fname + '?v=' + String(Date.now());
if (isSpaceShooterMissionUiMapPlay()) {
img.onerror = function () {
this.onerror = null;
this.src = BASE + '/img/quiz-carry/' + fname + '?v=' + String(Date.now());
};
img.src = violentCrimeAssetUrl(fname) + '?v=' + String(Date.now());
} else {
img.onerror = null;
img.src = BASE + '/img/quiz-carry/' + fname + '?v=' + String(Date.now());
}
el.classList.remove('is-hidden');
el.setAttribute('aria-hidden', 'false');
}
@@ -4009,6 +4290,12 @@
this.src = BASE + '/img/quiz-carry/timeup-txt.png?v=' + String(Date.now());
};
img.src = jumperAssetUrl('result-timeup.png') + '?v=' + String(Date.now());
} else if (isSpaceShooterMissionUiMapPlay()) {
img.onerror = function () {
this.onerror = null;
this.src = BASE + '/img/quiz-carry/timeup-txt.png?v=' + String(Date.now());
};
img.src = violentCrimeAssetUrl('result-timeup.png') + '?v=' + String(Date.now());
} else {
img.onerror = null;
img.src = BASE + '/img/quiz-carry/timeup-txt.png?v=' + String(Date.now());
@@ -5379,6 +5666,14 @@
playJumpSurviveMissionTimeSec = 0;
}
}
if (Object.prototype.hasOwnProperty.call(payload, 'spaceShooterMissionTimeSec')) {
const st = Number(payload.spaceShooterMissionTimeSec);
if (Number.isFinite(st) && st > 0) {
playSpaceShooterMissionTimeSec = Math.max(10, Math.min(7200, Math.floor(st)));
} else {
playSpaceShooterMissionTimeSec = 0;
}
}
if (isStack() && stackMini) reapplyStackMiniSizingFromGlobals();
}
@@ -5755,6 +6050,10 @@
/** Editor embed: quiz_carry / crown ใช้เกรด F = gameover — Jumper: ภาพจบใช้เฉพาะเมื่อไม่มีผู้รอด (0 คน) */
function gauntletCrownEmbedMissionAnyOk(mission) {
if (!mission) return false;
if (mission.uiSkin === 'violent_crime') {
const g = String(mission.grade || '').trim().toUpperCase().charAt(0);
return g !== 'F';
}
if (mission.uiSkin === 'jumper') {
const sc = Number(mission.survivorCount);
return Number.isFinite(sc) ? sc > 0 : (String(mission.grade || '').trim().toUpperCase().charAt(0) !== 'F');
@@ -5792,7 +6091,7 @@
*/
function gauntletCrownBuildDisplayMission(mission) {
if (!mission || !Array.isArray(mission.ranked)) return mission;
if (mission.uiSkin === 'jumper') return mission;
if (mission.uiSkin === 'jumper' || mission.uiSkin === 'violent_crime') return mission;
if (!(previewFillBots && isGauntletCrownHeistMapPlay() && others && typeof others.forEach === 'function')) return mission;
const seen = new Set();
@@ -5875,6 +6174,8 @@
let disp = gauntletCrownBuildDisplayMission(mission);
if (mission.uiSkin === 'jumper') {
disp = jumpSurviveMergePreviewBotsMission(disp) || disp;
} else if (mission.uiSkin === 'violent_crime') {
disp = spaceShooterMergePreviewBotsMission(disp) || disp;
}
if (!disp || !Array.isArray(disp.ranked)) return;
rowEl.innerHTML = '';
@@ -5929,7 +6230,7 @@
nick.textContent = r.nickname || '—';
const sc = document.createElement('div');
sc.className = 'gcm-sc';
const jumpPlain = mission && mission.uiSkin === 'jumper';
const jumpPlain = mission && (mission.uiSkin === 'jumper' || mission.uiSkin === 'violent_crime');
const botScorePlain = previewFillBots && isGauntletCrownHeistMapPlay() && isPreviewBotId(r.id);
sc.textContent = (jumpPlain || botScorePlain)
? String(Math.max(0, Number(r.baseScore) || 0))
@@ -6560,10 +6861,13 @@
const t = Number(mapData && mapData.spaceShooterTimeSec);
if (Number.isFinite(t) && t === 0) return 0;
if (Number.isFinite(t) && t > 0 && t < 7200) return Math.floor(t);
const g = Number(playSpaceShooterMissionTimeSec);
if (Number.isFinite(g) && g > 0 && g < 7200) return Math.floor(g);
return 90;
}
function spaceShooterRemainingSecPlay() {
if (isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase !== 'live') return null;
const lim = spaceShooterTimeLimitSecPlay();
if (lim <= 0) return null;
const elapsed = (performance.now() - spaceShooterSessionStartMs) / 1000;
@@ -6572,6 +6876,10 @@
function endSpaceShooterTimeUp() {
if (spaceShooterGameEnded || !mapData || mapData.gameType !== 'space_shooter') return;
if (isSpaceShooterMissionUiMapPlay()) {
endSpaceShooterMissionRound();
return;
}
spaceShooterGameEnded = true;
spaceShooterBullets = [];
spaceShooterAsteroids = [];
@@ -6631,6 +6939,7 @@
function stepSpaceShooterPreviewBots(dt) {
if (spaceShooterGameEnded || !previewFillBots || !mapData || mapData.gameType !== 'space_shooter') return;
if (isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase !== 'live') return;
const ts = tileSize;
const mw = (mapData.width || 20) * ts;
const mh = (mapData.height || 15) * ts;
@@ -6670,6 +6979,7 @@
function spaceShooterTickFrame() {
if (!mapData || mapData.gameType !== 'space_shooter') return;
if (isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase !== 'live') return;
const w = mapData.width || 20, h = mapData.height || 15;
const ts = tileSize;
const mw = w * ts, mh = h * ts;
@@ -8283,6 +8593,13 @@
if (prevWasSpaceShooter) {
const gend = document.getElementById('gauntlet-ended-overlay');
if (gend) gend.classList.add('is-hidden');
spaceShooterMissionPhase = null;
if (spaceShooterMissionCountdownTimer) {
clearTimeout(spaceShooterMissionCountdownTimer);
spaceShooterMissionCountdownTimer = null;
}
const gccLeave = document.getElementById('gauntlet-crown-countdown');
if (gccLeave) gccLeave.classList.add('is-hidden');
}
}
if (mapData.gameType !== 'balloon_boss') {
@@ -8476,16 +8793,32 @@
}
}
if (mapData.gameType === 'space_shooter') {
spaceShooterGameEnded = false;
normalizeShooterSpawnSlotsInPlay(mapData);
spaceShooterBullets = [];
spaceShooterAsteroids = [];
spaceShooterPopups = [];
spaceShooterLastTickMs = performance.now();
spaceShooterSpawnAccMs = 0;
spaceShooterFireCd = 0;
spaceShooterSessionStartMs = performance.now();
spaceShooterLastMoveEmit = 0;
if (spaceShooterMissionCountdownTimer) {
clearTimeout(spaceShooterMissionCountdownTimer);
spaceShooterMissionCountdownTimer = null;
}
if (isSpaceShooterMissionUiMapPlay()) {
spaceShooterMissionPhase = 'howto';
hideConflictingOverlaysForGauntletCrown();
applySpaceShooterMissionPanelImages();
fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (t) { if (t) applyGauntletTimingFromServer(t); })
.catch(function () {});
setTimeout(function () { showSpaceShooterMissionHowtoOverlay(); }, 50);
} else {
spaceShooterMissionPhase = null;
spaceShooterGameEnded = false;
spaceShooterBullets = [];
spaceShooterAsteroids = [];
spaceShooterPopups = [];
spaceShooterLastTickMs = performance.now();
spaceShooterSpawnAccMs = 0;
spaceShooterFireCd = 0;
spaceShooterSessionStartMs = performance.now();
spaceShooterLastMoveEmit = 0;
}
}
if (mapData.gameType === 'balloon_boss') {
balloonBossGameEnded = false;
@@ -8524,6 +8857,15 @@
if (gccRj) gccRj.classList.add('is-hidden');
gauntletCrownPregamePhase = null;
gauntletCrownLobbyReadyMap = {};
} else if (mapData.gameType === 'space_shooter' && isSpaceShooterMissionUiMapPlay()) {
if (spaceShooterMissionCountdownTimer) {
clearTimeout(spaceShooterMissionCountdownTimer);
spaceShooterMissionCountdownTimer = null;
}
const gccRs = document.getElementById('gauntlet-crown-countdown');
if (gccRs) gccRs.classList.add('is-hidden');
gauntletCrownPregamePhase = null;
gauntletCrownLobbyReadyMap = {};
} else {
const hov = document.getElementById('gauntlet-crown-howto-overlay');
if (hov) {
@@ -8942,6 +9284,11 @@
}
if (mapData.gameType !== 'space_shooter') {
spaceShooterGameEnded = false;
spaceShooterMissionPhase = null;
if (spaceShooterMissionCountdownTimer) {
clearTimeout(spaceShooterMissionCountdownTimer);
spaceShooterMissionCountdownTimer = null;
}
}
if (mapData.gameType !== 'balloon_boss') {
balloonBossGameEnded = false;
@@ -9041,7 +9388,8 @@
}
} else {
gauntletEndsAtMs = null;
const skipHideCrownShell = mapData.gameType === 'jump_survive' && isJumpSurviveMissionUiMapPlay();
const skipHideCrownShell = (mapData.gameType === 'jump_survive' && isJumpSurviveMissionUiMapPlay())
|| (mapData.gameType === 'space_shooter' && isSpaceShooterMissionUiMapPlay());
if (!skipHideCrownShell) {
const hto3 = document.getElementById('gauntlet-crown-howto-overlay');
if (hto3) hto3.classList.add('is-hidden');
@@ -9108,16 +9456,32 @@
}
}
if (mapData.gameType === 'space_shooter') {
spaceShooterGameEnded = false;
normalizeShooterSpawnSlotsInPlay(mapData);
spaceShooterBullets = [];
spaceShooterAsteroids = [];
spaceShooterPopups = [];
spaceShooterLastTickMs = performance.now();
spaceShooterSpawnAccMs = 0;
spaceShooterFireCd = 0;
spaceShooterSessionStartMs = performance.now();
spaceShooterLastMoveEmit = 0;
if (spaceShooterMissionCountdownTimer) {
clearTimeout(spaceShooterMissionCountdownTimer);
spaceShooterMissionCountdownTimer = null;
}
if (isSpaceShooterMissionUiMapPlay()) {
spaceShooterMissionPhase = 'howto';
hideConflictingOverlaysForGauntletCrown();
applySpaceShooterMissionPanelImages();
fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' })
.then((r) => (r.ok ? r.json() : null))
.then((t) => { if (t) applyGauntletTimingFromServer(t); })
.catch(() => {});
setTimeout(function () { showSpaceShooterMissionHowtoOverlay(); }, 60);
} else {
spaceShooterMissionPhase = null;
spaceShooterGameEnded = false;
spaceShooterBullets = [];
spaceShooterAsteroids = [];
spaceShooterPopups = [];
spaceShooterLastTickMs = performance.now();
spaceShooterSpawnAccMs = 0;
spaceShooterFireCd = 0;
spaceShooterSessionStartMs = performance.now();
spaceShooterLastMoveEmit = 0;
}
}
if (mapData.gameType === 'balloon_boss') {
balloonBossGameEnded = false;
@@ -10199,7 +10563,19 @@
timeVal.textContent = '∞';
}
} else if (isSpaceShooter()) {
if (spaceShooterGameEnded) {
if (isSpaceShooterMissionUiMapPlay()) {
if (spaceShooterMissionPhase !== 'live' || spaceShooterGameEnded) {
const limSs = spaceShooterTimeLimitSecPlay();
if (spaceShooterMissionPhase === 'howto' || spaceShooterMissionPhase === 'countdown') {
timeVal.textContent = String(limSs > 0 ? limSs : 0);
} else {
timeVal.textContent = '0';
}
} else {
const remSs = spaceShooterRemainingSecPlay();
timeVal.textContent = remSs != null ? String(remSs) : '∞';
}
} else if (spaceShooterGameEnded) {
timeVal.textContent = '0';
} else {
const rem = spaceShooterRemainingSecPlay();
@@ -11174,6 +11550,12 @@
document.addEventListener('keydown', (e) => {
if (isMovementKey(e.code) && isChatFocused()) return;
if (isSpaceShooter() && mapData && !isChatFocused() && isSpaceShooterMissionPregameBlockingPlay()) {
if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) {
e.preventDefault();
}
return;
}
if ((isSpaceShooter() || isBalloonBoss()) && mapData && !isChatFocused()) {
if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) {
e.preventDefault();
@@ -11291,6 +11673,12 @@
return;
}
if (isSpaceShooter()) {
if (isSpaceShooterMissionPregameBlockingPlay()) {
me.isWalking = false;
draw();
requestAnimationFrame(tick);
return;
}
spaceShooterTickFrame();
draw();
requestAnimationFrame(tick);
@@ -11507,6 +11895,13 @@
beginJumpSurviveMissionCountdownThenRun();
return;
}
if (isSpaceShooterMissionUiMapPlay()) {
if (spaceShooterMissionPhase !== 'howto') return;
const humansSs = quizCarryPregameHumanIds();
if (!(humansSs.length === 1 || isMePlayHost())) return;
beginSpaceShooterMissionCountdownThenRun();
return;
}
if (!isGauntletCrownHeistMapPlay()) return;
if (gauntletCrownPregamePhase !== 'howto') return;
if (myId == null || !isMePlayHost()) return;
+1 -1
View File
@@ -1,6 +1,6 @@
// ทุกครั้งที่มีการเพิ่มหรือเปลี่ยน ให้เพิ่ม v +0.0001
// หลังแก้ค่าแล้วต้อง copy ไป path ที่ Nginx ชี้ (หรือรัน copy-frogger-files-only.sh) ถึงจะเห็นบนเว็บ
window.APP_VERSION = '0.0182';
window.APP_VERSION = '0.0183';
document.addEventListener('DOMContentLoaded', function () {
var t = document.querySelector('.version-tag');
if (t) t.textContent = 'v ' + window.APP_VERSION;
+8 -11
View File
@@ -987,12 +987,9 @@
justify-content: center;
padding: clamp(4px, 1.2vw, 10px);
border-radius: 14px;
border: 2px solid rgba(0, 255, 240, 0.75);
box-shadow:
0 0 0 1px rgba(255, 0, 200, 0.55),
0 0 28px rgba(0, 255, 240, 0.22),
0 0 40px rgba(255, 0, 200, 0.12);
background: rgba(6, 8, 18, 0.35);
/* border: 2px solid rgba(0, 255, 240, 0.75); */
/* box-shadow: 0 0 0 1px rgba(255, 0, 200, 0.55), 0 0 28px rgba(0, 255, 240, 0.22), 0 0 40px rgba(255, 0, 200, 0.12); */
/* background: rgba(6, 8, 18, 0.35); */
pointer-events: auto;
}
.gch-bg,
@@ -1018,10 +1015,10 @@
box-sizing: border-box;
color: #e2e8f0;
font-family: ui-sans-serif, system-ui, 'Segoe UI', 'Kanit', sans-serif;
background: rgba(8, 10, 22, 0.72);
/* background: rgba(8, 10, 22, 0.72); */
border-radius: 12px;
border: 1px solid rgba(0, 255, 240, 0.35);
box-shadow: inset 0 0 24px rgba(0, 0, 0, 0.35);
/* border: 1px solid rgba(0, 255, 240, 0.35); */
/* box-shadow: inset 0 0 24px rgba(0, 0, 0, 0.35); */
}
.gch-inner.gch-inner--art {
left: 0;
@@ -1899,8 +1896,8 @@
</div>
</div>
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0182"></script>
<script src="js/play.js?v=0.210"></script>
<script src="js/version.js?v=0.0183"></script>
<script src="js/play.js?v=0.213"></script>
<div class="version-tag">v —</div>
</body>
</html>
+12
View File
@@ -692,6 +692,8 @@
let playJumpSurviveJumpHeightMult = 1.5;
/** จำกัดเวลารอบภารกิจกระโดดขึ้นแท่น (วินาที) — จาก GET /api/game-timing; 0 = ใช้ค่าเริ่ม 60 ในเกม */
let playJumpSurviveMissionTimeSec = 0;
/** ยิงยานอวกาศ (space_shooter): เวลารอบจาก game-timing; 0 = ใช้ค่าเริ่ม 90 ในเกมถ้าแมปไม่ทับ */
let playSpaceShooterMissionTimeSec = 0;
/** 0 = ไม่จำกัด — จาก game-timing / gauntlet-sync (พรมแดง Gauntlet เท่านั้น) */
let gauntletRuntimeTimeLimitSec = 0;
/** เวลาสิ้นสุดรอบ (epoch ms) จากเซิร์ฟเวอร์ — null = ไม่จับเวลา */
@@ -5379,6 +5381,14 @@
playJumpSurviveMissionTimeSec = 0;
}
}
if (Object.prototype.hasOwnProperty.call(payload, 'spaceShooterMissionTimeSec')) {
const st = Number(payload.spaceShooterMissionTimeSec);
if (Number.isFinite(st) && st > 0) {
playSpaceShooterMissionTimeSec = Math.max(10, Math.min(7200, Math.floor(st)));
} else {
playSpaceShooterMissionTimeSec = 0;
}
}
if (isStack() && stackMini) reapplyStackMiniSizingFromGlobals();
}
@@ -6560,6 +6570,8 @@
const t = Number(mapData && mapData.spaceShooterTimeSec);
if (Number.isFinite(t) && t === 0) return 0;
if (Number.isFinite(t) && t > 0 && t < 7200) return Math.floor(t);
const g = Number(playSpaceShooterMissionTimeSec);
if (Number.isFinite(g) && g > 0 && g < 7200) return Math.floor(g);
return 90;
}
+18
View File
@@ -540,6 +540,8 @@ function defaultGameTiming() {
jumpSurviveJumpHeightMult: 1.5,
/** จำกัดเวลารอบภารกิจ/มินิเกมกระโดดขึ้นแท่น (วินาที) — 0 = ไคลเอนต์ใช้ 60 วิ; ไม่ใช่ค่าเดียวกับพรมแดง */
jumpSurviveMissionTimeSec: 0,
/** ยิงยานอวกาศ / space_shooter: จำกัดเวลารอบ (วินาที) — 0 = ไคลเอนต์ใช้ค่าเริ่ม 90; แมป spaceShooterTimeSec > 0 ทับ */
spaceShooterMissionTimeSec: 0,
};
}
@@ -570,6 +572,12 @@ function clampJumpSurviveMissionTimeSec(n) {
return Math.max(10, Math.min(7200, Math.floor(v)));
}
function clampSpaceShooterMissionTimeSec(n) {
const v = Number(n);
if (Number.isNaN(v) || v <= 0) return 0;
return Math.max(10, Math.min(7200, Math.floor(v)));
}
function clampGauntletTickMs(n) {
const v = Number(n);
if (Number.isNaN(v)) return GAUNTLET_DEFAULT_TICK_MS;
@@ -681,6 +689,9 @@ function loadGameTiming() {
jumpSurviveMissionTimeSec: Object.prototype.hasOwnProperty.call(j, 'jumpSurviveMissionTimeSec')
? clampJumpSurviveMissionTimeSec(j.jumpSurviveMissionTimeSec)
: 0,
spaceShooterMissionTimeSec: Object.prototype.hasOwnProperty.call(j, 'spaceShooterMissionTimeSec')
? clampSpaceShooterMissionTimeSec(j.spaceShooterMissionTimeSec)
: 0,
};
}
} catch (e) { console.error('loadGameTiming', e.message); }
@@ -727,6 +738,12 @@ function saveGameTimingToFile(d) {
const jumpMissionSec = Object.prototype.hasOwnProperty.call(d, 'jumpSurviveMissionTimeSec')
? clampJumpSurviveMissionTimeSec(d.jumpSurviveMissionTimeSec)
: clampJumpSurviveMissionTimeSec(prevJumpMissionSec);
const prevSpaceShooterMissionSec = Object.prototype.hasOwnProperty.call(prev, 'spaceShooterMissionTimeSec')
? prev.spaceShooterMissionTimeSec
: 0;
const spaceShooterMissionSec = Object.prototype.hasOwnProperty.call(d, 'spaceShooterMissionTimeSec')
? clampSpaceShooterMissionTimeSec(d.spaceShooterMissionTimeSec)
: clampSpaceShooterMissionTimeSec(prevSpaceShooterMissionSec);
const out = {
gauntletTickMs: clampGauntletTickMs(d.gauntletTickMs),
gauntletJumpTicks: clampGauntletJumpTicks(d.gauntletJumpTicks),
@@ -736,6 +753,7 @@ function saveGameTimingToFile(d) {
stackBlockWidthTiles: stackBlockW,
jumpSurviveJumpHeightMult: jumpMult,
jumpSurviveMissionTimeSec: jumpMissionSec,
spaceShooterMissionTimeSec: spaceShooterMissionSec,
};
fs.writeFileSync(GAME_TIMING_PATH, JSON.stringify(out, null, 2), 'utf8');
runtimeGameTiming = out;