FAZA 17: 2.5D Minecraft-Style Terrain + Y-Layer Stacking + Custom Sprites

COMPLETED FEATURES:

 Custom Sprite Integration:
- Player, Zombie, Merchant sprites (0.2 scale)
- 11 custom sprites + 5 asset packs loaded
- Auto-transparency processing (white/brown removal)
- Gravestone system with atlas extraction

 2.5D Minecraft-Style Terrain:
- Volumetric blocks with 25px thickness
- Strong left/right side shading (30%/50% darker)
- Minecraft-style texture patterns (grass, dirt, stone)
- Crisp black outlines for definition

 Y-Layer Stacking System:
- GRASS_FULL: All green (elevation > 0.7)
- GRASS_TOP: Green top + brown sides (elevation 0.4-0.7)
- DIRT: All brown (elevation < 0.4)
- Dynamic terrain depth based on height

 Floating Island World Edge:
- Stone cliff walls at map borders
- 2-tile transition zone
- Elevation flattening for cliff drop-off effect
- 100x100 world with defined boundaries

 Performance & Polish:
- Canvas renderer for pixel-perfect sharpness
- CSS image-rendering: crisp-edges
- willReadFrequently optimization
- No Canvas2D warnings

 Technical:
- 3D volumetric trees and rocks
- Hybrid rendering (2.5D terrain + 2D characters)
- Procedural texture generation
- Y-layer aware terrain type selection
This commit is contained in:
2025-12-07 01:44:16 +01:00
parent 34a2d07538
commit 9eb57ed117
60 changed files with 5082 additions and 195 deletions

View File

@@ -0,0 +1,112 @@
class BuildingSystem {
constructor(scene) {
this.scene = scene;
this.isBuildMode = false;
this.selectedBuilding = 'fence'; // fence, wall, house
this.buildingsData = {
fence: { name: 'Fence', cost: { wood: 2 }, w: 1, h: 1 },
wall: { name: 'Stone Wall', cost: { stone: 2 }, w: 1, h: 1 },
house: { name: 'House', cost: { wood: 20, stone: 20, gold: 50 }, w: 1, h: 1 } // Visual is bigger but anchor is 1 tile
};
// Textures init
if (!this.scene.textures.exists('struct_fence')) TextureGenerator.createStructureSprite(this.scene, 'struct_fence', 'fence');
if (!this.scene.textures.exists('struct_wall')) TextureGenerator.createStructureSprite(this.scene, 'struct_wall', 'wall');
if (!this.scene.textures.exists('struct_house')) TextureGenerator.createStructureSprite(this.scene, 'struct_house', 'house');
}
toggleBuildMode() {
this.isBuildMode = !this.isBuildMode;
console.log(`🔨 Build Mode: ${this.isBuildMode}`);
// Update UI
const uiScene = this.scene.scene.get('UIScene');
if (uiScene) {
uiScene.toggleBuildMenu(this.isBuildMode);
}
}
selectBuilding(type) {
if (this.buildingsData[type]) {
this.selectedBuilding = type;
console.log(`🔨 Selected: ${this.selectedBuilding}`);
// UI feedback?
const uiScene = this.scene.scene.get('UIScene');
if (uiScene) uiScene.updateBuildSelection(type);
}
}
tryBuild(gridX, gridY) {
if (!this.isBuildMode) return false;
const building = this.buildingsData[this.selectedBuilding];
const inv = this.scene.inventorySystem;
const terrain = this.scene.terrainSystem;
// 1. Check Cost
if (building.cost.wood) {
if (!inv.hasItem('wood', building.cost.wood)) {
console.log('❌ Not enough Wood!');
this.showFloatingText('Need Wood!', gridX, gridY, '#FF0000');
return true; // We handled the click, even if failed
}
}
if (building.cost.stone) {
if (!inv.hasItem('stone', building.cost.stone)) {
console.log('❌ Not enough Stone!');
this.showFloatingText('Need Stone!', gridX, gridY, '#FF0000');
return true;
}
}
if (building.cost.gold) {
if (inv.gold < building.cost.gold) {
console.log('❌ Not enough Gold!');
this.showFloatingText('Need Gold!', gridX, gridY, '#FF0000');
return true;
}
}
// 2. Check Space
const tile = terrain.getTile(gridX, gridY);
if (!tile || tile.type === 'water' || tile.hasDecoration || tile.hasCrop || tile.hasBuilding) {
console.log('❌ Space occupied!');
this.showFloatingText('Occupied!', gridX, gridY, '#FF0000');
return true;
}
// 3. Consume Resources
if (building.cost.wood) inv.removeItem('wood', building.cost.wood);
if (building.cost.stone) inv.removeItem('stone', building.cost.stone);
if (building.cost.gold) {
inv.gold -= building.cost.gold;
inv.updateUI();
}
// 4. Place Building
// Using decorations layer for now, but marking as building
// Need to add texture to TerrainSystem pool?
// Or better: TerrainSystem should handle 'placing structure'
// Let's modify TerrainSystem to support 'structures' better or just hack decorations
const success = terrain.placeStructure(gridX, gridY, `struct_${this.selectedBuilding}`);
if (success) {
this.showFloatingText(`Built ${building.name}!`, gridX, gridY, '#00FF00');
}
return true;
}
showFloatingText(text, gridX, gridY, color) {
const iso = new IsometricUtils(48, 24);
const pos = iso.toScreen(gridX, gridY);
const popup = this.scene.add.text(
pos.x + this.scene.terrainOffsetX,
pos.y + this.scene.terrainOffsetY - 40,
text,
{ fontSize: '14px', fill: color, stroke: '#000', strokeThickness: 3 }
).setOrigin(0.5);
this.scene.tweens.add({ targets: popup, y: popup.y - 30, alpha: 0, duration: 2000, onComplete: () => popup.destroy() });
}
}

View File

@@ -0,0 +1,91 @@
class DayNightSystem {
constructor(scene, timeSystem) {
this.scene = scene;
this.timeSystem = timeSystem;
// Visual overlay
this.overlay = null;
this.currentPhase = 'day'; // dawn, day, dusk, night
this.init();
}
init() {
// Create lighting overlay
this.overlay = this.scene.add.graphics();
this.overlay.setDepth(4999); // Below weather, above everything else
this.overlay.setScrollFactor(0); // Fixed to camera
}
update() {
if (!this.timeSystem) return;
const hour = this.timeSystem.getCurrentHour();
const phase = this.getPhase(hour);
if (phase !== this.currentPhase) {
this.currentPhase = phase;
console.log(`🌅 Time of Day: ${phase} (${hour}:00)`);
}
this.updateLighting(hour);
}
getPhase(hour) {
if (hour >= 5 && hour < 7) return 'dawn'; // 5-7
if (hour >= 7 && hour < 18) return 'day'; // 7-18
if (hour >= 18 && hour < 20) return 'dusk'; // 18-20
return 'night'; // 20-5
}
updateLighting(hour) {
const width = this.scene.cameras.main.width;
const height = this.scene.cameras.main.height;
this.overlay.clear();
let color = 0x000033; // Default night blue
let alpha = 0;
if (hour >= 0 && hour < 5) {
// Deep Night (0-5h) - Dark blue
color = 0x000033;
alpha = 0.6;
} else if (hour >= 5 && hour < 7) {
// Dawn (5-7h) - Orange/Pink gradient
color = 0xFF6B35;
const progress = (hour - 5) / 2; // 0-1
alpha = 0.6 - (progress * 0.6); // 0.6 -> 0
} else if (hour >= 7 && hour < 18) {
// Day (7-18h) - No overlay (bright)
alpha = 0;
} else if (hour >= 18 && hour < 20) {
// Dusk (18-20h) - Orange/Purple
color = 0x8B4789;
const progress = (hour - 18) / 2; // 0-1
alpha = progress * 0.5; // 0 -> 0.5
} else if (hour >= 20 && hour < 24) {
// Night (20-24h) - Dark blue
color = 0x000033;
const progress = (hour - 20) / 4; // 0-1
alpha = 0.5 + (progress * 0.1); // 0.5 -> 0.6
}
if (alpha > 0) {
this.overlay.fillStyle(color, alpha);
this.overlay.fillRect(0, 0, width, height);
}
}
getCurrentPhase() {
return this.currentPhase;
}
isNight() {
return this.currentPhase === 'night';
}
isDay() {
return this.currentPhase === 'day';
}
}

View File

