โชว์การ์ดพิเศษที่ได้ + ปุ่มไปต่อ

This commit is contained in:
2026-06-02 08:29:04 +00:00
parent 18cad3e9f6
commit f98cbfa9e0
69 changed files with 725 additions and 48 deletions
+30
View File
@@ -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;
+139
View File
@@ -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();
+25 -2
View File
@@ -10,7 +10,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Kanit:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="admin.css?v=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>คำถาม &amp; การ์ดพิเศษ</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">รูปคดี &amp; Cutscene</span><span class="tab-desc">15 คดี · รูปเลือกคดี/รายละเอียด/เรื่องราว 2 รูป</span></button>
<div class="admin-tab-sep" aria-hidden="true"><span>ระบบ &amp; บัญชี</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">รูปคดี &amp; 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>