From e548b796fcb4e8142e05e4c7b34b35d64779ab51 Mon Sep 17 00:00:00 2001 From: giteaadmin Date: Fri, 1 May 2026 11:09:11 +0000 Subject: [PATCH] minigame 6 rocker 1.4 --- www/html/Admin/admin.css | 17 +- www/html/Admin/admin.js | 256 +++++++++++++++++- www/html/Admin/api/game-quiz-settings.php | 10 +- www/html/Admin/game-quiz-settings.php | 2 +- www/html/Admin/index.html | 27 +- www/html/Game/data/quiz-settings.json | 6 +- .../Game/public/api-quiz-carry-from-disk.php | 2 +- www/html/Game/public/js/play.js | 67 ++++- www/html/Game/public/play.html | 2 +- www/html/Game/server.js | 49 ++++ 10 files changed, 395 insertions(+), 43 deletions(-) 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 @@ - + @@ -182,6 +182,16 @@ สุ่มคำถามจากชุดจนกว่าจะครบจำนวนนี้ (นับทุกข้อที่จบด้วยถูกหรือหมดเวลา) · 0 = เล่นต่อไม่จบอัตโนมัติ · English: Preview / editor embed only. +
+ + +
แผงข้อความบนแผนที่ (คำถาม / ตัวเลือกที่ถือ)

ใช้กับ #quiz-map-question-panel ในโหมดหยิบมาวาง (โซนทองหรือโซนกลาง) · เก็บที่ carryMapPanelTheme ใน quiz-settings.json · English: Color pickers + alpha (0–100%) — saved as rgba().

@@ -505,9 +515,16 @@
อุกาบาต — PNG sequence -

บรรทัดที่ 1 = รูปตอนตก (loop ขณะล่วง) · บรรทัด 2 เป็นต้นไป = เฟรมแตกตามลำดับหลังโดนยิง/ชน · สูงสุด 32 บรรทัด · English: Line 1 = falling sprite; lines 2+ = explosion frames played once.

- - +

แถวแรก = รูปตอนตก (loop ขณะล่วง) · แถวถัดไป = เฟรมแตกตามลำดับหลังโดนยิง/ชน · รวมได้สูงสุด 32 URL · อัปโหลดผ่านคลัง Gauntlet เหมือนรอยความเสียหาย · English: First row = falling sprite; add rows for explosion frames (max 31 extra).

+
+
ตก
+
+

เฟรมแตก (ลำดับหลังยิง/ชน)

+
+
+ + สูงสุด 31 แถว · English: Up to 31 explosion rows. +
@@ -753,6 +770,6 @@ - + diff --git a/www/html/Game/data/quiz-settings.json b/www/html/Game/data/quiz-settings.json index 8cecb19..e14ea7e 100644 --- a/www/html/Game/data/quiz-settings.json +++ b/www/html/Game/data/quiz-settings.json @@ -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 + ] } \ No newline at end of file diff --git a/www/html/Game/public/api-quiz-carry-from-disk.php b/www/html/Game/public/api-quiz-carry-from-disk.php index e7f6ad8..4ddf2f3 100644 --- a/www/html/Game/public/api-quiz-carry-from-disk.php +++ b/www/html/Game/public/api-quiz-carry-from-disk.php @@ -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; diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js index 264592b..595fb09 100644 --- a/www/html/Game/public/js/play.js +++ b/www/html/Game/public/js/play.js @@ -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) — ไม่งั้นเดินทแยงหลุดออกนอกเลนได้ */ diff --git a/www/html/Game/public/play.html b/www/html/Game/public/play.html index 61cd925..081cc8f 100644 --- a/www/html/Game/public/play.html +++ b/www/html/Game/public/play.html @@ -1897,7 +1897,7 @@ - +
v —
diff --git a/www/html/Game/server.js b/www/html/Game/server.js index 2e76cfe..1038662 100644 --- a/www/html/Game/server.js +++ b/www/html/Game/server.js @@ -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 */ } }