phase 11 koncano
This commit is contained in:
38
TASKS.md
38
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**
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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', () => {
|
||||
// Fade out animation
|
||||
this.tweens.add({
|
||||
targets: [progressBar, progressBox, percentText, assetLoadingText, title, tipText, bg],
|
||||
alpha: 0,
|
||||
duration: 1000,
|
||||
onComplete: () => {
|
||||
progressBar.destroy();
|
||||
progressBox.destroy();
|
||||
loadingText.destroy();
|
||||
percentText.destroy();
|
||||
assetLoadingText.destroy();
|
||||
title.destroy();
|
||||
tipText.destroy();
|
||||
bg.destroy();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
// 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'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
|
||||
76
src/systems/CollectionSystem.js
Normal file
76
src/systems/CollectionSystem.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
110
src/systems/HybridSkillSystem.js
Normal file
110
src/systems/HybridSkillSystem.js
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,135 +285,107 @@ 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') {
|
||||
if (!isFarm && !isCity) {
|
||||
validPositions.push({ x, y });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DECORATIONS - Enhanced World Details
|
||||
console.log('🌸 Adding enhanced decorations...');
|
||||
|
||||
// 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++;
|
||||
// Direct Placement Calls (Trees/Rocks) - legacy method support
|
||||
// this.placeTree(x, y, terrainType.name); // Optional: keep this if preferred over random batch
|
||||
}
|
||||
}
|
||||
|
||||
// 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++;
|
||||
}
|
||||
// --- 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.
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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}`)) {
|
||||
// Rocks
|
||||
if (Math.random() < 0.02) {
|
||||
this.placeRock(pos.x, pos.y, 'grass');
|
||||
}
|
||||
|
||||
// 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++;
|
||||
}
|
||||
|
||||
// Path Stones
|
||||
if (Math.random() < 0.02) {
|
||||
this.addDecoration(pos.x, pos.y, 'path_stone');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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++;
|
||||
}
|
||||
}
|
||||
|
||||
// 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.`);
|
||||
}
|
||||
// Retained helper methods...
|
||||
|
||||
damageDecoration(x, y, amount) {
|
||||
const key = `${x},${y}`;
|
||||
@@ -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;
|
||||
|
||||
@@ -71,7 +71,25 @@ 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');
|
||||
|
||||
@@ -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 }
|
||||
|
||||
137
src/systems/WorldEventSystem.js
Normal file
137
src/systems/WorldEventSystem.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}`);
|
||||
|
||||
|
||||
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user