Files
justice/www/html/Game/public/game-timing-admin.html
T
2026-05-01 08:04:36 +00:00

218 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ตั้งค่าเวลาเกม — Game</title>
<link rel="stylesheet" href="css/style.css">
<style>
body { margin: 0; min-height: 100dvh; background: #1a1b26; color: #c0caf5; font-family: system-ui, "Segoe UI", "Kanit", sans-serif; }
.admin-shell { max-width: 920px; margin: 0 auto; padding: 1rem 1.25rem 2rem; }
.admin-shell h1 { font-size: 1.2rem; color: #7aa2f7; margin: 0 0 0.35rem; }
.admin-shell > p.sub { font-size: 0.88rem; color: #a9b1d6; margin: 0 0 1rem; }
nav.admin-tabs { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-bottom: 1rem; border-bottom: 1px solid #414868; padding-bottom: 0.5rem; }
nav.admin-tabs button {
padding: 0.5rem 0.85rem; border-radius: 8px; border: 1px solid #414868; background: #24283b; color: #a9b1d6;
font-size: 0.82rem; font-weight: 600; cursor: pointer;
}
nav.admin-tabs button:hover { background: #414868; color: #c0caf5; }
nav.admin-tabs button[aria-selected="true"] { background: #1a1b26; color: #7aa2f7; border-color: #7aa2f7; }
.panel { display: none; background: #24283b; border: 1px solid #414868; border-radius: 10px; padding: 1rem 1.15rem; }
.panel.active { display: block; }
.panel h2 { font-size: 1rem; color: #c0caf5; margin: 0 0 0.75rem; }
label { display: block; font-size: 0.85rem; color: #a9b1d6; margin-bottom: 0.35rem; }
input[type="number"] {
width: 100%; max-width: 220px; padding: 0.5rem 0.65rem; border-radius: 8px; border: 1px solid #414868;
background: #1a1b26; color: #c0caf5; box-sizing: border-box;
}
.hint { font-size: 0.78rem; color: #565f89; margin: 0.35rem 0 0.85rem; line-height: 1.45; }
.row { margin-bottom: 1rem; }
.actions { margin-top: 1.25rem; display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; }
.actions button.primary { padding: 0.55rem 1.1rem; border: none; border-radius: 8px; background: #7aa2f7; color: #1a1b26; font-weight: 700; cursor: pointer; }
.actions button.primary:hover { background: #89b4fa; }
#status { font-size: 0.85rem; margin-top: 0.75rem; }
#status.ok { color: #9ece6a; }
#status.err { color: #f7768e; }
a.back { color: #7aa2f7; font-size: 0.88rem; }
</style>
</head>
<body>
<div class="admin-shell">
<p><a class="back" href="index.html">← กลับหน้าแรก</a></p>
<h1>ตั้งค่าเวลา / พารามิเตอร์เกม</h1>
<p class="sub">บันทึกลง <code>data/game-timing.json</code> ผ่าน <code>PUT /Game/api/game-timing</code> — โหลดหน้าเล่นจะดึงค่าอัตโนมัติ</p>
<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>
<div id="panel-jump" class="panel active" role="tabpanel" aria-labelledby="tab-jump">
<h2>กระโดดให้รอด — ความสูงกระโดด</h2>
<div class="row">
<label for="jumpSurviveJumpHeightMult">ทวีคูณความสูงกระโดด × ความสูงตัวละคร (1 = เท่าตัว · ค่าเริ่ม 1.5 = สูงกว่าตัวครึ่งหนึ่ง)</label>
<input type="number" id="jumpSurviveJumpHeightMult" min="0.5" max="4" step="0.05" value="1.5">
<p class="hint">สูตร: ความสูงขึ้นสุด ≈ ค่านี้ × (ความสูงตัวเป็น pixel จากช่องบนแมป) · ถ้าในแมปตั้ง <code>jumpSurviveJumpImpulse</code> &gt; 0 จะใช้ค่านั้นแทน (override)</p>
</div>
<div class="row">
<label for="jumpSurviveMissionTimeSec">jumpSurviveMissionTimeSec — เวลารอบมินิเกมกระโดดขึ้นแท่น (วินาที · 0 = เกมใช้ค่าเริ่ม 60)</label>
<input type="number" id="jumpSurviveMissionTimeSec" min="0" max="7200" step="5" value="0">
<p class="hint">ถ้าในแมปตั้ง <code>jumpSurviveTimeSec</code> &gt; 0 จะใช้ค่าบนแมปแทน · ไม่ใช่ค่าเดียวกับพรมแดง</p>
</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">
<label for="gauntletTickMs">gauntletTickMs</label>
<input type="number" id="gauntletTickMs" min="80" max="800" step="1">
</div>
<div class="row">
<label for="gauntletJumpTicks">gauntletJumpTicks</label>
<input type="number" id="gauntletJumpTicks" min="4" max="40" step="1">
</div>
<div class="row">
<label for="gauntletTimeLimitSec">gauntletTimeLimitSec (0 = ไม่จำกัด)</label>
<input type="number" id="gauntletTimeLimitSec" min="0" max="7200" step="1">
</div>
</div>
<div id="panel-stack" class="panel" role="tabpanel" aria-labelledby="tab-stack" hidden>
<h2>Stack</h2>
<div class="row">
<label for="stackSwingHz">stackSwingHz (รอบ/วินาที)</label>
<input type="number" id="stackSwingHz" min="0.08" max="2.8" step="0.01">
</div>
<div class="row">
<label for="stackBlockWidthTiles">stackBlockWidthTiles (ว่าง = คำนวณจากแมป)</label>
<input type="number" id="stackBlockWidthTiles" min="0.85" max="3.2" step="0.05" placeholder="ว่าง">
</div>
</div>
<div class="actions">
<button type="button" class="primary" id="btn-save">บันทึกทั้งหมด</button>
</div>
<p id="status" class="hint"></p>
</div>
<script>
(function () {
const BASE = window.location.pathname.replace(/\/[^/]*$/, '') || '';
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'),
};
tabs.forEach((btn) => {
btn.addEventListener('click', () => {
const id = btn.getAttribute('data-panel');
tabs.forEach((b) => {
b.setAttribute('aria-selected', b === btn ? 'true' : 'false');
});
Object.keys(panels).forEach((k) => {
const p = panels[k];
const on = k === id;
p.classList.toggle('active', on);
p.hidden = !on;
});
});
});
let timing = {};
function fillForm() {
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;
document.getElementById('stackSwingHz').value = timing.stackSwingHz != null ? timing.stackSwingHz : 0.1;
const sb = document.getElementById('stackBlockWidthTiles');
sb.value = timing.stackBlockWidthTiles != null && timing.stackBlockWidthTiles !== '' ? timing.stackBlockWidthTiles : '';
}
function readForm() {
const jm = parseFloat(document.getElementById('jumpSurviveJumpHeightMult').value, 10);
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),
stackSwingHz: parseFloat(document.getElementById('stackSwingHz').value, 10),
stackBlockWidthTiles: (function () {
const v = document.getElementById('stackBlockWidthTiles').value.trim();
return v === '' ? null : parseFloat(v, 10);
})(),
};
}
const statusEl = document.getElementById('status');
fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' })
.then((r) => r.json())
.then((t) => {
timing = t && typeof t === 'object' ? t : {};
fillForm();
statusEl.textContent = 'โหลดค่าปัจจุบันแล้ว';
statusEl.className = 'hint ok';
})
.catch(() => {
statusEl.textContent = 'โหลดไม่ได้ — เช็กว่าเซิร์ฟเวอร์รันและ path ถูกต้อง';
statusEl.className = 'hint err';
});
document.getElementById('btn-save').addEventListener('click', () => {
const body = readForm();
statusEl.textContent = 'กำลังบันทึก…';
statusEl.className = 'hint';
fetch(BASE + '/api/game-timing', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
.then((r) => r.json())
.then((res) => {
if (res && res.ok) {
timing = { ...timing, ...res };
statusEl.textContent = 'บันทึกสำเร็จ';
statusEl.className = 'ok';
} else {
statusEl.textContent = (res && res.error) || 'บันทึกไม่สำเร็จ';
statusEl.className = 'err';
}
})
.catch(() => {
statusEl.textContent = 'เครือข่ายผิดพลาด';
statusEl.className = 'err';
});
});
})();
</script>
</body>
</html>