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>
This commit is contained in:
Cal Corum 2025-12-13 17:31:15 -06:00
parent 517b7045c7
commit 463a130c18
21 changed files with 2606 additions and 95 deletions

View File

@ -55,6 +55,7 @@
"Scene": "readonly", "Scene": "readonly",
"User": "readonly", "User": "readonly",
"Folder": "readonly", "Folder": "readonly",
"Macro": "readonly",
"Compendium": "readonly", "Compendium": "readonly",
"CompendiumCollection": "readonly", "CompendiumCollection": "readonly",
"DocumentSheetConfig": "readonly", "DocumentSheetConfig": "readonly",

View File

@ -290,6 +290,67 @@ docs: Update README with installation instructions
--- ---
## Architecture Decisions
### Roll Dialog System (Phase 2.5+)
**Decision Date:** 2024-12-13
| Decision | Choice | Rationale |
| --------------------- | ----------------------------------- | ------------------------------------------------------------------------------------ |
| Application API | **ApplicationV2** | Modern Foundry v13 API, forward-compatible, cleaner lifecycle |
| Dialog Behavior | **Hybrid** | Normal click = dialog, Shift+click = quick roll with defaults |
| Favor/Hinder | **Manual toggles + Active Effects** | GM announces situational favor/hinder; persistent sources use flags |
| Situational Modifiers | **Preset buttons + custom input** | Buttons: -5, -1, +1, +5; plus free-form input field |
| Dialog Structure | **Base class + subclasses** | `VagabondRollDialog` base → `SkillCheckDialog`, `AttackRollDialog`, `SaveRollDialog` |
| Chat Output | **Custom templates** | Rich chat cards with skill name, difficulty, success/fail, crit indicators |
### Favor/Hinder via Active Effects
Persistent favor/hinder from class features, perks, or conditions uses Foundry flags set by Active Effects:
```javascript
// Active Effect change for a perk granting Favor on Performance checks:
{ key: "flags.vagabond.favor.skills.performance", mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE, value: true }
// Active Effect change for Hinder on all attack rolls:
{ key: "flags.vagabond.hinder.attacks", mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE, value: true }
// Active Effect change for Favor on Reflex saves:
{ key: "flags.vagabond.favor.saves.reflex", mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE, value: true }
```
**Flag Convention:**
- `flags.vagabond.favor.skills.<skillId>` - Favor on specific skill checks
- `flags.vagabond.hinder.skills.<skillId>` - Hinder on specific skill checks
- `flags.vagabond.favor.attacks` - Favor on all attack rolls
- `flags.vagabond.hinder.attacks` - Hinder on all attack rolls
- `flags.vagabond.favor.saves.<saveType>` - Favor on specific save type
- `flags.vagabond.hinder.saves.<saveType>` - Hinder on specific save type
The roll dialog checks these flags and displays any automatic favor/hinder, while still allowing manual override for situational modifiers announced by the GM.
### File Structure for Dialogs
```
module/applications/
├── _module.mjs # Export barrel
├── base-roll-dialog.mjs # VagabondRollDialog (ApplicationV2 base)
├── skill-check-dialog.mjs # SkillCheckDialog extends VagabondRollDialog
├── attack-roll-dialog.mjs # AttackRollDialog (Phase 2.6)
└── save-roll-dialog.mjs # SaveRollDialog (Phase 2.7)
templates/dialog/
├── roll-dialog-base.hbs # Shared dialog structure
└── skill-check.hbs # Skill-specific content
templates/chat/
└── skill-roll.hbs # Skill check result card
```
---
## Resources ## Resources
- [Foundry VTT API Documentation](https://foundryvtt.com/api/) - [Foundry VTT API Documentation](https://foundryvtt.com/api/)

View File

@ -239,7 +239,7 @@
"id": "2.1", "id": "2.1",
"name": "Create main system entry point (vagabond.mjs)", "name": "Create main system entry point (vagabond.mjs)",
"description": "System initialization, hook registration, CONFIG setup, document class registration", "description": "System initialization, hook registration, CONFIG setup, document class registration",
"completed": false, "completed": true,
"tested": false, "tested": false,
"priority": "critical", "priority": "critical",
"dependencies": ["1.1", "1.6"] "dependencies": ["1.1", "1.6"]
@ -248,7 +248,7 @@
"id": "2.2", "id": "2.2",
"name": "Implement VagabondActor class", "name": "Implement VagabondActor class",
"description": "Extended Actor with prepareData for derived values calculation (HP, Speed, Saves, Skill difficulties)", "description": "Extended Actor with prepareData for derived values calculation (HP, Speed, Saves, Skill difficulties)",
"completed": false, "completed": true,
"tested": false, "tested": false,
"priority": "critical", "priority": "critical",
"dependencies": ["2.1", "1.2", "1.3"] "dependencies": ["2.1", "1.2", "1.3"]
@ -257,7 +257,7 @@
"id": "2.3", "id": "2.3",
"name": "Implement VagabondItem class", "name": "Implement VagabondItem class",
"description": "Extended Item with type-specific preparation and chat card generation", "description": "Extended Item with type-specific preparation and chat card generation",
"completed": false, "completed": true,
"tested": false, "tested": false,
"priority": "critical", "priority": "critical",
"dependencies": ["2.1", "1.6"] "dependencies": ["2.1", "1.6"]
@ -266,7 +266,7 @@
"id": "2.4", "id": "2.4",
"name": "Create dice rolling module", "name": "Create dice rolling module",
"description": "Core roll functions: d20 checks, damage rolls, exploding dice (d6!), countdown dice, favor/hinder modifiers", "description": "Core roll functions: d20 checks, damage rolls, exploding dice (d6!), countdown dice, favor/hinder modifiers",
"completed": false, "completed": true,
"tested": false, "tested": false,
"priority": "critical", "priority": "critical",
"dependencies": ["2.1"] "dependencies": ["2.1"]

View File

@ -163,6 +163,7 @@
"VAGABOND.Success": "Success", "VAGABOND.Success": "Success",
"VAGABOND.Failure": "Failure", "VAGABOND.Failure": "Failure",
"VAGABOND.Critical": "Critical!", "VAGABOND.Critical": "Critical!",
"VAGABOND.Fumble": "Fumble!",
"VAGABOND.Damage": "Damage", "VAGABOND.Damage": "Damage",
"VAGABOND.DamageType": "Damage Type", "VAGABOND.DamageType": "Damage Type",
@ -209,5 +210,23 @@
"VAGABOND.ItemTypeFeature": "Feature", "VAGABOND.ItemTypeFeature": "Feature",
"VAGABOND.ItemTypeWeapon": "Weapon", "VAGABOND.ItemTypeWeapon": "Weapon",
"VAGABOND.ItemTypeArmor": "Armor", "VAGABOND.ItemTypeArmor": "Armor",
"VAGABOND.ItemTypeEquipment": "Equipment" "VAGABOND.ItemTypeEquipment": "Equipment",
"VAGABOND.RollDialog": "Roll",
"VAGABOND.SkillCheck": "Skill Check",
"VAGABOND.Check": "Check",
"VAGABOND.Skill": "Skill",
"VAGABOND.Stat": "Stat",
"VAGABOND.Training": "Training",
"VAGABOND.Untrained": "Untrained",
"VAGABOND.SelectSkill": "Select Skill...",
"VAGABOND.SelectSkillFirst": "Please select a skill first",
"VAGABOND.FavorHinder": "Favor / Hinder",
"VAGABOND.SituationalModifier": "Situational Modifier",
"VAGABOND.AutoFavor": "Auto Favor",
"VAGABOND.AutoHinder": "Auto Hinder",
"VAGABOND.Formula": "Formula",
"VAGABOND.SelectActor": "Select Actor",
"VAGABOND.Save": "Save"
} }

View File

@ -0,0 +1,8 @@
/**
* Application classes for Vagabond RPG
* @module applications
*/
export { default as VagabondRollDialog } from "./base-roll-dialog.mjs";
export { default as SkillCheckDialog } from "./skill-check-dialog.mjs";
export { default as FavorHinderDebug } from "./favor-hinder-debug.mjs";

View File

@ -0,0 +1,255 @@
/**
* 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);
}
}

View File

@ -0,0 +1,334 @@
/**
* 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"],
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);
// 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);
}
/* -------------------------------------------- */
/* Static Methods */
/* -------------------------------------------- */
/**
* Open the debug panel.
* @returns {Promise<FavorHinderDebug>}
*/
static async open() {
const app = new this();
return app.render(true);
}
}

View File

@ -0,0 +1,248 @@
/**
* Skill Check Dialog for Vagabond RPG
*
* Extends VagabondRollDialog to handle skill check configuration:
* - Skill selection (if not pre-selected)
* - Displays calculated difficulty
* - Displays crit threshold
* - Shows trained/untrained status
*
* @extends VagabondRollDialog
*/
import VagabondRollDialog from "./base-roll-dialog.mjs";
import { skillCheck } from "../dice/rolls.mjs";
export default class SkillCheckDialog extends VagabondRollDialog {
/**
* @param {VagabondActor} actor - The actor making the roll
* @param {Object} options - Dialog options
* @param {string} [options.skillId] - Pre-selected skill ID
*/
constructor(actor, options = {}) {
super(actor, options);
this.skillId = options.skillId || null;
// Load automatic favor/hinder for this skill
if (this.skillId) {
this.rollConfig.autoFavorHinder = actor.getNetFavorHinder({ skillId: this.skillId });
}
}
/* -------------------------------------------- */
/* Static Properties */
/* -------------------------------------------- */
/** @override */
static DEFAULT_OPTIONS = foundry.utils.mergeObject(
super.DEFAULT_OPTIONS,
{
id: "vagabond-skill-check-dialog",
window: {
title: "VAGABOND.SkillCheck",
icon: "fa-solid fa-dice-d20",
},
},
{ inplace: false }
);
/** @override */
static PARTS = {
form: {
template: "systems/vagabond/templates/dialog/skill-check.hbs",
},
};
/* -------------------------------------------- */
/* Getters */
/* -------------------------------------------- */
/** @override */
get title() {
if (this.skillId) {
const skillLabel = CONFIG.VAGABOND?.skills?.[this.skillId]?.label || this.skillId;
return `${game.i18n.localize(skillLabel)} ${game.i18n.localize("VAGABOND.Check")}`;
}
return game.i18n.localize("VAGABOND.SkillCheck");
}
/**
* Get the current skill data.
* @returns {Object|null} Skill data from actor
*/
get skillData() {
if (!this.skillId) return null;
return this.actor.system.skills?.[this.skillId] || null;
}
/* -------------------------------------------- */
/* Data Preparation */
/* -------------------------------------------- */
/** @override */
async _prepareRollContext(_options) {
const context = {};
// Available skills for dropdown (if no skill pre-selected)
context.skills = Object.entries(CONFIG.VAGABOND?.skills || {}).map(([id, config]) => {
const skillData = this.actor.system.skills?.[id] || {};
return {
id,
label: game.i18n.localize(config.label),
stat: config.stat,
trained: skillData.trained || false,
difficulty: skillData.difficulty || 20,
critThreshold: skillData.critThreshold || 20,
selected: id === this.skillId,
};
});
// Selected skill info
context.selectedSkill = this.skillId;
context.skillData = this.skillData;
if (this.skillData) {
context.difficulty = this.skillData.difficulty;
context.critThreshold = this.skillData.critThreshold || 20;
context.trained = this.skillData.trained;
// Get the associated stat
const statKey = CONFIG.VAGABOND?.skills?.[this.skillId]?.stat;
if (statKey) {
context.statLabel = game.i18n.localize(CONFIG.VAGABOND?.stats?.[statKey]?.label || statKey);
context.statValue = this.actor.system.stats?.[statKey]?.value || 0;
}
}
return context;
}
/* -------------------------------------------- */
/* Event Handlers */
/* -------------------------------------------- */
/** @override */
_onRender(context, options) {
super._onRender(context, options);
// Skill selection dropdown
const skillSelect = this.element.querySelector('[name="skillId"]');
skillSelect?.addEventListener("change", (event) => {
this.skillId = event.target.value;
this.rollConfig.autoFavorHinder = this.actor.getNetFavorHinder({ skillId: this.skillId });
this.render();
});
}
/** @override */
async _executeRoll() {
if (!this.skillId) {
ui.notifications.warn(game.i18n.localize("VAGABOND.SelectSkillFirst"));
return;
}
// Perform the skill check
const result = await skillCheck(this.actor, this.skillId, {
favorHinder: this.netFavorHinder,
modifier: this.rollConfig.modifier,
});
// Send to chat with custom template
await this._sendToChat(result);
}
/**
* Send the roll result to chat.
*
* @param {VagabondRollResult} result - The roll result
* @returns {Promise<ChatMessage>}
* @private
*/
async _sendToChat(result) {
const skillLabel = game.i18n.localize(
CONFIG.VAGABOND?.skills?.[this.skillId]?.label || this.skillId
);
// Prepare template data
const templateData = {
actor: this.actor,
skillId: this.skillId,
skillLabel,
trained: this.skillData?.trained || false,
difficulty: result.difficulty,
critThreshold: result.critThreshold,
total: result.total,
d20Result: result.d20Result,
favorDie: result.favorDie,
modifier: this.rollConfig.modifier,
success: result.success,
isCrit: result.isCrit,
isFumble: result.isFumble,
formula: result.roll.formula,
netFavorHinder: this.netFavorHinder,
favorSources: this.rollConfig.autoFavorHinder.favorSources,
hinderSources: this.rollConfig.autoFavorHinder.hinderSources,
};
// Render the chat card template
const content = await renderTemplate(
"systems/vagabond/templates/chat/skill-roll.hbs",
templateData
);
// Create the chat message
const chatData = {
user: game.user.id,
speaker: ChatMessage.getSpeaker({ actor: this.actor }),
content,
rolls: [result.roll],
sound: CONFIG.sounds.dice,
};
return ChatMessage.create(chatData);
}
/* -------------------------------------------- */
/* Static Methods */
/* -------------------------------------------- */
/**
* Create and render a skill check dialog.
*
* @param {VagabondActor} actor - The actor making the roll
* @param {string} [skillId] - Optional pre-selected skill
* @param {Object} [options] - Additional options
* @returns {Promise<SkillCheckDialog>}
*/
static async prompt(actor, skillId = null, options = {}) {
return this.create(actor, { ...options, skillId });
}
/**
* Perform a quick roll without showing the dialog.
* Used for Shift+click fast rolling.
*
* @param {VagabondActor} actor - The actor making the roll
* @param {string} skillId - The skill to check
* @param {Object} [options] - Roll options
* @returns {Promise<VagabondRollResult>}
*/
static async quickRoll(actor, skillId, options = {}) {
// Get automatic favor/hinder
const autoFavorHinder = actor.getNetFavorHinder({ skillId });
// Perform the roll
const result = await skillCheck(actor, skillId, {
favorHinder: options.favorHinder ?? autoFavorHinder.net,
modifier: options.modifier || 0,
});
// Create a temporary dialog instance just for chat output
const tempDialog = new this(actor, { skillId });
tempDialog.rollConfig.autoFavorHinder = autoFavorHinder;
await tempDialog._sendToChat(result);
return result;
}
}

View File

@ -367,32 +367,14 @@ export default class CharacterData extends VagabondActorBase {
}), }),
}), }),
// Favor/Hinder tracking (d20 +/- d6 modifiers) // NOTE: Favor/Hinder is now handled via Active Effects flags instead of a data schema.
// Cancel each other 1-for-1, don't stack // See DEVELOPMENT.md "Favor/Hinder via Active Effects" for the flag convention:
favorHinder: new fields.SchemaField({ // - flags.vagabond.favor.skills.<skillId>
favor: new fields.ArrayField( // - flags.vagabond.hinder.skills.<skillId>
new fields.SchemaField({ // - flags.vagabond.favor.attacks
source: new fields.StringField({ required: true }), // "Flanking", "Virtuoso", etc. // - flags.vagabond.hinder.attacks
appliesTo: new fields.ArrayField(new fields.StringField()), // ["Attack Checks"], ["Reflex Saves"] // - flags.vagabond.favor.saves.<saveType>
duration: new fields.StringField({ // - flags.vagabond.hinder.saves.<saveType>
initial: "instant",
choices: ["instant", "until-next-turn", "focus", "continual", "permanent"],
}),
}),
{ initial: [] }
),
hinder: new fields.ArrayField(
new fields.SchemaField({
source: new fields.StringField({ required: true }), // "Heavy Armor", "Fog spell", etc.
appliesTo: new fields.ArrayField(new fields.StringField()), // ["Dodge Saves"], ["sight-based checks"]
duration: new fields.StringField({
initial: "instant",
choices: ["instant", "until-next-turn", "focus", "continual", "permanent"],
}),
}),
{ initial: [] }
),
}),
// Focus tracking for maintained spells // Focus tracking for maintained spells
focus: new fields.SchemaField({ focus: new fields.SchemaField({

View File

@ -130,8 +130,9 @@ export async function skillCheck(actor, skillId, options = {}) {
const difficulty = skillData.difficulty; const difficulty = skillData.difficulty;
const critThreshold = skillData.critThreshold || 20; const critThreshold = skillData.critThreshold || 20;
// Determine favor/hinder // Determine favor/hinder from Active Effect flags or override
const favorHinder = options.favorHinder ?? actor.getNetFavorHinder?.(`${skillId} Checks`) ?? 0; const favorHinderResult = actor.getNetFavorHinder?.({ skillId }) ?? { net: 0 };
const favorHinder = options.favorHinder ?? favorHinderResult.net;
return d20Check({ return d20Check({
difficulty, difficulty,
@ -170,8 +171,9 @@ export async function attackCheck(actor, weapon, options = {}) {
// Get crit threshold from attack data // Get crit threshold from attack data
const critThreshold = system.attacks?.[attackType]?.critThreshold || 20; const critThreshold = system.attacks?.[attackType]?.critThreshold || 20;
// Determine favor/hinder // Determine favor/hinder from Active Effect flags or override
const favorHinder = options.favorHinder ?? actor.getNetFavorHinder?.("Attack Checks") ?? 0; const favorHinderResult = actor.getNetFavorHinder?.({ isAttack: true }) ?? { net: 0 };
const favorHinder = options.favorHinder ?? favorHinderResult.net;
return d20Check({ return d20Check({
difficulty, difficulty,
@ -200,12 +202,9 @@ export async function saveRoll(actor, saveType, difficulty, options = {}) {
throw new Error(`Unknown save type: ${saveType}`); throw new Error(`Unknown save type: ${saveType}`);
} }
// Determine favor/hinder based on save type // Determine favor/hinder from Active Effect flags or override
let rollType = `${saveType.charAt(0).toUpperCase() + saveType.slice(1)} Saves`; const favorHinderResult = actor.getNetFavorHinder?.({ saveType }) ?? { net: 0 };
if (options.isBlock) rollType = "Block Saves"; const favorHinder = options.favorHinder ?? favorHinderResult.net;
if (options.isDodge) rollType = "Dodge Saves";
const favorHinder = options.favorHinder ?? actor.getNetFavorHinder?.(rollType) ?? 0;
return d20Check({ return d20Check({
difficulty, difficulty,

View File

@ -464,29 +464,99 @@ export default class VagabondActor extends Actor {
/** /**
* Get the net favor/hinder for a specific roll type. * Get the net favor/hinder for a specific roll type.
* Favor and Hinder cancel 1-for-1. * Checks Active Effect flags for persistent favor/hinder sources.
* Favor and Hinder cancel 1-for-1, capped at +1 or -1.
* *
* @param {string} rollType - The type of roll (e.g., "Attack Checks", "Reflex Saves") * Flag convention (set by Active Effects):
* @returns {number} Net modifier: positive = favor, negative = hinder, 0 = neutral * - flags.vagabond.favor.skills.<skillId> - Favor on specific skill
* - flags.vagabond.hinder.skills.<skillId> - Hinder on specific skill
* - flags.vagabond.favor.attacks - Favor on attack rolls
* - flags.vagabond.hinder.attacks - Hinder on attack rolls
* - flags.vagabond.favor.saves.<saveType> - Favor on specific save
* - flags.vagabond.hinder.saves.<saveType> - Hinder on specific save
*
* @param {Object} options - Options for determining favor/hinder
* @param {string} [options.skillId] - Skill ID for skill checks (e.g., "arcana", "brawl")
* @param {boolean} [options.isAttack] - True if this is an attack roll
* @param {string} [options.saveType] - Save type (e.g., "reflex", "endure", "will")
* @returns {Object} Result with net value and sources
* @returns {number} result.net - Net modifier: +1 (favor), 0 (neutral), -1 (hinder)
* @returns {string[]} result.favorSources - Names of active favor sources
* @returns {string[]} result.hinderSources - Names of active hinder sources
*/ */
getNetFavorHinder(rollType) { getNetFavorHinder({ skillId = null, isAttack = false, saveType = null } = {}) {
if (this.type !== "character") return 0; if (this.type !== "character") return { net: 0, favorSources: [], hinderSources: [] };
const favorHinder = this.system.favorHinder; const favorSources = [];
if (!favorHinder) return 0; const hinderSources = [];
// Count favor sources that apply to this roll type // Check skill-specific flags
const favorCount = (favorHinder.favor || []).filter( if (skillId) {
(f) => !f.appliesTo?.length || f.appliesTo.includes(rollType) if (this.getFlag("vagabond", `favor.skills.${skillId}`)) {
).length; favorSources.push(this._getFavorHinderSourceName("favor", "skills", skillId));
}
if (this.getFlag("vagabond", `hinder.skills.${skillId}`)) {
hinderSources.push(this._getFavorHinderSourceName("hinder", "skills", skillId));
}
}
// Count hinder sources that apply to this roll type // Check attack flags
const hinderCount = (favorHinder.hinder || []).filter( if (isAttack) {
(h) => !h.appliesTo?.length || h.appliesTo.includes(rollType) if (this.getFlag("vagabond", "favor.attacks")) {
).length; favorSources.push(this._getFavorHinderSourceName("favor", "attacks"));
}
if (this.getFlag("vagabond", "hinder.attacks")) {
hinderSources.push(this._getFavorHinderSourceName("hinder", "attacks"));
}
}
// Check save-specific flags
if (saveType) {
if (this.getFlag("vagabond", `favor.saves.${saveType}`)) {
favorSources.push(this._getFavorHinderSourceName("favor", "saves", saveType));
}
if (this.getFlag("vagabond", `hinder.saves.${saveType}`)) {
hinderSources.push(this._getFavorHinderSourceName("hinder", "saves", saveType));
}
}
// They cancel 1-for-1, max of +1 or -1 // They cancel 1-for-1, max of +1 or -1
const net = favorCount - hinderCount; const net = Math.clamp(favorSources.length - hinderSources.length, -1, 1);
return Math.clamp(net, -1, 1);
return { net, favorSources, hinderSources };
}
/**
* Get the source name for a favor/hinder flag by finding the Active Effect that set it.
*
* @param {string} type - "favor" or "hinder"
* @param {string} category - "skills", "attacks", or "saves"
* @param {string} [subtype] - Skill ID or save type
* @returns {string} Source name or generic description
* @private
*/
_getFavorHinderSourceName(type, category, subtype = null) {
const flagKey = subtype
? `flags.vagabond.${type}.${category}.${subtype}`
: `flags.vagabond.${type}.${category}`;
// Find the Active Effect that sets this flag
for (const effect of this.effects) {
if (!effect.active) continue;
for (const change of effect.changes) {
if (change.key === flagKey) {
return effect.name || effect.parent?.name || `${type} effect`;
}
}
}
// Fallback if source not found
const categoryLabel =
category === "skills"
? `${subtype} checks`
: category === "saves"
? `${subtype} saves`
: category;
return `${type} on ${categoryLabel}`;
} }
} }

View File

@ -260,32 +260,78 @@ export function registerActorTests(quenchRunner) {
}); });
describe("Favor/Hinder System", () => { describe("Favor/Hinder System", () => {
it("tracks favor and hinder modifiers separately", async () => { it("detects favor from Active Effect flags", async () => {
/** /**
* Favor adds +d6 to rolls, Hinder adds -d6. * Favor/Hinder is now tracked via Active Effect flags instead of data schema.
* They cancel 1-for-1 and don't stack (multiple favors = still +1d6). * Flag convention: flags.vagabond.favor.skills.<skillId>
* Each entry tracks: source, appliesTo (what rolls), duration. * The getNetFavorHinder method checks these flags.
*/ */
await testActor.update({ // Set a flag directly (simulating what an Active Effect would do)
"system.favorHinder.favor": [ await testActor.setFlag("vagabond", "favor.skills.performance", true);
{
source: "Flanking", const result = testActor.getNetFavorHinder({ skillId: "performance" });
appliesTo: ["Attack Checks"], expect(result.net).to.equal(1);
duration: "until-next-turn", expect(result.favorSources.length).to.equal(1);
},
], // Clean up
"system.favorHinder.hinder": [ await testActor.unsetFlag("vagabond", "favor.skills.performance");
{
source: "Heavy Armor",
appliesTo: ["Dodge Saves"],
duration: "permanent",
},
],
}); });
expect(testActor.system.favorHinder.favor.length).to.equal(1); it("detects hinder from Active Effect flags", async () => {
expect(testActor.system.favorHinder.hinder.length).to.equal(1); /**
expect(testActor.system.favorHinder.favor[0].source).to.equal("Flanking"); * Hinder flags work the same way as favor flags.
* Flag convention: flags.vagabond.hinder.skills.<skillId>
*/
await testActor.setFlag("vagabond", "hinder.skills.sneak", true);
const result = testActor.getNetFavorHinder({ skillId: "sneak" });
expect(result.net).to.equal(-1);
expect(result.hinderSources.length).to.equal(1);
// Clean up
await testActor.unsetFlag("vagabond", "hinder.skills.sneak");
});
it("cancels favor and hinder 1-for-1", async () => {
/**
* When both favor and hinder apply to the same roll, they cancel out.
* Net result is clamped to -1, 0, or +1.
*/
await testActor.setFlag("vagabond", "favor.skills.arcana", true);
await testActor.setFlag("vagabond", "hinder.skills.arcana", true);
const result = testActor.getNetFavorHinder({ skillId: "arcana" });
expect(result.net).to.equal(0);
// Clean up
await testActor.unsetFlag("vagabond", "favor.skills.arcana");
await testActor.unsetFlag("vagabond", "hinder.skills.arcana");
});
it("detects favor/hinder for attack rolls", async () => {
/**
* Attack rolls check flags.vagabond.favor.attacks and hinder.attacks.
*/
await testActor.setFlag("vagabond", "favor.attacks", true);
const result = testActor.getNetFavorHinder({ isAttack: true });
expect(result.net).to.equal(1);
// Clean up
await testActor.unsetFlag("vagabond", "favor.attacks");
});
it("detects favor/hinder for save rolls", async () => {
/**
* Save rolls check flags.vagabond.favor.saves.<saveType>.
*/
await testActor.setFlag("vagabond", "hinder.saves.reflex", true);
const result = testActor.getNetFavorHinder({ saveType: "reflex" });
expect(result.net).to.equal(-1);
// Clean up
await testActor.unsetFlag("vagabond", "hinder.saves.reflex");
}); });
}); });

View File

@ -22,6 +22,9 @@ import {
// Import document classes // Import document classes
import { VagabondActor, VagabondItem } from "./documents/_module.mjs"; import { VagabondActor, VagabondItem } from "./documents/_module.mjs";
// Import application classes
import { VagabondRollDialog, SkillCheckDialog, FavorHinderDebug } from "./applications/_module.mjs";
// Import sheet classes // Import sheet classes
// import { VagabondCharacterSheet } from "./sheets/actor-sheet.mjs"; // import { VagabondCharacterSheet } from "./sheets/actor-sheet.mjs";
// import { VagabondItemSheet } from "./sheets/item-sheet.mjs"; // import { VagabondItemSheet } from "./sheets/item-sheet.mjs";
@ -46,6 +49,15 @@ Hooks.once("init", () => {
// Add custom constants for configuration // Add custom constants for configuration
CONFIG.VAGABOND = VAGABOND; CONFIG.VAGABOND = VAGABOND;
// Expose application classes globally for macro/API access
game.vagabond = {
applications: {
VagabondRollDialog,
SkillCheckDialog,
FavorHinderDebug,
},
};
// Register Actor data models // Register Actor data models
CONFIG.Actor.dataModels = { CONFIG.Actor.dataModels = {
character: CharacterData, character: CharacterData,
@ -93,7 +105,7 @@ Hooks.once("init", () => {
/** /**
* Ready hook - runs when Foundry is fully loaded * Ready hook - runs when Foundry is fully loaded
*/ */
Hooks.once("ready", () => { Hooks.once("ready", async () => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log("Vagabond RPG | System Ready"); console.log("Vagabond RPG | System Ready");
@ -101,9 +113,58 @@ Hooks.once("ready", () => {
if (game.user.isGM) { if (game.user.isGM) {
const version = game.system.version; const version = game.system.version;
ui.notifications.info(`Vagabond RPG v${version} - System loaded successfully!`); ui.notifications.info(`Vagabond RPG v${version} - System loaded successfully!`);
// Create development macros if they don't exist
await _createDevMacros();
} }
}); });
/**
* Create development/debug macros if they don't already exist.
* @private
*/
async function _createDevMacros() {
// Favor/Hinder Debug macro
const debugMacroName = "Favor/Hinder Debug";
const existingMacro = game.macros.find((m) => m.name === debugMacroName);
if (!existingMacro) {
await Macro.create({
name: debugMacroName,
type: "script",
img: "icons/svg/bug.svg",
command: "game.vagabond.applications.FavorHinderDebug.open();",
flags: { vagabond: { systemMacro: true } },
});
// eslint-disable-next-line no-console
console.log("Vagabond RPG | Created Favor/Hinder Debug macro");
}
// Skill Check macro
const skillMacroName = "Skill Check";
const existingSkillMacro = game.macros.find((m) => m.name === skillMacroName);
if (!existingSkillMacro) {
await Macro.create({
name: skillMacroName,
type: "script",
img: "icons/svg/d20.svg",
command: `// Opens skill check dialog for selected token or prompts to select actor
const actor = canvas.tokens.controlled[0]?.actor
|| game.actors.find(a => a.type === "character");
if (!actor) {
ui.notifications.warn("Select a token or create a character first");
} else {
game.vagabond.applications.SkillCheckDialog.prompt(actor);
}`,
flags: { vagabond: { systemMacro: true } },
});
// eslint-disable-next-line no-console
console.log("Vagabond RPG | Created Skill Check macro");
}
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Handlebars Helpers */ /* Handlebars Helpers */
/* -------------------------------------------- */ /* -------------------------------------------- */

View File

@ -1,7 +1,7 @@
// Vagabond RPG - Chat Card Styles // Vagabond RPG - Chat Card Styles
// ================================ // ================================
// Placeholder - will be expanded in Phase 7 // Base chat card
.vagabond.chat-card { .vagabond.chat-card {
@include panel; @include panel;
overflow: hidden; overflow: hidden;
@ -13,16 +13,27 @@
background-color: $color-parchment-dark; background-color: $color-parchment-dark;
border-bottom: 1px solid $color-border; border-bottom: 1px solid $color-border;
.card-title { .card-title,
h3 {
font-family: $font-family-header; font-family: $font-family-header;
font-size: $font-size-base; font-size: $font-size-base;
font-weight: $font-weight-bold; font-weight: $font-weight-bold;
margin: 0;
} }
.card-subtitle { .card-subtitle {
font-size: $font-size-sm; font-size: $font-size-sm;
color: $color-text-muted; color: $color-text-muted;
} }
.trained-badge {
font-size: $font-size-xs;
padding: $spacing-1 $spacing-2;
background-color: rgba($color-success, 0.2);
color: $color-success;
border-radius: $radius-full;
font-weight: $font-weight-medium;
}
} }
// Card content // Card content
@ -30,27 +41,176 @@
padding: $spacing-3; padding: $spacing-3;
} }
// Roll result // Roll result (large total display)
.roll-result { .roll-result {
@include flex-center; @include flex-column;
gap: $spacing-3; align-items: center;
gap: $spacing-2;
padding: $spacing-3; padding: $spacing-3;
margin: $spacing-2 0;
background-color: $color-parchment-light; background-color: $color-parchment-light;
border-radius: $radius-md; border-radius: $radius-md;
border: 2px solid $color-border;
.roll-total { .roll-total {
font-family: $font-family-header; font-family: $font-family-header;
font-size: $font-size-3xl; font-size: $font-size-4xl;
font-weight: $font-weight-bold;
line-height: 1;
}
.roll-status {
.status {
display: inline-block;
padding: $spacing-1 $spacing-3;
font-size: $font-size-sm;
font-weight: $font-weight-bold;
text-transform: uppercase;
letter-spacing: 0.1em;
border-radius: $radius-md;
}
.success {
background-color: rgba($color-success, 0.2);
color: $color-success;
}
.failure {
background-color: rgba($color-danger, 0.2);
color: $color-danger;
}
.critical {
background-color: rgba($color-warning, 0.3);
color: $color-warning;
animation: pulse 1s ease-in-out;
}
.fumble {
background-color: rgba($color-danger, 0.3);
color: $color-danger;
animation: shake 0.5s ease-in-out;
}
}
// Conditional styling based on result
&.success {
border-color: $color-success;
}
&.failure {
border-color: $color-danger;
}
&.critical {
border-color: $color-warning;
background-color: rgba($color-warning, 0.1);
}
&.fumble {
border-color: $color-danger;
background-color: rgba($color-danger, 0.1);
}
}
// Roll details
.roll-details {
@include flex-column;
gap: $spacing-2;
padding: $spacing-2;
background-color: $color-parchment-dark;
border-radius: $radius-md;
font-size: $font-size-sm;
.roll-formula {
@include flex-between;
.label {
color: $color-text-muted;
}
.value {
font-family: $font-family-mono;
}
}
.roll-breakdown {
@include flex-center;
gap: $spacing-3;
padding-top: $spacing-2;
border-top: 1px solid $color-border;
span {
@include flex-center;
gap: $spacing-1;
}
.d20-result {
font-family: $font-family-mono;
font-weight: $font-weight-bold; font-weight: $font-weight-bold;
} }
.roll-formula { .favor-die {
font-size: $font-size-sm; font-family: $font-family-mono;
color: $color-text-muted;
&.favor {
color: $color-success;
}
&.hinder {
color: $color-danger;
} }
} }
// Result status .modifier {
font-family: $font-family-mono;
color: $color-text-secondary;
}
}
}
// Target info (difficulty, crit threshold)
.target-info {
@include grid(2, $spacing-2);
margin-top: $spacing-2;
padding: $spacing-2;
font-size: $font-size-sm;
> div {
@include flex-between;
}
.label {
color: $color-text-muted;
}
.value {
font-weight: $font-weight-medium;
}
}
// Favor/Hinder sources
.favor-sources,
.hinder-sources {
@include flex-center;
gap: $spacing-2;
margin-top: $spacing-2;
padding: $spacing-2;
font-size: $font-size-sm;
border-radius: $radius-md;
}
.favor-sources {
background-color: rgba($color-success, 0.1);
color: $color-success;
}
.hinder-sources {
background-color: rgba($color-danger, 0.1);
color: $color-danger;
}
// Result status (legacy)
.result-status { .result-status {
@include flex-center; @include flex-center;
padding: $spacing-2; padding: $spacing-2;
@ -117,6 +277,13 @@
} }
} }
// Skill roll card specific
.vagabond.chat-card.skill-roll {
.skill-name {
margin: 0;
}
}
// Spell card specific // Spell card specific
.vagabond.chat-card.spell-card { .vagabond.chat-card.spell-card {
.spell-effect { .spell-effect {
@ -142,7 +309,7 @@
} }
} }
// Animation // Animations
@keyframes pulse { @keyframes pulse {
0%, 0%,
100% { 100% {
@ -152,3 +319,16 @@
transform: scale(1.05); transform: scale(1.05);
} }
} }
@keyframes shake {
0%,
100% {
transform: translateX(0);
}
25% {
transform: translateX(-5px);
}
75% {
transform: translateX(5px);
}
}

View File

@ -37,12 +37,17 @@
select { select {
@include input-base; @include input-base;
width: 100%; width: 100%;
height: 2.5rem; // Fixed height for consistent visibility
padding: $spacing-2 $spacing-8 $spacing-2 $spacing-3;
cursor: pointer; cursor: pointer;
appearance: none; appearance: none;
background-color: $color-parchment-light !important;
color: $color-text-primary !important;
font-size: $font-size-base;
line-height: 1.5;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%232c2416' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%232c2416' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: right $spacing-3 center; background-position: right $spacing-3 center;
padding-right: $spacing-8;
} }
// Checkbox // Checkbox

View File

@ -1,7 +1,211 @@
// Vagabond RPG - Roll Dialog Styles // Vagabond RPG - Roll Dialog Styles
// ================================== // ==================================
// Placeholder - will be expanded in Phase 7 // ApplicationV2 Roll Dialog Base
.vagabond.roll-dialog {
// Force light background on the entire window
background-color: $color-parchment;
color: $color-text-primary;
.roll-dialog-content {
@include flex-column;
gap: $spacing-3;
padding: $spacing-4;
background-color: $color-parchment;
color: $color-text-primary;
}
// Automatic favor/hinder from Active Effects
.auto-favor-hinder {
@include flex-center;
gap: $spacing-2;
padding: $spacing-2 $spacing-3;
border-radius: $radius-md;
font-size: $font-size-sm;
i {
font-size: $font-size-base;
}
&.favor {
background-color: rgba($color-success, 0.15);
color: $color-success;
border: 1px solid $color-success;
}
&.hinder {
background-color: rgba($color-danger, 0.15);
color: $color-danger;
border: 1px solid $color-danger;
}
}
// Skill selection
.skill-selection {
@include flex-column;
gap: $spacing-2;
label {
font-weight: $font-weight-semibold;
}
}
// Skill info panel
.skill-info {
@include panel;
@include grid(2, $spacing-2);
padding: $spacing-3;
> div {
@include flex-between;
}
.label {
font-size: $font-size-sm;
color: $color-text-muted;
}
.value {
font-weight: $font-weight-medium;
&.trained {
color: $color-success;
}
&.untrained {
color: $color-text-muted;
}
&.difficulty {
font-family: $font-family-header;
font-size: $font-size-lg;
font-weight: $font-weight-bold;
}
&.crit {
color: $color-warning;
}
}
}
// Favor/Hinder toggle section
.favor-hinder-section {
@include flex-column;
gap: $spacing-2;
> label {
font-weight: $font-weight-semibold;
}
.favor-hinder-toggles {
display: flex;
gap: $spacing-2;
button {
@include button-secondary;
flex: 1;
gap: $spacing-2;
&.favor-btn {
&.active,
&:hover {
background-color: rgba($color-success, 0.15);
border-color: $color-success;
color: $color-success;
}
}
&.hinder-btn {
&.active,
&:hover {
background-color: rgba($color-danger, 0.15);
border-color: $color-danger;
color: $color-danger;
}
}
}
}
.net-favor-hinder {
@include flex-center;
padding: $spacing-1 $spacing-2;
font-size: $font-size-sm;
font-weight: $font-weight-medium;
border-radius: $radius-md;
&.favor {
background-color: rgba($color-success, 0.15);
color: $color-success;
}
&.hinder {
background-color: rgba($color-danger, 0.15);
color: $color-danger;
}
}
}
// Situational modifier section
.modifier-section {
@include flex-column;
gap: $spacing-2;
> label {
font-weight: $font-weight-semibold;
}
.modifier-presets {
display: flex;
gap: $spacing-2;
.modifier-preset {
@include button-secondary;
flex: 1;
padding: $spacing-1 $spacing-2;
font-family: $font-family-mono;
font-size: $font-size-sm;
&:hover {
background-color: $color-parchment-dark;
}
}
}
.modifier-input {
input {
@include input-base;
width: 100%;
text-align: center;
font-family: $font-family-mono;
}
}
}
// Roll button
.dialog-buttons {
margin-top: $spacing-2;
.roll-btn {
@include button-primary;
width: 100%;
padding: $spacing-3;
font-size: $font-size-lg;
gap: $spacing-2;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
}
}
// Skill check dialog specific
.vagabond.skill-check-dialog {
// Additional skill-specific styles if needed
}
// Legacy dialog styles (for backward compatibility)
.vagabond.dialog.roll-dialog { .vagabond.dialog.roll-dialog {
.dialog-content { .dialog-content {
padding: $spacing-4; padding: $spacing-4;
@ -152,3 +356,177 @@
} }
} }
} }
// Favor/Hinder Debug Panel
.vagabond.favor-hinder-debug {
// Force light background on the entire window content
// Using !important to override Foundry's dark theme styles
background-color: $color-parchment !important;
color: $color-text-primary !important;
* {
color: $color-text-primary;
}
// Enable scrolling on the window content
.window-content {
overflow-y: auto !important;
max-height: 80vh;
@include custom-scrollbar;
}
.favor-hinder-debug-content {
@include flex-column;
gap: $spacing-4;
padding: $spacing-4;
background-color: $color-parchment !important;
}
.actor-selection {
@include flex-column;
gap: $spacing-2;
label {
font-weight: $font-weight-semibold;
color: $color-text-primary;
}
}
.debug-panels {
@include flex-column;
gap: $spacing-4;
}
.debug-panel {
@include panel;
padding: $spacing-3;
background-color: $color-parchment-light;
h3 {
@include flex-center;
justify-content: flex-start;
gap: $spacing-2;
margin: 0 0 $spacing-3 0;
padding-bottom: $spacing-2;
border-bottom: 1px solid $color-border;
font-size: $font-size-base;
color: $color-text-primary;
i {
color: $color-accent-primary;
}
}
}
.flag-table {
width: 100%;
border-collapse: collapse;
background-color: $color-parchment-light;
th,
td {
padding: $spacing-2;
text-align: left;
border-bottom: 1px solid $color-border-light;
color: $color-text-primary;
}
th {
font-size: $font-size-sm;
font-weight: $font-weight-semibold;
color: $color-text-secondary;
background-color: $color-parchment-dark;
&.center {
text-align: center;
}
}
td {
background-color: $color-parchment-light !important;
&.center {
text-align: center;
}
&.skill-name {
@include flex-between;
}
}
// Alternating row colors with good contrast
tbody tr:nth-child(even) td {
background-color: $color-parchment !important;
}
.stat-tag {
font-size: $font-size-xs;
padding: $spacing-1 $spacing-2;
background-color: $color-parchment-dark;
border-radius: $radius-full;
color: $color-text-secondary;
text-transform: uppercase;
}
input[type="checkbox"] {
width: 1.25rem;
height: 1.25rem;
cursor: pointer;
accent-color: $color-accent-primary;
}
tbody tr:hover td {
background-color: $color-parchment-dark;
}
}
.debug-actions {
display: flex;
gap: $spacing-3;
padding-top: $spacing-3;
border-top: 1px solid $color-border;
button {
@include button-base;
flex: 1;
gap: $spacing-2;
}
.test-roll-btn {
@include button-primary;
}
.clear-btn {
@include button-secondary;
color: $color-danger;
border-color: $color-danger;
&:hover:not(:disabled) {
background-color: rgba($color-danger, 0.1);
}
}
}
.no-actor-message {
@include flex-column;
align-items: center;
gap: $spacing-3;
padding: $spacing-6;
text-align: center;
color: $color-text-muted;
i {
font-size: $font-size-4xl;
opacity: 0.5;
}
p {
margin: 0;
}
.hint {
font-size: $font-size-sm;
font-style: italic;
}
}
}

View File

@ -0,0 +1,79 @@
{{!-- Skill Check Chat Card Template --}}
{{!-- Displays skill check results with roll details, success/fail, and modifiers --}}
<div class="vagabond chat-card skill-roll">
{{!-- Header --}}
<header class="card-header">
<h3 class="skill-name">{{skillLabel}}</h3>
{{#if trained}}
<span class="trained-badge">{{localize "VAGABOND.Trained"}}</span>
{{/if}}
</header>
{{!-- Roll Result --}}
<div class="roll-result {{#if isCrit}}critical{{else if isFumble}}fumble{{else if success}}success{{else}}failure{{/if}}">
<div class="roll-total">{{total}}</div>
<div class="roll-status">
{{#if isCrit}}
<span class="status critical">{{localize "VAGABOND.Critical"}}</span>
{{else if isFumble}}
<span class="status fumble">{{localize "VAGABOND.Fumble"}}</span>
{{else if success}}
<span class="status success">{{localize "VAGABOND.Success"}}</span>
{{else}}
<span class="status failure">{{localize "VAGABOND.Failure"}}</span>
{{/if}}
</div>
</div>
{{!-- Roll Details --}}
<div class="roll-details">
<div class="roll-formula">
<span class="label">{{localize "VAGABOND.Formula"}}:</span>
<span class="value">{{formula}}</span>
</div>
<div class="roll-breakdown">
<span class="d20-result">
<i class="fa-solid fa-dice-d20"></i> {{d20Result}}
</span>
{{#if favorDie}}
<span class="favor-die {{#if (gt netFavorHinder 0)}}favor{{else}}hinder{{/if}}">
<i class="fa-solid fa-dice-d6"></i> {{favorDie}}
</span>
{{/if}}
{{#if modifier}}
<span class="modifier">
{{#if (gt modifier 0)}}+{{/if}}{{modifier}}
</span>
{{/if}}
</div>
</div>
{{!-- Target Info --}}
<div class="target-info">
<div class="difficulty">
<span class="label">{{localize "VAGABOND.Difficulty"}}:</span>
<span class="value">{{difficulty}}</span>
</div>
{{#if (lt critThreshold 20)}}
<div class="crit-threshold">
<span class="label">{{localize "VAGABOND.CritThreshold"}}:</span>
<span class="value">{{critThreshold}}+</span>
</div>
{{/if}}
</div>
{{!-- Favor/Hinder Sources --}}
{{#if favorSources.length}}
<div class="favor-sources">
<i class="fa-solid fa-arrow-up"></i>
<span>{{localize "VAGABOND.Favor"}}: {{#each favorSources}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}</span>
</div>
{{/if}}
{{#if hinderSources.length}}
<div class="hinder-sources">
<i class="fa-solid fa-arrow-down"></i>
<span>{{localize "VAGABOND.Hinder"}}: {{#each hinderSources}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}</span>
</div>
{{/if}}
</div>

View File

@ -0,0 +1,153 @@
{{!-- Favor/Hinder Debug Panel Template --}}
{{!-- Development tool for testing favor/hinder flags on actors --}}
<div class="vagabond favor-hinder-debug-content">
{{!-- Actor Selection --}}
<div class="actor-selection">
<label>{{localize "VAGABOND.SelectActor"}}</label>
<select name="actorId">
<option value="">-- Select Actor --</option>
{{#each actors}}
<option value="{{this.id}}" {{#if this.selected}}selected{{/if}}>
{{this.name}}
</option>
{{/each}}
</select>
</div>
{{#if actor}}
<div class="debug-panels">
{{!-- Skills Panel --}}
<div class="debug-panel skills-panel">
<h3>
<i class="fa-solid fa-book"></i>
{{localize "VAGABOND.Skills"}}
</h3>
<table class="flag-table">
<thead>
<tr>
<th>{{localize "VAGABOND.Skill"}}</th>
<th class="center">{{localize "VAGABOND.Favor"}}</th>
<th class="center">{{localize "VAGABOND.Hinder"}}</th>
</tr>
</thead>
<tbody>
{{#each skills}}
<tr>
<td class="skill-name">
{{this.label}}
<span class="stat-tag">{{this.stat}}</span>
</td>
<td class="center">
<input type="checkbox"
class="skill-flag"
data-skill="{{this.id}}"
data-flag-type="favor"
{{#if this.favor}}checked{{/if}}>
</td>
<td class="center">
<input type="checkbox"
class="skill-flag"
data-skill="{{this.id}}"
data-flag-type="hinder"
{{#if this.hinder}}checked{{/if}}>
</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
{{!-- Attacks Panel --}}
<div class="debug-panel attacks-panel">
<h3>
<i class="fa-solid fa-swords"></i>
{{localize "VAGABOND.Attacks"}}
</h3>
<table class="flag-table">
<thead>
<tr>
<th>Type</th>
<th class="center">{{localize "VAGABOND.Favor"}}</th>
<th class="center">{{localize "VAGABOND.Hinder"}}</th>
</tr>
</thead>
<tbody>
<tr>
<td>All Attacks</td>
<td class="center">
<input type="checkbox"
class="attack-flag"
data-flag-type="favor"
{{#if attacks.favor}}checked{{/if}}>
</td>
<td class="center">
<input type="checkbox"
class="attack-flag"
data-flag-type="hinder"
{{#if attacks.hinder}}checked{{/if}}>
</td>
</tr>
</tbody>
</table>
</div>
{{!-- Saves Panel --}}
<div class="debug-panel saves-panel">
<h3>
<i class="fa-solid fa-shield"></i>
{{localize "VAGABOND.Saves"}}
</h3>
<table class="flag-table">
<thead>
<tr>
<th>{{localize "VAGABOND.Save"}}</th>
<th class="center">{{localize "VAGABOND.Favor"}}</th>
<th class="center">{{localize "VAGABOND.Hinder"}}</th>
</tr>
</thead>
<tbody>
{{#each saves}}
<tr>
<td>{{this.label}}</td>
<td class="center">
<input type="checkbox"
class="save-flag"
data-save="{{this.id}}"
data-flag-type="favor"
{{#if this.favor}}checked{{/if}}>
</td>
<td class="center">
<input type="checkbox"
class="save-flag"
data-save="{{this.id}}"
data-flag-type="hinder"
{{#if this.hinder}}checked{{/if}}>
</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
</div>
{{!-- Action Buttons --}}
<div class="debug-actions">
<button type="button" class="test-roll-btn" data-action="test-roll">
<i class="fa-solid fa-dice-d20"></i>
Test Skill Roll
</button>
<button type="button" class="clear-btn" data-action="clear-all">
<i class="fa-solid fa-trash"></i>
Clear All Flags
</button>
</div>
{{else}}
<div class="no-actor-message">
<i class="fa-solid fa-user-slash"></i>
<p>Select an actor to manage favor/hinder flags.</p>
<p class="hint">You can also select a token on the canvas before opening this panel.</p>
</div>
{{/if}}
</div>

View File

@ -0,0 +1,66 @@
{{!-- Base Roll Dialog Template --}}
{{!-- Provides common UI for all roll dialogs: favor/hinder, modifiers, roll button --}}
<div class="vagabond roll-dialog-content">
{{!-- Automatic Favor/Hinder from Active Effects --}}
{{#if hasAutoFavor}}
<div class="auto-favor-hinder favor">
<i class="fa-solid fa-arrow-up"></i>
<span>{{localize "VAGABOND.AutoFavor"}}: {{#each autoFavorHinder.favorSources}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}</span>
</div>
{{/if}}
{{#if hasAutoHinder}}
<div class="auto-favor-hinder hinder">
<i class="fa-solid fa-arrow-down"></i>
<span>{{localize "VAGABOND.AutoHinder"}}: {{#each autoFavorHinder.hinderSources}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}</span>
</div>
{{/if}}
{{!-- Roll-specific content (provided by subclass) --}}
<div class="roll-specific">
{{> @partial-block}}
</div>
{{!-- Favor/Hinder Toggles --}}
<div class="favor-hinder-section">
<label>{{localize "VAGABOND.FavorHinder"}}</label>
<div class="favor-hinder-toggles">
<button type="button" class="favor-btn {{#if (eq config.favorHinder 1)}}active{{/if}}" data-action="toggle-favor">
<i class="fa-solid fa-arrow-up"></i>
{{localize "VAGABOND.Favor"}}
</button>
<button type="button" class="hinder-btn {{#if (eq config.favorHinder -1)}}active{{/if}}" data-action="toggle-hinder">
<i class="fa-solid fa-arrow-down"></i>
{{localize "VAGABOND.Hinder"}}
</button>
</div>
{{#if (gt netFavorHinder 0)}}
<div class="net-favor-hinder favor">+d6 {{localize "VAGABOND.Favor"}}</div>
{{else if (lt netFavorHinder 0)}}
<div class="net-favor-hinder hinder">-d6 {{localize "VAGABOND.Hinder"}}</div>
{{/if}}
</div>
{{!-- Situational Modifier --}}
<div class="modifier-section">
<label>{{localize "VAGABOND.SituationalModifier"}}</label>
<div class="modifier-presets">
{{#each modifierPresets}}
<button type="button" class="modifier-preset" data-modifier-preset="{{this.value}}">
{{this.label}}
</button>
{{/each}}
</div>
<div class="modifier-input">
<input type="number" name="modifier" value="{{config.modifier}}" placeholder="0">
</div>
</div>
{{!-- Roll Button --}}
<div class="dialog-buttons">
<button type="submit" class="roll-btn">
<i class="fa-solid fa-dice-d20"></i>
{{localize "VAGABOND.Roll"}}
</button>
</div>
</div>

View File

@ -0,0 +1,104 @@
{{!-- Skill Check Dialog Template --}}
{{!-- Extends roll-dialog-base with skill-specific information --}}
<div class="vagabond roll-dialog-content skill-check-dialog">
{{!-- Automatic Favor/Hinder from Active Effects --}}
{{#if hasAutoFavor}}
<div class="auto-favor-hinder favor">
<i class="fa-solid fa-arrow-up"></i>
<span>{{localize "VAGABOND.AutoFavor"}}: {{#each autoFavorHinder.favorSources}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}</span>
</div>
{{/if}}
{{#if hasAutoHinder}}
<div class="auto-favor-hinder hinder">
<i class="fa-solid fa-arrow-down"></i>
<span>{{localize "VAGABOND.AutoHinder"}}: {{#each autoFavorHinder.hinderSources}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}</span>
</div>
{{/if}}
{{!-- Skill Selection --}}
<div class="skill-selection">
<label>{{localize "VAGABOND.Skill"}}</label>
<select name="skillId">
<option value="">{{localize "VAGABOND.SelectSkill"}}</option>
{{#each rollSpecific.skills}}
<option value="{{this.id}}" {{#if this.selected}}selected{{/if}}>
{{this.label}} ({{capitalize this.stat}}) {{#if this.trained}}*{{/if}}
</option>
{{/each}}
</select>
</div>
{{!-- Skill Info (if skill selected) --}}
{{#if rollSpecific.selectedSkill}}
<div class="skill-info">
<div class="skill-stat">
<span class="label">{{localize "VAGABOND.Stat"}}:</span>
<span class="value">{{rollSpecific.statLabel}} ({{rollSpecific.statValue}})</span>
</div>
<div class="skill-trained">
<span class="label">{{localize "VAGABOND.Training"}}:</span>
<span class="value {{#if rollSpecific.trained}}trained{{else}}untrained{{/if}}">
{{#if rollSpecific.trained}}
{{localize "VAGABOND.Trained"}}
{{else}}
{{localize "VAGABOND.Untrained"}}
{{/if}}
</span>
</div>
<div class="skill-difficulty">
<span class="label">{{localize "VAGABOND.Difficulty"}}:</span>
<span class="value difficulty">{{rollSpecific.difficulty}}</span>
</div>
{{#if (lt rollSpecific.critThreshold 20)}}
<div class="skill-crit">
<span class="label">{{localize "VAGABOND.CritThreshold"}}:</span>
<span class="value crit">{{rollSpecific.critThreshold}}+</span>
</div>
{{/if}}
</div>
{{/if}}
{{!-- Favor/Hinder Toggles --}}
<div class="favor-hinder-section">
<label>{{localize "VAGABOND.FavorHinder"}}</label>
<div class="favor-hinder-toggles">
<button type="button" class="favor-btn {{#if (eq config.favorHinder 1)}}active{{/if}}" data-action="toggle-favor">
<i class="fa-solid fa-arrow-up"></i>
{{localize "VAGABOND.Favor"}}
</button>
<button type="button" class="hinder-btn {{#if (eq config.favorHinder -1)}}active{{/if}}" data-action="toggle-hinder">
<i class="fa-solid fa-arrow-down"></i>
{{localize "VAGABOND.Hinder"}}
</button>
</div>
{{#if (gt netFavorHinder 0)}}
<div class="net-favor-hinder favor">+d6 {{localize "VAGABOND.Favor"}}</div>
{{else if (lt netFavorHinder 0)}}
<div class="net-favor-hinder hinder">-d6 {{localize "VAGABOND.Hinder"}}</div>
{{/if}}
</div>
{{!-- Situational Modifier --}}
<div class="modifier-section">
<label>{{localize "VAGABOND.SituationalModifier"}}</label>
<div class="modifier-presets">
{{#each modifierPresets}}
<button type="button" class="modifier-preset" data-modifier-preset="{{this.value}}">
{{this.label}}
</button>
{{/each}}
</div>
<div class="modifier-input">
<input type="number" name="modifier" value="{{config.modifier}}" placeholder="0">
</div>
</div>
{{!-- Roll Button --}}
<div class="dialog-buttons">
<button type="submit" class="roll-btn" {{#unless rollSpecific.selectedSkill}}disabled{{/unless}}>
<i class="fa-solid fa-dice-d20"></i>
{{localize "VAGABOND.Roll"}}
</button>
</div>
</div>

View File

@ -0,0 +1,462 @@
(PASS) Test Complete: adds +d6 when favorHinder is positive
Object { test: {…} }
quench-reporter.ts:150:14
(PASS) Test Complete: subtracts d6 when favorHinder is negative
Object { test: {…} }
quench-reporter.ts:150:14
(PASS) Test Complete: has no extra die when favorHinder is 0
Object { test: {…} }
quench-reporter.ts:150:14
Suite: Flat Modifiers
Object { suite: {…} }
quench-reporter.ts:105:15
(PASS) Test Complete: applies positive modifiers to the roll
Object { test: {…} }
quench-reporter.ts:150:14
(PASS) Test Complete: applies negative modifiers to the roll
Object { test: {…} }
quench-reporter.ts:150:14
Vagabond: Skill Check System quench-reporter.ts:103:15
Suite: Skill Check Rolls
Object { suite: {…} }
quench-reporter.ts:105:15
Error: VagabondActor validation errors:
type: "character" is not a valid type for the Actor Document class
system:
combat:
currentZone: may not be a blank string
death:
deathCause: may not be a blank string
DataModelValidationError http://localhost:30000/scripts/foundry.mjs:5221
asError http://localhost:30000/scripts/foundry.mjs:5141
validate http://localhost:30000/scripts/foundry.mjs:11743
DataModel http://localhost:30000/scripts/foundry.mjs:11419
Document http://localhost:30000/scripts/foundry.mjs:12005
BaseActor http://localhost:30000/scripts/foundry.mjs:14207
ClientDocument http://localhost:30000/scripts/foundry.mjs:32362
Actor http://localhost:30000/scripts/foundry.mjs:41217
VagabondActor http://localhost:30000/systems/vagabond/module/documents/actor.mjs:16
#preCreateDocumentArray http://localhost:30000/scripts/foundry.mjs:58608
_createDocuments http://localhost:30000/scripts/foundry.mjs:58570
create http://localhost:30000/scripts/foundry.mjs:58201
foundry.mjs:23855:30
(FAIL) Test Complete: uses correct difficulty for trained skills
Object { test: {…}, err: TypeError }
quench-reporter.ts:170:14
Error: VagabondActor validation errors:
type: "character" is not a valid type for the Actor Document class
system:
combat:
currentZone: may not be a blank string
death:
deathCause: may not be a blank string
DataModelValidationError http://localhost:30000/scripts/foundry.mjs:5221
asError http://localhost:30000/scripts/foundry.mjs:5141
validate http://localhost:30000/scripts/foundry.mjs:11743
DataModel http://localhost:30000/scripts/foundry.mjs:11419
Document http://localhost:30000/scripts/foundry.mjs:12005
BaseActor http://localhost:30000/scripts/foundry.mjs:14207
ClientDocument http://localhost:30000/scripts/foundry.mjs:32362
Actor http://localhost:30000/scripts/foundry.mjs:41217
VagabondActor http://localhost:30000/systems/vagabond/module/documents/actor.mjs:16
#preCreateDocumentArray http://localhost:30000/scripts/foundry.mjs:58608
_createDocuments http://localhost:30000/scripts/foundry.mjs:58570
create http://localhost:30000/scripts/foundry.mjs:58201
foundry.mjs:23855:30
(FAIL) Test Complete: uses correct difficulty for untrained skills
Object { test: {…}, err: TypeError }
quench-reporter.ts:170:14
Error: VagabondActor validation errors:
type: "character" is not a valid type for the Actor Document class
system:
combat:
currentZone: may not be a blank string
death:
deathCause: may not be a blank string
DataModelValidationError http://localhost:30000/scripts/foundry.mjs:5221
asError http://localhost:30000/scripts/foundry.mjs:5141
validate http://localhost:30000/scripts/foundry.mjs:11743
DataModel http://localhost:30000/scripts/foundry.mjs:11419
Document http://localhost:30000/scripts/foundry.mjs:12005
BaseActor http://localhost:30000/scripts/foundry.mjs:14207
ClientDocument http://localhost:30000/scripts/foundry.mjs:32362
Actor http://localhost:30000/scripts/foundry.mjs:41217
VagabondActor http://localhost:30000/systems/vagabond/module/documents/actor.mjs:16
#preCreateDocumentArray http://localhost:30000/scripts/foundry.mjs:58608
_createDocuments http://localhost:30000/scripts/foundry.mjs:58570
create http://localhost:30000/scripts/foundry.mjs:58201
foundry.mjs:23855:30
(FAIL) Test Complete: uses skill-specific crit threshold
Object { test: {…}, err: TypeError }
quench-reporter.ts:170:14
Error: VagabondActor validation errors:
type: "character" is not a valid type for the Actor Document class
system:
combat:
currentZone: may not be a blank string
death:
deathCause: may not be a blank string
DataModelValidationError http://localhost:30000/scripts/foundry.mjs:5221
asError http://localhost:30000/scripts/foundry.mjs:5141
validate http://localhost:30000/scripts/foundry.mjs:11743
DataModel http://localhost:30000/scripts/foundry.mjs:11419
Document http://localhost:30000/scripts/foundry.mjs:12005
BaseActor http://localhost:30000/scripts/foundry.mjs:14207
ClientDocument http://localhost:30000/scripts/foundry.mjs:32362
Actor http://localhost:30000/scripts/foundry.mjs:41217
VagabondActor http://localhost:30000/systems/vagabond/module/documents/actor.mjs:16
#preCreateDocumentArray http://localhost:30000/scripts/foundry.mjs:58608
_createDocuments http://localhost:30000/scripts/foundry.mjs:58570
create http://localhost:30000/scripts/foundry.mjs:58201
foundry.mjs:23855:30
(PASS) Test Complete: throws error for unknown skill
Object { test: {…} }
quench-reporter.ts:150:14
Vagabond: Attack Check System quench-reporter.ts:103:15
Suite: Attack Check Rolls
Object { suite: {…} }
quench-reporter.ts:105:15
Error: VagabondActor validation errors:
type: "character" is not a valid type for the Actor Document class
system:
combat:
currentZone: may not be a blank string
death:
deathCause: may not be a blank string
DataModelValidationError http://localhost:30000/scripts/foundry.mjs:5221
asError http://localhost:30000/scripts/foundry.mjs:5141
validate http://localhost:30000/scripts/foundry.mjs:11743
DataModel http://localhost:30000/scripts/foundry.mjs:11419
Document http://localhost:30000/scripts/foundry.mjs:12005
BaseActor http://localhost:30000/scripts/foundry.mjs:14207
ClientDocument http://localhost:30000/scripts/foundry.mjs:32362
Actor http://localhost:30000/scripts/foundry.mjs:41217
VagabondActor http://localhost:30000/systems/vagabond/module/documents/actor.mjs:16
#preCreateDocumentArray http://localhost:30000/scripts/foundry.mjs:58608
_createDocuments http://localhost:30000/scripts/foundry.mjs:58570
create http://localhost:30000/scripts/foundry.mjs:58201
foundry.mjs:23855:30
Error: VagabondItem validation errors:
type: "weapon" is not a valid type for the Item Document class
system:
equippedHand: may not be a blank string
DataModelValidationError http://localhost:30000/scripts/foundry.mjs:5221
asError http://localhost:30000/scripts/foundry.mjs:5141
validate http://localhost:30000/scripts/foundry.mjs:11743
DataModel http://localhost:30000/scripts/foundry.mjs:11419
Document http://localhost:30000/scripts/foundry.mjs:12005
BaseItem http://localhost:30000/scripts/foundry.mjs:15503
ClientDocument http://localhost:30000/scripts/foundry.mjs:32362
Item http://localhost:30000/scripts/foundry.mjs:45138
VagabondItem http://localhost:30000/systems/vagabond/module/documents/item.mjs:15
#preCreateDocumentArray http://localhost:30000/scripts/foundry.mjs:58608
_createDocuments http://localhost:30000/scripts/foundry.mjs:58570
create http://localhost:30000/scripts/foundry.mjs:58201
foundry.mjs:23855:30
(FAIL) Test Complete: calculates difficulty from attack stat
Object { test: {…}, err: TypeError }
quench-reporter.ts:170:14
Error: VagabondActor validation errors:
type: "character" is not a valid type for the Actor Document class
system:
combat:
currentZone: may not be a blank string
death:
deathCause: may not be a blank string
DataModelValidationError http://localhost:30000/scripts/foundry.mjs:5221
asError http://localhost:30000/scripts/foundry.mjs:5141
validate http://localhost:30000/scripts/foundry.mjs:11743
DataModel http://localhost:30000/scripts/foundry.mjs:11419
Document http://localhost:30000/scripts/foundry.mjs:12005
BaseActor http://localhost:30000/scripts/foundry.mjs:14207
ClientDocument http://localhost:30000/scripts/foundry.mjs:32362
Actor http://localhost:30000/scripts/foundry.mjs:41217
VagabondActor http://localhost:30000/systems/vagabond/module/documents/actor.mjs:16
#preCreateDocumentArray http://localhost:30000/scripts/foundry.mjs:58608
_createDocuments http://localhost:30000/scripts/foundry.mjs:58570
create http://localhost:30000/scripts/foundry.mjs:58201
foundry.mjs:23855:30
Error: VagabondItem validation errors:
type: "weapon" is not a valid type for the Item Document class
system:
equippedHand: may not be a blank string
DataModelValidationError http://localhost:30000/scripts/foundry.mjs:5221
asError http://localhost:30000/scripts/foundry.mjs:5141
validate http://localhost:30000/scripts/foundry.mjs:11743
DataModel http://localhost:30000/scripts/foundry.mjs:11419
Document http://localhost:30000/scripts/foundry.mjs:12005
BaseItem http://localhost:30000/scripts/foundry.mjs:15503
ClientDocument http://localhost:30000/scripts/foundry.mjs:32362
Item http://localhost:30000/scripts/foundry.mjs:45138
VagabondItem http://localhost:30000/systems/vagabond/module/documents/item.mjs:15
#preCreateDocumentArray http://localhost:30000/scripts/foundry.mjs:58608
_createDocuments http://localhost:30000/scripts/foundry.mjs:58570
create http://localhost:30000/scripts/foundry.mjs:58201
foundry.mjs:23855:30
(FAIL) Test Complete: uses attack-specific crit threshold
Object { test: {…}, err: TypeError }
quench-reporter.ts:170:14
Vagabond: Save Roll System quench-reporter.ts:103:15
Suite: Save Rolls
Object { suite: {…} }
quench-reporter.ts:105:15
Error: VagabondActor validation errors:
type: "character" is not a valid type for the Actor Document class
system:
combat:
currentZone: may not be a blank string
death:
deathCause: may not be a blank string
DataModelValidationError http://localhost:30000/scripts/foundry.mjs:5221
asError http://localhost:30000/scripts/foundry.mjs:5141
validate http://localhost:30000/scripts/foundry.mjs:11743
DataModel http://localhost:30000/scripts/foundry.mjs:11419
Document http://localhost:30000/scripts/foundry.mjs:12005
BaseActor http://localhost:30000/scripts/foundry.mjs:14207
ClientDocument http://localhost:30000/scripts/foundry.mjs:32362
Actor http://localhost:30000/scripts/foundry.mjs:41217
VagabondActor http://localhost:30000/systems/vagabond/module/documents/actor.mjs:16
#preCreateDocumentArray http://localhost:30000/scripts/foundry.mjs:58608
_createDocuments http://localhost:30000/scripts/foundry.mjs:58570
create http://localhost:30000/scripts/foundry.mjs:58201
foundry.mjs:23855:30
(FAIL) Test Complete: rolls against provided difficulty
Object { test: {…}, err: TypeError }
quench-reporter.ts:170:14
Error: VagabondActor validation errors:
type: "character" is not a valid type for the Actor Document class
system:
combat:
currentZone: may not be a blank string
death:
deathCause: may not be a blank string
DataModelValidationError http://localhost:30000/scripts/foundry.mjs:5221
asError http://localhost:30000/scripts/foundry.mjs:5141
validate http://localhost:30000/scripts/foundry.mjs:11743
DataModel http://localhost:30000/scripts/foundry.mjs:11419
Document http://localhost:30000/scripts/foundry.mjs:12005
BaseActor http://localhost:30000/scripts/foundry.mjs:14207
ClientDocument http://localhost:30000/scripts/foundry.mjs:32362
Actor http://localhost:30000/scripts/foundry.mjs:41217
VagabondActor http://localhost:30000/systems/vagabond/module/documents/actor.mjs:16
#preCreateDocumentArray http://localhost:30000/scripts/foundry.mjs:58608
_createDocuments http://localhost:30000/scripts/foundry.mjs:58570
create http://localhost:30000/scripts/foundry.mjs:58201
foundry.mjs:23855:30
(FAIL) Test Complete: saves do not crit (threshold stays 20)
Object { test: {…}, err: TypeError }
quench-reporter.ts:170:14
Vagabond: Damage Roll System quench-reporter.ts:103:15
Suite: Damage Rolls
Object { suite: {…} }
quench-reporter.ts:105:15
(PASS) Test Complete: evaluates damage formula
Object { test: {…} }
quench-reporter.ts:150:14
(PASS) Test Complete: doubles dice on critical hit
Object { test: {…} }
quench-reporter.ts:150:14
(PASS) Test Complete: does not double modifiers on crit
Object { test: {…} }
quench-reporter.ts:150:14
Suite: doubleDice Helper
Object { suite: {…} }
quench-reporter.ts:105:15
(PASS) Test Complete: doubles dice count in formula
Object { test: {…} }
quench-reporter.ts:150:14
(PASS) Test Complete: preserves modifiers when doubling dice
Object { test: {…} }
quench-reporter.ts:150:14
(PASS) Test Complete: handles multiple dice types
Object { test: {…} }
quench-reporter.ts:150:14
Vagabond: Countdown Dice System quench-reporter.ts:103:15
Suite: Countdown Dice
Object { suite: {…} }
quench-reporter.ts:105:15
(PASS) Test Complete: rolls the specified die size
Object { test: {…} }
quench-reporter.ts:150:14
(PASS) Test Complete: continues on high rolls (3-6 on d6)
Object { test: {…} }
quench-reporter.ts:150:14
(PASS) Test Complete: shrinks die on low rolls (1-2)
Object { test: {…} }
quench-reporter.ts:150:14
(PASS) Test Complete: ends effect when d4 rolls 1-2
Object { test: {…} }
quench-reporter.ts:150:14
(PASS) Test Complete: returns ended state for die size 0
Object { test: {…} }
quench-reporter.ts:150:14
Vagabond: Morale Check System quench-reporter.ts:103:15
Suite: Morale Checks
Object { suite: {…} }
quench-reporter.ts:105:15
Error: VagabondActor validation errors:
type: "npc" is not a valid type for the Actor Document class
system:
moraleStatus:
lastTrigger: may not be a blank string
lastResult: may not be a blank string
DataModelValidationError http://localhost:30000/scripts/foundry.mjs:5221
asError http://localhost:30000/scripts/foundry.mjs:5141
validate http://localhost:30000/scripts/foundry.mjs:11743
DataModel http://localhost:30000/scripts/foundry.mjs:11419
Document http://localhost:30000/scripts/foundry.mjs:12005
BaseActor http://localhost:30000/scripts/foundry.mjs:14207
ClientDocument http://localhost:30000/scripts/foundry.mjs:32362
Actor http://localhost:30000/scripts/foundry.mjs:41217
VagabondActor http://localhost:30000/systems/vagabond/module/documents/actor.mjs:16
#preCreateDocumentArray http://localhost:30000/scripts/foundry.mjs:58608
_createDocuments http://localhost:30000/scripts/foundry.mjs:58570
create http://localhost:30000/scripts/foundry.mjs:58201
foundry.mjs:23855:30
(FAIL) Test Complete: rolls 2d6 against morale score
Object { test: {…}, err: TypeError }
quench-reporter.ts:170:14
Error: VagabondActor validation errors:
type: "npc" is not a valid type for the Actor Document class
system:
moraleStatus:
lastTrigger: may not be a blank string
lastResult: may not be a blank string
DataModelValidationError http://localhost:30000/scripts/foundry.mjs:5221
asError http://localhost:30000/scripts/foundry.mjs:5141
validate http://localhost:30000/scripts/foundry.mjs:11743
DataModel http://localhost:30000/scripts/foundry.mjs:11419
Document http://localhost:30000/scripts/foundry.mjs:12005
BaseActor http://localhost:30000/scripts/foundry.mjs:14207
ClientDocument http://localhost:30000/scripts/foundry.mjs:32362
Actor http://localhost:30000/scripts/foundry.mjs:41217
VagabondActor http://localhost:30000/systems/vagabond/module/documents/actor.mjs:16
#preCreateDocumentArray http://localhost:30000/scripts/foundry.mjs:58608
_createDocuments http://localhost:30000/scripts/foundry.mjs:58570
create http://localhost:30000/scripts/foundry.mjs:58201
foundry.mjs:23855:30
(FAIL) Test Complete: passes when roll <= morale
Object { test: {…}, err: TypeError }
quench-reporter.ts:170:14
Error: VagabondActor validation errors:
type: "npc" is not a valid type for the Actor Document class
system:
moraleStatus:
lastTrigger: may not be a blank string
lastResult: may not be a blank string
DataModelValidationError http://localhost:30000/scripts/foundry.mjs:5221
asError http://localhost:30000/scripts/foundry.mjs:5141
validate http://localhost:30000/scripts/foundry.mjs:11743
DataModel http://localhost:30000/scripts/foundry.mjs:11419
Document http://localhost:30000/scripts/foundry.mjs:12005
BaseActor http://localhost:30000/scripts/foundry.mjs:14207
ClientDocument http://localhost:30000/scripts/foundry.mjs:32362
Actor http://localhost:30000/scripts/foundry.mjs:41217
VagabondActor http://localhost:30000/systems/vagabond/module/documents/actor.mjs:16
#preCreateDocumentArray http://localhost:30000/scripts/foundry.mjs:58608
_createDocuments http://localhost:30000/scripts/foundry.mjs:58570
create http://localhost:30000/scripts/foundry.mjs:58201
foundry.mjs:23855:30
(FAIL) Test Complete: fails when roll > morale
Object { test: {…}, err: TypeError }
quench-reporter.ts:170:14
Error: VagabondActor validation errors:
type: "npc" is not a valid type for the Actor Document class
system:
moraleStatus:
lastTrigger: may not be a blank string
lastResult: may not be a blank string
DataModelValidationError http://localhost:30000/scripts/foundry.mjs:5221
asError http://localhost:30000/scripts/foundry.mjs:5141
validate http://localhost:30000/scripts/foundry.mjs:11743
DataModel http://localhost:30000/scripts/foundry.mjs:11419
Document http://localhost:30000/scripts/foundry.mjs:12005
BaseActor http://localhost:30000/scripts/foundry.mjs:14207
ClientDocument http://localhost:30000/scripts/foundry.mjs:32362
Actor http://localhost:30000/scripts/foundry.mjs:41217
VagabondActor http://localhost:30000/systems/vagabond/module/documents/actor.mjs:16
#preCreateDocumentArray http://localhost:30000/scripts/foundry.mjs:58608
_createDocuments http://localhost:30000/scripts/foundry.mjs:58570
create http://localhost:30000/scripts/foundry.mjs:58201
foundry.mjs:23855:30
Error: VagabondActor validation errors:
type: "character" is not a valid type for the Actor Document class
system:
combat:
currentZone: may not be a blank string
death:
deathCause: may not be a blank string
DataModelValidationError http://localhost:30000/scripts/foundry.mjs:5221
asError http://localhost:30000/scripts/foundry.mjs:5141
validate http://localhost:30000/scripts/foundry.mjs:11743
DataModel http://localhost:30000/scripts/foundry.mjs:11419
Document http://localhost:30000/scripts/foundry.mjs:12005
BaseActor http://localhost:30000/scripts/foundry.mjs:14207
ClientDocument http://localhost:30000/scripts/foundry.mjs:32362
Actor http://localhost:30000/scripts/foundry.mjs:41217
VagabondActor http://localhost:30000/systems/vagabond/module/documents/actor.mjs:16
#preCreateDocumentArray http://localhost:30000/scripts/foundry.mjs:58608
_createDocuments http://localhost:30000/scripts/foundry.mjs:58570
create http://localhost:30000/scripts/foundry.mjs:58201
foundry.mjs:23855:30
(FAIL) Test Complete: throws error for non-NPC actors
Object { test: {…}, err: TypeError }
quench-reporter.ts:170:14
QUENCH | TEST RUN COMPLETE
Object { stats: {…} }
quench-reporter.ts:185:14
VagabondActor validation errors:
type: "character" is not a valid type for the Actor Document class
system:
combat:
currentZone: may not be a blank string
death:
deathCause: may not be a blank string 11 foundry.mjs:115132:18
VagabondActor validation errors:
type: "npc" is not a valid type for the Actor Document class
system:
moraleStatus:
lastTrigger: may not be a blank string
lastResult: may not be a blank string 9 foundry.mjs:115132:18
VagabondActor validation errors:
type: "character" is not a valid type for the Actor Document class
system:
combat:
currentZone: may not be a blank string
death:
deathCause: may not be a blank string 5 foundry.mjs:115132:18
VagabondItem validation errors:
type: "weapon" is not a valid type for the Item Document class
system:
equippedHand: may not be a blank string foundry.mjs:115132:18
VagabondActor validation errors:
type: "character" is not a valid type for the Actor Document class
system:
combat:
currentZone: may not be a blank string
death:
deathCause: may not be a blank string foundry.mjs:115132:18
VagabondItem validation errors:
type: "weapon" is not a valid type for the Item Document class
system:
equippedHand: may not be a blank string foundry.mjs:115132:18
VagabondActor validation errors:
type: "character" is not a valid type for the Actor Document class
system:
combat:
currentZone: may not be a blank string
death:
deathCause: may not be a blank string 2 foundry.mjs:115132:18
VagabondActor validation errors:
type: "npc" is not a valid type for the Actor Document class
system:
moraleStatus:
lastTrigger: may not be a blank string
lastResult: may not be a blank string 4 foundry.mjs:115132:18
VagabondActor validation errors:
type: "character" is not a valid type for the Actor Document class
system:
combat:
currentZone: may not be a blank string
death:
deathCause: may not be a blank string foundry.mjs:115132:18