338 lines
10 KiB
JavaScript
338 lines
10 KiB
JavaScript
/**
|
|
* 👥 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 = [];
|
|
}
|
|
}
|