Files
justice/www/html/Admin/index.html
T

513 lines
44 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="robots" content="noindex, nofollow">
<meta name="theme-color" content="#0b0d14">
<title>Admin — JD JUSTICE DIVERS</title>
<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=14">
</head>
<body>
<a class="skip-link" href="#admin-main">ข้ามไปเนื้อหา</a>
<div class="admin-bg" aria-hidden="true"></div>
<div class="admin-shell">
<header class="admin-header-bar">
<div class="admin-brand">
<span class="admin-brand-mark" aria-hidden="true"></span>
<div class="admin-brand-text">
<h1 class="admin-title">ศูนย์ควบคุม Admin</h1>
<p class="admin-tagline">JD JUSTICE DIVERS</p>
</div>
</div>
<div class="admin-top-actions" id="top-actions" hidden>
<a href="/Admin/justice-git.html" class="btn btn-ghost" style="text-decoration:none;margin-right:0.35rem;">อัป Git (justice)</a>
<span class="admin-user" id="admin-user-label"></span>
<button type="button" class="btn btn-ghost btn-logout" id="btn-logout">ออกจากระบบ</button>
</div>
</header>
<main id="admin-main" class="admin-main-flow">
<section id="panel-setup" class="card card--auth" hidden>
<h2>ตั้งค่าครั้งแรก</h2>
<p class="muted">สร้างบัญชีแอดมินหลัก (super) — ใช้ชื่อผู้ใช้ <strong>admin</strong></p>
<form id="form-setup" class="form-grid">
<label>รหัสผ่าน (อย่างน้อย 8 ตัว)
<input type="password" name="password" required minlength="8" autocomplete="new-password">
</label>
<label>ยืนยันรหัสผ่าน
<input type="password" name="password2" required minlength="8" autocomplete="new-password">
</label>
<button type="submit" class="btn btn-primary">สร้างแอดมิน</button>
</form>
<p id="setup-msg" class="msg" role="status"></p>
</section>
<section id="panel-login" class="card card--auth" hidden>
<h2>เข้าสู่ระบบ Admin</h2>
<form id="form-login" class="form-grid">
<label>ชื่อผู้ใช้
<input type="text" name="username" autocomplete="username" required>
</label>
<label>รหัสผ่าน
<span class="password-input-wrap">
<input type="password" name="password" id="login-password" autocomplete="current-password" required>
<button type="button" class="btn-password-toggle" id="login-password-toggle" aria-pressed="false" aria-label="แสดงรหัสผ่าน" title="แสดง/ซ่อนรหัสผ่าน">แสดง</button>
</span>
</label>
<button type="submit" class="btn btn-primary">เข้าสู่ระบบ</button>
</form>
<p id="login-msg" class="msg" role="status"></p>
</section>
<div id="panel-app" class="admin-app" hidden>
<nav class="admin-tabs" role="tablist" aria-label="เมนูหลัก Admin">
<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-carry" role="tab" id="tab-quiz-carry" aria-controls="tab-panel-quiz-carry"><span class="tab-label">คำถามหลายข้อ</span><span class="tab-desc">หยิบมาวาง · AD</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">กระโดดให้รอด</span><span class="tab-desc">ความสูงกระโดด · timing</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">เวลาเกม</span><span class="tab-desc">พรมแดง · tick &amp; laser</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="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>
</nav>
<section id="tab-panel-change-password" class="tab-panel card card--password" hidden role="tabpanel" aria-labelledby="tab-change-password">
<h2 class="card-title-icon">เปลี่ยนรหัสผ่านของคุณ</h2>
<p class="muted">ใช้รหัสปัจจุบันยืนยันก่อนตั้งรหัสใหม่ (ทุกแอดมินใช้ได้) · <strong>กดแท็บ &quot;รหัสผ่าน&quot; อีกครั้งเพื่อปิด</strong>และกลับไป OAuth</p>
<form id="form-change-password" class="form-grid">
<label>รหัสผ่านปัจจุบัน
<input type="password" name="currentPassword" required autocomplete="current-password">
</label>
<label>รหัสผ่านใหม่ (อย่างน้อย 8 ตัว)
<input type="password" name="newPassword" required minlength="8" autocomplete="new-password">
</label>
<label>ยืนยันรหัสผ่านใหม่
<input type="password" name="newPassword2" required minlength="8" autocomplete="new-password">
</label>
<button type="submit" class="btn btn-primary">บันทึกรหัสใหม่</button>
</form>
<p id="password-self-msg" class="msg" role="status"></p>
</section>
<section id="tab-panel-oauth" class="tab-panel card" role="tabpanel" aria-labelledby="tab-oauth">
<h2>Token &amp; OAuth (Facebook / Google)</h2>
<p class="muted">เก็บ App ID / Client ID / Secret และ Redirect URI สำหรับ Login — หน้า Login จะดึงเฉพาะค่าที่ไม่เป็นความลับผ่าน <code>oauth-public.php</code></p>
<form id="form-oauth" class="form-grid form-oauth">
<fieldset>
<legend>Facebook Login</legend>
<label>App ID
<input type="text" name="facebookAppId" autocomplete="off" placeholder="ตัวเลขจาก Meta Developer">
</label>
<label>App Secret
<input type="password" name="facebookAppSecret" autocomplete="off" placeholder="ความลับ — ไม่ส่งให้ลูกค้า">
</label>
<label>Redirect URI
<input type="url" name="facebookRedirectUri" placeholder="https://your.domain/Login/facebook-callback.html">
</label>
</fieldset>
<fieldset>
<legend>Google Sign-In</legend>
<label>Client ID
<input type="text" name="googleClientId" autocomplete="off" placeholder="xxx.apps.googleusercontent.com">
</label>
<label>Client Secret
<input type="password" name="googleClientSecret" autocomplete="off">
</label>
<label>Redirect URI
<input type="url" name="googleRedirectUri" placeholder="https://your.domain/Login/google-callback.html">
</label>
</fieldset>
<button type="submit" class="btn btn-primary">บันทึกการตั้งค่า OAuth</button>
</form>
<p id="oauth-msg" class="msg" role="status"></p>
</section>
<section id="tab-panel-map-editor" class="tab-panel card admin-editor-panel" hidden role="tabpanel" aria-labelledby="tab-map-editor">
<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>
</div>
<button type="button" class="btn btn-ghost btn-editor-fs" id="btn-map-editor-fullscreen" aria-pressed="false" title="ขยายเต็มหน้าจอ (Esc ออก)">เต็มจอ</button>
</div>
<iframe class="admin-editor-iframe" id="admin-map-editor-frame" title="Editor ฉาก" src="/Game/editor.html?embed=1"></iframe>
</section>
<section id="tab-panel-quiz" class="tab-panel card" hidden role="tabpanel" aria-labelledby="tab-quiz">
<h2>คำถามเกม (ถูก / ผิด)</h2>
<p class="muted">ใช้กับฉากประเภท <strong>เกมตอบคำถาม</strong> ใน Editor — สุ่มสูงสุด <strong>10 ข้อ</strong> ต่อรอบ · เวลาเป็นวินาที (เก็บที่เซิร์ฟเวอร์เกม <code>/Game/data/quiz-settings.json</code>)</p>
<fieldset class="quiz-timing-fieldset">
<legend>เวลาในเกม</legend>
<div class="form-grid form-inline quiz-timing-grid">
<label>อ่านคำถาม (วินาที) <input type="number" id="quiz-read-sec" min="1" max="300" step="1" value="10"></label>
<label>เดินไปยืนในโซนตอบ (วินาที) <input type="number" id="quiz-answer-sec" min="1" max="300" step="1" value="5"></label>
<label>พักระหว่างข้อ (วินาที) <input type="number" id="quiz-between-sec" min="0" max="120" step="1" value="3"></label>
</div>
</fieldset>
<h3 class="admin-subheading">รายการคำถาม</h3>
<p class="muted">แต่ละข้อ: ข้อความ + คำตอบที่<strong>ถูกต้อง</strong> (ถูก=จริง / ผิด=เท็จ)</p>
<div id="quiz-admin-questions-list" class="quiz-admin-questions-list"></div>
<div class="quiz-admin-actions">
<button type="button" class="btn btn-ghost" id="btn-quiz-admin-add">+ เพิ่มคำถาม</button>
<button type="button" class="btn btn-primary" id="btn-quiz-admin-save">บันทึกทั้งหมด</button>
</div>
<p id="quiz-settings-msg" class="msg" role="status"></p>
</section>
<section id="tab-panel-quiz-carry" class="tab-panel card" hidden role="tabpanel" aria-labelledby="tab-quiz-carry">
<h2>คำถามหลายตัวเลือก (หยิบมาวาง)</h2>
<p class="muted">ใช้กับฉากประเภท <strong>ตอบคำถาม — หยิบคำตอบมาวางกลาง</strong> ใน Editor — ผู้เล่นหยิบตัวเลือกไปวางโซนกลาง · ถ้ามีรายการที่นี่ เกมจะใช้<strong>เฉพาะชุดนี้</strong> (ไม่ผสมกับคำถามถูก/ผิดแท็บคำถามเกม) · เก็บที่ <code>/Game/data/quiz-settings.json</code> ฟิลด์ <code>carryQuestions</code></p>
<p class="muted" style="margin-top:-0.35rem">อย่างน้อย <strong>2 ตัวเลือก</strong>ต่อข้อ (สูงสุด 6) · เวลายังตั้งที่แท็บ <strong>คำถามเกม</strong>ได้ (ใช้ร่วมกับโหมดถามตอบอื่น)</p>
<h3 class="admin-subheading">รายการคำถาม</h3>
<div id="quiz-carry-admin-list" class="quiz-carry-admin-list"></div>
<div class="quiz-admin-actions">
<button type="button" class="btn btn-ghost" id="btn-quiz-carry-add">+ เพิ่มคำถาม</button>
<button type="button" class="btn btn-primary" id="btn-quiz-carry-save">บันทึกชุดหลายตัวเลือก</button>
</div>
<p id="quiz-carry-settings-msg" class="msg" role="status"></p>
</section>
<section id="tab-panel-quiz-battle" class="tab-panel card tab-panel-quiz-battle" hidden role="tabpanel" aria-labelledby="tab-quiz-battle">
<h2>Quiz Battle — คำถาม 3 ตัวเลือก (สไตล์โดม)</h2>
<p class="muted">สร้างคำถามแบบ <strong>A / B / C</strong> แยกตาม<strong>หมวด</strong>ให้สอดคล้องธีม <a href="https://srv1361159.hstgr.cloud/Quiz-Battle/" target="_blank" rel="noopener noreferrer">Quiz Battle</a> · เก็บที่ <code>/Game/data/quiz-settings.json</code> ฟิลด์ <code>battleQuizMcq</code> · ตัวอย่าง UI ด้านขวา (โดม + ป๊อปอัป) จำลองจากรีเฟอเรนซ์ในเกม</p>
<p class="muted" style="margin-top:-0.35rem">เวลาอ่าน/ตอบใช้ร่วมกับแท็บ <strong>คำถามเกม</strong>ได้เมื่อนำไปผูกโหมดเล่นในอนาคต · <em>English:</em> MCQ with category; dome + modal preview for art direction.</p>
<div class="qb-admin-layout">
<div class="qb-admin-form-col">
<div class="qb-admin-toolbar">
<label class="qb-filter-label">กรองรายการตามหมวด
<select id="qb-battle-filter-cat" class="qb-battle-filter-cat" aria-label="กรองหมวด">
<option value="">ทุกหมวด</option>
</select>
</label>
</div>
<h3 class="admin-subheading">รายการคำถาม</h3>
<p class="muted" style="margin:-0.25rem 0 0.5rem;font-size:0.88rem">ก่อนกดบันทึก: ต้องมี<strong>ข้อความคำถาม</strong>และ<strong>ตัวเลือก A B C ครบทั้งสามช่อง</strong>ในแต่ละข้อที่ต้องการเก็บ · ถ้าช่องว่าง ระบบจะไม่บันทึกข้อนั้นและจะแจ้งเตือน</p>
<div id="qb-battle-admin-list" class="qb-battle-admin-list"></div>
<div class="quiz-admin-actions">
<button type="button" class="btn btn-ghost" id="btn-qb-battle-add">+ เพิ่มคำถาม</button>
<button type="button" class="btn btn-primary" id="btn-qb-battle-save">บันทึกชุด Quiz Battle</button>
</div>
<p id="qb-battle-settings-msg" class="msg" role="status"></p>
</div>
<div class="qb-preview-col" aria-label="ตัวอย่าง UI โดมและป๊อปอัป">
<h3 class="admin-subheading qb-preview-heading">ตัวอย่าง (ตามรีเฟอเรนซ์)</h3>
<div class="qb-preview-toolbar">
<span class="qb-preview-toolbar-label">โหมดดูตัวอย่าง</span>
<div class="qb-preview-mode-btns" role="group" aria-label="โหมดตัวอย่าง">
<button type="button" class="btn btn-ghost btn-sm qb-preview-mode-btn is-active" data-qb-mode="question">คำถาม</button>
<button type="button" class="btn btn-ghost btn-sm qb-preview-mode-btn" data-qb-mode="revealed">หลังเลือกตอบ</button>
<button type="button" class="btn btn-ghost btn-sm qb-preview-mode-btn" data-qb-mode="summary">สรุปผล</button>
</div>
</div>
<div class="qb-preview-stage" id="qb-preview-stage">
<div class="qb-preview-hud">
<div class="qb-hud-left">
<span class="qb-hud-icon" aria-hidden="true">💻</span>
<span class="qb-hud-cat" id="qb-preview-hud-cat">อาชญากรรมออนไลน์</span>
</div>
<div class="qb-hud-center">PLAYERS : —</div>
<div class="qb-hud-right">SCORE : —</div>
</div>
<div class="qb-preview-floor">
<div class="qb-path"></div>
<div class="qb-dome-scene">
<div class="qb-dome-float">
<div class="qb-dome-qbadge" aria-hidden="true">?</div>
<div class="qb-dome-chevron" aria-hidden="true"></div>
</div>
<div class="qb-dome-glass">
<div class="qb-dome-scanlines" aria-hidden="true"></div>
<div class="qb-dome-pedestal"></div>
<div class="qb-dome-lock" aria-hidden="true">🔒</div>
</div>
</div>
</div>
<div class="qb-modal-layer" id="qb-modal-layer">
<div class="qb-cyber-modal qb-cyber-modal--question" id="qb-cyber-modal-question">
<div class="qb-cyber-bracket qb-cyber-bracket--tl" aria-hidden="true"></div>
<div class="qb-cyber-bracket qb-cyber-bracket--tr" aria-hidden="true"></div>
<div class="qb-cyber-bracket qb-cyber-bracket--bl" aria-hidden="true"></div>
<div class="qb-cyber-bracket qb-cyber-bracket--br" aria-hidden="true"></div>
<button type="button" class="qb-cyber-close" tabindex="-1" aria-hidden="true">×</button>
<div class="qb-cyber-title-tab">QUESTION #<span id="qb-preview-qnum">1</span></div>
<p class="qb-cyber-qtext" id="qb-preview-modal-q">การกระทำใดผิดกฎหมายไซเบอร์?</p>
<div class="qb-cyber-choices" id="qb-preview-choices">
<div class="qb-cyber-choice" data-slot="0"><span class="qb-cyber-choice-label">A</span><span class="qb-cyber-choice-txt" id="qb-preview-c0">ตัวอย่างตัวเลือก A</span><span class="qb-cyber-choice-mark" aria-hidden="true"></span></div>
<div class="qb-cyber-choice" data-slot="1"><span class="qb-cyber-choice-label">B</span><span class="qb-cyber-choice-txt" id="qb-preview-c1">ตัวอย่างตัวเลือก B</span><span class="qb-cyber-choice-mark" aria-hidden="true"></span></div>
<div class="qb-cyber-choice" data-slot="2"><span class="qb-cyber-choice-label">C</span><span class="qb-cyber-choice-txt" id="qb-preview-c2">ตัวอย่างตัวเลือก C</span><span class="qb-cyber-choice-mark" aria-hidden="true"></span></div>
</div>
</div>
<div class="qb-cyber-modal qb-cyber-modal--summary" id="qb-cyber-modal-summary" hidden>
<div class="qb-cyber-bracket qb-cyber-bracket--tl" aria-hidden="true"></div>
<div class="qb-cyber-bracket qb-cyber-bracket--tr" aria-hidden="true"></div>
<div class="qb-cyber-bracket qb-cyber-bracket--bl" aria-hidden="true"></div>
<div class="qb-cyber-bracket qb-cyber-bracket--br" aria-hidden="true"></div>
<button type="button" class="qb-cyber-close" tabindex="-1" aria-hidden="true">×</button>
<div class="qb-summary-header">Completed</div>
<p class="qb-summary-msg">เย่! คุณตอบคำถามครบทุกข้อแล้ว มาสรุปคะแนนกัน</p>
<div class="qb-summary-stats">
<div class="qb-summary-card">
<span class="qb-summary-card-icon" aria-hidden="true"></span>
<span class="qb-summary-card-label">Correct</span>
<span class="qb-summary-card-val qb-summary-card-val--green">10</span>
</div>
<div class="qb-summary-card">
<span class="qb-summary-card-icon" aria-hidden="true">🕐</span>
<span class="qb-summary-card-label">Time Taken</span>
<span class="qb-summary-card-val qb-summary-card-val--cyan">27 m 33 s</span>
</div>
<div class="qb-summary-card">
<span class="qb-summary-card-icon" aria-hidden="true">🏆</span>
<span class="qb-summary-card-label">My Rank</span>
<span class="qb-summary-card-val qb-summary-card-val--gold">11</span>
</div>
</div>
<div class="qb-summary-footer-btn">ดูรายละเอียดเพิ่มเติม</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section id="tab-panel-jump-survive" class="tab-panel card" hidden role="tabpanel" aria-labelledby="tab-jump-survive">
<h2>กระโดดให้รอด — ตั้งค่าเกม</h2>
<p class="muted">คนละโหมดกับ <strong>พรมแดง (Gauntlet)</strong> · เก็บที่ <code>/Game/data/game-timing.json</code> ฟิลด์ <code>jumpSurviveJumpHeightMult</code> · ผู้เล่นได้ค่าใหม่เมื่อโหลดหน้าเล่น / sync timing จากเซิร์ฟเวอร์ · ถ้า API 404 ให้รีสตาร์ท Node ที่รัน <code>Game/server.js</code></p>
<fieldset class="quiz-timing-fieldset">
<legend>ความสูงกระโดด</legend>
<div class="form-grid form-inline quiz-timing-grid">
<label title="1 = สูงเท่าความสูงตัวละครจากแมป · 1.5 = สูงกว่าตัวครึ่งหนึ่ง">ทวีคูณความสูงกระโดด × ความสูงตัว <input type="number" id="jump-survive-height-mult" min="0.5" max="4" step="0.05" value="1.5"></label>
</div>
</fieldset>
<p class="muted" style="margin-top:-0.25rem;margin-bottom:0.65rem">ใช้กับฉากประเภท <strong>กระโดดให้รอด</strong> เท่านั้น · ถ้าในแมปตั้ง <code>jumpSurviveJumpImpulse</code> &gt; 0 จะใช้ค่านั้นแทน (override) · <em>English:</em> Jump height multiplier vs character height; per-map impulse overrides.</p>
<div class="quiz-admin-actions">
<button type="button" class="btn btn-primary" id="btn-jump-survive-save">บันทึก</button>
</div>
<p id="jump-survive-timing-msg" class="msg" role="status"></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>ตั้งที่แท็บชื่อเดียวกับโหมด</p>
<fieldset class="quiz-timing-fieldset">
<legend>พารามิเตอร์</legend>
<div class="form-grid form-inline quiz-timing-grid">
<label>ความถี่ tick (มิลลิวินาที) <input type="number" id="game-timing-tick-ms" min="80" max="800" step="10" value="220"></label>
<label>ระยะกระโดด (tick) <input type="number" id="game-timing-jump-ticks" min="4" max="40" step="1" value="16"></label>
<label>จำกัดเวลาเกม (วินาที) <input type="number" id="game-timing-limit-sec" min="0" max="7200" step="10" value="0" title="0 = เล่นได้ไม่จำกัดเวลา"></label>
</div>
</fieldset>
<p class="muted" style="margin-top:0.35rem">ค่าเวลาเกม: ตั้ง <strong>0</strong> = ไม่จำกัด · ตั้งอย่างน้อย <strong>10</strong> วินาทีถ้าต้องการจับเวลา (สูงสุด 7200 = 2 ชม.) · Per-round time limit; <strong>0</strong> = no limit, else min <strong>10</strong>s, max <strong>7200</strong>s.</p>
<fieldset class="quiz-timing-fieldset gauntlet-timing-fieldset">
<legend>รูปอุปสรรค์ Gauntlet / Obstacle &amp; laser art</legend>
<div class="gauntlet-editor-shell">
<header class="gauntlet-editor-toolbar">
<h3 class="gauntlet-editor-toolbar-title">พรมแดงสุดท้าย — อุปสรรค์ &amp; เลเซอร์</h3>
<div class="gauntlet-editor-toolbar-actions">
<a class="gauntlet-editor-link" href="/Game/editor.html?id=mnn93hpi&amp;embed=1" target="_blank" rel="noopener noreferrer">Editor ฉาก (ฝัง)</a>
<a class="gauntlet-editor-link gauntlet-editor-link-secondary" href="/Game/editor.html?id=mnn93hpi" target="_blank" rel="noopener noreferrer">Editor เต็มหน้า</a>
</div>
</header>
<p class="gauntlet-editor-legend">เลือกรูปจากคลัง → กด <strong>Lane</strong> / <strong>Laser</strong> ตามชิ้นส่วน (หัวบน · เส้น · ท้ายล่าง) แบบในเกม · โครง UI อ้างอิง <a href="/Game/editor.html?id=mnn93hpi&amp;embed=1" target="_blank" rel="noopener noreferrer">Editor ฉาก</a></p>
<div class="gauntlet-editor-panels">
<div class="gauntlet-asset-library gauntlet-editor-panel">
<h4 class="gauntlet-editor-panel-title">คลังรูป · Asset library</h4>
<p class="muted gauntlet-asset-library-intro">ลากวางหรือคลิกโซนอัปโหลด (png / jpg / gif / webp · สูงสุด 4 MB) · กด <strong>Lane</strong> / <strong>Laser บน·ล่าง·เส้น</strong> เพื่อใส่ในชุดที่ส่งเข้าเกม</p>
<div id="gauntlet-asset-drop" class="gauntlet-asset-drop" role="button" tabindex="0" aria-label="วางรูปหรือคลิกเพื่อเลือกไฟล์">
<span class="gauntlet-asset-drop-text">ลากรูปมาวางที่นี่ หรือคลิกเลือกไฟล์<br><small class="muted">Drop images here or click to browse</small></span>
</div>
<input type="file" id="gauntlet-asset-file-input" accept="image/png,image/jpeg,image/jpg,image/gif,image/webp" multiple hidden>
<p id="gauntlet-asset-upload-msg" class="msg gauntlet-asset-upload-msg" role="status"></p>
<div id="gauntlet-asset-list" class="gauntlet-asset-list" aria-live="polite"></div>
</div>
<div class="game-timing-assigned-visuals gauntlet-editor-panel gauntlet-editor-panel--stage">
<div class="game-timing-visual-block game-timing-visual-block--lanes">
<h3 class="game-timing-visual-title">Lane — สิ่งกีดขวางบนพรมแดง</h3>
<p class="muted game-timing-visual-hint">กด <strong>Lane</strong> ที่การ์ดในคลังเพื่อเพิ่ม · เรียงลำดับด้วยลูกศร · ลบออกจากรายการที่นี่ (ไม่ลบไฟล์ในคลัง)</p>
<div id="game-timing-lane-visual-list" class="game-timing-visual-grid" role="list" aria-label="รูป lane ที่ใช้"></div>
<textarea id="game-timing-lane-urls" hidden aria-hidden="true" tabindex="-1" spellcheck="false"></textarea>
</div>
<div class="game-timing-visual-block game-timing-visual-block--laser">
<h3 class="game-timing-visual-title">Laser — คอลัมน์แนวตั้ง (หัว · เส้น · ท้าย)</h3>
<p class="muted game-timing-visual-hint">กด <strong>Laser บน / ล่าง / เส้น</strong> ที่คลัง · ดูหรือล้างช่องด้านล่าง · เส้นกลาง = tile ซ้ำแนวตั้งในเกม</p>
<div class="game-timing-laser-slots-grid">
<div class="game-timing-laser-slot">
<div class="game-timing-laser-slot-label">หัวบน · Top emitter</div>
<div id="game-timing-laser-top-preview" class="game-timing-laser-preview" aria-label="ตัวอย่าง laser บน"></div>
<input type="hidden" id="game-timing-laser-top-url" value="">
<div class="game-timing-laser-slot-actions">
<button type="button" class="btn btn-ghost btn-sm" id="btn-game-timing-laser-top-view">ดู</button>
<button type="button" class="btn btn-ghost btn-sm" id="btn-game-timing-laser-top-clear">ล้าง</button>
</div>
</div>
<div class="game-timing-laser-slot game-timing-laser-slot--beam">
<div class="game-timing-laser-slot-label">เส้นกลาง (tile) · Beam</div>
<div id="game-timing-laser-line-preview" class="game-timing-laser-preview" aria-label="ตัวอย่างเส้น laser"></div>
<input type="hidden" id="game-timing-laser-line-url" value="">
<div class="game-timing-laser-slot-actions">
<button type="button" class="btn btn-ghost btn-sm" id="btn-game-timing-laser-line-view">ดู</button>
<button type="button" class="btn btn-ghost btn-sm" id="btn-game-timing-laser-line-clear">ล้าง</button>
</div>
</div>
<div class="game-timing-laser-slot">
<div class="game-timing-laser-slot-label">ท้ายล่าง · Bottom</div>
<div id="game-timing-laser-bottom-preview" class="game-timing-laser-preview" aria-label="ตัวอย่าง laser ล่าง"></div>
<input type="hidden" id="game-timing-laser-bottom-url" value="">
<div class="game-timing-laser-slot-actions">
<button type="button" class="btn btn-ghost btn-sm" id="btn-game-timing-laser-bottom-view">ดู</button>
<button type="button" class="btn btn-ghost btn-sm" id="btn-game-timing-laser-bottom-clear">ล้าง</button>
</div>
</div>
</div>
</div>
<div class="form-grid form-inline quiz-timing-grid game-timing-laser-colors-row gauntlet-editor-laser-colors">
<label>สีเติมคอลัมน์ laser (CSS) <input type="text" id="game-timing-laser-fill" maxlength="100" placeholder="rgba(239,68,68,0.35)" spellcheck="false"></label>
<label>สีเส้นกรอบ laser <input type="text" id="game-timing-laser-stroke" maxlength="100" placeholder="rgba(252,165,165,0.95)" spellcheck="false"></label>
<label>ความหนาเส้น (px, 0 = ไม่กรอบ) <input type="number" id="game-timing-laser-line-width" min="0" max="24" step="1" value="2"></label>
</div>
</div>
</div>
</div>
</fieldset>
<div class="quiz-admin-actions">
<button type="button" class="btn btn-primary" id="btn-game-timing-save">บันทึก</button>
</div>
<p id="game-timing-msg" class="msg" role="status"></p>
</section>
<section id="tab-panel-stack-game" class="tab-panel card" hidden role="tabpanel" aria-labelledby="tab-stack-game">
<h2>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>
<div class="form-grid form-inline quiz-timing-grid">
<label title="cycles per second — น้อย = ช้า, มาก = เร็ว · ใช้ช่องข้อความเพื่อไม่ให้เบราว์เซอร์ตัดค่าที่ไม่ตรง step ของ type=number">รอบสวิงต่อวินาที (Hz)
<input type="text" id="stack-swing-hz" inputmode="decimal" autocomplete="off" spellcheck="false" value="" placeholder="เช่น 0.55" title="0.082.8 · ว่าง = 0.55">
</label>
</div>
</fieldset>
<p class="muted" style="margin-top:0.35rem">แนะนำสวิง <strong>0.350.9</strong> · สูงสุด ~2.8 · Lower Hz = slower pendulum.</p>
<fieldset class="quiz-timing-fieldset" style="margin-top:0.75rem">
<legend>ความยาวแท่งบล็อก</legend>
<div class="form-grid form-inline quiz-timing-grid">
<label title="ความกว้างบล็อกเป็นหน่วย tile (1 tile = 1 ช่องบนแผนที่)">กว้างบล็อก (tile)
<input type="text" id="stack-block-width-tiles" inputmode="decimal" autocomplete="off" spellcheck="false" value="" placeholder="ว่าง = อัตโนมัติ" title="ตัวเลข 0.85–3.2 หรือปล่อยว่าง">
</label>
</div>
</fieldset>
<p class="muted" style="margin-top:0.35rem"><strong>ว่างช่อง</strong> = ใช้สูตรจากพื้นที่ลงบนแผนที่ (~48% ของความกว้างโซนลง) · ระหว่าง <strong>0.853.2</strong> tile · แท่งยาว = ยากขึ้น</p>
<div class="quiz-admin-actions">
<button type="button" class="btn btn-primary" id="btn-stack-game-save">บันทึก</button>
</div>
<p id="stack-game-msg" class="msg" role="status"></p>
</section>
<section id="tab-panel-characters" class="tab-panel card admin-editor-panel" hidden role="tabpanel" aria-labelledby="tab-characters">
<div class="admin-editor-panel-head">
<div class="admin-editor-panel-intro">
<h2>ตัวละคร (4 ทิศ)</h2>
<p class="muted">อัปโหลดสกิน 4 ทิศหรือแบบเลเยอร์ และเลือกตัวที่ใช้ในล็อบบี้/เกม — เปิดแยกได้ที่ <a href="/Game/character.html" target="_blank" rel="noopener noreferrer">/Game/character.html</a></p>
</div>
<button type="button" class="btn btn-ghost btn-editor-fs" id="btn-character-fullscreen" aria-pressed="false" title="ขยายเต็มหน้าจอ (Esc ออก)">เต็มจอ</button>
</div>
<iframe class="admin-editor-iframe" id="admin-character-frame" title="เลือกตัวละคร" src="/Game/character.html?embed=1&amp;from=admin"></iframe>
</section>
<section id="tab-panel-accounts" class="tab-panel card" hidden role="tabpanel" aria-labelledby="tab-accounts">
<h2>บัญชีผู้ใช้ (รายการภายในระบบ)</h2>
<p class="muted">ใช้บันทึก / บล็อกผู้เล่นหรือบัญชีที่เชื่อมกับ Guest / Facebook / Google — ผู้เล่น Guest ใน Main-Lobby จะถูกสร้างอัตโนมัติและมี COINS ตามที่เก็บที่นี่</p>
<p id="accounts-coins-summary" class="muted" aria-live="polite"></p>
<form id="form-account-add" class="form-grid form-inline">
<label>อีเมล <input type="email" name="email" placeholder="optional"></label>
<label>ชื่อแสดง <input type="text" name="displayName"></label>
<label>ประเภท
<select name="loginType">
<option value="guest">guest</option>
<option value="facebook">facebook</option>
<option value="google">google</option>
<option value="email">email</option>
</select>
</label>
<label>Provider User ID <input type="text" name="providerUserId" placeholder="id จาก Facebook/Google"></label>
<label>หมายเหตุ <input type="text" name="notes"></label>
<label>COINS เริ่มต้น <input type="number" name="coins" min="0" value="0" step="1"></label>
<label class="check"><input type="checkbox" name="blocked"> บล็อก</label>
<button type="submit" class="btn btn-primary">เพิ่มบัญชี</button>
</form>
<div class="table-wrap">
<table class="data-table" id="table-accounts">
<thead>
<tr>
<th>ชื่อ</th>
<th>อีเมล</th>
<th>ประเภท</th>
<th>Provider ID</th>
<th>COINS</th>
<th>สถานะ</th>
<th></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<p id="accounts-msg" class="msg" role="status"></p>
</section>
<section id="tab-panel-admins" class="tab-panel card" hidden role="tabpanel" aria-labelledby="tab-admins">
<h2>บัญชีแอดมิน</h2>
<p class="muted">เฉพาะ <strong>super admin</strong> เท่านั้นที่เพิ่ม/ลบแอดมินได้</p>
<form id="form-admin-add" class="form-grid form-inline">
<label>ชื่อผู้ใช้ <input type="text" name="username" required minlength="2" autocomplete="off"></label>
<label>รหัสผ่าน <input type="password" name="password" required minlength="8" autocomplete="new-password"></label>
<label>บทบาท
<select name="role">
<option value="admin">admin</option>
<option value="super">super</option>
</select>
</label>
<button type="submit" class="btn btn-primary">สร้างแอดมิน</button>
</form>
<h3 class="admin-subheading">ตั้งรหัสใหม่ให้แอดมินอื่น (super)</h3>
<p class="muted">ไม่ต้องรู้รหัสเดิม — ใช้เมื่อผู้ใช้ลืมรหัส</p>
<form id="form-admin-reset-other" class="form-grid form-inline">
<label>เลือกแอดมิน
<select name="targetId" id="select-admin-reset-target" required>
<option value="">— เลือก —</option>
</select>
</label>
<label>รหัสผ่านใหม่ <input type="password" name="newPassword" required minlength="8" autocomplete="new-password"></label>
<label>ยืนยันรหัส <input type="password" name="newPassword2" required minlength="8" autocomplete="new-password"></label>
<button type="submit" class="btn btn-primary">บันทึกรหัสใหม่</button>
</form>
<div class="table-wrap">
<table class="data-table" id="table-admins">
<thead>
<tr>
<th>ชื่อผู้ใช้</th>
<th>บทบาท</th>
<th>สร้างเมื่อ</th>
<th></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<p id="admins-msg" class="msg" role="status"></p>
</section>
</div>
</main>
</div>
<script src="admin.js?v=27"></script>
</body>
</html>