minigame 4 colour question
This commit is contained in:
@@ -63,6 +63,16 @@ server {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
# ชื่อไฟล์ .php อยู่ใต้ /Game/ ไม่ใช่ /Game/api/ — fastcgi จะหา $document_root/Game/... ผิดที่ (ไฟล์จริงใน Game/public/) → ส่งไป Node แทน
|
||||
location = /Game/api-quiz-carry-from-disk.php {
|
||||
client_max_body_size 20M;
|
||||
proxy_pass http://127.0.0.1:3001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
location ~ ^/Game/(?!socket\.io/)(.+\.(html|js))$ {
|
||||
alias /var/www/html/Game/public/$1;
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||
|
||||
@@ -1466,6 +1466,73 @@ code {
|
||||
}
|
||||
}
|
||||
|
||||
/* —— Quiz carry: map panel theme (color + alpha) —— */
|
||||
.quiz-carry-theme-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.85rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.quiz-carry-theme-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(7.5rem, 10rem) 1fr;
|
||||
gap: 0.65rem 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
.quiz-carry-theme-row--narrow {
|
||||
grid-template-columns: 1fr;
|
||||
max-width: 12rem;
|
||||
}
|
||||
.quiz-carry-theme-label {
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
.quiz-carry-color-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.65rem 1rem;
|
||||
}
|
||||
.quiz-carry-color-controls input[type='color'] {
|
||||
width: 44px;
|
||||
height: 36px;
|
||||
padding: 2px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--card);
|
||||
cursor: pointer;
|
||||
}
|
||||
.quiz-carry-alpha-label {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.35rem 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--muted);
|
||||
flex: 1;
|
||||
min-width: min(100%, 220px);
|
||||
}
|
||||
.quiz-carry-alpha-label input[type='range'] {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
max-width: 280px;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
.quiz-carry-alpha-out {
|
||||
font-variant-numeric: tabular-nums;
|
||||
min-width: 2.75rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text);
|
||||
}
|
||||
.quiz-carry-theme-code {
|
||||
font-size: 0.72rem;
|
||||
color: var(--muted);
|
||||
word-break: break-all;
|
||||
max-width: 100%;
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
/* —— Quiz Battle admin (โดม + ป๊อปอัป cyber) —— */
|
||||
.tab-panel-quiz-battle {
|
||||
--qb-cyan: #00f2ff;
|
||||
|
||||
+246
-7
@@ -352,8 +352,194 @@
|
||||
});
|
||||
}
|
||||
|
||||
function loadQuizCarryPanel() {
|
||||
gameQuizFetch('GET').then(function (data) {
|
||||
/** ค่าเริ่มต้น 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)));
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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') {
|
||||
data.carryMapPanelTheme = opts.themeOverride;
|
||||
}
|
||||
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;
|
||||
}
|
||||
renderQuizCarryAdminList(data.carryQuestions || []);
|
||||
var readSec = el('quiz-carry-read-sec');
|
||||
var ansSec = el('quiz-carry-answer-sec');
|
||||
@@ -370,10 +556,23 @@
|
||||
var sl = parseInt(String(data.carrySessionLength), 10);
|
||||
sessLen.value = String(Number.isFinite(sl) && sl >= 0 ? sl : 0);
|
||||
}
|
||||
setMsg('quiz-carry-settings-msg', '', '');
|
||||
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);
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
function saveQuizCarryPanel() {
|
||||
@@ -426,13 +625,52 @@
|
||||
if (!Number.isFinite(carrySessionLength) || carrySessionLength < 0) carrySessionLength = 0;
|
||||
if (carrySessionLength > 500) carrySessionLength = 500;
|
||||
|
||||
gameQuizFetch('PUT', {
|
||||
carryQuestions: carryQuestions,
|
||||
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;
|
||||
}
|
||||
|
||||
var putBody = {
|
||||
carryReadMs: carryReadMs,
|
||||
carryAnswerMs: carryAnswerMs,
|
||||
carrySessionLength: carrySessionLength,
|
||||
}).then(function () {
|
||||
setMsg('quiz-carry-settings-msg', 'บันทึกแล้ว', 'ok');
|
||||
carryMapPanelTheme: carryMapPanelTheme,
|
||||
};
|
||||
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 themeOk = !!themeFromRes;
|
||||
var themeTail = themeOk ? ' · ธีมแผงบันทึกแล้ว' : '';
|
||||
setMsg('quiz-carry-settings-msg', 'บันทึกแล้ว' + tail + themeTail, 'ok');
|
||||
loadQuizCarryPanel({
|
||||
clearMsg: false,
|
||||
themeOverride: themeFromRes || carryMapPanelTheme,
|
||||
timingOverride: {
|
||||
carryReadMs: carryReadMs,
|
||||
carryAnswerMs: carryAnswerMs,
|
||||
carrySessionLength: carrySessionLength,
|
||||
},
|
||||
fetchDelayMs: 120,
|
||||
});
|
||||
}).catch(function (e) {
|
||||
setMsg('quiz-carry-settings-msg', e.message || 'บันทึกไม่ได้', 'error');
|
||||
});
|
||||
@@ -1578,6 +1816,7 @@
|
||||
});
|
||||
}
|
||||
if (btnQuizCarrySave) btnQuizCarrySave.addEventListener('click', saveQuizCarryPanel);
|
||||
quizCarryBindThemePickersOnce();
|
||||
|
||||
var btnQbBattleAdd = el('btn-qb-battle-add');
|
||||
var btnQbBattleSave = el('btn-qb-battle-save');
|
||||
|
||||
@@ -6,7 +6,7 @@ declare(strict_types=1);
|
||||
* แก้กรณี nginx ส่ง /Game/ เป็น static แล้ว PUT ไม่ถึง Node — เบราว์เซอร์เรียก PHP แทน
|
||||
*
|
||||
* รองรับ Node เวอร์ชันเก่าที่ GET ไม่ส่ง battleQuizMcq / PUT เขียนทับไฟล์แล้วตัดฟิลด์:
|
||||
* - GET: ดึง battleQuizMcq จากไฟล์บนดิสก์มาแปะในคำตอบ
|
||||
* - GET: ดึงจากดิสก์มาแปะทับบางฟิลด์ (battleQuizMcq + carry*) เพราะหลัง PUT เรา merge ลงไฟล์ใต้ www เสมอ — Node อาจอ่านคนละ path กับ PHP
|
||||
* - PUT: merge body ลง quiz-settings.json บนดิสก์เสมอ (หลัง proxy Node) เพื่อให้ข้อมูลคงอยู่หลังรีเฟรช
|
||||
*/
|
||||
require __DIR__ . '/_common.php';
|
||||
@@ -37,16 +37,22 @@ function merge_quiz_settings_disk_from_put(string $rawBody): bool
|
||||
}
|
||||
$path = quiz_settings_disk_path();
|
||||
$base = [];
|
||||
$hadFileButInvalidJson = false;
|
||||
if (is_file($path)) {
|
||||
$prev = @file_get_contents($path);
|
||||
if ($prev !== false && $prev !== '') {
|
||||
$d = json_decode($prev, true);
|
||||
if (is_array($d)) {
|
||||
$base = $d;
|
||||
} else {
|
||||
$hadFileButInvalidJson = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
$mergeKeys = ['readMs', 'answerMs', 'betweenMs', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'questions', 'carryQuestions', 'battleQuizMcq'];
|
||||
if ($hadFileButInvalidJson) {
|
||||
return false;
|
||||
}
|
||||
$mergeKeys = ['readMs', 'answerMs', 'betweenMs', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryMapPanelTheme', 'questions', 'carryQuestions', 'battleQuizMcq'];
|
||||
foreach ($mergeKeys as $k) {
|
||||
if (array_key_exists($k, $patch)) {
|
||||
$base[$k] = $patch[$k];
|
||||
@@ -66,9 +72,10 @@ function merge_quiz_settings_disk_from_put(string $rawBody): bool
|
||||
}
|
||||
|
||||
/**
|
||||
* Node เก่าอาจไม่ใส่ battleQuizMcq ใน JSON — อ่านจากดิสก์มาแปะให้แอดมินโหลดได้หลังรีเฟรช
|
||||
* แปะค่าจาก quiz-settings.json ใต้ docroot ทับ JSON จาก Node — ให้แอดมินรีเฟรชแล้วเห็นค่าที่ merge หลัง PUT (กันคนละไฟล์กับ Node)
|
||||
* Overlay keys from on-disk quiz-settings.json over Node JSON — Admin refresh matches post-PUT merge (fixes Node vs disk path mismatch).
|
||||
*/
|
||||
function overlay_battle_quiz_mcq_from_disk(string $nodeBody): string
|
||||
function overlay_quiz_settings_from_disk(string $nodeBody): string
|
||||
{
|
||||
$j = json_decode($nodeBody, true);
|
||||
if (!is_array($j)) {
|
||||
@@ -83,10 +90,23 @@ function overlay_battle_quiz_mcq_from_disk(string $nodeBody): string
|
||||
return $nodeBody;
|
||||
}
|
||||
$disk = json_decode($raw, true);
|
||||
if (!is_array($disk) || !array_key_exists('battleQuizMcq', $disk)) {
|
||||
if (!is_array($disk)) {
|
||||
return $nodeBody;
|
||||
}
|
||||
$j['battleQuizMcq'] = $disk['battleQuizMcq'];
|
||||
if (array_key_exists('battleQuizMcq', $disk)) {
|
||||
$j['battleQuizMcq'] = $disk['battleQuizMcq'];
|
||||
}
|
||||
if (array_key_exists('carryMapPanelTheme', $disk) && is_array($disk['carryMapPanelTheme'])) {
|
||||
$j['carryMapPanelTheme'] = $disk['carryMapPanelTheme'];
|
||||
}
|
||||
foreach (['carryReadMs', 'carryAnswerMs', 'carrySessionLength'] as $ck) {
|
||||
if (array_key_exists($ck, $disk)) {
|
||||
$j[$ck] = $disk[$ck];
|
||||
}
|
||||
}
|
||||
if (array_key_exists('carryQuestions', $disk) && is_array($disk['carryQuestions'])) {
|
||||
$j['carryQuestions'] = $disk['carryQuestions'];
|
||||
}
|
||||
$enc = json_encode($j, JSON_UNESCAPED_UNICODE);
|
||||
return $enc !== false ? $enc : $nodeBody;
|
||||
}
|
||||
@@ -141,7 +161,26 @@ function proxy_quiz_curl(string $method, ?string $body = null): void
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Cache-Control: no-store');
|
||||
echo json_encode(['ok' => true, 'diskFallback' => true], JSON_UNESCAPED_UNICODE);
|
||||
$payload = ['ok' => true, 'diskFallback' => true];
|
||||
$rawDisk = @file_get_contents(quiz_settings_disk_path());
|
||||
if ($rawDisk !== false && $rawDisk !== '') {
|
||||
$dj = json_decode($rawDisk, true);
|
||||
if (is_array($dj)) {
|
||||
if (isset($dj['carryMapPanelTheme']) && is_array($dj['carryMapPanelTheme'])) {
|
||||
$payload['carryMapPanelTheme'] = $dj['carryMapPanelTheme'];
|
||||
}
|
||||
if (array_key_exists('carryReadMs', $dj)) {
|
||||
$payload['carryReadMs'] = $dj['carryReadMs'];
|
||||
}
|
||||
if (array_key_exists('carryAnswerMs', $dj)) {
|
||||
$payload['carryAnswerMs'] = $dj['carryAnswerMs'];
|
||||
}
|
||||
if (array_key_exists('carrySessionLength', $dj)) {
|
||||
$payload['carrySessionLength'] = $dj['carrySessionLength'];
|
||||
}
|
||||
}
|
||||
}
|
||||
echo json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
json_response([
|
||||
@@ -154,7 +193,7 @@ function proxy_quiz_curl(string $method, ?string $body = null): void
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Cache-Control: no-store');
|
||||
if ($method === 'GET' && $code >= 200 && $code < 300) {
|
||||
echo overlay_battle_quiz_mcq_from_disk($out);
|
||||
echo overlay_quiz_settings_from_disk($out);
|
||||
} else {
|
||||
echo $out;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Proxy ค่าคำถาม quiz-settings ระหว่างแผง Admin กับ Node (Game/server.js)
|
||||
* แก้กรณี nginx ส่ง /Game/ เป็น static แล้ว PUT ไม่ถึง Node — เบราว์เซอร์เรียก PHP แทน
|
||||
*
|
||||
* รองรับ Node เวอร์ชันเก่าที่ GET ไม่ส่ง battleQuizMcq / PUT เขียนทับไฟล์แล้วตัดฟิลด์:
|
||||
* - GET: ดึง battleQuizMcq จากไฟล์บนดิสก์มาแปะในคำตอบ
|
||||
* - PUT: merge body ลง quiz-settings.json บนดิสก์เสมอ (หลัง proxy Node) เพื่อให้ข้อมูลคงอยู่หลังรีเฟรช
|
||||
*/
|
||||
require __DIR__ . '/_common.php';
|
||||
|
||||
require_login();
|
||||
|
||||
if (!function_exists('curl_init')) {
|
||||
json_response(['ok' => false, 'error' => 'ต้องเปิด PHP extension curl (ติดตั้งแพ็กเกจ php-curl แล้วรีสตาร์ท PHP-FPM)'], 500);
|
||||
}
|
||||
|
||||
$port = preg_replace('/[^0-9]/', '', (string)(getenv('GAME_NODE_PORT') ?: '3001')) ?: '3001';
|
||||
$host = getenv('GAME_NODE_INTERNAL_HOST') ?: '127.0.0.1';
|
||||
$target = 'http://' . $host . ':' . $port . '/Game/api/quiz-settings';
|
||||
|
||||
function quiz_settings_disk_path(): string
|
||||
{
|
||||
return dirname(__DIR__, 2) . '/Game/data/quiz-settings.json';
|
||||
}
|
||||
|
||||
/**
|
||||
* รวมฟิลด์ที่ส่งมาใน PUT เข้ากับไฟล์บนดิสก์ (ไม่ทับฟิลด์ที่ไม่ได้ส่งมา)
|
||||
*/
|
||||
function merge_quiz_settings_disk_from_put(string $rawBody): bool
|
||||
{
|
||||
$patch = json_decode($rawBody, true);
|
||||
if (!is_array($patch)) {
|
||||
return false;
|
||||
}
|
||||
$path = quiz_settings_disk_path();
|
||||
$base = [];
|
||||
if (is_file($path)) {
|
||||
$prev = @file_get_contents($path);
|
||||
if ($prev !== false && $prev !== '') {
|
||||
$d = json_decode($prev, true);
|
||||
if (is_array($d)) {
|
||||
$base = $d;
|
||||
}
|
||||
}
|
||||
}
|
||||
$mergeKeys = ['readMs', 'answerMs', 'betweenMs', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryMapPanelTheme', 'questions', 'carryQuestions', 'battleQuizMcq'];
|
||||
foreach ($mergeKeys as $k) {
|
||||
if (array_key_exists($k, $patch)) {
|
||||
$base[$k] = $patch[$k];
|
||||
}
|
||||
}
|
||||
$dir = dirname($path);
|
||||
if (!is_dir($dir)) {
|
||||
if (!@mkdir($dir, 0755, true)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
$json = json_encode($base, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||
if ($json === false) {
|
||||
return false;
|
||||
}
|
||||
return @file_put_contents($path, $json) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Node เก่าอาจไม่ใส่ battleQuizMcq ใน JSON — อ่านจากดิสก์มาแปะให้แอดมินโหลดได้หลังรีเฟรช
|
||||
*/
|
||||
function overlay_battle_quiz_mcq_from_disk(string $nodeBody): string
|
||||
{
|
||||
$j = json_decode($nodeBody, true);
|
||||
if (!is_array($j)) {
|
||||
return $nodeBody;
|
||||
}
|
||||
$path = quiz_settings_disk_path();
|
||||
if (!is_file($path)) {
|
||||
return $nodeBody;
|
||||
}
|
||||
$raw = @file_get_contents($path);
|
||||
if ($raw === false || $raw === '') {
|
||||
return $nodeBody;
|
||||
}
|
||||
$disk = json_decode($raw, true);
|
||||
if (!is_array($disk) || !array_key_exists('battleQuizMcq', $disk)) {
|
||||
return $nodeBody;
|
||||
}
|
||||
$j['battleQuizMcq'] = $disk['battleQuizMcq'];
|
||||
$enc = json_encode($j, JSON_UNESCAPED_UNICODE);
|
||||
return $enc !== false ? $enc : $nodeBody;
|
||||
}
|
||||
|
||||
function proxy_quiz_curl(string $method, ?string $body = null): void
|
||||
{
|
||||
global $target;
|
||||
$ch = curl_init($target);
|
||||
if ($ch === false) {
|
||||
json_response(['ok' => false, 'error' => 'curl init failed'], 500);
|
||||
}
|
||||
$headers = ['Accept: application/json'];
|
||||
if ($body !== null && $method !== 'GET') {
|
||||
$headers[] = 'Content-Type: application/json';
|
||||
}
|
||||
$opts = [
|
||||
CURLOPT_CUSTOMREQUEST => $method,
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
];
|
||||
if ($body !== null && $method !== 'GET') {
|
||||
$opts[CURLOPT_POSTFIELDS] = $body;
|
||||
}
|
||||
curl_setopt_array($ch, $opts);
|
||||
$out = curl_exec($ch);
|
||||
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($method === 'PUT' && $body !== null && $body !== '') {
|
||||
merge_quiz_settings_disk_from_put($body);
|
||||
}
|
||||
|
||||
if ($out === false || $out === '') {
|
||||
if ($method === 'GET') {
|
||||
$disk = quiz_settings_disk_path();
|
||||
if (is_file($disk)) {
|
||||
$rawFile = @file_get_contents($disk);
|
||||
if ($rawFile !== false && $rawFile !== '') {
|
||||
http_response_code(200);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Cache-Control: no-store');
|
||||
echo $rawFile;
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($method === 'PUT' && $body !== null && $body !== '' && is_file(quiz_settings_disk_path())) {
|
||||
http_response_code(200);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Cache-Control: no-store');
|
||||
$payload = ['ok' => true, 'diskFallback' => true];
|
||||
$rawDisk = @file_get_contents(quiz_settings_disk_path());
|
||||
if ($rawDisk !== false && $rawDisk !== '') {
|
||||
$dj = json_decode($rawDisk, true);
|
||||
if (is_array($dj)) {
|
||||
if (isset($dj['carryMapPanelTheme']) && is_array($dj['carryMapPanelTheme'])) {
|
||||
$payload['carryMapPanelTheme'] = $dj['carryMapPanelTheme'];
|
||||
}
|
||||
if (array_key_exists('carryReadMs', $dj)) {
|
||||
$payload['carryReadMs'] = $dj['carryReadMs'];
|
||||
}
|
||||
if (array_key_exists('carryAnswerMs', $dj)) {
|
||||
$payload['carryAnswerMs'] = $dj['carryAnswerMs'];
|
||||
}
|
||||
if (array_key_exists('carrySessionLength', $dj)) {
|
||||
$payload['carrySessionLength'] = $dj['carrySessionLength'];
|
||||
}
|
||||
}
|
||||
}
|
||||
echo json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
json_response([
|
||||
'ok' => false,
|
||||
'error' => 'ไม่ต่อถึงเซิร์ฟเวอร์เกม (Node) — ตรวจว่า pm2/systemd รัน Game/server.js อยู่ที่พอร์ต ' . (getenv('GAME_NODE_PORT') ?: '3001') . ' · ' . $err,
|
||||
], 502);
|
||||
}
|
||||
http_response_code($code > 0 ? $code : 500);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Cache-Control: no-store');
|
||||
if ($method === 'GET' && $code >= 200 && $code < 300) {
|
||||
echo overlay_battle_quiz_mcq_from_disk($out);
|
||||
} else {
|
||||
echo $out;
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
if ($method === 'GET') {
|
||||
proxy_quiz_curl('GET', null);
|
||||
}
|
||||
if ($method === 'PUT') {
|
||||
$raw = file_get_contents('php://input');
|
||||
if ($raw === false) {
|
||||
$raw = '';
|
||||
}
|
||||
proxy_quiz_curl('PUT', $raw);
|
||||
}
|
||||
|
||||
json_response(['ok' => false, 'error' => 'Method not allowed'], 405);
|
||||
@@ -9,7 +9,7 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Kanit:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="admin.css?v=14">
|
||||
<link rel="stylesheet" href="admin.css?v=15">
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#admin-main">ข้ามไปเนื้อหา</a>
|
||||
@@ -180,6 +180,53 @@
|
||||
<span id="quiz-carry-session-hint" class="muted" style="display:block;font-size:0.85rem;margin-top:0.2rem">สุ่มคำถามจากชุดจนกว่าจะครบจำนวนนี้ (นับทุกข้อที่จบด้วยถูกหรือหมดเวลา) · 0 = เล่นต่อไม่จบอัตโนมัติ · <em>English:</em> Preview / editor embed only.</span>
|
||||
</label>
|
||||
</div>
|
||||
<fieldset class="quiz-carry-theme-fieldset" style="margin:0.75rem 0 1rem;padding:0.75rem 1rem;border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-elevated)">
|
||||
<legend class="quiz-carry-theme-legend" style="font-size:0.92rem;font-weight:600;padding:0 0.35rem">แผงข้อความบนแผนที่ (คำถาม / ตัวเลือกที่ถือ)</legend>
|
||||
<p class="muted" style="margin:0 0 0.65rem;font-size:0.82rem;line-height:1.45">ใช้กับ <code>#quiz-map-question-panel</code> ในโหมดหยิบมาวาง (โซนทองหรือโซนกลาง) · เก็บที่ <code>carryMapPanelTheme</code> ใน <code>quiz-settings.json</code> · <em>English:</em> Color pickers + alpha (0–100%) — saved as <code>rgba()</code>.</p>
|
||||
<div class="quiz-carry-theme-grid">
|
||||
<div class="quiz-carry-theme-row" data-quiz-carry-theme="bg">
|
||||
<span class="quiz-carry-theme-label">พื้นหลังแผง</span>
|
||||
<div class="quiz-carry-color-controls">
|
||||
<input type="color" id="quiz-carry-theme-bg-swatch" value="#0c0e1c" title="เลือกสี" aria-label="สีพื้นหลังแผง" />
|
||||
<label class="quiz-carry-alpha-label"><span>โปร่ง A</span>
|
||||
<input type="range" id="quiz-carry-theme-bg-alpha" min="0" max="100" value="88" step="1" aria-label="ความโปร่งใสพื้นหลังแผง เปอร์เซ็นต์" />
|
||||
<output id="quiz-carry-theme-bg-alpha-out" class="quiz-carry-alpha-out" for="quiz-carry-theme-bg-alpha">88%</output>
|
||||
</label>
|
||||
<code id="quiz-carry-theme-bg-preview" class="quiz-carry-theme-code" aria-hidden="true"></code>
|
||||
</div>
|
||||
<input type="hidden" id="quiz-carry-theme-bg" value="" />
|
||||
</div>
|
||||
<div class="quiz-carry-theme-row" data-quiz-carry-theme="border">
|
||||
<span class="quiz-carry-theme-label">สีขอบ</span>
|
||||
<div class="quiz-carry-color-controls">
|
||||
<input type="color" id="quiz-carry-theme-border-swatch" value="#ffd666" title="เลือกสีขอบ" aria-label="สีขอบแผง" />
|
||||
<label class="quiz-carry-alpha-label"><span>โปร่ง A</span>
|
||||
<input type="range" id="quiz-carry-theme-border-alpha" min="0" max="100" value="70" step="1" aria-label="ความโปร่งใสขอบ เปอร์เซ็นต์" />
|
||||
<output id="quiz-carry-theme-border-alpha-out" class="quiz-carry-alpha-out" for="quiz-carry-theme-border-alpha">70%</output>
|
||||
</label>
|
||||
<code id="quiz-carry-theme-border-preview" class="quiz-carry-theme-code" aria-hidden="true"></code>
|
||||
</div>
|
||||
<input type="hidden" id="quiz-carry-theme-border" value="" />
|
||||
</div>
|
||||
<div class="quiz-carry-theme-row quiz-carry-theme-row--narrow">
|
||||
<label class="admin-field quiz-carry-theme-label">ความหนาขอบ (px)
|
||||
<input type="number" id="quiz-carry-theme-border-w" class="admin-inp-num" min="0" max="12" step="1" value="2" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="quiz-carry-theme-row" data-quiz-carry-theme="text">
|
||||
<span class="quiz-carry-theme-label">สีตัวอักษร</span>
|
||||
<div class="quiz-carry-color-controls">
|
||||
<input type="color" id="quiz-carry-theme-text-swatch" value="#f1f5f9" title="เลือกสีตัวอักษร" aria-label="สีข้อความบนแผง" />
|
||||
<label class="quiz-carry-alpha-label"><span>โปร่ง A</span>
|
||||
<input type="range" id="quiz-carry-theme-text-alpha" min="0" max="100" value="100" step="1" aria-label="ความโปร่งใสตัวอักษร เปอร์เซ็นต์" />
|
||||
<output id="quiz-carry-theme-text-alpha-out" class="quiz-carry-alpha-out" for="quiz-carry-theme-text-alpha">100%</output>
|
||||
</label>
|
||||
<code id="quiz-carry-theme-text-preview" class="quiz-carry-theme-code" aria-hidden="true"></code>
|
||||
</div>
|
||||
<input type="hidden" id="quiz-carry-theme-text" value="" />
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<h3 class="admin-subheading">รายการคำถาม</h3>
|
||||
<div id="quiz-carry-admin-list" class="quiz-carry-admin-list"></div>
|
||||
<div class="quiz-admin-actions">
|
||||
@@ -521,6 +568,6 @@
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<script src="admin.js?v=27"></script>
|
||||
<script src="admin.js?v=31"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -117,5 +117,14 @@
|
||||
],
|
||||
"correctIndex": 0
|
||||
}
|
||||
]
|
||||
],
|
||||
"carryReadMs": 3000,
|
||||
"carryAnswerMs": 5000,
|
||||
"carrySessionLength": 10,
|
||||
"carryMapPanelTheme": {
|
||||
"panelBg": "rgba(255, 0, 0, 0)",
|
||||
"panelBorder": "rgba(255, 255, 255, 0)",
|
||||
"textColor": "rgba(241, 245, 249, 1)",
|
||||
"borderWidthPx": 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
/**
|
||||
* สาธารณะ — ส่งฟิลด์ quiz_carry จาก quiz-settings.json บนดิสก์ (ใต้ Game/data)
|
||||
* หน้าเล่น merge ทับคำตอบจาก Node เพื่อให้ธีมแผงคำถามตรงกับที่ Admin บันทึก (PHP merge ลงไฟล์เดียวกับนี้)
|
||||
* Public — carry fields from on-disk quiz-settings.json; play.js merges over Node so panel theme matches Admin saves.
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
header('Pragma: no-cache');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
|
||||
$path = dirname(__DIR__) . '/data/quiz-settings.json';
|
||||
$raw = @file_get_contents($path);
|
||||
if ($raw === false || $raw === '') {
|
||||
echo '{}';
|
||||
exit;
|
||||
}
|
||||
|
||||
$data = json_decode($raw, true);
|
||||
if (!is_array($data)) {
|
||||
echo '{}';
|
||||
exit;
|
||||
}
|
||||
|
||||
$out = [];
|
||||
$keys = ['carryMapPanelTheme', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryQuestions'];
|
||||
foreach ($keys as $k) {
|
||||
if (!array_key_exists($k, $data)) {
|
||||
continue;
|
||||
}
|
||||
if ($k === 'carryMapPanelTheme') {
|
||||
if (is_array($data[$k])) {
|
||||
$out[$k] = $data[$k];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ($k === 'carryQuestions') {
|
||||
if (is_array($data[$k])) {
|
||||
$out[$k] = $data[$k];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$out[$k] = $data[$k];
|
||||
}
|
||||
|
||||
echo json_encode($out, JSON_UNESCAPED_UNICODE);
|
||||
@@ -207,7 +207,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<script src="js/version.js?v=0.0169"></script>
|
||||
<script src="js/editor.js?v=20260428-zones-over-grid"></script>
|
||||
<script src="js/editor.js?v=20260428-play-cache-bust"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1709,7 +1709,8 @@
|
||||
var q = page + '?space=' + encodeURIComponent(sid)
|
||||
+ '&nick=' + encodeURIComponent(nick)
|
||||
+ '&map=' + encodeURIComponent(id)
|
||||
+ '&preview=1&defaultChar=1&editorEmbed=1';
|
||||
+ '&preview=1&defaultChar=1&editorEmbed=1'
|
||||
+ '&_h=' + Date.now();
|
||||
if (statusEl) statusEl.textContent = 'เปิดหน้าทดสอบเล่นในแท็บใหม่แล้ว — ปิดแท็บเมื่อเล่นเสร็จ';
|
||||
window.open(BASE + '/' + q, '_blank', 'noopener,noreferrer');
|
||||
})
|
||||
|
||||
+173
-13
@@ -580,6 +580,10 @@
|
||||
let playHostId = null;
|
||||
/** ซิงก์จากเซิร์ฟเวอร์: socketId → กด Ready แล้ว */
|
||||
let quizCarryLobbyReadyMap = {};
|
||||
/** จาก /api/quiz-settings carryMapPanelTheme — ใช้กับ #quiz-map-question-panel เฉพาะ quiz_carry */
|
||||
let quizCarryMapPanelTheme = null;
|
||||
/** สแนปจาก join-space (Node) — ใช้เมื่อ fetch HTTP quiz-settings ไม่ได้หรือไม่มีธีม */
|
||||
let quizCarryJoinSettingsSnap = null;
|
||||
|
||||
/** jump_survive — กล้องตามตัวในกรอบ + แพลตฟอร์มเลื่อนขึ้น (scroll) ไม่ลากทั้งฉาก */
|
||||
let jumpSurviveCamCenterX = 0;
|
||||
@@ -733,6 +737,92 @@
|
||||
return getTileBoundsForOnesGrid(md.quizQuestionArea);
|
||||
}
|
||||
|
||||
function clearQuizMapPanelThemeInline(panel, textEl) {
|
||||
if (!panel || !textEl) return;
|
||||
panel.style.removeProperty('--qmap-bg');
|
||||
panel.style.removeProperty('--qmap-border');
|
||||
panel.style.removeProperty('--qmap-border-w');
|
||||
panel.style.removeProperty('--qmap-shadow');
|
||||
panel.style.removeProperty('background');
|
||||
panel.style.removeProperty('border');
|
||||
panel.style.removeProperty('border-color');
|
||||
panel.style.removeProperty('border-width');
|
||||
panel.style.removeProperty('border-style');
|
||||
panel.style.removeProperty('box-shadow');
|
||||
textEl.style.removeProperty('--qmap-text');
|
||||
textEl.style.removeProperty('--qmap-text-shadow');
|
||||
textEl.style.removeProperty('color');
|
||||
textEl.style.removeProperty('text-shadow');
|
||||
}
|
||||
|
||||
function parseCarryMapPanelThemeObject(raw) {
|
||||
let t = raw;
|
||||
if (typeof t === 'string') {
|
||||
try {
|
||||
t = JSON.parse(t);
|
||||
} catch (e) {
|
||||
t = null;
|
||||
}
|
||||
}
|
||||
if (!t || typeof t !== 'object') return null;
|
||||
const bw = Number(t.borderWidthPx);
|
||||
const borderWidthPx = Number.isFinite(bw) ? Math.max(0, Math.min(12, Math.round(bw))) : 2;
|
||||
return {
|
||||
panelBg: String(t.panelBg || '').trim().slice(0, 120),
|
||||
panelBorder: String(t.panelBorder || '').trim().slice(0, 120),
|
||||
borderWidthPx,
|
||||
textColor: String(t.textColor || '').trim().slice(0, 120),
|
||||
};
|
||||
}
|
||||
|
||||
function setQuizCarryMapPanelThemeFromApi(s) {
|
||||
quizCarryMapPanelTheme = null;
|
||||
if (!s || typeof s !== 'object') return;
|
||||
const parsed = parseCarryMapPanelThemeObject(s.carryMapPanelTheme);
|
||||
if (!parsed) return;
|
||||
quizCarryMapPanelTheme = parsed;
|
||||
}
|
||||
|
||||
/** ธีมจากแผนที่ (ถ้ามี) ชนะค่าจาก API — ใส่ carryMapPanelTheme ใน JSON แมปได้โดยตรง */
|
||||
function getEffectiveCarryMapPanelThemeForApply() {
|
||||
if (mapData && mapData.carryMapPanelTheme != null) {
|
||||
const parsed = parseCarryMapPanelThemeObject(mapData.carryMapPanelTheme);
|
||||
if (parsed) return parsed;
|
||||
}
|
||||
return quizCarryMapPanelTheme;
|
||||
}
|
||||
|
||||
function applyQuizCarryMapPanelThemeIfNeeded(panel, textEl) {
|
||||
if (!panel || !textEl) return;
|
||||
if (!isQuizCarry()) {
|
||||
clearQuizMapPanelThemeInline(panel, textEl);
|
||||
return;
|
||||
}
|
||||
const th = getEffectiveCarryMapPanelThemeForApply();
|
||||
if (!th) return;
|
||||
clearQuizMapPanelThemeInline(panel, textEl);
|
||||
const bgStr = th.panelBg != null ? String(th.panelBg).trim() : '';
|
||||
const brStr = th.panelBorder != null ? String(th.panelBorder).trim() : '';
|
||||
const txStr = th.textColor != null ? String(th.textColor).trim() : '';
|
||||
const wRaw = Number(th.borderWidthPx);
|
||||
const w = Number.isFinite(wRaw) ? Math.max(0, Math.min(12, Math.round(wRaw))) : 0;
|
||||
const bgUse = bgStr || 'transparent';
|
||||
const brUse = brStr || 'transparent';
|
||||
const txUse = txStr || '#f1f5f9';
|
||||
panel.style.setProperty('--qmap-bg', bgUse);
|
||||
panel.style.setProperty('--qmap-border', brUse);
|
||||
panel.style.setProperty('--qmap-border-w', String(w) + 'px');
|
||||
panel.style.setProperty('--qmap-shadow', 'none');
|
||||
textEl.style.setProperty('--qmap-text', txUse);
|
||||
textEl.style.setProperty('--qmap-text-shadow', 'none');
|
||||
/* โหลด play.html แบบแคชเก่า (ไม่มี var) ยังต้องทับได้ — ใช้ !important คู่กับตัวแปร */
|
||||
panel.style.setProperty('background', bgUse, 'important');
|
||||
panel.style.setProperty('border', String(w) + 'px solid ' + brUse, 'important');
|
||||
panel.style.setProperty('box-shadow', 'none', 'important');
|
||||
textEl.style.setProperty('color', txUse, 'important');
|
||||
textEl.style.setProperty('text-shadow', 'none', 'important');
|
||||
}
|
||||
|
||||
function syncPlayQuizMapPanel() {
|
||||
const panel = document.getElementById('quiz-map-question-panel');
|
||||
const textEl = document.getElementById('quiz-map-question-text');
|
||||
@@ -741,11 +831,13 @@
|
||||
if (previewMode && editorEmbedReturn && !isQuizCarry()) {
|
||||
panel.classList.add('is-hidden');
|
||||
panel.setAttribute('aria-hidden', 'true');
|
||||
clearQuizMapPanelThemeInline(panel, textEl);
|
||||
return;
|
||||
}
|
||||
if (isQuizBattle()) {
|
||||
panel.classList.add('is-hidden');
|
||||
panel.setAttribute('aria-hidden', 'true');
|
||||
clearQuizMapPanelThemeInline(panel, textEl);
|
||||
return;
|
||||
}
|
||||
if (!isQuiz() && !isQuizCarry()) return;
|
||||
@@ -756,6 +848,7 @@
|
||||
if (!bounds || !text) {
|
||||
panel.classList.add('is-hidden');
|
||||
panel.setAttribute('aria-hidden', 'true');
|
||||
clearQuizMapPanelThemeInline(panel, textEl);
|
||||
return;
|
||||
}
|
||||
const camX = me.x * tileSize;
|
||||
@@ -771,6 +864,7 @@
|
||||
panel.style.height = Math.round(Math.max(40, hPx)) + 'px';
|
||||
panel.classList.remove('is-hidden');
|
||||
panel.setAttribute('aria-hidden', 'false');
|
||||
applyQuizCarryMapPanelThemeIfNeeded(panel, textEl);
|
||||
}
|
||||
|
||||
/** quiz_carry + embed: คำถามไปที่แผงทองบนแผนที่ / ใน overlay นับถอยหลัง — ไม่ใช้แถบบน */
|
||||
@@ -1361,7 +1455,7 @@
|
||||
resetPreviewBotsQuizState();
|
||||
let settings = null;
|
||||
try {
|
||||
const r = await fetch(BASE + '/api/quiz-settings');
|
||||
const r = await fetch(BASE + '/api/quiz-settings?_=' + Date.now(), { cache: 'no-store' });
|
||||
if (r.ok) settings = await r.json();
|
||||
} catch (e) { /* use map fallback */ }
|
||||
let pool = [];
|
||||
@@ -1419,9 +1513,11 @@
|
||||
const ov = document.getElementById('quiz-game-overlay');
|
||||
if (ov) ov.classList.add('is-hidden');
|
||||
const panel = document.getElementById('quiz-map-question-panel');
|
||||
const mapQText = document.getElementById('quiz-map-question-text');
|
||||
if (panel) {
|
||||
panel.classList.add('is-hidden');
|
||||
panel.setAttribute('aria-hidden', 'true');
|
||||
clearQuizMapPanelThemeInline(panel, mapQText);
|
||||
}
|
||||
playQuizPhaseLocal = null;
|
||||
playQuizPlayerLocal = { cannotTrue: false, cannotFalse: false, eliminated: false, score: 0 };
|
||||
@@ -1433,6 +1529,8 @@
|
||||
playLiveQuizScores = {};
|
||||
playPath = [];
|
||||
hidePlayQuizHud();
|
||||
quizCarryJoinSettingsSnap = null;
|
||||
quizCarryMapPanelTheme = null;
|
||||
resetQuizCarryPlayState();
|
||||
resetQuizBattlePlayState();
|
||||
}
|
||||
@@ -2364,27 +2462,75 @@
|
||||
});
|
||||
}
|
||||
|
||||
/** รวม carry จากดิสก์ผ่าน Node GET /Game/api/quiz-carry-from-disk (อยู่ใต้ /Game/api/ เพื่อให้ nginx proxy ตรงกับเซิร์ฟอื่น) — ทาง .php ใต้ /Game/ มักโดน fastcgi SCRIPT_FILENAME ผิดจึง 404 */
|
||||
async function mergeQuizCarrySettingsFromDisk(s) {
|
||||
const out = s && typeof s === 'object' ? s : {};
|
||||
const urls = [
|
||||
BASE + '/api/quiz-carry-from-disk?_=' + Date.now(),
|
||||
BASE + '/api-quiz-carry-from-disk.php?_=' + (Date.now() + 1),
|
||||
];
|
||||
for (let ui = 0; ui < urls.length; ui++) {
|
||||
try {
|
||||
const rd = await fetch(urls[ui], { cache: 'no-store' });
|
||||
if (!rd.ok) continue;
|
||||
const disk = await rd.json();
|
||||
if (!disk || typeof disk !== 'object') continue;
|
||||
if (disk.carryMapPanelTheme && typeof disk.carryMapPanelTheme === 'object') {
|
||||
out.carryMapPanelTheme = disk.carryMapPanelTheme;
|
||||
}
|
||||
if (disk.carryReadMs != null) out.carryReadMs = disk.carryReadMs;
|
||||
if (disk.carryAnswerMs != null) out.carryAnswerMs = disk.carryAnswerMs;
|
||||
if (disk.carrySessionLength != null) out.carrySessionLength = disk.carrySessionLength;
|
||||
if (Array.isArray(disk.carryQuestions) && disk.carryQuestions.length > 0) {
|
||||
out.carryQuestions = disk.carryQuestions;
|
||||
}
|
||||
} catch (e) { /* next url */ }
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function loadQuizCarryPoolAndStart() {
|
||||
quizCarryRoundsCompleted = 0;
|
||||
quizCarrySessionEnded = false;
|
||||
quizCarrySessionLength = 0;
|
||||
let pool = [];
|
||||
let s = {};
|
||||
const snap = quizCarryJoinSettingsSnap && typeof quizCarryJoinSettingsSnap === 'object' ? quizCarryJoinSettingsSnap : null;
|
||||
try {
|
||||
const r = await fetch(BASE + '/api/quiz-settings');
|
||||
const r = await fetch(BASE + '/api/quiz-settings?_=' + Date.now(), { cache: 'no-store' });
|
||||
if (r.ok) {
|
||||
const s = await r.json();
|
||||
quizCarryCarryTimingMs = {
|
||||
carryReadMs: clampPreviewMs(s.carryReadMs, 3000, 0, 120000),
|
||||
carryAnswerMs: clampPreviewMs(s.carryAnswerMs, 5000, 1000, 300000),
|
||||
};
|
||||
quizCarrySessionLength = clampCarrySessionLen(s.carrySessionLength, 0);
|
||||
const carryOnly = s && Array.isArray(s.carryQuestions) && s.carryQuestions.length > 0;
|
||||
const source = carryOnly ? s.carryQuestions : (s && Array.isArray(s.questions) ? s.questions : []);
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
const n = normalizeQuizCarryQuestionFromAny(source[i]);
|
||||
if (n) pool.push(n);
|
||||
const raw = await r.text();
|
||||
const trimmed = (raw || '').trim();
|
||||
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if (parsed && typeof parsed === 'object') s = parsed;
|
||||
}
|
||||
}
|
||||
} catch (e) { /* Node/HTML error — ต่อจาก merge ดิสก์ */ }
|
||||
try {
|
||||
s = await mergeQuizCarrySettingsFromDisk(s);
|
||||
} catch (e) { /* ignore */ }
|
||||
if (snap) {
|
||||
if (s.carryReadMs == null && snap.carryReadMs != null) s.carryReadMs = snap.carryReadMs;
|
||||
if (s.carryAnswerMs == null && snap.carryAnswerMs != null) s.carryAnswerMs = snap.carryAnswerMs;
|
||||
if (s.carrySessionLength == null && snap.carrySessionLength != null) s.carrySessionLength = snap.carrySessionLength;
|
||||
}
|
||||
if (snap && snap.carryMapPanelTheme && typeof snap.carryMapPanelTheme === 'object') {
|
||||
s.carryMapPanelTheme = snap.carryMapPanelTheme;
|
||||
}
|
||||
try {
|
||||
quizCarryCarryTimingMs = {
|
||||
carryReadMs: clampPreviewMs(s.carryReadMs, 3000, 0, 120000),
|
||||
carryAnswerMs: clampPreviewMs(s.carryAnswerMs, 5000, 1000, 300000),
|
||||
};
|
||||
quizCarrySessionLength = clampCarrySessionLen(s.carrySessionLength, 0);
|
||||
setQuizCarryMapPanelThemeFromApi(s);
|
||||
const carryOnly = s && Array.isArray(s.carryQuestions) && s.carryQuestions.length > 0;
|
||||
const source = carryOnly ? s.carryQuestions : (s && Array.isArray(s.questions) ? s.questions : []);
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
const n = normalizeQuizCarryQuestionFromAny(source[i]);
|
||||
if (n) pool.push(n);
|
||||
}
|
||||
} catch (e) { /* map only */ }
|
||||
if (!pool.length && mapData) pool = buildQuizCarryPoolFromMap(mapData);
|
||||
quizCarryPool = pool;
|
||||
@@ -2395,6 +2541,10 @@
|
||||
playQuizText = 'ไม่มีคำถาม — ตั้งใน Admin → คำถามหลายตัวเลือก หรือใส่ในแมป (quizQuestions)';
|
||||
const qEl = document.getElementById('quiz-game-question');
|
||||
if (qEl) qEl.textContent = playQuizText;
|
||||
try {
|
||||
syncPlayQuizMapPanel();
|
||||
} catch (e) { /* ignore */ }
|
||||
quizCarryJoinSettingsSnap = null;
|
||||
return;
|
||||
}
|
||||
if (previewMode && editorEmbedReturn) {
|
||||
@@ -2405,6 +2555,10 @@
|
||||
if (!(previewMode && editorEmbedReturn && quizCarryEmbedCountdownEndAt > Date.now())) {
|
||||
quizCarryRestoreEmbedPhaseLabel();
|
||||
}
|
||||
try {
|
||||
syncPlayQuizMapPanel();
|
||||
} catch (e) { /* ignore */ }
|
||||
quizCarryJoinSettingsSnap = null;
|
||||
}
|
||||
|
||||
function setupPlayQuizCarryUi() {
|
||||
@@ -5596,6 +5750,12 @@
|
||||
return;
|
||||
}
|
||||
myId = socket.id;
|
||||
if (res.quizCarrySettingsSnap && typeof res.quizCarrySettingsSnap === 'object') {
|
||||
quizCarryJoinSettingsSnap = res.quizCarrySettingsSnap;
|
||||
if (res.quizCarrySettingsSnap.carryMapPanelTheme && typeof res.quizCarrySettingsSnap.carryMapPanelTheme === 'object') {
|
||||
setQuizCarryMapPanelThemeFromApi({ carryMapPanelTheme: res.quizCarrySettingsSnap.carryMapPanelTheme });
|
||||
}
|
||||
}
|
||||
if (previewMode && editorEmbedReturn && res.mapData && res.mapData.gameType === 'lobby') {
|
||||
const q = [];
|
||||
q.push('space=' + encodeURIComponent(spaceId));
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="Cache-Control" content="no-store, max-age=0">
|
||||
<title>เล่น — Game</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
@@ -85,10 +86,14 @@
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
overflow: auto;
|
||||
background: rgba(12, 14, 28, 0.88);
|
||||
border: 2px solid rgba(255, 214, 102, 0.7);
|
||||
--qmap-bg: rgba(12, 14, 28, 0.88);
|
||||
--qmap-border: rgba(255, 214, 102, 0.7);
|
||||
--qmap-border-w: 2px;
|
||||
--qmap-shadow: 0 8px 28px rgba(0, 0, 0, 0.5);
|
||||
background: var(--qmap-bg);
|
||||
border: var(--qmap-border-w) solid var(--qmap-border);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: var(--qmap-shadow);
|
||||
}
|
||||
#quiz-map-question-panel.is-hidden { display: none !important; }
|
||||
#quiz-map-question-text {
|
||||
@@ -96,8 +101,10 @@
|
||||
font-size: clamp(0.78rem, 2.4vmin, 1.12rem);
|
||||
font-weight: 600;
|
||||
line-height: 1.45;
|
||||
color: #f1f5f9;
|
||||
text-shadow: 0 1px 10px rgba(0, 0, 0, 0.55);
|
||||
--qmap-text: #f1f5f9;
|
||||
--qmap-text-shadow: 0 1px 10px rgba(0, 0, 0, 0.55);
|
||||
color: var(--qmap-text);
|
||||
text-shadow: var(--qmap-text-shadow);
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
@@ -902,7 +909,7 @@
|
||||
</div>
|
||||
<script src="/Game/socket.io/socket.io.js"></script>
|
||||
<script src="js/version.js?v=0.0166"></script>
|
||||
<script src="js/play.js?v=0.104"></script>
|
||||
<script src="js/play.js?v=0.114"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -680,10 +680,14 @@
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
overflow: auto;
|
||||
background: rgba(12, 14, 28, 0.88);
|
||||
border: 2px solid rgba(255, 214, 102, 0.7);
|
||||
--qmap-bg: rgba(12, 14, 28, 0.88);
|
||||
--qmap-border: rgba(255, 214, 102, 0.7);
|
||||
--qmap-border-w: 2px;
|
||||
--qmap-shadow: 0 8px 28px rgba(0, 0, 0, 0.5);
|
||||
background: var(--qmap-bg);
|
||||
border: var(--qmap-border-w) solid var(--qmap-border);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: var(--qmap-shadow);
|
||||
}
|
||||
#quiz-map-question-panel.is-hidden {
|
||||
display: none !important;
|
||||
@@ -699,8 +703,10 @@
|
||||
font-size: clamp(0.78rem, 2.4vmin, 1.12rem);
|
||||
font-weight: 600;
|
||||
line-height: 1.45;
|
||||
color: #f1f5f9;
|
||||
text-shadow: 0 1px 10px rgba(0, 0, 0, 0.55);
|
||||
--qmap-text: #f1f5f9;
|
||||
--qmap-text-shadow: 0 1px 10px rgba(0, 0, 0, 0.55);
|
||||
color: var(--qmap-text);
|
||||
text-shadow: var(--qmap-text-shadow);
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
+118
-2
@@ -54,6 +54,10 @@ const CHARACTERS_DIR = path.join(__dirname, 'data', 'characters');
|
||||
const GAUNTLET_ASSETS_DIR = path.join(__dirname, 'data', 'gauntlet-assets');
|
||||
const GAUNTLET_ASSETS_META_PATH = path.join(__dirname, 'data', 'gauntlet-assets-meta.json');
|
||||
const QUIZ_SETTINGS_PATH = path.join(__dirname, 'data', 'quiz-settings.json');
|
||||
/** ถ้า Admin (PHP) merge ลงไฟล์ใต้ docroot แต่ Node รันจากโฟลเดอร์อื่น ให้ตั้ง absolute path ที่นี่ — carry* / ธีมแผงจะอ่านทับจาก mirror ทุกครั้งที่ loadQuizSettings */
|
||||
const QUIZ_SETTINGS_MIRROR_PATH = process.env.GAME_QUIZ_SETTINGS_MIRROR_PATH
|
||||
? path.resolve(String(process.env.GAME_QUIZ_SETTINGS_MIRROR_PATH).trim())
|
||||
: '';
|
||||
const GAME_TIMING_PATH = path.join(__dirname, 'data', 'game-timing.json');
|
||||
/** ช่องตัวเลือกบนกริด quiz_carry เก็บเลข 1..N (ไม่ใช่แค่ 0/1 เหมือน hub) */
|
||||
const QUIZ_CARRY_MAX_OPTION_SLOTS = 16;
|
||||
@@ -93,12 +97,50 @@ function defaultQuizSettings() {
|
||||
carryAnswerMs: 5000,
|
||||
/** quiz_carry: จำนวนข้อต่อเซสชันก่อนจบเกม (พรีวิว) — 0 = ไม่จบอัตโนมัติ */
|
||||
carrySessionLength: 0,
|
||||
/** quiz_carry: สีแผง #quiz-map-question-panel ในเกม/พรีวิว (จัดใน Admin แท็บหยิบมาวาง) */
|
||||
carryMapPanelTheme: defaultCarryMapPanelTheme(),
|
||||
questions: [],
|
||||
carryQuestions: [],
|
||||
battleQuizMcq: [],
|
||||
};
|
||||
}
|
||||
|
||||
function defaultCarryMapPanelTheme() {
|
||||
return {
|
||||
panelBg: 'rgba(12, 14, 28, 0.88)',
|
||||
panelBorder: 'rgba(255, 214, 102, 0.7)',
|
||||
borderWidthPx: 2,
|
||||
textColor: '#f1f5f9',
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeCssColorToken(input, fallback) {
|
||||
const t = String(input == null ? '' : input).trim().slice(0, 120);
|
||||
if (!t) return fallback;
|
||||
if (/url\s*\(|expression\s*\(|@import|\/\*|javascript|<|>|\\0/i.test(t)) return fallback;
|
||||
if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(t)) return t;
|
||||
if (/^rgba?\(\s*[^)]+\)$/i.test(t)) {
|
||||
const inner = t.replace(/^rgba?\(\s*/i, '').replace(/\)\s*$/, '');
|
||||
if (inner.length <= 96 && /^[0-9\s.,%+-]+$/.test(inner)) return t.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function sanitizeCarryMapPanelTheme(raw) {
|
||||
const d = defaultCarryMapPanelTheme();
|
||||
if (!raw || typeof raw !== 'object') return d;
|
||||
return {
|
||||
panelBg: sanitizeCssColorToken(raw.panelBg, d.panelBg),
|
||||
panelBorder: sanitizeCssColorToken(raw.panelBorder, d.panelBorder),
|
||||
borderWidthPx: (() => {
|
||||
let w = Number(raw.borderWidthPx);
|
||||
if (!Number.isFinite(w)) w = d.borderWidthPx;
|
||||
return Math.max(0, Math.min(12, Math.round(w)));
|
||||
})(),
|
||||
textColor: sanitizeCssColorToken(raw.textColor, d.textColor),
|
||||
};
|
||||
}
|
||||
|
||||
/** หมวด Quiz Battle (อ้างอิงธีมหน้า Quiz-Battle) — id ต้องตรงกับแอดมิน */
|
||||
const QUIZ_BATTLE_CATEGORY_IDS = new Set([
|
||||
'cybercrime',
|
||||
@@ -174,6 +216,35 @@ function clampCarrySessionLength(n, def) {
|
||||
return Math.max(0, Math.min(500, Math.floor(v)));
|
||||
}
|
||||
|
||||
function mergeQuizCarryFromMirrorIfSet(base) {
|
||||
if (!QUIZ_SETTINGS_MIRROR_PATH) return base;
|
||||
try {
|
||||
if (!fs.existsSync(QUIZ_SETTINGS_MIRROR_PATH)) return base;
|
||||
const raw = fs.readFileSync(QUIZ_SETTINGS_MIRROR_PATH, 'utf8');
|
||||
const j = JSON.parse(raw);
|
||||
if (!j || typeof j !== 'object') return base;
|
||||
if (j.carryMapPanelTheme != null && typeof j.carryMapPanelTheme === 'object') {
|
||||
base.carryMapPanelTheme = sanitizeCarryMapPanelTheme(j.carryMapPanelTheme);
|
||||
}
|
||||
if (j.carryReadMs != null) {
|
||||
base.carryReadMs = clampQuizBetweenMs(j.carryReadMs, base.carryReadMs);
|
||||
}
|
||||
if (j.carryAnswerMs != null) {
|
||||
base.carryAnswerMs = clampQuizMs(j.carryAnswerMs, base.carryAnswerMs);
|
||||
}
|
||||
if (j.carrySessionLength != null) {
|
||||
base.carrySessionLength = clampCarrySessionLength(j.carrySessionLength, base.carrySessionLength);
|
||||
}
|
||||
if (Array.isArray(j.carryQuestions) && j.carryQuestions.length) {
|
||||
base.carryQuestions = sanitizeCarryQuestions(j.carryQuestions);
|
||||
}
|
||||
return base;
|
||||
} catch (e) {
|
||||
console.error('mergeQuizCarryFromMirrorIfSet', e.message);
|
||||
return base;
|
||||
}
|
||||
}
|
||||
|
||||
function loadQuizSettings() {
|
||||
try {
|
||||
if (fs.existsSync(QUIZ_SETTINGS_PATH)) {
|
||||
@@ -182,20 +253,23 @@ function loadQuizSettings() {
|
||||
const questions = Array.isArray(j.questions) ? j.questions : [];
|
||||
const carryQuestions = sanitizeCarryQuestions(j.carryQuestions);
|
||||
const battleQuizMcq = sanitizeBattleQuizMcq(j.battleQuizMcq);
|
||||
return {
|
||||
const carryMapPanelTheme = sanitizeCarryMapPanelTheme(j.carryMapPanelTheme);
|
||||
const out = {
|
||||
readMs: clampQuizMs(j.readMs, d.readMs),
|
||||
answerMs: clampQuizMs(j.answerMs, d.answerMs),
|
||||
betweenMs: clampQuizBetweenMs(j.betweenMs, d.betweenMs),
|
||||
carryReadMs: clampQuizBetweenMs(j.carryReadMs, d.carryReadMs),
|
||||
carryAnswerMs: clampQuizMs(j.carryAnswerMs, d.carryAnswerMs),
|
||||
carrySessionLength: clampCarrySessionLength(j.carrySessionLength, d.carrySessionLength),
|
||||
carryMapPanelTheme,
|
||||
questions,
|
||||
carryQuestions,
|
||||
battleQuizMcq,
|
||||
};
|
||||
return mergeQuizCarryFromMirrorIfSet(out);
|
||||
}
|
||||
} catch (e) { console.error('loadQuizSettings', e.message); }
|
||||
return defaultQuizSettings();
|
||||
return mergeQuizCarryFromMirrorIfSet(defaultQuizSettings());
|
||||
}
|
||||
|
||||
const GAUNTLET_DEFAULT_TICK_MS = 220;
|
||||
@@ -500,6 +574,9 @@ function saveQuizSettings(d) {
|
||||
const battleQuizMcq = Array.isArray(d.battleQuizMcq)
|
||||
? sanitizeBattleQuizMcq(d.battleQuizMcq)
|
||||
: (prev.battleQuizMcq || []);
|
||||
const carryMapPanelTheme = d.carryMapPanelTheme != null && typeof d.carryMapPanelTheme === 'object'
|
||||
? sanitizeCarryMapPanelTheme(d.carryMapPanelTheme)
|
||||
: prev.carryMapPanelTheme;
|
||||
const out = {
|
||||
readMs,
|
||||
answerMs,
|
||||
@@ -507,6 +584,7 @@ function saveQuizSettings(d) {
|
||||
carryReadMs,
|
||||
carryAnswerMs,
|
||||
carrySessionLength,
|
||||
carryMapPanelTheme,
|
||||
questions,
|
||||
carryQuestions,
|
||||
battleQuizMcq,
|
||||
@@ -516,6 +594,7 @@ function saveQuizSettings(d) {
|
||||
ok: true,
|
||||
battleQuizMcqSaved: (out.battleQuizMcq || []).length,
|
||||
carryQuestionsSaved: (out.carryQuestions || []).length,
|
||||
carryMapPanelTheme: out.carryMapPanelTheme,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('saveQuizSettings', e.message);
|
||||
@@ -1139,6 +1218,31 @@ const server = http.createServer((req, res) => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const quizCarryDiskUrlPath = url.split('?')[0].replace(/\/+$/, '') || '/';
|
||||
const quizCarryDiskPaths = new Set([
|
||||
BASE_PATH + '/api/quiz-carry-from-disk',
|
||||
BASE_PATH + '/api-quiz-carry-from-disk.php',
|
||||
]);
|
||||
if (quizCarryDiskPaths.has(quizCarryDiskUrlPath)) {
|
||||
if (req.method === 'GET') {
|
||||
const g = loadQuizSettings();
|
||||
const slice = {
|
||||
carryMapPanelTheme: g.carryMapPanelTheme,
|
||||
carryReadMs: g.carryReadMs,
|
||||
carryAnswerMs: g.carryAnswerMs,
|
||||
carrySessionLength: g.carrySessionLength,
|
||||
carryQuestions: g.carryQuestions,
|
||||
};
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Cache-Control': 'no-store, no-cache, must-revalidate',
|
||||
Pragma: 'no-cache',
|
||||
});
|
||||
return res.end(JSON.stringify(slice));
|
||||
}
|
||||
res.writeHead(405);
|
||||
return res.end(JSON.stringify({ ok: false, error: 'Method not allowed' }));
|
||||
}
|
||||
const quizSettingsUrlPath = url.split('?')[0].replace(/\/+$/, '') || '/';
|
||||
if (quizSettingsUrlPath === BASE_PATH + '/api/quiz-settings') {
|
||||
if (req.method === 'GET') {
|
||||
@@ -1166,6 +1270,7 @@ const server = http.createServer((req, res) => {
|
||||
ok: true,
|
||||
battleQuizMcqSaved: saved.battleQuizMcqSaved,
|
||||
carryQuestionsSaved: saved.carryQuestionsSaved,
|
||||
carryMapPanelTheme: saved.carryMapPanelTheme,
|
||||
}));
|
||||
} catch (e) {
|
||||
res.writeHead(400);
|
||||
@@ -2365,6 +2470,17 @@ io.on('connection', (socket) => {
|
||||
suspectPhaseActive: !!space.suspectPhaseActive && serverMapIsPostCaseLobbyB(space),
|
||||
suspectPickIndex: typeof space.suspectPickIndex === 'number' ? space.suspectPickIndex : 0,
|
||||
};
|
||||
if (mdJoin.gameType === 'quiz_carry' || spaceAllowsQuizCarryLobbyRelaxed(space)) {
|
||||
try {
|
||||
const qs = loadQuizSettings();
|
||||
joinCb.quizCarrySettingsSnap = {
|
||||
carryMapPanelTheme: qs.carryMapPanelTheme,
|
||||
carryReadMs: qs.carryReadMs,
|
||||
carryAnswerMs: qs.carryAnswerMs,
|
||||
carrySessionLength: qs.carrySessionLength,
|
||||
};
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
if (mdJoin.gameType === 'gauntlet' && space.gauntletRun) {
|
||||
joinCb.gauntletEndsAt = space.gauntletRun.endsAt != null ? space.gauntletRun.endsAt : null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user