From a7862bedd52b7199309f06ab7fd8693c57e540b9 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Tue, 16 Dec 2025 12:14:08 -0600 Subject: [PATCH 1/4] Implement class level-up system with Active Effects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add level-up dialog (ApplicationV2) showing features gained per level - Class features with `changes` arrays auto-create Active Effects - Valor I/II/III on Fighter reduces crit threshold cumulatively (-1/-2/-3) - Perk selection UI in dialog (awaits perk compendium content) - Fix duplicate item creation bug (was double drop handling) - Configure proper dragDrop in ActorSheetV2 DEFAULT_OPTIONS - Add ancestries and classes compendium packs with LevelDB format - Docker compose PUID/PGID for proper file permissions Key patterns established: - Class progression stored in item.system.progression[] - Features with changes[] become ActiveEffects on level-up - applyClassFeatures() is idempotent (checks existing effects) - updateClassFeatures() handles level changes incrementally 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- docker-compose.yml | 6 +- module/applications/_module.mjs | 1 + module/applications/level-up-dialog.mjs | 427 ++++++++++ module/documents/actor.mjs | 79 +- module/documents/item.mjs | 112 ++- module/sheets/base-actor-sheet.mjs | 15 +- package-lock.json | 926 ++++++++++++++++++++++ package.json | 9 +- packs/_source/ancestries/draken.json | 26 + packs/_source/ancestries/dwarf.json | 26 + packs/_source/ancestries/elf.json | 26 + packs/_source/ancestries/goblin.json | 26 + packs/_source/ancestries/halfling.json | 26 + packs/_source/ancestries/human.json | 22 + packs/_source/ancestries/orc.json | 26 + packs/_source/classes/alchemist.json | 89 +++ packs/_source/classes/barbarian.json | 80 ++ packs/_source/classes/bard.json | 86 ++ packs/_source/classes/dancer.json | 92 +++ packs/_source/classes/druid.json | 86 ++ packs/_source/classes/fighter.json | 104 +++ packs/_source/classes/gunslinger.json | 86 ++ packs/_source/classes/hunter.json | 92 +++ packs/_source/classes/luminary.json | 86 ++ packs/_source/classes/magus.json | 92 +++ packs/_source/classes/merchant.json | 86 ++ packs/_source/classes/pugilist.json | 92 +++ packs/_source/classes/revelator.json | 95 +++ packs/_source/classes/rogue.json | 86 ++ packs/_source/classes/sorcerer.json | 107 +++ packs/_source/classes/vanguard.json | 86 ++ packs/_source/classes/witch.json | 86 ++ packs/_source/classes/wizard.json | 86 ++ packs/ancestries/000012.ldb | Bin 0 -> 2834 bytes packs/ancestries/000027.ldb | Bin 0 -> 893 bytes packs/ancestries/000029.ldb | Bin 0 -> 3258 bytes packs/ancestries/000032.ldb | Bin 0 -> 3258 bytes packs/ancestries/CURRENT | 1 + packs/ancestries/LOCK | 0 packs/ancestries/LOG | 5 + packs/ancestries/LOG.old | 5 + packs/ancestries/MANIFEST-000031 | Bin 0 -> 316 bytes packs/classes/000015.ldb | Bin 0 -> 24835 bytes packs/classes/000032.ldb | Bin 0 -> 2093 bytes packs/classes/000034.ldb | Bin 0 -> 26319 bytes packs/classes/000037.ldb | Bin 0 -> 26320 bytes packs/classes/CURRENT | 1 + packs/classes/LOCK | 0 packs/classes/LOG | 5 + packs/classes/LOG.old | 5 + packs/classes/MANIFEST-000036 | Bin 0 -> 319 bytes packs/perks/CURRENT | 1 + packs/perks/LOCK | 0 packs/perks/LOG | 3 + packs/perks/LOG.old | 3 + packs/perks/MANIFEST-000016 | Bin 0 -> 50 bytes packs/spells/CURRENT | 1 + packs/spells/LOCK | 0 packs/spells/LOG | 3 + packs/spells/LOG.old | 3 + packs/spells/MANIFEST-000016 | Bin 0 -> 50 bytes styles/scss/dialogs/_level-up-dialog.scss | 241 ++++++ styles/scss/vagabond.scss | 1 + system.json | 51 +- templates/dialog/level-up.hbs | 105 +++ 65 files changed, 3733 insertions(+), 71 deletions(-) create mode 100644 module/applications/level-up-dialog.mjs create mode 100755 packs/_source/ancestries/draken.json create mode 100755 packs/_source/ancestries/dwarf.json create mode 100755 packs/_source/ancestries/elf.json create mode 100755 packs/_source/ancestries/goblin.json create mode 100755 packs/_source/ancestries/halfling.json create mode 100755 packs/_source/ancestries/human.json create mode 100755 packs/_source/ancestries/orc.json create mode 100755 packs/_source/classes/alchemist.json create mode 100755 packs/_source/classes/barbarian.json create mode 100755 packs/_source/classes/bard.json create mode 100755 packs/_source/classes/dancer.json create mode 100755 packs/_source/classes/druid.json create mode 100755 packs/_source/classes/fighter.json create mode 100755 packs/_source/classes/gunslinger.json create mode 100755 packs/_source/classes/hunter.json create mode 100755 packs/_source/classes/luminary.json create mode 100755 packs/_source/classes/magus.json create mode 100755 packs/_source/classes/merchant.json create mode 100755 packs/_source/classes/pugilist.json create mode 100755 packs/_source/classes/revelator.json create mode 100755 packs/_source/classes/rogue.json create mode 100755 packs/_source/classes/sorcerer.json create mode 100755 packs/_source/classes/vanguard.json create mode 100755 packs/_source/classes/witch.json create mode 100755 packs/_source/classes/wizard.json create mode 100755 packs/ancestries/000012.ldb create mode 100644 packs/ancestries/000027.ldb create mode 100644 packs/ancestries/000029.ldb create mode 100644 packs/ancestries/000032.ldb create mode 100644 packs/ancestries/CURRENT create mode 100755 packs/ancestries/LOCK create mode 100644 packs/ancestries/LOG create mode 100644 packs/ancestries/LOG.old create mode 100644 packs/ancestries/MANIFEST-000031 create mode 100755 packs/classes/000015.ldb create mode 100644 packs/classes/000032.ldb create mode 100644 packs/classes/000034.ldb create mode 100644 packs/classes/000037.ldb create mode 100644 packs/classes/CURRENT create mode 100755 packs/classes/LOCK create mode 100644 packs/classes/LOG create mode 100644 packs/classes/LOG.old create mode 100644 packs/classes/MANIFEST-000036 create mode 100644 packs/perks/CURRENT create mode 100755 packs/perks/LOCK create mode 100644 packs/perks/LOG create mode 100644 packs/perks/LOG.old create mode 100644 packs/perks/MANIFEST-000016 create mode 100644 packs/spells/CURRENT create mode 100755 packs/spells/LOCK create mode 100644 packs/spells/LOG create mode 100644 packs/spells/LOG.old create mode 100644 packs/spells/MANIFEST-000016 create mode 100644 styles/scss/dialogs/_level-up-dialog.scss create mode 100644 templates/dialog/level-up.hbs diff --git a/docker-compose.yml b/docker-compose.yml index c38d104..ec7568c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,11 +10,15 @@ services: - FOUNDRY_ADMIN_KEY=${FOUNDRY_ADMIN_KEY:-vagabond-dev} - FOUNDRY_LICENSE_KEY=${FOUNDRY_LICENSE_KEY} - CONTAINER_PRESERVE_CONFIG=true + # Run as host user to avoid permission issues with bind-mounted packs + - PUID=1000 + - PGID=1001 volumes: # Foundry data directory - ./foundrydata:/data # Mount system directly for hot reload development - - ./:/data/Data/systems/vagabond:ro + # Note: Removed :ro because LevelDB packs need write access for temp files + - ./:/data/Data/systems/vagabond ports: - "30000:30000" # Required for Docker-in-LXC or rootless podman diff --git a/module/applications/_module.mjs b/module/applications/_module.mjs index 32c83e4..2f91ad9 100644 --- a/module/applications/_module.mjs +++ b/module/applications/_module.mjs @@ -9,3 +9,4 @@ export { default as AttackRollDialog } from "./attack-roll-dialog.mjs"; export { default as SaveRollDialog } from "./save-roll-dialog.mjs"; export { default as SpellCastDialog } from "./spell-cast-dialog.mjs"; export { default as FavorHinderDebug } from "./favor-hinder-debug.mjs"; +export { default as LevelUpDialog } from "./level-up-dialog.mjs"; diff --git a/module/applications/level-up-dialog.mjs b/module/applications/level-up-dialog.mjs new file mode 100644 index 0000000..d68a848 --- /dev/null +++ b/module/applications/level-up-dialog.mjs @@ -0,0 +1,427 @@ +/** + * Level Up Dialog for Vagabond RPG + * + * Displays features gained when leveling up and handles choices: + * - Shows automatic features with Active Effects + * - Presents Perk selection when "Perk" appears in progression + * - Handles choice features (e.g., Fighting Style) + * + * Uses Foundry VTT v13 ApplicationV2 API. + * + * @extends ApplicationV2 + * @mixes HandlebarsApplicationMixin + */ + +// Debug logging for level-up workflow - set to false to disable +const DEBUG_LEVELUP = true; +const debugLog = (...args) => { + if (DEBUG_LEVELUP) console.log("[LevelUpDialog]", ...args); +}; +const debugWarn = (...args) => { + if (DEBUG_LEVELUP) console.warn("[LevelUpDialog]", ...args); +}; + +const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; + +export default class LevelUpDialog extends HandlebarsApplicationMixin(ApplicationV2) { + /** + * @param {VagabondActor} actor - The actor leveling up + * @param {number} newLevel - The new level + * @param {number} oldLevel - The previous level + * @param {Object} options - Dialog options + */ + constructor(actor, newLevel, oldLevel, options = {}) { + super(options); + this.actor = actor; + this.newLevel = newLevel; + this.oldLevel = oldLevel; + + // Collected choices (perk selections, etc.) + this.choices = { + perks: [], + featureChoices: {}, + }; + + debugLog(`Constructor called`, { + actorName: actor.name, + oldLevel, + newLevel, + }); + } + + /* -------------------------------------------- */ + /* Static Properties */ + /* -------------------------------------------- */ + + /** @override */ + static DEFAULT_OPTIONS = { + id: "vagabond-level-up-dialog", + classes: ["vagabond", "level-up-dialog", "themed"], + tag: "form", + window: { + title: "VAGABOND.LevelUp", + icon: "fa-solid fa-arrow-up", + resizable: false, + }, + position: { + width: 500, + height: "auto", + }, + form: { + handler: LevelUpDialog.#onSubmit, + submitOnChange: false, + closeOnSubmit: true, + }, + }; + + /** @override */ + static PARTS = { + form: { + template: "systems/vagabond/templates/dialog/level-up.hbs", + }, + }; + + /* -------------------------------------------- */ + /* Getters */ + /* -------------------------------------------- */ + + /** + * Get the title for this dialog. + * @returns {string} + */ + get title() { + return `${this.actor.name} - Level ${this.newLevel}`; + } + + /* -------------------------------------------- */ + /* Data Preparation */ + /* -------------------------------------------- */ + + /** @override */ + async _prepareContext(options) { + debugLog("_prepareContext called"); + const context = await super._prepareContext(options); + + context.actor = this.actor; + context.newLevel = this.newLevel; + context.oldLevel = this.oldLevel; + + // Get all class items + const classes = this.actor.items.filter((i) => i.type === "class"); + debugLog( + `Found ${classes.length} class(es):`, + classes.map((c) => c.name) + ); + + // Gather features from all classes + const allFeatures = []; + const perkSlots = []; + const choiceFeatures = []; + + for (const classItem of classes) { + const features = classItem.system.features || []; + const progression = classItem.system.progression || []; + + debugLog(`Processing class "${classItem.name}"`, { + featuresCount: features.length, + progressionLevels: progression.map((p) => p.level), + }); + + // Get progression data for the new level + const levelProgression = progression.find((p) => p.level === this.newLevel); + debugLog(`Level ${this.newLevel} progression:`, levelProgression); + + // Check for "Perk" in progression + if (levelProgression?.features?.includes("Perk")) { + debugLog(`Perk slot found at level ${this.newLevel}`); + perkSlots.push({ + className: classItem.name, + classId: classItem.id, + }); + } + + // Get features gained at this level + for (const feature of features) { + if (feature.level > this.oldLevel && feature.level <= this.newLevel) { + debugLog(`Feature gained: "${feature.name}" (level ${feature.level})`, { + hasChanges: feature.changes?.length > 0, + requiresChoice: feature.requiresChoice, + }); + + const featureData = { + ...feature, + className: classItem.name, + classId: classItem.id, + hasChanges: feature.changes?.length > 0, + }; + + // Check if this feature requires a choice + if (feature.requiresChoice) { + choiceFeatures.push(featureData); + } else { + allFeatures.push(featureData); + } + } + } + } + + context.features = allFeatures; + context.perkSlots = perkSlots; + context.choiceFeatures = choiceFeatures; + context.hasPerks = perkSlots.length > 0; + context.hasChoices = choiceFeatures.length > 0; + + debugLog("Context prepared", { + featuresCount: allFeatures.length, + features: allFeatures.map((f) => f.name), + perkSlotsCount: perkSlots.length, + choiceFeaturesCount: choiceFeatures.length, + choiceFeatures: choiceFeatures.map((f) => f.name), + }); + + // Get available perks for selection + if (perkSlots.length > 0) { + debugLog("Loading available perks..."); + context.availablePerks = await this._getAvailablePerks(); + debugLog(`Loaded ${context.availablePerks?.length || 0} available perks`); + } + + return context; + } + + /** + * Get perks available for selection (from compendium, filtered by prerequisites). + * + * @returns {Promise} Available perk data + * @private + */ + async _getAvailablePerks() { + // Try to get perks from compendium + const pack = game.packs.get("vagabond.perks"); + if (!pack) { + // No compendium, return empty + return []; + } + + const index = await pack.getIndex(); + const perks = []; + + for (const entry of index) { + const perk = await pack.getDocument(entry._id); + if (!perk) continue; + + // Check prerequisites + const prereqResult = perk.checkPrerequisites?.(this.actor); + const met = prereqResult?.met ?? true; + + // Check if actor already has this perk + const alreadyHas = this.actor.items.some((i) => i.type === "perk" && i.name === perk.name); + + if (!alreadyHas) { + perks.push({ + id: perk.id, + uuid: perk.uuid, + name: perk.name, + description: perk.system.description, + prerequisites: perk.system.prerequisites || [], + prerequisitesMet: met, + missing: prereqResult?.missing || [], + }); + } + } + + // Sort: prerequisites met first, then alphabetically + perks.sort((a, b) => { + if (a.prerequisitesMet && !b.prerequisitesMet) return -1; + if (!a.prerequisitesMet && b.prerequisitesMet) return 1; + return a.name.localeCompare(b.name); + }); + + return perks; + } + + /* -------------------------------------------- */ + /* Event Handlers */ + /* -------------------------------------------- */ + + /** @override */ + _onRender(context, options) { + debugLog("_onRender called"); + super._onRender(context, options); + + // Apply theme class + this._applyThemeClass(); + + // Handle perk selection changes + const perkSelects = this.element.querySelectorAll("[data-perk-select]"); + debugLog(`Found ${perkSelects.length} perk select elements`); + for (const select of perkSelects) { + select.addEventListener("change", (event) => { + const slotIndex = parseInt(event.currentTarget.dataset.perkSelect, 10); + this.choices.perks[slotIndex] = event.currentTarget.value; + debugLog(`Perk selection changed: slot ${slotIndex} = ${event.currentTarget.value}`); + }); + } + + // Handle feature choice changes + const choiceSelects = this.element.querySelectorAll("[data-feature-choice]"); + debugLog(`Found ${choiceSelects.length} feature choice select elements`); + for (const select of choiceSelects) { + select.addEventListener("change", (event) => { + const featureName = event.currentTarget.dataset.featureChoice; + this.choices.featureChoices[featureName] = event.currentTarget.value; + debugLog(`Feature choice changed: "${featureName}" = ${event.currentTarget.value}`); + }); + } + } + + /** + * Apply the configured theme class to the dialog element. + * @protected + */ + _applyThemeClass() { + if (!this.element) return; + + this.element.classList.remove("theme-light", "theme-dark"); + + let theme = null; + try { + const uiConfig = game.settings.get("core", "uiConfig"); + const colorScheme = uiConfig?.colorScheme?.applications; + if (colorScheme === "dark") { + theme = "dark"; + } else if (colorScheme === "light") { + theme = "light"; + } + } catch { + // Settings not available + } + + if (theme === "dark") { + this.element.classList.add("theme-dark"); + } else if (theme === "light") { + this.element.classList.add("theme-light"); + } + } + + /** + * Handle form submission (confirm level up). + * @param {Event} event - The form submission event + * @param {HTMLFormElement} form - The form element + * @param {FormDataExtended} formData - The form data + * @private + */ + static async #onSubmit(event, form, formData) { + debugLog("#onSubmit called", { + formData: formData.object, + }); + + const dialog = this; + const data = foundry.utils.expandObject(formData.object); + debugLog("Expanded form data:", data); + + // Apply the level up + await dialog._applyLevelUp(data); + } + + /** + * Apply the level up, including class features and chosen perks. + * + * @param {Object} formData - Form data with choices + * @returns {Promise} + * @private + */ + async _applyLevelUp(formData) { + debugLog("_applyLevelUp called", { + actorName: this.actor.name, + oldLevel: this.oldLevel, + newLevel: this.newLevel, + choices: this.choices, + }); + + // 1. Update class features for all classes + const classes = this.actor.items.filter((i) => i.type === "class"); + debugLog(`Updating features for ${classes.length} class(es)`); + + for (const classItem of classes) { + debugLog(`Calling updateClassFeatures for "${classItem.name}"`); + const effects = await classItem.updateClassFeatures(this.newLevel, this.oldLevel); + debugLog(`updateClassFeatures returned ${effects.length} new effects`); + } + + // 2. Add selected perks + debugLog(`Processing ${this.choices.perks.length} perk selections:`, this.choices.perks); + for (const perkUuid of this.choices.perks) { + if (!perkUuid) { + debugLog("Skipping empty perk selection"); + continue; + } + + debugLog(`Adding perk from UUID: ${perkUuid}`); + try { + const perkDoc = await fromUuid(perkUuid); + if (perkDoc) { + debugLog(`Found perk document: "${perkDoc.name}"`); + // Create a copy of the perk on the actor + const created = await this.actor.createEmbeddedDocuments("Item", [perkDoc.toObject()]); + debugLog( + `Created perk on actor:`, + created.map((i) => i.name) + ); + } else { + debugWarn(`Perk document not found for UUID: ${perkUuid}`); + } + } catch (err) { + console.error(`Failed to add perk ${perkUuid}:`, err); + } + } + + // 3. Handle feature choices + debugLog(`Processing feature choices:`, this.choices.featureChoices); + for (const [featureName, choice] of Object.entries(this.choices.featureChoices)) { + debugLog(`Feature choice for "${featureName}": ${choice}`); + // Feature choices can be complex - for now just log + // TODO: Implement specific handling for choice features (e.g., add selected perk) + } + + // Log final actor state + debugLog( + "Level up complete. Actor effects:", + this.actor.effects.map((e) => ({ + name: e.name, + origin: e.origin, + changes: e.changes, + })) + ); + + // Notify user + ui.notifications.info(`${this.actor.name} advanced to level ${this.newLevel}!`); + } + + /* -------------------------------------------- */ + /* Static Methods */ + /* -------------------------------------------- */ + + /** + * Create and render a level up dialog. + * + * @param {VagabondActor} actor - The actor leveling up + * @param {number} newLevel - The new level + * @param {number} oldLevel - The previous level + * @param {Object} options - Dialog options + * @returns {Promise} The rendered dialog + */ + static async create(actor, newLevel, oldLevel, options = {}) { + debugLog(`static create() called`, { + actorName: actor.name, + oldLevel, + newLevel, + }); + + const dialog = new this(actor, newLevel, oldLevel, options); + debugLog("Rendering dialog..."); + const rendered = await dialog.render(true); + debugLog("Dialog rendered successfully"); + return rendered; + } +} diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 8abe1bb..8939536 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -1,3 +1,14 @@ +import { LevelUpDialog } from "../applications/_module.mjs"; + +// Debug logging for level-up workflow - set to false to disable +const DEBUG_LEVELUP = true; +const debugLog = (...args) => { + if (DEBUG_LEVELUP) console.log("[VagabondActor]", ...args); +}; +const debugWarn = (...args) => { + if (DEBUG_LEVELUP) console.warn("[VagabondActor]", ...args); +}; + /** * VagabondActor Document Class * @@ -24,18 +35,33 @@ export default class VagabondActor extends Actor { * @override */ async _onUpdate(changed, options, userId) { + debugLog(`_onUpdate called for "${this.name}"`, { + changed, + options, + userId, + currentUserId: game.user.id, + }); + await super._onUpdate(changed, options, userId); // Only process for the updating user - if (game.user.id !== userId) return; + if (game.user.id !== userId) { + debugLog("Skipping - not the updating user"); + return; + } // Check for level change on characters if (this.type === "character" && changed.system?.level !== undefined) { const newLevel = changed.system.level; const oldLevel = options._previousLevel ?? 1; + debugLog(`Level change detected: ${oldLevel} -> ${newLevel}`); + if (newLevel !== oldLevel) { + debugLog("Calling _onLevelChange..."); await this._onLevelChange(newLevel, oldLevel); + } else { + debugLog("Level unchanged, skipping level change handling"); } } } @@ -46,30 +72,69 @@ export default class VagabondActor extends Actor { * @override */ async _preUpdate(changed, options, userId) { + debugLog(`_preUpdate called for "${this.name}"`, { + changed, + currentLevel: this.system?.level, + }); + await super._preUpdate(changed, options, userId); // Store current level for level change detection if (this.type === "character" && changed.system?.level !== undefined) { options._previousLevel = this.system.level; + debugLog(`Stored previous level: ${options._previousLevel}`); } } /** * Handle character level changes. - * Updates class features for all owned class items. + * Shows level-up dialog to display gained features and handle choices. * * @param {number} newLevel - The new character level * @param {number} oldLevel - The previous character level * @private */ async _onLevelChange(newLevel, oldLevel) { - // Get all class items - const classes = this.items.filter((i) => i.type === "class"); + debugLog(`_onLevelChange called: ${oldLevel} -> ${newLevel}`); - // Update features for each class - for (const classItem of classes) { - await classItem.updateClassFeatures(newLevel, oldLevel); + // Check if there are any classes that have features to process + const classes = this.items.filter((i) => i.type === "class"); + debugLog( + `Found ${classes.length} class(es):`, + classes.map((c) => c.name) + ); + + if (classes.length === 0) { + // No class, just notify + debugWarn("No classes found on actor - showing simple notification"); + ui.notifications.info(`${this.name} advanced to level ${newLevel}!`); + return; } + + // Log class feature info + for (const classItem of classes) { + const features = classItem.system.features || []; + const progression = classItem.system.progression || []; + debugLog(`Class "${classItem.name}" data:`, { + featuresCount: features.length, + features: features.map((f) => ({ + name: f.name, + level: f.level, + hasChanges: f.changes?.length > 0, + requiresChoice: f.requiresChoice, + })), + progression: progression.map((p) => ({ + level: p.level, + features: p.features, + })), + }); + } + + // Show level-up dialog to handle features and choices + // The dialog will call updateClassFeatures after user confirms + debugLog("Creating LevelUpDialog..."); + await LevelUpDialog.create(this, newLevel, oldLevel); + debugLog("LevelUpDialog created/displayed"); } /* -------------------------------------------- */ diff --git a/module/documents/item.mjs b/module/documents/item.mjs index 6ad923c..6a8cce4 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -1,3 +1,12 @@ +// Debug logging for level-up workflow - set to false to disable +const DEBUG_LEVELUP = true; +const debugLog = (...args) => { + if (DEBUG_LEVELUP) console.log("[VagabondItem]", ...args); +}; +const debugWarn = (...args) => { + if (DEBUG_LEVELUP) console.warn("[VagabondItem]", ...args); +}; + /** * VagabondItem Document Class * @@ -25,19 +34,33 @@ export default class VagabondItem extends Item { * @override */ async _onCreate(data, options, userId) { + debugLog(`_onCreate called for "${this.name}" (type: ${this.type})`, { + parentType: this.parent?.type, + actorId: this.actor?.id, + itemId: this.id, + userId, + currentUserId: game.user.id, + }); + await super._onCreate(data, options, userId); // Only process for the creating user - if (game.user.id !== userId) return; + if (game.user.id !== userId) { + debugLog("Skipping - not the creating user"); + return; + } // Apply class features when class is added to a character // Check that actor still exists (may be deleted in tests) if (this.type === "class" && this.parent?.type === "character" && this.actor?.id) { + debugLog("Class added to character - applying initial features..."); try { - await this.applyClassFeatures(); + const effects = await this.applyClassFeatures(); + debugLog(`Applied ${effects.length} initial Active Effects`); } catch (err) { // Actor may have been deleted during tests - silently ignore if (!err.message?.includes("does not exist")) throw err; + debugWarn("Actor was deleted during feature application"); } } } @@ -113,12 +136,13 @@ export default class VagabondItem extends Item { if (!system) return; // Determine attack skill based on properties + // Note: system.properties is a SchemaField (object with boolean values), not an array if (!system.attackSkill) { - if (system.properties?.includes("finesse")) { + if (system.properties?.finesse) { system.attackSkill = "finesse"; - } else if (system.properties?.includes("brawl")) { + } else if (system.properties?.brawl) { system.attackSkill = "brawl"; - } else if (system.gripType === "ranged" || system.properties?.includes("thrown")) { + } else if (system.gripType === "ranged" || system.properties?.thrown) { system.attackSkill = "ranged"; } else { system.attackSkill = "melee"; @@ -555,19 +579,43 @@ export default class VagabondItem extends Item { * Called when class is added to character or when level changes. * This method is idempotent - it won't create duplicate effects. * + * Note: Race condition protection is handled at the _onCreate level via + * #initialFeaturesApplied map, which guards against duplicate calls. + * * @param {number} [targetLevel] - Level to apply features for (defaults to actor's level) * @returns {Promise} Created effects */ async applyClassFeatures(targetLevel = null) { - if (this.type !== "class" || !this.actor) return []; + debugLog(`applyClassFeatures called for class "${this.name}"`, { + targetLevel, + actorLevel: this.actor?.system?.level, + }); + if (this.type !== "class" || !this.actor) { + debugWarn("applyClassFeatures: Not a class or no actor"); + return []; + } const level = targetLevel ?? this.actor.system.level ?? 1; const features = this.system.features || []; + debugLog(`Processing features for level ${level}`, { + totalFeatures: features.length, + allFeatures: features.map((f) => ({ + name: f.name, + level: f.level, + changesCount: f.changes?.length || 0, + })), + }); + // Get features at or below current level that have changes const applicableFeatures = features.filter((f) => f.level <= level && f.changes?.length > 0); + debugLog( + `Applicable features (level <= ${level} with changes):`, + applicableFeatures.map((f) => f.name) + ); if (applicableFeatures.length === 0) { + debugLog("No applicable features with changes - applying progression only"); // Still apply progression even if no features with changes await this._applyClassProgression(level); await this._applyTrainedSkills(); @@ -579,9 +627,16 @@ export default class VagabondItem extends Item { const existingFeatureNames = new Set( existingEffects.map((e) => e.flags?.vagabond?.featureName) ); + debugLog(`Existing effects from this class:`, [...existingFeatureNames]); + const newFeatures = applicableFeatures.filter((f) => !existingFeatureNames.has(f.name)); + debugLog( + `New features to apply:`, + newFeatures.map((f) => f.name) + ); if (newFeatures.length === 0) { + debugLog("All features already applied - updating progression only"); // All features already applied, just update progression await this._applyClassProgression(level); await this._applyTrainedSkills(); @@ -609,8 +664,20 @@ export default class VagabondItem extends Item { }, })); + debugLog( + "Creating Active Effects:", + effectsData.map((e) => ({ + name: e.name, + changes: e.changes, + })) + ); + // Create the effects const createdEffects = await this.actor.createEmbeddedDocuments("ActiveEffect", effectsData); + debugLog( + `Created ${createdEffects.length} Active Effects:`, + createdEffects.map((e) => e.name) + ); // Also update actor's mana and casting max from class progression await this._applyClassProgression(level); @@ -630,16 +697,35 @@ export default class VagabondItem extends Item { * @returns {Promise} Newly created effects */ async updateClassFeatures(newLevel, oldLevel) { - if (this.type !== "class" || !this.actor) return []; + debugLog(`updateClassFeatures called for class "${this.name}"`, { + oldLevel, + newLevel, + }); + + if (this.type !== "class" || !this.actor) { + debugWarn("updateClassFeatures: Not a class or no actor"); + return []; + } const features = this.system.features || []; + debugLog(`Total features in class: ${features.length}`); // Find features gained between old and new level const newFeatures = features.filter( (f) => f.level > oldLevel && f.level <= newLevel && f.changes?.length > 0 ); + debugLog( + `Features gained between level ${oldLevel} and ${newLevel}:`, + newFeatures.map((f) => ({ + name: f.name, + level: f.level, + changesCount: f.changes?.length || 0, + })) + ); + if (newFeatures.length === 0) { + debugLog("No new features with changes - updating progression only"); // Still update progression stats even if no new features await this._applyClassProgression(newLevel); return []; @@ -666,7 +752,19 @@ export default class VagabondItem extends Item { }, })); + debugLog( + "Creating Active Effects for level-up:", + effectsData.map((e) => ({ + name: e.name, + changes: e.changes, + })) + ); + const createdEffects = await this.actor.createEmbeddedDocuments("ActiveEffect", effectsData); + debugLog( + `Created ${createdEffects.length} Active Effects:`, + createdEffects.map((e) => e.name) + ); // Update mana and casting max await this._applyClassProgression(newLevel); diff --git a/module/sheets/base-actor-sheet.mjs b/module/sheets/base-actor-sheet.mjs index bd28ba9..d5d82a7 100644 --- a/module/sheets/base-actor-sheet.mjs +++ b/module/sheets/base-actor-sheet.mjs @@ -67,6 +67,10 @@ export default class VagabondActorSheet extends HandlebarsApplicationMixin(Actor modifyResource: VagabondActorSheet.#onModifyResource, toggleTrained: VagabondActorSheet.#onToggleTrained, }, + // Drag-drop configuration - use Foundry's built-in system + // Setting to empty array disables ActorSheetV2's default handling + // so we can use our own _onDrop override + dragDrop: [{ dropSelector: null }], }; /** @override */ @@ -416,7 +420,9 @@ export default class VagabondActorSheet extends HandlebarsApplicationMixin(Actor } /** - * Set up drag-and-drop handlers. + * Set up drag-and-drop handlers for dragging items FROM this sheet. + * Drop handling is configured via DEFAULT_OPTIONS.dragDrop and uses + * the _onDrop method override - we don't add manual listeners for drops. * @protected */ _setupDragDrop() { @@ -426,10 +432,9 @@ export default class VagabondActorSheet extends HandlebarsApplicationMixin(Actor el.setAttribute("draggable", "true"); el.addEventListener("dragstart", this._onDragStart.bind(this)); } - - // Enable dropping items onto the sheet - this.element.addEventListener("dragover", this._onDragOver.bind(this)); - this.element.addEventListener("drop", this._onDrop.bind(this)); + // Note: Drop handling is managed by Foundry's dragDrop configuration + // in DEFAULT_OPTIONS, which calls our _onDrop override. We don't add + // manual drop listeners here to avoid duplicate item creation. } /** diff --git a/package-lock.json b/package-lock.json index 0202d13..c44c270 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "license": "MIT", "devDependencies": { + "@foundryvtt/foundryvtt-cli": "^3.0.2", "eslint": "^8.57.0", "husky": "^9.1.0", "lint-staged": "^15.2.0", @@ -79,6 +80,41 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@foundryvtt/foundryvtt-cli": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@foundryvtt/foundryvtt-cli/-/foundryvtt-cli-3.0.2.tgz", + "integrity": "sha512-coh4Cf4FD/GHxk2QMsd+3wLMivNeih4rfkbZy8CaYjdlpo6iciFQwxLqznZWtn+5p06zekvS2xLUF55NnbXQDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "classic-level": "^1.4.1", + "esm": "^3.2.25", + "js-yaml": "^4.1.0", + "mkdirp": "^3.0.1", + "nedb-promises": "^6.2.3", + "yargs": "^17.7.2" + }, + "bin": { + "fvtt": "fvtt.mjs" + }, + "engines": { + "node": ">17.0.0" + } + }, + "node_modules/@foundryvtt/foundryvtt-cli/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -465,6 +501,24 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@seald-io/binary-search-tree": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@seald-io/binary-search-tree/-/binary-search-tree-1.0.3.tgz", + "integrity": "sha512-qv3jnwoakeax2razYaMsGI/luWdliBLHTdC6jU55hQt1hcFqzauH/HsBollQ7IR4ySTtYhT+xyHoijpA16C+tA==", + "dev": true + }, + "node_modules/@seald-io/nedb": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@seald-io/nedb/-/nedb-4.1.2.tgz", + "integrity": "sha512-bDr6TqjBVS2rDyYM9CPxAnotj5FuNL9NF8o7h7YyFXM7yruqT4ddr+PkSb2mJvvw991bqdftazkEo38gykvaww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@seald-io/binary-search-tree": "^1.0.3", + "localforage": "^1.10.0", + "util": "^0.12.5" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -472,6 +526,25 @@ "dev": true, "license": "ISC" }, + "node_modules/abstract-level": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/abstract-level/-/abstract-level-1.0.4.tgz", + "integrity": "sha512-eUP/6pbXBkMbXFdx4IH2fVgvB7M0JvR7/lIL33zcs0IBcwjdzSSl31TOJsaCzmKSSDF9h8QYSOJux4Nd4YJqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "catering": "^2.1.0", + "is-buffer": "^2.0.5", + "level-supports": "^4.0.0", + "level-transcoder": "^1.0.1", + "module-error": "^1.0.1", + "queue-microtask": "^1.2.3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -561,6 +634,22 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -568,6 +657,27 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -592,6 +702,81 @@ "node": ">=8" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -602,6 +787,16 @@ "node": ">=6" } }, + "node_modules/catering": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/catering/-/catering-2.1.1.tgz", + "integrity": "sha512-K7Qy8O9p76sL3/3m7/zLKbRkyOlSZAgzEaLhyj2mXS8PsCud2Eo4hAb8aLtZqHh0QGqLcb9dlJSu6lHRVENm1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -635,6 +830,24 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/classic-level": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/classic-level/-/classic-level-1.4.1.tgz", + "integrity": "sha512-qGx/KJl3bvtOHrGau2WklEZuXhS3zme+jf+fsu6Ej7W7IP/C49v7KNlWIsT1jZu0YnfzSIYDGcEWpCa1wKGWXQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "abstract-level": "^1.0.2", + "catering": "^2.1.0", + "module-error": "^1.0.1", + "napi-macros": "^2.2.2", + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -668,6 +881,71 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -752,6 +1030,24 @@ "dev": true, "license": "MIT" }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", @@ -779,6 +1075,21 @@ "node": ">=6.0.0" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/emoji-regex": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", @@ -799,6 +1110,49 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -899,6 +1253,16 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -1090,6 +1454,22 @@ "dev": true, "license": "ISC" }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1097,6 +1477,36 @@ "dev": true, "license": "ISC" }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-east-asian-width": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", @@ -1110,6 +1520,45 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", @@ -1174,6 +1623,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -1191,6 +1653,61 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/human-signals": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", @@ -1217,6 +1734,27 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -1227,6 +1765,13 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "license": "MIT" + }, "node_modules/immutable": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", @@ -1280,6 +1825,60 @@ "dev": true, "license": "ISC" }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1303,6 +1902,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -1336,6 +1955,25 @@ "node": ">=8" } }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", @@ -1349,6 +1987,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1400,6 +2054,30 @@ "json-buffer": "3.0.1" } }, + "node_modules/level-supports": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-4.0.1.tgz", + "integrity": "sha512-PbXpve8rKeNcZ9C1mUicC9auIYFyGpkV9/i6g76tLgANwWhtG2v7I4xNBUlkn3lE2/dZF3Pi0ygYGtLc4RXXdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/level-transcoder": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/level-transcoder/-/level-transcoder-1.0.1.tgz", + "integrity": "sha512-t7bFwFtsQeD8cl8NIoQ2iwxA0CL/9IFw7/9gAjOonH0PWTTiRfY7Hq+Ejbsxh86tXobDQ6IOiddjNYIfOBs06w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "module-error": "^1.0.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -1414,6 +2092,16 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -1486,6 +2174,16 @@ "node": ">=18.0.0" } }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lie": "3.1.1" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -1604,6 +2302,16 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -1664,6 +2372,32 @@ "node": "*" } }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/module-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/module-error/-/module-error-1.0.2.tgz", + "integrity": "sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1671,6 +2405,13 @@ "dev": true, "license": "MIT" }, + "node_modules/napi-macros": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.2.2.tgz", + "integrity": "sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g==", + "dev": true, + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -1678,6 +2419,16 @@ "dev": true, "license": "MIT" }, + "node_modules/nedb-promises": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/nedb-promises/-/nedb-promises-6.2.3.tgz", + "integrity": "sha512-enq0IjNyBz9Qy9W/QPCcLGh/QORGBjXbIeZeWvIjO3OMLyAvlKT3hiJubP2BKEiFniUlR3L01o18ktqgn5jxqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@seald-io/nedb": "^4.0.2" + } + }, "node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", @@ -1686,6 +2437,18 @@ "license": "MIT", "optional": true }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/npm-run-path": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", @@ -1860,6 +2623,16 @@ "node": ">=0.10" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -1931,6 +2704,16 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2033,6 +2816,24 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/sass": { "version": "1.96.0", "resolved": "https://registry.npmjs.org/sass/-/sass-1.96.0.tgz", @@ -2054,6 +2855,24 @@ "@parcel/watcher": "^2.4.1" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2295,6 +3114,20 @@ "punycode": "^2.1.0" } }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -2311,6 +3144,28 @@ "node": ">= 8" } }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -2388,6 +3243,16 @@ "dev": true, "license": "ISC" }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yaml": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", @@ -2404,6 +3269,67 @@ "url": "https://github.com/sponsors/eemeli" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 8a7f393..a3a131b 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,13 @@ "version": "0.1.0", "description": "Foundry VTT system for Vagabond RPG", "scripts": { - "build": "sass styles/scss/vagabond.scss styles/vagabond.css --style=compressed", + "build": "npm run build:css && npm run build:packs", + "build:css": "sass styles/scss/vagabond.scss styles/vagabond.css --style=compressed", + "build:packs": "fvtt package workon vagabond --type System && npm run build:pack:ancestries && npm run build:pack:classes && npm run build:pack:spells && npm run build:pack:perks", + "build:pack:ancestries": "fvtt package pack -n ancestries -t Item --in packs/_source/ancestries --out packs", + "build:pack:classes": "fvtt package pack -n classes -t Item --in packs/_source/classes --out packs", + "build:pack:spells": "fvtt package pack -n spells -t Item --in packs/_source/spells --out packs", + "build:pack:perks": "fvtt package pack -n perks -t Item --in packs/_source/perks --out packs", "watch": "sass styles/scss/vagabond.scss styles/vagabond.css --watch --style=expanded --source-map", "lint": "eslint module/", "lint:fix": "eslint module/ --fix", @@ -23,6 +29,7 @@ }, "homepage": "https://github.com/calcorum/vagabond-rpg-foundryvtt#readme", "devDependencies": { + "@foundryvtt/foundryvtt-cli": "^3.0.2", "eslint": "^8.57.0", "husky": "^9.1.0", "lint-staged": "^15.2.0", diff --git a/packs/_source/ancestries/draken.json b/packs/_source/ancestries/draken.json new file mode 100755 index 0000000..14d5bc7 --- /dev/null +++ b/packs/_source/ancestries/draken.json @@ -0,0 +1,26 @@ +{ + "_id": "vagabondAnceDraken", + "_key": "!items!vagabondAnceDraken", + "name": "Draken", + "type": "ancestry", + "img": "icons/svg/mystery-man.svg", + "system": { + "description": "

