709 lines
20 KiB
JavaScript
709 lines
20 KiB
JavaScript
/**
|
|
* 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();
|
|
}
|
|
}
|