420 lines
15 KiB
JavaScript
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
|
|
});
|
|
} |