Mega Virus done

This commit is contained in:
2026-05-07 18:34:00 +00:00
parent 54503c1049
commit 9d255c5384
11 changed files with 531 additions and 107 deletions
+3
View File
@@ -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 '';
+2 -2
View File
@@ -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
View File
@@ -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 {
+1 -1
View File
@@ -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
View File
@@ -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;