TransportSystem + MagicSystem - COMPLETE! All 5 game systems implemented (3000 LOC total)
This commit is contained in:
708
src/systems/MagicSystem.js
Normal file
708
src/systems/MagicSystem.js
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
644
src/systems/TransportSystem.js
Normal file
644
src/systems/TransportSystem.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user