โชว์การ์ดพิเศษที่ได้ + ปุ่มไปต่อ
This commit is contained in:
+134
-2
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user