- Add _onCreate/_preDelete lifecycle methods to VagabondItem for automatic feature application and cleanup when classes are added/removed - Add updateActor hook to apply new features when character level increases - Implement applyClassFeatures() with idempotency to prevent duplicate effects - Add _applyClassProgression() for mana/castingMax from class progression - Add _applyTrainedSkills() to mark class skills as trained - Fix getCastingMaxAtLevel() to sum values instead of taking maximum - Add comprehensive test suite (10 tests) covering unit and integration tests Effects are tagged with vagabond flags for easy filtering and management. Methods calculate progression values directly for robustness with embedded items. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
358 lines
10 KiB
JavaScript
358 lines
10 KiB
JavaScript
/**
|
|
* Vagabond RPG System for Foundry VTT
|
|
* @module vagabond
|
|
*/
|
|
|
|
// 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, VagabondItem } from "./documents/_module.mjs";
|
|
|
|
// Import application classes
|
|
import {
|
|
VagabondRollDialog,
|
|
SkillCheckDialog,
|
|
AttackRollDialog,
|
|
SaveRollDialog,
|
|
SpellCastDialog,
|
|
FavorHinderDebug,
|
|
} from "./applications/_module.mjs";
|
|
|
|
// Import sheet classes
|
|
// import { VagabondCharacterSheet } from "./sheets/actor-sheet.mjs";
|
|
// import { VagabondItemSheet } from "./sheets/item-sheet.mjs";
|
|
|
|
// Import helper functions
|
|
// import { preloadHandlebarsTemplates } from "./helpers/templates.mjs";
|
|
|
|
// Import test registration (for Quench)
|
|
import { registerQuenchTests } from "./tests/quench-init.mjs";
|
|
|
|
/* -------------------------------------------- */
|
|
/* Foundry VTT Initialization */
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Init hook - runs once when Foundry initializes
|
|
*/
|
|
Hooks.once("init", () => {
|
|
// eslint-disable-next-line no-console
|
|
console.log("Vagabond RPG | Initializing Vagabond RPG System");
|
|
|
|
// Add custom constants for configuration
|
|
CONFIG.VAGABOND = VAGABOND;
|
|
|
|
// Expose application classes globally for macro/API access
|
|
game.vagabond = {
|
|
applications: {
|
|
VagabondRollDialog,
|
|
SkillCheckDialog,
|
|
AttackRollDialog,
|
|
SaveRollDialog,
|
|
SpellCastDialog,
|
|
FavorHinderDebug,
|
|
},
|
|
};
|
|
|
|
// 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
|
|
CONFIG.Actor.documentClass = VagabondActor;
|
|
CONFIG.Item.documentClass = VagabondItem;
|
|
|
|
// Register sheet application classes (TODO: Phase 3-4)
|
|
// Actors.unregisterSheet("core", ActorSheet);
|
|
// Actors.registerSheet("vagabond", VagabondCharacterSheet, {
|
|
// types: ["character"],
|
|
// makeDefault: true,
|
|
// label: "VAGABOND.SheetCharacter"
|
|
// });
|
|
|
|
// Items.unregisterSheet("core", ItemSheet);
|
|
// Items.registerSheet("vagabond", VagabondItemSheet, {
|
|
// makeDefault: true,
|
|
// label: "VAGABOND.SheetItem"
|
|
// });
|
|
|
|
// Preload Handlebars templates (TODO: Phase 3)
|
|
// return preloadHandlebarsTemplates();
|
|
});
|
|
|
|
/* -------------------------------------------- */
|
|
/* Ready Hook */
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Ready hook - runs when Foundry is fully loaded
|
|
*/
|
|
Hooks.once("ready", async () => {
|
|
// eslint-disable-next-line no-console
|
|
console.log("Vagabond RPG | System Ready");
|
|
|
|
// Display welcome message for GMs
|
|
if (game.user.isGM) {
|
|
const version = game.system.version;
|
|
ui.notifications.info(`Vagabond RPG v${version} - System loaded successfully!`);
|
|
|
|
// Create development macros if they don't exist
|
|
await _createDevMacros();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Create development/debug macros if they don't already exist.
|
|
* @private
|
|
*/
|
|
async function _createDevMacros() {
|
|
// Favor/Hinder Debug macro
|
|
const debugMacroName = "Favor/Hinder Debug";
|
|
const existingMacro = game.macros.find((m) => m.name === debugMacroName);
|
|
|
|
if (!existingMacro) {
|
|
await Macro.create({
|
|
name: debugMacroName,
|
|
type: "script",
|
|
img: "icons/svg/bug.svg",
|
|
command: "game.vagabond.applications.FavorHinderDebug.open();",
|
|
flags: { vagabond: { systemMacro: true } },
|
|
});
|
|
// eslint-disable-next-line no-console
|
|
console.log("Vagabond RPG | Created Favor/Hinder Debug macro");
|
|
}
|
|
|
|
// Skill Check macro
|
|
const skillMacroName = "Skill Check";
|
|
const existingSkillMacro = game.macros.find((m) => m.name === skillMacroName);
|
|
|
|
if (!existingSkillMacro) {
|
|
await Macro.create({
|
|
name: skillMacroName,
|
|
type: "script",
|
|
img: "icons/svg/d20.svg",
|
|
command: `// Opens skill check dialog for selected token or prompts to select actor
|
|
const actor = canvas.tokens.controlled[0]?.actor
|
|
|| game.actors.find(a => a.type === "character");
|
|
|
|
if (!actor) {
|
|
ui.notifications.warn("Select a token or create a character first");
|
|
} else {
|
|
game.vagabond.applications.SkillCheckDialog.prompt(actor);
|
|
}`,
|
|
flags: { vagabond: { systemMacro: true } },
|
|
});
|
|
// eslint-disable-next-line no-console
|
|
console.log("Vagabond RPG | Created Skill Check macro");
|
|
}
|
|
|
|
// Attack Roll macro
|
|
const attackMacroName = "Attack Roll";
|
|
const existingAttackMacro = game.macros.find((m) => m.name === attackMacroName);
|
|
|
|
if (!existingAttackMacro) {
|
|
await Macro.create({
|
|
name: attackMacroName,
|
|
type: "script",
|
|
img: "icons/svg/sword.svg",
|
|
command: `// Opens attack roll dialog for selected token
|
|
const actor = canvas.tokens.controlled[0]?.actor
|
|
|| game.actors.find(a => a.type === "character");
|
|
|
|
if (!actor) {
|
|
ui.notifications.warn("Select a token or create a character first");
|
|
} else {
|
|
game.vagabond.applications.AttackRollDialog.prompt(actor);
|
|
}`,
|
|
flags: { vagabond: { systemMacro: true } },
|
|
});
|
|
// eslint-disable-next-line no-console
|
|
console.log("Vagabond RPG | Created Attack Roll macro");
|
|
}
|
|
|
|
// Save Roll macro
|
|
const saveMacroName = "Save Roll";
|
|
const existingSaveMacro = game.macros.find((m) => m.name === saveMacroName);
|
|
|
|
if (!existingSaveMacro) {
|
|
await Macro.create({
|
|
name: saveMacroName,
|
|
type: "script",
|
|
img: "icons/svg/shield.svg",
|
|
command: `// Opens save roll dialog for selected token
|
|
const actor = canvas.tokens.controlled[0]?.actor
|
|
|| game.actors.find(a => a.type === "character");
|
|
|
|
if (!actor) {
|
|
ui.notifications.warn("Select a token or create a character first");
|
|
} else {
|
|
game.vagabond.applications.SaveRollDialog.prompt(actor);
|
|
}`,
|
|
flags: { vagabond: { systemMacro: true } },
|
|
});
|
|
// eslint-disable-next-line no-console
|
|
console.log("Vagabond RPG | Created Save Roll macro");
|
|
}
|
|
|
|
// Cast Spell macro
|
|
const castMacroName = "Cast Spell";
|
|
const existingCastMacro = game.macros.find((m) => m.name === castMacroName);
|
|
|
|
if (!existingCastMacro) {
|
|
await Macro.create({
|
|
name: castMacroName,
|
|
type: "script",
|
|
img: "icons/svg/lightning.svg",
|
|
command: `// Opens spell cast dialog for selected token
|
|
const actor = canvas.tokens.controlled[0]?.actor
|
|
|| game.actors.find(a => a.type === "character");
|
|
|
|
if (!actor) {
|
|
ui.notifications.warn("Select a token or create a character first");
|
|
} else {
|
|
game.vagabond.applications.SpellCastDialog.prompt(actor);
|
|
}`,
|
|
flags: { vagabond: { systemMacro: true } },
|
|
});
|
|
// eslint-disable-next-line no-console
|
|
console.log("Vagabond RPG | Created Cast Spell macro");
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
/* Handlebars Helpers */
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Define Handlebars helpers used throughout the system
|
|
*/
|
|
Hooks.once("init", () => {
|
|
// Multiply helper for formulas
|
|
Handlebars.registerHelper("multiply", (a, b) => Number(a) * Number(b));
|
|
|
|
// Subtract helper
|
|
Handlebars.registerHelper("subtract", (a, b) => Number(a) - Number(b));
|
|
|
|
// Calculate difficulty (20 - stat or 20 - stat*2 if trained)
|
|
Handlebars.registerHelper("difficulty", (stat, trained) => {
|
|
const statValue = Number(stat) || 0;
|
|
return trained ? 20 - statValue * 2 : 20 - statValue;
|
|
});
|
|
|
|
// Check if value equals comparison
|
|
Handlebars.registerHelper("eq", (a, b) => a === b);
|
|
|
|
// Check if value is greater than
|
|
Handlebars.registerHelper("gt", (a, b) => Number(a) > Number(b));
|
|
|
|
// Check if value is less than
|
|
Handlebars.registerHelper("lt", (a, b) => Number(a) < Number(b));
|
|
|
|
// Concatenate strings
|
|
Handlebars.registerHelper("concat", (...args) => {
|
|
// Remove the Handlebars options object from args
|
|
args.pop();
|
|
return args.join("");
|
|
});
|
|
|
|
// Capitalize first letter
|
|
Handlebars.registerHelper("capitalize", (str) => {
|
|
if (typeof str !== "string") return "";
|
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
});
|
|
|
|
// Format number with sign (+/-)
|
|
Handlebars.registerHelper("signedNumber", (num) => {
|
|
const n = Number(num) || 0;
|
|
return n >= 0 ? `+${n}` : `${n}`;
|
|
});
|
|
});
|
|
|
|
/* -------------------------------------------- */
|
|
/* Class Feature Automation */
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Class feature application is handled by VagabondItem._onCreate
|
|
* which runs as part of the document creation flow.
|
|
*
|
|
* Class effect cleanup is handled by VagabondItem._preDelete
|
|
* which runs before the document is deleted.
|
|
*/
|
|
|
|
/**
|
|
* When a character's level changes, update class features.
|
|
* This must be a Hook since level changes don't go through Item lifecycle.
|
|
*/
|
|
Hooks.on("updateActor", async (actor, changed, _options, userId) => {
|
|
// Only process for the updating user
|
|
if (game.user.id !== userId) return;
|
|
|
|
// Only process character level changes
|
|
if (actor.type !== "character") return;
|
|
if (!foundry.utils.hasProperty(changed, "system.level")) return;
|
|
|
|
const newLevel = changed.system.level;
|
|
const oldLevel = actor._source.system.level; // Get previous value from source
|
|
|
|
if (newLevel === oldLevel) return;
|
|
|
|
// Update features for each class
|
|
const classes = actor.items.filter((i) => i.type === "class");
|
|
for (const classItem of classes) {
|
|
await classItem.updateClassFeatures(newLevel, oldLevel);
|
|
}
|
|
});
|
|
|
|
/* -------------------------------------------- */
|
|
/* Quench Test Registration */
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Register tests with the Quench testing framework if available.
|
|
* Quench provides in-Foundry testing using Mocha + Chai.
|
|
* @see https://github.com/Ethaks/FVTT-Quench
|
|
*/
|
|
Hooks.once("quenchReady", (quenchRunner) => {
|
|
registerQuenchTests(quenchRunner);
|
|
});
|
|
|
|
/* -------------------------------------------- */
|
|
/* Hot Reload Support (Development) */
|
|
/* -------------------------------------------- */
|
|
|
|
if (import.meta.hot) {
|
|
import.meta.hot.accept((_newModule) => {
|
|
// eslint-disable-next-line no-console
|
|
console.log("Vagabond RPG | Hot reload triggered");
|
|
});
|
|
}
|