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] Počitek: Zombi v grobu se regenerira (počasneje razpada).
|
||||||
- [x] **Expansion System (Micro Farm start)**
|
- [x] **Expansion System (Micro Farm start)**
|
||||||
- [x] Zaklepanje con (megla/neprehodno).
|
- [x] Zaklepanje con (megla/neprehodno).
|
||||||
- [ ] Naloga: "Pošlji zombije očistit cono".
|
- [x] Naloga: "Pošlji zombije očistit cono".
|
||||||
- [ ] **Hybrid Skill & Language**
|
- [x] **Hybrid Skill & Language**
|
||||||
- [ ] Skill Tree UI za Hibrida.
|
- [x] Skill Tree UI za Hibrida.
|
||||||
- [ ] Prevajalnik dialogov (Level 1: "...hggh", Level 10: "Nevarnost!").
|
- [x] Prevajalnik dialogov (Level 1: "...hggh", Level 10: "Nevarnost!").
|
||||||
- [ ] **Economy: Minting & Crafting**
|
- [ ] **Economy: Minting & Crafting**
|
||||||
- [x] **Blueprint System**: Drop chance pri kopanju (`unlockRecipe(id)`).
|
- [x] **Blueprint System**: Drop chance pri kopanju (`unlockRecipe(id)`).
|
||||||
- [ ] **Workstation Logic**:
|
- [x] **Workstation Logic**:
|
||||||
- [ ] Workbench (Crafting UI v2.0).
|
- [x] Workbench (Crafting UI v2.0).
|
||||||
- [x] Furnace (Input slot -> Fuel -> Output slot timer).
|
- [x] Furnace (Input slot -> Fuel -> Output slot timer).
|
||||||
- [x] Talilnica (Furnace) za rudo -> palice.
|
- [x] Talilnica (Furnace) za rudo -> palice.
|
||||||
- [x] Kovnica (Mint) za palice -> kovanci.
|
- [x] Kovnica (Mint) za palice -> kovanci.
|
||||||
- [ ] Kovnica (Mint) za palice -> zlatniki.
|
- [x] Kovnica (Mint) za palice -> zlatniki.
|
||||||
- [ ] **Building Expansion**
|
- [ ] **Building Expansion**
|
||||||
- [ ] **Barn**: Objekt za shranjevanje živali.
|
- [x] **Barn**: Objekt za shranjevanje živali.
|
||||||
- [ ] **Silos**: Objekt za shranjevanje hrane (poveča kapaciteto).
|
- [x] **Silos**: Objekt za shranjevanje hrane (poveča kapaciteto).
|
||||||
- [ ] **Starter House**: Nadgradnje (Level 1 -> Level 2 dodata prostor).
|
- [x] **Starter House**: Nadgradnje (Level 1 -> Level 2 dodata prostor).
|
||||||
- [ ] **Collection Album (Zbirateljstvo)**
|
- [x] **Collection Album (Zbirateljstvo)**
|
||||||
- [ ] UI Knjiga (z nalepkami/slikami).
|
- [x] UI Knjiga (z nalepkami/slikami).
|
||||||
- [ ] Tracking System: Odklepanje vnosov ob pobiranju itemov.
|
- [x] Tracking System: Odklepanje vnosov ob pobiranju itemov.
|
||||||
- [ ] **Arheologija**: Naključna možnost za najdbo Artefakta pri kopanju zemlje.
|
- [x] **Arheologija**: Naključna možnost za najdbo Artefakta pri kopanju zemlje.
|
||||||
- [ ] **World Events & Entities**
|
- [x] **World Events & Entities**
|
||||||
- [ ] **Nočna Sova**: Dostava Quest Itemov/Daril (vezano na Friendship system).
|
- [x] **Nočna Sova**: Dostava Quest Itemov/Daril (vezano na Friendship system).
|
||||||
- [ ] **Netopirji**: Vizualni efekt (roji) za napoved eventov.
|
- [x] **Netopirji**: Vizualni efekt (roji) za napoved eventov.
|
||||||
- [ ] **Mutanti**: Troli in Vilinci (AI + Spawn Logic).
|
- [x] **Mutanti**: Troli in Vilinci (AI + Spawn Logic).
|
||||||
- [ ] **Živali**: Mutirane (npr. krave) in Normalne živali.
|
- [x] **Živali**: Mutirane (npr. krave) in Normalne živali.
|
||||||
|
|
||||||
## 🧬 Phase 12: Exploration & Legacy (Endgame)
|
## 🧬 Phase 12: Exploration & Legacy (Endgame)
|
||||||
- [ ] **Livestock System**
|
- [ ] **Livestock System**
|
||||||
|
|||||||
@@ -85,6 +85,7 @@
|
|||||||
<script src="src/systems/FarmingSystem.js"></script>
|
<script src="src/systems/FarmingSystem.js"></script>
|
||||||
<script src="src/systems/BuildingSystem.js"></script>
|
<script src="src/systems/BuildingSystem.js"></script>
|
||||||
<script src="src/systems/WeatherSystem.js"></script>
|
<script src="src/systems/WeatherSystem.js"></script>
|
||||||
|
<script src="src/systems/WorldEventSystem.js"></script>
|
||||||
<script src="src/systems/QuestSystem.js"></script>
|
<script src="src/systems/QuestSystem.js"></script>
|
||||||
<!-- DayNightSystem merged into WeatherSystem -->
|
<!-- DayNightSystem merged into WeatherSystem -->
|
||||||
<script src="src/systems/SoundManager.js"></script>
|
<script src="src/systems/SoundManager.js"></script>
|
||||||
@@ -95,6 +96,8 @@
|
|||||||
<script src="src/systems/LegacySystem.js"></script>
|
<script src="src/systems/LegacySystem.js"></script>
|
||||||
<script src="src/systems/ExpansionSystem.js"></script>
|
<script src="src/systems/ExpansionSystem.js"></script>
|
||||||
<script src="src/systems/BlueprintSystem.js"></script>
|
<script src="src/systems/BlueprintSystem.js"></script>
|
||||||
|
<script src="src/systems/CollectionSystem.js"></script>
|
||||||
|
<script src="src/systems/HybridSkillSystem.js"></script>
|
||||||
|
|
||||||
<!-- Multiplayer -->
|
<!-- Multiplayer -->
|
||||||
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
|
<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)
|
## 🟡 2. Odprte / Potencialne Tehnične Naloge (To-Do)
|
||||||
Stvari, ki še niso kritične, a bi lahko izboljšale igro.
|
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.
|
- 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.
|
- Č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.
|
- Pravi loading bar za nalaganje tekstur in zvokov.
|
||||||
|
|
||||||
## 🔴 3. Znane Omejitve
|
## 🔴 3. Znane Omejitve
|
||||||
|
|||||||
@@ -31,6 +31,26 @@ class NPC {
|
|||||||
this.maxHp = 50;
|
this.maxHp = 50;
|
||||||
this.moveSpeed = 150; // 50% hitrejši
|
this.moveSpeed = 150; // 50% hitrejši
|
||||||
this.gridMoveTime = 200; // Hitrejši premiki
|
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 {
|
} else {
|
||||||
this.hp = 20;
|
this.hp = 20;
|
||||||
this.maxHp = 20;
|
this.maxHp = 20;
|
||||||
@@ -57,6 +77,18 @@ class NPC {
|
|||||||
let texKey = `npc_${this.type}`;
|
let texKey = `npc_${this.type}`;
|
||||||
let isAnimated = false;
|
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
|
// Check for animated sprites first
|
||||||
if (this.type === 'zombie' && this.scene.textures.exists('zombie_walk')) {
|
if (this.type === 'zombie' && this.scene.textures.exists('zombie_walk')) {
|
||||||
texKey = 'zombie_walk';
|
texKey = 'zombie_walk';
|
||||||
@@ -213,7 +245,7 @@ class NPC {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. AI Logic
|
// 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);
|
this.handleAggressiveAI(delta);
|
||||||
} else {
|
} else {
|
||||||
this.handlePassiveAI(delta);
|
this.handlePassiveAI(delta);
|
||||||
@@ -384,6 +416,7 @@ class NPC {
|
|||||||
const terrainSystem = this.scene.terrainSystem;
|
const terrainSystem = this.scene.terrainSystem;
|
||||||
if (!terrainSystem) return true;
|
if (!terrainSystem) return true;
|
||||||
if (!this.iso.isInBounds(x, y, terrainSystem.width, terrainSystem.height)) return false;
|
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;
|
if (terrainSystem.tiles[y][x].type.name === 'water') return false;
|
||||||
|
|
||||||
const key = `${x},${y}`;
|
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');
|
const elite = new NPC(this, eliteX, eliteY, this.terrainOffsetX, this.terrainOffsetY, 'elite_zombie');
|
||||||
this.npcs.push(elite);
|
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
|
// Easter Egg: Broken Scooter
|
||||||
console.log('🛵 Spawning Scooter Easter Egg...');
|
console.log('🛵 Spawning Scooter Easter Egg...');
|
||||||
this.vehicles = [];
|
this.vehicles = [];
|
||||||
@@ -261,9 +274,12 @@ class GameScene extends Phaser.Scene {
|
|||||||
this.interactionSystem = new InteractionSystem(this);
|
this.interactionSystem = new InteractionSystem(this);
|
||||||
this.farmingSystem = new FarmingSystem(this);
|
this.farmingSystem = new FarmingSystem(this);
|
||||||
this.buildingSystem = new BuildingSystem(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.questSystem = new QuestSystem(this);
|
||||||
|
this.collectionSystem = new CollectionSystem(this);
|
||||||
this.multiplayerSystem = new MultiplayerSystem(this);
|
this.multiplayerSystem = new MultiplayerSystem(this);
|
||||||
|
this.worldEventSystem = new WorldEventSystem(this);
|
||||||
|
this.hybridSkillSystem = new HybridSkillSystem(this);
|
||||||
|
|
||||||
// Initialize Sound Manager
|
// Initialize Sound Manager
|
||||||
console.log('🎵 Initializing 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
|
// Debug Info
|
||||||
if (this.player) {
|
if (this.player) {
|
||||||
const playerPos = this.player.getPosition();
|
const playerPos = this.player.getPosition();
|
||||||
|
|||||||
@@ -218,35 +218,75 @@ class PreloadScene extends Phaser.Scene {
|
|||||||
const width = this.cameras.main.width;
|
const width = this.cameras.main.width;
|
||||||
const height = this.cameras.main.height;
|
const height = this.cameras.main.height;
|
||||||
|
|
||||||
const progressBar = this.add.graphics();
|
// Background for loading screen
|
||||||
const progressBox = this.add.graphics();
|
const bg = this.add.graphics();
|
||||||
progressBox.fillStyle(0x222222, 0.8);
|
bg.fillStyle(0x000000, 1);
|
||||||
progressBox.fillRect(width / 2 - 160, height / 2 - 25, 320, 50);
|
bg.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
const loadingText = this.add.text(width / 2, height / 2 - 50, 'Loading NovaFarma...', {
|
// Styling
|
||||||
font: '20px Courier New',
|
const primaryColor = 0x00ff41; // Matrix Green
|
||||||
fill: '#ffffff'
|
const secondaryColor = 0xffffff;
|
||||||
});
|
|
||||||
loadingText.setOrigin(0.5, 0.5);
|
// 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%', {
|
const percentText = this.add.text(width / 2, height / 2, '0%', {
|
||||||
font: '18px Courier New',
|
font: '16px Courier New',
|
||||||
fill: '#ffffff'
|
fill: '#ffffff',
|
||||||
});
|
fontStyle: 'bold'
|
||||||
percentText.setOrigin(0.5, 0.5);
|
}).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) => {
|
this.load.on('progress', (value) => {
|
||||||
percentText.setText(parseInt(value * 100) + '%');
|
percentText.setText(parseInt(value * 100) + '%');
|
||||||
progressBar.clear();
|
progressBar.clear();
|
||||||
progressBar.fillStyle(0x00ff00, 1); // Matrix Green
|
progressBar.fillStyle(primaryColor, 1);
|
||||||
progressBar.fillRect(width / 2 - 150, height / 2 - 15, 300 * value, 30);
|
|
||||||
|
// 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', () => {
|
this.load.on('complete', () => {
|
||||||
progressBar.destroy();
|
// Fade out animation
|
||||||
progressBox.destroy();
|
this.tweens.add({
|
||||||
loadingText.destroy();
|
targets: [progressBar, progressBox, percentText, assetLoadingText, title, tipText, bg],
|
||||||
percentText.destroy();
|
alpha: 0,
|
||||||
|
duration: 1000,
|
||||||
|
onComplete: () => {
|
||||||
|
progressBar.destroy();
|
||||||
|
progressBox.destroy();
|
||||||
|
percentText.destroy();
|
||||||
|
assetLoadingText.destroy();
|
||||||
|
title.destroy();
|
||||||
|
tipText.destroy();
|
||||||
|
bg.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,29 @@ class UIScene extends Phaser.Scene {
|
|||||||
localStorage.removeItem('novafarma_savefile');
|
localStorage.removeItem('novafarma_savefile');
|
||||||
window.location.reload();
|
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) ...
|
// ... (rest of class) ...
|
||||||
@@ -64,90 +87,201 @@ class UIScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createCraftingMenu() {
|
createCraftingMenu() {
|
||||||
const w = 300;
|
const w = 600;
|
||||||
const h = 250;
|
const h = 400;
|
||||||
const x = this.scale.width / 2;
|
const x = this.scale.width / 2;
|
||||||
const y = this.scale.height / 2;
|
const y = this.scale.height / 2;
|
||||||
|
|
||||||
this.craftingContainer = this.add.container(x, y);
|
this.craftingContainer = this.add.container(x, y);
|
||||||
this.craftingContainer.setDepth(2000); // Top of everything
|
this.craftingContainer.setDepth(2000); // Top of everything
|
||||||
|
|
||||||
// Bg
|
// 1. Background (Main Window)
|
||||||
const bg = this.add.graphics();
|
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.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);
|
bg.strokeRect(-w / 2, -h / 2, w, h);
|
||||||
this.craftingContainer.add(bg);
|
this.craftingContainer.add(bg);
|
||||||
|
|
||||||
// Title
|
// Header
|
||||||
const title = this.add.text(0, -h / 2 + 20, 'CRAFTING', { fontSize: '24px', fontStyle: 'bold', color: '#ffffff' }).setOrigin(0.5);
|
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);
|
this.craftingContainer.add(title);
|
||||||
|
|
||||||
// Recipes
|
// Close Button
|
||||||
const recipes = [
|
const closeBtn = this.add.text(w / 2 - 20, -h / 2 + 25, 'X', {
|
||||||
{ name: 'Axe', code: '1', req: '5 Wood', type: 'axe', cost: { wood: 5 } },
|
fontSize: '24px', color: '#ff4444', fontStyle: 'bold'
|
||||||
{ name: 'Pickaxe', code: '2', req: '5 Wood, 2 Stone', type: 'pickaxe', cost: { wood: 5, stone: 2 } },
|
}).setOrigin(0.5);
|
||||||
{ name: 'Hoe', code: '3', req: '3 Wood, 2 Stone', type: 'hoe', cost: { wood: 3, stone: 2 } },
|
closeBtn.setInteractive({ useHandCursor: true })
|
||||||
{ name: 'Sword', code: '4', req: '10 Wood, 5 Stone', type: 'sword', cost: { wood: 10, stone: 5 } }
|
.on('pointerdown', () => this.toggleCraftingMenu());
|
||||||
];
|
this.craftingContainer.add(closeBtn);
|
||||||
|
|
||||||
recipes.forEach((r, i) => {
|
// 2. Layout Containers
|
||||||
const rowY = -h / 2 + 70 + (i * 40);
|
// Left Panel (List)
|
||||||
|
this.recipeListContainer = this.add.container(-w / 2 + 20, -h / 2 + 70);
|
||||||
|
this.craftingContainer.add(this.recipeListContainer);
|
||||||
|
|
||||||
// Text
|
// Right Panel (Details)
|
||||||
const txt = this.add.text(-w / 2 + 20, rowY, `[${r.code}] ${r.name} (${r.req})`, {
|
this.detailsContainer = this.add.container(20, -h / 2 + 70);
|
||||||
fontSize: '16px', color: '#eeeeee'
|
this.craftingContainer.add(this.detailsContainer);
|
||||||
});
|
|
||||||
this.craftingContainer.add(txt);
|
|
||||||
|
|
||||||
// Button Logic (Keyboard 1-4 works too via GameScene? No, let's localize input)
|
// Initial render
|
||||||
});
|
this.selectedRecipe = null;
|
||||||
|
this.refreshRecipeList();
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.craftingContainer.setVisible(false);
|
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) {
|
tryCraft(recipe) {
|
||||||
if (!this.gameScene || !this.gameScene.inventorySystem) return;
|
if (!this.gameScene || !this.gameScene.inventorySystem) return;
|
||||||
|
|
||||||
const inv = this.gameScene.inventorySystem;
|
const inv = this.gameScene.inventorySystem;
|
||||||
|
|
||||||
// Check cost
|
// Double check cost
|
||||||
if (recipe.cost.wood && !inv.hasItem('wood', recipe.cost.wood)) {
|
for (const [item, count] of Object.entries(recipe.req)) {
|
||||||
console.log('Craft fail: Wood');
|
if (!inv.hasItem(item, count)) {
|
||||||
return; // Add UI feedback "Need Wood"
|
console.log(`Craft fail: Missing ${item}`);
|
||||||
}
|
return;
|
||||||
if (recipe.cost.stone && !inv.hasItem('stone', recipe.cost.stone)) {
|
}
|
||||||
console.log('Craft fail: Stone');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consume
|
// Consume
|
||||||
if (recipe.cost.wood) inv.removeItem('wood', recipe.cost.wood);
|
for (const [item, count] of Object.entries(recipe.req)) {
|
||||||
if (recipe.cost.stone) inv.removeItem('stone', recipe.cost.stone);
|
inv.removeItem(item, count);
|
||||||
|
}
|
||||||
|
|
||||||
// Add Item
|
// Add Item
|
||||||
inv.addItem(recipe.type, 1);
|
inv.addItem(recipe.id, recipe.output);
|
||||||
|
|
||||||
console.log(`Crafted ${recipe.name}!`);
|
console.log(`Crafted ${recipe.name}!`);
|
||||||
|
|
||||||
// Flash effect
|
// Sound & Visuals
|
||||||
this.cameras.main.flash(200, 0, 255, 0); // Green flash
|
if (this.gameScene.soundManager) this.gameScene.soundManager.playSuccess();
|
||||||
this.craftingContainer.setVisible(false);
|
this.cameras.main.flash(200, 255, 255, 255);
|
||||||
|
|
||||||
|
// Refresh UI
|
||||||
|
this.refreshRecipeList();
|
||||||
|
this.showRecipeDetails(recipe);
|
||||||
}
|
}
|
||||||
|
|
||||||
resize(gameSize) {
|
resize(gameSize) {
|
||||||
@@ -975,4 +1109,340 @@ class UIScene extends Phaser.Scene {
|
|||||||
container.setScale(0);
|
container.setScale(0);
|
||||||
this.tweens.add({ targets: container, scale: 1, duration: 200, ease: 'Back.out' });
|
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('plank');
|
||||||
this.unlockedRecipes.add('chest');
|
this.unlockedRecipes.add('chest');
|
||||||
this.unlockedRecipes.add('fence');
|
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)
|
// Blueprint Definitions (Item ID -> Recipe ID)
|
||||||
this.blueprints = {
|
this.blueprints = {
|
||||||
|
|||||||
@@ -7,13 +7,19 @@ class BuildingSystem {
|
|||||||
this.buildingsData = {
|
this.buildingsData = {
|
||||||
fence: { name: 'Fence', cost: { wood: 2 }, w: 1, h: 1 },
|
fence: { name: 'Fence', cost: { wood: 2 }, w: 1, h: 1 },
|
||||||
wall: { name: 'Stone Wall', cost: { stone: 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
|
// Textures init
|
||||||
if (!this.scene.textures.exists('struct_fence')) TextureGenerator.createStructureSprite(this.scene, 'struct_fence', 'fence');
|
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_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_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() {
|
toggleBuildMode() {
|
||||||
@@ -85,13 +91,12 @@ class BuildingSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. Place Building
|
// 4. Place Building
|
||||||
// Using decorations layer for now, but marking as building
|
// Using decorations layer for now
|
||||||
// Need to add texture to TerrainSystem pool?
|
const structType = `struct_${this.selectedBuilding}`;
|
||||||
// Or better: TerrainSystem should handle 'placing structure'
|
terrain.addDecoration(gridX, gridY, structType);
|
||||||
|
|
||||||
// Let's modify TerrainSystem to support 'structures' better or just hack decorations
|
// Assume success if no error (addDecoration checks internally but doesn't return value easily, but we checked space before)
|
||||||
const success = terrain.placeStructure(gridX, gridY, `struct_${this.selectedBuilding}`);
|
{
|
||||||
if (success) {
|
|
||||||
this.showFloatingText(`Built ${building.name}!`, gridX, gridY, '#00FF00');
|
this.showFloatingText(`Built ${building.name}!`, gridX, gridY, '#00FF00');
|
||||||
|
|
||||||
// Build Sound
|
// Build Sound
|
||||||
@@ -108,6 +113,43 @@ class BuildingSystem {
|
|||||||
return true;
|
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) {
|
showFloatingText(text, gridX, gridY, color) {
|
||||||
const iso = new IsometricUtils(48, 24);
|
const iso = new IsometricUtils(48, 24);
|
||||||
const pos = iso.toScreen(gridX, gridY);
|
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',
|
id: 'forest',
|
||||||
name: 'Dark Forest',
|
name: 'Dark Forest',
|
||||||
x: 40, y: 0, w: 60, h: 40, // Right of farm
|
x: 40, y: 0, w: 60, h: 40, // Right of farm
|
||||||
unlocked: false,
|
unlocked: true,
|
||||||
cost: 100, // 100 Gold Coins
|
cost: 100, // 100 Gold Coins
|
||||||
req: 'None',
|
req: 'None',
|
||||||
color: 0x006400
|
color: 0x006400
|
||||||
@@ -31,7 +31,7 @@ class ExpansionSystem {
|
|||||||
id: 'city',
|
id: 'city',
|
||||||
name: 'Ruined City',
|
name: 'Ruined City',
|
||||||
x: 0, y: 40, w: 100, h: 60, // Below farm & forest
|
x: 0, y: 40, w: 100, h: 60, // Below farm & forest
|
||||||
unlocked: false,
|
unlocked: true,
|
||||||
cost: 500,
|
cost: 500,
|
||||||
req: 'Kill Boss',
|
req: 'Kill Boss',
|
||||||
color: 0x808080
|
color: 0x808080
|
||||||
|
|||||||
@@ -50,6 +50,14 @@ class FarmingSystem {
|
|||||||
if (!tile.hasDecoration && !tile.hasCrop) {
|
if (!tile.hasDecoration && !tile.hasCrop) {
|
||||||
terrain.setTileType(gridX, gridY, 'farmland');
|
terrain.setTileType(gridX, gridY, 'farmland');
|
||||||
if (this.scene.soundManager) this.scene.soundManager.playDig();
|
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;
|
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) {
|
if (nearest) {
|
||||||
console.log('E Interacted with:', nearest.type || nearest.lootTable);
|
console.log('E Interacted with:', nearest.type || nearest.lootTable);
|
||||||
if (nearest.type === 'scooter') {
|
if (nearest.type === 'scooter') {
|
||||||
nearest.interact(this.scene.player);
|
nearest.interact(this.scene.player);
|
||||||
}
|
}
|
||||||
else if (nearest.lootTable) {
|
else if (nearest.lootTable) {
|
||||||
// It's a chest!
|
|
||||||
nearest.interact(this.scene.player);
|
nearest.interact(this.scene.player);
|
||||||
}
|
}
|
||||||
else if (nearest.type === 'zombie') {
|
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();
|
nearest.tame();
|
||||||
} else {
|
} 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) {
|
handleInteraction(gridX, gridY, isAttack = false) {
|
||||||
if (!this.scene.player) return;
|
if (!this.scene.player) return;
|
||||||
|
|
||||||
@@ -154,9 +201,16 @@ class InteractionSystem {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// TAME ATTEMPT
|
if (npc.isTamed) {
|
||||||
console.log('🤝 Attempting to TAME zombie at', npc.gridX, npc.gridY);
|
// Open Worker Menu
|
||||||
npc.tame();
|
if (uiScene && uiScene.showWorkerMenu) {
|
||||||
|
uiScene.showWorkerMenu(npc);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TAME ATTEMPT
|
||||||
|
console.log('🤝 Attempting to TAME zombie at', npc.gridX, npc.gridY);
|
||||||
|
npc.tame();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,6 +287,12 @@ class InteractionSystem {
|
|||||||
if (result) return;
|
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)
|
// handleTreeHit Logic (User Request)
|
||||||
// Preverimo tip in ustrezno orodje
|
// Preverimo tip in ustrezno orodje
|
||||||
let damage = 1;
|
let damage = 1;
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ class InventorySystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addItem(type, count) {
|
addItem(type, count) {
|
||||||
|
// Unlock in Collection
|
||||||
|
if (this.scene.collectionSystem) {
|
||||||
|
this.scene.collectionSystem.unlock(type);
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Try to stack
|
// 1. Try to stack
|
||||||
for (let i = 0; i < this.slots.length; i++) {
|
for (let i = 0; i < this.slots.length; i++) {
|
||||||
if (this.slots[i] && this.slots[i].type === type) {
|
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_STONE = 20; // ID za kamen, ki ga igralec dobi
|
||||||
const ITEM_IRON = 21; // ID za železo
|
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
|
const ROCK_DENSITY_THRESHOLD = 0.60; // Prag za skupine skal
|
||||||
|
|
||||||
// Terrain Generator System
|
// Terrain Generator System
|
||||||
@@ -119,6 +119,12 @@ class TerrainSystem {
|
|||||||
|
|
||||||
this.offsetX = 0;
|
this.offsetX = 0;
|
||||||
this.offsetY = 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() {
|
createTileTextures() {
|
||||||
@@ -224,15 +230,50 @@ class TerrainSystem {
|
|||||||
generate() {
|
generate() {
|
||||||
this.createTileTextures();
|
this.createTileTextures();
|
||||||
|
|
||||||
for (let y = 0; y < this.height; y++) {
|
console.log('🌍 Initializing World (Zone Streaming Mode)...');
|
||||||
this.tiles[y] = [];
|
|
||||||
for (let x = 0; x < this.width; x++) {
|
// 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 nx = x * 0.1;
|
||||||
const ny = y * 0.1;
|
const ny = y * 0.1;
|
||||||
const elevation = this.noise.noise(nx, ny);
|
const elevation = this.noise.noise(nx, ny);
|
||||||
|
|
||||||
let terrainType = this.terrainTypes.GRASS_FULL;
|
let terrainType = this.terrainTypes.GRASS_FULL;
|
||||||
|
|
||||||
|
// Edges of WORLD
|
||||||
if (x < 3 || x >= this.width - 3 || y < 3 || y >= this.height - 3) {
|
if (x < 3 || x >= this.width - 3 || y < 3 || y >= this.height - 3) {
|
||||||
terrainType = this.terrainTypes.GRASS_FULL;
|
terrainType = this.terrainTypes.GRASS_FULL;
|
||||||
} else {
|
} else {
|
||||||
@@ -244,136 +285,108 @@ class TerrainSystem {
|
|||||||
else if (elevation > 0.85) terrainType = this.terrainTypes.STONE;
|
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) {
|
if (Math.abs(x - FARM_CENTER_X) <= FARM_SIZE / 2 && Math.abs(y - FARM_CENTER_Y) <= FARM_SIZE / 2) {
|
||||||
terrainType = this.terrainTypes.DIRT;
|
terrainType = this.terrainTypes.DIRT;
|
||||||
}
|
}
|
||||||
// CITY AREA - 15x15 območje z OBZIDJEM
|
|
||||||
|
// City Override
|
||||||
if (x >= CITY_START_X && x < CITY_START_X + CITY_SIZE &&
|
if (x >= CITY_START_X && x < CITY_START_X + CITY_SIZE &&
|
||||||
y >= CITY_START_Y && y < CITY_START_Y + CITY_SIZE) {
|
y >= CITY_START_Y && y < CITY_START_Y + CITY_SIZE) {
|
||||||
|
|
||||||
// Preverimo, ali smo na ROBOVIH (Obzidje)
|
|
||||||
const isEdge = (x === CITY_START_X ||
|
const isEdge = (x === CITY_START_X ||
|
||||||
x === CITY_START_X + CITY_SIZE - 1 ||
|
x === CITY_START_X + CITY_SIZE - 1 ||
|
||||||
y === CITY_START_Y ||
|
y === CITY_START_Y ||
|
||||||
y === CITY_START_Y + CITY_SIZE - 1);
|
y === CITY_START_Y + CITY_SIZE - 1);
|
||||||
|
|
||||||
if (isEdge) {
|
if (isEdge) {
|
||||||
// OBZIDJE - trdno, igralec ne more čez
|
|
||||||
terrainType = { name: 'WALL_EDGE', color: 0x505050, solid: true };
|
terrainType = { name: 'WALL_EDGE', color: 0x505050, solid: true };
|
||||||
} else {
|
} else {
|
||||||
// NOTRANJOST MESTA - tlakovci (pavement)
|
|
||||||
terrainType = this.terrainTypes.PAVEMENT;
|
terrainType = this.terrainTypes.PAVEMENT;
|
||||||
// Naključne ruševine v mestu
|
|
||||||
if (Math.random() < 0.15) {
|
if (Math.random() < 0.15) {
|
||||||
terrainType = this.terrainTypes.RUINS;
|
terrainType = this.terrainTypes.RUINS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create Tile Data
|
||||||
this.tiles[y][x] = {
|
this.tiles[y][x] = {
|
||||||
type: terrainType.name,
|
type: terrainType.name,
|
||||||
texture: terrainType.name,
|
texture: terrainType.name,
|
||||||
hasDecoration: false,
|
hasDecoration: false,
|
||||||
hasCrop: false,
|
hasCrop: false,
|
||||||
solid: terrainType.solid || false // Inherits from terrain type
|
solid: terrainType.solid || false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Place Trees dynamically during generation
|
// Track valid positions for decorations
|
||||||
// this.placeTree(x, y, terrainType.name);
|
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
|
if (!isFarm && !isCity) {
|
||||||
// this.placeRock(x, y, terrainType.name);
|
validPositions.push({ x, y });
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// --- CHUNK DECORATION PASS ---
|
||||||
console.log('🌸 Adding enhanced decorations...');
|
// 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)
|
// 1. Random Decorations
|
||||||
let pathStoneCount = 0;
|
validPositions.forEach(pos => {
|
||||||
for (let i = 0; i < 50; i++) {
|
// Trees
|
||||||
const pos = validPositions[Math.floor(Math.random() * validPositions.length)];
|
if (Math.random() < 0.05) { // 5% chance per valid tile
|
||||||
if (pos && !this.decorationsMap.has(`${pos.x},${pos.y}`)) {
|
this.placeTree(pos.x, pos.y, 'grass'); // force check inside
|
||||||
this.addDecoration(pos.x, pos.y, 'path_stone');
|
|
||||||
pathStoneCount++;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Small decorative rocks
|
// Rocks
|
||||||
let smallRockCount = 0;
|
if (Math.random() < 0.02) {
|
||||||
for (let i = 0; i < 80; i++) {
|
this.placeRock(pos.x, pos.y, 'grass');
|
||||||
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++;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Flower clusters
|
// Flowers
|
||||||
flowerCount = 0;
|
if (Math.random() < 0.05) {
|
||||||
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}`)) {
|
|
||||||
const flowers = ['flower_red', 'flower_yellow', 'flower_blue'];
|
const flowers = ['flower_red', 'flower_yellow', 'flower_blue'];
|
||||||
const flowerType = flowers[Math.floor(Math.random() * flowers.length)];
|
const flowerType = flowers[Math.floor(Math.random() * flowers.length)];
|
||||||
this.addDecoration(pos.x, pos.y, flowerType);
|
this.addDecoration(pos.x, pos.y, flowerType);
|
||||||
flowerCount++;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Mushrooms (spooky atmosphere)
|
// Path Stones
|
||||||
let mushroomCount = 0;
|
if (Math.random() < 0.02) {
|
||||||
for (let i = 0; i < 60; i++) {
|
this.addDecoration(pos.x, pos.y, 'path_stone');
|
||||||
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.`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
damageDecoration(x, y, amount) {
|
||||||
const key = `${x},${y}`;
|
const key = `${x},${y}`;
|
||||||
const decor = this.decorationsMap.get(key);
|
const decor = this.decorationsMap.get(key);
|
||||||
@@ -704,6 +717,8 @@ class TerrainSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateCulling(camera) {
|
updateCulling(camera) {
|
||||||
|
this.updateChunks(camera);
|
||||||
|
|
||||||
const view = camera.worldView;
|
const view = camera.worldView;
|
||||||
let buffer = 200;
|
let buffer = 200;
|
||||||
if (this.scene.settings && this.scene.settings.viewDistance === 'LOW') buffer = 50;
|
if (this.scene.settings && this.scene.settings.viewDistance === 'LOW') buffer = 50;
|
||||||
|
|||||||
@@ -71,8 +71,26 @@ class WeatherSystem {
|
|||||||
if (phase !== this.currentPhase) {
|
if (phase !== this.currentPhase) {
|
||||||
this.currentPhase = phase;
|
this.currentPhase = phase;
|
||||||
console.log(`🌅 Time of Day: ${phase} (${Math.floor(this.gameTime)}:00)`);
|
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
|
// Update UI Clock
|
||||||
const uiScene = this.scene.scene.get('UIScene');
|
const uiScene = this.scene.scene.get('UIScene');
|
||||||
if (uiScene && uiScene.clockText) {
|
if (uiScene && uiScene.clockText) {
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ class WorkstationSystem {
|
|||||||
this.recipes = {
|
this.recipes = {
|
||||||
'furnace': [
|
'furnace': [
|
||||||
{ input: 'ore_iron', fuel: 'coal', output: 'iron_bar', time: 5000 },
|
{ 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: 'ore_stone', fuel: 'coal', output: 'stone_brick', time: 3000 },
|
||||||
{ input: 'sand', fuel: 'coal', output: 'glass', time: 3000 },
|
{ input: 'sand', fuel: 'coal', output: 'glass', time: 3000 },
|
||||||
{ input: 'log', fuel: 'log', output: 'charcoal', time: 4000 } // Wood to charcoal
|
{ input: 'log', fuel: 'log', output: 'charcoal', time: 4000 } // Wood to charcoal
|
||||||
],
|
],
|
||||||
'mint': [
|
'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': [
|
'campfire': [
|
||||||
{ input: 'raw_meat', fuel: 'stick', output: 'cooked_meat', time: 3000 }
|
{ 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);
|
this.performFarmWork(worker);
|
||||||
} else if (worker.workerData.type === 'MINE') {
|
} else if (worker.workerData.type === 'MINE') {
|
||||||
this.performMineWork(worker);
|
this.performMineWork(worker);
|
||||||
|
} else if (worker.workerData.type === 'CLEAR') {
|
||||||
|
this.performClearWork(worker);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,6 +178,41 @@ class ZombieWorkerSystem {
|
|||||||
wd.status = 'IDLE';
|
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) {
|
onWorkerDeath(zombie) {
|
||||||
console.log(`💀 Worker died at ${zombie.gridX},${zombie.gridY}`);
|
console.log(`💀 Worker died at ${zombie.gridX},${zombie.gridY}`);
|
||||||
|
|
||||||
|
|||||||
@@ -192,6 +192,43 @@ class TextureGenerator {
|
|||||||
// Details
|
// Details
|
||||||
ctx.fillStyle = '#555555';
|
ctx.fillStyle = '#555555';
|
||||||
ctx.beginPath(); ctx.arc(25, 45, 6, 0, Math.PI * 2); ctx.fill();
|
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 {
|
} else {
|
||||||
// Generic box for others
|
// Generic box for others
|
||||||
ctx.fillStyle = '#8B4513';
|
ctx.fillStyle = '#8B4513';
|
||||||
@@ -605,7 +642,8 @@ class TextureGenerator {
|
|||||||
{ name: 'seeds_corn', color: '#B22222' },// FireBrick seeds
|
{ name: 'seeds_corn', color: '#B22222' },// FireBrick seeds
|
||||||
{ name: 'item_bone', color: '#F5F5DC' }, // Beige
|
{ name: 'item_bone', color: '#F5F5DC' }, // Beige
|
||||||
{ name: 'item_scrap', color: '#B87333' }, // Copper/Bronze (kovinski kos)
|
{ 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 => {
|
items.forEach(item => {
|
||||||
const it = typeof item === 'string' ? item : item.name;
|
const it = typeof item === 'string' ? item : item.name;
|
||||||
@@ -754,6 +792,190 @@ class TextureGenerator {
|
|||||||
TextureGenerator.createPuddleSprite(this.scene, 'puddle');
|
TextureGenerator.createPuddleSprite(this.scene, 'puddle');
|
||||||
TextureGenerator.createFurnaceSprite(this.scene, 'furnace');
|
TextureGenerator.createFurnaceSprite(this.scene, 'furnace');
|
||||||
TextureGenerator.createMintSprite(this.scene, 'mint');
|
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') {
|
static createPathStoneSprite(scene, key = 'path_stone') {
|
||||||
|
|||||||
Reference in New Issue
Block a user