update game เกมจริงหรือไม่ ถ้าตายหมดให้เกม Overและอื่นๆมากมาย

This commit is contained in:
2026-06-18 16:09:43 +00:00
parent 8fb403a368
commit 7c3c4c9d71
77 changed files with 968 additions and 157 deletions
+41 -1
View File
@@ -4689,6 +4689,7 @@
if (name === 'stack-game') loadStackGamePanel();
if (name === 'highscore') loadHighscore();
if (name === 'achievements') loadAchievementsPanel();
if (name === 'vote-timing') loadVoteTimingPanel();
if (name === 'test-mode') { loadForcedMinigamePanel(); loadSpecialCardByMapPanel(); loadTroublesomeForcePanel(); }
}
@@ -5188,6 +5189,45 @@
});
})();
/* ===== เวลาโหวต (vote timing) ===== */
function loadVoteTimingPanel() {
gameTimingFetch('GET')
.then(function (data) {
var pk = (data && data.pickVoteSec != null) ? Number(data.pickVoteSec) : 25;
var tr = (data && data.trialVoteSec != null) ? Number(data.trialVoteSec) : 30;
if (el('vote-timing-pick')) el('vote-timing-pick').value = String(Number.isFinite(pk) ? pk : 25);
if (el('vote-timing-trial')) el('vote-timing-trial').value = String(Number.isFinite(tr) ? tr : 30);
setMsg('vote-timing-msg', '', '');
})
.catch(function (e) { setMsg('vote-timing-msg', (e.message || 'โหลดไม่ได้') + ' — เช็ค /Game/api/game-timing', 'error'); });
}
(function bindVoteTiming() {
var btn = el('btn-vote-timing-save');
if (!btn) return;
btn.addEventListener('click', function () {
var pk = Math.max(5, Math.min(120, parseInt((el('vote-timing-pick') || {}).value, 10) || 25));
var tr = Math.max(5, Math.min(180, parseInt((el('vote-timing-trial') || {}).value, 10) || 30));
btn.disabled = true;
gameTimingFetch('GET')
.then(function (data) {
data.pickVoteSec = pk;
data.trialVoteSec = tr;
return gameTimingFetch('PUT', data);
})
.then(function () { return gameTimingFetch('GET'); })
.then(function (fresh) {
var p = (fresh && fresh.pickVoteSec != null) ? fresh.pickVoteSec : pk;
var t = (fresh && fresh.trialVoteSec != null) ? fresh.trialVoteSec : tr;
if (el('vote-timing-pick')) el('vote-timing-pick').value = String(p);
if (el('vote-timing-trial')) el('vote-timing-trial').value = String(t);
setMsg('vote-timing-msg', 'บันทึกแล้ว — โหวตเลือกผู้เล่น ' + p + 's · ชี้ตัวคนร้าย ' + t + 's · ผู้เล่นต้องรีเฟรช/เข้าห้องใหม่', 'ok');
})
.catch(function (e) { setMsg('vote-timing-msg', (e.message || 'บันทึกไม่ได้') + ' — เช็ค Node + /Game/api/game-timing', 'error'); })
.then(function () { btn.disabled = false; });
});
})();
/* ===== Achievements admin ===== */
var achvCatalog = [];
var achvCatalogById = {};
@@ -5549,7 +5589,7 @@
try {
want = new URL(window.location.href).searchParams.get('tab');
} catch (e) { /* ignore */ }
var allowed = { 'change-password': true, oauth: true, 'map-editor': true, quiz: true, 'special-quiz': true, 'quiz-carry': true, 'quiz-battle': true, 'jump-survive': true, 'space-shooter': true, 'mega-virus': true, 'game-timing': true, 'stack-game': true, characters: true, accounts: true, admins: true };
var allowed = { 'change-password': true, oauth: true, 'map-editor': true, quiz: true, 'special-quiz': true, 'vote-timing': true, 'quiz-carry': true, 'quiz-battle': true, 'jump-survive': true, 'space-shooter': true, 'mega-virus': true, 'game-timing': true, 'stack-game': true, characters: true, accounts: true, admins: true };
if (!want || !allowed[want]) return 'oauth';
if (want === 'admins' && (!admin || admin.role !== 'super')) return 'oauth';
return want;
+17 -1
View File
@@ -112,6 +112,7 @@
<div class="admin-tab-sep" aria-hidden="true"><span>คำถาม &amp; การ์ดพิเศษ</span></div>
<button type="button" class="tab" data-tab="special-quiz" role="tab" id="tab-special-quiz" aria-controls="tab-panel-special-quiz"><span class="tab-label">ชุดคำถามพิเศษ</span><span class="tab-desc">15 ชุด (Level × Case) · การ์ดรางวัล</span></button>
<button type="button" class="tab" data-tab="vote-timing" role="tab" id="tab-vote-timing" aria-controls="tab-panel-vote-timing"><span class="tab-label">เวลาโหวต</span><span class="tab-desc">Silence/Ban · ชี้ตัวคนร้าย</span></button>
<button type="button" class="tab" data-tab="evidence-cards" role="tab" id="tab-evidence-cards" aria-controls="tab-panel-evidence-cards"><span class="tab-label">การ์ดหลักฐาน</span><span class="tab-desc">3 คดี × ผู้ต้องสงสัย × 3 ใบ · รูป/ข้อความ</span></button>
<button type="button" class="tab" data-tab="case-media" role="tab" id="tab-case-media" aria-controls="tab-panel-case-media"><span class="tab-label">รูปคดี &amp; Cutscene</span><span class="tab-desc">15 คดี · รูปเลือกคดี/รายละเอียด/เรื่องราว 2 รูป</span></button>
@@ -1131,6 +1132,21 @@
</div>
</section>
<section id="tab-panel-vote-timing" class="tab-panel card" hidden role="tabpanel" aria-labelledby="tab-vote-timing">
<h2>เวลาโหวต</h2>
<p class="muted">ตั้งเวลานับถอยหลังของการโหวตในเกม (วินาที) · มีผลเมื่อผู้เล่นรีเฟรชหน้าเล่น/เข้าห้องใหม่</p>
<form id="form-vote-timing" class="form-grid form-inline" autocomplete="off">
<label>โหวตเลือกผู้เล่น (การ์ด Silence/Ban)
<input type="number" id="vote-timing-pick" name="pickVoteSec" min="5" max="120" step="1" value="25"> วินาที
</label>
<label>โหวตชี้ตัวคนร้าย (พิจารณาคดี)
<input type="number" id="vote-timing-trial" name="trialVoteSec" min="5" max="180" step="1" value="30"> วินาที
</label>
<button type="button" class="btn btn-primary" id="btn-vote-timing-save">บันทึก</button>
</form>
<p id="vote-timing-msg" class="msg" role="status"></p>
</section>
<section id="tab-panel-admins" class="tab-panel card" hidden role="tabpanel" aria-labelledby="tab-admins">
<h2>บัญชีแอดมิน</h2>
<p class="muted">เฉพาะ <strong>super admin</strong> เท่านั้นที่เพิ่ม/ลบแอดมินได้</p>
@@ -1267,6 +1283,6 @@
</div>
</main>
</div>
<script src="admin.js?v=84"></script>
<script src="admin.js?v=85"></script>
</body>
</html>
+9 -7
View File
@@ -38,9 +38,9 @@
"providerUserId": "p_1775109142385_wq7wfy1p32j",
"notes": "auto: player-coins",
"blocked": false,
"coins": 565,
"coins": 815,
"createdAt": "2026-04-02T05:52:21+00:00",
"updatedAt": "2026-06-17T08:31:26+00:00",
"updatedAt": "2026-06-18T15:59:38+00:00",
"daily": {
"anchorMs": 1781197200000,
"claimedDays": [
@@ -52,13 +52,13 @@
false,
false
],
"lockUntilMs": 1781715600000
"lockUntilMs": 0
},
"score": 490,
"score": 730,
"scoreByCase": {
"1": 10,
"10": 120,
"8": 170,
"10": 310,
"8": 220,
"11": 20,
"13": 20,
"14": 20,
@@ -69,7 +69,9 @@
},
"lbName": "Q",
"achievements": {
"d1_minigame_solver": 2
"d1_minigame_solver": 20,
"a1_first_deduction": 1,
"a5_truth_hunter": 1
}
},
{
+3 -3
View File
@@ -89,9 +89,9 @@
"balloonBossPlayerBalloonFallbackUrl": "/Game/img/MegaVirus/Artboard%209.png",
"balloonBossBalloonsPerPlayer": 3,
"forcedMinigameKeys": [
"jump_survive",
"space_shooter",
"balloon_boss"
"quiz",
"gauntlet",
"stack"
],
"testSpecialCardByMap": {
"mng8a80o": 1,
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
{"qbroom1":[{"name":"Q","score":7,"characterId":"char-1777017632279","ts":1781171902962},{"name":"ผู้เล่น8916","score":4,"characterId":"char-1777017632279","ts":1781074083031},{"name":"ผู้เล่น2821","score":4,"characterId":"char-1777017632279","ts":1781079179108}],"qbroom2":[{"name":"Q","score":7,"characterId":"char-1777017632279","ts":1781175468983},{"name":"ผู้เล่น2612","score":1,"characterId":"char-1777017632279","ts":1781079252376}],"qbroom3":[{"name":"Q","score":9,"characterId":"char-1777017632279","ts":1781175654893}],"qbroom5":[{"name":"Q","score":6,"characterId":"char-1777017632279","ts":1781675299850}],"qbroom6":[{"name":"Q","score":3,"characterId":"char-1777017632279","ts":1781675383175}]}
{"qbroom1":[{"name":"Q","score":7,"characterId":"char-1777017632279","ts":1781171902962},{"name":"ผู้เล่น8916","score":4,"characterId":"char-1777017632279","ts":1781074083031},{"name":"ผู้เล่น2821","score":4,"characterId":"char-1777017632279","ts":1781079179108}],"qbroom2":[{"name":"Q","score":7,"characterId":"char-1777017632279","ts":1781175468983},{"name":"ผู้เล่น2612","score":1,"characterId":"char-1777017632279","ts":1781079252376}],"qbroom3":[{"name":"Q","score":9,"characterId":"char-1777017632279","ts":1781175654893}],"qbroom5":[{"name":"Q","score":6,"characterId":"char-1777017632279","ts":1781675299850}],"qbroom6":[{"name":"Q","score":3,"characterId":"char-1777017632279","ts":1781675383175}],"qbroom9":[{"name":"Q","score":7,"characterId":"char-1777017632279","ts":1781696345322}]}
Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+93 -16
View File
@@ -359,6 +359,11 @@
else sessionStorage.removeItem('justiceDetectiveMyEvidence');
}
} catch (e) { /* ignore */ }
/* MG2: เข้าเส้นชัยแล้วแต่ยังมีคนวิ่งอยู่ → รอในฉาก (server จะพากลับ LobbyB พร้อมกันตอนทุกคนจบ) */
if (res && res.holdInScene) {
showGauntletFinishWaitOverlay();
return;
}
/* โชว์การ์ดหลักฐานที่เพิ่งได้ "ในฉากมินิเกม" ก่อนกลับ LobbyB */
if (res && typeof res.suspectIndex === 'number' && res.suspectIndex >= 0) {
showInSceneEvidenceReveal(res, go);
@@ -371,6 +376,22 @@
}
}
/** MG2: ป้าย "เข้าเส้นชัยแล้ว — รอผู้เล่นอื่น" ระหว่างรอคนที่ยังวิ่งอยู่ (จะกลับ LobbyB พร้อมกันตอนเกมจบ) */
function showGauntletFinishWaitOverlay() {
var ov = document.getElementById('gauntlet-finish-wait');
if (!ov) {
ov = document.createElement('div');
ov.id = 'gauntlet-finish-wait';
ov.style.cssText = 'position:fixed;inset:0;z-index:100000;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;background:radial-gradient(ellipse at center,rgba(10,16,32,.82),rgba(6,9,20,.95));backdrop-filter:blur(3px)';
ov.innerHTML =
'<div style="font:900 38px/1.2 Kanit,system-ui,sans-serif;color:#5eeaff;text-shadow:0 0 18px rgba(94,234,255,.6);text-align:center">🏁 เข้าเส้นชัยแล้ว!</div>' +
'<div style="font:700 22px/1.4 Kanit,system-ui,sans-serif;color:#e7ecff;text-align:center">รอผู้เล่นคนอื่นเข้าเส้นชัย...</div>' +
'<div class="gfw-dots" style="font:900 30px/1 Kanit,system-ui,sans-serif;color:#7aa2f7;letter-spacing:4px">• • •</div>';
document.body.appendChild(ov);
}
ov.style.display = 'flex';
}
/* ===== หน้าโชว์การ์ดหลักฐานในฉากมินิเกม (ก่อนกลับ LobbyB) ===== */
var __evidenceConfigCache = null;
function ensureEvidenceConfigLoaded(cb) {
@@ -1004,6 +1025,8 @@
const ts = mapData.tileSize || 32;
const { cw } = getCharacterFootprintWH(mapData);
const mx = getGauntletCrownHeistMaxEntityXPlay(mapData);
/* "" (tx )
(ตอนหย เราจะ snap วทเหลอมายนทเสนเดยวกนดวย gauntletSnapLivingToFinishPlay) */
let leadTx = -Infinity;
const consider = (ent) => {
if (!ent || ent.gauntletEliminated) return;
@@ -1024,6 +1047,37 @@
return alignWorld;
}
/** : "" ,
เปาหมาย = ตำแหนงเสนชยท latch ไว (คงท) ายงไม latch ใช tx วนำส
เรยกทกเฟรมระหวาง freeze/latched เพอกนบอทท sim งขย "ไหล" หลดจากเส */
function gauntletSnapLivingToFinishPlay() {
if (!mapData || !isGauntletCrownHeistMapPlay()) return;
const ts = mapData.tileSize || 32;
const { cw } = getCharacterFootprintWH(mapData);
let targetTx = null;
if (gauntletCrownRunwayFinishAlignLatchedWorldX != null) {
targetTx = gauntletCrownRunwayFinishAlignLatchedWorldX / ts - cw * 0.5;
} else {
let leadTx = -Infinity;
const consider = (ent) => {
if (!ent || ent.gauntletEliminated) return;
const tx = Number.isFinite(ent.tx) ? ent.tx : Number(ent.x);
if (Number.isFinite(tx)) leadTx = Math.max(leadTx, tx);
};
consider(me);
others.forEach((o, id) => { if (!playBotsEnabled() && isPreviewBotId(id)) return; consider(o); });
if (Number.isFinite(leadTx) && leadTx !== -Infinity) targetTx = leadTx;
}
if (targetTx == null || !Number.isFinite(targetTx)) return;
const snap = (ent) => {
if (!ent || ent.gauntletEliminated) return;
if (Number.isFinite(ent.x)) ent.x = targetTx;
if (ent.tx != null) ent.tx = targetTx;
};
snap(me);
others.forEach((o, id) => { if (!playBotsEnabled() && isPreviewBotId(id)) return; snap(o); });
}
/**
* คอลมนบนกรดทเส FINISH องแนบ (map world px)
@@ -1336,6 +1390,7 @@
const leadAlign = gauntletCrownRunwayLeadingEntityAlignWorldXPlay();
if (leadAlign != null) gauntletCrownRunwayFinishAlignLatchedWorldX = leadAlign;
}
gauntletSnapLivingToFinishPlay(); /* ทุกคนที่ยังไม่ตายมายืนที่เส้นชัย ไม่มีใครเลยเส้น */
gauntletCrownRunwaySnapScrollToFinishStopPlay();
gauntletCrownRunwayBgStripFreezeSinceMs = Date.now();
/* แจ้ง server ครั้งเดียว: ถึงเส้นชัยแล้ว → หยุด obstacle + collision (กัน -10 ตอน obstacle หายแล้ว) */
@@ -1438,10 +1493,11 @@
if (specialQuizFreeze) return;
/** พรีรัน (howto / countdown) — ห้ามเลื่อนก่อน GO · Mega Virus ใช้ shell เดียวกัน */
if (usesCrownLobbyShellPlay() && gauntletCrownPregamePhase !== 'live') return;
if (gauntletCrownRunwayBgFinishLatched) return;
if (gauntletCrownRunwayBgFinishLatched) { gauntletSnapLivingToFinishPlay(); return; }
if (gauntletCrownRunwayBgStripFreezeSinceMs > 0) {
gauntletCrownRunwayMaintainFinishScrollPlay();
gauntletSnapLivingToFinishPlay(); /* ตรึงทุกคนที่ยังไม่ตายไว้ที่เส้นชัยทุกเฟรม กันบอทไหลหลุด */
if (Date.now() - gauntletCrownRunwayBgStripFreezeSinceMs >= GAUNTLET_CROWN_RUNWAY_POST_STOP_MS) {
gauntletCrownRunwayBgFinishLatched = true;
gauntletCrownRunwayBgStripFreezeSinceMs = 0;
@@ -1475,6 +1531,7 @@
const leadAlign = gauntletCrownRunwayLeadingEntityAlignWorldXPlay();
if (leadAlign != null) gauntletCrownRunwayFinishAlignLatchedWorldX = leadAlign;
}
gauntletSnapLivingToFinishPlay(); /* ทุกคนที่ยังไม่ตายมายืนที่เส้นชัย ไม่มีใครเลยเส้น */
gauntletCrownRunwaySnapScrollToFinishStopPlay();
gauntletCrownRunwayBgStripFreezeSinceMs = Date.now();
try {
@@ -1963,8 +2020,9 @@
function walkAnimPhaseIndex(now, isWalking) {
if (!isWalking) return 0;
const t = (typeof now === 'number' ? now : Date.now());
/* gauntlet วิ่งหลบ: ย่นเวลาเฟรม ~1.85× ให้ขาสลับเร็ว = วิ่ง (ไม่ใช่เดิน) */
const frameMs = gauntletRunnerAnimActivePlay() ? CHARACTER_ANIM_FRAME_MS * 0.54 : CHARACTER_ANIM_FRAME_MS;
/* gauntlet : "" 0.40 (80ms/)
(sprite แค 4 เฟรม าเรวกวาน เช 0.27 เฟรมจะสลบถเกนจนด/กระต) */
const frameMs = gauntletRunnerAnimActivePlay() ? CHARACTER_ANIM_FRAME_MS * 0.40 : CHARACTER_ANIM_FRAME_MS;
return Math.floor(t / frameMs) % CHARACTER_ANIM_FRAMES;
}
@@ -2182,6 +2240,16 @@
if (!Number.isFinite(idx) || idx < 0) return null;
/* server () "" ( hash )
bot เดยวก: bot N ((N % 8)+1), สก ((N % 3)+1) */
/* 1) สีที่ lobby บันทึกไว้ (ให้ตรงกับ lobby เป๊ะ) — ไม่เขียนทับ */
try {
const savedMap = JSON.parse(sessionStorage.getItem(lobbyBotTintsStorageKeyPlay()) || '{}');
const saved = savedMap['__lobby_bot_' + idx];
if (saved && (saved.colorTheme || saved.colorSkin)) {
const st = playTintFromLobbyRgb(saved.colorTheme, saved.colorSkin);
if (st) return st;
}
} catch (e) { /* ignore */ }
/* 2) ไม่มีค่าจาก lobby → ใช้ธีม server / กระจายตามดัชนี แล้วบันทึกไว้ */
const slot = playLobbyBotThemes[idx] || { themeIndex: (idx % 8) + 1, skinToneIndex: (idx % 3) + 1 };
const tint = playTintFromLobbyThemeIndices(slot.themeIndex, slot.skinToneIndex);
if (!tint) return null;
@@ -13160,6 +13228,7 @@
if (quizBody) quizBody.classList.remove('is-hidden');
const awardLayer = document.getElementById('sq-ov-award');
if (awardLayer) awardLayer.classList.add('is-hidden');
if (ov) ov.classList.remove('is-award');
const status = document.getElementById('sq-ov-status');
if (status) status.textContent = 'เลือกคำตอบ — ทุกคนต้องตอบถูกทุกข้อจึงได้การ์ดพิเศษ';
const choicesEl = document.getElementById('sq-ov-choices');
@@ -13388,7 +13457,7 @@
const ov = specialQuizOverlayEl();
const award = document.getElementById('sq-ov-award');
if (award) award.classList.add('is-hidden');
if (ov) ov.classList.add('is-hidden');
if (ov) { ov.classList.add('is-hidden'); ov.classList.remove('is-award'); }
/* แจ้ง server ให้เล่นรอบ quiz (mng8a80o) ต่อ — มินิเกมอื่น freeze ฝั่ง client พอ */
try { socket.emit('special-quiz-continue', {}, function () {}); } catch (e) { /* ignore */ }
endSpecialQuizFreeze();
@@ -13409,32 +13478,40 @@
result.textContent = success ? 'ได้รับการ์ดพิเศษ!' : 'รอบนี้ไม่ได้การ์ดพิเศษ';
result.className = 'sq-ov-result ' + (success ? 'sq-ov-result--ok' : 'sq-ov-result--fail');
}
/* แผงโชว์การ์ด + ปุ่ม «เล่นต่อ» — เกมหยุดค้างจนกว่าจะกด */
/* แผงโชว์การ์ด (mock 10-Game-1-result) + ปุ่ม «เล่นเกมต่อ» — เกมหยุดค้างจนกว่าจะกด */
const award = document.getElementById('sq-ov-award');
const titleEl = document.getElementById('sq-ov-award-title');
const imgEl = document.getElementById('sq-ov-award-img');
const nameEl = document.getElementById('sq-ov-award-name');
const fxEl = document.getElementById('sq-ov-award-effect');
const txtEl = document.getElementById('sq-ov-award-txt');
const effEl = document.getElementById('sq-ov-award-effect-img');
const msgEl = document.getElementById('sq-ov-award-msg');
const btn = document.getElementById('sq-ov-award-continue');
const ovEl = specialQuizOverlayEl();
if (award) {
if (success) {
if (titleEl) titleEl.textContent = 'ได้รับการ์ดพิเศษ!';
const cid = card.cardId;
const REWARD = '/Game/img/special-quiz/reward/';
if (imgEl) {
imgEl.removeAttribute('hidden');
imgEl.src = card.imageUrl || (card.cardId ? '/Game/img/special-cards/card-' + card.cardId + '.png' : '');
imgEl.src = cid ? (REWARD + 'card-' + cid + '.png') : (card.imageUrl || '');
}
if (nameEl) nameEl.textContent = card.en || card.th || ('การ์ด #' + (card.cardId || ''));
if (fxEl) fxEl.textContent = card.desc || card.th || '';
if (effEl) effEl.style.display = '';
if (txtEl) {
/* การ์ด 7 ใช้ชื่อไฟล์ card--7txt.png (ผิดแพทเทิร์น) นอกนั้น card-N-txt.png */
txtEl.removeAttribute('hidden');
txtEl.src = REWARD + (Number(cid) === 7 ? 'card--7txt.png' : ('card-' + cid + '-txt.png'));
}
if (msgEl) msgEl.textContent = '';
} else {
if (titleEl) titleEl.textContent = 'รอบนี้ไม่ได้การ์ดพิเศษ';
if (imgEl) { imgEl.setAttribute('hidden', ''); imgEl.removeAttribute('src'); }
if (nameEl) nameEl.textContent = '';
if (fxEl) fxEl.textContent = 'ตอบถูกครบทุกคนเพื่อรับการ์ดพิเศษในครั้งหน้า';
if (effEl) effEl.style.display = 'none';
if (txtEl) { txtEl.setAttribute('hidden', ''); txtEl.removeAttribute('src'); }
if (msgEl) msgEl.textContent = 'รอบนี้ไม่ได้รับการ์ดพิเศษ — ตอบถูกครบทุกคนเพื่อรับในครั้งหน้า';
}
if (btn) btn.onclick = specialQuizContinueAndResume;
/* สลับ layer: ซ่อนคำถาม/คำตอบ แล้วโชว์ผลรางวัลเต็มหน้า */
/* สลับ layer: ซ่อนคำถาม/คำตอบ แล้วโชว์ผลรางวัลเต็มจอ (ตัดกรอบควิซออก) */
const quizBody = document.getElementById('sq-ov-quiz-body');
if (quizBody) quizBody.classList.add('is-hidden');
if (ovEl) ovEl.classList.add('is-award');
award.classList.remove('is-hidden');
} else {
/* ไม่มี element สำรอง — เล่นต่อทันที */
+252 -76
View File
@@ -183,6 +183,8 @@
/** ขั้นพิจารณาคดี: null | 'voting' | 'revealed' */
let trialMode = null;
let myTrialVote = null;
let trialExcludedSuspect = null; /* รอบจับผิดตัว: ผู้ต้องสงสัยที่โหวตผิดไปแล้ว — ห้ามเลือกซ้ำ */
let trialSilencedMe = false; /* ฉันถูกปิดปาก (Silence) — ห้ามโหวตรอบนี้ */
let trialVoteCounts = [0, 0, 0];
let mapBackgroundImg = null;
let hostIconImg = null;
@@ -543,9 +545,10 @@
pending++;
preloadLobbyTintedCharacter(pid, myTintTheme, myTintSkin, doneOne);
}
lobbyBots.forEach(function (b) {
lobbyBots.forEach(function (b, id) {
if (!b || !b.characterId) return;
if (!b.colorTheme && !b.colorSkin) return;
saveLobbyBotTint(id, b); /* sync สีบอท lobby → เกม (เกมอ่าน sessionStorage นี้ ให้สีตรงกัน) */
pending++;
preloadLobbyTintedCharacter(b.characterId, b.colorTheme, b.colorSkin, doneOne);
});
@@ -4698,7 +4701,17 @@
'.suspect-card--innocent{filter:grayscale(1) brightness(.8)}' +
'#trial-lose-txt{position:absolute;left:50%;top:4%;transform:translateX(-50%);width:712px;max-width:88vw;height:auto;z-index:61;display:none}' +
'#trial-result-banner{position:absolute;left:50%;top:5%;transform:translateX(-50%);background:rgba(6,12,24,.96);border:2px solid #22d3ee;border-radius:16px;padding:14px 26px;color:#e7ecff;font:600 20px/1.5 Kanit,system-ui,sans-serif;text-align:center;z-index:60;max-width:82%;box-shadow:0 8px 40px rgba(0,0,0,.6)}' +
'#trial-reveal-btn{position:absolute;left:50%;bottom:5%;transform:translateX(-50%);background:linear-gradient(180deg,#22d3ee,#0e7490);color:#06121f;border:none;border-radius:14px;padding:14px 30px;font:700 20px/1 Kanit,system-ui,sans-serif;cursor:pointer;z-index:60;box-shadow:0 6px 22px rgba(34,211,238,.5)}';
'#trial-reveal-btn{position:absolute;left:50%;bottom:5%;transform:translateX(-50%);background:linear-gradient(180deg,#22d3ee,#0e7490);color:#06121f;border:none;border-radius:14px;padding:14px 30px;font:700 20px/1 Kanit,system-ui,sans-serif;cursor:pointer;z-index:60;box-shadow:0 6px 22px rgba(34,211,238,.5)}' +
// รอบจับผิดตัว: ใบที่โหวตผิดไปแล้ว — หรี่ + กันคลิก + ป้าย "โหวตผิดไปแล้ว"
'.suspect-card--excluded{filter:grayscale(1) brightness(.55);pointer-events:none;position:relative}' +
'.suspect-card--excluded .trial-vote-badge{display:none!important}' +
'.trial-excluded-badge{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%) rotate(-12deg);background:rgba(180,30,40,.92);color:#fff;border:3px solid #fff;border-radius:12px;padding:8px 18px;font:800 22px/1 Kanit,system-ui,sans-serif;white-space:nowrap;z-index:62;box-shadow:0 4px 18px rgba(0,0,0,.6);pointer-events:none}' +
// ถูกปิดปาก (Silence): บังหน้าโหวต โชว์แค่ตัวละครเรา + ข้อความห้ามโหวต
'#trial-silenced-layer{position:absolute;inset:0;z-index:70;display:none;flex-direction:column;align-items:center;justify-content:center;gap:18px;background:radial-gradient(ellipse at center,rgba(10,14,28,.86),rgba(6,8,18,.96))}' +
'#trial-silenced-layer .tsl-char{width:auto;height:42%;max-height:340px;object-fit:contain;filter:grayscale(.6) brightness(.9) drop-shadow(0 8px 20px rgba(0,0,0,.6))}' +
'#trial-silenced-layer .tsl-msg{color:#ffd6e0;font:700 26px/1.5 Kanit,system-ui,sans-serif;text-align:center;text-shadow:0 2px 10px rgba(0,0,0,.8)}' +
'#trial-silenced-layer .tsl-msg b{color:#ff6b81;font-size:30px}' +
'#trial-silenced-layer .tsl-icon{font-size:30px}';
document.head.appendChild(st);
}
@@ -4829,6 +4842,11 @@
function castTrialVote(idx) {
if (trialMode !== 'voting') return;
if (trialSilencedMe) { appendLobbySystemChat('— คุณถูกปิดปาก (Silence) · โหวตรอบนี้ไม่ได้'); return; }
if (trialExcludedSuspect != null && idx === trialExcludedSuspect) {
appendLobbySystemChat('— ผู้ต้องสงสัยหมายเลข ' + (idx + 1) + ' ถูกโหวตไปแล้ว (ผิด) · เลือกคนอื่น');
return;
}
myTrialVote = idx;
document.querySelectorAll('.suspect-card').forEach(function (c) {
var sel = parseInt(c.getAttribute('data-index'), 10) === idx;
@@ -4844,6 +4862,8 @@
function openTrialVoteUI(data) {
trialMode = 'voting';
myTrialVote = null;
trialExcludedSuspect = (data && data.revote && Number.isInteger(data.excludedSuspect)) ? data.excludedSuspect : null;
trialSilencedMe = !!(data && data.silenced);
trialVoteCounts = [0, 0, 0];
if (data && data.cardMinigames) setSuspectCardMinigames(data.cardMinigames);
applyPersonalSuspectState(data);
@@ -4858,8 +4878,22 @@
ov.classList.add('suspect-pick--trial');
ov.setAttribute('aria-hidden', 'false');
document.querySelectorAll('.suspect-card').forEach(function (c) {
c.classList.remove('suspect-card--myvote', 'suspect-card--culprit', 'suspect-card--selected', 'suspect-card--innocent');
c.classList.remove('suspect-card--myvote', 'suspect-card--culprit', 'suspect-card--selected', 'suspect-card--innocent', 'suspect-card--excluded');
var xb = c.querySelector('.trial-excluded-badge'); if (xb) xb.remove();
});
/* รอบจับผิดตัว: ตีตราใบที่โหวตผิดไปแล้ว + ห้ามเลือกซ้ำ */
if (trialExcludedSuspect != null) {
document.querySelectorAll('.suspect-card').forEach(function (c) {
if (parseInt(c.getAttribute('data-index'), 10) !== trialExcludedSuspect) return;
c.classList.add('suspect-card--excluded');
if (!c.querySelector('.trial-excluded-badge')) {
var xb = document.createElement('div');
xb.className = 'trial-excluded-badge';
xb.textContent = 'โหวตผิดไปแล้ว';
c.appendChild(xb);
}
});
}
var loseEl = document.getElementById('trial-lose-txt'); if (loseEl) loseEl.style.display = 'none';
applySuspectProgressVisual();
ensureTrialTitle();
@@ -4877,7 +4911,8 @@
var actions = document.getElementById('suspect-pick-actions');
if (actions) actions.classList.remove('suspect-pick-actions--visible');
var hint = document.getElementById('suspect-pick-hint');
if (hint) { hint.classList.remove('is-hidden'); hint.textContent = 'คลิกการ์ดที่คิดว่าเป็นคนร้าย'; }
if (hint) { hint.classList.remove('is-hidden'); hint.textContent = trialSilencedMe ? 'คุณถูกปิดปาก — โหวตรอบนี้ไม่ได้' : 'คลิกการ์ดที่คิดว่าเป็นคนร้าย'; }
applyTrialSilencedLayer();
/* ตัวจับเวลาโหวต + แจ้งการ์ดพิเศษ (Extension / Bail re-vote) */
startTrialVoteCountdown((data && data.voteEndsAt) || 0);
if (data && data.revote) {
@@ -4889,6 +4924,34 @@
scheduleSuspectPickScale();
}
/** ถ้าฉันถูกปิดปาก (Silence) — บังหน้าโหวตด้วยตัวละครของฉัน + ข้อความ "คุณห้ามโหวต" */
function applyTrialSilencedLayer() {
var ov = document.getElementById('suspect-pick-overlay');
if (!ov) return;
var layer = document.getElementById('trial-silenced-layer');
if (!trialSilencedMe) {
if (layer) layer.style.display = 'none';
ov.classList.remove('suspect-pick--silenced');
return;
}
ov.classList.add('suspect-pick--silenced');
if (!layer) {
layer = document.createElement('div');
layer.id = 'trial-silenced-layer';
ov.appendChild(layer);
}
layer.innerHTML = '';
layer.style.display = 'flex';
var info = resolveOccupantCharInfo(socket && socket.id);
var charImg = buildPodiumCharImg(info.characterId, info.colorTheme, info.colorSkin);
charImg.className = 'tsl-char';
var msg = document.createElement('div');
msg.className = 'tsl-msg';
msg.innerHTML = '<span class="tsl-icon">🔇</span> คุณถูกปิดปาก (Silence)<br><b>ห้ามโหวตรอบนี้</b>';
layer.appendChild(charImg);
layer.appendChild(msg);
}
function ensureTrialLoseTxt() {
var ov = document.getElementById('suspect-pick-overlay');
var el = document.getElementById('trial-lose-txt');
@@ -4904,6 +4967,9 @@
function showTrialResult(data) {
trialMode = 'revealed';
trialSilencedMe = false;
var silLayer = document.getElementById('trial-silenced-layer'); if (silLayer) silLayer.style.display = 'none';
var sov = document.getElementById('suspect-pick-overlay'); if (sov) sov.classList.remove('suspect-pick--silenced');
stopTrialVoteCountdown();
var culprit = (data && typeof data.culpritIndex === 'number') ? data.culpritIndex : 0;
if (data && Array.isArray(data.counts)) trialVoteCounts = data.counts;
@@ -6128,9 +6194,10 @@
const ov = document.getElementById('suspect-pick-overlay');
// รีเซ็ตสถานะพิจารณาคดี
if (trialMode) {
trialMode = null; myTrialVote = null; trialVoteCounts = [0, 0, 0];
if (ov) ov.classList.remove('suspect-pick--trial', 'trial-revealed');
document.querySelectorAll('.suspect-card').forEach(function (c) { c.classList.remove('suspect-card--myvote', 'suspect-card--culprit', 'suspect-card--innocent'); });
trialMode = null; myTrialVote = null; trialExcludedSuspect = null; trialSilencedMe = false; trialVoteCounts = [0, 0, 0];
var sl = document.getElementById('trial-silenced-layer'); if (sl) sl.style.display = 'none';
if (ov) ov.classList.remove('suspect-pick--trial', 'trial-revealed', 'suspect-pick--silenced');
document.querySelectorAll('.suspect-card').forEach(function (c) { c.classList.remove('suspect-card--myvote', 'suspect-card--culprit', 'suspect-card--innocent', 'suspect-card--excluded'); var xb = c.querySelector('.trial-excluded-badge'); if (xb) xb.remove(); });
document.querySelectorAll('.trial-vote-badge').forEach(function (b) { b.textContent = ''; });
var banner = document.getElementById('trial-result-banner'); if (banner) banner.style.display = 'none';
var rb = document.getElementById('trial-reveal-btn'); if (rb) rb.style.display = 'none';
@@ -6233,31 +6300,88 @@
});
/* ===== โหวตเลือกผู้เล่น (Silence / Ban) ===== */
var pickVoteState = { open: false, purpose: '', picked: null, raf: 0 };
var pickVoteState = { open: false, purpose: '', picked: null, raf: 0, confirmed: false, meta: null, closeTimer: 0 };
function pvAsset(dir, name) {
var p = dir + '/' + name;
return (typeof appPath === 'function') ? appPath(p) : p;
}
function pickVoteMeta(purpose) {
if (purpose === 'ban') {
return { badge: 'BAN CARD', title: 'โหวตห้ามเล่นมินิเกม', sub: 'เลือกผู้เล่น 1 คนที่จะถูกห้ามเล่นมินิเกมรอบนี้ (โหวตตัวเองได้ · เสมอ = สุ่ม)' };
return { purpose: 'ban', dir: '/Game/img/vote-ban', icon: 'item-6.png', label: '[ ห้ามเล่น ]', header: 'ban-txt.png',
resultTitle: 'txt-result.png', card: 'ban-card.png', cardBroken: 'ban-card-broken.png',
rtxtTarget: 'ban-txt-2.png', rtxtSafe: 'ban-txt-3.png', ctaPick: 'btn-confirm_1.png', ctaResult: 'btn-start-game.png' };
}
return { badge: 'SILENCE CARD', title: 'โหวตปิดปาก', sub: 'เลือกผู้เล่น 1 คนที่จะโหวตชี้ตัวคนร้ายไม่ได้ในรอบนี้ (โหวตตัวเองได้ · เสมอ = สุ่ม)' };
return { purpose: 'silence', dir: '/Game/img/vote-silence', icon: 'item-7.png', label: '[ ห้ามโหวต ]', header: 'txt-header.png',
resultTitle: 'txt-result.png', card: 'card-silence.png', cardBroken: 'card-silence-broken.png',
rtxtTarget: 'result-txt-1.png', rtxtSafe: 'result-txt-2.png', ctaPick: 'btn-confirm_1.png', ctaResult: 'btn-gotovote.png' };
}
function injectPickVoteStyle() {
if (document.getElementById('pvx-style')) return;
var st = document.createElement('style');
st.id = 'pvx-style';
st.textContent =
"#pick-vote-overlay{position:fixed;inset:0;z-index:12000;display:none;flex-direction:column;align-items:center;justify-content:flex-start;padding:3vh 2vw 0;background:rgba(8,11,22,.62);font-family:Kanit,system-ui,sans-serif;opacity:1;visibility:visible;pointer-events:auto}" +
"#pick-vote-overlay.is-open{display:flex}" +
"#pick-vote-overlay::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse at 50% 42%,rgba(10,16,34,.25),rgba(6,9,18,.82));z-index:-1}" +
"#pick-vote-overlay .pvx-ic{margin-top:1vh;width:clamp(104px,10vw,158px);height:auto;filter:drop-shadow(0 0 22px rgba(130,150,255,.6))}" +
"#pick-vote-overlay .pvx-label{margin-top:4px;font:800 clamp(20px,2.2vw,30px) Kanit,system-ui;color:#dfe6ff;text-shadow:0 0 14px rgba(140,120,255,.6)}" +
"#pick-vote-overlay .pvx-title{display:block;margin:12px auto 4px;max-width:min(820px,86vw);height:auto;filter:drop-shadow(0 2px 10px rgba(0,0,0,.5))}" +
"#pick-vote-overlay .pvx-result-title{display:none;margin:2vh auto 2.2vh;height:clamp(48px,5vw,72px);width:auto}" +
"#pick-vote-overlay .pvx-bar{position:relative;width:min(560px,52vw);height:clamp(26px,3vh,38px);margin:2.2vh auto 3vh}" +
"#pick-vote-overlay .pvx-bar .frame{position:absolute;inset:0;width:100%;height:100%;object-fit:fill}" +
"#pick-vote-overlay .pvx-bar .fill{position:absolute;top:16%;left:1.5%;height:68%;width:100%;border-radius:30px;background:linear-gradient(90deg,#ff2e54,#ff6b81);box-shadow:0 0 14px rgba(255,70,100,.8)}" +
"#pick-vote-overlay .pvx-players{display:flex;justify-content:center;align-items:flex-start;gap:clamp(12px,3vw,50px);flex-wrap:nowrap;margin:0 auto}" +
"#pick-vote-overlay .pvx-pl{display:flex;flex-direction:column;align-items:center;gap:8px;cursor:default}" +
"#pick-vote-overlay:not(.is-result) .pvx-pl{cursor:pointer}" +
"#pick-vote-overlay .pvx-ava{position:relative;width:clamp(96px,9vw,142px);height:clamp(96px,9vw,142px);overflow:hidden;border-radius:18px}" +
"#pick-vote-overlay .pvx-ava .pvx-bg{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;z-index:1}" +
"#pick-vote-overlay .pvx-ava .pvx-char{position:absolute;left:50%;top:14%;width:120%;height:auto;transform:translateX(-50%);z-index:2}" +
"#pick-vote-overlay .pvx-ava .pvx-frame{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;z-index:3}" +
"#pick-vote-overlay .pvx-ava .pvx-frame.sel{display:none}" +
"#pick-vote-overlay .pvx-pl.sel .pvx-ava .pvx-frame.norm{display:none}" +
"#pick-vote-overlay .pvx-pl.sel .pvx-ava .pvx-frame.sel{display:block}" +
"#pick-vote-overlay .pvx-name{font:800 clamp(14px,1.5vw,19px) Kanit,system-ui;color:#eaf0ff}" +
"#pick-vote-overlay .pvx-pl.sel .pvx-name{color:#ff6b81;text-shadow:0 0 10px rgba(255,80,110,.6)}" +
"#pick-vote-overlay .pvx-count{display:none;font:900 clamp(26px,2.8vw,38px) Kanit,system-ui;line-height:1;color:#eaf0ff;margin-top:2px}" +
"#pick-vote-overlay.is-result .pvx-count{display:block}" +
"#pick-vote-overlay.is-result .pvx-pl.sel .pvx-count{color:#ff3b5c;text-shadow:0 0 12px rgba(255,70,100,.7)}" +
"#pick-vote-overlay .pvx-banner{display:none;position:relative;width:min(900px,90vw);min-height:clamp(110px,13vh,150px);margin:1.4vh auto 2.2vh;align-items:center}" +
"#pick-vote-overlay.is-result .pvx-banner{display:flex}" +
"#pick-vote-overlay .pvx-banner .bg{position:absolute;inset:0;width:100%;height:100%;object-fit:fill}" +
"#pick-vote-overlay .pvx-banner .inner{position:relative;z-index:2;display:flex;align-items:center;justify-content:space-between;width:100%;padding:12px clamp(20px,3vw,40px);gap:18px}" +
"#pick-vote-overlay .pvx-banner .txtcol{flex:1;text-align:center}" +
"#pick-vote-overlay .pvx-banner .who{font:900 clamp(24px,2.6vw,32px) Kanit,system-ui;color:#fff;text-shadow:0 2px 8px rgba(0,0,0,.6);margin-bottom:2px}" +
"#pick-vote-overlay .pvx-banner .who:empty{display:none}" +
"#pick-vote-overlay .pvx-banner .rtxt{display:block;margin:0 auto;max-width:min(560px,52vw);height:auto}" +
"#pick-vote-overlay .pvx-banner .card{height:clamp(120px,15vh,168px);width:auto;filter:drop-shadow(0 6px 16px rgba(0,0,0,.5));flex:none}" +
"#pick-vote-overlay .pvx-cta{display:block;margin:1.6vh auto 0;cursor:pointer;width:clamp(230px,26vw,330px);height:auto;transition:transform .08s,filter .12s}" +
"#pick-vote-overlay .pvx-cta:hover{transform:translateY(-2px);filter:brightness(1.08)}" +
"#pick-vote-overlay .pvx-foot{margin-top:10px;font:600 clamp(13px,1.4vw,17px) Kanit,system-ui;color:#bcd4ff;min-height:18px}" +
"#pick-vote-overlay.is-result .pvx-ic,#pick-vote-overlay.is-result .pvx-label,#pick-vote-overlay.is-result .pvx-title,#pick-vote-overlay.is-result .pvx-bar,#pick-vote-overlay.is-result .pvx-foot{display:none}" +
"#pick-vote-overlay.is-result .pvx-result-title{display:block}";
document.head.appendChild(st);
}
function ensurePickVoteOverlay() {
var ov = document.getElementById('pick-vote-overlay');
if (ov) return ov;
injectPickVoteStyle();
ov = document.createElement('div');
ov.id = 'pick-vote-overlay';
ov.innerHTML =
'<div class="pv-shell">' +
'<span class="pv-badge" id="pv-badge">CARD</span>' +
'<h2 class="pv-title" id="pv-title"></h2>' +
'<p class="pv-sub" id="pv-sub"></p>' +
'<div class="pv-timer" id="pv-timer"></div>' +
'<div class="pv-list" id="pv-list"></div>' +
'<div class="pv-result" id="pv-result" style="display:none"></div>' +
'<div class="pv-foot" id="pv-foot"></div>' +
'</div>';
'<img class="pvx-result-title" id="pvx-result-title" alt="สรุปผลโหวต">' +
'<img class="pvx-ic" id="pvx-ic" alt="">' +
'<div class="pvx-label" id="pvx-label"></div>' +
'<img class="pvx-title" id="pvx-title" alt="">' +
'<div class="pvx-bar"><img class="frame" id="pvx-bar-frame" alt=""><div class="fill" id="pvx-bar-fill"></div></div>' +
'<div class="pvx-players" id="pvx-players"></div>' +
'<div class="pvx-banner"><img class="bg" id="pvx-banner-bg" alt=""><div class="inner"><div class="txtcol"><div class="who" id="pvx-who"></div><img class="rtxt" id="pvx-rtxt" alt=""></div><img class="card" id="pvx-card" alt=""></div></div>' +
'<img class="pvx-cta" id="pvx-cta" alt="">' +
'<div class="pvx-foot" id="pvx-foot"></div>';
document.body.appendChild(ov);
document.getElementById('pvx-cta').addEventListener('click', onPickVoteCta);
return ov;
}
@@ -6265,96 +6389,148 @@
if (pickVoteState.raf) { cancelAnimationFrame(pickVoteState.raf); pickVoteState.raf = 0; }
}
function startPickVoteCountdown(endsAt) {
function startPickVoteCountdown(endsAt, durationMs) {
stopPickVoteCountdown();
var el = document.getElementById('pv-timer');
if (!el || !endsAt) { if (el) el.textContent = ''; return; }
var fill = document.getElementById('pvx-bar-fill');
if (!fill || !endsAt) return;
var dur = durationMs || 15000;
function tick() {
if (!pickVoteState.open) { stopPickVoteCountdown(); return; }
var ms = endsAt - Date.now();
var s = Math.max(0, Math.ceil(ms / 1000));
el.textContent = '⏱ ' + s;
el.classList.toggle('low', s <= 5);
var frac = Math.max(0, Math.min(1, ms / dur));
fill.style.width = (frac * 100).toFixed(1) + '%';
if (ms > 0) pickVoteState.raf = requestAnimationFrame(tick);
}
tick();
}
function castPickVote(targetId) {
if (!pickVoteState.open) return;
/* คลิกเลือกผู้เล่น (ยังไม่ส่ง) — ไฮไลต์กรอบแดง */
function selectPickVote(targetId) {
if (!pickVoteState.open || pickVoteState.confirmed) return;
pickVoteState.picked = targetId;
var list = document.getElementById('pv-list');
if (list) {
Array.prototype.forEach.call(list.querySelectorAll('.pv-cand'), function (b) {
b.classList.toggle('is-picked', b.getAttribute('data-id') === targetId);
});
}
socket.emit('player-pick-vote', { targetId: targetId }, function (res) {
if (!(res && res.ok)) appendLobbySystemChat('— โหวตไม่สำเร็จ' + (res && res.error ? (' · ' + res.error) : ''));
var pl = document.getElementById('pvx-players');
if (pl) Array.prototype.forEach.call(pl.querySelectorAll('.pvx-pl'), function (c) {
c.classList.toggle('sel', c.getAttribute('data-id') === targetId);
});
}
function confirmPickVote() {
if (!pickVoteState.open || pickVoteState.confirmed) return;
var foot = document.getElementById('pvx-foot');
if (!pickVoteState.picked) { if (foot) foot.textContent = 'เลือกผู้เล่นก่อน'; return; }
pickVoteState.confirmed = true;
var cta = document.getElementById('pvx-cta');
if (cta) cta.style.opacity = '0.5';
if (foot) foot.textContent = 'ส่งโหวตแล้ว — รอผู้เล่นอื่น...';
socket.emit('player-pick-vote', { targetId: pickVoteState.picked }, function (res) {
if (!(res && res.ok)) {
pickVoteState.confirmed = false;
if (cta) cta.style.opacity = '';
appendLobbySystemChat('— โหวตไม่สำเร็จ' + (res && res.error ? (' · ' + res.error) : ''));
}
});
}
function onPickVoteCta() {
var ov = document.getElementById('pick-vote-overlay');
if (!ov) return;
if (ov.classList.contains('is-result')) { closePickVoteNow(); return; }
confirmPickVote();
}
function closePickVoteNow() {
pickVoteState.open = false;
stopPickVoteCountdown();
clearTimeout(pickVoteState.closeTimer);
var ov = document.getElementById('pick-vote-overlay');
if (ov) ov.classList.remove('is-open');
}
socket.on('player-pick-vote-open', (data) => {
var ov = ensurePickVoteOverlay();
var meta = pickVoteMeta(data && data.purpose);
pickVoteState.open = true;
pickVoteState.purpose = (data && data.purpose) || '';
pickVoteState.purpose = meta.purpose;
pickVoteState.picked = null;
document.getElementById('pv-badge').textContent = meta.badge;
document.getElementById('pv-title').textContent = meta.title;
document.getElementById('pv-sub').textContent = meta.sub;
var resEl = document.getElementById('pv-result');
if (resEl) { resEl.style.display = 'none'; resEl.textContent = ''; }
document.getElementById('pv-foot').textContent = '';
var list = document.getElementById('pv-list');
list.innerHTML = '';
pickVoteState.confirmed = false;
pickVoteState.meta = meta;
clearTimeout(pickVoteState.closeTimer);
ov.classList.remove('is-result');
document.getElementById('pvx-ic').src = pvAsset(meta.dir, meta.icon);
document.getElementById('pvx-label').textContent = meta.label;
document.getElementById('pvx-title').src = pvAsset(meta.dir, meta.header);
document.getElementById('pvx-result-title').src = pvAsset(meta.dir, meta.resultTitle);
document.getElementById('pvx-bar-frame').src = pvAsset(meta.dir, 'time-frame.png');
document.getElementById('pvx-bar-fill').style.width = '100%';
document.getElementById('pvx-banner-bg').src = pvAsset(meta.dir, 'frame-red.png');
var cta = document.getElementById('pvx-cta');
cta.src = pvAsset(meta.dir, meta.ctaPick); cta.style.opacity = '';
document.getElementById('pvx-foot').textContent = '';
document.getElementById('pvx-who').textContent = '';
var pl = document.getElementById('pvx-players');
pl.innerHTML = '';
var cands = (data && Array.isArray(data.candidates)) ? data.candidates : [];
cands.forEach(function (c) {
var b = document.createElement('button');
b.type = 'button';
b.className = 'pv-cand';
b.setAttribute('data-id', c.id);
var tag = (c.id === socket.id) ? '<span class="pv-me">(คุณ)</span>' : (c.isBot ? '<span class="pv-bot">บอท</span>' : '');
b.innerHTML = '<span>' + (c.nickname || '?') + '</span>' + tag + '<span class="pv-count" data-id="' + c.id + '"></span>';
b.addEventListener('click', function () { castPickVote(c.id); });
list.appendChild(b);
var info = (typeof resolveOccupantCharInfo === 'function') ? resolveOccupantCharInfo(c.id) : { name: c.nickname };
var cell = document.createElement('div');
cell.className = 'pvx-pl';
cell.setAttribute('data-id', c.id);
var ava = document.createElement('div'); ava.className = 'pvx-ava';
var bg = document.createElement('img'); bg.className = 'pvx-bg'; bg.src = pvAsset(meta.dir, 'player-icon-bg.png');
var ch = (typeof buildPodiumCharImg === 'function') ? buildPodiumCharImg(info.characterId, info.colorTheme, info.colorSkin) : document.createElement('img');
ch.className = 'pvx-char'; ch.style.filter = '';
var frN = document.createElement('img'); frN.className = 'pvx-frame norm'; frN.src = pvAsset(meta.dir, 'player-icon-frame.png');
var frS = document.createElement('img'); frS.className = 'pvx-frame sel'; frS.src = pvAsset(meta.dir, 'player-icon-frame-select.png');
ava.appendChild(bg); ava.appendChild(ch); ava.appendChild(frN); ava.appendChild(frS);
var nm = document.createElement('div'); nm.className = 'pvx-name'; nm.textContent = (info.name || c.nickname || '?');
var cn = document.createElement('div'); cn.className = 'pvx-count';
cell.appendChild(ava); cell.appendChild(nm); cell.appendChild(cn);
cell.addEventListener('click', function () { selectPickVote(c.id); });
pl.appendChild(cell);
});
ov.classList.add('is-open');
startPickVoteCountdown(data && data.endsAt);
startPickVoteCountdown(data && data.endsAt, data && data.durationMs);
});
socket.on('player-pick-vote-update', (data) => {
if (!pickVoteState.open) return;
var counts = (data && data.counts) || {};
var list = document.getElementById('pv-list');
if (list) {
Array.prototype.forEach.call(list.querySelectorAll('.pv-count'), function (c) {
var n = counts[c.getAttribute('data-id')] || 0;
c.textContent = n;
c.classList.toggle('show', n > 0);
});
}
var foot = document.getElementById('pv-foot');
if (foot) foot.textContent = 'โหวตแล้ว ' + ((data && data.voted) || 0) + '/' + ((data && data.total) || 0);
var foot = document.getElementById('pvx-foot');
if (foot && !pickVoteState.confirmed) foot.textContent = 'โหวตแล้ว ' + ((data && data.voted) || 0) + '/' + ((data && data.total) || 0);
});
socket.on('player-pick-vote-result', (data) => {
stopPickVoteCountdown();
pickVoteState.open = false;
var resEl = document.getElementById('pv-result');
var name = (data && data.targetName) ? data.targetName : '—';
var word = (data && data.purpose === 'ban') ? 'ถูกห้ามเล่นมินิเกมรอบนี้' : 'ถูกปิดปาก (โหวตไม่ได้)';
if (resEl) {
resEl.style.display = 'block';
resEl.textContent = '🎯 ' + name + ' ' + word;
var ov = document.getElementById('pick-vote-overlay');
if (!ov) return;
var meta = pickVoteState.meta || pickVoteMeta(data && data.purpose);
var counts = (data && data.counts) || {};
var targetId = (data && data.targetId) || null;
Array.prototype.forEach.call(ov.querySelectorAll('.pvx-pl'), function (c) {
var id = c.getAttribute('data-id');
var cn = c.querySelector('.pvx-count');
if (cn) cn.textContent = 'X' + (counts[id] || 0);
c.classList.toggle('sel', !!targetId && id === targetId);
});
var who = document.getElementById('pvx-who');
var rtxt = document.getElementById('pvx-rtxt');
var card = document.getElementById('pvx-card');
if (targetId) {
if (who) who.textContent = (data && data.targetName) || '';
if (rtxt) rtxt.src = pvAsset(meta.dir, meta.rtxtTarget);
if (card) card.src = pvAsset(meta.dir, meta.card);
} else {
if (who) who.textContent = '';
if (rtxt) rtxt.src = pvAsset(meta.dir, meta.rtxtSafe);
if (card) card.src = pvAsset(meta.dir, meta.cardBroken);
}
var foot = document.getElementById('pv-foot');
var cta = document.getElementById('pvx-cta');
if (cta) { cta.src = pvAsset(meta.dir, meta.ctaResult); cta.style.opacity = ''; }
var foot = document.getElementById('pvx-foot');
if (foot) foot.textContent = '';
var amTarget = data && data.targetId === socket.id;
setTimeout(function () {
var ov = document.getElementById('pick-vote-overlay');
if (ov) ov.classList.remove('is-open');
}, amTarget ? 2600 : 1900);
ov.classList.add('is-result');
clearTimeout(pickVoteState.closeTimer);
pickVoteState.closeTimer = setTimeout(closePickVoteNow, 14000);
});
socket.on('suspect-investigation-start', (data) => {
+30 -22
View File
@@ -5081,33 +5081,34 @@
#special-quiz-overlay.is-hidden { display: none; }
#special-quiz-overlay .sq-ov-backdrop { position: absolute; inset: 0; background: radial-gradient(ellipse at center, rgba(16,22,46,0.82), rgba(8,10,22,0.94)); backdrop-filter: blur(3px); }
/* ===== Special Quiz — ดีไซน์ตาม mock 09-Quiz (กรอบ neon + ตัวเลือกเต็มแถว) ===== */
#special-quiz-overlay .sq-ov-panel { position: relative; width: min(760px, 94vw); min-height: min(560px, 90vh); max-height: 96vh; overflow: auto; box-sizing: border-box; background: url('/Game/img/special-quiz/mock/special-quiz.png') center/100% 100% no-repeat; border: none; border-radius: 0; box-shadow: none; padding: clamp(78px, 14%, 140px) 10% clamp(70px, 11%, 120px); color: #e7ecff; }
#special-quiz-overlay .sq-ov-panel { position: relative; width: min(760px, 92vw, calc(94vh * 1305 / 1240)); aspect-ratio: 1305 / 1240; max-height: 94vh; overflow: auto; scrollbar-width: none; box-sizing: border-box; background: url('/Game/img/special-quiz/mock/special-quiz-tall.png') center/100% 100% no-repeat; border: none; border-radius: 0; box-shadow: none; padding: clamp(86px, 14%, 150px) 9% clamp(54px, 8%, 96px); color: #e7ecff; }
#special-quiz-overlay .sq-ov-panel::-webkit-scrollbar { width: 0; height: 0; }
/* หัวเก่า (ไอคอน/kicker/ข้อ) ซ่อน — ชื่อ "Special Quiz" อยู่ในรูปกรอบแล้ว */
#special-quiz-overlay .sq-ov-head { display: block; }
#special-quiz-overlay .sq-ov-icon,
#special-quiz-overlay .sq-ov-head-text { display: none; }
/* นาฬิกานับถอยหลัง — วงกลมกลางล่างของกรอบ */
#special-quiz-overlay .sq-ov-timer { position: absolute; left: 50%; bottom: 3.5%; transform: translateX(-50%); width: clamp(44px, 8%, 70px); aspect-ratio: 1; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: clamp(18px, 2.4vw, 26px); font-weight: 800; color: #eafcff; border: 3px solid rgba(94,234,255,0.85); background: rgba(8,16,30,0.85); box-shadow: 0 0 14px rgba(94,234,255,0.4); z-index: 3; }
#special-quiz-overlay .sq-ov-timer { position: absolute; left: 50%; bottom: 3.4%; transform: translateX(-50%); width: clamp(44px, 8%, 68px); aspect-ratio: 1; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: clamp(18px, 2.4vw, 26px); font-weight: 800; color: #eafcff; border: 3px solid rgba(94,234,255,0.85); background: rgba(8,16,30,0.85); box-shadow: 0 0 14px rgba(94,234,255,0.4); z-index: 3; }
#special-quiz-overlay .sq-ov-timer.sq-ov-timer--low { color: #ffd6e0; border-color: rgba(247,118,142,0.9); box-shadow: 0 0 14px rgba(247,118,142,0.5); }
#special-quiz-overlay .sq-ov-question { margin: 0 2px 16px; font-size: clamp(17px, 2.2vw, 24px); line-height: 1.4; font-weight: 800; text-align: center; color: #ffffff; text-shadow: 0 1px 6px rgba(0,0,0,0.5); }
#special-quiz-overlay .sq-ov-choices { display: flex; flex-direction: column; gap: clamp(14px, 2.6vh, 26px); }
#special-quiz-overlay .sq-ov-choice { position: relative; display: flex; align-items: center; justify-content: center; gap: 10px; text-align: center; padding: clamp(12px, 2vh, 20px) 44px; border: none; border-radius: 0; background: url('/Game/img/special-quiz/mock/choice-default.png') center/100% 100% no-repeat; color: #14263f; font-size: clamp(15px, 1.9vw, 21px); font-weight: 800; cursor: pointer; transition: transform .08s, filter .12s; }
#special-quiz-overlay .sq-ov-question { margin: 0 2px 22px; font-size: clamp(17px, 2.2vw, 25px); line-height: 1.4; font-weight: 800; text-align: center; color: #ffffff; text-shadow: 0 1px 6px rgba(0,0,0,0.5); }
#special-quiz-overlay .sq-ov-choices { display: flex; flex-direction: column; gap: clamp(16px, 2.8vh, 30px); }
#special-quiz-overlay .sq-ov-choice { position: relative; display: flex; align-items: center; justify-content: center; gap: 10px; text-align: center; padding: clamp(13px, 2.1vh, 21px) 46px; border: none; border-radius: 0; background: url('/Game/img/special-quiz/mock/choice-default.png') center/100% 100% no-repeat; color: #ffffff; text-shadow: 0 1px 4px rgba(0,0,0,0.6); font-size: clamp(15px, 1.9vw, 21px); font-weight: 800; cursor: pointer; transition: transform .08s, filter .12s; }
#special-quiz-overlay .sq-ov-choice:hover:not(:disabled) { filter: brightness(1.12); transform: translateY(-1px); }
#special-quiz-overlay .sq-ov-choice:disabled { cursor: default; }
#special-quiz-overlay .sq-ov-choice-key { flex: none; font-weight: 800; color: inherit; }
#special-quiz-overlay .sq-ov-choice-text { flex: 0 1 auto; }
#special-quiz-overlay .sq-ov-choice--picked { background-image: url('/Game/img/special-quiz/mock/choice-select.png'); }
#special-quiz-overlay .sq-ov-choice--correct { background-image: url('/Game/img/special-quiz/mock/choice-correct.png'); color: #0d3a18; }
#special-quiz-overlay .sq-ov-choice--correct { background-image: url('/Game/img/special-quiz/mock/choice-correct.png'); color: #ffffff; }
#special-quiz-overlay .sq-ov-choice--correct::after { content: ''; position: absolute; right: 14px; top: 50%; transform: translateY(-50%); width: clamp(26px, 4vw, 40px); height: clamp(26px, 4vw, 40px); background: url('/Game/img/special-quiz/mock/correct.png') center/contain no-repeat; }
#special-quiz-overlay .sq-ov-choice--correct .sq-ov-choice-key { color: #0e2a12; background: transparent; }
#special-quiz-overlay .sq-ov-choice--wrong { background-image: url('/Game/img/special-quiz/mock/choice-wrong.png'); color: #7a1222; }
#special-quiz-overlay .sq-ov-choice--wrong { background-image: url('/Game/img/special-quiz/mock/choice-wrong.png'); color: #ffffff; }
#special-quiz-overlay .sq-ov-choice--wrong::after { content: ''; position: absolute; right: 14px; top: 50%; transform: translateY(-50%); width: clamp(26px, 4vw, 40px); height: clamp(26px, 4vw, 40px); background: url('/Game/img/special-quiz/mock/wrong.png') center/contain no-repeat; }
/* mock 09-Quiz: ไม่มีชิป "สมาชิกในรอบนี้" / สถานะ — โชว์ avatar ใต้คำตอบที่เลือกแทน */
#special-quiz-overlay .sq-ov-status,
#special-quiz-overlay .sq-ov-members-head,
#special-quiz-overlay .sq-ov-members { display: none !important; }
/* แถว avatar ผู้เล่นที่เลือกคำตอบนี้ — เกาะขอบล่างของแถบ */
#special-quiz-overlay .sq-ov-choice-avatars { position: absolute; left: 50%; bottom: 0; transform: translate(-50%, 52%); display: flex; align-items: center; justify-content: center; gap: 3px; pointer-events: none; z-index: 4; min-height: 1px; }
#special-quiz-overlay .sq-ov-choice-avatars { position: absolute; left: 50%; bottom: 0; transform: translate(-50%, 29%); display: flex; align-items: center; justify-content: center; gap: 3px; pointer-events: none; z-index: 4; min-height: 1px; }
#special-quiz-overlay .sq-ov-choice-avatars:empty { display: none; }
#special-quiz-overlay .sq-ov-av { width: clamp(26px, 3.4vw, 38px); height: clamp(26px, 3.4vw, 38px); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: clamp(12px, 1.5vw, 16px); font-weight: 800; color: #fff; border: 2px solid #eafcff; box-shadow: 0 2px 6px rgba(0,0,0,0.5); background: #5eeaff; }
#special-quiz-overlay .sq-ov-av--me { border-color: #ffe95a; box-shadow: 0 0 8px rgba(255,233,90,0.7); }
@@ -5122,16 +5123,21 @@
#special-quiz-overlay .sq-ov-result--ok { color: #0e1226; background: #9ece6a; }
#special-quiz-overlay .sq-ov-result--fail { color: #fff; background: rgba(247,118,142,0.9); }
#special-quiz-overlay #sq-ov-quiz-body.is-hidden { display: none; }
#special-quiz-overlay .sq-ov-award { display: flex; flex-direction: column; align-items: center; gap: 10px; text-align: center; }
/* ===== หน้ารับการ์ด (mock 10-Game-1-result) — เต็มจอ ไม่มีกรอบควิซ ===== */
#special-quiz-overlay.is-award .sq-ov-panel { background: none !important; padding: 2vh 4vw !important; min-height: 0 !important; box-shadow: none; }
#special-quiz-overlay .sq-ov-award { display: flex; flex-direction: column; align-items: center; gap: 8px; text-align: center; }
#special-quiz-overlay .sq-ov-award.is-hidden { display: none; }
#special-quiz-overlay .sq-ov-award-title { font-size: 20px; font-weight: 900; color: #ffe7a3; text-shadow: 0 0 14px rgba(255,214,102,0.55); }
#special-quiz-overlay .sq-ov-award-img { width: 170px; max-width: 60vw; height: auto; border-radius: 14px; box-shadow: 0 0 34px rgba(255,200,90,0.55), 0 14px 40px rgba(0,0,0,0.55); animation: sqAwardPop .55s cubic-bezier(.2,.9,.3,1.45); }
#special-quiz-overlay .sq-ov-award-stage { position: relative; display: flex; align-items: center; justify-content: center; }
#special-quiz-overlay .sq-ov-award-effect { position: absolute; left: 50%; top: 46%; transform: translate(-50%,-50%); width: clamp(320px, 46vw, 560px); opacity: .92; pointer-events: none; z-index: 1; }
#special-quiz-overlay .sq-ov-award-img { position: relative; z-index: 3; height: clamp(200px, 36vh, 320px); width: auto; filter: drop-shadow(0 10px 30px rgba(0,0,0,0.6)); animation: sqAwardPop .55s cubic-bezier(.2,.9,.3,1.45); }
#special-quiz-overlay .sq-ov-award-img[hidden] { display: none; }
@keyframes sqAwardPop { from { opacity: 0; transform: translateY(22px) scale(.82) rotate(-5deg); } to { opacity: 1; transform: none; } }
#special-quiz-overlay .sq-ov-award-name { font-size: 18px; font-weight: 800; color: #fff; }
#special-quiz-overlay .sq-ov-award-effect { font-size: 14px; color: #c0caf5; max-width: 90%; line-height: 1.4; }
#special-quiz-overlay .sq-ov-award-btn { margin-top: 10px; cursor: pointer; border: 2px solid rgba(94,234,255,0.9); border-radius: 14px; padding: 13px 44px; font-size: 18px; font-weight: 800; color: #eafcff; background: rgba(20,38,63,0.92); box-shadow: 0 0 18px rgba(94,234,255,0.45), inset 0 0 12px rgba(94,234,255,0.18); transition: filter .12s, transform .08s; }
#special-quiz-overlay .sq-ov-award-btn:hover { filter: brightness(1.12); transform: translateY(-1px); }
@keyframes sqAwardPop { from { opacity: 0; transform: scale(.8) translateY(20px); } to { opacity: 1; transform: none; } }
#special-quiz-overlay .sq-ov-award-txt { display: block; max-width: min(720px, 86vw); height: auto; margin: 4px auto 14px; }
#special-quiz-overlay .sq-ov-award-txt[hidden] { display: none; }
#special-quiz-overlay .sq-ov-award-msg { font: 700 clamp(16px,2vw,22px)/1.4 Kanit, system-ui, sans-serif; color: #cfd6ea; text-shadow: 0 1px 6px rgba(0,0,0,0.6); margin: 6px 0 14px; }
#special-quiz-overlay .sq-ov-award-msg:empty { display: none; }
#special-quiz-overlay .sq-ov-award-btn { margin-top: 4px; cursor: pointer; border: none; background: url('/Game/img/special-quiz/reward/btn-continue-play.png') center/100% 100% no-repeat; width: clamp(220px, 26vw, 320px); aspect-ratio: 354 / 120; font-size: 0; color: transparent; transition: filter .12s, transform .08s; }
#special-quiz-overlay .sq-ov-award-btn:hover { filter: brightness(1.1); transform: translateY(-2px); }
#special-quiz-overlay .sq-ov-members-head { margin-top: 16px; font-size: 12px; letter-spacing: .04em; text-transform: uppercase; color: #7aa2f7; font-weight: 700; }
#special-quiz-overlay .sq-ov-members { margin-top: 8px; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
@media (max-width: 560px) { #special-quiz-overlay .sq-ov-members { grid-template-columns: 1fr; } }
@@ -5186,18 +5192,20 @@
<button id="sq-ov-next" type="button" class="sq-ov-next-btn is-hidden" aria-label="ถัดไป"></button>
</div>
<div id="sq-ov-award" class="sq-ov-award is-hidden">
<div id="sq-ov-award-title" class="sq-ov-award-title">ได้รับการ์ดพิเศษ!</div>
<img id="sq-ov-award-img" class="sq-ov-award-img" alt="" />
<div id="sq-ov-award-name" class="sq-ov-award-name"></div>
<div id="sq-ov-award-effect" class="sq-ov-award-effect"></div>
<button type="button" id="sq-ov-award-continue" class="sq-ov-award-btn">เล่นต่อ ▶</button>
<div class="sq-ov-award-stage">
<img id="sq-ov-award-effect-img" class="sq-ov-award-effect" src="/Game/img/special-quiz/reward/card-effect.png" alt="" />
<img id="sq-ov-award-img" class="sq-ov-award-img" alt="" />
</div>
<img id="sq-ov-award-txt" class="sq-ov-award-txt" alt="" />
<div id="sq-ov-award-msg" class="sq-ov-award-msg"></div>
<button type="button" id="sq-ov-award-continue" class="sq-ov-award-btn" aria-label="เล่นเกมต่อ">เล่นเกมต่อ</button>
</div>
</div>
</div>
<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.006131831"></script>
<script src="js/play.js?v=0.006131839"></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)
+1 -1
View File
@@ -1617,7 +1617,7 @@
<script src="js/version.js?v=0.0122"></script>
<script src="js/customize-popup.js?v=31" data-customize-triggers="" data-customize-asset-base="img/03-5-Customize"></script>
<script src="js/achievements.js?v=0.002" data-asset-base="img/03-6-Profile"></script>
<script src="js/room-lobby.js?v=0.0286"></script>
<script src="js/room-lobby.js?v=0.0291"></script>
<div class="version-tag">v —</div>
</body>
</html>
+97 -28
View File
@@ -1100,10 +1100,29 @@ function applySpecialCardImmediate(sid, space, card) {
คงไวใน pendingSpecialCard เพอใชภายหล */
}
/* ===== คลังการ์ดพิเศษที่สะสมในเคสนี้ (ใช้ได้พร้อมกันทุกใบ ไม่ทับกัน) ===== */
function pendingSpecialCardList(space) {
if (!Array.isArray(space.pendingSpecialCards)) space.pendingSpecialCards = [];
return space.pendingSpecialCards;
}
/** เพิ่มการ์ดเข้าคลัง — กันได้การ์ดซ้ำในเกมเดียว (ถ้ามีใบเดิมยังไม่ได้ใช้ จะไม่เพิ่มซ้ำ) */
function addPendingSpecialCard(space, card) {
if (!card || !card.cardId) return null;
const list = pendingSpecialCardList(space);
const cid = Number(card.cardId);
if (list.some((e) => Number(e.cardId) === cid && !e.consumed)) return null; /* มีใบนี้อยู่แล้ว ไม่ซ้ำ */
const entry = { cardId: cid, card: card, consumed: false };
list.push(entry);
return entry;
}
/** หาการ์ดในคลังตาม cardId ที่ยังไม่ถูกใช้ */
function findPendingSpecialCard(space, cardId) {
const cid = Number(cardId);
return pendingSpecialCardList(space).find((e) => Number(e.cardId) === cid && !e.consumed) || null;
}
function consumePendingSpecialCard(space, cardId) {
if (space.pendingSpecialCard && space.pendingSpecialCard.cardId === Number(cardId)) {
space.pendingSpecialCard.consumed = true;
}
const e = findPendingSpecialCard(space, cardId);
if (e) e.consumed = true;
}
function endSpecialQuizSession(sid, space, success) {
@@ -1116,7 +1135,7 @@ function endSpecialQuizSession(sid, space, success) {
if (success && sq.card && (sq.card.cardId || sq.card.imageUrl)) {
space.specialCardAwardedThisRun = sq.card;
cardOut = specialCardClientPayload(sq.card);
space.pendingSpecialCard = { cardId: Number(sq.card.cardId) || null, card: sq.card, consumed: false };
addPendingSpecialCard(space, sq.card); /* สะสมเข้าคลัง — ใช้ได้พร้อมกันทุกใบ ไม่ทับใบเดิม */
applySpecialCardImmediate(sid, space, sq.card);
}
io.to(sid).emit('special-quiz-ended', { success: !!success, card: cardOut });
@@ -2037,6 +2056,10 @@ function defaultGameTiming() {
troublesomeForceOffer: false,
/** เวลาที่ไอคอนคำถามพิเศษอยู่บนฉากก่อนหายไป (วินาที) — 0 = ไม่หาย */
specialQuizIconExpireSec: 10,
/** เวลาโหวตเลือกผู้เล่น (การ์ด Silence/Ban) วินาที */
pickVoteSec: 25,
/** เวลาโหวตชี้ตัวคนร้าย (พิจารณาคดี) วินาที */
trialVoteSec: 30,
};
}
@@ -2436,6 +2459,12 @@ function loadGameTiming() {
specialQuizIconExpireSec: Object.prototype.hasOwnProperty.call(j, 'specialQuizIconExpireSec')
? Math.max(0, Math.min(120, Math.floor(Number(j.specialQuizIconExpireSec)) || 0))
: 10,
pickVoteSec: Object.prototype.hasOwnProperty.call(j, 'pickVoteSec')
? Math.max(5, Math.min(120, Math.floor(Number(j.pickVoteSec)) || 25))
: 25,
trialVoteSec: Object.prototype.hasOwnProperty.call(j, 'trialVoteSec')
? Math.max(5, Math.min(180, Math.floor(Number(j.trialVoteSec)) || 30))
: 30,
};
}
} catch (e) { console.error('loadGameTiming', e.message); }
@@ -2655,6 +2684,12 @@ function saveGameTimingToFile(d) {
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),
pickVoteSec: Object.prototype.hasOwnProperty.call(d, 'pickVoteSec')
? Math.max(5, Math.min(120, Math.floor(Number(d.pickVoteSec)) || 25))
: (prev.pickVoteSec != null ? Math.max(5, Math.min(120, Math.floor(Number(prev.pickVoteSec)) || 25)) : 25),
trialVoteSec: Object.prototype.hasOwnProperty.call(d, 'trialVoteSec')
? Math.max(5, Math.min(180, Math.floor(Number(d.trialVoteSec)) || 30))
: (prev.trialVoteSec != null ? Math.max(5, Math.min(180, Math.floor(Number(prev.trialVoteSec)) || 30)) : 30),
};
fs.writeFileSync(GAME_TIMING_PATH, JSON.stringify(out, null, 2), 'utf8');
runtimeGameTiming = out;
@@ -3339,8 +3374,8 @@ function grantDetectiveEvidenceForCurrentRun(sid, space, playerIds) {
ids.forEach((pid) => { if (awarded[pid]) awardedList[pid].push(awarded[pid]); });
/* Card 7 Free Evidence — ทุกคนรับหลักฐานเพิ่ม (after_game) */
let freeEvidenceCount = 0;
const pend = space.pendingSpecialCard;
if (pend && Number(pend.cardId) === 7 && !pend.consumed) {
const pend = findPendingSpecialCard(space, 7);
if (pend) {
const def = specialCardDef(7);
freeEvidenceCount = (def && def.evidence) || 1;
for (let n = 0; n < freeEvidenceCount; n++) {
@@ -3428,6 +3463,7 @@ function beginDetectiveSuspectMinigame(sid, space, cardEntry, selectedIndex) {
p.gauntletScore = 0;
p.gauntletJumpPending = false;
p.gauntletEliminated = false;
p.detectiveFinished = false; /* MG2: รอให้ทุกคน(ที่ยังไม่ตาย)เข้าเส้นชัยก่อนจบเกมพร้อมกัน */
});
const peersSnap = buildPeersSnapForSpace(space, { gauntlet: md.gameType === 'gauntlet' });
@@ -3573,13 +3609,11 @@ function returnDetectiveSpaceToLobbyB(sid, space, message, awardOptions) {
emitDetectiveLobbyReturnToPeers(sid, space, message, basePayload);
/* Card 3 Ban (when: lobby_next): " LobbyB "
bannedPlayerId ใหเกมรอบถดไปกนไมใหคนทกโหวตเล */
const banPend = space.pendingSpecialCard;
console.log('[ban-debug] return->LobbyB: pendingSpecialCard=', banPend ? ('card' + banPend.cardId + ' consumed=' + banPend.consumed) : 'none', 'pickVote=', !!space.pickVote);
if (banPend && Number(banPend.cardId) === 3 && !banPend.consumed && !space.pickVote) {
const banPend = findPendingSpecialCard(space, 3);
if (banPend && !space.pickVote) {
banPend.consumed = true;
/* ตั้ง flag — เริ่มโหวตตอนผู้เล่น "rejoin เข้า room-lobby" (ตอน return ผู้เล่นกำลัง navigate play.html→room-lobby.html อยู่ event จะหาย) */
space.pendingBanVoteCard = banPend.card;
console.log('[ban-debug] set pendingBanVoteCard (จะเปิดโหวตตอน rejoin)');
}
}
@@ -3612,12 +3646,13 @@ function computeAndEmitTrialResult(sid, space) {
let mostVoted = 0; for (let k = 1; k <= 2; k++) { if (c2[k] > c2[mostVoted]) mostVoted = k; }
const anyVotes = (c2[0] + c2[1] + c2[2]) > 0;
const wrong = forceWrong ? anyVotes : (anyVotes && mostVoted !== culprit);
const pend = space.pendingSpecialCard;
if (wrong && pend && Number(pend.cardId) === 5 && !pend.consumed && !space.trialBailUsed) {
const pend = findPendingSpecialCard(space, 5);
if (wrong && pend && !space.trialBailUsed) {
pend.consumed = true;
space.trialBailUsed = true;
space.trialRevoteExcluded = mostVoted; /* ผู้ต้องสงสัยที่โหวตไปแล้ว (ผิด) — ห้ามโหวตซ้ำในรอบใหม่ */
io.to(sid).emit('special-card-applied', { card: specialCardClientPayload(pend.card), context: 'bail' });
openTrialVotingPhase(sid, space, { revote: true });
openTrialVotingPhase(sid, space, { revote: true, excludedSuspect: mostVoted });
return;
}
}
@@ -3802,7 +3837,8 @@ function doTestimonyReveal(sid, space) {
/** สุ่มโหวตชี้คนร้ายให้บอท — สมจริงๆ ก็คือเลือกใครก็ได้ใน 3 คน (มี bias ให้ตัวร้ายจริง 40% เพื่อให้สนุก) */
/** เวลาโหวตพิจารณาคดี (มิลลิวิ) + เวลาที่การ์ด Extension เพิ่มให้ */
const TRIAL_VOTE_MS = 30000;
/* เวลาโหวตชี้ตัวคนร้าย (ms) — ตั้งได้ในหน้า admin (game-timing.trialVoteSec) */
function trialVoteMs() { return Math.max(5, Math.min(180, Math.floor(Number(runtimeGameTiming.trialVoteSec)) || 30)) * 1000; }
const TRIAL_VOTE_EXTENSION_MS = 10000;
function clearTrialVoteTimer(space) {
@@ -3815,18 +3851,24 @@ function openTrialVotingPhase(sid, space, opts) {
space.testimonyActive = false;
space.trialPhase = 'voting';
space.trialVotes = {};
/* ผู้ต้องสงสัยที่ถูกตัดออก (โหวตผิดไปแล้วในรอบจับผิดตัว) — null ถ้าเป็นการโหวตปกติ */
const excludedSuspect = opts.revote
? (Number.isInteger(opts.excludedSuspect) ? opts.excludedSuspect
: (Number.isInteger(space.trialRevoteExcluded) ? space.trialRevoteExcluded : null))
: null;
if (!opts.revote) space.trialRevoteExcluded = null;
autoVoteBotsTrial(space);
clearTrialVoteTimer(space);
/* Card 2 Extension — เพิ่มเวลาโหวต +10 วิ (ใช้ครั้งเดียว) */
let extraMs = 0;
let extensionCard = false;
const pend = space.pendingSpecialCard;
if (pend && Number(pend.cardId) === 2 && !pend.consumed) {
const pend = findPendingSpecialCard(space, 2);
if (pend) {
extraMs = TRIAL_VOTE_EXTENSION_MS;
extensionCard = true;
pend.consumed = true;
}
const durMs = TRIAL_VOTE_MS + extraMs;
const durMs = trialVoteMs() + extraMs;
space.trialVoteEndsAt = Date.now() + durMs;
space.peers.forEach((_p, pid) => {
const sock = io.sockets.sockets.get(pid);
@@ -3841,6 +3883,8 @@ function openTrialVotingPhase(sid, space, opts) {
extensionCard,
extensionSec: extensionCard ? (TRIAL_VOTE_EXTENSION_MS / 1000) : 0,
revote: !!opts.revote,
excludedSuspect: excludedSuspect,
silenced: !!(space.silencedPlayerId && pid === space.silencedPlayerId),
});
});
if (extensionCard) {
@@ -3859,7 +3903,8 @@ function openTrialVotingPhase(sid, space, opts) {
}
/* ===== โหวตเลือกผู้เล่น 1 คน (ใช้ร่วม: Silence ปิดปาก / Ban ห้ามเล่น) ===== */
const PICK_VOTE_MS = 15000;
/* เวลาโหวตเลือกผู้เล่น Silence/Ban (ms) — ตั้งได้ในหน้า admin (game-timing.pickVoteSec) */
function pickVoteMs() { return Math.max(5, Math.min(120, Math.floor(Number(runtimeGameTiming.pickVoteSec)) || 25)) * 1000; }
function clearPickVoteTimer(space) {
if (space.pickVoteTimer) { clearTimeout(space.pickVoteTimer); space.pickVoteTimer = null; }
@@ -3897,8 +3942,9 @@ function emitPickVoteUpdate(sid, space) {
function startPlayerPickVote(sid, space, purpose, onResolve) {
clearPickVoteTimer(space);
const PV_MS = pickVoteMs();
const candidates = pickVoteCandidates(space);
space.pickVote = { purpose, votes: {}, candidates, endsAt: Date.now() + PICK_VOTE_MS, resolved: false, onResolve };
space.pickVote = { purpose, votes: {}, candidates, endsAt: Date.now() + PV_MS, resolved: false, onResolve };
autoVotePickBots(space);
space.peers.forEach((_p, pid) => {
const sock = io.sockets.sockets.get(pid);
@@ -3907,7 +3953,7 @@ function startPlayerPickVote(sid, space, purpose, onResolve) {
purpose,
candidates: candidates.map((c) => ({ id: c.id, nickname: c.nickname, isBot: c.isBot })),
endsAt: space.pickVote.endsAt,
durationMs: PICK_VOTE_MS,
durationMs: PV_MS,
});
});
emitPickVoteUpdate(sid, space);
@@ -3915,7 +3961,7 @@ function startPlayerPickVote(sid, space, purpose, onResolve) {
const sp = spaces.get(sid);
if (!sp || !sp.pickVote || sp.pickVote.resolved) return;
resolvePlayerPickVote(sid, sp);
}, PICK_VOTE_MS + 120);
}, PV_MS + 120);
maybeResolvePickVote(sid, space);
}
@@ -3933,10 +3979,11 @@ function resolvePlayerPickVote(sid, space) {
clearPickVoteTimer(space);
const counts = {};
Object.keys(pv.votes).forEach((vid) => { const t = pv.votes[vid]; counts[t] = (counts[t] || 0) + 1; });
let max = -1;
let max = 0;
Object.keys(counts).forEach((tid) => { if (counts[tid] > max) max = counts[tid]; });
const maxIds = Object.keys(counts).filter((tid) => counts[tid] === max);
const target = maxIds.length ? maxIds[Math.floor(Math.random() * maxIds.length)] : null;
/* เสมอ (มากกว่า 1 คนได้สูงสุดเท่ากัน) หรือไม่มีใครโหวต → ไม่มีใครโดน (เป้าหมายปลอดภัย) */
const target = (max > 0 && maxIds.length === 1) ? maxIds[0] : null;
const tc = pv.candidates.find((c) => c.id === target);
const targetName = tc ? tc.nickname : '';
io.to(sid).emit('player-pick-vote-result', { purpose: pv.purpose, targetId: target, targetName, counts });
@@ -3961,6 +4008,7 @@ function trialEligibleVoterTotal(space) {
function autoVoteBotsTrial(space) {
if (!space.trialVotes) space.trialVotes = {};
const culprit = (typeof space.culpritIndex === 'number') ? space.culpritIndex : Math.floor(Math.random() * 3);
const excluded = Number.isInteger(space.trialRevoteExcluded) ? space.trialRevoteExcluded : -1;
testimonyBotIds(space).forEach((bid) => {
if (bid === space.silencedPlayerId) return; /* บอทที่ถูกปิดปากไม่โหวต */
if (space.trialVotes[bid] != null) return;
@@ -3971,6 +4019,8 @@ function autoVoteBotsTrial(space) {
} else {
pick = Math.floor(Math.random() * 3);
}
/* รอบจับผิดตัว: ห้ามบอทโหวตผู้ต้องสงสัยที่ถูกตัดออก → เปลี่ยนไปโหวตคนร้ายจริง */
if (pick === excluded) pick = culprit;
space.trialVotes[bid] = pick;
});
}
@@ -3981,8 +4031,8 @@ function advanceTestimony(sid, space) {
space.trialBailUsed = false;
space.silencedPlayerId = null;
/* Card 4 Silence — โหวตเลือกผู้เล่น 1 คนให้ห้ามโหวต ก่อนเข้าโหวตจริง */
const pend = space.pendingSpecialCard;
if (pend && Number(pend.cardId) === 4 && !pend.consumed) {
const pend = findPendingSpecialCard(space, 4);
if (pend) {
pend.consumed = true;
io.to(sid).emit('special-card-applied', { card: specialCardClientPayload(pend.card), context: 'silence' });
startPlayerPickVote(sid, space, 'silence', (targetId) => {
@@ -7509,7 +7559,7 @@ io.on('connection', (socket) => {
purpose: pv.purpose,
candidates: pv.candidates.map((c) => ({ id: c.id, nickname: c.nickname, isBot: c.isBot })),
endsAt: pv.endsAt,
durationMs: PICK_VOTE_MS,
durationMs: pickVoteMs(),
});
}
}
@@ -7736,8 +7786,8 @@ io.on('connection', (socket) => {
return reply({ ok: false, error: 'ยังไม่ได้สุ่มมินิเกมสำหรับการ์ดนี้' });
}
/* Card 3 Ban — ก่อนเริ่มมินิเกมรอบหน้า โหวตเลือกผู้เล่น 1 คนที่ห้ามเล่น */
const pend = space.pendingSpecialCard;
if (pend && Number(pend.cardId) === 3 && !pend.consumed && !space.pickVote) {
const pend = findPendingSpecialCard(space, 3);
if (pend && !space.pickVote) {
pend.consumed = true;
io.to(sid).emit('special-card-applied', { card: specialCardClientPayload(pend.card), context: 'ban' });
startPlayerPickVote(sid, space, 'ban', (targetId) => {
@@ -7785,6 +7835,21 @@ io.on('connection', (socket) => {
}
return reply({ ok: false, error: 'ไม่ได้อยู่ในมินิเกมสืบสวน' });
}
/* MG2 (gauntlet): ""
จนกวาทกคนทงไมตาย/ไมกแบนจะเขาเสนครบ แลวคอยจบพรอมก (returnDetective แจกการ
กคนพรอมกนตอนทกคนยงอยในหอง) · endGauntletGame timer เป fallback าใครไมเขาเส */
const finMd = (space.mapId && maps.get(space.mapId)) || space.mapData;
if (finMd && finMd.gameType === 'gauntlet' && space.detectiveMinigameActive) {
if (finPeer) finPeer.detectiveFinished = true;
const allDone = [...space.peers.values()].every((p) =>
p.gauntletEliminated || p.bannedSpectator || p.detectiveFinished
);
if (!allDone) {
/* ยังมีคนวิ่งอยู่ → ให้ client นี้รอในฉาก (ไม่เด้งออก) จะกลับ LobbyB พร้อมกันตอนเกมจบ */
return reply({ ok: true, holdInScene: true });
}
/* ครบทุกคนแล้ว → จบเกมให้ทุกคนด้านล่าง */
}
// เล่นมินิเกม 1 ครั้ง = ได้การ์ด 1 ใบ (ไม่ขึ้นเกรด/คะแนน)
returnDetectiveSpaceToLobbyB(sid, space, 'จบมินิเกม — กลับ LobbyB', { allPlayers: true });
const g = space.lastEvidenceGrant || {};
@@ -7935,6 +8000,10 @@ io.on('connection', (socket) => {
if (socket.id === space.silencedPlayerId) return reply({ ok: false, error: 'คุณถูกปิดปาก (Silence) — โหวตรอบนี้ไม่ได้', silenced: true });
let idx = Math.floor(Number(data && data.index));
if (Number.isNaN(idx) || idx < 0 || idx > 2) return reply({ ok: false, error: 'เลือกผู้ต้องสงสัยไม่ถูกต้อง' });
/* รอบจับผิดตัว: ห้ามโหวตผู้ต้องสงสัยที่โหวตผิดไปแล้ว */
if (Number.isInteger(space.trialRevoteExcluded) && idx === space.trialRevoteExcluded) {
return reply({ ok: false, error: 'ผู้ต้องสงสัยนี้ถูกโหวตไปแล้ว (ผิด) — เลือกคนอื่น', excluded: true });
}
space.trialVotes = space.trialVotes || {};
space.trialVotes[socket.id] = idx;
emitTrialVoteUpdate(sid, space);
+129
View File
@@ -0,0 +1,129 @@
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex,nofollow">
<title>สรุปงานพัฒนา Justice — 17 มิ.ย. 2026</title>
<style>
:root{--blue:#1f3864;--blue2:#2e75b6;--bg:#0e1426;--card:#161e36;--ink:#e7ecff;--mut:#9aa6c8;--ok:#5dffa0;--warn:#ffd166}
*{box-sizing:border-box}
body{margin:0;background:linear-gradient(160deg,#0b1020,#101a33);color:var(--ink);font-family:"Segoe UI","Tahoma","Leelawadee UI",system-ui,sans-serif;line-height:1.6}
.wrap{max-width:880px;margin:0 auto;padding:32px 20px 80px}
header{text-align:center;padding:28px 16px 22px;border-bottom:2px solid var(--blue2);margin-bottom:26px}
h1{margin:0 0 6px;font-size:30px;color:#bcd4ff}
.sub{color:var(--mut);font-size:15px}
.badge{display:inline-block;margin-top:10px;padding:4px 12px;border:1px solid var(--ok);border-radius:999px;color:var(--ok);font-size:13px}
section{background:var(--card);border:1px solid #25304f;border-radius:14px;padding:18px 22px;margin:16px 0;box-shadow:0 6px 24px rgba(0,0,0,.3)}
h2{margin:0 0 10px;font-size:20px;color:#9fc3ff;display:flex;align-items:center;gap:10px}
h2 .n{flex:none;width:30px;height:30px;border-radius:8px;background:var(--blue2);color:#fff;display:flex;align-items:center;justify-content:center;font-size:15px;font-weight:700}
h3{margin:14px 0 6px;font-size:15px;color:#c8d6ff;letter-spacing:.02em}
ul{margin:6px 0;padding-left:22px}
li{margin:4px 0}
.lead{color:var(--mut);font-style:italic;margin:0 0 8px}
.pend{color:var(--warn)}
.pend b{color:#ffe08a}
.ref b{color:#9fffd0}
code{background:#0c1224;border:1px solid #2a3656;border-radius:5px;padding:1px 6px;font-size:13px;color:#bfe0ff}
footer{text-align:center;color:var(--mut);font-size:13px;margin-top:30px}
.pill{display:inline-block;background:#0c1224;border:1px solid #2a3656;border-radius:6px;padding:1px 8px;font-size:12px;color:#9fc3ff;margin-left:6px}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>สรุปงานพัฒนา — เกม Justice (นักสืบไซเบอร์)</h1>
<div class="sub">วันที่ 17 มิถุนายน 2026 · Production: srv1361159.hstgr.cloud</div>
<div class="badge">✓ Deploy ขึ้น prod แล้วทั้งหมด</div>
</header>
<p class="lead">เอกสารนี้สรุปงานที่พัฒนา/แก้ไขในวันนี้ทั้งหมด 6 เรื่องหลัก — ทุกอย่างถูก deploy ขึ้น production และตรวจสอบการทำงานแล้ว</p>
<section>
<h2><span class="n">1</span> Special Quiz — หน้าคำถาม/เฉลย</h2>
<p class="lead">รื้อดีไซน์ใหม่ให้ตรงกับ mock 09-Quiz</p>
<ul>
<li>กรอบ panel ใช้ <code>special-quiz.png</code>, ตัวเลือกแบบ "A : ข้อความ" ใช้ <code>choice-*.png</code> + เครื่องหมายถูก/ผิด</li>
<li>วงนับเวลาย้ายมากลาง-ล่างของกรอบ</li>
<li>ตัด chip "สมาชิกในรอบนี้" ออก → แสดง <b>avatar ผู้เล่นใต้คำตอบที่เลือก</b> (ตัวเองตอนกดตอบ, ทุกคนตอนเฉลย)</li>
<li class="pend"><b>ยังค้าง:</b> avatar เป็นวงเลขสี (ยังไม่ใช่หน้าตัวละครจริง), ปุ่ม "ถัดไป" ยังซ่อน, หน้าการ์ดรางวัล (m3) ยัง layout เดิม</li>
</ul>
</section>
<section>
<h2><span class="n">2</span> หน้าสรุปผลภารกิจ — โบนัสพิเศษ</h2>
<ul>
<li>แสดงรูปสถานะการ์ดเสมอ (ไม่ขึ้นกับเกรด): ได้การ์ด = <code>mission-complete-select.png</code>, ไม่ได้ = <code>mission-complete.png</code></li>
<li>ช่องการ์ด: ได้ = โชว์การ์ดที่ได้, ไม่ได้ = <code>popup-result-card-bg.png</code></li>
<li>เอาข้อความ "ไม่ได้รับการ์ด" ออกตามที่ขอ</li>
</ul>
</section>
<section>
<h2><span class="n">3</span> ระบบ Achievements <span class="pill">สร้างใหม่ทั้งระบบ</span></h2>
<p class="lead">25 รางวัล แบ่ง 5 หมวด (Analyst / Explorer / Risk / Support / Elite)</p>
<h3>ส่วนผู้เล่น (โปรไฟล์)</h3>
<ul>
<li>แสดงแคตตาล็อก 25 รางวัล (ชื่อ + คำอธิบาย + แถบ progress + ตัวนับ) สลับหมวด 5 ปุ่ม</li>
<li>ใส่รูป badge 25 ใบในกรอบ icon</li>
</ul>
<h3>ส่วน Admin</h3>
<ul>
<li>แท็บ "Achievements": แก้แคตตาล็อก + จัดการ progress รายผู้เล่น (ตั้งค่า/ปลดล็อก/รีเซ็ต)</li>
</ul>
<h3>Auto-track (ติดตามอัตโนมัติ)</h3>
<ul>
<li>d1 Minigame Solver — รอดมินิเกม</li>
<li>a1 First Deduction / a5 Truth Hunter — โหวตถูกตัวคนร้าย</li>
<li>e2 The Impostor / e4 Agent of Chaos — รับบทตัวป่วน · e3 / e5 — เมื่อตัวป่วนชนะ</li>
<li class="pend"><b>ยังค้าง:</b> a2, a3, a4, a6, b*, c*, d2-d4, e1 (ต้องเก็บ state เพิ่ม) — ตั้งค่าผ่าน Admin ได้</li>
</ul>
</section>
<section>
<h2><span class="n">4</span> ฉากจบ (Trial Final)</h2>
<ul>
<li>ชนะ + ไม่มีตัวป่วน → แสดง <b>ผู้เล่นทุกคน</b>บนโพเดียม (เดิมแสดงแค่ผู้โหวตถูก)</li>
<li>โหวตผิด + มีตัวป่วน → <code>troll-win-bg.png</code> + ตัวละครตัวป่วน + <code>txt-troll-win.png</code></li>
<li>โหวตผิด + ไม่มีตัวป่วน → <code>lose-bg.png</code> + <code>txt-your-lose.png</code> (เดิมขึ้น troll-win ผิด)</li>
</ul>
</section>
<section>
<h2><span class="n">5</span> Leaderboard (Main-Lobby)</h2>
<ul>
<li>เปลี่ยนจาก "(เหรียญ : ...)" → <b>"(คดี : ชื่อคดี)"</b></li>
<li>แสดงคะแนนสูงสุดของแต่ละผู้เล่น + ระบุว่าได้จากคดีไหน (ดึงชื่อจาก case-media.json)</li>
<li class="pend"><b>หมายเหตุ:</b> คดี 4-15 ยังเป็นชื่อ default "คดีที่ N" — ตั้งชื่อจริงที่ Admin → "รูปคดี &amp; Cutscene"</li>
</ul>
</section>
<section>
<h2><span class="n">6</span> การ์ด Bail Coin — รอบโหวตใหม่ (จับผิดตัว)</h2>
<ul>
<li>ใบที่โหวตผิดไปแล้วถูกตีตรา "โหวตผิดไปแล้ว" + หรี่/เทาทั้งใบ</li>
<li>ห้ามโหวตซ้ำ — กัน 3 ชั้น: client (กดไม่ได้), server (ปฏิเสธ), บอท (หลบไปโหวตคนร้ายจริง)</li>
</ul>
</section>
<section class="ref">
<h2>ตอบคำถาม / อ้างอิง</h2>
<ul>
<li><b>แก้ Quiz-Battle:</b> เนื้อหา/หมวด → Admin → แท็บ "Quiz Battle" · หน้าตาเพจ → ไฟล์ <code>Quiz-Battle/index.html, quiz-battle.js, style.css</code></li>
<li><b>ทดสอบฉากจบ:</b> Test Mode → <code>Ctrl+Alt+1</code> ชนะ · <code>Ctrl+Alt+2</code> แพ้ · <code>Ctrl+Alt+3</code> สลับตัวป่วน · <code>Ctrl+Alt+W</code> บังคับโหวตผิด</li>
</ul>
</section>
<section>
<h2>งานที่ยังเปิดค้าง</h2>
<ul>
<li>Special Quiz: avatar หน้าตัวละครจริง + ปุ่ม "ถัดไป" + หน้าการ์ดรางวัล (m3)</li>
<li>Achievements: auto-track เงื่อนไขที่เหลือ (ต้องเก็บ state ระดับหลักฐาน/สตรีค)</li>
<li>ตั้งชื่อคดี 4-15 ใน Admin เพื่อให้ leaderboard แสดงชื่อจริง</li>
</ul>
</section>
<footer>Justice — สรุปงานพัฒนา 17 มิถุนายน 2026</footer>
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

@@ -0,0 +1,168 @@
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex,nofollow">
<title>Silence Vote — Popup Design Mockup</title>
<style>
*{box-sizing:border-box}
html,body{margin:0;height:100%}
body{
background:#10131c url('cut/12-1-ประกาศผล/cyber-prison.png') center/cover no-repeat;
font-family:"Kanit","Segoe UI",system-ui,sans-serif;color:#e7ecff;min-height:100vh;overflow-x:hidden;
}
/* overlay เต็มจอ (popup ทับฉากเกม) — ไม่มีกล่อง */
.ov{position:fixed;inset:0;z-index:10;display:none;flex-direction:column;align-items:center;justify-content:flex-start;
padding:3vh 2vw 0;background:rgba(8,11,22,.55)}
.ov.on{display:flex}
.ov::before{content:"";position:absolute;inset:0;background:radial-gradient(ellipse at 50% 42%,rgba(10,16,34,.30),rgba(6,9,18,.78));z-index:-1}
/* ===== ไอคอน [ห้ามโหวต] ===== */
.novote-ic-img{margin-top:1vh;width:clamp(104px,10vw,158px);height:auto;display:block;
filter:drop-shadow(0 0 22px rgba(130,150,255,.6))}
.novote-label{margin-top:6px;font:800 clamp(20px,2.2vw,30px) Kanit;color:#dfe6ff;text-shadow:0 0 14px rgba(140,120,255,.6)}
.title-img{display:block;margin:14px auto 4px;max-width:min(820px,86vw);height:auto;filter:drop-shadow(0 2px 10px rgba(0,0,0,.5))}
.result-title{display:block;margin:2vh auto 2.4vh;height:clamp(48px,5vw,72px);width:auto}
/* progress */
.bar{position:relative;width:min(560px,52vw);height:clamp(26px,3vh,38px);margin:2.2vh auto 3vh}
.bar .frame{position:absolute;inset:0;width:100%;height:100%;object-fit:fill}
.bar .fill{position:absolute;top:16%;left:1.5%;height:68%;width:62%;border-radius:30px;background:linear-gradient(90deg,#ff2e54,#ff6b81);box-shadow:0 0 14px rgba(255,70,100,.8)}
/* players row */
.players{display:flex;justify-content:center;align-items:flex-start;gap:clamp(16px,3.4vw,56px);flex-wrap:nowrap;margin:0 auto}
.pl{display:flex;flex-direction:column;align-items:center;gap:10px;cursor:pointer}
.ava{position:relative;width:clamp(100px,9.4vw,150px);height:clamp(100px,9.4vw,150px)}
.ava .bg{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;z-index:1}
.ava .frame{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;z-index:3}
.ava .frame.sel{display:none}
.pl.sel .ava .frame.norm{display:none}
.pl.sel .ava .frame.sel{display:block}
/* chibi placeholder เต็มกรอบ (จริงในเกม = ตัวละคร+สีของผู้เล่น) */
.ava .chibi{position:absolute;z-index:2;left:11%;right:11%;top:13%;bottom:6%}
.ava .chibi .hair{position:absolute;top:0;left:8%;right:8%;height:46%;border-radius:48% 48% 30% 30%}
.ava .chibi .face{position:absolute;top:24%;left:20%;right:20%;height:54%;border-radius:42% 42% 50% 50%;background:#ffe2cc}
.ava .chibi .eye{position:absolute;top:46%;width:11%;height:13%;background:#3a2a22;border-radius:50%}
.ava .chibi .eye.l{left:30%}.ava .chibi .eye.r{right:30%}
.ava .chibi .body{position:absolute;bottom:0;left:14%;right:14%;height:30%;border-radius:30% 30% 12% 12%;opacity:.92}
.name{font:800 clamp(15px,1.5vw,20px) Kanit;letter-spacing:.02em}
.pl.sel .name{color:#ff6b81;text-shadow:0 0 10px rgba(255,80,110,.6)}
.count{font:900 clamp(28px,3vw,40px) Kanit;line-height:1;margin-top:2px}
.pl.sel .count{color:#ff3b5c;text-shadow:0 0 12px rgba(255,70,100,.7)}
/* result banner */
.banner{position:relative;width:min(900px,90vw);min-height:clamp(110px,13vh,150px);margin:1.5vh auto 2.4vh;display:flex;align-items:center}
.banner .bg{position:absolute;inset:0;width:100%;height:100%;object-fit:fill}
.banner .inner{position:relative;z-index:2;display:flex;align-items:center;justify-content:space-between;width:100%;padding:12px clamp(20px,3vw,40px);gap:18px}
.banner .txtcol{flex:1;text-align:center}
.banner .who{font:900 clamp(24px,2.6vw,32px) Kanit;color:#fff;text-shadow:0 2px 8px rgba(0,0,0,.6);margin-bottom:2px}
.banner .rtxt{display:block;margin:0 auto;max-width:100%;height:auto}
.banner .card{height:clamp(120px,15vh,168px);width:auto;filter:drop-shadow(0 6px 16px rgba(0,0,0,.5));flex:none}
.cta{display:block;margin:2vh auto 0;cursor:pointer;width:clamp(230px,26vw,330px);height:auto;transition:transform .08s,filter .12s}
.cta:hover{transform:translateY(-2px);filter:brightness(1.08)}
/* tabs (mockup only) */
.tabs{position:fixed;top:10px;left:50%;transform:translateX(-50%);z-index:60;display:flex;gap:8px;background:rgba(8,12,24,.9);border:1px solid #2a3656;border-radius:12px;padding:6px}
.tabs button{background:#16203a;color:#bcd4ff;border:1px solid #2a3656;border-radius:8px;padding:7px 14px;font:600 13px Kanit;cursor:pointer}
.tabs button.on{background:#1f3a6e;color:#fff;border-color:#5eeaff;box-shadow:0 0 10px rgba(94,234,255,.4)}
.note{position:fixed;bottom:8px;left:50%;transform:translateX(-50%);z-index:60;color:#9ab;font:500 12px Kanit;background:rgba(8,12,24,.85);padding:5px 12px;border-radius:8px;border:1px solid #243}
</style>
</head>
<body>
<div class="tabs">
<button class="on" data-v="pick">1 · เลือกโหวต</button>
<button data-v="silenced">2 · ผล: โดนปิดปาก</button>
<button data-v="safe">3 · ผล: รอดตัว</button>
</div>
<!-- VIEW 1: PICK -->
<div class="ov on" id="v-pick">
<img class="novote-ic-img" src="cut/11-1-2-ห้ามโหวต/item-7.png" alt="ห้ามโหวต">
<div class="novote-label">[ ห้ามโหวต ]</div>
<img class="title-img" src="cut/11-1-2-ห้ามโหวต/txt-header.png" alt="เลือกผู้เล่น 1 คน เพื่อระงับสิทธิ์ออกเสียง ในห้องพิจารณาคดี">
<div class="bar"><img class="frame" src="cut/11-1-2-ห้ามโหวต/time-frame.png" alt=""><div class="fill"></div></div>
<div class="players" data-row="pick"></div>
<img class="cta" src="cut/11-1-2-ห้ามโหวต/btn-confirm_1.png" alt="ยืนยัน">
</div>
<!-- VIEW 2: SILENCED -->
<div class="ov" id="v-silenced">
<img class="result-title" src="cut/11-1-2-ห้ามโหวต/txt-result.png" alt="สรุปผลโหวต">
<div class="players" data-row="silenced"></div>
<div class="banner">
<img class="bg" src="cut/11-1-2-ห้ามโหวต/frame-red.png" alt="">
<div class="inner">
<div class="txtcol"><div class="who">Rainy</div><img class="rtxt" src="cut/11-1-2-ห้ามโหวต/result-txt-1.png" style="max-width:min(560px,52vw)" alt="ถูกใช้การ์ดห้ามชี้ตัวผู้ร้าย ในห้องพิจารณาคดี"></div>
<img class="card" src="cut/11-1-2-ห้ามโหวต/card-silence.png" alt="Silence Card">
</div>
</div>
<img class="cta" src="cut/11-1-2-ห้ามโหวต/btn-gotovote.png" alt="ไปชี้ตัวคนร้าย">
</div>
<!-- VIEW 3: SAFE -->
<div class="ov" id="v-safe">
<img class="result-title" src="cut/11-1-2-ห้ามโหวต/txt-result.png" alt="สรุปผลโหวต">
<div class="players" data-row="safe"></div>
<div class="banner">
<img class="bg" src="cut/11-1-2-ห้ามโหวต/frame-red.png" alt="">
<div class="inner">
<div class="txtcol"><img class="rtxt" src="cut/11-1-2-ห้ามโหวต/result-txt-2.png" style="max-width:min(520px,48vw)" alt="รอดตัวไป! ตกลงกันไม่ได้.. เป้าหมายปลอดภัย"></div>
<img class="card" src="cut/11-1-2-ห้ามโหวต/card-silence-broken.png" alt="">
</div>
</div>
<img class="cta" src="cut/11-1-2-ห้ามโหวต/btn-gotovote.png" alt="ไปชี้ตัวคนร้าย">
</div>
<div class="note">Mockup · ตัวละคร+พื้นหลัง = placeholder (ในเกมจริงใช้ตัวละคร/สีของผู้เล่นจริง) · กดแท็บสลับ 3 สถานะ</div>
<script>
var A = 'cut/11-1-2-ห้ามโหวต/';
var PLAYERS = [
{n:'JACKSON', hair:'#e8b53a', sel:false},
{n:'MONE', hair:'#ff7ec8', sel:false},
{n:'RAINY', hair:'#ff5a5a', sel:true},
{n:'MICKY', hair:'#5a86ff', sel:false},
{n:'JAME', hair:'#5ad65f', sel:false},
];
var COUNTS = { silenced:['X1','X0','X3','X1','X0'], safe:['X2','X0','X2','X1','X0'] };
var SEL = { pick:2, silenced:2, safe:-1 };
function chibi(hair){
return '<div class="chibi"><div class="hair" style="background:'+hair+'"></div><div class="face"></div>'
+'<div class="eye l"></div><div class="eye r"></div>'
+'<div class="body" style="background:'+hair+'"></div></div>';
}
function buildRow(mode){
var selIdx = SEL[mode];
return PLAYERS.map(function(p,i){
var isSel = (i===selIdx);
var counts = COUNTS[mode];
var countHtml = counts ? '<div class="count">'+counts[i]+'</div>' : '';
return '<div class="pl'+(isSel?' sel':'')+'">'
+'<div class="ava">'
+'<img class="bg" src="'+A+'player-icon-bg.png" alt="">'
+ chibi(p.hair)
+'<img class="frame norm" src="'+A+'player-icon-frame.png" alt="">'
+'<img class="frame sel" src="'+A+'player-icon-frame-select.png" alt="">'
+'</div>'
+'<div class="name">'+p.n+'</div>'+countHtml
+'</div>';
}).join('');
}
document.querySelectorAll('.players').forEach(function(row){ row.innerHTML = buildRow(row.getAttribute('data-row')); });
var tabs = document.querySelectorAll('.tabs button');
var views = { pick:'v-pick', silenced:'v-silenced', safe:'v-safe' };
tabs.forEach(function(b){
b.addEventListener('click', function(){
tabs.forEach(function(x){ x.classList.remove('on'); });
b.classList.add('on');
Object.keys(views).forEach(function(k){ document.getElementById(views[k]).classList.toggle('on', k===b.getAttribute('data-v')); });
});
});
</script>
</body>
</html>
@@ -0,0 +1,126 @@
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex,nofollow">
<title>Special Quiz — Design Mockup</title>
<style>
*{box-sizing:border-box}
html,body{margin:0;height:100%}
body{background:#0d1018 url('cut/12-1-ประกาศผล/cyber-prison.png') center/cover no-repeat;
font-family:"Kanit","Segoe UI",system-ui,sans-serif;color:#e7ecff;min-height:100vh;overflow-x:hidden}
.ov{position:fixed;inset:0;z-index:10;display:none;flex-direction:column;align-items:center;justify-content:center;padding:2vh 2vw;background:rgba(8,11,22,.6)}
.ov.on{display:flex}
.ov::before{content:"";position:absolute;inset:0;background:radial-gradient(ellipse at 50% 45%,rgba(10,16,34,.3),rgba(6,9,18,.8));z-index:-1}
/* ===== Q/Answer panel (09-Quiz) ===== */
.sq-panel{position:relative;width:min(760px,92vw,calc(94vh * 1305 / 1240));aspect-ratio:1305 / 1240;max-height:94vh;overflow:auto;scrollbar-width:none;box-sizing:border-box;
background:url('cut/09-Quiz/special-quiz-tall.png') center/100% 100% no-repeat;
padding:clamp(86px,14%,150px) 9% clamp(54px,8%,96px)}
.sq-panel::-webkit-scrollbar{width:0;height:0}
.sq-q{margin:0 2px 22px;font:800 clamp(17px,2.2vw,25px)/1.4 Kanit;text-align:center;color:#fff;text-shadow:0 1px 6px rgba(0,0,0,.5)}
.sq-choices{display:flex;flex-direction:column;gap:clamp(16px,2.8vh,30px)}
.sq-choice{position:relative;display:flex;align-items:center;justify-content:center;text-align:center;
padding:clamp(13px,2.1vh,21px) 46px;background:url('cut/09-Quiz/choice-default.png') center/100% 100% no-repeat;
color:#ffffff;font:800 clamp(15px,1.9vw,21px) Kanit;text-shadow:0 1px 4px rgba(0,0,0,.6)}
.sq-choice.correct{background-image:url('cut/09-Quiz/choice-correct.png');color:#ffffff}
.sq-choice.correct::after{content:"";position:absolute;right:16px;top:50%;transform:translateY(-50%);width:clamp(28px,4vw,42px);height:clamp(28px,4vw,42px);background:url('cut/09-Quiz/correct.png') center/contain no-repeat}
.sq-avs{position:absolute;left:50%;bottom:0;transform:translate(-50%,29%);display:flex;gap:4px;z-index:4}
.sq-av{position:relative;width:clamp(30px,3.6vw,42px);height:clamp(30px,3.6vw,42px);display:flex;align-items:center;justify-content:center}
.sq-av .frame{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;z-index:2}
.sq-av .ch{position:absolute;left:18%;right:18%;top:14%;bottom:10%;z-index:1;border-radius:50% 50% 40% 40%}
.sq-timer{position:absolute;left:50%;bottom:3.4%;transform:translateX(-50%);width:clamp(44px,8%,68px);aspect-ratio:1;border-radius:50%;display:flex;align-items:center;justify-content:center;font:800 clamp(18px,2.4vw,26px) Kanit;color:#eafcff;border:3px solid rgba(94,234,255,.85);background:rgba(8,16,30,.85);box-shadow:0 0 14px rgba(94,234,255,.4)}
.sq-next{position:absolute;left:50%;bottom:0;transform:translate(-50%,-12%);width:clamp(150px,22%,210px);aspect-ratio:354/132;background:url('cut/09-Quiz/btn-next_1.png') center/100% 100% no-repeat;cursor:pointer}
/* ===== Card reward (10-Game-1-result) ===== */
.cr{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;width:100%;height:100%}
.cr-stage{position:relative;width:min(560px,82vw);height:min(420px,52vh);display:flex;align-items:center;justify-content:center;margin-bottom:1vh}
.cr-effect{position:absolute;left:50%;top:42%;transform:translate(-50%,-50%);width:min(540px,80vw);opacity:.9;z-index:1;pointer-events:none}
.cr-card{position:relative;z-index:3;height:clamp(220px,38vh,330px);width:auto;filter:drop-shadow(0 10px 30px rgba(0,0,0,.6));animation:crPop .5s cubic-bezier(.2,.9,.3,1.4)}
@keyframes crPop{from{opacity:0;transform:scale(.8) translateY(20px)}to{opacity:1;transform:none}}
/* characters in a ring around the card */
.cr-ring{position:absolute;left:50%;top:50%;width:1px;height:1px;z-index:2}
.cr-char{position:absolute;left:0;top:0;display:flex;flex-direction:column;align-items:center;transform:translate(-50%,-50%)}
.cr-char .ring{width:clamp(70px,9vw,108px);height:clamp(26px,3.2vw,38px);border-radius:50%;border:2px solid rgba(94,234,255,.55);box-shadow:0 0 16px rgba(94,234,255,.4);position:absolute;bottom:-6px;left:50%;transform:translateX(-50%)}
.cr-chibi{position:relative;width:clamp(56px,7vw,84px);height:clamp(70px,9vw,104px)}
.cr-chibi .hair{position:absolute;top:2%;left:18%;right:18%;height:34%;border-radius:48% 48% 30% 30%}
.cr-chibi .face{position:absolute;top:24%;left:28%;right:28%;height:36%;border-radius:44% 44% 50% 50%;background:#ffe2cc}
.cr-chibi .body{position:absolute;bottom:6%;left:24%;right:24%;height:42%;border-radius:34% 34% 18% 18%}
.cr-txt{display:block;max-width:min(720px,86vw);height:auto;margin:0 auto 2.4vh}
.cr-cta{display:block;width:clamp(220px,26vw,320px);height:auto;cursor:pointer;transition:transform .08s,filter .12s}
.cr-cta:hover{transform:translateY(-2px);filter:brightness(1.08)}
.tabs{position:fixed;top:10px;left:50%;transform:translateX(-50%);z-index:60;display:flex;gap:8px;background:rgba(8,12,24,.9);border:1px solid #2a3656;border-radius:12px;padding:6px}
.tabs button{background:#16203a;color:#bcd4ff;border:1px solid #2a3656;border-radius:8px;padding:7px 14px;font:600 13px Kanit;cursor:pointer}
.tabs button.on{background:#1f3a6e;color:#fff;border-color:#5eeaff;box-shadow:0 0 10px rgba(94,234,255,.4)}
.note{position:fixed;bottom:8px;left:50%;transform:translateX(-50%);z-index:60;color:#9ab;font:500 12px Kanit;background:rgba(8,12,24,.85);padding:5px 12px;border-radius:8px;border:1px solid #243}
</style>
</head>
<body>
<div class="tabs">
<button class="on" data-v="q">1 · คำถาม</button>
<button data-v="a">2 · เฉลย</button>
<button data-v="card">3 · รับการ์ด</button>
</div>
<!-- VIEW 1: QUESTION -->
<div class="ov on" id="v-q">
<div class="sq-panel">
<div class="sq-q">การกระทำใดผิดกฎหมายไซเบอร์?</div>
<div class="sq-choices">
<div class="sq-choice">A : การเจาะระบบเข้าสู่ข้อมูลส่วนบุคคล<div class="sq-avs" id="avs-q"></div></div>
<div class="sq-choice">B : การใช้ WI-FI สาธารณะเพื่อดูข่าว</div>
<div class="sq-choice">C : การอัปเดตซอฟต์แวร์เพื่อความปลอดภัย</div>
</div>
<div class="sq-timer">10</div>
</div>
</div>
<!-- VIEW 2: ANSWERED -->
<div class="ov" id="v-a">
<div class="sq-panel">
<div class="sq-q">การกระทำใดผิดกฎหมายไซเบอร์?</div>
<div class="sq-choices">
<div class="sq-choice correct">A : การเจาะระบบเข้าสู่ข้อมูลส่วนบุคคล<div class="sq-avs" id="avs-a"></div></div>
<div class="sq-choice">B : การใช้ WI-FI สาธารณะเพื่อดูข่าว</div>
<div class="sq-choice">C : การอัปเดตซอฟต์แวร์เพื่อความปลอดภัย</div>
</div>
<div class="sq-next"></div>
</div>
</div>
<!-- VIEW 3: CARD REWARD -->
<div class="ov" id="v-card">
<div class="cr">
<div class="cr-stage">
<img class="cr-effect" src="cut/10-Game-1-result/card-effect.png" alt="">
<img class="cr-card" src="cut/10-Game-1-result/card-5.png" alt="Bail Coin">
</div>
<img class="cr-txt" src="cut/10-Game-1-result/card-5-txt.png" alt="เหรียญประกันตัว (กันตายเมื่อทายผิด) · วิธีใช้งาน : ใช้ในห้องพิจารณาคดี กรณีชี้ตัวคนร้ายผิดตัว">
<img class="cr-cta" src="cut/10-Game-1-result/btn-continue-play.png" alt="เล่นเกมต่อ">
</div>
</div>
<div class="note">Special Quiz mockup · ตัวละคร = placeholder (ในเกมจริงใช้ตัวละครผู้เล่น) · กดแท็บสลับ 3 หน้า</div>
<script>
var HAIR=['#e8b53a','#ff7ec8','#5a86ff','#ff5a5a','#5ad65f'];
function chibiSmall(hair){
return '<img class="frame" src="cut/09-Quiz/player-pic-frame.png" alt="">'
+'<div class="ch" style="background:'+hair+'"></div>';
}
['avs-q','avs-a'].forEach(function(id){
var el=document.getElementById(id); if(!el) return;
el.innerHTML=HAIR.map(function(h){ return '<div class="sq-av">'+chibiSmall(h)+'</div>'; }).join('');
});
/* หน้ารับการ์ด: ไม่ใส่ตัวละคร placeholder — ในเกมจริงจะวางตัวละครผู้เล่นล้อมการ์ดเอง */
var tabs=document.querySelectorAll('.tabs button');
var views={q:'v-q',a:'v-a',card:'v-card'};
tabs.forEach(function(b){ b.addEventListener('click',function(){
tabs.forEach(function(x){x.classList.remove('on');}); b.classList.add('on');
Object.keys(views).forEach(function(k){ document.getElementById(views[k]).classList.toggle('on', k===b.getAttribute('data-v')); });
});});
</script>
</body>
</html>