MASTER SYSTEMS + Town & Privacy
This commit is contained in:
460
src/systems/TownGrowthSystem.js
Normal file
460
src/systems/TownGrowthSystem.js
Normal file
@@ -0,0 +1,460 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
export 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();
|
||||
}
|
||||
}
|
||||
|
||||
export default TownGrowthSystem;
|
||||
Reference in New Issue
Block a user