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