minigame 1 question 1.2

This commit is contained in:
2026-05-02 06:51:21 +00:00
parent f2706e5318
commit 39d3f8606e
8 changed files with 133 additions and 19 deletions
+4
View File
@@ -725,6 +725,10 @@ code {
margin-top: 0.35rem;
}
.quiz-round-q-fieldset .quiz-round-q-label {
max-width: 280px;
}
.quiz-admin-questions-list {
display: flex;
flex-direction: column;
+13
View File
@@ -187,6 +187,13 @@
el('quiz-answer-sec').value = String(Math.round((data.answerMs || 5000) / 1000));
var bMs = data.betweenMs != null ? data.betweenMs : 3500;
el('quiz-between-sec').value = String(Math.max(0, Math.round(bMs / 1000)));
var rqIn = el('quiz-round-q-count');
if (rqIn) {
var rq = data.quizRoundQuestionCount != null ? parseInt(data.quizRoundQuestionCount, 10) : 10;
if (Number.isNaN(rq) || rq < 1) rq = 10;
rq = Math.min(50, rq);
rqIn.value = String(rq);
}
renderQuizAdminQuestions(data.questions);
if (data.quizMapPanelTheme && typeof data.quizMapPanelTheme === 'object') {
quizTfFillFormFromTheme(data.quizMapPanelTheme);
@@ -210,6 +217,11 @@
ansS = Math.min(300, ansS);
betS = Math.min(120, betS);
var roundQEl = el('quiz-round-q-count');
var roundQ = roundQEl ? parseInt(roundQEl.value, 10) : 10;
if (Number.isNaN(roundQ) || roundQ < 1) roundQ = 10;
roundQ = Math.min(50, roundQ);
var questions = [];
document.querySelectorAll('#quiz-admin-questions-list .quiz-admin-q-row').forEach(function (row) {
var inp = row.querySelector('.quiz-admin-q-text');
@@ -223,6 +235,7 @@
readMs: readS * 1000,
answerMs: ansS * 1000,
betweenMs: betS * 1000,
quizRoundQuestionCount: roundQ,
questions: questions,
quizMapPanelTheme: quizTfBuildThemeForSave(),
}).then(function () {
+2 -2
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', 'quizMapPanelTheme', 'carryEmbedCountdownTheme', 'carryChoicePlaqueTheme', 'carryChoicePlaqueThemes', 'carryChoicePlaqueMapScale', 'carryWalkSpeedMultForMapId', 'carryWalkSpeedMult', 'questions', 'carryQuestions', 'battleQuizMcq'];
$mergeKeys = ['readMs', 'answerMs', 'betweenMs', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryMapPanelTheme', 'quizMapPanelTheme', 'carryEmbedCountdownTheme', 'carryChoicePlaqueTheme', 'carryChoicePlaqueThemes', 'carryChoicePlaqueMapScale', 'carryWalkSpeedMultForMapId', 'carryWalkSpeedMult', 'quizRoundQuestionCount', 'questions', 'carryQuestions', 'battleQuizMcq'];
foreach ($mergeKeys as $k) {
if (array_key_exists($k, $patch)) {
$base[$k] = $patch[$k];
@@ -106,7 +106,7 @@ function overlay_quiz_settings_from_disk(string $nodeBody): string
$j['carryEmbedCountdownTheme'] = $disk['carryEmbedCountdownTheme'];
}
/* ไม่ทับ carryChoicePlaqueThemes / carryChoicePlaqueTheme จากดิสก์ — ใช้ค่าจาก Node (อ่านไฟล์เดียวกันกับ disk merge) เพื่อกันทับด้วย JSON เก่าที่ไม่มี plaqueImageUrl ทำให้ช่อง URL ใน Admin ว่างหลังรีเฟรช */
foreach (['carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryChoicePlaqueMapScale', 'carryWalkSpeedMultForMapId', 'carryWalkSpeedMult'] as $ck) {
foreach (['carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryChoicePlaqueMapScale', 'carryWalkSpeedMultForMapId', 'carryWalkSpeedMult', 'quizRoundQuestionCount'] as $ck) {
if (array_key_exists($ck, $disk)) {
$j[$ck] = $disk[$ck];
}
+6 -1
View File
@@ -145,7 +145,7 @@
<section id="tab-panel-quiz" class="tab-panel card" hidden role="tabpanel" aria-labelledby="tab-quiz">
<h2>คำถามเกม (ถูก / ผิด)</h2>
<p class="muted">ใช้กับฉากประเภท <strong>เกมตอบคำถาม</strong> ใน Editor — สุ่มสูงสุด <strong>10 ข้อ</strong> ต่อรอบ · เวลาเป็นวินาที (เก็บที่เซิร์ฟเวอร์เกม <code>/Game/data/quiz-settings.json</code>)</p>
<p class="muted">ใช้กับฉากประเภท <strong>เกมตอบคำถาม</strong> ใน Editor — จำนวนข้อที่สุ่มต่อรอบตั้งได้ด้านล่าง (สูงสุด 50, ค่าเริ่ม 10) · เวลาเป็นวินาที (เก็บที่เซิร์ฟเวอร์เกม <code>/Game/data/quiz-settings.json</code>) · <em>English:</em> True/false quiz draws up to N shuffled questions per game session.</p>
<fieldset class="quiz-timing-fieldset">
<legend>เวลาในเกม</legend>
<div class="form-grid form-inline quiz-timing-grid">
@@ -154,6 +154,11 @@
<label>พักระหว่างข้อ (วินาที) <input type="number" id="quiz-between-sec" min="0" max="120" step="1" value="3"></label>
</div>
</fieldset>
<fieldset class="quiz-timing-fieldset quiz-round-q-fieldset">
<legend>จำนวนข้อต่อรอบ</legend>
<p class="muted" style="margin:0 0 0.65rem;font-size:0.82rem;line-height:1.45">สุ่มจากชุดคำถามทั้งหมด (Admin + แมป) แล้วเล่นตามลำดับ — ไม่เกินจำนวนข้อที่มีจริง · เก็บที่ <code>quizRoundQuestionCount</code> ใน <code>quiz-settings.json</code> · <em>English:</em> How many shuffled true/false questions per game (150, default 10).</p>
<label class="quiz-round-q-label" title="150 ข้อต่อรอบ">สุ่มสูงสุดกี่ข้อต่อรอบ <input type="number" id="quiz-round-q-count" min="1" max="50" step="1" value="10" aria-label="จำนวนข้อที่สุ่มต่อรอบ 1 ถึง 50"></label>
</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>
+5 -4
View File
@@ -1,5 +1,5 @@
{
"readMs": 30000,
"readMs": 5000,
"answerMs": 5000,
"betweenMs": 3000,
"carryReadMs": 3000,
@@ -8,14 +8,14 @@
"carryMapPanelTheme": {
"panelBg": "rgba(255, 0, 0, 0)",
"panelBorder": "rgba(255, 255, 255, 0)",
"textColor": "rgba(241, 245, 249, 1)",
"borderWidthPx": 0,
"textColor": "rgba(241, 245, 249, 1)",
"questionFontMinPx": 10,
"questionFontMaxPx": 24
},
"quizMapPanelTheme": {
"panelBg": "rgba(12, 14, 28, 0.88)",
"panelBorder": "rgba(255, 214, 102, 0.7)",
"panelBg": "rgba(12, 14, 28, 0)",
"panelBorder": "rgba(255, 214, 102, 0)",
"textColor": "rgba(255, 224, 102, 1)",
"borderWidthPx": 2,
"questionFontMinPx": 10,
@@ -175,6 +175,7 @@
"carryChoicePlaqueMapScale": 1.9,
"carryWalkSpeedMultForMapId": "",
"carryWalkSpeedMult": null,
"quizRoundQuestionCount": 3,
"questions": [
{
"text": "test1",
+79 -10
View File
@@ -1003,6 +1003,8 @@
let playQuizTimerInterval = null;
/** Preview-only: real question pool + phased timer (matches server quiz-settings / map quizQuestions). */
let previewQuizPool = [];
/** ดัชนีข้อในรอบพรีวิว (0..len-1) — หลังสับแล้วตัดตาม quizRoundQuestionCount เหมือนเซิร์ฟเวอร์ */
let previewQuizQIndex = 0;
let previewQuizTiming = { readMs: 10000, answerMs: 5000, betweenMs: 3500 };
let previewQuizStep = 'read';
let previewQuizCurrent = null;
@@ -2090,9 +2092,53 @@
.map((q) => ({ text: String(q.text).trim(), answerTrue: !!q.answerTrue }));
}
function pickRandomQuizFromPool(pool) {
if (!pool || !pool.length) return null;
return pool[Math.floor(Math.random() * pool.length)];
function shufflePreviewQuizQuestionsPlay(arr) {
const a = (arr || []).slice();
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
/** ตรงกับ server clampQuizRoundQuestionCount — 150 ค่าเริ่ม 10 */
function clampPreviewQuizRoundQuestionCount(settings) {
const v = Number(settings && settings.quizRoundQuestionCount);
if (!Number.isFinite(v)) return 10;
return Math.max(1, Math.min(50, Math.floor(v)));
}
function finishPreviewQuizSessionPlay() {
previewQuizStep = 'done';
playQuizPhaseLocal = null;
previewQuizCurrent = null;
playQuizPhaseEndsAt = 0;
if (playQuizTimerInterval) {
clearInterval(playQuizTimerInterval);
playQuizTimerInterval = null;
}
const qov = document.getElementById('quiz-game-overlay');
if (qov) qov.classList.add('is-hidden');
const mapPanel = document.getElementById('quiz-map-question-panel');
if (mapPanel) {
mapPanel.classList.add('is-hidden');
mapPanel.setAttribute('aria-hidden', 'true');
}
if (isQuizQuestionMissionUiMapPlay()) {
quizQuestionMissionPhase = 'ended';
applyQuizQuestionMissionPanelImages();
const mission = quizQuestionMissionBuildPayload();
showGauntletCrownMissionOverlay(mission);
return;
}
playQuizText = 'ครบทุกข้อในรอบทดสอบแล้ว';
if (qov) qov.classList.remove('is-hidden');
const phaseEl = document.getElementById('quiz-game-phase-label');
if (phaseEl) phaseEl.textContent = '[ทดสอบ] จบรอบ';
const qEl = document.getElementById('quiz-game-question');
if (qEl) qEl.textContent = playQuizText;
const leg = document.getElementById('quiz-play-legend');
if (leg) leg.textContent = 'จำนวนข้อต่อรอบใช้ค่าเดียวกับ Admin (quizRoundQuestionCount)';
}
function clearPreviewBotAnswerPaths() {
@@ -2123,7 +2169,9 @@
playQuizText = q.text;
playQuizPhaseEndsAt = Date.now() + previewQuizTiming.readMs;
const phaseEl = document.getElementById('quiz-game-phase-label');
if (phaseEl) phaseEl.textContent = '[ทดสอบ] อ่านคำถาม · สุ่มจากชุด ' + poolLen + ' ข้อ';
if (phaseEl) {
phaseEl.textContent = '[ทดสอบ] อ่านคำถาม · ข้อ ' + (previewQuizQIndex + 1) + ' / ' + poolLen;
}
const qEl = document.getElementById('quiz-game-question');
if (qEl) qEl.textContent = playQuizText;
const leg = document.getElementById('quiz-play-legend');
@@ -2156,7 +2204,7 @@
const qEl = document.getElementById('quiz-game-question');
if (qEl) qEl.textContent = playQuizText;
const leg = document.getElementById('quiz-play-legend');
if (leg) leg.textContent = 'รอสักครู่แล้วจะสุ่มคำถามใหม่จากชุดเดิม';
if (leg) leg.textContent = 'รอสักครู่แล้วไปข้อถัดไป (สับแล้วตามจำนวนข้อต่อรอบ)';
}
function quizCellOnPlay(grid, x, y) {
@@ -2691,6 +2739,7 @@
function advancePreviewQuizIfDue() {
if (isQuizQuestionMissionUiMapPlay() && isQuizQuestionMissionPregameBlockingPlay()) return;
if (previewQuizStep === 'done') return;
if (!previewMode || !isQuiz() || !playQuizPhaseEndsAt || Date.now() < playQuizPhaseEndsAt) return;
const pool = previewQuizPool;
if (!pool || !pool.length) return;
@@ -2704,7 +2753,12 @@
return;
}
if (previewQuizStep === 'between') {
const q = pickRandomQuizFromPool(pool);
previewQuizQIndex += 1;
if (previewQuizQIndex >= pool.length) {
finishPreviewQuizSessionPlay();
return;
}
const q = pool[previewQuizQIndex];
if (q) applyPreviewReadPhase(q, pool.length);
}
}
@@ -2723,7 +2777,10 @@
.map((q) => ({ text: String(q.text).trim(), answerTrue: !!q.answerTrue }));
}
if (!pool.length && mapData) pool = buildQuizPoolFromMap(mapData);
previewQuizPool = pool;
const cap = clampPreviewQuizRoundQuestionCount(settings);
const shuffled = shufflePreviewQuizQuestionsPlay(pool);
previewQuizPool = shuffled.slice(0, Math.min(cap, shuffled.length));
previewQuizQIndex = 0;
const dRead = 10000;
const dAns = 5000;
const dBet = 3500;
@@ -2748,8 +2805,9 @@
startPlayQuizTimer();
return;
}
const q = pickRandomQuizFromPool(pool);
if (q) applyPreviewReadPhase(q, pool.length);
const session = previewQuizPool;
const q = session[0];
if (q) applyPreviewReadPhase(q, session.length);
initPlayLiveQuizScoresZeros();
startPlayQuizTimer();
}
@@ -2782,6 +2840,7 @@
playQuizText = '';
playQuizPhaseEndsAt = 0;
previewQuizPool = [];
previewQuizQIndex = 0;
previewQuizCurrent = null;
previewQuizStep = 'read';
playLiveQuizScores = {};
@@ -2800,6 +2859,9 @@
clearTimeout(quizQuestionMissionCountdownTimer);
quizQuestionMissionCountdownTimer = null;
}
const gcmTeardown = document.getElementById('gauntlet-crown-mission-overlay');
if (gcmTeardown) gcmTeardown.classList.add('is-hidden');
cancelEmbedPreviewLobbyReturnTimer();
const grabBtnTeardown = document.getElementById('quiz-carry-grab-btn');
if (grabBtnTeardown) {
grabBtnTeardown.classList.add('is-hidden');
@@ -7095,7 +7157,14 @@
btn.onclick = function () {
if (previewMode && editorEmbedReturn) {
ov.classList.add('is-hidden');
showQuizCarryTimeupOnDeskLayer(function () { return gauntletCrownEmbedMissionAnyOk(disp); });
if (mission && mission.uiSkin === 'question_mission') {
cancelQuizCarryResultEndAfterTimeup();
hideQuizCarryTimeupOnDeskLayer();
hideQuizCarryResultEndLayer();
scheduleEmbedPreviewReturnToLobbyAfterResultEnd();
} else {
showQuizCarryTimeupOnDeskLayer(function () { return gauntletCrownEmbedMissionAnyOk(disp); });
}
} else {
goLobby();
}
+1 -1
View File
@@ -1966,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.238"></script>
<script src="js/play.js?v=0.240"></script>
<div class="version-tag">v —</div>
</body>
</html>
+23 -1
View File
@@ -196,6 +196,8 @@ function defaultQuizSettings() {
/** quiz_carry: ถ้าใส่รหัสฉาก + คูณ — เดินเร็วเฉพาะแมปนั้น (เทียบกับค่าเริ่มในไคลเอนต์) */
carryWalkSpeedMultForMapId: '',
carryWalkSpeedMult: null,
/** ถูก/ผิด: สุ่มกี่ข้อต่อรอบจากชุดคำถาม (สูงสุดเท่าที่มีในชุด) */
quizRoundQuestionCount: 10,
questions: [],
carryQuestions: [],
battleQuizMcq: [],
@@ -445,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;
@@ -509,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);
@@ -551,6 +567,7 @@ function loadQuizSettings() {
carryChoicePlaqueMapScale,
carryWalkSpeedMultForMapId,
carryWalkSpeedMult,
quizRoundQuestionCount: clampQuizRoundQuestionCount(j.quizRoundQuestionCount, d.quizRoundQuestionCount),
questions,
carryQuestions,
battleQuizMcq,
@@ -1090,6 +1107,9 @@ function saveQuizSettings(d) {
} else if (carryWalkSpeedMult == null) {
carryWalkSpeedMult = clampCarryWalkSpeedMultSetting(1.42);
}
const quizRoundQuestionCount = d.quizRoundQuestionCount != null
? clampQuizRoundQuestionCount(d.quizRoundQuestionCount, prev.quizRoundQuestionCount)
: prev.quizRoundQuestionCount;
const out = {
readMs,
answerMs,
@@ -1105,6 +1125,7 @@ function saveQuizSettings(d) {
carryChoicePlaqueMapScale,
carryWalkSpeedMultForMapId,
carryWalkSpeedMult,
quizRoundQuestionCount,
questions,
carryQuestions,
battleQuizMcq,
@@ -1460,7 +1481,8 @@ function startQuizGame(sid, space, md) {
const settings = loadQuizSettings();
const pool = getQuizQuestionPool(md);
const shuffled = shuffleQuizQuestions(pool);
const picked = shuffled.slice(0, Math.min(10, shuffled.length));
const cap = clampQuizRoundQuestionCount(settings.quizRoundQuestionCount, 10);
const picked = shuffled.slice(0, Math.min(cap, shuffled.length));
const players = {};
space.peers.forEach((_, peerId) => {
players[peerId] = { cannotTrue: false, cannotFalse: false, eliminated: false, score: 0 };