diff --git a/nginx/srv1361159.hstgr.cloud b/nginx/srv1361159.hstgr.cloud index e7a76b2..1ea5f43 100644 --- a/nginx/srv1361159.hstgr.cloud +++ b/nginx/srv1361159.hstgr.cloud @@ -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; diff --git a/www/html/Admin/admin.js b/www/html/Admin/admin.js index 43dc87b..6d1b43b 100644 --- a/www/html/Admin/admin.js +++ b/www/html/Admin/admin.js @@ -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); diff --git a/www/html/Admin/index.html b/www/html/Admin/index.html index 7554a90..304806e 100644 --- a/www/html/Admin/index.html +++ b/www/html/Admin/index.html @@ -5,6 +5,7 @@ +
จองเมนูไว้สำหรับตั้งค่ามินิเกมนี้ในอนาคต (ยังไม่มีฟอร์ม) · English: Placeholder tab — admin options will be added later.
+เก็บที่ /Game/data/game-timing.json · แมป balloonBossTimeSec ทับค่าด้านล่าง · แมป = 0 = ไม่จับเวลา · ถ้าแมปไม่ตั้งเวลา: ใช้ช่องล่าง (หรือ 120 วิ ถ้าใส่ 0) · English: Map time overrides; map 0 = unlimited; else global seconds below (or 120s if 0).