grechka-game/game.js

3937 lines
150 KiB
JavaScript

(() => {
// src/core/constants.js
var TILE = 40;
var SEA_GY = 14;
var BEDROCK_GY = 140;
var GEN_MARGIN_X = 40;
var GRAV = 2200;
var GRAV_WATER = 550;
var MOVE = 320;
var JUMP = 760;
var DAY_LEN = 360;
var SAVE_KEY = "minegrechka_save";
var MOVE_SEND_INTERVAL = 0.05;
var MAX_RAINDROPS = 200;
var MAX_CHAT_MESSAGES = 50;
var VOICE_SERVER = "wss://voicegrech.mkn8n.ru";
// src/core/state.js
var state = {
// Константы (для доступа из модулей)
TILE,
SEA_GY,
BEDROCK_GY,
GEN_MARGIN_X,
GRAV,
GRAV_WATER,
MOVE,
JUMP,
DAY_LEN,
SAVE_KEY,
MOVE_SEND_INTERVAL,
MAX_RAINDROPS,
MAX_CHAT_MESSAGES,
VOICE_SERVER,
// Камера
camX: 0,
camY: 0,
W: 0,
H: 0,
dpr: 1,
// Canvas/DOM (заполняется в main.js)
gameEl: null,
canvas: null,
ctx: null,
lightC: null,
lightCtx: null,
// День/ночь
worldTime: 0,
isNightTime: false,
// Мультиплеер
isMultiplayer: false,
mySocketId: null,
socket: null,
voiceSocket: null,
voicePosT: 0,
// Сервер
SERVER_URL: "",
TELEGRAM_BOT_USERNAME: "",
TELEGRAM_APP_SHORT_NAME: "",
// Игрок
playerName: null,
worldId: null,
// Инвентарь/UI
selected: 0,
showFullInventory: false,
craftOpen: false,
inventoryOpen: false,
chatOpen: false,
minimapOpen: false,
modeIdx: 0,
currentFurnaceKey: null,
// Мир
worldSeed: Math.floor(Math.random() * 1e6),
// Погода
isRaining: false,
rainIntensity: 0,
weatherTimer: 0,
weatherChangeInterval: 60 + Math.random() * 120,
// Мобы/спавн
spawnT: 0,
// Цикл
last: 0,
prevJump: false,
// Сеть — throttle отправки позиции
lastMoveSendTime: 0,
lastSentX: 0,
lastSentY: 0,
// Mining progress
mining: { gx: null, gy: null, progress: 0, hardness: 0 },
// Trade/Market
myTgId: null,
tradePanel: null,
tradeContent: null,
marketPanel: null,
marketContent: null,
// DOM-ссылки (заполняются в main.js)
hpEl: null,
foodEl: null,
sxEl: null,
syEl: null,
todEl: null,
worldIdEl: null,
playerCountEl: null,
deathEl: null,
hotbarEl: null,
craftPanel: null,
recipesEl: null,
inventoryPanel: null,
inventoryGrid: null,
multiplayerStatus: null,
// Игрок
player: {
x: 6 * TILE,
y: 0 * TILE,
w: 34,
h: 34,
vx: 0,
vy: 0,
grounded: false,
inWater: false,
headInWater: false,
hp: 100,
hunger: 100,
o2: 100,
invuln: 0,
fallStartY: 0,
lastStepTime: 0,
sleeping: false,
inBoat: false,
armor: 0,
equippedArmor: null
},
// Точка спавна
spawnPoint: { x: 6 * TILE, y: 0 * TILE },
// Инвентарь
inv: {
dirt: 6,
stone: 0,
sand: 0,
gravel: 0,
clay: 0,
wood: 0,
planks: 0,
ladder: 0,
leaves: 0,
coal: 0,
copper_ore: 0,
iron_ore: 0,
gold_ore: 0,
diamond_ore: 0,
brick: 0,
glass: 0,
tnt: 1,
campfire: 0,
torch: 0,
meat: 0,
cooked: 0,
arrow: 0,
wood_pickaxe: 0,
stone_pickaxe: 0,
iron_pickaxe: 0,
wood_sword: 0,
stone_sword: 0,
iron_sword: 0,
wood_axe: 0,
stone_axe: 0,
iron_axe: 0,
wood_shovel: 0,
stone_shovel: 0,
iron_shovel: 0,
iron_armor: 0,
bow: 0,
furnace: 0,
bed: 0,
boat: 0,
iron_ingot: 0,
gold_ingot: 0,
copper_ingot: 0,
cobblestone: 0
},
// Лодка
boat: {
x: 0,
y: 0,
w: 34,
h: 34,
vx: 0,
vy: 0,
active: false,
inWater: false
},
// Ввод
inp: { up: false, down: false, left: false, right: false, jump: false, mine: false, build: false, bow: false, l: false, r: false, j: false, s: false },
// Мышь
mouse: { x: 0, y: 0 },
// Другие игроки (MP)
otherPlayers: /* @__PURE__ */ new Map(),
// Серверные мобы (MP)
serverMobs: /* @__PURE__ */ new Map(),
// Мобы
mobs: [],
// Снаряды
projectiles: [],
// Отслеживание изменений мира
placedBlocks: [],
removedBlocks: [],
// Серверные изменения
serverOverrides: /* @__PURE__ */ new Map(),
// Чат
chatMessages: [],
// Погода — капли
raindrops: [],
// Облака
clouds: Array.from({ length: 10 }, () => ({
x: Math.random() * 2e3,
y: -200 - Math.random() * 260,
w: 80 + Math.random() * 120,
s: 12 + Math.random() * 20
})),
// Частицы
parts: [],
// Активный TNT
activeTNT: /* @__PURE__ */ new Set(),
// Прочность инструментов
toolDurability: /* @__PURE__ */ new Map(),
// Последние выбранные предметы
recentItems: [],
// Активные печи
activeFurnaces: /* @__PURE__ */ new Map(),
// Сгенерированные колонны
generated: /* @__PURE__ */ new Set(),
// Изображение героя
heroImg: null,
// Сетка мира
grid: /* @__PURE__ */ new Map(),
blocks: [],
// In-memory save fallback
inMemorySave: null
};
// src/core/canvas.js
function initCanvas() {
const gameEl = document.getElementById("game");
const canvas = document.getElementById("c");
const ctx = canvas.getContext("2d");
const lightC = document.createElement("canvas");
const lightCtx = lightC.getContext("2d");
const dpr = Math.max(1, window.devicePixelRatio || 1);
function resize() {
state.W = gameEl.clientWidth;
state.H = gameEl.clientHeight;
canvas.width = state.W * dpr;
canvas.height = state.H * dpr;
lightC.width = state.W * dpr;
lightC.height = state.H * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
window.addEventListener("resize", resize);
resize();
state.gameEl = gameEl;
state.canvas = canvas;
state.ctx = ctx;
state.lightC = lightC;
state.lightCtx = lightCtx;
state.dpr = dpr;
}
// src/config.js
var urlParams = new URLSearchParams(window.location.search);
var SERVER_URL = urlParams.get("server") || "https://apigrech.mkn8n.ru";
if (location.protocol === "https:" && SERVER_URL.startsWith("http://")) {
console.warn("\u26A0\uFE0F Mixed content warning: page is HTTPS but server URL is HTTP");
alert("\u26A0\uFE0F \u041F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u0435: \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0430 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u0430 \u043F\u043E HTTPS, \u043D\u043E \u0441\u0435\u0440\u0432\u0435\u0440 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442 HTTP. \u042D\u0442\u043E \u043C\u043E\u0436\u0435\u0442 \u0432\u044B\u0437\u0432\u0430\u0442\u044C \u043F\u0440\u043E\u0431\u043B\u0435\u043C\u044B.");
}
// src/data/blocks.js
var BLOCKS = {
air: { n: "\u0412\u043E\u0437\u0434\u0443\u0445", solid: false, hardness: 0 },
grass: { n: "\u0422\u0440\u0430\u0432\u0430", c: "#7cfc00", solid: true, hardness: 3, toolType: "shovel" },
dirt: { n: "\u0413\u0440\u044F\u0437\u044C", c: "#8b4513", solid: true, hardness: 3, toolType: "shovel" },
stone: { n: "\u041A\u0430\u043C\u0435\u043D\u044C", c: "#7f8c8d", solid: true, hardness: 6, toolType: "pickaxe" },
sand: { n: "\u041F\u0435\u0441\u043E\u043A", c: "#f4d06f", solid: true, hardness: 2, toolType: "shovel" },
gravel: { n: "\u0413\u0440\u0430\u0432\u0438\u0439", c: "#95a5a6", solid: true, hardness: 3, toolType: "shovel" },
clay: { n: "\u0413\u043B\u0438\u043D\u0430", c: "#74b9ff", solid: true, hardness: 3, toolType: "shovel" },
wood: { n: "\u0414\u0435\u0440\u0435\u0432\u043E", c: "#d35400", solid: true, hardness: 4, toolType: "axe" },
planks: { n: "\u0414\u043E\u0441\u043A\u0438", c: "#e67e22", solid: true, hardness: 3, toolType: "axe" },
ladder: { n: "\u041B\u0435\u0441\u0442\u043D\u0438\u0446\u0430", c: "#d35400", solid: false, climbable: true, hardness: 1, toolType: null },
leaves: { n: "\u041B\u0438\u0441\u0442\u0432\u0430", c: "#2ecc71", solid: true, hardness: 1, toolType: "axe", minTool: null },
glass: { n: "\u0421\u0442\u0435\u043A\u043B\u043E", c: "rgba(200,240,255,0.25)", solid: true, alpha: 0.55, hardness: 1, toolType: null },
water: { n: "\u0412\u043E\u0434\u0430", c: "rgba(52,152,219,0.55)", solid: false, fluid: true, hardness: 0 },
coal: { n: "\u0423\u0433\u043E\u043B\u044C", c: "#2c3e50", solid: true, hardness: 8, toolType: "pickaxe", minTool: "wood" },
copper_ore: { n: "\u041C\u0435\u0434\u044C", c: "#e17055", solid: true, hardness: 10, toolType: "pickaxe", minTool: "wood" },
iron_ore: { n: "\u0416\u0435\u043B\u0435\u0437\u043E", c: "#dcdde1", solid: true, hardness: 12, toolType: "pickaxe", minTool: "stone" },
iron_armor: { n: "\u0416\u0435\u043B\u0435\u0437\u043D\u0430\u044F \u0431\u0440\u043E\u043D\u044F", c: "#95a5a6", solid: false, armor: 0.5, hardness: 0 },
gold_ore: { n: "\u0417\u043E\u043B\u043E\u0442\u043E", c: "#f1c40f", solid: true, hardness: 15, toolType: "pickaxe", minTool: "stone" },
diamond_ore: { n: "\u0410\u043B\u043C\u0430\u0437", c: "#00a8ff", solid: true, hardness: 20, toolType: "pickaxe", minTool: "iron" },
brick: { n: "\u041A\u0438\u0440\u043F\u0438\u0447", c: "#c0392b", solid: true, hardness: 8, toolType: "pickaxe" },
tnt: { n: "TNT", c: "#e74c3c", solid: true, explosive: true, hardness: 1, toolType: null },
campfire: { n: "\u041A\u043E\u0441\u0442\u0451\u0440", c: "#e67e22", solid: true, lightRadius: 190, hardness: 2, toolType: null },
torch: { n: "\u0424\u0430\u043A\u0435\u043B", c: "#f9ca24", solid: true, lightRadius: 140, hardness: 1, toolType: null },
bedrock: { n: "\u0411\u0435\u0434\u0440\u043E\u043A", c: "#2d3436", solid: true, unbreakable: true, hardness: Infinity },
flower: { n: "\u0426\u0432\u0435\u0442\u043E\u043A", c: "#ff4757", solid: false, decor: true, hardness: 0 },
bed: { n: "\u041A\u0440\u043E\u0432\u0430\u0442\u044C", c: "#e91e63", solid: true, bed: true, hardness: 2, toolType: "axe" },
boat: { n: "\u041B\u043E\u0434\u043A\u0430", c: "#8B4513", solid: false, hardness: 0 },
furnace: { n: "\u041F\u0435\u0447\u044C", c: "#696969", solid: true, smelting: true, hardness: 8, toolType: "pickaxe", minTool: "wood" },
cobblestone: { n: "\u0411\u0443\u043B\u044B\u0436\u043D\u0438\u043A", c: "#7f8c8d", solid: true, hardness: 6, toolType: "pickaxe" }
};
// src/world/world-storage.js
var grid = state.grid;
var blocks = state.blocks;
function k(gx, gy) {
return gx + "," + gy;
}
function getBlock(gx, gy) {
return grid.get(k(gx, gy));
}
function isSolid(gx, gy) {
const b = getBlock(gx, gy);
if (!b || b.dead) return false;
const def = BLOCKS[b.t];
return !!def.solid && !def.fluid && !def.decor;
}
function setBlock(gx, gy, t, isPlayerPlaced = false) {
const key = k(gx, gy);
if (grid.has(key)) return false;
const b = { gx, gy, t, dead: false, active: false, fuse: 0 };
grid.set(key, b);
blocks.push(b);
if (isPlayerPlaced) {
state.placedBlocks.push({ gx, gy, t });
}
return true;
}
function removeBlock(gx, gy) {
const key = k(gx, gy);
const b = grid.get(key);
if (!b) return null;
if (BLOCKS[b.t].unbreakable) return null;
grid.delete(key);
b.dead = true;
const wasPlayerPlaced = state.placedBlocks.some((pb) => pb.gx === gx && pb.gy === gy);
if (wasPlayerPlaced) {
state.placedBlocks = state.placedBlocks.filter((pb) => !(pb.gx === gx && pb.gy === gy));
} else {
state.removedBlocks.push({ gx, gy });
}
return b;
}
// src/world/generation.js
var generated = state.generated;
function surfaceGyAt(gx) {
const n1 = Math.sin(gx * 0.025 + state.worldSeed * 1e-3) * 8;
const n2 = Math.sin(gx * 0.012 + state.worldSeed * 2e-3) * 12;
const n3 = Math.sin(gx * 6e-3 + state.worldSeed * 3e-3) * 6;
const n4 = Math.sin(gx * 0.045 + state.worldSeed * 4e-3) * 4;
const n5 = Math.cos(gx * 0.018 + state.worldSeed * 5e-3) * 5;
const h = Math.floor(SEA_GY - 8 + n1 + n2 + n3 + n4 + n5);
return h;
}
function genColumn(gx) {
if (generated.has(gx)) return;
generated.add(gx);
const sgy = surfaceGyAt(gx);
if (sgy > SEA_GY) {
for (let gy = SEA_GY; gy < sgy; gy++) {
setBlock(gx, gy, "water");
}
setBlock(gx, sgy, "sand");
} else {
if (sgy < SEA_GY - 10) setBlock(gx, sgy, "stone");
else setBlock(gx, sgy, "grass");
}
for (let gy = sgy + 1; gy <= BEDROCK_GY; gy++) {
if (gy === BEDROCK_GY) {
setBlock(gx, gy, "bedrock");
continue;
}
let t = "stone";
if (gy <= sgy + 3) t = "dirt";
if (sgy > SEA_GY && gy === sgy + 1 && seededRandom(gx, gy) < 0.25) t = "clay";
if (gy > sgy + 6 && seededRandom(gx, gy) < 0.07) t = "gravel";
const depth = gy - sgy;
const r = seededRandom(gx, gy);
if (t === "stone") {
if (r < 0.06) t = "coal";
else if (r < 0.1) t = "copper_ore";
else if (r < 0.13) t = "iron_ore";
else if (depth > 40 && r < 0.145) t = "gold_ore";
else if (depth > 70 && r < 0.152) t = "diamond_ore";
}
setBlock(gx, gy, t);
}
const top = getBlock(gx, sgy);
if (top && top.t === "grass") {
if (seededRandom(gx, sgy - 1) < 0.1) {
setBlock(gx, sgy - 1, "flower");
}
if (seededRandom(gx, sgy - 2) < 0.12) {
setBlock(gx, sgy - 1, "wood");
setBlock(gx, sgy - 2, "wood");
setBlock(gx, sgy - 3, "leaves");
setBlock(gx - 1, sgy - 3, "leaves");
setBlock(gx + 1, sgy - 3, "leaves");
}
}
const colPrefix = gx + ",";
for (const [key, ov] of state.serverOverrides) {
if (!key.startsWith(colPrefix)) continue;
if (ov.op === "remove") {
const b = grid.get(key);
if (b) {
grid.delete(key);
b.dead = true;
}
} else if (ov.op === "set") {
if (!grid.has(key)) {
const gy = parseInt(key.split(",")[1]);
const nb = { gx, gy, t: ov.t, dead: false, active: false, fuse: 0 };
grid.set(key, nb);
blocks.push(nb);
}
}
}
}
function regenerateVisibleChunks() {
const gx0 = Math.floor(state.camX / TILE);
for (let gx = gx0 - GEN_MARGIN_X; gx <= gx0 + GEN_MARGIN_X; gx++) {
generated.delete(gx);
genColumn(gx);
}
}
function ensureGenAroundCamera() {
const gx0 = Math.floor(state.camX / TILE);
for (let gx = gx0 - GEN_MARGIN_X; gx <= gx0 + GEN_MARGIN_X; gx++) {
genColumn(gx);
}
}
function seededRandom(gx, gy) {
const n = Math.sin(gx * 12.9898 + gy * 78.233 + state.worldSeed * 0.1) * 43758.5453;
return n - Math.floor(n);
}
// src/audio/sound-engine.js
var sounds = {};
function loadSound(id, src) {
const audio = new Audio();
audio.src = src;
audio.volume = 0.3;
sounds[id] = audio;
}
loadSound("splash", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//splash.mp3");
loadSound("sand1", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//sand1.mp3");
loadSound("snow1", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//snow1.mp3");
loadSound("stone1", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//stone1.mp3");
loadSound("wood1", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//wood1.mp3");
loadSound("cloth1", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//cloth1.mp3");
loadSound("fire", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//fire.mp3");
loadSound("hit1", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//hit1.mp3");
loadSound("attack", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//attack.mp3");
loadSound("hurt_chicken", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//hurt1_chicken.mp3");
loadSound("stone_build", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//stone1%20(1).mp3");
loadSound("wood_build", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//wood1%20(1).mp3");
loadSound("click", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//click.mp3");
loadSound("explode1", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//explode1.mp3");
loadSound("glass1", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//glass1.mp3");
loadSound("eat1", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//eat1.mp3");
loadSound("step", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//sand2.mp3");
function playSound(id) {
if (sounds[id]) {
sounds[id].currentTime = 0;
sounds[id].play().catch((e) => console.error("Sound error:", e));
}
}
// src/ui/chat.js
var chatMessages = [];
var MAX_CHAT_MESSAGES2 = 20;
function addChatMessage(sender, message) {
const time = (/* @__PURE__ */ new Date()).toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" });
chatMessages.push({ sender, message, time });
if (chatMessages.length > MAX_CHAT_MESSAGES2) {
chatMessages.shift();
}
renderChatMessages();
}
function renderChatMessages() {
const chatMessagesEl = document.getElementById("chatMessages");
if (!chatMessagesEl) return;
chatMessagesEl.innerHTML = chatMessages.map(
(m) => `<div style="margin-bottom:4px;"><span style="color:#aaa;font-size:11px;">${m.time}</span> <strong style="color:${m.sender === "\u0421\u0438\u0441\u0442\u0435\u043C\u0430" ? "#f39c12" : "#3498db"};">${m.sender}:</strong> ${m.message}</div>`
).join("");
chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight;
}
function sendChatMessage(message) {
if (!message || message.trim() === "") return;
if (state.isMultiplayer && state.socket && state.socket.connected) {
state.socket.emit("chat_message", { message: message.trim() });
} else {
addChatMessage("\u0412\u044B", message.trim());
}
}
function initChat() {
document.getElementById("chatToggle").onclick = () => {
playSound("click");
state.chatOpen = !state.chatOpen;
document.getElementById("chatPanel").style.display = state.chatOpen ? "block" : "none";
if (state.chatOpen) {
document.getElementById("chatInput").focus();
}
};
document.getElementById("chatClose").onclick = () => {
playSound("click");
state.chatOpen = false;
document.getElementById("chatPanel").style.display = "none";
};
document.getElementById("chatSend").onclick = () => {
const input = document.getElementById("chatInput");
sendChatMessage(input.value);
input.value = "";
};
document.getElementById("chatInput").addEventListener("keypress", (e) => {
if (e.key === "Enter") {
sendChatMessage(e.target.value);
e.target.value = "";
}
});
}
// src/entities/player.js
function calculateDamage(baseDamage) {
const reduction = state.player.armor;
const actualDamage = baseDamage * (1 - reduction);
console.log("[DAMAGE] Base:", baseDamage, "- Armor:", reduction, "- Actual:", actualDamage.toFixed(1));
return actualDamage;
}
// src/data/items.js
var ITEMS = {
meat: { n: "\u0421\u044B\u0440\u043E\u0435 \u043C\u044F\u0441\u043E", icon: "\u{1F969}", food: 15 },
cooked: { n: "\u0416\u0430\u0440\u0435\u043D\u043E\u0435 \u043C\u044F\u0441\u043E", icon: "\u{1F356}", food: 45 },
arrow: { n: "\u0421\u0442\u0440\u0435\u043B\u0430", icon: "\u27A1\uFE0F", stack: 64 }
};
ITEMS.iron_ingot = { n: "\u0416\u0435\u043B\u0435\u0437\u043D\u044B\u0439 \u0441\u043B\u0438\u0442\u043E\u043A", icon: "\u{1F529}" };
ITEMS.gold_ingot = { n: "\u0417\u043E\u043B\u043E\u0442\u043E\u0439 \u0441\u043B\u0438\u0442\u043E\u043A", icon: "\u{1FA99}" };
ITEMS.copper_ingot = { n: "\u041C\u0435\u0434\u043D\u044B\u0439 \u0441\u043B\u0438\u0442\u043E\u043A", icon: "\u{1F7E4}" };
ITEMS.wood_axe = { n: "\u0414\u0435\u0440\u0435\u0432\u044F\u043D\u043D\u044B\u0439 \u0442\u043E\u043F\u043E\u0440", icon: "\u{1FA93}" };
ITEMS.stone_axe = { n: "\u041A\u0430\u043C\u0435\u043D\u043D\u044B\u0439 \u0442\u043E\u043F\u043E\u0440", icon: "\u{1FA93}" };
ITEMS.iron_axe = { n: "\u0416\u0435\u043B\u0435\u0437\u043D\u044B\u0439 \u0442\u043E\u043F\u043E\u0440", icon: "\u{1FA93}" };
ITEMS.wood_shovel = { n: "\u0414\u0435\u0440\u0435\u0432\u044F\u043D\u043D\u0430\u044F \u043B\u043E\u043F\u0430\u0442\u0430", icon: "\u{1F531}" };
ITEMS.stone_shovel = { n: "\u041A\u0430\u043C\u0435\u043D\u043D\u0430\u044F \u043B\u043E\u043F\u0430\u0442\u0430", icon: "\u{1F531}" };
ITEMS.iron_shovel = { n: "\u0416\u0435\u043B\u0435\u0437\u043D\u0430\u044F \u043B\u043E\u043F\u0430\u0442\u0430", icon: "\u{1F531}" };
// src/data/tools.js
var TOOLS = {
wood_pickaxe: { n: "\u0414\u0435\u0440\u0435\u0432\u044F\u043D\u043D\u0430\u044F \u043A\u0438\u0440\u043A\u0430", icon: "\u26CF\uFE0F", damage: 1, durability: 60, toolType: "pickaxe", toolTier: "wood", speed: 2 },
stone_pickaxe: { n: "\u041A\u0430\u043C\u0435\u043D\u043D\u0430\u044F \u043A\u0438\u0440\u043A\u0430", icon: "\u26CF\uFE0F", damage: 2, durability: 132, toolType: "pickaxe", toolTier: "stone", speed: 4 },
iron_pickaxe: { n: "\u0416\u0435\u043B\u0435\u0437\u043D\u0430\u044F \u043A\u0438\u0440\u043A\u0430", icon: "\u26CF\uFE0F", damage: 3, durability: 250, toolType: "pickaxe", toolTier: "iron", speed: 6 },
wood_sword: { n: "\u0414\u0435\u0440\u0435\u0432\u044F\u043D\u043D\u044B\u0439 \u043C\u0435\u0447", icon: "\u{1F5E1}\uFE0F", damage: 4, durability: 60 },
stone_sword: { n: "\u041A\u0430\u043C\u0435\u043D\u043D\u044B\u0439 \u043C\u0435\u0447", icon: "\u{1F5E1}\uFE0F", damage: 5, durability: 132 },
iron_sword: { n: "\u0416\u0435\u043B\u0435\u0437\u043D\u044B\u0439 \u043C\u0435\u0447", icon: "\u{1F5E1}\uFE0F", damage: 7, durability: 250 },
wood_axe: { n: "\u0414\u0435\u0440\u0435\u0432\u044F\u043D\u043D\u044B\u0439 \u0442\u043E\u043F\u043E\u0440", icon: "\u{1FA93}", damage: 3, durability: 60, toolType: "axe", toolTier: "wood", speed: 2 },
stone_axe: { n: "\u041A\u0430\u043C\u0435\u043D\u043D\u044B\u0439 \u0442\u043E\u043F\u043E\u0440", icon: "\u{1FA93}", damage: 4, durability: 132, toolType: "axe", toolTier: "stone", speed: 4 },
iron_axe: { n: "\u0416\u0435\u043B\u0435\u0437\u043D\u044B\u0439 \u0442\u043E\u043F\u043E\u0440", icon: "\u{1FA93}", damage: 5, durability: 250, toolType: "axe", toolTier: "iron", speed: 6 },
wood_shovel: { n: "\u0414\u0435\u0440\u0435\u0432\u044F\u043D\u043D\u0430\u044F \u043B\u043E\u043F\u0430\u0442\u0430", icon: "\u{1F531}", damage: 1, durability: 60, toolType: "shovel", toolTier: "wood", speed: 2 },
stone_shovel: { n: "\u041A\u0430\u043C\u0435\u043D\u043D\u0430\u044F \u043B\u043E\u043F\u0430\u0442\u0430", icon: "\u{1F531}", damage: 2, durability: 132, toolType: "shovel", toolTier: "stone", speed: 4 },
iron_shovel: { n: "\u0416\u0435\u043B\u0435\u0437\u043D\u0430\u044F \u043B\u043E\u043F\u0430\u0442\u0430", icon: "\u{1F531}", damage: 3, durability: 250, toolType: "shovel", toolTier: "iron", speed: 6 },
bow: { n: "\u041B\u0443\u043A", icon: "\u{1F3F9}", damage: 10, durability: 200 }
};
function addTool(type) {
const maxDur = TOOLS[type]?.durability || 60;
const key = `${type}_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
state.toolDurability.set(key, { type, current: maxDur, max: maxDur });
}
function useTool(type) {
for (const [tid, dur] of state.toolDurability) {
if (dur.type === type && dur.current > 0) {
dur.current--;
if (dur.current <= 0) {
state.toolDurability.delete(tid);
state.inv[type] = Math.max(0, (state.inv[type] || 0) - 1);
return true;
}
return false;
}
}
return false;
}
function getBestTool(blockType) {
const block = BLOCKS[blockType];
if (!block || !block.toolType) return null;
const toolType = block.toolType;
for (const tier of ["iron", "stone", "wood"]) {
const toolId = `${tier}_${toolType}`;
if (TOOLS[toolId] && state.inv[toolId] > 0) return toolId;
}
return null;
}
function toolTierLessThan(toolTier, minTier) {
const tiers = { wood: 1, stone: 2, iron: 3 };
return (tiers[toolTier] || 0) < (tiers[minTier] || 0);
}
function getMiningSpeed(blockType) {
const block = BLOCKS[blockType];
if (!block) return 1;
const toolId = getBestTool(blockType);
if (toolId) {
const tool = TOOLS[toolId];
if (tool.toolType === block.toolType) return tool.speed;
}
return 1;
}
// src/render/textures.js
var tex = {};
var itemTex = {};
function drawArrow(ctx, color) {
ctx.fillStyle = "#8B4513";
ctx.fillRect(8, 14, 16, 3);
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(24, 15);
ctx.lineTo(18, 10);
ctx.lineTo(18, 20);
ctx.closePath();
ctx.fill();
ctx.fillStyle = "#e74c3c";
ctx.beginPath();
ctx.moveTo(8, 15);
ctx.lineTo(4, 10);
ctx.lineTo(10, 14);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(8, 16);
ctx.lineTo(4, 21);
ctx.lineTo(10, 17);
ctx.closePath();
ctx.fill();
}
function drawPickaxe(ctx, headColor, handleColor) {
ctx.fillStyle = handleColor || "#8B4513";
ctx.save();
ctx.translate(16, 16);
ctx.rotate(Math.PI / 4);
ctx.fillRect(-2, 0, 4, 18);
ctx.fillStyle = headColor;
ctx.fillRect(-8, -3, 16, 5);
ctx.fillRect(-8, -3, 3, 8);
ctx.fillRect(5, -3, 3, 8);
ctx.restore();
}
function drawAxe(ctx, headColor, handleColor) {
ctx.fillStyle = handleColor || "#8B4513";
ctx.save();
ctx.translate(16, 16);
ctx.rotate(Math.PI / 4);
ctx.fillRect(-2, 0, 4, 18);
ctx.fillStyle = headColor;
ctx.fillRect(-2, -8, 10, 8);
ctx.fillRect(-2, -8, 4, 12);
ctx.restore();
}
function drawShovel(ctx, headColor, handleColor) {
ctx.fillStyle = handleColor || "#8B4513";
ctx.save();
ctx.translate(16, 16);
ctx.rotate(Math.PI / 4);
ctx.fillRect(-2, 0, 4, 18);
ctx.fillStyle = headColor;
ctx.fillRect(-6, -8, 12, 8);
ctx.fillRect(-4, -12, 8, 4);
ctx.restore();
}
function drawSword(ctx, bladeColor, guardColor) {
ctx.fillStyle = "#8B4513";
ctx.fillRect(14, 18, 4, 10);
ctx.fillStyle = guardColor || "#f1c40f";
ctx.fillRect(10, 16, 12, 3);
ctx.fillStyle = bladeColor;
ctx.beginPath();
ctx.moveTo(16, 2);
ctx.lineTo(12, 16);
ctx.lineTo(20, 16);
ctx.closePath();
ctx.fill();
}
function drawBow(ctx) {
ctx.strokeStyle = "#8B4513";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(12, 16, 12, -Math.PI * 0.7, Math.PI * 0.7);
ctx.stroke();
ctx.strokeStyle = "#ddd";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(12, 4);
ctx.lineTo(12, 28);
ctx.stroke();
}
function drawIngot(ctx, color) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(6, 12);
ctx.lineTo(26, 12);
ctx.lineTo(24, 24);
ctx.lineTo(8, 24);
ctx.closePath();
ctx.fill();
ctx.fillStyle = "rgba(255,255,255,0.3)";
ctx.fillRect(8, 8, 16, 4);
}
function drawMeat(ctx, cooked) {
ctx.fillStyle = cooked ? "#8B4513" : "#e74c3c";
ctx.beginPath();
ctx.ellipse(16, 18, 10, 8, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#f5f5dc";
ctx.fillRect(8, 6, 3, 12);
ctx.fillRect(14, 4, 3, 8);
}
function makeItemTex(id) {
const c = document.createElement("canvas");
c.width = 32;
c.height = 32;
const g = c.getContext("2d");
const tierColors = {
wood: { head: "#a0522d", blade: "#a0522d" },
stone: { head: "#7f8c8d", blade: "#95a5a6" },
iron: { head: "#bdc3c7", blade: "#ecf0f1" }
};
switch (id) {
case "arrow":
drawArrow(g, "#7f8c8d");
break;
case "bow":
drawBow(g);
break;
case "wood_pickaxe":
drawPickaxe(g, tierColors.wood.head, tierColors.wood.blade);
break;
case "stone_pickaxe":
drawPickaxe(g, tierColors.stone.head);
break;
case "iron_pickaxe":
drawPickaxe(g, tierColors.iron.head);
break;
case "wood_axe":
drawAxe(g, tierColors.wood.head);
break;
case "stone_axe":
drawAxe(g, tierColors.stone.head);
break;
case "iron_axe":
drawAxe(g, tierColors.iron.head);
break;
case "wood_shovel":
drawShovel(g, tierColors.wood.head);
break;
case "stone_shovel":
drawShovel(g, tierColors.stone.head);
break;
case "iron_shovel":
drawShovel(g, tierColors.iron.head);
break;
case "wood_sword":
drawSword(g, tierColors.wood.blade, "#a0522d");
break;
case "stone_sword":
drawSword(g, tierColors.stone.blade, "#7f8c8d");
break;
case "iron_sword":
drawSword(g, tierColors.iron.blade, "#bdc3c7");
break;
case "iron_ingot":
drawIngot(g, "#bdc3c7");
break;
case "gold_ingot":
drawIngot(g, "#f1c40f");
break;
case "copper_ingot":
drawIngot(g, "#e67e22");
break;
case "meat":
drawMeat(g, false);
break;
case "cooked":
drawMeat(g, true);
break;
default:
g.fillStyle = "#7f8c8d";
g.fillRect(4, 4, 24, 24);
g.fillStyle = "rgba(255,255,255,0.2)";
g.fillRect(4, 4, 24, 4);
}
return c;
}
function makeTex(type) {
const t = BLOCKS[type];
const c = document.createElement("canvas");
c.width = 32;
c.height = 32;
const g = c.getContext("2d");
if (type === "tnt") {
g.fillStyle = "#c0392b";
g.fillRect(0, 0, 32, 32);
g.fillStyle = "#fff";
g.fillRect(0, 12, 32, 8);
g.fillStyle = "#000";
g.font = "bold 10px sans-serif";
g.fillText("TNT", 6, 20);
return c;
}
if (type === "campfire") {
g.fillStyle = "#5d4037";
g.fillRect(4, 26, 24, 6);
g.fillStyle = "#3e2723";
g.fillRect(7, 23, 18, 4);
return c;
}
if (type === "torch") {
g.fillStyle = "#6d4c41";
g.fillRect(14, 10, 4, 18);
g.fillStyle = "#f39c12";
g.fillRect(12, 6, 8, 8);
return c;
}
if (type === "glass") {
g.fillStyle = "rgba(200,240,255,0.25)";
g.fillRect(0, 0, 32, 32);
g.strokeStyle = "rgba(255,255,255,0.65)";
g.strokeRect(2, 2, 28, 28);
g.beginPath();
g.moveTo(5, 27);
g.lineTo(27, 5);
g.stroke();
return c;
}
if (type === "water") {
g.fillStyle = t.c;
g.fillRect(0, 0, 32, 32);
g.fillStyle = "rgba(255,255,255,0.08)";
g.fillRect(0, 6, 32, 2);
return c;
}
if (type === "bed") {
g.fillStyle = "#e91e63";
g.fillRect(0, 0, 32, 32);
g.fillStyle = "#f8bbd0";
g.fillRect(2, 2, 14, 14);
g.fillStyle = "#c2185b";
g.fillRect(16, 4, 14, 24);
g.fillStyle = "#e91e63";
g.fillRect(18, 6, 10, 20);
return c;
}
if (type === "flower") {
g.fillStyle = "#2ecc71";
g.fillRect(14, 14, 4, 18);
g.fillStyle = t.c;
g.beginPath();
g.arc(16, 12, 6, 0, 6.28);
g.fill();
return c;
}
if (type === "boat") {
g.fillStyle = "#8B4513";
g.fillRect(2, 12, 28, 8);
g.fillStyle = "#A0522D";
g.fillRect(0, 10, 32, 12);
g.fillStyle = "#DEB887";
g.fillRect(4, 14, 24, 4);
g.fillStyle = "#654321";
g.fillRect(2, 20, 28, 4);
return c;
}
if (type === "ladder") {
g.fillStyle = "#8B4513";
g.fillRect(4, 0, 4, 32);
g.fillRect(24, 0, 4, 32);
g.fillStyle = "#A0522D";
g.fillRect(4, 4, 24, 3);
g.fillRect(4, 12, 24, 3);
g.fillRect(4, 20, 24, 3);
g.fillRect(4, 28, 24, 3);
return c;
}
g.fillStyle = t.c || "#000";
g.fillRect(0, 0, 32, 32);
g.fillStyle = "rgba(0,0,0,0.10)";
for (let i = 0; i < 6; i++) g.fillRect(Math.random() * 28 | 0, Math.random() * 28 | 0, 4, 4);
if (type.endsWith("_ore") || type === "coal") {
g.fillStyle = "rgba(0,0,0,0.35)";
for (let i = 0; i < 4; i++) g.fillRect(Math.random() * 24 | 0, Math.random() * 24 | 0, 6, 6);
}
return c;
}
function initTextures() {
Object.keys(BLOCKS).forEach((k2) => tex[k2] = makeTex(k2));
const itemIds = [
"arrow",
"bow",
"wood_pickaxe",
"stone_pickaxe",
"iron_pickaxe",
"wood_axe",
"stone_axe",
"iron_axe",
"wood_shovel",
"stone_shovel",
"iron_shovel",
"wood_sword",
"stone_sword",
"iron_sword",
"iron_ingot",
"gold_ingot",
"copper_ingot",
"meat",
"cooked"
];
itemIds.forEach((id) => {
itemTex[id] = makeItemTex(id);
});
}
// src/ui/hotbar.js
function rebuildHotbar() {
const hotbarEl = state.hotbarEl;
const inv = state.inv;
const selected = state.selected;
const recentItems = state.recentItems;
const toolDurability = state.toolDurability;
hotbarEl.innerHTML = "";
const items = recentItems.filter((id) => inv[id] > 0).slice(0, 5);
for (const id of items) {
const s = document.createElement("div");
s.className = "slot" + (id === selected ? " sel" : "");
if (BLOCKS[id] && tex[id]) {
s.style.backgroundImage = `url(${tex[id].toDataURL()})`;
s.style.backgroundSize = "cover";
} else if (itemTex[id]) {
s.style.backgroundImage = `url(${itemTex[id].toDataURL()})`;
s.style.backgroundSize = "cover";
} else if (ITEMS[id]) {
s.textContent = ITEMS[id].icon || "\u{1F4E6}";
} else if (TOOLS[id]) {
s.textContent = TOOLS[id].icon || "\u{1F527}";
} else if (id === "iron_armor") {
s.textContent = "\u{1F6E1}\uFE0F";
s.style.background = "linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%)";
}
const c = document.createElement("div");
c.className = "count";
c.textContent = inv[id];
s.appendChild(c);
s.onclick = () => {
playSound("click");
state.selected = id;
state.recentItems = state.recentItems.filter((item) => item !== id);
state.recentItems.unshift(id);
state.recentItems = state.recentItems.slice(0, 5);
rebuildHotbar();
};
if (id === "iron_armor" && state.player.equippedArmor === "iron_armor") {
const equipped = document.createElement("div");
equipped.className = "equipped-indicator";
equipped.textContent = "\u2713";
s.appendChild(equipped);
}
if (TOOLS[id] && inv[id] > 0) {
let curDur = 0, maxDur = TOOLS[id].durability;
for (const [tid, dur] of toolDurability) {
if (dur.type === id) {
curDur = dur.current;
maxDur = dur.max;
break;
}
}
if (maxDur > 0) {
const bar = document.createElement("div");
bar.style.cssText = `position:absolute;bottom:0;left:0;right:0;height:3px;background:#333;border-radius:0 0 6px 6px;overflow:hidden;`;
const fill = document.createElement("div");
const pct = curDur / maxDur;
const color = pct > 0.5 ? "#2ecc71" : pct > 0.25 ? "#f39c12" : "#e74c3c";
fill.style.cssText = `width:${pct * 100}%;height:100%;background:${color};`;
bar.appendChild(fill);
s.appendChild(bar);
}
}
hotbarEl.appendChild(s);
}
}
// src/render/particles.js
var parts = [];
function spawnExplosion(x, y, power) {
const n = Math.floor(16 + power * 10);
for (let i = 0; i < n; i++) {
parts.push({
x,
y,
vx: (Math.random() - 0.5) * (300 + power * 200),
vy: (Math.random() - 0.5) * (300 + power * 200),
t: 0.7,
c: "#ffa500"
});
}
}
// src/world/tnt.js
function activateTNT(b, fuse = 3.2) {
if (b.dead) return;
if (b.active) return;
b.active = true;
b.fuse = fuse;
state.activeTNT.add(k(b.gx, b.gy));
}
function explodeAt(gx, gy) {
const center = getBlock(gx, gy);
if (!center) return;
let bonus = 0;
for (let x = gx - 2; x <= gx + 2; x++) {
for (let y = gy - 2; y <= gy + 2; y++) {
const b = getBlock(x, y);
if (b && !b.dead && b.t === "tnt" && !(x === gx && y === gy)) {
bonus += 0.8;
activateTNT(b, 0.12);
}
}
}
const power = 1 + bonus;
const radius = 3.2 + bonus * 0.7;
const dmgR = 150 + bonus * 60;
removeBlock(gx, gy);
state.activeTNT.delete(k(gx, gy));
playSound("explode1");
spawnExplosion(gx * TILE + TILE / 2, gy * TILE + TILE / 2, power);
for (let x = Math.floor(gx - radius); x <= Math.ceil(gx + radius); x++) {
for (let y = Math.floor(gy - radius); y <= Math.ceil(gy + radius); y++) {
const d = Math.hypot(x - gx, y - gy);
if (d > radius) continue;
const b = getBlock(x, y);
if (!b || b.dead) continue;
if (BLOCKS[b.t].fluid) continue;
if (BLOCKS[b.t].unbreakable) continue;
if (b.t === "tnt") {
activateTNT(b, 0.12);
continue;
}
removeBlock(x, y);
if (state.inv[b.t] !== void 0 && Math.random() < 0.2) state.inv[b.t]++;
}
}
rebuildHotbar();
const hurt = (e) => {
const dx = e.x + e.w / 2 - (gx * TILE + TILE / 2);
const dy = e.y + e.h / 2 - (gy * TILE + TILE / 2);
const dist = Math.hypot(dx, dy);
if (dist < dmgR) {
const dmg = (dmgR - dist) * 0.06 * power;
if (e === state.player) {
const actualDamage = calculateDamage(dmg);
state.player.hp -= actualDamage;
} else {
e.hp -= dmg;
}
e.vx += (dx / dist || 0) * 600;
e.vy -= 320;
}
};
hurt(state.player);
state.mobs.forEach(hurt);
}
// src/game/save.js
var SAVE_KEY2 = state.SAVE_KEY;
function initDB() {
return new Promise((resolve) => {
console.log("\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u043C localStorage \u0434\u043B\u044F \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0439 (sandbox \u0440\u0435\u0436\u0438\u043C)");
resolve(null);
});
}
function saveGame() {
const saveData = {
version: 2,
worldSeed: state.worldSeed,
player: {
x: state.player.x,
y: state.player.y,
hp: state.player.hp,
hunger: state.player.hunger,
o2: state.player.o2
},
inventory: state.inv,
time: state.worldTime,
isNight: state.isNightTime,
// Сохраняем только изменения
placedBlocks: state.placedBlocks.slice(),
removedBlocks: state.removedBlocks.slice()
};
const saveSize = JSON.stringify(saveData).length;
console.log("\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435: player HP:", state.player.hp, "hunger:", state.player.hunger, "o2:", state.player.o2);
try {
localStorage.setItem(SAVE_KEY2, JSON.stringify(saveData));
console.log(`\u0418\u0433\u0440\u0430 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0430 \u0432 localStorage (\u0440\u0430\u0437\u043C\u0435\u0440: ${saveSize} \u0431\u0430\u0439\u0442)`);
} catch (e) {
console.warn("\u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u044F \u0432 localStorage, \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u043C \u0442\u043E\u043B\u044C\u043A\u043E in-memory:", e);
state.inMemorySave = saveData;
console.log(`\u0418\u0433\u0440\u0430 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0430 \u0432 \u043F\u0430\u043C\u044F\u0442\u0438 (\u0440\u0430\u0437\u043C\u0435\u0440: ${saveSize} \u0431\u0430\u0439\u0442)`);
}
}
function loadGame() {
return new Promise((resolve, reject) => {
try {
const localSave = localStorage.getItem(SAVE_KEY2);
if (localSave) {
const parsed = JSON.parse(localSave);
console.log("\u0417\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u043E \u0438\u0437 localStorage, player HP:", parsed.player?.hp);
resolve(parsed);
return;
}
} catch (e) {
console.warn("\u041E\u0448\u0438\u0431\u043A\u0430 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043A localStorage:", e);
}
if (state.inMemorySave) {
console.log("\u0417\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u043E \u0438\u0437 in-memory \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u044F, player HP:", state.inMemorySave.player?.hp);
resolve(state.inMemorySave);
return;
}
console.log("\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E");
resolve(null);
});
}
function migrateV1toV2(saveData) {
console.log("\u041C\u0438\u0433\u0440\u0430\u0446\u0438\u044F \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u044F \u0441 \u0432\u0435\u0440\u0441\u0438\u0438 1 \u043D\u0430 \u0432\u0435\u0440\u0441\u0438\u044E 2...");
saveData.worldSeed = state.worldSeed;
saveData.placedBlocks = [];
saveData.removedBlocks = [];
delete saveData.generatedBlocks;
saveData.version = 2;
console.log("\u041C\u0438\u0433\u0440\u0430\u0446\u0438\u044F \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0430");
}
async function applySave(saveData) {
if (!saveData) return;
console.log("=== applySave START ===");
console.log("player HP before applySave:", state.player.hp);
console.log("saveData.player.hp:", saveData.player?.hp);
if (saveData.version === 1) {
migrateV1toV2(saveData);
}
if (saveData.worldSeed !== void 0) {
state.worldSeed = saveData.worldSeed;
}
if (saveData.player) {
state.player.x = saveData.player.x;
state.player.y = saveData.player.y;
state.player.hunger = saveData.player.hunger;
state.player.o2 = saveData.player.o2;
state.spawnPoint.x = state.player.x;
state.spawnPoint.y = state.player.y;
const savedHP = saveData.player.hp;
console.log("Saved HP from file:", savedHP);
if (savedHP <= 0) {
console.log("WARNING: Saved HP is <= 0, setting to 100!");
state.player.hp = 100;
} else {
state.player.hp = savedHP;
}
console.log("player HP after restore:", state.player.hp);
console.log("spawnPoint \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D \u0438\u0437 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u044F: x=", state.spawnPoint.x, "y=", state.spawnPoint.y);
} else {
console.log("No player data in save, setting default HP: 100");
state.player.hp = 100;
}
console.log("=== applySave END ===");
if (saveData.inventory) {
for (const key in saveData.inventory) {
state.inv[key] = saveData.inventory[key];
}
}
if (saveData.time !== void 0) {
state.worldTime = saveData.time;
}
if (saveData.isNight !== void 0) {
state.isNightTime = saveData.isNight;
}
regenerateVisibleChunks();
if (saveData.version === 2) {
for (const block of saveData.placedBlocks) {
setBlock(block.gx, block.gy, block.t, true);
}
for (const block of saveData.removedBlocks) {
removeBlock(block.gx, block.gy);
}
state.placedBlocks = saveData.placedBlocks || [];
state.removedBlocks = saveData.removedBlocks || [];
}
rebuildHotbar();
console.log("\u0418\u0433\u0440\u0430 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u0430");
}
// src/ui/save-controls.js
var dropdownOpen = false;
function initSaveControls() {
const menuBtn = document.getElementById("menuBtn");
const menuDropdown = document.getElementById("menuDropdown");
const saveItem = document.getElementById("saveItem");
const resetItem = document.getElementById("resetItem");
const marketItem = document.getElementById("marketItem");
menuBtn.onclick = null;
menuBtn.addEventListener("click", (e) => {
e.stopPropagation();
dropdownOpen = !dropdownOpen;
menuDropdown.classList.toggle("open", dropdownOpen);
});
document.addEventListener("click", () => {
if (dropdownOpen) {
dropdownOpen = false;
menuDropdown.classList.remove("open");
}
});
menuDropdown.addEventListener("click", (e) => {
e.stopPropagation();
});
saveItem.addEventListener("click", () => {
playSound("click");
saveGame();
dropdownOpen = false;
menuDropdown.classList.remove("open");
alert("\u0418\u0433\u0440\u0430 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0430!");
});
resetItem.addEventListener("click", () => {
if (confirm("\u0412\u044B \u0443\u0432\u0435\u0440\u0435\u043D\u044B, \u0447\u0442\u043E \u0445\u043E\u0442\u0438\u0442\u0435 \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435 \u0438 \u043D\u0430\u0447\u0430\u0442\u044C \u043D\u043E\u0432\u0443\u044E \u0438\u0433\u0440\u0443?")) {
playSound("click");
try {
localStorage.removeItem(state.SAVE_KEY);
console.log("\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435 \u0443\u0434\u0430\u043B\u0435\u043D\u043E \u0438\u0437 localStorage");
} catch (e) {
console.warn("\u041E\u0448\u0438\u0431\u043A\u0430 \u0443\u0434\u0430\u043B\u0435\u043D\u0438\u044F \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u044F:", e);
}
state.inMemorySave = null;
state.worldId = Math.random().toString(36).substring(2, 10);
console.log("\u041D\u043E\u0432\u044B\u0439 worldId \u043F\u043E\u0441\u043B\u0435 \u0441\u0431\u0440\u043E\u0441\u0430:", state.worldId);
try {
const newUrl = new URL(window.location.href);
newUrl.searchParams.set("world", state.worldId);
if (typeof window.history !== "undefined" && typeof window.history.replaceState === "function") {
window.history.replaceState(null, "", newUrl.toString());
console.log("URL \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D:", newUrl.toString());
}
} catch (e) {
console.error("\u041E\u0448\u0438\u0431\u043A\u0430 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044F URL:", e);
}
location.reload();
} else {
dropdownOpen = false;
menuDropdown.classList.remove("open");
}
});
marketItem.addEventListener("click", () => {
dropdownOpen = false;
menuDropdown.classList.remove("open");
document.dispatchEvent(new CustomEvent("toggleMarket"));
});
}
function updateSaveButtonVisibility() {
const saveItem = document.getElementById("saveItem");
if (!saveItem) return;
if (state.isMultiplayer && state.otherPlayers.size > 0) {
saveItem.style.display = "none";
} else {
saveItem.style.display = "block";
}
}
// src/multiplayer/socket-helpers.js
function getRandomPlayerColor(socketId) {
const colors = ["#e74c3c", "#3498db", "#2ecc71", "#f39c12", "#9b59b6", "#1abc9c", "#e91e63", "#00bcd4"];
let hash = 0;
for (let i = 0; i < socketId.length; i++) {
hash = (hash << 5) - hash + socketId.charCodeAt(i);
hash = hash & hash;
}
return colors[Math.abs(hash) % colors.length];
}
var lastMoveSendTime = 0;
var MOVE_SEND_INTERVAL2 = 0.05;
var lastSentX = 0;
var lastSentY = 0;
function sendPlayerPosition() {
if (!state.isMultiplayer || !state.socket || !state.socket.connected) return;
const now = performance.now() / 1e3;
if (now - lastMoveSendTime < MOVE_SEND_INTERVAL2) return;
const dx = Math.abs(state.player.x - lastSentX);
const dy = Math.abs(state.player.y - lastSentY);
if (dx < 1 && dy < 1) return;
lastMoveSendTime = now;
lastSentX = state.player.x;
lastSentY = state.player.y;
state.socket.emit("player_move", { x: state.player.x, y: state.player.y, player_name: state.playerName });
}
function sendBlockChange(gx, gy, t, op) {
if (!state.isMultiplayer || !state.socket || !state.socket.connected) return;
state.socket.emit("block_change", { gx, gy, t, op });
}
// src/ui/trade.js
function itemName(id) {
return BLOCKS[id]?.n || TOOLS[id]?.n || ITEMS[id]?.n || id;
}
function itemIcon(id) {
if (BLOCKS[id]) return "\u{1F7EB}";
if (TOOLS[id]) return TOOLS[id].icon || "\u{1F527}";
if (ITEMS[id]) return ITEMS[id].icon || "\u{1F4E6}";
return "\u{1F4E6}";
}
var tradeOpen = false;
var currentTradeId = null;
var myOffer = {};
var partnerOffer = {};
var iConfirmed = false;
function initTradeSocket(socket2) {
socket2.on("trade_request", (data) => {
playSound("click");
const overlay = document.createElement("div");
overlay.id = "tradeRequestOverlay";
overlay.className = "custom-modal-overlay";
overlay.innerHTML = `
<div class="custom-modal-box">
<div style="font-size:18px;margin-bottom:12px;">\u{1F91D} ${data.from_name} \u0445\u043E\u0447\u0435\u0442 \u0442\u043E\u0440\u0433\u043E\u0432\u0430\u0442\u044C</div>
<div class="modal-btns">
<button id="tradeAcceptBtn" style="background:#2ecc71;padding:10px 20px;border:none;border-radius:10px;color:#fff;font-weight:900;font-size:15px;cursor:pointer;">\u041F\u0440\u0438\u043D\u044F\u0442\u044C</button>
<button id="tradeDeclineBtn" style="background:#e74c3c;padding:10px 20px;border:none;border-radius:10px;color:#fff;font-weight:900;font-size:15px;cursor:pointer;">\u041E\u0442\u043A\u043B\u043E\u043D\u0438\u0442\u044C</button>
</div>
</div>`;
document.getElementById("game").appendChild(overlay);
document.getElementById("tradeAcceptBtn").addEventListener("click", () => {
overlay.remove();
socket2.emit("trade_accept", { from_socket_id: data.from });
});
document.getElementById("tradeDeclineBtn").addEventListener("click", () => {
overlay.remove();
socket2.emit("trade_decline", { from_socket_id: data.from });
});
});
socket2.on("trade_start", (data) => {
currentTradeId = data.trade_id;
myOffer = {};
partnerOffer = {};
iConfirmed = false;
tradeOpen = true;
renderTradeUI(data.partner_name);
state.tradePanel.style.display = "block";
});
socket2.on("trade_update", (data) => {
partnerOffer = data.player1Offer || data.player2Offer || {};
const isP1 = state.socket && state.socket.id === data.player1Offer;
if (state.mySocketId) {
}
renderTradeOffers();
});
socket2.on("trade_status", (data) => {
const confirmBtn = document.getElementById("tradeConfirmBtn");
if (confirmBtn) {
const amIPlayer1 = true;
if (iConfirmed) {
confirmBtn.textContent = "\u2705 \u041E\u0436\u0438\u0434\u0430\u043D\u0438\u0435...";
confirmBtn.disabled = true;
confirmBtn.style.background = "#555";
}
}
const partnerStatus = document.getElementById("partnerConfirmStatus");
if (partnerStatus) {
const partnerConfirmed = data.player1Confirm || data.player2Confirm;
partnerStatus.textContent = partnerConfirmed ? "\u2705 \u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u043B" : "\u23F3 \u0415\u0449\u0451 \u043D\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u043B";
}
});
socket2.on("trade_complete", (data) => {
const received = data.received || {};
for (const [item, qty] of Object.entries(received)) {
if (qty > 0) {
state.inv[item] = (state.inv[item] || 0) + qty;
}
}
const sent = data.sent || {};
for (const [item, qty] of Object.entries(sent)) {
if (qty > 0) {
state.inv[item] = (state.inv[item] || 0) - qty;
if (state.inv[item] < 0) state.inv[item] = 0;
}
}
addChatMessage("\u0421\u0438\u0441\u0442\u0435\u043C\u0430", "\u{1F91D} \u0421\u0434\u0435\u043B\u043A\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0430!");
closeTrade();
});
socket2.on("trade_declined", () => {
addChatMessage("\u0421\u0438\u0441\u0442\u0435\u043C\u0430", "\u0422\u043E\u0440\u0433\u043E\u0432\u043B\u044F \u043E\u0442\u043A\u043B\u043E\u043D\u0435\u043D\u0430");
closeTrade();
});
socket2.on("trade_cancelled", () => {
addChatMessage("\u0421\u0438\u0441\u0442\u0435\u043C\u0430", "\u0422\u043E\u0440\u0433\u043E\u0432\u043B\u044F \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u0430");
closeTrade();
});
socket2.on("trade_error", (data) => {
addChatMessage("\u0421\u0438\u0441\u0442\u0435\u043C\u0430", "\u274C " + data.msg);
});
}
function renderTradeUI(partnerName) {
const panel = state.tradePanel;
const content = state.tradeContent;
content.innerHTML = `
<div style="text-align:center;color:#f1c40f;font-weight:900;font-size:16px;margin-bottom:8px;">\u{1F91D} \u0422\u043E\u0440\u0433\u043E\u0432\u043B\u044F \u0441 ${partnerName}</div>
<div style="display:flex;gap:12px;">
<div id="myOfferArea" style="flex:1;">
<div style="color:#2ecc71;font-weight:900;font-size:13px;margin-bottom:4px;">\u0412\u044B \u043F\u0440\u0435\u0434\u043B\u0430\u0433\u0430\u0435\u0442\u0435:</div>
<div id="myOfferItems" style="min-height:40px;padding:6px;border-radius:8px;background:rgba(46,204,113,0.1);border:1px solid rgba(46,204,113,0.3);"></div>
<button id="tradeAddItemBtn" style="width:100%;margin-top:6px;padding:6px;background:#3498db;color:#fff;border:none;border-radius:6px;font-weight:700;cursor:pointer;">+ \u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C</button>
</div>
<div style="display:flex;align-items:center;font-size:24px;">\u21C4</div>
<div id="partnerOfferArea" style="flex:1;">
<div style="color:#e74c3c;font-weight:900;font-size:13px;margin-bottom:4px;">\u041F\u0430\u0440\u0442\u043D\u0451\u0440 \u043F\u0440\u0435\u0434\u043B\u0430\u0433\u0430\u0435\u0442:</div>
<div id="partnerOfferItems" style="min-height:40px;padding:6px;border-radius:8px;background:rgba(231,76,60,0.1);border:1px solid rgba(231,76,60,0.3);">
<span style="color:#888;font-size:12px;">\u041E\u0436\u0438\u0434\u0430\u043D\u0438\u0435...</span>
</div>
</div>
</div>
<div id="partnerConfirmStatus" style="color:#bbb;font-size:11px;text-align:center;margin-top:6px;">\u23F3 \u041F\u0430\u0440\u0442\u043D\u0451\u0440 \u0435\u0449\u0451 \u043D\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u043B</div>
<div style="display:flex;gap:8px;margin-top:10px;">
<button id="tradeConfirmBtn" style="flex:1;padding:8px;background:#2ecc71;color:#fff;border:none;border-radius:8px;font-weight:900;cursor:pointer;">\u2705 \u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044C</button>
<button id="tradeCancelBtn" style="flex:1;padding:8px;background:#e74c3c;color:#fff;border:none;border-radius:8px;font-weight:900;cursor:pointer;">\u2715 \u041E\u0442\u043C\u0435\u043D\u0430</button>
</div>`;
document.getElementById("tradeAddItemBtn")?.addEventListener("click", showAddItemDialog);
document.getElementById("tradeConfirmBtn")?.addEventListener("click", confirmTrade);
document.getElementById("tradeCancelBtn")?.addEventListener("click", cancelTrade);
}
function renderTradeOffers() {
const myItems = document.getElementById("myOfferItems");
if (myItems) {
const keys = Object.keys(myOffer).filter((k2) => myOffer[k2] > 0);
if (keys.length === 0) {
myItems.innerHTML = '<span style="color:#888;font-size:12px;">\u041D\u0438\u0447\u0435\u0433\u043E</span>';
} else {
myItems.innerHTML = keys.map((k2) => `<div style="color:#fff;font-size:12px;">${itemIcon(k2)} ${itemName(k2)} x${myOffer[k2]}</div>`).join("");
}
}
}
function showAddItemDialog() {
const inv = state.inv;
const items = Object.keys(inv).filter((id) => inv[id] > 0);
if (items.length === 0) {
addChatMessage("\u0421\u0438\u0441\u0442\u0435\u043C\u0430", "\u0418\u043D\u0432\u0435\u043D\u0442\u0430\u0440\u044C \u043F\u0443\u0441\u0442!");
return;
}
const overlay = document.createElement("div");
overlay.id = "tradeItemPicker";
overlay.className = "custom-modal-overlay";
let html = '<div class="custom-modal-box" style="max-height:80vh;overflow-y:auto;max-width:360px;"><div style="font-weight:900;font-size:16px;margin-bottom:8px;">\u0427\u0442\u043E \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0438\u0442\u044C?</div>';
for (const id of items) {
const offered = myOffer[id] || 0;
const available = inv[id] - offered;
if (available <= 0) continue;
html += `<div style="display:flex;align-items:center;gap:8px;padding:6px;border-radius:6px;cursor:pointer;border:1px solid rgba(255,255,255,0.1);margin-bottom:4px;" class="trade-pick-item" data-item="${id}">
<span style="font-size:18px;">${itemIcon(id)}</span>
<span style="flex:1;color:#fff;font-size:13px;">${itemName(id)}</span>
<span style="color:#bbb;font-size:11px;">x${available}</span>
</div>`;
}
html += "</div></div>";
overlay.innerHTML = html;
document.getElementById("game").appendChild(overlay);
overlay.querySelectorAll(".trade-pick-item").forEach((el) => {
el.addEventListener("click", () => {
const itemId = el.dataset.item;
const available = inv[itemId] - (myOffer[itemId] || 0);
const qty = Math.min(available, available > 1 ? promptForQty(itemId, available) : 1);
if (qty > 0) {
myOffer[itemId] = (myOffer[itemId] || 0) + qty;
renderTradeOffers();
iConfirmed = false;
const btn = document.getElementById("tradeConfirmBtn");
if (btn) {
btn.textContent = "\u2705 \u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044C";
btn.disabled = false;
btn.style.background = "#2ecc71";
}
}
overlay.remove();
});
});
}
function promptForQty(itemId, max) {
const val = parseInt(prompt(`\u0421\u043A\u043E\u043B\u044C\u043A\u043E ${itemName(itemId)}? (\u043C\u0430\u043A\u0441: ${max})`, "1"));
return isNaN(val) || val < 1 ? 1 : Math.min(val, max);
}
function confirmTrade() {
if (!currentTradeId || !state.socket) return;
for (const [item, qty] of Object.entries(myOffer)) {
if (qty > 0) {
if (state.inv[item] < qty) {
addChatMessage("\u0421\u0438\u0441\u0442\u0435\u043C\u0430", `\u041D\u0435\u0434\u043E\u0441\u0442\u0430\u0442\u043E\u0447\u043D\u043E ${itemName(item)}!`);
return;
}
}
}
state.socket.emit("trade_offer", { trade_id: currentTradeId, offer: myOffer });
setTimeout(() => {
state.socket.emit("trade_confirm", { trade_id: currentTradeId });
iConfirmed = true;
const btn = document.getElementById("tradeConfirmBtn");
if (btn) {
btn.textContent = "\u2705 \u041E\u0436\u0438\u0434\u0430\u043D\u0438\u0435...";
btn.disabled = true;
btn.style.background = "#555";
}
}, 200);
}
function cancelTrade() {
if (!currentTradeId || !state.socket) return;
state.socket.emit("trade_cancel", { trade_id: currentTradeId });
closeTrade();
}
function closeTrade() {
tradeOpen = false;
currentTradeId = null;
myOffer = {};
partnerOffer = {};
iConfirmed = false;
if (state.tradePanel) state.tradePanel.style.display = "none";
document.getElementById("tradeItemPicker")?.remove();
}
// src/multiplayer/socket.js
var socket = null;
function initSocket() {
try {
socket = io(SERVER_URL, {
path: "/socket.io/",
transports: ["websocket", "polling"]
});
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 });
if (state.worldIdEl) state.worldIdEl.textContent = state.worldId;
if (state.multiplayerStatus) state.multiplayerStatus.style.display = "block";
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";
});
socket.on("world_state", (data) => {
console.log("Received world_state:", data);
if (data.seed !== void 0 && 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 !== void 0) {
state.worldTime = data.time;
state.isNightTime = state.worldTime > 0.5;
}
{
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 || "\u0418\u0433\u0440\u043E\u043A"
});
}
}
state.playerCountEl.textContent = data.players.length;
}
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);
}
}
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 || "\u0418\u0433\u0440\u043E\u043A"
});
addChatMessage("\u0421\u0438\u0441\u0442\u0435\u043C\u0430", `\u0418\u0433\u0440\u043E\u043A \u043F\u0440\u0438\u0441\u043E\u0435\u0434\u0438\u043D\u0438\u043B\u0441\u044F`);
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("\u0421\u0438\u0441\u0442\u0435\u043C\u0430", `\u0418\u0433\u0440\u043E\u043A \u043F\u043E\u043A\u0438\u043D\u0443\u043B \u0438\u0433\u0440\u0443`);
updateSaveButtonVisibility();
});
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) {
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 ? "\u0412\u044B" : `\u0418\u0433\u0440\u043E\u043A ${data.socket_id.substring(0, 6)}`;
addChatMessage(senderName, data.message);
});
socket.on("time_update", (data) => {
if (data.time !== void 0) {
state.worldTime = data.time;
state.isNightTime = state.worldTime > 0.5;
}
});
} catch (e) {
console.error("Error initializing socket:", e);
state.isMultiplayer = false;
}
}
// src/data/recipes.js
var RECIPES = [
{ out: "planks", qty: 4, cost: { wood: 1 } },
{ out: "ladder", qty: 3, cost: { planks: 7 } },
{ out: "torch", qty: 2, cost: { coal: 1, planks: 1 } },
{ out: "glass", qty: 1, cost: { sand: 3 } },
{ out: "brick", qty: 1, cost: { stone: 2, clay: 1 } },
{ out: "campfire", qty: 1, cost: { wood: 1, coal: 1 } },
{ out: "tnt", qty: 1, cost: { sand: 2, coal: 1 } },
{ out: "bed", qty: 1, cost: { wood: 3, planks: 3 } },
{ out: "boat", qty: 1, cost: { wood: 5 } },
{ out: "wood_pickaxe", qty: 1, cost: { wood: 3, planks: 2 } },
{ out: "stone_pickaxe", qty: 1, cost: { wood: 1, stone: 3 } },
{ out: "iron_pickaxe", qty: 1, cost: { wood: 1, iron_ore: 2 } },
{ out: "wood_sword", qty: 1, cost: { wood: 2, planks: 1 } },
{ out: "stone_sword", qty: 1, cost: { wood: 1, stone: 2 } },
{ out: "iron_sword", qty: 1, cost: { wood: 1, iron_ore: 1 } },
{ out: "wood_axe", qty: 1, cost: { wood: 3, planks: 2 } },
{ out: "stone_axe", qty: 1, cost: { wood: 1, stone: 3 } },
{ out: "iron_axe", qty: 1, cost: { wood: 1, iron_ore: 2 } },
{ out: "wood_shovel", qty: 1, cost: { wood: 1, planks: 1 } },
{ out: "stone_shovel", qty: 1, cost: { wood: 1, stone: 2 } },
{ out: "iron_shovel", qty: 1, cost: { wood: 1, iron_ore: 1 } },
{ out: "iron_armor", qty: 1, cost: { iron_ore: 5 } },
{ out: "furnace", qty: 1, cost: { stone: 8 } },
{ out: "bow", qty: 1, cost: { wood: 3, planks: 2 } },
{ out: "arrow", qty: 4, cost: { stone: 1, wood: 1 } }
];
var SMELTING_RECIPES = [
{ in: "sand", qty: 1, out: "glass", outQty: 1, time: 3 },
{ in: "clay", qty: 1, out: "brick", outQty: 1, time: 3 },
{ in: "iron_ore", qty: 1, out: "iron_ingot", outQty: 1, time: 5 },
{ in: "gold_ore", qty: 1, out: "gold_ingot", outQty: 1, time: 5 },
{ in: "copper_ore", qty: 1, out: "copper_ingot", outQty: 1, time: 4 },
{ in: "meat", qty: 1, out: "cooked", outQty: 1, time: 2 },
{ in: "cobblestone", qty: 1, out: "stone", outQty: 1, time: 2 }
];
// src/world/water.js
var waterUpdateQueue = /* @__PURE__ */ new Set();
var waterUpdateTimer = 0;
var WATER_UPDATE_INTERVAL = 0.05;
function updateWaterPhysics(dt) {
waterUpdateTimer += dt;
if (waterUpdateTimer < WATER_UPDATE_INTERVAL) return;
waterUpdateTimer = 0;
const MAX_WATER_BLOCKS_PER_UPDATE = 50;
let processedCount = 0;
waterUpdateQueue.clear();
const W = state.W;
const H = state.H;
const minGX = Math.floor(state.camX / TILE) - 10;
const maxGX = Math.floor((state.camX + W) / TILE) + 10;
const minGY = Math.floor(state.camY / TILE) - 10;
const maxGY = Math.floor((state.camY + H) / TILE) + 10;
for (const b of blocks) {
if (processedCount >= MAX_WATER_BLOCKS_PER_UPDATE) break;
if (!b.dead && b.t === "water" && b.gx >= minGX && b.gx <= maxGX && b.gy >= minGY && b.gy <= maxGY) {
waterUpdateQueue.add(k(b.gx, b.gy));
processedCount++;
}
}
const processed = /* @__PURE__ */ new Set();
const toAdd = [];
const MAX_WATER_DEPTH = 20;
for (const key of waterUpdateQueue) {
if (processed.has(key)) continue;
const b = grid.get(key);
if (!b || b.dead) continue;
processed.add(key);
const gx = b.gx;
const gy = b.gy;
if (gy > SEA_GY + MAX_WATER_DEPTH) continue;
const belowKey = k(gx, gy + 1);
const below = grid.get(belowKey);
if (!below || below.dead) {
if (toAdd.length < 20) {
toAdd.push({ gx, gy: gy + 1, t: "water" });
processed.add(belowKey);
}
continue;
}
if (!isSolid(gx, gy + 1) && below && below.t !== "water") {
if (toAdd.length < 20) {
toAdd.push({ gx, gy: gy + 1, t: "water" });
processed.add(belowKey);
}
continue;
}
const leftKey = k(gx - 1, gy);
const left = grid.get(leftKey);
if (!left || left.dead) {
if (toAdd.length < 20) {
toAdd.push({ gx: gx - 1, gy, t: "water" });
processed.add(leftKey);
}
continue;
}
const rightKey = k(gx + 1, gy);
const right = grid.get(rightKey);
if (!right || right.dead) {
if (toAdd.length < 20) {
toAdd.push({ gx: gx + 1, gy, t: "water" });
processed.add(rightKey);
}
continue;
}
}
for (const newData of toAdd) {
const key = k(newData.gx, newData.gy);
if (!grid.has(key)) {
const b = {
gx: newData.gx,
gy: newData.gy,
t: newData.t,
dead: false,
active: false,
fuse: 0
};
grid.set(key, b);
blocks.push(b);
}
}
for (let i = blocks.length - 1; i >= 0; i--) {
if (blocks[i].dead) {
blocks.splice(i, 1);
}
}
}
// src/physics/water-detect.js
function isWaterAt(px, py) {
const gx = Math.floor(px / TILE);
const gy = Math.floor(py / TILE);
const b = getBlock(gx, gy);
return !!(b && b.t === "water");
}
function updateWaterFlag(e) {
const cx = e.x + e.w / 2;
const wasInWater = e.inWater;
const mid = isWaterAt(cx, e.y + e.h / 2);
const feet = isWaterAt(cx, e.y + e.h - 2);
e.inWater = mid || feet;
e.headInWater = isWaterAt(cx, e.y + 4);
if (e === state.player && !wasInWater && e.inWater && e.vy > 100) {
playSound("splash");
}
}
// src/physics/collision.js
function resolveY(e) {
e.grounded = false;
const x1 = e.x + 2;
const x2 = e.x + e.w - 2;
const cx = e.x + e.w / 2;
const cy = e.y + e.h / 2;
const gx = Math.floor(cx / TILE);
const gy = Math.floor(cy / TILE);
const b = getBlock(gx, gy);
const onLadder = b && BLOCKS[b.t] && BLOCKS[b.t].climbable;
if (onLadder) {
e.grounded = true;
if (state.inp.jump) {
e.vy = -200;
} else if (state.inp.down) {
e.vy = 100;
} else {
e.vy = 0;
}
return;
}
const leftGX = Math.floor((e.x - 4) / TILE);
const rightGX = Math.floor((e.x + e.w + 4) / TILE);
const playerGY = Math.floor((e.y + e.h / 2) / TILE);
const leftBlock = getBlock(leftGX, playerGY);
const rightBlock = getBlock(rightGX, playerGY);
const leftLadder = leftBlock && BLOCKS[leftBlock.t] && BLOCKS[leftBlock.t].climbable;
const rightLadder = rightBlock && BLOCKS[rightBlock.t] && BLOCKS[rightBlock.t].climbable;
if ((leftLadder || rightLadder) && state.inp.jump && e.vy < 0) {
if (leftLadder && e.x > leftGX * TILE + TILE / 2) {
e.x = leftGX * TILE + TILE / 2 - e.w / 2;
} else if (rightLadder && e.x < rightGX * TILE + TILE / 2) {
e.x = rightGX * TILE + TILE / 2 - e.w / 2;
}
e.grounded = true;
e.vy = -150;
return;
}
if (e.vy >= 0) {
const probeY = e.y + e.h + 1;
const gy2 = Math.floor(probeY / TILE);
const gxA = Math.floor(x1 / TILE);
const gxB = Math.floor(x2 / TILE);
if (isSolid(gxA, gy2) || isSolid(gxB, gy2)) {
e.y = gy2 * TILE - e.h;
e.vy = 0;
e.grounded = true;
if (e === state.player && !state.player.inWater) {
const fallTiles = (e.y - e.fallStartY) / TILE;
if (fallTiles > 6) {
const damage = calculateDamage((fallTiles - 6) * 10);
state.player.hp -= damage;
}
}
if (e === state.player) e.fallStartY = e.y;
}
}
if (e.vy < 0 && e === state.player) {
const gy2 = Math.floor(e.y / TILE);
const gxA = Math.floor(x1 / TILE);
const gxB = Math.floor(x2 / TILE);
if ((isSolid(gxA, gy2) || isSolid(gxB, gy2)) && !isSolid(gxA, gy2 - 1) && !isSolid(gxB, gy2 - 1)) {
e.y = (gy2 + 1) * TILE;
e.vy = 0;
e.grounded = true;
if (e === state.player) e.fallStartY = e.y;
console.log("Jumped onto block!");
}
}
if (e.vy < 0) {
const gy2 = Math.floor(e.y / TILE);
const gxA = Math.floor(x1 / TILE);
const gxB = Math.floor(x2 / TILE);
if (isSolid(gxA, gy2) || isSolid(gxB, gy2)) {
e.y = (gy2 + 1) * TILE;
e.vy = 0;
}
}
}
function resolveX(e) {
const y1 = e.y + 2;
const y2 = e.y + e.h - 2;
const cx = e.x + e.w / 2;
const cy = e.y + e.h / 2;
const gx = Math.floor(cx / TILE);
const gy = Math.floor(cy / TILE);
const b = getBlock(gx, gy);
const onLadder = b && BLOCKS[b.t] && BLOCKS[b.t].climbable;
if (e.vx > 0) {
const gx2 = Math.floor((e.x + e.w) / TILE);
const gyA = Math.floor(y1 / TILE);
const gyB = Math.floor(y2 / TILE);
const solidA = isSolid(gx2, gyA);
const solidB = isSolid(gx2, gyB);
if (solidA || solidB) {
e.x = gx2 * TILE - e.w;
e.vx = 0;
}
} else if (e.vx < 0) {
const gx2 = Math.floor(e.x / TILE);
const gyA = Math.floor(y1 / TILE);
const gyB = Math.floor(y2 / TILE);
const solidA = isSolid(gx2, gyA);
const solidB = isSolid(gx2, gyB);
if (solidA || solidB) {
e.x = (gx2 + 1) * TILE;
e.vx = 0;
}
}
}
// src/entities/mob-ai.js
function mobAI(m, dt) {
updateWaterFlag(m);
if (m.kind === "zombie") {
const night = isNight();
if (!night) {
m.hp -= 30 * dt;
m.vx *= 0.5;
return;
}
const dir = Math.sign(state.player.x - m.x);
m.vx = dir * m.speed;
if (m.inWater && Math.random() < 0.06) m.vy = -260;
if (Math.abs(m.x + m.w / 2 - (state.player.x + state.player.w / 2)) < 28 && Math.abs(m.y + m.h / 2 - (state.player.y + state.player.h / 2)) < 40 && state.player.invuln <= 0) {
const damage = calculateDamage(15);
state.player.hp -= damage;
state.player.invuln = 0.8;
state.player.vx += dir * 420;
state.player.vy -= 260;
playSound("hit1");
}
} else if (m.kind === "creeper") {
const night = isNight();
if (!night) {
m.hp -= 30 * dt;
m.vx *= 0.5;
return;
}
const dir = Math.sign(state.player.x - m.x);
const dist = Math.hypot(state.player.x + state.player.w / 2 - (m.x + m.w / 2), state.player.y + state.player.h / 2 - (m.y + m.h / 2));
m.vx = dir * m.speed;
if (m.inWater && Math.random() < 0.06) m.vy = -260;
if (dist < 60) {
m.fuse -= dt;
if (m.fuse <= 0) {
explodeAt(Math.floor((m.x + m.w / 2) / TILE), Math.floor((m.y + m.h / 2) / TILE));
m.hp = 0;
}
} else {
if (dist < 40) {
m.fuse = 0.5;
}
}
} else if (m.kind === "skeleton") {
const night = isNight();
if (!night) {
m.hp -= 30 * dt;
m.vx *= 0.5;
return;
}
const dir = Math.sign(state.player.x - m.x);
const dist = Math.hypot(state.player.x + state.player.w / 2 - (m.x + m.w / 2), state.player.y + state.player.h / 2 - (m.y + m.h / 2));
m.vx = dir * m.speed;
if (m.inWater && Math.random() < 0.06) m.vy = -260;
m.shootCooldown -= dt;
if (dist < 300 && m.shootCooldown <= 0) {
m.shootCooldown = 2;
const dx = state.player.x + state.player.w / 2 - (m.x + m.w / 2);
const dy = state.player.y + state.player.h / 2 - (m.y + m.h / 2);
const angle = Math.atan2(dy, dx);
const speed = 450;
state.projectiles.push({
x: m.x + m.w / 2,
y: m.y + m.h / 3,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
dmg: 6,
owner: "mob",
life: 3
});
}
} else {
m.aiT -= dt;
if (m.aiT <= 0) {
m.aiT = 1.8 + Math.random() * 2.5;
m.dir = Math.random() < 0.5 ? -1 : 1;
if (Math.random() < 0.25) m.dir = 0;
}
m.vx = m.dir * (m.kind === "chicken" ? 55 : 40);
if (m.inWater) m.vy = -120;
}
const g = m.inWater ? GRAV_WATER : GRAV;
m.vy += g * dt;
m.y += m.vy * dt;
m.grounded = false;
resolveY(m);
m.x += m.vx * dt;
resolveX(m);
}
function isNight() {
return state.worldTime > 0.5;
}
// src/ui/furnace.js
var furnacePanel = document.getElementById("furnacePanel");
var furnaceContent = document.getElementById("furnaceContent");
function initFurnace() {
document.getElementById("furnaceClose").onclick = () => {
furnacePanel.style.display = "none";
state.currentFurnaceKey = null;
};
}
function openFurnaceUI(gx, gy) {
state.currentFurnaceKey = `${gx},${gy}`;
furnacePanel.style.display = "block";
renderFurnaceUI();
}
function renderFurnaceUI() {
if (!state.currentFurnaceKey) return;
const [fgx, fgy] = state.currentFurnaceKey.split(",").map(Number);
const fb = getBlock(fgx, fgy);
if (!fb || fb.t !== "furnace") {
furnacePanel.style.display = "none";
state.currentFurnaceKey = null;
return;
}
const active = state.activeFurnaces.get(state.currentFurnaceKey);
let html = '<div style="color:#fff;font-size:13px;">';
for (let i = 0; i < SMELTING_RECIPES.length; i++) {
const recipe = SMELTING_RECIPES[i];
const haveCount = state.inv[recipe.in] || 0;
const canSmelt = haveCount >= recipe.qty;
const outDef = BLOCKS[recipe.out];
const outItem = ITEMS[recipe.out];
const iconStr = outItem ? outItem.icon : outDef ? "\u{1F9F1}" : "\u2753";
const nameStr = outItem ? outItem.n : outDef ? outDef.n : recipe.out;
const inItem = ITEMS[recipe.in];
const inDef = BLOCKS[recipe.in];
const inName = inItem ? inItem.n : inDef ? inDef.n : recipe.in;
html += `<div class="recipe">`;
html += `<div class="ricon" style="font-size:24px;display:flex;align-items:center;justify-content:center;">${iconStr}</div>`;
html += `<div class="rinfo">`;
html += `<div class="rname">${nameStr}</div>`;
html += `<div class="rcost">${inName} x${recipe.qty} (\u0435\u0441\u0442\u044C: ${haveCount}) \u2022 ${recipe.time}\u0441</div>`;
html += `</div>`;
html += `<button class="rcraft" onclick="window._smelt(${i})" ${canSmelt ? "" : "disabled"}>\u{1F525}</button>`;
html += `</div>`;
}
if (active) {
const pct = Math.min(100, Math.floor(active.progress / active.recipe.time * 100));
html += `<div style="margin-top:10px;padding:8px;background:rgba(255,255,255,0.1);border-radius:8px;">`;
html += `<div style="color:#f39c12;font-weight:900;">\u{1F525} \u041E\u0431\u0436\u0438\u0433: ${pct}%</div>`;
html += `<div style="background:#333;height:8px;border-radius:4px;margin-top:4px;">`;
html += `<div style="background:#f39c12;height:8px;border-radius:4px;width:${pct}%;"></div>`;
html += `</div></div>`;
}
html += "</div>";
furnaceContent.innerHTML = html;
}
window._smelt = (recipeIdx) => {
if (!state.currentFurnaceKey) return;
const recipe = SMELTING_RECIPES[recipeIdx];
if ((state.inv[recipe.in] || 0) < recipe.qty) return;
if (state.activeFurnaces.has(state.currentFurnaceKey)) return;
state.inv[recipe.in] -= recipe.qty;
state.activeFurnaces.set(state.currentFurnaceKey, {
recipe,
progress: 0
});
playSound("fire");
rebuildHotbar();
renderFurnaceUI();
};
function tickFurnaces(dt) {
for (const [key, furnace] of state.activeFurnaces) {
furnace.progress += dt;
if (furnace.progress >= furnace.recipe.time) {
const outItem = furnace.recipe.out;
if (ITEMS[outItem]) {
state.inv[outItem] = (state.inv[outItem] || 0) + furnace.recipe.outQty;
} else if (BLOCKS[outItem]) {
state.inv[outItem] = (state.inv[outItem] || 0) + furnace.recipe.outQty;
}
playSound("stone_build");
state.activeFurnaces.delete(key);
if (key === state.currentFurnaceKey) {
renderFurnaceUI();
}
}
}
}
// src/world/weather.js
function updateWeather(dt) {
state.weatherTimer += dt;
if (state.weatherTimer >= state.weatherChangeInterval) {
state.weatherTimer = 0;
state.weatherChangeInterval = 60 + Math.random() * 120;
const nightChance = isNight() ? 0.25 : 0.4;
state.isRaining = Math.random() < nightChance;
}
const target = state.isRaining ? 0.4 + Math.random() * 0.01 : 0;
state.rainIntensity += (target - state.rainIntensity) * dt * 0.5;
if (state.rainIntensity < 0.01) state.rainIntensity = 0;
}
function updateRain(dt) {
if (!state.isRaining || state.rainIntensity < 0.01) {
state.raindrops.length = 0;
return;
}
const spawnRate = Math.floor(state.rainIntensity * 60 * dt);
for (let i = 0; i < spawnRate && state.raindrops.length < state.MAX_RAINDROPS; i++) {
state.raindrops.push({
x: state.camX + Math.random() * state.W,
y: state.camY - 20,
vy: 400 + Math.random() * 200,
len: 8 + Math.random() * 12
});
}
for (let i = state.raindrops.length - 1; i >= 0; i--) {
const d = state.raindrops[i];
d.y += d.vy * dt;
d.x -= 30 * dt;
if (d.y > state.camY + state.H + 20) {
state.raindrops.splice(i, 1);
}
}
}
function drawRain() {
if (state.raindrops.length === 0) return;
const ctx = state.ctx;
ctx.save();
ctx.strokeStyle = "rgba(174,194,224,0.5)";
ctx.lineWidth = 1.5;
ctx.beginPath();
for (const d of state.raindrops) {
ctx.moveTo(d.x, d.y);
ctx.lineTo(d.x - 3, d.y + d.len);
}
ctx.stroke();
ctx.restore();
}
// src/entities/mobs.js
var Entity = class {
constructor(x, y, w, h) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.vx = 0;
this.vy = 0;
this.hp = 3;
this.grounded = false;
this.inWater = false;
this.aiT = 0;
this.dir = 1;
}
};
var Pig = class extends Entity {
constructor(x, y) {
super(x, y, 34, 24);
this.kind = "pig";
this.hp = 2;
}
};
var Chicken = class extends Entity {
constructor(x, y) {
super(x, y, 26, 22);
this.kind = "chicken";
this.hp = 1;
}
};
var Zombie = class extends Entity {
constructor(x, y) {
super(x, y, 34, 50);
this.kind = "zombie";
this.hp = 4;
this.speed = 80 + Math.random() * 40;
}
};
var Creeper = class extends Entity {
constructor(x, y) {
super(x, y, 34, 50);
this.kind = "creeper";
this.hp = 4;
this.speed = 60 + Math.random() * 30;
this.fuse = 3.2;
}
};
var Skeleton = class extends Entity {
constructor(x, y) {
super(x, y, 34, 50);
this.kind = "skeleton";
this.hp = 4;
this.speed = 70 + Math.random() * 30;
this.shootCooldown = 0;
}
};
// src/ui/minimap.js
var minimapWrap = document.getElementById("minimapWrap");
var minimapCanvas = document.getElementById("minimap");
var minimapCtx = minimapCanvas.getContext("2d");
var MINIMAP_COLORS = {
grass: "#4a8c2a",
dirt: "#6b3410",
stone: "#6a6a6a",
sand: "#d4b84a",
gravel: "#888",
clay: "#5a9ad4",
wood: "#a63d00",
planks: "#c46a10",
leaves: "#2a8a3a",
glass: "#aaddee",
water: "#2980b9",
coal: "#1a1a2a",
copper_ore: "#a05535",
iron_ore: "#b0b0b0",
gold_ore: "#d4a017",
diamond_ore: "#0090d0",
brick: "#8a2010",
tnt: "#c02020",
campfire: "#d08020",
torch: "#d0a020",
bedrock: "#1a1a1a",
flower: "#d03050",
bed: "#c03060",
ladder: "#a04000",
boat: "#6b3410"
};
function initMinimap() {
document.getElementById("mapToggle").onclick = () => {
playSound("click");
state.minimapOpen = !state.minimapOpen;
minimapWrap.style.display = state.minimapOpen ? "block" : "none";
};
}
function renderMinimap() {
if (!state.minimapOpen) return;
const mW = minimapCanvas.width;
const mH = minimapCanvas.height;
const scale = 2;
const TILE2 = state.TILE;
const player = state.player;
const pGX = Math.floor(player.x / TILE2);
const pGY = Math.floor(player.y / TILE2);
const viewW = Math.floor(mW / scale);
const viewH = Math.floor(mH / scale);
const startGX = pGX - Math.floor(viewW / 2);
const startGY = pGY - Math.floor(viewH / 2);
minimapCtx.fillStyle = isNight() ? "#070816" : "#87CEEB";
minimapCtx.fillRect(0, 0, mW, mH);
const imgData = minimapCtx.createImageData(mW, mH);
const data = imgData.data;
for (let dx = 0; dx < viewW; dx++) {
for (let dy = 0; dy < viewH; dy++) {
const gx = startGX + dx;
const gy = startGY + dy;
const b = getBlock(gx, gy);
if (!b || b.dead || b.t === "air") continue;
const color = MINIMAP_COLORS[b.t];
if (!color) continue;
const r = parseInt(color.slice(1, 3), 16);
const g = parseInt(color.slice(3, 5), 16);
const bl = parseInt(color.slice(5, 7), 16);
for (let sx = 0; sx < scale; sx++) {
for (let sy = 0; sy < scale; sy++) {
const px = dx * scale + sx;
const py = dy * scale + sy;
if (px >= mW || py >= mH) continue;
const idx = (py * mW + px) * 4;
data[idx] = r;
data[idx + 1] = g;
data[idx + 2] = bl;
data[idx + 3] = 255;
}
}
}
}
minimapCtx.putImageData(imgData, 0, 0);
minimapCtx.fillStyle = "#fff";
minimapCtx.fillRect(Math.floor(mW / 2) - 2, Math.floor(mH / 2) - 2, 4, 4);
for (const [sid, p] of state.otherPlayers) {
const dx = Math.floor(p.x / TILE2) - startGX;
const dy = Math.floor(p.y / TILE2) - startGY;
if (dx >= 0 && dx < viewW && dy >= 0 && dy < viewH) {
minimapCtx.fillStyle = "#f1c40f";
minimapCtx.fillRect(dx * scale - 1, dy * scale - 1, 3, 3);
}
}
const allMobsForMap = state.isMultiplayer ? Array.from(state.serverMobs.values()) : state.mobs;
for (const m of allMobsForMap) {
const dx = Math.floor(m.x / TILE2) - startGX;
const dy = Math.floor(m.y / TILE2) - startGY;
if (dx >= 0 && dx < viewW && dy >= 0 && dy < viewH) {
const hostile = m.kind === "zombie" || m.kind === "creeper" || m.kind === "skeleton";
minimapCtx.fillStyle = hostile ? "#e74c3c" : "#2ecc71";
minimapCtx.fillRect(dx * scale, dy * scale, 2, 2);
}
}
}
// src/game/modes.js
var MODES = [{ id: "mine", icon: "\u26CF\uFE0F" }, { id: "build", icon: "\u{1F9F1}" }];
function mode() {
return MODES[state.modeIdx].id;
}
function initModes() {
const modeBtn = document.getElementById("modeBtn");
modeBtn.onclick = () => {
playSound("click");
state.modeIdx = (state.modeIdx + 1) % MODES.length;
modeBtn.textContent = MODES[state.modeIdx].icon;
};
}
// src/input/mouse-handler.js
var mouse = { x: null, y: null };
var miningHeld = false;
var miningInterval = null;
function initMouseHandlers() {
const canvas = state.canvas;
canvas.addEventListener("pointermove", (e) => {
const r = canvas.getBoundingClientRect();
mouse.x = e.clientX - r.left;
mouse.y = e.clientY - r.top;
});
canvas.addEventListener("pointerdown", (e) => {
if (state.craftOpen) return;
if (state.player.hp <= 0) return;
const r = canvas.getBoundingClientRect();
const sx = e.clientX - r.left;
const sy = e.clientY - r.top;
const wx = sx + state.camX;
const wy = sy + state.camY;
const gx = Math.floor(wx / state.TILE);
const gy = Math.floor(wy / state.TILE);
const b = getBlock(gx, gy);
if (state.player.sleeping && b && b.t === "bed") {
state.player.sleeping = false;
return;
}
if (state.player.sleeping) return;
if (b && b.t === "furnace" && mode() === "mine") {
openFurnaceUI(gx, gy);
return;
}
if (mode() === "mine") {
if (state.isMultiplayer) {
for (const [id, sm] of state.serverMobs) {
if (sm.dead) continue;
if (wx >= sm.x && wx <= sm.x + sm.w && wy >= sm.y && wy <= sm.y + sm.h) {
let dmg = 1;
const swordTypes = ["iron_sword", "stone_sword", "wood_sword"];
for (const st of swordTypes) {
if (state.inv[st] > 0) {
dmg = TOOLS[st].damage || 3;
useTool(st);
break;
}
}
state.socket.emit("mob_hurt", { id: sm.id, dmg });
playSound("attack");
return;
}
}
}
for (let i = state.mobs.length - 1; i >= 0; i--) {
const m = state.mobs[i];
if (wx >= m.x && wx <= m.x + m.w && wy >= m.y && wy <= m.y + m.h) {
let dmg = 1;
const swordTypes = ["iron_sword", "stone_sword", "wood_sword"];
for (const st of swordTypes) {
if (state.inv[st] > 0) {
dmg = TOOLS[st].damage || 3;
useTool(st);
break;
}
}
m.hp -= dmg;
m.vx += (m.x - state.player.x) * 2;
m.vy -= 200;
playSound("attack");
if (m.hp <= 0) {
if (m.kind === "chicken") playSound("hurt_chicken");
state.inv.meat += m.kind === "chicken" ? 1 : 2;
if (m.kind === "skeleton") {
state.inv.arrow += 2 + Math.floor(Math.random() * 3);
if (Math.random() < 0.15) state.inv.bow = (state.inv.bow || 0) + 1;
}
state.mobs.splice(i, 1);
rebuildHotbar();
}
return;
}
}
}
if (state.selected === "bow" && state.inv.bow > 0 && state.inv.arrow > 0) {
const aimX = wx - state.player.x - state.player.w / 2;
const aimY = wy - state.player.y - state.player.h / 2;
const angle = Math.atan2(aimY, aimX);
state.projectiles.push({
x: state.player.x + state.player.w / 2,
y: state.player.y + state.player.h / 3,
vx: Math.cos(angle) * 550,
vy: Math.sin(angle) * 550,
dmg: 10,
owner: "player",
life: 4
});
state.inv.arrow--;
useTool("bow");
playSound("hit1");
rebuildHotbar();
return;
}
if (ITEMS[state.selected] && state.inv[state.selected] > 0) {
const it = ITEMS[state.selected];
if (state.player.hp < 100 || state.player.hunger < 100) {
playSound("eat1");
state.player.hunger = Math.min(100, state.player.hunger + it.food);
state.player.hp = Math.min(100, state.player.hp + 15);
state.inv[state.selected]--;
rebuildHotbar();
}
return;
}
if (b && b.t === "campfire" && state.selected === "meat" && state.inv.meat > 0) {
playSound("fire");
state.inv.meat--;
state.inv.cooked++;
rebuildHotbar();
return;
}
if (b && b.t === "bed" && isNight()) {
state.player.sleeping = true;
saveGame();
return;
}
if (mode() === "mine") {
if (!b) return;
if (BLOCKS[b.t].fluid || BLOCKS[b.t].decor) return;
if (b.t === "tnt") {
activateTNT(b, 3.2);
return;
}
const hardness = BLOCKS[b.t].hardness || 1;
if (hardness === Infinity) return;
if (hardness === 0) {
const removed = removeBlock(gx, gy);
if (removed) {
state.inv[removed.t] = (state.inv[removed.t] || 0) + 1;
sendBlockChange(gx, gy, removed.t, "remove");
rebuildHotbar();
}
return;
}
if (state.mining.gx === gx && state.mining.gy === gy) {
state.mining.progress += getMiningSpeed(b.t);
} else {
state.mining = { gx, gy, progress: getMiningSpeed(b.t), hardness };
}
if (b.t === "glass") playSound("glass1");
else if (b.t === "sand") playSound("sand1");
else if (b.t === "stone" || b.t.endsWith("_ore")) playSound("stone1");
else if (b.t === "wood" || b.t === "planks") playSound("wood1");
else playSound("cloth1");
if (state.mining.progress >= hardness) {
const minTool = BLOCKS[b.t].minTool;
let drops = true;
if (minTool) {
const toolId = getBestTool(b.t);
if (toolId) {
const tool = TOOLS[toolId];
if (toolTierLessThan(tool.toolTier, minTool)) drops = false;
else useTool(toolId);
} else {
drops = false;
}
} else {
const toolId = getBestTool(b.t);
if (toolId) useTool(toolId);
}
const removed = removeBlock(gx, gy);
if (removed) {
if (drops) {
state.inv[removed.t] = (state.inv[removed.t] || 0) + 1;
} else {
if (BLOCKS[removed.t]?.toolType === "pickaxe") {
state.inv.cobblestone = (state.inv.cobblestone || 0) + 1;
}
}
sendBlockChange(gx, gy, removed.t, "remove");
if (removed.t === "glass") playSound("glass1");
else if (removed.t === "sand") playSound("sand1");
else if (removed.t === "stone" || removed.t.endsWith("_ore")) playSound("stone1");
else if (removed.t === "wood") playSound("wood1");
else playSound("cloth1");
rebuildHotbar();
}
state.mining = { gx: null, gy: null, progress: 0, hardness: 0 };
}
miningHeld = true;
if (miningInterval) clearInterval(miningInterval);
miningInterval = setInterval(() => {
if (!miningHeld) {
clearInterval(miningInterval);
miningInterval = null;
return;
}
const mb = getBlock(state.mining.gx, state.mining.gy);
if (!mb || mb.dead || mb.t !== b.t) {
miningHeld = false;
clearInterval(miningInterval);
miningInterval = null;
return;
}
if (state.mining.gx !== null && state.mining.hardness > 0) {
state.mining.progress += getMiningSpeed(mb.t) * 0.3;
if (state.mining.progress >= state.mining.hardness) {
const mgx = state.mining.gx;
const mgy = state.mining.gy;
const mMinTool = BLOCKS[mb.t].minTool;
let mDrops = true;
if (mMinTool) {
const mToolId = getBestTool(mb.t);
if (mToolId) {
const mTool = TOOLS[mToolId];
if (toolTierLessThan(mTool.toolTier, mMinTool)) mDrops = false;
else useTool(mToolId);
} else {
mDrops = false;
}
} else {
const mToolId = getBestTool(mb.t);
if (mToolId) useTool(mToolId);
}
const removed = removeBlock(mgx, mgy);
if (removed) {
if (mDrops) {
state.inv[removed.t] = (state.inv[removed.t] || 0) + 1;
} else {
if (BLOCKS[removed.t]?.toolType === "pickaxe") {
state.inv.cobblestone = (state.inv.cobblestone || 0) + 1;
}
}
sendBlockChange(mgx, mgy, removed.t, "remove");
rebuildHotbar();
}
state.mining = { gx: null, gy: null, progress: 0, hardness: 0 };
miningHeld = false;
clearInterval(miningInterval);
miningInterval = null;
}
}
}, 100);
return;
}
if (mode() === "build") {
if (state.inv[state.selected] <= 0) return;
if (!BLOCKS[state.selected]) return;
if (b) return;
if (state.selected === "boat") {
const waterBelow = getBlock(gx, gy + 1);
if (!waterBelow || waterBelow.t !== "water") {
return;
}
state.boat.x = gx * state.TILE;
state.boat.y = gy * state.TILE;
state.boat.vx = 0;
state.boat.vy = 0;
state.boat.active = true;
state.boat.inWater = true;
state.player.inBoat = true;
state.player.x = state.boat.x;
state.player.y = state.boat.y;
state.player.vx = 0;
state.player.vy = 0;
playSound("splash");
state.inv[state.selected]--;
rebuildHotbar();
return;
}
const TILE2 = state.TILE;
const bx = gx * TILE2, by = gy * TILE2;
const overlap = !(bx >= state.player.x + state.player.w || bx + TILE2 <= state.player.x || by >= state.player.y + state.player.h || by + TILE2 <= state.player.y);
if (overlap) return;
setBlock(gx, gy, state.selected, true);
state.inv[state.selected]--;
sendBlockChange(gx, gy, state.selected, "set");
if (state.selected === "stone" || state.selected === "brick") playSound("stone_build");
else if (state.selected === "wood" || state.selected === "planks") playSound("wood_build");
else if (state.selected === "glass") playSound("glass1");
else if (state.selected === "sand") playSound("sand1");
else if (state.selected === "dirt" || state.selected === "grass") playSound("cloth1");
rebuildHotbar();
return;
}
});
canvas.addEventListener("pointerup", () => {
miningHeld = false;
});
canvas.addEventListener("pointerleave", () => {
miningHeld = false;
});
}
// src/render/draw-fire.js
function drawFire(ctx, wx, wy, now) {
const baseX = wx;
const baseY = wy;
const flick = 6 + (Math.sin(now / 90) + 1) * 4;
ctx.fillStyle = "rgba(255,140,0,0.85)";
ctx.beginPath();
ctx.moveTo(baseX + 10, baseY + 30);
ctx.lineTo(baseX + 20, baseY + 30 - flick);
ctx.lineTo(baseX + 30, baseY + 30);
ctx.fill();
ctx.fillStyle = "rgba(255,230,150,0.75)";
ctx.beginPath();
ctx.moveTo(baseX + 14, baseY + 30);
ctx.lineTo(baseX + 20, baseY + 30 - flick * 0.7);
ctx.lineTo(baseX + 26, baseY + 30);
ctx.fill();
}
// src/game/loop.js
var last = performance.now();
var prevJump = false;
document.addEventListener("visibilitychange", () => {
if (!document.hidden) last = performance.now();
});
function loop(now) {
const rawDt = Math.min(0.05, (now - last) / 1e3);
last = now;
const PHYSICS_STEP = 0.016;
const steps = Math.max(1, Math.ceil(rawDt / PHYSICS_STEP));
const dt = rawDt / steps;
const player = state.player;
const inv = state.inv;
const inp2 = state.inp;
const clouds = state.clouds;
const mobs = state.mobs;
const projectiles = state.projectiles;
const parts2 = state.parts;
const boat = state.boat;
const jumpPressed = inp2.j && !prevJump;
prevJump = inp2.j;
if (player.sleeping && isNight()) {
state.worldTime += dt * 8 / state.DAY_LEN;
player.hp = Math.min(100, player.hp + dt * 20);
if (!isNight()) {
player.sleeping = false;
}
} else {
state.worldTime += dt / state.DAY_LEN;
}
if (state.worldTime >= 1) state.worldTime -= 1;
state.camX = Math.floor(player.x + player.w / 2 - state.W / 2);
state.camY = Math.floor(player.y + player.h / 2 - state.H / 2);
ensureGenAroundCamera();
for (const c of clouds) {
c.x -= c.s * dt;
if (c.x + c.w < state.camX - 400) c.x = state.camX + state.W + Math.random() * 700;
}
updateWaterFlag(player);
if (player.headInWater) {
player.o2 = Math.max(0, player.o2 - 6 * dt);
if (player.o2 === 0) {
const damage = calculateDamage(4 * dt);
player.hp -= damage;
}
} else {
player.o2 = Math.min(100, player.o2 + 10 * dt);
}
player.hunger = Math.max(0, player.hunger - dt * 0.2);
if (player.sleeping) {
player.vx = 0;
player.vy = 0;
} else {
const dir = (inp2.r ? 1 : 0) - (inp2.l ? 1 : 0);
if (dir) player.vx = dir * state.MOVE;
else player.vx *= 0.82;
}
if (player.grounded && !player.inWater && Math.abs(player.vx) > 50) {
const stepInterval = 0.35;
if (now / 1e3 - player.lastStepTime > stepInterval) {
playSound("step");
player.lastStepTime = now / 1e3;
}
}
if (player.inBoat) {
const dir = (inp2.r ? 1 : 0) - (inp2.l ? 1 : 0);
if (dir) boat.vx = dir * state.MOVE;
else boat.vx *= 0.95;
boat.vy = 0;
player.x = boat.x + 2;
player.y = boat.y - 4;
player.vx = boat.vx;
player.vy = boat.vy;
player.grounded = true;
player.inWater = false;
if (jumpPressed) {
inv.boat = (inv.boat || 0) + 1;
player.inBoat = false;
boat.active = false;
player.y += state.TILE;
player.vy = -state.JUMP * 0.5;
playSound("splash");
}
} else if (player.inWater) {
player.vx *= 0.9;
player.vy *= 0.92;
if (!jumpPressed && !inp2.j) {
player.vy += state.GRAV_WATER * dt;
} else {
if (jumpPressed) {
player.vy = Math.min(player.vy, -520);
} else if (inp2.j) {
player.vy = Math.min(player.vy, -260);
}
}
} else {
if (jumpPressed && player.grounded && !player.sleeping) {
player.vy = -state.JUMP;
player.grounded = false;
player.fallStartY = player.y;
}
}
if (!player.inWater && !player.inBoat) {
player.vy += state.GRAV * dt;
}
if (boat.active) {
boat.x += boat.vx * dt;
boat.y += boat.vy * dt;
const boatGX = Math.floor(boat.x / state.TILE);
const boatGY = Math.floor(boat.y / state.TILE);
const below = getBlock(boatGX, boatGY + 1);
if (!below || below.t !== "water") {
inv.boat = (inv.boat || 0) + 1;
player.inBoat = false;
boat.active = false;
player.y += state.TILE;
player.vy = -200;
playSound("splash");
}
}
if (player.inBoat && !boat.active) {
inv.boat = (inv.boat || 0) + 1;
player.inBoat = false;
player.y += state.TILE;
player.vy = -200;
playSound("splash");
}
for (let step = 0; step < steps; step++) {
player.y += player.vy * dt;
resolveY(player);
player.x += player.vx * dt;
resolveX(player);
}
sendPlayerPosition();
updateWaterPhysics(dt);
updateWeather(dt);
updateRain(dt);
player.invuln = Math.max(0, player.invuln - dt);
state.voicePosT += dt;
if (state.voicePosT > 0.5 && state.voiceSocket && state.voiceSocket.connected) {
state.voicePosT = 0;
state.voiceSocket.emit("voice_pos", { x: player.x, y: player.y });
}
tickFurnaces(dt);
if (state.currentFurnaceKey && Math.random() < 0.1) {
renderFurnaceUI();
}
for (let i = projectiles.length - 1; i >= 0; i--) {
const p = projectiles[i];
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vy += 400 * dt;
p.life -= dt;
const gx = Math.floor(p.x / state.TILE);
const gy = Math.floor(p.y / state.TILE);
const blk = getBlock(gx, gy);
if (blk && !blk.dead && BLOCKS[blk.t] && BLOCKS[blk.t].solid) {
if (p.owner === "player" && Math.random() < 0.5) inv.arrow++;
projectiles.splice(i, 1);
continue;
}
if (p.owner === "mob") {
if (p.x > player.x && p.x < player.x + player.w && p.y > player.y && p.y < player.y + player.h) {
if (player.invuln <= 0) {
player.hp -= calculateDamage(p.dmg);
player.invuln = 0.4;
player.vx += p.vx * 0.3;
player.vy -= 150;
playSound("hit1");
}
projectiles.splice(i, 1);
continue;
}
} else {
let hitMob = false;
if (state.isMultiplayer) {
for (const [id, sm] of state.serverMobs) {
if (sm.dead) continue;
if (p.x > sm.x && p.x < sm.x + sm.w && p.y > sm.y && p.y < sm.y + sm.h) {
state.socket.emit("mob_arrow_hit", { id: sm.id, dmg: p.dmg, vx: p.vx });
projectiles.splice(i, 1);
hitMob = true;
break;
}
}
}
if (!hitMob) {
for (let j = mobs.length - 1; j >= 0; j--) {
const m = mobs[j];
if (p.x > m.x && p.x < m.x + m.w && p.y > m.y && p.y < m.y + m.h) {
m.hp -= p.dmg;
m.vx += p.vx * 0.2;
m.vy -= 200;
if (m.hp <= 0) {
inv.meat += m.kind === "chicken" ? 1 : 2;
if (m.kind === "skeleton") {
inv.arrow += 2 + Math.floor(Math.random() * 3);
if (Math.random() < 0.15) inv.bow = (inv.bow || 0) + 1;
}
mobs.splice(j, 1);
rebuildHotbar();
}
projectiles.splice(i, 1);
break;
}
}
}
}
if (p.life <= 0) projectiles.splice(i, 1);
}
for (const key of Array.from(state.activeTNT)) {
const b = state.grid.get(key);
if (!b || b.dead) {
state.activeTNT.delete(key);
continue;
}
b.fuse -= dt;
if (b.fuse <= 0) {
explodeAt(b.gx, b.gy);
}
}
state.spawnT += dt;
if (!state.isMultiplayer && state.spawnT > 1.8 && mobs.length < 30) {
state.spawnT = 0;
const spawnLeft = Math.random() < 0.5;
const gx = spawnLeft ? Math.floor((state.camX - 200) / state.TILE) : Math.floor((state.camX + state.W + 200) / state.TILE);
genColumn(gx);
const sgy = surfaceGyAt(gx);
const wx = gx * state.TILE + 4;
const wy = (sgy - 2) * state.TILE;
const top = getBlock(gx, sgy);
if (top && top.t === "water") {
} else {
const night2 = isNight();
if (night2) {
const hostileCount = mobs.filter((m) => m.kind === "zombie" || m.kind === "creeper" || m.kind === "skeleton").length;
if (hostileCount < 12) {
const rand = Math.random();
if (rand < 0.35) {
mobs.push(new Zombie(wx, wy));
} else if (rand < 0.55) {
mobs.push(new Creeper(wx, wy));
} else {
mobs.push(new Skeleton(wx, wy));
}
}
}
const animalCount = mobs.filter((m) => m.kind === "pig" || m.kind === "chicken").length;
if (animalCount < 8) {
mobs.push(Math.random() < 0.5 ? new Pig(wx, wy) : new Chicken(wx, wy));
}
}
}
if (!state.isMultiplayer) {
for (let i = mobs.length - 1; i >= 0; i--) {
const m = mobs[i];
mobAI(m, dt);
if (m.hp <= 0) mobs.splice(i, 1);
}
}
for (let i = parts2.length - 1; i >= 0; i--) {
const p = parts2[i];
p.t -= dt;
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vy += state.GRAV * dt;
if (p.t <= 0) parts2.splice(i, 1);
}
if (player.hp <= 0) {
state.deathEl.style.display = "flex";
} else if (state.deathEl.style.display === "flex") {
state.deathEl.style.display = "none";
}
const ctx = state.ctx;
const W = state.W;
const H = state.H;
const camX = state.camX;
const camY = state.camY;
const TILE2 = state.TILE;
const tex2 = state.tex;
const night = isNight();
ctx.fillStyle = night ? "#070816" : state.isRaining ? "#6B7B8D" : "#87CEEB";
ctx.fillRect(0, 0, W, H);
ctx.save();
ctx.translate(-camX * 0.5, -camY * 0.15);
ctx.fillStyle = "rgba(255,255,255,0.65)";
for (const c of clouds) {
ctx.fillRect(c.x, c.y, c.w, 26);
ctx.fillRect(c.x + 20, c.y - 10, c.w * 0.6, 22);
}
ctx.restore();
ctx.save();
ctx.translate(-camX, -camY);
const minGX = Math.floor(camX / TILE2) - 2;
const maxGX = Math.floor((camX + W) / TILE2) + 2;
const minGY = Math.floor(camY / TILE2) - 6;
const maxGY = Math.floor((camY + H) / TILE2) + 6;
const blocks2 = state.blocks;
for (const b of blocks2) {
if (b.dead) continue;
if (b.gx < minGX || b.gx > maxGX || b.gy < minGY || b.gy > maxGY) continue;
const def = BLOCKS[b.t];
if (def.alpha) {
ctx.save();
ctx.globalAlpha = def.alpha;
ctx.drawImage(tex2[b.t], b.gx * TILE2, b.gy * TILE2, TILE2, TILE2);
ctx.restore();
} else {
ctx.drawImage(tex2[b.t], b.gx * TILE2, b.gy * TILE2, TILE2, TILE2);
}
if (b.t === "tnt" && b.active && Math.sin(now / 60) > 0) {
ctx.fillStyle = "rgba(255,255,255,0.45)";
ctx.fillRect(b.gx * TILE2, b.gy * TILE2, TILE2, TILE2);
}
if (b.t === "campfire") {
drawFire(ctx, b.gx * TILE2, b.gy * TILE2, now);
}
if (b.t === "furnace" && state.activeFurnaces.has(`${b.gx},${b.gy}`)) {
drawFire(ctx, b.gx * TILE2 + 8, b.gy * TILE2 + 5, now);
}
}
if (state.mining.gx !== null && state.mining.hardness > 0) {
const mg = state.mining;
const progress = Math.min(1, mg.progress / mg.hardness);
const cx = mg.gx * TILE2 - camX + camX;
const cy = mg.gy * TILE2 - camY + camY;
const wx = mg.gx * TILE2;
const wy = mg.gy * TILE2;
ctx.save();
ctx.globalAlpha = Math.min(0.8, progress);
const numCracks = Math.floor(progress * 5);
ctx.strokeStyle = "#000";
ctx.lineWidth = 2;
for (let i = 0; i < numCracks; i++) {
ctx.beginPath();
ctx.moveTo(wx + TILE2 / 2, wy + TILE2 / 2);
const angle = (i * 72 + 36) * Math.PI / 180;
ctx.lineTo(wx + TILE2 / 2 + Math.cos(angle) * TILE2 * 0.4, wy + TILE2 / 2 + Math.sin(angle) * TILE2 * 0.4);
ctx.stroke();
}
ctx.fillStyle = `rgba(0,0,0,${progress * 0.4})`;
ctx.fillRect(wx, wy, TILE2, TILE2);
ctx.globalAlpha = 1;
ctx.fillStyle = "#333";
ctx.fillRect(wx, wy + TILE2 - 4, TILE2, 4);
ctx.fillStyle = "#2ecc71";
ctx.fillRect(wx, wy + TILE2 - 4, TILE2 * progress, 4);
ctx.restore();
}
const allMobsRender = state.isMultiplayer ? Array.from(state.serverMobs.values()) : mobs;
for (const m of allMobsRender) {
if (m.kind === "zombie") {
ctx.fillStyle = "#2ecc71";
ctx.fillRect(m.x, m.y, m.w, m.h);
ctx.fillStyle = "#c0392b";
ctx.fillRect(m.x + 6, m.y + 12, 6, 6);
ctx.fillRect(m.x + 22, m.y + 12, 6, 6);
} else if (m.kind === "pig") {
ctx.fillStyle = "#ffb6c1";
ctx.fillRect(m.x, m.y, m.w, m.h);
ctx.fillStyle = "#000";
ctx.fillRect(m.x + 22, m.y + 5, 3, 3);
ctx.fillStyle = "#ff69b4";
ctx.fillRect(m.x + 28, m.y + 12, 6, 6);
} else if (m.kind === "chicken") {
ctx.fillStyle = "#ecf0f1";
ctx.fillRect(m.x, m.y, m.w, m.h);
ctx.fillStyle = "#f39c12";
ctx.fillRect(m.x + 18, m.y + 10, 6, 4);
ctx.fillStyle = "#000";
ctx.fillRect(m.x + 8, m.y + 6, 3, 3);
} else if (m.kind === "creeper") {
ctx.fillStyle = "#4CAF50";
ctx.fillRect(m.x, m.y, m.w, m.h);
ctx.fillStyle = "#000";
ctx.fillRect(m.x + 8, m.y + 8, 4, 4);
ctx.fillRect(m.x + 22, m.y + 8, 4, 4);
ctx.fillRect(m.x + 12, m.y + 20, 10, 4);
ctx.fillStyle = "#4CAF50";
ctx.fillRect(m.x + 4, m.y + 30, 6, 20);
ctx.fillRect(m.x + 24, m.y + 30, 6, 20);
} else if (m.kind === "skeleton") {
ctx.fillStyle = "#ECEFF1";
ctx.fillRect(m.x + 10, m.y + 20, 14, 12);
ctx.fillRect(m.x + 8, m.y + 0, 18, 18);
ctx.fillStyle = "#000";
ctx.fillRect(m.x + 10, m.y + 6, 4, 4);
ctx.fillRect(m.x + 20, m.y + 6, 4, 4);
ctx.fillRect(m.x + 15, m.y + 12, 4, 2);
ctx.fillStyle = "#ECEFF1";
ctx.fillRect(m.x + 2, m.y + 20, 6, 14);
ctx.fillRect(m.x + 26, m.y + 20, 6, 14);
ctx.fillRect(m.x + 10, m.y + 32, 6, 18);
ctx.fillRect(m.x + 18, m.y + 32, 6, 18);
ctx.save();
ctx.translate(m.x + 30, m.y + 22);
ctx.strokeStyle = "#8B4513";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(0, 0, 8, -Math.PI * 0.7, Math.PI * 0.7);
ctx.stroke();
ctx.strokeStyle = "#ccc";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(8 * Math.cos(-Math.PI * 0.7), 8 * Math.sin(-Math.PI * 0.7));
ctx.lineTo(8 * Math.cos(Math.PI * 0.7), 8 * Math.sin(Math.PI * 0.7));
ctx.stroke();
ctx.restore();
}
}
if (boat.active) {
ctx.drawImage(tex2["boat"], boat.x - (TILE2 - boat.w) / 2, boat.y - (TILE2 - boat.h) / 2, TILE2, TILE2);
}
for (const [socketId, p] of state.otherPlayers) {
if (state.heroImg.complete) {
ctx.drawImage(state.heroImg, p.x - (TILE2 - player.w) / 2, p.y - (TILE2 - player.h) / 2, TILE2, TILE2);
} else {
ctx.fillStyle = p.color;
ctx.fillRect(p.x, p.y, 34, 34);
}
ctx.fillStyle = "#fff";
ctx.font = "12px system-ui";
ctx.textAlign = "center";
ctx.fillText(p.name, p.x + 17, p.y - 8);
}
if (state.heroImg.complete) {
ctx.drawImage(state.heroImg, player.x - (TILE2 - player.w) / 2, player.y - (TILE2 - player.h) / 2, TILE2, TILE2);
} else {
ctx.fillStyle = "#fff";
ctx.fillRect(player.x, player.y, player.w, player.h);
}
for (const p of projectiles) {
const angle = Math.atan2(p.vy, p.vx);
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(angle);
ctx.fillStyle = p.owner === "mob" ? "#c0392b" : "#f1c40f";
ctx.fillRect(-12, -1.5, 24, 3);
ctx.beginPath();
ctx.moveTo(12, -4);
ctx.lineTo(16, 0);
ctx.lineTo(12, 4);
ctx.closePath();
ctx.fill();
ctx.fillStyle = "#888";
ctx.fillRect(-12, -3, 4, 2);
ctx.fillRect(-12, 1, 4, 2);
ctx.restore();
}
for (const p of parts2) {
ctx.fillStyle = p.c;
ctx.fillRect(p.x - 2, p.y - 2, 4, 4);
}
for (const m of mobs) {
if (m.kind === "skeleton" && m.shootCooldown > 0.5) {
const arrowX = m.x + m.w / 2;
const arrowY = m.y + 15;
const targetX = player.x + player.w / 2;
const targetY = player.y + player.h / 2;
const angle = Math.atan2(targetY - arrowY, targetX - arrowX);
const dx = targetX - arrowX;
const dy = targetY - arrowY;
const dist = Math.hypot(dx, dy);
ctx.save();
ctx.translate(arrowX, arrowY);
ctx.rotate(angle);
ctx.fillStyle = "#ECEFF1";
ctx.fillRect(0, -1, 16, 2);
ctx.restore();
if (dist < 150 && player.invuln <= 0) {
player.hp -= 8;
player.invuln = 0.5;
player.vx += Math.cos(angle) * 300;
player.vy -= 200;
playSound("hit1");
}
}
}
if (mode() === "build" && mouse.x !== null && !state.craftOpen && player.hp > 0) {
const wx = mouse.x + camX;
const wy = mouse.y + camY;
const ggx = Math.floor(wx / TILE2);
const ggy = Math.floor(wy / TILE2);
ctx.strokeStyle = "rgba(255,255,255,0.9)";
ctx.lineWidth = 2;
ctx.strokeRect(ggx * TILE2, ggy * TILE2, TILE2, TILE2);
}
ctx.restore();
if (night) {
let castLight = function(sx, sy, radius) {
const flick = 0.88 + Math.sin(now / 80 + sx * 0.01) * 0.06 + Math.sin(now / 130 + sy * 0.02) * 0.06;
const r = radius * flick;
const steps2 = 12;
const dists = new Float32Array(steps2);
for (let i = 0; i < steps2; i++) {
const ang = i / steps2 * Math.PI * 2;
const ddx = Math.cos(ang);
const ddy = Math.sin(ang);
let maxDist = r;
for (let step = TILE2 * 0.5; step < r; step += TILE2 * 0.6) {
const lgx = Math.floor((sx + ddx * step) / TILE2);
const lgy = Math.floor((sy + ddy * step) / TILE2);
const blk = getBlock(lgx, lgy);
if (blk && !blk.dead && BLOCKS[blk.t] && BLOCKS[blk.t].solid && step > TILE2 * 0.3) {
maxDist = step;
break;
}
}
dists[i] = maxDist;
}
const cx = sx - camX, cy = sy - camY;
const maxR = Math.max(...dists);
const grad = lightCtx.createRadialGradient(cx, cy, 0, cx, cy, maxR);
grad.addColorStop(0, "rgba(255,255,255,1)");
grad.addColorStop(0.5, "rgba(255,255,255,0.65)");
grad.addColorStop(1, "rgba(255,255,255,0)");
lightCtx.fillStyle = grad;
lightCtx.beginPath();
for (let i = 0; i <= steps2; i++) {
const idx = i % steps2;
const ang = idx / steps2 * Math.PI * 2;
const px = cx + Math.cos(ang) * dists[idx];
const py = cy + Math.sin(ang) * dists[idx];
if (i === 0) lightCtx.moveTo(px, py);
else lightCtx.lineTo(px, py);
}
lightCtx.closePath();
lightCtx.fill();
};
const lightC = state.lightC;
const lightCtx = state.lightCtx;
lightC.width = W * state.dpr;
lightC.height = H * state.dpr;
lightCtx.setTransform(state.dpr, 0, 0, state.dpr, 0, 0);
lightCtx.fillStyle = "rgba(0,0,12,0.82)";
lightCtx.fillRect(0, 0, W, H);
lightCtx.globalCompositeOperation = "destination-out";
for (const b of blocks2) {
if (b.dead) continue;
if (b.gx < minGX - 5 || b.gx > maxGX + 5 || b.gy < minGY - 5 || b.gy > maxGY + 5) continue;
const def = BLOCKS[b.t];
if (def.lightRadius) {
castLight(b.gx * TILE2 + TILE2 / 2, b.gy * TILE2 + TILE2 / 2, def.lightRadius);
}
}
lightCtx.globalCompositeOperation = "source-over";
ctx.drawImage(lightC, 0, 0, W, H);
ctx.save();
ctx.globalCompositeOperation = "lighter";
for (const b of blocks2) {
if (b.dead) continue;
if (b.gx < minGX - 3 || b.gx > maxGX + 3 || b.gy < minGY - 3 || b.gy > maxGY + 3) continue;
const def = BLOCKS[b.t];
if (def.lightRadius) {
const flick = 0.7 + Math.sin(now / 90 + b.gx * 3.7) * 0.15 + Math.sin(now / 140 + b.gy * 2.3) * 0.15;
const wx = b.gx * TILE2 + TILE2 / 2 - camX;
const wy = b.gy * TILE2 + TILE2 / 2 - camY;
const r = def.lightRadius * 0.6 * flick;
const grad = ctx.createRadialGradient(wx, wy, 0, wx, wy, r);
grad.addColorStop(0, `rgba(255,180,80,${0.12 * flick})`);
grad.addColorStop(0.5, `rgba(255,140,40,${0.06 * flick})`);
grad.addColorStop(1, "rgba(255,100,20,0)");
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(wx, wy, r, 0, Math.PI * 2);
ctx.fill();
}
}
ctx.restore();
}
drawRain();
if (Math.random() < 0.25) {
state.hpEl.textContent = Math.max(0, Math.ceil(player.hp));
state.foodEl.textContent = Math.ceil(player.hunger);
document.getElementById("o2").textContent = Math.ceil(player.o2);
state.sxEl.textContent = Math.floor(player.x / TILE2);
state.syEl.textContent = Math.floor(player.y / TILE2);
state.todEl.textContent = night ? "\u041D\u043E\u0447\u044C" : "\u0414\u0435\u043D\u044C";
state.worldIdEl.textContent = state.worldId;
if (state.isMultiplayer) {
document.getElementById("multiplayerStatus").style.display = "flex";
state.playerCountEl.textContent = state.otherPlayers.size + 1;
} else {
document.getElementById("multiplayerStatus").style.display = "none";
}
}
if (player.sleeping) {
ctx.fillStyle = "rgba(0,0,0,0.7)";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "#fff";
ctx.font = "bold 32px system-ui";
ctx.textAlign = "center";
ctx.fillText("\u{1F4A4} \u0421\u043F\u0438\u043C...", W / 2, H / 2);
ctx.font = "18px system-ui";
ctx.fillText("\u041D\u0430\u0436\u043C\u0438\u0442\u0435 \u043D\u0430 \u043A\u0440\u043E\u0432\u0430\u0442\u044C \u0447\u0442\u043E\u0431\u044B \u043F\u0440\u043E\u0441\u043D\u0443\u0442\u044C\u0441\u044F", W / 2, H / 2 + 40);
}
if (state.minimapOpen && Math.random() < 0.25) {
renderMinimap();
}
requestAnimationFrame(loop);
}
// src/ui/market.js
var marketPanel = document.getElementById("marketPanel");
var marketClose = document.getElementById("marketClose");
var marketContent = document.getElementById("marketContent");
function initMarket() {
marketClose.addEventListener("click", () => closeMarket());
document.addEventListener("toggleMarket", () => toggleMarket());
}
function toggleMarket() {
if (marketPanel.style.display === "none" || !marketPanel.style.display) {
openMarket();
} else {
closeMarket();
}
}
function closeMarket() {
marketPanel.style.display = "none";
}
function openMarket() {
marketPanel.style.display = "block";
renderMarket();
}
function renderMarket() {
if (!state.socket) {
marketContent.innerHTML = '<p style="color:#aaa;">\u041F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435 \u043A \u0441\u0435\u0440\u0432\u0435\u0440\u0443...</p>';
return;
}
state.socket.emit("market_list", {}, (orders) => {
if (!orders || !orders.length) {
marketContent.innerHTML = `
<p style="color:#aaa;text-align:center;">\u041F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439 \u043F\u043E\u043A\u0430 \u043D\u0435\u0442</p>
<div style="margin-top:12px;">
<button id="createOrderBtn" style="width:100%;padding:10px;background:#4a8c4a;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:14px;">
+ \u0412\u044B\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043F\u0440\u0435\u0434\u043C\u0435\u0442
</button>
</div>
`;
const btn2 = document.getElementById("createOrderBtn");
if (btn2) btn2.addEventListener("click", showCreateOrder);
return;
}
let html = '<div style="margin-bottom:8px;"><button id="createOrderBtn" style="padding:8px 14px;background:#4a8c4a;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px;">+ \u0412\u044B\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043F\u0440\u0435\u0434\u043C\u0435\u0442</button></div>';
html += orders.map((o) => `
<div style="display:flex;align-items:center;justify-content:space-between;padding:6px 0;border-bottom:1px solid rgba(255,255,255,0.1);">
<div>
<span style="color:#ffd700;">${o.offer_item}</span> \xD7${o.offer_qty}
<span style="color:#888;"> \u21C4 </span>
<span style="color:#7ecfff;">${o.want_item}</span> \xD7${o.want_qty}
</div>
<div style="font-size:11px;color:#888;">${o.seller || "\u0410\u043D\u043E\u043D\u0438\u043C"}</div>
</div>
`).join("");
marketContent.innerHTML = html;
const btn = document.getElementById("createOrderBtn");
if (btn) btn.addEventListener("click", showCreateOrder);
});
}
function showCreateOrder() {
const inv = state.player.inventory || [];
const items = inv.filter((slot) => slot && slot.id);
marketContent.innerHTML = `
<div style="padding:8px;">
<h3 style="margin:0 0 10px;font-size:14px;">\u0412\u044B\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043F\u0440\u0435\u0434\u043C\u0435\u0442</h3>
<label style="font-size:12px;color:#aaa;">\u041E\u0442\u0434\u0430\u044E:</label>
<select id="offerItem" style="width:100%;margin-bottom:8px;padding:6px;background:#222;color:#fff;border:1px solid #555;border-radius:4px;">
${items.map((s) => `<option value="${s.id}">${s.id} (\xD7${s.count || 1})</option>`).join("")}
<option value="">\u2014 \u041F\u0443\u0441\u0442\u043E \u2014</option>
</select>
<label style="font-size:12px;color:#aaa;">\u0425\u043E\u0447\u0443:</label>
<input id="wantItem" placeholder="\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u043F\u0440\u0435\u0434\u043C\u0435\u0442\u0430" style="width:100%;margin-bottom:8px;padding:6px;background:#222;color:#fff;border:1px solid #555;border-radius:4px;" />
<div style="display:flex;gap:8px;">
<button id="submitOrder" style="flex:1;padding:8px;background:#4a8c4a;color:#fff;border:none;border-radius:6px;cursor:pointer;">\u0412\u044B\u0441\u0442\u0430\u0432\u0438\u0442\u044C</button>
<button id="cancelOrder" style="flex:1;padding:8px;background:#555;color:#fff;border:none;border-radius:6px;cursor:pointer;">\u041E\u0442\u043C\u0435\u043D\u0430</button>
</div>
</div>
`;
document.getElementById("cancelOrder").addEventListener("click", () => renderMarket());
document.getElementById("submitOrder").addEventListener("click", () => {
const offerItem = document.getElementById("offerItem").value;
const wantItem = document.getElementById("wantItem").value.trim();
if (!offerItem || !wantItem) {
alert("\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043F\u0440\u0435\u0434\u043C\u0435\u0442 \u0438 \u0443\u043A\u0430\u0436\u0438\u0442\u0435 \u0447\u0442\u043E \u0445\u043E\u0442\u0438\u0442\u0435 \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C");
return;
}
if (state.socket) {
state.socket.emit("market_create", {
offer_item: offerItem,
offer_qty: 1,
want_item: wantItem,
want_qty: 1
});
playSound("click");
setTimeout(renderMarket, 500);
}
});
}
// src/ui/respawn.js
function initRespawn() {
document.getElementById("respawnBtn").onclick = async () => {
playSound("click");
console.log("=== RESPAWN CLICKED ===");
console.log("isMultiplayer:", state.isMultiplayer);
console.log("otherPlayers.size:", state.otherPlayers.size);
console.log("player.hp before respawn:", state.player.hp);
const player = state.player;
const deathEl = state.deathEl;
if (state.isMultiplayer && state.otherPlayers.size > 0) {
console.log("\u041C\u0443\u043B\u044C\u0442\u0438\u043F\u043B\u0435\u0435\u0440 \u0440\u0435\u0436\u0438\u043C - \u0432\u043E\u0437\u0440\u043E\u0436\u0434\u0435\u043D\u0438\u0435 \u0432 \u043D\u0430\u0447\u0430\u043B\u044C\u043D\u043E\u0439 \u0442\u043E\u0447\u043A\u0435");
player.hp = 100;
player.hunger = 100;
player.o2 = 100;
player.vx = player.vy = 0;
player.invuln = 0;
player.x = state.spawnPoint.x;
player.y = state.spawnPoint.y;
player.fallStartY = player.y;
console.log("\u0412\u043E\u0437\u0440\u043E\u0436\u0434\u0435\u043D\u0438\u0435 \u0432 \u043D\u0430\u0447\u0430\u043B\u044C\u043D\u043E\u0439 \u0442\u043E\u0447\u043A\u0435, HP:", player.hp);
} else {
console.log("\u041E\u0434\u0438\u043D\u043E\u0447\u043D\u044B\u0439 \u0440\u0435\u0436\u0438\u043C - \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u043C \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0435 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435");
const loadedSave = await loadGame();
if (loadedSave) {
await applySave(loadedSave);
console.log("\u0417\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u043E \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0435 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435 \u043F\u043E\u0441\u043B\u0435 \u0441\u043C\u0435\u0440\u0442\u0438, final HP:", player.hp);
} else {
player.hp = 100;
player.hunger = 100;
player.o2 = 100;
player.vx = player.vy = 0;
player.invuln = 0;
player.x = state.spawnPoint.x;
player.y = state.spawnPoint.y;
player.fallStartY = player.y;
console.log("\u0412\u043E\u0437\u0440\u043E\u0436\u0434\u0435\u043D\u0438\u0435 \u0432 \u043D\u0430\u0447\u0430\u043B\u044C\u043D\u043E\u0439 \u0442\u043E\u0447\u043A\u0435, HP:", player.hp);
}
}
console.log("player.hp after respawn logic:", player.hp);
console.log("Hiding death screen...");
deathEl.style.display = "none";
console.log("=== RESPAWN END ===");
};
}
// src/ui/share.js
function initShare() {
document.getElementById("worldId").onclick = () => {
const shareUrl = new URL(window.location.href);
shareUrl.searchParams.set("world", state.worldId);
const shareUrlString = shareUrl.toString();
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(shareUrlString).then(() => {
alert("\u0421\u0441\u044B\u043B\u043A\u0430 \u0441\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u043D\u0430!");
}).catch(() => {
alert("\u0421\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 \u043C\u0438\u0440:\n" + shareUrlString);
});
} else {
alert("\u0421\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 \u043C\u0438\u0440:\n" + shareUrlString);
}
};
}
// src/input/controls.js
var inp = state.inp;
function bindHold(el, key) {
const down = (e) => {
e.preventDefault();
state.inp[key] = true;
};
const up = (e) => {
e.preventDefault();
state.inp[key] = false;
};
el.addEventListener("pointerdown", down);
el.addEventListener("pointerup", up);
el.addEventListener("pointerleave", up);
}
function initControls() {
const leftBtn = document.getElementById("left");
const rightBtn = document.getElementById("right");
const jumpBtn = document.getElementById("jump");
const downBtn = document.getElementById("down");
if (leftBtn) bindHold(leftBtn, "l");
if (rightBtn) bindHold(rightBtn, "r");
if (jumpBtn) bindHold(jumpBtn, "j");
if (downBtn) bindHold(downBtn, "s");
window.addEventListener("keydown", (e) => {
if (e.code === "KeyA" || e.code === "ArrowLeft") state.inp.l = true;
if (e.code === "KeyD" || e.code === "ArrowRight") state.inp.r = true;
if (e.code === "Space" || e.code === "KeyW" || e.code === "ArrowUp") state.inp.j = true;
if (e.code === "KeyS" || e.code === "ArrowDown") state.inp.s = true;
});
window.addEventListener("keyup", (e) => {
if (e.code === "KeyA" || e.code === "ArrowLeft") state.inp.l = false;
if (e.code === "KeyD" || e.code === "ArrowRight") state.inp.r = false;
if (e.code === "Space" || e.code === "KeyW" || e.code === "ArrowUp") state.inp.j = false;
if (e.code === "KeyS" || e.code === "ArrowDown") state.inp.s = false;
});
}
// src/multiplayer/voice-chat.js
var voiceSocket = null;
var voiceStream = null;
var audioCtx = null;
var voiceProcessor = null;
var voiceActive = false;
var VOICE_SERVER2 = "https://voicegrech.mkn8n.ru";
var voiceBtn = document.createElement("div");
voiceBtn.innerHTML = '\u{1F3A4}<span style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:36px;color:#e74c3c;font-weight:bold;">/</span>';
voiceBtn.title = "\u0413\u043E\u043B\u043E\u0441\u043E\u0432\u043E\u0439 \u0447\u0430\u0442 (\u0432\u044B\u043A\u043B)";
voiceBtn.style.cssText = "position:absolute;top:74px;right:130px;width:52px;height:52px;border-radius:12px;background:#555;z-index:200;display:flex;align-items:center;justify-content:center;font-size:24px;cursor:pointer;border:2px solid rgba(255,255,255,0.9);box-shadow:0 4px 0 rgba(0,0,0,0.5);pointer-events:auto;";
document.querySelector(".ui").appendChild(voiceBtn);
var speakingIndicator = document.createElement("div");
speakingIndicator.style.cssText = "position:absolute;top:130px;right:10px;z-index:200;display:none;background:rgba(0,0,0,0.7);color:#2ecc71;padding:4px 8px;border-radius:6px;font-size:12px;";
speakingIndicator.textContent = "\u{1F50A}";
document.querySelector(".ui").appendChild(speakingIndicator);
var speakingTimeout = null;
voiceBtn.onclick = async () => {
if (voiceActive) {
voiceActive = false;
voiceBtn.innerHTML = '\u{1F3A4}<span style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:36px;color:#e74c3c;font-weight:bold;">/</span>';
voiceBtn.style.background = "#555";
if (voiceStream) {
voiceStream.getTracks().forEach((t) => t.stop());
voiceStream = null;
}
if (voiceProcessor) {
voiceProcessor.disconnect();
voiceProcessor = null;
}
if (audioCtx) {
audioCtx.close();
audioCtx = null;
}
if (voiceSocket) {
voiceSocket.disconnect();
voiceSocket = null;
}
return;
}
try {
voiceStream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, sampleRate: 24e3 } });
audioCtx = new AudioContext({ sampleRate: 24e3 });
const source = audioCtx.createMediaStreamSource(voiceStream);
voiceProcessor = audioCtx.createScriptProcessor(4096, 1, 1);
voiceProcessor.onaudioprocess = (e) => {
if (!voiceActive || !voiceSocket || !voiceSocket.connected) return;
const pcm = e.inputBuffer.getChannelData(0);
const int16 = new Int16Array(pcm.length);
for (let i = 0; i < pcm.length; i++) {
const s = Math.max(-1, Math.min(1, pcm[i]));
int16[i] = s < 0 ? s * 32768 : s * 32767;
}
voiceSocket.emit("voice_data", int16.buffer);
};
source.connect(voiceProcessor);
voiceProcessor.connect(audioCtx.createGain());
voiceSocket = io(VOICE_SERVER2, { transports: ["websocket"] });
voiceSocket.on("connect", () => {
voiceSocket.emit("voice_join", { world_id: state.worldId, x: state.player.x, y: state.player.y, name: state.playerName || "\u0418\u0433\u0440\u043E\u043A" });
});
voiceSocket.on("voice_in", (payload) => {
const { data, meta, volume } = payload;
if (!audioCtx || audioCtx.state === "closed") return;
const int16 = new Int16Array(data);
const float32 = new Float32Array(int16.length);
for (let i = 0; i < int16.length; i++) {
float32[i] = int16[i] / (int16[i] < 0 ? 32768 : 32767) * volume;
}
const buf = audioCtx.createBuffer(1, float32.length, 24e3);
buf.getChannelData(0).set(float32);
const src = audioCtx.createBufferSource();
src.buffer = buf;
const gain = audioCtx.createGain();
gain.gain.value = volume;
src.connect(gain).connect(audioCtx.destination);
src.start();
speakingIndicator.style.display = "block";
speakingIndicator.textContent = `\u{1F50A} ${meta.name}`;
clearTimeout(speakingTimeout);
speakingTimeout = setTimeout(() => {
speakingIndicator.style.display = "none";
}, 500);
});
voiceActive = true;
voiceBtn.textContent = "\u{1F3A4}";
voiceBtn.style.background = "#2ecc71";
} catch (e) {
console.error("Voice error:", e);
voiceBtn.style.background = "#e74c3c";
}
};
function initVoice() {
let voicePosT = 0;
return {
update(dt) {
voicePosT += dt;
if (voicePosT > 0.5 && voiceSocket && voiceSocket.connected) {
voicePosT = 0;
voiceSocket.emit("voice_pos", { x: state.player.x, y: state.player.y });
}
}
};
}
// src/ui/inventory.js
function renderInventory() {
const inventoryGrid = state.inventoryGrid;
const inv = state.inv;
const selected = state.selected;
inventoryGrid.innerHTML = "";
const items = Object.keys(inv).filter((id) => inv[id] > 0);
for (let i = 0; i < 21; i++) {
const slot = document.createElement("div");
slot.className = "inv-slot" + (i < items.length && items[i] === selected ? " sel" : "");
if (i < items.length) {
const id = items[i];
if (BLOCKS[id] && tex[id]) {
slot.style.backgroundImage = `url(${tex[id].toDataURL()})`;
slot.style.backgroundSize = "cover";
} else if (itemTex[id]) {
slot.style.backgroundImage = `url(${itemTex[id].toDataURL()})`;
slot.style.backgroundSize = "cover";
} else if (ITEMS[id]) {
slot.textContent = ITEMS[id].icon || "\u{1F4E6}";
} else if (TOOLS[id]) {
slot.textContent = TOOLS[id].icon || "\u{1F527}";
} else if (id === "iron_armor") {
slot.textContent = "\u{1F6E1}\uFE0F";
slot.style.background = "linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%)";
}
const count = document.createElement("div");
count.className = "inv-count";
count.textContent = inv[id];
slot.appendChild(count);
slot.onclick = () => {
playSound("click");
state.selected = id;
state.recentItems = state.recentItems.filter((item) => item !== id);
state.recentItems.unshift(id);
state.recentItems = state.recentItems.slice(0, 5);
rebuildHotbar();
renderInventory();
};
slot.ondblclick = () => {
if (id === "iron_armor" && inv.iron_armor > 0) {
if (state.player.equippedArmor === "iron_armor") {
state.player.equippedArmor = null;
state.player.armor = 0;
} else {
state.player.equippedArmor = "iron_armor";
state.player.armor = BLOCKS["iron_armor"].armor;
}
playSound("click");
renderInventory();
}
};
}
inventoryGrid.appendChild(slot);
}
}
// src/ui/craft.js
function canCraft(r) {
const inv = state.inv;
for (const res in r.cost) {
if ((inv[res] || 0) < r.cost[res]) return false;
}
return true;
}
function renderCraft() {
const inv = state.inv;
const recipesEl = state.recipesEl;
recipesEl.innerHTML = "";
for (const r of RECIPES) {
const row = document.createElement("div");
row.className = "recipe";
const icon = document.createElement("div");
icon.className = "ricon";
if (tex[r.out]) {
icon.style.backgroundImage = `url(${tex[r.out].toDataURL()})`;
} else if (itemTex[r.out]) {
icon.style.backgroundImage = `url(${itemTex[r.out].toDataURL()})`;
} else if (TOOLS[r.out]) {
icon.textContent = TOOLS[r.out].icon || "\u{1F527}";
icon.style.fontSize = "24px";
icon.style.display = "flex";
icon.style.alignItems = "center";
icon.style.justifyContent = "center";
} else if (ITEMS[r.out]) {
icon.textContent = ITEMS[r.out].icon || "\u{1F4E6}";
icon.style.fontSize = "24px";
icon.style.display = "flex";
icon.style.alignItems = "center";
icon.style.justifyContent = "center";
}
const info = document.createElement("div");
info.className = "rinfo";
const nm = document.createElement("div");
nm.className = "rname";
const itemName2 = BLOCKS[r.out]?.n || TOOLS[r.out]?.n || ITEMS[r.out]?.n || r.out;
nm.textContent = `${itemName2} x${r.qty}`;
const cs = document.createElement("div");
cs.className = "rcost";
cs.textContent = Object.keys(r.cost).map((x) => {
const cn = BLOCKS[x]?.n || TOOLS[x]?.n || ITEMS[x]?.n || x;
return `${cn}: ${inv[x] || 0}/${r.cost[x]}`;
}).join(" ");
info.appendChild(nm);
info.appendChild(cs);
const btn = document.createElement("button");
btn.className = "rcraft";
btn.textContent = "\u0421\u043E\u0437\u0434\u0430\u0442\u044C";
btn.disabled = !canCraft(r);
btn.onclick = () => {
if (!canCraft(r)) return;
playSound("click");
for (const res in r.cost) inv[res] -= r.cost[res];
inv[r.out] = (inv[r.out] || 0) + r.qty;
if (TOOLS[r.out]) addTool(r.out);
rebuildHotbar();
renderCraft();
};
row.appendChild(icon);
row.appendChild(info);
row.appendChild(btn);
recipesEl.appendChild(row);
}
}
var craftOpen = false;
var inventoryOpen = false;
function toggleCraft() {
playSound("click");
craftOpen = !craftOpen;
state.craftPanel.style.display = craftOpen ? "block" : "none";
if (craftOpen) {
renderCraft();
inventoryOpen = false;
state.inventoryPanel.style.display = "none";
}
}
function closeCraft() {
playSound("click");
craftOpen = false;
state.craftPanel.style.display = "none";
}
function toggleInventory() {
playSound("click");
inventoryOpen = true;
state.inventoryPanel.style.display = "block";
renderInventory();
craftOpen = false;
state.craftPanel.style.display = "none";
}
function closeInventory() {
playSound("click");
inventoryOpen = false;
state.inventoryPanel.style.display = "none";
}
// src/main.js
var urlParams2 = new URLSearchParams(window.location.search);
state.SERVER_URL = SERVER_URL;
state.TELEGRAM_BOT_USERNAME = "Grechkacraft_bot";
state.TELEGRAM_APP_SHORT_NAME = "minegrechka";
state.playerName = localStorage.getItem("minegrechka_playerName") || null;
if (!state.playerName) {
state.playerName = prompt("\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0432\u0430\u0448\u0435 \u0438\u043C\u044F \u0434\u043B\u044F \u0438\u0433\u0440\u044B:") || "\u0418\u0433\u0440\u043E\u043A";
localStorage.setItem("minegrechka_playerName", state.playerName);
console.log("Player name set:", state.playerName);
}
console.log("Current URL:", window.location.href);
var worldParam = urlParams2.get("world");
console.log("world param:", worldParam);
state.worldId = worldParam && worldParam.trim() !== "" ? worldParam : null;
console.log("worldId after params:", state.worldId, "type:", typeof state.worldId);
if (!state.worldId) {
state.worldId = Math.random().toString(36).substring(2, 10);
console.log("Generated worldId:", state.worldId);
try {
const newUrl = new URL(window.location.href);
newUrl.searchParams.set("world", state.worldId);
const newUrlString = newUrl.toString();
console.log("New URL to set:", newUrlString);
if (typeof window.history !== "undefined" && typeof window.history.replaceState === "function") {
window.history.replaceState(null, "", newUrlString);
console.log("URL after replaceState:", window.location.href);
} else {
console.error("History API not supported!");
}
} catch (e) {
console.error("Error updating URL:", e);
}
console.log("Generated new worldId for browser:", state.worldId);
}
console.log("Final worldId:", state.worldId, "Player name:", state.playerName);
initCanvas();
state.hpEl = document.getElementById("hp");
state.foodEl = document.getElementById("food");
state.sxEl = document.getElementById("sx");
state.syEl = document.getElementById("sy");
state.todEl = document.getElementById("tod");
state.worldIdEl = document.getElementById("worldId");
state.playerCountEl = document.getElementById("playerCount");
state.deathEl = document.getElementById("death");
state.hotbarEl = document.getElementById("hotbar");
state.craftPanel = document.getElementById("craftPanel");
state.recipesEl = document.getElementById("recipes");
state.inventoryPanel = document.getElementById("inventoryPanel");
state.inventoryGrid = document.getElementById("inventoryGrid");
state.marketPanel = document.getElementById("marketPanel");
state.marketContent = document.getElementById("marketContent");
state.tradePanel = document.getElementById("tradePanel");
state.tradeContent = document.getElementById("tradeContent");
state.multiplayerStatus = document.getElementById("multiplayerStatus");
initTextures();
state.tex = tex;
state.itemTex = itemTex;
var craftBtn = document.getElementById("craftBtn");
var invToggle = document.getElementById("invToggle");
var craftClose = document.getElementById("craftClose");
var inventoryClose = document.getElementById("inventoryClose");
if (craftBtn) craftBtn.onclick = () => toggleCraft();
if (craftClose) craftClose.onclick = () => closeCraft();
if (invToggle) invToggle.onclick = () => toggleInventory();
if (inventoryClose) inventoryClose.onclick = () => closeInventory();
initControls();
initMouseHandlers();
initChat();
initFurnace();
initMinimap();
initSaveControls();
initMarket();
initRespawn();
initShare();
initModes();
initSocket();
initVoice();
rebuildHotbar();
initDB().then(async () => {
const loadedSave = await loadGame();
if (loadedSave) {
await applySave(loadedSave);
console.log("\u0417\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u043E \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435, HP:", state.player.hp);
if (state.player.hp <= 0) {
console.log("WARNING: HP <= 0 \u043F\u043E\u0441\u043B\u0435 \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0438, \u0432\u043E\u0437\u0440\u043E\u0436\u0434\u0430\u0435\u043C\u0441\u044F");
state.player.hp = 100;
state.player.hunger = 100;
state.player.o2 = 100;
state.player.x = state.spawnPoint.x;
state.player.y = state.spawnPoint.y;
state.player.vx = state.player.vy = 0;
state.player.invuln = 0;
state.player.fallStartY = state.player.y;
}
} else {
console.log("\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E, \u043D\u0430\u0447\u0438\u043D\u0430\u0435\u043C \u043D\u043E\u0432\u0443\u044E \u0438\u0433\u0440\u0443");
state.player.hp = 100;
state.player.hunger = 100;
state.player.o2 = 100;
state.player.vx = state.player.vy = 0;
state.player.invuln = 0;
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 = state.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.player.y = safeGY * state.TILE;
state.player.x = startGX * state.TILE;
state.player.fallStartY = state.player.y;
state.spawnPoint.x = state.player.x;
state.spawnPoint.y = state.player.y;
console.log("\u041D\u043E\u0432\u0430\u044F \u0438\u0433\u0440\u0430: startGX=", startGX, "surfaceY=", surfaceY, "player.y=", state.player.y);
for (let gx = startGX - 50; gx <= startGX + 50; gx++) {
genColumn(gx);
}
}
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
saveGame();
}
});
window.addEventListener("beforeunload", () => {
saveGame();
});
}).catch((err) => {
console.error("\u041E\u0448\u0438\u0431\u043A\u0430 \u0438\u043D\u0438\u0446\u0438\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u0438:", err);
const startGX = 6;
genColumn(startGX);
state.player.y = (surfaceGyAt(startGX) - 1) * state.TILE;
state.player.fallStartY = state.player.y;
for (let gx = startGX - 50; gx <= startGX + 50; gx++) {
genColumn(gx);
}
});
requestAnimationFrame(loop);
})();