diff --git a/www/html/Admin/admin.css b/www/html/Admin/admin.css index ad6093d..a992581 100644 --- a/www/html/Admin/admin.css +++ b/www/html/Admin/admin.css @@ -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; +} diff --git a/www/html/Admin/admin.js b/www/html/Admin/admin.js index 4b16960..f54bf5c 100644 --- a/www/html/Admin/admin.js +++ b/www/html/Admin/admin.js @@ -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; diff --git a/www/html/Admin/index.html b/www/html/Admin/index.html index e085068..75043b6 100644 --- a/www/html/Admin/index.html +++ b/www/html/Admin/index.html @@ -9,7 +9,7 @@ - +
ข้ามไปเนื้อหา @@ -68,13 +68,14 @@ - + + + - - + @@ -135,7 +136,7 @@สร้างและบันทึกแผนที่ — ใช้รหัสฉากตอนสร้างห้องในล็อบบี้ · คลิกซ้ายวาด / คลิกขวาลบ · ห้องโถง: พื้นที่เริ่มเกม (ส้ม) · เกมตอบคำถาม: วาดโซนถูก/ผิด + พื้นที่โชว์คำถาม (ทอง) — ข้อความคำถามจากแท็บ คำถามเกม · เลื่อนแถบเครื่องมือด้านบนได้แยกจากพื้นที่วาด
+สร้างและบันทึกแผนที่ — ใช้รหัสฉากตอนสร้างห้องในล็อบบี้ · คลิกซ้ายวาด / คลิกขวาลบ · ห้องโถง: พื้นที่เริ่มเกม (ส้ม) · เกมตอบคำถาม: วาดโซนถูก/ผิด + พื้นที่โชว์คำถาม (ทอง) — ข้อความคำถามจากแท็บ Minigame-1-จริงหรือไม่ · เลื่อนแถบเครื่องมือด้านบนได้แยกจากพื้นที่วาด
เก็บที่ /Game/data/game-timing.json ฟิลด์ spaceShooterMissionTimeSec · spaceShooterShipImageUrls (รูปยาน 6 ช่อง) · ผู้เล่นได้ค่าใหม่เมื่อโหลดหน้าเล่น / GET /api/game-timing · รูปเกมอัปโหลดไปที่ /Game/public/img/ViolentCrime/ (ชื่อโฟลเดอร์ไม่มีช่องว่าง — FTP หลายตัวจะ 550 ถ้าใช้ Violent Crime)
เก็บที่ /Game/data/game-timing.json — spaceShooterMissionTimeSec · spaceShooterShipImageUrls · spaceShooterShipDamageOverlayUrls (ทับรอยร้าวเมื่อโดนอุกาบาต 1–3 ครั้ง) · spaceShooterAsteroidSpriteUrls + spaceShooterAsteroidExplodeFrameMs · ผู้เล่นได้ค่าใหม่เมื่อโหลดหน้าเล่น / GET /api/game-timing · อัปโหลด PNG ไปที่ /Game/public/img/ViolentCrime/ (โฟลเดอร์ไม่มีช่องว่าง) · อัปโหลด overlay ผ่านคลัง Gauntlet (/Game/img/gauntlet-assets/)
นับจากเริ่มรอบในเกม · ถ้าในแมปตั้ง spaceShooterTimeSec > 0 จะใช้ค่าบนแมปแทนทั้งหมด · ตั้ง spaceShooterTimeSec = 0 บนแมป = ไม่จับเวลา
จองเมนูไว้สำหรับตั้งค่ามินิเกมนี้ในอนาคต (ยังไม่มีฟอร์ม) · English: Placeholder tab — admin options will be added later.
+เก็บที่เซิร์ฟเวอร์เกม /Game/data/game-timing.json · หลังบันทึกห้องที่กำลังเล่นจะปรับความถี่ tick ทันที · ค่าจะส่งไปยังผู้เล่นผ่าน gauntlet-sync · จำกัดเวลานับจากตอนโฮสต์เริ่มเกมพรมแดง — หมดเวลาแล้วเกมจบและส่งทุกคนกลับฉากล็อบบี้บนเซิร์ฟเวอร์ · ถ้าขึ้น Not Found ให้รีสตาร์ท Nodeที่รัน Game/server.js (เช่น pm2 restart) หลัง deploy · เวลามินิเกมกระโดดขึ้นแท่นตั้งที่แท็บ Minigame-5 · เวลายิงอุกาบาตตั้งที่แท็บ Minigame-6
ปรับความเร็วสวิง (crane) และความยาวแท่งบล็อกในเกม Stack · เก็บที่ /Game/data/game-timing.json · ผู้เล่นได้ค่าใหม่เมื่อเข้าห้อง / รีเฟรชหน้าเล่น