diff --git a/www/html/Admin/admin.css b/www/html/Admin/admin.css
index a992581..d12aa6f 100644
--- a/www/html/Admin/admin.css
+++ b/www/html/Admin/admin.css
@@ -2426,20 +2426,7 @@ code {
max-width: 200px;
font-size: 0.78rem;
}
-
-.space-shooter-asteroid-urls-ta {
- width: 100%;
- max-width: 720px;
- min-height: 9rem;
- margin-top: 0.35rem;
- padding: 0.55rem 0.65rem;
- font-family: ui-monospace, monospace;
+.space-shooter-ship-slot--ast {
+ flex: 0 0 2.25rem;
font-size: 0.82rem;
- line-height: 1.45;
- border-radius: 8px;
- border: 1px solid var(--border);
- background: rgba(0, 0, 0, 0.25);
- color: var(--text);
- resize: vertical;
- box-sizing: border-box;
}
diff --git a/www/html/Admin/admin.js b/www/html/Admin/admin.js
index 5612a36..234bddd 100644
--- a/www/html/Admin/admin.js
+++ b/www/html/Admin/admin.js
@@ -1273,6 +1273,8 @@
if (t.carryReadMs != null) data.carryReadMs = t.carryReadMs;
if (t.carryAnswerMs != null) data.carryAnswerMs = t.carryAnswerMs;
if (t.carrySessionLength != null) data.carrySessionLength = t.carrySessionLength;
+ if (t.carryWalkSpeedMultForMapId !== undefined) data.carryWalkSpeedMultForMapId = t.carryWalkSpeedMultForMapId;
+ if (t.carryWalkSpeedMult !== undefined) data.carryWalkSpeedMult = t.carryWalkSpeedMult;
}
renderQuizCarryAdminList(data.carryQuestions || []);
var readSec = el('quiz-carry-read-sec');
@@ -1290,6 +1292,15 @@
var sl = parseInt(String(data.carrySessionLength), 10);
sessLen.value = String(Number.isFinite(sl) && sl >= 0 ? sl : 0);
}
+ var walkMapIdInp = el('quiz-carry-walk-map-id');
+ if (walkMapIdInp) {
+ walkMapIdInp.value = data.carryWalkSpeedMultForMapId ? String(data.carryWalkSpeedMultForMapId).trim() : '';
+ }
+ var walkMultInp = el('quiz-carry-walk-mult');
+ if (walkMultInp) {
+ var wmv = Number(data.carryWalkSpeedMult);
+ walkMultInp.value = String(Number.isFinite(wmv) ? wmv : 1.42);
+ }
quizCarrySetPlaqueMapScaleInputs(data.carryChoicePlaqueMapScale);
quizCarryBindThemePickersOnce();
var th = data.carryMapPanelTheme && typeof data.carryMapPanelTheme === 'object' ? data.carryMapPanelTheme : {};
@@ -1472,6 +1483,9 @@
var psn = parseFloat(String(plaqueScaleEl.value || '').replace(',', '.'));
if (Number.isFinite(psn)) plaqueMapScale = Math.max(0.85, Math.min(2.5, psn));
}
+ var walkMapIdRaw = el('quiz-carry-walk-map-id') && el('quiz-carry-walk-map-id').value ? String(el('quiz-carry-walk-map-id').value).trim() : '';
+ var walkMapId = walkMapIdRaw.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 64);
+ var walkMultRaw = el('quiz-carry-walk-mult') ? parseFloat(String(el('quiz-carry-walk-mult').value || '').replace(',', '.')) : NaN;
var putBody = {
carryReadMs: carryReadMs,
carryAnswerMs: carryAnswerMs,
@@ -1481,6 +1495,13 @@
carryChoicePlaqueThemes: quizCarryPlaqueBuildForSave(),
carryChoicePlaqueMapScale: plaqueMapScale,
};
+ if (walkMapId) {
+ putBody.carryWalkSpeedMultForMapId = walkMapId;
+ putBody.carryWalkSpeedMult = Number.isFinite(walkMultRaw) ? Math.max(0.5, Math.min(3, walkMultRaw)) : 1.42;
+ } else {
+ putBody.carryWalkSpeedMultForMapId = '';
+ putBody.carryWalkSpeedMult = null;
+ }
if (carryQuestions.length > 0) {
putBody.carryQuestions = carryQuestions;
}
@@ -1520,6 +1541,8 @@
carryReadMs: carryReadMs,
carryAnswerMs: carryAnswerMs,
carrySessionLength: carrySessionLength,
+ carryWalkSpeedMultForMapId: walkMapId,
+ carryWalkSpeedMult: walkMapId ? (Number.isFinite(walkMultRaw) ? Math.max(0.5, Math.min(3, walkMultRaw)) : 1.42) : null,
},
fetchDelayMs: 120,
});
@@ -2756,18 +2779,223 @@
}
}
- function parseSpaceShooterAsteroidSpriteUrlsTextarea() {
- var ta = el('space-shooter-asteroid-sprite-urls');
- var raw = ta && ta.value ? String(ta.value) : '';
- var lines = raw.split(/[\n\r]+/);
+ function updateSpaceShooterAstFallPreview() {
+ var img = el('space-shooter-ast-fall-prev');
+ var inp = el('space-shooter-ast-fall-url');
+ if (!img) return;
+ var v = inp && inp.value ? String(inp.value).trim() : '';
+ if (!v) {
+ img.removeAttribute('src');
+ img.alt = '';
+ return;
+ }
+ if (!/^https?:\/\//i.test(v) && v.charAt(0) !== '/') v = '/' + v.replace(/^\/+/, '');
+ img.alt = 'Asteroid falling';
+ img.src = v;
+ }
+
+ function updateSpaceShooterAstExplodeRowPreview(row) {
+ if (!row) return;
+ var img = row.querySelector('.space-shooter-ast-explode-prev');
+ var inp = row.querySelector('.space-shooter-ast-explode-url');
+ if (!img) return;
+ var v = inp && inp.value ? String(inp.value).trim() : '';
+ if (!v) {
+ img.removeAttribute('src');
+ img.alt = '';
+ return;
+ }
+ if (!/^https?:\/\//i.test(v) && v.charAt(0) !== '/') v = '/' + v.replace(/^\/+/, '');
+ img.alt = 'Asteroid explode frame';
+ img.src = v;
+ }
+
+ function renumberSpaceShooterAstExplodeRows() {
+ var wrap = el('space-shooter-ast-explode-rows');
+ if (!wrap) return;
+ var rows = wrap.querySelectorAll('.space-shooter-ast-explode-row');
+ rows.forEach(function (row, i) {
+ var slot = row.querySelector('.space-shooter-ship-slot');
+ if (slot) slot.textContent = String(i + 1);
+ });
+ }
+
+ function addSpaceShooterAstExplodeRow(initialUrl) {
+ var wrap = el('space-shooter-ast-explode-rows');
+ if (!wrap) return;
+ var rows = wrap.querySelectorAll('.space-shooter-ast-explode-row');
+ if (rows.length >= 31) {
+ setMsg('space-shooter-timing-msg', 'เฟรมแตกสูงสุด 31 แถว (รวมรูปตกแล้วไม่เกิน 32 URL) · Max 31 explosion rows', 'error');
+ return;
+ }
+ var ix = rows.length + 1;
+ var row = document.createElement('div');
+ row.className = 'space-shooter-ship-row space-shooter-ast-explode-row';
+ row.innerHTML =
+ '' +
+ ix +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '';
+ var inp = row.querySelector('.space-shooter-ast-explode-url');
+ if (inp && initialUrl != null && String(initialUrl).trim()) inp.value = String(initialUrl).trim();
+ if (inp) {
+ inp.addEventListener('change', function () {
+ updateSpaceShooterAstExplodeRowPreview(row);
+ });
+ inp.addEventListener('blur', function () {
+ updateSpaceShooterAstExplodeRowPreview(row);
+ });
+ }
+ wrap.appendChild(row);
+ updateSpaceShooterAstExplodeRowPreview(row);
+ }
+
+ function clearSpaceShooterAstExplodeRows() {
+ var wrap = el('space-shooter-ast-explode-rows');
+ if (wrap) wrap.innerHTML = '';
+ }
+
+ function readSpaceShooterAsteroidSpriteUrlsFromForm() {
var out = [];
- for (var i = 0; i < lines.length && out.length < 32; i++) {
- var t = lines[i].trim();
- if (t) out.push(t);
+ var fallInp = el('space-shooter-ast-fall-url');
+ var fall = fallInp && fallInp.value ? String(fallInp.value).trim() : '';
+ if (fall) out.push(fall);
+ var wrap = el('space-shooter-ast-explode-rows');
+ if (wrap) {
+ var rows = wrap.querySelectorAll('.space-shooter-ast-explode-row');
+ for (var ri = 0; ri < rows.length && out.length < 32; ri++) {
+ var inp = rows[ri].querySelector('.space-shooter-ast-explode-url');
+ var u = inp && inp.value ? String(inp.value).trim() : '';
+ if (u) out.push(u);
+ }
}
return out;
}
+ function bindSpaceShooterAsteroidSpritePanel() {
+ var wrap = el('space-shooter-ast-explode-rows');
+ if (!wrap || wrap.getAttribute('data-bound') === '1') return;
+ wrap.setAttribute('data-bound', '1');
+ wrap.addEventListener('click', function (ev) {
+ var t = ev.target;
+ if (!t || !t.closest) return;
+ var rm = t.closest('.btn-space-shooter-ast-explode-remove');
+ if (rm) {
+ var row = rm.closest('.space-shooter-ast-explode-row');
+ if (row && row.parentNode) row.parentNode.removeChild(row);
+ renumberSpaceShooterAstExplodeRows();
+ return;
+ }
+ var clr = t.closest('.btn-space-shooter-ast-explode-clear');
+ if (clr) {
+ var rowC = clr.closest('.space-shooter-ast-explode-row');
+ if (!rowC) return;
+ var iu = rowC.querySelector('.space-shooter-ast-explode-url');
+ var fi = rowC.querySelector('.space-shooter-ast-explode-file');
+ if (iu) iu.value = '';
+ if (fi) fi.value = '';
+ updateSpaceShooterAstExplodeRowPreview(rowC);
+ return;
+ }
+ var up = t.closest('.btn-space-shooter-ast-explode-upload');
+ if (!up) return;
+ var rowU = up.closest('.space-shooter-ast-explode-row');
+ if (!rowU) return;
+ var fileInp = rowU.querySelector('.space-shooter-ast-explode-file');
+ if (!fileInp || !fileInp.files || !fileInp.files[0]) {
+ setMsg('space-shooter-timing-msg', 'เลือกไฟล์รูปก่อน · Pick an image file first', 'error');
+ return;
+ }
+ readFileAsDataURL(fileInp.files[0])
+ .then(function (dataUrl) {
+ return gauntletAssetsJsonFetch(GAME_GAUNTLET_ASSETS_API + '/upload', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ imageDataUrl: dataUrl,
+ label: 'space-shooter-ast-explode',
+ }),
+ });
+ })
+ .then(function (j) {
+ if (!j || !j.ok || !j.url) {
+ throw new Error((j && j.error) || 'อัปโหลดไม่สำเร็จ · Upload failed');
+ }
+ var inpU = rowU.querySelector('.space-shooter-ast-explode-url');
+ if (inpU) inpU.value = String(j.url).trim();
+ updateSpaceShooterAstExplodeRowPreview(rowU);
+ setMsg(
+ 'space-shooter-timing-msg',
+ 'อัปโหลดเฟรมแตกแล้ว — กดบันทึกด้านล่างเพื่อเก็บ · Uploaded; click Save',
+ 'ok'
+ );
+ })
+ .catch(function (e) {
+ setMsg('space-shooter-timing-msg', e.message || 'อัปโหลดไม่สำเร็จ', 'error');
+ });
+ });
+ var btnAdd = el('btn-space-shooter-ast-add-explode');
+ if (btnAdd) {
+ btnAdd.addEventListener('click', function () {
+ setMsg('space-shooter-timing-msg', '', '');
+ addSpaceShooterAstExplodeRow('');
+ });
+ }
+ var fallInp = el('space-shooter-ast-fall-url');
+ var fallFile = el('space-shooter-ast-fall-file');
+ var fallUp = el('btn-space-shooter-ast-fall-upload');
+ var fallClr = el('btn-space-shooter-ast-fall-clear');
+ if (fallInp) {
+ fallInp.addEventListener('change', updateSpaceShooterAstFallPreview);
+ fallInp.addEventListener('blur', updateSpaceShooterAstFallPreview);
+ }
+ if (fallClr) {
+ fallClr.addEventListener('click', function () {
+ if (fallInp) fallInp.value = '';
+ if (fallFile) fallFile.value = '';
+ updateSpaceShooterAstFallPreview();
+ });
+ }
+ if (fallUp && fallFile) {
+ fallUp.addEventListener('click', function () {
+ if (!fallFile.files || !fallFile.files[0]) {
+ setMsg('space-shooter-timing-msg', 'เลือกไฟล์รูปก่อน · Pick an image file first', 'error');
+ return;
+ }
+ readFileAsDataURL(fallFile.files[0])
+ .then(function (dataUrl) {
+ return gauntletAssetsJsonFetch(GAME_GAUNTLET_ASSETS_API + '/upload', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ imageDataUrl: dataUrl,
+ label: 'space-shooter-ast-fall',
+ }),
+ });
+ })
+ .then(function (j) {
+ if (!j || !j.ok || !j.url) {
+ throw new Error((j && j.error) || 'อัปโหลดไม่สำเร็จ · Upload failed');
+ }
+ if (fallInp) fallInp.value = String(j.url).trim();
+ updateSpaceShooterAstFallPreview();
+ setMsg(
+ 'space-shooter-timing-msg',
+ 'อัปโหลดรูปตกแล้ว — กดบันทึกด้านล่างเพื่อเก็บ · Uploaded; click Save',
+ 'ok'
+ );
+ })
+ .catch(function (e) {
+ setMsg('space-shooter-timing-msg', e.message || 'อัปโหลดไม่สำเร็จ', 'error');
+ });
+ });
+ }
+ }
+
function loadSpaceShooterTimingPanel() {
gameTimingFetch('GET')
.then(function (data) {
@@ -2788,11 +3016,16 @@
if (inpU) inpU.value = urls[k - 1] != null ? String(urls[k - 1]) : '';
updateSpaceShooterShipPreview(k);
}
- var taAst = el('space-shooter-asteroid-sprite-urls');
- if (taAst) {
+ var fallInpL = el('space-shooter-ast-fall-url');
+ if (fallInpL) {
var astArr = data.spaceShooterAsteroidSpriteUrls;
if (!Array.isArray(astArr)) astArr = [];
- taAst.value = astArr.map(function (u) { return String(u || '').trim(); }).filter(Boolean).join('\n');
+ fallInpL.value = astArr[0] != null ? String(astArr[0]) : '';
+ updateSpaceShooterAstFallPreview();
+ clearSpaceShooterAstExplodeRows();
+ for (var ai = 1; ai < astArr.length && ai < 32; ai++) {
+ addSpaceShooterAstExplodeRow(astArr[ai]);
+ }
}
var inpFms = el('space-shooter-asteroid-explode-ms');
if (inpFms) {
@@ -2819,7 +3052,7 @@
limSs = limSs <= 0 ? 0 : Math.max(10, Math.min(7200, limSs));
var shipUrls = readSpaceShooterShipImageUrlsFromForm();
var damageUrls = readSpaceShooterShipDamageOverlayUrlsFromForm();
- var astUrls = parseSpaceShooterAsteroidSpriteUrlsTextarea();
+ var astUrls = readSpaceShooterAsteroidSpriteUrlsFromForm();
var inpFms = el('space-shooter-asteroid-explode-ms');
var explodeMs = inpFms ? parseInt(String(inpFms.value), 10) : 70;
if (Number.isNaN(explodeMs)) explodeMs = 70;
@@ -3073,6 +3306,7 @@
}
bindSpaceShooterShipUrlInputs();
bindSpaceShooterDamageOverlayPanel();
+ bindSpaceShooterAsteroidSpritePanel();
var btnStackGameSave = el('btn-stack-game-save');
if (btnStackGameSave) {
btnStackGameSave.addEventListener('click', saveStackGamePanel);
diff --git a/www/html/Admin/api/game-quiz-settings.php b/www/html/Admin/api/game-quiz-settings.php
index da3cbda..cf27433 100644
--- a/www/html/Admin/api/game-quiz-settings.php
+++ b/www/html/Admin/api/game-quiz-settings.php
@@ -52,7 +52,7 @@ function merge_quiz_settings_disk_from_put(string $rawBody): bool
if ($hadFileButInvalidJson) {
return false;
}
- $mergeKeys = ['readMs', 'answerMs', 'betweenMs', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryMapPanelTheme', 'carryEmbedCountdownTheme', 'carryChoicePlaqueTheme', 'carryChoicePlaqueThemes', 'carryChoicePlaqueMapScale', 'questions', 'carryQuestions', 'battleQuizMcq'];
+ $mergeKeys = ['readMs', 'answerMs', 'betweenMs', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryMapPanelTheme', 'carryEmbedCountdownTheme', 'carryChoicePlaqueTheme', 'carryChoicePlaqueThemes', 'carryChoicePlaqueMapScale', 'carryWalkSpeedMultForMapId', 'carryWalkSpeedMult', 'questions', 'carryQuestions', 'battleQuizMcq'];
foreach ($mergeKeys as $k) {
if (array_key_exists($k, $patch)) {
$base[$k] = $patch[$k];
@@ -103,7 +103,7 @@ function overlay_quiz_settings_from_disk(string $nodeBody): string
$j['carryEmbedCountdownTheme'] = $disk['carryEmbedCountdownTheme'];
}
/* ไม่ทับ carryChoicePlaqueThemes / carryChoicePlaqueTheme จากดิสก์ — ใช้ค่าจาก Node (อ่านไฟล์เดียวกันกับ disk merge) เพื่อกันทับด้วย JSON เก่าที่ไม่มี plaqueImageUrl ทำให้ช่อง URL ใน Admin ว่างหลังรีเฟรช */
- foreach (['carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryChoicePlaqueMapScale'] as $ck) {
+ foreach (['carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryChoicePlaqueMapScale', 'carryWalkSpeedMultForMapId', 'carryWalkSpeedMult'] as $ck) {
if (array_key_exists($ck, $disk)) {
$j[$ck] = $disk[$ck];
}
@@ -191,6 +191,12 @@ function proxy_quiz_curl(string $method, ?string $body = null): void
if (array_key_exists('carrySessionLength', $dj)) {
$payload['carrySessionLength'] = $dj['carrySessionLength'];
}
+ if (array_key_exists('carryWalkSpeedMultForMapId', $dj)) {
+ $payload['carryWalkSpeedMultForMapId'] = $dj['carryWalkSpeedMultForMapId'];
+ }
+ if (array_key_exists('carryWalkSpeedMult', $dj)) {
+ $payload['carryWalkSpeedMult'] = $dj['carryWalkSpeedMult'];
+ }
}
}
echo json_encode($payload, JSON_UNESCAPED_UNICODE);
diff --git a/www/html/Admin/game-quiz-settings.php b/www/html/Admin/game-quiz-settings.php
index 12a832c..2178bb2 100644
--- a/www/html/Admin/game-quiz-settings.php
+++ b/www/html/Admin/game-quiz-settings.php
@@ -46,7 +46,7 @@ function merge_quiz_settings_disk_from_put(string $rawBody): bool
}
}
}
- $mergeKeys = ['readMs', 'answerMs', 'betweenMs', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryMapPanelTheme', 'carryEmbedCountdownTheme', 'questions', 'carryQuestions', 'battleQuizMcq'];
+ $mergeKeys = ['readMs', 'answerMs', 'betweenMs', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryMapPanelTheme', 'carryEmbedCountdownTheme', 'carryWalkSpeedMultForMapId', 'carryWalkSpeedMult', 'questions', 'carryQuestions', 'battleQuizMcq'];
foreach ($mergeKeys as $k) {
if (array_key_exists($k, $patch)) {
$base[$k] = $patch[$k];
diff --git a/www/html/Admin/index.html b/www/html/Admin/index.html
index 038fc4b..186d6ab 100644
--- a/www/html/Admin/index.html
+++ b/www/html/Admin/index.html
@@ -9,7 +9,7 @@
-
+