grechka-game/src3d/input/mouse-handler.js

420 lines
15 KiB
JavaScript

// ==================== ВЗАИМОДЕЙСТВИЕ МЫШЬ/ТАП ====================
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
});
}