diff --git a/src/systems/MasterGameSystemsManager.js b/src/systems/MasterGameSystemsManager.js new file mode 100644 index 000000000..8c550408b --- /dev/null +++ b/src/systems/MasterGameSystemsManager.js @@ -0,0 +1,454 @@ +/** + * MASTER GAME SYSTEMS MANAGER + * Centralized coordinator for all game systems + * Created: January 4, 2026 + * + * Integrates: + * - Sleep System + * - Crafting Tables System + * - Bakery Shop System + * - Barber Shop System + * - Lawyer Office System + * - Zombie Miner Automation System + * - Town Growth System + * - NPC Privacy System + * - Existing Mining System + */ + +import SleepSystem from './SleepSystem.js'; +import CraftingTablesSystem from './CraftingTablesSystem.js'; +import BakeryShopSystem from './BakeryShopSystem.js'; +import BarberShopSystem from './BarberShopSystem.js'; +import LawyerOfficeSystem from './LawyerOfficeSystem.js'; +import ZombieMinerAutomationSystem from './ZombieMinerAutomationSystem.js'; +import TownGrowthSystem from './TownGrowthSystem.js'; +import NPCPrivacySystem from './NPCPrivacySystem.js'; + +export class MasterGameSystemsManager { + constructor(game) { + this.game = game; + this.scene = game.scene; + + console.log('🎮 Initializing Master Game Systems Manager...'); + + // Initialize all systems + this.initializeSystems(); + + // Set up cross-system event listeners + this.setupEventListeners(); + + console.log('✅ Master Game Systems Manager initialized!'); + } + + /** + * Initialize all game systems + */ + initializeSystems() { + // HOME & SLEEP + this.sleepSystem = new SleepSystem(this.game); + console.log(' ✓ Sleep System'); + + // CRAFTING + this.craftingSystem = new CraftingTablesSystem(this.game); + console.log(' ✓ Crafting Tables System'); + + // TOWN BUILDINGS + this.bakerySystem = new BakeryShopSystem(this.game); + console.log(' ✓ Bakery Shop System'); + + this.barberSystem = new BarberShopSystem(this.game); + console.log(' ✓ Barber Shop System'); + + this.lawyerSystem = new LawyerOfficeSystem(this.game); + console.log(' ✓ Lawyer Office System'); + + // MINING & AUTOMATION + this.zombieMinerSystem = new ZombieMinerAutomationSystem(this.game); + console.log(' ✓ Zombie Miner Automation System'); + + // TOWN GROWTH + this.townGrowthSystem = new TownGrowthSystem(this.game); + console.log(' ✓ Town Growth System'); + + // NPC SYSTEMS + this.npcPrivacySystem = new NPCPrivacySystem(this.game); + console.log(' ✓ NPC Privacy System'); + + // Register all systems globally + this.registerSystems(); + } + + /** + * Register systems to game registry + */ + registerSystems() { + this.game.registry.set('sleepSystem', this.sleepSystem); + this.game.registry.set('craftingSystem', this.craftingSystem); + this.game.registry.set('bakerySystem', this.bakerySystem); + this.game.registry.set('barberSystem', this.barberSystem); + this.game.registry.set('lawyerSystem', this.lawyerSystem); + this.game.registry.set('zombieMinerSystem', this.zombieMinerSystem); + this.game.registry.set('townGrowthSystem', this.townGrowthSystem); + this.game.registry.set('npcPrivacySystem', this.npcPrivacySystem); + + console.log(' ✓ All systems registered to game registry'); + } + + /** + * Set up cross-system event listeners + */ + setupEventListeners() { + // TOWN GROWTH → SERVICES + this.game.events.on('serviceUnlocked', (data) => { + this.onServiceUnlocked(data); + }); + + // MARRIAGE → LAWYER AUTO-UNLOCK + this.game.events.on('marriageComplete', () => { + this.lawyerSystem.checkAutoUnlock(); + }); + + // RELATIONSHIP CHANGE → LAWYER AUTO-UNLOCK + this.game.events.on('relationshipChanged', (data) => { + if (data.hearts <= 3) { + this.lawyerSystem.checkAutoUnlock(); + } + }); + + // BUILDING UNLOCKED → TOWN GROWTH + this.game.events.on('buildingUnlocked', (data) => { + this.townGrowthSystem.checkPopulationUnlocks(); + }); + + // ZOMBIE HIRED → TOWN GROWTH CHECK + this.game.events.on('zombieWorkerHired', () => { + this.townGrowthSystem.checkPopulationUnlocks(); + }); + + // SLEEP → ZOMBIE LOYALTY DECAY PAUSE + this.game.events.on('sleepStarted', () => { + this.pauseZombieLoyaltyDecay = true; + }); + + this.game.events.on('wakeUp', () => { + this.pauseZombieLoyaltyDecay = false; + }); + + console.log(' ✓ Cross-system event listeners configured'); + } + + /** + * Handle service unlock + */ + onServiceUnlocked(data) { + const { serviceId, population } = data; + + console.log(`🏛️ Service unlocked: ${serviceId} at pop ${population}`); + + // Trigger service-specific initialization + switch (serviceId) { + case 'market': + this.initializeMarket(); + break; + case 'hospital': + this.initializeHospital(); + break; + case 'school': + this.initializeSchool(); + break; + case 'bank': + this.initializeBank(); + break; + case 'museum': + this.initializeMuseum(); + break; + case 'theater': + this.initializeTheater(); + break; + } + } + + /** + * Service initializers (placeholders for future implementation) + */ + initializeMarket() { + console.log(' → Market initialized'); + // TODO: Implement market system + } + + initializeHospital() { + console.log(' → Hospital initialized'); + // TODO: Implement hospital system + } + + initializeSchool() { + console.log(' → School initialized'); + // TODO: Implement school system + } + + initializeBank() { + console.log(' → Bank initialized'); + // TODO: Implement bank system + } + + initializeMuseum() { + console.log(' → Museum initialized'); + // TODO: Implement museum system + } + + initializeTheater() { + console.log(' → Theater initialized'); + // TODO: Implement theater system + } + + /** + * Update all systems (called every frame) + */ + update(time, delta) { + const deltaSeconds = delta / 1000; + + // Update systems that need per-frame updates + this.sleepSystem.update(deltaSeconds); + this.craftingSystem.update(deltaSeconds); + + // Update zombie miners (if not paused by sleep) + if (!this.pauseZombieLoyaltyDecay) { + this.zombieMinerSystem.update(deltaSeconds); + } + } + + /** + * Hourly update (called when game hour changes) + */ + onHourChange(hour) { + // Update time-sensitive systems + this.bakerySystem.update(); + this.townGrowthSystem.update(); + + // Check for automation collection reminders + if (hour % 4 === 0) { // Every 4 hours + this.checkAutomationReminders(); + } + } + + /** + * Check automation reminders + */ + checkAutomationReminders() { + // Zombie miner automation + if (this.zombieMinerSystem.automationActive) { + const hours = this.zombieMinerSystem.getHoursSinceLastCollection(); + + if (hours >= 8) { + this.game.showNotification({ + title: '⛏️ Automation Ready', + text: `${hours.toFixed(0)}h of mining ready to collect!`, + icon: '⛏️' + }); + } + } + } + + /** + * Daily update (called at midnight) + */ + onDayChange(day) { + console.log(`📅 Day ${day} - Running daily system updates...`); + + // Town growth check + this.townGrowthSystem.update(); + + // Restock shops + this.bakerySystem.restockInventory(); + + // Birthday cake deliveries + this.bakerySystem.checkBirthdayCakeDeliveries(); + } + + /** + * Save all systems state + */ + saveAllSystems() { + return { + version: '1.0', + timestamp: Date.now(), + systems: { + sleep: { + playerBed: this.sleepSystem.playerBed, + unlockedBeds: Object.entries(this.sleepSystem.bedTypes) + .filter(([_, bed]) => bed.unlocked) + .map(([key, _]) => key) + }, + crafting: { + currentTable: this.craftingSystem.currentTable.id, + unlockedRecipes: this.craftingSystem.unlockedRecipes, + largeTableUnlocked: this.craftingSystem.tables.LARGE.unlocked + }, + bakery: { + isUnlocked: this.bakerySystem.isUnlocked, + inventory: this.bakerySystem.inventory, + birthdayOrders: this.bakerySystem.birthdayCakeOrders + }, + barber: { + isUnlocked: this.barberSystem.isUnlocked, + playerAppearance: this.barberSystem.playerAppearance, + savedLooks: this.barberSystem.savedLooks, + visitCount: this.barberSystem.visitCount + }, + lawyer: { + isUnlocked: this.lawyerSystem.isUnlocked, + hasPrenup: this.lawyerSystem.hasPrenup, + divorceHistory: this.lawyerSystem.divorceHistory, + counselingInProgress: this.lawyerSystem.counselingInProgress + }, + zombieMiners: { + miners: this.zombieMinerSystem.zombieMiners, + equipment: this.zombieMinerSystem.zombieEquipment, + lastCollectionTime: this.zombieMinerSystem.lastCollectionTime + }, + townGrowth: { + population: this.townGrowthSystem.population, + populationSlots: this.townGrowthSystem.populationSlots, + villages: this.townGrowthSystem.villages, + services: this.townGrowthSystem.services + }, + npcPrivacy: { + npcHomes: this.npcPrivacySystem.npcHomes, + visitHistory: this.npcPrivacySystem.visitHistory, + lastVisitTimes: this.npcPrivacySystem.lastVisitTimes + } + } + }; + } + + /** + * Load all systems state + */ + loadAllSystems(saveData) { + if (!saveData || !saveData.systems) { + console.warn('No save data to load'); + return false; + } + + try { + const systems = saveData.systems; + + // Load sleep system + if (systems.sleep) { + systems.sleep.unlockedBeds.forEach(bedKey => { + this.sleepSystem.bedTypes[bedKey].unlocked = true; + }); + } + + // Load crafting system + if (systems.crafting) { + this.craftingSystem.tables.LARGE.unlocked = systems.crafting.largeTableUnlocked; + this.craftingSystem.unlockedRecipes = systems.crafting.unlockedRecipes || []; + } + + // Load bakery + if (systems.bakery) { + this.bakerySystem.isUnlocked = systems.bakery.isUnlocked; + this.bakerySystem.birthdayCakeOrders = systems.bakery.birthdayOrders || []; + } + + // Load barber + if (systems.barber) { + this.barberSystem.isUnlocked = systems.barber.isUnlocked; + this.barberSystem.playerAppearance = systems.barber.playerAppearance; + this.barberSystem.savedLooks = systems.barber.savedLooks || []; + this.barberSystem.visitCount = systems.barber.visitCount || 0; + } + + // Load lawyer + if (systems.lawyer) { + this.lawyerSystem.isUnlocked = systems.lawyer.isUnlocked; + this.lawyerSystem.hasPrenup = systems.lawyer.hasPrenup || false; + this.lawyerSystem.divorceHistory = systems.lawyer.divorceHistory || []; + this.lawyerSystem.counselingInProgress = systems.lawyer.counselingInProgress || false; + } + + // Load zombie miners + if (systems.zombieMiners) { + this.zombieMinerSystem.zombieMiners = systems.zombieMiners.miners || []; + this.zombieMinerSystem.zombieEquipment = systems.zombieMiners.equipment || {}; + this.zombieMinerSystem.lastCollectionTime = systems.zombieMiners.lastCollectionTime; + this.zombieMinerSystem.updateAutomationYield(); + } + + // Load town growth + if (systems.townGrowth) { + this.townGrowthSystem.population = systems.townGrowth.population; + this.townGrowthSystem.populationSlots = systems.townGrowth.populationSlots; + this.townGrowthSystem.villages = systems.townGrowth.villages || []; + this.townGrowthSystem.services = systems.townGrowth.services || {}; + } + + // Load NPC privacy + if (systems.npcPrivacy) { + this.npcPrivacySystem.npcHomes = systems.npcPrivacy.npcHomes || {}; + this.npcPrivacySystem.visitHistory = systems.npcPrivacy.visitHistory || {}; + this.npcPrivacySystem.lastVisitTimes = systems.npcPrivacy.lastVisitTimes || {}; + } + + console.log('✅ All systems loaded from save data'); + return true; + + } catch (error) { + console.error('❌ Error loading systems:', error); + return false; + } + } + + /** + * Get all systems status (for debug/admin panel) + */ + getAllSystemsStatus() { + return { + sleep: { + active: true, + currentBed: this.sleepSystem.playerBed.name, + isSleeping: this.sleepSystem.isSleeping + }, + crafting: { + active: true, + currentTable: this.craftingSystem.currentTable.name, + isCrafting: this.craftingSystem.isCrafting, + queueLength: this.craftingSystem.craftingQueue.length + }, + bakery: { + active: this.bakerySystem.isUnlocked, + isOpen: this.bakerySystem.isOpen, + stockLevel: Object.values(this.bakerySystem.inventory) + .reduce((sum, item) => sum + item.stock, 0) + }, + barber: { + active: this.barberSystem.isUnlocked, + isOpen: this.barberSystem.isOpen, + currentStyle: this.barberSystem.playerAppearance.hairstyle + }, + lawyer: { + active: this.lawyerSystem.isUnlocked, + counselingActive: this.lawyerSystem.counselingInProgress, + hasPrenup: this.lawyerSystem.hasPrenup + }, + zombieMiners: { + active: this.zombieMinerSystem.automationActive, + minerCount: this.zombieMinerSystem.zombieMiners.length, + yieldPerHour: this.zombieMinerSystem.totalYieldPerHour + }, + townGrowth: { + active: true, + population: this.townGrowthSystem.population, + maxPopulation: this.townGrowthSystem.maxPopulation, + status: this.townGrowthSystem.getTownStatus() + }, + npcPrivacy: { + active: true, + homesGenerated: Object.keys(this.npcPrivacySystem.npcHomes).length + } + }; + } +} + +export default MasterGameSystemsManager; diff --git a/src/systems/NPCPrivacySystem.js b/src/systems/NPCPrivacySystem.js new file mode 100644 index 000000000..374a21364 --- /dev/null +++ b/src/systems/NPCPrivacySystem.js @@ -0,0 +1,454 @@ +/** + * NPC PRIVACY & HOME DECORATION SYSTEM + * Part of: Game Systems Expansion + * Created: January 4, 2026 + * + * Features: + * - Hobby-based automatic interior generation + * - Door lock system (heart-based access) + * - NPC home visit mechanics + * - Relationship effects from visits + * - Privacy violations & consequences + */ + +export class NPCPrivacySystem { + constructor(game) { + this.game = game; + this.player = game.player; + + // Privacy settings + this.privacyLevels = { + PUBLIC: 0, // Anyone can enter (living room, kitchen) + FRIENDLY: 3, // 3+ hearts required + PRIVATE: 5, // 5+ hearts required (bedroom) + INTIMATE: 8, // 8+ hearts required (special rooms) + LOCKED: 10 // 10 hearts or marriage required + }; + + // Visit tracking + this.visitHistory = {}; + this.lastVisitTimes = {}; + + // NPC home decorations (generated based on hobby) + this.npcHomes = {}; + } + + /** + * Generate NPC home interior based on hobby + */ + generateNPCHome(npcId) { + const npc = this.game.npcs.get(npcId); + if (!npc) return null; + + const hobby = npc.hobby; + + // Base home structure + const home = { + npcId: npcId, + rooms: { + living_room: { + name: 'Living Room', + privacyLevel: this.privacyLevels.PUBLIC, + objects: this.generateLivingRoomObjects(hobby) + }, + kitchen: { + name: 'Kitchen', + privacyLevel: this.privacyLevels.PUBLIC, + objects: this.generateKitchenObjects(hobby) + }, + bedroom: { + name: 'Bedroom', + privacyLevel: this.privacyLevels.PRIVATE, + objects: this.generateBedroomObjects(hobby) + }, + hobby_room: { + name: this.getHobbyRoomName(hobby), + privacyLevel: this.privacyLevels.FRIENDLY, + objects: this.generateHobbyRoomObjects(hobby) + } + }, + visitCount: 0, + lastVisit: null + }; + + // Store home + this.npcHomes[npcId] = home; + + return home; + } + + /** + * Generate living room objects + */ + generateLivingRoomObjects(hobby) { + const base = [ + 'interior_table_small', + 'interior_bookshelf', + 'interior_gothic_lantern' + ]; + + // Hobby-specific additions + const hobbyAdditions = { + fishing: ['mounted_fish', 'fishing_net'], + farming: ['grain_sack', 'tool_rack'], + cooking: ['recipe_shelf'], + reading: ['bookshelf', 'reading_chair'], + music: ['guitar_stand', 'music_sheets'], + crafting: ['crafting_workshop'] + }; + + return [...base, ...(hobbyAdditions[hobby] || [])]; + } + + /** + * Generate kitchen objects + */ + generateKitchenObjects(hobby) { + const base = [ + 'interior_kitchen_stove', + 'interior_kitchen_counter' + ]; + + if (hobby === 'cooking') { + base.push( + 'interior_kitchen_fridge', + 'interior_kitchen_sink', + 'interior_recipe_shelf' + ); + } + + return base; + } + + /** + * Generate bedroom objects + */ + generateBedroomObjects(hobby) { + const base = [ + 'interior_bed_wooden', + 'interior_wardrobe', + 'interior_chest_locked' + ]; + + // Personal items based on hobby + const hobbyItems = { + zombie_worker: ['brain_jar', 'work_uniform'], + baker: ['flour_workspace', 'dough_tools'], + barber: ['dreadlock_kit', 'styling_tools'], + fisherman: ['tackle_box', 'fish_collection'] + }; + + return [...base, ...(hobbyItems[hobby] || [])]; + } + + /** + * Generate hobby room objects + */ + generateHobbyRoomObjects(hobby) { + const hobbyRooms = { + fishing: [ + 'fishing_rod_rack', + 'bait_storage', + 'fish_tank', + 'mounted_trophy_fish' + ], + zombie_worker: [ + 'brain_jar', + 'work_bench', + 'tool_storage', + 'miner_equipment' + ], + baker: [ + 'flour_workspace', + 'mixing_bowls', + 'bread_storage', + 'recipe_collection' + ], + alchemy: [ + 'interior_alchemy_bottle', + 'interior_brewing_cauldron', + 'interior_chemistry_set', + 'interior_potion_shelf' + ], + crafting: [ + 'interior_crafting_workshop', + 'interior_tool_rack', + 'material_storage', + 'blueprint_table' + ], + reading: [ + 'interior_bookshelf', + 'reading_chair', + 'ancient_manuscripts', + 'writing_desk' + ] + }; + + return hobbyRooms[hobby] || ['generic_workspace']; + } + + /** + * Get hobby room name + */ + getHobbyRoomName(hobby) { + const names = { + fishing: 'Fishing Den', + zombie_worker: 'Worker\'s Quarters', + baker: 'Baking Workshop', + alchemy: 'Alchemy Lab', + crafting: 'Craft Room', + reading: 'Library', + music: 'Music Studio', + farming: 'Storage Shed' + }; + + return names[hobby] || 'Hobby Room'; + } + + /** + * Attempt to enter NPC room + */ + attemptEntry(npcId, roomId) { + const npc = this.game.npcs.get(npcId); + if (!npc) { + return { success: false, message: 'NPC not found!' }; + } + + // Generate home if doesn't exist + if (!this.npcHomes[npcId]) { + this.generateNPCHome(npcId); + } + + const home = this.npcHomes[npcId]; + const room = home.rooms[roomId]; + + if (!room) { + return { success: false, message: 'Room not found!' }; + } + + // Check privacy level + const playerHearts = this.player.getRelationshipHearts(npcId); + const requiredHearts = room.privacyLevel; + + if (playerHearts < requiredHearts) { + // Privacy violation! + return this.handlePrivacyViolation(npcId, roomId, requiredHearts); + } + + // Allowed entry + return this.handleSuccessfulEntry(npcId, roomId); + } + + /** + * Handle privacy violation + */ + handlePrivacyViolation(npcId, roomId, requiredHearts) { + const npc = this.game.npcs.get(npcId); + + // Relationship penalty + const penalty = (requiredHearts - this.player.getRelationshipHearts(npcId)) * 20; + npc.addRelationshipPoints(-penalty); + + // NPC reaction + const reactions = [ + "Hey! That's private!", + "What are you doing in my room?!", + "Get out! This is MY space!", + "I can't believe you just walked in here...", + "Privacy, please!" + ]; + + const reaction = Phaser.Utils.Array.GetRandom(reactions); + + this.game.showDialogue(npc.name, reaction, { + mood: 'angry', + animation: 'shocked' + }); + + this.game.showMessage( + `${npc.name} is upset! -${penalty} relationship points` + ); + + // Player gets kicked out + this.game.player.teleportToLocation('outside_' + npcId + '_home'); + + return { + success: false, + privacyViolation: true, + penalty: penalty, + requiredHearts: requiredHearts, + currentHearts: this.player.getRelationshipHearts(npcId) + }; + } + + /** + * Handle successful entry + */ + handleSuccessfulEntry(npcId, roomId) { + const npc = this.game.npcs.get(npcId); + const home = this.npcHomes[npcId]; + + // Track visit + if (!this.visitHistory[npcId]) { + this.visitHistory[npcId] = []; + } + + const visit = { + roomId: roomId, + timestamp: this.game.time.currentTime, + timeOfDay: this.game.time.getTimeOfDay() + }; + + this.visitHistory[npcId].push(visit); + this.lastVisitTimes[npcId] = this.game.time.currentTime; + + home.visitCount++; + home.lastVisit = this.game.time.currentTime; + + // Relationship effects based on visit + this.applyVisitEffects(npcId, roomId, visit); + + // Enter room scene + this.game.scene.start('NPCRoomScene', { + npcId: npcId, + roomId: roomId, + room: home.rooms[roomId] + }); + + return { + success: true, + room: home.rooms[roomId], + npc: npc + }; + } + + /** + * Apply relationship effects from visit + */ + applyVisitEffects(npcId, roomId, visit) { + const npc = this.game.npcs.get(npcId); + const timeOfDay = visit.timeOfDay; + + // Visit frequency check + const recentVisits = this.visitHistory[npcId].filter(v => { + const hoursSince = (this.game.time.currentTime - v.timestamp) / 3600; + return hoursSince < 24; // Last 24 hours + }); + + if (recentVisits.length > 3) { + // Visiting TOO much = annoying + npc.addRelationshipPoints(-10); + this.game.showMessage( + `${npc.name} seems a bit annoyed by frequent visits...` + ); + return; + } + + // Time of day effects + if (timeOfDay === 'night' && roomId === 'bedroom') { + // Visiting bedroom at night = awkward + npc.addRelationshipPoints(-5); + this.game.showDialogue( + npc.name, + "It's quite late... Is everything okay?", + { mood: 'concerned' } + ); + } else if (timeOfDay === 'morning' && roomId === 'kitchen') { + // Morning kitchen visit = breakfast together! + npc.addRelationshipPoints(5); + this.game.showDialogue( + npc.name, + "Good morning! Care to join me for breakfast?", + { mood: 'happy' } + ); + } else if (roomId === 'hobby_room') { + // Showing interest in hobby = bonus points! + npc.addRelationshipPoints(10); + this.game.showDialogue( + npc.name, + "I'm glad you're interested in my hobby!", + { mood: 'excited' } + ); + } + } + + /** + * Get visit statistics for NPC + */ + getVisitStats(npcId) { + const visits = this.visitHistory[npcId] || []; + + // Count visits per room + const roomCounts = {}; + visits.forEach(visit => { + roomCounts[visit.roomId] = (roomCounts[visit.roomId] || 0) + 1; + }); + + // Average visits per day + const daysSinceFirstVisit = visits.length > 0 + ? (this.game.time.currentTime - visits[0].timestamp) / 86400 + : 0; + + const avgVisitsPerDay = daysSinceFirstVisit > 0 + ? visits.length / daysSinceFirstVisit + : 0; + + return { + totalVisits: visits.length, + roomCounts: roomCounts, + avgVisitsPerDay: avgVisitsPerDay, + lastVisit: this.lastVisitTimes[npcId], + favoriteRoom: Object.keys(roomCounts).reduce((a, b) => + roomCounts[a] > roomCounts[b] ? a : b, null) + }; + } + + /** + * Check if player can access special room + */ + canAccessSpecialRoom(npcId, roomId) { + const npc = this.game.npcs.get(npcId); + if (!npc) return false; + + const home = this.npcHomes[npcId]; + if (!home) return false; + + const room = home.rooms[roomId]; + if (!room) return false; + + const playerHearts = this.player.getRelationshipHearts(npcId); + + // Special case: marriage allows LOCKED access + if (room.privacyLevel === this.privacyLevels.LOCKED) { + return this.player.spouse === npcId; + } + + return playerHearts >= room.privacyLevel; + } + + /** + * Get NPC home UI data + */ + getNPCHomeUIData(npcId) { + if (!this.npcHomes[npcId]) { + this.generateNPCHome(npcId); + } + + const home = this.npcHomes[npcId]; + const npc = this.game.npcs.get(npcId); + + return { + npc: npc, + home: home, + accessibleRooms: Object.keys(home.rooms).filter(roomId => + this.canAccessSpecialRoom(npcId, roomId) + ), + lockedRooms: Object.keys(home.rooms).filter(roomId => + !this.canAccessSpecialRoom(npcId, roomId) + ), + visitStats: this.getVisitStats(npcId) + }; + } +} + +export default NPCPrivacySystem; diff --git a/src/systems/TownGrowthSystem.js b/src/systems/TownGrowthSystem.js new file mode 100644 index 000000000..f9c49e5b8 --- /dev/null +++ b/src/systems/TownGrowthSystem.js @@ -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;