@@ -0,0 +1,116 @@
class FarmingSystem {
constructor(scene) {
this.scene = scene;
this.growthTimer = 0;
this.growthTickRate = 5000; // Check growth every 5 seconds (real time)
// Or better: based on TimeSystem days?
// For fast testing: rapid growth.
}
// Called by InteractionSystem
interact(gridX, gridY, toolType) {
const terrain = this.scene.terrainSystem;
const tile = terrain.getTile(gridX, gridY);
if (!tile) return false;
// 1. HARVEST (Right click or just click ripe crop?)
// Let's say if it has crop and it is ripe, harvest it regardless of tool.
if (tile.hasCrop) {
const crop = terrain.cropsMap.get(`${gridX},${gridY}`);
if (crop && crop.stage === 4) {
this.harvest(gridX, gridY);
return true;
}
}
// 2. TILLING (Requires Hoe)
if (toolType === 'hoe') {
if (tile.type === 'grass' || tile.type === 'dirt') {
if (!tile.hasDecoration && !tile.hasCrop) {
console.log('🚜 Tilling soil...');
terrain.setTileType(gridX, gridY, 'farmland');
// Play sound
return true;
}
}
}
// 3. PLANTING (Requires Seeds)
if (toolType === 'seeds') {
if (tile.type === 'farmland' && !tile.hasCrop && !tile.hasDecoration) {
console.log('🌱 Planting seeds...');
this.plant(gridX, gridY);
return true; // Consume seed logic handled by caller?
}
}
return false;
}
plant(x, y) {
const terrain = this.scene.terrainSystem;
const cropData = {
gridX: x,
gridY: y,
stage: 1, // Seeds
type: 'wheat', // Default for now
timer: 0,
maxTime: 10 // Seconds per stage?
};
terrain.addCrop(x, y, cropData);
}
harvest(x, y) {
const terrain = this.scene.terrainSystem;
console.log('🌾 Harvesting!');
// Spawn loot
if (this.scene.interactionSystem) {
this.scene.interactionSystem.spawnLoot(x, y, 'wheat');
this.scene.interactionSystem.spawnLoot(x, y, 'seeds'); // Return seeds
// 50% chance for extra seeds
if (Math.random() > 0.5) this.scene.interactionSystem.spawnLoot(x, y, 'seeds');
}
// Remove crop
terrain.removeCrop(x, y);
// Revert to dirt? Or keep farmland? Usually keeps farmland.
}
update(delta) {
// Growth Logic
// Iterate all crops? Expensive?
// Better: Random tick like Minecraft or list iteration.
// Since we have cropsMap, iteration is easy.
// Only run every 1 second (1000ms) to save PERF
this.growthTimer += delta;
if (this.growthTimer < 1000) return;
const secondsPassed = this.growthTimer / 1000;
this.growthTimer = 0;
const terrain = this.scene.terrainSystem;
if (!terrain) return;
for (const [key, crop] of terrain.cropsMap) {
if (crop.stage < 4) {
crop.timer += secondsPassed;
// Growth thresholds (fast for testing)
// Stage 1 -> 2: 5s
// Stage 2 -> 3: 10s
// Stage 3 -> 4: 15s
const needed = 5;
if (crop.timer >= needed) {
crop.stage++;
crop.timer = 0;
terrain.updateCropVisual(crop.gridX, crop.gridY, crop.stage);
// Particle effect?
}
}
}
}
}

View File

@@ -0,0 +1,251 @@
class InteractionSystem {
constructor(scene) {
this.scene = scene;
this.iso = new IsometricUtils(48, 24);
// Input listener setup (only once)
this.scene.input.on('pointerdown', (pointer) => {
if (pointer.button === 0) { // Left Click
this.handleLeftClick(pointer);
}
});
// Loot Array
this.drops = [];
}
handleLeftClick(pointer) {
if (!this.scene.player) return;
// 1. Account for camera and offset
const worldX = pointer.worldX - this.scene.terrainOffsetX;
const worldY = pointer.worldY - this.scene.terrainOffsetY;
// 2. Convert to Grid
const gridPos = this.iso.toGrid(worldX, worldY);
// 3. Check distance
const playerPos = this.scene.player.getPosition();
const dist = Phaser.Math.Distance.Between(playerPos.x, playerPos.y, gridPos.x, gridPos.y);
// Allow interaction within radius of 2.5 tiles
if (dist > 2.5) {
console.log('Too far:', dist.toFixed(1));
return;
}
console.log(`☝️ Clicked tile: ${gridPos.x},${gridPos.y}`);
// DETERMINE TOOL / ACTION
let activeTool = null;
const uiScene = this.scene.scene.get('UIScene');
const invSys = this.scene.inventorySystem;
if (uiScene && invSys) {
const selectedIdx = uiScene.selectedSlot;
const slotData = invSys.slots[selectedIdx];
if (slotData) activeTool = slotData.type;
}
// 0. Build Mode Override
if (this.scene.buildingSystem && this.scene.buildingSystem.isBuildMode) {
this.scene.buildingSystem.tryBuild(gridPos.x, gridPos.y);
return; // Consume click
}
// 3.5 Check for NPC Click
if (this.scene.npcs) {
for (const npc of this.scene.npcs) {
if (Math.abs(npc.gridX - gridPos.x) < 2.5 && Math.abs(npc.gridY - gridPos.y) < 2.5) {
console.log(`🗣️ Interact with NPC: ${npc.type}`);
if (npc.type === 'zombie') {
// Taming Logic
npc.toggleState();
return; // Done
}
if (npc.type === 'merchant') {
// Open Trade Menu
if (uiScene && invSys) {
uiScene.showTradeMenu(invSys);
}
return; // Stop processing
}
return; // Stop processing other clicks (farming/terrain) if clicked NPC
}
}
}
// 4. Try Farming Action (Tilling, Planting, Harvesting)
if (this.scene.farmingSystem) {
const didFarm = this.scene.farmingSystem.interact(gridPos.x, gridPos.y, activeTool);
if (didFarm) {
// Animation?
return;
}
}
// 5. Try damage decoration (fallback)
// 5. Try damage or interact decoration
if (this.scene.terrainSystem) {
const id = `${gridPos.x},${gridPos.y}`;
if (this.scene.terrainSystem.decorationsMap.has(id)) {
const decor = this.scene.terrainSystem.decorationsMap.get(id);
// Ruin Interaction - Town Restoration
if (decor.type === 'ruin' || decor.type === 'ruin_borut') {
// Check if near
if (dist > 2.5) {
console.log('Ruin too far.');
return;
}
// Show Project Menu
const uiScene = this.scene.scene.get('UIScene');
if (uiScene) {
// Define requirements based on ruin type
let req = { reqWood: 100, reqStone: 50, reqGold: 50 };
let ruinName = "Borut's Smithy"; // Default
let npcType = 'merchant';
if (decor.type === 'ruin') {
ruinName = "Merchant House";
req = { reqWood: 50, reqStone: 30, reqGold: 30 };
}
uiScene.showProjectMenu(req, () => {
// On Contribute Logic
if (invSys) {
const hasWood = invSys.hasItem('wood', req.reqWood);
const hasStone = invSys.hasItem('stone', req.reqStone || 0);
const hasGold = invSys.hasItem('gold', req.reqGold || 0);
// Check all requirements
if (hasWood && hasStone && hasGold) {
// Consume materials
invSys.removeItem('wood', req.reqWood);
invSys.removeItem('stone', req.reqStone);
invSys.removeItem('gold', req.reqGold);
invSys.updateUI();
console.log(`🏗️ Restoring ${ruinName}...`);
// Transform Ruin -> House
this.scene.terrainSystem.removeDecoration(gridPos.x, gridPos.y);
this.scene.terrainSystem.placeStructure(gridPos.x, gridPos.y, 'house');
// Spawn NPC nearby
const npc = new NPC(this.scene, gridPos.x + 1, gridPos.y + 1,
this.scene.terrainOffsetX, this.scene.terrainOffsetY, npcType);
this.scene.npcs.push(npc);
// Increase friendship (hearts)
if (this.scene.statsSystem) {
this.scene.statsSystem.addFriendship(npcType, 10); // +10 hearts
}
console.log(`${ruinName} Restored! +10 ❤️ Friendship`);
// Play build sound
if (this.scene.soundManager) this.scene.soundManager.playBuild();
} else {
// Not enough materials
const missing = [];
if (!hasWood) missing.push(`${req.reqWood} Wood`);
if (!hasStone) missing.push(`${req.reqStone} Stone`);
if (!hasGold) missing.push(`${req.reqGold} Gold`);
console.log(`❌ Not enough materials! Need: ${missing.join(', ')}`);
alert(`Potrebuješ še: ${missing.join(', ')} da obnoviš ${ruinName}.`);
}
}
});
}
return; // Don't damage it
}
}
const result = this.scene.terrainSystem.damageDecoration(gridPos.x, gridPos.y, 1);
if (result === 'destroyed') {
// Play chop sound
if (this.scene.soundManager) this.scene.soundManager.playChop();
// Spawn loot
this.spawnLoot(gridPos.x, gridPos.y, 'wood');
} else if (result === 'hit') {
// Play hit sound
if (this.scene.soundManager) this.scene.soundManager.playChop();
}
}
}
spawnLoot(gridX, gridY, type) {
console.log(`🎁 Spawning ${type} at ${gridX},${gridY}`);
// Convert to Screen
const screenPos = this.iso.toScreen(gridX, gridY);
const x = screenPos.x + this.scene.terrainOffsetX;
const y = screenPos.y + this.scene.terrainOffsetY;
// Create simplistic item drop sprite
let symbol = '?';
if (type === 'wood') symbol = '🪵';
if (type === 'seeds') symbol = '🌱';
if (type === 'wheat') symbol = '🌾';
if (type === 'hoe') symbol = '🛠️';
const drop = this.scene.add.text(x, y - 20, symbol, { fontSize: '20px' });
drop.setOrigin(0.5);
drop.setDepth(this.iso.getDepth(gridX, gridY) + 500); // above tiles
// Bounce animation
this.scene.tweens.add({
targets: drop,
y: y - 40,
duration: 500,
yoyo: true,
ease: 'Sine.easeOut',
repeat: -1
});
this.drops.push({
gridX,
gridY,
sprite: drop,
type: type
});
}
update() {
// Check for player pickup
if (!this.scene.player) return;
const playerPos = this.scene.player.getPosition();
// Filter drops to pick up
for (let i = this.drops.length - 1; i >= 0; i--) {
const drop = this.drops[i];
// Check if player is ON the drop tile
if (Math.abs(drop.gridX - playerPos.x) < 0.8 && Math.abs(drop.gridY - playerPos.y) < 0.8) {
// Pick up!
console.log('🎒 Picked up:', drop.type);
// Play pickup sound
if (this.scene.soundManager) this.scene.soundManager.playPickup();
// Add to inventory
if (this.scene.inventorySystem) {
this.scene.inventorySystem.addItem(drop.type, 1);
}
// Destroy visual
drop.sprite.destroy();
this.drops.splice(i, 1);
}
}
}
}

