vagabond-rpg-foundryvtt/module/applications/base-roll-dialog.mjs
Cal Corum 463a130c18 Implement skill check system with roll dialogs and debug tools
Phase 2.5: Skill Check System Implementation

Features:
- ApplicationV2-based roll dialogs with HandlebarsApplicationMixin
- Base VagabondRollDialog class for shared dialog functionality
- SkillCheckDialog for skill checks with auto-calculated difficulty
- Favor/Hinder system using Active Effects flags (simplified from schema)
- FavorHinderDebug panel for testing flags without actor sheets
- Auto-created development macros (Favor/Hinder Debug, Skill Check)
- Custom chat cards for skill roll results

Technical Changes:
- Removed favorHinder from character schema (now uses flags)
- Updated getNetFavorHinder() to use flag-based approach
- Returns { net, favorSources, hinderSources } for transparency
- Universal form styling fixes for Foundry dark theme compatibility
- Added Macro to ESLint globals

Flag Convention:
- flags.vagabond.favor.skills.<skillId>
- flags.vagabond.hinder.skills.<skillId>
- flags.vagabond.favor.attacks
- flags.vagabond.hinder.attacks
- flags.vagabond.favor.saves.<saveType>
- flags.vagabond.hinder.saves.<saveType>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 17:31:15 -06:00

256 lines
7.0 KiB
JavaScript

/**
* Base Roll Dialog for Vagabond RPG
*
* Provides common UI elements for all roll dialogs:
* - Favor/Hinder toggles
* - Situational modifier input (presets + custom)
* - Roll button
*
* Subclasses (SkillCheckDialog, AttackRollDialog, SaveRollDialog) extend this
* to add roll-type-specific configuration.
*
* Uses Foundry VTT v13 ApplicationV2 API.
*
* @extends ApplicationV2
* @mixes HandlebarsApplicationMixin
*/
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class VagabondRollDialog extends HandlebarsApplicationMixin(ApplicationV2) {
/**
* @param {VagabondActor} actor - The actor making the roll
* @param {Object} options - Dialog options
* @param {string} [options.title] - Dialog title
* @param {Function} [options.onRoll] - Callback when roll is executed
*/
constructor(actor, options = {}) {
super(options);
this.actor = actor;
this.onRollCallback = options.onRoll || null;
// Roll configuration state
this.rollConfig = {
favorHinder: 0, // -1, 0, or +1
modifier: 0, // Situational modifier
autoFavorHinder: { net: 0, favorSources: [], hinderSources: [] },
};
}
/* -------------------------------------------- */
/* Static Properties */
/* -------------------------------------------- */
/** @override */
static DEFAULT_OPTIONS = {
id: "vagabond-roll-dialog",
classes: ["vagabond", "roll-dialog"],
tag: "form",
window: {
title: "VAGABOND.RollDialog",
icon: "fa-solid fa-dice-d20",
resizable: false,
},
position: {
width: 320,
height: "auto",
},
form: {
handler: VagabondRollDialog.#onSubmit,
submitOnChange: false,
closeOnSubmit: true,
},
};
/** @override */
static PARTS = {
form: {
template: "systems/vagabond/templates/dialog/roll-dialog-base.hbs",
},
};
/* -------------------------------------------- */
/* Getters */
/* -------------------------------------------- */
/**
* Get the title for this dialog.
* Subclasses should override this.
* @returns {string}
*/
get title() {
return game.i18n.localize("VAGABOND.RollDialog");
}
/**
* Get the net favor/hinder value (manual + automatic).
* @returns {number} -1, 0, or +1
*/
get netFavorHinder() {
const manual = this.rollConfig.favorHinder;
const auto = this.rollConfig.autoFavorHinder.net;
return Math.clamp(manual + auto, -1, 1);
}
/* -------------------------------------------- */
/* Data Preparation */
/* -------------------------------------------- */
/** @override */
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.actor = this.actor;
context.config = this.rollConfig;
context.netFavorHinder = this.netFavorHinder;
// Automatic favor/hinder from Active Effects
context.autoFavorHinder = this.rollConfig.autoFavorHinder;
context.hasAutoFavor = this.rollConfig.autoFavorHinder.favorSources.length > 0;
context.hasAutoHinder = this.rollConfig.autoFavorHinder.hinderSources.length > 0;
// Modifier presets
context.modifierPresets = [
{ value: -5, label: "-5" },
{ value: -1, label: "-1" },
{ value: 1, label: "+1" },
{ value: 5, label: "+5" },
];
// Subclass-specific context
context.rollSpecific = await this._prepareRollContext(options);
return context;
}
/**
* Prepare roll-type-specific context data.
* Subclasses should override this.
*
* @param {Object} options - Render options
* @returns {Promise<Object>} Additional context data
* @protected
*/
async _prepareRollContext(_options) {
return {};
}
/* -------------------------------------------- */
/* Event Handlers */
/* -------------------------------------------- */
/** @override */
_onRender(context, options) {
super._onRender(context, options);
// Favor/Hinder toggle buttons
const favorBtn = this.element.querySelector('[data-action="toggle-favor"]');
const hinderBtn = this.element.querySelector('[data-action="toggle-hinder"]');
favorBtn?.addEventListener("click", () => this._onToggleFavor());
hinderBtn?.addEventListener("click", () => this._onToggleHinder());
// Modifier preset buttons
const presetBtns = this.element.querySelectorAll("[data-modifier-preset]");
for (const btn of presetBtns) {
btn.addEventListener("click", (event) => {
const value = parseInt(event.currentTarget.dataset.modifierPreset, 10);
this._onModifierPreset(value);
});
}
// Custom modifier input
const modifierInput = this.element.querySelector('[name="modifier"]');
modifierInput?.addEventListener("change", (event) => {
this.rollConfig.modifier = parseInt(event.target.value, 10) || 0;
});
}
/**
* Toggle favor on/off.
* @private
*/
_onToggleFavor() {
if (this.rollConfig.favorHinder === 1) {
this.rollConfig.favorHinder = 0;
} else {
this.rollConfig.favorHinder = 1;
}
this.render();
}
/**
* Toggle hinder on/off.
* @private
*/
_onToggleHinder() {
if (this.rollConfig.favorHinder === -1) {
this.rollConfig.favorHinder = 0;
} else {
this.rollConfig.favorHinder = -1;
}
this.render();
}
/**
* Apply a modifier preset.
* @param {number} value - The preset value
* @private
*/
_onModifierPreset(value) {
this.rollConfig.modifier += value;
this.render();
}
/**
* Handle form submission (roll button).
* @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) {
// 'this' is the dialog instance
const dialog = this;
const data = foundry.utils.expandObject(formData.object);
// Update modifier from form
dialog.rollConfig.modifier = parseInt(data.modifier, 10) || 0;
// Execute the roll
await dialog._executeRoll();
// Call the callback if provided
if (dialog.onRollCallback) {
dialog.onRollCallback(dialog.rollConfig);
}
}
/**
* Execute the roll with current configuration.
* Subclasses must override this.
*
* @returns {Promise<void>}
* @protected
*/
async _executeRoll() {
throw new Error("Subclasses must implement _executeRoll()");
}
/* -------------------------------------------- */
/* Static Methods */
/* -------------------------------------------- */
/**
* Create and render a roll dialog.
*
* @param {VagabondActor} actor - The actor making the roll
* @param {Object} options - Dialog options
* @returns {Promise<VagabondRollDialog>} The rendered dialog
*/
static async create(actor, options = {}) {
const dialog = new this(actor, options);
return dialog.render(true);
}
}