join room เพิ่มรูปให้เข้าใจง่าย

This commit is contained in:
2026-06-23 12:52:17 +00:00
parent 064c3621ab
commit 38581636b4
15 changed files with 988 additions and 64 deletions
+59 -31
View File
@@ -51,9 +51,9 @@
"providerUserId": "p_1775109142385_wq7wfy1p32j",
"notes": "auto: player-coins",
"blocked": false,
"coins": 953,
"coins": 1051,
"createdAt": "2026-04-02T05:52:21+00:00",
"updatedAt": "2026-06-22T17:14:58+00:00",
"updatedAt": "2026-06-23T09:46:51+00:00",
"daily": {
"anchorMs": 1781197200000,
"claimedDays": [
@@ -67,16 +67,16 @@
],
"lockUntilMs": 0
},
"score": 848,
"score": 936,
"scoreByCase": {
"1": 16,
"10": 320,
"8": 220,
"10": 340,
"8": 230,
"11": 20,
"13": 40,
"13": 88,
"14": 20,
"9": 40,
"12": 116,
"12": 126,
"15": 20,
"4": 10,
"5": 20,
@@ -86,9 +86,9 @@
"achievements": {
"d1_minigame_solver": 20,
"a1_first_deduction": 1,
"a5_truth_hunter": 1
"a5_truth_hunter": 2
},
"lobbyColorThemeIndex": 3,
"lobbyColorThemeIndex": 5,
"lobbySkinToneIndex": 1
},
{
@@ -196,31 +196,36 @@
"providerUserId": "p_1780400804441_lsc3lqqvs0i",
"notes": "auto: player-coins",
"blocked": false,
"coins": 16,
"coins": 57,
"createdAt": "2026-06-02T11:46:46+00:00",
"updatedAt": "2026-06-19T07:10:26+00:00",
"updatedAt": "2026-06-23T06:29:36+00:00",
"daily": {
"anchorMs": 1781802000000,
"claimedDays": [
true,
false,
true,
false,
false,
false,
false,
false
],
"lockUntilMs": 1781888400000
"lockUntilMs": 1782234000000
},
"score": 6,
"score": 32,
"scoreByCase": {
"1": 4,
"7": 2
"7": 2,
"10": 26
},
"lbName": "Pear",
"achievements": {
"d1_minigame_solver": 2
}
"d1_minigame_solver": 5,
"a1_first_deduction": 1,
"a5_truth_hunter": 1
},
"lobbyColorThemeIndex": 8,
"lobbySkinToneIndex": 1
},
{
"id": "9404ea306bffed187b43a490",
@@ -634,18 +639,32 @@
"providerUserId": "p_1782122326943_7p2fnvivscm",
"notes": "auto: player-coins",
"blocked": false,
"coins": 20,
"coins": 48,
"createdAt": "2026-06-22T09:58:46+00:00",
"updatedAt": "2026-06-22T10:04:04+00:00",
"updatedAt": "2026-06-23T07:42:20+00:00",
"lobbyColorThemeIndex": 3,
"lobbySkinToneIndex": 1,
"score": 20,
"score": 38,
"scoreByCase": {
"10": 20
"10": 20,
"12": 18
},
"lbName": "MONE",
"lbName": "ผู้เล่น",
"achievements": {
"d1_minigame_solver": 1
"d1_minigame_solver": 3
},
"daily": {
"anchorMs": 1782147600000,
"claimedDays": [
true,
false,
false,
false,
false,
false,
false
],
"lockUntilMs": 1782234000000
}
},
{
@@ -699,17 +718,18 @@
"providerUserId": "p_1778097234255_htbbepc8x3m",
"notes": "auto: player-coins",
"blocked": false,
"coins": 50,
"coins": 58,
"createdAt": "2026-06-22T13:52:23+00:00",
"updatedAt": "2026-06-22T14:43:06+00:00",
"score": 50,
"updatedAt": "2026-06-23T09:46:51+00:00",
"score": 58,
"scoreByCase": {
"12": 20,
"13": 30
"13": 30,
"8": 8
},
"lbName": "ผู้เล่น",
"achievements": {
"d1_minigame_solver": 5
"d1_minigame_solver": 6
}
},
{
@@ -737,14 +757,22 @@
{
"id": "4647f699cdb5676b884008f8",
"email": "",
"displayName": "Guest",
"displayName": "ผู้เล่น",
"loginType": "guest",
"providerUserId": "p_1782187303882_g81ebi60min",
"notes": "auto: player-coins",
"blocked": false,
"coins": 0,
"coins": 40,
"createdAt": "2026-06-23T04:01:44+00:00",
"updatedAt": "2026-06-23T04:01:44+00:00"
"updatedAt": "2026-06-23T06:16:19+00:00",
"score": 30,
"scoreByCase": {
"13": 30
},
"lbName": "ผู้เล่น",
"achievements": {
"d1_minigame_solver": 3
}
}
]
}
+10 -1
View File
@@ -7,7 +7,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Kanit:wght@500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css?v=1.006">
<link rel="stylesheet" href="style.css?v=1.007">
</head>
<body>
<div class="create-bg" aria-hidden="true">
@@ -120,6 +120,15 @@
if(panel){
new MutationObserver(function(){ setTimeout(autoFit, 80); }).observe(panel, {childList:true, subtree:true, attributes:true, attributeFilter:['class']});
}
/* summary เด้ง (bump) ทุกครั้งที่ตัวเลขสรุปเปลี่ยน */
var sumEl = document.getElementById('create-summary');
if(sumEl){
new MutationObserver(function(){
sumEl.classList.remove('is-bump');
void sumEl.offsetWidth; /* reflow → restart animation */
sumEl.classList.add('is-bump');
}).observe(sumEl, {childList:true, characterData:true, subtree:true});
}
})();
</script>
</body>
+39
View File
@@ -1004,3 +1004,42 @@ body {
margin-top: 0.5cqh;
}
/* ================================================== */
/* Animations — entrance (ตอนขึ้น) / press (ตอนกด) */
/* transitions (ตอนเปลี่ยน) */
/* ================================================== */
@keyframes cr-rise { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
@keyframes cr-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes cr-char-in { from { opacity: 0; transform: translateY(4%) scale(1.05); } to { opacity: .85; transform: translateY(0) scale(1); } }
@keyframes cr-reveal { from { opacity: 0; transform: translateY(-10px) scaleY(.85); } to { opacity: 1; transform: translateY(0) scaleY(1); } }
@keyframes cr-pop { 0% { transform: scale(1); } 45% { transform: scale(1.13); } 100% { transform: scale(1); } }
@keyframes cr-bump { 0% { transform: scale(1); } 35% { transform: scale(1.05); filter: brightness(1.3); } 100% { transform: scale(1); } }
/* press feedback — เปิดเสมอ (เร็ว ไม่เวียนหัว) */
.create-ui .menu-btn-back { transition: transform .12s ease, filter .2s; }
.create-ui .menu-btn-back:hover { filter: brightness(1.15); }
.create-ui .menu-btn-back:active { transform: scale(.88); }
.create-confirm:active:not(:disabled) { transform: scale(.94); }
.create-mode-btn:active { transform: scale(.93); }
.create-num-btn:active { transform: scale(.9); }
@media (prefers-reduced-motion: no-preference) {
/* --- entrance: โผล่ทีละชิ้นแบบไล่ลำดับ --- */
.create-char-img { animation: cr-char-in 1s ease both; }
.create-ui .menu-btn-back { animation: cr-fade .6s ease .08s backwards; }
.create-title-img { animation: cr-rise .55s cubic-bezier(.2,.85,.25,1) .05s both; }
.create-title-line { animation: cr-rise .55s cubic-bezier(.2,.85,.25,1) .13s both; }
.create-field--room { animation: cr-rise .55s cubic-bezier(.2,.85,.25,1) .2s both; }
.create-field--mode { animation: cr-rise .55s cubic-bezier(.2,.85,.25,1) .28s both; }
.create-field--players { animation: cr-rise .55s cubic-bezier(.2,.85,.25,1) .36s both; }
.create-summary-wrap { animation: cr-rise .55s cubic-bezier(.2,.85,.25,1) .44s both; }
/* confirm ใช้ fade (opacity) เท่านั้น → :active scale ยังทำงาน + ไม่ทับสถานะ disabled */
.create-confirm { animation: cr-fade .6s ease .52s backwards; }
/* --- transitions: ตอนเปลี่ยนค่า/สลับ --- */
.create-private-block:not(.create-view--hidden) { animation: cr-reveal .34s cubic-bezier(.2,.85,.25,1); transform-origin: top; }
.create-mode-btn--active { animation: cr-pop .32s ease; }
.create-num-btn[aria-pressed="true"] { animation: cr-pop .32s ease; }
.create-summary.is-bump { animation: cr-bump .42s ease; }
}
File diff suppressed because one or more lines are too long
+488 -22
View File
@@ -835,6 +835,56 @@
window.__jdGameSocket = socket;
const canvas = document.getElementById('game-canvas');
const ctx = canvas.getContext('2d');
/** popup / ( alert() )
opts: { type:'error'|'info', okText, onClose } onClose เรยกเมอกดตกลง/ */
function showPlayAlert(message, opts) {
opts = opts || {};
const prev = document.getElementById('play-alert-modal');
if (prev) prev.remove();
const overlay = document.createElement('div');
overlay.id = 'play-alert-modal';
overlay.className = 'play-alert-modal' + (opts.type === 'error' ? ' play-alert-modal--error' : '');
const card = document.createElement('div');
card.className = 'play-alert-card';
card.setAttribute('role', 'alertdialog');
card.setAttribute('aria-modal', 'true');
const backdrop = document.createElement('div');
backdrop.className = 'play-alert-backdrop';
const icon = document.createElement('div');
icon.className = 'play-alert-icon';
icon.textContent = opts.type === 'error' ? '!' : 'i';
const msg = document.createElement('div');
msg.className = 'play-alert-msg';
msg.textContent = (message == null ? '' : String(message));
const ok = document.createElement('button');
ok.type = 'button';
ok.className = 'play-alert-ok';
ok.textContent = opts.okText || 'ตกลง';
card.appendChild(icon);
card.appendChild(msg);
card.appendChild(ok);
overlay.appendChild(backdrop);
overlay.appendChild(card);
document.body.appendChild(overlay);
let closed = false;
const done = function () {
if (closed) return;
closed = true;
overlay.remove();
document.removeEventListener('keydown', onKey, true);
if (typeof opts.onClose === 'function') { try { opts.onClose(); } catch (e) { /* ignore */ } }
};
const onKey = function (e) {
if (e.key === 'Enter' || e.key === 'Escape') { e.preventDefault(); done(); }
};
ok.addEventListener('click', done);
if (!opts.requireButton) backdrop.addEventListener('click', done);
document.addEventListener('keydown', onKey, true);
setTimeout(function () { try { ok.focus(); } catch (e) { /* ignore */ } }, 30);
return { close: done };
}
window.__jdShowPlayAlert = showPlayAlert;
let mapData = null, tileSize = 32, myId = null;
let stackMini = null;
let stackFall = null;
@@ -6047,6 +6097,17 @@
const mwPx = w * tileSize;
const mhPx = h * tileSize;
zDraw = Math.min(canvas.width / mwPx, canvas.height / mhPx) * 0.96;
/* MG7 (balloon_boss) fit : + (crop -)
แมป 20×15 (aspect 1.33) บนจอ 16:9 fit งแมปจะเหลอขอบดำซาย-ขวาเยอะ */
if (isBalloonBoss()) {
const win = getBalloonBossVisibleHalfTilesPlay(mapData);
if (win) {
const needWpx = win.halfW * 2 * tileSize;
const needHpx = win.halfH * 2 * tileSize;
const zContent = Math.min(canvas.width / needWpx, canvas.height / needHpx);
if (Number.isFinite(zContent) && zContent > zDraw) zDraw = zContent;
}
}
} else if (isQuizQuestionMissionHudActivePlay()) {
const qmc = getQuizQuestionMissionMapCenterWorldPxPlay();
if (qmc) {
@@ -6201,6 +6262,12 @@
return isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase === 'live';
}
/** Mega Virus mnq1eml7 (MG7) : HUD/score-row MG6 (qm-mock)
นคะแนน/อใช layout เกาท text /อน */
function isBalloonBossMissionHudActivePlay() {
return isMegaVirusMissionShellMapPlay() && balloonBossSessionStartMs > 0 && !balloonBossGameEnded;
}
function stackTowerAssetUrl(file) {
return BASE + '/img/TowerBlock/' + String(file || '').replace(/^\/+/, '');
}
@@ -13838,16 +13905,41 @@
}, 2600);
}
/** ปิด overlay + คืนเวลาเล่นต่อ (เรียกตอนกด «เล่นต่อ» หรือ safety timeout) */
function specialQuizContinueAndResume() {
/** «» ack server "" ( freeze )
server จะรวบ ack ครบทกผเลนจร แล broadcast `special-quiz-resume-all` กคนเลนตอพรอมก */
function specialQuizClickContinue() {
const btn = document.getElementById('sq-ov-award-continue');
if (btn) {
if (btn.dataset.sqWaiting === '1') return; /* กันกดซ้ำ */
btn.dataset.sqWaiting = '1';
btn.disabled = true;
btn.classList.add('is-waiting');
btn.textContent = 'รอผู้เล่นอื่น…';
}
/* แจ้ง server ว่าเรากด «เล่นต่อ» แล้ว (ack) */
try { socket.emit('special-quiz-continue', {}, function () {}); } catch (e) { /* ignore */ }
/* fallback กันค้าง: ถ้าไม่ได้รับ resume-all จาก server นาน → ปลดเอง */
clearTimeout(specialQuizClickContinue._fb);
specialQuizClickContinue._fb = setTimeout(specialQuizDoResume, 40000);
}
/** ปลด freeze จริง + ปิด overlay (เรียกเมื่อ server แจ้ง `special-quiz-resume-all` หรือ fallback/safety) */
function specialQuizDoResume() {
clearTimeout(endSpecialQuiz._safety);
endSpecialQuiz._safety = 0;
clearTimeout(specialQuizClickContinue._fb);
specialQuizClickContinue._fb = 0;
const ov = specialQuizOverlayEl();
const award = document.getElementById('sq-ov-award');
if (award) award.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 */ }
const btn = document.getElementById('sq-ov-award-continue');
if (btn) {
btn.disabled = false;
btn.classList.remove('is-waiting');
delete btn.dataset.sqWaiting;
btn.textContent = 'เล่นเกมต่อ';
}
endSpecialQuizFreeze();
}
@@ -13895,20 +13987,29 @@
if (txtEl) { txtEl.setAttribute('hidden', ''); txtEl.removeAttribute('src'); }
if (msgEl) msgEl.textContent = 'รอบนี้ไม่ได้รับการ์ดพิเศษ — ตอบถูกครบทุกคนเพื่อรับในครั้งหน้า';
}
if (btn) btn.onclick = specialQuizContinueAndResume;
if (btn) {
/* รีเซ็ตสถานะปุ่ม (เผื่อ overlay ถูกใช้ซ้ำรอบก่อน) */
btn.disabled = false;
btn.classList.remove('is-waiting');
delete btn.dataset.sqWaiting;
btn.textContent = 'เล่นเกมต่อ';
btn.onclick = specialQuizClickContinue;
}
/* สลับ 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 สำรอง — เล่นต่อทันที */
specialQuizContinueAndResume();
/* ไม่มี element สำรอง — ส่ง ack แล้วเล่นต่อทันที */
try { socket.emit('special-quiz-continue', {}, function () {}); } catch (e) { /* ignore */ }
specialQuizDoResume();
return;
}
/* กันค้าง: ถ้าไม่กดภายใน 30 วิ ให้ไปต่อเอง */
/* (fallback client): server broadcast resume-all ( safety 32s server)
งไวนานกว server เลกนอย เผ resume-all หาย */
clearTimeout(endSpecialQuiz._safety);
endSpecialQuiz._safety = setTimeout(specialQuizContinueAndResume, 30000);
endSpecialQuiz._safety = setTimeout(specialQuizDoResume, 40000);
}
function showGauntletCrownMissionOverlay(mission) {
@@ -16360,6 +16461,42 @@
md.balloonBossPlayerSlots = rows;
}
/** MG7 (tiles) = slot + + margin
ใชมใหเตมจอกว fit งแมป (แมปแคบ aspect 1.33 ขอบดำเยอะ) โดยไม crop วผเล */
let _balloonBossVisHalfCache = null;
function getBalloonBossVisibleHalfTilesPlay(md) {
if (!md || md.gameType !== 'balloon_boss') return null;
if (_balloonBossVisHalfCache && _balloonBossVisHalfCache._md === md) return _balloonBossVisHalfCache;
const w = md.width || 20, h = md.height || 15;
const ccx = w / 2, ccy = h / 2;
let maxDX = 0, maxDY = 0;
const g = md.balloonBossPlayerSlots;
if (g) {
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
if (g[y] && g[y][x]) {
maxDX = Math.max(maxDX, Math.abs((x + 0.5) - ccx));
maxDY = Math.max(maxDY, Math.abs((y + 0.5) - ccy));
}
}
}
}
const bx = md.balloonBossBossSpawn;
if (bx && Number.isFinite(Number(bx.x))) {
maxDX = Math.max(maxDX, Math.abs((Number(bx.x) + 0.5) - ccx));
maxDY = Math.max(maxDY, Math.abs((Number(bx.y) + 0.5) - ccy) + 2.5 /* บอสลอยขึ้นได้ */);
}
if (maxDX <= 0 && maxDY <= 0) { maxDX = w / 2; maxDY = h / 2; }
const margin = 1.6; /* เผื่อขนาดตัวละคร + ลูกศร/ลูกโป่ง */
const res = {
_md: md,
halfW: Math.min(w / 2, maxDX + margin),
halfH: Math.min(h / 2, maxDY + margin),
};
_balloonBossVisHalfCache = res;
return res;
}
function getBalloonBossPlayerSpawnWorldCenterFromMap(md, slot1to6, ts) {
if (!md || !md.balloonBossPlayerSlots) return null;
const w = md.width || 20, h = md.height || 15;
@@ -17133,6 +17270,9 @@
const syncRefs = buildBalloonBossParticipantRefsPlay();
socket.emit('balloon-boss-sync', {
bossDmg: Math.max(0, balloonBossTeamBossDamagePlay() | 0),
/* anchor (server ms) clients adopt ""
(เด anchor fallback = serverNow() ตอนเขาเล างกนตอเครอง วงๆ ไม sync) */
anchor: balloonBossLiveAnchorServerMs > 0 ? balloonBossLiveAnchorServerMs : 0,
players: syncRefs.map((e) => ({
id: e.id,
d: Math.max(0, (e.ref && (e.ref.balloonBossBossDmg | 0)) || 0),
@@ -17923,11 +18063,26 @@
onGround = false;
}
/* / (object wall ) "" (head crush)
ตามคำขอ: ชนดานบนไมตาย แคหยดการพงข นทะลเพดาน (ไม died)
หมายเหต: การชนเพดานตอนกระโดดถกบลอก vy vy<0 านบนแล นนนกรณ scroll นชนเพดาน */
headTop = feetY - bh;
const headBandBottom = headTop + bh * 0.42;
if (jumpSurviveOverlapsSolid(md, pxc - bw, headTop, pxc + bw, feetY) &&
if (vy < 0 &&
jumpSurviveOverlapsSolid(md, pxc - bw, headTop, pxc + bw, feetY) &&
jumpSurviveOverlapsObjectWall(md, pxc - bw, headTop, pxc + bw, headBandBottom)) {
return { pxc, feetY, vy, onGround, died: true };
vy = 0;
}
/* (objects 0 1 ) (tunneling)
clamp วใหอย "ใต้เพดาน" (headTop>=ts) าม clamp y=0 (=ในเพดาน) เพราะ landing branch จะดนหวขนพนเพดาน
แล clamp นลง นคางถาวร (เพดานเป object ตร ไมเลอนลงตาม). ใตเพดาน standing branch จะพาเลอนลงเอง
ชนเพดานแลวหย+ตกลง ไมตาย ไมดคาง ไมหลดนอกฉาก */
headTop = feetY - bh;
if (headTop < ts) {
feetY = ts + bh;
if (vy < 0) vy = 0;
headTop = feetY - bh;
}
headTop = feetY - bh;
@@ -17936,9 +18091,9 @@
}
headTop = feetY - bh;
/* ไม่ใช้ขอบกล้อง (cam topKill/botKill) — ตายเฉพาะ hazard ที่วาง + ตกออกนอกโลกแมปจริง (ไม่ตายในอากาศระหว่างแพลตฟอร์ม) */
/* ตายเฉพาะ hazard ที่วาง + ตกหลุดออก "ด้านล่าง" แมป (ขอบบน clamp ไว้แล้ว ไม่ตาย) */
const mapHpx = h * ts;
if (feetY > mapHpx + ts * 8 || headTop < -ts * 32) {
if (feetY > mapHpx + ts * 8) {
return { pxc, feetY, vy, onGround, died: true };
}
@@ -19042,13 +19197,18 @@
}, (res) => {
if (!res || !res.ok) {
const errMsg = (res && res.error) || 'เข้าร่วมไม่ได้';
alert(errMsg);
const caseLocked = /เริ่มคดี|ไม่รับผู้เล่น/.test(errMsg);
if (caseLocked) {
window.location.replace(buildRoomLobbyReturnHref());
} else {
window.location.replace(playDesignMainMenuHref());
}
showPlayAlert(errMsg, {
type: 'error',
requireButton: true,
onClose: function () {
if (caseLocked) {
window.location.replace(buildRoomLobbyReturnHref());
} else {
window.location.replace(playDesignMainMenuHref());
}
},
});
return;
}
myId = socket.id;
@@ -19493,6 +19653,11 @@
if (isMePlayHost()) return;
const bd = Number(d.bossDmg);
if (Number.isFinite(bd)) balloonBossHostBossDmg = Math.max(0, bd);
/* adopt anchor ของ host → มุมลูกศรวงกลม (balloonBossAutoAimRadPlay) คำนวณตรงกับ host ทุกเครื่อง */
const anc = Number(d.anchor);
if (Number.isFinite(anc) && anc > 0 && Math.abs((balloonBossLiveAnchorServerMs || 0) - anc) > 1) {
balloonBossLiveAnchorServerMs = anc;
}
if (Array.isArray(d.players)) {
d.players.forEach((p) => {
if (!p || p.id == null) return;
@@ -19692,6 +19857,292 @@
box.scrollTop = 1e9;
});
/* ============================================================
ไมคในเกม (MG1-7) WebRTC mesh เหมอนหน lobby
- แยกเป IIFE สระ: ไมแตะ logic เกมเด (เพ socket listener เสรมเทาน
socket.io รองรบหลาย listener event), ไม hook ใน draw/update ของเกม
- server รองร voice events space อยแล
(voice-state / voice-activity / webrtc-signal / peer-voice-state / peer-speaking)
- ใชวแปรเกม: socket, others(Map peers), BASE, isPreviewBotId, previewMode
- offer จากเพอนไดนเสยงแบบ passive แมงไมเปดไมคเอง
============================================================ */
(function setupPlayVoiceChat() {
var btnVoice = document.getElementById('btn-voice');
if (previewMode) { if (btnVoice) btnVoice.style.display = 'none'; return; }
var localStream = null;
var voiceActivityInterval = null;
var voiceAnalyserContext = null;
var voiceAnalyser = null;
var peerConnections = {};
var remoteAudios = {};
var iceQueue = {};
var audioUnlocked = false;
var cachedIceServers = null;
var defaultIceServers = [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' }];
/* เพื่อนที่ต่อสายเสียงได้ = ไม่ใช่ตัวเอง และไม่ใช่บอท */
function isVoicePeerId(id) {
return id != null && String(id) !== String(socket.id) && !isPreviewBotId(id);
}
function unlockAudio() {
if (audioUnlocked) return;
try {
var a = new Audio(); a.volume = 0;
a.play().then(function () { audioUnlocked = true; playAllRemoteAudios(); }).catch(function () {});
} catch (e) { /* ignore */ }
}
function playAllRemoteAudios() {
Object.keys(remoteAudios).forEach(function (pid) { tryPlayPeer(pid); });
}
function tryPlayPeer(pid) {
var el = remoteAudios[pid];
if (el && el.srcObject) el.play().catch(function () {});
}
function addRemoteAudio(pid, stream) {
if (!stream || !stream.getTracks().length) return;
unlockAudio();
if (remoteAudios[pid]) { remoteAudios[pid].srcObject = stream; tryPlayPeer(pid); return; }
var el = document.createElement('audio');
el.autoplay = true; el.setAttribute('playsinline', ''); el.volume = 1;
el.srcObject = stream;
el.style.cssText = 'position:absolute;width:0;height:0;opacity:0;pointer-events:none;';
remoteAudios[pid] = el;
document.body.appendChild(el);
el.onloadedmetadata = function () { tryPlayPeer(pid); };
el.oncanplay = function () { tryPlayPeer(pid); };
tryPlayPeer(pid);
}
function getIceServers() {
if (cachedIceServers) return Promise.resolve(cachedIceServers);
return fetch(BASE + '/api/ice-servers')
.then(function (r) { return r.json(); })
.then(function (d) {
cachedIceServers = (d && d.iceServers && d.iceServers.length) ? d.iceServers : defaultIceServers;
return cachedIceServers;
})
.catch(function () { cachedIceServers = defaultIceServers; return cachedIceServers; });
}
function createPeerConnection(pid, fromOffer) {
if (peerConnections[pid]) return Promise.resolve(peerConnections[pid]);
return getIceServers().then(function (iceServers) {
if (peerConnections[pid]) return peerConnections[pid];
var pc = new RTCPeerConnection({ iceServers: iceServers });
pc._isInitiator = !fromOffer;
if (localStream) localStream.getTracks().forEach(function (t) { pc.addTrack(t, localStream); });
pc.ontrack = function (e) {
if (e.track.kind !== 'audio') return;
e.track.enabled = true;
var stream = (e.streams && e.streams[0]) ? e.streams[0] : new MediaStream([e.track]);
addRemoteAudio(pid, stream);
};
pc.onicecandidate = function (e) {
if (e.candidate) socket.emit('webrtc-signal', { to: pid, type: 'ice', candidate: e.candidate });
};
pc.oniceconnectionstatechange = function () {
if (pc.iceConnectionState === 'connected' || pc.iceConnectionState === 'completed') tryPlayPeer(pid);
};
pc.onnegotiationneeded = function () {
if (!pc._isInitiator) return;
pc.createOffer().then(function (offer) {
return pc.setLocalDescription(offer).then(function () {
socket.emit('webrtc-signal', { to: pid, type: 'offer', sdp: { type: offer.type, sdp: offer.sdp } });
});
}).catch(function (err) { console.warn('voice offer', err); });
};
peerConnections[pid] = pc;
iceQueue[pid] = [];
return pc;
});
}
function drainIceQueue(pid) {
var pc = peerConnections[pid]; var q = iceQueue[pid];
if (!pc || !q || !q.length) return Promise.resolve();
var chain = Promise.resolve();
while (q.length) {
(function (c) {
chain = chain.then(function () {
return pc.addIceCandidate(new RTCIceCandidate(c)).catch(function (e) { console.warn('addIce', e); });
});
})(q.shift());
}
return chain;
}
function closePeer(pid) {
var pc = peerConnections[pid];
if (pc) { try { pc.close(); } catch (e) {} delete peerConnections[pid]; }
var el = remoteAudios[pid];
if (el) { el.srcObject = null; el.remove(); delete remoteAudios[pid]; }
delete iceQueue[pid];
}
socket.on('webrtc-signal', function (data) {
var from = data && data.from, type = data && data.type, sdp = data && data.sdp, candidate = data && data.candidate;
if (!from || from === socket.id) return;
var sdpObj = (sdp && sdp.sdp !== undefined) ? sdp : (sdp ? { type: sdp.type, sdp: sdp.sdp || '' } : null);
Promise.resolve().then(function () {
if (type === 'offer' && sdpObj) {
return createPeerConnection(from, true).then(function (pc) {
return pc.setRemoteDescription(new RTCSessionDescription(sdpObj))
.then(function () { return pc.createAnswer(); })
.then(function (answer) {
return pc.setLocalDescription(answer).then(function () {
socket.emit('webrtc-signal', { to: from, type: 'answer', sdp: { type: answer.type, sdp: answer.sdp } });
});
})
.then(function () { return drainIceQueue(from); });
});
} else if (type === 'answer' && sdpObj) {
var pcA = peerConnections[from];
if (pcA) return pcA.setRemoteDescription(new RTCSessionDescription(sdpObj)).then(function () { return drainIceQueue(from); });
} else if (type === 'ice' && candidate) {
var pcI = peerConnections[from];
if (pcI) {
if (pcI.remoteDescription) {
return pcI.addIceCandidate(new RTCIceCandidate(candidate)).catch(function () { (iceQueue[from] = iceQueue[from] || []).push(candidate); });
}
(iceQueue[from] = iceQueue[from] || []).push(candidate);
}
}
}).catch(function (err) { console.warn('voice signal', err); });
});
socket.on('peer-voice-state', function (d) {
if (!d) return; var p = others.get(d.id); if (p) p.voiceMicOn = d.micOn !== false;
});
socket.on('peer-speaking', function (d) {
if (!d) return; var p = others.get(d.id);
if (p) { p.speakingUntil = d.until; p.speakingLevel = d.level; }
});
/* peer ใหม่เข้าระหว่างเราเปิดไมค์ → ต่อสายเสียงให้ (listener เสริม ไม่แตะ handler เกม) */
socket.on('user-joined', function (data) {
if (!data || !isVoicePeerId(data.id)) return;
if (localStream) createPeerConnection(data.id);
});
socket.on('user-left', function (data) {
if (data && data.id != null) closePeer(data.id);
});
function startVoiceActivityDetection(stream) {
if (!stream || !stream.getTracks().length) return;
try {
var ctx = new (window.AudioContext || window.webkitAudioContext)();
if (ctx.state === 'suspended') ctx.resume().catch(function () {});
var src = ctx.createMediaStreamSource(stream);
var analyser = ctx.createAnalyser();
analyser.fftSize = 256; analyser.smoothingTimeConstant = 0.5;
src.connect(analyser);
voiceAnalyserContext = ctx; voiceAnalyser = analyser;
var dataArray = new Uint8Array(analyser.frequencyBinCount);
var lastEmit = 0;
voiceActivityInterval = setInterval(function () {
if (!voiceAnalyser || !stream.active) return;
voiceAnalyser.getByteFrequencyData(dataArray);
var sum = 0; for (var i = 0; i < dataArray.length; i++) sum += dataArray[i];
var level = Math.min(1, (sum / dataArray.length) / 60);
if (level > 0.08 && Date.now() - lastEmit > 80) { lastEmit = Date.now(); socket.emit('voice-activity', { level: level }); }
}, 100);
} catch (e) { /* ignore */ }
}
function stopVoiceActivityDetection() {
if (voiceActivityInterval) { clearInterval(voiceActivityInterval); voiceActivityInterval = null; }
if (voiceAnalyserContext) { try { voiceAnalyserContext.close(); } catch (e) {} voiceAnalyserContext = null; }
voiceAnalyser = null;
}
function setMicIcon(on) {
if (!btnVoice) return;
var img = btnVoice.querySelector('#btn-voice-icon-img') || btnVoice.querySelector('img');
if (img) { img.src = BASE + (on ? '/img/btn-mic-on.png' : '/img/btn-mic-mute.png'); img.alt = on ? 'ปิดไมค์' : 'เปิดไมค์'; }
btnVoice.classList.toggle('is-mic-on', !!on);
btnVoice.title = on ? 'ปิดไมค์' : 'เปิดไมค์';
}
function enableMic() {
var audioConstraints = { audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } };
return navigator.mediaDevices.getUserMedia(audioConstraints).then(function (stream) {
localStream = stream;
startVoiceActivityDetection(localStream);
setMicIcon(true);
socket.emit('voice-state', { micOn: true });
var ids = [];
others.forEach(function (_, id) { if (isVoicePeerId(id)) ids.push(id); });
return ids.reduce(function (chain, pid) {
return chain.then(function () {
return createPeerConnection(pid).then(function (pc) {
if (!localStream || !pc) return;
var senders = pc.getSenders();
var added = false;
localStream.getTracks().forEach(function (track) {
if (!senders.find(function (s) { return s.track === track; })) { pc.addTrack(track, localStream); added = true; }
});
if (added) {
return pc.createOffer().then(function (offer) {
return pc.setLocalDescription(offer).then(function () {
socket.emit('webrtc-signal', { to: pid, type: 'offer', sdp: { type: offer.type, sdp: offer.sdp } });
});
}).catch(function (e) { console.warn('voice renegotiate', e); });
}
});
});
}, Promise.resolve());
});
}
function disableMic() {
stopVoiceActivityDetection();
Object.keys(peerConnections).forEach(function (pid) {
var pc = peerConnections[pid];
if (pc && pc.getSenders) pc.getSenders().forEach(function (s) { try { pc.removeTrack(s); } catch (e) {} });
});
if (localStream) { localStream.getTracks().forEach(function (t) { t.stop(); }); localStream = null; }
socket.emit('voice-state', { micOn: false });
setMicIcon(false);
}
if (btnVoice) {
btnVoice.style.display = '';
setMicIcon(false);
btnVoice.addEventListener('click', function () {
unlockAudio();
if (localStream) { disableMic(); return; }
btnVoice.disabled = true;
enableMic().catch(function (err) {
showPlayAlert('ไม่สามารถเปิดไมค์ได้: ' + (err && (err.message || err)), { type: 'error' });
try { disableMic(); } catch (e) {}
}).then(function () { btnVoice.disabled = false; });
});
}
/* Quiz Battle (qbroom*, 50 ) voice mesh P2P +
ตรวจจาก #qb-game-hud (โชวเฉพาะ quiz_battle); ไมแตะ logic เกม */
var qbHud = document.getElementById('qb-game-hud');
if (qbHud && btnVoice) {
var syncQbVoiceHide = function () {
var qbActive = !qbHud.classList.contains('is-hidden');
if (qbActive) {
if (localStream) { try { disableMic(); } catch (e) {} }
btnVoice.style.display = 'none';
} else {
btnVoice.style.display = '';
}
};
try {
var qbMo = new MutationObserver(syncQbVoiceHide);
qbMo.observe(qbHud, { attributes: true, attributeFilter: ['class'] });
} catch (e) { /* ignore */ }
syncQbVoiceHide();
}
/* ปลดล็อก autoplay เสียงเพื่อนตั้งแต่คลิก/แตะแรก (ได้ยินคนอื่นแม้ไม่เปิดไมค์เอง) */
var unlockOnce = function () { unlockAudio(); document.body.removeEventListener('click', unlockOnce); document.body.removeEventListener('touchstart', unlockOnce); };
document.body.addEventListener('click', unlockOnce, { once: true });
document.body.addEventListener('touchstart', unlockOnce, { once: true });
})();
socket.on('quiz-phase', (p) => {
if (previewMode) return;
if (!p || !mapData || !isQuiz()) return;
@@ -19819,6 +20270,20 @@
if (previewMode) return;
endSpecialQuiz(data);
});
/* server แจ้งว่าทุกคนกด «เล่นต่อ» ครบแล้ว (หรือ safety timeout) → ทุกคนเล่นต่อพร้อมกัน */
socket.on('special-quiz-resume-all', () => {
if (previewMode) return;
specialQuizDoResume();
});
/* อัปเดตจำนวนคนที่กด «เล่นต่อ» แล้ว (X/Y) บนปุ่มระหว่างรอ */
socket.on('special-quiz-continue-progress', (d) => {
if (previewMode) return;
const btn = document.getElementById('sq-ov-award-continue');
if (!btn || btn.dataset.sqWaiting !== '1') return;
const ready = (d && Number(d.ready)) || 0;
const total = (d && Number(d.total)) || 0;
if (total > 0) btn.textContent = 'รอผู้เล่นอื่น… (' + ready + '/' + total + ')';
});
socket.on('special-card-applied', (data) => {
if (previewMode || !data || !data.card) return;
applySpecialCardClient(data);
@@ -22232,7 +22697,7 @@
root.classList.toggle(
'play-cyber-hud--question-mission',
!!(isQuizQuestionMissionHudActivePlay() || isStackTowerMissionHudActivePlay() || isJumpSurviveMissionHudActivePlay()
|| isSpaceShooterMissionHudActivePlay()),
|| isSpaceShooterMissionHudActivePlay() || isBalloonBossMissionHudActivePlay()),
);
/* Minigame 17 mission — HUD fix 1920×1080 ตาม mock (score-bar / profile qb-room) */
const missionMockOn = !!isMissionMockHudPlay();
@@ -22551,8 +23016,9 @@
const stackMissionHud = isStackTowerMissionHudActivePlay();
const jumpMissionLiveHud = isJumpSurviveMissionHudActivePlay();
const spaceMissionLiveHud = isSpaceShooterMissionHudActivePlay();
/** แถบ SCORE แบบ mock mng8a80o (แนวนอน อวาตาร์+ชื่อ | คะแนน) — ภารกิจคำถาม + Stack Tower + Jumper mnptfts2 + Space mnpz6rkp */
const cyberQmMockHud = quizMissionHud || stackMissionHud || jumpMissionLiveHud || spaceMissionLiveHud;
const balloonMissionLiveHud = isBalloonBossMissionHudActivePlay();
/** แถบ SCORE แบบ mock mng8a80o (แนวนอน อวาตาร์+ชื่อ | คะแนน) — ภารกิจคำถาม + Stack Tower + Jumper mnptfts2 + Space mnpz6rkp + MegaVirus mnq1eml7 */
const cyberQmMockHud = quizMissionHud || stackMissionHud || jumpMissionLiveHud || spaceMissionLiveHud || balloonMissionLiveHud;
/** mission mock HUD — layout แถว: [avatar | ชื่อบน/คะแนนล่าง] (meta column) */
const tfMockHud = isMissionMockHudPlay();
const stackTeamPts = Math.max(0, Number(stackMini && stackMini.score) || 0);
+75 -1
View File
@@ -5331,6 +5331,77 @@
'#trial-final-disruptor img.av{width:100%;height:auto}';
document.head.appendChild(st);
}
/* ===== "" Main-Lobby =====
mock ตามดไซน (งไม asset) ใช CSS approximate + วละครเทพความร AI-chat-char.png */
function injectCyberEduStyle() {
if (document.getElementById('cyber-edu-style')) return;
var st = document.createElement('style');
st.id = 'cyber-edu-style';
st.textContent =
'#cyber-edu{position:fixed;inset:0;z-index:9600;display:flex;align-items:center;justify-content:center;padding:2vmin;box-sizing:border-box;font-family:Kanit,"NotoSansThai",system-ui,sans-serif;' +
'background:radial-gradient(circle at 28% 38%,rgba(20,60,110,.55),rgba(4,6,14,.96) 68%),repeating-linear-gradient(115deg,rgba(0,200,255,.05) 0 2px,transparent 2px 60px),#04060c}' +
'#cyber-edu.is-hidden{display:none}' +
'#cyber-edu .cedu-stage{display:flex;align-items:center;justify-content:center;gap:min(3vw,40px);width:100%;max-width:1180px;max-height:96vh}' +
'#cyber-edu .cedu-char-col{position:relative;flex:0 0 auto;display:flex;flex-direction:column;align-items:center;align-self:flex-end}' +
'#cyber-edu .cedu-char{width:min(30vw,360px);height:auto;filter:drop-shadow(0 10px 30px rgba(0,180,255,.35))}' +
'#cyber-edu .cedu-bubble{position:relative;margin-bottom:10px;padding:10px 18px;border-radius:16px;background:#eaf6ff;color:#0c3a5e;font-weight:800;font-size:clamp(14px,1.7vw,22px);box-shadow:0 4px 16px rgba(0,0,0,.4);white-space:nowrap}' +
'#cyber-edu .cedu-bubble::after{content:"";position:absolute;left:34px;bottom:-12px;border:8px solid transparent;border-top-color:#eaf6ff;border-bottom:0}' +
'#cyber-edu .cedu-main{flex:1 1 auto;min-width:0;max-width:640px;display:flex;flex-direction:column;align-items:center;gap:min(2.4vh,22px)}' +
'#cyber-edu .cedu-panel{position:relative;width:100%;box-sizing:border-box;padding:clamp(18px,3vw,34px) clamp(16px,3vw,38px);text-align:center;' +
'background:linear-gradient(160deg,rgba(10,28,54,.94),rgba(6,14,32,.96));border:2px solid rgba(94,234,255,.6);border-radius:18px;' +
'box-shadow:0 0 0 1px rgba(94,234,255,.15) inset,0 0 34px rgba(0,200,255,.18),0 18px 50px rgba(0,0,0,.55)}' +
'#cyber-edu .cedu-title{display:flex;align-items:center;justify-content:center;gap:10px;margin:0 0 16px;color:#7df0ff;font-weight:800;font-size:clamp(17px,2.2vw,28px);text-shadow:0 0 14px rgba(94,234,255,.5)}' +
'#cyber-edu .cedu-hacker{display:flex;align-items:center;justify-content:center;gap:14px;height:clamp(120px,20vh,200px);margin:0 0 16px;border-radius:12px;' +
'background:radial-gradient(circle at 60% 50%,rgba(255,40,60,.28),rgba(8,10,22,.9));border:1px solid rgba(255,80,100,.4);font-size:clamp(48px,9vh,90px);filter:drop-shadow(0 0 18px rgba(255,60,80,.4))}' +
'#cyber-edu .cedu-lead{color:#ffd54a;font-weight:800;font-size:clamp(16px,2.1vw,26px);margin:0 0 10px;text-shadow:0 1px 8px rgba(0,0,0,.6)}' +
'#cyber-edu .cedu-body{color:#e6f2ff;font-size:clamp(13px,1.6vw,19px);line-height:1.6;margin:0 0 12px}' +
'#cyber-edu .cedu-penalty{display:flex;align-items:center;justify-content:center;gap:8px;color:#ffd54a;font-weight:800;font-size:clamp(13px,1.7vw,20px);line-height:1.5;margin:0 0 14px}' +
'#cyber-edu .cedu-link{display:inline-block;color:#9be7ff;font-weight:700;font-size:clamp(13px,1.5vw,18px);text-decoration:underline;text-underline-offset:3px;cursor:pointer}' +
'#cyber-edu .cedu-ok{padding:14px 40px;border-radius:12px;cursor:pointer;font-family:inherit;font-weight:800;font-size:clamp(16px,2vw,24px);letter-spacing:.5px;' +
'color:#3a2c05;background:linear-gradient(180deg,#ffe27a,#f3b51e);border:3px solid #ffefb0;box-shadow:0 6px 20px rgba(243,181,30,.45),0 0 0 1px rgba(0,0,0,.2);transition:transform .12s ease,filter .12s ease}' +
'#cyber-edu .cedu-ok:hover{filter:brightness(1.07)}#cyber-edu .cedu-ok:active{transform:scale(.96)}' +
'@media(max-width:780px){#cyber-edu .cedu-stage{flex-direction:column;gap:12px}#cyber-edu .cedu-char-col{align-self:center}#cyber-edu .cedu-char{width:min(40vw,180px)}}';
document.head.appendChild(st);
}
function showCyberAwarenessInterstitial(onContinue) {
injectCyberEduStyle();
var ov = document.getElementById('cyber-edu');
if (!ov) {
ov = document.createElement('div');
ov.id = 'cyber-edu';
ov.className = 'is-hidden';
ov.setAttribute('role', 'dialog');
ov.setAttribute('aria-modal', 'true');
ov.innerHTML =
'<div class="cedu-stage">' +
'<div class="cedu-char-col">' +
'<div class="cedu-bubble">เรื่องนี้สำคัญนะ!</div>' +
'<img class="cedu-char" src="/Main-Lobby/IMAGE/AI-chat-char.png" alt="" decoding="async">' +
'</div>' +
'<div class="cedu-main">' +
'<div class="cedu-panel">' +
'<div class="cedu-title">⚠️ รู้ทันภัยไซเบอร์: การเข้าถึงระบบโดยมิชอบ</div>' +
'<div class="cedu-hacker" aria-hidden="true">🖥️ 🕵️‍♂️🔒</div>' +
'<div class="cedu-lead">แค่เดารหัสผ่านเล่นๆ ก็ผิดกฎหมาย!</div>' +
'<p class="cedu-body">ตาม พ.ร.บ. คอมพิวเตอร์ฯ การแอบ Log-in เข้าใช้งานระบบ<br>หรือข้อมูลของผู้อื่นโดยไม่ได้รับอนุญาต (แม้จะไม่ได้ขโมยเงิน)</p>' +
'<div class="cedu-penalty">⚠️ มีโทษจำคุกสูงสุด 2 ปี หรือปรับไม่เกิน 40,000 บาท หรือทั้งจำทั้งปรับ!</div>' +
'<a class="cedu-link" href="#" role="button">ดูข้อมูลกฎหมายเพิ่มเติม &gt;</a>' +
'</div>' +
'<button type="button" id="cedu-ok" class="cedu-ok">🛡 เข้าใจแล้ว!</button>' +
'</div>' +
'</div>';
document.body.appendChild(ov);
var lk = ov.querySelector('.cedu-link');
if (lk) lk.addEventListener('click', function (e) { e.preventDefault(); }); /* mock — ยังไม่มีหน้ากฎหมาย */
}
var okBtn = ov.querySelector('#cedu-ok');
if (okBtn) okBtn.onclick = function () {
ov.classList.add('is-hidden');
if (typeof onContinue === 'function') onContinue();
};
ov.classList.remove('is-hidden');
}
function showFinalResult(correct, culprit, anyVotes) {
injectFinalResultStyle();
var R = '/Main-Lobby/IMAGE/Result/';
@@ -5342,7 +5413,10 @@
ov.innerHTML = '<img id="trial-final-culprit" alt="คนร้าย"><img id="trial-final-txt" alt="ภารกิจล้มเหลว">' +
'<button type="button" id="trial-final-home" aria-label="กลับหน้าหลัก">กลับหน้าหลัก</button>';
document.body.appendChild(ov);
ov.querySelector('#trial-final-home').addEventListener('click', function () { window.location.href = '/Main-Lobby/'; });
ov.querySelector('#trial-final-home').addEventListener('click', function () {
/* โชว์หน้าให้ความรู้ "รู้ทันภัยไซเบอร์" ก่อน แล้วค่อยกลับ Main-Lobby */
showCyberAwarenessInterstitial(function () { window.location.href = '/Main-Lobby/'; });
});
}
var culpEl = ov.querySelector('#trial-final-culprit');
var txtEl = ov.querySelector('#trial-final-txt');
+167 -1
View File
@@ -1882,6 +1882,168 @@
overflow: visible;
min-height: unset;
}
/* ไมค์ในเกม (ทุก MG1-7) — child ใน .play-cyber-self วางทับพิกัดไมค์ตามดีไซน์เป๊ะ
quiz-tf-mock: stage 1920×1080 scale ด้วย --qtf-scale, self = absolute(1729,21) display:block
→ ปุ่มที่ absolute (67,184) ขนาด 121 = stage (1796,205) = .mg-mic ใต้ host (1729,21) */
#btn-voice.play-voice-btn {
box-sizing: border-box;
pointer-events: auto;
padding: 0;
margin: 0;
border: none;
background: transparent;
border-radius: 50%;
cursor: pointer;
opacity: 0.95;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
transition: transform 0.14s ease, filter 0.14s ease, opacity 0.15s ease;
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.5));
/* fallback (ไม่ใช่ mission-mock) — flow ใต้ avatar */
position: relative;
width: clamp(46px, 7.6vmin, 60px);
height: clamp(46px, 7.6vmin, 60px);
}
/* ทุก mission-mock (MG1-7) = quiz-tf-mock → พิกัดดีไซน์เดียวกับไมค์ตกแต่งเดิม */
#play-cyber-hud.play-cyber-hud--quiz-tf-mock #btn-voice.play-voice-btn {
position: absolute;
left: 67px;
top: 184px;
width: 121px;
height: 121px;
margin: 0;
}
#btn-voice.play-voice-btn:hover { opacity: 1; }
#btn-voice.play-voice-btn:active { transform: scale(0.94); }
#btn-voice.play-voice-btn.is-mic-on {
opacity: 1;
filter: drop-shadow(0 0 12px rgba(94, 234, 212, 0.7));
}
#btn-voice.play-voice-btn img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
pointer-events: none;
user-select: none;
}
/* ไมค์ตกแต่งเดิม (.play-cyber-mic-img) — ซ่อน กันซ้ำกับปุ่มจริงที่ทับตำแหน่งเดียวกัน */
#play-cyber-hud.play-cyber-hud--quiz-tf-mock .play-cyber-mic-img {
display: none !important;
}
/* MG3 stack ซ่อน .play-cyber-self ทั้งก้อน (ใช้ #mg3-tower-hud แทน) — แต่ยังต้องมีไมค์
→ คง self (display:block ที่ left:1729/top:21) เฉพาะปุ่มไมค์ ซ่อน avatar/status เดิม */
html #play-cyber-hud.play-cyber-hud--stack-tower-canvas-hud .play-cyber-self {
display: block !important;
z-index: 9;
pointer-events: none;
}
html #play-cyber-hud.play-cyber-hud--stack-tower-canvas-hud .play-cyber-self > *:not(#btn-voice) {
display: none !important;
}
/* popup แจ้งเตือน/ผิดพลาด ดีไซน์เอง (แทน alert() เบราว์เซอร์) — ธีม cyber เข้ากับเกม */
.play-alert-modal {
position: fixed;
inset: 0;
z-index: 11000;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
box-sizing: border-box;
font-family: 'Kanit', 'NotoSansThai', system-ui, sans-serif;
}
.play-alert-backdrop {
position: absolute;
inset: 0;
background: rgba(4, 7, 16, 0.72);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
animation: playAlertFade 0.18s ease both;
}
.play-alert-card {
position: relative;
z-index: 1;
width: min(92vw, 384px);
box-sizing: border-box;
padding: 26px 22px 20px;
text-align: center;
background: linear-gradient(165deg, rgba(16, 22, 42, 0.98), rgba(9, 12, 26, 0.98));
border: 1.5px solid rgba(94, 234, 255, 0.5);
border-radius: 16px;
box-shadow:
0 18px 60px rgba(0, 0, 0, 0.6),
0 0 28px rgba(0, 200, 255, 0.12),
inset 0 0 0 1px rgba(255, 255, 255, 0.04);
animation: playAlertPop 0.24s cubic-bezier(0.2, 0.85, 0.3, 1.25) both;
}
@keyframes playAlertFade { from { opacity: 0; } to { opacity: 1; } }
@keyframes playAlertPop {
0% { transform: scale(0.84) translateY(8px); opacity: 0; }
100% { transform: scale(1) translateY(0); opacity: 1; }
}
.play-alert-icon {
width: 54px;
height: 54px;
margin: 0 auto 14px;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Orbitron', sans-serif;
font-size: 30px;
font-weight: 800;
line-height: 1;
border-radius: 50%;
color: #5eeaff;
background: rgba(94, 234, 255, 0.12);
border: 1.5px solid rgba(94, 234, 255, 0.5);
box-shadow: 0 0 18px rgba(94, 234, 255, 0.25);
}
.play-alert-msg {
color: #e8f3ff;
font-size: 1rem;
line-height: 1.5;
font-weight: 500;
margin-bottom: 20px;
white-space: pre-line;
word-break: break-word;
}
.play-alert-ok {
min-width: 128px;
padding: 10px 28px;
font-family: inherit;
font-size: 0.96rem;
font-weight: 700;
letter-spacing: 0.02em;
color: #04263a;
cursor: pointer;
background: linear-gradient(180deg, #7df0ff, #2bc4e6);
border: none;
border-radius: 10px;
box-shadow: 0 6px 18px rgba(43, 196, 230, 0.35);
transition: transform 0.12s ease, filter 0.12s ease;
}
.play-alert-ok:hover { filter: brightness(1.08); }
.play-alert-ok:active { transform: scale(0.96); }
/* ชนิด error — โทนแดง */
.play-alert-modal--error .play-alert-card {
border-color: rgba(255, 90, 110, 0.55);
box-shadow:
0 18px 60px rgba(0, 0, 0, 0.6),
0 0 28px rgba(255, 60, 80, 0.18),
inset 0 0 0 1px rgba(255, 255, 255, 0.04);
}
.play-alert-modal--error .play-alert-icon {
color: #ff7088;
background: rgba(255, 90, 110, 0.12);
border-color: rgba(255, 90, 110, 0.55);
box-shadow: 0 0 18px rgba(255, 80, 100, 0.3);
}
.play-alert-modal--error .play-alert-ok {
color: #2a0a10;
background: linear-gradient(180deg, #ff8a9c, #ef4d62);
box-shadow: 0 6px 18px rgba(239, 77, 98, 0.35);
}
/* mng8a80o — virtual joystick (Roblox-style) มุมซ้ายล่าง */
.quiz-q-mission-joystick {
position: fixed;
@@ -4792,6 +4954,10 @@
</div>
</div>
<img id="play-cyber-mic-img" class="play-cyber-mic-img is-hidden" alt="" decoding="async" aria-hidden="true" />
<!-- ไมค์ในเกม (MG1-7) — WebRTC mesh เหมือนหน้า lobby; วางใต้ avatar ตามดีไซน์ (mg-mic). play.js เปิดแสดง (ซ่อน preview/Quiz Battle) -->
<button type="button" id="btn-voice" class="play-voice-btn" title="เปิดไมค์" aria-label="เปิด/ปิดไมค์" style="display:none;">
<img src="/Game/img/btn-mic-mute.png" alt="" id="btn-voice-icon-img" width="128" height="128" decoding="async" />
</button>
<div id="play-cyber-self-status" class="play-cyber-self-status"></div>
<div id="play-cyber-op-widgets" class="play-cyber-op-widgets" aria-hidden="true">
<div id="play-cyber-voice-chip" class="play-cyber-voice-chip"></div>
@@ -5205,7 +5371,7 @@
<script src="/app-base.js?v=2"></script>
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0306"></script>
<script src="js/play.js?v=0.006131863"></script>
<script src="js/play.js?v=0.006232008"></script>
<div class="version-tag">v —</div>
<style id="qc-howto-mg2-fix">
/* HOW TO PLAY ของ quiz_carry (Minigame-4) -> ให้เหมือน Minigame-2 (gch-mg2-mock)
+1 -1
View File
@@ -1617,7 +1617,7 @@
<script src="js/version.js?v=0.0122"></script>
<script src="js/customize-popup.js?v=40" 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.0302"></script>
<script src="js/room-lobby.js?v=0.0303"></script>
<div class="version-tag">v —</div>
</body>
</html>
+32 -3
View File
@@ -1151,21 +1151,39 @@ function endSpecialQuizSession(sid, space, success) {
/* client «» quiz ()
safety timeout นคางถาไมใครกด */
space.specialQuizAwaitContinue = true;
space.specialQuizContinueAcks = new Set(); /* รวบ ack «เล่นต่อ» จากผู้เล่นจริง — ครบทุกคนถึง resume พร้อมกัน */
/* แจ้งจำนวนเริ่มต้น 0/N ให้ทุก client (โชว์บนปุ่ม) */
io.to(sid).emit('special-quiz-continue-progress', { ready: 0, total: space.peers.size });
if (space.specialQuizContinueTimer) clearTimeout(space.specialQuizContinueTimer);
space.specialQuizContinueTimer = setTimeout(() => {
space.specialQuizContinueTimer = null;
finishSpecialQuizContinue(sid, space);
finishSpecialQuizContinue(sid, space); /* safety: ไม่ครบใน 32s → ไปต่อทั้งห้อง */
}, 32000);
}
/** นับ ack «เล่นต่อ» — ถ้าผู้เล่นจริงที่ยังอยู่กดครบทุกคน → resume พร้อมกัน (ไม่นับบอท) */
function maybeFinishSpecialQuizContinue(sid, space) {
if (!space || !space.specialQuizAwaitContinue) return;
const acks = space.specialQuizContinueAcks || (space.specialQuizContinueAcks = new Set());
const expected = [...space.peers.keys()]; /* ผู้เล่นจริงที่ยังอยู่ในห้อง (บอทไม่ต้องกด) */
const ready = expected.filter((pid) => acks.has(pid)).length;
io.to(sid).emit('special-quiz-continue-progress', { ready, total: expected.length });
if (expected.length === 0 || expected.every((pid) => acks.has(pid))) {
finishSpecialQuizContinue(sid, space);
}
}
/** เล่นรอบ quiz ต่อหลังผู้เล่นกด «เล่นต่อ» (เรียกครั้งเดียว / หรือ safety timeout) */
function finishSpecialQuizContinue(sid, space) {
if (!space || !space.specialQuizAwaitContinue) return;
space.specialQuizAwaitContinue = false;
space.specialQuizContinueAcks = null;
if (space.specialQuizContinueTimer) {
clearTimeout(space.specialQuizContinueTimer);
space.specialQuizContinueTimer = null;
}
/* แจ้งทุก client ให้ปลด freeze + ปิด overlay พร้อมกัน (กดครบ หรือ safety timeout) */
io.to(sid).emit('special-quiz-resume-all', {});
resumeQuizAfterSpecialQuiz(sid, space);
}
@@ -7810,8 +7828,13 @@ io.on('connection', (socket) => {
const sid = socket.data.spaceId;
const space = sid ? spaces.get(sid) : null;
if (!space || !space.peers.has(socket.id)) return reply({ ok: false });
finishSpecialQuizContinue(sid, space);
/* รอบจบไปแล้ว (resume แล้ว) — ตอบ ok เฉย ๆ */
if (!space.specialQuizAwaitContinue) return reply({ ok: true });
/* เก็บ ack ของผู้เล่นคนนี้ แล้วเช็ค: ครบทุกคน → resume พร้อมกัน (ไม่ resume ทันทีคนเดียว) */
if (!space.specialQuizContinueAcks) space.specialQuizContinueAcks = new Set();
space.specialQuizContinueAcks.add(socket.id);
reply({ ok: true });
maybeFinishSpecialQuizContinue(sid, space);
});
/** เปลี่ยนสีเสื้อ/ผิวใน lobby — ห้ามเลือกสีที่คนอื่นใช้แล้ว */
@@ -7918,6 +7941,7 @@ io.on('connection', (socket) => {
const space = sid ? spaces.get(sid) : null;
if (!space || space.hostId !== socket.id || !data) return; /* host เท่านั้น */
const bossDmg = Math.max(0, Number(data.bossDmg) || 0);
const anchor = Math.max(0, Number(data.anchor) || 0); /* forward anchor host → clients adopt (มุมลูกศรวงกลม sync) */
const players = Array.isArray(data.players)
? data.players.filter((p) => p && p.id != null).slice(0, 12).map((p) => ({
id: p.id,
@@ -7926,7 +7950,7 @@ io.on('connection', (socket) => {
e: !!p.e,
}))
: [];
socket.to(sid).emit('balloon-boss-sync', { bossDmg, players });
socket.to(sid).emit('balloon-boss-sync', { bossDmg, players, anchor });
});
/* MG3 stack (shared tower) — relay ผลการวาง (เจ้าของตา/บอทของ host) ให้ทุกคนวางบล็อกเดียวกัน + เลื่อนตาพร้อมกัน */
@@ -8696,6 +8720,11 @@ io.on('connection', (socket) => {
io.to(sid).emit('user-left', { id: socket.id });
if (space.qbwSolved) qbwBroadcastScores(sid, space); // Quiz Battle ฉากเดิน — อัปเดตอันดับหลังมีคนออก
emitDetectiveLobbyBPresence(sid, space); /* คนออกจาก LobbyB → อัปเดตจำนวนที่พร้อมให้ host */
/* กำลังรอกด «เล่นต่อ» (special-quiz) แล้วมีคนออก — เอา ack คนนั้นออกแล้วเช็คใหม่ (กันค้างรอคนที่ไม่อยู่แล้ว) */
if (space.specialQuizAwaitContinue) {
if (space.specialQuizContinueAcks) space.specialQuizContinueAcks.delete(socket.id);
maybeFinishSpecialQuizContinue(sid, space);
}
}
});
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>เข้าร่วมเกม — JD JUSTICE DIVERS</title>
<link rel="stylesheet" href="style.css?v=0.004">
<link rel="stylesheet" href="style.css?v=0.005">
</head>
<body>
<div class="join-bg" role="img" aria-label="พื้นหลัง">
+38
View File
@@ -643,3 +643,41 @@ input.join-textfield-input,
object-fit: contain;
}
}
/* ================================================== */
/* Animations — entrance / press / transitions */
/* ================================================== */
@keyframes jr-rise { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
@keyframes jr-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes jr-char-in { from { opacity: 0; transform: translateY(4%) scale(1.05); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes jr-view-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
@keyframes jr-pop { 0% { transform: scale(1); } 45% { transform: scale(1.1); } 100% { transform: scale(1); } }
/* press feedback — เปิดเสมอ */
.join-btn-back { transition: transform .12s ease, filter .2s; }
.join-btn-back:hover { filter: brightness(1.15); }
.join-btn-back:active { transform: scale(.88); }
.join-tab { transition: transform .14s ease, filter .2s; }
.join-tab:hover { filter: brightness(1.12); }
.join-tab:active { transform: scale(.94); }
.join-room-btn { transition: transform .14s ease, filter .2s; }
.join-room-btn:hover:not(:disabled) { filter: brightness(1.15); }
.join-room-btn:active:not(:disabled) { transform: scale(.9); }
.join-private-submit { transition: transform .14s ease, filter .2s; }
.join-private-submit:hover:not(:disabled) { filter: brightness(1.18); }
.join-private-submit:active:not(:disabled) { transform: scale(.94); }
.join-room-row-inner { transition: transform .16s ease, filter .2s; }
.join-room-row:hover .join-room-row-inner { transform: translateY(-2px); filter: brightness(1.08); }
@media (prefers-reduced-motion: no-preference) {
/* entrance: โผล่ไล่ทีละชิ้น */
.join-char-img { animation: jr-char-in 1s ease both; }
.join-btn-back { animation: jr-fade .6s ease .08s backwards; }
.join-title-img { animation: jr-rise .55s cubic-bezier(.2,.85,.25,1) .05s both; }
.join-title-line { animation: jr-rise .55s cubic-bezier(.2,.85,.25,1) .13s both; }
.join-tabs { animation: jr-rise .55s cubic-bezier(.2,.85,.25,1) .2s both; }
.join-panel { animation: jr-rise .55s cubic-bezier(.2,.85,.25,1) .28s both; }
/* transitions: สลับแท็บ → view เลื่อนเข้า · แท็บที่เลือก → เด้ง */
.join-view:not(.join-view--hidden) { animation: jr-view-in .3s ease; }
.join-tab[aria-selected="true"] { animation: jr-pop .32s ease; }
}
+1 -1
View File
@@ -7,7 +7,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Kanit:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css?v=0.0189">
<link rel="stylesheet" href="style.css?v=0.0190">
<link rel="stylesheet" href="daily-popup.css?v=8">
<link rel="stylesheet" href="../Game/css/customize-popup.css?v=42">
<link rel="stylesheet" href="../Game/css/profile-popup.css?v=7">
+41
View File
@@ -1972,3 +1972,44 @@ body.lobby-guide-active:not(.lobby-guide-top-step) .lobby-footer {
width: var(--lobby-profile-coin-size) !important;
height: var(--lobby-profile-coin-size) !important;
}
/* ================================================== */
/* Animations — entrance (ตอนขึ้น) / press (ตอนกด) */
/* accents (ตอนเปลี่ยน/แจ้งเตือน) */
/* ================================================== */
@keyframes lb-rise { from { opacity: 0; transform: translateY(22px); } to { opacity: 1; transform: translateY(0); } }
@keyframes lb-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes lb-from-left { from { opacity: 0; transform: translateX(-26px); } to { opacity: 1; transform: translateX(0); } }
@keyframes lb-from-right { from { opacity: 0; transform: translateX(28px); } to { opacity: 1; transform: translateX(0); } }
@keyframes lb-char-in { from { opacity: 0; transform: translateY(18px) scale(.96); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes lb-dot-pulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.35); opacity: .55; } }
/* press feedback — เปิดเสมอ (ทุกปุ่ม) */
.lobby-btn-tutorial, .lobby-btn-daily, .lobby-btn-cloth, .lobby-btn-ai-chat,
.lobby-btn-start, .lobby-btn-quiz, .lobby-btn-profile, .lobby-guide-next, .lobby-icon-btn {
transition: transform .13s ease, filter .2s;
-webkit-tap-highlight-color: transparent;
}
.lobby-btn-tutorial:hover, .lobby-btn-daily:hover, .lobby-btn-cloth:hover, .lobby-btn-ai-chat:hover,
.lobby-btn-start:hover, .lobby-btn-quiz:hover, .lobby-btn-profile:hover,
.lobby-guide-next:hover, .lobby-icon-btn:hover { filter: brightness(1.13); }
.lobby-btn-tutorial:active, .lobby-btn-daily:active, .lobby-btn-cloth:active, .lobby-btn-ai-chat:active,
.lobby-btn-profile:active, .lobby-guide-next:active, .lobby-icon-btn:active { transform: scale(.91); }
.lobby-btn-start:active, .lobby-btn-quiz:active { transform: scale(.95); }
.lobby-leaderboard-list li { transition: filter .18s ease, transform .18s ease; }
.lobby-leaderboard-list li:hover { filter: brightness(1.12); transform: translateX(2px); }
@media (prefers-reduced-motion: no-preference) {
/* entrance: โผล่ไล่ทีละส่วน */
.lobby-profile-panel { animation: lb-from-left .55s cubic-bezier(.2,.85,.25,1) .05s both; }
.lobby-news-wrap { animation: lb-rise .55s cubic-bezier(.2,.85,.25,1) .12s both; }
.lobby-header-row2-left { animation: lb-fade .5s ease .18s both; }
.lobby-btn-daily { animation: lb-fade .5s ease .2s backwards; }
.lobby-center-left { animation: lb-fade .5s ease .24s both; }
.lobby-character-center { animation: lb-char-in .7s cubic-bezier(.2,.85,.25,1) .22s both; }
.lobby-leaderboard { animation: lb-from-right .6s cubic-bezier(.2,.85,.25,1) .3s both; }
.lobby-footer { animation: lb-rise .6s cubic-bezier(.2,.85,.25,1) .4s both; }
/* accent: จุดแจ้งเตือน daily เต้น */
.lobby-daily-dot { animation: lb-dot-pulse 1.6s ease-in-out infinite; }
}
+1 -1
View File
@@ -7,7 +7,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Kanit:wght@500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css?v=0.0221">
<link rel="stylesheet" href="style.css?v=0.0222">
</head>
<body>
<div class="qb-bg" aria-hidden="true">
+34
View File
@@ -594,6 +594,40 @@ body {
.qb-btn:disabled { opacity: 0.5; cursor: default; }
.qb-btn.ghost { background: rgba(255,255,255,0.1); color: #fff; border: 1px solid rgba(255,255,255,0.2); box-shadow: none; }
/* ================================================== */
/* Animations — entrance (ตอนขึ้น) / press (ตอนกด) */
/* ================================================== */
@keyframes qb-rise { from { opacity: 0; transform: translateY(22px); } to { opacity: 1; transform: translateY(0); } }
@keyframes qb-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes qb-card-in { from { opacity: 0; transform: translateY(26px) scale(.96); } to { opacity: 1; transform: translateY(0) scale(1); } }
/* press feedback — เปิดเสมอ */
.qb-join-back { transition: transform .13s ease, filter .2s; -webkit-tap-highlight-color: transparent; }
.qb-join-back:hover { filter: brightness(1.13); }
.qb-join-back:active { transform: scale(.9); }
.btn-voice-icon { transition: transform .13s ease, filter .2s; }
.btn-voice-icon:hover { filter: brightness(1.13); }
.btn-voice-icon:active { transform: scale(.9); }
/* การ์ดห้อง: hover ยกขึ้น (มี transition แล้ว) + override :active ของปุ่มเริ่ม คง translateX(-50%) */
.qb-card { transition: transform .18s ease, filter .2s; }
.qb-card:hover { transform: translateY(-4px); filter: brightness(1.06); }
.qb-card-start:active { transform: translateX(-50%) scale(.93); }
@media (prefers-reduced-motion: no-preference) {
/* entrance: โผล่ไล่ทีละชิ้น */
.qb-join-back { animation: qb-fade .55s ease .06s backwards; }
.qb-room-profile-wrap { animation: qb-fade .6s ease .12s both; }
.qb-room-banner { animation: qb-rise .55s cubic-bezier(.2,.85,.25,1) .12s both; }
/* การ์ดใช้ backwards → หลัง entrance กลับสู่สภาพปกติ (hover transform ยังทำงาน) */
.qb-card { animation: qb-card-in .5s cubic-bezier(.2,.85,.25,1) backwards; }
.qb-card:nth-child(1) { animation-delay: .2s; }
.qb-card:nth-child(2) { animation-delay: .28s; }
.qb-card:nth-child(3) { animation-delay: .36s; }
.qb-card:nth-child(4) { animation-delay: .44s; }
.qb-card:nth-child(5) { animation-delay: .52s; }
.qb-card:nth-child(n+6) { animation-delay: .58s; }
}