update game เกมจริงหรือไม่ ถ้าตายหมดให้เกม Overและอื่นๆมากมาย
@@ -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;
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
|
||||
<div class="admin-tab-sep" aria-hidden="true"><span>คำถาม & การ์ดพิเศษ</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">รูปคดี & 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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}]}
|
||||
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 409 B |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 409 B |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -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 สำรอง — เล่นต่อทันที */
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 → "รูปคดี & 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>
|
||||
|
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>
|
||||