/** * 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({ required: false, nullable: true, blank: false, initial: null, 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({ required: false, nullable: true, blank: false, initial: null, 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; } }