join room เพิ่มรูปให้เข้าใจง่าย host client game 1-7
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user