update special cards flow
This commit is contained in:
@@ -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
@@ -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() : '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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';
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user