minigame 6 rocker 1.4

This commit is contained in:
2026-05-01 11:09:11 +00:00
parent ed0f24b344
commit e548b796fc
10 changed files with 395 additions and 43 deletions
+2 -15
View File
@@ -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;
}
+245 -11
View File
@@ -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 =
'<span class="space-shooter-ship-slot space-shooter-ship-slot--ast">' +
ix +
'</span><label class="space-shooter-ship-url-label">URL <input type="text" class="space-shooter-ast-explode-url" maxlength="500" spellcheck="false" placeholder="/Game/img/gauntlet-assets/....png" autocomplete="off"></label>' +
'<input type="file" class="space-shooter-ast-explode-file space-shooter-damage-file" accept="image/png,image/webp,image/jpeg,image/gif">' +
'<button type="button" class="btn btn-ghost btn-space-shooter-ast-explode-upload">อัปโหลด</button>' +
'<img class="space-shooter-ship-prev space-shooter-damage-prev space-shooter-ast-explode-prev" alt="" width="56" height="56" decoding="async">' +
'<button type="button" class="btn btn-ghost btn-space-shooter-ast-explode-clear">ล้าง</button>' +
'<button type="button" class="btn btn-ghost btn-space-shooter-ast-explode-remove">ลบแถว</button>';
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);
+8 -2
View File
@@ -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);
+1 -1
View File
@@ -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];
+22 -5
View File
@@ -9,7 +9,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Kanit:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="admin.css?v=25">
<link rel="stylesheet" href="admin.css?v=26">
</head>
<body>
<a class="skip-link" href="#admin-main">ข้ามไปเนื้อหา</a>
@@ -182,6 +182,16 @@
<span id="quiz-carry-session-hint" class="muted" style="display:block;font-size:0.85rem;margin-top:0.2rem">สุ่มคำถามจากชุดจนกว่าจะครบจำนวนนี้ (นับทุกข้อที่จบด้วยถูกหรือหมดเวลา) · 0 = เล่นต่อไม่จบอัตโนมัติ · <em>English:</em> Preview / editor embed only.</span>
</label>
</div>
<div class="admin-form-row" style="margin:0 0 1rem;flex-wrap:wrap;gap:1rem;align-items:flex-end">
<label class="admin-field">ความเร็วเดิน — รหัสฉาก (map id)
<input type="text" id="quiz-carry-walk-map-id" class="admin-inp-num" style="width:11rem;max-width:100%" maxlength="64" pattern="[a-zA-Z0-9_-]*" placeholder="เช่น mnorwqx1" autocomplete="off" spellcheck="false" aria-describedby="quiz-carry-walk-map-hint" />
<span id="quiz-carry-walk-map-hint" class="muted" style="display:block;font-size:0.85rem;margin-top:0.2rem">ใส่<strong>รหัสฉาก</strong>จาก Editor (หลังบันทึก) ให้ตรงกับแมปที่ต้องการ · ว่าง = ไม่ override · เก็บที่ <code>carryWalkSpeedMultForMapId</code> · <em>English:</em> Scene id must match the map you want to speed up.</span>
</label>
<label class="admin-field">คูณความเร็วเดิน (0.5–3)
<input type="number" id="quiz-carry-walk-mult" class="admin-inp-num" min="0.5" max="3" step="0.05" value="1.42" aria-describedby="quiz-carry-walk-mult-hint" />
<span id="quiz-carry-walk-mult-hint" class="muted" style="display:block;font-size:0.85rem;margin-top:0.2rem">ใช้เฉพาะเมื่อรหัสฉากตรงกับห้องเล่น/พรีวิว · ค่าเริ่มในเกมถ้าไม่ตั้ง = <strong>1.42</strong> · <code>carryWalkSpeedMult</code></span>
</label>
</div>
<fieldset class="quiz-carry-theme-fieldset" style="margin:0.75rem 0 1rem;padding:0.75rem 1rem;border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-elevated)">
<legend class="quiz-carry-theme-legend" style="font-size:0.92rem;font-weight:600;padding:0 0.35rem">แผงข้อความบนแผนที่ (คำถาม / ตัวเลือกที่ถือ)</legend>
<p class="muted" style="margin:0 0 0.65rem;font-size:0.82rem;line-height:1.45">ใช้กับ <code>#quiz-map-question-panel</code> ในโหมดหยิบมาวาง (โซนทองหรือโซนกลาง) · เก็บที่ <code>carryMapPanelTheme</code> ใน <code>quiz-settings.json</code> · <em>English:</em> Color pickers + alpha (0100%) — saved as <code>rgba()</code>.</p>
@@ -505,9 +515,16 @@
</fieldset>
<fieldset class="quiz-timing-fieldset">
<legend>อุกาบาต — PNG sequence</legend>
<p class="muted" style="margin-top:0"><strong>บรรทัดที่ 1</strong> = รูปตอนตก (loop ขณะล่วง) · <strong>บรรทัด 2 เป็นต้นไป</strong> = เฟรมแตกตามลำดับหลังโดนยิง/ชน · สูงสุด 32 บรรทัด · <em>English:</em> Line 1 = falling sprite; lines 2+ = explosion frames played once.</p>
<label for="space-shooter-asteroid-sprite-urls" class="sr-only">URLs อุกาบาต ทีละบรรทัด</label>
<textarea id="space-shooter-asteroid-sprite-urls" class="space-shooter-asteroid-urls-ta" rows="8" spellcheck="false" placeholder="/Game/img/ViolentCrime/ast-fall.png&#10;/Game/img/ViolentCrime/ast-x1.png&#10;/Game/img/ViolentCrime/ast-x2.png"></textarea>
<p class="muted" style="margin-top:0"><strong>แถวแรก</strong> = รูปตอนตก (loop ขณะล่วง) · <strong>แถวถัดไป</strong> = เฟรมแตกตามลำดับหลังโดนยิง/ชน · รวมได้สูงสุด 32 URL · อัปโหลดผ่านคลัง Gauntlet เหมือนรอยความเสียหาย · <em>English:</em> First row = falling sprite; add rows for explosion frames (max 31 extra).</p>
<div class="space-shooter-ship-grid" role="group" aria-label="อุกาบาต รูปตอนตก">
<div class="space-shooter-ship-row"><span class="space-shooter-ship-slot space-shooter-ship-slot--ast" title="รูปขณะอุกาบาตร่วง">ตก</span><label class="space-shooter-ship-url-label">URL <input type="text" id="space-shooter-ast-fall-url" maxlength="500" spellcheck="false" placeholder="/Game/img/gauntlet-assets/....png" autocomplete="off"></label><input type="file" id="space-shooter-ast-fall-file" class="space-shooter-damage-file" accept="image/png,image/webp,image/jpeg,image/gif"><button type="button" class="btn btn-ghost" id="btn-space-shooter-ast-fall-upload">อัปโหลด</button><img class="space-shooter-ship-prev space-shooter-damage-prev" id="space-shooter-ast-fall-prev" alt="" width="56" height="56" decoding="async"><button type="button" class="btn btn-ghost" id="btn-space-shooter-ast-fall-clear">ล้าง</button></div>
</div>
<p class="muted" style="margin:0.75rem 0 0.35rem">เฟรมแตก (ลำดับหลังยิง/ชน)</p>
<div id="space-shooter-ast-explode-rows" class="space-shooter-ship-grid" role="group" aria-label="เฟรมแตกอุกาบาต"></div>
<div class="space-shooter-ast-explode-actions" style="margin-top:0.5rem;display:flex;flex-wrap:wrap;gap:0.5rem;align-items:center">
<button type="button" class="btn btn-ghost" id="btn-space-shooter-ast-add-explode">+ เพิ่มเฟรมแตก</button>
<span class="muted" style="font-size:0.85rem">สูงสุด 31 แถว · <em>English:</em> Up to 31 explosion rows.</span>
</div>
<div class="form-grid form-inline quiz-timing-grid" style="margin-top:0.65rem">
<label title="ความเร็วเฟรมแอนิเมชันแตก (มิลลิวินาทีต่อเฟรม)">เฟรมแตก (ms) <input type="number" id="space-shooter-asteroid-explode-ms" min="30" max="500" step="5" value="70"></label>
</div>
@@ -753,6 +770,6 @@
</div>
</main>
</div>
<script src="admin.js?v=60"></script>
<script src="admin.js?v=61"></script>
</body>
</html>
+4 -2
View File
@@ -164,6 +164,9 @@
"borderWidthPx": 0,
"plaqueImageUrl": "https:\/\/srv1361159.hstgr.cloud\/Game\/img\/quiz-carry\/board-2.png"
},
"carryChoicePlaqueMapScale": 1.9,
"carryWalkSpeedMultForMapId": "",
"carryWalkSpeedMult": null,
"questions": [
{
"text": "test1",
@@ -275,6 +278,5 @@
],
"correctIndex": 0
}
],
"carryChoicePlaqueMapScale": 1.9
]
}
@@ -25,7 +25,7 @@ if (!is_array($data)) {
}
$out = [];
$keys = ['carryMapPanelTheme', 'carryEmbedCountdownTheme', 'carryChoicePlaqueThemes', 'carryChoicePlaqueTheme', 'carryChoicePlaqueMapScale', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryQuestions'];
$keys = ['carryMapPanelTheme', 'carryEmbedCountdownTheme', 'carryChoicePlaqueThemes', 'carryChoicePlaqueTheme', 'carryChoicePlaqueMapScale', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryWalkSpeedMultForMapId', 'carryWalkSpeedMult', 'carryQuestions'];
foreach ($keys as $k) {
if (!array_key_exists($k, $data)) {
continue;
+62 -5
View File
@@ -718,8 +718,42 @@
return getStoredCharacterId();
}
const MOVE_SPEED = 0.15;
/** quiz_carry — ค่าเริ่มเมื่อไม่มี override จาก Admin (รหัสฉากตรงกันใน quiz-settings.json) */
const QUIZ_CARRY_WALK_SPEED_MULT = 1.42;
/** ค่าที่ใช้จริงต่อเฟรม — อัปเดตจาก /api/quiz-settings + join snap */
let quizCarryWalkSpeedMultActive = QUIZ_CARRY_WALK_SPEED_MULT;
const PATH_ARRIVE_THRESH = 0.15;
function clampQuizCarryWalkSpeedMultClient(n, def) {
const v = Number(n);
if (!Number.isFinite(v)) return def;
return Math.round(Math.max(0.5, Math.min(3, v)) * 100) / 100;
}
function resolveQuizCarryWalkSpeedMultFromSettingsObj(s) {
const base = QUIZ_CARRY_WALK_SPEED_MULT;
if (!s || typeof s !== 'object') return base;
const forId = String(s.carryWalkSpeedMultForMapId ?? '').trim();
const mult = Number(s.carryWalkSpeedMult);
const mapId = (currentPlayMapId() || '').trim();
if (forId && mapId && forId === mapId && Number.isFinite(mult)) {
return clampQuizCarryWalkSpeedMultClient(mult, base);
}
return base;
}
function applyQuizCarryWalkSpeedFromSettingsObj(s) {
if (!isQuizCarry()) {
quizCarryWalkSpeedMultActive = QUIZ_CARRY_WALK_SPEED_MULT;
return;
}
quizCarryWalkSpeedMultActive = resolveQuizCarryWalkSpeedMultFromSettingsObj(s);
}
function moveSpeedTilesThisFrameForWalk() {
return MOVE_SPEED * (isQuizCarry() ? quizCarryWalkSpeedMultActive : 1);
}
function isMovementKey(code) {
return ['KeyW','KeyA','KeyS','KeyD','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].indexOf(code) !== -1;
}
@@ -2518,6 +2552,7 @@
playPath = [];
hidePlayQuizHud();
quizCarryJoinSettingsSnap = null;
quizCarryWalkSpeedMultActive = QUIZ_CARRY_WALK_SPEED_MULT;
quizCarryMapPanelTheme = null;
quizCarryChoicePlaqueThemes = null;
resetQuizCarryPlayState();
@@ -4842,6 +4877,13 @@
if (disk.carryReadMs != null) out.carryReadMs = disk.carryReadMs;
if (disk.carryAnswerMs != null) out.carryAnswerMs = disk.carryAnswerMs;
if (disk.carrySessionLength != null) out.carrySessionLength = disk.carrySessionLength;
if (disk.carryWalkSpeedMultForMapId != null) {
out.carryWalkSpeedMultForMapId = String(disk.carryWalkSpeedMultForMapId).trim();
}
if (disk.carryWalkSpeedMult != null) {
const wm = Number(disk.carryWalkSpeedMult);
if (Number.isFinite(wm)) out.carryWalkSpeedMult = wm;
}
const diskPlaqueScale = Number(disk.carryChoicePlaqueMapScale);
if (Number.isFinite(diskPlaqueScale)) {
out.carryChoicePlaqueMapScale = Math.max(0.85, Math.min(2.5, diskPlaqueScale));
@@ -4901,6 +4943,13 @@
const sm = Number(snap.carryChoicePlaqueMapScale);
if (Number.isFinite(sm)) s.carryChoicePlaqueMapScale = sm;
}
if (snap) {
if ((s.carryWalkSpeedMultForMapId == null || String(s.carryWalkSpeedMultForMapId).trim() === '') && snap.carryWalkSpeedMultForMapId != null) {
s.carryWalkSpeedMultForMapId = snap.carryWalkSpeedMultForMapId;
}
if (s.carryWalkSpeedMult == null && snap.carryWalkSpeedMult != null) s.carryWalkSpeedMult = snap.carryWalkSpeedMult;
}
applyQuizCarryWalkSpeedFromSettingsObj(s);
{
const sc = Number(s.carryChoicePlaqueMapScale);
quizCarryPlaqueMapScale = Number.isFinite(sc) ? Math.max(0.85, Math.min(2.5, sc)) : 1.25;
@@ -5070,7 +5119,7 @@
const accY = o.botWanderDy;
if (Math.abs(accY) > Math.abs(accX)) o.direction = accY > 0 ? 'down' : 'up';
else if (accX !== 0) o.direction = accX > 0 ? 'right' : 'left';
const step = MOVE_SPEED;
const step = MOVE_SPEED * quizCarryWalkSpeedMultActive;
const nx = o.x + accX * step;
const ny = o.y + accY * step;
const ox = o.x;
@@ -8853,6 +8902,9 @@
const prevWasSpaceShooter = mapData && mapData.gameType === 'space_shooter';
const prevWasBalloonBoss = mapData && mapData.gameType === 'balloon_boss';
mapData = gameMapData;
if (mapData.gameType !== 'quiz_carry') {
quizCarryWalkSpeedMultActive = QUIZ_CARRY_WALK_SPEED_MULT;
}
if (res && res.mapId != null && String(res.mapId).trim() !== '') {
playSessionMapId = String(res.mapId).trim();
}
@@ -8897,6 +8949,7 @@
if (!mapData.quizQuestions) mapData.quizQuestions = [];
if (!mapData.quizQuestionArea) mapData.quizQuestionArea = [];
normalizeQuizCarryLayersInPlay(mapData);
applyQuizCarryWalkSpeedFromSettingsObj(quizCarryJoinSettingsSnap || {});
}
if (mapData.gameType === 'quiz_battle') {
if (!mapData.quizBattleDomeArea) mapData.quizBattleDomeArea = [];
@@ -10226,8 +10279,12 @@
return;
}
const len = dist || 1;
const pathStepMul = (previewFillBots && editorEmbedReturn) ? 0.56 : 1;
const step = Math.min(MOVE_SPEED * 1.28 * pathStepMul, len);
const carryWalk = isQuizCarry() ? quizCarryWalkSpeedMultActive : 1;
const baseEmbedCarryPathMul = 0.74;
const pathStepMul = (previewFillBots && editorEmbedReturn)
? (isQuizCarry() ? baseEmbedCarryPathMul * (quizCarryWalkSpeedMultActive / QUIZ_CARRY_WALK_SPEED_MULT) : 0.56)
: 1;
const step = Math.min(MOVE_SPEED * 1.28 * pathStepMul * carryWalk, len);
const nx = o.x + (dx / len) * step;
const ny = o.y + (dy / len) * step;
if (Math.abs(dy) > Math.abs(dx)) o.direction = dy > 0 ? 'down' : 'up';
@@ -10244,7 +10301,7 @@
o.y = ny;
} else {
/* เส้นตรงไป waypoint อาจตัดมุม hub / block — ลองก้าวแนวแกนตามทิศหลักอย่างใดอย่างหนึ่ง */
const cstep = Math.min(MOVE_SPEED * 1.2 * pathStepMul, Math.max(Math.abs(dx), Math.abs(dy), 1e-4));
const cstep = Math.min(MOVE_SPEED * 1.2 * pathStepMul * carryWalk, Math.max(Math.abs(dx), Math.abs(dy), 1e-4));
const sx = dx > 1e-4 ? 1 : dx < -1e-4 ? -1 : 0;
const sy = dy > 1e-4 ? 1 : dy < -1e-4 ? -1 : 0;
const tryAxisX = () => {
@@ -12076,7 +12133,7 @@
const preWalkX = me.x, preWalkY = me.y;
if (accX !== 0 || accY !== 0) {
const len = Math.sqrt(accX * accX + accY * accY) || 1;
const step = Math.min(MOVE_SPEED, len);
const step = Math.min(moveSpeedTilesThisFrameForWalk(), len);
const nx = me.x + (accX / len) * step;
const ny = me.y + (accY / len) * step;
/** Quiz Battle + เส้นทาง: ห้ามเลื่อนแกนเดียว (slide) — ไม่งั้นเดินทแยงหลุดออกนอกเลนได้ */
+1 -1
View File
@@ -1897,7 +1897,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.222"></script>
<script src="js/play.js?v=0.224"></script>
<div class="version-tag">v —</div>
</body>
</html>
+49
View File
@@ -191,6 +191,9 @@ function defaultQuizSettings() {
carryChoicePlaqueTheme: defaultCarryChoicePlaqueTheme(),
/** quiz_carry: ขยายป้ายตัวเลือกบนแมป (กว้าง/สูง/ฟอนต์) — 0.85–2.5 */
carryChoicePlaqueMapScale: 1.25,
/** quiz_carry: ถ้าใส่รหัสฉาก + คูณ — เดินเร็วเฉพาะแมปนั้น (เทียบกับค่าเริ่มในไคลเอนต์) */
carryWalkSpeedMultForMapId: '',
carryWalkSpeedMult: null,
questions: [],
carryQuestions: [],
battleQuizMcq: [],
@@ -446,6 +449,22 @@ function clampCarryChoicePlaqueMapScale(n, def) {
return Math.round(Math.max(0.85, Math.min(2.5, v)) * 100) / 100;
}
/** รหัสฉาก (เช่น mnorwqx1) — ว่าง = ไม่ใช้คูณเดินแยกแมป */
function sanitizeCarryWalkSpeedMultForMapId(raw) {
const t = String(raw == null ? '' : raw).trim().slice(0, 64);
if (!t) return '';
if (!/^[a-zA-Z0-9_-]+$/.test(t)) return '';
return t;
}
/** quiz_carry: คูณความเร็วเดินเมื่อ map id ตรง — null = ไม่ตั้ง */
function clampCarryWalkSpeedMultSetting(n) {
if (n === null || n === undefined || n === '') return null;
const v = Number(n);
if (Number.isNaN(v)) return null;
return Math.round(Math.max(0.5, Math.min(3, v)) * 100) / 100;
}
function mergeQuizCarryFromMirrorIfSet(base) {
if (!QUIZ_SETTINGS_MIRROR_PATH) return base;
try {
@@ -465,6 +484,13 @@ function mergeQuizCarryFromMirrorIfSet(base) {
if (j.carrySessionLength != null) {
base.carrySessionLength = clampCarrySessionLength(j.carrySessionLength, base.carrySessionLength);
}
if (j.carryWalkSpeedMultForMapId != null) {
base.carryWalkSpeedMultForMapId = sanitizeCarryWalkSpeedMultForMapId(j.carryWalkSpeedMultForMapId);
}
if (j.carryWalkSpeedMult != null) {
const wm = clampCarryWalkSpeedMultSetting(j.carryWalkSpeedMult);
if (wm != null) base.carryWalkSpeedMult = wm;
}
if (j.carryEmbedCountdownTheme != null && typeof j.carryEmbedCountdownTheme === 'object') {
base.carryEmbedCountdownTheme = sanitizeCarryEmbedCountdownTheme(j.carryEmbedCountdownTheme);
}
@@ -500,6 +526,10 @@ function loadQuizSettings() {
: sanitizeCarryChoicePlaqueThemes(j.carryChoicePlaqueTheme);
const carryChoicePlaqueTheme = carryChoicePlaqueThemes[0];
const carryChoicePlaqueMapScale = clampCarryChoicePlaqueMapScale(j.carryChoicePlaqueMapScale, d.carryChoicePlaqueMapScale);
const carryWalkSpeedMultForMapId = sanitizeCarryWalkSpeedMultForMapId(j.carryWalkSpeedMultForMapId);
let carryWalkSpeedMult = clampCarryWalkSpeedMultSetting(j.carryWalkSpeedMult);
if (!carryWalkSpeedMultForMapId) carryWalkSpeedMult = null;
else if (carryWalkSpeedMult == null) carryWalkSpeedMult = clampCarryWalkSpeedMultSetting(1.42);
const out = {
readMs: clampQuizMs(j.readMs, d.readMs),
answerMs: clampQuizMs(j.answerMs, d.answerMs),
@@ -512,6 +542,8 @@ function loadQuizSettings() {
carryChoicePlaqueThemes,
carryChoicePlaqueTheme,
carryChoicePlaqueMapScale,
carryWalkSpeedMultForMapId,
carryWalkSpeedMult,
questions,
carryQuestions,
battleQuizMcq,
@@ -1037,6 +1069,17 @@ function saveQuizSettings(d) {
const carryChoicePlaqueMapScale = d.carryChoicePlaqueMapScale != null
? clampCarryChoicePlaqueMapScale(d.carryChoicePlaqueMapScale, prevScale)
: prevScale;
const prevWalkId = sanitizeCarryWalkSpeedMultForMapId(prev.carryWalkSpeedMultForMapId);
const prevWalkMult = prev.carryWalkSpeedMult != null ? clampCarryWalkSpeedMultSetting(prev.carryWalkSpeedMult) : null;
let carryWalkSpeedMultForMapId = d.carryWalkSpeedMultForMapId !== undefined
? sanitizeCarryWalkSpeedMultForMapId(d.carryWalkSpeedMultForMapId)
: prevWalkId;
let carryWalkSpeedMult = d.carryWalkSpeedMult !== undefined ? clampCarryWalkSpeedMultSetting(d.carryWalkSpeedMult) : prevWalkMult;
if (!carryWalkSpeedMultForMapId) {
carryWalkSpeedMult = null;
} else if (carryWalkSpeedMult == null) {
carryWalkSpeedMult = clampCarryWalkSpeedMultSetting(1.42);
}
const out = {
readMs,
answerMs,
@@ -1049,6 +1092,8 @@ function saveQuizSettings(d) {
carryChoicePlaqueThemes,
carryChoicePlaqueTheme,
carryChoicePlaqueMapScale,
carryWalkSpeedMultForMapId,
carryWalkSpeedMult,
questions,
carryQuestions,
battleQuizMcq,
@@ -1748,6 +1793,8 @@ const server = http.createServer((req, res) => {
carryReadMs: g.carryReadMs,
carryAnswerMs: g.carryAnswerMs,
carrySessionLength: g.carrySessionLength,
carryWalkSpeedMultForMapId: g.carryWalkSpeedMultForMapId,
carryWalkSpeedMult: g.carryWalkSpeedMult,
carryQuestions: g.carryQuestions,
};
res.writeHead(200, {
@@ -3416,6 +3463,8 @@ io.on('connection', (socket) => {
carryReadMs: qs.carryReadMs,
carryAnswerMs: qs.carryAnswerMs,
carrySessionLength: qs.carrySessionLength,
carryWalkSpeedMultForMapId: qs.carryWalkSpeedMultForMapId,
carryWalkSpeedMult: qs.carryWalkSpeedMult,
};
} catch (e) { /* ignore */ }
}