Add Dodge and Block defense rolls to character sheet

- Add Dodge roll under Reflex save (auto-hindered by heavy armor)
- Add Block roll under Endure save (hindered vs ranged attacks toggle)
- Create DodgeRollDialog and BlockRollDialog with templates
- Display defense rolls as indented sub-rows on Main tab
- Block row visually dimmed when no shield equipped, shows notification on click

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2025-12-29 11:40:12 -06:00
parent ed694372d5
commit 77c9359601
11 changed files with 742 additions and 7 deletions

View File

@ -262,6 +262,9 @@
"VAGABOND.DodgeInfo": "Dodge allows you to avoid the attack entirely",
"VAGABOND.BlockedWith": "Blocked with shield",
"VAGABOND.DodgedAttack": "Dodged the attack",
"VAGABOND.VsRangedAttack": "vs Ranged Attack?",
"VAGABOND.HinderedByHeavyArmor": "Heavy Armor",
"VAGABOND.HinderedByRanged": "Ranged Attack",
"VAGABOND.CriticalSuccess": "Critical Success!",
"VAGABOND.CastSpell": "Cast Spell",

View File

@ -7,6 +7,8 @@ export { default as VagabondRollDialog } from "./base-roll-dialog.mjs";
export { default as SkillCheckDialog } from "./skill-check-dialog.mjs";
export { default as AttackRollDialog } from "./attack-roll-dialog.mjs";
export { default as SaveRollDialog } from "./save-roll-dialog.mjs";
export { default as DodgeRollDialog } from "./dodge-roll-dialog.mjs";
export { default as BlockRollDialog } from "./block-roll-dialog.mjs";
export { default as SpellCastDialog } from "./spell-cast-dialog.mjs";
export { default as FavorHinderDebug } from "./favor-hinder-debug.mjs";
export { default as LevelUpDialog } from "./level-up-dialog.mjs";

View File

@ -0,0 +1,208 @@
/**
* Block Roll Dialog for Vagabond RPG
*
* Dialog for block defense rolls:
* - Uses Endure save difficulty
* - Has "vs Ranged Attack?" toggle that applies hinder when checked
* - Requires shield to be equipped
*
* @extends VagabondRollDialog
*/
import VagabondRollDialog from "./base-roll-dialog.mjs";
import { saveRoll } from "../dice/rolls.mjs";
export default class BlockRollDialog extends VagabondRollDialog {
/**
* @param {VagabondActor} actor - The actor making the roll
* @param {Object} options - Dialog options
*/
constructor(actor, options = {}) {
super(actor, options);
// Block always uses Endure
this.saveType = "endure";
this.defenseType = "block";
this.vsRanged = false; // Toggle state for ranged attack hinder
// Load base favor/hinder for endure saves
this.rollConfig.autoFavorHinder = actor.getNetFavorHinder({ saveType: "endure" });
}
/* -------------------------------------------- */
/* Static Properties */
/* -------------------------------------------- */
/** @override */
static DEFAULT_OPTIONS = foundry.utils.mergeObject(
super.DEFAULT_OPTIONS,
{
id: "vagabond-block-roll-dialog",
window: {
title: "VAGABOND.Block",
icon: "fa-solid fa-shield",
},
position: {
width: 340,
},
},
{ inplace: false }
);
/** @override */
static PARTS = {
form: {
template: "systems/vagabond/templates/dialog/block-roll.hbs",
},
};
/* -------------------------------------------- */
/* Getters */
/* -------------------------------------------- */
/** @override */
get title() {
return game.i18n.localize("VAGABOND.Block");
}
/**
* Get the difficulty for this block (Endure save difficulty).
* @returns {number}
*/
get difficulty() {
return this.actor.system.saves.endure.difficulty;
}
/**
* Get the net favor/hinder value including ranged toggle.
* @override
* @returns {number} -1, 0, or +1
*/
get netFavorHinder() {
const manual = this.rollConfig.favorHinder;
let auto = this.rollConfig.autoFavorHinder.net;
// Apply ranged hinder if toggled
if (this.vsRanged) {
auto = Math.max(-1, auto - 1);
}
return Math.clamp(manual + auto, -1, 1);
}
/* -------------------------------------------- */
/* Data Preparation */
/* -------------------------------------------- */
/** @override */
async _prepareRollContext(_options) {
// Build hinder sources including ranged if toggled
const hinderSources = [...this.rollConfig.autoFavorHinder.hinderSources];
if (this.vsRanged) {
hinderSources.push(game.i18n.localize("VAGABOND.HinderedByRanged"));
}
return {
difficulty: this.difficulty,
vsRanged: this.vsRanged,
saveStats: "MIT + MIT",
hinderSources,
};
}
/* -------------------------------------------- */
/* Event Handlers */
/* -------------------------------------------- */
/** @override */
_onRender(context, options) {
super._onRender(context, options);
// vs Ranged toggle
const rangedToggle = this.element.querySelector('[name="vsRanged"]');
rangedToggle?.addEventListener("change", (event) => {
this.vsRanged = event.target.checked;
this.render();
});
}
/* -------------------------------------------- */
/* Roll Execution */
/* -------------------------------------------- */
/** @override */
async _executeRoll() {
const result = await saveRoll(this.actor, "endure", this.difficulty, {
favorHinder: this.netFavorHinder,
modifier: this.rollConfig.modifier,
isBlock: true,
});
await this._sendToChat(result);
}
/**
* Send the roll result to chat.
*
* @param {VagabondRollResult} result - The roll result
* @returns {Promise<ChatMessage>}
* @private
*/
async _sendToChat(result) {
// Build hinder sources including ranged
const hinderSources = [...this.rollConfig.autoFavorHinder.hinderSources];
if (this.vsRanged) {
hinderSources.push(game.i18n.localize("VAGABOND.HinderedByRanged"));
}
const templateData = {
actor: this.actor,
saveType: "endure",
saveLabel: game.i18n.localize("VAGABOND.SaveEndure"),
stats: ["MIT", "MIT"],
difficulty: result.difficulty,
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,
isDefense: true,
defenseType: "block",
defenseLabel: game.i18n.localize("VAGABOND.Block"),
};
const content = await renderTemplate(
"systems/vagabond/templates/chat/save-roll.hbs",
templateData
);
return ChatMessage.create({
user: game.user.id,
speaker: ChatMessage.getSpeaker({ actor: this.actor }),
content,
rolls: [result.roll],
sound: CONFIG.sounds.dice,
});
}
/* -------------------------------------------- */
/* Static Methods */
/* -------------------------------------------- */
/**
* Create and render a block roll dialog.
*
* @param {VagabondActor} actor - The actor making the roll
* @param {Object} [options] - Additional options
* @returns {Promise<BlockRollDialog>}
*/
static async prompt(actor, options = {}) {
return this.create(actor, options);
}
}

View File

@ -0,0 +1,169 @@
/**
* Dodge Roll Dialog for Vagabond RPG
*
* Simplified dialog for dodge defense rolls:
* - Uses Reflex save difficulty
* - Automatically applies hinder if wearing heavy armor
* - Shows favor/hinder toggles and modifiers
*
* @extends VagabondRollDialog
*/
import VagabondRollDialog from "./base-roll-dialog.mjs";
import { saveRoll } from "../dice/rolls.mjs";
export default class DodgeRollDialog extends VagabondRollDialog {
/**
* @param {VagabondActor} actor - The actor making the roll
* @param {Object} options - Dialog options
*/
constructor(actor, options = {}) {
super(actor, options);
// Dodge always uses Reflex
this.saveType = "reflex";
this.defenseType = "dodge";
// Check for heavy armor hinder
this.hasHeavyArmor = actor.getEquippedArmor().some((a) => a.system.hindersDodge);
// Build auto favor/hinder including heavy armor
const baseFavorHinder = actor.getNetFavorHinder({ saveType: "reflex" });
if (this.hasHeavyArmor) {
baseFavorHinder.hinderSources.push(game.i18n.localize("VAGABOND.HinderedByHeavyArmor"));
baseFavorHinder.net = Math.max(-1, baseFavorHinder.net - 1);
}
this.rollConfig.autoFavorHinder = baseFavorHinder;
}
/* -------------------------------------------- */
/* Static Properties */
/* -------------------------------------------- */
/** @override */
static DEFAULT_OPTIONS = foundry.utils.mergeObject(
super.DEFAULT_OPTIONS,
{
id: "vagabond-dodge-roll-dialog",
window: {
title: "VAGABOND.Dodge",
icon: "fa-solid fa-person-running",
},
position: {
width: 340,
},
},
{ inplace: false }
);
/** @override */
static PARTS = {
form: {
template: "systems/vagabond/templates/dialog/dodge-roll.hbs",
},
};
/* -------------------------------------------- */
/* Getters */
/* -------------------------------------------- */
/** @override */
get title() {
return game.i18n.localize("VAGABOND.Dodge");
}
/**
* Get the difficulty for this dodge (Reflex save difficulty).
* @returns {number}
*/
get difficulty() {
return this.actor.system.saves.reflex.difficulty;
}
/* -------------------------------------------- */
/* Data Preparation */
/* -------------------------------------------- */
/** @override */
async _prepareRollContext(_options) {
return {
difficulty: this.difficulty,
hasHeavyArmor: this.hasHeavyArmor,
saveStats: "DEX + AWR",
};
}
/* -------------------------------------------- */
/* Roll Execution */
/* -------------------------------------------- */
/** @override */
async _executeRoll() {
const result = await saveRoll(this.actor, "reflex", this.difficulty, {
favorHinder: this.netFavorHinder,
modifier: this.rollConfig.modifier,
isDodge: true,
});
await this._sendToChat(result);
}
/**
* Send the roll result to chat.
*
* @param {VagabondRollResult} result - The roll result
* @returns {Promise<ChatMessage>}
* @private
*/
async _sendToChat(result) {
const templateData = {
actor: this.actor,
saveType: "reflex",
saveLabel: game.i18n.localize("VAGABOND.SaveReflex"),
stats: ["DEX", "AWR"],
difficulty: result.difficulty,
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,
isDefense: true,
defenseType: "dodge",
defenseLabel: game.i18n.localize("VAGABOND.Dodge"),
};
const content = await renderTemplate(
"systems/vagabond/templates/chat/save-roll.hbs",
templateData
);
return ChatMessage.create({
user: game.user.id,
speaker: ChatMessage.getSpeaker({ actor: this.actor }),
content,
rolls: [result.roll],
sound: CONFIG.sounds.dice,
});
}
/* -------------------------------------------- */
/* Static Methods */
/* -------------------------------------------- */
/**
* Create and render a dodge roll dialog.
*
* @param {VagabondActor} actor - The actor making the roll
* @param {Object} [options] - Additional options
* @returns {Promise<DodgeRollDialog>}
*/
static async prompt(actor, options = {}) {
return this.create(actor, options);
}
}

View File

