update Minigame-4 1.3

This commit is contained in:
2026-04-26 08:06:12 +00:00
parent 4d0ca2f1d2
commit 06dc156ebc
6 changed files with 153 additions and 15 deletions
+26 -1
View File
@@ -355,6 +355,16 @@
function loadQuizCarryPanel() {
gameQuizFetch('GET').then(function (data) {
renderQuizCarryAdminList(data.carryQuestions || []);
var readSec = el('quiz-carry-read-sec');
var ansSec = el('quiz-carry-answer-sec');
if (readSec) {
var rms = Number(data.carryReadMs);
readSec.value = String(Number.isFinite(rms) ? Math.round(rms / 1000) : 3);
}
if (ansSec) {
var ams = Number(data.carryAnswerMs);
ansSec.value = String(Number.isFinite(ams) ? Math.round(ams / 1000) : 5);
}
setMsg('quiz-carry-settings-msg', '', '');
}).catch(function (e) {
setMsg('quiz-carry-settings-msg', e.message || 'โหลดไม่ได้', 'error');
@@ -396,7 +406,22 @@
});
});
gameQuizFetch('PUT', { carryQuestions: carryQuestions }).then(function () {
var readSecEl = el('quiz-carry-read-sec');
var ansSecEl = el('quiz-carry-answer-sec');
var readSec = readSecEl ? parseInt(String(readSecEl.value || '3'), 10) : 3;
var ansSec = ansSecEl ? parseInt(String(ansSecEl.value || '5'), 10) : 5;
if (!Number.isFinite(readSec) || readSec < 0) readSec = 0;
if (readSec > 120) readSec = 120;
if (!Number.isFinite(ansSec) || ansSec < 1) ansSec = 1;
if (ansSec > 300) ansSec = 300;
var carryReadMs = readSec * 1000;
var carryAnswerMs = ansSec * 1000;
gameQuizFetch('PUT', {
carryQuestions: carryQuestions,
carryReadMs: carryReadMs,
carryAnswerMs: carryAnswerMs,
}).then(function () {
setMsg('quiz-carry-settings-msg', 'บันทึกแล้ว', 'ok');
}).catch(function (e) {
setMsg('quiz-carry-settings-msg', e.message || 'บันทึกไม่ได้', 'error');
+1 -1
View File
@@ -46,7 +46,7 @@ function merge_quiz_settings_disk_from_put(string $rawBody): bool
}
}
}
$mergeKeys = ['readMs', 'answerMs', 'betweenMs', 'questions', 'carryQuestions', 'battleQuizMcq'];
$mergeKeys = ['readMs', 'answerMs', 'betweenMs', 'carryReadMs', 'carryAnswerMs', 'questions', 'carryQuestions', 'battleQuizMcq'];
foreach ($mergeKeys as $k) {
if (array_key_exists($k, $patch)) {
$base[$k] = $patch[$k];
+11 -1
View File
@@ -165,7 +165,17 @@
<section id="tab-panel-quiz-carry" class="tab-panel card" hidden role="tabpanel" aria-labelledby="tab-quiz-carry">
<h2>คำถามหลายตัวเลือก (หยิบมาวาง)</h2>
<p class="muted">ใช้กับฉากประเภท <strong>ตอบคำถาม — หยิบคำตอบมาวางกลาง</strong> ใน Editor — ผู้เล่นหยิบตัวเลือกไปวางโซนกลาง · ถ้ามีรายการที่นี่ เกมจะใช้<strong>เฉพาะชุดนี้</strong> (ไม่ผสมกับคำถามถูก/ผิดแท็บคำถามเกม) · เก็บที่ <code>/Game/data/quiz-settings.json</code> ฟิลด์ <code>carryQuestions</code></p>
<p class="muted" style="margin-top:-0.35rem">อย่างน้อย <strong>2 ตัวเลือก</strong>ต่อข้อ (สูงสุด 6) · เวลายังตั้งที่แท็บ <strong>คำถามเกม</strong>ได้ (ใช้ร่วมกับโหมดถามตอบอื่น)</p>
<p class="muted" style="margin-top:-0.35rem">อย่างน้อย <strong>2 ตัวเลือก</strong>ต่อข้อ (สูงสุด 6) · <em>English:</em> Per-round timing below applies only to <strong>quiz carry</strong> (after 3-2-1 in embed preview: question first, then options).</p>
<div class="admin-form-row" style="margin:0.75rem 0 1rem;flex-wrap:wrap;gap:1rem">
<label class="admin-field">อ่านคำถามก่อนหยิบตัวเลือก (วินาที)
<input type="number" id="quiz-carry-read-sec" class="admin-inp-num" min="0" max="120" step="1" value="3" aria-describedby="quiz-carry-read-hint" />
<span id="quiz-carry-read-hint" class="muted" style="display:block;font-size:0.85rem;margin-top:0.2rem">0 = แสดงตัวเลือกทันทีหลังคำถามขึ้น · สูงสุด 120 วิ</span>
</label>
<label class="admin-field">เวลาตอบหลังตัวเลือกขึ้น (วินาที)
<input type="number" id="quiz-carry-answer-sec" class="admin-inp-num" min="1" max="300" step="1" value="5" aria-describedby="quiz-carry-answer-hint" />
<span id="quiz-carry-answer-hint" class="muted" style="display:block;font-size:0.85rem;margin-top:0.2rem">นับจากเมื่อหยิบได้ · สูงสุด 300 วิ</span>
</label>
</div>
<h3 class="admin-subheading">รายการคำถาม</h3>
<div id="quiz-carry-admin-list" class="quiz-carry-admin-list"></div>
<div class="quiz-admin-actions">
+104 -11
View File
@@ -565,6 +565,10 @@
let quizCarryEmbedPendingQuestion = null;
let quizCarryEmbedCountdownStartAt = 0;
let quizCarryEmbedCountdownEndAt = 0;
/** quiz_carry: epoch ให้หยิบตัวเลือกได้ · epoch ปิดรอบตอบ (ตั้งที่ Admin แท็บหยิบมาวาง) */
let quizCarryOptionRevealAt = 0;
let quizCarryAnswerCloseAt = 0;
let quizCarryCarryTimingMs = { carryReadMs: 3000, carryAnswerMs: 5000 };
/** jump_survive — กล้องตามตัวในกรอบ + แพลตฟอร์มเลื่อนขึ้น (scroll) ไม่ลากทั้งฉาก */
let jumpSurviveCamCenterX = 0;
@@ -1660,6 +1664,7 @@
function drawQuizCarryChoiceLabels(ctx, worldToScreen, zoom) {
if (!isQuizCarry() || !mapData || !quizCarryCurrent) return;
if (!quizCarryOptionsPickableNow()) return;
const choices = quizCarryCurrent.choices;
if (!choices || !choices.length) return;
const maxSlots = choices.length;
@@ -1858,6 +1863,67 @@
return !!(previewMode && editorEmbedReturn && isQuizCarry() && quizCarryEmbedCountdownEndAt > Date.now());
}
function quizCarryOptionsPickableNow() {
if (!isQuizCarry() || !quizCarryCurrent) return true;
if (!quizCarryOptionRevealAt || quizCarryOptionRevealAt <= 0) return true;
return Date.now() >= quizCarryOptionRevealAt;
}
function quizCarryApplyPhaseTimersForCurrentQuestion() {
if (!quizCarryCurrent) {
quizCarryOptionRevealAt = 0;
quizCarryAnswerCloseAt = 0;
return;
}
const readMs = Math.max(0, Math.floor(Number(quizCarryCarryTimingMs.carryReadMs)) || 0);
const ansMs = Math.max(1000, Math.floor(Number(quizCarryCarryTimingMs.carryAnswerMs)) || 5000);
const t0 = Date.now();
if (readMs <= 0) {
quizCarryOptionRevealAt = 0;
quizCarryAnswerCloseAt = t0 + ansMs;
} else {
quizCarryOptionRevealAt = t0 + readMs;
quizCarryAnswerCloseAt = quizCarryOptionRevealAt + ansMs;
}
}
function updateQuizCarryCarryPhaseHud() {
if (!isQuizCarry() || !quizCarryCurrent) return;
const phaseEl = document.getElementById('quiz-game-phase-label');
if (!phaseEl) return;
const now = Date.now();
if (quizCarryOptionRevealAt > 0 && now < quizCarryOptionRevealAt) {
const remain = Math.max(0, Math.ceil((quizCarryOptionRevealAt - now) / 1000));
phaseEl.textContent = 'อ่านคำถาม — ตัวเลือกขึ้นใน ' + remain + ' วิ';
return;
}
if (quizCarryAnswerCloseAt > now) {
const remain = Math.max(0, Math.ceil((quizCarryAnswerCloseAt - now) / 1000));
phaseEl.textContent = 'หยิบ/ส่งคำตอบ — เหลือ ' + remain + ' วิ';
return;
}
quizCarryRestoreEmbedPhaseLabel();
}
function tickQuizCarryRoundTimers() {
if (!isQuizCarry() || !mapData || !quizCarryCurrent) return;
if (quizCarryEmbedCountdownEndAt > Date.now()) return;
if (quizCarryEmbedPendingQuestion) return;
if (!quizCarryAnswerCloseAt || quizCarryAnswerCloseAt <= 0) return;
if (Date.now() < quizCarryAnswerCloseAt) return;
quizCarryAnswerCloseAt = 0;
quizCarryOptionRevealAt = 0;
showQuizCarryToast('หมดเวลา — ข้อถัดไป', false);
me.quizCarryHeld = null;
others.forEach((o) => {
if (!o) return;
o.quizCarryHeld = null;
o.botPath = [];
});
renderPlayQuizScoreboard(playLiveQuizScores);
quizCarryPickNextQuestion();
}
/** นับ 3–2–1: ไม่โชว์ข้อความคำถามใน overlay — มีแค่เลข */
function hideQuizCarryEmbedCountdownQuestionChrome() {
const cq = document.getElementById('quiz-carry-embed-countdown-q');
@@ -1901,6 +1967,7 @@
o.botQuizCarryPathfindAfter = 0;
});
syncQuizCarryEmbedQuestionStrip();
quizCarryApplyPhaseTimersForCurrentQuestion();
return;
}
const elapsed = now - quizCarryEmbedCountdownStartAt;
@@ -1920,6 +1987,8 @@
quizCarryEmbedPendingQuestion = null;
quizCarryEmbedCountdownEndAt = 0;
quizCarryEmbedCountdownStartAt = 0;
quizCarryOptionRevealAt = 0;
quizCarryAnswerCloseAt = 0;
hideQuizCarryEmbedCountdownOverlay();
playQuizText = 'ไม่มีคำถาม — ใส่ quizQuestions ในแมป (choices + correctIndex) หรือตั้งใน Admin';
return;
@@ -1936,8 +2005,12 @@
quizCarryEmbedCountdownEndAt = quizCarryEmbedCountdownStartAt + 3000;
quizCarryCurrent = null;
playQuizText = '';
quizCarryOptionRevealAt = 0;
quizCarryAnswerCloseAt = 0;
me.quizCarryHeld = null;
others.forEach((o) => {
if (!o) return;
o.quizCarryHeld = null;
o.botPath = [];
o.botPathStuckTicks = 0;
/* ต้องใช้ performance.now() ช่วงกับ stepQuizCarryPreviewBots — ห้ามใช้ Date.now() epoch ไม่งั้นบอทค้าง pathfind ตลอด */
@@ -1955,6 +2028,12 @@
playQuizText = formatQuizCarryQuestionHud(q);
const qEl = document.getElementById('quiz-game-question');
if (qEl) qEl.textContent = playQuizText;
me.quizCarryHeld = null;
others.forEach((o) => {
if (!o) return;
o.quizCarryHeld = null;
});
quizCarryApplyPhaseTimersForCurrentQuestion();
}
function showQuizCarryToast(msg, ok) {
@@ -1989,6 +2068,7 @@
const correct = quizCarryCurrent.correctIndex;
let held = ent.quizCarryHeld;
if (held != null && canSubmitAnswer) {
if (!quizCarryOptionsPickableNow()) return false;
ent.quizCarryHeld = null;
const ok = held === correct;
if (ok) {
@@ -2004,6 +2084,7 @@
return true;
}
if (held == null && pickupIdx != null) {
if (!quizCarryOptionsPickableNow()) return false;
if (quizCarryOptionHeldByAnyone(pickupIdx)) {
if (opts.fromKey && actorId === myId && !opts.silent) {
showQuizCarryToast('ตัวเลือกนี้มีคนถืออยู่แล้ว — เลือกข้ออื่น', false);
@@ -2039,6 +2120,8 @@
quizCarryEmbedPendingQuestion = null;
quizCarryEmbedCountdownStartAt = 0;
quizCarryEmbedCountdownEndAt = 0;
quizCarryOptionRevealAt = 0;
quizCarryAnswerCloseAt = 0;
hideQuizCarryEmbedCountdownOverlay();
me.quizCarryHeld = null;
others.forEach((o) => {
@@ -2054,6 +2137,10 @@
const r = await fetch(BASE + '/api/quiz-settings');
if (r.ok) {
const s = await r.json();
quizCarryCarryTimingMs = {
carryReadMs: clampPreviewMs(s.carryReadMs, 3000, 0, 120000),
carryAnswerMs: clampPreviewMs(s.carryAnswerMs, 5000, 1000, 300000),
};
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++) {
@@ -2195,20 +2282,24 @@
if (typeof o.botQuizCarryPathfindAfter !== 'number') o.botQuizCarryPathfindAfter = 0;
if (nowPf < o.botQuizCarryPathfindAfter) return;
const jitter = (String(id).length * 31 + ((o.x | 0) ^ (o.y | 0)) * 7) % 160;
if (o.quizCarryHeld == null && wantIdx != null) {
const dest = pickRandomTileForQuizCarryOption(mapData, wantIdx, o);
if (dest) {
const path = pathfindPlayForBot(o.x, o.y, dest.x + 0.5, dest.y + 0.5, o);
if (path && path.length > 1) o.botPath = path.slice(1);
if (quizCarryOptionsPickableNow()) {
if (o.quizCarryHeld == null && wantIdx != null) {
const dest = pickRandomTileForQuizCarryOption(mapData, wantIdx, o);
if (dest) {
const path = pathfindPlayForBot(o.x, o.y, dest.x + 0.5, dest.y + 0.5, o);
if (path && path.length > 1) o.botPath = path.slice(1);
}
} else {
const sub = pickRandomTileForQuizCarrySubmit(mapData, o);
if (sub) {
const path = pathfindPlayForBot(o.x, o.y, sub.x + 0.5, sub.y + 0.5, o);
if (path && path.length > 1) o.botPath = path.slice(1);
}
}
o.botQuizCarryPathfindAfter = nowPf + pathfindMinGap + jitter;
} else {
const sub = pickRandomTileForQuizCarrySubmit(mapData, o);
if (sub) {
const path = pathfindPlayForBot(o.x, o.y, sub.x + 0.5, sub.y + 0.5, o);
if (path && path.length > 1) o.botPath = path.slice(1);
}
o.botQuizCarryPathfindAfter = nowPf + 120;
}
o.botQuizCarryPathfindAfter = nowPf + pathfindMinGap + jitter;
} else {
stepPreviewBotAlongPath(o, w, h);
}
@@ -7529,6 +7620,8 @@
if (o.ty != null) o.y += (o.ty - o.y) * LERP;
});
tickQuizCarryEmbedCountdown();
tickQuizCarryRoundTimers();
updateQuizCarryCarryPhaseHud();
stepPreviewBots();
if (isChatFocused()) {
me.isWalking = false;
+1 -1
View File
@@ -765,7 +765,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.094"></script>
<script src="js/play.js?v=0.095"></script>
<div class="version-tag">v —</div>
</body>
</html>
+10
View File
@@ -84,6 +84,10 @@ function defaultQuizSettings() {
readMs: 10000,
answerMs: 5000,
betweenMs: 3500,
/** quiz_carry: โชว์เฉพาะคำถามก่อน (มิลลิวิ) แล้วค่อยให้หยิบตัวเลือก — 0 = ไม่รอ */
carryReadMs: 3000,
/** quiz_carry: หลังตัวเลือกขึ้น ให้เวลาตอบ (มิลลิวิ) */
carryAnswerMs: 5000,
questions: [],
carryQuestions: [],
battleQuizMcq: [],
@@ -171,6 +175,8 @@ function loadQuizSettings() {
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),
questions,
carryQuestions,
battleQuizMcq,
@@ -466,6 +472,8 @@ function saveQuizSettings(d) {
const readMs = d.readMs != null ? clampQuizMs(d.readMs, prev.readMs) : prev.readMs;
const answerMs = d.answerMs != null ? clampQuizMs(d.answerMs, prev.answerMs) : prev.answerMs;
const betweenMs = d.betweenMs != null ? clampQuizBetweenMs(d.betweenMs, prev.betweenMs) : prev.betweenMs;
const carryReadMs = d.carryReadMs != null ? clampQuizBetweenMs(d.carryReadMs, prev.carryReadMs) : prev.carryReadMs;
const carryAnswerMs = d.carryAnswerMs != null ? clampQuizMs(d.carryAnswerMs, prev.carryAnswerMs) : prev.carryAnswerMs;
const questions = Array.isArray(d.questions)
? (d.questions || [])
.filter((q) => q && String(q.text || '').trim())
@@ -481,6 +489,8 @@ function saveQuizSettings(d) {
readMs,
answerMs,
betweenMs,
carryReadMs,
carryAnswerMs,
questions,
carryQuestions,
battleQuizMcq,