FAZA 17: 2.5D Minecraft-Style Terrain + Y-Layer Stacking + Custom Sprites
COMPLETED FEATURES: Custom Sprite Integration: - Player, Zombie, Merchant sprites (0.2 scale) - 11 custom sprites + 5 asset packs loaded - Auto-transparency processing (white/brown removal) - Gravestone system with atlas extraction 2.5D Minecraft-Style Terrain: - Volumetric blocks with 25px thickness - Strong left/right side shading (30%/50% darker) - Minecraft-style texture patterns (grass, dirt, stone) - Crisp black outlines for definition Y-Layer Stacking System: - GRASS_FULL: All green (elevation > 0.7) - GRASS_TOP: Green top + brown sides (elevation 0.4-0.7) - DIRT: All brown (elevation < 0.4) - Dynamic terrain depth based on height Floating Island World Edge: - Stone cliff walls at map borders - 2-tile transition zone - Elevation flattening for cliff drop-off effect - 100x100 world with defined boundaries Performance & Polish: - Canvas renderer for pixel-perfect sharpness - CSS image-rendering: crisp-edges - willReadFrequently optimization - No Canvas2D warnings Technical: - 3D volumetric trees and rocks - Hybrid rendering (2.5D terrain + 2D characters) - Procedural texture generation - Y-layer aware terrain type selection
This commit is contained in:
251
src/systems/InteractionSystem.js
Normal file
251
src/systems/InteractionSystem.js
Normal file
@@ -0,0 +1,251 @@
|
||||
class InteractionSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
this.iso = new IsometricUtils(48, 24);
|
||||
|
||||
// Input listener setup (only once)
|
||||
this.scene.input.on('pointerdown', (pointer) => {
|
||||
if (pointer.button === 0) { // Left Click
|
||||
this.handleLeftClick(pointer);
|
||||
}
|
||||
});
|
||||
|
||||
// Loot Array
|
||||
this.drops = [];
|
||||
}
|
||||
|
||||
handleLeftClick(pointer) {
|
||||
if (!this.scene.player) return;
|
||||
|
||||
// 1. Account for camera and offset
|
||||
const worldX = pointer.worldX - this.scene.terrainOffsetX;
|
||||
const worldY = pointer.worldY - this.scene.terrainOffsetY;
|
||||
|
||||
// 2. Convert to Grid
|
||||
const gridPos = this.iso.toGrid(worldX, worldY);
|
||||
|
||||
// 3. Check distance
|
||||
const playerPos = this.scene.player.getPosition();
|
||||
const dist = Phaser.Math.Distance.Between(playerPos.x, playerPos.y, gridPos.x, gridPos.y);
|
||||
|
||||
// Allow interaction within radius of 2.5 tiles
|
||||
if (dist > 2.5) {
|
||||
console.log('Too far:', dist.toFixed(1));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`☝️ Clicked tile: ${gridPos.x},${gridPos.y}`);
|
||||
|
||||
// DETERMINE TOOL / ACTION
|
||||
let activeTool = null;
|
||||
const uiScene = this.scene.scene.get('UIScene');
|
||||
const invSys = this.scene.inventorySystem;
|
||||
|
||||
if (uiScene && invSys) {
|
||||
const selectedIdx = uiScene.selectedSlot;
|
||||
const slotData = invSys.slots[selectedIdx];
|
||||
if (slotData) activeTool = slotData.type;
|
||||
}
|
||||
|
||||
// 0. Build Mode Override
|
||||
if (this.scene.buildingSystem && this.scene.buildingSystem.isBuildMode) {
|
||||
this.scene.buildingSystem.tryBuild(gridPos.x, gridPos.y);
|
||||
return; // Consume click
|
||||
}
|
||||
|
||||
// 3.5 Check for NPC Click
|
||||
if (this.scene.npcs) {
|
||||
for (const npc of this.scene.npcs) {
|
||||
if (Math.abs(npc.gridX - gridPos.x) < 2.5 && Math.abs(npc.gridY - gridPos.y) < 2.5) {
|
||||
console.log(`🗣️ Interact with NPC: ${npc.type}`);
|
||||
|
||||
if (npc.type === 'zombie') {
|
||||
// Taming Logic
|
||||
npc.toggleState();
|
||||
return; // Done
|
||||
}
|
||||
|
||||
if (npc.type === 'merchant') {
|
||||
// Open Trade Menu
|
||||
if (uiScene && invSys) {
|
||||
uiScene.showTradeMenu(invSys);
|
||||
}
|
||||
return; // Stop processing
|
||||
}
|
||||
return; // Stop processing other clicks (farming/terrain) if clicked NPC
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Try Farming Action (Tilling, Planting, Harvesting)
|
||||
if (this.scene.farmingSystem) {
|
||||
const didFarm = this.scene.farmingSystem.interact(gridPos.x, gridPos.y, activeTool);
|
||||
if (didFarm) {
|
||||
// Animation?
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Try damage decoration (fallback)
|
||||
// 5. Try damage or interact decoration
|
||||
if (this.scene.terrainSystem) {
|
||||
const id = `${gridPos.x},${gridPos.y}`;
|
||||
if (this.scene.terrainSystem.decorationsMap.has(id)) {
|
||||
const decor = this.scene.terrainSystem.decorationsMap.get(id);
|
||||
|
||||
|
||||
// Ruin Interaction - Town Restoration
|
||||
if (decor.type === 'ruin' || decor.type === 'ruin_borut') {
|
||||
// Check if near
|
||||
if (dist > 2.5) {
|
||||
console.log('Ruin too far.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show Project Menu
|
||||
const uiScene = this.scene.scene.get('UIScene');
|
||||
if (uiScene) {
|
||||
// Define requirements based on ruin type
|
||||
let req = { reqWood: 100, reqStone: 50, reqGold: 50 };
|
||||
let ruinName = "Borut's Smithy"; // Default
|
||||
let npcType = 'merchant';
|
||||
|
||||
if (decor.type === 'ruin') {
|
||||
ruinName = "Merchant House";
|
||||
req = { reqWood: 50, reqStone: 30, reqGold: 30 };
|
||||
}
|
||||
|
||||
uiScene.showProjectMenu(req, () => {
|
||||
// On Contribute Logic
|
||||
if (invSys) {
|
||||
const hasWood = invSys.hasItem('wood', req.reqWood);
|
||||
const hasStone = invSys.hasItem('stone', req.reqStone || 0);
|
||||
const hasGold = invSys.hasItem('gold', req.reqGold || 0);
|
||||
|
||||
// Check all requirements
|
||||
if (hasWood && hasStone && hasGold) {
|
||||
// Consume materials
|
||||
invSys.removeItem('wood', req.reqWood);
|
||||
invSys.removeItem('stone', req.reqStone);
|
||||
invSys.removeItem('gold', req.reqGold);
|
||||
invSys.updateUI();
|
||||
|
||||
console.log(`🏗️ Restoring ${ruinName}...`);
|
||||
|
||||
// Transform Ruin -> House
|
||||
this.scene.terrainSystem.removeDecoration(gridPos.x, gridPos.y);
|
||||
this.scene.terrainSystem.placeStructure(gridPos.x, gridPos.y, 'house');
|
||||
|
||||
// Spawn NPC nearby
|
||||
const npc = new NPC(this.scene, gridPos.x + 1, gridPos.y + 1,
|
||||
this.scene.terrainOffsetX, this.scene.terrainOffsetY, npcType);
|
||||
this.scene.npcs.push(npc);
|
||||
|
||||
// Increase friendship (hearts)
|
||||
if (this.scene.statsSystem) {
|
||||
this.scene.statsSystem.addFriendship(npcType, 10); // +10 hearts
|
||||
}
|
||||
|
||||
console.log(`✅ ${ruinName} Restored! +10 ❤️ Friendship`);
|
||||
|
||||
// Play build sound
|
||||
if (this.scene.soundManager) this.scene.soundManager.playBuild();
|
||||
} else {
|
||||
// Not enough materials
|
||||
const missing = [];
|
||||
if (!hasWood) missing.push(`${req.reqWood} Wood`);
|
||||
if (!hasStone) missing.push(`${req.reqStone} Stone`);
|
||||
if (!hasGold) missing.push(`${req.reqGold} Gold`);
|
||||
|
||||
console.log(`❌ Not enough materials! Need: ${missing.join(', ')}`);
|
||||
alert(`Potrebuješ še: ${missing.join(', ')} da obnoviš ${ruinName}.`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return; // Don't damage it
|
||||
}
|
||||
}
|
||||
|
||||
const result = this.scene.terrainSystem.damageDecoration(gridPos.x, gridPos.y, 1);
|
||||
|
||||
if (result === 'destroyed') {
|
||||
// Play chop sound
|
||||
if (this.scene.soundManager) this.scene.soundManager.playChop();
|
||||
|
||||
// Spawn loot
|
||||
this.spawnLoot(gridPos.x, gridPos.y, 'wood');
|
||||
} else if (result === 'hit') {
|
||||
// Play hit sound
|
||||
if (this.scene.soundManager) this.scene.soundManager.playChop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spawnLoot(gridX, gridY, type) {
|
||||
console.log(`🎁 Spawning ${type} at ${gridX},${gridY}`);
|
||||
|
||||
// Convert to Screen
|
||||
const screenPos = this.iso.toScreen(gridX, gridY);
|
||||
const x = screenPos.x + this.scene.terrainOffsetX;
|
||||
const y = screenPos.y + this.scene.terrainOffsetY;
|
||||
|
||||
// Create simplistic item drop sprite
|
||||
let symbol = '?';
|
||||
if (type === 'wood') symbol = '🪵';
|
||||
if (type === 'seeds') symbol = '🌱';
|
||||
if (type === 'wheat') symbol = '🌾';
|
||||
if (type === 'hoe') symbol = '🛠️';
|
||||
|
||||
const drop = this.scene.add.text(x, y - 20, symbol, { fontSize: '20px' });
|
||||
drop.setOrigin(0.5);
|
||||
drop.setDepth(this.iso.getDepth(gridX, gridY) + 500); // above tiles
|
||||
|
||||
// Bounce animation
|
||||
this.scene.tweens.add({
|
||||
targets: drop,
|
||||
y: y - 40,
|
||||
duration: 500,
|
||||
yoyo: true,
|
||||
ease: 'Sine.easeOut',
|
||||
repeat: -1
|
||||
});
|
||||
|
||||
this.drops.push({
|
||||
gridX,
|
||||
gridY,
|
||||
sprite: drop,
|
||||
type: type
|
||||
});
|
||||
}
|
||||
|
||||
update() {
|
||||
// Check for player pickup
|
||||
if (!this.scene.player) return;
|
||||
|
||||
const playerPos = this.scene.player.getPosition();
|
||||
|
||||
// Filter drops to pick up
|
||||
for (let i = this.drops.length - 1; i >= 0; i--) {
|
||||
const drop = this.drops[i];
|
||||
|
||||
// Check if player is ON the drop tile
|
||||
if (Math.abs(drop.gridX - playerPos.x) < 0.8 && Math.abs(drop.gridY - playerPos.y) < 0.8) {
|
||||
// Pick up!
|
||||
console.log('🎒 Picked up:', drop.type);
|
||||
|
||||
// Play pickup sound
|
||||
if (this.scene.soundManager) this.scene.soundManager.playPickup();
|
||||
|
||||
// Add to inventory
|
||||
if (this.scene.inventorySystem) {
|
||||
this.scene.inventorySystem.addItem(drop.type, 1);
|
||||
}
|
||||
|
||||
// Destroy visual
|
||||
drop.sprite.destroy();
|
||||
this.drops.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user