Mega Virus done
This commit is contained in:
@@ -3457,6 +3457,9 @@
|
||||
/** ให้สอดคล้องกับ server sanitizeGauntletAssetUrl + กัน placeholder .... */
|
||||
function normalizeMegaVirusAssetUrl(v) {
|
||||
var t = v != null ? normalizeGameAssetUrlForWebAdmin(decodeAssetUrlPercentRuns(String(v).trim())).replace(/\/+$/, '') : '';
|
||||
if (/^\/Game\/img\/MegaVirus\/Artboard$/i.test(t) || /^Game\/img\/MegaVirus\/Artboard$/i.test(t)) {
|
||||
t = '/Game/img/MegaVirus/Artboard%209.png';
|
||||
}
|
||||
if (!t || t.length > 500) return '';
|
||||
if (/\.{4,}/.test(t)) return '';
|
||||
if (/[\t\n\r<>"'`]/.test(t)) return '';
|
||||
|
||||
@@ -629,9 +629,9 @@
|
||||
<div class="space-shooter-ship-row"><span class="space-shooter-ship-slot">P5</span><label class="space-shooter-ship-url-label">URL <input type="text" id="mega-virus-balloon-url-5" maxlength="500" spellcheck="false" placeholder="/Game/img/MegaVirus/balloon-5.png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="mega-virus-balloon-prev-5" alt="" width="40" height="48" decoding="async"><button type="button" class="btn btn-ghost" id="btn-mega-virus-balloon-clear-5">ล้าง</button></div>
|
||||
<div class="space-shooter-ship-row"><span class="space-shooter-ship-slot">P6</span><label class="space-shooter-ship-url-label">URL <input type="text" id="mega-virus-balloon-url-6" maxlength="500" spellcheck="false" placeholder="/Game/img/MegaVirus/balloon-6.png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="mega-virus-balloon-prev-6" alt="" width="40" height="48" decoding="async"><button type="button" class="btn btn-ghost" id="btn-mega-virus-balloon-clear-6">ล้าง</button></div>
|
||||
</div>
|
||||
<p class="muted" style="margin-top:0.5rem">Fallback ใช้เมื่อช่อง P ว่าง <strong>หรือ</strong> รูปต่อที่นั่งโหลดไม่สำเร็จ (404) · ใช้ URL แบบ <code>/Game/img/...</code> เท่านั้น — <strong>ไม่ใช่</strong> <code>/Game/public/img/...</code> (public ไม่ปรากฏใน URL) · <em>English:</em> Use <code>/Game/img/...</code> only, not <code>/Game/public/img/...</code>.</p>
|
||||
<p class="muted" style="margin-top:0.5rem">Fallback ใช้เมื่อช่อง P ว่าง <strong>หรือ</strong> รูปต่อที่นั่งโหลดไม่สำเร็จ (404) · ใช้ URL แบบ <code>/Game/img/...</code> เท่านั้น — <strong>ไม่ใช่</strong> <code>/Game/public/img/...</code> (public ไม่ปรากฏใน URL) · <strong>กรอบฟองต้องใส่ชื่อไฟล์เต็ม</strong> เช่น <code>/Game/img/MegaVirus/Artboard%209.png</code> หรือ <code>…/Artboard 9.png</code> — <em>อย่าจบแค่</em> <code>…/Artboard</code> · <em>English:</em> Use full filename (e.g. <code>Artboard%209.png</code>); a bare <code>…/Artboard</code> path will not load.</p>
|
||||
<div class="form-grid form-inline" style="margin-top:0.75rem;align-items:flex-end;gap:0.75rem;flex-wrap:wrap">
|
||||
<label class="space-shooter-ship-url-label" style="flex:1;min-width:14rem">รูปลูกโป่งร่วม / กรอบฟอง (fallback) <input type="text" id="mega-virus-balloon-fallback-url" maxlength="500" spellcheck="false" placeholder="/Game/img/MegaVirus/Artboard 9.png" autocomplete="off"></label>
|
||||
<label class="space-shooter-ship-url-label" style="flex:2;min-width:28rem;max-width:100%" title="ต้องลงท้ายด้วยไฟล์ เช่น Artboard%209.png หรือ Artboard 9.png">รูปลูกโป่งร่วม / กรอบฟอง (fallback) <input type="text" id="mega-virus-balloon-fallback-url" maxlength="500" spellcheck="false" placeholder="/Game/img/MegaVirus/Artboard%209.png" autocomplete="off"></label>
|
||||
<img class="space-shooter-ship-prev" id="mega-virus-balloon-fallback-prev" alt="" width="40" height="48" decoding="async">
|
||||
<button type="button" class="btn btn-ghost" id="btn-mega-virus-balloon-fallback-clear">ล้าง</button>
|
||||
</div>
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
+503
-100
@@ -2121,7 +2121,11 @@
|
||||
/** แปลง URL จาก Admin/game-timing ให้โหลดได้ (nginx เสิร์ฟจาก /Game/...) */
|
||||
function normalizeGauntletAssetUrlForPlay(u) {
|
||||
if (typeof u !== 'string') return '';
|
||||
const raw = normalizeGameAssetUrlForWebPlay(decodeAssetUrlPercentRuns(u.trim()));
|
||||
let raw = normalizeGameAssetUrlForWebPlay(decodeAssetUrlPercentRuns(u.trim()));
|
||||
const bare0 = raw.split('?')[0].replace(/\/+$/, '');
|
||||
if (/^\/Game\/img\/MegaVirus\/Artboard$/i.test(bare0) || /^Game\/img\/MegaVirus\/Artboard$/i.test(bare0)) {
|
||||
raw = '/Game/img/MegaVirus/Artboard%209.png';
|
||||
}
|
||||
if (!raw) return '';
|
||||
if (/^https?:\/\//i.test(raw)) {
|
||||
const z = raw.replace(/\/+$/, '');
|
||||
@@ -2380,6 +2384,8 @@
|
||||
let balloonBossPlayerFireCd = 0;
|
||||
let balloonBossGameEnded = false;
|
||||
let balloonBossHitFx = [];
|
||||
/** Mega Virus — ป๊อปดาเมจต่อบอส (ข้อความ +2) เมื่อกระสุนโดนบอส */
|
||||
let balloonBossScorePopups = [];
|
||||
let balloonBossBossFireAcc = 0;
|
||||
/** quiz_battle — โดม MCQ กด E (ข้อจาก battleQuizMcq) */
|
||||
let quizBattleMcqPool = [];
|
||||
@@ -3896,6 +3902,7 @@
|
||||
jumpSurviveEliminated: false,
|
||||
spaceShooterScore: 0,
|
||||
balloonBossScore: 0,
|
||||
balloonBossBossDmg: 0,
|
||||
balloonBossBalloons: mapData && mapData.gameType === 'balloon_boss' ? balloonBossBalloonsStartPlay() : 5,
|
||||
balloonBossEliminated: false,
|
||||
botQuizCarryPathfindAfter: performance.now() + previewBotSeq * 75 + Math.floor(Math.random() * 160),
|
||||
@@ -5177,6 +5184,42 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mega Virus (balloon_boss shell): แฟลชรูปเดียวแล้วค่อย GCM
|
||||
* — แพ้ (ทุกคนตาย / หมดเวลา) = result-gameover.png (TowerBlock เหมือนภารกิจอื่น — รูป2 flow)
|
||||
* — ชนะ = end-victory-2.png ใน MegaVirus
|
||||
*/
|
||||
function beginBalloonBossMegaVirusResultFlashSequenceThenGcm(mission, endReason) {
|
||||
if (!mission || mission.uiSkin !== 'mega_virus') {
|
||||
showGauntletCrownMissionOverlay(mission);
|
||||
return;
|
||||
}
|
||||
hideStackTowerResultFlashDomOnlyPlay();
|
||||
const ov = document.getElementById('stack-tower-result-flash');
|
||||
const imgEl = document.getElementById('stack-tower-result-flash-img');
|
||||
if (!ov || !imgEl) {
|
||||
showGauntletCrownMissionOverlay(mission);
|
||||
return;
|
||||
}
|
||||
const lose = endReason === 'all_dead' || endReason === 'time';
|
||||
const flashSrc = lose ? stackTowerAssetUrl('result-gameover.png') : megaVirusAssetUrl('end-victory-2.png');
|
||||
const finishToGcm = function () {
|
||||
hideStackTowerResultFlashDomOnlyPlay();
|
||||
showGauntletCrownMissionOverlay(mission);
|
||||
};
|
||||
imgEl.onerror = function () {
|
||||
imgEl.onerror = null;
|
||||
finishToGcm();
|
||||
};
|
||||
imgEl.src = flashSrc;
|
||||
ov.classList.remove('is-hidden');
|
||||
ov.setAttribute('aria-hidden', 'false');
|
||||
stackTowerResultFlashTimer = setTimeout(function () {
|
||||
stackTowerResultFlashTimer = null;
|
||||
finishToGcm();
|
||||
}, STACK_TOWER_RESULT_FLASH_MS);
|
||||
}
|
||||
|
||||
function stackTowerMissionTimeLimitSecPlay() {
|
||||
const t = Number(playStackTowerMissionTimeSec);
|
||||
if (Number.isFinite(t) && t > 0) return Math.max(10, Math.min(7200, Math.floor(t)));
|
||||
@@ -6260,10 +6303,16 @@
|
||||
const n2 = ranked.length || 1;
|
||||
const averageScore = Math.floor(totalSum / n2);
|
||||
const maxHp = balloonBossMaxHpPlay();
|
||||
const progress = Math.min(1, totalSum / Math.max(1, maxHp));
|
||||
let bossDmgSum = Math.max(0, me.balloonBossBossDmg | 0);
|
||||
others.forEach(function (o) {
|
||||
if (o) bossDmgSum += Math.max(0, o.balloonBossBossDmg | 0);
|
||||
});
|
||||
const progress = Math.min(1, bossDmgSum / Math.max(1, maxHp));
|
||||
const score100 = Math.min(100, Math.floor(progress * 100));
|
||||
let grade = 'C';
|
||||
if (progress >= 1) grade = 'A';
|
||||
const endR = mission && mission.balloonBossEndReason;
|
||||
if (endR === 'victory' || progress >= 1) grade = 'A';
|
||||
else if (endR === 'all_dead') grade = 'F';
|
||||
else if (score100 >= 50) grade = 'B';
|
||||
else if (totalSum <= 0) grade = 'F';
|
||||
const rewardCard = grade === 'F' ? null : gauntletCrownRollRewardCardLocal(grade);
|
||||
@@ -6325,10 +6374,15 @@
|
||||
const n2 = ranked.length || 1;
|
||||
const averageScore = Math.floor(totalSum / n2);
|
||||
const maxHp = balloonBossMaxHpPlay();
|
||||
const progress = Math.min(1, totalSum / Math.max(1, maxHp));
|
||||
let bossDmgSum = Math.max(0, me.balloonBossBossDmg | 0);
|
||||
others.forEach((o) => {
|
||||
if (o) bossDmgSum += Math.max(0, o.balloonBossBossDmg | 0);
|
||||
});
|
||||
const progress = Math.min(1, bossDmgSum / 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 (reason === 'all_dead') grade = 'F';
|
||||
else if (score100 >= 50) grade = 'B';
|
||||
else if (totalSum <= 0) grade = 'F';
|
||||
const rewardCard = grade === 'F' ? null : gauntletCrownRollRewardCardLocal(grade);
|
||||
@@ -10278,11 +10332,14 @@
|
||||
balloonBossPlayerBullets = [];
|
||||
balloonBossBossBullets = [];
|
||||
balloonBossBossFireAcc = 0;
|
||||
balloonBossScorePopups = [];
|
||||
me.balloonBossScore = 0;
|
||||
me.balloonBossBossDmg = 0;
|
||||
me.balloonBossBalloons = balloonBossBalloonsStartPlay();
|
||||
me.balloonBossEliminated = false;
|
||||
others.forEach((o) => {
|
||||
o.balloonBossScore = 0;
|
||||
o.balloonBossBossDmg = 0;
|
||||
o.balloonBossBalloons = balloonBossBalloonsStartPlay();
|
||||
o.balloonBossEliminated = false;
|
||||
});
|
||||
@@ -11870,6 +11927,100 @@
|
||||
return { cx: (w * 0.5) * ts, cy: (h * 0.38) * ts };
|
||||
}
|
||||
|
||||
/**
|
||||
* จุดกลางบอสระหว่างเล่น — ลอยแบบลูกโป่งจากผลรวม sine (deterministic ต่อเวลาในรอบ)
|
||||
* เพื่อให้ทุก client ได้ตำแหน่งใกล้เคียงกัน (ไม่ใช้ state สุ่มต่อเครื่อง)
|
||||
*/
|
||||
function getBalloonBossBossLiveCenterPlay(md, ts) {
|
||||
if (!md || md.gameType !== 'balloon_boss') return getBalloonBossBossWorldCenterPlay(md, ts);
|
||||
const base = getBalloonBossBossWorldCenterPlay(md, ts);
|
||||
const w = md.width || 20, h = md.height || 15;
|
||||
const mw = w * ts, mh = h * ts;
|
||||
const tSec = balloonBossSessionStartMs > 0
|
||||
? (performance.now() - balloonBossSessionStartMs) / 1000
|
||||
: performance.now() / 1000;
|
||||
const ax = Math.min(mw * 0.19, 240);
|
||||
const ay = Math.min(mh * 0.17, 200);
|
||||
const cx = base.cx + Math.sin(tSec * 0.33 + 0.75) * ax + Math.sin(tSec * 0.69 + 0.15) * (ax * 0.4);
|
||||
const cy = base.cy + Math.cos(tSec * 0.29 + 1.05) * ay + Math.cos(tSec * 0.64 + 0.45) * (ay * 0.37);
|
||||
return clampBalloonBossWorld(cx, cy, mw, mh);
|
||||
}
|
||||
|
||||
/** จุดโลกประมาณกลางกระจุกลูกโป่ง (ใช้ hit จากกระสุนบอส) — ไม่ใช่จุดเท้า */
|
||||
function balloonBossBalloonClusterWorldPlay(ent, bossCx, bossCy) {
|
||||
if (!ent || ent.balloonBossCx == null) return { cx: 0, cy: 0 };
|
||||
const dx = ent.balloonBossCx - bossCx;
|
||||
const dy = ent.balloonBossCy - bossCy;
|
||||
const L = Math.hypot(dx, dy) || 1;
|
||||
const ox = (dx / L) * 22;
|
||||
const oy = (dy / L) * 12;
|
||||
return {
|
||||
cx: ent.balloonBossCx + ox,
|
||||
cy: ent.balloonBossCy - 52 + oy * 0.35,
|
||||
};
|
||||
}
|
||||
|
||||
/** ผู้เล่นหลักเด้งกับวงชนบอส (บอสเป็นวงนิ่งใน world) */
|
||||
function balloonBossPlayerBossElasticPlay(meEnt, bossCx, bossCy, bossCollideR, mw, mh) {
|
||||
if (!meEnt || meEnt.balloonBossEliminated || meEnt.balloonBossCx == null) return;
|
||||
const rMe = 44;
|
||||
const rB = Math.max(30, bossCollideR * 0.9);
|
||||
const dx = meEnt.balloonBossCx - bossCx;
|
||||
const dy = meEnt.balloonBossCy - bossCy;
|
||||
const dist = Math.hypot(dx, dy) || 1e-6;
|
||||
const minD = rMe + rB;
|
||||
if (dist >= minD) return;
|
||||
const nx = dx / dist;
|
||||
const ny = dy / dist;
|
||||
const push = (minD - dist) * 0.58;
|
||||
meEnt.balloonBossCx += nx * push;
|
||||
meEnt.balloonBossCy += ny * push;
|
||||
const vn = meEnt.balloonBossVelX * nx + meEnt.balloonBossVelY * ny;
|
||||
if (vn < 0) {
|
||||
meEnt.balloonBossVelX -= 1.85 * vn * nx;
|
||||
meEnt.balloonBossVelY -= 1.85 * vn * ny;
|
||||
}
|
||||
const cl = clampBalloonBossWorld(meEnt.balloonBossCx, meEnt.balloonBossCy, mw, mh);
|
||||
meEnt.balloonBossCx = cl.cx;
|
||||
meEnt.balloonBossCy = cl.cy;
|
||||
}
|
||||
|
||||
/** ผู้เล่นหลักเด้งกับคนอื่น (บอทขยับรับแรงปะทะเล็กน้อย) */
|
||||
function balloonBossPlayerElasticCollisionsPlay(meEnt, aliveRefs, mw, mh) {
|
||||
if (!meEnt || meEnt.balloonBossEliminated || meEnt.balloonBossCx == null) return;
|
||||
const rMe = 44;
|
||||
const rO = 44;
|
||||
const minD = rMe + rO - 2;
|
||||
for (let ii = 0; ii < aliveRefs.length; ii++) {
|
||||
const entry = aliveRefs[ii];
|
||||
if (!entry || entry.ref === meEnt) continue;
|
||||
const o = entry.ref;
|
||||
if (!o || o.balloonBossEliminated || o.balloonBossCx == null) continue;
|
||||
const dx = meEnt.balloonBossCx - o.balloonBossCx;
|
||||
const dy = meEnt.balloonBossCy - o.balloonBossCy;
|
||||
const dist = Math.hypot(dx, dy);
|
||||
if (!dist || dist >= minD) continue;
|
||||
const nx = dx / dist;
|
||||
const ny = dy / dist;
|
||||
const push = (minD - dist) * 0.52;
|
||||
meEnt.balloonBossCx += nx * push;
|
||||
meEnt.balloonBossCy += ny * push;
|
||||
const vn = meEnt.balloonBossVelX * nx + meEnt.balloonBossVelY * ny;
|
||||
if (vn < 0) {
|
||||
meEnt.balloonBossVelX -= 1.85 * vn * nx;
|
||||
meEnt.balloonBossVelY -= 1.85 * vn * ny;
|
||||
}
|
||||
if (isPreviewBotId(entry.id)) {
|
||||
const pushO = (minD - dist) * 0.48;
|
||||
o.balloonBossCx -= nx * pushO;
|
||||
o.balloonBossCy -= ny * pushO;
|
||||
}
|
||||
}
|
||||
const cl2 = clampBalloonBossWorld(meEnt.balloonBossCx, meEnt.balloonBossCy, mw, mh);
|
||||
meEnt.balloonBossCx = cl2.cx;
|
||||
meEnt.balloonBossCy = cl2.cy;
|
||||
}
|
||||
|
||||
/** หมายเลขสล็อต 1–6 ที่มีบนแมป (เรียง) — ถ้ามีแค่ 3 ช่อง ผู้เล่นจะวนที่นั่ง 3 จุดรอบบอส · Occupied P-slots on map for spawn cycling */
|
||||
function getBalloonBossOccupiedSlotNumbersPlay(md) {
|
||||
if (!md || !md.balloonBossPlayerSlots) return [];
|
||||
@@ -11960,6 +12111,13 @@
|
||||
return s;
|
||||
}
|
||||
|
||||
/** รวมดาเมจที่ทำกับบอส (ไม่เท่ากับคะแนน — คะแนน +10 ต่อครั้ง, HP บอส -2 ต่อครั้ง) */
|
||||
function balloonBossTeamBossDamagePlay() {
|
||||
let s = Math.max(0, me.balloonBossBossDmg | 0);
|
||||
others.forEach((o) => { s += Math.max(0, o.balloonBossBossDmg | 0); });
|
||||
return s;
|
||||
}
|
||||
|
||||
function syncBalloonBossFootFromCenter(ent) {
|
||||
if (!ent || !mapData || ent.balloonBossCx == null || ent.balloonBossCy == null) return;
|
||||
const { cw, ch } = getCharacterFootprintWH(mapData);
|
||||
@@ -12005,6 +12163,7 @@
|
||||
ent.balloonBossCx = cl.cx;
|
||||
ent.balloonBossCy = cl.cy;
|
||||
if (typeof ent.balloonBossScore !== 'number' || !Number.isFinite(ent.balloonBossScore)) ent.balloonBossScore = 0;
|
||||
if (typeof ent.balloonBossBossDmg !== 'number' || !Number.isFinite(ent.balloonBossBossDmg)) ent.balloonBossBossDmg = 0;
|
||||
/** จำนวนลูกโป่งตามแมปเสมอเมื่อจัดตำแหน่ง — ไม่ค้าง 5 จาก preview bot · Always sync start count */
|
||||
ent.balloonBossBalloons = bStart;
|
||||
if (typeof ent.balloonBossHitIframe !== 'number') ent.balloonBossHitIframe = 0;
|
||||
@@ -12015,6 +12174,9 @@
|
||||
ent.tx = ent.x;
|
||||
ent.ty = ent.y;
|
||||
ent.direction = 'down';
|
||||
if (typeof ent.balloonBossAimRad !== 'number' || !Number.isFinite(ent.balloonBossAimRad)) {
|
||||
ent.balloonBossAimRad = Math.random() * Math.PI * 2;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12024,9 +12186,10 @@
|
||||
balloonBossPendingShots = [];
|
||||
balloonBossPlayerBullets = [];
|
||||
balloonBossBossBullets = [];
|
||||
balloonBossScorePopups = [];
|
||||
if (isMegaVirusMissionShellMapPlay()) {
|
||||
applyMegaVirusMissionPanelImages();
|
||||
showGauntletCrownMissionOverlay(balloonBossBuildMissionPayloadPlay(reason));
|
||||
beginBalloonBossMegaVirusResultFlashSequenceThenGcm(balloonBossBuildMissionPayloadPlay(reason), reason);
|
||||
return;
|
||||
}
|
||||
const ov = document.getElementById('gauntlet-ended-overlay');
|
||||
@@ -12039,8 +12202,8 @@
|
||||
titleEl.textContent = reason === 'victory' ? 'ชนะบอส! · Boss defeated' : 'หมดเวลา · Time up';
|
||||
}
|
||||
msgEl.textContent = reason === 'victory'
|
||||
? 'MEGA VIRUS ถูกกำจัดแล้ว — อันดับจากความเสียหายที่ทำกับบอส'
|
||||
: 'หมดเวลา — อันดับจากความเสียหายที่ทำกับบอส';
|
||||
? 'MEGA VIRUS ถูกกำจัดแล้ว — อันดับจากคะแนนการยิงโดน'
|
||||
: 'หมดเวลา — อันดับจากคะแนนการยิงโดน';
|
||||
listEl.innerHTML = '';
|
||||
const ranks = [];
|
||||
if (myId != null) {
|
||||
@@ -12053,7 +12216,7 @@
|
||||
ranks.forEach((r, i) => {
|
||||
const li = document.createElement('li');
|
||||
const isMe = myId != null && r && String(r.id) === String(myId);
|
||||
li.textContent = `${i + 1}. ${(r && r.nickname) || '—'} — DMG ${Math.max(0, Number(r && r.score) || 0)}`;
|
||||
li.textContent = `${i + 1}. ${(r && r.nickname) || '—'} — ${Math.max(0, Number(r && r.score) || 0)} pts`;
|
||||
if (isMe) li.className = 'gauntlet-ended-me';
|
||||
listEl.appendChild(li);
|
||||
});
|
||||
@@ -12091,12 +12254,15 @@
|
||||
if (o.balloonBossBotNextFire <= 0) {
|
||||
o.balloonBossBotNextFire = 0.9 + Math.random() * 1.1;
|
||||
const delayMs = balloonBossFireDelayMsPlay();
|
||||
const dx = bossCx - o.balloonBossCx, dy = bossCy - o.balloonBossCy;
|
||||
const d = Math.sqrt(dx * dx + dy * dy) || 1;
|
||||
const dx = bossCx - o.balloonBossCx;
|
||||
const dy = bossCy - o.balloonBossCy;
|
||||
const L = Math.hypot(dx, dy) || 1;
|
||||
const bspd = 500 + Math.random() * 90;
|
||||
balloonBossPendingShots.push({
|
||||
releaseAt: performance.now() + delayMs,
|
||||
sx: o.balloonBossCx, sy: o.balloonBossCy,
|
||||
vx: (dx / d) * 520, vy: (dy / d) * 520,
|
||||
vx: (dx / L) * bspd,
|
||||
vy: (dy / L) * bspd,
|
||||
ownerId: bid,
|
||||
});
|
||||
}
|
||||
@@ -12124,10 +12290,10 @@
|
||||
const now = performance.now();
|
||||
const dt = Math.min(0.055, balloonBossLastTickMs ? (now - balloonBossLastTickMs) / 1000 : 0.016);
|
||||
balloonBossLastTickMs = now;
|
||||
const bossC = getBalloonBossBossWorldCenterPlay(mapData, ts);
|
||||
const bossC = getBalloonBossBossLiveCenterPlay(mapData, ts);
|
||||
const bossR = Math.max(36, ts * 1.15);
|
||||
const maxHp = balloonBossMaxHpPlay();
|
||||
const teamDmg = balloonBossTeamDamagePlay();
|
||||
const teamBossDmg = balloonBossTeamBossDamagePlay();
|
||||
|
||||
if (me.balloonBossEliminated) {
|
||||
me.balloonBossVelX = 0;
|
||||
@@ -12138,6 +12304,11 @@
|
||||
balloonBossHitFx[hi].t -= dt;
|
||||
if (balloonBossHitFx[hi].t <= 0) balloonBossHitFx.splice(hi, 1);
|
||||
}
|
||||
for (let si = balloonBossScorePopups.length - 1; si >= 0; si--) {
|
||||
const sp = balloonBossScorePopups[si];
|
||||
sp.t -= dt;
|
||||
if (sp.t <= 0) balloonBossScorePopups.splice(si, 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const remSec = balloonBossRemainingSecPlay();
|
||||
@@ -12145,17 +12316,27 @@
|
||||
endBalloonBossGame('time');
|
||||
return;
|
||||
}
|
||||
if (teamDmg >= maxHp) {
|
||||
if (teamBossDmg >= maxHp) {
|
||||
endBalloonBossGame('victory');
|
||||
return;
|
||||
}
|
||||
const allRefsEarly = buildBalloonBossParticipantRefsPlay();
|
||||
if (allRefsEarly.length > 0 && allRefsEarly.every((e) => e.ref && e.ref.balloonBossEliminated)) {
|
||||
endBalloonBossGame('all_dead');
|
||||
return;
|
||||
}
|
||||
|
||||
for (let hi = balloonBossHitFx.length - 1; hi >= 0; hi--) {
|
||||
balloonBossHitFx[hi].t -= dt;
|
||||
if (balloonBossHitFx[hi].t <= 0) balloonBossHitFx.splice(hi, 1);
|
||||
}
|
||||
for (let si = balloonBossScorePopups.length - 1; si >= 0; si--) {
|
||||
const sp = balloonBossScorePopups[si];
|
||||
sp.t -= dt;
|
||||
if (sp.t <= 0) balloonBossScorePopups.splice(si, 1);
|
||||
}
|
||||
|
||||
const aliveRefs = buildBalloonBossParticipantRefsPlay().filter((e) => e.ref && !e.ref.balloonBossEliminated);
|
||||
const aliveRefs = allRefsEarly.filter((e) => e.ref && !e.ref.balloonBossEliminated);
|
||||
|
||||
for (let pi = balloonBossPendingShots.length - 1; pi >= 0; pi--) {
|
||||
const pnd = balloonBossPendingShots[pi];
|
||||
@@ -12165,19 +12346,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
const bossFireEvery = 0.92;
|
||||
const bossFireEvery = 1.42;
|
||||
balloonBossBossFireAcc += dt;
|
||||
while (balloonBossBossFireAcc >= bossFireEvery) {
|
||||
balloonBossBossFireAcc -= bossFireEvery;
|
||||
if (aliveRefs.length) {
|
||||
const wave = Math.floor(now / 1000);
|
||||
const tgt = aliveRefs[wave % aliveRefs.length].ref;
|
||||
if (tgt && tgt.balloonBossCx != null) {
|
||||
const dx = tgt.balloonBossCx - bossC.cx, dy = tgt.balloonBossCy - bossC.cy;
|
||||
const d = Math.sqrt(dx * dx + dy * dy) || 1;
|
||||
const spd = 290;
|
||||
balloonBossBossBullets.push({ x: bossC.cx, y: bossC.cy, vx: (dx / d) * spd, vy: (dy / d) * spd });
|
||||
}
|
||||
const ang = Math.random() * Math.PI * 2;
|
||||
const spd = 88 + Math.random() * 72;
|
||||
balloonBossBossBullets.push({
|
||||
x: bossC.cx, y: bossC.cy,
|
||||
vx: Math.cos(ang) * spd, vy: Math.sin(ang) * spd,
|
||||
gy: 48 + Math.random() * 56,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12196,12 +12376,15 @@
|
||||
balloonBossBalloons: me.balloonBossBalloons,
|
||||
balloonBossEliminated: !!me.balloonBossEliminated,
|
||||
balloonBossScore: Math.max(0, me.balloonBossScore | 0),
|
||||
balloonBossBossDmg: Math.max(0, me.balloonBossBossDmg | 0),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (let bi = balloonBossBossBullets.length - 1; bi >= 0; bi--) {
|
||||
const b = balloonBossBossBullets[bi];
|
||||
const gy = typeof b.gy === 'number' && Number.isFinite(b.gy) ? b.gy : 62;
|
||||
b.vy += gy * dt;
|
||||
b.x += b.vx * dt;
|
||||
b.y += b.vy * dt;
|
||||
if (b.x < -80 || b.x > mw + 80 || b.y < -80 || b.y > mh + 80) {
|
||||
@@ -12213,8 +12396,9 @@
|
||||
if (hit) return;
|
||||
const ent = entry.ref;
|
||||
if (!ent || ent.balloonBossCx == null) return;
|
||||
const dx = b.x - ent.balloonBossCx, dy = b.y - ent.balloonBossCy;
|
||||
if (dx * dx + dy * dy <= (26 * 26)) {
|
||||
const cc = balloonBossBalloonClusterWorldPlay(ent, bossC.cx, bossC.cy);
|
||||
const dx = b.x - cc.cx, dy = b.y - cc.cy;
|
||||
if (dx * dx + dy * dy <= (36 * 36)) {
|
||||
hit = true;
|
||||
applyBalloonBossHitToEntity(ent);
|
||||
}
|
||||
@@ -12230,39 +12414,71 @@
|
||||
balloonBossPlayerBullets.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
let pHit = false;
|
||||
for (let ji = 0; ji < aliveRefs.length; ji++) {
|
||||
if (pHit) break;
|
||||
const entry = aliveRefs[ji];
|
||||
const ent = entry.ref;
|
||||
if (!ent || ent.balloonBossCx == null) continue;
|
||||
if (String(entry.id) === String(b.ownerId)) continue;
|
||||
const canBalloonHit = isPreviewBotId(entry.id) || ent === me;
|
||||
if (!canBalloonHit) continue;
|
||||
const cc = balloonBossBalloonClusterWorldPlay(ent, bossC.cx, bossC.cy);
|
||||
const pdx = b.x - cc.cx, pdy = b.y - cc.cy;
|
||||
if (pdx * pdx + pdy * pdy <= (34 * 34)) {
|
||||
applyBalloonBossHitToEntity(ent);
|
||||
balloonBossPlayerBullets.splice(i, 1);
|
||||
pHit = true;
|
||||
}
|
||||
}
|
||||
if (pHit) continue;
|
||||
const dx = b.x - bossC.cx, dy = b.y - bossC.cy;
|
||||
if (dx * dx + dy * dy <= bossR * bossR) {
|
||||
balloonBossHitFx.push({ x: bossC.cx, y: bossC.cy, t: 0.22 });
|
||||
const add = 1;
|
||||
balloonBossScorePopups.push({
|
||||
ownerId: b.ownerId,
|
||||
x: b.x,
|
||||
y: b.y,
|
||||
t: 0.85,
|
||||
tMax: 0.85,
|
||||
});
|
||||
const addScore = 10;
|
||||
const addBossDmg = 2;
|
||||
if (b.ownerId === myId) {
|
||||
me.balloonBossScore = Math.max(0, (me.balloonBossScore | 0) + add);
|
||||
me.balloonBossScore = Math.max(0, (me.balloonBossScore | 0) + addScore);
|
||||
me.balloonBossBossDmg = Math.max(0, (me.balloonBossBossDmg | 0) + addBossDmg);
|
||||
if (socket && myId != null) {
|
||||
balloonBossLastMoveEmit = Date.now();
|
||||
socket.emit('move', {
|
||||
x: me.x, y: me.y, direction: me.direction,
|
||||
balloonBossScore: Math.max(0, me.balloonBossScore | 0),
|
||||
balloonBossBossDmg: Math.max(0, me.balloonBossBossDmg | 0),
|
||||
balloonBossBalloons: Math.max(0, me.balloonBossBalloons | 0),
|
||||
balloonBossEliminated: !!me.balloonBossEliminated,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const o = others.get(b.ownerId);
|
||||
if (o) o.balloonBossScore = Math.max(0, (o.balloonBossScore | 0) + add);
|
||||
if (o) {
|
||||
o.balloonBossScore = Math.max(0, (o.balloonBossScore | 0) + addScore);
|
||||
o.balloonBossBossDmg = Math.max(0, (o.balloonBossBossDmg | 0) + addBossDmg);
|
||||
}
|
||||
}
|
||||
balloonBossPlayerBullets.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
let inX = 0, inY = 0;
|
||||
if (!me.balloonBossEliminated && !isChatFocused()) {
|
||||
if (keys['ArrowLeft'] || keys['KeyA']) inX -= 1;
|
||||
if (keys['ArrowRight'] || keys['KeyD']) inX += 1;
|
||||
if (keys['ArrowUp'] || keys['KeyW']) inY -= 1;
|
||||
if (keys['ArrowDown'] || keys['KeyS']) inY += 1;
|
||||
}
|
||||
const accel = balloonBossShipAccelPlay();
|
||||
const maxSpd = balloonBossShipMaxSpeedPlay();
|
||||
const dragPerSec = balloonBossShipDragPlay();
|
||||
const tWind = performance.now() * 0.001;
|
||||
const gustAx = Math.sin(tWind * 0.36 + 1.15) * 62 + Math.sin(tWind * 1.05) * 24;
|
||||
const gustAy = Math.cos(tWind * 0.39 + 0.28) * 58 + Math.cos(tWind * 0.93) * 22;
|
||||
const maxSpd = Math.min(228, balloonBossShipMaxSpeedPlay() * 1.32);
|
||||
const dragPerSec = balloonBossShipDragPlay() * 0.4;
|
||||
if (typeof me.balloonBossVelX !== 'number' || !Number.isFinite(me.balloonBossVelX)) me.balloonBossVelX = 0;
|
||||
if (typeof me.balloonBossVelY !== 'number' || !Number.isFinite(me.balloonBossVelY)) me.balloonBossVelY = 0;
|
||||
let nvx = me.balloonBossVelX;
|
||||
let nvy = me.balloonBossVelY;
|
||||
if (inX !== 0 || inY !== 0) {
|
||||
const il = Math.sqrt(inX * inX + inY * inY) || 1;
|
||||
nvx += (inX / il) * accel * dt;
|
||||
nvy += (inY / il) * accel * dt;
|
||||
}
|
||||
nvx += gustAx * dt;
|
||||
nvy += gustAy * dt;
|
||||
const drag = Math.min(1, dragPerSec * dt);
|
||||
nvx *= (1 - drag);
|
||||
nvy *= (1 - drag);
|
||||
@@ -12278,27 +12494,55 @@
|
||||
const ncx = me.balloonBossCx + nvx * dt;
|
||||
const ncy = me.balloonBossCy + nvy * dt;
|
||||
const cl = clampBalloonBossWorld(ncx, ncy, mw, mh);
|
||||
if (Math.abs(cl.cx - ncx) > 0.5) me.balloonBossVelX = 0;
|
||||
if (Math.abs(cl.cy - ncy) > 0.5) me.balloonBossVelY = 0;
|
||||
if (Math.abs(cl.cx - ncx) > 0.5) me.balloonBossVelX *= -0.88;
|
||||
if (Math.abs(cl.cy - ncy) > 0.5) me.balloonBossVelY *= -0.88;
|
||||
me.balloonBossCx = cl.cx;
|
||||
me.balloonBossCy = cl.cy;
|
||||
}
|
||||
syncBalloonBossFootFromCenter(me);
|
||||
balloonBossPlayerElasticCollisionsPlay(me, aliveRefs, mw, mh);
|
||||
balloonBossPlayerBossElasticPlay(me, bossC.cx, bossC.cy, bossR, mw, mh);
|
||||
syncBalloonBossFootFromCenter(me);
|
||||
/** ฉากนี้แสดงตัวหันหน้าเข้ากล้อง — sync กับ combat draw */
|
||||
me.direction = 'down';
|
||||
me.isWalking = Math.abs(me.balloonBossVelX) > 8 || Math.abs(me.balloonBossVelY) > 8 || inX !== 0 || inY !== 0;
|
||||
me.isWalking = Math.abs(me.balloonBossVelX) > 12 || Math.abs(me.balloonBossVelY) > 12;
|
||||
|
||||
/** วงลูกศรหมุนเองเรื่อย ๆ (คุมไม่ได้) — Mega Virus ยิงตามมุมลูกศร ณ ตอนกด Space */
|
||||
aliveRefs.forEach((e) => {
|
||||
const r = e.ref;
|
||||
if (!r || r.balloonBossEliminated) return;
|
||||
if (typeof r.balloonBossAimRad !== 'number' || !Number.isFinite(r.balloonBossAimRad)) {
|
||||
r.balloonBossAimRad = Math.random() * Math.PI * 2;
|
||||
}
|
||||
r.balloonBossAimRad += 1.02 * dt;
|
||||
});
|
||||
|
||||
balloonBossPlayerFireCd -= dt;
|
||||
const wantFire = !isChatFocused() && !!keys['Space'] && !me.balloonBossEliminated;
|
||||
if (wantFire && balloonBossPlayerFireCd <= 0 && me.balloonBossCy != null) {
|
||||
balloonBossPlayerFireCd = 0.35;
|
||||
const delayMs = balloonBossFireDelayMsPlay();
|
||||
const dx = bossC.cx - me.balloonBossCx, dy = bossC.cy - me.balloonBossCy;
|
||||
const d = Math.sqrt(dx * dx + dy * dy) || 1;
|
||||
const bspd = 480;
|
||||
let vx;
|
||||
let vy;
|
||||
if (isMegaVirusMissionShellMapPlay()) {
|
||||
const aim = (typeof me.balloonBossAimRad === 'number' && Number.isFinite(me.balloonBossAimRad))
|
||||
? me.balloonBossAimRad
|
||||
: Math.atan2(bossC.cy - me.balloonBossCy, bossC.cx - me.balloonBossCx);
|
||||
vx = Math.cos(aim) * bspd;
|
||||
vy = Math.sin(aim) * bspd;
|
||||
} else {
|
||||
const dx = bossC.cx - me.balloonBossCx;
|
||||
const dy = bossC.cy - me.balloonBossCy;
|
||||
const L = Math.hypot(dx, dy) || 1;
|
||||
vx = (dx / L) * bspd;
|
||||
vy = (dy / L) * bspd;
|
||||
}
|
||||
balloonBossPendingShots.push({
|
||||
releaseAt: now + delayMs,
|
||||
sx: me.balloonBossCx, sy: me.balloonBossCy,
|
||||
vx: (dx / d) * 480, vy: (dy / d) * 480,
|
||||
vx,
|
||||
vy,
|
||||
ownerId: myId,
|
||||
});
|
||||
}
|
||||
@@ -12311,6 +12555,7 @@
|
||||
socket.emit('move', {
|
||||
x: me.x, y: me.y, direction: me.direction,
|
||||
balloonBossScore: Math.max(0, me.balloonBossScore | 0),
|
||||
balloonBossBossDmg: Math.max(0, me.balloonBossBossDmg | 0),
|
||||
balloonBossBalloons: Math.max(0, me.balloonBossBalloons | 0),
|
||||
balloonBossEliminated: !!me.balloonBossEliminated,
|
||||
});
|
||||
@@ -12322,9 +12567,9 @@
|
||||
const ts = tileSize;
|
||||
const w = mapData.width || 20, h = mapData.height || 15;
|
||||
const mw = w * ts, mh = h * ts;
|
||||
const bossC = getBalloonBossBossWorldCenterPlay(mapData, ts);
|
||||
const bossC = getBalloonBossBossLiveCenterPlay(mapData, ts);
|
||||
const maxHp = balloonBossMaxHpPlay();
|
||||
const dmg = balloonBossTeamDamagePlay();
|
||||
const dmg = balloonBossTeamBossDamagePlay();
|
||||
const hpLeft = Math.max(0, maxHp - dmg);
|
||||
const bossR = Math.max(36, ts * 1.15);
|
||||
const refs = buildBalloonBossParticipantRefsPlay();
|
||||
@@ -12353,6 +12598,48 @@
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
const score10PopUrl = normalizeGauntletAssetUrlForPlay('/Game/img/MegaVirus/score+10.png');
|
||||
const score10PopRec = score10PopUrl ? ensureGauntletAssetImage(score10PopUrl) : null;
|
||||
for (let si = 0; si < balloonBossScorePopups.length; si++) {
|
||||
const sp = balloonBossScorePopups[si];
|
||||
let wx = sp.x;
|
||||
let wy = sp.y;
|
||||
if (sp.ownerId != null) {
|
||||
const oid = sp.ownerId;
|
||||
const ent = (myId != null && String(oid) === String(myId)) ? me : others.get(oid);
|
||||
if (ent && ent.balloonBossCx != null && !ent.balloonBossEliminated) {
|
||||
const cc = balloonBossBalloonClusterWorldPlay(ent, bossC.cx, bossC.cy);
|
||||
const tMax = typeof sp.tMax === 'number' && sp.tMax > 0 ? sp.tMax : 0.95;
|
||||
const rise = ((tMax - Math.max(0, sp.t || 0)) / tMax) * 58;
|
||||
wx = cc.cx;
|
||||
wy = cc.cy - 46 - rise;
|
||||
}
|
||||
}
|
||||
const [sx, sy] = worldToScreen(wx, wy);
|
||||
ctx.save();
|
||||
ctx.globalAlpha = Math.min(1, Math.max(0, sp.t || 0) * 1.12);
|
||||
if (sp.dmgTxt) {
|
||||
const fpx = Math.max(11, 14 * zDraw);
|
||||
ctx.font = `bold ${fpx}px ui-sans-serif, system-ui, sans-serif`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.strokeStyle = 'rgba(0, 0, 0, 0.62)';
|
||||
ctx.lineWidth = Math.max(2, 2.4 * zDraw);
|
||||
ctx.strokeText(String(sp.dmgTxt), sx, sy);
|
||||
ctx.fillStyle = '#f8fafc';
|
||||
ctx.fillText(String(sp.dmgTxt), sx, sy);
|
||||
} else if (score10PopRec && score10PopRec.ready && score10PopRec.img && score10PopRec.img.naturalWidth > 0) {
|
||||
const iw = score10PopRec.img.naturalWidth;
|
||||
const ih = score10PopRec.img.naturalHeight;
|
||||
const mh = Math.max(14, 20 * zDraw);
|
||||
const sc = Math.min(1.35, mh / ih);
|
||||
const dw = iw * sc;
|
||||
const dh = ih * sc;
|
||||
ctx.drawImage(score10PopRec.img, sx - dw / 2, sy - dh / 2, dw, dh);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
const [bsx, bsy] = worldToScreen(bossC.cx, bossC.cy);
|
||||
ctx.save();
|
||||
ctx.translate(bsx, bsy);
|
||||
@@ -12370,14 +12657,7 @@
|
||||
ctx.clip();
|
||||
ctx.drawImage(bossImgRec.img, -dw / 2, -dh / 2, dw, dh);
|
||||
ctx.restore();
|
||||
ctx.strokeStyle = 'rgba(0, 255, 255, 0.75)';
|
||||
ctx.lineWidth = Math.max(2, 2.5 * zDraw);
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, limR * 1.05, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
} else {
|
||||
ctx.strokeStyle = 'rgba(0, 255, 255, 0.75)';
|
||||
ctx.lineWidth = Math.max(2, 2.5 * zDraw);
|
||||
ctx.beginPath();
|
||||
for (let k = 0; k < 6; k++) {
|
||||
const ang = (k / 6) * Math.PI * 2 - Math.PI / 2;
|
||||
@@ -12387,7 +12667,6 @@
|
||||
else ctx.lineTo(px, py);
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = 'rgba(0, 40, 60, 0.35)';
|
||||
ctx.fill();
|
||||
|
||||
@@ -12409,31 +12688,119 @@
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
|
||||
ctx.fillStyle = 'rgba(255, 100, 200, 0.9)';
|
||||
ctx.font = `bold ${Math.max(10, 11 * zDraw)}px ui-monospace, monospace`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('MEGA VIRUS : [ ' + hpLeft + ' / ' + maxHp + ' ]', 0, -bossR * zDraw - 14);
|
||||
const barW = bossR * zDraw * 2.2;
|
||||
const barH = Math.max(5, 6 * zDraw);
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.55)';
|
||||
ctx.fillRect(-barW / 2, -bossR * zDraw - 8, barW, barH);
|
||||
ctx.fillStyle = 'rgba(255, 80, 200, 0.85)';
|
||||
ctx.fillRect(-barW / 2, -bossR * zDraw - 8, barW * (hpLeft / maxHp), barH);
|
||||
ctx.strokeStyle = 'rgba(180, 255, 255, 0.5)';
|
||||
ctx.strokeRect(-barW / 2, -bossR * zDraw - 8, barW, barH);
|
||||
ctx.restore();
|
||||
/** HUD บอส: แถบชิดเหนือหัวบอส; โลโก้ Artboard10 + [hp/max] เหนือแถบ — bbHudS ลดสเกลทั้งแถว (ขอให้เล็กลง ~ครึ่ง) */
|
||||
const bbHudS = 0.5;
|
||||
const fontPx = Math.max(8, 12.5 * zDraw * bbHudS);
|
||||
ctx.font = `bold ${fontPx}px ui-monospace, monospace`;
|
||||
ctx.textBaseline = 'middle';
|
||||
const hpStr = '[ ' + hpLeft + ' / ' + maxHp + ' ]';
|
||||
const hpW = ctx.measureText(hpStr).width;
|
||||
const gapHp = 5 * zDraw * bbHudS;
|
||||
|
||||
/** ทิศออกจากบอสบนจอ (isotropic) — วางลูกโป่งฝั่งนอกตาม mock */
|
||||
function balloonBossScreenOutwardUnit(worldX, worldY) {
|
||||
const [sx0, sy0] = worldToScreen(bossC.cx, bossC.cy);
|
||||
const [sx1, sy1] = worldToScreen(worldX, worldY);
|
||||
let dx = sx1 - sx0, dy = sy1 - sy0;
|
||||
const L = Math.sqrt(dx * dx + dy * dy) || 1;
|
||||
dx /= L;
|
||||
dy /= L;
|
||||
return { ux: dx, uy: dy, ang: Math.atan2(dy, dx) };
|
||||
const powBgUrl = normalizeGauntletAssetUrlForPlay('/Game/img/MegaVirus/boss-power-bg.png');
|
||||
const powFillUrl = normalizeGauntletAssetUrlForPlay('/Game/img/MegaVirus/boss-power.png');
|
||||
const powBgRec = powBgUrl ? ensureGauntletAssetImage(powBgUrl) : null;
|
||||
const powFillRec = powFillUrl ? ensureGauntletAssetImage(powFillUrl) : null;
|
||||
|
||||
/** ความสูงหลอด: ยึดความกว้างก่อน — คูณ bbHudS ให้สัดส่วนเทียบหัวบอสเล็กลง */
|
||||
const barHMin = Math.max(9, 10.5 * zDraw) * bbHudS;
|
||||
const barHMax = Math.max(17, 21 * zDraw) * bbHudS;
|
||||
|
||||
const titleImgUrl = normalizeGauntletAssetUrlForPlay('/Game/img/MegaVirus/Artboard%2010.png');
|
||||
const titleImgRec = titleImgUrl ? ensureGauntletAssetImage(titleImgUrl) : null;
|
||||
let imgW = 0;
|
||||
let imgH = 0;
|
||||
if (titleImgRec && titleImgRec.ready && titleImgRec.img && titleImgRec.img.naturalWidth > 0) {
|
||||
const iw = titleImgRec.img.naturalWidth;
|
||||
const ih = titleImgRec.img.naturalHeight;
|
||||
let barHForTitle = barHMin;
|
||||
if (powBgRec && powBgRec.ready && powBgRec.img && powBgRec.img.naturalWidth > 0) {
|
||||
const biw = powBgRec.img.naturalWidth;
|
||||
const bijh = powBgRec.img.naturalHeight;
|
||||
const wEst = Math.max(limR * 2.12 * bbHudS, hpW + gapHp + iw * 0.35 * bbHudS);
|
||||
barHForTitle = Math.min(barHMax, Math.max(barHMin, bijh * (wEst / biw)));
|
||||
}
|
||||
const capTitle = Math.min(17 * zDraw * bbHudS, Math.max(fontPx * 1.15, barHForTitle * 0.88));
|
||||
const maxH = Math.max(fontPx * 1.12, 14 * zDraw * bbHudS, capTitle);
|
||||
const sc = Math.min(1.0, maxH / ih);
|
||||
imgW = iw * sc;
|
||||
imgH = ih * sc;
|
||||
}
|
||||
|
||||
let rowW = (imgW > 0 ? imgW + gapHp : 0) + hpW;
|
||||
if (imgW <= 0) {
|
||||
const legacyPre = 'MEGA VIRUS : ';
|
||||
rowW = ctx.measureText(legacyPre).width + hpW;
|
||||
}
|
||||
const barPadRow = 8 * zDraw * bbHudS;
|
||||
const barWTarget = Math.max(limR * 2.08 * bbHudS, rowW + barPadRow);
|
||||
|
||||
let barDw = barWTarget;
|
||||
let barDh = barHMin;
|
||||
if (powBgRec && powBgRec.ready && powBgRec.img && powBgRec.img.naturalWidth > 0) {
|
||||
const iw = powBgRec.img.naturalWidth;
|
||||
const ih = powBgRec.img.naturalHeight;
|
||||
const naturalH = ih * (barWTarget / iw);
|
||||
barDh = Math.min(barHMax, Math.max(barHMin, naturalH));
|
||||
}
|
||||
|
||||
const gapBarBoss = 4 * zDraw * bbHudS;
|
||||
const gapTitleBar = 5 * zDraw * bbHudS;
|
||||
const barBottomEdge = -limR * 1.0 - gapBarBoss;
|
||||
const barTopY = barBottomEdge - barDh;
|
||||
const bx = -barDw / 2;
|
||||
const by = barTopY;
|
||||
const ratHp = Math.max(0, Math.min(1, hpLeft / maxHp));
|
||||
|
||||
const titleRowH = Math.max(imgH, fontPx * 1.12);
|
||||
const labelY = barTopY - gapTitleBar - titleRowH * 0.5;
|
||||
|
||||
let xLeft = -(imgW + (imgW > 0 ? gapHp : 0) + hpW) / 2;
|
||||
if (imgW > 0) {
|
||||
ctx.drawImage(titleImgRec.img, xLeft, labelY - imgH / 2, imgW, imgH);
|
||||
xLeft += imgW + gapHp;
|
||||
} else {
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillStyle = 'rgba(255, 100, 200, 0.9)';
|
||||
const legacyPre = 'MEGA VIRUS : ';
|
||||
const pw = ctx.measureText(legacyPre).width;
|
||||
xLeft = -(pw + hpW) / 2;
|
||||
ctx.fillText(legacyPre, xLeft, labelY);
|
||||
xLeft += pw;
|
||||
}
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.strokeStyle = 'rgba(0, 0, 0, 0.22)';
|
||||
ctx.lineWidth = Math.max(0.5, 1 * zDraw * bbHudS);
|
||||
ctx.strokeText(hpStr, xLeft, labelY);
|
||||
ctx.fillText(hpStr, xLeft, labelY);
|
||||
|
||||
if (powBgRec && powBgRec.ready && powBgRec.img && powBgRec.img.naturalWidth > 0) {
|
||||
ctx.drawImage(powBgRec.img, bx, by, barDw, barDh);
|
||||
if (ratHp > 0 && powFillRec && powFillRec.ready && powFillRec.img && powFillRec.img.naturalWidth > 0) {
|
||||
const fiw = powFillRec.img.naturalWidth;
|
||||
const fih = powFillRec.img.naturalHeight;
|
||||
const padIn = barDw * 0.04;
|
||||
const innerW = barDw - padIn * 2;
|
||||
const innerH = barDh - padIn * 1.2;
|
||||
const fsc2 = Math.min(innerW / fiw, innerH / fih);
|
||||
const fdw2 = fiw * fsc2;
|
||||
const fdh2 = fih * fsc2;
|
||||
const fx0 = bx + (barDw - fdw2) * 0.5;
|
||||
const fy0 = by + (barDh - fdh2) * 0.5;
|
||||
const srcW = fiw * ratHp;
|
||||
ctx.drawImage(powFillRec.img, 0, 0, srcW, fih, fx0, fy0, fdw2 * ratHp, fdh2);
|
||||
}
|
||||
} else {
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.55)';
|
||||
ctx.fillRect(bx, by, barDw, barDh);
|
||||
ctx.fillStyle = 'rgba(255, 80, 200, 0.85)';
|
||||
ctx.fillRect(bx, by, barDw * ratHp, barDh);
|
||||
ctx.strokeStyle = 'rgba(180, 255, 255, 0.5)';
|
||||
ctx.strokeRect(bx, by, barDw, barDh);
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
/** จุดก้นลูกโป่งหลังหมุน tilt (รัศมีจากกลางสู่ก้นใน local) */
|
||||
function balloonBossBalloonKnotScreen(bx, by, dw, dh, tiltRad) {
|
||||
const lx = 0;
|
||||
@@ -12538,7 +12905,10 @@
|
||||
const ringR = 25 * z;
|
||||
/** จุดกลางฟองบนจอ — ยกจากจุด world เล็กน้อยให้ตัวอยู่กลางวง ลูกโป่งชิดขอบบน (ตาม mock) */
|
||||
const bubbleCy = sy - ringR * 0.42;
|
||||
const { ux: outUx, uy: outUy } = balloonBossScreenOutwardUnit(ent.balloonBossCx, ent.balloonBossCy);
|
||||
/** มุมลูกศร Artboard 9 — หมุนอัตโนมัติ; Mega Virus ยิงตามมุมนี้ ณ ตอนกด Space */
|
||||
const angRing = (typeof ent.balloonBossAimRad === 'number' && Number.isFinite(ent.balloonBossAimRad))
|
||||
? ent.balloonBossAimRad
|
||||
: ((timeMs * 0.0019 + idx * 1.73) % (Math.PI * 2));
|
||||
/** หันหน้าเข้ากล้อง (mock) — ไม่ใช้ทิศออกจากบอสในเลเยอร์นี้ */
|
||||
const faceDir = 'down';
|
||||
const slotIdxRing = Math.max(0, Math.min(5, skinSlot - 1));
|
||||
@@ -12559,8 +12929,7 @@
|
||||
const sc = Math.min(maxBox / iw, maxBox / ih);
|
||||
const dw = iw * sc;
|
||||
const dh = ih * sc;
|
||||
/** หางฟอง (Artboard +Y) หมุนให้ชี้ออกจากบอส — tail points +canvas Y in asset */
|
||||
const angRing = Math.atan2(outUy, outUx);
|
||||
/** หางฟอง (Artboard +Y) หมุนตาม angRing — tail ชี้ทิศยิง */
|
||||
ctx.save();
|
||||
ctx.translate(sx, bubbleCy);
|
||||
ctx.rotate(angRing - Math.PI / 2);
|
||||
@@ -12617,23 +12986,36 @@
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
const bulletCodeUrl = normalizeGauntletAssetUrlForPlay('/Game/img/MegaVirus/bullet-code.png');
|
||||
const bulletCodeRec = bulletCodeUrl ? ensureGauntletAssetImage(bulletCodeUrl) : null;
|
||||
for (let i = 0; i < balloonBossPlayerBullets.length; i++) {
|
||||
const b = balloonBossPlayerBullets[i];
|
||||
const [bx, by] = worldToScreen(b.x, b.y);
|
||||
ctx.save();
|
||||
ctx.translate(bx, by);
|
||||
ctx.rotate(Math.atan2(b.vy, b.vx) + Math.PI / 2);
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.95)';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -8 * zDraw);
|
||||
ctx.lineTo(-5 * zDraw, 6 * zDraw);
|
||||
ctx.lineTo(5 * zDraw, 6 * zDraw);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.fillStyle = 'rgba(0, 255, 200, 0.5)';
|
||||
ctx.font = `${Math.max(7, 8 * zDraw)}px monospace`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('01001', 0, 12 * zDraw);
|
||||
/** bullet-code.png หัวลูกศรชี้ขวา (+x) — หมุนตามทิศความเร็วโดยไม่บวก π/2 (ก่อนหน้านี้ดูเหมือนยิงเฉียง) */
|
||||
ctx.rotate(Math.atan2(b.vy, b.vx));
|
||||
if (bulletCodeRec && bulletCodeRec.ready && bulletCodeRec.img && bulletCodeRec.img.naturalWidth > 0) {
|
||||
const iw = bulletCodeRec.img.naturalWidth;
|
||||
const ih = bulletCodeRec.img.naturalHeight;
|
||||
const mh = Math.max(10, 15 * zDraw);
|
||||
const sc = Math.min(1.4, mh / ih);
|
||||
const dw = iw * sc;
|
||||
const dh = ih * sc;
|
||||
ctx.drawImage(bulletCodeRec.img, -dw / 2, -dh / 2, dw, dh);
|
||||
} else {
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.95)';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -8 * zDraw);
|
||||
ctx.lineTo(-5 * zDraw, 6 * zDraw);
|
||||
ctx.lineTo(5 * zDraw, 6 * zDraw);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.fillStyle = 'rgba(0, 255, 200, 0.5)';
|
||||
ctx.font = `${Math.max(7, 8 * zDraw)}px monospace`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('01001', 0, 12 * zDraw);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
for (let i = 0; i < balloonBossBossBullets.length; i++) {
|
||||
@@ -13658,6 +14040,8 @@
|
||||
const bSt = balloonBossBalloonsStartPlay();
|
||||
const bs0 = Number(myPeer && myPeer.balloonBossScore);
|
||||
me.balloonBossScore = Number.isFinite(bs0) ? Math.max(0, bs0) : 0;
|
||||
const bd0 = Number(myPeer && myPeer.balloonBossBossDmg);
|
||||
me.balloonBossBossDmg = Number.isFinite(bd0) ? Math.max(0, bd0) : 0;
|
||||
const bb0 = Number(myPeer && myPeer.balloonBossBalloons);
|
||||
me.balloonBossBalloons = Number.isFinite(bb0) ? Math.max(0, Math.floor(bb0)) : bSt;
|
||||
me.balloonBossEliminated = !!(myPeer && myPeer.balloonBossEliminated);
|
||||
@@ -13699,6 +14083,7 @@
|
||||
jumpSurviveEliminated: false,
|
||||
spaceShooterScore: (() => { const s = Number(p.spaceShooterScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(),
|
||||
balloonBossScore: (() => { const s = Number(p.balloonBossScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(),
|
||||
balloonBossBossDmg: (() => { const s = Number(p.balloonBossBossDmg); return Number.isFinite(s) ? Math.max(0, s) : 0; })(),
|
||||
balloonBossBalloons: (() => {
|
||||
const s = Number(p.balloonBossBalloons);
|
||||
return Number.isFinite(s) ? Math.max(0, Math.floor(s)) : balloonBossBalloonsStartPlay();
|
||||
@@ -13839,6 +14224,7 @@
|
||||
balloonBossPlayerBullets = [];
|
||||
balloonBossBossBullets = [];
|
||||
balloonBossHitFx = [];
|
||||
balloonBossScorePopups = [];
|
||||
balloonBossLastTickMs = performance.now();
|
||||
balloonBossPlayerFireCd = 0;
|
||||
balloonBossSessionStartMs = performance.now();
|
||||
@@ -14036,6 +14422,7 @@
|
||||
jumpSurviveEliminated: false,
|
||||
spaceShooterScore: (() => { const s = Number(data.spaceShooterScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(),
|
||||
balloonBossScore: (() => { const s = Number(data.balloonBossScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(),
|
||||
balloonBossBossDmg: (() => { const s = Number(data.balloonBossBossDmg); return Number.isFinite(s) ? Math.max(0, s) : 0; })(),
|
||||
balloonBossBalloons: (() => {
|
||||
const s = Number(data.balloonBossBalloons);
|
||||
return Number.isFinite(s) ? Math.max(0, Math.floor(s)) : balloonBossBalloonsStartPlay();
|
||||
@@ -14134,6 +14521,10 @@
|
||||
const s = Number(data.balloonBossScore);
|
||||
if (Number.isFinite(s)) me.balloonBossScore = Math.max(0, s);
|
||||
}
|
||||
if (data.balloonBossBossDmg != null) {
|
||||
const d = Number(data.balloonBossBossDmg);
|
||||
if (Number.isFinite(d)) me.balloonBossBossDmg = Math.max(0, d);
|
||||
}
|
||||
if (data.balloonBossBalloons != null) {
|
||||
const b = Number(data.balloonBossBalloons);
|
||||
if (Number.isFinite(b)) me.balloonBossBalloons = Math.max(0, Math.floor(b));
|
||||
@@ -14201,6 +14592,10 @@
|
||||
const s = Number(data.balloonBossScore);
|
||||
if (Number.isFinite(s)) o.balloonBossScore = Math.max(0, s);
|
||||
}
|
||||
if (data.balloonBossBossDmg != null) {
|
||||
const d = Number(data.balloonBossBossDmg);
|
||||
if (Number.isFinite(d)) o.balloonBossBossDmg = Math.max(0, d);
|
||||
}
|
||||
if (data.balloonBossBalloons != null) {
|
||||
const b = Number(data.balloonBossBalloons);
|
||||
if (Number.isFinite(b)) o.balloonBossBalloons = Math.max(0, Math.floor(b));
|
||||
@@ -14682,15 +15077,18 @@
|
||||
balloonBossPlayerBullets = [];
|
||||
balloonBossBossBullets = [];
|
||||
balloonBossHitFx = [];
|
||||
balloonBossScorePopups = [];
|
||||
balloonBossLastTickMs = performance.now();
|
||||
balloonBossPlayerFireCd = 0;
|
||||
balloonBossLastMoveEmit = 0;
|
||||
balloonBossBossFireAcc = 0;
|
||||
me.balloonBossScore = 0;
|
||||
me.balloonBossBossDmg = 0;
|
||||
me.balloonBossBalloons = balloonBossBalloonsStartPlay();
|
||||
me.balloonBossEliminated = false;
|
||||
others.forEach((o) => {
|
||||
o.balloonBossScore = 0;
|
||||
o.balloonBossBossDmg = 0;
|
||||
o.balloonBossBalloons = balloonBossBalloonsStartPlay();
|
||||
o.balloonBossEliminated = false;
|
||||
});
|
||||
@@ -16103,6 +16501,7 @@
|
||||
const previewEl = document.getElementById('play-cyber-preview-line');
|
||||
|
||||
if (timeSub) {
|
||||
const bbMegaLiveHud = isBalloonBoss() && isMegaVirusMissionShellMapPlay() && !isGauntletCrownPregameBlockingPlay();
|
||||
timeSub.textContent = isQuizQuestionMissionHudActivePlay()
|
||||
? (playQuizPhaseLocal === 'read' ? 'READ · อ่านคำถาม'
|
||||
: (playQuizPhaseLocal === 'answer' ? 'ANSWER · เดินไปโซน จริง / เท็จ'
|
||||
@@ -16115,12 +16514,14 @@
|
||||
? 'QUIZ CARRY · COURT — หยิบป้ายถูกแล้วส่งที่ฮับ (F / Grab)'
|
||||
: (isGauntlet() ? 'GAUNTLET · SURVIVAL RUN'
|
||||
: (isSpaceShooter() ? 'SPACE SHOOTER · ARCADE'
|
||||
: (isBalloonBoss() ? 'BALLOON BOSS · MEGA VIRUS'
|
||||
: (isBalloonBoss() ? (bbMegaLiveHud ? '' : 'BALLOON BOSS · MEGA VIRUS')
|
||||
: (isJumpSurviveMissionUiMapPlay() ? 'JUMPER · SURVIVE — เหลือเวลา (วินาที) · TIME (sec)' : 'JUMP SURVIVE · NODE UPLINK')))))));
|
||||
timeSub.style.display = bbMegaLiveHud ? 'none' : '';
|
||||
}
|
||||
const zoomHintEl = document.getElementById('play-cyber-embed-zoom-hint');
|
||||
if (zoomHintEl) {
|
||||
const showEmbedZoom = !!(previewMode && editorEmbedReturn && mapData && !isQuizQuestionMissionHudActivePlay());
|
||||
const showEmbedZoom = !!(previewMode && editorEmbedReturn && mapData && !isQuizQuestionMissionHudActivePlay()
|
||||
&& !(isBalloonBoss() && isMegaVirusMissionShellMapPlay()));
|
||||
if (showEmbedZoom) {
|
||||
const zNum = Number(playEmbedUserZoomMul.toFixed(2));
|
||||
zoomHintEl.textContent = '×' + String(zNum);
|
||||
@@ -16340,7 +16741,9 @@
|
||||
? 'จบแล้ว · Session ended — see overlay'
|
||||
: (me.balloonBossEliminated
|
||||
? 'คุณตกรอบแล้ว — ดูเพื่อนเล่น · You are out — spectate'
|
||||
: 'ยานมีแรงเฉื่อย — A D / arrows / W S เร่งทิศทาง · ปล่อยปุ่มแล้วยังไหล (แรงหน่วง) · Space = ยิง (ดีเลย์) · ลูกโป้งหมด = ตกรอบ');
|
||||
: (isMegaVirusMissionShellMapPlay()
|
||||
? 'ลูกศรบนวงหมุนเอง — ยิงตามมุมลูกศร ณ ตอนกด Space · Ring spins · Space = fire along arrow'
|
||||
: 'ยานมีแรงเฉื่อย — A D / arrows / W S เร่งทิศทาง · ปล่อยปุ่มแล้วยังไหล (แรงหน่วง) · Space = ยิง (ดีเลย์) · ลูกโป้งหมด = ตกรอบ'));
|
||||
} else if (isGauntletCrownHeistMapPlay()) {
|
||||
hintEl.textContent = 'Space / W / ↑ jump · Start 100 pts · Hit -10 · Fall off left edge = out · Rank bonus at end';
|
||||
} else {
|
||||
|
||||
@@ -3203,7 +3203,7 @@
|
||||
</div>
|
||||
<script src="/Game/socket.io/socket.io.js"></script>
|
||||
<script src="js/version.js?v=0.0306"></script>
|
||||
<script src="js/play.js?v=0.0413"></script>
|
||||
<script src="js/play.js?v=0.0431"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+21
-3
@@ -879,8 +879,18 @@ function decodeAssetUrlPercentRuns(t) {
|
||||
return base + query;
|
||||
}
|
||||
|
||||
/** ช่อง Admin มักใส่แค่ …/Artboard ลืม 9.png — ชี้ไป Artboard%209.png */
|
||||
function fixBareMegaVirusArtboardUrl(t) {
|
||||
const s = String(t || '').trim().replace(/\/+$/, '');
|
||||
if (/^\/Game\/img\/MegaVirus\/Artboard$/i.test(s)) return '/Game/img/MegaVirus/Artboard%209.png';
|
||||
if (/^Game\/img\/MegaVirus\/Artboard$/i.test(s)) return '/Game/img/MegaVirus/Artboard%209.png';
|
||||
return t;
|
||||
}
|
||||
|
||||
function sanitizeGauntletAssetUrl(s) {
|
||||
const t = normalizeGameAssetUrlForWeb(decodeAssetUrlPercentRuns(String(s || '').trim())).replace(/\/+$/, '');
|
||||
const t = fixBareMegaVirusArtboardUrl(
|
||||
normalizeGameAssetUrlForWeb(decodeAssetUrlPercentRuns(String(s || '').trim())).replace(/\/+$/, '')
|
||||
);
|
||||
if (!t || t.length > 500) return '';
|
||||
/** ห้ามแท็บ/บรรทัดใหม่/ตัวอักษรอันตราย — อนุญาตช่องว่าง (U+0020) ในชื่อไฟล์ */
|
||||
if (/[\t\n\r\x00-\x08\x0b\x0c\x0e-\x1f<>"'`]/.test(t)) return '';
|
||||
@@ -3910,7 +3920,7 @@ io.on('connection', (socket) => {
|
||||
id: socket.id, x: +spawnPt.x, y: +spawnPt.y, direction: 'down', nickname: nickname || 'ผู้เล่น', ready: false, characterId: characterId || null, voiceMicOn: true,
|
||||
spawnJoinOrder,
|
||||
gauntletJumpTicks: 0, gauntletScore: 0, gauntletJumpPending: false, gauntletEliminated: false, spaceShooterScore: 0,
|
||||
balloonBossScore: 0, balloonBossBalloons: mdJoin.gameType === 'balloon_boss' ? bbStartBalloons : 5, balloonBossEliminated: false,
|
||||
balloonBossScore: 0, balloonBossBossDmg: 0, balloonBossBalloons: mdJoin.gameType === 'balloon_boss' ? bbStartBalloons : 5, balloonBossEliminated: false,
|
||||
};
|
||||
if (mdJoin.gameType === 'quiz_battle' && quizBattlePathModeActiveServer(mdJoin)) {
|
||||
const sn = snapPositionOntoQuizBattlePathServer(mdJoin, Number(peer.x) + 0.5, Number(peer.y) + 0.5);
|
||||
@@ -4528,7 +4538,14 @@ io.on('connection', (socket) => {
|
||||
const ns = Math.floor(Number(data.balloonBossScore));
|
||||
if (Number.isFinite(ns) && ns >= 0) {
|
||||
const prev = Math.max(0, p.balloonBossScore | 0);
|
||||
if (ns <= prev + 25) p.balloonBossScore = Math.max(prev, ns);
|
||||
if (ns <= prev + 35) p.balloonBossScore = Math.max(prev, ns);
|
||||
}
|
||||
}
|
||||
if (data.balloonBossBossDmg != null) {
|
||||
const nd = Math.floor(Number(data.balloonBossBossDmg));
|
||||
if (Number.isFinite(nd) && nd >= 0) {
|
||||
const prevD = Math.max(0, p.balloonBossBossDmg | 0);
|
||||
if (nd <= prevD + 8) p.balloonBossBossDmg = Math.max(prevD, nd);
|
||||
}
|
||||
}
|
||||
if (data.balloonBossBalloons != null) {
|
||||
@@ -4551,6 +4568,7 @@ io.on('connection', (socket) => {
|
||||
if (md && md.gameType === 'balloon_boss') {
|
||||
const bbDef = Math.max(1, Math.min(12, Math.floor(Number(md.balloonBossBalloonsPerPlayer)) || 3));
|
||||
out.balloonBossScore = Math.max(0, p.balloonBossScore | 0);
|
||||
out.balloonBossBossDmg = Math.max(0, p.balloonBossBossDmg | 0);
|
||||
out.balloonBossBalloons = typeof p.balloonBossBalloons === 'number' && Number.isFinite(p.balloonBossBalloons)
|
||||
? Math.max(0, Math.floor(p.balloonBossBalloons))
|
||||
: bbDef;
|
||||
|
||||
Reference in New Issue
Block a user