update menu admin and rocker 1.1
This commit is contained in:
@@ -1,5 +1,17 @@
|
||||
/* Admin — JD JUSTICE DIVERS · UX-focused responsive layout */
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg: #0b0d14;
|
||||
--bg-elevated: #12141f;
|
||||
@@ -2407,3 +2419,27 @@ code {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.space-shooter-damage-prev {
|
||||
flex: 0 0 56px;
|
||||
}
|
||||
.space-shooter-damage-file {
|
||||
max-width: 200px;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.space-shooter-asteroid-urls-ta {
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
min-height: 9rem;
|
||||
margin-top: 0.35rem;
|
||||
padding: 0.55rem 0.65rem;
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.45;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
color: var(--text);
|
||||
resize: vertical;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
+125
-3
@@ -2674,6 +2674,100 @@
|
||||
return urls;
|
||||
}
|
||||
|
||||
function updateSpaceShooterDamageOverlayPreview(hit) {
|
||||
var img = el('space-shooter-damage-prev-' + hit);
|
||||
var inp = el('space-shooter-damage-url-' + hit);
|
||||
if (!img) return;
|
||||
var v = inp && inp.value ? String(inp.value).trim() : '';
|
||||
if (!v) {
|
||||
img.removeAttribute('src');
|
||||
img.alt = '';
|
||||
return;
|
||||
}
|
||||
if (!/^https?:\/\//i.test(v) && v.charAt(0) !== '/') v = '/' + v.replace(/^\/+/, '');
|
||||
img.alt = 'Damage overlay hit ' + hit;
|
||||
img.src = v;
|
||||
}
|
||||
|
||||
function readSpaceShooterShipDamageOverlayUrlsFromForm() {
|
||||
var urls = [];
|
||||
for (var h = 1; h <= 3; h++) {
|
||||
var inp = el('space-shooter-damage-url-' + h);
|
||||
urls.push(inp && inp.value ? String(inp.value).trim() : '');
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
function bindSpaceShooterDamageOverlayPanel() {
|
||||
for (var h = 1; h <= 3; h++) {
|
||||
(function (hit) {
|
||||
var inp = el('space-shooter-damage-url-' + hit);
|
||||
var btnClr = el('btn-space-shooter-damage-clear-' + hit);
|
||||
var btnUp = el('btn-space-shooter-damage-upload-' + hit);
|
||||
var fileInp = el('space-shooter-damage-file-' + hit);
|
||||
if (inp) {
|
||||
inp.addEventListener('change', function () { updateSpaceShooterDamageOverlayPreview(hit); });
|
||||
inp.addEventListener('blur', function () { updateSpaceShooterDamageOverlayPreview(hit); });
|
||||
}
|
||||
if (btnClr) {
|
||||
btnClr.addEventListener('click', function () {
|
||||
var i = el('space-shooter-damage-url-' + hit);
|
||||
if (i) i.value = '';
|
||||
if (fileInp) fileInp.value = '';
|
||||
updateSpaceShooterDamageOverlayPreview(hit);
|
||||
});
|
||||
}
|
||||
if (btnUp && fileInp) {
|
||||
btnUp.addEventListener('click', function () {
|
||||
if (!fileInp.files || !fileInp.files[0]) {
|
||||
setMsg('space-shooter-timing-msg', 'เลือกไฟล์รูปก่อน · Pick an image file first', 'error');
|
||||
return;
|
||||
}
|
||||
readFileAsDataURL(fileInp.files[0])
|
||||
.then(function (dataUrl) {
|
||||
return gauntletAssetsJsonFetch(GAME_GAUNTLET_ASSETS_API + '/upload', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
imageDataUrl: dataUrl,
|
||||
label: 'space-shooter-damage-hit-' + hit,
|
||||
}),
|
||||
});
|
||||
})
|
||||
.then(function (j) {
|
||||
if (!j || !j.ok || !j.url) {
|
||||
throw new Error((j && j.error) || 'อัปโหลดไม่สำเร็จ · Upload failed');
|
||||
}
|
||||
var inpU = el('space-shooter-damage-url-' + hit);
|
||||
if (inpU) inpU.value = String(j.url).trim();
|
||||
updateSpaceShooterDamageOverlayPreview(hit);
|
||||
setMsg(
|
||||
'space-shooter-timing-msg',
|
||||
'อัปโหลด overlay ช่อง ' + hit + ' แล้ว — กดบันทึกด้านล่างเพื่อเก็บลง game-timing.json · Uploaded; click Save',
|
||||
'ok'
|
||||
);
|
||||
})
|
||||
.catch(function (e) {
|
||||
setMsg('space-shooter-timing-msg', e.message || 'อัปโหลดไม่สำเร็จ', 'error');
|
||||
});
|
||||
});
|
||||
}
|
||||
})(h);
|
||||
}
|
||||
}
|
||||
|
||||
function parseSpaceShooterAsteroidSpriteUrlsTextarea() {
|
||||
var ta = el('space-shooter-asteroid-sprite-urls');
|
||||
var raw = ta && ta.value ? String(ta.value) : '';
|
||||
var lines = raw.split(/[\n\r]+/);
|
||||
var out = [];
|
||||
for (var i = 0; i < lines.length && out.length < 32; i++) {
|
||||
var t = lines[i].trim();
|
||||
if (t) out.push(t);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function loadSpaceShooterTimingPanel() {
|
||||
gameTimingFetch('GET')
|
||||
.then(function (data) {
|
||||
@@ -2689,6 +2783,24 @@
|
||||
if (inpU) inpU.value = urls[k - 1] != null ? String(urls[k - 1]) : '';
|
||||
updateSpaceShooterShipPreview(k);
|
||||
}
|
||||
var taAst = el('space-shooter-asteroid-sprite-urls');
|
||||
if (taAst) {
|
||||
var astArr = data.spaceShooterAsteroidSpriteUrls;
|
||||
if (!Array.isArray(astArr)) astArr = [];
|
||||
taAst.value = astArr.map(function (u) { return String(u || '').trim(); }).filter(Boolean).join('\n');
|
||||
}
|
||||
var inpFms = el('space-shooter-asteroid-explode-ms');
|
||||
if (inpFms) {
|
||||
var ef = Number(data.spaceShooterAsteroidExplodeFrameMs);
|
||||
inpFms.value = String(Number.isFinite(ef) ? Math.max(30, Math.min(500, Math.round(ef))) : 70);
|
||||
}
|
||||
var dmg = data.spaceShooterShipDamageOverlayUrls;
|
||||
if (!Array.isArray(dmg)) dmg = [];
|
||||
for (var dh = 1; dh <= 3; dh++) {
|
||||
var inpD = el('space-shooter-damage-url-' + dh);
|
||||
if (inpD) inpD.value = dmg[dh - 1] != null ? String(dmg[dh - 1]) : '';
|
||||
updateSpaceShooterDamageOverlayPreview(dh);
|
||||
}
|
||||
setMsg('space-shooter-timing-msg', '', '');
|
||||
})
|
||||
.catch(function (e) {
|
||||
@@ -2701,14 +2813,23 @@
|
||||
if (Number.isNaN(limSs) || limSs < 0) limSs = 0;
|
||||
limSs = limSs <= 0 ? 0 : Math.max(10, Math.min(7200, limSs));
|
||||
var shipUrls = readSpaceShooterShipImageUrlsFromForm();
|
||||
var damageUrls = readSpaceShooterShipDamageOverlayUrlsFromForm();
|
||||
var astUrls = parseSpaceShooterAsteroidSpriteUrlsTextarea();
|
||||
var inpFms = el('space-shooter-asteroid-explode-ms');
|
||||
var explodeMs = inpFms ? parseInt(String(inpFms.value), 10) : 70;
|
||||
if (Number.isNaN(explodeMs)) explodeMs = 70;
|
||||
explodeMs = Math.max(30, Math.min(500, explodeMs));
|
||||
gameTimingFetch('GET')
|
||||
.then(function (data) {
|
||||
data.spaceShooterMissionTimeSec = limSs;
|
||||
data.spaceShooterShipImageUrls = shipUrls;
|
||||
data.spaceShooterShipDamageOverlayUrls = damageUrls;
|
||||
data.spaceShooterAsteroidSpriteUrls = astUrls;
|
||||
data.spaceShooterAsteroidExplodeFrameMs = explodeMs;
|
||||
return gameTimingFetch('PUT', data);
|
||||
})
|
||||
.then(function () {
|
||||
setMsg('space-shooter-timing-msg', 'บันทึกแล้ว — ผู้เล่นยิงยานอวกาศได้ค่าใหม่เมื่อรีเฟรชหน้าเล่นหรือโหลด /api/game-timing', 'ok');
|
||||
setMsg('space-shooter-timing-msg', 'บันทึกแล้ว — เวลา / รูปยาน / overlay ความเสียหาย / อุกาบาต มีผลเมื่อรีเฟรชหน้าเล่นหรือโหลด /api/game-timing', 'ok');
|
||||
})
|
||||
.catch(function (e) {
|
||||
setMsg('space-shooter-timing-msg', e.message || 'บันทึกไม่ได้', 'error');
|
||||
@@ -2766,7 +2887,7 @@
|
||||
return gameTimingFetch('PUT', data);
|
||||
})
|
||||
.then(function () {
|
||||
setMsg('game-timing-msg', 'บันทึกแล้ว — พรมแดง tick·กระโดด / รูปอุปสรรค์ · Minigame-5 / Minigame-6 ตั้งแท็บของแต่ละเกม · ค่า Stack แก้ที่แท็บ Stack เท่านั้น · จำกัดเวลาพรมแดงใช้กับรอบถัดไปที่โฮสต์เริ่มเกม', 'ok');
|
||||
setMsg('game-timing-msg', 'บันทึกแล้ว — พรมแดง tick·กระโดด / รูปอุปสรรค์ · Minigame-5 / Minigame-6 ตั้งแท็บของแต่ละเกม · ค่า Tower block (Stack) แก้ที่แท็บ Minigame-3 เท่านั้น · จำกัดเวลาพรมแดงใช้กับรอบถัดไปที่โฮสต์เริ่มเกม', 'ok');
|
||||
})
|
||||
.catch(function (e) {
|
||||
setMsg('game-timing-msg', e.message || 'บันทึกไม่ได้', 'error');
|
||||
@@ -2941,6 +3062,7 @@
|
||||
btnSpaceShooterSave.addEventListener('click', saveSpaceShooterTimingPanel);
|
||||
}
|
||||
bindSpaceShooterShipUrlInputs();
|
||||
bindSpaceShooterDamageOverlayPanel();
|
||||
var btnStackGameSave = el('btn-stack-game-save');
|
||||
if (btnStackGameSave) {
|
||||
btnStackGameSave.addEventListener('click', saveStackGamePanel);
|
||||
@@ -3251,7 +3373,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, 'space-shooter': 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, 'mega-virus': 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;
|
||||
|
||||
@@ -9,7 +9,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=22">
|
||||
<link rel="stylesheet" href="admin.css?v=25">
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#admin-main">ข้ามไปเนื้อหา</a>
|
||||
@@ -68,13 +68,14 @@
|
||||
<button type="button" class="tab" data-tab="change-password" role="tab" id="tab-change-password" aria-selected="false" aria-controls="tab-panel-change-password"><span class="tab-label">รหัสผ่าน</span><span class="tab-desc tab-desc--wide">เปลี่ยนรหัสของคุณ · กดซ้ำเพื่อปิด</span></button>
|
||||
<button type="button" class="tab is-active" data-tab="oauth" role="tab" id="tab-oauth" aria-selected="true" aria-controls="tab-panel-oauth"><span class="tab-label">OAuth</span><span class="tab-desc">Facebook / Google</span></button>
|
||||
<button type="button" class="tab" data-tab="map-editor" role="tab" id="tab-map-editor" aria-controls="tab-panel-map-editor"><span class="tab-label">Editor</span><span class="tab-desc">แผนที่ฉาก</span></button>
|
||||
<button type="button" class="tab" data-tab="quiz" role="tab" id="tab-quiz" aria-controls="tab-panel-quiz"><span class="tab-label">คำถามเกม</span><span class="tab-desc">ถูก/ผิด · เวลา</span></button>
|
||||
<button type="button" class="tab" data-tab="quiz" role="tab" id="tab-quiz" aria-controls="tab-panel-quiz"><span class="tab-label">Minigame-1-จริงหรือไม่</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">Minigame-3-Tower block</span><span class="tab-desc" aria-hidden="true"></span></button>
|
||||
<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="mega-virus" role="tab" id="tab-mega-virus" aria-controls="tab-panel-mega-virus"><span class="tab-label">Minigame-7-ยิง Mega Virus</span><span class="tab-desc" aria-hidden="true"></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>
|
||||
<button type="button" class="tab" data-tab="accounts" role="tab" id="tab-accounts" aria-controls="tab-panel-accounts"><span class="tab-label">ผู้ใช้</span><span class="tab-desc">บัญชี & COINS</span></button>
|
||||
<button type="button" class="tab" data-tab="admins" role="tab" id="tab-admins" aria-controls="tab-panel-admins"><span class="tab-label">แอดมิน</span><span class="tab-desc">สิทธิ์ระบบ</span></button>
|
||||
@@ -135,7 +136,7 @@
|
||||
<div class="admin-editor-panel-head">
|
||||
<div class="admin-editor-panel-intro">
|
||||
<h2>Editor ฉาก (Lobby / ZEP / กบ / ตอบคำถาม)</h2>
|
||||
<p class="muted">สร้างและบันทึกแผนที่ — ใช้รหัสฉากตอนสร้างห้องในล็อบบี้ · คลิกซ้ายวาด / คลิกขวาลบ · ห้องโถง: พื้นที่เริ่มเกม (ส้ม) · <strong>เกมตอบคำถาม</strong>: วาดโซนถูก/ผิด + พื้นที่โชว์คำถาม (ทอง) — ข้อความคำถามจากแท็บ <strong>คำถามเกม</strong> · <strong>เลื่อนแถบเครื่องมือด้านบน</strong>ได้แยกจากพื้นที่วาด</p>
|
||||
<p class="muted">สร้างและบันทึกแผนที่ — ใช้รหัสฉากตอนสร้างห้องในล็อบบี้ · คลิกซ้ายวาด / คลิกขวาลบ · ห้องโถง: พื้นที่เริ่มเกม (ส้ม) · <strong>เกมตอบคำถาม</strong>: วาดโซนถูก/ผิด + พื้นที่โชว์คำถาม (ทอง) — ข้อความคำถามจากแท็บ <strong>Minigame-1-จริงหรือไม่</strong> · <strong>เลื่อนแถบเครื่องมือด้านบน</strong>ได้แยกจากพื้นที่วาด</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-ghost btn-editor-fs" id="btn-map-editor-fullscreen" aria-pressed="false" title="ขยายเต็มหน้าจอ (Esc ออก)">เต็มจอ</button>
|
||||
</div>
|
||||
@@ -467,7 +468,7 @@
|
||||
|
||||
<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>spaceShooterShipImageUrls</code> (รูปยาน 6 ช่อง) · ผู้เล่นได้ค่าใหม่เมื่อโหลดหน้าเล่น / <code>GET /api/game-timing</code> · รูปเกมอัปโหลดไปที่ <code>/Game/public/img/ViolentCrime/</code> (ชื่อโฟลเดอร์<strong>ไม่มีช่องว่าง</strong> — FTP หลายตัวจะ <code>550</code> ถ้าใช้ <code>Violent Crime</code>)</p>
|
||||
<p class="muted">เก็บที่ <code>/Game/data/game-timing.json</code> — <code>spaceShooterMissionTimeSec</code> · <code>spaceShooterShipImageUrls</code> · <code>spaceShooterShipDamageOverlayUrls</code> (ทับรอยร้าวเมื่อโดนอุกาบาต 1–3 ครั้ง) · <code>spaceShooterAsteroidSpriteUrls</code> + <code>spaceShooterAsteroidExplodeFrameMs</code> · ผู้เล่นได้ค่าใหม่เมื่อโหลดหน้าเล่น / <code>GET /api/game-timing</code> · อัปโหลด PNG ไปที่ <code>/Game/public/img/ViolentCrime/</code> (โฟลเดอร์<strong>ไม่มีช่องว่าง</strong>) · อัปโหลด overlay ผ่านคลัง Gauntlet (<code>/Game/img/gauntlet-assets/</code>)</p>
|
||||
<fieldset class="quiz-timing-fieldset">
|
||||
<legend>จำกัดเวลารอบ (ยิงอุกาบาต)</legend>
|
||||
<div class="form-grid form-inline quiz-timing-grid">
|
||||
@@ -486,6 +487,24 @@
|
||||
<div class="space-shooter-ship-row"><span class="space-shooter-ship-slot">6</span><label class="space-shooter-ship-url-label">URL <input type="text" id="space-shooter-ship-url-6" maxlength="500" spellcheck="false" placeholder="/Game/img/ViolentCrime/ship6.png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="space-shooter-ship-prev-6" alt="" width="48" height="48" decoding="async"><button type="button" class="btn btn-ghost" id="btn-space-shooter-ship-clear-6">ล้าง</button></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="quiz-timing-fieldset">
|
||||
<legend>รอยความเสียหายทับยาน (ชนอุกาบาต ครั้งที่ 1–3)</legend>
|
||||
<p class="muted" style="margin-top:0">ใช้ PNG/WebP โปร่งพื้นหลัง · แสดงทับรูปยานตามจำนวนครั้งที่โดน (ก่อน OUT) · <em>English:</em> Three optional overlays for hit 1 / 2 / 3; scales with the ship footprint.</p>
|
||||
<div class="space-shooter-ship-grid" role="group" aria-label="Space shooter damage overlays">
|
||||
<div class="space-shooter-ship-row"><span class="space-shooter-ship-slot" title="โดนครั้งที่ 1">1</span><label class="space-shooter-ship-url-label">URL <input type="text" id="space-shooter-damage-url-1" maxlength="500" spellcheck="false" placeholder="/Game/img/gauntlet-assets/....png" autocomplete="off"></label><input type="file" id="space-shooter-damage-file-1" class="space-shooter-damage-file" accept="image/png,image/webp,image/jpeg,image/gif"><button type="button" class="btn btn-ghost" id="btn-space-shooter-damage-upload-1">อัปโหลด</button><img class="space-shooter-ship-prev space-shooter-damage-prev" id="space-shooter-damage-prev-1" alt="" width="56" height="56" decoding="async"><button type="button" class="btn btn-ghost" id="btn-space-shooter-damage-clear-1">ล้าง</button></div>
|
||||
<div class="space-shooter-ship-row"><span class="space-shooter-ship-slot" title="โดนครั้งที่ 2">2</span><label class="space-shooter-ship-url-label">URL <input type="text" id="space-shooter-damage-url-2" maxlength="500" spellcheck="false" placeholder="/Game/img/gauntlet-assets/....png" autocomplete="off"></label><input type="file" id="space-shooter-damage-file-2" class="space-shooter-damage-file" accept="image/png,image/webp,image/jpeg,image/gif"><button type="button" class="btn btn-ghost" id="btn-space-shooter-damage-upload-2">อัปโหลด</button><img class="space-shooter-ship-prev space-shooter-damage-prev" id="space-shooter-damage-prev-2" alt="" width="56" height="56" decoding="async"><button type="button" class="btn btn-ghost" id="btn-space-shooter-damage-clear-2">ล้าง</button></div>
|
||||
<div class="space-shooter-ship-row"><span class="space-shooter-ship-slot" title="โดนครั้งที่ 3">3</span><label class="space-shooter-ship-url-label">URL <input type="text" id="space-shooter-damage-url-3" maxlength="500" spellcheck="false" placeholder="/Game/img/gauntlet-assets/....png" autocomplete="off"></label><input type="file" id="space-shooter-damage-file-3" class="space-shooter-damage-file" accept="image/png,image/webp,image/jpeg,image/gif"><button type="button" class="btn btn-ghost" id="btn-space-shooter-damage-upload-3">อัปโหลด</button><img class="space-shooter-ship-prev space-shooter-damage-prev" id="space-shooter-damage-prev-3" alt="" width="56" height="56" decoding="async"><button type="button" class="btn btn-ghost" id="btn-space-shooter-damage-clear-3">ล้าง</button></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="quiz-timing-fieldset">
|
||||
<legend>อุกาบาต — PNG sequence</legend>
|
||||
<p class="muted" style="margin-top:0"><strong>บรรทัดที่ 1</strong> = รูปตอนตก (loop ขณะล่วง) · <strong>บรรทัด 2 เป็นต้นไป</strong> = เฟรมแตกตามลำดับหลังโดนยิง/ชน · สูงสุด 32 บรรทัด · <em>English:</em> Line 1 = falling sprite; lines 2+ = explosion frames played once.</p>
|
||||
<label for="space-shooter-asteroid-sprite-urls" class="sr-only">URLs อุกาบาต ทีละบรรทัด</label>
|
||||
<textarea id="space-shooter-asteroid-sprite-urls" class="space-shooter-asteroid-urls-ta" rows="8" spellcheck="false" placeholder="/Game/img/ViolentCrime/ast-fall.png /Game/img/ViolentCrime/ast-x1.png /Game/img/ViolentCrime/ast-x2.png"></textarea>
|
||||
<div class="form-grid form-inline quiz-timing-grid" style="margin-top:0.65rem">
|
||||
<label title="ความเร็วเฟรมแอนิเมชันแตก (มิลลิวินาทีต่อเฟรม)">เฟรมแตก (ms) <input type="number" id="space-shooter-asteroid-explode-ms" min="30" max="500" step="5" value="70"></label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<p class="muted" style="margin-top:-0.25rem;margin-bottom:0.65rem">นับจากเริ่มรอบในเกม · ถ้าในแมปตั้ง <code>spaceShooterTimeSec</code> > 0 จะใช้ค่าบนแมปแทนทั้งหมด · ตั้ง <code>spaceShooterTimeSec</code> = 0 บนแมป = ไม่จับเวลา</p>
|
||||
<div class="quiz-admin-actions">
|
||||
<button type="button" class="btn btn-primary" id="btn-space-shooter-save">บันทึก</button>
|
||||
@@ -493,6 +512,11 @@
|
||||
<p id="space-shooter-timing-msg" class="msg" role="status"></p>
|
||||
</section>
|
||||
|
||||
<section id="tab-panel-mega-virus" class="tab-panel card" hidden role="tabpanel" aria-labelledby="tab-mega-virus">
|
||||
<h2>Minigame-7 — ยิง Mega Virus</h2>
|
||||
<p class="muted">จองเมนูไว้สำหรับตั้งค่ามินิเกมนี้ในอนาคต (ยังไม่มีฟอร์ม) · <em>English:</em> Placeholder tab — admin options will be added later.</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 · <strong>เวลายิงอุกาบาต</strong>ตั้งที่แท็บ Minigame-6</p>
|
||||
@@ -599,7 +623,7 @@
|
||||
</section>
|
||||
|
||||
<section id="tab-panel-stack-game" class="tab-panel card" hidden role="tabpanel" aria-labelledby="tab-stack-game">
|
||||
<h2>Stack — สลับจังหวะลงตึก</h2>
|
||||
<h2>Minigame-3 — Tower block (Stack)</h2>
|
||||
<p class="muted">ปรับความเร็วสวิง (crane) และ<strong>ความยาวแท่งบล็อก</strong>ในเกม <strong>Stack</strong> · เก็บที่ <code>/Game/data/game-timing.json</code> · ผู้เล่นได้ค่าใหม่เมื่อเข้าห้อง / รีเฟรชหน้าเล่น</p>
|
||||
<fieldset class="quiz-timing-fieldset">
|
||||
<legend>ความเร็วสวิง</legend>
|
||||
@@ -722,6 +746,6 @@
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<script src="admin.js?v=54"></script>
|
||||
<script src="admin.js?v=58"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -26,5 +26,8 @@
|
||||
"/Game/img/ViolentCrime/Rocket-4.png",
|
||||
"/Game/img/ViolentCrime/Rocket-5.png",
|
||||
"/Game/img/ViolentCrime/Rocket-6.png"
|
||||
]
|
||||
],
|
||||
"spaceShooterAsteroidSpriteUrls": [],
|
||||
"spaceShooterAsteroidExplodeFrameMs": 70,
|
||||
"spaceShooterShipDamageOverlayUrls": ["", "", ""]
|
||||
}
|
||||
+165
-23
@@ -61,6 +61,7 @@
|
||||
if (dx * dx + dy * dy > rr) continue;
|
||||
ent.spaceShooterHits = Math.max(0, Number(ent.spaceShooterHits) || 0) + 1;
|
||||
ent.spaceShooterInvulnUntil = now + SPACE_SHOOTER_MISSION_HIT_INVULN_MS;
|
||||
spaceShooterSpawnAsteroidExplosion(a.x, a.y, a.r);
|
||||
spaceShooterAsteroids.splice(ai, 1);
|
||||
spaceShooterPopups.push({ x: cx, y: cy - 28, text: 'HIT', until: Date.now() + 650 });
|
||||
if (ent.spaceShooterHits >= SPACE_SHOOTER_MISSION_MAX_ASTEROID_HITS) {
|
||||
@@ -751,6 +752,11 @@
|
||||
let playSpaceShooterMissionTimeSec = 0;
|
||||
/** รูปยานช่อง 1–6 จาก game-timing (spawn slot); ว่าง = วาดยานเวกเตอร์ */
|
||||
let playSpaceShooterShipImageUrls = ['', '', '', '', '', ''];
|
||||
/** อุกาบาต: [0]=ตก, [1..]=แตก — จาก game-timing */
|
||||
let playSpaceShooterAsteroidSpriteUrls = [];
|
||||
let playSpaceShooterAsteroidExplodeFrameMs = 70;
|
||||
/** ทับยานเมื่อชนอุกาบาต ครั้งที่ 1–3 (PNG โปร่ง) — ภารกิจ Violent Crime */
|
||||
let playSpaceShooterShipDamageOverlayUrls = ['', '', ''];
|
||||
/** 0 = ไม่จำกัด — จาก game-timing / gauntlet-sync (พรมแดง Gauntlet เท่านั้น) */
|
||||
let gauntletRuntimeTimeLimitSec = 0;
|
||||
/** เวลาสิ้นสุดรอบ (epoch ms) จากเซิร์ฟเวอร์ — null = ไม่จับเวลา */
|
||||
@@ -889,6 +895,8 @@
|
||||
/** space_shooter — ยิงหิน (จุดเกิดจากกริด P1–P6) */
|
||||
let spaceShooterBullets = [];
|
||||
let spaceShooterAsteroids = [];
|
||||
/** { x, y, r, fi, acc } — fi = index เฟรมแตก (0 = urls[1]) */
|
||||
let spaceShooterAsteroidExplosions = [];
|
||||
let spaceShooterPopups = [];
|
||||
let spaceShooterLastTickMs = 0;
|
||||
let spaceShooterSpawnAccMs = 0;
|
||||
@@ -899,6 +907,40 @@
|
||||
let spaceShooterMissionPhase = null;
|
||||
let spaceShooterMissionCountdownTimer = null;
|
||||
|
||||
function spaceShooterSpawnAsteroidExplosion(worldX, worldY, r) {
|
||||
const urls = playSpaceShooterAsteroidSpriteUrls;
|
||||
if (!urls || urls.length < 2) return;
|
||||
spaceShooterAsteroidExplosions.push({
|
||||
x: worldX,
|
||||
y: worldY,
|
||||
r: Math.max(6, Number(r) || 14),
|
||||
fi: 0,
|
||||
acc: 0,
|
||||
});
|
||||
}
|
||||
|
||||
function spaceShooterTickAsteroidExplosions(dt) {
|
||||
const urls = playSpaceShooterAsteroidSpriteUrls;
|
||||
if (!urls || urls.length < 2) {
|
||||
spaceShooterAsteroidExplosions.length = 0;
|
||||
return;
|
||||
}
|
||||
const fms = Math.max(30, Math.min(500, Number(playSpaceShooterAsteroidExplodeFrameMs) || 70));
|
||||
const maxFi = urls.length - 2;
|
||||
for (let i = spaceShooterAsteroidExplosions.length - 1; i >= 0; i--) {
|
||||
const ex = spaceShooterAsteroidExplosions[i];
|
||||
ex.acc += dt * 1000;
|
||||
while (ex.acc >= fms) {
|
||||
ex.acc -= fms;
|
||||
ex.fi++;
|
||||
if (ex.fi > maxFi) {
|
||||
spaceShooterAsteroidExplosions.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** balloon_boss — ลูกโป้งยิงบอส (Mega Virus) */
|
||||
let balloonBossPendingShots = [];
|
||||
let balloonBossPlayerBullets = [];
|
||||
@@ -2988,6 +3030,7 @@
|
||||
spaceShooterGameEnded = false;
|
||||
spaceShooterBullets = [];
|
||||
spaceShooterAsteroids = [];
|
||||
spaceShooterAsteroidExplosions = [];
|
||||
spaceShooterPopups = [];
|
||||
spaceShooterLastTickMs = performance.now();
|
||||
spaceShooterSpawnAccMs = 0;
|
||||
@@ -3161,6 +3204,7 @@
|
||||
applySpaceShooterMissionPanelImages();
|
||||
spaceShooterBullets = [];
|
||||
spaceShooterAsteroids = [];
|
||||
spaceShooterAsteroidExplosions = [];
|
||||
spaceShooterSpawnAccMs = 0;
|
||||
spaceShooterPopups = [];
|
||||
showGauntletCrownMissionOverlay(spaceShooterBuildMissionPayload());
|
||||
@@ -5748,6 +5792,38 @@
|
||||
}
|
||||
playSpaceShooterShipImageUrls = next;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'spaceShooterAsteroidSpriteUrls')) {
|
||||
const raw = payload.spaceShooterAsteroidSpriteUrls;
|
||||
const next = [];
|
||||
if (Array.isArray(raw)) {
|
||||
for (let ai = 0; ai < raw.length && next.length < 32; ai++) {
|
||||
const u = normalizeGauntletAssetUrlForPlay(typeof raw[ai] === 'string' ? raw[ai] : '');
|
||||
if (u) next.push(u);
|
||||
}
|
||||
}
|
||||
playSpaceShooterAsteroidSpriteUrls = next;
|
||||
next.forEach((u) => {
|
||||
if (u) ensureGauntletAssetImage(u);
|
||||
});
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'spaceShooterAsteroidExplodeFrameMs')) {
|
||||
const ef = Number(payload.spaceShooterAsteroidExplodeFrameMs);
|
||||
if (Number.isFinite(ef)) {
|
||||
playSpaceShooterAsteroidExplodeFrameMs = Math.max(30, Math.min(500, Math.round(ef)));
|
||||
}
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'spaceShooterShipDamageOverlayUrls')) {
|
||||
const rawD = payload.spaceShooterShipDamageOverlayUrls;
|
||||
const nextD = ['', '', ''];
|
||||
if (Array.isArray(rawD)) {
|
||||
for (let di = 0; di < 3; di++) {
|
||||
const u = normalizeGauntletAssetUrlForPlay(typeof rawD[di] === 'string' ? rawD[di] : '');
|
||||
nextD[di] = u;
|
||||
if (u) ensureGauntletAssetImage(u);
|
||||
}
|
||||
}
|
||||
playSpaceShooterShipDamageOverlayUrls = nextD;
|
||||
}
|
||||
if (isStack() && stackMini) reapplyStackMiniSizingFromGlobals();
|
||||
}
|
||||
|
||||
@@ -6965,6 +7041,7 @@
|
||||
spaceShooterGameEnded = true;
|
||||
spaceShooterBullets = [];
|
||||
spaceShooterAsteroids = [];
|
||||
spaceShooterAsteroidExplosions = [];
|
||||
spaceShooterSpawnAccMs = 0;
|
||||
const ov = document.getElementById('gauntlet-ended-overlay');
|
||||
const msgEl = document.getElementById('gauntlet-ended-message');
|
||||
@@ -7069,6 +7146,7 @@
|
||||
const now = performance.now();
|
||||
const dt = Math.min(0.055, spaceShooterLastTickMs ? (now - spaceShooterLastTickMs) / 1000 : 0.016);
|
||||
spaceShooterLastTickMs = now;
|
||||
spaceShooterTickAsteroidExplosions(dt);
|
||||
|
||||
if (spaceShooterGameEnded) {
|
||||
const wallClock = Date.now();
|
||||
@@ -7160,6 +7238,7 @@
|
||||
a.hp = (a.hp || 1) - 1;
|
||||
spaceShooterBullets.splice(bi, 1);
|
||||
if (a.hp <= 0) {
|
||||
spaceShooterSpawnAsteroidExplosion(a.x, a.y, a.r);
|
||||
spaceShooterAsteroids.splice(hit, 1);
|
||||
const add = 5;
|
||||
if (b.ownerId === myId) {
|
||||
@@ -7199,33 +7278,68 @@
|
||||
const refs = buildSpaceShooterParticipantRefsPlay();
|
||||
const wallNow = typeof timeMs === 'number' ? timeMs : Date.now();
|
||||
|
||||
const astUrls = playSpaceShooterAsteroidSpriteUrls;
|
||||
const fallAstUrl = astUrls && astUrls.length ? astUrls[0] : '';
|
||||
const fallAstRec = fallAstUrl ? ensureGauntletAssetImage(fallAstUrl) : null;
|
||||
const fallAstImg = fallAstRec && fallAstRec.ready && fallAstRec.img && fallAstRec.img.naturalWidth > 0 ? fallAstRec.img : null;
|
||||
|
||||
for (let i = 0; i < spaceShooterAsteroids.length; i++) {
|
||||
const a = spaceShooterAsteroids[i];
|
||||
const [sx, sy] = worldToScreen(a.x, a.y);
|
||||
const sr = Math.max(6, a.r * zDraw);
|
||||
const grd = ctx.createRadialGradient(sx - sr * 0.25, sy - sr * 0.25, sr * 0.1, sx, sy, sr);
|
||||
grd.addColorStop(0, 'rgba(90, 85, 95, 0.95)');
|
||||
grd.addColorStop(0.55, 'rgba(35, 32, 42, 0.98)');
|
||||
grd.addColorStop(1, 'rgba(18, 16, 24, 1)');
|
||||
ctx.fillStyle = grd;
|
||||
ctx.beginPath();
|
||||
ctx.arc(sx, sy, sr, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = 'rgba(255, 90, 60, 0.75)';
|
||||
ctx.lineWidth = Math.max(1.5, zDraw * 1.2);
|
||||
ctx.beginPath();
|
||||
ctx.arc(sx, sy, sr * 0.88, 0.6, 1.9);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.arc(sx + sr * 0.15, sy + sr * 0.1, sr * 0.35, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
ctx.shadowColor = 'rgba(255, 60, 40, 0.55)';
|
||||
ctx.shadowBlur = sr * 0.45;
|
||||
ctx.strokeStyle = 'rgba(255, 120, 80, 0.35)';
|
||||
ctx.beginPath();
|
||||
ctx.arc(sx, sy, sr + 3 * zDraw, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
if (fallAstImg) {
|
||||
const iw = fallAstImg.naturalWidth;
|
||||
const ih = fallAstImg.naturalHeight;
|
||||
const diam = sr * 2.15;
|
||||
const sc = Math.min(diam / iw, diam / ih);
|
||||
const dw = iw * sc;
|
||||
const dh = ih * sc;
|
||||
ctx.drawImage(fallAstImg, sx - dw * 0.5, sy - dh * 0.5, dw, dh);
|
||||
} else {
|
||||
const grd = ctx.createRadialGradient(sx - sr * 0.25, sy - sr * 0.25, sr * 0.1, sx, sy, sr);
|
||||
grd.addColorStop(0, 'rgba(90, 85, 95, 0.95)');
|
||||
grd.addColorStop(0.55, 'rgba(35, 32, 42, 0.98)');
|
||||
grd.addColorStop(1, 'rgba(18, 16, 24, 1)');
|
||||
ctx.fillStyle = grd;
|
||||
ctx.beginPath();
|
||||
ctx.arc(sx, sy, sr, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = 'rgba(255, 90, 60, 0.75)';
|
||||
ctx.lineWidth = Math.max(1.5, zDraw * 1.2);
|
||||
ctx.beginPath();
|
||||
ctx.arc(sx, sy, sr * 0.88, 0.6, 1.9);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.arc(sx + sr * 0.15, sy + sr * 0.1, sr * 0.35, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
ctx.shadowColor = 'rgba(255, 60, 40, 0.55)';
|
||||
ctx.shadowBlur = sr * 0.45;
|
||||
ctx.strokeStyle = 'rgba(255, 120, 80, 0.35)';
|
||||
ctx.beginPath();
|
||||
ctx.arc(sx, sy, sr + 3 * zDraw, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (astUrls && astUrls.length >= 2) {
|
||||
for (let ei = 0; ei < spaceShooterAsteroidExplosions.length; ei++) {
|
||||
const ex = spaceShooterAsteroidExplosions[ei];
|
||||
const u = astUrls[ex.fi + 1];
|
||||
if (!u) continue;
|
||||
const rec = ensureGauntletAssetImage(u);
|
||||
const img = rec && rec.ready && rec.img && rec.img.naturalWidth > 0 ? rec.img : null;
|
||||
if (!img) continue;
|
||||
const [sx, sy] = worldToScreen(ex.x, ex.y);
|
||||
const sr = Math.max(6, ex.r * zDraw);
|
||||
const iw = img.naturalWidth;
|
||||
const ih = img.naturalHeight;
|
||||
const diam = sr * 2.4;
|
||||
const sc = Math.min(diam / iw, diam / ih);
|
||||
const dw = iw * sc;
|
||||
const dh = ih * sc;
|
||||
ctx.drawImage(img, sx - dw * 0.5, sy - dh * 0.5, dw, dh);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < spaceShooterBullets.length; i++) {
|
||||
@@ -7253,6 +7367,8 @@
|
||||
const shipUrl = (playSpaceShooterShipImageUrls[slot - 1] || '').trim();
|
||||
const shipRec = shipUrl ? ensureGauntletAssetImage(shipUrl) : null;
|
||||
const shipImg = shipRec && shipRec.ready && shipRec.img && shipRec.img.naturalWidth > 0 ? shipRec.img : null;
|
||||
let shipDrawW = bodyW * 2.2;
|
||||
let shipDrawH = bodyH * 2.2;
|
||||
if (shipImg) {
|
||||
ctx.save();
|
||||
ctx.translate(sx, sy);
|
||||
@@ -7264,6 +7380,8 @@
|
||||
const sc = Math.min(maxW / iw, maxH / ih);
|
||||
const dw = iw * sc;
|
||||
const dh = ih * sc;
|
||||
shipDrawW = dw;
|
||||
shipDrawH = dh;
|
||||
ctx.drawImage(shipImg, -dw * 0.5, -dh * 0.5, dw, dh);
|
||||
ctx.restore();
|
||||
} else {
|
||||
@@ -7290,6 +7408,27 @@
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
if (isSpaceShooterMissionUiMapPlay()) {
|
||||
const hits = Math.max(0, Math.min(3, Number(ent.spaceShooterHits) || 0));
|
||||
if (hits > 0 && playSpaceShooterShipDamageOverlayUrls && playSpaceShooterShipDamageOverlayUrls.length) {
|
||||
const oUrl = (playSpaceShooterShipDamageOverlayUrls[hits - 1] || '').trim();
|
||||
if (oUrl) {
|
||||
const oRec = ensureGauntletAssetImage(oUrl);
|
||||
const oImg = oRec && oRec.ready && oRec.img && oRec.img.naturalWidth > 0 ? oRec.img : null;
|
||||
if (oImg) {
|
||||
ctx.save();
|
||||
if (elimShip) ctx.globalAlpha = 0.38;
|
||||
const ow = oImg.naturalWidth;
|
||||
const oh = oImg.naturalHeight;
|
||||
const scO = Math.min(shipDrawW / ow, shipDrawH / oh);
|
||||
const odw = ow * scO;
|
||||
const odh = oh * scO;
|
||||
ctx.drawImage(oImg, sx - odw * 0.5, sy - odh * 0.5, odw, odh);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const name = (ent.nickname || '').slice(0, 10);
|
||||
if (name) {
|
||||
ctx.font = `${Math.max(9, 10 * zDraw)}px system-ui, "Kanit", sans-serif`;
|
||||
@@ -8702,6 +8841,7 @@
|
||||
const gend = document.getElementById('gauntlet-ended-overlay');
|
||||
if (gend) gend.classList.add('is-hidden');
|
||||
spaceShooterMissionPhase = null;
|
||||
spaceShooterAsteroidExplosions = [];
|
||||
if (spaceShooterMissionCountdownTimer) {
|
||||
clearTimeout(spaceShooterMissionCountdownTimer);
|
||||
spaceShooterMissionCountdownTimer = null;
|
||||
@@ -8920,6 +9060,7 @@
|
||||
spaceShooterGameEnded = false;
|
||||
spaceShooterBullets = [];
|
||||
spaceShooterAsteroids = [];
|
||||
spaceShooterAsteroidExplosions = [];
|
||||
spaceShooterPopups = [];
|
||||
spaceShooterLastTickMs = performance.now();
|
||||
spaceShooterSpawnAccMs = 0;
|
||||
@@ -9583,6 +9724,7 @@
|
||||
spaceShooterGameEnded = false;
|
||||
spaceShooterBullets = [];
|
||||
spaceShooterAsteroids = [];
|
||||
spaceShooterAsteroidExplosions = [];
|
||||
spaceShooterPopups = [];
|
||||
spaceShooterLastTickMs = performance.now();
|
||||
spaceShooterSpawnAccMs = 0;
|
||||
|
||||
@@ -1897,7 +1897,7 @@
|
||||
</div>
|
||||
<script src="/Game/socket.io/socket.io.js"></script>
|
||||
<script src="js/version.js?v=0.0184"></script>
|
||||
<script src="js/play.js?v=0.218"></script>
|
||||
<script src="js/play.js?v=0.220"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -543,6 +543,9 @@ function defaultGameTiming() {
|
||||
/** ยิงยานอวกาศ / space_shooter: จำกัดเวลารอบ (วินาที) — 0 = ไคลเอนต์ใช้ค่าเริ่ม 90; แมป spaceShooterTimeSec > 0 ทับ */
|
||||
spaceShooterMissionTimeSec: 0,
|
||||
spaceShooterShipImageUrls: ['', '', '', '', '', ''],
|
||||
spaceShooterAsteroidSpriteUrls: [],
|
||||
spaceShooterAsteroidExplodeFrameMs: 70,
|
||||
spaceShooterShipDamageOverlayUrls: ['', '', ''],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -598,6 +601,51 @@ function normalizeSpaceShooterShipImageUrls(val) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** อุกาบาต space_shooter: บรรทัดแรก = ตอนตก, ถัดไป = เฟรมแตก (สูงสุด 32 URL) */
|
||||
function normalizeSpaceShooterAsteroidSpriteUrls(val) {
|
||||
let arr = val;
|
||||
if (typeof arr === 'string') {
|
||||
try {
|
||||
const p = JSON.parse(arr);
|
||||
if (Array.isArray(p)) arr = p;
|
||||
} catch (e) {
|
||||
arr = arr.split(/[\n\r]+/);
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(arr)) arr = [];
|
||||
const out = [];
|
||||
for (let i = 0; i < arr.length && out.length < 32; i++) {
|
||||
const u = sanitizeGauntletAssetUrl(arr[i]);
|
||||
if (u) out.push(u);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function clampSpaceShooterAsteroidExplodeFrameMs(n) {
|
||||
const v = Number(n);
|
||||
if (Number.isNaN(v)) return 70;
|
||||
return Math.max(30, Math.min(500, Math.round(v)));
|
||||
}
|
||||
|
||||
/** รูปทับยานเมื่อชนอุกาบาต ครั้งที่ 1–3 (ภารกิจ Violent Crime) */
|
||||
function normalizeSpaceShooterShipDamageOverlayUrls(val) {
|
||||
let arr = val;
|
||||
if (typeof arr === 'string') {
|
||||
try {
|
||||
const p = JSON.parse(arr);
|
||||
if (Array.isArray(p)) arr = p;
|
||||
} catch (e) {
|
||||
arr = arr.split(/[\n\r]+/);
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(arr)) arr = [];
|
||||
const out = ['', '', ''];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
out[i] = sanitizeGauntletAssetUrl(arr[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function clampGauntletTickMs(n) {
|
||||
const v = Number(n);
|
||||
if (Number.isNaN(v)) return GAUNTLET_DEFAULT_TICK_MS;
|
||||
@@ -713,6 +761,11 @@ function loadGameTiming() {
|
||||
? clampSpaceShooterMissionTimeSec(j.spaceShooterMissionTimeSec)
|
||||
: 0,
|
||||
spaceShooterShipImageUrls: normalizeSpaceShooterShipImageUrls(j.spaceShooterShipImageUrls),
|
||||
spaceShooterAsteroidSpriteUrls: normalizeSpaceShooterAsteroidSpriteUrls(j.spaceShooterAsteroidSpriteUrls),
|
||||
spaceShooterAsteroidExplodeFrameMs: Object.prototype.hasOwnProperty.call(j, 'spaceShooterAsteroidExplodeFrameMs')
|
||||
? clampSpaceShooterAsteroidExplodeFrameMs(j.spaceShooterAsteroidExplodeFrameMs)
|
||||
: 70,
|
||||
spaceShooterShipDamageOverlayUrls: normalizeSpaceShooterShipDamageOverlayUrls(j.spaceShooterShipDamageOverlayUrls),
|
||||
};
|
||||
}
|
||||
} catch (e) { console.error('loadGameTiming', e.message); }
|
||||
@@ -771,6 +824,24 @@ function saveGameTimingToFile(d) {
|
||||
const spaceShooterShipImageUrls = Object.prototype.hasOwnProperty.call(d, 'spaceShooterShipImageUrls')
|
||||
? normalizeSpaceShooterShipImageUrls(d.spaceShooterShipImageUrls)
|
||||
: prevShipUrls;
|
||||
const prevAstUrls = Array.isArray(prev.spaceShooterAsteroidSpriteUrls)
|
||||
? normalizeSpaceShooterAsteroidSpriteUrls(prev.spaceShooterAsteroidSpriteUrls)
|
||||
: [];
|
||||
const spaceShooterAsteroidSpriteUrls = Object.prototype.hasOwnProperty.call(d, 'spaceShooterAsteroidSpriteUrls')
|
||||
? normalizeSpaceShooterAsteroidSpriteUrls(d.spaceShooterAsteroidSpriteUrls)
|
||||
: prevAstUrls;
|
||||
const prevAstFms = Object.prototype.hasOwnProperty.call(prev, 'spaceShooterAsteroidExplodeFrameMs')
|
||||
? prev.spaceShooterAsteroidExplodeFrameMs
|
||||
: 70;
|
||||
const spaceShooterAsteroidExplodeFrameMs = Object.prototype.hasOwnProperty.call(d, 'spaceShooterAsteroidExplodeFrameMs')
|
||||
? clampSpaceShooterAsteroidExplodeFrameMs(d.spaceShooterAsteroidExplodeFrameMs)
|
||||
: clampSpaceShooterAsteroidExplodeFrameMs(prevAstFms);
|
||||
const prevDmg = Array.isArray(prev.spaceShooterShipDamageOverlayUrls)
|
||||
? normalizeSpaceShooterShipDamageOverlayUrls(prev.spaceShooterShipDamageOverlayUrls)
|
||||
: normalizeSpaceShooterShipDamageOverlayUrls([]);
|
||||
const spaceShooterShipDamageOverlayUrls = Object.prototype.hasOwnProperty.call(d, 'spaceShooterShipDamageOverlayUrls')
|
||||
? normalizeSpaceShooterShipDamageOverlayUrls(d.spaceShooterShipDamageOverlayUrls)
|
||||
: prevDmg;
|
||||
const out = {
|
||||
gauntletTickMs: clampGauntletTickMs(d.gauntletTickMs),
|
||||
gauntletJumpTicks: clampGauntletJumpTicks(d.gauntletJumpTicks),
|
||||
@@ -782,6 +853,9 @@ function saveGameTimingToFile(d) {
|
||||
jumpSurviveMissionTimeSec: jumpMissionSec,
|
||||
spaceShooterMissionTimeSec: spaceShooterMissionSec,
|
||||
spaceShooterShipImageUrls,
|
||||
spaceShooterAsteroidSpriteUrls,
|
||||
spaceShooterAsteroidExplodeFrameMs,
|
||||
spaceShooterShipDamageOverlayUrls,
|
||||
};
|
||||
fs.writeFileSync(GAME_TIMING_PATH, JSON.stringify(out, null, 2), 'utf8');
|
||||
runtimeGameTiming = out;
|
||||
|
||||
Reference in New Issue
Block a user