phase 11 koncano

This commit is contained in:
2025-12-08 14:01:41 +01:00
parent 07f0752d81
commit f3d476e843
21 changed files with 1503 additions and 200 deletions

View File

@@ -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**

View File

@@ -85,6 +85,7 @@
<script src="src/systems/FarmingSystem.js"></script>
<script src="src/systems/BuildingSystem.js"></script>
<script src="src/systems/WeatherSystem.js"></script>
<script src="src/systems/WorldEventSystem.js"></script>
<script src="src/systems/QuestSystem.js"></script>
<!-- DayNightSystem merged into WeatherSystem -->
<script src="src/systems/SoundManager.js"></script>
@@ -95,6 +96,8 @@
<script src="src/systems/LegacySystem.js"></script>
<script src="src/systems/ExpansionSystem.js"></script>
<script src="src/systems/BlueprintSystem.js"></script>
<script src="src/systems/CollectionSystem.js"></script>
<script src="src/systems/HybridSkillSystem.js"></script>
<!-- Multiplayer -->
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>

View File

@@ -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

View File

@@ -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}`;

View File

@@ -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();

View File

@@ -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();
}
});
});
}

View File

@@ -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'
});
}
}
}

View File

@@ -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 = {

View File

@@ -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);

View File

@@ -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);
}
}
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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;

View File

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

View File

@@ -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;

View File

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

View File

@@ -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 }

View File

@@ -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);
}
}
}

View File

@@ -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}`);

View File

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