done font minigame4

This commit is contained in:
2026-04-26 11:41:15 +00:00
parent d7a2dfdd9c
commit f70e13a8aa
9 changed files with 526 additions and 8 deletions
+10
View File
@@ -365,6 +365,11 @@
var ams = Number(data.carryAnswerMs);
ansSec.value = String(Number.isFinite(ams) ? Math.round(ams / 1000) : 5);
}
var sessLen = el('quiz-carry-session-len');
if (sessLen) {
var sl = parseInt(String(data.carrySessionLength), 10);
sessLen.value = String(Number.isFinite(sl) && sl >= 0 ? sl : 0);
}
setMsg('quiz-carry-settings-msg', '', '');
}).catch(function (e) {
setMsg('quiz-carry-settings-msg', e.message || 'โหลดไม่ได้', 'error');
@@ -416,11 +421,16 @@
if (ansSec > 300) ansSec = 300;
var carryReadMs = readSec * 1000;
var carryAnswerMs = ansSec * 1000;
var sessEl = el('quiz-carry-session-len');
var carrySessionLength = sessEl ? parseInt(String(sessEl.value || '0'), 10) : 0;
if (!Number.isFinite(carrySessionLength) || carrySessionLength < 0) carrySessionLength = 0;
if (carrySessionLength > 500) carrySessionLength = 500;
gameQuizFetch('PUT', {
carryQuestions: carryQuestions,
carryReadMs: carryReadMs,
carryAnswerMs: carryAnswerMs,
carrySessionLength: carrySessionLength,
}).then(function () {
setMsg('quiz-carry-settings-msg', 'บันทึกแล้ว', 'ok');
}).catch(function (e) {
+1 -1
View File
@@ -46,7 +46,7 @@ function merge_quiz_settings_disk_from_put(string $rawBody): bool
}
}
}
$mergeKeys = ['readMs', 'answerMs', 'betweenMs', 'carryReadMs', 'carryAnswerMs', 'questions', 'carryQuestions', 'battleQuizMcq'];
$mergeKeys = ['readMs', 'answerMs', 'betweenMs', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'questions', 'carryQuestions', 'battleQuizMcq'];
foreach ($mergeKeys as $k) {
if (array_key_exists($k, $patch)) {
$base[$k] = $patch[$k];
+4
View File
@@ -175,6 +175,10 @@
<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>
<label class="admin-field">จำนวนข้อต่อเซสชันก่อนจบ (พรีวิว)
<input type="number" id="quiz-carry-session-len" class="admin-inp-num" min="0" max="500" step="1" value="0" aria-describedby="quiz-carry-session-hint" />
<span id="quiz-carry-session-hint" class="muted" style="display:block;font-size:0.85rem;margin-top:0.2rem">สุ่มคำถามจากชุดจนกว่าจะครบจำนวนนี้ (นับทุกข้อที่จบด้วยถูกหรือหมดเวลา) · 0 = เล่นต่อไม่จบอัตโนมัติ · <em>English:</em> Preview / editor embed only.</span>
</label>
</div>
<h3 class="admin-subheading">รายการคำถาม</h3>
<div id="quiz-carry-admin-list" class="quiz-carry-admin-list"></div>
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: 538 KiB

+291 -6
View File
@@ -569,6 +569,15 @@
let quizCarryOptionRevealAt = 0;
let quizCarryAnswerCloseAt = 0;
let quizCarryCarryTimingMs = { carryReadMs: 3000, carryAnswerMs: 5000 };
/** พรีวิว (รวม editor embed): จำนวนข้อที่เล่นก่อนโอเวอร์เลย์จบ — 0 = ไม่จบอัตโนมัติ */
let quizCarrySessionLength = 0;
let quizCarryRoundsCompleted = 0;
let quizCarrySessionEnded = false;
/** embed พรีวิว: รอ Ready / START ของโฮสต์ก่อนนับ 3–2–1 */
let quizCarryPregameActive = false;
let playHostId = null;
/** ซิงก์จากเซิร์ฟเวอร์: socketId → กด Ready แล้ว */
let quizCarryLobbyReadyMap = {};
/** jump_survive — กล้องตามตัวในกรอบ + แพลตฟอร์มเลื่อนขึ้น (scroll) ไม่ลากทั้งฉาก */
let jumpSurviveCamCenterX = 0;
@@ -789,6 +798,12 @@
return Math.max(minV, Math.min(maxV, Math.floor(v)));
}
function clampCarrySessionLen(n, def) {
const v = Number(n);
if (Number.isNaN(v)) return def;
return Math.max(0, Math.min(500, Math.floor(v)));
}
function buildQuizPoolFromMap(md) {
if (!md || !Array.isArray(md.quizQuestions)) return [];
return md.quizQuestions
@@ -1157,6 +1172,7 @@
if (mapData.gameType === 'balloon_boss') {
applyBalloonBossSpawnLayoutPlay();
}
if (quizCarryPregameActive && mapData && mapData.gameType === 'quiz_carry') updateQuizCarryPregameHud();
}
function pickRandomWalkableCellInAnswerGrid(grid, o) {
@@ -1863,6 +1879,120 @@
return !!(previewMode && editorEmbedReturn && isQuizCarry() && quizCarryEmbedCountdownEndAt > Date.now());
}
function isQuizCarryEmbedLobbyBlockingMovement() {
return !!(previewMode && editorEmbedReturn && isQuizCarry() && quizCarryPregameActive);
}
function quizCarryPregameHumanIds() {
const ids = [];
if (myId != null && !isPreviewBotId(myId)) ids.push(String(myId));
others.forEach((_, id) => {
if (!isPreviewBotId(id)) ids.push(String(id));
});
return ids;
}
function quizCarryPregameBotCount() {
let n = 0;
others.forEach((_, id) => {
if (isPreviewBotId(id)) n++;
});
return n;
}
function quizCarryPregameReadyNumerator() {
let n = quizCarryPregameBotCount();
quizCarryPregameHumanIds().forEach((id) => {
if (quizCarryLobbyReadyMap[id]) n++;
});
return n;
}
function quizCarryPregameTotalPlayers() {
return quizCarryPregameHumanIds().length + quizCarryPregameBotCount();
}
function isMePlayHost() {
if (myId == null) return false;
if (playHostId == null) return true;
return String(playHostId) === String(myId);
}
function hideQuizCarryPregameOverlay() {
const ov = document.getElementById('quiz-carry-pregame-overlay');
if (ov) {
ov.classList.add('is-hidden');
ov.setAttribute('aria-hidden', 'true');
}
}
function showQuizCarryPregameOverlay() {
const ov = document.getElementById('quiz-carry-pregame-overlay');
if (ov) {
ov.classList.remove('is-hidden');
ov.setAttribute('aria-hidden', 'false');
}
updateQuizCarryPregameHud();
}
function quizCarrySyncGuestReadyIfNeeded() {
if (!quizCarryPregameActive || myId == null || isMePlayHost()) return;
const sid = String(myId);
if (quizCarryLobbyReadyMap[sid]) return;
quizCarryLobbyReadyMap[sid] = true;
if (socket && socket.connected) socket.emit('quiz-carry-lobby-ready', { ready: true });
}
function updateQuizCarryPregameHud() {
if (!quizCarryPregameActive) return;
const st = document.getElementById('quiz-carry-pregame-status');
const primary = document.getElementById('quiz-carry-pregame-primary');
const primaryImg = document.getElementById('quiz-carry-pregame-primary-img');
const num = quizCarryPregameReadyNumerator();
const tot = Math.max(1, quizCarryPregameTotalPlayers());
if (st) {
st.textContent = 'Ready Status : ' + num + '/' + tot;
}
const humans = quizCarryPregameHumanIds();
const humansReady = humans.length > 0 && humans.every((id) => !!quizCarryLobbyReadyMap[id]);
const meReady = !!(myId && quizCarryLobbyReadyMap[String(myId)]);
if (primaryImg) {
primaryImg.src = humansReady ? '/Game/img/quiz-carry/btn-start.png' : '/Game/img/quiz-carry/btn-ready.png';
primaryImg.alt = humansReady ? 'START' : 'READY';
}
if (primary) {
primary.classList.toggle('is-start-phase', humansReady);
primary.classList.toggle('is-read-only', !isMePlayHost());
primary.classList.toggle('is-pressed', isMePlayHost() && meReady && !humansReady);
primary.disabled = !isMePlayHost();
primary.setAttribute('aria-pressed', humansReady ? 'false' : (meReady ? 'true' : 'false'));
primary.title = isMePlayHost()
? (humansReady ? 'START' : (meReady ? 'ยกเลิก READY' : 'READY'))
: (humansReady ? 'START (โฮสต์เท่านั้น)' : 'READY (โฮสต์เท่านั้น)');
}
}
function beginQuizCarryEmbedPregame() {
if (!(previewMode && editorEmbedReturn && isQuizCarry())) return;
quizCarryPregameActive = true;
quizCarryLobbyReadyMap = {};
quizCarryPregameHumanIds().forEach((id) => { quizCarryLobbyReadyMap[id] = false; });
showQuizCarryPregameOverlay();
if (socket && socket.connected) socket.emit('quiz-carry-lobby-sync-request');
quizCarrySyncGuestReadyIfNeeded();
updateQuizCarryPregameHud();
}
function endQuizCarryEmbedPregameAndStart() {
if (!quizCarryPregameActive) return;
quizCarryPregameActive = false;
hideQuizCarryPregameOverlay();
quizCarryPickNextQuestion();
if (!(previewMode && editorEmbedReturn && quizCarryEmbedCountdownEndAt > Date.now())) {
quizCarryRestoreEmbedPhaseLabel();
}
}
function quizCarryOptionsPickableNow() {
if (!isQuizCarry() || !quizCarryCurrent) return true;
if (!quizCarryOptionRevealAt || quizCarryOptionRevealAt <= 0) return true;
@@ -1888,6 +2018,8 @@
}
function updateQuizCarryCarryPhaseHud() {
if (quizCarrySessionEnded) return;
if (quizCarryPregameActive) return;
if (!isQuizCarry() || !quizCarryCurrent) return;
const phaseEl = document.getElementById('quiz-game-phase-label');
if (!phaseEl) return;
@@ -1906,6 +2038,8 @@
}
function tickQuizCarryRoundTimers() {
if (quizCarrySessionEnded) return;
if (quizCarryPregameActive) return;
if (!isQuizCarry() || !mapData || !quizCarryCurrent) return;
if (quizCarryEmbedCountdownEndAt > Date.now()) return;
if (quizCarryEmbedPendingQuestion) return;
@@ -1913,7 +2047,9 @@
if (Date.now() < quizCarryAnswerCloseAt) return;
quizCarryAnswerCloseAt = 0;
quizCarryOptionRevealAt = 0;
showQuizCarryToast('หมดเวลา — ข้อถัดไป', false);
const limT = previewMode && isQuizCarry() ? quizCarrySessionLength : 0;
const willEndT = limT > 0 && (quizCarryRoundsCompleted + 1) >= limT;
showQuizCarryToast(willEndT ? 'หมดเวลา — ครบชุดคำถาม' : 'หมดเวลา — ข้อถัดไป', false);
me.quizCarryHeld = null;
others.forEach((o) => {
if (!o) return;
@@ -1921,7 +2057,7 @@
o.botPath = [];
});
renderPlayQuizScoreboard(playLiveQuizScores);
quizCarryPickNextQuestion();
quizCarryAfterRoundResolved();
}
/** นับ 3–2–1: ไม่โชว์ข้อความคำถามใน overlay — มีแค่เลข */
@@ -1982,6 +2118,8 @@
}
function quizCarryPickNextQuestion() {
if (quizCarrySessionEnded) return;
if (quizCarryPregameActive) return;
if (!quizCarryPool.length) {
quizCarryCurrent = null;
quizCarryEmbedPendingQuestion = null;
@@ -2036,6 +2174,91 @@
quizCarryApplyPhaseTimersForCurrentQuestion();
}
/** หลังจบข้อหนึ่งข้อ (ถูกหรือหมดเวลา): พรีวิวนับรอบจนครบ carrySessionLength แล้วจบเซสชัน */
function quizCarryAfterRoundResolved() {
if (!isQuizCarry()) return;
if (!previewMode) {
quizCarryPickNextQuestion();
return;
}
quizCarryRoundsCompleted++;
const lim = quizCarrySessionLength;
if (lim > 0 && quizCarryRoundsCompleted >= lim) {
showQuizCarrySessionCompleteOverlay();
return;
}
quizCarryPickNextQuestion();
}
function showQuizCarrySessionCompleteOverlay() {
if (quizCarrySessionEnded) return;
quizCarrySessionEnded = true;
quizCarryPregameActive = false;
hideQuizCarryPregameOverlay();
quizCarryCurrent = null;
quizCarryEmbedPendingQuestion = null;
quizCarryEmbedCountdownEndAt = 0;
quizCarryEmbedCountdownStartAt = 0;
quizCarryOptionRevealAt = 0;
quizCarryAnswerCloseAt = 0;
hideQuizCarryEmbedCountdownOverlay();
me.quizCarryHeld = null;
others.forEach((o) => {
if (!o) return;
o.quizCarryHeld = null;
o.botPath = [];
});
playQuizText = 'ครบชุดคำถามแล้ว';
const qEl = document.getElementById('quiz-game-question');
if (qEl) qEl.textContent = playQuizText;
const phaseEl = document.getElementById('quiz-game-phase-label');
if (phaseEl) phaseEl.textContent = 'ครบ ' + String(quizCarryRoundsCompleted) + ' ข้อ';
const ov = document.getElementById('gauntlet-ended-overlay');
const msgEl = document.getElementById('gauntlet-ended-message');
const titleEl = document.getElementById('gauntlet-ended-title');
const listEl = document.getElementById('gauntlet-ended-rankings');
const btn = document.getElementById('btn-gauntlet-ended-lobby');
if (!ov || !msgEl || !listEl) return;
if (titleEl) titleEl.textContent = 'เกมจบ · Game over';
msgEl.textContent = 'ครบจำนวนคำถามที่ตั้งไว้ (' + String(quizCarryRoundsCompleted) + ' ข้อ) · Quiz carry session finished';
listEl.innerHTML = '';
const ranks = [];
if (myId != null) {
ranks.push({
id: myId,
nickname: (me.nickname || nick || 'คุณ').trim() || 'คุณ',
score: Math.max(0, Number(playLiveQuizScores[myId]) || 0),
});
}
others.forEach((o, id) => {
if (!o) return;
ranks.push({
id,
nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : String(id),
score: Math.max(0, Number(playLiveQuizScores[id]) || 0),
});
});
ranks.sort((a, b) => b.score - a.score || String(a.nickname).localeCompare(String(b.nickname), 'th'));
ranks.forEach((r, i) => {
const li = document.createElement('li');
const isMe = myId != null && String(r.id) === String(myId);
li.textContent = `${i + 1}. ${(r && r.nickname) || '—'}${Math.max(0, Number(r && r.score) || 0)}`;
if (isMe) li.className = 'gauntlet-ended-me';
listEl.appendChild(li);
});
ov.classList.remove('is-hidden');
function goLobby() {
window.location.href = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick);
}
if (btn) {
btn.onclick = () => {
if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden');
else goLobby();
};
}
renderPlayQuizScoreboard(playLiveQuizScores);
}
function showQuizCarryToast(msg, ok) {
const el = document.getElementById('play-quiz-feedback');
if (!el) return;
@@ -2052,6 +2275,8 @@
*/
function tryQuizCarryInteractionForPlayer(actorId, ox, oy, opts) {
opts = opts || {};
if (quizCarryPregameActive) return false;
if (quizCarrySessionEnded) return false;
if (!isQuizCarry() || !mapData || !quizCarryCurrent) return false;
const md = mapData;
const footprint = quizTilesFootprintPlay(ox, oy);
@@ -2073,10 +2298,12 @@
const ok = held === correct;
if (ok) {
playLiveQuizScores[actorId] = (playLiveQuizScores[actorId] || 0) + 1;
showQuizCarryToast('ถูกต้อง +1 แต้ม — สุ่มคำถามใหม่', true);
const lim = previewMode && isQuizCarry() ? quizCarrySessionLength : 0;
const willEnd = lim > 0 && (quizCarryRoundsCompleted + 1) >= lim;
showQuizCarryToast(willEnd ? 'ถูกต้อง +1 แต้ม — ครบชุดคำถาม' : 'ถูกต้อง +1 แต้ม — สุ่มคำถามใหม่', true);
me.quizCarryHeld = null;
others.forEach((o) => { if (o) o.quizCarryHeld = null; });
quizCarryPickNextQuestion();
quizCarryAfterRoundResolved();
} else {
showQuizCarryToast('ผิด — คืนป้ายที่โซนตัวเลือกแล้ว กด F หยิบใหม่ได้', false);
}
@@ -2115,6 +2342,8 @@
}
function resetQuizCarryPlayState() {
quizCarryPregameActive = false;
hideQuizCarryPregameOverlay();
quizCarryPool = [];
quizCarryCurrent = null;
quizCarryEmbedPendingQuestion = null;
@@ -2122,6 +2351,8 @@
quizCarryEmbedCountdownEndAt = 0;
quizCarryOptionRevealAt = 0;
quizCarryAnswerCloseAt = 0;
quizCarryRoundsCompleted = 0;
quizCarrySessionEnded = false;
hideQuizCarryEmbedCountdownOverlay();
me.quizCarryHeld = null;
others.forEach((o) => {
@@ -2132,6 +2363,9 @@
}
async function loadQuizCarryPoolAndStart() {
quizCarryRoundsCompleted = 0;
quizCarrySessionEnded = false;
quizCarrySessionLength = 0;
let pool = [];
try {
const r = await fetch(BASE + '/api/quiz-settings');
@@ -2141,6 +2375,7 @@
carryReadMs: clampPreviewMs(s.carryReadMs, 3000, 0, 120000),
carryAnswerMs: clampPreviewMs(s.carryAnswerMs, 5000, 1000, 300000),
};
quizCarrySessionLength = clampCarrySessionLen(s.carrySessionLength, 0);
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++) {
@@ -2160,7 +2395,11 @@
if (qEl) qEl.textContent = playQuizText;
return;
}
quizCarryPickNextQuestion();
if (previewMode && editorEmbedReturn) {
beginQuizCarryEmbedPregame();
} else {
quizCarryPickNextQuestion();
}
if (!(previewMode && editorEmbedReturn && quizCarryEmbedCountdownEndAt > Date.now())) {
quizCarryRestoreEmbedPhaseLabel();
}
@@ -2252,6 +2491,8 @@
}
function stepQuizCarryPreviewBots() {
if (quizCarrySessionEnded) return;
if (quizCarryPregameActive) return;
if (!previewFillBots || !isQuizCarry() || !mapData || !quizCarryCurrent) return;
const w = mapData.width || 20, h = mapData.height || 15;
const nowPf = performance.now();
@@ -5062,6 +5303,7 @@
const prevWasSpaceShooter = mapData && mapData.gameType === 'space_shooter';
const prevWasBalloonBoss = mapData && mapData.gameType === 'balloon_boss';
mapData = gameMapData;
if (res && res.hostId != null) playHostId = res.hostId;
if (mapData.gameType !== 'space_shooter') {
spaceShooterGameEnded = false;
if (prevWasSpaceShooter) {
@@ -5336,6 +5578,23 @@
quizCarryHeld: null,
});
if (previewFillBots) rebalancePreviewBots();
if (quizCarryPregameActive && isQuizCarry()) updateQuizCarryPregameHud();
});
socket.on('host-changed', (d) => {
if (d && d.hostId != null) playHostId = d.hostId;
if (quizCarryPregameActive && isQuizCarry()) updateQuizCarryPregameHud();
});
socket.on('quiz-carry-lobby-sync', (d) => {
if (!d || typeof d.readyMap !== 'object') return;
quizCarryLobbyReadyMap = { ...d.readyMap };
if (quizCarryPregameActive && isQuizCarry()) {
quizCarrySyncGuestReadyIfNeeded();
updateQuizCarryPregameHud();
}
});
socket.on('quiz-carry-lobby-started', () => {
if (!(previewMode && editorEmbedReturn && isQuizCarry())) return;
endQuizCarryEmbedPregameAndStart();
});
socket.on('user-move', (data) => {
if (data && myId != null && data.id === myId) {
@@ -7630,7 +7889,7 @@
requestAnimationFrame(tick);
return;
}
if (isQuizCarryEmbedCountdownBlockingMovement()) {
if (isQuizCarryEmbedLobbyBlockingMovement() || isQuizCarryEmbedCountdownBlockingMovement()) {
me.isWalking = false;
playPath = [];
enforceQuizBattleLaneOnMePlay();
@@ -7758,6 +8017,32 @@
}
});
})();
(function wireQuizCarryPregameOverlay() {
const primary = document.getElementById('quiz-carry-pregame-primary');
if (!primary) return;
primary.addEventListener('click', () => {
if (!quizCarryPregameActive || myId == null) return;
if (!isMePlayHost()) return;
const humans = quizCarryPregameHumanIds();
const humansReady = humans.length > 0 && humans.every((id) => !!quizCarryLobbyReadyMap[id]);
if (humansReady) {
/* ผู้เล่นจริงคนเดียว + พรีวิว: ไม่ต้องรอ ACK เซิร์ฟ (ห้องที่ map บนเซิร์ฟยังไม่ใช่ quiz_carry จะเคยล้มเหลว) */
if (humans.length === 1 && isMePlayHost()) {
endQuizCarryEmbedPregameAndStart();
return;
}
socket.emit('quiz-carry-lobby-start', {}, (r) => {
if (!r || !r.ok) showQuizCarryToast((r && r.error) || 'START ไม่ได้', false);
});
return;
}
const sid = String(myId);
const next = !quizCarryLobbyReadyMap[sid];
quizCarryLobbyReadyMap[sid] = next;
updateQuizCarryPregameHud();
socket.emit('quiz-carry-lobby-ready', { ready: next });
});
})();
window.addEventListener('beforeunload', () => socket.emit('leave-space'));
window.addEventListener('resize', () => {
if (!mapData || !canvas) return;
+138 -1
View File
@@ -186,6 +186,126 @@
z-index: 10120;
top: max(52px, calc(env(safe-area-inset-top) + 46px));
}
/* พรีวิว embed quiz_carry: How to play + Ready / START ก่อน 321 */
#quiz-carry-pregame-overlay {
position: fixed;
inset: 0;
z-index: 10078;
display: flex;
align-items: center;
justify-content: center;
padding: max(10px, env(safe-area-inset-top)) max(10px, env(safe-area-inset-right)) max(10px, env(safe-area-inset-bottom)) max(10px, env(safe-area-inset-left));
box-sizing: border-box;
}
#quiz-carry-pregame-overlay.is-hidden { display: none !important; }
.quiz-carry-pregame-backdrop {
position: absolute;
inset: 0;
background: rgba(4, 6, 14, 0.78);
backdrop-filter: blur(10px);
}
.quiz-carry-pregame-sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.quiz-carry-pregame-card {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: stretch;
max-width: min(94vw, 900px);
width: 100%;
max-height: min(94vh, 900px);
overflow: hidden;
background: rgba(10, 12, 26, 0.55);
border: none;
border-radius: 4px;
box-shadow: none;
pointer-events: auto;
}
.quiz-carry-pregame-art {
flex: 1 1 auto;
min-height: 0;
margin: 0;
padding: 0;
overflow: auto;
background: transparent;
display: flex;
align-items: flex-start;
justify-content: center;
}
.quiz-carry-pregame-howto-img {
display: block;
width: 100%;
height: auto;
max-height: min(62vh, 560px);
object-fit: contain;
object-position: top center;
border: none;
outline: none;
box-shadow: none;
}
.quiz-carry-pregame-footer {
flex: 0 0 auto;
padding: 0.85rem 1rem 1rem;
background: linear-gradient(180deg, rgba(8, 10, 22, 0.2), rgba(8, 10, 22, 0.96));
border-top: none;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.65rem;
}
.quiz-carry-pregame-status {
margin: 0;
text-align: center;
font: 700 clamp(0.8rem, 2.4vw, 0.95rem) / 1.35 system-ui, "Kanit", sans-serif;
color: #fbcfe8;
text-shadow: 0 0 16px rgba(244, 114, 182, 0.5);
letter-spacing: 0.02em;
}
.quiz-carry-pregame-actions {
display: flex;
flex-wrap: wrap;
gap: 0.85rem 1.25rem;
justify-content: center;
align-items: center;
}
.quiz-carry-pregame-primary-btn {
background: transparent;
border: none;
padding: 0;
cursor: pointer;
line-height: 0;
border-radius: 6px;
transition: transform 0.12s ease, filter 0.12s ease, opacity 0.15s ease;
}
.quiz-carry-pregame-primary-btn:hover:not(:disabled):not(.is-read-only) {
transform: scale(1.03);
filter: brightness(1.08);
}
.quiz-carry-pregame-primary-btn.is-pressed:not(.is-start-phase) {
filter: brightness(1.12) drop-shadow(0 0 12px rgba(94, 234, 212, 0.55));
}
.quiz-carry-pregame-primary-btn.is-read-only,
.quiz-carry-pregame-primary-btn:disabled {
opacity: 0.38;
pointer-events: none;
cursor: default;
filter: grayscale(0.2);
}
.quiz-carry-pregame-primary-btn img {
display: block;
width: min(220px, 42vw);
height: auto;
}
#play-quiz-scoreboard {
position: fixed;
top: max(56px, env(safe-area-inset-top));
@@ -696,6 +816,23 @@
<ul id="play-quiz-scoreboard-list" class="play-quiz-scoreboard-list"></ul>
</div>
<div id="play-quiz-feedback" class="is-hidden play-quiz-feedback" role="status" aria-atomic="true"></div>
<div id="quiz-carry-pregame-overlay" class="is-hidden" role="dialog" aria-modal="true" aria-labelledby="quiz-carry-pregame-sr-title" aria-hidden="true">
<div class="quiz-carry-pregame-backdrop" aria-hidden="true"></div>
<div class="quiz-carry-pregame-card">
<h2 id="quiz-carry-pregame-sr-title" class="quiz-carry-pregame-sr-only">ประลองความรู้ คำศัพท์ในกระบวนการยุติธรรม — How to play</h2>
<div class="quiz-carry-pregame-art">
<img id="quiz-carry-pregame-howto-img" class="quiz-carry-pregame-howto-img" src="/Game/img/quiz-carry/howto.png" width="900" height="520" alt="HOW TO PLAY — ประลองความรู้ คำศัพท์ในกระบวนการยุติธรรม" loading="lazy" decoding="async" />
</div>
<div class="quiz-carry-pregame-footer">
<p id="quiz-carry-pregame-status" class="quiz-carry-pregame-status" aria-live="polite"></p>
<div class="quiz-carry-pregame-actions">
<button type="button" id="quiz-carry-pregame-primary" class="quiz-carry-pregame-primary-btn" aria-pressed="false" title="READY">
<img id="quiz-carry-pregame-primary-img" src="/Game/img/quiz-carry/btn-ready.png" width="220" height="56" alt="READY" decoding="async" />
</button>
</div>
</div>
</div>
</div>
<div id="quiz-carry-embed-countdown" class="is-hidden" role="alert" aria-live="assertive" aria-atomic="true" aria-hidden="true">
<div id="quiz-carry-embed-countdown-inner">
<p id="quiz-carry-embed-countdown-kicker" class="quiz-carry-embed-countdown-kicker">คำถาม · Question</p>
@@ -765,7 +902,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.095"></script>
<script src="js/play.js?v=0.100"></script>
<div class="version-tag">v —</div>
</body>
</html>
+82
View File
@@ -88,6 +88,8 @@ function defaultQuizSettings() {
carryReadMs: 3000,
/** quiz_carry: หลังตัวเลือกขึ้น ให้เวลาตอบ (มิลลิวิ) */
carryAnswerMs: 5000,
/** quiz_carry: จำนวนข้อต่อเซสชันก่อนจบเกม (พรีวิว) — 0 = ไม่จบอัตโนมัติ */
carrySessionLength: 0,
questions: [],
carryQuestions: [],
battleQuizMcq: [],
@@ -163,6 +165,12 @@ function clampQuizBetweenMs(n, def) {
return Math.max(0, Math.min(300000, Math.floor(v)));
}
function clampCarrySessionLength(n, def) {
const v = Number(n);
if (Number.isNaN(v)) return def;
return Math.max(0, Math.min(500, Math.floor(v)));
}
function loadQuizSettings() {
try {
if (fs.existsSync(QUIZ_SETTINGS_PATH)) {
@@ -177,6 +185,7 @@ function loadQuizSettings() {
betweenMs: clampQuizBetweenMs(j.betweenMs, d.betweenMs),
carryReadMs: clampQuizBetweenMs(j.carryReadMs, d.carryReadMs),
carryAnswerMs: clampQuizMs(j.carryAnswerMs, d.carryAnswerMs),
carrySessionLength: clampCarrySessionLength(j.carrySessionLength, d.carrySessionLength),
questions,
carryQuestions,
battleQuizMcq,
@@ -474,6 +483,9 @@ function saveQuizSettings(d) {
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 carrySessionLength = d.carrySessionLength != null
? clampCarrySessionLength(d.carrySessionLength, prev.carrySessionLength)
: prev.carrySessionLength;
const questions = Array.isArray(d.questions)
? (d.questions || [])
.filter((q) => q && String(q.text || '').trim())
@@ -491,6 +503,7 @@ function saveQuizSettings(d) {
betweenMs,
carryReadMs,
carryAnswerMs,
carrySessionLength,
questions,
carryQuestions,
battleQuizMcq,
@@ -2259,6 +2272,22 @@ function scheduleTroublesomeRoll(sid) {
}
}
/**
* ห้องทดสอบเอดิเตอร์ / join ด้วย mapId: mapData บน space อาจยังเป็น lobby แต่ไคลเอนต์เล่น quiz_carry จากพารามิเตอร์
* — ให้ lobby Ready/START ทำงานถ้า mapId ชี้แมป quiz_carry หรือเป็นห้อง preview
*/
function spaceAllowsQuizCarryLobbyRelaxed(space) {
if (!space) return false;
if (space.mapId) {
const byId = maps.get(space.mapId);
if (byId && byId.gameType === 'quiz_carry') return true;
}
const md = space.mapData;
if (md && md.gameType === 'quiz_carry') return true;
return !!(space.previewEditorTest
|| (space.isPrivate && String(space.spaceName || '').toLowerCase() === 'preview'));
}
io.on('connection', (socket) => {
socket.on('join-space', ({ spaceId, nickname, characterId }, cb) => {
const space = spaces.get(spaceId);
@@ -2304,6 +2333,11 @@ io.on('connection', (socket) => {
peer.y = sn.y;
}
space.peers.set(socket.id, peer);
if (spaceAllowsQuizCarryLobbyRelaxed(space)) {
if (!space.quizCarryLobbyReady || typeof space.quizCarryLobbyReady !== 'object') space.quizCarryLobbyReady = {};
space.quizCarryLobbyReady[socket.id] = false;
io.to(spaceId).emit('quiz-carry-lobby-sync', { readyMap: { ...space.quizCarryLobbyReady } });
}
if (mdJoin.gameType === 'gauntlet') {
const ord = [...space.peers.keys()].indexOf(socket.id);
const pos = gauntletSpawnPositions(mdJoin, space.peers.size);
@@ -2624,6 +2658,12 @@ io.on('connection', (socket) => {
if (space.troublesomeEligible) space.troublesomeEligible.delete(socket.id);
if (space.quizSession && space.quizSession.players) delete space.quizSession.players[socket.id];
if (space.gauntletPreviewRowsBySocket) space.gauntletPreviewRowsBySocket.delete(socket.id);
if (space.quizCarryLobbyReady && typeof space.quizCarryLobbyReady === 'object') {
delete space.quizCarryLobbyReady[socket.id];
if (spaceAllowsQuizCarryLobbyRelaxed(space)) {
io.to(sid).emit('quiz-carry-lobby-sync', { readyMap: { ...space.quizCarryLobbyReady } });
}
}
space.peers.delete(socket.id);
if (space.peers.size === 0) {
stopGauntletTicker(space);
@@ -2672,6 +2712,48 @@ io.on('connection', (socket) => {
space.gauntletPreviewRowsBySocket.set(socket.id, set);
});
socket.on('quiz-carry-lobby-sync-request', () => {
const sid = socket.data.spaceId;
const space = sid ? spaces.get(sid) : null;
if (!space || !space.peers.has(socket.id)) return;
if (!spaceAllowsQuizCarryLobbyRelaxed(space)) return;
if (!space.quizCarryLobbyReady || typeof space.quizCarryLobbyReady !== 'object') {
space.quizCarryLobbyReady = {};
}
for (const id of space.peers.keys()) {
if (space.quizCarryLobbyReady[id] === undefined) space.quizCarryLobbyReady[id] = false;
}
socket.emit('quiz-carry-lobby-sync', { readyMap: { ...space.quizCarryLobbyReady } });
});
socket.on('quiz-carry-lobby-ready', (data) => {
const sid = socket.data.spaceId;
const space = sid ? spaces.get(sid) : null;
if (!space || !space.peers.has(socket.id)) return;
if (!spaceAllowsQuizCarryLobbyRelaxed(space)) return;
if (!space.quizCarryLobbyReady || typeof space.quizCarryLobbyReady !== 'object') space.quizCarryLobbyReady = {};
for (const id of space.peers.keys()) {
if (space.quizCarryLobbyReady[id] === undefined) space.quizCarryLobbyReady[id] = false;
}
space.quizCarryLobbyReady[socket.id] = !!(data && data.ready);
io.to(sid).emit('quiz-carry-lobby-sync', { readyMap: { ...space.quizCarryLobbyReady } });
});
socket.on('quiz-carry-lobby-start', (_data, cb) => {
const reply = typeof cb === 'function' ? cb : () => {};
const sid = socket.data.spaceId;
const space = sid ? spaces.get(sid) : null;
if (!space || !space.peers.has(socket.id)) return reply({ ok: false, error: 'ไม่อยู่ในห้อง' });
if (!spaceAllowsQuizCarryLobbyRelaxed(space)) return reply({ ok: false, error: 'ห้องนี้ไม่ใช่โหมด lobby หยิบมาวาง / พรีวิว' });
if (space.hostId !== socket.id) return reply({ ok: false, error: 'เฉพาะโฮสต์กด START' });
const rm = space.quizCarryLobbyReady || {};
const humanIds = [...space.peers.keys()];
const allReady = humanIds.length > 0 && humanIds.every((id) => rm[id]);
if (!allReady) return reply({ ok: false, error: 'ยังมีผู้เล่นที่ยังไม่ Ready' });
io.to(sid).emit('quiz-carry-lobby-started', {});
reply({ ok: true });
});
socket.on('move', (data) => {
for (const [sid, space] of spaces) {
if (space.peers.has(socket.id)) {