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:
parent
517b7045c7
commit
463a130c18
@ -55,6 +55,7 @@
|
||||
"Scene": "readonly",
|
||||
"User": "readonly",
|
||||
"Folder": "readonly",
|
||||
"Macro": "readonly",
|
||||
"Compendium": "readonly",
|
||||
"CompendiumCollection": "readonly",
|
||||
"DocumentSheetConfig": "readonly",
|
||||
|
||||
@ -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
|
||||
|
||||
- [Foundry VTT API Documentation](https://foundryvtt.com/api/)
|
||||
|
||||
@ -239,7 +239,7 @@
|
||||
"id": "2.1",
|
||||
"name": "Create main system entry point (vagabond.mjs)",
|
||||
"description": "System initialization, hook registration, CONFIG setup, document class registration",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "critical",
|
||||
"dependencies": ["1.1", "1.6"]
|
||||
@ -248,7 +248,7 @@
|
||||
"id": "2.2",
|
||||
"name": "Implement VagabondActor class",
|
||||
"description": "Extended Actor with prepareData for derived values calculation (HP, Speed, Saves, Skill difficulties)",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "critical",
|
||||
"dependencies": ["2.1", "1.2", "1.3"]
|
||||
@ -257,7 +257,7 @@
|
||||
"id": "2.3",
|
||||
"name": "Implement VagabondItem class",
|
||||
"description": "Extended Item with type-specific preparation and chat card generation",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "critical",
|
||||
"dependencies": ["2.1", "1.6"]
|
||||
@ -266,7 +266,7 @@
|
||||
"id": "2.4",
|
||||
"name": "Create dice rolling module",
|
||||
"description": "Core roll functions: d20 checks, damage rolls, exploding dice (d6!), countdown dice, favor/hinder modifiers",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "critical",
|
||||
"dependencies": ["2.1"]
|
||||
|
||||
21
lang/en.json
21
lang/en.json
@ -163,6 +163,7 @@
|
||||
"VAGABOND.Success": "Success",
|
||||
"VAGABOND.Failure": "Failure",
|
||||
"VAGABOND.Critical": "Critical!",
|
||||
"VAGABOND.Fumble": "Fumble!",
|
||||
|
||||
"VAGABOND.Damage": "Damage",
|
||||
"VAGABOND.DamageType": "Damage Type",
|
||||
@ -209,5 +210,23 @@
|
||||
"VAGABOND.ItemTypeFeature": "Feature",
|
||||
"VAGABOND.ItemTypeWeapon": "Weapon",
|
||||
"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"
|
||||
}
|
||||
|
||||
8
module/applications/_module.mjs
Normal file
8
module/applications/_module.mjs
Normal 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";
|
||||
255
module/applications/base-roll-dialog.mjs
Normal file
255
module/applications/base-roll-dialog.mjs
Normal 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);
|
||||
}
|
||||
}
|
||||
334
module/applications/favor-hinder-debug.mjs
Normal file
334
module/applications/favor-hinder-debug.mjs
Normal 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);
|
||||
}
|
||||
}
|
||||
248
module/applications/skill-check-dialog.mjs
Normal file
248
module/applications/skill-check-dialog.mjs
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -367,32 +367,14 @@ export default class CharacterData extends VagabondActorBase {
|
||||
}),
|
||||
}),
|
||||
|
||||
// Favor/Hinder tracking (d20 +/- d6 modifiers)
|
||||
// Cancel each other 1-for-1, don't stack
|
||||
favorHinder: new fields.SchemaField({
|
||||
favor: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
source: new fields.StringField({ required: true }), // "Flanking", "Virtuoso", etc.
|
||||
appliesTo: new fields.ArrayField(new fields.StringField()), // ["Attack Checks"], ["Reflex Saves"]
|
||||
duration: new fields.StringField({
|
||||
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: [] }
|
||||
),
|
||||
}),
|
||||
// NOTE: Favor/Hinder is now handled via Active Effects flags instead of a data schema.
|
||||
// See DEVELOPMENT.md "Favor/Hinder via Active Effects" for the 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>
|
||||
|
||||
// Focus tracking for maintained spells
|
||||
focus: new fields.SchemaField({
|
||||
|
||||
@ -130,8 +130,9 @@ export async function skillCheck(actor, skillId, options = {}) {
|
||||
const difficulty = skillData.difficulty;
|
||||
const critThreshold = skillData.critThreshold || 20;
|
||||
|
||||
// Determine favor/hinder
|
||||
const favorHinder = options.favorHinder ?? actor.getNetFavorHinder?.(`${skillId} Checks`) ?? 0;
|
||||
// Determine favor/hinder from Active Effect flags or override
|
||||
const favorHinderResult = actor.getNetFavorHinder?.({ skillId }) ?? { net: 0 };
|
||||
const favorHinder = options.favorHinder ?? favorHinderResult.net;
|
||||
|
||||
return d20Check({
|
||||
difficulty,
|
||||
@ -170,8 +171,9 @@ export async function attackCheck(actor, weapon, options = {}) {
|
||||
// Get crit threshold from attack data
|
||||
const critThreshold = system.attacks?.[attackType]?.critThreshold || 20;
|
||||
|
||||
// Determine favor/hinder
|
||||
const favorHinder = options.favorHinder ?? actor.getNetFavorHinder?.("Attack Checks") ?? 0;
|
||||
// Determine favor/hinder from Active Effect flags or override
|
||||
const favorHinderResult = actor.getNetFavorHinder?.({ isAttack: true }) ?? { net: 0 };
|
||||
const favorHinder = options.favorHinder ?? favorHinderResult.net;
|
||||
|
||||
return d20Check({
|
||||
difficulty,
|
||||
@ -200,12 +202,9 @@ export async function saveRoll(actor, saveType, difficulty, options = {}) {
|
||||
throw new Error(`Unknown save type: ${saveType}`);
|
||||
}
|
||||
|
||||
// Determine favor/hinder based on save type
|
||||
let rollType = `${saveType.charAt(0).toUpperCase() + saveType.slice(1)} Saves`;
|
||||
if (options.isBlock) rollType = "Block Saves";
|
||||
if (options.isDodge) rollType = "Dodge Saves";
|
||||
|
||||
const favorHinder = options.favorHinder ?? actor.getNetFavorHinder?.(rollType) ?? 0;
|
||||
// Determine favor/hinder from Active Effect flags or override
|
||||
const favorHinderResult = actor.getNetFavorHinder?.({ saveType }) ?? { net: 0 };
|
||||
const favorHinder = options.favorHinder ?? favorHinderResult.net;
|
||||
|
||||
return d20Check({
|
||||
difficulty,
|
||||
|
||||
@ -464,29 +464,99 @@ export default class VagabondActor extends Actor {
|
||||
|
||||
/**
|
||||
* 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")
|
||||
* @returns {number} Net modifier: positive = favor, negative = hinder, 0 = neutral
|
||||
* Flag convention (set by Active Effects):
|
||||
* - 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) {
|
||||
if (this.type !== "character") return 0;
|
||||
getNetFavorHinder({ skillId = null, isAttack = false, saveType = null } = {}) {
|
||||
if (this.type !== "character") return { net: 0, favorSources: [], hinderSources: [] };
|
||||
|
||||
const favorHinder = this.system.favorHinder;
|
||||
if (!favorHinder) return 0;
|
||||
const favorSources = [];
|
||||
const hinderSources = [];
|
||||
|
||||
// Count favor sources that apply to this roll type
|
||||
const favorCount = (favorHinder.favor || []).filter(
|
||||
(f) => !f.appliesTo?.length || f.appliesTo.includes(rollType)
|
||||
).length;
|
||||
// Check skill-specific flags
|
||||
if (skillId) {
|
||||
if (this.getFlag("vagabond", `favor.skills.${skillId}`)) {
|
||||
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
|
||||
const hinderCount = (favorHinder.hinder || []).filter(
|
||||
(h) => !h.appliesTo?.length || h.appliesTo.includes(rollType)
|
||||
).length;
|
||||
// Check attack flags
|
||||
if (isAttack) {
|
||||
if (this.getFlag("vagabond", "favor.attacks")) {
|
||||
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
|
||||
const net = favorCount - hinderCount;
|
||||
return Math.clamp(net, -1, 1);
|
||||
const net = Math.clamp(favorSources.length - hinderSources.length, -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}`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -260,32 +260,78 @@ export function registerActorTests(quenchRunner) {
|
||||
});
|
||||
|
||||
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.
|
||||
* They cancel 1-for-1 and don't stack (multiple favors = still +1d6).
|
||||
* Each entry tracks: source, appliesTo (what rolls), duration.
|
||||
* Favor/Hinder is now tracked via Active Effect flags instead of data schema.
|
||||
* Flag convention: flags.vagabond.favor.skills.<skillId>
|
||||
* The getNetFavorHinder method checks these flags.
|
||||
*/
|
||||
await testActor.update({
|
||||
"system.favorHinder.favor": [
|
||||
{
|
||||
source: "Flanking",
|
||||
appliesTo: ["Attack Checks"],
|
||||
duration: "until-next-turn",
|
||||
},
|
||||
],
|
||||
"system.favorHinder.hinder": [
|
||||
{
|
||||
source: "Heavy Armor",
|
||||
appliesTo: ["Dodge Saves"],
|
||||
duration: "permanent",
|
||||
},
|
||||
],
|
||||
// Set a flag directly (simulating what an Active Effect would do)
|
||||
await testActor.setFlag("vagabond", "favor.skills.performance", true);
|
||||
|
||||
const result = testActor.getNetFavorHinder({ skillId: "performance" });
|
||||
expect(result.net).to.equal(1);
|
||||
expect(result.favorSources.length).to.equal(1);
|
||||
|
||||
// Clean up
|
||||
await testActor.unsetFlag("vagabond", "favor.skills.performance");
|
||||
});
|
||||
|
||||
expect(testActor.system.favorHinder.favor.length).to.equal(1);
|
||||
expect(testActor.system.favorHinder.hinder.length).to.equal(1);
|
||||
expect(testActor.system.favorHinder.favor[0].source).to.equal("Flanking");
|
||||
it("detects hinder from Active Effect flags", async () => {
|
||||
/**
|
||||
* 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");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -22,6 +22,9 @@ import {
|
||||
// Import document classes
|
||||
import { VagabondActor, VagabondItem } from "./documents/_module.mjs";
|
||||
|
||||
// Import application classes
|
||||
import { VagabondRollDialog, SkillCheckDialog, FavorHinderDebug } from "./applications/_module.mjs";
|
||||
|
||||
// Import sheet classes
|
||||
// import { VagabondCharacterSheet } from "./sheets/actor-sheet.mjs";
|
||||
// import { VagabondItemSheet } from "./sheets/item-sheet.mjs";
|
||||
@ -46,6 +49,15 @@ Hooks.once("init", () => {
|
||||
// Add custom constants for configuration
|
||||
CONFIG.VAGABOND = VAGABOND;
|
||||
|
||||
// Expose application classes globally for macro/API access
|
||||
game.vagabond = {
|
||||
applications: {
|
||||
VagabondRollDialog,
|
||||
SkillCheckDialog,
|
||||
FavorHinderDebug,
|
||||
},
|
||||
};
|
||||
|
||||
// Register Actor data models
|
||||
CONFIG.Actor.dataModels = {
|
||||
character: CharacterData,
|
||||
@ -93,7 +105,7 @@ Hooks.once("init", () => {
|
||||
/**
|
||||
* Ready hook - runs when Foundry is fully loaded
|
||||
*/
|
||||
Hooks.once("ready", () => {
|
||||
Hooks.once("ready", async () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("Vagabond RPG | System Ready");
|
||||
|
||||
@ -101,9 +113,58 @@ Hooks.once("ready", () => {
|
||||
if (game.user.isGM) {
|
||||
const version = game.system.version;
|
||||
ui.notifications.info(`Vagabond RPG v${version} - System loaded successfully!`);
|
||||
|
||||
// Create development macros if they don't exist
|
||||
await _createDevMacros();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Create development/debug macros if they don't already exist.
|
||||
* @private
|
||||
*/
|
||||
async function _createDevMacros() {
|
||||
// Favor/Hinder Debug macro
|
||||
const debugMacroName = "Favor/Hinder Debug";
|
||||
const existingMacro = game.macros.find((m) => m.name === debugMacroName);
|
||||
|
||||
if (!existingMacro) {
|
||||
await Macro.create({
|
||||
name: debugMacroName,
|
||||
type: "script",
|
||||
img: "icons/svg/bug.svg",
|
||||
command: "game.vagabond.applications.FavorHinderDebug.open();",
|
||||
flags: { vagabond: { systemMacro: true } },
|
||||
});
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("Vagabond RPG | Created Favor/Hinder Debug macro");
|
||||
}
|
||||
|
||||
// Skill Check macro
|
||||
const skillMacroName = "Skill Check";
|
||||
const existingSkillMacro = game.macros.find((m) => m.name === skillMacroName);
|
||||
|
||||
if (!existingSkillMacro) {
|
||||
await Macro.create({
|
||||
name: skillMacroName,
|
||||
type: "script",
|
||||
img: "icons/svg/d20.svg",
|
||||
command: `// Opens skill check dialog for selected token or prompts to select actor
|
||||
const actor = canvas.tokens.controlled[0]?.actor
|
||||
|| game.actors.find(a => a.type === "character");
|
||||
|
||||
if (!actor) {
|
||||
ui.notifications.warn("Select a token or create a character first");
|
||||
} else {
|
||||
game.vagabond.applications.SkillCheckDialog.prompt(actor);
|
||||
}`,
|
||||
flags: { vagabond: { systemMacro: true } },
|
||||
});
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("Vagabond RPG | Created Skill Check macro");
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Handlebars Helpers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Vagabond RPG - Chat Card Styles
|
||||
// ================================
|
||||
|
||||
// Placeholder - will be expanded in Phase 7
|
||||
// Base chat card
|
||||
.vagabond.chat-card {
|
||||
@include panel;
|
||||
overflow: hidden;
|
||||
@ -13,16 +13,27 @@
|
||||
background-color: $color-parchment-dark;
|
||||
border-bottom: 1px solid $color-border;
|
||||
|
||||
.card-title {
|
||||
.card-title,
|
||||
h3 {
|
||||
font-family: $font-family-header;
|
||||
font-size: $font-size-base;
|
||||
font-weight: $font-weight-bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: $font-size-sm;
|
||||
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
|
||||
@ -30,27 +41,176 @@
|
||||
padding: $spacing-3;
|
||||
}
|
||||
|
||||
// Roll result
|
||||
// Roll result (large total display)
|
||||
.roll-result {
|
||||
@include flex-center;
|
||||
gap: $spacing-3;
|
||||
@include flex-column;
|
||||
align-items: center;
|
||||
gap: $spacing-2;
|
||||
padding: $spacing-3;
|
||||
margin: $spacing-2 0;
|
||||
background-color: $color-parchment-light;
|
||||
border-radius: $radius-md;
|
||||
border: 2px solid $color-border;
|
||||
|
||||
.roll-total {
|
||||
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;
|
||||
}
|
||||
|
||||
.roll-formula {
|
||||
font-size: $font-size-sm;
|
||||
color: $color-text-muted;
|
||||
.favor-die {
|
||||
font-family: $font-family-mono;
|
||||
|
||||
&.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 {
|
||||
@include flex-center;
|
||||
padding: $spacing-2;
|
||||
@ -117,6 +277,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Skill roll card specific
|
||||
.vagabond.chat-card.skill-roll {
|
||||
.skill-name {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Spell card specific
|
||||
.vagabond.chat-card.spell-card {
|
||||
.spell-effect {
|
||||
@ -142,7 +309,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Animation
|
||||
// Animations
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
@ -152,3 +319,16 @@
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateX(-5px);
|
||||
}
|
||||
75% {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,12 +37,17 @@
|
||||
select {
|
||||
@include input-base;
|
||||
width: 100%;
|
||||
height: 2.5rem; // Fixed height for consistent visibility
|
||||
padding: $spacing-2 $spacing-8 $spacing-2 $spacing-3;
|
||||
cursor: pointer;
|
||||
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-repeat: no-repeat;
|
||||
background-position: right $spacing-3 center;
|
||||
padding-right: $spacing-8;
|
||||
}
|
||||
|
||||
// Checkbox
|
||||
|
||||
@ -1,7 +1,211 @@
|
||||
// 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 {
|
||||
.dialog-content {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
79
templates/chat/skill-roll.hbs
Normal file
79
templates/chat/skill-roll.hbs
Normal 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>
|
||||
153
templates/dialog/favor-hinder-debug.hbs
Normal file
153
templates/dialog/favor-hinder-debug.hbs
Normal 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>
|
||||
66
templates/dialog/roll-dialog-base.hbs
Normal file
66
templates/dialog/roll-dialog-base.hbs
Normal 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>
|
||||
104
templates/dialog/skill-check.hbs
Normal file
104
templates/dialog/skill-check.hbs
Normal 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>
|
||||
462
test_results/2025-12-12-2241.txt
Normal file
462
test_results/2025-12-12-2241.txt
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user