// Socket.IO клиент — инициализация и все обработчики import { state } from '../core/state.js'; import { SERVER_URL } from '../config.js'; import { TILE, SEA_GY } from '../core/constants.js'; import { BLOCKS } from '../data/blocks.js'; import { k, setBlock, removeBlock, grid, blocks, getBlock } from '../world/world-storage.js'; import { genColumn, surfaceGyAt } from '../world/generation.js'; import { addChatMessage } from '../ui/chat.js'; import { calculateDamage } from '../entities/player.js'; import { playSound } from '../audio/sound-engine.js'; import { rebuildHotbar } from '../ui/hotbar.js'; import { explodeAt } from '../world/tnt.js'; import { updateSaveButtonVisibility } from '../ui/save-controls.js'; import { getRandomPlayerColor, sendPlayerPosition, sendBlockChange } from './socket-helpers.js'; import { initTradeSocket } from '../ui/trade.js'; // Mob sizes for server mob sync (server doesn't send w/h) const MOB_SIZES = { pig: { w: 34, h: 24 }, chicken: { w: 26, h: 22 }, zombie: { w: 34, h: 50 }, creeper: { w: 34, h: 50 }, skeleton: { w: 34, h: 50 }, scorpion: { w: 26, h: 26 }, polar_bear: { w: 40, h: 34 }, slime: { w: 24, h: 24 }, eagle: { w: 30, h: 22 }, }; function applyMobSize(sm) { const s = MOB_SIZES[sm.kind]; if (s && !sm.w) { sm.w = s.w; sm.h = s.h; } return sm; } let socket = null; export { socket }; export function initSocket() { try { socket = io(SERVER_URL, { path: '/socket.io/', transports: ['websocket', 'polling'] }); // Store socket on state for other modules state.socket = socket; socket.on('connect', () => { console.log(`Connected to server: ${SERVER_URL}, socket.id: ${socket.id}, worldId: ${state.worldId}`); state.mySocketId = socket.id; state.isMultiplayer = true; // Присоединяемся к миру socket.emit('join_world', { world_id: state.worldId, player_name: state.playerName }); // Показываем в UI if (state.worldIdEl) state.worldIdEl.textContent = state.worldId; if (state.multiplayerStatus) state.multiplayerStatus.style.display = 'block'; // Initialize trade socket handlers initTradeSocket(socket); }); socket.on('connect_error', (error) => { console.error('Socket connection error:', error); state.isMultiplayer = false; }); socket.on('disconnect', () => { console.log('Disconnected from server'); state.isMultiplayer = false; state.otherPlayers.clear(); if (state.multiplayerStatus) state.multiplayerStatus.style.display = 'none'; }); // Обработка world_state socket.on('world_state', (data) => { console.log('Received world_state:', data); if (data.seed !== undefined && data.seed !== state.worldSeed) { const oldSeed = state.worldSeed; state.worldSeed = data.seed; console.log('World seed changed from', oldSeed, 'to', state.worldSeed); state.generated.clear(); grid.clear(); blocks.length = 0; state.placedBlocks = []; state.removedBlocks = []; console.log('World regenerated with new seed:', state.worldSeed); } if (data.blocks && Array.isArray(data.blocks)) { for (const block of data.blocks) { const key = k(block.gx, block.gy); state.serverOverrides.set(key, { op: block.op, t: block.t }); if (block.op === 'set') { setBlock(block.gx, block.gy, block.t, false); } else if (block.op === 'remove') { removeBlock(block.gx, block.gy); } } } if (data.time !== undefined) { state.worldTime = data.time; state.isNightTime = state.worldTime > 0.5; } // Spawn point { const startGX = 6; for (let dx = -3; dx <= 3; dx++) genColumn(startGX + dx); const surfaceY = surfaceGyAt(startGX); let safeGY = surfaceY - 1; const aboveBlock = getBlock(startGX, surfaceY - 1); if (aboveBlock && aboveBlock.t === 'water') { for (let gy = SEA_GY - 1; gy >= 0; gy--) { const b = getBlock(startGX, gy); if (!b || b.dead || b.t === 'air' || b.t === 'water') continue; safeGY = gy - 1; break; } } state.spawnPoint.x = startGX * TILE; state.spawnPoint.y = safeGY * TILE; console.log('Client-side spawn point:', state.spawnPoint, 'surfaceY:', surfaceY, 'safeGY:', safeGY); } state.player.x = state.spawnPoint.x; state.player.y = state.spawnPoint.y; state.player.vx = 0; state.player.vy = 0; state.player.fallStartY = state.player.y; console.log('Player moved to spawn point:', state.player.x, state.player.y); state.player.hp = 100; state.player.hunger = 100; state.player.o2 = 100; state.player.invuln = 0; console.log('[MULTIPLAYER CONNECT] Player HP set to 100% on connect'); if (data.players && Array.isArray(data.players)) { state.otherPlayers.clear(); for (const p of data.players) { if (p.socket_id !== state.mySocketId) { state.otherPlayers.set(p.socket_id, { x: p.x, y: p.y, color: getRandomPlayerColor(p.socket_id), name: p.player_name || 'Игрок' }); } } state.playerCountEl.textContent = data.players.length; } // Server mobs if (data.mobs && Array.isArray(data.mobs)) { state.serverMobs.clear(); for (const m of data.mobs) { let 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 }; applyMobSize(sm); state.serverMobs.set(m.id, sm); } } // Set myTgId from server if provided if (data.tg_id) state.myTgId = data.tg_id; }); socket.on('player_joined', (data) => { console.log('Player joined:', data.socket_id); if (data.socket_id !== state.mySocketId) { const spawnGX = 6; genColumn(spawnGX); const surfaceY = surfaceGyAt(spawnGX); const safeSpawnX = spawnGX * TILE; const safeSpawnY = (surfaceY - 1) * TILE; state.otherPlayers.set(data.socket_id, { x: safeSpawnX, y: safeSpawnY, color: getRandomPlayerColor(data.socket_id), name: data.player_name || 'Игрок' }); addChatMessage('Система', `Игрок присоединился`); updateSaveButtonVisibility(); } }); socket.on('player_moved', (data) => { if (data.socket_id !== state.mySocketId && state.otherPlayers.has(data.socket_id)) { const p = state.otherPlayers.get(data.socket_id); p.x = data.x; p.y = data.y; if (data.player_name) { p.name = data.player_name; } } }); socket.on('player_left', (data) => { console.log('Player left:', data.socket_id); state.otherPlayers.delete(data.socket_id); addChatMessage('Система', `Игрок покинул игру`); updateSaveButtonVisibility(); }); // === MOB SYNC === socket.on('mob_spawned', (data) => { let 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 }; applyMobSize(sm); state.serverMobs.set(data.id, sm); }); socket.on('mob_positions', (arr) => { for (const u of arr) { const sm = state.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) => { state.serverMobs.delete(data.id); }); socket.on('mob_died', (data) => { const sm = state.serverMobs.get(data.id); if (sm && data.killer === state.mySocketId) { if (sm.kind === 'chicken') playSound('hurt_chicken'); state.inv.meat += (sm.kind === 'chicken' ? 1 : 2); if (sm.kind === 'skeleton') { state.inv.arrow += 2 + Math.floor(Math.random() * 3); if (Math.random() < 0.15) state.inv.bow = (state.inv.bow || 0) + 1; } rebuildHotbar(); } state.serverMobs.delete(data.id); }); socket.on('mob_hurt_ack', (data) => { const sm = state.serverMobs.get(data.id); if (sm) sm.hp = data.hp; }); socket.on('mob_explode', (data) => { explodeAt(data.gx, data.gy); state.serverMobs.delete(data.id); }); socket.on('mob_shoot', (data) => { state.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) => { const key = k(data.gx, data.gy); state.serverOverrides.set(key, { op: data.op, t: data.t }); if (data.op === 'set') { setBlock(data.gx, data.gy, data.t, false); } else if (data.op === 'remove') { removeBlock(data.gx, data.gy); } }); socket.on('chat_message', (data) => { const senderName = data.socket_id === state.mySocketId ? 'Вы' : `Игрок ${data.socket_id.substring(0, 6)}`; addChatMessage(senderName, data.message); }); socket.on('time_update', (data) => { if (data.time !== undefined) { state.worldTime = data.time; state.isNightTime = state.worldTime > 0.5; } }); } catch (e) { console.error('Error initializing socket:', e); state.isMultiplayer = false; } }