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,
|
||||
"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",
|
||||
|
||||
@ -97,7 +97,7 @@
|
||||
"id": "1.1",
|
||||
"name": "Create base Actor data model",
|
||||
"description": "Shared fields for all actors: name, img, type, system data container",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "critical",
|
||||
"dependencies": ["0.1", "0.2"]
|
||||
@ -106,7 +106,7 @@
|
||||
"id": "1.2",
|
||||
"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",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "critical",
|
||||
"dependencies": ["1.1"]
|
||||
@ -115,7 +115,7 @@
|
||||
"id": "1.3",
|
||||
"name": "Create NPC/Monster data model",
|
||||
"description": "HD, HP, TL, Zone, Morale, Appearing, Armor, Immune, Weak, Actions array, Abilities array",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "critical",
|
||||
"dependencies": ["1.1"]
|
||||
@ -124,7 +124,7 @@
|
||||
"id": "1.4",
|
||||
"name": "Create dynamic resources system",
|
||||
"description": "Extensible resource tracker: HP, Mana, Luck, Fatigue, Studied Dice, custom resources with current/max/bonus fields",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "critical",
|
||||
"dependencies": ["1.2"]
|
||||
@ -133,7 +133,7 @@
|
||||
"id": "1.5",
|
||||
"name": "Create Skills data structure",
|
||||
"description": "12 skills with associated stat, trained boolean, difficulty calculation, custom crit threshold",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "critical",
|
||||
"dependencies": ["1.2"]
|
||||
@ -142,7 +142,7 @@
|
||||
"id": "1.6",
|
||||
"name": "Create base Item data model",
|
||||
"description": "Shared fields for all items: name, img, type, system data container, description",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "critical",
|
||||
"dependencies": ["0.1", "0.2"]
|
||||
@ -151,7 +151,7 @@
|
||||
"id": "1.7",
|
||||
"name": "Create Ancestry item data model",
|
||||
"description": "Being type, size, traits array with name/description pairs",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "high",
|
||||
"dependencies": ["1.6"]
|
||||
@ -160,7 +160,7 @@
|
||||
"id": "1.8",
|
||||
"name": "Create Class item data model",
|
||||
"description": "Key stat, action style, zone, training grants, starting pack, progression table (level -> features/mana/spells), feature definitions",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "high",
|
||||
"dependencies": ["1.6"]
|
||||
@ -169,7 +169,7 @@
|
||||
"id": "1.9",
|
||||
"name": "Create Spell item data model",
|
||||
"description": "Damage type, base effect, crit effect, valid delivery types, duration options, mana cost formula components",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "high",
|
||||
"dependencies": ["1.6"]
|
||||
@ -178,7 +178,7 @@
|
||||
"id": "1.10",
|
||||
"name": "Create Perk item data model",
|
||||
"description": "Prerequisites (stat requirements, training requirements, spell requirements), full description, mechanical effects",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "high",
|
||||
"dependencies": ["1.6"]
|
||||
@ -187,7 +187,7 @@
|
||||
"id": "1.11",
|
||||
"name": "Create Weapon item data model",
|
||||
"description": "Damage dice, grip type (1H/2H/Versatile), properties (Finesse, Thrown, Cleave, etc.), attack skill, slot cost, value",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "high",
|
||||
"dependencies": ["1.6"]
|
||||
@ -196,7 +196,7 @@
|
||||
"id": "1.12",
|
||||
"name": "Create Armor item data model",
|
||||
"description": "Armor value, type (Light/Heavy/Shield), slot cost, value, dodge penalty flag",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "high",
|
||||
"dependencies": ["1.6"]
|
||||
@ -205,7 +205,7 @@
|
||||
"id": "1.13",
|
||||
"name": "Create Equipment item data model",
|
||||
"description": "Generic items: slot cost, value, description, quantity, consumable flag",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "high",
|
||||
"dependencies": ["1.6"]
|
||||
@ -214,7 +214,7 @@
|
||||
"id": "1.14",
|
||||
"name": "Create Feature item data model",
|
||||
"description": "Class features as separate items: source class, level gained, description, mechanical effects, passive vs active",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "high",
|
||||
"dependencies": ["1.6"]
|
||||
@ -223,7 +223,7 @@
|
||||
"id": "1.15",
|
||||
"name": "Create Active Effects integration",
|
||||
"description": "System for items (classes, perks, features) to modify actor stats, crit thresholds, resources",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "high",
|
||||
"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 { 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 { VagabondActor } from "./documents/actor.mjs";
|
||||
// import { VagabondItem } from "./documents/item.mjs";
|
||||
@ -34,11 +47,29 @@ Hooks.once("init", () => {
|
||||
// Add custom constants for configuration
|
||||
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.Item.documentClass = VagabondItem;
|
||||
|
||||
// Register sheet application classes
|
||||
// Register sheet application classes (TODO: Phase 3-4)
|
||||
// Actors.unregisterSheet("core", ActorSheet);
|
||||
// Actors.registerSheet("vagabond", VagabondCharacterSheet, {
|
||||
// types: ["character"],
|
||||
@ -52,7 +83,7 @@ Hooks.once("init", () => {
|
||||
// label: "VAGABOND.SheetItem"
|
||||
// });
|
||||
|
||||
// Preload Handlebars templates
|
||||
// Preload Handlebars templates (TODO: Phase 3)
|
||||
// return preloadHandlebarsTemplates();
|
||||
});
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user