/** * Favor/Hinder Debug Application for Vagabond RPG * * A development/testing tool that allows setting and viewing * favor/hinder flags on actors. Useful for testing the roll system * without a full actor sheet implementation. * * @extends ApplicationV2 * @mixes HandlebarsApplicationMixin */ const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; export default class FavorHinderDebug extends HandlebarsApplicationMixin(ApplicationV2) { constructor(options = {}) { super(options); // Default to selected token's actor, or first character actor this.selectedActorId = this._getDefaultActorId(); } /* -------------------------------------------- */ /* Static Properties */ /* -------------------------------------------- */ /** @override */ static DEFAULT_OPTIONS = { id: "vagabond-favor-hinder-debug", classes: ["vagabond", "favor-hinder-debug", "themed"], tag: "div", window: { title: "Favor/Hinder Debug", icon: "fa-solid fa-bug", resizable: true, }, position: { width: 500, height: "auto", }, }; /** @override */ static PARTS = { main: { template: "systems/vagabond/templates/dialog/favor-hinder-debug.hbs", }, }; /* -------------------------------------------- */ /* Getters */ /* -------------------------------------------- */ /** * Get the currently selected actor. * @returns {VagabondActor|null} */ get actor() { return game.actors.get(this.selectedActorId) || null; } /* -------------------------------------------- */ /* Helper Methods */ /* -------------------------------------------- */ /** * Get the default actor ID (selected token only, otherwise blank). * @returns {string|null} * @private */ _getDefaultActorId() { // Only default to selected token's actor, otherwise blank const controlled = canvas.tokens?.controlled?.[0]; if (controlled?.actor?.type === "character") { return controlled.actor.id; } return null; } /** * Get all current favor/hinder flags for an actor. * @param {VagabondActor} actor * @returns {Object} Organized flag data * @private */ _getActorFlags(actor) { if (!actor) return { skills: {}, attacks: {}, saves: {} }; const flags = { skills: {}, attacks: { favor: false, hinder: false }, saves: {}, }; // Skills for (const skillId of Object.keys(CONFIG.VAGABOND.skills)) { flags.skills[skillId] = { favor: actor.getFlag("vagabond", `favor.skills.${skillId}`) || false, hinder: actor.getFlag("vagabond", `hinder.skills.${skillId}`) || false, }; } // Attacks flags.attacks.favor = actor.getFlag("vagabond", "favor.attacks") || false; flags.attacks.hinder = actor.getFlag("vagabond", "hinder.attacks") || false; // Saves for (const saveId of Object.keys(CONFIG.VAGABOND.saves)) { flags.saves[saveId] = { favor: actor.getFlag("vagabond", `favor.saves.${saveId}`) || false, hinder: actor.getFlag("vagabond", `hinder.saves.${saveId}`) || false, }; } return flags; } /* -------------------------------------------- */ /* Data Preparation */ /* -------------------------------------------- */ /** @override */ async _prepareContext(_options) { const context = {}; // Get all character actors for dropdown context.actors = game.actors .filter((a) => a.type === "character") .map((a) => ({ id: a.id, name: a.name, selected: a.id === this.selectedActorId, })); context.selectedActorId = this.selectedActorId; context.actor = this.actor; // Get current flags if actor selected if (this.actor) { const flags = this._getActorFlags(this.actor); // Skills with labels context.skills = Object.entries(CONFIG.VAGABOND.skills).map(([id, config]) => ({ id, label: game.i18n.localize(config.label), stat: config.stat, favor: flags.skills[id]?.favor || false, hinder: flags.skills[id]?.hinder || false, })); // Attacks context.attacks = { favor: flags.attacks.favor, hinder: flags.attacks.hinder, }; // Saves with labels context.saves = Object.entries(CONFIG.VAGABOND.saves).map(([id, config]) => ({ id, label: game.i18n.localize(config.label), favor: flags.saves[id]?.favor || false, hinder: flags.saves[id]?.hinder || false, })); } return context; } /* -------------------------------------------- */ /* Event Handlers */ /* -------------------------------------------- */ /** @override */ _onRender(context, options) { super._onRender(context, options); // Apply theme class based on configured theme this._applyThemeClass(); // Actor selection dropdown const actorSelect = this.element.querySelector('[name="actorId"]'); actorSelect?.addEventListener("change", (event) => { this.selectedActorId = event.target.value; this.render(); }); // Skill checkboxes const skillCheckboxes = this.element.querySelectorAll(".skill-flag"); for (const checkbox of skillCheckboxes) { checkbox.addEventListener("change", (event) => this._onSkillFlagChange(event)); } // Attack checkboxes const attackCheckboxes = this.element.querySelectorAll(".attack-flag"); for (const checkbox of attackCheckboxes) { checkbox.addEventListener("change", (event) => this._onAttackFlagChange(event)); } // Save checkboxes const saveCheckboxes = this.element.querySelectorAll(".save-flag"); for (const checkbox of saveCheckboxes) { checkbox.addEventListener("change", (event) => this._onSaveFlagChange(event)); } // Clear all button const clearBtn = this.element.querySelector('[data-action="clear-all"]'); clearBtn?.addEventListener("click", () => this._onClearAll()); // Test roll button const testRollBtn = this.element.querySelector('[data-action="test-roll"]'); testRollBtn?.addEventListener("click", () => this._onTestRoll()); } /** * Handle skill flag checkbox change. * @param {Event} event * @private */ async _onSkillFlagChange(event) { if (!this.actor) return; const checkbox = event.currentTarget; const skillId = checkbox.dataset.skill; const flagType = checkbox.dataset.flagType; // "favor" or "hinder" const isChecked = checkbox.checked; const flagPath = `${flagType}.skills.${skillId}`; if (isChecked) { await this.actor.setFlag("vagabond", flagPath, true); } else { await this.actor.unsetFlag("vagabond", flagPath); } // Show notification const skillLabel = game.i18n.localize(CONFIG.VAGABOND.skills[skillId].label); const action = isChecked ? "added to" : "removed from"; ui.notifications.info( `${flagType.charAt(0).toUpperCase() + flagType.slice(1)} ${action} ${this.actor.name} for ${skillLabel}` ); } /** * Handle attack flag checkbox change. * @param {Event} event * @private */ async _onAttackFlagChange(event) { if (!this.actor) return; const checkbox = event.currentTarget; const flagType = checkbox.dataset.flagType; const isChecked = checkbox.checked; const flagPath = `${flagType}.attacks`; if (isChecked) { await this.actor.setFlag("vagabond", flagPath, true); } else { await this.actor.unsetFlag("vagabond", flagPath); } const action = isChecked ? "added to" : "removed from"; ui.notifications.info(`Attack ${flagType} ${action} ${this.actor.name}`); } /** * Handle save flag checkbox change. * @param {Event} event * @private */ async _onSaveFlagChange(event) { if (!this.actor) return; const checkbox = event.currentTarget; const saveId = checkbox.dataset.save; const flagType = checkbox.dataset.flagType; const isChecked = checkbox.checked; const flagPath = `${flagType}.saves.${saveId}`; if (isChecked) { await this.actor.setFlag("vagabond", flagPath, true); } else { await this.actor.unsetFlag("vagabond", flagPath); } const saveLabel = game.i18n.localize(CONFIG.VAGABOND.saves[saveId].label); const action = isChecked ? "added to" : "removed from"; ui.notifications.info( `${flagType.charAt(0).toUpperCase() + flagType.slice(1)} ${action} ${this.actor.name} for ${saveLabel} save` ); } /** * Clear all favor/hinder flags from the selected actor. * @private */ async _onClearAll() { if (!this.actor) return; // Clear all flags by unsetting the root favor/hinder objects await this.actor.unsetFlag("vagabond", "favor"); await this.actor.unsetFlag("vagabond", "hinder"); ui.notifications.info(`Cleared all favor/hinder flags from ${this.actor.name}`); this.render(); } /** * Open a skill check dialog for testing. * @private */ async _onTestRoll() { if (!this.actor) { ui.notifications.warn("Select an actor first"); return; } // Import and open the skill check dialog const { SkillCheckDialog } = game.vagabond.applications; SkillCheckDialog.prompt(this.actor); } /** * Apply the configured theme class to the dialog element. * Foundry v13 doesn't automatically add theme classes to ApplicationV2, * so we handle it manually. * @protected */ _applyThemeClass() { if (!this.element) return; // Remove any existing theme classes this.element.classList.remove("theme-light", "theme-dark"); // Check global preference 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, use default } // Apply the theme class if (theme === "dark") { this.element.classList.add("theme-dark"); } else if (theme === "light") { this.element.classList.add("theme-light"); } } /* -------------------------------------------- */ /* Static Methods */ /* -------------------------------------------- */ /** * Open the debug panel. * @returns {Promise} */ static async open() { const app = new this(); return app.render(true); } }