fixed game 3

This commit is contained in:
2026-07-01 01:19:34 +00:00
parent e1aee01a7d
commit 789be85df6
59 changed files with 444135 additions and 165 deletions
+46 -2
View File
@@ -3203,7 +3203,9 @@
.then(function (data) {
el('game-timing-tick-ms').value = String(data.gauntletTickMs != null ? data.gauntletTickMs : 220);
el('game-timing-jump-ticks').value = String(data.gauntletJumpTicks != null ? data.gauntletJumpTicks : 16);
if (el('game-timing-jump-height')) el('game-timing-jump-height').value = String(data.gauntletJumpHeightMult != null ? data.gauntletJumpHeightMult : 0.52);
el('game-timing-limit-sec').value = String(data.gauntletTimeLimitSec != null ? data.gauntletTimeLimitSec : 0);
/* troublesomeOfferSec/RollMaxSec ย้ายไปแท็บ "บทตัวป่วน" แล้ว */
if (el('game-timing-lane-urls')) el('game-timing-lane-urls').value = laneUrlsToText(data.gauntletLaneImageUrls);
if (el('game-timing-laser-top-url')) el('game-timing-laser-top-url').value = data.gauntletLaserTopUrl != null ? String(data.gauntletLaserTopUrl) : '';
if (el('game-timing-laser-bottom-url')) el('game-timing-laser-bottom-url').value = data.gauntletLaserBottomUrl != null ? String(data.gauntletLaserBottomUrl) : '';
@@ -4209,7 +4211,7 @@
if (Number.isNaN(jumpT)) jumpT = 16;
if (Number.isNaN(limS) || limS < 0) limS = 0;
tickMs = Math.max(80, Math.min(800, tickMs));
jumpT = Math.max(4, Math.min(40, jumpT));
jumpT = Math.max(1, Math.min(40, jumpT));
limS = limS <= 0 ? 0 : Math.max(10, Math.min(7200, limS));
var lwPx = el('game-timing-laser-line-width') ? parseInt(el('game-timing-laser-line-width').value, 10) : 2;
if (Number.isNaN(lwPx)) lwPx = 2;
@@ -4219,7 +4221,9 @@
.then(function (data) {
data.gauntletTickMs = tickMs;
data.gauntletJumpTicks = jumpT;
data.gauntletJumpHeightMult = (function () { var v = parseFloat(el('game-timing-jump-height') && el('game-timing-jump-height').value); if (Number.isNaN(v)) v = 0.52; return Math.max(0.2, Math.min(1.5, v)); })();
data.gauntletTimeLimitSec = limS;
/* troublesomeOfferSec/RollMaxSec ย้ายไปแท็บ "บทตัวป่วน" — ไม่แตะที่นี่ (ค่าจาก GET ส่งต่อคงเดิม) */
data.gauntletLaneImageUrls = parseLaneUrlsTextarea();
data.gauntletLaserTopUrl = el('game-timing-laser-top-url') ? String(el('game-timing-laser-top-url').value).trim() : '';
data.gauntletLaserBottomUrl = el('game-timing-laser-bottom-url') ? String(el('game-timing-laser-bottom-url').value).trim() : '';
@@ -4696,6 +4700,7 @@
if (name === 'highscore') loadHighscore();
if (name === 'achievements') loadAchievementsPanel();
if (name === 'vote-timing') loadVoteTimingPanel();
if (name === 'troublesome') loadTroublesomePanel();
if (name === 'test-mode') { loadForcedMinigamePanel(); loadSpecialCardByMapPanel(); loadTroublesomeForcePanel(); }
}
@@ -5234,6 +5239,45 @@
});
})();
/* ===== บทตัวป่วน (แยกจากหมวด Minigame-2) — troublesomeRollMaxSec + troublesomeOfferSec ===== */
function loadTroublesomePanel() {
gameTimingFetch('GET')
.then(function (data) {
var rs = (data && data.troublesomeRollMaxSec != null) ? Number(data.troublesomeRollMaxSec) : 8;
var os = (data && data.troublesomeOfferSec != null) ? Number(data.troublesomeOfferSec) : 15;
if (el('troublesome-roll-sec')) el('troublesome-roll-sec').value = String(Number.isFinite(rs) ? rs : 8);
if (el('troublesome-offer-sec')) el('troublesome-offer-sec').value = String(Number.isFinite(os) ? os : 15);
setMsg('troublesome-msg', '', '');
})
.catch(function (e) { setMsg('troublesome-msg', (e.message || 'โหลดไม่ได้') + ' — เช็ค /Game/api/game-timing', 'error'); });
}
(function bindTroublesome() {
var btn = el('btn-troublesome-save');
if (!btn) return;
btn.addEventListener('click', function () {
var rs = Math.max(1, Math.min(30, parseInt((el('troublesome-roll-sec') || {}).value, 10) || 8));
var os = Math.max(5, Math.min(120, parseInt((el('troublesome-offer-sec') || {}).value, 10) || 15));
btn.disabled = true;
gameTimingFetch('GET')
.then(function (data) {
data.troublesomeRollMaxSec = rs;
data.troublesomeOfferSec = os;
return gameTimingFetch('PUT', data);
})
.then(function () { return gameTimingFetch('GET'); })
.then(function (fresh) {
var r = (fresh && fresh.troublesomeRollMaxSec != null) ? fresh.troublesomeRollMaxSec : rs;
var o = (fresh && fresh.troublesomeOfferSec != null) ? fresh.troublesomeOfferSec : os;
if (el('troublesome-roll-sec')) el('troublesome-roll-sec').value = String(r);
if (el('troublesome-offer-sec')) el('troublesome-offer-sec').value = String(o);
setMsg('troublesome-msg', 'บันทึกแล้ว — เวลาเสนอ ' + r + 's · เวลารับ ' + o + 's · ผู้เล่นต้องรีเฟรช/เข้าห้องใหม่', 'ok');
})
.catch(function (e) { setMsg('troublesome-msg', (e.message || 'บันทึกไม่ได้') + ' — เช็ค Node + /Game/api/game-timing', 'error'); })
.then(function () { btn.disabled = false; });
});
})();
/* ===== Achievements admin ===== */
var achvCatalog = [];
var achvCatalogById = {};
@@ -5595,7 +5639,7 @@
try {
want = new URL(window.location.href).searchParams.get('tab');
} catch (e) { /* ignore */ }
var allowed = { 'change-password': true, oauth: true, 'map-editor': true, quiz: true, 'special-quiz': true, 'vote-timing': true, 'quiz-carry': true, 'quiz-battle': true, 'jump-survive': true, 'space-shooter': true, 'mega-virus': true, 'game-timing': true, 'stack-game': true, characters: true, accounts: true, admins: true };
var allowed = { 'change-password': true, oauth: true, 'map-editor': true, quiz: true, 'special-quiz': true, 'vote-timing': true, troublesome: true, 'quiz-carry': true, 'quiz-battle': true, 'jump-survive': true, 'space-shooter': true, 'mega-virus': true, 'game-timing': true, 'stack-game': true, characters: true, accounts: true, admins: true };
if (!want || !allowed[want]) return 'oauth';
if (want === 'admins' && (!admin || admin.role !== 'super')) return 'oauth';
return want;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+20 -2
View File
@@ -117,6 +117,7 @@
<div class="admin-tab-sep" aria-hidden="true"><span>คำถาม &amp; การ์ดพิเศษ</span></div>
<button type="button" class="tab" data-tab="special-quiz" role="tab" id="tab-special-quiz" aria-controls="tab-panel-special-quiz"><span class="tab-label">ชุดคำถามพิเศษ</span><span class="tab-desc">15 ชุด (Level × Case) · การ์ดรางวัล</span></button>
<button type="button" class="tab" data-tab="vote-timing" role="tab" id="tab-vote-timing" aria-controls="tab-panel-vote-timing"><span class="tab-label">เวลาโหวต</span><span class="tab-desc">Silence/Ban · ชี้ตัวคนร้าย</span></button>
<button type="button" class="tab" data-tab="troublesome" role="tab" id="tab-troublesome" aria-controls="tab-panel-troublesome"><span class="tab-label">บทตัวป่วน</span><span class="tab-desc">เวลาเสนอ · เวลารับ</span></button>
<button type="button" class="tab" data-tab="evidence-cards" role="tab" id="tab-evidence-cards" aria-controls="tab-panel-evidence-cards"><span class="tab-label">การ์ดหลักฐาน</span><span class="tab-desc">3 คดี × ผู้ต้องสงสัย × 3 ใบ · รูป/ข้อความ</span></button>
<button type="button" class="tab" data-tab="case-media" role="tab" id="tab-case-media" aria-controls="tab-panel-case-media"><span class="tab-label">รูปคดี &amp; Cutscene</span><span class="tab-desc">15 คดี · รูปเลือกคดี/รายละเอียด/เรื่องราว 2 รูป</span></button>
@@ -831,7 +832,8 @@
<legend>พารามิเตอร์</legend>
<div class="form-grid form-inline quiz-timing-grid">
<label>ความถี่ tick (มิลลิวินาที) <input type="number" id="game-timing-tick-ms" min="80" max="800" step="10" value="220"></label>
<label>ระยะกระโดด (tick) <input type="number" id="game-timing-jump-ticks" min="4" max="40" step="1" value="16"></label>
<label>เวลากระโดดขึ้น-ลง (tick) <input type="number" id="game-timing-jump-ticks" min="1" max="40" step="1" value="16" title="จำนวน tick ที่ตัวละครลอยกลางอากาศตอนกระโดด (MG2) — มาก = กระโดดช้า/ลอยนาน · น้อย = กระโดดเร็ว (ต่ำสุด 1)"></label>
<label>ความสูงกระโดด MG2 (×tile) <input type="number" id="game-timing-jump-height" min="0.2" max="1.5" step="0.02" value="0.52" title="ความสูงสูงสุดตอนกระโดดของ MG2 (วิ่งหลบ) เป็นเท่าของความสูง 1 ช่อง — มาก = กระโดดสูงขึ้น"></label>
<label>จำกัดเวลาเกม (วินาที) <input type="number" id="game-timing-limit-sec" min="0" max="7200" step="10" value="0" title="0 = เล่นได้ไม่จำกัดเวลา"></label>
</div>
</fieldset>
@@ -1153,6 +1155,22 @@
<p id="vote-timing-msg" class="msg" role="status"></p>
</section>
<section id="tab-panel-troublesome" class="tab-panel card" hidden role="tabpanel" aria-labelledby="tab-troublesome">
<h2>บทตัวป่วน</h2>
<p class="muted">ตั้งเวลาเกี่ยวกับการเสนอ/รับ "บทตัวป่วน" หลังจบคดี · มีผลเมื่อผู้เล่นรีเฟรช/เข้าห้องใหม่ · ปุ่ม <strong>บังคับข้ามเงื่อนไข ≥4 คน</strong> อยู่แท็บ <strong>Test Mode</strong></p>
<form id="form-troublesome" class="form-grid form-inline" autocomplete="off">
<label>เวลาเสนอบทตัวป่วน
<input type="number" id="troublesome-roll-sec" min="1" max="30" step="1" value="8"> วินาที
</label>
<label>เวลารับตัวป่วน
<input type="number" id="troublesome-offer-sec" min="5" max="120" step="1" value="15"> วินาที
</label>
<button type="button" class="btn btn-primary" id="btn-troublesome-save">บันทึก</button>
</form>
<p class="muted" style="font-size:0.82rem;line-height:1.5"><strong>เวลาเสนอ</strong> = เวลาสูงสุดที่ระบบรอให้ทุกคนพร้อม ก่อนสุ่มเลือกผู้รับบท (ถ้าพร้อมก่อน เสนอเร็วกว่านี้)<br><strong>เวลารับ</strong> = นับถอยหลังให้ผู้ถูกเลือกกดรับ/ปฏิเสธบทตัวป่วน</p>
<p id="troublesome-msg" class="msg" role="status"></p>
</section>
<section id="tab-panel-admins" class="tab-panel card" hidden role="tabpanel" aria-labelledby="tab-admins">
<h2>บัญชีแอดมิน</h2>
<p class="muted">เฉพาะ <strong>super admin</strong> เท่านั้นที่เพิ่ม/ลบแอดมินได้</p>
@@ -1289,7 +1307,7 @@
</div>
</main>
</div>
<script src="admin.js?v=87"></script>
<script src="admin.js?v=91"></script>
<script>
/* toggle: เปิด/ปิด HUD โหลด LobbyA (Iron Man) ให้ "ผู้เล่นทั่วไป" เห็น (เก็บใน game-timing.json: lobbyDebugHud)
— dev เปิดเครื่องตัวเองได้เสมอด้วย ?dbg=1 หรือ localStorage.lobbyDbg='1' โดยไม่ต้องใช้ปุ่มนี้ */
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+40 -15
View File
@@ -51,9 +51,9 @@
"providerUserId": "p_1775109142385_wq7wfy1p32j",
"notes": "auto: player-coins",
"blocked": false,
"coins": 2564,
"coins": 2642,
"createdAt": "2026-04-02T05:52:21+00:00",
"updatedAt": "2026-06-30T03:46:22+00:00",
"updatedAt": "2026-07-01T00:43:02+00:00",
"daily": {
"anchorMs": 1781197200000,
"claimedDays": [
@@ -67,13 +67,13 @@
],
"lockUntilMs": 0
},
"score": 490,
"score": 568,
"scoreByCase": {
"1": 26,
"10": 744,
"10": 782,
"8": 296,
"11": 20,
"13": 324,
"13": 344,
"14": 30,
"9": 40,
"12": 466,
@@ -83,7 +83,7 @@
"7": 6,
"3": 150,
"6": 44,
"2": 146
"2": 166
},
"lbName": "Q",
"achievements": {
@@ -904,16 +904,18 @@
"providerUserId": "p_1782327780535_twosoa33hsr",
"notes": "auto: player-coins",
"blocked": false,
"coins": 30,
"coins": 70,
"createdAt": "2026-06-24T19:03:00+00:00",
"updatedAt": "2026-06-24T19:12:57+00:00",
"score": 30,
"updatedAt": "2026-07-01T01:03:57+00:00",
"score": 70,
"scoreByCase": {
"10": 30
"10": 40,
"8": 20,
"3": 10
},
"lbName": "ผู้เล่น",
"achievements": {
"d1_minigame_solver": 3
"d1_minigame_solver": 7
}
},
{
@@ -924,16 +926,16 @@
"providerUserId": "p_1782376007927_lpt9m4xge9",
"notes": "auto: player-coins",
"blocked": false,
"coins": 250,
"coins": 268,
"createdAt": "2026-06-25T08:26:49+00:00",
"updatedAt": "2026-06-30T03:46:22+00:00",
"score": 250,
"updatedAt": "2026-06-30T05:05:53+00:00",
"score": 268,
"scoreByCase": {
"12": 78,
"8": 20,
"3": 20,
"2": 80,
"10": 52
"10": 70
},
"lbName": "MONE",
"achievements": {
@@ -1023,6 +1025,29 @@
"achievements": {
"d1_minigame_solver": 2
}
},
{
"id": "b8650c36f3daa1d07eeb45cc",
"email": "",
"displayName": "MONE",
"loginType": "guest",
"providerUserId": "p_1782806759319_y8m8t8zug7",
"notes": "auto: player-coins",
"blocked": false,
"coins": 30,
"createdAt": "2026-06-30T08:05:59+00:00",
"updatedAt": "2026-06-30T08:23:55+00:00",
"score": 30,
"scoreByCase": {
"8": 10,
"5": 20
},
"lbName": "MONE",
"achievements": {
"d1_minigame_solver": 3,
"a1_first_deduction": 1,
"a5_truth_hunter": 1
}
}
]
}
+7 -4
View File
@@ -1,7 +1,9 @@
{
"gauntletTickMs": 220,
"gauntletJumpTicks": 6,
"gauntletJumpTicks": 2,
"gauntletJumpHeightMult": 1,
"gauntletTimeLimitSec": 0,
"troublesomeOfferSec": 15,
"gauntletLaneImageUrls": [
"/Game/img/gauntlet-assets/gauntlet-077c293962b8fe07.png",
"/Game/img/gauntlet-assets/gauntlet-67a585790f67fe67.png",
@@ -95,12 +97,13 @@
"balloonBossPlayerBalloonFallbackUrl": "/Game/img/MegaVirus/Artboard%209.png",
"balloonBossBalloonsPerPlayer": 3,
"forcedMinigameKeys": [
"quiz_carry",
"balloon_boss",
"space_shooter"
"gauntlet",
"stack",
"quiz_carry"
],
"testSpecialCardByMap": {},
"troublesomeForceOffer": false,
"troublesomeRollMaxSec": 15,
"specialQuizIconExpireSec": 20,
"pickVoteSec": 25,
"trialVoteSec": 30,
@@ -0,0 +1,110 @@
{
"gauntletTickMs": 220,
"gauntletJumpTicks": 6,
"gauntletTimeLimitSec": 0,
"troublesomeOfferSec": 15,
"gauntletLaneImageUrls": [
"/Game/img/gauntlet-assets/gauntlet-077c293962b8fe07.png",
"/Game/img/gauntlet-assets/gauntlet-67a585790f67fe67.png",
"/Game/img/gauntlet-assets/gauntlet-c4d4482e2a60eac9.png",
"/Game/img/gauntlet-assets/gauntlet-6aa244ab734b12e4.png"
],
"gauntletLaserTopUrl": "",
"gauntletLaserBottomUrl": "",
"gauntletLaserLineUrl": "",
"gauntletLaserFillColor": "rgba(140,230,255,0.42)",
"gauntletLaserStrokeColor": "rgba(255,220,255,0.9)",
"gauntletLaserLineWidthPx": 2,
"stackSwingHz": 0.1,
"stackBlockWidthTiles": 3.2,
"stackTowerMissionTimeSec": 90,
"stackTeamMissesMax": 3,
"stackTowerProgressBlocks": 50,
"stackBlockNormalImageUrls": [
"/Game/img/TowerBlock/Block-Code-1-red.png",
"/Game/img/TowerBlock/Block-Code-2-orange.png",
"/Game/img/TowerBlock/Block-Code-3-Yellow.png",
"/Game/img/TowerBlock/Block-Code-4-Green.png",
"/Game/img/TowerBlock/Block-Code-5-Blue.png",
"/Game/img/TowerBlock/Block-Code-6-Navy.png",
"",
""
],
"stackBlockHeavyImageUrls": [
"/Game/img/TowerBlock/Block-Code-1-red-2.png",
"/Game/img/TowerBlock/Block-Code-2-orange-2.png",
"/Game/img/TowerBlock/Block-Code-3-Yellow-2.png",
"/Game/img/TowerBlock/Block-Code-4-Green-2.png",
"/Game/img/TowerBlock/Block-Code-5-Blue-2.png",
"/Game/img/TowerBlock/Block-Code-6-Navy-2.png",
"",
""
],
"stackHeavyBlockPercent": 35,
"jumpSurviveJumpHeightMult": 1.7,
"jumpSurviveMissionTimeSec": 0,
"jumpSurvivePlatformTiles": [
{
"url": "/Game/img/Jumper/platfrom-1.png",
"w": 1392,
"h": 60
},
{
"url": "/Game/img/Jumper/platfrom-2.png",
"w": 389,
"h": 36
},
{
"url": "/Game/img/Jumper/platfrom-3.png",
"w": 212,
"h": 33
}
],
"spaceShooterMissionTimeSec": 0,
"spaceShooterShipImageUrls": [
"/Game/img/ViolentCrime/Rocket-1.png",
"/Game/img/ViolentCrime/Rocket-2.png",
"/Game/img/ViolentCrime/Rocket-3.png",
"/Game/img/ViolentCrime/Rocket-4.png",
"/Game/img/ViolentCrime/Rocket-5.png",
"/Game/img/ViolentCrime/Rocket-6.png"
],
"spaceShooterAsteroidSpriteUrls": [
"/Game/img/ViolentCrime/Meteo-1.png",
"/Game/img/ViolentCrime/Meteo-2.png",
"/Game/img/ViolentCrime/Meteo-3.png"
],
"spaceShooterAsteroidExplodeFrameMs": 60,
"spaceShooterAsteroidIntervalMs": 300,
"spaceShooterShipDamageOverlayUrls": [
"/Game/img/ViolentCrime/Rocket-broke-1.png",
"/Game/img/ViolentCrime/Rocket-broke-2.png",
"/Game/img/ViolentCrime/Rocket-broke-3.png"
],
"balloonBossMissionTimeSec": 60,
"balloonBossBossImageUrl": "/Game/img/MegaVirus/boss.png",
"balloonBossPlayerBalloonImageUrls": [
"/Game/img/MegaVirus/balloon-1.png",
"/Game/img/MegaVirus/balloon-2.png",
"/Game/img/MegaVirus/balloon-3.png",
"/Game/img/MegaVirus/balloon-4.png",
"/Game/img/MegaVirus/balloon-5.png",
"/Game/img/MegaVirus/balloon-6.png",
"",
""
],
"balloonBossPlayerBalloonFallbackUrl": "/Game/img/MegaVirus/Artboard%209.png",
"balloonBossBalloonsPerPlayer": 3,
"forcedMinigameKeys": [
"space_shooter",
"gauntlet",
"stack"
],
"testSpecialCardByMap": {},
"troublesomeForceOffer": false,
"specialQuizIconExpireSec": 20,
"pickVoteSec": 25,
"trialVoteSec": 30,
"lobbyDebugHud": false,
"lobbyDebugHudNames": []
}
@@ -0,0 +1,110 @@
{
"gauntletTickMs": 220,
"gauntletJumpTicks": 4,
"gauntletTimeLimitSec": 0,
"troublesomeOfferSec": 15,
"gauntletLaneImageUrls": [
"/Game/img/gauntlet-assets/gauntlet-077c293962b8fe07.png",
"/Game/img/gauntlet-assets/gauntlet-67a585790f67fe67.png",
"/Game/img/gauntlet-assets/gauntlet-c4d4482e2a60eac9.png",
"/Game/img/gauntlet-assets/gauntlet-6aa244ab734b12e4.png"
],
"gauntletLaserTopUrl": "",
"gauntletLaserBottomUrl": "",
"gauntletLaserLineUrl": "",
"gauntletLaserFillColor": "rgba(140,230,255,0.42)",
"gauntletLaserStrokeColor": "rgba(255,220,255,0.9)",
"gauntletLaserLineWidthPx": 2,
"stackSwingHz": 0.1,
"stackBlockWidthTiles": 3.2,
"stackTowerMissionTimeSec": 90,
"stackTeamMissesMax": 3,
"stackTowerProgressBlocks": 50,
"stackBlockNormalImageUrls": [
"/Game/img/TowerBlock/Block-Code-1-red.png",
"/Game/img/TowerBlock/Block-Code-2-orange.png",
"/Game/img/TowerBlock/Block-Code-3-Yellow.png",
"/Game/img/TowerBlock/Block-Code-4-Green.png",
"/Game/img/TowerBlock/Block-Code-5-Blue.png",
"/Game/img/TowerBlock/Block-Code-6-Navy.png",
"",
""
],
"stackBlockHeavyImageUrls": [
"/Game/img/TowerBlock/Block-Code-1-red-2.png",
"/Game/img/TowerBlock/Block-Code-2-orange-2.png",
"/Game/img/TowerBlock/Block-Code-3-Yellow-2.png",
"/Game/img/TowerBlock/Block-Code-4-Green-2.png",
"/Game/img/TowerBlock/Block-Code-5-Blue-2.png",
"/Game/img/TowerBlock/Block-Code-6-Navy-2.png",
"",
""
],
"stackHeavyBlockPercent": 35,
"jumpSurviveJumpHeightMult": 1.7,
"jumpSurviveMissionTimeSec": 0,
"jumpSurvivePlatformTiles": [
{
"url": "/Game/img/Jumper/platfrom-1.png",
"w": 1392,
"h": 60
},
{
"url": "/Game/img/Jumper/platfrom-2.png",
"w": 389,
"h": 36
},
{
"url": "/Game/img/Jumper/platfrom-3.png",
"w": 212,
"h": 33
}
],
"spaceShooterMissionTimeSec": 0,
"spaceShooterShipImageUrls": [
"/Game/img/ViolentCrime/Rocket-1.png",
"/Game/img/ViolentCrime/Rocket-2.png",
"/Game/img/ViolentCrime/Rocket-3.png",
"/Game/img/ViolentCrime/Rocket-4.png",
"/Game/img/ViolentCrime/Rocket-5.png",
"/Game/img/ViolentCrime/Rocket-6.png"
],
"spaceShooterAsteroidSpriteUrls": [
"/Game/img/ViolentCrime/Meteo-1.png",
"/Game/img/ViolentCrime/Meteo-2.png",
"/Game/img/ViolentCrime/Meteo-3.png"
],
"spaceShooterAsteroidExplodeFrameMs": 60,
"spaceShooterAsteroidIntervalMs": 300,
"spaceShooterShipDamageOverlayUrls": [
"/Game/img/ViolentCrime/Rocket-broke-1.png",
"/Game/img/ViolentCrime/Rocket-broke-2.png",
"/Game/img/ViolentCrime/Rocket-broke-3.png"
],
"balloonBossMissionTimeSec": 60,
"balloonBossBossImageUrl": "/Game/img/MegaVirus/boss.png",
"balloonBossPlayerBalloonImageUrls": [
"/Game/img/MegaVirus/balloon-1.png",
"/Game/img/MegaVirus/balloon-2.png",
"/Game/img/MegaVirus/balloon-3.png",
"/Game/img/MegaVirus/balloon-4.png",
"/Game/img/MegaVirus/balloon-5.png",
"/Game/img/MegaVirus/balloon-6.png",
"",
""
],
"balloonBossPlayerBalloonFallbackUrl": "/Game/img/MegaVirus/Artboard%209.png",
"balloonBossBalloonsPerPlayer": 3,
"forcedMinigameKeys": [
"quiz_carry",
"stack",
"jump_survive"
],
"testSpecialCardByMap": {},
"troublesomeForceOffer": false,
"specialQuizIconExpireSec": 20,
"pickVoteSec": 25,
"trialVoteSec": 30,
"lobbyDebugHud": false,
"lobbyDebugHudNames": []
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -87,6 +87,10 @@
<label for="gauntletTimeLimitSec">gauntletTimeLimitSec (0 = ไม่จำกัด)</label>
<input type="number" id="gauntletTimeLimitSec" min="0" max="7200" step="1">
</div>
<div class="row">
<label for="troublesomeOfferSec">troublesomeOfferSec (เวลารับตัวป่วน วินาที)</label>
<input type="number" id="troublesomeOfferSec" min="5" max="120" step="1">
</div>
</div>
<div id="panel-stack" class="panel" role="tabpanel" aria-labelledby="tab-stack" hidden>
@@ -188,6 +192,7 @@
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;
document.getElementById('troublesomeOfferSec').value = timing.troublesomeOfferSec != null ? timing.troublesomeOfferSec : 15;
document.getElementById('stackSwingHz').value = timing.stackSwingHz != null ? timing.stackSwingHz : 0.1;
const sb = document.getElementById('stackBlockWidthTiles');
sb.value = timing.stackBlockWidthTiles != null && timing.stackBlockWidthTiles !== '' ? timing.stackBlockWidthTiles : '';
@@ -214,6 +219,7 @@
gauntletTickMs: parseInt(document.getElementById('gauntletTickMs').value, 10),
gauntletJumpTicks: parseInt(document.getElementById('gauntletJumpTicks').value, 10),
gauntletTimeLimitSec: parseInt(document.getElementById('gauntletTimeLimitSec').value, 10),
troublesomeOfferSec: parseInt(document.getElementById('troublesomeOfferSec').value, 10),
stackSwingHz: parseFloat(document.getElementById('stackSwingHz').value, 10),
stackBlockWidthTiles: (function () {
const v = document.getElementById('stackBlockWidthTiles').value.trim();
@@ -0,0 +1,272 @@
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ตั้งค่าเวลาเกม — Game</title>
<link rel="stylesheet" href="css/style.css">
<style>
body { margin: 0; min-height: 100dvh; background: #1a1b26; color: #c0caf5; font-family: system-ui, "Segoe UI", "Kanit", sans-serif; }
.admin-shell { max-width: 920px; margin: 0 auto; padding: 1rem 1.25rem 2rem; }
.admin-shell h1 { font-size: 1.2rem; color: #7aa2f7; margin: 0 0 0.35rem; }
.admin-shell > p.sub { font-size: 0.88rem; color: #a9b1d6; margin: 0 0 1rem; }
nav.admin-tabs { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-bottom: 1rem; border-bottom: 1px solid #414868; padding-bottom: 0.5rem; }
nav.admin-tabs button {
padding: 0.5rem 0.85rem; border-radius: 8px; border: 1px solid #414868; background: #24283b; color: #a9b1d6;
font-size: 0.82rem; font-weight: 600; cursor: pointer;
}
nav.admin-tabs button:hover { background: #414868; color: #c0caf5; }
nav.admin-tabs button[aria-selected="true"] { background: #1a1b26; color: #7aa2f7; border-color: #7aa2f7; }
.panel { display: none; background: #24283b; border: 1px solid #414868; border-radius: 10px; padding: 1rem 1.15rem; }
.panel.active { display: block; }
.panel h2 { font-size: 1rem; color: #c0caf5; margin: 0 0 0.75rem; }
label { display: block; font-size: 0.85rem; color: #a9b1d6; margin-bottom: 0.35rem; }
input[type="number"] {
width: 100%; max-width: 220px; padding: 0.5rem 0.65rem; border-radius: 8px; border: 1px solid #414868;
background: #1a1b26; color: #c0caf5; box-sizing: border-box;
}
.hint { font-size: 0.78rem; color: #565f89; margin: 0.35rem 0 0.85rem; line-height: 1.45; }
.row { margin-bottom: 1rem; }
.actions { margin-top: 1.25rem; display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; }
.actions button.primary { padding: 0.55rem 1.1rem; border: none; border-radius: 8px; background: #7aa2f7; color: #1a1b26; font-weight: 700; cursor: pointer; }
.actions button.primary:hover { background: #89b4fa; }
#status { font-size: 0.85rem; margin-top: 0.75rem; }
#status.ok { color: #9ece6a; }
#status.err { color: #f7768e; }
a.back { color: #7aa2f7; font-size: 0.88rem; }
</style>
</head>
<body>
<div class="admin-shell">
<p><a class="back" href="index.html">← กลับหน้าแรก</a></p>
<h1>ตั้งค่าเวลา / พารามิเตอร์เกม</h1>
<p class="sub">บันทึกลง <code>data/game-timing.json</code> ผ่าน <code>PUT /Game/api/game-timing</code> — โหลดหน้าเล่นจะดึงค่าอัตโนมัติ</p>
<nav class="admin-tabs" role="tablist" aria-label="เมนูหลัก Admin">
<button type="button" role="tab" id="tab-jump" aria-selected="true" aria-controls="panel-jump" data-panel="panel-jump">กระโดดให้รอด</button>
<button type="button" role="tab" id="tab-shooter" aria-selected="false" aria-controls="panel-shooter" data-panel="panel-shooter">ยิงอุกาบาต · Violent Crime</button>
<button type="button" role="tab" id="tab-gauntlet" aria-selected="false" aria-controls="panel-gauntlet" data-panel="panel-gauntlet">พรมแดง · tick &amp; กระโดด</button>
<button type="button" role="tab" id="tab-stack" aria-selected="false" aria-controls="panel-stack" data-panel="panel-stack">Stack · สวิง</button>
<button type="button" role="tab" id="tab-test" aria-selected="false" aria-controls="panel-test" data-panel="panel-test">เทสต์ · บังคับมินิเกม</button>
</nav>
<div id="panel-jump" class="panel active" role="tabpanel" aria-labelledby="tab-jump">
<h2>กระโดดให้รอด — ความสูงกระโดด</h2>
<div class="row">
<label for="jumpSurviveJumpHeightMult">ทวีคูณความสูงกระโดด × ความสูงตัวละคร (1 = เท่าตัว · ค่าเริ่ม 1.5 = สูงกว่าตัวครึ่งหนึ่ง)</label>
<input type="number" id="jumpSurviveJumpHeightMult" min="0.5" max="4" step="0.05" value="1.5">
<p class="hint">สูตร: ความสูงขึ้นสุด ≈ ค่านี้ × (ความสูงตัวเป็น pixel จากช่องบนแมป) · ถ้าในแมปตั้ง <code>jumpSurviveJumpImpulse</code> &gt; 0 จะใช้ค่านั้นแทน (override)</p>
</div>
<div class="row">
<label for="jumpSurviveMissionTimeSec">jumpSurviveMissionTimeSec — เวลารอบมินิเกมกระโดดขึ้นแท่น (วินาที · 0 = เกมใช้ค่าเริ่ม 60)</label>
<input type="number" id="jumpSurviveMissionTimeSec" min="0" max="7200" step="5" value="0">
<p class="hint">ถ้าในแมปตั้ง <code>jumpSurviveTimeSec</code> &gt; 0 จะใช้ค่าบนแมปแทน · ไม่ใช่ค่าเดียวกับพรมแดง</p>
</div>
</div>
<div id="panel-shooter" class="panel" role="tabpanel" aria-labelledby="tab-shooter" hidden>
<h2>ยิงยานอวกาศ (space_shooter)</h2>
<div class="row">
<label for="spaceShooterMissionTimeSec">spaceShooterMissionTimeSec — เวลารอบ (วินาที · 0 = เกมใช้ค่าเริ่ม 90)</label>
<input type="number" id="spaceShooterMissionTimeSec" min="0" max="7200" step="5" value="0">
<p class="hint">ถ้าในแมปตั้ง <code>spaceShooterTimeSec</code> &gt; 0 จะใช้ค่าบนแมปแทน · ตั้งเป็น 0 บนแมป = ไม่จับเวลา</p>
</div>
</div>
<div id="panel-gauntlet" class="panel" role="tabpanel" aria-labelledby="tab-gauntlet" hidden>
<h2>พรมแดง (Gauntlet)</h2>
<div class="row">
<label for="gauntletTickMs">gauntletTickMs</label>
<input type="number" id="gauntletTickMs" min="80" max="800" step="1">
</div>
<div class="row">
<label for="gauntletJumpTicks">gauntletJumpTicks</label>
<input type="number" id="gauntletJumpTicks" min="4" max="40" step="1">
</div>
<div class="row">
<label for="gauntletTimeLimitSec">gauntletTimeLimitSec (0 = ไม่จำกัด)</label>
<input type="number" id="gauntletTimeLimitSec" min="0" max="7200" step="1">
</div>
</div>
<div id="panel-stack" class="panel" role="tabpanel" aria-labelledby="tab-stack" hidden>
<h2>Stack</h2>
<div class="row">
<label for="stackSwingHz">stackSwingHz (รอบ/วินาที)</label>
<input type="number" id="stackSwingHz" min="0.08" max="2.8" step="0.01">
</div>
<div class="row">
<label for="stackBlockWidthTiles">stackBlockWidthTiles (ว่าง = คำนวณจากแมป)</label>
<input type="number" id="stackBlockWidthTiles" min="0.85" max="3.2" step="0.05" placeholder="ว่าง">
</div>
</div>
<div id="panel-test" class="panel" role="tabpanel" aria-labelledby="tab-test" hidden>
<h2>เทสต์ — บังคับมินิเกมที่ออก</h2>
<p class="hint">เลือกเกมต่อการ์ดผู้ต้องสงสัย 3 ใบ — ช่องที่เลือก "— สุ่ม —" จะสุ่มจาก 7 เกมเหมือนเดิม · ตั้งครบแล้วเริ่มคดีใหม่ (LobbyA→LobbyB) เพื่อเทสต์ตอนเล่นจริง</p>
<div class="row">
<label for="forcedMg1">การ์ดใบที่ 1</label>
<select id="forcedMg1">
<option value="">— สุ่ม —</option>
<option value="quiz">Minigame-1 จริงหรือไม่</option>
<option value="gauntlet">Minigame-2 วิ่งหลบสิ่งกีดขวาง</option>
<option value="stack">Minigame-3 Tower block</option>
<option value="quiz_carry">Minigame-4 คำศัพท์ในกระบวนการยุติธรรม</option>
<option value="jump_survive">Minigame-5 กระโดดขึ้นแท่น</option>
<option value="space_shooter">Minigame-6 ยิงอุกาบาต Violent Crime</option>
<option value="balloon_boss">Minigame-7 ยิง Mega Virus</option>
</select>
</div>
<div class="row">
<label for="forcedMg2">การ์ดใบที่ 2</label>
<select id="forcedMg2">
<option value="">— สุ่ม —</option>
<option value="quiz">Minigame-1 จริงหรือไม่</option>
<option value="gauntlet">Minigame-2 วิ่งหลบสิ่งกีดขวาง</option>
<option value="stack">Minigame-3 Tower block</option>
<option value="quiz_carry">Minigame-4 คำศัพท์ในกระบวนการยุติธรรม</option>
<option value="jump_survive">Minigame-5 กระโดดขึ้นแท่น</option>
<option value="space_shooter">Minigame-6 ยิงอุกาบาต Violent Crime</option>
<option value="balloon_boss">Minigame-7 ยิง Mega Virus</option>
</select>
</div>
<div class="row">
<label for="forcedMg3">การ์ดใบที่ 3</label>
<select id="forcedMg3">
<option value="">— สุ่ม —</option>
<option value="quiz">Minigame-1 จริงหรือไม่</option>
<option value="gauntlet">Minigame-2 วิ่งหลบสิ่งกีดขวาง</option>
<option value="stack">Minigame-3 Tower block</option>
<option value="quiz_carry">Minigame-4 คำศัพท์ในกระบวนการยุติธรรม</option>
<option value="jump_survive">Minigame-5 กระโดดขึ้นแท่น</option>
<option value="space_shooter">Minigame-6 ยิงอุกาบาต Violent Crime</option>
<option value="balloon_boss">Minigame-7 ยิง Mega Virus</option>
</select>
</div>
</div>
<div class="actions">
<button type="button" class="primary" id="btn-save">บันทึกทั้งหมด</button>
</div>
<p id="status" class="hint"></p>
</div>
<script>
(function () {
const BASE = window.location.pathname.replace(/\/[^/]*$/, '') || '';
const tabs = document.querySelectorAll('nav.admin-tabs [role="tab"]');
const panels = {
'panel-jump': document.getElementById('panel-jump'),
'panel-shooter': document.getElementById('panel-shooter'),
'panel-gauntlet': document.getElementById('panel-gauntlet'),
'panel-stack': document.getElementById('panel-stack'),
'panel-test': document.getElementById('panel-test'),
};
tabs.forEach((btn) => {
btn.addEventListener('click', () => {
const id = btn.getAttribute('data-panel');
tabs.forEach((b) => {
b.setAttribute('aria-selected', b === btn ? 'true' : 'false');
});
Object.keys(panels).forEach((k) => {
const p = panels[k];
const on = k === id;
p.classList.toggle('active', on);
p.hidden = !on;
});
});
});
let timing = {};
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;
const sms = Number(timing.spaceShooterMissionTimeSec);
document.getElementById('spaceShooterMissionTimeSec').value = Number.isFinite(sms) && sms > 0 ? Math.floor(sms) : 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;
document.getElementById('stackSwingHz').value = timing.stackSwingHz != null ? timing.stackSwingHz : 0.1;
const sb = document.getElementById('stackBlockWidthTiles');
sb.value = timing.stackBlockWidthTiles != null && timing.stackBlockWidthTiles !== '' ? timing.stackBlockWidthTiles : '';
const fks = Array.isArray(timing.forcedMinigameKeys) ? timing.forcedMinigameKeys : [];
for (let i = 1; i <= 3; i++) {
const s = document.getElementById('forcedMg' + i);
if (s) s.value = fks[i - 1] || '';
}
}
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));
let sst = parseInt(document.getElementById('spaceShooterMissionTimeSec').value, 10);
if (Number.isNaN(sst) || sst < 0) sst = 0;
sst = sst <= 0 ? 0 : Math.max(10, Math.min(7200, sst));
return {
...timing,
jumpSurviveJumpHeightMult: Number.isFinite(jm) ? jm : 1.5,
jumpSurviveMissionTimeSec: jmt,
spaceShooterMissionTimeSec: sst,
gauntletTickMs: parseInt(document.getElementById('gauntletTickMs').value, 10),
gauntletJumpTicks: parseInt(document.getElementById('gauntletJumpTicks').value, 10),
gauntletTimeLimitSec: parseInt(document.getElementById('gauntletTimeLimitSec').value, 10),
stackSwingHz: parseFloat(document.getElementById('stackSwingHz').value, 10),
stackBlockWidthTiles: (function () {
const v = document.getElementById('stackBlockWidthTiles').value.trim();
return v === '' ? null : parseFloat(v, 10);
})(),
forcedMinigameKeys: [1, 2, 3].map(function (i) {
const s = document.getElementById('forcedMg' + i);
return s ? (s.value || '') : '';
}),
};
}
const statusEl = document.getElementById('status');
fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' })
.then((r) => r.json())
.then((t) => {
timing = t && typeof t === 'object' ? t : {};
fillForm();
statusEl.textContent = 'โหลดค่าปัจจุบันแล้ว';
statusEl.className = 'hint ok';
})
.catch(() => {
statusEl.textContent = 'โหลดไม่ได้ — เช็กว่าเซิร์ฟเวอร์รันและ path ถูกต้อง';
statusEl.className = 'hint err';
});
document.getElementById('btn-save').addEventListener('click', () => {
const body = readForm();
statusEl.textContent = 'กำลังบันทึก…';
statusEl.className = 'hint';
fetch(BASE + '/api/game-timing', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
.then((r) => r.json())
.then((res) => {
if (res && res.ok) {
timing = { ...timing, ...res };
statusEl.textContent = 'บันทึกสำเร็จ';
statusEl.className = 'ok';
} else {
statusEl.textContent = (res && res.error) || 'บันทึกไม่สำเร็จ';
statusEl.className = 'err';
}
})
.catch(() => {
statusEl.textContent = 'เครือข่ายผิดพลาด';
statusEl.className = 'err';
});
});
})();
</script>
</body>
</html>
+133 -74
View File
@@ -167,7 +167,7 @@
const STACK_OVERLAP_MISS_MIN_ON_LAND = 0.42;
const STACK_OVERLAP_MISS_FRAC_ON_LAND = 0.14;
const STACK_OVERLAP_MISS_MIN_ON_LAYER = 0.4;
const STACK_OVERLAP_MISS_FRAC_ON_LAYER = 0.3;
const STACK_OVERLAP_MISS_FRAC_ON_LAYER = 0.5; /* ต้องตรงกับ server — miss เมื่อ overlap < 50% บล็อก */
/** จุดกลางแท่งที่วาง vs กลางโซน land — เกิน (ความกว้างโซน × นี้) = พลาด (ชั้นที่ 2 ขึ้นไป) */
const STACK_MAX_CENTER_DRIFT_FRAC_OF_LAND = 0.38;
/** ไม่มี stackLandArea: ใช้กลางแมป — เยื้องได้ไม่เกิน fallW × นี้ (รวม) */
@@ -3615,6 +3615,7 @@
/** ซิงก์จากเซิร์ฟเวอร์ (gauntlet-sync / GET /api/game-timing) */
let gauntletRuntimeTickMs = 220;
let gauntletRuntimeJumpTicks = 6;
let gauntletRuntimeJumpHeightMult = 0.52; /* MG2 ความสูงกระโดด (×tile) — รับจาก server game-timing (Admin ปรับได้) */
/** Stack: รอบสวิงต่อวินาที — จาก GET /api/game-timing · ค่าน้อย = สวิงช้า */
let playStackSwingHz = 0.55;
/** Stack: ความกว้างบล็อก (tile) — null = คำนวณจากโซนลงบนแผนที่ */
@@ -4065,8 +4066,9 @@
let jumpSurviveJumpQueued = false;
let jumpSurviveOnGround = false;
let jumpSurviveLastEmitT = 0;
let jumpSurviveHiddenTimer = null; /* แท็บ hidden → rAF หยุด → ขับ physics ต่อด้วย interval (ผู้เล่นไหลลง+ตกรอบ ไม่ลอยค้าง) */
let jumpSurviveHiddenTimer = null; /* แท็บ hidden → rAF หยุด → แจ้ง server ให้จำลองการตกแทน (คนอื่นเห็นลื่น) */
let jumpSurviveHiddenLastMs = 0;
let jumpSurviveHiddenSent = false; /* แจ้ง server แล้วว่า hidden (กันส่งซ้ำทุก tick) */
let jumpSurviveServerAuthPlay = false; /* true เมื่อรับ jump-survive-bot-move/ended ครั้งแรก — server เป็นเจ้าของบอท/รอบ */
let spaceShooterServerAuthPlay = false; /* MG6: true เมื่อรับ space-shooter-bot-move — server เป็นเจ้าของบอท/รอบ */
let balloonBossServerAuthPlay = false; /* MG7: true เมื่อรับ balloon-boss-bot-move — server เป็นเจ้าของบอท/รอบ/HP บอส */
@@ -5850,6 +5852,36 @@
});
}
/* MG5/6/7 server-auth: server entity "client " ( human server /
race LobbyBplay / reconnect / ghost) fill-bot-state กปดสำหรบเกม server-auth งไม self-heal
*-bot-move packet กดรอป (if(!o)return) = บอทตวนนคาง/หายเฉพาะ client (= อาการ MG6).
สราง preview-bot entity เดยวน + กำหนด avatar/ายตาม index เรยง (deterministic กเครอง).
null าเป takeover id (=socket-id ของคนทออก) entity งไมมา caller รอ join/snapshot. */
function ensureServerAuthBotEntityPlay(id, dir) {
if (id == null) return null;
let o = others.get(id);
if (o) return o;
if (!isPreviewBotId(id)) return null;
o = {
x: 1, y: 1, tx: 1, ty: 1,
direction: dir || 'down',
nickname: 'บอท',
characterId: null,
playTint: playTintForPreviewBotId(id) || resolvePlayTintForPeer(id),
botTier: 'avg',
spaceShooterScore: 0, spaceShooterHits: 0, spaceShooterEliminated: false,
balloonBossScore: 0, balloonBossBossDmg: 0, balloonBossEliminated: false,
jumpSurviveEliminated: false,
};
try { o.balloonBossBalloons = balloonBossBalloonsStartPlay(); } catch (e) { o.balloonBossBalloons = 5; }
others.set(id, o);
/* avatar ตาม index เรียง (เหมือน rebalancePreviewBots 5967-5969) → ทุกเครื่องได้ชุด avatar เดียวกัน */
[...others.keys()].filter(isPreviewBotId).sort().forEach((bid, bi) => { const bo = others.get(bid); if (bo) bo.characterId = pickPreviewBotCharacterId(bi); });
try { relabelPreviewBotsPlay(); } catch (e) { /* ignore */ }
try { if (o.characterId) preloadPlayTintForAvatar(o.characterId, o.playTint); } catch (e) { /* ignore */ }
return o;
}
function rebalancePreviewBots() {
if (!playBotsEnabled() || !mapData) return;
const human = countPlayHumans();
@@ -9895,6 +9927,7 @@
let playClockOffset = 0; /* serverTime - clientTime (ms) */
let playClockBestRtt = null;
let playClockSyncInterval = null;
let playClockEstablished = false; /* sync ครั้งแรกแล้วหรือยัง — ครั้งแรก hard-set, re-sync ถัดไป blend (กันกระโดด) */
function serverNow() { return Date.now() + playClockOffset; }
function syncPlayClock(round) {
round = round || 0;
@@ -9910,7 +9943,16 @@
const rtt = t1 - t0;
if (playClockBestRtt == null || rtt <= playClockBestRtt) {
playClockBestRtt = rtt;
playClockOffset = res.t + rtt / 2 - t1; /* ชดเชยครึ่งหนึ่งของ round-trip latency */
const newOffset = res.t + rtt / 2 - t1; /* ชดเชยครึ่งหนึ่งของ round-trip latency */
if (!playClockEstablished) {
playClockOffset = newOffset; /* ครั้งแรก: ตั้งตรง ๆ ให้ baseline แม่น */
playClockEstablished = true;
} else {
/* re-sync : serverNow 12 (= MG3/timer ).
าวใหญ >250ms = สแน (drift จร/เปลยน server) lag าง; าวเล = blend 25% */
const dOff = newOffset - playClockOffset;
playClockOffset += (Math.abs(dOff) > 250) ? dOff : dOff * 0.25;
}
}
}
if (round < 5) setTimeout(function () { syncPlayClock(round + 1); }, 300);
@@ -12135,7 +12177,7 @@
if (rel) {
swingWorldY = (rel.cy + 0.5) * tileSize;
const spanR = Math.max(1, rel.maxX - rel.minX + 1);
swingAmp = Math.max(swingAmp, Math.min(spanR * 0.42, 4.2));
swingAmp = Math.max(swingAmp, Math.min(spanR * 0.32, 3.0)); /* ต้องตรงกับ server newStackSessionState */
}
const autoW = Math.min(3.2, Math.max(0.85, land ? (land.maxX - land.minX + 1) * 0.48 : 2));
const initW = playStackBlockWidthTiles != null && Number.isFinite(playStackBlockWidthTiles)
@@ -12216,8 +12258,11 @@
}
}
if (towerLive && stackMini.teamMissesLeft != null) {
if (stackServerAuthPlay && hit.teamMissesLeft != null) {
stackMini.teamMissesLeft = Math.max(0, Math.floor(Number(hit.teamMissesLeft)));
if (stackServerAuthPlay) {
/* "" (server s.teamMissesLeft) server
ไมลดเอง local. เด fallback ลด local เม hit ไม แตละเครองลดคนละจงหวะ = เหนหวใจไมตรงก.
packet ไม ปลอยคาเดมไว รอ stack-drop echo จาก server มาตงใหตรงทกเครอง */
if (hit.teamMissesLeft != null) stackMini.teamMissesLeft = Math.max(0, Math.floor(Number(hit.teamMissesLeft)));
} else {
stackMini.teamMissesLeft = Math.max(0, stackMini.teamMissesLeft - 1);
}
@@ -12420,19 +12465,9 @@
: Math.max(STACK_OVERLAP_MISS_MIN_ON_LAYER, fallWEff * STACK_OVERLAP_MISS_FRAC_ON_LAYER);
const overlapMiss = overlap < missThreshold;
const placedCx = c1;
let driftMiss = false;
if (!overlapMiss && m.layers.length > 0) {
const anchorCx = land ? land.cx : wMap * 0.5;
let maxDriftTiles;
if (land) {
const landSpan = Math.max(1, land.maxX - land.minX + 1);
maxDriftTiles = landSpan * STACK_MAX_CENTER_DRIFT_FRAC_OF_LAND;
} else {
maxDriftTiles = Math.max(0.85, fallW * STACK_MAX_CENTER_DRIFT_NO_LAND_MULT);
}
if (Math.abs(placedCx - anchorCx) > maxDriftTiles) driftMiss = true;
}
const miss = overlapMiss || driftMiss;
/* MISS = overlapMiss (). driftMiss ()
ทำใหอเอยงเปนบนไดถกน miss = "ตึกโยกตกทั้งที่ไม่ริม". ใหตรงก server. */
const miss = overlapMiss;
const delta = c1 - supportCx;
const span = supportW * 0.5 + fallW * 0.5;
const offN = Math.abs(delta) / Math.max(0.01, span);
@@ -12484,7 +12519,13 @@
const swingReleaseOff = (stackServerAuthPlay && Number.isFinite(hit.landOffsetTiles))
? hit.landOffsetTiles
: (m.swingAmp * Math.sin(m.phase));
const swingXW = (m.topCenterX + swingReleaseOff) * tileSize;
/* release base = topCenterX server "" (= placedCx landOffset) release x == placedCx == landing x
ตกตรงดงเสมอ ไมไถลเฉยง. เดมใช m.topCenterX client stale ตอน bot/remote drop
(applyNextRemoteStackDropPlay ไมไดเซ topCenterX อนเร fall) = บลอกไถลขางตอนรวง */
const releaseBaseCx = (stackServerAuthPlay && Number.isFinite(hit.placedCx) && Number.isFinite(hit.landOffsetTiles))
? (hit.placedCx - hit.landOffsetTiles)
: m.topCenterX;
const swingXW = (releaseBaseCx + swingReleaseOff) * tileSize;
const lh = m.layerWorldH || Math.max(14, tileSize * 0.3);
const swingLift = getStackTowerSwingLiftWorldPx(m.layers.length, lh);
const swingAttachY = m.swingWorldY - swingLift;
@@ -12552,6 +12593,14 @@
if (stackServerAuthPlay) {
/* server-auth: ส่งเจตนากดวาง + เฟสที่เราเห็น ณ ตอนนี้ → server วางตรงจุดที่เราเห็นแกว่ง (ไม่เยื้องตาม half-RTT) */
try { socket.emit('stack-drop-req', { phase: stackMini.phase }); } catch (e) { /* ignore */ }
/* [DBG-MG3drop] client ส่งอะไร + เห็น swing model ยังไง (เทียบกับ server log) — ถอดออกหลังวินิจฉัย */
try {
var _m = stackMini;
gamelogPost('MG3', 'mg3-send', 'srvAuth=' + stackServerAuthPlay
+ ' sentPhase=' + Number(_m.phase).toFixed(3) + ' amp=' + Number(_m.swingAmp).toFixed(3)
+ ' topCx=' + Number(_m.topCenterX).toFixed(3) + ' hangCx=' + Number(_m.topCenterX + _m.swingAmp * Math.sin(_m.phase)).toFixed(3)
+ ' hearts=' + _m.teamMissesLeft + ' layers=' + (_m.layers ? _m.layers.length : '?'), 'client');
} catch (e) { /* ignore */ }
stackMini.lastDropAt = Date.now(); /* กัน double-tap (server มี busyUntil อยู่แล้ว) */
/* freeze เฟสที่เห็น ณ ตอนกด ไว้จนดรอปกลับมา → ไม่แกว่งต่อแล้วเด้งกลับช่วงรอ echo */
stackLocalSwingFrozen = true;
@@ -12731,7 +12780,26 @@
if (!stackLocalSwingFrozen && !stackFall && !stackMini.settling && stackSrvPhaseAnchorAt > 0) {
/* extrapolate จาก anchor (เวลา server) ด้วย serverNow() → เฟสที่แสดง = เฟส server ปัจจุบัน (ลื่น+ตรง) */
const elapsed = (serverNow() - stackSrvPhaseAnchorAt) / 1000;
stackMini.phase = stackSrvPhaseAnchor + elapsed * stackMini.phaseSpeed * Math.PI * 2;
const targetPhase = stackSrvPhaseAnchor + elapsed * stackMini.phaseSpeed * Math.PI * 2;
/* SLEW-LIMIT: targetPhase snap "" anchor (re-anchor packet)
และของ serverNow (playClockOffset re-sync 12 ) ให = แก "ตัวห้อยกระตุก".
เพดานกาว/เฟรม = าวแกวงปกต ×2.2 (ตามทนไดแตไมวาป) ปกตเดนเท target พอด ไมกระทบ */
if (!Number.isFinite(stackMini.phase)) {
stackMini.phase = targetPhase;
} else {
let dPh = targetPhase - stackMini.phase;
if (Math.abs(dPh) > 0.9) {
/* gap = freeze ( server rad) resync
snap ตรงเฟส server นท. ไม slew ไลตามช = บลอกหอยลาก + เฟสทงตอนกดเยองเก
clamp 1.2rad server ใช s.phase แทน = "วางกลางพอดีแต่ลงเยื้อง/ร่วง" */
stackMini.phase = targetPhase;
} else {
/* gap เล็ก (jitter/re-anchor) → slew เนียน กันกระตุก */
const maxStep = stackMini.phaseSpeed * dt * Math.PI * 2 * 2.2 + 0.06;
if (dPh > maxStep) dPh = maxStep; else if (dPh < -maxStep) dPh = -maxStep;
stackMini.phase += dPh;
}
}
}
advancePhase = false;
emitSwing = false;
@@ -12957,7 +13025,9 @@
const tm = Number(payload.gauntletTickMs);
if (Number.isFinite(tm)) gauntletRuntimeTickMs = Math.max(80, Math.min(800, tm));
const jt = Number(payload.gauntletJumpTicks);
if (Number.isFinite(jt)) gauntletRuntimeJumpTicks = Math.max(4, Math.min(40, jt));
if (Number.isFinite(jt)) gauntletRuntimeJumpTicks = Math.max(1, Math.min(40, jt));
const jhm = Number(payload.gauntletJumpHeightMult);
if (Number.isFinite(jhm)) gauntletRuntimeJumpHeightMult = Math.max(0.2, Math.min(1.5, jhm)); /* MG2 ความสูงกระโดด (Admin) */
const tl = Number(payload.gauntletTimeLimitSec);
if (Number.isFinite(tl)) gauntletRuntimeTimeLimitSec = Math.max(0, Math.min(7200, tl));
if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLiveStartMs')) {
@@ -13244,13 +13314,13 @@
/** โค้งยกตัว: ขึ้นถึงจุดสูงสุดเร็ว ลงชัน (รู้สึกลงพื้นเร็วกว่าแบบสมมาตร) */
function gauntletLiftHeightNorm(air, jumpTicksMax) {
const jm = Math.max(4, jumpTicksMax || 16);
const jm = Math.max(1, jumpTicksMax || 16); /* floor 1 → tick ต่ำ (กระโดดเร็ว) อาร์คยังขึ้น-ลงครบ */
const a = Math.max(0, Math.min(jm, Number(air) || 0));
const t = a / jm;
/* design (rn-jump): sine ease-in-out peak
เด: ขาข quadratic / ขาลง linear (อสมมาตร peak ~72% เวลา ขาลงกระต) */
const p = Math.max(0, Math.min(1, 1 - t)); /* 0 = เริ่มกระโดด, 1 = แตะพื้น */
/* อาร์ค sine นุ่มสมมาตร — ขึ้น-ลงลื่นไม่กระตุก (เวลากระโดดสั้นลงแล้ว = ไม่ลอยค้าง, sine นุ่ม = ไม่วาปลง) */
/* อาร์ค sine นุ่มสมมาตร (ความเร็วกระโดด = จำนวน tick = gauntletJumpTicks ปรับใน admin; ลด tick = ขึ้นเร็ว-ลงเร็ว) */
return Math.sin(Math.PI * p);
}
@@ -13292,7 +13362,7 @@
&& shouldGauntletCrownHeistSkipAvatarDrawPlay(ent, isMe, 0, 0)) return;
seen.add(id);
const air = isMe ? (meGauntletJumpVis || 0) : ((ent.gauntletJumpVis != null ? ent.gauntletJumpVis : ent.gauntletJumpTicks) || 0);
const liftWorldY = air > 0 ? gauntletLiftHeightNorm(air, gauntletRuntimeJumpTicks) * tileSizePx * 0.52 : 0;
const liftWorldY = air > 0 ? gauntletLiftHeightNorm(air, gauntletRuntimeJumpTicks) * tileSizePx * gauntletRuntimeJumpHeightMult : 0;
const cxWorld = (v(ent.x) + cwv * 0.5) * tileSizePx;
const cyBottomWorld = (v(ent.y) + chv) * tileSizePx - liftWorldY;
const scr = worldToScreenFn(cxWorld, cyBottomWorld);
@@ -19211,48 +19281,26 @@
}
/* hidden (/) requestAnimationFrame jumpSurviveTickFrame
เลนไมไหลลงตามแท + ไม move = "ลอยค้างกลางจอ" ในสายตาคนอ
แก: บฟกสอดวย setInterval (timer งเดนตอน hidden ~1Hz) เลนไหลลง+ตกรอบเหมอนคนทไมกระโดด */
เลนไมไหลลงตามแท. เดมข physics อดวย setInterval แต browser throttle timer เหล ~1Hz
emit move ~1/ = "คนอื่นเห็นตกเป็นก้อนใหญ่ ๆ ไม่เนียน".
แก: server-auth แค "แจ้ง server ว่า hidden" (พรอม state จจ) server จำลองการตก (jsPhysicsIntegrate)
แล broadcast าน jump-survive-bot-move แบบ full-rate กคนเหนล + ตำแหนงตรง. กลบมา visible
แจ hidden:false แล client มเองต (me sync จาก server ระหวาง hidden แล handler bot-move). */
function jumpSurviveHiddenFallStep() {
try {
if (typeof document !== 'undefined' && !document.hidden) { jumpSurviveHiddenLastMs = 0; return; }
if (!mapData || !isJumpSurvive() || !isJumpSurviveMissionUiMapPlay()) return;
if (jumpSurviveMissionPhase !== 'live' || jumpSurviveGameEnded || jumpSurviveEliminated) return;
if (!jumpSurviveServerAuthActive()) return; /* เกมจริงเท่านั้น (editor preview คง client-sim) */
const md = mapData;
const ts = tileSize;
const now = Date.now();
let elapsed = jumpSurviveHiddenLastMs ? (now - jumpSurviveHiddenLastMs) / 1000 : 0.1;
jumpSurviveHiddenLastMs = now;
elapsed = Math.max(0, Math.min(0.6, elapsed)); /* จำกัดงานต่อ tick (hidden timer อาจหน่วงเป็นวินาที) */
const risePerSec = Number(md.jumpSurviveRisePxPerSec) > 0 ? Number(md.jumpSurviveRisePxPerSec) : 42;
if (jumpSurviveLiveAnchorServerMs > 0) {
jumpSurvivePlatformScrollPx = Math.max(0, (serverNow() - jumpSurviveLiveAnchorServerMs) / 1000) * risePerSec;
const hidden = (typeof document !== 'undefined') && document.hidden;
const live = !!mapData && isJumpSurvive() && isJumpSurviveMissionUiMapPlay()
&& jumpSurviveMissionPhase === 'live' && !jumpSurviveGameEnded && !jumpSurviveEliminated;
if (!jumpSurviveServerAuthActive()) { jumpSurviveHiddenSent = false; return; } /* editor preview: ไม่มี server sim — คงเดิม (ไม่ทำอะไร) */
if (hidden && live && !jumpSurviveHiddenSent) {
/* เพิ่งสลับไปแท็บอื่น → ส่ง snapshot ให้ server จำลองต่อ */
jumpSurviveHiddenSent = true;
try { if (socket && myId != null) socket.emit('jump-survive-hidden', { hidden: true, x: me.x, y: me.y, vy: jumpSurviveVy, onGround: jumpSurviveOnGround, direction: me.direction }); } catch (e) { /* ignore */ }
} else if ((!hidden || !live) && jumpSurviveHiddenSent) {
/* กลับมาโฟกัส (หรือจบ/ตาย) → ปลด server (client คุมเองต่อ) */
jumpSurviveHiddenSent = false;
try { if (socket && myId != null) socket.emit('jump-survive-hidden', { hidden: false }); } catch (e) { /* ignore */ }
}
const { cw, ch } = getCharacterFootprintWH(md);
let remain = elapsed, died = false;
while (remain > 1e-3 && !died) {
const step = Math.min(0.05, remain); remain -= step; /* sub-step กัน tunneling ทะลุแท่น */
const pxc = (me.x + cw * 0.5) * ts;
const feetY = (me.y + ch) * ts;
const r = jumpSurvivePhysicsIntegrate(md, {
dt: step, cw, ch, pxc0: pxc, feetY0: feetY,
vy0: jumpSurviveVy, wasOnGround: jumpSurviveOnGround,
jumpQueued: false, moveLeft: false, moveRight: false,
});
jumpSurviveVy = r.vy;
jumpSurviveOnGround = r.onGround;
jumpSurviveSyncMeFromHitbox(r.pxc, r.feetY);
if (r.died) died = true;
}
if (died) {
jumpSurviveEliminated = true;
jumpSurviveVy = 0;
try { if (socket && myId != null) socket.emit('move', { x: me.x, y: me.y, direction: me.direction, jumpSurviveEliminated: true }); } catch (e) { /* ignore */ }
return;
}
me.isWalking = false;
try { if (socket && myId != null) socket.emit('move', { x: me.x, y: me.y, direction: me.direction, jsWalking: false }); } catch (e) { /* ignore */ }
} catch (e) { /* ignore */ }
}
@@ -21039,11 +21087,11 @@
/* anchor " server" (d.t) render extrapolate serverNow() = server
(ไมใชเฟสเกาคาง half-RTT). re-anchor packet "ลื่นไม่กระตุก" เพราะ (ph,t) อยบนเสนเวลา-เฟสเดยวก
extrapolate อเนอง; และ "ไม่วาป/ไม่ไถลตอนปล่อย" เพราะจดทเห = ดท server วาง (placedCx) */
const sp2 = (Number.isFinite(stackMini.phaseSpeed) && stackMini.phaseSpeed > 0) ? stackMini.phaseSpeed : 0.6;
const srvT = Number(d.t);
stackSrvPhaseAnchor = ph;
stackSrvPhaseAnchorAt = Number.isFinite(srvT) ? srvT : serverNow(); /* เก็บเป็น "เวลา server" */
stackMini.phase = ph + ((serverNow() - stackSrvPhaseAnchorAt) / 1000) * sp2 * Math.PI * 2;
/* stackMini.phase frame loop (slew-limited) .
เด snap เฟสท packet = กระต 1 เฟรมตอน anchor/clock เด (ตอนนแค update anchor พอ) */
}
} else {
stackMini.phase = ph;
@@ -21407,8 +21455,19 @@
if (isJumpSurviveMissionPregameBlockingPlay()) return; /* ยังนับถอยหลัง 3-2-1 → อย่าเพิ่งขยับบอท (กันวาปก่อน "1") */
d.bots.forEach((rb) => {
if (!rb || typeof rb.id !== 'string') return;
const o = others.get(rb.id);
if (!o) return; /* avatar บอทยังไม่ถูกสร้างฝั่ง client — รอ rebalancePreviewBots */
/* server (hidden) adopt server me ( );
เฉพาะตอน hidden จร (server id เราเฉพาะตอน hidden อยแล guard packet างหลงโฟก) */
if (myId != null && String(rb.id) === String(myId)) {
if (typeof document !== 'undefined' && document.hidden && !jumpSurviveEliminated) {
if (Number.isFinite(rb.x)) me.x = rb.x;
if (Number.isFinite(rb.y)) me.y = rb.y;
if (rb.dir) me.direction = rb.dir;
if (rb.elim) { jumpSurviveEliminated = true; jumpSurviveVy = 0; }
}
return;
}
const o = ensureServerAuthBotEntityPlay(rb.id, rb.dir); /* สร้าง entity ถ้า client นี้ยังไม่มี (กันบอทค้าง) */
if (!o) return; /* takeover id (socket-id) ที่ entity ยังไม่มา — รอ join/snapshot */
/* lerp ปกติ; แต่ delta ใหญ่ (>3 tile = spawn เริ่ม/หลุด sync) → snap ทันที กันบอทไถลข้ามจอ */
if (Number.isFinite(rb.x)) { const big = !Number.isFinite(o.x) || Math.abs(rb.x - o.x) > 3; o.tx = rb.x; if (big) o.x = rb.x; }
if (Number.isFinite(rb.y)) { const big = !Number.isFinite(o.y) || Math.abs(rb.y - o.y) > 3; o.ty = rb.y; if (big) o.y = rb.y; }
@@ -21449,8 +21508,8 @@
spaceShooterServerAuthPlay = true; /* สัญญาณ: server เป็นเจ้าของบอท/รอบรอบนี้ */
d.bots.forEach((rb) => {
if (!rb || typeof rb.id !== 'string') return;
const o = others.get(rb.id);
if (!o) return; /* avatar บอทยังไม่ถูกสร้าง — รอ rebalancePreviewBots */
const o = ensureServerAuthBotEntityPlay(rb.id, rb.dir); /* สร้าง entity ถ้า client นี้ยังไม่มี (กันบอทค้าง) */
if (!o) return; /* takeover id (socket-id) ที่ entity ยังไม่มา — รอ join/snapshot */
/* entity server ( takeover bot id socket-id ) lerp/judge ""
ไม takeover bot ตกไป "ทาง human" (tx/ty) = ขยบเฉพาะ snap (กระต) + ยานเพยนจากตำแหน server (กระสนหล) + ไมโดน judge ชน (ไมตาย) */
o.isSpaceShooterServerBot = true;
@@ -21493,8 +21552,8 @@
balloonBossServerAuthPlay = true; /* สัญญาณ: server เป็นเจ้าของบอท/รอบ/HP บอสรอบนี้ */
d.bots.forEach((rb) => {
if (!rb || typeof rb.id !== 'string') return;
const o = others.get(rb.id);
if (!o) return; /* avatar บอทยังไม่ถูกสร้าง — รอ rebalancePreviewBots */
const o = ensureServerAuthBotEntityPlay(rb.id, 'down'); /* สร้าง entity ถ้า client นี้ยังไม่มี (กันบอทค้าง) */
if (!o) return; /* takeover id (socket-id) ที่ entity ยังไม่มา — รอ join/snapshot */
/* entity ขับโดย server (รวม takeover bot id=socket-id) → ทำเครื่องหมาย เพื่อให้ tick ใช้ "ทางบอท" (lerp center) ไม่ใช่ทาง human (tx/ty ทับตำแหน่ง server = กระตุก/วาป) */
o.isBalloonBossServerBot = true;
/* ตั้ง target แล้ว lerp ใน tick (เดิม snap ตรงทุก ~70ms = กระตุก); delta ใหญ่ค่อย snap */
@@ -25844,7 +25903,7 @@
const air = Number(gauntletAirTicks) || 0;
let liftWorldY = 0;
if (isGauntlet() && air > 0) {
liftWorldY = gauntletLiftHeightNorm(air, gauntletRuntimeJumpTicks) * tileSize * 0.52;
liftWorldY = gauntletLiftHeightNorm(air, gauntletRuntimeJumpTicks) * tileSize * gauntletRuntimeJumpHeightMult;
}
const cxWorld = (ax + cw * 0.5) * tileSize;
/* ใช้ ay แบบต่อเนื่อง — ห้าม floor(ay) เดิมทำให้เท้ากระโดดทีละช่องขณะเดิน (กระตุกแนวตั้ง) */
@@ -25986,7 +26045,7 @@
const air = airVis > 0.08 ? airVis : airTicks;
let liftWorldY = 0;
if (isGauntlet() && air > 0) {
liftWorldY = gauntletLiftHeightNorm(air, gauntletRuntimeJumpTicks) * tileSize * 0.52;
liftWorldY = gauntletLiftHeightNorm(air, gauntletRuntimeJumpTicks) * tileSize * gauntletRuntimeJumpHeightMult;
}
const cxWorld = (ax + cw * 0.5) * tileSize;
const cyBottomWorld = (ay + ch) * tileSize - liftWorldY;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+150 -15
View File
@@ -290,6 +290,7 @@
let trialSilencedMe = false; /* ฉันถูกปิดปาก (Silence) — ห้ามโหวตรอบนี้ */
let trialVoteCounts = [0, 0, 0];
let mapBackgroundImg = null;
let lobbyGridImgCache = {}; /* รูปที่วางในเอดิเตอร์ (gridImageSprites) → cache Image ต่อ lib index */
let hostIconImg = null;
const peers = new Map();
/** รองรับ x/y string จาก Socket — ไม่งั้นตำแหน่งจากสุ่มจุดเกิดจะถูกทิ้ง */
@@ -624,41 +625,53 @@
var el = document.querySelector('#room-loading-overlay .rl-txt');
if (!el) return;
var p = Math.round(Math.max(0, Math.min(100, Number(pct) || 0)));
el.textContent = 'กำลังโหลดตัวละคร… ' + p + '%';
el.textContent = 'กำลังโหลด... ' + p + '%';
}
/* PERF (L.1): tint 4×5 = 20 / (toDataURL ) "" 6 = 120 = ~13s
แก: "บล็อกแค่ idle 4 ทิศ" (โชวฉากไดเร วละครยนนงครบส) แล tint เฟรมเด (0-3) แบบ lazy เบองหล
(render fallback ใช idle ตอนเฟรมเดนยงไมเสรจอยแล เปนยนนงจนเฟรมเดนโหลดท ไมขาว) */
function preloadLobbyTintedCharacter(id, theme, skin, cb, onFrame) {
if (!id || (!theme && !skin)) { if (cb) cb(); return; }
var dirs = ['down', 'up', 'left', 'right'];
var suffixes = ['idle', '0', '1', '2', '3'];
var total = dirs.length * suffixes.length;
var total = dirs.length; /* บล็อกแค่เฟรม idle 4 ทิศ */
var doneCount = 0;
var finished = false;
function startLazyWalkFrames() {
/* เฟรมเดิน 0-3 ทั้ง 4 ทิศ — โหลดเบื้องหลัง (ไม่บล็อกการโชว์ฉาก) */
dirs.forEach(function (dir) {
var ckey = id + '|' + (theme || '') + '|' + (skin || '') + '|' + dir;
var anim = tintedCharCache[ckey] || (tintedCharCache[ckey] = { frames: [], fallback: null });
['0', '1', '2', '3'].forEach(function (suf) {
rlBuildTintedFrame(id, dir, suf, theme, skin, function (img) {
if (img) anim.frames[parseInt(suf, 10)] = img;
if (mapData && canvas) drawLobbyMap();
});
});
});
}
function one() {
doneCount++;
if (onFrame) { try { onFrame(doneCount, total, id); } catch (e) { /* ignore */ } }
if (!finished && doneCount >= total) {
finished = true;
if (cb) cb();
startLazyWalkFrames();
}
}
setTimeout(function () {
if (!finished) {
finished = true;
if (cb) cb();
startLazyWalkFrames();
}
}, 6000);
}, 4000);
dirs.forEach(function (dir) {
var ckey = id + '|' + (theme || '') + '|' + (skin || '') + '|' + dir;
var anim = tintedCharCache[ckey] || (tintedCharCache[ckey] = { frames: [], fallback: null });
suffixes.forEach(function (suf) {
rlBuildTintedFrame(id, dir, suf, theme, skin, function (img) {
if (img) {
if (suf === 'idle') anim.fallback = img;
else anim.frames[parseInt(suf, 10)] = img;
}
one();
});
rlBuildTintedFrame(id, dir, 'idle', theme, skin, function (img) {
if (img) anim.fallback = img;
one();
});
});
}
@@ -711,7 +724,7 @@
/* pointer-events:none → overlay สี (peer เข้า/เปลี่ยนสี) ไม่บังการกดปุ่ม (เดิม all = re-tint ทุกคนทุก join = กดไม่ได้) */
+ 'pointer-events:none;font-family:Kanit,system-ui,sans-serif';
el.innerHTML =
'<div style="color:#9fe8ff;font-size:17px;letter-spacing:.4px">กำลังโหลดสีตัวละคร...</div>'
'<div style="color:#9fe8ff;font-size:17px;letter-spacing:.4px">กำลังโหลด...</div>'
+ '<div id="lcl-pct" style="color:#fff;font-size:38px;font-weight:800;text-shadow:0 0 14px rgba(80,200,255,.6)">0%</div>'
+ '<div style="width:260px;height:9px;border-radius:7px;background:rgba(255,255,255,.13);overflow:hidden">'
+ '<div id="lcl-bar" style="height:100%;width:0%;background:linear-gradient(90deg,#36e0ff,#b15bff);transition:width .12s ease"></div>'
@@ -1020,6 +1033,46 @@
return (dx * dx + dy * dy) <= 2.25;
}
/* ── ปุ่มกลางแมป (LobbyA=START GAME / LobbyB=เรมสบสวน) = gridImageSprite วางในเอดเตอร
เดนเขาใกลแลวกด F; glow เหลอง neon วาดใตปปมน */
function lobbyButtonSprite() {
if (!mapData || !Array.isArray(mapData.gridImageSprites) || !mapData.gridImageSprites.length) return null;
return mapData.gridImageSprites[0]; /* lobby มี sprite เดียว = ปุ่ม */
}
function lobbyCenterSpot() {
var sp = lobbyButtonSprite();
if (sp) {
return { x: Math.floor(sp.x) + (Math.floor(sp.w) || 1) / 2, y: Math.floor(sp.y) + (Math.floor(sp.h) || 1) / 2 };
}
var w = (mapData && mapData.width) || 20;
var h = (mapData && mapData.height) || 15;
return { x: w / 2, y: h / 2 };
}
function isNearLobbyCenterSpot(me) {
if (!me || typeof me.x !== 'number' || typeof me.y !== 'number') return false;
var c = lobbyCenterSpot();
var dx = me.x - c.x, dy = me.y - c.y;
return (dx * dx + dy * dy) <= 9; /* ~3 ช่อง (วัตถุกลางใหญ่ ยืนรอบ ๆ ก็กดได้) */
}
/** มนุษย์ทุกคน (รวม host) กดพร้อมครบ — peers = คนจริง (บอทอยู่ lobbyBots แยก) */
function lobbyAllHumansReady() {
var n = 0, allReady = true;
peers.forEach(function (p) { n++; if (!(p && p.ready)) allReady = false; });
return n > 0 && allReady;
}
/** สถานะรูปกลาง: { mode:'start'(A)/'suspect'(B), active } — null = ไม่ต้องโชว์ */
function lobbyCenterInfo() {
if (!mapData) return null;
if (isPostCaseLobbyRoom()) {
if (!serverSuspectPhaseActive) return null; /* ไม่มีเฟสให้เปิด → ไม่โชว์ */
return { mode: 'suspect', active: !suspectPickOverlayOpen };
}
if (isCurrentRoomLobbyA()) {
return { mode: 'start', active: lobbyAllHumansReady() }; /* glow เหลืองเมื่อทุกคนพร้อม */
}
return null;
}
function openHostConsoleIfHost() {
if (hostId !== socket.id) {
appendLobbySystemChat('— เฉพาะ Host เท่านั้น');
@@ -1424,6 +1477,55 @@
ctx.drawImage(mapBackgroundImg, 0, 0, nw, nh, 0, 0, mapWpx, mapHpx);
}
/* glow neon "" sprite ().
active: LobbyA=กคนกดพรอม / LobbyB=เฟสผองสงสยรอเป บอกวากด F ได */
(function drawLobbyButtonGlow() {
var info = lobbyCenterInfo();
if (!info || !info.active) return;
var sp = lobbyButtonSprite();
if (!sp) return;
var dx = Math.floor(sp.x) * tileSize, dy = Math.floor(sp.y) * tileSize;
var dw = (Math.floor(sp.w) || 1) * tileSize, dh = (Math.floor(sp.h) || 1) * tileSize;
var bcx = dx + dw / 2;
var pulse = 0.5 + 0.5 * Math.sin(Date.now() / 340); /* 0..1 กระพริบ */
ctx.save();
ctx.globalCompositeOperation = 'lighter'; /* แสงบวก = นีออนเรือง */
/* halo รอบปุ่ม */
ctx.shadowColor = 'rgba(255,214,80,0.95)';
ctx.shadowBlur = 30 + 28 * pulse;
ctx.fillStyle = 'rgba(255,210,70,' + (0.14 + 0.20 * pulse) + ')';
ctx.beginPath();
ctx.ellipse(bcx, dy + dh / 2, dw * 0.60, dh * 0.58, 0, 0, Math.PI * 2);
ctx.fill();
/* แสงเน้นใต้ปุ่ม */
ctx.shadowBlur = 22 + 20 * pulse;
ctx.fillStyle = 'rgba(255,226,120,' + (0.18 + 0.22 * pulse) + ')';
ctx.beginPath();
ctx.ellipse(bcx, dy + dh * 0.94, dw * 0.5, dh * 0.2, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}());
/* รูปที่ผู้เล่นวางในเอดิเตอร์ (gridImageSprites) — เดิม lobby ไม่ render → รูปที่ user เพิ่มไม่ขึ้น */
if (Array.isArray(mapData.gridImageSprites) && Array.isArray(mapData.gridImageLibrary)) {
for (const sp of mapData.gridImageSprites) {
if (!sp) continue;
const idx = Math.floor(Number(sp.i));
const lib = mapData.gridImageLibrary[idx];
const url = (typeof lib === 'string') ? lib : (lib && lib.idle ? lib.idle : '');
if (!url) continue;
let img = lobbyGridImgCache[idx];
if (!img) { img = new Image(); img.onload = () => { redrawLobbyMap(); }; img.src = url; lobbyGridImgCache[idx] = img; }
if (img.complete && img.naturalWidth) {
const dx = Math.floor(Number(sp.x)) * tileSize;
const dy = Math.floor(Number(sp.y)) * tileSize;
const dw = Math.max(1, Math.floor(Number(sp.w)) || 1) * tileSize;
const dh = Math.max(1, Math.floor(Number(sp.h)) || 1) * tileSize;
ctx.drawImage(img, dx, dy, dw, dh);
}
}
}
const showGrid = mapData.showMapInGame !== false && mapData.showMapInGame !== 'false';
const timeMs = Date.now();
if (showGrid) {
@@ -1580,6 +1682,26 @@
ctx.drawImage(roomHcIcon, _hcx + tileSize / 2 - _hcS / 2, _hcy + tileSize - _hcS, _hcS, _hcS);
}
/* คำใบ้ "กด F" เหนือปุ่ม — เฉพาะตอนยืนใกล้ + กดได้ (รูปปุ่ม sci-fi เอง = กราฟิกหลักจาก gridImageSprite) */
(function drawLobbyButtonFHint() {
var info = lobbyCenterInfo();
if (!info) return;
var canPress = info.mode === 'suspect' ? info.active : (info.active && hostId === socket.id);
if (!canPress || !isNearLobbyCenterSpot(peers.get(socket.id))) return;
var sp = lobbyButtonSprite();
if (!sp) return;
var bcx = (Math.floor(sp.x) + (Math.floor(sp.w) || 1) / 2) * tileSize;
var topY = Math.floor(sp.y) * tileSize;
ctx.save();
ctx.textAlign = 'center';
ctx.font = '800 ' + Math.round(tileSize * 0.5) + 'px sans-serif';
ctx.lineWidth = 4; ctx.strokeStyle = 'rgba(0,0,0,0.72)';
ctx.strokeText('▸ กด F', bcx, topY - tileSize * 0.3);
ctx.fillStyle = '#ffe07a';
ctx.fillText('▸ กด F', bcx, topY - tileSize * 0.3);
ctx.restore();
}());
const peerList = [...peers.entries(), ...lobbyBots.entries()].sort(function (a, b) {
const pa = a[1], pb = b[1];
const ya = pa.y != null ? pa.y : 1, yb = pb.y != null ? pb.y : 1;
@@ -6656,8 +6778,8 @@
function updateSuspectFloatingOpenBtn() {
const btn = document.getElementById('btn-suspect-reopen');
if (!btn) return;
const show = !!serverSuspectPhaseActive && !suspectPickOverlayOpen && isPostCaseLobbyRoom();
btn.classList.toggle('is-hidden', !show);
/* แทนปุ่มลอยเดิมด้วย "รูปกลางแมป (กด F)" — ซ่อนปุ่มนี้เสมอ (กลไกเปิดอยู่ที่รูปกลาง + glow) */
btn.classList.add('is-hidden');
}
/** ชี้ตัวคนร้ายได้ต่อเมื่อสืบครบทั้ง 3 ผู้ต้องสงสัย (เล่นมินิเกมอย่างน้อยการ์ดละ 1 ครั้ง) */
@@ -7576,11 +7698,24 @@
openHostConsoleIfHost();
return;
}
/* รูปกลาง LobbyB = เปิดหน้าผู้ต้องสงสัย (แทนปุ่มลอย #btn-suspect-reopen) — ไม่ต้องกดพร้อม */
if (isPostCaseLobbyRoom() && isNearLobbyCenterSpot(me) && serverSuspectPhaseActive && !suspectPickOverlayOpen) {
e.preventDefault();
openSuspectOverlay(suspectSelectedIndex);
return;
}
/* F อื่นในโถง (Host เลือกระดับ/คดี และช่องเขียว) — ต้องกดพร้อมก่อน */
if (!me.ready) {
appendLobbySystemChat('— กดพร้อมก่อน แล้วค่อยกด F');
return;
}
/* รูปกลาง LobbyA = host เริ่มเกม (เมื่อทุกคนกดพร้อมครบ — ตรงกับ glow เหลือง) */
if (isCurrentRoomLobbyA() && hostId === socket.id && isNearLobbyCenterSpot(me)) {
e.preventDefault();
if (!lobbyAllHumansReady()) { appendLobbySystemChat('— รอให้ทุกคนกดพร้อมก่อน'); return; }
openLobbyPreplayWizard();
return;
}
if (hostCanOpenLobbyAPreplayWithF()) {
openLobbyPreplayWizard();
return;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -5438,7 +5438,7 @@
<script src="/app-base.js?v=2"></script>
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0306"></script>
<script src="js/play.js?v=0.006300340"></script>
<script src="js/play.js?v=0.006301050"></script>
<div class="version-tag">v —</div>
<style id="qc-howto-mg2-fix">
/* HOW TO PLAY ของ quiz_carry (Minigame-4) -> ให้เหมือน Minigame-2 (gch-mg2-mock)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1648,7 +1648,7 @@
<script src="js/version.js?v=0.0122"></script>
<script src="js/customize-popup.js?v=40" data-customize-triggers="" data-customize-asset-base="img/03-5-Customize"></script>
<script src="js/achievements.js?v=0.002" data-asset-base="img/03-6-Profile"></script>
<script src="js/room-lobby.js?v=0.0333"></script>
<script src="js/room-lobby.js?v=0.0339"></script>
<div class="version-tag">v —</div>
</body>
</html>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+260 -49
View File
@@ -2196,6 +2196,7 @@ function loadQuizSettings() {
const GAUNTLET_DEFAULT_TICK_MS = 220;
const GAUNTLET_DEFAULT_JUMP_TICKS = 6;
const GAUNTLET_DEFAULT_JUMP_HEIGHT_MULT = 0.52; /* MG2 ความสูงกระโดด = ×tile (จุดสูงสุดของส่วนโค้ง sin) — ปรับได้ใน Admin */
const GAUNTLET_DEFAULT_TIME_LIMIT_SEC = 0;
/** รอบสวิงซ้าย-ขวา ต่อวินาที (Stack) — ค่าน้อย = ช้า · cycles per second of horizontal swing */
const STACK_SWING_HZ_DEFAULT = 0.55;
@@ -2204,7 +2205,9 @@ function defaultGameTiming() {
return {
gauntletTickMs: GAUNTLET_DEFAULT_TICK_MS,
gauntletJumpTicks: GAUNTLET_DEFAULT_JUMP_TICKS,
gauntletJumpHeightMult: GAUNTLET_DEFAULT_JUMP_HEIGHT_MULT,
gauntletTimeLimitSec: GAUNTLET_DEFAULT_TIME_LIMIT_SEC,
troublesomeOfferSec: 15,
...defaultGauntletVisuals(),
stackSwingHz: STACK_SWING_HZ_DEFAULT,
stackBlockWidthTiles: null,
@@ -2250,6 +2253,7 @@ function defaultGameTiming() {
testSpecialCardByMap: {},
/** โหมดเทสต์: บังคับเสนอบทตัวป่วน (ปกติเสนอเฉพาะเมื่อมีคนจริง ≥ 4) */
troublesomeForceOffer: false,
troublesomeRollMaxSec: 8,
/** เวลาที่ไอคอนคำถามพิเศษอยู่บนฉากก่อนหายไป (วินาที) — 0 = ไม่หาย */
specialQuizIconExpireSec: 10,
/** เวลาโหวตเลือกผู้เล่น (การ์ด Silence/Ban) วินาที */
@@ -2452,7 +2456,24 @@ function clampGauntletTickMs(n) {
function clampGauntletJumpTicks(n) {
const v = Number(n);
if (Number.isNaN(v)) return GAUNTLET_DEFAULT_JUMP_TICKS;
return Math.max(4, Math.min(40, Math.floor(v)));
return Math.max(1, Math.min(40, Math.floor(v)));
}
function clampGauntletJumpHeightMult(n) {
const v = Number(n);
if (Number.isNaN(v)) return GAUNTLET_DEFAULT_JUMP_HEIGHT_MULT;
return Math.max(0.2, Math.min(1.5, v));
}
/** เวลารับข้อเสนอ "ตัวป่วน" (วินาที) — admin ปรับได้ใน game-timing; 5120, default 15 */
function clampTroublesomeOfferSec(n) {
const v = Number(n);
if (!Number.isFinite(v)) return 15;
return Math.max(5, Math.min(120, Math.floor(v)));
}
/* เวลาสูงสุด (วินาที) ที่ระบบรอให้ทุกคนรายงานพร้อม ก่อน "บังคับสุ่ม" ผู้รับบทตัวป่วน (เดิม hardcode 8000ms) */
function clampTroublesomeRollMaxSec(n) {
const v = Number(n);
if (!Number.isFinite(v)) return 8;
return Math.max(1, Math.min(30, Math.floor(v)));
}
/** 0 = ไม่จำกัดเวลา · ค่าอื่น = วินาที (สูงสุด 2 ชม.) */
@@ -2590,7 +2611,9 @@ function loadGameTiming() {
return {
gauntletTickMs: clampGauntletTickMs(j.gauntletTickMs),
gauntletJumpTicks: clampGauntletJumpTicks(j.gauntletJumpTicks),
gauntletJumpHeightMult: clampGauntletJumpHeightMult(j.gauntletJumpHeightMult),
gauntletTimeLimitSec: clampGauntletTimeLimitSec(j.gauntletTimeLimitSec),
troublesomeOfferSec: Object.prototype.hasOwnProperty.call(j, 'troublesomeOfferSec') ? clampTroublesomeOfferSec(j.troublesomeOfferSec) : 15,
...vis,
stackSwingHz: Object.prototype.hasOwnProperty.call(j, 'stackSwingHz')
? clampStackSwingHz(j.stackSwingHz)
@@ -2652,6 +2675,7 @@ function loadGameTiming() {
),
testSpecialCardByMap: normalizeTestSpecialCardByMap(j.testSpecialCardByMap),
troublesomeForceOffer: !!j.troublesomeForceOffer,
troublesomeRollMaxSec: clampTroublesomeRollMaxSec(j.troublesomeRollMaxSec),
specialQuizIconExpireSec: Object.prototype.hasOwnProperty.call(j, 'specialQuizIconExpireSec')
? Math.max(0, Math.min(120, Math.floor(Number(j.specialQuizIconExpireSec)) || 0))
: 10,
@@ -2842,7 +2866,9 @@ function saveGameTimingToFile(d) {
const out = {
gauntletTickMs: clampGauntletTickMs(d.gauntletTickMs),
gauntletJumpTicks: clampGauntletJumpTicks(d.gauntletJumpTicks),
gauntletJumpHeightMult: clampGauntletJumpHeightMult(d.gauntletJumpHeightMult),
gauntletTimeLimitSec: clampGauntletTimeLimitSec(d.gauntletTimeLimitSec),
troublesomeOfferSec: Object.prototype.hasOwnProperty.call(d, 'troublesomeOfferSec') ? clampTroublesomeOfferSec(d.troublesomeOfferSec) : ((runtimeGameTiming && runtimeGameTiming.troublesomeOfferSec) || 15),
...vis,
stackSwingHz: stackHz,
stackBlockWidthTiles: stackBlockW,
@@ -2877,6 +2903,9 @@ function saveGameTimingToFile(d) {
troublesomeForceOffer: Object.prototype.hasOwnProperty.call(d, 'troublesomeForceOffer')
? !!d.troublesomeForceOffer
: !!prev.troublesomeForceOffer,
troublesomeRollMaxSec: Object.prototype.hasOwnProperty.call(d, 'troublesomeRollMaxSec')
? clampTroublesomeRollMaxSec(d.troublesomeRollMaxSec)
: clampTroublesomeRollMaxSec(prev.troublesomeRollMaxSec),
specialQuizIconExpireSec: Object.prototype.hasOwnProperty.call(d, 'specialQuizIconExpireSec')
? Math.max(0, Math.min(120, Math.floor(Number(d.specialQuizIconExpireSec)) || 0))
: (prev.specialQuizIconExpireSec != null ? Math.max(0, Math.min(120, Math.floor(Number(prev.specialQuizIconExpireSec)) || 0)) : 10),
@@ -4865,6 +4894,8 @@ function quizCarryEntityCanStandServer(md, tx, ty) {
const [cx, cy] = k.split(',').map(Number);
if (!quizTileWalkableServer(md, cx + 0.5, cy + 0.5)) return false;
if (quizCellOn(md.quizCarryHubArea || [], cx, cy)) return false;
/* โต๊ะ submit กลาง (interactive/เขียว) = สิ่งกีดขวาง ห้ามยืนทับ/เดินทะลุ (ส่งคำตอบจากช่องข้างๆ ได้ — quizCarryCanSubmitAtServer รับ adjacent) */
if (quizCellOn(md.interactive || [], cx, cy)) return false;
}
return true;
}
@@ -4944,7 +4975,45 @@ function quizCarryOptionHeldByAnyoneServer(s, optionIdx, exceptId) {
return found;
}
/** รวมรายชื่อช่องว่าง (เดินยืนได้ + ไม่ใช่ option/interactive) สำหรับเกิดบอท */
/* "" (footprint-aware BFS) option/spawn /
"คอนโซลกลาง" นไดแตออกไมได (footprint 3×4 ลอดชองแคบ/ทางทดกำแพง+hub+interactive ไมได
= อาการ "บอทยืนบนโต๊ะ"). cache md (โครงสรางแมปคงทอรอบเกม) */
function quizCarryReachableTilesServer(md) {
if (md.__qcReach) return md.__qcReach;
const w = md.width || 20, h = md.height || 15;
const oa = md.quizCarryOptionArea, sa = md.spawnArea;
const seeds = [];
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const isSeed = (Array.isArray(oa) && oa[y] && Number(oa[y][x]) >= 1) || (Array.isArray(sa) && quizCellOn(sa, x, y));
if (isSeed && quizCarryEntityCanStandServer(md, x, y)) seeds.push([x, y]);
}
}
const reach = new Set();
if (!seeds.length) {
/* ไม่มี seed (แมปไม่มี option/spawn) → ไม่กรอง: ทุก standable นับว่าถึงได้ */
for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) if (quizCarryEntityCanStandServer(md, x, y)) reach.add(x + ',' + y);
md.__qcReach = reach;
return reach;
}
const q = [];
for (const s of seeds) { const k = s[0] + ',' + s[1]; if (!reach.has(k)) { reach.add(k); q.push(s); } }
while (q.length) {
const cur = q.shift();
const nb = [[cur[0] + 1, cur[1]], [cur[0] - 1, cur[1]], [cur[0], cur[1] + 1], [cur[0], cur[1] - 1]];
for (const n of nb) {
const k = n[0] + ',' + n[1];
if (!reach.has(k) && quizCarryEntityCanStandServer(md, n[0], n[1])) { reach.add(k); q.push(n); }
}
}
md.__qcReach = reach;
return reach;
}
function quizCarryTileReachableServer(md, x, y) {
return quizCarryReachableTilesServer(md).has(Math.floor(x) + ',' + Math.floor(y));
}
/** รวมรายชื่อช่องว่าง (เดินยืนได้ + ถึงได้จริง + ไม่ใช่ option/interactive) สำหรับเกิดบอท */
function quizCarrySpawnTilesServer(md) {
const w = md.width || 20, h = md.height || 15;
const free = [], any = [];
@@ -4952,6 +5021,7 @@ function quizCarrySpawnTilesServer(md) {
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
if (!quizCarryEntityCanStandServer(md, x, y)) continue;
if (!quizCarryTileReachableServer(md, x, y)) continue; /* ข้ามช่องที่ยืนได้แต่ออกไม่ได้ (คอนโซลกลาง) → บอทไม่เกิดติด */
any.push({ x, y });
const isZone = (Array.isArray(md.quizCarryOptionArea) && md.quizCarryOptionArea[y] && Number(md.quizCarryOptionArea[y][x]) >= 1)
|| quizCellOn(md.interactive || [], x, y);
@@ -5268,6 +5338,12 @@ function stepQuizCarryBots(sid, space, dt, now) {
const answering = s.phase === 'answer';
const choiceCount = s.current ? (s.current.choices || []).length : 0;
/* (x,y) : ( objects) + hub + submit (interactive)
บอทไมเดนทะล "โต๊ะตรงกลาง" (เดมเชคแค hub+objects ทำใหทะลโต interactive ไมไดอยใน objects) */
const canStepTo = (x, y) => quizTileWalkableServer(md, x, y)
&& !quizCarryFootprintOverlapsHubServer(md, x, y)
&& !quizCarryFootprintOverlapsInteractiveServer(md, x, y);
const followPath = (b) => {
if (!b.botPath || !b.botPath.length) return false;
const tgt = b.botPath[0];
@@ -5280,9 +5356,9 @@ function stepQuizCarryBots(sid, space, dt, now) {
const sx = Math.min(step, dist);
const nx = b.x + dx * sx, ny = b.y + dy * sx;
const ox = b.x, oy = b.y;
if (!quizCarryFootprintOverlapsHubServer(md, nx, ny) && quizTileWalkableServer(md, nx, ny)) { b.x = nx; b.y = ny; }
else if (!quizCarryFootprintOverlapsHubServer(md, nx, b.y) && quizTileWalkableServer(md, nx, b.y)) { b.x = nx; }
else if (!quizCarryFootprintOverlapsHubServer(md, b.x, ny) && quizTileWalkableServer(md, b.x, ny)) { b.y = ny; }
if (canStepTo(nx, ny)) { b.x = nx; b.y = ny; }
else if (canStepTo(nx, b.y)) { b.x = nx; }
else if (canStepTo(b.x, ny)) { b.y = ny; }
else { b.botPath = []; }
b.isWalking = Math.abs(b.x - ox) > 1e-5 || Math.abs(b.y - oy) > 1e-5;
return true;
@@ -5304,16 +5380,16 @@ function stepQuizCarryBots(sid, space, dt, now) {
const sx = Math.min(step, dist);
const nx = b.x + dx * sx, ny = b.y + dy * sx;
const ox = b.x, oy = b.y;
if (!quizCarryFootprintOverlapsHubServer(md, nx, ny) && quizTileWalkableServer(md, nx, ny)) { b.x = nx; b.y = ny; }
else if (!quizCarryFootprintOverlapsHubServer(md, nx, b.y) && quizTileWalkableServer(md, nx, b.y)) { b.x = nx; }
else if (!quizCarryFootprintOverlapsHubServer(md, b.x, ny) && quizTileWalkableServer(md, b.x, ny)) { b.y = ny; }
if (canStepTo(nx, ny)) { b.x = nx; b.y = ny; }
else if (canStepTo(nx, b.y)) { b.x = nx; }
else if (canStepTo(b.x, ny)) { b.y = ny; }
else { b.wanderTX = null; }
b.isWalking = Math.abs(b.x - ox) > 1e-5 || Math.abs(b.y - oy) > 1e-5;
};
s.bots.forEach((b, bid) => {
/* ถ้าบังเอิญทับ hub → ดันออก */
if (quizCarryFootprintOverlapsHubServer(md, b.x, b.y)) {
/* ทับ hub / โต๊ะ submit กลาง (interactive) / หรือ "ติดในคอนโซลกลาง" (ยืนได้แต่เดินออกไม่ได้) → ดันออกไปช่องว่างที่ถึงได้ */
if (quizCarryFootprintOverlapsHubServer(md, b.x, b.y) || quizCarryFootprintOverlapsInteractiveServer(md, b.x, b.y) || !quizCarryTileReachableServer(md, b.x, b.y)) {
const tiles = quizCarrySpawnTilesServer(md);
if (tiles.length) { const t = tiles[(b.x | 0) % tiles.length]; b.x = t.x + 0.5; b.y = t.y + 0.5; b.botPath = []; }
}
@@ -5368,10 +5444,12 @@ function quizCarryFindSubmitTileServer(md, b) {
const w = md.width || 20, h = md.height || 15;
const pool = [];
if (quizCarryMapHasInteractiveServer(md)) {
/* ช่องที่ "ยืนได้จริง + ชิดโต๊ะ interactive" (ไม่ใช่ยืนทับโต๊ะ — โต๊ะเป็นสิ่งกีดขวางแล้ว) → ส่งจากข้างๆ */
for (let y = 0; y < h; y++) {
const row = md.interactive && md.interactive[y];
if (!row) continue;
for (let x = 0; x < w; x++) if (row[x] === 1 && quizTileWalkableServer(md, x + 0.5, y + 0.5)) pool.push({ x, y });
for (let x = 0; x < w; x++) {
if (!quizCarryEntityCanStandServer(md, x, y)) continue;
if (quizCarryFootprintAdjacentInteractiveServer(md, x + 0.5, y + 0.5)) pool.push({ x, y });
}
}
if (pool.length) return pool[Math.floor(Math.random() * pool.length)];
}
@@ -7757,7 +7835,7 @@ function emitGauntletSync(sid, space) {
const STACK_OVERLAP_MISS_MIN_ON_LAND = 0.42;
const STACK_OVERLAP_MISS_FRAC_ON_LAND = 0.14;
const STACK_OVERLAP_MISS_MIN_ON_LAYER = 0.4;
const STACK_OVERLAP_MISS_FRAC_ON_LAYER = 0.3;
const STACK_OVERLAP_MISS_FRAC_ON_LAYER = 0.5; /* miss เมื่อ overlap < 50% บล็อก (เดิม 30% หลวมไป = ลงเยื้อง 3 ช่องยังไม่ miss หัวใจไม่ลด) — ต้องตรงกับ client */
const STACK_MAX_CENTER_DRIFT_FRAC_OF_LAND = 0.38;
const STACK_MAX_CENTER_DRIFT_NO_LAND_MULT = 1.0;
const STACK_TOWER_BLOCK_WORLD_SCALE = 1.5;
@@ -7766,7 +7844,12 @@ const STACK_DROP_BUSY_MS = 1500; /* รอแอนิเมชันบล็
const STACK_BOT_THINK_MIN_MS = 560;
const STACK_BOT_THINK_MAX_MS = 1180;
const STACK_SEATS = 6;
const STACK_DROP_PHASE_TOL = 1.2; /* เฟส client (ตอนกดวาง) ต้องใกล้เฟส server ไม่เกินนี้ (rad) ถึงจะใช้ — กันโกง */
/* client () server (rad) .
เด 1.2rad (~0.35s@0.55Hz) latency/desync เกนน server งเฟส client ใช s.phase (เลอนไปแล)
= "เล็งกลางแต่ลงเยื้อง/ร่วง" (intermittent ตาม latency). ขยายเป ~1รอบ (2π 1.8s@0.55Hz)
= ครอบ latency เลนไดจรงทงหมด ใชเฟสทเลนเหนเสมอ (ลงตรงจดทเห) เหลอไวแคนค garbage.
เกม co-op สรางตกรวมก ไมองกนโกงเฟส */
const STACK_DROP_PHASE_TOL = Math.PI * 2;
function stackBoundsServer(area, w, h) {
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity, any = false;
@@ -7801,6 +7884,15 @@ function newStackSessionState(space, md) {
const span = Math.max(1, land.maxX - land.minX + 1);
swingAmp = Math.min(span * 0.44, Math.max(0.6, span * 0.36));
}
/* rel-boost: stackReleaseArea ( client play.js ~12177-12180 )
เด server ใชแค land span client ( rel-boost) แกว "กว้างกว่า" server: บลอกหอยกวางแตตกแคบ
= ไถลเฉยงตอนปลอย + logical ตกใกลกลางเสมอจน "ทับ support ตลอด ไม่นับ miss → หัวใจไม่ลด".
ทำให 2 งใชสวงเดยวก ตกตรง (straight) + miss/วใจ ทำงานถ */
const rel = stackBoundsServer(md.stackReleaseArea, w, h);
if (rel) {
const spanR = Math.max(1, rel.maxX - rel.minX + 1);
swingAmp = Math.max(swingAmp, Math.min(spanR * 0.32, 3.0)); /* ต้องตรงกับ client play.js — ลดจาก 0.42/4.2 (สวิงกว้างเกิน = หน่วงนิ้วนิดเดียวเยื้องเยอะ "ต่อตรงแต่ลงเยื้อง") แต่ยังกว้างพอให้ miss ได้เมื่อเยื้องจริง */
}
const autoW = Math.min(3.2, Math.max(0.85, land ? (land.maxX - land.minX + 1) * 0.48 : 2));
const cfgW = Number(runtimeGameTiming.stackBlockWidthTiles);
const initW = Number.isFinite(cfgW) && cfgW > 0 ? Math.max(0.85, Math.min(3.2, cfgW)) : autoW;
@@ -7864,15 +7956,11 @@ function stackComputeDropServer(s, md, phaseOverride) {
: Math.max(STACK_OVERLAP_MISS_MIN_ON_LAYER, fallWEff * STACK_OVERLAP_MISS_FRAC_ON_LAYER);
const overlapMiss = overlap < missThreshold;
const placedCx = c1;
let driftMiss = false;
if (!overlapMiss && s.layers.length > 0) {
const anchorCx = land ? land.cx : w * 0.5;
const maxDrift = land
? Math.max(1, land.maxX - land.minX + 1) * STACK_MAX_CENTER_DRIFT_FRAC_OF_LAND
: Math.max(0.85, fallW * STACK_MAX_CENTER_DRIFT_NO_LAND_MULT);
if (Math.abs(placedCx - anchorCx) > maxDrift) driftMiss = true;
}
const miss = overlapMiss || driftMiss;
/* MISS = overlapMiss () .
เดมม driftMiss เทยบ placedCx "กลางพื้นดินตายตัว" (anchorCx=land.cx) อเอยงเปนบนได
(แตละชนทบชนลางพอด) พอ center สะสมหางจากกลางพ >38% ฐาน บลอกทวางด กน miss
= อาการ "ตึกโยกตกทั้งที่ไม่ได้อยู่ริมๆ". ดออก overlapMiss อเงอนไขตกทกตองอยแล. */
const miss = overlapMiss;
const delta = c1 - supportCx;
const span = supportW * 0.5 + fallW * 0.5;
const offN = Math.abs(delta) / Math.max(0.01, span);
@@ -8103,11 +8191,27 @@ function runStackTick(sid, space) {
(ไมสแนปเฟส = ไมกระต) ปลอยเมอถงเป หรอเลย deadline นคาง */
if (s.botAimPhase == null) {
s.botAimPhase = stackBotComputeAimPhaseServer(s, md, tier);
s.botAimDeadline = now + 2600;
/* deadline "" = phase aimPhase + buffer.
เด 2600ms ตายต ตอน hz (สวงช) กวาจะถงเปาใช >2.6s deadline เตะกอน = บอทปลอยผ phase (เฉยง/miss) */
const _gap = Math.max(0, s.botAimPhase - s.phase);
const _radPerSec = Math.max(1e-4, s.phaseSpeed * Math.PI * 2);
s.botAimDeadline = now + Math.min(15000, Math.ceil((_gap / _radPerSec) * 1000) + 1500);
}
if (!(s.phase >= s.botAimPhase || now >= s.botAimDeadline)) return;
const _dbgAim = s.botAimPhase; const _dbgDeadline = (now >= s.botAimDeadline) && !(s.phase >= s.botAimPhase);
s.botAimPhase = null;
const hit = stackComputeDropServer(s, md);
/* [DBG-MG3drop] บอทดรอปที่ s.phase → เทียบ aimPhase/จุดลง/รองรับ/overlap/miss (ถอดออกหลังวินิจฉัย) */
try {
const supportCx = hit.placedCx - (hit.delta || 0);
glog(sid, space, 'mg3-drop', 'BOT ' + (entry.botId || '') + ' phase=' + s.phase.toFixed(3)
+ ' aimPhase=' + (Number.isFinite(_dbgAim) ? _dbgAim.toFixed(3) : 'x') + (_dbgDeadline ? '(DEADLINE!)' : '(reached)')
+ ' sinDrop=' + Math.sin(s.phase).toFixed(3)
+ ' amp=' + s.swingAmp.toFixed(3) + ' topCx=' + s.topCenterX.toFixed(3)
+ ' placedCx=' + hit.placedCx.toFixed(3) + ' supportCx=' + supportCx.toFixed(3)
+ ' overlap=' + (hit.overlap != null ? hit.overlap.toFixed(3) : '?') + ' miss=' + hit.miss
+ ' layers=' + s.layers.length + ' hearts=' + s.teamMissesLeft);
} catch (e) { /* ignore */ }
stackApplyHitServer(s, hit, { botId: entry.botId });
io.to(sid).emit('stack-drop', {
placedCx: hit.placedCx, placedW: hit.placedW, miss: hit.miss, perfect: hit.perfect,
@@ -8493,7 +8597,8 @@ function stepJumpSurviveBots(sid, space, dt, now) {
const lowDangerY = h * ts * 0.60;
const outBots = [];
s.bots.forEach((b, id) => {
if (b.eliminated) return;
/* บอทตายแล้ว: ยัง emit elim:true ต่อทุก tick (เดิม return ข้าม = ส่ง elim ครั้งเดียว → client พลาด = ค้างนิ่ง) */
if (b.eliminated) { outBots.push({ id, x: b.x, y: b.y, dir: b.dir || 'down', walking: false, elim: true }); return; }
let pxc = (b.x + cw * 0.5) * ts;
let feetY = (b.y + ch) * ts;
const lowDanger = feetY > lowDangerY;
@@ -8537,6 +8642,31 @@ function stepJumpSurviveBots(sid, space, dt, now) {
b.walking = (!r.onGround && Math.abs(r.vy) > 40) || moveLeft || moveRight;
outBots.push({ id, x: b.x, y: b.y, dir: b.dir, walking: b.walking, elim: false });
});
/* (hidden) timer client throttle full-rate ().
ไม input (ตก/ไหลตามแทนเฉย ) เหมอนคนปลอยจอ. ตาย mark eliminated + p.jumpSurviveEliminated (เขาเช all_dead/เกรด) */
if (s.players) {
for (const pid in s.players) {
const st = s.players[pid];
if (!st || !st.hidden || st.eliminated) continue;
if (!space.peers.has(pid)) { st.hidden = false; continue; } /* หลุดห้องแล้ว → ปล่อยให้ disconnect logic จัดการ */
const pxc = ((Number.isFinite(st.x) ? st.x : 1) + cw * 0.5) * ts;
const feetY = ((Number.isFinite(st.y) ? st.y : 1) + ch) * ts;
const r = jsPhysicsIntegrate(md, ts, sc, {
dt, ch, pxc0: pxc, feetY0: feetY, vy0: st.vy || 0, wasOnGround: !!st.onGround,
jumpQueued: false, moveLeft: false, moveRight: false,
});
st.x = r.pxc / ts - cw * 0.5;
st.y = r.feetY / ts - ch;
st.vy = r.vy; st.onGround = r.onGround;
if (r.died) {
st.eliminated = true; st.hidden = false; st.vy = 0;
const p = space.peers.get(pid); if (p) p.jumpSurviveEliminated = true;
outBots.push({ id: pid, x: st.x, y: st.y, dir: st.dir || 'down', walking: false, elim: true });
} else {
outBots.push({ id: pid, x: st.x, y: st.y, dir: st.dir || 'down', walking: Math.abs(r.vy) > 40, elim: false });
}
}
}
if (now - s.lastEmit >= JS_BOT_EMIT_MS) {
s.lastEmit = now;
if (outBots.length) io.to(sid).emit('jump-survive-bot-move', { bots: outBots });
@@ -8661,30 +8791,45 @@ function stepSpaceShooterBots(sid, space, dt, now) {
const padX = ts * 0.6;
const outBots = [];
s.bots.forEach((b, id) => {
if (b.eliminated) return;
if (now >= b.wanderNext) {
b.wanderNext = now + 400 + Math.floor(Math.random() * 700);
b.wanderVx = Math.random() < 0.5 ? -1 : (Math.random() < 0.5 ? 1 : 0);
b.wanderVy = Math.random() < 0.5 ? -1 : 1;
/* : / " emit elim:true " return ( elim )
client พลาด packet = ไมาตาย ยานคางนงบนจอ. งซำท tick client อนไดแนนอน */
if (!b.eliminated) {
if (now >= b.wanderNext) {
b.wanderNext = now + 400 + Math.floor(Math.random() * 700);
b.wanderVx = Math.random() < 0.5 ? -1 : (Math.random() < 0.5 ? 1 : 0);
b.wanderVy = Math.random() < 0.5 ? -1 : 1;
}
const spd = b.tier === 'sharp' ? 195 : b.tier === 'weak' ? 115 : 155;
b.cx = Math.max(padX, Math.min(mw - padX, b.cx + b.wanderVx * spd * dt));
b.cy = ssClampCy(b.cy + b.wanderVy * spd * 0.55 * dt, mh);
b.fireCd -= dt;
if (b.fireCd <= 0) {
b.fireCd = b.tier === 'sharp' ? 0.22 : b.tier === 'weak' ? 0.48 : 0.34;
const bX = b.cx, bY = b.cy - 16;
io.to(sid).emit('space-shooter-shot', { x: bX, y: bY, vy: -500, ownerId: id, t0: now });
/* คะแนนบอท: โอกาสยิงโดน (ตาม tier) → +5 (ทำให้คะแนนสมเหตุผลโดยไม่ต้องจำลอง asteroid ฝั่ง server) */
const hitProb = b.tier === 'sharp' ? 0.6 : b.tier === 'weak' ? 0.28 : 0.42;
if (Math.random() < hitProb) b.score += 5;
}
}
const spd = b.tier === 'sharp' ? 195 : b.tier === 'weak' ? 115 : 155;
b.cx = Math.max(padX, Math.min(mw - padX, b.cx + b.wanderVx * spd * dt));
b.cy = ssClampCy(b.cy + b.wanderVy * spd * 0.55 * dt, mh);
b.fireCd -= dt;
if (b.fireCd <= 0) {
b.fireCd = b.tier === 'sharp' ? 0.22 : b.tier === 'weak' ? 0.48 : 0.34;
const bX = b.cx, bY = b.cy - 16;
io.to(sid).emit('space-shooter-shot', { x: bX, y: bY, vy: -500, ownerId: id, t0: now });
/* คะแนนบอท: โอกาสยิงโดน (ตาม tier) → +5 (ทำให้คะแนนสมเหตุผลโดยไม่ต้องจำลอง asteroid ฝั่ง server) */
const hitProb = b.tier === 'sharp' ? 0.6 : b.tier === 'weak' ? 0.28 : 0.42;
if (Math.random() < hitProb) b.score += 5;
}
outBots.push({ id, cx: b.cx, cy: b.cy, dir: 'up', score: b.score, elim: false, hits: b.ssHits || 0 });
outBots.push({ id, cx: b.cx, cy: b.cy, dir: 'up', score: b.score, elim: !!b.eliminated, hits: b.ssHits || 0 });
});
if (now - s.lastEmit >= SS_BOT_EMIT_MS) {
s.lastEmit = now;
if (outBots.length) io.to(sid).emit('space-shooter-bot-move', { bots: outBots });
}
/* [DBG-MG6move] จับสถานะบอททุก ~2.5วิ → STILL=server ไม่ขับ, DEAD=ตาย(client ต้องซ่อน), MOV=ปกติ */
if (!s._dbgMT || now - s._dbgMT > 2500) {
s._dbgMT = now;
const rows = [];
s.bots.forEach((b, id) => {
const mv = (b._ldx == null) ? '?' : ((Math.abs(b.cx - b._ldx) + Math.abs(b.cy - b._ldy) > 2) ? 'MOV' : 'STILL');
b._ldx = b.cx; b._ldy = b.cy;
const tag = (id.indexOf(JS_BOT_PREFIX) === 0 ? 'pv' + id.slice(JS_BOT_PREFIX.length) : 'TK:' + String(id).slice(0, 5));
rows.push(tag + (b.takeover ? 'tk' : '') + '·' + (b.eliminated ? 'DEAD' : mv));
});
glog(sid, space, 'mg6-move', '[' + rows.join(' ') + ']');
}
}
/* ============================================================
@@ -8835,7 +8980,7 @@ function stepBalloonBossBots(sid, space, dt, now) {
const outBots = [];
const alive = [];
s.bots.forEach((b, id) => {
if (b.eliminated) return;
if (b.eliminated) { outBots.push({ id, cx: b.cx, cy: b.cy, score: b.score, dmg: b.dmg, balloons: b.balloons, elim: true }); return; } /* ตายแล้ว: emit elim:true ต่อ (กัน client พลาด packet = ค้างนิ่ง) */
alive.push(b);
/* หลบบอส: ดริฟต์ "home" ออกจากบอสที่เคลื่อนเข้าใกล้ (แทน clamp ตำแหน่ง = ไม่ติดนิ่งขอบบอส) */
const hx = b.homeCx - boss.cx, hy = b.homeCy - boss.cy;
@@ -9050,6 +9195,27 @@ function pickLobbyBotSpawnServer(md, space, slotOrd) {
return { x: 1, y: 1 };
}
/* "" (BFS spawn slot) /strip (isolated walkable)
cache space (invalidate เม mapId เปลยน) */
function lobbyMainCompServer(md, space) {
const mid = space && space.mapId;
if (space && space._lobbyMainComp && space._lobbyMainCompMap === mid) return space._lobbyMainComp;
const w = md.width || 20, h = md.height || 15;
const isW = (x, y) => x >= 0 && x < w && y >= 0 && y < h && !(md.objects && md.objects[y] && md.objects[y][x] === 1);
const slots = parseLobbyPlayerSpawnsServer(md).filter(Boolean);
let seed = null;
for (const s of slots) { const sx = Math.floor(s.x), sy = Math.floor(s.y); if (isW(sx, sy)) { seed = [sx, sy]; break; } }
if (!seed && md.spawn && isW(Math.floor(md.spawn.x), Math.floor(md.spawn.y))) seed = [Math.floor(md.spawn.x), Math.floor(md.spawn.y)];
if (!seed) { for (let y = 0; y < h && !seed; y++) for (let x = 0; x < w; x++) { if (isW(x, y)) { seed = [x, y]; break; } } }
const comp = new Set();
if (seed) {
comp.add(seed[0] + ',' + seed[1]);
const q = [seed];
while (q.length) { const [a, b] = q.shift(); for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) { const nx = a + dx, ny = b + dy, k = nx + ',' + ny; if (!comp.has(k) && isW(nx, ny)) { comp.add(k); q.push([nx, ny]); } } }
}
if (space) { space._lobbyMainComp = comp; space._lobbyMainCompMap = mid; }
return comp;
}
/* เดินบอท lobby ของ space เดียว (เรียกจาก sweeper ทุก ~60ms) — time-based, ความเร็วคงที่ */
function stepLobbyBotsForSpace(sid, space, dt, nowMs) {
const md = lobbyMapOfSpace(space);
@@ -9075,7 +9241,17 @@ function stepLobbyBotsForSpace(sid, space, dt, nowMs) {
}
const w = md.width || 20, h = md.height || 15;
const step = LOBBY_MOVE_SPEED_PER_SEC * dt;
const mainComp = lobbyMainCompServer(md, space);
const inMain = (x, y) => !mainComp.size || mainComp.has(Math.floor(x) + ',' + Math.floor(y));
bots.forEach((b, id) => {
/* กันบอทไปติดเกาะแยก/strip ข้างฉาก (isolated) → ดึงกลับ spawn หลัก (main component) */
if (!inMain(b.x, b.y)) {
const slot = parseInt(id.slice(LOBBY_BOT_PREFIX.length), 10);
const sp = pickLobbyBotSpawnServer(md, space, slot);
if (inMain(sp.x, sp.y)) { b.x = sp.x + 0.5; b.y = sp.y + 0.5; }
else { const first = mainComp.values().next().value; if (first) { const [mx, my] = first.split(',').map(Number); b.x = mx + 0.5; b.y = my + 0.5; } }
b.wTX = null; b.wTY = null; b.retargetAt = 0; b.pauseUntil = 0;
}
const arrived = (typeof b.wTX === 'number') && Math.hypot(b.wTX - b.x, b.wTY - b.y) < 0.5;
if (typeof b.wTX !== 'number' || arrived || nowMs >= b.retargetAt) {
/* เป้า wander = สุ่มทั้งแมป (เหมือน client เดิม) — การเดินถูกจำกัดด้วยกำแพงอยู่แล้ว */
@@ -9816,7 +9992,7 @@ function attemptTroublesomeRoll(sid, force) {
const chosen = pool[Math.floor(Math.random() * pool.length)];
space.troublesomeTargetId = chosen;
space.troublesomeResponseReceived = false;
io.to(chosen).emit('troublesome-offer', { seconds: 15 });
io.to(chosen).emit('troublesome-offer', { seconds: clampTroublesomeOfferSec(runtimeGameTiming && runtimeGameTiming.troublesomeOfferSec) });
}
function scheduleTroublesomeRoll(sid) {
@@ -9825,7 +10001,8 @@ function scheduleTroublesomeRoll(sid) {
if (space.troublesomeDebTimer) clearTimeout(space.troublesomeDebTimer);
space.troublesomeDebTimer = setTimeout(() => attemptTroublesomeRoll(sid, false), 450);
if (!space.troublesomeMaxTimer) {
space.troublesomeMaxTimer = setTimeout(() => attemptTroublesomeRoll(sid, true), 8000);
const rollMaxMs = Math.max(1000, Math.round(clampTroublesomeRollMaxSec(runtimeGameTiming && runtimeGameTiming.troublesomeRollMaxSec) * 1000));
space.troublesomeMaxTimer = setTimeout(() => attemptTroublesomeRoll(sid, true), rollMaxMs);
}
}
@@ -10862,12 +11039,22 @@ io.on('connection', (socket) => {
if (!entry || entry.kind !== 'human' || String(entry.id) !== String(socket.id)) return; /* ต้องเป็นตาของคนนี้ */
const md = (space.mapId && maps.get(space.mapId)) || space.mapData;
if (!md || md.gameType !== 'stack') return;
/* (client ) half-RTT.
clamp นโกง: งใกลเฟส server ( latency ได ~±1.2rad 300ms@0.6Hz); = ใช s.phase */
/* (client ) half-RTT/latency.
sanity bound าง (STACK_DROP_PHASE_TOL = 2π) เหลไวนค garbage ( const) */
let dropPhase = s.phase;
const cph = data && Number(data.phase);
if (Number.isFinite(cph) && Math.abs(cph - s.phase) <= STACK_DROP_PHASE_TOL) dropPhase = cph;
const hit = stackComputeDropServer(s, md, dropPhase);
/* [DBG-MG3drop] เทียบ client-sent phase vs s.phase + จุดลง/รองรับ/overlap/miss/หัวใจ (ถอดออกหลังวินิจฉัย) */
try {
const supportCx = hit.placedCx - (hit.delta || 0);
glog(sid, space, 'mg3-drop', 'HUMAN cph=' + (Number.isFinite(cph) ? cph.toFixed(3) : 'x')
+ ' sPhase=' + s.phase.toFixed(3) + ' used=' + dropPhase.toFixed(3) + (dropPhase === cph ? '(client)' : '(server)')
+ ' amp=' + s.swingAmp.toFixed(3) + ' topCx=' + s.topCenterX.toFixed(3)
+ ' placedCx=' + hit.placedCx.toFixed(3) + ' supportCx=' + supportCx.toFixed(3)
+ ' overlap=' + (hit.overlap != null ? hit.overlap.toFixed(3) : '?') + ' miss=' + hit.miss + ' perfect=' + hit.perfect
+ ' layers=' + s.layers.length + ' hearts=' + s.teamMissesLeft);
} catch (e) { /* ignore */ }
stackApplyHitServer(s, hit, { id: entry.id });
const peer = space.peers.get(socket.id);
const theme = stackHumanThemeServer(peer) || (entry.theme || null);
@@ -10973,6 +11160,30 @@ io.on('connection', (socket) => {
}
});
/* MG5 (hidden): browser throttle setInterval ~1Hz emit move ~1/ = .
แก: client แจ hidden พรอม state จจ server จำลองการตก (jsPhysicsIntegrate) แทน ใน stepJumpSurviveBots
แล broadcast าน jump-survive-bot-move (full rate) คนอนเหนล. visible hidden:false client มเอง. */
socket.on('jump-survive-hidden', (data) => {
const sid = socket.data.spaceId;
const space = sid ? spaces.get(sid) : null;
if (!space || !data) return;
const s = space.jumpSurviveSession;
if (!s || !s.active || s.ended) return;
if (!s.players[socket.id]) s.players[socket.id] = {};
const st = s.players[socket.id];
if (data.hidden) {
if (st.eliminated) return;
st.hidden = true;
st.x = Number.isFinite(data.x) ? data.x : (Number.isFinite(st.x) ? st.x : 1);
st.y = Number.isFinite(data.y) ? data.y : (Number.isFinite(st.y) ? st.y : 1);
st.vy = Number.isFinite(data.vy) ? data.vy : 0;
st.onGround = !!data.onGround;
st.dir = data.direction || st.dir || 'down';
} else {
st.hidden = false;
}
});
/** เปลี่ยนชื่อที่แสดงใน lobby / เกม (ซิงก์ทุก client ในห้อง) */
socket.on('update-display-name', ({ nickname }, cb) => {
const reply = typeof cb === 'function' ? cb : () => {};
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff