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",
|
"Scene": "readonly",
|
||||||
"User": "readonly",
|
"User": "readonly",
|
||||||
"Folder": "readonly",
|
"Folder": "readonly",
|
||||||
|
"Macro": "readonly",
|
||||||
"Compendium": "readonly",
|
"Compendium": "readonly",
|
||||||
"CompendiumCollection": "readonly",
|
"CompendiumCollection": "readonly",
|
||||||
"DocumentSheetConfig": "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
|
## Resources
|
||||||
|
|
||||||
- [Foundry VTT API Documentation](https://foundryvtt.com/api/)
|
- [Foundry VTT API Documentation](https://foundryvtt.com/api/)
|
||||||
|
|||||||
@ -239,7 +239,7 @@
|
|||||||
"id": "2.1",
|
"id": "2.1",
|
||||||
"name": "Create main system entry point (vagabond.mjs)",
|
"name": "Create main system entry point (vagabond.mjs)",
|
||||||
"description": "System initialization, hook registration, CONFIG setup, document class registration",
|
"description": "System initialization, hook registration, CONFIG setup, document class registration",
|
||||||
"completed": false,
|
"completed": true,
|
||||||
"tested": false,
|
"tested": false,
|
||||||
"priority": "critical",
|
"priority": "critical",
|
||||||
"dependencies": ["1.1", "1.6"]
|
"dependencies": ["1.1", "1.6"]
|
||||||
@ -248,7 +248,7 @@
|
|||||||
"id": "2.2",
|
"id": "2.2",
|
||||||
"name": "Implement VagabondActor class",
|
"name": "Implement VagabondActor class",
|
||||||
"description": "Extended Actor with prepareData for derived values calculation (HP, Speed, Saves, Skill difficulties)",
|
"description": "Extended Actor with prepareData for derived values calculation (HP, Speed, Saves, Skill difficulties)",
|
||||||
"completed": false,
|
"completed": true,
|
||||||
"tested": false,
|
"tested": false,
|
||||||
"priority": "critical",
|
"priority": "critical",
|
||||||
"dependencies": ["2.1", "1.2", "1.3"]
|
"dependencies": ["2.1", "1.2", "1.3"]
|
||||||
@ -257,7 +257,7 @@
|
|||||||
"id": "2.3",
|
"id": "2.3",
|
||||||
"name": "Implement VagabondItem class",
|
"name": "Implement VagabondItem class",
|
||||||
"description": "Extended Item with type-specific preparation and chat card generation",
|
"description": "Extended Item with type-specific preparation and chat card generation",
|
||||||
"completed": false,
|
"completed": true,
|
||||||
"tested": false,
|
"tested": false,
|
||||||
"priority": "critical",
|
"priority": "critical",
|
||||||
"dependencies": ["2.1", "1.6"]
|
"dependencies": ["2.1", "1.6"]
|
||||||
@ -266,7 +266,7 @@
|
|||||||
"id": "2.4",
|
"id": "2.4",
|
||||||
"name": "Create dice rolling module",
|
"name": "Create dice rolling module",
|
||||||
"description": "Core roll functions: d20 checks, damage rolls, exploding dice (d6!), countdown dice, favor/hinder modifiers",
|
"description": "Core roll functions: d20 checks, damage rolls, exploding dice (d6!), countdown dice, favor/hinder modifiers",
|
||||||
"completed": false,
|
"completed": true,
|
||||||
"tested": false,
|
"tested": false,
|
||||||
"priority": "critical",
|
"priority": "critical",
|
||||||
"dependencies": ["2.1"]
|
"dependencies": ["2.1"]
|
||||||
|
|||||||
21
lang/en.json
21
lang/en.json
@ -163,6 +163,7 @@
|
|||||||
"VAGABOND.Success": "Success",
|
"VAGABOND.Success": "Success",
|
||||||
"VAGABOND.Failure": "Failure",
|
"VAGABOND.Failure": "Failure",
|
||||||
"VAGABOND.Critical": "Critical!",
|
"VAGABOND.Critical": "Critical!",
|
||||||
|
"VAGABOND.Fumble": "Fumble!",
|
||||||
|
|
||||||
"VAGABOND.Damage": "Damage",
|
"VAGABOND.Damage": "Damage",
|
||||||
"VAGABOND.DamageType": "Damage Type",
|
"VAGABOND.DamageType": "Damage Type",
|
||||||
@ -209,5 +210,23 @@
|
|||||||
"VAGABOND.ItemTypeFeature": "Feature",
|
"VAGABOND.ItemTypeFeature": "Feature",
|
||||||
"VAGABOND.ItemTypeWeapon": "Weapon",
|
"VAGABOND.ItemTypeWeapon": "Weapon",
|
||||||
"VAGABOND.ItemTypeArmor": "Armor",
|
"VAGABOND.ItemTypeArmor": "Armor",
|
||||||
"VAGABOND.ItemTypeEquipment": "Equipment"
|
"VAGABOND.ItemTypeEquipment": "Equipment",
|
||||||
|
|
||||||
|
"VAGABOND.RollDialog": "Roll",
|
||||||
|
"VAGABOND.SkillCheck": "Skill Check",
|
||||||
|
"VAGABOND.Check": "Check",
|
||||||
|
"VAGABOND.Skill": "Skill",
|
||||||
|
"VAGABOND.Stat": "Stat",
|
||||||
|
"VAGABOND.Training": "Training",
|
||||||
|
"VAGABOND.Untrained": "Untrained",
|
||||||
|
"VAGABOND.SelectSkill": "Select Skill...",
|
||||||
|
"VAGABOND.SelectSkillFirst": "Please select a skill first",
|
||||||
|
"VAGABOND.FavorHinder": "Favor / Hinder",
|
||||||
|
"VAGABOND.SituationalModifier": "Situational Modifier",
|
||||||
|
"VAGABOND.AutoFavor": "Auto Favor",
|
||||||
|
"VAGABOND.AutoHinder": "Auto Hinder",
|
||||||
|
"VAGABOND.Formula": "Formula",
|
||||||
|
|
||||||
|
"VAGABOND.SelectActor": "Select Actor",
|
||||||
|
"VAGABOND.Save": "Save"
|
||||||
}
|
}
|
||||||
|
|||||||
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)
|
// NOTE: Favor/Hinder is now handled via Active Effects flags instead of a data schema.
|
||||||
// Cancel each other 1-for-1, don't stack
|
// See DEVELOPMENT.md "Favor/Hinder via Active Effects" for the flag convention:
|
||||||
favorHinder: new fields.SchemaField({
|
// - flags.vagabond.favor.skills.<skillId>
|
||||||
favor: new fields.ArrayField(
|
// - flags.vagabond.hinder.skills.<skillId>
|
||||||
new fields.SchemaField({
|
// - flags.vagabond.favor.attacks
|
||||||
source: new fields.StringField({ required: true }), // "Flanking", "Virtuoso", etc.
|
// - flags.vagabond.hinder.attacks
|
||||||
appliesTo: new fields.ArrayField(new fields.StringField()), // ["Attack Checks"], ["Reflex Saves"]
|
// - flags.vagabond.favor.saves.<saveType>
|
||||||
duration: new fields.StringField({
|
// - flags.vagabond.hinder.saves.<saveType>
|
||||||
initial: "instant",
|
|
||||||
choices: ["instant", "until-next-turn", "focus", "continual", "permanent"],
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
{ initial: [] }
|
|
||||||
),
|
|
||||||
hinder: new fields.ArrayField(
|
|
||||||
new fields.SchemaField({
|
|
||||||
source: new fields.StringField({ required: true }), // "Heavy Armor", "Fog spell", etc.
|
|
||||||
appliesTo: new fields.ArrayField(new fields.StringField()), // ["Dodge Saves"], ["sight-based checks"]
|
|
||||||
duration: new fields.StringField({
|
|
||||||
initial: "instant",
|
|
||||||
choices: ["instant", "until-next-turn", "focus", "continual", "permanent"],
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
{ initial: [] }
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Focus tracking for maintained spells
|
// Focus tracking for maintained spells
|
||||||
focus: new fields.SchemaField({
|
focus: new fields.SchemaField({
|
||||||
|
|||||||
@ -130,8 +130,9 @@ export async function skillCheck(actor, skillId, options = {}) {
|
|||||||
const difficulty = skillData.difficulty;
|
const difficulty = skillData.difficulty;
|
||||||
const critThreshold = skillData.critThreshold || 20;
|
const critThreshold = skillData.critThreshold || 20;
|
||||||
|
|
||||||
// Determine favor/hinder
|
// Determine favor/hinder from Active Effect flags or override
|
||||||
const favorHinder = options.favorHinder ?? actor.getNetFavorHinder?.(`${skillId} Checks`) ?? 0;
|
const favorHinderResult = actor.getNetFavorHinder?.({ skillId }) ?? { net: 0 };
|
||||||
|
const favorHinder = options.favorHinder ?? favorHinderResult.net;
|
||||||
|
|
||||||
return d20Check({
|
return d20Check({
|
||||||
difficulty,
|
difficulty,
|
||||||
@ -170,8 +171,9 @@ export async function attackCheck(actor, weapon, options = {}) {
|
|||||||
// Get crit threshold from attack data
|
// Get crit threshold from attack data
|
||||||
const critThreshold = system.attacks?.[attackType]?.critThreshold || 20;
|
const critThreshold = system.attacks?.[attackType]?.critThreshold || 20;
|
||||||
|
|
||||||
// Determine favor/hinder
|
// Determine favor/hinder from Active Effect flags or override
|
||||||
const favorHinder = options.favorHinder ?? actor.getNetFavorHinder?.("Attack Checks") ?? 0;
|
const favorHinderResult = actor.getNetFavorHinder?.({ isAttack: true }) ?? { net: 0 };
|
||||||
|
const favorHinder = options.favorHinder ?? favorHinderResult.net;
|
||||||
|
|
||||||
return d20Check({
|
return d20Check({
|
||||||
difficulty,
|
difficulty,
|
||||||
@ -200,12 +202,9 @@ export async function saveRoll(actor, saveType, difficulty, options = {}) {
|
|||||||
throw new Error(`Unknown save type: ${saveType}`);
|
throw new Error(`Unknown save type: ${saveType}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine favor/hinder based on save type
|
// Determine favor/hinder from Active Effect flags or override
|
||||||
let rollType = `${saveType.charAt(0).toUpperCase() + saveType.slice(1)} Saves`;
|
const favorHinderResult = actor.getNetFavorHinder?.({ saveType }) ?? { net: 0 };
|
||||||
if (options.isBlock) rollType = "Block Saves";
|
const favorHinder = options.favorHinder ?? favorHinderResult.net;
|
||||||
if (options.isDodge) rollType = "Dodge Saves";
|
|
||||||
|
|
||||||
const favorHinder = options.favorHinder ?? actor.getNetFavorHinder?.(rollType) ?? 0;
|
|
||||||
|
|
||||||
return d20Check({
|
return d20Check({
|
||||||
difficulty,
|
difficulty,
|
||||||
|
|||||||
@ -464,29 +464,99 @@ export default class VagabondActor extends Actor {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the net favor/hinder for a specific roll type.
|
* Get the net favor/hinder for a specific roll type.
|
||||||
* Favor and Hinder cancel 1-for-1.
|
* Checks Active Effect flags for persistent favor/hinder sources.
|
||||||
|
* Favor and Hinder cancel 1-for-1, capped at +1 or -1.
|
||||||
*
|
*
|
||||||
* @param {string} rollType - The type of roll (e.g., "Attack Checks", "Reflex Saves")
|
* Flag convention (set by Active Effects):
|
||||||
* @returns {number} Net modifier: positive = favor, negative = hinder, 0 = neutral
|
* - flags.vagabond.favor.skills.<skillId> - Favor on specific skill
|
||||||
|
* - flags.vagabond.hinder.skills.<skillId> - Hinder on specific skill
|
||||||
|
* - flags.vagabond.favor.attacks - Favor on attack rolls
|
||||||
|
* - flags.vagabond.hinder.attacks - Hinder on attack rolls
|
||||||
|
* - flags.vagabond.favor.saves.<saveType> - Favor on specific save
|
||||||
|
* - flags.vagabond.hinder.saves.<saveType> - Hinder on specific save
|
||||||
|
*
|
||||||
|
* @param {Object} options - Options for determining favor/hinder
|
||||||
|
* @param {string} [options.skillId] - Skill ID for skill checks (e.g., "arcana", "brawl")
|
||||||
|
* @param {boolean} [options.isAttack] - True if this is an attack roll
|
||||||
|
* @param {string} [options.saveType] - Save type (e.g., "reflex", "endure", "will")
|
||||||
|
* @returns {Object} Result with net value and sources
|
||||||
|
* @returns {number} result.net - Net modifier: +1 (favor), 0 (neutral), -1 (hinder)
|
||||||
|
* @returns {string[]} result.favorSources - Names of active favor sources
|
||||||
|
* @returns {string[]} result.hinderSources - Names of active hinder sources
|
||||||
*/
|
*/
|
||||||
getNetFavorHinder(rollType) {
|
getNetFavorHinder({ skillId = null, isAttack = false, saveType = null } = {}) {
|
||||||
if (this.type !== "character") return 0;
|
if (this.type !== "character") return { net: 0, favorSources: [], hinderSources: [] };
|
||||||
|
|
||||||
const favorHinder = this.system.favorHinder;
|
const favorSources = [];
|
||||||
if (!favorHinder) return 0;
|
const hinderSources = [];
|
||||||
|
|
||||||
// Count favor sources that apply to this roll type
|
// Check skill-specific flags
|
||||||
const favorCount = (favorHinder.favor || []).filter(
|
if (skillId) {
|
||||||
(f) => !f.appliesTo?.length || f.appliesTo.includes(rollType)
|
if (this.getFlag("vagabond", `favor.skills.${skillId}`)) {
|
||||||
).length;
|
favorSources.push(this._getFavorHinderSourceName("favor", "skills", skillId));
|
||||||
|
}
|
||||||
|
if (this.getFlag("vagabond", `hinder.skills.${skillId}`)) {
|
||||||
|
hinderSources.push(this._getFavorHinderSourceName("hinder", "skills", skillId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Count hinder sources that apply to this roll type
|
// Check attack flags
|
||||||
const hinderCount = (favorHinder.hinder || []).filter(
|
if (isAttack) {
|
||||||
(h) => !h.appliesTo?.length || h.appliesTo.includes(rollType)
|
if (this.getFlag("vagabond", "favor.attacks")) {
|
||||||
).length;
|
favorSources.push(this._getFavorHinderSourceName("favor", "attacks"));
|
||||||
|
}
|
||||||
|
if (this.getFlag("vagabond", "hinder.attacks")) {
|
||||||
|
hinderSources.push(this._getFavorHinderSourceName("hinder", "attacks"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check save-specific flags
|
||||||
|
if (saveType) {
|
||||||
|
if (this.getFlag("vagabond", `favor.saves.${saveType}`)) {
|
||||||
|
favorSources.push(this._getFavorHinderSourceName("favor", "saves", saveType));
|
||||||
|
}
|
||||||
|
if (this.getFlag("vagabond", `hinder.saves.${saveType}`)) {
|
||||||
|
hinderSources.push(this._getFavorHinderSourceName("hinder", "saves", saveType));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// They cancel 1-for-1, max of +1 or -1
|
// They cancel 1-for-1, max of +1 or -1
|
||||||
const net = favorCount - hinderCount;
|
const net = Math.clamp(favorSources.length - hinderSources.length, -1, 1);
|
||||||
return Math.clamp(net, -1, 1);
|
|
||||||
|
return { net, favorSources, hinderSources };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the source name for a favor/hinder flag by finding the Active Effect that set it.
|
||||||
|
*
|
||||||
|
* @param {string} type - "favor" or "hinder"
|
||||||
|
* @param {string} category - "skills", "attacks", or "saves"
|
||||||
|
* @param {string} [subtype] - Skill ID or save type
|
||||||
|
* @returns {string} Source name or generic description
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_getFavorHinderSourceName(type, category, subtype = null) {
|
||||||
|
const flagKey = subtype
|
||||||
|
? `flags.vagabond.${type}.${category}.${subtype}`
|
||||||
|
: `flags.vagabond.${type}.${category}`;
|
||||||
|
|
||||||
|
// Find the Active Effect that sets this flag
|
||||||
|
for (const effect of this.effects) {
|
||||||
|
if (!effect.active) continue;
|
||||||
|
for (const change of effect.changes) {
|
||||||
|
if (change.key === flagKey) {
|
||||||
|
return effect.name || effect.parent?.name || `${type} effect`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback if source not found
|
||||||
|
const categoryLabel =
|
||||||
|
category === "skills"
|
||||||
|
? `${subtype} checks`
|
||||||
|
: category === "saves"
|
||||||
|
? `${subtype} saves`
|
||||||
|
: category;
|
||||||
|
return `${type} on ${categoryLabel}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -260,32 +260,78 @@ export function registerActorTests(quenchRunner) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Favor/Hinder System", () => {
|
describe("Favor/Hinder System", () => {
|
||||||
it("tracks favor and hinder modifiers separately", async () => {
|
it("detects favor from Active Effect flags", async () => {
|
||||||
/**
|
/**
|
||||||
* Favor adds +d6 to rolls, Hinder adds -d6.
|
* Favor/Hinder is now tracked via Active Effect flags instead of data schema.
|
||||||
* They cancel 1-for-1 and don't stack (multiple favors = still +1d6).
|
* Flag convention: flags.vagabond.favor.skills.<skillId>
|
||||||
* Each entry tracks: source, appliesTo (what rolls), duration.
|
* The getNetFavorHinder method checks these flags.
|
||||||
*/
|
*/
|
||||||
await testActor.update({
|
// Set a flag directly (simulating what an Active Effect would do)
|
||||||
"system.favorHinder.favor": [
|
await testActor.setFlag("vagabond", "favor.skills.performance", true);
|
||||||
{
|
|
||||||
source: "Flanking",
|
const result = testActor.getNetFavorHinder({ skillId: "performance" });
|
||||||
appliesTo: ["Attack Checks"],
|
expect(result.net).to.equal(1);
|
||||||
duration: "until-next-turn",
|
expect(result.favorSources.length).to.equal(1);
|
||||||
},
|
|
||||||
],
|
// Clean up
|
||||||
"system.favorHinder.hinder": [
|
await testActor.unsetFlag("vagabond", "favor.skills.performance");
|
||||||
{
|
|
||||||
source: "Heavy Armor",
|
|
||||||
appliesTo: ["Dodge Saves"],
|
|
||||||
duration: "permanent",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(testActor.system.favorHinder.favor.length).to.equal(1);
|
it("detects hinder from Active Effect flags", async () => {
|
||||||
expect(testActor.system.favorHinder.hinder.length).to.equal(1);
|
/**
|
||||||
expect(testActor.system.favorHinder.favor[0].source).to.equal("Flanking");
|
* Hinder flags work the same way as favor flags.
|
||||||
|
* Flag convention: flags.vagabond.hinder.skills.<skillId>
|
||||||
|
*/
|
||||||
|
await testActor.setFlag("vagabond", "hinder.skills.sneak", true);
|
||||||
|
|
||||||
|
const result = testActor.getNetFavorHinder({ skillId: "sneak" });
|
||||||
|
expect(result.net).to.equal(-1);
|
||||||
|
expect(result.hinderSources.length).to.equal(1);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
await testActor.unsetFlag("vagabond", "hinder.skills.sneak");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cancels favor and hinder 1-for-1", async () => {
|
||||||
|
/**
|
||||||
|
* When both favor and hinder apply to the same roll, they cancel out.
|
||||||
|
* Net result is clamped to -1, 0, or +1.
|
||||||
|
*/
|
||||||
|
await testActor.setFlag("vagabond", "favor.skills.arcana", true);
|
||||||
|
await testActor.setFlag("vagabond", "hinder.skills.arcana", true);
|
||||||
|
|
||||||
|
const result = testActor.getNetFavorHinder({ skillId: "arcana" });
|
||||||
|
expect(result.net).to.equal(0);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
await testActor.unsetFlag("vagabond", "favor.skills.arcana");
|
||||||
|
await testActor.unsetFlag("vagabond", "hinder.skills.arcana");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects favor/hinder for attack rolls", async () => {
|
||||||
|
/**
|
||||||
|
* Attack rolls check flags.vagabond.favor.attacks and hinder.attacks.
|
||||||
|
*/
|
||||||
|
await testActor.setFlag("vagabond", "favor.attacks", true);
|
||||||
|
|
||||||
|
const result = testActor.getNetFavorHinder({ isAttack: true });
|
||||||
|
expect(result.net).to.equal(1);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
await testActor.unsetFlag("vagabond", "favor.attacks");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects favor/hinder for save rolls", async () => {
|
||||||
|
/**
|
||||||
|
* Save rolls check flags.vagabond.favor.saves.<saveType>.
|
||||||
|
*/
|
||||||
|
await testActor.setFlag("vagabond", "hinder.saves.reflex", true);
|
||||||
|
|
||||||
|
const result = testActor.getNetFavorHinder({ saveType: "reflex" });
|
||||||
|
expect(result.net).to.equal(-1);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
await testActor.unsetFlag("vagabond", "hinder.saves.reflex");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -22,6 +22,9 @@ import {
|
|||||||
// Import document classes
|
// Import document classes
|
||||||
import { VagabondActor, VagabondItem } from "./documents/_module.mjs";
|
import { VagabondActor, VagabondItem } from "./documents/_module.mjs";
|
||||||
|
|
||||||
|
// Import application classes
|
||||||
|
import { VagabondRollDialog, SkillCheckDialog, FavorHinderDebug } from "./applications/_module.mjs";
|
||||||
|
|
||||||
// Import sheet classes
|
// Import sheet classes
|
||||||
// import { VagabondCharacterSheet } from "./sheets/actor-sheet.mjs";
|
// import { VagabondCharacterSheet } from "./sheets/actor-sheet.mjs";
|
||||||
// import { VagabondItemSheet } from "./sheets/item-sheet.mjs";
|
// import { VagabondItemSheet } from "./sheets/item-sheet.mjs";
|
||||||
@ -46,6 +49,15 @@ Hooks.once("init", () => {
|
|||||||
// Add custom constants for configuration
|
// Add custom constants for configuration
|
||||||
CONFIG.VAGABOND = VAGABOND;
|
CONFIG.VAGABOND = VAGABOND;
|
||||||
|
|
||||||
|
// Expose application classes globally for macro/API access
|
||||||
|
game.vagabond = {
|
||||||
|
applications: {
|
||||||
|
VagabondRollDialog,
|
||||||
|
SkillCheckDialog,
|
||||||
|
FavorHinderDebug,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// Register Actor data models
|
// Register Actor data models
|
||||||
CONFIG.Actor.dataModels = {
|
CONFIG.Actor.dataModels = {
|
||||||
character: CharacterData,
|
character: CharacterData,
|
||||||
@ -93,7 +105,7 @@ Hooks.once("init", () => {
|
|||||||
/**
|
/**
|
||||||
* Ready hook - runs when Foundry is fully loaded
|
* Ready hook - runs when Foundry is fully loaded
|
||||||
*/
|
*/
|
||||||
Hooks.once("ready", () => {
|
Hooks.once("ready", async () => {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log("Vagabond RPG | System Ready");
|
console.log("Vagabond RPG | System Ready");
|
||||||
|
|
||||||
@ -101,9 +113,58 @@ Hooks.once("ready", () => {
|
|||||||
if (game.user.isGM) {
|
if (game.user.isGM) {
|
||||||
const version = game.system.version;
|
const version = game.system.version;
|
||||||
ui.notifications.info(`Vagabond RPG v${version} - System loaded successfully!`);
|
ui.notifications.info(`Vagabond RPG v${version} - System loaded successfully!`);
|
||||||
|
|
||||||
|
// Create development macros if they don't exist
|
||||||
|
await _createDevMacros();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create development/debug macros if they don't already exist.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
async function _createDevMacros() {
|
||||||
|
// Favor/Hinder Debug macro
|
||||||
|
const debugMacroName = "Favor/Hinder Debug";
|
||||||
|
const existingMacro = game.macros.find((m) => m.name === debugMacroName);
|
||||||
|
|
||||||
|
if (!existingMacro) {
|
||||||
|
await Macro.create({
|
||||||
|
name: debugMacroName,
|
||||||
|
type: "script",
|
||||||
|
img: "icons/svg/bug.svg",
|
||||||
|
command: "game.vagabond.applications.FavorHinderDebug.open();",
|
||||||
|
flags: { vagabond: { systemMacro: true } },
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log("Vagabond RPG | Created Favor/Hinder Debug macro");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skill Check macro
|
||||||
|
const skillMacroName = "Skill Check";
|
||||||
|
const existingSkillMacro = game.macros.find((m) => m.name === skillMacroName);
|
||||||
|
|
||||||
|
if (!existingSkillMacro) {
|
||||||
|
await Macro.create({
|
||||||
|
name: skillMacroName,
|
||||||
|
type: "script",
|
||||||
|
img: "icons/svg/d20.svg",
|
||||||
|
command: `// Opens skill check dialog for selected token or prompts to select actor
|
||||||
|
const actor = canvas.tokens.controlled[0]?.actor
|
||||||
|
|| game.actors.find(a => a.type === "character");
|
||||||
|
|
||||||
|
if (!actor) {
|
||||||
|
ui.notifications.warn("Select a token or create a character first");
|
||||||
|
} else {
|
||||||
|
game.vagabond.applications.SkillCheckDialog.prompt(actor);
|
||||||
|
}`,
|
||||||
|
flags: { vagabond: { systemMacro: true } },
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log("Vagabond RPG | Created Skill Check macro");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
/* Handlebars Helpers */
|
/* Handlebars Helpers */
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Vagabond RPG - Chat Card Styles
|
// Vagabond RPG - Chat Card Styles
|
||||||
// ================================
|
// ================================
|
||||||
|
|
||||||
// Placeholder - will be expanded in Phase 7
|
// Base chat card
|
||||||
.vagabond.chat-card {
|
.vagabond.chat-card {
|
||||||
@include panel;
|
@include panel;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -13,16 +13,27 @@
|
|||||||
background-color: $color-parchment-dark;
|
background-color: $color-parchment-dark;
|
||||||
border-bottom: 1px solid $color-border;
|
border-bottom: 1px solid $color-border;
|
||||||
|
|
||||||
.card-title {
|
.card-title,
|
||||||
|
h3 {
|
||||||
font-family: $font-family-header;
|
font-family: $font-family-header;
|
||||||
font-size: $font-size-base;
|
font-size: $font-size-base;
|
||||||
font-weight: $font-weight-bold;
|
font-weight: $font-weight-bold;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-subtitle {
|
.card-subtitle {
|
||||||
font-size: $font-size-sm;
|
font-size: $font-size-sm;
|
||||||
color: $color-text-muted;
|
color: $color-text-muted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.trained-badge {
|
||||||
|
font-size: $font-size-xs;
|
||||||
|
padding: $spacing-1 $spacing-2;
|
||||||
|
background-color: rgba($color-success, 0.2);
|
||||||
|
color: $color-success;
|
||||||
|
border-radius: $radius-full;
|
||||||
|
font-weight: $font-weight-medium;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Card content
|
// Card content
|
||||||
@ -30,27 +41,176 @@
|
|||||||
padding: $spacing-3;
|
padding: $spacing-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Roll result
|
// Roll result (large total display)
|
||||||
.roll-result {
|
.roll-result {
|
||||||
@include flex-center;
|
@include flex-column;
|
||||||
gap: $spacing-3;
|
align-items: center;
|
||||||
|
gap: $spacing-2;
|
||||||
padding: $spacing-3;
|
padding: $spacing-3;
|
||||||
|
margin: $spacing-2 0;
|
||||||
background-color: $color-parchment-light;
|
background-color: $color-parchment-light;
|
||||||
border-radius: $radius-md;
|
border-radius: $radius-md;
|
||||||
|
border: 2px solid $color-border;
|
||||||
|
|
||||||
.roll-total {
|
.roll-total {
|
||||||
font-family: $font-family-header;
|
font-family: $font-family-header;
|
||||||
font-size: $font-size-3xl;
|
font-size: $font-size-4xl;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roll-status {
|
||||||
|
.status {
|
||||||
|
display: inline-block;
|
||||||
|
padding: $spacing-1 $spacing-3;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
background-color: rgba($color-success, 0.2);
|
||||||
|
color: $color-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
.failure {
|
||||||
|
background-color: rgba($color-danger, 0.2);
|
||||||
|
color: $color-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.critical {
|
||||||
|
background-color: rgba($color-warning, 0.3);
|
||||||
|
color: $color-warning;
|
||||||
|
animation: pulse 1s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fumble {
|
||||||
|
background-color: rgba($color-danger, 0.3);
|
||||||
|
color: $color-danger;
|
||||||
|
animation: shake 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conditional styling based on result
|
||||||
|
&.success {
|
||||||
|
border-color: $color-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.failure {
|
||||||
|
border-color: $color-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.critical {
|
||||||
|
border-color: $color-warning;
|
||||||
|
background-color: rgba($color-warning, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fumble {
|
||||||
|
border-color: $color-danger;
|
||||||
|
background-color: rgba($color-danger, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Roll details
|
||||||
|
.roll-details {
|
||||||
|
@include flex-column;
|
||||||
|
gap: $spacing-2;
|
||||||
|
padding: $spacing-2;
|
||||||
|
background-color: $color-parchment-dark;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
|
||||||
|
.roll-formula {
|
||||||
|
@include flex-between;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: $color-text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-family: $font-family-mono;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.roll-breakdown {
|
||||||
|
@include flex-center;
|
||||||
|
gap: $spacing-3;
|
||||||
|
padding-top: $spacing-2;
|
||||||
|
border-top: 1px solid $color-border;
|
||||||
|
|
||||||
|
span {
|
||||||
|
@include flex-center;
|
||||||
|
gap: $spacing-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d20-result {
|
||||||
|
font-family: $font-family-mono;
|
||||||
font-weight: $font-weight-bold;
|
font-weight: $font-weight-bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.roll-formula {
|
.favor-die {
|
||||||
font-size: $font-size-sm;
|
font-family: $font-family-mono;
|
||||||
color: $color-text-muted;
|
|
||||||
|
&.favor {
|
||||||
|
color: $color-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hinder {
|
||||||
|
color: $color-danger;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Result status
|
.modifier {
|
||||||
|
font-family: $font-family-mono;
|
||||||
|
color: $color-text-secondary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Target info (difficulty, crit threshold)
|
||||||
|
.target-info {
|
||||||
|
@include grid(2, $spacing-2);
|
||||||
|
margin-top: $spacing-2;
|
||||||
|
padding: $spacing-2;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include flex-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: $color-text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-weight: $font-weight-medium;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Favor/Hinder sources
|
||||||
|
.favor-sources,
|
||||||
|
.hinder-sources {
|
||||||
|
@include flex-center;
|
||||||
|
gap: $spacing-2;
|
||||||
|
margin-top: $spacing-2;
|
||||||
|
padding: $spacing-2;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favor-sources {
|
||||||
|
background-color: rgba($color-success, 0.1);
|
||||||
|
color: $color-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hinder-sources {
|
||||||
|
background-color: rgba($color-danger, 0.1);
|
||||||
|
color: $color-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result status (legacy)
|
||||||
.result-status {
|
.result-status {
|
||||||
@include flex-center;
|
@include flex-center;
|
||||||
padding: $spacing-2;
|
padding: $spacing-2;
|
||||||
@ -117,6 +277,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skill roll card specific
|
||||||
|
.vagabond.chat-card.skill-roll {
|
||||||
|
.skill-name {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Spell card specific
|
// Spell card specific
|
||||||
.vagabond.chat-card.spell-card {
|
.vagabond.chat-card.spell-card {
|
||||||
.spell-effect {
|
.spell-effect {
|
||||||
@ -142,7 +309,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Animation
|
// Animations
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%,
|
0%,
|
||||||
100% {
|
100% {
|
||||||
@ -152,3 +319,16 @@
|
|||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes shake {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: translateX(-5px);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -37,12 +37,17 @@
|
|||||||
select {
|
select {
|
||||||
@include input-base;
|
@include input-base;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 2.5rem; // Fixed height for consistent visibility
|
||||||
|
padding: $spacing-2 $spacing-8 $spacing-2 $spacing-3;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
background-color: $color-parchment-light !important;
|
||||||
|
color: $color-text-primary !important;
|
||||||
|
font-size: $font-size-base;
|
||||||
|
line-height: 1.5;
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%232c2416' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%232c2416' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: right $spacing-3 center;
|
background-position: right $spacing-3 center;
|
||||||
padding-right: $spacing-8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checkbox
|
// Checkbox
|
||||||
|
|||||||
@ -1,7 +1,211 @@
|
|||||||
// Vagabond RPG - Roll Dialog Styles
|
// Vagabond RPG - Roll Dialog Styles
|
||||||
// ==================================
|
// ==================================
|
||||||
|
|
||||||
// Placeholder - will be expanded in Phase 7
|
// ApplicationV2 Roll Dialog Base
|
||||||
|
.vagabond.roll-dialog {
|
||||||
|
// Force light background on the entire window
|
||||||
|
background-color: $color-parchment;
|
||||||
|
color: $color-text-primary;
|
||||||
|
|
||||||
|
.roll-dialog-content {
|
||||||
|
@include flex-column;
|
||||||
|
gap: $spacing-3;
|
||||||
|
padding: $spacing-4;
|
||||||
|
background-color: $color-parchment;
|
||||||
|
color: $color-text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Automatic favor/hinder from Active Effects
|
||||||
|
.auto-favor-hinder {
|
||||||
|
@include flex-center;
|
||||||
|
gap: $spacing-2;
|
||||||
|
padding: $spacing-2 $spacing-3;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: $font-size-base;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.favor {
|
||||||
|
background-color: rgba($color-success, 0.15);
|
||||||
|
color: $color-success;
|
||||||
|
border: 1px solid $color-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hinder {
|
||||||
|
background-color: rgba($color-danger, 0.15);
|
||||||
|
color: $color-danger;
|
||||||
|
border: 1px solid $color-danger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skill selection
|
||||||
|
.skill-selection {
|
||||||
|
@include flex-column;
|
||||||
|
gap: $spacing-2;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: $font-weight-semibold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skill info panel
|
||||||
|
.skill-info {
|
||||||
|
@include panel;
|
||||||
|
@include grid(2, $spacing-2);
|
||||||
|
padding: $spacing-3;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@include flex-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
color: $color-text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-weight: $font-weight-medium;
|
||||||
|
|
||||||
|
&.trained {
|
||||||
|
color: $color-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.untrained {
|
||||||
|
color: $color-text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.difficulty {
|
||||||
|
font-family: $font-family-header;
|
||||||
|
font-size: $font-size-lg;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.crit {
|
||||||
|
color: $color-warning;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Favor/Hinder toggle section
|
||||||
|
.favor-hinder-section {
|
||||||
|
@include flex-column;
|
||||||
|
gap: $spacing-2;
|
||||||
|
|
||||||
|
> label {
|
||||||
|
font-weight: $font-weight-semibold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favor-hinder-toggles {
|
||||||
|
display: flex;
|
||||||
|
gap: $spacing-2;
|
||||||
|
|
||||||
|
button {
|
||||||
|
@include button-secondary;
|
||||||
|
flex: 1;
|
||||||
|
gap: $spacing-2;
|
||||||
|
|
||||||
|
&.favor-btn {
|
||||||
|
&.active,
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba($color-success, 0.15);
|
||||||
|
border-color: $color-success;
|
||||||
|
color: $color-success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hinder-btn {
|
||||||
|
&.active,
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba($color-danger, 0.15);
|
||||||
|
border-color: $color-danger;
|
||||||
|
color: $color-danger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.net-favor-hinder {
|
||||||
|
@include flex-center;
|
||||||
|
padding: $spacing-1 $spacing-2;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
font-weight: $font-weight-medium;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
|
||||||
|
&.favor {
|
||||||
|
background-color: rgba($color-success, 0.15);
|
||||||
|
color: $color-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hinder {
|
||||||
|
background-color: rgba($color-danger, 0.15);
|
||||||
|
color: $color-danger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Situational modifier section
|
||||||
|
.modifier-section {
|
||||||
|
@include flex-column;
|
||||||
|
gap: $spacing-2;
|
||||||
|
|
||||||
|
> label {
|
||||||
|
font-weight: $font-weight-semibold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modifier-presets {
|
||||||
|
display: flex;
|
||||||
|
gap: $spacing-2;
|
||||||
|
|
||||||
|
.modifier-preset {
|
||||||
|
@include button-secondary;
|
||||||
|
flex: 1;
|
||||||
|
padding: $spacing-1 $spacing-2;
|
||||||
|
font-family: $font-family-mono;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $color-parchment-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modifier-input {
|
||||||
|
input {
|
||||||
|
@include input-base;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-family: $font-family-mono;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Roll button
|
||||||
|
.dialog-buttons {
|
||||||
|
margin-top: $spacing-2;
|
||||||
|
|
||||||
|
.roll-btn {
|
||||||
|
@include button-primary;
|
||||||
|
width: 100%;
|
||||||
|
padding: $spacing-3;
|
||||||
|
font-size: $font-size-lg;
|
||||||
|
gap: $spacing-2;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skill check dialog specific
|
||||||
|
.vagabond.skill-check-dialog {
|
||||||
|
// Additional skill-specific styles if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy dialog styles (for backward compatibility)
|
||||||
.vagabond.dialog.roll-dialog {
|
.vagabond.dialog.roll-dialog {
|
||||||
.dialog-content {
|
.dialog-content {
|
||||||
padding: $spacing-4;
|
padding: $spacing-4;
|
||||||
@ -152,3 +356,177 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Favor/Hinder Debug Panel
|
||||||
|
.vagabond.favor-hinder-debug {
|
||||||
|
// Force light background on the entire window content
|
||||||
|
// Using !important to override Foundry's dark theme styles
|
||||||
|
background-color: $color-parchment !important;
|
||||||
|
color: $color-text-primary !important;
|
||||||
|
|
||||||
|
* {
|
||||||
|
color: $color-text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable scrolling on the window content
|
||||||
|
.window-content {
|
||||||
|
overflow-y: auto !important;
|
||||||
|
max-height: 80vh;
|
||||||
|
@include custom-scrollbar;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favor-hinder-debug-content {
|
||||||
|
@include flex-column;
|
||||||
|
gap: $spacing-4;
|
||||||
|
padding: $spacing-4;
|
||||||
|
background-color: $color-parchment !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actor-selection {
|
||||||
|
@include flex-column;
|
||||||
|
gap: $spacing-2;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: $font-weight-semibold;
|
||||||
|
color: $color-text-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-panels {
|
||||||
|
@include flex-column;
|
||||||
|
gap: $spacing-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-panel {
|
||||||
|
@include panel;
|
||||||
|
padding: $spacing-3;
|
||||||
|
background-color: $color-parchment-light;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
@include flex-center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: $spacing-2;
|
||||||
|
margin: 0 0 $spacing-3 0;
|
||||||
|
padding-bottom: $spacing-2;
|
||||||
|
border-bottom: 1px solid $color-border;
|
||||||
|
font-size: $font-size-base;
|
||||||
|
color: $color-text-primary;
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: $color-accent-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background-color: $color-parchment-light;
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: $spacing-2;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid $color-border-light;
|
||||||
|
color: $color-text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
font-weight: $font-weight-semibold;
|
||||||
|
color: $color-text-secondary;
|
||||||
|
background-color: $color-parchment-dark;
|
||||||
|
|
||||||
|
&.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
background-color: $color-parchment-light !important;
|
||||||
|
|
||||||
|
&.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.skill-name {
|
||||||
|
@include flex-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alternating row colors with good contrast
|
||||||
|
tbody tr:nth-child(even) td {
|
||||||
|
background-color: $color-parchment !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-tag {
|
||||||
|
font-size: $font-size-xs;
|
||||||
|
padding: $spacing-1 $spacing-2;
|
||||||
|
background-color: $color-parchment-dark;
|
||||||
|
border-radius: $radius-full;
|
||||||
|
color: $color-text-secondary;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: $color-accent-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:hover td {
|
||||||
|
background-color: $color-parchment-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: $spacing-3;
|
||||||
|
padding-top: $spacing-3;
|
||||||
|
border-top: 1px solid $color-border;
|
||||||
|
|
||||||
|
button {
|
||||||
|
@include button-base;
|
||||||
|
flex: 1;
|
||||||
|
gap: $spacing-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-roll-btn {
|
||||||
|
@include button-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-btn {
|
||||||
|
@include button-secondary;
|
||||||
|
color: $color-danger;
|
||||||
|
border-color: $color-danger;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background-color: rgba($color-danger, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-actor-message {
|
||||||
|
@include flex-column;
|
||||||
|
align-items: center;
|
||||||
|
gap: $spacing-3;
|
||||||
|
padding: $spacing-6;
|
||||||
|
text-align: center;
|
||||||
|
color: $color-text-muted;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: $font-size-4xl;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
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