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>
168 lines
4.2 KiB
JavaScript
168 lines
4.2 KiB
JavaScript
/**
|
|
* Feature Item Data Model
|
|
*
|
|
* Defines the data schema for class features and abilities in Vagabond RPG.
|
|
* Features are granted by classes at specific levels and can provide
|
|
* passive bonuses or active abilities.
|
|
*
|
|
* @extends VagabondItemBase
|
|
*/
|
|
import VagabondItemBase from "./base-item.mjs";
|
|
|
|
export default class FeatureData extends VagabondItemBase {
|
|
/**
|
|
* Define the schema for feature items.
|
|
*
|
|
* @returns {Object} The schema definition
|
|
*/
|
|
static defineSchema() {
|
|
const fields = foundry.data.fields;
|
|
const baseSchema = super.defineSchema();
|
|
|
|
return {
|
|
...baseSchema,
|
|
|
|
// Source class that grants this feature
|
|
sourceClass: new fields.StringField({
|
|
required: false,
|
|
blank: true,
|
|
}),
|
|
|
|
// Level at which this feature is gained
|
|
level: new fields.NumberField({
|
|
required: true,
|
|
integer: true,
|
|
initial: 1,
|
|
min: 1,
|
|
max: 10,
|
|
}),
|
|
|
|
// Is this a passive or active feature?
|
|
passive: new fields.BooleanField({ initial: true }),
|
|
|
|
// For active features: activation type
|
|
activation: new fields.SchemaField({
|
|
type: new fields.StringField({
|
|
initial: "",
|
|
choices: ["", "action", "bonus", "reaction", "free", "special"],
|
|
}),
|
|
cost: new fields.StringField({ required: false, blank: true }),
|
|
}),
|
|
|
|
// Usage tracking (for limited-use features)
|
|
uses: new fields.SchemaField({
|
|
value: new fields.NumberField({ integer: true, initial: 0 }),
|
|
max: new fields.NumberField({ integer: true, initial: 0 }),
|
|
per: new fields.StringField({
|
|
initial: "",
|
|
choices: ["", "short", "long", "day", "encounter"],
|
|
}),
|
|
}),
|
|
|
|
// 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, // CONST.ACTIVE_EFFECT_MODES.ADD
|
|
}),
|
|
value: new fields.StringField({ required: true }),
|
|
priority: new fields.NumberField({ integer: true, nullable: true }),
|
|
}),
|
|
{ initial: [] }
|
|
),
|
|
|
|
// Requirements beyond level (e.g., specific class choices)
|
|
requirements: new fields.StringField({
|
|
required: false,
|
|
blank: true,
|
|
}),
|
|
|
|
// Tags for categorization
|
|
tags: new fields.ArrayField(new fields.StringField(), { initial: [] }),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if this feature has uses that can be tracked.
|
|
*
|
|
* @returns {boolean} True if feature has limited uses
|
|
*/
|
|
hasUses() {
|
|
return this.uses.max > 0;
|
|
}
|
|
|
|
/**
|
|
* Check if this feature has remaining uses.
|
|
*
|
|
* @returns {boolean} True if uses remain or feature is unlimited
|
|
*/
|
|
hasRemainingUses() {
|
|
if (!this.hasUses()) return true;
|
|
return this.uses.value > 0;
|
|
}
|
|
|
|
/**
|
|
* Use one charge of this feature.
|
|
*
|
|
* @returns {Object} Result with success and new value
|
|
*/
|
|
use() {
|
|
if (!this.hasUses()) {
|
|
return { success: true, unlimited: true };
|
|
}
|
|
|
|
if (this.uses.value <= 0) {
|
|
return { success: false, reason: "No uses remaining" };
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
newValue: this.uses.value - 1,
|
|
remaining: this.uses.value - 1,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get the recovery text for this feature's uses.
|
|
*
|
|
* @returns {string} Recovery description
|
|
*/
|
|
getRecoveryText() {
|
|
if (!this.hasUses()) return "";
|
|
|
|
const perText = {
|
|
short: "per short rest",
|
|
long: "per long rest",
|
|
day: "per day",
|
|
encounter: "per encounter",
|
|
};
|
|
|
|
return perText[this.uses.per] || "";
|
|
}
|
|
|
|
/**
|
|
* Get chat card data for displaying feature information.
|
|
*
|
|
* @returns {Object} Chat card data
|
|
*/
|
|
getChatData() {
|
|
const data = super.getChatData();
|
|
|
|
data.sourceClass = this.sourceClass;
|
|
data.level = this.level;
|
|
data.passive = this.passive;
|
|
|
|
if (!this.passive && this.activation.type) {
|
|
data.activation = this.activation.type;
|
|
}
|
|
|
|
if (this.hasUses()) {
|
|
data.uses = `${this.uses.value}/${this.uses.max} ${this.getRecoveryText()}`;
|
|
}
|
|
|
|
return data;
|
|
}
|
|
}
|