minigame before end

This commit is contained in:
2026-04-29 16:08:25 +00:00
parent 9e2cff2935
commit 3bf9fe669d
28 changed files with 259 additions and 55 deletions
+3 -3
View File
@@ -10,16 +10,16 @@
"panelBorder": "rgba(255, 255, 255, 0)",
"textColor": "rgba(241, 245, 249, 1)",
"borderWidthPx": 0,
"questionFontMinPx": 10,
"questionFontMinPx": 17,
"questionFontMaxPx": 24
},
"carryEmbedCountdownTheme": {
"overlayBackdrop": "rgba(8, 10, 20, 0)",
"innerBg": "rgba(12, 14, 28, 0)",
"innerBorder": "rgba(122, 162, 247, 0.45)",
"innerBorder": "rgba(122, 162, 247, 0)",
"innerBorderWpx": 1,
"innerRadiusPx": 12,
"digitColor": "#ffe066",
"digitColor": "#f2f2f2",
"mapDigitCqmin": 78,
"mapDigitCqh": 82,
"mapDigitMaxPx": 200,
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: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 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: 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: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 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: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

+208 -50
View File
@@ -1,5 +1,12 @@
(function () {
const BASE = '/Game';
/** แผงคำถามบนแมป quiz_carry — ไฟล์อยู่ public/img/quiz-carry */
const QUIZ_CARRY_MAP_FEEDBACK_IMG = {
correct: BASE + '/img/quiz-carry/icon-Correct.png',
incorrect: BASE + '/img/quiz-carry/icon-Incorrect.png',
scorePlus: BASE + '/img/quiz-carry/score+10.png',
};
const QUIZ_MAP_CARRY_FEEDBACK_MS = 1400;
const params = new URLSearchParams(window.location.search);
const spaceId = params.get('space');
const nick = params.get('nick') || 'ผู้เล่น';
@@ -811,6 +818,53 @@
textEl.style.removeProperty('overflow');
}
function clearQuizMapQuestionCarryFeedback() {
if (typeof window.__quizMapCarryFeedbackT === 'number') {
clearTimeout(window.__quizMapCarryFeedbackT);
window.__quizMapCarryFeedbackT = null;
}
const icon = document.getElementById('quiz-map-q-feedback-icon');
const scoreWrap = document.getElementById('quiz-map-q-feedback-score-wrap');
const scoreEl = document.getElementById('quiz-map-q-feedback-score');
if (icon) {
icon.classList.add('is-hidden');
icon.removeAttribute('src');
}
if (scoreEl) scoreEl.classList.remove('quiz-map-q-feedback-score--pop');
if (scoreWrap) {
scoreWrap.classList.add('is-hidden');
scoreWrap.setAttribute('aria-hidden', 'true');
}
}
/** ไอคอนถูก/ผิดที่ขอบบนแผง + score+10 ตรงกลางใต้ข้อความ (เฉพาะถูก) — ตาม mock quiz_carry */
function showQuizMapQuestionCarryFeedback(isCorrect) {
if (!isQuizCarry()) return;
const panel = document.getElementById('quiz-map-question-panel');
const icon = document.getElementById('quiz-map-q-feedback-icon');
const scoreWrap = document.getElementById('quiz-map-q-feedback-score-wrap');
const scoreEl = document.getElementById('quiz-map-q-feedback-score');
if (!panel || !icon || panel.classList.contains('is-hidden')) return;
clearQuizMapQuestionCarryFeedback();
icon.src = isCorrect ? QUIZ_CARRY_MAP_FEEDBACK_IMG.correct : QUIZ_CARRY_MAP_FEEDBACK_IMG.incorrect;
icon.classList.remove('is-hidden');
if (isCorrect && scoreWrap && scoreEl) {
scoreEl.src = QUIZ_CARRY_MAP_FEEDBACK_IMG.scorePlus;
scoreWrap.classList.remove('is-hidden');
scoreWrap.setAttribute('aria-hidden', 'false');
scoreEl.classList.remove('quiz-map-q-feedback-score--pop');
void scoreEl.offsetWidth;
scoreEl.classList.add('quiz-map-q-feedback-score--pop');
} else if (scoreWrap) {
scoreWrap.classList.add('is-hidden');
scoreWrap.setAttribute('aria-hidden', 'true');
}
window.__quizMapCarryFeedbackT = setTimeout(() => {
window.__quizMapCarryFeedbackT = null;
clearQuizMapQuestionCarryFeedback();
}, QUIZ_MAP_CARRY_FEEDBACK_MS);
}
/** ค่าเริ่มต้นแผงคำถามบนแผนที่ (ตรงกับ server defaultCarryMapPanelTheme) — ใช้เมื่อยังไม่โหลดจาก API */
function defaultCarryMapPanelThemePlay() {
return {
@@ -1272,23 +1326,29 @@
if (!panel || !textEl || !mapData) return;
/* เอดิเตอร์ embed: ซ่อนแผงทองเฉพาะโหมด quiz (ทับกลาง) — quiz_carry ต้องโชว์ตามโซนที่วาดบนแผนที่ */
if (previewMode && editorEmbedReturn && !isQuizCarry()) {
clearQuizMapQuestionCarryFeedback();
panel.classList.add('is-hidden');
panel.setAttribute('aria-hidden', 'true');
clearQuizMapPanelThemeInline(panel, textEl);
return;
}
if (isQuizBattle()) {
clearQuizMapQuestionCarryFeedback();
panel.classList.add('is-hidden');
panel.setAttribute('aria-hidden', 'true');
clearQuizMapPanelThemeInline(panel, textEl);
return;
}
if (!isQuiz() && !isQuizCarry()) return;
if (!isQuiz() && !isQuizCarry()) {
clearQuizMapQuestionCarryFeedback();
return;
}
const bounds = isQuiz()
? getQuizQuestionAreaTileBounds(mapData)
: (getQuizCarryQuestionAreaTileBounds(mapData) || getQuizCarryHubTileBounds(mapData));
const text = (playQuizText || '').trim();
if (!bounds || !text) {
clearQuizMapQuestionCarryFeedback();
panel.classList.add('is-hidden');
panel.setAttribute('aria-hidden', 'true');
clearQuizMapPanelThemeInline(panel, textEl);
@@ -2444,43 +2504,123 @@
}
}
/** พรีวิวเอดิเตอร์: ช่วงนับ 3–2–1 — ขอบเรืองรอบโซนตัวเลือก (ช่อง = quizCarryOptN บนกริด, ตามข้อถูกหรือ countdownHighlightSlot) */
function drawQuizCarryEmbedCountdownHighlight(ctx, worldToScreen, zoom, timeMs) {
if (!previewMode || !editorEmbedReturn || !isQuizCarry() || !mapData) return;
if (mapData.carryEmbedCountdownHighlight === false) return;
const pq = quizCarryEmbedPendingQuestion;
if (!pq || !quizCarryEmbedCountdownEndAt || Date.now() >= quizCarryEmbedCountdownEndAt) return;
const choices = pq.choices;
if (!choices || !choices.length) return;
let slot1 = pq.countdownHighlightSlot;
if (!Number.isFinite(slot1) || slot1 < 1) {
let ci = Number(pq.correctIndex);
if (!Number.isFinite(ci) || ci < 0 || ci >= choices.length) ci = 0;
slot1 = ci + 1;
} else {
slot1 = Math.min(16, Math.max(1, Math.floor(slot1)));
}
const b = getQuizCarryOptionClusterBounds(mapData, slot1);
if (!b || b.minX === Infinity) return;
/**
* กลองปายบนจอ (signX, signY, w, h) ใหตรงก drawQuizCarryChoiceLabels สำหรบชอง idx 0-based
*/
function computeQuizCarryChoicePlaqueScreenRect(ctx, worldToScreen, zoom, idx0Based) {
if (!isQuizCarry() || !mapData || !quizCarryCurrent) return null;
const q = quizCarryCurrent;
const choices = q.choices;
if (!choices || !choices.length || idx0Based < 0 || idx0Based >= choices.length) return null;
const i = idx0Based;
const b = getQuizCarryOptionClusterBounds(mapData, i + 1);
if (!b) return null;
const ts = tileSize * zoom;
const wx0 = b.minX * tileSize;
const wy0 = b.minY * tileSize;
const wx1 = (b.maxX + 1) * tileSize;
const wy1 = (b.maxY + 1) * tileSize;
const corners = [[wx0, wy0], [wx1, wy0], [wx1, wy1], [wx0, wy1]];
let sxMin = Infinity, syMin = Infinity, sxMax = -Infinity, syMax = -Infinity;
for (let i = 0; i < corners.length; i++) {
const [sx, sy] = worldToScreen(corners[i][0], corners[i][1]);
if (sx < sxMin) sxMin = sx;
if (sx > sxMax) sxMax = sx;
if (sy < syMin) syMin = sy;
if (sy > syMax) syMax = sy;
const ps = quizCarryPlaqueMapScaleClampedPlay();
const choiceText = String(choices[i] || '').trim();
const imgUrl = getQuizCarryPlaqueImageUrlForIndex(q, i);
const gridIdleEl = findQuizCarryGridIdleImgForChoiceIndex(i);
const plaqueExtra = imgUrl ? { imageUrl: imgUrl }
: (gridIdleEl && gridIdleEl.complete && gridIdleEl.naturalWidth ? { imageElement: gridIdleEl } : {});
if (!choiceText && !plaqueExtra.imageUrl && !plaqueExtra.imageElement) return null;
const tileSpanX = (b.maxX - b.minX + 1);
const tileSpanY = (b.maxY - b.minY + 1);
const minPlaqueW = Math.ceil(Math.max(72, tileSpanX * ts - 16) * ps);
const minPlaqueH = Math.ceil(Math.max(52, tileSpanY * ts - 12) * ps);
const wx = b.cx * tileSize;
const wy = b.cy * tileSize;
const [sx, sy] = worldToScreen(wx, wy);
ctx.save();
let signX;
let signY;
let w;
let h;
if (!choiceText && (plaqueExtra.imageUrl || plaqueExtra.imageElement)) {
const minW = minPlaqueW;
const minH = minPlaqueH;
signX = sx - minW / 2;
signY = sy - minH / 2;
w = minW;
h = minH;
ctx.restore();
return { signX, signY, w, h };
}
const pad = Math.max(4, ts * 0.1);
const rx = sxMin - pad;
const ry = syMin - pad;
const rw = Math.max(8, sxMax - sxMin + pad * 2);
const rh = Math.max(8, syMax - syMin + pad * 2);
const line = choiceText;
const fontPx = Math.max(10, Math.min(24, ts * 0.24 * ps));
ctx.font = '600 ' + fontPx + 'px system-ui, "Segoe UI", "Kanit", sans-serif';
let maxW = Math.max(56, tileSpanX * ts - 14);
const lineH = fontPx * 1.2;
let textLines = canvasWordWrapLines(ctx, line, maxW);
if (!textLines.length) textLines = [line];
if (textLines.length * lineH > tileSpanY * ts - 8 && textLines.length > 1) {
const fp2 = Math.max(9, fontPx * 0.85);
ctx.font = '600 ' + fp2 + 'px system-ui, "Segoe UI", "Kanit", sans-serif';
const lh2 = fp2 * 1.2;
maxW = Math.max(48, maxW - 4);
textLines = canvasWordWrapLines(ctx, line, maxW);
if (textLines.length * lh2 > tileSpanY * ts - 8) {
textLines = textLines.slice(0, Math.max(1, Math.floor((tileSpanY * ts - 8) / lh2)));
}
for (let ti = 0; ti < textLines.length; ti++) {
let s = textLines[ti];
if (ctx.measureText(s).width <= maxW) continue;
while (s.length > 2 && ctx.measureText(s + '…').width > maxW) s = s.slice(0, -1);
textLines[ti] = s + '…';
}
const maxLineW = Math.max(...textLines.map((t) => ctx.measureText(t).width), 40);
const padX = 10;
const padY = 8;
w = Math.ceil(maxLineW + padX * 2);
h = Math.ceil(textLines.length * lh2 + padY * 2);
if (imgUrl || plaqueExtra.imageElement) {
w = Math.max(w, minPlaqueW);
h = Math.max(h, minPlaqueH);
}
signX = sx - w / 2;
signY = sy - h / 2;
ctx.restore();
return { signX, signY, w, h };
}
for (let ti = 0; ti < textLines.length; ti++) {
let s = textLines[ti];
if (ctx.measureText(s).width <= maxW) continue;
while (s.length > 2 && ctx.measureText(s + '…').width > maxW) s = s.slice(0, -1);
textLines[ti] = s + '…';
}
let maxLineW = 0;
for (let ti = 0; ti < textLines.length; ti++) {
const lw = ctx.measureText(textLines[ti]).width;
if (lw > maxLineW) maxLineW = lw;
}
const padX = 10;
const padY = 8;
w = Math.ceil(Math.max(maxLineW, 52) + padX * 2);
h = Math.ceil(textLines.length * lineH + padY * 2);
if (imgUrl || plaqueExtra.imageElement) {
w = Math.max(w, minPlaqueW);
h = Math.max(h, minPlaqueH);
}
signX = sx - w / 2;
signY = sy - h / 2;
ctx.restore();
return { signX, signY, w, h };
}
/**
* ขอบเรองรอบชองตวเลอก (carryEmbedCountdownHighlight + carryEmbedCountdownHighlightColor)
* ขนาดกรอบเทาปายบนแมป (เดยวก drawQuizCarryChoiceLabels) ไมใช cluster กร
*/
function drawQuizCarryEmbedCountdownHighlight(ctx, worldToScreen, zoom, timeMs) {
if (!isQuizCarry() || !mapData) return;
if (mapData.carryEmbedCountdownHighlight === false) return;
const optIdx = getQuizCarryLocalGrabbableOptionIndex();
if (optIdx == null) return;
const pr = computeQuizCarryChoicePlaqueScreenRect(ctx, worldToScreen, zoom, optIdx);
if (!pr || pr.w < 4 || pr.h < 4) return;
const rx = pr.signX;
const ry = pr.signY;
const rw = pr.w;
const rh = pr.h;
const rr = Math.max(6, Math.min(16, Math.min(rw, rh) * 0.14));
const colRaw = mapData.carryEmbedCountdownHighlightColor;
const strokeBase = typeof colRaw === 'string' && /^#[0-9a-fA-F]{6}$/.test(colRaw.trim()) ? colRaw.trim() : '#ffb44a';
@@ -2913,9 +3053,6 @@
if (Date.now() < quizCarryAnswerCloseAt) return;
quizCarryAnswerCloseAt = 0;
quizCarryOptionRevealAt = 0;
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;
@@ -3013,6 +3150,7 @@
quizCarryEmbedCountdownEndAt = quizCarryEmbedCountdownStartAt + 3000;
quizCarryCurrent = null;
playQuizText = '';
clearQuizMapQuestionCarryFeedback();
quizCarryOptionRevealAt = 0;
quizCarryAnswerCloseAt = 0;
me.quizCarryHeld = null;
@@ -3035,6 +3173,7 @@
quizCarryCurrent = q;
preloadQuizCarryChoiceImages(q);
playQuizText = formatQuizCarryQuestionHud(q);
clearQuizMapQuestionCarryFeedback();
const qEl = document.getElementById('quiz-game-question');
if (qEl) qEl.textContent = playQuizText;
me.quizCarryHeld = null;
@@ -3130,14 +3269,15 @@
renderPlayQuizScoreboard(playLiveQuizScores);
}
/** Toast กลางบน (หยิบ / ถูก / ผิด / คำใบ้) — ปิดการแสดงตามคำขอ UX */
function showQuizCarryToast(msg, ok) {
if (typeof window.__quizCarryToastT === 'number') clearTimeout(window.__quizCarryToastT);
window.__quizCarryToastT = null;
const el = document.getElementById('play-quiz-feedback');
if (!el) return;
el.classList.remove('is-hidden');
el.className = ok ? 'play-quiz-feedback play-quiz-feedback-ok' : 'play-quiz-feedback play-quiz-feedback-bad';
el.textContent = msg || '';
if (typeof window.__quizCarryToastT === 'number') clearTimeout(window.__quizCarryToastT);
window.__quizCarryToastT = setTimeout(() => { el.classList.add('is-hidden'); }, 2800);
el.classList.add('is-hidden');
el.className = 'play-quiz-feedback';
el.textContent = '';
}
/**
@@ -3171,13 +3311,23 @@
playLiveQuizScores[actorId] = (playLiveQuizScores[actorId] || 0) + 1;
const lim = previewMode && isQuizCarry() ? quizCarrySessionLength : 0;
const willEnd = lim > 0 && (quizCarryRoundsCompleted + 1) >= lim;
const showMapFb = !opts.silent && actorId === myId;
if (showMapFb) showQuizMapQuestionCarryFeedback(true);
if (!opts.silent) {
showQuizCarryToast(willEnd ? 'ถูกต้อง +1 แต้ม — ครบชุดคำถาม' : 'ถูกต้อง +1 แต้ม — สุ่มคำถามใหม่', true);
}
me.quizCarryHeld = null;
others.forEach((o) => { if (o) o.quizCarryHeld = null; });
quizCarryAfterRoundResolved();
const afterOk = () => { quizCarryAfterRoundResolved(); };
if (showMapFb) {
window.setTimeout(afterOk, 560);
} else {
afterOk();
}
} else {
if (!opts.silent && actorId === myId) {
showQuizMapQuestionCarryFeedback(false);
}
if (!opts.silent) {
showQuizCarryToast('ผิด — คืนป้ายที่โซนตัวเลือกแล้ว กด F หยิบใหม่ได้', false);
}
@@ -3227,10 +3377,10 @@
return true;
}
/** ยืนบนช่องตัวเลือกและหยิบได้ */
function quizCarryGrabPickupAvailable() {
if (!quizCarryGrabCoreGatesOk()) return false;
if (me.quizCarryHeld != null) return false;
/** ดัชนีตัวเลือก 0-based ที่ผู้เล่นท้องถิ่นยืนอยู่และหยิบได้ — ไม่มีคืน null */
function getQuizCarryLocalGrabbableOptionIndex() {
if (!quizCarryGrabCoreGatesOk()) return null;
if (me.quizCarryHeld != null) return null;
const md = mapData;
const footprint = quizTilesFootprintPlay(me.x, me.y);
let pickupIdx = null;
@@ -3241,7 +3391,13 @@
const opt = quizCarryOptionIndexAtPlay(md, tx, ty);
if (opt != null) pickupIdx = pickupIdx === null ? opt : pickupIdx;
}
return pickupIdx != null && !quizCarryOptionHeldByAnyone(pickupIdx);
if (pickupIdx == null || quizCarryOptionHeldByAnyone(pickupIdx)) return null;
return pickupIdx;
}
/** ยืนบนช่องตัวเลือกและหยิบได้ */
function quizCarryGrabPickupAvailable() {
return getQuizCarryLocalGrabbableOptionIndex() != null;
}
/** ถือป้ายแล้วยืนโซนส่งได้ */
@@ -8276,6 +8432,8 @@
const gw = document.querySelector('.game-wrap');
if (gw) gw.classList.toggle('play-cyber-active', !!on);
if (!on || !root) return;
const timeLabelEl = root.querySelector('.play-cyber-time-label');
if (timeLabelEl) timeLabelEl.style.display = isQuizCarry() ? 'none' : '';
const timeVal = document.getElementById('play-cyber-time-val');
const timeSub = document.getElementById('play-cyber-time-sub');
const portrait = document.getElementById('play-cyber-portrait-img');
+48 -2
View File
@@ -127,7 +127,7 @@
align-items: stretch;
justify-content: flex-start;
text-align: center;
overflow: hidden;
overflow: visible;
scrollbar-width: none;
-ms-overflow-style: none;
--qmap-bg: rgba(12, 14, 28, 0.88);
@@ -172,6 +172,48 @@
width: 0;
height: 0;
}
/* quiz_carry: ไอคอนถูก/ผิดโผล่ครึ่งบนกรอบแผง — ข้อความยัง clip ใน p */
#quiz-map-q-feedback-icon {
position: absolute;
left: 50%;
top: 0;
transform: translate(-50%, -44%);
width: clamp(28px, min(24%, 5vmin), 76px);
height: auto;
z-index: 4;
pointer-events: none;
object-fit: contain;
filter: drop-shadow(0 6px 14px rgba(0, 0, 0, 0.55));
}
#quiz-map-q-feedback-icon.is-hidden {
display: none !important;
}
#quiz-map-q-feedback-score-wrap {
flex: 1 1 auto;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
padding: 0.12em 0 0.2em;
}
#quiz-map-q-feedback-score-wrap.is-hidden {
display: none !important;
}
#quiz-map-q-feedback-score {
width: clamp(44px, min(58%, 12vmin), 160px);
height: auto;
object-fit: contain;
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.45));
}
#quiz-map-q-feedback-score.quiz-map-q-feedback-score--pop {
animation: quizMapQScorePop 0.58s ease-out both;
}
@keyframes quizMapQScorePop {
0% { transform: scale(0.55); opacity: 0; }
58% { transform: scale(1.06); opacity: 1; }
100% { transform: scale(1); opacity: 1; }
}
#quiz-carry-embed-countdown {
position: fixed;
inset: 0;
@@ -918,7 +960,11 @@
<img src="/Game/img/quiz-carry/btn-grab.png" alt="" width="256" height="256" decoding="async" />
</button>
<div id="quiz-map-question-panel" class="is-hidden" aria-hidden="true">
<img id="quiz-map-q-feedback-icon" class="quiz-map-q-feedback-icon is-hidden" alt="" width="64" height="64" decoding="async" />
<p id="quiz-map-question-text"></p>
<div id="quiz-map-q-feedback-score-wrap" class="is-hidden" aria-hidden="true">
<img id="quiz-map-q-feedback-score" alt="" width="120" height="48" decoding="async" />
</div>
</div>
</div>
</div>
@@ -1013,7 +1059,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.155"></script>
<script src="js/play.js?v=0.160"></script>
<div class="version-tag">v —</div>
</body>
</html>