grechka-game/srcv3/multiplayer/socket.js

302 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 { k3, setBlock, removeBlock, grid, blocks, getBlock } from '../world/world-storage3d.js';
import { occClear } from '../world/occ3d.js';
import { genColumn, surfaceGyAt } from '../world/generation3d.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 { explodeTNT3D } from '../world/tnt3d.js';
import { updateSaveButtonVisibility } from '../ui/save-controls.js';
import { getRandomPlayerColor, sendPlayerPosition, sendBlockChange } from './socket-helpers.js';
import { initTradeSocket } from '../ui/trade.js';
import { placePlayerOnStandSpot, ensurePlayerOnGround } from '../spawn3d.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;
// Окклюзивная сетка обновляется только из setBlock/removeBlock, а здесь
// мир чистится напрямую. Без сброса сетка осталась бы от старого сида
// и перестроение мешей отсекло бы свежие блоки как скрытые.
occClear();
state.placedBlocks = [];
state.removedBlocks = [];
console.log('World regenerated with new seed:', state.worldSeed);
// КРИТИЧНО: после перегенерации позиция игрока невалидна — он стоит
// в координатах ПРЕДЫДУЩЕГО мира и оказывается в яме. Ставим заново
// и пересчитываем опору. Иначе камера уходит в грунт, схлопывается
// в первое лицо (camDist = 0), и весь кадр занимает пустое небо.
if (typeof ensurePlayerOnGround === 'function') {
try { ensurePlayerOnGround(); } catch (e) { /* не критично */ }
}
}
if (data.blocks && Array.isArray(data.blocks)) {
for (const block of data.blocks) {
const key = k3(block.gx, block.gy, block.gz || 0);
state.serverOverrides.set(key, { op: block.op, t: block.t });
if (block.op === 'set') {
setBlock(block.gx, block.gy, block.gz || 0, block.t, false);
} else if (block.op === 'remove') {
removeBlock(block.gx, block.gy, block.gz || 0);
}
}
}
if (data.time !== undefined) {
state.worldTime = data.time;
state.isNightTime = state.worldTime > 0.5;
}
// Стартовая колонка: X=6 по центру мира, Z игрока.
// Z фиксируем ОДИН РАЗ при входе: если брать текущую позицию каждый
// раз, то второй world_state ищет площадку уже рядом с первой
// найденной точкой и уводит игрока всё дальше в сторону.
if (state.__spawnGZ === undefined) {
state.__spawnGZ = Math.floor((state.player.z || 0) / state.TILE);
}
const startGX = 6;
const startGZ = state.__spawnGZ;
// Спавн: единая реализация из spawn3d.js. Раньше здесь была вторая
// копия поиска площадки, которая считала feet как (top-1)*T и искала
// воздух НИЖЕ top — игрок тонул в грунте либо падал в яму.
placePlayerOnStandSpot(startGX, startGZ, 20);
// Позицию уже выставил placePlayerOnStandSpot (feet = (top+1)*T,
// player.y = feet - h). Здесь только обнуляем скорости.
state.player.vx = 0;
state.player.vy = 0;
state.player.vz = 0;
state.player.fallStartY = 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) {
// Позиция ДРУГОГО игрока. Формула та же, что и у нас: feet на
// верхней грани опоры. Раньше было (surfaceY - 1) * TILE — это
// тайл ПОД поверхностью, то есть чужий игрок появлялся в грунте.
const spawnGX = 6;
const spawnGZ = Math.floor((state.player.z || 0) / state.TILE);
genColumn(spawnGX, spawnGZ);
const surfaceY = surfaceGyAt(spawnGX, spawnGZ);
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;
// Ось Z обязательна: без неё чужие игроки стоят в одной плоскости
// и не двигаются вглубь мира. Раньше z просто игнорировался.
p.z = data.z || 0;
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);
// Ось Z у серверных мобов: без неё в сетевом режиме все мобы
// стоят в одной плоскости и не преследуют игрока по глубине.
if (sm.z === undefined) sm.z = data.z || 0;
if (sm.vz === undefined) sm.vz = data.vz || 0;
if (sm.d === undefined) sm.d = data.d || sm.w || 30;
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) => {
explodeTNT3D({ gx: data.gx, gy: data.gy, gz: data.gz || 0 }, state);
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 = k3(data.gx, data.gy, data.gz || 0);
state.serverOverrides.set(key, { op: data.op, t: data.t });
if (data.op === 'set') {
setBlock(data.gx, data.gy, data.gz || 0, data.t, false);
} else if (data.op === 'remove') {
removeBlock(data.gx, data.gy, data.gz || 0);
}
});
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;
}
}