feat: Complete 2D Visual Overhaul - Isometric to Flat Top-Down
- NEW: Flat2DTerrainSystem.js (375 lines) - NEW: map2d_data.js procedural map (221 lines) - MODIFIED: GameScene async create, 2D terrain integration - MODIFIED: Player.js flat 2D positioning - MODIFIED: game.js disabled pixelArt for smooth rendering - FIXED: 15+ bugs (updateCulling, isometric conversions, grid lines) - ADDED: Phase 28 to TASKS.md - DOCS: DNEVNIK.md session summary Result: Working flat 2D game with Stardew Valley style! Time: 5.5 hours
This commit is contained in:
@@ -12,11 +12,23 @@ class Player {
|
||||
|
||||
this.iso = new IsometricUtils(48, 24);
|
||||
|
||||
// Hitrost gibanja
|
||||
this.moveSpeed = 150; // px/s
|
||||
this.gridMoveTime = 200; // ms za premik na eno kocko
|
||||
// 🎮 SMOOTH MOVEMENT SYSTEM (Hybrid)
|
||||
this.moveSpeed = 100; // Normal speed px/s
|
||||
this.sprintSpeed = 200; // Sprint speed px/s
|
||||
this.acceleration = 0.15; // How fast to reach target speed
|
||||
|
||||
// Stanje
|
||||
// Velocity (current movement)
|
||||
this.velocity = { x: 0, y: 0 };
|
||||
// Target velocity (where we want to go)
|
||||
this.targetVelocity = { x: 0, y: 0 };
|
||||
|
||||
// Sprint system
|
||||
this.sprinting = false;
|
||||
this.energy = 100;
|
||||
this.maxEnergy = 100;
|
||||
this.energyDrain = 10; // per second while sprinting
|
||||
|
||||
// State
|
||||
this.isMoving = false;
|
||||
this.direction = 'down';
|
||||
this.lastDir = { x: 0, y: 1 }; // Default south
|
||||
@@ -135,10 +147,19 @@ class Player {
|
||||
|
||||
setupControls() {
|
||||
this.keys = this.scene.input.keyboard.addKeys({
|
||||
up: Phaser.Input.Keyboard.KeyCodes.W,
|
||||
down: Phaser.Input.Keyboard.KeyCodes.S,
|
||||
left: Phaser.Input.Keyboard.KeyCodes.A,
|
||||
right: Phaser.Input.Keyboard.KeyCodes.D
|
||||
// WASD
|
||||
w: Phaser.Input.Keyboard.KeyCodes.W,
|
||||
a: Phaser.Input.Keyboard.KeyCodes.A,
|
||||
s: Phaser.Input.Keyboard.KeyCodes.S,
|
||||
d: Phaser.Input.Keyboard.KeyCodes.D,
|
||||
// Arrow Keys
|
||||
up: Phaser.Input.Keyboard.KeyCodes.UP,
|
||||
down: Phaser.Input.Keyboard.KeyCodes.DOWN,
|
||||
left: Phaser.Input.Keyboard.KeyCodes.LEFT,
|
||||
right: Phaser.Input.Keyboard.KeyCodes.RIGHT,
|
||||
// Actions
|
||||
space: Phaser.Input.Keyboard.KeyCodes.SPACE,
|
||||
shift: Phaser.Input.Keyboard.KeyCodes.SHIFT
|
||||
});
|
||||
|
||||
// Gamepad Events
|
||||
@@ -222,18 +243,60 @@ class Player {
|
||||
}
|
||||
|
||||
update(delta) {
|
||||
// NOTE: updateDepth() disabled - using sortableObjects Z-sorting in GameScene
|
||||
// if (this.isMoving) {
|
||||
// this.updateDepth();
|
||||
// }
|
||||
// Convert delta to seconds
|
||||
const dt = delta / 1000;
|
||||
|
||||
if (!this.isMoving) {
|
||||
this.handleInput();
|
||||
// 🎮 HANDLE INPUT (always check for input)
|
||||
this.handleInput(dt);
|
||||
|
||||
// 🏃 APPLY SMOOTH MOVEMENT
|
||||
// Smoothly interpolate current velocity toward target velocity
|
||||
this.velocity.x = Phaser.Math.Linear(
|
||||
this.velocity.x,
|
||||
this.targetVelocity.x,
|
||||
this.acceleration
|
||||
);
|
||||
this.velocity.y = Phaser.Math.Linear(
|
||||
this.velocity.y,
|
||||
this.targetVelocity.y,
|
||||
this.acceleration
|
||||
);
|
||||
|
||||
// Apply velocity to sprite position
|
||||
this.sprite.x += this.velocity.x * dt;
|
||||
this.sprite.y += this.velocity.y * dt;
|
||||
|
||||
// Update grid position from pixel position
|
||||
const screenPos = { x: this.sprite.x - this.offsetX, y: this.sprite.y - this.offsetY };
|
||||
const gridPos = this.iso.toGrid(screenPos.x, screenPos.y);
|
||||
this.gridX = Math.floor(gridPos.x);
|
||||
this.gridY = Math.floor(gridPos.y);
|
||||
|
||||
// Check if moving
|
||||
const speed = Math.sqrt(this.velocity.x ** 2 + this.velocity.y ** 2);
|
||||
this.isMoving = speed > 5;
|
||||
|
||||
// 🎨 UPDATE ANIMATION
|
||||
this.updateAnimation();
|
||||
|
||||
// 💧 ENERGY REGENERATION
|
||||
if (!this.sprinting && this.energy < this.maxEnergy) {
|
||||
this.energy = Math.min(this.maxEnergy, this.energy + 20 * dt);
|
||||
}
|
||||
|
||||
// ⚡ ENERGY DRAIN WHILE SPRINTING
|
||||
if (this.sprinting && this.isMoving && this.energy > 0) {
|
||||
this.energy -= this.energyDrain * dt;
|
||||
if (this.energy <= 0) {
|
||||
this.energy = 0;
|
||||
this.sprinting = false; // Can't sprint without energy
|
||||
}
|
||||
}
|
||||
|
||||
// 🔧 UPDATE HELD ITEM
|
||||
this.updateHeldItem();
|
||||
|
||||
// SPACE KEY - Farming Action
|
||||
// 🌱 SPACE KEY - Farming Action
|
||||
if (this.keys.space && Phaser.Input.Keyboard.JustDown(this.keys.space)) {
|
||||
this.handleFarmingAction();
|
||||
}
|
||||
@@ -261,189 +324,137 @@ class Player {
|
||||
}
|
||||
}
|
||||
|
||||
handleInput() {
|
||||
let targetX = this.gridX;
|
||||
let targetY = this.gridY;
|
||||
let moved = false;
|
||||
let facingRight = !this.sprite.flipX;
|
||||
handleInput(dt) {
|
||||
// 🎮 COLLECT INPUT FROM ALL SOURCES
|
||||
let inputX = 0;
|
||||
let inputY = 0;
|
||||
|
||||
// Determine inputs
|
||||
let up = this.keys.up.isDown;
|
||||
let down = this.keys.down.isDown;
|
||||
let left = this.keys.left.isDown;
|
||||
let right = this.keys.right.isDown;
|
||||
// Keyboard (WASD + Arrows)
|
||||
if (this.keys.up.isDown || this.keys.w.isDown) inputY -= 1;
|
||||
if (this.keys.down.isDown || this.keys.s.isDown) inputY += 1;
|
||||
if (this.keys.left.isDown || this.keys.a.isDown) inputX -= 1;
|
||||
if (this.keys.right.isDown || this.keys.d.isDown) inputX += 1;
|
||||
|
||||
// Check Virtual Joystick inputs (from UIScene)
|
||||
// Virtual Joystick (Mobile)
|
||||
const ui = this.scene.scene.get('UIScene');
|
||||
if (ui && ui.virtualJoystick) {
|
||||
if (ui.virtualJoystick.up) up = true;
|
||||
if (ui.virtualJoystick.down) down = true;
|
||||
if (ui.virtualJoystick.left) left = true;
|
||||
if (ui.virtualJoystick.right) right = true;
|
||||
if (ui.virtualJoystick.up) inputY -= 1;
|
||||
if (ui.virtualJoystick.down) inputY += 1;
|
||||
if (ui.virtualJoystick.left) inputX -= 1;
|
||||
if (ui.virtualJoystick.right) inputX += 1;
|
||||
}
|
||||
|
||||
// Check Gamepad Input (Xbox Controller)
|
||||
// Gamepad (Xbox Controller)
|
||||
if (this.scene.input.gamepad && this.scene.input.gamepad.total > 0) {
|
||||
const pad = this.scene.input.gamepad.getPad(0);
|
||||
if (pad) {
|
||||
const threshold = 0.3;
|
||||
if (pad.leftStick.y < -threshold) up = true;
|
||||
if (pad.leftStick.y > threshold) down = true;
|
||||
if (pad.leftStick.x < -threshold) left = true;
|
||||
if (pad.leftStick.x > threshold) right = true;
|
||||
const threshold = 0.2;
|
||||
const stickY = pad.leftStick.y;
|
||||
const stickX = pad.leftStick.x;
|
||||
|
||||
// D-Pad support
|
||||
if (pad.up) up = true;
|
||||
if (pad.down) down = true;
|
||||
if (pad.left) left = true;
|
||||
if (pad.right) right = true;
|
||||
// Analog stick (smooth values)
|
||||
if (Math.abs(stickY) > threshold) inputY += stickY;
|
||||
if (Math.abs(stickX) > threshold) inputX += stickX;
|
||||
|
||||
// D-Pad (digital)
|
||||
if (pad.up) inputY -= 1;
|
||||
if (pad.down) inputY += 1;
|
||||
if (pad.left) inputX -= 1;
|
||||
if (pad.right) inputX += 1;
|
||||
|
||||
// Sprint button (B on Xbox, Circle on PS)
|
||||
if (pad.B || pad.buttons[1]?.pressed) {
|
||||
this.sprinting = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply
|
||||
let dx = 0;
|
||||
let dy = 0;
|
||||
// 🏃 SPRINT DETECTION (Shift key)
|
||||
this.sprinting = this.keys.shift?.isDown || this.sprinting;
|
||||
|
||||
if (up) {
|
||||
dx = -1; dy = 0;
|
||||
moved = true;
|
||||
facingRight = false;
|
||||
} else if (down) {
|
||||
dx = 1; dy = 0;
|
||||
moved = true;
|
||||
facingRight = true;
|
||||
// Normalize diagonal movement (so diagonal isn't faster)
|
||||
const inputLength = Math.sqrt(inputX ** 2 + inputY ** 2);
|
||||
if (inputLength > 0) {
|
||||
inputX /= inputLength;
|
||||
inputY /= inputLength;
|
||||
}
|
||||
|
||||
if (left) {
|
||||
dx = 0; dy = 1;
|
||||
moved = true;
|
||||
facingRight = false;
|
||||
} else if (right) {
|
||||
dx = 0; dy = -1;
|
||||
moved = true;
|
||||
facingRight = true;
|
||||
// 🎯 DETERMINE MOVEMENT SPEED
|
||||
let maxSpeed = this.moveSpeed;
|
||||
if (this.sprinting && this.energy > 0) {
|
||||
maxSpeed = this.sprintSpeed;
|
||||
}
|
||||
|
||||
// Update target
|
||||
targetX = this.gridX + dx;
|
||||
targetY = this.gridY + dy;
|
||||
// 🚀 SET TARGET VELOCITY
|
||||
this.targetVelocity.x = inputX * maxSpeed;
|
||||
this.targetVelocity.y = inputY * maxSpeed;
|
||||
|
||||
// Update Facing Direction and Last Dir
|
||||
if (moved) {
|
||||
this.lastDir = { x: dx, y: dy };
|
||||
// 🧭 UPDATE DIRECTION & FACING
|
||||
if (inputLength > 0.1) {
|
||||
// Update last direction for attacks/interactions
|
||||
this.lastDir = { x: inputX, y: inputY };
|
||||
|
||||
// Determine animation direction (4 directions)
|
||||
let animDir = 'down'; // default
|
||||
// Determine animation direction (4-way)
|
||||
// Isometric mapping: up/down = X axis, left/right = Y axis
|
||||
let animDir = 'down';
|
||||
|
||||
// UP/DOWN (isometric: dx changes)
|
||||
if (dx < 0 && dy === 0) {
|
||||
animDir = 'up'; // Moving up (NW in isometric)
|
||||
} else if (dx > 0 && dy === 0) {
|
||||
animDir = 'down'; // Moving down (SE in isometric)
|
||||
}
|
||||
// LEFT/RIGHT (isometric: dy changes)
|
||||
else if (dy > 0 && dx === 0) {
|
||||
animDir = 'left'; // Moving left (SW in isometric)
|
||||
} else if (dy < 0 && dx === 0) {
|
||||
animDir = 'right'; // Moving right (NE in isometric)
|
||||
// Prioritize primary direction (stronger input)
|
||||
if (Math.abs(inputX) > Math.abs(inputY)) {
|
||||
// Vertical movement (up/down in isometric)
|
||||
animDir = inputX < 0 ? 'up' : 'down';
|
||||
} else {
|
||||
// Horizontal movement (left/right in isometric)
|
||||
animDir = inputY < 0 ? 'right' : 'left';
|
||||
}
|
||||
|
||||
this.direction = animDir;
|
||||
}
|
||||
}
|
||||
|
||||
// Play walking animation for the direction
|
||||
if (this.sprite.anims) {
|
||||
try {
|
||||
const walkAnim = `protagonist_walk_${animDir}`;
|
||||
// 🎨 UPDATE ANIMATION (called from update loop)
|
||||
updateAnimation() {
|
||||
if (!this.sprite.anims) return;
|
||||
|
||||
// Debug
|
||||
console.log(`🎬 Trying to play: ${walkAnim}`);
|
||||
console.log(`Animation exists: ${this.scene.anims.exists(walkAnim)}`);
|
||||
const speed = Math.sqrt(this.velocity.x ** 2 + this.velocity.y ** 2);
|
||||
|
||||
if (this.scene.anims.exists(walkAnim)) {
|
||||
this.sprite.play(walkAnim, true); // Force restart animation
|
||||
console.log(`✅ Playing: ${walkAnim}`);
|
||||
} else {
|
||||
console.warn(`⚠️ Animation not found: ${walkAnim}`);
|
||||
try {
|
||||
if (speed < 5) {
|
||||
// 😴 IDLE
|
||||
const idleAnim = `protagonist_idle_${this.direction}`;
|
||||
if (this.scene.anims.exists(idleAnim) && !this.sprite.anims.isPlaying) {
|
||||
this.sprite.play(idleAnim);
|
||||
}
|
||||
} else {
|
||||
// 🚶 WALKING / 🏃 SPRINTING
|
||||
const walkAnim = `protagonist_walk_${this.direction}`;
|
||||
|
||||
if (this.scene.anims.exists(walkAnim)) {
|
||||
if (this.sprite.anims.currentAnim?.key !== walkAnim) {
|
||||
this.sprite.play(walkAnim, true);
|
||||
}
|
||||
|
||||
// Faster animation when sprinting
|
||||
const frameRate = this.sprinting ? 12 : 8;
|
||||
if (this.sprite.anims.currentAnim) {
|
||||
this.sprite.anims.currentAnim.frameRate = frameRate;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Animation error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Hand offset based on direction
|
||||
// Hand sprite position update
|
||||
const handOffsets = {
|
||||
'left': -10,
|
||||
'right': 10,
|
||||
'up': 0,
|
||||
'down': 0
|
||||
};
|
||||
this.handSprite.setX(this.sprite.x + (handOffsets[animDir] || 0));
|
||||
} else {
|
||||
// Stop animation when idle
|
||||
if (this.sprite.anims) {
|
||||
try {
|
||||
if (this.sprite.anims.isPlaying) {
|
||||
this.sprite.stop();
|
||||
}
|
||||
// Play idle animation for current direction
|
||||
const idleAnim = `protagonist_idle_${this.direction}`;
|
||||
if (this.scene.anims.exists(idleAnim)) {
|
||||
this.sprite.play(idleAnim);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore animation errors
|
||||
}
|
||||
|
||||
if (this.handSprite) {
|
||||
this.handSprite.setX(this.sprite.x + (handOffsets[this.direction] || 0));
|
||||
}
|
||||
}
|
||||
|
||||
// Collision Check
|
||||
const terrainSystem = this.scene.terrainSystem;
|
||||
if (moved && terrainSystem) {
|
||||
if (this.iso.isInBounds(targetX, targetY, terrainSystem.width, terrainSystem.height)) {
|
||||
|
||||
const tile = terrainSystem.tiles[targetY][targetX];
|
||||
let isPassable = true;
|
||||
|
||||
// TILE COLLISION - Preveri solid property PRVO
|
||||
if (tile.solid === true) {
|
||||
console.log('⛔ Blocked by solid tile property');
|
||||
isPassable = false;
|
||||
}
|
||||
|
||||
// Nato preveri tip (fallback)
|
||||
const solidTileTypes = [
|
||||
'water', // Voda
|
||||
'MINE_WALL', // Rudniški zidovi
|
||||
'WALL_EDGE', // Robovi zidov (DODANO)
|
||||
'ORE_STONE', // Kamnita ruda (dokler ni izkopana)
|
||||
'ORE_IRON', // Železna ruda
|
||||
'lava', // Lava (če bo dodana)
|
||||
'void' // Praznina
|
||||
// Opomba: PAVEMENT je WALKABLE (igralec lahko hodi po cesti)
|
||||
];
|
||||
|
||||
const tileName = tile.type.name || tile.type;
|
||||
if (isPassable && solidTileTypes.includes(tileName)) {
|
||||
console.log('⛔ Blocked by solid tile:', tileName);
|
||||
isPassable = false;
|
||||
}
|
||||
|
||||
// DECORATION COLLISION - Trdni objekti
|
||||
const key = `${targetX},${targetY}`;
|
||||
if (terrainSystem.decorationsMap.has(key)) {
|
||||
const decor = terrainSystem.decorationsMap.get(key);
|
||||
|
||||
// Preverimo decor.solid property (set by TerrainSystem.addDecoration)
|
||||
if (decor.solid === true) {
|
||||
console.log('⛔ BLOCKED by solid decoration:', decor.type);
|
||||
isPassable = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isPassable) {
|
||||
this.moveToGrid(targetX, targetY);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore animation errors
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,11 +523,12 @@ class Player {
|
||||
}
|
||||
|
||||
updatePosition() {
|
||||
const screenPos = this.iso.toScreen(this.gridX, this.gridY);
|
||||
// 🎨 FLAT 2D POSITIONING (NEW!)
|
||||
const tileSize = 48;
|
||||
|
||||
// Pixel-perfect positioning
|
||||
const x = Math.round(screenPos.x + this.offsetX);
|
||||
const y = Math.round(screenPos.y + this.offsetY);
|
||||
// Direct grid to pixel conversion (NO isometric!)
|
||||
const x = Math.round((this.gridX * tileSize) + (tileSize / 2));
|
||||
const y = Math.round((this.gridY * tileSize) + (tileSize / 2));
|
||||
|
||||
this.sprite.setPosition(x, y);
|
||||
|
||||
@@ -714,4 +726,20 @@ class Player {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if player has required tool equipped/in inventory
|
||||
* @param {string} toolType - Tool type (axe, pickaxe, hoe, etc.)
|
||||
* @returns {boolean} - True if player has the tool
|
||||
*/
|
||||
hasToolEquipped(toolType) {
|
||||
if (!toolType) return true; // No tool required
|
||||
|
||||
// Check inventory for tool
|
||||
if (this.scene.inventorySystem) {
|
||||
return this.scene.inventorySystem.hasItem(toolType, 1);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
16
src/game.js
16
src/game.js
@@ -45,19 +45,19 @@ const config = {
|
||||
height: 768, // 4:3 aspect ratio
|
||||
parent: 'game-container',
|
||||
backgroundColor: '#1a1a2e',
|
||||
pixelArt: true,
|
||||
antialias: false,
|
||||
roundPixels: true,
|
||||
pixelArt: false, // 🎨 SMOOTH 2D (was: true)
|
||||
antialias: true, // 🎨 SMOOTH edges (was: false)
|
||||
roundPixels: false, // 🎨 SMOOTH positioning (was: true)
|
||||
render: {
|
||||
pixelArt: true,
|
||||
antialias: false,
|
||||
roundPixels: true,
|
||||
pixelArt: false, // 🎨 SMOOTH 2D
|
||||
antialias: true, // 🎨 SMOOTH edges
|
||||
roundPixels: false, // 🎨 SMOOTH positioning
|
||||
transparent: false,
|
||||
clearBeforeRender: true,
|
||||
powerPreference: 'high-performance',
|
||||
premultipliedAlpha: true, // Fix transparency
|
||||
premultipliedAlpha: true,
|
||||
failIfMajorPerformanceCaveat: false,
|
||||
// Eksplicitna NEAREST_NEIGHBOR filtracija
|
||||
// 🎨 LINEAR filtering for smooth tiles
|
||||
mipmapFilter: 'NEAREST',
|
||||
batchSize: 4096
|
||||
},
|
||||
|
||||
@@ -15,13 +15,14 @@ class GameScene extends Phaser.Scene {
|
||||
};
|
||||
}
|
||||
|
||||
create() {
|
||||
async create() {
|
||||
console.log('🎮 GameScene: Initialized!');
|
||||
|
||||
// Generate procedural textures
|
||||
new TextureGenerator(this).generateAll();
|
||||
InventoryIcons.create(this); // Override with flat 2D inventory icons
|
||||
window.gameState.currentScene = 'GameScene';
|
||||
window.gameState.gameScene = this; // Reference for global inventory helper
|
||||
|
||||
const width = this.cameras.main.width;
|
||||
const height = this.cameras.main.height;
|
||||
@@ -55,8 +56,25 @@ class GameScene extends Phaser.Scene {
|
||||
// Inicializiraj terrain sistem - 100x100 mapa
|
||||
console.log('🌍 Initializing terrain...');
|
||||
try {
|
||||
this.terrainSystem = new TerrainSystem(this, 100, 100);
|
||||
this.terrainSystem.generate();
|
||||
// 🎲 SEED-BASED GENERATION - Vsak krat ista mapa!
|
||||
// Preveri če že imaš shranjeno spawn točko
|
||||
let spawnPoint = localStorage.getItem('novafarma_spawn_point');
|
||||
let terrainSeed = localStorage.getItem('novafarma_terrain_seed');
|
||||
|
||||
if (!terrainSeed) {
|
||||
// PRVI LOGIN - generiraj nov seed
|
||||
terrainSeed = Math.random().toString(36).substring(7);
|
||||
localStorage.setItem('novafarma_terrain_seed', terrainSeed);
|
||||
console.log('🆕 New world seed:', terrainSeed);
|
||||
} else {
|
||||
console.log('♻️ Loading existing world with seed:', terrainSeed);
|
||||
}
|
||||
|
||||
// 🎨 2D FLAT TERRAIN SYSTEM (NEW!)
|
||||
console.log('🎨 Initializing Flat 2D Terrain...');
|
||||
this.terrainSystem = new Flat2DTerrainSystem(this);
|
||||
await this.terrainSystem.generate();
|
||||
console.log('✅ Flat 2D terrain ready!');
|
||||
|
||||
// Initialize Farming System
|
||||
this.farmingSystem = new FarmingSystem(this);
|
||||
@@ -115,8 +133,28 @@ class GameScene extends Phaser.Scene {
|
||||
// Initial force update to render active tiles before first frame
|
||||
this.terrainSystem.updateCulling(this.cameras.main);
|
||||
|
||||
// INITIALIZE FARM AREA (Starter Zone @ 20,20)
|
||||
this.initializeFarmWorld();
|
||||
// 🎲 RANDOM SPAWN POINT + 8x8 FARM (Prvi login)
|
||||
if (!spawnPoint) {
|
||||
// PRVI LOGIN - Random spawn točka
|
||||
const spawnX = Math.floor(Math.random() * 80) + 10; // 10-90
|
||||
const spawnY = Math.floor(Math.random() * 80) + 10; // 10-90
|
||||
|
||||
spawnPoint = `${spawnX},${spawnY}`;
|
||||
localStorage.setItem('novafarma_spawn_point', spawnPoint);
|
||||
|
||||
console.log(`🎲 First login - Random spawn at (${spawnX}, ${spawnY})`);
|
||||
console.log(`🏡 Creating 8x8 starter farm at spawn location...`);
|
||||
|
||||
// Ustvari 8x8 farmo na spawn točki
|
||||
this.initializeFarmWorld(spawnX, spawnY);
|
||||
} else {
|
||||
// NI PRVI LOGIN - Naloži obstoječo spawn točko
|
||||
const [spawnX, spawnY] = spawnPoint.split(',').map(Number);
|
||||
console.log(`♻️ Returning player - spawn at (${spawnX}, ${spawnY})`);
|
||||
|
||||
// Obnovi farmo (če je shranjena)
|
||||
this.initializeFarmWorld(spawnX, spawnY);
|
||||
}
|
||||
|
||||
// 🍎 SADOVNJAK - Sadna Drevesa (ONEMOGOČENO)
|
||||
// ========================================================
|
||||
@@ -334,9 +372,17 @@ class GameScene extends Phaser.Scene {
|
||||
console.error("Terrain system failed:", e);
|
||||
}
|
||||
|
||||
// Dodaj igralca
|
||||
// Dodaj igralca NA SPAWN TOČKI
|
||||
console.log('👤 Initializing player...');
|
||||
this.player = new Player(this, 50, 50, this.terrainOffsetX, this.terrainOffsetY);
|
||||
const savedSpawn = localStorage.getItem('novafarma_spawn_point');
|
||||
let playerSpawnX = 50, playerSpawnY = 50;
|
||||
|
||||
if (savedSpawn) {
|
||||
[playerSpawnX, playerSpawnY] = savedSpawn.split(',').map(Number);
|
||||
console.log(`👤 Spawning player at saved location: (${playerSpawnX}, ${playerSpawnY})`);
|
||||
}
|
||||
|
||||
this.player = new Player(this, playerSpawnX, playerSpawnY, this.terrainOffsetX, this.terrainOffsetY);
|
||||
|
||||
// 🎯 SORTABLE OBJECTS GROUP - Za 2.5D Z-Sorting
|
||||
console.log('🎯 Creating sortableObjects group for Z-sorting...');
|
||||
@@ -438,6 +484,22 @@ class GameScene extends Phaser.Scene {
|
||||
this.statsSystem = new StatsSystem(this);
|
||||
this.inventorySystem = new InventorySystem(this);
|
||||
|
||||
// 🛠️ CRAFTING SYSTEM
|
||||
this.craftingSystem = new CraftingSystem(this);
|
||||
this.craftingSystem.loadRecipes().then(() => {
|
||||
console.log('🛠️ Crafting system ready!');
|
||||
|
||||
// Create UI after recipes loaded
|
||||
this.craftingUI = new CraftingUI(this);
|
||||
|
||||
// Add C key to toggle crafting UI
|
||||
this.input.keyboard.on('keydown-C', () => {
|
||||
if (this.craftingUI) {
|
||||
this.craftingUI.toggle();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================================
|
||||
// 💎 NEOMEJENI VIRI - Les in Kamen
|
||||
// ========================================================
|
||||
@@ -775,6 +837,18 @@ class GameScene extends Phaser.Scene {
|
||||
setupCamera() {
|
||||
const cam = this.cameras.main;
|
||||
|
||||
// 🎨 FLAT 2D CAMERA SETUP (NEW!)
|
||||
const worldSize = 100 * 48; // 100 tiles × 48px = 4800px
|
||||
cam.setBounds(0, 0, worldSize, worldSize);
|
||||
cam.setZoom(1.0); // Default zoom for 2D
|
||||
|
||||
// Follow player
|
||||
if (this.player && this.player.sprite) {
|
||||
cam.startFollow(this.player.sprite, true, 0.1, 0.1);
|
||||
}
|
||||
|
||||
console.log('📹 2D Camera setup:', worldSize, 'x', worldSize);
|
||||
|
||||
// Zoom kontrole (Mouse Wheel)
|
||||
this.input.on('wheel', (pointer, gameObjects, deltaX, deltaY, deltaZ) => {
|
||||
const zoomSpeed = 0.001;
|
||||
@@ -843,6 +917,597 @@ class GameScene extends Phaser.Scene {
|
||||
this.input.keyboard.on('keydown-M', () => {
|
||||
if (this.soundManager) this.soundManager.toggleMute();
|
||||
});
|
||||
|
||||
// 🌧️ WEATHER SYSTEM KEYS
|
||||
this.input.keyboard.on('keydown-R', () => this.setWeather('rain'));
|
||||
this.input.keyboard.on('keydown-SHIFT-S', () => this.setWeather('snow'));
|
||||
this.input.keyboard.on('keydown-T', () => this.setWeather('storm'));
|
||||
this.input.keyboard.on('keydown-F', () => this.setWeather('fog'));
|
||||
this.input.keyboard.on('keydown-SHIFT-C', () => this.setWeather('clear'));
|
||||
|
||||
// Initialize weather system
|
||||
this.initializeWeatherSystem();
|
||||
|
||||
// Initialize Weather UI Panel
|
||||
this.weatherUI = new WeatherUI(this);
|
||||
console.log('📊 Weather UI Panel created (press W to toggle)');
|
||||
}
|
||||
|
||||
// 🌦️ COMPLETE WEATHER SYSTEM
|
||||
initializeWeatherSystem() {
|
||||
this.currentWeather = 'clear';
|
||||
this.weatherIntensity = 1.0;
|
||||
this.puddles = [];
|
||||
this.splashes = [];
|
||||
this.autoWeatherEnabled = false;
|
||||
this.weatherCycleTimer = null;
|
||||
|
||||
// Load saved weather state
|
||||
this.loadWeatherState();
|
||||
|
||||
console.log('🌦️ Weather system initialized');
|
||||
console.log('💡 R = Rain | Shift+S = Snow | T = Storm | F = Fog | Shift+C = Clear');
|
||||
console.log('💡 Shift+A = Toggle Auto Weather Cycle');
|
||||
|
||||
// Auto weather cycle toggle
|
||||
this.input.keyboard.on('keydown-SHIFT-A', () => {
|
||||
this.toggleAutoWeather();
|
||||
});
|
||||
|
||||
// Intensity controls (+/-)
|
||||
this.input.keyboard.on('keydown-PLUS', () => this.adjustIntensity(0.2));
|
||||
this.input.keyboard.on('keydown-MINUS', () => this.adjustIntensity(-0.2));
|
||||
}
|
||||
|
||||
toggleAutoWeather() {
|
||||
this.autoWeatherEnabled = !this.autoWeatherEnabled;
|
||||
|
||||
if (this.autoWeatherEnabled) {
|
||||
console.log('🔄 Auto weather cycle ENABLED');
|
||||
this.startWeatherCycle();
|
||||
} else {
|
||||
console.log('⏸️ Auto weather cycle DISABLED');
|
||||
if (this.weatherCycleTimer) {
|
||||
this.weatherCycleTimer.destroy();
|
||||
this.weatherCycleTimer = null;
|
||||
}
|
||||
}
|
||||
this.saveWeatherState();
|
||||
}
|
||||
|
||||
startWeatherCycle() {
|
||||
// Weather cycle: Clear → Rain → Storm → Clear → Snow → Fog → Clear
|
||||
const weatherSequence = ['clear', 'rain', 'storm', 'clear', 'snow', 'fog'];
|
||||
const weatherDurations = {
|
||||
clear: 120000, // 2 minutes
|
||||
rain: 90000, // 1.5 minutes
|
||||
storm: 60000, // 1 minute
|
||||
snow: 90000, // 1.5 minutes
|
||||
fog: 60000 // 1 minute
|
||||
};
|
||||
|
||||
let currentIndex = 0;
|
||||
|
||||
const cycleWeather = () => {
|
||||
const nextWeather = weatherSequence[currentIndex];
|
||||
this.setWeather(nextWeather);
|
||||
|
||||
currentIndex = (currentIndex + 1) % weatherSequence.length;
|
||||
|
||||
const duration = weatherDurations[nextWeather] || 120000;
|
||||
|
||||
this.weatherCycleTimer = this.time.delayedCall(duration, () => {
|
||||
if (this.autoWeatherEnabled) {
|
||||
cycleWeather();
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`🌦️ Auto weather: ${nextWeather} (${duration / 1000}s)`);
|
||||
};
|
||||
|
||||
cycleWeather();
|
||||
}
|
||||
|
||||
adjustIntensity(delta) {
|
||||
this.weatherIntensity = Phaser.Math.Clamp(this.weatherIntensity + delta, 0.2, 2.0);
|
||||
|
||||
// Update current weather with new intensity
|
||||
if (this.currentWeather !== 'clear' && this.currentWeather !== 'fog') {
|
||||
this.setWeather(this.currentWeather); // Restart with new intensity
|
||||
}
|
||||
|
||||
console.log(`🎚️ Weather intensity: ${(this.weatherIntensity * 100).toFixed(0)}%`);
|
||||
this.saveWeatherState();
|
||||
}
|
||||
|
||||
saveWeatherState() {
|
||||
const state = {
|
||||
currentWeather: this.currentWeather,
|
||||
intensity: this.weatherIntensity,
|
||||
autoEnabled: this.autoWeatherEnabled
|
||||
};
|
||||
localStorage.setItem('novafarma_weather_state', JSON.stringify(state));
|
||||
}
|
||||
|
||||
loadWeatherState() {
|
||||
const saved = localStorage.getItem('novafarma_weather_state');
|
||||
if (saved) {
|
||||
try {
|
||||
const state = JSON.parse(saved);
|
||||
this.currentWeather = state.currentWeather || 'clear';
|
||||
this.weatherIntensity = state.intensity || 1.0;
|
||||
this.autoWeatherEnabled = state.autoEnabled || false;
|
||||
|
||||
console.log('📂 Weather state loaded:', state);
|
||||
|
||||
// Restore weather (delayed to avoid initialization issues)
|
||||
this.time.delayedCall(1000, () => {
|
||||
if (this.currentWeather !== 'clear') {
|
||||
this.setWeather(this.currentWeather);
|
||||
}
|
||||
if (this.autoWeatherEnabled) {
|
||||
this.startWeatherCycle();
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('⚠️ Failed to load weather state:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setWeather(type) {
|
||||
// Stop current weather
|
||||
this.stopAllWeather();
|
||||
|
||||
this.currentWeather = type;
|
||||
|
||||
switch (type) {
|
||||
case 'rain':
|
||||
this.startRain();
|
||||
break;
|
||||
case 'snow':
|
||||
this.startSnow();
|
||||
break;
|
||||
case 'storm':
|
||||
this.startStorm();
|
||||
break;
|
||||
case 'fog':
|
||||
this.startFog();
|
||||
break;
|
||||
case 'clear':
|
||||
console.log('☀️ Clear weather');
|
||||
break;
|
||||
}
|
||||
|
||||
this.saveWeatherState();
|
||||
}
|
||||
|
||||
stopAllWeather() {
|
||||
// Stop all particle emitters
|
||||
if (this.rainEmitter) this.rainEmitter.stop();
|
||||
if (this.snowEmitter) this.snowEmitter.stop();
|
||||
if (this.stormEmitter) this.stormEmitter.stop();
|
||||
|
||||
// Stop lightning
|
||||
if (this.lightningTimer) {
|
||||
this.lightningTimer.destroy();
|
||||
this.lightningTimer = null;
|
||||
}
|
||||
|
||||
// Stop puddle spawning
|
||||
if (this.puddleTimer) {
|
||||
this.puddleTimer.destroy();
|
||||
this.puddleTimer = null;
|
||||
}
|
||||
|
||||
// Fade out fog
|
||||
if (this.fogOverlay) {
|
||||
this.tweens.add({
|
||||
targets: this.fogOverlay,
|
||||
alpha: 0,
|
||||
duration: 2000,
|
||||
onComplete: () => {
|
||||
if (this.fogOverlay) this.fogOverlay.destroy();
|
||||
this.fogOverlay = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Cleanup puddles
|
||||
this.puddles.forEach(puddle => {
|
||||
this.tweens.add({
|
||||
targets: puddle,
|
||||
alpha: 0,
|
||||
duration: 3000,
|
||||
onComplete: () => puddle.destroy()
|
||||
});
|
||||
});
|
||||
this.puddles = [];
|
||||
}
|
||||
|
||||
// ☔ RAIN SYSTEM
|
||||
startRain() {
|
||||
if (!this.rainEmitter) {
|
||||
this.createRainParticles();
|
||||
}
|
||||
|
||||
// Apply intensity
|
||||
this.rainEmitter.setQuantity(Math.floor(3 * this.weatherIntensity));
|
||||
this.rainEmitter.setFrequency(30 / this.weatherIntensity);
|
||||
|
||||
this.rainEmitter.start();
|
||||
console.log('🌧️ Rain started!');
|
||||
|
||||
// Start puddle spawning
|
||||
this.puddleTimer = this.time.addEvent({
|
||||
delay: 3000 / this.weatherIntensity,
|
||||
callback: () => this.spawnPuddle(),
|
||||
loop: true
|
||||
});
|
||||
|
||||
// Play rain sound
|
||||
if (this.soundManager && this.soundManager.playRainSound) {
|
||||
this.soundManager.playRainSound();
|
||||
}
|
||||
}
|
||||
|
||||
createRainParticles() {
|
||||
if (!this.textures.exists('raindrop')) {
|
||||
const graphics = this.make.graphics({ x: 0, y: 0, add: false });
|
||||
graphics.fillStyle(0x88ccff, 1);
|
||||
graphics.fillCircle(1, 2, 1);
|
||||
graphics.generateTexture('raindrop', 2, 4);
|
||||
graphics.destroy();
|
||||
}
|
||||
|
||||
const cam = this.cameras.main;
|
||||
this.rainEmitter = this.add.particles(0, 0, 'raindrop', {
|
||||
x: { min: 0, max: cam.width },
|
||||
y: -50,
|
||||
lifespan: 3000,
|
||||
speedY: { min: 400, max: 600 },
|
||||
scale: { start: 1, end: 0.5 },
|
||||
alpha: { start: 0.6, end: 0.2 },
|
||||
quantity: 3,
|
||||
frequency: 30,
|
||||
blendMode: 'ADD',
|
||||
|
||||
// 🌊 DETECT WHEN RAINDROP HITS GROUND
|
||||
deathCallback: (particle) => {
|
||||
// Convert camera-relative position to world position
|
||||
const worldX = particle.x + cam.scrollX;
|
||||
const worldY = particle.y + cam.scrollY;
|
||||
|
||||
// Check if hit water tile
|
||||
this.checkRainImpactOnWater(worldX, worldY);
|
||||
}
|
||||
});
|
||||
|
||||
this.rainEmitter.setScrollFactor(0);
|
||||
this.rainEmitter.setDepth(999999);
|
||||
this.rainEmitter.stop();
|
||||
}
|
||||
|
||||
// ❄️ SNOW SYSTEM
|
||||
startSnow() {
|
||||
if (!this.snowEmitter) {
|
||||
this.createSnowParticles();
|
||||
}
|
||||
|
||||
this.snowEmitter.start();
|
||||
console.log('❄️ Snow started!');
|
||||
}
|
||||
|
||||
createSnowParticles() {
|
||||
if (!this.textures.exists('snowflake')) {
|
||||
const graphics = this.make.graphics({ x: 0, y: 0, add: false });
|
||||
graphics.fillStyle(0xffffff, 1);
|
||||
graphics.fillCircle(2, 2, 2);
|
||||
graphics.generateTexture('snowflake', 4, 4);
|
||||
graphics.destroy();
|
||||
}
|
||||
|
||||
const cam = this.cameras.main;
|
||||
this.snowEmitter = this.add.particles(0, 0, 'snowflake', {
|
||||
x: { min: 0, max: cam.width },
|
||||
y: -50,
|
||||
lifespan: 8000,
|
||||
speedY: { min: 50, max: 150 },
|
||||
speedX: { min: -30, max: 30 },
|
||||
scale: { start: 0.5, end: 1.5 },
|
||||
alpha: { start: 0.8, end: 0.3 },
|
||||
quantity: 2,
|
||||
frequency: 80,
|
||||
blendMode: 'ADD'
|
||||
});
|
||||
|
||||
this.snowEmitter.setScrollFactor(0);
|
||||
this.snowEmitter.setDepth(999999);
|
||||
this.snowEmitter.stop();
|
||||
}
|
||||
|
||||
// ⚡ STORM SYSTEM
|
||||
startStorm() {
|
||||
if (!this.stormEmitter) {
|
||||
this.createStormParticles();
|
||||
}
|
||||
|
||||
this.stormEmitter.start();
|
||||
console.log('⛈️ Storm started!');
|
||||
|
||||
// Lightning flashes
|
||||
this.lightningTimer = this.time.addEvent({
|
||||
delay: Phaser.Math.Between(3000, 8000),
|
||||
callback: () => this.triggerLightning(),
|
||||
loop: true
|
||||
});
|
||||
|
||||
// Puddles (faster spawn)
|
||||
this.puddleTimer = this.time.addEvent({
|
||||
delay: 2000,
|
||||
callback: () => this.spawnPuddle(),
|
||||
loop: true
|
||||
});
|
||||
}
|
||||
|
||||
createStormParticles() {
|
||||
if (!this.textures.exists('raindrop')) {
|
||||
this.createRainParticles();
|
||||
}
|
||||
|
||||
const cam = this.cameras.main;
|
||||
this.stormEmitter = this.add.particles(0, 0, 'raindrop', {
|
||||
x: { min: 0, max: cam.width },
|
||||
y: -50,
|
||||
lifespan: 2000,
|
||||
speedY: { min: 600, max: 900 },
|
||||
speedX: { min: 50, max: 150 },
|
||||
scale: { start: 1.5, end: 0.5 },
|
||||
alpha: { start: 0.8, end: 0.3 },
|
||||
quantity: 5,
|
||||
frequency: 20,
|
||||
blendMode: 'ADD'
|
||||
});
|
||||
|
||||
this.stormEmitter.setScrollFactor(0);
|
||||
this.stormEmitter.setDepth(999999);
|
||||
this.stormEmitter.stop();
|
||||
}
|
||||
|
||||
triggerLightning() {
|
||||
// White flash overlay
|
||||
const flash = this.add.rectangle(
|
||||
this.cameras.main.scrollX,
|
||||
this.cameras.main.scrollY,
|
||||
this.cameras.main.width,
|
||||
this.cameras.main.height,
|
||||
0xffffff,
|
||||
0.8
|
||||
);
|
||||
flash.setOrigin(0, 0);
|
||||
flash.setScrollFactor(0);
|
||||
flash.setDepth(1000000);
|
||||
|
||||
// Fade out
|
||||
this.tweens.add({
|
||||
targets: flash,
|
||||
alpha: 0,
|
||||
duration: 200,
|
||||
onComplete: () => flash.destroy()
|
||||
});
|
||||
|
||||
// Screen shake
|
||||
this.cameras.main.shake(200, 0.005);
|
||||
|
||||
// Thunder sound (if available)
|
||||
if (this.soundManager && this.soundManager.playThunderSound) {
|
||||
this.time.delayedCall(300, () => {
|
||||
this.soundManager.playThunderSound();
|
||||
});
|
||||
}
|
||||
|
||||
console.log('⚡ Lightning strike!');
|
||||
}
|
||||
|
||||
// 🌫️ FOG SYSTEM
|
||||
startFog() {
|
||||
if (!this.fogOverlay) {
|
||||
this.fogOverlay = this.add.rectangle(
|
||||
0, 0,
|
||||
this.cameras.main.width * 2,
|
||||
this.cameras.main.height * 2,
|
||||
0xcccccc,
|
||||
0
|
||||
);
|
||||
this.fogOverlay.setOrigin(0, 0);
|
||||
this.fogOverlay.setScrollFactor(0);
|
||||
this.fogOverlay.setDepth(500000);
|
||||
}
|
||||
|
||||
this.tweens.add({
|
||||
targets: this.fogOverlay,
|
||||
alpha: 0.4,
|
||||
duration: 3000
|
||||
});
|
||||
|
||||
console.log('🌫️ Fog started!');
|
||||
}
|
||||
|
||||
|
||||
// 💧 PUDDLES SYSTEM (Spawn on grass/dirt where rain lands!)
|
||||
spawnPuddle() {
|
||||
if (!this.terrainSystem) return;
|
||||
|
||||
const cam = this.cameras.main;
|
||||
const worldX = cam.scrollX + Phaser.Math.Between(100, cam.width - 100);
|
||||
const worldY = cam.scrollY + Phaser.Math.Between(100, cam.height - 100);
|
||||
|
||||
// 🎨 FLAT 2D CONVERSION (NEW!)
|
||||
const tileSize = 48;
|
||||
const x = Math.floor(worldX / tileSize);
|
||||
const y = Math.floor(worldY / tileSize);
|
||||
|
||||
// Get tile type
|
||||
const tile = this.terrainSystem.getTile(x, y);
|
||||
|
||||
// ONLY spawn puddles on grass or dirt!
|
||||
if (!tile || (tile.type !== 'grass' && tile.type !== 'dirt' && tile.type !== 'farmland')) {
|
||||
return; // Skip - not valid surface
|
||||
}
|
||||
|
||||
// Limit max puddles
|
||||
if (this.puddles.length >= 15) {
|
||||
const oldest = this.puddles.shift();
|
||||
if (oldest && oldest.active) oldest.destroy();
|
||||
}
|
||||
|
||||
// Create puddle SPRITE (realistic!)
|
||||
const puddle = this.add.image(worldX, worldY, 'luza_sprite');
|
||||
puddle.setOrigin(0.5, 0.5);
|
||||
puddle.setScale(1.5); // BIGGER - more visible!
|
||||
puddle.setDepth(10); // ABOVE terrain
|
||||
puddle.setScrollFactor(1);
|
||||
puddle.setAlpha(0); // Start invisible
|
||||
|
||||
// Fade in
|
||||
this.tweens.add({
|
||||
targets: puddle,
|
||||
alpha: 0.35,
|
||||
duration: 2000
|
||||
});
|
||||
|
||||
this.puddles.push(puddle);
|
||||
|
||||
// Auto cleanup after 30 seconds
|
||||
this.time.delayedCall(30000, () => {
|
||||
if (puddle && puddle.active) {
|
||||
this.tweens.add({
|
||||
targets: puddle,
|
||||
alpha: 0,
|
||||
duration: 3000,
|
||||
onComplete: () => {
|
||||
puddle.destroy();
|
||||
const index = this.puddles.indexOf(puddle);
|
||||
if (index > -1) this.puddles.splice(index, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Random splash effects
|
||||
this.time.addEvent({
|
||||
delay: Phaser.Math.Between(1000, 3000),
|
||||
callback: () => {
|
||||
if (puddle && puddle.active && (this.currentWeather === 'rain' || this.currentWeather === 'storm')) {
|
||||
this.createSplash(puddle.x, puddle.y);
|
||||
}
|
||||
},
|
||||
loop: true,
|
||||
repeat: 5
|
||||
});
|
||||
}
|
||||
|
||||
// 🌊 CHECK IF RAIN HIT WATER TILE
|
||||
checkRainImpactOnWater(worldX, worldY) {
|
||||
if (!this.terrainSystem) return;
|
||||
|
||||
// 🎨 FLAT 2D CONVERSION (NEW!)
|
||||
const tileSize = 48;
|
||||
const x = Math.floor(worldX / tileSize);
|
||||
const y = Math.floor(worldY / tileSize);
|
||||
|
||||
// Get tile at position
|
||||
const tile = this.terrainSystem.getTile(x, y);
|
||||
|
||||
// If water tile, create ripple!
|
||||
if (tile && tile.type === 'water') {
|
||||
this.createWaterRipple(worldX, worldY);
|
||||
}
|
||||
// If grass/dirt, 3% chance to spawn puddle
|
||||
else if (tile && (tile.type === 'grass' || tile.type === 'dirt' || tile.type === 'farmland')) {
|
||||
if (Math.random() < 0.03) {
|
||||
this.spawnPuddleAtLocation(worldX, worldY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 💧 CREATE WATER RIPPLE EFFECT
|
||||
createWaterRipple(x, y) {
|
||||
// Small expanding circle on water surface
|
||||
const ripple = this.add.circle(x, y, 2, 0xffffff, 0.5);
|
||||
ripple.setDepth(500); // Above water tiles
|
||||
ripple.setScrollFactor(1); // World-bound
|
||||
|
||||
this.tweens.add({
|
||||
targets: ripple,
|
||||
radius: 10,
|
||||
alpha: 0,
|
||||
duration: 400,
|
||||
ease: 'Quad.easeOut',
|
||||
onComplete: () => ripple.destroy()
|
||||
});
|
||||
}
|
||||
|
||||
// 💧 SPAWN PUDDLE AT EXACT LOCATION (from rain impact)
|
||||
spawnPuddleAtLocation(worldX, worldY) {
|
||||
// Limit max puddles
|
||||
if (this.puddles.length >= 15) {
|
||||
// Remove oldest puddle
|
||||
const oldest = this.puddles.shift();
|
||||
if (oldest && oldest.active) oldest.destroy();
|
||||
}
|
||||
|
||||
// Create puddle SPRITE (realistic!)
|
||||
const puddle = this.add.image(worldX, worldY, 'luza_sprite');
|
||||
puddle.setOrigin(0.5, 0.5);
|
||||
puddle.setScale(1.5); // BIGGER - more visible!
|
||||
puddle.setDepth(10); // ABOVE terrain
|
||||
puddle.setScrollFactor(1);
|
||||
puddle.setAlpha(0); // Start invisible
|
||||
|
||||
// Fade in
|
||||
this.tweens.add({
|
||||
targets: puddle,
|
||||
alpha: 0.35,
|
||||
duration: 2000
|
||||
});
|
||||
|
||||
this.puddles.push(puddle);
|
||||
|
||||
// Auto cleanup after 30 seconds
|
||||
this.time.delayedCall(30000, () => {
|
||||
if (puddle && puddle.active) {
|
||||
this.tweens.add({
|
||||
targets: puddle,
|
||||
alpha: 0,
|
||||
duration: 3000,
|
||||
onComplete: () => {
|
||||
puddle.destroy();
|
||||
const index = this.puddles.indexOf(puddle);
|
||||
if (index > -1) this.puddles.splice(index, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 💦 SPLASH EFFECT (Ripples)
|
||||
createSplash(x, y) {
|
||||
// Concentric circles
|
||||
for (let i = 0; i < 3; i++) {
|
||||
this.time.delayedCall(i * 100, () => {
|
||||
const circle = this.add.circle(x, y, 3, 0xffffff, 0.5);
|
||||
circle.setDepth(2);
|
||||
|
||||
this.tweens.add({
|
||||
targets: circle,
|
||||
radius: 20 + (i * 5),
|
||||
alpha: 0,
|
||||
duration: 600,
|
||||
onComplete: () => circle.destroy()
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
update(time, delta) {
|
||||
@@ -862,9 +1527,13 @@ class GameScene extends Phaser.Scene {
|
||||
});
|
||||
}
|
||||
|
||||
// Weather UI Update
|
||||
if (this.weatherUI) this.weatherUI.update();
|
||||
|
||||
// Update Systems
|
||||
if (this.terrainSystem) this.terrainSystem.update(time, delta); // Water animation!
|
||||
if (this.statsSystem) this.statsSystem.update(delta);
|
||||
if (this.craftingSystem) this.craftingSystem.update(delta); // 🛠️ Crafting progress
|
||||
if (this.lootSystem) this.lootSystem.update(delta);
|
||||
if (this.interactionSystem) this.interactionSystem.update(delta);
|
||||
if (this.farmingSystem) this.farmingSystem.update(delta);
|
||||
@@ -948,26 +1617,30 @@ class GameScene extends Phaser.Scene {
|
||||
// Parallax Logic
|
||||
if (this.parallaxSystem && this.player) {
|
||||
const playerPos = this.player.getPosition();
|
||||
const screenPos = this.iso.toScreen(playerPos.x, playerPos.y);
|
||||
this.parallaxSystem.update(
|
||||
screenPos.x + this.terrainOffsetX,
|
||||
screenPos.y + this.terrainOffsetY
|
||||
);
|
||||
// 🎨 FLAT 2D (NEW!) - Direct position, no conversion
|
||||
const tileSize = 48;
|
||||
const screenX = playerPos.x * tileSize + tileSize / 2;
|
||||
const screenY = playerPos.y * tileSize + tileSize / 2;
|
||||
this.parallaxSystem.update(screenX, screenY);
|
||||
}
|
||||
|
||||
// Terrain Culling & Update
|
||||
// Terrain Update
|
||||
if (this.terrainSystem) {
|
||||
this.terrainSystem.updateCulling(this.cameras.main);
|
||||
// Note: Flat2D doesn't need culling (already optimized)
|
||||
// this.terrainSystem.updateCulling(this.cameras.main);
|
||||
this.terrainSystem.update(delta);
|
||||
}
|
||||
|
||||
|
||||
// Clouds
|
||||
if (this.clouds) {
|
||||
for (const cloud of this.clouds) {
|
||||
cloud.sprite.x += cloud.speed * (delta / 1000);
|
||||
if (cloud.sprite.x > this.terrainOffsetX + 2000) {
|
||||
cloud.sprite.x = this.terrainOffsetX - 2000;
|
||||
cloud.sprite.y = Phaser.Math.Between(0, 1000);
|
||||
if (cloud && cloud.sprite) {
|
||||
cloud.sprite.x += cloud.speed * (delta / 1000);
|
||||
if (cloud.sprite.x > this.terrainOffsetX + 2000) {
|
||||
cloud.sprite.x = this.terrainOffsetX - 2000;
|
||||
cloud.sprite.y = Phaser.Math.Between(0, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1138,12 +1811,12 @@ class GameScene extends Phaser.Scene {
|
||||
if (this.saveSystem) this.saveSystem.loadGame();
|
||||
}
|
||||
|
||||
initializeFarmWorld() {
|
||||
console.log('🌾 Initializing Farm Area (Starter Zone)...');
|
||||
initializeFarmWorld(spawnX = 20, spawnY = 20) {
|
||||
console.log(`🌾 Initializing 8x8 Farm Area at (${spawnX}, ${spawnY})...`);
|
||||
|
||||
const farmX = 20; // Farm center
|
||||
const farmY = 20;
|
||||
const farmRadius = 8;
|
||||
const farmX = spawnX; // Farm center (custom spawn)
|
||||
const farmY = spawnY;
|
||||
const farmRadius = 4; // 8x8 = radius 4 (4 tiles in each direction)
|
||||
|
||||
// 1. Clear farm area (odstrani drevesa in kamne)
|
||||
for (let x = farmX - farmRadius; x <= farmX + farmRadius; x++) {
|
||||
@@ -1154,27 +1827,25 @@ class GameScene extends Phaser.Scene {
|
||||
if (this.terrainSystem.decorationsMap.has(key)) {
|
||||
this.terrainSystem.removeDecoration(x, y);
|
||||
}
|
||||
// Make it DIRT for farming
|
||||
|
||||
// Change terrain to grass
|
||||
if (this.terrainSystem.tiles[y] && this.terrainSystem.tiles[y][x]) {
|
||||
this.terrainSystem.tiles[y][x].type = 'dirt';
|
||||
this.terrainSystem.tiles[y][x].type = 'grass';
|
||||
this.terrainSystem.tiles[y][x].solid = false;
|
||||
if (this.terrainSystem.tiles[y][x].sprite) {
|
||||
this.terrainSystem.tiles[y][x].sprite.setTexture('dirt');
|
||||
this.terrainSystem.tiles[y][x].sprite.setTint(0xffffff); // Clear tint
|
||||
this.terrainSystem.tiles[y][x].sprite.setTexture('grass');
|
||||
this.terrainSystem.tiles[y][x].sprite.clearTint();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Place starter resources (chest s semeni) - REMOVED PER USER REQUEST (Floating chest bug)
|
||||
// this.terrainSystem.placeStructure(farmX + 3, farmY + 3, 'chest');
|
||||
|
||||
// 3. Place FULL FENCE around farm
|
||||
// console.log('🚧 Building Farm Fence...');
|
||||
// const minX = farmX - farmRadius;
|
||||
// const maxX = farmX + farmRadius;
|
||||
// const minY = farmY - farmRadius;
|
||||
// const maxY = farmY + farmRadius;
|
||||
// 2. Optional: Add fence around farm (commented out)
|
||||
// const minX = farmX - farmRadius - 1;
|
||||
// const maxX = farmX + farmRadius + 1;
|
||||
// const minY = farmY - farmRadius - 1;
|
||||
// const maxY = farmY + farmRadius + 1;
|
||||
|
||||
// // Top and bottom horizontal fences
|
||||
// for (let x = minX; x <= maxX; x++) {
|
||||
@@ -1192,7 +1863,7 @@ class GameScene extends Phaser.Scene {
|
||||
// }
|
||||
// }
|
||||
|
||||
console.log('✅ Farm Area Initialized at (20,20)');
|
||||
console.log(`✅ 8x8 Farm Area Initialized at (${spawnX},${spawnY})`);
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
|
||||
@@ -22,6 +22,9 @@ class PreloadScene extends Phaser.Scene {
|
||||
this.load.image('wheat_sprite', 'assets/wheat_sprite.png');
|
||||
this.load.image('stone_texture', 'assets/stone_texture.png');
|
||||
|
||||
// 💧 WEATHER EFFECTS
|
||||
this.load.image('luza_sprite', 'assets/sprites/luza.png'); // Puddle sprite
|
||||
|
||||
// New asset packs
|
||||
this.load.image('objects_pack', 'assets/objects_pack.png');
|
||||
this.load.image('walls_pack', 'assets/walls_pack.png');
|
||||
|
||||
344
src/systems/CraftingSystem.js
Normal file
344
src/systems/CraftingSystem.js
Normal file
@@ -0,0 +1,344 @@
|
||||
// Crafting System - Handles recipe management and item crafting
|
||||
class CraftingSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
this.recipes = {};
|
||||
this.categories = [];
|
||||
this.unlockedRecipes = new Set();
|
||||
|
||||
// Crafting queue
|
||||
this.craftingQueue = [];
|
||||
this.isCrafting = false;
|
||||
this.currentCraft = null;
|
||||
this.craftProgress = 0;
|
||||
|
||||
console.log('🛠️ CraftingSystem initialized');
|
||||
}
|
||||
|
||||
async loadRecipes() {
|
||||
try {
|
||||
// Load recipes from JSON file
|
||||
const response = await fetch('data/recipes.json');
|
||||
const data = await response.json();
|
||||
|
||||
this.recipes = data.recipes;
|
||||
this.categories = data.categories;
|
||||
|
||||
// Initialize unlocked recipes
|
||||
Object.keys(this.recipes).forEach(recipeId => {
|
||||
const recipe = this.recipes[recipeId];
|
||||
if (recipe.unlocked) {
|
||||
this.unlockedRecipes.add(recipeId);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`✅ Loaded ${Object.keys(this.recipes).length} recipes`);
|
||||
console.log(`🔓 ${this.unlockedRecipes.size} unlocked recipes`);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to load recipes:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get all recipes (optionally filtered by category)
|
||||
getRecipes(category = 'all') {
|
||||
const recipeList = Object.values(this.recipes);
|
||||
|
||||
if (category === 'all') {
|
||||
return recipeList;
|
||||
}
|
||||
|
||||
return recipeList.filter(recipe => recipe.category === category);
|
||||
}
|
||||
|
||||
// Get only unlocked recipes
|
||||
getUnlockedRecipes(category = 'all') {
|
||||
return this.getRecipes(category).filter(recipe =>
|
||||
this.unlockedRecipes.has(recipe.id)
|
||||
);
|
||||
}
|
||||
|
||||
// Check if recipe is unlocked
|
||||
isUnlocked(recipeId) {
|
||||
return this.unlockedRecipes.has(recipeId);
|
||||
}
|
||||
|
||||
// Unlock a recipe
|
||||
unlockRecipe(recipeId) {
|
||||
if (this.recipes[recipeId]) {
|
||||
this.unlockedRecipes.add(recipeId);
|
||||
console.log(`🔓 Unlocked recipe: ${this.recipes[recipeId].name}`);
|
||||
|
||||
// Notify UI
|
||||
this.scene.events.emit('recipe-unlocked', recipeId);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if player has required ingredients
|
||||
canCraft(recipeId) {
|
||||
const recipe = this.recipes[recipeId];
|
||||
if (!recipe) return false;
|
||||
|
||||
// Check if unlocked
|
||||
if (!this.isUnlocked(recipeId)) {
|
||||
return { canCraft: false, reason: 'locked' };
|
||||
}
|
||||
|
||||
// Check ingredients
|
||||
const inventory = this.scene.inventorySystem;
|
||||
if (!inventory) {
|
||||
return { canCraft: false, reason: 'no_inventory' };
|
||||
}
|
||||
|
||||
const missing = [];
|
||||
|
||||
for (const [itemId, requiredAmount] of Object.entries(recipe.ingredients)) {
|
||||
const hasAmount = inventory.getItemCount(itemId);
|
||||
|
||||
if (hasAmount < requiredAmount) {
|
||||
missing.push({
|
||||
item: itemId,
|
||||
required: requiredAmount,
|
||||
has: hasAmount,
|
||||
need: requiredAmount - hasAmount
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
return { canCraft: false, reason: 'missing_ingredients', missing };
|
||||
}
|
||||
|
||||
return { canCraft: true };
|
||||
}
|
||||
|
||||
// Start crafting an item
|
||||
craftItem(recipeId) {
|
||||
const recipe = this.recipes[recipeId];
|
||||
if (!recipe) {
|
||||
console.warn(`⚠️ Recipe not found: ${recipeId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if can craft
|
||||
const check = this.canCraft(recipeId);
|
||||
if (!check.canCraft) {
|
||||
console.warn(`⚠️ Cannot craft ${recipe.name}: ${check.reason}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Consume ingredients
|
||||
const inventory = this.scene.inventorySystem;
|
||||
for (const [itemId, amount] of Object.entries(recipe.ingredients)) {
|
||||
inventory.removeItem(itemId, amount);
|
||||
}
|
||||
|
||||
// Add to crafting queue
|
||||
this.craftingQueue.push({
|
||||
recipeId: recipeId,
|
||||
recipe: recipe,
|
||||
startTime: Date.now(),
|
||||
duration: recipe.craftTime || 1000
|
||||
});
|
||||
|
||||
console.log(`🔨 Started crafting: ${recipe.name}`);
|
||||
|
||||
// Start crafting if not already crafting
|
||||
if (!this.isCrafting) {
|
||||
this.startNextCraft();
|
||||
}
|
||||
|
||||
// Emit event
|
||||
this.scene.events.emit('craft-started', recipeId);
|
||||
|
||||
// Play sound
|
||||
if (this.scene.soundManager && this.scene.soundManager.playCraftSound) {
|
||||
this.scene.soundManager.playCraftSound();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Start next item in queue
|
||||
startNextCraft() {
|
||||
if (this.craftingQueue.length === 0) {
|
||||
this.isCrafting = false;
|
||||
this.currentCraft = null;
|
||||
this.craftProgress = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
this.isCrafting = true;
|
||||
this.currentCraft = this.craftingQueue[0];
|
||||
this.craftProgress = 0;
|
||||
|
||||
console.log(`⏳ Processing: ${this.currentCraft.recipe.name}`);
|
||||
}
|
||||
|
||||
// Update crafting progress
|
||||
update(delta) {
|
||||
if (!this.isCrafting || !this.currentCraft) return;
|
||||
|
||||
const elapsed = Date.now() - this.currentCraft.startTime;
|
||||
this.craftProgress = Math.min(1.0, elapsed / this.currentCraft.duration);
|
||||
|
||||
// Check if finished
|
||||
if (this.craftProgress >= 1.0) {
|
||||
this.completeCraft();
|
||||
}
|
||||
|
||||
// Emit progress event for UI
|
||||
this.scene.events.emit('craft-progress', {
|
||||
recipe: this.currentCraft.recipe,
|
||||
progress: this.craftProgress
|
||||
});
|
||||
}
|
||||
|
||||
// Complete current craft
|
||||
completeCraft() {
|
||||
if (!this.currentCraft) return;
|
||||
|
||||
const recipe = this.currentCraft.recipe;
|
||||
|
||||
// Add result to inventory
|
||||
const inventory = this.scene.inventorySystem;
|
||||
inventory.addItem(recipe.result.item, recipe.result.quantity);
|
||||
|
||||
console.log(`✅ Crafted: ${recipe.result.quantity}x ${recipe.name}`);
|
||||
|
||||
// Emit event
|
||||
this.scene.events.emit('craft-complete', {
|
||||
recipeId: recipe.id,
|
||||
item: recipe.result.item,
|
||||
quantity: recipe.result.quantity
|
||||
});
|
||||
|
||||
// Play sound
|
||||
if (this.scene.soundManager && this.scene.soundManager.playSuccessSound) {
|
||||
this.scene.soundManager.playSuccessSound();
|
||||
}
|
||||
|
||||
// Show floating text
|
||||
if (this.scene.player && this.scene.player.sprite) {
|
||||
const text = `+${recipe.result.quantity} ${recipe.name}`;
|
||||
this.scene.events.emit('floating-text', {
|
||||
x: this.scene.player.sprite.x,
|
||||
y: this.scene.player.sprite.y - 50,
|
||||
text: text,
|
||||
color: '#00ff00'
|
||||
});
|
||||
}
|
||||
|
||||
// Remove from queue
|
||||
this.craftingQueue.shift();
|
||||
|
||||
// Start next craft
|
||||
this.startNextCraft();
|
||||
}
|
||||
|
||||
// Cancel current craft
|
||||
cancelCraft() {
|
||||
if (!this.currentCraft) return false;
|
||||
|
||||
const recipe = this.currentCraft.recipe;
|
||||
|
||||
// Refund ingredients (partial refund based on progress)
|
||||
const refundPercent = 1.0 - this.craftProgress;
|
||||
const inventory = this.scene.inventorySystem;
|
||||
|
||||
for (const [itemId, amount] of Object.entries(recipe.ingredients)) {
|
||||
const refundAmount = Math.floor(amount * refundPercent);
|
||||
if (refundAmount > 0) {
|
||||
inventory.addItem(itemId, refundAmount);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`❌ Cancelled crafting: ${recipe.name} (${Math.floor(refundPercent * 100)}% refund)`);
|
||||
|
||||
// Remove from queue
|
||||
this.craftingQueue.shift();
|
||||
|
||||
// Emit event
|
||||
this.scene.events.emit('craft-cancelled', recipe.id);
|
||||
|
||||
// Start next
|
||||
this.startNextCraft();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get current crafting info
|
||||
getCurrentCraft() {
|
||||
if (!this.currentCraft) return null;
|
||||
|
||||
return {
|
||||
recipe: this.currentCraft.recipe,
|
||||
progress: this.craftProgress,
|
||||
timeRemaining: this.currentCraft.duration * (1 - this.craftProgress)
|
||||
};
|
||||
}
|
||||
|
||||
// Get queue length
|
||||
getQueueLength() {
|
||||
return this.craftingQueue.length;
|
||||
}
|
||||
|
||||
// Clear entire queue
|
||||
clearQueue() {
|
||||
// Refund all queued items
|
||||
this.craftingQueue.forEach(craft => {
|
||||
const inventory = this.scene.inventorySystem;
|
||||
for (const [itemId, amount] of Object.entries(craft.recipe.ingredients)) {
|
||||
inventory.addItem(itemId, amount);
|
||||
}
|
||||
});
|
||||
|
||||
this.craftingQueue = [];
|
||||
this.isCrafting = false;
|
||||
this.currentCraft = null;
|
||||
this.craftProgress = 0;
|
||||
|
||||
console.log('🗑️ Cleared crafting queue');
|
||||
}
|
||||
|
||||
// Save crafting state
|
||||
getSaveData() {
|
||||
return {
|
||||
unlockedRecipes: Array.from(this.unlockedRecipes),
|
||||
craftingQueue: this.craftingQueue.map(craft => ({
|
||||
recipeId: craft.recipeId,
|
||||
startTime: craft.startTime,
|
||||
duration: craft.duration
|
||||
})),
|
||||
currentProgress: this.craftProgress
|
||||
};
|
||||
}
|
||||
|
||||
// Load crafting state
|
||||
loadSaveData(data) {
|
||||
if (!data) return;
|
||||
|
||||
// Restore unlocked recipes
|
||||
if (data.unlockedRecipes) {
|
||||
this.unlockedRecipes = new Set(data.unlockedRecipes);
|
||||
}
|
||||
|
||||
// Restore crafting queue
|
||||
if (data.craftingQueue && data.craftingQueue.length > 0) {
|
||||
this.craftingQueue = data.craftingQueue.map(saved => ({
|
||||
recipeId: saved.recipeId,
|
||||
recipe: this.recipes[saved.recipeId],
|
||||
startTime: saved.startTime,
|
||||
duration: saved.duration
|
||||
}));
|
||||
|
||||
this.startNextCraft();
|
||||
}
|
||||
|
||||
console.log('💾 Loaded crafting state');
|
||||
}
|
||||
}
|
||||
386
src/systems/Flat2DTerrainSystem.js
Normal file
386
src/systems/Flat2DTerrainSystem.js
Normal file
@@ -0,0 +1,386 @@
|
||||
// Flat2DTerrainSystem - Complete 2D top-down tile rendering
|
||||
// Replaces isometric TerrainSystem for Stardew Valley style
|
||||
|
||||
class Flat2DTerrainSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
this.tileSize = 48;
|
||||
this.width = 100;
|
||||
this.height = 100;
|
||||
|
||||
// Tile map data
|
||||
this.tiles = [];
|
||||
|
||||
// Rendering containers
|
||||
this.groundLayer = null;
|
||||
this.pathsLayer = null;
|
||||
this.decorLayer = null;
|
||||
|
||||
// Textures ready flag
|
||||
this.texturesReady = false;
|
||||
|
||||
console.log('🎨 Flat2DTerrainSystem initialized');
|
||||
}
|
||||
|
||||
async generate() {
|
||||
console.log('🗺️ Generating flat 2D map...');
|
||||
|
||||
// Create textures first
|
||||
this.createTileTextures();
|
||||
|
||||
// Load map data
|
||||
if (typeof Map2DData !== 'undefined') {
|
||||
this.tiles = Map2DData.generateMap();
|
||||
console.log('✅ Map data generated:', this.tiles.length, 'rows');
|
||||
} else {
|
||||
console.error('❌ Map2DData not loaded!');
|
||||
this.createFallbackMap();
|
||||
}
|
||||
|
||||
// Render the map
|
||||
this.renderMap();
|
||||
|
||||
console.log('✅ Flat 2D map ready!');
|
||||
}
|
||||
|
||||
createTileTextures() {
|
||||
const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
|
||||
const size = this.tileSize;
|
||||
|
||||
// GRASS - VIBRANT RICH GREEN! 🌿
|
||||
graphics.clear();
|
||||
graphics.fillStyle(0x59b36a); // BRIGHT rich green!
|
||||
graphics.fillRect(0, 0, size, size);
|
||||
|
||||
// Add grass texture - DARKER spots
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const x = Math.random() * size;
|
||||
const y = Math.random() * size;
|
||||
graphics.fillStyle(0x3a8d4f, 0.5);
|
||||
graphics.fillCircle(x, y, 2 + Math.random() * 3);
|
||||
}
|
||||
// LIGHTER highlights
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const x = Math.random() * size;
|
||||
const y = Math.random() * size;
|
||||
graphics.fillStyle(0x7ad389, 0.6);
|
||||
graphics.fillCircle(x, y, 1.5);
|
||||
}
|
||||
graphics.generateTexture('tile2d_grass', size, size);
|
||||
|
||||
// GRASS WITH FLOWERS
|
||||
graphics.clear();
|
||||
graphics.fillStyle(0x4a9d5f);
|
||||
graphics.fillRect(0, 0, size, size);
|
||||
|
||||
// Grass texture
|
||||
for (let i = 0; i < 10; i++) {
|
||||
graphics.fillStyle(0x3a8d4f, 0.4);
|
||||
graphics.fillCircle(Math.random() * size, Math.random() * size, 1.5);
|
||||
}
|
||||
|
||||
// Small flowers
|
||||
const flowerColors = [0xff6b6b, 0xffd93d, 0x6bcbff];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
graphics.fillStyle(flowerColors[Math.floor(Math.random() * 3)]);
|
||||
graphics.fillCircle(Math.random() * size, Math.random() * size, 2);
|
||||
}
|
||||
graphics.generateTexture('tile2d_grass_flowers', size, size);
|
||||
|
||||
// DIRT - VIBRANT BROWN! 🟤
|
||||
graphics.clear();
|
||||
graphics.fillStyle(0xa87f5a); // BRIGHT brown!
|
||||
graphics.fillRect(0, 0, size, size);
|
||||
|
||||
// Dirt texture - darker clumps
|
||||
for (let i = 0; i < 20; i++) {
|
||||
graphics.fillStyle(0x7a5f3a, 0.6);
|
||||
graphics.fillCircle(Math.random() * size, Math.random() * size, 3 + Math.random() * 4);
|
||||
}
|
||||
// Lighter spots
|
||||
for (let i = 0; i < 12; i++) {
|
||||
graphics.fillStyle(0xc89f6f, 0.5);
|
||||
graphics.fillCircle(Math.random() * size, Math.random() * size, 2);
|
||||
}
|
||||
graphics.generateTexture('tile2d_dirt', size, size);
|
||||
|
||||
// DIRT EDGE - Transition to grass
|
||||
graphics.clear();
|
||||
graphics.fillGradientStyle(0x8b6f47, 0x8b6f47, 0x6a9d5f, 0x6a9d5f, 1);
|
||||
graphics.fillRect(0, 0, size, size);
|
||||
graphics.generateTexture('tile2d_dirt_edge', size, size);
|
||||
|
||||
// WATER - BRIGHT BLUE! 💧
|
||||
graphics.clear();
|
||||
graphics.fillStyle(0x3498db); // VIBRANT blue!
|
||||
graphics.fillRect(0, 0, size, size);
|
||||
|
||||
// Water highlights - darker depth
|
||||
for (let i = 0; i < 8; i++) {
|
||||
graphics.fillStyle(0x2078ab, 0.4);
|
||||
graphics.fillCircle(Math.random() * size, Math.random() * size, 4 + Math.random() * 6);
|
||||
}
|
||||
// Light reflections
|
||||
for (let i = 0; i < 12; i++) {
|
||||
graphics.fillStyle(0x5dade2, 0.5);
|
||||
graphics.fillCircle(Math.random() * size, Math.random() * size, 2);
|
||||
}
|
||||
// White sparkles
|
||||
for (let i = 0; i < 10; i++) {
|
||||
graphics.fillStyle(0xffffff, 0.6);
|
||||
graphics.fillCircle(Math.random() * size, Math.random() * size, 1);
|
||||
}
|
||||
graphics.generateTexture('tile2d_water', size, size);
|
||||
|
||||
// WATER EDGE - Lighter border
|
||||
graphics.clear();
|
||||
graphics.fillGradientStyle(0x4aacdc, 0x4aacdc, 0x1a5f7a, 0x1a5f7a, 0.7);
|
||||
graphics.fillRect(0, 0, size, size);
|
||||
graphics.generateTexture('tile2d_water_edge', size, size);
|
||||
|
||||
// STONE - Gray
|
||||
graphics.clear();
|
||||
graphics.fillStyle(0x808080);
|
||||
graphics.fillRect(0, 0, size, size);
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
graphics.fillStyle(0x606060, 0.6);
|
||||
graphics.fillCircle(Math.random() * size, Math.random() * size, 2 + Math.random() * 3);
|
||||
}
|
||||
graphics.generateTexture('tile2d_stone', size, size);
|
||||
|
||||
graphics.destroy();
|
||||
|
||||
this.texturesReady = true;
|
||||
console.log('✅ Tile textures created');
|
||||
}
|
||||
|
||||
renderMap() {
|
||||
// Create layer containers
|
||||
this.groundLayer = this.scene.add.container(0, 0);
|
||||
this.pathsLayer = this.scene.add.container(0, 0);
|
||||
this.decorLayer = this.scene.add.container(0, 0);
|
||||
|
||||
// Set depths
|
||||
this.groundLayer.setDepth(1);
|
||||
this.pathsLayer.setDepth(2);
|
||||
this.decorLayer.setDepth(3);
|
||||
|
||||
const size = this.tileSize;
|
||||
|
||||
// Render all tiles
|
||||
for (let y = 0; y < this.height; y++) {
|
||||
for (let x = 0; x < this.width; x++) {
|
||||
const tile = this.tiles[y][x];
|
||||
const worldX = x * size;
|
||||
const worldY = y * size;
|
||||
|
||||
// Get texture key from tile type
|
||||
const textureKey = this.getTileTexture(tile.base);
|
||||
|
||||
// Create tile sprite
|
||||
const tileSprite = this.scene.add.image(worldX, worldY, textureKey);
|
||||
tileSprite.setOrigin(0, 0);
|
||||
tileSprite.setDisplaySize(size, size);
|
||||
|
||||
// Add to appropriate layer
|
||||
if (tile.base <= 1) {
|
||||
this.groundLayer.add(tileSprite);
|
||||
} else {
|
||||
this.pathsLayer.add(tileSprite);
|
||||
}
|
||||
|
||||
// Add decoration if exists
|
||||
if (tile.decoration) {
|
||||
this.addDecoration(x, y, tile.decoration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ Map rendered: 3 layers created');
|
||||
}
|
||||
|
||||
getTileTexture(tileType) {
|
||||
const types = Map2DData.tileTypes;
|
||||
|
||||
switch (tileType) {
|
||||
case types.GRASS: return 'tile2d_grass';
|
||||
case types.GRASS_FLOWERS: return 'tile2d_grass_flowers';
|
||||
case types.DIRT: return 'tile2d_dirt';
|
||||
case types.DIRT_EDGE: return 'tile2d_dirt_edge';
|
||||
case types.WATER: return 'tile2d_water';
|
||||
case types.WATER_EDGE: return 'tile2d_water_edge';
|
||||
case types.STONE: return 'tile2d_stone';
|
||||
default: return 'tile2d_grass';
|
||||
}
|
||||
}
|
||||
|
||||
addDecoration(gridX, gridY, decorType) {
|
||||
const size = this.tileSize;
|
||||
const worldX = gridX * size + size / 2;
|
||||
const worldY = gridY * size + size / 2;
|
||||
|
||||
const types = Map2DData.tileTypes;
|
||||
|
||||
let sprite;
|
||||
|
||||
switch (decorType) {
|
||||
case types.TREE:
|
||||
sprite = this.createTree(worldX, worldY);
|
||||
break;
|
||||
case types.FLOWER_RED:
|
||||
sprite = this.createFlower(worldX, worldY, 0xff6b6b);
|
||||
break;
|
||||
case types.FLOWER_YELLOW:
|
||||
sprite = this.createFlower(worldX, worldY, 0xffd93d);
|
||||
break;
|
||||
case types.FLOWER_BLUE:
|
||||
sprite = this.createFlower(worldX, worldY, 0x6bcbff);
|
||||
break;
|
||||
case types.LILY_PAD:
|
||||
sprite = this.createLilyPad(worldX, worldY);
|
||||
break;
|
||||
case types.BUSH:
|
||||
sprite = this.createBush(worldX, worldY);
|
||||
break;
|
||||
case 'puddle':
|
||||
sprite = this.createPuddle(worldX, worldY);
|
||||
break;
|
||||
}
|
||||
|
||||
if (sprite) {
|
||||
this.decorLayer.add(sprite);
|
||||
}
|
||||
}
|
||||
|
||||
createTree(x, y) {
|
||||
const graphics = this.scene.add.graphics();
|
||||
|
||||
// Trunk
|
||||
graphics.fillStyle(0x8B4513);
|
||||
graphics.fillRect(x - 6, y, 12, 20);
|
||||
|
||||
// Crown (round)
|
||||
graphics.fillStyle(0x2d5016, 0.9);
|
||||
graphics.fillCircle(x, y - 10, 18);
|
||||
|
||||
graphics.fillStyle(0x3a6b1f, 0.8);
|
||||
graphics.fillCircle(x - 5, y - 12, 14);
|
||||
graphics.fillCircle(x + 5, y - 8, 12);
|
||||
|
||||
graphics.fillStyle(0x4a8d2f, 0.7);
|
||||
graphics.fillCircle(x, y - 15, 10);
|
||||
|
||||
return graphics;
|
||||
}
|
||||
|
||||
createFlower(x, y, color) {
|
||||
const graphics = this.scene.add.graphics();
|
||||
|
||||
// Petals
|
||||
graphics.fillStyle(color);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const angle = (Math.PI * 2 * i) / 5;
|
||||
const px = x + Math.cos(angle) * 3;
|
||||
const py = y + Math.sin(angle) * 3;
|
||||
graphics.fillCircle(px, py, 2);
|
||||
}
|
||||
|
||||
// Center
|
||||
graphics.fillStyle(0xFFEB3B);
|
||||
graphics.fillCircle(x, y, 2);
|
||||
|
||||
return graphics;
|
||||
}
|
||||
|
||||
createLilyPad(x, y) {
|
||||
const graphics = this.scene.add.graphics();
|
||||
|
||||
// Lily pad (green circle)
|
||||
graphics.fillStyle(0x4a8d2f);
|
||||
graphics.fillCircle(x, y, 8);
|
||||
|
||||
// Pink flower
|
||||
graphics.fillStyle(0xFF69B4);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const angle = (Math.PI * 2 * i) / 5;
|
||||
const px = x + Math.cos(angle) * 3;
|
||||
const py = y + Math.sin(angle) * 3;
|
||||
graphics.fillCircle(px, py, 2);
|
||||
}
|
||||
graphics.fillStyle(0xFFD700);
|
||||
graphics.fillCircle(x, y, 1.5);
|
||||
|
||||
return graphics;
|
||||
}
|
||||
|
||||
createBush(x, y) {
|
||||
const graphics = this.scene.add.graphics();
|
||||
|
||||
graphics.fillStyle(0x3a6b1f, 0.9);
|
||||
graphics.fillCircle(x, y, 10);
|
||||
graphics.fillCircle(x - 6, y + 2, 8);
|
||||
graphics.fillCircle(x + 6, y + 2, 8);
|
||||
|
||||
graphics.fillStyle(0x4a8d2f, 0.7);
|
||||
graphics.fillCircle(x, y - 3, 6);
|
||||
|
||||
return graphics;
|
||||
}
|
||||
|
||||
createPuddle(x, y) {
|
||||
// Use existing puddle sprite if available
|
||||
if (this.scene.textures.exists('luza_sprite')) {
|
||||
const sprite = this.scene.add.image(x, y, 'luza_sprite');
|
||||
sprite.setScale(0.8);
|
||||
sprite.setAlpha(0.4);
|
||||
return sprite;
|
||||
}
|
||||
|
||||
// Fallback
|
||||
const graphics = this.scene.add.graphics();
|
||||
graphics.fillStyle(0x4488bb, 0.5);
|
||||
graphics.fillEllipse(x, y, 12, 8);
|
||||
return graphics;
|
||||
}
|
||||
|
||||
createFallbackMap() {
|
||||
// Create simple fallback if Map2DData fails
|
||||
for (let y = 0; y < this.height; y++) {
|
||||
this.tiles[y] = [];
|
||||
for (let x = 0; x < this.width; x++) {
|
||||
this.tiles[y][x] = {
|
||||
base: 0, // Grass
|
||||
decoration: null,
|
||||
walkable: true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getTile(x, y) {
|
||||
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
|
||||
return null;
|
||||
}
|
||||
// Safety check: ensure tiles array is initialized
|
||||
if (!this.tiles || !this.tiles[y]) {
|
||||
return null;
|
||||
}
|
||||
return this.tiles[y][x];
|
||||
}
|
||||
|
||||
isWalkable(x, y) {
|
||||
const tile = this.getTile(x, y);
|
||||
return tile ? tile.walkable : false;
|
||||
}
|
||||
|
||||
update(time, delta) {
|
||||
// Reserved for animations (water waves, etc)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.groundLayer) this.groundLayer.destroy();
|
||||
if (this.pathsLayer) this.pathsLayer.destroy();
|
||||
if (this.decorLayer) this.decorLayer.destroy();
|
||||
}
|
||||
}
|
||||
@@ -112,4 +112,13 @@ class InventorySystem {
|
||||
}
|
||||
return total >= count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for addItem() - for simple crafting system compatibility
|
||||
* @param {string} itemKey - Item type/key
|
||||
* @param {number} quantity - Amount to add
|
||||
*/
|
||||
addItemToInventory(itemKey, quantity) {
|
||||
return this.addItem(itemKey, quantity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,13 +38,20 @@ const POND_RADIUS = 4; // Radij ribnika (8x8)
|
||||
|
||||
// Terrain Generator System
|
||||
class TerrainSystem {
|
||||
constructor(scene, width = 100, height = 100) {
|
||||
constructor(scene, width = 100, height = 100, seed = null) {
|
||||
this.scene = scene;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
|
||||
// 🎲 SEED-BASED GENERATION
|
||||
this.seed = seed || Date.now().toString();
|
||||
console.log(`🌍 TerrainSystem initialized with seed: ${this.seed}`);
|
||||
|
||||
// Seeded RNG - vedno isti rezultati za isti seed!
|
||||
this.rng = this.createSeededRNG(this.seed);
|
||||
|
||||
this.iso = new IsometricUtils(48, 24);
|
||||
this.noise = new PerlinNoise(Date.now());
|
||||
this.noise = new PerlinNoise(this.hashCode(this.seed));
|
||||
|
||||
this.tiles = [];
|
||||
this.decorations = [];
|
||||
@@ -151,6 +158,33 @@ class TerrainSystem {
|
||||
this.tiles = Array.from({ length: this.height }, () => Array(this.width).fill(null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create seeded random number generator
|
||||
* Uses Mulberry32 algorithm for consistent randomness
|
||||
*/
|
||||
createSeededRNG(seed) {
|
||||
let h = this.hashCode(seed);
|
||||
return function () {
|
||||
h = Math.imul(h ^ (h >>> 16), 2246822507);
|
||||
h = Math.imul(h ^ (h >>> 13), 3266489909);
|
||||
h = (h ^= h >>> 16) >>> 0;
|
||||
return h / 4294967296; // Return 0-1
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert string seed to numeric hash
|
||||
*/
|
||||
hashCode(str) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash; // Convert to 32bit integer
|
||||
}
|
||||
return Math.abs(hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Preveri, ali je nova lokacija (newX, newY) dovolj oddaljena od vseh
|
||||
* že postavljenih dreves. Uporablja kvadrat razdalje za hitrejšo optimizacijo.
|
||||
@@ -175,34 +209,41 @@ class TerrainSystem {
|
||||
createTileTextures() {
|
||||
const tileWidth = 48;
|
||||
const tileHeight = 60;
|
||||
const P = 2; // PADDING (Margin) za preprečevanje črt
|
||||
const P = 0; // NO PADDING - seamless tiles!
|
||||
|
||||
const types = Object.values(this.terrainTypes);
|
||||
|
||||
// POSEBNA OBDELAVA ZA VODO - 2D Stardew Valley Style!
|
||||
// POSEBNA OBDELAVA ZA VODO - 2D Smooth Pond/Lake Style!
|
||||
if (!this.scene.textures.exists('water')) {
|
||||
const waterGraphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
|
||||
|
||||
// TEMNA MODRA VODA - dobro vidna!
|
||||
// RICH BLUE WATER - Stardew Valley style gradient
|
||||
waterGraphics.fillGradientStyle(
|
||||
0x0a3d62, 0x0a3d62, // Temno modra (zgoraj)
|
||||
0x1e5f8c, 0x1e5f8c // Srednje modra (spodaj)
|
||||
0x1a5f7a, 0x1a5f7a, // Deep blue (top)
|
||||
0x2a7fbc, 0x2a7fbc // Medium blue (bottom)
|
||||
);
|
||||
waterGraphics.fillRect(0, 0, 48, 48);
|
||||
|
||||
// Svetli highlights za valovanje
|
||||
waterGraphics.fillStyle(0x3a8fc2, 0.5);
|
||||
waterGraphics.fillCircle(12, 12, 10);
|
||||
waterGraphics.fillCircle(36, 28, 8);
|
||||
waterGraphics.fillCircle(24, 38, 6);
|
||||
// SMOOTH WAVE HIGHLIGHTS (organic circles)
|
||||
waterGraphics.fillStyle(0x4aa3d0, 0.4);
|
||||
waterGraphics.fillCircle(10, 10, 12);
|
||||
waterGraphics.fillCircle(35, 25, 10);
|
||||
waterGraphics.fillCircle(20, 38, 8);
|
||||
|
||||
// Temnejši border za kontrast
|
||||
waterGraphics.lineStyle(2, 0x062a40, 1);
|
||||
waterGraphics.strokeRect(0, 0, 48, 48);
|
||||
// Soft shimmer spots
|
||||
waterGraphics.fillStyle(0x7fc9e8, 0.3);
|
||||
waterGraphics.fillCircle(15, 20, 6);
|
||||
waterGraphics.fillCircle(38, 12, 5);
|
||||
|
||||
// Bright reflection highlights
|
||||
waterGraphics.fillStyle(0xffffff, 0.2);
|
||||
waterGraphics.fillCircle(12, 15, 4);
|
||||
waterGraphics.fillCircle(32, 30, 3);
|
||||
|
||||
// NO BORDER - seamless tiles!
|
||||
waterGraphics.generateTexture('water', 48, 48);
|
||||
waterGraphics.destroy();
|
||||
console.log('🌊 2D Water texture created (Stardew Valley style)!');
|
||||
console.log('🌊 Smooth pond water texture created (Stardew Valley style)!');
|
||||
}
|
||||
|
||||
types.forEach((type) => {
|
||||
@@ -231,8 +272,8 @@ class TerrainSystem {
|
||||
graphics.lineTo(xs, midY);
|
||||
graphics.closePath();
|
||||
graphics.fill();
|
||||
graphics.lineStyle(2, cLeft); // Overdraw
|
||||
graphics.strokePath();
|
||||
// graphics.lineStyle(2, cLeft); // NO STROKE!
|
||||
// graphics.strokePath();
|
||||
|
||||
// Right Face
|
||||
const cRight = 0x6B3410; // RJAVA DIRT - Right face (temnejša)
|
||||
@@ -244,8 +285,8 @@ class TerrainSystem {
|
||||
graphics.lineTo(midX, bottomY);
|
||||
graphics.closePath();
|
||||
graphics.fill();
|
||||
graphics.lineStyle(2, cRight);
|
||||
graphics.strokePath();
|
||||
// graphics.lineStyle(2, cRight); // NO STROKE!
|
||||
// graphics.strokePath();
|
||||
|
||||
// 2. ZGORNJA PLOSKEV (Top Face)
|
||||
graphics.fillStyle(type.color);
|
||||
@@ -256,32 +297,107 @@ class TerrainSystem {
|
||||
graphics.lineTo(midX, bottomY);
|
||||
graphics.closePath();
|
||||
graphics.fill();
|
||||
graphics.lineStyle(2, type.color); // Overdraw
|
||||
graphics.strokePath();
|
||||
// graphics.lineStyle(2, type.color); // NO STROKE!
|
||||
// graphics.strokePath();
|
||||
|
||||
// Highlight
|
||||
graphics.lineStyle(1, 0xffffff, 0.15);
|
||||
graphics.beginPath();
|
||||
graphics.moveTo(xs, midY);
|
||||
graphics.lineTo(midX, topY);
|
||||
graphics.lineTo(xe, midY);
|
||||
graphics.strokePath();
|
||||
// Highlight - REMOVED for seamless tiles
|
||||
// graphics.lineStyle(1, 0xffffff, 0.15);
|
||||
// graphics.beginPath();
|
||||
// graphics.moveTo(xs, midY);
|
||||
// graphics.lineTo(midX, topY);
|
||||
// graphics.lineTo(xe, midY);
|
||||
// graphics.strokePath();
|
||||
|
||||
// 3. DETAJLI
|
||||
// 3. DETAJLI - ENHANCED!
|
||||
if (type.name.includes('grass')) {
|
||||
graphics.fillStyle(Phaser.Display.Color.IntegerToColor(type.color).lighten(10).color);
|
||||
// Darker grass spots (rich texture)
|
||||
graphics.fillStyle(Phaser.Display.Color.IntegerToColor(type.color).darken(15).color, 0.4);
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const rx = xs + 8 + Math.random() * 32;
|
||||
const ry = topY + 3 + Math.random() * 18;
|
||||
const size = 1 + Math.random() * 2;
|
||||
graphics.fillCircle(rx, ry, size);
|
||||
}
|
||||
|
||||
// Lighter grass highlights (freshness)
|
||||
graphics.fillStyle(Phaser.Display.Color.IntegerToColor(type.color).lighten(20).color, 0.5);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const rx = xs + 10 + Math.random() * 28;
|
||||
const ry = topY + 4 + Math.random() * 16;
|
||||
graphics.fillCircle(rx, ry, 1);
|
||||
}
|
||||
|
||||
// Medium grass blades
|
||||
graphics.fillStyle(Phaser.Display.Color.IntegerToColor(type.color).lighten(10).color, 0.6);
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const rx = xs + 12 + Math.random() * 24;
|
||||
const ry = topY + 5 + Math.random() * 14;
|
||||
graphics.fillRect(rx, ry, 1, 2);
|
||||
}
|
||||
}
|
||||
|
||||
// DIRT texture - Enhanced!
|
||||
if (type.name.includes('dirt') || type.name === 'DIRT') {
|
||||
// Darker dirt clumps
|
||||
graphics.fillStyle(Phaser.Display.Color.IntegerToColor(type.color).darken(20).color, 0.5);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const rx = xs + 8 + Math.random() * 32;
|
||||
const ry = topY + 3 + Math.random() * 18;
|
||||
const size = 2 + Math.random() * 3;
|
||||
graphics.fillCircle(rx, ry, size);
|
||||
}
|
||||
|
||||
// Lighter dirt spots
|
||||
graphics.fillStyle(Phaser.Display.Color.IntegerToColor(type.color).lighten(15).color, 0.4);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const rx = xs + 10 + Math.random() * 28;
|
||||
const ry = topY + 4 + Math.random() * 16;
|
||||
graphics.fillCircle(rx, ry, 1.5);
|
||||
}
|
||||
|
||||
// Small stones in dirt
|
||||
graphics.fillStyle(0x9b8f77, 0.6);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const rx = xs + 12 + Math.random() * 24;
|
||||
const ry = topY + 5 + Math.random() * 14;
|
||||
graphics.fillRect(rx, ry, 2, 2);
|
||||
}
|
||||
}
|
||||
|
||||
if (type.name.includes('stone') || type.name.includes('ruins')) {
|
||||
graphics.fillStyle(0x444444);
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const rx = xs + 8 + Math.random() * 30;
|
||||
// Dark stone spots
|
||||
graphics.fillStyle(0x404040, 0.6);
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const rx = xs + 6 + Math.random() * 36;
|
||||
const ry = topY + 3 + Math.random() * 18;
|
||||
const size = 2 + Math.random() * 4;
|
||||
graphics.fillCircle(rx, ry, size);
|
||||
}
|
||||
|
||||
// Medium gray spots
|
||||
graphics.fillStyle(0x606060, 0.5);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const rx = xs + 8 + Math.random() * 32;
|
||||
const ry = topY + 4 + Math.random() * 16;
|
||||
graphics.fillRect(rx, ry, 3, 3);
|
||||
const size = 1.5 + Math.random() * 3;
|
||||
graphics.fillCircle(rx, ry, size);
|
||||
}
|
||||
|
||||
// Lighter highlights
|
||||
graphics.fillStyle(0xa0a0a0, 0.4);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const rx = xs + 10 + Math.random() * 28;
|
||||
const ry = topY + 5 + Math.random() * 14;
|
||||
graphics.fillCircle(rx, ry, 1);
|
||||
}
|
||||
|
||||
// Crack lines
|
||||
graphics.lineStyle(1, 0x303030, 0.3);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
graphics.beginPath();
|
||||
graphics.moveTo(xs + Math.random() * 48, topY + Math.random() * 20);
|
||||
graphics.lineTo(xs + Math.random() * 48, topY + Math.random() * 20);
|
||||
graphics.strokePath();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,90 +420,43 @@ class TerrainSystem {
|
||||
createWaterFrames() {
|
||||
const tileWidth = 48;
|
||||
const tileHeight = 48;
|
||||
const P = 2;
|
||||
|
||||
// Generiraj 4 frame-e za water animacijo
|
||||
// 🌊 SMOOTH ANIMATED POND WATER (Stardew Valley style)
|
||||
for (let frame = 0; frame < 4; frame++) {
|
||||
const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
|
||||
|
||||
const xs = P;
|
||||
const xe = 48 + P;
|
||||
const midX = 24 + P;
|
||||
const topY = P;
|
||||
const midY = 12 + P;
|
||||
const bottomY = 24 + P;
|
||||
const depth = 14;
|
||||
// 1. BASE WATER - Rich gradient
|
||||
graphics.fillGradientStyle(
|
||||
0x1a5f7a, 0x1a5f7a, // Deep blue
|
||||
0x2a7fbc, 0x2a7fbc // Medium blue
|
||||
);
|
||||
graphics.fillRect(0, 0, tileWidth, tileHeight);
|
||||
|
||||
// 1. STRANICE (DARK BLUE - kot na sliki!)
|
||||
// Left Face - temno modra
|
||||
const cLeft = 0x0066aa;
|
||||
graphics.fillStyle(cLeft);
|
||||
graphics.beginPath();
|
||||
graphics.moveTo(midX, bottomY);
|
||||
graphics.lineTo(midX, bottomY + depth);
|
||||
graphics.lineTo(xs, midY + depth);
|
||||
graphics.lineTo(xs, midY);
|
||||
graphics.closePath();
|
||||
graphics.fill();
|
||||
// 2. ANIMATED WAVE CIRCLES (moving highlights)
|
||||
const waveOffset = frame * 3;
|
||||
|
||||
// Right Face - še temnejša modra
|
||||
const cRight = 0x004488;
|
||||
graphics.fillStyle(cRight);
|
||||
graphics.beginPath();
|
||||
graphics.moveTo(xe, midY);
|
||||
graphics.lineTo(xe, midY + depth);
|
||||
graphics.lineTo(midX, bottomY + depth);
|
||||
graphics.lineTo(midX, bottomY);
|
||||
graphics.closePath();
|
||||
graphics.fill();
|
||||
// Primary wave circles
|
||||
graphics.fillStyle(0x4aa3d0, 0.35);
|
||||
graphics.fillCircle(10 + waveOffset, 10, 12 - frame);
|
||||
graphics.fillCircle(35 - waveOffset, 25, 10 + frame * 0.5);
|
||||
graphics.fillCircle(20, 38 + (frame % 2), 8);
|
||||
|
||||
// 2. TOP SURFACE - SVETLO CYAN (kot na sliki!)
|
||||
const waterColor = 0x33ccff;
|
||||
graphics.fillStyle(waterColor);
|
||||
graphics.beginPath();
|
||||
graphics.moveTo(xs, midY);
|
||||
graphics.lineTo(midX, topY);
|
||||
graphics.lineTo(xe, midY);
|
||||
graphics.lineTo(midX, bottomY);
|
||||
graphics.closePath();
|
||||
graphics.fill();
|
||||
// Secondary shimmer
|
||||
graphics.fillStyle(0x7fc9e8, 0.25);
|
||||
graphics.fillCircle(15 + (frame * 2), 20, 6);
|
||||
graphics.fillCircle(38 - frame, 12, 5);
|
||||
graphics.fillCircle(8, 32 + frame, 4);
|
||||
|
||||
// 3. WAVE PATTERN
|
||||
const offset = frame * 3;
|
||||
graphics.lineStyle(1, 0x66ddff, 0.3);
|
||||
// 3. BRIGHT REFLECTION SPOTS (twinkling)
|
||||
graphics.fillStyle(0xffffff, 0.15 + (frame % 2) * 0.1);
|
||||
graphics.fillCircle(12, 15, 3 + (frame % 2));
|
||||
graphics.fillCircle(32 + (frame % 3), 30, 2);
|
||||
graphics.fillCircle(25, 8, 2);
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
graphics.beginPath();
|
||||
const baseY = topY + 6 + i * 5;
|
||||
for (let px = xs; px <= xe; px += 2) {
|
||||
const relativeX = px - xs;
|
||||
const waveOffset = Math.sin((relativeX + offset + i * 10) * 0.15) * 1.5;
|
||||
const py = baseY + waveOffset;
|
||||
if (px === xs) graphics.moveTo(px, py);
|
||||
else graphics.lineTo(px, py);
|
||||
}
|
||||
graphics.strokePath();
|
||||
}
|
||||
|
||||
// 4. SPARKLE POINTS
|
||||
graphics.fillStyle(0xffffff);
|
||||
const sparkles = [
|
||||
{ x: midX - 10 + (frame * 2) % 20, y: midY + 3 },
|
||||
{ x: midX + 8 - (frame * 3) % 16, y: midY + 8 },
|
||||
{ x: midX - 4 + Math.floor(frame * 1.5) % 8, y: midY + 13 }
|
||||
];
|
||||
sparkles.forEach(s => {
|
||||
graphics.fillRect(s.x, s.y, 1, 1);
|
||||
graphics.fillRect(s.x - 2, s.y, 1, 1);
|
||||
graphics.fillRect(s.x + 2, s.y, 1, 1);
|
||||
graphics.fillRect(s.x, s.y - 2, 1, 1);
|
||||
graphics.fillRect(s.x, s.y + 2, 1, 1);
|
||||
});
|
||||
|
||||
graphics.generateTexture(`water_frame_${frame}`, tileWidth + P * 2, tileHeight + P * 2);
|
||||
graphics.generateTexture(`water_frame_${frame}`, tileWidth, tileHeight);
|
||||
graphics.destroy();
|
||||
}
|
||||
console.log('🌊 Water frames created!');
|
||||
console.log('🌊 Smooth animated pond water created!');
|
||||
}
|
||||
|
||||
generate() {
|
||||
@@ -468,6 +537,12 @@ class TerrainSystem {
|
||||
terrainType = this.terrainTypes.WATER; // Voda!
|
||||
}
|
||||
|
||||
// 🏔️ HEIGHT GENERATION (2.5D Elevation)
|
||||
// Using second Perlin noise layer for smooth hills
|
||||
const heightNoise = this.noise.noise(x * 0.05, y * 0.05); // Low frequency = smooth hills
|
||||
const rawHeight = (heightNoise + 1) * 2.5; // Convert -1..1 to 0..5 range
|
||||
const elevationHeight = Math.floor(rawHeight); // Discrete levels (0-5)
|
||||
|
||||
// Create Tile Data
|
||||
this.tiles[y][x] = {
|
||||
type: terrainType.name,
|
||||
@@ -475,7 +550,8 @@ class TerrainSystem {
|
||||
hasDecoration: false,
|
||||
hasCrop: false,
|
||||
solid: terrainType.solid || false,
|
||||
isHouse: isHouse
|
||||
isHouse: isHouse,
|
||||
height: elevationHeight // 🏔️ NEW: Elevation data (0-5)
|
||||
};
|
||||
|
||||
// Track valid positions for decorations (TREES!)
|
||||
@@ -736,7 +812,7 @@ class TerrainSystem {
|
||||
if (type === 'ruin') {
|
||||
for (let y = 0; y < 6; y++) {
|
||||
for (let x = 0; x < 6; x++) {
|
||||
if (Math.random() > 0.6) this.addDecoration(gridX + x, gridY + y, 'fence');
|
||||
// TEMP DISABLED: if (Math.random() > 0.6) this.addDecoration(gridX + x, gridY + y, 'fence');
|
||||
this.setTile(gridX + x, gridY + y, 'stone');
|
||||
}
|
||||
}
|
||||
@@ -751,7 +827,7 @@ class TerrainSystem {
|
||||
this.setTile(tx, ty, 'stone');
|
||||
if (x === 0 || x === size - 1 || y === 0 || y === size - 1) {
|
||||
if (!(x === Math.floor(size / 2) && y === size - 1)) {
|
||||
this.addDecoration(tx, ty, 'fence');
|
||||
// TEMP DISABLED: this.addDecoration(tx, ty, 'fence');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -770,7 +846,7 @@ class TerrainSystem {
|
||||
const isCenter = (x === 2 || y === 2);
|
||||
if (isCenter && Math.random() > 0.5) continue;
|
||||
if (Math.random() > 0.3) {
|
||||
this.addDecoration(tx, ty, 'fence');
|
||||
// TEMP DISABLED: this.addDecoration(tx, ty, 'fence');
|
||||
} else {
|
||||
// User rocks in ruins
|
||||
if (Math.random() > 0.5) {
|
||||
@@ -970,25 +1046,48 @@ class TerrainSystem {
|
||||
if (!this.visibleTiles.has(key)) {
|
||||
const sprite = this.tilePool.get();
|
||||
|
||||
// Use water texture with animation support
|
||||
// Use ANIMATED water frames (not static bubble texture!)
|
||||
if (tile.type === 'water') {
|
||||
sprite.setTexture('water');
|
||||
sprite.setTexture('water_frame_0'); // Start with frame 0
|
||||
sprite.isWater = true; // Mark for animation
|
||||
|
||||
// ANIMACIJA: Dodaj alpha tween za valovanje
|
||||
this.scene.tweens.add({
|
||||
targets: sprite,
|
||||
alpha: 0.7,
|
||||
duration: 1000,
|
||||
yoyo: true,
|
||||
repeat: -1,
|
||||
ease: 'Sine.easeInOut'
|
||||
});
|
||||
// NO alpha tween - animation handles it
|
||||
} else {
|
||||
sprite.setTexture(tile.type);
|
||||
}
|
||||
|
||||
const screenPos = this.iso.toScreen(x, y);
|
||||
sprite.setPosition(Math.round(screenPos.x + this.offsetX), Math.round(screenPos.y + this.offsetY));
|
||||
|
||||
// 🌊 SKIP HEIGHT EFFECTS FOR WATER (prevents grid lines!)
|
||||
if (tile.type === 'water') {
|
||||
sprite.setPosition(
|
||||
Math.round(screenPos.x + this.offsetX),
|
||||
Math.round(screenPos.y + this.offsetY)
|
||||
);
|
||||
sprite.setScale(1.0);
|
||||
sprite.clearTint();
|
||||
} else {
|
||||
// 🏔️ HEIGHT VISUALIZATION (2.5D Effect) - EXTREME!
|
||||
const height = tile.height || 0;
|
||||
|
||||
// 1. Tint Effect (EXTREME CONTRAST - black valleys, white peaks)
|
||||
// Height 0 = 0x666666 (dark gray), Height 5 = 0xffffff (pure white)
|
||||
const tintValue = 0x666666 + (height * 0x333333);
|
||||
sprite.setTint(tintValue);
|
||||
|
||||
// 2. Scale Variation (MASSIVE - 50% size increase!)
|
||||
const scaleBonus = 1.0 + (height * 0.1); // Max +50% at height 5
|
||||
sprite.setScale(scaleBonus);
|
||||
|
||||
// 3. Y-Offset (HUGE elevation - mountains!)
|
||||
const elevationOffset = -(height * 15); // Each height level = 15px up!
|
||||
|
||||
sprite.setPosition(
|
||||
Math.round(screenPos.x + this.offsetX),
|
||||
Math.round(screenPos.y + this.offsetY + elevationOffset)
|
||||
);
|
||||
}
|
||||
|
||||
sprite.setDepth(this.iso.getDepth(x, y, this.iso.LAYER_FLOOR)); // Tiles = Floor
|
||||
this.visibleTiles.set(key, sprite);
|
||||
}
|
||||
@@ -1019,6 +1118,38 @@ class TerrainSystem {
|
||||
|
||||
// Layer Objects
|
||||
sprite.setDepth(this.iso.getDepth(x, y, this.iso.LAYER_OBJECTS));
|
||||
|
||||
// 🎯 HYBRID POINTER EVENTS - Click-to-collect system
|
||||
// Only for collectible resources (trees, rocks, etc.)
|
||||
const isCollectible = decor.type.includes('tree') ||
|
||||
decor.type.includes('rock') ||
|
||||
decor.type.includes('bush') ||
|
||||
decor.type.includes('flower');
|
||||
|
||||
if (isCollectible) {
|
||||
// Make interactive with hand cursor
|
||||
sprite.setInteractive({ useHandCursor: true });
|
||||
|
||||
// Store grid position for later use
|
||||
sprite.setData('gridX', x);
|
||||
sprite.setData('gridY', y);
|
||||
sprite.setData('decorType', decor.type);
|
||||
|
||||
// HOVER EVENT - Yellow highlight
|
||||
sprite.on('pointerover', () => {
|
||||
sprite.setTint(0xffff00); // Yellow highlight
|
||||
});
|
||||
|
||||
sprite.on('pointerout', () => {
|
||||
sprite.clearTint(); // Remove highlight
|
||||
});
|
||||
|
||||
// CLICK EVENT - Collect resource (with proximity check)
|
||||
sprite.on('pointerdown', () => {
|
||||
this.handleResourceClick(x, y, decor.type, sprite);
|
||||
});
|
||||
}
|
||||
|
||||
this.visibleDecorations.set(key, sprite);
|
||||
}
|
||||
}
|
||||
@@ -1166,11 +1297,136 @@ class TerrainSystem {
|
||||
}
|
||||
}
|
||||
|
||||
isSolid(x, y) {
|
||||
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
|
||||
return this.tiles[y][x].solid || false;
|
||||
// 🏔️ HEIGHT-AWARE COLLISION
|
||||
// Returns true if tile is unwalkable (solid OR cliff)
|
||||
isSolid(x, y, fromX = null, fromY = null) {
|
||||
// Out of bounds = solid
|
||||
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
|
||||
return true;
|
||||
}
|
||||
return true; // Out of bounds = solid
|
||||
|
||||
const tile = this.tiles[y][x];
|
||||
|
||||
// 1. Check if tile itself is solid (walls, etc.)
|
||||
if (tile.solid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Check height difference (cliff detection)
|
||||
if (fromX !== null && fromY !== null) {
|
||||
const fromTile = this.getTile(fromX, fromY);
|
||||
if (fromTile) {
|
||||
const fromHeight = fromTile.height || 0;
|
||||
const toHeight = tile.height || 0;
|
||||
const heightDiff = Math.abs(toHeight - fromHeight);
|
||||
|
||||
// Can't walk over height difference > 1 (cliffs!)
|
||||
if (heightDiff > 1) {
|
||||
console.log(`🏔️ Blocked by cliff! Height diff: ${heightDiff} (from ${fromHeight} to ${toHeight})`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false; // Walkable
|
||||
}
|
||||
|
||||
/**
|
||||
* 🎯 HYBRID RESOURCE CLICK HANDLER
|
||||
* Handles click-to-collect with proximity check
|
||||
* @param {number} x - Grid X position
|
||||
* @param {number} y - Grid Y position
|
||||
* @param {string} decorType - Type of decoration (tree, rock, etc.)
|
||||
* @param {Phaser.GameObjects.Sprite} sprite - The clicked sprite
|
||||
*/
|
||||
handleResourceClick(x, y, decorType, sprite) {
|
||||
// 1. Get player position
|
||||
if (!this.scene.player) {
|
||||
console.warn('⚠️ Player not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const playerPos = this.scene.player.getPosition();
|
||||
const playerX = playerPos.x;
|
||||
const playerY = playerPos.y;
|
||||
|
||||
// 2. PROXIMITY CHECK - Player must be within 3 tiles
|
||||
const distance = Phaser.Math.Distance.Between(playerX, playerY, x, y);
|
||||
const MAX_DISTANCE = 3; // 3 tiles
|
||||
|
||||
if (distance > MAX_DISTANCE) {
|
||||
// Too far - show warning
|
||||
console.log(`⚠️ Too far! Distance: ${distance.toFixed(1)} tiles`);
|
||||
|
||||
// Visual feedback - shake sprite
|
||||
this.scene.tweens.add({
|
||||
targets: sprite,
|
||||
x: sprite.x + 5,
|
||||
duration: 50,
|
||||
yoyo: true,
|
||||
repeat: 2
|
||||
});
|
||||
|
||||
// Floating text
|
||||
if (this.scene.events) {
|
||||
this.scene.events.emit('show-floating-text', {
|
||||
x: sprite.x,
|
||||
y: sprite.y - 50,
|
||||
text: 'Preblizu!',
|
||||
color: '#ff4444'
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. TOOL CHECK - Player needs correct tool
|
||||
const requiredTool = this.getRequiredTool(decorType);
|
||||
const hasTool = this.scene.player.hasToolEquipped(requiredTool);
|
||||
|
||||
if (!hasTool && requiredTool) {
|
||||
console.log(`⚠️ Need tool: ${requiredTool}`);
|
||||
|
||||
// Floating text
|
||||
if (this.scene.events) {
|
||||
this.scene.events.emit('show-floating-text', {
|
||||
x: sprite.x,
|
||||
y: sprite.y - 50,
|
||||
text: `Potrebuješ: ${requiredTool}`,
|
||||
color: '#ff4444'
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. COLLECT - Damage decoration (uses existing HP system)
|
||||
console.log(`✅ Collecting ${decorType} at (${x}, ${y})`);
|
||||
|
||||
// Use existing damage system (maintains HP logic)
|
||||
const result = this.damageDecoration(x, y, 1); // 1 hit per click
|
||||
|
||||
// Optional: Instant collect mode (if you want 1-click collect)
|
||||
// this.damageDecoration(x, y, 999);
|
||||
|
||||
// Sound effect
|
||||
if (this.scene.soundManager) {
|
||||
if (decorType.includes('tree')) {
|
||||
this.scene.soundManager.playChopSound();
|
||||
} else if (decorType.includes('rock')) {
|
||||
this.scene.soundManager.playMineSound();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get required tool for decoration type
|
||||
*/
|
||||
getRequiredTool(decorType) {
|
||||
if (decorType.includes('tree')) return 'axe';
|
||||
if (decorType.includes('rock')) return 'pickaxe';
|
||||
if (decorType.includes('bush')) return 'axe';
|
||||
return null; // No tool required (flowers, etc.)
|
||||
}
|
||||
|
||||
// Water Animation Update - DISABLED (using tweens now)
|
||||
|
||||
380
src/ui/CraftingUI.js
Normal file
380
src/ui/CraftingUI.js
Normal file
@@ -0,0 +1,380 @@
|
||||
// Crafting UI - Visual panel for crafting interface
|
||||
class CraftingUI {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
this.craftingSystem = scene.craftingSystem;
|
||||
|
||||
this.isOpen = false;
|
||||
this.currentCategory = 'all';
|
||||
this.selectedRecipe = null;
|
||||
|
||||
// UI elements
|
||||
this.container = null;
|
||||
this.panel = null;
|
||||
this.categoryButtons = [];
|
||||
this.recipeList = [];
|
||||
this.detailsPanel = null;
|
||||
|
||||
this.createUI();
|
||||
this.hide();
|
||||
|
||||
// Listen for crafting events
|
||||
this.setupEventListeners();
|
||||
|
||||
console.log('🎨 CraftingUI initialized');
|
||||
}
|
||||
|
||||
createUI() {
|
||||
const width = this.scene.cameras.main.width;
|
||||
const height = this.scene.cameras.main.height;
|
||||
|
||||
// Main container
|
||||
this.container = this.scene.add.container(0, 0);
|
||||
this.container.setDepth(10000);
|
||||
this.container.setScrollFactor(0);
|
||||
|
||||
// Semi-transparent background overlay
|
||||
const overlay = this.scene.add.rectangle(0, 0, width, height, 0x000000, 0.7);
|
||||
overlay.setOrigin(0);
|
||||
overlay.setInteractive();
|
||||
this.container.add(overlay);
|
||||
|
||||
// Main panel (centered)
|
||||
const panelWidth = 700;
|
||||
const panelHeight = 500;
|
||||
const panelX = width / 2 - panelWidth / 2;
|
||||
const panelY = height / 2 - panelHeight / 2;
|
||||
|
||||
this.panel = this.scene.add.rectangle(panelX, panelY, panelWidth, panelHeight, 0x2a1810);
|
||||
this.panel.setOrigin(0);
|
||||
this.panel.setStrokeStyle(3, 0x4a3820);
|
||||
this.container.add(this.panel);
|
||||
|
||||
// Title
|
||||
const title = this.scene.add.text(width / 2, panelY + 20, '🛠️ CRAFTING', {
|
||||
fontSize: '32px',
|
||||
fontFamily: 'Georgia, serif',
|
||||
fill: '#f4e4c1',
|
||||
stroke: '#2d1b00',
|
||||
strokeThickness: 4
|
||||
}).setOrigin(0.5);
|
||||
this.container.add(title);
|
||||
|
||||
// Close button
|
||||
const closeBtn = this.scene.add.text(panelX + panelWidth - 40, panelY + 20, '✖', {
|
||||
fontSize: '24px',
|
||||
fill: '#ff6666'
|
||||
}).setOrigin(0.5);
|
||||
closeBtn.setInteractive({ useHandCursor: true });
|
||||
closeBtn.on('pointerdown', () => this.hide());
|
||||
closeBtn.on('pointerover', () => closeBtn.setScale(1.2));
|
||||
closeBtn.on('pointerout', () => closeBtn.setScale(1.0));
|
||||
this.container.add(closeBtn);
|
||||
|
||||
// Category buttons (top)
|
||||
this.createCategoryButtons(panelX, panelY + 60, panelWidth);
|
||||
|
||||
// Recipe list (left side)
|
||||
this.createRecipeListPanel(panelX + 10, panelY + 120, 300, 350);
|
||||
|
||||
// Details panel (right side)
|
||||
this.createDetailsPanel(panelX + 320, panelY + 120, 370, 350);
|
||||
}
|
||||
|
||||
createCategoryButtons(x, y, width) {
|
||||
const categories = this.craftingSystem.categories;
|
||||
const buttonWidth = (width - 40) / categories.length;
|
||||
|
||||
categories.forEach((category, index) => {
|
||||
const btnX = x + 20 + (index * buttonWidth);
|
||||
|
||||
const btn = this.scene.add.rectangle(btnX, y, buttonWidth - 10, 40, 0x4a3820);
|
||||
btn.setOrigin(0, 0.5);
|
||||
btn.setStrokeStyle(2, 0x6a5840);
|
||||
btn.setInteractive({ useHandCursor: true });
|
||||
|
||||
const text = this.scene.add.text(btnX + buttonWidth / 2 - 5, y, `${category.icon} ${category.name}`, {
|
||||
fontSize: '14px',
|
||||
fontFamily: 'Arial',
|
||||
fill: '#d4c4a1'
|
||||
}).setOrigin(0.5);
|
||||
|
||||
btn.on('pointerdown', () => this.selectCategory(category.id));
|
||||
btn.on('pointerover', () => {
|
||||
btn.setFillStyle(0x6a5840);
|
||||
text.setScale(1.05);
|
||||
});
|
||||
btn.on('pointerout', () => {
|
||||
btn.setFillStyle(0x4a3820);
|
||||
text.setScale(1.0);
|
||||
});
|
||||
|
||||
this.container.add(btn);
|
||||
this.container.add(text);
|
||||
|
||||
this.categoryButtons.push({ category: category.id, btn, text });
|
||||
});
|
||||
}
|
||||
|
||||
createRecipeListPanel(x, y, width, height) {
|
||||
// Background
|
||||
const bg = this.scene.add.rectangle(x, y, width, height, 0x1a1410);
|
||||
bg.setOrigin(0);
|
||||
bg.setStrokeStyle(2, 0x4a3820);
|
||||
this.container.add(bg);
|
||||
|
||||
// Title
|
||||
const title = this.scene.add.text(x + width / 2, y + 10, 'Recipes', {
|
||||
fontSize: '18px',
|
||||
fontFamily: 'Georgia, serif',
|
||||
fill: '#f4e4c1'
|
||||
}).setOrigin(0.5, 0);
|
||||
this.container.add(title);
|
||||
|
||||
// Store panel bounds for recipe items
|
||||
this.recipePanelBounds = { x, y: y + 40, width, height: height - 40 };
|
||||
}
|
||||
|
||||
createDetailsPanel(x, y, width, height) {
|
||||
// Background
|
||||
const bg = this.scene.add.rectangle(x, y, width, height, 0x1a1410);
|
||||
bg.setOrigin(0);
|
||||
bg.setStrokeStyle(2, 0x4a3820);
|
||||
this.container.add(bg);
|
||||
|
||||
this.detailsPanelBounds = { x, y, width, height };
|
||||
}
|
||||
|
||||
selectCategory(categoryId) {
|
||||
this.currentCategory = categoryId;
|
||||
this.refreshRecipeList();
|
||||
|
||||
// Update button styles
|
||||
this.categoryButtons.forEach(({ category, btn, text }) => {
|
||||
if (category === categoryId) {
|
||||
btn.setFillStyle(0x6a5840);
|
||||
text.setStyle({ fill: '#ffffff' });
|
||||
} else {
|
||||
btn.setFillStyle(0x4a3820);
|
||||
text.setStyle({ fill: '#d4c4a1' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
refreshRecipeList() {
|
||||
// Clear existing recipe items
|
||||
this.recipeList.forEach(item => item.destroy());
|
||||
this.recipeList = [];
|
||||
|
||||
// Get recipes for current category
|
||||
const recipes = this.craftingSystem.getUnlockedRecipes(this.currentCategory);
|
||||
|
||||
const { x, y, width } = this.recipePanelBounds;
|
||||
const itemHeight = 50;
|
||||
|
||||
recipes.forEach((recipe, index) => {
|
||||
const itemY = y + (index * itemHeight);
|
||||
|
||||
// Check if can craft
|
||||
const canCraft = this.craftingSystem.canCraft(recipe.id);
|
||||
|
||||
// Background
|
||||
const bg = this.scene.add.rectangle(x + 5, itemY, width - 10, itemHeight - 5, 0x2a1810);
|
||||
bg.setOrigin(0);
|
||||
bg.setStrokeStyle(1, canCraft.canCraft ? 0x4a9d5f : 0x6a5840);
|
||||
bg.setInteractive({ useHandCursor: true });
|
||||
|
||||
// Recipe name
|
||||
const name = this.scene.add.text(x + 15, itemY + itemHeight / 2, recipe.name, {
|
||||
fontSize: '16px',
|
||||
fontFamily: 'Arial',
|
||||
fill: canCraft.canCraft ? '#ffffff' : '#888888'
|
||||
}).setOrigin(0, 0.5);
|
||||
|
||||
// Hover effect
|
||||
bg.on('pointerover', () => {
|
||||
bg.setFillStyle(0x3a2820);
|
||||
name.setScale(1.05);
|
||||
});
|
||||
bg.on('pointerout', () => {
|
||||
bg.setFillStyle(0x2a1810);
|
||||
name.setScale(1.0);
|
||||
});
|
||||
|
||||
// Click to select
|
||||
bg.on('pointerdown', () => this.selectRecipe(recipe));
|
||||
|
||||
this.container.add(bg);
|
||||
this.container.add(name);
|
||||
|
||||
this.recipeList.push(bg, name);
|
||||
});
|
||||
}
|
||||
|
||||
selectRecipe(recipe) {
|
||||
this.selectedRecipe = recipe;
|
||||
this.showRecipeDetails();
|
||||
}
|
||||
|
||||
showRecipeDetails() {
|
||||
if (!this.selectedRecipe) return;
|
||||
|
||||
// Clear existing details
|
||||
if (this.detailsContent) {
|
||||
this.detailsContent.forEach(item => item.destroy());
|
||||
}
|
||||
this.detailsContent = [];
|
||||
|
||||
const { x, y, width } = this.detailsPanelBounds;
|
||||
const recipe = this.selectedRecipe;
|
||||
|
||||
// Recipe name
|
||||
const name = this.scene.add.text(x + width / 2, y + 20, recipe.name, {
|
||||
fontSize: '24px',
|
||||
fontFamily: 'Georgia, serif',
|
||||
fill: '#f4e4c1',
|
||||
stroke: '#2d1b00',
|
||||
strokeThickness: 2
|
||||
}).setOrigin(0.5, 0);
|
||||
this.detailsContent.push(name);
|
||||
|
||||
// Description
|
||||
const desc = this.scene.add.text(x + 20, y + 60, recipe.description, {
|
||||
fontSize: '14px',
|
||||
fontFamily: 'Arial',
|
||||
fill: '#d4c4a1',
|
||||
wordWrap: { width: width - 40 }
|
||||
});
|
||||
this.detailsContent.push(desc);
|
||||
|
||||
// Ingredients title
|
||||
const ingredTitle = this.scene.add.text(x + 20, y + 120, 'Required Ingredients:', {
|
||||
fontSize: '16px',
|
||||
fontFamily: 'Arial',
|
||||
fill: '#f4e4c1',
|
||||
fontStyle: 'bold'
|
||||
});
|
||||
this.detailsContent.push(ingredTitle);
|
||||
|
||||
// Ingredients list
|
||||
const inventory = this.scene.inventorySystem;
|
||||
let ingredY = y + 150;
|
||||
|
||||
for (const [itemId, required] of Object.entries(recipe.ingredients)) {
|
||||
const has = inventory ? inventory.getItemCount(itemId) : 0;
|
||||
const hasEnough = has >= required;
|
||||
|
||||
const text = this.scene.add.text(x + 30, ingredY, `• ${itemId}: ${has}/${required}`, {
|
||||
fontSize: '14px',
|
||||
fontFamily: 'Arial',
|
||||
fill: hasEnough ? '#4a9d5f' : '#ff6666'
|
||||
});
|
||||
this.detailsContent.push(text);
|
||||
ingredY += 25;
|
||||
}
|
||||
|
||||
// Result
|
||||
const resultText = this.scene.add.text(x + 20, ingredY + 20, `Produces: ${recipe.result.quantity}x ${recipe.result.item}`, {
|
||||
fontSize: '16px',
|
||||
fontFamily: 'Arial',
|
||||
fill: '#4aa3d0',
|
||||
fontStyle: 'bold'
|
||||
});
|
||||
this.detailsContent.push(resultText);
|
||||
|
||||
// Craft button
|
||||
const canCraft = this.craftingSystem.canCraft(recipe.id);
|
||||
const btnY = y + 320;
|
||||
|
||||
const craftBtn = this.scene.add.rectangle(x + width / 2, btnY, 200, 40, canCraft.canCraft ? 0x4a9d5f : 0x666666);
|
||||
craftBtn.setStrokeStyle(2, 0x000000);
|
||||
|
||||
const btnText = this.scene.add.text(x + width / 2, btnY, canCraft.canCraft ? '🔨 CRAFT' : '❌ Cannot Craft', {
|
||||
fontSize: '18px',
|
||||
fontFamily: 'Arial',
|
||||
fill: '#ffffff',
|
||||
fontStyle: 'bold'
|
||||
}).setOrigin(0.5);
|
||||
|
||||
if (canCraft.canCraft) {
|
||||
craftBtn.setInteractive({ useHandCursor: true });
|
||||
craftBtn.on('pointerover', () => {
|
||||
craftBtn.setFillStyle(0x5abd6f);
|
||||
btnText.setScale(1.1);
|
||||
});
|
||||
craftBtn.on('pointerout', () => {
|
||||
craftBtn.setFillStyle(0x4a9d5f);
|
||||
btnText.setScale(1.0);
|
||||
});
|
||||
craftBtn.on('pointerdown', () => this.craftSelectedRecipe());
|
||||
}
|
||||
|
||||
this.detailsContent.push(craftBtn, btnText);
|
||||
|
||||
// Add all to container
|
||||
this.detailsContent.forEach(item => this.container.add(item));
|
||||
}
|
||||
|
||||
craftSelectedRecipe() {
|
||||
if (!this.selectedRecipe) return;
|
||||
|
||||
const success = this.craftingSystem.craftItem(this.selectedRecipe.id);
|
||||
|
||||
if (success) {
|
||||
// Refresh UI
|
||||
this.refreshRecipeList();
|
||||
this.showRecipeDetails();
|
||||
|
||||
console.log(`✅ Crafting started: ${this.selectedRecipe.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Listen for inventory changes to update recipe availability
|
||||
this.scene.events.on('inventory-changed', () => {
|
||||
if (this.isOpen) {
|
||||
this.refreshRecipeList();
|
||||
if (this.selectedRecipe) {
|
||||
this.showRecipeDetails();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for craft completion
|
||||
this.scene.events.on('craft-complete', (data) => {
|
||||
if (this.isOpen) {
|
||||
this.refreshRecipeList();
|
||||
if (this.selectedRecipe) {
|
||||
this.showRecipeDetails();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
show() {
|
||||
this.isOpen = true;
|
||||
this.container.setVisible(true);
|
||||
this.selectCategory(this.currentCategory);
|
||||
console.log('🛠️ Crafting UI opened');
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.isOpen = false;
|
||||
this.container.setVisible(false);
|
||||
console.log('🛠️ Crafting UI closed');
|
||||
}
|
||||
|
||||
toggle() {
|
||||
if (this.isOpen) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.container) {
|
||||
this.container.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
265
src/ui/WeatherUI.js
Normal file
265
src/ui/WeatherUI.js
Normal file
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* 🌦️ WEATHER CONTROL UI PANEL
|
||||
* Separate UI for weather system controls
|
||||
*/
|
||||
|
||||
class WeatherUI {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
this.container = null;
|
||||
this.isVisible = false;
|
||||
|
||||
this.createPanel();
|
||||
this.hide(); // Start hidden
|
||||
|
||||
// Toggle with W key
|
||||
this.scene.input.keyboard.on('keydown-W', () => {
|
||||
this.toggle();
|
||||
});
|
||||
}
|
||||
|
||||
createPanel() {
|
||||
const width = 300;
|
||||
const height = 400;
|
||||
const x = 20;
|
||||
const y = 100;
|
||||
|
||||
// Main container
|
||||
this.container = this.scene.add.container(x, y);
|
||||
this.container.setDepth(900000); // High depth (below debug UI)
|
||||
this.container.setScrollFactor(0);
|
||||
|
||||
// Background
|
||||
const bg = this.scene.add.rectangle(0, 0, width, height, 0x222222, 0.9);
|
||||
bg.setOrigin(0, 0);
|
||||
bg.setStrokeStyle(2, 0x44aaff);
|
||||
this.container.add(bg);
|
||||
|
||||
// Title
|
||||
const title = this.scene.add.text(width / 2, 20, '🌦️ WEATHER CONTROL', {
|
||||
fontSize: '20px',
|
||||
fontFamily: 'Arial',
|
||||
fontStyle: 'bold',
|
||||
fill: '#44aaff'
|
||||
});
|
||||
title.setOrigin(0.5, 0);
|
||||
this.container.add(title);
|
||||
|
||||
// Current Weather Display
|
||||
const currentLabel = this.scene.add.text(20, 60, 'Current:', {
|
||||
fontSize: '16px',
|
||||
fill: '#ffffff'
|
||||
});
|
||||
this.container.add(currentLabel);
|
||||
|
||||
this.currentWeatherText = this.scene.add.text(width - 20, 60, 'Clear ☀️', {
|
||||
fontSize: '16px',
|
||||
fontStyle: 'bold',
|
||||
fill: '#ffdd00'
|
||||
});
|
||||
this.currentWeatherText.setOrigin(1, 0);
|
||||
this.container.add(this.currentWeatherText);
|
||||
|
||||
// Intensity Display
|
||||
const intensityLabel = this.scene.add.text(20, 90, 'Intensity:', {
|
||||
fontSize: '16px',
|
||||
fill: '#ffffff'
|
||||
});
|
||||
this.container.add(intensityLabel);
|
||||
|
||||
this.intensityText = this.scene.add.text(width - 20, 90, '100%', {
|
||||
fontSize: '16px',
|
||||
fontStyle: 'bold',
|
||||
fill: '#00ff88'
|
||||
});
|
||||
this.intensityText.setOrigin(1, 0);
|
||||
this.container.add(this.intensityText);
|
||||
|
||||
// Intensity Slider Visual
|
||||
const sliderBg = this.scene.add.rectangle(20, 120, width - 40, 10, 0x444444);
|
||||
sliderBg.setOrigin(0, 0);
|
||||
this.container.add(sliderBg);
|
||||
|
||||
this.intensityBar = this.scene.add.rectangle(20, 120, (width - 40) * 1.0, 10, 0x00ff88);
|
||||
this.intensityBar.setOrigin(0, 0);
|
||||
this.container.add(this.intensityBar);
|
||||
|
||||
// Auto Cycle Status
|
||||
const autoLabel = this.scene.add.text(20, 150, 'Auto Cycle:', {
|
||||
fontSize: '16px',
|
||||
fill: '#ffffff'
|
||||
});
|
||||
this.container.add(autoLabel);
|
||||
|
||||
this.autoText = this.scene.add.text(width - 20, 150, 'OFF', {
|
||||
fontSize: '16px',
|
||||
fontStyle: 'bold',
|
||||
fill: '#ff4444'
|
||||
});
|
||||
this.autoText.setOrigin(1, 0);
|
||||
this.container.add(this.autoText);
|
||||
|
||||
// Weather Buttons
|
||||
const buttonY = 190;
|
||||
const buttonData = [
|
||||
{ label: '☀️ Clear', weather: 'clear', color: 0xffdd00 },
|
||||
{ label: '🌧️ Rain', weather: 'rain', color: 0x44aaff },
|
||||
{ label: '❄️ Snow', weather: 'snow', color: 0xccccff },
|
||||
{ label: '⚡ Storm', weather: 'storm', color: 0xff4444 },
|
||||
{ label: '🌫️ Fog', weather: 'fog', color: 0x888888 }
|
||||
];
|
||||
|
||||
buttonData.forEach((data, index) => {
|
||||
const btn = this.createButton(
|
||||
width / 2,
|
||||
buttonY + (index * 35),
|
||||
width - 40,
|
||||
30,
|
||||
data.label,
|
||||
data.color,
|
||||
() => this.scene.setWeather(data.weather)
|
||||
);
|
||||
this.container.add(btn);
|
||||
});
|
||||
|
||||
// Controls Help
|
||||
const helpY = buttonY + (buttonData.length * 35) + 10;
|
||||
const help = this.scene.add.text(width / 2, helpY,
|
||||
'CONTROLS:\n' +
|
||||
'W = Toggle Panel\n' +
|
||||
'Shift+A = Auto Cycle\n' +
|
||||
'+/- = Intensity', {
|
||||
fontSize: '12px',
|
||||
fill: '#888888',
|
||||
align: 'center'
|
||||
});
|
||||
help.setOrigin(0.5, 0);
|
||||
this.container.add(help);
|
||||
}
|
||||
|
||||
createButton(x, y, width, height, text, color, onClick) {
|
||||
const container = this.scene.add.container(x, y);
|
||||
|
||||
const bg = this.scene.add.rectangle(0, 0, width, height, color, 0.8);
|
||||
bg.setStrokeStyle(2, 0xffffff, 0.5);
|
||||
bg.setInteractive({ useHandCursor: true });
|
||||
|
||||
const label = this.scene.add.text(0, 0, text, {
|
||||
fontSize: '14px',
|
||||
fontFamily: 'Arial',
|
||||
fontStyle: 'bold',
|
||||
fill: '#ffffff'
|
||||
});
|
||||
label.setOrigin(0.5, 0.5);
|
||||
|
||||
bg.on('pointerover', () => {
|
||||
bg.setAlpha(1.0);
|
||||
bg.setStrokeStyle(2, 0xffffff, 1.0);
|
||||
});
|
||||
|
||||
bg.on('pointerout', () => {
|
||||
bg.setAlpha(0.8);
|
||||
bg.setStrokeStyle(2, 0xffffff, 0.5);
|
||||
});
|
||||
|
||||
bg.on('pointerdown', () => {
|
||||
this.scene.tweens.add({
|
||||
targets: container,
|
||||
scaleX: 0.95,
|
||||
scaleY: 0.95,
|
||||
duration: 100,
|
||||
yoyo: true
|
||||
});
|
||||
onClick();
|
||||
});
|
||||
|
||||
container.add(bg);
|
||||
container.add(label);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
update() {
|
||||
if (!this.isVisible) return;
|
||||
|
||||
// Update current weather display
|
||||
const weatherIcons = {
|
||||
clear: '☀️',
|
||||
rain: '🌧️',
|
||||
snow: '❄️',
|
||||
storm: '⚡',
|
||||
fog: '🌫️'
|
||||
};
|
||||
const weatherColors = {
|
||||
clear: '#ffdd00',
|
||||
rain: '#44aaff',
|
||||
snow: '#ccccff',
|
||||
storm: '#ff4444',
|
||||
fog: '#888888'
|
||||
};
|
||||
|
||||
const currentWeather = this.scene.currentWeather || 'clear';
|
||||
const icon = weatherIcons[currentWeather] || '☀️';
|
||||
const color = weatherColors[currentWeather] || '#ffdd00';
|
||||
|
||||
this.currentWeatherText.setText(`${currentWeather.charAt(0).toUpperCase() + currentWeather.slice(1)} ${icon}`);
|
||||
this.currentWeatherText.setColor(color);
|
||||
|
||||
// Update intensity
|
||||
const intensity = this.scene.weatherIntensity || 1.0;
|
||||
this.intensityText.setText(`${Math.round(intensity * 100)}%`);
|
||||
this.intensityBar.setScale((intensity / 2.0), 1); // Max 2.0 = 100% width
|
||||
|
||||
// Update auto cycle status
|
||||
const autoEnabled = this.scene.autoWeatherEnabled || false;
|
||||
this.autoText.setText(autoEnabled ? 'ON' : 'OFF');
|
||||
this.autoText.setColor(autoEnabled ? '#00ff88' : '#ff4444');
|
||||
}
|
||||
|
||||
toggle() {
|
||||
if (this.isVisible) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
|
||||
show() {
|
||||
this.isVisible = true;
|
||||
this.container.setVisible(true);
|
||||
|
||||
// Slide in animation
|
||||
this.container.setAlpha(0);
|
||||
this.container.x = -320;
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: this.container,
|
||||
x: 20,
|
||||
alpha: 1,
|
||||
duration: 300,
|
||||
ease: 'Back.easeOut'
|
||||
});
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.isVisible = false;
|
||||
|
||||
// Slide out animation
|
||||
this.scene.tweens.add({
|
||||
targets: this.container,
|
||||
x: -320,
|
||||
alpha: 0,
|
||||
duration: 300,
|
||||
ease: 'Back.easeIn',
|
||||
onComplete: () => {
|
||||
this.container.setVisible(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.container) {
|
||||
this.container.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
86
src/utils/GlobalInventoryHelper.js
Normal file
86
src/utils/GlobalInventoryHelper.js
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* GLOBAL INVENTORY HELPER
|
||||
* Provides simple object-based inventory access for compatibility
|
||||
* with simple crafting system format
|
||||
*
|
||||
* Automatically syncs with InventorySystem
|
||||
*/
|
||||
|
||||
window.inventory = new Proxy({}, {
|
||||
/**
|
||||
* GET - Read inventory count
|
||||
* Usage: inventory.wood → returns wood count
|
||||
*/
|
||||
get(target, prop) {
|
||||
if (typeof prop === 'symbol' || prop === 'toJSON' || prop === 'toString') {
|
||||
return target[prop];
|
||||
}
|
||||
|
||||
const gameScene = window.gameState?.gameScene;
|
||||
if (!gameScene || !gameScene.inventorySystem) {
|
||||
console.warn('⚠️ InventorySystem not ready yet');
|
||||
return 0;
|
||||
}
|
||||
|
||||
return gameScene.inventorySystem.getItemCount(prop);
|
||||
},
|
||||
|
||||
/**
|
||||
* SET - Modify inventory count
|
||||
* Usage: inventory.wood = 10 → sets wood to 10
|
||||
* Usage: inventory.wood += 5 → adds 5 wood
|
||||
* Usage: inventory.wood -= 2 → removes 2 wood
|
||||
*/
|
||||
set(target, prop, value) {
|
||||
if (typeof prop === 'symbol') {
|
||||
target[prop] = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
const gameScene = window.gameState?.gameScene;
|
||||
if (!gameScene || !gameScene.inventorySystem) {
|
||||
console.warn('⚠️ InventorySystem not ready yet');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get current count
|
||||
const currentCount = gameScene.inventorySystem.getItemCount(prop);
|
||||
const difference = value - currentCount;
|
||||
|
||||
if (difference > 0) {
|
||||
// Add items
|
||||
gameScene.inventorySystem.addItem(prop, difference);
|
||||
} else if (difference < 0) {
|
||||
// Remove items
|
||||
gameScene.inventorySystem.removeItem(prop, Math.abs(difference));
|
||||
}
|
||||
|
||||
// ✨ AUTO UI UPDATE
|
||||
gameScene.inventorySystem.updateUI();
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Global helper function for simple crafting system
|
||||
*/
|
||||
window.addItemToInventory = function (itemKey, quantity) {
|
||||
const gameScene = window.gameState?.gameScene;
|
||||
if (!gameScene || !gameScene.inventorySystem) {
|
||||
console.warn('⚠️ InventorySystem not ready yet');
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = gameScene.inventorySystem.addItem(itemKey, quantity);
|
||||
|
||||
// ✨ AUTO UI UPDATE
|
||||
gameScene.inventorySystem.updateUI();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
console.log('✅ Global inventory helper initialized');
|
||||
console.log('💡 Usage: inventory.wood, inventory.stone, etc.');
|
||||
console.log('💡 Usage: inventory.wood = 10, inventory.wood += 5, etc.');
|
||||
console.log('✨ UI auto-updates on inventory change!');
|
||||
Reference in New Issue
Block a user