optimise and get more flow

This commit is contained in:
2026-05-26 10:48:16 +00:00
parent 39395e173e
commit 4e4231879e
234 changed files with 66473 additions and 10400 deletions
+2 -2
View File
@@ -238,9 +238,9 @@
<input type="number" id="quiz-carry-answer-sec" class="admin-inp-num" min="1" max="300" step="1" value="5" aria-describedby="quiz-carry-answer-hint" />
<span id="quiz-carry-answer-hint" class="muted" style="display:block;font-size:0.85rem;margin-top:0.2rem">นับจากเมื่อหยิบได้ · สูงสุด 300 วิ</span>
</label>
<label class="admin-field">จำนวนข้อต่อเซสชันก่อนจบ (พรีวิว)
<label class="admin-field">จำนวนข้อต่อเซสชัน (หยิบมาวาง)
<input type="number" id="quiz-carry-session-len" class="admin-inp-num" min="0" max="500" step="1" value="0" aria-describedby="quiz-carry-session-hint" />
<span id="quiz-carry-session-hint" class="muted" style="display:block;font-size:0.85rem;margin-top:0.2rem">สุ่มคำถามจากชุดจนกว่าจะครบจำนวนนี้ (นับทุกข้อที่จบด้วยถูกหรือหมดเวลา) · 0 = เล่นต่อไม่จบอัตโนมัติ · <em>English:</em> Preview / editor embed only.</span>
<span id="quiz-carry-session-hint" class="muted" style="display:block;font-size:0.85rem;margin-top:0.2rem">ใช้กับแมป <strong>หยิบมาวาง</strong> (เช่น mnorwqx1) ทั้ง preview และ flow สืบสวนจริง · สุ่มจากชุด carryQuestions จนครบจำนวนนี้ · 0 = เล่นต่อไม่จบอัตโนมัติ · <em>ไม่ใช้กับ mng8a80o</em> — เกมถูก/ผิดใช้ «จำนวนข้อต่อรอบ» แท็บคำถามเกม</span>
</label>
</div>
<div class="admin-form-row" style="margin:0 0 1rem;flex-wrap:wrap;gap:1rem;align-items:flex-end">
@@ -1,219 +0,0 @@
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ตั้งค่า AI แชท — Game</title>
<link rel="stylesheet" href="css/style.css">
<style>
body { max-width: 640px; margin: 2rem auto; padding: 0 1rem; }
h1 { font-size: 1.25rem; margin-bottom: 0.5rem; }
.form-group { margin-bottom: 1rem; }
.form-group label { display: block; margin-bottom: 0.35rem; font-size: 0.9rem; }
.form-group input, .form-group select, .form-group textarea { width: 100%; padding: 0.5rem; border-radius: 6px; border: 1px solid #414868; background: #24283b; color: #c0caf5; box-sizing: border-box; }
.form-group textarea { min-height: 80px; resize: vertical; }
.form-group .hint, .form-group small { font-size: 0.8rem; color: #a9b1d6; margin-top: 0.25rem; display: block; }
button { padding: 0.5rem 1rem; border-radius: 6px; border: none; background: #7aa2f7; color: #1a1b26; cursor: pointer; }
button:hover { background: #89b4fa; }
.error { color: #f7768e; font-size: 0.9rem; margin-top: 0.5rem; }
.success { color: #9ece6a; font-size: 0.9rem; margin-top: 0.5rem; }
#settings-panel { display: none; }
#login-panel { display: block; }
a { color: #7aa2f7; }
.tabs { display: flex; background: #1a1b26; border-bottom: 2px solid #414868; border-radius: 8px 8px 0 0; overflow: hidden; }
.tab { flex: 1; padding: 12px 16px; text-align: center; cursor: pointer; background: #24283b; border: none; font-size: 0.95rem; font-weight: 600; color: #a9b1d6; transition: all 0.2s; }
.tab:hover { background: #414868; color: #c0caf5; }
.tab.active { background: #1a1b26; color: #7aa2f7; border-bottom: 2px solid #7aa2f7; }
.tab-content { display: none; padding: 1.25rem; background: #1a1b26; border: 1px solid #414868; border-top: none; border-radius: 0 0 8px 8px; }
.tab-content.active { display: block; }
.condition-box { background: #24283b; border-radius: 6px; padding: 0.75rem 1rem; margin-top: 1rem; font-size: 0.85rem; color: #a9b1d6; border-left: 3px solid #7aa2f7; }
.condition-box strong { color: #c0caf5; }
</style>
</head>
<body>
<h1>ตั้งค่า AI แชท (Room Lobby)</h1>
<p style="font-size:0.9rem;color:#a9b1d6;">ตั้งค่า OpenAI API Key และ Model สำหรับฟีเจอร์ "คุยกับ AI" ในห้องโถง</p>
<div id="login-panel">
<p style="font-size:0.85rem;color:#a9b1d6;">รหัสผ่านเริ่มต้น: <code>game-ai-admin</code> (ตั้ง env <code>GAME_AI_ADMIN_PASSWORD</code> เพื่อเปลี่ยน)</p>
<form id="login-form">
<input type="text" name="username" id="admin-username" value="admin" autocomplete="username" tabindex="-1" aria-hidden="true" style="position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0;">
<div class="form-group">
<label for="admin-password">รหัสผ่าน Admin</label>
<input type="password" id="admin-password" name="password" placeholder="รหัสผ่าน" autocomplete="current-password" required>
</div>
<button type="submit">เข้าสู่ระบบ</button>
<div id="login-error" class="error"></div>
</form>
</div>
<div id="settings-panel">
<div class="tabs">
<button type="button" class="tab active" data-tab="settings">การตั้งค่าระบบ</button>
<button type="button" class="tab" data-tab="intent-rag">Intent & RAG</button>
</div>
<form id="settings-form">
<div id="settings-tab" class="tab-content active">
<h2 style="font-size:1.1rem;margin-bottom:1rem;color:#c0caf5;">การตั้งค่าระบบ</h2>
<div class="form-group">
<label for="openai-key">OpenAI API Key</label>
<input type="password" id="openai-key" name="openai_api_key" placeholder="sk-..." autocomplete="off">
<small>เว้นว่างถ้าไม่ต้องการเปลี่ยน</small>
</div>
<div class="form-group">
<label for="openai-model">Model</label>
<select id="openai-model" name="model"></select>
</div>
</div>
<div id="intent-rag-tab" class="tab-content">
<h2 style="font-size:1.1rem;margin-bottom:1rem;color:#c0caf5;">Intent & RAG</h2>
<div class="form-group">
<label for="ai-intent">Intent / System Prompt</label>
<textarea id="ai-intent" name="intent" placeholder="บทบาทหรือคำสั่งให้ AI (เช่น คุณคือผู้ช่วยในห้องเกม ตอบสั้น ใช้ภาษาธรรมดา)"></textarea>
<small><strong>เงื่อนไข:</strong> ถ้ามีข้อความ จะถูกส่งเป็น system message ให้โมเดลใช้เป็นแนวทางในการตอบ</small>
</div>
<div class="form-group">
<label><input type="checkbox" id="rag-enabled" name="rag_enabled" style="width:auto;margin-right:8px;"> เปิดใช้ RAG (ใส่บริบทเพิ่มเติม)</label>
<small><strong>เงื่อนไข:</strong> เมื่อเปิด RAG เท่านั้น ระบบจะแนบ RAG Context ด้านล่างเข้า system prompt</small>
</div>
<div class="form-group" id="rag-context-wrap">
<label for="rag-context">RAG Context / ความรู้เพิ่มเติม</label>
<textarea id="rag-context" name="rag_context" placeholder="ข้อความหรือความรู้ที่ต้องการให้ AI อ้างอิง (ยัดเข้า system prompt)"></textarea>
<small>เมื่อเปิด RAG ข้อความนี้จะถูกแนบเป็นบริบทให้ AI ใช้ประกอบการตอบ</small>
</div>
<div class="condition-box">
<strong>ลำดับการทำงาน:</strong> (1) Intent เป็น system message แรก (2) ถ้าเปิด RAG จะต่อ RAG Context เข้า system (3) ข้อความผู้ใช้เป็น user message
</div>
</div>
<div style="margin-top:1rem;padding-top:1rem;border-top:1px solid #414868;">
<button type="submit">บันทึกการตั้งค่า</button>
<span id="settings-message" class="success" style="display:none;margin-left:0.75rem;"></span>
<div id="settings-error" class="error"></div>
</div>
</form>
<p style="margin-top:1.5rem;"><a href="room-lobby.html">← กลับไปล็อบบี้</a></p>
</div>
<script src="../app-base.js"></script>
<script>
(function () {
const BASE = typeof appPath === 'function' ? appPath('/Game') : '/Game';
function showLogin() {
document.getElementById('login-panel').style.display = 'block';
document.getElementById('settings-panel').style.display = 'none';
}
function showSettings() {
document.getElementById('login-panel').style.display = 'none';
document.getElementById('settings-panel').style.display = 'block';
}
document.getElementById('login-form').addEventListener('submit', function (e) {
e.preventDefault();
var errEl = document.getElementById('login-error');
errEl.textContent = '';
var password = document.getElementById('admin-password').value.trim();
fetch(BASE + '/api/ai-admin-login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: password }),
})
.then(function (r) {
if (!r.ok) return r.text().then(function (t) { throw new Error(r.status === 404 ? 'Server ยังไม่รองรับ API นี้ (404)' : (t || 'Error ' + r.status)); });
return r.json();
})
.then(function (data) {
if (data.ok) {
showSettings();
loadSettings();
} else {
errEl.textContent = data.error || 'เข้าสู่ระบบไม่สำเร็จ';
}
})
.catch(function (e) { errEl.textContent = e.message || 'เชื่อมต่อไม่สำเร็จ'; });
});
function loadSettings() {
fetch(BASE + '/api/ai-settings', { credentials: 'include' })
.then(function (r) {
if (r.status === 401) { showLogin(); return null; }
if (!r.ok) return r.text().then(function () { return null; });
return r.json();
})
.then(function (data) {
if (!data) return;
if (data.models && data.models.length) {
var sel = document.getElementById('openai-model');
sel.innerHTML = '';
data.models.forEach(function (m) {
var opt = document.createElement('option');
opt.value = m;
opt.textContent = m;
sel.appendChild(opt);
});
}
document.getElementById('openai-model').value = data.model || 'gpt-4o-mini';
document.getElementById('ai-intent').value = data.intent != null ? data.intent : '';
document.getElementById('rag-enabled').checked = !!data.rag_enabled;
document.getElementById('rag-context').value = data.rag_context != null ? data.rag_context : '';
document.getElementById('rag-context-wrap').style.opacity = data.rag_enabled ? '1' : '0.7';
});
}
document.getElementById('rag-enabled').addEventListener('change', function () {
document.getElementById('rag-context-wrap').style.opacity = this.checked ? '1' : '0.7';
});
function switchTab(tabId) {
document.querySelectorAll('.tab').forEach(function (t) { t.classList.remove('active'); });
document.querySelectorAll('.tab-content').forEach(function (c) { c.classList.remove('active'); });
var tabBtn = document.querySelector('.tab[data-tab="' + tabId + '"]');
var tabPanel = document.getElementById(tabId + '-tab');
if (tabBtn) tabBtn.classList.add('active');
if (tabPanel) tabPanel.classList.add('active');
}
document.querySelectorAll('.tab').forEach(function (btn) {
btn.addEventListener('click', function () { switchTab(btn.getAttribute('data-tab')); });
});
document.getElementById('settings-form').addEventListener('submit', function (e) {
e.preventDefault();
var msgEl = document.getElementById('settings-message');
var errEl = document.getElementById('settings-error');
msgEl.style.display = 'none';
errEl.textContent = '';
var payload = {
model: document.getElementById('openai-model').value,
intent: document.getElementById('ai-intent').value,
rag_enabled: document.getElementById('rag-enabled').checked,
rag_context: document.getElementById('rag-context').value,
};
var key = document.getElementById('openai-key').value.trim();
if (key) payload.openai_api_key = key;
fetch(BASE + '/api/ai-settings', {
method: 'PUT',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
.then(function (r) {
if (!r.ok) return r.text().then(function (t) { throw new Error(r.status === 404 ? 'Server ยังไม่รองรับ API นี้ (404)' : (t || 'Error ' + r.status)); });
return r.json();
})
.then(function (data) {
if (data && data.ok) {
msgEl.textContent = 'บันทึกแล้ว';
msgEl.style.display = 'block';
document.getElementById('openai-key').value = '';
} else {
errEl.textContent = (data && data.error) || 'บันทึกไม่สำเร็จ';
}
})
.catch(function (e) { errEl.textContent = e.message || 'เชื่อมต่อไม่สำเร็จ'; });
});
loadSettings();
})();
</script>
</body>
</html>
@@ -1,217 +0,0 @@
<!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>
.char-upload { margin: 1.5rem 0; padding: 1rem; background: #24283b; border-radius: 8px; max-width: 100%; }
.char-upload h2 { margin: 0 0 0.75rem 0; font-size: 1rem; color: #7aa2f7; }
.char-upload-grid { display: grid; gap: 0.75rem; margin: 0.75rem 0; }
/* Simple upload: 2x2 grid กระชับ ไม่ยาว */
.char-upload-simple .char-upload-grid {
grid-template-columns: repeat(2, 1fr);
max-width: 420px;
}
@media (min-width: 520px) {
.char-upload-simple .char-upload-grid {
grid-template-columns: repeat(4, 1fr);
max-width: 520px;
}
}
/* Advanced layered: 2 คอลัมน์บนมือถือ, 4 คอลัมน์จอใหญ่ พร้อม scroll ถ้าจำเป็น */
#char-upload-advanced .char-upload-grid {
grid-template-columns: repeat(2, 1fr);
max-width: 100%;
}
#char-upload-advanced .char-upload-cell { font-size: 0.8rem; }
#char-upload-advanced .char-upload-cell strong { display: block; margin-top: 0.4rem; color: #7aa2f7; font-size: 0.75rem; }
#char-upload-advanced .char-upload-cell strong:first-of-type { margin-top: 0; }
#char-upload-advanced .char-upload-cell label { font-size: 0.75rem; margin-bottom: 0.15rem; }
#char-upload-advanced .char-upload-cell input[type="file"] { font-size: 0.7rem; padding: 0.2rem 0.4rem; }
@media (min-width: 900px) {
#char-upload-advanced .char-upload-grid {
grid-template-columns: repeat(4, 1fr);
}
}
.char-upload-cell { text-align: center; min-width: 0; }
.char-upload-cell label { display: block; font-size: 0.85rem; color: #a9b1d6; margin-bottom: 0.25rem; }
.char-upload-cell input[type="file"] { font-size: 0.8rem; max-width: 100%; }
.char-upload-cell .preview { width: 56px; height: 56px; margin: 0.25rem auto; background: #1a1b26; border-radius: 6px; object-fit: contain; display: block; }
.char-upload-actions { margin-top: 1rem; display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.character-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 1rem; margin: 1rem 0; }
.character-item { text-align: center; cursor: pointer; padding: 0.5rem; border-radius: 8px; border: 2px solid transparent; transition: border-color 0.2s, background 0.2s; }
.character-item:hover { background: rgba(122,162,247,0.15); border-color: #7aa2f7; }
.character-item.selected { border-color: #9ece6a; background: rgba(158,206,106,0.2); }
.character-item .previews { display: flex; flex-wrap: wrap; justify-content: center; gap: 2px; margin-bottom: 0.25rem; }
.character-item .previews img { width: 36px; height: 36px; object-fit: contain; background: #24283b; border-radius: 4px; }
.character-item .name { font-size: 0.85rem; color: #a9b1d6; }
.character-actions { display: flex; justify-content: center; gap: 0.4rem; margin-top: 0.35rem; flex-wrap: wrap; }
.character-actions button { font-size: 0.75rem; padding: 0.25rem 0.5rem; }
.char-edit-btn { background: #565f89; color: #c0caf5; }
.char-edit-btn:hover { background: #7aa2f7; color: #1a1b26; }
.char-delete-btn { background: #414868; color: #a9b1d6; }
.char-delete-btn:hover { background: #f7768e; color: #1a1b26; }
/* ฝังใน Admin (iframe): ลดหัวข้อ/ลิงก์นำทาง */
html.character-embed-admin .character-page-links,
html.character-embed-admin body > .container > h1:first-of-type,
html.character-embed-admin .version-tag {
display: none !important;
}
html.character-embed-admin body {
overflow-x: hidden;
}
html.character-embed-admin .container {
padding-top: 0.35rem;
padding-bottom: 1.5rem;
}
</style>
</head>
<body>
<script>
(function () {
try {
var q = new URLSearchParams(location.search);
if (q.get('embed') === '1' || q.get('from') === 'admin') {
document.documentElement.classList.add('character-embed-admin');
document.body.classList.add('character-embed-admin');
}
} catch (e) {}
})();
</script>
<div class="container">
<h1>ตัวละคร (4 ทิศ)</h1>
<p class="character-page-links"><a href="index.html">กลับหน้าหลัก</a> · <a href="lobby.html">ล็อบบี้</a></p>
<section class="char-upload char-upload-simple">
<h2>อัปโหลดตัวละคร (บน · ล่าง · ซ้าย · ขวา)</h2>
<p class="legend">เลือกรูปทั้ง 4 ทิศ แล้วใส่ชื่อ (ไม่บังคับ) กดอัปโหลด</p>
<div class="char-upload-grid">
<div class="char-upload-cell">
<label>บน (up) <small>เลือกได้หลายเฟรม</small></label>
<input type="file" id="file-up" accept="image/*" multiple>
<img id="preview-up" class="preview" alt="">
</div>
<div class="char-upload-cell">
<label>ล่าง (down) <small>เลือกได้หลายเฟรม</small></label>
<input type="file" id="file-down" accept="image/*" multiple>
<img id="preview-down" class="preview" alt="">
</div>
<div class="char-upload-cell">
<label>ซ้าย (left) <small>เลือกได้หลายเฟรม</small></label>
<input type="file" id="file-left" accept="image/*" multiple>
<img id="preview-left" class="preview" alt="">
</div>
<div class="char-upload-cell">
<label>ขวา (right) <small>เลือกได้หลายเฟรม</small></label>
<input type="file" id="file-right" accept="image/*" multiple>
<img id="preview-right" class="preview" alt="">
</div>
</div>
<div class="char-upload-actions">
<label>ชื่อตัวละคร <input type="text" id="char-name" placeholder="เช่น mychar"></label>
<button type="button" id="btn-upload">อัปโหลด</button>
</div>
<p id="upload-status" style="margin-top:0.5rem;font-size:0.9rem;color:#a9b1d6;"></p>
</section>
<section class="char-upload" id="char-upload-advanced">
<h2>อัปโหลดตัวละครแบบเลเยอร์ (Shadow / Body / Hair / Head / Face)</h2>
<p class="legend">สำหรับผู้สร้างสกิน: แยกไฟล์ตามเลเยอร์ + ทิศ ระบบจะรวมเป็นเฟรมให้เองก่อนอัปโหลด (ไม่ใส่เงา = ใช้ค่าเริ่มต้น)</p>
<div class="char-upload-grid">
<div class="char-upload-cell">
<label>บน (up)</label>
<div>
<strong>Shadow</strong>
<label>Shadow <input type="file" id="layer-up-shadow" accept="image/*" multiple></label>
<strong>Body</strong>
<label>Color <input type="file" id="layer-up-bodyColor" accept="image/*" multiple></label>
<label>Stroke <input type="file" id="layer-up-bodyStroke" accept="image/*" multiple></label>
<strong>Hair</strong>
<label>Color <input type="file" id="layer-up-hairColor" accept="image/*" multiple></label>
<label>Stroke <input type="file" id="layer-up-hairStroke" accept="image/*" multiple></label>
<strong>Head</strong>
<label>Color <input type="file" id="layer-up-headColor" accept="image/*" multiple></label>
<label>Stroke <input type="file" id="layer-up-headStroke" accept="image/*" multiple></label>
<strong>Face</strong>
<label>Face <input type="file" id="layer-up-face" accept="image/*" multiple></label>
</div>
<img id="preview-up-layer" class="preview" alt="">
</div>
<div class="char-upload-cell">
<label>ล่าง (down)</label>
<div>
<strong>Shadow</strong>
<label>Shadow <input type="file" id="layer-down-shadow" accept="image/*" multiple></label>
<strong>Body</strong>
<label>Color <input type="file" id="layer-down-bodyColor" accept="image/*" multiple></label>
<label>Stroke <input type="file" id="layer-down-bodyStroke" accept="image/*" multiple></label>
<strong>Hair</strong>
<label>Color <input type="file" id="layer-down-hairColor" accept="image/*" multiple></label>
<label>Stroke <input type="file" id="layer-down-hairStroke" accept="image/*" multiple></label>
<strong>Head</strong>
<label>Color <input type="file" id="layer-down-headColor" accept="image/*" multiple></label>
<label>Stroke <input type="file" id="layer-down-headStroke" accept="image/*" multiple></label>
<strong>Face</strong>
<label>Face <input type="file" id="layer-down-face" accept="image/*" multiple></label>
</div>
<img id="preview-down-layer" class="preview" alt="">
</div>
<div class="char-upload-cell">
<label>ซ้าย (left)</label>
<div>
<strong>Shadow</strong>
<label>Shadow <input type="file" id="layer-left-shadow" accept="image/*" multiple></label>
<strong>Body</strong>
<label>Color <input type="file" id="layer-left-bodyColor" accept="image/*" multiple></label>
<label>Stroke <input type="file" id="layer-left-bodyStroke" accept="image/*" multiple></label>
<strong>Hair</strong>
<label>Color <input type="file" id="layer-left-hairColor" accept="image/*" multiple></label>
<label>Stroke <input type="file" id="layer-left-hairStroke" accept="image/*" multiple></label>
<strong>Head</strong>
<label>Color <input type="file" id="layer-left-headColor" accept="image/*" multiple></label>
<label>Stroke <input type="file" id="layer-left-headStroke" accept="image/*" multiple></label>
<strong>Face</strong>
<label>Face <input type="file" id="layer-left-face" accept="image/*" multiple></label>
</div>
<img id="preview-left-layer" class="preview" alt="">
</div>
<div class="char-upload-cell">
<label>ขวา (right)</label>
<div>
<strong>Shadow</strong>
<label>Shadow <input type="file" id="layer-right-shadow" accept="image/*" multiple></label>
<strong>Body</strong>
<label>Color <input type="file" id="layer-right-bodyColor" accept="image/*" multiple></label>
<label>Stroke <input type="file" id="layer-right-bodyStroke" accept="image/*" multiple></label>
<strong>Hair</strong>
<label>Color <input type="file" id="layer-right-hairColor" accept="image/*" multiple></label>
<label>Stroke <input type="file" id="layer-right-hairStroke" accept="image/*" multiple></label>
<strong>Head</strong>
<label>Color <input type="file" id="layer-right-headColor" accept="image/*" multiple></label>
<label>Stroke <input type="file" id="layer-right-headStroke" accept="image/*" multiple></label>
<strong>Face</strong>
<label>Face <input type="file" id="layer-right-face" accept="image/*" multiple></label>
</div>
<img id="preview-right-layer" class="preview" alt="">
</div>
</div>
<div class="char-upload-actions">
<p class="legend">ใช้ "ชื่อตัวละคร" ช่องเดียวกับด้านบน</p>
<button type="button" id="btn-upload-layered">อัปโหลดแบบเลเยอร์</button>
</div>
</section>
<h2>เลือกตัวละครที่ใช้ในเกม</h2>
<p class="legend">คลิกเลือก — ตัวนี้จะแสดงเมื่อเข้าโถงรอและเล่นเกม (เปลี่ยนทิศตามการเดิน)</p>
<p id="char-status">โหลดรายการตัวละคร...</p>
<div class="character-grid" id="character-grid"></div>
<p id="char-hint" style="display:none; color: #a9b1d6;">ยังไม่มีตัวละคร — อัปโหลดด้านบน</p>
</div>
<script src="../app-base.js"></script>
<script src="js/version.js?v=0.0112"></script>
<script src="js/character.js"></script>
<div class="version-tag">v —</div>
</body>
</html>
@@ -1,78 +0,0 @@
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Editor ฉาก — Game</title>
<link rel="stylesheet" href="css/style.css?v=0.0142">
</head>
<body>
<div class="container editor-page">
<h1>Editor ฉาก</h1>
<div class="editor-toolbar-wrap">
<div class="editor-toolbar">
<label>ประเภทเกม
<select id="game-type">
<option value="zep">เดินอิสระ (ZEP)</option>
<option value="lobby">ห้องโถง (รอผู้เล่น)</option>
<option value="gauntlet">พรมแดงสุดท้าย (Last Light)</option>
<option value="frogger">กบข้ามถนน</option>
<option value="quiz">เกมตอบคำถาม (ถูก/ผิด)</option>
<option value="stack">สลับจังหวะลงตึก (Stack)</option>
</select>
</label>
<span class="game-type-hint" id="game-type-hint"><strong>กบข้ามถนน</strong> = ตารางแถว · <strong>พรมแดงสุดท้าย</strong> = กระโดดหลบ · <strong>สลับลงตึก</strong> = กดจังหวะซ้อนบล็อก · <strong>ห้องโถง</strong> = วาดฉากรอผู้เล่น · <strong>พื้นที่เริ่มเกม</strong> = host ยืนช่องส้มก่อนกดเริ่ม</span>
<label>แก้จากฉากเดิม <select id="map-load"><option value="">โหลดรายการ...</option></select></label>
<button type="button" id="btn-load">โหลดฉาก</button>
<label>ชื่อฉาก <input type="text" id="map-name" placeholder="ชื่อฉาก (ไม่ซ้ำ)"></label>
<label>กว้าง <input type="number" id="map-w" value="20" min="10" max="50"></label>
<label>สูง <input type="number" id="map-h" value="15" min="10" max="40"></label>
<label>ขนาดタイル <input type="number" id="tile-size" value="32" min="16" max="64"></label>
<label>ขนาดตัวละคร (ช่อง) <input type="number" id="character-cells" value="1" min="1" max="4" title="ตัวละครจะใหญ่กี่ช่อง (1=ปกติ, 2=2x2)"></label>
<button type="button" id="btn-new">สร้างกริดใหม่</button>
<label>โหมดวาด:
<select id="draw-mode">
<option value="wall">กำแพง (เดินไม่ได้)</option>
<option value="noOverlap">โซนห้ามซ้อนผู้เล่น</option>
<option value="interactive">โซน interactive (จุดกด/ใช้ได้)</option>
<option value="spawnArea" id="draw-mode-option-spawn-area">พื้นที่สุ่มจุดเกิด (ฟ้า — ผู้เล่นใหม่สุ่มในช่องนี้)</option>
<option value="gauntletPlayerSpawn" id="draw-mode-option-gauntlet-player-spawn" hidden>พรมแดง: ลำดับเกิด 1–6 (คลิกซ้ายตามลำดับ)</option>
<option value="startGame" id="draw-mode-option-start-game">พื้นที่เริ่มเกม (host ยืนแล้วกดเริ่มได้)</option>
<option value="quizTrue" id="draw-mode-option-quiz-true">โซน ถูก (ตอบ จริง)</option>
<option value="quizFalse" id="draw-mode-option-quiz-false">โซน ผิด (ตอบ เท็จ)</option>
<option value="quizQuestion" id="draw-mode-option-quiz-question">พื้นที่โชว์ข้อความคำถาม</option>
<option value="color">สีช่อง</option>
</select>
</label>
<span id="cell-color-wrap" style="display:none;">
<label>สี <input type="color" id="cell-color" value="#24283b" title="สี"></label>
<label>Alpha <input type="range" id="cell-alpha" min="0" max="100" value="100" title="ความโปร่งใส 0=ใส 100=ทึบ"> <span id="cell-alpha-value">100</span>%</label>
</span>
<label class="editor-toggle"><input type="checkbox" id="show-map-in-game" checked> โชว์กริดในเกม</label>
<button type="button" id="btn-clear-gauntlet-spawns" hidden title="ล้างรายการจุดเกิดพรมแดงที่กำหนดเอง">ล้างลำดับ 16</button>
<button type="button" id="btn-spawn">ตั้งจุดเกิด</button>
<button type="button" id="btn-save">บันทึกฉาก</button>
<button type="button" id="btn-play-test" title="บันทึกฉากแล้วเท่านั้น — เปิดแท็บใหม่เล่นทดสอบด้วยตัวละครเริ่มต้น">ทดลองเล่น</button>
</div>
<div id="frogger-lanes-wrap" class="frogger-lanes" style="display:none;">
<h3 id="frogger-lanes-title">ตั้งค่าแถว — กบข้ามถนน</h3>
<p class="legend" id="frogger-lanes-legend">แถวบน (y=0) = ปลายทาง · แถวล่าง = จุดเกิด · ถนน/น้ำ ตั้งความเร็ว ทิศทาง และ <strong>ระยะห่าง</strong> (ยิ่งมากยิ่งห่าง เกมง่ายขึ้น)</p>
<div id="frogger-lanes-table"></div>
</div>
<div class="editor-bg-section">
<label>พื้นหลัง (รูปแผนที่) <input type="file" id="bg-image" accept="image/*"></label>
<button type="button" id="btn-clear-bg">ลบรูปพื้นหลัง</button>
</div>
<p class="legend editor-legend-short">คลิกซ้าย = วาด · คลิกขวา = ลบ · กำแพง · โซนห้ามซ้อน · โซน interactive · <strong>พื้นที่สุ่มจุดเกิด</strong> (ฟ้าอ่อน — ผู้เล่นเข้าห้องสุ่มยืนในช่องนี้; ไม่วาด = ใช้ปุ่มตั้งจุดเกิด) · <strong>พื้นที่เริ่มเกม</strong> (เฉพาะห้องโถง สีส้ม) · <strong>เกมถามตอบ</strong>: พื้นที่โชว์คำถาม (ทอง) · สีช่อง = เลือกสี+Alpha แล้วคลิกบนกริด · «โชว์กริดในเกม» = ยกเลิก = ไม่โชว์แค่กริด/กำแพง (รูปพื้นหลังยังโชว์)</p>
</div>
<div class="editor-workspace">
<div class="canvas-wrap"><canvas id="editor-canvas"></canvas></div>
<p class="legend" id="editor-status">บันทึกแล้วจะได้รหัสฉาก ใช้รหัสนี้ตอนสร้างห้องในล็อบบี้</p>
</div>
</div>
<script src="../app-base.js"></script>
<script src="js/version.js?v=0.0104"></script>
<script src="js/editor.js?v=0.016"></script>
<div class="version-tag">v —</div>
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 272 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 995 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 867 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Some files were not shown because too many files have changed in this diff Show More