@ -58,6 +58,8 @@ export default class VagabondActorSheet extends HandlebarsApplicationMixin(Actor
editImage: VagabondActorSheet.#onEditImage,
rollSkill: VagabondActorSheet.#onRollSkill,
rollSave: VagabondActorSheet.#onRollSave,
rollDodge: VagabondActorSheet.#onRollDodge,
rollBlock: VagabondActorSheet.#onRollBlock,
rollAttack: VagabondActorSheet.#onRollAttack,
castSpell: VagabondActorSheet.#onCastSpell,
itemEdit: VagabondActorSheet.#onItemEdit,
@ -685,6 +687,39 @@ export default class VagabondActorSheet extends HandlebarsApplicationMixin(Actor
await SaveRollDialog.prompt(this.actor, saveType);
}
/**
* Handle dodge roll action.
* @param {PointerEvent} event
* @param {HTMLElement} target
*/
static async #onRollDodge(event, target) {
event.preventDefault();
const { DodgeRollDialog } = game.vagabond.applications;
await DodgeRollDialog.prompt(this.actor);
}
/**
* Handle block roll action.
* @param {PointerEvent} event
* @param {HTMLElement} target
*/
static async #onRollBlock(event, target) {
event.preventDefault();
// Check if shield is equipped
const hasShield = this.actor
.getEquippedArmor()
.some((armor) => armor.system.armorType === "shield");
if (!hasShield) {
ui.notifications.warn(game.i18n.localize("VAGABOND.RequiresShield"));
return;
}
const { BlockRollDialog } = game.vagabond.applications;
await BlockRollDialog.prompt(this.actor);
}
/**
* Handle attack roll action.
* @param {PointerEvent} event

View File

@ -226,6 +226,11 @@ export default class VagabondCharacterSheet extends VagabondActorSheet {
_prepareSaves() {
const system = this.actor.system;
// Check armor conditions for defense rolls
const equippedArmor = this.actor.getEquippedArmor();
const hasHeavyArmor = equippedArmor.some((a) => a.system.hindersDodge);
const hasShield = equippedArmor.some((a) => a.system.armorType === "shield");
return {
reflex: {
id: "reflex",
@ -233,6 +238,11 @@ export default class VagabondCharacterSheet extends VagabondActorSheet {
stats: "DEX + AWR",
difficulty: system.saves.reflex.difficulty,
bonus: system.saves.reflex.bonus,
dodge: {
difficulty: system.saves.reflex.difficulty,
hindered: hasHeavyArmor,
hinderSource: hasHeavyArmor ? "VAGABOND.HinderedByHeavyArmor" : null,
},
},
endure: {
id: "endure",
@ -240,6 +250,10 @@ export default class VagabondCharacterSheet extends VagabondActorSheet {
stats: "MIT + MIT",
difficulty: system.saves.endure.difficulty,
bonus: system.saves.endure.bonus,
block: {
difficulty: system.saves.endure.difficulty,
hasShield,
},
},
will: {
id: "will",

View File

@ -31,6 +31,8 @@ import {
SkillCheckDialog,
AttackRollDialog,
SaveRollDialog,
DodgeRollDialog,
BlockRollDialog,
SpellCastDialog,
FavorHinderDebug,
} from "./applications/_module.mjs";
@ -109,6 +111,8 @@ Hooks.once("init", async () => {
SkillCheckDialog,
AttackRollDialog,
SaveRollDialog,
DodgeRollDialog,
BlockRollDialog,
SpellCastDialog,
FavorHinderDebug,
},

View File

@ -603,6 +603,87 @@
transition: color $transition-fast;
}
}
// Defense sub-rows (Dodge, Block) - indented under parent saves
.defense-row {
display: flex;
align-items: center;
gap: $spacing-2;
padding: $spacing-1 $spacing-3;
padding-left: $spacing-6;
margin-left: $spacing-4;
background-color: var(--color-bg-input);
border: 1px solid var(--color-border-light);
border-radius: $radius-md;
cursor: pointer;
transition: all $transition-fast;
font-size: $font-size-sm;
&:hover:not(.disabled) {
background-color: var(--color-bg-secondary);
border-color: var(--color-accent-primary);
.roll-icon {
color: var(--color-accent-primary);
}
}
&.no-shield {
opacity: 0.5;
&:hover {
background-color: var(--color-bg-input);
border-color: var(--color-border-light);
}
}
.defense-icon {
font-size: $font-size-sm;
color: var(--color-text-secondary);
width: 16px;
text-align: center;
}
.defense-label {
font-weight: $font-weight-medium;
color: var(--color-text-primary);
margin-right: auto;
}
.hinder-indicator {
color: var(--color-warning);
font-size: $font-size-xs;
i {
font-size: 10px;
}
}
.defense-difficulty {
font-family: $font-family-header;
font-size: $font-size-base;
font-weight: $font-weight-bold;
color: var(--color-text-primary);
min-width: 24px;
text-align: center;
}
.roll-icon {
color: var(--color-text-muted);
font-size: $font-size-sm;
transition: color $transition-fast;
}
// Dodge row icon color
&.dodge-row .defense-icon {
color: var(--color-info);
}
// Block row icon color
&.block-row .defense-icon {
color: var(--color-success);
}
}
}
// Skills Section

View File

@ -22,16 +22,57 @@
<div class="saves-section">
<h2 class="section-header">{{localize "VAGABOND.Saves"}}</h2>
<div class="saves-list">
{{#each saves}}
{{!-- Reflex Save --}}
<div class="save-row interactive-row" role="button" tabindex="0"
data-action="rollSave" data-save="{{this.id}}"
aria-label="{{localize this.label}} {{localize 'VAGABOND.Save'}}: {{localize 'VAGABOND.Difficulty'}} {{this.difficulty}}">
<span class="save-label">{{localize this.label}}</span>
<span class="save-stats">({{this.stats}})</span>
<span class="save-difficulty">{{this.difficulty}}</span>
data-action="rollSave" data-save="reflex"
aria-label="{{localize saves.reflex.label}} {{localize 'VAGABOND.Save'}}: {{localize 'VAGABOND.Difficulty'}} {{saves.reflex.difficulty}}">
<span class="save-label">{{localize saves.reflex.label}}</span>
<span class="save-stats">({{saves.reflex.stats}})</span>
<span class="save-difficulty">{{saves.reflex.difficulty}}</span>
<i class="fa-solid fa-dice-d20 roll-icon" aria-hidden="true"></i>
</div>
{{!-- Dodge sub-row (indented under Reflex) --}}
<div class="defense-row dodge-row interactive-row" role="button" tabindex="0"
data-action="rollDodge"
aria-label="{{localize 'VAGABOND.Dodge'}}: {{localize 'VAGABOND.Difficulty'}} {{saves.reflex.dodge.difficulty}}">
<span class="defense-label">{{localize "VAGABOND.Dodge"}}</span>
{{#if saves.reflex.dodge.hindered}}
<span class="hinder-indicator" data-tooltip="{{localize saves.reflex.dodge.hinderSource}}">
<i class="fa-solid fa-arrow-down"></i>
</span>
{{/if}}
<span class="defense-difficulty">{{saves.reflex.dodge.difficulty}}</span>
<i class="fa-solid fa-dice-d20 roll-icon" aria-hidden="true"></i>
</div>
{{!-- Endure Save --}}
<div class="save-row interactive-row" role="button" tabindex="0"
data-action="rollSave" data-save="endure"
aria-label="{{localize saves.endure.label}} {{localize 'VAGABOND.Save'}}: {{localize 'VAGABOND.Difficulty'}} {{saves.endure.difficulty}}">
<span class="save-label">{{localize saves.endure.label}}</span>
<span class="save-stats">({{saves.endure.stats}})</span>
<span class="save-difficulty">{{saves.endure.difficulty}}</span>
<i class="fa-solid fa-dice-d20 roll-icon" aria-hidden="true"></i>
</div>
{{!-- Block sub-row (indented under Endure) --}}
<div class="defense-row block-row interactive-row {{#unless saves.endure.block.hasShield}}no-shield{{/unless}}"
role="button" tabindex="0"
data-action="rollBlock"
aria-label="{{localize 'VAGABOND.Block'}}: {{localize 'VAGABOND.Difficulty'}} {{saves.endure.block.difficulty}}">
<span class="defense-label">{{localize "VAGABOND.Block"}}</span>
<span class="defense-difficulty">{{saves.endure.block.difficulty}}</span>
<i class="fa-solid fa-dice-d20 roll-icon" aria-hidden="true"></i>
</div>
{{!-- Will Save --}}
<div class="save-row interactive-row" role="button" tabindex="0"
data-action="rollSave" data-save="will"
aria-label="{{localize saves.will.label}} {{localize 'VAGABOND.Save'}}: {{localize 'VAGABOND.Difficulty'}} {{saves.will.difficulty}}">
<span class="save-label">{{localize saves.will.label}}</span>
<span class="save-stats">({{saves.will.stats}})</span>
<span class="save-difficulty">{{saves.will.difficulty}}</span>
<i class="fa-solid fa-dice-d20 roll-icon" aria-hidden="true"></i>
</div>
{{/each}}
</div>
</div>

View File

@ -0,0 +1,92 @@
{{!-- Block Roll Dialog Template --}}
{{!-- Defense roll using Endure save with vs Ranged toggle --}}
<div class="vagabond roll-dialog-content block-roll-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 rollSpecific.hinderSources.length}}
<div class="auto-favor-hinder hinder">
<i class="fa-solid fa-arrow-down"></i>
<span>{{localize "VAGABOND.AutoHinder"}}: {{#each rollSpecific.hinderSources}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}</span>
</div>
{{/if}}
{{!-- Block Info Header --}}
<div class="defense-info-header">
<h3>{{localize "VAGABOND.Block"}}</h3>
</div>
{{!-- Save Info --}}
<div class="save-info">
<div class="save-stat">
<span class="label">{{localize "VAGABOND.Stats"}}:</span>
<span class="value">{{rollSpecific.saveStats}}</span>
</div>
<div class="save-difficulty">
<span class="label">{{localize "VAGABOND.Difficulty"}}:</span>
<span class="value difficulty">{{rollSpecific.difficulty}}</span>
</div>
</div>
{{!-- vs Ranged Attack Toggle --}}
<div class="ranged-toggle-section">
<label class="ranged-toggle checkbox-label">
<input type="checkbox" name="vsRanged" {{#if rollSpecific.vsRanged}}checked{{/if}}>
<span>{{localize "VAGABOND.VsRangedAttack"}}</span>
</label>
{{#if rollSpecific.vsRanged}}
<div class="ranged-warning hinder-warning">
<i class="fa-solid fa-arrow-down"></i>
<span>{{localize "VAGABOND.Hinder"}}</span>
</div>
{{/if}}
</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-shield"></i>
{{localize "VAGABOND.Block"}}
</button>
</div>
</div>

View File

@ -0,0 +1,86 @@
{{!-- Dodge Roll Dialog Template --}}
{{!-- Simplified defense roll using Reflex save --}}
<div class="vagabond roll-dialog-content dodge-roll-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}}
{{!-- Dodge Info Header --}}
<div class="defense-info-header">
<h3>{{localize "VAGABOND.Dodge"}}</h3>
</div>
{{!-- Save Info --}}
<div class="save-info">
<div class="save-stat">
<span class="label">{{localize "VAGABOND.Stats"}}:</span>
<span class="value">{{rollSpecific.saveStats}}</span>
</div>
<div class="save-difficulty">
<span class="label">{{localize "VAGABOND.Difficulty"}}:</span>
<span class="value difficulty">{{rollSpecific.difficulty}}</span>
</div>
</div>
{{!-- Heavy Armor Warning --}}
{{#if rollSpecific.hasHeavyArmor}}
<div class="armor-warning hinder-warning">
<i class="fa-solid fa-weight-hanging"></i>
<span>{{localize "VAGABOND.HinderedByHeavyArmor"}}</span>
</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">
<i class="fa-solid fa-person-running"></i>
{{localize "VAGABOND.Dodge"}}
</button>
</div>
</div>