diff --git a/src/systems/BarberShopSystem.js b/src/systems/BarberShopSystem.js new file mode 100644 index 000000000..86b8d28c3 --- /dev/null +++ b/src/systems/BarberShopSystem.js @@ -0,0 +1,663 @@ +/** + * BARBER SHOP SYSTEM - Character Customization & Styling + * Part of: New Town Buildings + * Created: January 4, 2026 + * + * Features: + * - Hairstyle changes (dreadlocks, mohawk, long hair, ponytail, bald) + * - Piercings & body modifications (ear gauges, nose ring, eyebrow, lip) + * - Color customization (hair dye, clothing) + * - Zombie makeover (cosmetic for workers) + * - NPC customization (change other characters) + * - Save/load favorite looks + */ + +export class BarberShopSystem { + constructor(game) { + this.game = game; + this.player = game.player; + + // Barber shop status + this.isUnlocked = false; + this.isOpen = false; + this.barber = null; + + // Hairstyle catalog + this.hairstyles = this.initializeHairstyles(); + + // Piercing catalog + this.piercings = this.initializePiercings(); + + // Hair dye colors + this.dyeColors = this.initializeDyeColors(); + + // Player's current appearance + this.playerAppearance = { + hairstyle: 'default', + hairColor: 'brown', + piercings: [], + clothing: { + shirt: 'default', + pants: 'default', + colorShirt: 'blue', + colorPants: 'brown' + } + }; + + // Saved looks (5 slots) + this.savedLooks = [null, null, null, null, null]; + + // Visit counter (for discounts) + this.visitCount = 0; + this.lastVisitDate = null; + } + + /** + * Initialize hairstyle catalog + */ + initializeHairstyles() { + return { + default: { + id: 'default', + name: 'Default Hair', + price: 0, + unlocked: true, + sprite: 'hair_default', + description: 'Your natural hair.' + }, + dreadlocks_pink_green: { + id: 'dreadlocks_pink_green', + name: 'Pink & Green Dreadlocks', + price: 500, + unlocked: true, + signature: 'kai', // Kai's signature style + sprite: 'hair_dreadlocks_pg', + description: 'Kai\'s iconic pink and green dreads!' + }, + mohawk_blue: { + id: 'mohawk_blue', + name: 'Blue Mohawk', + price: 300, + unlocked: true, + sprite: 'hair_mohawk_blue', + description: 'Stand tall with this punk mohawk.' + }, + mohawk_purple: { + id: 'mohawk_purple', + name: 'Purple Mohawk', + price: 300, + unlocked: true, + sprite: 'hair_mohawk_purple', + description: 'Purple power!' + }, + mohawk_red: { + id: 'mohawk_red', + name: 'Red Mohawk', + price: 300, + unlocked: true, + sprite: 'hair_mohawk_red', + description: 'Fiery red mohawk.' + }, + long_hair: { + id: 'long_hair', + name: 'Long Hair', + price: 400, + unlocked: true, + dyeable: true, + sprite: 'hair_long', + description: 'Flowing long hair, can be dyed any color.' + }, + ponytail: { + id: 'ponytail', + name: 'Ponytail', + price: 250, + unlocked: true, + dyeable: true, + sprite: 'hair_ponytail', + description: 'Practical and stylish.' + }, + bald: { + id: 'bald', + name: 'Bald', + price: 100, + unlocked: true, + reversible: true, // Can revert for free + sprite: 'hair_none', + description: 'Clean shaven head. Can reverse for FREE!' + } + }; + } + + /** + * Initialize piercing catalog + */ + initializePiercings() { + return { + ear_gauges: { + id: 'ear_gauges', + name: 'Ear Gauges', + price: 200, + unlocked: true, + signature: 'kai', // Kai's trademark + sprite: 'piercing_gauges', + description: 'Large ear gauges like Kai\'s!' + }, + nose_ring: { + id: 'nose_ring', + name: 'Nose Ring', + price: 150, + unlocked: true, + sprite: 'piercing_nose', + description: 'Simple nose ring.' + }, + eyebrow_piercing: { + id: 'eyebrow_piercing', + name: 'Eyebrow Piercing', + price: 100, + unlocked: true, + sprite: 'piercing_eyebrow', + description: 'Edgy eyebrow piercing.' + }, + lip_ring: { + id: 'lip_ring', + name: 'Lip Ring', + price: 150, + unlocked: true, + sprite: 'piercing_lip', + description: 'Lip ring for that punk look.' + }, + multiple_ear: { + id: 'multiple_ear', + name: 'Multiple Ear Piercings', + price: 50, + stackable: true, // Can have multiple + sprite: 'piercing_ear_multiple', + description: 'Each additional piercing costs 50g.' + } + }; + } + + /** + * Initialize hair dye colors + */ + initializeDyeColors() { + return { + brown: { name: 'Brown', price: 0, hex: '#654321' }, + black: { name: 'Black', price: 100, hex: '#000000' }, + blonde: { name: 'Blonde', price: 150, hex: '#FFD700' }, + red: { name: 'Red', price: 200, hex: '#FF0000' }, + blue: { name: 'Blue', price: 200, hex: '#0000FF' }, + purple: { name: 'Purple', price: 200, hex: '#800080' }, + pink: { name: 'Pink', price: 200, hex: '#FF69B4' }, + green: { name: 'Green', price: 200, hex: '#00FF00' }, + white: { name: 'White', price: 250, hex: '#FFFFFF' } + }; + } + + /** + * Unlock barber shop + */ + unlockBarberShop() { + // Check requirements + const npcCount = this.game.npcs.getPopulationCount(); + if (npcCount < 3) { + return { + success: false, + message: 'Need at least 3 NPCs in town first!' + }; + } + + if (!this.player.hasCompletedQuest('style_matters')) { + return { + success: false, + message: 'Complete "Style Matters" quest first!' + }; + } + + if (!this.player.hasMaterials({ wood: 75, iron: 25 })) { + return { + success: false, + message: 'Need 75 Wood + 25 Iron to build barber shop!' + }; + } + + if (this.player.money < 6000) { + return { + success: false, + message: 'Need 6,000g to build barber shop!' + }; + } + + // Build barber shop + this.player.removeMaterials({ wood: 75, iron: 25 }); + this.player.money -= 6000; + this.isUnlocked = true; + this.isOpen = true; + + // Spawn barber NPC + this.barber = this.game.npcs.spawn('barber', { + name: 'Razor', + position: { x: 1400, y: 900 }, + appearance: { + hairstyle: 'mohawk_purple', + piercings: ['ear_gauges', 'nose_ring', 'lip_ring'] + } + }); + + this.game.showMessage('💇 Barber Shop is now open!'); + + return { success: true }; + } + + /** + * Change hairstyle + */ + changeHairstyle(hairstyleId) { + const hairstyle = this.hairstyles[hairstyleId]; + + if (!hairstyle) { + return { success: false, message: 'Hairstyle not found!' }; + } + + // Check if shop is open + if (!this.isOpen) { + return { success: false, message: 'Barber shop is closed!' }; + } + + // Check price (with discount) + const discount = this.getVisitDiscount(); + const price = Math.floor(hairstyle.price * (1 - discount)); + + // Free reversal from bald + if (hairstyle.reversible && this.playerAppearance.hairstyle === 'bald') { + // Revert to previous hairstyle for free + this.playerAppearance.hairstyle = this.previousHairstyle || 'default'; + this.game.showMessage('Hair restored for FREE!'); + return { success: true, price: 0 }; + } + + // Check money + if (this.player.money < price) { + return { + success: false, + message: `Not enough money! Need ${price}g` + }; + } + + // Save previous hairstyle (for bald reversal) + this.previousHairstyle = this.playerAppearance.hairstyle; + + // Change hairstyle + this.player.money -= price; + this.playerAppearance.hairstyle = hairstyleId; + + // Update sprite + this.updatePlayerSprite(); + + // Track visit + this.incrementVisitCount(); + + let message = `Changed to ${hairstyle.name}! ${price}g`; + if (discount > 0) { + message += ` (${Math.floor(discount * 100)}% repeat customer discount!)`; + } + + this.game.showMessage(message); + + return { success: true, price: price, discount: discount }; + } + + /** + * Add piercing + */ + addPiercing(piercingId) { + const piercing = this.piercings[piercingId]; + + if (!piercing) { + return { success: false, message: 'Piercing not found!' }; + } + + // Check if already have (unless stackable) + if (!piercing.stackable && this.playerAppearance.piercings.includes(piercingId)) { + return { + success: false, + message: 'You already have this piercing!' + }; + } + + // Check price + const price = piercing.price; + if (this.player.money < price) { + return { + success: false, + message: `Not enough money! Need ${price}g` + }; + } + + // Add piercing + this.player.money -= price; + + if (piercing.stackable) { + // Count how many of this type + const count = this.playerAppearance.piercings.filter(p => p === piercingId).length; + this.playerAppearance.piercings.push(`${piercingId}_${count + 1}`); + } else { + this.playerAppearance.piercings.push(piercingId); + } + + // Update sprite + this.updatePlayerSprite(); + + // Track visit + this.incrementVisitCount(); + + this.game.showMessage(`Added ${piercing.name}! ${price}g`); + + return { success: true, price: price }; + } + + /** + * Remove piercing + */ + removePiercing(piercingId) { + const index = this.playerAppearance.piercings.indexOf(piercingId); + + if (index === -1) { + return { success: false, message: 'You don\'t have this piercing!' }; + } + + // Remove free of charge + this.playerAppearance.piercings.splice(index, 1); + + // Update sprite + this.updatePlayerSprite(); + + this.game.showMessage('Piercing removed (free).'); + + return { success: true }; + } + + /** + * Dye hair color + */ + dyeHair(colorId) { + const color = this.dyeColors[colorId]; + + if (!color) { + return { success: false, message: 'Color not found!' }; + } + + // Check if current hairstyle is dyeable + const currentHairstyle = this.hairstyles[this.playerAppearance.hairstyle]; + if (!currentHairstyle.dyeable && currentHairstyle.id !== 'default') { + return { + success: false, + message: 'This hairstyle cannot be dyed!' + }; + } + + // Check price + if (this.player.money < color.price) { + return { + success: false, + message: `Not enough money! Need ${color.price}g` + }; + } + + // Dye hair + this.player.money -= color.price; + this.playerAppearance.hairColor = colorId; + + // Update sprite + this.updatePlayerSprite(); + + // Track visit + this.incrementVisitCount(); + + this.game.showMessage(`Hair dyed ${color.name}! ${color.price}g`); + + return { success: true, price: color.price }; + } + + /** + * Dye clothing + */ + dyeClothing(clothingPart, colorId) { + const color = this.dyeColors[colorId]; + + if (!color) { + return { success: false, message: 'Color not found!' }; + } + + // Clothing dye is cheaper (half price) + const price = Math.floor(color.price / 2); + + if (this.player.money < price) { + return { + success: false, + message: `Not enough money! Need ${price}g` + }; + } + + // Dye clothing + this.player.money -= price; + + if (clothingPart === 'shirt') { + this.playerAppearance.clothing.colorShirt = colorId; + } else if (clothingPart === 'pants') { + this.playerAppearance.clothing.colorPants = colorId; + } + + // Update sprite + this.updatePlayerSprite(); + + this.game.showMessage(`${clothingPart} dyed ${color.name}! ${price}g`); + + return { success: true, price: price }; + } + + /** + * Zombie makeover (cosmetic for workers) + */ + zombieMakeover(zombieId) { + const zombie = this.game.zombieWorkers.get(zombieId); + + if (!zombie) { + return { success: false, message: 'Zombie not found!' }; + } + + const price = 1000; + if (this.player.money < price) { + return { + success: false, + message: 'Zombie makeover costs 1,000g!' + }; + } + + // Apply makeover + this.player.money -= price; + + // Random fancy appearance for zombie + const fancyStyles = ['mohawk_blue', 'mohawk_purple', 'dreadlocks_pink_green']; + const style = Phaser.Utils.Array.GetRandom(fancyStyles); + + zombie.appearance = { + hairstyle: style, + accessories: ['fancy_bow_tie', 'top_hat'] + }; + + // Update zombie sprite + this.game.zombieWorkers.updateZombieSprite(zombieId); + + // Loyalty bonus (zombies appreciate looking good!) + zombie.addLoyalty(20); + + this.game.showMessage(`${zombie.name} looks fabulous! +20 Loyalty! 1,000g`); + + return { success: true, price: price }; + } + + /** + * Customize NPC appearance + */ + customizeNPC(npcId, changes) { + const npc = this.game.npcs.get(npcId); + + if (!npc) { + return { success: false, message: 'NPC not found!' }; + } + + // Check relationship (need 5+ hearts) + if (this.player.getRelationshipLevel(npcId) < 5) { + return { + success: false, + message: `Need 5+ hearts with ${npc.name} first!` + }; + } + + const price = 500; + if (this.player.money < price) { + return { + success: false, + message: 'NPC customization costs 500g!' + }; + } + + // Apply changes + this.player.money -= price; + + if (changes.hairstyle) { + npc.appearance.hairstyle = changes.hairstyle; + } + if (changes.hairColor) { + npc.appearance.hairColor = changes.hairColor; + } + + // Update NPC sprite + this.game.npcs.updateNPCSprite(npcId); + + // NPC reaction + npc.addRelationshipPoints(10); + this.game.showDialogue(npc.name, "Wow, I love it! Thank you!"); + + this.game.showMessage(`Styled ${npc.name}! +10 ❤️ 500g`); + + return { success: true, price: price }; + } + + /** + * Save current look to slot + */ + saveLook(slotIndex) { + if (slotIndex < 0 || slotIndex >= 5) { + return { success: false, message: 'Invalid slot!' }; + } + + this.savedLooks[slotIndex] = { + hairstyle: this.playerAppearance.hairstyle, + hairColor: this.playerAppearance.hairColor, + piercings: [...this.playerAppearance.piercings], + clothing: { ...this.playerAppearance.clothing }, + name: `Look ${slotIndex + 1}` + }; + + this.game.showMessage(`Saved look to slot ${slotIndex + 1}!`); + + return { success: true }; + } + + /** + * Load saved look from slot + */ + loadLook(slotIndex) { + if (slotIndex < 0 || slotIndex >= 5) { + return { success: false, message: 'Invalid slot!' }; + } + + const savedLook = this.savedLooks[slotIndex]; + if (!savedLook) { + return { success: false, message: 'No look saved in this slot!' }; + } + + // Apply saved look (free!) + this.playerAppearance = { + hairstyle: savedLook.hairstyle, + hairColor: savedLook.hairColor, + piercings: [...savedLook.piercings], + clothing: { ...savedLook.clothing } + }; + + // Update sprite + this.updatePlayerSprite(); + + this.game.showMessage(`Loaded ${savedLook.name}!`); + + return { success: true }; + } + + /** + * Get visit discount (repeat customers) + */ + getVisitDiscount() { + // 5+ visits = 10% off + if (this.visitCount >= 5) { + return 0.10; + } + return 0; + } + + /** + * Increment visit count + */ + incrementVisitCount() { + const today = this.game.time.currentDate; + + // Only count once per day + if (this.lastVisitDate !== today) { + this.visitCount++; + this.lastVisitDate = today; + } + } + + /** + * Update player sprite with new appearance + */ + updatePlayerSprite() { + // Emit event for sprite manager to update + this.game.emit('playerAppearanceChanged', { + appearance: this.playerAppearance + }); + + // Rebuild player sprite + this.player.rebuildSprite(this.playerAppearance); + } + + /** + * Get preview of appearance change + */ + getPreview(changes) { + return { + ...this.playerAppearance, + ...changes + }; + } + + /** + * Get shop UI data + */ + getShopUIData() { + return { + isUnlocked: this.isUnlocked, + isOpen: this.isOpen, + hairstyles: this.hairstyles, + piercings: this.piercings, + dyeColors: this.dyeColors, + currentAppearance: this.playerAppearance, + savedLooks: this.savedLooks, + visitDiscount: this.getVisitDiscount(), + barber: this.barber + }; + } +} + +export default BarberShopSystem; diff --git a/src/systems/LawyerOfficeSystem.js b/src/systems/LawyerOfficeSystem.js new file mode 100644 index 000000000..93128ce05 --- /dev/null +++ b/src/systems/LawyerOfficeSystem.js @@ -0,0 +1,554 @@ +/** + * LAWYER OFFICE SYSTEM - Divorce & Marriage Legal Services + * Part of: New Town Buildings ("Drama Hub") + * Created: January 4, 2026 + * + * Features: + * - Divorce processing (50,000g + 25% money loss) + * - Prenuptial agreement system (preventive, 10,000g) + * - Marriage counseling (alternative to divorce, 5,000g) + * - Relationship crisis detection + * - Post-divorce quests + */ + +export class LawyerOfficeSystem { + constructor(game) { + this.game = game; + this.player = game.player; + + // Lawyer office status + this.isUnlocked = false; + this.lawyer = null; + + // Divorce costs + this.divorceCost = 50000; + this.divorceMoneyLoss = 0.25; // 25% of remaining money + this.divorceMoneyLossWithPrenup = 0.10; // 10% with prenup + + // Prenup system + this.hasPrenup = false; + this.prenupCost = 10000; + + // Marriage counseling + this.counselingCost = 5000; + this.counselingInProgress = false; + this.counselingTasksCompleted = 0; + this.counselingTasksRequired = 3; + + // Divorce history + this.divorceHistory = []; + this.hasEverDivorced = false; + } + + /** + * Auto-unlock lawyer office when marriage is in crisis + */ + checkAutoUnlock() { + // Auto-unlock if married AND relationship ≤ 3 hearts + if (this.player.isMarried) { + const spouse = this.game.npcs.getSpouse(); + if (spouse && spouse.relationshipHearts <= 3) { + this.unlockLawyerOffice(); + } + } + } + + /** + * Unlock lawyer office + */ + unlockLawyerOffice() { + if (this.isUnlocked) return; + + // Auto-build (15,000g automatically deducted) + if (this.player.money < 15000) { + // Build on credit (lawyer is greedy!) + this.player.money = Math.max(0, this.player.money - 15000); + this.player.debt = Math.abs(Math.min(0, this.player.money)); + } else { + this.player.money -= 15000; + } + + this.isUnlocked = true; + + // Spawn lawyer NPC + this.lawyer = this.game.npcs.spawn('lawyer', { + name: 'Mr. Sterling', + position: { x: 1600, y: 1000 }, + appearance: { + clothing: 'black_suit', + demeanor: 'cold_professional' + }, + dialogue: this.getLawyerDialogue() + }); + + // Trigger ominous quest + this.game.quests.start('till_death_do_us_part'); + + this.game.showMessage('⚖️ Lawyer Office has been built...'); + + return { success: true }; + } + + /** + * Get lawyer dialogue + */ + getLawyerDialogue() { + return { + greeting: [ + "Marriage is a contract. Divorce is... expensive.", + "Love is temporary. Legal documents are forever.", + "Here for business or pleasure? I only deal in business." + ], + divorce_warning: [ + "Are you ABSOLUTELY sure? This will cost you everything.", + "Divorce is final. There's no going back.", + "I've seen many regret this decision. Proceed?" + ], + prenup: [ + "Smart move. Protect your assets before it's too late.", + "A prenup? Planning ahead, I see. Wise.", + "Marriage without a prenup is... optimistic." + ], + counseling: [ + "There may be another way. Have you tried talking?", + "Marriage counseling. Less expensive than divorce.", + "Fix it now or pay later. Your choice." + ], + post_divorce: [ + "Single again. How does freedom feel?", + "Expensive lesson learned, I hope.", + "The papers are filed. You're free to go." + ] + }; + } + + /** + * Purchase prenuptial agreement (BEFORE marriage only!) + */ + purchasePrenup() { + // Can only buy BEFORE getting married + if (this.player.isMarried) { + return { + success: false, + message: 'Too late! Prenups must be signed BEFORE marriage!' + }; + } + + if (this.hasPrenup) { + return { + success: false, + message: 'You already have a prenup!' + }; + } + + if (this.player.money < this.prenupCost) { + return { + success: false, + message: `Need ${this.prenupCost}g for a prenup!` + }; + } + + // Purchase + this.player.money -= this.prenupCost; + this.hasPrenup = true; + + this.game.showMessage( + `Prenup signed! Divorce money loss reduced to 10%. ${this.prenupCost}g` + ); + + return { success: true }; + } + + /** + * Initiate divorce process + */ + initiateDivorce() { + // Check if married + if (!this.player.isMarried) { + return { + success: false, + message: 'You\'re not married!' + }; + } + + const spouse = this.game.npcs.getSpouse(); + + // Show divorce confirmation with full consequences + const moneyLossPercent = this.hasPrenup ? 10 : 25; + const estimatedLoss = Math.floor(this.player.money * (this.hasPrenup ? 0.10 : 0.25)); + + const divorceInfo = { + cost: this.divorceCost, + moneyLoss: estimatedLoss, + moneyLossPercent: moneyLossPercent, + spouse: spouse.name, + consequences: [ + `Divorce fee: ${this.divorceCost}g`, + `Money loss: ${moneyLossPercent}% (${estimatedLoss}g)`, + `${spouse.name} relationship reset to 0 hearts`, + `${spouse.name} moves out of farmhouse`, + 'Lose "married" status benefits', + 'King-size bed reverts to single use' + ] + }; + + // Emit event for UI to show confirmation dialog + this.game.emit('divorceConfirmationRequired', divorceInfo); + + return { + success: false, // Not completed yet, waiting for confirmation + requiresConfirmation: true, + divorceInfo: divorceInfo + }; + } + + /** + * Confirm and process divorce (after player confirms) + */ + processDivorce() { + if (!this.player.isMarried) { + return { success: false, message: 'Not married!' }; + } + + const spouse = this.game.npcs.getSpouse(); + + // Calculate total cost + const divorceFee = this.divorceCost; + const moneyLossPercent = this.hasPrenup ? this.divorceMoneyLossWithPrenup : this.divorceMoneyLoss; + + // Check if can afford divorce fee + if (this.player.money < divorceFee) { + return { + success: false, + message: `Not enough money! Divorce costs ${divorceFee}g!` + }; + } + + // Deduct divorce fee + this.player.money -= divorceFee; + + // Calculate and deduct money loss + const moneyLoss = Math.floor(this.player.money * moneyLossPercent); + this.player.money -= moneyLoss; + + // Reset relationship + const previousHearts = spouse.relationshipHearts; + spouse.relationshipHearts = 0; + spouse.relationshipPoints = 0; + + // Spouse moves out + spouse.homeLocation = spouse.originalHome; + spouse.isLivingWithPlayer = false; + + // Remove married status + this.player.isMarried = false; + this.player.spouse = null; + + // Downgrade bed + if (this.game.sleepSystem) { + const kingSizeBed = this.game.sleepSystem.bedTypes.KING_SIZE_BED; + kingSizeBed.spousePresent = false; + } + + // Record divorce in history + this.divorceHistory.push({ + spouse: spouse.name, + date: this.game.time.currentDate, + costTotal: divorceFee + moneyLoss, + heartsLost: previousHearts + }); + + this.hasEverDivorced = true; + + // Trigger post-divorce quest + this.game.quests.start('single_again'); + + // Spouse reaction (they're devastated) + this.game.showDialogue( + spouse.name, + "I can't believe this is happening... Goodbye.", + { + mood: 'heartbroken', + animation: 'crying' + } + ); + + // Show final message + this.game.showMessage( + `Divorced ${spouse.name}. Total cost: ${divorceFee + moneyLoss}g. You are now single.` + ); + + // Lawyer's cold response + setTimeout(() => { + this.game.showDialogue( + this.lawyer.name, + "The papers are filed. You're free to go. That'll be " + divorceFee + "g." + ); + }, 3000); + + return { + success: true, + totalCost: divorceFee + moneyLoss, + moneyLoss: moneyLoss, + spouse: spouse.name + }; + } + + /** + * Start marriage counseling (alternative to divorce) + */ + startCounseling() { + if (!this.player.isMarried) { + return { + success: false, + message: 'You\'re not married!' + }; + } + + if (this.counselingInProgress) { + return { + success: false, + message: 'Counseling already in progress!' + }; + } + + if (this.player.money < this.counselingCost) { + return { + success: false, + message: `Marriage counseling costs ${this.counselingCost}g!` + }; + } + + // Pay for counseling + this.player.money -= this.counselingCost; + this.counselingInProgress = true; + this.counselingTasksCompleted = 0; + + const spouse = this.game.npcs.getSpouse(); + + // Generate counseling tasks + this.counselingTasks = this.generateCounselingTasks(spouse); + + // Start counseling quest + this.game.quests.start('marriage_counseling', { + tasks: this.counselingTasks + }); + + this.game.showMessage( + `Marriage counseling started! Complete 3 tasks to save your marriage. ${this.counselingCost}g` + ); + + return { + success: true, + tasks: this.counselingTasks + }; + } + + /** + * Generate counseling tasks based on spouse + */ + generateCounselingTasks(spouse) { + const tasks = [ + { + id: 'gift_favorite', + description: `Give ${spouse.name} their favorite gift`, + requirement: { + type: 'gift', + item: spouse.favoriteGift, + target: spouse.id + }, + completed: false + }, + { + id: 'date_night', + description: `Take ${spouse.name} on a date to their favorite location`, + requirement: { + type: 'visit_location', + location: spouse.favoriteLocation, + withNPC: spouse.id + }, + completed: false + }, + { + id: 'heart_to_heart', + description: `Have a deep conversation with ${spouse.name}`, + requirement: { + type: 'dialogue', + dialogueId: 'heart_to_heart', + target: spouse.id + }, + completed: false + } + ]; + + return tasks; + } + + /** + * Complete counseling task + */ + completeCounselingTask(taskId) { + if (!this.counselingInProgress) { + return { success: false }; + } + + const task = this.counselingTasks.find(t => t.id === taskId); + if (!task || task.completed) { + return { success: false }; + } + + // Mark task as completed + task.completed = true; + this.counselingTasksCompleted++; + + this.game.showMessage( + `Counseling task completed! (${this.counselingTasksCompleted}/${this.counselingTasksRequired})` + ); + + // Check if all tasks done + if (this.counselingTasksCompleted >= this.counselingTasksRequired) { + this.finishCounseling(true); + } + + return { success: true }; + } + + /** + * Finish counseling (success or failure) + */ + finishCounseling(success) { + const spouse = this.game.npcs.getSpouse(); + + if (success) { + // Restore relationship! + spouse.addRelationshipPoints(500); // +5 hearts + + this.game.showDialogue( + spouse.name, + "I'm so glad we worked through this. I love you.", + { + mood: 'happy', + animation: 'hug' + } + ); + + this.game.showMessage( + `Marriage saved! ${spouse.name}: +5 ❤️` + ); + + // Complete quest + this.game.quests.complete('marriage_counseling'); + + } else { + // Counseling failed + this.game.showDialogue( + spouse.name, + "This isn't working. Maybe it's time to let go...", + { + mood: 'sad' + } + ); + + this.game.showMessage( + 'Counseling failed. Relationship worsened.' + ); + + // Relationship damage + spouse.addRelationshipPoints(-200); // -2 hearts + + // Fail quest + this.game.quests.fail('marriage_counseling'); + } + + this.counselingInProgress = false; + this.counselingTasks = []; + this.counselingTasksCompleted = 0; + } + + /** + * Get relationship crisis warning + */ + getRelationshipCrisisWarning() { + if (!this.player.isMarried) { + return null; + } + + const spouse = this.game.npcs.getSpouse(); + + if (spouse.relationshipHearts <= 3) { + return { + severity: 'critical', + message: `Your relationship with ${spouse.name} is in CRISIS! (${spouse.relationshipHearts} ❤️)`, + suggestion: 'Visit the Lawyer Office for marriage counseling before it\'s too late!' + }; + } + + if (spouse.relationshipHearts <= 5) { + return { + severity: 'warning', + message: `Your relationship with ${spouse.name} is struggling. (${spouse.relationshipHearts} ❤️)`, + suggestion: 'Spend more time together and give gifts.' + }; + } + + return null; + } + + /** + * Check if can remarry after divorce + */ + canRemarry() { + if (this.player.isMarried) { + return { canRemarry: false, reason: 'Already married!' }; + } + + if (!this.hasEverDivorced) { + return { canRemarry: true }; + } + + // Must wait 28 days (4 weeks) after divorce to remarry + const lastDivorce = this.divorceHistory[this.divorceHistory.length - 1]; + const daysSinceDivorce = this.game.time.currentDate - lastDivorce.date; + + if (daysSinceDivorce < 28) { + return { + canRemarry: false, + reason: `Must wait ${28 - daysSinceDivorce} more days after divorce.` + }; + } + + return { canRemarry: true }; + } + + /** + * Get office UI data + */ + getOfficeUIData() { + return { + isUnlocked: this.isUnlocked, + lawyer: this.lawyer, + isMarried: this.player.isMarried, + spouse: this.player.isMarried ? this.game.npcs.getSpouse() : null, + divorceCost: this.divorceCost, + moneyLossPercent: this.hasPrenup ? 10 : 25, + hasPrenup: this.hasPrenup, + prenupCost: this.prenupCost, + counselingCost: this.counselingCost, + counselingInProgress: this.counselingInProgress, + counselingTasks: this.counselingTasks, + divorceHistory: this.divorceHistory, + crisisWarning: this.getRelationshipCrisisWarning() + }; + } + + /** + * Update (check for auto-unlock) + */ + update() { + if (!this.isUnlocked) { + this.checkAutoUnlock(); + } + } +} + +export default LawyerOfficeSystem; diff --git a/src/systems/ZombieMinerAutomationSystem.js b/src/systems/ZombieMinerAutomationSystem.js new file mode 100644 index 000000000..3c21dba3b --- /dev/null +++ b/src/systems/ZombieMinerAutomationSystem.js @@ -0,0 +1,464 @@ +/** + * ZOMBIE MINER AUTOMATION SYSTEM + * Part of: Mining System Expansion + * Created: January 4, 2026 + * + * Features: + * - Hire zombie miners for passive resource generation + * - Assign miners to specific mine depths + * - Efficiency & loyalty mechanics + * - Automated ore collection + * - Zombie equipment upgrades + */ + +export class ZombieMinerAutomationSystem { + constructor(game) { + this.game = game; + this.player = game.player; + + // Zombie miners + this.zombieMiners = []; + this.maxZombieMiners = 10; + this.zombieMinerCost = 5000; + + // Automation settings + this.automationActive = false; + this.totalYieldPerHour = 0; + this.lastCollectionTime = null; + + // Equipment for zombies + this.zombieEquipment = { + pickaxe_tier: 1, // 1-5 + helmet_lamp: false, // Better visibility + oxygen_tank: false, // Deeper mining + cart: false // Auto-transport + }; + } + + /** + * Hire a zombie miner + */ + hireZombieMiner() { + if (this.zombieMiners.length >= this.maxZombieMiners) { + return { + success: false, + message: `Maximum ${this.maxZombieMiners} zombie miners allowed!` + }; + } + + if (this.player.money < this.zombieMinerCost) { + return { + success: false, + message: `Need ${this.zombieMinerCost}g to hire zombie miner!` + }; + } + + // Hire zombie + this.player.money -= this.zombieMinerCost; + + const miner = { + id: `zombie_miner_${this.zombieMiners.length + 1}`, + name: this.generateZombieName(), + assignedMine: null, + assignedDepth: 0, + efficiency: 1.0, // 0.5 - 2.0 + loyalty: 50, // 0-100 + yieldPerHour: 5, // Base yield + level: 1, + experience: 0 + }; + + this.zombieMiners.push(miner); + + // Recalculate automation + this.updateAutomationYield(); + + this.game.showMessage( + `Hired ${miner.name}! (${this.zombieMiners.length}/${this.maxZombieMiners})` + ); + + return { success: true, miner: miner }; + } + + /** + * Generate random zombie miner name + */ + generateZombieName() { + const prefixes = ['Grumpy', 'Rusty', 'Dusty', 'Grumbly', 'Moaning', 'Shuffling']; + const suffixes = ['Zed', 'Mort', 'Bones', 'Guts', 'Picks', 'Drills']; + + const prefix = Phaser.Utils.Array.GetRandom(prefixes); + const suffix = Phaser.Utils.Array.GetRandom(suffixes); + + return `${prefix} ${suffix}`; + } + + /** + * Assign zombie miner to specific mine & depth + */ + assignZombieMiner(minerId, mineId, depth) { + const miner = this.zombieMiners.find(m => m.id === minerId); + + if (!miner) { + return { success: false, message: 'Zombie miner not found!' }; + } + + // Check if mine exists + const mine = this.game.miningSystem.getMineInfo(mineId); + if (!mine) { + return { success: false, message: 'Mine not found!' }; + } + + // Check if depth is unlocked + const maxDepth = this.game.miningSystem.maxDepthReached || 0; + if (depth > maxDepth) { + return { + success: false, + message: `Must explore depth ${depth} first!` + }; + } + + // Assign miner + miner.assignedMine = mineId; + miner.assignedDepth = depth; + + // Update automation + this.updateAutomationYield(); + + this.game.showMessage( + `${miner.name} assigned to ${mine.name} - Depth ${depth}` + ); + + return { success: true }; + } + + /** + * Unassign zombie miner (return to surface) + */ + unassignZombieMiner(minerId) { + const miner = this.zombieMiners.find(m => m.id === minerId); + + if (!miner) { + return { success: false }; + } + + miner.assignedMine = null; + miner.assignedDepth = 0; + + // Update automation + this.updateAutomationYield(); + + this.game.showMessage( + `${miner.name} returned to surface.` + ); + + return { success: true }; + } + + /** + * Update total automation yield + */ + updateAutomationYield() { + let totalYield = 0; + + this.zombieMiners.forEach(miner => { + if (miner.assignedMine && miner.assignedDepth > 0) { + // Base yield + let yield = miner.yieldPerHour; + + // Depth bonus (+10% per 10 levels) + const depthBonus = (miner.assignedDepth / 10) * 0.1; + yield *= (1 + depthBonus); + + // Efficiency factor + yield *= miner.efficiency; + + // Loyalty factor (50% loyalty = 0.5x yield, 100% = 1.5x yield) + const loyaltyFactor = 0.5 + (miner.loyalty / 100); + yield *= loyaltyFactor; + + // Equipment bonuses + if (this.zombieEquipment.pickaxe_tier > 1) { + yield *= (1 + (this.zombieEquipment.pickaxe_tier - 1) * 0.25); + } + if (this.zombieEquipment.cart) { + yield *= 1.5; // 50% faster collection + } + + totalYield += yield; + } + }); + + this.totalYieldPerHour = Math.floor(totalYield); + this.automationActive = (totalYield > 0); + } + + /** + * Collect automated mining resources + */ + collectAutomatedYield() { + if (!this.automationActive) { + return { + success: false, + message: 'No zombie miners assigned!' + }; + } + + // Calculate time since last collection + const hoursSinceLastCollection = this.getHoursSinceLastCollection(); + + if (hoursSinceLastCollection < 0.1) { + return { + success: false, + message: 'Collected too recently! Wait a bit.' + }; + } + + const resources = {}; + + // Collect from each zombie + this.zombieMiners.forEach(miner => { + if (miner.assignedMine && miner.assignedDepth > 0) { + // Get mine info + const mine = this.game.miningSystem.getMineInfo(miner.assignedMine); + + // Determine ore type based on depth + const oreType = this.getOreTypeForDepth(mine, miner.assignedDepth); + + // Calculate yield + const hourlyYield = miner.yieldPerHour * miner.efficiency * + (0.5 + miner.loyalty / 100); + + const amount = Math.floor(hourlyYield * hoursSinceLastCollection); + + if (amount > 0) { + resources[oreType] = (resources[oreType] || 0) + amount; + + // Grant XP to miner + miner.experience += amount; + this.checkMinerLevelUp(miner); + } + } + }); + + // Add resources to inventory + let totalCollected = 0; + for (const [ore, amount] of Object.entries(resources)) { + this.player.inventory.addItem(ore, amount); + totalCollected += amount; + } + + // Update last collection time + this.lastCollectionTime = this.game.time.currentTime; + + // Show collection message + const resourceList = Object.entries(resources) + .map(([ore, amount]) => `${amount}x ${ore}`) + .join(', '); + + this.game.showMessage( + `Collected: ${resourceList} (${hoursSinceLastCollection.toFixed(1)}h)` + ); + + return { + success: true, + resources: resources, + totalAmount: totalCollected, + hours: hoursSinceLastCollection + }; + } + + /** + * Get ore type based on mine and depth + */ + getOreTypeForDepth(mine, depth) { + // Logic from mine zones + if (depth <= 25) return 'copper_ore'; + if (depth <= 50) return 'iron_ore'; + if (depth <= 75) return 'gold_ore'; + return 'diamond_ore'; + } + + /** + * Check if miner levels up + */ + checkMinerLevelUp(miner) { + const xpRequired = miner.level * 100; + + if (miner.experience >= xpRequired) { + miner.level++; + miner.experience = 0; + + // Bonuses per level + miner.yieldPerHour += 1; + miner.efficiency += 0.05; + + this.game.showMessage( + `${miner.name} leveled up! (Lv.${miner.level})` + ); + + this.updateAutomationYield(); + } + } + + /** + * Feed zombie miner (restore loyalty) + */ + feedZombieMiner(minerId, foodType) { + const miner = this.zombieMiners.find(m => m.id === minerId); + + if (!miner) { + return { success: false }; + } + + // Check if player has food + if (!this.player.inventory.hasItem(foodType)) { + return { + success: false, + message: 'You don\'t have this food!' + }; + } + + // Remove food + this.player.inventory.removeItem(foodType, 1); + + // Loyalty bonus based on food quality + const loyaltyGain = this.getFoodLoyaltyValue(foodType); + miner.loyalty = Math.min(100, miner.loyalty + loyaltyGain); + + this.game.showMessage( + `${miner.name} ate ${foodType}. Loyalty +${loyaltyGain} (${miner.loyalty}/100)` + ); + + // Update automation + this.updateAutomationYield(); + + return { success: true, loyaltyGain: loyaltyGain }; + } + + /** + * Get food loyalty value + */ + getFoodLoyaltyValue(foodType) { + const foodValues = { + 'brain': 20, // Best food + 'meat': 15, + 'bread': 10, + 'vegetables': 5 + }; + + return foodValues[foodType] || 5; + } + + /** + * Upgrade zombie equipment + */ + upgradeEquipment(equipmentType) { + const costs = { + pickaxe_tier: 3000, + helmet_lamp: 2000, + oxygen_tank: 2500, + cart: 4000 + }; + + const cost = costs[equipmentType]; + + if (!cost) { + return { success: false, message: 'Invalid equipment!' }; + } + + // Check if already owned (except pickaxe tiers) + if (equipmentType !== 'pickaxe_tier') { + if (this.zombieEquipment[equipmentType]) { + return { + success: false, + message: 'Already owned!' + }; + } + } else { + // Check pickaxe tier limit + if (this.zombieEquipment.pickaxe_tier >= 5) { + return { + success: false, + message: 'Pickaxe already max tier!' + }; + } + } + + // Check money + if (this.player.money < cost) { + return { + success: false, + message: `Need ${cost}g!` + }; + } + + // Purchase + this.player.money -= cost; + + if (equipmentType === 'pickaxe_tier') { + this.zombieEquipment.pickaxe_tier++; + } else { + this.zombieEquipment[equipmentType] = true; + } + + // Update automation + this.updateAutomationYield(); + + this.game.showMessage( + `Upgraded zombie equipment: ${equipmentType}! ${cost}g` + ); + + return { success: true }; + } + + /** + * Get hours since last collection + */ + getHoursSinceLastCollection() { + if (!this.lastCollectionTime) { + this.lastCollectionTime = this.game.time.currentTime; + return 0; + } + + const secondsElapsed = this.game.time.currentTime - this.lastCollectionTime; + return secondsElapsed / 3600; + } + + /** + * Get automation UI data + */ + getAutomationUIData() { + return { + zombieMiners: this.zombieMiners, + maxZombieMiners: this.maxZombieMiners, + totalYieldPerHour: this.totalYieldPerHour, + automationActive: this.automationActive, + equipment: this.zombieEquipment, + canHireMore: this.zombieMiners.length < this.maxZombieMiners, + hireCost: this.zombieMinerCost, + hoursSinceCollection: this.getHoursSinceLastCollection() + }; + } + + /** + * Update (passive decay of loyalty) + */ + update(deltaTime) { + // Loyalty slowly decays if zombies are working + this.zombieMiners.forEach(miner => { + if (miner.assignedMine && miner.assignedDepth > 0) { + // Lose 1 loyalty per hour worked + const loyaltyLoss = (deltaTime / 3600); + miner.loyalty = Math.max(0, miner.loyalty - loyaltyLoss); + } + }); + + // Recalculate if needed + if (this.automationActive) { + this.updateAutomationYield(); + } + } +} + +export default ZombieMinerAutomationSystem;