เพิ่มเวลา ทนาย ตำรวจ ใส่ Design

This commit is contained in:
2026-06-17 05:02:18 +00:00
parent eb87009382
commit 2f15ac301c
7 changed files with 174 additions and 35 deletions
+37 -1
View File
@@ -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) {
+12
View File
@@ -272,6 +272,18 @@
<h2 class="sq-title">ชุดคำถามพิเศษ — รับการ์ดพิเศษ</h2>
<p class="muted">มี <strong>15 ชุด</strong> = <strong>ระดับ (Level 15)</strong> × <strong>คดี (Case 13)</strong> · แต่ละชุดมีคำถามเป็นของตัวเอง (สุ่มมาเล่นในเกม) และให้ <strong>การ์ดพิเศษ 1 ใบ</strong> เป็นรางวัล · เก็บที่ <code>specialQuizSets</code> ใน <code>quiz-settings.json</code> · <em>English:</em> 15 sets (Level × Case), multiple-choice (A/B/C/D), each grants one special card.</p>
<fieldset class="sq-fieldset">
<legend>เวลาไอคอนอยู่บนฉาก (ทุกชุด)</legend>
<p class="muted" style="margin:0 0 .6rem">ไอคอนคำถามพิเศษตอนขึ้นต้นเกม จะ<strong>หายไปเอง</strong>ถ้าไม่มีใครเก็บภายในเวลาที่ตั้ง · <strong>0 = ไม่หาย</strong> (อยู่จนจบเกม) · เก็บที่ <code>specialQuizIconExpireSec</code> ใน <code>game-timing.json</code></p>
<label style="display:flex;align-items:center;gap:10px;color:#cfd6ea;font-size:14px;">
หายไปใน
<input type="number" id="special-quiz-icon-expire-sec" min="0" max="120" step="1" style="width:90px;padding:6px 8px;font-size:14px;">
วินาที
<button type="button" class="btn" id="btn-special-quiz-icon-expire-save" style="margin-left:8px;">บันทึก</button>
</label>
<p id="special-quiz-icon-expire-msg" class="msg" role="status" style="margin-top:8px;"></p>
</fieldset>
<ol class="sq-steps">
<li><span class="sq-step-no">1</span> เลือก <strong>ระดับ + คดี</strong> ที่จะแก้</li>
<li><span class="sq-step-no">2</span> ตั้ง <strong>การ์ดพิเศษ</strong> ของชุดนั้น</li>
+6 -4
View File
@@ -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"
},
+1 -1
View File
@@ -102,5 +102,5 @@
"mnpz6rkp": 6,
"mnq1eml7": 7
},
"troublesomeForceOffer": true
"troublesomeForceOffer": false
}
+34 -2
View File
@@ -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;
}
+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.006131821"></script>
<script src="js/play.js?v=0.006131822"></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)
+83 -26
View File
@@ -592,7 +592,7 @@ function specialQuizTileInGameZone(md, x, y) {
}
/** หา tile ที่เดินถึงได้ (BFS) และไม่อยู่ในโซน gameplay — คืนพิกัดกึ่งกลาง tile */
function pickSpecialQuizSpawnTile(md) {
function pickSpecialQuizSpawnTile(md, occupiedLaneCount) {
const w = md.width || 20;
const h = md.height || 15;
/* quiz_carry (): / "" blockPlayer
@@ -639,7 +639,12 @@ function pickSpecialQuizSpawnTile(md) {
/* gauntlet (): "" gauntletPlayerSpawns
( x กลางทาง) เลนวงผานเกบได/คลกได ไมนลอยออกนอกเลนเกบไมได */
if (md.gameType === 'gauntlet' && Array.isArray(md.gauntletPlayerSpawns) && md.gauntletPlayerSpawns.length) {
const rows = Array.from(new Set(md.gauntletPlayerSpawns
/* ใช้เฉพาะเลนที่ "มีผู้เล่นจริง" (occupiedLaneCount ตัวแรกตามลำดับ P1..PN) — กันไอคอนเกิดในเลนว่าง */
const nLanes = (Number.isFinite(occupiedLaneCount) && occupiedLaneCount > 0)
? Math.min(md.gauntletPlayerSpawns.length, Math.floor(occupiedLaneCount))
: md.gauntletPlayerSpawns.length;
const usableSpawns = md.gauntletPlayerSpawns.slice(0, nLanes);
const rows = Array.from(new Set(usableSpawns
.map((s) => Math.floor(Number(s.y)))
.filter((n) => Number.isFinite(n) && n >= 0 && n < h)));
const laneCands = [];
@@ -828,7 +833,9 @@ function maybeSpawnSpecialQuizForRun(space) {
if (forced) console.log('[special-quiz] FORCE-spawn (debug)', mapId);
const md = (space.mapId && maps.get(space.mapId)) || space.mapData;
if (!md) { console.log('[special-quiz] skip:no-map', mapId); return; }
const tile = pickSpecialQuizSpawnTile(md);
/* จำนวน "เลนที่มีผู้เล่นจริง" (คนจริง + บอท) — gauntlet เกิดไอคอนเฉพาะเลนที่ใช้จริง ไม่ใช่ทุกเลนที่ config */
const occupiedLanes = (space.peers ? space.peers.size : 0) + effectiveBotSlotCount(space);
const tile = pickSpecialQuizSpawnTile(md, occupiedLanes);
if (!tile) { console.log('[special-quiz] skip:no-tile', mapId); return; }
const iconType = SPECIAL_QUIZ_ICON_TYPES[Math.floor(Math.random() * SPECIAL_QUIZ_ICON_TYPES.length)];
console.log('[special-quiz] SPAWN', mapId, 'L' + level + '_C' + caseId, iconType, 'tile', tile.x, tile.y);
@@ -850,6 +857,24 @@ function emitSpecialQuizIcon(sid, space) {
if (payload) io.to(sid).emit('special-quiz-icon', payload);
}
/** ตั้งเวลาให้ไอคอนคำถามพิเศษ "หายไป" หลัง N วินาที ถ้ายังไม่มีใครเก็บ (ตั้งค่าใน admin) */
function scheduleSpecialQuizIconExpiry(sid, space) {
const sq = space && space.specialQuiz;
if (!sq || sq.triggered || sq.done) return;
const sec = readSpecialQuizIconExpireSec();
if (!(sec > 0)) return; /* 0 = ไม่หาย */
const t = setTimeout(() => {
const sp = spaces.get(sid);
if (!sp || !sp.specialQuiz) return;
if (sp.specialQuiz.triggered || sp.specialQuiz.done) return; /* ถูกเก็บ/ทริกแล้ว */
sp.specialQuiz = null;
io.to(sid).emit('special-quiz-icon-clear', {});
console.log('[special-quiz] icon EXPIRED after', sec + 's');
}, sec * 1000);
if (!Array.isArray(space.specialQuizTimers)) space.specialQuizTimers = [];
space.specialQuizTimers.push(t);
}
/** roster ของเซสชัน: ผู้เล่นจริง + บอทเติมช่อง (บอทตอบถูกเสมอ) — โชว์ในแผง "ใครตอบอะไร" */
function buildSpecialQuizRoster(space) {
const roster = [];
@@ -1976,9 +2001,18 @@ function defaultGameTiming() {
testSpecialCardByMap: {},
/** โหมดเทสต์: บังคับเสนอบทตัวป่วน (ปกติเสนอเฉพาะเมื่อมีคนจริง > 4) */
troublesomeForceOffer: false,
/** เวลาที่ไอคอนคำถามพิเศษอยู่บนฉากก่อนหายไป (วินาที) — 0 = ไม่หาย */
specialQuizIconExpireSec: 10,
};
}
/** อ่านเวลา expiry ไอคอนคำถามพิเศษ (วินาที) จาก game-timing — clamp 0..120 */
function readSpecialQuizIconExpireSec() {
const v = Math.floor(Number(runtimeGameTiming && runtimeGameTiming.specialQuizIconExpireSec));
if (!Number.isFinite(v) || v < 0) return 10;
return Math.min(120, v);
}
function clampStackSwingHz(n) {
const v = Number(n);
if (Number.isNaN(v)) return STACK_SWING_HZ_DEFAULT;
@@ -2365,6 +2399,9 @@ function loadGameTiming() {
),
testSpecialCardByMap: normalizeTestSpecialCardByMap(j.testSpecialCardByMap),
troublesomeForceOffer: !!j.troublesomeForceOffer,
specialQuizIconExpireSec: Object.prototype.hasOwnProperty.call(j, 'specialQuizIconExpireSec')
? Math.max(0, Math.min(120, Math.floor(Number(j.specialQuizIconExpireSec)) || 0))
: 10,
};
}
} catch (e) { console.error('loadGameTiming', e.message); }
@@ -2581,6 +2618,9 @@ function saveGameTimingToFile(d) {
troublesomeForceOffer: Object.prototype.hasOwnProperty.call(d, 'troublesomeForceOffer')
? !!d.troublesomeForceOffer
: !!prev.troublesomeForceOffer,
specialQuizIconExpireSec: Object.prototype.hasOwnProperty.call(d, 'specialQuizIconExpireSec')
? Math.max(0, Math.min(120, Math.floor(Number(d.specialQuizIconExpireSec)) || 0))
: (prev.specialQuizIconExpireSec != null ? Math.max(0, Math.min(120, Math.floor(Number(prev.specialQuizIconExpireSec)) || 0)) : 10),
};
fs.writeFileSync(GAME_TIMING_PATH, JSON.stringify(out, null, 2), 'utf8');
runtimeGameTiming = out;
@@ -3343,6 +3383,7 @@ function beginDetectiveSuspectMinigame(sid, space, cardEntry, selectedIndex) {
/* สุ่มไอคอนคำถามพิเศษสำหรับรอบนี้ (1 ใน 3) — ส่งจริงผ่าน joinCb เมื่อ client เข้า play.html */
maybeSpawnSpecialQuizForRun(space);
emitSpecialQuizIcon(sid, space);
scheduleSpecialQuizIconExpiry(sid, space);
let si = 0;
space.peers.forEach((p) => {
@@ -6751,6 +6792,33 @@ function initTroublesomeState(space) {
if (space.suspectPhaseActive == null) space.suspectPhaseActive = false;
}
/** เปิดเฟส "เลือกผู้ต้องสงสัย" ให้ทั้งห้อง — เรียกหลังตอบข้อเสนอตัวป่วน หรือเมื่อข้ามการเสนอ (คน ≤ 4) */
function openSuspectPhaseForRoom(sid, space, disruptorAccepted) {
if (!space) return;
let idx = Math.floor(Number(space.suspectPickIndex));
if (Number.isNaN(idx) || idx < 0 || idx > 2) idx = 0;
space.suspectPickIndex = idx;
if (!space.suspectCardMinigames || space.suspectCardMinigames.length < 3) {
assignDetectiveSuspectCardMinigames(space);
}
space.suspectPhaseActive = true;
const phaseBase = {
hostId: space.hostId,
selectedIndex: space.suspectPickIndex,
disruptorAccepted: !!disruptorAccepted,
cardMinigames: space.suspectCardMinigames || [],
};
space.peers.forEach((_p, pid) => {
const sock = io.sockets.sockets.get(pid);
if (!sock) return;
sock.emit('suspect-phase-open', {
...phaseBase,
myPlayerEvidence: clonePlayerEvidence(space, pid),
suspectProgress: playerSuspectProgressFromEvidence(space, pid),
});
});
}
function attemptTroublesomeRoll(sid, force) {
const space = spaces.get(sid);
if (!space || space.troublesomeOfferSent) return;
@@ -6760,7 +6828,17 @@ function attemptTroublesomeRoll(sid, force) {
/* เสนอบทตัวป่วนเฉพาะเมื่อมี "คนจริง > 4 คน" — ยกเว้น admin บังคับ (troublesomeForceOffer) */
const forceOffer = !!(runtimeGameTiming && runtimeGameTiming.troublesomeForceOffer);
if (peerIds.length <= 4 && !forceOffer) {
space.troublesomeOfferSent = true; /* มาร์คว่าผ่านรอบนี้ไปแล้ว — ไม่มีตัวป่วน */
/* ไม่เสนอบทตัวป่วน (คน ≤ 4) → ข้ามไปเปิดหน้าเลือกผู้ต้องสงสัยเลย (ไม่มีตัวป่วน) */
space.troublesomeOfferSent = true;
space.troublesomeResponseReceived = true;
space.troublesomeAccept = false;
space.disruptorPlayerKey = '';
space.disruptorNickname = '';
if (space.troublesomeDebTimer) clearTimeout(space.troublesomeDebTimer);
if (space.troublesomeMaxTimer) clearTimeout(space.troublesomeMaxTimer);
space.troublesomeDebTimer = null;
space.troublesomeMaxTimer = null;
openSuspectPhaseForRoom(sid, space, false);
return;
}
space.troublesomeOfferSent = true;
@@ -7534,28 +7612,7 @@ io.on('connection', (socket) => {
space.disruptorPlayerKey = '';
space.disruptorNickname = '';
}
let idx = Math.floor(Number(space.suspectPickIndex));
if (Number.isNaN(idx) || idx < 0 || idx > 2) idx = 0;
space.suspectPickIndex = idx;
if (!space.suspectCardMinigames || space.suspectCardMinigames.length < 3) {
assignDetectiveSuspectCardMinigames(space);
}
space.suspectPhaseActive = true;
const phaseBase = {
hostId: space.hostId,
selectedIndex: space.suspectPickIndex,
disruptorAccepted: accept,
cardMinigames: space.suspectCardMinigames || [],
};
space.peers.forEach((_p, pid) => {
const sock = io.sockets.sockets.get(pid);
if (!sock) return;
sock.emit('suspect-phase-open', {
...phaseBase,
myPlayerEvidence: clonePlayerEvidence(space, pid),
suspectProgress: playerSuspectProgressFromEvidence(space, pid),
});
});
openSuspectPhaseForRoom(sid, space, accept);
socket.emit('troublesome-ack', { ok: true, accept });
});