View File

@@ -0,0 +1,85 @@
class InventorySystem {
constructor(scene) {
this.scene = scene;
// Data structure: Array of slots
// Each slot: { type: 'wood', count: 5 } or null
this.slots = new Array(9).fill(null);
this.maxStack = 99;
// Initial test items
this.addItem('hoe', 1);
this.addItem('seeds', 10);
this.addItem('wood', 100); // For restoration
this.addItem('stone', 100); // For restoration
this.gold = 0;
}
addItem(type, count) {
// 1. Try to stack
for (let i = 0; i < this.slots.length; i++) {
if (this.slots[i] && this.slots[i].type === type) {
const space = this.maxStack - this.slots[i].count;
if (space > 0) {
const toAdd = Math.min(space, count);
this.slots[i].count += toAdd;
count -= toAdd;
if (count === 0) break;
}
}
}
// 2. Empty slots
if (count > 0) {
for (let i = 0; i < this.slots.length; i++) {
if (!this.slots[i]) {
const toAdd = Math.min(this.maxStack, count);
this.slots[i] = { type: type, count: toAdd };
count -= toAdd;
if (count === 0) break;
}
}
}
this.updateUI();
return count === 0; // True if everything added
}
removeItem(type, count) {
for (let i = 0; i < this.slots.length; i++) {
if (this.slots[i] && this.slots[i].type === type) {
if (this.slots[i].count >= count) {
this.slots[i].count -= count;
if (this.slots[i].count === 0) this.slots[i] = null;
this.updateUI();
return true;
} else {
count -= this.slots[i].count;
this.slots[i] = null;
}
}
}
this.updateUI();
return false; // Not enough items
}
updateUI() {
const uiScene = this.scene.scene.get('UIScene');
if (uiScene) {
uiScene.updateInventory(this.slots);
if (uiScene.updateGold) uiScene.updateGold(this.gold);
}
}
hasItem(type, count) {
let total = 0;
for (const slot of this.slots) {
if (slot && slot.type === type) {
total += slot.count;
}
}
return total >= count;
}
}

View File

@@ -0,0 +1,198 @@
class ParallaxSystem {
constructor(scene) {
this.scene = scene;
this.layers = [];
// Layer depths (Phaser depth sorting)
this.DEPTH = {
SKY: -1000,
DISTANT_HILLS: -500,
FAR_TREES: -100,
TERRAIN: 0,
GAME_OBJECTS: 1000,
FOREGROUND_GRASS: 5000,
FOREGROUND_LEAVES: 5500
};
this.init();
}
init() {
console.log('🌄 ParallaxSystem: Initialized');
// Layer 1: Sky/Hills (Distant background)
this.createSkyLayer();
this.createDistantHills();
// Layer 4: Foreground overlay (High grass patches)
this.createForegroundGrass();
}
createSkyLayer() {
// Gradient sky rectangle
const width = 3000;
const height = 2000;
const skyBg = this.scene.add.rectangle(0, 0, width, height, 0x87CEEB); // Sky blue
skyBg.setOrigin(0, 0);
skyBg.setScrollFactor(0); // Fixed (no parallax)
skyBg.setDepth(this.DEPTH.SKY);
this.layers.push({
name: 'sky',
objects: [skyBg],
scrollFactor: 0
});
}
createDistantHills() {
// Create simple hill silhouettes in background
const hillCount = 5;
const hills = [];
for (let i = 0; i < hillCount; i++) {
const x = i * 800 - 1000;
const y = 600;
const width = Phaser.Math.Between(400, 800);
const height = Phaser.Math.Between(150, 300);
// Create hill as ellipse
const hill = this.scene.add.ellipse(x, y, width, height, 0x4a5f3a); // Dark green
hill.setAlpha(0.4);
hill.setDepth(this.DEPTH.DISTANT_HILLS);
hill.setScrollFactor(0.2, 0.2); // Slow parallax
hills.push(hill);
}
this.layers.push({
name: 'distant_hills',
objects: hills,
scrollFactor: 0.2
});
}
createForegroundGrass() {
// Create tall grass patches that appear in front of player
const grassPatches = [];
const patchCount = 30;
for (let i = 0; i < patchCount; i++) {
const x = Phaser.Math.Between(-500, 2500);
const y = Phaser.Math.Between(-500, 2500);
const grass = this.createGrassPatch(x, y);
grass.setDepth(this.DEPTH.FOREGROUND_GRASS);
grass.setScrollFactor(1.05, 1.05); // Slight forward parallax
grass.setAlpha(0.6);
grassPatches.push(grass);
}
this.layers.push({
name: 'foreground_grass',
objects: grassPatches,
scrollFactor: 1.05
});
}
createGrassPatch(x, y) {
// Create procedural grass patch
const graphics = this.scene.add.graphics();
// Draw several grass blades
for (let i = 0; i < 6; i++) {
const offsetX = Phaser.Math.Between(-10, 10);
const offsetY = Phaser.Math.Between(-5, 5);
const height = Phaser.Math.Between(20, 40);
graphics.fillStyle(0x3a5f2a, 0.8); // Dark grass green
// Draw grass blade (thin triangle)
graphics.beginPath();
graphics.moveTo(x + offsetX, y + offsetY);
graphics.lineTo(x + offsetX - 2, y + offsetY + height);
graphics.lineTo(x + offsetX + 2, y + offsetY + height);
graphics.closePath();
graphics.fillPath();
}
return graphics;
}
update(playerX, playerY) {
// Update foreground grass visibility based on player position
// Hide/show grass patches that are too close to player for better gameplay
const foregroundLayer = this.layers.find(l => l.name === 'foreground_grass');
if (!foregroundLayer) return;
for (const grass of foregroundLayer.objects) {
const distance = Phaser.Math.Distance.Between(
grass.x, grass.y,
playerX, playerY
);
// Fade out grass when player is very close
if (distance < 50) {
grass.setAlpha(0.2);
} else if (distance < 100) {
grass.setAlpha(0.4);
} else {
grass.setAlpha(0.6);
}
}
}
addFarTree(x, y) {
// Add a background tree (Layer 2)
if (!this.scene.textures.exists('tree')) return;
const tree = this.scene.add.sprite(x, y, 'tree');
tree.setDepth(this.DEPTH.FAR_TREES);
tree.setScrollFactor(0.7, 0.7); // Medium parallax
tree.setAlpha(0.7);
tree.setScale(1.5);
return tree;
}
addForegroundLeaves(x, y) {
// Add falling leaves or branch overlay (Layer 4)
const graphics = this.scene.add.graphics();
// Draw some leaves
for (let i = 0; i < 3; i++) {
const leafX = x + Phaser.Math.Between(-20, 20);
const leafY = y + Phaser.Math.Between(-10, 10);
graphics.fillStyle(0x2d4a1f, 0.5); // Dark green leaf
graphics.fillEllipse(leafX, leafY, 8, 12);
}
graphics.setDepth(this.DEPTH.FOREGROUND_LEAVES);
graphics.setScrollFactor(1.1, 1.1); // Fastest parallax (closest)
return graphics;
}
clearLayer(layerName) {
const layer = this.layers.find(l => l.name === layerName);
if (!layer) return;
for (const obj of layer.objects) {
obj.destroy();
}
layer.objects = [];
}
destroy() {
for (const layer of this.layers) {
for (const obj of layer.objects) {
obj.destroy();
}
}
this.layers = [];
}
}

246
src/systems/SaveSystem.js Normal file
View File

