279 lines
11 KiB
JavaScript
279 lines
11 KiB
JavaScript
// Socket.IO клиент — инициализация и все обработчики
|
||
import { state } from '../core/state.js';
|
||
import { SERVER_URL, worldId, playerName } from '../config.js';
|
||
import { TILE, SEA_GY } from '../core/constants.js';
|
||
import { BLOCKS } from '../data/blocks.js';
|
||
import { k, setBlock, removeBlock, grid, blocks } 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';
|
||
|
||
let socket = null;
|
||
|
||
export { socket };
|
||
|
||
export function initSocket() {
|
||
try {
|
||
socket = io(SERVER_URL, {
|
||
path: '/socket.io/',
|
||
transports: ['websocket', 'polling']
|
||
});
|
||
|
||
socket.on('connect', () => {
|
||
console.log(`Connected to server: ${SERVER_URL}, socket.id: ${socket.id}, worldId: ${worldId}`);
|
||
state.mySocketId = socket.id;
|
||
state.isMultiplayer = true;
|
||
|
||
// Присоединяемся к миру
|
||
socket.emit('join_world', { world_id: worldId, player_name: playerName });
|
||
|
||
// Показываем в UI
|
||
state.worldIdEl.textContent = worldId;
|
||
state.multiplayerStatus.style.display = 'block';
|
||
});
|
||
|
||
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();
|
||
state.multiplayerStatus.style.display = 'none';
|
||
});
|
||
|
||
// Обработка world_state
|
||
socket.on('world_state', (data) => {
|
||
console.log('Received world_state:', data);
|
||
|
||
// Устанавливаем seed и перегенерируем мир если он изменился
|
||
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);
|
||
|
||
// Очищаем и перегенерируем мир с новым seed
|
||
state.generated.clear();
|
||
grid.clear();
|
||
blocks.length = 0;
|
||
state.placedBlocks = [];
|
||
state.removedBlocks = [];
|
||
console.log('World regenerated with new seed:', state.worldSeed);
|
||
}
|
||
|
||
// Применяем блоки — сохраняем в state.serverOverrides для применения после genColumn
|
||
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;
|
||
}
|
||
|
||
// Всегда считаем spawnPoint на клиенте по той же формуле что и surfaceGyAt
|
||
// Это гарантирует совпадение с terrain generation
|
||
{
|
||
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);
|
||
|
||
// Устанавливаем HP на 100% при каждом подключении к миру
|
||
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) {
|
||
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 };
|
||
state.serverMobs.set(m.id, sm);
|
||
}
|
||
}
|
||
});
|
||
|
||
// Игрок присоединился
|
||
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 (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 };
|
||
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) {
|
||
// Give loot to the killer
|
||
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;
|
||
}
|
||
} |