Draken are dragon-blooded humanoids with scales, claws, and the ability to breathe elemental energy. Their draconic heritage grants them natural armor and resistance to one damage type.

", + "beingType": "cryptid", + "size": "medium", + "traits": [ + { + "name": "Breath Attack", + "description": "

As an Action, you can breathe elemental energy in a 15-foot cone, dealing 2d6! damage of your chosen type (fire, cold, shock, or acid, chosen at character creation). Targets may make a Reflex save to take half damage.

" + }, + { + "name": "Scale", + "description": "

Your natural scales grant you +1 Armor.

" + }, + { + "name": "Draconic Resilience", + "description": "

You take half damage from one damage type of your choice (matching your breath weapon).

" + } + ] + } +} diff --git a/packs/_source/ancestries/dwarf.json b/packs/_source/ancestries/dwarf.json new file mode 100755 index 0000000..8279cff --- /dev/null +++ b/packs/_source/ancestries/dwarf.json @@ -0,0 +1,26 @@ +{ + "_id": "vagabondAnceDwarf", + "_key": "!items!vagabondAnceDwarf", + "name": "Dwarf", + "type": "ancestry", + "img": "icons/svg/mystery-man.svg", + "system": { + "description": "

Stout and resilient, dwarves are known for their toughness and resistance to adversity. Their sturdy constitution makes them excellent warriors and craftsmen.

", + "beingType": "humanlike", + "size": "medium", + "traits": [ + { + "name": "Darksight", + "description": "

You can see in darkness as if it were dim light.

" + }, + { + "name": "Sturdy", + "description": "

You gain +1 to saves against Fear, Sickened, and Shove effects.

" + }, + { + "name": "Tough", + "description": "

You gain +Level additional maximum HP.

" + } + ] + } +} diff --git a/packs/_source/ancestries/elf.json b/packs/_source/ancestries/elf.json new file mode 100755 index 0000000..16121c3 --- /dev/null +++ b/packs/_source/ancestries/elf.json @@ -0,0 +1,26 @@ +{ + "_id": "vagabondAnceElf00", + "_key": "!items!vagabondAnceElf00", + "name": "Elf", + "type": "ancestry", + "img": "icons/svg/mystery-man.svg", + "system": { + "description": "

Elves are graceful fae beings with an innate connection to magic. Their keen senses and natural attunement to the arcane make them exceptional spellcasters and perceptive scouts.

", + "beingType": "fae", + "size": "medium", + "traits": [ + { + "name": "Ascendancy", + "description": "

You are Trained in one Skill of your choice.

" + }, + { + "name": "Elven Eyes", + "description": "

You gain Favor on sight-based Detect checks.

" + }, + { + "name": "Naturally Attuned", + "description": "

You know one Spell of your choice (even if you are not a caster).

" + } + ] + } +} diff --git a/packs/_source/ancestries/goblin.json b/packs/_source/ancestries/goblin.json new file mode 100755 index 0000000..40d26e8 --- /dev/null +++ b/packs/_source/ancestries/goblin.json @@ -0,0 +1,26 @@ +{ + "_id": "vagabondAnceGoblin", + "_key": "!items!vagabondAnceGoblin", + "name": "Goblin", + "type": "ancestry", + "img": "icons/svg/mystery-man.svg", + "system": { + "description": "

Goblins are small, cunning creatures who thrive in darkness and refuse. Their ability to see in the dark and resistance to toxins makes them surprisingly resilient survivors.

", + "beingType": "cryptid", + "size": "small", + "traits": [ + { + "name": "Darksight", + "description": "

You can see in darkness as if it were dim light.

" + }, + { + "name": "Nimble", + "description": "

Your base Speed is increased by 5.

" + }, + { + "name": "Scavenger", + "description": "

You gain Favor on saves against the Sickened condition.

" + } + ] + } +} diff --git a/packs/_source/ancestries/halfling.json b/packs/_source/ancestries/halfling.json new file mode 100755 index 0000000..fb1ed36 --- /dev/null +++ b/packs/_source/ancestries/halfling.json @@ -0,0 +1,26 @@ +{ + "_id": "vagabondAnceHalfli", + "_key": "!items!vagabondAnceHalfli", + "name": "Halfling", + "type": "ancestry", + "img": "icons/svg/mystery-man.svg", + "system": { + "description": "

Small but surprisingly lucky, halflings are nimble folk who can slip through tight spaces and always seem to land on their feet. Their natural good fortune is legendary.

", + "beingType": "humanlike", + "size": "small", + "traits": [ + { + "name": "Nimble", + "description": "

Your base Speed is increased by 5.

" + }, + { + "name": "Squat", + "description": "

You can move through spaces occupied by larger creatures.

" + }, + { + "name": "Tricksy", + "description": "

When you take a Rest, you regain +1 additional Luck.

" + } + ] + } +} diff --git a/packs/_source/ancestries/human.json b/packs/_source/ancestries/human.json new file mode 100755 index 0000000..d82d9f0 --- /dev/null +++ b/packs/_source/ancestries/human.json @@ -0,0 +1,22 @@ +{ + "_id": "vagabondAnceHuman", + "_key": "!items!vagabondAnceHuman", + "name": "Human", + "type": "ancestry", + "img": "icons/svg/mystery-man.svg", + "system": { + "description": "

Humans are the most common and adaptable of all the peoples. Their knack for learning and strong potential make them capable of excelling in any field.

", + "beingType": "humanlike", + "size": "medium", + "traits": [ + { + "name": "Knack", + "description": "

You gain one Perk and Training in one Skill of your choice.

" + }, + { + "name": "Strong Potential", + "description": "

You gain +1 to one Stat of your choice (maximum 7).

" + } + ] + } +} diff --git a/packs/_source/ancestries/orc.json b/packs/_source/ancestries/orc.json new file mode 100755 index 0000000..5b0a878 --- /dev/null +++ b/packs/_source/ancestries/orc.json @@ -0,0 +1,26 @@ +{ + "_id": "vagabondAnceOrc000", + "_key": "!items!vagabondAnceOrc000", + "name": "Orc", + "type": "ancestry", + "img": "icons/svg/mystery-man.svg", + "system": { + "description": "

Orcs are powerful, hulking warriors with a natural talent for physical confrontation. Their impressive size allows them to carry more equipment and overpower foes in close combat.

", + "beingType": "cryptid", + "size": "medium", + "traits": [ + { + "name": "Darksight", + "description": "

You can see in darkness as if it were dim light.

" + }, + { + "name": "Beefy", + "description": "

You gain Favor on Grapple and Shove attempts.

" + }, + { + "name": "Hulking", + "description": "

You have +2 additional Item Slots.

" + } + ] + } +} diff --git a/packs/_source/classes/alchemist.json b/packs/_source/classes/alchemist.json new file mode 100755 index 0000000..2a02255 --- /dev/null +++ b/packs/_source/classes/alchemist.json @@ -0,0 +1,89 @@ +{ + "_id": "vagabondClsAlchem", + "_key": "!items!vagabondClsAlchem", + "name": "Alchemist", + "type": "class", + "img": "icons/svg/flask.svg", + "system": { + "description": "

Alchemists are masters of chemical warfare and crafting, using their knowledge to create powerful potions, bombs, and elixirs that devastate their foes or aid their allies.

", + "keyStat": "reason", + "actionStyle": "craft", + "zone": "backline", + "trainedSkills": ["craft"], + "startingPack": "

Alchemist or Assassin

