Phase 29: Gameplay Systems (5/5) - Structure interaction, NPCs, Enemies, Quests, Map

This commit is contained in:
2025-12-17 20:03:11 +01:00
parent 17e988f96f
commit 9c39b51303
14 changed files with 3546 additions and 9 deletions

View File

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

View 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 = [];
}
}

View File

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

View 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 = [];
}
}

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

View 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 = [];
}
}

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

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