@@ -0,0 +1,246 @@
class SaveSystem {
constructor(scene) {
this.scene = scene;
this.storageKey = 'novafarma_savefile';
}
saveGame() {
console.log('💾 Saving game...');
if (!this.scene.player || !this.scene.terrainSystem) {
console.error('Cannot save: Player or TerrainSystem missing.');
return;
}
const playerPos = this.scene.player.getPosition();
// Zberi podatke o NPCjih
const npcsData = this.scene.npcs.map(npc => ({
type: npc.type,
x: npc.gridX,
y: npc.gridY
}));
// Zberi podatke o terenu
const terrainSeed = this.scene.terrainSystem.noise.seed;
// Zberi dinamične podatke terena (Crops & Modified Decor/Buildings)
const cropsData = Array.from(this.scene.terrainSystem.cropsMap.entries());
// We only save Decorations that are NOT default?
// For simplicity, let's save ALL current decorations, and on Load clear everything and rebuild.
// Actually, decorationsMap contains objects with gridX, gridY, type.
const decorData = Array.from(this.scene.terrainSystem.decorationsMap.values());
// Inventory
const inventoryData = {
slots: this.scene.inventorySystem.slots,
gold: this.scene.inventorySystem.gold || 0
};
const saveData = {
version: 1.1,
timestamp: Date.now(),
player: { x: playerPos.x, y: playerPos.y },
terrain: {
seed: terrainSeed,
crops: cropsData, // array of [key, value]
decorations: decorData // array of objects
},
npcs: npcsData,
inventory: inventoryData,
time: {
gameTime: this.scene.timeSystem ? this.scene.timeSystem.gameTime : 8,
dayCount: this.scene.timeSystem ? this.scene.timeSystem.dayCount : 1
},
stats: this.scene.statsSystem ? {
health: this.scene.statsSystem.health,
hunger: this.scene.statsSystem.hunger,
thirst: this.scene.statsSystem.thirst
} : null,
camera: { zoom: this.scene.cameras.main.zoom }
};
try {
const jsonString = JSON.stringify(saveData);
localStorage.setItem(this.storageKey, jsonString);
// Pokaži obvestilo (preko UIScene če obstaja)
this.showNotification('GAME SAVED');
console.log('✅ Game saved successfully!', saveData);
} catch (e) {
console.error('❌ Failed to save game:', e);
this.showNotification('SAVE FAILED');
}
}
loadGame() {
console.log('📂 Loading game...');
const jsonString = localStorage.getItem(this.storageKey);
if (!jsonString) {
console.log('⚠️ No save file found.');
this.showNotification('NO SAVE FOUND');
return false;
}
try {
const saveData = JSON.parse(jsonString);
console.log('Loading save data:', saveData);
// 1. Load Player
if (this.scene.player) {
// Zahteva metodo setPosition(gridX, gridY) v Player.js
// Trenutno imamo moveToGrid ampak za instant load rabimo direkten set.
// Uporabimo updatePosition logic iz NPC, ali pa kar moveToGrid s hitrostjo 0?
// Bolje dodati setGridPosition v Player.js.
// Za zdaj workaround:
this.scene.player.gridX = saveData.player.x;
this.scene.player.gridY = saveData.player.y;
// Force update screen pos
const screenPos = this.scene.player.iso.toScreen(saveData.player.x, saveData.player.y);
this.scene.player.sprite.setPosition(
screenPos.x + this.scene.player.offsetX,
screenPos.y + this.scene.player.offsetY
);
this.scene.player.updateDepth();
}
// 2. Load Terrain (Regenerate + Restore dynamic)
if (this.scene.terrainSystem && saveData.terrain) {
// A) Seed / Base Terrain
if (saveData.terrain.seed && this.scene.terrainSystem.noise.seed !== saveData.terrain.seed) {
// Regenerate world if seed mismatch
// (Actually we might want to ALWAYS regenerate to clear default decors then overwrite?)
// Current logic: generate() spawns default decorations.
// To handle persistence properly:
// 1. Clear current decorations
// 2. Load saved decorations
this.scene.terrainSystem.noise = new PerlinNoise(saveData.terrain.seed);
// this.scene.terrainSystem.generate(); // This re-adds random flowers
// Instead of full generate, we might just re-calc tiles if seed changed?
// For now assume seed is constant for "New Game", but let's re-run generate to be safe
}
// Clear EVERYTHING first
this.scene.terrainSystem.decorationsMap.clear();
this.scene.terrainSystem.decorations = [];
this.scene.terrainSystem.cropsMap.clear();
// We should also hide active sprites?
this.scene.terrainSystem.visibleDecorations.forEach(s => s.setVisible(false));
this.scene.terrainSystem.visibleDecorations.clear();
this.scene.terrainSystem.visibleCrops.forEach(s => s.setVisible(false));
this.scene.terrainSystem.visibleCrops.clear();
this.scene.terrainSystem.decorationPool.releaseAll();
this.scene.terrainSystem.cropPool.releaseAll();
// B) Restore Crops
if (saveData.terrain.crops) {
// Map was saved as array of entries
saveData.terrain.crops.forEach(entry => {
const [key, cropData] = entry;
this.scene.terrainSystem.cropsMap.set(key, cropData);
// Set flag on tile
const [gx, gy] = key.split(',').map(Number);
const tile = this.scene.terrainSystem.getTile(gx, gy);
if (tile) tile.hasCrop = true;
});
}
// C) Restore Decorations (Flowers, Houses, Walls, Fences...)
if (saveData.terrain.decorations) {
saveData.terrain.decorations.forEach(d => {
this.scene.terrainSystem.decorations.push(d);
this.scene.terrainSystem.decorationsMap.set(d.id, d);
const tile = this.scene.terrainSystem.getTile(d.gridX, d.gridY);
if (tile) tile.hasDecoration = true;
});
}
// Force Update Visuals
this.scene.terrainSystem.updateCulling(this.scene.cameras.main);
}
// 3. Load Inventory
if (this.scene.inventorySystem && saveData.inventory) {
this.scene.inventorySystem.slots = saveData.inventory.slots;
this.scene.inventorySystem.gold = saveData.inventory.gold;
this.scene.inventorySystem.updateUI();
}
// 4. Load Time & Stats
if (this.scene.timeSystem && saveData.time) {
this.scene.timeSystem.gameTime = saveData.time.gameTime;
this.scene.timeSystem.dayCount = saveData.time.dayCount || 1;
}
if (this.scene.statsSystem && saveData.stats) {
this.scene.statsSystem.health = saveData.stats.health;
this.scene.statsSystem.hunger = saveData.stats.hunger;
this.scene.statsSystem.thirst = saveData.stats.thirst;
}
// 3. Load NPCs
// Pobriši trenutne
this.scene.npcs.forEach(npc => npc.destroy());
this.scene.npcs = [];
// Ustvari shranjene
if (saveData.npcs) {
saveData.npcs.forEach(npcData => {
const npc = new NPC(
this.scene,
npcData.x,
npcData.y,
this.scene.terrainOffsetX,
this.scene.terrainOffsetY,
npcData.type
);
this.scene.npcs.push(npc);
});
}
// 4. Camera
if (saveData.camera) {
this.scene.cameras.main.setZoom(saveData.camera.zoom);
}
this.showNotification('GAME LOADED');
return true;
} catch (e) {
console.error('❌ Failed to load game:', e);
this.showNotification('LOAD FAILED');
return false;
}
}
showNotification(text) {
const uiScene = this.scene.scene.get('UIScene');
if (uiScene) {
const width = uiScene.cameras.main.width;
const height = uiScene.cameras.main.height;
const msg = uiScene.add.text(width / 2, height / 2, text, {
fontFamily: 'Courier New',
fontSize: '32px',
fill: '#ffffff',
backgroundColor: '#000000',
padding: { x: 10, y: 5 }
});
msg.setOrigin(0.5);
msg.setScrollFactor(0);
uiScene.tweens.add({
targets: msg,
alpha: 0,
duration: 2000,
delay: 500,
onComplete: () => {
msg.destroy();
}
});
}
}
}

148
src/systems/SoundManager.js Normal file
View File

@@ -0,0 +1,148 @@
class SoundManager {
constructor(scene) {
this.scene = scene;
this.musicVolume = 0.3;
this.sfxVolume = 0.5;
this.isMuted = false;
this.currentMusic = null;
this.currentAmbient = null;
console.log('🎵 SoundManager: Initialized');
}
playSFX(key) {
if (this.isMuted) return;
if (this.scene.sound.get(key)) {
this.scene.sound.play(key, { volume: this.sfxVolume });
} else {
// Enhanced placeholder beeps
if (key === 'chop') {
this.beepChop();
} else if (key === 'pickup') {
this.beepPickup();
} else if (key === 'plant') {
this.beepPlant();
} else if (key === 'harvest') {
this.beepHarvest();
} else if (key === 'build') {
this.beepBuild();
}
}
}
beepChop() {
if (!this.scene.sound.context) return;
const ctx = this.scene.sound.context;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.frequency.value = 150;
osc.type = 'sawtooth';
gain.gain.setValueAtTime(0.15, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.15);
osc.start();
osc.stop(ctx.currentTime + 0.15);
}
beepPickup() {
if (!this.scene.sound.context) return;
const ctx = this.scene.sound.context;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.frequency.setValueAtTime(600, ctx.currentTime);
osc.frequency.linearRampToValueAtTime(1200, ctx.currentTime + 0.1);
osc.type = 'sine';
gain.gain.setValueAtTime(0.12, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.1);
osc.start();
osc.stop(ctx.currentTime + 0.1);
}
beepPlant() {
if (!this.scene.sound.context) return;
const ctx = this.scene.sound.context;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.frequency.value = 300;
osc.type = 'triangle';
gain.gain.setValueAtTime(0.1, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.12);
osc.start();
osc.stop(ctx.currentTime + 0.12);
}
beepHarvest() {
if (!this.scene.sound.context) return;
const ctx = this.scene.sound.context;
const osc1 = ctx.createOscillator();
const gain1 = ctx.createGain();
osc1.connect(gain1);
gain1.connect(ctx.destination);
osc1.frequency.value = 523;
osc1.type = 'sine';
gain1.gain.setValueAtTime(0.1, ctx.currentTime);
gain1.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.08);
osc1.start();
osc1.stop(ctx.currentTime + 0.08);
const osc2 = ctx.createOscillator();
const gain2 = ctx.createGain();
osc2.connect(gain2);
gain2.connect(ctx.destination);
osc2.frequency.value = 659;
osc2.type = 'sine';
gain2.gain.setValueAtTime(0.1, ctx.currentTime + 0.08);
gain2.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.16);
osc2.start(ctx.currentTime + 0.08);
osc2.stop(ctx.currentTime + 0.16);
}
beepBuild() {
if (!this.scene.sound.context) return;
const ctx = this.scene.sound.context;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.frequency.value = 80;
osc.type = 'square';
gain.gain.setValueAtTime(0.2, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.2);
osc.start();
osc.stop(ctx.currentTime + 0.2);
}
playAmbient(key, loop = true) {
if (this.isMuted) return;
if (this.currentAmbient) this.currentAmbient.stop();
if (!this.scene.sound.get(key)) return;
this.currentAmbient = this.scene.sound.add(key, { volume: this.sfxVolume * 0.5, loop: loop });
this.currentAmbient.play();
}
stopAmbient() {
if (this.currentAmbient) {
this.currentAmbient.stop();
this.currentAmbient = null;
}
}
toggleMute() {
this.isMuted = !this.isMuted;
this.scene.sound.mute = this.isMuted;
console.log(this.isMuted ? '🔇 Muted' : '🔊 Unmuted');
}
playChop() { this.playSFX('chop'); }
playPlant() { this.playSFX('plant'); }
playHarvest() { this.playSFX('harvest'); }
playBuild() { this.playSFX('build'); }
playPickup() { this.playSFX('pickup'); }
playRainSound() { this.playAmbient('rain_loop'); }
stopRainSound() { this.stopAmbient(); }
}

