Phase 29: Gameplay Systems (5/5) - Structure interaction, NPCs, Enemies, Quests, Map
This commit is contained in:
@@ -102,14 +102,54 @@ class GameScene extends Phaser.Scene {
|
||||
this.lakeSystem.generateLakes(this.riverSystem);
|
||||
console.log('✅ Lake System ready!');
|
||||
|
||||
// 🏛️ PHASE 28 SESSION 6: STRUCTURE SYSTEM
|
||||
console.log('🏛️ Initializing Structure System...');
|
||||
this.structureSystem = new StructureSystem(500, 500, this.biomeSystem, this.riverSystem, this.lakeSystem);
|
||||
this.structureSystem.generateAll();
|
||||
const structStats = this.structureSystem.getStats();
|
||||
console.log(`✅ Structure System ready! (${structStats.structures} structures, ${structStats.landmarks} landmarks, ${structStats.roads.length} roads)`);
|
||||
|
||||
// Connect systems to terrainSystem
|
||||
this.terrainSystem.biomeSystem = this.biomeSystem;
|
||||
this.terrainSystem.chunkManager = this.chunkManager;
|
||||
this.terrainSystem.transitionSystem = this.transitionSystem;
|
||||
this.terrainSystem.riverSystem = this.riverSystem;
|
||||
this.terrainSystem.lakeSystem = this.lakeSystem;
|
||||
this.terrainSystem.structureSystem = this.structureSystem; // 🏛️ SESSION 6
|
||||
console.log('✅ BiomeSystem & ChunkManager connected to terrainSystem');
|
||||
|
||||
// 🎮 PHASE 29: GAMEPLAY SYSTEMS
|
||||
console.log('🎮 Initializing Phase 29 Systems...');
|
||||
|
||||
// Structure Interaction
|
||||
this.structureInteraction = new StructureInteractionSystem(this);
|
||||
this.structureInteraction.generateChestsForStructures(this.structureSystem);
|
||||
console.log(`✅ Structure Interaction ready! (${this.structureInteraction.chests.size} chests)`);
|
||||
|
||||
// NPC Population
|
||||
this.npcPopulation = new NPCPopulationSystem(this);
|
||||
this.npcPopulation.populateStructures(this.structureSystem);
|
||||
const npcStats = this.npcPopulation.getStats();
|
||||
console.log(`✅ NPC Population ready! (${npcStats.totalNPCs} NPCs)`);
|
||||
|
||||
// Biome Enemies
|
||||
this.biomeEnemies = new BiomeEnemySystem(this);
|
||||
this.biomeEnemies.generateSpawns(this.biomeSystem);
|
||||
const enemyStats = this.biomeEnemies.getStats();
|
||||
console.log(`✅ Biome Enemies ready! (${enemyStats.alive} enemies)`);
|
||||
|
||||
// Quest System
|
||||
this.landmarkQuests = new LandmarkQuestSystem(this);
|
||||
this.landmarkQuests.startMainQuest();
|
||||
this.landmarkQuests.startExplorationQuests();
|
||||
console.log(`✅ Quest System ready! (${this.landmarkQuests.activeQuests.length} active quests)`);
|
||||
|
||||
// Map Reveal
|
||||
this.mapReveal = new MapRevealSystem(this);
|
||||
console.log('✅ Map Reveal System ready!');
|
||||
|
||||
console.log('🎉 Phase 29 Systems: ALL READY!');
|
||||
|
||||
await this.terrainSystem.generate();
|
||||
console.log('✅ Flat 2D terrain ready!');
|
||||
|
||||
|
||||
274
src/systems/BiomeEnemySystem.js
Normal file
274
src/systems/BiomeEnemySystem.js
Normal file
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* 👹 BIOME ENEMY SYSTEM
|
||||
* Spawns biome-specific enemies across the world
|
||||
* - Different enemies per biome
|
||||
* - Difficulty scaling
|
||||
* - Loot drops
|
||||
* - Combat integration
|
||||
*/
|
||||
|
||||
class BiomeEnemySystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// All enemies in the world
|
||||
this.enemies = [];
|
||||
|
||||
// Enemy types per biome
|
||||
this.enemyTypes = {
|
||||
'Grassland': [
|
||||
{ type: 'wolf', hp: 30, damage: 5, speed: 1.5, loot: ['meat', 'fur'], color: 0x8B4513 },
|
||||
{ type: 'boar', hp: 40, damage: 7, speed: 1.2, loot: ['meat', 'tusk'], color: 0x654321 },
|
||||
{ type: 'bandit', hp: 50, damage: 10, speed: 1.0, loot: ['gold', 'sword'], color: 0x696969 }
|
||||
],
|
||||
'Forest': [
|
||||
{ type: 'goblin', hp: 25, damage: 6, speed: 1.3, loot: ['gold', 'dagger'], color: 0x228B22 },
|
||||
{ type: 'spider', hp: 20, damage: 4, speed: 1.8, loot: ['web', 'poison'], color: 0x2F4F4F },
|
||||
{ type: 'ent', hp: 80, damage: 15, speed: 0.8, loot: ['wood', 'seed'], color: 0x8B4513 }
|
||||
],
|
||||
'Desert': [
|
||||
{ type: 'scorpion', hp: 35, damage: 8, speed: 1.4, loot: ['poison', 'chitin'], color: 0xD2691E },
|
||||
{ type: 'mummy', hp: 60, damage: 12, speed: 0.9, loot: ['bandage', 'ancient_coin'], color: 0xDEB887 },
|
||||
{ type: 'sand_worm', hp: 100, damage: 20, speed: 0.7, loot: ['scales', 'tooth'], color: 0xC0B090 }
|
||||
],
|
||||
'Mountain': [
|
||||
{ type: 'troll', hp: 90, damage: 18, speed: 0.8, loot: ['stone', 'club'], color: 0x708090 },
|
||||
{ type: 'golem', hp: 120, damage: 25, speed: 0.6, loot: ['ore', 'core'], color: 0x696969 },
|
||||
{ type: 'harpy', hp: 45, damage: 10, speed: 2.0, loot: ['feather', 'talon'], color: 0x9370DB }
|
||||
],
|
||||
'Swamp': [
|
||||
{ type: 'zombie', hp: 50, damage: 10, speed: 0.9, loot: ['bone', 'rotten_flesh'], color: 0x556B2F },
|
||||
{ type: 'will_o_wisp', hp: 30, damage: 8, speed: 2.5, loot: ['essence', 'spark'], color: 0x00FFFF },
|
||||
{ type: 'swamp_dragon', hp: 150, damage: 30, speed: 0.7, loot: ['scale', 'heart'], color: 0x2F4F2F }
|
||||
]
|
||||
};
|
||||
|
||||
// Spawn density per biome
|
||||
this.spawnDensity = {
|
||||
'Grassland': 0.02, // 2% per tile
|
||||
'Forest': 0.03, // 3%
|
||||
'Desert': 0.015, // 1.5%
|
||||
'Mountain': 0.025, // 2.5%
|
||||
'Swamp': 0.035 // 3.5%
|
||||
};
|
||||
|
||||
console.log('👹 BiomeEnemySystem initialized');
|
||||
}
|
||||
|
||||
// Generate enemy spawns across the world
|
||||
generateSpawns(biomeSystem) {
|
||||
if (!biomeSystem) return;
|
||||
|
||||
let enemiesSpawned = 0;
|
||||
const sampleRate = 10; // Check every 10th tile
|
||||
|
||||
for (let x = 0; x < 500; x += sampleRate) {
|
||||
for (let y = 0; y < 500; y += sampleRate) {
|
||||
const biome = biomeSystem.getBiome(x, y);
|
||||
const density = this.spawnDensity[biome] || 0.02;
|
||||
|
||||
if (Math.random() < density) {
|
||||
this.spawnEnemy(x, y, biome);
|
||||
enemiesSpawned++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ Spawned ${enemiesSpawned} enemies across world`);
|
||||
}
|
||||
|
||||
// Spawn single enemy
|
||||
spawnEnemy(x, y, biome) {
|
||||
const enemyList = this.enemyTypes[biome] || this.enemyTypes['Grassland'];
|
||||
const enemyTemplate = enemyList[Math.floor(Math.random() * enemyList.length)];
|
||||
|
||||
const enemy = {
|
||||
x,
|
||||
y,
|
||||
type: enemyTemplate.type,
|
||||
biome,
|
||||
hp: enemyTemplate.hp,
|
||||
maxHp: enemyTemplate.hp,
|
||||
damage: enemyTemplate.damage,
|
||||
speed: enemyTemplate.speed,
|
||||
loot: [...enemyTemplate.loot],
|
||||
color: enemyTemplate.color,
|
||||
alive: true,
|
||||
sprite: null,
|
||||
lastMoveTime: 0
|
||||
};
|
||||
|
||||
this.enemies.push(enemy);
|
||||
return enemy;
|
||||
}
|
||||
|
||||
// Create enemy sprite when chunk loads
|
||||
createEnemySprite(enemy, chunk) {
|
||||
if (enemy.sprite || !enemy.alive) return;
|
||||
|
||||
const worldX = enemy.x * 48 + 24;
|
||||
const worldY = enemy.y * 48 + 24;
|
||||
|
||||
// Simple circle sprite
|
||||
const sprite = this.scene.add.circle(worldX, worldY, 15, enemy.color);
|
||||
sprite.setDepth(10 + worldY);
|
||||
|
||||
// HP bar
|
||||
const hpBar = this.scene.add.rectangle(worldX, worldY - 25, 30, 4, 0xFF0000);
|
||||
hpBar.setOrigin(0, 0.5);
|
||||
hpBar.setDepth(10 + worldY);
|
||||
|
||||
const hpFill = this.scene.add.rectangle(worldX, worldY - 25, 30, 4, 0x00FF00);
|
||||
hpFill.setOrigin(0, 0.5);
|
||||
hpFill.setDepth(10 + worldY);
|
||||
|
||||
enemy.sprite = sprite;
|
||||
enemy.hpBar = hpBar;
|
||||
enemy.hpFill = hpFill;
|
||||
|
||||
if (chunk) {
|
||||
chunk.sprites.push(sprite);
|
||||
chunk.sprites.push(hpBar);
|
||||
chunk.sprites.push(hpFill);
|
||||
}
|
||||
}
|
||||
|
||||
// Update enemies (AI, movement)
|
||||
update(time, delta, playerX, playerY) {
|
||||
for (const enemy of this.enemies) {
|
||||
if (!enemy.alive || !enemy.sprite) continue;
|
||||
|
||||
// Simple AI: Move towards player if nearby
|
||||
const dist = Math.sqrt((enemy.x - playerX) ** 2 + (enemy.y - playerY) ** 2);
|
||||
|
||||
if (dist < 10 && time > enemy.lastMoveTime + 500) {
|
||||
// Move towards player
|
||||
const dx = playerX - enemy.x;
|
||||
const dy = playerY - enemy.y;
|
||||
const len = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (len > 0) {
|
||||
enemy.x += (dx / len) * enemy.speed * 0.1;
|
||||
enemy.y += (dy / len) * enemy.speed * 0.1;
|
||||
|
||||
// Update sprite position
|
||||
enemy.sprite.setPosition(enemy.x * 48 + 24, enemy.y * 48 + 24);
|
||||
if (enemy.hpBar) {
|
||||
enemy.hpBar.setPosition(enemy.x * 48 + 24, enemy.y * 48 - 1);
|
||||
enemy.hpFill.setPosition(enemy.x * 48 + 24, enemy.y * 48 - 1);
|
||||
}
|
||||
}
|
||||
|
||||
enemy.lastMoveTime = time;
|
||||
}
|
||||
|
||||
// Attack player if very close
|
||||
if (dist < 1.5 && time > enemy.lastAttackTime + 2000) {
|
||||
this.attackPlayer(enemy);
|
||||
enemy.lastAttackTime = time;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enemy attacks player
|
||||
attackPlayer(enemy) {
|
||||
if (this.scene.player) {
|
||||
console.log(`👹 ${enemy.type} attacks for ${enemy.damage} damage!`);
|
||||
// TODO: Integrate with player health system
|
||||
|
||||
// Visual feedback
|
||||
if (this.scene.cameras) {
|
||||
this.scene.cameras.main.shake(200, 0.005);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Player attacks enemy
|
||||
damageEnemy(enemy, damage) {
|
||||
if (!enemy.alive) return;
|
||||
|
||||
enemy.hp -= damage;
|
||||
|
||||
// Update HP bar
|
||||
if (enemy.hpFill) {
|
||||
const hpPercent = Math.max(0, enemy.hp / enemy.maxHp);
|
||||
enemy.hpFill.setScale(hpPercent, 1);
|
||||
}
|
||||
|
||||
console.log(`⚔️ ${enemy.type} takes ${damage} damage! (${enemy.hp}/${enemy.maxHp} HP)`);
|
||||
|
||||
// Death
|
||||
if (enemy.hp <= 0) {
|
||||
this.killEnemy(enemy);
|
||||
}
|
||||
}
|
||||
|
||||
// Kill enemy and drop loot
|
||||
killEnemy(enemy) {
|
||||
enemy.alive = false;
|
||||
|
||||
console.log(`💀 ${enemy.type} died!`);
|
||||
|
||||
// Drop loot
|
||||
if (this.scene.inventorySystem && enemy.loot.length > 0) {
|
||||
const lootItem = enemy.loot[Math.floor(Math.random() * enemy.loot.length)];
|
||||
const amount = Math.floor(Math.random() * 3) + 1;
|
||||
|
||||
this.scene.inventorySystem.addItem(lootItem, amount);
|
||||
console.log(` 💰 Dropped: ${amount}x ${lootItem}`);
|
||||
}
|
||||
|
||||
// Destroy sprite
|
||||
if (enemy.sprite) {
|
||||
enemy.sprite.destroy();
|
||||
if (enemy.hpBar) enemy.hpBar.destroy();
|
||||
if (enemy.hpFill) enemy.hpFill.destroy();
|
||||
enemy.sprite = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Find nearest enemy to point
|
||||
findNearestEnemy(x, y, maxDistance = 2) {
|
||||
let nearest = null;
|
||||
let minDist = maxDistance;
|
||||
|
||||
for (const enemy of this.enemies) {
|
||||
if (!enemy.alive) continue;
|
||||
|
||||
const dist = Math.sqrt((enemy.x - x) ** 2 + (enemy.y - y) ** 2);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
nearest = enemy;
|
||||
}
|
||||
}
|
||||
|
||||
return nearest;
|
||||
}
|
||||
|
||||
// Get statistics
|
||||
getStats() {
|
||||
const alive = this.enemies.filter(e => e.alive).length;
|
||||
const byBiome = {};
|
||||
|
||||
for (const enemy of this.enemies) {
|
||||
if (!enemy.alive) continue;
|
||||
byBiome[enemy.biome] = (byBiome[enemy.biome] || 0) + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
totalEnemies: this.enemies.length,
|
||||
alive,
|
||||
dead: this.enemies.length - alive,
|
||||
byBiome
|
||||
};
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.enemies.forEach(enemy => {
|
||||
if (enemy.sprite) enemy.sprite.destroy();
|
||||
if (enemy.hpBar) enemy.hpBar.destroy();
|
||||
if (enemy.hpFill) enemy.hpFill.destroy();
|
||||
});
|
||||
this.enemies = [];
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,11 @@ class Flat2DTerrainSystem {
|
||||
this.chunkManager = null; // Will be set by GameScene
|
||||
this.chunkSize = 50; // Chunk size for rendering (matches ChunkManager)
|
||||
|
||||
// 🏛️ PHASE 28 SESSION 6: Structure support
|
||||
this.structureSystem = null; // Will be set by GameScene
|
||||
this.riverSystem = null; // Will be set by GameScene
|
||||
this.lakeSystem = null; // Will be set by GameScene
|
||||
|
||||
console.log('🎨 Flat2DTerrainSystem initialized (500x500 world)');
|
||||
}
|
||||
|
||||
@@ -442,6 +447,70 @@ class Flat2DTerrainSystem {
|
||||
continue; // Skip to next tile
|
||||
}
|
||||
|
||||
// 🏛️ PHASE 28 SESSION 6: Check for ROADS
|
||||
if (this.structureSystem && this.structureSystem.isRoad(x, y)) {
|
||||
// Get biome-specific road color
|
||||
const baseColor = (biome === 'desert') ? 0xcda869 :
|
||||
(biome === 'mountain') ? 0x9090a0 :
|
||||
(biome === 'swamp') ? 0x5a4a3d :
|
||||
0x8B7355; // Brown dirt road
|
||||
|
||||
const roadRect = this.scene.add.rectangle(worldX, worldY, size, size, baseColor, 1.0);
|
||||
roadRect.setOrigin(0, 0);
|
||||
roadRect.setDepth(1.5); // Above ground, below decorations
|
||||
chunk.sprites.push(roadRect);
|
||||
|
||||
// Add some variation to road texture
|
||||
if (Math.random() < 0.3) {
|
||||
const detail = this.scene.add.rectangle(
|
||||
worldX + Math.random() * size,
|
||||
worldY + Math.random() * size,
|
||||
size * 0.3,
|
||||
size * 0.3,
|
||||
baseColor - 0x202020,
|
||||
0.5
|
||||
);
|
||||
detail.setOrigin(0, 0);
|
||||
detail.setDepth(1.5);
|
||||
chunk.sprites.push(detail);
|
||||
}
|
||||
|
||||
// Roads block biome features
|
||||
continue;
|
||||
}
|
||||
|
||||
// 🏛️ PHASE 28 SESSION 6: Check for STRUCTURES
|
||||
const structureData = this.structureSystem ? this.structureSystem.getStructure(x, y) : null;
|
||||
if (structureData) {
|
||||
// Structure exists here - render visual marker
|
||||
if (structureData.type === 'landmark') {
|
||||
// Landmark marker (large, special)
|
||||
const landmarkMarker = this.scene.add.rectangle(worldX, worldY, size, size, 0xFFD700, 0.7);
|
||||
landmarkMarker.setOrigin(0, 0);
|
||||
landmarkMarker.setDepth(5);
|
||||
chunk.sprites.push(landmarkMarker);
|
||||
|
||||
// Add a star symbol for landmarks (simple)
|
||||
const star = this.scene.add.text(worldX + size / 2, worldY + size / 2, '★', {
|
||||
fontSize: '20px',
|
||||
color: '#ffffff'
|
||||
});
|
||||
star.setOrigin(0.5, 0.5);
|
||||
star.setDepth(6);
|
||||
chunk.sprites.push(star);
|
||||
} else if (structureData.type === 'structure') {
|
||||
// Regular structure marker
|
||||
const structureColor = this.getStructureColor(structureData.structureType);
|
||||
const structureMarker = this.scene.add.rectangle(worldX, worldY, size, size, structureColor, 0.8);
|
||||
structureMarker.setOrigin(0, 0);
|
||||
structureMarker.setDepth(4);
|
||||
chunk.sprites.push(structureMarker);
|
||||
}
|
||||
|
||||
// Structures block biome features
|
||||
continue;
|
||||
}
|
||||
|
||||
// 🌈 Apply mixed features for transitions
|
||||
let features = [];
|
||||
|
||||
@@ -610,6 +679,45 @@ class Flat2DTerrainSystem {
|
||||
return graphics;
|
||||
}
|
||||
|
||||
// 🏛️ PHASE 28 SESSION 6: Get color for structure type
|
||||
getStructureColor(structureType) {
|
||||
const colors = {
|
||||
// Grassland structures
|
||||
'farm': 0x8B4513,
|
||||
'house': 0xA0522D,
|
||||
'barn': 0x654321,
|
||||
'windmill': 0xD2691E,
|
||||
'well': 0x708090,
|
||||
|
||||
// Forest structures
|
||||
'cabin': 0x8B4513,
|
||||
'ruins': 0x696969,
|
||||
'treehouse': 0x8B7355,
|
||||
'camp': 0x8B4513,
|
||||
'shrine': 0x9370DB,
|
||||
|
||||
// Desert structures
|
||||
'pyramid': 0xDAA520,
|
||||
'oasis_camp': 0x8B7355,
|
||||
'tomb': 0xCD853F,
|
||||
'pillar': 0xD2B48C,
|
||||
|
||||
// Mountain structures
|
||||
'mine': 0x2F4F4F,
|
||||
'cave': 0x363636,
|
||||
'tower': 0x708090,
|
||||
'altar': 0x9370DB,
|
||||
|
||||
// Swamp structures
|
||||
'hut': 0x556B2F,
|
||||
'totem': 0x8B4513,
|
||||
'bog_shrine': 0x6B8E23,
|
||||
'abandoned_dock': 0x654321
|
||||
};
|
||||
|
||||
return colors[structureType] || 0x808080; // Default gray
|
||||
}
|
||||
|
||||
getTileTexture(tileType) {
|
||||
const types = Map2DData.tileTypes;
|
||||
|
||||
|
||||
353
src/systems/LandmarkQuestSystem.js
Normal file
353
src/systems/LandmarkQuestSystem.js
Normal file
@@ -0,0 +1,353 @@
|
||||
/**
|
||||
* 📜 LANDMARK QUEST SYSTEM
|
||||
* Quest system integrated with landmarks and structures
|
||||
* - Main quest: Visit all 5 landmarks
|
||||
* - Side quests from NPCs
|
||||
* - Exploration rewards
|
||||
* - Quest tracking
|
||||
*/
|
||||
|
||||
class LandmarkQuestSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// Active quests
|
||||
this.activeQuests = [];
|
||||
|
||||
// Completed quests
|
||||
this.completedQuests = [];
|
||||
|
||||
// Quest definitions
|
||||
this.quests = {
|
||||
// Main quest chain
|
||||
'main_explore_landmarks': {
|
||||
id: 'main_explore_landmarks',
|
||||
name: 'The Five Landmarks',
|
||||
description: 'Discover all 5 legendary landmarks across the world',
|
||||
type: 'main',
|
||||
objectives: [
|
||||
{ type: 'visit_landmark', target: 'ancient_temple', name: 'Visit Ancient Temple', completed: false },
|
||||
{ type: 'visit_landmark', target: 'great_pyramid', name: 'Visit Great Pyramid', completed: false },
|
||||
{ type: 'visit_landmark', target: 'mountain_peak', name: 'Reach Mountain Peak', completed: false },
|
||||
{ type: 'visit_landmark', target: 'abandoned_city', name: 'Explore Abandoned City', completed: false },
|
||||
{ type: 'visit_landmark', target: 'dragon_skeleton', name: 'Find Dragon Skeleton', completed: false }
|
||||
],
|
||||
rewards: {
|
||||
gold: 5000,
|
||||
xp: 10000,
|
||||
item: 'legendary_compass'
|
||||
}
|
||||
},
|
||||
|
||||
// Biome exploration quests
|
||||
'explore_grassland': {
|
||||
id: 'explore_grassland',
|
||||
name: 'Grassland Explorer',
|
||||
description: 'Visit 10 structures in Grassland biome',
|
||||
type: 'side',
|
||||
objectives: [
|
||||
{ type: 'visit_structures', biome: 'Grassland', count: 0, target: 10, completed: false }
|
||||
],
|
||||
rewards: { gold: 500, xp: 1000 }
|
||||
},
|
||||
|
||||
'explore_forest': {
|
||||
id: 'explore_forest',
|
||||
name: 'Forest Wanderer',
|
||||
description: 'Visit 10 structures in Forest biome',
|
||||
type: 'side',
|
||||
objectives: [
|
||||
{ type: 'visit_structures', biome: 'Forest', count: 0, target: 10, completed: false }
|
||||
],
|
||||
rewards: { gold: 500, xp: 1000 }
|
||||
},
|
||||
|
||||
'explore_desert': {
|
||||
id: 'explore_desert',
|
||||
name: 'Desert Nomad',
|
||||
description: 'Visit 5 structures in Desert biome',
|
||||
type: 'side',
|
||||
objectives: [
|
||||
{ type: 'visit_structures', biome: 'Desert', count: 0, target: 5, completed: false }
|
||||
],
|
||||
rewards: { gold: 750, xp: 1500 }
|
||||
},
|
||||
|
||||
'explore_mountain': {
|
||||
id: 'explore_mountain',
|
||||
name: 'Mountain Climber',
|
||||
description: 'Visit 5 structures in Mountain biome',
|
||||
type: 'side',
|
||||
objectives: [
|
||||
{ type: 'visit_structures', biome: 'Mountain', count: 0, target: 5, completed: false }
|
||||
],
|
||||
rewards: { gold: 750, xp: 1500 }
|
||||
},
|
||||
|
||||
'explore_swamp': {
|
||||
id: 'explore_swamp',
|
||||
name: 'Swamp Explorer',
|
||||
description: 'Visit 5 structures in Swamp biome',
|
||||
type: 'side',
|
||||
objectives: [
|
||||
{ type: 'visit_structures', biome: 'Swamp', count: 0, target: 5, completed: false }
|
||||
],
|
||||
rewards: { gold: 750, xp: 1500 }
|
||||
},
|
||||
|
||||
// Enemy quests
|
||||
'slay_enemies': {
|
||||
id: 'slay_enemies',
|
||||
name: 'Monster Hunter',
|
||||
description: 'Defeat 20 enemies',
|
||||
type: 'side',
|
||||
objectives: [
|
||||
{ type: 'kill_enemies', count: 0, target: 20, completed: false }
|
||||
],
|
||||
rewards: { gold: 1000, xp: 2000 }
|
||||
},
|
||||
|
||||
// Collection quest
|
||||
'treasure_hunter': {
|
||||
id: 'treasure_hunter',
|
||||
name: 'Treasure Hunter',
|
||||
description: 'Open 30 chests',
|
||||
type: 'side',
|
||||
objectives: [
|
||||
{ type: 'open_chests', count: 0, target: 30, completed: false }
|
||||
],
|
||||
rewards: { gold: 2000, xp: 3000 }
|
||||
}
|
||||
};
|
||||
|
||||
// Tracking
|
||||
this.landmarksVisited = [];
|
||||
this.structuresVisitedByBiome = {};
|
||||
|
||||
console.log('📜 LandmarkQuestSystem initialized');
|
||||
}
|
||||
|
||||
// Start main quest automatically
|
||||
startMainQuest() {
|
||||
this.activeQuests.push('main_explore_landmarks');
|
||||
console.log('📜 Main quest started: The Five Landmarks');
|
||||
this.showQuestNotification('New Quest!', 'The Five Landmarks', 'Discover all 5 legendary landmarks');
|
||||
}
|
||||
|
||||
// Start exploration quests
|
||||
startExplorationQuests() {
|
||||
this.activeQuests.push('explore_grassland');
|
||||
this.activeQuests.push('explore_forest');
|
||||
this.activeQuests.push('explore_desert');
|
||||
this.activeQuests.push('explore_mountain');
|
||||
this.activeQuests.push('explore_swamp');
|
||||
console.log('📜 Started 5 biome exploration quests');
|
||||
}
|
||||
|
||||
// Visit landmark (called from player)
|
||||
visitLandmark(landmarkType) {
|
||||
if (this.landmarksVisited.includes(landmarkType)) return;
|
||||
|
||||
this.landmarksVisited.push(landmarkType);
|
||||
console.log(`🗿 Visited landmark: ${landmarkType}`);
|
||||
|
||||
// Update main quest
|
||||
const mainQuest = this.quests['main_explore_landmarks'];
|
||||
if (this.activeQuests.includes('main_explore_landmarks')) {
|
||||
for (const obj of mainQuest.objectives) {
|
||||
if (obj.target === landmarkType) {
|
||||
obj.completed = true;
|
||||
this.showQuestNotification('Objective Complete!', obj.name, '+2000 XP');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if all objectives complete
|
||||
if (mainQuest.objectives.every(o => o.completed)) {
|
||||
this.completeQuest('main_explore_landmarks');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Visit structure (for biome quests)
|
||||
visitStructure(x, y, biome) {
|
||||
const key = `${x},${y}`;
|
||||
|
||||
if (!this.structuresVisitedByBiome[biome]) {
|
||||
this.structuresVisitedByBiome[biome] = [];
|
||||
}
|
||||
|
||||
if (this.structuresVisitedByBiome[biome].includes(key)) return;
|
||||
|
||||
this.structuresVisitedByBiome[biome].push(key);
|
||||
|
||||
// Update biome exploration quests
|
||||
const questId = `explore_${biome.toLowerCase()}`;
|
||||
if (this.activeQuests.includes(questId)) {
|
||||
const quest = this.quests[questId];
|
||||
for (const obj of quest.objectives) {
|
||||
if (obj.biome === biome) {
|
||||
obj.count++;
|
||||
if (obj.count >= obj.target && !obj.completed) {
|
||||
obj.completed = true;
|
||||
this.completeQuest(questId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Complete quest and give rewards
|
||||
completeQuest(questId) {
|
||||
const quest = this.quests[questId];
|
||||
if (!quest) return;
|
||||
|
||||
console.log(`✅ Quest completed: ${quest.name}`);
|
||||
|
||||
// Remove from active
|
||||
const index = this.activeQuests.indexOf(questId);
|
||||
if (index > -1) {
|
||||
this.activeQuests.splice(index, 1);
|
||||
}
|
||||
|
||||
// Add to completed
|
||||
this.completedQuests.push(questId);
|
||||
|
||||
// Give rewards
|
||||
if (this.scene.inventorySystem && quest.rewards) {
|
||||
if (quest.rewards.gold) {
|
||||
this.scene.inventorySystem.gold += quest.rewards.gold;
|
||||
}
|
||||
if (quest.rewards.item) {
|
||||
this.scene.inventorySystem.addItem(quest.rewards.item, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Show completion
|
||||
this.showQuestCompleteNotification(quest);
|
||||
|
||||
// Play sound
|
||||
if (this.scene.soundManager) {
|
||||
this.scene.soundManager.beepPickup();
|
||||
}
|
||||
}
|
||||
|
||||
// Show quest notification
|
||||
showQuestNotification(title, questName, description) {
|
||||
const notification = this.scene.add.text(
|
||||
this.scene.cameras.main.centerX,
|
||||
100,
|
||||
`${title}\n${questName}\n${description}`,
|
||||
{
|
||||
fontSize: '24px',
|
||||
color: '#FFD700',
|
||||
backgroundColor: '#000000',
|
||||
padding: { x: 30, y: 20 },
|
||||
align: 'center'
|
||||
}
|
||||
);
|
||||
notification.setOrigin(0.5);
|
||||
notification.setScrollFactor(0);
|
||||
notification.setDepth(10001);
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: notification,
|
||||
alpha: 0,
|
||||
duration: 1000,
|
||||
delay: 3000,
|
||||
onComplete: () => notification.destroy()
|
||||
});
|
||||
}
|
||||
|
||||
// Show quest complete notification
|
||||
showQuestCompleteNotification(quest) {
|
||||
const rewardText = [];
|
||||
if (quest.rewards.gold) rewardText.push(`+${quest.rewards.gold} gold`);
|
||||
if (quest.rewards.xp) rewardText.push(`+${quest.rewards.xp} XP`);
|
||||
if (quest.rewards.item) rewardText.push(`+${quest.rewards.item}`);
|
||||
|
||||
const notification = this.scene.add.text(
|
||||
this.scene.cameras.main.centerX,
|
||||
this.scene.cameras.main.centerY,
|
||||
`🎉 QUEST COMPLETE! 🎉\n${quest.name}\n\nRewards:\n${rewardText.join('\n')}`,
|
||||
{
|
||||
fontSize: '32px',
|
||||
color: '#FFD700',
|
||||
backgroundColor: '#000000',
|
||||
padding: { x: 40, y: 30 },
|
||||
align: 'center'
|
||||
}
|
||||
);
|
||||
notification.setOrigin(0.5);
|
||||
notification.setScrollFactor(0);
|
||||
notification.setDepth(10002);
|
||||
|
||||
// Fireworks effect
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const particle = this.scene.add.circle(
|
||||
this.scene.cameras.main.centerX,
|
||||
this.scene.cameras.main.centerY,
|
||||
5,
|
||||
[0xFFD700, 0xFF69B4, 0x00BFFF][i % 3]
|
||||
);
|
||||
particle.setScrollFactor(0);
|
||||
particle.setDepth(10001);
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: particle,
|
||||
x: particle.x + (Math.random() - 0.5) * 400,
|
||||
y: particle.y + (Math.random() - 0.5) * 400,
|
||||
alpha: 0,
|
||||
duration: 2000,
|
||||
onComplete: () => particle.destroy()
|
||||
});
|
||||
}
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: notification,
|
||||
alpha: 0,
|
||||
duration: 1000,
|
||||
delay: 4000,
|
||||
onComplete: () => notification.destroy()
|
||||
});
|
||||
}
|
||||
|
||||
// Get active quests for UI
|
||||
getActiveQuests() {
|
||||
return this.activeQuests.map(id => this.quests[id]);
|
||||
}
|
||||
|
||||
// Get quest progress
|
||||
getQuestProgress(questId) {
|
||||
const quest = this.quests[questId];
|
||||
if (!quest) return null;
|
||||
|
||||
const completed = quest.objectives.filter(o => o.completed).length;
|
||||
const total = quest.objectives.length;
|
||||
|
||||
return { completed, total, objectives: quest.objectives };
|
||||
}
|
||||
|
||||
// Export/Import for save system
|
||||
exportData() {
|
||||
return {
|
||||
activeQuests: this.activeQuests,
|
||||
completedQuests: this.completedQuests,
|
||||
landmarksVisited: this.landmarksVisited,
|
||||
structuresVisitedByBiome: this.structuresVisitedByBiome
|
||||
};
|
||||
}
|
||||
|
||||
importData(data) {
|
||||
if (!data) return;
|
||||
this.activeQuests = data.activeQuests || [];
|
||||
this.completedQuests = data.completedQuests || [];
|
||||
this.landmarksVisited = data.landmarksVisited || [];
|
||||
this.structuresVisitedByBiome = data.structuresVisitedByBiome || {};
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.activeQuests = [];
|
||||
this.completedQuests = [];
|
||||
}
|
||||
}
|
||||
334
src/systems/MapRevealSystem.js
Normal file
334
src/systems/MapRevealSystem.js
Normal file
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* 🗺️ MAP REVEAL SYSTEM
|
||||
* Fog of war system that reveals map as player explores
|
||||
* - Reveals tiles around player
|
||||
* - Persistent (saves discovered areas)
|
||||
* - Minimap integration
|
||||
* - Full map view (M key)
|
||||
*/
|
||||
|
||||
class MapRevealSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// Revealed tiles (500x500 grid)
|
||||
this.revealed = Array(500).fill(null).map(() => Array(500).fill(false));
|
||||
|
||||
// Reveal radius around player
|
||||
this.revealRadius = 15; // tiles
|
||||
|
||||
// Map UI elements
|
||||
this.mapOpen = false;
|
||||
this.mapContainer = null;
|
||||
|
||||
// Minimap
|
||||
this.minimap = null;
|
||||
this.minimapSize = 200;
|
||||
this.minimapScale = 2; // pixels per tile on minimap
|
||||
|
||||
console.log('🗺️ MapRevealSystem initialized');
|
||||
}
|
||||
|
||||
// Reveal tiles around player
|
||||
revealArea(playerX, playerY) {
|
||||
const gridX = Math.floor(playerX);
|
||||
const gridY = Math.floor(playerY);
|
||||
|
||||
let newTilesRevealed = 0;
|
||||
|
||||
for (let dx = -this.revealRadius; dx <= this.revealRadius; dx++) {
|
||||
for (let dy = -this.revealRadius; dy <= this.revealRadius; dy++) {
|
||||
const x = gridX + dx;
|
||||
const y = gridY + dy;
|
||||
|
||||
// Check if within world bounds
|
||||
if (x < 0 || x >= 500 || y < 0 || y >= 500) continue;
|
||||
|
||||
// Check if within circular radius
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
if (dist > this.revealRadius) continue;
|
||||
|
||||
// Reveal tile
|
||||
if (!this.revealed[y][x]) {
|
||||
this.revealed[y][x] = true;
|
||||
newTilesRevealed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newTilesRevealed > 0) {
|
||||
// Update minimap
|
||||
this.updateMinimap();
|
||||
}
|
||||
}
|
||||
|
||||
// Create minimap (bottom-right corner)
|
||||
createMinimap() {
|
||||
if (this.minimap) return;
|
||||
|
||||
const size = this.minimapSize;
|
||||
const x = this.scene.cameras.main.width - size - 20;
|
||||
const y = this.scene.cameras.main.height - size - 20;
|
||||
|
||||
// Background
|
||||
this.minimapBg = this.scene.add.rectangle(x, y, size, size, 0x000000, 0.7);
|
||||
this.minimapBg.setOrigin(0);
|
||||
this.minimapBg.setScrollFactor(0);
|
||||
this.minimapBg.setDepth(9000);
|
||||
|
||||
// Border
|
||||
this.minimapBorder = this.scene.add.rectangle(x, y, size, size);
|
||||
this.minimapBorder.setOrigin(0);
|
||||
this.minimapBorder.setStrokeStyle(2, 0xFFFFFF);
|
||||
this.minimapBorder.setScrollFactor(0);
|
||||
this.minimapBorder.setDepth(9001);
|
||||
|
||||
// Canvas for map rendering
|
||||
this.minimapTexture = this.scene.add.renderTexture(x, y, size, size);
|
||||
this.minimapTexture.setOrigin(0);
|
||||
this.minimapTexture.setScrollFactor(0);
|
||||
this.minimapTexture.setDepth(9002);
|
||||
|
||||
// Label
|
||||
this.minimapLabel = this.scene.add.text(x + size / 2, y - 15, 'Map (M)', {
|
||||
fontSize: '14px',
|
||||
color: '#ffffff',
|
||||
backgroundColor: '#000000',
|
||||
padding: { x: 5, y: 2 }
|
||||
});
|
||||
this.minimapLabel.setOrigin(0.5);
|
||||
this.minimapLabel.setScrollFactor(0);
|
||||
this.minimapLabel.setDepth(9003);
|
||||
|
||||
this.minimap = {
|
||||
bg: this.minimapBg,
|
||||
border: this.minimapBorder,
|
||||
texture: this.minimapTexture,
|
||||
label: this.minimapLabel,
|
||||
x, y, size
|
||||
};
|
||||
|
||||
this.updateMinimap();
|
||||
}
|
||||
|
||||
// Update minimap rendering
|
||||
updateMinimap() {
|
||||
if (!this.minimap) return;
|
||||
|
||||
const player = this.scene.player;
|
||||
if (!player) return;
|
||||
|
||||
const playerX = Math.floor(player.gridX);
|
||||
const playerY = Math.floor(player.gridY);
|
||||
|
||||
// Clear
|
||||
this.minimap.texture.clear();
|
||||
|
||||
// Calculate view area (centered on player)
|
||||
const viewSize = Math.floor(this.minimap.size / this.minimapScale);
|
||||
const startX = Math.max(0, playerX - viewSize / 2);
|
||||
const startY = Math.max(0, playerY - viewSize / 2);
|
||||
const endX = Math.min(500, startX + viewSize);
|
||||
const endY = Math.min(500, startY + viewSize);
|
||||
|
||||
// Draw revealed tiles
|
||||
for (let y = startY; y < endY; y++) {
|
||||
for (let x = startX; x < endX; x++) {
|
||||
if (!this.revealed[y] || !this.revealed[y][x]) continue;
|
||||
|
||||
const screenX = (x - startX) * this.minimapScale;
|
||||
const screenY = (y - startY) * this.minimapScale;
|
||||
|
||||
// Get biome color
|
||||
const biome = this.scene.biomeSystem ? this.scene.biomeSystem.getBiome(x, y) : 'Grassland';
|
||||
const color = this.getBiomeMinimapColor(biome);
|
||||
|
||||
this.minimap.texture.fill(color, 1, screenX, screenY, this.minimapScale, this.minimapScale);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw player position
|
||||
const playerScreenX = (playerX - startX) * this.minimapScale;
|
||||
const playerScreenY = (playerY - startY) * this.minimapScale;
|
||||
this.minimap.texture.fill(0xFFFF00, 1, playerScreenX - 2, playerScreenY - 2, 4, 4);
|
||||
}
|
||||
|
||||
// Get minimap color for biome
|
||||
getBiomeMinimapColor(biome) {
|
||||
const colors = {
|
||||
'Grassland': 0x3CB371,
|
||||
'Forest': 0x2d5016,
|
||||
'Desert': 0xd4c4a1,
|
||||
'Mountain': 0x808080,
|
||||
'Swamp': 0x3d5a3d
|
||||
};
|
||||
return colors[biome] || 0x3CB371;
|
||||
}
|
||||
|
||||
// Toggle full map view (M key)
|
||||
toggleFullMap() {
|
||||
if (this.mapOpen) {
|
||||
this.closeFullMap();
|
||||
} else {
|
||||
this.openFullMap();
|
||||
}
|
||||
}
|
||||
|
||||
// Open full map
|
||||
openFullMap() {
|
||||
if (this.mapOpen) return;
|
||||
|
||||
this.mapOpen = true;
|
||||
|
||||
// Semi-transparent background
|
||||
this.fullMapBg = this.scene.add.rectangle(
|
||||
this.scene.cameras.main.centerX,
|
||||
this.scene.cameras.main.centerY,
|
||||
this.scene.cameras.main.width,
|
||||
this.scene.cameras.main.height,
|
||||
0x000000,
|
||||
0.9
|
||||
);
|
||||
this.fullMapBg.setScrollFactor(0);
|
||||
this.fullMapBg.setDepth(15000);
|
||||
|
||||
// Map title
|
||||
this.fullMapTitle = this.scene.add.text(
|
||||
this.scene.cameras.main.centerX,
|
||||
50,
|
||||
'World Map (Press M to close)',
|
||||
{
|
||||
fontSize: '32px',
|
||||
color: '#FFD700',
|
||||
backgroundColor: '#000000',
|
||||
padding: { x: 20, y: 10 }
|
||||
}
|
||||
);
|
||||
this.fullMapTitle.setOrigin(0.5);
|
||||
this.fullMapTitle.setScrollFactor(0);
|
||||
this.fullMapTitle.setDepth(15001);
|
||||
|
||||
// Render full map
|
||||
const mapSize = 600;
|
||||
const mapX = this.scene.cameras.main.centerX - mapSize / 2;
|
||||
const mapY = this.scene.cameras.main.centerY - mapSize / 2;
|
||||
|
||||
this.fullMapTexture = this.scene.add.renderTexture(mapX, mapY, mapSize, mapSize);
|
||||
this.fullMapTexture.setScrollFactor(0);
|
||||
this.fullMapTexture.setDepth(15002);
|
||||
|
||||
// Draw entire world
|
||||
const scale = mapSize / 500;
|
||||
for (let y = 0; y < 500; y += 5) {
|
||||
for (let x = 0; x < 500; x += 5) {
|
||||
if (!this.revealed[y] || !this.revealed[y][x]) continue;
|
||||
|
||||
const biome = this.scene.biomeSystem ? this.scene.biomeSystem.getBiome(x, y) : 'Grassland';
|
||||
const color = this.getBiomeMinimapColor(biome);
|
||||
|
||||
this.fullMapTexture.fill(color, 1, x * scale, y * scale, scale * 5, scale * 5);
|
||||
}
|
||||
}
|
||||
|
||||
// Player position
|
||||
if (this.scene.player) {
|
||||
const px = Math.floor(this.scene.player.gridX) * scale;
|
||||
const py = Math.floor(this.scene.player.gridY) * scale;
|
||||
this.fullMapTexture.fill(0xFFFF00, 1, px - 3, py - 3, 6, 6);
|
||||
}
|
||||
|
||||
// Stats
|
||||
const exploredTiles = this.revealed.flat().filter(r => r).length;
|
||||
const totalTiles = 500 * 500;
|
||||
const percent = ((exploredTiles / totalTiles) * 100).toFixed(1);
|
||||
|
||||
this.fullMapStats = this.scene.add.text(
|
||||
this.scene.cameras.main.centerX,
|
||||
mapY + mapSize + 30,
|
||||
`Explored: ${exploredTiles} / ${totalTiles} tiles (${percent}%)`,
|
||||
{
|
||||
fontSize: '20px',
|
||||
color: '#ffffff',
|
||||
backgroundColor: '#000000',
|
||||
padding: { x: 15, y: 8 }
|
||||
}
|
||||
);
|
||||
this.fullMapStats.setOrigin(0.5);
|
||||
this.fullMapStats.setScrollFactor(0);
|
||||
this.fullMapStats.setDepth(15003);
|
||||
}
|
||||
|
||||
// Close full map
|
||||
closeFullMap() {
|
||||
if (!this.mapOpen) return;
|
||||
|
||||
this.mapOpen = false;
|
||||
|
||||
if (this.fullMapBg) {
|
||||
this.fullMapBg.destroy();
|
||||
this.fullMapTitle.destroy();
|
||||
this.fullMapTexture.destroy();
|
||||
this.fullMapStats.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// Update (called every frame)
|
||||
update() {
|
||||
if (this.scene.player) {
|
||||
this.revealArea(this.scene.player.gridX, this.scene.player.gridY);
|
||||
}
|
||||
}
|
||||
|
||||
// Get exploration statistics
|
||||
getStats() {
|
||||
const exploredTiles = this.revealed.flat().filter(r => r).length;
|
||||
const totalTiles = 500 * 500;
|
||||
const percent = ((exploredTiles / totalTiles) * 100).toFixed(2);
|
||||
|
||||
return {
|
||||
explored: exploredTiles,
|
||||
total: totalTiles,
|
||||
percent: parseFloat(percent)
|
||||
};
|
||||
}
|
||||
|
||||
// Export/Import for save system
|
||||
exportData() {
|
||||
// Convert 2D array to compressed format
|
||||
const compressed = [];
|
||||
for (let y = 0; y < 500; y++) {
|
||||
for (let x = 0; x < 500; x++) {
|
||||
if (this.revealed[y][x]) {
|
||||
compressed.push(`${x},${y}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { revealed: compressed };
|
||||
}
|
||||
|
||||
importData(data) {
|
||||
if (!data || !data.revealed) return;
|
||||
|
||||
// Clear current
|
||||
this.revealed = Array(500).fill(null).map(() => Array(500).fill(false));
|
||||
|
||||
// Decompress
|
||||
for (const key of data.revealed) {
|
||||
const [x, y] = key.split(',').map(Number);
|
||||
if (x >= 0 && x < 500 && y >= 0 && y < 500) {
|
||||
this.revealed[y][x] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.closeFullMap();
|
||||
if (this.minimap) {
|
||||
this.minimap.bg.destroy();
|
||||
this.minimap.border.destroy();
|
||||
this.minimap.texture.destroy();
|
||||
this.minimap.label.destroy();
|
||||
this.minimap = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
337
src/systems/NPCPopulationSystem.js
Normal file
337
src/systems/NPCPopulationSystem.js
Normal file
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* 👥 NPC POPULATION SYSTEM
|
||||
* Spawns and manages NPCs in structures across the world
|
||||
* - Biome-specific NPCs
|
||||
* - Dialog system
|
||||
* - Trading functionality
|
||||
* - Quest giving
|
||||
*/
|
||||
|
||||
class NPCPopulationSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// All NPCs in the world
|
||||
this.npcs = [];
|
||||
|
||||
// NPC types per biome
|
||||
this.npcTypes = {
|
||||
'Grassland': ['farmer', 'blacksmith', 'merchant', 'guard'],
|
||||
'Forest': ['hunter', 'herbalist', 'ranger', 'druid'],
|
||||
'Desert': ['nomad', 'treasure_hunter', 'merchant', 'archaeologist'],
|
||||
'Mountain': ['miner', 'dwarf', 'geologist', 'mountaineer'],
|
||||
'Swamp': ['witch', 'alchemist', 'hermit', 'shaman']
|
||||
};
|
||||
|
||||
// Dialog templates
|
||||
this.dialogs = {
|
||||
'farmer': [
|
||||
"Welcome to my farm! Need any seeds?",
|
||||
"The harvest this year is bountiful!",
|
||||
"I sell the best wheat in the land!"
|
||||
],
|
||||
'merchant': [
|
||||
"Looking to buy or sell? I've got the best deals!",
|
||||
"Welcome, traveler! Check out my wares!",
|
||||
"Gold for goods, goods for gold!"
|
||||
],
|
||||
'hunter': [
|
||||
"The forest is full of game. Happy hunting!",
|
||||
"Watch out for the wolves at night.",
|
||||
"I can sell you some arrows if you need them."
|
||||
],
|
||||
'nomad': [
|
||||
"The desert holds many secrets...",
|
||||
"Water is more valuable than gold here.",
|
||||
"I've traveled far and wide, seen many things."
|
||||
],
|
||||
'miner': [
|
||||
"These mountains are rich with ore!",
|
||||
"I can sell you some iron if you need it.",
|
||||
"Watch your step in those caves!"
|
||||
],
|
||||
'witch': [
|
||||
"Potions and hexes, what do you need?",
|
||||
"The swamp holds ancient magic...",
|
||||
"I can brew you something special."
|
||||
]
|
||||
};
|
||||
|
||||
// Trade goods per NPC type
|
||||
this.tradeGoods = {
|
||||
'farmer': [
|
||||
{ item: 'wheat_seeds', price: 10, stock: 50 },
|
||||
{ item: 'wheat', price: 5, stock: 100 },
|
||||
{ item: 'bread', price: 15, stock: 20 }
|
||||
],
|
||||
'merchant': [
|
||||
{ item: 'wood', price: 8, stock: 200 },
|
||||
{ item: 'stone', price: 6, stock: 150 },
|
||||
{ item: 'iron_ore', price: 20, stock: 50 }
|
||||
],
|
||||
'blacksmith': [
|
||||
{ item: 'iron_sword', price: 150, stock: 5 },
|
||||
{ item: 'iron_pickaxe', price: 100, stock: 10 },
|
||||
{ item: 'iron_axe', price: 120, stock: 8 }
|
||||
],
|
||||
'hunter': [
|
||||
{ item: 'arrow', price: 5, stock: 100 },
|
||||
{ item: 'bow', price: 80, stock: 3 },
|
||||
{ item: 'meat', price: 20, stock: 30 }
|
||||
]
|
||||
};
|
||||
|
||||
console.log('👥 NPCPopulationSystem initialized');
|
||||
}
|
||||
|
||||
// Populate structures with NPCs
|
||||
populateStructures(structureSystem) {
|
||||
if (!structureSystem) return;
|
||||
|
||||
let npcsSpawned = 0;
|
||||
|
||||
// Spawn NPCs in structures (30% chance)
|
||||
for (const structure of structureSystem.structures) {
|
||||
if (Math.random() < 0.3) {
|
||||
const npcType = this.selectNPCType(structure.biome);
|
||||
this.spawnNPC(structure.x, structure.y, npcType, structure.biome);
|
||||
npcsSpawned++;
|
||||
}
|
||||
}
|
||||
|
||||
// Always spawn special NPCs at landmarks
|
||||
for (const landmark of structureSystem.landmarks) {
|
||||
this.spawnNPC(landmark.x, landmark.y, 'quest_giver', landmark.type, true);
|
||||
npcsSpawned++;
|
||||
}
|
||||
|
||||
console.log(`✅ Spawned ${npcsSpawned} NPCs in structures`);
|
||||
}
|
||||
|
||||
// Select NPC type for biome
|
||||
selectNPCType(biome) {
|
||||
const types = this.npcTypes[biome] || this.npcTypes['Grassland'];
|
||||
return types[Math.floor(Math.random() * types.length)];
|
||||
}
|
||||
|
||||
// Spawn single NPC
|
||||
spawnNPC(x, y, type, biome, isQuestGiver = false) {
|
||||
const npc = {
|
||||
x,
|
||||
y,
|
||||
type,
|
||||
biome,
|
||||
isQuestGiver,
|
||||
dialogIndex: 0,
|
||||
hasQuest: isQuestGiver,
|
||||
questCompleted: false,
|
||||
sprite: null
|
||||
};
|
||||
|
||||
this.npcs.push(npc);
|
||||
return npc;
|
||||
}
|
||||
|
||||
// Create NPC sprite (called when chunk loads)
|
||||
createNPCSprite(npc, chunk) {
|
||||
if (npc.sprite) return; // Already has sprite
|
||||
|
||||
const worldX = npc.x * 48 + 24;
|
||||
const worldY = npc.y * 48 + 24;
|
||||
|
||||
// Create simple circle sprite for NPC
|
||||
const color = this.getNPCColor(npc.type);
|
||||
const sprite = this.scene.add.circle(worldX, worldY, 12, color);
|
||||
sprite.setDepth(10 + worldY);
|
||||
|
||||
// Add name label
|
||||
const label = this.scene.add.text(worldX, worldY - 20, npc.type, {
|
||||
fontSize: '12px',
|
||||
color: '#ffffff',
|
||||
backgroundColor: '#000000',
|
||||
padding: { x: 4, y: 2 }
|
||||
});
|
||||
label.setOrigin(0.5);
|
||||
label.setDepth(10 + worldY);
|
||||
|
||||
// Quest marker for quest givers
|
||||
if (npc.isQuestGiver && !npc.questCompleted) {
|
||||
const questMarker = this.scene.add.text(worldX, worldY - 35, '!', {
|
||||
fontSize: '24px',
|
||||
color: '#FFD700',
|
||||
fontStyle: 'bold'
|
||||
});
|
||||
questMarker.setOrigin(0.5);
|
||||
questMarker.setDepth(10 + worldY);
|
||||
|
||||
npc.questMarker = questMarker;
|
||||
if (chunk) chunk.sprites.push(questMarker);
|
||||
}
|
||||
|
||||
npc.sprite = sprite;
|
||||
npc.label = label;
|
||||
|
||||
if (chunk) {
|
||||
chunk.sprites.push(sprite);
|
||||
chunk.sprites.push(label);
|
||||
}
|
||||
}
|
||||
|
||||
// Get NPC color
|
||||
getNPCColor(type) {
|
||||
const colors = {
|
||||
'farmer': 0x8B4513,
|
||||
'merchant': 0xDAA520,
|
||||
'blacksmith': 0x696969,
|
||||
'hunter': 0x228B22,
|
||||
'nomad': 0xD2691E,
|
||||
'miner': 0x708090,
|
||||
'witch': 0x9370DB,
|
||||
'quest_giver': 0xFFD700
|
||||
};
|
||||
return colors[type] || 0x808080;
|
||||
}
|
||||
|
||||
// Check for nearby NPCs
|
||||
update(playerX, playerY) {
|
||||
let nearestNPC = null;
|
||||
let minDist = 3;
|
||||
|
||||
for (const npc of this.npcs) {
|
||||
const dist = Math.sqrt((npc.x - playerX) ** 2 + (npc.y - playerY) ** 2);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
nearestNPC = npc;
|
||||
}
|
||||
}
|
||||
|
||||
if (nearestNPC && !this.currentNPC) {
|
||||
this.showTalkPrompt(nearestNPC);
|
||||
this.currentNPC = nearestNPC;
|
||||
} else if (!nearestNPC && this.currentNPC) {
|
||||
this.hideTalkPrompt();
|
||||
this.currentNPC = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Show prompt to talk
|
||||
showTalkPrompt(npc) {
|
||||
if (this.talkPrompt) return;
|
||||
|
||||
const promptText = npc.isQuestGiver
|
||||
? '💬 Press T to talk (QUEST AVAILABLE)'
|
||||
: `💬 Press T to talk to ${npc.type}`;
|
||||
|
||||
this.talkPrompt = this.scene.add.text(
|
||||
this.scene.cameras.main.centerX,
|
||||
this.scene.cameras.main.height - 100,
|
||||
promptText,
|
||||
{
|
||||
fontSize: '24px',
|
||||
color: npc.isQuestGiver ? '#FFD700' : '#ffffff',
|
||||
backgroundColor: '#000000',
|
||||
padding: { x: 20, y: 10 }
|
||||
}
|
||||
);
|
||||
this.talkPrompt.setOrigin(0.5);
|
||||
this.talkPrompt.setScrollFactor(0);
|
||||
this.talkPrompt.setDepth(10000);
|
||||
}
|
||||
|
||||
// Hide talk prompt
|
||||
hideTalkPrompt() {
|
||||
if (this.talkPrompt) {
|
||||
this.talkPrompt.destroy();
|
||||
this.talkPrompt = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Talk to NPC (T key)
|
||||
talkToNPC() {
|
||||
if (!this.currentNPC) return;
|
||||
|
||||
const npc = this.currentNPC;
|
||||
|
||||
// Get dialog
|
||||
const dialogs = this.dialogs[npc.type] || ["Hello, traveler!"];
|
||||
const dialog = dialogs[npc.dialogIndex % dialogs.length];
|
||||
npc.dialogIndex++;
|
||||
|
||||
// Show dialog box
|
||||
this.showDialog(npc, dialog);
|
||||
}
|
||||
|
||||
// Show dialog UI
|
||||
showDialog(npc, text) {
|
||||
// Close existing dialog
|
||||
if (this.dialogBox) {
|
||||
this.dialogBox.destroy();
|
||||
this.dialogText.destroy();
|
||||
this.dialogBox = null;
|
||||
}
|
||||
|
||||
// Create dialog box
|
||||
const centerX = this.scene.cameras.main.centerX;
|
||||
const centerY = this.scene.cameras.main.height - 150;
|
||||
|
||||
this.dialogBox = this.scene.add.rectangle(
|
||||
centerX, centerY,
|
||||
600, 120,
|
||||
0x000000, 0.8
|
||||
);
|
||||
this.dialogBox.setStrokeStyle(3, 0xFFFFFF);
|
||||
this.dialogBox.setScrollFactor(0);
|
||||
this.dialogBox.setDepth(10001);
|
||||
|
||||
this.dialogText = this.scene.add.text(
|
||||
centerX, centerY - 30,
|
||||
`${npc.type}:\n${text}`,
|
||||
{
|
||||
fontSize: '20px',
|
||||
color: '#ffffff',
|
||||
align: 'center',
|
||||
wordWrap: { width: 550 }
|
||||
}
|
||||
);
|
||||
this.dialogText.setOrigin(0.5, 0);
|
||||
this.dialogText.setScrollFactor(0);
|
||||
this.dialogText.setDepth(10002);
|
||||
|
||||
// Auto-close after 4 seconds
|
||||
this.scene.time.delayedCall(4000, () => {
|
||||
if (this.dialogBox) {
|
||||
this.dialogBox.destroy();
|
||||
this.dialogText.destroy();
|
||||
this.dialogBox = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get statistics
|
||||
getStats() {
|
||||
const byBiome = {};
|
||||
for (const npc of this.npcs) {
|
||||
byBiome[npc.biome] = (byBiome[npc.biome] || 0) + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
totalNPCs: this.npcs.length,
|
||||
questGivers: this.npcs.filter(n => n.isQuestGiver).length,
|
||||
byBiome
|
||||
};
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.hideTalkPrompt();
|
||||
if (this.dialogBox) {
|
||||
this.dialogBox.destroy();
|
||||
this.dialogText.destroy();
|
||||
}
|
||||
this.npcs.forEach(npc => {
|
||||
if (npc.sprite) npc.sprite.destroy();
|
||||
if (npc.label) npc.label.destroy();
|
||||
if (npc.questMarker) npc.questMarker.destroy();
|
||||
});
|
||||
this.npcs = [];
|
||||
}
|
||||
}
|
||||
371
src/systems/StructureInteractionSystem.js
Normal file
371
src/systems/StructureInteractionSystem.js
Normal file
@@ -0,0 +1,371 @@
|
||||
/**
|
||||
* 🏛️ STRUCTURE INTERACTION SYSTEM
|
||||
* Enables player interaction with structures in the world
|
||||
* - Enter buildings
|
||||
* - Loot chests
|
||||
* - Landmark treasures
|
||||
* - Lock/unlock mechanics
|
||||
*/
|
||||
|
||||
class StructureInteractionSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// Interaction markers
|
||||
this.interactionMarkers = [];
|
||||
|
||||
// Loot chests (generated per structure)
|
||||
this.chests = new Map(); // key: "x,y" → chest data
|
||||
|
||||
// Active interactions
|
||||
this.nearbyStructures = [];
|
||||
this.currentInteraction = null;
|
||||
|
||||
// Loot tables per biome
|
||||
this.lootTables = {
|
||||
'Grassland': [
|
||||
{ item: 'wheat_seeds', min: 5, max: 15, chance: 0.7 },
|
||||
{ item: 'wood', min: 10, max: 30, chance: 0.8 },
|
||||
{ item: 'gold', min: 20, max: 50, chance: 0.5 },
|
||||
{ item: 'iron_ore', min: 1, max: 5, chance: 0.3 }
|
||||
],
|
||||
'Forest': [
|
||||
{ item: 'wood', min: 20, max: 50, chance: 0.9 },
|
||||
{ item: 'apple', min: 3, max: 10, chance: 0.6 },
|
||||
{ item: 'berry', min: 5, max: 15, chance: 0.7 },
|
||||
{ item: 'mushroom', min: 2, max: 8, chance: 0.4 }
|
||||
],
|
||||
'Desert': [
|
||||
{ item: 'gold', min: 50, max: 150, chance: 0.6 },
|
||||
{ item: 'ruby', min: 1, max: 3, chance: 0.2 },
|
||||
{ item: 'ancient_scroll', min: 1, max: 1, chance: 0.1 },
|
||||
{ item: 'cactus_fruit', min: 3, max: 10, chance: 0.5 }
|
||||
],
|
||||
'Mountain': [
|
||||
{ item: 'iron_ore', min: 10, max: 30, chance: 0.8 },
|
||||
{ item: 'gold_ore', min: 5, max: 15, chance: 0.6 },
|
||||
{ item: 'diamond', min: 1, max: 3, chance: 0.15 },
|
||||
{ item: 'stone', min: 20, max: 50, chance: 0.9 }
|
||||
],
|
||||
'Swamp': [
|
||||
{ item: 'herbs', min: 5, max: 20, chance: 0.7 },
|
||||
{ item: 'mushroom', min: 10, max: 30, chance: 0.8 },
|
||||
{ item: 'slime', min: 5, max: 15, chance: 0.6 },
|
||||
{ item: 'ancient_bone', min: 1, max: 5, chance: 0.3 }
|
||||
]
|
||||
};
|
||||
|
||||
// Landmark treasure (special rare items)
|
||||
this.landmarkTreasures = {
|
||||
'ancient_temple': [
|
||||
{ item: 'legendary_sword', min: 1, max: 1, chance: 1.0 },
|
||||
{ item: 'gold', min: 500, max: 1000, chance: 1.0 },
|
||||
{ item: 'ancient_artifact', min: 1, max: 1, chance: 1.0 }
|
||||
],
|
||||
'great_pyramid': [
|
||||
{ item: 'pharaoh_staff', min: 1, max: 1, chance: 1.0 },
|
||||
{ item: 'ruby', min: 10, max: 20, chance: 1.0 },
|
||||
{ item: 'mummy_wraps', min: 5, max: 10, chance: 1.0 }
|
||||
],
|
||||
'mountain_peak': [
|
||||
{ item: 'titan_hammer', min: 1, max: 1, chance: 1.0 },
|
||||
{ item: 'diamond', min: 10, max: 20, chance: 1.0 },
|
||||
{ item: 'eagle_feather', min: 1, max: 1, chance: 1.0 }
|
||||
],
|
||||
'abandoned_city': [
|
||||
{ item: 'ancient_key', min: 1, max: 1, chance: 1.0 },
|
||||
{ item: 'gold', min: 1000, max: 2000, chance: 1.0 },
|
||||
{ item: 'city_map', min: 1, max: 1, chance: 1.0 }
|
||||
],
|
||||
'dragon_skeleton': [
|
||||
{ item: 'dragon_scale', min: 5, max: 10, chance: 1.0 },
|
||||
{ item: 'dragon_tooth', min: 1, max: 3, chance: 1.0 },
|
||||
{ item: 'dragon_heart', min: 1, max: 1, chance: 1.0 }
|
||||
]
|
||||
};
|
||||
|
||||
console.log('🏛️ StructureInteractionSystem initialized');
|
||||
}
|
||||
|
||||
// Generate chests for all structures
|
||||
generateChestsForStructures(structureSystem) {
|
||||
if (!structureSystem) return;
|
||||
|
||||
let chestsGenerated = 0;
|
||||
|
||||
// Regular structures
|
||||
for (const structure of structureSystem.structures) {
|
||||
// 70% chance to have a chest
|
||||
if (Math.random() < 0.7) {
|
||||
const chestKey = `${structure.x},${structure.y}`;
|
||||
this.chests.set(chestKey, {
|
||||
x: structure.x,
|
||||
y: structure.y,
|
||||
biome: structure.biome,
|
||||
opened: false,
|
||||
loot: this.generateLoot(structure.biome)
|
||||
});
|
||||
chestsGenerated++;
|
||||
}
|
||||
}
|
||||
|
||||
// Landmarks (always have treasure)
|
||||
for (const landmark of structureSystem.landmarks) {
|
||||
const chestKey = `${landmark.x},${landmark.y}`;
|
||||
this.chests.set(chestKey, {
|
||||
x: landmark.x,
|
||||
y: landmark.y,
|
||||
type: 'landmark',
|
||||
landmarkType: landmark.type,
|
||||
opened: false,
|
||||
loot: this.generateLandmarkTreasure(landmark.type)
|
||||
});
|
||||
chestsGenerated++;
|
||||
}
|
||||
|
||||
console.log(`✅ Generated ${chestsGenerated} chests (${this.chests.size} total)`);
|
||||
}
|
||||
|
||||
// Generate loot based on biome
|
||||
generateLoot(biome) {
|
||||
const lootTable = this.lootTables[biome] || this.lootTables['Grassland'];
|
||||
const loot = [];
|
||||
|
||||
for (const entry of lootTable) {
|
||||
if (Math.random() < entry.chance) {
|
||||
const amount = Math.floor(Math.random() * (entry.max - entry.min + 1)) + entry.min;
|
||||
loot.push({
|
||||
item: entry.item,
|
||||
amount: amount
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return loot;
|
||||
}
|
||||
|
||||
// Generate landmark treasure (always guaranteed)
|
||||
generateLandmarkTreasure(landmarkType) {
|
||||
const treasureTable = this.landmarkTreasures[landmarkType];
|
||||
if (!treasureTable) return [];
|
||||
|
||||
const loot = [];
|
||||
for (const entry of treasureTable) {
|
||||
const amount = Math.floor(Math.random() * (entry.max - entry.min + 1)) + entry.min;
|
||||
loot.push({
|
||||
item: entry.item,
|
||||
amount: amount,
|
||||
legendary: true // Mark as legendary loot
|
||||
});
|
||||
}
|
||||
|
||||
return loot;
|
||||
}
|
||||
|
||||
// Check for nearby structures
|
||||
update(playerX, playerY) {
|
||||
this.nearbyStructures = [];
|
||||
|
||||
// Check all chests
|
||||
for (const [key, chest] of this.chests) {
|
||||
const dist = Math.sqrt((chest.x - playerX) ** 2 + (chest.y - playerY) ** 2);
|
||||
|
||||
if (dist < 3 && !chest.opened) {
|
||||
this.nearbyStructures.push({
|
||||
type: chest.type === 'landmark' ? 'landmark' : 'structure',
|
||||
x: chest.x,
|
||||
y: chest.y,
|
||||
key: key,
|
||||
data: chest
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Show interaction prompt if nearby
|
||||
if (this.nearbyStructures.length > 0 && !this.currentInteraction) {
|
||||
this.showInteractionPrompt();
|
||||
} else if (this.nearbyStructures.length === 0) {
|
||||
this.hideInteractionPrompt();
|
||||
}
|
||||
}
|
||||
|
||||
// Show UI prompt to interact
|
||||
showInteractionPrompt() {
|
||||
if (this.interactionPrompt) return;
|
||||
|
||||
const nearest = this.nearbyStructures[0];
|
||||
const promptText = nearest.type === 'landmark'
|
||||
? '⭐ Press E to open LEGENDARY TREASURE'
|
||||
: '📦 Press E to open chest';
|
||||
|
||||
this.interactionPrompt = this.scene.add.text(
|
||||
this.scene.cameras.main.centerX,
|
||||
this.scene.cameras.main.height - 100,
|
||||
promptText,
|
||||
{
|
||||
fontSize: '24px',
|
||||
fontFamily: 'Arial',
|
||||
color: nearest.type === 'landmark' ? '#FFD700' : '#ffffff',
|
||||
backgroundColor: '#000000',
|
||||
padding: { x: 20, y: 10 }
|
||||
}
|
||||
);
|
||||
this.interactionPrompt.setOrigin(0.5);
|
||||
this.interactionPrompt.setScrollFactor(0);
|
||||
this.interactionPrompt.setDepth(10000);
|
||||
}
|
||||
|
||||
// Hide interaction prompt
|
||||
hideInteractionPrompt() {
|
||||
if (this.interactionPrompt) {
|
||||
this.interactionPrompt.destroy();
|
||||
this.interactionPrompt = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Player presses E to interact
|
||||
interact() {
|
||||
if (this.nearbyStructures.length === 0) return;
|
||||
|
||||
const nearest = this.nearbyStructures[0];
|
||||
this.openChest(nearest.key, nearest.data);
|
||||
}
|
||||
|
||||
// Open chest and give loot to player
|
||||
openChest(chestKey, chestData) {
|
||||
if (chestData.opened) return;
|
||||
|
||||
// Mark as opened
|
||||
chestData.opened = true;
|
||||
this.chests.set(chestKey, chestData);
|
||||
|
||||
// Give loot to player
|
||||
const inventory = this.scene.inventorySystem;
|
||||
if (!inventory) {
|
||||
console.warn('⚠️ InventorySystem not found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📦 Opening chest at (${chestData.x}, ${chestData.y})`);
|
||||
|
||||
let totalValue = 0;
|
||||
for (const lootItem of chestData.loot) {
|
||||
if (lootItem.item === 'gold') {
|
||||
inventory.gold += lootItem.amount;
|
||||
totalValue += lootItem.amount;
|
||||
} else {
|
||||
inventory.addItem(lootItem.item, lootItem.amount);
|
||||
totalValue += lootItem.amount * 10; // Estimate value
|
||||
}
|
||||
|
||||
console.log(` + ${lootItem.amount}x ${lootItem.item}${lootItem.legendary ? ' (LEGENDARY!)' : ''}`);
|
||||
}
|
||||
|
||||
// Show loot notification
|
||||
this.showLootNotification(chestData, totalValue);
|
||||
|
||||
// Play sound
|
||||
if (this.scene.soundManager) {
|
||||
this.scene.soundManager.beepPickup();
|
||||
}
|
||||
|
||||
// Hide prompt
|
||||
this.hideInteractionPrompt();
|
||||
}
|
||||
|
||||
// Show loot notification UI
|
||||
showLootNotification(chestData, totalValue) {
|
||||
const isLandmark = chestData.type === 'landmark';
|
||||
|
||||
const notification = this.scene.add.text(
|
||||
this.scene.cameras.main.centerX,
|
||||
this.scene.cameras.main.centerY - 100,
|
||||
isLandmark
|
||||
? `⭐ LEGENDARY TREASURE FOUND! ⭐\n${chestData.landmarkType}\nValue: ${totalValue} gold`
|
||||
: `📦 Chest Opened!\nValue: ${totalValue} gold`,
|
||||
{
|
||||
fontSize: isLandmark ? '32px' : '24px',
|
||||
fontFamily: 'Arial',
|
||||
color: isLandmark ? '#FFD700' : '#ffffff',
|
||||
backgroundColor: '#000000',
|
||||
padding: { x: 30, y: 20 },
|
||||
align: 'center'
|
||||
}
|
||||
);
|
||||
notification.setOrigin(0.5);
|
||||
notification.setScrollFactor(0);
|
||||
notification.setDepth(10001);
|
||||
|
||||
// Fade out after 3 seconds
|
||||
this.scene.tweens.add({
|
||||
targets: notification,
|
||||
alpha: 0,
|
||||
duration: 1000,
|
||||
delay: 2000,
|
||||
onComplete: () => notification.destroy()
|
||||
});
|
||||
|
||||
// Particle effect
|
||||
if (this.scene.particleEffects && isLandmark) {
|
||||
// Golden particles for landmarks
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const particle = this.scene.add.circle(
|
||||
this.scene.cameras.main.centerX + (Math.random() - 0.5) * 100,
|
||||
this.scene.cameras.main.centerY - 100,
|
||||
5,
|
||||
0xFFD700
|
||||
);
|
||||
particle.setScrollFactor(0);
|
||||
particle.setDepth(10000);
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: particle,
|
||||
y: particle.y - 100,
|
||||
alpha: 0,
|
||||
duration: 1500,
|
||||
onComplete: () => particle.destroy()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get statistics
|
||||
getStats() {
|
||||
const opened = Array.from(this.chests.values()).filter(c => c.opened).length;
|
||||
return {
|
||||
totalChests: this.chests.size,
|
||||
chestsOpened: opened,
|
||||
chestsRemaining: this.chests.size - opened
|
||||
};
|
||||
}
|
||||
|
||||
// Export/Import for save system
|
||||
exportData() {
|
||||
const chestsArray = [];
|
||||
for (const [key, chest] of this.chests) {
|
||||
chestsArray.push({
|
||||
key,
|
||||
opened: chest.opened
|
||||
});
|
||||
}
|
||||
return { chests: chestsArray };
|
||||
}
|
||||
|
||||
importData(data) {
|
||||
if (!data || !data.chests) return;
|
||||
|
||||
for (const savedChest of data.chests) {
|
||||
if (this.chests.has(savedChest.key)) {
|
||||
const chest = this.chests.get(savedChest.key);
|
||||
chest.opened = savedChest.opened;
|
||||
this.chests.set(savedChest.key, chest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.hideInteractionPrompt();
|
||||
this.interactionMarkers.forEach(m => m.destroy());
|
||||
this.chests.clear();
|
||||
}
|
||||
}
|
||||
392
src/systems/StructureSystem.js
Normal file
392
src/systems/StructureSystem.js
Normal file
@@ -0,0 +1,392 @@
|
||||
/**
|
||||
* 🏛️ STRUCTURE SYSTEM
|
||||
* Generates and manages structures across the 500x500 world
|
||||
* - Creates buildings, ruins, landmarks across biomes
|
||||
* - Handles roads and paths between biomes
|
||||
* - Biome-aware structure placement
|
||||
*/
|
||||
|
||||
class StructureSystem {
|
||||
constructor(worldWidth, worldHeight, biomeSystem, riverSystem, lakeSystem) {
|
||||
this.worldWidth = worldWidth;
|
||||
this.worldHeight = worldHeight;
|
||||
this.biomeSystem = biomeSystem;
|
||||
this.riverSystem = riverSystem;
|
||||
this.lakeSystem = lakeSystem;
|
||||
|
||||
// Structure map (marks where structures are)
|
||||
this.structureMap = Array(worldHeight).fill(null).map(() => Array(worldWidth).fill(null));
|
||||
|
||||
// Road map (marks where roads are)
|
||||
this.roadMap = Array(worldHeight).fill(null).map(() => Array(worldWidth).fill(false));
|
||||
|
||||
// List of all structures
|
||||
this.structures = [];
|
||||
|
||||
// List of all landmarks
|
||||
this.landmarks = [];
|
||||
|
||||
// Road paths
|
||||
this.roads = [];
|
||||
|
||||
// Structure types by biome
|
||||
this.structureTypes = {
|
||||
'Grassland': ['farm', 'house', 'barn', 'windmill', 'well'],
|
||||
'Forest': ['cabin', 'ruins', 'treehouse', 'camp', 'shrine'],
|
||||
'Desert': ['pyramid', 'ruins', 'oasis_camp', 'tomb', 'pillar'],
|
||||
'Mountain': ['mine', 'cave', 'tower', 'ruins', 'altar'],
|
||||
'Swamp': ['hut', 'ruins', 'totem', 'bog_shrine', 'abandoned_dock']
|
||||
};
|
||||
|
||||
// Landmark types (unique, 1-3 per world)
|
||||
this.landmarkTypes = [
|
||||
{ type: 'ancient_temple', biome: 'Forest', count: 1 },
|
||||
{ type: 'great_pyramid', biome: 'Desert', count: 1 },
|
||||
{ type: 'mountain_peak', biome: 'Mountain', count: 1 },
|
||||
{ type: 'abandoned_city', biome: 'Grassland', count: 1 },
|
||||
{ type: 'dragon_skeleton', biome: 'Swamp', count: 1 }
|
||||
];
|
||||
}
|
||||
|
||||
// Generate all structures and roads
|
||||
generateAll() {
|
||||
console.log('🏛️ StructureSystem: Starting structure generation...');
|
||||
|
||||
// 1. Generate roads between biome centers
|
||||
this.generateRoads();
|
||||
|
||||
// 2. Generate landmarks (major points of interest)
|
||||
this.generateLandmarks();
|
||||
|
||||
// 3. Generate regular structures
|
||||
this.generateStructures();
|
||||
|
||||
console.log(`✅ Generated ${this.structures.length} structures, ${this.landmarks.length} landmarks, ${this.roads.length} roads`);
|
||||
}
|
||||
|
||||
// Generate roads connecting biome centers
|
||||
generateRoads() {
|
||||
console.log('🛤️ Generating roads...');
|
||||
|
||||
// Find biome centers
|
||||
const biomeLocations = {
|
||||
'Grassland': [],
|
||||
'Forest': [],
|
||||
'Desert': [],
|
||||
'Mountain': [],
|
||||
'Swamp': []
|
||||
};
|
||||
|
||||
// Sample the world to find biome centers
|
||||
const sampleRate = 50;
|
||||
for (let x = 0; x < this.worldWidth; x += sampleRate) {
|
||||
for (let y = 0; y < this.worldHeight; y += sampleRate) {
|
||||
const biome = this.biomeSystem.getBiome(x, y);
|
||||
if (!biomeLocations[biome]) biomeLocations[biome] = [];
|
||||
biomeLocations[biome].push({ x, y });
|
||||
}
|
||||
}
|
||||
|
||||
// Create main roads connecting different biomes
|
||||
const roadPoints = [];
|
||||
|
||||
// Central hub (spawn point)
|
||||
roadPoints.push({ x: 250, y: 250, name: 'Center' });
|
||||
|
||||
// Add one major location per biome
|
||||
for (const [biomeName, locations] of Object.entries(biomeLocations)) {
|
||||
if (locations.length > 0) {
|
||||
// Pick central location
|
||||
const centerIdx = Math.floor(locations.length / 2);
|
||||
roadPoints.push({
|
||||
x: locations[centerIdx].x,
|
||||
y: locations[centerIdx].y,
|
||||
name: `${biomeName} Hub`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Connect road points
|
||||
for (let i = 0; i < roadPoints.length; i++) {
|
||||
const start = roadPoints[i];
|
||||
|
||||
// Connect to nearest 2-3 other points
|
||||
const distances = roadPoints
|
||||
.map((point, idx) => ({
|
||||
idx,
|
||||
dist: Math.sqrt((point.x - start.x) ** 2 + (point.y - start.y) ** 2)
|
||||
}))
|
||||
.filter(d => d.idx !== i)
|
||||
.sort((a, b) => a.dist - b.dist);
|
||||
|
||||
// Connect to 1-2 nearest points
|
||||
const connectCount = Math.min(2, distances.length);
|
||||
for (let j = 0; j < connectCount; j++) {
|
||||
const end = roadPoints[distances[j].idx];
|
||||
this.createRoad(start, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a road between two points
|
||||
createRoad(start, end) {
|
||||
const path = [];
|
||||
const dx = end.x - start.x;
|
||||
const dy = end.y - start.y;
|
||||
const steps = Math.max(Math.abs(dx), Math.abs(dy));
|
||||
|
||||
// Create path with some randomness (looks more natural)
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const t = i / steps;
|
||||
let x = Math.round(start.x + dx * t);
|
||||
let y = Math.round(start.y + dy * t);
|
||||
|
||||
// Add slight curve (Perlin-like noise)
|
||||
const noise = Math.sin(t * Math.PI * 3) * 10;
|
||||
x += Math.round(noise);
|
||||
|
||||
path.push({ x, y });
|
||||
}
|
||||
|
||||
// Mark road tiles (3 tiles wide)
|
||||
for (const point of path) {
|
||||
for (let offsetX = -1; offsetX <= 1; offsetX++) {
|
||||
for (let offsetY = -1; offsetY <= 1; offsetY++) {
|
||||
const x = point.x + offsetX;
|
||||
const y = point.y + offsetY;
|
||||
|
||||
if (x >= 0 && x < this.worldWidth && y >= 0 && y < this.worldHeight) {
|
||||
// Don't place roads over water
|
||||
if (!this.riverSystem.isRiver(x, y) && !this.lakeSystem.isLake(x, y)) {
|
||||
this.roadMap[y][x] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.roads.push({
|
||||
start: start.name || 'unknown',
|
||||
end: end.name || 'unknown',
|
||||
path
|
||||
});
|
||||
}
|
||||
|
||||
// Generate landmarks (unique structures)
|
||||
generateLandmarks() {
|
||||
console.log('🗿 Generating landmarks...');
|
||||
|
||||
for (const landmarkDef of this.landmarkTypes) {
|
||||
for (let i = 0; i < landmarkDef.count; i++) {
|
||||
const location = this.findBiomeLocation(landmarkDef.biome, 100, 100);
|
||||
if (location) {
|
||||
this.createLandmark(landmarkDef.type, location.x, location.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a single landmark
|
||||
createLandmark(type, x, y) {
|
||||
const size = 15; // Landmarks are large (15x15)
|
||||
|
||||
// Mark area as occupied
|
||||
for (let dx = -size; dx <= size; dx++) {
|
||||
for (let dy = -size; dy <= size; dy++) {
|
||||
const tx = x + dx;
|
||||
const ty = y + dy;
|
||||
if (tx >= 0 && tx < this.worldWidth && ty >= 0 && ty < this.worldHeight) {
|
||||
this.structureMap[ty][tx] = { type: 'landmark', landmarkType: type };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.landmarks.push({
|
||||
type,
|
||||
x,
|
||||
y,
|
||||
size,
|
||||
discovered: false
|
||||
});
|
||||
}
|
||||
|
||||
// Generate regular structures
|
||||
generateStructures() {
|
||||
console.log('🏠 Generating structures...');
|
||||
|
||||
const structureCount = 80; // 80 structures across the world
|
||||
let placed = 0;
|
||||
let attempts = 0;
|
||||
const maxAttempts = structureCount * 10;
|
||||
|
||||
while (placed < structureCount && attempts < maxAttempts) {
|
||||
attempts++;
|
||||
|
||||
// Random location
|
||||
const x = Math.floor(Math.random() * this.worldWidth);
|
||||
const y = Math.floor(Math.random() * this.worldHeight);
|
||||
|
||||
// Check if location is valid
|
||||
if (this.canPlaceStructure(x, y, 20)) {
|
||||
const biome = this.biomeSystem.getBiome(x, y);
|
||||
const types = this.structureTypes[biome];
|
||||
|
||||
if (types && types.length > 0) {
|
||||
const type = types[Math.floor(Math.random() * types.length)];
|
||||
this.createStructure(type, x, y, biome);
|
||||
placed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ Placed ${placed} structures (${attempts} attempts)`);
|
||||
}
|
||||
|
||||
// Check if structure can be placed at location
|
||||
canPlaceStructure(x, y, minDistance) {
|
||||
// Check if on water
|
||||
if (this.riverSystem.isRiver(x, y) || this.lakeSystem.isLake(x, y)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if too close to other structures
|
||||
for (const structure of this.structures) {
|
||||
const dist = Math.sqrt((structure.x - x) ** 2 + (structure.y - y) ** 2);
|
||||
if (dist < minDistance) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if too close to landmarks
|
||||
for (const landmark of this.landmarks) {
|
||||
const dist = Math.sqrt((landmark.x - x) ** 2 + (landmark.y - y) ** 2);
|
||||
if (dist < 50) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create a single structure
|
||||
createStructure(type, x, y, biome) {
|
||||
const size = this.getStructureSize(type);
|
||||
|
||||
// Mark area as occupied
|
||||
for (let dx = -size; dx <= size; dx++) {
|
||||
for (let dy = -size; dy <= size; dy++) {
|
||||
const tx = x + dx;
|
||||
const ty = y + dy;
|
||||
if (tx >= 0 && tx < this.worldWidth && ty >= 0 && ty < this.worldHeight) {
|
||||
this.structureMap[ty][tx] = { type: 'structure', structureType: type };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.structures.push({
|
||||
type,
|
||||
x,
|
||||
y,
|
||||
size,
|
||||
biome,
|
||||
explored: false
|
||||
});
|
||||
}
|
||||
|
||||
// Get structure size
|
||||
getStructureSize(type) {
|
||||
const sizes = {
|
||||
// Small structures
|
||||
'well': 2,
|
||||
'camp': 3,
|
||||
'totem': 2,
|
||||
'pillar': 2,
|
||||
|
||||
// Medium structures
|
||||
'house': 4,
|
||||
'cabin': 4,
|
||||
'hut': 3,
|
||||
'shrine': 4,
|
||||
'altar': 4,
|
||||
'tomb': 5,
|
||||
|
||||
// Large structures
|
||||
'farm': 7,
|
||||
'barn': 6,
|
||||
'windmill': 5,
|
||||
'ruins': 6,
|
||||
'mine': 5,
|
||||
'tower': 4,
|
||||
'pyramid': 8,
|
||||
'cave': 5,
|
||||
'treehouse': 4,
|
||||
'oasis_camp': 5,
|
||||
'bog_shrine': 4,
|
||||
'abandoned_dock': 5
|
||||
};
|
||||
|
||||
return sizes[type] || 4;
|
||||
}
|
||||
|
||||
// Find a location in specific biome
|
||||
findBiomeLocation(biomeName, minDistance, maxAttempts) {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
const x = Math.floor(Math.random() * this.worldWidth);
|
||||
const y = Math.floor(Math.random() * this.worldHeight);
|
||||
|
||||
const biome = this.biomeSystem.getBiome(x, y);
|
||||
|
||||
if (biome === biomeName && this.canPlaceStructure(x, y, minDistance)) {
|
||||
return { x, y };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if tile is a road
|
||||
isRoad(x, y) {
|
||||
if (x < 0 || x >= this.worldWidth || y < 0 || y >= this.worldHeight) {
|
||||
return false;
|
||||
}
|
||||
return this.roadMap[y][x];
|
||||
}
|
||||
|
||||
// Get structure at tile
|
||||
getStructure(x, y) {
|
||||
if (x < 0 || x >= this.worldWidth || y < 0 || y >= this.worldHeight) {
|
||||
return null;
|
||||
}
|
||||
return this.structureMap[y][x];
|
||||
}
|
||||
|
||||
// Get statistics
|
||||
getStats() {
|
||||
return {
|
||||
structures: this.structures.length,
|
||||
landmarks: this.landmarks.length,
|
||||
roads: this.roads.length,
|
||||
roadTiles: this.roadMap.flat().filter(r => r).length
|
||||
};
|
||||
}
|
||||
|
||||
// Export data for saving
|
||||
exportData() {
|
||||
return {
|
||||
structures: this.structures,
|
||||
landmarks: this.landmarks,
|
||||
roads: this.roads,
|
||||
structureMap: this.structureMap,
|
||||
roadMap: this.roadMap
|
||||
};
|
||||
}
|
||||
|
||||
// Import data from save
|
||||
importData(data) {
|
||||
this.structures = data.structures || [];
|
||||
this.landmarks = data.landmarks || [];
|
||||
this.roads = data.roads || [];
|
||||
this.structureMap = data.structureMap || this.structureMap;
|
||||
this.roadMap = data.roadMap || this.roadMap;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user