(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 = '' + escapeHtml(a.displayName || '—') + '' + '' + escapeHtml(a.email || '—') + '' + '' + escapeHtml(a.loginType || '') + '' + '' + escapeHtml(a.providerUserId || '—') + '' + '' + '' + '' + '' + '' + (a.blocked ? 'บล็อก' : 'ปกติ') + '' + ' ' + ''; tb.appendChild(tr); }); var sumEl = el('accounts-coins-summary'); if (sumEl) { sumEl.textContent = (list && list.length) ? ('ผู้ใช้ ' + list.length + ' บัญชี · รวม COINS ' + sumCoins) : 'ยังไม่มีบัญชี'; } tb.querySelectorAll('.btn-sm-del').forEach(function (b) { b.addEventListener('click', function () { if (!confirm('ลบบัญชีนี้?')) return; api('accounts.php?id=' + encodeURIComponent(b.getAttribute('data-id')), { method: 'DELETE' }) .then(loadAccounts) .catch(function (err) { setMsg('accounts-msg', err.message, 'error'); }); }); }); tb.querySelectorAll('.btn-sm-toggle').forEach(function (b) { b.addEventListener('click', function () { var id = b.getAttribute('data-id'); var blocked = b.getAttribute('data-blocked') === '1'; api('accounts.php', { method: 'PATCH', body: { id: id, blocked: !blocked }, }) .then(loadAccounts) .catch(function (err) { setMsg('accounts-msg', err.message, 'error'); }); }); }); tb.querySelectorAll('.btn-coins-save').forEach(function (b) { b.addEventListener('click', function () { var id = b.getAttribute('data-id'); var row = b.closest('tr'); var inp = row ? row.querySelector('input.input-coins') : null; var coins = Math.max(0, parseInt(inp && inp.value, 10) || 0); api('accounts.php', { method: 'PATCH', body: { id: id, coins: coins } }) .then(function () { setMsg('accounts-msg', 'อัปเดต COINS แล้ว', 'ok'); return loadAccounts(); }) .catch(function (err) { setMsg('accounts-msg', err.message, 'error'); }); }); }); } function escapeHtml(s) { var d = document.createElement('div'); d.textContent = s == null ? '' : String(s); return d.innerHTML; } function escapeAttr(s) { return String(s || '').replace(/"/g, '"'); } function loadAccounts() { return api('accounts.php').then(function (r) { renderAccounts(r.accounts); setMsg('accounts-msg', '', ''); }); } /* ===== กระดานผู้นำ (High Score) ===== */ function renderHighscore(rows) { var tbEl = el('table-highscore'); if (!tbEl) return; var tb = tbEl.querySelector('tbody'); tb.innerHTML = ''; (rows || []).forEach(function (r) { var rank = parseInt(r.rank, 10) || 0; var score = Math.max(0, parseInt(r.score, 10) || 0); var coins = Math.max(0, parseInt(r.coins, 10) || 0); var medal = rank === 1 ? '🥇' : rank === 2 ? '🥈' : rank === 3 ? '🥉' : ('#' + rank); var tr = document.createElement('tr'); tr.innerHTML = '' + medal + '' + '' + escapeHtml(r.name || 'ผู้เล่น') + (r.blocked ? ' บล็อก' : '') + '' + '' + '' + '' + '' + '' + coins + '' + ''; tb.appendChild(tr); }); tb.querySelectorAll('.btn-score-save').forEach(function (b) { b.addEventListener('click', function () { var id = b.getAttribute('data-id'); var row = b.closest('tr'); var inp = row ? row.querySelector('input.input-score') : null; var score = Math.max(0, parseInt(inp && inp.value, 10) || 0); api('accounts.php', { method: 'PATCH', body: { id: id, score: score } }) .then(function () { setMsg('highscore-msg', 'บันทึกคะแนนแล้ว', 'ok'); return loadHighscore(); }) .catch(function (err) { setMsg('highscore-msg', err.message, 'error'); }); }); }); tb.querySelectorAll('.btn-score-reset').forEach(function (b) { b.addEventListener('click', function () { var id = b.getAttribute('data-id'); if (!confirm('รีเซ็ตคะแนนคนนี้เป็น 0?')) return; api('leaderboard-admin.php', { method: 'POST', body: { action: 'reset', id: id } }) .then(function () { return loadHighscore(); }) .catch(function (err) { setMsg('highscore-msg', err.message, 'error'); }); }); }); } function loadHighscore() { return api('leaderboard-admin.php').then(function (r) { renderHighscore(r.rows); var n = (r.rows && r.rows.length) || 0; setMsg('highscore-msg', n ? ('ผู้เล่นมีคะแนน ' + r.total + ' คน') : 'ยังไม่มีผู้เล่นทำคะแนน', ''); }).catch(function (err) { setMsg('highscore-msg', err.message, 'error'); }); } (function bindHighscoreControls() { var refresh = el('btn-highscore-refresh'); if (refresh) refresh.addEventListener('click', function () { loadHighscore(); }); var resetAll = el('btn-highscore-reset-all'); if (resetAll) resetAll.addEventListener('click', function () { if (!confirm('รีเซ็ตคะแนนสะสมของผู้เล่นทุกคนเป็น 0? (ย้อนกลับไม่ได้)')) return; api('leaderboard-admin.php', { method: 'POST', body: { action: 'resetAll' } }) .then(function (r) { setMsg('highscore-msg', 'รีเซ็ตแล้ว ' + (r.reset || 0) + ' คน', 'ok'); return loadHighscore(); }) .catch(function (err) { setMsg('highscore-msg', err.message, 'error'); }); }); })(); /* ===== เวลาโหวต (vote timing) ===== */ function loadVoteTimingPanel() { gameTimingFetch('GET') .then(function (data) { var pk = (data && data.pickVoteSec != null) ? Number(data.pickVoteSec) : 25; var tr = (data && data.trialVoteSec != null) ? Number(data.trialVoteSec) : 30; var rv = (data && data.trialResultRevealMs != null) ? Number(data.trialResultRevealMs) : 7000; if (el('vote-timing-pick')) el('vote-timing-pick').value = String(Number.isFinite(pk) ? pk : 25); if (el('vote-timing-trial')) el('vote-timing-trial').value = String(Number.isFinite(tr) ? tr : 30); if (el('vote-timing-reveal')) el('vote-timing-reveal').value = String(Number.isFinite(rv) ? rv : 7000); setMsg('vote-timing-msg', '', ''); }) .catch(function (e) { setMsg('vote-timing-msg', (e.message || 'โหลดไม่ได้') + ' — เช็ค /Game/api/game-timing', 'error'); }); } (function bindVoteTiming() { var btn = el('btn-vote-timing-save'); if (!btn) return; btn.addEventListener('click', function () { var pk = Math.max(5, Math.min(120, parseInt((el('vote-timing-pick') || {}).value, 10) || 25)); var tr = Math.max(5, Math.min(180, parseInt((el('vote-timing-trial') || {}).value, 10) || 30)); var rv = Math.max(1500, Math.min(20000, parseInt((el('vote-timing-reveal') || {}).value, 10) || 7000)); btn.disabled = true; gameTimingFetch('GET') .then(function (data) { data.pickVoteSec = pk; data.trialVoteSec = tr; data.trialResultRevealMs = rv; return gameTimingFetch('PUT', data); }) .then(function () { return gameTimingFetch('GET'); }) .then(function (fresh) { var p = (fresh && fresh.pickVoteSec != null) ? fresh.pickVoteSec : pk; var t = (fresh && fresh.trialVoteSec != null) ? fresh.trialVoteSec : tr; var r = (fresh && fresh.trialResultRevealMs != null) ? fresh.trialResultRevealMs : rv; if (el('vote-timing-pick')) el('vote-timing-pick').value = String(p); if (el('vote-timing-trial')) el('vote-timing-trial').value = String(t); if (el('vote-timing-reveal')) el('vote-timing-reveal').value = String(r); setMsg('vote-timing-msg', 'บันทึกแล้ว — โหวตเลือกผู้เล่น ' + p + 's · ชี้ตัวคนร้าย ' + t + 's · โชว์ผล ' + Math.round(r/1000) + 's · ผู้เล่นต้องรีเฟรช/เข้าห้องใหม่', 'ok'); }) .catch(function (e) { setMsg('vote-timing-msg', (e.message || 'บันทึกไม่ได้') + ' — เช็ค Node + /Game/api/game-timing', 'error'); }) .then(function () { btn.disabled = false; }); }); })(); /* ===== เสียง (sound settings) — master/music/sfx volume 0..1 ===== */ function soundSettingsFetch(method, body) { var m = method || 'GET'; var url = SOUND_SETTINGS_API; if (m === 'GET' && body == null) url += (url.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) { var plain = (text || r.statusText || 'Request failed').trim(); if (r.status === 404) plain += ' — รีสตาร์ท Node (Game/server.js) หลัง deploy'; throw new Error(plain); } if (!r.ok) throw new Error(j.error || ('HTTP ' + r.status + ' ' + r.statusText)); return j; }); }); } function pctFromVol(v, d) { var n = Number(v); return Math.round((Number.isFinite(n) ? n : d) * 100); } function loadSoundPanel() { soundSettingsFetch('GET') .then(function (d) { var setR = function (id, pct) { var e = el(id); if (e) { e.value = String(pct); var lbl = el(id + '-val'); if (lbl) lbl.textContent = pct + '%'; } }; setR('sound-master', pctFromVol(d && d.masterVol, 1.0)); setR('sound-music', pctFromVol(d && d.musicVol, 0.35)); setR('sound-sfx', pctFromVol(d && d.sfxVol, 0.75)); setMsg('sound-msg', '', ''); }) .catch(function (e) { setMsg('sound-msg', (e.message || 'โหลดไม่ได้') + ' — เช็ค /Game/api/sound-settings', 'error'); }); } (function bindSound() { var btn = el('btn-sound-save'); if (!btn) return; btn.addEventListener('click', function () { var rd = function (id, d) { var v = parseInt((el(id) || {}).value, 10); return Math.max(0, Math.min(100, Number.isFinite(v) ? v : d)) / 100; }; var body = { masterVol: rd('sound-master', 100), musicVol: rd('sound-music', 35), sfxVol: rd('sound-sfx', 75) }; btn.disabled = true; soundSettingsFetch('PUT', body) .then(function () { return soundSettingsFetch('GET'); }) .then(function (fresh) { loadSoundPanel(); setMsg('sound-msg', 'บันทึกแล้ว — Master ' + pctFromVol(fresh.masterVol, 1) + '% · Music ' + pctFromVol(fresh.musicVol, .35) + '% · SFX ' + pctFromVol(fresh.sfxVol, .75) + '% · ผู้เล่นต้องรีเฟรช', 'ok'); }) .catch(function (e) { setMsg('sound-msg', (e.message || 'บันทึกไม่ได้') + ' — เช็ค Node + /Game/api/sound-settings', 'error'); }) .then(function () { btn.disabled = false; }); }); })(); /* ===== บทตัวป่วน (แยกจากหมวด Minigame-2) — troublesomeRollMaxSec + troublesomeOfferSec ===== */ function loadTroublesomePanel() { gameTimingFetch('GET') .then(function (data) { var rs = (data && data.troublesomeRollMaxSec != null) ? Number(data.troublesomeRollMaxSec) : 8; var os = (data && data.troublesomeOfferSec != null) ? Number(data.troublesomeOfferSec) : 15; if (el('troublesome-roll-sec')) el('troublesome-roll-sec').value = String(Number.isFinite(rs) ? rs : 8); if (el('troublesome-offer-sec')) el('troublesome-offer-sec').value = String(Number.isFinite(os) ? os : 15); setMsg('troublesome-msg', '', ''); }) .catch(function (e) { setMsg('troublesome-msg', (e.message || 'โหลดไม่ได้') + ' — เช็ค /Game/api/game-timing', 'error'); }); } (function bindTroublesome() { var btn = el('btn-troublesome-save'); if (!btn) return; btn.addEventListener('click', function () { var rs = Math.max(1, Math.min(30, parseInt((el('troublesome-roll-sec') || {}).value, 10) || 8)); var os = Math.max(5, Math.min(120, parseInt((el('troublesome-offer-sec') || {}).value, 10) || 15)); btn.disabled = true; gameTimingFetch('GET') .then(function (data) { data.troublesomeRollMaxSec = rs; data.troublesomeOfferSec = os; return gameTimingFetch('PUT', data); }) .then(function () { return gameTimingFetch('GET'); }) .then(function (fresh) { var r = (fresh && fresh.troublesomeRollMaxSec != null) ? fresh.troublesomeRollMaxSec : rs; var o = (fresh && fresh.troublesomeOfferSec != null) ? fresh.troublesomeOfferSec : os; if (el('troublesome-roll-sec')) el('troublesome-roll-sec').value = String(r); if (el('troublesome-offer-sec')) el('troublesome-offer-sec').value = String(o); setMsg('troublesome-msg', 'บันทึกแล้ว — เวลาเสนอ ' + r + 's · เวลารับ ' + o + 's · ผู้เล่นต้องรีเฟรช/เข้าห้องใหม่', 'ok'); }) .catch(function (e) { setMsg('troublesome-msg', (e.message || 'บันทึกไม่ได้') + ' — เช็ค Node + /Game/api/game-timing', 'error'); }) .then(function () { btn.disabled = false; }); }); })(); /* ===== Achievements admin ===== */ var achvCatalog = []; var achvCatalogById = {}; var achvCurrentPlayer = null; function achvGroupLabel(g) { var names = { 1: '1 · Analyst', 2: '2 · Explorer', 3: '3 · Risk', 4: '4 · Support', 5: '5 · Elite' }; return names[g] || String(g); } function renderAchvCatalog() { var tb = el('table-achv-catalog'); if (!tb) return; tb = tb.querySelector('tbody'); tb.innerHTML = ''; achvCatalog.slice().sort(function (a, b) { return (a.g - b.g) || 0; }).forEach(function (a) { var tr = document.createElement('tr'); tr.setAttribute('data-achv-id', a.id); var tdG = document.createElement('td'); var sel = document.createElement('select'); sel.className = 'achv-f-g'; for (var g = 1; g <= 5; g++) { var op = document.createElement('option'); op.value = String(g); op.textContent = achvGroupLabel(g); if (g === (parseInt(a.g, 10) || 1)) op.selected = true; sel.appendChild(op); } tdG.appendChild(sel); var tdT = document.createElement('td'); var inT = document.createElement('input'); inT.type = 'text'; inT.className = 'achv-f-title'; inT.value = a.title || ''; inT.style.width = '100%'; tdT.appendChild(inT); var tdD = document.createElement('td'); var inD = document.createElement('input'); inD.type = 'text'; inD.className = 'achv-f-desc'; inD.value = a.desc || ''; inD.style.width = '100%'; tdD.appendChild(inD); var tdTar = document.createElement('td'); var inTar = document.createElement('input'); inTar.type = 'number'; inTar.min = '1'; inTar.step = '1'; inTar.className = 'achv-f-target'; inTar.value = String(Math.max(1, parseInt(a.target, 10) || 1)); inTar.style.width = '72px'; tdTar.appendChild(inTar); var tdE = document.createElement('td'); var inE = document.createElement('input'); inE.type = 'checkbox'; inE.className = 'achv-f-enabled'; inE.checked = a.enabled !== false; tdE.appendChild(inE); tr.appendChild(tdG); tr.appendChild(tdT); tr.appendChild(tdD); tr.appendChild(tdTar); tr.appendChild(tdE); tb.appendChild(tr); }); } function collectAchvCatalogFromTable() { var rows = document.querySelectorAll('#table-achv-catalog tbody tr'); var out = []; rows.forEach(function (tr) { out.push({ id: tr.getAttribute('data-achv-id'), g: parseInt(tr.querySelector('.achv-f-g').value, 10) || 1, title: tr.querySelector('.achv-f-title').value, desc: tr.querySelector('.achv-f-desc').value, target: Math.max(1, parseInt(tr.querySelector('.achv-f-target').value, 10) || 1), enabled: tr.querySelector('.achv-f-enabled').checked, }); }); return out; } function loadAchvCatalog() { return api('achievements.php?action=catalog').then(function (r) { achvCatalog = (r && r.catalog) || []; achvCatalogById = {}; achvCatalog.forEach(function (a) { achvCatalogById[a.id] = a; }); renderAchvCatalog(); setMsg('achv-catalog-msg', 'โหลดแคตตาล็อก ' + achvCatalog.length + ' รายการ', ''); }).catch(function (err) { setMsg('achv-catalog-msg', err.message, 'error'); }); } function renderAchvPlayers(rows) { var tb = el('table-achv-players'); if (!tb) return; tb = tb.querySelector('tbody'); tb.innerHTML = ''; (rows || []).forEach(function (p) { var tr = document.createElement('tr'); var c1 = document.createElement('td'); c1.textContent = p.displayName || 'Guest'; var c2 = document.createElement('td'); c2.textContent = p.playerKey; c2.style.fontSize = '12px'; c2.style.opacity = '0.8'; var c3 = document.createElement('td'); c3.textContent = (p.unlocked || 0) + ' / ' + (p.total || 0); var c4 = document.createElement('td'); c4.textContent = String(p.coins || 0); var c5 = document.createElement('td'); var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'btn btn-ghost'; btn.textContent = 'จัดการ'; btn.addEventListener('click', function () { openAchvPlayerDetail(p.playerKey, p.displayName); }); c5.appendChild(btn); tr.appendChild(c1); tr.appendChild(c2); tr.appendChild(c3); tr.appendChild(c4); tr.appendChild(c5); tb.appendChild(tr); }); } function loadAchvPlayers() { var q = (el('achv-player-search') && el('achv-player-search').value || '').trim().toLowerCase(); return api('achievements.php?action=players').then(function (r) { var rows = (r && r.players) || []; if (q) { rows = rows.filter(function (p) { return (p.displayName || '').toLowerCase().indexOf(q) >= 0 || (p.playerKey || '').toLowerCase().indexOf(q) >= 0; }); } renderAchvPlayers(rows); setMsg('achv-players-msg', 'ผู้เล่น ' + rows.length + ' คน', ''); }).catch(function (err) { setMsg('achv-players-msg', err.message, 'error'); }); } function openAchvPlayerDetail(key, name) { achvCurrentPlayer = key; var box = el('achv-player-detail'); if (box) box.hidden = false; if (el('achv-player-detail-name')) el('achv-player-detail-name').textContent = (name || 'Guest') + ' (' + key + ')'; api('achievements.php?action=player&playerKey=' + encodeURIComponent(key)).then(function (r) { var prog = (r && r.progress) || {}; var tb = el('table-achv-player-detail').querySelector('tbody'); tb.innerHTML = ''; achvCatalog.slice().sort(function (a, b) { return (a.g - b.g) || 0; }).forEach(function (a) { var target = Math.max(1, parseInt(a.target, 10) || 1); var cur = Math.max(0, parseInt(prog[a.id], 10) || 0); var tr = document.createElement('tr'); var c1 = document.createElement('td'); c1.innerHTML = '' + achvGroupLabel(a.g).charAt(0) + ' · ' + (a.title || a.id); var c2 = document.createElement('td'); var inp = document.createElement('input'); inp.type = 'number'; inp.min = '0'; inp.max = String(target); inp.step = '1'; inp.value = String(Math.min(cur, target)); inp.style.width = '70px'; c2.appendChild(inp); c2.appendChild(document.createTextNode(' / ' + target + (cur >= target ? ' ✔' : ''))); var c3 = document.createElement('td'); var setBtn = document.createElement('button'); setBtn.type = 'button'; setBtn.className = 'btn btn-ghost'; setBtn.textContent = 'ตั้งค่า'; setBtn.style.marginRight = '6px'; setBtn.addEventListener('click', function () { api('achievements.php', { method: 'POST', body: { action: 'setProgress', playerKey: key, id: a.id, value: parseInt(inp.value, 10) || 0 } }) .then(function () { setMsg('achv-players-msg', 'บันทึก ' + a.id, 'ok'); openAchvPlayerDetail(key, name); }) .catch(function (err) { setMsg('achv-players-msg', err.message, 'error'); }); }); var unBtn = document.createElement('button'); unBtn.type = 'button'; unBtn.className = 'btn btn-primary'; unBtn.textContent = 'ปลดล็อก'; unBtn.addEventListener('click', function () { api('achievements.php', { method: 'POST', body: { action: 'unlock', playerKey: key, id: a.id } }) .then(function () { setMsg('achv-players-msg', 'ปลดล็อก ' + a.id, 'ok'); openAchvPlayerDetail(key, name); }) .catch(function (err) { setMsg('achv-players-msg', err.message, 'error'); }); }); c3.appendChild(setBtn); c3.appendChild(unBtn); tr.appendChild(c1); tr.appendChild(c2); tr.appendChild(c3); tb.appendChild(tr); }); }).catch(function (err) { setMsg('achv-players-msg', err.message, 'error'); }); } function showAchvSubtab(which) { var isCat = which !== 'players'; if (el('achv-panel-catalog')) el('achv-panel-catalog').hidden = !isCat; if (el('achv-panel-players')) el('achv-panel-players').hidden = isCat; if (el('achv-subtab-catalog')) el('achv-subtab-catalog').classList.toggle('is-active', isCat); if (el('achv-subtab-players')) el('achv-subtab-players').classList.toggle('is-active', !isCat); if (!isCat) loadAchvPlayers(); } var achvPanelInited = false; function loadAchievementsPanel() { if (!achvPanelInited) { achvPanelInited = true; var sc = el('achv-subtab-catalog'); if (sc) sc.addEventListener('click', function () { showAchvSubtab('catalog'); }); var sp = el('achv-subtab-players'); if (sp) sp.addEventListener('click', function () { showAchvSubtab('players'); }); var save = el('btn-achv-save'); if (save) save.addEventListener('click', function () { api('achievements.php', { method: 'POST', body: { action: 'saveCatalog', catalog: collectAchvCatalogFromTable() } }) .then(function (r) { achvCatalog = (r && r.catalog) || achvCatalog; achvCatalogById = {}; achvCatalog.forEach(function (a) { achvCatalogById[a.id] = a; }); renderAchvCatalog(); setMsg('achv-catalog-msg', 'บันทึกแคตตาล็อกแล้ว', 'ok'); }) .catch(function (err) { setMsg('achv-catalog-msg', err.message, 'error'); }); }); var reload = el('btn-achv-reload'); if (reload) reload.addEventListener('click', function () { loadAchvCatalog(); }); var resetDef = el('btn-achv-reset-default'); if (resetDef) resetDef.addEventListener('click', function () { if (!confirm('คืนค่าแคตตาล็อกกลับเป็นค่าเริ่มต้น 25 รายการ? (ค่าที่แก้ไว้จะหาย)')) return; api('achievements.php', { method: 'POST', body: { action: 'resetCatalog' } }) .then(function () { setMsg('achv-catalog-msg', 'คืนค่าเริ่มต้นแล้ว', 'ok'); return loadAchvCatalog(); }) .catch(function (err) { setMsg('achv-catalog-msg', err.message, 'error'); }); }); var pr = el('btn-achv-players-refresh'); if (pr) pr.addEventListener('click', function () { loadAchvPlayers(); }); var ps = el('achv-player-search'); if (ps) ps.addEventListener('input', function () { loadAchvPlayers(); }); var rpb = el('btn-achv-player-reset'); if (rpb) rpb.addEventListener('click', function () { if (!achvCurrentPlayer) return; if (!confirm('รีเซ็ต achievement ทั้งหมดของผู้เล่นนี้?')) return; api('achievements.php', { method: 'POST', body: { action: 'resetPlayer', playerKey: achvCurrentPlayer } }) .then(function () { setMsg('achv-players-msg', 'รีเซ็ตแล้ว', 'ok'); openAchvPlayerDetail(achvCurrentPlayer, el('achv-player-detail-name') ? el('achv-player-detail-name').textContent : ''); loadAchvPlayers(); }) .catch(function (err) { setMsg('achv-players-msg', err.message, 'error'); }); }); } return loadAchvCatalog(); } el('form-account-add').addEventListener('submit', function (e) { e.preventDefault(); var fd = new FormData(e.target); var body = { email: fd.get('email'), displayName: fd.get('displayName'), loginType: fd.get('loginType'), providerUserId: fd.get('providerUserId'), notes: fd.get('notes'), blocked: fd.get('blocked') === 'on', coins: Math.max(0, parseInt(fd.get('coins'), 10) || 0), }; api('accounts.php', { method: 'POST', body: body }) .then(function () { e.target.reset(); return loadAccounts(); }) .catch(function (err) { setMsg('accounts-msg', err.message, 'error'); }); }); function renderAdmins(list, myId) { var tb = el('table-admins').querySelector('tbody'); tb.innerHTML = ''; (list || []).forEach(function (a) { var tr = document.createElement('tr'); var delBtn = a.id === myId ? '—' : ''; tr.innerHTML = '' + escapeHtml(a.username) + '' + '' + escapeHtml(a.role || 'admin') + '' + '' + escapeHtml((a.createdAt || '').slice(0, 10)) + '' + '' + delBtn + ''; tb.appendChild(tr); }); tb.querySelectorAll('.btn-adm-del').forEach(function (b) { b.addEventListener('click', function () { if (!confirm('ลบแอดมินนี้?')) return; api('admins.php?id=' + encodeURIComponent(b.getAttribute('data-id')), { method: 'DELETE' }) .then(loadAdmins) .catch(function (err) { setMsg('admins-msg', err.message, 'error'); }); }); }); } function fillAdminResetSelect(admins, myId) { var sel = el('select-admin-reset-target'); if (!sel) return; sel.innerHTML = ''; (admins || []).forEach(function (a) { if (!a.id || a.id === myId) return; var opt = document.createElement('option'); opt.value = a.id; opt.textContent = a.username + ' (' + (a.role || 'admin') + ')'; sel.appendChild(opt); }); } function loadAdmins() { return api('admins.php').then(function (r) { return api('session.php').then(function (s) { var myId = s.admin && s.admin.id; renderAdmins(r.admins, myId); fillAdminResetSelect(r.admins, myId); setMsg('admins-msg', '', ''); }); }); } el('form-change-password').addEventListener('submit', function (e) { e.preventDefault(); var fd = new FormData(e.target); var cur = fd.get('currentPassword'); var n1 = fd.get('newPassword'); var n2 = fd.get('newPassword2'); if (n1 !== n2) { setMsg('password-self-msg', 'รหัสใหม่ไม่ตรงกัน', 'error'); return; } api('password.php', { method: 'POST', body: { currentPassword: cur, newPassword: n1 }, }) .then(function () { e.target.reset(); setMsg('password-self-msg', 'เปลี่ยนรหัสผ่านแล้ว', 'ok'); }) .catch(function (err) { setMsg('password-self-msg', err.message, 'error'); }); }); el('form-admin-reset-other').addEventListener('submit', function (e) { e.preventDefault(); var fd = new FormData(e.target); var tid = fd.get('targetId'); var n1 = fd.get('newPassword'); var n2 = fd.get('newPassword2'); if (!tid) { setMsg('admins-msg', 'เลือกแอดมิน', 'error'); return; } if (n1 !== n2) { setMsg('admins-msg', 'รหัสใหม่ไม่ตรงกัน', 'error'); return; } api('admins.php', { method: 'PATCH', body: { id: tid, newPassword: n1 }, }) .then(function () { e.target.reset(); setMsg('admins-msg', 'ตั้งรหัสใหม่ให้แอดมินที่เลือกแล้ว', 'ok'); }) .catch(function (err) { setMsg('admins-msg', err.message, 'error'); }); }); el('form-admin-add').addEventListener('submit', function (e) { e.preventDefault(); var fd = new FormData(e.target); api('admins.php', { method: 'POST', body: { username: fd.get('username'), password: fd.get('password'), role: fd.get('role'), }, }) .then(function () { e.target.reset(); return loadAdmins(); }) .catch(function (err) { setMsg('admins-msg', err.message, 'error'); }); }); function getDesiredAdminTab(admin) { var want = null; try { want = new URL(window.location.href).searchParams.get('tab'); } catch (e) { /* ignore */ } var allowed = { 'change-password': true, oauth: true, 'map-editor': true, quiz: true, 'special-quiz': true, 'vote-timing': true, troublesome: true, 'quiz-carry': true, 'quiz-battle': true, 'jump-survive': true, 'space-shooter': true, 'mega-virus': true, 'game-timing': true, 'stack-game': true, characters: true, accounts: true, admins: true }; if (!want || !allowed[want]) return 'oauth'; if (want === 'admins' && (!admin || admin.role !== 'super')) return 'oauth'; return want; } function loadAll(admin) { loadOAuth().catch(function (e) { setMsg('oauth-msg', e.message, 'error'); }); loadAccounts().catch(function (e) { setMsg('accounts-msg', e.message, 'error'); }); var isSuper = admin && admin.role === 'super'; el('tab-admins').hidden = !isSuper; el('form-admin-add').hidden = !isSuper; if (isSuper) { loadAdmins().catch(function (e) { setMsg('admins-msg', e.message, 'error'); }); } setTab(getDesiredAdminTab(admin)); /** อย่า prefetch ซิงก์ Stack ที่นี่ — GET ที่เริ่มก่อน PUT บันทึกอาจกลับช้ากว่าและทับช่องกว้าง/HZ ด้วยข้อมูลเก่า · โหลดจริงที่แท็บ Stack / เวลาเกมแทน */ } boot().then(function (admin) { if (admin) loadAll(admin); }); })();