vagabond-rpg-foundryvtt/module/data/item/class.mjs
Cal Corum 8afcf8c07b Implement class feature automation system
- Add _onCreate/_preDelete lifecycle methods to VagabondItem for automatic
  feature application and cleanup when classes are added/removed
- Add updateActor hook to apply new features when character level increases
- Implement applyClassFeatures() with idempotency to prevent duplicate effects
- Add _applyClassProgression() for mana/castingMax from class progression
- Add _applyTrainedSkills() to mark class skills as trained
- Fix getCastingMaxAtLevel() to sum values instead of taking maximum
- Add comprehensive test suite (10 tests) covering unit and integration tests

Effects are tagged with vagabond flags for easy filtering and management.
Methods calculate progression values directly for robustness with embedded items.

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

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

170 lines
5.0 KiB
JavaScript

/**
* Class Item Data Model
*
* Defines the data schema for character classes in Vagabond RPG.
* Classes are designed as draggable items that can be added to characters.
*
* Each class provides:
* - Key stat recommendation
* - Action style (casting skill for casters)
* - Preferred combat zone
* - Training grants (skills trained)
* - Starting equipment pack
* - Progression table (level 1-10)
*
* @extends VagabondItemBase
*/
import VagabondItemBase from "./base-item.mjs";
export default class ClassData extends VagabondItemBase {
/**
* Define the schema for class items.
*
* @returns {Object} The schema definition
*/
static defineSchema() {
const fields = foundry.data.fields;
const baseSchema = super.defineSchema();
return {
...baseSchema,
// Key stat recommendation for this class
keyStat: new fields.StringField({
required: true,
initial: "might",
}),
// Action style / casting skill (for casters)
// e.g., "arcana" for Wizard, "mysticism" for Druid, "influence" for Sorcerer
actionStyle: new fields.StringField({
required: false,
blank: true,
}),
// Preferred combat zone
zone: new fields.StringField({
required: true,
initial: "frontline",
choices: ["frontline", "midline", "backline"],
}),
// Skills trained by this class (array of skill IDs)
trainedSkills: new fields.ArrayField(new fields.StringField(), { initial: [] }),
// Starting equipment pack description
startingPack: new fields.HTMLField({ required: false, blank: true }),
// Is this a spellcasting class?
isCaster: new fields.BooleanField({ initial: false }),
// Progression table - level-by-level benefits
progression: new fields.ArrayField(
new fields.SchemaField({
level: new fields.NumberField({ integer: true, min: 1, max: 10 }),
mana: new fields.NumberField({ integer: true, initial: 0 }),
castingMax: new fields.NumberField({ integer: true, initial: 0 }),
spellsKnown: new fields.NumberField({ integer: true, initial: 0 }),
features: new fields.ArrayField(new fields.StringField(), { initial: [] }),
}),
{ initial: [] }
),
// Class features - detailed definitions
features: new fields.ArrayField(
new fields.SchemaField({
name: new fields.StringField({ required: true }),
level: new fields.NumberField({ integer: true, min: 1, max: 10 }),
description: new fields.HTMLField({ required: true }),
passive: new fields.BooleanField({ initial: true }),
// Active Effect changes this feature applies
changes: new fields.ArrayField(
new fields.SchemaField({
key: new fields.StringField({ required: true }),
mode: new fields.NumberField({ integer: true, initial: 2 }),
value: new fields.StringField({ required: true }),
}),
{ initial: [] }
),
}),
{ initial: [] }
),
// Resource this class uses (if any) - e.g., "Studied Dice" for Alchemist
customResource: new fields.SchemaField({
name: new fields.StringField({ required: false }),
max: new fields.StringField({ required: false }), // Can be a formula like "@level"
}),
};
}
/**
* Get the features available at a given level.
*
* @param {number} level - Character level to check
* @returns {Array} Array of features available at or before this level
*/
getFeaturesAtLevel(level) {
return this.features.filter((f) => f.level <= level);
}
/**
* Get the progression entry for a given level.
*
* @param {number} level - Character level to check
* @returns {Object|null} Progression data for the level
*/
getProgressionAtLevel(level) {
return this.progression.find((p) => p.level === level) || null;
}
/**
* Get cumulative mana pool at a given level.
*
* @param {number} level - Character level
* @returns {number} Total mana pool
*/
getManaAtLevel(level) {
let totalMana = 0;
for (const prog of this.progression) {
if (prog.level <= level) {
totalMana += prog.mana || 0;
}
}
return totalMana;
}
/**
* Get cumulative casting max at a given level.
* Casting max increases are cumulative across levels.
*
* @param {number} level - Character level
* @returns {number} Casting max (max mana per spell)
*/
getCastingMaxAtLevel(level) {
let castingMax = 0;
for (const prog of this.progression) {
if (prog.level <= level) {
castingMax += prog.castingMax || 0;
}
}
return castingMax;
}
/**
* Get chat card data for displaying class information.
*
* @returns {Object} Chat card data
*/
getChatData() {
const data = super.getChatData();
data.keyStat = this.keyStat;
data.zone = this.zone;
data.isCaster = this.isCaster;
data.trainedSkills = this.trainedSkills;
return data;
}
}