update game

This commit is contained in:
2026-05-27 04:30:21 +00:00
parent 4e4231879e
commit c5b086e8f1
1167 changed files with 161743 additions and 381 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+7
View File
@@ -4920,6 +4920,13 @@
if (data && data.detectiveMinigame) {
try { sessionStorage.setItem('detectiveMinigameReturn', '1'); } catch (e) { /* ignore */ }
}
try {
var avEl = document.getElementById('room-lobby-profile-avatar');
var cidPlay = getStoredCharacterId();
if (avEl && avEl.src && cidPlay && String(avEl.src).indexOf('data:image/') === 0) {
sessionStorage.setItem('justicePlayLobbyAvatar:' + cidPlay, avEl.src);
}
} catch (eAv) { /* ignore */ }
let q = 'play.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick);
if (mid) q += '&map=' + encodeURIComponent(mid);
if (lobbyLevelStr) q += '&lobbyLevel=' + encodeURIComponent(lobbyLevelStr);
+11 -1
View File
@@ -13,6 +13,7 @@
if (q.indexOf('preview=1') !== -1 && q.indexOf('editorEmbed=1') !== -1) {
document.documentElement.classList.add('play-preview-editor-embed');
}
document.documentElement.classList.add('play-hide-top-chrome');
} catch (e) { /* ignore */ }
})();
</script>
@@ -28,6 +29,15 @@
box-sizing: border-box;
}
body { min-height: 100dvh; }
/* ทุกเกม — ซ่อนเฉพาะแถบห้องบน (ออกจากห้อง + ชื่อห้อง); คง SCORE/TIME/หน้าผู้เล่นใน #play-cyber-hud */
html.play-hide-top-chrome .game-header {
display: none !important;
visibility: hidden !important;
pointer-events: none !important;
}
html.play-hide-top-chrome .play-canvas-stack {
margin-top: 0;
}
.game-wrap {
width: 100%;
box-sizing: border-box;
@@ -3203,7 +3213,7 @@
</div>
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0306"></script>
<script src="js/play.js?v=0.0446"></script>
<script src="js/play.js?v=0.0457"></script>
<div class="version-tag">v —</div>
</body>
</html>
@@ -0,0 +1,219 @@
<!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>
@@ -0,0 +1,217 @@
<!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>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,78 @@
<!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.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 995 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 867 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

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