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

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
+134 -2
View File
@@ -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);