461 lines
15 KiB
JavaScript
461 lines
15 KiB
JavaScript
/**
|
|
* TOWN GROWTH SYSTEM - Population & Expansion
|
|
* Part of: Game Systems Expansion
|
|
* Created: January 4, 2026
|
|
*
|
|
* Features:
|
|
* - Dynamic town population (4 → 20 NPCs)
|
|
* - Town sign with live stats
|
|
* - Small villages (5-10 scattered across map)
|
|
* - Population unlock requirements
|
|
* - Town Services unlock based on population
|
|
*/
|
|
|
|
class TownGrowthSystem {
|
|
constructor(game) {
|
|
this.game = game;
|
|
this.player = game.player;
|
|
|
|
// Town stats
|
|
this.townName = 'Dolina Smrti';
|
|
this.population = 4; // Starting NPCs: Kai, Ana, Gronk, Baker
|
|
this.maxPopulation = 20;
|
|
this.populationSlots = [
|
|
{ index: 1, unlocked: true, npc: 'kai' },
|
|
{ index: 2, unlocked: true, npc: 'ana' },
|
|
{ index: 3, unlocked: true, npc: 'gronk' },
|
|
{ index: 4, unlocked: true, npc: 'baker' },
|
|
{ index: 5, unlocked: false, npc: null, requirement: { farmLevel: 2 } },
|
|
{ index: 6, unlocked: false, npc: null, requirement: { money: 10000 } },
|
|
{ index: 7, unlocked: false, npc: null, requirement: { quest: 'expand_town_1' } },
|
|
{ index: 8, unlocked: false, npc: null, requirement: { population: 6 } },
|
|
{ index: 9, unlocked: false, npc: null, requirement: { building: 'bakery' } },
|
|
{ index: 10, unlocked: false, npc: null, requirement: { building: 'barbershop' } },
|
|
{ index: 11, unlocked: false, npc: null, requirement: { hearts: 5, npcId: 'any' } },
|
|
{ index: 12, unlocked: false, npc: null, requirement: { quest: 'expand_town_2' } },
|
|
{ index: 13, unlocked: false, npc: null, requirement: { zombieWorkers: 5 } },
|
|
{ index: 14, unlocked: false, npc: null, requirement: { building: 'mine' } },
|
|
{ index: 15, unlocked: false, npc: null, requirement: { money: 50000 } },
|
|
{ index: 16, unlocked: false, npc: null, requirement: { quest: 'expand_town_3' } },
|
|
{ index: 17, unlocked: false, npc: null, requirement: { marriage: true } },
|
|
{ index: 18, unlocked: false, npc: null, requirement: { population: 15 } },
|
|
{ index: 19, unlocked: false, npc: null, requirement: { allBuildings: true } },
|
|
{ index: 20, unlocked: false, npc: null, requirement: { quest: 'town_master' } }
|
|
];
|
|
|
|
// Town sign
|
|
this.townSign = {
|
|
location: { x: 400, y: 300 },
|
|
visible: true,
|
|
displayMode: 'population' // 'population', 'status', 'full'
|
|
};
|
|
|
|
// Small villages
|
|
this.villages = this.initializeVillages();
|
|
|
|
// Town services (unlock based on population)
|
|
this.services = {
|
|
market: { unlocked: false, requiredPopulation: 6 },
|
|
hospital: { unlocked: false, requiredPopulation: 8 },
|
|
school: { unlocked: false, requiredPopulation: 10 },
|
|
bank: { unlocked: false, requiredPopulation: 12 },
|
|
museum: { unlocked: false, requiredPopulation: 15 },
|
|
theater: { unlocked: false, requiredPopulation: 18 }
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Initialize small villages
|
|
*/
|
|
initializeVillages() {
|
|
return [
|
|
{
|
|
id: 'village_north',
|
|
name: 'Severna Vas',
|
|
position: { x: 3000, y: 500 },
|
|
population: 7,
|
|
discovered: false,
|
|
npcs: [
|
|
{ id: 'fisherman', name: 'Old Fisher', hobby: 'fishing' },
|
|
{ id: 'hunter', name: 'Hunter Dane', hobby: 'hunting' },
|
|
{ id: 'hermit', name: 'Wise Hermit', hobby: 'meditation' }
|
|
],
|
|
specialItems: ['ancient_fishing_rod', 'hunter_bow', 'meditation_mat']
|
|
},
|
|
{
|
|
id: 'village_east',
|
|
name: 'Vzhodna Stran',
|
|
position: { x: 5000, y: 2000 },
|
|
population: 5,
|
|
discovered: false,
|
|
npcs: [
|
|
{ id: 'blacksmith', name: 'Master Smith', hobby: 'forging' },
|
|
{ id: 'alchemist', name: 'Mysterious Alchemist', hobby: 'alchemy' }
|
|
],
|
|
specialItems: ['master_anvil', 'legendary_hammer', 'philosopher_stone']
|
|
},
|
|
{
|
|
id: 'village_south',
|
|
name: 'Južno Naselje',
|
|
position: { x: 2500, y: 4500 },
|
|
population: 6,
|
|
discovered: false,
|
|
npcs: [
|
|
{ id: 'trader', name: 'Traveling Trader', hobby: 'collecting' },
|
|
{ id: 'musician', name: 'Bard Luka', hobby: 'music' }
|
|
],
|
|
specialItems: ['exotic_seeds', 'rare_instruments', 'ancient_map']
|
|
},
|
|
{
|
|
id: 'village_west',
|
|
name: 'Zahodna Dolina',
|
|
position: { x: 500, y: 3000 },
|
|
population: 8,
|
|
discovered: false,
|
|
npcs: [
|
|
{ id: 'chef', name: 'Chef Antonio', hobby: 'cooking' },
|
|
{ id: 'librarian', name: 'Keeper of Books', hobby: 'reading' },
|
|
{ id: 'artist', name: 'Painter Ana', hobby: 'painting' }
|
|
],
|
|
specialItems: ['master_cookbook', 'ancient_tome', 'rare_pigments']
|
|
},
|
|
{
|
|
id: 'village_mysterious',
|
|
name: '??? Mystery Village',
|
|
position: { x: 4000, y: 4000 },
|
|
population: 10,
|
|
discovered: false,
|
|
hidden: true, // Only visible after completing special quest
|
|
npcs: [
|
|
{ id: 'time_keeper', name: 'Keeper of Time', hobby: 'timekeeping' },
|
|
{ id: 'oracle', name: 'Oracle of Dolina', hobby: 'prophecy' }
|
|
],
|
|
specialItems: ['time_crystal', 'prophecy_scroll', 'reality_gem']
|
|
}
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Check and unlock new population slot
|
|
*/
|
|
checkPopulationUnlocks() {
|
|
let newUnlocks = 0;
|
|
|
|
this.populationSlots.forEach(slot => {
|
|
if (!slot.unlocked && slot.requirement) {
|
|
if (this.meetsRequirement(slot.requirement)) {
|
|
slot.unlocked = true;
|
|
newUnlocks++;
|
|
|
|
this.game.showMessage(
|
|
`New population slot unlocked! (${this.population}/${this.maxPopulation})`
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
if (newUnlocks > 0) {
|
|
this.updateTownServices();
|
|
}
|
|
|
|
return newUnlocks;
|
|
}
|
|
|
|
/**
|
|
* Check if requirement is met
|
|
*/
|
|
meetsRequirement(requirement) {
|
|
// Farm level
|
|
if (requirement.farmLevel) {
|
|
if (this.player.farmLevel < requirement.farmLevel) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Money
|
|
if (requirement.money) {
|
|
if (this.player.money < requirement.money) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Quest
|
|
if (requirement.quest) {
|
|
if (!this.player.hasCompletedQuest(requirement.quest)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Building
|
|
if (requirement.building) {
|
|
if (!this.player.hasBuilding(requirement.building)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Current population
|
|
if (requirement.population) {
|
|
if (this.population < requirement.population) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Zombie workers
|
|
if (requirement.zombieWorkers) {
|
|
const zombieCount = this.game.zombieWorkers?.getWorkerCount() || 0;
|
|
if (zombieCount < requirement.zombieWorkers) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Marriage
|
|
if (requirement.marriage) {
|
|
if (!this.player.isMarried) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Hearts with any NPC
|
|
if (requirement.hearts && requirement.npcId === 'any') {
|
|
const hasHighRelationship = this.game.npcs.getAllNPCs()
|
|
.some(npc => npc.relationshipHearts >= requirement.hearts);
|
|
if (!hasHighRelationship) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// All buildings
|
|
if (requirement.allBuildings) {
|
|
const requiredBuildings = ['bakery', 'barbershop', 'lawyer', 'mine', 'hospital'];
|
|
if (!requiredBuildings.every(b => this.player.hasBuilding(b))) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Invite new NPC to town
|
|
*/
|
|
inviteNPC(npcId) {
|
|
// Find available slot
|
|
const availableSlot = this.populationSlots.find(
|
|
slot => slot.unlocked && slot.npc === null
|
|
);
|
|
|
|
if (!availableSlot) {
|
|
return {
|
|
success: false,
|
|
message: 'No available population slots!'
|
|
};
|
|
}
|
|
|
|
// Check if NPC exists
|
|
const npcData = this.game.npcs.getNPCData(npcId);
|
|
if (!npcData) {
|
|
return { success: false, message: 'NPC not found!' };
|
|
}
|
|
|
|
// Assign NPC to slot
|
|
availableSlot.npc = npcId;
|
|
|
|
// Spawn NPC in town
|
|
this.game.npcs.spawn(npcId, {
|
|
homeLocation: 'town',
|
|
moveInDate: this.game.time.currentDate
|
|
});
|
|
|
|
// Increase population
|
|
this.population++;
|
|
|
|
// Update town sign
|
|
this.updateTownSign();
|
|
|
|
// Check for service unlocks
|
|
this.updateTownServices();
|
|
|
|
this.game.showMessage(
|
|
`${npcData.name} moved to town! Population: ${this.population}/${this.maxPopulation}`
|
|
);
|
|
|
|
return { success: true, npc: npcData };
|
|
}
|
|
|
|
/**
|
|
* Update town services based on population
|
|
*/
|
|
updateTownServices() {
|
|
let newServices = [];
|
|
|
|
Object.entries(this.services).forEach(([serviceId, service]) => {
|
|
if (!service.unlocked && this.population >= service.requiredPopulation) {
|
|
service.unlocked = true;
|
|
newServices.push(serviceId);
|
|
|
|
this.game.showMessage(
|
|
`🏛️ New service unlocked: ${serviceId}! (Pop: ${this.population})`
|
|
);
|
|
|
|
// Trigger service built event
|
|
this.game.emit('serviceUnlocked', {
|
|
serviceId: serviceId,
|
|
population: this.population
|
|
});
|
|
}
|
|
});
|
|
|
|
return newServices;
|
|
}
|
|
|
|
/**
|
|
* Update town sign display
|
|
*/
|
|
updateTownSign() {
|
|
const signData = {
|
|
townName: this.townName,
|
|
population: this.population,
|
|
maxPopulation: this.maxPopulation,
|
|
status: this.getTownStatus(),
|
|
services: Object.keys(this.services).filter(s => this.services[s].unlocked).length
|
|
};
|
|
|
|
// Emit event to update sign sprite
|
|
this.game.emit('townSignUpdate', signData);
|
|
}
|
|
|
|
/**
|
|
* Get town status description
|
|
*/
|
|
getTownStatus() {
|
|
if (this.population >= 18) {
|
|
return 'Thriving City';
|
|
}
|
|
if (this.population >= 15) {
|
|
return 'Prosperous Town';
|
|
}
|
|
if (this.population >= 10) {
|
|
return 'Growing Town';
|
|
}
|
|
if (this.population >= 6) {
|
|
return 'Small Town';
|
|
}
|
|
return 'Village';
|
|
}
|
|
|
|
/**
|
|
* Discover village
|
|
*/
|
|
discoverVillage(villageId) {
|
|
const village = this.villages.find(v => v.id === villageId);
|
|
|
|
if (!village) {
|
|
return { success: false };
|
|
}
|
|
|
|
if (village.discovered) {
|
|
return {
|
|
success: false,
|
|
message: 'Village already discovered!'
|
|
};
|
|
}
|
|
|
|
// Check if hidden village requires quest
|
|
if (village.hidden && !this.player.hasCompletedQuest('find_mystery_village')) {
|
|
return {
|
|
success: false,
|
|
message: 'This village remains hidden...'
|
|
};
|
|
}
|
|
|
|
// Discover village
|
|
village.discovered = true;
|
|
|
|
// Spawn village NPCs
|
|
village.npcs.forEach(npcData => {
|
|
this.game.npcs.spawn(npcData.id, {
|
|
homeLocation: villageId,
|
|
position: village.position,
|
|
hobby: npcData.hobby
|
|
});
|
|
});
|
|
|
|
// Mark special items as available
|
|
village.specialItems.forEach(itemId => {
|
|
this.game.items.markAsDiscovered(itemId, villageId);
|
|
});
|
|
|
|
this.game.showMessage(
|
|
`Discovered ${village.name}! Population: ${village.population} NPCs`
|
|
);
|
|
|
|
// Achievement
|
|
const discoveredCount = this.villages.filter(v => v.discovered).length;
|
|
if (discoveredCount === this.villages.length) {
|
|
this.game.achievements.unlock('village_explorer');
|
|
}
|
|
|
|
return { success: true, village: village };
|
|
}
|
|
|
|
/**
|
|
* Get travel distance to village
|
|
*/
|
|
getTravelDistance(villageId) {
|
|
const village = this.villages.find(v => v.id === villageId);
|
|
if (!village) return null;
|
|
|
|
const playerPos = this.player.getPosition();
|
|
const dx = village.position.x - playerPos.x;
|
|
const dy = village.position.y - playerPos.y;
|
|
|
|
return Math.sqrt(dx * dx + dy * dy);
|
|
}
|
|
|
|
/**
|
|
* Get village trade options
|
|
*/
|
|
getVillageTradeOptions(villageId) {
|
|
const village = this.villages.find(v => v.id === villageId);
|
|
if (!village || !village.discovered) return null;
|
|
|
|
return {
|
|
villageName: village.name,
|
|
npcs: village.npcs,
|
|
specialItems: village.specialItems,
|
|
population: village.population
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get town growth UI data
|
|
*/
|
|
getTownGrowthUIData() {
|
|
return {
|
|
townName: this.townName,
|
|
population: this.population,
|
|
maxPopulation: this.maxPopulation,
|
|
status: this.getTownStatus(),
|
|
populationSlots: this.populationSlots,
|
|
availableSlots: this.populationSlots.filter(s => s.unlocked && !s.npc).length,
|
|
services: this.services,
|
|
villages: this.villages.filter(v => !v.hidden || v.discovered),
|
|
discoveredVillages: this.villages.filter(v => v.discovered).length,
|
|
totalVillages: this.villages.filter(v => !v.hidden).length
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Update (check for new unlocks)
|
|
*/
|
|
update() {
|
|
// Check for new population slot unlocks
|
|
this.checkPopulationUnlocks();
|
|
|
|
// Update town sign
|
|
this.updateTownSign();
|
|
}
|
|
}
|
|
|
|
|