135
src/systems/StatsSystem.js Normal file
View File

@@ -0,0 +1,135 @@
class StatsSystem {
constructor(scene) {
this.scene = scene;
// Stats
this.health = 100;
this.maxHealth = 100;
this.hunger = 100; // 100 = full
this.maxHunger = 100;
this.thirst = 100; // 100 = not thirsty
this.maxThirst = 100;
// Decay rates (per second)
this.hungerDecay = 0.5; // Pade na 0 v 200s (cca 3 min)
this.thirstDecay = 0.8; // Pade na 0 v 125s (cca 2 min)
this.damageTickTimer = 0;
// Friendship System (Hearts ❤️)
this.friendship = {
merchant: 0,
zombie: 0,
villager: 0
};
}
update(delta) {
const seconds = delta / 1000;
// Decay
if (this.hunger > 0) {
this.hunger -= this.hungerDecay * seconds;
}
if (this.thirst > 0) {
this.thirst -= this.thirstDecay * seconds;
}
// Clamp values
this.hunger = Math.max(0, this.hunger);
this.thirst = Math.max(0, this.thirst);
// Starvation / Dehydration logic
if (this.hunger <= 0 || this.thirst <= 0) {
this.damageTickTimer += delta;
if (this.damageTickTimer >= 1000) { // Vsako sekundo damage
this.damageTickTimer = 0;
this.takeDamage(5); // 5 DMG na sekundo če si lačen/žejen
// Shake camera effect za opozorilo
this.scene.cameras.main.shake(100, 0.005);
}
} else {
this.damageTickTimer = 0;
// Natural regeneration if full
if (this.hunger > 80 && this.thirst > 80 && this.health < this.maxHealth) {
this.health += 1 * seconds;
}
}
this.health = Math.min(this.health, this.maxHealth);
this.updateUI();
}
takeDamage(amount) {
this.health -= amount;
if (this.health <= 0) {
this.health = 0;
this.die();
}
}
eat(amount) {
this.hunger += amount;
this.hunger = Math.min(this.hunger, this.maxHunger);
}
drink(amount) {
this.thirst += amount;
this.thirst = Math.min(this.thirst, this.maxThirst);
}
die() {
console.log('💀 Player died!');
// Zaenkrat samo respawn / reset
this.health = 100;
this.hunger = 100;
this.thirst = 100;
// Teleport to spawn
const spawnX = this.scene.terrainOffsetX + 500; // Dummy
const spawnY = this.scene.terrainOffsetY + 100; // Dummy
// Reset player pos...
// V pravi implementaciji bi klicali GameScene.respawnPlayer()
// Show notification
const uiScene = this.scene.scene.get('UIScene');
if (uiScene) {
const txt = uiScene.add.text(uiScene.width / 2, uiScene.height / 2, 'YOU DIED', {
fontSize: '64px', color: '#ff0000', fontStyle: 'bold'
}).setOrigin(0.5);
uiScene.time.delayedCall(2000, () => txt.destroy());
}
}
updateUI() {
const uiScene = this.scene.scene.get('UIScene');
if (uiScene) {
if (uiScene.healthBar) uiScene.setBarValue(uiScene.healthBar, this.health);
if (uiScene.hungerBar) uiScene.setBarValue(uiScene.hungerBar, this.hunger);
if (uiScene.thirstBar) uiScene.setBarValue(uiScene.thirstBar, this.thirst);
}
}
// Friendship System
addFriendship(npcType, amount) {
if (this.friendship[npcType] !== undefined) {
this.friendship[npcType] += amount;
console.log(`❤️ +${amount} Friendship with ${npcType} (Total: ${this.friendship[npcType]})`);
}
}
getFriendship(npcType) {
return this.friendship[npcType] || 0;
}
setFriendship(npcType, amount) {
if (this.friendship[npcType] !== undefined) {
this.friendship[npcType] = amount;
}
}
}

View File

