update special cards flow

This commit is contained in:
2026-05-31 09:05:24 +00:00
parent 8f05d6019d
commit a88ffdc70a
10 changed files with 2315 additions and 104 deletions
+82 -4
View File
@@ -669,14 +669,92 @@ legend {
border-radius: 999px;
white-space: nowrap;
}
.sq-card-grid {
/* —— เลือกการ์ดพิเศษ (card-1..7 จาก trycss/10-Game-1-result) —— */
.sq-card-picker-wrap {
display: flex;
flex-wrap: wrap;
gap: 1.25rem;
align-items: flex-start;
margin-bottom: 1rem;
}
.sq-card-picker {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
gap: 0.65rem;
flex: 1 1 420px;
max-width: 720px;
}
.sq-card-pick {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.35rem;
padding: 0.45rem 0.35rem 0.5rem;
border: 2px solid var(--border);
border-radius: 12px;
background: rgba(255, 255, 255, 0.03);
cursor: pointer;
transition: border-color 0.15s, box-shadow 0.15s, transform 0.12s;
}
.sq-card-pick:hover {
border-color: rgba(122, 162, 247, 0.55);
transform: translateY(-1px);
}
.sq-card-pick.is-selected {
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(122, 162, 247, 0.35), 0 4px 16px rgba(0, 0, 0, 0.25);
background: var(--accent-dim);
}
.sq-card-pick img {
width: 72px;
height: auto;
max-height: 96px;
object-fit: contain;
display: block;
image-rendering: auto;
}
.sq-card-pick-cap {
font-size: 0.68rem;
font-weight: 600;
color: var(--muted);
text-align: center;
line-height: 1.15;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sq-card-pick.is-selected .sq-card-pick-cap { color: var(--accent); }
.sq-card-preview {
flex: 0 0 auto;
padding: 0.75rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: rgba(0, 0, 0, 0.25);
text-align: center;
}
.sq-card-preview-label {
margin: 0 0 0.5rem;
font-size: 0.78rem;
color: var(--muted);
}
.sq-card-preview-img {
width: 120px;
height: auto;
max-height: 160px;
object-fit: contain;
display: block;
margin: 0 auto;
}
.sq-card-meta {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.85rem;
margin-top: 0.25rem;
}
.sq-card-grid .sq-card-url { grid-column: 1 / -1; }
.sq-card-grid input,
.sq-card-grid select { width: 100%; }
.sq-card-meta input,
.sq-card-meta select { width: 100%; }
.sq-count {
font-weight: 400;
font-size: 0.8rem;
+133 -4
View File
@@ -248,8 +248,129 @@
/* ===== ชุดคำถามพิเศษ (Level×Case = 15 ชุด) รับการ์ดพิเศษ ===== */
var SPECIAL_CARD_RARITIES = ['common', 'rare', 'legendary'];
var SPECIAL_CARD_ASSET_BASE = '/Game/img/special-cards/';
var SPECIAL_CARD_CATALOG = [
{ id: 1, en: 'Time Dilation', th: 'เพิ่มเวลาเล่นมินิเกมทันที (+10 วินาที)' },
{ id: 2, en: 'Extension', th: 'เพิ่มเวลาในห้องพิจารณาคดี (+10 วิ FINAL VOTE)' },
{ id: 3, en: 'Ban Card', th: 'ห้ามเล่นมินิเกม 1 ตา' },
{ id: 4, en: 'Silence Card', th: 'การ์ดปิดปาก' },
{ id: 5, en: 'Bail Coin', th: 'เหรียญประกันตัว' },
{ id: 6, en: 'Fund', th: 'รับ coin เพิ่ม' },
{ id: 7, en: 'Free Evidence', th: 'รับหลักฐานฟรี' },
];
var specialQuizSetsState = {};
var specialQuizCurrentKey = 'L1_C1';
var specialCardPickerBuilt = false;
function specialCardTxtFilename(id) {
return id === 7 ? 'card--7txt.png' : 'card-' + id + '-txt.png';
}
function specialCardImageUrl(id) {
return SPECIAL_CARD_ASSET_BASE + 'card-' + id + '.png';
}
function specialCardTxtImageUrl(id) {
return SPECIAL_CARD_ASSET_BASE + specialCardTxtFilename(id);
}
function specialCardCatalogEntry(id) {
for (var i = 0; i < SPECIAL_CARD_CATALOG.length; i++) {
if (SPECIAL_CARD_CATALOG[i].id === id) return SPECIAL_CARD_CATALOG[i];
}
return null;
}
function resolveSpecialCardIdFromSaved(card) {
if (!card || typeof card !== 'object') return null;
var cid = Number(card.cardId);
if (cid >= 1 && cid <= 7) return cid;
var url = String(card.imageUrl || '');
var m = url.match(/\/card-(\d)\.png/i);
return m ? Number(m[1]) : null;
}
function updateSpecialCardPreview() {
var prev = el('special-card-preview');
var img = el('special-card-preview-img');
var cid = parseInt(el('special-card-id') && el('special-card-id').value, 10);
if (!prev || !img) return;
if (!(cid >= 1 && cid <= 7)) {
prev.hidden = true;
img.removeAttribute('src');
return;
}
img.src = specialCardImageUrl(cid);
img.alt = (specialCardCatalogEntry(cid) || {}).en || ('Card ' + cid);
prev.hidden = false;
}
function setSpecialCardPickerSelection(cardId) {
var picker = el('special-card-picker');
if (!picker) return;
picker.querySelectorAll('.sq-card-pick').forEach(function (btn) {
var on = String(btn.getAttribute('data-card-id')) === String(cardId);
btn.classList.toggle('is-selected', on);
btn.setAttribute('aria-checked', on ? 'true' : 'false');
});
}
function selectSpecialCard(cardId, fillMeta) {
var idInp = el('special-card-id');
var imgInp = el('special-card-image');
var txtInp = el('special-card-txt-image');
if (!(cardId >= 1 && cardId <= 7)) {
if (idInp) idInp.value = '';
if (imgInp) imgInp.value = '';
if (txtInp) txtInp.value = '';
setSpecialCardPickerSelection(null);
updateSpecialCardPreview();
return;
}
var entry = specialCardCatalogEntry(cardId);
if (idInp) idInp.value = String(cardId);
if (imgInp) imgInp.value = specialCardImageUrl(cardId);
if (txtInp) txtInp.value = specialCardTxtImageUrl(cardId);
if (fillMeta && entry) {
if (el('special-card-th')) el('special-card-th').value = entry.th;
if (el('special-card-en')) el('special-card-en').value = entry.en;
}
setSpecialCardPickerSelection(cardId);
updateSpecialCardPreview();
}
function ensureSpecialCardPicker() {
if (specialCardPickerBuilt) return;
var picker = el('special-card-picker');
if (!picker) return;
picker.innerHTML = '';
SPECIAL_CARD_CATALOG.forEach(function (entry) {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'sq-card-pick';
btn.setAttribute('role', 'radio');
btn.setAttribute('aria-checked', 'false');
btn.setAttribute('data-card-id', String(entry.id));
btn.title = entry.en + ' — ' + entry.th;
var img = document.createElement('img');
img.src = specialCardImageUrl(entry.id);
img.alt = entry.en;
img.width = 72;
img.height = 96;
img.decoding = 'async';
img.loading = 'lazy';
var cap = document.createElement('span');
cap.className = 'sq-card-pick-cap';
cap.textContent = entry.en;
btn.appendChild(img);
btn.appendChild(cap);
btn.addEventListener('click', function () {
selectSpecialCard(entry.id, true);
});
picker.appendChild(btn);
});
specialCardPickerBuilt = true;
}
function specialQuizSelectorKey() {
var L = el('special-quiz-level');
@@ -260,7 +381,7 @@
function updateSpecialQuizCount() {
var c = el('special-quiz-count');
if (!c) return;
var n = document.querySelectorAll('#special-quiz-questions-list .quiz-admin-q-row').length;
var n = document.querySelectorAll('#special-quiz-questions-list .sq-mcq-row').length;
c.textContent = '(' + n + ' ข้อ)';
}
@@ -345,14 +466,18 @@
}
function renderSpecialQuizSet(key) {
ensureSpecialCardPicker();
var set = specialQuizSetsState[key] || { questions: [], specialCard: {} };
var card = (set.specialCard && typeof set.specialCard === 'object') ? set.specialCard : {};
if (el('special-card-th')) el('special-card-th').value = card.th || '';
if (el('special-card-en')) el('special-card-en').value = card.en || '';
var cardId = resolveSpecialCardIdFromSaved(card);
selectSpecialCard(cardId, false);
if (el('special-card-th')) el('special-card-th').value = card.th || (cardId ? (specialCardCatalogEntry(cardId) || {}).th : '') || '';
if (el('special-card-en')) el('special-card-en').value = card.en || (cardId ? (specialCardCatalogEntry(cardId) || {}).en : '') || '';
if (el('special-card-rarity')) {
el('special-card-rarity').value = SPECIAL_CARD_RARITIES.indexOf(card.rarity) >= 0 ? card.rarity : 'rare';
}
if (el('special-card-image')) el('special-card-image').value = card.imageUrl || '';
if (cardId && el('special-card-image')) el('special-card-image').value = card.imageUrl || specialCardImageUrl(cardId);
if (cardId && el('special-card-txt-image')) el('special-card-txt-image').value = card.txtImageUrl || specialCardTxtImageUrl(cardId);
var list = el('special-quiz-questions-list');
if (list) {
list.innerHTML = '';
@@ -386,13 +511,17 @@
questions.push({ type: 'mcq', text: t, choices: choices, correctIndex: correctIndex });
});
var rar = el('special-card-rarity') ? el('special-card-rarity').value : 'rare';
var cardId = parseInt(el('special-card-id') && el('special-card-id').value, 10);
if (!(cardId >= 1 && cardId <= 7)) cardId = null;
specialQuizSetsState[key] = {
questions: questions,
specialCard: {
cardId: cardId,
th: el('special-card-th') ? String(el('special-card-th').value || '').trim() : '',
en: el('special-card-en') ? String(el('special-card-en').value || '').trim() : '',
rarity: SPECIAL_CARD_RARITIES.indexOf(rar) >= 0 ? rar : 'rare',
imageUrl: el('special-card-image') ? String(el('special-card-image').value || '').trim() : '',
txtImageUrl: el('special-card-txt-image') ? String(el('special-card-txt-image').value || '').trim() : '',
},
};
}
+16 -8
View File
@@ -10,7 +10,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Kanit:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="admin.css?v=33">
<link rel="stylesheet" href="admin.css?v=34">
</head>
<body>
<a class="skip-link" href="#admin-main">ข้ามไปเนื้อหา</a>
@@ -269,12 +269,23 @@
<fieldset class="sq-fieldset sq-card">
<legend>2 · การ์ดพิเศษของชุดนี้</legend>
<div class="sq-card-grid">
<p class="muted" style="margin:0 0 0.85rem">เลือกการ์ดจากชุดโบนัส <code>/Game/img/special-cards/</code> (card-1 ถึง card-7) — แสดงในโซน «โบนัสพิเศษ» ตาม mockup สรุปผล · <em>English:</em> Pick one bonus card asset (Time Dilation, Bail Coin, Free Evidence, etc.)</p>
<div class="sq-card-picker-wrap">
<div id="special-card-picker" class="sq-card-picker" role="radiogroup" aria-label="เลือกการ์ดพิเศษ"></div>
<div class="sq-card-preview" id="special-card-preview" hidden>
<p class="sq-card-preview-label">ตัวอย่างการ์ดที่เลือก</p>
<img id="special-card-preview-img" class="sq-card-preview-img" alt="" width="120" height="160" decoding="async">
</div>
</div>
<input type="hidden" id="special-card-id" value="">
<input type="hidden" id="special-card-image" value="">
<input type="hidden" id="special-card-txt-image" value="">
<div class="sq-card-meta">
<label class="admin-field">ชื่อการ์ด (ไทย)
<input type="text" id="special-card-th" maxlength="160" placeholder="เช่น เบาะแสลับ">
<input type="text" id="special-card-th" maxlength="160" placeholder="เติมอัตโนมัติเมื่อเลือกการ์ด">
</label>
<label class="admin-field">ชื่อการ์ด (อังกฤษ)
<input type="text" id="special-card-en" maxlength="160" placeholder="e.g. Secret Clue">
<input type="text" id="special-card-en" maxlength="160" placeholder="Auto-filled from card art">
</label>
<label class="admin-field">ระดับการ์ด
<select id="special-card-rarity" class="admin-inp-num">
@@ -283,9 +294,6 @@
<option value="legendary">Legendary</option>
</select>
</label>
<label class="admin-field sq-card-url">URL รูปการ์ด
<input type="text" id="special-card-image" maxlength="600" placeholder="/Main-Lobby/IMAGE/... หรือ https://...">
</label>
</div>
</fieldset>
@@ -1002,6 +1010,6 @@
</div>
</main>
</div>
<script src="admin.js?v=73"></script>
<script src="admin.js?v=75"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
../trycss/10-Game-1-result
Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

+256 -17
View File
@@ -8806,6 +8806,8 @@
pl.appendChild(s);
cardTextEl.appendChild(pl);
}
/* การ์ดพิเศษจากชุดคำถาม (ได้มาจากการตอบคำถามพิเศษในฉาก) — แสดงเสมอถ้าได้รับ */
if (specialQuizAwardedCard) renderSpecialCardInBonus(cardTextEl, specialQuizAwardedCard);
}
overlay.classList.remove('is-hidden');
const btn = document.getElementById('btn-quiz-carry-mission-done');
@@ -11309,25 +11311,31 @@
/** วาดการ์ดพิเศษลงในโซนโบนัสของสรุปผล (#gcm-bonus / .sm-bonus-right) */
function renderSpecialCardInBonus(bonusEl, card) {
if (!bonusEl || !card || !(card.th || card.en || card.imageUrl)) return;
if (!bonusEl || !card || !(card.th || card.en || card.imageUrl || card.cardId)) return;
const imgUrl = card.imageUrl || (card.cardId >= 1 && card.cardId <= 7
? '/Game/img/special-cards/card-' + card.cardId + '.png' : '');
if (!imgUrl) return;
const isPresetAsset = imgUrl.indexOf('/img/special-cards/') >= 0 || imgUrl.indexOf('/trycss/10-Game-1-result/') >= 0;
const wrap = document.createElement('div');
wrap.className = 'gcm-special-card sm-bonus-special rarity-' + (card.rarity || 'rare');
if (card.imageUrl) {
const img = document.createElement('img');
img.className = 'gcm-special-card-img';
img.src = card.imageUrl;
img.alt = card.th || card.en || '';
wrap.appendChild(img);
wrap.className = isPresetAsset
? 'sm-bonus-right gcm-special-card-preset'
: 'gcm-special-card sm-bonus-special rarity-' + (card.rarity || 'rare');
const img = document.createElement('img');
img.className = isPresetAsset ? 'bail-card gcm-special-card-img' : 'gcm-special-card-img';
img.src = imgUrl;
img.alt = card.th || card.en || '';
wrap.appendChild(img);
if (!isPresetAsset) {
const cap = document.createElement('div');
cap.className = 'gcm-special-card-cap';
const strong = document.createElement('strong');
strong.textContent = card.th || '';
const span = document.createElement('span');
span.textContent = card.en || '';
cap.appendChild(strong);
cap.appendChild(span);
wrap.appendChild(cap);
}
const cap = document.createElement('div');
cap.className = 'gcm-special-card-cap';
const strong = document.createElement('strong');
strong.textContent = card.th || '';
const span = document.createElement('span');
span.textContent = card.en || '';
cap.appendChild(strong);
cap.appendChild(span);
wrap.appendChild(cap);
bonusEl.appendChild(wrap);
}
@@ -11340,6 +11348,211 @@
renderCardInBonus: renderSpecialCardInBonus,
};
/* ===== Special Quiz () + overlay multiplayer =====
* server เปนผดส: มไอคอน 1/3, เปดคำถามใหกคนพรอมก, ตอบถกทกคนทกข -> ไดการ
*/
const SPECIAL_QUIZ_CHOICE_LABELS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'];
let specialQuizIcon = null; // { iconType, x, y } (พิกัด tile-center)
let specialQuizCollideSent = false;
let specialQuizAwardedCard = null; // การ์ดที่ได้รอบนี้ (จาก special-quiz-ended)
let specialQuizActiveQuestion = null; // { index, total, choices, endsAt, iconType }
let specialQuizAnswered = false;
let specialQuizTimerRaf = 0;
const specialQuizIconImgs = {};
function specialQuizIconUrl(type) {
return '/Game/img/special-quiz/icon-' + (type === 'police' ? 'police' : 'lawyer') + '.png';
}
function specialQuizGetIconImg(type) {
const key = type === 'police' ? 'police' : 'lawyer';
if (specialQuizIconImgs[key]) return specialQuizIconImgs[key];
const img = new Image();
img.src = specialQuizIconUrl(key);
specialQuizIconImgs[key] = img;
return img;
}
function setSpecialQuizIconFromServer(payload) {
if (!payload || payload.x == null || payload.y == null) { specialQuizIcon = null; return; }
specialQuizIcon = {
iconType: payload.iconType === 'police' ? 'police' : 'lawyer',
x: Number(payload.x),
y: Number(payload.y),
};
specialQuizCollideSent = false;
specialQuizGetIconImg(specialQuizIcon.iconType);
}
function clearSpecialQuizIcon() {
specialQuizIcon = null;
specialQuizCollideSent = false;
}
/** วาดไอคอนในฉาก — ใช้ worldToScreen + zDraw จาก draw() เพื่อให้ตรงทุกมินิเกม */
function drawSpecialQuizIconWorld(w2s, zDraw) {
if (!specialQuizIcon || typeof w2s !== 'function') return;
const img = specialQuizGetIconImg(specialQuizIcon.iconType);
const sc = w2s(specialQuizIcon.x * tileSize, specialQuizIcon.y * tileSize);
const sx = sc[0];
const sy = sc[1];
if (!Number.isFinite(sx) || !Number.isFinite(sy)) return;
const size = Math.max(30, tileSize * zDraw * 1.75);
const bob = Math.sin(Date.now() / 320) * size * 0.06;
const cy = sy + bob;
ctx.save();
const r = size * 0.62;
const grad = ctx.createRadialGradient(sx, cy, r * 0.18, sx, cy, r);
grad.addColorStop(0, 'rgba(122, 162, 247, 0.5)');
grad.addColorStop(1, 'rgba(122, 162, 247, 0)');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(sx, cy, r, 0, Math.PI * 2);
ctx.fill();
if (img && img.complete && img.naturalWidth > 0) {
ctx.drawImage(img, sx - size / 2, cy - size / 2, size, size);
} else {
ctx.fillStyle = '#7aa2f7';
ctx.fillRect(sx - size / 2, cy - size / 2, size, size);
}
ctx.fillStyle = '#ffd166';
ctx.font = '700 ' + Math.round(size * 0.42) + 'px system-ui, "Kanit", sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('?', sx + size * 0.4, cy - size * 0.38);
ctx.restore();
}
/** เช็คผู้เล่นเดินชนไอคอน — ใครชนก่อนเป็นคนเปิดคำถามให้ทั้งห้อง (server กันซ้ำ) */
function checkSpecialQuizCollision() {
if (!specialQuizIcon || specialQuizCollideSent || specialQuizActiveQuestion) return;
if (!Number.isFinite(me.x) || !Number.isFinite(me.y)) return;
const dx = me.x - specialQuizIcon.x;
const dy = me.y - specialQuizIcon.y;
if (dx * dx + dy * dy <= 0.85 * 0.85) {
specialQuizCollideSent = true;
try { socket.emit('special-quiz-collide', {}, function () {}); } catch (e) { /* ignore */ }
}
}
function specialQuizOverlayEl() { return document.getElementById('special-quiz-overlay'); }
function openSpecialQuizQuestion(q) {
if (!q) return;
clearSpecialQuizIcon();
specialQuizActiveQuestion = {
index: q.index | 0,
total: q.total | 0,
choices: Array.isArray(q.choices) ? q.choices : [],
endsAt: Number(q.endsAt) || 0,
iconType: q.iconType,
};
specialQuizAnswered = false;
const ov = specialQuizOverlayEl();
if (!ov) return;
const iconEl = document.getElementById('sq-ov-icon');
if (iconEl) iconEl.src = specialQuizIconUrl(q.iconType);
const prog = document.getElementById('sq-ov-progress');
if (prog) prog.textContent = 'ข้อ ' + ((q.index | 0) + 1) + ' / ' + (q.total | 0);
const qEl = document.getElementById('sq-ov-question');
if (qEl) qEl.textContent = q.text || '';
const result = document.getElementById('sq-ov-result');
if (result) { result.classList.add('is-hidden'); result.textContent = ''; result.className = 'sq-ov-result is-hidden'; }
const status = document.getElementById('sq-ov-status');
if (status) status.textContent = 'เลือกคำตอบ — ทุกคนต้องตอบถูกทุกข้อจึงได้การ์ดพิเศษ';
const choicesEl = document.getElementById('sq-ov-choices');
if (choicesEl) {
choicesEl.innerHTML = '';
specialQuizActiveQuestion.choices.forEach(function (c, i) {
const b = document.createElement('button');
b.type = 'button';
b.className = 'sq-ov-choice';
b.dataset.idx = String(i);
const k = document.createElement('span');
k.className = 'sq-ov-choice-key';
k.textContent = SPECIAL_QUIZ_CHOICE_LABELS[i] || String(i + 1);
const t = document.createElement('span');
t.className = 'sq-ov-choice-text';
t.textContent = c;
b.appendChild(k);
b.appendChild(t);
b.onclick = function () { submitSpecialQuizAnswer(i); };
choicesEl.appendChild(b);
});
}
ov.classList.remove('is-hidden');
startSpecialQuizTimerLoop();
}
function submitSpecialQuizAnswer(i) {
if (specialQuizAnswered || !specialQuizActiveQuestion) return;
specialQuizAnswered = true;
const choicesEl = document.getElementById('sq-ov-choices');
if (choicesEl) {
Array.prototype.forEach.call(choicesEl.children, function (b) {
b.classList.toggle('sq-ov-choice--picked', Number(b.dataset.idx) === i);
b.disabled = true;
});
}
const status = document.getElementById('sq-ov-status');
if (status) status.textContent = 'ส่งคำตอบแล้ว — รอผู้เล่นคนอื่น...';
try { socket.emit('special-quiz-answer', { choiceIndex: i }, function () {}); } catch (e) { /* ignore */ }
}
function startSpecialQuizTimerLoop() {
cancelAnimationFrame(specialQuizTimerRaf);
function tick() {
if (!specialQuizActiveQuestion) return;
const timerEl = document.getElementById('sq-ov-timer');
const ms = specialQuizActiveQuestion.endsAt - Date.now();
const s = Math.max(0, Math.ceil(ms / 1000));
if (timerEl) {
timerEl.textContent = String(s);
timerEl.classList.toggle('sq-ov-timer--low', s <= 5);
}
if (ms > 0) specialQuizTimerRaf = requestAnimationFrame(tick);
}
tick();
}
function handleSpecialQuizResult(r) {
if (!r || !specialQuizActiveQuestion) return;
const choicesEl = document.getElementById('sq-ov-choices');
if (choicesEl) {
Array.prototype.forEach.call(choicesEl.children, function (b) {
const idx = Number(b.dataset.idx);
b.disabled = true;
if (idx === r.correctIndex) b.classList.add('sq-ov-choice--correct');
else if (b.classList.contains('sq-ov-choice--picked')) b.classList.add('sq-ov-choice--wrong');
});
}
const result = document.getElementById('sq-ov-result');
if (result) {
result.classList.remove('is-hidden');
result.textContent = r.allCorrect
? 'ทุกคนตอบถูก!'
: (r.timeout ? 'หมดเวลา — ตอบไม่ครบทุกคน' : 'มีคนตอบผิด — ไม่ได้การ์ดพิเศษ');
result.className = 'sq-ov-result ' + (r.allCorrect ? 'sq-ov-result--ok' : 'sq-ov-result--fail');
}
cancelAnimationFrame(specialQuizTimerRaf);
}
function endSpecialQuiz(data) {
cancelAnimationFrame(specialQuizTimerRaf);
specialQuizActiveQuestion = null;
specialQuizAnswered = false;
if (data && data.success && data.card) specialQuizAwardedCard = data.card;
const ov = specialQuizOverlayEl();
const result = document.getElementById('sq-ov-result');
if (result) {
result.classList.remove('is-hidden');
result.textContent = (data && data.success)
? 'ได้รับการ์ดพิเศษ! ดูในสรุปผลภารกิจ'
: 'รอบนี้ไม่ได้การ์ดพิเศษ';
result.className = 'sq-ov-result ' + ((data && data.success) ? 'sq-ov-result--ok' : 'sq-ov-result--fail');
}
const status = document.getElementById('sq-ov-status');
if (status) status.textContent = '';
if (ov) setTimeout(function () { ov.classList.add('is-hidden'); }, 1900);
}
function showGauntletCrownMissionOverlay(mission) {
const ov = document.getElementById('gauntlet-crown-mission-overlay');
const rowEl = document.getElementById('gcm-rank-row');
@@ -11348,6 +11561,7 @@
const bonusEl = document.getElementById('gcm-bonus');
const btn = document.getElementById('btn-gcm-done');
if (!ov || !rowEl || !mission || !Array.isArray(mission.ranked)) return;
if (!mission.specialCardAwarded && specialQuizAwardedCard) mission.specialCardAwarded = specialQuizAwardedCard;
let disp = gauntletCrownBuildDisplayMission(mission);
if (mission.uiSkin === 'jumper') {
disp = jumpSurviveMergePreviewBotsMission(disp) || disp;
@@ -15254,6 +15468,7 @@
quizCarryPlaqueMapScale = Math.max(0.85, Math.min(2.5, joinSc));
}
}
if (res.specialQuizIcon) setSpecialQuizIconFromServer(res.specialQuizIcon);
if (previewMode && editorEmbedReturn && res.mapData && res.mapData.gameType === 'lobby') {
const q = [];
q.push('space=' + encodeURIComponent(spaceId));
@@ -15627,6 +15842,25 @@
}
});
/* ===== Special Quiz ในเกม — socket events ===== */
socket.on('special-quiz-icon', (payload) => {
if (previewMode) return;
setSpecialQuizIconFromServer(payload);
});
socket.on('special-quiz-icon-clear', () => { clearSpecialQuizIcon(); });
socket.on('special-quiz-question', (q) => {
if (previewMode) return;
openSpecialQuizQuestion(q);
});
socket.on('special-quiz-result', (r) => {
if (previewMode) return;
handleSpecialQuizResult(r);
});
socket.on('special-quiz-ended', (data) => {
if (previewMode) return;
endSpecialQuiz(data);
});
socket.on('gauntlet-sync', (data) => {
if (!data || typeof data !== 'object') return;
if (isMegaVirusMissionShellMapPlay()) {
@@ -19005,6 +19239,11 @@
}
});
}
/* ไอคอนคำถามพิเศษในฉาก (วาดทับ map/ผู้เล่น) + ตรวจการเดินชน */
if (specialQuizIcon) {
drawSpecialQuizIconWorld(worldToScreen, zDraw);
checkSpecialQuizCollision();
}
if (isFrogger()) {
ctx.fillStyle = '#7aa2f7';
ctx.font = 'bold 14px sans-serif';
+61 -1
View File
@@ -3531,6 +3531,19 @@
object-fit: contain;
filter: drop-shadow(0 0 10px rgba(91, 255, 141, 0.45));
}
/* การ์ดพิเศษจาก trycss/10-Game-1-result (mockup .bail-card) */
#gauntlet-crown-mission-overlay.gcm-quiz-tf-mock .gcm-bonus img.bail-card,
#gauntlet-crown-mission-overlay.gcm-quiz-tf-mock .gcm-bonus img.gcm-special-card-img {
width: auto !important;
height: 258px !important;
max-width: 152px !important;
max-height: none !important;
filter: drop-shadow(0 0 14px rgba(120, 220, 255, 0.55));
}
#gauntlet-crown-mission-overlay.gcm-quiz-tf-mock .gcm-bonus .gcm-special-card-preset {
display: block;
margin-top: 8px;
}
#gauntlet-crown-mission-overlay.gcm-quiz-tf-mock .gcm-plaque {
display: none !important;
}
@@ -3857,10 +3870,57 @@
</div>
</div>
</div>
<style id="special-quiz-style">
#special-quiz-overlay { position: fixed; inset: 0; z-index: 1200; display: flex; align-items: center; justify-content: center; padding: 24px; }
#special-quiz-overlay.is-hidden { display: none; }
#special-quiz-overlay .sq-ov-backdrop { position: absolute; inset: 0; background: radial-gradient(ellipse at center, rgba(16,22,46,0.82), rgba(8,10,22,0.94)); backdrop-filter: blur(3px); }
#special-quiz-overlay .sq-ov-panel { position: relative; width: min(720px, 94vw); max-height: 92vh; overflow: auto; background: linear-gradient(180deg, #141a33, #0e1226); border: 1px solid rgba(122,162,247,0.45); border-radius: 18px; box-shadow: 0 24px 70px rgba(0,0,0,0.55), 0 0 0 4px rgba(122,162,247,0.08); padding: 22px 24px 24px; color: #e7ecff; }
#special-quiz-overlay .sq-ov-head { display: flex; align-items: center; gap: 14px; }
#special-quiz-overlay .sq-ov-icon { width: 56px; height: 56px; object-fit: contain; filter: drop-shadow(0 4px 10px rgba(122,162,247,0.5)); }
#special-quiz-overlay .sq-ov-head-text { flex: 1; min-width: 0; }
#special-quiz-overlay .sq-ov-kicker { font-size: 13px; letter-spacing: .08em; text-transform: uppercase; color: #7aa2f7; font-weight: 700; }
#special-quiz-overlay .sq-ov-progress { font-size: 15px; color: #c0caf5; margin-top: 2px; }
#special-quiz-overlay .sq-ov-timer { flex: none; width: 54px; height: 54px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 22px; font-weight: 800; color: #9ece6a; border: 3px solid rgba(158,206,106,0.55); background: rgba(158,206,106,0.08); }
#special-quiz-overlay .sq-ov-timer.sq-ov-timer--low { color: #f7768e; border-color: rgba(247,118,142,0.6); background: rgba(247,118,142,0.1); }
#special-quiz-overlay .sq-ov-question { margin: 18px 2px 16px; font-size: 21px; line-height: 1.45; font-weight: 700; }
#special-quiz-overlay .sq-ov-choices { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
@media (max-width: 560px) { #special-quiz-overlay .sq-ov-choices { grid-template-columns: 1fr; } }
#special-quiz-overlay .sq-ov-choice { display: flex; align-items: center; gap: 12px; text-align: left; padding: 14px 16px; border-radius: 12px; border: 1px solid rgba(122,162,247,0.35); background: rgba(122,162,247,0.07); color: #e7ecff; font-size: 17px; cursor: pointer; transition: transform .08s, background .12s, border-color .12s; }
#special-quiz-overlay .sq-ov-choice:hover:not(:disabled) { background: rgba(122,162,247,0.16); transform: translateY(-1px); }
#special-quiz-overlay .sq-ov-choice:disabled { cursor: default; opacity: .92; }
#special-quiz-overlay .sq-ov-choice-key { flex: none; width: 30px; height: 30px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-weight: 800; background: rgba(122,162,247,0.25); color: #cdd6ff; }
#special-quiz-overlay .sq-ov-choice-text { flex: 1; }
#special-quiz-overlay .sq-ov-choice--picked { border-color: #7aa2f7; background: rgba(122,162,247,0.28); }
#special-quiz-overlay .sq-ov-choice--correct { border-color: #9ece6a; background: rgba(158,206,106,0.22); }
#special-quiz-overlay .sq-ov-choice--correct .sq-ov-choice-key { background: rgba(158,206,106,0.5); color: #0e1226; }
#special-quiz-overlay .sq-ov-choice--wrong { border-color: #f7768e; background: rgba(247,118,142,0.18); }
#special-quiz-overlay .sq-ov-status { margin-top: 14px; font-size: 14px; color: #a9b1d6; min-height: 18px; }
#special-quiz-overlay .sq-ov-result { margin-top: 12px; font-size: 18px; font-weight: 800; text-align: center; padding: 10px; border-radius: 10px; }
#special-quiz-overlay .sq-ov-result.is-hidden { display: none; }
#special-quiz-overlay .sq-ov-result--ok { color: #0e1226; background: #9ece6a; }
#special-quiz-overlay .sq-ov-result--fail { color: #fff; background: rgba(247,118,142,0.9); }
</style>
<div id="special-quiz-overlay" class="is-hidden" role="dialog" aria-modal="true" aria-labelledby="sq-overlay-title">
<div class="sq-ov-backdrop" aria-hidden="true"></div>
<div class="sq-ov-panel">
<div class="sq-ov-head">
<img id="sq-ov-icon" class="sq-ov-icon" src="/Game/img/special-quiz/icon-lawyer.png" alt="" />
<div class="sq-ov-head-text">
<div id="sq-overlay-title" class="sq-ov-kicker">คำถามพิเศษ · Special Quiz</div>
<div id="sq-ov-progress" class="sq-ov-progress">ข้อ 1 / 1</div>
</div>
<div id="sq-ov-timer" class="sq-ov-timer">20</div>
</div>
<div id="sq-ov-question" class="sq-ov-question"></div>
<div id="sq-ov-choices" class="sq-ov-choices"></div>
<div id="sq-ov-status" class="sq-ov-status"></div>
<div id="sq-ov-result" class="sq-ov-result is-hidden"></div>
</div>
</div>
<script src="/app-base.js?v=2"></script>
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0306"></script>
<script src="js/play.js?v=0.0500"></script>
<script src="js/play.js?v=0.0502"></script>
<div class="version-tag">v —</div>
</body>
</html>
+332 -1
View File
@@ -287,6 +287,19 @@ const defaultMap = () => ({
const SPECIAL_QUIZ_LEVELS = [1, 2, 3, 4, 5];
const SPECIAL_QUIZ_CASES = [1, 2, 3];
const SPECIAL_QUIZ_CARD_RARITIES = ['common', 'rare', 'legendary'];
const SPECIAL_CARD_ASSET_BASE = '/Game/img/special-cards/';
function specialCardTxtFilename(id) {
return id === 7 ? 'card--7txt.png' : `card-${id}-txt.png`;
}
function specialCardImageUrl(id) {
return `${SPECIAL_CARD_ASSET_BASE}card-${id}.png`;
}
function specialCardTxtImageUrl(id) {
return `${SPECIAL_CARD_ASSET_BASE}${specialCardTxtFilename(id)}`;
}
function specialQuizSetKey(level, caseId) {
return 'L' + level + '_C' + caseId;
@@ -318,11 +331,28 @@ function sanitizeSpecialQuizQuestion(q) {
function sanitizeSpecialCard(c) {
const src = (c && typeof c === 'object') ? c : {};
const rarity = SPECIAL_QUIZ_CARD_RARITIES.indexOf(src.rarity) >= 0 ? src.rarity : 'rare';
let cardId = Number(src.cardId);
if (!(cardId >= 1 && cardId <= 7)) cardId = null;
let imageUrl = String(src.imageUrl == null ? '' : src.imageUrl).trim().slice(0, 600);
let txtImageUrl = String(src.txtImageUrl == null ? '' : src.txtImageUrl).trim().slice(0, 600);
if (cardId) {
if (!imageUrl) imageUrl = specialCardImageUrl(cardId);
if (!txtImageUrl) txtImageUrl = specialCardTxtImageUrl(cardId);
} else if (imageUrl) {
const m = imageUrl.match(/\/card-(\d)\.png/i);
if (m) {
cardId = Number(m[1]);
imageUrl = specialCardImageUrl(cardId);
if (!txtImageUrl) txtImageUrl = specialCardTxtImageUrl(cardId);
}
}
return {
cardId: cardId || null,
th: String(src.th == null ? '' : src.th).trim().slice(0, 160),
en: String(src.en == null ? '' : src.en).trim().slice(0, 160),
rarity,
imageUrl: String(src.imageUrl == null ? '' : src.imageUrl).trim().slice(0, 600),
imageUrl,
txtImageUrl,
};
}
@@ -349,6 +379,263 @@ function getSpecialQuizSet(settings, level, caseId) {
return sets[specialQuizSetKey(L, C)] || sets[specialQuizSetKey(1, 1)] || { questions: [], specialCard: sanitizeSpecialCard({}) };
}
/* ===== Special Quiz ในเกม — ไอคอนสุ่มในฉาก + เซสชันถาม-ตอบ multiplayer (ทุกคนต้องตอบถูก) ===== */
/** เฉพาะ 4 มินิเกมนี้ (ตอนสืบสวน) ที่ไอคอนพิเศษจะสุ่มโผล่ */
const SPECIAL_QUIZ_ELIGIBLE_MAP_IDS = ['mng8a80o', 'mnorwqx1', 'mnptfts2', 'mnpz6rkp'];
const SPECIAL_QUIZ_ICON_TYPES = ['lawyer', 'police'];
/** โอกาสโผล่ = 1 ใน 3 ของการเล่นแต่ละรอบ */
const SPECIAL_QUIZ_SPAWN_CHANCE = 1 / 3;
/** เวลาให้ตอบต่อข้อ (มิลลิวิ) — ค่าคงที่เริ่มต้น (ย้ายไป Admin ได้ภายหลัง) */
const SPECIAL_QUIZ_ANSWER_MS = 20000;
/** หน่วงโชว์เฉลยก่อนข้อต่อไป */
const SPECIAL_QUIZ_REVEAL_MS = 2600;
/** แปลงคำถาม 1 ข้อให้อยู่ในรูป {text, choices[], correctIndex} เสมอ (tf -> จริง/เท็จ) */
function specialQuizNormalizeQuestion(q) {
if (!q || typeof q !== 'object') return null;
const text = String(q.text == null ? '' : q.text).trim();
if (!text) return null;
if (q.type === 'mcq') {
const choices = (Array.isArray(q.choices) ? q.choices : []).map((c) => String(c == null ? '' : c)).filter((c) => c.trim() !== '');
if (choices.length < 2) return null;
let ci = Number(q.correctIndex);
if (!Number.isInteger(ci) || ci < 0 || ci >= choices.length) ci = 0;
return { text, choices, correctIndex: ci };
}
return { text, choices: ['จริง', 'เท็จ'], correctIndex: q.answerTrue ? 0 : 1 };
}
function specialQuizShuffle(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;
}
function specialQuizTileWalkable(md, x, y) {
const w = md.width || 20;
const h = md.height || 15;
if (x < 0 || x >= w || y < 0 || y >= h) return false;
const row = md.objects && md.objects[y];
return !!(row && row[x] !== 1);
}
/** หลีกเลี่ยงโซน gameplay (คำตอบจริง/ผิด, hub, ตัวเลือกหยิบมาวาง) เพื่อไม่ชนกลไกเกม */
function specialQuizTileInGameZone(md, x, y) {
if (quizCellOn(md.quizTrueArea || [], x, y)) return true;
if (quizCellOn(md.quizFalseArea || [], x, y)) return true;
if (quizCellOn(md.quizCarryHubArea || [], x, y)) return true;
const oa = md.quizCarryOptionArea;
if (oa && oa[y] && oa[y][x]) return true;
return false;
}
/** หา tile ที่เดินถึงได้จาก spawn (BFS) และไม่อยู่ในโซน gameplay — คืนพิกัดกึ่งกลาง tile */
function pickSpecialQuizSpawnTile(md) {
const sp = md.spawn || { x: 1, y: 1 };
const sx = Math.floor(Number(sp.x));
const sy = Math.floor(Number(sp.y));
if (!specialQuizTileWalkable(md, sx, sy)) return null;
const q = [[sx, sy]];
const seen = new Set([sx + ',' + sy]);
const dirs = [[0, 1], [0, -1], [1, 0], [-1, 0]];
const cands = [];
while (q.length) {
const [cx, cy] = q.shift();
for (let di = 0; di < dirs.length; di++) {
const nx = cx + dirs[di][0];
const ny = cy + dirs[di][1];
const k = nx + ',' + ny;
if (seen.has(k)) continue;
if (!specialQuizTileWalkable(md, nx, ny)) continue;
seen.add(k);
q.push([nx, ny]);
if (!specialQuizTileInGameZone(md, nx, ny)) {
const dist = Math.abs(nx - sx) + Math.abs(ny - sy);
if (dist >= 2) cands.push({ x: nx, y: ny });
}
}
}
if (!cands.length) return null;
const pick = cands[Math.floor(Math.random() * cands.length)];
return { x: pick.x + 0.5, y: pick.y + 0.5 };
}
function clearSpecialQuizTimers(space) {
if (Array.isArray(space.specialQuizTimers)) {
space.specialQuizTimers.forEach((t) => clearTimeout(t));
}
space.specialQuizTimers = [];
}
/** payload ไอคอนสำหรับ client (null = ไม่มี/ถูกทริกแล้ว) */
function specialQuizIconPayload(space) {
const sq = space && space.specialQuiz;
if (!sq || sq.triggered || sq.done) return null;
return { iconType: sq.iconType, x: sq.x, y: sq.y };
}
/** เริ่มมินิเกมสืบสวน: สุ่ม 1/3 ว่ารอบนี้จะมีไอคอนพิเศษไหม + เลือกตำแหน่ง */
function maybeSpawnSpecialQuizForRun(space) {
clearSpecialQuizTimers(space);
space.specialQuiz = null;
space.specialCardAwardedThisRun = null;
if (!space || !space.detectiveMinigameActive) return;
const mapId = String(space.mapId || '').trim();
if (SPECIAL_QUIZ_ELIGIBLE_MAP_IDS.indexOf(mapId) < 0) return;
const level = Number(space.detectiveLobbyLevel) || 1;
const caseId = Number(space.detectiveLobbyCaseId) || 1;
const settings = loadQuizSettings();
const set = getSpecialQuizSet(settings, level, caseId);
const questions = (set && Array.isArray(set.questions) ? set.questions : [])
.map(specialQuizNormalizeQuestion)
.filter(Boolean);
const card = set && set.specialCard;
const hasCard = !!(card && (card.cardId || card.imageUrl));
if (!questions.length || !hasCard) return;
if (Math.random() > SPECIAL_QUIZ_SPAWN_CHANCE) return;
const md = (space.mapId && maps.get(space.mapId)) || space.mapData;
if (!md) return;
const tile = pickSpecialQuizSpawnTile(md);
if (!tile) return;
const iconType = SPECIAL_QUIZ_ICON_TYPES[Math.floor(Math.random() * SPECIAL_QUIZ_ICON_TYPES.length)];
space.specialQuiz = {
iconType,
x: tile.x,
y: tile.y,
level,
caseId,
card,
triggered: false,
done: false,
session: null,
};
}
function emitSpecialQuizIcon(sid, space) {
const payload = specialQuizIconPayload(space);
if (payload) io.to(sid).emit('special-quiz-icon', payload);
}
/** รายชื่อผู้เล่นจริงที่ยังอยู่ในห้องตอนนี้ (ใช้ตัดสิน "ทุกคนตอบถูก") */
function specialQuizActiveParticipants(space) {
return [...space.peers.keys()];
}
function sendSpecialQuizQuestion(sid, space) {
const sq = space.specialQuiz;
const sess = sq && sq.session;
if (!sess) return;
const q = sess.questions[sess.qIndex];
if (!q) { endSpecialQuizSession(sid, space, false); return; }
sess.answers = {};
sess.phaseEndsAt = Date.now() + SPECIAL_QUIZ_ANSWER_MS;
io.to(sid).emit('special-quiz-question', {
index: sess.qIndex,
total: sess.questions.length,
text: q.text,
choices: q.choices,
endsAt: sess.phaseEndsAt,
iconType: sq.iconType,
});
const t = setTimeout(() => {
const spNow = spaces.get(sid);
if (!spNow || spNow.specialQuiz !== sq || sq.session !== sess) return;
resolveSpecialQuizQuestion(sid, spNow, true);
}, SPECIAL_QUIZ_ANSWER_MS + 80);
space.specialQuizTimers.push(t);
}
/** ตัดสินข้อปัจจุบัน: ต้องตอบครบทุกคน และถูกทุกคน จึงผ่าน */
function resolveSpecialQuizQuestion(sid, space, fromTimeout) {
const sq = space.specialQuiz;
const sess = sq && sq.session;
if (!sess || sess.resolving) return;
sess.resolving = true;
clearSpecialQuizTimers(space);
const q = sess.questions[sess.qIndex];
const participants = specialQuizActiveParticipants(space);
let allCorrect = participants.length > 0;
participants.forEach((pid) => {
const a = sess.answers[pid];
if (!(Number.isInteger(a) && a === q.correctIndex)) allCorrect = false;
});
io.to(sid).emit('special-quiz-result', {
index: sess.qIndex,
correctIndex: q.correctIndex,
answers: { ...sess.answers },
allCorrect,
timeout: !!fromTimeout,
});
if (!allCorrect) {
const t = setTimeout(() => {
const spNow = spaces.get(sid);
if (!spNow || spNow.specialQuiz !== sq) return;
endSpecialQuizSession(sid, spNow, false);
}, SPECIAL_QUIZ_REVEAL_MS);
space.specialQuizTimers.push(t);
return;
}
sess.qIndex++;
if (sess.qIndex >= sess.questions.length) {
const t = setTimeout(() => {
const spNow = spaces.get(sid);
if (!spNow || spNow.specialQuiz !== sq) return;
endSpecialQuizSession(sid, spNow, true);
}, SPECIAL_QUIZ_REVEAL_MS);
space.specialQuizTimers.push(t);
return;
}
const t = setTimeout(() => {
const spNow = spaces.get(sid);
if (!spNow || spNow.specialQuiz !== sq || sq.session !== sess) return;
sess.resolving = false;
sendSpecialQuizQuestion(sid, spNow);
}, SPECIAL_QUIZ_REVEAL_MS);
space.specialQuizTimers.push(t);
}
function endSpecialQuizSession(sid, space, success) {
const sq = space.specialQuiz;
if (!sq) return;
clearSpecialQuizTimers(space);
sq.done = true;
sq.session = null;
let cardOut = null;
if (success && sq.card && (sq.card.cardId || sq.card.imageUrl)) {
space.specialCardAwardedThisRun = sq.card;
cardOut = sq.card;
}
io.to(sid).emit('special-quiz-ended', { success: !!success, card: cardOut });
}
function startSpecialQuizSession(sid, space) {
const sq = space.specialQuiz;
if (!sq || sq.triggered || sq.done) return false;
const settings = loadQuizSettings();
const set = getSpecialQuizSet(settings, sq.level, sq.caseId);
const all = (set && Array.isArray(set.questions) ? set.questions : [])
.map(specialQuizNormalizeQuestion)
.filter(Boolean);
if (!all.length) return false;
const cap = clampQuizRoundQuestionCount(settings.quizRoundQuestionCount, 10);
const picked = specialQuizShuffle(all).slice(0, Math.max(1, Math.min(cap, all.length)));
sq.triggered = true;
sq.session = {
questions: picked,
qIndex: 0,
answers: {},
phaseEndsAt: 0,
resolving: false,
};
clearSpecialQuizTimers(space);
io.to(sid).emit('special-quiz-icon-clear', {});
sendSpecialQuizQuestion(sid, space);
return true;
}
function defaultQuizSettings() {
return {
readMs: 10000,
@@ -2013,6 +2300,9 @@ function beginDetectiveSuspectMinigame(sid, space, cardEntry, selectedIndex) {
space.quizSession = null;
space.mapId = mapId;
space.mapData = md;
/* สุ่มไอคอนคำถามพิเศษสำหรับรอบนี้ (1 ใน 3) — ส่งจริงผ่าน joinCb เมื่อ client เข้า play.html */
maybeSpawnSpecialQuizForRun(space);
emitSpecialQuizIcon(sid, space);
let si = 0;
space.peers.forEach((p) => {
@@ -2098,6 +2388,8 @@ function beginDetectiveSuspectMinigame(sid, space, cardEntry, selectedIndex) {
function returnDetectiveSpaceToLobbyB(sid, space, message, awardOptions) {
ensurePostCaseLobbyMapLoaded();
clearSpaceQuizTimers(space);
clearSpecialQuizTimers(space);
space.specialQuiz = null;
if (space.quizSession) space.quizSession.active = false;
space.quizSession = null;
space.detectiveMinigameActive = false;
@@ -4877,6 +5169,9 @@ io.on('connection', (socket) => {
if ((isBalloonBossMissionShellSpace(space) || isQuizQuestionMissionShellSpace(space) || isStackTowerMissionShellSpace(space) || isJumpSurviveMissionShellSpace(space) || isSpaceShooterMissionShellSpace(space)) && space.gauntletRun) {
joinCb.gauntletCrownRunHeld = !!space.gauntletRun.crownRunHeld;
}
/* ไอคอนคำถามพิเศษของรอบนี้ (ถ้ามีและยังไม่ถูกทริก) */
const sqIcon = specialQuizIconPayload(space);
if (sqIcon) joinCb.specialQuizIcon = sqIcon;
if (typeof cb === 'function') cb(joinCb);
emitLobbyTintSync(spaceId, space);
socket.to(spaceId).emit('user-joined', peer);
@@ -4887,6 +5182,40 @@ io.on('connection', (socket) => {
}
});
/** ผู้เล่นเดินชนไอคอนคำถามพิเศษ — ใครชนก่อนเป็นคนเปิดเซสชันให้ทั้งห้อง */
socket.on('special-quiz-collide', (_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 });
const sq = space.specialQuiz;
if (!sq || sq.triggered || sq.done) return reply({ ok: false });
const started = startSpecialQuizSession(sid, space);
reply({ ok: !!started });
});
/** ผู้เล่นส่งคำตอบของข้อปัจจุบัน */
socket.on('special-quiz-answer', (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 });
const sess = space.specialQuiz && space.specialQuiz.session;
if (!sess || sess.resolving) return reply({ ok: false });
const choice = Number(data && data.choiceIndex);
const q = sess.questions[sess.qIndex];
if (!q || !Number.isInteger(choice) || choice < 0 || choice >= q.choices.length) {
return reply({ ok: false });
}
if (Number.isInteger(sess.answers[socket.id])) return reply({ ok: false, error: 'ตอบไปแล้ว' });
sess.answers[socket.id] = choice;
reply({ ok: true });
/* ตอบครบทุกคนแล้วตัดสินทันที ไม่ต้องรอหมดเวลา */
const participants = specialQuizActiveParticipants(space);
const allAnswered = participants.length > 0 && participants.every((pid) => Number.isInteger(sess.answers[pid]));
if (allAnswered) resolveSpecialQuizQuestion(sid, space, false);
});
/** เปลี่ยนสีเสื้อ/ผิวใน lobby — ห้ามเลือกสีที่คนอื่นใช้แล้ว */
socket.on('lobby-tint-update', (data, cb) => {
const reply = typeof cb === 'function' ? cb : () => {};
@@ -5345,6 +5674,8 @@ io.on('connection', (socket) => {
space.troublesomeResponseReceived = false;
space.suspectPhaseActive = false;
space.suspectPickIndex = 0;
space.detectiveLobbyLevel = Number(lobbyLevel) || 1;
space.detectiveLobbyCaseId = Number(caseId) || 1;
if (space.troublesomeEligible) space.troublesomeEligible.clear();
else space.troublesomeEligible = new Set();
if (space.troublesomeDebTimer) clearTimeout(space.troublesomeDebTimer);