update Minigame-4 1.3
This commit is contained in:
+26
-1
@@ -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');
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user