@@ -1,5 +1,5 @@
// Terrain Generator System
// Generira proceduralni isometrični teren
// Generira proceduralni isometrični teren in skrbi za optimizacijo (Culling, Object Pooling)
class TerrainSystem {
constructor(scene, width = 100, height = 100) {
this.scene = scene;
@@ -10,43 +10,670 @@ class TerrainSystem {
this.noise = new PerlinNoise(Date.now());
this.tiles = [];
this.tileSprites = [];
this.decorations = []; // Array za save/load compat
this.decorationsMap = new Map(); // Fast lookup key->decor
this.cropsMap = new Map(); // Store dynamic crops separately
// Tipi terena z threshold vrednostmi
// Render state monitoring
this.visibleTiles = new Map(); // Key: "x,y", Value: Sprite
this.visibleDecorations = new Map(); // Key: "x,y", Value: Sprite
this.visibleCrops = new Map(); // Key: "x,y", Value: Sprite
this.offsetX = 0;
this.offsetY = 0;
// Object Pools
this.tilePool = new ObjectPool(
() => {
const sprite = this.scene.add.image(0, 0, 'tile_grass');
sprite.setOrigin(0.5, 0); // Isometrični tiles imajo origin zgoraj/center ali po potrebi
return sprite;
},
(sprite) => {
sprite.setVisible(true);
sprite.setAlpha(1);
sprite.clearTint();
}
);
this.decorationPool = new ObjectPool(
() => {
const sprite = this.scene.add.sprite(0, 0, 'flower');
sprite.setOrigin(0.5, 1);
return sprite;
},
(sprite) => {
sprite.setVisible(true);
sprite.setAlpha(1);
sprite.clearTint(); // Reset damage tint
}
);
this.cropPool = new ObjectPool(
() => {
const sprite = this.scene.add.sprite(0, 0, 'crop_stage_1'); // Default texture logic needed
sprite.setOrigin(0.5, 1);
return sprite;
},
(sprite) => {
sprite.setVisible(true);
sprite.setAlpha(1);
}
);
// Tipi terena z threshold vrednostmi + Y-LAYER STACKING
this.terrainTypes = {
WATER: { threshold: 0.3, color: 0x2166aa, name: 'water' },
SAND: { threshold: 0.4, color: 0xf4e7c6, name: 'sand' },
GRASS: { threshold: 0.65, color: 0x5cb85c, name: 'grass' },
DIRT: { threshold: 0.75, color: 0x8b6f47, name: 'dirt' },
STONE: { threshold: 1.0, color: 0x7d7d7d, name: 'stone' }
WATER: { threshold: 0.3, color: 0x2166aa, name: 'water', texture: 'tile_water', yLayer: -1 },
SAND: { threshold: 0.4, color: 0xf4e7c6, name: 'sand', texture: 'tile_sand', yLayer: 0 },
// Y-LAYER GRASS VARIANTS (A, B, C systém)
GRASS_FULL: { threshold: 0.50, color: 0x5cb85c, name: 'grass_full', texture: 'tile_grass_full', yLayer: 0 }, // A: Full grass
GRASS_TOP: { threshold: 0.60, color: 0x5cb85c, name: 'grass_top', texture: 'tile_grass_top', yLayer: 1 }, // B: Grass top, dirt sides
DIRT: { threshold: 0.75, color: 0x8b6f47, name: 'dirt', texture: 'tile_dirt', yLayer: 2 }, // C: Full dirt
STONE: { threshold: 1.0, color: 0x7d7d7d, name: 'stone', texture: 'tile_stone', yLayer: 3 },
FARMLAND: { threshold: 999, color: 0x4a3c2a, name: 'farmland', texture: 'tile_farmland', yLayer: 0 }
};
}
// Generiraj teren
// Helper da dobi terrain type glede na elevation (Y-layer)
getTerrainTypeByElevation(noiseValue, elevation) {
// Osnovni terrain type iz noise
let baseType = this.getTerrainType(noiseValue);
// Če je grass, določi Y-layer variant glede na elevation
if (baseType.name.includes('grass') || baseType === this.terrainTypes.GRASS_FULL ||
baseType === this.terrainTypes.GRASS_TOP) {
if (elevation > 0.7) {
return this.terrainTypes.GRASS_FULL; // A: Najvišja plast (full grass)
} else if (elevation > 0.4) {
return this.terrainTypes.GRASS_TOP; // B: Srednja (grass top, dirt sides)
} else {
return this.terrainTypes.DIRT; // C: Nizka (full dirt)
}
}
return baseType;
}
// Generiraj teksture za tiles (da ne uporabljamo Počasnih Graphics objektov)
createTileTextures() {
console.log('🎨 Creating tile textures...');
for (const type of Object.values(this.terrainTypes)) {
const key = `tile_${type.name}`;
if (this.scene.textures.exists(key)) continue;
// Check for custom grass tile
if (type.name === 'grass' && this.scene.textures.exists('grass_tile')) {
this.scene.textures.addImage(key, this.scene.textures.get('grass_tile').getSourceImage());
type.texture = key;
continue; // Skip procedural generation
}
const tileW = this.iso.tileWidth;
const tileH = this.iso.tileHeight;
const thickness = 25; // Minecraft-style thickness (increased from 10)
const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
// Helper for colors
const baseColor = Phaser.Display.Color.IntegerToColor(type.color);
let darkColor, darkerColor;
// Y-LAYER STACKING: Different side colors based on layer
if (type.name === 'grass_full') {
// A: Full Grass Block - všechno zeleno
darkColor = 0x5cb85c; // Green (lighter)
darkerColor = 0x4a9d3f; // Green (darker)
} else if (type.name === 'grass_top') {
// B: Grass Top - zgoraj zeleno, stranice zemlja
darkColor = 0x8b6f47; // Dirt color (brown)
darkerColor = 0x5d4a2e; // Darker Dirt
} else if (type.name === 'dirt') {
// C: Full Dirt - vse rjavo
darkColor = 0x8b6f47; // Dirt
darkerColor = 0x654321; // Darker Dirt
} else {
// Standard block: Darken base color significantly
darkColor = Phaser.Display.Color.IntegerToColor(type.color).darken(30).color;
darkerColor = Phaser.Display.Color.IntegerToColor(type.color).darken(50).color;
}
// 1. Draw LEFT Side (Darker) - Minecraft volumetric effect
graphics.fillStyle(darkColor, 1);
graphics.beginPath();
graphics.moveTo(0, tileH / 2); // Left Corner
graphics.lineTo(tileW / 2, tileH); // Bottom Corner
graphics.lineTo(tileW / 2, tileH + thickness); // Bottom Corner (Lowered)
graphics.lineTo(0, tileH / 2 + thickness); // Left Corner (Lowered)
graphics.closePath();
graphics.fillPath();
// 2. Draw RIGHT Side (Darkest) - Strong shadow
graphics.fillStyle(darkerColor, 1);
graphics.beginPath();
graphics.moveTo(tileW / 2, tileH); // Bottom Corner
graphics.lineTo(tileW, tileH / 2); // Right Corner
graphics.lineTo(tileW, tileH / 2 + thickness); // Right Corner (Lowered)
graphics.lineTo(tileW / 2, tileH + thickness); // Bottom Corner (Lowered)
graphics.closePath();
graphics.fillPath();
// 3. Draw TOP Surface (bright)
graphics.fillStyle(type.color, 1);
graphics.beginPath();
graphics.moveTo(tileW / 2, 0); // Top
graphics.lineTo(tileW, tileH / 2); // Right
graphics.lineTo(tileW / 2, tileH); // Bottom
graphics.lineTo(0, tileH / 2); // Left
graphics.closePath();
graphics.fillPath();
// 4. Add Minecraft-style texture pattern on top
if (type.name === 'grass_full' || type.name === 'grass_top') {
// Grass texture: Random pixel pattern
graphics.fillStyle(0x4a9d3f, 0.3); // Slightly darker green
for (let i = 0; i < 8; i++) {
const px = Math.random() * tileW;
const py = Math.random() * tileH;
graphics.fillRect(px, py, 2, 2);
}
} else if (type.name === 'dirt') {
// Dirt texture: Darker spots
graphics.fillStyle(0x6d5838, 0.4);
for (let i = 0; i < 6; i++) {
const px = Math.random() * tileW;
const py = Math.random() * tileH;
graphics.fillRect(px, py, 3, 3);
}
} else if (type.name === 'stone') {
// Stone texture: Gray spots
graphics.fillStyle(0x666666, 0.3);
for (let i = 0; i < 10; i++) {
const px = Math.random() * tileW;
const py = Math.random() * tileH;
graphics.fillRect(px, py, 2, 1);
}
}
// 5. Crisp black outline for block definition
graphics.lineStyle(1, 0x000000, 0.3);
graphics.strokePath();
// Generate texture
graphics.generateTexture(key, tileW, tileH + thickness);
graphics.destroy();
// Update texture name in type def
type.texture = key;
}
}
createGravestoneSprite() {
// Extract gravestone from objects_pack (approx position in atlas)
// Gravestone appears to be around position row 4, column 4-5 in the pack
const canvas = document.createElement('canvas');
const size = 32; // Approximate sprite size
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const sourceTexture = this.scene.textures.get('objects_pack');
const sourceImg = sourceTexture.getSourceImage();
// Extract gravestone (cross tombstone) - estimated coords
// Adjust these values based on actual sprite sheet layout
const sourceX = 240; // Approximate X position
const sourceY = 160; // Approximate Y position
ctx.drawImage(sourceImg, sourceX, sourceY, size, size, 0, 0, size, size);
// Create texture
this.scene.textures.addCanvas('gravestone', canvas);
console.log('✅ Gravestone sprite extracted!');
}
// Generiraj teren (data only)
generate() {
console.log(`🌍 Generating terrain: ${this.width}x${this.height}...`);
console.log(`🌍 Generating terrain data: ${this.width}x${this.height}...`);
// Zagotovi teksture
this.createTileTextures();
// Zagotovi decoration teksture - check for custom sprites first
if (!this.scene.textures.exists('flower')) {
TextureGenerator.createFlowerSprite(this.scene, 'flower');
}
// Bush - use custom stone sprite if available
if (this.scene.textures.exists('stone_sprite')) {
// Use stone_sprite for bushes (rocks)
if (!this.scene.textures.exists('bush')) {
this.scene.textures.addImage('bush', this.scene.textures.get('stone_sprite').getSourceImage());
}
} else if (!this.scene.textures.exists('bush')) {
TextureGenerator.createBushSprite(this.scene, 'bush');
}
// Tree - use custom tree sprite if available
if (this.scene.textures.exists('tree_sprite')) {
if (!this.scene.textures.exists('tree')) {
this.scene.textures.addImage('tree', this.scene.textures.get('tree_sprite').getSourceImage());
}
} else if (!this.scene.textures.exists('tree')) {
TextureGenerator.createTreeSprite(this.scene, 'tree');
}
// Gravestone - extract from objects_pack
if (this.scene.textures.exists('objects_pack') && !this.scene.textures.exists('gravestone')) {
this.createGravestoneSprite();
}
// Crop textures
for (let i = 1; i <= 4; i++) {
if (!this.scene.textures.exists(`crop_stage_${i}`))
TextureGenerator.createCropSprite(this.scene, `crop_stage_${i}`, i);
}
this.decorationsMap.clear();
this.decorations = [];
this.cropsMap.clear();
// Generiraj tile podatke
for (let y = 0; y < this.height; y++) {
this.tiles[y] = [];
for (let x = 0; x < this.width; x++) {
const noiseValue = this.noise.getNormalized(x, y, 0.05, 4);
const terrainType = this.getTerrainType(noiseValue);
// Elevation (druga Perlin noise layer za hribe)
let elevation = this.noise.getNormalized(x, y, 0.03, 3);
// Get terrain type based on BOTH noise and elevation (Y-layer)
let terrainType = this.getTerrainTypeByElevation(noiseValue, elevation);
// === FLOATING ISLAND EDGE ===
const edgeDistance = 2; // Tiles from edge (tighter border)
const isNearEdge = x < edgeDistance || x >= this.width - edgeDistance ||
y < edgeDistance || y >= this.height - edgeDistance;
const isEdge = x === 0 || x === this.width - 1 ||
y === 0 || y === this.height - 1;
// Override terrain type at edges
if (isEdge) {
terrainType = this.terrainTypes.STONE; // Cliff wall (stone edge)
} else if (isNearEdge) {
// Keep Y-layer system active
}
// Flatten edges (cliff drop-off for floating island effect)
if (isEdge) {
elevation = 0; // Flat cliff wall
} else if (isNearEdge) {
elevation = Math.max(0, elevation - 0.2); // Slight dip near edge
}
this.tiles[y][x] = {
gridX: x,
gridY: y,
type: terrainType.name,
color: terrainType.color,
height: noiseValue
texture: terrainType.texture,
height: noiseValue,
elevation: elevation, // 0-1 (0=low, 1=high)
yLayer: terrainType.yLayer, // Y-stacking layer
hasDecoration: false,
hasCrop: false
};
// Generacija dekoracij (shranimo v data, ne ustvarjamo sprite-ov še)
if (x > 5 && x < this.width - 5 && y > 5 && y < this.height - 5) {
let decorType = null;
let maxHp = 1;
if (terrainType.name === 'grass') {
const rand = Math.random();
// Na hribih več kamnov
if (elevation > 0.6 && rand < 0.05) {
decorType = 'bush'; // Kamni (bomo kasneje naredili 'stone' tip)
maxHp = 5;
} else if (rand < 0.01) {
decorType = 'tree';
maxHp = 5;
} else if (rand < 0.015) {
decorType = 'gravestone'; // 💀 Nagrobniki
maxHp = 10; // Težje uničiti
} else if (rand < 0.1) {
decorType = 'flower';
maxHp = 1;
}
} else if (terrainType.name === 'dirt' && Math.random() < 0.02) {
decorType = 'bush';
maxHp = 3;
}
if (decorType) {
const key = `${x},${y}`;
const decorData = {
gridX: x,
gridY: y,
type: decorType,
id: key,
maxHp: maxHp,
hp: maxHp
};
this.decorations.push(decorData);
this.decorationsMap.set(key, decorData);
this.tiles[y][x].hasDecoration = true;
}
}
}
}
console.log('✅ Terrain data generated!');
console.log('✅ Terrain and decorations data generated!');
}
// Določi tip terena glede na noise vrednost
// DAMAGE / INTERACTION LOGIC
damageDecoration(x, y, amount) {
const key = `${x},${y}`;
const decor = this.decorationsMap.get(key);
if (!decor) return false;
decor.hp -= amount;
// Visual feedback (flash red)
if (this.visibleDecorations.has(key)) {
const sprite = this.visibleDecorations.get(key);
sprite.setTint(0xff0000);
this.scene.time.delayedCall(100, () => sprite.clearTint());
// Shake effect?
this.scene.tweens.add({
targets: sprite,
x: sprite.x + 2,
duration: 50,
yoyo: true,
repeat: 1
});
}
if (decor.hp <= 0) {
this.removeDecoration(x, y);
return 'destroyed';
}
return 'hit';
}
removeDecoration(x, y) {
const key = `${x},${y}`;
const decor = this.decorationsMap.get(key);
if (!decor) return;
// Remove visual
if (this.visibleDecorations.has(key)) {
const sprite = this.visibleDecorations.get(key);
sprite.setVisible(false);
this.decorationPool.release(sprite);
this.visibleDecorations.delete(key);
}
// Remove data
this.decorationsMap.delete(key);
// Remove from array (slow but needed for save compat for now)
const index = this.decorations.indexOf(decor);
if (index > -1) this.decorations.splice(index, 1);
// Update tile flag
if (this.tiles[y] && this.tiles[y][x]) {
this.tiles[y][x].hasDecoration = false;
}
return decor.type; // Return type for dropping loot
}
placeStructure(x, y, structureType) {
if (this.decorationsMap.has(`${x},${y}`)) return false;
const decorData = {
gridX: x,
gridY: y,
type: structureType, // 'struct_fence', etc.
id: `${x},${y}`,
maxHp: 5,
hp: 5
};
// Add to data
this.decorations.push(decorData);
this.decorationsMap.set(decorData.id, decorData);
// Update tile
const tile = this.getTile(x, y);
if (tile) tile.hasDecoration = true;
// Force Visual Update immediately?
// updateCulling will catch it on next frame, but to be safe:
// Or leave it to update loop.
return true;
}
// --- Dynamic Tile Modification ---
setTileType(x, y, typeName) {
if (!this.tiles[y] || !this.tiles[y][x]) return;
const typeDef = Object.values(this.terrainTypes).find(t => t.name === typeName);
if (!typeDef) return;
this.tiles[y][x].type = typeName;
this.tiles[y][x].texture = typeDef.texture;
// Force visual update if visible
const key = `${x},${y}`;
if (this.visibleTiles.has(key)) {
const sprite = this.visibleTiles.get(key);
sprite.setTexture(typeDef.texture);
}
}
addCrop(x, y, cropData) {
const key = `${x},${y}`;
this.cropsMap.set(key, cropData);
this.tiles[y][x].hasCrop = true;
// updateCulling loop will pick it up on next frame
}
removeCrop(x, y) {
const key = `${x},${y}`;
if (this.cropsMap.has(key)) {
// Remove visual
if (this.visibleCrops.has(key)) {
const sprite = this.visibleCrops.get(key);
sprite.setVisible(false);
this.cropPool.release(sprite);
this.visibleCrops.delete(key);
}
this.cropsMap.delete(key);
this.tiles[y][x].hasCrop = false;
}
}
updateCropVisual(x, y, stage) {
const key = `${x},${y}`;
if (this.visibleCrops.has(key)) {
const sprite = this.visibleCrops.get(key);
sprite.setTexture(`crop_stage_${stage}`);
}
}
// Initialize rendering (called once)
init(offsetX, offsetY) {
this.offsetX = offsetX;
this.offsetY = offsetY;
}
// Update culling (called every frame)
updateCulling(camera) {
const view = camera.worldView;
const buffer = 200;
const left = view.x - buffer - this.offsetX;
const top = view.y - buffer - this.offsetY;
const right = view.x + view.width + buffer - this.offsetX;
const bottom = view.y + view.height + buffer - this.offsetY;
// Calculate visible bounding box (rough)
const p1 = this.iso.toGrid(left, top);
const p2 = this.iso.toGrid(right, top);
const p3 = this.iso.toGrid(left, bottom);
const p4 = this.iso.toGrid(right, bottom);
const minGridX = Math.floor(Math.min(p1.x, p2.x, p3.x, p4.x));
const maxGridX = Math.ceil(Math.max(p1.x, p2.x, p3.x, p4.x));
const minGridY = Math.floor(Math.min(p1.y, p2.y, p3.y, p4.y));
const maxGridY = Math.ceil(Math.max(p1.y, p2.y, p3.y, p4.y));
const startX = Math.max(0, minGridX);
const endX = Math.min(this.width, maxGridX);
const startY = Math.max(0, minGridY);
const endY = Math.min(this.height, maxGridY);
const neededKeys = new Set();
const neededDecorKeys = new Set();
const neededCropKeys = new Set();
for (let y = startY; y < endY; y++) {
for (let x = startX; x < endX; x++) {
const key = `${x},${y}`;
neededKeys.add(key);
// Tile Logic
if (!this.visibleTiles.has(key)) {
const tilePos = this.iso.toScreen(x, y);
const tileData = this.tiles[y][x];
const sprite = this.tilePool.get();
sprite.setTexture(tileData.texture);
// Elevation effect: MOČAN vertikalni offset za hribe
const elevationOffset = tileData.elevation * -25; // Povečano iz -10 na -25
sprite.setPosition(
tilePos.x + this.offsetX,
tilePos.y + this.offsetY + elevationOffset
);
// DRAMATIČNO senčenje glede na višino
if (tileData.type === 'grass') {
let brightness = 1.0;
if (tileData.elevation > 0.5) {
// Visoko = svetlo (1.0 - 1.5)
brightness = 1.0 + (tileData.elevation - 0.5) * 1.0;
} else {
// Nizko = temno (0.7 - 1.0)
brightness = 0.7 + tileData.elevation * 0.6;
}
sprite.setTint(Phaser.Display.Color.GetColor(
Math.min(255, Math.floor(92 * brightness)),
Math.min(255, Math.floor(184 * brightness)),
Math.min(255, Math.floor(92 * brightness))
));
}
sprite.setDepth(this.iso.getDepth(x, y));
this.visibleTiles.set(key, sprite);
}
// Crop Logic (render before decor or after? Same layer mostly)
if (this.tiles[y][x].hasCrop) {
neededCropKeys.add(key);
if (!this.visibleCrops.has(key)) {
const cropData = this.cropsMap.get(key);
if (cropData) {
const cropPos = this.iso.toScreen(x, y);
const sprite = this.cropPool.get();
sprite.setTexture(`crop_stage_${cropData.stage}`);
sprite.setPosition(
cropPos.x + this.offsetX,
cropPos.y + this.offsetY + this.iso.tileHeight / 2
);
const depth = this.iso.getDepth(x, y);
sprite.setDepth(depth + 1); // Just slightly above tile
this.visibleCrops.set(key, sprite);
}
}
}
// Decoration Logic
if (this.tiles[y][x].hasDecoration) {
neededDecorKeys.add(key);
if (!this.visibleDecorations.has(key)) {
// Fast lookup from map
const decor = this.decorationsMap.get(key);
if (decor) {
const decorPos = this.iso.toScreen(x, y);
const sprite = this.decorationPool.get();
sprite.setTexture(decor.type);
sprite.setPosition(
decorPos.x + this.offsetX,
decorPos.y + this.offsetY + this.iso.tileHeight / 2
);
const depth = this.iso.getDepth(x, y);
if (decor.type === 'flower') sprite.setDepth(depth + 1);
else sprite.setDepth(depth + 1000); // Taller objects update depth
sprite.flipX = (x + y) % 2 === 0;
this.visibleDecorations.set(key, sprite);
}
}
}
}
}
// Cleanup invisible tiles
for (const [key, sprite] of this.visibleTiles) {
if (!neededKeys.has(key)) {
sprite.setVisible(false);
this.tilePool.release(sprite);
this.visibleTiles.delete(key);
}
}
// Cleanup invisible decorations
for (const [key, sprite] of this.visibleDecorations) {
if (!neededDecorKeys.has(key)) {
sprite.setVisible(false);
this.decorationPool.release(sprite);
this.visibleDecorations.delete(key);
}
}
// Cleanup visible crops
for (const [key, sprite] of this.visibleCrops) {
if (!neededCropKeys.has(key)) {
sprite.setVisible(false);
this.cropPool.release(sprite);
this.visibleCrops.delete(key);
}
}
}
// Helper functions
getTerrainType(value) {
for (const type of Object.values(this.terrainTypes)) {
if (value < type.threshold) {
@@ -56,71 +683,10 @@ class TerrainSystem {
return this.terrainTypes.STONE;
}
// Renderaj teren (visual sprites)
render(offsetX = 0, offsetY = 300) {
console.log('🎨 Rendering terrain sprites...');
const container = this.scene.add.container(offsetX, offsetY);
// Renderaj vse tile-e
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
const tile = this.tiles[y][x];
const screenPos = this.iso.toScreen(x, y);
// Kreira diamond (romb) obliko za isometric tile
const graphics = this.scene.add.graphics();
// Osnovna barva
const baseColor = tile.color;
graphics.fillStyle(baseColor, 1);
// Nariši isometric tile (diamond shape)
const tileWidth = this.iso.tileWidth;
const tileHeight = this.iso.tileHeight;
graphics.beginPath();
graphics.moveTo(screenPos.x, screenPos.y); // Top
graphics.lineTo(screenPos.x + tileWidth / 2, screenPos.y + tileHeight / 2); // Right
graphics.lineTo(screenPos.x, screenPos.y + tileHeight); // Bottom
graphics.lineTo(screenPos.x - tileWidth / 2, screenPos.y + tileHeight / 2); // Left
graphics.closePath();
graphics.fillPath();
// Outline za boljšo vidljivost
graphics.lineStyle(1, 0x000000, 0.2);
graphics.strokePath();
// Dodaj v container
container.add(graphics);
// Shrani referenco
this.tileSprites.push({
graphics: graphics,
tile: tile,
depth: this.iso.getDepth(x, y)
});
}
}
// Sortiraj po depth
container.setDepth(0);
console.log(`✅ Rendered ${this.tileSprites.length} tiles!`);
return container;
}
// Pridobi tile na določenih grid koordinatah
getTile(gridX, gridY) {
if (gridX >= 0 && gridX < this.width && gridY >= 0 && gridY < this.height) {
return this.tiles[gridY][gridX];
}
return null;
}
// Screen koordinate -> tile
getTileAtScreen(screenX, screenY) {
const grid = this.iso.toGrid(screenX, screenY);
return this.getTile(grid.x, grid.y);
}
}

107
src/systems/TimeSystem.js Normal file
View File

@@ -0,0 +1,107 @@
class TimeSystem {
constructor(scene) {
this.scene = scene;
// Konfiguracija
this.fullDaySeconds = 300; // 5 minut za cel dan (real-time)
this.startTime = 8; // Začetek ob 8:00
// Stanje
this.gameTime = this.startTime; // 0 - 24
this.dayCount = 1;
// Lighting overlay
this.lightOverlay = null; // Ustvarjen bo v GameScene ali tu? Bolje tu če imamo dostop.
}
create() {
// Overlay za temo
// Uporabimo velik pravokotnik čez cel ekran, ki je fixiran na kamero
// Ampak ker imamo UIScene, je bolje da je overlay v GameScene, ampak nad vsem razen UI.
// Najlažje: canvas tinting ali graphics overlay.
// Za preprostost: Modificiramo ambient light ali tintamo igralca/teren?
// Phaser 3 ima setTint.
// Najboljši efekt za 2D: Temno moder rectangle z 'MULTIPLY' blend mode čez GameScene.
const width = this.scene.cameras.main.width * 2; // Malo večji za varnost
const height = this.scene.cameras.main.height * 2;
this.lightOverlay = this.scene.add.rectangle(0, 0, width, height, 0x000022);
this.lightOverlay.setScrollFactor(0);
this.lightOverlay.setDepth(9000); // Pod UI (UI je v drugi sceni), ampak nad igro
this.lightOverlay.setBlendMode(Phaser.BlendModes.MULTIPLY);
this.lightOverlay.setAlpha(0); // Začetek dan (0 alpha)
}
update(delta) {
// Povečaj čas
// delta je v ms.
// fullDaySeconds = 24 game hours.
// 1 game hour = fullDaySeconds / 24 seconds.
const seconds = delta / 1000;
const gameHoursPerRealSecond = 24 / this.fullDaySeconds;
this.gameTime += seconds * gameHoursPerRealSecond;
if (this.gameTime >= 24) {
this.gameTime -= 24;
this.dayCount++;
console.log(`🌞 Day ${this.dayCount} started!`);
}
this.updateLighting();
this.updateUI();
}
updateLighting() {
if (!this.lightOverlay) return;
// Izračunaj svetlobo (Alpha vrednost senc)
// 0 = Dan (Prozoren overlay)
// 0.8 = Polnoč (Temen overlay)
let alpha = 0;
const t = this.gameTime;
// Preprosta logika:
// 6:00 - 18:00 = Dan (0 alpha)
// 18:00 - 20:00 = Mrak (prehod 0 -> 0.7)
// 20:00 - 4:00 = Noč (0.7 alpha)
// 4:00 - 6:00 = Jutro (prehod 0.7 -> 0)
if (t >= 6 && t < 18) {
alpha = 0; // Dan
} else if (t >= 18 && t < 20) {
alpha = ((t - 18) / 2) * 0.7; // Mrak
} else if (t >= 20 || t < 4) {
alpha = 0.7; // Noč
} else if (t >= 4 && t < 6) {
alpha = 0.7 - ((t - 4) / 2) * 0.7; // Jutro
}
this.lightOverlay.setAlpha(alpha);
}
updateUI() {
const uiScene = this.scene.scene.get('UIScene');
if (uiScene && uiScene.clockText) {
const hours = Math.floor(this.gameTime);
const minutes = Math.floor((this.gameTime - hours) * 60);
const timeString = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
uiScene.clockText.setText(`Day ${this.dayCount} - ${timeString}`);
}
}
getCurrentHour() {
return Math.floor(this.gameTime);
}
getGameTime() {
return this.gameTime;
}
getDayCount() {
return this.dayCount;
}
}

View File

@@ -0,0 +1,159 @@
class WeatherSystem {
constructor(scene) {
this.scene = scene;
this.currentWeather = 'clear'; // clear, rain, storm, fog
this.weatherDuration = 0;
this.maxWeatherDuration = 30000; // 30s per weather cycle
// Weather effects containers
this.rainParticles = [];
this.overlay = null;
this.init();
}
init() {
// Create weather overlay
this.overlay = this.scene.add.graphics();
this.overlay.setDepth(5000); // Above everything but UI
this.overlay.setScrollFactor(0); // Fixed to camera
// Start with random weather
this.changeWeather();
}
changeWeather() {
// Clean up old weather
this.clearWeather();
// Random new weather
const weathers = ['clear', 'clear', 'rain', 'fog']; // Clear more common
this.currentWeather = weathers[Math.floor(Math.random() * weathers.length)];
this.weatherDuration = 0;
console.log(`🌦️ Weather changed to: ${this.currentWeather}`);
// Apply new weather effects
this.applyWeather();
}
applyWeather() {
if (this.currentWeather === 'rain') {
this.startRain();
// Play rain sound
if (this.scene.soundManager) {
this.scene.soundManager.playRainSound();
}
} else if (this.currentWeather === 'fog') {
this.applyFog();
} else if (this.currentWeather === 'storm') {
this.startRain(true);
// Play rain sound (louder?)
if (this.scene.soundManager) {
this.scene.soundManager.playRainSound();
}
} else {
// Clear weather - stop ambient sounds
if (this.scene.soundManager) {
this.scene.soundManager.stopRainSound();
}
}
}
startRain(heavy = false) {
const width = this.scene.cameras.main.width;
const height = this.scene.cameras.main.height;
// Create rain drops
const dropCount = heavy ? 150 : 100;
for (let i = 0; i < dropCount; i++) {
const drop = {
x: Math.random() * width,
y: Math.random() * height,
speed: heavy ? Phaser.Math.Between(400, 600) : Phaser.Math.Between(200, 400),
length: heavy ? Phaser.Math.Between(10, 15) : Phaser.Math.Between(5, 10)
};
this.rainParticles.push(drop);
}
// Darken screen slightly
this.overlay.clear();
this.overlay.fillStyle(0x000033, heavy ? 0.3 : 0.2);
this.overlay.fillRect(0, 0, width, height);
}
applyFog() {
const width = this.scene.cameras.main.width;
const height = this.scene.cameras.main.height;
this.overlay.clear();
this.overlay.fillStyle(0xcccccc, 0.4);
this.overlay.fillRect(0, 0, width, height);
}
clearWeather() {
// Remove all weather effects
this.rainParticles = [];
if (this.overlay) {
this.overlay.clear();
}
}
update(delta) {
this.weatherDuration += delta;
// Change weather periodically
if (this.weatherDuration > this.maxWeatherDuration) {
this.changeWeather();
}
// Update rain
if (this.currentWeather === 'rain' || this.currentWeather === 'storm') {
this.updateRain(delta);
}
}
updateRain(delta) {
const height = this.scene.cameras.main.height;
const width = this.scene.cameras.main.width;
// Update drop positions
for (const drop of this.rainParticles) {
drop.y += (drop.speed * delta) / 1000;
// Reset if off screen
if (drop.y > height) {
drop.y = -10;
drop.x = Math.random() * width;
}
}
// Render rain
this.overlay.clear();
// Background darkening
const isDark = this.currentWeather === 'storm' ? 0.3 : 0.2;
this.overlay.fillStyle(0x000033, isDark);
this.overlay.fillRect(0, 0, width, height);
// Draw drops
this.overlay.lineStyle(1, 0x88aaff, 0.5);
for (const drop of this.rainParticles) {
this.overlay.beginPath();
this.overlay.moveTo(drop.x, drop.y);
this.overlay.lineTo(drop.x - 2, drop.y + drop.length);
this.overlay.strokePath();
}
}
getCurrentWeather() {
return this.currentWeather;
}
setWeather(weather) {
this.currentWeather = weather;
this.weatherDuration = 0;
this.applyWeather();
}
}