minigame 2 just begin1.6
This commit is contained in:
@@ -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
@@ -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,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;
|
||||
|
||||
@@ -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: เฉลี่ย 80–100 · B: 60–79 · C: 0–59</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
@@ -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
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user