vagabond-rpg-foundryvtt/module/data/item/spell.mjs
Cal Corum 6247004b58 Implement spell casting system with rulebook-accurate mana costs
- Add SpellCastDialog with delivery/duration/damage configuration
- Fix mana cost calculation to match rulebook formula:
  - Effect-only or 1d6 damage-only = 0 mana
  - Both damage AND effect = 1 mana base
  - +1 per extra damage die beyond first
  - +delivery cost (Touch/Remote/Imbue=0, Cube=1, Area=2)
  - Duration has no initial cost (Focus requires maintenance)
- Add "Include Effect" toggle for damage vs effect choice
- Create spell cast chat card template
- Add 20+ i18n strings for spell casting UI
- Create comprehensive Quench tests for mana calculation
- Add Cast Spell macro for testing
- Update CLAUDE.md with NoteDiscovery access instructions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 20:22:20 -06:00

201 lines
5.9 KiB
JavaScript

/**
* Spell Item Data Model
*
* Defines the data schema for spells in Vagabond RPG.
* Spells have dynamic mana costs based on:
* - Damage dice selected (each d6 costs 1 mana)
* - Delivery type (touch=0, remote=0, cube=1, aura/sphere/cone/line/glyph=2)
* - Duration (instant=0, focus=0, continual=+2 if >0 damage)
*
* @extends VagabondItemBase
*/
import VagabondItemBase from "./base-item.mjs";
export default class SpellData extends VagabondItemBase {
/**
* Define the schema for spell items.
*
* @returns {Object} The schema definition
*/
static defineSchema() {
const fields = foundry.data.fields;
const baseSchema = super.defineSchema();
return {
...baseSchema,
// Damage type (fire, cold, shock, etc.) - null for non-damaging spells
damageType: new fields.StringField({
required: false,
blank: true,
initial: "",
}),
// Base damage dice (e.g., "d6" - number selected at cast time)
damageBase: new fields.StringField({
required: false,
blank: true,
initial: "d6",
}),
// Maximum damage dice that can be used
maxDice: new fields.NumberField({
integer: true,
initial: 0, // 0 = no limit (use casting max)
min: 0,
}),
// Base effect description (what the spell does)
effect: new fields.HTMLField({
required: true,
blank: true,
}),
// Critical effect bonus
critEffect: new fields.HTMLField({
required: false,
blank: true,
}),
// Valid delivery types for this spell
deliveryTypes: new fields.SchemaField({
touch: new fields.BooleanField({ initial: false }),
remote: new fields.BooleanField({ initial: false }),
imbue: new fields.BooleanField({ initial: false }),
cube: new fields.BooleanField({ initial: false }),
aura: new fields.BooleanField({ initial: false }),
cone: new fields.BooleanField({ initial: false }),
glyph: new fields.BooleanField({ initial: false }),
line: new fields.BooleanField({ initial: false }),
sphere: new fields.BooleanField({ initial: false }),
}),
// Valid duration types for this spell
durationTypes: new fields.SchemaField({
instant: new fields.BooleanField({ initial: true }),
focus: new fields.BooleanField({ initial: false }),
continual: new fields.BooleanField({ initial: false }),
}),
// Casting skill used (can be overridden per-spell if different from class)
castingSkill: new fields.StringField({
required: false,
blank: true, // Empty = use class's actionStyle
}),
// Is this spell currently being focused?
focusing: new fields.BooleanField({ initial: false }),
// Tags for categorization/filtering
tags: new fields.ArrayField(new fields.StringField(), { initial: [] }),
};
}
/**
* Calculate the mana cost for casting this spell with given options.
*
* Mana Cost Formula (from rulebook):
* 1. Base cost: Only 1d6 damage OR only effect = 0 Mana; Both damage AND effect = 1 Mana
* 2. + Extra damage dice: +1 Mana per d6 beyond the first
* 3. + Delivery cost: Touch(0), Remote(0), Imbue(0), Cube(1), Aura/Cone/Glyph/Line/Sphere(2)
* 4. Duration: No initial cost (Focus requires 1 Mana/round to maintain on unwilling targets)
*
* @param {Object} options - Casting options
* @param {number} options.damageDice - Number of damage dice (default 0)
* @param {string} options.delivery - Delivery type (default "touch")
* @param {string} options.duration - Duration type (default "instant")
* @param {boolean} options.includeEffect - Whether casting includes the spell's effect (default true if spell has effect)
* @returns {number} Total mana cost
*/
calculateManaCost({
damageDice = 0,
delivery = "touch",
duration = "instant",
includeEffect = null,
} = {}) {
let cost = 0;
// Determine if spell has an effect (beyond just damage)
const hasEffect = includeEffect ?? Boolean(this.effect && this.effect.trim());
const hasDamage = damageDice > 0;
// Base cost: Both damage AND effect = 1 Mana; otherwise 0
if (hasDamage && hasEffect) {
cost += 1;
}
// Extra damage dice cost (+1 per die beyond the first)
if (damageDice > 1) {
cost += damageDice - 1;
}
// Delivery cost
const deliveryCosts = {
touch: 0,
remote: 0,
imbue: 0,
cube: 1,
aura: 2,
cone: 2,
glyph: 2,
line: 2,
sphere: 2,
};
cost += deliveryCosts[delivery] || 0;
// Duration doesn't add initial cost
// (Focus maintenance cost is handled separately during gameplay)
return cost;
}
/**
* Get valid delivery types as an array.
*
* @returns {Array<string>} Array of valid delivery type keys
*/
getValidDeliveryTypes() {
return Object.entries(this.deliveryTypes)
.filter(([, valid]) => valid)
.map(([type]) => type);
}
/**
* Get valid duration types as an array.
*
* @returns {Array<string>} Array of valid duration type keys
*/
getValidDurationTypes() {
return Object.entries(this.durationTypes)
.filter(([, valid]) => valid)
.map(([type]) => type);
}
/**
* Check if this is a damaging spell.
*
* @returns {boolean} True if spell deals damage
*/
isDamaging() {
return Boolean(this.damageType && this.damageBase);
}
/**
* Get chat card data for displaying spell information.
*
* @returns {Object} Chat card data
*/
getChatData() {
const data = super.getChatData();
data.damageType = this.damageType;
data.effect = this.effect;
data.critEffect = this.critEffect;
data.validDelivery = this.getValidDeliveryTypes();
data.validDuration = this.getValidDurationTypes();
data.isDamaging = this.isDamaging();
return data;
}
}