diff --git a/src/systems/BakeryShopSystem.js b/src/systems/BakeryShopSystem.js new file mode 100644 index 000000000..1c564cee7 --- /dev/null +++ b/src/systems/BakeryShopSystem.js @@ -0,0 +1,549 @@ +/** + * BAKERY SHOP SYSTEM - Food & Gift Purchasing + * Part of: New Town Buildings + * Created: January 4, 2026 + * + * Features: + * - Food shop with fresh baked goods + * - Gift system (best gifts for NPCs) + * - Bulk purchase discounts + * - Time-of-day inventory changes + * - Special events (baking competition, birthday cakes) + */ + +export class BakeryShopSystem { + constructor(game) { + this.game = game; + this.player = game.player; + + // Bakery status + this.isUnlocked = false; + this.isOpen = false; + this.baker = null; + + // Shop inventory + this.inventory = this.initializeInventory(); + + // Special events + this.currentEvent = null; + this.lastBakingCompetition = null; + + // Birthday cake orders + this.birthdayCakeOrders = []; + } + + /** + * Initialize bakery inventory + */ + initializeInventory() { + return { + fresh_bread: { + id: 'fresh_bread', + name: 'Fresh Bread', + price: 50, + energyRestore: 20, + giftValue: 1, // +1 heart + stock: 20, + restockTime: 'morning', // Restocks at 6 AM + description: 'Warm, freshly baked bread.', + favoredBy: ['all'] // Everyone likes bread! + }, + cake: { + id: 'cake', + name: 'Cake', + price: 200, + energyRestore: 50, + giftValue: 5, // +5 hearts + stock: 5, + restockTime: 'afternoon', // Restocks at 2 PM + description: 'A beautiful layered cake.', + favoredBy: ['ana', 'kai', 'gronk', 'children'] + }, + pie: { + id: 'pie', + name: 'Fruit Pie', + price: 150, + energyRestore: 40, + giftValue: 3, // +3 hearts + stock: 8, + restockTime: 'morning', + description: 'Sweet fruit pie with flaky crust.', + favoredBy: ['ana', 'elderly_npcs'] + }, + cookies: { + id: 'cookies', + name: 'Cookies', + price: 25, + energyRestore: 10, + giftValue: 1, // +1 heart + stock: 30, + restockTime: 'always', // Always available + description: 'Crunchy homemade cookies.', + favoredBy: ['children', 'kai'] + }, + croissants: { + id: 'croissants', + name: 'Croissants', + price: 75, + energyRestore: 25, + giftValue: 2, // +2 hearts + stock: 15, + restockTime: 'morning', + description: 'Buttery, flaky croissants. Fancy!', + favoredBy: ['ana', 'lawyer', 'wealthy_npcs'] + }, + + // SPECIAL SEASONAL ITEMS + pumpkin_pie: { + id: 'pumpkin_pie', + name: 'Pumpkin Pie', + price: 180, + energyRestore: 45, + giftValue: 4, + stock: 6, + seasonal: 'autumn', + description: 'Spiced pumpkin pie for autumn.', + favoredBy: ['gronk', 'farmer_npcs'] + }, + gingerbread: { + id: 'gingerbread', + name: 'Gingerbread', + price: 100, + energyRestore: 30, + giftValue: 3, + stock: 12, + seasonal: 'winter', + description: 'Festive gingerbread cookies.', + favoredBy: ['children', 'kai', 'festive_npcs'] + } + }; + } + + /** + * Unlock bakery (requires town restoration) + */ + unlockBakery() { + // Check requirements + if (!this.player.hasCompletedQuest('first_bread')) { + return { + success: false, + message: 'Complete "First Bread" quest first!' + }; + } + + if (!this.player.hasMaterials({ wood: 100, wheat: 50 })) { + return { + success: false, + message: 'Need 100 Wood + 50 Wheat to build bakery!' + }; + } + + if (this.player.money < 8000) { + return { + success: false, + message: 'Need 8,000g to build bakery!' + }; + } + + // Build bakery + this.player.removeMaterials({ wood: 100, wheat: 50 }); + this.player.money -= 8000; + this.isUnlocked = true; + + // Spawn baker NPC + this.baker = this.game.npcs.spawn('baker', { + name: 'Maria', + position: { x: 1200, y: 800 }, // Bakery location + dialogue: this.getBakerDialogue() + }); + + // Open immediately + this.isOpen = true; + + this.game.showMessage('🥖 Bakery is now open!'); + + return { success: true }; + } + + /** + * Get baker NPC dialogue + */ + getBakerDialogue() { + return { + greeting: [ + "Welcome to the bakery! Fresh bread daily!", + "Hello dear! What can I bake for you today?", + "Mmm, smell that fresh bread!" + ], + buying: [ + "Excellent choice!", + "Fresh from the oven!", + "Enjoy, dear!" + ], + gift_suggestion: [ + "Cakes make wonderful gifts!", + "Everyone loves fresh bread.", + "Looking for a special gift? Try the pie!" + ], + competition: [ + "Weekly baking competition this Sunday!", + "Show me your best recipe!", + "The winner gets a special prize!" + ], + birthday: [ + "Need a birthday cake? I can make one!", + "Tell me who it's for, I'll personalize it!", + "Birthday cakes are my specialty!" + ] + }; + } + + /** + * Purchase item from bakery + */ + buyItem(itemId, quantity = 1) { + const item = this.inventory[itemId]; + + if (!item) { + return { success: false, message: 'Item not found!' }; + } + + // Check if bakery is open + if (!this.isOpen) { + return { success: false, message: 'Bakery is closed!' }; + } + + // Check seasonal availability + if (item.seasonal && !this.isCurrentSeason(item.seasonal)) { + return { + success: false, + message: `${item.name} is only available in ${item.seasonal}!` + }; + } + + // Check stock + if (item.stock < quantity) { + return { + success: false, + message: `Only ${item.stock} ${item.name} in stock!` + }; + } + + // Calculate price (with bulk discount) + const discount = this.getBulkDiscount(quantity); + const totalPrice = Math.floor(item.price * quantity * (1 - discount)); + + // Check money + if (this.player.money < totalPrice) { + return { + success: false, + message: `Not enough money! Need ${totalPrice}g` + }; + } + + // Purchase + this.player.money -= totalPrice; + item.stock -= quantity; + + // Add to inventory + this.player.inventory.addItem(itemId, quantity); + + // Show message + let message = `Purchased ${quantity}x ${item.name} for ${totalPrice}g!`; + if (discount > 0) { + message += ` (${Math.floor(discount * 100)}% bulk discount!)`; + } + + this.game.showMessage(message); + + return { success: true, totalPrice: totalPrice, discount: discount }; + } + + /** + * Calculate bulk discount + */ + getBulkDiscount(quantity) { + // 10+ items = 20% off + if (quantity >= 10) { + return 0.20; + } + // 5-9 items = 10% off + if (quantity >= 5) { + return 0.10; + } + return 0; + } + + /** + * Gift baked good to NPC + */ + giftItem(itemId, npcId) { + const item = this.inventory[itemId]; + const npc = this.game.npcs.get(npcId); + + if (!item || !npc) { + return { success: false }; + } + + // Check if player has item + if (!this.player.inventory.hasItem(itemId)) { + return { + success: false, + message: 'You don\'t have this item!' + }; + } + + // Calculate gift value (bonus if NPC favors this item) + let giftValue = item.giftValue; + if (item.favoredBy.includes(npc.id) || item.favoredBy.includes('all')) { + giftValue *= 2; // Double hearts if favored! + } + + // Remove from inventory + this.player.inventory.removeItem(itemId, 1); + + // Add relationship points + npc.addRelationshipPoints(giftValue); + + // NPC reaction + const reaction = this.getNPCGiftReaction(npc, item, giftValue); + + this.game.showMessage( + `${npc.name}: "${reaction}" +${giftValue} ❤️` + ); + + return { success: true, heartsAdded: giftValue, reaction: reaction }; + } + + /** + * Get NPC reaction to gift + */ + getNPCGiftReaction(npc, item, heartsAdded) { + if (heartsAdded >= 5) { + return "Oh wow! This is amazing! Thank you so much!"; + } + if (heartsAdded >= 3) { + return "This is wonderful! You're so thoughtful!"; + } + if (heartsAdded >= 1) { + return "Thank you! I love fresh baked goods!"; + } + return "Thanks."; + } + + /** + * Order birthday cake + */ + orderBirthdayCake(npcId) { + const npc = this.game.npcs.get(npcId); + + if (!npc) { + return { success: false, message: 'NPC not found!' }; + } + + // Check cost + const cakePrice = 500; // Special birthday cake + if (this.player.money < cakePrice) { + return { + success: false, + message: `Birthday cakes cost ${cakePrice}g!` + }; + } + + // Order cake + this.player.money -= cakePrice; + + const order = { + npc: npc, + orderDate: this.game.time.currentDate, + deliveryDate: npc.birthday, + delivered: false + }; + + this.birthdayCakeOrders.push(order); + + this.game.showMessage( + `Ordered birthday cake for ${npc.name}! Will be delivered on their birthday.` + ); + + return { success: true, order: order }; + } + + /** + * Start weekly baking competition + */ + startBakingCompetition() { + this.currentEvent = { + type: 'baking_competition', + startDate: this.game.time.currentDate, + endDate: this.game.time.currentDate + 7, // Lasts 1 week + participants: [], + playerScore: 0, + prize: { + money: 1000, + item: 'golden_whisk', + hearts: 5 // +5 hearts with baker + } + }; + + this.game.showMessage( + '🥖 Weekly Baking Competition started! Bring your best recipe!' + ); + + return this.currentEvent; + } + + /** + * Submit recipe to competition + */ + submitCompetitionRecipe(recipeId) { + if (!this.currentEvent || this.currentEvent.type !== 'baking_competition') { + return { + success: false, + message: 'No baking competition active!' + }; + } + + // Check if player has baked this recipe + const recipe = this.game.crafting.recipes[recipeId]; + if (!recipe || !this.player.inventory.hasItem(recipeId)) { + return { + success: false, + message: 'You need to bake this recipe first!' + }; + } + + // Calculate score based on recipe complexity + const score = this.calculateRecipeScore(recipe); + this.currentEvent.playerScore = Math.max(this.currentEvent.playerScore, score); + + this.game.showMessage( + `Recipe submitted! Score: ${score}/100` + ); + + return { success: true, score: score }; + } + + /** + * Calculate recipe score for competition + */ + calculateRecipeScore(recipe) { + let score = 0; + + // Base score from ingredients count + score += Object.keys(recipe.ingredients).length * 10; + + // Bonus for craft time (complexity) + score += Math.min(recipe.craftTime, 50); + + // Bonus for energy restore (if food) + if (recipe.energy_restore) { + score += recipe.energy_restore / 2; + } + + // Random factor (luck) + score += Math.random() * 20; + + return Math.min(Math.floor(score), 100); + } + + /** + * Check seasonal availability + */ + isCurrentSeason(season) { + const currentSeason = this.game.time.getSeason(); + return currentSeason === season; + } + + /** + * Restock inventory (called at specific times) + */ + restockInventory() { + const hour = this.game.time.getHour(); + + Object.values(this.inventory).forEach(item => { + // Morning restock (6 AM) + if (hour === 6 && item.restockTime === 'morning') { + item.stock = this.getMaxStock(item.id); + } + + // Afternoon restock (2 PM) + if (hour === 14 && item.restockTime === 'afternoon') { + item.stock = this.getMaxStock(item.id); + } + + // Always available items restock every hour + if (item.restockTime === 'always') { + item.stock = this.getMaxStock(item.id); + } + }); + } + + /** + * Get max stock for item + */ + getMaxStock(itemId) { + const defaults = { + fresh_bread: 20, + cake: 5, + pie: 8, + cookies: 30, + croissants: 15, + pumpkin_pie: 6, + gingerbread: 12 + }; + + return defaults[itemId] || 10; + } + + /** + * Update shop (called every hour) + */ + update() { + const hour = this.game.time.getHour(); + + // Open/close shop + this.isOpen = (hour >= 6 && hour <= 20); // Open 6 AM - 8 PM + + // Restock + this.restockInventory(); + + // Check birthday cake deliveries + this.checkBirthdayCakeDeliveries(); + } + + /** + * Check and deliver birthday cakes + */ + checkBirthdayCakeDeliveries() { + const today = this.game.time.currentDate; + + this.birthdayCakeOrders.forEach(order => { + if (!order.delivered && order.deliveryDate === today) { + // Deliver cake + this.player.inventory.addItem('birthday_cake_' + order.npc.id, 1); + order.delivered = true; + + this.game.showMessage( + `🎂 Birthday cake for ${order.npc.name} is ready!` + ); + } + }); + } + + /** + * Get shop UI data + */ + getShopUIData() { + return { + isUnlocked: this.isUnlocked, + isOpen: this.isOpen, + inventory: this.inventory, + currentEvent: this.currentEvent, + baker: this.baker, + openingHours: '6 AM - 8 PM' + }; + } +} + +export default BakeryShopSystem; diff --git a/src/systems/CraftingTablesSystem.js b/src/systems/CraftingTablesSystem.js new file mode 100644 index 000000000..a9c89f65c --- /dev/null +++ b/src/systems/CraftingTablesSystem.js @@ -0,0 +1,574 @@ +/** + * CRAFTING TABLES SYSTEM - Recipe Management & Item Creation + * Part of: Home Upgrade & Customization System + * Created: January 4, 2026 + * + * Features: + * - Small crafting table (50 basic recipes) + * - Large planning table (100+ advanced recipes) + * - Recipe unlocking system + * - Ingredient checking + * - Batch crafting + * - Gronk's expedition integration + */ + +export class CraftingTablesSystem { + constructor(game) { + this.game = game; + this.player = game.player; + + // Table types + this.tables = { + SMALL: { + id: 'small_table', + name: 'Small Crafting Table', + cost: 0, + unlocked: true, + maxRecipes: 50, + craftingSpeed: 1.0, + sprite: 'interior_table_small.png', + description: 'A basic workbench for simple crafting.', + categories: ['tools_wooden', 'repairs', 'food_basic', 'potions_beginner'] + }, + LARGE: { + id: 'large_table', + name: 'Large Planning Table', + cost: 5000, + requirements: { + metGronk: true, + questCompleted: 'builders_vision' + }, + unlocked: false, + maxRecipes: 100, + craftingSpeed: 1.5, + sprite: 'interior_table_large.png', + description: 'An advanced table for complex projects and expeditions.', + categories: ['tools_wooden', 'repairs', 'food_basic', 'potions_beginner', + 'expedition_planning', 'mechanisms', 'weapons_advanced', 'gronk_special'] + } + }; + + // Crafting recipes database + this.recipes = this.initializeRecipes(); + + // Player's current table + this.currentTable = { ...this.tables.SMALL }; + + // Unlocked recipes + this.unlockedRecipes = []; + + // Crafting state + this.isCrafting = false; + this.craftingQueue = []; + this.currentCraft = null; + } + + /** + * Initialize all crafting recipes + */ + initializeRecipes() { + return { + // TOOLS - WOODEN TIER + wooden_hoe: { + id: 'wooden_hoe', + name: 'Wooden Hoe', + category: 'tools_wooden', + table: 'SMALL', + ingredients: { + wood: 10, + rope: 2 + }, + output: { + item: 'wooden_hoe', + quantity: 1 + }, + craftTime: 5, // seconds + unlocked: true, + description: 'Basic farming tool for tilling soil.' + }, + wooden_pickaxe: { + id: 'wooden_pickaxe', + name: 'Wooden Pickaxe', + category: 'tools_wooden', + table: 'SMALL', + ingredients: { + wood: 15, + stone: 5 + }, + output: { + item: 'wooden_pickaxe', + quantity: 1 + }, + craftTime: 8, + unlocked: true, + description: 'Basic mining tool for breaking rocks.' + }, + + // REPAIRS + locket_cleaning: { + id: 'locket_cleaning', + name: 'Clean Ana\'s Locket', + category: 'repairs', + table: 'SMALL', + ingredients: { + cloth: 1, + water: 1 + }, + output: { + item: 'locket_clean', + quantity: 1 + }, + craftTime: 3, + unlocked: false, + questRequired: 'memory_lane', + description: 'Restore Ana\'s precious locket to its former shine.' + }, + + // FOOD - BASIC + sandwich: { + id: 'sandwich', + name: 'Sandwich', + category: 'food_basic', + table: 'SMALL', + ingredients: { + bread: 2, + cheese: 1 + }, + output: { + item: 'sandwich', + quantity: 1 + }, + craftTime: 2, + energy_restore: 20, + unlocked: true, + description: 'A simple but filling meal.' + }, + salad: { + id: 'salad', + name: 'Garden Salad', + category: 'food_basic', + table: 'SMALL', + ingredients: { + lettuce: 3, + tomato: 2, + cucumber: 1 + }, + output: { + item: 'salad', + quantity: 1 + }, + craftTime: 3, + energy_restore: 25, + unlocked: true, + description: 'Fresh vegetables in a healthy mix.' + }, + + // POTIONS - BEGINNER + healing_potion_basic: { + id: 'healing_potion_basic', + name: 'Basic Healing Potion', + category: 'potions_beginner', + table: 'SMALL', + ingredients: { + red_herb: 2, + water: 1, + glass_bottle: 1 + }, + output: { + item: 'healing_potion_basic', + quantity: 1 + }, + craftTime: 10, + health_restore: 50, + unlocked: false, + skillRequired: { alchemy: 1 }, + description: 'Restores 50 HP.' + }, + + // EXPEDITION PLANNING (Large Table Only) + expedition_map: { + id: 'expedition_map', + name: 'Expedition Map', + category: 'expedition_planning', + table: 'LARGE', + ingredients: { + paper: 5, + ink: 2, + compass: 1 + }, + output: { + item: 'expedition_map', + quantity: 1 + }, + craftTime: 20, + unlocked: false, + npcRequired: 'gronk', + description: 'Required for planning expeditions with Gronk.' + }, + expedition_supplies: { + id: 'expedition_supplies', + name: 'Expedition Supplies', + category: 'expedition_planning', + table: 'LARGE', + ingredients: { + rope: 10, + torch: 5, + food_rations: 20, + water_bottle: 10 + }, + output: { + item: 'expedition_supplies', + quantity: 1 + }, + craftTime: 30, + unlocked: false, + npcRequired: 'gronk', + description: 'Everything needed for a successful expedition.' + }, + + // COMPLEX MECHANISMS (Large Table Only) + automated_sprinkler: { + id: 'automated_sprinkler', + name: 'Automated Sprinkler', + category: 'mechanisms', + table: 'LARGE', + ingredients: { + iron_pipe: 10, + gears: 5, + water_pump: 1 + }, + output: { + item: 'automated_sprinkler', + quantity: 1 + }, + craftTime: 60, + unlocked: false, + skillRequired: { engineering: 3 }, + description: 'Waters crops automatically in a 3x3 area.' + }, + + // ADVANCED WEAPONS (Large Table Only) + steel_sword: { + id: 'steel_sword', + name: 'Steel Sword', + category: 'weapons_advanced', + table: 'LARGE', + ingredients: { + steel_ingot: 5, + leather: 2, + ruby: 1 + }, + output: { + item: 'steel_sword', + quantity: 1 + }, + craftTime: 45, + damage: 50, + unlocked: false, + skillRequired: { combat: 5 }, + description: 'A powerful blade forged from steel.' + }, + + // GRONK SPECIAL (Large Table Only) + gronk_explosive: { + id: 'gronk_explosive', + name: 'Gronk\'s Explosive', + category: 'gronk_special', + table: 'LARGE', + ingredients: { + gunpowder: 5, + wire: 3, + metal_casing: 1, + gronks_secret_ingredient: 1 + }, + output: { + item: 'gronk_explosive', + quantity: 3 + }, + craftTime: 90, + unlocked: false, + npcRequired: 'gronk', + relationshipRequired: { gronk: 7 }, + description: 'BOOM! Gronk\'s special explosive for clearing obstacles.' + } + }; + } + + /** + * Purchase Large Planning Table + */ + purchaseLargeTable() { + const largeTable = this.tables.LARGE; + + // Check if already purchased + if (largeTable.unlocked) { + return { success: false, message: 'You already own the Large Table!' }; + } + + // Check requirements + if (!this.player.hasMetNPC('gronk')) { + return { + success: false, + message: 'You need to meet Gronk first!' + }; + } + + if (!this.player.hasCompletedQuest('builders_vision')) { + return { + success: false, + message: 'Complete "Builder\'s Vision" quest first!' + }; + } + + // Check cost + if (this.player.money < largeTable.cost) { + return { + success: false, + message: `Not enough money! Need ${largeTable.cost}g` + }; + } + + // Purchase + this.player.money -= largeTable.cost; + largeTable.unlocked = true; + this.currentTable = { ...largeTable }; + + // Unlock advanced recipes + this.unlockAdvancedRecipes(); + + return { + success: true, + message: `Purchased ${largeTable.name} for ${largeTable.cost}g!` + }; + } + + /** + * Unlock advanced recipes when Large Table is purchased + */ + unlockAdvancedRecipes() { + Object.values(this.recipes).forEach(recipe => { + if (recipe.table === 'LARGE' && !recipe.npcRequired && !recipe.skillRequired) { + recipe.unlocked = true; + } + }); + } + + /** + * Check if player can craft a recipe + */ + canCraft(recipeId) { + const recipe = this.recipes[recipeId]; + + if (!recipe) { + return { canCraft: false, reason: 'Recipe not found' }; + } + + // Check if recipe is unlocked + if (!recipe.unlocked) { + return { canCraft: false, reason: 'Recipe locked' }; + } + + // Check table requirement + if (recipe.table === 'LARGE' && !this.tables.LARGE.unlocked) { + return { canCraft: false, reason: 'Requires Large Planning Table' }; + } + + // Check NPC requirement + if (recipe.npcRequired && !this.player.hasMetNPC(recipe.npcRequired)) { + return { canCraft: false, reason: `Requires meeting ${recipe.npcRequired}` }; + } + + // Check relationship requirement + if (recipe.relationshipRequired) { + for (const [npc, level] of Object.entries(recipe.relationshipRequired)) { + if (this.player.getRelationshipLevel(npc) < level) { + return { + canCraft: false, + reason: `Requires ${level}+ hearts with ${npc}` + }; + } + } + } + + // Check skill requirement + if (recipe.skillRequired) { + for (const [skill, level] of Object.entries(recipe.skillRequired)) { + if (this.player.getSkillLevel(skill) < level) { + return { + canCraft: false, + reason: `Requires ${skill} level ${level}` + }; + } + } + } + + // Check ingredients + const missingIngredients = []; + for (const [ingredient, amount] of Object.entries(recipe.ingredients)) { + const playerAmount = this.player.inventory.getItemCount(ingredient); + if (playerAmount < amount) { + missingIngredients.push({ + item: ingredient, + have: playerAmount, + need: amount + }); + } + } + + if (missingIngredients.length > 0) { + return { + canCraft: false, + reason: 'Missing ingredients', + missingIngredients: missingIngredients + }; + } + + return { canCraft: true }; + } + + /** + * Start crafting a recipe + */ + craft(recipeId, quantity = 1) { + const canCraftCheck = this.canCraft(recipeId); + + if (!canCraftCheck.canCraft) { + return { + success: false, + message: canCraftCheck.reason, + missingIngredients: canCraftCheck.missingIngredients + }; + } + + const recipe = this.recipes[recipeId]; + + // Remove ingredients + for (const [ingredient, amount] of Object.entries(recipe.ingredients)) { + this.player.inventory.removeItem(ingredient, amount * quantity); + } + + // Calculate craft time (with table speed bonus) + const tableSpeed = this.currentTable.craftingSpeed; + const totalTime = (recipe.craftTime / tableSpeed) * quantity; + + // Add to crafting queue + const craftJob = { + recipe: recipe, + quantity: quantity, + timeRemaining: totalTime, + totalTime: totalTime + }; + + this.craftingQueue.push(craftJob); + + // Start crafting if not already crafting + if (!this.isCrafting) { + this.startNextCraft(); + } + + return { + success: true, + message: `Crafting ${quantity}x ${recipe.name}... (${Math.floor(totalTime)}s)`, + craftJob: craftJob + }; + } + + /** + * Start next craft in queue + */ + startNextCraft() { + if (this.craftingQueue.length === 0) { + this.isCrafting = false; + this.currentCraft = null; + return; + } + + this.isCrafting = true; + this.currentCraft = this.craftingQueue[0]; + + // Emit crafting started event + this.game.emit('craftingStarted', { + recipe: this.currentCraft.recipe, + quantity: this.currentCraft.quantity, + time: this.currentCraft.totalTime + }); + } + + /** + * Update crafting progress + */ + update(deltaTime) { + if (!this.isCrafting || !this.currentCraft) return; + + // Decrease time remaining + this.currentCraft.timeRemaining -= deltaTime; + + // Check if completed + if (this.currentCraft.timeRemaining <= 0) { + this.completeCraft(); + } + } + + /** + * Complete current craft + */ + completeCraft() { + if (!this.currentCraft) return; + + const recipe = this.currentCraft.recipe; + const quantity = this.currentCraft.quantity; + + // Add crafted item to inventory + this.player.inventory.addItem( + recipe.output.item, + recipe.output.quantity * quantity + ); + + // Show completion message + this.game.showMessage( + `✅ Crafted ${quantity}x ${recipe.name}!` + ); + + // Emit crafting completed event + this.game.emit('craftingCompleted', { + recipe: recipe, + quantity: quantity + }); + + // Remove from queue + this.craftingQueue.shift(); + + // Start next craft + this.startNextCraft(); + } + + /** + * Get all available recipes for current table + */ + getAvailableRecipes() { + return Object.values(this.recipes).filter(recipe => { + // Filter by table type + if (recipe.table === 'LARGE' && !this.tables.LARGE.unlocked) { + return false; + } + + return recipe.unlocked; + }); + } + + /** + * Get crafting UI data + */ + getCraftingUIData() { + return { + currentTable: this.currentTable, + isCrafting: this.isCrafting, + currentCraft: this.currentCraft, + queue: this.craftingQueue, + availableRecipes: this.getAvailableRecipes(), + largeTableUnlocked: this.tables.LARGE.unlocked + }; + } +} + +export default CraftingTablesSystem; diff --git a/src/systems/SleepSystem.js b/src/systems/SleepSystem.js new file mode 100644 index 000000000..b923b7f15 --- /dev/null +++ b/src/systems/SleepSystem.js @@ -0,0 +1,384 @@ +/** + * SLEEP SYSTEM - Energy Regeneration & Bed Upgrades + * Part of: Home Upgrade & Customization System + * Created: January 4, 2026 + * + * Features: + * - 3-tier bed system (sleeping bag → wooden → king-size) + * - Energy regeneration based on bed quality + * - Time skip mechanics (8 hours sleep) + * - Partner bonuses for married players + * - Dream sequences and nightmares + */ + +export class SleepSystem { + constructor(game) { + this.game = game; + this.player = game.player; + this.isSleeping = false; + this.currentBed = null; + + // Bed types configuration + this.bedTypes = { + SLEEPING_BAG: { + id: 'sleeping_bag', + name: 'Sleeping Bag', + cost: 0, + unlocked: true, + energyRegen: 50, // Restores 50% energy + sleepDuration: 8, // hours + dreamChance: 0, // No dreams + nightmareChance: 0.1, // 10% nightmare chance + partnerBonus: false, + sprite: 'interior_bed_sleepingbag.png', + description: 'A basic sleeping bag. Uncomfortable but functional.' + }, + WOODEN_BED: { + id: 'wooden_bed', + name: 'Wooden Bed', + cost: 2000, + requirements: { + farmhouseLevel: 1 + }, + unlocked: false, + energyRegen: 90, // Restores 90% energy + sleepDuration: 8, + dreamChance: 0.2, // 20% dream chance + nightmareChance: 0.05, // 5% nightmare chance + partnerBonus: false, + sprite: 'interior_bed_wooden.png', + description: 'A comfortable wooden bed. Much better than the floor!' + }, + KING_SIZE_BED: { + id: 'king_size', + name: 'King-size Bed', + cost: 10000, + requirements: { + farmhouseLevel: 2, + married: true + }, + unlocked: false, + energyRegen: 100, // Restores 100% energy + sleepDuration: 8, + dreamChance: 0.3, // 30% dream chance + nightmareChance: 0, // No nightmares! + partnerBonus: true, // +50 relationship points + spousePresent: false, + sprite: 'interior_bed_kingsize.png', + description: 'A luxurious king-size bed. Perfect for two!' + } + }; + + // Current player's bed (starts with sleeping bag) + this.playerBed = { ...this.bedTypes.SLEEPING_BAG }; + + // Sleep state + this.sleepStartTime = null; + this.sleepEndTime = null; + this.dreamSequence = null; + } + + /** + * Check if player can use a specific bed type + */ + canUseBed(bedType) { + const bed = this.bedTypes[bedType]; + + // Check if unlocked + if (!bed.unlocked && bedType !== 'SLEEPING_BAG') { + return { canUse: false, reason: 'Bed not purchased yet' }; + } + + // Check requirements + if (bed.requirements) { + if (bed.requirements.farmhouseLevel && + this.player.farmhouseLevel < bed.requirements.farmhouseLevel) { + return { + canUse: false, + reason: `Requires Farmhouse Level ${bed.requirements.farmhouseLevel}` + }; + } + + if (bed.requirements.married && !this.player.isMarried) { + return { + canUse: false, + reason: 'Requires marriage' + }; + } + } + + return { canUse: true }; + } + + /** + * Purchase a bed upgrade + */ + purchaseBed(bedType) { + const bed = this.bedTypes[bedType]; + + // Check if can purchase + if (bed.unlocked) { + return { success: false, message: 'You already own this bed!' }; + } + + // Check cost + if (this.player.money < bed.cost) { + return { + success: false, + message: `Not enough money! Need ${bed.cost}g` + }; + } + + // Check requirements + const canUse = this.canUseBed(bedType); + if (!canUse.canUse) { + return { success: false, message: canUse.reason }; + } + + // Purchase bed + this.player.money -= bed.cost; + bed.unlocked = true; + this.playerBed = { ...bed }; + + // Trigger furniture placement + this.game.emit('bedPurchased', { + bedType: bedType, + bed: bed + }); + + return { + success: true, + message: `Purchased ${bed.name} for ${bed.cost}g!` + }; + } + + /** + * Initiate sleep sequence + */ + sleep() { + // Check if already sleeping + if (this.isSleeping) { + return { success: false, message: 'Already sleeping!' }; + } + + // Check if tired enough (energy < 50%) + if (this.player.energy > this.player.maxEnergy * 0.5) { + return { + success: false, + message: "You're not tired enough to sleep yet." + }; + } + + // Start sleep + this.isSleeping = true; + this.sleepStartTime = this.game.time.currentTime; + this.sleepEndTime = this.game.time.currentTime + (this.playerBed.sleepDuration * 3600); // Convert to seconds + + // Roll for dream/nightmare + this.dreamSequence = this.rollForDream(); + + // Trigger sleep animation + this.game.emit('sleepStarted', { + bed: this.playerBed, + duration: this.playerBed.sleepDuration, + dreamSequence: this.dreamSequence + }); + + // Fast-forward time + this.game.time.skipTime(this.playerBed.sleepDuration); + + // Wake up after duration + setTimeout(() => { + this.wakeUp(); + }, 100); // Instant wake-up after time skip + + return { success: true }; + } + + /** + * Roll for dream or nightmare + */ + rollForDream() { + const rand = Math.random(); + + // Check nightmare first + if (rand < this.playerBed.nightmareChance) { + return { + type: 'nightmare', + sequence: this.getRandomNightmare() + }; + } + + // Check dream + if (rand < this.playerBed.dreamChance + this.playerBed.nightmareChance) { + return { + type: 'dream', + sequence: this.getRandomDream() + }; + } + + return null; // No dream + } + + /** + * Get random dream sequence + */ + getRandomDream() { + const dreams = [ + { + id: 'memory_ana', + title: 'Memory of Ana', + description: 'You dream of Ana, smiling in the sunlight...', + effect: { mood: +10 } + }, + { + id: 'farming_success', + title: 'Bountiful Harvest', + description: 'You dream of your crops growing tall and strong.', + effect: { farming_xp: 50 } + }, + { + id: 'zombie_peace', + title: 'Peaceful Workers', + description: 'Your zombie workers are happy and productive.', + effect: { loyalty: +5 } + } + ]; + + return dreams[Math.floor(Math.random() * dreams.length)]; + } + + /** + * Get random nightmare sequence + */ + getRandomNightmare() { + const nightmares = [ + { + id: 'ana_loss', + title: 'The Loss', + description: 'Fragments of that terrible day flash before you...', + effect: { mood: -20 } + }, + { + id: 'zombie_attack', + title: 'Undead Uprising', + description: 'Your workers turn hostile, advancing slowly...', + effect: { energy: -10 } + }, + { + id: 'crop_failure', + title: 'Withered Fields', + description: 'Your entire farm crumbles to dust.', + effect: { mood: -10 } + } + ]; + + return nightmares[Math.floor(Math.random() * nightmares.length)]; + } + + /** + * Wake up from sleep + */ + wakeUp() { + if (!this.isSleeping) return; + + // Calculate energy restoration + const energyRestored = (this.player.maxEnergy * this.playerBed.energyRegen) / 100; + this.player.energy = Math.min( + this.player.maxEnergy, + this.player.energy + energyRestored + ); + + // Partner bonus (if married & king-size bed) + if (this.playerBed.partnerBonus && this.player.isMarried) { + const spouse = this.game.npcs.getSpouse(); + if (spouse) { + spouse.addRelationshipPoints(50); + this.game.showMessage(`${spouse.name} cuddles closer. +50 ❤️`); + } + } + + // Apply dream/nightmare effects + if (this.dreamSequence) { + this.applyDreamEffects(this.dreamSequence); + } + + // Update state + this.isSleeping = false; + this.currentBed = null; + this.dreamSequence = null; + + // Trigger wake-up event + this.game.emit('wakeUp', { + energyRestored: energyRestored, + newEnergy: this.player.energy, + time: this.game.time.currentTime + }); + + // New day announcements + this.game.showMessage(`Good morning! Energy restored: ${Math.floor(energyRestored)}`); + } + + /** + * Apply dream/nightmare effects + */ + applyDreamEffects(dreamSequence) { + const sequence = dreamSequence.sequence; + + // Show dream/nightmare message + const typeIcon = dreamSequence.type === 'dream' ? '✨' : '💀'; + this.game.showMessage(`${typeIcon} ${sequence.title}: ${sequence.description}`); + + // Apply effects + if (sequence.effect.mood) { + this.player.mood += sequence.effect.mood; + } + if (sequence.effect.energy) { + this.player.energy += sequence.effect.energy; + } + if (sequence.effect.farming_xp) { + this.player.addXP('farming', sequence.effect.farming_xp); + } + if (sequence.effect.loyalty && this.game.zombieWorkers) { + this.game.zombieWorkers.addGlobalLoyalty(sequence.effect.loyalty); + } + } + + /** + * Check if it's a good time to sleep + */ + canSleepNow() { + const hour = this.game.time.getHour(); + + // Can sleep between 8 PM (20:00) and 2 AM (02:00) + if ((hour >= 20 && hour <= 23) || (hour >= 0 && hour <= 2)) { + return { canSleep: true }; + } + + return { + canSleep: false, + message: "It's not nighttime yet. Sleep between 8 PM - 2 AM." + }; + } + + /** + * Get current bed info for UI + */ + getCurrentBedInfo() { + return { + ...this.playerBed, + isSleeping: this.isSleeping, + canSleep: this.canSleepNow().canSleep + }; + } + + /** + * Update (called every frame) + */ + update(deltaTime) { + // Sleep system update logic (if needed) + // Currently handled by events and timers + } +} + +export default SleepSystem;