diff --git a/www/html/Admin/admin.js b/www/html/Admin/admin.js
index 708750f..d05c902 100644
--- a/www/html/Admin/admin.js
+++ b/www/html/Admin/admin.js
@@ -4674,6 +4674,8 @@
var embedPanels = [
{ tab: 'map-editor', panel: 'tab-panel-map-editor', fsBtn: 'btn-map-editor-fullscreen' },
{ tab: 'characters', panel: 'tab-panel-characters', fsBtn: 'btn-character-fullscreen' },
+ { tab: 'qb-map-editor', panel: 'tab-panel-qb-map-editor', fsBtn: 'btn-qb-map-editor-fullscreen' },
+ { tab: 'ai-admin', panel: 'tab-panel-ai-admin', fsBtn: 'btn-ai-admin-fullscreen' },
];
embedPanels.forEach(function (ep) {
if (name === ep.tab) return;
@@ -4704,6 +4706,14 @@
if (name === 'troublesome') loadTroublesomePanel();
if (name === 'test-mode') { loadForcedMinigamePanel(); loadSpecialCardByMapPanel(); loadTroublesomeForcePanel(); }
if (name === 'sound') loadSoundPanel();
+ if (name === 'qb-map-editor') ensureEmbedFrameSrc('admin-qb-map-editor-frame');
+ if (name === 'ai-admin') ensureEmbedFrameSrc('admin-ai-admin-frame');
+ }
+
+ /* ฝัง iframe editor แบบ lazy — ตั้ง src จาก data-src ครั้งแรกที่เปิด tab (ไม่โหลดตอนเปิดหน้า Admin) */
+ function ensureEmbedFrameSrc(id) {
+ var f = el(id);
+ if (f && !f.getAttribute('src') && f.getAttribute('data-src')) f.setAttribute('src', f.getAttribute('data-src'));
}
function boot() {
@@ -4894,6 +4904,8 @@
}
bindEmbedPanelFullscreen('btn-map-editor-fullscreen', 'tab-panel-map-editor');
bindEmbedPanelFullscreen('btn-character-fullscreen', 'tab-panel-characters');
+ bindEmbedPanelFullscreen('btn-qb-map-editor-fullscreen', 'tab-panel-qb-map-editor');
+ bindEmbedPanelFullscreen('btn-ai-admin-fullscreen', 'tab-panel-ai-admin');
/* Test Mode toggle — เก็บใน localStorage (share ระหว่าง Admin กับ Game ภายใต้ origin เดียวกัน) */
var TEST_MODE_KEY = 'justiceTestMode';
diff --git a/www/html/Admin/admin.js.bak-20260706-062808 b/www/html/Admin/admin.js.bak-20260706-062808
new file mode 100644
index 0000000..708750f
--- /dev/null
+++ b/www/html/Admin/admin.js.bak-20260706-062808
@@ -0,0 +1,5729 @@
+(function () {
+ 'use strict';
+
+ var API = '/Admin/api/';
+ /** ผ่าน PHP Admin เพื่อให้ PUT ถึง Node แม้ nginx ไม่ proxy /Game/api/ ไปยัง Node */
+ var GAME_QUIZ_API = '/Admin/api/game-quiz-settings.php';
+ var GAME_TIMING_API = '/Game/api/game-timing';
+ var SOUND_SETTINGS_API = '/Game/api/sound-settings';
+ var GAME_GAUNTLET_ASSETS_API = '/Game/api/gauntlet-assets';
+ /** ลำดับอัปโหลดจริงอยู่ที่ quizCarryPlaqueUploadWithFallback — Node ก่อน แล้วค่อย PHP */
+ var GAME_QUIZ_CARRY_PLAQUE_UPLOAD_NODE = '/Game/api/quiz-carry-plaque-upload';
+ var GAME_QUIZ_CARRY_PLAQUE_UPLOAD_PHP = '/Admin/api/game-quiz-carry-plaque-upload.php';
+ /** >0 ระหว่างอัปโหลดรูปป้าย — ห้ามบันทึกก่อนจบไม่งั้น plaqueImageUrl ว่างถูกเขียนลง JSON */
+ var quizCarryPlaqueUploadPending = 0;
+ /** epoch ms เมื่อเริ่มอัปโหลดล่าสุด — กันสถานะค้างบล็อกบันทึกตลอด */
+ var quizCarryPlaqueUploadStartMs = 0;
+ var quizCarrySaveInFlight = false;
+ /** ซิงก์ range ↔ ตัวเลข carryChoicePlaqueMapScale (wire ครั้งเดียว) */
+ var quizCarryPlaqueScaleControlsWired = false;
+ /** สำรอง URL หลังอัปโหลดสำเร็จต่อช่อง — กัน race ที่ช่อง text ว่างตอน buildForSave แต่รูปอัปโหลดแล้วจริง */
+ var quizCarryPlaquePendingUrlBySlot = {};
+ var stackGameSaveInFlight = false;
+ var megaVirusSaveInFlight = false;
+ /** โหมดแทนที่รูป: ชื่อไฟล์เป้าหมายก่อนเปิด file picker */
+ var gauntletReplaceTarget = null;
+ var cred = { credentials: 'include' };
+
+ function el(id) { return document.getElementById(id); }
+
+ function api(path, opts) {
+ opts = opts || {};
+ opts.credentials = 'include';
+ if (!opts.headers) opts.headers = {};
+ if (opts.body && typeof opts.body === 'object' && !(opts.body instanceof FormData)) {
+ opts.headers['Content-Type'] = 'application/json';
+ opts.body = JSON.stringify(opts.body);
+ }
+ return fetch(API + path, opts).then(function (r) {
+ return r.text().then(function (text) {
+ var j = {};
+ try {
+ j = text ? JSON.parse(text) : {};
+ } catch (e) {
+ if (!r.ok) throw new Error(text || r.statusText);
+ throw e;
+ }
+ if (!r.ok) throw new Error(j.error || r.statusText || 'Request failed');
+ return j;
+ });
+ });
+ }
+
+ function setMsg(id, text, cls) {
+ var n = el(id);
+ if (!n) return;
+ n.textContent = text || '';
+ n.className = 'msg' + (cls ? ' ' + cls : '');
+ }
+
+ function gameQuizFetch(method, body) {
+ var m = method || 'GET';
+ var url = GAME_QUIZ_API;
+ if (m === 'GET' && body == null) {
+ url += (GAME_QUIZ_API.indexOf('?') >= 0 ? '&' : '?') + '_=' + Date.now();
+ }
+ var opts = { method: m, credentials: 'include', cache: 'no-store' };
+ if (body != null) {
+ opts.headers = { 'Content-Type': 'application/json' };
+ opts.body = JSON.stringify(body);
+ }
+ return fetch(url, opts).then(function (r) {
+ return r.text().then(function (text) {
+ var j = {};
+ try {
+ j = text ? JSON.parse(text) : {};
+ } catch (e) {
+ if (!r.ok) {
+ throw new Error((text || '').slice(0, 280) || r.statusText || 'Request failed');
+ }
+ throw new Error('คำตอบไม่ใช่ JSON (HTTP ' + r.status + ') — ตรวจ nginx / PHP proxy');
+ }
+ if (!r.ok) {
+ throw new Error(j.error || ('HTTP ' + r.status + ' ' + r.statusText));
+ }
+ return j;
+ });
+ });
+ }
+
+ function gameTimingFetch(method, body) {
+ var m = method || 'GET';
+ var url = GAME_TIMING_API;
+ if (m === 'GET' && body == null) {
+ url += (GAME_TIMING_API.indexOf('?') >= 0 ? '&' : '?') + '_=' + Date.now();
+ }
+ var opts = { method: m, credentials: 'include', cache: 'no-store' };
+ if (body != null) {
+ opts.headers = { 'Content-Type': 'application/json' };
+ opts.body = JSON.stringify(body);
+ }
+ return fetch(url, opts).then(function (r) {
+ return r.text().then(function (text) {
+ var j = {};
+ try {
+ j = text ? JSON.parse(text) : {};
+ } catch (e) {
+ if (!r.ok) {
+ var plain = (text || r.statusText || 'Request failed').trim();
+ if (r.status === 404) {
+ plain += ' — รีสตาร์ท Node ที่รัน Game/server.js (pm2 / systemd) หลัง deploy · Restart Game Node after deploy';
+ }
+ throw new Error(plain);
+ }
+ throw e;
+ }
+ if (!r.ok) {
+ var base = j.error || r.statusText || 'Request failed';
+ if (r.status === 404) {
+ base += ' — รีสตาร์ท Node ที่รัน Game/server.js (pm2 / systemd) หลัง deploy · Restart Game Node after deploy';
+ }
+ throw new Error(base);
+ }
+ return j;
+ });
+ });
+ }
+
+ function addQuizAdminRow(text, answerTrue) {
+ var list = el('quiz-admin-questions-list');
+ if (!list) return;
+ var row = document.createElement('div');
+ row.className = 'quiz-admin-q-row';
+
+ var lab1 = document.createElement('label');
+ lab1.className = 'quiz-admin-q-label';
+ lab1.appendChild(document.createTextNode('คำถาม '));
+ var inp = document.createElement('input');
+ inp.type = 'text';
+ inp.className = 'quiz-admin-q-text';
+ inp.placeholder = 'พิมพ์คำถาม...';
+ inp.maxLength = 500;
+ inp.value = text || '';
+ lab1.appendChild(inp);
+
+ var lab2 = document.createElement('label');
+ lab2.className = 'quiz-admin-ans-label';
+ lab2.appendChild(document.createTextNode('คำตอบถูก '));
+ var sel = document.createElement('select');
+ sel.className = 'quiz-admin-q-ans';
+ [['true', 'จริง (ถูก)'], ['false', 'เท็จ (ผิด)']].forEach(function (pair) {
+ var o = document.createElement('option');
+ o.value = pair[0];
+ o.textContent = pair[1];
+ var isTrue = answerTrue !== false;
+ if (pair[0] === 'true' ? isTrue : !isTrue) o.selected = true;
+ sel.appendChild(o);
+ });
+ lab2.appendChild(sel);
+
+ var rm = document.createElement('button');
+ rm.type = 'button';
+ rm.className = 'btn btn-danger btn-sm quiz-admin-q-remove';
+ rm.setAttribute('aria-label', 'ลบข้อนี้');
+ rm.textContent = 'ลบ';
+ rm.addEventListener('click', function () {
+ row.remove();
+ if (!list.querySelector('.quiz-admin-q-row')) addQuizAdminRow('', true);
+ });
+
+ row.appendChild(lab1);
+ row.appendChild(lab2);
+ row.appendChild(rm);
+ list.appendChild(row);
+ }
+
+ function renderQuizAdminQuestions(arr) {
+ var list = el('quiz-admin-questions-list');
+ if (!list) return;
+ list.innerHTML = '';
+ var rows = (arr && arr.length) ? arr : [{ text: '', answerTrue: true }];
+ rows.forEach(function (q) {
+ addQuizAdminRow(q.text || '', q.answerTrue !== false);
+ });
+ }
+
+ function loadQuizSettingsPanel() {
+ gameQuizFetch('GET').then(function (data) {
+ el('quiz-read-sec').value = String(Math.round((data.readMs || 10000) / 1000));
+ el('quiz-answer-sec').value = String(Math.round((data.answerMs || 5000) / 1000));
+ var bMs = data.betweenMs != null ? data.betweenMs : 3500;
+ el('quiz-between-sec').value = String(Math.max(0, Math.round(bMs / 1000)));
+ var rqIn = el('quiz-round-q-count');
+ if (rqIn) {
+ var rq = data.quizRoundQuestionCount != null ? parseInt(data.quizRoundQuestionCount, 10) : 10;
+ if (Number.isNaN(rq) || rq < 1) rq = 10;
+ rq = Math.min(50, rq);
+ rqIn.value = String(rq);
+ }
+ renderQuizAdminQuestions(data.questions);
+ if (data.quizMapPanelTheme && typeof data.quizMapPanelTheme === 'object') {
+ quizTfFillFormFromTheme(data.quizMapPanelTheme);
+ } else {
+ quizTfFillFormFromTheme({});
+ }
+ setMsg('quiz-settings-msg', '', '');
+ }).catch(function (e) {
+ setMsg('quiz-settings-msg', e.message || 'โหลดไม่ได้', 'error');
+ });
+ }
+
+ function saveQuizSettingsPanel() {
+ var readS = parseInt(el('quiz-read-sec').value, 10);
+ var ansS = parseInt(el('quiz-answer-sec').value, 10);
+ var betS = parseInt(el('quiz-between-sec').value, 10);
+ if (Number.isNaN(readS) || readS < 1) readS = 10;
+ if (Number.isNaN(ansS) || ansS < 1) ansS = 5;
+ if (Number.isNaN(betS) || betS < 0) betS = 3;
+ readS = Math.min(300, readS);
+ ansS = Math.min(300, ansS);
+ betS = Math.min(120, betS);
+
+ var roundQEl = el('quiz-round-q-count');
+ var roundQ = roundQEl ? parseInt(roundQEl.value, 10) : 10;
+ if (Number.isNaN(roundQ) || roundQ < 1) roundQ = 10;
+ roundQ = Math.min(50, roundQ);
+
+ var questions = [];
+ document.querySelectorAll('#quiz-admin-questions-list .quiz-admin-q-row').forEach(function (row) {
+ var inp = row.querySelector('.quiz-admin-q-text');
+ var sel = row.querySelector('.quiz-admin-q-ans');
+ var t = inp && inp.value ? String(inp.value).trim() : '';
+ if (!t) return;
+ questions.push({ text: t, answerTrue: !!(sel && sel.value === 'true') });
+ });
+
+ gameQuizFetch('PUT', {
+ readMs: readS * 1000,
+ answerMs: ansS * 1000,
+ betweenMs: betS * 1000,
+ quizRoundQuestionCount: roundQ,
+ questions: questions,
+ quizMapPanelTheme: quizTfBuildThemeForSave(),
+ }).then(function () {
+ setMsg('quiz-settings-msg', 'บันทึกแล้ว', 'ok');
+ }).catch(function (e) {
+ setMsg('quiz-settings-msg', e.message || 'บันทึกไม่ได้', 'error');
+ });
+ }
+
+ /* ===== ชุดคำถามพิเศษ (Level×Case = 15 ชุด) รับการ์ดพิเศษ ===== */
+ var SPECIAL_CARD_RARITIES = ['common', 'rare', 'legendary'];
+ var SPECIAL_CARD_ASSET_BASE = '/Game/img/special-cards/';
+ var SPECIAL_CARD_CATALOG = [
+ { id: 1, en: 'Time Dilation', th: 'เพิ่มเวลาเล่นมินิเกมทันที (+10 วินาที)' },
+ { id: 2, en: 'Extension', th: 'เพิ่มเวลาในห้องพิจารณาคดี (+10 วิ FINAL VOTE)' },
+ { id: 3, en: 'Ban Card', th: 'ห้ามเล่นมินิเกม 1 ตา' },
+ { id: 4, en: 'Silence Card', th: 'การ์ดปิดปาก' },
+ { id: 5, en: 'Bail Coin', th: 'เหรียญประกันตัว' },
+ { id: 6, en: 'Fund', th: 'รับ coin เพิ่ม' },
+ { id: 7, en: 'Free Evidence', th: 'รับหลักฐานฟรี' },
+ ];
+ var specialQuizSetsState = {};
+ var specialQuizCurrentKey = 'L1_C1';
+ var specialCardPickerBuilt = false;
+
+ function specialCardTxtFilename(id) {
+ return id === 7 ? 'card--7txt.png' : 'card-' + id + '-txt.png';
+ }
+
+ function specialCardImageUrl(id) {
+ return SPECIAL_CARD_ASSET_BASE + 'card-' + id + '.png';
+ }
+
+ function specialCardTxtImageUrl(id) {
+ return SPECIAL_CARD_ASSET_BASE + specialCardTxtFilename(id);
+ }
+
+ function specialCardCatalogEntry(id) {
+ for (var i = 0; i < SPECIAL_CARD_CATALOG.length; i++) {
+ if (SPECIAL_CARD_CATALOG[i].id === id) return SPECIAL_CARD_CATALOG[i];
+ }
+ return null;
+ }
+
+ function resolveSpecialCardIdFromSaved(card) {
+ if (!card || typeof card !== 'object') return null;
+ var cid = Number(card.cardId);
+ if (cid >= 1 && cid <= 7) return cid;
+ var url = String(card.imageUrl || '');
+ var m = url.match(/\/card-(\d)\.png/i);
+ return m ? Number(m[1]) : null;
+ }
+
+ function updateSpecialCardPreview() {
+ var prev = el('special-card-preview');
+ var img = el('special-card-preview-img');
+ var cid = parseInt(el('special-card-id') && el('special-card-id').value, 10);
+ if (!prev || !img) return;
+ if (!(cid >= 1 && cid <= 7)) {
+ prev.hidden = true;
+ img.removeAttribute('src');
+ return;
+ }
+ img.src = specialCardImageUrl(cid);
+ img.alt = (specialCardCatalogEntry(cid) || {}).en || ('Card ' + cid);
+ prev.hidden = false;
+ }
+
+ function setSpecialCardPickerSelection(cardId) {
+ var picker = el('special-card-picker');
+ if (!picker) return;
+ picker.querySelectorAll('.sq-card-pick').forEach(function (btn) {
+ var on = String(btn.getAttribute('data-card-id')) === String(cardId);
+ btn.classList.toggle('is-selected', on);
+ btn.setAttribute('aria-checked', on ? 'true' : 'false');
+ });
+ }
+
+ function selectSpecialCard(cardId, fillMeta) {
+ var idInp = el('special-card-id');
+ var imgInp = el('special-card-image');
+ var txtInp = el('special-card-txt-image');
+ if (!(cardId >= 1 && cardId <= 7)) {
+ if (idInp) idInp.value = '';
+ if (imgInp) imgInp.value = '';
+ if (txtInp) txtInp.value = '';
+ setSpecialCardPickerSelection(null);
+ updateSpecialCardPreview();
+ return;
+ }
+ var entry = specialCardCatalogEntry(cardId);
+ if (idInp) idInp.value = String(cardId);
+ if (imgInp) imgInp.value = specialCardImageUrl(cardId);
+ if (txtInp) txtInp.value = specialCardTxtImageUrl(cardId);
+ if (fillMeta && entry) {
+ if (el('special-card-th')) el('special-card-th').value = entry.th;
+ if (el('special-card-en')) el('special-card-en').value = entry.en;
+ }
+ setSpecialCardPickerSelection(cardId);
+ updateSpecialCardPreview();
+ }
+
+ function ensureSpecialCardPicker() {
+ if (specialCardPickerBuilt) return;
+ var picker = el('special-card-picker');
+ if (!picker) return;
+ picker.innerHTML = '';
+ SPECIAL_CARD_CATALOG.forEach(function (entry) {
+ var btn = document.createElement('button');
+ btn.type = 'button';
+ btn.className = 'sq-card-pick';
+ btn.setAttribute('role', 'radio');
+ btn.setAttribute('aria-checked', 'false');
+ btn.setAttribute('data-card-id', String(entry.id));
+ btn.title = entry.en + ' — ' + entry.th;
+ var img = document.createElement('img');
+ img.src = specialCardImageUrl(entry.id);
+ img.alt = entry.en;
+ img.width = 72;
+ img.height = 96;
+ img.decoding = 'async';
+ img.loading = 'lazy';
+ var cap = document.createElement('span');
+ cap.className = 'sq-card-pick-cap';
+ cap.textContent = entry.en;
+ btn.appendChild(img);
+ btn.appendChild(cap);
+ btn.addEventListener('click', function () {
+ selectSpecialCard(entry.id, true);
+ });
+ picker.appendChild(btn);
+ });
+ specialCardPickerBuilt = true;
+ }
+
+ function specialQuizSelectorKey() {
+ var L = el('special-quiz-level');
+ var C = el('special-quiz-case');
+ return 'L' + ((L && L.value) || '1') + '_C' + ((C && C.value) || '1');
+ }
+
+ function updateSpecialQuizCount() {
+ var c = el('special-quiz-count');
+ if (!c) return;
+ var n = document.querySelectorAll('#special-quiz-questions-list .sq-mcq-row').length;
+ c.textContent = '(' + n + ' ข้อ)';
+ }
+
+ var SPECIAL_QUIZ_CHOICE_COUNT = 4;
+ var SPECIAL_QUIZ_LETTERS = ['A', 'B', 'C', 'D', 'E', 'F'];
+
+ /** คำถามตัวเลือก A/B/C/D — เลือกคำตอบถูก 1 ข้อ */
+ function addSpecialQuizRow(q) {
+ q = q || {};
+ var list = el('special-quiz-questions-list');
+ if (!list) return;
+ var choices = Array.isArray(q.choices) ? q.choices : [];
+ var correctIndex = Number(q.correctIndex);
+ if (!(correctIndex >= 0 && correctIndex < SPECIAL_QUIZ_CHOICE_COUNT)) correctIndex = 0;
+
+ var row = document.createElement('div');
+ row.className = 'quiz-admin-q-row sq-mcq-row';
+
+ var qLab = document.createElement('label');
+ qLab.className = 'sq-mcq-q';
+ qLab.appendChild(document.createTextNode('คำถาม'));
+ var qInp = document.createElement('input');
+ qInp.type = 'text';
+ qInp.className = 'sq-mcq-text';
+ qInp.maxLength = 500;
+ qInp.placeholder = 'พิมพ์คำถาม...';
+ qInp.value = q.text || '';
+ qLab.appendChild(qInp);
+
+ var grid = document.createElement('div');
+ grid.className = 'sq-mcq-choices';
+ for (var i = 0; i < SPECIAL_QUIZ_CHOICE_COUNT; i++) {
+ var cl = document.createElement('label');
+ cl.className = 'sq-mcq-choice';
+ var badge = document.createElement('span');
+ badge.className = 'sq-mcq-letter';
+ badge.textContent = SPECIAL_QUIZ_LETTERS[i];
+ var ci = document.createElement('input');
+ ci.type = 'text';
+ ci.className = 'sq-mcq-choice-input';
+ ci.maxLength = 200;
+ ci.placeholder = 'ตัวเลือก ' + SPECIAL_QUIZ_LETTERS[i];
+ ci.value = choices[i] || '';
+ ci.setAttribute('data-choice-index', String(i));
+ cl.appendChild(badge);
+ cl.appendChild(ci);
+ grid.appendChild(cl);
+ }
+
+ var foot = document.createElement('div');
+ foot.className = 'sq-mcq-foot';
+ var ansLab = document.createElement('label');
+ ansLab.className = 'sq-mcq-ans';
+ ansLab.appendChild(document.createTextNode('คำตอบที่ถูก '));
+ var ansSel = document.createElement('select');
+ ansSel.className = 'sq-mcq-correct';
+ for (var j = 0; j < SPECIAL_QUIZ_CHOICE_COUNT; j++) {
+ var o = document.createElement('option');
+ o.value = String(j);
+ o.textContent = SPECIAL_QUIZ_LETTERS[j];
+ if (j === correctIndex) o.selected = true;
+ ansSel.appendChild(o);
+ }
+ ansLab.appendChild(ansSel);
+
+ var rm = document.createElement('button');
+ rm.type = 'button';
+ rm.className = 'btn btn-danger btn-sm quiz-admin-q-remove';
+ rm.setAttribute('aria-label', 'ลบข้อนี้');
+ rm.textContent = 'ลบ';
+ rm.addEventListener('click', function () {
+ row.remove();
+ updateSpecialQuizCount();
+ });
+ foot.appendChild(ansLab);
+ foot.appendChild(rm);
+
+ row.appendChild(qLab);
+ row.appendChild(grid);
+ row.appendChild(foot);
+ list.appendChild(row);
+ }
+
+ function renderSpecialQuizSet(key) {
+ ensureSpecialCardPicker();
+ var set = specialQuizSetsState[key] || { questions: [], specialCard: {} };
+ var card = (set.specialCard && typeof set.specialCard === 'object') ? set.specialCard : {};
+ var cardId = resolveSpecialCardIdFromSaved(card);
+ selectSpecialCard(cardId, false);
+ if (el('special-card-th')) el('special-card-th').value = card.th || (cardId ? (specialCardCatalogEntry(cardId) || {}).th : '') || '';
+ if (el('special-card-en')) el('special-card-en').value = card.en || (cardId ? (specialCardCatalogEntry(cardId) || {}).en : '') || '';
+ if (el('special-card-rarity')) {
+ el('special-card-rarity').value = SPECIAL_CARD_RARITIES.indexOf(card.rarity) >= 0 ? card.rarity : 'rare';
+ }
+ if (cardId && el('special-card-image')) el('special-card-image').value = card.imageUrl || specialCardImageUrl(cardId);
+ if (cardId && el('special-card-txt-image')) el('special-card-txt-image').value = card.txtImageUrl || specialCardTxtImageUrl(cardId);
+ var list = el('special-quiz-questions-list');
+ if (list) {
+ list.innerHTML = '';
+ var qs = (set.questions && set.questions.length)
+ ? set.questions
+ : [{ type: 'mcq', text: '', choices: ['', '', '', ''], correctIndex: 0 }];
+ qs.forEach(addSpecialQuizRow);
+ }
+ var tag = el('special-quiz-set-tag');
+ if (tag) tag.textContent = 'ชุดที่แก้: ' + key;
+ updateSpecialQuizCount();
+ }
+
+ function captureSpecialQuizForm(key) {
+ var questions = [];
+ document.querySelectorAll('#special-quiz-questions-list .sq-mcq-row').forEach(function (row) {
+ var inp = row.querySelector('.sq-mcq-text');
+ var t = inp && inp.value ? String(inp.value).trim() : '';
+ if (!t) return;
+ var choices = [];
+ row.querySelectorAll('.sq-mcq-choice-input').forEach(function (ci) {
+ choices.push(String(ci.value || '').trim());
+ });
+ if (choices.filter(function (c) { return c; }).length < 2) return;
+ var sel = row.querySelector('.sq-mcq-correct');
+ var correctIndex = sel ? parseInt(sel.value, 10) : 0;
+ if (!(correctIndex >= 0 && correctIndex < choices.length) || !choices[correctIndex]) {
+ correctIndex = choices.findIndex(function (c) { return c; });
+ if (correctIndex < 0) correctIndex = 0;
+ }
+ questions.push({ type: 'mcq', text: t, choices: choices, correctIndex: correctIndex });
+ });
+ var rar = el('special-card-rarity') ? el('special-card-rarity').value : 'rare';
+ var cardId = parseInt(el('special-card-id') && el('special-card-id').value, 10);
+ if (!(cardId >= 1 && cardId <= 7)) cardId = null;
+ specialQuizSetsState[key] = {
+ questions: questions,
+ specialCard: {
+ cardId: cardId,
+ th: el('special-card-th') ? String(el('special-card-th').value || '').trim() : '',
+ en: el('special-card-en') ? String(el('special-card-en').value || '').trim() : '',
+ rarity: SPECIAL_CARD_RARITIES.indexOf(rar) >= 0 ? rar : 'rare',
+ imageUrl: el('special-card-image') ? String(el('special-card-image').value || '').trim() : '',
+ txtImageUrl: el('special-card-txt-image') ? String(el('special-card-txt-image').value || '').trim() : '',
+ },
+ };
+ }
+
+ function onSpecialQuizSelectorChange() {
+ captureSpecialQuizForm(specialQuizCurrentKey);
+ specialQuizCurrentKey = specialQuizSelectorKey();
+ renderSpecialQuizSet(specialQuizCurrentKey);
+ }
+
+ function loadSpecialQuizPanel() {
+ setMsg('special-quiz-msg', 'กำลังโหลด...', '');
+ gameQuizFetch('GET').then(function (data) {
+ specialQuizSetsState = (data.specialQuizSets && typeof data.specialQuizSets === 'object') ? data.specialQuizSets : {};
+ specialQuizCurrentKey = specialQuizSelectorKey();
+ renderSpecialQuizSet(specialQuizCurrentKey);
+ setMsg('special-quiz-msg', '', '');
+ }).catch(function (e) {
+ setMsg('special-quiz-msg', e.message || 'โหลดไม่ได้', 'error');
+ });
+ }
+
+ function saveSpecialQuizPanel() {
+ captureSpecialQuizForm(specialQuizCurrentKey);
+ setMsg('special-quiz-msg', 'กำลังบันทึก...', '');
+ gameQuizFetch('PUT', { specialQuizSets: specialQuizSetsState }).then(function (res) {
+ if (res && res.specialQuizSets && typeof res.specialQuizSets === 'object') {
+ specialQuizSetsState = res.specialQuizSets;
+ }
+ setMsg('special-quiz-msg', 'บันทึกแล้ว (15 ชุด)', 'ok');
+ renderSpecialQuizSet(specialQuizCurrentKey);
+ }).catch(function (e) {
+ setMsg('special-quiz-msg', e.message || 'บันทึกไม่ได้', 'error');
+ });
+ }
+
+ function quizTfThemeHiddenId(kind) {
+ if (kind === 'bg') return 'quiz-tf-theme-bg';
+ if (kind === 'border') return 'quiz-tf-theme-border';
+ return 'quiz-tf-theme-text';
+ }
+
+ function quizTfRowPrefix(kind) {
+ return 'quiz-tf-theme-' + kind;
+ }
+
+ function quizTfSetPickersFromColorString(kind, cssStr) {
+ var fb = QUIZ_CARRY_THEME_DEFAULTS[kind];
+ var o = quizCarryParseCssColor(cssStr, fb);
+ var pref = quizTfRowPrefix(kind);
+ var sw = el(pref + '-swatch');
+ var ra = el(pref + '-alpha');
+ var out = el(pref + '-alpha-out');
+ var hi = el(quizTfThemeHiddenId(kind));
+ var pr = el(pref + '-preview');
+ if (!sw || !ra || !hi) return;
+ sw.value = quizCarryHexFromRgb(o.r, o.g, o.b);
+ var pct = Math.max(0, Math.min(100, Math.round(Number(o.a) * 100)));
+ ra.value = String(pct);
+ if (out) out.textContent = pct + '%';
+ hi.value = quizCarryRgbToRgbaString(o);
+ if (pr) pr.textContent = hi.value;
+ }
+
+ function quizTfComposeFromPickers(kind) {
+ var pref = quizTfRowPrefix(kind);
+ var sw = el(pref + '-swatch');
+ var ra = el(pref + '-alpha');
+ if (!sw || !ra) return '';
+ var pr = quizCarryParseHexToRgb(sw.value);
+ if (!pr) return '';
+ var pct = parseInt(String(ra.value), 10);
+ if (!Number.isFinite(pct)) pct = 100;
+ pct = Math.max(0, Math.min(100, pct));
+ return quizCarryRgbToRgbaString({ r: pr.r, g: pr.g, b: pr.b, a: pct / 100 });
+ }
+
+ function quizTfSyncRowOutput(kind) {
+ var s = quizTfComposeFromPickers(kind);
+ var hi = el(quizTfThemeHiddenId(kind));
+ var pref = quizTfRowPrefix(kind);
+ var ra = el(pref + '-alpha');
+ var out = el(pref + '-alpha-out');
+ var pr = el(pref + '-preview');
+ if (hi) hi.value = s;
+ if (out && ra) out.textContent = String(Math.max(0, Math.min(100, parseInt(ra.value, 10) || 0))) + '%';
+ if (pr && hi) pr.textContent = hi.value;
+ }
+
+ function quizTfBindThemePickersOnce() {
+ if (quizTfBindThemePickersOnce._done) return;
+ ['bg', 'border', 'text'].forEach(function (kind) {
+ var pref = quizTfRowPrefix(kind);
+ var sw = el(pref + '-swatch');
+ var ra = el(pref + '-alpha');
+ if (!sw || !ra) return;
+ if (ra.getAttribute('data-tf-bound') === '1') return;
+ ra.setAttribute('data-tf-bound', '1');
+ function sync() {
+ quizTfSyncRowOutput(kind);
+ }
+ sw.addEventListener('input', sync);
+ sw.addEventListener('change', sync);
+ ra.addEventListener('input', sync);
+ ra.addEventListener('change', sync);
+ });
+ quizTfBindThemePickersOnce._done = true;
+ }
+
+ function quizTfBuildThemeForSave() {
+ quizTfBindThemePickersOnce();
+ ['bg', 'border', 'text'].forEach(function (k) {
+ quizTfSyncRowOutput(k);
+ });
+ var fbBg = QUIZ_CARRY_THEME_DEFAULTS.bg;
+ var fbBr = QUIZ_CARRY_THEME_DEFAULTS.border;
+ var fbTx = QUIZ_CARRY_THEME_DEFAULTS.text;
+ var sBg = quizTfComposeFromPickers('bg');
+ var sBr = quizTfComposeFromPickers('border');
+ var sTx = quizTfComposeFromPickers('text');
+ var bwEl = el('quiz-tf-theme-border-w');
+ var bw = bwEl ? parseInt(String(bwEl.value || '2'), 10) : 2;
+ if (!Number.isFinite(bw)) bw = 2;
+ bw = Math.max(0, Math.min(12, Math.round(bw)));
+ var qfMinEl = el('quiz-tf-theme-qfont-min');
+ var qfMaxEl = el('quiz-tf-theme-qfont-max');
+ var qMin = qfMinEl ? parseInt(String(qfMinEl.value || '10'), 10) : 10;
+ var qMax = qfMaxEl ? parseInt(String(qfMaxEl.value || '24'), 10) : 24;
+ if (!Number.isFinite(qMin)) qMin = 10;
+ if (!Number.isFinite(qMax)) qMax = 24;
+ qMin = Math.max(10, Math.min(40, Math.round(qMin)));
+ qMax = Math.max(14, Math.min(56, Math.round(qMax)));
+ if (qMax < qMin) {
+ var swap = qMin;
+ qMin = qMax;
+ qMax = swap;
+ }
+ return {
+ panelBg: (sBg && sBg.trim()) ? sBg.trim().slice(0, 120) : quizCarryRgbToRgbaString(fbBg),
+ panelBorder: (sBr && sBr.trim()) ? sBr.trim().slice(0, 120) : quizCarryRgbToRgbaString(fbBr),
+ textColor: (sTx && sTx.trim()) ? sTx.trim().slice(0, 120) : quizCarryRgbToRgbaString(fbTx),
+ borderWidthPx: bw,
+ questionFontMinPx: qMin,
+ questionFontMaxPx: qMax,
+ };
+ }
+
+ function quizTfFillFormFromTheme(th) {
+ quizTfBindThemePickersOnce();
+ var t = th && typeof th === 'object' ? th : {};
+ quizTfSetPickersFromColorString('bg', t.panelBg);
+ quizTfSetPickersFromColorString('border', t.panelBorder);
+ quizTfSetPickersFromColorString('text', t.textColor);
+ var bwEl = el('quiz-tf-theme-border-w');
+ var bw = parseInt(String(t.borderWidthPx), 10);
+ if (bwEl) bwEl.value = String(Number.isFinite(bw) && bw >= 0 ? Math.min(12, bw) : 2);
+ var qfMinEl = el('quiz-tf-theme-qfont-min');
+ var qfMaxEl = el('quiz-tf-theme-qfont-max');
+ var qMin = parseInt(String(t.questionFontMinPx), 10);
+ var qMax = parseInt(String(t.questionFontMaxPx), 10);
+ if (qfMinEl) qfMinEl.value = String(Number.isFinite(qMin) ? Math.max(10, Math.min(40, qMin)) : 10);
+ if (qfMaxEl) qfMaxEl.value = String(Number.isFinite(qMax) ? Math.max(14, Math.min(56, qMax)) : 24);
+ }
+
+ var QUIZ_CARRY_MAX_SLOTS = 16;
+
+ function rebuildQuizCarryCorrectSelect(row) {
+ var sel = row.querySelector('.quiz-carry-correct-sel');
+ if (!sel) return;
+ var inputs = row.querySelectorAll('.quiz-carry-slot-inp');
+ var prev = sel.value;
+ sel.innerHTML = '';
+ var has = false;
+ for (var i = 0; i < inputs.length; i++) {
+ var t = inputs[i].value ? String(inputs[i].value).trim() : '';
+ if (!t) continue;
+ has = true;
+ var o = document.createElement('option');
+ o.value = String(i);
+ var preview = t.length > 48 ? t.slice(0, 45) + '…' : t;
+ o.textContent = String(i + 1) + '. ' + preview;
+ sel.appendChild(o);
+ }
+ if (!has) {
+ var ox = document.createElement('option');
+ ox.value = '';
+ ox.textContent = '— กรอกตัวเลือกอย่างน้อย 2 ช่อง —';
+ sel.appendChild(ox);
+ sel.disabled = true;
+ return;
+ }
+ sel.disabled = false;
+ var ok = false;
+ for (var j = 0; j < sel.options.length; j++) {
+ if (sel.options[j].value === prev) {
+ sel.selectedIndex = j;
+ ok = true;
+ break;
+ }
+ }
+ if (!ok) sel.selectedIndex = 0;
+ }
+
+ function bindQuizCarryRowInputs(row) {
+ row.querySelectorAll('.quiz-carry-slot-inp').forEach(function (inp) {
+ inp.addEventListener('input', function () {
+ rebuildQuizCarryCorrectSelect(row);
+ });
+ });
+ }
+
+ function addQuizCarryAdminRow(q) {
+ var list = el('quiz-carry-admin-list');
+ if (!list) return;
+ q = q || {};
+ var choices = Array.isArray(q.choices) ? q.choices : [];
+ var choiceImages = Array.isArray(q.choiceImageUrls) ? q.choiceImageUrls : [];
+ var block = document.createElement('div');
+ block.className = 'quiz-carry-admin-block';
+
+ var head = document.createElement('div');
+ head.className = 'quiz-carry-admin-block-head';
+
+ var labQ = document.createElement('label');
+ labQ.className = 'quiz-carry-q-label';
+ labQ.appendChild(document.createTextNode('คำถาม'));
+ var inpQ = document.createElement('input');
+ inpQ.type = 'text';
+ inpQ.className = 'quiz-carry-q-text';
+ inpQ.placeholder = 'พิมพ์คำถาม…';
+ inpQ.maxLength = 500;
+ inpQ.value = q.text ? String(q.text) : '';
+ labQ.appendChild(inpQ);
+
+ var rm = document.createElement('button');
+ rm.type = 'button';
+ rm.className = 'btn btn-danger btn-sm quiz-carry-block-remove';
+ rm.setAttribute('aria-label', 'ลบข้อนี้');
+ rm.textContent = 'ลบข้อ';
+ rm.addEventListener('click', function () {
+ block.remove();
+ if (!list.querySelector('.quiz-carry-admin-block')) addQuizCarryAdminRow(null);
+ });
+
+ head.appendChild(labQ);
+ head.appendChild(rm);
+ block.appendChild(head);
+
+ var grid = document.createElement('div');
+ grid.className = 'quiz-carry-choice-grid';
+ for (var s = 0; s < QUIZ_CARRY_MAX_SLOTS; s++) {
+ var lab = document.createElement('label');
+ lab.className = 'quiz-carry-slot-label';
+ lab.appendChild(document.createTextNode('ตัวเลือก ' + (s + 1)));
+ var inp = document.createElement('input');
+ inp.type = 'text';
+ inp.className = 'quiz-carry-slot-inp';
+ inp.placeholder = s < 2 ? 'จำเป็น' : 'ไม่บังคับ';
+ inp.maxLength = 160;
+ inp.value = choices[s] != null ? String(choices[s]) : '';
+ lab.appendChild(inp);
+ var imgInp = document.createElement('input');
+ imgInp.type = 'url';
+ imgInp.className = 'quiz-carry-slot-img';
+ imgInp.placeholder = 'รูป (URL — ไม่บังคับ)';
+ imgInp.maxLength = 512;
+ imgInp.autocomplete = 'off';
+ imgInp.value = choiceImages[s] != null ? String(choiceImages[s]) : '';
+ imgInp.setAttribute('aria-label', 'URL รูปตัวเลือก ' + (s + 1));
+ lab.appendChild(imgInp);
+ grid.appendChild(lab);
+ }
+ block.appendChild(grid);
+
+ var cw = document.createElement('div');
+ cw.className = 'quiz-carry-correct-wrap';
+ var labC = document.createElement('label');
+ labC.className = 'quiz-carry-correct-label';
+ labC.appendChild(document.createTextNode('ข้อที่ถูกต้อง'));
+ var sel = document.createElement('select');
+ sel.className = 'quiz-carry-correct-sel';
+ sel.setAttribute('aria-label', 'เลือกข้อที่ถูก');
+ labC.appendChild(sel);
+ cw.appendChild(labC);
+ block.appendChild(cw);
+
+ list.appendChild(block);
+ bindQuizCarryRowInputs(block);
+ rebuildQuizCarryCorrectSelect(block);
+ var ci = Number(q.correctIndex);
+ if (Number.isFinite(ci) && ci >= 0) {
+ var targetSlot = ci;
+ var opts = sel.querySelectorAll('option');
+ for (var k = 0; k < opts.length; k++) {
+ if (opts[k].value === String(targetSlot)) {
+ sel.selectedIndex = k;
+ break;
+ }
+ }
+ }
+ }
+
+ function renderQuizCarryAdminList(arr) {
+ var list = el('quiz-carry-admin-list');
+ if (!list) return;
+ list.innerHTML = '';
+ var rows = arr && arr.length ? arr : [null];
+ rows.forEach(function (q) {
+ addQuizCarryAdminRow(q);
+ });
+ }
+
+ /** ค่าเริ่มต้น RGBA สำหรับแผงคำถามบนแผนที่ (quiz_carry) */
+ var QUIZ_CARRY_THEME_DEFAULTS = {
+ bg: { r: 12, g: 14, b: 28, a: 0.88 },
+ border: { r: 255, g: 214, b: 102, a: 0.7 },
+ text: { r: 241, g: 245, b: 249, a: 1 },
+ };
+
+ function quizCarryClampByte(n) {
+ return Math.max(0, Math.min(255, Math.round(Number(n)) || 0));
+ }
+
+ function quizCarryHexFromRgb(r, g, b) {
+ return '#' + [r, g, b].map(function (x) {
+ var h = quizCarryClampByte(x).toString(16);
+ return h.length === 1 ? '0' + h : h;
+ }).join('');
+ }
+
+ function quizCarryParseHexToRgb(hex) {
+ var h = String(hex || '').trim().replace(/^#/, '');
+ if (!h) return null;
+ if (h.length === 3) {
+ return {
+ r: parseInt(h[0] + h[0], 16),
+ g: parseInt(h[1] + h[1], 16),
+ b: parseInt(h[2] + h[2], 16),
+ alpha01: null,
+ };
+ }
+ if (h.length === 6) {
+ return {
+ r: parseInt(h.slice(0, 2), 16),
+ g: parseInt(h.slice(2, 4), 16),
+ b: parseInt(h.slice(4, 6), 16),
+ alpha01: null,
+ };
+ }
+ if (h.length === 8) {
+ return {
+ r: parseInt(h.slice(0, 2), 16),
+ g: parseInt(h.slice(2, 4), 16),
+ b: parseInt(h.slice(4, 6), 16),
+ alpha01: parseInt(h.slice(6, 8), 16) / 255,
+ };
+ }
+ return null;
+ }
+
+ function quizCarryParseCssColor(str, fallback) {
+ var s = String(str == null ? '' : str).trim();
+ if (!s) return fallback;
+ var m = /^rgba?\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*(?:,\s*([0-9.]+)\s*)?\)$/i.exec(s);
+ if (m) {
+ var a = m[4] != null && m[4] !== '' ? parseFloat(m[4]) : 1;
+ if (!Number.isFinite(a)) a = 1;
+ return {
+ r: quizCarryClampByte(parseInt(m[1], 10)),
+ g: quizCarryClampByte(parseInt(m[2], 10)),
+ b: quizCarryClampByte(parseInt(m[3], 10)),
+ a: Math.max(0, Math.min(1, a)),
+ };
+ }
+ if (/^#/.test(s)) {
+ var pr = quizCarryParseHexToRgb(s);
+ if (!pr) return fallback;
+ var a = pr.alpha01 != null ? pr.alpha01 : 1;
+ return { r: pr.r, g: pr.g, b: pr.b, a: Math.max(0, Math.min(1, a)) };
+ }
+ return fallback;
+ }
+
+ function quizCarryRgbToRgbaString(o) {
+ var a = Math.max(0, Math.min(1, Number(o.a)));
+ var t = Math.round(a * 1000) / 1000;
+ return 'rgba(' + quizCarryClampByte(o.r) + ', ' + quizCarryClampByte(o.g) + ', ' + quizCarryClampByte(o.b) + ', ' + t + ')';
+ }
+
+ function quizCarryRowPrefix(kind) {
+ return 'quiz-carry-theme-' + kind;
+ }
+
+ function quizCarryThemeHiddenId(kind) {
+ if (kind === 'bg') return 'quiz-carry-theme-bg';
+ if (kind === 'border') return 'quiz-carry-theme-border';
+ return 'quiz-carry-theme-text';
+ }
+
+ function quizCarrySetPickersFromColorString(kind, cssStr) {
+ var fb = QUIZ_CARRY_THEME_DEFAULTS[kind];
+ var o = quizCarryParseCssColor(cssStr, fb);
+ var pref = quizCarryRowPrefix(kind);
+ var sw = el(pref + '-swatch');
+ var ra = el(pref + '-alpha');
+ var out = el(pref + '-alpha-out');
+ var hi = el(quizCarryThemeHiddenId(kind));
+ var pr = el(pref + '-preview');
+ if (!sw || !ra || !hi) return;
+ sw.value = quizCarryHexFromRgb(o.r, o.g, o.b);
+ var pct = Math.max(0, Math.min(100, Math.round(Number(o.a) * 100)));
+ ra.value = String(pct);
+ if (out) out.textContent = pct + '%';
+ hi.value = quizCarryRgbToRgbaString(o);
+ if (pr) pr.textContent = hi.value;
+ }
+
+ function quizCarryComposeFromPickers(kind) {
+ var pref = quizCarryRowPrefix(kind);
+ var sw = el(pref + '-swatch');
+ var ra = el(pref + '-alpha');
+ if (!sw || !ra) return '';
+ var pr = quizCarryParseHexToRgb(sw.value);
+ if (!pr) return '';
+ var pct = parseInt(String(ra.value), 10);
+ if (!Number.isFinite(pct)) pct = 100;
+ pct = Math.max(0, Math.min(100, pct));
+ return quizCarryRgbToRgbaString({ r: pr.r, g: pr.g, b: pr.b, a: pct / 100 });
+ }
+
+ function quizCarrySyncRowOutput(kind) {
+ var s = quizCarryComposeFromPickers(kind);
+ var hi = el(quizCarryThemeHiddenId(kind));
+ var pref = quizCarryRowPrefix(kind);
+ var ra = el(pref + '-alpha');
+ var out = el(pref + '-alpha-out');
+ var pr = el(pref + '-preview');
+ if (hi) hi.value = s;
+ if (out && ra) out.textContent = String(Math.max(0, Math.min(100, parseInt(ra.value, 10) || 0))) + '%';
+ if (pr && hi) pr.textContent = hi.value;
+ }
+
+ /** อ่านค่าจาก color + range โดยตรง (ไม่พึ่ง hidden ที่อาจว่าง) ก่อน PUT */
+ function quizCarryBuildThemeForSave() {
+ quizCarryBindThemePickersOnce();
+ ['bg', 'border', 'text'].forEach(function (k) {
+ quizCarrySyncRowOutput(k);
+ });
+ var fbBg = QUIZ_CARRY_THEME_DEFAULTS.bg;
+ var fbBr = QUIZ_CARRY_THEME_DEFAULTS.border;
+ var fbTx = QUIZ_CARRY_THEME_DEFAULTS.text;
+ var sBg = quizCarryComposeFromPickers('bg');
+ var sBr = quizCarryComposeFromPickers('border');
+ var sTx = quizCarryComposeFromPickers('text');
+ var bwEl = el('quiz-carry-theme-border-w');
+ var bw = bwEl ? parseInt(String(bwEl.value || '2'), 10) : 2;
+ if (!Number.isFinite(bw)) bw = 2;
+ bw = Math.max(0, Math.min(12, Math.round(bw)));
+ var qfMinEl = el('quiz-carry-theme-qfont-min');
+ var qfMaxEl = el('quiz-carry-theme-qfont-max');
+ var qMin = qfMinEl ? parseInt(String(qfMinEl.value || '10'), 10) : 10;
+ var qMax = qfMaxEl ? parseInt(String(qfMaxEl.value || '24'), 10) : 24;
+ if (!Number.isFinite(qMin)) qMin = 10;
+ if (!Number.isFinite(qMax)) qMax = 24;
+ qMin = Math.max(10, Math.min(40, Math.round(qMin)));
+ qMax = Math.max(14, Math.min(56, Math.round(qMax)));
+ if (qMax < qMin) {
+ var swap = qMin;
+ qMin = qMax;
+ qMax = swap;
+ }
+ return {
+ panelBg: (sBg && sBg.trim()) ? sBg.trim().slice(0, 120) : quizCarryRgbToRgbaString(fbBg),
+ panelBorder: (sBr && sBr.trim()) ? sBr.trim().slice(0, 120) : quizCarryRgbToRgbaString(fbBr),
+ textColor: (sTx && sTx.trim()) ? sTx.trim().slice(0, 120) : quizCarryRgbToRgbaString(fbTx),
+ borderWidthPx: bw,
+ questionFontMinPx: qMin,
+ questionFontMaxPx: qMax,
+ };
+ }
+
+ function quizCarryBindThemePickersOnce() {
+ if (quizCarryBindThemePickersOnce._done) return;
+ ['bg', 'border', 'text'].forEach(function (kind) {
+ var pref = quizCarryRowPrefix(kind);
+ var sw = el(pref + '-swatch');
+ var ra = el(pref + '-alpha');
+ if (!sw || !ra) return;
+ if (ra.getAttribute('data-bound') === '1') return;
+ ra.setAttribute('data-bound', '1');
+ function sync() {
+ quizCarrySyncRowOutput(kind);
+ }
+ sw.addEventListener('input', sync);
+ sw.addEventListener('change', sync);
+ ra.addEventListener('input', sync);
+ ra.addEventListener('change', sync);
+ });
+ quizCarryBindThemePickersOnce._done = true;
+ }
+
+ /** ธีมนับ 3-2-1 embed — สีม่าน / กล่อง / ขนาด (carryEmbedCountdownTheme) */
+ var QUIZ_CARRY_ECD_COLS = [
+ { sw: 'quiz-carry-ecd-overlay-swatch', ra: 'quiz-carry-ecd-overlay-alpha', out: 'quiz-carry-ecd-overlay-alpha-out', hi: 'quiz-carry-ecd-overlay-val', pr: 'quiz-carry-ecd-overlay-preview' },
+ { sw: 'quiz-carry-ecd-inner-bg-swatch', ra: 'quiz-carry-ecd-inner-bg-alpha', out: 'quiz-carry-ecd-inner-bg-alpha-out', hi: 'quiz-carry-ecd-inner-bg-val', pr: 'quiz-carry-ecd-inner-bg-preview' },
+ { sw: 'quiz-carry-ecd-inner-border-swatch', ra: 'quiz-carry-ecd-inner-border-alpha', out: 'quiz-carry-ecd-inner-border-alpha-out', hi: 'quiz-carry-ecd-inner-border-val', pr: 'quiz-carry-ecd-inner-border-preview' },
+ ];
+ var QUIZ_CARRY_ECD_DEFAULTS_RGBA = {
+ overlay: { r: 8, g: 10, b: 20, a: 0.42 },
+ innerBg: { r: 12, g: 14, b: 28, a: 0.82 },
+ innerBorder: { r: 122, g: 162, b: 247, a: 0.45 },
+ };
+
+ function quizCarryEcdColKey(idx) {
+ if (idx === 0) return 'overlay';
+ if (idx === 1) return 'innerBg';
+ return 'innerBorder';
+ }
+
+ function quizCarryEcdSyncRow(idx) {
+ var row = QUIZ_CARRY_ECD_COLS[idx];
+ var sw = el(row.sw);
+ var ra = el(row.ra);
+ var hi = el(row.hi);
+ var pr = el(row.pr);
+ var out = el(row.out);
+ if (!sw || !ra || !hi) return;
+ var prRgb = quizCarryParseHexToRgb(sw.value);
+ if (!prRgb) return;
+ var pct = parseInt(String(ra.value), 10);
+ if (!Number.isFinite(pct)) pct = 100;
+ pct = Math.max(0, Math.min(100, pct));
+ var s = quizCarryRgbToRgbaString({ r: prRgb.r, g: prRgb.g, b: prRgb.b, a: pct / 100 });
+ hi.value = s;
+ if (out) out.textContent = pct + '%';
+ if (pr) pr.textContent = s;
+ }
+
+ function quizCarryEcdSetRowFromRgba(idx, cssStr) {
+ var row = QUIZ_CARRY_ECD_COLS[idx];
+ var key = quizCarryEcdColKey(idx);
+ var fb = QUIZ_CARRY_ECD_DEFAULTS_RGBA[key];
+ var o = quizCarryParseCssColor(cssStr, fb);
+ var sw = el(row.sw);
+ var ra = el(row.ra);
+ var out = el(row.out);
+ var hi = el(row.hi);
+ var pr = el(row.pr);
+ if (!sw || !ra || !hi) return;
+ sw.value = quizCarryHexFromRgb(o.r, o.g, o.b);
+ var pct = Math.max(0, Math.min(100, Math.round(Number(o.a) * 100)));
+ ra.value = String(pct);
+ if (out) out.textContent = pct + '%';
+ hi.value = quizCarryRgbToRgbaString(o);
+ if (pr) pr.textContent = hi.value;
+ }
+
+ function quizCarryEcdBindPickersOnce() {
+ if (quizCarryEcdBindPickersOnce._done) return;
+ for (var i = 0; i < QUIZ_CARRY_ECD_COLS.length; i++) {
+ (function (idx) {
+ var row = QUIZ_CARRY_ECD_COLS[idx];
+ var sw = el(row.sw);
+ var ra = el(row.ra);
+ if (!sw || !ra) return;
+ if (ra.getAttribute('data-ecd-bound') === '1') return;
+ ra.setAttribute('data-ecd-bound', '1');
+ function sync() {
+ quizCarryEcdSyncRow(idx);
+ }
+ sw.addEventListener('input', sync);
+ sw.addEventListener('change', sync);
+ ra.addEventListener('input', sync);
+ ra.addEventListener('change', sync);
+ })(i);
+ }
+ quizCarryEcdBindPickersOnce._done = true;
+ }
+
+ function quizCarryEcdBuildForSave() {
+ quizCarryEcdBindPickersOnce();
+ for (var i = 0; i < QUIZ_CARRY_ECD_COLS.length; i++) quizCarryEcdSyncRow(i);
+ var o0 = el('quiz-carry-ecd-overlay-val');
+ var o1 = el('quiz-carry-ecd-inner-bg-val');
+ var o2 = el('quiz-carry-ecd-inner-border-val');
+ var bwEl = el('quiz-carry-ecd-inner-border-w');
+ var radEl = el('quiz-carry-ecd-inner-radius');
+ var dig = el('quiz-carry-ecd-digit-swatch');
+ var m1 = el('quiz-carry-ecd-map-cqmin');
+ var m2 = el('quiz-carry-ecd-map-cqh');
+ var m3 = el('quiz-carry-ecd-map-max-px');
+ var sv = el('quiz-carry-ecd-screen-vw');
+ var sm = el('quiz-carry-ecd-screen-max-px');
+ var bw = bwEl ? parseInt(String(bwEl.value || '1'), 10) : 1;
+ if (!Number.isFinite(bw)) bw = 1;
+ bw = Math.max(0, Math.min(12, Math.round(bw)));
+ var rad = radEl ? parseInt(String(radEl.value || '12'), 10) : 12;
+ if (!Number.isFinite(rad)) rad = 12;
+ rad = Math.max(0, Math.min(32, Math.round(rad)));
+ var cqmin = m1 ? parseInt(String(m1.value || '78'), 10) : 78;
+ var cqh = m2 ? parseInt(String(m2.value || '82'), 10) : 82;
+ var mpx = m3 ? parseInt(String(m3.value || '200'), 10) : 200;
+ cqmin = Math.max(35, Math.min(100, cqmin));
+ cqh = Math.max(35, Math.min(100, cqh));
+ mpx = Math.max(48, Math.min(400, mpx));
+ var vw = sv ? parseInt(String(sv.value || '28'), 10) : 28;
+ var smx = sm ? parseInt(String(sm.value || '132'), 10) : 132;
+ vw = Math.max(6, Math.min(44, vw));
+ smx = Math.max(48, Math.min(220, smx));
+ var digitHex = dig && /^#[0-9a-fA-F]{6}$/.test(String(dig.value || '').trim()) ? String(dig.value).trim() : '#ffe066';
+ return {
+ overlayBackdrop: (o0 && o0.value.trim()) ? o0.value.trim().slice(0, 120) : quizCarryRgbToRgbaString(QUIZ_CARRY_ECD_DEFAULTS_RGBA.overlay),
+ innerBg: (o1 && o1.value.trim()) ? o1.value.trim().slice(0, 120) : quizCarryRgbToRgbaString(QUIZ_CARRY_ECD_DEFAULTS_RGBA.innerBg),
+ innerBorder: (o2 && o2.value.trim()) ? o2.value.trim().slice(0, 120) : quizCarryRgbToRgbaString(QUIZ_CARRY_ECD_DEFAULTS_RGBA.innerBorder),
+ innerBorderWpx: bw,
+ innerRadiusPx: rad,
+ digitColor: digitHex.slice(0, 32),
+ mapDigitCqmin: cqmin,
+ mapDigitCqh: cqh,
+ mapDigitMaxPx: mpx,
+ screenDigitVw: vw,
+ screenDigitMaxPx: smx,
+ };
+ }
+
+ function quizCarryEcdFillForm(th) {
+ quizCarryEcdBindPickersOnce();
+ var t = th && typeof th === 'object' ? th : {};
+ quizCarryEcdSetRowFromRgba(0, t.overlayBackdrop);
+ quizCarryEcdSetRowFromRgba(1, t.innerBg);
+ quizCarryEcdSetRowFromRgba(2, t.innerBorder);
+ var bwEl = el('quiz-carry-ecd-inner-border-w');
+ var radEl = el('quiz-carry-ecd-inner-radius');
+ var dig = el('quiz-carry-ecd-digit-swatch');
+ var m1 = el('quiz-carry-ecd-map-cqmin');
+ var m2 = el('quiz-carry-ecd-map-cqh');
+ var m3 = el('quiz-carry-ecd-map-max-px');
+ var sv = el('quiz-carry-ecd-screen-vw');
+ var sm = el('quiz-carry-ecd-screen-max-px');
+ var bw = parseInt(String(t.innerBorderWpx), 10);
+ if (bwEl) bwEl.value = String(Number.isFinite(bw) && bw >= 0 ? Math.min(12, bw) : 1);
+ var rad = parseInt(String(t.innerRadiusPx), 10);
+ if (radEl) radEl.value = String(Number.isFinite(rad) && rad >= 0 ? Math.min(32, rad) : 12);
+ if (dig && typeof t.digitColor === 'string' && /^#[0-9a-fA-F]{6}$/.test(t.digitColor.trim())) dig.value = t.digitColor.trim();
+ var cqmin = parseInt(String(t.mapDigitCqmin), 10);
+ var cqh = parseInt(String(t.mapDigitCqh), 10);
+ var mpx = parseInt(String(t.mapDigitMaxPx), 10);
+ if (m1) m1.value = String(Number.isFinite(cqmin) ? Math.max(35, Math.min(100, cqmin)) : 78);
+ if (m2) m2.value = String(Number.isFinite(cqh) ? Math.max(35, Math.min(100, cqh)) : 82);
+ if (m3) m3.value = String(Number.isFinite(mpx) ? Math.max(48, Math.min(400, mpx)) : 200);
+ var vw = parseInt(String(t.screenDigitVw), 10);
+ var smx = parseInt(String(t.screenDigitMaxPx), 10);
+ if (sv) sv.value = String(Number.isFinite(vw) ? Math.max(6, Math.min(44, vw)) : 28);
+ if (sm) sm.value = String(Number.isFinite(smx) ? Math.max(48, Math.min(220, smx)) : 132);
+ }
+
+ var QUIZ_CARRY_PLAQUE_SLOT_COUNT = 16;
+ var QUIZ_CARRY_PLAQUE_KINDS = ['fixed', 'fill', 'text'];
+ var QUIZ_CARRY_PLAQUE_DEFAULTS_RGBA = {
+ fixed: { r: 122, g: 200, b: 255, a: 0.9 },
+ fill: { r: 12, g: 10, b: 20, a: 0.88 },
+ text: { r: 248, g: 249, b: 255, a: 1 },
+ };
+
+ function quizCarryPlaqueSlotPrefix(si) {
+ return 'quiz-carry-plaque-s' + si + '-';
+ }
+
+ /**
+ * อัปโหลดป้าย: ลอง POST ตรง Node (/Game/api/ — nginx proxy 20M) ก่อน แล้วค่อย PHP ถ้าล้ม
+ * เดิมลอง PHP ก่อนทำให้พังง่ายเมื่อ php-fpm ต่อ 127.0.0.1:3001 ไม่ได้หรือ session ผิด แม้เกมจะรันอยู่
+ */
+ function quizCarryPlaqueUploadWithFallback(dataUrl) {
+ var body = JSON.stringify({ imageDataUrl: dataUrl });
+ var opts = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: body };
+ return gauntletAssetsJsonFetch(GAME_QUIZ_CARRY_PLAQUE_UPLOAD_NODE, opts).catch(function (errFirst) {
+ var msg = String((errFirst && errFirst.message) || '');
+ if (/\b401\b|Unauthorized|หมดเซสชัน/i.test(msg)) {
+ return Promise.reject(errFirst);
+ }
+ var tryPhp =
+ /\b404\b|\b405\b|Not Found|ไม่พบ|502|503|504|Bad Gateway|Failed to fetch|NetworkError|Load failed|ECONNREFUSED/i.test(
+ msg,
+ ) ||
+ /\b413\b|Request Entity Too Large|Payload too large|รับ body ไม่ได้|too large/i.test(msg);
+ if (!tryPhp) {
+ return Promise.reject(errFirst);
+ }
+ return gauntletAssetsJsonFetch(GAME_QUIZ_CARRY_PLAQUE_UPLOAD_PHP, opts).then(function (res) {
+ try {
+ setMsg(
+ 'quiz-carry-settings-msg',
+ 'อัปโหลดสำเร็จผ่าน PHP (รองจาก /Game/api/) · Upload OK via PHP fallback',
+ 'ok',
+ );
+ } catch (e1) { /* ignore */ }
+ return res;
+ });
+ });
+ }
+
+ /** URL สำหรับ — path ที่ขึ้นต้นด้วย / ให้ต่อ origin (รองรับ Admin ใต้โดเมนเดียวกับ /Game/) */
+ function quizCarryPlaqueAbsMediaUrl(path) {
+ var s = String(path || '').trim();
+ if (!s) return '';
+ if (/^https?:\/\//i.test(s)) return s;
+ if (s.charAt(0) === '/') return String(window.location.origin || '') + s;
+ return '';
+ }
+
+ function quizCarryPlaqueUpdateImagePreview(si) {
+ var pfx = quizCarryPlaqueSlotPrefix(si);
+ var inp = el(pfx + 'plaque-image-url');
+ var img = el(pfx + 'plaque-preview-img');
+ var errEl = el(pfx + 'plaque-preview-err');
+ if (!img) return;
+ var raw = inp && inp.value ? String(inp.value).trim() : '';
+ if (errEl) errEl.textContent = '';
+ if (!raw) {
+ img.style.display = 'none';
+ img.removeAttribute('src');
+ return;
+ }
+ var abs = quizCarryPlaqueAbsMediaUrl(raw);
+ if (!abs) {
+ img.style.display = 'none';
+ if (errEl) errEl.textContent = 'รูปแบบ URL ไม่รองรับ';
+ return;
+ }
+ img.onerror = function () {
+ img.style.display = 'none';
+ if (errEl) errEl.textContent = 'โหลดตัวอย่างไม่ได้ — ตรวจ path / สิทธิ์ไฟล์ / รีสตาร์ท Node';
+ };
+ img.onload = function () {
+ try {
+ img.removeAttribute('width');
+ img.removeAttribute('height');
+ } catch (e) { /* ignore */ }
+ img.style.display = 'block';
+ if (errEl) errEl.textContent = '';
+ };
+ var sep = abs.indexOf('?') >= 0 ? '&' : '?';
+ img.src = abs + sep + '_pv=' + Date.now();
+ }
+
+ function quizCarryPlaqueUpdateNeonUi(si) {
+ var modeEl = el(quizCarryPlaqueSlotPrefix(si) + 'border-mode');
+ var grid = modeEl && modeEl.closest ? modeEl.closest('.quiz-carry-theme-grid') : null;
+ if (!grid) return;
+ if (modeEl && modeEl.value === 'fixed') grid.classList.remove('quiz-carry-plaque-grid--neon');
+ else grid.classList.add('quiz-carry-plaque-grid--neon');
+ var hint = grid.querySelector('.quiz-carry-plaque-neon-hint');
+ if (hint) hint.style.display = modeEl && modeEl.value === 'fixed' ? 'none' : '';
+ }
+
+ function quizCarryPlaqueRowIds(si, kind) {
+ var seg = kind === 'fixed' ? 'fixed' : kind === 'fill' ? 'fill' : 'text';
+ var p = quizCarryPlaqueSlotPrefix(si);
+ return {
+ sw: p + seg + '-swatch',
+ ra: p + seg + '-alpha',
+ out: p + seg + '-alpha-out',
+ hi: p + seg + '-val',
+ };
+ }
+
+ function quizCarryPlaqueEnsureMounted() {
+ var root = el('quiz-carry-plaque-slots-root');
+ if (!root || root.getAttribute('data-mounted') === '1') return;
+ for (var si = 0; si < QUIZ_CARRY_PLAQUE_SLOT_COUNT; si++) {
+ var det = document.createElement('details');
+ det.className = 'quiz-carry-plaque-slot-details';
+ if (si === 0) det.open = true;
+ var summ = document.createElement('summary');
+ summ.textContent = 'ตัวเลือก ' + (si + 1);
+ det.appendChild(summ);
+ var grid = document.createElement('div');
+ grid.className = 'quiz-carry-theme-grid quiz-carry-plaque-grid';
+ var rowMode = document.createElement('div');
+ rowMode.className = 'quiz-carry-theme-row quiz-carry-theme-row--narrow';
+ rowMode.style.marginBottom = '0.45rem';
+ var labMode = document.createElement('label');
+ labMode.className = 'admin-field quiz-carry-theme-label';
+ labMode.appendChild(document.createTextNode('โหมดขอบป้าย'));
+ var selMode = document.createElement('select');
+ selMode.id = quizCarryPlaqueSlotPrefix(si) + 'border-mode';
+ selMode.setAttribute('aria-label', 'โหมดขอบป้ายช่อง ' + (si + 1));
+ [['neon', 'เรืองตามช่อง (neon)'], ['fixed', 'สีขอบคงที่']].forEach(function (opt) {
+ var o = document.createElement('option');
+ o.value = opt[0];
+ o.textContent = opt[1];
+ selMode.appendChild(o);
+ });
+ labMode.appendChild(selMode);
+ rowMode.appendChild(labMode);
+ grid.appendChild(rowMode);
+ selMode.addEventListener('change', function () {
+ quizCarryPlaqueUpdateNeonUi(si);
+ });
+
+ var hintNeon = document.createElement('p');
+ hintNeon.className = 'muted quiz-carry-plaque-neon-hint';
+ hintNeon.style.cssText = 'margin:0 0 0.5rem;font-size:0.78rem;line-height:1.45';
+ hintNeon.innerHTML =
+ 'Neon / เรืองตามช่อง: ขอบบนแมปใช้สีตามเลขช่อง ไม่ใช้ «สีขอบ (fixed)» ด้านล่าง — ต้องการขอบตาม picker ให้เลือก สีขอบคงที่ · ' +
+ 'English: In Neon mode the map uses per-slot hue; Fixed border uses the fixed-border pickers.';
+ grid.appendChild(hintNeon);
+
+ function addColorRow(labelText, kind, defHex, defAlpha) {
+ var ids = quizCarryPlaqueRowIds(si, kind);
+ var row = document.createElement('div');
+ row.className = 'quiz-carry-theme-row';
+ row.setAttribute('data-quiz-carry-plaque-color-row', kind);
+ var lbl = document.createElement('span');
+ lbl.className = 'quiz-carry-theme-label';
+ lbl.textContent = labelText;
+ row.appendChild(lbl);
+ var controls = document.createElement('div');
+ controls.className = 'quiz-carry-color-controls';
+ var sw = document.createElement('input');
+ sw.type = 'color';
+ sw.id = ids.sw;
+ sw.value = defHex;
+ sw.title = labelText;
+ controls.appendChild(sw);
+ var labA = document.createElement('label');
+ labA.className = 'quiz-carry-alpha-label';
+ var spanA = document.createElement('span');
+ spanA.textContent = 'โปร่ง A';
+ labA.appendChild(spanA);
+ var ra = document.createElement('input');
+ ra.type = 'range';
+ ra.id = ids.ra;
+ ra.min = '0';
+ ra.max = '100';
+ ra.step = '1';
+ ra.value = String(defAlpha);
+ labA.appendChild(ra);
+ var out = document.createElement('output');
+ out.id = ids.out;
+ out.className = 'quiz-carry-alpha-out';
+ out.setAttribute('for', ids.ra);
+ out.textContent = defAlpha + '%';
+ labA.appendChild(out);
+ controls.appendChild(labA);
+ var hi = document.createElement('input');
+ hi.type = 'hidden';
+ hi.id = ids.hi;
+ controls.appendChild(hi);
+ row.appendChild(controls);
+ grid.appendChild(row);
+ }
+ addColorRow('สีขอบ (fixed)', 'fixed', '#7ac8ff', 90);
+ addColorRow('พื้นหลังกล่อง', 'fill', '#0c0a14', 88);
+ addColorRow('สีตัวอักษร', 'text', '#f8f9ff', 100);
+ var rowW = document.createElement('div');
+ rowW.className = 'quiz-carry-theme-row quiz-carry-theme-row--narrow';
+ var labW = document.createElement('label');
+ labW.className = 'admin-field quiz-carry-theme-label';
+ labW.appendChild(document.createTextNode('ความหนาขอบ (px)'));
+ var inpW = document.createElement('input');
+ inpW.type = 'number';
+ inpW.className = 'admin-inp-num';
+ inpW.id = quizCarryPlaqueSlotPrefix(si) + 'border-w';
+ inpW.min = '0';
+ inpW.max = '8';
+ inpW.step = '0.5';
+ inpW.value = '2.5';
+ labW.appendChild(inpW);
+ rowW.appendChild(labW);
+ grid.appendChild(rowW);
+
+ var rowImg = document.createElement('div');
+ /* ห้ามใช้ quiz-carry-theme-row--narrow — มี max-width:12rem ทำช่อง URL แคบมาก ดูเหมือนอัปโหลดไม่ทำงาน */
+ rowImg.className = 'quiz-carry-theme-row quiz-carry-plaque-image-row';
+ rowImg.style.cssText = 'flex-wrap:wrap;gap:0.65rem;align-items:flex-end';
+ var labUrl = document.createElement('label');
+ labUrl.className = 'admin-field';
+ labUrl.style.cssText = 'flex:1;min-width:200px;margin:0';
+ labUrl.appendChild(document.createTextNode('รูปบนป้าย (ช่องนี้)'));
+ var inpImgUrl = document.createElement('input');
+ inpImgUrl.type = 'text';
+ inpImgUrl.className = 'admin-inp-text';
+ inpImgUrl.id = quizCarryPlaqueSlotPrefix(si) + 'plaque-image-url';
+ inpImgUrl.placeholder = '/Game/img/quiz-carry-plaque-assets/... หรือ https://...';
+ inpImgUrl.setAttribute('aria-label', 'URL รูปป้ายช่อง ' + (si + 1));
+ labUrl.appendChild(document.createElement('br'));
+ labUrl.appendChild(inpImgUrl);
+ var prevErr = document.createElement('span');
+ prevErr.id = quizCarryPlaqueSlotPrefix(si) + 'plaque-preview-err';
+ prevErr.className = 'muted quiz-carry-plaque-preview-err';
+ prevErr.style.cssText = 'display:block;font-size:0.72rem;margin:0.25rem 0 0.15rem;min-height:1em';
+ labUrl.appendChild(prevErr);
+ var prevImg = document.createElement('img');
+ prevImg.id = quizCarryPlaqueSlotPrefix(si) + 'plaque-preview-img';
+ prevImg.alt = 'พรีวิวรูปป้ายช่อง ' + (si + 1);
+ prevImg.className = 'quiz-carry-plaque-preview-img';
+ /* ห้าม width/height = 0 — บางเบราว์เซอร์แสดงกล่อง 0×0 แม้ onload แล้ว · Don't use 0×0 attrs or preview stays invisible */
+ prevImg.style.cssText = 'display:none;max-height:120px;max-width:min(100%,320px);width:auto;height:auto;border-radius:8px;border:1px solid var(--border);object-fit:contain;background:var(--card)';
+ labUrl.appendChild(prevImg);
+ inpImgUrl.addEventListener('input', function () {
+ var v = String(inpImgUrl.value || '').trim();
+ if (!v) delete quizCarryPlaquePendingUrlBySlot[si];
+ quizCarryPlaqueUpdateImagePreview(si);
+ });
+ inpImgUrl.addEventListener('change', function () {
+ var v2 = String(inpImgUrl.value || '').trim();
+ if (!v2) delete quizCarryPlaquePendingUrlBySlot[si];
+ quizCarryPlaqueUpdateImagePreview(si);
+ });
+ rowImg.appendChild(labUrl);
+ var upWrap = document.createElement('div');
+ upWrap.className = 'admin-field';
+ upWrap.style.margin = '0';
+ var fileInp = document.createElement('input');
+ fileInp.type = 'file';
+ fileInp.accept = 'image/png,image/jpeg,image/webp,image/gif';
+ fileInp.id = quizCarryPlaqueSlotPrefix(si) + 'plaque-image-file';
+ fileInp.style.fontSize = '0.82rem';
+ var upMsg = document.createElement('span');
+ upMsg.className = 'muted';
+ upMsg.id = quizCarryPlaqueSlotPrefix(si) + 'plaque-image-msg';
+ upMsg.style.cssText = 'display:block;font-size:0.75rem;margin-top:0.25rem';
+ fileInp.addEventListener('change', function () {
+ while (upMsg.firstChild) upMsg.removeChild(upMsg.firstChild);
+ upMsg.textContent = '';
+ if (!fileInp.files || !fileInp.files[0]) return;
+ upMsg.textContent = 'กำลังอ่านไฟล์และอัปโหลด… · Reading file & uploading…';
+ try {
+ upMsg.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ } catch (scrollErr) { /* ignore */ }
+ quizCarryPlaqueUploadPending++;
+ quizCarryPlaqueUploadStartMs = Date.now();
+ readFileAsDataURL(fileInp.files[0])
+ .then(function (dataUrl) {
+ return quizCarryPlaqueUploadWithFallback(dataUrl);
+ })
+ .then(function (res) {
+ if (res && res.url) {
+ var uOk = String(res.url).trim();
+ inpImgUrl.value = uOk;
+ quizCarryPlaquePendingUrlBySlot[si] = uOk;
+ quizCarryPlaqueUpdateImagePreview(si);
+ upMsg.textContent = '';
+ var ok1 = document.createElement('span');
+ ok1.textContent = 'อัปโหลดแล้ว · path ที่ได้: ';
+ upMsg.appendChild(ok1);
+ var codeU = document.createElement('code');
+ codeU.style.cssText = 'font-size:0.78rem;word-break:break-all';
+ codeU.textContent = res.url;
+ upMsg.appendChild(codeU);
+ var br = document.createElement('br');
+ upMsg.appendChild(br);
+ var ok2 = document.createElement('span');
+ ok2.textContent = 'กด «บันทึก» ด้านล่างเพื่อเขียนลง quiz-settings.json · Then click Save below';
+ upMsg.appendChild(ok2);
+ } else {
+ upMsg.textContent = (res && res.error) || 'ตอบกลับไม่มี url — ตรวจ Node /Game/api/quiz-carry-plaque-upload';
+ }
+ })
+ .catch(function (e) {
+ upMsg.textContent = e.message || 'อัปโหลดไม่สำเร็จ';
+ setMsg('quiz-carry-settings-msg', e.message || 'อัปโหลดรูปป้ายไม่สำเร็จ · Plaque upload failed', 'error');
+ try {
+ upMsg.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ var smUp = el('quiz-carry-settings-msg');
+ if (smUp) smUp.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ } catch (scrollE) { /* ignore */ }
+ })
+ .finally(function () {
+ fileInp.value = '';
+ quizCarryPlaqueUploadPending = Math.max(0, quizCarryPlaqueUploadPending - 1);
+ if (quizCarryPlaqueUploadPending === 0) quizCarryPlaqueUploadStartMs = 0;
+ });
+ });
+ upWrap.appendChild(document.createTextNode('อัปโหลด'));
+ upWrap.appendChild(document.createElement('br'));
+ upWrap.appendChild(fileInp);
+ upWrap.appendChild(upMsg);
+ rowImg.appendChild(upWrap);
+ grid.appendChild(rowImg);
+
+ det.appendChild(grid);
+ root.appendChild(det);
+ quizCarryPlaqueUpdateNeonUi(si);
+ }
+ root.setAttribute('data-mounted', '1');
+ }
+
+ function quizCarryPlaqueComposeFromPickers(si, kind) {
+ var ids = quizCarryPlaqueRowIds(si, kind);
+ var sw = el(ids.sw);
+ var ra = el(ids.ra);
+ var fb = QUIZ_CARRY_PLAQUE_DEFAULTS_RGBA[kind] || QUIZ_CARRY_PLAQUE_DEFAULTS_RGBA.fill;
+ if (!sw || !ra) return quizCarryRgbToRgbaString(fb);
+ var pr = quizCarryParseHexToRgb(sw.value);
+ if (!pr) return quizCarryRgbToRgbaString(fb);
+ var pct = parseInt(String(ra.value), 10);
+ if (!Number.isFinite(pct)) pct = Math.round((fb.a != null ? fb.a : 1) * 100);
+ pct = Math.max(0, Math.min(100, pct));
+ return quizCarryRgbToRgbaString({ r: pr.r, g: pr.g, b: pr.b, a: pct / 100 });
+ }
+
+ function quizCarryPlaqueSetPickersFromString(si, kind, cssStr) {
+ var ids = quizCarryPlaqueRowIds(si, kind);
+ var fb = QUIZ_CARRY_PLAQUE_DEFAULTS_RGBA[kind] || QUIZ_CARRY_PLAQUE_DEFAULTS_RGBA.fill;
+ var o = quizCarryParseCssColor(cssStr, fb);
+ var sw = el(ids.sw);
+ var ra = el(ids.ra);
+ var hi = el(ids.hi);
+ var out = el(ids.out);
+ if (sw) sw.value = quizCarryHexFromRgb(o.r, o.g, o.b);
+ if (ra) ra.value = String(Math.round((o.a != null ? o.a : 1) * 100));
+ if (out) out.textContent = ra ? ra.value + '%' : '';
+ if (hi) hi.value = quizCarryRgbToRgbaString(o);
+ }
+
+ function quizCarryPlaqueSyncRow(si, kind) {
+ var s = quizCarryPlaqueComposeFromPickers(si, kind);
+ var ids = quizCarryPlaqueRowIds(si, kind);
+ var hi = el(ids.hi);
+ var out = el(ids.out);
+ var ra = el(ids.ra);
+ if (hi) hi.value = s;
+ if (out && ra) out.textContent = ra.value + '%';
+ }
+
+ var quizCarryPlaqueBindPickersDone = false;
+ function quizCarryPlaqueBindPickersOnce() {
+ if (quizCarryPlaqueBindPickersDone) return;
+ quizCarryPlaqueEnsureMounted();
+ for (var si = 0; si < QUIZ_CARRY_PLAQUE_SLOT_COUNT; si++) {
+ QUIZ_CARRY_PLAQUE_KINDS.forEach(function (kind) {
+ var ids = quizCarryPlaqueRowIds(si, kind);
+ var sw = el(ids.sw);
+ var ra = el(ids.ra);
+ if (sw) {
+ sw.addEventListener('input', function () {
+ quizCarryPlaqueSyncRow(si, kind);
+ });
+ }
+ if (ra) {
+ ra.addEventListener('input', function () {
+ quizCarryPlaqueSyncRow(si, kind);
+ });
+ }
+ });
+ }
+ quizCarryPlaqueBindPickersDone = true;
+ }
+
+ function quizCarryPlaqueBuildForSave() {
+ quizCarryPlaqueEnsureMounted();
+ quizCarryPlaqueBindPickersOnce();
+ var arr = [];
+ for (var si = 0; si < QUIZ_CARRY_PLAQUE_SLOT_COUNT; si++) {
+ for (var ki = 0; ki < QUIZ_CARRY_PLAQUE_KINDS.length; ki++) {
+ quizCarryPlaqueSyncRow(si, QUIZ_CARRY_PLAQUE_KINDS[ki]);
+ }
+ var modeEl = el(quizCarryPlaqueSlotPrefix(si) + 'border-mode');
+ var mode = modeEl && modeEl.value === 'fixed' ? 'fixed' : 'neon';
+ var bwEl = el(quizCarryPlaqueSlotPrefix(si) + 'border-w');
+ var bw = bwEl ? parseFloat(String(bwEl.value)) : 2.5;
+ if (!Number.isFinite(bw)) bw = 2.5;
+ bw = Math.round(Math.max(0, Math.min(8, bw)) * 10) / 10;
+ var imgUrlEl = el(quizCarryPlaqueSlotPrefix(si) + 'plaque-image-url');
+ var plaqueImageUrl = imgUrlEl ? String(imgUrlEl.value || '').trim() : '';
+ if (!plaqueImageUrl && quizCarryPlaquePendingUrlBySlot[si]) {
+ plaqueImageUrl = String(quizCarryPlaquePendingUrlBySlot[si] || '').trim();
+ if (imgUrlEl && plaqueImageUrl) imgUrlEl.value = plaqueImageUrl;
+ }
+ plaqueImageUrl = plaqueImageUrl.slice(0, 512);
+ arr.push({
+ borderMode: mode,
+ fixedBorder: quizCarryPlaqueComposeFromPickers(si, 'fixed'),
+ fillBg: quizCarryPlaqueComposeFromPickers(si, 'fill'),
+ textColor: quizCarryPlaqueComposeFromPickers(si, 'text'),
+ borderWidthPx: bw,
+ plaqueImageUrl: plaqueImageUrl,
+ });
+ }
+ return arr;
+ }
+
+ function quizCarryPlaqueFillSlot(si, t) {
+ var obj = t && typeof t === 'object' ? t : {};
+ var modeEl = el(quizCarryPlaqueSlotPrefix(si) + 'border-mode');
+ if (modeEl) modeEl.value = String(obj.borderMode || '').toLowerCase() === 'fixed' ? 'fixed' : 'neon';
+ quizCarryPlaqueSetPickersFromString(si, 'fixed', obj.fixedBorder);
+ quizCarryPlaqueSetPickersFromString(si, 'fill', obj.fillBg);
+ quizCarryPlaqueSetPickersFromString(si, 'text', obj.textColor);
+ var bwEl = el(quizCarryPlaqueSlotPrefix(si) + 'border-w');
+ var bw = parseFloat(String(obj.borderWidthPx));
+ if (bwEl) {
+ bwEl.value = String(Number.isFinite(bw) ? Math.round(Math.max(0, Math.min(8, bw)) * 10) / 10 : 2.5);
+ }
+ var imgEl = el(quizCarryPlaqueSlotPrefix(si) + 'plaque-image-url');
+ if (imgEl) {
+ imgEl.value = obj.plaqueImageUrl != null ? String(obj.plaqueImageUrl) : '';
+ }
+ var srvU = obj.plaqueImageUrl != null ? String(obj.plaqueImageUrl).trim() : '';
+ if (srvU) quizCarryPlaquePendingUrlBySlot[si] = srvU.slice(0, 512);
+ var msgEl = el(quizCarryPlaqueSlotPrefix(si) + 'plaque-image-msg');
+ if (msgEl) msgEl.textContent = '';
+ quizCarryPlaqueUpdateNeonUi(si);
+ quizCarryPlaqueUpdateImagePreview(si);
+ }
+
+ function quizCarryPlaqueFillAllFromApi(data) {
+ quizCarryPlaqueBindPickersOnce();
+ var arr = null;
+ if (data && Array.isArray(data.carryChoicePlaqueThemes) && data.carryChoicePlaqueThemes.length) {
+ arr = data.carryChoicePlaqueThemes;
+ } else if (data && data.carryChoicePlaqueTheme && typeof data.carryChoicePlaqueTheme === 'object') {
+ arr = [];
+ for (var u = 0; u < QUIZ_CARRY_PLAQUE_SLOT_COUNT; u++) arr.push(data.carryChoicePlaqueTheme);
+ }
+ for (var si = 0; si < QUIZ_CARRY_PLAQUE_SLOT_COUNT; si++) {
+ var th = (arr && arr[si]) ? arr[si] : {};
+ quizCarryPlaqueFillSlot(si, th);
+ quizCarryPlaqueUpdateImagePreview(si);
+ }
+ }
+
+ function quizCarrySetPlaqueMapScaleInputs(scale) {
+ var pms = Number(scale);
+ if (!Number.isFinite(pms)) pms = 1.25;
+ pms = Math.max(0.85, Math.min(2.5, pms));
+ var cent = Math.round(pms * 100);
+ cent = Math.max(85, Math.min(250, cent));
+ var num = el('quiz-carry-plaque-map-scale');
+ var rng = el('quiz-carry-plaque-map-scale-range');
+ var out = el('quiz-carry-plaque-map-scale-out');
+ if (num) num.value = String(Math.round((cent / 100) * 100) / 100);
+ if (rng) rng.value = String(cent);
+ if (out) out.textContent = '× ' + (num ? num.value : String(cent / 100));
+ }
+
+ function wireQuizCarryPlaqueMapScaleControls() {
+ if (quizCarryPlaqueScaleControlsWired) return;
+ var num = el('quiz-carry-plaque-map-scale');
+ var rng = el('quiz-carry-plaque-map-scale-range');
+ if (!num || !rng) return;
+ quizCarryPlaqueScaleControlsWired = true;
+ function fromRange() {
+ var c = parseInt(String(rng.value), 10);
+ if (!Number.isFinite(c)) c = 125;
+ c = Math.max(85, Math.min(250, c));
+ rng.value = String(c);
+ var sc = c / 100;
+ num.value = String(Math.round(sc * 100) / 100);
+ var out = el('quiz-carry-plaque-map-scale-out');
+ if (out) out.textContent = '× ' + num.value;
+ }
+ function fromNum() {
+ var p = parseFloat(String(num.value || '').replace(',', '.'));
+ if (!Number.isFinite(p)) p = 1.25;
+ p = Math.max(0.85, Math.min(2.5, p));
+ rng.value = String(Math.round(p * 100));
+ num.value = String(Math.round(p * 100) / 100);
+ var out = el('quiz-carry-plaque-map-scale-out');
+ if (out) out.textContent = '× ' + num.value;
+ }
+ rng.addEventListener('input', fromRange);
+ rng.addEventListener('change', fromRange);
+ num.addEventListener('input', fromNum);
+ num.addEventListener('change', fromNum);
+ }
+
+ function loadQuizCarryPanel(opts) {
+ opts = opts || {};
+ var delay = Number(opts.fetchDelayMs) || 0;
+ var runLoad = function () {
+ gameQuizFetch('GET').then(function (data) {
+ if (opts.themeOverride && typeof opts.themeOverride === 'object') {
+ var ov = opts.themeOverride;
+ if (
+ ov.panelBg != null ||
+ ov.borderWidthPx != null ||
+ ov.panelBorder != null ||
+ ov.textColor != null ||
+ ov.questionFontMinPx != null ||
+ ov.questionFontMaxPx != null
+ ) {
+ data.carryMapPanelTheme = ov;
+ } else if (ov.carryMapPanelTheme && typeof ov.carryMapPanelTheme === 'object') {
+ data.carryMapPanelTheme = ov.carryMapPanelTheme;
+ }
+ if (ov.carryEmbedCountdownTheme && typeof ov.carryEmbedCountdownTheme === 'object') {
+ data.carryEmbedCountdownTheme = ov.carryEmbedCountdownTheme;
+ }
+ if (ov.carryChoicePlaqueThemes && Array.isArray(ov.carryChoicePlaqueThemes)) {
+ data.carryChoicePlaqueThemes = ov.carryChoicePlaqueThemes;
+ if (ov.carryChoicePlaqueThemes[0] && typeof ov.carryChoicePlaqueThemes[0] === 'object') {
+ data.carryChoicePlaqueTheme = ov.carryChoicePlaqueThemes[0];
+ }
+ } else if (ov.carryChoicePlaqueTheme && typeof ov.carryChoicePlaqueTheme === 'object') {
+ /* legacy อ็อบเจ็กต์เดียว — ต้องตั้งทั้งอาร์เรย์ ไม่งั้น Fill ยังใช้ carryChoicePlaqueThemes จาก GET เก่า (URL ป้ายหายหลังบันทึก) */
+ data.carryChoicePlaqueTheme = ov.carryChoicePlaqueTheme;
+ var expPlaque = [];
+ for (var exi = 0; exi < QUIZ_CARRY_PLAQUE_SLOT_COUNT; exi++) {
+ expPlaque.push(ov.carryChoicePlaqueTheme);
+ }
+ data.carryChoicePlaqueThemes = expPlaque;
+ }
+ }
+ if (opts.timingOverride && typeof opts.timingOverride === 'object') {
+ var t = opts.timingOverride;
+ if (t.carryReadMs != null) data.carryReadMs = t.carryReadMs;
+ if (t.carryAnswerMs != null) data.carryAnswerMs = t.carryAnswerMs;
+ if (t.carrySessionLength != null) data.carrySessionLength = t.carrySessionLength;
+ if (t.carryWalkSpeedMultForMapId !== undefined) data.carryWalkSpeedMultForMapId = t.carryWalkSpeedMultForMapId;
+ if (t.carryWalkSpeedMult !== undefined) data.carryWalkSpeedMult = t.carryWalkSpeedMult;
+ }
+ renderQuizCarryAdminList(data.carryQuestions || []);
+ var readSec = el('quiz-carry-read-sec');
+ var ansSec = el('quiz-carry-answer-sec');
+ if (readSec) {
+ var rms = Number(data.carryReadMs);
+ readSec.value = String(Number.isFinite(rms) ? Math.round(rms / 1000) : 3);
+ }
+ if (ansSec) {
+ var ams = Number(data.carryAnswerMs);
+ ansSec.value = String(Number.isFinite(ams) ? Math.round(ams / 1000) : 5);
+ }
+ var sessLen = el('quiz-carry-session-len');
+ if (sessLen) {
+ var sl = parseInt(String(data.carrySessionLength), 10);
+ sessLen.value = String(Number.isFinite(sl) && sl >= 0 ? sl : 0);
+ }
+ var walkMapIdInp = el('quiz-carry-walk-map-id');
+ if (walkMapIdInp) {
+ walkMapIdInp.value = data.carryWalkSpeedMultForMapId ? String(data.carryWalkSpeedMultForMapId).trim() : '';
+ }
+ var walkMultInp = el('quiz-carry-walk-mult');
+ if (walkMultInp) {
+ var wmv = Number(data.carryWalkSpeedMult);
+ walkMultInp.value = String(Number.isFinite(wmv) ? wmv : 1.42);
+ }
+ quizCarrySetPlaqueMapScaleInputs(data.carryChoicePlaqueMapScale);
+ quizCarryBindThemePickersOnce();
+ var th = data.carryMapPanelTheme && typeof data.carryMapPanelTheme === 'object' ? data.carryMapPanelTheme : {};
+ quizCarrySetPickersFromColorString('bg', th.panelBg);
+ quizCarrySetPickersFromColorString('border', th.panelBorder);
+ quizCarrySetPickersFromColorString('text', th.textColor);
+ var bwInp = el('quiz-carry-theme-border-w');
+ if (bwInp) {
+ var bw = parseInt(String(th.borderWidthPx), 10);
+ bwInp.value = String(Number.isFinite(bw) && bw >= 0 ? Math.min(12, bw) : 2);
+ }
+ var qfMinInp = el('quiz-carry-theme-qfont-min');
+ var qfMaxInp = el('quiz-carry-theme-qfont-max');
+ if (qfMinInp) {
+ var qm = parseInt(String(th.questionFontMinPx), 10);
+ qfMinInp.value = String(Number.isFinite(qm) ? Math.max(10, Math.min(40, qm)) : 10);
+ }
+ if (qfMaxInp) {
+ var qx = parseInt(String(th.questionFontMaxPx), 10);
+ qfMaxInp.value = String(Number.isFinite(qx) ? Math.max(14, Math.min(56, qx)) : 24);
+ }
+ quizCarryEcdFillForm(data.carryEmbedCountdownTheme || {});
+ quizCarryPlaqueFillAllFromApi(data);
+ if (opts.clearMsg !== false) setMsg('quiz-carry-settings-msg', '', '');
+ }).catch(function (e) {
+ setMsg('quiz-carry-settings-msg', e.message || 'โหลดไม่ได้', 'error');
+ });
+ };
+ if (delay > 0) setTimeout(runLoad, delay);
+ else runLoad();
+ }
+
+ /** รอจน plaque upload จบ (pending=0) — กันกดบันทึกทันทีหลังเลือกไฟล์แล้ว build ได้ URL ว่าง · returns false if timeout */
+ function waitUntilQuizCarryUploadsDone(maxMs) {
+ maxMs = maxMs || 120000;
+ return new Promise(function (resolve) {
+ if (quizCarryPlaqueUploadPending <= 0) return resolve(true);
+ var started = Date.now();
+ var iv = setInterval(function () {
+ if (quizCarryPlaqueUploadPending <= 0) {
+ clearInterval(iv);
+ resolve(true);
+ } else if (Date.now() - started >= maxMs) {
+ clearInterval(iv);
+ resolve(false);
+ }
+ }, 40);
+ });
+ }
+
+ function saveQuizCarryPanel() {
+ var list = el('quiz-carry-admin-list');
+ var saveBtn = el('btn-quiz-carry-save');
+ if (!list) {
+ setMsg('quiz-carry-settings-msg', 'ไม่พบรายการคำถามในแท็บนี้ — รีเฟรชหน้า · quiz-carry-admin-list missing', 'error');
+ return;
+ }
+ if (quizCarryPlaqueUploadPending > 0 && quizCarryPlaqueUploadStartMs > 0 && Date.now() - quizCarryPlaqueUploadStartMs > 120000) {
+ quizCarryPlaqueUploadPending = 0;
+ quizCarryPlaqueUploadStartMs = 0;
+ setMsg(
+ 'quiz-carry-settings-msg',
+ 'ยกเลิกสถานะอัปโหลดค้าง (>2 นาที) แล้วดำเนินการบันทึก — ถ้ายังต้องการรูปให้อัปโหลดใหม่ · Stuck upload cleared; re-upload if needed',
+ 'error',
+ );
+ }
+ if (quizCarrySaveInFlight) {
+ setMsg('quiz-carry-settings-msg', 'กำลังบันทึกอยู่แล้ว — รอสักครู่ · Save already in progress', 'error');
+ return;
+ }
+ var carryQuestions = [];
+ list.querySelectorAll('.quiz-carry-admin-block').forEach(function (block) {
+ var inpQ = block.querySelector('.quiz-carry-q-text');
+ var text = inpQ && inpQ.value ? String(inpQ.value).trim() : '';
+ if (!text) return;
+ var slots = [];
+ block.querySelectorAll('.quiz-carry-slot-label').forEach(function (lab, idx) {
+ var ti = lab.querySelector('.quiz-carry-slot-inp');
+ var ii = lab.querySelector('.quiz-carry-slot-img');
+ var tv = ti && ti.value ? String(ti.value).trim() : '';
+ var iv = ii && ii.value ? String(ii.value).trim().slice(0, 512) : '';
+ if (tv) slots.push({ idx: idx, text: tv, imageUrl: iv });
+ });
+ if (slots.length < 2) return;
+ var sel = block.querySelector('.quiz-carry-correct-sel');
+ var slotStr = sel && !sel.disabled && sel.value !== '' ? sel.value : null;
+ if (slotStr == null) return;
+ var slotNum = parseInt(slotStr, 10);
+ var correctIndex = -1;
+ for (var i = 0; i < slots.length; i++) {
+ if (slots[i].idx === slotNum) {
+ correctIndex = i;
+ break;
+ }
+ }
+ if (correctIndex < 0) return;
+ var rowOut = {
+ text: text,
+ choices: slots.map(function (s) {
+ return s.text;
+ }),
+ correctIndex: correctIndex,
+ };
+ var urls = slots.map(function (s) {
+ return s.imageUrl || '';
+ });
+ if (urls.some(function (u) {
+ return u;
+ })) rowOut.choiceImageUrls = urls;
+ carryQuestions.push(rowOut);
+ });
+
+ var readSecEl = el('quiz-carry-read-sec');
+ var ansSecEl = el('quiz-carry-answer-sec');
+ var readSec = readSecEl ? parseInt(String(readSecEl.value || '3'), 10) : 3;
+ var ansSec = ansSecEl ? parseInt(String(ansSecEl.value || '5'), 10) : 5;
+ if (!Number.isFinite(readSec) || readSec < 0) readSec = 0;
+ if (readSec > 120) readSec = 120;
+ if (!Number.isFinite(ansSec) || ansSec < 1) ansSec = 1;
+ if (ansSec > 300) ansSec = 300;
+ var carryReadMs = readSec * 1000;
+ var carryAnswerMs = ansSec * 1000;
+ var sessEl = el('quiz-carry-session-len');
+ var carrySessionLength = sessEl ? parseInt(String(sessEl.value || '0'), 10) : 0;
+ if (!Number.isFinite(carrySessionLength) || carrySessionLength < 0) carrySessionLength = 0;
+ if (carrySessionLength > 500) carrySessionLength = 500;
+
+ var carryMapPanelTheme = quizCarryBuildThemeForSave();
+
+ var blockEls = list.querySelectorAll('.quiz-carry-admin-block');
+ var hasAnyQuestionText = false;
+ for (var bi = 0; bi < blockEls.length; bi++) {
+ var qn = blockEls[bi].querySelector('.quiz-carry-q-text');
+ if (qn && String(qn.value || '').trim()) {
+ hasAnyQuestionText = true;
+ break;
+ }
+ }
+ if (carryQuestions.length === 0 && hasAnyQuestionText) {
+ setMsg(
+ 'quiz-carry-settings-msg',
+ 'ยังบันทึกชุดคำถามไม่ได้: แต่ละข้อต้องมีคำถาม ตัวเลือกอย่างน้อย 2 ช่องที่กรอกแล้ว และเลือก «ข้อที่ถูกต้อง» — ตรวจแถวที่มีข้อความค้าง',
+ 'error',
+ );
+ return;
+ }
+
+ quizCarrySaveInFlight = true;
+ if (saveBtn) {
+ saveBtn.disabled = true;
+ saveBtn.setAttribute('aria-busy', 'true');
+ }
+ var waitLine =
+ quizCarryPlaqueUploadPending > 0
+ ? 'กำลังรอให้อัปโหลดรูปป้ายจบ แล้วจะบันทึกอัตโนมัติ (ไม่ต้องกดซ้ำ)… · Waiting for plaque upload, then save…'
+ : 'กำลังบันทึก… · Saving…';
+ setMsg('quiz-carry-settings-msg', waitLine, '');
+ try {
+ var smEl = el('quiz-carry-settings-msg');
+ if (smEl) smEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ } catch (scrollSave) { /* ignore */ }
+
+ waitUntilQuizCarryUploadsDone(120000).then(function (uploadsOk) {
+ if (!uploadsOk) {
+ quizCarrySaveInFlight = false;
+ if (saveBtn) {
+ saveBtn.disabled = false;
+ saveBtn.removeAttribute('aria-busy');
+ }
+ setMsg(
+ 'quiz-carry-settings-msg',
+ 'รออัปโหลดนานเกิน 2 นาที — ตรวจเครือข่าย/เซิร์ฟเกมแล้วลองใหม่ · Upload wait timeout',
+ 'error',
+ );
+ return;
+ }
+ var plaqueScaleEl = el('quiz-carry-plaque-map-scale');
+ var plaqueMapScale = 1.25;
+ if (plaqueScaleEl) {
+ var psn = parseFloat(String(plaqueScaleEl.value || '').replace(',', '.'));
+ if (Number.isFinite(psn)) plaqueMapScale = Math.max(0.85, Math.min(2.5, psn));
+ }
+ var walkMapIdRaw = el('quiz-carry-walk-map-id') && el('quiz-carry-walk-map-id').value ? String(el('quiz-carry-walk-map-id').value).trim() : '';
+ var walkMapId = walkMapIdRaw.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 64);
+ var walkMultRaw = el('quiz-carry-walk-mult') ? parseFloat(String(el('quiz-carry-walk-mult').value || '').replace(',', '.')) : NaN;
+ var putBody = {
+ carryReadMs: carryReadMs,
+ carryAnswerMs: carryAnswerMs,
+ carrySessionLength: carrySessionLength,
+ carryMapPanelTheme: carryMapPanelTheme,
+ carryEmbedCountdownTheme: quizCarryEcdBuildForSave(),
+ carryChoicePlaqueThemes: quizCarryPlaqueBuildForSave(),
+ carryChoicePlaqueMapScale: plaqueMapScale,
+ };
+ if (walkMapId) {
+ putBody.carryWalkSpeedMultForMapId = walkMapId;
+ putBody.carryWalkSpeedMult = Number.isFinite(walkMultRaw) ? Math.max(0.5, Math.min(3, walkMultRaw)) : 1.42;
+ } else {
+ putBody.carryWalkSpeedMultForMapId = '';
+ putBody.carryWalkSpeedMult = null;
+ }
+ if (carryQuestions.length > 0) {
+ putBody.carryQuestions = carryQuestions;
+ }
+
+ gameQuizFetch('PUT', putBody).then(function (res) {
+ var tail = carryQuestions.length ? ' · คำถาม ' + carryQuestions.length + ' ข้อ' : ' · คงชุดคำถามเดิม (อัปเดตเวลา/ธีมอย่างเดียว)';
+ var themeFromRes = res && res.carryMapPanelTheme && typeof res.carryMapPanelTheme === 'object' ? res.carryMapPanelTheme : null;
+ var ecdFromRes = res && res.carryEmbedCountdownTheme && typeof res.carryEmbedCountdownTheme === 'object' ? res.carryEmbedCountdownTheme : null;
+ var plaqueFromRes = null;
+ if (res && Array.isArray(res.carryChoicePlaqueThemes) && res.carryChoicePlaqueThemes.length) {
+ plaqueFromRes = res.carryChoicePlaqueThemes;
+ } else if (res && res.carryChoicePlaqueTheme && typeof res.carryChoicePlaqueTheme === 'object') {
+ plaqueFromRes = [];
+ for (var pxi = 0; pxi < QUIZ_CARRY_PLAQUE_SLOT_COUNT; pxi++) {
+ plaqueFromRes.push(res.carryChoicePlaqueTheme);
+ }
+ }
+ if (!plaqueFromRes) {
+ plaqueFromRes = quizCarryPlaqueBuildForSave();
+ }
+ var themeOk = !!themeFromRes || !!ecdFromRes || !!plaqueFromRes;
+ var themeTail = themeOk ? ' · ธีมแผง/ป้าย/นับถอยหลังบันทึกแล้ว' : '';
+ setMsg('quiz-carry-settings-msg', 'บันทึกแล้ว' + tail + themeTail, 'ok');
+ quizCarryPlaquePendingUrlBySlot = {};
+ loadQuizCarryPanel({
+ clearMsg: false,
+ themeOverride: {
+ carryMapPanelTheme: themeFromRes || carryMapPanelTheme,
+ carryEmbedCountdownTheme: ecdFromRes || quizCarryEcdBuildForSave(),
+ carryChoicePlaqueThemes: Array.isArray(plaqueFromRes) ? plaqueFromRes : null,
+ carryChoicePlaqueTheme:
+ Array.isArray(plaqueFromRes) && plaqueFromRes[0] && typeof plaqueFromRes[0] === 'object'
+ ? plaqueFromRes[0]
+ : null,
+ },
+ timingOverride: {
+ carryReadMs: carryReadMs,
+ carryAnswerMs: carryAnswerMs,
+ carrySessionLength: carrySessionLength,
+ carryWalkSpeedMultForMapId: walkMapId,
+ carryWalkSpeedMult: walkMapId ? (Number.isFinite(walkMultRaw) ? Math.max(0.5, Math.min(3, walkMultRaw)) : 1.42) : null,
+ },
+ fetchDelayMs: 120,
+ });
+ }).catch(function (e) {
+ setMsg('quiz-carry-settings-msg', e.message || 'บันทึกไม่ได้', 'error');
+ }).finally(function () {
+ quizCarrySaveInFlight = false;
+ if (saveBtn) {
+ saveBtn.disabled = false;
+ saveBtn.removeAttribute('aria-busy');
+ }
+ });
+ });
+ }
+
+ /** หมวด Quiz Battle — ชื่อไทยให้สอดคล้องธีมการศึกษาไซเบอร์ / หน้า Quiz-Battle */
+ var QB_LABELS_ABC = ['A', 'B', 'C'];
+
+ var QUIZ_BATTLE_CATEGORIES = [
+ { id: 'topic1', labelTh: 'ห้อง 1 — กฎหมายใกล้ตัว', labelEn: 'Room 1' },
+ { id: 'topic2', labelTh: 'ห้อง 2 — กฎหมายสิทธิ์พื้นฐาน', labelEn: 'Room 2' },
+ { id: 'topic3', labelTh: 'ห้อง 3 — กฎหมายจราจร', labelEn: 'Room 3' },
+ { id: 'topic4', labelTh: 'ห้อง 4 — คดีเกี่ยวกับทรัพย์', labelEn: 'Room 4' },
+ { id: 'topic5', labelTh: 'ห้อง 5 — คดีหมิ่นประมาท', labelEn: 'Room 5' },
+ { id: 'topic6', labelTh: 'ห้อง 6 — คดีทางเพศ', labelEn: 'Room 6' },
+ { id: 'topic7', labelTh: 'ห้อง 7 — คดีอาชญากรรม', labelEn: 'Room 7' },
+ { id: 'topic8', labelTh: 'ห้อง 8 — อาชญากรรมออนไลน์', labelEn: 'Room 8' },
+ { id: 'topic9', labelTh: 'ห้อง 9 — กระบวนการยุติธรรม', labelEn: 'Room 9' },
+ { id: 'topic10', labelTh: 'ห้อง 10 — งานบริการกระทรวงยุติธรรม', labelEn: 'Room 10' },
+ ];
+
+ function qbBattleCategoryLabel(catId) {
+ for (var i = 0; i < QUIZ_BATTLE_CATEGORIES.length; i++) {
+ if (QUIZ_BATTLE_CATEGORIES[i].id === catId) return QUIZ_BATTLE_CATEGORIES[i].labelTh;
+ }
+ return 'Quiz Battle';
+ }
+
+ function fillQbBattleFilterOptions() {
+ var sel = el('qb-battle-filter-cat');
+ if (!sel) return;
+ var keep = sel.value;
+ while (sel.options.length > 1) sel.remove(1);
+ QUIZ_BATTLE_CATEGORIES.forEach(function (c) {
+ var o = document.createElement('option');
+ o.value = c.id;
+ o.textContent = c.labelTh;
+ sel.appendChild(o);
+ });
+ if (keep) {
+ for (var oi = 0; oi < sel.options.length; oi++) {
+ if (sel.options[oi].value === keep) {
+ sel.value = keep;
+ break;
+ }
+ }
+ }
+ }
+
+ function applyQbBattleFilter() {
+ var list = el('qb-battle-admin-list');
+ var f = el('qb-battle-filter-cat');
+ if (!list || !f) return;
+ var fv = f.value || '';
+ list.querySelectorAll('.qb-battle-block').forEach(function (row) {
+ var catInp = row.querySelector('.qb-battle-cat');
+ var cat = catInp && catInp.value ? catInp.value : '';
+ row.hidden = !!(fv && cat !== fv);
+ });
+ refreshQbBattlePreview();
+ }
+
+ function getFirstVisibleQbBattleBlock() {
+ var list = el('qb-battle-admin-list');
+ if (!list) return null;
+ var rows = list.querySelectorAll('.qb-battle-block');
+ for (var i = 0; i < rows.length; i++) {
+ if (rows[i].hidden) continue;
+ var q = rows[i].querySelector('.qb-battle-q-text');
+ var t = q && q.value ? String(q.value).trim() : '';
+ if (t) return rows[i];
+ }
+ for (var j = 0; j < rows.length; j++) {
+ if (!rows[j].hidden) return rows[j];
+ }
+ return null;
+ }
+
+ function readQbBattleBlockData(row) {
+ if (!row) return null;
+ var catSel = row.querySelector('.qb-battle-cat');
+ var qInp = row.querySelector('.qb-battle-q-text');
+ var a0 = row.querySelector('.qb-battle-a');
+ var a1 = row.querySelector('.qb-battle-b');
+ var a2 = row.querySelector('.qb-battle-c');
+ var radios = row.querySelectorAll('input[type="radio"][name^="qb-correct-"]');
+ var ci = 0;
+ for (var i = 0; i < radios.length; i++) {
+ if (radios[i].checked) {
+ ci = parseInt(radios[i].value, 10);
+ if (!Number.isFinite(ci)) ci = 0;
+ break;
+ }
+ }
+ return {
+ categoryId: catSel && catSel.value ? catSel.value : 'topic1',
+ text: qInp && qInp.value ? String(qInp.value).trim() : '',
+ choices: [
+ a0 && a0.value ? String(a0.value).trim() : '',
+ a1 && a1.value ? String(a1.value).trim() : '',
+ a2 && a2.value ? String(a2.value).trim() : '',
+ ],
+ correctIndex: Math.max(0, Math.min(2, ci)),
+ };
+ }
+
+ function refreshQbBattlePreview() {
+ var hudCat = el('qb-preview-hud-cat');
+ var qtext = el('qb-preview-modal-q');
+ var c0 = el('qb-preview-c0');
+ var c1 = el('qb-preview-c1');
+ var c2 = el('qb-preview-c2');
+ var qModal = el('qb-cyber-modal-question');
+ var sModal = el('qb-cyber-modal-summary');
+ var layer = el('qb-modal-layer');
+ if (!hudCat || !qtext || !c0 || !c1 || !c2 || !qModal || !sModal) return;
+
+ var modeBtn = document.querySelector('.qb-preview-mode-btn.is-active');
+ var mode = modeBtn && modeBtn.getAttribute('data-qb-mode') ? modeBtn.getAttribute('data-qb-mode') : 'question';
+
+ var row = getFirstVisibleQbBattleBlock();
+ var d = readQbBattleBlockData(row);
+ var defQ = 'การกระทำใดผิดกฎหมายไซเบอร์?';
+ var defC = [
+ 'การเจาะระบบเข้าสู่ข้อมูลส่วนบุคคล',
+ 'การใช้ WI-FI สาธารณะเพื่อดูข่าว',
+ 'การอัปเดตซอฟต์แวร์เพื่อความปลอดภัย',
+ ];
+ hudCat.textContent = d ? qbBattleCategoryLabel(d.categoryId) : qbBattleCategoryLabel('topic1');
+ qtext.textContent = d && d.text ? d.text : defQ;
+ var ch = d ? d.choices : defC;
+ for (var s = 0; s < 3; s++) {
+ var eln = s === 0 ? c0 : s === 1 ? c1 : c2;
+ var txt = ch[s] ? ch[s] : defC[s];
+ eln.textContent = QB_LABELS_ABC[s] + ' : ' + txt;
+ }
+
+ var choiceEls = document.querySelectorAll('#qb-preview-choices .qb-cyber-choice');
+ choiceEls.forEach(function (node) {
+ node.classList.remove('qb-cyber-choice--correct', 'qb-cyber-choice--wrong', 'qb-cyber-choice--neutral');
+ var mark = node.querySelector('.qb-cyber-choice-mark');
+ if (mark) mark.textContent = '';
+ });
+
+ if (mode === 'summary') {
+ qModal.hidden = true;
+ sModal.hidden = false;
+ if (layer) layer.classList.add('qb-modal-layer--summary');
+ } else {
+ qModal.hidden = false;
+ sModal.hidden = true;
+ if (layer) layer.classList.remove('qb-modal-layer--summary');
+ var ci = d ? d.correctIndex : 0;
+ if (mode === 'revealed') {
+ var wrongPick = (ci + 1) % 3;
+ choiceEls.forEach(function (node, idx) {
+ var mark = node.querySelector('.qb-cyber-choice-mark');
+ if (idx === ci) {
+ node.classList.add('qb-cyber-choice--correct');
+ if (mark) mark.textContent = '✓';
+ } else if (idx === wrongPick) {
+ node.classList.add('qb-cyber-choice--wrong');
+ if (mark) mark.textContent = '✕';
+ } else {
+ node.classList.add('qb-cyber-choice--neutral');
+ }
+ });
+ }
+ }
+ }
+
+ function addQbBattleRow(q) {
+ var list = el('qb-battle-admin-list');
+ if (!list) return;
+ q = q || {};
+ var uid = 'r' + Math.random().toString(36).slice(2, 10);
+ var block = document.createElement('div');
+ block.className = 'qb-battle-block';
+ block.setAttribute('data-category', q.categoryId || 'topic1');
+
+ var head = document.createElement('div');
+ head.className = 'qb-battle-block-head';
+
+ var labCat = document.createElement('label');
+ labCat.className = 'qb-battle-cat-label';
+ labCat.appendChild(document.createTextNode('หมวด'));
+ var selCat = document.createElement('select');
+ selCat.className = 'qb-battle-cat';
+ QUIZ_BATTLE_CATEGORIES.forEach(function (c) {
+ var o = document.createElement('option');
+ o.value = c.id;
+ o.textContent = c.labelTh;
+ if ((q.categoryId || 'topic1') === c.id) o.selected = true;
+ selCat.appendChild(o);
+ });
+ selCat.addEventListener('change', function () {
+ block.setAttribute('data-category', selCat.value);
+ applyQbBattleFilter();
+ });
+ labCat.appendChild(selCat);
+
+ var rm = document.createElement('button');
+ rm.type = 'button';
+ rm.className = 'btn btn-danger btn-sm qb-battle-remove';
+ rm.textContent = 'ลบข้อ';
+ rm.addEventListener('click', function () {
+ block.remove();
+ if (!list.querySelector('.qb-battle-block')) addQbBattleRow(null);
+ applyQbBattleFilter();
+ });
+
+ head.appendChild(labCat);
+ head.appendChild(rm);
+ block.appendChild(head);
+
+ var labQ = document.createElement('label');
+ labQ.className = 'qb-battle-q-label';
+ labQ.appendChild(document.createTextNode('คำถาม'));
+ var inpQ = document.createElement('input');
+ inpQ.type = 'text';
+ inpQ.className = 'qb-battle-q-text';
+ inpQ.maxLength = 500;
+ inpQ.placeholder = 'พิมพ์คำถาม…';
+ inpQ.value = q.text ? String(q.text) : '';
+ labQ.appendChild(inpQ);
+ block.appendChild(labQ);
+
+ var choices = Array.isArray(q.choices) ? q.choices : [];
+ var grid = document.createElement('div');
+ grid.className = 'qb-battle-abc-grid';
+ [['A', 'a', 0], ['B', 'b', 1], ['C', 'c', 2]].forEach(function (tri) {
+ var lab = document.createElement('label');
+ lab.className = 'qb-battle-opt-label';
+ lab.appendChild(document.createTextNode('ตัวเลือก ' + tri[0]));
+ var inp = document.createElement('input');
+ inp.type = 'text';
+ inp.className = 'qb-battle-' + tri[1];
+ inp.maxLength = 160;
+ inp.placeholder = tri[0];
+ inp.value = choices[tri[2]] != null ? String(choices[tri[2]]) : '';
+ lab.appendChild(inp);
+ grid.appendChild(lab);
+ });
+ block.appendChild(grid);
+
+ var ci = Number(q.correctIndex);
+ if (!Number.isFinite(ci)) ci = 0;
+ ci = Math.max(0, Math.min(2, Math.floor(ci)));
+
+ var fs = document.createElement('fieldset');
+ fs.className = 'qb-battle-correct-fs';
+ var leg = document.createElement('legend');
+ leg.textContent = 'ข้อที่ถูกต้อง';
+ fs.appendChild(leg);
+ var rg = document.createElement('div');
+ rg.className = 'qb-battle-correct-rg';
+ for (var r = 0; r < 3; r++) {
+ var labR = document.createElement('label');
+ labR.className = 'qb-battle-radio-label';
+ var radio = document.createElement('input');
+ radio.type = 'radio';
+ radio.name = 'qb-correct-' + uid;
+ radio.value = String(r);
+ if (r === ci) radio.checked = true;
+ labR.appendChild(radio);
+ labR.appendChild(document.createTextNode(' ' + QB_LABELS_ABC[r]));
+ rg.appendChild(labR);
+ }
+ fs.appendChild(rg);
+ block.appendChild(fs);
+
+ function onFieldInput() {
+ refreshQbBattlePreview();
+ }
+ inpQ.addEventListener('input', onFieldInput);
+ grid.querySelectorAll('input').forEach(function (inp) {
+ inp.addEventListener('input', onFieldInput);
+ });
+ rg.querySelectorAll('input[type="radio"]').forEach(function (rdo) {
+ rdo.addEventListener('change', onFieldInput);
+ });
+
+ list.appendChild(block);
+ applyQbBattleFilter();
+ }
+
+ function renderQbBattleList(arr, clearMsg) {
+ var list = el('qb-battle-admin-list');
+ if (!list) return;
+ list.innerHTML = '';
+ fillQbBattleFilterOptions();
+ var rows = arr && arr.length ? arr : [null];
+ rows.forEach(function (q) {
+ addQbBattleRow(q);
+ });
+ if (clearMsg !== false) setMsg('qb-battle-settings-msg', '', '');
+ }
+
+ function loadQbBattlePanel(keepSavedMsg) {
+ gameQuizFetch('GET').then(function (data) {
+ renderQbBattleList(data.battleQuizMcq || [], !keepSavedMsg);
+ refreshQbBattlePreview();
+ }).catch(function (e) {
+ setMsg('qb-battle-settings-msg', e.message || 'โหลดไม่ได้', 'error');
+ });
+ }
+
+ function classifyQbBattleRow(block) {
+ var d = readQbBattleBlockData(block);
+ if (!d) return { kind: 'empty', d: null };
+ var hasQ = !!d.text;
+ var filled = 0;
+ for (var i = 0; i < d.choices.length; i++) {
+ if (d.choices[i]) filled++;
+ }
+ if (!hasQ && filled === 0) return { kind: 'empty', d: d };
+ if (hasQ && filled === 3) return { kind: 'ok', d: d };
+ return { kind: 'partial', d: d };
+ }
+
+ function saveQbBattlePanel() {
+ var list = el('qb-battle-admin-list');
+ if (!list) return;
+ var battleQuizMcq = [];
+ var partialLines = [];
+ var blocks = list.querySelectorAll('.qb-battle-block');
+ for (var bi = 0; bi < blocks.length; bi++) {
+ var block = blocks[bi];
+ var st = classifyQbBattleRow(block);
+ if (st.kind === 'empty') continue;
+ if (st.kind === 'partial') {
+ partialLines.push('แถวที่ ' + (bi + 1) + ': ต้องกรอกคำถามและตัวเลือก A, B, C ให้ครบทุกช่อง');
+ continue;
+ }
+ battleQuizMcq.push({
+ categoryId: st.d.categoryId,
+ text: st.d.text,
+ choices: st.d.choices,
+ correctIndex: st.d.correctIndex,
+ });
+ }
+ if (partialLines.length) {
+ setMsg(
+ 'qb-battle-settings-msg',
+ 'ยังบันทึกไม่ได้ — ' + partialLines.join(' · ') + ' · (Empty question/options are skipped by the server; fill all fields.)',
+ 'error'
+ );
+ return;
+ }
+
+ gameQuizFetch('PUT', { battleQuizMcq: battleQuizMcq }).then(function (res) {
+ var n = battleQuizMcq.length;
+ var savedN = res && typeof res.battleQuizMcqSaved === 'number' ? res.battleQuizMcqSaved : n;
+ if (n > 0 && savedN !== n) {
+ setMsg(
+ 'qb-battle-settings-msg',
+ 'เซิร์ฟเวอร์รับเพียง ' + savedN + ' จาก ' + n + ' ข้อ (ข้อที่ไม่ผ่านกฎถูกตัด) · Server kept ' + savedN + '/' + n + ' — ตรวจหมวด/คำถาม/A B C ให้ครบ · Restart Node (Game/server.js) ถ้ายังผิดปกติ',
+ 'error'
+ );
+ } else {
+ setMsg(
+ 'qb-battle-settings-msg',
+ n ? ('บันทึกแล้ว ' + n + ' ข้อ (ดิสก์ + Node) · Saved ' + n + ' question(s)') : 'บันทึกแล้ว (0 ข้อ) · Saved empty set',
+ 'ok'
+ );
+ }
+ return loadQbBattlePanel(true);
+ }).catch(function (e) {
+ setMsg('qb-battle-settings-msg', e.message || 'บันทึกไม่ได้', 'error');
+ });
+ }
+
+ function laneUrlsToText(arr) {
+ if (!arr || !arr.length) return '';
+ return arr.map(function (u) { return String(u || '').trim(); }).filter(Boolean).join('\n');
+ }
+
+ function parseLaneUrlsTextarea() {
+ var raw = el('game-timing-lane-urls') && el('game-timing-lane-urls').value ? String(el('game-timing-lane-urls').value) : '';
+ var lines = raw.split(/[\n\r]+/);
+ var out = [];
+ for (var i = 0; i < lines.length && out.length < 24; i++) {
+ var t = lines[i].trim();
+ if (t) out.push(t);
+ }
+ return out;
+ }
+
+ function moveLaneUrl(index, dir) {
+ var urls = parseLaneUrlsTextarea();
+ var j = index + dir;
+ if (j < 0 || j >= urls.length) return;
+ var t = urls[index];
+ urls[index] = urls[j];
+ urls[j] = t;
+ var ta = el('game-timing-lane-urls');
+ if (ta) ta.value = urls.join('\n');
+ renderGameTimingLaneVisuals();
+ }
+
+ function renderGameTimingLaneVisuals() {
+ var wrap = el('game-timing-lane-visual-list');
+ if (!wrap) return;
+ wrap.innerHTML = '';
+ var urls = parseLaneUrlsTextarea();
+ if (!urls.length) {
+ var ph = document.createElement('p');
+ ph.className = 'muted';
+ ph.style.margin = '0';
+ ph.textContent = 'ยังไม่มีรูป — กด «Lane» ที่การ์ดในคลัง · No lane images yet';
+ wrap.appendChild(ph);
+ return;
+ }
+ urls.forEach(function (u, i) {
+ var card = document.createElement('div');
+ card.className = 'game-timing-visual-card';
+ card.setAttribute('role', 'listitem');
+ var img = document.createElement('img');
+ img.className = 'game-timing-visual-card-img';
+ img.src = u;
+ img.alt = '';
+ img.referrerPolicy = 'no-referrer';
+ img.onerror = function () { img.classList.add('is-broken'); };
+ var actions = document.createElement('div');
+ actions.className = 'game-timing-visual-card-actions';
+ function mkBtn(cls, text, fn) {
+ var b = document.createElement('button');
+ b.type = 'button';
+ b.className = 'btn btn-sm ' + cls;
+ b.textContent = text;
+ b.addEventListener('click', fn);
+ return b;
+ }
+ actions.appendChild(mkBtn('btn-ghost', 'ดู', function () {
+ window.open(u, '_blank', 'noopener,noreferrer');
+ }));
+ actions.appendChild(mkBtn('btn-ghost', '←', function () {
+ moveLaneUrl(i, -1);
+ }));
+ actions.appendChild(mkBtn('btn-ghost', '→', function () {
+ moveLaneUrl(i, 1);
+ }));
+ actions.appendChild(mkBtn('btn-danger', 'ลบ', function () {
+ var next = urls.filter(function (_, j) { return j !== i; });
+ var ta = el('game-timing-lane-urls');
+ if (ta) ta.value = next.join('\n');
+ renderGameTimingLaneVisuals();
+ setGauntletUploadMsg('เอาออกจาก Lane แล้ว (ไฟล์ในคลังยังอยู่) · Removed from lane', 'ok');
+ }));
+ card.appendChild(img);
+ card.appendChild(actions);
+ wrap.appendChild(card);
+ });
+ }
+
+ /** พรีวิวคอลัมน์ beam — สูงแนวตั้ง + สีเติม/กรอบตามช่อง (คล้ายในเกม) */
+ function styleGameTimingLaserBeamPreviewShell() {
+ var prev = el('game-timing-laser-line-preview');
+ if (!prev) return;
+ var fillIn = el('game-timing-laser-fill');
+ var strokeIn = el('game-timing-laser-stroke');
+ var lwIn = el('game-timing-laser-line-width');
+ var fill = fillIn && fillIn.value ? String(fillIn.value).trim() : '';
+ var stroke = strokeIn && strokeIn.value ? String(strokeIn.value).trim() : '';
+ var lw = lwIn ? parseInt(lwIn.value, 10) : 2;
+ if (Number.isNaN(lw)) lw = 2;
+ lw = Math.max(0, Math.min(24, lw));
+ prev.style.backgroundColor = fill || '';
+ if (lw > 0 && stroke) {
+ prev.style.boxShadow = 'inset 0 0 0 ' + Math.min(8, lw) + 'px ' + stroke;
+ } else {
+ prev.style.boxShadow = '';
+ }
+ }
+
+ function renderGameTimingLaserVisuals() {
+ var slots = [
+ { inpId: 'game-timing-laser-top-url', prevId: 'game-timing-laser-top-preview' },
+ { inpId: 'game-timing-laser-bottom-url', prevId: 'game-timing-laser-bottom-preview' },
+ { inpId: 'game-timing-laser-line-url', prevId: 'game-timing-laser-line-preview' },
+ ];
+ slots.forEach(function (s) {
+ var inp = el(s.inpId);
+ var prev = el(s.prevId);
+ if (!prev) return;
+ prev.innerHTML = '';
+ var url = inp && inp.value ? String(inp.value).trim() : '';
+ if (!url) {
+ var ph = document.createElement('div');
+ ph.className = 'game-timing-laser-placeholder';
+ ph.textContent = 'ยังไม่ตั้ง · Not set';
+ prev.appendChild(ph);
+ return;
+ }
+ var img = document.createElement('img');
+ img.className = 'game-timing-laser-preview-img';
+ img.src = url;
+ img.alt = '';
+ img.referrerPolicy = 'no-referrer';
+ img.onerror = function () { img.classList.add('is-broken'); };
+ prev.appendChild(img);
+ });
+ styleGameTimingLaserBeamPreviewShell();
+ }
+
+ function clamp255(x) {
+ return Math.max(0, Math.min(255, Math.round(Number(x))));
+ }
+
+ function parseCssColorToRgba(str) {
+ str = String(str || '').trim();
+ if (!str) return null;
+ var m = str.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/i);
+ if (m) {
+ var r = clamp255(m[1]);
+ var g = clamp255(m[2]);
+ var b = clamp255(m[3]);
+ var a =
+ m[4] !== undefined && m[4] !== '' && !Number.isNaN(Number(m[4]))
+ ? Math.max(0, Math.min(1, Number(m[4])))
+ : 1;
+ return { r: r, g: g, b: b, a: a };
+ }
+ m = str.match(/^#([\da-f]{3}|[\da-f]{6})$/i);
+ if (m) {
+ var h = m[1];
+ var r2;
+ var g2;
+ var b2;
+ if (h.length === 3) {
+ r2 = parseInt(h[0] + h[0], 16);
+ g2 = parseInt(h[1] + h[1], 16);
+ b2 = parseInt(h[2] + h[2], 16);
+ } else {
+ r2 = parseInt(h.slice(0, 2), 16);
+ g2 = parseInt(h.slice(2, 4), 16);
+ b2 = parseInt(h.slice(4, 6), 16);
+ }
+ if (Number.isNaN(r2) || Number.isNaN(g2) || Number.isNaN(b2)) return null;
+ return { r: r2, g: g2, b: b2, a: 1 };
+ }
+ var probe = document.createElement('span');
+ probe.style.cssText = 'position:absolute;visibility:hidden;left:0;top:0;background-color:' + str;
+ document.body.appendChild(probe);
+ var bg = getComputedStyle(probe).backgroundColor;
+ document.body.removeChild(probe);
+ m = bg && bg.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/);
+ if (!m) return null;
+ r2 = clamp255(m[1]);
+ g2 = clamp255(m[2]);
+ b2 = clamp255(m[3]);
+ var a2 = m[4] !== undefined && !Number.isNaN(Number(m[4])) ? Math.max(0, Math.min(1, Number(m[4]))) : 1;
+ return { r: r2, g: g2, b: b2, a: a2 };
+ }
+
+ function rgbToHex6(r, g, b) {
+ function hx(n) {
+ var h = clamp255(n).toString(16);
+ return h.length === 1 ? '0' + h : h;
+ }
+ return '#' + hx(r) + hx(g) + hx(b);
+ }
+
+ function laserHexFromTextOrFallback(text, fallbackHex) {
+ var c = parseCssColorToRgba(text);
+ if (!c) return fallbackHex;
+ return rgbToHex6(c.r, c.g, c.b);
+ }
+
+ function formatLaserRgba(r, g, b, a) {
+ var aa = Math.round(Math.max(0, Math.min(1, a)) * 100) / 100;
+ return 'rgba(' + clamp255(r) + ',' + clamp255(g) + ',' + clamp255(b) + ',' + aa + ')';
+ }
+
+ function syncGameTimingLaserColorSwatches() {
+ function apply(swatchId, textVal) {
+ var sw = el(swatchId);
+ if (!sw) return;
+ var v = textVal != null ? String(textVal).trim() : '';
+ sw.style.backgroundColor = v || '';
+ sw.classList.toggle('is-empty', !v);
+ sw.title = v || 'ว่าง — พิมพ์ค่า CSS หรือกดสี่เหลี่ยมเพื่อเลือกสี';
+ }
+ var fillIn = el('game-timing-laser-fill');
+ var strokeIn = el('game-timing-laser-stroke');
+ apply('game-timing-laser-fill-swatch', fillIn && fillIn.value);
+ apply('game-timing-laser-stroke-swatch', strokeIn && strokeIn.value);
+ var fillPk = el('game-timing-laser-fill-picker');
+ var strokePk = el('game-timing-laser-stroke-picker');
+ if (fillPk && fillIn) {
+ var hf = laserHexFromTextOrFallback(fillIn.value, '#ef4444');
+ if (fillPk.value.toLowerCase() !== hf.toLowerCase()) fillPk.value = hf;
+ }
+ if (strokePk && strokeIn) {
+ var hs = laserHexFromTextOrFallback(strokeIn.value, '#fca5a5');
+ if (strokePk.value.toLowerCase() !== hs.toLowerCase()) strokePk.value = hs;
+ }
+ styleGameTimingLaserBeamPreviewShell();
+ }
+
+ function bindGameTimingLaserColorSwatches() {
+ var fillIn = el('game-timing-laser-fill');
+ var strokeIn = el('game-timing-laser-stroke');
+ if (fillIn) {
+ fillIn.addEventListener('input', syncGameTimingLaserColorSwatches);
+ fillIn.addEventListener('change', syncGameTimingLaserColorSwatches);
+ }
+ if (strokeIn) {
+ strokeIn.addEventListener('input', syncGameTimingLaserColorSwatches);
+ strokeIn.addEventListener('change', syncGameTimingLaserColorSwatches);
+ }
+ function wirePicker(pickerId, textEl, defaultAlpha) {
+ var pk = el(pickerId);
+ var tx = textEl;
+ if (!pk || !tx) return;
+ function applyHex() {
+ var hex = String(pk.value || '').trim();
+ if (!/^#[\da-f]{6}$/i.test(hex)) return;
+ var r = parseInt(hex.slice(1, 3), 16);
+ var g = parseInt(hex.slice(3, 5), 16);
+ var b = parseInt(hex.slice(5, 7), 16);
+ var prev = parseCssColorToRgba(tx.value);
+ var a = prev && typeof prev.a === 'number' ? prev.a : defaultAlpha;
+ tx.value = formatLaserRgba(r, g, b, a);
+ syncGameTimingLaserColorSwatches();
+ }
+ pk.addEventListener('input', applyHex);
+ pk.addEventListener('change', applyHex);
+ }
+ wirePicker('game-timing-laser-fill-picker', fillIn, 0.35);
+ wirePicker('game-timing-laser-stroke-picker', strokeIn, 0.95);
+ var lwIn = el('game-timing-laser-line-width');
+ if (lwIn) {
+ lwIn.addEventListener('input', styleGameTimingLaserBeamPreviewShell);
+ lwIn.addEventListener('change', styleGameTimingLaserBeamPreviewShell);
+ }
+ syncGameTimingLaserColorSwatches();
+ }
+
+ function bindGameTimingAssignedVisuals() {
+ function wire(which, viewId, clearId) {
+ var inpId = 'game-timing-laser-' + which + '-url';
+ var vBtn = el(viewId);
+ var cBtn = el(clearId);
+ if (vBtn) vBtn.addEventListener('click', function () {
+ var inp = el(inpId);
+ var u = inp && inp.value ? String(inp.value).trim() : '';
+ if (!u) return;
+ window.open(u, '_blank', 'noopener,noreferrer');
+ });
+ if (cBtn) cBtn.addEventListener('click', function () {
+ var inp = el(inpId);
+ if (inp) inp.value = '';
+ renderGameTimingLaserVisuals();
+ setGauntletUploadMsg('ล้างช่อง Laser แล้ว · Laser slot cleared', 'ok');
+ });
+ }
+ wire('top', 'btn-game-timing-laser-top-view', 'btn-game-timing-laser-top-clear');
+ wire('bottom', 'btn-game-timing-laser-bottom-view', 'btn-game-timing-laser-bottom-clear');
+ wire('line', 'btn-game-timing-laser-line-view', 'btn-game-timing-laser-line-clear');
+ }
+
+ function gauntletAssetsJsonFetch(url, opts) {
+ opts = opts || {};
+ opts.credentials = 'include';
+ return fetch(url, opts).then(function (r) {
+ return r.text().then(function (text) {
+ var j = {};
+ var parsed = false;
+ try {
+ j = text ? JSON.parse(text) : {};
+ parsed = true;
+ } catch (e) {
+ if (!r.ok) {
+ var snippet = (text || '').replace(/\s+/g, ' ').trim().slice(0, 220);
+ var tail = '';
+ if (r.status === 413) {
+ tail =
+ ' · คำขอใหญ่เกิน (nginx client_max_body_size / PHP post_max_size) — รัน deploy-nginx และตั้ง post_max_size ≥20M · Payload too large';
+ } else if (r.status === 401) {
+ tail = ' · ล็อกอิน Admin ใหม่ · Re-login to Admin';
+ } else if (r.status === 502 || r.status === 503 || r.status === 504) {
+ tail = ' · เซิร์ฟเกม/PHP upstream ล่ม — ตรวจ Node + php-fpm · Upstream error';
+ } else if (r.status === 404) {
+ tail = ' · ตรวจ path ไฟล์ / รีสตาร์ทบริการหลัง deploy · Not found';
+ }
+ throw new Error((snippet || r.statusText || 'Request failed') + tail + ' (HTTP ' + r.status + ')');
+ }
+ throw e;
+ }
+ if (!r.ok) {
+ var plain = (j && j.error) || r.statusText || 'Request failed';
+ if (r.status === 413) {
+ plain =
+ (j && j.error) ||
+ 'ไฟล์/คำขอใหญ่เกินขีดจำกัด (nginx หรือ PHP) — ตั้ง client_max_body_size (server หรือ /Admin/api/) และ post_max_size ≥20M · Payload too large';
+ }
+ if (r.status === 401) {
+ plain =
+ (j && j.error) ||
+ 'หมดเซสชันแอดมิน — ล็อกอินใหม่แล้วลองอัปโหลดอีกครั้ง · Re-login to Admin and retry upload';
+ }
+ if (r.status === 404) {
+ plain += ' — รีสตาร์ท Node ที่รัน Game/server.js หลัง deploy · Restart Game Node';
+ }
+ if (r.status === 502 || r.status === 503 || r.status === 504) {
+ plain +=
+ ' — PHP/Node upstream ล่ม — ตรวจ game-zep (Node) และ php-fpm · Check Node + PHP-FPM';
+ }
+ if (parsed && (!j || Object.keys(j).length === 0)) {
+ plain = (text || '').replace(/\s+/g, ' ').trim().slice(0, 200) || plain;
+ if (r.status) plain += ' (HTTP ' + r.status + ')';
+ }
+ throw new Error(plain);
+ }
+ return j;
+ });
+ });
+ }
+
+ function setGauntletUploadMsg(text, cls) {
+ setMsg('gauntlet-asset-upload-msg', text, cls);
+ }
+
+ function readFileAsDataURL(file) {
+ return new Promise(function (resolve, reject) {
+ var fr = new FileReader();
+ fr.onload = function () { resolve(fr.result); };
+ fr.onerror = function () { reject(new Error('อ่านไฟล์ไม่ได้ · Cannot read file')); };
+ fr.readAsDataURL(file);
+ });
+ }
+
+ function appendLaneUrlUnique(url) {
+ var ta = el('game-timing-lane-urls');
+ if (!ta || !url) return;
+ var lines = parseLaneUrlsTextarea();
+ if (lines.indexOf(url) >= 0) {
+ setGauntletUploadMsg('มี URL นี้ใน Lane แล้ว · Already in lane list', 'ok');
+ return;
+ }
+ lines.push(url);
+ ta.value = lines.join('\n');
+ renderGameTimingLaneVisuals();
+ setGauntletUploadMsg('เพิ่มใน Lane แล้ว · Added to lane URLs', 'ok');
+ }
+
+ function setLaserUrlField(which, url) {
+ var ids = { top: 'game-timing-laser-top-url', bottom: 'game-timing-laser-bottom-url', line: 'game-timing-laser-line-url' };
+ var inp = el(ids[which]);
+ if (!inp) return;
+ inp.value = url;
+ renderGameTimingLaserVisuals();
+ setGauntletUploadMsg('ตั้งค่า Laser แล้ว · Laser field updated', 'ok');
+ }
+
+ function uploadGauntletDataUrls(fileArr) {
+ var files = [].slice.call(fileArr || []);
+ if (!files.length) return Promise.resolve();
+ var rf = gauntletReplaceTarget;
+ gauntletReplaceTarget = null;
+ if (rf) files = files.slice(0, 1);
+ setGauntletUploadMsg('กำลังอัปโหลด… · Uploading…', '');
+ var idx = 0;
+ function step() {
+ if (idx >= files.length) {
+ setGauntletUploadMsg(rf ? 'แทนที่รูปแล้ว · Image replaced' : ('อัปโหลดแล้ว ' + files.length + ' ไฟล์ · Uploaded'), 'ok');
+ return refreshGauntletAssetLibrary();
+ }
+ var file = files[idx++];
+ return readFileAsDataURL(file).then(function (dataUrl) {
+ var body = { imageDataUrl: dataUrl };
+ if (rf) body.replaceFilename = rf;
+ else if (file.name) body.label = String(file.name).replace(/\.[^.]+$/, '').slice(0, 120);
+ return gauntletAssetsJsonFetch(GAME_GAUNTLET_ASSETS_API + '/upload', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body),
+ });
+ }).then(step).catch(function (e) {
+ setGauntletUploadMsg(e.message || 'อัปโหลดไม่สำเร็จ · Upload failed', 'error');
+ return refreshGauntletAssetLibrary();
+ });
+ }
+ return step();
+ }
+
+ function refreshGauntletAssetLibrary() {
+ var listEl = el('gauntlet-asset-list');
+ if (!listEl) return Promise.resolve();
+ return gauntletAssetsJsonFetch(GAME_GAUNTLET_ASSETS_API, { method: 'GET' })
+ .then(function (data) {
+ listEl.innerHTML = '';
+ var items = data.items || [];
+ if (!items.length) {
+ var empty = document.createElement('p');
+ empty.className = 'muted';
+ empty.style.margin = '0';
+ empty.textContent = 'ยังไม่มีรูปในคลัง — อัปโหลดด้านบน · No assets yet — upload above';
+ listEl.appendChild(empty);
+ return;
+ }
+ items.forEach(function (it, ix) {
+ var card = document.createElement('article');
+ card.className = 'gauntlet-asset-card';
+
+ var img = document.createElement('img');
+ img.className = 'gauntlet-asset-card-preview';
+ img.src = it.url + '?t=' + (it.mtime || 0);
+ img.alt = it.label || '';
+
+ var labelWrap = document.createElement('div');
+ labelWrap.className = 'gauntlet-asset-card-label-wrap';
+ var labelInp = document.createElement('input');
+ labelInp.type = 'text';
+ labelInp.maxLength = 120;
+ labelInp.value = it.label || '';
+ var lid = 'gauntlet-asset-label-' + ix;
+ labelInp.id = lid;
+ var lab = document.createElement('label');
+ lab.htmlFor = lid;
+ lab.textContent = 'ชื่อเรียก · Label';
+ labelWrap.appendChild(lab);
+ labelWrap.appendChild(labelInp);
+
+ var urlP = document.createElement('p');
+ urlP.className = 'gauntlet-asset-card-url';
+ urlP.textContent = it.url;
+
+ var actions = document.createElement('div');
+ actions.className = 'gauntlet-asset-card-actions';
+
+ function mkBtn(cls, text, handler) {
+ var b = document.createElement('button');
+ b.type = 'button';
+ b.className = 'btn ' + cls;
+ b.textContent = text;
+ b.addEventListener('click', handler);
+ return b;
+ }
+
+ actions.appendChild(mkBtn('btn-ghost', 'ดูรูป', function () {
+ window.open(it.url, '_blank', 'noopener,noreferrer');
+ }));
+ actions.appendChild(mkBtn('btn-ghost', 'บันทึกชื่อ', function () {
+ gauntletAssetsJsonFetch(GAME_GAUNTLET_ASSETS_API, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ filename: it.filename, label: labelInp.value }),
+ })
+ .then(function () {
+ setGauntletUploadMsg('บันทึกชื่อแล้ว · Label saved', 'ok');
+ return refreshGauntletAssetLibrary();
+ })
+ .catch(function (e) {
+ setGauntletUploadMsg(e.message, 'error');
+ });
+ }));
+ actions.appendChild(mkBtn('btn-ghost', 'แทนที่รูป', function () {
+ gauntletReplaceTarget = it.filename;
+ var finp = el('gauntlet-asset-file-input');
+ if (finp) finp.click();
+ }));
+ actions.appendChild(mkBtn('btn-ghost', 'Lane', function () {
+ appendLaneUrlUnique(it.url);
+ }));
+ actions.appendChild(mkBtn('btn-ghost', 'Laser บน', function () {
+ setLaserUrlField('top', it.url);
+ }));
+ actions.appendChild(mkBtn('btn-ghost', 'Laser ล่าง', function () {
+ setLaserUrlField('bottom', it.url);
+ }));
+ actions.appendChild(mkBtn('btn-ghost', 'Laser เส้น', function () {
+ setLaserUrlField('line', it.url);
+ }));
+ actions.appendChild(mkBtn('btn-danger', 'ลบ', function () {
+ if (!confirm('ลบไฟล์ ' + it.filename + ' จากเซิร์ฟเวอร์? · Delete from server?')) return;
+ gauntletAssetsJsonFetch(GAME_GAUNTLET_ASSETS_API + '?file=' + encodeURIComponent(it.filename), { method: 'DELETE' })
+ .then(function () {
+ setGauntletUploadMsg('ลบแล้ว · Deleted', 'ok');
+ return refreshGauntletAssetLibrary();
+ })
+ .catch(function (e) {
+ setGauntletUploadMsg(e.message, 'error');
+ });
+ }));
+
+ card.appendChild(img);
+ card.appendChild(labelWrap);
+ card.appendChild(urlP);
+ card.appendChild(actions);
+ listEl.appendChild(card);
+ });
+ })
+ .catch(function (e) {
+ listEl.innerHTML = '';
+ var p = document.createElement('p');
+ p.className = 'msg error';
+ p.textContent = e.message || 'โหลดคลังไม่ได้';
+ listEl.appendChild(p);
+ });
+ }
+
+ function bindGauntletAssetLibrary() {
+ var drop = el('gauntlet-asset-drop');
+ var finp = el('gauntlet-asset-file-input');
+ if (!drop || !finp) return;
+
+ drop.addEventListener('click', function () {
+ finp.click();
+ });
+ drop.addEventListener('keydown', function (e) {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ finp.click();
+ }
+ });
+
+ ['dragenter', 'dragover'].forEach(function (ev) {
+ drop.addEventListener(ev, function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ drop.classList.add('is-dragover');
+ });
+ });
+ ['dragleave', 'drop'].forEach(function (ev) {
+ drop.addEventListener(ev, function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ if (ev !== 'drop') drop.classList.remove('is-dragover');
+ });
+ });
+ drop.addEventListener('drop', function (e) {
+ drop.classList.remove('is-dragover');
+ var files = e.dataTransfer && e.dataTransfer.files;
+ if (!files || !files.length) return;
+ var arr = [];
+ for (var i = 0; i < files.length; i++) arr.push(files[i]);
+ uploadGauntletDataUrls(arr);
+ });
+
+ finp.addEventListener('change', function () {
+ if (!finp.files || !finp.files.length) return;
+ var arr = [];
+ for (var i = 0; i < finp.files.length; i++) arr.push(finp.files[i]);
+ finp.value = '';
+ uploadGauntletDataUrls(arr);
+ });
+ }
+
+ /** ว่าง / อัตโนมัติ / auto → null · ตัวเลข 0.85–3.2 tile */
+ function parseStackBlockWidthForSave(raw) {
+ var t = String(raw == null ? '' : raw).trim();
+ if (!t) return { ok: true, value: null };
+ var low = t.toLowerCase();
+ if (t === 'อัตโนมัติ' || low === 'auto' || low === 'automatic') return { ok: true, value: null };
+ var w = parseFloat(t.replace(',', '.'));
+ if (!Number.isFinite(w)) {
+ return { ok: false, error: 'กว้างบล็อก: ใส่ตัวเลข 0.85–3.2 หรือปล่อยว่างเพื่ออัตโนมัติ' };
+ }
+ if (w < 0.85 || w > 3.2) {
+ return { ok: false, error: 'กว้างบล็อกต้องอยู่ระหว่าง 0.85–3.2 tile หรือปล่อยว่าง' };
+ }
+ return { ok: true, value: Math.round(Math.max(0.85, Math.min(3.2, w)) * 100) / 100 };
+ }
+
+ /** ช่อง Hz ใช้ type=text — ปล่อยว่าง = 0.55 */
+ function parseStackSwingHzForSave(raw) {
+ var t = String(raw == null ? '' : raw).trim().replace(',', '.');
+ if (!t) return { ok: true, value: 0.55 };
+ var h = parseFloat(t);
+ if (!Number.isFinite(h)) {
+ return { ok: false, error: 'สวิง Hz: ใส่ตัวเลข (เช่น 0.58) หรือปล่อยว่างให้เป็น 0.55' };
+ }
+ h = Math.max(0.08, Math.min(2.8, h));
+ return { ok: true, value: Math.round(h * 100) / 100 };
+ }
+
+ /** ซิงก์ช่อง Stack จากข้อมูลเซิร์ฟเวอร์ (ช่องอยู่ใน DOM แม้แท็บอื่นเปิดอยู่ — ต้องไม่ปล่อยให้ค้างค่า default จาก HTML) */
+ function applyGameTimingDataToStackInputs(data) {
+ if (!data || typeof data !== 'object') return;
+ if (el('stack-swing-hz')) {
+ var hz = data.stackSwingHz != null && data.stackSwingHz !== '' ? Number(data.stackSwingHz) : NaN;
+ if (!Number.isFinite(hz)) hz = 0.55;
+ hz = Math.max(0.08, Math.min(2.8, hz));
+ el('stack-swing-hz').value = String(Math.round(hz * 100) / 100);
+ }
+ if (el('stack-block-width-tiles')) {
+ var rawW = data.stackBlockWidthTiles;
+ var bw = rawW != null && rawW !== '' ? Number(rawW) : NaN;
+ el('stack-block-width-tiles').value = Number.isFinite(bw) && bw >= 0.85 ? String(Math.round(bw * 100) / 100) : '';
+ }
+ if (el('stack-tower-mission-sec')) {
+ var st = Number(data.stackTowerMissionTimeSec);
+ el('stack-tower-mission-sec').value = String(Number.isFinite(st) && st > 0 ? Math.floor(Math.max(10, Math.min(7200, st))) : 90);
+ }
+ if (el('stack-team-misses-max')) {
+ var sm = Number(data.stackTeamMissesMax);
+ el('stack-team-misses-max').value = String(Number.isFinite(sm) ? Math.floor(Math.max(1, Math.min(20, sm))) : 3);
+ }
+ if (el('stack-tower-progress-blocks')) {
+ var pb = Number(data.stackTowerProgressBlocks);
+ el('stack-tower-progress-blocks').value = String(Number.isFinite(pb) ? Math.floor(Math.max(1, Math.min(500, pb))) : 50);
+ }
+ if (el('stack-heavy-block-percent')) {
+ var shp = Number(data.stackHeavyBlockPercent);
+ el('stack-heavy-block-percent').value = String(Number.isFinite(shp) ? Math.max(0, Math.min(100, Math.floor(shp))) : 35);
+ }
+ var normArr = Array.isArray(data.stackBlockNormalImageUrls) ? data.stackBlockNormalImageUrls : [];
+ var heavyArr = Array.isArray(data.stackBlockHeavyImageUrls) ? data.stackBlockHeavyImageUrls : [];
+ for (var si = 1; si <= 8; si++) {
+ var nin = el('stack-block-normal-url-' + si);
+ var hin = el('stack-block-heavy-url-' + si);
+ var nv = normArr[si - 1] != null && String(normArr[si - 1]) !== '' ? String(normArr[si - 1]) : STACK_BLOCK_DEFAULT_NORMAL[si - 1];
+ var hv = heavyArr[si - 1] != null && String(heavyArr[si - 1]) !== '' ? String(heavyArr[si - 1]) : STACK_BLOCK_DEFAULT_HEAVY[si - 1];
+ if (nin) nin.value = nv || '';
+ if (hin) hin.value = hv || '';
+ updateStackBlockSeatPreview(si);
+ }
+ }
+ /* #4 MG3 8 สีตาม lobby theme — ค่าเริ่มต้น Block-Code-1..8 (ตรง PLAY_LOBBY_THEME_HEX) */
+ var STACK_BLOCK_THEME_NAMES = ['1-red', '2-orange', '3-Yellow', '4-Green', '5-Blue', '6-Navy', '7-Violet', '8-Pink'];
+ var STACK_BLOCK_DEFAULT_NORMAL = STACK_BLOCK_THEME_NAMES.map(function (s) { return '/Game/img/TowerBlock/Block-Code-' + s + '.png'; });
+ var STACK_BLOCK_DEFAULT_HEAVY = STACK_BLOCK_THEME_NAMES.map(function (s) { return '/Game/img/TowerBlock/Block-Code-' + s + '-2.png'; });
+
+ function readStackBlockNormalUrlsFromForm() {
+ var out = [];
+ for (var i = 1; i <= 8; i++) {
+ var inp = el('stack-block-normal-url-' + i);
+ out.push(inp && inp.value ? String(inp.value).trim() : '');
+ }
+ return out;
+ }
+
+ function readStackBlockHeavyUrlsFromForm() {
+ var out = [];
+ for (var j = 1; j <= 8; j++) {
+ var inp = el('stack-block-heavy-url-' + j);
+ out.push(inp && inp.value ? String(inp.value).trim() : '');
+ }
+ return out;
+ }
+
+ function updateStackBlockSeatPreview(seat) {
+ ['normal', 'heavy'].forEach(function (kind) {
+ var inp = el('stack-block-' + kind + '-url-' + seat);
+ var img = el('stack-block-' + kind + '-prev-' + seat);
+ if (!img) return;
+ var v = inp && inp.value ? String(inp.value).trim() : '';
+ if (!v) {
+ img.removeAttribute('src');
+ img.alt = '';
+ return;
+ }
+ if (!/^https?:\/\//i.test(v) && v.charAt(0) !== '/') v = '/' + v.replace(/^\/+/, '');
+ img.alt = (kind === 'heavy' ? 'Heavy ' : 'Normal ') + 'P' + seat;
+ img.src = v;
+ });
+ }
+
+ function bindStackBlockVisualInputs() {
+ for (var s = 1; s <= 8; s++) {
+ (function (seat) {
+ var nIn = el('stack-block-normal-url-' + seat);
+ var hIn = el('stack-block-heavy-url-' + seat);
+ if (nIn) {
+ nIn.addEventListener('change', function () { updateStackBlockSeatPreview(seat); });
+ nIn.addEventListener('blur', function () { updateStackBlockSeatPreview(seat); });
+ }
+ if (hIn) {
+ hIn.addEventListener('change', function () { updateStackBlockSeatPreview(seat); });
+ hIn.addEventListener('blur', function () { updateStackBlockSeatPreview(seat); });
+ }
+ var bn = el('btn-stack-block-normal-clear-' + seat);
+ if (bn) {
+ bn.addEventListener('click', function () {
+ var i = el('stack-block-normal-url-' + seat);
+ if (i) i.value = '';
+ updateStackBlockSeatPreview(seat);
+ });
+ }
+ var bh = el('btn-stack-block-heavy-clear-' + seat);
+ if (bh) {
+ bh.addEventListener('click', function () {
+ var i = el('stack-block-heavy-url-' + seat);
+ if (i) i.value = '';
+ updateStackBlockSeatPreview(seat);
+ });
+ }
+ })(s);
+ }
+ }
+
+ function loadStackGamePanel() {
+ gameTimingFetch('GET')
+ .then(function (data) {
+ applyGameTimingDataToStackInputs(data);
+ setMsg('stack-game-msg', '', '');
+ })
+ .catch(function (e) {
+ setMsg('stack-game-msg', e.message || 'โหลดไม่ได้', 'error');
+ });
+ }
+
+ function saveStackGamePanel() {
+ if (stackGameSaveInFlight) return;
+ var hzParsed = parseStackSwingHzForSave(el('stack-swing-hz') ? el('stack-swing-hz').value : '');
+ if (!hzParsed.ok) {
+ setMsg('stack-game-msg', hzParsed.error, 'error');
+ return;
+ }
+ var hzIn = hzParsed.value;
+ var bwParsed = parseStackBlockWidthForSave(el('stack-block-width-tiles') ? el('stack-block-width-tiles').value : '');
+ if (!bwParsed.ok) {
+ setMsg('stack-game-msg', bwParsed.error, 'error');
+ return;
+ }
+ var blockW = bwParsed.value;
+ var secIn = el('stack-tower-mission-sec') ? parseInt(String(el('stack-tower-mission-sec').value), 10) : 90;
+ if (!Number.isFinite(secIn) || secIn < 10 || secIn > 7200) {
+ setMsg('stack-game-msg', 'เวลารอบต้องอยู่ระหว่าง 10–7200 วินาที', 'error');
+ return;
+ }
+ var missIn = el('stack-team-misses-max') ? parseInt(String(el('stack-team-misses-max').value), 10) : 3;
+ if (!Number.isFinite(missIn) || missIn < 1 || missIn > 20) {
+ setMsg('stack-game-msg', 'โอกาสพลาดต้องอยู่ระหว่าง 1–20 ครั้ง', 'error');
+ return;
+ }
+ var progBlocksIn = el('stack-tower-progress-blocks') ? parseInt(String(el('stack-tower-progress-blocks').value), 10) : 50;
+ if (!Number.isFinite(progBlocksIn) || progBlocksIn < 1 || progBlocksIn > 500) {
+ setMsg('stack-game-msg', 'Progress เต็ม 100%: จำนวนชั้นต้องอยู่ระหว่าง 1–500', 'error');
+ return;
+ }
+ var heavyPctIn = el('stack-heavy-block-percent') ? parseInt(String(el('stack-heavy-block-percent').value), 10) : 35;
+ if (!Number.isFinite(heavyPctIn) || heavyPctIn < 0 || heavyPctIn > 100) {
+ setMsg('stack-game-msg', 'โอกาสบล็อกใหญ่ (%) ต้องอยู่ระหว่าง 0–100', 'error');
+ return;
+ }
+ var btn = el('btn-stack-game-save');
+ stackGameSaveInFlight = true;
+ if (btn) {
+ btn.disabled = true;
+ if (!btn.dataset.prevLabel) btn.dataset.prevLabel = btn.textContent;
+ btn.textContent = 'กำลังบันทึก…';
+ }
+ gameTimingFetch('GET')
+ .then(function (data) {
+ data.stackSwingHz = hzIn;
+ data.stackBlockWidthTiles = blockW;
+ data.stackTowerMissionTimeSec = secIn;
+ data.stackTeamMissesMax = missIn;
+ data.stackTowerProgressBlocks = progBlocksIn;
+ data.stackHeavyBlockPercent = heavyPctIn;
+ data.stackBlockNormalImageUrls = readStackBlockNormalUrlsFromForm();
+ data.stackBlockHeavyImageUrls = readStackBlockHeavyUrlsFromForm();
+ return gameTimingFetch('PUT', data);
+ })
+ .then(function () {
+ return gameTimingFetch('GET');
+ })
+ .then(function (fresh) {
+ applyGameTimingDataToStackInputs(fresh);
+ var hz = fresh && fresh.stackSwingHz != null ? Number(fresh.stackSwingHz) : hzIn;
+ if (!Number.isFinite(hz)) hz = hzIn;
+ hz = Math.round(hz * 100) / 100;
+ var wLabel = fresh && fresh.stackBlockWidthTiles != null && Number.isFinite(Number(fresh.stackBlockWidthTiles))
+ ? String(Math.round(Number(fresh.stackBlockWidthTiles) * 100) / 100) + ' tile'
+ : 'อัตโนมัติ (จากแผนที่)';
+ var stf = fresh && fresh.stackTowerMissionTimeSec != null ? Number(fresh.stackTowerMissionTimeSec) : secIn;
+ var msf = fresh && fresh.stackTeamMissesMax != null ? Number(fresh.stackTeamMissesMax) : missIn;
+ var pbf = fresh && fresh.stackTowerProgressBlocks != null ? Number(fresh.stackTowerProgressBlocks) : progBlocksIn;
+ setMsg('stack-game-msg', 'บันทึกสำเร็จ — สวิง ' + hz + ' Hz · กว้าง ' + wLabel + ' · เวลา ' + stf + 's · พลาด ' + msf + ' ครั้ง · Progress 100% ที่ ' + pbf + ' ชั้น · รูปบล็อก P1–P6 + ใหญ่ ' + (fresh && fresh.stackHeavyBlockPercent != null ? fresh.stackHeavyBlockPercent : heavyPctIn) + '% · โหลดยืนยันจากเซิร์ฟเวอร์แล้ว · ผู้เล่นต้องรีเฟรชหน้าเล่นหรือเข้าห้องใหม่', 'ok');
+ })
+ .catch(function (e) {
+ setMsg('stack-game-msg', (e.message || 'บันทึกไม่ได้') + ' — เช็คว่า Node รัน Game/server.js และ URL /Game/api/game-timing ไม่ 404', 'error');
+ })
+ .then(function () {
+ stackGameSaveInFlight = false;
+ if (btn) {
+ btn.disabled = false;
+ btn.textContent = btn.dataset.prevLabel || 'บันทึก';
+ }
+ });
+ }
+
+ function loadGameTimingPanel() {
+ gameTimingFetch('GET')
+ .then(function (data) {
+ el('game-timing-tick-ms').value = String(data.gauntletTickMs != null ? data.gauntletTickMs : 220);
+ el('game-timing-jump-ticks').value = String(data.gauntletJumpTicks != null ? data.gauntletJumpTicks : 16);
+ if (el('game-timing-jump-height')) el('game-timing-jump-height').value = String(data.gauntletJumpHeightMult != null ? data.gauntletJumpHeightMult : 0.52);
+ el('game-timing-limit-sec').value = String(data.gauntletTimeLimitSec != null ? data.gauntletTimeLimitSec : 0);
+ /* troublesomeOfferSec/RollMaxSec ย้ายไปแท็บ "บทตัวป่วน" แล้ว */
+ if (el('game-timing-lane-urls')) el('game-timing-lane-urls').value = laneUrlsToText(data.gauntletLaneImageUrls);
+ if (el('game-timing-laser-top-url')) el('game-timing-laser-top-url').value = data.gauntletLaserTopUrl != null ? String(data.gauntletLaserTopUrl) : '';
+ if (el('game-timing-laser-bottom-url')) el('game-timing-laser-bottom-url').value = data.gauntletLaserBottomUrl != null ? String(data.gauntletLaserBottomUrl) : '';
+ if (el('game-timing-laser-line-url')) el('game-timing-laser-line-url').value = data.gauntletLaserLineUrl != null ? String(data.gauntletLaserLineUrl) : '';
+ if (el('game-timing-laser-fill')) el('game-timing-laser-fill').value = data.gauntletLaserFillColor != null ? String(data.gauntletLaserFillColor) : '';
+ if (el('game-timing-laser-stroke')) el('game-timing-laser-stroke').value = data.gauntletLaserStrokeColor != null ? String(data.gauntletLaserStrokeColor) : '';
+ var lw = data.gauntletLaserLineWidthPx != null ? Number(data.gauntletLaserLineWidthPx) : 2;
+ if (el('game-timing-laser-line-width')) el('game-timing-laser-line-width').value = String(Number.isFinite(lw) ? lw : 2);
+ renderGameTimingLaneVisuals();
+ renderGameTimingLaserVisuals();
+ syncGameTimingLaserColorSwatches();
+ applyGameTimingDataToStackInputs(data);
+ setMsg('game-timing-msg', '', '');
+ return refreshGauntletAssetLibrary();
+ })
+ .catch(function (e) {
+ setMsg('game-timing-msg', e.message || 'โหลดไม่ได้', 'error');
+ });
+ }
+
+ function normalizeJumpSurvivePlatformTileUrlInput(v) {
+ var s = String(v || '').trim().slice(0, 500);
+ if (!s) return '';
+ if (!/^https?:\/\//i.test(s) && s.charAt(0) !== '/') s = '/' + s.replace(/^\/+/, '');
+ return s;
+ }
+
+ function updateJumpSurvivePlatformTilePreview(slot) {
+ var img = el('jump-survive-platform-tile-prev-' + slot);
+ var inp = el('jump-survive-platform-tile-url-' + slot);
+ if (!img) return;
+ var v = inp && inp.value ? normalizeJumpSurvivePlatformTileUrlInput(inp.value) : '';
+ if (!v) {
+ img.removeAttribute('src');
+ img.alt = '';
+ return;
+ }
+ img.alt = 'Jump platform tile ' + slot + ' preview';
+ img.src = v;
+ }
+
+ function readJumpSurvivePlatformTilesFromForm() {
+ var out = [];
+ for (var s = 1; s <= 3; s++) {
+ var inpU = el('jump-survive-platform-tile-url-' + s);
+ var url = inpU ? normalizeJumpSurvivePlatformTileUrlInput(inpU.value) : '';
+ var inpW = el('jump-survive-platform-tile-w-' + s);
+ var inpH = el('jump-survive-platform-tile-h-' + s);
+ var w = inpW ? parseInt(String(inpW.value), 10) : 0;
+ var h = inpH ? parseInt(String(inpH.value), 10) : 0;
+ if (!Number.isFinite(w) || w < 0) w = 0;
+ if (!Number.isFinite(h) || h < 0) h = 0;
+ out.push({ url: url, w: w, h: h });
+ }
+ return out;
+ }
+
+ function bindJumpSurvivePlatformTileInputs() {
+ for (var s = 1; s <= 3; s++) {
+ (function (slot) {
+ var inp = el('jump-survive-platform-tile-url-' + slot);
+ var btn = el('btn-jump-survive-platform-tile-clear-' + slot);
+ if (inp) {
+ inp.addEventListener('change', function () { updateJumpSurvivePlatformTilePreview(slot); });
+ inp.addEventListener('blur', function () { updateJumpSurvivePlatformTilePreview(slot); });
+ }
+ if (btn) {
+ btn.addEventListener('click', function () {
+ var i = el('jump-survive-platform-tile-url-' + slot);
+ if (i) i.value = '';
+ updateJumpSurvivePlatformTilePreview(slot);
+ });
+ }
+ })(s);
+ }
+ }
+
+ function loadJumpSurviveTimingPanel() {
+ gameTimingFetch('GET')
+ .then(function (data) {
+ var inp = el('jump-survive-height-mult');
+ if (inp) {
+ var jsm = Number(data.jumpSurviveJumpHeightMult);
+ inp.value = String(Number.isFinite(jsm) ? Math.round(Math.max(0.5, Math.min(4, jsm)) * 100) / 100 : 1.5);
+ }
+ var inpT = el('jump-survive-mission-sec');
+ if (inpT) {
+ var jss = Number(data.jumpSurviveMissionTimeSec);
+ inpT.value = String(Number.isFinite(jss) && jss > 0 ? Math.floor(jss) : 0);
+ }
+ var tiles = Array.isArray(data.jumpSurvivePlatformTiles) ? data.jumpSurvivePlatformTiles : [];
+ var legacy = typeof data.jumpSurvivePlatformTileUrl === 'string' ? data.jumpSurvivePlatformTileUrl : '';
+ for (var s = 1; s <= 3; s++) {
+ var o = tiles[s - 1] && typeof tiles[s - 1] === 'object' ? tiles[s - 1] : {};
+ var url = typeof o.url === 'string' ? o.url : '';
+ if (s === 1 && !url && legacy) url = legacy;
+ var inpU = el('jump-survive-platform-tile-url-' + s);
+ if (inpU) inpU.value = url;
+ var w = Number(o.w);
+ var h = Number(o.h);
+ var inpW = el('jump-survive-platform-tile-w-' + s);
+ var inpH = el('jump-survive-platform-tile-h-' + s);
+ if (inpW) inpW.value = String(Number.isFinite(w) && w > 0 ? Math.floor(w) : 0);
+ if (inpH) inpH.value = String(Number.isFinite(h) && h > 0 ? Math.floor(h) : 0);
+ updateJumpSurvivePlatformTilePreview(s);
+ }
+ setMsg('jump-survive-timing-msg', '', '');
+ })
+ .catch(function (e) {
+ setMsg('jump-survive-timing-msg', e.message || 'โหลดไม่ได้', 'error');
+ });
+ }
+
+ function updateSpaceShooterShipPreview(slot) {
+ var img = el('space-shooter-ship-prev-' + slot);
+ var inp = el('space-shooter-ship-url-' + slot);
+ if (!img) return;
+ var v = inp && inp.value ? String(inp.value).trim() : '';
+ if (!v) {
+ img.removeAttribute('src');
+ img.alt = '';
+ return;
+ }
+ if (!/^https?:\/\//i.test(v) && v.charAt(0) !== '/') v = '/' + v.replace(/^\/+/, '');
+ img.alt = 'Ship slot ' + slot;
+ img.src = v;
+ }
+
+ function bindSpaceShooterShipUrlInputs() {
+ for (var s = 1; s <= 6; s++) {
+ (function (slot) {
+ var inp = el('space-shooter-ship-url-' + slot);
+ var btn = el('btn-space-shooter-ship-clear-' + slot);
+ if (inp) {
+ inp.addEventListener('change', function () { updateSpaceShooterShipPreview(slot); });
+ inp.addEventListener('blur', function () { updateSpaceShooterShipPreview(slot); });
+ }
+ if (btn) {
+ btn.addEventListener('click', function () {
+ var i = el('space-shooter-ship-url-' + slot);
+ if (i) i.value = '';
+ updateSpaceShooterShipPreview(slot);
+ });
+ }
+ })(s);
+ }
+ }
+
+ function readSpaceShooterShipImageUrlsFromForm() {
+ var urls = [];
+ for (var i = 1; i <= 6; i++) {
+ var inp = el('space-shooter-ship-url-' + i);
+ urls.push(inp && inp.value ? String(inp.value).trim() : '');
+ }
+ return urls;
+ }
+
+ function updateSpaceShooterDamageOverlayPreview(hit) {
+ var img = el('space-shooter-damage-prev-' + hit);
+ var inp = el('space-shooter-damage-url-' + hit);
+ if (!img) return;
+ var v = inp && inp.value ? String(inp.value).trim() : '';
+ if (!v) {
+ img.removeAttribute('src');
+ img.alt = '';
+ return;
+ }
+ if (!/^https?:\/\//i.test(v) && v.charAt(0) !== '/') v = '/' + v.replace(/^\/+/, '');
+ img.alt = 'Damage overlay hit ' + hit;
+ img.src = v;
+ }
+
+ function readSpaceShooterShipDamageOverlayUrlsFromForm() {
+ var urls = [];
+ for (var h = 1; h <= 3; h++) {
+ var inp = el('space-shooter-damage-url-' + h);
+ urls.push(inp && inp.value ? String(inp.value).trim() : '');
+ }
+ return urls;
+ }
+
+ function bindSpaceShooterDamageOverlayPanel() {
+ for (var h = 1; h <= 3; h++) {
+ (function (hit) {
+ var inp = el('space-shooter-damage-url-' + hit);
+ var btnClr = el('btn-space-shooter-damage-clear-' + hit);
+ var btnUp = el('btn-space-shooter-damage-upload-' + hit);
+ var fileInp = el('space-shooter-damage-file-' + hit);
+ if (inp) {
+ inp.addEventListener('change', function () { updateSpaceShooterDamageOverlayPreview(hit); });
+ inp.addEventListener('blur', function () { updateSpaceShooterDamageOverlayPreview(hit); });
+ }
+ if (btnClr) {
+ btnClr.addEventListener('click', function () {
+ var i = el('space-shooter-damage-url-' + hit);
+ if (i) i.value = '';
+ if (fileInp) fileInp.value = '';
+ updateSpaceShooterDamageOverlayPreview(hit);
+ });
+ }
+ if (btnUp && fileInp) {
+ btnUp.addEventListener('click', function () {
+ if (!fileInp.files || !fileInp.files[0]) {
+ setMsg('space-shooter-timing-msg', 'เลือกไฟล์รูปก่อน · Pick an image file first', 'error');
+ return;
+ }
+ readFileAsDataURL(fileInp.files[0])
+ .then(function (dataUrl) {
+ return gauntletAssetsJsonFetch(GAME_GAUNTLET_ASSETS_API + '/upload', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ imageDataUrl: dataUrl,
+ label: 'space-shooter-damage-hit-' + hit,
+ }),
+ });
+ })
+ .then(function (j) {
+ if (!j || !j.ok || !j.url) {
+ throw new Error((j && j.error) || 'อัปโหลดไม่สำเร็จ · Upload failed');
+ }
+ var inpU = el('space-shooter-damage-url-' + hit);
+ if (inpU) inpU.value = String(j.url).trim();
+ updateSpaceShooterDamageOverlayPreview(hit);
+ setMsg(
+ 'space-shooter-timing-msg',
+ 'อัปโหลด overlay ช่อง ' + hit + ' แล้ว — กดบันทึกด้านล่างเพื่อเก็บลง game-timing.json · Uploaded; click Save',
+ 'ok'
+ );
+ })
+ .catch(function (e) {
+ setMsg('space-shooter-timing-msg', e.message || 'อัปโหลดไม่สำเร็จ', 'error');
+ });
+ });
+ }
+ })(h);
+ }
+ }
+
+ function updateSpaceShooterAstFallPreview() {
+ var img = el('space-shooter-ast-fall-prev');
+ var inp = el('space-shooter-ast-fall-url');
+ if (!img) return;
+ var v = inp && inp.value ? String(inp.value).trim() : '';
+ if (!v) {
+ img.removeAttribute('src');
+ img.alt = '';
+ return;
+ }
+ if (!/^https?:\/\//i.test(v) && v.charAt(0) !== '/') v = '/' + v.replace(/^\/+/, '');
+ img.alt = 'Asteroid falling';
+ img.src = v;
+ }
+
+ function updateSpaceShooterAstExplodeRowPreview(row) {
+ if (!row) return;
+ var img = row.querySelector('.space-shooter-ast-explode-prev');
+ var inp = row.querySelector('.space-shooter-ast-explode-url');
+ if (!img) return;
+ var v = inp && inp.value ? String(inp.value).trim() : '';
+ if (!v) {
+ img.removeAttribute('src');
+ img.alt = '';
+ return;
+ }
+ if (!/^https?:\/\//i.test(v) && v.charAt(0) !== '/') v = '/' + v.replace(/^\/+/, '');
+ img.alt = 'Asteroid explode frame';
+ img.src = v;
+ }
+
+ function renumberSpaceShooterAstExplodeRows() {
+ var wrap = el('space-shooter-ast-explode-rows');
+ if (!wrap) return;
+ var rows = wrap.querySelectorAll('.space-shooter-ast-explode-row');
+ rows.forEach(function (row, i) {
+ var slot = row.querySelector('.space-shooter-ship-slot');
+ if (slot) slot.textContent = String(i + 1);
+ });
+ }
+
+ function addSpaceShooterAstExplodeRow(initialUrl) {
+ var wrap = el('space-shooter-ast-explode-rows');
+ if (!wrap) return;
+ var rows = wrap.querySelectorAll('.space-shooter-ast-explode-row');
+ if (rows.length >= 31) {
+ setMsg('space-shooter-timing-msg', 'เฟรมแตกสูงสุด 31 แถว (รวมรูปตกแล้วไม่เกิน 32 URL) · Max 31 explosion rows', 'error');
+ return;
+ }
+ var ix = rows.length + 1;
+ var row = document.createElement('div');
+ row.className = 'space-shooter-ship-row space-shooter-ast-explode-row';
+ row.innerHTML =
+ '' +
+ ix +
+ '' +
+ '' +
+ '' +
+ '
' +
+ '' +
+ '';
+ var inp = row.querySelector('.space-shooter-ast-explode-url');
+ if (inp && initialUrl != null && String(initialUrl).trim()) inp.value = String(initialUrl).trim();
+ if (inp) {
+ inp.addEventListener('change', function () {
+ updateSpaceShooterAstExplodeRowPreview(row);
+ });
+ inp.addEventListener('blur', function () {
+ updateSpaceShooterAstExplodeRowPreview(row);
+ });
+ }
+ wrap.appendChild(row);
+ updateSpaceShooterAstExplodeRowPreview(row);
+ }
+
+ function clearSpaceShooterAstExplodeRows() {
+ var wrap = el('space-shooter-ast-explode-rows');
+ if (wrap) wrap.innerHTML = '';
+ }
+
+ function readSpaceShooterAsteroidSpriteUrlsFromForm() {
+ var out = [];
+ var fallInp = el('space-shooter-ast-fall-url');
+ var fall = fallInp && fallInp.value ? String(fallInp.value).trim() : '';
+ if (fall) out.push(fall);
+ var wrap = el('space-shooter-ast-explode-rows');
+ if (wrap) {
+ var rows = wrap.querySelectorAll('.space-shooter-ast-explode-row');
+ for (var ri = 0; ri < rows.length && out.length < 32; ri++) {
+ var inp = rows[ri].querySelector('.space-shooter-ast-explode-url');
+ var u = inp && inp.value ? String(inp.value).trim() : '';
+ if (u) out.push(u);
+ }
+ }
+ return out;
+ }
+
+ function bindSpaceShooterAsteroidSpritePanel() {
+ var wrap = el('space-shooter-ast-explode-rows');
+ if (!wrap || wrap.getAttribute('data-bound') === '1') return;
+ wrap.setAttribute('data-bound', '1');
+ wrap.addEventListener('click', function (ev) {
+ var t = ev.target;
+ if (!t || !t.closest) return;
+ var rm = t.closest('.btn-space-shooter-ast-explode-remove');
+ if (rm) {
+ var row = rm.closest('.space-shooter-ast-explode-row');
+ if (row && row.parentNode) row.parentNode.removeChild(row);
+ renumberSpaceShooterAstExplodeRows();
+ return;
+ }
+ var clr = t.closest('.btn-space-shooter-ast-explode-clear');
+ if (clr) {
+ var rowC = clr.closest('.space-shooter-ast-explode-row');
+ if (!rowC) return;
+ var iu = rowC.querySelector('.space-shooter-ast-explode-url');
+ var fi = rowC.querySelector('.space-shooter-ast-explode-file');
+ if (iu) iu.value = '';
+ if (fi) fi.value = '';
+ updateSpaceShooterAstExplodeRowPreview(rowC);
+ return;
+ }
+ var up = t.closest('.btn-space-shooter-ast-explode-upload');
+ if (!up) return;
+ var rowU = up.closest('.space-shooter-ast-explode-row');
+ if (!rowU) return;
+ var fileInp = rowU.querySelector('.space-shooter-ast-explode-file');
+ if (!fileInp || !fileInp.files || !fileInp.files[0]) {
+ setMsg('space-shooter-timing-msg', 'เลือกไฟล์รูปก่อน · Pick an image file first', 'error');
+ return;
+ }
+ readFileAsDataURL(fileInp.files[0])
+ .then(function (dataUrl) {
+ return gauntletAssetsJsonFetch(GAME_GAUNTLET_ASSETS_API + '/upload', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ imageDataUrl: dataUrl,
+ label: 'space-shooter-ast-explode',
+ }),
+ });
+ })
+ .then(function (j) {
+ if (!j || !j.ok || !j.url) {
+ throw new Error((j && j.error) || 'อัปโหลดไม่สำเร็จ · Upload failed');
+ }
+ var inpU = rowU.querySelector('.space-shooter-ast-explode-url');
+ if (inpU) inpU.value = String(j.url).trim();
+ updateSpaceShooterAstExplodeRowPreview(rowU);
+ setMsg(
+ 'space-shooter-timing-msg',
+ 'อัปโหลดเฟรมแตกแล้ว — กดบันทึกด้านล่างเพื่อเก็บ · Uploaded; click Save',
+ 'ok'
+ );
+ })
+ .catch(function (e) {
+ setMsg('space-shooter-timing-msg', e.message || 'อัปโหลดไม่สำเร็จ', 'error');
+ });
+ });
+ var btnAdd = el('btn-space-shooter-ast-add-explode');
+ if (btnAdd) {
+ btnAdd.addEventListener('click', function () {
+ setMsg('space-shooter-timing-msg', '', '');
+ addSpaceShooterAstExplodeRow('');
+ });
+ }
+ var fallInp = el('space-shooter-ast-fall-url');
+ var fallFile = el('space-shooter-ast-fall-file');
+ var fallUp = el('btn-space-shooter-ast-fall-upload');
+ var fallClr = el('btn-space-shooter-ast-fall-clear');
+ if (fallInp) {
+ fallInp.addEventListener('change', updateSpaceShooterAstFallPreview);
+ fallInp.addEventListener('blur', updateSpaceShooterAstFallPreview);
+ }
+ if (fallClr) {
+ fallClr.addEventListener('click', function () {
+ if (fallInp) fallInp.value = '';
+ if (fallFile) fallFile.value = '';
+ updateSpaceShooterAstFallPreview();
+ });
+ }
+ if (fallUp && fallFile) {
+ fallUp.addEventListener('click', function () {
+ if (!fallFile.files || !fallFile.files[0]) {
+ setMsg('space-shooter-timing-msg', 'เลือกไฟล์รูปก่อน · Pick an image file first', 'error');
+ return;
+ }
+ readFileAsDataURL(fallFile.files[0])
+ .then(function (dataUrl) {
+ return gauntletAssetsJsonFetch(GAME_GAUNTLET_ASSETS_API + '/upload', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ imageDataUrl: dataUrl,
+ label: 'space-shooter-ast-fall',
+ }),
+ });
+ })
+ .then(function (j) {
+ if (!j || !j.ok || !j.url) {
+ throw new Error((j && j.error) || 'อัปโหลดไม่สำเร็จ · Upload failed');
+ }
+ if (fallInp) fallInp.value = String(j.url).trim();
+ updateSpaceShooterAstFallPreview();
+ setMsg(
+ 'space-shooter-timing-msg',
+ 'อัปโหลดรูปตกแล้ว — กดบันทึกด้านล่างเพื่อเก็บ · Uploaded; click Save',
+ 'ok'
+ );
+ })
+ .catch(function (e) {
+ setMsg('space-shooter-timing-msg', e.message || 'อัปโหลดไม่สำเร็จ', 'error');
+ });
+ });
+ }
+ }
+
+ function loadSpaceShooterTimingPanel() {
+ gameTimingFetch('GET')
+ .then(function (data) {
+ var inpT = el('space-shooter-mission-sec');
+ if (inpT) {
+ var ss = Number(data.spaceShooterMissionTimeSec);
+ inpT.value = String(Number.isFinite(ss) && ss > 0 ? Math.floor(ss) : 0);
+ }
+ var inpIv = el('space-shooter-asteroid-interval-ms');
+ if (inpIv) {
+ var iv = Number(data.spaceShooterAsteroidIntervalMs);
+ inpIv.value = String(Number.isFinite(iv) && iv >= 200 ? Math.min(10000, Math.floor(iv)) : 1040);
+ }
+ var urls = data.spaceShooterShipImageUrls;
+ if (!Array.isArray(urls)) urls = [];
+ for (var k = 1; k <= 6; k++) {
+ var inpU = el('space-shooter-ship-url-' + k);
+ if (inpU) inpU.value = urls[k - 1] != null ? String(urls[k - 1]) : '';
+ updateSpaceShooterShipPreview(k);
+ }
+ var fallInpL = el('space-shooter-ast-fall-url');
+ if (fallInpL) {
+ var astArr = data.spaceShooterAsteroidSpriteUrls;
+ if (!Array.isArray(astArr)) astArr = [];
+ fallInpL.value = astArr[0] != null ? String(astArr[0]) : '';
+ updateSpaceShooterAstFallPreview();
+ clearSpaceShooterAstExplodeRows();
+ for (var ai = 1; ai < astArr.length && ai < 32; ai++) {
+ addSpaceShooterAstExplodeRow(astArr[ai]);
+ }
+ }
+ var inpFms = el('space-shooter-asteroid-explode-ms');
+ if (inpFms) {
+ var ef = Number(data.spaceShooterAsteroidExplodeFrameMs);
+ inpFms.value = String(Number.isFinite(ef) ? Math.max(30, Math.min(500, Math.round(ef))) : 70);
+ }
+ var dmg = data.spaceShooterShipDamageOverlayUrls;
+ if (!Array.isArray(dmg)) dmg = [];
+ for (var dh = 1; dh <= 3; dh++) {
+ var inpD = el('space-shooter-damage-url-' + dh);
+ if (inpD) inpD.value = dmg[dh - 1] != null ? String(dmg[dh - 1]) : '';
+ updateSpaceShooterDamageOverlayPreview(dh);
+ }
+ setMsg('space-shooter-timing-msg', '', '');
+ })
+ .catch(function (e) {
+ setMsg('space-shooter-timing-msg', e.message || 'โหลดไม่ได้', 'error');
+ });
+ }
+
+ function saveSpaceShooterTimingPanel() {
+ var limSs = el('space-shooter-mission-sec') ? parseInt(String(el('space-shooter-mission-sec').value), 10) : 0;
+ if (Number.isNaN(limSs) || limSs < 0) limSs = 0;
+ limSs = limSs <= 0 ? 0 : Math.max(10, Math.min(7200, limSs));
+ var shipUrls = readSpaceShooterShipImageUrlsFromForm();
+ var damageUrls = readSpaceShooterShipDamageOverlayUrlsFromForm();
+ var astUrls = readSpaceShooterAsteroidSpriteUrlsFromForm();
+ var inpFms = el('space-shooter-asteroid-explode-ms');
+ var explodeMs = inpFms ? parseInt(String(inpFms.value), 10) : 70;
+ if (Number.isNaN(explodeMs)) explodeMs = 70;
+ explodeMs = Math.max(30, Math.min(500, explodeMs));
+ var inpIv = el('space-shooter-asteroid-interval-ms');
+ var asteroidIv = inpIv ? parseInt(String(inpIv.value), 10) : 1040;
+ if (Number.isNaN(asteroidIv)) asteroidIv = 1040;
+ asteroidIv = Math.max(200, Math.min(10000, asteroidIv));
+ gameTimingFetch('GET')
+ .then(function (data) {
+ data.spaceShooterMissionTimeSec = limSs;
+ data.spaceShooterShipImageUrls = shipUrls;
+ data.spaceShooterShipDamageOverlayUrls = damageUrls;
+ data.spaceShooterAsteroidSpriteUrls = astUrls;
+ data.spaceShooterAsteroidExplodeFrameMs = explodeMs;
+ data.spaceShooterAsteroidIntervalMs = asteroidIv;
+ return gameTimingFetch('PUT', data);
+ })
+ .then(function () {
+ setMsg('space-shooter-timing-msg', 'บันทึกแล้ว — เวลา / ความถี่เกิดอุกาบาต / รูปยาน / overlay / อุกาบาต มีผลเมื่อรีเฟรชหน้าเล่นหรือโหลด /api/game-timing', 'ok');
+ })
+ .catch(function (e) {
+ setMsg('space-shooter-timing-msg', e.message || 'บันทึกไม่ได้', 'error');
+ });
+ }
+
+ function encodeSpacesInUrlPath(t) {
+ var s = String(t || '');
+ var q = s.indexOf('?');
+ var pathPart = q === -1 ? s : s.slice(0, q);
+ var query = q === -1 ? '' : s.slice(q);
+ return pathPart.replace(/ /g, '%20') + query;
+ }
+
+ function decodeAssetUrlPercentRuns(t) {
+ var s = String(t || '');
+ var q = s.indexOf('?');
+ var base = q === -1 ? s : s.slice(0, q);
+ var query = q === -1 ? '' : s.slice(q);
+ for (var i = 0; i < 2; i++) {
+ try {
+ var d = decodeURIComponent(base);
+ if (d === base) break;
+ base = d;
+ } catch (e) {
+ break;
+ }
+ }
+ return base + query;
+ }
+
+ function normalizeGameAssetUrlForWebAdmin(t) {
+ var s = String(t || '');
+ s = s.replace(/^\/Game\/public\/img\//i, '/Game/img/');
+ s = s.replace(/^Game\/public\/img\//i, 'Game/img/');
+ return s;
+ }
+
+ /** ให้สอดคล้องกับ server sanitizeGauntletAssetUrl + กัน placeholder .... */
+ function normalizeMegaVirusAssetUrl(v) {
+ var t = v != null ? normalizeGameAssetUrlForWebAdmin(decodeAssetUrlPercentRuns(String(v).trim())).replace(/\/+$/, '') : '';
+ if (/^\/Game\/img\/MegaVirus\/Artboard$/i.test(t) || /^Game\/img\/MegaVirus\/Artboard$/i.test(t)) {
+ t = '/Game/img/MegaVirus/Artboard%209.png';
+ }
+ if (!t || t.length > 500) return '';
+ if (/\.{4,}/.test(t)) return '';
+ if (/[\t\n\r<>"'`]/.test(t)) return '';
+ if (/^https?:\/\//i.test(t)) return encodeSpacesInUrlPath(t);
+ if (/^Game\//i.test(t)) return encodeSpacesInUrlPath('/' + t.replace(/^\/+/, ''));
+ if (t.charAt(0) === '/') return encodeSpacesInUrlPath(t);
+ return '';
+ }
+
+ function updateMegaVirusBossPreview() {
+ var img = el('mega-virus-boss-prev');
+ var inp = el('mega-virus-boss-url');
+ if (!img) return;
+ var v = normalizeMegaVirusAssetUrl(inp && inp.value ? String(inp.value).trim() : '');
+ if (!v) {
+ img.removeAttribute('src');
+ img.alt = '';
+ return;
+ }
+ img.alt = 'Boss preview';
+ img.src = v;
+ }
+
+ function updateMegaVirusBalloonPreview(slot) {
+ var img = el('mega-virus-balloon-prev-' + slot);
+ var inp = el('mega-virus-balloon-url-' + slot);
+ if (!img) return;
+ var v = normalizeMegaVirusAssetUrl(inp && inp.value ? String(inp.value).trim() : '');
+ if (!v) {
+ img.removeAttribute('src');
+ img.alt = '';
+ return;
+ }
+ img.alt = 'Balloon P' + slot;
+ img.src = v;
+ }
+
+ function updateMegaVirusBalloonFallbackPreview() {
+ var img = el('mega-virus-balloon-fallback-prev');
+ var inp = el('mega-virus-balloon-fallback-url');
+ if (!img) return;
+ var v = normalizeMegaVirusAssetUrl(inp && inp.value ? String(inp.value).trim() : '');
+ if (!v) {
+ img.removeAttribute('src');
+ img.alt = '';
+ return;
+ }
+ img.alt = 'Fallback balloon';
+ img.src = v;
+ }
+
+ function readMegaVirusBalloonUrlsFromForm() {
+ var urls = [];
+ for (var i = 1; i <= 6; i++) {
+ var inp = el('mega-virus-balloon-url-' + i);
+ urls.push(normalizeMegaVirusAssetUrl(inp && inp.value ? String(inp.value).trim() : ''));
+ }
+ return urls;
+ }
+
+ function bindMegaVirusPanel() {
+ var formMv = el('form-mega-virus-timing');
+ if (formMv) {
+ formMv.addEventListener('submit', function (e) {
+ e.preventDefault();
+ saveMegaVirusTimingPanel();
+ });
+ }
+ var bossInp = el('mega-virus-boss-url');
+ if (bossInp) {
+ bossInp.addEventListener('change', updateMegaVirusBossPreview);
+ bossInp.addEventListener('blur', updateMegaVirusBossPreview);
+ }
+ var bossClr = el('btn-mega-virus-boss-clear');
+ if (bossClr) {
+ bossClr.addEventListener('click', function () {
+ var i = el('mega-virus-boss-url');
+ if (i) i.value = '';
+ updateMegaVirusBossPreview();
+ });
+ }
+ for (var s = 1; s <= 6; s++) {
+ (function (slot) {
+ var inp = el('mega-virus-balloon-url-' + slot);
+ var btn = el('btn-mega-virus-balloon-clear-' + slot);
+ if (inp) {
+ inp.addEventListener('change', function () { updateMegaVirusBalloonPreview(slot); });
+ inp.addEventListener('blur', function () { updateMegaVirusBalloonPreview(slot); });
+ }
+ if (btn) {
+ btn.addEventListener('click', function () {
+ var i = el('mega-virus-balloon-url-' + slot);
+ if (i) i.value = '';
+ updateMegaVirusBalloonPreview(slot);
+ });
+ }
+ })(s);
+ }
+ var fbInp = el('mega-virus-balloon-fallback-url');
+ if (fbInp) {
+ fbInp.addEventListener('change', updateMegaVirusBalloonFallbackPreview);
+ fbInp.addEventListener('blur', updateMegaVirusBalloonFallbackPreview);
+ }
+ var fbClr = el('btn-mega-virus-balloon-fallback-clear');
+ if (fbClr) {
+ fbClr.addEventListener('click', function () {
+ var i = el('mega-virus-balloon-fallback-url');
+ if (i) i.value = '';
+ updateMegaVirusBalloonFallbackPreview();
+ });
+ }
+ }
+
+ function applyMegaVirusPanelFromTimingData(data) {
+ if (!data || typeof data !== 'object') return;
+ var inpT = el('mega-virus-mission-sec');
+ if (inpT && Object.prototype.hasOwnProperty.call(data, 'balloonBossMissionTimeSec')) {
+ var bb = Number(data.balloonBossMissionTimeSec);
+ inpT.value = String(Number.isFinite(bb) && bb > 0 ? Math.floor(bb) : 0);
+ }
+ var inpBp = el('mega-virus-balloons-per-player');
+ if (inpBp && Object.prototype.hasOwnProperty.call(data, 'balloonBossBalloonsPerPlayer')) {
+ var bp = Number(data.balloonBossBalloonsPerPlayer);
+ inpBp.value = String(Number.isFinite(bp) && bp > 0 ? Math.min(12, Math.floor(bp)) : 0);
+ }
+ var bossInp = el('mega-virus-boss-url');
+ if (bossInp && Object.prototype.hasOwnProperty.call(data, 'balloonBossBossImageUrl')) {
+ bossInp.value = data.balloonBossBossImageUrl != null ? String(data.balloonBossBossImageUrl) : '';
+ updateMegaVirusBossPreview();
+ }
+ if (Array.isArray(data.balloonBossPlayerBalloonImageUrls)) {
+ var urls = data.balloonBossPlayerBalloonImageUrls;
+ for (var k = 1; k <= 6; k++) {
+ var inpU = el('mega-virus-balloon-url-' + k);
+ if (inpU) inpU.value = urls[k - 1] != null ? String(urls[k - 1]) : '';
+ updateMegaVirusBalloonPreview(k);
+ }
+ }
+ var fb = el('mega-virus-balloon-fallback-url');
+ if (fb && Object.prototype.hasOwnProperty.call(data, 'balloonBossPlayerBalloonFallbackUrl')) {
+ fb.value = data.balloonBossPlayerBalloonFallbackUrl != null ? String(data.balloonBossPlayerBalloonFallbackUrl) : '';
+ updateMegaVirusBalloonFallbackPreview();
+ }
+ }
+
+ function loadMegaVirusPanel() {
+ gameTimingFetch('GET')
+ .then(function (data) {
+ applyMegaVirusPanelFromTimingData(data);
+ setMsg('mega-virus-timing-msg', '', '');
+ })
+ .catch(function (e) {
+ setMsg('mega-virus-timing-msg', e.message || 'โหลดไม่ได้', 'error');
+ });
+ }
+
+ function saveMegaVirusTimingPanel() {
+ if (megaVirusSaveInFlight) return;
+ megaVirusSaveInFlight = true;
+ var btn = el('btn-mega-virus-save');
+ if (btn) btn.disabled = true;
+ var limSs = el('mega-virus-mission-sec') ? parseInt(String(el('mega-virus-mission-sec').value), 10) : 0;
+ if (Number.isNaN(limSs) || limSs < 0) limSs = 0;
+ limSs = limSs <= 0 ? 0 : Math.max(10, Math.min(7200, limSs));
+ var bossUrl = normalizeMegaVirusAssetUrl(el('mega-virus-boss-url') && el('mega-virus-boss-url').value ? String(el('mega-virus-boss-url').value).trim() : '');
+ var balloonUrls = readMegaVirusBalloonUrlsFromForm();
+ var fbUrl = normalizeMegaVirusAssetUrl(el('mega-virus-balloon-fallback-url') && el('mega-virus-balloon-fallback-url').value
+ ? String(el('mega-virus-balloon-fallback-url').value).trim()
+ : '');
+ var balloonsPer = el('mega-virus-balloons-per-player') ? parseInt(String(el('mega-virus-balloons-per-player').value), 10) : 0;
+ if (Number.isNaN(balloonsPer) || balloonsPer < 0) balloonsPer = 0;
+ balloonsPer = Math.max(0, Math.min(12, balloonsPer));
+ gameTimingFetch('GET')
+ .then(function (data) {
+ data.balloonBossMissionTimeSec = limSs;
+ data.balloonBossBossImageUrl = bossUrl;
+ data.balloonBossPlayerBalloonImageUrls = balloonUrls;
+ data.balloonBossPlayerBalloonFallbackUrl = fbUrl;
+ data.balloonBossBalloonsPerPlayer = balloonsPer;
+ return gameTimingFetch('PUT', data);
+ })
+ .then(function (res) {
+ if (res && typeof res === 'object') applyMegaVirusPanelFromTimingData(res);
+ setMsg('mega-virus-timing-msg', 'บันทึกแล้ว — รีเฟรชหน้าเล่น (หรือเปิดใหม่) เพื่อโหลดรูป · Saved; hard-refresh play.', 'ok');
+ })
+ .catch(function (e) {
+ setMsg('mega-virus-timing-msg', e.message || 'บันทึกไม่ได้', 'error');
+ })
+ .then(function () {
+ megaVirusSaveInFlight = false;
+ if (btn) btn.disabled = false;
+ });
+ }
+
+ function applyForcedMinigameKeysToForm(keys) {
+ var arr = Array.isArray(keys) ? keys : [];
+ for (var i = 1; i <= 3; i++) {
+ var sel = el('forced-mg-' + i);
+ if (sel) sel.value = arr[i - 1] || '';
+ }
+ }
+
+ function readForcedMinigameKeysFromForm() {
+ var out = [];
+ for (var i = 1; i <= 3; i++) {
+ var sel = el('forced-mg-' + i);
+ out.push(sel ? String(sel.value || '') : '');
+ }
+ return out;
+ }
+
+ function loadForcedMinigamePanel() {
+ gameTimingFetch('GET')
+ .then(function (data) {
+ applyForcedMinigameKeysToForm(data && data.forcedMinigameKeys);
+ setMsg('forced-minigame-msg', '', '');
+ })
+ .catch(function (e) {
+ setMsg('forced-minigame-msg', e.message || 'โหลดไม่ได้', 'error');
+ });
+ }
+
+ function saveForcedMinigamePanel() {
+ var keys = readForcedMinigameKeysFromForm();
+ var anyForced = keys.some(function (k) { return !!k; });
+ var btn = el('btn-forced-minigame-save');
+ if (btn) btn.disabled = true;
+ gameTimingFetch('GET')
+ .then(function (data) {
+ data.forcedMinigameKeys = keys;
+ return gameTimingFetch('PUT', data);
+ })
+ .then(function (res) {
+ if (res && typeof res === 'object') applyForcedMinigameKeysToForm(res.forcedMinigameKeys);
+ setMsg('forced-minigame-msg', anyForced ? 'บันทึกแล้ว — เริ่มคดีใหม่เพื่อเทสต์การ์ดที่ตั้งไว้' : 'กลับเป็นปกติ (สุ่มทั้ง 3 ใบ) แล้ว', 'ok');
+ })
+ .catch(function (e) {
+ setMsg('forced-minigame-msg', e.message || 'บันทึกไม่ได้', 'error');
+ })
+ .then(function () {
+ if (btn) btn.disabled = false;
+ });
+ }
+
+ var SPECIAL_CARD_GAMES = [
+ { id: 'mng8a80o', label: 'Minigame-1 จริงหรือไม่' },
+ { id: 'mno9kb07', label: 'Minigame-2 วิ่งหลบสิ่งกีดขวาง' },
+ { id: 'mnn93hpi', label: 'Minigame-3 Tower block' },
+ { id: 'mnorwqx1', label: 'Minigame-4 คำศัพท์ในกระบวนการยุติธรรม' },
+ { id: 'mnptfts2', label: 'Minigame-5 กระโดดขึ้นแท่น' },
+ { id: 'mnpz6rkp', label: 'Minigame-6 ยิงอุกาบาต Violent Crime' },
+ { id: 'mnq1eml7', label: 'Minigame-7 ยิง Mega Virus' }
+ ];
+ var SPECIAL_CARD_OPTS = [
+ { v: '', t: '— ไม่บังคับ —' },
+ { v: '1', t: '1 · เพิ่มเวลามินิเกม +10s' },
+ { v: '2', t: '2 · เพิ่มเวลาโหวต +10s' },
+ { v: '3', t: '3 · แบนผู้เล่น 1 คน' },
+ { v: '4', t: '4 · ปิดปากผู้เล่น 1 คน' },
+ { v: '5', t: '5 · จับผิดตัว โหวตใหม่' },
+ { v: '6', t: '6 · ทุกคน +10 COINS' },
+ { v: '7', t: '7 · ทุกคนหลักฐานฟรี +1' }
+ ];
+ var specialCardRowsBuilt = false;
+ function buildSpecialCardByMapRows() {
+ if (specialCardRowsBuilt) return;
+ var wrap = el('special-card-by-map-rows');
+ if (!wrap) return;
+ wrap.innerHTML = '';
+ SPECIAL_CARD_GAMES.forEach(function (g) {
+ var row = document.createElement('div');
+ row.style.cssText = 'display:flex;align-items:center;gap:10px;flex-wrap:wrap;';
+ var name = document.createElement('span');
+ name.style.cssText = 'min-width:240px;color:#cfd6ea;font-size:14px;';
+ name.textContent = g.label;
+ var sel = document.createElement('select');
+ sel.id = 'tsc-' + g.id;
+ sel.style.cssText = 'padding:6px 8px;min-width:300px;font-size:14px;';
+ SPECIAL_CARD_OPTS.forEach(function (o) {
+ var op = document.createElement('option');
+ op.value = o.v;
+ op.textContent = o.t;
+ sel.appendChild(op);
+ });
+ row.appendChild(name);
+ row.appendChild(sel);
+ wrap.appendChild(row);
+ });
+ specialCardRowsBuilt = true;
+ }
+ function loadSpecialCardByMapPanel() {
+ buildSpecialCardByMapRows();
+ gameTimingFetch('GET')
+ .then(function (data) {
+ var m = (data && data.testSpecialCardByMap) || {};
+ SPECIAL_CARD_GAMES.forEach(function (g) {
+ var sel = el('tsc-' + g.id);
+ if (sel) sel.value = (m[g.id] != null && m[g.id] !== '') ? String(m[g.id]) : '';
+ });
+ setMsg('special-card-by-map-msg', '', '');
+ })
+ .catch(function (e) { setMsg('special-card-by-map-msg', e.message || 'โหลดไม่ได้', 'error'); });
+ }
+ function saveSpecialCardByMapPanel() {
+ var map = {};
+ SPECIAL_CARD_GAMES.forEach(function (g) {
+ var sel = el('tsc-' + g.id);
+ var v = sel ? parseInt(String(sel.value || ''), 10) : 0;
+ if (v >= 1 && v <= 7) map[g.id] = v;
+ });
+ var btn = el('btn-special-card-by-map-save');
+ if (btn) btn.disabled = true;
+ gameTimingFetch('GET')
+ .then(function (data) {
+ data.testSpecialCardByMap = map;
+ return gameTimingFetch('PUT', data);
+ })
+ .then(function (res) {
+ if (res && res.testSpecialCardByMap) {
+ var m = res.testSpecialCardByMap;
+ SPECIAL_CARD_GAMES.forEach(function (g) {
+ var sel = el('tsc-' + g.id);
+ if (sel) sel.value = (m[g.id] != null) ? String(m[g.id]) : '';
+ });
+ }
+ var any = Object.keys(map).length > 0;
+ setMsg('special-card-by-map-msg', any ? 'บันทึกแล้ว — เกมที่ตั้งจะขึ้นคำถามพิเศษ + การ์ดนั้นแน่นอน' : 'ล้างการบังคับแล้ว (กลับเป็นสุ่มปกติ)', 'ok');
+ })
+ .catch(function (e) { setMsg('special-card-by-map-msg', e.message || 'บันทึกไม่ได้', 'error'); })
+ .then(function () { if (btn) btn.disabled = false; });
+ }
+
+ function loadTroublesomeForcePanel() {
+ var chk = el('troublesome-force-offer');
+ if (!chk) return;
+ gameTimingFetch('GET')
+ .then(function (data) {
+ chk.checked = !!(data && data.troublesomeForceOffer);
+ setMsg('troublesome-force-msg', '', '');
+ })
+ .catch(function (e) { setMsg('troublesome-force-msg', e.message || 'โหลดไม่ได้', 'error'); });
+ }
+ function saveTroublesomeForcePanel() {
+ var chk = el('troublesome-force-offer');
+ if (!chk) return;
+ var on = !!chk.checked;
+ var btn = el('btn-troublesome-force-save');
+ if (btn) btn.disabled = true;
+ gameTimingFetch('GET')
+ .then(function (data) {
+ data.troublesomeForceOffer = on;
+ return gameTimingFetch('PUT', data);
+ })
+ .then(function (res) {
+ if (res && typeof res === 'object') chk.checked = !!res.troublesomeForceOffer;
+ setMsg('troublesome-force-msg', on ? 'บันทึกแล้ว — บทตัวป่วนจะขึ้นเสมอ (เริ่มคดีใหม่เพื่อเทสต์)' : 'ปิดแล้ว — กลับเป็นปกติ (ขึ้นเฉพาะคนจริง ≥ 4)', 'ok');
+ })
+ .catch(function (e) { setMsg('troublesome-force-msg', e.message || 'บันทึกไม่ได้', 'error'); })
+ .then(function () { if (btn) btn.disabled = false; });
+ }
+
+ function loadSpecialQuizIconExpirePanel() {
+ var inp = el('special-quiz-icon-expire-sec');
+ if (!inp) return;
+ gameTimingFetch('GET')
+ .then(function (data) {
+ var v = (data && data.specialQuizIconExpireSec != null) ? Math.floor(Number(data.specialQuizIconExpireSec)) : 10;
+ if (!Number.isFinite(v) || v < 0) v = 10;
+ inp.value = String(Math.min(120, v));
+ setMsg('special-quiz-icon-expire-msg', '', '');
+ })
+ .catch(function (e) { setMsg('special-quiz-icon-expire-msg', e.message || 'โหลดไม่ได้', 'error'); });
+ }
+ function saveSpecialQuizIconExpirePanel() {
+ var inp = el('special-quiz-icon-expire-sec');
+ if (!inp) return;
+ var v = Math.floor(Number(inp.value));
+ if (!Number.isFinite(v) || v < 0) v = 10;
+ v = Math.min(120, v);
+ inp.value = String(v);
+ var btn = el('btn-special-quiz-icon-expire-save');
+ if (btn) btn.disabled = true;
+ gameTimingFetch('GET')
+ .then(function (data) {
+ data.specialQuizIconExpireSec = v;
+ return gameTimingFetch('PUT', data);
+ })
+ .then(function (res) {
+ if (res && res.specialQuizIconExpireSec != null) inp.value = String(res.specialQuizIconExpireSec);
+ setMsg('special-quiz-icon-expire-msg', v > 0 ? ('บันทึกแล้ว — ไอคอนจะหายใน ' + v + ' วินาที (เริ่มเกมใหม่เพื่อเทสต์)') : 'บันทึกแล้ว — ไอคอนจะไม่หาย (อยู่จนจบเกม)', 'ok');
+ })
+ .catch(function (e) { setMsg('special-quiz-icon-expire-msg', e.message || 'บันทึกไม่ได้', 'error'); })
+ .then(function () { if (btn) btn.disabled = false; });
+ }
+
+ function saveJumpSurviveTimingPanel() {
+ var inp = el('jump-survive-height-mult');
+ var jumpSurvMult = inp ? parseFloat(String(inp.value)) : 1.5;
+ if (Number.isNaN(jumpSurvMult)) jumpSurvMult = 1.5;
+ jumpSurvMult = Math.round(Math.max(0.5, Math.min(4, jumpSurvMult)) * 100) / 100;
+ var limJump = el('jump-survive-mission-sec') ? parseInt(String(el('jump-survive-mission-sec').value), 10) : 0;
+ if (Number.isNaN(limJump) || limJump < 0) limJump = 0;
+ limJump = limJump <= 0 ? 0 : Math.max(10, Math.min(7200, limJump));
+ var platTiles = readJumpSurvivePlatformTilesFromForm();
+ gameTimingFetch('GET')
+ .then(function (data) {
+ data.jumpSurviveJumpHeightMult = jumpSurvMult;
+ data.jumpSurviveMissionTimeSec = limJump;
+ delete data.jumpSurvivePlatformTileUrl;
+ data.jumpSurvivePlatformTiles = platTiles;
+ return gameTimingFetch('PUT', data);
+ })
+ .then(function () {
+ setMsg('jump-survive-timing-msg', 'บันทึกแล้ว — ผู้เล่นกระโดดให้รอดได้ค่าใหม่เมื่อรีเฟรชหน้าเล่นหรือ sync จากเซิร์ฟเวอร์', 'ok');
+ })
+ .catch(function (e) {
+ setMsg('jump-survive-timing-msg', e.message || 'บันทึกไม่ได้', 'error');
+ });
+ }
+
+ function saveGameTimingPanel() {
+ var tickMs = parseInt(el('game-timing-tick-ms').value, 10);
+ var jumpT = parseInt(el('game-timing-jump-ticks').value, 10);
+ var limS = parseInt(el('game-timing-limit-sec').value, 10);
+ if (Number.isNaN(tickMs)) tickMs = 220;
+ if (Number.isNaN(jumpT)) jumpT = 16;
+ if (Number.isNaN(limS) || limS < 0) limS = 0;
+ tickMs = Math.max(80, Math.min(800, tickMs));
+ jumpT = Math.max(1, Math.min(40, jumpT));
+ limS = limS <= 0 ? 0 : Math.max(10, Math.min(7200, limS));
+ var lwPx = el('game-timing-laser-line-width') ? parseInt(el('game-timing-laser-line-width').value, 10) : 2;
+ if (Number.isNaN(lwPx)) lwPx = 2;
+ lwPx = Math.max(0, Math.min(24, lwPx));
+ /** แท็บเวลาเกม = เฉพาะ Gauntlet — อย่าส่ง stackSwingHz/stackBlockWidthTiles จากฟอร์ม (หลังรีเฟรชช่อง Stack ค้าง 0.55 จาก HTML จะทับค่าที่บันทึกไว้) */
+ gameTimingFetch('GET')
+ .then(function (data) {
+ data.gauntletTickMs = tickMs;
+ data.gauntletJumpTicks = jumpT;
+ data.gauntletJumpHeightMult = (function () { var v = parseFloat(el('game-timing-jump-height') && el('game-timing-jump-height').value); if (Number.isNaN(v)) v = 0.52; return Math.max(0.2, Math.min(1.5, v)); })();
+ data.gauntletTimeLimitSec = limS;
+ /* troublesomeOfferSec/RollMaxSec ย้ายไปแท็บ "บทตัวป่วน" — ไม่แตะที่นี่ (ค่าจาก GET ส่งต่อคงเดิม) */
+ data.gauntletLaneImageUrls = parseLaneUrlsTextarea();
+ data.gauntletLaserTopUrl = el('game-timing-laser-top-url') ? String(el('game-timing-laser-top-url').value).trim() : '';
+ data.gauntletLaserBottomUrl = el('game-timing-laser-bottom-url') ? String(el('game-timing-laser-bottom-url').value).trim() : '';
+ data.gauntletLaserLineUrl = el('game-timing-laser-line-url') ? String(el('game-timing-laser-line-url').value).trim() : '';
+ data.gauntletLaserFillColor = el('game-timing-laser-fill') ? String(el('game-timing-laser-fill').value).trim() : '';
+ data.gauntletLaserStrokeColor = el('game-timing-laser-stroke') ? String(el('game-timing-laser-stroke').value).trim() : '';
+ data.gauntletLaserLineWidthPx = lwPx;
+ return gameTimingFetch('PUT', data);
+ })
+ .then(function () {
+ setMsg('game-timing-msg', 'บันทึกแล้ว — พรมแดง tick·กระโดด / รูปอุปสรรค์ · Minigame-5 / Minigame-6 ตั้งแท็บของแต่ละเกม · ค่า Tower block (Stack) แก้ที่แท็บ Minigame-3 เท่านั้น · จำกัดเวลาพรมแดงใช้กับรอบถัดไปที่โฮสต์เริ่มเกม', 'ok');
+ })
+ .catch(function (e) {
+ setMsg('game-timing-msg', e.message || 'บันทึกไม่ได้', 'error');
+ });
+ }
+
+ function showPanel(name) {
+ ['panel-setup', 'panel-login', 'panel-app'].forEach(function (id) {
+ var p = el(id);
+ if (p) p.hidden = id !== name;
+ });
+ var inApp = name === 'panel-app';
+ el('top-actions').hidden = !inApp;
+ var stats = el('admin-sys-stats');
+ if (stats) stats.hidden = !inApp;
+ var burger = el('btn-sidebar-toggle');
+ if (burger) burger.hidden = !inApp;
+ if (inApp) startSystemStats();
+ else stopSystemStats();
+ }
+
+ /* ===== ระบบ: ดึง CPU/RAM ของเซิร์ฟเวอร์มาแสดง (อัปเดตทุก 5 วิ) ===== */
+ var __sysStatsTimer = null;
+ function fmtBytes(n) {
+ if (!(n > 0)) return '0';
+ var u = ['B', 'KB', 'MB', 'GB', 'TB'];
+ var i = Math.floor(Math.log(n) / Math.log(1024));
+ i = Math.max(0, Math.min(u.length - 1, i));
+ return (n / Math.pow(1024, i)).toFixed(i >= 2 ? 1 : 0) + ' ' + u[i];
+ }
+ function fmtUptime(sec) {
+ sec = Math.max(0, Math.floor(sec || 0));
+ var d = Math.floor(sec / 86400); sec -= d * 86400;
+ var h = Math.floor(sec / 3600); sec -= h * 3600;
+ var m = Math.floor(sec / 60);
+ if (d > 0) return d + 'd ' + h + 'h';
+ if (h > 0) return h + 'h ' + m + 'm';
+ return m + 'm';
+ }
+ function setBarTone(bar, pct) {
+ if (!bar) return;
+ bar.classList.remove('is-warn', 'is-crit');
+ if (pct >= 85) bar.classList.add('is-crit');
+ else if (pct >= 65) bar.classList.add('is-warn');
+ }
+ function renderSystemStats(s) {
+ if (!s || !s.ok) return;
+ var cpuPct = (s.cpu && typeof s.cpu.percent === 'number') ? s.cpu.percent
+ : (s.cpu ? s.cpu.loadPercent : 0);
+ var cpuVal = el('sysstat-cpu-val'), cpuBar = el('sysstat-cpu-bar');
+ if (cpuVal) cpuVal.textContent = cpuPct + '%';
+ if (cpuBar) { cpuBar.style.width = cpuPct + '%'; setBarTone(cpuBar, cpuPct); }
+ var memPct = s.mem ? s.mem.usedPercent : 0;
+ var ramVal = el('sysstat-ram-val'), ramBar = el('sysstat-ram-bar');
+ if (ramVal && s.mem) ramVal.textContent = memPct + '% ' + fmtBytes(s.mem.used) + '/' + fmtBytes(s.mem.total);
+ if (ramBar) { ramBar.style.width = memPct + '%'; setBarTone(ramBar, memPct); }
+ // HDD / ดิสก์
+ var diskWrap = el('sysstat-disk-val') ? el('sysstat-disk-val').closest('.sysstat') : null;
+ if (s.disk) {
+ var dPct = s.disk.usedPercent;
+ var dVal = el('sysstat-disk-val'), dBar = el('sysstat-disk-bar');
+ if (dVal) dVal.textContent = dPct + '% ' + fmtBytes(s.disk.used) + '/' + fmtBytes(s.disk.total);
+ if (dBar) { dBar.style.width = dPct + '%'; setBarTone(dBar, dPct); }
+ if (diskWrap) diskWrap.hidden = false;
+ } else if (diskWrap) {
+ diskWrap.hidden = true; // อ่านดิสก์ไม่ได้ (เช่น Node เก่า) → ซ่อน
+ }
+ // Node ที่ใช้อยู่ (RAM ของ process เกม)
+ if (s.node) {
+ var nPct = typeof s.node.rssPercent === 'number' ? s.node.rssPercent : 0;
+ var nVal = el('sysstat-node-val'), nBar = el('sysstat-node-bar');
+ if (nVal) nVal.textContent = fmtBytes(s.node.rss) + (s.node.version ? ' ' + s.node.version : '');
+ if (nBar) { nBar.style.width = Math.min(100, nPct) + '%'; setBarTone(nBar, nPct); }
+ }
+ var upVal = el('sysstat-up-val');
+ if (upVal && s.uptime) upVal.textContent = fmtUptime(s.uptime.os);
+ var box = el('admin-sys-stats');
+ if (box && s.cpu) box.title = 'CPU ' + cpuPct + '% (load ' + s.cpu.load1 + ', ' + s.cpu.cores + ' cores) · RAM '
+ + memPct + '%'
+ + (s.disk ? ' · HDD ' + s.disk.usedPercent + '% (' + fmtBytes(s.disk.free) + ' ว่าง)' : '')
+ + (s.node ? ' · Node ' + fmtBytes(s.node.rss) + ' ' + (s.node.version || '') : '')
+ + ' · uptime ' + fmtUptime(s.uptime && s.uptime.os) + ' · ' + (s.hostname || '');
+ }
+ function pollSystemStats() {
+ fetch('/Game/api/system-stats', { cache: 'no-store' })
+ .then(function (r) { return r.ok ? r.json() : null; })
+ .then(function (s) { if (s) renderSystemStats(s); })
+ .catch(function () { /* เงียบไว้ ไม่รบกวน admin */ });
+ }
+ function startSystemStats() {
+ if (__sysStatsTimer) return;
+ pollSystemStats();
+ setTimeout(pollSystemStats, 1200); // ครั้งที่สองให้ได้ค่า CPU% (ครั้งแรก server คืน null)
+ __sysStatsTimer = setInterval(pollSystemStats, 5000);
+ }
+ function stopSystemStats() {
+ if (__sysStatsTimer) { clearInterval(__sysStatsTimer); __sysStatsTimer = null; }
+ }
+
+ // ===== Evidence Cards admin (รูปอย่างเดียว · 15 คดี · rarity จากเลขไฟล์) =====
+ var EVIDENCE_CARDS_API = '/Game/api/evidence-cards';
+ var EVIDENCE_IMAGES_API = '/Game/api/evidence-card-images';
+ var EVIDENCE_CASE_COUNT = 15;
+ var EVIDENCE_DIRS = ['suspect-1', 'suspect-2', 'suspect-3'];
+ var evidenceData = null;
+ var evidencePools = null; // { 'suspect-1': {common,rare,legendary}, ... }
+ var evidenceCurrentCase = '1';
+
+ function evidencePopulateCaseSelect() {
+ var sel = el('evidence-case');
+ if (!sel || sel.options.length) return;
+ for (var i = 1; i <= EVIDENCE_CASE_COUNT; i++) {
+ var o = document.createElement('option');
+ o.value = String(i); o.textContent = 'คดี ' + i;
+ sel.appendChild(o);
+ }
+ }
+
+ function loadEvidenceCardsPanel() {
+ evidencePopulateCaseSelect();
+ if (evidencePools == null) {
+ fetch(EVIDENCE_IMAGES_API + '?_=' + Date.now(), { credentials: 'include', cache: 'no-store' })
+ .then(function (r) { return r.json(); })
+ .then(function (j) { evidencePools = (j && j.pools) || {}; renderEvidenceCase(evidenceCurrentCase); })
+ .catch(function () { evidencePools = {}; });
+ }
+ fetch(EVIDENCE_CARDS_API + '?_=' + Date.now(), { credentials: 'include', cache: 'no-store' })
+ .then(function (r) { return r.json(); })
+ .then(function (j) {
+ evidenceData = (j && j.cases) ? j : { cases: {} };
+ var sel = el('evidence-case');
+ if (sel) evidenceCurrentCase = sel.value || '1';
+ renderEvidenceCase(evidenceCurrentCase);
+ })
+ .catch(function (e) {
+ setMsg('evidence-cards-msg', (e.message || 'โหลดไม่ได้') + ' — เช็คว่า Node รัน Game/server.js และ /Game/api/evidence-cards ไม่ 404', 'error');
+ });
+ }
+
+ function evidenceGetCase(cid) {
+ if (!evidenceData) evidenceData = { cases: {} };
+ if (!evidenceData.cases) evidenceData.cases = {};
+ if (!evidenceData.cases[cid]) evidenceData.cases[cid] = { suspects: [] };
+ var cse = evidenceData.cases[cid];
+ if (!Array.isArray(cse.suspects)) cse.suspects = [];
+ for (var si = 0; si < 3; si++) {
+ if (!cse.suspects[si]) cse.suspects[si] = {};
+ var s = cse.suspects[si];
+ if (!s.linkName) s.linkName = 'ผู้ต้องสงสัย ' + (si + 1);
+ if (EVIDENCE_DIRS.indexOf(s.imageDir) < 0) s.imageDir = EVIDENCE_DIRS[si] || EVIDENCE_DIRS[0];
+ }
+ return cse;
+ }
+
+ function evidencePoolBadge(dir) {
+ var p = (evidencePools && evidencePools[dir]) || null;
+ var box = document.createElement('div');
+ box.className = 'ev-pool-counts';
+ if (!p) { box.textContent = 'กำลังโหลดจำนวนรูป…'; return box; }
+ box.innerHTML =
+ 'ธรรมดา ' + (p.common || 0) + '' +
+ 'Rare ' + (p.rare || 0) + '' +
+ 'ชี้คนร้าย ' + (p.legendary || 0) + '';
+ return box;
+ }
+
+ function renderEvidenceCase(cid) {
+ evidenceCurrentCase = cid;
+ var tag = el('evidence-set-tag');
+ if (tag) tag.textContent = 'กำลังแก้: คดี ' + cid;
+ var root = el('evidence-suspects-root');
+ if (!root) return;
+ var cse = evidenceGetCase(cid);
+ root.innerHTML = '';
+ cse.suspects.forEach(function (s, si) {
+ var block = document.createElement('div');
+ block.className = 'ev-suspect-block';
+
+ var head = document.createElement('div');
+ head.className = 'ev-suspect-head';
+ head.innerHTML = 'ผู้ต้องสงสัย ' + (si + 1) + '';
+ block.appendChild(head);
+
+ var nameLabel = document.createElement('label');
+ nameLabel.className = 'admin-field';
+ nameLabel.textContent = 'ชื่อผู้ต้องสงสัย (Suspect Link)';
+ var nameInp = document.createElement('input');
+ nameInp.type = 'text';
+ nameInp.maxLength = 60;
+ nameInp.value = s.linkName || '';
+ nameInp.addEventListener('input', function () { s.linkName = nameInp.value; });
+ nameLabel.appendChild(nameInp);
+ block.appendChild(nameLabel);
+
+ var poolBox = evidencePoolBadge(s.imageDir);
+
+ var dirLabel = document.createElement('label');
+ dirLabel.className = 'admin-field';
+ dirLabel.textContent = 'โฟลเดอร์รูปการ์ด (พูลที่ใช้สุ่ม)';
+ var dirSel = document.createElement('select');
+ dirSel.className = 'admin-inp-num';
+ EVIDENCE_DIRS.forEach(function (d) {
+ var o = document.createElement('option');
+ o.value = d; o.textContent = d.replace('suspect-', 'ผู้ต้องสงสัย-');
+ if (d === s.imageDir) o.selected = true;
+ dirSel.appendChild(o);
+ });
+ dirSel.addEventListener('change', function () {
+ s.imageDir = dirSel.value;
+ var fresh = evidencePoolBadge(s.imageDir);
+ poolBox.replaceWith(fresh);
+ poolBox = fresh;
+ });
+ dirLabel.appendChild(dirSel);
+ block.appendChild(dirLabel);
+ block.appendChild(poolBox);
+
+ root.appendChild(block);
+ });
+ }
+
+ function saveEvidenceCardsPanel() {
+ if (!evidenceData) return;
+ var btn = el('btn-evidence-save');
+ if (btn) btn.disabled = true;
+ setMsg('evidence-cards-msg', 'กำลังบันทึก…', '');
+ fetch(EVIDENCE_CARDS_API, {
+ method: 'PUT',
+ credentials: 'include',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(evidenceData),
+ }).then(function (r) {
+ return r.text().then(function (t) {
+ var j = {};
+ try { j = t ? JSON.parse(t) : {}; } catch (e) { /* ignore */ }
+ if (!r.ok || !j.ok) throw new Error((j && j.error) || ('HTTP ' + r.status));
+ return j;
+ });
+ }).then(function (j) {
+ if (j && j.cases) evidenceData = { cases: j.cases };
+ renderEvidenceCase(evidenceCurrentCase);
+ setMsg('evidence-cards-msg', 'บันทึกแล้ว — มีผลในเกมเมื่อรีเฟรช LobbyB', 'ok');
+ }).catch(function (e) {
+ setMsg('evidence-cards-msg', e.message || 'บันทึกไม่ได้', 'error');
+ }).then(function () {
+ if (btn) btn.disabled = false;
+ });
+ }
+
+ (function bindEvidenceCardsPanel() {
+ evidencePopulateCaseSelect();
+ var caseSel = el('evidence-case');
+ if (caseSel) caseSel.addEventListener('change', function () { renderEvidenceCase(caseSel.value || '1'); });
+ var saveBtn = el('btn-evidence-save');
+ if (saveBtn) saveBtn.addEventListener('click', saveEvidenceCardsPanel);
+ })();
+
+ /* ===== รูปคดี & Cutscene (LobbyA → LobbyB) ===== */
+ var CASE_MEDIA_API = '/Game/api/case-media';
+ var CASE_MEDIA_IMAGES_API = '/Game/api/case-media-images';
+ var CASE_MEDIA_COUNT = 15;
+ var caseMediaData = null;
+ var caseMediaImages = null; // { case:[...], cutscene:[...] }
+ var caseMediaCurrent = '1';
+
+ function caseMediaPopulateSelect() {
+ var sel = el('case-media-case');
+ if (!sel || sel.options.length) return;
+ for (var i = 1; i <= CASE_MEDIA_COUNT; i++) {
+ var o = document.createElement('option');
+ o.value = String(i); o.textContent = 'คดี ' + i;
+ sel.appendChild(o);
+ }
+ }
+
+ function loadCaseMediaPanel() {
+ caseMediaPopulateSelect();
+ if (caseMediaImages == null) {
+ fetch(CASE_MEDIA_IMAGES_API + '?_=' + Date.now(), { credentials: 'include', cache: 'no-store' })
+ .then(function (r) { return r.json(); })
+ .then(function (j) {
+ caseMediaImages = (j && j.images) || { case: [], cutscene: [], 'suspect-pick': [], 'culprit-prison': [] };
+ renderCaseMedia(caseMediaCurrent);
+ })
+ .catch(function () { caseMediaImages = { case: [], cutscene: [], 'suspect-pick': [], 'culprit-prison': [] }; });
+ }
+ fetch(CASE_MEDIA_API + '?_=' + Date.now(), { credentials: 'include', cache: 'no-store' })
+ .then(function (r) { return r.json(); })
+ .then(function (j) {
+ caseMediaData = (j && j.cases) ? j : { cases: {} };
+ var sel = el('case-media-case');
+ if (sel) caseMediaCurrent = sel.value || '1';
+ renderCaseMedia(caseMediaCurrent);
+ })
+ .catch(function (e) {
+ setMsg('case-media-msg', (e.message || 'โหลดไม่ได้') + ' — เช็คว่า Node รัน Game/server.js และ /Game/api/case-media ไม่ 404', 'error');
+ });
+ }
+
+ function caseMediaGetCase(cid) {
+ if (!caseMediaData) caseMediaData = { cases: {} };
+ if (!caseMediaData.cases) caseMediaData.cases = {};
+ var n = parseInt(cid, 10) || 1;
+ if (!caseMediaData.cases[cid]) caseMediaData.cases[cid] = {};
+ var c = caseMediaData.cases[cid];
+ if (typeof c.name !== 'string') c.name = 'คดีที่ ' + n;
+ if (typeof c.art !== 'string') c.art = '/Main-Lobby/IMAGE/case/case-' + n + '.png';
+ if (typeof c.detail !== 'string') c.detail = '/Main-Lobby/IMAGE/case/case-detail-' + n + '.png';
+ if (!Array.isArray(c.story)) c.story = [];
+ if (typeof c.story[0] !== 'string') c.story[0] = '/Main-Lobby/IMAGE/cutscene/case-' + n + '-story-1.png';
+ if (typeof c.story[1] !== 'string') c.story[1] = '/Main-Lobby/IMAGE/cutscene/case-' + n + '-story-2.png';
+ if (!Array.isArray(c.suspects) || c.suspects.length < 3) {
+ c.suspects = [1, 2, 3].map(function (s) { return '/Main-Lobby/IMAGE/suspect-pick/case-' + n + '-suspect-' + s + '.png'; });
+ }
+ if (!Array.isArray(c.culprits) || c.culprits.length < 3) {
+ c.culprits = [1, 2, 3].map(function (s) { return '/Main-Lobby/IMAGE/culprit-prison/case-' + n + '-suspect-' + s + '.png'; });
+ }
+ return c;
+ }
+
+ /** ตัวเลือกรูป (dropdown + พรีวิว) — kind = 'case' | 'cutscene' */
+ function caseMediaImagePicker(label, kind, current, onChange) {
+ var wrap = document.createElement('div');
+ wrap.className = 'cm-pick';
+ var lab = document.createElement('div'); lab.className = 'cm-pick-label'; lab.textContent = label;
+ var prev = document.createElement('img'); prev.className = 'cm-pick-prev'; prev.alt = ''; prev.src = current;
+ prev.onerror = function () { this.style.opacity = '0.25'; };
+ var sel = document.createElement('select'); sel.className = 'admin-inp-num';
+ var list = (caseMediaImages && caseMediaImages[kind]) ? caseMediaImages[kind].slice() : [];
+ if (current && list.indexOf(current) < 0) list.unshift(current);
+ list.forEach(function (u) {
+ var o = document.createElement('option');
+ o.value = u; o.textContent = u.split('/').pop();
+ if (u === current) o.selected = true;
+ sel.appendChild(o);
+ });
+ sel.addEventListener('change', function () { prev.src = sel.value; onChange(sel.value); });
+ wrap.appendChild(lab); wrap.appendChild(prev); wrap.appendChild(sel);
+ return wrap;
+ }
+
+ function renderCaseMedia(cid) {
+ caseMediaCurrent = cid;
+ var tag = el('case-media-set-tag');
+ if (tag) tag.textContent = 'กำลังแก้: คดี ' + cid;
+ var root = el('case-media-root');
+ if (!root) return;
+ var c = caseMediaGetCase(cid);
+ root.innerHTML = '';
+
+ var nameLabel = document.createElement('label');
+ nameLabel.className = 'admin-field';
+ nameLabel.textContent = 'ชื่อคดี';
+ var nameInp = document.createElement('input');
+ nameInp.type = 'text'; nameInp.maxLength = 80; nameInp.value = c.name || '';
+ nameInp.addEventListener('input', function () { c.name = nameInp.value; });
+ nameLabel.appendChild(nameInp);
+ root.appendChild(nameLabel);
+
+ var grid = document.createElement('div');
+ grid.className = 'cm-grid';
+ grid.appendChild(caseMediaImagePicker('รูปเลือกคดี (การ์ด)', 'case', c.art, function (v) { c.art = v; }));
+ grid.appendChild(caseMediaImagePicker('รูปรายละเอียดคดี', 'case', c.detail, function (v) { c.detail = v; }));
+ grid.appendChild(caseMediaImagePicker('Cutscene รูปที่ 1', 'cutscene', c.story[0], function (v) { c.story[0] = v; }));
+ grid.appendChild(caseMediaImagePicker('Cutscene รูปที่ 2', 'cutscene', c.story[1], function (v) { c.story[1] = v; }));
+ root.appendChild(grid);
+
+ var susHead = document.createElement('h3');
+ susHead.className = 'cm-section-title';
+ susHead.textContent = 'เลือกผู้ต้องสงสัย (3 คน) — LobbyB';
+ root.appendChild(susHead);
+ var susGrid = document.createElement('div');
+ susGrid.className = 'cm-grid';
+ [0, 1, 2].forEach(function (si) {
+ susGrid.appendChild(caseMediaImagePicker('ผู้ต้องสงสัย ' + (si + 1), 'suspect-pick', c.suspects[si], function (v) { c.suspects[si] = v; }));
+ });
+ root.appendChild(susGrid);
+
+ var culHead = document.createElement('h3');
+ culHead.className = 'cm-section-title';
+ culHead.textContent = 'ส่งผู้ร้ายเข้าคุก (3 คน) — หน้าผลพิจารณาคดี';
+ root.appendChild(culHead);
+ var culGrid = document.createElement('div');
+ culGrid.className = 'cm-grid';
+ [0, 1, 2].forEach(function (si) {
+ culGrid.appendChild(caseMediaImagePicker('คนร้ายในคุก ' + (si + 1), 'culprit-prison', c.culprits[si], function (v) { c.culprits[si] = v; }));
+ });
+ root.appendChild(culGrid);
+ }
+
+ function saveCaseMediaPanel() {
+ if (!caseMediaData) return;
+ var btn = el('btn-case-media-save');
+ if (btn) btn.disabled = true;
+ setMsg('case-media-msg', 'กำลังบันทึก…', '');
+ fetch(CASE_MEDIA_API, {
+ method: 'PUT',
+ credentials: 'include',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(caseMediaData),
+ }).then(function (r) {
+ return r.text().then(function (t) {
+ var j = {};
+ try { j = t ? JSON.parse(t) : {}; } catch (e) { /* ignore */ }
+ if (!r.ok || !j.ok) throw new Error((j && j.error) || ('HTTP ' + r.status));
+ return j;
+ });
+ }).then(function (j) {
+ if (j && j.cases) caseMediaData = { cases: j.cases };
+ renderCaseMedia(caseMediaCurrent);
+ setMsg('case-media-msg', 'บันทึกแล้ว — มีผลในเกมเมื่อรีเฟรช LobbyA/LobbyB', 'ok');
+ }).catch(function (e) {
+ setMsg('case-media-msg', e.message || 'บันทึกไม่ได้', 'error');
+ }).then(function () {
+ if (btn) btn.disabled = false;
+ });
+ }
+
+ (function bindCaseMediaPanel() {
+ caseMediaPopulateSelect();
+ var caseSel = el('case-media-case');
+ if (caseSel) caseSel.addEventListener('change', function () { renderCaseMedia(caseSel.value || '1'); });
+ var saveBtn = el('btn-case-media-save');
+ if (saveBtn) saveBtn.addEventListener('click', saveCaseMediaPanel);
+ })();
+
+ function setTab(name) {
+ document.querySelectorAll('.tab').forEach(function (t) {
+ var on = t.getAttribute('data-tab') === name;
+ t.classList.toggle('is-active', on);
+ t.setAttribute('aria-selected', on ? 'true' : 'false');
+ });
+ document.querySelectorAll('.tab-panel').forEach(function (p) {
+ p.hidden = !p.id || p.id !== 'tab-panel-' + name;
+ });
+ var embedPanels = [
+ { tab: 'map-editor', panel: 'tab-panel-map-editor', fsBtn: 'btn-map-editor-fullscreen' },
+ { tab: 'characters', panel: 'tab-panel-characters', fsBtn: 'btn-character-fullscreen' },
+ ];
+ embedPanels.forEach(function (ep) {
+ if (name === ep.tab) return;
+ var pnl = el(ep.panel);
+ var fsBtn = el(ep.fsBtn);
+ if (pnl && pnl.classList.contains('is-fullscreen')) {
+ pnl.classList.remove('is-fullscreen');
+ if (fsBtn) {
+ fsBtn.textContent = 'เต็มจอ';
+ fsBtn.setAttribute('aria-pressed', 'false');
+ }
+ }
+ });
+ if (name === 'quiz') loadQuizSettingsPanel();
+ if (name === 'special-quiz') { loadSpecialQuizPanel(); loadSpecialQuizIconExpirePanel(); }
+ if (name === 'evidence-cards') loadEvidenceCardsPanel();
+ if (name === 'case-media') loadCaseMediaPanel();
+ if (name === 'quiz-carry') loadQuizCarryPanel();
+ if (name === 'quiz-battle') loadQbBattlePanel();
+ if (name === 'jump-survive') loadJumpSurviveTimingPanel();
+ if (name === 'space-shooter') loadSpaceShooterTimingPanel();
+ if (name === 'mega-virus') loadMegaVirusPanel();
+ if (name === 'game-timing') loadGameTimingPanel();
+ if (name === 'stack-game') loadStackGamePanel();
+ if (name === 'highscore') loadHighscore();
+ if (name === 'achievements') loadAchievementsPanel();
+ if (name === 'vote-timing') loadVoteTimingPanel();
+ if (name === 'troublesome') loadTroublesomePanel();
+ if (name === 'test-mode') { loadForcedMinigamePanel(); loadSpecialCardByMapPanel(); loadTroublesomeForcePanel(); }
+ if (name === 'sound') loadSoundPanel();
+ }
+
+ function boot() {
+ return api('session.php').then(function (s) {
+ if (s.needsSetup) {
+ showPanel('panel-setup');
+ return null;
+ }
+ if (!s.loggedIn) {
+ showPanel('panel-login');
+ return null;
+ }
+ showPanel('panel-app');
+ el('admin-user-label').textContent = (s.admin && s.admin.username) || '';
+ return s.admin;
+ });
+ }
+
+ el('form-setup').addEventListener('submit', function (e) {
+ e.preventDefault();
+ var fd = new FormData(e.target);
+ var p1 = fd.get('password');
+ var p2 = fd.get('password2');
+ if (p1 !== p2) {
+ setMsg('setup-msg', 'รหัสผ่านไม่ตรงกัน', 'error');
+ return;
+ }
+ api('setup.php', { method: 'POST', body: { password: p1 } })
+ .then(function () {
+ setMsg('setup-msg', 'สร้างสำเร็จ — กรุณาล็อกอิน', 'ok');
+ showPanel('panel-login');
+ })
+ .catch(function (err) {
+ setMsg('setup-msg', err.message || 'ผิดพลาด', 'error');
+ });
+ });
+
+ (function bindLoginPasswordToggle() {
+ var inp = el('login-password');
+ var btn = el('login-password-toggle');
+ if (!inp || !btn) return;
+ btn.addEventListener('click', function () {
+ var show = inp.type === 'password';
+ inp.type = show ? 'text' : 'password';
+ btn.setAttribute('aria-pressed', show ? 'true' : 'false');
+ btn.setAttribute('aria-label', show ? 'ซ่อนรหัสผ่าน' : 'แสดงรหัสผ่าน');
+ btn.textContent = show ? 'ซ่อน' : 'แสดง';
+ });
+ })();
+
+ el('form-login').addEventListener('submit', function (e) {
+ e.preventDefault();
+ var fd = new FormData(e.target);
+ api('login.php', {
+ method: 'POST',
+ body: {
+ username: fd.get('username'),
+ password: fd.get('password'),
+ },
+ })
+ .then(function () {
+ setMsg('login-msg', '', '');
+ return boot();
+ })
+ .then(function (admin) {
+ if (admin) loadAll(admin);
+ })
+ .catch(function (err) {
+ setMsg('login-msg', err.message || 'เข้าไม่ได้', 'error');
+ });
+ });
+
+ el('btn-logout').addEventListener('click', function () {
+ api('logout.php', { method: 'POST' })
+ .then(function () {
+ location.reload();
+ })
+ .catch(function () {
+ location.reload();
+ });
+ });
+
+ document.querySelectorAll('.tab').forEach(function (tab) {
+ tab.addEventListener('click', function () {
+ var name = tab.getAttribute('data-tab');
+ if (name === 'change-password' && tab.classList.contains('is-active')) {
+ setTab('oauth');
+ return;
+ }
+ setTab(name);
+ });
+ });
+
+ /* ===== Sidebar: ค้นหาเมนู / ยุบกลุ่ม / เปิด-ปิดบนมือถือ ===== */
+ (function setupAdminSidebar() {
+ var shell = document.querySelector('.admin-shell');
+ var nav = document.querySelector('.admin-tabs');
+ if (!nav) return;
+ var seps = Array.prototype.slice.call(nav.querySelectorAll('.admin-tab-sep'));
+ var tabs = Array.prototype.slice.call(nav.querySelectorAll('.tab'));
+
+ function isMobile() { return window.matchMedia('(max-width: 980px)').matches; }
+ function closeSidebar() {
+ if (shell) shell.classList.remove('sidebar-open');
+ var b = el('btn-sidebar-toggle'); if (b) b.setAttribute('aria-expanded', 'false');
+ var bd = el('admin-sidebar-backdrop'); if (bd) bd.hidden = true;
+ }
+ function openSidebar() {
+ if (shell) shell.classList.add('sidebar-open');
+ var b = el('btn-sidebar-toggle'); if (b) b.setAttribute('aria-expanded', 'true');
+ var bd = el('admin-sidebar-backdrop'); if (bd) bd.hidden = false;
+ }
+ var burger = el('btn-sidebar-toggle');
+ if (burger) burger.addEventListener('click', function () {
+ if (shell && shell.classList.contains('sidebar-open')) closeSidebar(); else openSidebar();
+ });
+ var backdrop = el('admin-sidebar-backdrop');
+ if (backdrop) backdrop.addEventListener('click', closeSidebar);
+ nav.addEventListener('click', function (e) {
+ var t = e.target && e.target.closest ? e.target.closest('.tab') : null;
+ if (t && isMobile()) closeSidebar();
+ });
+
+ /* ยุบ/ขยายกลุ่ม: ซ่อนแท็บที่อยู่หลัง separator จนถึง separator ถัดไป */
+ function applyGroupVisibility() {
+ seps.forEach(function (sep) {
+ var collapsed = sep.classList.contains('is-collapsed');
+ var n = sep.nextElementSibling;
+ while (n && !n.classList.contains('admin-tab-sep')) {
+ if (n.classList && n.classList.contains('tab')) n.classList.toggle('is-group-hidden', collapsed);
+ n = n.nextElementSibling;
+ }
+ });
+ }
+ function toggleGroup(sep) {
+ sep.classList.toggle('is-collapsed');
+ applyGroupVisibility();
+ }
+ seps.forEach(function (sep) {
+ sep.setAttribute('role', 'button');
+ sep.setAttribute('tabindex', '0');
+ sep.addEventListener('click', function () { toggleGroup(sep); });
+ sep.addEventListener('keydown', function (e) {
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleGroup(sep); }
+ });
+ });
+
+ /* ค้นหาเมนู */
+ var search = el('admin-nav-search-input');
+ var emptyMsg = el('admin-nav-empty');
+ if (search) search.addEventListener('input', function () {
+ var q = search.value.trim().toLowerCase();
+ if (q) {
+ var anyVisible = false;
+ tabs.forEach(function (t) {
+ t.classList.remove('is-group-hidden');
+ var hit = (t.textContent || '').toLowerCase().indexOf(q) >= 0;
+ t.classList.toggle('is-search-hidden', !hit);
+ if (hit) anyVisible = true;
+ });
+ seps.forEach(function (s) { s.style.display = 'none'; });
+ if (emptyMsg) emptyMsg.hidden = anyVisible;
+ } else {
+ tabs.forEach(function (t) { t.classList.remove('is-search-hidden'); });
+ seps.forEach(function (s) { s.style.display = ''; });
+ applyGroupVisibility();
+ if (emptyMsg) emptyMsg.hidden = true;
+ }
+ });
+ })();
+
+ function bindEmbedPanelFullscreen(btnId, panelId) {
+ var btn = el(btnId);
+ var panel = el(panelId);
+ if (!btn || !panel) return;
+ function setFs(on) {
+ panel.classList.toggle('is-fullscreen', on);
+ btn.textContent = on ? 'ออกจากเต็มจอ' : 'เต็มจอ';
+ btn.setAttribute('aria-pressed', on ? 'true' : 'false');
+ }
+ btn.addEventListener('click', function () {
+ setFs(!panel.classList.contains('is-fullscreen'));
+ });
+ document.addEventListener('keydown', function (e) {
+ if (e.code !== 'Escape' || !panel.classList.contains('is-fullscreen')) return;
+ setFs(false);
+ });
+ }
+ bindEmbedPanelFullscreen('btn-map-editor-fullscreen', 'tab-panel-map-editor');
+ bindEmbedPanelFullscreen('btn-character-fullscreen', 'tab-panel-characters');
+
+ /* Test Mode toggle — เก็บใน localStorage (share ระหว่าง Admin กับ Game ภายใต้ origin เดียวกัน) */
+ var TEST_MODE_KEY = 'justiceTestMode';
+ function isTestModeOn() {
+ try { return localStorage.getItem(TEST_MODE_KEY) === '1'; } catch (e) { return false; }
+ }
+ function setTestMode(on) {
+ try { localStorage.setItem(TEST_MODE_KEY, on ? '1' : '0'); } catch (e) { /* ignore */ }
+ }
+ function syncTestModeUi() {
+ var chk = el('test-mode-toggle');
+ var lbl = el('test-mode-state-label');
+ var msg = el('test-mode-msg');
+ var on = isTestModeOn();
+ if (chk) chk.checked = on;
+ if (lbl) {
+ lbl.textContent = on ? 'เปิดอยู่ (ON)' : 'ปิดอยู่ (OFF)';
+ lbl.style.color = on ? '#4ade80' : '#94a3b8';
+ }
+ if (msg) {
+ msg.textContent = on
+ ? '✓ Test Mode เปิด — Shortcuts ใช้งานได้ในแท็บอื่นของเบราว์เซอร์นี้'
+ : '○ Test Mode ปิด — Shortcuts ไม่ทำงาน';
+ msg.style.color = on ? '#4ade80' : '#94a3b8';
+ }
+ }
+ var testModeChk = el('test-mode-toggle');
+ if (testModeChk) {
+ testModeChk.addEventListener('change', function () {
+ setTestMode(!!testModeChk.checked);
+ syncTestModeUi();
+ });
+ }
+ var forcedMgSaveBtn = el('btn-forced-minigame-save');
+ if (forcedMgSaveBtn) forcedMgSaveBtn.addEventListener('click', saveForcedMinigamePanel);
+ var specialCardSaveBtn = el('btn-special-card-by-map-save');
+ if (specialCardSaveBtn) specialCardSaveBtn.addEventListener('click', saveSpecialCardByMapPanel);
+ var troublesomeForceSaveBtn = el('btn-troublesome-force-save');
+ if (troublesomeForceSaveBtn) troublesomeForceSaveBtn.addEventListener('click', saveTroublesomeForcePanel);
+ var sqIconExpireSaveBtn = el('btn-special-quiz-icon-expire-save');
+ if (sqIconExpireSaveBtn) sqIconExpireSaveBtn.addEventListener('click', saveSpecialQuizIconExpirePanel);
+ syncTestModeUi();
+ /* ฟัง storage event — sync เมื่อเปิดหลายแท็บ */
+ window.addEventListener('storage', function (e) {
+ if (e && e.key === TEST_MODE_KEY) syncTestModeUi();
+ });
+
+ var btnGameTimingSave = el('btn-game-timing-save');
+ if (btnGameTimingSave) {
+ btnGameTimingSave.addEventListener('click', function () {
+ saveGameTimingPanel();
+ });
+ }
+ var btnJumpSurviveSave = el('btn-jump-survive-save');
+ if (btnJumpSurviveSave) {
+ btnJumpSurviveSave.addEventListener('click', saveJumpSurviveTimingPanel);
+ }
+ bindJumpSurvivePlatformTileInputs();
+ var btnSpaceShooterSave = el('btn-space-shooter-save');
+ if (btnSpaceShooterSave) {
+ btnSpaceShooterSave.addEventListener('click', saveSpaceShooterTimingPanel);
+ }
+ bindSpaceShooterShipUrlInputs();
+ bindStackBlockVisualInputs();
+ bindSpaceShooterDamageOverlayPanel();
+ bindSpaceShooterAsteroidSpritePanel();
+ bindMegaVirusPanel();
+ var btnStackGameSave = el('btn-stack-game-save');
+ if (btnStackGameSave) {
+ btnStackGameSave.addEventListener('click', saveStackGamePanel);
+ }
+ bindGauntletAssetLibrary();
+ bindGameTimingAssignedVisuals();
+ bindGameTimingLaserColorSwatches();
+
+ var btnQuizAdd = el('btn-quiz-admin-add');
+ var btnQuizSave = el('btn-quiz-admin-save');
+ if (btnQuizAdd) {
+ btnQuizAdd.addEventListener('click', function () {
+ addQuizAdminRow('', true);
+ });
+ }
+ if (btnQuizSave) btnQuizSave.addEventListener('click', saveQuizSettingsPanel);
+
+ var btnSpecialQuizAdd = el('btn-special-quiz-add');
+ var btnSpecialQuizSave = el('btn-special-quiz-save');
+ if (btnSpecialQuizAdd) {
+ btnSpecialQuizAdd.addEventListener('click', function () {
+ addSpecialQuizRow({ type: 'tf', text: '', answerTrue: true });
+ updateSpecialQuizCount();
+ });
+ }
+ if (btnSpecialQuizSave) btnSpecialQuizSave.addEventListener('click', saveSpecialQuizPanel);
+ var specialQuizLevelSel = el('special-quiz-level');
+ var specialQuizCaseSel = el('special-quiz-case');
+ if (specialQuizLevelSel) specialQuizLevelSel.addEventListener('change', onSpecialQuizSelectorChange);
+ if (specialQuizCaseSel) specialQuizCaseSel.addEventListener('change', onSpecialQuizSelectorChange);
+
+ var btnQuizCarryAdd = el('btn-quiz-carry-add');
+ var btnQuizCarrySave = el('btn-quiz-carry-save');
+ if (btnQuizCarryAdd) {
+ btnQuizCarryAdd.addEventListener('click', function () {
+ addQuizCarryAdminRow(null);
+ });
+ }
+ if (btnQuizCarrySave) btnQuizCarrySave.addEventListener('click', saveQuizCarryPanel);
+ wireQuizCarryPlaqueMapScaleControls();
+ quizCarryBindThemePickersOnce();
+
+ var btnQbBattleAdd = el('btn-qb-battle-add');
+ var btnQbBattleSave = el('btn-qb-battle-save');
+ if (btnQbBattleAdd) {
+ btnQbBattleAdd.addEventListener('click', function () {
+ addQbBattleRow(null);
+ refreshQbBattlePreview();
+ });
+ }
+ if (btnQbBattleSave) btnQbBattleSave.addEventListener('click', saveQbBattlePanel);
+
+ var qbFilterCat = el('qb-battle-filter-cat');
+ if (qbFilterCat) qbFilterCat.addEventListener('change', applyQbBattleFilter);
+
+ document.querySelectorAll('.qb-preview-mode-btn').forEach(function (b) {
+ b.addEventListener('click', function () {
+ document.querySelectorAll('.qb-preview-mode-btn').forEach(function (x) {
+ x.classList.toggle('is-active', x === b);
+ });
+ refreshQbBattlePreview();
+ });
+ });
+
+ function loadOAuth() {
+ return api('oauth.php').then(function (r) {
+ var o = r.oauth || {};
+ var f = el('form-oauth');
+ ['facebookAppId', 'facebookAppSecret', 'facebookRedirectUri', 'googleClientId', 'googleClientSecret', 'googleRedirectUri'].forEach(function (k) {
+ var inp = f.querySelector('[name="' + k + '"]');
+ if (inp) inp.value = o[k] || '';
+ });
+ });
+ }
+
+ el('form-oauth').addEventListener('submit', function (e) {
+ e.preventDefault();
+ var fd = new FormData(e.target);
+ var body = {};
+ fd.forEach(function (v, k) {
+ body[k] = v;
+ });
+ api('oauth.php', { method: 'PUT', body: body })
+ .then(function () {
+ setMsg('oauth-msg', 'บันทึกแล้ว', 'ok');
+ })
+ .catch(function (err) {
+ setMsg('oauth-msg', err.message || 'บันทึกไม่สำเร็จ', 'error');
+ });
+ });
+
+ function renderAccounts(list) {
+ var tb = el('table-accounts').querySelector('tbody');
+ tb.innerHTML = '';
+ var sumCoins = 0;
+ (list || []).forEach(function (a) {
+ var c = Math.max(0, parseInt(a.coins, 10) || 0);
+ sumCoins += c;
+ var tr = document.createElement('tr');
+ tr.innerHTML =
+ '
แก้แผนที่ห้อง Quiz Battle (qbroom 1–10) — เลือกห้องจากเมนูในตัว editor · วาดเส้นทาง / โดม / จุดเกิด · คลิกซ้ายวาด คลิกขวาลบ
+ตั้งค่าผู้ช่วย AI ในเกม — หน้านี้มีการล็อกอินแยกของตัวเอง (ไม่ใช่ session Admin นี้)
+ใช้กับฉากประเภท เกมตอบคำถาม ใน Editor — จำนวนข้อที่สุ่มต่อรอบตั้งได้ด้านล่าง (สูงสุด 50, ค่าเริ่ม 10) · เวลาเป็นวินาที (เก็บที่เซิร์ฟเวอร์เกม /Game/data/quiz-settings.json) · English: True/false quiz draws up to N shuffled questions per game session.