join room เพิ่มรูปให้เข้าใจง่าย host client game 1-7

This commit is contained in:
2026-06-23 15:38:48 +00:00
parent 38581636b4
commit 7d8a29b96c
6 changed files with 128 additions and 31 deletions
+15 -12
View File
@@ -51,9 +51,9 @@
"providerUserId": "p_1775109142385_wq7wfy1p32j",
"notes": "auto: player-coins",
"blocked": false,
"coins": 1051,
"coins": 1233,
"createdAt": "2026-04-02T05:52:21+00:00",
"updatedAt": "2026-06-23T09:46:51+00:00",
"updatedAt": "2026-06-23T15:32:42+00:00",
"daily": {
"anchorMs": 1781197200000,
"claimedDays": [
@@ -67,20 +67,21 @@
],
"lockUntilMs": 0
},
"score": 936,
"score": 1118,
"scoreByCase": {
"1": 16,
"10": 340,
"8": 230,
"11": 20,
"13": 88,
"13": 96,
"14": 20,
"9": 40,
"12": 126,
"12": 246,
"15": 20,
"4": 10,
"5": 20,
"7": 6
"7": 6,
"3": 54
},
"lbName": "Q",
"achievements": {
@@ -639,19 +640,21 @@
"providerUserId": "p_1782122326943_7p2fnvivscm",
"notes": "auto: player-coins",
"blocked": false,
"coins": 48,
"coins": 202,
"createdAt": "2026-06-22T09:58:46+00:00",
"updatedAt": "2026-06-23T07:42:20+00:00",
"updatedAt": "2026-06-23T15:32:42+00:00",
"lobbyColorThemeIndex": 3,
"lobbySkinToneIndex": 1,
"score": 38,
"score": 192,
"scoreByCase": {
"10": 20,
"12": 18
"12": 108,
"3": 54,
"13": 10
},
"lbName": "ผู้เล่น",
"lbName": "MONE",
"achievements": {
"d1_minigame_solver": 3
"d1_minigame_solver": 20
},
"daily": {
"anchorMs": 1782147600000,
+2 -2
View File
@@ -89,9 +89,9 @@
"balloonBossPlayerBalloonFallbackUrl": "/Game/img/MegaVirus/Artboard%209.png",
"balloonBossBalloonsPerPlayer": 3,
"forcedMinigameKeys": [
"balloon_boss",
"jump_survive",
"space_shooter",
"balloon_boss"
"space_shooter"
],
"testSpecialCardByMap": {},
"troublesomeForceOffer": false,
File diff suppressed because one or more lines are too long
+105 -14
View File
@@ -5394,6 +5394,17 @@
return n;
}
/** ป้ายชื่อบอท "บอท N (tier)" — เรียงตาม id (เหมือนทุกเครื่อง) ใช้ o.botTier (host เป็นเจ้าของ ผ่าน fill-bot-state) */
function relabelPreviewBotsPlay() {
let k = 0;
[...others.keys()].filter(isPreviewBotId).sort().forEach((bid) => {
const o = others.get(bid);
if (!o) return;
const tag = o.botTier === 'sharp' ? '(ฉลาด)' : o.botTier === 'weak' ? '(พลาดบ่อย)' : '(กลาง)';
o.nickname = 'บอท ' + (++k) + ' ' + tag;
});
}
function rebalancePreviewBots() {
if (!playBotsEnabled() || !mapData) return;
const human = countPlayHumans();
@@ -5506,13 +5517,7 @@
} catch (ePB) { /* ignore */ }
botIds.push(id);
}
let k = 0;
[...others.keys()].filter(isPreviewBotId).sort().forEach((bid) => {
const o = others.get(bid);
if (!o) return;
const tag = o.botTier === 'sharp' ? '(ฉลาด)' : o.botTier === 'weak' ? '(พลาดบ่อย)' : '(กลาง)';
o.nickname = 'บอท ' + (++k) + ' ' + tag;
});
relabelPreviewBotsPlay();
[...others.keys()].filter(isPreviewBotId).sort().forEach((bid, botIdx) => {
const o = others.get(bid);
if (o) o.characterId = pickPreviewBotCharacterId(botIdx);
@@ -9270,7 +9275,7 @@
function isMePlayHost() {
if (myId == null) return false;
if (playHostId == null) return true;
if (playHostId == null) return true; /* fallback (load-bearing): host พึ่งตอน null — อย่าเปลี่ยน (เคยทำ MG3 ready ค้าง 4/5 + MG2 spawn ทับ) */
return String(playHostId) === String(myId);
}
@@ -9285,6 +9290,7 @@
if (!isPreviewBotId(id) || !o) return;
const row = { id: id, x: fbNum(o.x), y: fbNum(o.y), direction: o.direction || 'down', walking: !!o.botIsWalking };
if (o.characterId != null) row.cid = o.characterId; /* ให้ client สร้างบอทที่ขาดได้ → ชุดบอท host-authoritative */
if (o.botTier != null) row.tier = o.botTier; /* tier (ฉลาด/กลาง/พลาดบ่อย) — host เป็นเจ้าของ ให้ label/พฤติกรรมตรงกันทุกเครื่อง (เดิมสุ่ม local → ป้ายไม่ตรง) */
if (o.gauntletScore != null) row.gScore = o.gauntletScore;
if (o.gauntletEliminated != null) row.gElim = !!o.gauntletEliminated;
if (o.gauntletJumpTicks != null) row.gJump = o.gauntletJumpTicks;
@@ -9316,7 +9322,12 @@
}
arr.push(row);
});
if (arr.length) socket.emit('fill-bot-state', { bots: arr });
/* รายชื่อ "คนจริง" ที่ host เห็น (= myId(host) + others ที่ไม่ใช่บอท) — ให้ non-host force ลบผีที่ host ไม่มี
(humans ไม่ถูก sync แบบบอท → ผี reconnect ค้างบน client; id-based ไม่ชนชื่อ default) */
const humanRoster = [];
if (myId != null) humanRoster.push(String(myId));
others.forEach((o, id) => { if (!isPreviewBotId(id) && o) humanRoster.push(String(id)); });
if (arr.length) socket.emit('fill-bot-state', { bots: arr, humans: humanRoster });
}
setInterval(broadcastFillBotState, 80); /* ~12.5Hz — self-gate ถ้าไม่ใช่ host หรือไม่มีบอท */
@@ -19324,6 +19335,24 @@
if (!data || isPreviewBotId(data.id)) return;
if (myId != null && String(data.id) === String(myId)) return;
if (socket && socket.id && String(data.id) === String(socket.id)) return;
/* กันผี reconnect ("เงาตัวละคร"): คนเดิมกลับเข้ามาด้วย socket.id ใหม่ (เปลี่ยนหน้า/หลุดต่อใหม่)
— server ลบ peer เก่าด้วย playerKey เท่านั้น ถ้าไม่ match จะค้างเป็นตัวซ้ำ.
ลบ peer เดิม (คนจริง) ที่ "ชื่อ+ตัวละครเดียวกัน" แต่ id ต่าง ก่อนเพิ่มตัวใหม่ (สัญญาณว่าเป็นคนเดิม) */
const joinNickDedup = (data.nickname != null ? String(data.nickname).trim() : '');
const joinCidDedup = (data.characterId != null ? String(data.characterId) : '');
/* dedup เฉพาะชื่อ "เฉพาะตัว" (custom) — ข้ามชื่อ default ที่ซ้ำกันได้ ('ผู้เล่น', 'บอท', ว่าง)
ไม่งั้นผู้เล่นจริง 2 คนที่ไม่ตั้งชื่อ (default 'ผู้เล่น') จะถูกลบผิด → จำนวนคนหาย/ready ค้าง 4/5 */
const isGenericNick = !joinNickDedup || /^ผู้เล่น/.test(joinNickDedup) || joinNickDedup === 'บอท';
if (!isGenericNick) {
[...others.keys()].forEach((oid) => {
if (isPreviewBotId(oid) || String(oid) === String(data.id)) return;
const ex = others.get(oid);
if (!ex) return;
const exNick = (ex.nickname != null ? String(ex.nickname).trim() : '');
const exCid = (ex.characterId != null ? String(ex.characterId) : '');
if (exNick === joinNickDedup && exCid === joinCidDedup) others.delete(oid);
});
}
const x = Number(data.x);
const y = Number(data.y);
const px = Number.isFinite(x) ? x : 1;
@@ -19491,8 +19520,27 @@
});
/* รับ state ของ fill-bot จาก host (เฉพาะ client ที่ไม่ใช่ host) → render ตาม ไม่จำลองเอง */
socket.on('fill-bot-state', (data) => {
/* ได้รับ fill-bot-state = server relay จาก "host จริง" เท่านั้น (server gate + socket.to ไม่ส่งกลับผู้ส่ง)
→ เรา "ไม่ใช่ host" แน่นอน. ใช้ data.hostId แก้ playHostId ที่ผิด (race LobbyB→play ทำให้เข้าใจผิดว่าเป็น host
→ ignore fill-bot-state + sim บอทเอง = บอท/จำนวนคนละชุดบน 2 จอ) */
if (data && data.hostId != null && myId != null && String(data.hostId) !== String(myId)) {
if (String(playHostId) !== String(data.hostId)) playHostId = data.hostId;
}
if (isMePlayHost()) return;
if (!data || !Array.isArray(data.bots)) return;
/* force รายชื่อ "คนจริง" ให้ตรง host — ลบผี (peer ที่ host ไม่มี แต่ค้างบน client จาก reconnect/snapshot)
id-based (ไม่ชนชื่อ default). debounce 3 ครั้งติด กัน peer ที่ host ยังไม่เห็นชั่วคราวถูกลบผิด (self-heal) */
if (Array.isArray(data.humans)) {
const hostHumans = new Set(data.humans.map(String));
[...others.keys()].forEach((oid) => {
if (isPreviewBotId(oid)) return;
const o = others.get(oid);
if (!o) return;
if (hostHumans.has(String(oid))) { o._notInHostRoster = 0; return; }
o._notInHostRoster = (o._notInHostRoster || 0) + 1;
if (o._notInHostRoster >= 3) others.delete(oid);
});
}
/* เกมที่ render อ่านตำแหน่งจาก x/y หรือ Cx/Cy ตรงๆ (ไม่ผ่าน lerp ของ main tick) → สแนปตำแหน่งเลย */
const snapPos = isSpaceShooter() || isBalloonBoss() || isJumpSurvive();
/* ชุดบอทต้องตรงกับ host เป๊ะ — กัน client คำนวณ wantBots ต่างจาก host (เช่นนับ human เกิน)
@@ -19504,6 +19552,7 @@
if (!hostBotIds.has(String(bid))) others.delete(bid);
});
let quizBotScoreChanged = false;
let tierChangedFb = false;
data.bots.forEach((rb) => {
if (!rb || !isPreviewBotId(rb.id)) return;
let o = others.get(rb.id);
@@ -19520,6 +19569,7 @@
spaceShooterScore: 0, balloonBossScore: 0, balloonBossBossDmg: 0,
balloonBossBalloons: balloonBossBalloonsStartPlay(), balloonBossEliminated: false,
spaceShooterEliminated: false, jumpSurviveEliminated: false, quizCarryHeld: null,
botTier: (rb.tier === 'sharp' || rb.tier === 'weak' || rb.tier === 'avg') ? rb.tier : 'avg',
};
others.set(rb.id, o);
if (o.characterId) { try { preloadPlayTintForAvatar(o.characterId, o.playTint); } catch (_e) { /* ignore */ } }
@@ -19538,6 +19588,7 @@
}
}
o.botIsWalking = !!rb.walking;
if ((rb.tier === 'sharp' || rb.tier === 'weak' || rb.tier === 'avg') && o.botTier !== rb.tier) { o.botTier = rb.tier; tierChangedFb = true; }
if (rb.gScore != null) o.gauntletScore = rb.gScore;
if (rb.gElim != null) o.gauntletEliminated = !!rb.gElim;
if (rb.gJump != null) o.gauntletJumpTicks = rb.gJump;
@@ -19566,6 +19617,7 @@
if (playLiveQuizScores[k] !== nv) { playLiveQuizScores[k] = nv; quizBotScoreChanged = true; }
}
});
if (tierChangedFb) relabelPreviewBotsPlay(); /* tier จาก host เปลี่ยน → อัปเดตป้าย "บอท N (tier)" ให้ตรง host */
if (quizBotScoreChanged && isQuiz()) renderPlayQuizScoreboard(playLiveQuizScores);
});
@@ -21086,6 +21138,45 @@
clampPlayEntityFootprintToMap(b, mapData);
}
}
/* แยกบอทออกจาก "ผู้เล่นจริง" ด้วย — เดิมแยกแค่บอท↔บอท → บอท spawn/เดินทับคนจริง
แล้วติดกันใน blockPlayer zone (entity บล็อกกัน) เดินไม่ได้ทั้งคู่.
host ดันเฉพาะบอท (คนจริงเป็นเจ้าของตำแหน่งตัวเอง broadcast ผ่าน move) */
/* แยกบอท↔คน เฉพาะแมปที่มี blockPlayer (entity บล็อกกันได้ = ติดกันเดินไม่ได้) — แมปอื่นซ้อนได้ไม่ต้องแยก */
if (mapData.blockPlayer) {
const minDHuman = 0.95; /* ระยะ ~1 ช่อง — ให้ footprint แยกชัด คนไม่โดนบล็อก */
const humansSep = [];
if (me && Number.isFinite(me.x) && Number.isFinite(me.y)) humansSep.push(me);
others.forEach((p, pid) => {
if (!isPreviewBotId(pid) && p && Number.isFinite(p.x) && Number.isFinite(p.y)) humansSep.push(p);
});
for (let i = 0; i < ids.length; i++) {
const a = others.get(ids[i]);
if (!a) continue;
for (let k = 0; k < humansSep.length; k++) {
const hu = humansSep[k];
let ddx = a.x - hu.x, ddy = a.y - hu.y;
let d = Math.sqrt(ddx * ddx + ddy * ddy);
if (d >= minDHuman) continue;
if (d < 1e-5) { ddx = (i % 2 === 0) ? 1 : -1; ddy = (i % 3) - 1; d = Math.sqrt(ddx * ddx + ddy * ddy) || 1; }
const ux = ddx / d, uy = ddy / d; /* ดันบอทออกจากคน */
/* ดันให้พ้น minDHuman ในเฟรมเดียว + เช็คแค่กำแพง (ignorePeers) → หนีออกจาก blockPlayer zone ได้
(ถ้าเช็ค peer ด้วย ทุกช่องรอบตัวคนถูกบล็อก → บอทหนีไม่ออก ติดตลอด) */
const need = (minDHuman - d) + 0.06;
let placed = false;
const try1x = a.x + ux * need, try1y = a.y + uy * need;
if (canWalkLikeLobbyForBot(try1x, try1y, a.x, a.y, a, true)) { a.x = try1x; a.y = try1y; placed = true; }
if (!placed) {
/* ทิศตรงชนกำแพง → ลองทิศตั้งฉาก 2 ด้าน */
const perp = [[-uy, ux], [uy, -ux]];
for (let pI = 0; pI < perp.length && !placed; pI++) {
const tx2 = a.x + perp[pI][0] * need, ty2 = a.y + perp[pI][1] * need;
if (canWalkLikeLobbyForBot(tx2, ty2, a.x, a.y, a, true)) { a.x = tx2; a.y = ty2; placed = true; }
}
}
if (placed) clampPlayEntityFootprintToMap(a, mapData);
}
}
}
}
function quizTilesFootprintPlay(px, py) {
@@ -21301,7 +21392,7 @@
return true;
}
function canWalkLikeLobbyForBot(x, y, fromX, fromY, o) {
function canWalkLikeLobbyForBot(x, y, fromX, fromY, o, ignorePeers) {
if (!mapData || !mapData.objects) return false;
if (typeof x !== 'number' || typeof y !== 'number' || !Number.isFinite(x) || !Number.isFinite(y)) return false;
const w = mapData.width || 20, h = mapData.height || 15;
@@ -21319,7 +21410,7 @@
if (wallTilesB.size < colW * colH) return false;
}
const bp = mapData.blockPlayer;
if (bp) {
if (bp && !ignorePeers) {
for (const k of quizTilesFootprintPlay(x, y)) {
const p = k.split(',');
const tx = +p[0], ty = +p[1];
@@ -24510,9 +24601,9 @@
others.forEach((o, id) => {
/* host เดินบอทเองตรงๆ (o.x/o.y) → ข้าม lerp; แต่ client อื่นต้อง lerp ตามตำแหน่งที่ host ส่งมา (o.tx/o.ty) */
if (playBotsEnabled() && isPreviewBotId(id) && isMePlayHost()) return;
/* MG4 (quiz_carry) บอทเดินต่อเนื่องเร็ว → non-host ตามด้วย lerp 0.2 จะตามหลัง host ~1-2 ช่องตลอด ("เดินไม่ตรงกัน")
เร่ง lerp เฉพาะบอท quiz_carry ให้ตามตำแหน่ง host ทันขึ้น (ไม่แตะเกมอื่น/ผู้เล่นจริง) */
const lerpF = (isQuizCarry() && isPreviewBotId(id)) ? 0.5 : LERP;
/* MG4 (quiz_carry) + MG1 (quiz mng8a80o) บอทเดินต่อเนื่องเร็ว → non-host lerp 0.2 จะตามหลัง host ~1-2 ช่อง ("เดินไม่ตรงกัน")
เร่ง lerp เฉพาะบอท quiz/quiz_carry ให้ตามตำแหน่ง host ทันขึ้น (ไม่แตะเกมอื่น/ผู้เล่นจริง) */
const lerpF = (isPreviewBotId(id) && (isQuizCarry() || isQuiz())) ? 0.5 : LERP;
if (o.tx != null) o.x += (o.tx - o.x) * lerpF;
if (o.ty != null) o.y += (o.ty - o.y) * lerpF;
});
+1 -1
View File
@@ -5371,7 +5371,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.006232008"></script>
<script src="js/play.js?v=0.006232016"></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)
+4 -1
View File
@@ -7905,7 +7905,10 @@ io.on('connection', (socket) => {
const space = sid ? spaces.get(sid) : null;
if (!space || space.hostId !== socket.id) return; /* เฉพาะ host เท่านั้น */
if (!data || !Array.isArray(data.bots) || data.bots.length > 16) return;
socket.to(sid).emit('fill-bot-state', { bots: data.bots });
/* พ่วง hostId — ผู้รับ (non-host เท่านั้น เพราะ socket.to ไม่ส่งกลับผู้ส่ง) ใช้ยืนยันว่า "ตัวเองไม่ใช่ host"
แก้ playHostId ที่ผิดตอน race LobbyB→play (กัน 2 จอคิดว่าเป็น host → sim บอทคนละชุด) */
const humans = (Array.isArray(data.humans) ? data.humans.filter((h) => h != null).slice(0, 50).map(String) : undefined);
socket.to(sid).emit('fill-bot-state', { bots: data.bots, hostId: space.hostId, humans });
});
/* MG7 balloon_boss — relay กระสุนของผู้เล่น/บอท ให้ทุกคนในห้องเห็นตรงกัน (ทุกคนยิงได้ ไม่จำกัด host)