minigame 7 Mega Virus 1.3
This commit is contained in:
@@ -65,6 +65,12 @@ server {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
# รูปใต้ /Game/img/ (ยกเว้น characters ด้านบน) — ห้าม try_files ไป index.html เมื่อไม่เจอไฟล์ (ไม่งั้น <img> ได้ HTML แล้วแตกเงียบ ๆ)
|
||||
location ^~ /Game/img/ {
|
||||
alias /var/www/html/Game/public/img/;
|
||||
try_files $uri =404;
|
||||
access_log off;
|
||||
}
|
||||
# Game — API ต้องรับ body ใหญ่ (รูปพื้นหลัง base64) จึงตั้ง 20M
|
||||
location /Game/api/ {
|
||||
client_max_body_size 20M;
|
||||
|
||||
+25
-1
@@ -3357,9 +3357,33 @@
|
||||
return pathPart.replace(/ /g, '%20') + query;
|
||||
}
|
||||
|
||||
function decodeAssetUrlPercentRuns(t) {
|
||||
var s = String(t || '');
|
||||
var q = s.indexOf('?');
|
||||
var base = q === -1 ? s : s.slice(0, q);
|
||||
var query = q === -1 ? '' : s.slice(q);
|
||||
for (var i = 0; i < 2; i++) {
|
||||
try {
|
||||
var d = decodeURIComponent(base);
|
||||
if (d === base) break;
|
||||
base = d;
|
||||
} catch (e) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return base + query;
|
||||
}
|
||||
|
||||
function normalizeGameAssetUrlForWebAdmin(t) {
|
||||
var s = String(t || '');
|
||||
s = s.replace(/^\/Game\/public\/img\//i, '/Game/img/');
|
||||
s = s.replace(/^Game\/public\/img\//i, 'Game/img/');
|
||||
return s;
|
||||
}
|
||||
|
||||
/** ให้สอดคล้องกับ server sanitizeGauntletAssetUrl + กัน placeholder .... */
|
||||
function normalizeMegaVirusAssetUrl(v) {
|
||||
var t = v != null ? String(v).trim().replace(/\/+$/, '') : '';
|
||||
var t = v != null ? normalizeGameAssetUrlForWebAdmin(decodeAssetUrlPercentRuns(String(v).trim())).replace(/\/+$/, '') : '';
|
||||
if (!t || t.length > 500) return '';
|
||||
if (/\.{4,}/.test(t)) return '';
|
||||
if (/[\t\n\r<>"'`]/.test(t)) return '';
|
||||
|
||||
@@ -620,9 +620,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) · ห้ามใส่จุดสี่จุดจาก placeholder (<code>....</code>) — ต้องเป็น path ไฟล์จริง · <em>English:</em> Fallback when a P slot is empty <strong>or</strong> its image fails to load; <code>....</code> is rejected.</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) · <em>English:</em> Use <code>/Game/img/...</code> only, not <code>/Game/public/img/...</code>.</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/balloon-default.png" autocomplete="off"></label>
|
||||
<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>
|
||||
<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>
|
||||
@@ -895,6 +895,6 @@
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<script src="admin.js?v=66"></script>
|
||||
<script src="admin.js?v=68"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
File diff suppressed because one or more lines are too long
+198
-49
@@ -1271,10 +1271,34 @@
|
||||
return pathPart.replace(/ /g, '%20') + query;
|
||||
}
|
||||
|
||||
function decodeAssetUrlPercentRuns(t) {
|
||||
let s = String(t || '');
|
||||
const q = s.indexOf('?');
|
||||
let base = q === -1 ? s : s.slice(0, q);
|
||||
const query = q === -1 ? '' : s.slice(q);
|
||||
for (let i = 0; i < 2; i += 1) {
|
||||
try {
|
||||
const d = decodeURIComponent(base);
|
||||
if (d === base) break;
|
||||
base = d;
|
||||
} catch (e) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return base + query;
|
||||
}
|
||||
|
||||
function normalizeGameAssetUrlForWebPlay(t) {
|
||||
let s = String(t || '');
|
||||
s = s.replace(/^\/Game\/public\/img\//i, '/Game/img/');
|
||||
s = s.replace(/^Game\/public\/img\//i, 'Game/img/');
|
||||
return s;
|
||||
}
|
||||
|
||||
/** แปลง URL จาก Admin/game-timing ให้โหลดได้ (nginx เสิร์ฟจาก /Game/...) */
|
||||
function normalizeGauntletAssetUrlForPlay(u) {
|
||||
if (typeof u !== 'string') return '';
|
||||
const raw = u.trim();
|
||||
const raw = normalizeGameAssetUrlForWebPlay(decodeAssetUrlPercentRuns(u.trim()));
|
||||
if (!raw) return '';
|
||||
if (/^https?:\/\//i.test(raw)) {
|
||||
const z = raw.replace(/\/+$/, '');
|
||||
@@ -1302,6 +1326,9 @@
|
||||
rec.img.onload = function () { rec.ready = true; };
|
||||
rec.img.onerror = function () { rec.ready = false; };
|
||||
rec.img.src = u;
|
||||
/** รูปจากแคชบางครั้ง complete ก่อน onload — ต้องเซ็ต ready ทันทีไม่งั้นวาดลูกโป่งไม่ขึ้น */
|
||||
if (rec.img.complete && rec.img.naturalWidth > 0) rec.ready = true;
|
||||
else if (rec.img.complete && rec.img.naturalWidth <= 0) rec.ready = false;
|
||||
gauntletAssetImageCache.set(u, rec);
|
||||
return rec;
|
||||
}
|
||||
@@ -2912,7 +2939,7 @@
|
||||
jumpSurviveEliminated: false,
|
||||
spaceShooterScore: 0,
|
||||
balloonBossScore: 0,
|
||||
balloonBossBalloons: 5,
|
||||
balloonBossBalloons: mapData && mapData.gameType === 'balloon_boss' ? balloonBossBalloonsStartPlay() : 5,
|
||||
balloonBossEliminated: false,
|
||||
botQuizCarryPathfindAfter: performance.now() + previewBotSeq * 75 + Math.floor(Math.random() * 160),
|
||||
});
|
||||
@@ -9342,6 +9369,22 @@
|
||||
return { cx: (w * 0.5) * ts, cy: (h * 0.38) * ts };
|
||||
}
|
||||
|
||||
/** หมายเลขสล็อต 1–6 ที่มีบนแมป (เรียง) — ถ้ามีแค่ 3 ช่อง ผู้เล่นจะวนที่นั่ง 3 จุดรอบบอส · Occupied P-slots on map for spawn cycling */
|
||||
function getBalloonBossOccupiedSlotNumbersPlay(md) {
|
||||
if (!md || !md.balloonBossPlayerSlots) return [];
|
||||
const g = md.balloonBossPlayerSlots;
|
||||
const w = md.width || 20, h = md.height || 15;
|
||||
const seen = new Set();
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
const v = g[y] && g[y][x];
|
||||
const n = typeof v === 'number' ? v : parseInt(String(v), 10);
|
||||
if (Number.isFinite(n) && n >= 1 && n <= 6) seen.add(Math.floor(n));
|
||||
}
|
||||
}
|
||||
return Array.from(seen).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function buildBalloonBossParticipantRefsPlay() {
|
||||
const refs = [];
|
||||
if (me) refs.push({ id: myId, ref: me });
|
||||
@@ -9360,7 +9403,8 @@
|
||||
|
||||
function balloonBossBalloonsStartPlay() {
|
||||
const n = Number(mapData && mapData.balloonBossBalloonsPerPlayer);
|
||||
return Math.max(1, Math.min(12, Number.isFinite(n) ? Math.floor(n) : 5));
|
||||
/** ไม่มีค่าในแมป = 3 ลูก (Mega Virus mock) — ไม่ใช้ 5 เพราะบอท/preview ติด 5 */
|
||||
return Math.max(1, Math.min(12, Number.isFinite(n) ? Math.floor(n) : 3));
|
||||
}
|
||||
|
||||
function balloonBossMaxHpPlay() {
|
||||
@@ -9439,9 +9483,10 @@
|
||||
const refs = buildBalloonBossParticipantRefsPlay();
|
||||
const n = refs.length || 1;
|
||||
const bStart = balloonBossBalloonsStartPlay();
|
||||
const slotCycle = getBalloonBossOccupiedSlotNumbersPlay(mapData);
|
||||
refs.forEach((entry, idx) => {
|
||||
const ent = entry.ref;
|
||||
const slot = (idx % 6) + 1;
|
||||
const slot = slotCycle.length ? slotCycle[idx % slotCycle.length] : ((idx % 6) + 1);
|
||||
const cen = getBalloonBossPlayerSpawnWorldCenterFromMap(mapData, slot, ts);
|
||||
let cx, cy;
|
||||
if (cen) {
|
||||
@@ -9457,7 +9502,8 @@
|
||||
ent.balloonBossCx = cl.cx;
|
||||
ent.balloonBossCy = cl.cy;
|
||||
if (typeof ent.balloonBossScore !== 'number' || !Number.isFinite(ent.balloonBossScore)) ent.balloonBossScore = 0;
|
||||
if (typeof ent.balloonBossBalloons !== 'number' || !Number.isFinite(ent.balloonBossBalloons)) ent.balloonBossBalloons = bStart;
|
||||
/** จำนวนลูกโป่งตามแมปเสมอเมื่อจัดตำแหน่ง — ไม่ค้าง 5 จาก preview bot · Always sync start count */
|
||||
ent.balloonBossBalloons = bStart;
|
||||
if (typeof ent.balloonBossHitIframe !== 'number') ent.balloonBossHitIframe = 0;
|
||||
ent.balloonBossVelX = 0;
|
||||
ent.balloonBossVelY = 0;
|
||||
@@ -9465,7 +9511,7 @@
|
||||
ent.y = ent.balloonBossCy / ts - ch * 0.92;
|
||||
ent.tx = ent.x;
|
||||
ent.ty = ent.y;
|
||||
ent.direction = 'up';
|
||||
ent.direction = 'down';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9529,6 +9575,7 @@
|
||||
o.balloonBossCx = cl.cx;
|
||||
o.balloonBossCy = cl.cy;
|
||||
syncBalloonBossFootFromCenter(o);
|
||||
o.direction = 'down';
|
||||
o.tx = o.x;
|
||||
o.ty = o.y;
|
||||
if (typeof o.balloonBossBotNextFire === 'number') o.balloonBossBotNextFire -= dt;
|
||||
@@ -9728,7 +9775,8 @@
|
||||
me.balloonBossCy = cl.cy;
|
||||
}
|
||||
syncBalloonBossFootFromCenter(me);
|
||||
me.direction = 'up';
|
||||
/** ฉากนี้แสดงตัวหันหน้าเข้ากล้อง — sync กับ combat draw */
|
||||
me.direction = 'down';
|
||||
me.isWalking = Math.abs(me.balloonBossVelX) > 8 || Math.abs(me.balloonBossVelY) > 8 || inX !== 0 || inY !== 0;
|
||||
|
||||
balloonBossPlayerFireCd -= dt;
|
||||
@@ -9866,34 +9914,105 @@
|
||||
ctx.strokeRect(-barW / 2, -bossR * zDraw - 8, barW, barH);
|
||||
ctx.restore();
|
||||
|
||||
function drawBalloonsCluster(sx, sy, count, col, z, slot1to6) {
|
||||
/** ทิศออกจากบอสบนจอ (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) };
|
||||
}
|
||||
|
||||
/** จุดก้นลูกโป่งหลังหมุน tilt (รัศมีจากกลางสู่ก้นใน local) */
|
||||
function balloonBossBalloonKnotScreen(bx, by, dw, dh, tiltRad) {
|
||||
const lx = 0;
|
||||
const ly = dh * 0.5;
|
||||
const c = Math.cos(tiltRad);
|
||||
const s = Math.sin(tiltRad);
|
||||
return { x: bx + lx * c - ly * s, y: by + lx * s + ly * c };
|
||||
}
|
||||
|
||||
/** ลูกโป่งด้านบน +50% — กระจุกเอียงซ้าย/กลางตรง/ขวา (tilt) + เชือกบางไปจุดผูบนวง · Bouquet tilt like mock */
|
||||
function drawBalloonsCluster(sx, sy, count, col, z, slot1to6, playerRingR) {
|
||||
const slotIdx = Math.max(0, Math.min(5, (Math.floor(Number(slot1to6)) || 1) - 1));
|
||||
const perSeat = String((playBalloonBossPlayerBalloonImageUrls[slotIdx] || '')).trim();
|
||||
const fallback = String(playBalloonBossPlayerBalloonFallbackUrl || '').trim();
|
||||
const bRec = resolveBalloonBossBalloonSpriteRec(perSeat, fallback);
|
||||
const n = Math.max(0, Math.min(8, count | 0));
|
||||
const ringRpx = playerRingR != null && playerRingR > 0 ? playerRingR : 22 * z;
|
||||
const sz = 1.5;
|
||||
const tw = 22 * sz * z;
|
||||
const th = 27 * sz * z;
|
||||
const stackAnchor = ringRpx + 10 * z;
|
||||
const tieX = sx;
|
||||
const tieY = sy - ringRpx * 0.98;
|
||||
const tiltStep = 0.5;
|
||||
const spreadX = 5.4 * z;
|
||||
const baseY = sy - stackAnchor - 1.5 * z;
|
||||
const items = [];
|
||||
for (let b = 0; b < n; b++) {
|
||||
const ox = (b - (n - 1) / 2) * 7 * z;
|
||||
const oy = -18 * z - Math.abs(b - (n - 1) / 2) * 2 * z;
|
||||
const bx = sx + ox;
|
||||
const by = sy + oy;
|
||||
if (bRec && bRec.ready && bRec.img && bRec.img.naturalWidth > 0) {
|
||||
const iw = bRec.img.naturalWidth;
|
||||
const ih = bRec.img.naturalHeight;
|
||||
const tw = 13 * z;
|
||||
const th = 16 * z;
|
||||
const rel = b - (n - 1) / 2;
|
||||
const tilt = rel * tiltStep;
|
||||
const bx = sx + rel * spreadX;
|
||||
const by = baseY + Math.abs(rel) * 2.1 * z;
|
||||
const bImg = bRec && bRec.img;
|
||||
let dw = 0;
|
||||
let dh = 0;
|
||||
if (bImg && bImg.complete && bImg.naturalWidth > 0) {
|
||||
const iw = bImg.naturalWidth;
|
||||
const ih = bImg.naturalHeight;
|
||||
const sc = Math.min(tw / iw, th / ih);
|
||||
dw = iw * sc;
|
||||
dh = ih * sc;
|
||||
} else {
|
||||
const ry = 12 * sz * z;
|
||||
dw = 10 * sz * z * 2;
|
||||
dh = ry * 2;
|
||||
}
|
||||
const knot = balloonBossBalloonKnotScreen(bx, by, dw, dh, tilt);
|
||||
items.push({ bx, by, dw, dh, bImg, tilt, knotX: knot.x, knotY: knot.y });
|
||||
}
|
||||
if (n > 0) {
|
||||
ctx.strokeStyle = 'rgba(55, 48, 42, 0.55)';
|
||||
ctx.lineWidth = Math.max(0.65, 0.78 * z);
|
||||
ctx.lineCap = 'butt';
|
||||
ctx.lineJoin = 'bevel';
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const it = items[i];
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(tieX, tieY);
|
||||
ctx.lineTo(it.knotX, it.knotY);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const it = items[i];
|
||||
const bImg = it.bImg;
|
||||
if (bImg && bImg.complete && bImg.naturalWidth > 0) {
|
||||
const iw = bImg.naturalWidth;
|
||||
const ih = bImg.naturalHeight;
|
||||
const sc = Math.min(tw / iw, th / ih);
|
||||
const dw = iw * sc;
|
||||
const dh = ih * sc;
|
||||
ctx.drawImage(bRec.img, bx - dw / 2, by - dh / 2, dw, dh);
|
||||
ctx.save();
|
||||
ctx.translate(it.bx, it.by);
|
||||
ctx.rotate(it.tilt);
|
||||
ctx.drawImage(bImg, -dw / 2, -dh / 2, dw, dh);
|
||||
ctx.restore();
|
||||
} else {
|
||||
ctx.save();
|
||||
ctx.translate(it.bx, it.by);
|
||||
ctx.rotate(it.tilt);
|
||||
ctx.fillStyle = col;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(bx, by, 6.5 * z, 8 * z, 0, 0, Math.PI * 2);
|
||||
ctx.ellipse(0, 0, 10 * sz * z, 12 * sz * z, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.35)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9904,17 +10023,55 @@
|
||||
const [sx, sy] = worldToScreen(ent.balloonBossCx, ent.balloonBossCy);
|
||||
const col = BALLOON_BOSS_PLAYER_COLORS[idx % BALLOON_BOSS_PLAYER_COLORS.length];
|
||||
const z = zDraw;
|
||||
ctx.save();
|
||||
if (ent.balloonBossEliminated) ctx.globalAlpha = 0.38;
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.65)';
|
||||
ctx.lineWidth = Math.max(1.2, 1.5 * z);
|
||||
ctx.beginPath();
|
||||
ctx.arc(sx, sy, 22 * z, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
const skinSlot = (typeof ent.balloonBossSkinSlot === 'number' && ent.balloonBossSkinSlot >= 1 && ent.balloonBossSkinSlot <= 6)
|
||||
? ent.balloonBossSkinSlot
|
||||
: ((idx % 6) + 1);
|
||||
drawBalloonsCluster(sx, sy, ent.balloonBossBalloons | 0, col, z, skinSlot);
|
||||
const ringR = 25 * z;
|
||||
/** จุดกลางฟองบนจอ — ยกจากจุด world เล็กน้อยให้ตัวอยู่กลางวง ลูกโป่งชิดขอบบน (ตาม mock) */
|
||||
const bubbleCy = sy - ringR * 0.42;
|
||||
const { ux: outUx, uy: outUy } = balloonBossScreenOutwardUnit(ent.balloonBossCx, ent.balloonBossCy);
|
||||
/** หันหน้าเข้ากล้อง (mock) — ไม่ใช้ทิศออกจากบอสในเลเยอร์นี้ */
|
||||
const faceDir = 'down';
|
||||
const slotIdxRing = Math.max(0, Math.min(5, skinSlot - 1));
|
||||
const perRing = String((playBalloonBossPlayerBalloonImageUrls[slotIdxRing] || '')).trim();
|
||||
const fbRing = String(playBalloonBossPlayerBalloonFallbackUrl || '').trim();
|
||||
/** กรอบฟองรอบตัว = fallback (Artboard 9) เท่านั้น — ลูกโป่งต่อที่นั่งใช้แค่กองด้านบน · Ring frame is never per-seat balloon art */
|
||||
const ringRec = fbRing
|
||||
? resolveBalloonBossBalloonSpriteRec('', fbRing)
|
||||
: resolveBalloonBossBalloonSpriteRec(perRing, '');
|
||||
ctx.save();
|
||||
if (ent.balloonBossEliminated) ctx.globalAlpha = 0.38;
|
||||
const ringImg = ringRec && ringRec.img;
|
||||
const ringPainted = !!(ringImg && ringImg.complete && ringImg.naturalWidth > 0);
|
||||
if (ringPainted) {
|
||||
const iw = ringImg.naturalWidth;
|
||||
const ih = ringImg.naturalHeight;
|
||||
const maxBox = 2 * ringR * 1.14;
|
||||
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);
|
||||
ctx.save();
|
||||
ctx.translate(sx, bubbleCy);
|
||||
ctx.rotate(angRing - Math.PI / 2);
|
||||
ctx.drawImage(ringImg, -dw / 2, -dh / 2, dw, dh);
|
||||
ctx.restore();
|
||||
}
|
||||
if (!ringPainted) {
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.72)';
|
||||
ctx.lineWidth = Math.max(1.2, 1.6 * z);
|
||||
ctx.beginPath();
|
||||
ctx.arc(sx, bubbleCy, ringR, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
} else {
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.22)';
|
||||
ctx.lineWidth = Math.max(1, 1.1 * z);
|
||||
ctx.beginPath();
|
||||
ctx.arc(sx, bubbleCy, ringR, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
drawBalloonsCluster(sx, bubbleCy, ent.balloonBossBalloons | 0, col, z, skinSlot, ringR);
|
||||
const nm = String(ent.nickname || '').slice(0, 12);
|
||||
ctx.font = `${Math.max(9, 10 * z)}px system-ui, "Kanit", sans-serif`;
|
||||
ctx.textAlign = 'center';
|
||||
@@ -9923,11 +10080,12 @@
|
||||
ctx.lineWidth = 3;
|
||||
let label = nm;
|
||||
if (entry.id === leaderId && leaderId != null) label = '♔ ' + nm;
|
||||
ctx.strokeText(label, sx, sy + 28 * z);
|
||||
ctx.fillText(label, sx, sy + 28 * z);
|
||||
const labelY = bubbleCy + ringR + 8 * z;
|
||||
ctx.strokeText(label, sx, labelY);
|
||||
ctx.fillText(label, sx, labelY);
|
||||
const charImg = getPlayTintedAvatarSource(
|
||||
getAvatarImg(ent.characterId, 'up', timeMs, !!ent.isWalking),
|
||||
ent.characterId, 'up', timeMs, !!ent.isWalking,
|
||||
getAvatarImg(ent.characterId, faceDir, timeMs, !!ent.isWalking),
|
||||
ent.characterId, faceDir, timeMs, !!ent.isWalking,
|
||||
ent.playTint || playTintFromPeerId(entry.id)
|
||||
);
|
||||
const size = 26 * z;
|
||||
@@ -9936,10 +10094,16 @@
|
||||
const ih = charImg.tagName === 'CANVAS' ? charImg.height : charImg.naturalHeight;
|
||||
const sc = Math.min(size / iw, size / ih, 1);
|
||||
const dw = iw * sc, dh = ih * sc;
|
||||
ctx.drawImage(charImg, 0, 0, iw, ih, sx - dw / 2, sy - dh + 4 * z, dw, dh);
|
||||
const charTop = bubbleCy - dh * 0.48;
|
||||
ctx.drawImage(charImg, 0, 0, iw, ih, sx - dw / 2, charTop, dw, dh);
|
||||
} else {
|
||||
ctx.fillStyle = col;
|
||||
ctx.fillRect(sx - 10 * z, sy - 18 * z, 20 * z, 22 * z);
|
||||
ctx.beginPath();
|
||||
ctx.arc(sx, bubbleCy, 9 * z, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = 'rgba(0, 10, 30, 0.45)';
|
||||
ctx.lineWidth = Math.max(1, 1.2 * z);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
});
|
||||
@@ -13486,22 +13650,7 @@
|
||||
ctx.strokeRect(sx + 3, sy + 3, size - 6, size - 6);
|
||||
}
|
||||
}
|
||||
if (isBalloonBoss() && showGrid && mapData.balloonBossPlayerSlots) {
|
||||
const sv = mapData.balloonBossPlayerSlots[y] && mapData.balloonBossPlayerSlots[y][x];
|
||||
if (sv >= 1 && sv <= 6) {
|
||||
ctx.fillStyle = 'rgba(255, 120, 200, 0.12)';
|
||||
ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4);
|
||||
ctx.strokeStyle = 'rgba(255, 160, 220, 0.45)';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4);
|
||||
ctx.lineWidth = 1;
|
||||
ctx.fillStyle = 'rgba(255, 228, 250, 0.92)';
|
||||
ctx.font = `bold ${Math.max(8, 10 * zDraw)}px sans-serif`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('P' + sv, sx + size / 2, sy + size / 2 + 4);
|
||||
ctx.textAlign = 'left';
|
||||
}
|
||||
}
|
||||
/* ไม่วาด P1–P6 บนกริดระหว่างเล่น — ตำแหน่งไทล์คงที่ แต่ตัวละครอยู่ world px + บอทขยับ จึงไม่ตรง design · Slot labels belong in Map Editor only */
|
||||
if (isSpaceShooter() && showGrid && mapData.shooterSpawnSlots) {
|
||||
const sv = mapData.shooterSpawnSlots[y] && mapData.shooterSpawnSlots[y][x];
|
||||
if (sv >= 1 && sv <= 6) {
|
||||
|
||||
@@ -1421,7 +1421,7 @@
|
||||
left: max(10px, env(safe-area-inset-left));
|
||||
width: min(240px, 42vw);
|
||||
padding: 0.5rem 0.55rem 0.6rem;
|
||||
background: rgba(4, 8, 18, 0.82);
|
||||
background: rgba(4, 8, 18, 0.08);
|
||||
border: 1px solid rgba(0, 255, 240, 0.28);
|
||||
border-radius: 4px;
|
||||
box-shadow:
|
||||
@@ -2009,7 +2009,7 @@
|
||||
</div>
|
||||
<script src="/Game/socket.io/socket.io.js"></script>
|
||||
<script src="js/version.js?v=0.0184"></script>
|
||||
<script src="js/play.js?v=0.283"></script>
|
||||
<script src="js/play.js?v=0.297"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+32
-5
@@ -618,7 +618,8 @@ function defaultGameTiming() {
|
||||
balloonBossMissionTimeSec: 0,
|
||||
balloonBossBossImageUrl: '',
|
||||
balloonBossPlayerBalloonImageUrls: ['', '', '', '', '', ''],
|
||||
balloonBossPlayerBalloonFallbackUrl: '',
|
||||
/** กรอบฟองรอบผู้เล่น / ลูกโป่ง fallback เริ่มต้น — Artboard 9 (วง cyan +หาง) */
|
||||
balloonBossPlayerBalloonFallbackUrl: '/Game/img/MegaVirus/Artboard 9.png',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -809,8 +810,34 @@ function encodeSpacesInAssetUrlPath(t) {
|
||||
return pathPart.replace(/ /g, '%20') + query;
|
||||
}
|
||||
|
||||
/** รูปเสิร์ฟที่ `/Game/img/...` (alias ไป `public/img`) — อย่าใส่ `/Game/public/img/` จาก path บนดิสก์ */
|
||||
function normalizeGameAssetUrlForWeb(t) {
|
||||
let s = String(t || '');
|
||||
s = s.replace(/^\/Game\/public\/img\//i, '/Game/img/');
|
||||
s = s.replace(/^Game\/public\/img\//i, 'Game/img/');
|
||||
return s;
|
||||
}
|
||||
|
||||
/** ลด %2520 → %20 → space (สูงสุด 2 รอบ) — กันเข้ารหัสซ้ำจากบันทึก/คัดลอก URL */
|
||||
function decodeAssetUrlPercentRuns(t) {
|
||||
let s = String(t || '');
|
||||
const q = s.indexOf('?');
|
||||
let base = q === -1 ? s : s.slice(0, q);
|
||||
const query = q === -1 ? '' : s.slice(q);
|
||||
for (let i = 0; i < 2; i += 1) {
|
||||
try {
|
||||
const d = decodeURIComponent(base);
|
||||
if (d === base) break;
|
||||
base = d;
|
||||
} catch (e) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return base + query;
|
||||
}
|
||||
|
||||
function sanitizeGauntletAssetUrl(s) {
|
||||
const t = String(s || '').trim().replace(/\/+$/, '');
|
||||
const t = 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 '';
|
||||
@@ -3611,7 +3638,7 @@ io.on('connection', (socket) => {
|
||||
const mdJoin = (space.mapId && maps.get(space.mapId)) || space.mapData;
|
||||
const spawnJoinOrder = space.peers.size;
|
||||
const spawnPt = pickSpawnForJoin(mdJoin, spawnJoinOrder);
|
||||
const bbStartBalloons = Math.max(1, Math.min(12, Math.floor(Number(mdJoin.balloonBossBalloonsPerPlayer)) || 5));
|
||||
const bbStartBalloons = Math.max(1, Math.min(12, Math.floor(Number(mdJoin.balloonBossBalloonsPerPlayer)) || 3));
|
||||
const peer = {
|
||||
id: socket.id, x: +spawnPt.x, y: +spawnPt.y, direction: 'down', nickname: nickname || 'ผู้เล่น', ready: false, characterId: characterId || null, voiceMicOn: true,
|
||||
spawnJoinOrder,
|
||||
@@ -4204,7 +4231,7 @@ io.on('connection', (socket) => {
|
||||
}
|
||||
}
|
||||
if (p && md && md.gameType === 'balloon_boss' && data) {
|
||||
const bbDefaultBalloons = Math.max(1, Math.min(12, Math.floor(Number(md.balloonBossBalloonsPerPlayer)) || 5));
|
||||
const bbDefaultBalloons = Math.max(1, Math.min(12, Math.floor(Number(md.balloonBossBalloonsPerPlayer)) || 3));
|
||||
if (data.balloonBossScore != null) {
|
||||
const ns = Math.floor(Number(data.balloonBossScore));
|
||||
if (Number.isFinite(ns) && ns >= 0) {
|
||||
@@ -4230,7 +4257,7 @@ io.on('connection', (socket) => {
|
||||
const out = { id: socket.id, x: nx, y: ny, direction: p.direction, characterId: p.characterId };
|
||||
if (md && md.gameType === 'space_shooter') out.spaceShooterScore = Math.max(0, p.spaceShooterScore | 0);
|
||||
if (md && md.gameType === 'balloon_boss') {
|
||||
const bbDef = Math.max(1, Math.min(12, Math.floor(Number(md.balloonBossBalloonsPerPlayer)) || 5));
|
||||
const bbDef = Math.max(1, Math.min(12, Math.floor(Number(md.balloonBossBalloonsPerPlayer)) || 3));
|
||||
out.balloonBossScore = Math.max(0, p.balloonBossScore | 0);
|
||||
out.balloonBossBalloons = typeof p.balloonBossBalloons === 'number' && Number.isFinite(p.balloonBossBalloons)
|
||||
? Math.max(0, Math.floor(p.balloonBossBalloons))
|
||||
|
||||
Reference in New Issue
Block a user