(() => { // 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; // src/core/state.js var state = { // Камера camX: 0, camY: 0, // День/ночь worldTime: 0, isNightTime: false, // Мультиплеер isMultiplayer: false, mySocketId: null, socket: null, // Инвентарь/UI selected: 0, showFullInventory: false, craftOpen: false, inventoryOpen: false, chatOpen: false, modeIdx: 0, // Мир 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, // Игрок 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, iron_armor: 0, bow: 0, furnace: 0, bed: 0, boat: 0, iron_ingot: 0, gold_ingot: 0, copper_ingot: 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 }, // Мышь 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 }; // 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."); } var worldId = null; var playerName = localStorage.getItem("minegrechka_playerName") || null; if (!playerName) { 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", playerName); console.log("Player name set:", playerName); } console.log("Current URL:", window.location.href); var worldParam = urlParams.get("world"); console.log("world param:", worldParam); worldId = worldParam && worldParam.trim() !== "" ? worldParam : null; console.log("worldId after params:", worldId, "type:", typeof worldId); if (!worldId) { worldId = Math.random().toString(36).substring(2, 10); console.log("Generated worldId:", worldId); try { const newUrl = new URL(window.location.href); newUrl.searchParams.set("world", 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); console.log("URL after replaceState (direct check):", window.location.search); } else { console.error("History API not supported!"); } } catch (e) { console.error("Error updating URL:", e); } console.log("Generated new worldId for browser:", worldId); } console.log("Final worldId:", worldId, "Player name:", playerName); console.log(`Server URL: ${SERVER_URL}, World ID: ${worldId}`); // src/data/blocks.js var BLOCKS = { air: { n: "\u0412\u043E\u0437\u0434\u0443\u0445", solid: false }, grass: { n: "\u0422\u0440\u0430\u0432\u0430", c: "#7cfc00", solid: true }, dirt: { n: "\u0413\u0440\u044F\u0437\u044C", c: "#8b4513", solid: true }, stone: { n: "\u041A\u0430\u043C\u0435\u043D\u044C", c: "#7f8c8d", solid: true }, sand: { n: "\u041F\u0435\u0441\u043E\u043A", c: "#f4d06f", solid: true }, gravel: { n: "\u0413\u0440\u0430\u0432\u0438\u0439", c: "#95a5a6", solid: true }, clay: { n: "\u0413\u043B\u0438\u043D\u0430", c: "#74b9ff", solid: true }, wood: { n: "\u0414\u0435\u0440\u0435\u0432\u043E", c: "#d35400", solid: true }, planks: { n: "\u0414\u043E\u0441\u043A\u0438", c: "#e67e22", solid: true }, ladder: { n: "\u041B\u0435\u0441\u0442\u043D\u0438\u0446\u0430", c: "#d35400", solid: false, climbable: true }, leaves: { n: "\u041B\u0438\u0441\u0442\u0432\u0430", c: "#2ecc71", solid: true }, glass: { n: "\u0421\u0442\u0435\u043A\u043B\u043E", c: "rgba(200,240,255,0.25)", solid: true, alpha: 0.55 }, water: { n: "\u0412\u043E\u0434\u0430", c: "rgba(52,152,219,0.55)", solid: false, fluid: true }, coal: { n: "\u0423\u0433\u043E\u043B\u044C", c: "#2c3e50", solid: true }, copper_ore: { n: "\u041C\u0435\u0434\u044C", c: "#e17055", solid: true }, iron_ore: { n: "\u0416\u0435\u043B\u0435\u0437\u043E", c: "#dcdde1", solid: true }, iron_armor: { n: "\u0416\u0435\u043B\u0435\u0437\u043D\u0430\u044F \u0431\u0440\u043E\u043D\u044F", c: "#95a5a6", solid: false, armor: 0.5 }, gold_ore: { n: "\u0417\u043E\u043B\u043E\u0442\u043E", c: "#f1c40f", solid: true }, diamond_ore: { n: "\u0410\u043B\u043C\u0430\u0437", c: "#00a8ff", solid: true }, brick: { n: "\u041A\u0438\u0440\u043F\u0438\u0447", c: "#c0392b", solid: true }, tnt: { n: "TNT", c: "#e74c3c", solid: true, explosive: true }, campfire: { n: "\u041A\u043E\u0441\u0442\u0451\u0440", c: "#e67e22", solid: true, lightRadius: 190 }, torch: { n: "\u0424\u0430\u043A\u0435\u043B", c: "#f9ca24", solid: true, lightRadius: 140 }, bedrock: { n: "\u0411\u0435\u0434\u0440\u043E\u043A", c: "#2d3436", solid: true, unbreakable: true }, flower: { n: "\u0426\u0432\u0435\u0442\u043E\u043A", c: "#ff4757", solid: false, decor: true }, bed: { n: "\u041A\u0440\u043E\u0432\u0430\u0442\u044C", c: "#e91e63", solid: true, bed: true }, boat: { n: "\u041B\u043E\u0434\u043A\u0430", c: "#8B4513", solid: false }, furnace: { n: "\u041F\u0435\u0447\u044C", c: "#696969", solid: true, smelting: true } }; // src/world/world-storage.js var grid = /* @__PURE__ */ new Map(); var blocks = []; function k(gx, gy) { return gx + "," + gy; } function getBlock2(gx, gy) { return grid.get(k(gx, gy)); } function isSolid(gx, gy) { const b = getBlock2(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 = getBlock2(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_MESSAGES = 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_MESSAGES) { chatMessages.shift(); } renderChatMessages(); } function renderChatMessages() { const chatMessagesEl = document.getElementById("chatMessages"); if (!chatMessagesEl) return; chatMessagesEl.innerHTML = chatMessages.map( (m) => `
${m.time} ${m.sender}: ${m.message}
` ).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}" }; // 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", durability: 60, miningPower: 1, craft: { wood: 3, planks: 2 } }, stone_pickaxe: { n: "\u041A\u0430\u043C\u0435\u043D\u043D\u0430\u044F \u043A\u0438\u0440\u043A\u0430", icon: "\u26CF\uFE0F", durability: 130, miningPower: 2, craft: { wood: 1, stone: 3 } }, iron_pickaxe: { n: "\u0416\u0435\u043B\u0435\u0437\u043D\u0430\u044F \u043A\u0438\u0440\u043A\u0430", icon: "\u26CF\uFE0F", durability: 250, miningPower: 3, craft: { wood: 1, iron_ore: 2 } }, wood_sword: { n: "\u0414\u0435\u0440\u0435\u0432\u044F\u043D\u043D\u044B\u0439 \u043C\u0435\u0447", icon: "\u2694\uFE0F", durability: 40, damage: 5, craft: { wood: 2, planks: 1 } }, stone_sword: { n: "\u041A\u0430\u043C\u0435\u043D\u043D\u044B\u0439 \u043C\u0435\u0447", icon: "\u2694\uFE0F", durability: 100, damage: 8, craft: { wood: 1, stone: 2 } }, iron_sword: { n: "\u0416\u0435\u043B\u0435\u0437\u043D\u044B\u0439 \u043C\u0435\u0447", icon: "\u2694\uFE0F", durability: 200, damage: 12, craft: { wood: 1, iron_ore: 1 } }, bow: { n: "\u041B\u0443\u043A", icon: "\u{1F3F9}", durability: 150, craft: { wood: 3, string: 0 } } }; 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; } // src/render/textures.js var tex = {}; 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)); } // 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]) { s.style.backgroundImage = `url(${tex[id].toDataURL()})`; s.style.backgroundSize = "cover"; } else if (ITEMS[id]) { s.textContent = ITEMS[id].icon; } else if (TOOLS[id]) { s.textContent = TOOLS[id].icon; } 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 = getBlock2(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 = getBlock2(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 = getBlock2(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_KEY = 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_KEY, 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_KEY); 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 function initSaveControls() { const saveBtn = document.getElementById("saveBtn"); saveBtn.onclick = () => { playSound("click"); saveGame(); alert("\u0418\u0433\u0440\u0430 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0430!"); }; const resetBtn = document.getElementById("resetBtn"); resetBtn.onclick = () => { 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); const newUrlString = newUrl.toString(); if (typeof window.history !== "undefined" && typeof window.history.replaceState === "function") { window.history.replaceState(null, "", newUrlString); console.log("URL \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D:", newUrlString); } } catch (e) { console.error("\u041E\u0448\u0438\u0431\u043A\u0430 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044F URL:", e); } location.reload(); } }; } function updateSaveButtonVisibility() { const saveBtn = document.getElementById("saveBtn"); if (state.isMultiplayer && state.otherPlayers.size > 0) { saveBtn.style.display = "none"; } else { saveBtn.style.display = "flex"; } } // 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_INTERVAL = 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_INTERVAL) 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/multiplayer/socket.js var socket = null; function initSocket() { try { socket = io(SERVER_URL, { path: "/socket.io/", transports: ["websocket", "polling"] }); socket.on("connect", () => { console.log(`Connected to server: ${SERVER_URL}, socket.id: ${socket.id}, worldId: ${worldId}`); state.mySocketId = socket.id; state.isMultiplayer = true; socket.emit("join_world", { world_id: worldId, player_name: playerName }); state.worldIdEl.textContent = worldId; state.multiplayerStatus.style.display = "block"; }); socket.on("connect_error", (error) => { console.error("Socket connection error:", error); state.isMultiplayer = false; }); socket.on("disconnect", () => { console.log("Disconnected from server"); state.isMultiplayer = false; state.otherPlayers.clear(); state.multiplayerStatus.style.display = "none"; }); 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); } } }); 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 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/core/canvas.js var gameEl = document.getElementById("game"); var canvas = document.getElementById("c"); var ctx = canvas.getContext("2d"); var lightC = document.createElement("canvas"); var lightCtx = lightC.getContext("2d"); var dpr = Math.max(1, window.devicePixelRatio || 1); var W = 0; var H = 0; // 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 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 = getBlock2(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 = getBlock2(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 = getBlock2(leftGX, playerGY); const rightBlock = getBlock2(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 = getBlock2(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 = getBlock2(fgx, fgy); if (!fb || fb.t !== "furnace") { furnacePanel.style.display = "none"; state.currentFurnaceKey = null; return; } const active = state.activeFurnaces.get(state.currentFurnaceKey); let html = '
'; 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 += `
`; html += `
${iconStr}
`; html += `
`; html += `
${nameStr}
`; html += `
${inName} x${recipe.qty} (\u0435\u0441\u0442\u044C: ${haveCount}) \u2022 ${recipe.time}\u0441
`; html += `
`; html += ``; html += `
`; } if (active) { const pct = Math.min(100, Math.floor(active.progress / active.recipe.time * 100)); html += `
`; html += `
\u{1F525} \u041E\u0431\u0436\u0438\u0433: ${pct}%
`; html += `
`; html += `
`; html += `
`; } html += "
"; 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 ctx2 = state.ctx; ctx2.save(); ctx2.strokeStyle = "rgba(174,194,224,0.5)"; ctx2.lineWidth = 1.5; ctx2.beginPath(); for (const d of state.raindrops) { ctx2.moveTo(d.x, d.y); ctx2.lineTo(d.x - 3, d.y + d.len); } ctx2.stroke(); ctx2.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 = getBlock2(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 }; function initMouseHandlers() { const canvas2 = state.canvas; canvas2.addEventListener("pointermove", (e) => { const r = canvas2.getBoundingClientRect(); mouse.x = e.clientX - r.left; mouse.y = e.clientY - r.top; }); canvas2.addEventListener("pointerdown", (e) => { if (state.craftOpen) return; if (state.player.hp <= 0) return; const r = canvas2.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 = getBlock2(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 removed = removeBlock(gx, gy); if (removed) { state.inv[removed.t] = (state.inv[removed.t] || 0) + 1; const pickTypes = ["iron_pickaxe", "stone_pickaxe", "wood_pickaxe"]; for (const pt of pickTypes) { if (state.inv[pt] > 0) { const broke = useTool(pt); if (broke) playSound("cloth1"); break; } } sendBlockChange(gx, gy, removed.t, "remove"); if (removed.t === "glass") playSound("glass1"); else if (removed.t === "sand") playSound("sand1"); else if (removed.t === "snow") playSound("snow1"); else if (removed.t === "stone" || removed.t.endsWith("_ore")) playSound("stone1"); else if (removed.t === "wood") playSound("wood1"); else playSound("cloth1"); rebuildHotbar(); } 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 = getBlock2(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 === "snow") playSound("snow1"); else if (state.selected === "dirt" || state.selected === "grass") playSound("cloth1"); rebuildHotbar(); return; } }); } // src/render/draw-fire.js function drawFire(ctx2, wx, wy, now) { const baseX = wx; const baseY = wy; const flick = 6 + (Math.sin(now / 90) + 1) * 4; ctx2.fillStyle = "rgba(255,140,0,0.85)"; ctx2.beginPath(); ctx2.moveTo(baseX + 10, baseY + 30); ctx2.lineTo(baseX + 20, baseY + 30 - flick); ctx2.lineTo(baseX + 30, baseY + 30); ctx2.fill(); ctx2.fillStyle = "rgba(255,230,150,0.75)"; ctx2.beginPath(); ctx2.moveTo(baseX + 14, baseY + 30); ctx2.lineTo(baseX + 20, baseY + 30 - flick * 0.7); ctx2.lineTo(baseX + 26, baseY + 30); ctx2.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 = getBlock2(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 = getBlock2(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 = getBlock2(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 ctx2 = state.ctx; const W2 = state.W; const H2 = state.H; const camX = state.camX; const camY = state.camY; const TILE2 = state.TILE; const tex2 = state.tex; const night = isNight(); ctx2.fillStyle = night ? "#070816" : state.isRaining ? "#6B7B8D" : "#87CEEB"; ctx2.fillRect(0, 0, W2, H2); ctx2.save(); ctx2.translate(-camX * 0.5, -camY * 0.15); ctx2.fillStyle = "rgba(255,255,255,0.65)"; for (const c of clouds) { ctx2.fillRect(c.x, c.y, c.w, 26); ctx2.fillRect(c.x + 20, c.y - 10, c.w * 0.6, 22); } ctx2.restore(); ctx2.save(); ctx2.translate(-camX, -camY); const minGX = Math.floor(camX / TILE2) - 2; const maxGX = Math.floor((camX + W2) / TILE2) + 2; const minGY = Math.floor(camY / TILE2) - 6; const maxGY = Math.floor((camY + H2) / 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) { ctx2.save(); ctx2.globalAlpha = def.alpha; ctx2.drawImage(tex2[b.t], b.gx * TILE2, b.gy * TILE2, TILE2, TILE2); ctx2.restore(); } else { ctx2.drawImage(tex2[b.t], b.gx * TILE2, b.gy * TILE2, TILE2, TILE2); } if (b.t === "tnt" && b.active && Math.sin(now / 60) > 0) { ctx2.fillStyle = "rgba(255,255,255,0.45)"; ctx2.fillRect(b.gx * TILE2, b.gy * TILE2, TILE2, TILE2); } if (b.t === "campfire") { drawFire(ctx2, b.gx * TILE2, b.gy * TILE2, now); } if (b.t === "furnace" && state.activeFurnaces.has(`${b.gx},${b.gy}`)) { drawFire(ctx2, b.gx * TILE2 + 8, b.gy * TILE2 + 5, now); } } const allMobsRender = state.isMultiplayer ? Array.from(state.serverMobs.values()) : mobs; for (const m of allMobsRender) { if (m.kind === "zombie") { ctx2.fillStyle = "#2ecc71"; ctx2.fillRect(m.x, m.y, m.w, m.h); ctx2.fillStyle = "#c0392b"; ctx2.fillRect(m.x + 6, m.y + 12, 6, 6); ctx2.fillRect(m.x + 22, m.y + 12, 6, 6); } else if (m.kind === "pig") { ctx2.fillStyle = "#ffb6c1"; ctx2.fillRect(m.x, m.y, m.w, m.h); ctx2.fillStyle = "#000"; ctx2.fillRect(m.x + 22, m.y + 5, 3, 3); ctx2.fillStyle = "#ff69b4"; ctx2.fillRect(m.x + 28, m.y + 12, 6, 6); } else if (m.kind === "chicken") { ctx2.fillStyle = "#ecf0f1"; ctx2.fillRect(m.x, m.y, m.w, m.h); ctx2.fillStyle = "#f39c12"; ctx2.fillRect(m.x + 18, m.y + 10, 6, 4); ctx2.fillStyle = "#000"; ctx2.fillRect(m.x + 8, m.y + 6, 3, 3); } else if (m.kind === "creeper") { ctx2.fillStyle = "#4CAF50"; ctx2.fillRect(m.x, m.y, m.w, m.h); ctx2.fillStyle = "#000"; ctx2.fillRect(m.x + 8, m.y + 8, 4, 4); ctx2.fillRect(m.x + 22, m.y + 8, 4, 4); ctx2.fillStyle = "#000"; ctx2.fillRect(m.x + 12, m.y + 20, 10, 4); ctx2.fillStyle = "#4CAF50"; ctx2.fillRect(m.x + 4, m.y + 30, 6, 20); ctx2.fillRect(m.x + 24, m.y + 30, 6, 20); } else if (m.kind === "skeleton") { ctx2.fillStyle = "#ECEFF1"; ctx2.fillRect(m.x + 10, m.y + 20, 14, 12); ctx2.fillRect(m.x + 8, m.y + 0, 18, 18); ctx2.fillStyle = "#000"; ctx2.fillRect(m.x + 10, m.y + 6, 4, 4); ctx2.fillRect(m.x + 20, m.y + 6, 4, 4); ctx2.fillRect(m.x + 15, m.y + 12, 4, 2); ctx2.fillStyle = "#ECEFF1"; ctx2.fillRect(m.x + 2, m.y + 20, 6, 14); ctx2.fillRect(m.x + 26, m.y + 20, 6, 14); ctx2.fillRect(m.x + 10, m.y + 32, 6, 18); ctx2.fillRect(m.x + 18, m.y + 32, 6, 18); ctx2.save(); ctx2.translate(m.x + 30, m.y + 22); ctx2.strokeStyle = "#8B4513"; ctx2.lineWidth = 2; ctx2.beginPath(); ctx2.arc(0, 0, 8, -Math.PI * 0.7, Math.PI * 0.7); ctx2.stroke(); ctx2.strokeStyle = "#ccc"; ctx2.lineWidth = 1; ctx2.beginPath(); ctx2.moveTo(8 * Math.cos(-Math.PI * 0.7), 8 * Math.sin(-Math.PI * 0.7)); ctx2.lineTo(8 * Math.cos(Math.PI * 0.7), 8 * Math.sin(Math.PI * 0.7)); ctx2.stroke(); ctx2.restore(); } } if (boat.active) { ctx2.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) { ctx2.drawImage(state.heroImg, p.x - (TILE2 - player.w) / 2, p.y - (TILE2 - player.h) / 2, TILE2, TILE2); } else { ctx2.fillStyle = p.color; ctx2.fillRect(p.x, p.y, 34, 34); } ctx2.fillStyle = "#fff"; ctx2.font = "12px system-ui"; ctx2.textAlign = "center"; ctx2.fillText(p.name, p.x + 17, p.y - 8); } if (state.heroImg.complete) { ctx2.drawImage(state.heroImg, player.x - (TILE2 - player.w) / 2, player.y - (TILE2 - player.h) / 2, TILE2, TILE2); } else { ctx2.fillStyle = "#fff"; ctx2.fillRect(player.x, player.y, player.w, player.h); } for (const p of projectiles) { const angle = Math.atan2(p.vy, p.vx); ctx2.save(); ctx2.translate(p.x, p.y); ctx2.rotate(angle); ctx2.fillStyle = p.owner === "mob" ? "#c0392b" : "#f1c40f"; ctx2.fillRect(-12, -1.5, 24, 3); ctx2.beginPath(); ctx2.moveTo(12, -4); ctx2.lineTo(16, 0); ctx2.lineTo(12, 4); ctx2.closePath(); ctx2.fill(); ctx2.fillStyle = "#888"; ctx2.fillRect(-12, -3, 4, 2); ctx2.fillRect(-12, 1, 4, 2); ctx2.restore(); } for (const p of parts2) { ctx2.fillStyle = p.c; ctx2.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); ctx2.save(); ctx2.translate(arrowX, arrowY); ctx2.rotate(angle); ctx2.fillStyle = "#ECEFF1"; ctx2.fillRect(0, -1, 16, 2); ctx2.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); ctx2.strokeStyle = "rgba(255,255,255,0.9)"; ctx2.lineWidth = 2; ctx2.strokeRect(ggx * TILE2, ggy * TILE2, TILE2, TILE2); } ctx2.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 = getBlock2(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 = lightCtx2.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)"); lightCtx2.fillStyle = grad; lightCtx2.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) lightCtx2.moveTo(px, py); else lightCtx2.lineTo(px, py); } lightCtx2.closePath(); lightCtx2.fill(); }; const lightC2 = state.lightC; const lightCtx2 = state.lightCtx; lightC2.width = W2 * state.dpr; lightC2.height = H2 * state.dpr; lightCtx2.setTransform(state.dpr, 0, 0, state.dpr, 0, 0); lightCtx2.fillStyle = "rgba(0,0,12,0.82)"; lightCtx2.fillRect(0, 0, W2, H2); lightCtx2.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); } } lightCtx2.globalCompositeOperation = "source-over"; ctx2.drawImage(lightC2, 0, 0, W2, H2); ctx2.save(); ctx2.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 = ctx2.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)"); ctx2.fillStyle = grad; ctx2.beginPath(); ctx2.arc(wx, wy, r, 0, Math.PI * 2); ctx2.fill(); } } ctx2.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) { ctx2.fillStyle = "rgba(0,0,0,0.7)"; ctx2.fillRect(0, 0, W2, H2); ctx2.fillStyle = "#fff"; ctx2.font = "bold 32px system-ui"; ctx2.textAlign = "center"; ctx2.fillText("\u{1F4A4} \u0421\u043F\u0438\u043C...", W2 / 2, H2 / 2); ctx2.font = "18px system-ui"; ctx2.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", W2 / 2, H2 / 2 + 40); } if (state.minimapOpen && Math.random() < 0.25) { renderMinimap(); } requestAnimationFrame(loop); } // 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_SERVER = "https://voicegrech.mkn8n.ru"; var voiceBtn = document.createElement("div"); voiceBtn.innerHTML = '\u{1F3A4}/'; 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}/'; 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_SERVER, { transports: ["websocket"] }); voiceSocket.on("connect", () => { voiceSocket.emit("voice_join", { world_id: 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/main.js var urlParams2 = new URLSearchParams(window.location.search); state.SERVER_URL = urlParams2.get("server") || "https://apigrech.mkn8n.ru"; state.TELEGRAM_BOT_USERNAME = "Grechkacraft_bot"; state.TELEGRAM_APP_SHORT_NAME = "minegrechka"; if (location.protocol === "https:" && state.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."); } 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 worldParam2 = urlParams2.get("world"); console.log("world param:", worldParam2); state.worldId = worldParam2 && worldParam2.trim() !== "" ? worldParam2 : 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); state.gameEl = document.getElementById("game"); state.canvas = document.getElementById("c"); state.ctx = state.canvas.getContext("2d"); state.lightC = document.createElement("canvas"); state.lightCtx = state.lightC.getContext("2d"); state.dpr = Math.max(1, window.devicePixelRatio || 1); function resize() { state.W = state.gameEl.clientWidth; state.H = state.gameEl.clientHeight; state.canvas.width = state.W * state.dpr; state.canvas.height = state.H * state.dpr; state.lightC.width = state.W * state.dpr; state.lightC.height = state.H * state.dpr; state.ctx.setTransform(state.dpr, 0, 0, state.dpr, 0, 0); } window.addEventListener("resize", resize); resize(); state.otherPlayers = /* @__PURE__ */ new Map(); state.serverMobs = /* @__PURE__ */ new Map(); state.grid = /* @__PURE__ */ new Map(); state.blocks = []; state.generated = /* @__PURE__ */ new Set(); state.serverOverrides = /* @__PURE__ */ new Map(); state.activeFurnaces = /* @__PURE__ */ new Map(); state.activeTNT = /* @__PURE__ */ new Set(); state.toolDurability = /* @__PURE__ */ new Map(); state.worldSeed = Math.floor(Math.random() * 1e6); state.clouds = Array.from({ length: 10 }, () => ({ x: Math.random() * 2e3, y: -200 - Math.random() * 260, w: 80 + Math.random() * 120, s: 12 + Math.random() * 20 })); state.weatherTimer = 0; state.weatherChangeInterval = 60 + Math.random() * 120; state.player.x = 6 * state.TILE; state.player.y = 0; state.spawnPoint.x = 6 * state.TILE; state.spawnPoint.y = 0; state.heroImg = new Image(); state.heroImg.src = "https://supamg.mkn8n.ru/storage/v1/object/public/img//grechka.png"; 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"); initTextures(); 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"); initControls(); initMouseHandlers(); initChat(); initFurnace(); initMinimap(); initSaveControls(); 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 = getBlock2(startGX, surfaceY - 1); if (aboveBlock && aboveBlock.t === "water") { for (let gy = state.SEA_GY - 1; gy >= 0; gy--) { const b = getBlock2(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); })();