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:
parent
ed694372d5
commit
77c9359601
@ -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",
|
||||
|
||||
@ -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";
|
||||
|
||||
208
module/applications/block-roll-dialog.mjs
Normal file
208
module/applications/block-roll-dialog.mjs
Normal 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);
|
||||
}
|
||||
}
|
||||
169
module/applications/dodge-roll-dialog.mjs
Normal file
169
module/applications/dodge-roll-dialog.mjs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
92
templates/dialog/block-roll.hbs
Normal file
92
templates/dialog/block-roll.hbs
Normal 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>
|
||||
86
templates/dialog/dodge-roll.hbs
Normal file
86
templates/dialog/dodge-roll.hbs
Normal 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>
|
||||
Loading…
Reference in New Issue
Block a user