", + "isCaster": false, + "progression": [ + { + "level": 1, + "mana": 0, + "castingMax": 0, + "spellsKnown": 0, + "features": ["Alchemy", "Catalyze"] + }, + { "level": 2, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Eureka"] }, + { "level": 3, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 4, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Potency"] }, + { "level": 5, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 6, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Mix"] }, + { "level": 7, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 8, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Big Bang"] }, + { "level": 9, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 10, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Prima Materia"] } + ], + "features": [ + { + "name": "Alchemy", + "level": 1, + "description": "

You can attack with alchemical items using Craft. Formulae: Choose 4 alchemical items with a value no higher than (your Alchemist Level × 50s). You only need to provide 5s of materials to Craft these items and have Alchemy Tools equipped. You learn to Craft 1 other alchemical item this way every 2 Levels in this Class hereafter.

", + "passive": true, + "changes": [] + }, + { + "name": "Catalyze", + "level": 1, + "description": "

You gain the Deft Hands Perk, and you can Craft alchemical items with the Use Action.

", + "passive": true, + "changes": [] + }, + { + "name": "Eureka", + "level": 2, + "description": "

You gain a Studied die when you Crit on a Craft Check.

", + "passive": true, + "changes": [] + }, + { + "name": "Potency", + "level": 4, + "description": "

The damage and healing dice of your alchemical items can explode.

", + "passive": true, + "changes": [] + }, + { + "name": "Mix", + "level": 6, + "description": "

You can take the Use Action to combine two alchemical items together, causing their effects to both occur when you Use the combined item. This combined item lasts for the Round, then goes inert.

", + "passive": true, + "changes": [] + }, + { + "name": "Big Bang", + "level": 8, + "description": "

You gain a d6 bonus to the damage and healing of your alchemical items, and they can explode on a roll of their two highest values.

", + "passive": true, + "changes": [] + }, + { + "name": "Prima Materia", + "level": 10, + "description": "

Once per Day, you can use your Action or skip your Move to Craft an alchemical item with a value as high as 10g without materials.

", + "passive": true, + "changes": [] + } + ], + "customResource": { + "name": "Formulae Known", + "max": "4 + floor(@classes.alchemist.level / 2)" + } + } +} diff --git a/packs/_source/classes/barbarian.json b/packs/_source/classes/barbarian.json new file mode 100755 index 0000000..889dee3 --- /dev/null +++ b/packs/_source/classes/barbarian.json @@ -0,0 +1,80 @@ +{ + "_id": "vagabondClsBarbar", + "_key": "!items!vagabondClsBarbar", + "name": "Barbarian", + "type": "class", + "img": "icons/svg/combat.svg", + "system": { + "description": "

Barbarians are ferocious warriors who channel their rage into devastating attacks. Their fury makes them formidable frontline bruisers who can shrug off damage and strike fear into their enemies.

", + "keyStat": "might", + "actionStyle": "attack", + "zone": "frontline", + "trainedSkills": ["brawl", "detect", "influence", "leadership", "mysticism", "survival"], + "startingPack": "

Gladiator or Warrior

", + "isCaster": false, + "progression": [ + { "level": 1, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Rage", "Wrath"] }, + { "level": 2, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Aggressor"] }, + { "level": 3, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 4, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Fearmonger"] }, + { "level": 5, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 6, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Mindless Rancor"] }, + { "level": 7, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 8, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Bloodthirsty"] }, + { "level": 9, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 10, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Rip and Tear"] } + ], + "features": [ + { + "name": "Rage", + "level": 1, + "description": "

While Berserk and wearing Light Armor or no Armor, you reduce damage you take by 1 per damage die, and your attack damage dice are one size larger and can explode. Further, you can go Berserk after you take damage or as part of making an attack. You remain Berserk this way for 1 minute, unless you end it (no Action) or go Unconscious.

", + "passive": true, + "changes": [] + }, + { + "name": "Wrath", + "level": 1, + "description": "

You gain the Interceptor Perk, and can make its attack against an Enemy that makes a Ranged Attack, Casts, or that damages you or an Ally.

", + "passive": true, + "changes": [] + }, + { + "name": "Aggressor", + "level": 2, + "description": "

You have a 10 foot bonus to Speed during the first Round of Combat, and having 3 or more Fatigue does not prevent you from taking the Rush Action.

", + "passive": true, + "changes": [] + }, + { + "name": "Fearmonger", + "level": 4, + "description": "

When you kill an Enemy, every Near Enemy with HD lower than your Level becomes Frightened until the end of your next Turn.

", + "passive": true, + "changes": [] + }, + { + "name": "Mindless Rancor", + "level": 6, + "description": "

You cannot be Charmed, Confused, or compelled to act against your will.

", + "passive": true, + "changes": [] + }, + { + "name": "Bloodthirsty", + "level": 8, + "description": "

Your attacks against Beings that are missing any HP are Favored, and you can sense them within Far as if by Blindsight.

", + "passive": true, + "changes": [] + }, + { + "name": "Rip and Tear", + "level": 10, + "description": "

While Berserk, you reduce damage you take by 2 per damage die, rather than 1, and you gain a +1 bonus to each die of damage you deal.

", + "passive": true, + "changes": [] + } + ], + "customResource": {} + } +} diff --git a/packs/_source/classes/bard.json b/packs/_source/classes/bard.json new file mode 100755 index 0000000..170554c --- /dev/null +++ b/packs/_source/classes/bard.json @@ -0,0 +1,86 @@ +{ + "_id": "vagabondClsBard00", + "_key": "!items!vagabondClsBard00", + "name": "Bard", + "type": "class", + "img": "icons/svg/sound.svg", + "system": { + "description": "

Bards are inspiring performers who use their musical talents to empower allies and bewitch enemies. Their well-versed knowledge allows them to master any perk without prerequisites.

", + "keyStat": "presence", + "actionStyle": "performance", + "zone": "midline", + "trainedSkills": ["performance"], + "startingPack": "

Musician

", + "isCaster": false, + "progression": [ + { + "level": 1, + "mana": 0, + "castingMax": 0, + "spellsKnown": 0, + "features": ["Virtuoso", "Well-Versed"] + }, + { "level": 2, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Song of Rest"] }, + { "level": 3, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 4, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Starstruck"] }, + { "level": 5, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 6, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Bravado"] }, + { "level": 7, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 8, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Awe-Inspiring"] }, + { "level": 9, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 10, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Encore"] } + ], + "features": [ + { + "name": "Virtuoso", + "level": 1, + "description": "

During Combat, you can use your Action or skip your Move to perform with a Musical Instrument you have Equipped. When you do, your Allies that can hear you gain Favor on certain rolls of your choice until your next Turn: Inspiration: Healing rolls (as d6 bonus), Resolve: Saves, Valor: Attack or Cast Checks.

", + "passive": false, + "changes": [] + }, + { + "name": "Well-Versed", + "level": 1, + "description": "

You ignore Prerequisites for Perks, and take a Perk of your choice now.

", + "passive": true, + "changes": [] + }, + { + "name": "Song of Rest", + "level": 2, + "description": "

During a Breather while you are not Incapacitated, you and your Allies gain a Studied die and regain additional HP equal to (your Presence + your Bard Level).

", + "passive": true, + "changes": [] + }, + { + "name": "Starstruck", + "level": 4, + "description": "

When you perform Virtuoso, you can choose a Near Enemy who hears the performance and make a Performance Check. If you pass, you can choose one of the following Statuses that affects it for Cd4 Rounds: Berserk, Charmed, Confused, Frightened.

", + "passive": false, + "changes": [] + }, + { + "name": "Bravado", + "level": 6, + "description": "

Your Will Saves cannot be Hindered while you are not Incapacitated, and you can ignore effects that rely on you hearing them to be affected (such as a banshee's scream).

", + "passive": true, + "changes": [] + }, + { + "name": "Awe-Inspiring", + "level": 8, + "description": "

Your Virtuoso now grants two Favor.

", + "passive": true, + "changes": [] + }, + { + "name": "Encore", + "level": 10, + "description": "

Your Starstruck Feature can now affect all Near Enemies.

", + "passive": true, + "changes": [] + } + ], + "customResource": {} + } +} diff --git a/packs/_source/classes/dancer.json b/packs/_source/classes/dancer.json new file mode 100755 index 0000000..81a9a66 --- /dev/null +++ b/packs/_source/classes/dancer.json @@ -0,0 +1,92 @@ +{ + "_id": "vagabondClsDancer", + "_key": "!items!vagabondClsDancer", + "name": "Dancer", + "type": "class", + "img": "icons/svg/silhouette.svg", + "system": { + "description": "

Dancers are graceful performers who use their movements to inspire allies and evade attacks. Their fleet footwork makes them exceptional support characters who can grant allies extra actions.

", + "keyStat": "dexterity", + "actionStyle": "finesse", + "zone": "midline", + "trainedSkills": ["finesse", "performance", "brawl", "influence", "sneak"], + "startingPack": "

Courtesan or Musician

", + "isCaster": false, + "progression": [ + { + "level": 1, + "mana": 0, + "castingMax": 0, + "spellsKnown": 0, + "features": ["Fleet of Foot", "Step Up"] + }, + { "level": 2, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Evasive"] }, + { "level": 3, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { + "level": 4, + "mana": 0, + "castingMax": 0, + "spellsKnown": 0, + "features": ["Don't Stop Me Now"] + }, + { "level": 5, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 6, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Choreographer"] }, + { "level": 7, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 8, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Flash of Beauty"] }, + { "level": 9, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 10, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Double Time"] } + ], + "features": [ + { + "name": "Fleet of Foot", + "level": 1, + "description": "

You gain the Treads Lightly Perk, and the roll for you to Crit on Reflex Saves is reduced by an amount equal to (your Dancer Level divided by 4, round up).

", + "passive": true, + "changes": [] + }, + { + "name": "Step Up", + "level": 1, + "description": "

Once per Turn, you can use your Action to perform an enlivening dance. Doing so allows you to roll a second d20 on Reflex Saves until the start of your next Turn and use the higher result, and it gives one Ally of your choice that sees you a second Action this Turn.

", + "passive": false, + "changes": [] + }, + { + "name": "Evasive", + "level": 2, + "description": "

While you are not Incapacitated, you ignore Hinder on Reflex Saves and you ignore two of a Dodged attack's damage dice on a passed Save, rather than one.

", + "passive": true, + "changes": [] + }, + { + "name": "Don't Stop Me Now", + "level": 4, + "description": "

Your Speed is not affected by Difficult Terrain and you have Favor on Saves against being Paralyzed, Restrained, or moved.

", + "passive": true, + "changes": [] + }, + { + "name": "Choreographer", + "level": 6, + "description": "

When you use your Step Up Feature, the Ally gains Favor on the first Check they make with the Action you give them, and you both gain a 10 foot bonus to Speed for the Round.

", + "passive": true, + "changes": [] + }, + { + "name": "Flash of Beauty", + "level": 8, + "description": "

When you Crit on a Save, you can take two Actions, rather than one.

", + "passive": true, + "changes": [] + }, + { + "name": "Double Time", + "level": 10, + "description": "

You can Target two Allies with your Step Up Feature, rather than one.

", + "passive": true, + "changes": [] + } + ], + "customResource": {} + } +} diff --git a/packs/_source/classes/druid.json b/packs/_source/classes/druid.json new file mode 100755 index 0000000..1d63d30 --- /dev/null +++ b/packs/_source/classes/druid.json @@ -0,0 +1,86 @@ +{ + "_id": "vagabondClsDruid0", + "_key": "!items!vagabondClsDruid0", + "name": "Druid", + "type": "class", + "img": "icons/svg/oak.svg", + "system": { + "description": "

Druids are primal mystics who draw power from nature. Their signature ability to shapeshift into beasts makes them versatile combatants who can adapt to any situation.

", + "keyStat": "awareness", + "actionStyle": "mysticism", + "zone": "midline", + "trainedSkills": ["mysticism", "brawl", "craft", "detect", "medicine", "survival"], + "startingPack": "

Hermit

", + "isCaster": true, + "progression": [ + { + "level": 1, + "mana": 4, + "castingMax": 0, + "spellsKnown": 4, + "features": ["Primal Mystic", "Feral Shift"] + }, + { "level": 2, "mana": 4, "castingMax": 0, "spellsKnown": 0, "features": ["Tempest Within"] }, + { "level": 3, "mana": 4, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { "level": 4, "mana": 4, "castingMax": 0, "spellsKnown": 0, "features": ["Innervate"] }, + { "level": 5, "mana": 4, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { "level": 6, "mana": 4, "castingMax": 0, "spellsKnown": 0, "features": ["Ancient Growth"] }, + { "level": 7, "mana": 4, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { "level": 8, "mana": 4, "castingMax": 0, "spellsKnown": 0, "features": ["Savagery"] }, + { "level": 9, "mana": 4, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { "level": 10, "mana": 4, "castingMax": 0, "spellsKnown": 0, "features": ["Force of Nature"] } + ], + "features": [ + { + "name": "Primal Mystic", + "level": 1, + "description": "

You can Cast Spells using Mysticism. Spells: You learn 4 Spells, one of which must always be Polymorph. You learn 1 other Spell every 2 Levels in this Class hereafter. Mana: Your Maximum Mana is equal to (4 × your Druid Level), and the highest amount of Mana you can spend to Cast a Spell is equal to (Awareness + half your Druid Level, round up). You regain spent Mana when you Rest.

", + "passive": true, + "changes": [] + }, + { + "name": "Feral Shift", + "level": 1, + "description": "

You get the Shapechanger Perk and you can take an Action granted by the Beast you turn into as a part of the Cast Action.

", + "passive": true, + "changes": [] + }, + { + "name": "Tempest Within", + "level": 2, + "description": "

You reduce Cold, Fire, and Shock damage you take by (half your Druid Level) per damage die.

", + "passive": true, + "changes": [] + }, + { + "name": "Innervate", + "level": 4, + "description": "

You can use your Action to give a Close Being some of your Mana, or to end one Status affecting it from either Charmed, Confused, Frightened, or Sickened. This can be yourself.

", + "passive": false, + "changes": [] + }, + { + "name": "Ancient Growth", + "level": 6, + "description": "

While you Focus on a Casting of Polymorph that only Targets yourself, you can Focus one additional Spell. Further, your attacks with Beasts you Polymorph into count as (+1) Relics. This bonus increases every 6 Druid Levels hereafter, to a max of (+3).

", + "passive": true, + "changes": [] + }, + { + "name": "Savagery", + "level": 8, + "description": "

While you are polymorphed into a Beast, you have a +1 bonus to Armor.

", + "passive": true, + "changes": [] + }, + { + "name": "Force of Nature", + "level": 10, + "description": "

If you are reduced to 0 HP, roll a d10. If the result is equal to or lower than your Awareness, you are instead at 1 HP.

", + "passive": true, + "changes": [] + } + ], + "customResource": {} + } +} diff --git a/packs/_source/classes/fighter.json b/packs/_source/classes/fighter.json new file mode 100755 index 0000000..a49d38c --- /dev/null +++ b/packs/_source/classes/fighter.json @@ -0,0 +1,104 @@ +{ + "_id": "vagabondClsFighter", + "_key": "!items!vagabondClsFighter", + "name": "Fighter", + "type": "class", + "img": "icons/svg/sword.svg", + "system": { + "description": "

Fighters are versatile combatants who excel in any battlefield role. Whether wielding sword or bow, their martial prowess and tactical awareness make them formidable opponents.

", + "keyStat": "might", + "actionStyle": "attack", + "zone": "frontline", + "trainedSkills": ["brawl", "finesse", "detect", "leadership", "sneak"], + "startingPack": "

Gladiator, Knight, Warrior, or Watchman

", + "isCaster": false, + "progression": [ + { + "level": 1, + "mana": 0, + "castingMax": 0, + "spellsKnown": 0, + "features": ["Fighting Style", "Valor I"] + }, + { "level": 2, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Momentum"] }, + { "level": 3, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 4, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Valor II"] }, + { "level": 5, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { + "level": 6, + "mana": 0, + "castingMax": 0, + "spellsKnown": 0, + "features": ["Muster for Battle"] + }, + { "level": 7, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 8, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Valor III"] }, + { "level": 9, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 10, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Harrying"] } + ], + "features": [ + { + "name": "Fighting Style", + "level": 1, + "description": "

You gain the Situational Awareness Perk and another Perk with the Melee or Ranged Training Prerequisite. You can ignore Stat prerequisites for this Perk.

", + "passive": true, + "requiresChoice": true, + "choiceType": "perk", + "choiceFilter": { "prerequisite": ["Melee Training", "Ranged Training"] }, + "changes": [] + }, + { + "name": "Valor I", + "level": 1, + "description": "

The roll required for you to Crit on Attack Checks, and Saves to Dodge or Block Attacks is reduced by 1.

", + "passive": true, + "changes": [ + { "key": "system.attacks.melee.critThreshold", "mode": 2, "value": "-1", "priority": 10 }, + { "key": "system.attacks.ranged.critThreshold", "mode": 2, "value": "-1", "priority": 10 } + ] + }, + { + "name": "Valor II", + "level": 4, + "description": "

Your Valor improves. The crit threshold reduction increases to -2 total.

", + "passive": true, + "changes": [ + { "key": "system.attacks.melee.critThreshold", "mode": 2, "value": "-1", "priority": 10 }, + { "key": "system.attacks.ranged.critThreshold", "mode": 2, "value": "-1", "priority": 10 } + ] + }, + { + "name": "Valor III", + "level": 8, + "description": "

Your Valor reaches its peak. The crit threshold reduction increases to -3 total.

", + "passive": true, + "changes": [ + { "key": "system.attacks.melee.critThreshold", "mode": 2, "value": "-1", "priority": 10 }, + { "key": "system.attacks.ranged.critThreshold", "mode": 2, "value": "-1", "priority": 10 } + ] + }, + { + "name": "Momentum", + "level": 2, + "description": "

If you pass a Save against an attack, the next attack you make before the end of your next Turn is Favored.

", + "passive": true, + "changes": [] + }, + { + "name": "Muster for Battle", + "level": 6, + "description": "

You have two Actions on your first Turn of Combat.

", + "passive": true, + "changes": [] + }, + { + "name": "Harrying", + "level": 10, + "description": "

You can attack twice with the Attack Action, rather than just once.

", + "passive": true, + "changes": [] + } + ], + "customResource": {} + } +} diff --git a/packs/_source/classes/gunslinger.json b/packs/_source/classes/gunslinger.json new file mode 100755 index 0000000..8175694 --- /dev/null +++ b/packs/_source/classes/gunslinger.json @@ -0,0 +1,86 @@ +{ + "_id": "vagabondClsGunsli", + "_key": "!items!vagabondClsGunsli", + "name": "Gunslinger", + "type": "class", + "img": "icons/svg/target.svg", + "system": { + "description": "

Gunslingers are deadly marksmen who specialize in ranged combat. Their quick draw and deadly aim make them feared backline strikers who excel at taking down targets from a distance.

", + "keyStat": "awareness", + "actionStyle": "attack", + "zone": "backline", + "trainedSkills": ["detect", "brawl", "finesse", "influence", "sneak", "survival"], + "startingPack": "

Bounty Hunter or Tinker

", + "isCaster": false, + "progression": [ + { + "level": 1, + "mana": 0, + "castingMax": 0, + "spellsKnown": 0, + "features": ["Quick Draw", "Deadeye"] + }, + { "level": 2, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Skeet Shooter"] }, + { "level": 3, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 4, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Grit"] }, + { "level": 5, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 6, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Devastator"] }, + { "level": 7, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 8, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Bad Medicine"] }, + { "level": 9, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 10, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["High Noon"] } + ], + "features": [ + { + "name": "Quick Draw", + "level": 1, + "description": "

You gain the Marksmanship Perk. Further, when combat occurs, you can make one attack before the first Turn.

", + "passive": true, + "changes": [] + }, + { + "name": "Deadeye", + "level": 1, + "description": "

After you pass a Ranged Check, you Crit on subsequent Ranged attacks on a d20 roll 1 lower, but no lower than 17. This resets to 0 at the end of your Turn if you did not pass a Ranged Check since your last Turn.

", + "passive": true, + "changes": [] + }, + { + "name": "Skeet Shooter", + "level": 2, + "description": "

Once per Round, you can make a Ranged attack on an Off-Turn to Target a projectile from an attack you can see. If you pass, reduce the damage of the triggering attack by the damage you would deal with your attack. The projectile falls out of the air if reduced to 0.

", + "passive": false, + "changes": [] + }, + { + "name": "Grit", + "level": 4, + "description": "

When you Crit on a Ranged attack, the damage dice can explode.

", + "passive": true, + "changes": [] + }, + { + "name": "Devastator", + "level": 6, + "description": "

When you reduce an Enemy to 0 HP, the roll on the d20 to Crit as per your Deadeye Feature is immediately set to 17.

", + "passive": true, + "changes": [] + }, + { + "name": "Bad Medicine", + "level": 8, + "description": "

You deal an extra die of damage when you Crit with a Ranged Check.

", + "passive": true, + "changes": [] + }, + { + "name": "High Noon", + "level": 10, + "description": "

Once per Turn, if you Crit on a Ranged Check, you can make one additional attack.

", + "passive": true, + "changes": [] + } + ], + "customResource": {} + } +} diff --git a/packs/_source/classes/hunter.json b/packs/_source/classes/hunter.json new file mode 100755 index 0000000..68a0d62 --- /dev/null +++ b/packs/_source/classes/hunter.json @@ -0,0 +1,92 @@ +{ + "_id": "vagabondClsHunter", + "_key": "!items!vagabondClsHunter", + "name": "Hunter", + "type": "class", + "img": "icons/svg/pawprint.svg", + "system": { + "description": "

Hunters are expert trackers and survivalists who mark their prey for destruction. Their ability to focus on a single target makes them deadly strikers who rarely miss their mark.

", + "keyStat": "awareness", + "actionStyle": "attack", + "zone": "midline", + "trainedSkills": ["survival"], + "startingPack": "

Scout or Warrior

", + "isCaster": false, + "progression": [ + { + "level": 1, + "mana": 0, + "castingMax": 0, + "spellsKnown": 0, + "features": ["Hunter's Mark", "Survivalist"] + }, + { "level": 2, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Rover"] }, + { "level": 3, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 4, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Overwatch"] }, + { "level": 5, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 6, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Quarry"] }, + { "level": 7, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { + "level": 8, + "mana": 0, + "castingMax": 0, + "spellsKnown": 0, + "features": ["Lethal Precision"] + }, + { "level": 9, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 10, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Apex Predator"] } + ], + "features": [ + { + "name": "Hunter's Mark", + "level": 1, + "description": "

You can mark a Being either when you attack it, or by skipping your Move if you can sense it. When you do: You must Focus on the mark. When you make an attack against it, roll two d20s and use the highest for the Check.

", + "passive": false, + "changes": [] + }, + { + "name": "Survivalist", + "level": 1, + "description": "

You gain the Padfoot Perk, you have Favor on Checks to track and navigate, and you can Forage while Traveling at a Normal Pace.

", + "passive": true, + "changes": [] + }, + { + "name": "Rover", + "level": 2, + "description": "

Difficult Terrain does not impede your walking Speed, and you have Climb and Swim.

", + "passive": true, + "changes": [] + }, + { + "name": "Overwatch", + "level": 4, + "description": "

Your additional d20 for attacks with your Hunter's Mark also applies to your Saves provoked by the marked Target.

", + "passive": true, + "changes": [] + }, + { + "name": "Quarry", + "level": 6, + "description": "

You can sense Beings that are missing any HP within Far or that are marked by your Hunter's Mark as if by Blindsight.

", + "passive": true, + "changes": [] + }, + { + "name": "Lethal Precision", + "level": 8, + "description": "

You now roll three d20s with your Hunter's Mark and Overwatch Features and use the highest result of the three for the result.

", + "passive": true, + "changes": [] + }, + { + "name": "Apex Predator", + "level": 10, + "description": "

Damage dealt to the Target of your Hunter's Mark ignores Immune and 1 of its Armor.

", + "passive": true, + "changes": [] + } + ], + "customResource": {} + } +} diff --git a/packs/_source/classes/luminary.json b/packs/_source/classes/luminary.json new file mode 100755 index 0000000..82132a8 --- /dev/null +++ b/packs/_source/classes/luminary.json @@ -0,0 +1,86 @@ +{ + "_id": "vagabondClsLumina", + "_key": "!items!vagabondClsLumina", + "name": "Luminary", + "type": "class", + "img": "icons/svg/sun.svg", + "system": { + "description": "

Luminaries are divine healers who channel radiant energy to restore their allies. Their powerful healing magic and ability to revive the fallen make them invaluable support characters.

", + "keyStat": "awareness", + "actionStyle": "mysticism", + "zone": "midline", + "trainedSkills": ["mysticism", "arcana", "detect", "influence", "leadership", "medicine"], + "startingPack": "

Devout

", + "isCaster": true, + "progression": [ + { + "level": 1, + "mana": 4, + "castingMax": 0, + "spellsKnown": 4, + "features": ["Theurgy", "Radiant Healer"] + }, + { "level": 2, "mana": 4, "castingMax": 0, "spellsKnown": 0, "features": ["Overheal"] }, + { "level": 3, "mana": 4, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { "level": 4, "mana": 4, "castingMax": 0, "spellsKnown": 0, "features": ["Ever-Cure"] }, + { "level": 5, "mana": 4, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { "level": 6, "mana": 4, "castingMax": 0, "spellsKnown": 0, "features": ["Revivify"] }, + { "level": 7, "mana": 4, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { "level": 8, "mana": 4, "castingMax": 0, "spellsKnown": 0, "features": ["Saving Grace"] }, + { "level": 9, "mana": 4, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { "level": 10, "mana": 4, "castingMax": 0, "spellsKnown": 0, "features": ["Life-Giver"] } + ], + "features": [ + { + "name": "Theurgy", + "level": 1, + "description": "

You can Cast Spells using Mysticism. Spells: You learn 4 Spells, two of which must always be Life and Light. You learn 1 other Spell every 2 Luminary Levels hereafter. Mana: Your Maximum Mana is equal to (4 × your Luminary Level), and the highest amount of Mana you can spend to Cast a Spell is equal to (Awareness + half your Luminary Level, round up). You regain spent Mana when you Rest.

", + "passive": true, + "changes": [] + }, + { + "name": "Radiant Healer", + "level": 1, + "description": "

You get the Assured Healer Perk, and the healing rolls of your Spells can also explode on their highest value.

", + "passive": true, + "changes": [] + }, + { + "name": "Overheal", + "level": 2, + "description": "

If you restore HP that exceeds the Being's Max HP, you can give the excess to yourself or a Being you can see.

", + "passive": true, + "changes": [] + }, + { + "name": "Ever-Cure", + "level": 4, + "description": "

When you restore HP, you can end either a Charmed, Confused, Dazed, Frightened, or Sickened Status affecting the Target.

", + "passive": true, + "changes": [] + }, + { + "name": "Revivify", + "level": 6, + "description": "

You can revive a Being with the Life Spell if it has been dead for as long as 1 hour. If you die, you are revived automatically. Afterward, you cannot be revived by this Feature for 1 day.

", + "passive": true, + "changes": [] + }, + { + "name": "Saving Grace", + "level": 8, + "description": "

Your healing rolls can also explode on a 2.

", + "passive": true, + "changes": [] + }, + { + "name": "Life-Giver", + "level": 10, + "description": "

Beings you revive are revived at 4 Fatigue if their Fatigue was previously higher, and do not gain Fatigue from your Life Spell.

", + "passive": true, + "changes": [] + } + ], + "customResource": {} + } +} diff --git a/packs/_source/classes/magus.json b/packs/_source/classes/magus.json new file mode 100755 index 0000000..751e29d --- /dev/null +++ b/packs/_source/classes/magus.json @@ -0,0 +1,92 @@ +{ + "_id": "vagabondClsMagus0", + "_key": "!items!vagabondClsMagus0", + "name": "Magus", + "type": "class", + "img": "icons/svg/lightning.svg", + "system": { + "description": "

Magi are spell-swords who blend martial prowess with arcane magic. Their ability to channel spells through their weapons makes them versatile combatants who excel in both melee and magical combat.

", + "keyStat": "reason", + "actionStyle": "arcana", + "zone": "frontline", + "trainedSkills": ["arcana", "brawl", "finesse"], + "startingPack": "

Knight or Scholar

", + "isCaster": true, + "progression": [ + { + "level": 1, + "mana": 4, + "castingMax": 0, + "spellsKnown": 4, + "features": ["Arcane Warrior", "Spell Strike"] + }, + { "level": 2, "mana": 4, "castingMax": 0, "spellsKnown": 0, "features": ["Arcane Pool"] }, + { "level": 3, "mana": 4, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { "level": 4, "mana": 4, "castingMax": 0, "spellsKnown": 0, "features": ["Spell Combat"] }, + { "level": 5, "mana": 4, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { "level": 6, "mana": 4, "castingMax": 0, "spellsKnown": 0, "features": ["Counterstrike"] }, + { "level": 7, "mana": 4, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { + "level": 8, + "mana": 4, + "castingMax": 0, + "spellsKnown": 0, + "features": ["Greater Spell Strike"] + }, + { "level": 9, "mana": 4, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { "level": 10, "mana": 4, "castingMax": 0, "spellsKnown": 0, "features": ["True Magus"] } + ], + "features": [ + { + "name": "Arcane Warrior", + "level": 1, + "description": "

You can Cast Spells using Arcana. Spells: You learn 4 Spells. You learn 1 other Spell every 2 Levels in this Class hereafter. Mana: Your Maximum Mana is equal to (4 × your Magus Level), and the highest amount of Mana you can spend to Cast a Spell is equal to (Reason + half your Magus Level, round up). You regain spent Mana when you Rest.

", + "passive": true, + "changes": [] + }, + { + "name": "Spell Strike", + "level": 1, + "description": "

When you Cast a Spell that has a Range of Touch, you can deliver the Spell through a Melee Weapon attack. If the attack hits, the target is affected by both the attack and the Spell.

", + "passive": false, + "changes": [] + }, + { + "name": "Arcane Pool", + "level": 2, + "description": "

You can spend 1 Mana to enhance your weapon. For 1 minute, it counts as a (+1) Relic. This bonus increases by +1 at 6th and 10th Levels in this Class.

", + "passive": false, + "changes": [] + }, + { + "name": "Spell Combat", + "level": 4, + "description": "

When you take the Attack Action, you can also Cast a Spell with a Casting Time of 1 Action as part of that Action.

", + "passive": true, + "changes": [] + }, + { + "name": "Counterstrike", + "level": 6, + "description": "

When an Enemy within your Melee Reach Casts a Spell, you can make an attack against them as a Reaction.

", + "passive": false, + "changes": [] + }, + { + "name": "Greater Spell Strike", + "level": 8, + "description": "

When you use Spell Strike and hit, you can spend 2 Mana to maximize the Spell's damage dice instead of rolling them.

", + "passive": false, + "changes": [] + }, + { + "name": "True Magus", + "level": 10, + "description": "

When you use Spell Combat, you can Cast two Spells instead of one.

", + "passive": true, + "changes": [] + } + ], + "customResource": {} + } +} diff --git a/packs/_source/classes/merchant.json b/packs/_source/classes/merchant.json new file mode 100755 index 0000000..9d9135b --- /dev/null +++ b/packs/_source/classes/merchant.json @@ -0,0 +1,86 @@ +{ + "_id": "vagabondClsMercha", + "_key": "!items!vagabondClsMercha", + "name": "Merchant", + "type": "class", + "img": "icons/svg/coins.svg", + "system": { + "description": "

Merchants are savvy traders who use their wealth and social connections to support their allies. Their ability to acquire rare items and manipulate markets makes them invaluable support characters.

", + "keyStat": "reason", + "actionStyle": "influence", + "zone": "midline", + "trainedSkills": ["influence", "craft", "detect"], + "startingPack": "

Noble or Traveler

", + "isCaster": false, + "progression": [ + { + "level": 1, + "mana": 0, + "castingMax": 0, + "spellsKnown": 0, + "features": ["Deep Pockets", "Connections"] + }, + { "level": 2, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Appraisal"] }, + { "level": 3, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 4, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Haggle"] }, + { "level": 5, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 6, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Investment"] }, + { "level": 7, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 8, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Black Market"] }, + { "level": 9, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 10, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Tycoon"] } + ], + "features": [ + { + "name": "Deep Pockets", + "level": 1, + "description": "

You start with an additional 5g, and you gain 1g at the start of each session.

", + "passive": true, + "changes": [] + }, + { + "name": "Connections", + "level": 1, + "description": "

You gain the Silver Tongue Perk. In any settlement, you can find a contact who can help you acquire goods, information, or services.

", + "passive": true, + "changes": [] + }, + { + "name": "Appraisal", + "level": 2, + "description": "

You can determine the exact value of any item, and you have Favor on Checks to identify magical items.

", + "passive": true, + "changes": [] + }, + { + "name": "Haggle", + "level": 4, + "description": "

You buy items for 75% of their listed price and sell items for 75% of their value (instead of 50%).

", + "passive": true, + "changes": [] + }, + { + "name": "Investment", + "level": 6, + "description": "

During downtime, you can invest gold. At the start of the next session, roll a d6: on 4-6, you gain 50% profit; on 2-3, you break even; on 1, you lose 50%.

", + "passive": false, + "changes": [] + }, + { + "name": "Black Market", + "level": 8, + "description": "

You can acquire any item regardless of its legality or rarity, given enough time and gold.

", + "passive": true, + "changes": [] + }, + { + "name": "Tycoon", + "level": 10, + "description": "

Your session gold increases to 3g, and your investments always succeed (minimum break even on a 1).

", + "passive": true, + "changes": [] + } + ], + "customResource": {} + } +} diff --git a/packs/_source/classes/pugilist.json b/packs/_source/classes/pugilist.json new file mode 100755 index 0000000..c28be51 --- /dev/null +++ b/packs/_source/classes/pugilist.json @@ -0,0 +1,92 @@ +{ + "_id": "vagabondClsPugili", + "_key": "!items!vagabondClsPugili", + "name": "Pugilist", + "type": "class", + "img": "icons/svg/fist.svg", + "system": { + "description": "

Pugilists are masters of unarmed combat who turn their bodies into deadly weapons. Their devastating punches and grappling techniques make them formidable frontline bruisers.

", + "keyStat": "might", + "actionStyle": "brawl", + "zone": "frontline", + "trainedSkills": ["brawl", "finesse", "detect", "influence"], + "startingPack": "

Gladiator or Traveler

", + "isCaster": false, + "progression": [ + { + "level": 1, + "mana": 0, + "castingMax": 0, + "spellsKnown": 0, + "features": ["Fisticuffs", "Haymaker"] + }, + { + "level": 2, + "mana": 0, + "castingMax": 0, + "spellsKnown": 0, + "features": ["Deflect Missiles"] + }, + { "level": 3, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 4, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Stunning Blow"] }, + { "level": 5, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 6, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Iron Body"] }, + { "level": 7, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 8, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Flurry of Blows"] }, + { "level": 9, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 10, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perfect Form"] } + ], + "features": [ + { + "name": "Fisticuffs", + "level": 1, + "description": "

Your Unarmed attacks deal 1d6 damage (increasing to 1d8 at 5th level and 1d10 at 9th level), and you can make an Unarmed attack as a Move.

", + "passive": true, + "changes": [] + }, + { + "name": "Haymaker", + "level": 1, + "description": "

You gain the Wrestler Perk. When you hit with an Unarmed attack, you can push the target 5 feet.

", + "passive": true, + "changes": [] + }, + { + "name": "Deflect Missiles", + "level": 2, + "description": "

When hit by a Ranged attack, you can use your Reaction to reduce the damage by 1d10 + your Dexterity + your Pugilist Level.

", + "passive": false, + "changes": [] + }, + { + "name": "Stunning Blow", + "level": 4, + "description": "

When you hit with an Unarmed attack, you can force the target to make a Fortitude Save or be Dazed until the end of your next Turn.

", + "passive": false, + "changes": [] + }, + { + "name": "Iron Body", + "level": 6, + "description": "

While you are not wearing Armor, you have +1 Armor. You also have resistance to Bludgeoning damage.

", + "passive": true, + "changes": [] + }, + { + "name": "Flurry of Blows", + "level": 8, + "description": "

When you take the Attack Action with an Unarmed attack, you can make two additional Unarmed attacks.

", + "passive": true, + "changes": [] + }, + { + "name": "Perfect Form", + "level": 10, + "description": "

Your Unarmed attacks count as (+2) Relics, and you have Favor on all Saves while you are not wearing Armor.

", + "passive": true, + "changes": [] + } + ], + "customResource": {} + } +} diff --git a/packs/_source/classes/revelator.json b/packs/_source/classes/revelator.json new file mode 100755 index 0000000..7a2da81 --- /dev/null +++ b/packs/_source/classes/revelator.json @@ -0,0 +1,95 @@ +{ + "_id": "vagabondClsRevela", + "_key": "!items!vagabondClsRevela", + "name": "Revelator", + "type": "class", + "img": "icons/svg/holy-shield.svg", + "system": { + "description": "

Revelators are holy warriors who combine martial prowess with divine magic. Their ability to smite evil and protect allies makes them formidable frontline paladins.

", + "keyStat": "might", + "actionStyle": "leadership", + "zone": "frontline", + "trainedSkills": ["leadership", "brawl", "influence", "mysticism"], + "startingPack": "

Knight or Devout

", + "isCaster": true, + "progression": [ + { + "level": 1, + "mana": 2, + "castingMax": 0, + "spellsKnown": 2, + "features": ["Divine Sense", "Lay on Hands"] + }, + { "level": 2, "mana": 2, "castingMax": 0, "spellsKnown": 0, "features": ["Divine Smite"] }, + { "level": 3, "mana": 2, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { + "level": 4, + "mana": 2, + "castingMax": 0, + "spellsKnown": 0, + "features": ["Aura of Protection"] + }, + { "level": 5, "mana": 2, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { "level": 6, "mana": 2, "castingMax": 0, "spellsKnown": 0, "features": ["Aura of Courage"] }, + { "level": 7, "mana": 2, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { "level": 8, "mana": 2, "castingMax": 0, "spellsKnown": 0, "features": ["Improved Smite"] }, + { "level": 9, "mana": 2, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { "level": 10, "mana": 2, "castingMax": 0, "spellsKnown": 0, "features": ["Holy Champion"] } + ], + "features": [ + { + "name": "Divine Sense", + "level": 1, + "description": "

You can Cast Spells using Leadership. Spells: You learn 2 Spells. You learn 1 other Spell every 2 Levels in this Class hereafter. Mana: Your Maximum Mana is equal to (2 × your Revelator Level). You regain spent Mana when you Rest.

", + "passive": true, + "changes": [] + }, + { + "name": "Lay on Hands", + "level": 1, + "description": "

You have a pool of healing power equal to (5 × your Revelator Level). As an Action, you can touch a Being and restore HP from this pool. You can also spend 5 HP from this pool to cure one disease or neutralize one poison.

", + "passive": false, + "changes": [] + }, + { + "name": "Divine Smite", + "level": 2, + "description": "

When you hit with a Melee attack, you can spend Mana to deal extra Radiant damage: 2d8 for 1 Mana, +1d8 for each additional Mana spent (max 5d8).

", + "passive": false, + "changes": [] + }, + { + "name": "Aura of Protection", + "level": 4, + "description": "

You and Allies within 10 feet gain a bonus to Saves equal to your Presence (minimum +1).

", + "passive": true, + "changes": [] + }, + { + "name": "Aura of Courage", + "level": 6, + "description": "

You and Allies within 10 feet cannot be Frightened while you are conscious.

", + "passive": true, + "changes": [] + }, + { + "name": "Improved Smite", + "level": 8, + "description": "

All your Melee attacks deal an extra 1d8 Radiant damage.

", + "passive": true, + "changes": [] + }, + { + "name": "Holy Champion", + "level": 10, + "description": "

When you are reduced to 0 HP and not killed outright, you can choose to drop to 1 HP instead. Once you use this feature, you can't use it again until you Rest.

", + "passive": false, + "changes": [] + } + ], + "customResource": { + "name": "Lay on Hands Pool", + "max": "5 * @classes.revelator.level" + } + } +} diff --git a/packs/_source/classes/rogue.json b/packs/_source/classes/rogue.json new file mode 100755 index 0000000..c9e9cb3 --- /dev/null +++ b/packs/_source/classes/rogue.json @@ -0,0 +1,86 @@ +{ + "_id": "vagabondClsRogue0", + "_key": "!items!vagabondClsRogue0", + "name": "Rogue", + "type": "class", + "img": "icons/svg/dagger.svg", + "system": { + "description": "

Rogues are cunning opportunists who strike when their enemies least expect it. Their sneak attacks and luck manipulation make them deadly midline strikers who excel at exploiting weaknesses.

", + "keyStat": "dexterity", + "actionStyle": "attack", + "zone": "midline", + "trainedSkills": ["sneak", "finesse", "detect", "influence"], + "startingPack": "

Assassin or Thief

", + "isCaster": false, + "progression": [ + { + "level": 1, + "mana": 0, + "castingMax": 0, + "spellsKnown": 0, + "features": ["Expertise", "Sneak Attack"] + }, + { "level": 2, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Cunning Action"] }, + { "level": 3, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 4, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Uncanny Dodge"] }, + { "level": 5, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 6, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Evasion"] }, + { "level": 7, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 8, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Reliable Talent"] }, + { "level": 9, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 10, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Stroke of Luck"] } + ], + "features": [ + { + "name": "Expertise", + "level": 1, + "description": "

You gain the Jack of All Trades Perk. Choose two Skills you are Trained in; you have Favor on Checks with those Skills.

", + "passive": true, + "changes": [] + }, + { + "name": "Sneak Attack", + "level": 1, + "description": "

Once per Turn, when you hit a Target with an attack and have Favor on the attack roll, or an Enemy of the Target is within 5 feet of it, you deal an extra Cd6 damage. This damage increases by 1d6 at 3rd, 5th, 7th, and 9th Levels in this Class.

", + "passive": true, + "changes": [] + }, + { + "name": "Cunning Action", + "level": 2, + "description": "

You can take the Dash, Disengage, or Hide Action as a Move, rather than an Action.

", + "passive": true, + "changes": [] + }, + { + "name": "Uncanny Dodge", + "level": 4, + "description": "

When an attack hits you, you can use your Reaction to halve the attack's damage against you.

", + "passive": false, + "changes": [] + }, + { + "name": "Evasion", + "level": 6, + "description": "

When you pass a Reflex Save, you take no damage instead of half damage.

", + "passive": true, + "changes": [] + }, + { + "name": "Reliable Talent", + "level": 8, + "description": "

When you make a Check with a Skill you are Trained in and roll lower than 10 on the d20, you can treat the roll as a 10.

", + "passive": true, + "changes": [] + }, + { + "name": "Stroke of Luck", + "level": 10, + "description": "

Once per Rest, if you miss with an attack, you can turn the miss into a hit.

", + "passive": false, + "changes": [] + } + ], + "customResource": {} + } +} diff --git a/packs/_source/classes/sorcerer.json b/packs/_source/classes/sorcerer.json new file mode 100755 index 0000000..34340d0 --- /dev/null +++ b/packs/_source/classes/sorcerer.json @@ -0,0 +1,107 @@ +{ + "_id": "vagabondClsSorcer", + "_key": "!items!vagabondClsSorcer", + "name": "Sorcerer", + "type": "class", + "img": "icons/svg/fire.svg", + "system": { + "description": "

Sorcerers are innate spellcasters who channel raw magical power through force of personality. Their ability to manipulate their spells on the fly makes them devastating backline blasters.

", + "keyStat": "presence", + "actionStyle": "influence", + "zone": "backline", + "trainedSkills": ["influence", "arcana", "detect", "mysticism"], + "startingPack": "

Noble or Hermit

", + "isCaster": true, + "progression": [ + { + "level": 1, + "mana": 4, + "castingMax": 0, + "spellsKnown": 4, + "features": ["Innate Magic", "Metamagic"] + }, + { "level": 2, "mana": 4, "castingMax": 0, "spellsKnown": 0, "features": ["Font of Magic"] }, + { "level": 3, "mana": 4, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { + "level": 4, + "mana": 4, + "castingMax": 0, + "spellsKnown": 0, + "features": ["Sorcerous Origin"] + }, + { "level": 5, "mana": 4, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { + "level": 6, + "mana": 4, + "castingMax": 0, + "spellsKnown": 0, + "features": ["Empowered Spells"] + }, + { "level": 7, "mana": 4, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { "level": 8, "mana": 4, "castingMax": 0, "spellsKnown": 0, "features": ["Origin Mastery"] }, + { "level": 9, "mana": 4, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { + "level": 10, + "mana": 4, + "castingMax": 0, + "spellsKnown": 0, + "features": ["Arcane Apotheosis"] + } + ], + "features": [ + { + "name": "Innate Magic", + "level": 1, + "description": "

You can Cast Spells using Influence. Spells: You learn 4 Spells. You learn 1 other Spell every 2 Levels in this Class hereafter. Mana: Your Maximum Mana is equal to (4 × your Sorcerer Level), and the highest amount of Mana you can spend to Cast a Spell is equal to (Presence + half your Sorcerer Level, round up). You regain spent Mana when you Rest.

", + "passive": true, + "changes": [] + }, + { + "name": "Metamagic", + "level": 1, + "description": "

You can modify your Spells as you Cast them. Choose two Metamagic options: Careful (choose creatures to auto-succeed), Distant (double Range), Extended (double Duration), Quickened (Cast as Move), Subtle (no verbal/somatic), Twinned (Target second creature).

", + "passive": false, + "changes": [] + }, + { + "name": "Font of Magic", + "level": 2, + "description": "

You can convert Mana to Sorcery Points and vice versa. 1 Sorcery Point costs 1 Mana to create, and 1 Mana costs 2 Sorcery Points.

", + "passive": false, + "changes": [] + }, + { + "name": "Sorcerous Origin", + "level": 4, + "description": "

Choose an Origin that grants additional abilities: Draconic (resistance to one element, +1 Armor), Wild (random magical effects), or Shadow (see in darkness, teleport through shadows).

", + "passive": true, + "changes": [] + }, + { + "name": "Empowered Spells", + "level": 6, + "description": "

When you roll damage for a Spell, you can reroll a number of damage dice up to your Presence (minimum 1). You must use the new rolls.

", + "passive": false, + "changes": [] + }, + { + "name": "Origin Mastery", + "level": 8, + "description": "

Your Sorcerous Origin grants an improved ability based on your choice.

", + "passive": true, + "changes": [] + }, + { + "name": "Arcane Apotheosis", + "level": 10, + "description": "

You regain 4 Mana when you finish a Breather, and your Metamagic options cost 1 less Sorcery Point (minimum 1).

", + "passive": true, + "changes": [] + } + ], + "customResource": { + "name": "Sorcery Points", + "max": "@classes.sorcerer.level" + } + } +} diff --git a/packs/_source/classes/vanguard.json b/packs/_source/classes/vanguard.json new file mode 100755 index 0000000..096baac --- /dev/null +++ b/packs/_source/classes/vanguard.json @@ -0,0 +1,86 @@ +{ + "_id": "vagabondClsVangua", + "_key": "!items!vagabondClsVangua", + "name": "Vanguard", + "type": "class", + "img": "icons/svg/shield.svg", + "system": { + "description": "

Vanguards are stalwart defenders who protect their allies from harm. Their ability to absorb damage and control the battlefield makes them invaluable frontline tanks.

", + "keyStat": "might", + "actionStyle": "brawl", + "zone": "frontline", + "trainedSkills": ["brawl", "detect", "leadership", "influence"], + "startingPack": "

Knight or Warrior

", + "isCaster": false, + "progression": [ + { + "level": 1, + "mana": 0, + "castingMax": 0, + "spellsKnown": 0, + "features": ["Shield Wall", "Protector"] + }, + { "level": 2, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Taunt"] }, + { "level": 3, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 4, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Resolute"] }, + { "level": 5, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 6, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Hold the Line"] }, + { "level": 7, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 8, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Bulwark"] }, + { "level": 9, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Perk"] }, + { "level": 10, "mana": 0, "castingMax": 0, "spellsKnown": 0, "features": ["Immovable"] } + ], + "features": [ + { + "name": "Shield Wall", + "level": 1, + "description": "

While you have a Shield equipped, you have +1 Armor, and Allies within 5 feet of you have +1 to their Block Saves.

", + "passive": true, + "changes": [] + }, + { + "name": "Protector", + "level": 1, + "description": "

You gain the Sentinel Perk. When an Ally within 5 feet is targeted by an attack, you can use your Reaction to become the target instead.

", + "passive": false, + "changes": [] + }, + { + "name": "Taunt", + "level": 2, + "description": "

As an Action, you can taunt a Near Enemy. Until the end of your next Turn, it has Hinder on attacks that don't include you as a Target.

", + "passive": false, + "changes": [] + }, + { + "name": "Resolute", + "level": 4, + "description": "

You have Favor on Saves against being Frightened, Charmed, or moved against your will.

", + "passive": true, + "changes": [] + }, + { + "name": "Hold the Line", + "level": 6, + "description": "

Enemies provoke an Opportunity Attack from you when they move while within your Reach, even if they Disengage.

", + "passive": true, + "changes": [] + }, + { + "name": "Bulwark", + "level": 8, + "description": "

While you have a Shield equipped, you reduce all damage you take by 2 per damage die.

", + "passive": true, + "changes": [] + }, + { + "name": "Immovable", + "level": 10, + "description": "

You cannot be moved against your will, and you cannot be knocked Prone. Additionally, your Shield Wall bonus increases to +2 Armor and +2 to Block Saves.

", + "passive": true, + "changes": [] + } + ], + "customResource": {} + } +} diff --git a/packs/_source/classes/witch.json b/packs/_source/classes/witch.json new file mode 100755 index 0000000..d2f8a85 --- /dev/null +++ b/packs/_source/classes/witch.json @@ -0,0 +1,86 @@ +{ + "_id": "vagabondClsWitch0", + "_key": "!items!vagabondClsWitch0", + "name": "Witch", + "type": "class", + "img": "icons/svg/eye.svg", + "system": { + "description": "

Witches are mysterious practitioners of dark magic who hex their enemies and commune with spirits. Their curses and debuffs make them powerful backline controllers.

", + "keyStat": "awareness", + "actionStyle": "mysticism", + "zone": "backline", + "trainedSkills": ["mysticism", "arcana", "craft", "detect", "survival"], + "startingPack": "

Hermit or Occultist

", + "isCaster": true, + "progression": [ + { + "level": 1, + "mana": 4, + "castingMax": 0, + "spellsKnown": 4, + "features": ["Witchcraft", "Hex"] + }, + { "level": 2, "mana": 4, "castingMax": 0, "spellsKnown": 0, "features": ["Familiar"] }, + { "level": 3, "mana": 4, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { "level": 4, "mana": 4, "castingMax": 0, "spellsKnown": 0, "features": ["Evil Eye"] }, + { "level": 5, "mana": 4, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { "level": 6, "mana": 4, "castingMax": 0, "spellsKnown": 0, "features": ["Coven Magic"] }, + { "level": 7, "mana": 4, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { "level": 8, "mana": 4, "castingMax": 0, "spellsKnown": 0, "features": ["Greater Hex"] }, + { "level": 9, "mana": 4, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { "level": 10, "mana": 4, "castingMax": 0, "spellsKnown": 0, "features": ["Grand Hex"] } + ], + "features": [ + { + "name": "Witchcraft", + "level": 1, + "description": "

You can Cast Spells using Mysticism. Spells: You learn 4 Spells. You learn 1 other Spell every 2 Levels in this Class hereafter. Mana: Your Maximum Mana is equal to (4 × your Witch Level), and the highest amount of Mana you can spend to Cast a Spell is equal to (Awareness + half your Witch Level, round up). You regain spent Mana when you Rest.

", + "passive": true, + "changes": [] + }, + { + "name": "Hex", + "level": 1, + "description": "

You can place a Hex on a Near Being as an Action. Choose one: Misfortune (Hinder on one roll per Turn), Slumber (Will Save or fall asleep), Cackle (extend Hex duration). The Hex lasts for 1 minute or until you place another Hex.

", + "passive": false, + "changes": [] + }, + { + "name": "Familiar", + "level": 2, + "description": "

You gain a magical familiar that can scout, deliver Touch spells, and aid you. If your familiar dies, you can resummon it during a Rest.

", + "passive": true, + "changes": [] + }, + { + "name": "Evil Eye", + "level": 4, + "description": "

You can curse a Near Enemy with your gaze. It has Hinder on Saves against your Spells and Hexes until the end of your next Turn.

", + "passive": false, + "changes": [] + }, + { + "name": "Coven Magic", + "level": 6, + "description": "

When you are within 30 feet of another caster, you can combine your magic. When you both Cast the same Spell targeting the same area or creature, combine the effects.

", + "passive": true, + "changes": [] + }, + { + "name": "Greater Hex", + "level": 8, + "description": "

You learn additional Hex options: Agony (target takes Cd6 damage each Turn), Disguise (target appears different), Flight (target gains Fly).

", + "passive": true, + "changes": [] + }, + { + "name": "Grand Hex", + "level": 10, + "description": "

You learn the most powerful Hexes: Death Curse (target must Save or die), Eternal Slumber (target falls into magical sleep until curse is broken), Forced Reincarnation (target's soul is placed in a new body).

", + "passive": true, + "changes": [] + } + ], + "customResource": {} + } +} diff --git a/packs/_source/classes/wizard.json b/packs/_source/classes/wizard.json new file mode 100755 index 0000000..c58a64f --- /dev/null +++ b/packs/_source/classes/wizard.json @@ -0,0 +1,86 @@ +{ + "_id": "vagabondClsWizard0", + "_key": "!items!vagabondClsWizard0", + "name": "Wizard", + "type": "class", + "img": "icons/svg/book.svg", + "system": { + "description": "

Wizards are scholars of the arcane who cast spells through careful study and precise technique. Their deep understanding of magical theory makes them versatile backline blasters.

", + "keyStat": "reason", + "actionStyle": "arcana", + "zone": "backline", + "trainedSkills": ["arcana", "craft", "detect", "influence", "medicine"], + "startingPack": "

Scholar

", + "isCaster": true, + "progression": [ + { + "level": 1, + "mana": 4, + "castingMax": 0, + "spellsKnown": 4, + "features": ["Arcane Scholar", "Spellbook"] + }, + { "level": 2, "mana": 4, "castingMax": 0, "spellsKnown": 0, "features": ["Focused Casting"] }, + { "level": 3, "mana": 4, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { "level": 4, "mana": 4, "castingMax": 0, "spellsKnown": 0, "features": ["Magical Thesis"] }, + { "level": 5, "mana": 4, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { "level": 6, "mana": 4, "castingMax": 0, "spellsKnown": 0, "features": ["Multicast"] }, + { "level": 7, "mana": 4, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { "level": 8, "mana": 4, "castingMax": 0, "spellsKnown": 0, "features": ["Greater Thesis"] }, + { "level": 9, "mana": 4, "castingMax": 0, "spellsKnown": 1, "features": ["Perk"] }, + { "level": 10, "mana": 4, "castingMax": 0, "spellsKnown": 0, "features": ["Arcane Mastery"] } + ], + "features": [ + { + "name": "Arcane Scholar", + "level": 1, + "description": "

You can Cast Spells using Arcana. Spells: You learn 4 Spells. You learn 1 other Spell every 2 Levels in this Class hereafter. Mana: Your Maximum Mana is equal to (4 × your Wizard Level), and the highest amount of Mana you can spend to Cast a Spell is equal to (Reason + half your Wizard Level, round up). You regain spent Mana when you Rest.

", + "passive": true, + "changes": [] + }, + { + "name": "Spellbook", + "level": 1, + "description": "

You get the Ritualist Perk. You have a Spellbook that contains the details and methods of Casting your Spells. If you lose your Spellbook, you can recopy it during a Rest using materials worth 5s × your Wizard Level. You can copy Spells from others' Spellbooks or Scrolls; doing so costs 5s and requires 1 hour per Spell.

", + "passive": true, + "changes": [] + }, + { + "name": "Focused Casting", + "level": 2, + "description": "

You can Focus on two Spells simultaneously, rather than one.

", + "passive": true, + "changes": [] + }, + { + "name": "Magical Thesis", + "level": 4, + "description": "

Choose one: Abjurer (reduce damage from Spells by 1 per die), Conjurer (summons have +1 Armor), Evoker (Spell damage dice can explode), or Illusionist (Illusions are Favored to disbelieve).

", + "passive": true, + "changes": [] + }, + { + "name": "Multicast", + "level": 6, + "description": "

When you Cast a Spell with a Range of Close or farther, you can spend 1 additional Mana to Target an additional Being within Range.

", + "passive": true, + "changes": [] + }, + { + "name": "Greater Thesis", + "level": 8, + "description": "

Your Magical Thesis improves. Abjurer (reduce by 2), Conjurer (summons have +2 Armor), Evoker (explode on two highest values), Illusionist (Illusions deal psychic damage on failed disbelief).

", + "passive": true, + "changes": [] + }, + { + "name": "Arcane Mastery", + "level": 10, + "description": "

Once per Day, you can Cast a Spell without spending Mana.

", + "passive": true, + "changes": [] + } + ], + "customResource": {} + } +} diff --git a/packs/ancestries/000012.ldb b/packs/ancestries/000012.ldb new file mode 100755 index 0000000000000000000000000000000000000000..e8a28d9a5bc9577152cc8011f5c72b65069d4805 GIT binary patch literal 2834 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 KJ)JF=Kl}k8s}_3z literal 0 HcmV?d00001 diff --git a/packs/ancestries/000029.ldb b/packs/ancestries/000029.ldb new file mode 100644 index 0000000000000000000000000000000000000000..dba1bf3521d57f5c1fa99222215cc18099118185 GIT binary patch literal 3258 zcmb`KZEPIH8OLX{d&k{zKF-c19F-f*ULE8k=ZhU|Cv`)VGk%Fl;)Ju4I0-R~cW3X0 z+c(e5+Fp!N0tx~uS_u_Un=LdAc7wS!JWn`h9v66?$vP(3B{JZ|#**07 zmDut)_UoZw2DHK87WZ69qMplmvQ8Pt%<(xDg3VDZ*ew(j+xF%KmUZf2>0tx<53%FH z;B+=%z7zFIToZe9lrK*Q#-IW-7aO)GC=BAv5Lw>7+1~++nxQbs$-ob ziG<+$QVbqMXjk#R=^vqbok07hEtr@liy{UQ9D|qegzc@{yZ_iysX#4@8p{(uRw~>m zBvH~Giv@%#J#CS|R<2NbdTmmJ5HBt*r7w9f-c8-Ar)GBrkblitb!1kJ z9nzwJZi9`7?G`fHNXK)BQ`?D2d`V^?TAmf9M{HxKPd$j9h;oQbOa)iX`BokQqgYZ{^@V88UCQ%b1B|Wc_a2%dJ)4=m}@6lg$+sp@HaWAOVu{?bykA+XbA%<_w5qqA5;`DjsRK9?0a*YP* zc2yqaX;CU?SpsDJ%sc6t=fPg=lmV=p32f6EbwLc_*q3hKsv@C0r(ZePucGnTQ%8QY zro{0K0YOgql$y#sgyyu@E~trTLfn_UwY))1%m##?izDYO?OETI&3T3q_$)b*w+XLN zjun~)God4Q#dDLK84XeWQ#^gBPF?b(hUJMMPjD6dtrTn3IHgT(Q&0I@@$?=DLCe|Z zg1UNE7u3Yx;$>?=&5n9AHfx@WUw0~=oMH!upU9T55xC!dcA2-cUH0N(eG{ z;x!^PA@%FJ@dT`!x)3!m&-FOfgs(52Zm1^izNE{7pJ7Oiz0J4c{cpv!VSD4_+rf@% z9QGEx8e4?owtJErZf$9I!Mn9qNW`2^^Tv0)qYo2>b zBl7HzkpsCPz2UYT;oyLjI!YEfZMjriF4F@;YdO~gRN!Kxc`~(n-h49Ej^I$DYxD_KdHphg>l>efYw1&P-LneUA%L9~;R5h_ z+1n@nUv!<29>_RiT9j2b1IH?16ULeYu0_<1R_ZRJ6wDDc3GdhH5rEe9L6#`@0&P8@ z1OQP$o?Zm20PUC|R!;RwCW8nd%O)t4y?Y$I;(rlKF9JMvV9Mack^%_(X?iB#-&T)& zI`YF6cXePg+EtS?CLldz-Odmn{WmsWd_=D%XDyY{#2 zOGn^rAss;b3((ZdpM>W6RcLP2#?0&Q85>+khAj0e;{XT^&&Zc{=bs* zAD>CibBRrqBUBY?mIKk;%GB6m<@KgiiA+vum{g8!giJeL``PC1jLwrno0 zbdGPoSnd%HB^u=L-Ldfbe1t`lILcOCO+YFQepSt1sJ$xX+wWD;VC=Cxu1E>g9)E64 zdj+!G#TBFSbh?B?k3Bqn9(6}`7QsDzU6$v`6XrGwlAy$nO3p22zux*Y&9ju z)Q$bt$DJ1A@R|YTNC}I&XEaJUnv}uJQfsl7M0P@8B~bTbLwRy1D53~riC7~AXd^uN zEQT?w)UxRUeWN>4m4qPUI*_;Jj^Y>5(nACKOXF*lG$c0fd$9OX@p0wpEC!fY^zRSu zM`)<$^$1DJA~vR{(cm{x7DLVJ?D&gP?o~RV5{OVQjkt9-Tn9D(1*vr+!2ia{@;5C9!UTI literal 0 HcmV?d00001 diff --git a/packs/ancestries/000032.ldb b/packs/ancestries/000032.ldb new file mode 100644 index 0000000000000000000000000000000000000000..0f9849c4d5754fa21ce8015ec6619ec5de9e6b19 GIT binary patch literal 3258 zcmb`KZEPIH8OLX{d&k{zKF-c19F-f*ULE8kmx~>2Cv`)VGqw|x#3W}YaS~!O-krT0 zZr?mJYkM(92`C7tXeCrYYKq#R7L~MAD*Zs!LaUUfttugqsvuBB+n|(}iWUS^70@1i z)=msav|rlQuFgBVJ1@^X|Nrmt>B1mdc_FSdNgbiPfo&_BQB29t7j9-|p`!Qg{P7hFO>0hfJMB_CAUk;NQ2#Dw6?VR|&O|kM$&sZIn zf{BFS`%(<-M`%a!{>dMrdYwS~x{a8aCW|5l5gdV+@R;qb+P&wn7me(@Yh-9uapWX6I3<#r<4ixCT$yt`lUb}U zhq5*ia(7E!8wmHln1d+#)YTu$jd=k#Xlovufp5buO4@fN%F#LB|@c3n3lck-qLXrccwD&bCt%rPf$@TOu* ztyxf)&uT$U{59Tp3;Yn8Cf8|@ zZr9{No))EYnk7KiXWvQJJP-C_n+#yxOkkVVs0(5UN4|8&CKUw>fzOg-d7JPW zeMAqX;>Z)@&s4G-%7DojZ@mx)^(S^6;JPi5VV|W z&Z*01bxuwEE#7O*soCw`l+Bta<2Rg)Cnwncp&w;S*a+P3Ji0GOjReFdb1u12Phv0_ zPOU)D>Fe@jirGwtWa@LH$unl*2_|{UsXf38Bg(es`-Mt@ddTr0F}G2+Cyu#H*PBIPbCDI zIsPgUnvnVpU3d)EOsejrH+zCPFpV3mdkX2tb`9TGDzn7%p2vjPr0G2$wy9Zc)^-# zy6;w1v&fmVcU_%IRD^N9UvrJ>P&{*ci=J%zeCo`5r`uEd_MD4{LMpyqR5Pm+X#IK8 zW8y(7!P!7O)M(sy@Lv;X^n3=A6f(TBb|*3?2}(CyZ+7)`N*_!|zHrU6sT}U45X;PP zCIQNey_$19Km{%~nkQ1r=Pf5v?FbGfy8O$Z{Kacm09;@H6kM<^QNP<|1X z>$>%Pqy~LdX&VF}41>FumIYCYz1y{f&WjWrU*{*P{6^nd^={ zEjOkkaJG;Rp#4Q?>g7*DbM-PbH)$j0-g`&3uPSbiYOWF#5Esc=dGtS!v$cAOoIm?t z$@!1ZBSY3N_1tXl`XsKJCp1hdM>ca(^W61QHh4AIM!oX#gvZQ_ z^_hS&j0&(ko6L%lVrK-~i}qGIgr@Y{2cd!5G^(;7ZeTpT795{2!Ad-r8Msb4lcct2 zF0OQrZMjtL5e_99rXY$Qg*g4 z@>Nhb_FEr!T8zVM29zTuEb5-oDB);Q1~W^o#U2vb34xVB-HQ$7scoQ$B8VkojTE4b z@Witi#;j7yrgQZ5u1Hl9f{g1x-j+LyUqlNJ_vu29mD*u3|l;zz|NlxMOSU|!L` zH?Rkx!S2^0BrS^An4Cld-#}RmHLtVvFG{&b>3~WgLcKKV`au*br;7Jyp>`y`-Zpe) z^Qg9bjvm!McB!C5)?$=E$~Ns7ii2;V6kM01!@v9EPE=4WX2Xigc9iqxRCyqPd*%+n z{yW2qk2c_T`RX`|tk^3|=4X literal 0 HcmV?d00001 diff --git a/packs/ancestries/CURRENT b/packs/ancestries/CURRENT new file mode 100644 index 0000000..d95f027 --- /dev/null +++ b/packs/ancestries/CURRENT @@ -0,0 +1 @@ +MANIFEST-000031 diff --git a/packs/ancestries/LOCK b/packs/ancestries/LOCK new file mode 100755 index 0000000..e69de29 diff --git a/packs/ancestries/LOG b/packs/ancestries/LOG new file mode 100644 index 0000000..211130e --- /dev/null +++ b/packs/ancestries/LOG @@ -0,0 +1,5 @@ +2025/12/16-18:10:04.194010 7efc368006c0 Recovering log #30 +2025/12/16-18:10:04.194143 7efc368006c0 Level-0 table #32: started +2025/12/16-18:10:04.195439 7efc368006c0 Level-0 table #32: 3258 bytes OK +2025/12/16-18:10:04.198818 7efc368006c0 Delete type=0 #30 +2025/12/16-18:10:04.198914 7efc368006c0 Delete type=3 #28 diff --git a/packs/ancestries/LOG.old b/packs/ancestries/LOG.old new file mode 100644 index 0000000..1149ab3 --- /dev/null +++ b/packs/ancestries/LOG.old @@ -0,0 +1,5 @@ +2025/12/16-18:07:40.012790 7f160a8006c0 Recovering log #26 +2025/12/16-18:07:40.012916 7f160a8006c0 Level-0 table #29: started +2025/12/16-18:07:40.014122 7f160a8006c0 Level-0 table #29: 3258 bytes OK +2025/12/16-18:07:40.016513 7f160a8006c0 Delete type=0 #26 +2025/12/16-18:07:40.016583 7f160a8006c0 Delete type=3 #24 diff --git a/packs/ancestries/MANIFEST-000031 b/packs/ancestries/MANIFEST-000031 new file mode 100644 index 0000000000000000000000000000000000000000..b51cd7c1153ccd13f1b7ff2928e46757ca2c229b GIT binary patch literal 316 zcmaDZQ26F210$nUPHI_dPD+xVQ)NkNd1i5{bAE0?Vo_pAei18!uwrIOYHqP&UTIDa zBR>Neurew_MavS?6O;1uQXKP=Q~isQ4Gatz`5I1_d-1TVQd7 H5kw3CMAlAD literal 0 HcmV?d00001 diff --git a/packs/classes/000015.ldb b/packs/classes/000015.ldb new file mode 100755 index 0000000000000000000000000000000000000000..bd98752696b04575676ed9ef1ab214766eea87b8 GIT binary patch literal 24835 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 literal 0 HcmV?d00001 diff --git a/packs/classes/000032.ldb b/packs/classes/000032.ldb new file mode 100644 index 0000000000000000000000000000000000000000..a2b47a238fcd71f58eef466f823890211a857636 GIT binary patch literal 2093 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 literal 0 HcmV?d00001 diff --git a/packs/classes/000037.ldb b/packs/classes/000037.ldb new file mode 100644 index 0000000000000000000000000000000000000000..2393e63ec7de9134f04ce55810459970335e0f62 GIT binary patch literal 26320 zcmb`w3w%@c{XYIVB?r&xxik$)3|%QFv8^qH(uTH{3P=L2w9-=A0)m2^HmB{Oxs;rw zC8*4*IB`P<0ztFfTR>xn1380*m)ts&Jg@J~J5j^FC7(TFkG=x(3f5{-5i;Q@YSBn=$bQ+cr` zs2Qy>y(_LqBY5((t{IEi`lBliMU82S(b^FWsWC%|wkh!r4L@sDBbw6F5mj1MBd!=- zS}0`TSFvcKy#qgsX>Ey+V#E`{q@qTGN>@y4)eTLFYpop-ePu!`Qkpw7J*EUTtxHKn zf?CXoCy9O3rQ(OHqY-yOX05w&T1>@p zf=!(|K7yz60*}L6+!|Bc;&@U}!y9J!Y(3HzN@$T*=B(vmEvUER4?Ih8^5SYNjC_zSm5hKM)R#7uJkL-Z`Y)azJA zeA|{0OR?9tjAU0wTN8#BRG7y&9L(25$dqmPQc~VTxCSj&ej_2J+vbo;e6OpUV#m2R zbi=oekg{!!WDUNTp>+Z*Wn+Z8yG z3R_*E7Aez=cq|%epTUpgDyL1BtVChXc7-%G7Ewy2N>Vh=?kZ8DSaLBwD_6nE!+P*3 zbJaf<7>XXjg3}FUR^Pf@1<%lM&9vCquvmvvTi!`>V+ziy9!`W6CflXpncB)kUwUy= zDJWGQ|Amr_CSuBt5BYX`p_eYwzKOGy4!s?xCa$RAXd)8Fa_6f^Qu><10%r|y>+u|w_FGCyT5D#iy_;|lh39Ivr|Q8u;_9)*m&8hcoacSM8xKU)7c=GjT_h(?i8 zljpK3p{Sva8_O&vUdCG0UX1M+?MfrA7c*njXyPsTwZ-Rc>lwwCxs)g_V^5Ud+h8aLE>;L(K#L}f zP!i`;jjJ`;_^c5;OWU)VRDR7mHj&cWO@y1(5q1Ag#E&Z%%yju%R$!yV7EG;B3SwF? z(W)sy6;VVhOibz(Tarpi-ZX1a*9yIkS=_8>WX5cgZ6v~B1Vp8(H+y1<;*W)+v2jzx zs_tl~FG1Lr%(#e+uH_BdwN+iAXpjk6qA^EQtOFK4 z4MG;H6U&#UzlM*Pmp(m#8o>;hTh0$WZK=S5mI z?L2O_np`XeW~Xt{M7+l1{$k+tYd&G(h>3_p@n{$e9lNU)@%G|NFZFt~wl=LbE>9F( zc*aEtn~bPj>}kbWTd2j$vIha6gt;Pme!bj9?u1vb$PAC zkFkfNLA^~cw-fm;k<>{pE|By{6S&rBcwmdDC&EqAFZlr0bXN!oB(9a8PUz*sYA&)9 z1#hG+8bYecY!nUr%yy=zAkrdvbm(2qze-}8ng}stnG!Liwi|hwR`Ilh*?qS8vV~mS z*eG-H)(-h}i6mq5Wll?K*EYx^ex$A2W(g^te$ZA53@QNv(;PA)V<8YAuPj zHpAwZnQB*9>5BGfG5IDr=L^Zka|0Dbs}IQ8Y7f$FL^I@bGnl*ct7PVJD>9T3-ZDon znoQc7WyMIux^-N2PvacfK1Yj%^?0A3o7dXfcSGNQEnPF@cQS~fmiaA%&2oLSOcuEx z${`uz9Bq}ywvW$>2yfNOE;X!&bT#IiO9p4j$7addRo!|>sY+_TO=NJod~CXGorT30 zIdGZp7i91p8Mmx+;4~RG%ek@M9Dd7^vt-|)w7UJIWZ_q**?!EcONe6I#^+3t zxw@pG|0kbtyi)#*G*~X8bD}TV*Riz&3BkR0$qRRi3IK|)Ef3>AZsXs%^t<#M*T=qmno6sd#=IHW6cBY<& z5T(D`_ADnhCt{Ha>v(HZC=qV)9TDt{011>Pd8=%b1qBaPBLuiIuFO7*q7Q9ch|^ZZ z<=XgIBoWt`3#>>dddRj#mcF(`qHysajn5?v|^We5#&R9x7=$JqY13U zzw#U+uk&dur|LoG{+tyV@?`OV*NPaV8Tke293rnxl>+Sx?968NU7XQfo0SENw9&0h;{DgIrZCf&| zILDeXX@`8l>F)PP)YfXX$CL=pK~-Jg&m^{!DJmW9cscff#9>9UPtgoLrC4dxG~0CB z{37{bJJBZWl0+=~7CCvk?WZ=|uTICc@ubOO3zGbfQs$6o0Lt>8a!Op44@mOkf@7?R zgQ@;sl2$n&hzRpu#|6w8S-%nj|0R(_OwQ|)&tX~einQ}KqUOhC^+LR{QDccQA=@gp zM`7YS8&O|^5g zHP!vTDphAOViI?T9<$0N66@ZESmyBu~E4^!JQ{_2Xq8^DyYZi03(A&p`-TIHS zflpdu(N3*s1yfNSMeGa$F6a?p>B#o+fg6eIOeDxC0+fHdl?3J69g2!`-=nldgBuqy zgXha5w{7Rk*#!SF^!~=o#qZu)3JUSl(@&%jx1B^G&L5!=3xP)x$4P|k=*;gT5eS-E zDEPxPVxiP{bjA>kh^rw4D}WBIO*1jSNg_{#*uX-Jg(*{DUB7KX z^ob{Fj#XO_yjzA6yvZ&o5CRUN2LLgM$3t2h(`Pq{gTXkxVJt+N^VF7*#$0%GMkH=B z2LO%EZ=nrgTyl#2Pi74*F|{W&L>sa#Z853ZJ-Ol(=HT#Jf*SLN9Zv&ww8|(YwJg6{(Wq zbsf7h=pCCqZ5X4OP+jqd*8X}Lg;EGHWi-N2tnQmkEb*xO#bT_YKq%VUsWjnGn`3i_ z@b)kzGeNiZGtJn!CAp4xBl}4<nOvHWNR)dhBKr?`@fj6G8KtVg~9WWdY41?DZUW z&!nx*so<;7yyc}>p2qDtdjEP3C{FsQ-{%5J2u34kGEa)Fp+u1VhYKkTM{Lf^+4lv*>077sg`6Wz>QW zZ;CC-Cl0;-YUCyLjMH4tRRtCyFw!i=wbZWZ;*?UsX2HNy5y} zEJ}|*CSubj*}L&rGs08ZU;FEFJG_+XMHIb_pQs>4Yms(9`91r|hI<{q2jcxb+4A|Q z+##HgC7l!Ddh^9NWCAtY>QlM^@Wxt#`o6o!4gHS0`^3P_7Qe%aOA?9tN#69@!_2zt zn488J`jfP*6Q`A5KaD5;%AVjKQ8@L4J1YXq2VFU2qcSwP>sSe*@xm4Iy568_!p!An$VCYC>My zm%L(%KHtQ=A7K??OJ)oC5;eNsPXT ziw5wvP_pPQK@4i$sy`Di)NX1$wz~w<;(oxQ#lAQze}~|LpoDeNDqkTS_$g`1`n8bX zA`oxReM06u_naB`3P8R9A+7Rs!Q%CJ9u|}^gf0^nFT7W<)JNq7!2U5M1%?AVg&ix` z{8}KT-Sms^JH(f{6R}j`GK*M* zP3n2XpilY3@Kf->d`e*v2PH$EE|VlA8_$!%YE!f6|D7`~uQml3;#b23n&R)GQc2X% z-4#oo6)OLkR`(t$QSYwowtg~&C~{H8r0Y0@rQRk|latIKMfqn4dH0e^&o}|P%Zw4- zWjfVeEF}u?EMymX7iUkYE^;zs;2amODHUDhvoe2Jb&=NyRs?Z1G!V9$qRYTbmY7m= z440ADN#T*d_nqTCQF2+i&k|9YGC7^=3t1vTW^SGU!DXLCH^ym}hS-i-_LSmcdsv*& zb+Y0jSYn#IF`Zoo!R5EMzw+{B9EaRhoh|H0>u;+5t9Zlj@;QFRi_GDdNs{TDbR{w5 zEdsNeeRn3#8BhiU1!yExi%=XFS(K=4tIc9mmK}4=0{(=WIrW&Opp~z%)p4>j(_LAr zCgRaaMxqsTI0#)~Iz^L!6!CG>xnMNW&yhy?kJ7k8t_l?4V;c!pPaxuKumpJ{KW>tB zenM~Ul;04>6|t|{5tQqq-9_gUdsCtX=w)2N%t(}qoh@o;vJnkK@@ySykaFbVJ!-p0OHG;7ZEfBs(SBC?z!sJh|{!MANO3lRty_%BK>~G;YgJWWDPzD;#gu z|5mDta>#8vS5R3duC&Kg_RpSG0tZwkKW?|^+R`htXU9~$w%$7ZDat{rF0|mq28t=q z(n4AoS=FoLtg%jioddrrnv`abK`e@fJ$rbI)@EBFSPH%Jt(?bUwRWh%XwSG!pVJTcfQ#Hb#h7I}SCV9aS{RrL+B?Ru;FlqD9;k4PL?*oJ+! z--uPSus+x`vVoaF?tsRxNY4_SBaj+IEeUtbfcz@3sThzA6qF zo9%umFKyB%pDl8{g~&k`%hzShSeUlp3_EfZBp2r&GOBWMf?HHbuyUTj9)PYF)m!&1 zm#deY=I9&PNrLrdr|o>zQmX9#+1?j(+w_QTbRZE2de5|UXjy|cc3WUMt6k;-V+`RF zO8*GI<-3Z>$`=yXRoA4|&yaKMV+w1f^W{Dl3CnLgEuNXS`&?E?Jx0`7Dv=^*J~x<< zM{c!=6DUB&Q)fTBvji02;gcx9EhkZcxg!*yG1{Kc5cX&hA>+I8e^702*J3}6{~MWk zM}L~a|E-Bgga-8J5E#IQf!2s30#snVlhzZE^|2d=RT9tu0BK+mXgHK0=<(sQff3Qv zP9+7maBOHPp#<>WP)fmmfs94#2J~oQ&hN60$ zx$WrO9vqXdFPznH6cd7Axf&FjBu>o9%pmn9@MBaIXnkSmRou1TC@iqQx8iASI@_+` zyIEU^yW6Lo$`6>0q=8wSS3lEl7*r^R_!$ES(zUklWd~RPpV`6g>?)jJR5A?aSFR@W z%=$sWX_4VV@B<{fS#?h0&#U(}QijlYGD9d{0FD}oB$e6GV7ul+`YRiv0+c3jRo!ad zYej1q5CoG8m;|As5mE-FV-Pq)2nasZC8oaIL_l*>T%ouLgbtSv^`^OrR)3ZSgyAhp z7>*UapO!2R{5e%t0AyeLtKt#FUJLNev=sOu)ilRc6tooP4-~p9#V3XLb?G97uG?Lt zd7UC6vYgG@LniAmm0T);&g#%wJFzywFtCJ}S^PO0ZX#X1nHAl*#Hqqgibd?J%Gvpn zZ|^A3h_^^76-O!lOhNJ;o0uI)Z>T0v9Tzl#Ur%G_j>|>5Vk`F&4y!YBoRv!G-roDN z@jDadyU%14X)e+EH)+&M(HmwwV3U_k02iS!RxuOHF{UaIG=_L^QSaBpkfck7fhs;b zmA&zMMCmE9pjRo216QBLu*~h&srXcL`%beMO5P(PP3HZ7*;B(*?y^9cKMy=)VPTrd zVB+e5Yo;l)RRe!Hg2VHi;v7Awt^aRBicBDLX{J<<5)96jyNyI7_nv!5`J)!c4)BW| zge)3JGWzzH+#gllTzDl)R47%se?CNweyLDSk19&HmiIJYLC!4OKvZ=+L?>oS;C^!O zwuQe;+whuW7b#!1i}B2$FPp)`dR~@SvHTMoESMwZ)5+!g>^%JXmRDZJ1mLV4u#FP zQ(Z?<{pSa^liG~5A5z4B{;lNTaHcXMi052S$-!Ic&O0bM_-8t4b=TU5*uiMw|HuyH zFVjz?2m40o!6p0&ig*)1|2GuzC&%7wdqPIKoKN1H#93+ml`HV4kow_pBPaH%Ps zr?*Klpi%=dbnAUzk|1-oe7b;b6AHfmEn1v{F za9JO5#Fw*_Uwsj%!orJ)`ypS0iT)cXM`$2&IS+}2(g8sHtwi(#8TS1X7`JM+%t=L` z63=78>V;&@E#`wcWXUZA@;aVWxm5H#Ev#-LYi=a;0sj0(qST}+KS8DX1;%q=m>{${ ze=HYS7e1KDCFb2n#` zBF}pQ?*>13k@ACchWLR`nnC#ihq|ook34%;_HbFY@$&Q^W(3Mm9aatXRagsrsKX9y zk#FZM-P-nl^D9Upzmekj>v=o%6hz|uIiyGaoR^FEftLcmIBng!h1+>Dw5WoCxX4WWSLMs#zXyz1gX-q>Y#S1{D zSiPDur=j0r4IP_I-!90m2}IwJ zzM9XncSUu$3<9N0?K2$ldM=dbZ`yt)@y_2=UoP6#3+@}_m}L7>a;ZukK&zJ3E=)~x zA0olRLx9>&ujDM0T#Z<=U=CRyrdul&U$A1pXHiPnNupPoSR!RwpefJdpiT5C7B$3< zXBQR3m4dLkN+}Cg6z=yZX-tzRQ{?RjyvjmMT8b35T~VByXc!W7zws_vu z#qX2WtcpxK>Z`G67gIq>aMztLODG=LZV}aDdAqFrGwUI{W0vL2IGZi^TdU@awBnNI zS@>T|?Ng)buNQMJ$SlfWMbMzAY)=P@Zg$u`Wy%C)rfufwA~0WU_f}GL;OB#X7DsNi ze-@7`>6c7{2}U1R(i;-(kgN?VB~v7A`))f<8=m#UIJ!YOtNQiF`91fs#D>BV`ujs&O|2Z$PFs6-5cOtdX;a`^$O zu@B?scOc-m`P&p!kHRU9{VxdCK0LTzP;zT&w>EZj*Z@{^R=!(1;j^Rs*lkaBzAC9-o9X>GiLDQ~wpULd8*ULgJ)=9M)o z8LBWy`zOVy%F?KE1#y^mf*PP{x7I(e6oGR#s;V#rQQ{u6)|!c$ic)K1{+E63=SlEQ)`v4PR>Jt+|UC zsI4#suj96p%A=z^S8*HvHpWzGUx@*V>;`}S; z8kWS96!h*-cdnzL_x1F?ts{!~C;ul!{QF!WD^I|@%zz!W$sy3o!3M(o%XR6c;%V~GI9PI(&>3Z7`TCh5pu?p z0?|E}37%&J(3YTV6YCEMMMXrUZbx?_>lvZ(_og(iiG#lr9{Zh;J)4>{8=|f5Db#y7 zYwBib;Nv;lbsX^w1P+ZAAvkxT25P7wUmY2IP&oFW00i9L9@2ap$>1IVHw7A`5vQV` zZwnc`UpRKZ;0nO8(Fr&-&$pEf-Y4K@Z%(#Gqmj0IgvuAw>K+F$z4bKtOwLxD@x{8Y zJ1k}Guf1;jBHdEbE?*~5(kzwI+(+y9is7uu*if@^-DySosIRVGatC48FssP4tffM6 zlMYe|E)IiXLYuKlD6XN!yCkGjS|au@aC&&TO`u68sHlAxbEw@pt23F?fh0p+^3(j@ zWlDQA8id{mEJZS5C|L!F*L$N2S&k~Gd#7ytklbK#d_a6lJ|N7``f^3^k%&_jP=I0X z?t7GI?kMxBNS(V=Zzwl7nAS8oBF(|o1%pOJXrt~{vtGiUweLZ)zQD1s-VpqLt|gJ= zm&whbJZF{VG4~w!QvT7HY=n`n7+!b(-JGF?q@Pw#BK-R<#B9oZZA%5QtT?Z5&%(5u zYa9>MHwl*63ENB|8xksBDXcAa6MgM9+4xAP+AIFU*Dn6Vf4S`=q3*Le)VWCQElRMw zlByr2lgjzps{0CXJj&DB^!VA#ILX8b5~o&`iC6%v?YZ}t!yh%vL;#OXE`paOu69~BsS?j_3&~a6}SnKa7Cm< zcwl1as<3-bU+KP@Qk->KPQ!&s zlZ_!IYRGnNE}3B^bcirh5rryJv|fFR8#^Va4&go=xDy!9ltr;QhTPOC^au))dJ8FO z6xtJYQeN!Q&<1tE7Erdy&Z0`7FDpISEy3J4huW>F zUmaIy*l(S;)oz^Xx4x73ndhNR4tX+g*_-G^QlLpfr8a#ND$Lg^HDA}O0CnJ>zIoJg zjV~qT^G<+y+d$8MLHNIsS$TsH0AROnAZxa!?AD^Bk;$os$`NqTwXt44!;ViDH@s}7J>?N` zpFK@AZLkRAz@&6*vE!v;qPiof0r8yykr}oFN=(&eqV4Da4;O)C+g?p8d=vH9s5I8v_T(k-ZE1)5@x`-!!Yrf_zr|JgkE*uTIM@-V0J&V4&a6kKYGYoS>yoNft zk;F}fSlutuZ0G(Bb)JL+8<^M&gfzcacBB?~4GpFqF?s zRoW27ht&hwEv3ZZLoP_Zw4q0yDm6;dkkmVvCT&cUXp>AL*6tr~X;ce$lR0?@fqpx= zEO+|rUyI^A1Ooq$s2Td0c4->HF_F+??|+ zZ0;LIk)mDii^RMC?*)Lp(BFW2VH{tA(uE0tz%rQ!>}DBsu6)Gm%n+UV(h{4r^TC{w z^g&6QAW7vHkU^{Dvr2GLFY=9%%2$#>rxbHal~17RvxZ5Ne<8|oG3Su%LfH|DXK?3e zSVmT6dR~_&OqHQQ?!U_{R2lAn+Q5Hi`tke?SxOmb~TRSAcRk9>hFUy}hZAMk2Weo~<9U%p8M z2u9-?XGfz8Bo^E940)!A%7#|>5KKs#1hc9_?IJL8$getzVQ^CXv{wnMH^@(*r8dz0 z-gW#&TW^Lflu4?cRtIY!3kTKY9+~l-Il9bNKPbX@aCK(+)KT>lMgiXX_Vf~W&A|-E zj~iicI7W=TnT4auXD|?lOOH< z*?M%=cNvMh)E>~9$p3=`Iw!$2&OvI19#0(sFcO1p!@qpRJ0;RKx2o+B@;9=bBjl;1k|v=Ud*g0 zh~dqc)&j_sc@{m!Ub6g~(AW%95iO6msIAP}6gD33J<&M8SY5-uftla%4P<^$d{Va| zGd(YVS`+OFIMF$LrPXu}yEjbxhH?0!?@>B0{9dJV6XM>db|lg|HO4$anx_6N)qHpn zbk2*2wGp-7*thzI6&fIWRP@I{8h|4prC(ArayRremj81rqnF(I}0*!+BAlTlF#Lg{%uMj>k>NQ!E z84!cfN`>jPQ#Zd-z2SbUKCsDgg>Wyzo}flD4VOxd!$zFFxYT6P)If$JvZ3^yL&ZjR zwrj}Yw!MgH7OAmd^E1PEbaV9(9vzkU`p*9r4Z=pSri5WOv--5*&d3He*cL?z#(jh; zq7LHC%gjCQRp{H!Zi933xBEHv2_>R->+P^*dRf`s<=!v{$Pm6+vt9$Eg(--;S5nZY zD)o?v{daO|tG0W$<9(F7(W<)SFj=?B@mDIHTGNVWqYVNIcPOqjYq7p3r@=@J-iSCw zEl(PJ?tSR}=utzR%yo9qBLrQ?gX|~PSs^{#!j2J}di3y~K61mIj_p8_+tH)u?-r@d z*nb_0SpLks{}OfoMth3b@^||Vvk;FVX)3htpbun`D1SyN6;WwNRTL&-Af&havj)~t zt7P-xxqJRd*1zEBC*=$KN#0?7f|eM#%KaDA4i9f~FSS#ZXHElq2-SFm!kD^-#2xt- zEaBUs^72nsen`;ow84v25P%~nXn?ox+fM?cS;w;k)=2{SwI_VL*b&n-_w_5TW;leG zvX3lE^(ULFrp$O@6!o?F*IOXLZ{aVbIe?Y<>^(O#<$jiU6GupK?aRbITLn?q((ACPcX%Ldjz#d17+KABGUT=YK2Oj)Z+=pA)hc2-!4f8_)}F3xo;?8B{aXSDW^~Sihb4u!Uz5{4cimG0669;%R+9D8&SLVJqQ~rwR`n*mbkk83T*10 zNY>|g0kfwS`vIk0i0H579C}vBTvPv?FlPia9v3C#GepLFycJv&sR>QKK@6G;AnBnXpzj%yRHIu$R1?D*>E4AYtUmG%Vm}Eum4XdXzHYW3r+By z#s^OmaVtMf9Uj|8c>k?E-QaF-3sEEh{-OX zO6OQ{-7DzBZKX|z&X3dO-=^F81rqK5pKU$r@GTnFxAT_*T#M+@H!Bv`@TU>4oLkXTJDGq4$uDxG>Xl*S#G+ExnuE#fQ~=Hx4(j3-(Q*npvUX6Es~f?z!kTIxd_ zI{!Mw3OF8VTccNLK|YH_d0D=U7b8l^S$)r1P;kG3vuJVKa{?*eetkCV;Zz)p+9U}+ zV&-Fc9WTEm0BHqn=L)sA5&5A^REn1f15vhOPcBm)wUkY$e@7tmm`>A{)kqt(3x8Dh zU|B`sm^>T&oAcV*Cb7?;c$1UH<4ySlaRu#Ey;VHW?b$0apE8YhxKC*lwZMMqg5W^A zi)$oHP_-@Nb6(@^aCXB6Y5OwWYW9A>-Qup7&SaXbJ?Pa64wR6PZH&O!Nld!ieJ!EF zxD^b?P;85A&dPMG+L(JC-%Enb0a6@L8Peu#w=Hzb+bhZz*KZL$pUCs|$~uO4i=HQ` zhUC1VhU9%uNsp8|HrBU`j*a}k zNe^F{^J@Vr0cyfwN_+rFtL?zmHjiK_IV;f2M6Q#48l~(NwQr5;{Z-0t_LxZRX7W26 z(frfu#(VCuvJV0n|4%BrbZy$6t>nR=V{@ZhOw_8`#OuGv|4a4?ym)0Vsq7fF-JRbr z*L;4>b}~3#9Jy^9e*&(&U{dW(pOgVtzL9mJ7Q5vnT-iM0PpzgZE1D8gYHa3r*VRo? z^@pWc)YYp~pc0Vf2Lw@?ScW>xX!je{&9OMp7L)=Sj9mn7RM<<89;VQ&CE5e8vI#&> z)pM!I0rdtHk%4)J@F8U0iO7=vdqj4n=j@cG zHP7(?XL?`pl#$p+4O2KJXk)`qP0Q^`I27Tv@Z92&vb$G-K6?lDh^d=0nO>ZMMHB%d z=1k)s9fGG%%sdsI>h51$abqewhd{ur*^hw56#9fnCJ3dbpOQC6nD$`gN14c zFIrbXR=Z=0@!M+X&6$U^){j>lcFAa<(TVAfsoC~3I^&* zMGCXJfC{}SM4|Mek*aH)hZt2($Xl>9Ehk0m9TlfEz_Igb-53oIdaJ`pT3MMi@Awbt zQfCscWap~&(Aq}J#h#)N>CNCD$lS@{>Z9Gwzm4;>b}%pYp0hUZV5F|l;_*gSFkEwo zhO`<|r29#beS0Rw*-$BI=%-A|AMe|c4PH*|wB9_;YC<|DSEOd)yz{MQo1QdrCgPEi zimfSiv}8#EGxkBuxWPCVPvyZ+Ek!CDf{^;Z?aKLlYGD zO6ECq3THlFSm=L^HtCryUxiDuC;9M^(7QS~jY-{>Ok<``Cl%e(nP~u3t<*I`!CMzf zV|%I?GG(xeN!=Q&m@l4A`~@P&Y{pX4raj$zGvrO}ethX$WJfxsmo8{~hqs!304bB5 z0t5tRfvnqy_cEOD*m`)@y=I5Hde}8IaN_bi(~4${s=nwnNTTJnPmsRrQ_O%e5WiN4 z67!an*UQY@{Iw#)qgl3N>3i0bmmYLnIWVemc?QSU2$fP)vQYAWb*Sb$X_fGHAms; zo-1D}@qd7QDV)DtG8N5b5)@5KOU$_fP4p<*%XJDK{xj6M8i~9sotjwj15~WGk?b0jA7kBWy!gkc_^r4EJ--<)WpeiWxd#$E{m67yZ7~8`|`JEE0kjnh? zrGYDt_qSnC2h}1?f?n&Kg4=T+$N^?9|A(|NT)@fy!lLC-Z{?G`RBKjuJVZ){7*O(KYVAT zS1tzwp8tCN`|19hoFTaYN^A&vY&Duk3qSON>rl#O5@Y^X%nNixO+n)x##%_>dFEtG zP`~zmsvW;=B{h4Kj$KO}avm-8up3eI&+&&Us8s+IV-|HCAV;e=ap;f3fQgUO{kIdZ z{J536YBvZZ;_S0pA?9I|c+wf4MUOjw#x)NKLf?>yArQ;&*-eGm1pv`xO2qO59L_qb zRW1CDrW766$7FqBU-$r1`2kzT*S{iQtdIO_r@GQ=j^C31jAq5EV&)0x0yoT(=LqsF zAs5(tMPFWV$}LiYzM7ggrMH}xd>|G1qO6G17qaPs zZGog^?Cs@WVU|`572c@wRDwUh=Feo%kVbCX4C%NnW5(V>&tpDNfg4YZN8HZ{P4B7D$y6i0^Kmcxz>SVKYksi+6j2kluKF=SAU!W_VdNIX>VqNWbK z4eAk4m`gZ7J!UJ1AS$)rooWF}HKTnCH8`^sV)XWj%gU*r*=U`_MI~10>fd@80F8kBMK$r$#1l<<<5>K z%nmm_?ANJYP0hHwGiShzJ08E@hZK~Q)Me< z)521u?Q6aNK00j}WsmejQN8qJBuz~qsbO|zBerSAFeUHVy)|46&W<}L)jTkcNli-1 zo4Y;NZ?LOGI$ns~low*Ib9WV+N_q@LfkvCcpIE^$+I4q(Hto*vheAomtW@W1|E(Cs zfx~M2X2<%>*I*DZ*p9#j%+5;|UdU`XQg$H~xfiEd7tx_3%(I7Qq2D^L)S!{EOKsKR zheu0yWJsmOL=8HsCNQ2cQ>9py>^`(!bQzfG814u69?@r2olQ;vG208b)*q?sc;T-kh7086K~bFs@3d$W1sNvDM%@NCRt;w1*yrKrt6x?n15C8IsPZ0 zV})y8WyYA}n%D(aWa5%yTLD!G>68rh!a}Y0Ql^76_MWlF!pwM=EgOyIyV1;scQ9e_ z&YH)2OGun~#eb`QJ~0LpX%D{Z*xFy*_z25fW*_R1yc~L2B0Vp-a5wXWb=2}xNXPMk za#eqk7xCXZSYtPTe6U8Cx-A|NoR`9F;rz3}fzb8l?EkAY#+4c*r}^yU1o`_1#%g?v z)#B0-tHrGJ6FTQ69VZ;eXUTo-y56?kbXwdciUE{L7%6)i*yI z6HR(ByRY5$rr7_XZI@_8UJ9%7{ZyBYB$=y7XVs6V(ZV4Lq9Y9Hqv2;|UULCZ(oUDJJf4}{5s&;JC(=UR zfjdN6!t1RLKe2WO$hC8<6*4P!w10_66Eri~If>7$8k#4Bkz;pomSB{Pk{Gag?~Bk% z)cD~X+Cmm%E9Kj{{5~!l$byd7RODa6W34tHo}5)7IJXI`&9h!WQ=B3d_5%-hX5#T! z$%;`T=xMu}pKmV!RtuIp_i*TuaGX6bi-QY!+*!82^JCpsQ=l3*-r|qTWj1$-?8(Rhwi#1Vk&5>0 zS;zaHw>WODpD)%efg>Ow=W_B30_nN7RV1pjN?dSy62Kz!9P0RcRLA4|K* z+^^78Hm5}%SXS8UUzfJ&%=%Kf1n zhS?g{t7wn^hi;0xHb^2J*r&Ild2EYBT5{0Hs0FX8N+W8GpP9CDT-I0V>sm!x zua|pd`8T4w$DNhq;w>%Oq|=w<{1m`Q+>vTwX;C9aho&K}8}esp7Tb4DTKOytE}b8! ze@M(D^UKwnoTW~P8WTm^N_o%DQ3ua(7|S-J<<0-2EGw5YLC%{=FVUn-d3E~Yy>eH2 z7IAKVp?Yh^;ML;DZQIo+sNmeR{>(p7==<17(0AiW(D%X-6&wMiMU4SoVMkI|@|f?Q z)R1!b{xI?lWHueWGzEPz0T1m8baV|oDs(Ve3i$$znhxWXrfpW2hkDD7w~?KSb`$7T zIsxOJplgrF(j%>73afPiHW(eT1g1o^wGI3CD3+#{i%#YyRB`zp{wH@22f%MkYWSuk z?w-KkMu)OtKm%jt@T&Y(?mF7IaMIZ4VIAC5znQtm3w=dLt4|WbM|y;D$m3$mDbt|M z3ECslIEBMd#oZT44}T99zWaNy@bd4qX&mAj(W>%WWf8DOhY5fiV9;nF7JoZ?Go1F+ zgE0lV^-1ElQjKVb5xDJI`4$R%>q!9LP2gJx$V2ClD3G38D@*9`2D8a5<9V!dd=ZtLyGW*UdC2T$9tJ04NRqAENWr!IV`Z6PMN!AZXBfF{YJ6MPiG4Kd2b;sF?*%EL>?dlVxd-j@=_t3l?(`-@DT`6W512r08ry z5DF|;=6Pr#(&ZeQt=PxKFxHBr2Hp?7?|G8!TU&gB<2Dhg+Qz(-P3w|?qXlks==YVA zLZ-vo9>u)2{wC7eKW4Xnss`VmP1@IXtX*>OSL;H+f-7!im>r~@`Q==S^M}nJHPjpq zPS|&*e?FxGV+%uW4<;F0i(wEL5`#p?R8gm_CAW$W@&qfz)Pj8I2dcWtOKS#xvnc6R z`Z_Fzm-!2uU)T)s%58BkM(vX#<~<*h2PMa+6c4XYV?MpaM3@xSnFKWj>nLY1;VFiU zRoNt?R4Krw5n^lZ&Ke6TQ$!ymdoBU6}ryP%#?kZKyxj=M$xFu(-j?29gHHUqk; z%4HT?8_~q46uz_PFnMID<9)#4_fr^^O%BwSZB)!baEKrE1CJHnS!_<@m@_+7FLwWo zJ&W>}{v!oIzzA_*KCt=ipvWlIZfEO7vzu1v(0Z~diJ&xP-yt$%Gm8ek^4b@O`$0;l z-AiUe_4`JSv13KxpaVZf3UMETQ6|1%6T4lc!|wJ(7~eM5aWkn{cr)2>2{9ZAtLZnA zdtmU2uU(6|1BT%*9d@CNv6^~4^9bb+%y#A-^tMxrk^f8*&b!rWT2$7CEqYt;pgmAF$u1P*PT*Y03$`%ds|+R(qtto$c_*sLs1N+(HqGsVyt1VCl`uV4=uc9GDrW#463 zuG(V%j?r|-G4wabF)vF8D~x2N3EwrD?rqlw+n+>2>JN(9{t(J8IMJBzu==2AZ37wa zyy{P)qtS_Hgl$d0Z0`5i6}ZG9`DKx`$sdStpu;~xb@PD0d6k4V-nmdzh9R@2K2!K( zRL^?1+hI|L#y^mqjn=Ogz)hp3)ei6fDjnf6CsWeSl}Af`oBJ1A^q{mHe)v%1UeSv4 zVnpTFr1E_tyFGue=%V%$xQpaNLf|(dKas=bsJ>V%%VK`U!D2h`7>NvCB5t`v1rKhg>Z>J#KNgSuShP1_Mrx$ByhgwueR_##yF?_bd>&@7 zOT^8kx5F3$6pc~LArUuk11(%AbzrhF-QTbzEG~Xx_4Da+UwW1(+TI0`T*m}nn(H@OJyNJju(`g$cb#I~BO)0}vgmB?$pm&?o**kjo!gL`0 zuCL7Ycsjp)8bng`(3k`I&4Hi%MOLKTScJKNpDEI26y}ynj>*4XbY`U!AAgSsl_SFc zh|H2-wOP&C|F+T6o*1`Thk`o z;SEeZx4_U2X~O!K9Q&4?E3*4ADZvEGa)l`BaL(nd66`6BMLx>$+c5(Zb<908m<mbj}%r$$jOjFL(4)?q6kE9DpP&)$& zsaal;0p1F`%}r>awXM!zitT1F_A&w&QQGBk99#%mY~Org?;NC-_3M{piSr9B!SeiL z>CBZ`|G;SM=?_ArUicfv`7TG>Ad*FoGBv8%7r{(*R`BknYp+3GS$E^UVr%);h(npL zH=Ze4&GAO^*8*JCNoT(3Ujk*`h7QzNc(wdtiiq*Aav);1=Xu>B7iE@jW%Y-5FM#`m z#sAlUD=BVgvn|nt++^LKPeRwePU>^Tob1fqpAyBn3sV1>3jAL%Jf-OJX#>WH70k4~ zd^!h~E8WFmL_*$&c4e~SEA$L@iX*q$&J!^7pPYsFU6zYBc>Fi||E~x1U_9=&q5F^U zJs0;$L-%rgKL>YA%r);QHWPttoBi?!#KX4fF#pAh%>VC4u%#%Fz`ykfMNnbX+3_pi zeNQu2(MiqbxMtFM-5(}ilQxr#8Fiu+rjPK9^-&iYV<{R-$gR9feB3p7dSwEj#mc>TFgjiHZk;Ip{bm)tv}l#pNWS*~kWzF;R~*rLk`+018I-(SmI<*LL7 zxt{+~!Dlx!UpffSdYJ#=RX)C-`Ms0L{uy3}Ou-$3eif~jiKJB#myNQnbbHLnAKSpF|sqLUO;p6 LA}r3B2oVDSV(3+3 literal 0 HcmV?d00001 diff --git a/packs/perks/CURRENT b/packs/perks/CURRENT new file mode 100644 index 0000000..32108be --- /dev/null +++ b/packs/perks/CURRENT @@ -0,0 +1 @@ +MANIFEST-000016 diff --git a/packs/perks/LOCK b/packs/perks/LOCK new file mode 100755 index 0000000..e69de29 diff --git a/packs/perks/LOG b/packs/perks/LOG new file mode 100644 index 0000000..fad62b4 --- /dev/null +++ b/packs/perks/LOG @@ -0,0 +1,3 @@ +2025/12/16-18:10:04.216777 7efc37c006c0 Recovering log #15 +2025/12/16-18:10:04.220349 7efc37c006c0 Delete type=0 #15 +2025/12/16-18:10:04.220487 7efc37c006c0 Delete type=3 #14 diff --git a/packs/perks/LOG.old b/packs/perks/LOG.old new file mode 100644 index 0000000..8e88dc2 --- /dev/null +++ b/packs/perks/LOG.old @@ -0,0 +1,3 @@ +2025/12/16-18:07:40.030894 7f16094006c0 Recovering log #13 +2025/12/16-18:07:40.033308 7f16094006c0 Delete type=0 #13 +2025/12/16-18:07:40.033390 7f16094006c0 Delete type=3 #12 diff --git a/packs/perks/MANIFEST-000016 b/packs/perks/MANIFEST-000016 new file mode 100644 index 0000000000000000000000000000000000000000..4552a69d5984fab0c2faf95187c6074880fab6b0 GIT binary patch literal 50 zcmWIhx#Ncn10$nUPHI_dPD+xVQ)NkNd1i5{bAE0?Vo_pAevwAlqxT#Pj7)-@49r3- F3;^J;5H$b* literal 0 HcmV?d00001 diff --git a/packs/spells/CURRENT b/packs/spells/CURRENT new file mode 100644 index 0000000..32108be --- /dev/null +++ b/packs/spells/CURRENT @@ -0,0 +1 @@ +MANIFEST-000016 diff --git a/packs/spells/LOCK b/packs/spells/LOCK new file mode 100755 index 0000000..e69de29 diff --git a/packs/spells/LOG b/packs/spells/LOG new file mode 100644 index 0000000..37de3c3 --- /dev/null +++ b/packs/spells/LOG @@ -0,0 +1,3 @@ +2025/12/16-18:10:04.211149 7efc372006c0 Recovering log #15 +2025/12/16-18:10:04.214021 7efc372006c0 Delete type=0 #15 +2025/12/16-18:10:04.214179 7efc372006c0 Delete type=3 #14 diff --git a/packs/spells/LOG.old b/packs/spells/LOG.old new file mode 100644 index 0000000..32d08a8 --- /dev/null +++ b/packs/spells/LOG.old @@ -0,0 +1,3 @@ +2025/12/16-18:07:40.026292 7f160b2006c0 Recovering log #13 +2025/12/16-18:07:40.028734 7f160b2006c0 Delete type=0 #13 +2025/12/16-18:07:40.028819 7f160b2006c0 Delete type=3 #12 diff --git a/packs/spells/MANIFEST-000016 b/packs/spells/MANIFEST-000016 new file mode 100644 index 0000000000000000000000000000000000000000..4552a69d5984fab0c2faf95187c6074880fab6b0 GIT binary patch literal 50 zcmWIhx#Ncn10$nUPHI_dPD+xVQ)NkNd1i5{bAE0?Vo_pAevwAlqxT#Pj7)-@49r3- F3;^J;5H$b* literal 0 HcmV?d00001 diff --git a/styles/scss/dialogs/_level-up-dialog.scss b/styles/scss/dialogs/_level-up-dialog.scss new file mode 100644 index 0000000..7cf5553 --- /dev/null +++ b/styles/scss/dialogs/_level-up-dialog.scss @@ -0,0 +1,241 @@ +// Vagabond RPG - Level Up Dialog Styles +// ====================================== +// Styles for the level-up dialog that shows gained features and choices. + +.vagabond.level-up-dialog { + &.themed { + background-color: var(--color-bg-primary); + color: var(--color-text-primary); + } + + .level-up-content { + @include flex-column; + gap: $spacing-4; + padding: $spacing-4; + background-color: var(--color-bg-primary); + color: var(--color-text-primary); + } + + // Header section + .level-up-header { + text-align: center; + padding-bottom: $spacing-3; + border-bottom: 2px solid var(--color-accent-primary); + + h2 { + margin: 0 0 $spacing-2 0; + font-family: $font-family-header; + font-size: $font-size-2xl; + font-weight: $font-weight-bold; + color: var(--color-accent-primary); + } + + .level-announcement { + margin: 0; + font-size: $font-size-lg; + color: var(--color-text-primary); + + strong { + color: var(--color-accent-primary); + } + } + } + + // Features gained section + .features-gained { + @include flex-column; + gap: $spacing-3; + + h3 { + margin: 0; + font-size: $font-size-lg; + font-weight: $font-weight-semibold; + color: var(--color-text-primary); + } + + .feature-list { + list-style: none; + margin: 0; + padding: 0; + @include flex-column; + gap: $spacing-2; + } + + .feature-item { + @include panel; + padding: $spacing-3; + background-color: var(--color-bg-secondary); + + &.has-effects { + border-left: 3px solid var(--color-success); + } + + .feature-header { + @include flex-center; + justify-content: flex-start; + gap: $spacing-2; + margin-bottom: $spacing-2; + + .feature-name { + font-weight: $font-weight-semibold; + color: var(--color-text-primary); + } + + .feature-class { + font-size: $font-size-sm; + color: var(--color-text-muted); + } + + .feature-auto { + color: var(--color-success); + font-size: $font-size-sm; + + i { + font-size: $font-size-base; + } + } + } + + .feature-description { + font-size: $font-size-sm; + color: var(--color-text-secondary); + line-height: 1.5; + + p { + margin: 0; + } + + strong { + color: var(--color-text-primary); + } + } + } + } + + // Choice features section + .choice-features { + @include flex-column; + gap: $spacing-3; + + h3 { + margin: 0; + font-size: $font-size-lg; + font-weight: $font-weight-semibold; + color: var(--color-text-primary); + } + + .choice-feature { + @include panel; + padding: $spacing-3; + background-color: var(--color-bg-secondary); + border-left: 3px solid var(--color-warning); + + .choice-label { + @include flex-center; + justify-content: flex-start; + gap: $spacing-2; + margin-bottom: $spacing-2; + + .feature-name { + font-weight: $font-weight-semibold; + color: var(--color-text-primary); + } + + .feature-class { + font-size: $font-size-sm; + color: var(--color-text-muted); + } + } + + .feature-description { + font-size: $font-size-sm; + color: var(--color-text-secondary); + margin-bottom: $spacing-3; + + p { + margin: 0; + } + } + + .perk-choice-select { + @include input-base; + width: 100%; + cursor: pointer; + } + } + } + + // Perk selection section + .perk-selection { + @include flex-column; + gap: $spacing-3; + + h3 { + margin: 0; + font-size: $font-size-lg; + font-weight: $font-weight-semibold; + color: var(--color-text-primary); + } + + .perk-instruction { + margin: 0; + font-size: $font-size-sm; + color: var(--color-text-secondary); + } + + .perk-slot { + @include panel; + padding: $spacing-3; + background-color: var(--color-bg-secondary); + + .perk-slot-label { + display: block; + font-weight: $font-weight-semibold; + color: var(--color-text-primary); + margin-bottom: $spacing-2; + } + + .perk-select { + @include input-base; + width: 100%; + cursor: pointer; + + option:disabled { + color: var(--color-text-muted); + } + } + } + } + + // No features message + .no-features { + @include panel; + padding: $spacing-4; + text-align: center; + background-color: var(--color-bg-secondary); + + p { + margin: 0; + color: var(--color-text-muted); + font-style: italic; + } + } + + // Confirm button + .dialog-buttons { + margin-top: $spacing-2; + + .confirm-btn { + @include button-primary; + width: 100%; + padding: $spacing-3; + font-size: $font-size-lg; + gap: $spacing-2; + + &:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + } + } + } +} diff --git a/styles/scss/vagabond.scss b/styles/scss/vagabond.scss index 836b4fc..a586287 100644 --- a/styles/scss/vagabond.scss +++ b/styles/scss/vagabond.scss @@ -22,6 +22,7 @@ // Dialogs @import "dialogs/roll-dialog"; +@import "dialogs/level-up-dialog"; // Chat @import "chat/chat-cards"; diff --git a/system.json b/system.json index 6ca897f..e101bfc 100644 --- a/system.json +++ b/system.json @@ -74,62 +74,13 @@ "PLAYER": "OBSERVER", "ASSISTANT": "OWNER" } - }, - { - "name": "weapons", - "label": "Weapons", - "path": "packs/weapons", - "type": "Item", - "ownership": { - "PLAYER": "OBSERVER", - "ASSISTANT": "OWNER" - } - }, - { - "name": "armor", - "label": "Armor", - "path": "packs/armor", - "type": "Item", - "ownership": { - "PLAYER": "OBSERVER", - "ASSISTANT": "OWNER" - } - }, - { - "name": "equipment", - "label": "Equipment", - "path": "packs/equipment", - "type": "Item", - "ownership": { - "PLAYER": "OBSERVER", - "ASSISTANT": "OWNER" - } - }, - { - "name": "bestiary", - "label": "Bestiary", - "path": "packs/bestiary", - "type": "Actor", - "ownership": { - "PLAYER": "NONE", - "ASSISTANT": "OWNER" - } } ], "packFolders": [ { "name": "Vagabond RPG", "sorting": "a", - "packs": [ - "ancestries", - "classes", - "spells", - "perks", - "weapons", - "armor", - "equipment", - "bestiary" - ] + "packs": ["ancestries", "classes"] } ], "documentTypes": { diff --git a/templates/dialog/level-up.hbs b/templates/dialog/level-up.hbs new file mode 100644 index 0000000..93ffa1b --- /dev/null +++ b/templates/dialog/level-up.hbs @@ -0,0 +1,105 @@ +{{!-- Level Up Dialog Template --}} +{{!-- Shows features gained and handles perk/choice selections --}} + +
+
+

Level Up!

+

+ {{actor.name}} has reached Level {{newLevel}} +

+
+ + {{!-- Features Gained --}} + {{#if features.length}} +
+

Features Gained

+
    + {{#each features}} +
  • +
    + {{name}} + ({{className}}) + {{#if hasChanges}} + + + + {{/if}} +
    +
    {{{description}}}
    +
  • + {{/each}} +
+
+ {{/if}} + + {{!-- Choice Features (e.g., Fighting Style) --}} + {{#if hasChoices}} +
+

Feature Choices

+ {{#each choiceFeatures}} +
+ +
{{{description}}}
+ + {{!-- For perk choices --}} + {{#if (eq choiceType "perk")}} + + {{/if}} +
+ {{/each}} +
+ {{/if}} + + {{!-- Perk Selection --}} + {{#if hasPerks}} +
+

Perk Selection

+

Choose a perk from the list below:

+ + {{#each perkSlots}} +
+ + +
+ {{/each}} +
+ {{/if}} + + {{!-- No features message --}} + {{#unless features.length}} + {{#unless hasPerks}} + {{#unless hasChoices}} +
+

No new features at this level.

+
+ {{/unless}} + {{/unless}} + {{/unless}} + + {{!-- Confirm Button --}} +
+ +
+
From 7d66bea10fa908c6d2c9de7202b8c801b983f35e Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Tue, 16 Dec 2025 12:17:03 -0600 Subject: [PATCH 2/4] Update roadmap: reference class level-up design doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 2.12 now references: - NoteDiscovery design doc: gaming/vagabond-rpg/class-level-system-design.md - Prototype branch: prototype/class-level-system commit a7862be 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- PROJECT_ROADMAP.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PROJECT_ROADMAP.json b/PROJECT_ROADMAP.json index 05f7b7f..2d472cd 100644 --- a/PROJECT_ROADMAP.json +++ b/PROJECT_ROADMAP.json @@ -346,7 +346,7 @@ "tested": true, "priority": "high", "dependencies": ["2.2", "2.3", "1.8", "1.15"], - "notes": "Implemented via _onCreate/_preDelete lifecycle methods and updateActor hook. Made applyClassFeatures() idempotent. Commit 8afcf8c." + "notes": "Implemented via _onCreate lifecycle method and LevelUpDialog. Features with changes[] arrays auto-create ActiveEffects. applyClassFeatures() is idempotent, updateClassFeatures() handles level-up incrementally. Fixed duplicate item creation bug (dragDrop config). Design doc: NoteDiscovery gaming/vagabond-rpg/class-level-system-design.md. Prototype branch: prototype/class-level-system commit a7862be." }, { "id": "2.13", From 06e0dc01c018da453422aa465d260e0f0ce52bc8 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Tue, 16 Dec 2025 14:23:44 -0600 Subject: [PATCH 3/4] Complete P2-P5: Perks, Feature Choices, Ancestry Traits, Caster Progression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - P2: Perks with changes[] arrays create Active Effects on drop/delete - P3: Feature choice UI for Fighting Style (auto-grants Situational Awareness + selected training perk, ignoring prerequisites) - P4: Ancestry traits apply Active Effects (Dwarf Darksight/Tough working) - P5: Caster progression accumulates mana from class progression table Key patterns: - Manual UUID construction: Compendium.${pack.collection}.Item.${entry._id} - ignorePrereqs flag for specific choices bypassing all prerequisites - Mode 5 (OVERRIDE) for boolean senses like darkvision - Form data merging with direct DOM reading for reliable selection capture 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- PROTOTYPE_PLAN.json | 124 +++++++ module/applications/level-up-dialog.mjs | 251 ++++++++++++- module/data/item/ancestry.mjs | 9 + module/data/item/class.mjs | 4 + module/documents/item.mjs | 349 +++++++++++++++--- module/sheets/base-actor-sheet.mjs | 31 ++ packs/_source/ancestries/dwarf.json | 21 +- packs/_source/perks/sharpshooter.json | 44 +++ .../_source/perks/situational-awareness.json | 38 ++ packs/_source/perks/tough.json | 44 +++ packs/_source/perks/weapon-mastery.json | 44 +++ templates/dialog/level-up.hbs | 10 +- 12 files changed, 898 insertions(+), 71 deletions(-) create mode 100644 PROTOTYPE_PLAN.json create mode 100644 packs/_source/perks/sharpshooter.json create mode 100644 packs/_source/perks/situational-awareness.json create mode 100644 packs/_source/perks/tough.json create mode 100644 packs/_source/perks/weapon-mastery.json diff --git a/PROTOTYPE_PLAN.json b/PROTOTYPE_PLAN.json new file mode 100644 index 0000000..fd0330a --- /dev/null +++ b/PROTOTYPE_PLAN.json @@ -0,0 +1,124 @@ +{ + "prototype": { + "name": "Class & Character Progression Prototype", + "branch": "prototype/class-level-system", + "description": "Proof-of-concept implementations for Active Effects automation across classes, perks, ancestries, and caster progression", + "created": "2024-12-16", + "status": "in_progress", + "design_doc": "NoteDiscovery: gaming/vagabond-rpg/class-level-system-design.md" + }, + "tasks": [ + { + "id": "P1", + "name": "Class Level-Up with Active Effects", + "description": "Class features with changes[] arrays automatically create Active Effects on drop and level-up", + "status": "complete", + "commit": "a7862be", + "files_changed": [ + "module/documents/item.mjs", + "module/documents/actor.mjs", + "module/applications/level-up-dialog.mjs", + "module/sheets/base-actor-sheet.mjs", + "packs/_source/classes/fighter.json" + ], + "patterns_established": [ + "Features with changes[] become ActiveEffects via applyClassFeatures()", + "updateClassFeatures() handles level-up incrementally", + "Idempotency via checking existing effect flags", + "dragDrop config in DEFAULT_OPTIONS (not manual listeners)", + "ActiveEffect flags: vagabond.classFeature, className, featureName, featureLevel" + ], + "tested": true, + "notes": "Fighter Valor I/II/III reduces crit threshold cumulatively. Fixed duplicate item creation bug." + }, + { + "id": "P2", + "name": "Perk Active Effects", + "description": "Perks apply Active Effects when added to character, using same changes[] pattern as class features", + "status": "complete", + "dependencies": ["P1"], + "implementation_plan": [ + "Add changes[] array to PerkData schema if not present", + "Update VagabondItem._onCreate() to handle type === 'perk'", + "Create test perk with mechanical effect (e.g., 'Tough' adds +5 HP)", + "Verify effect applies on drop and removes on delete", + "Update level-up dialog perk selection to show effect preview" + ], + "files_to_modify": [ + "module/documents/item.mjs", + "module/data/perk.mjs", + "packs/_source/perks/ (create test perks)" + ], + "acceptance_criteria": [ + "Dropping perk on character creates ActiveEffect", + "Deleting perk removes its ActiveEffect", + "Perk effects stack with class effects", + "Level-up dialog perk selection works end-to-end" + ] + }, + { + "id": "P3", + "name": "Feature Choices (Fighting Style)", + "description": "Implement UI for features that require player choice, starting with Fighter's Fighting Style perk selection", + "status": "complete", + "dependencies": ["P2"], + "files_changed": [ + "module/applications/level-up-dialog.mjs", + "module/documents/item.mjs", + "module/data/item/class.mjs", + "templates/dialog/level-up.hbs" + ], + "patterns_established": [ + "Class features schema includes requiresChoice, choiceType, choiceFilter fields", + "_getFilteredPerksForChoice() filters perks by custom prerequisite text", + "ignorePrereqs flag allows bypassing all prerequisites for specific choices", + "Classes with choice features at level 1 show dialog on initial drop", + "_applyFightingStyle() auto-grants Situational Awareness + selected training perk", + "UUID must be constructed manually: Compendium.${pack.collection}.Item.${entry._id}" + ], + "tested": true, + "notes": "Fighting Style grants Situational Awareness AND one Melee/Ranged Training perk (ignoring prereqs). Fixed UUID null issue by constructing UUID from pack collection and entry ID." + }, + { + "id": "P4", + "name": "Ancestry Traits as Active Effects", + "description": "Ancestry traits with mechanical effects apply as Active Effects when ancestry is added to character", + "status": "complete", + "dependencies": ["P1"], + "files_changed": [ + "module/documents/item.mjs", + "module/data/item/ancestry.mjs", + "packs/_source/ancestries/dwarf.json" + ], + "patterns_established": [ + "Ancestry traits use same changes[] pattern as class features", + "applyAncestryTraits() creates effects with vagabond.ancestryTrait flag", + "_removeAncestryEffects() cleans up on ancestry deletion/replacement", + "Mode 5 (OVERRIDE) for boolean senses like darkvision" + ], + "tested": true, + "notes": "Dwarf Darksight (+darkvision) and Tough (+3 HP bonus) traits verified working" + }, + { + "id": "P5", + "name": "Caster Class Progression", + "description": "Verify mana and castingMax from class progression apply correctly for caster classes", + "status": "complete", + "dependencies": ["P1"], + "files_changed": ["module/sheets/base-actor-sheet.mjs"], + "patterns_established": [ + "_applyClassProgression() accumulates mana from progression entries", + "Mana.value set to max only on initial grant (when value === 0)", + "Form submission cleanup via #cleanNumericFields() prevents empty string validation errors" + ], + "tested": true, + "notes": "Wizard Level 1 = 4 mana, Level 2 = 8 mana. Fixed validation error for empty numeric inputs." + } + ], + "merge_criteria": [ + "All tasks complete and tested", + "No console errors in normal workflows", + "Design doc updated with new patterns", + "Compendiums rebuilt with test content" + ] +} diff --git a/module/applications/level-up-dialog.mjs b/module/applications/level-up-dialog.mjs index d68a848..e099c3a 100644 --- a/module/applications/level-up-dialog.mjs +++ b/module/applications/level-up-dialog.mjs @@ -13,7 +13,7 @@ */ // Debug logging for level-up workflow - set to false to disable -const DEBUG_LEVELUP = true; +const DEBUG_LEVELUP = false; const debugLog = (...args) => { if (DEBUG_LEVELUP) console.log("[LevelUpDialog]", ...args); }; @@ -186,6 +186,23 @@ export default class LevelUpDialog extends HandlebarsApplicationMixin(Applicatio debugLog(`Loaded ${context.availablePerks?.length || 0} available perks`); } + // Load filtered perks for each choice feature + for (const choiceFeature of choiceFeatures) { + if (choiceFeature.choiceType === "perk" && choiceFeature.choiceFilter) { + debugLog(`Loading filtered perks for "${choiceFeature.name}"...`); + // Fighting Style ignores all prerequisites per the feature rules + const ignorePrereqs = choiceFeature.name === "Fighting Style"; + choiceFeature.filteredPerks = await this._getFilteredPerksForChoice( + choiceFeature.choiceFilter, + ignorePrereqs + ); + debugLog( + `Loaded ${choiceFeature.filteredPerks?.length || 0} filtered perks for "${choiceFeature.name}"`, + choiceFeature.filteredPerks?.map((p) => ({ name: p.name, uuid: p.uuid, id: p.id })) + ); + } + } + return context; } @@ -223,7 +240,7 @@ export default class LevelUpDialog extends HandlebarsApplicationMixin(Applicatio uuid: perk.uuid, name: perk.name, description: perk.system.description, - prerequisites: perk.system.prerequisites || [], + prerequisites: perk.system.prerequisites || {}, prerequisitesMet: met, missing: prereqResult?.missing || [], }); @@ -240,6 +257,79 @@ export default class LevelUpDialog extends HandlebarsApplicationMixin(Applicatio return perks; } + /** + * Get perks filtered for a specific choice feature. + * Filters by the choiceFilter.prerequisite array (matches against perk's custom prerequisite text). + * + * @param {Object} choiceFilter - The filter from the feature (e.g., { prerequisite: ["Melee Training", "Ranged Training"] }) + * @param {boolean} ignorePrereqs - If true, mark all filtered perks as available (for features like Fighting Style) + * @returns {Promise} Filtered perk data + * @private + */ + async _getFilteredPerksForChoice(choiceFilter, ignorePrereqs = false) { + const pack = game.packs.get("vagabond.perks"); + if (!pack) return []; + + const index = await pack.getIndex(); + const perks = []; + const filterValues = choiceFilter?.prerequisite || []; + + debugLog("Filtering perks for choice", { filterValues, ignorePrereqs }); + + for (const entry of index) { + const perk = await pack.getDocument(entry._id); + if (!perk) continue; + + // Build the UUID from pack and entry info (more reliable than perk.uuid) + const perkUuid = `Compendium.${pack.collection}.Item.${entry._id}`; + + // Check if perk matches the filter (by custom prerequisite text) + const customPrereq = perk.system.prerequisites?.custom || ""; + const matchesFilter = filterValues.length === 0 || filterValues.includes(customPrereq); + + if (!matchesFilter) { + debugLog(`Perk "${perk.name}" filtered out (custom: "${customPrereq}")`); + continue; + } + + // Check prerequisites (optionally ignoring all for features like Fighting Style) + let met = true; + let missing = []; + + if (ignorePrereqs) { + // Feature allows ignoring prerequisites - all filtered perks are selectable + met = true; + missing = []; + } else { + const prereqResult = perk.checkPrerequisites?.(this.actor); + met = prereqResult?.met ?? true; + missing = prereqResult?.missing || []; + } + + // Check if actor already has this perk + const alreadyHas = this.actor.items.some((i) => i.type === "perk" && i.name === perk.name); + + if (!alreadyHas) { + perks.push({ + id: entry._id, + uuid: perkUuid, + name: perk.name, + description: perk.system.description, + prerequisites: perk.system.prerequisites || {}, + prerequisitesMet: met, + missing, + customPrereq, + }); + debugLog(`Perk "${perk.name}" added to choice list (met: ${met}, uuid: ${perkUuid})`); + } + } + + // Sort alphabetically (all should be selectable for Fighting Style) + perks.sort((a, b) => a.name.localeCompare(b.name)); + + return perks; + } + /* -------------------------------------------- */ /* Event Handlers */ /* -------------------------------------------- */ @@ -263,14 +353,38 @@ export default class LevelUpDialog extends HandlebarsApplicationMixin(Applicatio }); } - // Handle feature choice changes + // Handle feature choice changes - use 'input' event as well as 'change' for better capture const choiceSelects = this.element.querySelectorAll("[data-feature-choice]"); debugLog(`Found ${choiceSelects.length} feature choice select elements`); for (const select of choiceSelects) { - select.addEventListener("change", (event) => { + // Log initial state + debugLog(`Select initial state for "${select.dataset.featureChoice}":`, { + value: select.value, + selectedIndex: select.selectedIndex, + optionsCount: select.options.length, + }); + + // Track both change and input events + const handleSelection = (event) => { const featureName = event.currentTarget.dataset.featureChoice; - this.choices.featureChoices[featureName] = event.currentTarget.value; - debugLog(`Feature choice changed: "${featureName}" = ${event.currentTarget.value}`); + const newValue = event.currentTarget.value; + const selectedIndex = event.currentTarget.selectedIndex; + const selectedText = event.currentTarget.options[selectedIndex]?.textContent; + + this.choices.featureChoices[featureName] = newValue; + debugLog( + `Feature choice ${event.type}: "${featureName}" = "${newValue}" (index: ${selectedIndex}, text: "${selectedText}")` + ); + }; + + select.addEventListener("change", handleSelection); + select.addEventListener("input", handleSelection); + + // Also track focus/blur to see if something is resetting + select.addEventListener("blur", (event) => { + debugLog( + `Select blur for "${event.currentTarget.dataset.featureChoice}": value="${event.currentTarget.value}"` + ); }); } } @@ -320,6 +434,40 @@ export default class LevelUpDialog extends HandlebarsApplicationMixin(Applicatio const data = foundry.utils.expandObject(formData.object); debugLog("Expanded form data:", data); + // Also read feature choice selects directly from the form (backup method) + const featureChoiceSelects = form.querySelectorAll("[data-feature-choice]"); + const directChoices = {}; + let missingRequiredChoice = false; + + for (const select of featureChoiceSelects) { + const featureName = select.dataset.featureChoice; + const selectedValue = select.value; + const selectedIndex = select.selectedIndex; + const selectedOption = select.options[selectedIndex]; + + debugLog(`Direct select read: "${featureName}"`, { + value: selectedValue, + selectedIndex, + selectedOptionText: selectedOption?.textContent, + optionsCount: select.options.length, + }); + + directChoices[featureName] = selectedValue; + + // Check if this is a required choice with no selection + if (!selectedValue) { + missingRequiredChoice = true; + } + } + + // Warn if no choice was made + if (missingRequiredChoice) { + ui.notifications.warn("Please select a perk for Feature Choices before confirming."); + return; // Don't proceed with level up + } + + data.directFeatureChoices = directChoices; + // Apply the level up await dialog._applyLevelUp(data); } @@ -332,11 +480,23 @@ export default class LevelUpDialog extends HandlebarsApplicationMixin(Applicatio * @private */ async _applyLevelUp(formData) { + // Merge all sources of feature choices (direct DOM read takes priority) + const featureChoicesFromForm = formData.featureChoice || {}; + const directFeatureChoices = formData.directFeatureChoices || {}; + const mergedFeatureChoices = { + ...this.choices.featureChoices, + ...featureChoicesFromForm, + ...directFeatureChoices, // Direct DOM read is most reliable + }; + debugLog("_applyLevelUp called", { actorName: this.actor.name, oldLevel: this.oldLevel, newLevel: this.newLevel, choices: this.choices, + featureChoicesFromForm, + directFeatureChoices, + mergedFeatureChoices, }); // 1. Update class features for all classes @@ -376,12 +536,26 @@ export default class LevelUpDialog extends HandlebarsApplicationMixin(Applicatio } } - // 3. Handle feature choices - debugLog(`Processing feature choices:`, this.choices.featureChoices); - for (const [featureName, choice] of Object.entries(this.choices.featureChoices)) { + // 3. Handle feature choices (using merged form data + event-tracked choices) + debugLog(`Processing feature choices:`, mergedFeatureChoices); + for (const [featureName, choice] of Object.entries(mergedFeatureChoices)) { debugLog(`Feature choice for "${featureName}": ${choice}`); - // Feature choices can be complex - for now just log - // TODO: Implement specific handling for choice features (e.g., add selected perk) + + // Handle Fighting Style specifically + if (featureName === "Fighting Style") { + await this._applyFightingStyle(choice); + } else if (choice) { + // Generic perk choice handling - add the selected perk + try { + const perkDoc = await fromUuid(choice); + if (perkDoc) { + debugLog(`Adding choice perk: "${perkDoc.name}"`); + await this.actor.createEmbeddedDocuments("Item", [perkDoc.toObject()]); + } + } catch (err) { + console.error(`Failed to add choice perk ${choice}:`, err); + } + } } // Log final actor state @@ -398,6 +572,61 @@ export default class LevelUpDialog extends HandlebarsApplicationMixin(Applicatio ui.notifications.info(`${this.actor.name} advanced to level ${this.newLevel}!`); } + /** + * Apply the Fighting Style feature. + * Grants Situational Awareness perk AND the selected training perk. + * + * @param {string} selectedPerkUuid - UUID of the selected training perk + * @returns {Promise} + * @private + */ + async _applyFightingStyle(selectedPerkUuid) { + debugLog("Applying Fighting Style feature"); + + const pack = game.packs.get("vagabond.perks"); + if (!pack) { + debugWarn("Perks compendium not found, cannot apply Fighting Style"); + return; + } + + // 1. Auto-grant Situational Awareness + const index = await pack.getIndex(); + const saEntry = index.find((e) => e.name === "Situational Awareness"); + + if (saEntry) { + const saPerk = await pack.getDocument(saEntry._id); + if (saPerk) { + // Check if actor already has it + const alreadyHas = this.actor.items.some( + (i) => i.type === "perk" && i.name === "Situational Awareness" + ); + if (!alreadyHas) { + debugLog("Granting Situational Awareness perk"); + await this.actor.createEmbeddedDocuments("Item", [saPerk.toObject()]); + } else { + debugLog("Actor already has Situational Awareness"); + } + } + } else { + debugWarn("Situational Awareness perk not found in compendium"); + } + + // 2. Add the selected training perk + if (selectedPerkUuid) { + try { + const perkDoc = await fromUuid(selectedPerkUuid); + if (perkDoc) { + debugLog(`Granting selected training perk: "${perkDoc.name}"`); + await this.actor.createEmbeddedDocuments("Item", [perkDoc.toObject()]); + } + } catch (err) { + console.error(`Failed to add training perk ${selectedPerkUuid}:`, err); + } + } else { + debugWarn("No training perk selected for Fighting Style"); + } + } + /* -------------------------------------------- */ /* Static Methods */ /* -------------------------------------------- */ diff --git a/module/data/item/ancestry.mjs b/module/data/item/ancestry.mjs index d6f7696..b9d16a9 100644 --- a/module/data/item/ancestry.mjs +++ b/module/data/item/ancestry.mjs @@ -43,6 +43,15 @@ export default class AncestryData extends VagabondItemBase { new fields.SchemaField({ name: new fields.StringField({ required: true }), description: new fields.HTMLField({ required: 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: [] } + ), }), { initial: [] } ), diff --git a/module/data/item/class.mjs b/module/data/item/class.mjs index 80bc50e..d6e1899 100644 --- a/module/data/item/class.mjs +++ b/module/data/item/class.mjs @@ -86,6 +86,10 @@ export default class ClassData extends VagabondItemBase { }), { initial: [] } ), + // Choice features - features that require player selection + requiresChoice: new fields.BooleanField({ initial: false }), + choiceType: new fields.StringField({ required: false, blank: true }), // "perk", "spell", etc. + choiceFilter: new fields.ObjectField({ required: false }), // Filter criteria for choices }), { initial: [] } ), diff --git a/module/documents/item.mjs b/module/documents/item.mjs index 6a8cce4..cfbcd64 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -1,5 +1,7 @@ +import LevelUpDialog from "../applications/level-up-dialog.mjs"; + // Debug logging for level-up workflow - set to false to disable -const DEBUG_LEVELUP = true; +const DEBUG_LEVELUP = false; const debugLog = (...args) => { if (DEBUG_LEVELUP) console.log("[VagabondItem]", ...args); }; @@ -53,20 +55,76 @@ export default class VagabondItem extends Item { // Apply class features when class is added to a character // Check that actor still exists (may be deleted in tests) if (this.type === "class" && this.parent?.type === "character" && this.actor?.id) { - debugLog("Class added to character - applying initial features..."); + debugLog("Class added to character - checking for choice features..."); + + // Check if there are choice features at the actor's current level + const currentLevel = this.actor.system.level || 1; + const features = this.system.features || []; + + debugLog( + "Features array:", + features.map((f) => ({ + name: f.name, + level: f.level, + requiresChoice: f.requiresChoice, + hasRequiresChoice: "requiresChoice" in f, + })) + ); + + const choiceFeatures = features.filter( + (f) => f.level <= currentLevel && f.requiresChoice === true + ); + + if (choiceFeatures.length > 0) { + // Show level-up dialog for choice features + debugLog(`Found ${choiceFeatures.length} choice features, showing dialog...`); + try { + // Use oldLevel=0 to indicate this is initial class assignment + await LevelUpDialog.create(this.actor, currentLevel, 0); + } catch (err) { + console.error("Failed to show level-up dialog:", err); + } + } else { + // No choices needed, apply features directly + debugLog("No choice features - applying initial features directly..."); + try { + const effects = await this.applyClassFeatures(); + debugLog(`Applied ${effects.length} initial Active Effects`); + } catch (err) { + // Actor may have been deleted during tests - silently ignore + if (!err.message?.includes("does not exist")) throw err; + debugWarn("Actor was deleted during feature application"); + } + } + } + + // Apply perk effects when perk is added to a character + if (this.type === "perk" && this.parent?.type === "character" && this.actor?.id) { + debugLog("Perk added to character - applying effects..."); try { - const effects = await this.applyClassFeatures(); - debugLog(`Applied ${effects.length} initial Active Effects`); + const effects = await this.applyPerkEffects(); + debugLog(`Applied ${effects.length} perk Active Effects`); } catch (err) { - // Actor may have been deleted during tests - silently ignore if (!err.message?.includes("does not exist")) throw err; - debugWarn("Actor was deleted during feature application"); + debugWarn("Actor was deleted during perk effect application"); + } + } + + // Apply ancestry traits when ancestry is added to a character + if (this.type === "ancestry" && this.parent?.type === "character" && this.actor?.id) { + debugLog("Ancestry added to character - applying traits..."); + try { + const effects = await this.applyAncestryTraits(); + debugLog(`Applied ${effects.length} ancestry trait Active Effects`); + } catch (err) { + if (!err.message?.includes("does not exist")) throw err; + debugWarn("Actor was deleted during ancestry trait application"); } } } /** - * Handle item deletion. For class items, remove associated Active Effects. + * Handle item deletion. For class/perk/ancestry items, remove associated Active Effects. * * @override */ @@ -76,6 +134,16 @@ export default class VagabondItem extends Item { await this._removeClassEffects(); } + // Remove perk effects before deletion + if (this.type === "perk" && this.parent?.type === "character") { + await this._removePerkEffects(); + } + + // Remove ancestry trait effects before deletion + if (this.type === "ancestry" && this.parent?.type === "character") { + await this._removeAncestryEffects(); + } + return super._preDelete(options, userId); } @@ -475,59 +543,66 @@ export default class VagabondItem extends Item { return { met: true, missing: [] }; } - const prereqs = this.system.prerequisites || []; + const prereqs = this.system.prerequisites; + if (!prereqs) { + return { met: true, missing: [] }; + } + const missing = []; - for (const prereq of prereqs) { - let met = false; - - switch (prereq.type) { - case "stat": { - // Check stat minimum - const statValue = actor.system.stats?.[prereq.stat]?.value || 0; - met = statValue >= (prereq.value || 0); - break; - } - - case "training": { - // Check if trained in skill - const skillData = actor.system.skills?.[prereq.skill]; - met = skillData?.trained === true; - break; - } - - case "spell": { - // Check if actor knows the spell - const knownSpells = actor.getSpells?.() || []; - met = knownSpells.some((s) => s.name === prereq.spellName); - break; - } - - case "perk": { - // Check if actor has the prerequisite perk - const perks = actor.getPerks?.() || []; - met = perks.some((p) => p.name === prereq.perkName); - break; - } - - case "level": - // Check minimum level - met = (actor.system.level || 1) >= (prereq.value || 1); - break; - - case "class": { - // Check if actor has the class - const classes = actor.getClasses?.() || []; - met = classes.some((c) => c.name === prereq.className); - break; + // Check stat requirements + if (prereqs.stats) { + for (const [stat, required] of Object.entries(prereqs.stats)) { + if (required !== null && required > 0) { + const actorStat = actor.system.stats?.[stat]?.value || 0; + if (actorStat < required) { + const statLabel = stat.charAt(0).toUpperCase() + stat.slice(1); + missing.push({ + type: "stat", + stat, + value: required, + label: `${statLabel} ${required}`, + }); + } } } + } - if (!met) { - missing.push(prereq); + // Check skill training requirements + if (prereqs.trainedSkills) { + for (const skillId of prereqs.trainedSkills) { + const skill = actor.system.skills?.[skillId]; + if (!skill?.trained) { + missing.push({ type: "training", skill: skillId, label: `Trained in ${skillId}` }); + } } } + // Check spell requirements + if (prereqs.spells?.length > 0) { + const knownSpells = actor.items.filter((i) => i.type === "spell"); + for (const spellName of prereqs.spells) { + if (!knownSpells.some((s) => s.name === spellName)) { + missing.push({ type: "spell", spellName, label: `Spell: ${spellName}` }); + } + } + } + + // Check perk requirements + if (prereqs.perks?.length > 0) { + const actorPerks = actor.items.filter((i) => i.type === "perk"); + for (const perkName of prereqs.perks) { + if (!actorPerks.some((p) => p.name === perkName)) { + missing.push({ type: "perk", perkName, label: `Perk: ${perkName}` }); + } + } + } + + // Custom requirements are always flagged as missing (need manual review) + if (prereqs.custom) { + missing.push({ type: "custom", label: prereqs.custom }); + } + return { met: missing.length === 0, missing, @@ -788,6 +863,176 @@ export default class VagabondItem extends Item { } } + /* -------------------------------------------- */ + /* Perk Helpers */ + /* -------------------------------------------- */ + + /** + * Apply perk effects as Active Effects when perk is added to character. + * This method is idempotent - it won't create duplicate effects. + * + * @returns {Promise} Created effects + */ + async applyPerkEffects() { + debugLog(`applyPerkEffects called for perk "${this.name}"`, { + hasChanges: this.system.changes?.length > 0, + }); + + if (this.type !== "perk" || !this.actor) { + debugWarn("applyPerkEffects: Not a perk or no actor"); + return []; + } + + const changes = this.system.changes || []; + if (changes.length === 0) { + debugLog("Perk has no mechanical changes - skipping effect creation"); + return []; + } + + // Check if effect already exists (idempotent) + const existingEffect = this.actor.effects.find((e) => e.origin === this.uuid); + if (existingEffect) { + debugLog("Perk effect already exists - skipping"); + return []; + } + + // Build Active Effect data + const effectData = { + name: this.name, + icon: this.img || "icons/svg/upgrade.svg", + origin: this.uuid, + changes: changes.map((change) => ({ + key: change.key, + mode: change.mode ?? 2, // Default to ADD mode + value: String(change.value), + priority: change.priority ?? null, + })), + flags: { + vagabond: { + perkEffect: true, + perkName: this.name, + }, + }, + }; + + debugLog("Creating perk Active Effect:", { + name: effectData.name, + changes: effectData.changes, + }); + + const createdEffects = await this.actor.createEmbeddedDocuments("ActiveEffect", [effectData]); + debugLog(`Created ${createdEffects.length} perk Active Effects`); + + return createdEffects; + } + + /** + * Remove all Active Effects originating from this perk. + * + * @private + * @returns {Promise} + */ + async _removePerkEffects() { + if (!this.actor) return; + + const perkEffects = this.actor.effects.filter((e) => e.origin === this.uuid); + if (perkEffects.length > 0) { + debugLog(`Removing ${perkEffects.length} perk effects for "${this.name}"`); + const ids = perkEffects.map((e) => e.id); + await this.actor.deleteEmbeddedDocuments("ActiveEffect", ids); + } + } + + /* -------------------------------------------- */ + /* Ancestry Helpers */ + /* -------------------------------------------- */ + + /** + * Apply ancestry trait effects as Active Effects when ancestry is added to character. + * Each trait with a changes[] array creates a separate Active Effect. + * This method is idempotent - it won't create duplicate effects. + * + * @returns {Promise} Created effects + */ + async applyAncestryTraits() { + debugLog(`applyAncestryTraits called for ancestry "${this.name}"`, { + traitsCount: this.system.traits?.length || 0, + }); + + if (this.type !== "ancestry" || !this.actor) { + debugWarn("applyAncestryTraits: Not an ancestry or no actor"); + return []; + } + + const traits = this.system.traits || []; + const traitsWithChanges = traits.filter((t) => t.changes?.length > 0); + + if (traitsWithChanges.length === 0) { + debugLog("Ancestry has no traits with mechanical changes - skipping effect creation"); + return []; + } + + // Check for existing effects (idempotent) + const existingEffects = this.actor.effects.filter((e) => e.origin === this.uuid); + const existingTraitNames = new Set(existingEffects.map((e) => e.flags?.vagabond?.traitName)); + + const newTraits = traitsWithChanges.filter((t) => !existingTraitNames.has(t.name)); + if (newTraits.length === 0) { + debugLog("All ancestry traits already applied - skipping"); + return []; + } + + // Build Active Effect data for each trait + const effectsData = newTraits.map((trait) => ({ + name: `${this.name}: ${trait.name}`, + icon: this.img || "icons/svg/mystery-man.svg", + origin: this.uuid, + changes: trait.changes.map((change) => ({ + key: change.key, + mode: change.mode ?? 2, + value: String(change.value), + priority: change.priority ?? null, + })), + flags: { + vagabond: { + ancestryTrait: true, + ancestryName: this.name, + traitName: trait.name, + }, + }, + })); + + debugLog( + "Creating ancestry trait Active Effects:", + effectsData.map((e) => ({ + name: e.name, + changes: e.changes, + })) + ); + + const createdEffects = await this.actor.createEmbeddedDocuments("ActiveEffect", effectsData); + debugLog(`Created ${createdEffects.length} ancestry trait Active Effects`); + + return createdEffects; + } + + /** + * Remove all Active Effects originating from this ancestry. + * + * @private + * @returns {Promise} + */ + async _removeAncestryEffects() { + if (!this.actor) return; + + const ancestryEffects = this.actor.effects.filter((e) => e.origin === this.uuid); + if (ancestryEffects.length > 0) { + debugLog(`Removing ${ancestryEffects.length} ancestry effects for "${this.name}"`); + const ids = ancestryEffects.map((e) => e.id); + await this.actor.deleteEmbeddedDocuments("ActiveEffect", ids); + } + } + /** * Apply class progression stats (mana, casting max) to the actor. * diff --git a/module/sheets/base-actor-sheet.mjs b/module/sheets/base-actor-sheet.mjs index d5d82a7..63943fd 100644 --- a/module/sheets/base-actor-sheet.mjs +++ b/module/sheets/base-actor-sheet.mjs @@ -616,9 +616,40 @@ export default class VagabondActorSheet extends HandlebarsApplicationMixin(Actor static async #onFormSubmit(event, form, formData) { const sheet = this; const updateData = foundry.utils.expandObject(formData.object); + + // Clean up numeric fields that may have empty string values + // This can happen when number inputs are cleared by the user + VagabondActorSheet.#cleanNumericFields(updateData); + await sheet.actor.update(updateData); } + /** + * Recursively clean numeric fields in update data. + * Empty strings are converted to 0 for numeric resource fields. + * @param {Object} obj - Object to clean + * @param {string} path - Current path for debugging + * @private + */ + static #cleanNumericFields(obj, path = "") { + if (!obj || typeof obj !== "object") return; + + for (const [key, value] of Object.entries(obj)) { + const currentPath = path ? `${path}.${key}` : key; + + if (typeof value === "object" && value !== null) { + // Recurse into nested objects + VagabondActorSheet.#cleanNumericFields(value, currentPath); + } else if (value === "" || value === null) { + // Check if this should be a numeric field based on common patterns + const numericKeys = ["value", "max", "bonus", "min", "base", "level", "castingMax"]; + if (numericKeys.includes(key)) { + obj[key] = 0; + } + } + } + } + /** * Handle skill roll action. * @param {PointerEvent} event diff --git a/packs/_source/ancestries/dwarf.json b/packs/_source/ancestries/dwarf.json index 8279cff..551b7e8 100755 --- a/packs/_source/ancestries/dwarf.json +++ b/packs/_source/ancestries/dwarf.json @@ -11,15 +11,30 @@ "traits": [ { "name": "Darksight", - "description": "

You can see in darkness as if it were dim light.

" + "description": "

You can see in darkness as if it were dim light.

", + "changes": [ + { + "key": "system.senses.darkvision", + "mode": 5, + "value": "true" + } + ] }, { "name": "Sturdy", - "description": "

You gain +1 to saves against Fear, Sickened, and Shove effects.

" + "description": "

You gain +1 to saves against Fear, Sickened, and Shove effects.

", + "changes": [] }, { "name": "Tough", - "description": "

You gain +Level additional maximum HP.

" + "description": "

You gain +3 additional maximum HP.

", + "changes": [ + { + "key": "system.resources.hp.bonus", + "mode": 2, + "value": "3" + } + ] } ] } diff --git a/packs/_source/perks/sharpshooter.json b/packs/_source/perks/sharpshooter.json new file mode 100644 index 0000000..930a38d --- /dev/null +++ b/packs/_source/perks/sharpshooter.json @@ -0,0 +1,44 @@ +{ + "_id": "vagabondPerkSharpshooter", + "name": "Sharpshooter", + "type": "perk", + "img": "icons/svg/target.svg", + "system": { + "description": "

You have trained extensively with ranged weapons. Your ranged attacks deal +1 damage.

", + "prerequisites": { + "stats": { + "might": null, + "dexterity": 4, + "awareness": null, + "reason": null, + "presence": null, + "luck": null + }, + "trainedSkills": ["ranged"], + "spells": [], + "perks": [], + "custom": "Ranged Training" + }, + "changes": [ + { + "key": "system.attacks.ranged.damageBonus", + "mode": 2, + "value": "1" + } + ], + "passive": true, + "uses": { + "value": 0, + "max": 0, + "per": "" + }, + "luckCost": 0, + "grantsLuck": 0, + "isRitual": false, + "ritualDuration": 0, + "ritualComponents": "", + "tags": ["combat", "ranged", "training"] + }, + "effects": [], + "_key": "!items!vagabondPerkSharpshooter" +} diff --git a/packs/_source/perks/situational-awareness.json b/packs/_source/perks/situational-awareness.json new file mode 100644 index 0000000..9c918c1 --- /dev/null +++ b/packs/_source/perks/situational-awareness.json @@ -0,0 +1,38 @@ +{ + "_id": "vagabondPerkSituationalAwareness", + "name": "Situational Awareness", + "type": "perk", + "img": "icons/svg/eye.svg", + "system": { + "description": "

You are always alert to danger. You cannot be Surprised, and you have Favor on Initiative rolls.

", + "prerequisites": { + "stats": { + "might": null, + "dexterity": null, + "awareness": 4, + "reason": null, + "presence": null, + "luck": null + }, + "trainedSkills": [], + "spells": [], + "perks": [], + "custom": "" + }, + "changes": [], + "passive": true, + "uses": { + "value": 0, + "max": 0, + "per": "" + }, + "luckCost": 0, + "grantsLuck": 0, + "isRitual": false, + "ritualDuration": 0, + "ritualComponents": "", + "tags": ["combat", "awareness"] + }, + "effects": [], + "_key": "!items!vagabondPerkSituationalAwareness" +} diff --git a/packs/_source/perks/tough.json b/packs/_source/perks/tough.json new file mode 100644 index 0000000..0e19acc --- /dev/null +++ b/packs/_source/perks/tough.json @@ -0,0 +1,44 @@ +{ + "_id": "vagabondPerkTough", + "name": "Tough", + "type": "perk", + "img": "icons/svg/shield.svg", + "system": { + "description": "

Your body is hardened through training or natural resilience. You gain +5 maximum HP.

", + "prerequisites": { + "stats": { + "might": 4, + "dexterity": null, + "awareness": null, + "reason": null, + "presence": null, + "luck": null + }, + "trainedSkills": [], + "spells": [], + "perks": [], + "custom": "" + }, + "changes": [ + { + "key": "system.resources.hp.bonus", + "mode": 2, + "value": "5" + } + ], + "passive": true, + "uses": { + "value": 0, + "max": 0, + "per": "" + }, + "luckCost": 0, + "grantsLuck": 0, + "isRitual": false, + "ritualDuration": 0, + "ritualComponents": "", + "tags": ["combat", "defensive"] + }, + "effects": [], + "_key": "!items!vagabondPerkTough" +} diff --git a/packs/_source/perks/weapon-mastery.json b/packs/_source/perks/weapon-mastery.json new file mode 100644 index 0000000..18bd92f --- /dev/null +++ b/packs/_source/perks/weapon-mastery.json @@ -0,0 +1,44 @@ +{ + "_id": "vagabondPerkWeaponMastery", + "name": "Weapon Mastery", + "type": "perk", + "img": "icons/svg/sword.svg", + "system": { + "description": "

You have trained extensively with melee weapons. Your melee attacks deal +1 damage.

", + "prerequisites": { + "stats": { + "might": 4, + "dexterity": null, + "awareness": null, + "reason": null, + "presence": null, + "luck": null + }, + "trainedSkills": ["melee"], + "spells": [], + "perks": [], + "custom": "Melee Training" + }, + "changes": [ + { + "key": "system.attacks.melee.damageBonus", + "mode": 2, + "value": "1" + } + ], + "passive": true, + "uses": { + "value": 0, + "max": 0, + "per": "" + }, + "luckCost": 0, + "grantsLuck": 0, + "isRitual": false, + "ritualDuration": 0, + "ritualComponents": "", + "tags": ["combat", "melee", "training"] + }, + "effects": [], + "_key": "!items!vagabondPerkWeaponMastery" +} diff --git a/templates/dialog/level-up.hbs b/templates/dialog/level-up.hbs index 93ffa1b..a2d20c7 100644 --- a/templates/dialog/level-up.hbs +++ b/templates/dialog/level-up.hbs @@ -44,13 +44,13 @@
{{{description}}}
- {{!-- For perk choices --}} + {{!-- For perk choices with filtered list --}} {{#if (eq choiceType "perk")}} - - {{#each ../availablePerks}} - {{/each}} From 607c65cffcb74c25ea9e17851cccf94833f21149 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Tue, 16 Dec 2025 14:46:20 -0600 Subject: [PATCH 4/4] Add missing Handlebars join helper for item sheet templates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Class and perk item sheets use {{join arr ", "}} to display array fields but the helper was never registered. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- module/vagabond.mjs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/module/vagabond.mjs b/module/vagabond.mjs index 729fe47..e8431e3 100644 --- a/module/vagabond.mjs +++ b/module/vagabond.mjs @@ -367,6 +367,12 @@ Hooks.once("init", () => { const n = Number(num) || 0; return n >= 0 ? `+${n}` : `${n}`; }); + + // Join array elements with separator + Handlebars.registerHelper("join", (arr, separator) => { + if (!Array.isArray(arr)) return ""; + return arr.join(separator); + }); }); /* -------------------------------------------- */