diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js
index 02da3cb..6629aff 100644
--- a/www/html/Game/public/js/play.js
+++ b/www/html/Game/public/js/play.js
@@ -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 = 'คะแนนรวม (' + parts.join('+') + ') = ' + sum + ' · ผู้เล่น ' + pc + ' → เกรด';
+ } else if (mission && mission.uiSkin === 'mega_virus') {
+ const pc = Math.max(0, Math.floor(Number(disp.participantCount) || 0));
+ totalEl.innerHTML = 'ความเสียหายรวม (' + parts.join('+') + ') = ' + sum + ' · ผู้เล่น ' + pc + ' → เกรด';
} else {
totalEl.innerHTML = 'คะแนนรวม (' + parts.join('+') + ') = ' + sum + ' · เฉลี่ย ' + 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();
diff --git a/www/html/Game/server.js b/www/html/Game/server.js
index ff851df..4d3d73d 100644
--- a/www/html/Game/server.js
+++ b/www/html/Game/server.js
@@ -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: 'เริ่มแล้ว' });