From 980ba6a5416f37f627333d975896c9a1b7b83ce4 Mon Sep 17 00:00:00 2001 From: Mk Date: Tue, 26 May 2026 17:27:10 +0000 Subject: [PATCH] feat: biomes, weather, new mobs, crops, structures, level unlocks --- game.js | 911 +++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 764 insertions(+), 147 deletions(-) diff --git a/game.js b/game.js index f304d36..4b00664 100644 --- a/game.js +++ b/game.js @@ -141,7 +141,11 @@ function customConfirm(msg, onYes) { creeper: { w: 34, h: 50, hp: 4, speed: 60 + Math.random() * 30, hostile: true, fuse: 3.2, shootCooldown: 2 }, skeleton: { w: 34, h: 50, hp: 4, speed: 70 + Math.random() * 30, hostile: true, fuse: 0, shootCooldown: 0 }, pig: { w: 34, h: 24, hp: 2, speed: 40, hostile: false, fuse: 0, shootCooldown: 2 }, - chicken: { w: 26, h: 22, hp: 1, speed: 55, hostile: false, fuse: 0, shootCooldown: 2 } + chicken: { w: 26, h: 22, hp: 1, speed: 55, hostile: false, fuse: 0, shootCooldown: 2 }, + scorpion: { w: 26, h: 26, hp: 3, speed: 90, hostile: true, fuse: 0, shootCooldown: 0, biome:'desert' }, + polar_bear:{ w: 40, h: 34, hp: 8, speed: 50, hostile: false, fuse: 0, shootCooldown: 2, biome:'tundra' }, + slime: { w: 24, h: 24, hp: 2, speed: 30, hostile: true, fuse: 0, shootCooldown: 2, biome:'swamp' }, + eagle: { w: 30, h: 22, hp: 3, speed: 120, hostile: true, fuse: 0, shootCooldown: 0, biome:'mountains' } }; const props = kindProps[data.kind] || kindProps['pig']; // fallback return { @@ -654,7 +658,30 @@ function customConfirm(msg, onYes) { flower: { n:'Цветок', c:'#ff4757', solid:false, decor:true }, bed: { n:'Кровать', c:'#e91e63', solid:true, bed:true }, boat: { n:'Лодка', c:'#8B4513', solid:false }, - furnace: { n:'Печь', c:'#696969', solid:true, smelting:true } + furnace: { n:'Печь', c:'#696969', solid:true, smelting:true }, + // === BIOME BLOCKS === + snow: { n:'Снег', c:'#ecf0f1', solid:true }, + ice: { n:'Лёд', c:'#74b9ff', solid:true, slip:true }, + cactus: { n:'Кактус', c:'#27ae60', solid:true, hurt:true }, + mushroom: { n:'Гриб', c:'#e74c3c', solid:false, decor:true }, + moss: { n:'Мох', c:'#0a6640', solid:true }, + swamp_water:{ n:'Болотная вода', c:'rgba(100,120,40,0.6)', solid:false, fluid:true, poison:true }, + farmland: { n:'Грядка', c:'#8B6914', solid:true, farmable:true }, + dead_bush: { n:'Сухой куст', c:'#b2bec3', solid:false, decor:true }, + spruce_leaves:{ n:'Ель', c:'#0a6640', solid:true }, + // === CROP STAGES === + wheat_stage0:{ n:'Росток пшеницы', c:'#a8e6a0', solid:false, decor:true, growthStage:0, growthTime:30, growsTo:'wheat_stage1' }, + wheat_stage1:{ n:'Пшеница', c:'#7dcea0', solid:false, decor:true, growthStage:1, growthTime:30, growsTo:'wheat_stage2' }, + wheat_stage2:{ n:'Пшеница', c:'#b8d730', solid:false, decor:true, growthStage:2, growthTime:30, growsTo:'wheat_stage3' }, + wheat_stage3:{ n:'Пшеница', c:'#f1c40f', solid:false, decor:true, harvestable:true, harvestItem:'wheat', harvestQty:2 }, + carrot_stage0:{ n:'Росток моркови', c:'#a8e6a0', solid:false, decor:true, growthStage:0, growthTime:30, growsTo:'carrot_stage1' }, + carrot_stage1:{ n:'Морковь', c:'#f0c27a', solid:false, decor:true, growthStage:1, growthTime:30, growsTo:'carrot_stage2' }, + carrot_stage2:{ n:'Морковь', c:'#e8a040', solid:false, decor:true, growthStage:2, growthTime:30, growsTo:'carrot_stage3' }, + carrot_stage3:{ n:'Морковь', c:'#e67e22', solid:false, decor:true, harvestable:true, harvestItem:'carrot', harvestQty:3 }, + potato_stage0:{ n:'Росток картофеля', c:'#a8e6a0', solid:false, decor:true, growthStage:0, growthTime:30, growsTo:'potato_stage1' }, + potato_stage1:{ n:'Картофель', c:'#c8b888', solid:false, decor:true, growthStage:1, growthTime:30, growsTo:'potato_stage2' }, + potato_stage2:{ n:'Картофель', c:'#bfaa78', solid:false, decor:true, growthStage:2, growthTime:30, growsTo:'potato_stage3' }, + potato_stage3:{ n:'Картофель', c:'#dfe6e9', solid:false, decor:true, harvestable:true, harvestItem:'potato', harvestQty:2 } }; const ITEMS = { @@ -664,7 +691,21 @@ function customConfirm(msg, onYes) { chicken_meat: { n:'Курица', icon:'🍗', food:12, stack:64 }, feather: { n:'Перо', icon:'🪶', stack:64 }, bone: { n:'Кость', icon:'🦴', stack:64 }, - gunpowder: { n:'Порох', icon:'💥', stack:64 } + gunpowder: { n:'Порох', icon:'💥', stack:64 }, + // === FARMING ITEMS === + wheat: { n:'Пшеница', icon:'🌾', stack:64 }, + bread: { n:'Хлеб', icon:'🍞', food:30 }, + carrot: { n:'Морковь', icon:'🥕', food:8, stack:64 }, + potato: { n:'Картофель', icon:'🥔', stack:64 }, + baked_potato:{ n:'Печёная картошка', icon:'🥔', food:25 }, + mushroom_stew:{ n:'Грибной суп', icon:'🍲', food:40 }, + // === MOB DROP ITEMS === + scorpion_stinger:{ n:'Жало скорпиона', icon:'🔺', stack:64 }, + polar_fur: { n:'Шкура медведя', icon:'🧥', stack:64 }, + slime_ball: { n:'Слизь', icon:'🟢', stack:64 }, + eagle_feather:{ n:'Перо орла', icon:'🪶', stack:64 }, + // === NEW ARMOR & TOOLS === + gold_armor: { n:'Золотая броня', icon:'🛡️', stack:1, armor:0.65 } }; // Seed мира для детерминированной генерации @@ -680,13 +721,16 @@ function customConfirm(msg, onYes) { // Инструменты const TOOLS = { - wood_pickaxe: { n:'Деревянная кирка', icon:'⛏️', durability: 60, miningPower: 1, craft: { wood: 3, planks: 2 } }, - stone_pickaxe: { n:'Каменная кирка', icon:'⛏️', durability: 130, miningPower: 2, craft: { wood: 1, stone: 3 } }, - iron_pickaxe: { n:'Железная кирка', icon:'⛏️', durability: 250, miningPower: 3, craft: { wood: 1, iron_ore: 2 } }, - wood_sword: { n:'Деревянный меч', icon:'⚔️', durability: 40, damage: 5, craft: { wood: 2, planks: 1 } }, - stone_sword: { n:'Каменный меч', icon:'⚔️', durability: 100, damage: 8, craft: { wood: 1, stone: 2 } }, - iron_sword: { n:'Железный меч', icon:'⚔️', durability: 200, damage: 12, craft: { wood: 1, iron_ore: 1 } }, - bow: { n:'Лук', icon:'🏹', durability: 150, craft: { wood: 3, string: 0 } } + wood_pickaxe: { n:'Деревянная кирка', icon:'⛏️', durability: 60, miningPower: 1, craft: { wood: 3, planks: 2 }, requiredLevel: 1 }, + stone_pickaxe: { n:'Каменная кирка', icon:'⛏️', durability: 130, miningPower: 2, craft: { wood: 1, stone: 3 }, requiredLevel: 2 }, + iron_pickaxe: { n:'Железная кирка', icon:'⛏️', durability: 250, miningPower: 3, craft: { wood: 1, iron_ore: 2 }, requiredLevel: 3 }, + wood_sword: { n:'Деревянный меч', icon:'⚔️', durability: 40, damage: 5, craft: { wood: 2, planks: 1 }, requiredLevel: 1 }, + stone_sword: { n:'Каменный меч', icon:'⚔️', durability: 100, damage: 8, craft: { wood: 1, stone: 2 }, requiredLevel: 2 }, + iron_sword: { n:'Железный меч', icon:'⚔️', durability: 200, damage: 12, craft: { wood: 1, iron_ore: 1 }, requiredLevel: 3 }, + bow: { n:'Лук', icon:'🏹', durability: 150, craft: { wood: 3, planks: 2 }, requiredLevel: 4 }, + diamond_pickaxe: { n:'Алмазная кирка', icon:'⛏️', durability: 500, miningPower: 5, craft: { wood: 1, diamond_ore: 2 }, requiredLevel: 7 }, + diamond_sword: { n:'Алмазный меч', icon:'⚔️', durability: 400, damage: 18, craft: { wood: 1, diamond_ore: 2 }, requiredLevel: 7 }, + hoe: { n:'Мотыга', icon:'🔨', durability: 80, tillTo: 'farmland', craft: { wood: 2, planks: 1 }, requiredLevel: 1 } }; // Текстуры блоков (простые) @@ -773,6 +817,96 @@ function customConfirm(msg, onYes) { g.fillRect(4, 28, 24, 3); return c; } + // === BIOME BLOCK TEXTURES === + if (type === 'snow') { + g.fillStyle = '#ecf0f1'; + g.fillRect(0, 0, 32, 32); + g.fillStyle = '#dfe6e9'; + for (let i = 0; i < 4; i++) g.fillRect((Math.random()*26)|0, (Math.random()*26)|0, 5, 3); + return c; + } + if (type === 'ice') { + g.fillStyle = '#74b9ff'; + g.fillRect(0, 0, 32, 32); + g.strokeStyle = 'rgba(255,255,255,0.4)'; + g.beginPath(); g.moveTo(4,10); g.lineTo(20,16); g.lineTo(10,28); g.stroke(); + g.beginPath(); g.moveTo(18,4); g.lineTo(28,12); g.stroke(); + return c; + } + if (type === 'cactus') { + g.fillStyle = '#27ae60'; + g.fillRect(6, 2, 20, 28); + g.fillStyle = '#2ecc71'; + g.fillRect(2, 8, 6, 4); + g.fillRect(24, 14, 6, 4); + g.fillStyle = '#1e8449'; + g.fillRect(12, 0, 2, 30); + g.fillRect(18, 0, 2, 30); + return c; + } + if (type === 'mushroom') { + g.fillStyle = '#f5e6cc'; g.fillRect(14, 20, 4, 12); + g.fillStyle = '#e74c3c'; g.beginPath(); g.arc(16, 14, 10, Math.PI, 0); g.fill(); + g.fillStyle = '#fff'; g.fillRect(10, 10, 3, 3); g.fillRect(17, 8, 4, 3); + return c; + } + if (type === 'moss') { + g.fillStyle = '#0a6640'; + g.fillRect(0, 0, 32, 32); + g.fillStyle = '#1e8449'; + for (let i = 0; i < 6; i++) g.fillRect((Math.random()*26)|0, (Math.random()*26)|0, 4, 3); + return c; + } + if (type === 'swamp_water') { + g.fillStyle = 'rgba(100,120,40,0.6)'; + g.fillRect(0, 0, 32, 32); + g.fillStyle = 'rgba(80,100,20,0.3)'; + g.fillRect(0, 10, 32, 2); + g.fillRect(0, 22, 32, 2); + return c; + } + if (type === 'farmland') { + g.fillStyle = '#8B6914'; + g.fillRect(0, 0, 32, 32); + g.fillStyle = '#7a5c10'; + for (let i = 0; i < 4; i++) g.fillRect(0, 6+i*8, 32, 2); + g.fillStyle = '#6B4E0A'; + g.fillRect(8, 2, 2, 28); + g.fillRect(18, 2, 2, 28); + return c; + } + if (type === 'dead_bush') { + g.fillStyle = '#b2bec3'; + g.fillRect(12, 16, 2, 16); + g.fillRect(8, 14, 8, 2); + g.fillRect(16, 12, 8, 2); + g.fillRect(6, 18, 4, 2); + return c; + } + if (type === 'spruce_leaves') { + g.fillStyle = '#0a6640'; + g.fillRect(0, 0, 32, 32); + g.fillStyle = '#0d7a4d'; + for (let i = 0; i < 5; i++) g.fillRect((Math.random()*24)|0, (Math.random()*24)|0, 6, 4); + return c; + } + // CROP STAGES + if (type.startsWith('wheat_stage') || type.startsWith('carrot_stage') || type.startsWith('potato_stage')) { + const st = parseInt(type.charAt(type.length-1)); + const colors = type.startsWith('wheat') ? ['#a8e6a0','#7dcea0','#b8d730','#f1c40f'] : + type.startsWith('carrot') ? ['#a8e6a0','#f0c27a','#e8a040','#e67e22'] : + ['#a8e6a0','#c8b888','#bfaa78','#dfe6e9']; + g.fillStyle = '#5d4037'; + g.fillRect(15, 18, 2, 14); + if (st >= 1) { + g.fillStyle = colors[st]; + g.fillRect(10, 8 + (3-st)*3, 12, 10); + } else { + g.fillStyle = '#a8e6a0'; + g.fillRect(14, 22, 4, 4); + } + return c; + } g.fillStyle = t.c || '#000'; g.fillRect(0,0,32,32); @@ -965,10 +1099,16 @@ function customConfirm(msg, onYes) { 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, + iron_armor:0, gold_armor:0, bow:0, furnace:0, bed:0, boat:0, - iron_ingot:0, gold_ingot:0, copper_ingot:0 + iron_ingot:0, gold_ingot:0, copper_ingot:0, + diamond_pickaxe:0, diamond_sword:0, hoe:0, + wheat:0, bread:0, carrot:0, potato:0, baked_potato:0, + scorpion_stinger:0, polar_fur:0, slime_ball:0, eagle_feather:0, + snow:0, ice:0, cactus:0, mushroom:0, moss:0, farmland:0, + spruce_leaves:0, dead_bush:0, + wheat_stage0:0, carrot_stage0:0, potato_stage0:0 }; let selected = 'dirt'; @@ -1014,25 +1154,31 @@ function customConfirm(msg, onYes) { } const RECIPES = [ - { out:'planks', qty:4, cost:{ wood:1 } }, - { out:'ladder', qty:3, cost:{ planks:7 } }, - { out:'torch', qty:2, cost:{ coal:1, planks:1 } }, - { out:'glass', qty:1, cost:{ sand:3 } }, - { out:'brick', qty:1, cost:{ stone:2, clay:1 } }, - { out:'campfire', qty:1, cost:{ wood:1, coal:1 } }, - { out:'tnt', qty:1, cost:{ sand:2, coal:1 } }, - { out:'bed', qty:1, cost:{ wood: 3, planks: 3 } }, - { out:'boat', qty:1, cost:{ wood: 5 } }, - { out:'wood_pickaxe', qty:1, cost:{ wood: 3, planks: 2 } }, - { out:'stone_pickaxe', qty:1, cost:{ wood: 1, stone: 3 } }, - { out:'iron_pickaxe', qty:1, cost:{ wood: 1, iron_ore: 2 } }, - { out:'wood_sword', qty:1, cost:{ wood: 2, planks: 1 } }, - { out:'stone_sword', qty:1, cost:{ wood: 1, stone: 2 } }, - { out:'iron_sword', qty:1, cost:{ wood: 1, iron_ore: 1 } }, - { out:'iron_armor', qty:1, cost:{ iron_ore: 5 } }, - { out:'furnace', qty:1, cost:{ stone: 8 } }, - { out:'bow', qty:1, cost:{ wood: 3, planks: 2 } }, - { out:'arrow', qty:4, cost:{ stone: 1, wood: 1 } } + { out:'planks', qty:4, cost:{ wood:1 }, requiredLevel:1 }, + { out:'ladder', qty:3, cost:{ planks:7 }, requiredLevel:1 }, + { out:'torch', qty:2, cost:{ coal:1, planks:1 }, requiredLevel:1 }, + { out:'glass', qty:1, cost:{ sand:3 }, requiredLevel:1 }, + { out:'brick', qty:1, cost:{ stone:2, clay:1 }, requiredLevel:1 }, + { out:'campfire', qty:1, cost:{ wood:1, coal:1 }, requiredLevel:1 }, + { out:'tnt', qty:1, cost:{ sand:2, coal:1 }, requiredLevel:8 }, + { out:'bed', qty:1, cost:{ wood: 3, planks: 3 }, requiredLevel:1 }, + { out:'boat', qty:1, cost:{ wood: 5 }, requiredLevel:2 }, + { out:'wood_pickaxe', qty:1, cost:{ wood: 3, planks: 2 }, requiredLevel:1 }, + { out:'stone_pickaxe', qty:1, cost:{ wood: 1, stone: 3 }, requiredLevel:2 }, + { out:'iron_pickaxe', qty:1, cost:{ wood: 1, iron_ore: 2 }, requiredLevel:3 }, + { out:'wood_sword', qty:1, cost:{ wood: 2, planks: 1 }, requiredLevel:1 }, + { out:'stone_sword', qty:1, cost:{ wood: 1, stone: 2 }, requiredLevel:2 }, + { out:'iron_sword', qty:1, cost:{ wood: 1, iron_ore: 1 }, requiredLevel:3 }, + { out:'iron_armor', qty:1, cost:{ iron_ore: 5 }, requiredLevel:5 }, + { out:'gold_armor', qty:1, cost:{ gold_ore: 8 }, requiredLevel:6 }, + { out:'furnace', qty:1, cost:{ stone: 8 }, requiredLevel:3 }, + { out:'bow', qty:1, cost:{ wood: 3, planks: 2 }, requiredLevel:4 }, + { out:'arrow', qty:4, cost:{ stone: 1, wood: 1 }, requiredLevel:1 }, + // === NEW RECIPES === + { out:'bread', qty:1, cost:{ wheat: 3 }, requiredLevel:1 }, + { out:'diamond_pickaxe', qty:1, cost:{ wood: 1, diamond_ore: 2 }, requiredLevel:7 }, + { out:'diamond_sword', qty:1, cost:{ wood: 1, diamond_ore: 2 }, requiredLevel:7 }, + { out:'hoe', qty:1, cost:{ wood: 2, planks: 1 }, requiredLevel:1 } ]; // Рецепты печи (обжиг) @@ -1043,13 +1189,17 @@ function customConfirm(msg, onYes) { { 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 } // булыжник → камень + { in:'cobblestone', qty:1, out:'stone', outQty:1, time:2 }, // булыжник → камень + { in:'potato', qty:1, out:'baked_potato', outQty:1, time:2 } // картофель → печёная картошка ]; // Новые предметы от обжига ITEMS.iron_ingot = { n:'Железный слиток', icon:'🔩' }; ITEMS.gold_ingot = { n:'Золотой слиток', icon:'🪙' }; ITEMS.copper_ingot = { n:'Медный слиток', icon:'🟤' }; + ITEMS.diamond_pickaxe = { n:'Алмазная кирка', icon:'⛏️', durability:500, miningPower:5 }; + ITEMS.diamond_sword = { n:'Алмазный меч', icon:'⚔️', durability:400, damage:18 }; + ITEMS.hoe = { n:'Мотыга', icon:'🔨', durability:80, tillTo:'farmland' }; // Активные печи: Map ключа блока → { recipe, progress, totalTime } const activeFurnaces = new Map(); @@ -1089,7 +1239,13 @@ function customConfirm(msg, onYes) { 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' + flower: '#d03050', bed: '#c03060', ladder: '#a04000', boat: '#6b3410', + snow: '#ecf0f1', ice: '#74b9ff', cactus: '#27ae60', mushroom: '#e74c3c', + moss: '#0a6640', swamp_water: '#687828', farmland: '#8B6914', + dead_bush: '#b2bec3', spruce_leaves: '#0a6640', + wheat_stage0: '#a8e6a0', wheat_stage1: '#7dcea0', wheat_stage2: '#b8d730', wheat_stage3: '#f1c40f', + carrot_stage0: '#a8e6a0', carrot_stage1: '#f0c27a', carrot_stage2: '#e8a040', carrot_stage3: '#e67e22', + potato_stage0: '#a8e6a0', potato_stage1: '#c8b888', potato_stage2: '#bfaa78', potato_stage3: '#dfe6e9' }; function renderMinimap() { @@ -1645,6 +1801,17 @@ function customConfirm(msg, onYes) { playSound('click'); renderInventory(); } + if(id === 'gold_armor' && inv.gold_armor > 0) { + if(player.equippedArmor === 'gold_armor') { + player.equippedArmor = null; + player.armor = 0; + } else { + player.equippedArmor = 'gold_armor'; + player.armor = ITEMS['gold_armor'].armor; + } + playSound('click'); + renderInventory(); + } }; } @@ -1667,6 +1834,8 @@ function customConfirm(msg, onYes) { for(const r of RECIPES){ const row = document.createElement('div'); row.className='recipe'; + const reqLv = r.requiredLevel || 1; + const locked = player.level < reqLv; const icon = document.createElement('div'); icon.className='ricon'; // Иконка — блок, инструмент или предмет @@ -1690,20 +1859,23 @@ function customConfirm(msg, onYes) { const nm = document.createElement('div'); nm.className='rname'; const itemName = BLOCKS[r.out]?.n || TOOLS[r.out]?.n || ITEMS[r.out]?.n || r.out; - nm.textContent = `${itemName} x${r.qty}`; + nm.textContent = `${itemName} x${r.qty}` + (locked ? ` (Lv.${reqLv})` : ''); + if(locked) { nm.style.color = '#888'; nm.style.textDecoration = 'line-through'; } const cs = document.createElement('div'); cs.className='rcost'; cs.textContent = Object.keys(r.cost).map(x => { const cn = BLOCKS[x]?.n || TOOLS[x]?.n || ITEMS[x]?.n || x; return `${cn}: ${(inv[x]||0)}/${r.cost[x]}`; }).join(' '); + if(locked) cs.style.color = '#666'; info.appendChild(nm); info.appendChild(cs); const btn = document.createElement('button'); btn.className='rcraft'; btn.textContent='Создать'; - btn.disabled = !canCraft(r); + btn.disabled = !canCraft(r) || locked; + if(locked) btn.title = `Требуется уровень ${reqLv}`; btn.onclick = () => { - if(!canCraft(r)) return; + if(!canCraft(r) || locked) return; playSound('click'); for(const res in r.cost) inv[res]-=r.cost[res]; inv[r.out] = (inv[r.out]||0) + r.qty; @@ -1889,6 +2061,7 @@ function customConfirm(msg, onYes) { hunger: 100, o2: 100, invuln: 0, + slowTimer: 0, // яд скорпиона — замедление fallStartY: 0, lastStepTime: 0, sleeping: false, @@ -1919,13 +2092,17 @@ function customConfirm(msg, onYes) { pig: [{item:'meat',min:1,max:2,chance:1}], zombie: [{item:'meat',min:0,max:1,chance:0.5},{item:'iron_ingot',min:0,max:1,chance:0.3}], skeleton: [{item:'arrow',min:2,max:4,chance:1},{item:'bone',min:0,max:2,chance:0.6},{item:'bow',min:1,max:1,chance:0.15}], - creeper: [{item:'gunpowder',min:1,max:2,chance:1}] + creeper: [{item:'gunpowder',min:1,max:2,chance:1}], + scorpion: [{item:'scorpion_stinger',min:0,max:1,chance:0.4}], + polar_bear:[{item:'polar_fur',min:1,max:2,chance:0.8},{item:'meat',min:1,max:2,chance:1}], + slime: [{item:'slime_ball',min:1,max:2,chance:1}], + eagle: [{item:'eagle_feather',min:1,max:2,chance:0.7}] }; return table[kind] || []; } function getMobXP(kind) { - const xpTable = { chicken:3, pig:5, zombie:10, skeleton:12, creeper:15 }; + const xpTable = { chicken:3, pig:5, zombie:10, skeleton:12, creeper:15, scorpion:8, polar_bear:20, slime:5, eagle:15 }; return xpTable[kind] || 0; } @@ -1946,11 +2123,22 @@ function customConfirm(msg, onYes) { } } + const LEVEL_UNLOCKS = { + 2: 'Каменные инструменты, Лодка', + 3: 'Железные инструменты, Печь', + 4: 'Лук и стрелы', + 5: 'Железная броня', + 6: 'Золотая броня', + 7: 'Алмазные инструменты', + 8: 'TNT' + }; + function grantXP(amount) { player.xp += amount; while (player.xp >= xpForLevel(player.level + 1)) { player.level++; - levelUpPopup = { text: '⭐ Уровень ' + player.level + '!', timer: 180 }; + const unlock = LEVEL_UNLOCKS[player.level] || ''; + levelUpPopup = { text: '⭐ Уровень ' + player.level + '!' + (unlock ? ' ' + unlock : ''), timer: 240 }; } } @@ -2231,64 +2419,113 @@ function customConfirm(msg, onYes) { // Дождь let isRaining = false; let rainIntensity = 0; // 0..1 - let weatherTimer = 0; - let weatherChangeInterval = 60 + Math.random() * 120; // смена погоды 60-180с + const snowflakes = []; // снежинки для тундры + const MAX_SNOWFLAKES = 150; const raindrops = []; const MAX_RAINDROPS = 200; - function updateWeather(dt) { - weatherTimer += dt; - if (weatherTimer >= weatherChangeInterval) { - weatherTimer = 0; - weatherChangeInterval = 60 + Math.random() * 120; - // Выбираем новую погоду: 40% дождь днём, 25% дождь ночью, остальное ясно - const nightChance = isNight() ? 0.25 : 0.40; - isRaining = Math.random() < nightChance; + // ==================== РОСТ КУЛЬТУР ==================== + const growthTimers = {}; // ключ: "gx,gy" → { stage:0-3, growTimer:X } + + // Старая функция заменена — теперь погода через биомы (см. weatherState выше) + // updateWeather(dt) вызывается из основного цикла — биом-зависимая + + // Интеграция: определяем isRaining из weatherState для визуализации + function syncWeatherVisual() { + isRaining = (weatherState.type === 'rain' || weatherState.type === 'storm'); + if (weatherState.type === 'clear') { + rainIntensity *= 0.95; // плавное затухание + if (rainIntensity < 0.01) rainIntensity = 0; } - // Плавная интерполяция интенсивности - const target = isRaining ? (0.4 + Math.random() * 0.01) : 0; - rainIntensity += (target - rainIntensity) * dt * 0.5; - if (rainIntensity < 0.01) rainIntensity = 0; } function updateRain(dt) { - if (!isRaining || rainIntensity < 0.01) { + syncWeatherVisual(); + // Дождь + if ((weatherState.type === 'rain' || weatherState.type === 'storm') && rainIntensity < weatherState.intensity * 0.6) { + rainIntensity += dt * 0.3; + } else if (weatherState.type === 'clear' || weatherState.type === 'snow' || weatherState.type === 'fog') { + rainIntensity = Math.max(0, rainIntensity - dt * 0.5); + } + if (rainIntensity < 0.01) { raindrops.length = 0; - return; - } - // Спавн капель - const spawnRate = Math.floor(rainIntensity * 60 * dt); // ~60 капель/сек при интенсити=1 - for (let i = 0; i < spawnRate && raindrops.length < MAX_RAINDROPS; i++) { - raindrops.push({ - x: camX + Math.random() * W, - y: camY - 20, - vy: 400 + Math.random() * 200, - len: 8 + Math.random() * 12 - }); - } - // Обновление - for (let i = raindrops.length - 1; i >= 0; i--) { - const d = raindrops[i]; - d.y += d.vy * dt; - d.x -= 30 * dt; // лёгкий ветер - if (d.y > camY + H + 20) { - raindrops.splice(i, 1); + } else { + const spawnRate = Math.floor(rainIntensity * 80 * dt); + for (let i = 0; i < spawnRate && raindrops.length < MAX_RAINDROPS; i++) { + raindrops.push({ + x: camX + Math.random() * W, + y: camY - 20, + vy: 400 + Math.random() * 200, + len: 8 + Math.random() * 12 + }); } + for (let i = raindrops.length - 1; i >= 0; i--) { + const d = raindrops[i]; + d.y += d.vy * dt; + d.x -= 30 * dt; + if (d.y > camY + H + 20) raindrops.splice(i, 1); + } + } + // Снег + if (weatherState.type === 'snow') { + const spawnRate = Math.floor(weatherState.intensity * 30 * dt); + for (let i = 0; i < spawnRate && snowflakes.length < MAX_SNOWFLAKES; i++) { + snowflakes.push({ + x: camX + Math.random() * W, + y: camY - 10, + vy: 40 + Math.random() * 60, + vx: (Math.random() - 0.5) * 30, + size: 2 + Math.random() * 3 + }); + } + } + for (let i = snowflakes.length - 1; i >= 0; i--) { + const s = snowflakes[i]; + s.y += s.vy * dt; + s.x += s.vx * dt + Math.sin(s.y * 0.02) * 10 * dt; + if (s.y > camY + H + 20 || weatherState.type !== 'snow') snowflakes.splice(i, 1); } } function drawRain() { - if (raindrops.length === 0) return; - ctx.save(); - ctx.strokeStyle = 'rgba(174,194,224,0.5)'; - ctx.lineWidth = 1.5; - ctx.beginPath(); - for (const d of raindrops) { - ctx.moveTo(d.x, d.y); - ctx.lineTo(d.x - 3, d.y + d.len); + // Дождь + if (raindrops.length > 0) { + ctx.save(); + ctx.strokeStyle = (weatherState.type === 'storm') ? 'rgba(174,194,224,0.7)' : 'rgba(174,194,224,0.5)'; + ctx.lineWidth = 1.5; + ctx.beginPath(); + for (const d of raindrops) { + ctx.moveTo(d.x, d.y); + ctx.lineTo(d.x - 3, d.y + d.len); + } + ctx.stroke(); + ctx.restore(); + } + // Снег + if (snowflakes.length > 0) { + ctx.save(); + ctx.fillStyle = 'rgba(255,255,255,0.8)'; + for (const s of snowflakes) { + ctx.beginPath(); + ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2); + ctx.fill(); + } + ctx.restore(); + } + // Гроза — вспышка + if (weatherState.type === 'storm' && Math.random() < 0.003) { + ctx.save(); + ctx.fillStyle = 'rgba(255,255,255,0.3)'; + ctx.fillRect(camX, camY, W, H); + ctx.restore(); + } + // Туман — серый оверлей + if (weatherState.type === 'fog') { + ctx.save(); + ctx.fillStyle = 'rgba(200,200,200,0.4)'; + ctx.fillRect(camX, camY, W, H); + ctx.restore(); } - ctx.stroke(); - ctx.restore(); } // Частицы (взрыв) @@ -2681,7 +2918,7 @@ function customConfirm(msg, onYes) { if(ITEMS[selected] && inv[selected]>0){ const it = ITEMS[selected]; if(player.hp < 100 || player.hunger < 100){ - playSound('eat1'); // Звук употребления еды + playSound('eat1'); player.hunger = Math.min(100, player.hunger + it.food); player.hp = Math.min(100, player.hp + 15); inv[selected]--; @@ -2689,6 +2926,34 @@ function customConfirm(msg, onYes) { } return; } + + //Посадка семян на грядку + if(b && b.t === 'farmland' && mode()==='build'){ + const seedMap = { wheat: 'wheat_stage0', carrot: 'carrot_stage0', potato: 'potato_stage0' }; + if(seedMap[selected] && inv[selected] > 0){ + inv[selected]--; + const cropType = seedMap[selected]; + setBlock(gx, gy-1, cropType); + growthTimers[gx+','+(gy-1)] = { stage:0, growTimer: 10+Math.random()*5 }; + sendBlockChange(gx, gy-1, cropType, 'add'); + playSound('cloth1'); + rebuildHotbar(); + return; + } + } + + // Мотыга — превращает grass/dirt в farmland (в любом режиме) + if(selected === 'hoe' && inv.hoe > 0 && b){ + if(b.t === 'grass' || b.t === 'dirt'){ + setBlock(gx, gy, 'farmland'); + sendBlockChange(gx, gy, b.t, 'remove'); + sendBlockChange(gx, gy, 'farmland', 'add'); + useTool('hoe'); + playSound('cloth1'); + rebuildHotbar(); + return; + } + } // жарка на костре: выбран meat + клик по campfire if(b && b.t==='campfire' && selected==='meat' && inv.meat>0){ @@ -2707,7 +2972,19 @@ function customConfirm(msg, onYes) { if(mode()==='mine'){ if(!b) return; - if(BLOCKS[b.t].fluid || BLOCKS[b.t].decor) return; + if(BLOCKS[b.t].fluid) return; + // Клик на урожайную культуру — сбор + if(BLOCKS[b.t].harvestable){ + const hInfo = BLOCKS[b.t]; + inv[hInfo.harvestItem] = (inv[hInfo.harvestItem]||0) + hInfo.harvestQty; + removeBlock(gx, gy); + sendBlockChange(gx, gy, b.t, 'remove'); + delete growthTimers[gx+','+gy]; + playSound('cloth1'); + rebuildHotbar(); + return; + } + if(BLOCKS[b.t].decor) return; if(b.t==='tnt'){ activateTNT(b, 3.2); return; } // не взрывается сразу @@ -2799,86 +3076,279 @@ function customConfirm(msg, onYes) { } }); - // Генерация (по X, на всю глубину до bedrock) + // ==================== БИОМЫ ==================== + const BIOMES = { + plains: { name:'Равнина', surface:'grass', subsurface:'dirt', trees:true, flowers:true, treeChance:0.12 }, + desert: { name:'Пустыня', surface:'sand', subsurface:'sand', trees:false, flowers:false, treeChance:0 }, + tundra: { name:'Тундра', surface:'snow', subsurface:'dirt', trees:true, flowers:false, treeChance:0.06 }, + swamp: { name:'Болото', surface:'moss', subsurface:'dirt', trees:true, flowers:false, treeChance:0.10 }, + mountains:{ name:'Горы', surface:'stone', subsurface:'stone', trees:false, flowers:false, treeChance:0 } + }; + + function getBiome(gx) { + const temp = Math.sin(gx*0.003 + worldSeed*0.01)*0.5 + Math.sin(gx*0.007 + worldSeed*0.02)*0.3 + 0.5; + const humid = Math.sin(gx*0.004 + worldSeed*0.015 + 1000)*0.5 + Math.cos(gx*0.006 + worldSeed*0.02 + 2000)*0.3 + 0.5; + const mtVal = Math.sin(gx*0.001 + worldSeed*0.005)*0.5 + 0.5; + if (temp > 0.7) return 'desert'; + if (temp < 0.3) return 'tundra'; + if (humid > 0.7 && temp >= 0.3 && temp <= 0.7) return 'swamp'; + if (temp >= 0.5 && temp <= 0.7 && mtVal > 0.75) return 'mountains'; + return 'plains'; + } + + const biomeCache = {}; + function getCachedBiome(gx) { + const chunk = Math.floor(gx / 8); // cache per 8-tile chunk for smoother biomes + if (biomeCache[chunk] === undefined) biomeCache[chunk] = getBiome(chunk * 8); + return biomeCache[chunk]; + } + + // ==================== ПОГОДА ==================== + const weatherState = { type: 'clear', intensity: 0, timer: 0, duration: 180, nextChange: 120 + Math.random()*180 }; + const BIOME_WEATHER = { + plains: { clear:0.50, rain:0.30, storm:0.10, snow:0, fog:0.10 }, + desert: { clear:0.80, rain:0.05, storm:0, snow:0, fog:0.15 }, + tundra: { clear:0.20, rain:0, storm:0.10, snow:0.60, fog:0.10 }, + swamp: { clear:0.20, rain:0.30, storm:0.10, snow:0, fog:0.40 }, + mountains:{ clear:0.40, rain:0.30, storm:0.10, snow:0.10, fog:0.10 } + }; + + function updateWeather(dt) { + weatherState.timer += dt; + if (weatherState.timer >= weatherState.nextChange) { + weatherState.timer = 0; + weatherState.nextChange = 60 + Math.random() * 240; + const biome = getCachedBiome(Math.floor(player.x / TILE)); + const probs = BIOME_WEATHER[biome] || BIOME_WEATHER.plains; + const r = Math.random(); + let cum = 0; + if ((cum += probs.clear) > r) { weatherState.type = 'clear'; } + else if ((cum += probs.rain) > r) { weatherState.type = 'rain'; } + else if ((cum += probs.storm) > r) { weatherState.type = 'storm'; } + else if ((cum += probs.snow) > r) { weatherState.type = 'snow'; } + else { weatherState.type = 'fog'; } + weatherState.duration = 60 + Math.random() * 300; + } + // Intensity interpolation + const target = (weatherState.type === 'clear') ? 0 : 1; + weatherState.intensity += (target - weatherState.intensity) * dt * 0.5; + } + + function getWeatherSpeedMultiplier() { + if (weatherState.type === 'rain') return 0.85; + if (weatherState.type === 'snow') return 0.7; + if (weatherState.type === 'storm') return 0.85; + return 1; + } + + function isOutdoorLight(lx, ly) { + // Check if position is outdoors (no block above) + const aboveGy = Math.floor(ly / TILE) - 1; + const aboveGx = Math.floor(lx / TILE); + const above = getBlock(aboveGx, aboveGy); + return !above || !BLOCKS[above.t]?.solid; + } + + // ==================== СТРУКТУРЫ МИРА ==================== + function placeStructure(startGx, startGy, pattern) { + for (let dy = 0; dy < pattern.length; dy++) { + for (let dx = 0; dx < pattern[dy].length; dx++) { + const bt = pattern[dy][dx]; + if (bt && bt !== 'air') { + setBlock(startGx + dx, startGy + dy, bt); + } + } + } + } + + // Пирамида в пустыне + const PYRAMID_PATTERN = [ + ['sand','sand','sand','sand','sand','sand','sand'], + ['sand','stone','stone','stone','stone','stone','sand'], + ['sand','stone','air','air','air','stone','sand'], + ['sand','stone','air','air','air','stone','sand'], + ['sand','stone','air','air','air','stone','sand'], + ['sand','sand','stone','stone','stone','sand','sand'] + ]; + + // Дом в равнине + const HOUSE_PATTERN = [ + ['air','planks','planks','planks','planks','air'], + ['planks','air','air','air','air','planks'], + ['planks','air','torch','air','air','planks'], + ['planks','air','air','air','air','planks'], + ['planks','planks','air','planks','planks','planks'] + ]; + + // Хижина в болоте + const HUT_PATTERN = [ + ['air','wood','wood','wood','air'], + ['wood','air','air','air','wood'], + ['wood','air','torch','air','wood'], + ['moss','moss','air','moss','moss'] + ]; + + // ==================== ГЕНЕРАЦИЯ ==================== const generated = new Set(); // gx already generated - function surfaceGyAt(gx){ - // базовая поверхность выше уровня воды с вариациями + "горы" - // Используем seed для детерминированной генерации - // Увеличили амплитуду и добавили больше частот для разнообразия - const n1 = Math.sin(gx*0.025 + worldSeed*0.001)*8; // крупные горы - const n2 = Math.sin(gx*0.012 + worldSeed*0.002)*12; // средние горы - const n3 = Math.sin(gx*0.006 + worldSeed*0.003)*6; // мелкие холмы - const n4 = Math.sin(gx*0.045 + worldSeed*0.004)*4; // детали - const n5 = Math.cos(gx*0.018 + worldSeed*0.005)*5; // дополнительные вариации - const h = Math.floor(SEA_GY - 8 + n1 + n2 + n3 + n4 + n5); // чем меньше gy - тем выше + function surfaceGyAt(gx) { + const biome = getCachedBiome(gx); + // Base noise (same for all biomes) + const n1 = Math.sin(gx*0.025 + worldSeed*0.001)*8; + const n2 = Math.sin(gx*0.012 + worldSeed*0.002)*12; + const n3 = Math.sin(gx*0.006 + worldSeed*0.003)*6; + const n4 = Math.sin(gx*0.045 + worldSeed*0.004)*4; + const n5 = Math.cos(gx*0.018 + worldSeed*0.005)*5; + let h; + switch(biome) { + case 'desert': + h = Math.floor(SEA_GY - 4 + n3*0.3 + n4*0.5); // flatter, slightly higher + break; + case 'tundra': + h = Math.floor(SEA_GY - 6 + n2*0.5 + n3*0.4 + n5*0.3); // gentle rolling + break; + case 'swamp': + h = Math.floor(SEA_GY - 2 + n3*0.2 + n4*0.3); // very flat, near sea level + h = Math.max(h, SEA_GY - 3); // never too deep + break; + case 'mountains': + h = Math.floor(SEA_GY - 15 + n1*1.5 + n2*1.2 + n3); // tall peaks + break; + default: // plains + h = Math.floor(SEA_GY - 8 + n1 + n2 + n3 + n4 + n5); + } return h; } - - function genColumn(gx){ + + function genColumn(gx) { if(generated.has(gx)) return; generated.add(gx); - + const sgy = surfaceGyAt(gx); - - // вода (если поверхность ниже уровня моря => sgy > SEA_GY) - if(sgy > SEA_GY){ - for(let gy=SEA_GY; gy SEA_GY) { + // ниже уровня моря — заливаем водой + for(let gy = SEA_GY; gy < sgy; gy++) { + const blockType = (biome === 'swamp' && gy >= SEA_GY - 1) ? 'swamp_water' : 'water'; + setBlock(gx, gy, blockType); } - // пляж 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; + // поверхность + const b = BIOMES[biome]; + setBlock(gx, sgy, b.surface); + // болото: случайные лужи болотной воды + if(biome === 'swamp' && seededRandom(gx*3, sgy) < 0.15) { + setBlock(gx, sgy-1, 'swamp_water'); } - - 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'; - - // руды: чем глубже, тем интереснее + // тундра: лёд на воде рядом + if(biome === 'tundra' && sgy === SEA_GY && seededRandom(gx, SEA_GY-1) < 0.3) { + setBlock(gx, SEA_GY-1, 'ice'); + } + } + + // === Подповерхностные слои === + for(let gy = sgy+1; gy <= BEDROCK_GY; gy++) { + if(gy === BEDROCK_GY) { setBlock(gx,gy,'bedrock'); continue; } + + let t = BIOMES[biome].subsurface; + + // глубже — камень + if(gy > sgy + 3) t = 'stone'; + + // пустыня: sand глубже + if(biome === 'desert' && gy <= sgy + 6) t = 'sand'; + + // болото: глина ближе к поверхности + if(biome === 'swamp' && gy <= sgy + 2 && seededRandom(gx, gy) < 0.3) t = 'clay'; + + // горы: gravel + if(biome === 'mountains' && gy > sgy + 4 && seededRandom(gx, gy) < 0.12) t = 'gravel'; + + // общие руды const depth = gy - sgy; const r = seededRandom(gx, gy); - if(t==='stone'){ - if(r < 0.06) t='coal'; - else if(r < 0.10) 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'; + if(t === 'stone') { + if(r < 0.06) t = 'coal'; + else if(r < 0.10) 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); + + setBlock(gx, gy, t); } - - // Деревья и цветы (только на траве, и не в воде) + + // === Растительность и декор === + const b = BIOMES[biome]; const top = getBlock(gx, sgy); - if(top && top.t==='grass'){ - if(seededRandom(gx, sgy-1) < 0.10){ - setBlock(gx, sgy-1,'flower'); - } - if(seededRandom(gx, sgy-2) < 0.12){ - // простое дерево + + // цветы (только plain) + if(biome === 'plains' && top && top.t === 'grass' && seededRandom(gx, sgy-1) < 0.10) { + setBlock(gx, sgy-1, 'flower'); + } + + // деревья + if(b.trees && seededRandom(gx*7, sgy-2) < b.treeChance) { + if(biome === 'tundra') { + // ёлки (треугольные, 3-5 высоты) + const th = 3 + Math.floor(seededRandom(gx, sgy) * 3); + for(let i = 0; i < th; i++) setBlock(gx, sgy-1-i, 'wood'); + // крона (треугольник) + for(let row = 0; row < th; row++) { + const w = Math.min(row + 1, 2); + for(let dx = -w; dx <= w; dx++) { + const ly = sgy-1-th+row; + if(ly >= 0) setBlock(gx+dx, ly, 'spruce_leaves'); + } + } + } else if(biome === 'swamp') { + // болотное дерево (короче, с мхом) + setBlock(gx, sgy-1, 'wood'); + setBlock(gx, sgy-2, 'moss'); + setBlock(gx-1, sgy-2, 'leaves'); + setBlock(gx+1, sgy-2, 'leaves'); + } else { + // обычное дерево (plains) 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'); + setBlock(gx-1, sgy-3, 'leaves'); + setBlock(gx+1, sgy-3, 'leaves'); } } + // кактусы (пустыня) + if(biome === 'desert' && seededRandom(gx*11, sgy) < 0.04) { + const ch = 1 + Math.floor(seededRandom(gx, sgy+1) * 2); + for(let i = 0; i < ch; i++) setBlock(gx, sgy-1-i, 'cactus'); + } + + // грибы (болото) + if(biome === 'swamp' && seededRandom(gx*13, sgy) < 0.06) { + setBlock(gx, sgy-1, 'mushroom'); + } + + // сухие кусты (пустыня) + if(biome === 'desert' && seededRandom(gx*17, sgy) < 0.05) { + setBlock(gx, sgy-1, 'dead_bush'); + } + + // === Структуры мира === + // Пирамида в пустыне + if(biome === 'desert' && ((gx % 200 + 200) % 200) === 47 && sgy < SEA_GY && sgy > SEA_GY - 5) { + placeStructure(gx, sgy - 5, PYRAMID_PATTERN); + } + // Дом в равнине + if(biome === 'plains' && ((gx % 150 + 150) % 150) === 33 && sgy < SEA_GY) { + placeStructure(gx, sgy - 4, HOUSE_PATTERN); + } + // Хижина в болоте + if(biome === 'swamp' && ((gx % 180 + 180) % 180) === 55 && sgy < SEA_GY) { + placeStructure(gx, sgy - 3, HUT_PATTERN); + } + // Применяем серверные оверрайды для этой колонны const colPrefix = gx + ','; for (const [key, ov] of serverOverrides) { @@ -3012,8 +3482,76 @@ function customConfirm(msg, onYes) { life: 3 }); } + } else if(m.kind==='scorpion') { + // Скорпион — бежит к игроку, яд (замедление) + const dir = Math.sign((player.x) - m.x); + m.vx = dir * m.speed; + if(m.inWater && Math.random()<0.06) m.vy = -260; + // Яд при касании — замедление на 3 сек + if(Math.abs((m.x+ m.w/2) - (player.x+player.w/2)) < 28 && + Math.abs((m.y+ m.h/2) - (player.y+player.h/2)) < 40 && + player.invuln <= 0){ + const damage = calculateDamage(8); + player.hp -= damage; + player.invuln = 0.8; + player.slowTimer = 3; // замедление + player.vx += dir*300; + player.vy -= 200; + playSound('hit1'); + } + } else if(m.kind==='polar_bear') { + // Белый медведь — нейтрален, атакует если ударили (hostile пока нет, атакует через proximity) + m.aiT -= dt; + if(m.aiT <= 0){ + m.aiT = 2.0 + Math.random()*3; + m.dir = Math.random()<0.5 ? -1 : 1; + if(Math.random()<0.3) m.dir = 0; + } + m.vx = m.dir * m.speed; + if(m.inWater) m.vy = -120; + } else if(m.kind==='slime') { + // Слизь — прыгает к игроку + const dir = Math.sign((player.x+player.w/2) - (m.x+m.w/2)); + m.aiT -= dt; + if(m.aiT <= 0){ + m.aiT = 1.5 + Math.random()*1.5; + m.dir = dir; + // Прыжок + m.vy = -200; + } + m.vx = m.dir * m.speed; + } else if(m.kind==='eagle') { + // Орёл — летает, атакует пикированием + const dx = (player.x+player.w/2) - (m.x+m.w/2); + const dy = (player.y+player.h/2) - (m.y+m.h/2); + const dist = Math.hypot(dx, dy); + if(dist < 400) { + // Пикирует на игрока + m.vx = Math.sign(dx) * m.speed; + m.vy = dy > 0 ? 60 : -60; // летит вниз к игроку + } else { + // Патрулирует + m.aiT -= dt; + if(m.aiT <= 0){ + m.aiT = 2+Math.random()*3; + m.dir = Math.random()<0.5 ? -1 : 1; + } + m.vx = m.dir * m.speed * 0.5; + m.vy = Math.sin(performance.now()/1000) * 30; // мягкое покачивание + } + // Атака при касании + if(dist < 30 && player.invuln <= 0){ + const damage = calculateDamage(10); + player.hp -= damage; + player.invuln = 0.8; + player.vy -= 200; + playSound('hit1'); + } + // Орёл не падает — летающий моб + m.vy *= 0.5; + m.grounded = false; } else { - // животные + // животные (pig, chicken) m.aiT -= dt; if(m.aiT <= 0){ m.aiT = 1.8 + Math.random()*2.5; @@ -3229,7 +3767,10 @@ function customConfirm(msg, onYes) { c.x -= c.s * dt; if(c.x + c.w < camX - 400) c.x = camX + W + Math.random()*700; } - + + // Погода (биом-зависимая) + updateWeather(dt); + // player updateWaterFlag(player); @@ -3253,7 +3794,8 @@ function customConfirm(msg, onYes) { player.vy = 0; } else { const dir = (inp.r?1:0) - (inp.l?1:0); - if(dir) player.vx = dir*MOVE; + const speedMult = getWeatherSpeedMultiplier() * (player.slowTimer > 0 ? 0.4 : 1.0); + if(dir) player.vx = dir * MOVE * speedMult; else player.vx *= 0.82; } @@ -3378,6 +3920,32 @@ function customConfirm(msg, onYes) { updateRain(dt); player.invuln = Math.max(0, player.invuln - dt); + if(player.slowTimer > 0) player.slowTimer = Math.max(0, player.slowTimer - dt); + + // Рост культур + for(const key of Object.keys(growthTimers)){ + const tile = growthTimers[key]; + if(tile.stage < 3){ + tile.growTimer -= dt; + if(tile.growTimer <= 0){ + tile.stage++; + tile.growTimer = 8 + Math.random()*6; + // Обновляем визуальный блок + const [gxStr, gyStr] = key.split(','); + const gx = parseInt(gxStr), gy = parseInt(gyStr); + const curBlock = getBlock(gx, gy); + if(curBlock && curBlock.t !== curBlock.t.replace(/_stage\d/, '_stage'+tile.stage)){ + // Вычисляем следующую стадию + const baseType = curBlock.t.replace(/_stage\d/, ''); + const nextType = baseType + '_stage' + tile.stage; + setBlock(gx, gy, nextType); + sendBlockChange(gx, gy, nextType, 'add'); + // Проверяем созрел ли + if(tile.stage >= 3) delete growthTimers[key]; + } + } + } + } // Voice position update voicePosT += dt; @@ -3694,6 +4262,55 @@ function customConfirm(msg, onYes) { ctx.lineTo(8*Math.cos(Math.PI*0.7), 8*Math.sin(Math.PI*0.7)); ctx.stroke(); ctx.restore(); + } else if(m.kind==='scorpion') { + // скорпион — оранжево-коричневый + ctx.fillStyle = '#d35400'; + ctx.fillRect(m.x+2, m.y+4, m.w-4, m.h-8); + ctx.fillStyle = '#c0392b'; + ctx.fillRect(m.x+m.w-4, m.y-6, 4, 10); + ctx.fillRect(m.x+m.w-2, m.y-10, 3, 5); + ctx.fillStyle = '#000'; + ctx.fillRect(m.x+4, m.y+6, 3, 3); + ctx.fillRect(m.x+m.w-7, m.y+6, 3, 3); + ctx.fillStyle = '#d35400'; + ctx.fillRect(m.x-4, m.y+8, 6, 4); + ctx.fillRect(m.x+m.w-2, m.y+8, 6, 4); + } else if(m.kind==='polar_bear') { + ctx.fillStyle = '#ecf0f1'; + ctx.fillRect(m.x+2, m.y+4, m.w-4, m.h-4); + ctx.fillRect(m.x+8, m.y-2, m.w-16, 10); + ctx.fillStyle = '#bdc3c7'; + ctx.fillRect(m.x+m.w/2-3, m.y+2, 6, 4); + ctx.fillStyle = '#000'; + ctx.fillRect(m.x+12, m.y+1, 3, 3); + ctx.fillRect(m.x+m.w-14, m.y+1, 3, 3); + ctx.fillStyle = '#ecf0f1'; + ctx.fillRect(m.x+8, m.y-4, 4, 4); + ctx.fillRect(m.x+m.w-12, m.y-4, 4, 4); + } else if(m.kind==='slime') { + const bounce = Math.sin(performance.now()/200)*3; + ctx.fillStyle = 'rgba(46,204,113,0.8)'; + ctx.fillRect(m.x-1, m.y-1+bounce, m.w+2, m.h+2); + ctx.fillStyle = 'rgba(39,174,96,0.9)'; + ctx.fillRect(m.x+1, m.y+1+bounce, m.w-2, m.h-2); + ctx.fillStyle = '#fff'; + ctx.fillRect(m.x+4, m.y+6+bounce, 6, 6); + ctx.fillRect(m.x+m.w-10, m.y+6+bounce, 6, 6); + ctx.fillStyle = '#000'; + ctx.fillRect(m.x+6, m.y+8+bounce, 3, 3); + ctx.fillRect(m.x+m.w-8, m.y+8+bounce, 3, 3); + } else if(m.kind==='eagle') { + ctx.fillStyle = '#8B4513'; + ctx.fillRect(m.x+8, m.y+6, m.w-16, m.h-10); + const wingY = Math.sin(performance.now()/150)*4; + ctx.fillRect(m.x-6, m.y+4+wingY, 16, 6); + ctx.fillRect(m.x+m.w-10, m.y+4-wingY, 16, 6); + ctx.fillStyle = '#fff'; + ctx.fillRect(m.x+m.w/2-3, m.y+2, 6, 6); + ctx.fillStyle = '#f39c12'; + ctx.fillRect(m.x+m.w/2-1, m.y+6, 4, 3); + ctx.fillStyle = '#000'; + ctx.fillRect(m.x+m.w/2-1, m.y+3, 2, 2); } }