optimise and get more flow
@@ -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="ล้างรายการจุดเกิดพรมแดงที่กำหนดเอง">ล้างลำดับ 1–6</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>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 195 B |
|
Before Width: | Height: | Size: 264 B |
|
Before Width: | Height: | Size: 325 B |
|
Before Width: | Height: | Size: 272 B |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 262 B |
|
Before Width: | Height: | Size: 250 B |
|
Before Width: | Height: | Size: 259 B |
|
Before Width: | Height: | Size: 273 B |
|
Before Width: | Height: | Size: 273 B |
|
Before Width: | Height: | Size: 271 B |
|
Before Width: | Height: | Size: 287 B |
|
Before Width: | Height: | Size: 267 B |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 279 B |
|
Before Width: | Height: | Size: 298 B |
|
Before Width: | Height: | Size: 264 B |
|
Before Width: | Height: | Size: 254 B |
|
Before Width: | Height: | Size: 282 B |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 219 B |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 299 B |
|
Before Width: | Height: | Size: 333 B |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 218 B |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 995 B |
|
Before Width: | Height: | Size: 867 B |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 443 B |
|
Before Width: | Height: | Size: 201 B |
|
Before Width: | Height: | Size: 294 B |
|
Before Width: | Height: | Size: 228 B |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.3 MiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |