Files
arcane-tycoon/js/game/magic.js
T
deepseek ac00687480 Arcane Tycoon — Heroes & Magic theme park tycoon game
Complete browser game inspired by OpenRCT2 with fantasy twist:
- Custom roller coaster designer with physics-based ratings + on-ride POV
- 10 animated rides, 7 shops, 16 scenery items, path network & guest AI
- Heroes guild vs monster invasions (5 classes, XP/gear/bosses)
- Magic spell system (8 spells), research tree, economy/marketing/loans
- Day-night cycle, weather, park rating, awards, 4 scenarios
- Save/load slots + autosave, procedural WebAudio SFX/music
- Isometric canvas renderer, minimap, diagnostics overlay
- Test suites: smoke(13), linkcheck, inputcheck, framecheck, rendercheck, flow
2026-08-23 06:59:21 +00:00

100 lines
3.3 KiB
JavaScript

// ============ 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;
}