minigame 1 question 1.1

This commit is contained in:
2026-05-02 06:21:45 +00:00
parent 306637996e
commit e55dadf41c
46 changed files with 1021 additions and 66 deletions
+5
View File
@@ -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
+134
View File
@@ -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) {
+4 -1
View File
@@ -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'];
}
+55
View File
@@ -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>
File diff suppressed because one or more lines are too long
+8
View File
@@ -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];
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

File diff suppressed because it is too large Load Diff
+2
View File
@@ -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() {
+73 -6
View File
@@ -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>
+5
View File
@@ -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;
+47 -5
View File
@@ -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) ต่อช่อง 116 */
@@ -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;
}