1893 lines
73 KiB
JavaScript
1893 lines
73 KiB
JavaScript
(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';
|
||
var stackGameSaveInFlight = 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)));
|
||
renderQuizAdminQuestions(data.questions);
|
||
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 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,
|
||
questions: questions,
|
||
}).then(function () {
|
||
setMsg('quiz-settings-msg', 'บันทึกแล้ว', 'ok');
|
||
}).catch(function (e) {
|
||
setMsg('quiz-settings-msg', e.message || 'บันทึกไม่ได้', 'error');
|
||
});
|
||
}
|
||
|
||
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 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);
|
||
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);
|
||
});
|
||
}
|
||
|
||
function loadQuizCarryPanel() {
|
||
gameQuizFetch('GET').then(function (data) {
|
||
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);
|
||
}
|
||
setMsg('quiz-carry-settings-msg', '', '');
|
||
}).catch(function (e) {
|
||
setMsg('quiz-carry-settings-msg', e.message || 'โหลดไม่ได้', 'error');
|
||
});
|
||
}
|
||
|
||
function saveQuizCarryPanel() {
|
||
var list = el('quiz-carry-admin-list');
|
||
if (!list) 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-inp').forEach(function (inp, idx) {
|
||
var v = inp.value ? String(inp.value).trim() : '';
|
||
if (v) slots.push({ idx: idx, text: v });
|
||
});
|
||
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;
|
||
carryQuestions.push({
|
||
text: text,
|
||
choices: slots.map(function (s) {
|
||
return s.text;
|
||
}),
|
||
correctIndex: correctIndex,
|
||
});
|
||
});
|
||
|
||
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;
|
||
|
||
gameQuizFetch('PUT', {
|
||
carryQuestions: carryQuestions,
|
||
carryReadMs: carryReadMs,
|
||
carryAnswerMs: carryAnswerMs,
|
||
carrySessionLength: carrySessionLength,
|
||
}).then(function () {
|
||
setMsg('quiz-carry-settings-msg', 'บันทึกแล้ว', 'ok');
|
||
}).catch(function (e) {
|
||
setMsg('quiz-carry-settings-msg', e.message || 'บันทึกไม่ได้', 'error');
|
||
});
|
||
}
|
||
|
||
/** หมวด 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);
|
||
});
|
||
}
|
||
|
||
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);
|
||
});
|
||
}
|
||
|
||
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 = {};
|
||
try {
|
||
j = text ? JSON.parse(text) : {};
|
||
} catch (e) {
|
||
if (!r.ok) throw new Error(text || r.statusText);
|
||
throw e;
|
||
}
|
||
if (!r.ok) {
|
||
var plain = j.error || r.statusText || 'Request failed';
|
||
if (r.status === 404) {
|
||
plain += ' — รีสตาร์ท Node ที่รัน Game/server.js หลัง deploy · Restart Game Node';
|
||
}
|
||
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) : '';
|
||
}
|
||
}
|
||
|
||
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 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;
|
||
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'
|
||
: 'อัตโนมัติ (จากแผนที่)';
|
||
setMsg('stack-game-msg', 'บันทึกสำเร็จ — สวิง ' + hz + ' Hz · กว้าง ' + wLabel + ' · โหลดยืนยันจากเซิร์ฟเวอร์แล้ว · ผู้เล่นต้องรีเฟรชหน้าเล่นหรือเข้าห้องใหม่', '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();
|
||
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);
|
||
}
|
||
setMsg('jump-survive-timing-msg', '', '');
|
||
})
|
||
.catch(function (e) {
|
||
setMsg('jump-survive-timing-msg', e.message || 'โหลดไม่ได้', 'error');
|
||
});
|
||
}
|
||
|
||
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;
|
||
gameTimingFetch('GET')
|
||
.then(function (data) {
|
||
data.jumpSurviveJumpHeightMult = jumpSurvMult;
|
||
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·กระโดด / รูปอุปสรรค์ · กระโดดให้รอดตั้งที่แท็บกระโดดให้รอด · ค่า Stack แก้ที่แท็บ Stack เท่านั้น · จำกัดเวลาใช้กับรอบถัดไปที่โฮสต์เริ่มเกม', '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 === '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 btnStackGameSave = el('btn-stack-game-save');
|
||
if (btnStackGameSave) {
|
||
btnStackGameSave.addEventListener('click', saveStackGamePanel);
|
||
}
|
||
bindGauntletAssetLibrary();
|
||
bindGameTimingAssignedVisuals();
|
||
|
||
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);
|
||
|
||
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 =
|
||
'<td>' + escapeHtml(a.displayName || '—') + '</td>' +
|
||
'<td>' + escapeHtml(a.email || '—') + '</td>' +
|
||
'<td>' + escapeHtml(a.loginType || '') + '</td>' +
|
||
'<td><small>' + escapeHtml(a.providerUserId || '—') + '</small></td>' +
|
||
'<td class="td-coins">' +
|
||
'<input type="number" class="input-coins" min="0" step="1" value="' + c + '" data-account-id="' + escapeAttr(a.id) + '" aria-label="COINS">' +
|
||
'<button type="button" class="btn btn-primary btn-coins-save" data-id="' + escapeAttr(a.id) + '">บันทึก COINS</button>' +
|
||
'</td>' +
|
||
'<td>' + (a.blocked ? '<span class="msg error">บล็อก</span>' : 'ปกติ') + '</td>' +
|
||
'<td><button type="button" class="btn btn-danger btn-sm-del" data-id="' + escapeAttr(a.id) + '">ลบ</button> ' +
|
||
'<button type="button" class="btn btn-ghost btn-sm-toggle" data-id="' + escapeAttr(a.id) + '" data-blocked="' + (a.blocked ? '1' : '0') + '">' + (a.blocked ? 'ปลดบล็อก' : 'บล็อก') + '</button></td>';
|
||
tb.appendChild(tr);
|
||
});
|
||
var sumEl = el('accounts-coins-summary');
|
||
if (sumEl) {
|
||
sumEl.textContent = (list && list.length) ? ('ผู้ใช้ ' + list.length + ' บัญชี · รวม COINS ' + sumCoins) : 'ยังไม่มีบัญชี';
|
||
}
|
||
tb.querySelectorAll('.btn-sm-del').forEach(function (b) {
|
||
b.addEventListener('click', function () {
|
||
if (!confirm('ลบบัญชีนี้?')) return;
|
||
api('accounts.php?id=' + encodeURIComponent(b.getAttribute('data-id')), { method: 'DELETE' })
|
||
.then(loadAccounts)
|
||
.catch(function (err) {
|
||
setMsg('accounts-msg', err.message, 'error');
|
||
});
|
||
});
|
||
});
|
||
tb.querySelectorAll('.btn-sm-toggle').forEach(function (b) {
|
||
b.addEventListener('click', function () {
|
||
var id = b.getAttribute('data-id');
|
||
var blocked = b.getAttribute('data-blocked') === '1';
|
||
api('accounts.php', {
|
||
method: 'PATCH',
|
||
body: { id: id, blocked: !blocked },
|
||
})
|
||
.then(loadAccounts)
|
||
.catch(function (err) {
|
||
setMsg('accounts-msg', err.message, 'error');
|
||
});
|
||
});
|
||
});
|
||
tb.querySelectorAll('.btn-coins-save').forEach(function (b) {
|
||
b.addEventListener('click', function () {
|
||
var id = b.getAttribute('data-id');
|
||
var row = b.closest('tr');
|
||
var inp = row ? row.querySelector('input.input-coins') : null;
|
||
var coins = Math.max(0, parseInt(inp && inp.value, 10) || 0);
|
||
api('accounts.php', { method: 'PATCH', body: { id: id, coins: coins } })
|
||
.then(function () {
|
||
setMsg('accounts-msg', 'อัปเดต COINS แล้ว', 'ok');
|
||
return loadAccounts();
|
||
})
|
||
.catch(function (err) {
|
||
setMsg('accounts-msg', err.message, 'error');
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
function escapeHtml(s) {
|
||
var d = document.createElement('div');
|
||
d.textContent = s == null ? '' : String(s);
|
||
return d.innerHTML;
|
||
}
|
||
|
||
function escapeAttr(s) {
|
||
return String(s || '').replace(/"/g, '"');
|
||
}
|
||
|
||
function loadAccounts() {
|
||
return api('accounts.php').then(function (r) {
|
||
renderAccounts(r.accounts);
|
||
setMsg('accounts-msg', '', '');
|
||
});
|
||
}
|
||
|
||
el('form-account-add').addEventListener('submit', function (e) {
|
||
e.preventDefault();
|
||
var fd = new FormData(e.target);
|
||
var body = {
|
||
email: fd.get('email'),
|
||
displayName: fd.get('displayName'),
|
||
loginType: fd.get('loginType'),
|
||
providerUserId: fd.get('providerUserId'),
|
||
notes: fd.get('notes'),
|
||
blocked: fd.get('blocked') === 'on',
|
||
coins: Math.max(0, parseInt(fd.get('coins'), 10) || 0),
|
||
};
|
||
api('accounts.php', { method: 'POST', body: body })
|
||
.then(function () {
|
||
e.target.reset();
|
||
return loadAccounts();
|
||
})
|
||
.catch(function (err) {
|
||
setMsg('accounts-msg', err.message, 'error');
|
||
});
|
||
});
|
||
|
||
function renderAdmins(list, myId) {
|
||
var tb = el('table-admins').querySelector('tbody');
|
||
tb.innerHTML = '';
|
||
(list || []).forEach(function (a) {
|
||
var tr = document.createElement('tr');
|
||
var delBtn = a.id === myId
|
||
? '—'
|
||
: '<button type="button" class="btn btn-danger btn-adm-del" data-id="' + escapeAttr(a.id) + '">ลบ</button>';
|
||
tr.innerHTML =
|
||
'<td>' + escapeHtml(a.username) + '</td>' +
|
||
'<td>' + escapeHtml(a.role || 'admin') + '</td>' +
|
||
'<td><small>' + escapeHtml((a.createdAt || '').slice(0, 10)) + '</small></td>' +
|
||
'<td>' + delBtn + '</td>';
|
||
tb.appendChild(tr);
|
||
});
|
||
tb.querySelectorAll('.btn-adm-del').forEach(function (b) {
|
||
b.addEventListener('click', function () {
|
||
if (!confirm('ลบแอดมินนี้?')) return;
|
||
api('admins.php?id=' + encodeURIComponent(b.getAttribute('data-id')), { method: 'DELETE' })
|
||
.then(loadAdmins)
|
||
.catch(function (err) {
|
||
setMsg('admins-msg', err.message, 'error');
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
function fillAdminResetSelect(admins, myId) {
|
||
var sel = el('select-admin-reset-target');
|
||
if (!sel) return;
|
||
sel.innerHTML = '<option value="">— เลือก —</option>';
|
||
(admins || []).forEach(function (a) {
|
||
if (!a.id || a.id === myId) return;
|
||
var opt = document.createElement('option');
|
||
opt.value = a.id;
|
||
opt.textContent = a.username + ' (' + (a.role || 'admin') + ')';
|
||
sel.appendChild(opt);
|
||
});
|
||
}
|
||
|
||
function loadAdmins() {
|
||
return api('admins.php').then(function (r) {
|
||
return api('session.php').then(function (s) {
|
||
var myId = s.admin && s.admin.id;
|
||
renderAdmins(r.admins, myId);
|
||
fillAdminResetSelect(r.admins, myId);
|
||
setMsg('admins-msg', '', '');
|
||
});
|
||
});
|
||
}
|
||
|
||
el('form-change-password').addEventListener('submit', function (e) {
|
||
e.preventDefault();
|
||
var fd = new FormData(e.target);
|
||
var cur = fd.get('currentPassword');
|
||
var n1 = fd.get('newPassword');
|
||
var n2 = fd.get('newPassword2');
|
||
if (n1 !== n2) {
|
||
setMsg('password-self-msg', 'รหัสใหม่ไม่ตรงกัน', 'error');
|
||
return;
|
||
}
|
||
api('password.php', {
|
||
method: 'POST',
|
||
body: { currentPassword: cur, newPassword: n1 },
|
||
})
|
||
.then(function () {
|
||
e.target.reset();
|
||
setMsg('password-self-msg', 'เปลี่ยนรหัสผ่านแล้ว', 'ok');
|
||
})
|
||
.catch(function (err) {
|
||
setMsg('password-self-msg', err.message, 'error');
|
||
});
|
||
});
|
||
|
||
el('form-admin-reset-other').addEventListener('submit', function (e) {
|
||
e.preventDefault();
|
||
var fd = new FormData(e.target);
|
||
var tid = fd.get('targetId');
|
||
var n1 = fd.get('newPassword');
|
||
var n2 = fd.get('newPassword2');
|
||
if (!tid) {
|
||
setMsg('admins-msg', 'เลือกแอดมิน', 'error');
|
||
return;
|
||
}
|
||
if (n1 !== n2) {
|
||
setMsg('admins-msg', 'รหัสใหม่ไม่ตรงกัน', 'error');
|
||
return;
|
||
}
|
||
api('admins.php', {
|
||
method: 'PATCH',
|
||
body: { id: tid, newPassword: n1 },
|
||
})
|
||
.then(function () {
|
||
e.target.reset();
|
||
setMsg('admins-msg', 'ตั้งรหัสใหม่ให้แอดมินที่เลือกแล้ว', 'ok');
|
||
})
|
||
.catch(function (err) {
|
||
setMsg('admins-msg', err.message, 'error');
|
||
});
|
||
});
|
||
|
||
el('form-admin-add').addEventListener('submit', function (e) {
|
||
e.preventDefault();
|
||
var fd = new FormData(e.target);
|
||
api('admins.php', {
|
||
method: 'POST',
|
||
body: {
|
||
username: fd.get('username'),
|
||
password: fd.get('password'),
|
||
role: fd.get('role'),
|
||
},
|
||
})
|
||
.then(function () {
|
||
e.target.reset();
|
||
return loadAdmins();
|
||
})
|
||
.catch(function (err) {
|
||
setMsg('admins-msg', err.message, 'error');
|
||
});
|
||
});
|
||
|
||
function getDesiredAdminTab(admin) {
|
||
var want = null;
|
||
try {
|
||
want = new URL(window.location.href).searchParams.get('tab');
|
||
} catch (e) { /* ignore */ }
|
||
var allowed = { 'change-password': true, oauth: true, 'map-editor': true, quiz: true, 'quiz-carry': true, 'quiz-battle': true, 'jump-survive': true, 'game-timing': true, 'stack-game': true, characters: true, accounts: true, admins: true };
|
||
if (!want || !allowed[want]) return 'oauth';
|
||
if (want === 'admins' && (!admin || admin.role !== 'super')) return 'oauth';
|
||
return want;
|
||
}
|
||
|
||
function loadAll(admin) {
|
||
loadOAuth().catch(function (e) {
|
||
setMsg('oauth-msg', e.message, 'error');
|
||
});
|
||
loadAccounts().catch(function (e) {
|
||
setMsg('accounts-msg', e.message, 'error');
|
||
});
|
||
var isSuper = admin && admin.role === 'super';
|
||
el('tab-admins').hidden = !isSuper;
|
||
el('form-admin-add').hidden = !isSuper;
|
||
if (isSuper) {
|
||
loadAdmins().catch(function (e) {
|
||
setMsg('admins-msg', e.message, 'error');
|
||
});
|
||
}
|
||
setTab(getDesiredAdminTab(admin));
|
||
/** อย่า prefetch ซิงก์ Stack ที่นี่ — GET ที่เริ่มก่อน PUT บันทึกอาจกลับช้ากว่าและทับช่องกว้าง/HZ ด้วยข้อมูลเก่า · โหลดจริงที่แท็บ Stack / เวลาเกมแทน */
|
||
}
|
||
|
||
boot().then(function (admin) {
|
||
if (admin) loadAll(admin);
|
||
});
|
||
})();
|