update all games

This commit is contained in:
2026-06-14 04:46:57 +00:00
parent 680ccdb618
commit 52a45f0ae0
8 changed files with 273 additions and 41 deletions
+53
View File
@@ -3969,6 +3969,56 @@
});
}
function applyForcedMinigameKeysToForm(keys) {
var arr = Array.isArray(keys) ? keys : [];
for (var i = 1; i <= 3; i++) {
var sel = el('forced-mg-' + i);
if (sel) sel.value = arr[i - 1] || '';
}
}
function readForcedMinigameKeysFromForm() {
var out = [];
for (var i = 1; i <= 3; i++) {
var sel = el('forced-mg-' + i);
out.push(sel ? String(sel.value || '') : '');
}
return out;
}
function loadForcedMinigamePanel() {
gameTimingFetch('GET')
.then(function (data) {
applyForcedMinigameKeysToForm(data && data.forcedMinigameKeys);
setMsg('forced-minigame-msg', '', '');
})
.catch(function (e) {
setMsg('forced-minigame-msg', e.message || 'โหลดไม่ได้', 'error');
});
}
function saveForcedMinigamePanel() {
var keys = readForcedMinigameKeysFromForm();
var anyForced = keys.some(function (k) { return !!k; });
var btn = el('btn-forced-minigame-save');
if (btn) btn.disabled = true;
gameTimingFetch('GET')
.then(function (data) {
data.forcedMinigameKeys = keys;
return gameTimingFetch('PUT', data);
})
.then(function (res) {
if (res && typeof res === 'object') applyForcedMinigameKeysToForm(res.forcedMinigameKeys);
setMsg('forced-minigame-msg', anyForced ? 'บันทึกแล้ว — เริ่มคดีใหม่เพื่อเทสต์การ์ดที่ตั้งไว้' : 'กลับเป็นปกติ (สุ่มทั้ง 3 ใบ) แล้ว', 'ok');
})
.catch(function (e) {
setMsg('forced-minigame-msg', e.message || 'บันทึกไม่ได้', 'error');
})
.then(function () {
if (btn) btn.disabled = false;
});
}
function saveJumpSurviveTimingPanel() {
var inp = el('jump-survive-height-mult');
var jumpSurvMult = inp ? parseFloat(String(inp.value)) : 1.5;
@@ -4487,6 +4537,7 @@
if (name === 'game-timing') loadGameTimingPanel();
if (name === 'stack-game') loadStackGamePanel();
if (name === 'highscore') loadHighscore();
if (name === 'test-mode') loadForcedMinigamePanel();
}
function boot() {
@@ -4710,6 +4761,8 @@
syncTestModeUi();
});
}
var forcedMgSaveBtn = el('btn-forced-minigame-save');
if (forcedMgSaveBtn) forcedMgSaveBtn.addEventListener('click', saveForcedMinigamePanel);
syncTestModeUi();
/* ฟัง storage event — sync เมื่อเปิดหลายแท็บ */
window.addEventListener('storage', function (e) {
+43
View File
@@ -1112,6 +1112,49 @@
</ul>
<p id="test-mode-msg" class="msg" role="status" style="margin-top:14px;"></p>
<h3 class="admin-subheading" style="margin-top:28px;">บังคับมินิเกม (เทสต์ตอนเล่นจริง)</h3>
<p class="muted">เลือกเกมต่อการ์ดผู้ต้องสงสัย 3 ใบ → บันทึก → เริ่มคดีใหม่ (สร้างห้อง → LobbyA → เลือกคดี → LobbyB) · ช่องที่เลือก "— สุ่ม —" จะสุ่มจาก 7 เกมเหมือนเดิม · เลือก "— สุ่ม —" ครบทั้ง 3 ช่อง = กลับเป็นปกติ</p>
<div class="row" style="display:flex;align-items:flex-end;gap:14px;margin:12px 0;flex-wrap:wrap;">
<label style="display:flex;flex-direction:column;gap:4px;font-size:13px;color:#cfd6ea;">การ์ดใบที่ 1
<select id="forced-mg-1" style="padding:8px 10px;min-width:300px;font-size:15px;">
<option value="">— สุ่ม —</option>
<option value="quiz">Minigame-1 จริงหรือไม่</option>
<option value="gauntlet">Minigame-2 วิ่งหลบสิ่งกีดขวาง</option>
<option value="stack">Minigame-3 Tower block</option>
<option value="quiz_carry">Minigame-4 คำศัพท์ในกระบวนการยุติธรรม</option>
<option value="jump_survive">Minigame-5 กระโดดขึ้นแท่น</option>
<option value="space_shooter">Minigame-6 ยิงอุกาบาต Violent Crime</option>
<option value="balloon_boss">Minigame-7 ยิง Mega Virus</option>
</select>
</label>
<label style="display:flex;flex-direction:column;gap:4px;font-size:13px;color:#cfd6ea;">การ์ดใบที่ 2
<select id="forced-mg-2" style="padding:8px 10px;min-width:300px;font-size:15px;">
<option value="">— สุ่ม —</option>
<option value="quiz">Minigame-1 จริงหรือไม่</option>
<option value="gauntlet">Minigame-2 วิ่งหลบสิ่งกีดขวาง</option>
<option value="stack">Minigame-3 Tower block</option>
<option value="quiz_carry">Minigame-4 คำศัพท์ในกระบวนการยุติธรรม</option>
<option value="jump_survive">Minigame-5 กระโดดขึ้นแท่น</option>
<option value="space_shooter">Minigame-6 ยิงอุกาบาต Violent Crime</option>
<option value="balloon_boss">Minigame-7 ยิง Mega Virus</option>
</select>
</label>
<label style="display:flex;flex-direction:column;gap:4px;font-size:13px;color:#cfd6ea;">การ์ดใบที่ 3
<select id="forced-mg-3" style="padding:8px 10px;min-width:300px;font-size:15px;">
<option value="">— สุ่ม —</option>
<option value="quiz">Minigame-1 จริงหรือไม่</option>
<option value="gauntlet">Minigame-2 วิ่งหลบสิ่งกีดขวาง</option>
<option value="stack">Minigame-3 Tower block</option>
<option value="quiz_carry">Minigame-4 คำศัพท์ในกระบวนการยุติธรรม</option>
<option value="jump_survive">Minigame-5 กระโดดขึ้นแท่น</option>
<option value="space_shooter">Minigame-6 ยิงอุกาบาต Violent Crime</option>
<option value="balloon_boss">Minigame-7 ยิง Mega Virus</option>
</select>
</label>
<button type="button" class="btn" id="btn-forced-minigame-save">บันทึก</button>
</div>
<p id="forced-minigame-msg" class="msg" role="status"></p>
</section>
</div><!-- /.admin-content -->
</div>
+9 -3
View File
@@ -77,14 +77,20 @@
{
"id": "d1d5c39d42a2dce9a4949d5a",
"email": "",
"displayName": "Guest",
"displayName": "ผู้เล่น",
"loginType": "guest",
"providerUserId": "p_1777015659667_xqfjh247bgp",
"notes": "auto: player-coins",
"blocked": false,
"coins": 0,
"coins": 90,
"createdAt": "2026-04-24T07:28:02+00:00",
"updatedAt": "2026-04-24T07:28:02+00:00"
"updatedAt": "2026-06-14T04:39:10+00:00",
"score": 90,
"scoreByCase": {
"8": 60,
"5": 30
},
"lbName": "ผู้เล่น"
},
{
"id": "d666c2fbf6005b69ee192882",
+6 -1
View File
@@ -87,5 +87,10 @@
"/Game/img/MegaVirus/balloon-6.png"
],
"balloonBossPlayerBalloonFallbackUrl": "/Game/img/MegaVirus/Artboard%209.png",
"balloonBossBalloonsPerPlayer": 3
"balloonBossBalloonsPerPlayer": 3,
"forcedMinigameKeys": [
"quiz",
"quiz_carry",
"balloon_boss"
]
}
@@ -47,6 +47,7 @@
<button type="button" role="tab" id="tab-shooter" aria-selected="false" aria-controls="panel-shooter" data-panel="panel-shooter">ยิงอุกาบาต · Violent Crime</button>
<button type="button" role="tab" id="tab-gauntlet" aria-selected="false" aria-controls="panel-gauntlet" data-panel="panel-gauntlet">พรมแดง · tick &amp; กระโดด</button>
<button type="button" role="tab" id="tab-stack" aria-selected="false" aria-controls="panel-stack" data-panel="panel-stack">Stack · สวิง</button>
<button type="button" role="tab" id="tab-test" aria-selected="false" aria-controls="panel-test" data-panel="panel-test">เทสต์ · บังคับมินิเกม</button>
</nav>
<div id="panel-jump" class="panel active" role="tabpanel" aria-labelledby="tab-jump">
@@ -100,6 +101,50 @@
</div>
</div>
<div id="panel-test" class="panel" role="tabpanel" aria-labelledby="tab-test" hidden>
<h2>เทสต์ — บังคับมินิเกมที่ออก</h2>
<p class="hint">เลือกเกมต่อการ์ดผู้ต้องสงสัย 3 ใบ — ช่องที่เลือก "— สุ่ม —" จะสุ่มจาก 7 เกมเหมือนเดิม · ตั้งครบแล้วเริ่มคดีใหม่ (LobbyA→LobbyB) เพื่อเทสต์ตอนเล่นจริง</p>
<div class="row">
<label for="forcedMg1">การ์ดใบที่ 1</label>
<select id="forcedMg1">
<option value="">— สุ่ม —</option>
<option value="quiz">Minigame-1 จริงหรือไม่</option>
<option value="gauntlet">Minigame-2 วิ่งหลบสิ่งกีดขวาง</option>
<option value="stack">Minigame-3 Tower block</option>
<option value="quiz_carry">Minigame-4 คำศัพท์ในกระบวนการยุติธรรม</option>
<option value="jump_survive">Minigame-5 กระโดดขึ้นแท่น</option>
<option value="space_shooter">Minigame-6 ยิงอุกาบาต Violent Crime</option>
<option value="balloon_boss">Minigame-7 ยิง Mega Virus</option>
</select>
</div>
<div class="row">
<label for="forcedMg2">การ์ดใบที่ 2</label>
<select id="forcedMg2">
<option value="">— สุ่ม —</option>
<option value="quiz">Minigame-1 จริงหรือไม่</option>
<option value="gauntlet">Minigame-2 วิ่งหลบสิ่งกีดขวาง</option>
<option value="stack">Minigame-3 Tower block</option>
<option value="quiz_carry">Minigame-4 คำศัพท์ในกระบวนการยุติธรรม</option>
<option value="jump_survive">Minigame-5 กระโดดขึ้นแท่น</option>
<option value="space_shooter">Minigame-6 ยิงอุกาบาต Violent Crime</option>
<option value="balloon_boss">Minigame-7 ยิง Mega Virus</option>
</select>
</div>
<div class="row">
<label for="forcedMg3">การ์ดใบที่ 3</label>
<select id="forcedMg3">
<option value="">— สุ่ม —</option>
<option value="quiz">Minigame-1 จริงหรือไม่</option>
<option value="gauntlet">Minigame-2 วิ่งหลบสิ่งกีดขวาง</option>
<option value="stack">Minigame-3 Tower block</option>
<option value="quiz_carry">Minigame-4 คำศัพท์ในกระบวนการยุติธรรม</option>
<option value="jump_survive">Minigame-5 กระโดดขึ้นแท่น</option>
<option value="space_shooter">Minigame-6 ยิงอุกาบาต Violent Crime</option>
<option value="balloon_boss">Minigame-7 ยิง Mega Virus</option>
</select>
</div>
</div>
<div class="actions">
<button type="button" class="primary" id="btn-save">บันทึกทั้งหมด</button>
</div>
@@ -115,6 +160,7 @@
'panel-shooter': document.getElementById('panel-shooter'),
'panel-gauntlet': document.getElementById('panel-gauntlet'),
'panel-stack': document.getElementById('panel-stack'),
'panel-test': document.getElementById('panel-test'),
};
tabs.forEach((btn) => {
btn.addEventListener('click', () => {
@@ -145,6 +191,11 @@
document.getElementById('stackSwingHz').value = timing.stackSwingHz != null ? timing.stackSwingHz : 0.1;
const sb = document.getElementById('stackBlockWidthTiles');
sb.value = timing.stackBlockWidthTiles != null && timing.stackBlockWidthTiles !== '' ? timing.stackBlockWidthTiles : '';
const fks = Array.isArray(timing.forcedMinigameKeys) ? timing.forcedMinigameKeys : [];
for (let i = 1; i <= 3; i++) {
const s = document.getElementById('forcedMg' + i);
if (s) s.value = fks[i - 1] || '';
}
}
function readForm() {
@@ -168,6 +219,10 @@
const v = document.getElementById('stackBlockWidthTiles').value.trim();
return v === '' ? null : parseFloat(v, 10);
})(),
forcedMinigameKeys: [1, 2, 3].map(function (i) {
const s = document.getElementById('forcedMg' + i);
return s ? (s.value || '') : '';
}),
};
}
+72 -34
View File
@@ -8220,6 +8220,22 @@
return lines;
}
/** ตัดบรรทัดระดับตัวอักษรให้แต่ละบรรทัดพอดีกว้าง (รองรับคำไทยยาวไม่มีช่องว่าง) — คืนทุกบรรทัด */
function quizCarryWrapLinesFit(ctx, text, maxWidth) {
const raw = String(text || '').trim();
if (!raw) return [];
const lines = [];
let cur = '';
for (const chr of raw) {
if (chr === '\n') { lines.push(cur); cur = ''; continue; }
const test = cur + chr;
if (cur === '' || ctx.measureText(test).width <= maxWidth) cur = test;
else { lines.push(cur); cur = chr; }
}
if (cur) lines.push(cur);
return lines;
}
function quizCarryOptionHeldByAnyone(optionIdx) {
if (optionIdx == null || optionIdx < 0) return false;
if (me.quizCarryHeld === optionIdx) return true;
@@ -15931,28 +15947,17 @@
if (o.balloonBossBotNextFire <= 0) {
o.balloonBossBotNextFire = 0.9 + Math.random() * 1.1;
const bspd = 500 + Math.random() * 90;
let bvx;
let bvy;
let bdelay;
if (isMegaVirusMissionShellMapPlay()) {
/** Mega Virus: บอทยิงตามมุม "ลูกศรวงกลม" ของตัวเอง (ทันที) เหมือนผู้เล่น — ไม่ auto-aim ใส่บอส */
const aim = (typeof o.balloonBossAimRad === 'number' && Number.isFinite(o.balloonBossAimRad))
? o.balloonBossAimRad
: Math.atan2(bossCy - o.balloonBossCy, bossCx - o.balloonBossCx);
bvx = Math.cos(aim) * bspd;
bvy = Math.sin(aim) * bspd;
bdelay = 0;
} else {
const dx = bossCx - o.balloonBossCx;
const dy = bossCy - o.balloonBossCy;
const L = Math.hypot(dx, dy) || 1;
bvx = (dx / L) * bspd;
bvy = (dy / L) * bspd;
bdelay = balloonBossFireDelayMsPlay();
}
/** บอทยิงตามมุม "ลูกศรวงกลม" ของตัวเอง (ทันที) เหมือนผู้เล่น — ลูกศรหมุนตลอดทั้งทดสอบ/เล่นจริง ไม่ auto-aim ใส่บอส */
const aim = (typeof o.balloonBossAimRad === 'number' && Number.isFinite(o.balloonBossAimRad))
? o.balloonBossAimRad
: Math.atan2(bossCy - o.balloonBossCy, bossCx - o.balloonBossCx);
const bvx = Math.cos(aim) * bspd;
const bvy = Math.sin(aim) * bspd;
/** ยิงจากปลายลูกศรเหมือนผู้เล่น */
balloonBossPendingShots.push({
releaseAt: performance.now() + bdelay,
sx: o.balloonBossCx, sy: o.balloonBossCy,
releaseAt: performance.now(),
sx: o.balloonBossCx + Math.cos(aim) * 26,
sy: o.balloonBossCy - 10.5 + Math.sin(aim) * 26,
vx: bvx,
vy: bvy,
ownerId: bid,
@@ -16214,17 +16219,19 @@
if (wantFire && balloonBossPlayerFireCd <= 0 && me.balloonBossCy != null) {
balloonBossPlayerFireCd = 0.35;
/** ยิงตามมุม "ลูกศรวงกลม" เสมอ (me.balloonBossAimRad อัปเดตทุกเฟรม) — fallback เล็งบอสเฉพาะตอนมุมยังไม่พร้อม */
/** Mega Virus: ยิงทันที (delay 0) ไม่งั้นลูกศรหมุนต่อระหว่างหน่วง → กระสุนโผล่ไม่ตรงลูกศร */
const delayMs = isMegaVirusMissionShellMapPlay() ? 0 : balloonBossFireDelayMsPlay();
/** ลูกศรหมุนตลอดสำหรับ balloon_boss → ยิงทันที (delay 0) ทุกกรณี ทั้งทดสอบ/เล่นจริง ไม่งั้นกระสุนโผล่ไม่ตรงลูกศร */
const delayMs = 0;
const bspd = 480;
const aim = (typeof me.balloonBossAimRad === 'number' && Number.isFinite(me.balloonBossAimRad))
? me.balloonBossAimRad
: Math.atan2(bossC.cy - me.balloonBossCy, bossC.cx - me.balloonBossCx);
const vx = Math.cos(aim) * bspd;
const vy = Math.sin(aim) * bspd;
/** ยิงออกจาก "ปลายลูกศร" ไม่ใช่กลางตัว — วงอยู่เหนือตัว ~10.5px (world) รัศมีปลายหาง ~26px */
balloonBossPendingShots.push({
releaseAt: now + delayMs,
sx: me.balloonBossCx, sy: me.balloonBossCy,
sx: me.balloonBossCx + Math.cos(aim) * 26,
sy: me.balloonBossCy - 10.5 + Math.sin(aim) * 26,
vx,
vy,
ownerId: myId,
@@ -18499,6 +18506,30 @@
}
});
}
/** เกมจริง: bot ไม่อยู่ในผล server → เช็คโซนยืนเอง ผิด = เป็นผี (เหมือน path preview) */
if (isQuiz() && Array.isArray(r.results) && playBotsEnabled()) {
const correctTrueBot = !!r.correctTrue;
others.forEach(function (o, id) {
if (!isPreviewBotId(id) || !o) return;
const br = quizResolveChoiceFromStanding(o.x, o.y, correctTrueBot);
o.botPath = [];
o.botAnswerWander = false;
if (br.right) {
playLiveQuizScores[id] = (playLiveQuizScores[id] || 0) + QUIZ_TF_POINTS_PER_CORRECT;
} else {
playQuizEverWrong[String(id)] = true;
o.quizCannotTrue = true;
o.quizCannotFalse = true;
const ordB = quizPreviewJoinOrderForRespawn(id);
const sp = pickSpawnForJoinPlay(mapData, ordB);
const pos = findNearestOutsideQuizAnswerZonesPlay(mapData, sp.x + 0.5, sp.y + 0.5);
o.x = pos.x + (Math.random() - 0.5) * 0.35;
o.y = pos.y + (Math.random() - 0.5) * 0.35;
o.tx = o.x;
o.ty = o.y;
}
});
}
if (isQuiz() && Array.isArray(r.results) && r.results.some(function (row) { return row && row.right; })) {
spawnQuizTrueFalseScorePopupPlay(!!r.correctTrue);
}
@@ -22069,24 +22100,31 @@
const baseMaxW = Math.min(280, Math.max(100, halfW * 2.4 + tileSize * zDraw * 0.5));
const maxPlaqueW = Math.min(340, Math.round(baseMaxW * heldPs));
const fontPx = Math.max(12, Math.min(22, tileSize * zDraw * 0.22 * heldPs));
ctx.font = `600 ${fontPx}px system-ui, "Segoe UI", "Kanit", sans-serif`;
const padX = 12;
const padY = 9;
const maxInner = Math.min(maxPlaqueW - padX * 2, Math.max(72, tileSize * zDraw * 4.2));
const MAX_LINES = 3;
const MIN_FONT = 8;
let fontPx = Math.max(12, Math.min(22, tileSize * zDraw * 0.22 * heldPs));
let lines;
if (!raw) {
lines = [];
ctx.font = `600 ${fontPx}px system-ui, "Segoe UI", "Kanit", sans-serif`;
} else {
lines = canvasWordWrapLines(ctx, raw, maxInner);
if (!lines.length) lines = [raw];
if (lines.length > 3) lines = lines.slice(0, 3);
for (let i = 0; i < lines.length; i++) {
let s = lines[i];
if (ctx.measureText(s).width <= maxInner) continue;
while (s.length > 2 && ctx.measureText(s + '…').width > maxInner) s = s.slice(0, -1);
lines[i] = s + '…';
/* ย่อฟอนต์จนข้อความพอดีกรอบ (คำไทยยาวไม่มีช่องว่าง → char-wrap + ย่อ ไม่ตัด/ล้นกรอบ) */
for (;;) {
ctx.font = `600 ${fontPx}px system-ui, "Segoe UI", "Kanit", sans-serif`;
lines = quizCarryWrapLinesFit(ctx, raw, maxInner);
if (lines.length <= MAX_LINES || fontPx <= MIN_FONT) break;
fontPx -= 1;
}
if (lines.length > MAX_LINES) {
lines = lines.slice(0, MAX_LINES);
let s = lines[MAX_LINES - 1];
while (s.length > 1 && ctx.measureText(s + '…').width > maxInner) s = s.slice(0, -1);
lines[MAX_LINES - 1] = s + '…';
}
if (!lines.length) lines = [raw];
}
let maxLineW = 0;
for (let i = 0; i < lines.length; i++) {
+1 -1
View File
@@ -5180,7 +5180,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.006131806"></script>
<script src="js/play.js?v=0.006131808"></script>
<div class="version-tag">v —</div>
<style id="qc-howto-mg2-fix">
/* HOW TO PLAY ของ quiz_carry (Minigame-4) -> ให้เหมือน Minigame-2 (gch-mg2-mock)
+34 -2
View File
@@ -26,6 +26,18 @@ const DETECTIVE_MINIGAME_POOL = [
{ key: 'space_shooter', mapId: 'mnpz6rkp', labelTh: 'Minigame-6 ยิงอุกาบาต Violent Crime' },
{ key: 'balloon_boss', mapId: 'mnq1eml7', labelTh: 'Minigame-7 ยิง Mega Virus' },
];
/** โหมดเทสต์: บังคับเกมต่อการ์ด 3 ใบ — array[3], แต่ละช่อง '' = สุ่ม หรือ key จาก DETECTIVE_MINIGAME_POOL · รับ array หรือ key เดี่ยว (เก่า) */
function normalizeForcedMinigameKeys(v) {
let arr;
if (Array.isArray(v)) arr = v;
else if (v != null && String(v).trim()) arr = [v, v, v];
else arr = [];
const one = (k) => {
const s = (k == null ? '' : String(k)).trim();
return s && DETECTIVE_MINIGAME_POOL.some((e) => e.key === s) ? s : '';
};
return [one(arr[0]), one(arr[1]), one(arr[2])];
}
const LOBBY_SLOT_TOTAL = 6;
const LOBBY_THEME_COUNT = 8;
const LOBBY_SKIN_COUNT = 3;
@@ -1805,6 +1817,8 @@ function defaultGameTiming() {
balloonBossPlayerBalloonFallbackUrl: '/Game/img/MegaVirus/Artboard 9.png',
/** จำนวนลูกโป่งต่อคน (0 = ใช้ค่าจากแมป หรือ default 3) */
balloonBossBalloonsPerPlayer: 0,
/** โหมดเทสต์: เกมบังคับต่อการ์ด 3 ใบ — ['','',''] = สุ่มหมด, แต่ละช่องใส่ key เพื่อเจาะจงใบนั้น */
forcedMinigameKeys: ['', '', ''],
};
}
@@ -2189,6 +2203,9 @@ function loadGameTiming() {
balloonBossBalloonsPerPlayer: Object.prototype.hasOwnProperty.call(j, 'balloonBossBalloonsPerPlayer')
? Math.max(0, Math.min(12, Math.floor(Number(j.balloonBossBalloonsPerPlayer)) || 0))
: 0,
forcedMinigameKeys: normalizeForcedMinigameKeys(
j.forcedMinigameKeys != null ? j.forcedMinigameKeys : j.forcedMinigameKey,
),
};
}
} catch (e) { console.error('loadGameTiming', e.message); }
@@ -2394,6 +2411,11 @@ function saveGameTimingToFile(d) {
balloonBossPlayerBalloonImageUrls,
balloonBossPlayerBalloonFallbackUrl,
balloonBossBalloonsPerPlayer,
forcedMinigameKeys: Object.prototype.hasOwnProperty.call(d, 'forcedMinigameKeys')
? normalizeForcedMinigameKeys(d.forcedMinigameKeys)
: (Object.prototype.hasOwnProperty.call(d, 'forcedMinigameKey')
? normalizeForcedMinigameKeys(d.forcedMinigameKey)
: normalizeForcedMinigameKeys(prev.forcedMinigameKeys)),
};
fs.writeFileSync(GAME_TIMING_PATH, JSON.stringify(out, null, 2), 'utf8');
runtimeGameTiming = out;
@@ -2930,8 +2952,18 @@ function normalizeDetectiveCardEntry(entry) {
function assignDetectiveSuspectCardMinigames(space) {
const available = DETECTIVE_MINIGAME_POOL.filter((e) => maps.has(e.mapId));
const shuffled = shuffleQuizQuestions(available.length ? available : [{ key: 'quiz', mapId: SUSPECT_INVESTIGATION_QUIZ_MAP_ID, labelTh: 'QuestionGame' }]);
space.suspectCardMinigames = shuffled.slice(0, 3).map((entry, i) => {
/** โหมดเทสต์ (admin game-timing): เลือกเกมต่อการ์ด 3 ใบ — ช่องที่ตั้ง key = เจาะจง, ช่องว่าง = สุ่ม */
const forcedKeys = normalizeForcedMinigameKeys(runtimeGameTiming && runtimeGameTiming.forcedMinigameKeys);
const randomPool = shuffleQuizQuestions(available.length ? available : [{ key: 'quiz', mapId: SUSPECT_INVESTIGATION_QUIZ_MAP_ID, labelTh: 'QuestionGame' }]);
let ri = 0;
const pick3 = [];
for (let i = 0; i < 3; i++) {
const fk = forcedKeys[i];
let entry = fk ? available.find((e) => e.key === fk) : null;
if (!entry) { entry = randomPool[ri % randomPool.length]; ri += 1; }
pick3.push(entry);
}
space.suspectCardMinigames = pick3.map((entry, i) => {
const md = maps.get(entry.mapId);
return {
cardIndex: i,