update minigame 2

This commit is contained in:
2026-07-03 11:07:32 +00:00
parent 342b441839
commit 09c0a7bf76
14 changed files with 130456 additions and 25 deletions
+31 -19
View File
@@ -51,9 +51,9 @@
"providerUserId": "p_1775109142385_wq7wfy1p32j",
"notes": "auto: player-coins",
"blocked": false,
"coins": 3064,
"coins": 3082,
"createdAt": "2026-04-02T05:52:21+00:00",
"updatedAt": "2026-07-03T04:30:33+00:00",
"updatedAt": "2026-07-03T10:29:39+00:00",
"daily": {
"anchorMs": 1781197200000,
"claimedDays": [
@@ -67,7 +67,7 @@
],
"lockUntilMs": 0
},
"score": 990,
"score": 1008,
"scoreByCase": {
"1": 26,
"10": 970,
@@ -76,11 +76,11 @@
"13": 368,
"14": 30,
"9": 40,
"12": 466,
"12": 476,
"15": 20,
"4": 20,
"5": 32,
"7": 68,
"7": 76,
"3": 170,
"6": 44,
"2": 284
@@ -456,17 +456,19 @@
"providerUserId": "p_1781850663247_l0g0d1r7pcm",
"notes": "auto: player-coins",
"blocked": false,
"coins": 14,
"coins": 36,
"createdAt": "2026-06-19T06:31:03+00:00",
"updatedAt": "2026-07-03T04:30:33+00:00",
"score": 14,
"updatedAt": "2026-07-03T10:29:39+00:00",
"score": 36,
"scoreByCase": {
"10": 12,
"13": 2
"13": 2,
"7": 6,
"12": 16
},
"lbName": "sdd",
"achievements": {
"d1_minigame_solver": 3
"d1_minigame_solver": 6
}
},
{
@@ -1190,29 +1192,39 @@
"providerUserId": "p_1783048961594_ret1ovutfmg",
"notes": "auto: player-coins",
"blocked": false,
"coins": 18,
"coins": 48,
"createdAt": "2026-07-03T03:22:43+00:00",
"updatedAt": "2026-07-03T04:30:33+00:00",
"score": 18,
"updatedAt": "2026-07-03T10:29:39+00:00",
"score": 48,
"scoreByCase": {
"10": 18
"10": 18,
"7": 10,
"12": 20
},
"lbName": "MONE",
"lbName": "ผู้เล่น",
"achievements": {
"d1_minigame_solver": 4
"d1_minigame_solver": 7
}
},
{
"id": "dc5f68fed544857b2b8b7aa1",
"email": "",
"displayName": "Guest",
"displayName": "MONE",
"loginType": "guest",
"providerUserId": "p_1783049887754_gy0fhp1tq7",
"notes": "auto: player-coins",
"blocked": false,
"coins": 0,
"coins": 10,
"createdAt": "2026-07-03T03:38:25+00:00",
"updatedAt": "2026-07-03T03:38:25+00:00"
"updatedAt": "2026-07-03T10:29:39+00:00",
"achievements": {
"d1_minigame_solver": 2
},
"score": 10,
"scoreByCase": {
"12": 10
},
"lbName": "MONE"
}
]
}
+64 -2
View File
@@ -2031,12 +2031,36 @@
}
}
/** (me + others ) = gameover ()
บอทเต (__lobby_bot_N) ไม server ดส all_dead จาก space.peers (คนจรงเทาน) เหมอนก
เดมนบบอทดวยเม playBotsEnabled()=true (องจรงเปดเสมอ) บอทไมตายเลย = เชคนไมทรกเกอรเลย */
function gauntletCrownAllEntitiesEliminatedPlay() {
let total = 0, elim = 0;
if (Number.isFinite(me.x) || Number.isFinite(me.y)) { total++; if (me.gauntletEliminated) elim++; }
others.forEach((o, id) => {
if (!o) return;
if (isPreviewBotId(id)) return;
total++; if (o.gauntletEliminated) elim++;
});
return total >= 1 && elim === total;
}
function tickGauntletCrownRunwayBgPlay(dtSec) {
if (!gauntletCrownRunwayBgDrawActivePlay()) return;
/* หยุดเลื่อน BG ระหว่างคำถามพิเศษ (special-quiz freeze) — กัน BG/เส้นชัยเดินตอนค้างตอบ */
if (specialQuizFreeze) return;
/** พรีรัน (howto / countdown) — ห้ามเลื่อนก่อน GO · Mega Virus ใช้ shell เดียวกัน */
if (usesCrownLobbyShellPlay() && gauntletCrownPregamePhase !== 'live') return;
/* ทุกคนตกขอบหมด → เข้า flow gameover ทันที (ไม่รอ scroll เดินเข้าเส้นชัย) — server ก็จบเกม all_dead พร้อมกัน */
if (playBotsEnabled() && gauntletCrownRunwayBgMapEligiblePlay()
&& !gauntletCrownRunwayClientMissionShown && gauntletCrownAllEntitiesEliminatedPlay()) {
gauntletCrownRunwayClientMissionShown = true;
gauntletCrownRunwayWalkActive = false;
const mg2AllDeadMission = gauntletCrownHeistBuildLocalCrownMissionPlay();
beginSimpleResultFlashThenGcmPlay(mg2AllDeadMission, 'result-gameover.png');
try { draw(); } catch (_d) { /* ignore */ }
return;
}
if (gauntletCrownRunwayBgFinishLatched) { gauntletSnapLivingToFinishPlay(); return; }
/* เฟส "เดินเข้าเส้นชัย" — ค่อยๆ เดินทุกคนมาที่เส้นแทน snap กระพริบ แล้วค่อยเข้า freeze→จบ */
@@ -3926,8 +3950,8 @@
function drawQuizWrongGhostOverlayPlay(sxCenter, syBottom, heightPx, entId, nameLabel) {
const img = getQuizWrongGhostImg();
if (!img || !img.complete || !img.naturalWidth) return;
/* ผีสูงเท่า avatar ตามจริง — คงสัดส่วนภาพ */
const hPx = Math.max(24, Number(heightPx) || 0);
/* ผีเล็กลง 25% จากความสูง avatar (คงสัดส่วนภาพ · เท้ายังตรงพื้น) */
const hPx = (Math.max(24, Number(heightPx) || 0)) * 0.75;
const ratio = img.naturalWidth / Math.max(1, img.naturalHeight);
const wPx = hPx * ratio;
const sid = String(entId == null ? '' : entId);
@@ -6535,6 +6559,31 @@
}
function isFrogger() { return mapData && mapData.gameType === 'frogger'; }
/* net-ready badge: emit client-ready ครั้งเดียวเมื่อฉากพร้อม */
let playClientReadyEmitted = false;
/* วาด badge "กำลังโหลด…" เหนือหัว peer ที่ยังโหลดไม่เสร็จ (netReady=false) — คนอื่นรู้ว่าไม่ได้ค้าง */
function drawNetLoadingBadgePlay(axTile, ayTile, w2s, zDrawIn) {
try {
const fw = getCharacterFootprintWH(mapData);
const cxw = (axTile + (fw.cw || 1) * 0.5) * tileSize;
const topYw = ayTile * tileSize;
const scr = w2s(cxw, topYw);
const sx = scr[0], sy = scr[1];
const now = Date.now();
const spin = ['◐', '◓', '◑', '◒'][Math.floor(now / 180) % 4];
const txt = spin + ' กำลังโหลด…';
ctx.save();
ctx.font = '700 13px Kanit, system-ui, sans-serif';
ctx.textAlign = 'center';
const tw = ctx.measureText(txt).width + 12;
const by = sy - 46, bh = 20;
ctx.fillStyle = 'rgba(10,16,30,0.82)';
if (ctx.roundRect) { ctx.beginPath(); ctx.roundRect(sx - tw / 2, by, tw, bh, 6); ctx.fill(); } else ctx.fillRect(sx - tw / 2, by, tw, bh);
ctx.fillStyle = '#ffd34d'; ctx.textBaseline = 'middle';
ctx.fillText(txt, sx, by + bh / 2 + 1);
ctx.restore();
} catch (e) { /* ignore */ }
}
/* ===== Sound (jdSound) — scene + end-sfx helpers ===== */
var JD_SCENE_MAP = { quiz: 'mg1', gauntlet: 'mg2', stack: 'mg3', quiz_carry: 'mg4', jump_survive: 'mg5', space_shooter: 'mg6', balloon_boss: 'mg7' };
function jdPlayScene() { var gt = mapData && mapData.gameType; return (gt && JD_SCENE_MAP[gt]) || 'mg1'; }
@@ -20649,6 +20698,7 @@
x: px, y: py, tx: px, ty: py,
direction: data.direction || 'down', nickname: data.nickname,
characterId: data.characterId ?? null,
netReady: (data.nr !== false && data.netReady !== false), /* badge กำลังโหลด ตอนเพิ่ง join */
playerKey: joinPkDedup || null, /* ใช้ dedup ผีตอน reconnect (เก็บไว้เทียบรอบหน้า) */
lobbyThemeIndexSrv: parsePlayLobbyThemeIndex(data.lobbyColorThemeIndex) || null, /* สี server-authoritative → block MG3 ตรงทุกจอ */
spawnJoinOrder: spawnOrdJ,
@@ -20710,6 +20760,7 @@
host บผกแบนเปนผเลนทองรอ Ready humansReady ไมครบ เรมเกมไมได */
const nb = !!p.bannedSpectator;
if (existing.bannedSpectator !== nb) { existing.bannedSpectator = nb; changed = true; }
if (p.nr !== undefined) existing.netReady = p.nr !== false;
return;
}
const px = Number(p.x);
@@ -20722,6 +20773,7 @@
direction: p.direction || 'down',
nickname: p.nickname,
characterId: p.characterId ?? null,
netReady: p.nr !== false,
playerKey: pkSnap || null, /* dedup ผี reconnect */
lobbyThemeIndexSrv: parsePlayLobbyThemeIndex(p.lobbyColorThemeIndex) || null, /* สี server-authoritative → block MG3 ตรงทุกจอ */
spawnJoinOrder: Number.isFinite(joP) ? Math.max(0, Math.floor(joP)) : 0,
@@ -21492,6 +21544,13 @@
}
}
});
/* สถานะ net ของ peer (โหลดเสร็จ) → เอา badge "กำลังโหลด" ออก */
socket.on('peer-net', (data) => {
if (!data || data.id == null) return;
const o = others.get(data.id) || others.get(String(data.id));
if (o) o.netReady = data.nr !== false;
});
socket.on('user-left', (data) => {
if (!data || isPreviewBotId(data.id)) return;
const leftWasCurrentStack = !!(isStack() && detectiveCaseFillBots && isMePlayHost()
@@ -26265,6 +26324,7 @@
(isGauntletCrownHeistMapPlay() && gauntletCrownPregamePhase === 'live') ? (o.gauntletCrownPenaltyFxUntil || 0) : 0,
id);
}
if (o.netReady === false && !isPreviewBotId(id)) drawNetLoadingBadgePlay(axO, ayO, worldToScreen, zDraw);
if (botOut) ctx.restore();
} else {
if (shouldGauntletCrownHeistSkipAvatarDrawPlay(me, true, 0, 0)) return;
@@ -26586,6 +26646,8 @@
if (!mapData) { playTickLastTs = 0; requestAnimationFrame(tick); return; }
/* BGM ต่อมินิเกม (idempotent — playBgm return เร็วถ้า scene เดิม) */
if (window.jdSound) { try { window.jdSound.playBgm(jdPlayScene()); } catch (e) { /* ignore */ } }
/* โหลดฉากมินิเกมเสร็จ (ครั้งเดียว) → เอา badge "กำลังโหลด" ออกให้คนอื่นเห็น */
if (!playClientReadyEmitted && socket && socket.connected) { playClientReadyEmitted = true; try { socket.emit('client-ready'); } catch (e) { /* ignore */ } }
/* dt frame (วินาที) → playDtMul = dt*60 (clamp 0.05s = step ≤0.45 tile กัน tunneling) ใช้ทำ movement time-based */
{
const _pnow = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
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
+28 -1
View File
@@ -316,7 +316,8 @@
const y = Number(p.y);
const nx = Number.isFinite(x) ? x : fx;
const ny = Number.isFinite(y) ? y : fy;
return { ...p, x: nx, y: ny, tx: nx, ty: ny };
/* netReady: false = เพิ่ง join กำลังโหลด (snap ส่ง nr, user-joined ส่ง netReady) → โชว์ badge */
return { ...p, x: nx, y: ny, tx: nx, ty: ny, netReady: (p.nr !== false && p.netReady !== false) };
}
const characterImages = {};
@@ -614,6 +615,8 @@
try { lobbyLog('loading-hide', 'เริ่มเล่นได้'); } catch (e) { /* ignore */ }
/* แจ้ง server ว่าโหลดฉากเสร็จ → นับเป็น present ใน LobbyB จริง (กัน host เริ่มก่อนคนโหลดเสร็จ) */
try { if (isPostCaseLobbyRoom() && socket && socket.connected) socket.emit('lobbyb-loaded'); } catch (e) { /* ignore */ }
/* โหลดฉากเสร็จ (ทุกล็อบบี้) → เอา badge "กำลังโหลด" ออกให้คนอื่นเห็น */
try { if (socket && socket.connected) socket.emit('client-ready'); } catch (e) { /* ignore */ }
var ov = document.getElementById('room-loading-overlay');
if (ov) { ov.classList.add('is-hidden'); setTimeout(function () { if (ov.parentNode) ov.parentNode.removeChild(ov); }, 600); }
}
@@ -1802,6 +1805,22 @@
const labelY = cellBottom - drawH - (tileSize * 0.2);
const headTop = cellBottom - drawH - 4;
const now = Date.now();
/* badge "กำลังโหลด…" เหนือหัว — คนที่เพิ่ง join ยังโหลดไม่เสร็จ (netReady=false) → คนอื่นเห็นว่าไม่ได้ค้าง */
if (p.netReady === false) {
const spin = ['◐', '◓', '◑', '◒'][Math.floor(now / 180) % 4];
const txt = spin + ' กำลังโหลด…';
ctx.save();
ctx.font = '700 ' + Math.max(9, Math.round(tileSize * 0.32)) + 'px Kanit, system-ui, sans-serif';
ctx.textAlign = 'center';
const tw = ctx.measureText(txt).width + 12;
const bx = cx - tw / 2, by = headTop - Math.round(tileSize * 0.5), bh2 = Math.max(15, Math.round(tileSize * 0.42));
ctx.fillStyle = 'rgba(10,16,30,0.82)';
if (ctx.roundRect) { ctx.beginPath(); ctx.roundRect(bx, by, tw, bh2, 6); ctx.fill(); } else ctx.fillRect(bx, by, tw, bh2);
ctx.fillStyle = '#ffd34d';
ctx.textBaseline = 'middle';
ctx.fillText(txt, cx, by + bh2 / 2 + 1);
ctx.restore();
}
if (p.speakingUntil > now) {
var bubbleImg = getSpeakingBubbleFrame();
if (bubbleImg) {
@@ -4214,6 +4233,13 @@
if (window.jdSound) window.jdSound.notifySpeaking(); /* หรี่ BGM ตอน peer อื่นพูด */
});
/* สถานะ net ของ peer (โหลดเสร็จ/lag) → เอา/ใส่ badge "กำลังโหลด" */
socket.on('peer-net', function (data) {
if (!data || data.id == null) return;
var p = peers.get(data.id);
if (p) { p.netReady = data.nr !== false; redrawLobbyMap(); try { renderPeers(); } catch (e) { /* ignore */ } }
});
const iceQueue = {};
const defaultIceServers = [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' }];
let cachedIceServers = null;
@@ -7543,6 +7569,7 @@
if (row.nickname) p.nickname = row.nickname;
p.ready = !!row.ready;
if (row.characterId != null) p.characterId = row.characterId;
if (row.nr !== undefined) p.netReady = row.nr !== false;
});
const meAfter = peers.get(socket.id);
if (readyCheck && meAfter) {
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -5462,7 +5462,7 @@
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0306"></script>
<script src="js/sound.js?v=1"></script>
<script src="js/play.js?v=0.006301070"></script>
<script src="js/play.js?v=0.006301074"></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)
+1 -1
View File
@@ -1649,7 +1649,7 @@
<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/sound.js?v=1"></script>
<script src="js/room-lobby.js?v=0.0344"></script>
<script src="js/room-lobby.js?v=0.0345"></script>
<div class="version-tag">v —</div>
</body>
</html>
+67 -1
View File
@@ -3448,6 +3448,7 @@ function buildPeersSnapForSpace(space, extraFields) {
lobbyColorThemeIndex: p.lobbyColorThemeIndex || null,
lobbySkinToneIndex: p.lobbySkinToneIndex || null,
bannedSpectator: !!p.bannedSpectator,
nr: p.netReady !== false, /* net-ready: false = เพิ่ง join กำลังโหลด (client โชว์ badge) */
};
if (extraFields && extraFields.gauntlet) {
row.gauntletJumpTicks = p.gauntletJumpTicks || 0;
@@ -7896,6 +7897,7 @@ function newGauntletRunState(space) {
crownRunHeld: crown,
finishPhase: false,
finishEndAt: null,
gameoverAt: null, /* != null = อยู่ในช่วง grace "ตายหมด" (โชว์ game over ก่อนกลับ LobbyB) */
};
}
@@ -9542,7 +9544,8 @@ function endGauntletGame(sid, space, reason) {
const md = (space.mapId && maps.get(space.mapId)) || space.mapData;
if (!md || md.gameType !== 'gauntlet') return;
if (space.detectiveMinigameActive) {
const msg = reason === 'time' ? 'หมดเวลา — จบมินิเกม' : 'จบมินิเกม';
const msg = reason === 'time' ? 'หมดเวลา — จบมินิเกม'
: (reason === 'all_dead' ? 'ตกขอบหมดแล้ว — จบมินิเกม' : 'จบมินิเกม');
returnDetectiveSpaceToLobbyB(sid, space, msg, { allPlayers: true });
return;
}
@@ -9654,10 +9657,31 @@ function runGauntletTick(sid, space) {
return;
}
/* (all_dead) grace: + client result-gameover GCM
อน server งกล LobbyB. เด all_dead จบ+return นท (0 grace) = client งไมนวาด game over
โดนดงกล = "ตายหมดแต่ไม่ขึ้น game over". ใช grace เทากบทางชนะ (GAUNTLET_FINISH_WALK_MS) */
if (gr.gameoverAt != null) {
if (gr.obstacles && gr.obstacles.length) gr.obstacles = [];
if (Date.now() >= gr.gameoverAt) {
endGauntletGame(sid, space, 'all_dead');
return;
}
/* ไม่ emit sync ระหว่าง grace — ปล่อย client คุม overlay result เอง (เหมือน finish grace) */
return;
}
/* (client ) obstacle/collision grace client
เดนทกคน "ค่อยๆ" เขาเสนชยจนครบ อยสรปผล (ไม snap กระพร / ไมจบทนท) */
if (gr.finishPhase) {
if (gr.obstacles.length) { gr.obstacles = []; }
/* / finish-walk grace grace game over ( result-complete )
(race: client วหน scroll งเสนชยกอน set finishPhase early-return ามเช all_dead าย tick) */
if (gauntletAllHumansEliminated(space)) {
gr.gameoverAt = Date.now() + GAUNTLET_FINISH_WALK_MS;
gr.obstacles = [];
try { glog(sid, space, 'gauntlet-end', 'ตายหมด (ระหว่าง finish-phase) → grace game over'); } catch (e) { /* ignore */ }
return;
}
if (gr.finishEndAt == null) gr.finishEndAt = Date.now() + GAUNTLET_FINISH_WALK_MS;
if (Date.now() >= gr.finishEndAt) {
endGauntletGame(sid, space, 'finish');
@@ -9808,9 +9832,34 @@ function runGauntletTick(sid, space) {
});
}
/* () grace game over () return LobbyB
sync ครงสดทาย (กคน eliminated) ให client ทร overlay result-gameover อนหมด grace */
if (gauntletAllHumansEliminated(space)) {
gr.gameoverAt = Date.now() + GAUNTLET_FINISH_WALK_MS;
gr.obstacles = [];
try { glog(sid, space, 'gauntlet-end', 'ผู้เล่นตกขอบหมด → grace game over แล้วกลับ LobbyB'); } catch (e) { /* ignore */ }
emitGauntletSync(sid, space);
return;
}
emitGauntletSync(sid, space);
}
/** MG2 crown/gauntlet: "" (/) = all_dead
บอทอยใน gr.bots ไมใช space.peers ไมกนบอยแล (server ดสนจากคนจรงเทาน)
- บเฉพาะ crown map · องมคนเลนจร 1 คน (นจบทนทตอน 0 คน) */
function gauntletAllHumansEliminated(space) {
if (!isGauntletCrownHeistMapBySpace(space) || !space.peers || space.peers.size === 0) return false;
let active = 0;
let alive = 0;
space.peers.forEach((p) => {
if (!p || p.bannedSpectator) return; /* ผู้ถูกแบน = เข้ามาดู ไม่ได้เล่น → ไม่บล็อกการจบเกม */
active++;
if (!p.gauntletEliminated) alive++;
});
return active > 0 && alive === 0;
}
function rescheduleAllGauntletTickers() {
for (const [sid, space] of spaces) {
const md = (space.mapId && maps.get(space.mapId)) || space.mapData;
@@ -10738,6 +10787,7 @@ io.on('connection', (socket) => {
playerKey: (typeof playerKey === 'string' && /^[a-zA-Z0-9_-]{8,128}$/.test(playerKey)) ? playerKey : '',
reportedMiniScore: 0, reportedMiniSurvived: true,
bannedSpectator: !!bannedSpectator,
netReady: false, /* เพิ่ง join → กำลังโหลด จนกว่า client ส่ง client-ready (badge ให้คนอื่นเห็นว่า "กำลังโหลด" ไม่ใช่ค้าง) */
};
if (mdJoin.gameType === 'quiz_battle' && quizBattlePathModeActiveServer(mdJoin)) {
const sn = quizBattleSpawnWorldFromJoinOrderServer(mdJoin, spawnJoinOrder);
@@ -11848,6 +11898,9 @@ io.on('connection', (socket) => {
try { glog(sid, space, 'story-gate', '[TC156] ทุกคนอ่าน story ครบ (' + st.total + ') → ปลดล็อกเลือกผู้ต้องสงสัย (pass)'); } catch (e) { /* ignore */ }
}
emitDetectiveStoryReadPresence(sid, space);
/* piggyback lobbyBLoadedKeys broadcast lobbyb-presence
ไม client างโชว "รอผู้เล่น (2/3)" (าเก) host disable งทครบแล */
emitDetectiveLobbyBPresence(sid, space);
break;
}
});
@@ -11872,6 +11925,19 @@ io.on('connection', (socket) => {
}
});
/* client โหลดฉากเสร็จ (ทุกฉาก lobby/minigame) → เอา badge "กำลังโหลด" ออก (คนอื่นเห็นว่าพร้อมแล้ว) */
socket.on('client-ready', () => {
for (const [sid, space] of spaces) {
if (!space.peers.has(socket.id)) continue;
const p = space.peers.get(socket.id);
if (p && p.netReady !== true) {
p.netReady = true;
io.to(sid).emit('peer-net', { id: socket.id, nr: true });
}
break;
}
});
socket.on('start-game', (data, cb) => {
const reply = typeof cb === 'function' ? cb : () => {};
for (const [sid, space] of spaces) {
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