vagabond-rpg-foundryvtt/module/data/actor/character.mjs
Cal Corum c06192f90f Add minor data model improvements from rulebook audit (16 items)
CharacterData additions:
- Senses (darksight, blindsight, tremorsense, elvenEyes, witchsight, sixthSense)
- Languages array (default: ["Common"])
- Rest tracking (lastRest, breathersTaken, restBonuses)
- Travel tracking (milesThisDay, pace, canForage, shiftsElapsed)
- Crafting projects (activeProjects with materials, shifts, bonuses)
- Combat tracking (isFlanked, flankingAllies, ignoresFlankingPenalty,
  currentZone, isDualWielding, mainHandWeapon, offHandWeapon)
- Casting tracking (equippedTrinket, canCastThroughWeapon/Instrument)
- Downtime activities with type and shifts
- Quest tracking (active, completed, lastQuestCompleted)
- Death state (isDead, deathCause, canBeRevived, luminaryRevivifyUsed)
- Summoned creatures (active array with type, HD, HP, command method)
- Size mechanical effects (unitsOccupied, allowsMovementThrough)
- Being type choices (humanlike, fae, cryptid, etc.)
- preferredZone for class-based positioning

NPCData additions:
- Morale status tracking (checkedThisCombat, lastTrigger, lastResult, broken)
- Senses (darksight, blindsight, tremorsense)

ArmorData additions:
- Medium armor type, hindersDodge, preventsRage flags

WeaponData additions:
- Damage type choices (blunt/piercing/slashing + elemental)
- equippedHand for dual-wielding (main/off/both)

EquipmentData additions:
- isTrinket and canCastThrough for spell component tracking

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 16:02:23 -06:00

