1243 lines
51 KiB
JavaScript
1243 lines
51 KiB
JavaScript
const express = require('express');
|
||
const app = express();
|
||
const http = require('http').createServer(app);
|
||
const io = require('socket.io')(http);
|
||
const path = require('path');
|
||
const fs = require('fs');
|
||
|
||
// Глобальная защита от падения сервера
|
||
process.on('uncaughtException', (err) => {
|
||
console.error('SERVER ERROR CAUGHT:', err);
|
||
});
|
||
|
||
app.use(express.static(path.join(__dirname, 'public')));
|
||
|
||
if (!fs.existsSync('./saves')){
|
||
fs.mkdirSync('./saves');
|
||
}
|
||
|
||
const BLOCK_SIZE = 32;
|
||
const WORLD_WIDTH = 400;
|
||
const WORLD_HEIGHT = 150;
|
||
const DAY_DURATION = 600000; // 10 минут
|
||
const GRAVITY = 0.5;
|
||
|
||
const BLOCKS = {
|
||
AIR: 0, DIRT: 1, STONE: 2, GRASS: 3, WOOD: 4, LEAVES: 5,
|
||
WORKBENCH: 6, FURNACE: 7, SAND: 8, COAL_ORE: 9, IRON_ORE: 10,
|
||
GOLD_ORE: 11, COPPER_ORE: 12, GLASS: 13, BRICK: 14, TORCH: 15,
|
||
WATER: 16, CHEST: 17
|
||
};
|
||
|
||
// Блоки, через которые можно проходить (фоновые, как деревья в Террарии)
|
||
const PASSABLE_BLOCKS = [BLOCKS.AIR, BLOCKS.WOOD, BLOCKS.LEAVES, BLOCKS.TORCH];
|
||
|
||
function isPassable(blockType) {
|
||
return PASSABLE_BLOCKS.includes(blockType);
|
||
}
|
||
const ITEMS = {
|
||
WOODEN_SWORD: 100, WOODEN_PICKAXE: 101, WOODEN_AXE: 102,
|
||
STONE_SWORD: 103, STONE_PICKAXE: 104, STONE_AXE: 105,
|
||
IRON_SWORD: 106, IRON_PICKAXE: 107, IRON_AXE: 108,
|
||
COAL: 109, IRON_BAR: 110, GOLD_BAR: 111, COPPER_BAR: 112,
|
||
HEALTH_POTION: 113, MANA_POTION: 114
|
||
};
|
||
|
||
// Характеристики мобов
|
||
const MOB_STATS = {
|
||
slime: {
|
||
hp: 20,
|
||
damage: 1,
|
||
speed: 1.5,
|
||
jumpForce: 6,
|
||
attackRange: 30,
|
||
aggroRange: 150,
|
||
attackCooldown: 1000,
|
||
width: 32,
|
||
height: 32
|
||
},
|
||
zombie: {
|
||
hp: 40,
|
||
damage: 2,
|
||
speed: 1.2,
|
||
jumpForce: 8,
|
||
attackRange: 35,
|
||
aggroRange: 200,
|
||
attackCooldown: 800,
|
||
width: 32,
|
||
height: 48
|
||
},
|
||
skeleton: {
|
||
hp: 30,
|
||
damage: 2,
|
||
speed: 1.8,
|
||
jumpForce: 7,
|
||
attackRange: 40,
|
||
aggroRange: 250,
|
||
attackCooldown: 1200,
|
||
width: 32,
|
||
height: 48
|
||
},
|
||
demon: {
|
||
hp: 60,
|
||
damage: 3,
|
||
speed: 2.0,
|
||
jumpForce: 10,
|
||
attackRange: 45,
|
||
aggroRange: 300,
|
||
attackCooldown: 600,
|
||
width: 32,
|
||
height: 36
|
||
}
|
||
};
|
||
|
||
const lobbies = {};
|
||
|
||
function getEmptyInventory() {
|
||
let inv = {};
|
||
[...Object.values(BLOCKS), ...Object.values(ITEMS)].forEach(id => inv[id] = 0);
|
||
return inv;
|
||
}
|
||
|
||
function generateWorld() {
|
||
let world = Array(WORLD_HEIGHT).fill().map(() => Array(WORLD_WIDTH).fill(BLOCKS.AIR));
|
||
const surfaceHeight = [];
|
||
|
||
// Базовый уровень поверхности
|
||
const baseLevel = 50;
|
||
|
||
// Уровень воды
|
||
const waterLevel = baseLevel + 12;
|
||
|
||
// Генерация поверхности с разнообразным рельефом
|
||
for (let x = 0; x < WORLD_WIDTH; x++) {
|
||
let height = baseLevel;
|
||
height += Math.sin(x * 0.02) * 8;
|
||
height += Math.sin(x * 0.05) * 4;
|
||
height += Math.cos(x * 0.1) * 2;
|
||
|
||
// Озеро слева (x: 15-70) - глубокая впадина
|
||
if (x >= 15 && x <= 70) {
|
||
const lakeCenter = 42;
|
||
const distFromCenter = Math.abs(x - lakeCenter);
|
||
const lakeHalfWidth = 28;
|
||
|
||
if (distFromCenter < lakeHalfWidth) {
|
||
const depthFactor = 1 - (distFromCenter / lakeHalfWidth);
|
||
const maxDepth = 20;
|
||
height = waterLevel + 3 + Math.floor(maxDepth * depthFactor * depthFactor);
|
||
}
|
||
}
|
||
|
||
surfaceHeight[x] = Math.floor(height);
|
||
}
|
||
|
||
// Заполнение блоками
|
||
for (let x = 0; x < WORLD_WIDTH; x++) {
|
||
const surface = surfaceHeight[x];
|
||
for (let y = surface; y < WORLD_HEIGHT; y++) {
|
||
if (y === surface) {
|
||
// Песок вокруг озера и на дне
|
||
if (x >= 10 && x <= 75) {
|
||
world[y][x] = BLOCKS.SAND;
|
||
} else {
|
||
world[y][x] = Math.random() < 0.1 ? BLOCKS.SAND : BLOCKS.GRASS;
|
||
}
|
||
} else if (y < surface + 4 + Math.floor(Math.random() * 2)) {
|
||
if (x >= 10 && x <= 75) {
|
||
world[y][x] = BLOCKS.SAND;
|
||
} else {
|
||
world[y][x] = BLOCKS.DIRT;
|
||
}
|
||
} else {
|
||
world[y][x] = BLOCKS.STONE;
|
||
const depth = y - surface;
|
||
if (Math.random() < 0.025 && depth > 8) world[y][x] = BLOCKS.COAL_ORE;
|
||
else if (Math.random() < 0.012 && depth > 15) world[y][x] = BLOCKS.COPPER_ORE;
|
||
else if (Math.random() < 0.010 && depth > 25) world[y][x] = BLOCKS.IRON_ORE;
|
||
else if (Math.random() < 0.006 && depth > 40) world[y][x] = BLOCKS.GOLD_ORE;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Заливаем озеро водой
|
||
for (let x = 15; x <= 70; x++) {
|
||
const bottomY = surfaceHeight[x];
|
||
if (bottomY > waterLevel) {
|
||
for (let y = waterLevel; y < bottomY; y++) {
|
||
if (y >= 0 && y < WORLD_HEIGHT) {
|
||
world[y][x] = BLOCKS.WATER;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Пещеры
|
||
for (let i = 0; i < 30; i++) {
|
||
let cx = Math.floor(Math.random() * (WORLD_WIDTH - 40)) + 20;
|
||
let cy = Math.floor(Math.random() * 50) + 70;
|
||
const caveLength = Math.floor(Math.random() * 40) + 20;
|
||
|
||
for (let j = 0; j < caveLength; j++) {
|
||
const radius = Math.floor(Math.random() * 3) + 2;
|
||
for (let dy = -radius; dy <= radius; dy++) {
|
||
for (let dx = -radius; dx <= radius; dx++) {
|
||
if (dx*dx + dy*dy <= radius*radius) {
|
||
const nx = cx + dx;
|
||
const ny = cy + dy;
|
||
if (ny >= 60 && ny < WORLD_HEIGHT && nx >= 0 && nx < WORLD_WIDTH) {
|
||
if (world[ny][nx] !== BLOCKS.WATER) {
|
||
world[ny][nx] = BLOCKS.AIR;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
cx += Math.floor(Math.random() * 5) - 2;
|
||
cy += Math.floor(Math.random() * 5) - 2;
|
||
cx = Math.max(5, Math.min(WORLD_WIDTH - 5, cx));
|
||
cy = Math.max(60, Math.min(WORLD_HEIGHT - 5, cy));
|
||
}
|
||
}
|
||
|
||
// Деревья справа от озера
|
||
for (let x = 85; x < WORLD_WIDTH - 10; x += Math.floor(Math.random() * 8) + 6) {
|
||
const surface = surfaceHeight[x];
|
||
if (!world[surface] || world[surface][x] !== BLOCKS.GRASS) continue;
|
||
|
||
if (surface < WORLD_HEIGHT - 1 && surface > 10) {
|
||
const treeHeight = Math.floor(Math.random() * 4) + 5;
|
||
for (let y = surface - treeHeight; y < surface; y++) {
|
||
if (y >= 0) world[y][x] = BLOCKS.WOOD;
|
||
}
|
||
// Крона (овальная форма - широкая и высокая)
|
||
for (let dy = -4; dy <= 1; dy++) {
|
||
for (let dx = -3; dx <= 3; dx++) {
|
||
const ty = surface - treeHeight + dy;
|
||
const tx = x + dx;
|
||
// Овальная форма: шире по X, выше по Y
|
||
const distX = dx / 3.5; // Ширина ~7 блоков
|
||
const distY = (dy + 1.5) / 2.5; // Высота ~5 блоков
|
||
const dist = Math.sqrt(distX * distX + distY * distY);
|
||
if (dist > 1) continue;
|
||
|
||
if (ty >= 0 && ty < WORLD_HEIGHT && tx >= 0 && tx < WORLD_WIDTH) {
|
||
if (world[ty][tx] === BLOCKS.AIR) {
|
||
world[ty][tx] = BLOCKS.LEAVES;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Немного деревьев слева от озера
|
||
for (let x = 3; x < 10; x += Math.floor(Math.random() * 4) + 3) {
|
||
const surface = surfaceHeight[x];
|
||
if (!world[surface] || world[surface][x] !== BLOCKS.GRASS) continue;
|
||
|
||
if (surface < WORLD_HEIGHT - 1 && surface > 10) {
|
||
const treeHeight = Math.floor(Math.random() * 3) + 4;
|
||
for (let y = surface - treeHeight; y < surface; y++) {
|
||
if (y >= 0) world[y][x] = BLOCKS.WOOD;
|
||
}
|
||
// Крона (овальная форма)
|
||
for (let dy = -3; dy <= 1; dy++) {
|
||
for (let dx = -2; dx <= 2; dx++) {
|
||
const ty = surface - treeHeight + dy;
|
||
const tx = x + dx;
|
||
// Овальная форма
|
||
const distX = dx / 2.5;
|
||
const distY = (dy + 1) / 2;
|
||
const dist = Math.sqrt(distX * distX + distY * distY);
|
||
if (dist > 1) continue;
|
||
|
||
if (ty >= 0 && ty < WORLD_HEIGHT && tx >= 0 && tx < WORLD_WIDTH) {
|
||
if (world[ty][tx] === BLOCKS.AIR) {
|
||
world[ty][tx] = BLOCKS.LEAVES;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return world;
|
||
}
|
||
|
||
function saveLobby(lobbyId) {
|
||
if (!lobbies[lobbyId]) return;
|
||
const savedPlayersData = lobbies[lobbyId].savedPlayers || {};
|
||
for (let socketId in lobbies[lobbyId].players) {
|
||
const p = lobbies[lobbyId].players[socketId];
|
||
savedPlayersData[p.nickname] = {
|
||
inventory: p.inventory,
|
||
hotbar: p.hotbarSlots,
|
||
health: p.health || 20,
|
||
team: p.team || null
|
||
};
|
||
}
|
||
const data = {
|
||
world: lobbies[lobbyId].world,
|
||
savedPlayers: savedPlayersData,
|
||
timeOfDay: lobbies[lobbyId].timeOfDay,
|
||
dayCount: lobbies[lobbyId].dayCount || 1, // Сохраняем счётчик дней
|
||
chests: lobbies[lobbyId].chests || {} // Сохраняем сундуки
|
||
};
|
||
fs.writeFile(`./saves/${lobbyId}.json`, JSON.stringify(data), (err) => { if (err) console.error(`Err save:`, err); });
|
||
}
|
||
|
||
function loadLobby(lobbyId) {
|
||
try {
|
||
if (fs.existsSync(`./saves/${lobbyId}.json`)) {
|
||
const raw = fs.readFileSync(`./saves/${lobbyId}.json`);
|
||
const data = JSON.parse(raw);
|
||
return {
|
||
world: data.world,
|
||
savedPlayers: data.savedPlayers || {},
|
||
players: {},
|
||
mobs: [],
|
||
chests: data.chests || {},
|
||
droppedItems: [], // Выпавшие предметы
|
||
nextDropId: 1,
|
||
timeOfDay: data.timeOfDay || 0,
|
||
dayCount: data.dayCount || 1, // Счётчик дней
|
||
lastUpdate: Date.now()
|
||
};
|
||
}
|
||
} catch (e) { console.error("Load err:", e); }
|
||
return null;
|
||
}
|
||
|
||
// Проверка столкновения двух прямоугольников
|
||
function checkCollision(x1, y1, w1, h1, x2, y2, w2, h2) {
|
||
return x1 < x2 + w2 && x1 + w1 > x2 && y1 < y2 + h2 && y1 + h1 > y2;
|
||
}
|
||
|
||
// Расстояние между двумя объектами
|
||
function getDistance(obj1, obj2) {
|
||
const cx1 = obj1.x + (obj1.width || 24) / 2;
|
||
const cy1 = obj1.y + (obj1.height || 48) / 2;
|
||
const cx2 = obj2.x + obj2.width / 2;
|
||
const cy2 = obj2.y + obj2.height / 2;
|
||
return Math.sqrt(Math.pow(cx1 - cx2, 2) + Math.pow(cy1 - cy2, 2));
|
||
}
|
||
|
||
// Найти ближайшего игрока к мобу
|
||
function findNearestPlayer(mob, players) {
|
||
let nearest = null;
|
||
let minDist = Infinity;
|
||
|
||
for (let id in players) {
|
||
const player = players[id];
|
||
if (player.health <= 0) continue; // Пропускаем мёртвых игроков
|
||
|
||
const dist = getDistance(player, mob);
|
||
if (dist < minDist) {
|
||
minDist = dist;
|
||
nearest = { id, player, distance: dist };
|
||
}
|
||
}
|
||
|
||
return nearest;
|
||
}
|
||
|
||
// Проверка столкновения с блоками (игнорирует фоновые блоки)
|
||
function checkBlockCollision(x, y, width, height, world) {
|
||
const left = Math.floor(x / BLOCK_SIZE);
|
||
const right = Math.floor((x + width - 1) / BLOCK_SIZE);
|
||
const top = Math.floor(y / BLOCK_SIZE);
|
||
const bottom = Math.floor((y + height - 1) / BLOCK_SIZE);
|
||
|
||
for (let by = top; by <= bottom; by++) {
|
||
for (let bx = left; bx <= right; bx++) {
|
||
if (by >= 0 && by < WORLD_HEIGHT && bx >= 0 && bx < WORLD_WIDTH) {
|
||
const block = world[by] && world[by][bx];
|
||
if (block && !isPassable(block)) return true;
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
setInterval(() => { Object.keys(lobbies).forEach(id => saveLobby(id)); }, 30000);
|
||
|
||
// === ТЕКУЧЕСТЬ ВОДЫ ===
|
||
const WATER_UPDATE_INTERVAL = 100; // Быстрее обновление
|
||
const WATER_MAX_SPREAD = 7; // Максимальное распространение в стороны
|
||
|
||
function updateWaterForLobby(lobby, lobbyId) {
|
||
const world = lobby.world;
|
||
const waterUpdates = [];
|
||
|
||
// Функция подсчёта расстояния до источника воды (блок воды сверху или большое количество воды рядом)
|
||
function countNearbyWaterSource(x, y) {
|
||
let waterAbove = 0;
|
||
// Проверяем воду сверху (это источник)
|
||
for (let checkY = y - 1; checkY >= Math.max(0, y - 10); checkY--) {
|
||
if (world[checkY] && world[checkY][x] === BLOCKS.WATER) {
|
||
waterAbove++;
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
return waterAbove;
|
||
}
|
||
|
||
// Функция проверки, есть ли рядом источник воды
|
||
function hasWaterSource(x, y) {
|
||
// Проверяем воду сверху
|
||
if (y > 0 && world[y - 1] && world[y - 1][x] === BLOCKS.WATER) {
|
||
return true;
|
||
}
|
||
|
||
// Проверяем сколько воды рядом по горизонтали в пределах WATER_MAX_SPREAD
|
||
let waterLeft = 0;
|
||
let waterRight = 0;
|
||
|
||
for (let i = 1; i <= WATER_MAX_SPREAD; i++) {
|
||
if (x - i >= 0 && world[y] && world[y][x - i] === BLOCKS.WATER) {
|
||
// Проверяем есть ли у этой воды источник сверху
|
||
if (y > 0 && world[y - 1] && world[y - 1][x - i] === BLOCKS.WATER) {
|
||
return true;
|
||
}
|
||
waterLeft++;
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
|
||
for (let i = 1; i <= WATER_MAX_SPREAD; i++) {
|
||
if (x + i < WORLD_WIDTH && world[y] && world[y][x + i] === BLOCKS.WATER) {
|
||
// Проверяем есть ли у этой воды источник сверху
|
||
if (y > 0 && world[y - 1] && world[y - 1][x + i] === BLOCKS.WATER) {
|
||
return true;
|
||
}
|
||
waterRight++;
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
// Функция подсчёта расстояния до ближайшего источника
|
||
function getDistanceToSource(x, y) {
|
||
// Ищем воду с источником сверху
|
||
for (let dist = 0; dist <= WATER_MAX_SPREAD; dist++) {
|
||
// Проверяем слева
|
||
if (x - dist >= 0 && world[y] && world[y][x - dist] === BLOCKS.WATER) {
|
||
if (y > 0 && world[y - 1] && world[y - 1][x - dist] === BLOCKS.WATER) {
|
||
return dist;
|
||
}
|
||
}
|
||
// Проверяем справа
|
||
if (x + dist < WORLD_WIDTH && world[y] && world[y][x + dist] === BLOCKS.WATER) {
|
||
if (y > 0 && world[y - 1] && world[y - 1][x + dist] === BLOCKS.WATER) {
|
||
return dist;
|
||
}
|
||
}
|
||
}
|
||
return WATER_MAX_SPREAD + 1;
|
||
}
|
||
|
||
// Сканируем весь мир
|
||
for (let y = WORLD_HEIGHT - 2; y >= 0; y--) {
|
||
if (!world[y]) continue;
|
||
for (let x = 0; x < WORLD_WIDTH; x++) {
|
||
if (world[y][x] === BLOCKS.WATER) {
|
||
// Течём вниз - всегда
|
||
if (y + 1 < WORLD_HEIGHT && world[y + 1] && world[y + 1][x] === BLOCKS.AIR) {
|
||
waterUpdates.push({ x, y: y + 1, type: BLOCKS.WATER });
|
||
}
|
||
|
||
// Течём в стороны только если снизу твёрдый блок
|
||
if (y + 1 < WORLD_HEIGHT && world[y + 1] && world[y + 1][x] !== BLOCKS.AIR && world[y + 1][x] !== BLOCKS.WATER) {
|
||
// Проверяем расстояние до источника
|
||
const distToSource = getDistanceToSource(x, y);
|
||
|
||
// Распространяемся только если не достигли максимума
|
||
if (distToSource < WATER_MAX_SPREAD) {
|
||
// Влево
|
||
if (x - 1 >= 0 && world[y][x - 1] === BLOCKS.AIR) {
|
||
const newDist = getDistanceToSource(x - 1, y);
|
||
if (newDist <= WATER_MAX_SPREAD) {
|
||
waterUpdates.push({ x: x - 1, y, type: BLOCKS.WATER });
|
||
}
|
||
}
|
||
// Вправо
|
||
if (x + 1 < WORLD_WIDTH && world[y][x + 1] === BLOCKS.AIR) {
|
||
const newDist = getDistanceToSource(x + 1, y);
|
||
if (newDist <= WATER_MAX_SPREAD) {
|
||
waterUpdates.push({ x: x + 1, y, type: BLOCKS.WATER });
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Применяем обновления и отправляем клиентам
|
||
const maxUpdates = 50; // Больше обновлений за тик
|
||
for (let i = 0; i < Math.min(waterUpdates.length, maxUpdates); i++) {
|
||
const update = waterUpdates[i];
|
||
if (world[update.y] && world[update.y][update.x] === BLOCKS.AIR) {
|
||
world[update.y][update.x] = update.type;
|
||
// Отправляем обновление всем игрокам в лобби
|
||
io.to(lobbyId).emit('blockUpdated', { x: update.x, y: update.y, type: update.type });
|
||
}
|
||
}
|
||
}
|
||
|
||
// === ГЛАВНЫЙ ИГРОВОЙ ЦИКЛ ===
|
||
setInterval(() => {
|
||
try {
|
||
const dt = 33;
|
||
const now = Date.now();
|
||
|
||
Object.keys(lobbies).forEach(lobbyId => {
|
||
const lobby = lobbies[lobbyId];
|
||
if (!lobby) return;
|
||
|
||
// Счётчик дней
|
||
const prevTimeOfDay = lobby.timeOfDay;
|
||
lobby.timeOfDay += dt / DAY_DURATION;
|
||
if (lobby.timeOfDay >= 1) {
|
||
lobby.timeOfDay = 0;
|
||
lobby.dayCount = (lobby.dayCount || 1) + 1; // Увеличиваем счётчик дней
|
||
}
|
||
if (!lobby.dayCount) lobby.dayCount = 1;
|
||
|
||
// --- СПАВН МОБОВ ---
|
||
const isNight = lobby.timeOfDay > 0.40 && lobby.timeOfDay < 0.70;
|
||
const dayCount = lobby.dayCount || 1;
|
||
|
||
// Днём очень мало мобов (1-2 слизня), ночью больше
|
||
let maxMobs = isNight ? Math.min(3 + dayCount, 12) : 2;
|
||
let spawnChance = isNight ? 0.015 : 0.002;
|
||
|
||
const playerIds = Object.keys(lobby.players);
|
||
if (lobby.mobs.length < maxMobs && Math.random() < spawnChance && playerIds.length > 0) {
|
||
// Выбираем случайного игрока для спавна рядом с ним
|
||
const randomPlayer = lobby.players[playerIds[Math.floor(Math.random() * playerIds.length)]];
|
||
|
||
// Спавн за пределами экрана (500-800 пикселей от игрока)
|
||
const minDistance = 500;
|
||
const maxDistance = 800;
|
||
const spawnDistance = minDistance + Math.random() * (maxDistance - minDistance);
|
||
const spawnDirection = Math.random() > 0.5 ? 1 : -1;
|
||
|
||
let spawnX = randomPlayer.x + spawnDirection * spawnDistance;
|
||
|
||
// Ограничиваем границами мира
|
||
spawnX = Math.max(BLOCK_SIZE * 5, Math.min(spawnX, (WORLD_WIDTH - 5) * BLOCK_SIZE));
|
||
|
||
// Проверяем что спавн далеко от ВСЕХ игроков И от других мобов
|
||
let tooClose = false;
|
||
for (let pid of playerIds) {
|
||
const p = lobby.players[pid];
|
||
const dist = Math.abs(p.x - spawnX);
|
||
if (dist < 450) {
|
||
tooClose = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Проверяем что не слишком близко к другим мобам (чтобы не сбивались в кучу)
|
||
if (!tooClose) {
|
||
for (let mob of lobby.mobs) {
|
||
const dist = Math.abs(mob.x - spawnX);
|
||
if (dist < 150) {
|
||
tooClose = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (tooClose) return; // Не спавним если слишком близко
|
||
|
||
// Находим высоту спавна НА ЗЕМЛЕ (ищем твёрдый блок, игнорируя деревья)
|
||
let spawnY = 0;
|
||
const bx = Math.floor(spawnX / BLOCK_SIZE);
|
||
let foundGround = false;
|
||
for(let y = 0; y < WORLD_HEIGHT; y++) {
|
||
const block = lobby.world[y] && lobby.world[y][bx];
|
||
if (block && !isPassable(block) && block !== BLOCKS.WATER) {
|
||
spawnY = (y - 1) * BLOCK_SIZE; // Спавн прямо над землёй
|
||
foundGround = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!foundGround) return; // Не спавним если нет земли
|
||
|
||
// Определение типа моба с прогрессией по дням
|
||
// День 1-4: только слизни
|
||
// День 5-9: слизни + зомби
|
||
// День 10+: слизни + зомби + скелеты
|
||
// Демоны не спавнятся обычным способом
|
||
let type = 'slime';
|
||
|
||
if (isNight) {
|
||
const rand = Math.random();
|
||
|
||
if (dayCount >= 10) {
|
||
// День 10+: слизни, зомби, скелеты
|
||
if (rand < 0.35) type = 'zombie';
|
||
else if (rand < 0.55) type = 'skeleton';
|
||
else type = 'slime';
|
||
} else if (dayCount >= 5) {
|
||
// День 5-9: слизни и зомби
|
||
if (rand < 0.45) type = 'zombie';
|
||
else type = 'slime';
|
||
} else {
|
||
// День 1-4: только слизни
|
||
type = 'slime';
|
||
}
|
||
} else {
|
||
// Днём только слизни (очень редко)
|
||
type = 'slime';
|
||
}
|
||
|
||
const stats = MOB_STATS[type];
|
||
|
||
lobby.mobs.push({
|
||
id: Date.now() + Math.random(),
|
||
x: spawnX,
|
||
y: spawnY - stats.height + BLOCK_SIZE, // Корректная позиция на земле
|
||
vx: 0,
|
||
vy: 0,
|
||
width: stats.width,
|
||
height: stats.height,
|
||
health: stats.hp,
|
||
maxHealth: stats.hp,
|
||
type: type,
|
||
direction: 1,
|
||
lastAttack: 0,
|
||
targetPlayer: null,
|
||
onGround: true // Спавнится на земле
|
||
});
|
||
}
|
||
|
||
// --- ИИ И ФИЗИКА МОБОВ ---
|
||
lobby.mobs.forEach(mob => {
|
||
const stats = MOB_STATS[mob.type] || MOB_STATS.slime;
|
||
|
||
// Поиск ближайшего игрока
|
||
const nearest = findNearestPlayer(mob, lobby.players);
|
||
|
||
if (nearest && nearest.distance < stats.aggroRange) {
|
||
mob.targetPlayer = nearest.id;
|
||
const player = nearest.player;
|
||
|
||
// Движение к игроку
|
||
const dx = player.x - mob.x;
|
||
|
||
if (Math.abs(dx) > stats.attackRange) {
|
||
// Двигаемся к игроку
|
||
mob.vx = dx > 0 ? stats.speed : -stats.speed;
|
||
mob.direction = dx > 0 ? 1 : -1;
|
||
} else {
|
||
// Близко к игроку - атакуем
|
||
mob.vx *= 0.8; // Замедляемся
|
||
|
||
// Проверяем кулдаун атаки
|
||
if (now - mob.lastAttack > stats.attackCooldown) {
|
||
// Проверяем коллизию для атаки
|
||
const attackDist = getDistance(player, mob);
|
||
|
||
if (attackDist < stats.attackRange + 20) {
|
||
mob.lastAttack = now;
|
||
|
||
// Наносим урон игроку
|
||
player.health = (player.health || 20) - stats.damage;
|
||
|
||
// Отталкиваем игрока
|
||
const knockbackX = (player.x - mob.x) > 0 ? 8 : -8;
|
||
const knockbackY = -5;
|
||
|
||
// Отправляем событие урона
|
||
io.to(nearest.id).emit('playerDamaged', {
|
||
health: player.health,
|
||
knockbackX: knockbackX,
|
||
knockbackY: knockbackY,
|
||
attackerId: mob.id,
|
||
damage: stats.damage
|
||
});
|
||
|
||
// Если игрок умер
|
||
if (player.health <= 0) {
|
||
io.to(nearest.id).emit('playerDied');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Прыжок если игрок выше или есть препятствие
|
||
if (mob.onGround) {
|
||
const playerAbove = player.y < mob.y - 32;
|
||
const obstacleAhead = checkBlockCollision(
|
||
mob.x + mob.direction * 20,
|
||
mob.y - 10,
|
||
mob.width,
|
||
mob.height - 10,
|
||
lobby.world
|
||
);
|
||
|
||
if (playerAbove || obstacleAhead) {
|
||
mob.vy = -stats.jumpForce;
|
||
mob.onGround = false;
|
||
}
|
||
}
|
||
} else {
|
||
// Нет цели - бродим случайно
|
||
mob.targetPlayer = null;
|
||
|
||
if (Math.random() < 0.02) {
|
||
mob.vx = (Math.random() - 0.5) * stats.speed * 2;
|
||
mob.direction = mob.vx > 0 ? 1 : -1;
|
||
}
|
||
|
||
// Случайные прыжки для слизней
|
||
if (mob.type === 'slime' && mob.onGround && Math.random() < 0.03) {
|
||
mob.vy = -stats.jumpForce;
|
||
mob.onGround = false;
|
||
}
|
||
}
|
||
|
||
// Гравитация
|
||
mob.vy += GRAVITY;
|
||
if (mob.vy > 15) mob.vy = 15;
|
||
|
||
// Применяем скорость
|
||
mob.x += mob.vx;
|
||
mob.y += mob.vy;
|
||
|
||
// Трение
|
||
mob.vx *= 0.95;
|
||
|
||
// Коллизия с землёй (игнорируем фоновые блоки)
|
||
const bx = Math.floor((mob.x + mob.width/2) / BLOCK_SIZE);
|
||
const by = Math.floor((mob.y + mob.height) / BLOCK_SIZE);
|
||
|
||
mob.onGround = false;
|
||
if (by >= 0 && by < WORLD_HEIGHT && bx >= 0 && bx < WORLD_WIDTH) {
|
||
const groundBlock = lobby.world[by] && lobby.world[by][bx];
|
||
if (groundBlock && !isPassable(groundBlock)) {
|
||
mob.y = by * BLOCK_SIZE - mob.height;
|
||
mob.vy = 0;
|
||
mob.onGround = true;
|
||
}
|
||
}
|
||
|
||
// Коллизия со стенами (игнорируем фоновые блоки)
|
||
const wallBx = Math.floor((mob.x + mob.width/2 + mob.direction * mob.width/2) / BLOCK_SIZE);
|
||
const wallBy = Math.floor((mob.y + mob.height/2) / BLOCK_SIZE);
|
||
if (wallBy >= 0 && wallBy < WORLD_HEIGHT && wallBx >= 0 && wallBx < WORLD_WIDTH) {
|
||
const wallBlock = lobby.world[wallBy] && lobby.world[wallBy][wallBx];
|
||
if (wallBlock && !isPassable(wallBlock)) {
|
||
mob.vx = 0;
|
||
mob.x -= mob.direction * 2;
|
||
}
|
||
}
|
||
|
||
// Границы мира
|
||
if (mob.x < 0) { mob.x = 0; mob.vx *= -1; }
|
||
if (mob.x > (WORLD_WIDTH - 1) * BLOCK_SIZE) { mob.x = (WORLD_WIDTH - 1) * BLOCK_SIZE; mob.vx *= -1; }
|
||
if (mob.y > (WORLD_HEIGHT + 10) * BLOCK_SIZE) mob.health = 0;
|
||
});
|
||
|
||
// Удаляем мёртвых мобов
|
||
lobby.mobs = lobby.mobs.filter(m => m.health > 0);
|
||
|
||
// Обновляем текучесть воды
|
||
updateWaterForLobby(lobby, lobbyId);
|
||
|
||
// Отправляем обновление состояния
|
||
io.to(lobbyId).emit('gameStateUpdate', {
|
||
timeOfDay: lobby.timeOfDay,
|
||
dayCount: lobby.dayCount || 1,
|
||
mobs: lobby.mobs.map(m => ({
|
||
id: m.id,
|
||
x: m.x,
|
||
y: m.y,
|
||
width: m.width,
|
||
height: m.height,
|
||
health: m.health,
|
||
maxHealth: m.maxHealth,
|
||
type: m.type,
|
||
direction: m.direction,
|
||
targetPlayer: m.targetPlayer
|
||
}))
|
||
});
|
||
});
|
||
} catch (err) {
|
||
console.error("Game loop error:", err);
|
||
}
|
||
}, 33);
|
||
|
||
io.on('connection', (socket) => {
|
||
let currentLobby = null;
|
||
|
||
socket.on('getLobbies', () => {
|
||
try {
|
||
const savedFiles = fs.readdirSync('./saves').filter(f => f.endsWith('.json')).map(f => f.replace('.json', ''));
|
||
const activeIds = Object.keys(lobbies);
|
||
const allIds = [...new Set([...savedFiles, ...activeIds])];
|
||
socket.emit('lobbyList', allIds.map(id => ({
|
||
id,
|
||
players: lobbies[id] ? Object.keys(lobbies[id].players).length : 0,
|
||
status: lobbies[id] ? 'Active' : 'Saved'
|
||
})));
|
||
} catch(e) { socket.emit('lobbyList', []); }
|
||
});
|
||
|
||
// Удаление мира
|
||
socket.on('deleteWorld', (lobbyId) => {
|
||
try {
|
||
// Проверяем, что в лобби никого нет
|
||
if (lobbies[lobbyId] && Object.keys(lobbies[lobbyId].players).length > 0) {
|
||
socket.emit('errorMsg', 'Нельзя удалить мир, в котором есть игроки!');
|
||
return;
|
||
}
|
||
|
||
// Удаляем из памяти
|
||
if (lobbies[lobbyId]) {
|
||
delete lobbies[lobbyId];
|
||
}
|
||
|
||
// Удаляем файл сохранения
|
||
const savePath = `./saves/${lobbyId}.json`;
|
||
if (fs.existsSync(savePath)) {
|
||
fs.unlinkSync(savePath);
|
||
}
|
||
|
||
socket.emit('worldDeleted', lobbyId);
|
||
// Обновляем список лобби для всех
|
||
io.emit('lobbyListUpdated');
|
||
} catch(e) {
|
||
console.error('Error deleting world:', e);
|
||
socket.emit('errorMsg', 'Ошибка при удалении мира');
|
||
}
|
||
});
|
||
|
||
socket.on('createLobby', (data) => handleJoin(socket, data));
|
||
socket.on('joinLobby', (data) => handleJoin(socket, data));
|
||
|
||
function handleJoin(socket, data) {
|
||
const { lobbyId, nickname } = data;
|
||
let safeNick = (nickname || "Player").trim().substring(0, 15);
|
||
if (safeNick.length === 0) safeNick = "Player";
|
||
|
||
if (!lobbies[lobbyId]) {
|
||
const loaded = loadLobby(lobbyId);
|
||
if (loaded) {
|
||
lobbies[lobbyId] = loaded;
|
||
} else {
|
||
lobbies[lobbyId] = {
|
||
world: generateWorld(),
|
||
players: {},
|
||
savedPlayers: {},
|
||
mobs: [],
|
||
chests: {},
|
||
droppedItems: [], // Выпавшие предметы
|
||
nextDropId: 1,
|
||
timeOfDay: 0,
|
||
lastUpdate: Date.now()
|
||
};
|
||
}
|
||
}
|
||
|
||
const players = lobbies[lobbyId].players;
|
||
for (let id in players) {
|
||
if (players[id].nickname === safeNick) {
|
||
socket.emit('errorMsg', 'Игрок с таким именем уже есть в лобби!');
|
||
return;
|
||
}
|
||
}
|
||
joinLobby(socket, lobbyId, safeNick);
|
||
}
|
||
|
||
function joinLobby(socket, lobbyId, safeNick) {
|
||
currentLobby = lobbyId;
|
||
socket.join(lobbyId);
|
||
|
||
const spawnX = Math.floor(WORLD_WIDTH / 2);
|
||
let spawnY = 0;
|
||
const world = lobbies[lobbyId].world;
|
||
for (let y = 0; y < WORLD_HEIGHT; y++) {
|
||
if (world[y] && world[y][spawnX] !== BLOCKS.AIR) {
|
||
spawnY = (y - 2) * BLOCK_SIZE;
|
||
break;
|
||
}
|
||
}
|
||
|
||
let playerInv = getEmptyInventory();
|
||
let playerHotbar = [0, 0, 0, 0, 0, 0, 0, 0, 0]; // Пустой хотбар
|
||
let playerHealth = 20;
|
||
|
||
if (lobbies[lobbyId].savedPlayers[safeNick]) {
|
||
playerInv = lobbies[lobbyId].savedPlayers[safeNick].inventory;
|
||
playerHotbar = lobbies[lobbyId].savedPlayers[safeNick].hotbar;
|
||
playerHealth = lobbies[lobbyId].savedPlayers[safeNick].health || 20;
|
||
}
|
||
|
||
lobbies[lobbyId].players[socket.id] = {
|
||
x: spawnX * BLOCK_SIZE,
|
||
y: spawnY,
|
||
width: 24,
|
||
height: 48,
|
||
vx: 0, vy: 0, direction: 1, walkCycle: 0, heldItem: null,
|
||
nickname: safeNick,
|
||
inventory: playerInv,
|
||
hotbarSlots: playerHotbar,
|
||
health: playerHealth,
|
||
maxHealth: 20,
|
||
lastDamageTime: 0,
|
||
team: null // null, 'red', 'blue', 'green', 'yellow'
|
||
};
|
||
|
||
socket.emit('gameStart', {
|
||
world: lobbies[lobbyId].world,
|
||
players: lobbies[lobbyId].players,
|
||
selfId: socket.id,
|
||
spawn: { x: spawnX * BLOCK_SIZE, y: spawnY },
|
||
nickname: safeNick,
|
||
inventory: playerInv,
|
||
hotbarSlots: playerHotbar,
|
||
health: playerHealth,
|
||
chests: lobbies[lobbyId].chests || {}
|
||
});
|
||
|
||
// Отправляем список существующих дропов
|
||
if (lobbies[lobbyId].droppedItems && lobbies[lobbyId].droppedItems.length > 0) {
|
||
socket.emit('droppedItemsSync', lobbies[lobbyId].droppedItems);
|
||
}
|
||
|
||
socket.to(lobbyId).emit('playerJoined', {
|
||
id: socket.id,
|
||
...lobbies[lobbyId].players[socket.id]
|
||
});
|
||
}
|
||
|
||
socket.on('playerMove', (data) => {
|
||
if (!currentLobby || !lobbies[currentLobby]) return;
|
||
const player = lobbies[currentLobby].players[socket.id];
|
||
if (player) {
|
||
player.x = data.x; player.y = data.y; player.vx = data.vx; player.vy = data.vy;
|
||
player.direction = data.direction; player.walkCycle = data.walkCycle; player.heldItem = data.heldItem;
|
||
if (data.team !== undefined) player.team = data.team;
|
||
socket.to(currentLobby).emit('playerMoved', { id: socket.id, x: player.x, y: player.y, direction: player.direction, walkCycle: player.walkCycle, heldItem: player.heldItem, nickname: player.nickname, team: player.team });
|
||
}
|
||
});
|
||
|
||
// Смена команды
|
||
socket.on('setTeam', (team) => {
|
||
if (!currentLobby || !lobbies[currentLobby]) return;
|
||
const player = lobbies[currentLobby].players[socket.id];
|
||
if (player) {
|
||
player.team = team;
|
||
// Оповещаем всех игроков о смене команды
|
||
io.to(currentLobby).emit('playerTeamChanged', { id: socket.id, team: team });
|
||
}
|
||
});
|
||
|
||
// PvP атака
|
||
socket.on('attackPlayer', (data) => {
|
||
if (!currentLobby || !lobbies[currentLobby]) return;
|
||
const attacker = lobbies[currentLobby].players[socket.id];
|
||
const target = lobbies[currentLobby].players[data.targetId];
|
||
|
||
if (!attacker || !target) return;
|
||
|
||
// Проверка команд - нельзя атаковать союзников
|
||
if (attacker.team && attacker.team === target.team) return;
|
||
|
||
// Проверка кулдауна урона цели
|
||
const now = Date.now();
|
||
if (now - (target.lastDamageTime || 0) < 1000) return;
|
||
target.lastDamageTime = now;
|
||
|
||
// Наносим урон
|
||
target.health = (target.health || 20) - data.damage;
|
||
if (target.health < 0) target.health = 0;
|
||
|
||
// Отправляем событие урона цели
|
||
io.to(data.targetId).emit('pvpDamage', {
|
||
damage: data.damage,
|
||
knockbackX: data.knockbackX,
|
||
knockbackY: data.knockbackY,
|
||
attackerName: attacker.nickname,
|
||
attackerId: socket.id
|
||
});
|
||
|
||
// Если цель убита
|
||
if (target.health <= 0) {
|
||
io.to(currentLobby).emit('chatMessage', {
|
||
nickname: '⚔️ PvP',
|
||
text: `${attacker.nickname} убил ${target.nickname}!`
|
||
});
|
||
}
|
||
});
|
||
|
||
socket.on('updateBlock', (data) => {
|
||
if (!currentLobby || !lobbies[currentLobby]) return;
|
||
const { x, y, type } = data;
|
||
|
||
// Запрет на размещение воды игроками
|
||
if (type === BLOCKS.WATER) return;
|
||
|
||
if(y >= 0 && y < WORLD_HEIGHT && x >= 0 && x < WORLD_WIDTH) {
|
||
const world = lobbies[currentLobby].world;
|
||
if (world[y]) {
|
||
const currentBlock = world[y][x];
|
||
|
||
// Запрет на ломание воды
|
||
if (type === 0 && currentBlock === BLOCKS.WATER) return;
|
||
|
||
// Если ломаем блок (type === 0)
|
||
if (type === 0) {
|
||
world[y][x] = type;
|
||
io.to(currentLobby).emit('blockUpdated', { x, y, type });
|
||
return;
|
||
}
|
||
|
||
// Если ставим блок - проверяем что можно ставить на воздух или воду
|
||
if (currentBlock === BLOCKS.AIR || currentBlock === BLOCKS.WATER) {
|
||
// Проверка что рядом есть блок (не воздух и не вода)
|
||
let hasNeighbor = false;
|
||
const neighbors = [[x-1, y], [x+1, y], [x, y-1], [x, y+1]];
|
||
|
||
for (const [nx, ny] of neighbors) {
|
||
if (ny >= 0 && ny < WORLD_HEIGHT && nx >= 0 && nx < WORLD_WIDTH) {
|
||
const neighborBlock = world[ny] && world[ny][nx];
|
||
if (neighborBlock && neighborBlock !== BLOCKS.AIR && neighborBlock !== BLOCKS.WATER) {
|
||
hasNeighbor = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (hasNeighbor) {
|
||
world[y][x] = type;
|
||
io.to(currentLobby).emit('blockUpdated', { x, y, type });
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
socket.on('damageMob', (mobId) => {
|
||
if (!currentLobby || !lobbies[currentLobby]) return;
|
||
const lobby = lobbies[currentLobby];
|
||
const mobIndex = lobby.mobs.findIndex(m => m.id === mobId);
|
||
|
||
if (mobIndex !== -1) {
|
||
const mob = lobby.mobs[mobIndex];
|
||
const player = lobby.players[socket.id];
|
||
|
||
let damage = 1;
|
||
if(player && player.heldItem) {
|
||
if(player.heldItem === ITEMS.WOODEN_SWORD) damage = 3;
|
||
if(player.heldItem === ITEMS.STONE_SWORD) damage = 5;
|
||
if(player.heldItem === ITEMS.IRON_SWORD) damage = 7;
|
||
}
|
||
|
||
mob.health -= damage;
|
||
|
||
if(player) {
|
||
mob.vx = (mob.x - player.x > 0 ? 1 : -1) * 8;
|
||
mob.vy = -6;
|
||
}
|
||
|
||
if (mob.health <= 0) {
|
||
lobby.mobs.splice(mobIndex, 1);
|
||
|
||
// Дроп с мобов
|
||
if (player) {
|
||
const mobType = mob.type;
|
||
// Можно добавить дроп предметов
|
||
}
|
||
}
|
||
|
||
io.to(currentLobby).emit('gameStateUpdate', {
|
||
timeOfDay: lobby.timeOfDay,
|
||
mobs: lobby.mobs.map(m => ({
|
||
id: m.id, x: m.x, y: m.y, width: m.width, height: m.height,
|
||
health: m.health, maxHealth: m.maxHealth, type: m.type, direction: m.direction
|
||
}))
|
||
});
|
||
}
|
||
});
|
||
|
||
// Синхронизация здоровья игрока
|
||
socket.on('syncHealth', (health) => {
|
||
if (!currentLobby || !lobbies[currentLobby]) return;
|
||
const player = lobbies[currentLobby].players[socket.id];
|
||
if (player) {
|
||
player.health = health;
|
||
}
|
||
});
|
||
|
||
// Респавн игрока
|
||
socket.on('playerRespawn', () => {
|
||
if (!currentLobby || !lobbies[currentLobby]) return;
|
||
const player = lobbies[currentLobby].players[socket.id];
|
||
if (player) {
|
||
player.health = player.maxHealth;
|
||
// Можно добавить телепорт на спавн
|
||
}
|
||
});
|
||
|
||
socket.on('syncInventory', (data) => {
|
||
if (!currentLobby || !lobbies[currentLobby]) return;
|
||
const p = lobbies[currentLobby].players[socket.id];
|
||
if (p) {
|
||
p.inventory = data.inventory;
|
||
p.hotbarSlots = data.hotbarSlots;
|
||
}
|
||
});
|
||
|
||
// Обновление содержимого сундука
|
||
socket.on('updateChest', (data) => {
|
||
if (!currentLobby || !lobbies[currentLobby]) return;
|
||
const { x, y, items } = data;
|
||
const key = `${x},${y}`;
|
||
|
||
if (!lobbies[currentLobby].chests) {
|
||
lobbies[currentLobby].chests = {};
|
||
}
|
||
|
||
lobbies[currentLobby].chests[key] = { items };
|
||
|
||
// Отправляем обновление всем игрокам в лобби
|
||
socket.to(currentLobby).emit('chestUpdated', { x, y, items });
|
||
});
|
||
|
||
// Запрос содержимого сундука
|
||
socket.on('getChest', (data) => {
|
||
if (!currentLobby || !lobbies[currentLobby]) return;
|
||
const { x, y } = data;
|
||
const key = `${x},${y}`;
|
||
|
||
const chests = lobbies[currentLobby].chests || {};
|
||
const chest = chests[key] || { items: {} };
|
||
|
||
socket.emit('chestData', { x, y, items: chest.items });
|
||
});
|
||
|
||
// Удаление сундука (перенос вещей в инвентарь)
|
||
socket.on('breakChest', (data) => {
|
||
if (!currentLobby || !lobbies[currentLobby]) return;
|
||
const { x, y } = data;
|
||
const key = `${x},${y}`;
|
||
|
||
const chests = lobbies[currentLobby].chests || {};
|
||
const chest = chests[key];
|
||
|
||
if (chest && chest.items) {
|
||
const player = lobbies[currentLobby].players[socket.id];
|
||
if (player) {
|
||
// Переносим вещи из сундука в инвентарь игрока
|
||
for (let itemId in chest.items) {
|
||
const count = chest.items[itemId];
|
||
if (count > 0) {
|
||
player.inventory[itemId] = (player.inventory[itemId] || 0) + count;
|
||
}
|
||
}
|
||
// Отправляем обновлённый инвентарь игроку
|
||
socket.emit('inventoryUpdated', { inventory: player.inventory });
|
||
}
|
||
}
|
||
|
||
// Удаляем сундук из данных
|
||
delete chests[key];
|
||
|
||
// Оповещаем всех о разрушении сундука
|
||
io.to(currentLobby).emit('chestRemoved', { x, y });
|
||
});
|
||
|
||
// === СИНХРОНИЗАЦИЯ ДРОПА ПРЕДМЕТОВ ===
|
||
|
||
// Когда игрок создаёт дроп (ломает блок, выбрасывает предмет)
|
||
socket.on('itemDropped', (data) => {
|
||
if (!currentLobby || !lobbies[currentLobby]) return;
|
||
const lobby = lobbies[currentLobby];
|
||
|
||
const drop = {
|
||
id: lobby.nextDropId++,
|
||
x: data.x,
|
||
y: data.y,
|
||
vx: data.vx || (Math.random() - 0.5) * 4,
|
||
vy: data.vy || -3,
|
||
itemId: data.itemId,
|
||
count: data.count || 1,
|
||
createdBy: socket.id,
|
||
pickupDelay: 500, // 0.5 секунды до подбора
|
||
createdAt: Date.now()
|
||
};
|
||
|
||
if (!lobby.droppedItems) lobby.droppedItems = [];
|
||
lobby.droppedItems.push(drop);
|
||
|
||
// Отправляем всем игрокам в лобби (включая отправителя для синхронизации id)
|
||
io.to(currentLobby).emit('itemDroppedSync', drop);
|
||
});
|
||
|
||
// Когда игрок подбирает дроп
|
||
socket.on('itemPickedUp', (dropId) => {
|
||
if (!currentLobby || !lobbies[currentLobby]) return;
|
||
const lobby = lobbies[currentLobby];
|
||
|
||
if (!lobby.droppedItems) return;
|
||
|
||
const index = lobby.droppedItems.findIndex(d => d.id === dropId);
|
||
if (index !== -1) {
|
||
const drop = lobby.droppedItems[index];
|
||
// Проверяем таймер подбора на сервере
|
||
const timePassed = Date.now() - (drop.createdAt || 0);
|
||
if (timePassed < (drop.pickupDelay || 0)) {
|
||
return; // Ещё нельзя подбирать
|
||
}
|
||
|
||
lobby.droppedItems.splice(index, 1);
|
||
// Оповещаем всех игроков что дроп подобран
|
||
io.to(currentLobby).emit('itemRemovedSync', dropId);
|
||
}
|
||
});
|
||
|
||
socket.on('chatMessage', (msg) => {
|
||
if (currentLobby && lobbies[currentLobby].players[socket.id]) {
|
||
const nickname = lobbies[currentLobby].players[socket.id].nickname;
|
||
const safeMsg = msg.replace(/</g, "<").replace(/>/g, ">");
|
||
io.to(currentLobby).emit('chatMessage', { nickname: nickname, text: safeMsg });
|
||
}
|
||
});
|
||
|
||
socket.on('pingCheck', () => { socket.emit('pongCheck'); });
|
||
|
||
socket.on('disconnect', () => {
|
||
if (currentLobby && lobbies[currentLobby]) {
|
||
const p = lobbies[currentLobby].players[socket.id];
|
||
if(p) {
|
||
lobbies[currentLobby].savedPlayers[p.nickname] = {
|
||
inventory: p.inventory,
|
||
hotbar: p.hotbarSlots,
|
||
health: p.health
|
||
};
|
||
io.to(currentLobby).emit('playerDisconnected', { id: socket.id, nickname: p.nickname });
|
||
delete lobbies[currentLobby].players[socket.id];
|
||
saveLobby(currentLobby);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
const PORT = process.env.PORT || 3000;
|
||
const HOST = '0.0.0.0';
|
||
|
||
http.listen(PORT, HOST, () => {
|
||
console.log(`Сервер запущен! LAN: http://192.168.18.50:${PORT}`);
|
||
}); |