218 lines
12 KiB
HTML
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 & กระโดด</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> > 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> > 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> > 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>
|