feat: server-authoritative mob sync for multiplayer — animals/hostiles now synced across players
This commit is contained in:
parent
0ed7d9966d
commit
1f921f7620
182
game.js
182
game.js
|
|
@ -83,8 +83,9 @@
|
|||
// ==================== SOCKET.IO КЛИЕНТ ====================
|
||||
let socket = null;
|
||||
let isMultiplayer = false; // Флаг для мультиплеерного режима
|
||||
const otherPlayers = new Map(); // socket_id -> {x, y, color}
|
||||
let mySocketId = null;
|
||||
const otherPlayers = new Map(); // socket_id -> {x, y, color}
|
||||
const serverMobs = new Map(); // id -> mob (server-authoritative in MP)
|
||||
let mySocketId = null;
|
||||
|
||||
// Throttle для отправки позиции (10-20 раз в секунду)
|
||||
let lastMoveSendTime = 0;
|
||||
|
|
@ -215,10 +216,18 @@
|
|||
});
|
||||
}
|
||||
}
|
||||
// Обновляем счётчик игроков
|
||||
playerCountEl.textContent = data.players.length;
|
||||
}
|
||||
});
|
||||
// Обновляем счётчик игроков
|
||||
playerCountEl.textContent = data.players.length;
|
||||
}
|
||||
// Server mobs
|
||||
if (data.mobs && Array.isArray(data.mobs)) {
|
||||
serverMobs.clear();
|
||||
for (const m of data.mobs) {
|
||||
const sm = { ...m, maxHp: m.maxHp || m.hp, vx: m.vx||0, vy: m.vy||0, grounded:false, inWater:false, aiT:0, dir:m.dir||1, dead:false, fuse:m.fuse||0, shootCooldown:2, speed: m.speed || 80 };
|
||||
serverMobs.set(m.id, sm);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Игрок присоединился
|
||||
socket.on('player_joined', (data) => {
|
||||
|
|
@ -257,13 +266,61 @@
|
|||
});
|
||||
|
||||
// Игрок покинул
|
||||
socket.on('player_left', (data) => {
|
||||
console.log('Player left:', data.socket_id);
|
||||
otherPlayers.delete(data.socket_id);
|
||||
addChatMessage('Система', `Игрок покинул игру`);
|
||||
// Обновляем видимость кнопки сохранения
|
||||
updateSaveButtonVisibility();
|
||||
});
|
||||
socket.on('player_left', (data) => {
|
||||
console.log('Player left:', data.socket_id);
|
||||
otherPlayers.delete(data.socket_id);
|
||||
addChatMessage('Система', `Игрок покинул игру`);
|
||||
// Обновляем видимость кнопки сохранения
|
||||
updateSaveButtonVisibility();
|
||||
});
|
||||
|
||||
// === MOB SYNC (multiplayer) ===
|
||||
|
||||
socket.on('mob_spawned', (data) => {
|
||||
const sm = { ...data, maxHp: data.maxHp || data.hp, vx: data.vx||0, vy: data.vy||0, grounded:false, inWater:false, aiT:0, dir:data.dir||1, dead:false, fuse:data.fuse||0, shootCooldown:2, speed: data.speed || 80 };
|
||||
serverMobs.set(data.id, sm);
|
||||
});
|
||||
|
||||
socket.on('mob_positions', (arr) => {
|
||||
for (const u of arr) {
|
||||
const sm = serverMobs.get(u.id);
|
||||
if (sm) { sm.x=u.x; sm.y=u.y; sm.vx=u.vx; sm.vy=u.vy; sm.dir=u.dir; sm.hp=u.hp; sm.fuse=u.fuse||0; }
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('mob_despawned', (data) => { serverMobs.delete(data.id); });
|
||||
|
||||
socket.on('mob_died', (data) => {
|
||||
const sm = serverMobs.get(data.id);
|
||||
if (sm && data.killer === mySocketId) {
|
||||
// Give loot to the killer
|
||||
if (sm.kind === 'chicken') playSound('hurt_chicken');
|
||||
inv.meat += (sm.kind==='chicken' ? 1 : 2);
|
||||
if (sm.kind === 'skeleton') {
|
||||
inv.arrow += 2 + Math.floor(Math.random()*3);
|
||||
if (Math.random() < 0.15) inv.bow = (inv.bow||0) + 1;
|
||||
}
|
||||
rebuildHotbar();
|
||||
}
|
||||
serverMobs.delete(data.id);
|
||||
});
|
||||
|
||||
socket.on('mob_hurt_ack', (data) => {
|
||||
const sm = serverMobs.get(data.id);
|
||||
if (sm) sm.hp = data.hp;
|
||||
});
|
||||
|
||||
socket.on('mob_explode', (data) => {
|
||||
explodeAt(data.gx, data.gy);
|
||||
serverMobs.delete(data.id);
|
||||
});
|
||||
|
||||
socket.on('mob_shoot', (data) => {
|
||||
projectiles.push({
|
||||
x: data.x, y: data.y, vx: data.vx, vy: data.vy,
|
||||
dmg: data.dmg, owner: 'mob', life: data.life
|
||||
});
|
||||
});
|
||||
|
||||
// Блок изменён
|
||||
socket.on('block_changed', (data) => {
|
||||
|
|
@ -1014,7 +1071,8 @@
|
|||
}
|
||||
|
||||
// Мобы — красные (враждебные) / зелёные (животные)
|
||||
for (const m of mobs) {
|
||||
const allMobsForMap = isMultiplayer ? Array.from(serverMobs.values()) : mobs;
|
||||
for (const m of allMobsForMap) {
|
||||
const dx = Math.floor(m.x / TILE) - startGX;
|
||||
const dy = Math.floor(m.y / TILE) - startGY;
|
||||
if (dx >= 0 && dx < viewW && dy >= 0 && dy < viewH) {
|
||||
|
|
@ -2232,28 +2290,38 @@
|
|||
|
||||
// клик по мобу (в режиме mine)
|
||||
if(mode()==='mine'){
|
||||
// Check server mobs first (multiplayer)
|
||||
if(isMultiplayer){
|
||||
for (const [id, sm] of serverMobs) {
|
||||
if(sm.dead) continue;
|
||||
if(wx>=sm.x && wx<=sm.x+sm.w && wy>=sm.y && wy<=sm.y+sm.h){
|
||||
let dmg = 1;
|
||||
const swordTypes = ['iron_sword','stone_sword','wood_sword'];
|
||||
for (const st of swordTypes) {
|
||||
if (inv[st] > 0) { dmg = TOOLS[st].damage || 3; useTool(st); break; }
|
||||
}
|
||||
socket.emit('mob_hurt', { id: sm.id, dmg });
|
||||
playSound('attack');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Local mobs (singleplayer or if not hit server mob)
|
||||
for(let i=mobs.length-1;i>=0;i--){
|
||||
const m = mobs[i];
|
||||
if(wx>=m.x && wx<=m.x+m.w && wy>=m.y && wy<=m.y+m.h){
|
||||
// Урон зависит от меча
|
||||
let dmg = 1;
|
||||
const swordTypes = ['iron_sword','stone_sword','wood_sword'];
|
||||
for (const st of swordTypes) {
|
||||
if (inv[st] > 0) {
|
||||
dmg = TOOLS[st].damage || 3;
|
||||
useTool(st);
|
||||
break;
|
||||
}
|
||||
if (inv[st] > 0) { dmg = TOOLS[st].damage || 3; useTool(st); break; }
|
||||
}
|
||||
m.hp -= dmg;
|
||||
m.vx += (m.x - player.x) * 2;
|
||||
m.vy -= 200;
|
||||
playSound('attack'); // Звук атаки игрока
|
||||
playSound('attack');
|
||||
if(m.hp<=0){
|
||||
// дроп еды
|
||||
if(m.kind === 'chicken') playSound('hurt_chicken');
|
||||
inv.meat += (m.kind==='chicken' ? 1 : 2);
|
||||
// скелет дропает стрелы (иногда лук)
|
||||
if(m.kind === 'skeleton'){
|
||||
inv.arrow += 2 + Math.floor(Math.random()*3);
|
||||
if(Math.random() < 0.15) inv.bow = (inv.bow||0) + 1;
|
||||
|
|
@ -3038,24 +3106,39 @@
|
|||
continue;
|
||||
}
|
||||
} else {
|
||||
// Попал в моба
|
||||
for(let j = mobs.length-1; j>=0; j--){
|
||||
const m = mobs[j];
|
||||
if(p.x > m.x && p.x < m.x+m.w && p.y > m.y && p.y < m.y+m.h){
|
||||
m.hp -= p.dmg;
|
||||
m.vx += p.vx * 0.2;
|
||||
m.vy -= 200;
|
||||
if(m.hp <= 0){
|
||||
inv.meat += (m.kind==='chicken' ? 1 : 2);
|
||||
if(m.kind === 'skeleton'){
|
||||
inv.arrow += 2 + Math.floor(Math.random()*3);
|
||||
if(Math.random() < 0.15) inv.bow = (inv.bow||0) + 1;
|
||||
}
|
||||
mobs.splice(j, 1);
|
||||
rebuildHotbar();
|
||||
// Попал в моба — server mobs first in multiplayer
|
||||
let hitMob = false;
|
||||
if(isMultiplayer){
|
||||
for (const [id, sm] of serverMobs) {
|
||||
if(sm.dead) continue;
|
||||
if(p.x > sm.x && p.x < sm.x+sm.w && p.y > sm.y && p.y < sm.y+sm.h){
|
||||
socket.emit('mob_arrow_hit', { id: sm.id, dmg: p.dmg, vx: p.vx });
|
||||
projectiles.splice(i, 1);
|
||||
hitMob = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!hitMob){
|
||||
// Local mobs
|
||||
for(let j = mobs.length-1; j>=0; j--){
|
||||
const m = mobs[j];
|
||||
if(p.x > m.x && p.x < m.x+m.w && p.y > m.y && p.y < m.y+m.h){
|
||||
m.hp -= p.dmg;
|
||||
m.vx += p.vx * 0.2;
|
||||
m.vy -= 200;
|
||||
if(m.hp <= 0){
|
||||
inv.meat += (m.kind==='chicken' ? 1 : 2);
|
||||
if(m.kind === 'skeleton'){
|
||||
inv.arrow += 2 + Math.floor(Math.random()*3);
|
||||
if(Math.random() < 0.15) inv.bow = (inv.bow||0) + 1;
|
||||
}
|
||||
mobs.splice(j, 1);
|
||||
rebuildHotbar();
|
||||
}
|
||||
projectiles.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
projectiles.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3074,9 +3157,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
// mobs spawn (с обеих сторон камеры)
|
||||
// mobs spawn (с обеих сторон камеры) — только в одиночном режиме
|
||||
spawnT += dt;
|
||||
if(spawnT > 1.8 && mobs.length < 30){
|
||||
if(!isMultiplayer && spawnT > 1.8 && mobs.length < 30){
|
||||
spawnT = 0;
|
||||
|
||||
// Выбираем сторону спавна (левая или правая)
|
||||
|
|
@ -3118,11 +3201,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
// mobs update
|
||||
for(let i=mobs.length-1;i>=0;i--){
|
||||
const m = mobs[i];
|
||||
mobAI(m, dt);
|
||||
if(m.hp<=0) mobs.splice(i,1);
|
||||
// mobs update — только локальные (singleplayer)
|
||||
if(!isMultiplayer){
|
||||
for(let i=mobs.length-1;i>=0;i--){
|
||||
const m = mobs[i];
|
||||
mobAI(m, dt);
|
||||
if(m.hp<=0) mobs.splice(i,1);
|
||||
}
|
||||
}
|
||||
|
||||
// particles
|
||||
|
|
@ -3201,7 +3286,8 @@
|
|||
}
|
||||
|
||||
// mobs
|
||||
for(const m of mobs){
|
||||
const allMobsRender = isMultiplayer ? Array.from(serverMobs.values()) : mobs;
|
||||
for(const m of allMobsRender){
|
||||
if(m.kind==='zombie'){
|
||||
ctx.fillStyle = '#2ecc71';
|
||||
ctx.fillRect(m.x, m.y, m.w, m.h);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<title>GrechkaCraft: Multiplayer</title>
|
||||
<!-- Socket.io Client -->
|
||||
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
|
||||
<link rel="stylesheet" href="style.css?v=4">
|
||||
<link rel="stylesheet" href="style.css?v=5">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
@ -92,6 +92,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script src="game.js?v=6"></script>
|
||||
<script src="game.js?v=7"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Reference in New Issue