diff --git a/www/html/Admin/admin.js b/www/html/Admin/admin.js
index c7e1f8f..c165dfc 100644
--- a/www/html/Admin/admin.js
+++ b/www/html/Admin/admin.js
@@ -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) {
diff --git a/www/html/Admin/index.html b/www/html/Admin/index.html
index a20962a..b1dbc34 100644
--- a/www/html/Admin/index.html
+++ b/www/html/Admin/index.html
@@ -1112,6 +1112,49 @@
+
+ บังคับมินิเกม (เทสต์ตอนเล่นจริง)
+ เลือกเกมต่อการ์ดผู้ต้องสงสัย 3 ใบ → บันทึก → เริ่มคดีใหม่ (สร้างห้อง → LobbyA → เลือกคดี → LobbyB) · ช่องที่เลือก "— สุ่ม —" จะสุ่มจาก 7 เกมเหมือนเดิม · เลือก "— สุ่ม —" ครบทั้ง 3 ช่อง = กลับเป็นปกติ
+
+
+
+
+
+
+
diff --git a/www/html/Admin/private/store.json b/www/html/Admin/private/store.json
index 87594b6..6d707e1 100644
--- a/www/html/Admin/private/store.json
+++ b/www/html/Admin/private/store.json
@@ -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",
diff --git a/www/html/Game/data/game-timing.json b/www/html/Game/data/game-timing.json
index 62342ee..fa15f0a 100644
--- a/www/html/Game/data/game-timing.json
+++ b/www/html/Game/data/game-timing.json
@@ -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"
+ ]
}
\ No newline at end of file
diff --git a/www/html/Game/public/game-timing-admin.html b/www/html/Game/public/game-timing-admin.html
index e1c0abd..4b118bd 100644
--- a/www/html/Game/public/game-timing-admin.html
+++ b/www/html/Game/public/game-timing-admin.html
@@ -47,6 +47,7 @@
+
@@ -100,6 +101,50 @@
+
+
เทสต์ — บังคับมินิเกมที่ออก
+
เลือกเกมต่อการ์ดผู้ต้องสงสัย 3 ใบ — ช่องที่เลือก "— สุ่ม —" จะสุ่มจาก 7 เกมเหมือนเดิม · ตั้งครบแล้วเริ่มคดีใหม่ (LobbyA→LobbyB) เพื่อเทสต์ตอนเล่นจริง
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -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 || '') : '';
+ }),
};
}
diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js
index 246918e..908eb24 100644
--- a/www/html/Game/public/js/play.js
+++ b/www/html/Game/public/js/play.js
@@ -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++) {
diff --git a/www/html/Game/public/play.html b/www/html/Game/public/play.html
index 48f9c22..02592e0 100644
--- a/www/html/Game/public/play.html
+++ b/www/html/Game/public/play.html
@@ -5180,7 +5180,7 @@
-
+
v —