Implement Phase 1: Complete data model system for actors and items
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>
This commit is contained in:
parent
44dbd00e1b
commit
51f0472d99
@ -77,7 +77,18 @@
|
|||||||
{
|
{
|
||||||
"builtinGlobals": true,
|
"builtinGlobals": true,
|
||||||
"hoist": "all",
|
"hoist": "all",
|
||||||
"allow": ["event", "name", "status", "parent", "top", "close", "open", "print"]
|
"allow": [
|
||||||
|
"event",
|
||||||
|
"name",
|
||||||
|
"status",
|
||||||
|
"parent",
|
||||||
|
"top",
|
||||||
|
"close",
|
||||||
|
"open",
|
||||||
|
"print",
|
||||||
|
"origin",
|
||||||
|
"CharacterData"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"no-var": "error",
|
"no-var": "error",
|
||||||
|
|||||||
@ -97,7 +97,7 @@
|
|||||||
"id": "1.1",
|
"id": "1.1",
|
||||||
"name": "Create base Actor data model",
|
"name": "Create base Actor data model",
|
||||||
"description": "Shared fields for all actors: name, img, type, system data container",
|
"description": "Shared fields for all actors: name, img, type, system data container",
|
||||||
"completed": false,
|
"completed": true,
|
||||||
"tested": false,
|
"tested": false,
|
||||||
"priority": "critical",
|
"priority": "critical",
|
||||||
"dependencies": ["0.1", "0.2"]
|
"dependencies": ["0.1", "0.2"]
|
||||||
@ -106,7 +106,7 @@
|
|||||||
"id": "1.2",
|
"id": "1.2",
|
||||||
"name": "Create Character (PC) data model",
|
"name": "Create Character (PC) data model",
|
||||||
"description": "Stats (MIT/DEX/AWR/RSN/PRS/LUK), derived values (HP, Speed, Saves, Skill difficulties), level, XP, ancestry, class reference",
|
"description": "Stats (MIT/DEX/AWR/RSN/PRS/LUK), derived values (HP, Speed, Saves, Skill difficulties), level, XP, ancestry, class reference",
|
||||||
"completed": false,
|
"completed": true,
|
||||||
"tested": false,
|
"tested": false,
|
||||||
"priority": "critical",
|
"priority": "critical",
|
||||||
"dependencies": ["1.1"]
|
"dependencies": ["1.1"]
|
||||||
@ -115,7 +115,7 @@
|
|||||||
"id": "1.3",
|
"id": "1.3",
|
||||||
"name": "Create NPC/Monster data model",
|
"name": "Create NPC/Monster data model",
|
||||||
"description": "HD, HP, TL, Zone, Morale, Appearing, Armor, Immune, Weak, Actions array, Abilities array",
|
"description": "HD, HP, TL, Zone, Morale, Appearing, Armor, Immune, Weak, Actions array, Abilities array",
|
||||||
"completed": false,
|
"completed": true,
|
||||||
"tested": false,
|
"tested": false,
|
||||||
"priority": "critical",
|
"priority": "critical",
|
||||||
"dependencies": ["1.1"]
|
"dependencies": ["1.1"]
|
||||||
@ -124,7 +124,7 @@
|
|||||||
"id": "1.4",
|
"id": "1.4",
|
||||||
"name": "Create dynamic resources system",
|
"name": "Create dynamic resources system",
|
||||||
"description": "Extensible resource tracker: HP, Mana, Luck, Fatigue, Studied Dice, custom resources with current/max/bonus fields",
|
"description": "Extensible resource tracker: HP, Mana, Luck, Fatigue, Studied Dice, custom resources with current/max/bonus fields",
|
||||||
"completed": false,
|
"completed": true,
|
||||||
"tested": false,
|
"tested": false,
|
||||||
"priority": "critical",
|
"priority": "critical",
|
||||||
"dependencies": ["1.2"]
|
"dependencies": ["1.2"]
|
||||||
@ -133,7 +133,7 @@
|
|||||||
"id": "1.5",
|
"id": "1.5",
|
||||||
"name": "Create Skills data structure",
|
"name": "Create Skills data structure",
|
||||||
"description": "12 skills with associated stat, trained boolean, difficulty calculation, custom crit threshold",
|
"description": "12 skills with associated stat, trained boolean, difficulty calculation, custom crit threshold",
|
||||||
"completed": false,
|
"completed": true,
|
||||||
"tested": false,
|
"tested": false,
|
||||||
"priority": "critical",
|
"priority": "critical",
|
||||||
"dependencies": ["1.2"]
|
"dependencies": ["1.2"]
|
||||||
@ -142,7 +142,7 @@
|
|||||||
"id": "1.6",
|
"id": "1.6",
|
||||||
"name": "Create base Item data model",
|
"name": "Create base Item data model",
|
||||||
"description": "Shared fields for all items: name, img, type, system data container, description",
|
"description": "Shared fields for all items: name, img, type, system data container, description",
|
||||||
"completed": false,
|
"completed": true,
|
||||||
"tested": false,
|
"tested": false,
|
||||||
"priority": "critical",
|
"priority": "critical",
|
||||||
"dependencies": ["0.1", "0.2"]
|
"dependencies": ["0.1", "0.2"]
|
||||||
@ -151,7 +151,7 @@
|
|||||||
"id": "1.7",
|
"id": "1.7",
|
||||||
"name": "Create Ancestry item data model",
|
"name": "Create Ancestry item data model",
|
||||||
"description": "Being type, size, traits array with name/description pairs",
|
"description": "Being type, size, traits array with name/description pairs",
|
||||||
"completed": false,
|
"completed": true,
|
||||||
"tested": false,
|
"tested": false,
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"dependencies": ["1.6"]
|
"dependencies": ["1.6"]
|
||||||
@ -160,7 +160,7 @@
|
|||||||
"id": "1.8",
|
"id": "1.8",
|
||||||
"name": "Create Class item data model",
|
"name": "Create Class item data model",
|
||||||
"description": "Key stat, action style, zone, training grants, starting pack, progression table (level -> features/mana/spells), feature definitions",
|
"description": "Key stat, action style, zone, training grants, starting pack, progression table (level -> features/mana/spells), feature definitions",
|
||||||
"completed": false,
|
"completed": true,
|
||||||
"tested": false,
|
"tested": false,
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"dependencies": ["1.6"]
|
"dependencies": ["1.6"]
|
||||||
@ -169,7 +169,7 @@
|
|||||||
"id": "1.9",
|
"id": "1.9",
|
||||||
"name": "Create Spell item data model",
|
"name": "Create Spell item data model",
|
||||||
"description": "Damage type, base effect, crit effect, valid delivery types, duration options, mana cost formula components",
|
"description": "Damage type, base effect, crit effect, valid delivery types, duration options, mana cost formula components",
|
||||||
"completed": false,
|
"completed": true,
|
||||||
"tested": false,
|
"tested": false,
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"dependencies": ["1.6"]
|
"dependencies": ["1.6"]
|
||||||
@ -178,7 +178,7 @@
|
|||||||
"id": "1.10",
|
"id": "1.10",
|
||||||
"name": "Create Perk item data model",
|
"name": "Create Perk item data model",
|
||||||
"description": "Prerequisites (stat requirements, training requirements, spell requirements), full description, mechanical effects",
|
"description": "Prerequisites (stat requirements, training requirements, spell requirements), full description, mechanical effects",
|
||||||
"completed": false,
|
"completed": true,
|
||||||
"tested": false,
|
"tested": false,
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"dependencies": ["1.6"]
|
"dependencies": ["1.6"]
|
||||||
@ -187,7 +187,7 @@
|
|||||||
"id": "1.11",
|
"id": "1.11",
|
||||||
"name": "Create Weapon item data model",
|
"name": "Create Weapon item data model",
|
||||||
"description": "Damage dice, grip type (1H/2H/Versatile), properties (Finesse, Thrown, Cleave, etc.), attack skill, slot cost, value",
|
"description": "Damage dice, grip type (1H/2H/Versatile), properties (Finesse, Thrown, Cleave, etc.), attack skill, slot cost, value",
|
||||||
"completed": false,
|
"completed": true,
|
||||||
"tested": false,
|
"tested": false,
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"dependencies": ["1.6"]
|
"dependencies": ["1.6"]
|
||||||
@ -196,7 +196,7 @@
|
|||||||
"id": "1.12",
|
"id": "1.12",
|
||||||
"name": "Create Armor item data model",
|
"name": "Create Armor item data model",
|
||||||
"description": "Armor value, type (Light/Heavy/Shield), slot cost, value, dodge penalty flag",
|
"description": "Armor value, type (Light/Heavy/Shield), slot cost, value, dodge penalty flag",
|
||||||
"completed": false,
|
"completed": true,
|
||||||
"tested": false,
|
"tested": false,
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"dependencies": ["1.6"]
|
"dependencies": ["1.6"]
|
||||||
@ -205,7 +205,7 @@
|
|||||||
"id": "1.13",
|
"id": "1.13",
|
||||||
"name": "Create Equipment item data model",
|
"name": "Create Equipment item data model",
|
||||||
"description": "Generic items: slot cost, value, description, quantity, consumable flag",
|
"description": "Generic items: slot cost, value, description, quantity, consumable flag",
|
||||||
"completed": false,
|
"completed": true,
|
||||||
"tested": false,
|
"tested": false,
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"dependencies": ["1.6"]
|
"dependencies": ["1.6"]
|
||||||
@ -214,7 +214,7 @@
|
|||||||
"id": "1.14",
|
"id": "1.14",
|
||||||
"name": "Create Feature item data model",
|
"name": "Create Feature item data model",
|
||||||
"description": "Class features as separate items: source class, level gained, description, mechanical effects, passive vs active",
|
"description": "Class features as separate items: source class, level gained, description, mechanical effects, passive vs active",
|
||||||
"completed": false,
|
"completed": true,
|
||||||
"tested": false,
|
"tested": false,
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"dependencies": ["1.6"]
|
"dependencies": ["1.6"]
|
||||||
@ -223,7 +223,7 @@
|
|||||||
"id": "1.15",
|
"id": "1.15",
|
||||||
"name": "Create Active Effects integration",
|
"name": "Create Active Effects integration",
|
||||||
"description": "System for items (classes, perks, features) to modify actor stats, crit thresholds, resources",
|
"description": "System for items (classes, perks, features) to modify actor stats, crit thresholds, resources",
|
||||||
"completed": false,
|
"completed": true,
|
||||||
"tested": false,
|
"tested": false,
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"dependencies": ["1.2", "1.8", "1.10", "1.14"]
|
"dependencies": ["1.2", "1.8", "1.10", "1.14"]
|
||||||
|
|||||||
27
module/data/_module.mjs
Normal file
27
module/data/_module.mjs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Vagabond RPG Data Models
|
||||||
|
*
|
||||||
|
* Central export point for all data model classes.
|
||||||
|
* These models define the schema and behavior for Actor and Item documents.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Actor data models
|
||||||
|
export * from "./actor/_module.mjs";
|
||||||
|
|
||||||
|
// Item data models
|
||||||
|
export * from "./item/_module.mjs";
|
||||||
|
|
||||||
|
// Re-export for convenience
|
||||||
|
export { VagabondActorBase, CharacterData, NPCData } from "./actor/_module.mjs";
|
||||||
|
|
||||||
|
export {
|
||||||
|
VagabondItemBase,
|
||||||
|
AncestryData,
|
||||||
|
ClassData,
|
||||||
|
SpellData,
|
||||||
|
PerkData,
|
||||||
|
WeaponData,
|
||||||
|
ArmorData,
|
||||||
|
EquipmentData,
|
||||||
|
FeatureData,
|
||||||
|
} from "./item/_module.mjs";
|
||||||
9
module/data/actor/_module.mjs
Normal file
9
module/data/actor/_module.mjs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Actor Data Models
|
||||||
|
*
|
||||||
|
* Exports all actor data model classes for registration with Foundry.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { default as VagabondActorBase } from "./base-actor.mjs";
|
||||||
|
export { default as CharacterData } from "./character.mjs";
|
||||||
|
export { default as NPCData } from "./npc.mjs";
|
||||||
51
module/data/actor/base-actor.mjs
Normal file
51
module/data/actor/base-actor.mjs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Base Actor Data Model
|
||||||
|
*
|
||||||
|
* Provides shared data fields and methods for all actor types in Vagabond RPG.
|
||||||
|
* This is an abstract base class - use CharacterData or NPCData for actual actors.
|
||||||
|
*
|
||||||
|
* @extends foundry.abstract.TypeDataModel
|
||||||
|
*/
|
||||||
|
export default class VagabondActorBase extends foundry.abstract.TypeDataModel {
|
||||||
|
/**
|
||||||
|
* Define the base schema shared by all actors.
|
||||||
|
* Subclasses should call super.defineSchema() and extend the result.
|
||||||
|
*
|
||||||
|
* @returns {Object} The schema definition
|
||||||
|
*/
|
||||||
|
static defineSchema() {
|
||||||
|
const fields = foundry.data.fields;
|
||||||
|
return {
|
||||||
|
// Biography/description - rich text HTML content
|
||||||
|
biography: new fields.HTMLField({ required: false, blank: true }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare base data for the actor.
|
||||||
|
* Called before derived data calculation.
|
||||||
|
* Override in subclasses to add type-specific base preparation.
|
||||||
|
*/
|
||||||
|
prepareBaseData() {
|
||||||
|
// Base preparation - subclasses can override
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare derived data for the actor.
|
||||||
|
* Called after base data and before render.
|
||||||
|
* Override in subclasses to calculate derived values.
|
||||||
|
*/
|
||||||
|
prepareDerivedData() {
|
||||||
|
// Derived data calculation - subclasses should override
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the roll data for this actor for use in Roll formulas.
|
||||||
|
*
|
||||||
|
* @returns {Object} Roll data object with actor's stats and values
|
||||||
|
*/
|
||||||
|
getRollData() {
|
||||||
|
const data = { ...this };
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
356
module/data/actor/character.mjs
Normal file
356
module/data/actor/character.mjs
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
|
||||||
|
// 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 }),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Custom resources (for class-specific tracking like Studied Dice)
|
||||||
|
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 }),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
|
||||||
|
// Armor value (from equipped armor)
|
||||||
|
armor: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
|
||||||
|
|
||||||
|
// Item slots tracking
|
||||||
|
itemSlots: new fields.SchemaField({
|
||||||
|
used: 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 }),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Movement speed
|
||||||
|
speed: new fields.SchemaField({
|
||||||
|
value: new fields.NumberField({ integer: true, initial: 30 }),
|
||||||
|
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: new fields.StringField({ initial: "medium" }),
|
||||||
|
beingType: new fields.StringField({ initial: "humanlike" }),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 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.value = (speedByDex[dexValue] || 30) + this.speed.bonus;
|
||||||
|
|
||||||
|
// Calculate Item Slots: 8 + Might - Fatigue + bonus
|
||||||
|
const baseSlots = CONFIG.VAGABOND?.baseItemSlots || 8;
|
||||||
|
this.itemSlots.max =
|
||||||
|
baseSlots + stats.might.value - this.resources.fatigue.value + this.itemSlots.bonus;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
199
module/data/actor/npc.mjs
Normal file
199
module/data/actor/npc.mjs
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
/**
|
||||||
|
* NPC/Monster Data Model
|
||||||
|
*
|
||||||
|
* Defines the data schema for non-player characters and monsters in Vagabond RPG.
|
||||||
|
* Uses a simplified stat block format common in OSR-style games.
|
||||||
|
*
|
||||||
|
* Key Attributes:
|
||||||
|
* - HD (Hit Dice): Determines combat prowess
|
||||||
|
* - HP: Hit points (separate from HD for flexibility)
|
||||||
|
* - TL (Threat Level): Encounter balancing value (0.1 to 10+)
|
||||||
|
* - Zone: AI behavior hint (Frontline, Midline, Backline)
|
||||||
|
* - Morale: 2d6 check threshold for fleeing
|
||||||
|
* - Actions: Attack actions with names and damage
|
||||||
|
* - Abilities: Special abilities and traits
|
||||||
|
*
|
||||||
|
* @extends VagabondActorBase
|
||||||
|
*/
|
||||||
|
import VagabondActorBase from "./base-actor.mjs";
|
||||||
|
|
||||||
|
export default class NPCData extends VagabondActorBase {
|
||||||
|
/**
|
||||||
|
* Define the schema for NPC actors.
|
||||||
|
*
|
||||||
|
* @returns {Object} The schema definition
|
||||||
|
*/
|
||||||
|
static defineSchema() {
|
||||||
|
const fields = foundry.data.fields;
|
||||||
|
const baseSchema = super.defineSchema();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseSchema,
|
||||||
|
|
||||||
|
// Hit Dice - represents overall power level
|
||||||
|
hd: new fields.NumberField({
|
||||||
|
required: true,
|
||||||
|
nullable: false,
|
||||||
|
integer: true,
|
||||||
|
initial: 1,
|
||||||
|
min: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Hit Points
|
||||||
|
hp: new fields.SchemaField({
|
||||||
|
value: new fields.NumberField({ integer: true, initial: 4, min: 0 }),
|
||||||
|
max: new fields.NumberField({ integer: true, initial: 4, min: 1 }),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Threat Level - used for encounter balancing
|
||||||
|
// 0.1 = minion, 1.0 = standard, 2.0+ = elite/boss
|
||||||
|
tl: new fields.NumberField({
|
||||||
|
required: true,
|
||||||
|
nullable: false,
|
||||||
|
initial: 1,
|
||||||
|
min: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Armor value
|
||||||
|
armor: new fields.NumberField({
|
||||||
|
required: true,
|
||||||
|
nullable: false,
|
||||||
|
integer: true,
|
||||||
|
initial: 0,
|
||||||
|
min: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Morale score - flee on 2d6 > Morale when triggered
|
||||||
|
morale: new fields.NumberField({
|
||||||
|
required: true,
|
||||||
|
nullable: false,
|
||||||
|
integer: true,
|
||||||
|
initial: 7,
|
||||||
|
min: 2,
|
||||||
|
max: 12,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Number appearing (for random encounters)
|
||||||
|
appearing: new fields.StringField({ initial: "1d6" }),
|
||||||
|
|
||||||
|
// Preferred combat zone (AI hint)
|
||||||
|
zone: new fields.StringField({
|
||||||
|
required: true,
|
||||||
|
initial: "frontline",
|
||||||
|
choices: ["frontline", "midline", "backline"],
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Size category
|
||||||
|
size: new fields.StringField({ initial: "medium" }),
|
||||||
|
|
||||||
|
// Being type (for targeting by certain effects)
|
||||||
|
beingType: new fields.StringField({ initial: "beast" }),
|
||||||
|
|
||||||
|
// Movement speed
|
||||||
|
speed: new fields.SchemaField({
|
||||||
|
value: new fields.NumberField({ integer: true, initial: 30 }),
|
||||||
|
fly: new fields.NumberField({ integer: true, initial: 0 }),
|
||||||
|
swim: new fields.NumberField({ integer: true, initial: 0 }),
|
||||||
|
climb: new fields.NumberField({ integer: true, initial: 0 }),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Damage immunities
|
||||||
|
immunities: new fields.ArrayField(new fields.StringField(), { initial: [] }),
|
||||||
|
|
||||||
|
// Damage vulnerabilities (weaknesses)
|
||||||
|
weaknesses: new fields.ArrayField(new fields.StringField(), { initial: [] }),
|
||||||
|
|
||||||
|
// Damage resistances
|
||||||
|
resistances: new fields.ArrayField(new fields.StringField(), { initial: [] }),
|
||||||
|
|
||||||
|
// Attack actions
|
||||||
|
actions: new fields.ArrayField(
|
||||||
|
new fields.SchemaField({
|
||||||
|
name: new fields.StringField({ required: true }),
|
||||||
|
description: new fields.HTMLField({ required: false, blank: true }),
|
||||||
|
attackType: new fields.StringField({ initial: "melee" }),
|
||||||
|
damage: new fields.StringField({ initial: "1d6" }),
|
||||||
|
damageType: new fields.StringField({ initial: "blunt" }),
|
||||||
|
range: new fields.StringField({ required: false }),
|
||||||
|
properties: new fields.ArrayField(new fields.StringField(), { initial: [] }),
|
||||||
|
}),
|
||||||
|
{ initial: [] }
|
||||||
|
),
|
||||||
|
|
||||||
|
// Special abilities
|
||||||
|
abilities: new fields.ArrayField(
|
||||||
|
new fields.SchemaField({
|
||||||
|
name: new fields.StringField({ required: true }),
|
||||||
|
description: new fields.HTMLField({ required: true }),
|
||||||
|
passive: new fields.BooleanField({ initial: true }),
|
||||||
|
}),
|
||||||
|
{ initial: [] }
|
||||||
|
),
|
||||||
|
|
||||||
|
// Loot/treasure
|
||||||
|
loot: new fields.HTMLField({ required: false, blank: true }),
|
||||||
|
|
||||||
|
// Notes for GM
|
||||||
|
gmNotes: new fields.HTMLField({ required: false, blank: true }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare derived data for the NPC.
|
||||||
|
*/
|
||||||
|
prepareDerivedData() {
|
||||||
|
super.prepareDerivedData();
|
||||||
|
|
||||||
|
// Ensure HP max is at least HD if not set higher
|
||||||
|
if (this.hp.max < this.hd) {
|
||||||
|
this.hp.max = this.hd * 4; // Default HD to HP conversion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get zone behavior description for the GM.
|
||||||
|
*
|
||||||
|
* @returns {string} Description of typical behavior for this zone
|
||||||
|
*/
|
||||||
|
getZoneBehavior() {
|
||||||
|
const behaviors = {
|
||||||
|
frontline:
|
||||||
|
"Engages in melee combat. Moves toward nearest enemy. Prioritizes blocking access to allies.",
|
||||||
|
midline:
|
||||||
|
"Maintains medium range. Uses ranged attacks or support abilities. Retreats if engaged in melee.",
|
||||||
|
backline:
|
||||||
|
"Stays at maximum range. Uses ranged attacks, magic, or support. Flees if enemies approach.",
|
||||||
|
};
|
||||||
|
return behaviors[this.zone] || behaviors.frontline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if morale check is needed.
|
||||||
|
* Typically triggered when:
|
||||||
|
* - First ally dies
|
||||||
|
* - NPC reduced to half HP
|
||||||
|
* - Leader dies
|
||||||
|
*
|
||||||
|
* @returns {boolean} True if morale should be checked
|
||||||
|
*/
|
||||||
|
shouldCheckMorale() {
|
||||||
|
// Check if at or below half HP
|
||||||
|
return this.hp.value <= Math.floor(this.hp.max / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the roll data for this NPC.
|
||||||
|
*
|
||||||
|
* @returns {Object} Roll data object
|
||||||
|
*/
|
||||||
|
getRollData() {
|
||||||
|
const data = super.getRollData();
|
||||||
|
|
||||||
|
// Add core stats for formulas
|
||||||
|
data.hd = this.hd;
|
||||||
|
data.armor = this.armor;
|
||||||
|
data.morale = this.morale;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
module/data/item/_module.mjs
Normal file
15
module/data/item/_module.mjs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Item Data Models
|
||||||
|
*
|
||||||
|
* Exports all item data model classes for registration with Foundry.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { default as VagabondItemBase } from "./base-item.mjs";
|
||||||
|
export { default as AncestryData } from "./ancestry.mjs";
|
||||||
|
export { default as ClassData } from "./class.mjs";
|
||||||
|
export { default as SpellData } from "./spell.mjs";
|
||||||
|
export { default as PerkData } from "./perk.mjs";
|
||||||
|
export { default as WeaponData } from "./weapon.mjs";
|
||||||
|
export { default as ArmorData } from "./armor.mjs";
|
||||||
|
export { default as EquipmentData } from "./equipment.mjs";
|
||||||
|
export { default as FeatureData } from "./feature.mjs";
|
||||||
66
module/data/item/ancestry.mjs
Normal file
66
module/data/item/ancestry.mjs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* Ancestry Item Data Model
|
||||||
|
*
|
||||||
|
* Defines the data schema for character ancestries (races) in Vagabond RPG.
|
||||||
|
* Examples: Human, Dwarf, Elf, Halfling, Draken, Goblin, Orc
|
||||||
|
*
|
||||||
|
* Ancestries provide:
|
||||||
|
* - Being type classification
|
||||||
|
* - Size category
|
||||||
|
* - Racial traits (abilities/features)
|
||||||
|
*
|
||||||
|
* @extends VagabondItemBase
|
||||||
|
*/
|
||||||
|
import VagabondItemBase from "./base-item.mjs";
|
||||||
|
|
||||||
|
export default class AncestryData extends VagabondItemBase {
|
||||||
|
/**
|
||||||
|
* Define the schema for ancestry items.
|
||||||
|
*
|
||||||
|
* @returns {Object} The schema definition
|
||||||
|
*/
|
||||||
|
static defineSchema() {
|
||||||
|
const fields = foundry.data.fields;
|
||||||
|
const baseSchema = super.defineSchema();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseSchema,
|
||||||
|
|
||||||
|
// Being type (Humanlike, Fae, Cryptid, etc.)
|
||||||
|
beingType: new fields.StringField({
|
||||||
|
required: true,
|
||||||
|
initial: "humanlike",
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Size category
|
||||||
|
size: new fields.StringField({
|
||||||
|
required: true,
|
||||||
|
initial: "medium",
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Racial traits - abilities granted by this ancestry
|
||||||
|
traits: new fields.ArrayField(
|
||||||
|
new fields.SchemaField({
|
||||||
|
name: new fields.StringField({ required: true }),
|
||||||
|
description: new fields.HTMLField({ required: true }),
|
||||||
|
}),
|
||||||
|
{ initial: [] }
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get chat card data for displaying ancestry information.
|
||||||
|
*
|
||||||
|
* @returns {Object} Chat card data
|
||||||
|
*/
|
||||||
|
getChatData() {
|
||||||
|
const data = super.getChatData();
|
||||||
|
|
||||||
|
data.beingType = this.beingType;
|
||||||
|
data.size = this.size;
|
||||||
|
data.traits = this.traits;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
125
module/data/item/armor.mjs
Normal file
125
module/data/item/armor.mjs
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
/**
|
||||||
|
* Armor Item Data Model
|
||||||
|
*
|
||||||
|
* Defines the data schema for armor in Vagabond RPG.
|
||||||
|
*
|
||||||
|
* Armor Types:
|
||||||
|
* - Light: Basic protection, no dodge penalty
|
||||||
|
* - Heavy: Better protection, may have dodge penalty
|
||||||
|
* - Shield: Adds to armor, held in hand
|
||||||
|
*
|
||||||
|
* @extends VagabondItemBase
|
||||||
|
*/
|
||||||
|
import VagabondItemBase from "./base-item.mjs";
|
||||||
|
|
||||||
|
export default class ArmorData extends VagabondItemBase {
|
||||||
|
/**
|
||||||
|
* Define the schema for armor items.
|
||||||
|
*
|
||||||
|
* @returns {Object} The schema definition
|
||||||
|
*/
|
||||||
|
static defineSchema() {
|
||||||
|
const fields = foundry.data.fields;
|
||||||
|
const baseSchema = super.defineSchema();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseSchema,
|
||||||
|
|
||||||
|
// Armor value provided
|
||||||
|
armorValue: new fields.NumberField({
|
||||||
|
required: true,
|
||||||
|
integer: true,
|
||||||
|
initial: 1,
|
||||||
|
min: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Armor type (light, heavy, shield)
|
||||||
|
armorType: new fields.StringField({
|
||||||
|
required: true,
|
||||||
|
initial: "light",
|
||||||
|
choices: ["light", "heavy", "shield"],
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Does this armor impose a dodge penalty?
|
||||||
|
dodgePenalty: new fields.BooleanField({ initial: false }),
|
||||||
|
|
||||||
|
// Inventory slot cost
|
||||||
|
slots: new fields.NumberField({
|
||||||
|
integer: true,
|
||||||
|
initial: 1,
|
||||||
|
min: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Monetary value (in copper)
|
||||||
|
value: new fields.NumberField({
|
||||||
|
integer: true,
|
||||||
|
initial: 0,
|
||||||
|
min: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Is this armor equipped?
|
||||||
|
equipped: new fields.BooleanField({ initial: false }),
|
||||||
|
|
||||||
|
// Bonus effects (magical armor)
|
||||||
|
magicBonus: new fields.NumberField({
|
||||||
|
integer: true,
|
||||||
|
initial: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Special properties or enchantments
|
||||||
|
properties: new fields.ArrayField(new fields.StringField(), { initial: [] }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the total armor value including magic bonus.
|
||||||
|
*
|
||||||
|
* @returns {number} Total armor value
|
||||||
|
*/
|
||||||
|
getTotalArmorValue() {
|
||||||
|
return this.armorValue + this.magicBonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the effective armor when equipped.
|
||||||
|
*
|
||||||
|
* @returns {number} Armor value if equipped, 0 otherwise
|
||||||
|
*/
|
||||||
|
getEquippedArmor() {
|
||||||
|
return this.equipped ? this.getTotalArmorValue() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate slot cost when equipped.
|
||||||
|
*
|
||||||
|
* @returns {number} Slot cost
|
||||||
|
*/
|
||||||
|
getEquippedSlots() {
|
||||||
|
return this.equipped ? this.slots : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this armor imposes dodge penalty when equipped.
|
||||||
|
*
|
||||||
|
* @returns {boolean} True if equipped and has dodge penalty
|
||||||
|
*/
|
||||||
|
hasActiveDodgePenalty() {
|
||||||
|
return this.equipped && this.dodgePenalty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get chat card data for displaying armor information.
|
||||||
|
*
|
||||||
|
* @returns {Object} Chat card data
|
||||||
|
*/
|
||||||
|
getChatData() {
|
||||||
|
const data = super.getChatData();
|
||||||
|
|
||||||
|
data.armorValue = this.getTotalArmorValue();
|
||||||
|
data.armorType = this.armorType;
|
||||||
|
data.dodgePenalty = this.dodgePenalty;
|
||||||
|
data.properties = this.properties;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
67
module/data/item/base-item.mjs
Normal file
67
module/data/item/base-item.mjs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* Base Item Data Model
|
||||||
|
*
|
||||||
|
* Provides shared data fields and methods for all item types in Vagabond RPG.
|
||||||
|
* This is an abstract base class - use specific item data models for actual items.
|
||||||
|
*
|
||||||
|
* @extends foundry.abstract.TypeDataModel
|
||||||
|
*/
|
||||||
|
export default class VagabondItemBase extends foundry.abstract.TypeDataModel {
|
||||||
|
/**
|
||||||
|
* Define the base schema shared by all items.
|
||||||
|
* Subclasses should call super.defineSchema() and extend the result.
|
||||||
|
*
|
||||||
|
* @returns {Object} The schema definition
|
||||||
|
*/
|
||||||
|
static defineSchema() {
|
||||||
|
const fields = foundry.data.fields;
|
||||||
|
return {
|
||||||
|
// Rich text description
|
||||||
|
description: new fields.HTMLField({ required: false, blank: true }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare base data for the item.
|
||||||
|
* Called before derived data calculation.
|
||||||
|
*/
|
||||||
|
prepareBaseData() {
|
||||||
|
// Base preparation - subclasses can override
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare derived data for the item.
|
||||||
|
* Called after base data and before render.
|
||||||
|
*/
|
||||||
|
prepareDerivedData() {
|
||||||
|
// Derived data calculation - subclasses should override
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the roll data for this item for use in Roll formulas.
|
||||||
|
*
|
||||||
|
* @returns {Object} Roll data object with item's values
|
||||||
|
*/
|
||||||
|
getRollData() {
|
||||||
|
const data = { ...this };
|
||||||
|
|
||||||
|
// Include parent actor's roll data if available
|
||||||
|
if (this.parent?.actor) {
|
||||||
|
data.actor = this.parent.actor.getRollData();
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a chat card data object for this item.
|
||||||
|
* Override in subclasses for type-specific chat output.
|
||||||
|
*
|
||||||
|
* @returns {Object} Chat card data
|
||||||
|
*/
|
||||||
|
getChatData() {
|
||||||
|
return {
|
||||||
|
description: this.description,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
168
module/data/item/class.mjs
Normal file
168
module/data/item/class.mjs
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
/**
|
||||||
|
* 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 casting max at a given level.
|
||||||
|
*
|
||||||
|
* @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 && prog.castingMax > castingMax) {
|
||||||
|
castingMax = prog.castingMax;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
153
module/data/item/equipment.mjs
Normal file
153
module/data/item/equipment.mjs
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
/**
|
||||||
|
* Equipment Item Data Model
|
||||||
|
*
|
||||||
|
* Defines the data schema for generic equipment/gear in Vagabond RPG.
|
||||||
|
* This covers adventuring gear, tools, consumables, and miscellaneous items.
|
||||||
|
*
|
||||||
|
* Examples: Rope, Torches, Potions, Lockpicks, Rations
|
||||||
|
*
|
||||||
|
* @extends VagabondItemBase
|
||||||
|
*/
|
||||||
|
import VagabondItemBase from "./base-item.mjs";
|
||||||
|
|
||||||
|
export default class EquipmentData extends VagabondItemBase {
|
||||||
|
/**
|
||||||
|
* Define the schema for equipment items.
|
||||||
|
*
|
||||||
|
* @returns {Object} The schema definition
|
||||||
|
*/
|
||||||
|
static defineSchema() {
|
||||||
|
const fields = foundry.data.fields;
|
||||||
|
const baseSchema = super.defineSchema();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseSchema,
|
||||||
|
|
||||||
|
// Item quantity
|
||||||
|
quantity: new fields.NumberField({
|
||||||
|
required: true,
|
||||||
|
integer: true,
|
||||||
|
initial: 1,
|
||||||
|
min: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Inventory slot cost (per item or per stack)
|
||||||
|
slots: new fields.NumberField({
|
||||||
|
integer: true,
|
||||||
|
initial: 1,
|
||||||
|
min: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Whether slots are per-item or for the whole stack
|
||||||
|
slotsPerItem: new fields.BooleanField({ initial: false }),
|
||||||
|
|
||||||
|
// Monetary value per item (in copper)
|
||||||
|
value: new fields.NumberField({
|
||||||
|
integer: true,
|
||||||
|
initial: 0,
|
||||||
|
min: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Is this a consumable item?
|
||||||
|
consumable: new fields.BooleanField({ initial: false }),
|
||||||
|
|
||||||
|
// For consumables: uses remaining
|
||||||
|
uses: new fields.SchemaField({
|
||||||
|
value: new fields.NumberField({ integer: true, initial: 0 }),
|
||||||
|
max: new fields.NumberField({ integer: true, initial: 0 }),
|
||||||
|
autoDestroy: new fields.BooleanField({ initial: true }),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Equipment category for organization
|
||||||
|
category: new fields.StringField({
|
||||||
|
initial: "gear",
|
||||||
|
choices: ["gear", "tool", "consumable", "container", "treasure", "misc"],
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Is this item currently equipped/active?
|
||||||
|
equipped: new fields.BooleanField({ initial: false }),
|
||||||
|
|
||||||
|
// Container capacity (if this is a container)
|
||||||
|
containerCapacity: new fields.NumberField({
|
||||||
|
integer: true,
|
||||||
|
initial: 0,
|
||||||
|
min: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Tags for filtering/searching
|
||||||
|
tags: new fields.ArrayField(new fields.StringField(), { initial: [] }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the total slot cost for this item stack.
|
||||||
|
*
|
||||||
|
* @returns {number} Total slots used
|
||||||
|
*/
|
||||||
|
getTotalSlots() {
|
||||||
|
if (this.slotsPerItem) {
|
||||||
|
return this.slots * this.quantity;
|
||||||
|
}
|
||||||
|
return this.slots;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the total value of this item stack.
|
||||||
|
*
|
||||||
|
* @returns {number} Total value in copper
|
||||||
|
*/
|
||||||
|
getTotalValue() {
|
||||||
|
return this.value * this.quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consume one use of a consumable item.
|
||||||
|
*
|
||||||
|
* @returns {Object} Result with new values
|
||||||
|
*/
|
||||||
|
consume() {
|
||||||
|
if (!this.consumable) {
|
||||||
|
return { consumed: false, reason: "Not consumable" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.uses.max > 0) {
|
||||||
|
// Uses-based consumable
|
||||||
|
if (this.uses.value <= 0) {
|
||||||
|
return { consumed: false, reason: "No uses remaining" };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
consumed: true,
|
||||||
|
newUses: this.uses.value - 1,
|
||||||
|
depleted: this.uses.value - 1 <= 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Quantity-based consumable
|
||||||
|
if (this.quantity <= 0) {
|
||||||
|
return { consumed: false, reason: "No items remaining" };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
consumed: true,
|
||||||
|
newQuantity: this.quantity - 1,
|
||||||
|
depleted: this.quantity - 1 <= 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get chat card data for displaying equipment information.
|
||||||
|
*
|
||||||
|
* @returns {Object} Chat card data
|
||||||
|
*/
|
||||||
|
getChatData() {
|
||||||
|
const data = super.getChatData();
|
||||||
|
|
||||||
|
data.quantity = this.quantity;
|
||||||
|
data.category = this.category;
|
||||||
|
data.consumable = this.consumable;
|
||||||
|
|
||||||
|
if (this.consumable && this.uses.max > 0) {
|
||||||
|
data.uses = `${this.uses.value}/${this.uses.max}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
167
module/data/item/feature.mjs
Normal file
167
module/data/item/feature.mjs
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
180
module/data/item/perk.mjs
Normal file
180
module/data/item/perk.mjs
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
/**
|
||||||
|
* 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", ""
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
179
module/data/item/spell.mjs
Normal file
179
module/data/item/spell.mjs
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
/**
|
||||||
|
* Spell Item Data Model
|
||||||
|
*
|
||||||
|
* Defines the data schema for spells in Vagabond RPG.
|
||||||
|
* Spells have dynamic mana costs based on:
|
||||||
|
* - Damage dice selected (each d6 costs 1 mana)
|
||||||
|
* - Delivery type (touch=0, remote=0, cube=1, aura/sphere/cone/line/glyph=2)
|
||||||
|
* - Duration (instant=0, focus=0, continual=+2 if >0 damage)
|
||||||
|
*
|
||||||
|
* @extends VagabondItemBase
|
||||||
|
*/
|
||||||
|
import VagabondItemBase from "./base-item.mjs";
|
||||||
|
|
||||||
|
export default class SpellData extends VagabondItemBase {
|
||||||
|
/**
|
||||||
|
* Define the schema for spell items.
|
||||||
|
*
|
||||||
|
* @returns {Object} The schema definition
|
||||||
|
*/
|
||||||
|
static defineSchema() {
|
||||||
|
const fields = foundry.data.fields;
|
||||||
|
const baseSchema = super.defineSchema();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseSchema,
|
||||||
|
|
||||||
|
// Damage type (fire, cold, shock, etc.) - null for non-damaging spells
|
||||||
|
damageType: new fields.StringField({
|
||||||
|
required: false,
|
||||||
|
blank: true,
|
||||||
|
initial: "",
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Base damage dice (e.g., "d6" - number selected at cast time)
|
||||||
|
damageBase: new fields.StringField({
|
||||||
|
required: false,
|
||||||
|
blank: true,
|
||||||
|
initial: "d6",
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Maximum damage dice that can be used
|
||||||
|
maxDice: new fields.NumberField({
|
||||||
|
integer: true,
|
||||||
|
initial: 0, // 0 = no limit (use casting max)
|
||||||
|
min: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Base effect description (what the spell does)
|
||||||
|
effect: new fields.HTMLField({
|
||||||
|
required: true,
|
||||||
|
blank: true,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Critical effect bonus
|
||||||
|
critEffect: new fields.HTMLField({
|
||||||
|
required: false,
|
||||||
|
blank: true,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Valid delivery types for this spell
|
||||||
|
deliveryTypes: new fields.SchemaField({
|
||||||
|
touch: new fields.BooleanField({ initial: false }),
|
||||||
|
remote: new fields.BooleanField({ initial: false }),
|
||||||
|
imbue: new fields.BooleanField({ initial: false }),
|
||||||
|
cube: new fields.BooleanField({ initial: false }),
|
||||||
|
aura: new fields.BooleanField({ initial: false }),
|
||||||
|
cone: new fields.BooleanField({ initial: false }),
|
||||||
|
glyph: new fields.BooleanField({ initial: false }),
|
||||||
|
line: new fields.BooleanField({ initial: false }),
|
||||||
|
sphere: new fields.BooleanField({ initial: false }),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Valid duration types for this spell
|
||||||
|
durationTypes: new fields.SchemaField({
|
||||||
|
instant: new fields.BooleanField({ initial: true }),
|
||||||
|
focus: new fields.BooleanField({ initial: false }),
|
||||||
|
continual: new fields.BooleanField({ initial: false }),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Casting skill used (can be overridden per-spell if different from class)
|
||||||
|
castingSkill: new fields.StringField({
|
||||||
|
required: false,
|
||||||
|
blank: true, // Empty = use class's actionStyle
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Is this spell currently being focused?
|
||||||
|
focusing: new fields.BooleanField({ initial: false }),
|
||||||
|
|
||||||
|
// Tags for categorization/filtering
|
||||||
|
tags: new fields.ArrayField(new fields.StringField(), { initial: [] }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the mana cost for casting this spell with given options.
|
||||||
|
*
|
||||||
|
* @param {Object} options - Casting options
|
||||||
|
* @param {number} options.damageDice - Number of damage dice (default 0)
|
||||||
|
* @param {string} options.delivery - Delivery type (default "touch")
|
||||||
|
* @param {string} options.duration - Duration type (default "instant")
|
||||||
|
* @returns {number} Total mana cost
|
||||||
|
*/
|
||||||
|
calculateManaCost({ damageDice = 0, delivery = "touch", duration = "instant" } = {}) {
|
||||||
|
let cost = 0;
|
||||||
|
|
||||||
|
// Damage dice cost (1 mana per die)
|
||||||
|
cost += damageDice;
|
||||||
|
|
||||||
|
// Delivery cost
|
||||||
|
const deliveryCosts = {
|
||||||
|
touch: 0,
|
||||||
|
remote: 0,
|
||||||
|
imbue: 0,
|
||||||
|
cube: 1,
|
||||||
|
aura: 2,
|
||||||
|
cone: 2,
|
||||||
|
glyph: 2,
|
||||||
|
line: 2,
|
||||||
|
sphere: 2,
|
||||||
|
};
|
||||||
|
cost += deliveryCosts[delivery] || 0;
|
||||||
|
|
||||||
|
// Duration cost (continual adds +2 if dealing damage)
|
||||||
|
if (duration === "continual" && damageDice > 0) {
|
||||||
|
cost += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cost;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get valid delivery types as an array.
|
||||||
|
*
|
||||||
|
* @returns {Array<string>} Array of valid delivery type keys
|
||||||
|
*/
|
||||||
|
getValidDeliveryTypes() {
|
||||||
|
return Object.entries(this.deliveryTypes)
|
||||||
|
.filter(([, valid]) => valid)
|
||||||
|
.map(([type]) => type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get valid duration types as an array.
|
||||||
|
*
|
||||||
|
* @returns {Array<string>} Array of valid duration type keys
|
||||||
|
*/
|
||||||
|
getValidDurationTypes() {
|
||||||
|
return Object.entries(this.durationTypes)
|
||||||
|
.filter(([, valid]) => valid)
|
||||||
|
.map(([type]) => type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this is a damaging spell.
|
||||||
|
*
|
||||||
|
* @returns {boolean} True if spell deals damage
|
||||||
|
*/
|
||||||
|
isDamaging() {
|
||||||
|
return Boolean(this.damageType && this.damageBase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get chat card data for displaying spell information.
|
||||||
|
*
|
||||||
|
* @returns {Object} Chat card data
|
||||||
|
*/
|
||||||
|
getChatData() {
|
||||||
|
const data = super.getChatData();
|
||||||
|
|
||||||
|
data.damageType = this.damageType;
|
||||||
|
data.effect = this.effect;
|
||||||
|
data.critEffect = this.critEffect;
|
||||||
|
data.validDelivery = this.getValidDeliveryTypes();
|
||||||
|
data.validDuration = this.getValidDurationTypes();
|
||||||
|
data.isDamaging = this.isDamaging();
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
203
module/data/item/weapon.mjs
Normal file
203
module/data/item/weapon.mjs
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
/**
|
||||||
|
* Weapon Item Data Model
|
||||||
|
*
|
||||||
|
* Defines the data schema for weapons in Vagabond RPG.
|
||||||
|
*
|
||||||
|
* Key properties:
|
||||||
|
* - Damage dice (e.g., "1d6", "2d6")
|
||||||
|
* - Grip type (1H, 2H, Versatile)
|
||||||
|
* - Attack skill (Melee, Brawl, Ranged, Finesse)
|
||||||
|
* - Weapon properties (Finesse, Thrown, Cleave, Reach, etc.)
|
||||||
|
* - Slot cost for inventory management
|
||||||
|
*
|
||||||
|
* @extends VagabondItemBase
|
||||||
|
*/
|
||||||
|
import VagabondItemBase from "./base-item.mjs";
|
||||||
|
|
||||||
|
export default class WeaponData extends VagabondItemBase {
|
||||||
|
/**
|
||||||
|
* Define the schema for weapon items.
|
||||||
|
*
|
||||||
|
* @returns {Object} The schema definition
|
||||||
|
*/
|
||||||
|
static defineSchema() {
|
||||||
|
const fields = foundry.data.fields;
|
||||||
|
const baseSchema = super.defineSchema();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseSchema,
|
||||||
|
|
||||||
|
// Damage dice formula
|
||||||
|
damage: new fields.StringField({
|
||||||
|
required: true,
|
||||||
|
initial: "1d6",
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Damage type
|
||||||
|
damageType: new fields.StringField({
|
||||||
|
required: true,
|
||||||
|
initial: "blunt",
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Bonus damage (from magic, etc.)
|
||||||
|
bonusDamage: new fields.NumberField({
|
||||||
|
integer: true,
|
||||||
|
initial: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Grip type (1h, 2h, versatile, fist)
|
||||||
|
grip: new fields.StringField({
|
||||||
|
required: true,
|
||||||
|
initial: "1h",
|
||||||
|
choices: ["1h", "2h", "versatile", "fist"],
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Versatile damage (when wielded 2H)
|
||||||
|
versatileDamage: new fields.StringField({
|
||||||
|
required: false,
|
||||||
|
blank: true,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Attack skill used
|
||||||
|
attackType: new fields.StringField({
|
||||||
|
required: true,
|
||||||
|
initial: "melee",
|
||||||
|
choices: ["melee", "brawl", "ranged", "finesse"],
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Range (for ranged/thrown weapons)
|
||||||
|
range: new fields.SchemaField({
|
||||||
|
value: new fields.NumberField({ integer: true, initial: 0 }),
|
||||||
|
units: new fields.StringField({ initial: "ft" }),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Weapon properties
|
||||||
|
properties: new fields.SchemaField({
|
||||||
|
finesse: new fields.BooleanField({ initial: false }),
|
||||||
|
thrown: new fields.BooleanField({ initial: false }),
|
||||||
|
cleave: new fields.BooleanField({ initial: false }),
|
||||||
|
reach: new fields.BooleanField({ initial: false }),
|
||||||
|
loading: new fields.BooleanField({ initial: false }),
|
||||||
|
brawl: new fields.BooleanField({ initial: false }),
|
||||||
|
crude: new fields.BooleanField({ initial: false }),
|
||||||
|
versatile: new fields.BooleanField({ initial: false }),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Inventory slot cost
|
||||||
|
slots: new fields.NumberField({
|
||||||
|
integer: true,
|
||||||
|
initial: 1,
|
||||||
|
min: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Monetary value (in copper)
|
||||||
|
value: new fields.NumberField({
|
||||||
|
integer: true,
|
||||||
|
initial: 0,
|
||||||
|
min: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Is this weapon equipped?
|
||||||
|
equipped: new fields.BooleanField({ initial: false }),
|
||||||
|
|
||||||
|
// Quantity (for ammunition, thrown weapons)
|
||||||
|
quantity: new fields.NumberField({
|
||||||
|
integer: true,
|
||||||
|
initial: 1,
|
||||||
|
min: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Attack bonus (from magic, etc.)
|
||||||
|
attackBonus: new fields.NumberField({
|
||||||
|
integer: true,
|
||||||
|
initial: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Critical threshold override (if different from skill default)
|
||||||
|
critThreshold: new fields.NumberField({
|
||||||
|
integer: true,
|
||||||
|
nullable: true,
|
||||||
|
initial: null,
|
||||||
|
min: 1,
|
||||||
|
max: 20,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the effective attack stat for this weapon.
|
||||||
|
* Accounts for Finesse property allowing DEX for melee.
|
||||||
|
*
|
||||||
|
* @returns {string} The stat key to use for attack rolls
|
||||||
|
*/
|
||||||
|
getAttackStat() {
|
||||||
|
const attackStats = {
|
||||||
|
melee: "might",
|
||||||
|
brawl: "might",
|
||||||
|
ranged: "awareness",
|
||||||
|
finesse: "dexterity",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Finesse weapons can use DEX for melee
|
||||||
|
if (this.properties.finesse && this.attackType === "melee") {
|
||||||
|
return "dexterity";
|
||||||
|
}
|
||||||
|
|
||||||
|
return attackStats[this.attackType] || "might";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the active weapon properties as an array.
|
||||||
|
*
|
||||||
|
* @returns {Array<string>} Array of active property keys
|
||||||
|
*/
|
||||||
|
getActiveProperties() {
|
||||||
|
return Object.entries(this.properties)
|
||||||
|
.filter(([, active]) => active)
|
||||||
|
.map(([prop]) => prop);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the effective damage formula.
|
||||||
|
*
|
||||||
|
* @param {boolean} twoHanded - Whether using two-handed grip for versatile
|
||||||
|
* @returns {string} Damage formula
|
||||||
|
*/
|
||||||
|
getDamageFormula(twoHanded = false) {
|
||||||
|
// Use versatile damage if 2H and available
|
||||||
|
if (twoHanded && this.properties.versatile && this.versatileDamage) {
|
||||||
|
return this.bonusDamage > 0
|
||||||
|
? `${this.versatileDamage}+${this.bonusDamage}`
|
||||||
|
: this.versatileDamage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.bonusDamage > 0 ? `${this.damage}+${this.bonusDamage}` : this.damage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the slot cost when equipped.
|
||||||
|
*
|
||||||
|
* @returns {number} Slot cost
|
||||||
|
*/
|
||||||
|
getEquippedSlots() {
|
||||||
|
return this.equipped ? this.slots : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get chat card data for displaying weapon information.
|
||||||
|
*
|
||||||
|
* @returns {Object} Chat card data
|
||||||
|
*/
|
||||||
|
getChatData() {
|
||||||
|
const data = super.getChatData();
|
||||||
|
|
||||||
|
data.damage = this.damage;
|
||||||
|
data.damageType = this.damageType;
|
||||||
|
data.grip = this.grip;
|
||||||
|
data.attackType = this.attackType;
|
||||||
|
data.properties = this.getActiveProperties();
|
||||||
|
data.range = this.range.value > 0 ? `${this.range.value} ${this.range.units}` : null;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
260
module/helpers/effects.mjs
Normal file
260
module/helpers/effects.mjs
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
/**
|
||||||
|
* Active Effects Helper Module
|
||||||
|
*
|
||||||
|
* Provides utilities for managing Active Effects in Vagabond RPG.
|
||||||
|
* Active Effects allow items (classes, perks, features) to modify actor stats,
|
||||||
|
* crit thresholds, resources, and other values.
|
||||||
|
*
|
||||||
|
* Key Use Cases:
|
||||||
|
* - Class features modifying crit thresholds for specific skills
|
||||||
|
* - Perks adding bonuses to stats or saves
|
||||||
|
* - Equipment providing armor or stat bonuses
|
||||||
|
* - Conditions applying temporary penalties
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Effect modes matching Foundry's CONST.ACTIVE_EFFECT_MODES
|
||||||
|
*/
|
||||||
|
export const EFFECT_MODES = {
|
||||||
|
CUSTOM: 0,
|
||||||
|
MULTIPLY: 1,
|
||||||
|
ADD: 2,
|
||||||
|
DOWNGRADE: 3,
|
||||||
|
UPGRADE: 4,
|
||||||
|
OVERRIDE: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common effect change keys for Vagabond RPG
|
||||||
|
* Maps human-readable names to data paths
|
||||||
|
*/
|
||||||
|
export const EFFECT_KEYS = {
|
||||||
|
// Stats
|
||||||
|
"stat.might": "system.stats.might.value",
|
||||||
|
"stat.dexterity": "system.stats.dexterity.value",
|
||||||
|
"stat.awareness": "system.stats.awareness.value",
|
||||||
|
"stat.reason": "system.stats.reason.value",
|
||||||
|
"stat.presence": "system.stats.presence.value",
|
||||||
|
"stat.luck": "system.stats.luck.value",
|
||||||
|
|
||||||
|
// Resources
|
||||||
|
"hp.bonus": "system.resources.hp.bonus",
|
||||||
|
"mana.bonus": "system.resources.mana.bonus",
|
||||||
|
"mana.castingMax": "system.resources.mana.castingMax",
|
||||||
|
"speed.bonus": "system.speed.bonus",
|
||||||
|
armor: "system.armor",
|
||||||
|
"itemSlots.bonus": "system.itemSlots.bonus",
|
||||||
|
|
||||||
|
// Save bonuses (reduce difficulty)
|
||||||
|
"save.reflex": "system.saves.reflex.bonus",
|
||||||
|
"save.endure": "system.saves.endure.bonus",
|
||||||
|
"save.will": "system.saves.will.bonus",
|
||||||
|
|
||||||
|
// Skill crit thresholds
|
||||||
|
"crit.arcana": "system.skills.arcana.critThreshold",
|
||||||
|
"crit.brawl": "system.skills.brawl.critThreshold",
|
||||||
|
"crit.craft": "system.skills.craft.critThreshold",
|
||||||
|
"crit.detect": "system.skills.detect.critThreshold",
|
||||||
|
"crit.finesse": "system.skills.finesse.critThreshold",
|
||||||
|
"crit.influence": "system.skills.influence.critThreshold",
|
||||||
|
"crit.leadership": "system.skills.leadership.critThreshold",
|
||||||
|
"crit.medicine": "system.skills.medicine.critThreshold",
|
||||||
|
"crit.mysticism": "system.skills.mysticism.critThreshold",
|
||||||
|
"crit.performance": "system.skills.performance.critThreshold",
|
||||||
|
"crit.sneak": "system.skills.sneak.critThreshold",
|
||||||
|
"crit.survival": "system.skills.survival.critThreshold",
|
||||||
|
|
||||||
|
// Attack crit thresholds
|
||||||
|
"crit.attack.melee": "system.attacks.melee.critThreshold",
|
||||||
|
"crit.attack.brawl": "system.attacks.brawl.critThreshold",
|
||||||
|
"crit.attack.ranged": "system.attacks.ranged.critThreshold",
|
||||||
|
"crit.attack.finesse": "system.attacks.finesse.critThreshold",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an Active Effect data object from a simplified definition.
|
||||||
|
*
|
||||||
|
* @param {Object} options - Effect options
|
||||||
|
* @param {string} options.name - Display name of the effect
|
||||||
|
* @param {string} options.icon - Icon path
|
||||||
|
* @param {Array} options.changes - Array of {key, value, mode} objects
|
||||||
|
* @param {boolean} options.disabled - Whether effect starts disabled
|
||||||
|
* @param {string} options.origin - UUID of the source item
|
||||||
|
* @returns {Object} Active Effect data object
|
||||||
|
*/
|
||||||
|
export function createEffectData({
|
||||||
|
name,
|
||||||
|
icon = "icons/svg/aura.svg",
|
||||||
|
changes = [],
|
||||||
|
disabled = false,
|
||||||
|
origin = null,
|
||||||
|
}) {
|
||||||
|
// Convert simplified keys to full data paths
|
||||||
|
const mappedChanges = changes.map((change) => ({
|
||||||
|
key: EFFECT_KEYS[change.key] || change.key,
|
||||||
|
mode: change.mode ?? EFFECT_MODES.ADD,
|
||||||
|
value: String(change.value),
|
||||||
|
priority: change.priority ?? null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
icon,
|
||||||
|
changes: mappedChanges,
|
||||||
|
disabled,
|
||||||
|
origin,
|
||||||
|
transfer: true, // Transfer to actor when item is owned
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a crit threshold reduction effect.
|
||||||
|
* Common for class features that improve crits on specific skills.
|
||||||
|
*
|
||||||
|
* @param {string} skillOrAttack - Skill ID or "attack.type"
|
||||||
|
* @param {number} reduction - Amount to reduce crit threshold (positive number)
|
||||||
|
* @param {string} name - Display name
|
||||||
|
* @param {string} origin - Source item UUID
|
||||||
|
* @returns {Object} Active Effect data
|
||||||
|
*/
|
||||||
|
export function createCritReductionEffect(skillOrAttack, reduction, name, origin = null) {
|
||||||
|
const key = skillOrAttack.startsWith("attack.")
|
||||||
|
? `crit.${skillOrAttack}`
|
||||||
|
: `crit.${skillOrAttack}`;
|
||||||
|
|
||||||
|
return createEffectData({
|
||||||
|
name,
|
||||||
|
icon: "icons/svg/sword.svg",
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
key,
|
||||||
|
value: -Math.abs(reduction), // Negative to reduce threshold
|
||||||
|
mode: EFFECT_MODES.ADD,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
origin,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a stat bonus effect.
|
||||||
|
*
|
||||||
|
* @param {string} stat - Stat ID (might, dexterity, etc.)
|
||||||
|
* @param {number} bonus - Bonus amount
|
||||||
|
* @param {string} name - Display name
|
||||||
|
* @param {string} origin - Source item UUID
|
||||||
|
* @returns {Object} Active Effect data
|
||||||
|
*/
|
||||||
|
export function createStatBonusEffect(stat, bonus, name, origin = null) {
|
||||||
|
return createEffectData({
|
||||||
|
name,
|
||||||
|
icon: "icons/svg/upgrade.svg",
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
key: `stat.${stat}`,
|
||||||
|
value: bonus,
|
||||||
|
mode: EFFECT_MODES.ADD,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
origin,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a save bonus effect.
|
||||||
|
*
|
||||||
|
* @param {string} save - Save type (reflex, endure, will)
|
||||||
|
* @param {number} bonus - Bonus amount (reduces difficulty)
|
||||||
|
* @param {string} name - Display name
|
||||||
|
* @param {string} origin - Source item UUID
|
||||||
|
* @returns {Object} Active Effect data
|
||||||
|
*/
|
||||||
|
export function createSaveBonusEffect(save, bonus, name, origin = null) {
|
||||||
|
return createEffectData({
|
||||||
|
name,
|
||||||
|
icon: "icons/svg/shield.svg",
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
key: `save.${save}`,
|
||||||
|
value: bonus,
|
||||||
|
mode: EFFECT_MODES.ADD,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
origin,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply effects from an item to its parent actor.
|
||||||
|
* Called when items with changes are added to an actor.
|
||||||
|
*
|
||||||
|
* @param {Item} item - The item with effects to apply
|
||||||
|
* @returns {Promise<ActiveEffect[]>} Created effects
|
||||||
|
*/
|
||||||
|
export async function applyItemEffects(item) {
|
||||||
|
const actor = item.parent;
|
||||||
|
if (!actor || !item.system.changes?.length) return [];
|
||||||
|
|
||||||
|
const effectData = createEffectData({
|
||||||
|
name: item.name,
|
||||||
|
icon: item.img,
|
||||||
|
changes: item.system.changes,
|
||||||
|
origin: item.uuid,
|
||||||
|
});
|
||||||
|
|
||||||
|
return actor.createEmbeddedDocuments("ActiveEffect", [effectData]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove effects originating from a specific item.
|
||||||
|
*
|
||||||
|
* @param {Actor} actor - The actor to remove effects from
|
||||||
|
* @param {string} itemUuid - UUID of the source item
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function removeItemEffects(actor, itemUuid) {
|
||||||
|
const effects = actor.effects.filter((e) => e.origin === itemUuid);
|
||||||
|
if (effects.length) {
|
||||||
|
const ids = effects.map((e) => e.id);
|
||||||
|
await actor.deleteEmbeddedDocuments("ActiveEffect", ids);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all effects on an actor grouped by source type.
|
||||||
|
*
|
||||||
|
* @param {Actor} actor - The actor to analyze
|
||||||
|
* @returns {Object} Effects grouped by source (class, perk, feature, equipment, other)
|
||||||
|
*/
|
||||||
|
export function getEffectsBySource(actor) {
|
||||||
|
const grouped = {
|
||||||
|
class: [],
|
||||||
|
perk: [],
|
||||||
|
feature: [],
|
||||||
|
equipment: [],
|
||||||
|
temporary: [],
|
||||||
|
other: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const effect of actor.effects) {
|
||||||
|
if (!effect.origin) {
|
||||||
|
grouped.temporary.push(effect);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to determine source type from origin UUID
|
||||||
|
const sourceItem = fromUuidSync(effect.origin);
|
||||||
|
if (sourceItem) {
|
||||||
|
const type = sourceItem.type;
|
||||||
|
if (grouped[type]) {
|
||||||
|
grouped[type].push(effect);
|
||||||
|
} else {
|
||||||
|
grouped.other.push(effect);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
grouped.other.push(effect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return grouped;
|
||||||
|
}
|
||||||
@ -6,6 +6,19 @@
|
|||||||
// Import configuration
|
// Import configuration
|
||||||
import { VAGABOND } from "./helpers/config.mjs";
|
import { VAGABOND } from "./helpers/config.mjs";
|
||||||
|
|
||||||
|
// Import data models
|
||||||
|
import { CharacterData, NPCData } from "./data/actor/_module.mjs";
|
||||||
|
import {
|
||||||
|
AncestryData,
|
||||||
|
ClassData,
|
||||||
|
SpellData,
|
||||||
|
PerkData,
|
||||||
|
WeaponData,
|
||||||
|
ArmorData,
|
||||||
|
EquipmentData,
|
||||||
|
FeatureData,
|
||||||
|
} from "./data/item/_module.mjs";
|
||||||
|
|
||||||
// Import document classes
|
// Import document classes
|
||||||
// import { VagabondActor } from "./documents/actor.mjs";
|
// import { VagabondActor } from "./documents/actor.mjs";
|
||||||
// import { VagabondItem } from "./documents/item.mjs";
|
// import { VagabondItem } from "./documents/item.mjs";
|
||||||
@ -34,11 +47,29 @@ Hooks.once("init", () => {
|
|||||||
// Add custom constants for configuration
|
// Add custom constants for configuration
|
||||||
CONFIG.VAGABOND = VAGABOND;
|
CONFIG.VAGABOND = VAGABOND;
|
||||||
|
|
||||||
// Define custom Document classes
|
// Register Actor data models
|
||||||
|
CONFIG.Actor.dataModels = {
|
||||||
|
character: CharacterData,
|
||||||
|
npc: NPCData,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register Item data models
|
||||||
|
CONFIG.Item.dataModels = {
|
||||||
|
ancestry: AncestryData,
|
||||||
|
class: ClassData,
|
||||||
|
spell: SpellData,
|
||||||
|
perk: PerkData,
|
||||||
|
weapon: WeaponData,
|
||||||
|
armor: ArmorData,
|
||||||
|
equipment: EquipmentData,
|
||||||
|
feature: FeatureData,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define custom Document classes (for future use)
|
||||||
// CONFIG.Actor.documentClass = VagabondActor;
|
// CONFIG.Actor.documentClass = VagabondActor;
|
||||||
// CONFIG.Item.documentClass = VagabondItem;
|
// CONFIG.Item.documentClass = VagabondItem;
|
||||||
|
|
||||||
// Register sheet application classes
|
// Register sheet application classes (TODO: Phase 3-4)
|
||||||
// Actors.unregisterSheet("core", ActorSheet);
|
// Actors.unregisterSheet("core", ActorSheet);
|
||||||
// Actors.registerSheet("vagabond", VagabondCharacterSheet, {
|
// Actors.registerSheet("vagabond", VagabondCharacterSheet, {
|
||||||
// types: ["character"],
|
// types: ["character"],
|
||||||
@ -52,7 +83,7 @@ Hooks.once("init", () => {
|
|||||||
// label: "VAGABOND.SheetItem"
|
// label: "VAGABOND.SheetItem"
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// Preload Handlebars templates
|
// Preload Handlebars templates (TODO: Phase 3)
|
||||||
// return preloadHandlebarsTemplates();
|
// return preloadHandlebarsTemplates();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user