update คดี, พูลรูปแยก rarity ถูกต้อง (suspect-1: 22/22/10, suspect-2: 20/0/10, suspect-3: 22/22/10), save/load API ทำงาน
@@ -2756,3 +2756,138 @@ code {
|
||||
aspect-ratio: 382 / 72;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
/* ===== Evidence Cards admin panel ===== */
|
||||
.ev-suspect-block {
|
||||
border: 1px solid rgba(124, 154, 255, 0.25);
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
margin: 0 0 16px;
|
||||
background: rgba(20, 26, 48, 0.4);
|
||||
}
|
||||
.ev-suspect-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.ev-suspect-head .ev-suspect-no {
|
||||
font-weight: 900;
|
||||
font-size: 15px;
|
||||
color: #ffd666;
|
||||
background: rgba(255, 214, 102, 0.14);
|
||||
border: 1px solid rgba(255, 214, 102, 0.4);
|
||||
border-radius: 999px;
|
||||
padding: 4px 12px;
|
||||
}
|
||||
.ev-suspect-head label {
|
||||
flex: 1 1 240px;
|
||||
}
|
||||
.ev-cards-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.ev-cards-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
.ev-card-edit {
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
background: rgba(10, 14, 28, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.ev-card-edit .ev-card-edit-rar {
|
||||
font-weight: 800;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.ev-card-edit.rar-common .ev-card-edit-rar { color: #9aa3b2; }
|
||||
.ev-card-edit.rar-rare .ev-card-edit-rar { color: #6cc7ff; }
|
||||
.ev-card-edit.rar-legendary .ev-card-edit-rar { color: #f1b94a; }
|
||||
.ev-card-edit input,
|
||||
.ev-card-edit textarea,
|
||||
.ev-card-edit select {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ev-card-edit textarea { min-height: 60px; resize: vertical; }
|
||||
.ev-card-edit-imgwrap {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.ev-card-edit-thumb {
|
||||
width: 72px;
|
||||
height: 102px;
|
||||
object-fit: contain;
|
||||
border-radius: 6px;
|
||||
background: #1b2240;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ev-card-edit-imgcol {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.ev-img-picker {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9000;
|
||||
background: rgba(6, 9, 20, 0.85);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.ev-img-picker[hidden] { display: none; }
|
||||
.ev-img-picker-shell {
|
||||
width: min(900px, 94vw);
|
||||
max-height: 86vh;
|
||||
background: linear-gradient(180deg, #161d38, #0e1326);
|
||||
border: 1px solid rgba(124, 154, 255, 0.4);
|
||||
border-radius: 14px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.ev-img-picker-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.ev-img-picker-head strong { font-size: 16px; color: #fff; }
|
||||
.ev-img-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
||||
gap: 10px;
|
||||
overflow: auto;
|
||||
padding: 4px;
|
||||
}
|
||||
.ev-img-grid button {
|
||||
border: 2px solid rgba(124, 154, 255, 0.25);
|
||||
border-radius: 8px;
|
||||
background: #1b2240;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.12s, transform 0.12s;
|
||||
}
|
||||
.ev-img-grid button:hover { border-color: #7c9aff; transform: translateY(-2px); }
|
||||
.ev-img-grid button.is-current { border-color: #9ece6a; }
|
||||
.ev-img-grid img { width: 100%; height: auto; display: block; border-radius: 4px; }
|
||||
.ev-img-grid .ev-img-name { font-size: 11px; color: #9fb0d8; text-align: center; margin-top: 2px; }
|
||||
|
||||
/* Evidence pool count badges (รูปอย่างเดียว) */
|
||||
.ev-pool-counts { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 6px; }
|
||||
.ev-pool-pill { font-size: 12px; font-weight: 700; padding: 4px 10px; border-radius: 999px; color: #0a0e1a; }
|
||||
.ev-pool-pill.rar-common { background: #c8d2e0; }
|
||||
.ev-pool-pill.rar-rare { background: #7fb6ff; }
|
||||
.ev-pool-pill.rar-legendary { background: #ffcf6b; }
|
||||
|
||||
@@ -4026,6 +4026,164 @@
|
||||
el('top-actions').hidden = name !== 'panel-app';
|
||||
}
|
||||
|
||||
// ===== Evidence Cards admin (รูปอย่างเดียว · 15 คดี · rarity จากเลขไฟล์) =====
|
||||
var EVIDENCE_CARDS_API = '/Game/api/evidence-cards';
|
||||
var EVIDENCE_IMAGES_API = '/Game/api/evidence-card-images';
|
||||
var EVIDENCE_CASE_COUNT = 15;
|
||||
var EVIDENCE_DIRS = ['suspect-1', 'suspect-2', 'suspect-3'];
|
||||
var evidenceData = null;
|
||||
var evidencePools = null; // { 'suspect-1': {common,rare,legendary}, ... }
|
||||
var evidenceCurrentCase = '1';
|
||||
|
||||
function evidencePopulateCaseSelect() {
|
||||
var sel = el('evidence-case');
|
||||
if (!sel || sel.options.length) return;
|
||||
for (var i = 1; i <= EVIDENCE_CASE_COUNT; i++) {
|
||||
var o = document.createElement('option');
|
||||
o.value = String(i); o.textContent = 'คดี ' + i;
|
||||
sel.appendChild(o);
|
||||
}
|
||||
}
|
||||
|
||||
function loadEvidenceCardsPanel() {
|
||||
evidencePopulateCaseSelect();
|
||||
if (evidencePools == null) {
|
||||
fetch(EVIDENCE_IMAGES_API + '?_=' + Date.now(), { credentials: 'include', cache: 'no-store' })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (j) { evidencePools = (j && j.pools) || {}; renderEvidenceCase(evidenceCurrentCase); })
|
||||
.catch(function () { evidencePools = {}; });
|
||||
}
|
||||
fetch(EVIDENCE_CARDS_API + '?_=' + Date.now(), { credentials: 'include', cache: 'no-store' })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (j) {
|
||||
evidenceData = (j && j.cases) ? j : { cases: {} };
|
||||
var sel = el('evidence-case');
|
||||
if (sel) evidenceCurrentCase = sel.value || '1';
|
||||
renderEvidenceCase(evidenceCurrentCase);
|
||||
})
|
||||
.catch(function (e) {
|
||||
setMsg('evidence-cards-msg', (e.message || 'โหลดไม่ได้') + ' — เช็คว่า Node รัน Game/server.js และ /Game/api/evidence-cards ไม่ 404', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function evidenceGetCase(cid) {
|
||||
if (!evidenceData) evidenceData = { cases: {} };
|
||||
if (!evidenceData.cases) evidenceData.cases = {};
|
||||
if (!evidenceData.cases[cid]) evidenceData.cases[cid] = { suspects: [] };
|
||||
var cse = evidenceData.cases[cid];
|
||||
if (!Array.isArray(cse.suspects)) cse.suspects = [];
|
||||
for (var si = 0; si < 3; si++) {
|
||||
if (!cse.suspects[si]) cse.suspects[si] = {};
|
||||
var s = cse.suspects[si];
|
||||
if (!s.linkName) s.linkName = 'ผู้ต้องสงสัย ' + (si + 1);
|
||||
if (EVIDENCE_DIRS.indexOf(s.imageDir) < 0) s.imageDir = EVIDENCE_DIRS[si] || EVIDENCE_DIRS[0];
|
||||
}
|
||||
return cse;
|
||||
}
|
||||
|
||||
function evidencePoolBadge(dir) {
|
||||
var p = (evidencePools && evidencePools[dir]) || null;
|
||||
var box = document.createElement('div');
|
||||
box.className = 'ev-pool-counts';
|
||||
if (!p) { box.textContent = 'กำลังโหลดจำนวนรูป…'; return box; }
|
||||
box.innerHTML =
|
||||
'<span class="ev-pool-pill rar-common">ธรรมดา ' + (p.common || 0) + '</span>' +
|
||||
'<span class="ev-pool-pill rar-rare">Rare ' + (p.rare || 0) + '</span>' +
|
||||
'<span class="ev-pool-pill rar-legendary">ชี้คนร้าย ' + (p.legendary || 0) + '</span>';
|
||||
return box;
|
||||
}
|
||||
|
||||
function renderEvidenceCase(cid) {
|
||||
evidenceCurrentCase = cid;
|
||||
var tag = el('evidence-set-tag');
|
||||
if (tag) tag.textContent = 'กำลังแก้: คดี ' + cid;
|
||||
var root = el('evidence-suspects-root');
|
||||
if (!root) return;
|
||||
var cse = evidenceGetCase(cid);
|
||||
root.innerHTML = '';
|
||||
cse.suspects.forEach(function (s, si) {
|
||||
var block = document.createElement('div');
|
||||
block.className = 'ev-suspect-block';
|
||||
|
||||
var head = document.createElement('div');
|
||||
head.className = 'ev-suspect-head';
|
||||
head.innerHTML = '<span class="ev-suspect-no">ผู้ต้องสงสัย ' + (si + 1) + '</span>';
|
||||
block.appendChild(head);
|
||||
|
||||
var nameLabel = document.createElement('label');
|
||||
nameLabel.className = 'admin-field';
|
||||
nameLabel.textContent = 'ชื่อผู้ต้องสงสัย (Suspect Link)';
|
||||
var nameInp = document.createElement('input');
|
||||
nameInp.type = 'text';
|
||||
nameInp.maxLength = 60;
|
||||
nameInp.value = s.linkName || '';
|
||||
nameInp.addEventListener('input', function () { s.linkName = nameInp.value; });
|
||||
nameLabel.appendChild(nameInp);
|
||||
block.appendChild(nameLabel);
|
||||
|
||||
var poolBox = evidencePoolBadge(s.imageDir);
|
||||
|
||||
var dirLabel = document.createElement('label');
|
||||
dirLabel.className = 'admin-field';
|
||||
dirLabel.textContent = 'โฟลเดอร์รูปการ์ด (พูลที่ใช้สุ่ม)';
|
||||
var dirSel = document.createElement('select');
|
||||
dirSel.className = 'admin-inp-num';
|
||||
EVIDENCE_DIRS.forEach(function (d) {
|
||||
var o = document.createElement('option');
|
||||
o.value = d; o.textContent = d.replace('suspect-', 'ผู้ต้องสงสัย-');
|
||||
if (d === s.imageDir) o.selected = true;
|
||||
dirSel.appendChild(o);
|
||||
});
|
||||
dirSel.addEventListener('change', function () {
|
||||
s.imageDir = dirSel.value;
|
||||
var fresh = evidencePoolBadge(s.imageDir);
|
||||
poolBox.replaceWith(fresh);
|
||||
poolBox = fresh;
|
||||
});
|
||||
dirLabel.appendChild(dirSel);
|
||||
block.appendChild(dirLabel);
|
||||
block.appendChild(poolBox);
|
||||
|
||||
root.appendChild(block);
|
||||
});
|
||||
}
|
||||
|
||||
function saveEvidenceCardsPanel() {
|
||||
if (!evidenceData) return;
|
||||
var btn = el('btn-evidence-save');
|
||||
if (btn) btn.disabled = true;
|
||||
setMsg('evidence-cards-msg', 'กำลังบันทึก…', '');
|
||||
fetch(EVIDENCE_CARDS_API, {
|
||||
method: 'PUT',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(evidenceData),
|
||||
}).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) evidenceData = { cases: j.cases };
|
||||
renderEvidenceCase(evidenceCurrentCase);
|
||||
setMsg('evidence-cards-msg', 'บันทึกแล้ว — มีผลในเกมเมื่อรีเฟรช LobbyB', 'ok');
|
||||
}).catch(function (e) {
|
||||
setMsg('evidence-cards-msg', e.message || 'บันทึกไม่ได้', 'error');
|
||||
}).then(function () {
|
||||
if (btn) btn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
(function bindEvidenceCardsPanel() {
|
||||
evidencePopulateCaseSelect();
|
||||
var caseSel = el('evidence-case');
|
||||
if (caseSel) caseSel.addEventListener('change', function () { renderEvidenceCase(caseSel.value || '1'); });
|
||||
var saveBtn = el('btn-evidence-save');
|
||||
if (saveBtn) saveBtn.addEventListener('click', saveEvidenceCardsPanel);
|
||||
})();
|
||||
|
||||
function setTab(name) {
|
||||
document.querySelectorAll('.tab').forEach(function (t) {
|
||||
var on = t.getAttribute('data-tab') === name;
|
||||
@@ -4053,6 +4211,7 @@
|
||||
});
|
||||
if (name === 'quiz') loadQuizSettingsPanel();
|
||||
if (name === 'special-quiz') loadSpecialQuizPanel();
|
||||
if (name === 'evidence-cards') loadEvidenceCardsPanel();
|
||||
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=34">
|
||||
<link rel="stylesheet" href="admin.css?v=36">
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#admin-main">ข้ามไปเนื้อหา</a>
|
||||
@@ -83,6 +83,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>
|
||||
|
||||
<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>
|
||||
@@ -309,6 +310,28 @@
|
||||
<p id="special-quiz-msg" class="msg" role="status"></p>
|
||||
</section>
|
||||
|
||||
<section id="tab-panel-evidence-cards" class="tab-panel card" hidden role="tabpanel" aria-labelledby="tab-evidence-cards">
|
||||
<h2 class="sq-title">การ์ดหลักฐาน — แฟ้มหลักฐาน LobbyB</h2>
|
||||
<p class="muted">มี <strong>15 คดี</strong> · แต่ละคดีมี <strong>ผู้ต้องสงสัย 3 คน</strong> · ตั้ง <strong>ชื่อผู้ต้องสงสัย</strong> + เลือก <strong>โฟลเดอร์รูป</strong> (suspect-1/2/3) ได้ · การ์ดเป็น<strong>รูปอย่างเดียว</strong> โดยระดับ (rarity) คิดอัตโนมัติจากเลขไฟล์: <code>Card-01–22 = ธรรมดา</code>, <code>Card-23–44 = Rare</code>, <code>Card-45–54 = ชี้คนร้าย</code> · ตอนจบมินิเกมระบบจะ<strong>สุ่มการ์ดตามเกรดของทีม</strong> (A/B/C) · เก็บที่ <code>/Game/data/evidence-cards.json</code></p>
|
||||
|
||||
<fieldset class="sq-fieldset sq-pick">
|
||||
<legend>เลือกคดีที่จะแก้</legend>
|
||||
<div class="sq-pick-row">
|
||||
<label class="admin-field">คดี (Case)
|
||||
<select id="evidence-case" class="admin-inp-num sq-select"></select>
|
||||
</label>
|
||||
<span id="evidence-set-tag" class="sq-set-tag">กำลังแก้: คดี 1</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div id="evidence-suspects-root"></div>
|
||||
|
||||
<div class="quiz-admin-actions">
|
||||
<button type="button" class="btn btn-primary" id="btn-evidence-save">บันทึกการ์ดหลักฐานทั้งหมด (15 คดี)</button>
|
||||
</div>
|
||||
<p id="evidence-cards-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>
|
||||
@@ -1013,6 +1036,6 @@
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<script src="admin.js?v=75"></script>
|
||||
<script src="admin.js?v=77"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
{
|
||||
"cases": {
|
||||
"1": {
|
||||
"suspects": [
|
||||
{
|
||||
"linkName": "สมชาย",
|
||||
"imageDir": "suspect-1"
|
||||
},
|
||||
{
|
||||
"linkName": "สมหญิง",
|
||||
"imageDir": "suspect-2"
|
||||
},
|
||||
{
|
||||
"linkName": "วิชัย",
|
||||
"imageDir": "suspect-3"
|
||||
}
|
||||
]
|
||||
},
|
||||
"2": {
|
||||
"suspects": [
|
||||
{
|
||||
"linkName": "เปี๊ยก",
|
||||
"imageDir": "suspect-1"
|
||||
},
|
||||
{
|
||||
"linkName": "มาลี",
|
||||
"imageDir": "suspect-2"
|
||||
},
|
||||
{
|
||||
"linkName": "เสก",
|
||||
"imageDir": "suspect-3"
|
||||
}
|
||||
]
|
||||
},
|
||||
"3": {
|
||||
"suspects": [
|
||||
{
|
||||
"linkName": "ธนา",
|
||||
"imageDir": "suspect-1"
|
||||
},
|
||||
{
|
||||
"linkName": "รุ้ง",
|
||||
"imageDir": "suspect-2"
|
||||
},
|
||||
{
|
||||
"linkName": "ภูผา",
|
||||
"imageDir": "suspect-3"
|
||||
}
|
||||
]
|
||||
},
|
||||
"4": {
|
||||
"suspects": [
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 1",
|
||||
"imageDir": "suspect-1"
|
||||
},
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 2",
|
||||
"imageDir": "suspect-2"
|
||||
},
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 3",
|
||||
"imageDir": "suspect-3"
|
||||
}
|
||||
]
|
||||
},
|
||||
"5": {
|
||||
"suspects": [
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 1",
|
||||
"imageDir": "suspect-1"
|
||||
},
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 2",
|
||||
"imageDir": "suspect-2"
|
||||
},
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 3",
|
||||
"imageDir": "suspect-3"
|
||||
}
|
||||
]
|
||||
},
|
||||
"6": {
|
||||
"suspects": [
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 1",
|
||||
"imageDir": "suspect-1"
|
||||
},
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 2",
|
||||
"imageDir": "suspect-2"
|
||||
},
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 3",
|
||||
"imageDir": "suspect-3"
|
||||
}
|
||||
]
|
||||
},
|
||||
"7": {
|
||||
"suspects": [
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 1",
|
||||
"imageDir": "suspect-1"
|
||||
},
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 2",
|
||||
"imageDir": "suspect-2"
|
||||
},
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 3",
|
||||
"imageDir": "suspect-3"
|
||||
}
|
||||
]
|
||||
},
|
||||
"8": {
|
||||
"suspects": [
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 1",
|
||||
"imageDir": "suspect-1"
|
||||
},
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 2",
|
||||
"imageDir": "suspect-2"
|
||||
},
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 3",
|
||||
"imageDir": "suspect-3"
|
||||
}
|
||||
]
|
||||
},
|
||||
"9": {
|
||||
"suspects": [
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 1",
|
||||
"imageDir": "suspect-1"
|
||||
},
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 2",
|
||||
"imageDir": "suspect-2"
|
||||
},
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 3",
|
||||
"imageDir": "suspect-3"
|
||||
}
|
||||
]
|
||||
},
|
||||
"10": {
|
||||
"suspects": [
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 1",
|
||||
"imageDir": "suspect-1"
|
||||
},
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 2",
|
||||
"imageDir": "suspect-2"
|
||||
},
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 3",
|
||||
"imageDir": "suspect-3"
|
||||
}
|
||||
]
|
||||
},
|
||||
"11": {
|
||||
"suspects": [
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 1",
|
||||
"imageDir": "suspect-1"
|
||||
},
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 2",
|
||||
"imageDir": "suspect-2"
|
||||
},
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 3",
|
||||
"imageDir": "suspect-3"
|
||||
}
|
||||
]
|
||||
},
|
||||
"12": {
|
||||
"suspects": [
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 1",
|
||||
"imageDir": "suspect-1"
|
||||
},
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 2",
|
||||
"imageDir": "suspect-2"
|
||||
},
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 3",
|
||||
"imageDir": "suspect-3"
|
||||
}
|
||||
]
|
||||
},
|
||||
"13": {
|
||||
"suspects": [
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 1",
|
||||
"imageDir": "suspect-1"
|
||||
},
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 2",
|
||||
"imageDir": "suspect-2"
|
||||
},
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 3",
|
||||
"imageDir": "suspect-3"
|
||||
}
|
||||
]
|
||||
},
|
||||
"14": {
|
||||
"suspects": [
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 1",
|
||||
"imageDir": "suspect-1"
|
||||
},
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 2",
|
||||
"imageDir": "suspect-2"
|
||||
},
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 3",
|
||||
"imageDir": "suspect-3"
|
||||
}
|
||||
]
|
||||
},
|
||||
"15": {
|
||||
"suspects": [
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 1",
|
||||
"imageDir": "suspect-1"
|
||||
},
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 2",
|
||||
"imageDir": "suspect-2"
|
||||
},
|
||||
{
|
||||
"linkName": "ผู้ต้องสงสัย 3",
|
||||
"imageDir": "suspect-3"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,6 +208,22 @@
|
||||
box-shadow: 0 0 28px rgba(241, 185, 74, 0.55);
|
||||
}
|
||||
|
||||
/* การ์ดแบบรูปเต็มใบ (PNG design) — แทน HTML chrome ทั้งหมด */
|
||||
#lobby-evidence-overlay .ev-card.ev-card--img {
|
||||
background: transparent;
|
||||
border: none !important;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
#lobby-evidence-overlay .ev-card.ev-card--img .ev-card-full-img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
#lobby-evidence-overlay .ev-card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -381,6 +381,29 @@
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
/* การ์ดแบบรูปเต็มใบ (PNG design) — ห้องสรุปหลักฐาน + เปิดเผยผล */
|
||||
#testimony-overlay .es3-card.es3-card--img,
|
||||
#testimony-overlay .es-reveal-wrap .es-card.es-card--img {
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
#testimony-overlay .es3-card.es3-card--img .es3-card-full-img,
|
||||
#testimony-overlay .es-reveal-wrap .es-card.es-card--img .es-card-full-img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 12px;
|
||||
}
|
||||
#testimony-overlay .es3-card.es3-card--img.is-selected {
|
||||
outline: 4px solid #9ece6a;
|
||||
outline-offset: -3px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
#testimony-overlay .es3-card {
|
||||
position: relative;
|
||||
/* คงสัดส่วน portrait — ไม่ยืดเต็มจอเมื่อเก็บได้แค่ใบเดียว; 252×380 stage px ตรงตามดีไซน์ */
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/var/www/html/onlydesign/Card หลักฐาน/ผู้ต้องสงสัย-1
|
||||
@@ -0,0 +1 @@
|
||||
/var/www/html/onlydesign/Card หลักฐาน/ผู้ต้องสงสัย-2
|
||||
@@ -0,0 +1 @@
|
||||
/var/www/html/onlydesign/Card หลักฐาน/ผู้ต้องสงสัย-3
|
||||
@@ -335,23 +335,103 @@
|
||||
if (hasAny) sessionStorage.setItem('justiceDetectiveMyEvidence', JSON.stringify(res.myPlayerEvidence));
|
||||
else sessionStorage.removeItem('justiceDetectiveMyEvidence');
|
||||
}
|
||||
/* โชว์การ์ดหลักฐานที่เพิ่งได้ ตอนกลับถึง LobbyB (เปิดแฟ้มอัตโนมัติ + ไฮไลต์ใบใหม่) */
|
||||
if (res && typeof res.suspectIndex === 'number' && res.suspectIndex >= 0) {
|
||||
sessionStorage.setItem('justiceEvidenceReveal', JSON.stringify({
|
||||
suspectIndex: res.suspectIndex,
|
||||
awardedCard: (typeof res.awardedCard === 'number') ? res.awardedCard : null,
|
||||
freeEvidenceCount: res.freeEvidenceCount || 0,
|
||||
ts: Date.now(),
|
||||
}));
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
go();
|
||||
/* โชว์การ์ดหลักฐานที่เพิ่งได้ "ในฉากมินิเกม" ก่อนกลับ LobbyB */
|
||||
if (res && typeof res.suspectIndex === 'number' && res.suspectIndex >= 0) {
|
||||
showInSceneEvidenceReveal(res, go);
|
||||
} else {
|
||||
go();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
go();
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== หน้าโชว์การ์ดหลักฐานในฉากมินิเกม (ก่อนกลับ LobbyB) ===== */
|
||||
var __evidenceConfigCache = null;
|
||||
function ensureEvidenceConfigLoaded(cb) {
|
||||
if (__evidenceConfigCache) { cb(__evidenceConfigCache); return; }
|
||||
try {
|
||||
fetch(BASE + '/api/evidence-cards?_=' + Date.now(), { cache: 'no-store' })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (j) { __evidenceConfigCache = (j && j.cases) ? j : { cases: {} }; cb(__evidenceConfigCache); })
|
||||
.catch(function () { cb({ cases: {} }); });
|
||||
} catch (e) { cb({ cases: {} }); }
|
||||
}
|
||||
|
||||
var EVIDENCE_RARITY_GLOW = {
|
||||
common: 'rgba(168,180,200,.55)',
|
||||
rare: 'rgba(94,170,255,.7)',
|
||||
legendary: 'rgba(255,186,72,.85)',
|
||||
};
|
||||
var EVIDENCE_RARITY_LABEL = { common: 'การ์ดธรรมดา', rare: 'การ์ด Rare', legendary: 'การ์ดชี้คนร้าย' };
|
||||
|
||||
function showInSceneEvidenceReveal(res, onDone) {
|
||||
var done = false;
|
||||
var finish = function () { if (done) return; done = true; onDone(); };
|
||||
var si = res.suspectIndex;
|
||||
/* การ์ดที่เพิ่งได้รอบนี้ (object: rarity/imageUrl) — fallback ใช้ N ใบท้ายของ owned */
|
||||
var awarded = (res.awardedCards && Array.isArray(res.awardedCards))
|
||||
? res.awardedCards.filter(function (c) { return c && c.imageUrl; })
|
||||
: [];
|
||||
if (!awarded.length) {
|
||||
var owned = (res.myPlayerEvidence && res.myPlayerEvidence[si]) || [];
|
||||
var freeN = Math.max(0, Number(res.freeEvidenceCount) || 0);
|
||||
var newN = Math.min(owned.length, 1 + freeN);
|
||||
awarded = owned.slice(owned.length - newN).filter(function (c) { return c && c.imageUrl; });
|
||||
}
|
||||
if (!awarded.length) { finish(); return; }
|
||||
var newN = awarded.length;
|
||||
var ov = document.createElement('div');
|
||||
ov.id = 'inscene-evidence-reveal';
|
||||
ov.style.cssText = 'position:fixed;inset:0;z-index:99999;background:rgba(5,8,18,.9);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:22px;backdrop-filter:blur(3px)';
|
||||
var title = document.createElement('div');
|
||||
title.style.cssText = 'font:900 30px/1.2 Kanit,system-ui,sans-serif;color:#ffe7a3;text-shadow:0 0 18px rgba(255,214,102,.5);text-align:center';
|
||||
title.textContent = newN > 1 ? ('คุณได้รับหลักฐาน ' + newN + ' ใบ!') : 'คุณได้รับหลักฐาน!';
|
||||
if (res.grade) {
|
||||
var gradeEl = document.createElement('div');
|
||||
gradeEl.style.cssText = 'font:800 16px/1 Kanit,system-ui,sans-serif;color:#bfe3ff;opacity:.9';
|
||||
gradeEl.textContent = 'ผลงานทีม: เกรด ' + res.grade;
|
||||
title.appendChild(document.createElement('br'));
|
||||
title.appendChild(gradeEl);
|
||||
}
|
||||
var row = document.createElement('div');
|
||||
row.style.cssText = 'display:flex;gap:18px;flex-wrap:wrap;justify-content:center;align-items:center;max-width:92vw';
|
||||
awarded.forEach(function (c, i) {
|
||||
var rar = String(c.rarity || 'common').toLowerCase();
|
||||
var glow = EVIDENCE_RARITY_GLOW[rar] || EVIDENCE_RARITY_GLOW.common;
|
||||
var cardWrap = document.createElement('div');
|
||||
cardWrap.style.cssText = 'display:flex;flex-direction:column;align-items:center;gap:8px;opacity:0;transform:translateY(24px) scale(.92);transition:opacity .45s ease,transform .45s cubic-bezier(.2,.9,.3,1.3)';
|
||||
var img = document.createElement('img');
|
||||
img.src = c.imageUrl;
|
||||
img.alt = EVIDENCE_RARITY_LABEL[rar] || 'หลักฐาน';
|
||||
img.style.cssText = 'display:block;width:min(34vw,260px);height:auto;border-radius:14px;box-shadow:0 0 36px ' + glow + ',0 18px 50px rgba(0,0,0,.6)';
|
||||
img.onerror = function () { this.onerror = null; this.style.width = '200px'; this.style.height = '300px'; this.style.background = '#1b2240'; this.removeAttribute('src'); };
|
||||
var tag = document.createElement('div');
|
||||
tag.style.cssText = 'font:800 14px/1 Kanit,system-ui,sans-serif;color:#fff;opacity:.92';
|
||||
tag.textContent = EVIDENCE_RARITY_LABEL[rar] || '';
|
||||
cardWrap.appendChild(img);
|
||||
cardWrap.appendChild(tag);
|
||||
row.appendChild(cardWrap);
|
||||
setTimeout(function () { cardWrap.style.opacity = '1'; cardWrap.style.transform = 'translateY(0) scale(1)'; }, 120 + i * 160);
|
||||
});
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.textContent = 'คุณได้หลักฐาน · เก็บเข้าแฟ้ม';
|
||||
btn.style.cssText = 'cursor:pointer;border:none;border-radius:999px;padding:14px 40px;font:800 18px/1 Kanit,system-ui,sans-serif;color:#0a0e1a;background:linear-gradient(180deg,#ffd666,#f0b429);box-shadow:0 10px 30px rgba(240,180,41,.45)';
|
||||
btn.addEventListener('click', function () {
|
||||
ov.style.transition = 'opacity .25s'; ov.style.opacity = '0';
|
||||
setTimeout(finish, 220);
|
||||
});
|
||||
ov.appendChild(title);
|
||||
ov.appendChild(row);
|
||||
ov.appendChild(btn);
|
||||
document.body.appendChild(ov);
|
||||
/* กันค้าง: ถ้าไม่กดภายใน 12 วิ ให้ไปต่อเอง */
|
||||
setTimeout(function () { if (!done) { try { ov.remove(); } catch (e) {} finish(); } }, 12000);
|
||||
}
|
||||
|
||||
/** จบมินิเกมโหมดสืบสวน → LobbyB; คืน true ถ้า redirect แล้ว */
|
||||
function tryFinishDetectiveMinigameAndReturnLobby() {
|
||||
if (!isDetectiveMinigamePlay()) return false;
|
||||
@@ -11367,6 +11447,7 @@
|
||||
try { return localStorage.getItem('justiceTestMode') === '1'; } catch (e) { return false; }
|
||||
}
|
||||
let specialQuizIcon = null; // { iconType, x, y } (พิกัด tile-center)
|
||||
let specialQuizIconScreenRect = null; // { x, y, r } พิกัดบนจอ (ไว้คลิก/แตะไอคอน)
|
||||
let specialQuizCollideSent = false;
|
||||
let specialQuizAwardedCard = null; // การ์ดที่ได้รอบนี้ (จาก special-quiz-ended)
|
||||
let specialQuizActiveQuestion = null; // { index, total, choices, endsAt, iconType }
|
||||
@@ -11429,9 +11510,28 @@
|
||||
}
|
||||
function clearSpecialQuizIcon() {
|
||||
specialQuizIcon = null;
|
||||
specialQuizIconScreenRect = null;
|
||||
specialQuizCollideSent = false;
|
||||
}
|
||||
|
||||
/** กด/แตะไอคอนควิซเพื่อเปิดคำถาม (ใช้ได้ทุกมินิเกม รวม space shooter ที่เดินไปแตะไม่ได้) */
|
||||
function trySpecialQuizClickAt(clientX, clientY) {
|
||||
if (!specialQuizIcon || specialQuizCollideSent || specialQuizActiveQuestion) return false;
|
||||
if (!specialQuizIconScreenRect || !canvas) return false;
|
||||
const r = canvas.getBoundingClientRect();
|
||||
if (!r.width || !r.height) return false;
|
||||
const px = (clientX - r.left) * (canvas.width / r.width);
|
||||
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 */ }
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** วาดไอคอนในฉาก — ใช้ worldToScreen + zDraw จาก draw() เพื่อให้ตรงทุกมินิเกม */
|
||||
function drawSpecialQuizIconWorld(w2s, zDraw) {
|
||||
if (!specialQuizIcon || typeof w2s !== 'function') return;
|
||||
@@ -11452,6 +11552,8 @@
|
||||
ctx.beginPath();
|
||||
ctx.arc(sx, cy, r, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
/* เก็บตำแหน่งบนจอไว้ให้กด/แตะไอคอนได้ (สำหรับมินิเกมที่เดินไปแตะไม่ได้ เช่น space shooter) */
|
||||
specialQuizIconScreenRect = { x: sx, y: cy, r: Math.max(size * 0.7, 36) };
|
||||
if (img && img.complete && img.naturalWidth > 0) {
|
||||
ctx.drawImage(img, sx - size / 2, cy - size / 2, size, size);
|
||||
} else {
|
||||
@@ -16649,6 +16751,14 @@
|
||||
stack.addEventListener('wheel', wheelZoomHandler, { passive: false, capture: true });
|
||||
})();
|
||||
|
||||
/* กด/แตะไอคอนควิซพิเศษ — ทำงานทุกมินิเกม (กันเคสเดินไปแตะไม่ได้ เช่น space shooter) */
|
||||
canvas.addEventListener('pointerdown', (e) => {
|
||||
if (trySpecialQuizClickAt(e.clientX, e.clientY)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}, true);
|
||||
|
||||
canvas.addEventListener('dblclick', (e) => {
|
||||
if (!mapData || isFrogger() || isGauntlet() || isStack() || isJumpSurvive() || isSpaceShooter() || isBalloonBoss() || isChatFocused() || isQuizCarryEmbedCountdownBlockingMovement()) return;
|
||||
const r = canvas.getBoundingClientRect();
|
||||
|
||||
@@ -104,6 +104,26 @@
|
||||
}
|
||||
};
|
||||
|
||||
/* การ์ดหลักฐานจัดการผ่าน Admin (server) — โหลดทับ default ด้านบนเมื่อพร้อม */
|
||||
var lobbyEvidenceCasesRemote = null;
|
||||
function loadEvidenceCardsConfig() {
|
||||
try {
|
||||
fetch(SERVER + '/api/evidence-cards', { cache: 'no-store' })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (j) {
|
||||
if (j && j.cases) {
|
||||
lobbyEvidenceCasesRemote = j.cases;
|
||||
var ov = document.getElementById('lobby-evidence-overlay');
|
||||
if (ov && typeof lobbyEvidenceSuspectIdx === 'number') {
|
||||
try { renderLobbyEvidenceCards(lobbyEvidenceSuspectIdx); } catch (e) { /* ignore */ }
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function () { /* ใช้ default */ });
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
loadEvidenceCardsConfig();
|
||||
|
||||
const socketOrigin = (typeof GAME_NODE_ORIGIN !== 'undefined' && GAME_NODE_ORIGIN)
|
||||
? GAME_NODE_ORIGIN
|
||||
: (typeof GAME_SERVER !== 'undefined' ? GAME_SERVER : undefined);
|
||||
@@ -2849,8 +2869,19 @@
|
||||
if (res.cardMinigames) setSuspectCardMinigames(res.cardMinigames);
|
||||
applyPersonalSuspectState(res);
|
||||
serverSuspectPhaseActive = !!res.suspectPhaseActive;
|
||||
/* กลับ LobbyB ด้วยการ reload (full navigation) — โชว์การ์ดหลักฐานที่เพิ่งได้จาก sessionStorage */
|
||||
var revealedOnJoin = false;
|
||||
var hasPendingReveal = false;
|
||||
try { hasPendingReveal = !!sessionStorage.getItem('justiceEvidenceReveal'); } catch (eRev) { hasPendingReveal = false; }
|
||||
if (hasPendingReveal && (clientLobbyMapId === POST_CASE_LOBBY_SPACE_ID || res.suspectPhaseActive)) {
|
||||
revealedOnJoin = showEvidenceRevealOnReturn();
|
||||
}
|
||||
if (res.suspectPhaseActive) {
|
||||
openSuspectOverlay(res.suspectPickIndex != null ? res.suspectPickIndex : 0);
|
||||
if (revealedOnJoin) {
|
||||
setTimeout(function () { openSuspectOverlay(res.suspectPickIndex != null ? res.suspectPickIndex : 0); }, 1600);
|
||||
} else {
|
||||
openSuspectOverlay(res.suspectPickIndex != null ? res.suspectPickIndex : 0);
|
||||
}
|
||||
} else {
|
||||
updateSuspectFloatingOpenBtn();
|
||||
}
|
||||
@@ -3788,10 +3819,12 @@
|
||||
try {
|
||||
const meta = window.__detectiveLobbyMeta;
|
||||
let cid = meta && meta.caseId != null ? String(meta.caseId).trim() : '1';
|
||||
if (!/^[123]$/.test(cid)) cid = '1';
|
||||
var n = parseInt(cid, 10);
|
||||
if (!(n >= 1 && n <= 15)) cid = '1'; else cid = String(n);
|
||||
if (lobbyEvidenceCasesRemote && lobbyEvidenceCasesRemote[cid]) return lobbyEvidenceCasesRemote[cid];
|
||||
return LOBBY_EVIDENCE_CASES[cid] || LOBBY_EVIDENCE_CASES['1'];
|
||||
} catch (e) {
|
||||
return LOBBY_EVIDENCE_CASES['1'];
|
||||
return (lobbyEvidenceCasesRemote && lobbyEvidenceCasesRemote['1']) || LOBBY_EVIDENCE_CASES['1'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3863,7 +3896,71 @@
|
||||
return lines.join('<br>');
|
||||
}
|
||||
|
||||
function buildEvHtmlCard(card, c, linkName, cardIdx) {
|
||||
var rar = String(c.rarity || 'common').toLowerCase();
|
||||
if (!LOBBY_EVIDENCE_RARITY[rar]) rar = 'common';
|
||||
var nStar = Math.max(1, Math.min(3, Number(c.stars) || 1));
|
||||
var stars = '';
|
||||
for (var st = 0; st < nStar; st++) stars += '★';
|
||||
var nm = linkName || '?';
|
||||
var icon = LOBBY_EVIDENCE_CARD_ICONS[cardIdx] || '🔎';
|
||||
card.classList.remove('ev-card--img');
|
||||
card.innerHTML =
|
||||
'<div class="ev-card-head">' +
|
||||
'<div class="icon" aria-hidden="true"></div>' +
|
||||
'<div class="name">' +
|
||||
'<div class="th"></div>' +
|
||||
'<div class="en"></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="ev-card-art" aria-hidden="true">[การ์ดยังไม่มี PNG]</div>' +
|
||||
'<div class="ev-card-desc"></div>' +
|
||||
'<div class="ev-card-link-wrap">' +
|
||||
'<div class="ev-card-link">' +
|
||||
'<span class="label">Suspect Link</span>' +
|
||||
'<span class="stars"></span>' +
|
||||
'</div>' +
|
||||
'<div class="ev-card-link">' +
|
||||
'<div class="suspect">' +
|
||||
'<div class="avatar"></div>' +
|
||||
'<div class="sname"></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
card.querySelector('.icon').textContent = icon;
|
||||
card.querySelector('.th').textContent = c.titleTh || '';
|
||||
card.querySelector('.en').textContent = c.titleEn ? '(' + c.titleEn + ')' : '';
|
||||
card.querySelector('.ev-card-desc').innerHTML = formatLobbyEvCardDescHtml(c.body);
|
||||
card.querySelector('.stars').textContent = stars + ' (' + LOBBY_EVIDENCE_RARITY[rar] + ')';
|
||||
card.querySelector('.avatar').textContent = nm.charAt(0);
|
||||
card.querySelector('.sname').textContent = nm;
|
||||
}
|
||||
|
||||
function evLobbyEvidenceCardEl(c, linkName, cardIdx) {
|
||||
var rar = String(c.rarity || 'common').toLowerCase();
|
||||
if (!LOBBY_EVIDENCE_RARITY[rar]) rar = 'common';
|
||||
var card = document.createElement('article');
|
||||
card.className = 'ev-card rarity-' + rar;
|
||||
/* การ์ดออกแบบเป็น PNG เต็มใบ — แสดงรูปแทน HTML chrome (มี fallback ถ้ารูปโหลดไม่ได้) */
|
||||
if (c && c.imageUrl) {
|
||||
card.classList.add('ev-card--img');
|
||||
var fullImg = document.createElement('img');
|
||||
fullImg.className = 'ev-card-full-img';
|
||||
fullImg.alt = (c.titleTh || '') + (c.titleEn ? ' (' + c.titleEn + ')' : '');
|
||||
fullImg.decoding = 'async';
|
||||
fullImg.onerror = function () {
|
||||
this.onerror = null;
|
||||
buildEvHtmlCard(card, c, linkName, cardIdx);
|
||||
};
|
||||
fullImg.src = c.imageUrl;
|
||||
card.appendChild(fullImg);
|
||||
return card;
|
||||
}
|
||||
buildEvHtmlCard(card, c, linkName, cardIdx);
|
||||
return card;
|
||||
}
|
||||
|
||||
function evLobbyEvidenceCardElLegacy(c, linkName, cardIdx) {
|
||||
var rar = String(c.rarity || 'common').toLowerCase();
|
||||
if (!LOBBY_EVIDENCE_RARITY[rar]) rar = 'common';
|
||||
var nStar = Math.max(1, Math.min(3, Number(c.stars) || 1));
|
||||
@@ -3907,22 +4004,25 @@
|
||||
|
||||
var DETECTIVE_EVIDENCE_STORAGE_KEY = 'justiceDetectiveMyEvidence';
|
||||
|
||||
/* แถวหลักฐาน = array ของ card object {rarity, stars, num, imageUrl} (ไม่จำกัดจำนวน) */
|
||||
function normalizeEvidenceSlotList(cards) {
|
||||
if (!Array.isArray(cards)) return [];
|
||||
var n = Math.min(3, cards.length);
|
||||
var out = [];
|
||||
for (var i = 0; i < n; i++) out.push(i);
|
||||
for (var i = 0; i < cards.length; i++) {
|
||||
var c = cards[i];
|
||||
if (c && typeof c === 'object' && c.imageUrl) out.push(c);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function getSuspectEvidenceCount(si) {
|
||||
var i = Math.floor(Number(si));
|
||||
if (Number.isNaN(i) || i < 0 || i > 2) return 0;
|
||||
return Math.min(3, (myPlayerEvidence[i] || []).length);
|
||||
return (myPlayerEvidence[i] || []).length;
|
||||
}
|
||||
|
||||
function canInvestigateSuspect(si) {
|
||||
return getSuspectEvidenceCount(si) < 3;
|
||||
return true; // เก็บได้ไม่จำกัด
|
||||
}
|
||||
|
||||
function persistMyPlayerEvidenceSession() {
|
||||
@@ -3936,9 +4036,7 @@
|
||||
function applyMyPlayerEvidence(data) {
|
||||
if (!Array.isArray(data)) return;
|
||||
myPlayerEvidence = [0, 1, 2].map(function (si) {
|
||||
var row = data[si];
|
||||
if (!Array.isArray(row)) return [];
|
||||
return normalizeEvidenceSlotList(row);
|
||||
return normalizeEvidenceSlotList(data[si]);
|
||||
});
|
||||
persistMyPlayerEvidenceSession();
|
||||
}
|
||||
@@ -3964,7 +4062,7 @@
|
||||
}
|
||||
if (data && Array.isArray(data.suspectProgress)) setSuspectProgress(data.suspectProgress);
|
||||
else if (myPlayerEvidence && myPlayerEvidence.some(function (row) { return row && row.length; })) {
|
||||
setSuspectProgress([0, 1, 2].map(function (i) { return Math.min(3, (myPlayerEvidence[i] || []).length); }));
|
||||
setSuspectProgress([0, 1, 2].map(function (i) { return (myPlayerEvidence[i] || []).length; }));
|
||||
}
|
||||
applySuspectProgressVisual();
|
||||
/* อัปเดต UI host เร็วทันที — ปุ่ม "ชี้ตัวคนร้าย" ต้องโผล่ทันทีเมื่อสืบครบ */
|
||||
@@ -3981,13 +4079,12 @@
|
||||
const payload = getLobbyEvidenceCasePayload();
|
||||
const suspects = payload.suspects || [];
|
||||
const si = Math.max(0, Math.min(suspects.length - 1, suspectIdx));
|
||||
const s = suspects[si];
|
||||
const s = suspects[si] || {};
|
||||
root.textContent = '';
|
||||
if (!s || !s.cards) return;
|
||||
var count = getSuspectEvidenceCount(si);
|
||||
for (var slot = 0; slot < count; slot++) {
|
||||
if (!s.cards[slot]) continue;
|
||||
root.appendChild(evLobbyEvidenceCardEl(s.cards[slot], s.linkName, slot));
|
||||
var owned = (myPlayerEvidence[si] || []);
|
||||
for (var slot = 0; slot < owned.length; slot++) {
|
||||
if (!owned[slot]) continue;
|
||||
root.appendChild(evLobbyEvidenceCardEl(owned[slot], s.linkName, slot));
|
||||
}
|
||||
if (!root.children.length) {
|
||||
const empty = document.createElement('p');
|
||||
@@ -4318,12 +4415,7 @@
|
||||
var card = document.querySelector('.suspect-card[data-index="' + i + '"]');
|
||||
if (!card) continue;
|
||||
var count = getSuspectEvidenceCount(i);
|
||||
if (!count && suspectProgress && suspectProgress[i] > 0) {
|
||||
count = Math.min(3, suspectProgress[i]);
|
||||
myPlayerEvidence[i] = normalizeEvidenceSlotList(
|
||||
(myPlayerEvidence[i] || []).length ? myPlayerEvidence[i] : new Array(count)
|
||||
);
|
||||
}
|
||||
if (!count && suspectProgress && suspectProgress[i] > 0) count = suspectProgress[i];
|
||||
card.classList.toggle('suspect-card--evidence-full', count >= 3);
|
||||
var list = card.querySelector('.suspect-symbol-list');
|
||||
if (!list) continue;
|
||||
@@ -4702,16 +4794,13 @@
|
||||
/* ===== ห้องสรุปหลักฐาน — การไต่สวน (เลือกหลักฐาน 2 ใบ/ปากคำ ก่อนโหวต) ===== */
|
||||
var tmState = { round: 0, members: [], picks: [], submitted: false, mode: 'select', submittedPicksById: {}, ownedSlots: [], requiredPicks: 2, hostId: '', submittedIds: [], readyIds: [], iReadied: false };
|
||||
|
||||
/** สล็อตหลักฐานที่ผู้เล่นเก็บได้สำหรับ suspect ตามรอบ (เช่น [0,2]) */
|
||||
/** ตำแหน่งการ์ดที่ผู้เล่นเก็บได้สำหรับ suspect ตามรอบ — index เข้าลิสต์ owned (0..n-1) */
|
||||
function tmOwnedSlotsForRound(round) {
|
||||
var i = Math.floor(Number(round));
|
||||
if (Number.isNaN(i) || i < 0 || i > 2) i = 0;
|
||||
var row = (myPlayerEvidence && myPlayerEvidence[i]) || [];
|
||||
var out = [];
|
||||
row.forEach(function (s) {
|
||||
var v = Math.floor(Number(s));
|
||||
if (v >= 0 && v <= 2 && out.indexOf(v) === -1) out.push(v);
|
||||
});
|
||||
for (var k = 0; k < row.length; k++) if (row[k] && row[k].imageUrl) out.push(k);
|
||||
return out;
|
||||
}
|
||||
var SUSPECT_NAMES_EN = ['SOMCHAI', 'ARIN', 'JIRAPHA'];
|
||||
@@ -4753,7 +4842,7 @@
|
||||
window.addEventListener('orientationchange', syncTestimonyScale);
|
||||
}
|
||||
|
||||
function es3EvidenceCardEl(c, linkName, cardIndex, selectable) {
|
||||
function buildEs3HtmlCard(card, c, linkName, cardIndex) {
|
||||
var rar = String(c.rarity || 'common').toLowerCase();
|
||||
if (!LOBBY_EVIDENCE_RARITY[rar]) rar = 'common';
|
||||
var nStar = Math.max(1, Math.min(3, Number(c.stars) || 1));
|
||||
@@ -4761,9 +4850,7 @@
|
||||
for (var st = 0; st < nStar; st++) stars += '★';
|
||||
var nm = linkName || '?';
|
||||
var icon = LOBBY_EVIDENCE_CARD_ICONS[cardIndex] || '🔎';
|
||||
var card = document.createElement('article');
|
||||
card.className = 'es3-card';
|
||||
if (selectable) card.setAttribute('data-card-index', String(cardIndex));
|
||||
card.classList.remove('es3-card--img');
|
||||
card.innerHTML =
|
||||
'<div class="es3-card-head"><div class="ico" aria-hidden="true"></div>' +
|
||||
'<div class="nm"><div class="th"></div><div class="en"></div></div></div>' +
|
||||
@@ -4777,6 +4864,24 @@
|
||||
card.querySelector('.avatar').textContent = nm.charAt(0);
|
||||
card.querySelector('.sname').textContent = nm;
|
||||
card.querySelector('.stars').textContent = stars + ' (' + LOBBY_EVIDENCE_RARITY[rar] + ')';
|
||||
}
|
||||
|
||||
function es3EvidenceCardEl(c, linkName, cardIndex, selectable) {
|
||||
var card = document.createElement('article');
|
||||
card.className = 'es3-card';
|
||||
if (selectable) card.setAttribute('data-card-index', String(cardIndex));
|
||||
if (c && c.imageUrl) {
|
||||
card.classList.add('es3-card--img');
|
||||
var img = document.createElement('img');
|
||||
img.className = 'es3-card-full-img';
|
||||
img.alt = (c.titleTh || '') + (c.titleEn ? ' (' + c.titleEn + ')' : '');
|
||||
img.decoding = 'async';
|
||||
img.onerror = function () { this.onerror = null; buildEs3HtmlCard(card, c, linkName, cardIndex); };
|
||||
img.src = c.imageUrl;
|
||||
card.appendChild(img);
|
||||
return card;
|
||||
}
|
||||
buildEs3HtmlCard(card, c, linkName, cardIndex);
|
||||
return card;
|
||||
}
|
||||
|
||||
@@ -4983,13 +5088,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
function esRevealCardEl(c, linkName, cardIdx) {
|
||||
var rar = String(c.rarity || 'common').toLowerCase();
|
||||
if (!LOBBY_EVIDENCE_RARITY[rar]) rar = 'common';
|
||||
function buildEsRevealHtmlCard(card, c, linkName, cardIdx) {
|
||||
var nm = linkName || '?';
|
||||
var icon = LOBBY_EVIDENCE_CARD_ICONS[cardIdx] || '🔎';
|
||||
var card = document.createElement('div');
|
||||
card.className = 'es-card rarity-' + rar;
|
||||
card.classList.remove('es-card--img');
|
||||
card.innerHTML =
|
||||
'<div class="es-card-head"><div class="ico" aria-hidden="true"></div>' +
|
||||
'<div class="nm"><div class="th"></div><div class="en"></div></div></div>' +
|
||||
@@ -5002,6 +5104,25 @@
|
||||
card.querySelector('.es-card-desc').textContent = c.body || '';
|
||||
card.querySelector('.avatar').textContent = nm.charAt(0);
|
||||
card.querySelector('.sname').textContent = nm;
|
||||
}
|
||||
|
||||
function esRevealCardEl(c, linkName, cardIdx) {
|
||||
var rar = String(c.rarity || 'common').toLowerCase();
|
||||
if (!LOBBY_EVIDENCE_RARITY[rar]) rar = 'common';
|
||||
var card = document.createElement('div');
|
||||
card.className = 'es-card rarity-' + rar;
|
||||
if (c && c.imageUrl) {
|
||||
card.classList.add('es-card--img');
|
||||
var img = document.createElement('img');
|
||||
img.className = 'es-card-full-img';
|
||||
img.alt = (c.titleTh || '') + (c.titleEn ? ' (' + c.titleEn + ')' : '');
|
||||
img.decoding = 'async';
|
||||
img.onerror = function () { this.onerror = null; buildEsRevealHtmlCard(card, c, linkName, cardIdx); };
|
||||
img.src = c.imageUrl;
|
||||
card.appendChild(img);
|
||||
return card;
|
||||
}
|
||||
buildEsRevealHtmlCard(card, c, linkName, cardIdx);
|
||||
return card;
|
||||
}
|
||||
|
||||
@@ -5048,9 +5169,8 @@
|
||||
none.textContent = '(ไม่ได้ส่ง)';
|
||||
col.appendChild(none);
|
||||
} else {
|
||||
sel.forEach(function (ci) {
|
||||
var c = cards[ci];
|
||||
if (c) col.appendChild(esRevealCardEl(c, sd.linkName, ci));
|
||||
sel.forEach(function (c, ci) {
|
||||
if (c && typeof c === 'object' && c.imageUrl) col.appendChild(esRevealCardEl(c, sd.linkName, ci));
|
||||
});
|
||||
if (!col.querySelector('.es-card')) {
|
||||
var bad = document.createElement('div');
|
||||
@@ -5157,8 +5277,9 @@
|
||||
}
|
||||
});
|
||||
} else {
|
||||
var ownedRow = (myPlayerEvidence[tmState.round] || []);
|
||||
tmState.ownedSlots.forEach(function (slot) {
|
||||
var c = (sd.cards || [])[slot];
|
||||
var c = ownedRow[slot];
|
||||
if (c) cardsRoot.appendChild(es3EvidenceCardEl(c, sd.linkName, slot, true));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3954,7 +3954,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.0506"></script>
|
||||
<script src="js/play.js?v=0.0508"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
<link rel="stylesheet" href="css/profile-popup.css?v=6">
|
||||
<link rel="stylesheet" href="css/customize-popup.css?v=42">
|
||||
<link rel="stylesheet" href="css/leaderboard-popup.css?v=2">
|
||||
<link rel="stylesheet" href="css/testimony-overlay.css?v=14">
|
||||
<link rel="stylesheet" href="css/evidence-view-overlay.css?v=12">
|
||||
<link rel="stylesheet" href="css/testimony-overlay.css?v=15">
|
||||
<link rel="stylesheet" href="css/evidence-view-overlay.css?v=13">
|
||||
<style>
|
||||
html, body.room-lobby-page { margin: 0; padding: 0; overflow: hidden; width: 100%; height: 100%; min-height: 100%; }
|
||||
#mic-permission-overlay {
|
||||
@@ -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.0258"></script>
|
||||
<script src="js/room-lobby.js?v=0.0262"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1169,6 +1169,219 @@ function mergeQuizCarryFromMirrorIfSet(base) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== การ์ดหลักฐาน (Evidence Cards) — 3 คดี × ผู้ต้องสงสัย 3 × การ์ด 3 ใบ จัดการผ่าน Admin ===== */
|
||||
const EVIDENCE_CARDS_PATH = path.join(__dirname, 'data', 'evidence-cards.json');
|
||||
const EVIDENCE_RARITIES = ['common', 'rare', 'legendary'];
|
||||
const EVIDENCE_CASE_COUNT = 15;
|
||||
const EVIDENCE_IMAGE_DIRS = ['suspect-1', 'suspect-2', 'suspect-3'];
|
||||
/* rarity คิดจากเลขไฟล์การ์ด: 01-22 = common, 23-44 = rare, 45-54 = legendary (ชี้คนร้าย) */
|
||||
const EVIDENCE_RARITY_RANGES = [
|
||||
{ rarity: 'common', min: 1, max: 22, stars: 1 },
|
||||
{ rarity: 'rare', min: 23, max: 44, stars: 2 },
|
||||
{ rarity: 'legendary', min: 45, max: 54, stars: 3 },
|
||||
];
|
||||
|
||||
function evidenceRarityForNum(n) {
|
||||
for (const r of EVIDENCE_RARITY_RANGES) if (n >= r.min && n <= r.max) return r;
|
||||
return EVIDENCE_RARITY_RANGES[0];
|
||||
}
|
||||
|
||||
function evidenceCardImageDefault(suspectIdx, cardIdx) {
|
||||
return '/Game/img/evidence-cards/suspect-' + (suspectIdx + 1) + '/Card-0' + (cardIdx + 1) + '.png';
|
||||
}
|
||||
|
||||
/* แคชพูลรูปการ์ดต่อโฟลเดอร์ผู้ต้องสงสัย แยกตาม rarity (สแกนไฟล์จริง) */
|
||||
let _evidencePoolCache = null;
|
||||
let _evidencePoolCacheAt = 0;
|
||||
function evidenceImagePools() {
|
||||
const now = Date.now();
|
||||
if (_evidencePoolCache && (now - _evidencePoolCacheAt) < 30000) return _evidencePoolCache;
|
||||
const pools = {};
|
||||
EVIDENCE_IMAGE_DIRS.forEach((dir) => {
|
||||
const buckets = { common: [], rare: [], legendary: [] };
|
||||
try {
|
||||
const p = path.join(__dirname, 'public', 'img', 'evidence-cards', dir);
|
||||
fs.readdirSync(p).forEach((f) => {
|
||||
const m = /^Card-0*(\d+)\.(png|jpe?g|webp)$/i.exec(f);
|
||||
if (!m) return;
|
||||
const num = parseInt(m[1], 10);
|
||||
const rr = evidenceRarityForNum(num);
|
||||
buckets[rr.rarity].push({ num, rarity: rr.rarity, stars: rr.stars, imageUrl: '/Game/img/evidence-cards/' + dir + '/' + f });
|
||||
});
|
||||
} catch (e) { /* โฟลเดอร์อาจไม่มี */ }
|
||||
Object.keys(buckets).forEach((k) => buckets[k].sort((a, b) => a.num - b.num));
|
||||
pools[dir] = buckets;
|
||||
});
|
||||
_evidencePoolCache = pools;
|
||||
_evidencePoolCacheAt = now;
|
||||
return pools;
|
||||
}
|
||||
|
||||
function evidencePoolCounts() {
|
||||
const pools = evidenceImagePools();
|
||||
const out = {};
|
||||
EVIDENCE_IMAGE_DIRS.forEach((dir) => {
|
||||
const b = pools[dir] || { common: [], rare: [], legendary: [] };
|
||||
out[dir] = { common: b.common.length, rare: b.rare.length, legendary: b.legendary.length };
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/* แปลงเกรด -> rarity key ('legendary'=ชี้คนร้าย/'rare'/'common') ตามเปอร์เซ็นต์ที่กำหนด */
|
||||
function rollEvidenceRarityKey(grade) {
|
||||
const r = Math.random();
|
||||
if (grade === 'A') return r < 0.30 ? 'legendary' : (r < 0.80 ? 'rare' : 'common');
|
||||
if (grade === 'B') return r < 0.20 ? 'legendary' : (r < 0.50 ? 'rare' : 'common');
|
||||
return r < 0.20 ? 'rare' : 'common'; // C ลงไป: ไม่มีชี้คนร้าย
|
||||
}
|
||||
|
||||
/* โฟลเดอร์รูปของผู้ต้องสงสัยตาม config คดี (fallback = suspect-(idx+1)) */
|
||||
function evidenceImageDirForSuspect(space, suspectIndex) {
|
||||
const def = EVIDENCE_IMAGE_DIRS[suspectIndex] || EVIDENCE_IMAGE_DIRS[0];
|
||||
try {
|
||||
const cid = String((space && space.caseId) || '1');
|
||||
const cfg = loadEvidenceCards();
|
||||
const cse = cfg.cases && cfg.cases[cid];
|
||||
const s = cse && cse.suspects && cse.suspects[suspectIndex];
|
||||
if (s && s.imageDir && EVIDENCE_IMAGE_DIRS.indexOf(s.imageDir) >= 0) return s.imageDir;
|
||||
} catch (e) { /* ใช้ default */ }
|
||||
return def;
|
||||
}
|
||||
|
||||
/* เลือกการ์ด 1 ใบจากพูลของผู้ต้องสงสัยตามเกรด (มี fallback ถ้า rarity นั้นไม่มีรูป) */
|
||||
function pickEvidenceCardForSuspect(space, suspectIndex, grade) {
|
||||
const imageDir = evidenceImageDirForSuspect(space, suspectIndex);
|
||||
const pools = evidenceImagePools();
|
||||
const buckets = pools[imageDir] || pools[EVIDENCE_IMAGE_DIRS[0]] || { common: [], rare: [], legendary: [] };
|
||||
let want = rollEvidenceRarityKey(grade || 'C');
|
||||
let pool = buckets[want];
|
||||
if (!pool || !pool.length) {
|
||||
const tryOrder = ['common', 'rare', 'legendary'];
|
||||
for (const k of tryOrder) { if (buckets[k] && buckets[k].length) { pool = buckets[k]; want = k; break; } }
|
||||
}
|
||||
if (!pool || !pool.length) return null;
|
||||
const pick = pool[Math.floor(Math.random() * pool.length)];
|
||||
return { rarity: pick.rarity, stars: pick.stars, num: pick.num, imageUrl: pick.imageUrl };
|
||||
}
|
||||
|
||||
function evidenceGradeFromPct(avgPct) {
|
||||
return avgPct >= 80 ? 'A' : avgPct >= 60 ? 'B' : 'C';
|
||||
}
|
||||
|
||||
/* คะแนนดิบของผู้เล่นต่อ gameType (ใช้กรณี normalize เทียบ best) */
|
||||
function evidencePlayerRawScore(space, p, gameType) {
|
||||
switch (gameType) {
|
||||
case 'gauntlet':
|
||||
case 'jump_survive': return Math.max(0, p.gauntletScore | 0);
|
||||
case 'space_shooter': return Math.max(0, p.spaceShooterScore | 0);
|
||||
case 'balloon_boss': return Math.max(0, p.balloonBossScore | 0);
|
||||
default:
|
||||
return Math.max(0, p.gauntletScore | 0) || Math.max(0, p.spaceShooterScore | 0) || Math.max(0, p.balloonBossScore | 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* เกรดของรอบ (A/B/C) — quiz ใช้ % สัมบูรณ์, เกมอื่น normalize เทียบ best แล้วเฉลี่ยทั้งห้อง */
|
||||
function computeRunGradeForEvidence(space) {
|
||||
try {
|
||||
const md = (space.mapId && maps.get(space.mapId)) || space.mapData;
|
||||
const gameType = md && md.gameType;
|
||||
if (gameType === 'gauntlet' || gameType === 'jump_survive') {
|
||||
return buildGauntletCrownMissionPayload(space).grade || 'C';
|
||||
}
|
||||
/* เกมจริงหรือไม่ (quiz TF): เฉลี่ย % ของคะแนนเทียบเต็ม (จำนวนข้อ × แต้มต่อข้อ) */
|
||||
if (gameType === 'quiz' && space.quizSession && space.quizSession.players) {
|
||||
const total = (space.quizSession.questions || []).length * QUIZ_TF_POINTS_PER_CORRECT;
|
||||
const ids = Object.keys(space.quizSession.players);
|
||||
if (total > 0 && ids.length) {
|
||||
const avgPct = ids.reduce((s, id) => {
|
||||
const sc = Math.max(0, Number(space.quizSession.players[id].score) || 0);
|
||||
return s + Math.min(100, (sc / total) * 100);
|
||||
}, 0) / ids.length;
|
||||
return evidenceGradeFromPct(avgPct);
|
||||
}
|
||||
}
|
||||
const peers = [...space.peers.values()];
|
||||
if (!peers.length) return 'C';
|
||||
const raw = peers.map((p) => evidencePlayerRawScore(space, p, gameType));
|
||||
const max = Math.max(1, ...raw);
|
||||
const avgPct = raw.reduce((s, v) => s + (v / max) * 100, 0) / raw.length;
|
||||
return evidenceGradeFromPct(avgPct);
|
||||
} catch (e) { return 'C'; }
|
||||
}
|
||||
|
||||
/* card object ที่เก็บในแฟ้มหลักฐาน */
|
||||
function sanitizeStoredEvidenceCard(c) {
|
||||
if (!c || typeof c !== 'object') return null;
|
||||
let rar = String(c.rarity || 'common').toLowerCase();
|
||||
if (EVIDENCE_RARITIES.indexOf(rar) < 0) rar = 'common';
|
||||
const img = String(c.imageUrl || '').slice(0, 400);
|
||||
if (!img) return null;
|
||||
let stars = Math.floor(Number(c.stars));
|
||||
if (!(stars >= 1 && stars <= 3)) stars = rar === 'legendary' ? 3 : rar === 'rare' ? 2 : 1;
|
||||
let num = Math.floor(Number(c.num));
|
||||
if (!(num >= 1)) num = 0;
|
||||
return { rarity: rar, stars, num, imageUrl: img };
|
||||
}
|
||||
|
||||
function sanitizeEvidenceCases(obj) {
|
||||
const src = (obj && obj.cases) ? obj.cases : {};
|
||||
const out = {};
|
||||
for (let i = 1; i <= EVIDENCE_CASE_COUNT; i++) {
|
||||
const cid = String(i);
|
||||
const cse = src[cid] || {};
|
||||
const suspects = Array.isArray(cse.suspects) ? cse.suspects : [];
|
||||
const outSus = [];
|
||||
for (let si = 0; si < 3; si++) {
|
||||
const s = suspects[si] || {};
|
||||
let dir = String(s.imageDir || EVIDENCE_IMAGE_DIRS[si] || EVIDENCE_IMAGE_DIRS[0]);
|
||||
if (EVIDENCE_IMAGE_DIRS.indexOf(dir) < 0) dir = EVIDENCE_IMAGE_DIRS[si] || EVIDENCE_IMAGE_DIRS[0];
|
||||
outSus.push({
|
||||
linkName: String(s.linkName || ('ผู้ต้องสงสัย ' + (si + 1))).slice(0, 60),
|
||||
imageDir: dir,
|
||||
});
|
||||
}
|
||||
out[cid] = { suspects: outSus };
|
||||
}
|
||||
return { cases: out };
|
||||
}
|
||||
|
||||
function loadEvidenceCards() {
|
||||
try {
|
||||
if (fs.existsSync(EVIDENCE_CARDS_PATH)) {
|
||||
return sanitizeEvidenceCases(JSON.parse(fs.readFileSync(EVIDENCE_CARDS_PATH, 'utf8')));
|
||||
}
|
||||
} catch (e) { /* ignore — คืนค่า default */ }
|
||||
return sanitizeEvidenceCases({});
|
||||
}
|
||||
|
||||
/** รายการรูปการ์ดที่มีจริงในแต่ละโฟลเดอร์ผู้ต้องสงสัย (ไว้ให้ Admin เลือกรูป) */
|
||||
function listEvidenceCardImages() {
|
||||
const out = { 'suspect-1': [], 'suspect-2': [], 'suspect-3': [] };
|
||||
Object.keys(out).forEach((dir) => {
|
||||
try {
|
||||
const p = path.join(__dirname, 'public', 'img', 'evidence-cards', dir);
|
||||
const files = fs.readdirSync(p)
|
||||
.filter((f) => /\.(png|jpe?g|webp)$/i.test(f))
|
||||
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
||||
out[dir] = files.map((f) => '/Game/img/evidence-cards/' + dir + '/' + f);
|
||||
} catch (e) { /* โฟลเดอร์อาจไม่มี */ }
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function saveEvidenceCards(d) {
|
||||
try {
|
||||
const clean = sanitizeEvidenceCases(d);
|
||||
fs.writeFileSync(EVIDENCE_CARDS_PATH, JSON.stringify(clean, null, 2), 'utf8');
|
||||
return { ok: true, cases: clean.cases };
|
||||
} catch (e) {
|
||||
let err = 'บันทึกไม่ได้';
|
||||
if (e && e.code === 'EACCES') err = 'ไม่มีสิทธิ์เขียนไฟล์ evidence-cards.json (chown เป็น user ที่รันเกม)';
|
||||
else if (e && e.message) err = 'บันทึกไม่ได้: ' + e.message;
|
||||
return { ok: false, error: err };
|
||||
}
|
||||
}
|
||||
|
||||
function loadQuizSettings() {
|
||||
try {
|
||||
if (fs.existsSync(QUIZ_SETTINGS_PATH)) {
|
||||
@@ -2369,11 +2582,14 @@ function ensureSpacePlayerEvidence(space) {
|
||||
if (!space.evidenceByNick || typeof space.evidenceByNick !== 'object') space.evidenceByNick = {};
|
||||
}
|
||||
|
||||
/* แถวหลักฐานต่อผู้ต้องสงสัย = array ของ card object (ไม่จำกัดจำนวน) — กรอง element ที่ไม่ใช่การ์ดทิ้ง */
|
||||
function normalizeEvidenceSlotList(cards) {
|
||||
if (!Array.isArray(cards)) return [];
|
||||
const n = Math.min(3, cards.length);
|
||||
const out = [];
|
||||
for (let i = 0; i < n; i++) out.push(i);
|
||||
for (let i = 0; i < cards.length; i++) {
|
||||
const c = sanitizeStoredEvidenceCard(cards[i]);
|
||||
if (c) out.push(c);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -2409,14 +2625,14 @@ function syncEvidenceByNick(space, playerId) {
|
||||
if (Array.isArray(row)) space.evidenceByNick[nk] = row;
|
||||
}
|
||||
|
||||
/** จำนวนหลักฐานที่เก็บได้ต่อผู้ต้องสงสัย (0–3) — ใช้แสดงติกบนการ์ด suspect */
|
||||
/** จำนวนหลักฐานที่เก็บได้ต่อผู้ต้องสงสัย (ไม่จำกัด) — ใช้แสดงติก/จำนวนบนการ์ด suspect */
|
||||
function playerSuspectProgressFromEvidence(space, playerId) {
|
||||
const row = clonePlayerEvidence(space, playerId);
|
||||
return row.map((arr) => Math.min(3, arr.length));
|
||||
return row.map((arr) => arr.length);
|
||||
}
|
||||
|
||||
/** มอบการ์ดหลักฐานชิ้นถัดไป — เรียงซ้าย→ขวา (0 แล้ว 1 แล้ว 2) ไม่สุ่มช่อง */
|
||||
function awardRandomEvidenceCardToPlayer(space, playerId, suspectIndex) {
|
||||
/** มอบการ์ดหลักฐาน 1 ใบ (สุ่ม rarity ตามเกรด → สุ่มรูปจากพูล) เก็บเป็น card object ไม่จำกัดจำนวน */
|
||||
function awardRandomEvidenceCardToPlayer(space, playerId, suspectIndex, grade) {
|
||||
if (suspectIndex < 0 || suspectIndex > 2) return null;
|
||||
ensureSpacePlayerEvidence(space);
|
||||
/* เผื่อ rejoin: ดึงหลักฐานเก่าจาก nickname ถ้ามี ก่อนเพิ่มใบใหม่ */
|
||||
@@ -2426,27 +2642,24 @@ function awardRandomEvidenceCardToPlayer(space, playerId, suspectIndex) {
|
||||
? space.evidenceByNick[nk]
|
||||
: [[], [], []];
|
||||
}
|
||||
if (!Array.isArray(space.playerEvidence[playerId][suspectIndex])) {
|
||||
space.playerEvidence[playerId][suspectIndex] = [];
|
||||
}
|
||||
const list = normalizeEvidenceSlotList(space.playerEvidence[playerId][suspectIndex]);
|
||||
if (list.length >= 3) {
|
||||
const card = pickEvidenceCardForSuspect(space, suspectIndex, grade);
|
||||
if (!card) {
|
||||
space.playerEvidence[playerId][suspectIndex] = list;
|
||||
syncEvidenceByNick(space, playerId);
|
||||
return null;
|
||||
}
|
||||
const pick = list.length;
|
||||
list.push(pick);
|
||||
list.push(card);
|
||||
space.playerEvidence[playerId][suspectIndex] = list;
|
||||
syncEvidenceByNick(space, playerId);
|
||||
return pick;
|
||||
return card;
|
||||
}
|
||||
|
||||
function awardDetectiveEvidenceForMinigameEnd(space, suspectIndex, playerIds) {
|
||||
function awardDetectiveEvidenceForMinigameEnd(space, suspectIndex, playerIds, grade) {
|
||||
const out = {};
|
||||
(playerIds || []).forEach((pid) => {
|
||||
if (!space.peers.has(pid)) return;
|
||||
const card = awardRandomEvidenceCardToPlayer(space, pid, suspectIndex);
|
||||
const card = awardRandomEvidenceCardToPlayer(space, pid, suspectIndex, grade);
|
||||
if (card != null) out[pid] = card;
|
||||
});
|
||||
return out;
|
||||
@@ -2474,7 +2687,11 @@ function grantDetectiveEvidenceForCurrentRun(sid, space, playerIds) {
|
||||
return { suspectIdx, awarded: {}, alreadyGranted: true };
|
||||
}
|
||||
const ids = (playerIds && playerIds.length) ? playerIds : [...space.peers.keys()];
|
||||
const awarded = awardDetectiveEvidenceForMinigameEnd(space, suspectIdx, ids);
|
||||
const grade = computeRunGradeForEvidence(space);
|
||||
const awardedList = {};
|
||||
ids.forEach((pid) => { awardedList[pid] = []; });
|
||||
const awarded = awardDetectiveEvidenceForMinigameEnd(space, suspectIdx, ids, grade);
|
||||
ids.forEach((pid) => { if (awarded[pid]) awardedList[pid].push(awarded[pid]); });
|
||||
/* Card 7 Free Evidence — ทุกคนรับหลักฐานเพิ่ม (after_game) */
|
||||
let freeEvidenceCount = 0;
|
||||
const pend = space.pendingSpecialCard;
|
||||
@@ -2484,17 +2701,17 @@ function grantDetectiveEvidenceForCurrentRun(sid, space, playerIds) {
|
||||
for (let n = 0; n < freeEvidenceCount; n++) {
|
||||
ids.forEach((pid) => {
|
||||
if (!space.peers.has(pid)) return;
|
||||
const extra = awardRandomEvidenceCardToPlayer(space, pid, suspectIdx);
|
||||
if (extra != null && awarded[pid] == null) awarded[pid] = extra;
|
||||
const extra = awardRandomEvidenceCardToPlayer(space, pid, suspectIdx, grade);
|
||||
if (extra != null) { awardedList[pid].push(extra); if (awarded[pid] == null) awarded[pid] = extra; }
|
||||
});
|
||||
}
|
||||
pend.consumed = true;
|
||||
}
|
||||
space.detectiveAwardDoneForRun = true;
|
||||
space.lastDetectiveInvestigatedSuspect = suspectIdx;
|
||||
space.lastEvidenceGrant = { suspectIdx, awarded, freeEvidenceCount };
|
||||
space.lastEvidenceGrant = { suspectIdx, awarded, awardedList, grade, freeEvidenceCount };
|
||||
emitLobbyEvidenceSync(sid, space);
|
||||
return { suspectIdx, awarded, alreadyGranted: false, freeEvidenceCount };
|
||||
return { suspectIdx, awarded, awardedList, grade, alreadyGranted: false, freeEvidenceCount };
|
||||
}
|
||||
|
||||
function emitLobbyEvidenceSync(sid, space) {
|
||||
@@ -2807,20 +3024,18 @@ function botTestimonyPickCount(space) {
|
||||
return Math.max(1, Math.min(2, maxOwned));
|
||||
}
|
||||
|
||||
/** สุ่ม picks ให้บอทในรอบ testimony ปัจจุบัน — จำนวนใบเท่ากับผู้เล่นจริง */
|
||||
/** สุ่ม picks ให้บอทในรอบ testimony ปัจจุบัน — เก็บเป็น card object (สุ่มจากพูล rarity 'C') */
|
||||
function autoSubmitBotTestimonyPicks(space) {
|
||||
if (!space.testimonySubmitted) space.testimonySubmitted = {};
|
||||
const slots = [0, 1, 2];
|
||||
const round = typeof space.testimonyRound === 'number' ? space.testimonyRound : 0;
|
||||
const want = botTestimonyPickCount(space);
|
||||
testimonyBotIds(space).forEach((bid) => {
|
||||
if (space.testimonySubmitted[bid] != null) return;
|
||||
const pool = slots.slice();
|
||||
const picks = [];
|
||||
while (picks.length < want && pool.length) {
|
||||
const idx = Math.floor(Math.random() * pool.length);
|
||||
picks.push(pool.splice(idx, 1)[0]);
|
||||
for (let n = 0; n < want; n++) {
|
||||
const c = pickEvidenceCardForSuspect(space, round, 'C');
|
||||
if (c) picks.push(c);
|
||||
}
|
||||
picks.sort((a, b) => a - b);
|
||||
space.testimonySubmitted[bid] = picks;
|
||||
});
|
||||
}
|
||||
@@ -3793,6 +4008,53 @@ const server = http.createServer((req, res) => {
|
||||
res.writeHead(405);
|
||||
return res.end(JSON.stringify({ ok: false, error: 'Method not allowed' }));
|
||||
}
|
||||
const evidenceCardsUrlPath = url.split('?')[0].replace(/\/+$/, '') || '/';
|
||||
if (evidenceCardsUrlPath === BASE_PATH + '/api/evidence-cards') {
|
||||
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(loadEvidenceCards()));
|
||||
}
|
||||
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 = saveEvidenceCards(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 evidenceImagesUrlPath = url.split('?')[0].replace(/\/+$/, '') || '/';
|
||||
if (evidenceImagesUrlPath === BASE_PATH + '/api/evidence-card-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: listEvidenceCardImages(), pools: evidencePoolCounts(), dirs: EVIDENCE_IMAGE_DIRS }));
|
||||
}
|
||||
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') {
|
||||
@@ -5811,11 +6073,6 @@ io.on('connection', (socket) => {
|
||||
assignDetectiveSuspectCardMinigames(space);
|
||||
}
|
||||
const selectedIndex = typeof space.suspectPickIndex === 'number' ? space.suspectPickIndex : 0;
|
||||
const myEvidence = clonePlayerEvidence(space, socket.id);
|
||||
const ownedCount = (myEvidence[selectedIndex] || []).length;
|
||||
if (ownedCount >= 3) {
|
||||
return reply({ ok: false, error: 'สืบผู้ต้องสงสัยคนนี้ครบ 3 หลักฐานแล้ว — เลือกคนอื่น' });
|
||||
}
|
||||
const cardEntry = space.suspectCardMinigames[selectedIndex];
|
||||
if (!cardEntry || !cardEntry.mapId) {
|
||||
return reply({ ok: false, error: 'ยังไม่ได้สุ่มมินิเกมสำหรับการ์ดนี้' });
|
||||
@@ -5855,6 +6112,8 @@ io.on('connection', (socket) => {
|
||||
alreadyOnLobbyB: true,
|
||||
suspectIndex: grant.suspectIdx,
|
||||
awardedCard: grant.awarded && grant.awarded[socket.id] != null ? grant.awarded[socket.id] : null,
|
||||
awardedCards: (grant.awardedList && grant.awardedList[socket.id]) || [],
|
||||
grade: grant.grade || null,
|
||||
freeEvidenceCount: grant.freeEvidenceCount || 0,
|
||||
myPlayerEvidence: clonePlayerEvidence(space, socket.id),
|
||||
suspectProgress: playerSuspectProgressFromEvidence(space, socket.id),
|
||||
@@ -5869,6 +6128,8 @@ io.on('connection', (socket) => {
|
||||
ok: true,
|
||||
suspectIndex: typeof g.suspectIdx === 'number' ? g.suspectIdx : -1,
|
||||
awardedCard: (g.awarded && g.awarded[socket.id] != null) ? g.awarded[socket.id] : null,
|
||||
awardedCards: (g.awardedList && g.awardedList[socket.id]) || [],
|
||||
grade: g.grade || null,
|
||||
freeEvidenceCount: g.freeEvidenceCount || 0,
|
||||
myPlayerEvidence: clonePlayerEvidence(space, socket.id),
|
||||
suspectProgress: playerSuspectProgressFromEvidence(space, socket.id),
|
||||
@@ -5887,8 +6148,8 @@ io.on('connection', (socket) => {
|
||||
if (!serverMapIsPostCaseLobbyB(space)) return reply({ ok: false, error: 'ต้องอยู่โถง LobbyB' });
|
||||
const idx = Math.floor(Number(data && data.suspectIndex));
|
||||
if (!(idx >= 0 && idx <= 2)) return reply({ ok: false, error: 'suspectIndex ต้องเป็น 0–2' });
|
||||
const card = awardRandomEvidenceCardToPlayer(space, socket.id, idx);
|
||||
if (card == null) return reply({ ok: false, error: 'เก็บครบ 3 ใบแล้วสำหรับ suspect นี้' });
|
||||
const card = awardRandomEvidenceCardToPlayer(space, socket.id, idx, 'C');
|
||||
if (card == null) return reply({ ok: false, error: 'ไม่มีรูปการ์ดในพูลของ suspect นี้' });
|
||||
emitLobbyEvidenceSync(sid, space);
|
||||
reply({
|
||||
ok: true,
|
||||
@@ -5928,10 +6189,12 @@ io.on('connection', (socket) => {
|
||||
const raw = (data && Array.isArray(data.picks)) ? data.picks : [];
|
||||
const owned = normalizeEvidenceSlotList(clonePlayerEvidence(space, socket.id)[space.testimonyRound] || []);
|
||||
const required = Math.max(1, Math.min(2, owned.length));
|
||||
/* raw = ตำแหน่ง index ใน owned (0..n-1) → เก็บเป็น card object */
|
||||
const usedIdx = [];
|
||||
const picks = [];
|
||||
raw.forEach((n) => {
|
||||
const v = Math.floor(Number(n));
|
||||
if (v >= 0 && v <= 2 && picks.indexOf(v) === -1 && owned.indexOf(v) >= 0) picks.push(v);
|
||||
if (v >= 0 && v < owned.length && usedIdx.indexOf(v) === -1) { usedIdx.push(v); picks.push(owned[v]); }
|
||||
});
|
||||
if (owned.length === 0) {
|
||||
/* ไม่มีหลักฐานของ suspect รอบนี้ — ข้ามได้ (auto-submit ว่าง) */
|
||||
|
||||
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 676 KiB |
|
After Width: | Height: | Size: 675 KiB |
|
After Width: | Height: | Size: 678 KiB |
|
After Width: | Height: | Size: 674 KiB |
|
After Width: | Height: | Size: 673 KiB |
|
After Width: | Height: | Size: 673 KiB |
|
After Width: | Height: | Size: 678 KiB |
|
After Width: | Height: | Size: 673 KiB |
|
After Width: | Height: | Size: 679 KiB |
|
After Width: | Height: | Size: 675 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 674 KiB |
|
After Width: | Height: | Size: 674 KiB |
|
After Width: | Height: | Size: 677 KiB |
|
After Width: | Height: | Size: 673 KiB |
|
After Width: | Height: | Size: 672 KiB |
|
After Width: | Height: | Size: 672 KiB |
|
After Width: | Height: | Size: 676 KiB |
|
After Width: | Height: | Size: 672 KiB |
|
After Width: | Height: | Size: 677 KiB |
|
After Width: | Height: | Size: 674 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.8 MiB |