Add trained toggle to attack skills (Melee, Brawl, Ranged, Finesse)
Attack skills now have trained/untrained status like regular skills: - Untrained: difficulty = 20 - stat - Trained: difficulty = 20 - (stat × 2) Includes UI toggle button, data model fields, and matching styling. 🤖 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
fe91764f3b
commit
0eb258d2c1
@ -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.<attack>.trained` | Boolean | true/false | Is attack skill trained? |
|
||||
| `system.attacks.<attack>.difficulty` | Number | 1-20 | Roll target (derived) |
|
||||
| `system.attacks.<attack>.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
|
||||
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -76,9 +76,16 @@
|
||||
<h2 class="section-header">{{localize "VAGABOND.Attacks"}}</h2>
|
||||
<div class="attack-skills-grid">
|
||||
{{#each attackSkills}}
|
||||
<div class="attack-skill-row interactive-row" role="button" tabindex="0"
|
||||
data-action="rollAttack" data-attack-skill="{{this.id}}"
|
||||
aria-label="{{localize this.label}} {{localize 'VAGABOND.Attack'}}: {{localize 'VAGABOND.Difficulty'}} {{this.difficulty}}">
|
||||
<div class="attack-skill-row {{#if this.trained}}trained{{/if}}" data-attack-skill="{{this.id}}">
|
||||
<button type="button" class="attack-trained-toggle"
|
||||
data-action="toggleAttackTrained" data-attack="{{this.id}}"
|
||||
data-tooltip="{{localize 'VAGABOND.ToggleTrained'}}">
|
||||
{{#if this.trained}}
|
||||
<i class="fa-solid fa-check"></i>
|
||||
{{else}}
|
||||
<i class="fa-regular fa-circle"></i>
|
||||
{{/if}}
|
||||
</button>
|
||||
<span class="attack-name">{{localize this.label}}</span>
|
||||
<span class="attack-stat">({{this.statAbbr}})</span>
|
||||
<span class="attack-difficulty">{{this.difficulty}}</span>
|
||||
@ -87,6 +94,11 @@
|
||||
<i class="fa-solid fa-star" aria-hidden="true"></i>{{this.critThreshold}}
|
||||
</span>
|
||||
{{/if}}
|
||||
<button type="button" class="attack-roll-btn"
|
||||
data-action="rollAttack" data-attack-skill="{{this.id}}"
|
||||
data-tooltip="{{localize 'VAGABOND.RollAttack'}}">
|
||||
<i class="fa-solid fa-dice-d20"></i>
|
||||
</button>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user