diff --git a/www/html/Admin/admin.js b/www/html/Admin/admin.js
index adc6857..e979fd8 100644
--- a/www/html/Admin/admin.js
+++ b/www/html/Admin/admin.js
@@ -4136,6 +4136,40 @@
.then(function () { if (btn) btn.disabled = false; });
}
+ function loadSpecialQuizIconExpirePanel() {
+ var inp = el('special-quiz-icon-expire-sec');
+ if (!inp) return;
+ gameTimingFetch('GET')
+ .then(function (data) {
+ var v = (data && data.specialQuizIconExpireSec != null) ? Math.floor(Number(data.specialQuizIconExpireSec)) : 10;
+ if (!Number.isFinite(v) || v < 0) v = 10;
+ inp.value = String(Math.min(120, v));
+ setMsg('special-quiz-icon-expire-msg', '', '');
+ })
+ .catch(function (e) { setMsg('special-quiz-icon-expire-msg', e.message || 'โหลดไม่ได้', 'error'); });
+ }
+ function saveSpecialQuizIconExpirePanel() {
+ var inp = el('special-quiz-icon-expire-sec');
+ if (!inp) return;
+ var v = Math.floor(Number(inp.value));
+ if (!Number.isFinite(v) || v < 0) v = 10;
+ v = Math.min(120, v);
+ inp.value = String(v);
+ var btn = el('btn-special-quiz-icon-expire-save');
+ if (btn) btn.disabled = true;
+ gameTimingFetch('GET')
+ .then(function (data) {
+ data.specialQuizIconExpireSec = v;
+ return gameTimingFetch('PUT', data);
+ })
+ .then(function (res) {
+ if (res && res.specialQuizIconExpireSec != null) inp.value = String(res.specialQuizIconExpireSec);
+ setMsg('special-quiz-icon-expire-msg', v > 0 ? ('บันทึกแล้ว — ไอคอนจะหายใน ' + v + ' วินาที (เริ่มเกมใหม่เพื่อเทสต์)') : 'บันทึกแล้ว — ไอคอนจะไม่หาย (อยู่จนจบเกม)', 'ok');
+ })
+ .catch(function (e) { setMsg('special-quiz-icon-expire-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;
@@ -4643,7 +4677,7 @@
}
});
if (name === 'quiz') loadQuizSettingsPanel();
- if (name === 'special-quiz') loadSpecialQuizPanel();
+ if (name === 'special-quiz') { loadSpecialQuizPanel(); loadSpecialQuizIconExpirePanel(); }
if (name === 'evidence-cards') loadEvidenceCardsPanel();
if (name === 'case-media') loadCaseMediaPanel();
if (name === 'quiz-carry') loadQuizCarryPanel();
@@ -4884,6 +4918,8 @@
if (specialCardSaveBtn) specialCardSaveBtn.addEventListener('click', saveSpecialCardByMapPanel);
var troublesomeForceSaveBtn = el('btn-troublesome-force-save');
if (troublesomeForceSaveBtn) troublesomeForceSaveBtn.addEventListener('click', saveTroublesomeForcePanel);
+ var sqIconExpireSaveBtn = el('btn-special-quiz-icon-expire-save');
+ if (sqIconExpireSaveBtn) sqIconExpireSaveBtn.addEventListener('click', saveSpecialQuizIconExpirePanel);
syncTestModeUi();
/* ฟัง storage event — sync เมื่อเปิดหลายแท็บ */
window.addEventListener('storage', function (e) {
diff --git a/www/html/Admin/index.html b/www/html/Admin/index.html
index d61e198..3c34aff 100644
--- a/www/html/Admin/index.html
+++ b/www/html/Admin/index.html
@@ -272,6 +272,18 @@
ชุดคำถามพิเศษ — รับการ์ดพิเศษ
มี 15 ชุด = ระดับ (Level 1–5) × คดี (Case 1–3) · แต่ละชุดมีคำถามเป็นของตัวเอง (สุ่มมาเล่นในเกม) และให้ การ์ดพิเศษ 1 ใบ เป็นรางวัล · เก็บที่ specialQuizSets ใน quiz-settings.json · English: 15 sets (Level × Case), multiple-choice (A/B/C/D), each grants one special card.
+
+
- 1 เลือก ระดับ + คดี ที่จะแก้
- 2 ตั้ง การ์ดพิเศษ ของชุดนั้น
diff --git a/www/html/Admin/private/store.json b/www/html/Admin/private/store.json
index 39ff16c..b8e840d 100644
--- a/www/html/Admin/private/store.json
+++ b/www/html/Admin/private/store.json
@@ -38,9 +38,9 @@
"providerUserId": "p_1775109142385_wq7wfy1p32j",
"notes": "auto: player-coins",
"blocked": false,
- "coins": 280,
+ "coins": 330,
"createdAt": "2026-04-02T05:52:21+00:00",
- "updatedAt": "2026-06-16T14:58:05+00:00",
+ "updatedAt": "2026-06-17T03:36:39+00:00",
"daily": {
"anchorMs": 1781197200000,
"claimedDays": [
@@ -54,13 +54,15 @@
],
"lockUntilMs": 0
},
- "score": 230,
+ "score": 280,
"scoreByCase": {
"1": 10,
"10": 70,
"8": 110,
"11": 20,
- "13": 20
+ "13": 20,
+ "14": 20,
+ "9": 30
},
"lbName": "Q"
},
diff --git a/www/html/Game/data/game-timing.json b/www/html/Game/data/game-timing.json
index 14297fd..2c0e146 100644
--- a/www/html/Game/data/game-timing.json
+++ b/www/html/Game/data/game-timing.json
@@ -102,5 +102,5 @@
"mnpz6rkp": 6,
"mnq1eml7": 7
},
- "troublesomeForceOffer": true
+ "troublesomeForceOffer": false
}
\ No newline at end of file
diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js
index 6e40535..50da276 100644
--- a/www/html/Game/public/js/play.js
+++ b/www/html/Game/public/js/play.js
@@ -3126,6 +3126,26 @@
return true;
}
+ /** สุ่มช่องในโซนผีที่ footprint ลงได้ (ใช้คุมบอทผีให้อยู่ในโซน) — คืน {x,y} หรือ null */
+ function randomGhostZoneCellPlay() {
+ const g = quizGhostAreaGridPlay();
+ if (!g) return null;
+ const cells = [];
+ for (let y = 0; y < g.length; y++) {
+ const row = g[y] || [];
+ for (let x = 0; x < row.length; x++) {
+ if (row[x] === 1 && quizGhostFootprintInsidePlay(x, y)) cells.push({ x: x, y: y });
+ }
+ }
+ if (!cells.length) {
+ for (let y = 0; y < g.length; y++) {
+ const row = g[y] || [];
+ for (let x = 0; x < row.length; x++) if (row[x] === 1) cells.push({ x: x, y: y });
+ }
+ }
+ return cells.length ? cells[Math.floor(Math.random() * cells.length)] : null;
+ }
+
/** ตรวจว่า peer/me ตอบผิดในแมป mng8a80o แล้ว → คุมผีแทน avatar */
function isQuizWrongGhostActiveForEnt(entId) {
if (!isQuizQuestionMissionUiMapPlay()) return false;
@@ -19845,6 +19865,11 @@
/* ข้าม path-following → ตกไป wander logic ด้านล่าง */
o.botPath = [];
o.botAnswerWander = true;
+ /* คุมบอทผีให้อยู่ในโซนผี — ถ้าหลุดออกนอกโซน ดึงกลับเข้าช่องในโซน (เหมือน me ที่โดนวาร์ป) */
+ if (quizGhostAreaGridPlay() && !quizGhostFootprintInsidePlay(o.x, o.y)) {
+ const gz = randomGhostZoneCellPlay();
+ if (gz) { o.x = gz.x; o.y = gz.y; o.tx = gz.x; o.ty = gz.y; o.botWanderTX = undefined; o.botWanderTY = undefined; }
+ }
}
if (inAnswerPhase && o.botPath && o.botPath.length > 0 && !o.botAnswerWander) {
stepPreviewBotAlongPath(o, w, h);
@@ -19858,8 +19883,15 @@
&& Math.hypot(o.botWanderTX - o.x, o.botWanderTY - o.y) < 0.5;
const timedOutQ = typeof o.botWanderRetargetAt === 'number' && now >= o.botWanderRetargetAt;
if (typeof o.botWanderTX !== 'number' || typeof o.botWanderTY !== 'number' || arrivedQ || timedOutQ) {
- o.botWanderTX = 1 + Math.random() * Math.max(1, w - 2);
- o.botWanderTY = 1 + Math.random() * Math.max(1, h - 2);
+ if (ghostWanderBot) {
+ /* บอทผี: สุ่มเป้าหมายเฉพาะ "ในโซนผี" → ไม่เดินออกนอกเขต */
+ const gz = randomGhostZoneCellPlay();
+ o.botWanderTX = gz ? gz.x : o.x;
+ o.botWanderTY = gz ? gz.y : o.y;
+ } else {
+ o.botWanderTX = 1 + Math.random() * Math.max(1, w - 2);
+ o.botWanderTY = 1 + Math.random() * Math.max(1, h - 2);
+ }
o.botWanderRetargetAt = now + 3000 + Math.floor(Math.random() * 4500);
o.botWanderPauseUntil = (arrivedQ && Math.random() < 0.4) ? now + 500 + Math.floor(Math.random() * 1500) : 0;
}
diff --git a/www/html/Game/public/play.html b/www/html/Game/public/play.html
index 7392a7d..1d32538 100644
--- a/www/html/Game/public/play.html
+++ b/www/html/Game/public/play.html
@@ -5180,7 +5180,7 @@
-
+
v —