update menu admin and rocker 1.1

This commit is contained in:
2026-05-01 10:08:01 +00:00
parent e2dcf6f678
commit c67ce56920
7 changed files with 437 additions and 36 deletions
+36
View File
@@ -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
View File
@@ -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;
+32 -8
View File
@@ -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">บัญชี &amp; 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&#10;/Game/img/ViolentCrime/ast-x1.png&#10;/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> &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>
@@ -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>
+4 -1
View File
@@ -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
View File
@@ -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;
/** รูปยานช่อง 16 จาก 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;
+1 -1
View File
@@ -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>
+74
View File
@@ -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;