vagabond-rpg-foundryvtt/module/data/item/perk.mjs
Cal Corum b975c1070f Add critical data model omissions from rulebook audit
- CharacterData: Add ancestryId reference, studiedDice resource pool,
  and statusEffects array with Countdown Dice support (d6→d4→ends)
- PerkData: Add luckCost/grantsLuck for Luck system integration,
  isRitual/ritualDuration/ritualComponents for ritual perks
- WeaponData/ArmorData/EquipmentData: Add relic schema with tier,
  unique abilities, attunement, uses per day, and lore fields
- Effects helper: Add effect keys for luck.max and studiedDice.max

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

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

212 lines
6.3 KiB
JavaScript

/**
* Perk Item Data Model
*
* Defines the data schema for perks (feats/talents) in Vagabond RPG.
* Characters gain 1 perk at odd levels (1, 3, 5, 7, 9).
*
* Perks have prerequisites that may include:
* - Minimum stat values
* - Training in specific skills
* - Knowledge of specific spells
* - Other perks
*
* @extends VagabondItemBase
*/
import VagabondItemBase from "./base-item.mjs";
export default class PerkData extends VagabondItemBase {
/**
* Define the schema for perk items.
*
* @returns {Object} The schema definition
*/
static defineSchema() {
const fields = foundry.data.fields;
const baseSchema = super.defineSchema();
return {
...baseSchema,
// Prerequisites
prerequisites: new fields.SchemaField({
// Stat requirements (e.g., { might: 4, dexterity: 3 })
stats: new fields.SchemaField({
might: new fields.NumberField({ integer: true, nullable: true, initial: null }),
dexterity: new fields.NumberField({ integer: true, nullable: true, initial: null }),
awareness: new fields.NumberField({ integer: true, nullable: true, initial: null }),
reason: new fields.NumberField({ integer: true, nullable: true, initial: null }),
presence: new fields.NumberField({ integer: true, nullable: true, initial: null }),
luck: new fields.NumberField({ integer: true, nullable: true, initial: null }),
}),
// Required skill training (array of skill IDs)
trainedSkills: new fields.ArrayField(new fields.StringField(), { initial: [] }),
// Required spells (array of spell names)
spells: new fields.ArrayField(new fields.StringField(), { initial: [] }),
// Required other perks (array of perk names)
perks: new fields.ArrayField(new fields.StringField(), { initial: [] }),
// Custom prerequisite text (for complex requirements)
custom: new fields.StringField({ required: false, blank: true }),
}),
// Mechanical effects (as Active Effect changes)
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: [] }
),
// Is this perk passive or does it require activation?
passive: new fields.BooleanField({ initial: true }),
// Usage tracking (for limited-use perks)
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: "" }), // "short", "long", "day", ""
}),
// Luck System Integration
// Cost in Luck points to activate this perk
luckCost: new fields.NumberField({
integer: true,
initial: 0,
min: 0,
}),
// Does this perk grant Luck when triggered?
grantsLuck: new fields.NumberField({
integer: true,
initial: 0,
min: 0,
}),
// Ritual System (specific perks are rituals with extended casting)
isRitual: new fields.BooleanField({ initial: false }),
// Ritual duration in minutes (10, 60, etc.)
ritualDuration: new fields.NumberField({
integer: true,
initial: 0,
min: 0,
}),
// Ritual components required (text description)
ritualComponents: new fields.StringField({
required: false,
blank: true,
}),
// Tags for categorization
tags: new fields.ArrayField(new fields.StringField(), { initial: [] }),
};
}
/**
* Check if a character meets this perk's prerequisites.
*
* @param {Object} actorData - The actor's system data
* @returns {Object} Result with 'met' boolean and 'missing' array of unmet requirements
*/
checkPrerequisites(actorData) {
const missing = [];
// Check stat requirements
for (const [stat, required] of Object.entries(this.prerequisites.stats)) {
if (required !== null && required > 0) {
const actorStat = actorData.stats?.[stat]?.value || 0;
if (actorStat < required) {
missing.push(`${stat.charAt(0).toUpperCase() + stat.slice(1)} ${required}`);
}
}
}
// Check skill training requirements
for (const skillId of this.prerequisites.trainedSkills) {
const skill = actorData.skills?.[skillId];
if (!skill?.trained) {
missing.push(`Trained in ${skillId}`);
}
}
// Note: Spell and perk prerequisites would need item checks on the parent actor
// These are tracked but validation requires access to actor items
if (this.prerequisites.spells.length > 0) {
missing.push(`Spells: ${this.prerequisites.spells.join(", ")}`);
}
if (this.prerequisites.perks.length > 0) {
missing.push(`Perks: ${this.prerequisites.perks.join(", ")}`);
}
if (this.prerequisites.custom) {
missing.push(this.prerequisites.custom);
}
return {
met: missing.length === 0,
missing,
};
}
/**
* Get a formatted string of prerequisites for display.
*
* @returns {string} Formatted prerequisite string
*/
getPrerequisiteString() {
const parts = [];
// Stat requirements
for (const [stat, required] of Object.entries(this.prerequisites.stats)) {
if (required !== null && required > 0) {
const abbr = stat.substring(0, 3).toUpperCase();
parts.push(`${abbr} ${required}`);
}
}
// Skill training
for (const skill of this.prerequisites.trainedSkills) {
parts.push(`Trained: ${skill}`);
}
// Spells
for (const spell of this.prerequisites.spells) {
parts.push(`Spell: ${spell}`);
}
// Perks
for (const perk of this.prerequisites.perks) {
parts.push(`Perk: ${perk}`);
}
// Custom
if (this.prerequisites.custom) {
parts.push(this.prerequisites.custom);
}
return parts.length > 0 ? parts.join(", ") : "None";
}
/**
* Get chat card data for displaying perk information.
*
* @returns {Object} Chat card data
*/
getChatData() {
const data = super.getChatData();
data.prerequisites = this.getPrerequisiteString();
data.passive = this.passive;
return data;
}
}