diff --git a/www/html/Admin/admin.js b/www/html/Admin/admin.js index 9588e35..b40ae61 100644 --- a/www/html/Admin/admin.js +++ b/www/html/Admin/admin.js @@ -2618,6 +2618,11 @@ var jsm = Number(data.jumpSurviveJumpHeightMult); inp.value = String(Number.isFinite(jsm) ? Math.round(Math.max(0.5, Math.min(4, jsm)) * 100) / 100 : 1.5); } + var inpT = el('jump-survive-mission-sec'); + if (inpT) { + var jss = Number(data.jumpSurviveMissionTimeSec); + inpT.value = String(Number.isFinite(jss) && jss > 0 ? Math.floor(jss) : 0); + } setMsg('jump-survive-timing-msg', '', ''); }) .catch(function (e) { @@ -2630,9 +2635,13 @@ var jumpSurvMult = inp ? parseFloat(String(inp.value)) : 1.5; if (Number.isNaN(jumpSurvMult)) jumpSurvMult = 1.5; jumpSurvMult = Math.round(Math.max(0.5, Math.min(4, jumpSurvMult)) * 100) / 100; + var limJump = el('jump-survive-mission-sec') ? parseInt(String(el('jump-survive-mission-sec').value), 10) : 0; + if (Number.isNaN(limJump) || limJump < 0) limJump = 0; + limJump = limJump <= 0 ? 0 : Math.max(10, Math.min(7200, limJump)); gameTimingFetch('GET') .then(function (data) { data.jumpSurviveJumpHeightMult = jumpSurvMult; + data.jumpSurviveMissionTimeSec = limJump; return gameTimingFetch('PUT', data); }) .then(function () { @@ -2672,7 +2681,7 @@ return gameTimingFetch('PUT', data); }) .then(function () { - setMsg('game-timing-msg', 'บันทึกแล้ว — พรมแดง tick·กระโดด / รูปอุปสรรค์ · กระโดดให้รอดตั้งที่แท็บกระโดดให้รอด · ค่า Stack แก้ที่แท็บ Stack เท่านั้น · จำกัดเวลาใช้กับรอบถัดไปที่โฮสต์เริ่มเกม', 'ok'); + setMsg('game-timing-msg', 'บันทึกแล้ว — พรมแดง tick·กระโดด / รูปอุปสรรค์ · มินิเกมกระโดดขึ้นแท่นตั้งที่แท็บ Minigame-5 · ค่า Stack แก้ที่แท็บ Stack เท่านั้น · จำกัดเวลาพรมแดงใช้กับรอบถัดไปที่โฮสต์เริ่มเกม', 'ok'); }) .catch(function (e) { setMsg('game-timing-msg', e.message || 'บันทึกไม่ได้', 'error'); diff --git a/www/html/Admin/index.html b/www/html/Admin/index.html index 3f8d841..810f512 100644 --- a/www/html/Admin/index.html +++ b/www/html/Admin/index.html @@ -443,7 +443,7 @@ กระโดดให้รอด — ตั้งค่าเกม - คนละโหมดกับ พรมแดง (Gauntlet) · เก็บที่ /Game/data/game-timing.json ฟิลด์ jumpSurviveJumpHeightMult · ผู้เล่นได้ค่าใหม่เมื่อโหลดหน้าเล่น / sync timing จากเซิร์ฟเวอร์ · ถ้า API 404 ให้รีสตาร์ท Node ที่รัน Game/server.js + คนละโหมดกับ พรมแดง (Gauntlet) · เก็บที่ /Game/data/game-timing.json ฟิลด์ jumpSurviveJumpHeightMult และ jumpSurviveMissionTimeSec · ผู้เล่นได้ค่าใหม่เมื่อโหลดหน้าเล่น / GET /api/game-timing · ถ้า API 404 ให้รีสตาร์ท Node ที่รัน Game/server.js ความสูงกระโดด @@ -451,6 +451,13 @@ ใช้กับฉากประเภท กระโดดให้รอด เท่านั้น · ถ้าในแมปตั้ง jumpSurviveJumpImpulse > 0 จะใช้ค่านั้นแทน (override) · English: Jump height multiplier vs character height; per-map impulse overrides. + + จำกัดเวลารอบ (มินิเกม / ภารกิจกระโดดขึ้นแท่น) + + เวลารอบ (วินาที) + + + นับจากเริ่มรอบจริง (หลังนับถอยหลัง) · ถ้าในแมปตั้ง jumpSurviveTimeSec > 0 จะใช้ค่าบนแมปแทนทั้งหมด · ไม่ใช่ค่าเดียวกับ "จำกัดเวลาเกม" ของพรมแดง · English: Per-map jumpSurviveTimeSec overrides this global default. บันทึก @@ -459,7 +466,7 @@ เวลาเกม — พรมแดงสุดท้าย (Gauntlet) - เก็บที่เซิร์ฟเวอร์เกม /Game/data/game-timing.json · หลังบันทึกห้องที่กำลังเล่นจะปรับความถี่ tick ทันที · ค่าจะส่งไปยังผู้เล่นผ่าน gauntlet-sync · จำกัดเวลานับจากตอนโฮสต์เริ่มเกมพรมแดง — หมดเวลาแล้วเกมจบและส่งทุกคนกลับฉากล็อบบี้บนเซิร์ฟเวอร์ · ถ้าขึ้น Not Found ให้รีสตาร์ท Nodeที่รัน Game/server.js (เช่น pm2 restart) หลัง deploy · กระโดดให้รอดตั้งที่แท็บชื่อเดียวกับโหมด + เก็บที่เซิร์ฟเวอร์เกม /Game/data/game-timing.json · หลังบันทึกห้องที่กำลังเล่นจะปรับความถี่ tick ทันที · ค่าจะส่งไปยังผู้เล่นผ่าน gauntlet-sync · จำกัดเวลานับจากตอนโฮสต์เริ่มเกมพรมแดง — หมดเวลาแล้วเกมจบและส่งทุกคนกลับฉากล็อบบี้บนเซิร์ฟเวอร์ · ถ้าขึ้น Not Found ให้รีสตาร์ท Nodeที่รัน Game/server.js (เช่น pm2 restart) หลัง deploy · เวลามินิเกมกระโดดขึ้นแท่นตั้งที่แท็บ Minigame-5 พารามิเตอร์ diff --git a/www/html/Game/data/game-timing.json b/www/html/Game/data/game-timing.json index 7e9e120..6ee5ddc 100644 --- a/www/html/Game/data/game-timing.json +++ b/www/html/Game/data/game-timing.json @@ -16,5 +16,6 @@ "gauntletLaserLineWidthPx": 0, "stackSwingHz": 0.1, "stackBlockWidthTiles": 3.2, - "jumpSurviveJumpHeightMult": 1.5 + "jumpSurviveJumpHeightMult": 1.5, + "jumpSurviveMissionTimeSec": 0 } \ No newline at end of file diff --git a/www/html/Game/public/game-timing-admin.html b/www/html/Game/public/game-timing-admin.html index 2e23b7a..106d54d 100644 --- a/www/html/Game/public/game-timing-admin.html +++ b/www/html/Game/public/game-timing-admin.html @@ -55,6 +55,11 @@ สูตร: ความสูงขึ้นสุด ≈ ค่านี้ × (ความสูงตัวเป็น pixel จากช่องบนแมป) · ถ้าในแมปตั้ง jumpSurviveJumpImpulse > 0 จะใช้ค่านั้นแทน (override) + + jumpSurviveMissionTimeSec — เวลารอบมินิเกมกระโดดขึ้นแท่น (วินาที · 0 = เกมใช้ค่าเริ่ม 60) + + ถ้าในแมปตั้ง jumpSurviveTimeSec > 0 จะใช้ค่าบนแมปแทน · ไม่ใช่ค่าเดียวกับพรมแดง + @@ -119,6 +124,8 @@ function fillForm() { document.getElementById('jumpSurviveJumpHeightMult').value = timing.jumpSurviveJumpHeightMult != null ? timing.jumpSurviveJumpHeightMult : 1.5; + const jms = Number(timing.jumpSurviveMissionTimeSec); + document.getElementById('jumpSurviveMissionTimeSec').value = Number.isFinite(jms) && jms > 0 ? Math.floor(jms) : 0; document.getElementById('gauntletTickMs').value = timing.gauntletTickMs != null ? timing.gauntletTickMs : 150; document.getElementById('gauntletJumpTicks').value = timing.gauntletJumpTicks != null ? timing.gauntletJumpTicks : 5; document.getElementById('gauntletTimeLimitSec').value = timing.gauntletTimeLimitSec != null ? timing.gauntletTimeLimitSec : 0; @@ -129,9 +136,13 @@ function readForm() { const jm = parseFloat(document.getElementById('jumpSurviveJumpHeightMult').value, 10); + let jmt = parseInt(document.getElementById('jumpSurviveMissionTimeSec').value, 10); + if (Number.isNaN(jmt) || jmt < 0) jmt = 0; + jmt = jmt <= 0 ? 0 : Math.max(10, Math.min(7200, jmt)); return { ...timing, jumpSurviveJumpHeightMult: Number.isFinite(jm) ? jm : 1.5, + jumpSurviveMissionTimeSec: jmt, gauntletTickMs: parseInt(document.getElementById('gauntletTickMs').value, 10), gauntletJumpTicks: parseInt(document.getElementById('gauntletJumpTicks').value, 10), gauntletTimeLimitSec: parseInt(document.getElementById('gauntletTimeLimitSec').value, 10), diff --git a/www/html/Game/public/img/Jumper/README.txt b/www/html/Game/public/img/Jumper/README.txt index 2aa0355..651180a 100644 --- a/www/html/Game/public/img/Jumper/README.txt +++ b/www/html/Game/public/img/Jumper/README.txt @@ -5,5 +5,6 @@ Upload files here; path in browser: /Game/img/Jumper/ (public URL) - **popup-Howto.png** — พื้นหลัง HOW TO ก่อนเริ่ม (ข้อความวิธีเล่นใส่ในรูปนี้ — หน้าเกมไม่ซ้อนข้อความ HTML) - **popup-result.png** — พื้นหลังสรุปภารกิจตอนจบ - **stamp-sacrifice.png** — ตราประทับ “ผู้เสียสละ” บนผู้เล่นที่ตกรอบ (optional; ถ้าโหลดไม่ได้ใช้ result-Lose_stamp) +- **result-timeup.png** — หมดเวลา (embed: หลังกดรับหลักฐาน ก่อน result-complete/gameover; ถ้าไม่มีไฟล์จะ fallback ไป quiz-carry/timeup-txt.png) โฟลเดอร์นี้สร้างจาก repo + deploy — ถ้า FTP ยังอัปโหลดไม่ได้ ให้เช็คสิทธิ์ parent diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js index cc1e5b2..6739c7b 100644 --- a/www/html/Game/public/js/play.js +++ b/www/html/Game/public/js/play.js @@ -690,7 +690,9 @@ let playStackBlockWidthTiles = null; /** กระโดดให้รอด: ความสูงกระโดด = ทวีคูณ × ความสูงตัว (ch×tile) — จาก GET /api/game-timing */ let playJumpSurviveJumpHeightMult = 1.5; - /** 0 = ไม่จำกัด — จาก game-timing / gauntlet-sync */ + /** จำกัดเวลารอบภารกิจกระโดดขึ้นแท่น (วินาที) — จาก GET /api/game-timing; 0 = ใช้ค่าเริ่ม 60 ในเกม */ + let playJumpSurviveMissionTimeSec = 0; + /** 0 = ไม่จำกัด — จาก game-timing / gauntlet-sync (พรมแดง Gauntlet เท่านั้น) */ let gauntletRuntimeTimeLimitSec = 0; /** เวลาสิ้นสุดรอบ (epoch ms) จากเซิร์ฟเวอร์ — null = ไม่จับเวลา */ let gauntletEndsAtMs = null; @@ -2505,8 +2507,8 @@ function jumpSurviveTimeLimitSecForMission() { const m = Number(mapData && mapData.jumpSurviveTimeSec); if (Number.isFinite(m) && m > 0 && m < 7200) return Math.floor(m); - const g = Number(gauntletRuntimeTimeLimitSec); - if (Number.isFinite(g) && g > 0 && g < 7200) return Math.floor(g); + const j = Number(playJumpSurviveMissionTimeSec); + if (Number.isFinite(j) && j > 0 && j < 7200) return Math.floor(j); return 60; } @@ -3994,14 +3996,23 @@ el.setAttribute('aria-hidden', 'true'); } - /** หลังกดรับหลักฐาน (embed preview) — แสดง timeup-txt กลางโต๊ะบนแคนวาส แยกจากป๊อปสรุปคะแนน + /** หลังกดรับหลักฐาน (embed preview) — แสดง timeup กลางแคนวาส (Jumper mnptfts2 = img/Jumper/result-timeup.png) แยกจากป๊อปสรุปคะแนน * @param {function():boolean} [embedResultOkFn] — ถ้าไม่ส่ง = ใช้คะแนน quiz_carry (คนตอบถูก); ส่งเมื่อ crown เพื่อเลือก complete vs gameover */ function showQuizCarryTimeupOnDeskLayer(embedResultOkFn) { const el = document.getElementById('quiz-carry-timeup-desk-layer'); if (!el) return; const img = document.getElementById('quiz-carry-timeup-txt-img'); if (img) { - img.src = BASE + '/img/quiz-carry/timeup-txt.png?v=' + String(Date.now()); + if (isJumpSurviveMissionUiMapPlay()) { + img.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/quiz-carry/timeup-txt.png?v=' + String(Date.now()); + }; + img.src = jumperAssetUrl('result-timeup.png') + '?v=' + String(Date.now()); + } else { + img.onerror = null; + img.src = BASE + '/img/quiz-carry/timeup-txt.png?v=' + String(Date.now()); + } } el.classList.remove('is-hidden'); el.setAttribute('aria-hidden', 'false'); @@ -5360,6 +5371,14 @@ playJumpSurviveJumpHeightMult = Math.round(Math.max(0.5, Math.min(4, jm)) * 100) / 100; } } + if (Object.prototype.hasOwnProperty.call(payload, 'jumpSurviveMissionTimeSec')) { + const jt = Number(payload.jumpSurviveMissionTimeSec); + if (Number.isFinite(jt) && jt > 0) { + playJumpSurviveMissionTimeSec = Math.max(10, Math.min(7200, Math.floor(jt))); + } else { + playJumpSurviveMissionTimeSec = 0; + } + } if (isStack() && stackMini) reapplyStackMiniSizingFromGlobals(); } @@ -7654,7 +7673,8 @@ const impulse = Math.sqrt(Math.max(1e-6, 2 * gFull * mult * charHpixels)); jumpV0 = -Math.min(impulse, ts * 36); } - const moveX = (Number(md.jumpSurviveMovePxPerSec) > 0 ? Number(md.jumpSurviveMovePxPerSec) : 200) * dt; + let moveX = (Number(md.jumpSurviveMovePxPerSec) > 0 ? Number(md.jumpSurviveMovePxPerSec) : 200) * dt; + if (isJumpSurviveMissionUiMapPlay()) moveX *= 1.35; if (jumpQueued && onGround) { vy = jumpV0; @@ -7670,7 +7690,7 @@ const headY0 = feetY - bh; const tryL = nx - bw; const tryR = nx + bw; - if (!jumpSurviveOverlapsSolid(md, tryL, headY0, tryR, feetY)) { + if (!jumpSurviveOverlapsSolidForHorzMove(md, tryL, headY0, tryR, feetY)) { pxc = nx; } @@ -7794,6 +7814,19 @@ return jumpSurviveOverlapsPlatforms(md, left, top, right, bottom, jumpSurvivePlatformScrollPx); } + /** + * ชนแนวนอน: objects เต็มกล่อง; แพลตฟอร์มไม่นับช่วงปลายเท้าเล็กน้อย + * (ถ้าใช้กล่องเต็มกับแพลตฟอร์ม ขณะยืนบนแพลตฟอร์มมักทับช่องแนวตั้ง — เดินซ้ายขวาถูกปฏิเสธทั้งก้อน) + */ + function jumpSurviveOverlapsSolidForHorzMove(md, left, top, right, bottom) { + if (jumpSurviveOverlapsObjectSolids(md, left, top, right, bottom)) return true; + const ts = tileSize; + const footSlop = ts * 0.16; + const platformBottom = bottom - footSlop; + if (platformBottom <= top + ts * 0.05) return false; + return jumpSurviveOverlapsPlatforms(md, left, top, right, platformBottom, jumpSurvivePlatformScrollPx); + } + /** Head/body overlaps กำแพง (objects=1) เท่านั้น — ใช้ตรวจชนฝาบนแล้วตาย */ function jumpSurviveOverlapsObjectWall(md, left, top, right, bottom) { const ts = tileSize; @@ -7849,6 +7882,20 @@ return { px: sx / n, py: sy / n }; } + /** มีแพลตฟอร์มใต้จุดเล็กน้อยข้างหน้าแนว wishDir หรือไม่ — กันบอทเดินหลุมแล้วตกจอ */ + function jumpSurviveBotHasFloorAhead(md, pxc, feetY, wishDir, scrollPx) { + if (!wishDir) return true; + const ts = tileSize; + const bw = ts * 0.34; + const probeX = pxc + wishDir * (bw + ts * 0.82); + const pad = ts * 0.24; + const left = probeX - pad; + const right = probeX + pad; + const top = feetY - ts * 0.45; + const bottom = feetY + ts * 8; + return jumpSurviveOverlapsPlatforms(md, left, top, right, bottom, scrollPx); + } + function stepJumpSurvivePreviewBots(dt, halfViewH) { if (!previewFillBots || !mapData || !isJumpSurvive()) return; const md = mapData; @@ -7868,28 +7915,55 @@ let pxc = (o.x + cw * 0.5) * ts; let feetY = (o.y + ch) * ts; - let jumpQ = false; - if (o.jumpSurviveOnGround && Date.now() >= o.jumpSurviveNextJumpAi) { - o.jumpSurviveNextJumpAi = Date.now() + 320 + Math.floor(Math.random() * 1100); - const tier = o.botTier || 'avg'; - const jp = tier === 'sharp' ? 0.5 : tier === 'weak' ? 0.25 : 0.36; - if (Math.random() < jp) jumpQ = true; - } + const camCy = jumpSurviveCamCenterY; + const mission = isJumpSurviveMissionUiMapPlay(); + const lowDanger = feetY > camCy + halfViewH - ts * 8.5; if (Date.now() >= o.jumpSurviveWishNext) { - o.jumpSurviveWishNext = Date.now() + 380 + Math.floor(Math.random() * 700); + const wMin = mission && lowDanger ? 180 : 380; + const wSpan = mission && lowDanger ? 480 : 700; + o.jumpSurviveWishNext = Date.now() + wMin + Math.floor(Math.random() * wSpan); const tier = o.botTier || 'avg'; - if (pxc < centerPx - ts * 1.8) o.jumpSurviveWishX = 1; + if (mission && lowDanger && Math.random() < 0.55) { + o.jumpSurviveWishX = 0; + } else if (pxc < centerPx - ts * 1.8) o.jumpSurviveWishX = 1; else if (pxc > centerPx + ts * 1.8) o.jumpSurviveWishX = -1; else if (tier === 'sharp') { - o.jumpSurviveWishX = Math.random() < 0.55 ? (Math.random() < 0.5 ? -1 : 1) : 0; + const roam = mission ? 0.42 : 0.55; + o.jumpSurviveWishX = Math.random() < roam ? (Math.random() < 0.5 ? -1 : 1) : 0; } else { - o.jumpSurviveWishX = Math.random() < 0.4 ? (Math.random() < 0.5 ? -1 : 1) : 0; + const roam = mission ? 0.28 : 0.4; + o.jumpSurviveWishX = Math.random() < roam ? (Math.random() < 0.5 ? -1 : 1) : 0; } } - const moveLeft = o.jumpSurviveWishX < 0; - const moveRight = o.jumpSurviveWishX > 0; + let moveLeft = o.jumpSurviveWishX < 0; + let moveRight = o.jumpSurviveWishX > 0; + let hazardJump = false; + if (o.jumpSurviveOnGround) { + const sc = jumpSurvivePlatformScrollPx; + if (moveRight && !jumpSurviveBotHasFloorAhead(md, pxc, feetY, 1, sc)) { + o.jumpSurviveWishX = Math.random() < 0.65 ? -1 : 0; + hazardJump = true; + } else if (moveLeft && !jumpSurviveBotHasFloorAhead(md, pxc, feetY, -1, sc)) { + o.jumpSurviveWishX = Math.random() < 0.65 ? 1 : 0; + hazardJump = true; + } + moveLeft = o.jumpSurviveWishX < 0; + moveRight = o.jumpSurviveWishX > 0; + } + + let jumpQ = false; + if (hazardJump) o.jumpSurviveNextJumpAi = Math.min(o.jumpSurviveNextJumpAi, Date.now()); + if (o.jumpSurviveOnGround && Date.now() >= o.jumpSurviveNextJumpAi) { + o.jumpSurviveNextJumpAi = Date.now() + 300 + Math.floor(Math.random() * (hazardJump ? 520 : 1100)); + const tier = o.botTier || 'avg'; + let jp = tier === 'sharp' ? 0.5 : tier === 'weak' ? 0.25 : 0.36; + if (mission) jp += 0.12; + if (lowDanger) jp += 0.22; + jp = Math.min(0.9, jp); + if (hazardJump || Math.random() < jp) jumpQ = true; + } const r = jumpSurvivePhysicsIntegrate(md, { dt, diff --git a/www/html/Game/public/js/version.js b/www/html/Game/public/js/version.js index dbe50aa..4eee043 100644 --- a/www/html/Game/public/js/version.js +++ b/www/html/Game/public/js/version.js @@ -1,6 +1,6 @@ // ทุกครั้งที่มีการเพิ่มหรือเปลี่ยน ให้เพิ่ม v +0.0001 // หลังแก้ค่าแล้วต้อง copy ไป path ที่ Nginx ชี้ (หรือรัน copy-frogger-files-only.sh) ถึงจะเห็นบนเว็บ -window.APP_VERSION = '0.0180'; +window.APP_VERSION = '0.0182'; document.addEventListener('DOMContentLoaded', function () { var t = document.querySelector('.version-tag'); if (t) t.textContent = 'v ' + window.APP_VERSION; diff --git a/www/html/Game/public/play.html b/www/html/Game/public/play.html index d583ab9..8368935 100644 --- a/www/html/Game/public/play.html +++ b/www/html/Game/public/play.html @@ -1899,8 +1899,8 @@ - - + + v —
คนละโหมดกับ พรมแดง (Gauntlet) · เก็บที่ /Game/data/game-timing.json ฟิลด์ jumpSurviveJumpHeightMult · ผู้เล่นได้ค่าใหม่เมื่อโหลดหน้าเล่น / sync timing จากเซิร์ฟเวอร์ · ถ้า API 404 ให้รีสตาร์ท Node ที่รัน Game/server.js
/Game/data/game-timing.json
jumpSurviveJumpHeightMult
Game/server.js
คนละโหมดกับ พรมแดง (Gauntlet) · เก็บที่ /Game/data/game-timing.json ฟิลด์ jumpSurviveJumpHeightMult และ jumpSurviveMissionTimeSec · ผู้เล่นได้ค่าใหม่เมื่อโหลดหน้าเล่น / GET /api/game-timing · ถ้า API 404 ให้รีสตาร์ท Node ที่รัน Game/server.js
jumpSurviveMissionTimeSec
GET /api/game-timing
ใช้กับฉากประเภท กระโดดให้รอด เท่านั้น · ถ้าในแมปตั้ง jumpSurviveJumpImpulse > 0 จะใช้ค่านั้นแทน (override) · English: Jump height multiplier vs character height; per-map impulse overrides.
jumpSurviveJumpImpulse
นับจากเริ่มรอบจริง (หลังนับถอยหลัง) · ถ้าในแมปตั้ง jumpSurviveTimeSec > 0 จะใช้ค่าบนแมปแทนทั้งหมด · ไม่ใช่ค่าเดียวกับ "จำกัดเวลาเกม" ของพรมแดง · English: Per-map jumpSurviveTimeSec overrides this global default.
jumpSurviveTimeSec
เก็บที่เซิร์ฟเวอร์เกม /Game/data/game-timing.json · หลังบันทึกห้องที่กำลังเล่นจะปรับความถี่ tick ทันที · ค่าจะส่งไปยังผู้เล่นผ่าน gauntlet-sync · จำกัดเวลานับจากตอนโฮสต์เริ่มเกมพรมแดง — หมดเวลาแล้วเกมจบและส่งทุกคนกลับฉากล็อบบี้บนเซิร์ฟเวอร์ · ถ้าขึ้น Not Found ให้รีสตาร์ท Nodeที่รัน Game/server.js (เช่น pm2 restart) หลัง deploy · กระโดดให้รอดตั้งที่แท็บชื่อเดียวกับโหมด
gauntlet-sync
pm2 restart
เก็บที่เซิร์ฟเวอร์เกม /Game/data/game-timing.json · หลังบันทึกห้องที่กำลังเล่นจะปรับความถี่ tick ทันที · ค่าจะส่งไปยังผู้เล่นผ่าน gauntlet-sync · จำกัดเวลานับจากตอนโฮสต์เริ่มเกมพรมแดง — หมดเวลาแล้วเกมจบและส่งทุกคนกลับฉากล็อบบี้บนเซิร์ฟเวอร์ · ถ้าขึ้น Not Found ให้รีสตาร์ท Nodeที่รัน Game/server.js (เช่น pm2 restart) หลัง deploy · เวลามินิเกมกระโดดขึ้นแท่นตั้งที่แท็บ Minigame-5
สูตร: ความสูงขึ้นสุด ≈ ค่านี้ × (ความสูงตัวเป็น pixel จากช่องบนแมป) · ถ้าในแมปตั้ง jumpSurviveJumpImpulse > 0 จะใช้ค่านั้นแทน (override)
ถ้าในแมปตั้ง jumpSurviveTimeSec > 0 จะใช้ค่าบนแมปแทน · ไม่ใช่ค่าเดียวกับพรมแดง