700 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Character (PC) Data Model
*
* Defines the data schema for player characters in Vagabond RPG.
* Includes stats, skills, saves, resources, and derived value calculations.
*
* Key Mechanics:
* - Six stats: Might, Dexterity, Awareness, Reason, Presence, Luck
* - Derived values: Max HP = Might × Level, Speed based on DEX
* - 12 skills with trained/untrained status affecting difficulty
* - 3 saves (Reflex, Endure, Will) calculated from stat pairs
* - Resources: HP, Mana, Luck pool, Fatigue
* - Item slots: 8 + Might - Fatigue
*
* @extends VagabondActorBase
*/
import VagabondActorBase from "./base-actor.mjs";
export default class CharacterData extends VagabondActorBase {
/**
* Define the schema for character actors.
*
* @returns {Object} The schema definition
*/
static defineSchema() {
const fields = foundry.data.fields;
const baseSchema = super.defineSchema();
return {
...baseSchema,
// Reference to equipped ancestry item (UUID)
ancestryId: new fields.StringField({
required: false,
nullable: true,
blank: true,
initial: null,
}),
// Character level (1-10)
level: new fields.NumberField({
required: true,
nullable: false,
integer: true,
initial: 1,
min: 1,
max: 10,
}),
// Experience points
xp: new fields.NumberField({
required: true,
nullable: false,
integer: true,
initial: 0,
min: 0,
}),
// Six core stats
stats: new fields.SchemaField({
might: new fields.SchemaField({
value: new fields.NumberField({
required: true,
nullable: false,
integer: true,
initial: 2,
min: 1,
max: 10,
}),
}),
dexterity: new fields.SchemaField({
value: new fields.NumberField({
required: true,
nullable: false,
integer: true,
initial: 2,
min: 1,
max: 10,
}),
}),
awareness: new fields.SchemaField({
value: new fields.NumberField({
required: true,
nullable: false,
integer: true,
initial: 2,
min: 1,
max: 10,
}),
}),
reason: new fields.SchemaField({
value: new fields.NumberField({
required: true,
nullable: false,
integer: true,
initial: 2,
min: 1,
max: 10,
}),
}),
presence: new fields.SchemaField({
value: new fields.NumberField({
required: true,
nullable: false,
integer: true,
initial: 2,
min: 1,
max: 10,
}),
}),
luck: new fields.SchemaField({
value: new fields.NumberField({
required: true,
nullable: false,
integer: true,
initial: 2,
min: 1,
max: 10,
}),
}),
}),
// 12 skills with training and custom crit thresholds
skills: new fields.SchemaField({
arcana: new fields.SchemaField({
trained: new fields.BooleanField({ initial: false }),
critThreshold: new fields.NumberField({ integer: true, initial: 20, min: 1, max: 20 }),
}),
brawl: new fields.SchemaField({
trained: new fields.BooleanField({ initial: false }),
critThreshold: new fields.NumberField({ integer: true, initial: 20, min: 1, max: 20 }),
}),
craft: new fields.SchemaField({
trained: new fields.BooleanField({ initial: false }),
critThreshold: new fields.NumberField({ integer: true, initial: 20, min: 1, max: 20 }),
}),
detect: new fields.SchemaField({
trained: new fields.BooleanField({ initial: false }),
critThreshold: new fields.NumberField({ integer: true, initial: 20, min: 1, max: 20 }),
}),
finesse: new fields.SchemaField({
trained: new fields.BooleanField({ initial: false }),
critThreshold: new fields.NumberField({ integer: true, initial: 20, min: 1, max: 20 }),
}),
influence: new fields.SchemaField({
trained: new fields.BooleanField({ initial: false }),
critThreshold: new fields.NumberField({ integer: true, initial: 20, min: 1, max: 20 }),
}),
leadership: new fields.SchemaField({
trained: new fields.BooleanField({ initial: false }),
critThreshold: new fields.NumberField({ integer: true, initial: 20, min: 1, max: 20 }),
}),
medicine: new fields.SchemaField({
trained: new fields.BooleanField({ initial: false }),
critThreshold: new fields.NumberField({ integer: true, initial: 20, min: 1, max: 20 }),
}),
mysticism: new fields.SchemaField({
trained: new fields.BooleanField({ initial: false }),
critThreshold: new fields.NumberField({ integer: true, initial: 20, min: 1, max: 20 }),
}),
performance: new fields.SchemaField({
trained: new fields.BooleanField({ initial: false }),
critThreshold: new fields.NumberField({ integer: true, initial: 20, min: 1, max: 20 }),
}),
sneak: new fields.SchemaField({
trained: new fields.BooleanField({ initial: false }),
critThreshold: new fields.NumberField({ integer: true, initial: 20, min: 1, max: 20 }),
}),
survival: new fields.SchemaField({
trained: new fields.BooleanField({ initial: false }),
critThreshold: new fields.NumberField({ integer: true, initial: 20, min: 1, max: 20 }),
}),
}),
// Attack skills with crit thresholds
attacks: new fields.SchemaField({
melee: new fields.SchemaField({
critThreshold: new fields.NumberField({ integer: true, initial: 20, min: 1, max: 20 }),
}),
brawl: new fields.SchemaField({
critThreshold: new fields.NumberField({ integer: true, initial: 20, min: 1, max: 20 }),
}),
ranged: new fields.SchemaField({
critThreshold: new fields.NumberField({ integer: true, initial: 20, min: 1, max: 20 }),
}),
finesse: new fields.SchemaField({
critThreshold: new fields.NumberField({ integer: true, initial: 20, min: 1, max: 20 }),
}),
}),
// Resources (HP, Mana, Luck, Fatigue)
resources: new fields.SchemaField({
hp: new fields.SchemaField({
value: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
max: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
bonus: new fields.NumberField({ integer: true, initial: 0 }),
}),
mana: new fields.SchemaField({
value: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
max: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
castingMax: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
bonus: new fields.NumberField({ integer: true, initial: 0 }),
}),
luck: new fields.SchemaField({
value: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
max: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
}),
fatigue: new fields.SchemaField({
value: new fields.NumberField({ integer: true, initial: 0, min: 0, max: 5 }),
}),
// Studied Dice pool (Scholar class feature - d8s that can replace d20 rolls)
studiedDice: new fields.SchemaField({
value: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
max: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
}),
}),
// Custom resources (for class-specific tracking like Alchemist Formulae, Hunter's Mark, etc.)
customResources: new fields.ArrayField(
new fields.SchemaField({
name: new fields.StringField({ required: true }),
value: new fields.NumberField({ integer: true, initial: 0 }),
max: new fields.NumberField({ integer: true, initial: 0 }),
// Resource type for different tracking behaviors
type: new fields.StringField({
initial: "counter",
choices: ["counter", "tracker", "toggle", "list"],
}),
// Subtype for specific mechanics (formulae, marked-target, crit-reduction, etc.)
subtype: new fields.StringField({ required: false, blank: true }),
// When this resource resets
resetOn: new fields.StringField({
initial: "",
choices: ["", "rest", "turn", "round", "day", "combat"],
}),
// Flexible data storage for complex resources (formulae lists, target IDs, etc.)
data: new fields.ObjectField({ initial: {} }),
})
),
// Status Effects with Countdown Dice support
// Countdown Dice: Track duration with shrinking dice (d6 -> d4 -> ends)
statusEffects: new fields.ArrayField(
new fields.SchemaField({
// Effect name (e.g., "Burning", "Poisoned", "Blessed")
name: new fields.StringField({ required: true }),
// Effect description
description: new fields.StringField({ required: false, blank: true }),
// Source of the effect (item UUID, spell name, etc.)
source: new fields.StringField({ required: false, blank: true }),
// Icon path for display
icon: new fields.StringField({ initial: "icons/svg/aura.svg" }),
// Is this a beneficial or harmful effect?
beneficial: new fields.BooleanField({ initial: false }),
// Duration type: "countdown" (Cd4/Cd6), "rounds", "turns", "permanent"
durationType: new fields.StringField({
initial: "countdown",
choices: ["countdown", "rounds", "turns", "permanent"],
}),
// Current countdown die size (6 = d6, 4 = d4, 0 = ended)
countdownDie: new fields.NumberField({ integer: true, initial: 6, min: 0, max: 12 }),
// For rounds/turns duration: remaining count
durationValue: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
// Active Effect changes to apply while this status is active
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: [] }
),
// Armor value (from equipped armor)
armor: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
// Item slots tracking (8 + Might - Fatigue + bonuses)
itemSlots: new fields.SchemaField({
// Currently used slots (auto-calculated from inventory)
used: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
// Maximum available slots (auto-calculated)
max: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
// Total bonus from all sources (auto-calculated from bonuses array)
bonus: new fields.NumberField({ integer: true, initial: 0 }),
// Individual bonus sources for tracking (Orc Hulking, Pack Mule, etc.)
bonuses: new fields.ArrayField(
new fields.SchemaField({
source: new fields.StringField({ required: true }), // "Orc Hulking", "Pack Mule"
value: new fields.NumberField({ integer: true, initial: 0 }),
}),
{ initial: [] }
),
// Is the character overburdened (used > max)?
overburdened: new fields.BooleanField({ initial: false }),
}),
// Movement speeds (multiple types like NPCs)
speed: new fields.SchemaField({
// Walking speed (base from DEX)
walk: new fields.NumberField({ integer: true, initial: 30, min: 0 }),
// Flying speed (from spells, ancestry, features)
fly: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
// Swimming speed (Hunter Rover, some ancestries)
swim: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
// Climbing speed (Hunter Rover, some ancestries)
climb: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
// Burrowing speed (rare, some beasts)
burrow: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
// Bonus to walking speed from effects
bonus: new fields.NumberField({ integer: true, initial: 0 }),
}),
// Saves - difficulties will be calculated
saves: new fields.SchemaField({
reflex: new fields.SchemaField({
difficulty: new fields.NumberField({ integer: true, initial: 20 }),
bonus: new fields.NumberField({ integer: true, initial: 0 }),
}),
endure: new fields.SchemaField({
difficulty: new fields.NumberField({ integer: true, initial: 20 }),
bonus: new fields.NumberField({ integer: true, initial: 0 }),
}),
will: new fields.SchemaField({
difficulty: new fields.NumberField({ integer: true, initial: 20 }),
bonus: new fields.NumberField({ integer: true, initial: 0 }),
}),
}),
// Wealth tracking (Gold, Silver, Copper)
wealth: new fields.SchemaField({
gold: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
silver: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
copper: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
}),
// Character details
details: new fields.SchemaField({
// Size category with mechanical effects
size: new fields.StringField({
initial: "medium",
choices: ["small", "medium", "large", "huge", "giant", "colossal"],
}),
// Units occupied on grid (derived from size)
unitsOccupied: new fields.NumberField({ integer: true, initial: 1, min: 1 }),
// Small creatures don't block movement through their space
allowsMovementThrough: new fields.BooleanField({ initial: false }),
// Being type for targeting effects
beingType: new fields.StringField({
initial: "humanlike",
choices: [
"humanlike",
"fae",
"cryptid",
"artificial",
"undead",
"primordial",
"hellspawn",
"beast",
],
}),
}),
// Favor/Hinder tracking (d20 +/- d6 modifiers)
// Cancel each other 1-for-1, don't stack
favorHinder: new fields.SchemaField({
favor: new fields.ArrayField(
new fields.SchemaField({
source: new fields.StringField({ required: true }), // "Flanking", "Virtuoso", etc.
appliesTo: new fields.ArrayField(new fields.StringField()), // ["Attack Checks"], ["Reflex Saves"]
duration: new fields.StringField({
initial: "instant",
choices: ["instant", "until-next-turn", "focus", "continual", "permanent"],
}),
}),
{ initial: [] }
),
hinder: new fields.ArrayField(
new fields.SchemaField({
source: new fields.StringField({ required: true }), // "Heavy Armor", "Fog spell", etc.
appliesTo: new fields.ArrayField(new fields.StringField()), // ["Dodge Saves"], ["sight-based checks"]
duration: new fields.StringField({
initial: "instant",
choices: ["instant", "until-next-turn", "focus", "continual", "permanent"],
}),
}),
{ initial: [] }
),
}),
// Focus tracking for maintained spells
focus: new fields.SchemaField({
// Currently focused spell/effect
active: new fields.ArrayField(
new fields.SchemaField({
spellId: new fields.StringField({ required: false, blank: true }),
spellName: new fields.StringField({ required: true }),
target: new fields.StringField({ required: false, blank: true }), // Target ID or description
manaCostPerRound: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
requiresSaveCheck: new fields.BooleanField({ initial: false }), // Cast Check each Round?
canBeBroken: new fields.BooleanField({ initial: true }), // Can Focus be broken by damage?
}),
{ initial: [] }
),
// Maximum concurrent focus (usually 1, Ancient Growth = 2)
maxConcurrent: new fields.NumberField({ integer: true, initial: 1, min: 1 }),
}),
// Progression tracking for leveling
progression: new fields.SchemaField({
// XP pacing determines XP required per level
xpPacing: new fields.StringField({
initial: "normal",
choices: ["quick", "normal", "epic", "saga"],
}),
// Track which perks were gained at which level
perksGainedByLevel: new fields.ObjectField({ initial: {} }), // { "3": ["perkId1"], "5": ["perkId2"] }
// Track which stats were increased at which level
statIncreasesByLevel: new fields.ObjectField({ initial: {} }), // { "2": "might", "4": "dexterity" }
}),
// Senses and vision types
senses: new fields.SchemaField({
darksight: new fields.BooleanField({ initial: false }), // Dwarf, Goblin, Orc, Infravision perk
blindsight: new fields.NumberField({ integer: true, initial: 0, min: 0 }), // Range in feet
tremorsense: new fields.NumberField({ integer: true, initial: 0, min: 0 }), // Range in feet
// Special vision abilities from perks/ancestry
specialVision: new fields.SchemaField({
elvenEyes: new fields.BooleanField({ initial: false }), // Favor on sight Detect
witchsight: new fields.BooleanField({ initial: false }), // See Invisible, Favor vs illusions
sixthSense: new fields.BooleanField({ initial: false }), // Ignore Blinded for sight-based checks
}),
}),
// Known languages
languages: new fields.ArrayField(new fields.StringField(), { initial: ["Common"] }),
// Rest and breather tracking
restTracking: new fields.SchemaField({
// Timestamp of last full rest
lastRest: new fields.StringField({ required: false, blank: true }),
// Number of breathers taken in current combat/scene
breathersTaken: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
// Bonuses to rest (Song of Rest, Tricksy, etc.)
restBonuses: new fields.ArrayField(
new fields.SchemaField({
source: new fields.StringField({ required: true }), // "Song of Rest", "Tricksy"
effect: new fields.StringField({ required: true }), // "+PRS HP", "+1 Luck"
}),
{ initial: [] }
),
}),
// Travel and exploration tracking
travel: new fields.SchemaField({
// Miles traveled today (for Padfoot perk)
milesThisDay: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
// Current travel pace
pace: new fields.StringField({
initial: "normal",
choices: ["slow", "normal", "fast"],
}),
// Can forage at normal pace (Hunter Survivalist)
canForage: new fields.BooleanField({ initial: false }),
// Shifts elapsed (time tracking for cooldowns)
shiftsElapsed: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
}),
// Crafting projects in progress
crafting: new fields.SchemaField({
activeProjects: new fields.ArrayField(
new fields.SchemaField({
itemName: new fields.StringField({ required: true }),
targetValue: new fields.NumberField({ integer: true, initial: 0 }), // In silver
materialsCost: new fields.NumberField({ integer: true, initial: 0 }),
shiftsRequired: new fields.NumberField({ integer: true, initial: 1 }),
shiftsCompleted: new fields.NumberField({ integer: true, initial: 0 }),
bonuses: new fields.ArrayField(
new fields.SchemaField({
source: new fields.StringField({ required: true }), // "Master Artisan"
effect: new fields.StringField({ required: true }), // "2× Shifts"
}),
{ initial: [] }
),
}),
{ initial: [] }
),
}),
// Combat positioning and flanking
combat: new fields.SchemaField({
// Is this character currently flanked?
isFlanked: new fields.BooleanField({ initial: false }),
// IDs of allies providing flanking
flankingAllies: new fields.ArrayField(new fields.StringField(), { initial: [] }),
// Ignores flanking penalties (Situational Awareness perk)
ignoresFlankingPenalty: new fields.BooleanField({ initial: false }),
// Current combat zone
currentZone: new fields.StringField({
initial: "",
choices: ["", "frontline", "midline", "backline"],
}),
// Is dual-wielding?
isDualWielding: new fields.BooleanField({ initial: false }),
// Main hand weapon ID
mainHandWeapon: new fields.StringField({ required: false, blank: true }),
// Off hand weapon/shield ID
offHandWeapon: new fields.StringField({ required: false, blank: true }),
}),
// Casting and spell component tracking
casting: new fields.SchemaField({
// Currently equipped trinket item ID
equippedTrinket: new fields.StringField({ required: false, blank: true }),
// Can cast through weapon (Gish perk)
canCastThroughWeapon: new fields.BooleanField({ initial: false }),
// Can cast through musical instrument (Harmonic Resonance)
canCastThroughInstrument: new fields.BooleanField({ initial: false }),
}),
// Downtime activity tracking
downtime: new fields.SchemaField({
activities: new fields.ArrayField(
new fields.SchemaField({
type: new fields.StringField({
initial: "work",
choices: ["craft", "study", "carouse", "work", "research"],
}),
shiftsSpent: new fields.NumberField({ integer: true, initial: 0 }),
result: new fields.StringField({ required: false, blank: true }),
}),
{ initial: [] }
),
}),
// Quest tracking (for cooldowns like Medium perk)
quests: new fields.SchemaField({
activeQuests: new fields.ArrayField(new fields.StringField(), { initial: [] }),
completedQuests: new fields.ArrayField(new fields.StringField(), { initial: [] }),
// For Medium perk cooldown
lastQuestCompleted: new fields.StringField({ required: false, blank: true }),
}),
// Death and dying state
death: new fields.SchemaField({
isDead: new fields.BooleanField({ initial: false }),
deathCause: new fields.StringField({
initial: "",
choices: ["", "hp-zero", "body-destroyed", "fatigue-five"],
}),
canBeRevived: new fields.BooleanField({ initial: true }),
revivedCount: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
// Luminary Revivify used today?
luminaryRevivifyUsed: new fields.BooleanField({ initial: false }),
// Force of Nature used this combat?
forceOfNatureUsed: new fields.BooleanField({ initial: false }),
}),
// Summoned creatures tracking
summons: new fields.SchemaField({
active: new fields.ArrayField(
new fields.SchemaField({
id: new fields.StringField({ required: true }),
name: new fields.StringField({ required: true }),
type: new fields.StringField({
initial: "beast",
choices: ["companion", "familiar", "primordial", "beast", "undead"],
}),
source: new fields.StringField({ required: true }), // "Animal Companion", "Familiar perk"
hd: new fields.NumberField({ integer: true, initial: 1 }),
currentHP: new fields.NumberField({ integer: true, initial: 4 }),
maxHP: new fields.NumberField({ integer: true, initial: 4 }),
usesSkill: new fields.StringField({ initial: "survival" }), // Which skill for checks
commandMethod: new fields.StringField({
initial: "action",
choices: ["action", "skip-move", "automatic"],
}),
duration: new fields.StringField({
initial: "permanent",
choices: ["permanent", "focus", "shift", "scene"],
}),
}),
{ initial: [] }
),
maxConcurrent: new fields.NumberField({ integer: true, initial: 1, min: 1 }),
}),
// Preferred combat zone (from class)
preferredZone: new fields.StringField({
initial: "frontline",
choices: ["frontline", "midline", "backline", "flexible"],
}),
};
}
/**
* Prepare base data for the character.
* Sets up initial values before items are processed.
*/
prepareBaseData() {
super.prepareBaseData();
}
/**
* Prepare derived data for the character.
* Calculates all values that depend on stats, level, and other factors.
*/
prepareDerivedData() {
super.prepareDerivedData();
const stats = this.stats;
const level = this.level;
// Calculate Max HP: Might × Level + bonus
this.resources.hp.max = stats.might.value * level + this.resources.hp.bonus;
// Calculate Walking Speed based on Dexterity
const speedByDex = CONFIG.VAGABOND?.speedByDex || { 2: 25, 3: 25, 4: 30, 5: 30, 6: 35, 7: 35 };
const dexValue = Math.max(2, Math.min(7, stats.dexterity.value));
this.speed.walk = (speedByDex[dexValue] || 30) + this.speed.bonus;
// Calculate Item Slots: 8 + Might - Fatigue + bonus
const baseSlots = CONFIG.VAGABOND?.baseItemSlots || 8;
// Sum up all bonus sources
const totalBonus = this.itemSlots.bonuses.reduce((sum, b) => sum + b.value, 0);
this.itemSlots.bonus = totalBonus;
this.itemSlots.max =
baseSlots + stats.might.value - this.resources.fatigue.value + this.itemSlots.bonus;
// Check if overburdened
this.itemSlots.overburdened = this.itemSlots.used > this.itemSlots.max;
// Calculate Luck pool max (equals Luck stat)
this.resources.luck.max = stats.luck.value;
// Calculate Save Difficulties
// Reflex = DEX + AWR, Difficulty = 20 - (DEX + AWR) + bonus
this.saves.reflex.difficulty =
20 - (stats.dexterity.value + stats.awareness.value) - this.saves.reflex.bonus;
// Endure = MIT + MIT, Difficulty = 20 - (MIT × 2) + bonus
this.saves.endure.difficulty = 20 - stats.might.value * 2 - this.saves.endure.bonus;
// Will = RSN + PRS, Difficulty = 20 - (RSN + PRS) + bonus
this.saves.will.difficulty =
20 - (stats.reason.value + stats.presence.value) - this.saves.will.bonus;
// Calculate Skill Difficulties
this._calculateSkillDifficulties();
}
/**
* Calculate difficulty values for all skills.
* Untrained: 20 - stat
* Trained: 20 - (stat × 2)
*
* @private
*/
_calculateSkillDifficulties() {
const skillStats = CONFIG.VAGABOND?.skills || {};
for (const [skillId, skillData] of Object.entries(this.skills)) {
const skillConfig = skillStats[skillId];
if (!skillConfig) continue;
const statKey = skillConfig.stat;
const statValue = this.stats[statKey]?.value || 0;
const trained = skillData.trained;
// Calculate difficulty: 20 - stat (untrained) or 20 - stat×2 (trained)
skillData.difficulty = trained ? 20 - statValue * 2 : 20 - statValue;
// Store the associated stat for reference
skillData.stat = statKey;
}
}
/**
* Get the roll data for this character.
* Includes all stats, skills, and resources for use in Roll formulas.
*
* @returns {Object} Roll data object
*/
getRollData() {
const data = super.getRollData();
// Add stat values at top level for easy access in formulas
for (const [key, stat] of Object.entries(this.stats)) {
data[key] = stat.value;
}
// Add level
data.level = this.level;
return data;
}
}