vagabond-rpg-foundryvtt/module/data/item/spell.mjs
Cal Corum 51f0472d99 Implement Phase 1: Complete data model system for actors and items
Actor Data Models:
- VagabondActorBase: Shared base class with biography field
- CharacterData: Full PC schema with stats, skills, saves, resources,
  custom crit thresholds, dynamic resources, item slots, wealth tracking
- NPCData: Monster stat block with HD, HP, TL, zone, morale, actions,
  abilities, immunities/weaknesses

Item Data Models:
- VagabondItemBase: Shared base with description field
- AncestryData: Being type, size, racial traits
- ClassData: Progression tables, features, mana/casting, trained skills
- SpellData: Dynamic mana cost calculation, delivery/duration types
- PerkData: Prerequisites system, stat/skill/spell requirements
- WeaponData: Damage, grip, properties, attack types, crit thresholds
- ArmorData: Armor value, type, dodge penalty
- EquipmentData: Quantity, slots, consumables
- FeatureData: Class features with Active Effect changes

Active Effects Integration:
- Helper module for creating and managing Active Effects
- Effect key mapping for stats, saves, skills, crit thresholds
- Utilities for applying/removing item effects

Derived Value Calculations (CharacterData):
- Max HP = Might × Level
- Speed by Dexterity lookup
- Item Slots = 8 + Might - Fatigue
- Save difficulties from stat pairs
- Skill difficulties (trained doubles stat contribution)

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

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

180 lines
5.0 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.
*
* @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")
* @returns {number} Total mana cost
*/
calculateManaCost({ damageDice = 0, delivery = "touch", duration = "instant" } = {}) {
let cost = 0;
// Damage dice cost (1 mana per die)
cost += damageDice;
// 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 cost (continual adds +2 if dealing damage)
if (duration === "continual" && damageDice > 0) {
cost += 2;
}
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;
}
}