โชว์การ์ดพิเศษที่ได้ + ปุ่มไปต่อ
@@ -2758,6 +2758,36 @@ code {
|
||||
}
|
||||
|
||||
/* ===== Evidence Cards admin panel ===== */
|
||||
.cm-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.cm-pick {
|
||||
border: 1px solid rgba(124, 154, 255, 0.25);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
background: rgba(20, 26, 48, 0.4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.cm-pick-label {
|
||||
font-weight: 800;
|
||||
font-size: 0.92rem;
|
||||
color: #cdd6ff;
|
||||
}
|
||||
.cm-pick-prev {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
object-fit: contain;
|
||||
background: #0a0e1a;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(124, 154, 255, 0.18);
|
||||
}
|
||||
.cm-pick select { width: 100%; }
|
||||
|
||||
.ev-suspect-block {
|
||||
border: 1px solid rgba(124, 154, 255, 0.25);
|
||||
border-radius: 12px;
|
||||
|
||||
@@ -4184,6 +4184,144 @@
|
||||
if (saveBtn) saveBtn.addEventListener('click', saveEvidenceCardsPanel);
|
||||
})();
|
||||
|
||||
/* ===== รูปคดี & Cutscene (LobbyA → LobbyB) ===== */
|
||||
var CASE_MEDIA_API = '/Game/api/case-media';
|
||||
var CASE_MEDIA_IMAGES_API = '/Game/api/case-media-images';
|
||||
var CASE_MEDIA_COUNT = 15;
|
||||
var caseMediaData = null;
|
||||
var caseMediaImages = null; // { case:[...], cutscene:[...] }
|
||||
var caseMediaCurrent = '1';
|
||||
|
||||
function caseMediaPopulateSelect() {
|
||||
var sel = el('case-media-case');
|
||||
if (!sel || sel.options.length) return;
|
||||
for (var i = 1; i <= CASE_MEDIA_COUNT; i++) {
|
||||
var o = document.createElement('option');
|
||||
o.value = String(i); o.textContent = 'คดี ' + i;
|
||||
sel.appendChild(o);
|
||||
}
|
||||
}
|
||||
|
||||
function loadCaseMediaPanel() {
|
||||
caseMediaPopulateSelect();
|
||||
if (caseMediaImages == null) {
|
||||
fetch(CASE_MEDIA_IMAGES_API + '?_=' + Date.now(), { credentials: 'include', cache: 'no-store' })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (j) { caseMediaImages = (j && j.images) || { case: [], cutscene: [] }; renderCaseMedia(caseMediaCurrent); })
|
||||
.catch(function () { caseMediaImages = { case: [], cutscene: [] }; });
|
||||
}
|
||||
fetch(CASE_MEDIA_API + '?_=' + Date.now(), { credentials: 'include', cache: 'no-store' })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (j) {
|
||||
caseMediaData = (j && j.cases) ? j : { cases: {} };
|
||||
var sel = el('case-media-case');
|
||||
if (sel) caseMediaCurrent = sel.value || '1';
|
||||
renderCaseMedia(caseMediaCurrent);
|
||||
})
|
||||
.catch(function (e) {
|
||||
setMsg('case-media-msg', (e.message || 'โหลดไม่ได้') + ' — เช็คว่า Node รัน Game/server.js และ /Game/api/case-media ไม่ 404', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function caseMediaGetCase(cid) {
|
||||
if (!caseMediaData) caseMediaData = { cases: {} };
|
||||
if (!caseMediaData.cases) caseMediaData.cases = {};
|
||||
var n = parseInt(cid, 10) || 1;
|
||||
if (!caseMediaData.cases[cid]) caseMediaData.cases[cid] = {};
|
||||
var c = caseMediaData.cases[cid];
|
||||
if (typeof c.name !== 'string') c.name = 'คดีที่ ' + n;
|
||||
if (typeof c.art !== 'string') c.art = '/Main-Lobby/IMAGE/case/case-' + n + '.png';
|
||||
if (typeof c.detail !== 'string') c.detail = '/Main-Lobby/IMAGE/case/case-detail-' + n + '.png';
|
||||
if (!Array.isArray(c.story)) c.story = [];
|
||||
if (typeof c.story[0] !== 'string') c.story[0] = '/Main-Lobby/IMAGE/cutscene/case-' + n + '-story-1.png';
|
||||
if (typeof c.story[1] !== 'string') c.story[1] = '/Main-Lobby/IMAGE/cutscene/case-' + n + '-story-2.png';
|
||||
return c;
|
||||
}
|
||||
|
||||
/** ตัวเลือกรูป (dropdown + พรีวิว) — kind = 'case' | 'cutscene' */
|
||||
function caseMediaImagePicker(label, kind, current, onChange) {
|
||||
var wrap = document.createElement('div');
|
||||
wrap.className = 'cm-pick';
|
||||
var lab = document.createElement('div'); lab.className = 'cm-pick-label'; lab.textContent = label;
|
||||
var prev = document.createElement('img'); prev.className = 'cm-pick-prev'; prev.alt = ''; prev.src = current;
|
||||
prev.onerror = function () { this.style.opacity = '0.25'; };
|
||||
var sel = document.createElement('select'); sel.className = 'admin-inp-num';
|
||||
var list = (caseMediaImages && caseMediaImages[kind]) ? caseMediaImages[kind].slice() : [];
|
||||
if (current && list.indexOf(current) < 0) list.unshift(current);
|
||||
list.forEach(function (u) {
|
||||
var o = document.createElement('option');
|
||||
o.value = u; o.textContent = u.split('/').pop();
|
||||
if (u === current) o.selected = true;
|
||||
sel.appendChild(o);
|
||||
});
|
||||
sel.addEventListener('change', function () { prev.src = sel.value; onChange(sel.value); });
|
||||
wrap.appendChild(lab); wrap.appendChild(prev); wrap.appendChild(sel);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function renderCaseMedia(cid) {
|
||||
caseMediaCurrent = cid;
|
||||
var tag = el('case-media-set-tag');
|
||||
if (tag) tag.textContent = 'กำลังแก้: คดี ' + cid;
|
||||
var root = el('case-media-root');
|
||||
if (!root) return;
|
||||
var c = caseMediaGetCase(cid);
|
||||
root.innerHTML = '';
|
||||
|
||||
var nameLabel = document.createElement('label');
|
||||
nameLabel.className = 'admin-field';
|
||||
nameLabel.textContent = 'ชื่อคดี';
|
||||
var nameInp = document.createElement('input');
|
||||
nameInp.type = 'text'; nameInp.maxLength = 80; nameInp.value = c.name || '';
|
||||
nameInp.addEventListener('input', function () { c.name = nameInp.value; });
|
||||
nameLabel.appendChild(nameInp);
|
||||
root.appendChild(nameLabel);
|
||||
|
||||
var grid = document.createElement('div');
|
||||
grid.className = 'cm-grid';
|
||||
grid.appendChild(caseMediaImagePicker('รูปเลือกคดี (การ์ด)', 'case', c.art, function (v) { c.art = v; }));
|
||||
grid.appendChild(caseMediaImagePicker('รูปรายละเอียดคดี', 'case', c.detail, function (v) { c.detail = v; }));
|
||||
grid.appendChild(caseMediaImagePicker('Cutscene รูปที่ 1', 'cutscene', c.story[0], function (v) { c.story[0] = v; }));
|
||||
grid.appendChild(caseMediaImagePicker('Cutscene รูปที่ 2', 'cutscene', c.story[1], function (v) { c.story[1] = v; }));
|
||||
root.appendChild(grid);
|
||||
}
|
||||
|
||||
function saveCaseMediaPanel() {
|
||||
if (!caseMediaData) return;
|
||||
var btn = el('btn-case-media-save');
|
||||
if (btn) btn.disabled = true;
|
||||
setMsg('case-media-msg', 'กำลังบันทึก…', '');
|
||||
fetch(CASE_MEDIA_API, {
|
||||
method: 'PUT',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(caseMediaData),
|
||||
}).then(function (r) {
|
||||
return r.text().then(function (t) {
|
||||
var j = {};
|
||||
try { j = t ? JSON.parse(t) : {}; } catch (e) { /* ignore */ }
|
||||
if (!r.ok || !j.ok) throw new Error((j && j.error) || ('HTTP ' + r.status));
|
||||
return j;
|
||||
});
|
||||
}).then(function (j) {
|
||||
if (j && j.cases) caseMediaData = { cases: j.cases };
|
||||
renderCaseMedia(caseMediaCurrent);
|
||||
setMsg('case-media-msg', 'บันทึกแล้ว — มีผลในเกมเมื่อรีเฟรช LobbyA/LobbyB', 'ok');
|
||||
}).catch(function (e) {
|
||||
setMsg('case-media-msg', e.message || 'บันทึกไม่ได้', 'error');
|
||||
}).then(function () {
|
||||
if (btn) btn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
(function bindCaseMediaPanel() {
|
||||
caseMediaPopulateSelect();
|
||||
var caseSel = el('case-media-case');
|
||||
if (caseSel) caseSel.addEventListener('change', function () { renderCaseMedia(caseSel.value || '1'); });
|
||||
var saveBtn = el('btn-case-media-save');
|
||||
if (saveBtn) saveBtn.addEventListener('click', saveCaseMediaPanel);
|
||||
})();
|
||||
|
||||
function setTab(name) {
|
||||
document.querySelectorAll('.tab').forEach(function (t) {
|
||||
var on = t.getAttribute('data-tab') === name;
|
||||
@@ -4212,6 +4350,7 @@
|
||||
if (name === 'quiz') loadQuizSettingsPanel();
|
||||
if (name === 'special-quiz') loadSpecialQuizPanel();
|
||||
if (name === 'evidence-cards') loadEvidenceCardsPanel();
|
||||
if (name === 'case-media') loadCaseMediaPanel();
|
||||
if (name === 'quiz-carry') loadQuizCarryPanel();
|
||||
if (name === 'quiz-battle') loadQbBattlePanel();
|
||||
if (name === 'jump-survive') loadJumpSurviveTimingPanel();
|
||||
|
||||
@@ -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=36">
|
||||
<link rel="stylesheet" href="admin.css?v=37">
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#admin-main">ข้ามไปเนื้อหา</a>
|
||||
@@ -84,6 +84,7 @@
|
||||
<div class="admin-tab-sep" aria-hidden="true"><span>คำถาม & การ์ดพิเศษ</span></div>
|
||||
<button type="button" class="tab" data-tab="special-quiz" role="tab" id="tab-special-quiz" aria-controls="tab-panel-special-quiz"><span class="tab-label">ชุดคำถามพิเศษ</span><span class="tab-desc">15 ชุด (Level × Case) · การ์ดรางวัล</span></button>
|
||||
<button type="button" class="tab" data-tab="evidence-cards" role="tab" id="tab-evidence-cards" aria-controls="tab-panel-evidence-cards"><span class="tab-label">การ์ดหลักฐาน</span><span class="tab-desc">3 คดี × ผู้ต้องสงสัย × 3 ใบ · รูป/ข้อความ</span></button>
|
||||
<button type="button" class="tab" data-tab="case-media" role="tab" id="tab-case-media" aria-controls="tab-panel-case-media"><span class="tab-label">รูปคดี & Cutscene</span><span class="tab-desc">15 คดี · รูปเลือกคดี/รายละเอียด/เรื่องราว 2 รูป</span></button>
|
||||
|
||||
<div class="admin-tab-sep" aria-hidden="true"><span>ระบบ & บัญชี</span></div>
|
||||
<button type="button" class="tab" data-tab="characters" role="tab" id="tab-characters" aria-controls="tab-panel-characters"><span class="tab-label">ตัวละคร</span><span class="tab-desc">อัปโหลด · เลือกใช้ในเกม</span></button>
|
||||
@@ -332,6 +333,28 @@
|
||||
<p id="evidence-cards-msg" class="msg" role="status"></p>
|
||||
</section>
|
||||
|
||||
<section id="tab-panel-case-media" class="tab-panel card" hidden role="tabpanel" aria-labelledby="tab-case-media">
|
||||
<h2 class="sq-title">รูปคดี & Cutscene — LobbyA → LobbyB</h2>
|
||||
<p class="muted">มี <strong>15 คดี</strong> · แต่ละคดีตั้งได้: <strong>ชื่อคดี</strong>, <strong>รูปการ์ดเลือกคดี</strong>, <strong>รูปรายละเอียดคดี</strong>, และ <strong>Cutscene 2 รูป</strong> (โชว์ก่อนเข้า LobbyB · ต้องกด «ไปต่อ») · เลือกรูปจากที่อัปโหลดไว้ใน <code>/Main-Lobby/IMAGE/case</code> และ <code>/Main-Lobby/IMAGE/cutscene</code> · เก็บที่ <code>/Game/data/case-media.json</code></p>
|
||||
|
||||
<fieldset class="sq-fieldset sq-pick">
|
||||
<legend>เลือกคดีที่จะแก้</legend>
|
||||
<div class="sq-pick-row">
|
||||
<label class="admin-field">คดี (Case)
|
||||
<select id="case-media-case" class="admin-inp-num sq-select"></select>
|
||||
</label>
|
||||
<span id="case-media-set-tag" class="sq-set-tag">กำลังแก้: คดี 1</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div id="case-media-root"></div>
|
||||
|
||||
<div class="quiz-admin-actions">
|
||||
<button type="button" class="btn btn-primary" id="btn-case-media-save">บันทึกรูปคดีทั้งหมด (15 คดี)</button>
|
||||
</div>
|
||||
<p id="case-media-msg" class="msg" role="status"></p>
|
||||
</section>
|
||||
|
||||
<section id="tab-panel-quiz-carry" class="tab-panel card" hidden role="tabpanel" aria-labelledby="tab-quiz-carry">
|
||||
<h2>คำถามหลายตัวเลือก (หยิบมาวาง)</h2>
|
||||
<p class="muted">ใช้กับฉากประเภท <strong>ตอบคำถาม — หยิบคำตอบมาวางกลาง</strong> ใน Editor — ผู้เล่นหยิบตัวเลือกไปวางโซนกลาง · ถ้ามีรายการที่นี่ เกมจะใช้<strong>เฉพาะชุดนี้</strong> (ไม่ผสมกับคำถามถูก/ผิดแท็บคำถามเกม) · เก็บที่ <code>/Game/data/quiz-settings.json</code> ฟิลด์ <code>carryQuestions</code></p>
|
||||
@@ -1036,6 +1059,6 @@
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<script src="admin.js?v=77"></script>
|
||||
<script src="admin.js?v=78"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"cases": {
|
||||
"1": {
|
||||
"name": "คดีโจรกรรมไซเบอร์",
|
||||
"art": "/Main-Lobby/IMAGE/case/case-1.png",
|
||||
"detail": "/Main-Lobby/IMAGE/case/case-detail-1.png",
|
||||
"story": [
|
||||
"/Main-Lobby/IMAGE/cutscene/case-1-story-1.png",
|
||||
"/Main-Lobby/IMAGE/cutscene/case-1-story-2.png"
|
||||
]
|
||||
},
|
||||
"2": {
|
||||
"name": "คดีปล้นร้านอัญมณี",
|
||||
"art": "/Main-Lobby/IMAGE/case/case-2.png",
|
||||
"detail": "/Main-Lobby/IMAGE/case/case-detail-2.png",
|
||||
"story": [
|
||||
"/Main-Lobby/IMAGE/cutscene/case-2-story-1.png",
|
||||
"/Main-Lobby/IMAGE/cutscene/case-2-story-2.png"
|
||||
]
|
||||
},
|
||||
"3": {
|
||||
"name": "คดีฆาตกรรมปริศนา",
|
||||
"art": "/Main-Lobby/IMAGE/case/case-3.png",
|
||||
"detail": "/Main-Lobby/IMAGE/case/case-detail-3.png",
|
||||
"story": [
|
||||
"/Main-Lobby/IMAGE/cutscene/case-3-story-1.png",
|
||||
"/Main-Lobby/IMAGE/cutscene/case-3-story-2.png"
|
||||
]
|
||||
},
|
||||
"4": {
|
||||
"name": "คดีที่ 4",
|
||||
"art": "/Main-Lobby/IMAGE/case/case-4.png",
|
||||
"detail": "/Main-Lobby/IMAGE/case/case-detail-4.png",
|
||||
"story": [
|
||||
"/Main-Lobby/IMAGE/cutscene/case-4-story-1.png",
|
||||
"/Main-Lobby/IMAGE/cutscene/case-4-story-2.png"
|
||||
]
|
||||
},
|
||||
"5": {
|
||||
"name": "คดีที่ 5",
|
||||
"art": "/Main-Lobby/IMAGE/case/case-5.png",
|
||||
"detail": "/Main-Lobby/IMAGE/case/case-detail-5.png",
|
||||
"story": [
|
||||
"/Main-Lobby/IMAGE/cutscene/case-5-story-1.png",
|
||||
"/Main-Lobby/IMAGE/cutscene/case-5-story-2.png"
|
||||
]
|
||||
},
|
||||
"6": {
|
||||
"name": "คดีที่ 6",
|
||||
"art": "/Main-Lobby/IMAGE/case/case-6.png",
|
||||
"detail": "/Main-Lobby/IMAGE/case/case-detail-6.png",
|
||||
"story": [
|
||||
"/Main-Lobby/IMAGE/cutscene/case-6-story-1.png",
|
||||
"/Main-Lobby/IMAGE/cutscene/case-6-story-2.png"
|
||||
]
|
||||
},
|
||||
"7": {
|
||||
"name": "คดีที่ 7",
|
||||
"art": "/Main-Lobby/IMAGE/case/case-7.png",
|
||||
"detail": "/Main-Lobby/IMAGE/case/case-detail-7.png",
|
||||
"story": [
|
||||
"/Main-Lobby/IMAGE/cutscene/case-7-story-1.png",
|
||||
"/Main-Lobby/IMAGE/cutscene/case-7-story-2.png"
|
||||
]
|
||||
},
|
||||
"8": {
|
||||
"name": "คดีที่ 8",
|
||||
"art": "/Main-Lobby/IMAGE/case/case-8.png",
|
||||
"detail": "/Main-Lobby/IMAGE/case/case-detail-8.png",
|
||||
"story": [
|
||||
"/Main-Lobby/IMAGE/cutscene/case-8-story-1.png",
|
||||
"/Main-Lobby/IMAGE/cutscene/case-8-story-2.png"
|
||||
]
|
||||
},
|
||||
"9": {
|
||||
"name": "คดีที่ 9",
|
||||
"art": "/Main-Lobby/IMAGE/case/case-9.png",
|
||||
"detail": "/Main-Lobby/IMAGE/case/case-detail-9.png",
|
||||
"story": [
|
||||
"/Main-Lobby/IMAGE/cutscene/case-9-story-1.png",
|
||||
"/Main-Lobby/IMAGE/cutscene/case-9-story-2.png"
|
||||
]
|
||||
},
|
||||
"10": {
|
||||
"name": "คดีที่ 10",
|
||||
"art": "/Main-Lobby/IMAGE/case/case-10.png",
|
||||
"detail": "/Main-Lobby/IMAGE/case/case-detail-10.png",
|
||||
"story": [
|
||||
"/Main-Lobby/IMAGE/cutscene/case-10-story-1.png",
|
||||
"/Main-Lobby/IMAGE/cutscene/case-10-story-2.png"
|
||||
]
|
||||
},
|
||||
"11": {
|
||||
"name": "คดีที่ 11",
|
||||
"art": "/Main-Lobby/IMAGE/case/case-11.png",
|
||||
"detail": "/Main-Lobby/IMAGE/case/case-detail-11.png",
|
||||
"story": [
|
||||
"/Main-Lobby/IMAGE/cutscene/case-11-story-1.png",
|
||||
"/Main-Lobby/IMAGE/cutscene/case-11-story-2.png"
|
||||
]
|
||||
},
|
||||
"12": {
|
||||
"name": "คดีที่ 12",
|
||||
"art": "/Main-Lobby/IMAGE/case/case-12.png",
|
||||
"detail": "/Main-Lobby/IMAGE/case/case-detail-12.png",
|
||||
"story": [
|
||||
"/Main-Lobby/IMAGE/cutscene/case-12-story-1.png",
|
||||
"/Main-Lobby/IMAGE/cutscene/case-12-story-2.png"
|
||||
]
|
||||
},
|
||||
"13": {
|
||||
"name": "คดีที่ 13",
|
||||
"art": "/Main-Lobby/IMAGE/case/case-13.png",
|
||||
"detail": "/Main-Lobby/IMAGE/case/case-detail-13.png",
|
||||
"story": [
|
||||
"/Main-Lobby/IMAGE/cutscene/case-13-story-1.png",
|
||||
"/Main-Lobby/IMAGE/cutscene/case-13-story-2.png"
|
||||
]
|
||||
},
|
||||
"14": {
|
||||
"name": "คดีที่ 14",
|
||||
"art": "/Main-Lobby/IMAGE/case/case-14.png",
|
||||
"detail": "/Main-Lobby/IMAGE/case/case-detail-14.png",
|
||||
"story": [
|
||||
"/Main-Lobby/IMAGE/cutscene/case-14-story-1.png",
|
||||
"/Main-Lobby/IMAGE/cutscene/case-14-story-2.png"
|
||||
]
|
||||
},
|
||||
"15": {
|
||||
"name": "คดีที่ 15",
|
||||
"art": "/Main-Lobby/IMAGE/case/case-15.png",
|
||||
"detail": "/Main-Lobby/IMAGE/case/case-detail-15.png",
|
||||
"story": [
|
||||
"/Main-Lobby/IMAGE/cutscene/case-15-story-1.png",
|
||||
"/Main-Lobby/IMAGE/cutscene/case-15-story-2.png"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11457,6 +11457,7 @@
|
||||
let specialQuizIcon = null; // { iconType, x, y } (พิกัด tile-center)
|
||||
let specialQuizIconScreenRect = null; // { x, y, r } พิกัดบนจอ (ไว้คลิก/แตะไอคอน)
|
||||
let specialQuizCollideSent = false;
|
||||
let specialQuizCollideCooldownUntil = 0;
|
||||
let specialQuizAwardedCard = null; // การ์ดที่ได้รอบนี้ (จาก special-quiz-ended)
|
||||
let specialQuizActiveQuestion = null; // { index, total, choices, endsAt, iconType }
|
||||
let specialQuizAnswered = false;
|
||||
@@ -11468,31 +11469,49 @@
|
||||
let specialQuizRoster = []; // [{ id, nickname, isBot }]
|
||||
let specialQuizAnswerStatus = {}; // id -> true (ตอบแล้ว)
|
||||
|
||||
let specialQuizFreezeLastTickMs = 0;
|
||||
|
||||
function beginSpecialQuizFreeze() {
|
||||
if (specialQuizFreeze) return;
|
||||
specialQuizFreeze = true;
|
||||
specialQuizFreezeStartMs = Date.now();
|
||||
specialQuizFreezeLastTickMs = Date.now();
|
||||
try { for (const k in keys) keys[k] = false; } catch (e) { /* ignore */ }
|
||||
try { playPath = []; } catch (e) { /* ignore */ }
|
||||
if (me) me.isWalking = false;
|
||||
}
|
||||
|
||||
/** เลื่อน deadline ที่อิงเวลาจริง (quiz_carry / quiz HUD) ไปข้างหน้าเท่าเวลาที่หยุด */
|
||||
/** เลื่อน deadline/anchor เวลาทุกตัวไปข้างหน้าเท่า delta — ใช้เรียกทุกเฟรมระหว่าง freeze
|
||||
เพื่อให้ "เวลาที่แสดง" หยุดนิ่ง (ไม่ใช่แค่ชดเชยตอนจบ) */
|
||||
function shiftSpecialQuizFrozenTimers(deltaMs) {
|
||||
if (!(deltaMs > 0)) return;
|
||||
const shift = (v) => (typeof v === 'number' && v > 0) ? v + deltaMs : v;
|
||||
try { playQuizPhaseEndsAt = shift(playQuizPhaseEndsAt); } catch (e) {}
|
||||
try { quizCarryEmbedCountdownStartAt = shift(quizCarryEmbedCountdownStartAt); } catch (e) {}
|
||||
try { quizCarryEmbedCountdownEndAt = shift(quizCarryEmbedCountdownEndAt); } catch (e) {}
|
||||
try { quizCarryEmbedPreOptionCountdownStartAt = shift(quizCarryEmbedPreOptionCountdownStartAt); } catch (e) {}
|
||||
try { quizCarryEmbedPreOptionCountdownEndAt = shift(quizCarryEmbedPreOptionCountdownEndAt); } catch (e) {}
|
||||
try { quizCarryOptionRevealAt = shift(quizCarryOptionRevealAt); } catch (e) {}
|
||||
try { quizCarryAnswerCloseAt = shift(quizCarryAnswerCloseAt); } catch (e) {}
|
||||
try { if (typeof jumpSurviveSessionStartMs === 'number' && jumpSurviveSessionStartMs > 0) jumpSurviveSessionStartMs += deltaMs; } catch (e) {}
|
||||
try { if (typeof spaceShooterSessionStartMs === 'number' && spaceShooterSessionStartMs > 0) spaceShooterSessionStartMs += deltaMs; } catch (e) {}
|
||||
}
|
||||
|
||||
/** เรียกทุกเฟรมระหว่าง freeze — ดันเวลาไปข้างหน้าเท่าที่ผ่านไปจริง ทำให้ countdown หยุดนิ่ง */
|
||||
function tickSpecialQuizFreezeShift() {
|
||||
if (!specialQuizFreeze) return;
|
||||
const now = Date.now();
|
||||
shiftSpecialQuizFrozenTimers(now - specialQuizFreezeLastTickMs);
|
||||
specialQuizFreezeLastTickMs = now;
|
||||
}
|
||||
|
||||
function endSpecialQuizFreeze() {
|
||||
if (!specialQuizFreeze) return;
|
||||
const frozenMs = Date.now() - specialQuizFreezeStartMs;
|
||||
/* ชดเชย delta ที่เหลือตั้งแต่เฟรมสุดท้ายจนถึงตอนนี้ */
|
||||
shiftSpecialQuizFrozenTimers(Date.now() - specialQuizFreezeLastTickMs);
|
||||
specialQuizFreeze = false;
|
||||
specialQuizFreezeStartMs = 0;
|
||||
if (frozenMs > 0) {
|
||||
const shift = (v) => (typeof v === 'number' && v > 0) ? v + frozenMs : v;
|
||||
try { playQuizPhaseEndsAt = shift(playQuizPhaseEndsAt); } catch (e) {}
|
||||
try { quizCarryEmbedCountdownStartAt = shift(quizCarryEmbedCountdownStartAt); } catch (e) {}
|
||||
try { quizCarryEmbedCountdownEndAt = shift(quizCarryEmbedCountdownEndAt); } catch (e) {}
|
||||
try { quizCarryEmbedPreOptionCountdownStartAt = shift(quizCarryEmbedPreOptionCountdownStartAt); } catch (e) {}
|
||||
try { quizCarryEmbedPreOptionCountdownEndAt = shift(quizCarryEmbedPreOptionCountdownEndAt); } catch (e) {}
|
||||
try { quizCarryOptionRevealAt = shift(quizCarryOptionRevealAt); } catch (e) {}
|
||||
try { quizCarryAnswerCloseAt = shift(quizCarryAnswerCloseAt); } catch (e) {}
|
||||
}
|
||||
specialQuizFreezeLastTickMs = 0;
|
||||
}
|
||||
|
||||
function specialQuizIconUrl(type) {
|
||||
@@ -11514,12 +11533,27 @@
|
||||
y: Number(payload.y),
|
||||
};
|
||||
specialQuizCollideSent = false;
|
||||
specialQuizCollideCooldownUntil = 0;
|
||||
specialQuizGetIconImg(specialQuizIcon.iconType);
|
||||
}
|
||||
function clearSpecialQuizIcon() {
|
||||
specialQuizIcon = null;
|
||||
specialQuizIconScreenRect = null;
|
||||
specialQuizCollideSent = false;
|
||||
specialQuizCollideCooldownUntil = 0;
|
||||
}
|
||||
|
||||
/** ส่ง collide ไป server เพื่อเปิดคำถาม — ถ้า server ปฏิเสธ (เช่น race) ให้ลองใหม่ได้หลัง cooldown */
|
||||
function sendSpecialQuizCollide() {
|
||||
if (specialQuizCollideSent || specialQuizActiveQuestion) return;
|
||||
if (Date.now() < specialQuizCollideCooldownUntil) return;
|
||||
specialQuizCollideSent = true;
|
||||
specialQuizCollideCooldownUntil = Date.now() + 800;
|
||||
try {
|
||||
socket.emit('special-quiz-collide', {}, function (res) {
|
||||
if (!(res && res.ok) && !specialQuizActiveQuestion) specialQuizCollideSent = false;
|
||||
});
|
||||
} catch (e) { specialQuizCollideSent = false; }
|
||||
}
|
||||
|
||||
/** กด/แตะไอคอนควิซเพื่อเปิดคำถาม (ใช้ได้ทุกมินิเกม รวม space shooter ที่เดินไปแตะไม่ได้) */
|
||||
@@ -11532,9 +11566,10 @@
|
||||
const py = (clientY - r.top) * (canvas.height / r.height);
|
||||
const dx = px - specialQuizIconScreenRect.x;
|
||||
const dy = py - specialQuizIconScreenRect.y;
|
||||
if (dx * dx + dy * dy <= specialQuizIconScreenRect.r * specialQuizIconScreenRect.r) {
|
||||
specialQuizCollideSent = true;
|
||||
try { socket.emit('special-quiz-collide', {}, function () {}); } catch (e) { /* ignore */ }
|
||||
/* เผื่อระยะกดให้กว้างขึ้น — แตะใกล้ไอคอนก็เปิดได้ */
|
||||
const hitR = specialQuizIconScreenRect.r * 1.3;
|
||||
if (dx * dx + dy * dy <= hitR * hitR) {
|
||||
sendSpecialQuizCollide();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -11582,9 +11617,9 @@
|
||||
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 */ }
|
||||
/* รัศมีกว้างขึ้น (1.25 tile) — ไอคอนวาดใหญ่ 1.75 tile เดินเข้าใกล้ก็ทริกได้ ไม่ต้องทับ tile กลางพอดี */
|
||||
if (dx * dx + dy * dy <= 1.25 * 1.25) {
|
||||
sendSpecialQuizCollide();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11662,6 +11697,11 @@
|
||||
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'; }
|
||||
/* แสดงเฉพาะ layer คำถาม/คำตอบ — ซ่อน layer ผลรางวัล */
|
||||
const quizBody = document.getElementById('sq-ov-quiz-body');
|
||||
if (quizBody) quizBody.classList.remove('is-hidden');
|
||||
const awardLayer = document.getElementById('sq-ov-award');
|
||||
if (awardLayer) awardLayer.classList.add('is-hidden');
|
||||
const status = document.getElementById('sq-ov-status');
|
||||
if (status) status.textContent = 'เลือกคำตอบ — ทุกคนต้องตอบถูกทุกข้อจึงได้การ์ดพิเศษ';
|
||||
const choicesEl = document.getElementById('sq-ov-choices');
|
||||
@@ -11897,6 +11937,9 @@
|
||||
if (fxEl) fxEl.textContent = 'ตอบถูกครบทุกคนเพื่อรับการ์ดพิเศษในครั้งหน้า';
|
||||
}
|
||||
if (btn) btn.onclick = specialQuizContinueAndResume;
|
||||
/* สลับ layer: ซ่อนคำถาม/คำตอบ แล้วโชว์ผลรางวัลเต็มหน้า */
|
||||
const quizBody = document.getElementById('sq-ov-quiz-body');
|
||||
if (quizBody) quizBody.classList.add('is-hidden');
|
||||
award.classList.remove('is-hidden');
|
||||
} else {
|
||||
/* ไม่มี element สำรอง — เล่นต่อทันที */
|
||||
@@ -19888,8 +19931,10 @@
|
||||
let lastSend = 0;
|
||||
function tick() {
|
||||
if (!mapData) { requestAnimationFrame(tick); return; }
|
||||
/* คำถามพิเศษเปิดอยู่ — หยุดเกมทุกอย่าง (input/sim/timer) เห็นแต่ภาพค้างไว้ */
|
||||
/* คำถามพิเศษเปิดอยู่ — หยุดเกมทุกอย่าง (input/sim/timer) เห็นแต่ภาพค้างไว้
|
||||
ดันเวลาทุกตัวไปข้างหน้าเท่า delta ทุกเฟรม → countdown ที่แสดงหยุดนิ่งจริง */
|
||||
if (specialQuizFreeze) {
|
||||
tickSpecialQuizFreezeShift();
|
||||
if (me) me.isWalking = false;
|
||||
draw();
|
||||
requestAnimationFrame(tick);
|
||||
|
||||
@@ -1807,9 +1807,106 @@
|
||||
preplayStepCase.style.setProperty('--lobby-preplay-case-scale', scale.toFixed(4));
|
||||
}
|
||||
|
||||
/* ===== รูปคดี + Cutscene (โหลดจาก /api/case-media — แก้ใน Admin ได้) ===== */
|
||||
var caseMediaData = null;
|
||||
var caseMediaFetchPromise = null;
|
||||
function ensureCaseMediaLoaded() {
|
||||
if (caseMediaData) return Promise.resolve(caseMediaData);
|
||||
if (caseMediaFetchPromise) return caseMediaFetchPromise;
|
||||
caseMediaFetchPromise = fetch(SERVER + '/api/case-media', { cache: 'no-store' })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (j) { caseMediaData = (j && j.cases) ? j : { cases: {} }; return caseMediaData; })
|
||||
.catch(function () { caseMediaFetchPromise = null; return { cases: {} }; });
|
||||
return caseMediaFetchPromise;
|
||||
}
|
||||
function getCaseMedia(cid) {
|
||||
var n = parseInt(cid, 10);
|
||||
if (!(n >= 1)) n = 1;
|
||||
var def = {
|
||||
name: 'คดีที่ ' + n,
|
||||
art: '/Main-Lobby/IMAGE/case/case-' + n + '.png',
|
||||
detail: '/Main-Lobby/IMAGE/case/case-detail-' + n + '.png',
|
||||
story: ['/Main-Lobby/IMAGE/cutscene/case-' + n + '-story-1.png', '/Main-Lobby/IMAGE/cutscene/case-' + n + '-story-2.png'],
|
||||
};
|
||||
var c = (caseMediaData && caseMediaData.cases) ? (caseMediaData.cases[n] || caseMediaData.cases[String(n)]) : null;
|
||||
if (!c) return def;
|
||||
return {
|
||||
name: c.name || def.name,
|
||||
art: c.art || def.art,
|
||||
detail: c.detail || def.detail,
|
||||
story: (Array.isArray(c.story) && c.story.length) ? c.story : def.story,
|
||||
};
|
||||
}
|
||||
|
||||
/** สร้างการ์ดเลือกคดีตามระดับที่เลือก — 5 ระดับ × 3 คดี (L1=1-3, L2=4-6 … L5=13-15) */
|
||||
function renderPreplayCaseCards() {
|
||||
var row = preplayOverlay ? preplayOverlay.querySelector('.lobby-preplay-case-row') : null;
|
||||
if (!row) return;
|
||||
var lvl = parseInt(preplaySelectedLevel || (preplayOverlay && preplayOverlay.dataset.preplayLevel) || '1', 10);
|
||||
if (!(lvl >= 1 && lvl <= 5)) lvl = 1;
|
||||
var startCase = (lvl - 1) * 3 + 1;
|
||||
var endCase = startCase + 2;
|
||||
var html = '';
|
||||
for (var n = startCase; n <= endCase; n++) {
|
||||
var m = getCaseMedia(n);
|
||||
var nm = String(m.name || ('คดีที่ ' + n)).replace(/"/g, '"');
|
||||
html += '<div class="lobby-case-card-wrap">' +
|
||||
'<img class="lobby-case-art" src="' + m.art + '" alt="' + nm + '" decoding="async">' +
|
||||
'<div class="lobby-case-actions">' +
|
||||
'<button type="button" class="lobby-case-detail" data-case="' + n + '" title="รายละเอียด"><img src="/Main-Lobby/IMAGE/case/btn-detail.png" alt="รายละเอียด" decoding="async"></button>' +
|
||||
'<button type="button" class="lobby-case-start" data-case="' + n + '" title="เริ่มคดี"><img src="/Main-Lobby/IMAGE/case/btn-startcase.png" alt="เริ่มคดี" decoding="async"></button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
row.innerHTML = html;
|
||||
}
|
||||
|
||||
/** Cutscene ก่อนเข้า LobbyB — โชว์ภาพเรื่องราว 2 รูปของคดี ต้องกด «ไปต่อ» ทีละรูป */
|
||||
function showCaseCutscene(cid, onDone) {
|
||||
var media = getCaseMedia(cid);
|
||||
var imgs = (media.story || []).filter(function (s) { return typeof s === 'string' && s; });
|
||||
var finished = false;
|
||||
var done = function () { if (finished) return; finished = true; if (typeof onDone === 'function') onDone(); };
|
||||
if (!imgs.length) { done(); return; }
|
||||
var idx = 0;
|
||||
var ov = document.getElementById('case-cutscene-overlay');
|
||||
if (!ov) {
|
||||
ov = document.createElement('div');
|
||||
ov.id = 'case-cutscene-overlay';
|
||||
ov.style.cssText = 'position:fixed;inset:0;z-index:99990;display:flex;align-items:center;justify-content:center;background:#05070f';
|
||||
ov.innerHTML =
|
||||
'<img id="case-cutscene-img" alt="" decoding="async" style="max-width:100vw;max-height:100vh;width:auto;height:auto;object-fit:contain;display:block">' +
|
||||
'<div style="position:absolute;left:0;right:0;bottom:24px;display:flex;align-items:center;justify-content:center;gap:16px;padding:0 24px;pointer-events:none">' +
|
||||
'<span id="case-cutscene-progress" style="font:800 15px/1 Kanit,system-ui,sans-serif;color:#cdd6ff;background:rgba(8,12,26,.6);padding:8px 14px;border-radius:999px"></span>' +
|
||||
'<button type="button" id="case-cutscene-next" style="pointer-events:auto;cursor:pointer;border:none;border-radius:999px;padding:13px 38px;font:800 17px/1 Kanit,system-ui,sans-serif;color:#0a0e1a;background:linear-gradient(180deg,#ffd666,#f0b429);box-shadow:0 10px 28px rgba(240,180,41,.5)">ไปต่อ ▶</button>' +
|
||||
'</div>';
|
||||
document.body.appendChild(ov);
|
||||
}
|
||||
ov.classList.remove('is-hidden');
|
||||
ov.style.display = 'flex';
|
||||
var imgEl = document.getElementById('case-cutscene-img');
|
||||
var progEl = document.getElementById('case-cutscene-progress');
|
||||
var nextBtn = document.getElementById('case-cutscene-next');
|
||||
function render() {
|
||||
if (imgEl) imgEl.src = imgs[idx];
|
||||
if (progEl) progEl.textContent = (idx + 1) + ' / ' + imgs.length;
|
||||
if (nextBtn) nextBtn.textContent = (idx + 1 >= imgs.length) ? 'เริ่มคดี ▶' : 'ไปต่อ ▶';
|
||||
}
|
||||
function finish() { ov.classList.add('is-hidden'); ov.style.display = 'none'; done(); }
|
||||
if (nextBtn) nextBtn.onclick = function () { idx++; if (idx >= imgs.length) finish(); else render(); };
|
||||
/* preload รูปถัดไป */
|
||||
if (imgs[1]) { var pre = new Image(); pre.src = imgs[1]; }
|
||||
render();
|
||||
}
|
||||
|
||||
function showPreplayCaseStep() {
|
||||
if (preplayStepLevel) preplayStepLevel.classList.add('is-hidden');
|
||||
if (preplayStepCase) preplayStepCase.classList.remove('is-hidden');
|
||||
ensureCaseMediaLoaded().then(function () {
|
||||
renderPreplayCaseCards();
|
||||
syncPreplayCaseStepScale();
|
||||
requestAnimationFrame(syncPreplayCaseStepScale);
|
||||
});
|
||||
syncPreplayCaseStepScale();
|
||||
requestAnimationFrame(syncPreplayCaseStepScale);
|
||||
}
|
||||
@@ -1906,11 +2003,16 @@
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.lobby-case-detail').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var o = document.getElementById('lobby-preplay-case-detail-overlay');
|
||||
if (o) o.classList.remove('is-hidden');
|
||||
});
|
||||
/* delegation — การ์ดถูก render ใหม่ 15 ใบ; ตั้งรูปรายละเอียดตามคดีที่กด */
|
||||
preplayOverlay.addEventListener('click', function (ev) {
|
||||
var detailBtn = ev.target.closest('.lobby-case-detail');
|
||||
if (!detailBtn || !preplayOverlay.contains(detailBtn)) return;
|
||||
var cid = (detailBtn.getAttribute('data-case') || '').trim();
|
||||
var o = document.getElementById('lobby-preplay-case-detail-overlay');
|
||||
if (!o) return;
|
||||
var img = o.querySelector('img');
|
||||
if (img && cid) img.src = getCaseMedia(cid).detail;
|
||||
o.classList.remove('is-hidden');
|
||||
});
|
||||
|
||||
var detailClose = document.getElementById('lobby-preplay-case-detail-close');
|
||||
@@ -3954,6 +4056,7 @@
|
||||
};
|
||||
fullImg.src = c.imageUrl;
|
||||
card.appendChild(fullImg);
|
||||
makeEvidenceCardZoomable(card, c, linkName);
|
||||
return card;
|
||||
}
|
||||
buildEvHtmlCard(card, c, linkName, cardIdx);
|
||||
@@ -4975,7 +5078,11 @@
|
||||
if (res && res.ok) {
|
||||
tmState.submitted = true;
|
||||
if (!tmState.submittedPicksById) tmState.submittedPicksById = {};
|
||||
tmState.submittedPicksById[socket.id] = tmState.picks.slice(0, req);
|
||||
/* เก็บเป็น card object (สอดคล้องกับ data.picks จาก server) ไม่ใช่ index */
|
||||
var ownedRow = (myPlayerEvidence[tmState.round] || []);
|
||||
tmState.submittedPicksById[socket.id] = tmState.picks.slice(0, req)
|
||||
.map(function (slot) { return ownedRow[slot]; })
|
||||
.filter(function (c) { return c && typeof c === 'object' && c.imageUrl; });
|
||||
if (tmState.submittedIds.indexOf(socket.id) < 0) tmState.submittedIds.push(socket.id);
|
||||
es3SetStatus('ส่งหลักฐานแล้ว — รอผู้เล่นอื่น...');
|
||||
es3RenderMembers(tmState.submittedIds, [], false);
|
||||
@@ -5106,6 +5213,60 @@
|
||||
card.querySelector('.sname').textContent = nm;
|
||||
}
|
||||
|
||||
var EVIDENCE_LIGHTBOX_GLOW = { common: 'rgba(120,160,255,.55)', rare: 'rgba(150,120,255,.6)', legendary: 'rgba(255,196,90,.65)' };
|
||||
|
||||
/** เปิดดูการ์ดหลักฐานแบบเต็มจอ + ปุ่มปิด (คลิก backdrop / กากบาท / Esc เพื่อปิด) */
|
||||
function openEvidenceCardLightbox(imageUrl, label, rarity) {
|
||||
if (!imageUrl) return;
|
||||
var rar = String(rarity || 'common').toLowerCase();
|
||||
if (!EVIDENCE_LIGHTBOX_GLOW[rar]) rar = 'common';
|
||||
var ov = document.getElementById('evidence-card-lightbox');
|
||||
if (!ov) {
|
||||
ov = document.createElement('div');
|
||||
ov.id = 'evidence-card-lightbox';
|
||||
ov.style.cssText = 'position:fixed;inset:0;z-index:100000;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(5,8,18,.86);backdrop-filter:blur(4px)';
|
||||
ov.innerHTML =
|
||||
'<button type="button" class="ecl-close" aria-label="ปิด" ' +
|
||||
'style="position:absolute;top:22px;right:26px;width:46px;height:46px;border:none;border-radius:50%;cursor:pointer;font:900 26px/1 system-ui,sans-serif;color:#0a0e1a;background:linear-gradient(180deg,#ffd666,#f0b429);box-shadow:0 8px 22px rgba(0,0,0,.45)">×</button>' +
|
||||
'<div class="ecl-stage" style="display:flex;flex-direction:column;align-items:center;gap:14px;max-width:92vw">' +
|
||||
'<img class="ecl-img" alt="" style="display:block;width:auto;height:auto;max-width:min(86vw,520px);max-height:78vh;border-radius:16px;animation:eclPop .3s cubic-bezier(.2,.9,.3,1.35)" />' +
|
||||
'<div class="ecl-cap" style="font:800 16px/1.3 Kanit,system-ui,sans-serif;color:#e7ecff;text-align:center;max-width:90vw"></div>' +
|
||||
'</div>';
|
||||
document.body.appendChild(ov);
|
||||
if (!document.getElementById('ecl-anim-style')) {
|
||||
var st = document.createElement('style');
|
||||
st.id = 'ecl-anim-style';
|
||||
st.textContent = '@keyframes eclPop{from{opacity:0;transform:scale(.85)}to{opacity:1;transform:none}}';
|
||||
document.head.appendChild(st);
|
||||
}
|
||||
var closeFn = function () { ov.classList.add('is-hidden'); ov.style.display = 'none'; };
|
||||
ov.addEventListener('click', function (e) { if (e.target === ov || e.target.classList.contains('ecl-close')) closeFn(); });
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && ov.style.display !== 'none') closeFn();
|
||||
});
|
||||
}
|
||||
var img = ov.querySelector('.ecl-img');
|
||||
var cap = ov.querySelector('.ecl-cap');
|
||||
if (img) {
|
||||
img.style.boxShadow = '0 0 44px ' + EVIDENCE_LIGHTBOX_GLOW[rar] + ',0 22px 60px rgba(0,0,0,.6)';
|
||||
img.src = imageUrl;
|
||||
}
|
||||
if (cap) cap.textContent = label || '';
|
||||
ov.classList.remove('is-hidden');
|
||||
ov.style.display = 'flex';
|
||||
}
|
||||
|
||||
/** ทำให้การ์ด (PNG เต็มใบ) คลิกเพื่อดูใหญ่ได้ */
|
||||
function makeEvidenceCardZoomable(cardEl, c, linkName) {
|
||||
if (!cardEl || !c || !c.imageUrl) return;
|
||||
cardEl.style.cursor = 'zoom-in';
|
||||
cardEl.addEventListener('click', function () {
|
||||
var label = (c.titleTh || '') + (c.titleEn ? ' · ' + c.titleEn : '');
|
||||
if (!label && linkName) label = linkName;
|
||||
openEvidenceCardLightbox(c.imageUrl, label, c.rarity);
|
||||
});
|
||||
}
|
||||
|
||||
function esRevealCardEl(c, linkName, cardIdx) {
|
||||
var rar = String(c.rarity || 'common').toLowerCase();
|
||||
if (!LOBBY_EVIDENCE_RARITY[rar]) rar = 'common';
|
||||
@@ -5120,6 +5281,7 @@
|
||||
img.onerror = function () { this.onerror = null; buildEsRevealHtmlCard(card, c, linkName, cardIdx); };
|
||||
img.src = c.imageUrl;
|
||||
card.appendChild(img);
|
||||
makeEvidenceCardZoomable(card, c, linkName);
|
||||
return card;
|
||||
}
|
||||
buildEsRevealHtmlCard(card, c, linkName, cardIdx);
|
||||
@@ -5136,12 +5298,11 @@
|
||||
if (k === memberId || String(k) === String(memberId)) sel = picksMap[k];
|
||||
});
|
||||
}
|
||||
if (!sel) return [];
|
||||
if (!Array.isArray(sel)) return [];
|
||||
/* picks = array ของ card object (server เก็บเป็น object หลัง refactor) — คืน object ตามเดิม */
|
||||
var out = [];
|
||||
sel.forEach(function (ci) {
|
||||
var idx = Math.floor(Number(ci));
|
||||
if (idx >= 0 && out.indexOf(idx) === -1) out.push(idx);
|
||||
sel.forEach(function (c) {
|
||||
if (c && typeof c === 'object' && c.imageUrl) out.push(c);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
@@ -6019,6 +6180,11 @@
|
||||
function applyRoomLobbyBTransition(data) {
|
||||
const mid = (data && data.mapId) ? String(data.mapId).trim() : '';
|
||||
if (!mid) return;
|
||||
/* Cutscene เรื่องราวคดี ก่อนเข้า LobbyB — โหลด LobbyB ใต้ overlay พร้อมกัน */
|
||||
var cutsceneCid = (data && data.caseId != null) ? data.caseId : null;
|
||||
if (cutsceneCid) {
|
||||
ensureCaseMediaLoaded().then(function () { showCaseCutscene(cutsceneCid, function () {}); });
|
||||
}
|
||||
fetch(SERVER + '/api/maps/' + encodeURIComponent(mid))
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (json) {
|
||||
|
||||
@@ -3899,7 +3899,8 @@
|
||||
#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); }
|
||||
#special-quiz-overlay .sq-ov-award { margin-top: 16px; display: flex; flex-direction: column; align-items: center; gap: 10px; text-align: center; }
|
||||
#special-quiz-overlay #sq-ov-quiz-body.is-hidden { display: none; }
|
||||
#special-quiz-overlay .sq-ov-award { display: flex; flex-direction: column; align-items: center; gap: 10px; text-align: center; }
|
||||
#special-quiz-overlay .sq-ov-award.is-hidden { display: none; }
|
||||
#special-quiz-overlay .sq-ov-award-title { font-size: 20px; font-weight: 900; color: #ffe7a3; text-shadow: 0 0 14px rgba(255,214,102,0.55); }
|
||||
#special-quiz-overlay .sq-ov-award-img { width: 170px; max-width: 60vw; height: auto; border-radius: 14px; box-shadow: 0 0 34px rgba(255,200,90,0.55), 0 14px 40px rgba(0,0,0,0.55); animation: sqAwardPop .55s cubic-bezier(.2,.9,.3,1.45); }
|
||||
@@ -3945,20 +3946,22 @@
|
||||
<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 id="sq-ov-quiz-body">
|
||||
<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-timer" class="sq-ov-timer">20</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 class="sq-ov-members-head">สมาชิกในรอบนี้ — ต้องตอบถูกครบทุกคน</div>
|
||||
<div id="sq-ov-members" class="sq-ov-members"></div>
|
||||
<div id="sq-ov-result" class="sq-ov-result is-hidden"></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 class="sq-ov-members-head">สมาชิกในรอบนี้ — ต้องตอบถูกครบทุกคน</div>
|
||||
<div id="sq-ov-members" class="sq-ov-members"></div>
|
||||
<div id="sq-ov-result" class="sq-ov-result is-hidden"></div>
|
||||
<div id="sq-ov-award" class="sq-ov-award is-hidden">
|
||||
<div id="sq-ov-award-title" class="sq-ov-award-title">ได้รับการ์ดพิเศษ!</div>
|
||||
<img id="sq-ov-award-img" class="sq-ov-award-img" alt="" />
|
||||
@@ -3971,7 +3974,7 @@
|
||||
<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.0509"></script>
|
||||
<script src="js/play.js?v=0.0512"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1613,7 +1613,7 @@
|
||||
<script src="js/display-name.js?v=2"></script>
|
||||
<script src="js/version.js?v=0.0122"></script>
|
||||
<script src="js/customize-popup.js?v=31" data-customize-triggers="" data-customize-asset-base="img/03-5-Customize"></script>
|
||||
<script src="js/room-lobby.js?v=0.0262"></script>
|
||||
<script src="js/room-lobby.js?v=0.0266"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -375,7 +375,9 @@ function sanitizeSpecialQuizSets(obj) {
|
||||
function getSpecialQuizSet(settings, level, caseId) {
|
||||
const sets = (settings && settings.specialQuizSets) || sanitizeSpecialQuizSets({});
|
||||
const L = SPECIAL_QUIZ_LEVELS.indexOf(Number(level)) >= 0 ? Number(level) : 1;
|
||||
const C = SPECIAL_QUIZ_CASES.indexOf(Number(caseId)) >= 0 ? Number(caseId) : 1;
|
||||
/* caseId เป็นเลขคดีรวม 1-15 (5 ระดับ × 3 คดี) → แปลงเป็นคดีสัมพัทธ์ในระดับ 1-3 */
|
||||
const rel = ((Math.max(1, Number(caseId) || 1) - 1) % 3) + 1;
|
||||
const C = SPECIAL_QUIZ_CASES.indexOf(rel) >= 0 ? rel : 1;
|
||||
return sets[specialQuizSetKey(L, C)] || sets[specialQuizSetKey(1, 1)] || { questions: [], specialCard: sanitizeSpecialCard({}) };
|
||||
}
|
||||
|
||||
@@ -1196,6 +1198,87 @@ function mergeQuizCarryFromMirrorIfSet(base) {
|
||||
const EVIDENCE_CARDS_PATH = path.join(__dirname, 'data', 'evidence-cards.json');
|
||||
const EVIDENCE_RARITIES = ['common', 'rare', 'legendary'];
|
||||
const EVIDENCE_CASE_COUNT = 15;
|
||||
|
||||
/* ===== รูปภาพคดี + Cutscene (LobbyA → LobbyB) — จัดการผ่าน Admin ===== */
|
||||
const CASE_MEDIA_PATH = path.join(__dirname, 'data', 'case-media.json');
|
||||
const CASE_MEDIA_COUNT = 15;
|
||||
/* Main-Lobby อยู่นอกโฟลเดอร์ Game (เสิร์ฟจาก /var/www/html) — ขึ้นไป 1 ระดับ */
|
||||
const MAIN_LOBBY_IMAGE_ROOT = path.join(__dirname, '..', 'Main-Lobby', 'IMAGE');
|
||||
|
||||
function caseMediaDefault(n) {
|
||||
return {
|
||||
name: 'คดีที่ ' + n,
|
||||
art: '/Main-Lobby/IMAGE/case/case-' + n + '.png',
|
||||
detail: '/Main-Lobby/IMAGE/case/case-detail-' + n + '.png',
|
||||
story: [
|
||||
'/Main-Lobby/IMAGE/cutscene/case-' + n + '-story-1.png',
|
||||
'/Main-Lobby/IMAGE/cutscene/case-' + n + '-story-2.png',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/** อนุญาตเฉพาะ path รูปภายในเว็บ (กัน URL ภายนอก/สคริปต์) — ไม่ผ่าน → คืนค่า fallback */
|
||||
function sanitizeCaseMediaUrl(v, fallback) {
|
||||
if (typeof v !== 'string') return fallback;
|
||||
const s = v.trim();
|
||||
if (!s) return fallback;
|
||||
if (!/^\/[\w./-]*\.(png|jpe?g|webp)$/i.test(s)) return fallback;
|
||||
return s;
|
||||
}
|
||||
|
||||
function sanitizeCaseMedia(obj) {
|
||||
const src = (obj && typeof obj === 'object' && obj.cases && typeof obj.cases === 'object') ? obj.cases : {};
|
||||
const cases = {};
|
||||
for (let n = 1; n <= CASE_MEDIA_COUNT; n++) {
|
||||
const d = caseMediaDefault(n);
|
||||
const c = src[n] || src[String(n)] || {};
|
||||
const story = Array.isArray(c.story) ? c.story : [];
|
||||
cases[n] = {
|
||||
name: (typeof c.name === 'string' && c.name.trim()) ? c.name.trim().slice(0, 80) : d.name,
|
||||
art: sanitizeCaseMediaUrl(c.art, d.art),
|
||||
detail: sanitizeCaseMediaUrl(c.detail, d.detail),
|
||||
story: [sanitizeCaseMediaUrl(story[0], d.story[0]), sanitizeCaseMediaUrl(story[1], d.story[1])],
|
||||
};
|
||||
}
|
||||
return { cases };
|
||||
}
|
||||
|
||||
function loadCaseMedia() {
|
||||
try {
|
||||
if (fs.existsSync(CASE_MEDIA_PATH)) {
|
||||
return sanitizeCaseMedia(JSON.parse(fs.readFileSync(CASE_MEDIA_PATH, 'utf8')));
|
||||
}
|
||||
} catch (e) { /* ignore — คืนค่า default */ }
|
||||
return sanitizeCaseMedia({});
|
||||
}
|
||||
|
||||
function saveCaseMedia(d) {
|
||||
try {
|
||||
const clean = sanitizeCaseMedia(d);
|
||||
fs.writeFileSync(CASE_MEDIA_PATH, JSON.stringify(clean, null, 2), 'utf8');
|
||||
return { ok: true, cases: clean.cases };
|
||||
} catch (e) {
|
||||
let err = 'บันทึกไม่ได้';
|
||||
if (e && e.code === 'EACCES') err = 'ไม่มีสิทธิ์เขียนไฟล์ case-media.json (chown เป็น user ที่รันเกม)';
|
||||
else if (e && e.message) err = 'บันทึกไม่ได้: ' + e.message;
|
||||
return { ok: false, error: err };
|
||||
}
|
||||
}
|
||||
|
||||
/** รายการรูปที่มีจริงในโฟลเดอร์ case / cutscene (ไว้ให้ Admin เลือก) */
|
||||
function listCaseMediaImages() {
|
||||
const out = { case: [], cutscene: [] };
|
||||
['case', 'cutscene'].forEach((sub) => {
|
||||
try {
|
||||
const p = path.join(MAIN_LOBBY_IMAGE_ROOT, sub);
|
||||
out[sub] = fs.readdirSync(p)
|
||||
.filter((f) => /\.(png|jpe?g|webp)$/i.test(f))
|
||||
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
|
||||
.map((f) => '/Main-Lobby/IMAGE/' + sub + '/' + f);
|
||||
} catch (e) { /* โฟลเดอร์อาจไม่มี */ }
|
||||
});
|
||||
return out;
|
||||
}
|
||||
const EVIDENCE_IMAGE_DIRS = ['suspect-1', 'suspect-2', 'suspect-3'];
|
||||
/* rarity คิดจากเลขไฟล์การ์ด: 01-22 = common, 23-44 = rare, 45-54 = legendary (ชี้คนร้าย) */
|
||||
const EVIDENCE_RARITY_RANGES = [
|
||||
@@ -4078,6 +4161,53 @@ const server = http.createServer((req, res) => {
|
||||
res.writeHead(405);
|
||||
return res.end(JSON.stringify({ ok: false, error: 'Method not allowed' }));
|
||||
}
|
||||
const caseMediaUrlPath = url.split('?')[0].replace(/\/+$/, '') || '/';
|
||||
if (caseMediaUrlPath === BASE_PATH + '/api/case-media') {
|
||||
if (req.method === 'GET') {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Cache-Control': 'no-store, no-cache, must-revalidate',
|
||||
Pragma: 'no-cache',
|
||||
});
|
||||
return res.end(JSON.stringify(loadCaseMedia()));
|
||||
}
|
||||
if (req.method === 'PUT') {
|
||||
let body = '';
|
||||
req.on('data', (c) => { body += c; });
|
||||
req.on('end', () => {
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
try {
|
||||
const d = JSON.parse(body || '{}');
|
||||
const saved = saveCaseMedia(d);
|
||||
if (!saved.ok) {
|
||||
res.writeHead(500);
|
||||
return res.end(JSON.stringify({ ok: false, error: saved.error || 'บันทึกไม่ได้' }));
|
||||
}
|
||||
res.writeHead(200);
|
||||
return res.end(JSON.stringify({ ok: true, cases: saved.cases }));
|
||||
} catch (e) {
|
||||
res.writeHead(400);
|
||||
return res.end(JSON.stringify({ ok: false, error: 'ข้อมูลไม่ถูกต้อง' }));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.writeHead(405);
|
||||
return res.end(JSON.stringify({ ok: false, error: 'Method not allowed' }));
|
||||
}
|
||||
const caseMediaImagesUrlPath = url.split('?')[0].replace(/\/+$/, '') || '/';
|
||||
if (caseMediaImagesUrlPath === BASE_PATH + '/api/case-media-images') {
|
||||
if (req.method === 'GET') {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Cache-Control': 'no-store, no-cache, must-revalidate',
|
||||
Pragma: 'no-cache',
|
||||
});
|
||||
return res.end(JSON.stringify({ images: listCaseMediaImages(), count: CASE_MEDIA_COUNT }));
|
||||
}
|
||||
res.writeHead(405);
|
||||
return res.end(JSON.stringify({ ok: false, error: 'Method not allowed' }));
|
||||
}
|
||||
const gameTimingUrlPath = url.split('?')[0].replace(/\/+$/, '') || '/';
|
||||
if (gameTimingUrlPath === BASE_PATH + '/api/game-timing') {
|
||||
if (req.method === 'GET') {
|
||||
@@ -6413,7 +6543,8 @@ io.on('connection', (socket) => {
|
||||
let lobbyLevel = (data && data.lobbyLevel != null) ? String(data.lobbyLevel).trim() : '';
|
||||
let caseId = (data && data.caseId != null) ? String(data.caseId).trim() : '';
|
||||
if (lobbyLevel && !/^[1-5]$/.test(lobbyLevel)) lobbyLevel = '';
|
||||
if (caseId && !/^[1-3]$/.test(caseId)) caseId = '';
|
||||
/* 15 คดี (5 ระดับ × 3 คดี) — caseId 1-15 */
|
||||
if (caseId && !/^(?:[1-9]|1[0-5])$/.test(caseId)) caseId = '';
|
||||
ensurePostCaseLobbyMapLoaded();
|
||||
const mdBefore = getLobbyLayoutMapForSpace(space);
|
||||
if (mdBefore && mdBefore.gameType === 'quiz') {
|
||||
@@ -6462,6 +6593,7 @@ io.on('connection', (socket) => {
|
||||
space.suspectPickIndex = 0;
|
||||
space.detectiveLobbyLevel = Number(lobbyLevel) || 1;
|
||||
space.detectiveLobbyCaseId = Number(caseId) || 1;
|
||||
space.caseId = Number(caseId) || 1;
|
||||
if (space.troublesomeEligible) space.troublesomeEligible.clear();
|
||||
else space.troublesomeEligible = new Set();
|
||||
if (space.troublesomeDebTimer) clearTimeout(space.troublesomeDebTimer);
|
||||
|
||||
|
Before Width: | Height: | Size: 333 KiB After Width: | Height: | Size: 567 KiB |
|
After Width: | Height: | Size: 569 KiB |
|
After Width: | Height: | Size: 568 KiB |
|
After Width: | Height: | Size: 568 KiB |
|
After Width: | Height: | Size: 568 KiB |
|
After Width: | Height: | Size: 570 KiB |
|
After Width: | Height: | Size: 568 KiB |
|
Before Width: | Height: | Size: 349 KiB After Width: | Height: | Size: 569 KiB |
|
Before Width: | Height: | Size: 306 KiB After Width: | Height: | Size: 568 KiB |
|
After Width: | Height: | Size: 568 KiB |
|
After Width: | Height: | Size: 568 KiB |
|
After Width: | Height: | Size: 568 KiB |
|
After Width: | Height: | Size: 569 KiB |
|
After Width: | Height: | Size: 568 KiB |
|
After Width: | Height: | Size: 568 KiB |
|
After Width: | Height: | Size: 815 KiB |
|
After Width: | Height: | Size: 814 KiB |
|
After Width: | Height: | Size: 818 KiB |
|
After Width: | Height: | Size: 812 KiB |
|
After Width: | Height: | Size: 812 KiB |
|
After Width: | Height: | Size: 824 KiB |
|
After Width: | Height: | Size: 815 KiB |
|
After Width: | Height: | Size: 818 KiB |
|
After Width: | Height: | Size: 817 KiB |
|
After Width: | Height: | Size: 812 KiB |
|
After Width: | Height: | Size: 815 KiB |
|
After Width: | Height: | Size: 819 KiB |
|
After Width: | Height: | Size: 818 KiB |
|
After Width: | Height: | Size: 816 KiB |
|
After Width: | Height: | Size: 814 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.7 MiB |