fixed game synce
This commit is contained in:
@@ -111,20 +111,21 @@
|
||||
"providerUserId": "p_1777015659667_xqfjh247bgp",
|
||||
"notes": "auto: player-coins",
|
||||
"blocked": false,
|
||||
"coins": 330,
|
||||
"coins": 348,
|
||||
"createdAt": "2026-04-24T07:28:02+00:00",
|
||||
"updatedAt": "2026-06-20T17:54:37+00:00",
|
||||
"score": 330,
|
||||
"updatedAt": "2026-06-21T09:15:13+00:00",
|
||||
"score": 348,
|
||||
"scoreByCase": {
|
||||
"8": 220,
|
||||
"5": 30,
|
||||
"7": 60,
|
||||
"2": 10,
|
||||
"9": 10
|
||||
"9": 10,
|
||||
"10": 18
|
||||
},
|
||||
"lbName": "MONE",
|
||||
"achievements": {
|
||||
"d1_minigame_solver": 2
|
||||
"d1_minigame_solver": 4
|
||||
},
|
||||
"lobbyColorThemeIndex": 6,
|
||||
"lobbySkinToneIndex": 1
|
||||
@@ -535,19 +536,24 @@
|
||||
{
|
||||
"id": "80f31f73da0328dbf24e0206",
|
||||
"email": "",
|
||||
"displayName": "Guest",
|
||||
"displayName": "ผู้เล่น",
|
||||
"loginType": "guest",
|
||||
"providerUserId": "p_1781862203558_jhynnz90qrg",
|
||||
"notes": "auto: player-coins",
|
||||
"blocked": false,
|
||||
"coins": 0,
|
||||
"coins": 18,
|
||||
"createdAt": "2026-06-19T09:43:23+00:00",
|
||||
"updatedAt": "2026-06-20T17:27:35+00:00",
|
||||
"updatedAt": "2026-06-21T09:15:13+00:00",
|
||||
"lobbyColorThemeIndex": 6,
|
||||
"lobbySkinToneIndex": 1,
|
||||
"achievements": {
|
||||
"d1_minigame_solver": 1
|
||||
}
|
||||
"d1_minigame_solver": 2
|
||||
},
|
||||
"score": 18,
|
||||
"scoreByCase": {
|
||||
"10": 18
|
||||
},
|
||||
"lbName": "ผู้เล่น"
|
||||
},
|
||||
{
|
||||
"id": "fbb262e6c2aa47bd97aef554",
|
||||
@@ -562,6 +568,29 @@
|
||||
"updatedAt": "2026-06-20T06:58:53+00:00",
|
||||
"lobbyColorThemeIndex": 7,
|
||||
"lobbySkinToneIndex": 1
|
||||
},
|
||||
{
|
||||
"id": "8f0741532038e900e759c5ef",
|
||||
"email": "",
|
||||
"displayName": "MONE",
|
||||
"loginType": "guest",
|
||||
"providerUserId": "p_1782016990238_0u37bv7pxlxs",
|
||||
"notes": "auto: player-coins",
|
||||
"blocked": false,
|
||||
"coins": 20,
|
||||
"createdAt": "2026-06-21T04:43:11+00:00",
|
||||
"updatedAt": "2026-06-21T05:01:35+00:00",
|
||||
"lobbyColorThemeIndex": 8,
|
||||
"lobbySkinToneIndex": 1,
|
||||
"score": 20,
|
||||
"scoreByCase": {
|
||||
"13": 10,
|
||||
"10": 10
|
||||
},
|
||||
"lbName": "ผู้เล่น",
|
||||
"achievements": {
|
||||
"d1_minigame_solver": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+265
-70
@@ -145,6 +145,22 @@
|
||||
const SPACE_SHOOTER_ASTEROID_RADIUS_AT_FULL = 48;
|
||||
const SPACE_SHOOTER_ASTEROID_RADIUS_AT_MIN_HP = 11;
|
||||
|
||||
/* PRNG แบบ seed ได้ (mulberry32) — ใช้สุ่มสิ่งแวดล้อมมินิเกมแบบ deterministic
|
||||
seed เดียวกัน (อิง liveBeginsAt ของ server) → ทุกเครื่องได้ลำดับสุ่มเดียวกัน = อุกาบาต/บอลลูนชุดเดียวกัน */
|
||||
function makeSeededRng(seed) {
|
||||
let t = (seed >>> 0) || 1;
|
||||
return function () {
|
||||
t = (t + 0x6D2B79F5) >>> 0;
|
||||
let r = Math.imul(t ^ (t >>> 15), 1 | t);
|
||||
r = (r + Math.imul(r ^ (r >>> 7), 61 | r)) ^ r;
|
||||
return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
/* seed ต่อ index ของ spawn — รวม anchor (liveBeginsAt) กับ index ให้ก้อนแต่ละลำดับสุ่มต่างกันแต่ตรงกันทุกเครื่อง */
|
||||
function envSpawnSeed(anchorMs, index) {
|
||||
return (((anchorMs >>> 0) ^ Math.imul((index | 0) + 1, 2654435761)) >>> 0);
|
||||
}
|
||||
|
||||
function spaceShooterAsteroidRadiusFromHpPlay(hp) {
|
||||
const cap = SPACE_SHOOTER_ASTEROID_MAX_HP;
|
||||
const h = Math.max(0, Math.min(cap, Math.floor(Number(hp)) || 0));
|
||||
@@ -3428,6 +3444,9 @@
|
||||
let jumpSurviveEliminated = false;
|
||||
/** จับเวลาโหมดกระโดดให้รอด (วินาทีบน HUD) */
|
||||
let jumpSurviveSessionStartMs = 0;
|
||||
/* เวลา server (epoch ms) ที่ live เริ่มจริง — ใช้คำนวณ scroll/เวลานับถอยหลังจาก serverNow() ร่วมกัน
|
||||
ทุกเครื่องเห็นแพลตฟอร์มเลื่อนเท่ากัน + จบพร้อมกัน (เดิมสะสม dt local → เลื่อน/จบไม่ตรงกัน = desync) */
|
||||
let jumpSurviveLiveAnchorServerMs = 0;
|
||||
let jumpSurvivePlatformScrollPx = 0;
|
||||
let jumpSurviveVy = 0;
|
||||
let jumpSurviveLastTickMs = 0;
|
||||
@@ -3447,6 +3466,9 @@
|
||||
let spaceShooterPopups = [];
|
||||
let spaceShooterLastTickMs = 0;
|
||||
let spaceShooterSpawnAccMs = 0;
|
||||
/* เวลา server ที่ live เริ่ม + ตัวนับลำดับก้อนที่เกิดแล้ว — ใช้ทำ deterministic spawn ให้ทุกเครื่องตรงกัน */
|
||||
let spaceShooterLiveAnchorServerMs = 0;
|
||||
let spaceShooterSpawnCount = 0;
|
||||
let spaceShooterFireCd = 0;
|
||||
let spaceShooterSessionStartMs = 0;
|
||||
let spaceShooterLastMoveEmit = 0;
|
||||
@@ -3541,6 +3563,9 @@
|
||||
/** Mega Virus — ป๊อปดาเมจต่อบอส (ข้อความ +2) เมื่อกระสุนโดนบอส */
|
||||
let balloonBossScorePopups = [];
|
||||
let balloonBossBossFireAcc = 0;
|
||||
/* anchor เวลา server + ตัวนับกระสุนบอส — deterministic boss fire ให้ทุกเครื่องยิงทิศ/เวลาเดียวกัน */
|
||||
let balloonBossLiveAnchorServerMs = 0;
|
||||
let balloonBossBossShotCount = 0;
|
||||
/** quiz_battle — โดม MCQ กด E (ข้อจาก battleQuizMcq) */
|
||||
let quizBattleMcqPool = [];
|
||||
let quizBattleAnsweredComps = new Set();
|
||||
@@ -7092,7 +7117,10 @@
|
||||
if (!isJumpSurviveMissionUiMapPlay() || jumpSurviveMissionPhase !== 'live' || jumpSurviveSessionStartMs <= 0) return null;
|
||||
const lim = jumpSurviveTimeLimitSecForMission();
|
||||
if (!(lim > 0)) return null;
|
||||
const elapsed = (performance.now() - jumpSurviveSessionStartMs) / 1000;
|
||||
/* นับเวลาจาก anchor server (serverNow) → จบพร้อมกันทุกเครื่อง; fallback local ถ้ายังไม่มี anchor */
|
||||
const elapsed = jumpSurviveLiveAnchorServerMs > 0
|
||||
? (serverNow() - jumpSurviveLiveAnchorServerMs) / 1000
|
||||
: (performance.now() - jumpSurviveSessionStartMs) / 1000;
|
||||
return Math.max(0, Math.ceil(lim - elapsed));
|
||||
}
|
||||
|
||||
@@ -7182,6 +7210,9 @@
|
||||
jumpSurviveMissionPhase = 'live';
|
||||
jumpSurviveGameEnded = false;
|
||||
jumpSurviveEliminated = false;
|
||||
/* anchor เวลาเริ่ม live = liveBeginsAt ของ server (ถ้ามี) → scroll/timer ของทุกเครื่องอ้างอิงจุดเดียวกัน */
|
||||
jumpSurviveLiveAnchorServerMs = playMissionLiveBeginsAt > 0 ? playMissionLiveBeginsAt : serverNow();
|
||||
jumpSurvivePlatformScrollPx = 0;
|
||||
applyJumpSurvivePreviewSpawnLayout(false);
|
||||
};
|
||||
if (!cd || !numEl) {
|
||||
@@ -7564,6 +7595,9 @@
|
||||
spaceShooterPopups = [];
|
||||
spaceShooterLastTickMs = performance.now();
|
||||
spaceShooterSpawnAccMs = 0;
|
||||
spaceShooterSpawnCount = 0;
|
||||
/* anchor = liveBeginsAt ของ server (ทุกเครื่องได้ค่าเดียวกัน) → spawn/timer อ้างอิงจุดเดียวกัน */
|
||||
spaceShooterLiveAnchorServerMs = playMissionLiveBeginsAt > 0 ? playMissionLiveBeginsAt : serverNow();
|
||||
spaceShooterFireCd = 0;
|
||||
spaceShooterSessionStartMs = performance.now();
|
||||
spaceShooterLastMoveEmit = 0;
|
||||
@@ -8967,12 +9001,19 @@
|
||||
}
|
||||
|
||||
function quizCarryPregameHumanIds() {
|
||||
const ids = [];
|
||||
if (myId != null && !isPreviewBotId(myId)) ids.push(String(myId));
|
||||
const set = new Set();
|
||||
if (myId != null && !isPreviewBotId(myId)) set.add(String(myId));
|
||||
others.forEach((_, id) => {
|
||||
if (!isPreviewBotId(id)) ids.push(String(id));
|
||||
if (!isPreviewBotId(id)) set.add(String(id));
|
||||
});
|
||||
return ids;
|
||||
/* รวม id จาก readyMap ของ server ด้วย — กันกรณี host เข้า pregame ก่อน client reconnect (others ยังว่าง)
|
||||
ทำให้ host นับ/รอ client ครบจริง ไม่เริ่มคนเดียว และเห็นจำนวนผู้เล่นถูกต้อง */
|
||||
if (quizCarryLobbyReadyMap && typeof quizCarryLobbyReadyMap === 'object') {
|
||||
Object.keys(quizCarryLobbyReadyMap).forEach((id) => {
|
||||
if (id && !isPreviewBotId(id)) set.add(String(id));
|
||||
});
|
||||
}
|
||||
return [...set];
|
||||
}
|
||||
|
||||
function quizCarryPregameBotCount() {
|
||||
@@ -9126,14 +9167,16 @@
|
||||
primaryImg.alt = humansReady ? 'START' : 'READY';
|
||||
}
|
||||
if (primary) {
|
||||
/* READY phase: ทุกคน (รวม client) กดสลับ Ready ของตัวเองได้ | START phase: เฉพาะ host กดเริ่ม */
|
||||
const canPress = humansReady ? isMePlayHost() : true;
|
||||
primary.classList.toggle('is-start-phase', humansReady);
|
||||
primary.classList.toggle('is-read-only', !isMePlayHost());
|
||||
primary.classList.toggle('is-pressed', isMePlayHost() && meReady && !humansReady);
|
||||
primary.disabled = !isMePlayHost();
|
||||
primary.classList.toggle('is-read-only', !canPress);
|
||||
primary.classList.toggle('is-pressed', meReady && !humansReady);
|
||||
primary.disabled = !canPress;
|
||||
primary.setAttribute('aria-pressed', humansReady ? 'false' : (meReady ? 'true' : 'false'));
|
||||
primary.title = isMePlayHost()
|
||||
? (humansReady ? 'START' : (meReady ? 'ยกเลิก READY' : 'READY'))
|
||||
: (humansReady ? 'START (โฮสต์เท่านั้น)' : 'READY (โฮสต์เท่านั้น)');
|
||||
primary.title = humansReady
|
||||
? (isMePlayHost() ? 'START' : 'รอโฮสต์กด START')
|
||||
: (meReady ? 'ยกเลิก READY' : 'READY');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9143,8 +9186,8 @@
|
||||
quizCarryLobbyReadyMap = {};
|
||||
quizCarryPregameHumanIds().forEach((id) => { quizCarryLobbyReadyMap[id] = false; });
|
||||
showQuizCarryPregameOverlay();
|
||||
/* ขอสถานะ ready ปัจจุบันจาก server (authoritative) — ทุกคนรวม host ต้องกด READY เองตาม flow */
|
||||
if (socket && socket.connected) socket.emit('quiz-carry-lobby-sync-request');
|
||||
quizCarrySyncGuestReadyIfNeeded();
|
||||
updateQuizCarryPregameHud();
|
||||
}
|
||||
|
||||
@@ -9361,6 +9404,43 @@
|
||||
syncQuizCarryEmbedQuestionStrip();
|
||||
}
|
||||
|
||||
/* networked mission (quiz_carry detective): เฉพาะ host เลือกคำถาม + broadcast ให้คนอื่น
|
||||
กันแต่ละเครื่องสุ่มคนละข้อ (เดิม client สุ่ม Math.random() เอง → host/client เห็นคนละคำถาม) */
|
||||
function quizCarryIsNetworkedMission() {
|
||||
return quizCarryEmbedMissionFlowActive() && socket && socket.connected;
|
||||
}
|
||||
|
||||
/* ตั้งคำถามถัดไป (embed/detective flow) จาก q ที่ "เลือกแล้ว" — ใช้ได้ทั้ง host (หลังสุ่ม) และ client (รับ broadcast) */
|
||||
function applyQuizCarryEmbedNextQuestion(q) {
|
||||
if (!q) return;
|
||||
quizCarryEmbedPendingQuestion = q;
|
||||
preloadQuizCarryChoiceImages(q);
|
||||
quizCarryEmbedCountdownStartAt = Date.now();
|
||||
quizCarryEmbedCountdownEndAt = quizCarryEmbedCountdownStartAt + 3000;
|
||||
quizCarryEmbedPreOptionCountdownStartAt = 0;
|
||||
quizCarryEmbedPreOptionCountdownEndAt = 0;
|
||||
quizCarryCurrent = null;
|
||||
playQuizText = '';
|
||||
clearQuizMapQuestionCarryFeedback();
|
||||
quizCarryOptionRevealAt = 0;
|
||||
quizCarryAnswerCloseAt = 0;
|
||||
me.quizCarryHeld = null;
|
||||
others.forEach((o) => {
|
||||
if (!o) return;
|
||||
o.quizCarryHeld = null;
|
||||
o.botPath = [];
|
||||
o.botPathStuckTicks = 0;
|
||||
/* ต้องใช้ performance.now() ช่วงกับ stepQuizCarryPreviewBots — ห้ามใช้ Date.now() epoch ไม่งั้นบอทค้าง pathfind ตลอด */
|
||||
o.botQuizCarryPathfindAfter = 0;
|
||||
});
|
||||
const qEl = document.getElementById('quiz-game-question');
|
||||
if (qEl) qEl.textContent = '…';
|
||||
const phaseEl = document.getElementById('quiz-game-phase-label');
|
||||
if (phaseEl) phaseEl.textContent = 'รอเริ่มคำถาม — นับถอยหลัง 3 · 2 · 1';
|
||||
tickQuizCarryEmbedCountdown();
|
||||
syncQuizCarryEmbedQuestionStrip();
|
||||
}
|
||||
|
||||
function quizCarryPickNextQuestion() {
|
||||
if (quizCarrySessionEnded) return;
|
||||
if (quizCarryPregameActive) return;
|
||||
@@ -9377,39 +9457,20 @@
|
||||
playQuizText = 'ไม่มีคำถาม — ใส่ quizQuestions ในแมป (choices + correctIndex) หรือตั้งใน Admin';
|
||||
return;
|
||||
}
|
||||
/* client ที่ไม่ใช่ host ในห้องจริง: ไม่สุ่มเอง รอ event 'quiz-carry-question' จาก host */
|
||||
if (quizCarryIsNetworkedMission() && !isMePlayHost()) return;
|
||||
let q;
|
||||
let qIdx = 0;
|
||||
let guard = 0;
|
||||
do {
|
||||
q = quizCarryPool[Math.floor(Math.random() * quizCarryPool.length)];
|
||||
qIdx = Math.floor(Math.random() * quizCarryPool.length);
|
||||
q = quizCarryPool[qIdx];
|
||||
guard++;
|
||||
} while (guard < 12 && quizCarryPool.length > 1 && quizCarryCurrent && q && q.text === quizCarryCurrent.text);
|
||||
if (quizCarryEmbedMissionFlowActive()) {
|
||||
quizCarryEmbedPendingQuestion = q;
|
||||
preloadQuizCarryChoiceImages(q);
|
||||
quizCarryEmbedCountdownStartAt = Date.now();
|
||||
quizCarryEmbedCountdownEndAt = quizCarryEmbedCountdownStartAt + 3000;
|
||||
quizCarryEmbedPreOptionCountdownStartAt = 0;
|
||||
quizCarryEmbedPreOptionCountdownEndAt = 0;
|
||||
quizCarryCurrent = null;
|
||||
playQuizText = '';
|
||||
clearQuizMapQuestionCarryFeedback();
|
||||
quizCarryOptionRevealAt = 0;
|
||||
quizCarryAnswerCloseAt = 0;
|
||||
me.quizCarryHeld = null;
|
||||
others.forEach((o) => {
|
||||
if (!o) return;
|
||||
o.quizCarryHeld = null;
|
||||
o.botPath = [];
|
||||
o.botPathStuckTicks = 0;
|
||||
/* ต้องใช้ performance.now() ช่วงกับ stepQuizCarryPreviewBots — ห้ามใช้ Date.now() epoch ไม่งั้นบอทค้าง pathfind ตลอด */
|
||||
o.botQuizCarryPathfindAfter = 0;
|
||||
});
|
||||
const qEl = document.getElementById('quiz-game-question');
|
||||
if (qEl) qEl.textContent = '…';
|
||||
const phaseEl = document.getElementById('quiz-game-phase-label');
|
||||
if (phaseEl) phaseEl.textContent = 'รอเริ่มคำถาม — นับถอยหลัง 3 · 2 · 1';
|
||||
tickQuizCarryEmbedCountdown();
|
||||
syncQuizCarryEmbedQuestionStrip();
|
||||
/* host แจ้ง index คำถามให้ทุกคนใช้ข้อเดียวกัน */
|
||||
if (socket && socket.connected) socket.emit('quiz-carry-question', { idx: qIdx });
|
||||
applyQuizCarryEmbedNextQuestion(q);
|
||||
return;
|
||||
}
|
||||
quizCarryCurrent = q;
|
||||
@@ -9433,6 +9494,9 @@
|
||||
finishDetectiveMinigameAndReturnLobby();
|
||||
return;
|
||||
}
|
||||
/* ห้องจริง: เฉพาะ host คุมการเลื่อนรอบ/จบเซสชัน แล้ว broadcast — client อื่นรอ event ไม่เลื่อนเอง
|
||||
(กันแต่ละเครื่องเลื่อนรอบคนละจังหวะ/คนละคำถาม) */
|
||||
if (quizCarryIsNetworkedMission() && !isMePlayHost()) return;
|
||||
if (!quizCarryEmbedMissionFlowActive() && !previewMode) {
|
||||
quizCarryPickNextQuestion();
|
||||
return;
|
||||
@@ -9440,6 +9504,7 @@
|
||||
quizCarryRoundsCompleted++;
|
||||
const lim = quizCarrySessionLength;
|
||||
if (lim > 0 && quizCarryRoundsCompleted >= lim) {
|
||||
if (socket && socket.connected) socket.emit('quiz-carry-session-complete', {});
|
||||
showQuizCarrySessionCompleteOverlay();
|
||||
return;
|
||||
}
|
||||
@@ -12536,6 +12601,9 @@
|
||||
function applyMegaBalloonLiveStartPlay() {
|
||||
balloonBossSessionStartMs = performance.now();
|
||||
balloonBossLastTickMs = performance.now();
|
||||
/* anchor = liveBeginsAt server → boss fire + timer อ้างอิงจุดเดียวกันทุกเครื่อง */
|
||||
balloonBossLiveAnchorServerMs = playMissionLiveBeginsAt > 0 ? playMissionLiveBeginsAt : serverNow();
|
||||
balloonBossBossShotCount = 0;
|
||||
balloonBossGameEnded = false;
|
||||
balloonBossPendingShots = [];
|
||||
balloonBossPlayerBullets = [];
|
||||
@@ -15506,7 +15574,10 @@
|
||||
if (isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase !== 'live') return null;
|
||||
const lim = spaceShooterTimeLimitSecPlay();
|
||||
if (lim <= 0) return null;
|
||||
const elapsed = (performance.now() - spaceShooterSessionStartMs) / 1000;
|
||||
/* นับเวลาจาก anchor server → จบพร้อมกันทุกเครื่อง; fallback local ถ้ายังไม่มี anchor (preview) */
|
||||
const elapsed = spaceShooterLiveAnchorServerMs > 0
|
||||
? (serverNow() - spaceShooterLiveAnchorServerMs) / 1000
|
||||
: (performance.now() - spaceShooterSessionStartMs) / 1000;
|
||||
return Math.max(0, Math.ceil(lim - elapsed));
|
||||
}
|
||||
|
||||
@@ -15654,19 +15725,46 @@
|
||||
}
|
||||
|
||||
const interval = spaceShooterAsteroidSpawnIntervalMsPlay();
|
||||
spaceShooterSpawnAccMs += dt * 1000;
|
||||
while (spaceShooterSpawnAccMs >= interval) {
|
||||
spaceShooterSpawnAccMs -= interval;
|
||||
const maxHp = 1 + Math.floor(Math.random() * SPACE_SHOOTER_ASTEROID_MAX_HP);
|
||||
const hp0 = maxHp;
|
||||
spaceShooterAsteroids.push({
|
||||
x: 36 + Math.random() * (mw - 72),
|
||||
y: -45 - Math.random() * 90,
|
||||
r: spaceShooterAsteroidRadiusFromHpPlay(hp0),
|
||||
vy: 58 + Math.random() * 62,
|
||||
maxHp: maxHp,
|
||||
hp: hp0,
|
||||
});
|
||||
if (spaceShooterLiveAnchorServerMs > 0) {
|
||||
/* deterministic spawn: ก้อนลำดับ k เกิดที่เวลา anchor + k*interval ด้วยค่าสุ่มจาก seed(anchor,k)
|
||||
→ ทุกเครื่องเกิดก้อนชุดเดียวกัน ตำแหน่ง/HP/ความเร็วเท่ากัน ที่เวลา server เดียวกัน */
|
||||
const targetCount = Math.floor((serverNow() - spaceShooterLiveAnchorServerMs) / interval) + 1;
|
||||
let guard = 0;
|
||||
while (spaceShooterSpawnCount < targetCount && guard < 200) {
|
||||
guard++;
|
||||
const k = spaceShooterSpawnCount++;
|
||||
const rng = makeSeededRng(envSpawnSeed(spaceShooterLiveAnchorServerMs, k));
|
||||
const maxHp = 1 + Math.floor(rng() * SPACE_SHOOTER_ASTEROID_MAX_HP);
|
||||
const ax = 36 + rng() * (mw - 72);
|
||||
const baseY = -45 - rng() * 90;
|
||||
const avy = 58 + rng() * 62;
|
||||
const spawnTimeMs = spaceShooterLiveAnchorServerMs + k * interval;
|
||||
const elapsedSec = Math.max(0, (serverNow() - spawnTimeMs) / 1000);
|
||||
spaceShooterAsteroids.push({
|
||||
id: k,
|
||||
x: ax,
|
||||
y: baseY + avy * elapsedSec,
|
||||
r: spaceShooterAsteroidRadiusFromHpPlay(maxHp),
|
||||
vy: avy,
|
||||
maxHp: maxHp,
|
||||
hp: maxHp,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
spaceShooterSpawnAccMs += dt * 1000;
|
||||
while (spaceShooterSpawnAccMs >= interval) {
|
||||
spaceShooterSpawnAccMs -= interval;
|
||||
const maxHp = 1 + Math.floor(Math.random() * SPACE_SHOOTER_ASTEROID_MAX_HP);
|
||||
const hp0 = maxHp;
|
||||
spaceShooterAsteroids.push({
|
||||
x: 36 + Math.random() * (mw - 72),
|
||||
y: -45 - Math.random() * 90,
|
||||
r: spaceShooterAsteroidRadiusFromHpPlay(hp0),
|
||||
vy: 58 + Math.random() * 62,
|
||||
maxHp: maxHp,
|
||||
hp: hp0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
others.forEach((o, id) => {
|
||||
@@ -16177,8 +16275,11 @@
|
||||
const lim = balloonBossTimeLimitSecPlay();
|
||||
if (lim <= 0) return null;
|
||||
if (isMegaVirusMissionShellMapPlay() && isGauntletCrownPregameBlockingPlay()) return lim;
|
||||
if (!balloonBossSessionStartMs) return lim;
|
||||
const elapsed = (performance.now() - balloonBossSessionStartMs) / 1000;
|
||||
if (!balloonBossSessionStartMs && !(balloonBossLiveAnchorServerMs > 0)) return lim;
|
||||
/* นับเวลาจาก anchor server → จบพร้อมกันทุกเครื่อง; fallback local ถ้ายังไม่มี anchor */
|
||||
const elapsed = balloonBossLiveAnchorServerMs > 0
|
||||
? (serverNow() - balloonBossLiveAnchorServerMs) / 1000
|
||||
: (performance.now() - balloonBossSessionStartMs) / 1000;
|
||||
return Math.max(0, Math.ceil(lim - elapsed));
|
||||
}
|
||||
|
||||
@@ -16436,18 +16537,40 @@
|
||||
}
|
||||
|
||||
const bossFireEvery = 1.42;
|
||||
balloonBossBossFireAcc += dt;
|
||||
while (balloonBossBossFireAcc >= bossFireEvery) {
|
||||
balloonBossBossFireAcc -= bossFireEvery;
|
||||
if (aliveRefs.length) {
|
||||
const ang = Math.random() * Math.PI * 2;
|
||||
const spd = 88 + Math.random() * 72;
|
||||
if (balloonBossLiveAnchorServerMs > 0) {
|
||||
/* deterministic boss fire: กระสุนลำดับ k ยิงที่ anchor + k*bossFireEvery ด้วยค่าสุ่มจาก seed(anchor,k)
|
||||
→ ทุกเครื่องเห็นแพตเทิร์นกระสุนบอสเดียวกัน (เดิม Math.random แยกกัน = คนละลาย) */
|
||||
const fireIntervalMs = bossFireEvery * 1000;
|
||||
const targetShots = Math.floor((serverNow() - balloonBossLiveAnchorServerMs) / fireIntervalMs) + 1;
|
||||
let bguard = 0;
|
||||
while (balloonBossBossShotCount < targetShots && bguard < 200) {
|
||||
bguard++;
|
||||
const k = balloonBossBossShotCount++;
|
||||
if (!aliveRefs.length) continue;
|
||||
const rng = makeSeededRng(envSpawnSeed(balloonBossLiveAnchorServerMs, k));
|
||||
const ang = rng() * Math.PI * 2;
|
||||
const spd = 88 + rng() * 72;
|
||||
const gy = 48 + rng() * 56;
|
||||
balloonBossBossBullets.push({
|
||||
x: bossC.cx, y: bossC.cy,
|
||||
vx: Math.cos(ang) * spd, vy: Math.sin(ang) * spd,
|
||||
gy: 48 + Math.random() * 56,
|
||||
gy: gy,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
balloonBossBossFireAcc += dt;
|
||||
while (balloonBossBossFireAcc >= bossFireEvery) {
|
||||
balloonBossBossFireAcc -= bossFireEvery;
|
||||
if (aliveRefs.length) {
|
||||
const ang = Math.random() * Math.PI * 2;
|
||||
const spd = 88 + Math.random() * 72;
|
||||
balloonBossBossBullets.push({
|
||||
x: bossC.cx, y: bossC.cy,
|
||||
vx: Math.cos(ang) * spd, vy: Math.sin(ang) * spd,
|
||||
gy: 48 + Math.random() * 56,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyBalloonBossHitToEntity(ent) {
|
||||
@@ -17827,8 +17950,14 @@
|
||||
let zJumpSurviveWorld = zoom;
|
||||
if (previewMode && editorEmbedReturn && mapData) zJumpSurviveWorld *= playEmbedUserZoomMul;
|
||||
const halfViewH = canvas.height / (2 * zJumpSurviveWorld);
|
||||
const rise = (Number(md.jumpSurviveRisePxPerSec) > 0 ? Number(md.jumpSurviveRisePxPerSec) : 42) * dt;
|
||||
jumpSurvivePlatformScrollPx += rise;
|
||||
const risePerSec = Number(md.jumpSurviveRisePxPerSec) > 0 ? Number(md.jumpSurviveRisePxPerSec) : 42;
|
||||
if (jumpSurviveLiveAnchorServerMs > 0) {
|
||||
/* scroll = ฟังก์ชันของเวลา server (ไม่สะสม dt local) → ทุกเครื่องเลื่อนเท่ากันที่เวลาเดียวกัน */
|
||||
const scrollSec = Math.max(0, (serverNow() - jumpSurviveLiveAnchorServerMs) / 1000);
|
||||
jumpSurvivePlatformScrollPx = scrollSec * risePerSec;
|
||||
} else {
|
||||
jumpSurvivePlatformScrollPx += risePerSec * dt;
|
||||
}
|
||||
|
||||
const camTgt = jumpSurviveCameraTargetPx(md);
|
||||
const ck = Math.min(1, dt * 7);
|
||||
@@ -18714,8 +18843,35 @@
|
||||
socket.on('quiz-carry-lobby-sync', (d) => {
|
||||
if (!d || typeof d.readyMap !== 'object') return;
|
||||
quizCarryLobbyReadyMap = { ...d.readyMap };
|
||||
/* upsert others ที่ขาดหายจาก peers snapshot ที่ server แนบมา
|
||||
กัน host ไม่เห็นตัวละคร client เมื่อ user-joined/plist มาไม่ครบช่วงเปลี่ยนหน้า lobbyB→play */
|
||||
if (Array.isArray(d.peers)) {
|
||||
d.peers.forEach((p) => {
|
||||
if (!p || p.id == null || isPreviewBotId(p.id)) return;
|
||||
if (myId != null && String(p.id) === String(myId)) return;
|
||||
if (others.has(p.id) || others.has(String(p.id))) return;
|
||||
const px = Number(p.x);
|
||||
const py = Number(p.y);
|
||||
const x = Number.isFinite(px) ? px : 1;
|
||||
const y = Number.isFinite(py) ? py : 1;
|
||||
const joP = Number(p.spawnJoinOrder);
|
||||
others.set(p.id, {
|
||||
x, y, tx: x, ty: y,
|
||||
direction: p.direction || 'down',
|
||||
nickname: p.nickname,
|
||||
characterId: p.characterId ?? null,
|
||||
spawnJoinOrder: Number.isFinite(joP) ? Math.max(0, Math.floor(joP)) : 0,
|
||||
playTint: playTintFromJoinPeer(p) || playTintForPreviewBotId(p.id) || resolvePlayTintForPeer(p.id, p),
|
||||
gauntletJumpTicks: 0, gauntletJumpVis: 0, gauntletScore: 0,
|
||||
jumpSurviveEliminated: false, spaceShooterScore: 0,
|
||||
balloonBossScore: 0, balloonBossBossDmg: 0,
|
||||
balloonBossBalloons: balloonBossBalloonsStartPlay(),
|
||||
balloonBossEliminated: false, quizCarryHeld: null,
|
||||
});
|
||||
if (p.characterId) preloadPlayCharacterLayers(p.characterId);
|
||||
});
|
||||
}
|
||||
if (quizCarryPregameActive && isQuizCarry()) {
|
||||
quizCarrySyncGuestReadyIfNeeded();
|
||||
updateQuizCarryPregameHud();
|
||||
}
|
||||
});
|
||||
@@ -18724,6 +18880,18 @@
|
||||
if (!quizCarryEmbedMissionFlowActive()) return;
|
||||
endQuizCarryEmbedPregameAndStart();
|
||||
});
|
||||
/* host เลือกคำถาม → client อื่นใช้ข้อเดียวกัน (กันสุ่มไม่ตรงกัน) */
|
||||
socket.on('quiz-carry-question', (d) => {
|
||||
if (!d || isMePlayHost() || !isQuizCarry() || !quizCarryEmbedMissionFlowActive()) return;
|
||||
const idx = Number(d.idx);
|
||||
if (!Number.isInteger(idx) || idx < 0 || idx >= quizCarryPool.length) return;
|
||||
applyQuizCarryEmbedNextQuestion(quizCarryPool[idx]);
|
||||
});
|
||||
/* host จบเซสชัน → client อื่นโชว์สรุปพร้อมกัน */
|
||||
socket.on('quiz-carry-session-complete', () => {
|
||||
if (isMePlayHost() || !isQuizCarry() || !quizCarryEmbedMissionFlowActive()) return;
|
||||
showQuizCarrySessionCompleteOverlay();
|
||||
});
|
||||
socket.on('gauntlet-crown-lobby-sync', (d) => {
|
||||
if (!d || typeof d.readyMap !== 'object') return;
|
||||
gauntletCrownLobbyReadyMap = { ...d.readyMap };
|
||||
@@ -18888,7 +19056,9 @@
|
||||
} else {
|
||||
o.tx = data.x;
|
||||
o.ty = data.y;
|
||||
if (mapData && isQuiz()) {
|
||||
/* quiz + quiz_carry: ผู้เล่นยืน/ขยับเป็นช่องบนแมปคงที่ → สแนปตำแหน่งทันที (ไม่พึ่ง lerp อย่างเดียว)
|
||||
กัน host ค้าง/หายจากจอเมื่อ move มาห่างหรือมาไม่ต่อเนื่อง */
|
||||
if (mapData && (isQuiz() || isQuizCarry())) {
|
||||
const px = Number(data.x);
|
||||
const py = Number(data.y);
|
||||
if (Number.isFinite(px)) o.x = px;
|
||||
@@ -19319,6 +19489,28 @@
|
||||
o.gauntletEliminated = false;
|
||||
});
|
||||
}
|
||||
/* quiz_carry: sync ตำแหน่งจุดเกิดของผู้เล่นจริงระยะไกลจาก peersSnap ตอนเข้าเกม
|
||||
เดิม client apply peersSnap เฉพาะ gauntlet → quiz_carry ผู้เล่นยืนนิ่งตอบคำถาม ไม่มี move ส่ง
|
||||
→ others ค้างที่ตำแหน่งเก่า (lobby) นอกแมป quiz_carry = host หายจากจอ client */
|
||||
if (mapData.gameType === 'quiz_carry' && ev.peersSnap && Array.isArray(ev.peersSnap)) {
|
||||
ev.peersSnap.forEach((p) => {
|
||||
if (p.id == null) return;
|
||||
const px = Number(p.x);
|
||||
const py = Number(p.y);
|
||||
if (myId != null && String(p.id) === String(myId)) {
|
||||
if (Number.isFinite(px)) { me.x = px; me.tx = px; }
|
||||
if (Number.isFinite(py)) { me.y = py; me.ty = py; }
|
||||
if (p.direction) me.direction = p.direction;
|
||||
} else {
|
||||
const o = others.get(p.id) ?? others.get(String(p.id));
|
||||
if (o) {
|
||||
if (Number.isFinite(px)) { o.x = px; o.tx = px; }
|
||||
if (Number.isFinite(py)) { o.y = py; o.ty = py; }
|
||||
if (p.direction) o.direction = p.direction;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (mapData.gameType === 'gauntlet') {
|
||||
if (ev.gauntletEndsAt != null) {
|
||||
const ge = Number(ev.gauntletEndsAt);
|
||||
@@ -23696,12 +23888,14 @@
|
||||
if (!primary) return;
|
||||
primary.addEventListener('click', () => {
|
||||
if (!quizCarryPregameActive || myId == null) return;
|
||||
if (!isMePlayHost()) return;
|
||||
const humans = quizCarryPregameHumanIds();
|
||||
const humansReady = humans.length > 0 && humans.every((id) => !!quizCarryLobbyReadyMap[id]);
|
||||
if (humansReady) {
|
||||
/* ผู้เล่นจริงคนเดียว + พรีวิว: ไม่ต้องรอ ACK เซิร์ฟ (ห้องที่ map บนเซิร์ฟยังไม่ใช่ quiz_carry จะเคยล้มเหลว) */
|
||||
if (humans.length === 1 && isMePlayHost()) {
|
||||
/* ทุกคนพร้อมแล้ว → เฉพาะ host กด START */
|
||||
if (!isMePlayHost()) return;
|
||||
/* เฉพาะ offline/พรีวิวที่ไม่ได้ต่อเซิร์ฟเวอร์เท่านั้นที่เริ่มได้เลย — ห้องจริงต้องผ่าน server
|
||||
(กัน host เริ่มคนเดียวทิ้ง client ตอน others ยังว่าง) */
|
||||
if ((!socket || !socket.connected) && humans.length === 1) {
|
||||
endQuizCarryEmbedPregameAndStart();
|
||||
return;
|
||||
}
|
||||
@@ -23710,11 +23904,12 @@
|
||||
});
|
||||
return;
|
||||
}
|
||||
/* READY phase: ทุกคน (รวม client) กดสลับสถานะ Ready ของตัวเองได้ */
|
||||
const sid = String(myId);
|
||||
const next = !quizCarryLobbyReadyMap[sid];
|
||||
quizCarryLobbyReadyMap[sid] = next;
|
||||
updateQuizCarryPregameHud();
|
||||
socket.emit('quiz-carry-lobby-ready', { ready: next });
|
||||
if (socket && socket.connected) socket.emit('quiz-carry-lobby-ready', { ready: next });
|
||||
});
|
||||
})();
|
||||
window.addEventListener('beforeunload', () => { try { saveQuizBattleProgress(); } catch (e) { /* ignore */ } socket.emit('leave-space'); });
|
||||
|
||||
@@ -5205,7 +5205,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.006131844"></script>
|
||||
<script src="js/play.js?v=0.006131850"></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)
|
||||
|
||||
+39
-4
@@ -3212,6 +3212,16 @@ function buildPeersSnapForSpace(space, extraFields) {
|
||||
});
|
||||
}
|
||||
|
||||
/* payload ของ quiz-carry-lobby-sync — แนบ peers snapshot ไปด้วยเสมอ
|
||||
→ client (โดยเฉพาะ host) upsert others ที่ขาดหายให้ครบ กัน host ไม่เห็นตัวละคร client
|
||||
เมื่อ join/reconnect ไม่ทันรอบ (race ตอนเปลี่ยนหน้า lobbyB→play.html) */
|
||||
function quizCarryLobbySyncPayload(space) {
|
||||
const readyMap = (space && space.quizCarryLobbyReady && typeof space.quizCarryLobbyReady === 'object')
|
||||
? { ...space.quizCarryLobbyReady }
|
||||
: {};
|
||||
return { readyMap, peers: buildPeersSnapForSpace(space) };
|
||||
}
|
||||
|
||||
/** สุ่ม 3 minigame จาก pool 7 แบบ — เรียกครั้งเดียวตอนเข้า LobbyB (ล็อกบน space) */
|
||||
/** แมปเก่า (กบ frogger) → Minigame-2 Gauntlet ตาม Admin */
|
||||
function normalizeDetectiveCardEntry(entry) {
|
||||
@@ -3527,6 +3537,13 @@ function beginDetectiveSuspectMinigame(sid, space, cardEntry, selectedIndex) {
|
||||
space.peers.forEach((_p, id) => { space.gauntletCrownLobbyReady[id] = false; });
|
||||
space.gauntletRun = newBalloonBossShellGauntletRunState();
|
||||
}
|
||||
/* quiz_carry ใช้ ready map ของตัวเอง (ไม่ใช่ crown) — รีเซ็ตสถานะ Ready ใหม่ทุกคนเริ่มรอบ แล้วแจ้งทุก client
|
||||
กัน host เห็นจำนวนผู้เล่น/สถานะ ready ไม่ตรง และให้ flow READY→START ทำงานเหมือนเกมอื่น */
|
||||
if (md.gameType === 'quiz_carry') {
|
||||
space.quizCarryLobbyReady = {};
|
||||
space.peers.forEach((_p, id) => { space.quizCarryLobbyReady[id] = false; });
|
||||
io.to(sid).emit('quiz-carry-lobby-sync', quizCarryLobbySyncPayload(space));
|
||||
}
|
||||
|
||||
const gameStartPayload = {
|
||||
mapId,
|
||||
@@ -7481,7 +7498,7 @@ io.on('connection', (socket) => {
|
||||
if (spaceAllowsQuizCarryLobbyRelaxed(space)) {
|
||||
if (!space.quizCarryLobbyReady || typeof space.quizCarryLobbyReady !== 'object') space.quizCarryLobbyReady = {};
|
||||
space.quizCarryLobbyReady[socket.id] = false;
|
||||
io.to(spaceId).emit('quiz-carry-lobby-sync', { readyMap: { ...space.quizCarryLobbyReady } });
|
||||
io.to(spaceId).emit('quiz-carry-lobby-sync', quizCarryLobbySyncPayload(space));
|
||||
}
|
||||
if (mdJoin.gameType === 'gauntlet') {
|
||||
const ord = typeof peer.spawnJoinOrder === 'number' ? peer.spawnJoinOrder : [...space.peers.keys()].indexOf(socket.id);
|
||||
@@ -8379,7 +8396,7 @@ io.on('connection', (socket) => {
|
||||
if (space.quizCarryLobbyReady && typeof space.quizCarryLobbyReady === 'object') {
|
||||
delete space.quizCarryLobbyReady[socket.id];
|
||||
if (spaceAllowsQuizCarryLobbyRelaxed(space)) {
|
||||
io.to(sid).emit('quiz-carry-lobby-sync', { readyMap: { ...space.quizCarryLobbyReady } });
|
||||
io.to(sid).emit('quiz-carry-lobby-sync', quizCarryLobbySyncPayload(space));
|
||||
}
|
||||
}
|
||||
if (space.gauntletCrownLobbyReady && typeof space.gauntletCrownLobbyReady === 'object') {
|
||||
@@ -8506,7 +8523,7 @@ io.on('connection', (socket) => {
|
||||
for (const id of space.peers.keys()) {
|
||||
if (space.quizCarryLobbyReady[id] === undefined) space.quizCarryLobbyReady[id] = false;
|
||||
}
|
||||
socket.emit('quiz-carry-lobby-sync', { readyMap: { ...space.quizCarryLobbyReady } });
|
||||
socket.emit('quiz-carry-lobby-sync', quizCarryLobbySyncPayload(space));
|
||||
});
|
||||
|
||||
socket.on('quiz-carry-lobby-ready', (data) => {
|
||||
@@ -8519,7 +8536,7 @@ io.on('connection', (socket) => {
|
||||
if (space.quizCarryLobbyReady[id] === undefined) space.quizCarryLobbyReady[id] = false;
|
||||
}
|
||||
space.quizCarryLobbyReady[socket.id] = !!(data && data.ready);
|
||||
io.to(sid).emit('quiz-carry-lobby-sync', { readyMap: { ...space.quizCarryLobbyReady } });
|
||||
io.to(sid).emit('quiz-carry-lobby-sync', quizCarryLobbySyncPayload(space));
|
||||
});
|
||||
|
||||
socket.on('quiz-carry-lobby-start', (_data, cb) => {
|
||||
@@ -8539,6 +8556,24 @@ io.on('connection', (socket) => {
|
||||
reply({ ok: true });
|
||||
});
|
||||
|
||||
/* host เลือกคำถาม quiz_carry → relay index ให้คนอื่นในห้องใช้ข้อเดียวกัน (กันสุ่มไม่ตรงกัน) */
|
||||
socket.on('quiz-carry-question', (data) => {
|
||||
const sid = socket.data.spaceId;
|
||||
const space = sid ? spaces.get(sid) : null;
|
||||
if (!space || space.hostId !== socket.id) return; /* เฉพาะ host */
|
||||
const idx = Number(data && data.idx);
|
||||
if (!Number.isInteger(idx) || idx < 0) return;
|
||||
socket.to(sid).emit('quiz-carry-question', { idx });
|
||||
});
|
||||
|
||||
/* host จบเซสชัน quiz_carry → ให้คนอื่นโชว์สรุปพร้อมกัน */
|
||||
socket.on('quiz-carry-session-complete', () => {
|
||||
const sid = socket.data.spaceId;
|
||||
const space = sid ? spaces.get(sid) : null;
|
||||
if (!space || space.hostId !== socket.id) return;
|
||||
socket.to(sid).emit('quiz-carry-session-complete', {});
|
||||
});
|
||||
|
||||
/* ตอบเวลา server ปัจจุบัน → client ใช้คำนวณ clock offset (ชดเชย latency) ให้เวลาในเกมตรงกันทุกเครื่อง */
|
||||
socket.on('time-sync', (cb) => {
|
||||
if (typeof cb === 'function') cb({ t: Date.now() });
|
||||
|
||||
Reference in New Issue
Block a user