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') {