// ==================== ВЗАИМОДЕЙСТВИЕ МЫШЬ/ТАП ==================== import { state } from '../core/state.js'; import { BLOCKS } from '../data/blocks.js'; import { ITEMS } from '../data/items.js'; import { TOOLS } from '../data/tools.js'; import { getBestTool, useTool, toolTierLessThan, getMiningSpeed } from '../data/tools.js'; import { getBlock, setBlock, removeBlock } from '../world/world-storage.js'; import { playSound } from '../audio/sound-engine.js'; import { rebuildHotbar, addToRecentItems } from '../ui/hotbar.js'; import { mode } from '../game/modes.js'; import { openFurnaceUI } from '../ui/furnace.js'; import { saveGame } from '../game/save.js'; import { activateTNT } from '../world/tnt.js'; import { sendBlockChange } from '../multiplayer/socket-helpers.js'; import { isNight } from '../entities/mob-ai.js'; // Crop stage to seed/harvest mapping const CROP_SEED_MAP = { wheat_stage0: 'wheat_seeds', wheat_stage1: 'wheat_seeds', wheat_stage2: 'wheat_seeds', wheat_stage3: 'wheat_seeds', carrot_stage0: 'carrot', carrot_stage1: 'carrot', carrot_stage2: 'carrot', carrot_stage3: 'carrot', potato_stage0: 'potato', potato_stage1: 'potato', potato_stage2: 'potato', potato_stage3: 'potato' }; const CROP_HARVEST_MAP = { wheat_stage3: 'wheat', carrot_stage3: 'carrot', potato_stage3: 'potato' }; // Helper: handle block drops including farming items function handleBlockDrop(removed, inv) { // Grass drops wheat_seeds (50%) if (removed.t === 'grass') { if (Math.random() < 0.5) inv.wheat_seeds = (inv.wheat_seeds || 0) + 1; return; } // Crop drops: seeds for stages 0-2, harvest+seed for stage 3 const harvestItem = CROP_HARVEST_MAP[removed.t]; const seedItem = CROP_SEED_MAP[removed.t]; if (harvestItem) { inv[harvestItem] = (inv[harvestItem] || 0) + (BLOCKS[removed.t].harvestQty || 1); addToRecentItems(harvestItem); if (seedItem) { inv[seedItem] = (inv[seedItem] || 0) + 1; addToRecentItems(seedItem); } } else if (seedItem) { // Early crop stage — return seed inv[seedItem] = (inv[seedItem] || 0) + 1; addToRecentItems(seedItem); } else { inv[removed.t] = (inv[removed.t] || 0) + 1; addToRecentItems(removed.t); } } export const mouse = { x: null, y: null }; // Hold-to-mine tracking let miningHeld = false; let miningInterval = null; export function initMouseHandlers() { const canvas = state.canvas; canvas.addEventListener('pointermove', (e) => { const r = canvas.getBoundingClientRect(); mouse.x = e.clientX - r.left; mouse.y = e.clientY - r.top; }); export function handleTap(wx, wy) { if (state.craftOpen) return; if (state.player.hp <= 0) return; const gx = Math.floor(wx / state.TILE); const gy = Math.floor(wy / state.TILE); // Пробуждение: клик по любой кровати когда спишь const b = getBlock(gx, gy); if (state.player.sleeping && b && b.t === 'bed') { state.player.sleeping = false; return; } if (state.player.sleeping) return; // Нельзя взаимодействовать во время сна // Клик по печи — открываем панель обжига if (b && b.t === 'furnace' && mode() === 'mine') { openFurnaceUI(gx, gy); return; } // клик по мобу (в режиме mine) if (mode() === 'mine') { // Check server mobs first (multiplayer) 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; } } } // Local mobs (singleplayer or if not hit server mob) 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; } // жарка на костре: выбран meat + клик по campfire if (b && b.t === 'campfire' && state.selected === 'meat' && state.inv.meat > 0) { playSound('fire'); state.inv.meat--; state.inv.cooked++; rebuildHotbar(); return; } // Сон на кровати: клик по bed if (b && b.t === 'bed' && isNight()) { state.player.sleeping = true; saveGame(); return; } // ====== MINING PROGRESSION SYSTEM ====== if (mode() === 'mine') { if (!b) return; // Allow mining crops (growthStage) and harvestable blocks, block other decor and fluids if (BLOCKS[b.t].fluid || (BLOCKS[b.t].decor && BLOCKS[b.t].growthStage === undefined && !BLOCKS[b.t].harvestable)) return; if (b.t === 'tnt') { activateTNT(b, 3.2); return; } // Calculate mining progress const hardness = BLOCKS[b.t].hardness || 1; if (hardness === Infinity) return; // unbreakable (bedrock) if (hardness === 0) { // Instant-mine blocks (flower, boat, crops, etc.) const removed = removeBlock(gx, gy); if (removed) { handleBlockDrop(removed, state.inv); sendBlockChange(gx, gy, removed.t, 'remove'); rebuildHotbar(); } return; } // Same block being mined? if (state.mining.gx === gx && state.mining.gy === gy) { state.mining.progress += getMiningSpeed(b.t); } else { // New block, start mining state.mining = { gx, gy, progress: getMiningSpeed(b.t), hardness }; } // Play mining hit sound if (b.t === 'glass') playSound('glass1'); else if (b.t === 'sand') playSound('sand1'); else if (b.t === 'stone' || b.t.endsWith('_ore')) playSound('stone1'); else if (b.t === 'wood' || b.t === 'planks') playSound('wood1'); else playSound('cloth1'); // Check if block is fully mined if (state.mining.progress >= hardness) { // Check drop requirements const minTool = BLOCKS[b.t].minTool; let drops = true; if (minTool) { const toolId = getBestTool(b.t); if (toolId) { const tool = TOOLS[toolId]; if (toolTierLessThan(tool.toolTier, minTool)) drops = false; else useTool(toolId); } else { drops = false; } } else { // Use tool durability if applicable const toolId = getBestTool(b.t); if (toolId) useTool(toolId); } const removed = removeBlock(gx, gy); if (removed) { if (drops) { handleBlockDrop(removed, state.inv); } else { // Drop cobblestone instead for stone-type blocks if (BLOCKS[removed.t]?.toolType === 'pickaxe') { state.inv.cobblestone = (state.inv.cobblestone || 0) + 1; } // Else nothing drops } sendBlockChange(gx, gy, removed.t, 'remove'); // Sound for block break if (removed.t === 'glass') playSound('glass1'); else if (removed.t === 'sand') playSound('sand1'); else if (removed.t === 'stone' || removed.t.endsWith('_ore')) playSound('stone1'); else if (removed.t === 'wood') playSound('wood1'); else playSound('cloth1'); rebuildHotbar(); } state.mining = { gx: null, gy: null, progress: 0, hardness: 0 }; } // Start hold-to-mine for desktop miningHeld = true; if (miningInterval) clearInterval(miningInterval); miningInterval = setInterval(() => { if (!miningHeld) { clearInterval(miningInterval); miningInterval = null; return; } // Re-check the block still exists at mining position const mb = getBlock(state.mining.gx, state.mining.gy); if (!mb || mb.dead || mb.t !== b.t) { miningHeld = false; clearInterval(miningInterval); miningInterval = null; return; } // Accumulate progress if (state.mining.gx !== null && state.mining.hardness > 0) { state.mining.progress += getMiningSpeed(mb.t) * 0.3; // 0.3x per tick for held mining if (state.mining.progress >= state.mining.hardness) { const mgx = state.mining.gx; const mgy = state.mining.gy; const mMinTool = BLOCKS[mb.t].minTool; let mDrops = true; if (mMinTool) { const mToolId = getBestTool(mb.t); if (mToolId) { const mTool = TOOLS[mToolId]; if (toolTierLessThan(mTool.toolTier, mMinTool)) mDrops = false; else useTool(mToolId); } else { mDrops = false; } } else { const mToolId = getBestTool(mb.t); if (mToolId) useTool(mToolId); } const removed = removeBlock(mgx, mgy); if (removed) { if (mDrops) { handleBlockDrop(removed, state.inv); } else { if (BLOCKS[removed.t]?.toolType === 'pickaxe') { state.inv.cobblestone = (state.inv.cobblestone || 0) + 1; } } sendBlockChange(mgx, mgy, removed.t, 'remove'); rebuildHotbar(); } state.mining = { gx: null, gy: null, progress: 0, hardness: 0 }; miningHeld = false; clearInterval(miningInterval); miningInterval = null; } } }, 100); return; } // ====== BUILD MODE ====== if (mode() === 'build') { // Planting seeds on farmland (works even without farmland in inventory) const seedToCropBlock = { wheat_seeds: 'wheat_stage0', carrot: 'carrot_stage0', potato: 'potato_stage0' }; if (seedToCropBlock[state.selected] && state.inv[state.selected] > 0) { // Clicking on farmland — plant above it if (b && b.t === 'farmland') { const aboveKey = gx + ',' + (gy - 1); const above = state.grid.get(aboveKey); if (!above || above.dead) { setBlock(gx, gy - 1, seedToCropBlock[state.selected], true); state.inv[state.selected]--; const cropBlock = state.grid.get(gx + ',' + (gy - 1)); if (cropBlock) cropBlock.growthTimer = 0; rebuildHotbar(); return; } } // Clicking on empty space above farmland — plant there if (!b) { const belowKey = gx + ',' + (gy + 1); const below = state.grid.get(belowKey); if (below && below.t === 'farmland') { setBlock(gx, gy, seedToCropBlock[state.selected], true); state.inv[state.selected]--; const cropBlock = state.grid.get(gx + ',' + gy); if (cropBlock) cropBlock.growthTimer = 0; rebuildHotbar(); return; } } } if (state.inv[state.selected] <= 0) return; if (!BLOCKS[state.selected]) return; if (b) return; // занято // Проверяем, ставим ли лодку if (state.selected === 'boat') { const waterBelow = getBlock(gx, gy + 1); if (!waterBelow || waterBelow.t !== 'water') { return; } state.boat.x = gx * state.TILE; state.boat.y = gy * state.TILE; state.boat.vx = 0; state.boat.vy = 0; state.boat.active = true; state.boat.inWater = true; state.player.inBoat = true; state.player.x = state.boat.x; state.player.y = state.boat.y; state.player.vx = 0; state.player.vy = 0; playSound('splash'); state.inv[state.selected]--; rebuildHotbar(); return; } // запрет ставить в игрока const TILE = state.TILE; const bx = gx * TILE, by = gy * TILE; const overlap = !(bx >= state.player.x + state.player.w || bx + TILE <= state.player.x || by >= state.player.y + state.player.h || by + TILE <= state.player.y); if (overlap) return; setBlock(gx, gy, state.selected, true); state.inv[state.selected]--; sendBlockChange(gx, gy, state.selected, 'set'); if (state.selected === 'stone' || state.selected === 'brick') playSound('stone_build'); else if (state.selected === 'wood' || state.selected === 'planks') playSound('wood_build'); else if (state.selected === 'glass') playSound('glass1'); else if (state.selected === 'sand') playSound('sand1'); else if (state.selected === 'dirt' || state.selected === 'grass') playSound('cloth1'); rebuildHotbar(); return; } } export function stopMining() { miningHeld = false; if (miningInterval) { clearInterval(miningInterval); miningInterval = null; } } // Release mouse — stop hold-to-mine canvas.addEventListener('pointerup', () => { stopMining(); }); canvas.addEventListener('pointerleave', () => { stopMining(); // Reset mining when mouse leaves canvas }); }