diff --git a/src/systems/MagicSystem.js b/src/systems/MagicSystem.js new file mode 100644 index 0000000..39f19a6 --- /dev/null +++ b/src/systems/MagicSystem.js @@ -0,0 +1,708 @@ +/** + * MagicSystem.js + * ============== + * Spell casting, magic staffs, potions, and elemental effects + * + * Features: + * - 6 elemental staffs (fire, ice, lightning, earth, light, dark) + * - Spell system with mana costs + * - Potion brewing and consumption + * - Elemental damage types + * - Magic projectiles + * - Buff/debuff effects + * - Spell combos + * + * Uses: portal_states_broken_repaired_1766097098724.tsx (magic portals) + * + * @author NovaFarma Team + * @date 2025-12-22 + */ + +export default class MagicSystem { + constructor(scene) { + this.scene = scene; + + // Player magic state + this.mana = 100; + this.maxMana = 100; + this.manaRegenRate = 5; // per second + + // Equipment + this.equippedStaff = null; + this.equippedSpells = new Map(); // slot -> spell + + // Spell databases + this.spells = new Map(); + this.staffs = new Map(); + this.potions = new Map(); + + // Active effects + this.activeEffects = []; + this.activeProjectiles = []; + + // Cooldowns + this.spellCooldowns = new Map(); + + // Initialize databases + this.initializeSpells(); + this.initializeStaffs(); + this.initializePotions(); + + // Load saved data + this.loadMagicData(); + + console.log('โœจ MagicSystem initialized'); + } + + initializeSpells() { + // ===== FIRE MAGIC ===== + this.addSpell('fireball', { + name: 'Fireball', + element: 'fire', + manaCost: 20, + damage: 50, + range: 200, + speed: 300, + cooldown: 2000, + requiresStaff: 'fire', + description: 'Launch a ball of fire', + effect: 'burn' + }); + + this.addSpell('fire_blast', { + name: 'Fire Blast', + element: 'fire', + manaCost: 40, + damage: 100, + range: 150, + aoe: 80, + cooldown: 5000, + requiresStaff: 'fire', + description: 'Area explosion of flames', + effect: 'burn' + }); + + // ===== ICE MAGIC ===== + this.addSpell('ice_shard', { + name: 'Ice Shard', + element: 'ice', + manaCost: 15, + damage: 40, + range: 250, + speed: 350, + cooldown: 1500, + requiresStaff: 'ice', + description: 'Pierce enemies with ice', + effect: 'slow' + }); + + this.addSpell('frost_nova', { + name: 'Frost Nova', + element: 'ice', + manaCost: 50, + damage: 60, + aoe: 120, + cooldown: 6000, + requiresStaff: 'ice', + description: 'Freeze everything nearby', + effect: 'freeze' + }); + + // ===== LIGHTNING MAGIC ===== + this.addSpell('lightning_bolt', { + name: 'Lightning Bolt', + element: 'lightning', + manaCost: 25, + damage: 70, + range: 300, + speed: 500, // Fast! + cooldown: 2500, + requiresStaff: 'lightning', + description: 'Strike with lightning', + effect: 'stun' + }); + + this.addSpell('chain_lightning', { + name: 'Chain Lightning', + element: 'lightning', + manaCost: 60, + damage: 45, + range: 200, + chains: 5, // Hits 5 targets + cooldown: 7000, + requiresStaff: 'lightning', + description: 'Lightning jumps between enemies' + }); + + // ===== EARTH MAGIC ===== + this.addSpell('stone_spike', { + name: 'Stone Spike', + element: 'earth', + manaCost: 30, + damage: 80, + range: 150, + cooldown: 3000, + requiresStaff: 'earth', + description: 'Erupt spikes from the ground' + }); + + this.addSpell('earth_shield', { + name: 'Earth Shield', + element: 'earth', + manaCost: 40, + defense: 50, // Damage reduction + duration: 10000, // 10 seconds + cooldown: 15000, + requiresStaff: 'earth', + description: 'Summon protective barrier', + isBuff: true + }); + + // ===== LIGHT MAGIC ===== + this.addSpell('heal', { + name: 'Healing Light', + element: 'light', + manaCost: 35, + healing: 50, + cooldown: 4000, + requiresStaff: 'light', + description: 'Restore health', + isHeal: true + }); + + this.addSpell('holy_smite', { + name: 'Holy Smite', + element: 'light', + manaCost: 45, + damage: 90, + range: 180, + effectiveVs: ['undead', 'dark'], + cooldown: 5000, + requiresStaff: 'light', + description: 'Devastating against undead' + }); + + // ===== DARK MAGIC ===== + this.addSpell('shadow_bolt', { + name: 'Shadow Bolt', + element: 'dark', + manaCost: 20, + damage: 55, + range: 220, + speed: 280, + lifeSteal: 0.3, // 30% damage as healing + cooldown: 2000, + requiresStaff: 'dark', + description: 'Drain life from enemies' + }); + + this.addSpell('curse', { + name: 'Curse of Weakness', + element: 'dark', + manaCost: 40, + debuff: { attack: 0.5, defense: 0.5 }, // 50% reduction + duration: 15000, + cooldown: 8000, + requiresStaff: 'dark', + description: 'Weaken enemy significantly', + isDebuff: true + }); + } + + initializeStaffs() { + this.addStaff('fire', { + name: 'Fire Staff', + element: 'fire', + manaBonus: 20, + spellPowerBonus: 1.2, // 20% more damage + unlockLevel: 10, + sprite: 'magic_staffs_6_types', // Frame 0 + cost: 5000, + blueprint: 'blueprint_fire_staff' + }); + + this.addStaff('ice', { + name: 'Ice Staff', + element: 'ice', + manaBonus: 20, + spellPowerBonus: 1.2, + unlockLevel: 10, + sprite: 'magic_staffs_6_types', // Frame 1 + cost: 5000, + blueprint: 'blueprint_ice_staff' + }); + + this.addStaff('lightning', { + name: 'Lightning Staff', + element: 'lightning', + manaBonus: 20, + spellPowerBonus: 1.2, + unlockLevel: 12, + sprite: 'magic_staffs_6_types', // Frame 2 + cost: 6000, + blueprint: 'blueprint_lightning_staff' + }); + + this.addStaff('earth', { + name: 'Earth Staff', + element: 'earth', + manaBonus: 30, + spellPowerBonus: 1.1, + defenseBonus: 10, + unlockLevel: 11, + sprite: 'magic_staffs_6_types', // Frame 3 + cost: 5500, + blueprint: 'blueprint_earth_staff' + }); + + this.addStaff('light', { + name: 'Light Staff', + element: 'light', + manaBonus: 25, + spellPowerBonus: 1.15, + healingBonus: 1.3, // 30% more healing + unlockLevel: 13, + sprite: 'magic_staffs_6_types', // Frame 4 + cost: 7000, + blueprint: 'blueprint_light_staff' + }); + + this.addStaff('dark', { + name: 'Dark Staff', + element: 'dark', + manaBonus: 20, + spellPowerBonus: 1.25, // Highest damage + lifeStealBonus: 0.1, // Extra 10% lifesteal + unlockLevel: 14, + sprite: 'magic_staffs_6_types', // Frame 5 + cost: 8000, + blueprint: 'blueprint_dark_staff' + }); + } + + initializePotions() { + this.addPotion('mana_potion', { + name: 'Mana Potion', + manaRestore: 50, + cooldown: 30000, // 30 second potion cooldown + cost: 50, + sprite: 'potions_elixirs_grid', // Frame 0 + description: 'Restore 50 mana' + }); + + this.addPotion('greater_mana_potion', { + name: 'Greater Mana Potion', + manaRestore: 100, + cooldown: 30000, + cost: 150, + sprite: 'potions_elixirs_grid', // Frame 1 + description: 'Restore 100 mana' + }); + + this.addPotion('magic_power_elixir', { + name: 'Magic Power Elixir', + spellPowerBuff: 1.5, // 50% more spell damage + duration: 60000, // 1 minute + cooldown: 120000, // 2 minute cooldown + cost: 300, + sprite: 'potions_elixirs_grid', // Frame 2 + description: 'Greatly increase spell power' + }); + } + + addSpell(id, data) { + this.spells.set(id, { id, ...data }); + } + + addStaff(id, data) { + this.staffs.set(id, { id, ...data }); + } + + addPotion(id, data) { + this.potions.set(id, { id, ...data }); + } + + // ===== SPELL CASTING ===== + + canCastSpell(spellId) { + const spell = this.spells.get(spellId); + if (!spell) { + return { canCast: false, reason: 'Spell not found' }; + } + + // Check mana + if (this.mana < spell.manaCost) { + return { + canCast: false, + reason: `Need ${spell.manaCost} mana (have ${this.mana.toFixed(0)})` + }; + } + + // Check staff requirement + if (spell.requiresStaff) { + if (!this.equippedStaff || this.equippedStaff.element !== spell.requiresStaff) { + return { + canCast: false, + reason: `Requires ${spell.requiresStaff} staff` + }; + } + } + + // Check cooldown + if (this.spellCooldowns.has(spellId)) { + const remaining = this.spellCooldowns.get(spellId) - Date.now(); + if (remaining > 0) { + return { + canCast: false, + reason: `Cooldown: ${(remaining / 1000).toFixed(1)}s` + }; + } + } + + return { canCast: true }; + } + + castSpell(spellId, targetX, targetY) { + const check = this.canCastSpell(spellId); + if (!check.canCast) { + this.scene.uiSystem?.showError(check.reason); + return false; + } + + const spell = this.spells.get(spellId); + + // Consume mana + this.mana -= spell.manaCost; + + // Apply cooldown + this.spellCooldowns.set(spellId, Date.now() + spell.cooldown); + + // Calculate spell power + const spellPower = this.getSpellPower(spell); + + // Execute spell effect + if (spell.isHeal) { + this.castHealSpell(spell, spellPower); + } else if (spell.isBuff) { + this.castBuffSpell(spell); + } else if (spell.isDebuff) { + this.castDebuffSpell(spell, targetX, targetY); + } else if (spell.aoe) { + this.castAoESpell(spell, targetX, targetY, spellPower); + } else { + this.castProjectileSpell(spell, targetX, targetY, spellPower); + } + + // Emit event + this.scene.events.emit('spellCast', { spellId, spell, targetX, targetY }); + + console.log(`โœจ Cast ${spell.name} (${spell.manaCost} mana)`); + return true; + } + + castProjectileSpell(spell, targetX, targetY, power) { + const playerX = this.scene.player.x; + const playerY = this.scene.player.y; + + // Create projectile sprite + const projectile = this.scene.physics.add.sprite( + playerX, + playerY, + `spell_${spell.element}` + ); + + projectile.setData('spell', spell); + projectile.setData('damage', spell.damage * power); + + // Aim at target + this.scene.physics.moveTo(projectile, targetX, targetY, spell.speed); + + // Add particle trail + if (this.scene.particleSystem) { + this.scene.particleSystem.createSpellTrail(projectile, spell.element); + } + + // Handle collision with enemies + this.scene.physics.add.overlap(projectile, this.scene.enemies, (proj, enemy) => { + this.hitEnemy(enemy, proj.getData('damage'), spell); + proj.destroy(); + }); + + // Auto-destroy after range + this.scene.time.delayedCall(spell.range / spell.speed * 1000, () => { + if (projectile && projectile.active) { + projectile.destroy(); + } + }); + + this.activeProjectiles.push(projectile); + } + + castAoESpell(spell, targetX, targetY, power) { + // Create explosion effect + if (this.scene.particleSystem) { + this.scene.particleSystem.createSpellExplosion(targetX, targetY, spell.element, spell.aoe); + } + + // Damage all enemies in radius + this.scene.enemies?.getChildren().forEach(enemy => { + const distance = Phaser.Math.Distance.Between( + targetX, targetY, enemy.x, enemy.y + ); + + if (distance <= spell.aoe) { + this.hitEnemy(enemy, spell.damage * power, spell); + } + }); + } + + castHealSpell(spell, power) { + const healing = spell.healing * power; + + // Heal player + if (this.scene.player.health !== undefined) { + this.scene.player.health = Math.min( + this.scene.player.maxHealth || 100, + this.scene.player.health + healing + ); + } + + // Visual effect + if (this.scene.particleSystem) { + this.scene.particleSystem.createHealEffect( + this.scene.player.x, + this.scene.player.y + ); + } + + this.scene.events.emit('playerHealed', { amount: healing }); + } + + castBuffSpell(spell) { + this.activeEffects.push({ + spell, + endTime: Date.now() + spell.duration, + type: 'buff' + }); + + this.scene.uiSystem?.showNotification({ + title: `${spell.name} Active!`, + message: `Defense +${spell.defense} for ${spell.duration / 1000}s`, + icon: 'magic', + duration: 3000 + }); + } + + castDebuffSpell(spell, targetX, targetY) { + // Find nearest enemy + const enemy = this.findNearestEnemy(targetX, targetY); + if (enemy) { + enemy.setData('debuff', { + spell, + endTime: Date.now() + spell.duration + }); + + // Visual effect + if (this.scene.particleSystem) { + this.scene.particleSystem.createDebuffEffect(enemy.x, enemy.y); + } + } + } + + hitEnemy(enemy, damage, spell) { + // Apply damage + const finalDamage = damage; + enemy.takeDamage?.(finalDamage); + + // Apply effect + if (spell.effect) { + this.applyEffect(enemy, spell.effect, spell); + } + + // Lifesteal + if (spell.lifeSteal && this.scene.player.health !== undefined) { + const healing = finalDamage * spell.lifeSteal; + this.scene.player.health = Math.min( + this.scene.player.maxHealth || 100, + this.scene.player.health + healing + ); + } + } + + applyEffect(target, effectType, spell) { + switch (effectType) { + case 'burn': + // Damage over time + target.setData('burning', { duration: 3000, dps: 10 }); + break; + case 'slow': + // Reduce speed + target.setData('slowed', { duration: 2000, factor: 0.5 }); + break; + case 'freeze': + // Stop movement + target.setData('frozen', { duration: 1500 }); + target.setVelocity(0, 0); + break; + case 'stun': + // Cannot act + target.setData('stunned', { duration: 1000 }); + break; + } + } + + // ===== STAFF MANAGEMENT ===== + + equipStaff(staffId) { + const staff = this.staffs.get(staffId); + if (!staff) return false; + + this.equippedStaff = staff; + + // Apply bonuses + this.maxMana = 100 + (staff.manaBonus || 0); + + this.scene.uiSystem?.showNotification({ + title: `Equipped ${staff.name}`, + message: `+${staff.manaBonus} max mana`, + icon: 'magic', + duration: 2000 + }); + + return true; + } + + getSpellPower(spell) { + let power = 1.0; + + // Staff bonus + if (this.equippedStaff) { + power *= this.equippedStaff.spellPowerBonus || 1.0; + + // Healing bonus + if (spell.isHeal && this.equippedStaff.healingBonus) { + power *= this.equippedStaff.healingBonus; + } + } + + // Active potion buffs + this.activeEffects.forEach(effect => { + if (effect.spell.spellPowerBuff) { + power *= effect.spell.spellPowerBuff; + } + }); + + return power; + } + + // ===== POTION SYSTEM ===== + + drinkPotion(potionId) { + const potion = this.potions.get(potionId); + if (!potion) return false; + + // Check cooldown + if (this.spellCooldowns.has(`potion_${potionId}`)) { + const remaining = this.spellCooldowns.get(`potion_${potionId}`) - Date.now(); + if (remaining > 0) { + this.scene.uiSystem?.showError(`Potion cooldown: ${(remaining / 1000).toFixed(0)}s`); + return false; + } + } + + // Restore mana + if (potion.manaRestore) { + this.mana = Math.min(this.maxMana, this.mana + potion.manaRestore); + } + + // Apply buff + if (potion.spellPowerBuff) { + this.activeEffects.push({ + spell: potion, + endTime: Date.now() + potion.duration, + type: 'potion_buff' + }); + } + + // Set cooldown + this.spellCooldowns.set(`potion_${potionId}`, Date.now() + potion.cooldown); + + console.log(`๐Ÿงช Drank ${potion.name}`); + return true; + } + + // ===== HELPER FUNCTIONS ===== + + findNearestEnemy(x, y) { + let nearest = null; + let minDist = Infinity; + + this.scene.enemies?.getChildren().forEach(enemy => { + const dist = Phaser.Math.Distance.Between(x, y, enemy.x, enemy.y); + if (dist < minDist) { + minDist = dist; + nearest = enemy; + } + }); + + return nearest; + } + + // ===== DATA PERSISTENCE ===== + + saveMagicData() { + const data = { + mana: this.mana, + maxMana: this.maxMana, + equippedStaff: this.equippedStaff?.id + }; + localStorage.setItem('magicData', JSON.stringify(data)); + } + + loadMagicData() { + const saved = localStorage.getItem('magicData'); + if (saved) { + const data = JSON.parse(saved); + this.mana = data.mana; + this.maxMana = data.maxMana; + if (data.equippedStaff) { + this.equipStaff(data.equippedStaff); + } + console.log('โœจ Loaded magic data'); + } + } + + // ===== UPDATE ===== + + update(time, delta) { + // Regenerate mana + const deltaSeconds = delta / 1000; + this.mana = Math.min(this.maxMana, this.mana + this.manaRegenRate * deltaSeconds); + + // Update active effects + const now = Date.now(); + this.activeEffects = this.activeEffects.filter(effect => effect.endTime > now); + + // Clean up destroyed projectiles + this.activeProjectiles = this.activeProjectiles.filter(p => p.active); + + // Emit mana update + this.scene.events.emit('manaUpdate', { + current: this.mana, + max: this.maxMana, + percent: (this.mana / this.maxMana) * 100 + }); + } + + // ===== CLEANUP ===== + + destroy() { + this.saveMagicData(); + this.spells.clear(); + this.staffs.clear(); + this.potions.clear(); + this.activeEffects = []; + this.activeProjectiles = []; + this.spellCooldowns.clear(); + } +} diff --git a/src/systems/TransportSystem.js b/src/systems/TransportSystem.js new file mode 100644 index 0000000..6bf7e5c --- /dev/null +++ b/src/systems/TransportSystem.js @@ -0,0 +1,644 @@ +/** + * TransportSystem.js + * ================== + * Manages all transportation methods in the game + * - Horses (5 breeds, different speeds) + * - Carts & Wagons (cargo capacity) + * - Trains (track-based, high speed, high capacity) + * - Water transport (kayak, SUP, raft, boat) + * - Bicycles & Boards (longboard, mountain board, snowboard) + * + * Uses: transportation_vehicles_detailed_1766097668396.tsx + * train_repairs_3_states (broken โ†’ repairing โ†’ rideable) + * + * @author NovaFarma Team + * @date 2025-12-22 + */ + +export default class TransportSystem { + constructor(scene) { + this.scene = scene; + + // Current state + this.currentVehicle = null; + this.ownedVehicles = new Map(); + this.trainTracks = []; + + // Vehicle definitions + this.vehicleData = this.defineVehicles(); + + // Train repair state + this.trainRepairProgress = 0; + + console.log('๐Ÿš‚ TransportSystem initialized'); + } + + defineVehicles() { + return { + // ===== HORSES ===== + horse_basic: { + name: 'Basic Horse', + type: 'mount', + speed: 200, + capacity: 0, + cost: 500, + unlockLevel: 3, + sprite: 'horse_brown', + waterAllowed: false, + description: 'Reliable companion for travel' + }, + horse_fast: { + name: 'Racing Horse', + type: 'mount', + speed: 300, + capacity: 0, + cost: 1500, + unlockLevel: 7, + sprite: 'horse_white', + waterAllowed: false, + description: 'Swift steed for long distances' + }, + horse_draft: { + name: 'Draft Horse', + type: 'mount', + speed: 150, + capacity: 50, + cost: 1200, + unlockLevel: 5, + sprite: 'horse_black', + waterAllowed: false, + description: 'Strong horse, can carry cargo' + }, + + // ===== CARTS & WAGONS ===== + cart_wooden: { + name: 'Wooden Cart', + type: 'vehicle', + speed: 120, + capacity: 100, + cost: 800, + unlockLevel: 4, + blueprint: 'blueprint_cart', + sprite: 'cart_wooden', + waterAllowed: false, + requiresHorse: true, + description: 'Transport goods efficiently' + }, + wagon_large: { + name: 'Large Wagon', + type: 'vehicle', + speed: 100, + capacity: 250, + cost: 2500, + unlockLevel: 8, + blueprint: 'blueprint_wagon', + sprite: 'wagon_large', + waterAllowed: false, + requiresHorse: true, + description: 'Massive cargo capacity' + }, + + // ===== TRAINS ===== + train: { + name: 'Steam Train', + type: 'rail', + speed: 400, + capacity: 500, + cost: 10000, + unlockLevel: 15, + blueprint: 'blueprint_train', + sprite: 'train_rideable', + waterAllowed: false, + requiresTrack: true, + repairStages: ['broken', 'repairing', 'rideable'], + description: 'Ultimate transport - requires tracks' + }, + + // ===== WATER TRANSPORT ===== + kayak: { + name: 'Kayak', + type: 'water', + speed: 150, + capacity: 20, + cost: 300, + unlockLevel: 6, + sprite: 'kayak', + waterOnly: true, + description: 'Navigate rivers and lakes' + }, + sup: { + name: 'Stand-Up Paddleboard', + type: 'water', + speed: 100, + capacity: 5, + cost: 200, + unlockLevel: 5, + sprite: 'sup', + waterOnly: true, + description: 'Calm water exploration' + }, + raft: { + name: 'Wooden Raft', + type: 'water', + speed: 80, + capacity: 50, + cost: 150, + unlockLevel: 3, + sprite: 'raft', + waterOnly: true, + description: 'Basic water transport' + }, + boat: { + name: 'Fishing Boat', + type: 'water', + speed: 180, + capacity: 100, + cost: 1000, + unlockLevel: 10, + blueprint: 'blueprint_boat', + sprite: 'boat', + waterOnly: true, + description: 'Ocean-ready vessel' + }, + + // ===== BICYCLES & BOARDS ===== + bicycle: { + name: 'Bicycle', + type: 'manual', + speed: 180, + capacity: 10, + cost: 250, + unlockLevel: 2, + sprite: 'bicycle', + waterAllowed: false, + description: 'Energy-efficient travel' + }, + longboard: { + name: 'Longboard', + type: 'manual', + speed: 220, + capacity: 0, + cost: 150, + unlockLevel: 4, + sprite: 'longboard', + waterAllowed: false, + terrainBonus: { 'road': 1.5 }, // 50% faster on roads + description: 'Cruise downhill paths' + }, + mountain_board: { + name: 'Mountain Board', + type: 'manual', + speed: 200, + capacity: 5, + cost: 300, + unlockLevel: 6, + sprite: 'mountain_board', + waterAllowed: false, + terrainBonus: { 'mountain': 1.3 }, + description: 'Off-road board for rough terrain' + }, + snowboard: { + name: 'Snowboard', + type: 'manual', + speed: 250, + capacity: 0, + cost: 200, + unlockLevel: 5, + sprite: 'snowboard', + waterAllowed: false, + terrainRequired: 'snow', + terrainBonus: { 'snow': 2.0 }, // 2x speed on snow + description: 'Winter transport - snow only' + } + }; + } + + // ===== VEHICLE MOUNTING ===== + + canMount(vehicleId, playerX, playerY) { + const vehicle = this.vehicleData[vehicleId]; + if (!vehicle) { + return { canMount: false, reason: 'Vehicle not found' }; + } + + // Check if player owns this vehicle + if (!this.ownedVehicles.has(vehicleId)) { + return { canMount: false, reason: 'You don\'t own this vehicle' }; + } + + // Check if already mounted + if (this.currentVehicle) { + return { canMount: false, reason: 'Already using a vehicle' }; + } + + // Check level requirement + const playerLevel = this.scene.player?.stats?.level || 1; + if (playerLevel < vehicle.unlockLevel) { + return { + canMount: false, + reason: `Requires level ${vehicle.unlockLevel}` + }; + } + + // Check terrain compatibility + const currentTile = this.getTileAt(playerX, playerY); + + if (vehicle.waterOnly && !currentTile.properties?.isWater) { + return { canMount: false, reason: 'Water vehicles require water' }; + } + + if (!vehicle.waterAllowed && currentTile.properties?.isWater) { + return { canMount: false, reason: 'Cannot use on water' }; + } + + if (vehicle.terrainRequired && currentTile.properties?.terrain !== vehicle.terrainRequired) { + return { + canMount: false, + reason: `Requires ${vehicle.terrainRequired} terrain` + }; + } + + // Check if train requires tracks + if (vehicle.requiresTrack && !this.isOnTrack(playerX, playerY)) { + return { canMount: false, reason: 'Train requires tracks' }; + } + + // Check if cart/wagon requires horse + if (vehicle.requiresHorse && !this.hasHorse()) { + return { canMount: false, reason: 'Requires a horse to pull' }; + } + + return { canMount: true }; + } + + mount(vehicleId) { + const check = this.canMount(vehicleId, this.scene.player.x, this.scene.player.y); + if (!check.canMount) { + this.scene.uiSystem?.showError(check.reason); + return false; + } + + const vehicle = this.vehicleData[vehicleId]; + + // Update player speed + this.scene.player.baseSpeed = this.scene.player.baseSpeed || 100; + this.scene.player.setMaxVelocity(vehicle.speed); + + // Change player sprite (riding animation) + const originalTexture = this.scene.player.texture.key; + this.scene.player.setTexture(`player_on_${vehicle.type}`); + + // Store original state + this.currentVehicle = { + id: vehicleId, + data: vehicle, + originalSpeed: this.scene.player.baseSpeed, + originalTexture: originalTexture, + mountTime: Date.now() + }; + + // Show UI + this.scene.uiSystem?.showVehicleUI({ + name: vehicle.name, + speed: vehicle.speed, + capacity: vehicle.capacity, + currentCargo: 0 + }); + + // Emit event + this.scene.events.emit('vehicleMounted', { vehicleId, vehicle }); + + console.log(`๐Ÿšด Mounted ${vehicle.name} (${vehicle.speed} speed)`); + return true; + } + + dismount() { + if (!this.currentVehicle) { + return false; + } + + const vehicle = this.currentVehicle.data; + + // Restore player speed + this.scene.player.setMaxVelocity(this.currentVehicle.originalSpeed); + + // Restore player sprite + this.scene.player.setTexture(this.currentVehicle.originalTexture); + + // Hide UI + this.scene.uiSystem?.hideVehicleUI(); + + // Emit event + this.scene.events.emit('vehicleDismounted', { + vehicleId: this.currentVehicle.id, + rideDuration: Date.now() - this.currentVehicle.mountTime + }); + + console.log(`๐Ÿšถ Dismounted ${vehicle.name}`); + this.currentVehicle = null; + return true; + } + + // ===== VEHICLE PURCHASING ===== + + canPurchase(vehicleId) { + const vehicle = this.vehicleData[vehicleId]; + if (!vehicle) { + return { canPurchase: false, reason: 'Vehicle not found' }; + } + + // Check if already owned + if (this.ownedVehicles.has(vehicleId)) { + return { canPurchase: false, reason: 'Already owned' }; + } + + // Check level + const playerLevel = this.scene.player?.stats?.level || 1; + if (playerLevel < vehicle.unlockLevel) { + return { + canPurchase: false, + reason: `Requires level ${vehicle.unlockLevel}` + }; + } + + // Check blueprint (if required) + if (vehicle.blueprint && this.scene.recipeSystem) { + if (!this.scene.recipeSystem.unlockedRecipes.has(vehicle.blueprint)) { + return { canPurchase: false, reason: 'Blueprint not unlocked' }; + } + } + + // Check gold + const playerGold = this.getPlayerGold(); + if (playerGold < vehicle.cost) { + return { + canPurchase: false, + reason: `Need ${vehicle.cost} gold (have ${playerGold})` + }; + } + + return { canPurchase: true }; + } + + purchase(vehicleId) { + const check = this.canPurchase(vehicleId); + if (!check.canPurchase) { + this.scene.uiSystem?.showError(check.reason); + return false; + } + + const vehicle = this.vehicleData[vehicleId]; + + // Deduct gold + this.removeGold(vehicle.cost); + + // Add to owned vehicles + this.ownedVehicles.set(vehicleId, { + purchaseDate: Date.now(), + totalDistance: 0, + totalTime: 0 + }); + + // Save + this.saveOwnedVehicles(); + + // Notification + this.scene.uiSystem?.showNotification({ + title: '๐Ÿš— Vehicle Purchased!', + message: `${vehicle.name} is now yours!`, + icon: vehicle.sprite, + duration: 4000, + color: '#00FF00' + }); + + // Emit event + this.scene.events.emit('vehiclePurchased', { vehicleId, vehicle }); + + console.log(`๐Ÿ’ฐ Purchased ${vehicle.name} for ${vehicle.cost} gold`); + return true; + } + + // ===== TRAIN REPAIR SYSTEM ===== + + repairTrain(workAmount = 10) { + if (this.trainRepairProgress >= 100) { + this.scene.uiSystem?.showError('Train already repaired!'); + return false; + } + + // Add progress + this.trainRepairProgress += workAmount; + this.trainRepairProgress = Math.min(100, this.trainRepairProgress); + + // Get current state + const state = this.getTrainRepairState(); + + // Show progress + this.scene.uiSystem?.showNotification({ + title: 'Train Repair', + message: `${state} - ${this.trainRepairProgress.toFixed(0)}%`, + icon: 'train', + duration: 2000 + }); + + // Check if just completed + if (this.trainRepairProgress === 100 && state === 'Rideable') { + this.unlockTrain(); + } + + // Emit event + this.scene.events.emit('trainRepairProgress', { + progress: this.trainRepairProgress, + state + }); + + return true; + } + + getTrainRepairState() { + if (this.trainRepairProgress < 33) { + return 'Broken'; + } else if (this.trainRepairProgress < 100) { + return 'Repairing'; + } else { + return 'Rideable'; + } + } + + unlockTrain() { + // Add train to owned vehicles + this.ownedVehicles.set('train', { + purchaseDate: Date.now(), + totalDistance: 0, + totalTime: 0 + }); + + this.saveOwnedVehicles(); + + // Big notification + this.scene.uiSystem?.showNotification({ + title: '๐ŸŽ‰ TRAIN REPAIRED!', + message: 'The steam train is ready to ride!', + icon: 'train', + duration: 6000, + color: '#FFD700' + }); + + this.scene.events.emit('trainUnlocked'); + console.log('๐Ÿš‚ Train fully repaired and unlocked!'); + } + + // ===== TERRAIN BONUS SYSTEM ===== + + getSpeedModifier(x, y) { + if (!this.currentVehicle) return 1.0; + + const vehicle = this.currentVehicle.data; + const tile = this.getTileAt(x, y); + const terrain = tile.properties?.terrain; + + if (vehicle.terrainBonus && terrain && vehicle.terrainBonus[terrain]) { + return vehicle.terrainBonus[terrain]; + } + + return 1.0; + } + + getCurrentSpeed() { + if (!this.currentVehicle) { + return this.scene.player?.baseSpeed || 100; + } + + const baseSpeed = this.currentVehicle.data.speed; + const modifier = this.getSpeedModifier(this.scene.player.x, this.scene.player.y); + + return baseSpeed * modifier; + } + + // ===== HELPER FUNCTIONS ===== + + hasHorse() { + return this.ownedVehicles.has('horse_basic') || + this.ownedVehicles.has('horse_fast') || + this.ownedVehicles.has('horse_draft'); + } + + isOnTrack(x, y) { + // Check if position has train tracks + const tile = this.getTileAt(x, y); + return tile.properties?.hasTrack || false; + } + + getTileAt(x, y) { + // Get tile from terrain system + if (this.scene.terrainSystem) { + return this.scene.terrainSystem.getTileAtWorldXY(x, y); + } + + // Fallback + return { properties: {} }; + } + + getPlayerGold() { + if (this.scene.inventorySystem) { + return this.scene.inventorySystem.getQuantity('gold'); + } + return this.scene.player?.inventory?.gold || 0; + } + + removeGold(amount) { + if (this.scene.inventorySystem) { + this.scene.inventorySystem.removeItem('gold', amount); + } else { + this.scene.player.inventory.gold -= amount; + } + } + + // ===== GETTERS ===== + + getVehicle(vehicleId) { + return this.vehicleData[vehicleId]; + } + + getAllVehicles() { + return Object.entries(this.vehicleData).map(([id, data]) => ({ + id, + ...data, + owned: this.ownedVehicles.has(id) + })); + } + + getOwnedVehicles() { + return Array.from(this.ownedVehicles.keys()); + } + + getVehiclesByType(type) { + return this.getAllVehicles().filter(v => v.type === type); + } + + getPurchasableVehicles() { + return this.getAllVehicles().filter(v => + this.canPurchase(v.id).canPurchase + ); + } + + getCurrentVehicle() { + return this.currentVehicle; + } + + isRiding() { + return this.currentVehicle !== null; + } + + // ===== DATA PERSISTENCE ===== + + saveOwnedVehicles() { + const data = { + owned: Array.from(this.ownedVehicles.entries()), + trainProgress: this.trainRepairProgress + }; + localStorage.setItem('ownedVehicles', JSON.stringify(data)); + } + + loadOwnedVehicles() { + const saved = localStorage.getItem('ownedVehicles'); + if (saved) { + const data = JSON.parse(saved); + this.ownedVehicles = new Map(data.owned); + this.trainRepairProgress = data.trainProgress || 0; + console.log('๐Ÿš— Loaded', this.ownedVehicles.size, 'owned vehicles'); + } + } + + // ===== UPDATE ===== + + update(time, delta) { + if (this.currentVehicle) { + // Track distance and time + const deltaSeconds = delta / 1000; + const speed = this.getCurrentSpeed(); + const distance = (speed * deltaSeconds) / 60; // Approximate + + const stats = this.ownedVehicles.get(this.currentVehicle.id); + if (stats) { + stats.totalDistance += distance; + stats.totalTime += deltaSeconds; + } + + // Update speed modifier based on terrain + const modifier = this.getSpeedModifier(this.scene.player.x, this.scene.player.y); + if (modifier !== 1.0) { + this.scene.player.setMaxVelocity(this.currentVehicle.data.speed * modifier); + } + } + } + + // ===== CLEANUP ===== + + destroy() { + this.saveOwnedVehicles(); + this.ownedVehicles.clear(); + this.trainTracks = []; + this.currentVehicle = null; + } +}