join room เพิ่มรูปให้เข้าใจง่าย
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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 1–7 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);
|
||||
|
||||
@@ -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">ดูข้อมูลกฎหมายเพิ่มเติม ></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');
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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="พื้นหลัง">
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user