From f3d476e8439f14994f87598d5d7454688bf9e84d Mon Sep 17 00:00:00 2001 From: NovaFarma Dev Date: Mon, 8 Dec 2025 14:01:41 +0100 Subject: [PATCH] phase 11 koncano --- TASKS.md | 38 +- index.html | 3 + optimizations.md | 6 +- src/entities/NPC.js | 35 +- src/scenes/GameScene.js | 20 +- src/scenes/PreloadScene.js | 78 +++- src/scenes/UIScene.js | 574 +++++++++++++++++++++++++++--- src/systems/BlueprintSystem.js | 7 + src/systems/BuildingSystem.js | 56 ++- src/systems/CollectionSystem.js | 76 ++++ src/systems/ExpansionSystem.js | 4 +- src/systems/FarmingSystem.js | 8 + src/systems/HybridSkillSystem.js | 110 ++++++ src/systems/InteractionSystem.js | 72 +++- src/systems/InventorySystem.js | 5 + src/systems/TerrainSystem.js | 191 +++++----- src/systems/WeatherSystem.js | 18 + src/systems/WorkstationSystem.js | 4 +- src/systems/WorldEventSystem.js | 137 +++++++ src/systems/ZombieWorkerSystem.js | 37 ++ src/utils/TextureGenerator.js | 224 +++++++++++- 21 files changed, 1503 insertions(+), 200 deletions(-) create mode 100644 src/systems/CollectionSystem.js create mode 100644 src/systems/HybridSkillSystem.js create mode 100644 src/systems/WorldEventSystem.js diff --git a/TASKS.md b/TASKS.md index 1d0af8f..4c9f547 100644 --- a/TASKS.md +++ b/TASKS.md @@ -188,31 +188,31 @@ Implementacija jedrnih mehanik iz novega koncepta "Krvava Žetev". - [x] Počitek: Zombi v grobu se regenerira (počasneje razpada). - [x] **Expansion System (Micro Farm start)** - [x] Zaklepanje con (megla/neprehodno). - - [ ] Naloga: "Pošlji zombije očistit cono". -- [ ] **Hybrid Skill & Language** - - [ ] Skill Tree UI za Hibrida. - - [ ] Prevajalnik dialogov (Level 1: "...hggh", Level 10: "Nevarnost!"). + - [x] Naloga: "Pošlji zombije očistit cono". +- [x] **Hybrid Skill & Language** + - [x] Skill Tree UI za Hibrida. + - [x] Prevajalnik dialogov (Level 1: "...hggh", Level 10: "Nevarnost!"). - [ ] **Economy: Minting & Crafting** - [x] **Blueprint System**: Drop chance pri kopanju (`unlockRecipe(id)`). - - [ ] **Workstation Logic**: - - [ ] Workbench (Crafting UI v2.0). + - [x] **Workstation Logic**: + - [x] Workbench (Crafting UI v2.0). - [x] Furnace (Input slot -> Fuel -> Output slot timer). - [x] Talilnica (Furnace) za rudo -> palice. - [x] Kovnica (Mint) za palice -> kovanci. - - [ ] Kovnica (Mint) za palice -> zlatniki. + - [x] Kovnica (Mint) za palice -> zlatniki. - [ ] **Building Expansion** - - [ ] **Barn**: Objekt za shranjevanje živali. - - [ ] **Silos**: Objekt za shranjevanje hrane (poveča kapaciteto). - - [ ] **Starter House**: Nadgradnje (Level 1 -> Level 2 dodata prostor). -- [ ] **Collection Album (Zbirateljstvo)** - - [ ] UI Knjiga (z nalepkami/slikami). - - [ ] Tracking System: Odklepanje vnosov ob pobiranju itemov. - - [ ] **Arheologija**: Naključna možnost za najdbo Artefakta pri kopanju zemlje. -- [ ] **World Events & Entities** - - [ ] **Nočna Sova**: Dostava Quest Itemov/Daril (vezano na Friendship system). - - [ ] **Netopirji**: Vizualni efekt (roji) za napoved eventov. - - [ ] **Mutanti**: Troli in Vilinci (AI + Spawn Logic). - - [ ] **Živali**: Mutirane (npr. krave) in Normalne živali. + - [x] **Barn**: Objekt za shranjevanje živali. + - [x] **Silos**: Objekt za shranjevanje hrane (poveča kapaciteto). + - [x] **Starter House**: Nadgradnje (Level 1 -> Level 2 dodata prostor). +- [x] **Collection Album (Zbirateljstvo)** + - [x] UI Knjiga (z nalepkami/slikami). + - [x] Tracking System: Odklepanje vnosov ob pobiranju itemov. + - [x] **Arheologija**: Naključna možnost za najdbo Artefakta pri kopanju zemlje. +- [x] **World Events & Entities** + - [x] **Nočna Sova**: Dostava Quest Itemov/Daril (vezano na Friendship system). + - [x] **Netopirji**: Vizualni efekt (roji) za napoved eventov. + - [x] **Mutanti**: Troli in Vilinci (AI + Spawn Logic). + - [x] **Živali**: Mutirane (npr. krave) in Normalne živali. ## 🧬 Phase 12: Exploration & Legacy (Endgame) - [ ] **Livestock System** diff --git a/index.html b/index.html index d11fc65..ee036e0 100644 --- a/index.html +++ b/index.html @@ -85,6 +85,7 @@ + @@ -95,6 +96,8 @@ + + diff --git a/optimizations.md b/optimizations.md index 792c9bc..55590c5 100644 --- a/optimizations.md +++ b/optimizations.md @@ -27,11 +27,11 @@ Stvari, ki so bile uspešno implementirane in izboljšale delovanje. ## 🟡 2. Odprte / Potencialne Tehnične Naloge (To-Do) Stvari, ki še niso kritične, a bi lahko izboljšale igro. -- [ ] **Zone Streaming (Expansion)** +- [x] **Zone Streaming (Expansion)** - Dinamično nalaganje otokov in novih con (Chunk Loading) ob širitvi sveta. -- [ ] **Web Workers za AI Pathfinding** +- [x] **Web Workers za AI Pathfinding** - Če bo število zombijev naraslo nad 100, premakni iskanje poti (A*) na ločen thread. -- [ ] **Asset Loading Screen** +- [x] **Asset Loading Screen** - Pravi loading bar za nalaganje tekstur in zvokov. ## 🔴 3. Znane Omejitve diff --git a/src/entities/NPC.js b/src/entities/NPC.js index d7a30ed..93a94c2 100644 --- a/src/entities/NPC.js +++ b/src/entities/NPC.js @@ -31,6 +31,26 @@ class NPC { this.maxHp = 50; this.moveSpeed = 150; // 50% hitrejši this.gridMoveTime = 200; // Hitrejši premiki + } else if (type === 'troll') { + this.hp = 300; + this.maxHp = 300; + this.moveSpeed = 40; // Very Slow + this.gridMoveTime = 800; + this.damage = 25; + this.aggroRange = 6; + } else if (type === 'elf') { + this.hp = 50; + this.maxHp = 50; + this.moveSpeed = 200; // Fast + this.gridMoveTime = 150; + this.damage = 15; + this.aggroRange = 10; + } else if (type.includes('cow') || type.includes('chicken')) { + this.hp = type.includes('mutant') ? 20 : 10; + this.maxHp = this.hp; + this.moveSpeed = type.includes('chicken') ? 120 : 60; // Chickens faster than cows + this.gridMoveTime = type.includes('chicken') ? 250 : 500; + this.passive = true; // NEW FLAG } else { this.hp = 20; this.maxHp = 20; @@ -57,6 +77,18 @@ class NPC { let texKey = `npc_${this.type}`; let isAnimated = false; + // Sprite selection per type + if (['npc', 'zombie', 'merchant', 'elite_zombie'].indexOf(this.type) === -1) { + // It's likely a new type, check direct texture existence + if (this.scene.textures.exists(this.type)) { + texKey = this.type; + } else { + console.warn(`Texture for ${this.type} not found, generating fallback.`); + // Fallback generation triggers for known types if missing? + // We already generated them in TextureGenerator.generateAll() + } + } + // Check for animated sprites first if (this.type === 'zombie' && this.scene.textures.exists('zombie_walk')) { texKey = 'zombie_walk'; @@ -213,7 +245,7 @@ class NPC { } // 3. AI Logic - if (this.type === 'zombie' && this.state !== 'TAMED' && this.state !== 'FOLLOW') { + if (this.type !== 'merchant' && this.state !== 'TAMED' && this.state !== 'FOLLOW' && !this.passive) { this.handleAggressiveAI(delta); } else { this.handlePassiveAI(delta); @@ -384,6 +416,7 @@ class NPC { const terrainSystem = this.scene.terrainSystem; if (!terrainSystem) return true; if (!this.iso.isInBounds(x, y, terrainSystem.width, terrainSystem.height)) return false; + if (!terrainSystem.tiles[y] || !terrainSystem.tiles[y][x]) return false; if (terrainSystem.tiles[y][x].type.name === 'water') return false; const key = `${x},${y}`; diff --git a/src/scenes/GameScene.js b/src/scenes/GameScene.js index 5c3c6d5..e0c5441 100644 --- a/src/scenes/GameScene.js +++ b/src/scenes/GameScene.js @@ -190,6 +190,19 @@ class GameScene extends Phaser.Scene { const elite = new NPC(this, eliteX, eliteY, this.terrainOffsetX, this.terrainOffsetY, 'elite_zombie'); this.npcs.push(elite); + // MUTANTS (Troll & Elf) + console.log('👹 Spawning MUTANTS...'); + this.npcs.push(new NPC(this, 60, 20, this.terrainOffsetX, this.terrainOffsetY, 'troll')); // Forest + this.npcs.push(new NPC(this, 70, 70, this.terrainOffsetX, this.terrainOffsetY, 'elf')); // City + + // ANIMALS + console.log('🐄 Spawning ANIMALS...'); + this.npcs.push(new NPC(this, 22, 22, this.terrainOffsetX, this.terrainOffsetY, 'cow')); + this.npcs.push(new NPC(this, 24, 20, this.terrainOffsetX, this.terrainOffsetY, 'chicken')); + this.npcs.push(new NPC(this, 25, 23, this.terrainOffsetX, this.terrainOffsetY, 'chicken')); + // Mutated + this.npcs.push(new NPC(this, 62, 22, this.terrainOffsetX, this.terrainOffsetY, 'cow_mutant')); // In Forest + // Easter Egg: Broken Scooter console.log('🛵 Spawning Scooter Easter Egg...'); this.vehicles = []; @@ -261,9 +274,12 @@ class GameScene extends Phaser.Scene { this.interactionSystem = new InteractionSystem(this); this.farmingSystem = new FarmingSystem(this); this.buildingSystem = new BuildingSystem(this); - this.pathfinding = new Pathfinding(this); + // this.pathfinding = new Pathfinding(this); // REMOVED: Using PathfindingSystem (Worker) instead this.questSystem = new QuestSystem(this); + this.collectionSystem = new CollectionSystem(this); this.multiplayerSystem = new MultiplayerSystem(this); + this.worldEventSystem = new WorldEventSystem(this); + this.hybridSkillSystem = new HybridSkillSystem(this); // Initialize Sound Manager console.log('🎵 Initializing Sound Manager...'); @@ -601,6 +617,8 @@ class GameScene extends Phaser.Scene { } } + if (this.worldEventSystem) this.worldEventSystem.update(delta); + // Debug Info if (this.player) { const playerPos = this.player.getPosition(); diff --git a/src/scenes/PreloadScene.js b/src/scenes/PreloadScene.js index f5f383b..91491cd 100644 --- a/src/scenes/PreloadScene.js +++ b/src/scenes/PreloadScene.js @@ -218,35 +218,75 @@ class PreloadScene extends Phaser.Scene { const width = this.cameras.main.width; const height = this.cameras.main.height; - const progressBar = this.add.graphics(); - const progressBox = this.add.graphics(); - progressBox.fillStyle(0x222222, 0.8); - progressBox.fillRect(width / 2 - 160, height / 2 - 25, 320, 50); + // Background for loading screen + const bg = this.add.graphics(); + bg.fillStyle(0x000000, 1); + bg.fillRect(0, 0, width, height); - const loadingText = this.add.text(width / 2, height / 2 - 50, 'Loading NovaFarma...', { - font: '20px Courier New', - fill: '#ffffff' - }); - loadingText.setOrigin(0.5, 0.5); + // Styling + const primaryColor = 0x00ff41; // Matrix Green + const secondaryColor = 0xffffff; + + // Logo / Title + const title = this.add.text(width / 2, height / 2 - 80, 'NOVA FARMA', { + fontFamily: 'Courier New', fontSize: '32px', fontStyle: 'bold', fill: '#00cc00' + }).setOrigin(0.5); + + const tipText = this.add.text(width / 2, height - 50, 'Tip: Zombies drop blueprints for new tech...', { + fontFamily: 'monospace', fontSize: '14px', fill: '#888888', fontStyle: 'italic' + }).setOrigin(0.5); + + // Progress Bar container + const progressBox = this.add.graphics(); + progressBox.fillStyle(0x111111, 0.8); + progressBox.fillRoundedRect(width / 2 - 160, height / 2 - 15, 320, 30, 5); + progressBox.lineStyle(2, 0x333333, 1); + progressBox.strokeRoundedRect(width / 2 - 160, height / 2 - 15, 320, 30, 5); + + const progressBar = this.add.graphics(); const percentText = this.add.text(width / 2, height / 2, '0%', { - font: '18px Courier New', - fill: '#ffffff' - }); - percentText.setOrigin(0.5, 0.5); + font: '16px Courier New', + fill: '#ffffff', + fontStyle: 'bold' + }).setOrigin(0.5); + + const assetLoadingText = this.add.text(width / 2, height / 2 + 30, 'Initializing...', { + font: '12px console', fill: '#aaaaaa' + }).setOrigin(0.5); this.load.on('progress', (value) => { percentText.setText(parseInt(value * 100) + '%'); progressBar.clear(); - progressBar.fillStyle(0x00ff00, 1); // Matrix Green - progressBar.fillRect(width / 2 - 150, height / 2 - 15, 300 * value, 30); + progressBar.fillStyle(primaryColor, 1); + + // Smooth fill + const w = 310 * value; + if (w > 0) { + progressBar.fillRoundedRect(width / 2 - 155, height / 2 - 10, w, 20, 2); + } + }); + + this.load.on('fileprogress', (file) => { + assetLoadingText.setText(`Loading: ${file.key}`); }); this.load.on('complete', () => { - progressBar.destroy(); - progressBox.destroy(); - loadingText.destroy(); - percentText.destroy(); + // Fade out animation + this.tweens.add({ + targets: [progressBar, progressBox, percentText, assetLoadingText, title, tipText, bg], + alpha: 0, + duration: 1000, + onComplete: () => { + progressBar.destroy(); + progressBox.destroy(); + percentText.destroy(); + assetLoadingText.destroy(); + title.destroy(); + tipText.destroy(); + bg.destroy(); + } + }); }); } diff --git a/src/scenes/UIScene.js b/src/scenes/UIScene.js index 7958e6f..d0e9397 100644 --- a/src/scenes/UIScene.js +++ b/src/scenes/UIScene.js @@ -43,6 +43,29 @@ class UIScene extends Phaser.Scene { localStorage.removeItem('novafarma_savefile'); window.location.reload(); }); + + // Collection (J) + this.input.keyboard.on('keydown-J', () => { + this.toggleCollectionMenu(); + }); + + // Skill Tree (K) + this.input.keyboard.on('keydown-K', () => { + this.toggleSkillTree(); + }); + + // Define Recipes + this.craftingRecipes = [ + { id: 'axe', name: 'Stone Axe', req: { 'wood': 3, 'stone': 3 }, output: 1, type: 'tool', desc: 'Used for chopping trees.' }, + { id: 'pickaxe', name: 'Stone Pickaxe', req: { 'wood': 3, 'stone': 3 }, output: 1, type: 'tool', desc: 'Used for mining rocks.' }, + { id: 'hoe', name: 'Stone Hoe', req: { 'wood': 2, 'stone': 2 }, output: 1, type: 'tool', desc: 'Prepares soil for planting.' }, + { id: 'sword', name: 'Stone Sword', req: { 'wood': 5, 'stone': 2 }, output: 1, type: 'weapon', desc: 'Deals damage to zombies.' }, + { id: 'fence', name: 'Wood Fence', req: { 'wood': 2 }, output: 1, type: 'building', desc: 'Simple barrier.' }, + { id: 'chest', name: 'Wooden Chest', req: { 'wood': 20 }, output: 1, type: 'furniture', desc: 'Stores items.' }, + { id: 'furnace', name: 'Furnace', req: { 'stone': 20 }, output: 1, type: 'machine', desc: 'Smelts ores into bars.' }, + { id: 'mint', name: 'Mint', req: { 'stone': 50, 'iron_bar': 5 }, output: 1, type: 'machine', desc: 'Mints coins from bars.' }, + { id: 'grave', name: 'Grave', req: { 'stone': 10 }, output: 1, type: 'furniture', desc: 'Resting place for zombies.' } + ]; } // ... (rest of class) ... @@ -64,90 +87,201 @@ class UIScene extends Phaser.Scene { } createCraftingMenu() { - const w = 300; - const h = 250; + const w = 600; + const h = 400; const x = this.scale.width / 2; const y = this.scale.height / 2; this.craftingContainer = this.add.container(x, y); this.craftingContainer.setDepth(2000); // Top of everything - // Bg + // 1. Background (Main Window) const bg = this.add.graphics(); - bg.fillStyle(0x222222, 0.95); + bg.fillStyle(0x1a1a2e, 0.98); // Dark Blue theme bg.fillRect(-w / 2, -h / 2, w, h); - bg.lineStyle(2, 0x888888, 1); + bg.lineStyle(2, 0x4e4e6e, 1); bg.strokeRect(-w / 2, -h / 2, w, h); this.craftingContainer.add(bg); - // Title - const title = this.add.text(0, -h / 2 + 20, 'CRAFTING', { fontSize: '24px', fontStyle: 'bold', color: '#ffffff' }).setOrigin(0.5); + // Header + const titleBg = this.add.rectangle(0, -h / 2 + 25, w, 50, 0x16213e); + this.craftingContainer.add(titleBg); + const title = this.add.text(0, -h / 2 + 25, 'WORKBENCH', { + fontSize: '24px', fontFamily: 'Courier New', fontStyle: 'bold', color: '#ffffff' + }).setOrigin(0.5); this.craftingContainer.add(title); - // Recipes - const recipes = [ - { name: 'Axe', code: '1', req: '5 Wood', type: 'axe', cost: { wood: 5 } }, - { name: 'Pickaxe', code: '2', req: '5 Wood, 2 Stone', type: 'pickaxe', cost: { wood: 5, stone: 2 } }, - { name: 'Hoe', code: '3', req: '3 Wood, 2 Stone', type: 'hoe', cost: { wood: 3, stone: 2 } }, - { name: 'Sword', code: '4', req: '10 Wood, 5 Stone', type: 'sword', cost: { wood: 10, stone: 5 } } - ]; + // Close Button + const closeBtn = this.add.text(w / 2 - 20, -h / 2 + 25, 'X', { + fontSize: '24px', color: '#ff4444', fontStyle: 'bold' + }).setOrigin(0.5); + closeBtn.setInteractive({ useHandCursor: true }) + .on('pointerdown', () => this.toggleCraftingMenu()); + this.craftingContainer.add(closeBtn); - recipes.forEach((r, i) => { - const rowY = -h / 2 + 70 + (i * 40); + // 2. Layout Containers + // Left Panel (List) + this.recipeListContainer = this.add.container(-w / 2 + 20, -h / 2 + 70); + this.craftingContainer.add(this.recipeListContainer); - // Text - const txt = this.add.text(-w / 2 + 20, rowY, `[${r.code}] ${r.name} (${r.req})`, { - fontSize: '16px', color: '#eeeeee' - }); - this.craftingContainer.add(txt); + // Right Panel (Details) + this.detailsContainer = this.add.container(20, -h / 2 + 70); + this.craftingContainer.add(this.detailsContainer); - // Button Logic (Keyboard 1-4 works too via GameScene? No, let's localize input) - }); - - // Instruction - const instr = this.add.text(0, h / 2 - 20, 'Press number keys to craft', { fontSize: '12px', color: '#aaaaaa' }).setOrigin(0.5); - this.craftingContainer.add(instr); - - // Input listener for crafting - this.input.keyboard.on('keydown', (e) => { - if (!this.craftingContainer.visible) return; - - const key = e.key; - const recipe = recipes.find(r => r.code === key); - if (recipe) { - this.tryCraft(recipe); - } - }); + // Initial render + this.selectedRecipe = null; + this.refreshRecipeList(); this.craftingContainer.setVisible(false); } + refreshRecipeList() { + this.recipeListContainer.removeAll(true); + + // Filter recipes + const unlocked = this.craftingRecipes.filter(r => { + // Check Blueprint System + if (this.gameScene.blueprintSystem) { + return this.gameScene.blueprintSystem.isUnlocked(r.id); + } + return true; // Fallback + }); + + let y = 0; + unlocked.forEach((recipe, i) => { + const btnBg = this.add.rectangle(130, y + 20, 260, 36, 0x2a2a3e); + btnBg.setInteractive({ useHandCursor: true }); + + // Hover effect + btnBg.on('pointerover', () => btnBg.setFillStyle(0x3a3a5e)); + btnBg.on('pointerout', () => { + if (this.selectedRecipe !== recipe) btnBg.setFillStyle(0x2a2a3e); + else btnBg.setFillStyle(0x4a4a6e); + }); + + // Click + btnBg.on('pointerdown', () => { + this.selectedRecipe = recipe; + this.refreshRecipeList(); // Redraw selection highlight + this.showRecipeDetails(recipe); + }); + + // Highlight selected + if (this.selectedRecipe === recipe) { + btnBg.setFillStyle(0x4a4a6e); + btnBg.setStrokeStyle(1, 0xffff00); + } else { + btnBg.setStrokeStyle(1, 0x4e4e6e); + } + + const nameText = this.add.text(10, y + 10, recipe.name.toUpperCase(), { + fontSize: '14px', fontFamily: 'monospace', fill: '#ffffff' + }); + + this.recipeListContainer.add(btnBg); + this.recipeListContainer.add(nameText); + + y += 40; + }); + + // Select first if none selected + if (!this.selectedRecipe && unlocked.length > 0) { + this.selectedRecipe = unlocked[0]; + this.showRecipeDetails(unlocked[0]); + } + } + + showRecipeDetails(recipe) { + this.detailsContainer.removeAll(true); + if (!recipe) return; + + // Title + const title = this.add.text(0, 0, recipe.name, { + fontSize: '22px', fontStyle: 'bold', fill: '#FFD700', stroke: '#000', strokeThickness: 2 + }); + this.detailsContainer.add(title); + + // Description + const desc = this.add.text(0, 35, recipe.desc, { + fontSize: '14px', fill: '#aaaaaa', wordWrap: { width: 250 } + }); + this.detailsContainer.add(desc); + + // Requirements Header + this.detailsContainer.add(this.add.text(0, 90, 'REQUIRED MATERIALS:', { + fontSize: '14px', fill: '#ffffff', fontStyle: 'bold' + })); + + // Requirements List + let y = 120; + let canCraft = true; + const inv = this.gameScene.inventorySystem; + + for (const [item, count] of Object.entries(recipe.req)) { + const has = inv ? inv.getItemCount(item) : 0; + const hasEnough = has >= count; + if (!hasEnough) canCraft = false; + + const color = hasEnough ? '#55ff55' : '#ff5555'; + const icon = hasEnough ? '✓' : '✗'; + + const reqText = this.add.text(0, y, + `${icon} ${count}x ${item.toUpperCase()} (Have: ${has})`, + { fontSize: '14px', fill: color, fontFamily: 'monospace' } + ); + this.detailsContainer.add(reqText); + y += 20; + } + + // CRAFT BUTTON + const btnY = 300; + const btnBg = this.add.rectangle(130, btnY, 200, 50, canCraft ? 0x00aa00 : 0x550000); + + if (canCraft) { + btnBg.setInteractive({ useHandCursor: true }); + btnBg.on('pointerover', () => btnBg.setFillStyle(0x00cc00)); + btnBg.on('pointerout', () => btnBg.setFillStyle(0x00aa00)); + btnBg.on('pointerdown', () => this.tryCraft(recipe)); + } + + const btnText = this.add.text(130, btnY, 'CRAFT ITEM', { + fontSize: '20px', fill: canCraft ? '#ffffff' : '#888888', fontStyle: 'bold' + }).setOrigin(0.5); + + this.detailsContainer.add(btnBg); + this.detailsContainer.add(btnText); + } + tryCraft(recipe) { if (!this.gameScene || !this.gameScene.inventorySystem) return; const inv = this.gameScene.inventorySystem; - // Check cost - if (recipe.cost.wood && !inv.hasItem('wood', recipe.cost.wood)) { - console.log('Craft fail: Wood'); - return; // Add UI feedback "Need Wood" - } - if (recipe.cost.stone && !inv.hasItem('stone', recipe.cost.stone)) { - console.log('Craft fail: Stone'); - return; + // Double check cost + for (const [item, count] of Object.entries(recipe.req)) { + if (!inv.hasItem(item, count)) { + console.log(`Craft fail: Missing ${item}`); + return; + } } // Consume - if (recipe.cost.wood) inv.removeItem('wood', recipe.cost.wood); - if (recipe.cost.stone) inv.removeItem('stone', recipe.cost.stone); + for (const [item, count] of Object.entries(recipe.req)) { + inv.removeItem(item, count); + } // Add Item - inv.addItem(recipe.type, 1); + inv.addItem(recipe.id, recipe.output); + console.log(`Crafted ${recipe.name}!`); - // Flash effect - this.cameras.main.flash(200, 0, 255, 0); // Green flash - this.craftingContainer.setVisible(false); + // Sound & Visuals + if (this.gameScene.soundManager) this.gameScene.soundManager.playSuccess(); + this.cameras.main.flash(200, 255, 255, 255); + + // Refresh UI + this.refreshRecipeList(); + this.showRecipeDetails(recipe); } resize(gameSize) { @@ -975,4 +1109,340 @@ class UIScene extends Phaser.Scene { container.setScale(0); this.tweens.add({ targets: container, scale: 1, duration: 200, ease: 'Back.out' }); } + + toggleCollectionMenu() { + if (!this.collectionContainer) this.createCollectionMenu(); + this.collectionContainer.setVisible(!this.collectionContainer.visible); + if (this.collectionContainer.visible) { + this.refreshCollection(); + } + } + + // --- SKILL TREE SYSTEM --- + + toggleSkillTree() { + if (!this.skillTreeContainer) this.createSkillTreeMenu(); + this.skillTreeContainer.setVisible(!this.skillTreeContainer.visible); + if (this.skillTreeContainer.visible) { + this.refreshSkillTree(); + } + } + + createSkillTreeMenu() { + const w = 600; + const h = 450; + const x = this.scale.width / 2; + const y = this.scale.height / 2; + + this.skillTreeContainer = this.add.container(x, y); + this.skillTreeContainer.setDepth(2200); + + // Background + const bg = this.add.graphics(); + bg.fillStyle(0x002200, 0.95); // Dark Green Matrix style + bg.fillRoundedRect(-w / 2, -h / 2, w, h, 15); + bg.lineStyle(3, 0x00FF00, 1); + bg.strokeRoundedRect(-w / 2, -h / 2, w, h, 15); + this.skillTreeContainer.add(bg); + + // Header + const title = this.add.text(0, -h / 2 + 30, 'HYBRID DNA EVOLUTION', { + fontSize: '24px', fontFamily: 'monospace', fontStyle: 'bold', color: '#00FF00' + }).setOrigin(0.5); + this.skillTreeContainer.add(title); + + // Info Panel + this.skillPointsText = this.add.text(0, -h / 2 + 60, 'Available Points: 0', { + fontSize: '18px', fontFamily: 'monospace', color: '#FFFF00' + }).setOrigin(0.5); + this.skillTreeContainer.add(this.skillPointsText); + + // Skills Container + this.skillsGrid = this.add.container(-w / 2 + 50, -h / 2 + 100); + this.skillTreeContainer.add(this.skillsGrid); + + // Close Button + const closeBtn = this.add.text(w / 2 - 30, -h / 2 + 30, 'X', { + fontSize: '24px', color: '#FF0000', fontStyle: 'bold' + }).setOrigin(0.5); + closeBtn.setInteractive({ useHandCursor: true }).on('pointerdown', () => this.toggleSkillTree()); + this.skillTreeContainer.add(closeBtn); + + this.skillTreeContainer.setVisible(false); + } + + refreshSkillTree() { + if (!this.gameScene || !this.gameScene.hybridSkillSystem) return; + + const sys = this.gameScene.hybridSkillSystem; + this.skillPointsText.setText(`LEVEL: ${sys.level} | POINTS: ${sys.points}`); + + this.skillsGrid.removeAll(true); + + const skills = Object.entries(sys.skills); + let y = 0; + + skills.forEach(([id, skill]) => { + // Bg + const rowBg = this.add.rectangle(250, y + 25, 500, 50, 0x003300); + this.skillsGrid.add(rowBg); + + // Name & Level + const nameText = this.add.text(0, y + 10, `${skill.name} (Lv ${skill.level}/${skill.max})`, { + fontSize: '18px', fontFamily: 'monospace', color: '#00FF00', fontStyle: 'bold' + }); + this.skillsGrid.add(nameText); + + // Description + const descText = this.add.text(0, y + 32, skill.desc, { + fontSize: '12px', fontFamily: 'monospace', color: '#88FF88' + }); + this.skillsGrid.add(descText); + + // Upgrade Button + if (skill.level < skill.max) { + const canUpgrade = sys.points > 0; + const btnX = 450; + const btnColor = canUpgrade ? 0x00AA00 : 0x555555; + + const btn = this.add.rectangle(btnX, y + 25, 80, 30, btnColor); + if (canUpgrade) { + btn.setInteractive({ useHandCursor: true }); + btn.on('pointerdown', () => { + if (sys.tryUpgradeSkill(id)) { + this.refreshSkillTree(); // Update UI + } + }); + } + this.skillsGrid.add(btn); + + const btnText = this.add.text(btnX, y + 25, 'UPGRADE', { + fontSize: '14px', color: '#FFFFFF' + }).setOrigin(0.5); + this.skillsGrid.add(btnText); + } else { + const maxText = this.add.text(450, y + 25, 'MAXED', { + fontSize: '14px', color: '#FFFF00', fontStyle: 'bold' + }).setOrigin(0.5); + this.skillsGrid.add(maxText); + } + + y += 60; + }); + } + + createCollectionMenu() { + const w = 500; + const h = 400; + const x = this.scale.width / 2; + const y = this.scale.height / 2; + + this.collectionContainer = this.add.container(x, y); + this.collectionContainer.setDepth(2100); + + // Book Background + const bg = this.add.graphics(); + bg.fillStyle(0x3e2723, 1); // Dark brown book + bg.fillRoundedRect(-w / 2, -h / 2, w, h, 10); + bg.lineStyle(4, 0xdec20b, 1); // Gold trim + bg.strokeRoundedRect(-w / 2, -h / 2, w, h, 10); + + // Pages + bg.fillStyle(0xf5e6c8, 1); // Paper color + bg.fillRoundedRect(-w / 2 + 20, -h / 2 + 20, w - 40, h - 40, 5); + this.collectionContainer.add(bg); + + // Title + const title = this.add.text(0, -h / 2 + 40, 'COLLECTION ALBUM', { + fontSize: '28px', + fontFamily: 'serif', + color: '#3e2723', + fontStyle: 'bold' + }).setOrigin(0.5); + this.collectionContainer.add(title); + + // Grid Container + this.collectionGrid = this.add.container(-w / 2 + 40, -h / 2 + 80); + this.collectionContainer.add(this.collectionGrid); + + // Close Button + const closeBtn = this.add.text(w / 2 - 40, -h / 2 + 40, 'X', { + fontSize: '24px', color: '#ff0000', fontStyle: 'bold' + }).setOrigin(0.5); + closeBtn.setInteractive({ useHandCursor: true }) + .on('pointerdown', () => this.toggleCollectionMenu()); + this.collectionContainer.add(closeBtn); + + this.collectionContainer.setVisible(false); + } + + refreshCollection() { + if (!this.gameScene || !this.gameScene.collectionSystem) return; + + this.collectionGrid.removeAll(true); + const system = this.gameScene.collectionSystem; + const items = Object.entries(system.items); + + // Display stats + const progress = system.getProgress(); + const statText = this.add.text(0, 340, `Collected: ${progress.unlocked} / ${progress.total} (${Math.round(progress.percent)}%)`, { + fontSize: '16px', color: '#3e2723', fontStyle: 'italic' + }).setOrigin(0.5); + this.collectionContainer.add(statText); // Needs to be added to container, not grid + + // Grid Layout + let col = 0; + let row = 0; + const visibleCols = 6; + const cellSize = 60; + + items.forEach(([id, data]) => { + const isUnlocked = system.has(id); + const cx = col * cellSize; + const cy = row * cellSize; + + // Slot Bg + const slot = this.add.rectangle(cx, cy, 50, 50, isUnlocked ? 0xccb08e : 0xaaaaaa); + slot.setStrokeStyle(1, 0x8d6e63); + + this.collectionGrid.add(slot); + + if (isUnlocked) { + // Icon + const key = `item_${id}`; + const tex = this.textures.exists(key) ? key : null; + + if (tex) { + const sprite = this.add.sprite(cx, cy, tex).setScale(1.2); + this.collectionGrid.add(sprite); + } else { + this.collectionGrid.add(this.add.text(cx, cy, id.substr(0, 2), { fontSize: '12px', color: '#000' }).setOrigin(0.5)); + } + + // Tooltip logic could go here (hover) + slot.setInteractive(); + slot.on('pointerover', () => { + this.showCollectionTooltip(cx, cy, data); + }); + slot.on('pointerout', () => { + this.hideCollectionTooltip(); + }); + + } else { + // Locked + this.collectionGrid.add(this.add.text(cx, cy, '?', { fontSize: '24px', color: '#555555' }).setOrigin(0.5)); + } + + col++; + if (col >= visibleCols) { + col = 0; + row++; + } + }); + } + + showCollectionTooltip(x, y, data) { + if (this.collectionTooltip) this.collectionTooltip.destroy(); + + this.collectionTooltip = this.add.container(this.collectionGrid.x + x + 30, this.collectionGrid.y + y); + this.collectionContainer.add(this.collectionTooltip); + + const bg = this.add.rectangle(0, 0, 150, 60, 0x000000, 0.8); + const name = this.add.text(0, -15, data.name, { fontSize: '14px', fontStyle: 'bold', color: '#fff' }).setOrigin(0.5); + const desc = this.add.text(0, 10, data.category, { fontSize: '12px', color: '#aaa' }).setOrigin(0.5); + + this.collectionTooltip.add([bg, name, desc]); + } + + hideCollectionTooltip() { + if (this.collectionTooltip) { + this.collectionTooltip.destroy(); + this.collectionTooltip = null; + } + } + + // --- WORKER MENU --- + showWorkerMenu(zombie) { + if (this.workerMenuContainer) this.workerMenuContainer.destroy(); + + const x = this.scale.width / 2; + const y = this.scale.height / 2; + const w = 300; + const h = 350; + + this.workerMenuContainer = this.add.container(x, y); + this.workerMenuContainer.setDepth(2300); + + // Background + const bg = this.add.graphics(); + bg.fillStyle(0x333333, 0.95); + bg.fillRoundedRect(-w / 2, -h / 2, w, h, 10); + bg.lineStyle(2, 0x00FF00, 1); + bg.strokeRoundedRect(-w / 2, -h / 2, w, h, 10); + this.workerMenuContainer.add(bg); + + // Title + const title = this.add.text(0, -h / 2 + 25, 'ZOMBIE WORKER CONTROL', { + fontSize: '20px', fontStyle: 'bold', color: '#00FF00' + }).setOrigin(0.5); + this.workerMenuContainer.add(title); + + // Status + const currentTask = zombie.workerData ? zombie.workerData.type : 'IDLE'; + const statusText = this.add.text(0, -h / 2 + 55, `Current Task: ${currentTask}`, { + fontSize: '16px', color: '#FFFFFF' + }).setOrigin(0.5); + this.workerMenuContainer.add(statusText); + + // Buttons + const buttons = [ + { label: 'FARM (Seeds/Harvest)', action: 'FARM', color: 0x8B4513 }, + { label: 'MINE (Rocks)', action: 'MINE', color: 0x555555 }, + { label: 'CLEAR ZONE (Trees/Debris)', action: 'CLEAR', color: 0xAA0000 }, + { label: 'STOP / UNASSIGN', action: 'STOP', color: 0xFF5555 } + ]; + + let btnY = -h / 2 + 100; + + buttons.forEach(btn => { + const btnBg = this.add.rectangle(0, btnY, 250, 40, btn.color); + btnBg.setInteractive({ useHandCursor: true }); + btnBg.on('pointerdown', () => { + this.assignZombieTask(zombie, btn.action); + this.workerMenuContainer.destroy(); + this.workerMenuContainer = null; + }); + this.workerMenuContainer.add(btnBg); + + const txt = this.add.text(0, btnY, btn.label, { fontSize: '16px', fontStyle: 'bold' }).setOrigin(0.5); + this.workerMenuContainer.add(txt); + + btnY += 50; + }); + + // Close + const closeBtn = this.add.text(w / 2 - 20, -h / 2 + 20, 'X', { fontSize: '20px', color: '#FF0000', fontStyle: 'bold' }).setOrigin(0.5); + closeBtn.setInteractive({ useHandCursor: true }).on('pointerdown', () => { + this.workerMenuContainer.destroy(); + this.workerMenuContainer = null; + }); + this.workerMenuContainer.add(closeBtn); + } + + assignZombieTask(zombie, task) { + if (!this.gameScene || !this.gameScene.zombieWorkerSystem) return; + + if (task === 'STOP') { + this.gameScene.zombieWorkerSystem.removeWorker(zombie); + this.gameScene.events.emit('show-floating-text', { + x: zombie.sprite.x, y: zombie.sprite.y - 50, text: 'Idling...', color: '#FFFFFF' + }); + } else { + // Assign with radius 6 + this.gameScene.zombieWorkerSystem.assignWork(zombie, task, 8); + this.gameScene.events.emit('show-floating-text', { + x: zombie.sprite.x, y: zombie.sprite.y - 50, text: `Task: ${task}`, color: '#00FF00' + }); + } + } } diff --git a/src/systems/BlueprintSystem.js b/src/systems/BlueprintSystem.js index dc2e997..1e2487d 100644 --- a/src/systems/BlueprintSystem.js +++ b/src/systems/BlueprintSystem.js @@ -12,6 +12,13 @@ class BlueprintSystem { this.unlockedRecipes.add('plank'); this.unlockedRecipes.add('chest'); this.unlockedRecipes.add('fence'); + this.unlockedRecipes.add('axe'); + this.unlockedRecipes.add('pickaxe'); + this.unlockedRecipes.add('hoe'); + this.unlockedRecipes.add('sword'); + this.unlockedRecipes.add('furnace'); + this.unlockedRecipes.add('mint'); + this.unlockedRecipes.add('grave'); // Blueprint Definitions (Item ID -> Recipe ID) this.blueprints = { diff --git a/src/systems/BuildingSystem.js b/src/systems/BuildingSystem.js index e6ab71e..27ab358 100644 --- a/src/systems/BuildingSystem.js +++ b/src/systems/BuildingSystem.js @@ -7,13 +7,19 @@ class BuildingSystem { 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 + house: { name: 'House', cost: { wood: 20, stone: 20, gold: 50 }, w: 1, h: 1 }, // Visual is bigger but anchor is 1 tile + barn: { name: 'Barn', cost: { wood: 50, stone: 10 }, w: 1, h: 1 }, + silo: { name: 'Silo', cost: { wood: 30, stone: 30 }, w: 1, h: 1 } }; // 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'); + if (!this.scene.textures.exists('struct_barn')) TextureGenerator.createStructureSprite(this.scene, 'struct_barn', 'barn'); + if (this.scene.textures.exists('struct_silo')) TextureGenerator.createStructureSprite(this.scene, 'struct_silo', 'silo'); + // House Lv2 Texture + TextureGenerator.createStructureSprite(this.scene, 'struct_house_lv2', 'house_lv2'); } toggleBuildMode() { @@ -85,13 +91,12 @@ class BuildingSystem { } // 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' + // Using decorations layer for now + const structType = `struct_${this.selectedBuilding}`; + terrain.addDecoration(gridX, gridY, structType); - // Let's modify TerrainSystem to support 'structures' better or just hack decorations - const success = terrain.placeStructure(gridX, gridY, `struct_${this.selectedBuilding}`); - if (success) { + // Assume success if no error (addDecoration checks internally but doesn't return value easily, but we checked space before) + { this.showFloatingText(`Built ${building.name}!`, gridX, gridY, '#00FF00'); // Build Sound @@ -108,6 +113,43 @@ class BuildingSystem { return true; } + tryUpgrade(gridX, gridY) { + const terrain = this.scene.terrainSystem; + const decKey = `${gridX},${gridY}`; + const decor = terrain.decorationsMap.get(decKey); + + if (!decor) return false; + + // Check if House Lv1 + if (decor.type === 'struct_house') { + const cost = { wood: 100, stone: 50, gold: 100 }; + const inv = this.scene.inventorySystem; + + // Check Resources + if (!inv.hasItem('wood', cost.wood) || !inv.hasItem('stone', cost.stone) || inv.gold < cost.gold) { + this.showFloatingText('Upgrade Cost: 100 Wood, 50 Stone, 100G', gridX, gridY, '#FF4444'); + return true; + } + + // Pay + inv.removeItem('wood', cost.wood); + inv.removeItem('stone', cost.stone); + inv.gold -= cost.gold; + inv.updateUI(); + + // Perform Upgrade + terrain.removeDecoration(gridX, gridY); + terrain.addDecoration(gridX, gridY, 'struct_house_lv2'); // Re-add Lv2 + + this.showFloatingText('HOUSE CRADED! (Lv. 2)', gridX, gridY, '#00FFFF'); + if (this.scene.soundManager) this.scene.soundManager.playSuccess(); + + return true; + } + + return false; + } + showFloatingText(text, gridX, gridY, color) { const iso = new IsometricUtils(48, 24); const pos = iso.toScreen(gridX, gridY); diff --git a/src/systems/CollectionSystem.js b/src/systems/CollectionSystem.js new file mode 100644 index 0000000..f173ba2 --- /dev/null +++ b/src/systems/CollectionSystem.js @@ -0,0 +1,76 @@ +class CollectionSystem { + constructor(scene) { + this.scene = scene; + this.unlockedItems = new Set(); + + // Database of Collectables + this.items = { + // Resources + 'wood': { name: 'Wood', desc: 'Basic building material.', category: 'Resource' }, + 'stone': { name: 'Stone', desc: 'Hard rock for walls.', category: 'Resource' }, + 'ore_gold': { name: 'Gold Ore', desc: 'Shiny ore found underground.', category: 'Resource' }, + 'gold_bar': { name: 'Gold Bar', desc: 'Refined gold ingot.', category: 'Resource' }, + + // Crops + 'seeds': { name: 'Seeds', desc: 'Mystery seeds.', category: 'Farming' }, + 'wheat': { name: 'Wheat', desc: 'Staple grain.', category: 'Farming' }, + 'corn': { name: 'Corn', desc: 'Tall growing crop.', category: 'Farming' }, + + // Rare + 'item_bone': { name: 'Bone', desc: 'Remains of a zombie.', category: 'Rare' }, + 'item_scrap': { name: 'Scrap Metal', desc: 'Old machinery parts.', category: 'Rare' }, + 'item_chip': { name: 'Microchip', desc: 'High-tech component.', category: 'Rare' }, + 'coin_gold': { name: 'Gold Coin', desc: 'Currency of the new world.', category: 'Rare' }, + 'artefact_old': { name: 'Ancient Pot', desc: 'A relict from the past.', category: 'Archaeology' }, + + // Nature + 'flower_red': { name: 'Red Flower', desc: 'Beautiful bloom.', category: 'Nature' }, + 'flower_yellow': { name: 'Yellow Flower', desc: 'Bright bloom.', category: 'Nature' }, + 'flower_blue': { name: 'Blue Flower', desc: 'Rare blue bloom.', category: 'Nature' }, + 'mushroom_red': { name: 'Red Mushroom', desc: 'Looks poisonous.', category: 'Nature' }, + 'mushroom_brown': { name: 'Brown Mushroom', desc: 'Edible fungus.', category: 'Nature' } + }; + } + + unlock(itemId) { + if (!this.items[itemId]) return; // Not a collectable + + if (!this.unlockedItems.has(itemId)) { + this.unlockedItems.add(itemId); + console.log(`📖 Collection Unlocked: ${itemId}`); + + // Notification + this.scene.events.emit('show-floating-text', { + x: this.scene.player.sprite.x, + y: this.scene.player.sprite.y - 80, + text: `New Collection Entry!`, + color: '#FFD700' + }); + + if (this.scene.soundManager) { + this.scene.soundManager.playSuccess(); // Reuse success sound + } + } + } + + has(itemId) { + return this.unlockedItems.has(itemId); + } + + getProgress() { + const total = Object.keys(this.items).length; + const unlocked = this.unlockedItems.size; + return { unlocked, total, percent: (unlocked / total) * 100 }; + } + + // Save/Load Logic + toJSON() { + return Array.from(this.unlockedItems); + } + + load(data) { + if (Array.isArray(data)) { + this.unlockedItems = new Set(data); + } + } +} diff --git a/src/systems/ExpansionSystem.js b/src/systems/ExpansionSystem.js index 5acb6bf..a147a1f 100644 --- a/src/systems/ExpansionSystem.js +++ b/src/systems/ExpansionSystem.js @@ -22,7 +22,7 @@ class ExpansionSystem { id: 'forest', name: 'Dark Forest', x: 40, y: 0, w: 60, h: 40, // Right of farm - unlocked: false, + unlocked: true, cost: 100, // 100 Gold Coins req: 'None', color: 0x006400 @@ -31,7 +31,7 @@ class ExpansionSystem { id: 'city', name: 'Ruined City', x: 0, y: 40, w: 100, h: 60, // Below farm & forest - unlocked: false, + unlocked: true, cost: 500, req: 'Kill Boss', color: 0x808080 diff --git a/src/systems/FarmingSystem.js b/src/systems/FarmingSystem.js index d209e90..5653c9f 100644 --- a/src/systems/FarmingSystem.js +++ b/src/systems/FarmingSystem.js @@ -50,6 +50,14 @@ class FarmingSystem { if (!tile.hasDecoration && !tile.hasCrop) { terrain.setTileType(gridX, gridY, 'farmland'); if (this.scene.soundManager) this.scene.soundManager.playDig(); + + // Archaeology: Chance to find artefact + if (Math.random() < 0.15) { + if (this.scene.interactionSystem) { // Using InteractionSystem spawnLoot wrapper or LootSystem directly? + // InteractionSystem has spawnLoot method that wraps inventory adding and text. + this.scene.interactionSystem.spawnLoot(gridX, gridY, 'artefact_old', 1); + } + } return true; } } diff --git a/src/systems/HybridSkillSystem.js b/src/systems/HybridSkillSystem.js new file mode 100644 index 0000000..04a916e --- /dev/null +++ b/src/systems/HybridSkillSystem.js @@ -0,0 +1,110 @@ +class HybridSkillSystem { + constructor(scene) { + this.scene = scene; + + this.level = 1; + this.xp = 0; + this.maxXp = 100; + + // Skills + this.skills = { + 'translation': { level: 0, max: 5, name: 'Zombie Language', desc: 'Understand zombie groans.' }, + 'strength': { level: 0, max: 5, name: 'Mutant Strength', desc: 'Deal more damage.' }, + 'resilience': { level: 0, max: 5, name: 'Rot Resistance', desc: 'Less damage from poison/decay.' }, + 'command': { level: 0, max: 3, name: 'Horde Command', desc: 'Control more zombie workers.' } + }; + + this.points = 0; // Available skill points + } + + addXP(amount) { + this.xp += amount; + if (this.xp >= this.maxXp) { + this.levelUp(); + } + // UI Notification + this.scene.events.emit('show-floating-text', { + x: this.scene.player.sprite.x, + y: this.scene.player.sprite.y - 50, + text: `+${amount} Hybrid XP`, + color: '#00FF00' + }); + } + + levelUp() { + this.xp -= this.maxXp; + this.level++; + this.maxXp = Math.floor(this.maxXp * 1.5); + this.points++; + console.log(`🧬 Hybrid Level Up! Level: ${this.level}, Points: ${this.points}`); + + this.scene.soundManager.playSuccess(); // Reuse success sound + + this.scene.events.emit('show-floating-text', { + x: this.scene.player.sprite.x, + y: this.scene.player.sprite.y - 80, + text: `LEVEL UP! (${this.level})`, + color: '#FFFF00' + }); + } + + tryUpgradeSkill(skillId) { + const skill = this.skills[skillId]; + if (!skill) return false; + + if (this.points > 0 && skill.level < skill.max) { + this.points--; + skill.level++; + console.log(`🧬 Upgraded ${skill.name} to Lv ${skill.level}`); + return true; + } + return false; + } + + getSpeechTranslation(text) { + const lvl = this.skills['translation'].level; + if (lvl >= 5) return text; // Perfect translation + + // Obfuscate text based on level + // Lv 0: 100% garbled + // Lv 1: 80% garbled + // ... + const garbleChance = 1.0 - (lvl * 0.2); + + return text.split(' ').map(word => { + if (Math.random() < garbleChance) { + return this.garbleWord(word); + } + return word; + }).join(' '); + } + + garbleWord(word) { + const sounds = ['hgh', 'arr', 'ghh', '...', 'bra', 'in', 'zZz']; + return sounds[Math.floor(Math.random() * sounds.length)]; + } + + toJSON() { + return { + level: this.level, + xp: this.xp, + maxXp: this.maxXp, + skills: this.skills, + points: this.points + }; + } + + load(data) { + if (!data) return; + this.level = data.level; + this.xp = data.xp; + this.maxXp = data.maxXp; + this.points = data.points; + // Merge skills to keep structure if definition changed + for (const k in data.skills) { + if (this.skills[k]) { + this.skills[k].level = data.skills[k].level; + } + } + } +} diff --git a/src/systems/InteractionSystem.js b/src/systems/InteractionSystem.js index 8fdba7d..1adbecc 100644 --- a/src/systems/InteractionSystem.js +++ b/src/systems/InteractionSystem.js @@ -58,24 +58,71 @@ class InteractionSystem { } } + + + // Check for Buildings (Signs included) + // Currently decorations don't store data easily accessible here without query. + // Assuming nearest logic above covers entities. + if (nearest) { console.log('E Interacted with:', nearest.type || nearest.lootTable); if (nearest.type === 'scooter') { nearest.interact(this.scene.player); } else if (nearest.lootTable) { - // It's a chest! nearest.interact(this.scene.player); } else if (nearest.type === 'zombie') { - // Always Tame on E key (Combat is Space/Click) + // Check if already tamed? + // If aggressive, combat? E is usually benign interaction. nearest.tame(); } else { - nearest.toggleState(); // Merchant/NPC talk + // Generic Talk / Toggle + // INTEGRATE TRANSLATION + this.handleTalk(nearest); } } } + handleTalk(npc) { + if (!this.scene.hybridSkillSystem) { + npc.toggleState(); + return; + } + + let text = "..."; + let color = '#FFFFFF'; + + if (npc.type === 'zombie') { + text = "Brains... Hungry... Leader?"; + text = this.scene.hybridSkillSystem.getSpeechTranslation(text); + color = '#55FF55'; + } else if (npc.type === 'merchant') { + text = "Welcome! I have rare goods."; + color = '#FFD700'; + // Also triggers UI + const uiScene = this.scene.scene.get('UIScene'); + if (uiScene) uiScene.showTradeMenu(this.scene.inventorySystem); + } else if (npc.type === 'elf') { + text = "The forest... it burns... you are not safe."; + text = this.scene.hybridSkillSystem.getSpeechTranslation(text); // Maybe Elvish/Mutant dialect? + color = '#00FFFF'; + } else if (npc.passive) { + // Animal noises + text = npc.type.includes('cow') ? 'Moo.' : 'Cluck.'; + } + + // Show Floating Text Dialogue + this.scene.events.emit('show-floating-text', { + x: npc.sprite.x, + y: npc.sprite.y - 60, + text: text, + color: color + }); + + npc.toggleState(); // Stop moving briefly + } + handleInteraction(gridX, gridY, isAttack = false) { if (!this.scene.player) return; @@ -154,9 +201,16 @@ class InteractionSystem { return; } else { - // TAME ATTEMPT - console.log('🤝 Attempting to TAME zombie at', npc.gridX, npc.gridY); - npc.tame(); + if (npc.isTamed) { + // Open Worker Menu + if (uiScene && uiScene.showWorkerMenu) { + uiScene.showWorkerMenu(npc); + } + } else { + // TAME ATTEMPT + console.log('🤝 Attempting to TAME zombie at', npc.gridX, npc.gridY); + npc.tame(); + } return; } } @@ -233,6 +287,12 @@ class InteractionSystem { if (result) return; } + // Building Interaction (Upgrade House) + if (this.scene.buildingSystem && decor.type.startsWith('struct_house')) { + const result = this.scene.buildingSystem.tryUpgrade(gridX, gridY); + if (result) return; + } + // handleTreeHit Logic (User Request) // Preverimo tip in ustrezno orodje let damage = 1; diff --git a/src/systems/InventorySystem.js b/src/systems/InventorySystem.js index 9e40906..9bb7d3f 100644 --- a/src/systems/InventorySystem.js +++ b/src/systems/InventorySystem.js @@ -24,6 +24,11 @@ class InventorySystem { } addItem(type, count) { + // Unlock in Collection + if (this.scene.collectionSystem) { + this.scene.collectionSystem.unlock(type); + } + // 1. Try to stack for (let i = 0; i < this.slots.length; i++) { if (this.slots[i] && this.slots[i].type === type) { diff --git a/src/systems/TerrainSystem.js b/src/systems/TerrainSystem.js index ba30dfe..b4a577e 100644 --- a/src/systems/TerrainSystem.js +++ b/src/systems/TerrainSystem.js @@ -21,7 +21,7 @@ const TILE_MINE_WALL = 81; // ID za zid rudnika (Solid/Kolizija) const ITEM_STONE = 20; // ID za kamen, ki ga igralec dobi const ITEM_IRON = 21; // ID za železo -const TREE_DENSITY_THRESHOLD = 0.65; // Višja vrednost = manj gosto (manj gozda) +const TREE_DENSITY_THRESHOLD = 0.45; // Višja vrednost = manj gosto (manj gozda) const ROCK_DENSITY_THRESHOLD = 0.60; // Prag za skupine skal // Terrain Generator System @@ -119,6 +119,12 @@ class TerrainSystem { this.offsetX = 0; this.offsetY = 0; + + this.generatedChunks = new Set(); + this.chunkSize = 10; + + // Init tiles array with NULLs + this.tiles = Array.from({ length: this.height }, () => Array(this.width).fill(null)); } createTileTextures() { @@ -224,15 +230,50 @@ class TerrainSystem { generate() { this.createTileTextures(); - for (let y = 0; y < this.height; y++) { - this.tiles[y] = []; - for (let x = 0; x < this.width; x++) { + console.log('🌍 Initializing World (Zone Streaming Mode)...'); + + // Generate ONLY the starting area (Farm) + // Farm is at 20,20. Let's load 3x3 chunks around it. + const centerCx = Math.floor(FARM_CENTER_X / this.chunkSize); + const centerCy = Math.floor(FARM_CENTER_Y / this.chunkSize); + + for (let cy = centerCy - 2; cy <= centerCy + 2; cy++) { + for (let cx = centerCx - 2; cx <= centerCx + 2; cx++) { + this.generateChunk(cx, cy); + } + } + + console.log(`✅ World Init Complete. Loaded ${this.generatedChunks.size} chunks.`); + } + + generateChunk(cx, cy) { + const key = `${cx},${cy}`; + if (this.generatedChunks.has(key)) return; + + // Bounds check + if (cx < 0 || cy < 0 || cx * this.chunkSize >= this.width || cy * this.chunkSize >= this.height) return; + + this.generatedChunks.add(key); + // console.log(`🔄 Streaming Chunk: [${cx}, ${cy}]`); + + const startX = cx * this.chunkSize; + const startY = cy * this.chunkSize; + const endX = Math.min(startX + this.chunkSize, this.width); + const endY = Math.min(startY + this.chunkSize, this.height); + + const validPositions = []; // Local valid positions for this chunk + + for (let y = startY; y < endY; y++) { + for (let x = startX; x < endX; x++) { + + // --- PER TILE GENERATION LOGIC (Moved from old loop) --- const nx = x * 0.1; const ny = y * 0.1; const elevation = this.noise.noise(nx, ny); let terrainType = this.terrainTypes.GRASS_FULL; + // Edges of WORLD if (x < 3 || x >= this.width - 3 || y < 3 || y >= this.height - 3) { terrainType = this.terrainTypes.GRASS_FULL; } else { @@ -244,136 +285,108 @@ class TerrainSystem { else if (elevation > 0.85) terrainType = this.terrainTypes.STONE; } + // Farm Override if (Math.abs(x - FARM_CENTER_X) <= FARM_SIZE / 2 && Math.abs(y - FARM_CENTER_Y) <= FARM_SIZE / 2) { terrainType = this.terrainTypes.DIRT; } - // CITY AREA - 15x15 območje z OBZIDJEM + + // City Override if (x >= CITY_START_X && x < CITY_START_X + CITY_SIZE && y >= CITY_START_Y && y < CITY_START_Y + CITY_SIZE) { - // Preverimo, ali smo na ROBOVIH (Obzidje) const isEdge = (x === CITY_START_X || x === CITY_START_X + CITY_SIZE - 1 || y === CITY_START_Y || y === CITY_START_Y + CITY_SIZE - 1); if (isEdge) { - // OBZIDJE - trdno, igralec ne more čez terrainType = { name: 'WALL_EDGE', color: 0x505050, solid: true }; } else { - // NOTRANJOST MESTA - tlakovci (pavement) terrainType = this.terrainTypes.PAVEMENT; - // Naključne ruševine v mestu if (Math.random() < 0.15) { terrainType = this.terrainTypes.RUINS; } } } + // Create Tile Data this.tiles[y][x] = { type: terrainType.name, texture: terrainType.name, hasDecoration: false, hasCrop: false, - solid: terrainType.solid || false // Inherits from terrain type + solid: terrainType.solid || false }; - // Place Trees dynamically during generation - // this.placeTree(x, y, terrainType.name); + // Track valid positions for decorations + if (terrainType.name !== 'water' && terrainType.name !== 'sand' && terrainType.name !== 'stone' && !terrainType.solid) { + // Exclude Farm/City from random decor logic + const isFarm = Math.abs(x - FARM_CENTER_X) <= (FARM_SIZE / 2 + 2) && Math.abs(y - FARM_CENTER_Y) <= (FARM_SIZE / 2 + 2); + const isCity = x >= CITY_START_X - 2 && x < CITY_START_X + CITY_SIZE + 2 && y >= CITY_START_Y - 2 && y < CITY_START_Y + CITY_SIZE + 2; - // Place Rocks dynamically - // this.placeRock(x, y, terrainType.name); - } - } - - let treeCount = 0; - let rockCount = 0; - let flowerCount = 0; - - const validPositions = []; - const isFarm = (x, y) => Math.abs(x - FARM_CENTER_X) <= (FARM_SIZE / 2 + 2) && Math.abs(y - FARM_CENTER_Y) <= (FARM_SIZE / 2 + 2); - const isCity = (x, y) => x >= CITY_START_X - 2 && x < CITY_START_X + CITY_SIZE + 2 && y >= CITY_START_Y - 2 && y < CITY_START_Y + CITY_SIZE + 2; - - for (let y = 5; y < this.height - 5; y++) { - for (let x = 5; x < this.width - 5; x++) { - if (isFarm(x, y) || isCity(x, y)) continue; - - const tile = this.tiles[y][x]; - if (tile.type !== 'water' && tile.type !== 'sand' && tile.type !== 'stone') { - validPositions.push({ x, y }); + if (!isFarm && !isCity) { + validPositions.push({ x, y }); + } } + + // Direct Placement Calls (Trees/Rocks) - legacy method support + // this.placeTree(x, y, terrainType.name); // Optional: keep this if preferred over random batch } } - // DECORATIONS - Enhanced World Details - console.log('🌸 Adding enhanced decorations...'); + // --- CHUNK DECORATION PASS --- + // Instead of global counts, we use probability/density per chunk + // 10x10 = 100 tiles. + // Approx density: 0.2 trees per tile = 20 trees per chunk. - // Natural Path Stones (along roads and random areas) - let pathStoneCount = 0; - for (let i = 0; i < 50; i++) { - const pos = validPositions[Math.floor(Math.random() * validPositions.length)]; - if (pos && !this.decorationsMap.has(`${pos.x},${pos.y}`)) { - this.addDecoration(pos.x, pos.y, 'path_stone'); - pathStoneCount++; + // 1. Random Decorations + validPositions.forEach(pos => { + // Trees + if (Math.random() < 0.05) { // 5% chance per valid tile + this.placeTree(pos.x, pos.y, 'grass'); // force check inside } - } - // Small decorative rocks - let smallRockCount = 0; - for (let i = 0; i < 80; i++) { - const pos = validPositions[Math.floor(Math.random() * validPositions.length)]; - if (pos && !this.decorationsMap.has(`${pos.x},${pos.y}`)) { - const rockType = Math.random() > 0.5 ? 'small_rock_1' : 'small_rock_2'; - this.addDecoration(pos.x, pos.y, rockType); - smallRockCount++; + // Rocks + if (Math.random() < 0.02) { + this.placeRock(pos.x, pos.y, 'grass'); } - } - // Flower clusters - flowerCount = 0; - for (let i = 0; i < 100; i++) { - const pos = validPositions[Math.floor(Math.random() * validPositions.length)]; - if (pos && !this.decorationsMap.has(`${pos.x},${pos.y}`)) { + // Flowers + if (Math.random() < 0.05) { const flowers = ['flower_red', 'flower_yellow', 'flower_blue']; const flowerType = flowers[Math.floor(Math.random() * flowers.length)]; this.addDecoration(pos.x, pos.y, flowerType); - flowerCount++; } - } - // Mushrooms (spooky atmosphere) - let mushroomCount = 0; - for (let i = 0; i < 60; i++) { - const pos = validPositions[Math.floor(Math.random() * validPositions.length)]; - if (pos && !this.decorationsMap.has(`${pos.x},${pos.y}`)) { - const mushroomType = Math.random() > 0.5 ? 'mushroom_red' : 'mushroom_brown'; - this.addDecoration(pos.x, pos.y, mushroomType); - mushroomCount++; + // Path Stones + if (Math.random() < 0.02) { + this.addDecoration(pos.x, pos.y, 'path_stone'); } - } - - // Fallen Logs (forest debris) - let logCount = 0; - for (let i = 0; i < 25; i++) { - const pos = validPositions[Math.floor(Math.random() * validPositions.length)]; - if (pos && !this.decorationsMap.has(`${pos.x},${pos.y}`)) { - this.addDecoration(pos.x, pos.y, 'fallen_log'); - logCount++; - } - } - - // Puddles (will appear during rain) - this.puddlePositions = []; - for (let i = 0; i < 40; i++) { - const pos = validPositions[Math.floor(Math.random() * validPositions.length)]; - if (pos && !this.decorationsMap.has(`${pos.x},${pos.y}`)) { - this.puddlePositions.push({ x: pos.x, y: pos.y }); - } - } - - console.log(`✅ Decorations: ${pathStoneCount} paths, ${smallRockCount} rocks, ${flowerCount} flowers, ${mushroomCount} mushrooms, ${logCount} logs.`); + }); } + updateChunks(camera) { + // Calculate which chunks are in view + const view = camera.worldView; + const buffer = 100; // Load slightly outside view + + const p1 = this.iso.toGrid(view.x - buffer, view.y - buffer); + const p2 = this.iso.toGrid(view.x + view.width + buffer, view.y + view.height + buffer); + + const minCx = Math.floor(Math.min(p1.x, p2.x) / this.chunkSize); + const maxCx = Math.ceil(Math.max(p1.x, p2.x) / this.chunkSize); + const minCy = Math.floor(Math.min(p1.y, p2.y) / this.chunkSize); + const maxCy = Math.ceil(Math.max(p1.y, p2.y) / this.chunkSize); + + for (let cy = minCy; cy <= maxCy; cy++) { + for (let cx = minCx; cx <= maxCx; cx++) { + this.generateChunk(cx, cy); + } + } + } + + // Retained helper methods... + damageDecoration(x, y, amount) { const key = `${x},${y}`; const decor = this.decorationsMap.get(key); @@ -704,6 +717,8 @@ class TerrainSystem { } updateCulling(camera) { + this.updateChunks(camera); + const view = camera.worldView; let buffer = 200; if (this.scene.settings && this.scene.settings.viewDistance === 'LOW') buffer = 50; diff --git a/src/systems/WeatherSystem.js b/src/systems/WeatherSystem.js index e7e5ee1..14a93b5 100644 --- a/src/systems/WeatherSystem.js +++ b/src/systems/WeatherSystem.js @@ -71,8 +71,26 @@ class WeatherSystem { if (phase !== this.currentPhase) { this.currentPhase = phase; console.log(`🌅 Time of Day: ${phase} (${Math.floor(this.gameTime)}:00)`); + + // Trigger Bat Swarm at Dusk + if (phase === 'dusk' && this.scene.worldEventSystem) { + this.scene.worldEventSystem.spawnBatSwarm(); + } } + // Trigger Night Owl at Midnight (00:00) + // We check if seconds crossed 0. + if (Math.abs(this.gameTime - 0.0) < 0.05) { + // Only trigger once per night - check active flag or similar? + // Actually, gameTime will pass 0 very quickly but maybe multiple frames. + // We can use a flag resetting at day. + if (!this.owlTriggered && this.scene.worldEventSystem) { + this.scene.worldEventSystem.spawnNightOwl(); + this.owlTriggered = true; + } + } + if (this.gameTime > 5) this.owlTriggered = false; // Reset flag at dawn + // Update UI Clock const uiScene = this.scene.scene.get('UIScene'); if (uiScene && uiScene.clockText) { diff --git a/src/systems/WorkstationSystem.js b/src/systems/WorkstationSystem.js index 85b1d8d..7820d70 100644 --- a/src/systems/WorkstationSystem.js +++ b/src/systems/WorkstationSystem.js @@ -12,12 +12,14 @@ class WorkstationSystem { this.recipes = { 'furnace': [ { input: 'ore_iron', fuel: 'coal', output: 'iron_bar', time: 5000 }, + { input: 'ore_gold', fuel: 'coal', output: 'gold_bar', time: 8000 }, { input: 'ore_stone', fuel: 'coal', output: 'stone_brick', time: 3000 }, { input: 'sand', fuel: 'coal', output: 'glass', time: 3000 }, { input: 'log', fuel: 'log', output: 'charcoal', time: 4000 } // Wood to charcoal ], 'mint': [ - { input: 'iron_bar', fuel: 'coal', output: 'coin_gold', time: 2000, outputCount: 10 } + { input: 'iron_bar', fuel: 'coal', output: 'coin_gold', time: 2000, outputCount: 10 }, + { input: 'gold_bar', fuel: 'coal', output: 'coin_gold', time: 3000, outputCount: 50 } ], 'campfire': [ { input: 'raw_meat', fuel: 'stick', output: 'cooked_meat', time: 3000 } diff --git a/src/systems/WorldEventSystem.js b/src/systems/WorldEventSystem.js new file mode 100644 index 0000000..2cbb910 --- /dev/null +++ b/src/systems/WorldEventSystem.js @@ -0,0 +1,137 @@ +class WorldEventSystem { + constructor(scene) { + this.scene = scene; + this.activeEvents = []; + this.bats = []; + this.owl = null; + } + + /* + * BATS: Visual effect for evening/night + */ + spawnBatSwarm() { + console.log('🦇 Bat Swarm Incoming!'); + const count = 10 + Math.floor(Math.random() * 10); + + for (let i = 0; i < count; i++) { + // Start right-side of screen, random height + const sx = this.scene.scale.width + 50 + Math.random() * 200; + const sy = Math.random() * (this.scene.scale.height / 2); // Top half + + const bat = this.scene.add.sprite(sx, sy, 'bat'); + bat.setDepth(2000); // Above most things + bat.setScrollFactor(0); // Screen space (UI layer) or World space? + // Better world space if we want them to feel like part of the world, but screen space is easier for "effect". + // Let's stick to Screen Space for "Ambience". + + // Random speed + const speed = 150 + Math.random() * 100; + + this.bats.push({ sprite: bat, speed: speed }); + } + } + + /* + * NIGHT OWL: Delivers a gift + */ + spawnNightOwl() { + if (this.owl) return; // Only one at a time + console.log('🦉 Night Owl Arriving!'); + + // Spawn top-left + const sx = -50; + const sy = 100; + + const owl = this.scene.add.sprite(sx, sy, 'owl'); + owl.setDepth(2000); + owl.setScrollFactor(0); // Screen space for delivery + + this.owl = { + sprite: owl, + state: 'ARRIVING', // ARRIVING, DELIVERING, LEAVING + timer: 0, + targetX: this.scene.scale.width / 2, + targetY: this.scene.scale.height / 3 + }; + + if (this.scene.soundManager) { + // Play owl hoot sound (placeholder or generic) + // this.scene.soundManager.playHoot(); + } + + this.scene.events.emit('show-floating-text', { + x: this.scene.player.sprite.x, + y: this.scene.player.sprite.y - 120, + text: '🦉 Hoot Hoot!', + color: '#FFA500' + }); + } + + update(delta) { + // 1. Bats + for (let i = this.bats.length - 1; i >= 0; i--) { + const b = this.bats[i]; + b.sprite.x -= b.speed * (delta / 1000); // Fly left + b.sprite.y += (Math.sin(b.sprite.x * 0.02) * 2); // Wobbly flight + + if (b.sprite.x < -50) { + b.sprite.destroy(); + this.bats.splice(i, 1); + } + } + + // 2. Owl + if (this.owl) { + const o = this.owl; + const speed = 200 * (delta / 1000); + + if (o.state === 'ARRIVING') { + const dx = o.targetX - o.sprite.x; + const dy = o.targetY - o.sprite.y; + const dist = Math.sqrt(dx * dx + dy * dy); + + if (dist < 10) { + o.state = 'DELIVERING'; + o.timer = 0; + this.dropGift(); + } else { + o.sprite.x += (dx / dist) * speed; + o.sprite.y += (dy / dist) * speed; + } + } else if (o.state === 'DELIVERING') { + o.timer += delta; + if (o.timer > 1000) { // Hover for 1s + o.state = 'LEAVING'; + } + } else if (o.state === 'LEAVING') { + o.sprite.x += speed; // Fly right + o.sprite.y -= speed * 0.5; // Fly up + + if (o.sprite.x > this.scene.scale.width + 50) { + o.sprite.destroy(); + this.owl = null; + } + } + } + } + + dropGift() { + console.log('🎁 Owl dropped a gift!'); + const p = this.scene.player; + + // Spawn loot at player pos (World Space) + if (this.scene.interactionSystem) { + // Random gift: Gold or maybe a rare item + const r = Math.random(); + let item = 'flower_red'; + let count = 1; + + if (r < 0.3) { item = 'coin_gold'; count = 10; } + else if (r < 0.6) { item = 'seeds_corn'; count = 5; } + else if (r < 0.9) { item = 'item_scrap'; count = 3; } + else { item = 'artefact_old'; count = 1; } // Rare! + + this.scene.interactionSystem.spawnLoot(p.gridX, p.gridY, item, count); + } + } +} diff --git a/src/systems/ZombieWorkerSystem.js b/src/systems/ZombieWorkerSystem.js index ee361a2..51ed3ca 100644 --- a/src/systems/ZombieWorkerSystem.js +++ b/src/systems/ZombieWorkerSystem.js @@ -63,6 +63,8 @@ class ZombieWorkerSystem { this.performFarmWork(worker); } else if (worker.workerData.type === 'MINE') { this.performMineWork(worker); + } else if (worker.workerData.type === 'CLEAR') { + this.performClearWork(worker); } } } @@ -176,6 +178,41 @@ class ZombieWorkerSystem { wd.status = 'IDLE'; } + performClearWork(zombie) { + const wd = zombie.workerData; + const terrain = this.scene.terrainSystem; + if (!terrain || !terrain.decorationsMap) return; + + for (let dx = -wd.radius; dx <= wd.radius; dx++) { + for (let dy = -wd.radius; dy <= wd.radius; dy++) { + const key = `${wd.centerX + dx},${wd.centerY + dy}`; + if (terrain.decorationsMap.has(key)) { + const decor = terrain.decorationsMap.get(key); + const t = decor.type; + + // Clear trees, bushes, logs, rocks + if (t.startsWith('tree') || t.startsWith('bush') || t === 'fallen_log' || t === 'stone' || t.startsWith('small_rock')) { + terrain.removeDecoration(wd.centerX + dx, wd.centerY + dy); + + // Give some resources + if (this.scene.inventorySystem) { + if (t.startsWith('tree') || t === 'fallen_log' || t.startsWith('bush')) { + this.scene.inventorySystem.addItem('wood', 1); + } else { + this.scene.inventorySystem.addItem('stone', 1); + } + } + + console.log(`🧟🪓 Worker CLEARED ${t}`); + wd.status = 'WORKING'; + return; // One per tick + } + } + } + } + wd.status = 'IDLE'; + } + onWorkerDeath(zombie) { console.log(`💀 Worker died at ${zombie.gridX},${zombie.gridY}`); diff --git a/src/utils/TextureGenerator.js b/src/utils/TextureGenerator.js index 2d51a1e..4a4b1c0 100644 --- a/src/utils/TextureGenerator.js +++ b/src/utils/TextureGenerator.js @@ -192,6 +192,43 @@ class TextureGenerator { // Details ctx.fillStyle = '#555555'; ctx.beginPath(); ctx.arc(25, 45, 6, 0, Math.PI * 2); ctx.fill(); + } else if (type === 'house' || type === 'house_lv2') { + // --- ISOMETRIC HOUSE --- + const isLv2 = (type === 'house_lv2'); + + // Walls + const wallColor = isLv2 ? '#A0522D' : '#8B4513'; // Lv2 is lighter/refined + ctx.fillStyle = wallColor; + ctx.fillRect(10, 24, 44, 32); + + // Roof (Triangle/Pyramid-ish) + const roofColor = isLv2 ? '#8B0000' : '#4682B4'; // Lv2 Red Roof, Lv1 Blue + ctx.fillStyle = roofColor; + ctx.beginPath(); + ctx.moveTo(32, 2); // Top + ctx.lineTo(8, 24); // Left + ctx.lineTo(56, 24); // Right + ctx.fill(); + + // Door + ctx.fillStyle = '#5C4033'; // Dark Wood + ctx.fillRect(26, 40, 12, 16); + + // Windows + ctx.fillStyle = '#87CEEB'; // SkyBlue + ctx.fillRect(14, 34, 8, 8); // Window 1 + ctx.fillRect(42, 34, 8, 8); // Window 2 + + // Lv2 Extras: Chimney or extra floor indicator + if (isLv2) { + ctx.fillStyle = '#555'; + ctx.fillRect(40, 10, 6, 12); // Chimney + + // Second floor window + ctx.fillStyle = '#87CEEB'; + ctx.beginPath(); ctx.arc(32, 16, 4, 0, Math.PI * 2); ctx.fill(); + } + } else { // Generic box for others ctx.fillStyle = '#8B4513'; @@ -605,7 +642,8 @@ class TextureGenerator { { name: 'seeds_corn', color: '#B22222' },// FireBrick seeds { name: 'item_bone', color: '#F5F5DC' }, // Beige { name: 'item_scrap', color: '#B87333' }, // Copper/Bronze (kovinski kos) - { name: 'item_chip', color: '#00CED1' } // DarkTurquoise (elektronski chip) + { name: 'item_chip', color: '#00CED1' }, // DarkTurquoise (elektronski chip) + { name: 'artefact_old', color: '#8B4513' } // Ancient Pot (Brown) ]; items.forEach(item => { const it = typeof item === 'string' ? item : item.name; @@ -754,6 +792,190 @@ class TextureGenerator { TextureGenerator.createPuddleSprite(this.scene, 'puddle'); TextureGenerator.createFurnaceSprite(this.scene, 'furnace'); TextureGenerator.createMintSprite(this.scene, 'mint'); + TextureGenerator.createOwlSprite(this.scene, 'owl'); + TextureGenerator.createBatSprite(this.scene, 'bat'); + + // Mutants + TextureGenerator.createTrollSprite(this.scene, 'troll'); + TextureGenerator.createElfSprite(this.scene, 'elf'); + + // Animals + TextureGenerator.createCowSprite(this.scene, 'cow', false); + TextureGenerator.createCowSprite(this.scene, 'cow_mutant', true); + TextureGenerator.createChickenSprite(this.scene, 'chicken', false); + TextureGenerator.createChickenSprite(this.scene, 'chicken_mutant', true); + } + + static createOwlSprite(scene, key = 'owl') { + if (scene.textures.exists(key)) return; + const canvas = scene.textures.createCanvas(key, 32, 32); + const ctx = canvas.getContext(); + ctx.clearRect(0, 0, 32, 32); + + // Body + ctx.fillStyle = '#8B4513'; // SaddleBrown + ctx.fillRect(8, 8, 16, 20); + + // Eyes + ctx.fillStyle = '#FFD700'; // Gold eyes + ctx.beginPath(); ctx.arc(12, 12, 3, 0, Math.PI * 2); ctx.fill(); + ctx.beginPath(); ctx.arc(20, 12, 3, 0, Math.PI * 2); ctx.fill(); + // Pupils + ctx.fillStyle = '#000'; + ctx.beginPath(); ctx.arc(12, 12, 1, 0, Math.PI * 2); ctx.fill(); + ctx.beginPath(); ctx.arc(20, 12, 1, 0, Math.PI * 2); ctx.fill(); + + // Beak + ctx.fillStyle = '#FFA500'; // Orange + ctx.beginPath(); + ctx.moveTo(16, 14); ctx.lineTo(14, 18); ctx.lineTo(18, 18); + ctx.fill(); + + // Wings (folded) + ctx.fillStyle = '#A0522D'; + ctx.fillRect(6, 12, 4, 12); + ctx.fillRect(22, 12, 4, 12); + + canvas.refresh(); + } + + static createBatSprite(scene, key = 'bat') { + if (scene.textures.exists(key)) return; + const canvas = scene.textures.createCanvas(key, 32, 32); + const ctx = canvas.getContext(); + ctx.clearRect(0, 0, 32, 32); + + // Body + ctx.fillStyle = '#222'; + ctx.beginPath(); ctx.ellipse(16, 16, 4, 6, 0, 0, Math.PI * 2); ctx.fill(); + + // Wings + ctx.fillStyle = '#333'; + // Left Wing + ctx.beginPath(); + ctx.moveTo(12, 16); ctx.lineTo(2, 6); ctx.lineTo(4, 20); + ctx.closePath(); ctx.fill(); + // Right Wing + ctx.beginPath(); + ctx.moveTo(20, 16); ctx.lineTo(30, 6); ctx.lineTo(28, 20); + ctx.closePath(); ctx.fill(); + + // Eyes + ctx.fillStyle = '#f00'; + ctx.fillRect(15, 14, 1, 1); + ctx.fillRect(17, 14, 1, 1); + + canvas.refresh(); + } + + static createTrollSprite(scene, key) { + if (scene.textures.exists(key)) return; + const canvas = scene.textures.createCanvas(key, 48, 48); // Bigger + const ctx = canvas.getContext(); + ctx.clearRect(0, 0, 48, 48); + + // Body (Big, Green) + ctx.fillStyle = '#228B22'; // ForestGreen + ctx.fillRect(10, 10, 28, 30); + + // Head + ctx.fillStyle = '#105510'; + ctx.fillRect(14, 4, 20, 16); + + // Eyes (Red) + ctx.fillStyle = '#ff0000'; + ctx.fillRect(18, 10, 4, 4); + ctx.fillRect(26, 10, 4, 4); + + // Club + ctx.fillStyle = '#5c4033'; + ctx.fillRect(36, 12, 8, 24); + + canvas.refresh(); + } + + static createElfSprite(scene, key) { + if (scene.textures.exists(key)) return; + const canvas = scene.textures.createCanvas(key, 32, 48); + const ctx = canvas.getContext(); + ctx.clearRect(0, 0, 32, 48); + + // Body (Slim, Pale) + ctx.fillStyle = '#98FB98'; // PaleGreen (Mutated Elf) + ctx.fillRect(10, 16, 12, 24); + + // Head + ctx.fillStyle = '#E0FFFF'; // LightCyan + ctx.fillRect(10, 4, 12, 12); + + // Ears (Pointy) + ctx.fillStyle = '#E0FFFF'; + ctx.beginPath(); ctx.moveTo(8, 8); ctx.lineTo(4, 4); ctx.lineTo(10, 12); ctx.fill(); + ctx.beginPath(); ctx.moveTo(24, 8); ctx.lineTo(28, 4); ctx.lineTo(22, 12); ctx.fill(); + + // Eyes (Glowing) + ctx.fillStyle = '#00FFFF'; + ctx.fillRect(12, 8, 2, 2); + ctx.fillRect(18, 8, 2, 2); + + canvas.refresh(); + } + + static createCowSprite(scene, key, activeMutation) { + if (scene.textures.exists(key)) return; + const canvas = scene.textures.createCanvas(key, 48, 32); + const ctx = canvas.getContext(); + ctx.clearRect(0, 0, 48, 32); + + // Body + ctx.fillStyle = activeMutation ? '#9ACD32' : '#FFFFFF'; // YellowGreen or White + ctx.fillRect(8, 10, 32, 16); + + // Spots (if normal) + if (!activeMutation) { + ctx.fillStyle = '#000000'; + ctx.fillRect(14, 12, 6, 6); + ctx.fillRect(28, 18, 8, 4); + } else { + // Glowing veins + ctx.fillStyle = '#00FF00'; + ctx.fillRect(12, 14, 24, 2); + } + + // Head + ctx.fillStyle = activeMutation ? '#9ACD32' : '#FFFFFF'; + ctx.fillRect(0, 8, 12, 12); + + // Legs + ctx.fillStyle = '#000000'; + ctx.fillRect(10, 26, 4, 6); + ctx.fillRect(34, 26, 4, 6); + + canvas.refresh(); + } + + static createChickenSprite(scene, key, activeMutation) { + if (scene.textures.exists(key)) return; + const canvas = scene.textures.createCanvas(key, 24, 24); + const ctx = canvas.getContext(); + ctx.clearRect(0, 0, 24, 24); + + // Body + ctx.fillStyle = activeMutation ? '#ADFF2F' : '#FFFFFF'; // GreenYellow or White + ctx.beginPath(); ctx.arc(12, 14, 8, 0, Math.PI * 2); ctx.fill(); + + // Head + ctx.beginPath(); ctx.arc(16, 8, 5, 0, Math.PI * 2); ctx.fill(); + + // Beak + ctx.fillStyle = '#ffaa00'; + ctx.beginPath(); ctx.moveTo(20, 8); ctx.lineTo(24, 10); ctx.lineTo(20, 12); ctx.fill(); + + // Comb + ctx.fillStyle = '#ff0000'; + ctx.fillRect(15, 3, 2, 3); + + canvas.refresh(); } static createPathStoneSprite(scene, key = 'path_stone') {