// ============ magic.js — spell casting system ============ import { SPELLS } from '../core/config.js'; import { clamp } from '../core/util.js'; export function spellUnlocked(state, id) { const def = SPELLS[id]; if (!def) return false; if (state.sandbox || def.tier === 0) return true; return state.research.unlocked.includes(id); } export function canCast(state, id) { const def = SPELLS[id]; if (!spellUnlocked(state, id)) return { ok: false, why: 'Not researched' }; if ((state.spells.cds[id] || 0) > 0) return { ok: false, why: 'Recharging' }; if (state.mana < def.mana) return { ok: false, why: 'Not enough mana' }; return { ok: true }; } export function castSpell(state, id) { const chk = canCast(state, id); if (!chk.ok) return chk; const def = SPELLS[id]; state.mana -= def.mana; state.spells.cds[id] = def.cd; switch (id) { case 'sunburst': state.spells.active.sunburst = 2; // brief force-sunny state.weather.cur = 'sunny'; burst(state, '☀️'); break; case 'joy_aura': state.spells.active.joy_aura = def.dur; burst(state, '😊'); break; case 'healing_light': for (const h of state.heroes) if (h.alive) { h.hp = h.maxHp; state.effects.push({ kind: 'heal', x: h.x, y: h.y, t: 0, dur: 0.8 }); } burst(state, '💚'); break; case 'fortune_rain': state.spells.active.fortune_rain = def.dur; burst(state, '💸'); break; case 'swift_build': state.spells.active.swift_build = def.dur; burst(state, '⚡'); break; case 'monster_bane': { for (const mo of [...state.monsters]) mo.hp -= 70; for (let i = state.monsters.length - 1; i >= 0; i--) { if (state.monsters[i].hp <= 0) { /* deaths handled by battle loop */ } } burst(state, '💥'); state.toasts.push({ kind: 'magic', title: 'Monster Bane!', text: 'Arcane fire rains on the invaders.' }); break; } case 'warding_sigil': state.spells.active.warding_sigil = def.dur; burst(state, '🛡️'); state.toasts.push({ kind: 'magic', title: 'Warding Sigil raised', text: 'Monsters flee; invasions blocked while active.' }); break; case 'transmute': { transmuteGold(state); burst(state, '🪙'); break; } } return { ok: true }; } function transmuteGold(state) { state.cash += 900; state.finance.current['misc'] = (state.finance.current['misc'] || 0) + 900; state.toasts.push({ kind: 'gold', title: 'Transmutation!', text: '+$900 conjured from the arcane ether.' }); } function burst(state, icon) { state.effects.push({ kind: 'spellburst', icon, t: 0, dur: 1.4 }); } export function tickSpells(state, dt) { // cooldowns for (const k of Object.keys(state.spells.cds)) { if (state.spells.cds[k] > 0) state.spells.cds[k] -= dt; } // durations for (const k of Object.keys(state.spells.active)) { state.spells.active[k] -= dt; if (state.spells.active[k] <= 0) delete state.spells.active[k]; } // mana regen if (state.manaRegen === undefined) state.manaRegen = 0.4; state.mana = clamp(state.mana + state.manaRegen * dt, 0, state.manaMax); } /** construction discount multiplier from spells */ export function buildDiscount(state) { return state.spells.active.swift_build ? 0.5 : 1; }