minigame 1 question 1.1
@@ -43,6 +43,11 @@ VIOLENT_CRIME="$DST/Game/public/img/ViolentCrime"
|
||||
mkdir -p "$VIOLENT_CRIME"
|
||||
chown www-data:www-data "$VIOLENT_CRIME"
|
||||
chmod 2775 "$VIOLENT_CRIME"
|
||||
# Editor / เกม — รูปคำถามหรือ asset ชื่อ QUESTION (FTP MKD มัก 550 ถ้ายังไม่มีบนเซิร์ฟ)
|
||||
QUESTION_IMG="$DST/Game/public/img/QUESTION"
|
||||
mkdir -p "$QUESTION_IMG"
|
||||
chown www-data:www-data "$QUESTION_IMG"
|
||||
chmod 2775 "$QUESTION_IMG"
|
||||
|
||||
GAME_SRV_HASH_AFTER="$(hash_game_server_js "$GAME_SERVER_JS")"
|
||||
if [[ -n "$GAME_SRV_HASH_AFTER" && "$GAME_SRV_HASH_BEFORE" != "$GAME_SRV_HASH_AFTER" ]]; then
|
||||
|
||||
@@ -188,6 +188,11 @@
|
||||
var bMs = data.betweenMs != null ? data.betweenMs : 3500;
|
||||
el('quiz-between-sec').value = String(Math.max(0, Math.round(bMs / 1000)));
|
||||
renderQuizAdminQuestions(data.questions);
|
||||
if (data.quizMapPanelTheme && typeof data.quizMapPanelTheme === 'object') {
|
||||
quizTfFillFormFromTheme(data.quizMapPanelTheme);
|
||||
} else {
|
||||
quizTfFillFormFromTheme({});
|
||||
}
|
||||
setMsg('quiz-settings-msg', '', '');
|
||||
}).catch(function (e) {
|
||||
setMsg('quiz-settings-msg', e.message || 'โหลดไม่ได้', 'error');
|
||||
@@ -219,6 +224,7 @@
|
||||
answerMs: ansS * 1000,
|
||||
betweenMs: betS * 1000,
|
||||
questions: questions,
|
||||
quizMapPanelTheme: quizTfBuildThemeForSave(),
|
||||
}).then(function () {
|
||||
setMsg('quiz-settings-msg', 'บันทึกแล้ว', 'ok');
|
||||
}).catch(function (e) {
|
||||
@@ -226,6 +232,134 @@
|
||||
});
|
||||
}
|
||||
|
||||
function quizTfThemeHiddenId(kind) {
|
||||
if (kind === 'bg') return 'quiz-tf-theme-bg';
|
||||
if (kind === 'border') return 'quiz-tf-theme-border';
|
||||
return 'quiz-tf-theme-text';
|
||||
}
|
||||
|
||||
function quizTfRowPrefix(kind) {
|
||||
return 'quiz-tf-theme-' + kind;
|
||||
}
|
||||
|
||||
function quizTfSetPickersFromColorString(kind, cssStr) {
|
||||
var fb = QUIZ_CARRY_THEME_DEFAULTS[kind];
|
||||
var o = quizCarryParseCssColor(cssStr, fb);
|
||||
var pref = quizTfRowPrefix(kind);
|
||||
var sw = el(pref + '-swatch');
|
||||
var ra = el(pref + '-alpha');
|
||||
var out = el(pref + '-alpha-out');
|
||||
var hi = el(quizTfThemeHiddenId(kind));
|
||||
var pr = el(pref + '-preview');
|
||||
if (!sw || !ra || !hi) return;
|
||||
sw.value = quizCarryHexFromRgb(o.r, o.g, o.b);
|
||||
var pct = Math.max(0, Math.min(100, Math.round(Number(o.a) * 100)));
|
||||
ra.value = String(pct);
|
||||
if (out) out.textContent = pct + '%';
|
||||
hi.value = quizCarryRgbToRgbaString(o);
|
||||
if (pr) pr.textContent = hi.value;
|
||||
}
|
||||
|
||||
function quizTfComposeFromPickers(kind) {
|
||||
var pref = quizTfRowPrefix(kind);
|
||||
var sw = el(pref + '-swatch');
|
||||
var ra = el(pref + '-alpha');
|
||||
if (!sw || !ra) return '';
|
||||
var pr = quizCarryParseHexToRgb(sw.value);
|
||||
if (!pr) return '';
|
||||
var pct = parseInt(String(ra.value), 10);
|
||||
if (!Number.isFinite(pct)) pct = 100;
|
||||
pct = Math.max(0, Math.min(100, pct));
|
||||
return quizCarryRgbToRgbaString({ r: pr.r, g: pr.g, b: pr.b, a: pct / 100 });
|
||||
}
|
||||
|
||||
function quizTfSyncRowOutput(kind) {
|
||||
var s = quizTfComposeFromPickers(kind);
|
||||
var hi = el(quizTfThemeHiddenId(kind));
|
||||
var pref = quizTfRowPrefix(kind);
|
||||
var ra = el(pref + '-alpha');
|
||||
var out = el(pref + '-alpha-out');
|
||||
var pr = el(pref + '-preview');
|
||||
if (hi) hi.value = s;
|
||||
if (out && ra) out.textContent = String(Math.max(0, Math.min(100, parseInt(ra.value, 10) || 0))) + '%';
|
||||
if (pr && hi) pr.textContent = hi.value;
|
||||
}
|
||||
|
||||
function quizTfBindThemePickersOnce() {
|
||||
if (quizTfBindThemePickersOnce._done) return;
|
||||
['bg', 'border', 'text'].forEach(function (kind) {
|
||||
var pref = quizTfRowPrefix(kind);
|
||||
var sw = el(pref + '-swatch');
|
||||
var ra = el(pref + '-alpha');
|
||||
if (!sw || !ra) return;
|
||||
if (ra.getAttribute('data-tf-bound') === '1') return;
|
||||
ra.setAttribute('data-tf-bound', '1');
|
||||
function sync() {
|
||||
quizTfSyncRowOutput(kind);
|
||||
}
|
||||
sw.addEventListener('input', sync);
|
||||
sw.addEventListener('change', sync);
|
||||
ra.addEventListener('input', sync);
|
||||
ra.addEventListener('change', sync);
|
||||
});
|
||||
quizTfBindThemePickersOnce._done = true;
|
||||
}
|
||||
|
||||
function quizTfBuildThemeForSave() {
|
||||
quizTfBindThemePickersOnce();
|
||||
['bg', 'border', 'text'].forEach(function (k) {
|
||||
quizTfSyncRowOutput(k);
|
||||
});
|
||||
var fbBg = QUIZ_CARRY_THEME_DEFAULTS.bg;
|
||||
var fbBr = QUIZ_CARRY_THEME_DEFAULTS.border;
|
||||
var fbTx = QUIZ_CARRY_THEME_DEFAULTS.text;
|
||||
var sBg = quizTfComposeFromPickers('bg');
|
||||
var sBr = quizTfComposeFromPickers('border');
|
||||
var sTx = quizTfComposeFromPickers('text');
|
||||
var bwEl = el('quiz-tf-theme-border-w');
|
||||
var bw = bwEl ? parseInt(String(bwEl.value || '2'), 10) : 2;
|
||||
if (!Number.isFinite(bw)) bw = 2;
|
||||
bw = Math.max(0, Math.min(12, Math.round(bw)));
|
||||
var qfMinEl = el('quiz-tf-theme-qfont-min');
|
||||
var qfMaxEl = el('quiz-tf-theme-qfont-max');
|
||||
var qMin = qfMinEl ? parseInt(String(qfMinEl.value || '10'), 10) : 10;
|
||||
var qMax = qfMaxEl ? parseInt(String(qfMaxEl.value || '24'), 10) : 24;
|
||||
if (!Number.isFinite(qMin)) qMin = 10;
|
||||
if (!Number.isFinite(qMax)) qMax = 24;
|
||||
qMin = Math.max(10, Math.min(40, Math.round(qMin)));
|
||||
qMax = Math.max(14, Math.min(56, Math.round(qMax)));
|
||||
if (qMax < qMin) {
|
||||
var swap = qMin;
|
||||
qMin = qMax;
|
||||
qMax = swap;
|
||||
}
|
||||
return {
|
||||
panelBg: (sBg && sBg.trim()) ? sBg.trim().slice(0, 120) : quizCarryRgbToRgbaString(fbBg),
|
||||
panelBorder: (sBr && sBr.trim()) ? sBr.trim().slice(0, 120) : quizCarryRgbToRgbaString(fbBr),
|
||||
textColor: (sTx && sTx.trim()) ? sTx.trim().slice(0, 120) : quizCarryRgbToRgbaString(fbTx),
|
||||
borderWidthPx: bw,
|
||||
questionFontMinPx: qMin,
|
||||
questionFontMaxPx: qMax,
|
||||
};
|
||||
}
|
||||
|
||||
function quizTfFillFormFromTheme(th) {
|
||||
quizTfBindThemePickersOnce();
|
||||
var t = th && typeof th === 'object' ? th : {};
|
||||
quizTfSetPickersFromColorString('bg', t.panelBg);
|
||||
quizTfSetPickersFromColorString('border', t.panelBorder);
|
||||
quizTfSetPickersFromColorString('text', t.textColor);
|
||||
var bwEl = el('quiz-tf-theme-border-w');
|
||||
var bw = parseInt(String(t.borderWidthPx), 10);
|
||||
if (bwEl) bwEl.value = String(Number.isFinite(bw) && bw >= 0 ? Math.min(12, bw) : 2);
|
||||
var qfMinEl = el('quiz-tf-theme-qfont-min');
|
||||
var qfMaxEl = el('quiz-tf-theme-qfont-max');
|
||||
var qMin = parseInt(String(t.questionFontMinPx), 10);
|
||||
var qMax = parseInt(String(t.questionFontMaxPx), 10);
|
||||
if (qfMinEl) qfMinEl.value = String(Number.isFinite(qMin) ? Math.max(10, Math.min(40, qMin)) : 10);
|
||||
if (qfMaxEl) qfMaxEl.value = String(Number.isFinite(qMax) ? Math.max(14, Math.min(56, qMax)) : 24);
|
||||
}
|
||||
|
||||
var QUIZ_CARRY_MAX_SLOTS = 16;
|
||||
|
||||
function rebuildQuizCarryCorrectSelect(row) {
|
||||
|
||||
@@ -52,7 +52,7 @@ function merge_quiz_settings_disk_from_put(string $rawBody): bool
|
||||
if ($hadFileButInvalidJson) {
|
||||
return false;
|
||||
}
|
||||
$mergeKeys = ['readMs', 'answerMs', 'betweenMs', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryMapPanelTheme', 'carryEmbedCountdownTheme', 'carryChoicePlaqueTheme', 'carryChoicePlaqueThemes', 'carryChoicePlaqueMapScale', 'carryWalkSpeedMultForMapId', 'carryWalkSpeedMult', 'questions', 'carryQuestions', 'battleQuizMcq'];
|
||||
$mergeKeys = ['readMs', 'answerMs', 'betweenMs', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryMapPanelTheme', 'quizMapPanelTheme', 'carryEmbedCountdownTheme', 'carryChoicePlaqueTheme', 'carryChoicePlaqueThemes', 'carryChoicePlaqueMapScale', 'carryWalkSpeedMultForMapId', 'carryWalkSpeedMult', 'questions', 'carryQuestions', 'battleQuizMcq'];
|
||||
foreach ($mergeKeys as $k) {
|
||||
if (array_key_exists($k, $patch)) {
|
||||
$base[$k] = $patch[$k];
|
||||
@@ -99,6 +99,9 @@ function overlay_quiz_settings_from_disk(string $nodeBody): string
|
||||
if (array_key_exists('carryMapPanelTheme', $disk) && is_array($disk['carryMapPanelTheme'])) {
|
||||
$j['carryMapPanelTheme'] = $disk['carryMapPanelTheme'];
|
||||
}
|
||||
if (array_key_exists('quizMapPanelTheme', $disk) && is_array($disk['quizMapPanelTheme'])) {
|
||||
$j['quizMapPanelTheme'] = $disk['quizMapPanelTheme'];
|
||||
}
|
||||
if (array_key_exists('carryEmbedCountdownTheme', $disk) && is_array($disk['carryEmbedCountdownTheme'])) {
|
||||
$j['carryEmbedCountdownTheme'] = $disk['carryEmbedCountdownTheme'];
|
||||
}
|
||||
|
||||
@@ -154,6 +154,61 @@
|
||||
<label>พักระหว่างข้อ (วินาที) <input type="number" id="quiz-between-sec" min="0" max="120" step="1" value="3"></label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="quiz-tf-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-tf-theme-legend" style="font-size:0.92rem;font-weight:600;padding:0 0.35rem">แผงคำถามบนแผนที่ (โซนทองใน Editor)</legend>
|
||||
<p class="muted" style="margin:0 0 0.65rem;font-size:0.82rem;line-height:1.45">ใช้กับ <code>#quiz-map-question-panel</code> ในโหมด <strong>เกมตอบคำถามถูก/ผิด</strong> · เก็บที่ <code>quizMapPanelTheme</code> ใน <code>quiz-settings.json</code> · <em>English:</em> Panel BG, border, text — same as carry panel theme pickers.</p>
|
||||
<div class="quiz-carry-theme-grid">
|
||||
<div class="quiz-carry-theme-row" data-quiz-tf-theme="bg">
|
||||
<span class="quiz-carry-theme-label">พื้นหลังแผง</span>
|
||||
<div class="quiz-carry-color-controls">
|
||||
<input type="color" id="quiz-tf-theme-bg-swatch" value="#0c0e1c" title="เลือกสี" aria-label="สีพื้นหลังแผงคำถามถูกผิด" />
|
||||
<label class="quiz-carry-alpha-label"><span>โปร่ง A</span>
|
||||
<input type="range" id="quiz-tf-theme-bg-alpha" min="0" max="100" value="88" step="1" aria-label="ความโปร่งใสพื้นหลัง" />
|
||||
<output id="quiz-tf-theme-bg-alpha-out" class="quiz-carry-alpha-out" for="quiz-tf-theme-bg-alpha">88%</output>
|
||||
</label>
|
||||
<code id="quiz-tf-theme-bg-preview" class="quiz-carry-theme-code" aria-hidden="true"></code>
|
||||
</div>
|
||||
<input type="hidden" id="quiz-tf-theme-bg" value="" />
|
||||
</div>
|
||||
<div class="quiz-carry-theme-row" data-quiz-tf-theme="border">
|
||||
<span class="quiz-carry-theme-label">สีขอบ</span>
|
||||
<div class="quiz-carry-color-controls">
|
||||
<input type="color" id="quiz-tf-theme-border-swatch" value="#ffd666" title="เลือกสีขอบ" aria-label="สีขอบแผง" />
|
||||
<label class="quiz-carry-alpha-label"><span>โปร่ง A</span>
|
||||
<input type="range" id="quiz-tf-theme-border-alpha" min="0" max="100" value="70" step="1" aria-label="ความโปร่งใสขอบ" />
|
||||
<output id="quiz-tf-theme-border-alpha-out" class="quiz-carry-alpha-out" for="quiz-tf-theme-border-alpha">70%</output>
|
||||
</label>
|
||||
<code id="quiz-tf-theme-border-preview" class="quiz-carry-theme-code" aria-hidden="true"></code>
|
||||
</div>
|
||||
<input type="hidden" id="quiz-tf-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-tf-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-tf-theme="text">
|
||||
<span class="quiz-carry-theme-label">สีข้อความ</span>
|
||||
<div class="quiz-carry-color-controls">
|
||||
<input type="color" id="quiz-tf-theme-text-swatch" value="#f1f5f9" title="เลือกสีข้อความ" aria-label="สีข้อความคำถาม" />
|
||||
<label class="quiz-carry-alpha-label"><span>โปร่ง A</span>
|
||||
<input type="range" id="quiz-tf-theme-text-alpha" min="0" max="100" value="100" step="1" aria-label="ความโปร่งใสข้อความ" />
|
||||
<output id="quiz-tf-theme-text-alpha-out" class="quiz-carry-alpha-out" for="quiz-tf-theme-text-alpha">100%</output>
|
||||
</label>
|
||||
<code id="quiz-tf-theme-text-preview" class="quiz-carry-theme-code" aria-hidden="true"></code>
|
||||
</div>
|
||||
<input type="hidden" id="quiz-tf-theme-text" 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-tf-theme-qfont-min" class="admin-inp-num" min="10" max="40" step="1" value="10" />
|
||||
</label>
|
||||
<label class="admin-field quiz-carry-theme-label">สูงสุด (px)
|
||||
<input type="number" id="quiz-tf-theme-qfont-max" class="admin-inp-num" min="14" max="56" step="1" value="24" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<h3 class="admin-subheading">รายการคำถาม</h3>
|
||||
<p class="muted">แต่ละข้อ: ข้อความ + คำตอบที่<strong>ถูกต้อง</strong> (ถูก=จริง / ผิด=เท็จ)</p>
|
||||
<div id="quiz-admin-questions-list" class="quiz-admin-questions-list"></div>
|
||||
|
||||
@@ -13,6 +13,14 @@
|
||||
"questionFontMinPx": 10,
|
||||
"questionFontMaxPx": 24
|
||||
},
|
||||
"quizMapPanelTheme": {
|
||||
"panelBg": "rgba(12, 14, 28, 0.88)",
|
||||
"panelBorder": "rgba(255, 214, 102, 0.7)",
|
||||
"textColor": "rgba(255, 224, 102, 1)",
|
||||
"borderWidthPx": 2,
|
||||
"questionFontMinPx": 10,
|
||||
"questionFontMaxPx": 28
|
||||
},
|
||||
"carryEmbedCountdownTheme": {
|
||||
"overlayBackdrop": "rgba(8, 10, 20, 0)",
|
||||
"innerBg": "rgba(12, 14, 28, 0)",
|
||||
|
||||
@@ -25,12 +25,12 @@ if (!is_array($data)) {
|
||||
}
|
||||
|
||||
$out = [];
|
||||
$keys = ['carryMapPanelTheme', 'carryEmbedCountdownTheme', 'carryChoicePlaqueThemes', 'carryChoicePlaqueTheme', 'carryChoicePlaqueMapScale', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryWalkSpeedMultForMapId', 'carryWalkSpeedMult', 'carryQuestions'];
|
||||
$keys = ['carryMapPanelTheme', 'quizMapPanelTheme', 'carryEmbedCountdownTheme', 'carryChoicePlaqueThemes', 'carryChoicePlaqueTheme', 'carryChoicePlaqueMapScale', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryWalkSpeedMultForMapId', 'carryWalkSpeedMult', 'carryQuestions'];
|
||||
foreach ($keys as $k) {
|
||||
if (!array_key_exists($k, $data)) {
|
||||
continue;
|
||||
}
|
||||
if ($k === 'carryMapPanelTheme' || $k === 'carryEmbedCountdownTheme' || $k === 'carryChoicePlaqueTheme') {
|
||||
if ($k === 'carryMapPanelTheme' || $k === 'quizMapPanelTheme' || $k === 'carryEmbedCountdownTheme' || $k === 'carryChoicePlaqueTheme') {
|
||||
if (is_array($data[$k])) {
|
||||
$out[$k] = $data[$k];
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 526 B |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 2.9 MiB |
@@ -0,0 +1,24 @@
|
||||
QUESTION — รูปสำหรับฉาก / อัปโหลด
|
||||
================================
|
||||
|
||||
ฉาก quiz รหัส **mng8a80o** ใช้ flow ภารกิจแบบ crown (HOWTO → READY → นับ 3-2-1 → เล่น → สรุป)
|
||||
โหลดรูปจาก URL ประมาณ `/Game/img/QUESTION/` — ตั้งชื่อให้ตรงกับนี้ (เหมือนโฟลเดอร์ ViolentCrime):
|
||||
|
||||
- **popup-Howto.png** — พื้นหลังหน้า HOWTO / คำแนะนำก่อนเริ่ม
|
||||
- **popup-result.png** — พื้นหลังแผงสรุปคะแนนท้ายเกม
|
||||
- **hud-time-plaque.png** — (ทางเลือก) แผ่นตกแต่งด้านหลังตัวเลข **TIME** บน Cyber HUD กลางจอ ระหว่างเล่น
|
||||
- **hud-question-plaque.png** — (ทางเลือก) แผ่นพื้นหลังข้อความคำถามใต้ TIME ตาม mock
|
||||
|
||||
ถ้าไฟล์หาย ระบบ fallback ไป `/Game/img/gauntlet-assets/` ชื่อเดียวกัน (เฉพาะ popup-*) · แผ่น HUD ถ้าไม่มีไฟล์จะใช้ข้อความล้วน
|
||||
|
||||
English: Map **mng8a80o** uses mission UI; place **popup-Howto.png** and **popup-result.png** here. Optional HUD plates: **hud-time-plaque.png**, **hud-question-plaque.png**.
|
||||
|
||||
โฟลเดอร์นี้ถูกสร้างจาก repo + สคริปต์ deploy เพื่อหลีกเลี่ยงข้อผิดพลาด FTP:
|
||||
|
||||
550 Create directory operation failed. (MKD)
|
||||
|
||||
ถ้า FTP ยัง 550: ตรวจว่าเชื่อมที่โฟลเดอร์ **public/img** ใต้ document root (เช่น
|
||||
`/var/www/html/Game/public/img/QUESTION`) ไม่ใช่แค่ `/Game/...` บน chroot คนละระดับ
|
||||
|
||||
English: This folder is created on deploy with www-data + setgid (2775) so uploads work.
|
||||
If MKD still fails, the directory may already exist — use LIST then STOR into QUESTION.
|
||||
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 570 KiB |
|
After Width: | Height: | Size: 203 B |
|
After Width: | Height: | Size: 236 B |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 484 B |
|
After Width: | Height: | Size: 481 B |
|
After Width: | Height: | Size: 507 B |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 473 B |
|
After Width: | Height: | Size: 485 B |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 256 B |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
@@ -489,6 +489,7 @@
|
||||
: null;
|
||||
if (!bounds || !mapTransform || mapTransform.cw == null) {
|
||||
panel.classList.add('is-hidden');
|
||||
panel.classList.remove('quiz-map-question-panel--true-false');
|
||||
panel.setAttribute('aria-hidden', 'true');
|
||||
return;
|
||||
}
|
||||
@@ -509,6 +510,7 @@
|
||||
panel.style.height = Math.round(Math.max(40, hPx)) + 'px';
|
||||
panel.classList.remove('is-hidden');
|
||||
panel.setAttribute('aria-hidden', 'false');
|
||||
panel.classList.add('quiz-map-question-panel--true-false');
|
||||
}
|
||||
|
||||
function drawLobbyMap() {
|
||||
|
||||
@@ -224,6 +224,10 @@
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--qmap-shadow);
|
||||
}
|
||||
/* เกมถูก/ผิด: จัดข้อความกึ่งกลางแนวตั้งในโซนทอง (quiz_carry ไม่ใส่คลาสนี้ — ยังใช้ไอคอน/คะแนนด้านล่าง) */
|
||||
#quiz-map-question-panel.quiz-map-question-panel--true-false {
|
||||
justify-content: center;
|
||||
}
|
||||
#quiz-map-question-panel.is-hidden { display: none !important; }
|
||||
#quiz-map-question-panel::-webkit-scrollbar {
|
||||
display: none;
|
||||
@@ -245,6 +249,8 @@
|
||||
flex: 0 1 auto;
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
align-self: center;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
@@ -590,7 +596,9 @@
|
||||
justify-self: end;
|
||||
}
|
||||
.play-quiz-scoreboard-me .play-quiz-scoreboard-val { color: #9ece6a; }
|
||||
/* แจ้งผลรอบคำถามแบบ toast — ปิดการแสดง (กรอบกลางบนรบกวน HUD) โค้ดยังอัปเดตข้อความได้สำหรับ a11y/ดีบัก */
|
||||
#play-quiz-feedback {
|
||||
display: none !important;
|
||||
position: fixed;
|
||||
top: max(10px, env(safe-area-inset-top));
|
||||
left: 50%;
|
||||
@@ -1464,14 +1472,67 @@
|
||||
margin-right: 0.15rem;
|
||||
filter: drop-shadow(0 0 6px rgba(255, 220, 120, 0.7));
|
||||
}
|
||||
.play-cyber-time-block {
|
||||
/* กลางบน: TIME + (เฉพาะแมป quiz mng8a80o) แถบคำถาม */
|
||||
.play-cyber-center-stack {
|
||||
position: absolute;
|
||||
top: max(52px, calc(env(safe-area-inset-top) + 38px));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: clamp(4px, 1vmin, 12px);
|
||||
z-index: 4;
|
||||
max-width: min(94vw, 760px);
|
||||
pointer-events: none;
|
||||
}
|
||||
.play-cyber-time-block {
|
||||
position: relative;
|
||||
left: auto;
|
||||
top: auto;
|
||||
transform: none;
|
||||
text-align: center;
|
||||
min-width: 120px;
|
||||
}
|
||||
.play-cyber-quiz-mission-q-band {
|
||||
position: relative;
|
||||
width: min(92vw, 680px);
|
||||
max-width: 100%;
|
||||
padding: clamp(10px, 2vmin, 18px) clamp(12px, 2.5vmin, 22px);
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
/* is-hidden ใช้ทั่วโปรเจกต์แต่หลายจุดไม่มีกฎ display:none — แถบนี้ซ่อนแล้วต้องไม่กินที่และไม่เห็นกรอบว่าง */
|
||||
#play-cyber-quiz-mission-q-band.is-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
.play-cyber-quiz-mission-q-plaque {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: fill;
|
||||
object-position: center;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
opacity: 0.95;
|
||||
}
|
||||
.play-cyber-quiz-mission-q-text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
font-family: 'Sarabun', 'Noto Sans Thai', system-ui, sans-serif;
|
||||
font-size: clamp(0.78rem, 2.1vw, 1.05rem);
|
||||
font-weight: 700;
|
||||
line-height: 1.45;
|
||||
color: #ffe066;
|
||||
text-shadow:
|
||||
0 0 12px rgba(255, 200, 80, 0.55),
|
||||
0 2px 14px rgba(0, 0, 0, 0.85);
|
||||
white-space: pre-line;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
.play-cyber-time-label {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 0.62rem;
|
||||
@@ -1786,10 +1847,16 @@
|
||||
<div class="play-cyber-panel-title">SCORE</div>
|
||||
<ul id="play-cyber-score-list" class="play-cyber-score-list"></ul>
|
||||
</aside>
|
||||
<div class="play-cyber-time-block">
|
||||
<div class="play-cyber-time-label">TIME</div>
|
||||
<div id="play-cyber-time-val" class="play-cyber-time-val">0</div>
|
||||
<div id="play-cyber-time-sub" class="play-cyber-time-sub"></div>
|
||||
<div class="play-cyber-center-stack">
|
||||
<div class="play-cyber-time-block">
|
||||
<div class="play-cyber-time-label">TIME</div>
|
||||
<div id="play-cyber-time-val" class="play-cyber-time-val">0</div>
|
||||
<div id="play-cyber-time-sub" class="play-cyber-time-sub"></div>
|
||||
</div>
|
||||
<div id="play-cyber-quiz-mission-q-band" class="play-cyber-quiz-mission-q-band is-hidden" aria-hidden="true">
|
||||
<img id="play-cyber-quiz-mission-q-plaque" class="play-cyber-quiz-mission-q-plaque is-hidden" alt="" decoding="async" />
|
||||
<p id="play-cyber-quiz-mission-q-text" class="play-cyber-quiz-mission-q-text"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="play-cyber-self">
|
||||
<div class="play-cyber-self-frame">
|
||||
@@ -1899,7 +1966,7 @@
|
||||
</div>
|
||||
<script src="/Game/socket.io/socket.io.js"></script>
|
||||
<script src="js/version.js?v=0.0184"></script>
|
||||
<script src="js/play.js?v=0.231"></script>
|
||||
<script src="js/play.js?v=0.238"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -692,6 +692,9 @@
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--qmap-shadow);
|
||||
}
|
||||
#quiz-map-question-panel.quiz-map-question-panel--true-false {
|
||||
justify-content: center;
|
||||
}
|
||||
#quiz-map-question-panel.is-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -720,6 +723,8 @@
|
||||
flex: 0 1 auto;
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
align-self: center;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
|
||||
@@ -183,6 +183,8 @@ function defaultQuizSettings() {
|
||||
carrySessionLength: 0,
|
||||
/** quiz_carry: สีแผง #quiz-map-question-panel ในเกม/พรีวิว (จัดใน Admin แท็บหยิบมาวาง) */
|
||||
carryMapPanelTheme: defaultCarryMapPanelTheme(),
|
||||
/** เกมตอบคำถามถูก/ผิด: สีแผงคำถามบนแผนที่ (#quiz-map-question-panel) — Admin แท็บคำถามเกม */
|
||||
quizMapPanelTheme: defaultCarryMapPanelTheme(),
|
||||
/** quiz_carry: สี/ขอบ/ขนาดตัวเลขนับ 3-2-1 (embed พรีวิว) — Admin แท็บหยิบมาวาง */
|
||||
carryEmbedCountdownTheme: defaultCarryEmbedCountdownTheme(),
|
||||
/** quiz_carry: สีป้ายตัวเลือกบนแผนที่ (canvas) ต่อช่อง 1–16 */
|
||||
@@ -194,6 +196,8 @@ function defaultQuizSettings() {
|
||||
/** quiz_carry: ถ้าใส่รหัสฉาก + คูณ — เดินเร็วเฉพาะแมปนั้น (เทียบกับค่าเริ่มในไคลเอนต์) */
|
||||
carryWalkSpeedMultForMapId: '',
|
||||
carryWalkSpeedMult: null,
|
||||
/** ถูก/ผิด: สุ่มกี่ข้อต่อรอบจากชุดคำถาม (สูงสุดเท่าที่มีในชุด) */
|
||||
quizRoundQuestionCount: 10,
|
||||
questions: [],
|
||||
carryQuestions: [],
|
||||
battleQuizMcq: [],
|
||||
@@ -443,6 +447,17 @@ function clampCarrySessionLength(n, def) {
|
||||
return Math.max(0, Math.min(500, Math.floor(v)));
|
||||
}
|
||||
|
||||
/** เกมตอบคำถามถูก/ผิด: จำนวนข้อที่สุ่มจากชุดต่อรอบ (เซสชัน) — 1–50 */
|
||||
function clampQuizRoundQuestionCount(n, def) {
|
||||
const v = Number(n);
|
||||
if (!Number.isFinite(v)) {
|
||||
const d = Number(def);
|
||||
if (!Number.isFinite(d)) return 10;
|
||||
return Math.max(1, Math.min(50, Math.floor(d)));
|
||||
}
|
||||
return Math.max(1, Math.min(50, Math.floor(v)));
|
||||
}
|
||||
|
||||
function clampCarryChoicePlaqueMapScale(n, def) {
|
||||
const v = Number(n);
|
||||
if (Number.isNaN(v)) return def;
|
||||
@@ -475,6 +490,9 @@ function mergeQuizCarryFromMirrorIfSet(base) {
|
||||
if (j.carryMapPanelTheme != null && typeof j.carryMapPanelTheme === 'object') {
|
||||
base.carryMapPanelTheme = sanitizeCarryMapPanelTheme(j.carryMapPanelTheme);
|
||||
}
|
||||
if (j.quizMapPanelTheme != null && typeof j.quizMapPanelTheme === 'object') {
|
||||
base.quizMapPanelTheme = sanitizeCarryMapPanelTheme(j.quizMapPanelTheme);
|
||||
}
|
||||
if (j.carryReadMs != null) {
|
||||
base.carryReadMs = clampQuizBetweenMs(j.carryReadMs, base.carryReadMs);
|
||||
}
|
||||
@@ -504,6 +522,9 @@ function mergeQuizCarryFromMirrorIfSet(base) {
|
||||
if (Array.isArray(j.carryQuestions) && j.carryQuestions.length) {
|
||||
base.carryQuestions = sanitizeCarryQuestions(j.carryQuestions);
|
||||
}
|
||||
if (j.quizRoundQuestionCount != null) {
|
||||
base.quizRoundQuestionCount = clampQuizRoundQuestionCount(j.quizRoundQuestionCount, base.quizRoundQuestionCount);
|
||||
}
|
||||
return base;
|
||||
} catch (e) {
|
||||
console.error('mergeQuizCarryFromMirrorIfSet', e.message);
|
||||
@@ -520,6 +541,7 @@ function loadQuizSettings() {
|
||||
const carryQuestions = sanitizeCarryQuestions(j.carryQuestions);
|
||||
const battleQuizMcq = sanitizeBattleQuizMcq(j.battleQuizMcq);
|
||||
const carryMapPanelTheme = sanitizeCarryMapPanelTheme(j.carryMapPanelTheme);
|
||||
const quizMapPanelTheme = sanitizeCarryMapPanelTheme(j.quizMapPanelTheme);
|
||||
const carryEmbedCountdownTheme = sanitizeCarryEmbedCountdownTheme(j.carryEmbedCountdownTheme);
|
||||
const carryChoicePlaqueThemes = Array.isArray(j.carryChoicePlaqueThemes) && j.carryChoicePlaqueThemes.length
|
||||
? sanitizeCarryChoicePlaqueThemes(j.carryChoicePlaqueThemes)
|
||||
@@ -538,6 +560,7 @@ function loadQuizSettings() {
|
||||
carryAnswerMs: clampQuizMs(j.carryAnswerMs, d.carryAnswerMs),
|
||||
carrySessionLength: clampCarrySessionLength(j.carrySessionLength, d.carrySessionLength),
|
||||
carryMapPanelTheme,
|
||||
quizMapPanelTheme,
|
||||
carryEmbedCountdownTheme,
|
||||
carryChoicePlaqueThemes,
|
||||
carryChoicePlaqueTheme,
|
||||
@@ -1050,6 +1073,9 @@ function saveQuizSettings(d) {
|
||||
const carryMapPanelTheme = d.carryMapPanelTheme != null && typeof d.carryMapPanelTheme === 'object'
|
||||
? sanitizeCarryMapPanelTheme(d.carryMapPanelTheme)
|
||||
: prev.carryMapPanelTheme;
|
||||
const quizMapPanelTheme = d.quizMapPanelTheme != null && typeof d.quizMapPanelTheme === 'object'
|
||||
? sanitizeCarryMapPanelTheme(d.quizMapPanelTheme)
|
||||
: prev.quizMapPanelTheme;
|
||||
const carryEmbedCountdownTheme = d.carryEmbedCountdownTheme != null && typeof d.carryEmbedCountdownTheme === 'object'
|
||||
? sanitizeCarryEmbedCountdownTheme(d.carryEmbedCountdownTheme)
|
||||
: prev.carryEmbedCountdownTheme;
|
||||
@@ -1088,6 +1114,7 @@ function saveQuizSettings(d) {
|
||||
carryAnswerMs,
|
||||
carrySessionLength,
|
||||
carryMapPanelTheme,
|
||||
quizMapPanelTheme,
|
||||
carryEmbedCountdownTheme,
|
||||
carryChoicePlaqueThemes,
|
||||
carryChoicePlaqueTheme,
|
||||
@@ -1104,6 +1131,7 @@ function saveQuizSettings(d) {
|
||||
battleQuizMcqSaved: (out.battleQuizMcq || []).length,
|
||||
carryQuestionsSaved: (out.carryQuestions || []).length,
|
||||
carryMapPanelTheme: out.carryMapPanelTheme,
|
||||
quizMapPanelTheme: out.quizMapPanelTheme,
|
||||
carryEmbedCountdownTheme: out.carryEmbedCountdownTheme,
|
||||
carryChoicePlaqueThemes: out.carryChoicePlaqueThemes,
|
||||
carryChoicePlaqueTheme: out.carryChoicePlaqueTheme,
|
||||
@@ -1376,7 +1404,10 @@ function resolveQuizRound(sid, space, md) {
|
||||
if (!right) {
|
||||
st.cannotTrue = true;
|
||||
st.cannotFalse = true;
|
||||
const pos = quizWrongAnswerRespawnPosition(mdLive);
|
||||
const joinOrd = typeof p.spawnJoinOrder === 'number' && Number.isFinite(p.spawnJoinOrder)
|
||||
? Math.max(0, Math.floor(p.spawnJoinOrder))
|
||||
: [...space.peers.keys()].indexOf(peerId);
|
||||
const pos = quizWrongAnswerRespawnPosition(mdLive, joinOrd);
|
||||
p.x = pos.x;
|
||||
p.y = pos.y;
|
||||
ejectMoves.push({
|
||||
@@ -3312,9 +3343,10 @@ function pickRandomSpawnFromMap(md) {
|
||||
return { x: pick.x, y: pick.y };
|
||||
}
|
||||
|
||||
/** ตอบผิดในเกมคำถาม — กลับจุดเกิด (spawnArea / spawn) แล้วดีดออกนอกโซนถ้าจุดเกิดทับโซนตอบ */
|
||||
function quizWrongAnswerRespawnPosition(md) {
|
||||
const sp = pickRandomSpawnFromMap(md);
|
||||
/** ตอบผิดในเกมคำถาม — กลับจุดเกิดตามลำดับเข้า (เดียวกับ pickSpawnForJoin) แล้วดีดออกนอกโซนถ้าทับโซนตอบ */
|
||||
function quizWrongAnswerRespawnPosition(md, joinOrderIndex) {
|
||||
const ord = joinOrderIndex | 0;
|
||||
const sp = pickSpawnForJoin(md, ord);
|
||||
return findNearestOutsideQuizAnswerZones(md, Number(sp.x) + 0.5, Number(sp.y) + 0.5);
|
||||
}
|
||||
|
||||
@@ -3402,10 +3434,12 @@ io.on('connection', (socket) => {
|
||||
if (!space.hostId) space.hostId = socket.id;
|
||||
if (serverMapIsPostCaseLobbyB(space)) initTroublesomeState(space);
|
||||
const mdJoin = (space.mapId && maps.get(space.mapId)) || space.mapData;
|
||||
const spawnPt = pickSpawnForJoin(mdJoin, space.peers.size);
|
||||
const spawnJoinOrder = space.peers.size;
|
||||
const spawnPt = pickSpawnForJoin(mdJoin, spawnJoinOrder);
|
||||
const bbStartBalloons = Math.max(1, Math.min(12, Math.floor(Number(mdJoin.balloonBossBalloonsPerPlayer)) || 5));
|
||||
const peer = {
|
||||
id: socket.id, x: +spawnPt.x, y: +spawnPt.y, direction: 'down', nickname: nickname || 'ผู้เล่น', ready: false, characterId: characterId || null, voiceMicOn: true,
|
||||
spawnJoinOrder,
|
||||
gauntletJumpTicks: 0, gauntletScore: 0, gauntletJumpPending: false, gauntletEliminated: false, spaceShooterScore: 0,
|
||||
balloonBossScore: 0, balloonBossBalloons: mdJoin.gameType === 'balloon_boss' ? bbStartBalloons : 5, balloonBossEliminated: false,
|
||||
};
|
||||
@@ -3468,6 +3502,14 @@ io.on('connection', (socket) => {
|
||||
};
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
if (mdJoin.gameType === 'quiz') {
|
||||
try {
|
||||
const qsQuiz = loadQuizSettings();
|
||||
joinCb.quizSettingsSnap = {
|
||||
quizMapPanelTheme: qsQuiz.quizMapPanelTheme,
|
||||
};
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
if (mdJoin.gameType === 'gauntlet' && space.gauntletRun) {
|
||||
joinCb.gauntletEndsAt = space.gauntletRun.endsAt != null ? space.gauntletRun.endsAt : null;
|
||||
}
|
||||
|
||||