minigame 2 just begin1.6

This commit is contained in:
2026-04-30 20:24:47 +00:00
parent 53ee199a6d
commit 1aa90543ef
7 changed files with 680 additions and 85 deletions
+21
View File
@@ -5,10 +5,20 @@ set -euo pipefail
REPO="$(cd "$(dirname "$0")/.." && pwd)"
SRC="$REPO/www/html/"
DST="/var/www/html/"
GAME_SERVER_JS="$DST/Game/server.js"
if [[ "$(id -u)" -ne 0 ]]; then
echo "Run as root: sudo $0" >&2
exit 1
fi
# ถ้า Game/server.js เปลี่ยนหลัง deploy — Node ต้องรีโหลดโค้ด (systemd: game-zep.service)
hash_game_server_js() {
local f="$1"
[[ -f "$f" ]] || { echo ""; return 0; }
sha256sum "$f" | awk '{print $1}'
}
GAME_SRV_HASH_BEFORE="$(hash_game_server_js "$GAME_SERVER_JS")"
rsync -a \
--checksum \
--exclude 'node_modules' \
@@ -23,4 +33,15 @@ GA="$DST/Game/public/img/gauntlet-assets"
mkdir -p "$GA"
chown www-data:www-data "$GA"
chmod 2775 "$GA"
GAME_SRV_HASH_AFTER="$(hash_game_server_js "$GAME_SERVER_JS")"
if [[ -n "$GAME_SRV_HASH_AFTER" && "$GAME_SRV_HASH_BEFORE" != "$GAME_SRV_HASH_AFTER" ]]; then
if systemctl cat game-zep.service &>/dev/null; then
systemctl restart game-zep.service
echo "OK: restarted game-zep.service (Game/server.js changed)"
else
echo "WARN: Game/server.js changed but no game-zep.service — restart Node manually" >&2
fi
fi
echo "OK: deployed $SRC -> $DST (node_modules on server untouched)"
File diff suppressed because one or more lines are too long
+212 -11
View File
@@ -692,6 +692,12 @@
let gauntletRuntimeTimeLimitSec = 0;
/** เวลาสิ้นสุดรอบ (epoch ms) จากเซิร์ฟเวอร์ — null = ไม่จับเวลา */
let gauntletEndsAtMs = null;
/** Crown (mno9kb07): เซิร์ฟยังไม่ปล่อยรัน — หยุด tick / เวลา */
let gauntletCrownRunHeldRemote = false;
/** null | 'howto' | 'countdown' | 'live' — พรีรันก่อน GO */
let gauntletCrownPregamePhase = null;
let gauntletCrownLobbyReadyMap = {};
let gauntletCrownCountdownTimer = null;
let lastGauntletJumpKey = 0;
/** รูป lane: หลาย URL สุ่มตาม id คงที่ · laser: แยกบน/ล่าง/เส้น + สี/ความหนา */
let gauntletLaneImageUrls = [];
@@ -2455,8 +2461,11 @@
return isGauntletFaceRightMapMno9kb07();
}
let gauntletCrownHowtoVisible = false;
function isGauntletCrownHowtoBlocking() {
return isGauntletCrownHeistMapPlay() && gauntletCrownHowtoVisible;
function isGauntletCrownPregameBlockingPlay() {
if (!isGauntletCrownHeistMapPlay()) return false;
if (gauntletCrownRunHeldRemote) return true;
if (gauntletCrownPregamePhase != null && gauntletCrownPregamePhase !== 'live') return true;
return false;
}
function isStack() { return mapData && mapData.gameType === 'stack'; }
function isJumpSurvive() { return mapData && mapData.gameType === 'jump_survive'; }
@@ -4907,6 +4916,12 @@
gauntletEndsAtMs = Number.isFinite(ge) ? ge : null;
}
}
if (Object.prototype.hasOwnProperty.call(payload, 'gauntletCrownRunHeld')) {
gauntletCrownRunHeldRemote = !!payload.gauntletCrownRunHeld;
if (!gauntletCrownRunHeldRemote && isGauntletCrownHeistMapPlay()) {
gauntletCrownPregamePhase = 'live';
}
}
if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaneImageUrls') && Array.isArray(payload.gauntletLaneImageUrls)) {
gauntletLaneImageUrls = payload.gauntletLaneImageUrls
.filter((x) => typeof x === 'string')
@@ -5170,6 +5185,7 @@
function applyGauntletPreviewBotsAfterSync() {
if (!previewFillBots || !mapData || mapData.gameType !== 'gauntlet') return;
if (isGauntletCrownHeistMapPlay() && isGauntletCrownPregameBlockingPlay()) return;
const w = mapData.width || 20;
const h = mapData.height || 15;
others.forEach((o, id) => {
@@ -5182,6 +5198,7 @@
/** แจ้งเซิร์ฟเวอร์แถว y ที่มีคุณ+บอททดสอบ — lane spawn ใช้เฉพาะแถวที่มี peer; บอทไม่ใช่ peer */
function emitGauntletPreviewRowsToServer() {
if (!previewFillBots || !mapData || mapData.gameType !== 'gauntlet') return;
if (isGauntletCrownHeistMapPlay() && isGauntletCrownPregameBlockingPlay()) return;
const h = mapData.height || 15;
const ysSet = new Set();
const addY = (v) => {
@@ -5205,6 +5222,7 @@
'gauntlet-ended-overlay',
'quiz-battle-mcq-overlay',
'quiz-carry-embed-countdown',
'gauntlet-crown-countdown',
'quiz-carry-embed-q-strip',
'play-quiz-scoreboard',
'play-quiz-feedback',
@@ -5225,20 +5243,107 @@
return BASE + '/img/gauntlet-assets/result-txt-2.png';
}
function gauntletCrownPregameReadyNumerator() {
let n = quizCarryPregameBotCount();
quizCarryPregameHumanIds().forEach((id) => {
if (gauntletCrownLobbyReadyMap[id]) n++;
});
return n;
}
function gauntletCrownSyncGuestReadyIfNeeded() {
if (gauntletCrownPregamePhase !== 'howto') return;
if (myId == null || isMePlayHost()) return;
const sid = String(myId);
if (gauntletCrownLobbyReadyMap[sid]) return;
gauntletCrownLobbyReadyMap[sid] = true;
if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-ready', { ready: true });
}
function updateGauntletCrownHowtoHud() {
const st = document.getElementById('gauntlet-crown-howto-status');
const btn = document.getElementById('btn-gch-ready');
if (!btn || !isGauntletCrownHeistMapPlay() || gauntletCrownPregamePhase !== 'howto') return;
const humans = quizCarryPregameHumanIds();
const tot = Math.max(1, quizCarryPregameTotalPlayers());
const num = gauntletCrownPregameReadyNumerator();
if (st) {
st.classList.remove('is-hidden');
st.textContent = 'Ready Status : ' + num + '/' + tot;
}
const humansReady = humans.length > 0 && humans.every((id) => !!gauntletCrownLobbyReadyMap[id]);
btn.classList.toggle('is-start-phase', humansReady);
btn.classList.toggle('is-read-only', !isMePlayHost());
btn.disabled = !isMePlayHost();
btn.setAttribute('aria-pressed', humansReady ? 'false' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'true' : 'false'));
btn.title = isMePlayHost()
? (humansReady ? 'START' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'ยกเลิก READY' : 'READY'))
: (humansReady ? 'START (โฮสต์เท่านั้น)' : 'READY (โฮสต์เท่านั้น)');
}
function beginGauntletCrownCountdownThenRun() {
if (!isGauntletCrownHeistMapPlay()) return;
if (gauntletCrownCountdownTimer) {
clearTimeout(gauntletCrownCountdownTimer);
gauntletCrownCountdownTimer = null;
}
gauntletCrownPregamePhase = 'countdown';
const howto = document.getElementById('gauntlet-crown-howto-overlay');
if (howto) howto.classList.add('is-hidden');
gauntletCrownHowtoVisible = false;
const cd = document.getElementById('gauntlet-crown-countdown');
const numEl = document.getElementById('gauntlet-crown-countdown-num');
const runFinish = () => {
if (cd) cd.classList.add('is-hidden');
if (gauntletCrownCountdownTimer) {
clearTimeout(gauntletCrownCountdownTimer);
gauntletCrownCountdownTimer = null;
}
if (socket && socket.connected) {
socket.emit('gauntlet-crown-begin-run', (res) => {
if (res && res.ok) gauntletCrownPregamePhase = 'live';
});
} else {
gauntletCrownPregamePhase = 'live';
}
};
if (!cd || !numEl) {
runFinish();
return;
}
cd.classList.remove('is-hidden');
let n = 3;
numEl.textContent = String(n);
const step = () => {
n--;
if (n > 0) {
numEl.textContent = String(n);
gauntletCrownCountdownTimer = setTimeout(step, 1000);
} else {
gauntletCrownCountdownTimer = null;
runFinish();
}
};
gauntletCrownCountdownTimer = setTimeout(step, 1000);
}
function showGauntletCrownHowtoOverlay() {
if (!isGauntletCrownHeistMapPlay()) return;
hideConflictingOverlaysForGauntletCrown();
const cd = document.getElementById('gauntlet-crown-countdown');
if (cd) cd.classList.add('is-hidden');
if (gauntletCrownCountdownTimer) {
clearTimeout(gauntletCrownCountdownTimer);
gauntletCrownCountdownTimer = null;
}
gauntletCrownPregamePhase = 'howto';
const ov = document.getElementById('gauntlet-crown-howto-overlay');
if (!ov) return;
gauntletCrownHowtoVisible = true;
ov.classList.remove('is-hidden');
const btn = document.getElementById('btn-gch-ready');
if (btn) {
btn.onclick = function () {
ov.classList.add('is-hidden');
gauntletCrownHowtoVisible = false;
};
}
if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-sync-request');
gauntletCrownSyncGuestReadyIfNeeded();
updateGauntletCrownHowtoHud();
}
function showGauntletCrownMissionOverlay(mission) {
@@ -7752,6 +7857,14 @@
else teardownPlayQuizUi();
if (mapData.gameType === 'gauntlet' && isGauntletCrownHeistMapPlay()) {
hideConflictingOverlaysForGauntletCrown();
gauntletCrownPregamePhase = null;
gauntletCrownLobbyReadyMap = {};
if (gauntletCrownCountdownTimer) {
clearTimeout(gauntletCrownCountdownTimer);
gauntletCrownCountdownTimer = null;
}
const gccR = document.getElementById('gauntlet-crown-countdown');
if (gccR) gccR.classList.add('is-hidden');
setTimeout(function () { showGauntletCrownHowtoOverlay(); }, 50);
} else {
const hov = document.getElementById('gauntlet-crown-howto-overlay');
@@ -7759,6 +7872,14 @@
hov.classList.add('is-hidden');
gauntletCrownHowtoVisible = false;
}
gauntletCrownPregamePhase = null;
gauntletCrownLobbyReadyMap = {};
if (gauntletCrownCountdownTimer) {
clearTimeout(gauntletCrownCountdownTimer);
gauntletCrownCountdownTimer = null;
}
const gccR2 = document.getElementById('gauntlet-crown-countdown');
if (gccR2) gccR2.classList.add('is-hidden');
}
resizeCanvas(); draw(); tick();
}
@@ -7867,6 +7988,18 @@
if (!(previewMode && editorEmbedReturn && isQuizCarry())) return;
endQuizCarryEmbedPregameAndStart();
});
socket.on('gauntlet-crown-lobby-sync', (d) => {
if (!d || typeof d.readyMap !== 'object') return;
gauntletCrownLobbyReadyMap = { ...d.readyMap };
if (gauntletCrownPregamePhase === 'howto') {
gauntletCrownSyncGuestReadyIfNeeded();
updateGauntletCrownHowtoHud();
}
});
socket.on('gauntlet-crown-lobby-started', () => {
if (!isGauntletCrownHeistMapPlay()) return;
beginGauntletCrownCountdownThenRun();
});
socket.on('user-move', (data) => {
if (data && myId != null && data.id === myId) {
/* Gauntlet: ตำแหน่งอ้างอิงจาก gauntlet-sync + เลอร์ปเท่านั้น — ไม่งั้น echo จาก move จะสแนป me.x/y โดยไม่อัปเดต tx/ty ทำให้เลอร์ปผิดและชนเซิร์ฟเวอร์ไม่ตรง */
@@ -8093,6 +8226,15 @@
socket.on('gauntlet-ended', (data) => {
gauntletEndsAtMs = null;
gauntletCrownRunHeldRemote = false;
gauntletCrownPregamePhase = null;
gauntletCrownLobbyReadyMap = {};
if (gauntletCrownCountdownTimer) {
clearTimeout(gauntletCrownCountdownTimer);
gauntletCrownCountdownTimer = null;
}
const gcc = document.getElementById('gauntlet-crown-countdown');
if (gcc) gcc.classList.add('is-hidden');
const hto = document.getElementById('gauntlet-crown-howto-overlay');
if (hto) hto.classList.add('is-hidden');
gauntletCrownHowtoVisible = false;
@@ -8215,17 +8357,41 @@
})
.catch(() => {});
if (String(ev.mapId || '') === GAUNTLET_FACE_RIGHT_MAP_ID) {
gauntletCrownPregamePhase = null;
gauntletCrownLobbyReadyMap = {};
if (gauntletCrownCountdownTimer) {
clearTimeout(gauntletCrownCountdownTimer);
gauntletCrownCountdownTimer = null;
}
const gcc0 = document.getElementById('gauntlet-crown-countdown');
if (gcc0) gcc0.classList.add('is-hidden');
setTimeout(function () { showGauntletCrownHowtoOverlay(); }, 60);
} else {
const hto2 = document.getElementById('gauntlet-crown-howto-overlay');
if (hto2) hto2.classList.add('is-hidden');
gauntletCrownHowtoVisible = false;
gauntletCrownPregamePhase = null;
gauntletCrownLobbyReadyMap = {};
if (gauntletCrownCountdownTimer) {
clearTimeout(gauntletCrownCountdownTimer);
gauntletCrownCountdownTimer = null;
}
const gcc1 = document.getElementById('gauntlet-crown-countdown');
if (gcc1) gcc1.classList.add('is-hidden');
}
} else {
gauntletEndsAtMs = null;
const hto3 = document.getElementById('gauntlet-crown-howto-overlay');
if (hto3) hto3.classList.add('is-hidden');
gauntletCrownHowtoVisible = false;
gauntletCrownPregamePhase = null;
gauntletCrownLobbyReadyMap = {};
if (gauntletCrownCountdownTimer) {
clearTimeout(gauntletCrownCountdownTimer);
gauntletCrownCountdownTimer = null;
}
const gcc2 = document.getElementById('gauntlet-crown-countdown');
if (gcc2) gcc2.classList.add('is-hidden');
}
if (mapData.gameType !== 'gauntlet') {
me.tx = null;
@@ -9339,7 +9505,9 @@
timeVal.textContent = '0';
}
} else if (isGauntlet()) {
if (gauntletEndsAtMs != null && Number.isFinite(gauntletEndsAtMs)) {
if (isGauntletCrownHeistMapPlay() && isGauntletCrownPregameBlockingPlay()) {
timeVal.textContent = '···';
} else if (gauntletEndsAtMs != null && Number.isFinite(gauntletEndsAtMs)) {
const rem = Math.max(0, Math.ceil((gauntletEndsAtMs - Date.now()) / 1000));
const mm = Math.floor(rem / 60);
const ss = rem % 60;
@@ -10324,7 +10492,7 @@
if (isGauntlet() && mapData && !isChatFocused()) {
if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') {
e.preventDefault();
if (isGauntletCrownHowtoBlocking()) return;
if (isGauntletCrownPregameBlockingPlay()) return;
if (isGauntletCrownHeistMapPlay() && me.gauntletEliminated) return;
const now = Date.now();
if (now - lastGauntletJumpKey < 200) return;
@@ -10431,6 +10599,12 @@
return;
}
if (isGauntlet()) {
if (isGauntletCrownPregameBlockingPlay()) {
me.isWalking = false;
draw();
requestAnimationFrame(tick);
return;
}
me.isWalking = meGauntletJumpTicks > 0;
const stepGauntletJumpVis = (vis, tgt) => {
const t = Number(tgt) || 0;
@@ -10611,6 +10785,33 @@
});
})();
(function wireGauntletCrownHowtoPrimary() {
const btn = document.getElementById('btn-gch-ready');
if (!btn) return;
btn.addEventListener('click', () => {
if (!isGauntletCrownHeistMapPlay()) return;
if (gauntletCrownPregamePhase !== 'howto') return;
if (myId == null || !isMePlayHost()) return;
const humans = quizCarryPregameHumanIds();
const humansReady = humans.length > 0 && humans.every((id) => !!gauntletCrownLobbyReadyMap[id]);
if (humansReady) {
if (humans.length === 1 && isMePlayHost()) {
beginGauntletCrownCountdownThenRun();
return;
}
socket.emit('gauntlet-crown-lobby-start', {}, (r) => {
if (!r || !r.ok) { /* ignore */ }
});
return;
}
const sid = String(myId);
const next = !gauntletCrownLobbyReadyMap[sid];
gauntletCrownLobbyReadyMap[sid] = next;
updateGauntletCrownHowtoHud();
socket.emit('gauntlet-crown-lobby-ready', { ready: next });
});
})();
(function wireQuizCarryPregameOverlay() {
const primary = document.getElementById('quiz-carry-pregame-primary');
if (!primary) return;
+1 -1
View File
@@ -1,6 +1,6 @@
// ทุกครั้งที่มีการเพิ่มหรือเปลี่ยน ให้เพิ่ม v +0.0001
// หลังแก้ค่าแล้วต้อง copy ไป path ที่ Nginx ชี้ (หรือรัน copy-frogger-files-only.sh) ถึงจะเห็นบนเว็บ
window.APP_VERSION = '0.0170';
window.APP_VERSION = '0.0171';
document.addEventListener('DOMContentLoaded', function () {
var t = document.querySelector('.version-tag');
if (t) t.textContent = 'v ' + window.APP_VERSION;
+92 -25
View File
@@ -915,9 +915,47 @@
box-sizing: border-box;
z-index: 10140;
}
#gauntlet-crown-countdown {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: max(10px, env(safe-area-inset-top)) max(10px, env(safe-area-inset-right)) max(10px, env(safe-area-inset-bottom)) max(10px, env(safe-area-inset-left));
box-sizing: border-box;
z-index: 10142;
pointer-events: auto;
background: rgba(6, 8, 18, 0.55);
backdrop-filter: blur(3px);
}
#gauntlet-crown-mission-overlay { z-index: 10150; }
#gauntlet-crown-howto-overlay.is-hidden,
#gauntlet-crown-mission-overlay.is-hidden { display: none !important; }
#gauntlet-crown-mission-overlay.is-hidden,
#gauntlet-crown-countdown.is-hidden { display: none !important; }
.gcc-panel {
text-align: center;
padding: clamp(16px, 4vw, 28px);
border-radius: 16px;
border: 2px solid rgba(0, 255, 240, 0.75);
box-shadow: 0 0 28px rgba(0, 255, 240, 0.22), 0 0 40px rgba(255, 0, 200, 0.12);
background: rgba(8, 10, 22, 0.88);
}
.gcc-kicker {
margin: 0 0 12px;
font-size: clamp(0.85rem, 2.4vw, 1rem);
font-weight: 700;
color: #7af8ff;
letter-spacing: 0.04em;
}
.gcc-num {
display: block;
font-size: clamp(3rem, 14vw, 5.5rem);
font-weight: 900;
line-height: 1;
color: #fff;
text-shadow: 0 0 18px rgba(0, 255, 240, 0.55), 0 0 28px rgba(255, 0, 200, 0.35);
font-variant-numeric: tabular-nums;
}
.gch-backdrop,
.gcm-backdrop {
position: absolute;
@@ -971,6 +1009,34 @@
border: 1px solid rgba(0, 255, 240, 0.35);
box-shadow: inset 0 0 24px rgba(0, 0, 0, 0.35);
}
.gch-inner.gch-inner--art {
left: 0;
top: 0;
right: 0;
bottom: 0;
transform: none;
width: 100%;
max-height: none;
overflow: visible;
padding: 0 0 clamp(48px, 11%, 88px);
background: transparent;
border: none;
box-shadow: none;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
pointer-events: none;
}
.gch-inner.gch-inner--art .gch-status {
pointer-events: none;
margin-bottom: 6px;
max-width: min(92%, 520px);
}
.gch-inner.gch-inner--art .btn-gch-ready {
pointer-events: auto;
flex-shrink: 0;
}
.gch-title {
margin: 0 0 4px;
font-size: clamp(1rem, 3.2vw, 1.35rem);
@@ -1027,7 +1093,22 @@
background: url('/Game/img/gauntlet-assets/btn-ready.png') center / contain no-repeat;
background-color: transparent;
}
.btn-gch-ready.is-start-phase {
background-image: url('/Game/img/quiz-carry/btn-start.png');
}
.btn-gch-ready:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.btn-gch-ready:focus-visible { outline: 2px solid #7af8ff; outline-offset: 3px; }
.gch-status {
margin: 0;
text-align: center;
font-size: clamp(0.72rem, 1.9vw, 0.88rem);
font-weight: 700;
color: #c0caf5;
}
.gch-status.is-hidden { display: none !important; }
.gcm-header-tab {
text-align: center;
font-weight: 800;
@@ -1686,36 +1767,22 @@
<button type="button" id="quiz-battle-mcq-close">ปิด · Close</button>
</div>
</div>
<div id="gauntlet-crown-howto-overlay" class="is-hidden" role="dialog" aria-modal="true" aria-labelledby="gch-title">
<div id="gauntlet-crown-howto-overlay" class="is-hidden" role="dialog" aria-modal="true" aria-label="HOW TO PLAY — Last Light">
<div class="gch-backdrop" aria-hidden="true"></div>
<div class="gch-shell">
<img class="gch-bg" src="/Game/img/gauntlet-assets/popup-Howto.png" alt="" width="920" height="520" decoding="async" />
<div class="gch-inner">
<h2 id="gch-title" class="gch-title">HOW TO PLAY</h2>
<p class="gch-sub">วิ่งหลบสิ่งกีดขวางให้รอด! · ระงับเหตุโจรกรรม ปกป้องมงกุฎทองคำ!</p>
<div class="gch-grid">
<section class="gch-col">
<h3>วิ่งหลบสิ่งกีดขวาง</h3>
<p>หลบสิ่งกีดขวางระหว่างทาง กด <strong>JUMP</strong> (Space / W / ↑)</p>
<p class="warn">ห้ามวิ่งตกขอบจอ — ตกขอบจอ = ตายทันที</p>
<p>คะแนนเริ่มที่ <strong>100</strong> · ชนสิ่งกีดขวาง = <strong>-10</strong> คะแนน</p>
</section>
<section class="gch-col">
<h3>โบนัสอันดับ</h3>
<p>อันดับ 1 = +100 · อันดับ 2 = +80 · อันดับ 3 = +60</p>
<p class="warn">คะแนนเท่ากัน = อันดับเท่ากัน</p>
</section>
<section class="gch-col">
<h3>ระดับความสำเร็จ</h3>
<p>นำคะแนนสุดท้ายของทุกคน (รวมโบนัส) มาเฉลี่ย → เกรด A / B / C และสุ่มการ์ดรางวัล</p>
<p>A: เฉลี่ย 80100 · B: 6079 · C: 059</p>
</section>
</div>
<p class="gch-foot">กด READY เพื่อเริ่มภารกิจ · Press READY</p>
<div class="gch-inner gch-inner--art">
<p id="gauntlet-crown-howto-status" class="gch-status is-hidden" aria-live="polite"></p>
<button type="button" class="btn-gch-ready" id="btn-gch-ready" title="READY"><span class="sr-only">READY</span></button>
</div>
</div>
</div>
<div id="gauntlet-crown-countdown" class="is-hidden" role="alert" aria-live="assertive" aria-atomic="true">
<div class="gcc-panel">
<p class="gcc-kicker">ภารกิจเริ่ม · Mission start</p>
<span id="gauntlet-crown-countdown-num" class="gcc-num">3</span>
</div>
</div>
<div id="gauntlet-crown-mission-overlay" class="is-hidden" role="dialog" aria-modal="true" aria-labelledby="gcm-heading">
<div class="gcm-backdrop" aria-hidden="true"></div>
<div class="gcm-shell">
@@ -1777,7 +1844,7 @@
</div>
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0170"></script>
<script src="js/play.js?v=0.194"></script>
<script src="js/play.js?v=0.195"></script>
<div class="version-tag">v —</div>
</body>
</html>
+212 -11
View File
@@ -692,6 +692,12 @@
let gauntletRuntimeTimeLimitSec = 0;
/** เวลาสิ้นสุดรอบ (epoch ms) จากเซิร์ฟเวอร์ — null = ไม่จับเวลา */
let gauntletEndsAtMs = null;
/** Crown (mno9kb07): เซิร์ฟยังไม่ปล่อยรัน — หยุด tick / เวลา */
let gauntletCrownRunHeldRemote = false;
/** null | 'howto' | 'countdown' | 'live' — พรีรันก่อน GO */
let gauntletCrownPregamePhase = null;
let gauntletCrownLobbyReadyMap = {};
let gauntletCrownCountdownTimer = null;
let lastGauntletJumpKey = 0;
/** รูป lane: หลาย URL สุ่มตาม id คงที่ · laser: แยกบน/ล่าง/เส้น + สี/ความหนา */
let gauntletLaneImageUrls = [];
@@ -2455,8 +2461,11 @@
return isGauntletFaceRightMapMno9kb07();
}
let gauntletCrownHowtoVisible = false;
function isGauntletCrownHowtoBlocking() {
return isGauntletCrownHeistMapPlay() && gauntletCrownHowtoVisible;
function isGauntletCrownPregameBlockingPlay() {
if (!isGauntletCrownHeistMapPlay()) return false;
if (gauntletCrownRunHeldRemote) return true;
if (gauntletCrownPregamePhase != null && gauntletCrownPregamePhase !== 'live') return true;
return false;
}
function isStack() { return mapData && mapData.gameType === 'stack'; }
function isJumpSurvive() { return mapData && mapData.gameType === 'jump_survive'; }
@@ -4907,6 +4916,12 @@
gauntletEndsAtMs = Number.isFinite(ge) ? ge : null;
}
}
if (Object.prototype.hasOwnProperty.call(payload, 'gauntletCrownRunHeld')) {
gauntletCrownRunHeldRemote = !!payload.gauntletCrownRunHeld;
if (!gauntletCrownRunHeldRemote && isGauntletCrownHeistMapPlay()) {
gauntletCrownPregamePhase = 'live';
}
}
if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaneImageUrls') && Array.isArray(payload.gauntletLaneImageUrls)) {
gauntletLaneImageUrls = payload.gauntletLaneImageUrls
.filter((x) => typeof x === 'string')
@@ -5170,6 +5185,7 @@
function applyGauntletPreviewBotsAfterSync() {
if (!previewFillBots || !mapData || mapData.gameType !== 'gauntlet') return;
if (isGauntletCrownHeistMapPlay() && isGauntletCrownPregameBlockingPlay()) return;
const w = mapData.width || 20;
const h = mapData.height || 15;
others.forEach((o, id) => {
@@ -5182,6 +5198,7 @@
/** แจ้งเซิร์ฟเวอร์แถว y ที่มีคุณ+บอททดสอบ — lane spawn ใช้เฉพาะแถวที่มี peer; บอทไม่ใช่ peer */
function emitGauntletPreviewRowsToServer() {
if (!previewFillBots || !mapData || mapData.gameType !== 'gauntlet') return;
if (isGauntletCrownHeistMapPlay() && isGauntletCrownPregameBlockingPlay()) return;
const h = mapData.height || 15;
const ysSet = new Set();
const addY = (v) => {
@@ -5205,6 +5222,7 @@
'gauntlet-ended-overlay',
'quiz-battle-mcq-overlay',
'quiz-carry-embed-countdown',
'gauntlet-crown-countdown',
'quiz-carry-embed-q-strip',
'play-quiz-scoreboard',
'play-quiz-feedback',
@@ -5225,20 +5243,107 @@
return BASE + '/img/gauntlet-assets/result-txt-2.png';
}
function gauntletCrownPregameReadyNumerator() {
let n = quizCarryPregameBotCount();
quizCarryPregameHumanIds().forEach((id) => {
if (gauntletCrownLobbyReadyMap[id]) n++;
});
return n;
}
function gauntletCrownSyncGuestReadyIfNeeded() {
if (gauntletCrownPregamePhase !== 'howto') return;
if (myId == null || isMePlayHost()) return;
const sid = String(myId);
if (gauntletCrownLobbyReadyMap[sid]) return;
gauntletCrownLobbyReadyMap[sid] = true;
if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-ready', { ready: true });
}
function updateGauntletCrownHowtoHud() {
const st = document.getElementById('gauntlet-crown-howto-status');
const btn = document.getElementById('btn-gch-ready');
if (!btn || !isGauntletCrownHeistMapPlay() || gauntletCrownPregamePhase !== 'howto') return;
const humans = quizCarryPregameHumanIds();
const tot = Math.max(1, quizCarryPregameTotalPlayers());
const num = gauntletCrownPregameReadyNumerator();
if (st) {
st.classList.remove('is-hidden');
st.textContent = 'Ready Status : ' + num + '/' + tot;
}
const humansReady = humans.length > 0 && humans.every((id) => !!gauntletCrownLobbyReadyMap[id]);
btn.classList.toggle('is-start-phase', humansReady);
btn.classList.toggle('is-read-only', !isMePlayHost());
btn.disabled = !isMePlayHost();
btn.setAttribute('aria-pressed', humansReady ? 'false' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'true' : 'false'));
btn.title = isMePlayHost()
? (humansReady ? 'START' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'ยกเลิก READY' : 'READY'))
: (humansReady ? 'START (โฮสต์เท่านั้น)' : 'READY (โฮสต์เท่านั้น)');
}
function beginGauntletCrownCountdownThenRun() {
if (!isGauntletCrownHeistMapPlay()) return;
if (gauntletCrownCountdownTimer) {
clearTimeout(gauntletCrownCountdownTimer);
gauntletCrownCountdownTimer = null;
}
gauntletCrownPregamePhase = 'countdown';
const howto = document.getElementById('gauntlet-crown-howto-overlay');
if (howto) howto.classList.add('is-hidden');
gauntletCrownHowtoVisible = false;
const cd = document.getElementById('gauntlet-crown-countdown');
const numEl = document.getElementById('gauntlet-crown-countdown-num');
const runFinish = () => {
if (cd) cd.classList.add('is-hidden');
if (gauntletCrownCountdownTimer) {
clearTimeout(gauntletCrownCountdownTimer);
gauntletCrownCountdownTimer = null;
}
if (socket && socket.connected) {
socket.emit('gauntlet-crown-begin-run', (res) => {
if (res && res.ok) gauntletCrownPregamePhase = 'live';
});
} else {
gauntletCrownPregamePhase = 'live';
}
};
if (!cd || !numEl) {
runFinish();
return;
}
cd.classList.remove('is-hidden');
let n = 3;
numEl.textContent = String(n);
const step = () => {
n--;
if (n > 0) {
numEl.textContent = String(n);
gauntletCrownCountdownTimer = setTimeout(step, 1000);
} else {
gauntletCrownCountdownTimer = null;
runFinish();
}
};
gauntletCrownCountdownTimer = setTimeout(step, 1000);
}
function showGauntletCrownHowtoOverlay() {
if (!isGauntletCrownHeistMapPlay()) return;
hideConflictingOverlaysForGauntletCrown();
const cd = document.getElementById('gauntlet-crown-countdown');
if (cd) cd.classList.add('is-hidden');
if (gauntletCrownCountdownTimer) {
clearTimeout(gauntletCrownCountdownTimer);
gauntletCrownCountdownTimer = null;
}
gauntletCrownPregamePhase = 'howto';
const ov = document.getElementById('gauntlet-crown-howto-overlay');
if (!ov) return;
gauntletCrownHowtoVisible = true;
ov.classList.remove('is-hidden');
const btn = document.getElementById('btn-gch-ready');
if (btn) {
btn.onclick = function () {
ov.classList.add('is-hidden');
gauntletCrownHowtoVisible = false;
};
}
if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-sync-request');
gauntletCrownSyncGuestReadyIfNeeded();
updateGauntletCrownHowtoHud();
}
function showGauntletCrownMissionOverlay(mission) {
@@ -7752,6 +7857,14 @@
else teardownPlayQuizUi();
if (mapData.gameType === 'gauntlet' && isGauntletCrownHeistMapPlay()) {
hideConflictingOverlaysForGauntletCrown();
gauntletCrownPregamePhase = null;
gauntletCrownLobbyReadyMap = {};
if (gauntletCrownCountdownTimer) {
clearTimeout(gauntletCrownCountdownTimer);
gauntletCrownCountdownTimer = null;
}
const gccR = document.getElementById('gauntlet-crown-countdown');
if (gccR) gccR.classList.add('is-hidden');
setTimeout(function () { showGauntletCrownHowtoOverlay(); }, 50);
} else {
const hov = document.getElementById('gauntlet-crown-howto-overlay');
@@ -7759,6 +7872,14 @@
hov.classList.add('is-hidden');
gauntletCrownHowtoVisible = false;
}
gauntletCrownPregamePhase = null;
gauntletCrownLobbyReadyMap = {};
if (gauntletCrownCountdownTimer) {
clearTimeout(gauntletCrownCountdownTimer);
gauntletCrownCountdownTimer = null;
}
const gccR2 = document.getElementById('gauntlet-crown-countdown');
if (gccR2) gccR2.classList.add('is-hidden');
}
resizeCanvas(); draw(); tick();
}
@@ -7867,6 +7988,18 @@
if (!(previewMode && editorEmbedReturn && isQuizCarry())) return;
endQuizCarryEmbedPregameAndStart();
});
socket.on('gauntlet-crown-lobby-sync', (d) => {
if (!d || typeof d.readyMap !== 'object') return;
gauntletCrownLobbyReadyMap = { ...d.readyMap };
if (gauntletCrownPregamePhase === 'howto') {
gauntletCrownSyncGuestReadyIfNeeded();
updateGauntletCrownHowtoHud();
}
});
socket.on('gauntlet-crown-lobby-started', () => {
if (!isGauntletCrownHeistMapPlay()) return;
beginGauntletCrownCountdownThenRun();
});
socket.on('user-move', (data) => {
if (data && myId != null && data.id === myId) {
/* Gauntlet: ตำแหน่งอ้างอิงจาก gauntlet-sync + เลอร์ปเท่านั้น — ไม่งั้น echo จาก move จะสแนป me.x/y โดยไม่อัปเดต tx/ty ทำให้เลอร์ปผิดและชนเซิร์ฟเวอร์ไม่ตรง */
@@ -8093,6 +8226,15 @@
socket.on('gauntlet-ended', (data) => {
gauntletEndsAtMs = null;
gauntletCrownRunHeldRemote = false;
gauntletCrownPregamePhase = null;
gauntletCrownLobbyReadyMap = {};
if (gauntletCrownCountdownTimer) {
clearTimeout(gauntletCrownCountdownTimer);
gauntletCrownCountdownTimer = null;
}
const gcc = document.getElementById('gauntlet-crown-countdown');
if (gcc) gcc.classList.add('is-hidden');
const hto = document.getElementById('gauntlet-crown-howto-overlay');
if (hto) hto.classList.add('is-hidden');
gauntletCrownHowtoVisible = false;
@@ -8215,17 +8357,41 @@
})
.catch(() => {});
if (String(ev.mapId || '') === GAUNTLET_FACE_RIGHT_MAP_ID) {
gauntletCrownPregamePhase = null;
gauntletCrownLobbyReadyMap = {};
if (gauntletCrownCountdownTimer) {
clearTimeout(gauntletCrownCountdownTimer);
gauntletCrownCountdownTimer = null;
}
const gcc0 = document.getElementById('gauntlet-crown-countdown');
if (gcc0) gcc0.classList.add('is-hidden');
setTimeout(function () { showGauntletCrownHowtoOverlay(); }, 60);
} else {
const hto2 = document.getElementById('gauntlet-crown-howto-overlay');
if (hto2) hto2.classList.add('is-hidden');
gauntletCrownHowtoVisible = false;
gauntletCrownPregamePhase = null;
gauntletCrownLobbyReadyMap = {};
if (gauntletCrownCountdownTimer) {
clearTimeout(gauntletCrownCountdownTimer);
gauntletCrownCountdownTimer = null;
}
const gcc1 = document.getElementById('gauntlet-crown-countdown');
if (gcc1) gcc1.classList.add('is-hidden');
}
} else {
gauntletEndsAtMs = null;
const hto3 = document.getElementById('gauntlet-crown-howto-overlay');
if (hto3) hto3.classList.add('is-hidden');
gauntletCrownHowtoVisible = false;
gauntletCrownPregamePhase = null;
gauntletCrownLobbyReadyMap = {};
if (gauntletCrownCountdownTimer) {
clearTimeout(gauntletCrownCountdownTimer);
gauntletCrownCountdownTimer = null;
}
const gcc2 = document.getElementById('gauntlet-crown-countdown');
if (gcc2) gcc2.classList.add('is-hidden');
}
if (mapData.gameType !== 'gauntlet') {
me.tx = null;
@@ -9339,7 +9505,9 @@
timeVal.textContent = '0';
}
} else if (isGauntlet()) {
if (gauntletEndsAtMs != null && Number.isFinite(gauntletEndsAtMs)) {
if (isGauntletCrownHeistMapPlay() && isGauntletCrownPregameBlockingPlay()) {
timeVal.textContent = '···';
} else if (gauntletEndsAtMs != null && Number.isFinite(gauntletEndsAtMs)) {
const rem = Math.max(0, Math.ceil((gauntletEndsAtMs - Date.now()) / 1000));
const mm = Math.floor(rem / 60);
const ss = rem % 60;
@@ -10324,7 +10492,7 @@
if (isGauntlet() && mapData && !isChatFocused()) {
if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') {
e.preventDefault();
if (isGauntletCrownHowtoBlocking()) return;
if (isGauntletCrownPregameBlockingPlay()) return;
if (isGauntletCrownHeistMapPlay() && me.gauntletEliminated) return;
const now = Date.now();
if (now - lastGauntletJumpKey < 200) return;
@@ -10431,6 +10599,12 @@
return;
}
if (isGauntlet()) {
if (isGauntletCrownPregameBlockingPlay()) {
me.isWalking = false;
draw();
requestAnimationFrame(tick);
return;
}
me.isWalking = meGauntletJumpTicks > 0;
const stepGauntletJumpVis = (vis, tgt) => {
const t = Number(tgt) || 0;
@@ -10611,6 +10785,33 @@
});
})();
(function wireGauntletCrownHowtoPrimary() {
const btn = document.getElementById('btn-gch-ready');
if (!btn) return;
btn.addEventListener('click', () => {
if (!isGauntletCrownHeistMapPlay()) return;
if (gauntletCrownPregamePhase !== 'howto') return;
if (myId == null || !isMePlayHost()) return;
const humans = quizCarryPregameHumanIds();
const humansReady = humans.length > 0 && humans.every((id) => !!gauntletCrownLobbyReadyMap[id]);
if (humansReady) {
if (humans.length === 1 && isMePlayHost()) {
beginGauntletCrownCountdownThenRun();
return;
}
socket.emit('gauntlet-crown-lobby-start', {}, (r) => {
if (!r || !r.ok) { /* ignore */ }
});
return;
}
const sid = String(myId);
const next = !gauntletCrownLobbyReadyMap[sid];
gauntletCrownLobbyReadyMap[sid] = next;
updateGauntletCrownHowtoHud();
socket.emit('gauntlet-crown-lobby-ready', { ready: next });
});
})();
(function wireQuizCarryPregameOverlay() {
const primary = document.getElementById('quiz-carry-pregame-primary');
if (!primary) return;
+141 -36
View File
@@ -2628,18 +2628,66 @@ function stopGauntletTicker(space) {
function ensureGauntletEndsAtIfNeeded(space) {
const gr = space.gauntletRun;
if (!gr) return;
if (gr.crownRunHeld) return;
const lim = getGauntletTimeLimitSec();
if (lim > 0 && gr.endsAt == null) {
gr.endsAt = Date.now() + lim * 1000;
}
}
function newGauntletRunState(space) {
const crown = !!(space && isGauntletCrownHeistMapBySpace(space));
return {
obstacles: [],
nextObsId: 1,
spawnAcc: 0,
nextSpawnIn: 3,
crownRunHeld: crown,
};
}
function emitGauntletSync(sid, space) {
const md = (space.mapId && maps.get(space.mapId)) || space.mapData;
if (!md || md.gameType !== 'gauntlet') return;
const gr = space.gauntletRun;
if (!gr) return;
const h = md.height || 15;
const obsOut = gr.obstacles.map((o) => {
if (o.kind === 'laser') {
const y0 = o.y0 != null && Number.isFinite(Number(o.y0)) ? Math.floor(Number(o.y0)) : 0;
const y1 = o.y1 != null && Number.isFinite(Number(o.y1)) ? Math.floor(Number(o.y1)) : h - 1;
return { id: o.id, kind: 'laser', x: o.x, y: null, y0, y1 };
}
return { id: o.id, kind: o.kind, x: o.x, y: o.y };
});
const playersOut = [...space.peers.values()].map((p) => ({
id: p.id,
x: p.x,
y: p.y,
direction: p.direction || 'down',
characterId: p.characterId ?? null,
gauntletJumpTicks: p.gauntletJumpTicks || 0,
gauntletScore: p.gauntletScore || 0,
gauntletEliminated: !!p.gauntletEliminated,
}));
io.to(sid).emit('gauntlet-sync', {
obstacles: obsOut,
players: playersOut,
gauntletTickMs: getGauntletTickMs(),
gauntletJumpTicks: getGauntletJumpTicks(),
gauntletTimeLimitSec: getGauntletTimeLimitSec(),
gauntletEndsAt: gr.endsAt != null ? gr.endsAt : null,
gauntletCrownRunHeld: !!gr.crownRunHeld,
...getGauntletVisualsForClient(),
});
}
function startGauntletTicker(sid, space) {
const md = (space.mapId && maps.get(space.mapId)) || space.mapData;
if (!md || md.gameType !== 'gauntlet') return;
if (space.gauntletRun && space.gauntletRun.timerId) return;
if (!space.gauntletRun) {
space.gauntletRun = { obstacles: [], nextObsId: 1, spawnAcc: 0, nextSpawnIn: 3 };
space.gauntletRun = newGauntletRunState(space);
}
ensureGauntletEndsAtIfNeeded(space);
space.gauntletRun.timerId = setInterval(() => {
@@ -2660,7 +2708,13 @@ function initGauntletPeersAll(space, md) {
p.gauntletJumpPending = false;
p.gauntletEliminated = false;
});
space.gauntletRun = { obstacles: [], nextObsId: 1, spawnAcc: 0, nextSpawnIn: 3 };
space.gauntletRun = newGauntletRunState(space);
if (crown) {
if (!space.gauntletCrownLobbyReady || typeof space.gauntletCrownLobbyReady !== 'object') space.gauntletCrownLobbyReady = {};
space.peers.forEach((_p, id) => {
space.gauntletCrownLobbyReady[id] = false;
});
}
space.gauntletPreviewRowsBySocket = new Map();
}
@@ -2777,6 +2831,14 @@ function runGauntletTick(sid, space) {
return;
}
if (gr.crownRunHeld) {
space.peers.forEach((p) => {
p.gauntletJumpPending = false;
});
emitGauntletSync(sid, space);
return;
}
/* ใช้ pending ให้ jump กับ collision อยู่รอบ tick เดียวกัน (กัน race ระหว่าง setInterval กับ socket) */
space.peers.forEach((p) => {
if (p.gauntletJumpPending) {
@@ -2882,33 +2944,7 @@ function runGauntletTick(sid, space) {
p.y = py;
});
const obsOut = gr.obstacles.map((o) => {
if (o.kind === 'laser') {
const y0 = o.y0 != null && Number.isFinite(Number(o.y0)) ? Math.floor(Number(o.y0)) : 0;
const y1 = o.y1 != null && Number.isFinite(Number(o.y1)) ? Math.floor(Number(o.y1)) : h - 1;
return { id: o.id, kind: 'laser', x: o.x, y: null, y0, y1 };
}
return { id: o.id, kind: o.kind, x: o.x, y: o.y };
});
const playersOut = [...space.peers.values()].map((p) => ({
id: p.id,
x: p.x,
y: p.y,
direction: p.direction || 'down',
characterId: p.characterId ?? null,
gauntletJumpTicks: p.gauntletJumpTicks || 0,
gauntletScore: p.gauntletScore || 0,
gauntletEliminated: !!p.gauntletEliminated,
}));
io.to(sid).emit('gauntlet-sync', {
obstacles: obsOut,
players: playersOut,
gauntletTickMs: getGauntletTickMs(),
gauntletJumpTicks: getGauntletJumpTicks(),
gauntletTimeLimitSec: getGauntletTimeLimitSec(),
gauntletEndsAt: gr.endsAt != null ? gr.endsAt : null,
...getGauntletVisualsForClient(),
});
emitGauntletSync(sid, space);
}
function rescheduleAllGauntletTickers() {
@@ -2921,7 +2957,7 @@ function rescheduleAllGauntletTickers() {
}
if (space.peers.size === 0) continue;
if (!space.gauntletRun) {
space.gauntletRun = { obstacles: [], nextObsId: 1, spawnAcc: 0, nextSpawnIn: 3 };
space.gauntletRun = newGauntletRunState(space);
}
ensureGauntletEndsAtIfNeeded(space);
space.gauntletRun.timerId = setInterval(() => {
@@ -3189,6 +3225,9 @@ io.on('connection', (socket) => {
if (isGauntletCrownHeistMapBySpace(space)) {
peer.gauntletScore = 100;
peer.gauntletEliminated = false;
if (!space.gauntletCrownLobbyReady || typeof space.gauntletCrownLobbyReady !== 'object') space.gauntletCrownLobbyReady = {};
space.gauntletCrownLobbyReady[socket.id] = false;
io.to(spaceId).emit('gauntlet-crown-lobby-sync', { readyMap: { ...space.gauntletCrownLobbyReady } });
}
startGauntletTicker(spaceId, space);
}
@@ -3469,12 +3508,7 @@ io.on('connection', (socket) => {
stopGauntletTicker(space);
space.gauntletReturnMapId = prevMapIdForReturn;
initGauntletPeersAll(space, md);
const lim = getGauntletTimeLimitSec();
if (lim > 0 && space.gauntletRun) {
space.gauntletRun.endsAt = Date.now() + lim * 1000;
} else if (space.gauntletRun) {
space.gauntletRun.endsAt = null;
}
ensureGauntletEndsAtIfNeeded(space);
startGauntletTicker(sid, space);
gauntletEndsAtEmit = space.gauntletRun && space.gauntletRun.endsAt != null
? space.gauntletRun.endsAt
@@ -3527,6 +3561,12 @@ io.on('connection', (socket) => {
io.to(sid).emit('quiz-carry-lobby-sync', { readyMap: { ...space.quizCarryLobbyReady } });
}
}
if (space.gauntletCrownLobbyReady && typeof space.gauntletCrownLobbyReady === 'object') {
delete space.gauntletCrownLobbyReady[socket.id];
if (isGauntletCrownHeistMapBySpace(space)) {
io.to(sid).emit('gauntlet-crown-lobby-sync', { readyMap: { ...space.gauntletCrownLobbyReady } });
}
}
space.peers.delete(socket.id);
if (space.peers.size === 0) {
stopGauntletTicker(space);
@@ -3552,11 +3592,31 @@ io.on('connection', (socket) => {
const p = space.peers.get(socket.id);
const md = (space.mapId && maps.get(space.mapId)) || space.mapData;
if (!md || md.gameType !== 'gauntlet') return;
if (space.gauntletRun && space.gauntletRun.crownRunHeld) return;
if (isGauntletCrownHeistMapBySpace(space) && p.gauntletEliminated) return;
if ((p.gauntletJumpTicks || 0) > 0) return;
p.gauntletJumpPending = true;
});
socket.on('gauntlet-crown-begin-run', (cb) => {
const reply = typeof cb === 'function' ? cb : () => {};
const sid = socket.data.spaceId;
const space = sid ? spaces.get(sid) : null;
if (!space || !space.peers.has(socket.id)) return reply({ ok: false });
const gr = space.gauntletRun;
if (!gr) return reply({ ok: false });
const md = (space.mapId && maps.get(space.mapId)) || space.mapData;
if (!md || md.gameType !== 'gauntlet' || !isGauntletCrownHeistMapBySpace(space)) return reply({ ok: false });
const previewRoom = !!(space.previewEditorTest
|| (space.isPrivate && String(space.spaceName || '').toLowerCase() === 'preview'));
if (!previewRoom && space.hostId !== socket.id) return reply({ ok: false, error: 'host' });
if (!gr.crownRunHeld) return reply({ ok: true, already: true });
gr.crownRunHeld = false;
ensureGauntletEndsAtIfNeeded(space);
emitGauntletSync(sid, space);
return reply({ ok: true });
});
/** Client ส่งแถว y ของบอททดสอบ (ไม่อยู่ใน peers) เพื่อให้ spawn lane obstacles บนแถวนั้น */
socket.on('gauntlet-preview-rows', (data) => {
const sid = socket.data.spaceId;
@@ -3564,6 +3624,7 @@ io.on('connection', (socket) => {
if (!space || !space.peers.has(socket.id)) return;
const md = (space.mapId && maps.get(space.mapId)) || space.mapData;
if (!md || md.gameType !== 'gauntlet') return;
if (space.gauntletRun && space.gauntletRun.crownRunHeld) return;
const hh = md.height || 15;
if (!space.gauntletPreviewRowsBySocket) space.gauntletPreviewRowsBySocket = new Map();
const raw = data && Array.isArray(data.ys) ? data.ys : [];
@@ -3618,6 +3679,50 @@ io.on('connection', (socket) => {
reply({ ok: true });
});
socket.on('gauntlet-crown-lobby-sync-request', () => {
const sid = socket.data.spaceId;
const space = sid ? spaces.get(sid) : null;
if (!space || !space.peers.has(socket.id)) return;
if (!isGauntletCrownHeistMapBySpace(space)) return;
if (!space.gauntletCrownLobbyReady || typeof space.gauntletCrownLobbyReady !== 'object') {
space.gauntletCrownLobbyReady = {};
}
for (const id of space.peers.keys()) {
if (space.gauntletCrownLobbyReady[id] === undefined) space.gauntletCrownLobbyReady[id] = false;
}
socket.emit('gauntlet-crown-lobby-sync', { readyMap: { ...space.gauntletCrownLobbyReady } });
});
socket.on('gauntlet-crown-lobby-ready', (data) => {
const sid = socket.data.spaceId;
const space = sid ? spaces.get(sid) : null;
if (!space || !space.peers.has(socket.id)) return;
if (!isGauntletCrownHeistMapBySpace(space)) return;
if (!space.gauntletCrownLobbyReady || typeof space.gauntletCrownLobbyReady !== 'object') space.gauntletCrownLobbyReady = {};
for (const id of space.peers.keys()) {
if (space.gauntletCrownLobbyReady[id] === undefined) space.gauntletCrownLobbyReady[id] = false;
}
space.gauntletCrownLobbyReady[socket.id] = !!(data && data.ready);
io.to(sid).emit('gauntlet-crown-lobby-sync', { readyMap: { ...space.gauntletCrownLobbyReady } });
});
socket.on('gauntlet-crown-lobby-start', (_data, cb) => {
const reply = typeof cb === 'function' ? cb : () => {};
const sid = socket.data.spaceId;
const space = sid ? spaces.get(sid) : null;
if (!space || !space.peers.has(socket.id)) return reply({ ok: false, error: 'ไม่อยู่ในห้อง' });
if (!isGauntletCrownHeistMapBySpace(space)) return reply({ ok: false, error: 'ไม่ใช่แมป crown' });
if (space.hostId !== socket.id) return reply({ ok: false, error: 'เฉพาะโฮสต์กด START' });
const gr = space.gauntletRun;
if (!gr || !gr.crownRunHeld) return reply({ ok: false, error: 'เริ่มแล้ว' });
const rm = space.gauntletCrownLobbyReady || {};
const humanIds = [...space.peers.keys()];
const allReady = humanIds.length > 0 && humanIds.every((id) => rm[id]);
if (!allReady) return reply({ ok: false, error: 'ยังมีผู้เล่นที่ยังไม่ Ready' });
io.to(sid).emit('gauntlet-crown-lobby-started', {});
reply({ ok: true });
});
socket.on('move', (data) => {
for (const [sid, space] of spaces) {
if (space.peers.has(socket.id)) {