(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 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');
});
}
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: 'cybercrime', labelTh: 'อาชญากรรมออนไลน์', labelEn: 'Cybercrime' },
{ id: 'cyber_law', labelTh: 'กฎหมายไซเบอร์', labelEn: 'Cyber law' },
{ id: 'privacy', labelTh: 'ความเป็นส่วนตัวและข้อมูลส่วนบุคคล', labelEn: 'Privacy & PDPA' },
{ id: 'digital_security', labelTh: 'ความปลอดภัยดิจิทัล', labelEn: 'Digital security' },
{ id: 'social_media', labelTh: 'สื่อสังคมออนไลน์', labelEn: 'Social media' },
{ id: 'ethics', labelTh: 'จริยธรรมดิจิทัล', labelEn: 'Digital ethics' },
{ id: 'ai_data', labelTh: 'AI และข้อมูล', labelEn: 'AI & data' },
];
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 : 'cybercrime',
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('cybercrime');
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 || 'cybercrime');
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 || 'cybercrime') === 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 <= 6; si++) {
var nin = el('stack-block-normal-url-' + si);
var hin = el('stack-block-heavy-url-' + si);
if (nin) nin.value = normArr[si - 1] != null ? String(normArr[si - 1]) : '';
if (hin) hin.value = heavyArr[si - 1] != null ? String(heavyArr[si - 1]) : '';
updateStackBlockSeatPreview(si);
}
}
function readStackBlockNormalUrlsFromForm() {
var out = [];
for (var i = 1; i <= 6; 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 <= 6; 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 <= 6; 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);
el('game-timing-limit-sec').value = String(data.gauntletTimeLimitSec != null ? data.gauntletTimeLimitSec : 0);
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 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);
}
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 (!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 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()
: '');
gameTimingFetch('GET')
.then(function (data) {
data.balloonBossMissionTimeSec = limSs;
data.balloonBossBossImageUrl = bossUrl;
data.balloonBossPlayerBalloonImageUrls = balloonUrls;
data.balloonBossPlayerBalloonFallbackUrl = fbUrl;
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 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));
gameTimingFetch('GET')
.then(function (data) {
data.jumpSurviveJumpHeightMult = jumpSurvMult;
data.jumpSurviveMissionTimeSec = limJump;
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(4, 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.gauntletTimeLimitSec = limS;
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;
});
el('top-actions').hidden = name !== 'panel-app';
}
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 === '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();
}
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);
});
});
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');
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);
}
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 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 =
'