284 lines
9.7 KiB
JavaScript
284 lines
9.7 KiB
JavaScript
// 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;
|
|
}
|
|
}
|