diff --git a/game.js b/game.js index c00fa4f..05458ae 100644 --- a/game.js +++ b/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); diff --git a/index.html b/index.html index 64dfd90..66e9300 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ GrechkaCraft: Multiplayer - + @@ -92,6 +92,6 @@ - +