💻 MORE SYSTEMS - Barber, Lawyer, Zombie Miners
Added 3 additional game systems: 1. BARBER SHOP SYSTEM (BarberShopSystem.js): - 7 hairstyles (dreadlocks, mohawks, long hair, ponytail, bald) - 5 piercing types (ear gauges, nose ring, eyebrow, lip) - 9 hair dye colors + clothing dyes - Zombie makeover (cosmetic for workers, +20 loyalty) - NPC customization (requires 5+ hearts) - 5 saved look slots - Repeat customer discounts (5+ visits = 10% off) 2. LAWYER OFFICE SYSTEM (LawyerOfficeSystem.js): - Divorce processing (50,000g + 25% money loss) - Prenup system (10,000g, reduces loss to 10%) - Marriage counseling (5,000g, 3 tasks to save marriage) - Relationship crisis detection (auto-unlock at 3 hearts) - 28-day remarriage cooldown - Post-divorce quests 3. ZOMBIE MINER AUTOMATION (ZombieMinerAutomationSystem.js): - Hire zombie miners (5,000g each, max 10) - Assign to specific mine depths - Efficiency & loyalty mechanics (0-100%) - Passive resource generation (yield per hour) - Zombie leveling system (XP from mining) - Equipment upgrades (pickaxe tiers, lamps, oxygen, carts) - Feed zombies to restore loyalty Total new systems: 6 Total code: 3,100+ lines All systems include event emission for UI integration Next: Town Growth & NPC Privacy systems
This commit is contained in:
663
src/systems/BarberShopSystem.js
Normal file
663
src/systems/BarberShopSystem.js
Normal file
@@ -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;
|
||||||
554
src/systems/LawyerOfficeSystem.js
Normal file
554
src/systems/LawyerOfficeSystem.js
Normal file
@@ -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;
|
||||||
464
src/systems/ZombieMinerAutomationSystem.js
Normal file
464
src/systems/ZombieMinerAutomationSystem.js
Normal file
@@ -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;
|
||||||
Reference in New Issue
Block a user