minigame 7 Mega Virus 1.4

This commit is contained in:
2026-05-04 04:20:56 +00:00
parent 4b5fc93aca
commit 7e3e935e2d
2 changed files with 365 additions and 32 deletions
+301 -25
View File
@@ -29,6 +29,8 @@
const QUIZ_QUESTION_MISSION_MAP_ID = 'mng8a80o';
/** Stack ซ้อนตึก — ฉากภารกิจ (HOWTO → นับถอยหลัง → เล่น → สรุป) รูปใน `/Game/img/TowerBlock` */
const STACK_TOWER_MISSION_MAP_ID = 'mnn93hpi';
/** Mega Virus — balloon_boss ฉากภารกิจ (flow เดียวกับ crown / mno9kb07) รูปใน `/Game/img/MegaVirus` */
const BALLOON_BOSS_MISSION_MAP_ID = 'mnq1eml7';
/** ครบชั้นนี้ขึ้นไป — ยกจุดแกว่ง/เชือกขึ้น (world px) ป้องกันชนกองสูง (เฉพาะ Tower mission) */
const STACK_TOWER_SWING_LIFT_FROM_LAYER = 9;
/** Stack — เกณฑ์ใกล้ความจริง: ทับมากขึ้น, จำกัดการเยื้องจากกลางฐาน, ทับน้อยแล้วโคลงง่ายขึ้น */
@@ -3428,9 +3430,16 @@
function isGauntletCrownHeistMapPlay() {
return isGauntletFaceRightMapMno9kb07();
}
function isMegaVirusMissionShellMapPlay() {
return !!(mapData && mapData.gameType === 'balloon_boss' && currentPlayMapId() === BALLOON_BOSS_MISSION_MAP_ID);
}
/** Crown gauntlet หรือ Mega Virus — lobby HOWTO / Ready / นับถอยหลัง / held run */
function usesCrownLobbyShellPlay() {
return isGauntletCrownHeistMapPlay() || isMegaVirusMissionShellMapPlay();
}
let gauntletCrownHowtoVisible = false;
function isGauntletCrownPregameBlockingPlay() {
if (!isGauntletCrownHeistMapPlay()) return false;
if (!usesCrownLobbyShellPlay()) return false;
if (gauntletCrownRunHeldRemote) return true;
if (gauntletCrownPregamePhase != null && gauntletCrownPregamePhase !== 'live') return true;
return false;
@@ -4227,6 +4236,29 @@
return BASE + '/img/ViolentCrime/' + String(file || '').replace(/^\//, '');
}
function megaVirusAssetUrl(file) {
return BASE + '/img/MegaVirus/' + String(file || '').replace(/^\//, '');
}
function applyMegaVirusMissionPanelImages() {
const howtoBg = document.querySelector('#gauntlet-crown-howto-overlay .gch-bg');
if (howtoBg) {
howtoBg.src = megaVirusAssetUrl('popup-Howto.png');
howtoBg.onerror = function () {
this.onerror = null;
this.src = BASE + '/img/gauntlet-assets/popup-Howto.png';
};
}
const resBg = document.querySelector('#gauntlet-crown-mission-overlay .gcm-bg');
if (resBg) {
resBg.src = megaVirusAssetUrl('popup-result.png');
resBg.onerror = function () {
this.onerror = null;
this.src = BASE + '/img/gauntlet-assets/popup-result.png';
};
}
}
function applySpaceShooterMissionPanelImages() {
const howtoBg = document.querySelector('#gauntlet-crown-howto-overlay .gch-bg');
if (howtoBg) {
@@ -4475,6 +4507,149 @@
};
}
function balloonBossMergePreviewBotsMission(mission) {
if (!mission || mission.uiSkin !== 'mega_virus' || !previewFillBots || !others || typeof others.forEach !== 'function') return mission;
const seen = new Set();
(mission.ranked || []).forEach(function (r) {
if (r && r.id != null) seen.add(String(r.id));
});
const baseRows = (mission.ranked || []).map(function (r) {
return {
id: r.id,
nickname: r.nickname,
characterId: r.characterId,
baseScore: Math.max(0, Number(r.baseScore) || 0),
eliminated: !!r.eliminated,
rankBonus: 0,
finalScore: Math.max(0, Number(r.finalScore != null ? r.finalScore : r.baseScore) || 0),
};
});
others.forEach(function (o, id) {
if (!o || !isPreviewBotId(id)) return;
const sid = String(id);
if (seen.has(sid)) return;
seen.add(sid);
baseRows.push({
id: id,
nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : sid,
characterId: o.characterId ?? null,
baseScore: Math.max(0, Number(o.balloonBossScore) || 0),
eliminated: !!o.balloonBossEliminated,
rankBonus: 0,
finalScore: Math.max(0, Number(o.balloonBossScore) || 0),
});
});
baseRows.sort(function (a, b) {
if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore;
return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id));
});
const top = baseRows.slice(0, 5);
const ranked = top.map(function (row, idx) {
const pos = idx + 1;
const bs = Math.max(0, Number(row.baseScore) || 0);
return {
id: row.id,
nickname: row.nickname,
characterId: row.characterId,
baseScore: bs,
eliminated: !!row.eliminated,
rank: pos,
rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos),
rankBonus: 0,
finalScore: bs,
};
});
const totalParts = ranked.map(function (r) { return r.baseScore; });
const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0);
const n2 = ranked.length || 1;
const averageScore = Math.floor(totalSum / n2);
const maxHp = balloonBossMaxHpPlay();
const progress = Math.min(1, totalSum / Math.max(1, maxHp));
const score100 = Math.min(100, Math.floor(progress * 100));
let grade = 'C';
if (progress >= 1) grade = 'A';
else if (score100 >= 50) grade = 'B';
else if (totalSum <= 0) grade = 'F';
const rewardCard = grade === 'F' ? null : gauntletCrownRollRewardCardLocal(grade);
return {
ranked: ranked,
totalSum: totalSum,
averageScore: averageScore,
grade: grade,
rewardCard: rewardCard,
totalParts: totalParts,
uiSkin: 'mega_virus',
survivorCount: baseRows.filter(function (r) { return !r.eliminated; }).length,
participantCount: baseRows.length,
balloonBossEndReason: mission.balloonBossEndReason,
};
}
function balloonBossBuildMissionPayloadPlay(reason) {
const rows = [];
if (myId != null) {
rows.push({
id: myId,
nickname: ((me.nickname || nick || 'คุณ').trim() || 'คุณ'),
characterId: me.characterId ?? null,
baseScore: Math.max(0, me.balloonBossScore | 0),
eliminated: !!me.balloonBossEliminated,
});
}
others.forEach((o, id) => {
rows.push({
id: id,
nickname: (o && o.nickname) ? String(o.nickname).trim() : id,
characterId: o && o.characterId != null ? o.characterId : null,
baseScore: Math.max(0, o.balloonBossScore | 0),
eliminated: !!(o && o.balloonBossEliminated),
});
});
rows.sort((a, b) => b.baseScore - a.baseScore
|| String(a.nickname).localeCompare(String(b.nickname), 'th')
|| String(a.id).localeCompare(String(b.id)));
const top = rows.slice(0, 5);
const ranked = top.map((row, idx) => {
const pos = idx + 1;
const bs = Math.max(0, Number(row.baseScore) || 0);
return {
id: row.id,
nickname: row.nickname,
characterId: row.characterId,
baseScore: bs,
eliminated: !!row.eliminated,
rank: pos,
rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos),
rankBonus: 0,
finalScore: bs,
};
});
const totalParts = ranked.map((r) => r.baseScore);
const totalSum = totalParts.reduce((s, n) => s + n, 0);
const n2 = ranked.length || 1;
const averageScore = Math.floor(totalSum / n2);
const maxHp = balloonBossMaxHpPlay();
const progress = Math.min(1, totalSum / Math.max(1, maxHp));
const score100 = Math.min(100, Math.floor(progress * 100));
let grade = 'C';
if (reason === 'victory' || progress >= 1) grade = 'A';
else if (score100 >= 50) grade = 'B';
else if (totalSum <= 0) grade = 'F';
const rewardCard = grade === 'F' ? null : gauntletCrownRollRewardCardLocal(grade);
return {
ranked: ranked,
totalSum: totalSum,
averageScore: averageScore,
grade: grade,
rewardCard: rewardCard,
totalParts: totalParts,
uiSkin: 'mega_virus',
survivorCount: rows.filter((r) => !r.eliminated).length,
participantCount: rows.length,
balloonBossEndReason: reason,
};
}
function endSpaceShooterMissionRound() {
if (!isSpaceShooterMissionUiMapPlay() || spaceShooterGameEnded) return;
spaceShooterGameEnded = true;
@@ -7434,7 +7609,7 @@
}
if (Object.prototype.hasOwnProperty.call(payload, 'gauntletCrownRunHeld')) {
gauntletCrownRunHeldRemote = !!payload.gauntletCrownRunHeld;
if (!gauntletCrownRunHeldRemote && isGauntletCrownHeistMapPlay()) {
if (!gauntletCrownRunHeldRemote && usesCrownLobbyShellPlay()) {
gauntletCrownPregamePhase = 'live';
}
}
@@ -7920,7 +8095,7 @@
function updateGauntletCrownHowtoHud() {
const st = document.getElementById('gauntlet-crown-howto-status');
const btn = document.getElementById('btn-gch-ready');
if (!btn || !isGauntletCrownHeistMapPlay() || gauntletCrownPregamePhase !== 'howto') return;
if (!btn || !usesCrownLobbyShellPlay() || gauntletCrownPregamePhase !== 'howto') return;
const humans = quizCarryPregameHumanIds();
const tot = Math.max(1, quizCarryPregameTotalPlayers());
const num = gauntletCrownPregameReadyNumerator();
@@ -7939,7 +8114,7 @@
}
function beginGauntletCrownCountdownThenRun() {
if (!isGauntletCrownHeistMapPlay()) return;
if (!usesCrownLobbyShellPlay()) return;
if (gauntletCrownCountdownTimer) {
clearTimeout(gauntletCrownCountdownTimer);
gauntletCrownCountdownTimer = null;
@@ -7950,6 +8125,24 @@
gauntletCrownHowtoVisible = false;
const cd = document.getElementById('gauntlet-crown-countdown');
const numEl = document.getElementById('gauntlet-crown-countdown-num');
function applyMegaBalloonLiveStartPlay() {
balloonBossSessionStartMs = performance.now();
balloonBossLastTickMs = performance.now();
balloonBossGameEnded = false;
balloonBossPendingShots = [];
balloonBossPlayerBullets = [];
balloonBossBossBullets = [];
balloonBossBossFireAcc = 0;
me.balloonBossScore = 0;
me.balloonBossBalloons = balloonBossBalloonsStartPlay();
me.balloonBossEliminated = false;
others.forEach((o) => {
o.balloonBossScore = 0;
o.balloonBossBalloons = balloonBossBalloonsStartPlay();
o.balloonBossEliminated = false;
});
if (mapData) applyBalloonBossSpawnLayoutPlay();
}
const runFinish = () => {
if (cd) cd.classList.add('is-hidden');
if (gauntletCrownCountdownTimer) {
@@ -7958,10 +8151,14 @@
}
if (socket && socket.connected) {
socket.emit('gauntlet-crown-begin-run', (res) => {
if (res && res.ok) gauntletCrownPregamePhase = 'live';
if (res && res.ok) {
gauntletCrownPregamePhase = 'live';
if (isMegaVirusMissionShellMapPlay()) applyMegaBalloonLiveStartPlay();
}
});
} else {
gauntletCrownPregamePhase = 'live';
if (isMegaVirusMissionShellMapPlay()) applyMegaBalloonLiveStartPlay();
}
};
if (!cd || !numEl) {
@@ -7985,8 +8182,9 @@
}
function showGauntletCrownHowtoOverlay() {
if (!isGauntletCrownHeistMapPlay()) return;
if (!usesCrownLobbyShellPlay()) return;
hideConflictingOverlaysForGauntletCrown();
if (isMegaVirusMissionShellMapPlay()) applyMegaVirusMissionPanelImages();
const cd = document.getElementById('gauntlet-crown-countdown');
if (cd) cd.classList.add('is-hidden');
if (gauntletCrownCountdownTimer) {
@@ -8020,6 +8218,10 @@
const g = String(mission.grade || '').trim().toUpperCase().charAt(0);
return g !== 'F';
}
if (mission.uiSkin === 'mega_virus') {
const g = String(mission.grade || '').trim().toUpperCase().charAt(0);
return g !== 'F';
}
const g = String(mission.grade || '').trim().toUpperCase().charAt(0);
return g !== 'F';
}
@@ -8053,7 +8255,7 @@
*/
function gauntletCrownBuildDisplayMission(mission) {
if (!mission || !Array.isArray(mission.ranked)) return mission;
if (mission.uiSkin === 'jumper' || mission.uiSkin === 'violent_crime' || mission.uiSkin === 'question_mission' || mission.uiSkin === 'stack_tower') return mission;
if (mission.uiSkin === 'jumper' || mission.uiSkin === 'violent_crime' || mission.uiSkin === 'question_mission' || mission.uiSkin === 'stack_tower' || mission.uiSkin === 'mega_virus') return mission;
if (!(previewFillBots && isGauntletCrownHeistMapPlay() && others && typeof others.forEach === 'function')) return mission;
const seen = new Set();
@@ -8142,6 +8344,8 @@
disp = quizQuestionMissionMergePreviewBotsMission(disp) || disp;
} else if (mission.uiSkin === 'stack_tower') {
disp = stackTowerMissionMergePreviewBotsMission(disp) || disp;
} else if (mission.uiSkin === 'mega_virus') {
disp = balloonBossMergePreviewBotsMission(disp) || disp;
}
if (!disp || !Array.isArray(disp.ranked)) return;
rowEl.innerHTML = '';
@@ -8206,10 +8410,11 @@
nick.textContent = r.nickname || '—';
const sc = document.createElement('div');
sc.className = 'gcm-sc';
const jumpPlain = mission && (mission.uiSkin === 'jumper' || mission.uiSkin === 'violent_crime' || mission.uiSkin === 'question_mission' || mission.uiSkin === 'stack_tower');
const jumpPlain = mission && (mission.uiSkin === 'jumper' || mission.uiSkin === 'violent_crime' || mission.uiSkin === 'question_mission' || mission.uiSkin === 'stack_tower' || mission.uiSkin === 'mega_virus');
const botScorePlain = (previewFillBots && isGauntletCrownHeistMapPlay() && isPreviewBotId(r.id))
|| (previewFillBots && mission && mission.uiSkin === 'question_mission' && isPreviewBotId(r.id))
|| (previewFillBots && mission && mission.uiSkin === 'stack_tower' && isPreviewBotId(r.id));
|| (previewFillBots && mission && mission.uiSkin === 'stack_tower' && isPreviewBotId(r.id))
|| (previewFillBots && mission && mission.uiSkin === 'mega_virus' && isPreviewBotId(r.id));
sc.textContent = (jumpPlain || botScorePlain)
? String(Math.max(0, Number(r.baseScore) || 0))
: String(r.baseScore || 0) + '(+' + String(r.rankBonus || 0) + ')';
@@ -8240,6 +8445,9 @@
} else if (mission && mission.uiSkin === 'stack_tower') {
const pc = Math.max(0, Math.floor(Number(disp.participantCount) || 0));
totalEl.innerHTML = 'คะแนนรวม <span class="gcm-total-nums">(' + parts.join('+') + ') = ' + sum + '</span> · ผู้เล่น ' + pc + ' → เกรด';
} else if (mission && mission.uiSkin === 'mega_virus') {
const pc = Math.max(0, Math.floor(Number(disp.participantCount) || 0));
totalEl.innerHTML = 'ความเสียหายรวม <span class="gcm-total-nums">(' + parts.join('+') + ') = ' + sum + '</span> · ผู้เล่น ' + pc + ' → เกรด';
} else {
totalEl.innerHTML = 'คะแนนรวม <span class="gcm-total-nums">(' + parts.join('+') + ') = ' + sum + '</span> · เฉลี่ย ' + avg + ' → เกรด';
}
@@ -8260,9 +8468,10 @@
row.style.gap = '8px';
const chk = document.createElement('img');
const stackTS = mission && mission.uiSkin === 'stack_tower';
chk.src = stackTS ? stackTowerAssetUrl('result-check.png') : (BASE + '/img/gauntlet-assets/result-check.png');
const megaVs = mission && mission.uiSkin === 'mega_virus';
chk.src = stackTS ? stackTowerAssetUrl('result-check.png') : (megaVs ? megaVirusAssetUrl('result-check.png') : (BASE + '/img/gauntlet-assets/result-check.png'));
chk.alt = '';
if (stackTS) {
if (stackTS || megaVs) {
chk.onerror = function () {
this.onerror = null;
this.src = BASE + '/img/gauntlet-assets/result-check.png';
@@ -8295,7 +8504,7 @@
}
const gcmHead = document.getElementById('gcm-heading');
if (gcmHead) {
if (mission && (mission.uiSkin === 'question_mission' || mission.uiSkin === 'stack_tower')) gcmHead.classList.add('sr-only');
if (mission && (mission.uiSkin === 'question_mission' || mission.uiSkin === 'stack_tower' || mission.uiSkin === 'mega_virus')) gcmHead.classList.add('sr-only');
else gcmHead.classList.remove('sr-only');
}
ov.classList.remove('is-hidden');
@@ -8309,7 +8518,7 @@
if (previewMode && editorEmbedReturn) {
ov.classList.add('is-hidden');
if (gcmHead) gcmHead.classList.remove('sr-only');
if (mission && (mission.uiSkin === 'question_mission' || mission.uiSkin === 'stack_tower')) {
if (mission && (mission.uiSkin === 'question_mission' || mission.uiSkin === 'stack_tower' || mission.uiSkin === 'mega_virus')) {
cancelQuizCarryResultEndAfterTimeup();
hideQuizCarryTimeupOnDeskLayer();
hideQuizCarryResultEndLayer();
@@ -9447,6 +9656,8 @@
function balloonBossRemainingSecPlay() {
const lim = balloonBossTimeLimitSecPlay();
if (lim <= 0) return null;
if (isMegaVirusMissionShellMapPlay() && isGauntletCrownPregameBlockingPlay()) return lim;
if (!balloonBossSessionStartMs) return lim;
const elapsed = (performance.now() - balloonBossSessionStartMs) / 1000;
return Math.max(0, Math.ceil(lim - elapsed));
}
@@ -9521,6 +9732,11 @@
balloonBossPendingShots = [];
balloonBossPlayerBullets = [];
balloonBossBossBullets = [];
if (isMegaVirusMissionShellMapPlay()) {
applyMegaVirusMissionPanelImages();
showGauntletCrownMissionOverlay(balloonBossBuildMissionPayloadPlay(reason));
return;
}
const ov = document.getElementById('gauntlet-ended-overlay');
const msgEl = document.getElementById('gauntlet-ended-message');
const titleEl = document.getElementById('gauntlet-ended-title');
@@ -9597,6 +9813,7 @@
function balloonBossTickFrame() {
if (!mapData || mapData.gameType !== 'balloon_boss') return;
if (isMegaVirusMissionShellMapPlay() && isGauntletCrownPregameBlockingPlay()) return;
const w = mapData.width || 20, h = mapData.height || 15;
const ts = tileSize;
const mw = w * ts, mh = h * ts;
@@ -10884,8 +11101,13 @@
if (mapData.gameType !== 'quiz_carry') {
quizCarryWalkSpeedMultActive = QUIZ_CARRY_WALK_SPEED_MULT;
}
if (res && res.mapId != null && String(res.mapId).trim() !== '') {
playSessionMapId = String(res.mapId).trim();
{
const qMap = (playMapIdFromQuery || '').trim();
const dataId = mapData && mapData.id != null && String(mapData.id).trim() !== '' ? String(mapData.id).trim() : '';
const resMap = res && res.mapId != null && String(res.mapId).trim() !== '' ? String(res.mapId).trim() : '';
if (qMap) playSessionMapId = qMap;
else if (dataId) playSessionMapId = dataId;
else if (resMap) playSessionMapId = resMap;
}
if (res && res.hostId != null) playHostId = res.hostId;
if (!isQuizQuestionMissionUiMapPlay()) {
@@ -11208,6 +11430,20 @@
const gccR = document.getElementById('gauntlet-crown-countdown');
if (gccR) gccR.classList.add('is-hidden');
setTimeout(function () { showGauntletCrownHowtoOverlay(); }, 50);
} else if (mapData.gameType === 'balloon_boss' && isMegaVirusMissionShellMapPlay()) {
hideConflictingOverlaysForGauntletCrown();
gauntletCrownPregamePhase = null;
gauntletCrownLobbyReadyMap = {};
if (gauntletCrownCountdownTimer) {
clearTimeout(gauntletCrownCountdownTimer);
gauntletCrownCountdownTimer = null;
}
const gccRm = document.getElementById('gauntlet-crown-countdown');
if (gccRm) gccRm.classList.add('is-hidden');
if (res && Object.prototype.hasOwnProperty.call(res, 'gauntletCrownRunHeld')) {
applyGauntletTimingFromServer({ gauntletCrownRunHeld: res.gauntletCrownRunHeld });
}
setTimeout(function () { showGauntletCrownHowtoOverlay(); }, 50);
} else if (mapData.gameType === 'jump_survive' && isJumpSurviveMissionUiMapPlay()) {
if (gauntletCrownCountdownTimer) {
clearTimeout(gauntletCrownCountdownTimer);
@@ -11385,7 +11621,7 @@
}
});
socket.on('gauntlet-crown-lobby-started', () => {
if (!isGauntletCrownHeistMapPlay()) return;
if (!usesCrownLobbyShellPlay()) return;
beginGauntletCrownCountdownThenRun();
});
socket.on('user-move', (data) => {
@@ -11573,7 +11809,12 @@
});
socket.on('gauntlet-sync', (data) => {
if (!mapData || mapData.gameType !== 'gauntlet' || !data) return;
if (!data || typeof data !== 'object') return;
if (isMegaVirusMissionShellMapPlay()) {
applyGauntletTimingFromServer(data);
return;
}
if (!mapData || mapData.gameType !== 'gauntlet') return;
applyGauntletTimingFromServer(data);
if (Array.isArray(data.obstacles)) {
gauntletObstacles = data.obstacles;
@@ -11795,10 +12036,12 @@
}
} else {
gauntletEndsAtMs = null;
const megaShellGs = mapData.gameType === 'balloon_boss' && String(ev.mapId || '').trim() === BALLOON_BOSS_MISSION_MAP_ID;
const skipHideCrownShell = (mapData.gameType === 'jump_survive' && isJumpSurviveMissionUiMapPlay())
|| (mapData.gameType === 'space_shooter' && isSpaceShooterMissionUiMapPlay())
|| (mapData.gameType === 'quiz' && isQuizQuestionMissionUiMapPlay())
|| (mapData.gameType === 'stack' && isStackTowerMissionUiMapPlay());
|| (mapData.gameType === 'stack' && isStackTowerMissionUiMapPlay())
|| megaShellGs;
if (!skipHideCrownShell) {
const hto3 = document.getElementById('gauntlet-crown-howto-overlay');
if (hto3) hto3.classList.add('is-hidden');
@@ -11927,7 +12170,6 @@
balloonBossHitFx = [];
balloonBossLastTickMs = performance.now();
balloonBossPlayerFireCd = 0;
balloonBossSessionStartMs = performance.now();
balloonBossLastMoveEmit = 0;
balloonBossBossFireAcc = 0;
me.balloonBossScore = 0;
@@ -11938,10 +12180,36 @@
o.balloonBossBalloons = balloonBossBalloonsStartPlay();
o.balloonBossEliminated = false;
});
fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' })
.then((r) => (r.ok ? r.json() : null))
.then((t) => { if (t) applyGauntletTimingFromServer(t); })
.catch(() => {});
if (String(ev.mapId || '').trim() === BALLOON_BOSS_MISSION_MAP_ID) {
balloonBossSessionStartMs = 0;
if (Object.prototype.hasOwnProperty.call(ev, 'gauntletCrownRunHeld')) {
applyGauntletTimingFromServer({ gauntletCrownRunHeld: ev.gauntletCrownRunHeld });
}
fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' })
.then((r) => (r.ok ? r.json() : null))
.then((t) => {
if (t) applyGauntletTimingFromServer(t);
if (Object.prototype.hasOwnProperty.call(ev, 'gauntletCrownRunHeld')) {
applyGauntletTimingFromServer({ gauntletCrownRunHeld: ev.gauntletCrownRunHeld });
}
})
.catch(() => {});
gauntletCrownPregamePhase = null;
gauntletCrownLobbyReadyMap = {};
if (gauntletCrownCountdownTimer) {
clearTimeout(gauntletCrownCountdownTimer);
gauntletCrownCountdownTimer = null;
}
const gccMv = document.getElementById('gauntlet-crown-countdown');
if (gccMv) gccMv.classList.add('is-hidden');
setTimeout(function () { showGauntletCrownHowtoOverlay(); }, 60);
} else {
balloonBossSessionStartMs = performance.now();
fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' })
.then((r) => (r.ok ? r.json() : null))
.then((t) => { if (t) applyGauntletTimingFromServer(t); })
.catch(() => {});
}
}
if (mapData.gameType === 'quiz_battle') {
if (!mapData.quizBattleDomeArea) mapData.quizBattleDomeArea = [];
@@ -13124,7 +13392,9 @@
timeVal.textContent = rem != null ? String(rem) : '∞';
}
} else if (isBalloonBoss()) {
if (balloonBossGameEnded) {
if (isMegaVirusMissionShellMapPlay() && isGauntletCrownPregameBlockingPlay()) {
timeVal.textContent = '···';
} else if (balloonBossGameEnded) {
timeVal.textContent = '0';
} else {
const rem = balloonBossRemainingSecPlay();
@@ -14124,6 +14394,12 @@
}
return;
}
if (isBalloonBoss() && isMegaVirusMissionShellMapPlay() && isGauntletCrownPregameBlockingPlay() && mapData && !isChatFocused()) {
if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) {
e.preventDefault();
}
return;
}
if ((isSpaceShooter() || isBalloonBoss()) && mapData && !isChatFocused()) {
if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) {
e.preventDefault();
@@ -14500,7 +14776,7 @@
beginStackTowerMissionCountdownThenRun();
return;
}
if (!isGauntletCrownHeistMapPlay()) return;
if (!usesCrownLobbyShellPlay()) return;
if (gauntletCrownPregamePhase !== 'howto') return;
if (myId == null || !isMePlayHost()) return;
const humans = quizCarryPregameHumanIds();
+64 -7
View File
@@ -2895,11 +2895,43 @@ const GAUNTLET_LASER_SPAWN_CHANCE = 0.14;
const GAUNTLET_LANE_ROW_SPAWN_CHANCE = 0.34;
/** Last Light / museum gauntlet — scoring, mission UI, how-to flow */
const GAUNTLET_CROWN_HEIST_MAP_ID = 'mno9kb07';
/** Mega Virus — balloon_boss ฉากภารกิจ (flow เดียวกับ crown) */
const BALLOON_BOSS_MISSION_MAP_ID = 'mnq1eml7';
function isGauntletCrownHeistMapBySpace(space) {
return !!(space && space.mapId && String(space.mapId) === GAUNTLET_CROWN_HEIST_MAP_ID);
}
function isBalloonBossMissionShellSpace(space) {
if (!space || !space.mapId || String(space.mapId) !== BALLOON_BOSS_MISSION_MAP_ID) return false;
const md = (space.mapId && maps.get(space.mapId)) || space.mapData;
return !!(md && md.gameType === 'balloon_boss');
}
function isCrownLobbyShellSpace(space) {
return isGauntletCrownHeistMapBySpace(space) || isBalloonBossMissionShellSpace(space);
}
function newBalloonBossShellGauntletRunState() {
return {
obstacles: [],
nextObsId: 1,
spawnAcc: 0,
nextSpawnIn: 999,
crownRunHeld: true,
timerId: null,
endsAt: null,
};
}
function emitBalloonBossMissionShellGauntletSync(sid, space) {
if (!isBalloonBossMissionShellSpace(space)) return;
const gr = space.gauntletRun;
io.to(sid).emit('gauntlet-sync', {
gauntletCrownRunHeld: !!(gr && gr.crownRunHeld),
});
}
function rollGauntletCrownRewardCard(grade) {
const r = Math.random();
if (grade === 'A') {
@@ -3671,6 +3703,11 @@ io.on('connection', (socket) => {
io.to(spaceId).emit('gauntlet-crown-lobby-sync', { readyMap: { ...space.gauntletCrownLobbyReady } });
}
startGauntletTicker(spaceId, space);
} else if (isBalloonBossMissionShellSpace(space)) {
if (!space.gauntletCrownLobbyReady || typeof space.gauntletCrownLobbyReady !== 'object') space.gauntletCrownLobbyReady = {};
space.gauntletCrownLobbyReady[socket.id] = false;
if (!space.gauntletRun) space.gauntletRun = newBalloonBossShellGauntletRunState();
io.to(spaceId).emit('gauntlet-crown-lobby-sync', { readyMap: { ...space.gauntletCrownLobbyReady } });
}
const peersList = [...space.peers.values()];
const mapData = mdJoin;
@@ -3715,6 +3752,9 @@ io.on('connection', (socket) => {
if (mdJoin.gameType === 'gauntlet' && space.gauntletRun) {
joinCb.gauntletEndsAt = space.gauntletRun.endsAt != null ? space.gauntletRun.endsAt : null;
}
if (isBalloonBossMissionShellSpace(space) && space.gauntletRun) {
joinCb.gauntletCrownRunHeld = !!space.gauntletRun.crownRunHeld;
}
if (typeof cb === 'function') cb(joinCb);
socket.to(spaceId).emit('user-joined', peer);
if (space.voiceInits) {
@@ -3977,6 +4017,13 @@ io.on('connection', (socket) => {
gauntletEliminated: !!p.gauntletEliminated,
}));
}
if (md && isBalloonBossMissionShellSpace(space)) {
if (!space.gauntletCrownLobbyReady || typeof space.gauntletCrownLobbyReady !== 'object') space.gauntletCrownLobbyReady = {};
space.peers.forEach((_p, id) => {
space.gauntletCrownLobbyReady[id] = false;
});
space.gauntletRun = newBalloonBossShellGauntletRunState();
}
}
const gameStartPayload = {
mapId: mapId || undefined,
@@ -3985,6 +4032,10 @@ io.on('connection', (socket) => {
peersSnap: peersSnap || undefined,
};
if (peersSnap) gameStartPayload.gauntletEndsAt = gauntletEndsAtEmit != null ? gauntletEndsAtEmit : null;
if (mapId && maps.has(mapId) && isBalloonBossMissionShellSpace(space)) {
const grs = space.gauntletRun;
gameStartPayload.gauntletCrownRunHeld = !!(grs && grs.crownRunHeld);
}
io.to(sid).emit('game-start', gameStartPayload);
return reply({ ok: true });
}
@@ -4014,7 +4065,7 @@ io.on('connection', (socket) => {
}
if (space.gauntletCrownLobbyReady && typeof space.gauntletCrownLobbyReady === 'object') {
delete space.gauntletCrownLobbyReady[socket.id];
if (isGauntletCrownHeistMapBySpace(space)) {
if (isCrownLobbyShellSpace(space)) {
io.to(sid).emit('gauntlet-crown-lobby-sync', { readyMap: { ...space.gauntletCrownLobbyReady } });
}
}
@@ -4057,14 +4108,20 @@ io.on('connection', (socket) => {
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 okGauntletCrown = md && md.gameType === 'gauntlet' && isGauntletCrownHeistMapBySpace(space);
const okBalloonShell = md && md.gameType === 'balloon_boss' && isBalloonBossMissionShellSpace(space);
if (!okGauntletCrown && !okBalloonShell) 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);
if (okGauntletCrown) {
ensureGauntletEndsAtIfNeeded(space);
emitGauntletSync(sid, space);
} else {
emitBalloonBossMissionShellGauntletSync(sid, space);
}
return reply({ ok: true });
});
@@ -4134,7 +4191,7 @@ io.on('connection', (socket) => {
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 (!isCrownLobbyShellSpace(space)) return;
if (!space.gauntletCrownLobbyReady || typeof space.gauntletCrownLobbyReady !== 'object') {
space.gauntletCrownLobbyReady = {};
}
@@ -4148,7 +4205,7 @@ io.on('connection', (socket) => {
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 (!isCrownLobbyShellSpace(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;
@@ -4162,7 +4219,7 @@ io.on('connection', (socket) => {
const sid = socket.data.spaceId;
const space = sid ? spaces.get(sid) : null;
if (!space || !space.peers.has(socket.id)) return reply({ ok: false, error: 'ไม่อยู่ในห้อง' });
if (!isGauntletCrownHeistMapBySpace(space)) return reply({ ok: false, error: 'ไม่ใช่แมป crown' });
if (!isCrownLobbyShellSpace(space)) return reply({ ok: false, error: 'ไม่ใช่แมป crown / Mega Virus ภารกิจ' });
if (space.hostId !== socket.id) return reply({ ok: false, error: 'เฉพาะโฮสต์กด START' });
const gr = space.gauntletRun;
if (!gr || !gr.crownRunHeld) return reply({ ok: false, error: 'เริ่มแล้ว' });