diff --git a/docs/active-effect-reference.md b/docs/active-effect-reference.md index 9cf7d9a..6e37a8f 100644 --- a/docs/active-effect-reference.md +++ b/docs/active-effect-reference.md @@ -61,14 +61,23 @@ Each skill has `trained`, `difficulty`, and `critThreshold` properties. - `system.skills.melee.critThreshold` - Modify melee crit threshold - `system.skills.sneak.critThreshold` - Modify sneak crit threshold -### Attack Crit Thresholds +### Attack Skills -| Key | Type | Range | Description | -| -------------------------------------- | ------ | ----- | ----------------------------- | -| `system.attacks.melee.critThreshold` | Number | 1-20 | Melee attack crit threshold | -| `system.attacks.brawl.critThreshold` | Number | 1-20 | Brawl attack crit threshold | -| `system.attacks.ranged.critThreshold` | Number | 1-20 | Ranged attack crit threshold | -| `system.attacks.finesse.critThreshold` | Number | 1-20 | Finesse attack crit threshold | +Each attack skill has `trained`, `difficulty`, and `critThreshold` properties. + +**Attack Skills:** `melee`, `brawl`, `ranged`, `finesse` + +| Key Pattern | Type | Range | Description | +| --------------------------------------- | ------- | ---------- | --------------------------- | +| `system.attacks..trained` | Boolean | true/false | Is attack skill trained? | +| `system.attacks..difficulty` | Number | 1-20 | Roll target (derived) | +| `system.attacks..critThreshold` | Number | 1-20 | Crit on this roll or higher | + +**Examples:** + +- `system.attacks.melee.trained` - Train Melee attack skill +- `system.attacks.ranged.trained` - Train Ranged attack skill +- `system.attacks.melee.critThreshold` - Modify melee crit threshold ### Saves diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 0b80517..f4e9347 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -184,18 +184,26 @@ export default class CharacterData extends VagabondActorBase { }), }), - // Attack skills with crit thresholds + // Attack skills with trained status and crit thresholds attacks: new fields.SchemaField({ melee: new fields.SchemaField({ + trained: new fields.BooleanField({ initial: false }), + difficulty: new fields.NumberField({ integer: true, initial: 20, min: 1, max: 20 }), critThreshold: new fields.NumberField({ integer: true, initial: 20, min: 1, max: 20 }), }), brawl: new fields.SchemaField({ + trained: new fields.BooleanField({ initial: false }), + difficulty: new fields.NumberField({ integer: true, initial: 20, min: 1, max: 20 }), critThreshold: new fields.NumberField({ integer: true, initial: 20, min: 1, max: 20 }), }), ranged: new fields.SchemaField({ + trained: new fields.BooleanField({ initial: false }), + difficulty: new fields.NumberField({ integer: true, initial: 20, min: 1, max: 20 }), critThreshold: new fields.NumberField({ integer: true, initial: 20, min: 1, max: 20 }), }), finesse: new fields.SchemaField({ + trained: new fields.BooleanField({ initial: false }), + difficulty: new fields.NumberField({ integer: true, initial: 20, min: 1, max: 20 }), critThreshold: new fields.NumberField({ integer: true, initial: 20, min: 1, max: 20 }), }), }), @@ -652,8 +660,8 @@ export default class CharacterData extends VagabondActorBase { // Calculate Skill Difficulties (also clamps skill crit thresholds) this._calculateSkillDifficulties(); - // Clamp attack crit thresholds after Active Effects - this._clampAttackCritThresholds(); + // Calculate Attack Difficulties (also clamps attack crit thresholds) + this._calculateAttackDifficulties(); } /** @@ -683,13 +691,28 @@ export default class CharacterData extends VagabondActorBase { } /** - * Clamp attack crit thresholds after Active Effects are applied. - * Schema constraints don't apply to effect-modified data. + * Calculate difficulty values for all attack skills. + * Untrained: 20 - stat + * Trained: 20 - (stat × 2) + * Also clamps crit thresholds after Active Effects. * * @private */ - _clampAttackCritThresholds() { - for (const attackData of Object.values(this.attacks)) { + _calculateAttackDifficulties() { + const attackStats = CONFIG.VAGABOND?.attackTypes || {}; + + for (const [attackId, attackData] of Object.entries(this.attacks)) { + const attackConfig = attackStats[attackId]; + if (!attackConfig) continue; + + const statKey = attackConfig.stat; + const statValue = this.stats[statKey]?.value || 0; + const trained = attackData.trained; + + // Calculate difficulty: 20 - stat (untrained) or 20 - stat×2 (trained) + attackData.difficulty = trained ? 20 - statValue * 2 : 20 - statValue; + + // Clamp crit threshold after Active Effects (schema min doesn't apply to effect-modified data) attackData.critThreshold = Math.max(1, Math.min(20, attackData.critThreshold)); } } diff --git a/module/sheets/base-actor-sheet.mjs b/module/sheets/base-actor-sheet.mjs index 0c0bfcc..4703527 100644 --- a/module/sheets/base-actor-sheet.mjs +++ b/module/sheets/base-actor-sheet.mjs @@ -66,6 +66,7 @@ export default class VagabondActorSheet extends HandlebarsApplicationMixin(Actor changeTab: VagabondActorSheet.#onChangeTab, modifyResource: VagabondActorSheet.#onModifyResource, toggleTrained: VagabondActorSheet.#onToggleTrained, + toggleAttackTrained: VagabondActorSheet.#onToggleAttackTrained, removeStatus: VagabondActorSheet.#onRemoveStatus, }, // Drag-drop configuration - use Foundry's built-in system @@ -827,6 +828,21 @@ export default class VagabondActorSheet extends HandlebarsApplicationMixin(Actor await this.actor.update({ [`system.skills.${skillId}.trained`]: !currentValue }); } + /** + * Handle attack skill trained toggle. + * @param {PointerEvent} event + * @param {HTMLElement} target + */ + static async #onToggleAttackTrained(event, target) { + event.preventDefault(); + event.stopPropagation(); // Prevent triggering the row's rollAttack action + const attackId = target.dataset.attack; + if (!attackId) return; + + const currentValue = this.actor.system.attacks[attackId]?.trained ?? false; + await this.actor.update({ [`system.attacks.${attackId}.trained`]: !currentValue }); + } + /** * Handle status removal action. * @param {PointerEvent} event diff --git a/module/sheets/character-sheet.mjs b/module/sheets/character-sheet.mjs index 58bdab8..f906217 100644 --- a/module/sheets/character-sheet.mjs +++ b/module/sheets/character-sheet.mjs @@ -219,29 +219,23 @@ export default class VagabondCharacterSheet extends VagabondActorSheet { */ _prepareAttackSkills() { const system = this.actor.system; - - const attackConfig = { - melee: { label: "VAGABOND.AttackMelee", stat: "might" }, - brawl: { label: "VAGABOND.AttackBrawl", stat: "might" }, - ranged: { label: "VAGABOND.AttackRanged", stat: "awareness" }, - finesse: { label: "VAGABOND.AttackFinesse", stat: "dexterity" }, - }; + const attackConfig = CONFIG.VAGABOND?.attackTypes || {}; const attacks = {}; for (const [key, config] of Object.entries(attackConfig)) { - const statValue = system.stats[config.stat]?.value || 0; - // Attack difficulty is 20 - stat (always trained) - const difficulty = 20 - statValue * 2; + const attackData = system.attacks[key]; + if (!attackData) continue; attacks[key] = { id: key, label: config.label, stat: config.stat, statAbbr: this._getStatAbbr(config.stat), - difficulty, - critThreshold: system.attacks[key]?.critThreshold || 20, - hasCritBonus: (system.attacks[key]?.critThreshold || 20) < 20, + trained: attackData.trained, + difficulty: attackData.difficulty, + critThreshold: attackData.critThreshold || 20, + hasCritBonus: (attackData.critThreshold || 20) < 20, }; } diff --git a/styles/scss/sheets/_actor-sheet.scss b/styles/scss/sheets/_actor-sheet.scss index a9a1ab5..877f162 100644 --- a/styles/scss/sheets/_actor-sheet.scss +++ b/styles/scss/sheets/_actor-sheet.scss @@ -733,38 +733,91 @@ display: flex; align-items: center; gap: $spacing-2; - padding: $spacing-2 $spacing-3; + padding: $spacing-1 $spacing-2; background-color: var(--color-bg-input); border: 1px solid var(--color-border)-light; border-radius: $radius-md; - cursor: pointer; transition: all $transition-fast; &:hover { background-color: var(--color-bg-secondary); - border-color: var(--color-accent-primary); + border-color: var(--color-border); + } + + &.trained { + background-color: rgba(45, 90, 39, 0.1); + border-color: rgba(45, 90, 39, 0.3); + + .attack-trained-toggle { + color: var(--color-success); + border-color: var(--color-success); + } + } + + .attack-trained-toggle { + @include flex-center; + width: 20px; + height: 20px; + padding: 0; + background: transparent; + border: 1px solid var(--color-border); + border-radius: $radius-sm; + color: var(--color-text-muted); + cursor: pointer; + transition: all $transition-fast; + + &:hover { + border-color: var(--color-accent-primary); + color: var(--color-accent-primary); + } + + i { + font-size: 10px; + } } .attack-name { - font-weight: $font-weight-semibold; + font-weight: $font-weight-medium; color: var(--color-text-primary); + flex: 1; } .attack-stat { font-size: $font-size-xs; color: var(--color-text-secondary); - margin-right: auto; } .attack-difficulty { font-family: $font-family-header; font-weight: $font-weight-bold; color: var(--color-text-primary); + min-width: 24px; + text-align: center; } .attack-crit { font-size: $font-size-xs; color: var(--color-warning); + + i { + margin-right: 2px; + } + } + + .attack-roll-btn { + @include flex-center; + width: 24px; + height: 24px; + padding: 0; + background: transparent; + border: none; + color: var(--color-text-muted); + cursor: pointer; + transition: color $transition-fast; + + &:hover { + color: var(--color-accent-primary); + } } } diff --git a/templates/actor/character-main.hbs b/templates/actor/character-main.hbs index bfa2c15..467ff27 100644 --- a/templates/actor/character-main.hbs +++ b/templates/actor/character-main.hbs @@ -76,9 +76,16 @@

{{localize "VAGABOND.Attacks"}}

{{#each attackSkills}} -
+
+ {{localize this.label}} ({{this.statAbbr}}) {{this.difficulty}} @@ -87,6 +94,11 @@ {{this.critThreshold}} {{/if}} +
{{/each}}