minigame 7 Mega Virus 1.2
This commit is contained in:
@@ -11,6 +11,18 @@ server {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
# Browsers request /favicon.ico by default — serve branded SVG (no 404 noise)
|
||||
location = /favicon.ico {
|
||||
alias /var/www/html/favicon.svg;
|
||||
default_type image/svg+xml;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# /Game/img/.../ with trailing slash = directory (no index) → was 403 Forbidden
|
||||
location ~ ^/Game/img/(?!characters/).+/$ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
include snippets/fastcgi-php.conf;
|
||||
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
/** สำรอง URL หลังอัปโหลดสำเร็จต่อช่อง — กัน race ที่ช่อง text ว่างตอน buildForSave แต่รูปอัปโหลดแล้วจริง */
|
||||
var quizCarryPlaquePendingUrlBySlot = {};
|
||||
var stackGameSaveInFlight = false;
|
||||
var megaVirusSaveInFlight = false;
|
||||
/** โหมดแทนที่รูป: ชื่อไฟล์เป้าหมายก่อนเปิด file picker */
|
||||
var gauntletReplaceTarget = null;
|
||||
var cred = { credentials: 'include' };
|
||||
@@ -3348,6 +3349,202 @@
|
||||
});
|
||||
}
|
||||
|
||||
function encodeSpacesInUrlPath(t) {
|
||||
var s = String(t || '');
|
||||
var q = s.indexOf('?');
|
||||
var pathPart = q === -1 ? s : s.slice(0, q);
|
||||
var query = q === -1 ? '' : s.slice(q);
|
||||
return pathPart.replace(/ /g, '%20') + query;
|
||||
}
|
||||
|
||||
/** ให้สอดคล้องกับ server sanitizeGauntletAssetUrl + กัน placeholder .... */
|
||||
function normalizeMegaVirusAssetUrl(v) {
|
||||
var t = v != null ? String(v).trim().replace(/\/+$/, '') : '';
|
||||
if (!t || t.length > 500) return '';
|
||||
if (/\.{4,}/.test(t)) return '';
|
||||
if (/[\t\n\r<>"'`]/.test(t)) return '';
|
||||
if (/^https?:\/\//i.test(t)) return encodeSpacesInUrlPath(t);
|
||||
if (/^Game\//i.test(t)) return encodeSpacesInUrlPath('/' + t.replace(/^\/+/, ''));
|
||||
if (t.charAt(0) === '/') return encodeSpacesInUrlPath(t);
|
||||
return '';
|
||||
}
|
||||
|
||||
function updateMegaVirusBossPreview() {
|
||||
var img = el('mega-virus-boss-prev');
|
||||
var inp = el('mega-virus-boss-url');
|
||||
if (!img) return;
|
||||
var v = normalizeMegaVirusAssetUrl(inp && inp.value ? String(inp.value).trim() : '');
|
||||
if (!v) {
|
||||
img.removeAttribute('src');
|
||||
img.alt = '';
|
||||
return;
|
||||
}
|
||||
img.alt = 'Boss preview';
|
||||
img.src = v;
|
||||
}
|
||||
|
||||
function updateMegaVirusBalloonPreview(slot) {
|
||||
var img = el('mega-virus-balloon-prev-' + slot);
|
||||
var inp = el('mega-virus-balloon-url-' + slot);
|
||||
if (!img) return;
|
||||
var v = normalizeMegaVirusAssetUrl(inp && inp.value ? String(inp.value).trim() : '');
|
||||
if (!v) {
|
||||
img.removeAttribute('src');
|
||||
img.alt = '';
|
||||
return;
|
||||
}
|
||||
img.alt = 'Balloon P' + slot;
|
||||
img.src = v;
|
||||
}
|
||||
|
||||
function updateMegaVirusBalloonFallbackPreview() {
|
||||
var img = el('mega-virus-balloon-fallback-prev');
|
||||
var inp = el('mega-virus-balloon-fallback-url');
|
||||
if (!img) return;
|
||||
var v = normalizeMegaVirusAssetUrl(inp && inp.value ? String(inp.value).trim() : '');
|
||||
if (!v) {
|
||||
img.removeAttribute('src');
|
||||
img.alt = '';
|
||||
return;
|
||||
}
|
||||
img.alt = 'Fallback balloon';
|
||||
img.src = v;
|
||||
}
|
||||
|
||||
function readMegaVirusBalloonUrlsFromForm() {
|
||||
var urls = [];
|
||||
for (var i = 1; i <= 6; i++) {
|
||||
var inp = el('mega-virus-balloon-url-' + i);
|
||||
urls.push(normalizeMegaVirusAssetUrl(inp && inp.value ? String(inp.value).trim() : ''));
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
function bindMegaVirusPanel() {
|
||||
var formMv = el('form-mega-virus-timing');
|
||||
if (formMv) {
|
||||
formMv.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
saveMegaVirusTimingPanel();
|
||||
});
|
||||
}
|
||||
var bossInp = el('mega-virus-boss-url');
|
||||
if (bossInp) {
|
||||
bossInp.addEventListener('change', updateMegaVirusBossPreview);
|
||||
bossInp.addEventListener('blur', updateMegaVirusBossPreview);
|
||||
}
|
||||
var bossClr = el('btn-mega-virus-boss-clear');
|
||||
if (bossClr) {
|
||||
bossClr.addEventListener('click', function () {
|
||||
var i = el('mega-virus-boss-url');
|
||||
if (i) i.value = '';
|
||||
updateMegaVirusBossPreview();
|
||||
});
|
||||
}
|
||||
for (var s = 1; s <= 6; s++) {
|
||||
(function (slot) {
|
||||
var inp = el('mega-virus-balloon-url-' + slot);
|
||||
var btn = el('btn-mega-virus-balloon-clear-' + slot);
|
||||
if (inp) {
|
||||
inp.addEventListener('change', function () { updateMegaVirusBalloonPreview(slot); });
|
||||
inp.addEventListener('blur', function () { updateMegaVirusBalloonPreview(slot); });
|
||||
}
|
||||
if (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var i = el('mega-virus-balloon-url-' + slot);
|
||||
if (i) i.value = '';
|
||||
updateMegaVirusBalloonPreview(slot);
|
||||
});
|
||||
}
|
||||
})(s);
|
||||
}
|
||||
var fbInp = el('mega-virus-balloon-fallback-url');
|
||||
if (fbInp) {
|
||||
fbInp.addEventListener('change', updateMegaVirusBalloonFallbackPreview);
|
||||
fbInp.addEventListener('blur', updateMegaVirusBalloonFallbackPreview);
|
||||
}
|
||||
var fbClr = el('btn-mega-virus-balloon-fallback-clear');
|
||||
if (fbClr) {
|
||||
fbClr.addEventListener('click', function () {
|
||||
var i = el('mega-virus-balloon-fallback-url');
|
||||
if (i) i.value = '';
|
||||
updateMegaVirusBalloonFallbackPreview();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function applyMegaVirusPanelFromTimingData(data) {
|
||||
if (!data || typeof data !== 'object') return;
|
||||
var inpT = el('mega-virus-mission-sec');
|
||||
if (inpT && Object.prototype.hasOwnProperty.call(data, 'balloonBossMissionTimeSec')) {
|
||||
var bb = Number(data.balloonBossMissionTimeSec);
|
||||
inpT.value = String(Number.isFinite(bb) && bb > 0 ? Math.floor(bb) : 0);
|
||||
}
|
||||
var bossInp = el('mega-virus-boss-url');
|
||||
if (bossInp && Object.prototype.hasOwnProperty.call(data, 'balloonBossBossImageUrl')) {
|
||||
bossInp.value = data.balloonBossBossImageUrl != null ? String(data.balloonBossBossImageUrl) : '';
|
||||
updateMegaVirusBossPreview();
|
||||
}
|
||||
if (Array.isArray(data.balloonBossPlayerBalloonImageUrls)) {
|
||||
var urls = data.balloonBossPlayerBalloonImageUrls;
|
||||
for (var k = 1; k <= 6; k++) {
|
||||
var inpU = el('mega-virus-balloon-url-' + k);
|
||||
if (inpU) inpU.value = urls[k - 1] != null ? String(urls[k - 1]) : '';
|
||||
updateMegaVirusBalloonPreview(k);
|
||||
}
|
||||
}
|
||||
var fb = el('mega-virus-balloon-fallback-url');
|
||||
if (fb && Object.prototype.hasOwnProperty.call(data, 'balloonBossPlayerBalloonFallbackUrl')) {
|
||||
fb.value = data.balloonBossPlayerBalloonFallbackUrl != null ? String(data.balloonBossPlayerBalloonFallbackUrl) : '';
|
||||
updateMegaVirusBalloonFallbackPreview();
|
||||
}
|
||||
}
|
||||
|
||||
function loadMegaVirusPanel() {
|
||||
gameTimingFetch('GET')
|
||||
.then(function (data) {
|
||||
applyMegaVirusPanelFromTimingData(data);
|
||||
setMsg('mega-virus-timing-msg', '', '');
|
||||
})
|
||||
.catch(function (e) {
|
||||
setMsg('mega-virus-timing-msg', e.message || 'โหลดไม่ได้', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function saveMegaVirusTimingPanel() {
|
||||
if (megaVirusSaveInFlight) return;
|
||||
megaVirusSaveInFlight = true;
|
||||
var btn = el('btn-mega-virus-save');
|
||||
if (btn) btn.disabled = true;
|
||||
var limSs = el('mega-virus-mission-sec') ? parseInt(String(el('mega-virus-mission-sec').value), 10) : 0;
|
||||
if (Number.isNaN(limSs) || limSs < 0) limSs = 0;
|
||||
limSs = limSs <= 0 ? 0 : Math.max(10, Math.min(7200, limSs));
|
||||
var bossUrl = normalizeMegaVirusAssetUrl(el('mega-virus-boss-url') && el('mega-virus-boss-url').value ? String(el('mega-virus-boss-url').value).trim() : '');
|
||||
var balloonUrls = readMegaVirusBalloonUrlsFromForm();
|
||||
var fbUrl = normalizeMegaVirusAssetUrl(el('mega-virus-balloon-fallback-url') && el('mega-virus-balloon-fallback-url').value
|
||||
? String(el('mega-virus-balloon-fallback-url').value).trim()
|
||||
: '');
|
||||
gameTimingFetch('GET')
|
||||
.then(function (data) {
|
||||
data.balloonBossMissionTimeSec = limSs;
|
||||
data.balloonBossBossImageUrl = bossUrl;
|
||||
data.balloonBossPlayerBalloonImageUrls = balloonUrls;
|
||||
data.balloonBossPlayerBalloonFallbackUrl = fbUrl;
|
||||
return gameTimingFetch('PUT', data);
|
||||
})
|
||||
.then(function (res) {
|
||||
if (res && typeof res === 'object') applyMegaVirusPanelFromTimingData(res);
|
||||
setMsg('mega-virus-timing-msg', 'บันทึกแล้ว — รีเฟรชหน้าเล่น (หรือเปิดใหม่) เพื่อโหลดรูป · Saved; hard-refresh play.', 'ok');
|
||||
})
|
||||
.catch(function (e) {
|
||||
setMsg('mega-virus-timing-msg', e.message || 'บันทึกไม่ได้', 'error');
|
||||
})
|
||||
.then(function () {
|
||||
megaVirusSaveInFlight = false;
|
||||
if (btn) btn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function saveJumpSurviveTimingPanel() {
|
||||
var inp = el('jump-survive-height-mult');
|
||||
var jumpSurvMult = inp ? parseFloat(String(inp.value)) : 1.5;
|
||||
@@ -3444,6 +3641,7 @@
|
||||
if (name === 'quiz-battle') loadQbBattlePanel();
|
||||
if (name === 'jump-survive') loadJumpSurviveTimingPanel();
|
||||
if (name === 'space-shooter') loadSpaceShooterTimingPanel();
|
||||
if (name === 'mega-virus') loadMegaVirusPanel();
|
||||
if (name === 'game-timing') loadGameTimingPanel();
|
||||
if (name === 'stack-game') loadStackGamePanel();
|
||||
}
|
||||
@@ -3577,6 +3775,7 @@
|
||||
bindStackBlockVisualInputs();
|
||||
bindSpaceShooterDamageOverlayPanel();
|
||||
bindSpaceShooterAsteroidSpritePanel();
|
||||
bindMegaVirusPanel();
|
||||
var btnStackGameSave = el('btn-stack-game-save');
|
||||
if (btnStackGameSave) {
|
||||
btnStackGameSave.addEventListener('click', saveStackGamePanel);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<meta name="theme-color" content="#0b0d14">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" sizes="any">
|
||||
<title>Admin — JD JUSTICE DIVERS</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
@@ -597,8 +598,40 @@
|
||||
</section>
|
||||
|
||||
<section id="tab-panel-mega-virus" class="tab-panel card" hidden role="tabpanel" aria-labelledby="tab-mega-virus">
|
||||
<h2>Minigame-7 — ยิง Mega Virus</h2>
|
||||
<p class="muted">จองเมนูไว้สำหรับตั้งค่ามินิเกมนี้ในอนาคต (ยังไม่มีฟอร์ม) · <em>English:</em> Placeholder tab — admin options will be added later.</p>
|
||||
<h2>Minigame-7 — ยิง Mega Virus (balloon_boss)</h2>
|
||||
<p class="muted">เก็บที่ <code>/Game/data/game-timing.json</code> · แมป <code>balloonBossTimeSec</code> ทับค่าด้านล่าง · แมป = 0 = ไม่จับเวลา · ถ้าแมปไม่ตั้งเวลา: ใช้ช่องล่าง (หรือ 120 วิ ถ้าใส่ 0) · <em>English:</em> Map time overrides; map 0 = unlimited; else global seconds below (or 120s if 0).</p>
|
||||
<form id="form-mega-virus-timing" class="mega-virus-timing-form" action="#" method="post">
|
||||
<fieldset class="quiz-timing-fieldset">
|
||||
<legend>เวลา & รูป</legend>
|
||||
<div class="form-grid form-inline quiz-timing-grid">
|
||||
<label title="0 = ใช้ 120 วิเมื่อแมปไม่กำหนด · ตั้งอย่างน้อย 10 ถ้าต้องการกำหนดเอง">เวลารอบ (วินาที) <input type="number" id="mega-virus-mission-sec" min="0" max="7200" step="5" value="0"></label>
|
||||
</div>
|
||||
<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">รูปบอส (URL) <input type="text" id="mega-virus-boss-url" maxlength="500" spellcheck="false" placeholder="/Game/img/MegaVirus/boss.png" autocomplete="off"></label>
|
||||
<img class="space-shooter-ship-prev" id="mega-virus-boss-prev" alt="" width="64" height="64" decoding="async">
|
||||
<button type="button" class="btn btn-ghost" id="btn-mega-virus-boss-clear">ล้าง</button>
|
||||
</div>
|
||||
<p class="muted" style="margin-top:0.35rem">ลูกโป่งต่อที่นั่ง P1–P6 · ช่องว่างใช้ fallback · <em>English:</em> Per-seat balloon URLs; empty uses fallback.</p>
|
||||
<div class="space-shooter-ship-grid" role="group" aria-label="Mega Virus player balloon images">
|
||||
<div class="space-shooter-ship-row"><span class="space-shooter-ship-slot">P1</span><label class="space-shooter-ship-url-label">URL <input type="text" id="mega-virus-balloon-url-1" maxlength="500" spellcheck="false" placeholder="/Game/img/MegaVirus/balloon-1.png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="mega-virus-balloon-prev-1" alt="" width="40" height="48" decoding="async"><button type="button" class="btn btn-ghost" id="btn-mega-virus-balloon-clear-1">ล้าง</button></div>
|
||||
<div class="space-shooter-ship-row"><span class="space-shooter-ship-slot">P2</span><label class="space-shooter-ship-url-label">URL <input type="text" id="mega-virus-balloon-url-2" maxlength="500" spellcheck="false" placeholder="/Game/img/MegaVirus/balloon-2.png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="mega-virus-balloon-prev-2" alt="" width="40" height="48" decoding="async"><button type="button" class="btn btn-ghost" id="btn-mega-virus-balloon-clear-2">ล้าง</button></div>
|
||||
<div class="space-shooter-ship-row"><span class="space-shooter-ship-slot">P3</span><label class="space-shooter-ship-url-label">URL <input type="text" id="mega-virus-balloon-url-3" maxlength="500" spellcheck="false" placeholder="/Game/img/MegaVirus/balloon-3.png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="mega-virus-balloon-prev-3" alt="" width="40" height="48" decoding="async"><button type="button" class="btn btn-ghost" id="btn-mega-virus-balloon-clear-3">ล้าง</button></div>
|
||||
<div class="space-shooter-ship-row"><span class="space-shooter-ship-slot">P4</span><label class="space-shooter-ship-url-label">URL <input type="text" id="mega-virus-balloon-url-4" maxlength="500" spellcheck="false" placeholder="/Game/img/MegaVirus/balloon-4.png" autocomplete="off"></label><img class="space-shooter-ship-prev" id="mega-virus-balloon-prev-4" alt="" width="40" height="48" decoding="async"><button type="button" class="btn btn-ghost" id="btn-mega-virus-balloon-clear-4">ล้าง</button></div>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</fieldset>
|
||||
<div class="quiz-admin-actions" style="margin-top:0.75rem">
|
||||
<button type="submit" class="btn btn-primary" id="btn-mega-virus-save">บันทึก</button>
|
||||
</div>
|
||||
</form>
|
||||
<p id="mega-virus-timing-msg" class="msg" role="status"></p>
|
||||
</section>
|
||||
|
||||
<section id="tab-panel-game-timing" class="tab-panel card" hidden role="tabpanel" aria-labelledby="tab-game-timing">
|
||||
@@ -862,6 +895,6 @@
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<script src="admin.js?v=61"></script>
|
||||
<script src="admin.js?v=66"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -19,8 +19,22 @@
|
||||
"stackTowerMissionTimeSec": 90,
|
||||
"stackTeamMissesMax": 3,
|
||||
"stackTowerProgressBlocks": 50,
|
||||
"stackBlockNormalImageUrls": ["", "", "", "", "", ""],
|
||||
"stackBlockHeavyImageUrls": ["", "", "", "", "", ""],
|
||||
"stackBlockNormalImageUrls": [
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"stackBlockHeavyImageUrls": [
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"stackHeavyBlockPercent": 35,
|
||||
"jumpSurviveJumpHeightMult": 1.5,
|
||||
"jumpSurviveMissionTimeSec": 0,
|
||||
@@ -44,5 +58,16 @@
|
||||
"/Game/img/ViolentCrime/Rocket-broke-1.png",
|
||||
"/Game/img/ViolentCrime/Rocket-broke-2.png",
|
||||
"/Game/img/ViolentCrime/Rocket-broke-3.png"
|
||||
]
|
||||
],
|
||||
"balloonBossMissionTimeSec": 0,
|
||||
"balloonBossBossImageUrl": "/Game/img/MegaVirus/boss.png",
|
||||
"balloonBossPlayerBalloonImageUrls": [
|
||||
"/Game/img/MegaVirus/balloon-1.png",
|
||||
"/Game/img/MegaVirus/balloon-2.png",
|
||||
"/Game/img/MegaVirus/balloon-3.png",
|
||||
"/Game/img/MegaVirus/balloon-4.png",
|
||||
"/Game/img/MegaVirus/balloon-5.png",
|
||||
"/Game/img/MegaVirus/balloon-6.png"
|
||||
],
|
||||
"balloonBossPlayerBalloonFallbackUrl": "/Game/img/MegaVirus/Artboard%209.png"
|
||||
}
|
||||
@@ -12,3 +12,11 @@ English: Upload balloon-boss / Mega Virus assets under **`MegaVirus`** (no space
|
||||
โฟลเดอร์นี้ถูกสร้างใน repo + บนเซิร์ฟจาก `scripts/deploy-www.sh` (mkdir + www-data + setgid 2775)
|
||||
|
||||
ถ้า FTP ยัง 550: ตรวจว่าเชื่อมที่โฟลเดอร์ **public/img** ใต้ document root (เช่น `/var/www/html/Game/public/img/MegaVirus`) และสิทธิ์กลุ่ม `www-data`
|
||||
|
||||
ใน Admin ใส่ URL เป็น **ไฟล์** เช่น `/Game/img/MegaVirus/balloon-1.png` — **อย่าลงท้ายด้วย /** (`/Game/img/MegaVirus/` จะได้ 403/404 เพราะเป็นโฟลเดอร์ ไม่ใช่รูป)
|
||||
|
||||
English: Use a **file** URL in Admin (e.g. `.../balloon-1.png`). A **trailing slash** points at a **folder**, not an image (403/404).
|
||||
|
||||
ชื่อไฟล์มี **ช่องว่าง** (เช่น `Artboard 9.png`) ใช้ได้ — ระบบจะเก็บ/โหลดเป็น `%20` อัตโนมัติ
|
||||
|
||||
English: Filenames with **spaces** are OK; URLs are normalized to `%20` for the browser.
|
||||
|
||||
+170
-48
@@ -1237,6 +1237,11 @@
|
||||
let playSpaceShooterAsteroidIntervalMs = 1040;
|
||||
/** ทับยานเมื่อชนอุกาบาต ครั้งที่ 1–3 (PNG โปร่ง) — ภารกิจ Violent Crime */
|
||||
let playSpaceShooterShipDamageOverlayUrls = ['', '', ''];
|
||||
/** balloon_boss / Mega Virus — จาก game-timing */
|
||||
let playBalloonBossMissionTimeSec = 0;
|
||||
let playBalloonBossBossImageUrl = '';
|
||||
let playBalloonBossPlayerBalloonImageUrls = ['', '', '', '', '', ''];
|
||||
let playBalloonBossPlayerBalloonFallbackUrl = '';
|
||||
/** 0 = ไม่จำกัด — จาก game-timing / gauntlet-sync (พรมแดง Gauntlet เท่านั้น) */
|
||||
let gauntletRuntimeTimeLimitSec = 0;
|
||||
/** เวลาสิ้นสุดรอบ (epoch ms) จากเซิร์ฟเวอร์ — null = ไม่จับเวลา */
|
||||
@@ -1258,19 +1263,31 @@
|
||||
let gauntletLaserLineWidthPx = 2;
|
||||
const gauntletAssetImageCache = new Map();
|
||||
|
||||
function encodeSpacesInUrlPath(t) {
|
||||
const s = String(t || '');
|
||||
const q = s.indexOf('?');
|
||||
const pathPart = q === -1 ? s : s.slice(0, q);
|
||||
const query = q === -1 ? '' : s.slice(q);
|
||||
return pathPart.replace(/ /g, '%20') + query;
|
||||
}
|
||||
|
||||
/** แปลง URL จาก Admin/game-timing ให้โหลดได้ (nginx เสิร์ฟจาก /Game/...) */
|
||||
function normalizeGauntletAssetUrlForPlay(u) {
|
||||
if (typeof u !== 'string') return '';
|
||||
const t = u.trim();
|
||||
if (!t) return '';
|
||||
if (/^https?:\/\//i.test(t)) return t;
|
||||
const qIdx = t.indexOf('?');
|
||||
const base = (qIdx >= 0 ? t.slice(0, qIdx) : t).trim();
|
||||
const qs = qIdx >= 0 ? t.slice(qIdx) : '';
|
||||
const raw = u.trim();
|
||||
if (!raw) return '';
|
||||
if (/^https?:\/\//i.test(raw)) {
|
||||
const z = raw.replace(/\/+$/, '');
|
||||
return z ? encodeSpacesInUrlPath(z) : '';
|
||||
}
|
||||
const qIdx = raw.indexOf('?');
|
||||
const base = (qIdx >= 0 ? raw.slice(0, qIdx) : raw).trim().replace(/\/+$/, '');
|
||||
const qs = qIdx >= 0 ? raw.slice(qIdx) : '';
|
||||
if (!base) return '';
|
||||
if (base.startsWith('/')) return base + qs;
|
||||
if (/^Game\//i.test(base)) return '/' + base.replace(/^\/+/, '') + qs;
|
||||
return base + qs;
|
||||
if (/\.{4,}/.test(base)) return '';
|
||||
if (base.startsWith('/')) return encodeSpacesInUrlPath(base + qs);
|
||||
if (/^Game\//i.test(base)) return encodeSpacesInUrlPath('/' + base.replace(/^\/+/, '') + qs);
|
||||
return encodeSpacesInUrlPath(base + qs);
|
||||
}
|
||||
|
||||
function ensureGauntletAssetImage(url) {
|
||||
@@ -1289,6 +1306,21 @@
|
||||
return rec;
|
||||
}
|
||||
|
||||
/** ลูกโป่ง Mega Virus: ถ้ามี URL ต่อที่นั่งแต่โหลดไม่สำเร็จ (404) ให้ใช้ fallback แทน · Per-seat URL wins only when image loads */
|
||||
function resolveBalloonBossBalloonSpriteRec(perSeatRaw, fallbackRaw) {
|
||||
const per = normalizeGauntletAssetUrlForPlay(String(perSeatRaw || '').trim());
|
||||
const fb = normalizeGauntletAssetUrlForPlay(String(fallbackRaw || '').trim());
|
||||
if (per) {
|
||||
const rPer = ensureGauntletAssetImage(per);
|
||||
const img = rPer && rPer.img;
|
||||
if (img && img.complete && img.naturalWidth > 0) return rPer;
|
||||
if (img && img.complete && img.naturalWidth <= 0 && fb) return ensureGauntletAssetImage(fb);
|
||||
return rPer;
|
||||
}
|
||||
if (fb) return ensureGauntletAssetImage(fb);
|
||||
return null;
|
||||
}
|
||||
|
||||
function pickGauntletLaneImageRec(obsId) {
|
||||
if (!gauntletLaneImageUrls.length) return null;
|
||||
const n = gauntletLaneImageUrls.length;
|
||||
@@ -2932,6 +2964,10 @@
|
||||
}
|
||||
if (mapData.gameType === 'balloon_boss') {
|
||||
applyBalloonBossSpawnLayoutPlay();
|
||||
fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' })
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((t) => { if (t) applyGauntletTimingFromServer(t); })
|
||||
.catch(() => {});
|
||||
}
|
||||
if (quizCarryPregameActive && mapData && mapData.gameType === 'quiz_carry') updateQuizCarryPregameHud();
|
||||
}
|
||||
@@ -7532,6 +7568,38 @@
|
||||
}
|
||||
playSpaceShooterShipDamageOverlayUrls = nextD;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'balloonBossMissionTimeSec')) {
|
||||
const st = Number(payload.balloonBossMissionTimeSec);
|
||||
if (Number.isFinite(st) && st > 0) {
|
||||
playBalloonBossMissionTimeSec = Math.max(10, Math.min(7200, Math.floor(st)));
|
||||
} else {
|
||||
playBalloonBossMissionTimeSec = 0;
|
||||
}
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'balloonBossBossImageUrl')) {
|
||||
playBalloonBossBossImageUrl = normalizeGauntletAssetUrlForPlay(
|
||||
typeof payload.balloonBossBossImageUrl === 'string' ? payload.balloonBossBossImageUrl : ''
|
||||
);
|
||||
if (playBalloonBossBossImageUrl) ensureGauntletAssetImage(playBalloonBossBossImageUrl);
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'balloonBossPlayerBalloonImageUrls')) {
|
||||
const a = payload.balloonBossPlayerBalloonImageUrls;
|
||||
const nextBb = ['', '', '', '', '', ''];
|
||||
if (Array.isArray(a)) {
|
||||
for (let si = 0; si < 6; si++) {
|
||||
const u = normalizeGauntletAssetUrlForPlay(typeof a[si] === 'string' ? a[si] : '');
|
||||
nextBb[si] = u;
|
||||
if (u) ensureGauntletAssetImage(u);
|
||||
}
|
||||
}
|
||||
playBalloonBossPlayerBalloonImageUrls = nextBb;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'balloonBossPlayerBalloonFallbackUrl')) {
|
||||
playBalloonBossPlayerBalloonFallbackUrl = normalizeGauntletAssetUrlForPlay(
|
||||
typeof payload.balloonBossPlayerBalloonFallbackUrl === 'string' ? payload.balloonBossPlayerBalloonFallbackUrl : ''
|
||||
);
|
||||
if (playBalloonBossPlayerBalloonFallbackUrl) ensureGauntletAssetImage(playBalloonBossPlayerBalloonFallbackUrl);
|
||||
}
|
||||
if (isStack() && stackMini) reapplyStackMiniSizingFromGlobals();
|
||||
}
|
||||
|
||||
@@ -9327,6 +9395,8 @@
|
||||
const t = Number(mapData && mapData.balloonBossTimeSec);
|
||||
if (Number.isFinite(t) && t === 0) return 0;
|
||||
if (Number.isFinite(t) && t > 0 && t < 7200) return Math.floor(t);
|
||||
const g = Number(playBalloonBossMissionTimeSec);
|
||||
if (Number.isFinite(g) && g > 0 && g < 7200) return Math.floor(g);
|
||||
return 120;
|
||||
}
|
||||
|
||||
@@ -9383,6 +9453,7 @@
|
||||
cy = Math.min(mh - ts * 1.2, (h - 1.2) * ts);
|
||||
}
|
||||
const cl = clampBalloonBossWorld(cx, cy, mw, mh);
|
||||
ent.balloonBossSkinSlot = slot;
|
||||
ent.balloonBossCx = cl.cx;
|
||||
ent.balloonBossCy = cl.cy;
|
||||
if (typeof ent.balloonBossScore !== 'number' || !Number.isFinite(ent.balloonBossScore)) ent.balloonBossScore = 0;
|
||||
@@ -9708,6 +9779,8 @@
|
||||
});
|
||||
return best;
|
||||
})();
|
||||
const bossImgUrl = normalizeGauntletAssetUrlForPlay(String(playBalloonBossBossImageUrl || ''));
|
||||
const bossImgRec = bossImgUrl ? ensureGauntletAssetImage(bossImgUrl) : null;
|
||||
|
||||
for (let hi = 0; hi < balloonBossHitFx.length; hi++) {
|
||||
const fx = balloonBossHitFx[hi];
|
||||
@@ -9726,37 +9799,58 @@
|
||||
const [bsx, bsy] = worldToScreen(bossC.cx, bossC.cy);
|
||||
ctx.save();
|
||||
ctx.translate(bsx, bsy);
|
||||
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;
|
||||
const px = Math.cos(ang) * bossR * zDraw * 1.05;
|
||||
const py = Math.sin(ang) * bossR * zDraw * 1.05;
|
||||
if (k === 0) ctx.moveTo(px, py);
|
||||
else ctx.lineTo(px, py);
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = 'rgba(0, 40, 60, 0.35)';
|
||||
ctx.fill();
|
||||
const limR = bossR * zDraw;
|
||||
if (bossImgRec && bossImgRec.ready && bossImgRec.img && bossImgRec.img.naturalWidth > 0) {
|
||||
const iw = bossImgRec.img.naturalWidth;
|
||||
const ih = bossImgRec.img.naturalHeight;
|
||||
const diam = limR * 2.1;
|
||||
const scale = Math.min(diam / iw, diam / ih);
|
||||
const dw = iw * scale;
|
||||
const dh = ih * scale;
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, limR * 1.05, 0, Math.PI * 2);
|
||||
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;
|
||||
const px = Math.cos(ang) * bossR * zDraw * 1.05;
|
||||
const py = Math.sin(ang) * bossR * zDraw * 1.05;
|
||||
if (k === 0) ctx.moveTo(px, py);
|
||||
else ctx.lineTo(px, py);
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = 'rgba(0, 40, 60, 0.35)';
|
||||
ctx.fill();
|
||||
|
||||
const skullR = bossR * zDraw * 0.62;
|
||||
const grd = ctx.createRadialGradient(-skullR * 0.2, -skullR * 0.25, skullR * 0.1, 0, 0, skullR);
|
||||
grd.addColorStop(0, 'rgba(55, 50, 62, 0.98)');
|
||||
grd.addColorStop(1, 'rgba(22, 18, 30, 1)');
|
||||
ctx.fillStyle = grd;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, skullR, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = 'rgba(255, 40, 60, 0.95)';
|
||||
ctx.shadowColor = 'rgba(255, 0, 0, 0.8)';
|
||||
ctx.shadowBlur = 12 * zDraw;
|
||||
ctx.beginPath();
|
||||
ctx.arc(-skullR * 0.32, -skullR * 0.12, skullR * 0.14, 0, Math.PI * 2);
|
||||
ctx.arc(skullR * 0.32, -skullR * 0.12, skullR * 0.14, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
const skullR = bossR * zDraw * 0.62;
|
||||
const grd = ctx.createRadialGradient(-skullR * 0.2, -skullR * 0.25, skullR * 0.1, 0, 0, skullR);
|
||||
grd.addColorStop(0, 'rgba(55, 50, 62, 0.98)');
|
||||
grd.addColorStop(1, 'rgba(22, 18, 30, 1)');
|
||||
ctx.fillStyle = grd;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, skullR, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = 'rgba(255, 40, 60, 0.95)';
|
||||
ctx.shadowColor = 'rgba(255, 0, 0, 0.8)';
|
||||
ctx.shadowBlur = 12 * zDraw;
|
||||
ctx.beginPath();
|
||||
ctx.arc(-skullR * 0.32, -skullR * 0.12, skullR * 0.14, 0, Math.PI * 2);
|
||||
ctx.arc(skullR * 0.32, -skullR * 0.12, skullR * 0.14, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
|
||||
ctx.fillStyle = 'rgba(255, 100, 200, 0.9)';
|
||||
ctx.font = `bold ${Math.max(10, 11 * zDraw)}px ui-monospace, monospace`;
|
||||
@@ -9772,18 +9866,35 @@
|
||||
ctx.strokeRect(-barW / 2, -bossR * zDraw - 8, barW, barH);
|
||||
ctx.restore();
|
||||
|
||||
function drawBalloonsCluster(sx, sy, count, col, z) {
|
||||
function drawBalloonsCluster(sx, sy, count, col, z, slot1to6) {
|
||||
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));
|
||||
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;
|
||||
ctx.fillStyle = col;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(sx + ox, sy + oy, 6.5 * z, 8 * z, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.35)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
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 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);
|
||||
} else {
|
||||
ctx.fillStyle = col;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(bx, by, 6.5 * z, 8 * z, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.35)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9800,7 +9911,10 @@
|
||||
ctx.beginPath();
|
||||
ctx.arc(sx, sy, 22 * z, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
drawBalloonsCluster(sx, sy, ent.balloonBossBalloons | 0, col, z);
|
||||
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 nm = String(ent.nickname || '').slice(0, 12);
|
||||
ctx.font = `${Math.max(9, 10 * z)}px system-ui, "Kanit", sans-serif`;
|
||||
ctx.textAlign = 'center';
|
||||
@@ -10894,6 +11008,10 @@
|
||||
balloonBossSessionStartMs = performance.now();
|
||||
balloonBossLastMoveEmit = 0;
|
||||
balloonBossBossFireAcc = 0;
|
||||
fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (t) { if (t) applyGauntletTimingFromServer(t); })
|
||||
.catch(function () {});
|
||||
}
|
||||
if (isQuiz()) {
|
||||
setupPlayQuizUi();
|
||||
@@ -11656,6 +11774,10 @@
|
||||
o.balloonBossBalloons = balloonBossBalloonsStartPlay();
|
||||
o.balloonBossEliminated = false;
|
||||
});
|
||||
fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' })
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((t) => { if (t) applyGauntletTimingFromServer(t); })
|
||||
.catch(() => {});
|
||||
}
|
||||
if (mapData.gameType === 'quiz_battle') {
|
||||
if (!mapData.quizBattleDomeArea) mapData.quizBattleDomeArea = [];
|
||||
|
||||
@@ -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.277"></script>
|
||||
<script src="js/play.js?v=0.283"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+61
-4
@@ -614,6 +614,11 @@ function defaultGameTiming() {
|
||||
/** ระยะห่างเกิดอุกาบาต (ms) — ค่าน้อย = ถี่ขึ้น; แมป spaceShooterAsteroidIntervalMs ≥200 ทับ */
|
||||
spaceShooterAsteroidIntervalMs: 1040,
|
||||
spaceShooterShipDamageOverlayUrls: ['', '', ''],
|
||||
/** balloon_boss / Mega Virus — 0 = ไคลเอนต์ใช้ 120 วิเมื่อแมปไม่ตั้ง balloonBossTimeSec */
|
||||
balloonBossMissionTimeSec: 0,
|
||||
balloonBossBossImageUrl: '',
|
||||
balloonBossPlayerBalloonImageUrls: ['', '', '', '', '', ''],
|
||||
balloonBossPlayerBalloonFallbackUrl: '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -795,12 +800,26 @@ function defaultGauntletVisuals() {
|
||||
};
|
||||
}
|
||||
|
||||
/** ช่องว่างใน path (เช่น Artboard 9.png) — encode เป็น %20 ให้ nginx/เบราว์เซอร์โหลดได้ */
|
||||
function encodeSpacesInAssetUrlPath(t) {
|
||||
const s = String(t || '');
|
||||
const q = s.indexOf('?');
|
||||
const pathPart = q === -1 ? s : s.slice(0, q);
|
||||
const query = q === -1 ? '' : s.slice(q);
|
||||
return pathPart.replace(/ /g, '%20') + query;
|
||||
}
|
||||
|
||||
function sanitizeGauntletAssetUrl(s) {
|
||||
const t = String(s || '').trim();
|
||||
const t = String(s || '').trim().replace(/\/+$/, '');
|
||||
if (!t || t.length > 500) return '';
|
||||
if (/[\s<>"'`]/.test(t)) return '';
|
||||
if (t.startsWith('/')) return t;
|
||||
if (/^https?:\/\//i.test(t)) return t;
|
||||
/** ห้ามแท็บ/บรรทัดใหม่/ตัวอักษรอันตราย — อนุญาตช่องว่าง (U+0020) ในชื่อไฟล์ */
|
||||
if (/[\t\n\r\x00-\x08\x0b\x0c\x0e-\x1f<>"'`]/.test(t)) return '';
|
||||
/** placeholder จาก Admin เช่น /Game/img/....png — ไม่ใช่ path จริง */
|
||||
if (/\.{4,}/.test(t)) return '';
|
||||
if (/^https?:\/\//i.test(t)) return encodeSpacesInAssetUrlPath(t);
|
||||
if (t.startsWith('/')) return encodeSpacesInAssetUrlPath(t);
|
||||
/** Admin มักใส่แบบไม่มี slash นำหน้า — ให้เทียบเท่า /Game/... บน nginx */
|
||||
if (/^Game\//i.test(t)) return encodeSpacesInAssetUrlPath(`/${t.replace(/^\/+/, '')}`);
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -901,6 +920,16 @@ function loadGameTiming() {
|
||||
? clampSpaceShooterAsteroidIntervalMs(j.spaceShooterAsteroidIntervalMs)
|
||||
: SPACE_SHOOTER_ASTEROID_INTERVAL_MS_DEFAULT,
|
||||
spaceShooterShipDamageOverlayUrls: normalizeSpaceShooterShipDamageOverlayUrls(j.spaceShooterShipDamageOverlayUrls),
|
||||
balloonBossMissionTimeSec: Object.prototype.hasOwnProperty.call(j, 'balloonBossMissionTimeSec')
|
||||
? clampSpaceShooterMissionTimeSec(j.balloonBossMissionTimeSec)
|
||||
: 0,
|
||||
balloonBossBossImageUrl: Object.prototype.hasOwnProperty.call(j, 'balloonBossBossImageUrl')
|
||||
? sanitizeGauntletAssetUrl(j.balloonBossBossImageUrl)
|
||||
: '',
|
||||
balloonBossPlayerBalloonImageUrls: normalizeStackBlockSixImageUrls(j.balloonBossPlayerBalloonImageUrls),
|
||||
balloonBossPlayerBalloonFallbackUrl: Object.prototype.hasOwnProperty.call(j, 'balloonBossPlayerBalloonFallbackUrl')
|
||||
? sanitizeGauntletAssetUrl(j.balloonBossPlayerBalloonFallbackUrl)
|
||||
: '',
|
||||
};
|
||||
}
|
||||
} catch (e) { console.error('loadGameTiming', e.message); }
|
||||
@@ -983,6 +1012,30 @@ function saveGameTimingToFile(d) {
|
||||
const spaceShooterShipDamageOverlayUrls = Object.prototype.hasOwnProperty.call(d, 'spaceShooterShipDamageOverlayUrls')
|
||||
? normalizeSpaceShooterShipDamageOverlayUrls(d.spaceShooterShipDamageOverlayUrls)
|
||||
: prevDmg;
|
||||
const prevBbMission = Object.prototype.hasOwnProperty.call(prev, 'balloonBossMissionTimeSec')
|
||||
? prev.balloonBossMissionTimeSec
|
||||
: 0;
|
||||
const balloonBossMissionTimeSec = Object.prototype.hasOwnProperty.call(d, 'balloonBossMissionTimeSec')
|
||||
? clampSpaceShooterMissionTimeSec(d.balloonBossMissionTimeSec)
|
||||
: clampSpaceShooterMissionTimeSec(prevBbMission);
|
||||
const prevBbBoss = Object.prototype.hasOwnProperty.call(prev, 'balloonBossBossImageUrl')
|
||||
? sanitizeGauntletAssetUrl(prev.balloonBossBossImageUrl)
|
||||
: '';
|
||||
const balloonBossBossImageUrl = Object.prototype.hasOwnProperty.call(d, 'balloonBossBossImageUrl')
|
||||
? sanitizeGauntletAssetUrl(d.balloonBossBossImageUrl)
|
||||
: prevBbBoss;
|
||||
const prevBbBalloons = Array.isArray(prev.balloonBossPlayerBalloonImageUrls)
|
||||
? normalizeStackBlockSixImageUrls(prev.balloonBossPlayerBalloonImageUrls)
|
||||
: normalizeStackBlockSixImageUrls([]);
|
||||
const balloonBossPlayerBalloonImageUrls = Object.prototype.hasOwnProperty.call(d, 'balloonBossPlayerBalloonImageUrls')
|
||||
? normalizeStackBlockSixImageUrls(d.balloonBossPlayerBalloonImageUrls)
|
||||
: prevBbBalloons;
|
||||
const prevBbFb = Object.prototype.hasOwnProperty.call(prev, 'balloonBossPlayerBalloonFallbackUrl')
|
||||
? sanitizeGauntletAssetUrl(prev.balloonBossPlayerBalloonFallbackUrl)
|
||||
: '';
|
||||
const balloonBossPlayerBalloonFallbackUrl = Object.prototype.hasOwnProperty.call(d, 'balloonBossPlayerBalloonFallbackUrl')
|
||||
? sanitizeGauntletAssetUrl(d.balloonBossPlayerBalloonFallbackUrl)
|
||||
: prevBbFb;
|
||||
const prevStackTowerSec = Object.prototype.hasOwnProperty.call(prev, 'stackTowerMissionTimeSec')
|
||||
? prev.stackTowerMissionTimeSec
|
||||
: 90;
|
||||
@@ -1040,6 +1093,10 @@ function saveGameTimingToFile(d) {
|
||||
spaceShooterAsteroidExplodeFrameMs,
|
||||
spaceShooterAsteroidIntervalMs,
|
||||
spaceShooterShipDamageOverlayUrls,
|
||||
balloonBossMissionTimeSec,
|
||||
balloonBossBossImageUrl,
|
||||
balloonBossPlayerBalloonImageUrls,
|
||||
balloonBossPlayerBalloonFallbackUrl,
|
||||
};
|
||||
fs.writeFileSync(GAME_TIMING_PATH, JSON.stringify(out, null, 2), 'utf8');
|
||||
runtimeGameTiming = out;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="JD">
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#3d59a1"/>
|
||||
<stop offset="55%" stop-color="#7aa2f7"/>
|
||||
<stop offset="100%" stop-color="#9ece6a"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="32" height="32" rx="8" fill="url(#g)"/>
|
||||
<path fill="rgba(11,13,20,0.35)" d="M8 22h16v2H8z"/>
|
||||
<path fill="#0b0d14" d="M10 11c1.2-1.5 3-2.4 6-2.4s4.8.9 6 2.4c.6.8.9 1.7.9 2.7 0 2.1-1.4 3.8-3.2 4.3-.3 1.1-1.1 2-2.2 2.4-.4.1-.9.2-1.5.2s-1.1-.1-1.5-.2c-1.1-.4-1.9-1.3-2.2-2.4-1.8-.5-3.2-2.2-3.2-4.3 0-1 .3-1.9.9-2.7z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 678 B |
Reference in New Issue
Block a user