Implement movement capability system with boolean toggles

Movement types (Climb, Cling, Fly, Phase, Swim) now use boolean
toggles instead of separate speed values, matching RAW where all
special movement uses base speed.

Changes:
- Update NPC and Character data models with movement schema
- Add movement section to NPC stats template (grid layout)
- Add movement section to character biography template
- Add localization strings with tooltip hints for each type
- Style movement grid to match senses section pattern
- Add rollable # Appearing label for NPC sheets
- Fix NPC sheet scrollbar visibility

🤖 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-15 00:16:44 -06:00
parent 1e67466d95
commit 694b11f423
7 changed files with 1181 additions and 64 deletions

View File

@ -181,6 +181,7 @@
"VAGABOND.Zone": "Zone", "VAGABOND.Zone": "Zone",
"VAGABOND.Morale": "Morale", "VAGABOND.Morale": "Morale",
"VAGABOND.Appearing": "# Appearing", "VAGABOND.Appearing": "# Appearing",
"VAGABOND.RollAppearing": "Roll # Appearing",
"VAGABOND.Immune": "Immune", "VAGABOND.Immune": "Immune",
"VAGABOND.Weak": "Weak", "VAGABOND.Weak": "Weak",
"VAGABOND.Actions": "Actions", "VAGABOND.Actions": "Actions",
@ -337,6 +338,18 @@
"VAGABOND.Echolocation": "Echolocation", "VAGABOND.Echolocation": "Echolocation",
"VAGABOND.Seismicsense": "Seismicsense", "VAGABOND.Seismicsense": "Seismicsense",
"VAGABOND.Telepathy": "Telepathy", "VAGABOND.Telepathy": "Telepathy",
"VAGABOND.Movement": "Movement",
"VAGABOND.Climb": "Climb",
"VAGABOND.Cling": "Cling",
"VAGABOND.Fly": "Fly",
"VAGABOND.Phase": "Phase",
"VAGABOND.Swim": "Swim",
"VAGABOND.MovementClimbHint": "Moves at full Speed while climbing. Beings without are Vulnerable while climbing.",
"VAGABOND.MovementClingHint": "As Climb, but can also Move on ceilings.",
"VAGABOND.MovementFlyHint": "Can Move through the air at full Speed.",
"VAGABOND.MovementPhaseHint": "Can Move in occupied space, but takes 5 damage if ending Turn in occupied space.",
"VAGABOND.MovementSwimHint": "Moves at full Speed while swimming. Beings without are Vulnerable while in liquid.",
"VAGABOND.BiographyPlaceholder": "Enter character background...", "VAGABOND.BiographyPlaceholder": "Enter character background...",
"VAGABOND.Notes": "Notes", "VAGABOND.Notes": "Notes",
"VAGABOND.NotesPlaceholder": "Enter notes...", "VAGABOND.NotesPlaceholder": "Enter notes...",

View File

@ -313,22 +313,24 @@ export default class CharacterData extends VagabondActorBase {
overburdened: new fields.BooleanField({ initial: false }), overburdened: new fields.BooleanField({ initial: false }),
}), }),
// Movement speeds (multiple types like NPCs) // Movement speed - base walking speed plus bonus
speed: new fields.SchemaField({ speed: new fields.SchemaField({
// Walking speed (base from DEX) // Walking speed (base from DEX, calculated in prepareDerivedData)
walk: new fields.NumberField({ integer: true, initial: 30, min: 0 }), walk: new fields.NumberField({ integer: true, initial: 30, min: 0 }),
// Flying speed (from spells, ancestry, features)
fly: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
// Swimming speed (Hunter Rover, some ancestries)
swim: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
// Climbing speed (Hunter Rover, some ancestries)
climb: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
// Burrowing speed (rare, some beasts)
burrow: new fields.NumberField({ integer: true, initial: 0, min: 0 }),
// Bonus to walking speed from effects // Bonus to walking speed from effects
bonus: new fields.NumberField({ integer: true, initial: 0 }), bonus: new fields.NumberField({ integer: true, initial: 0 }),
}), }),
// Movement capabilities - boolean toggles for special movement types
// All use base speed value when enabled per RAW
movement: new fields.SchemaField({
climb: new fields.BooleanField({ initial: false }), // Full Speed while climbing
cling: new fields.BooleanField({ initial: false }), // As Climb, but can Move on ceilings
fly: new fields.BooleanField({ initial: false }), // Move through the air at full Speed
phase: new fields.BooleanField({ initial: false }), // Move in occupied space (5 dmg if ends turn there)
swim: new fields.BooleanField({ initial: false }), // Full Speed while swimming
}),
// Saves - difficulties will be calculated // Saves - difficulties will be calculated
saves: new fields.SchemaField({ saves: new fields.SchemaField({
reflex: new fields.SchemaField({ reflex: new fields.SchemaField({

View File

@ -123,12 +123,20 @@ export default class NPCData extends VagabondActorBase {
telepathy: new fields.BooleanField({ initial: false }), telepathy: new fields.BooleanField({ initial: false }),
}), }),
// Movement speed // Movement speed - base value plus boolean movement capabilities
// All special movement types use the base speed value per RAW
speed: new fields.SchemaField({ speed: new fields.SchemaField({
value: new fields.NumberField({ integer: true, initial: 30 }), value: new fields.NumberField({ integer: true, initial: 30 }),
fly: new fields.NumberField({ integer: true, initial: 0 }), }),
swim: new fields.NumberField({ integer: true, initial: 0 }),
climb: new fields.NumberField({ integer: true, initial: 0 }), // Movement capabilities - boolean toggles for special movement types
// All use base speed value when enabled
movement: new fields.SchemaField({
climb: new fields.BooleanField({ initial: false }), // Full Speed while climbing
cling: new fields.BooleanField({ initial: false }), // As Climb, but can Move on ceilings
fly: new fields.BooleanField({ initial: false }), // Move through the air at full Speed
phase: new fields.BooleanField({ initial: false }), // Move in occupied space (5 dmg if ends turn there)
swim: new fields.BooleanField({ initial: false }), // Full Speed while swimming
}), }),
// Damage immunities // Damage immunities

View File

@ -32,6 +32,7 @@ export default class VagabondNPCSheet extends VagabondActorSheet {
...VagabondActorSheet.DEFAULT_OPTIONS.actions, ...VagabondActorSheet.DEFAULT_OPTIONS.actions,
rollMorale: VagabondNPCSheet.#onRollMorale, rollMorale: VagabondNPCSheet.#onRollMorale,
rollAction: VagabondNPCSheet.#onRollAction, rollAction: VagabondNPCSheet.#onRollAction,
rollAppearing: VagabondNPCSheet.#onRollAppearing,
addAction: VagabondNPCSheet.#onAddAction, addAction: VagabondNPCSheet.#onAddAction,
deleteAction: VagabondNPCSheet.#onDeleteAction, deleteAction: VagabondNPCSheet.#onDeleteAction,
addAbility: VagabondNPCSheet.#onAddAbility, addAbility: VagabondNPCSheet.#onAddAbility,
@ -107,14 +108,17 @@ export default class VagabondNPCSheet extends VagabondActorSheet {
context.sizeOptions = CONFIG.VAGABOND?.sizes || {}; context.sizeOptions = CONFIG.VAGABOND?.sizes || {};
context.beingTypeOptions = CONFIG.VAGABOND?.beingTypes || {}; context.beingTypeOptions = CONFIG.VAGABOND?.beingTypes || {};
// Speed // Speed (base value only)
context.speed = { context.speed = system.speed.value;
walk: system.speed.value,
fly: system.speed.fly, // Movement capabilities (boolean toggles)
swim: system.speed.swim, context.movement = system.movement;
climb: system.speed.climb, context.hasMovement =
hasSpecialMovement: system.speed.fly > 0 || system.speed.swim > 0 || system.speed.climb > 0, system.movement.climb ||
}; system.movement.cling ||
system.movement.fly ||
system.movement.phase ||
system.movement.swim;
// Senses // Senses
context.senses = system.senses; context.senses = system.senses;
@ -220,6 +224,29 @@ export default class VagabondNPCSheet extends VagabondActorSheet {
}); });
} }
/**
* Handle appearing roll (# encountered).
* @param {PointerEvent} event
* @param {HTMLElement} _target
*/
static async #onRollAppearing(event, _target) {
event.preventDefault();
const appearing = this.actor.system.appearing;
if (!appearing) {
ui.notifications.warn("No appearing dice formula set");
return;
}
// Roll the appearing formula
const roll = await new Roll(appearing).evaluate();
// Create chat message
await roll.toMessage({
speaker: ChatMessage.getSpeaker({ actor: this.actor }),
flavor: `<strong>${this.actor.name}</strong> - # Appearing`,
});
}
/** /**
* Handle adding a new action. * Handle adding a new action.
* @param {PointerEvent} event * @param {PointerEvent} event

File diff suppressed because it is too large Load Diff

View File

@ -89,6 +89,48 @@
</div> </div>
</div> </div>
{{!-- Movement Capabilities --}}
<div class="biography-section movement">
<h2 class="section-header">{{localize "VAGABOND.Movement"}}</h2>
<div class="movement-grid">
<div class="movement-field">
<label data-tooltip="{{localize 'VAGABOND.MovementClimbHint'}}">
<input type="checkbox" name="system.movement.climb"
{{#if system.movement.climb}}checked{{/if}} />
{{localize "VAGABOND.Climb"}}
</label>
</div>
<div class="movement-field">
<label data-tooltip="{{localize 'VAGABOND.MovementClingHint'}}">
<input type="checkbox" name="system.movement.cling"
{{#if system.movement.cling}}checked{{/if}} />
{{localize "VAGABOND.Cling"}}
</label>
</div>
<div class="movement-field">
<label data-tooltip="{{localize 'VAGABOND.MovementFlyHint'}}">
<input type="checkbox" name="system.movement.fly"
{{#if system.movement.fly}}checked{{/if}} />
{{localize "VAGABOND.Fly"}}
</label>
</div>
<div class="movement-field">
<label data-tooltip="{{localize 'VAGABOND.MovementPhaseHint'}}">
<input type="checkbox" name="system.movement.phase"
{{#if system.movement.phase}}checked{{/if}} />
{{localize "VAGABOND.Phase"}}
</label>
</div>
<div class="movement-field">
<label data-tooltip="{{localize 'VAGABOND.MovementSwimHint'}}">
<input type="checkbox" name="system.movement.swim"
{{#if system.movement.swim}}checked{{/if}} />
{{localize "VAGABOND.Swim"}}
</label>
</div>
</div>
</div>
{{!-- Biography Text --}} {{!-- Biography Text --}}
<div class="biography-section biography-text"> <div class="biography-section biography-text">
<h2 class="section-header">{{localize "VAGABOND.Biography"}}</h2> <h2 class="section-header">{{localize "VAGABOND.Biography"}}</h2>

View File

@ -17,35 +17,9 @@
{{!-- Speed --}} {{!-- Speed --}}
<div class="stat-group speed"> <div class="stat-group speed">
<label>{{localize "VAGABOND.Speed"}}</label> <label>{{localize "VAGABOND.Speed"}}</label>
<div class="speed-values"> <div class="speed-value">
<div class="speed-item walk"> <input type="number" name="system.speed.value" value="{{speed}}" min="0" />
<i class="fa-solid fa-shoe-prints"></i> <span class="unit">ft</span>
<input type="number" name="system.speed.value" value="{{speed.walk}}" min="0" />
<span class="unit">ft</span>
</div>
{{#if speed.hasSpecialMovement}}
{{#if speed.fly}}
<div class="speed-item fly">
<i class="fa-solid fa-feather"></i>
<input type="number" name="system.speed.fly" value="{{speed.fly}}" min="0" />
<span class="unit">ft</span>
</div>
{{/if}}
{{#if speed.swim}}
<div class="speed-item swim">
<i class="fa-solid fa-water"></i>
<input type="number" name="system.speed.swim" value="{{speed.swim}}" min="0" />
<span class="unit">ft</span>
</div>
{{/if}}
{{#if speed.climb}}
<div class="speed-item climb">
<i class="fa-solid fa-mountain"></i>
<input type="number" name="system.speed.climb" value="{{speed.climb}}" min="0" />
<span class="unit">ft</span>
</div>
{{/if}}
{{/if}}
</div> </div>
</div> </div>
@ -75,11 +49,51 @@
{{!-- Appearing --}} {{!-- Appearing --}}
<div class="stat-group appearing"> <div class="stat-group appearing">
<label>{{localize "VAGABOND.Appearing"}}</label> <label class="rollable" data-action="rollAppearing" data-tooltip="{{localize 'VAGABOND.RollAppearing'}}">
<i class="fa-solid fa-dice"></i>
{{localize "VAGABOND.Appearing"}}
</label>
<input type="text" name="system.appearing" value="{{appearing}}" placeholder="1d6" /> <input type="text" name="system.appearing" value="{{appearing}}" placeholder="1d6" />
</div> </div>
</div> </div>
{{!-- Movement Types --}}
<div class="biography-section movement">
<h2 class="section-header">{{localize "VAGABOND.Movement"}}</h2>
<div class="movement-grid">
<div class="movement-field">
<label data-tooltip="{{localize 'VAGABOND.MovementClimbHint'}}">
<input type="checkbox" name="system.movement.climb" {{#if movement.climb}}checked{{/if}} />
{{localize "VAGABOND.Climb"}}
</label>
</div>
<div class="movement-field">
<label data-tooltip="{{localize 'VAGABOND.MovementClingHint'}}">
<input type="checkbox" name="system.movement.cling" {{#if movement.cling}}checked{{/if}} />
{{localize "VAGABOND.Cling"}}
</label>
</div>
<div class="movement-field">
<label data-tooltip="{{localize 'VAGABOND.MovementFlyHint'}}">
<input type="checkbox" name="system.movement.fly" {{#if movement.fly}}checked{{/if}} />
{{localize "VAGABOND.Fly"}}
</label>
</div>
<div class="movement-field">
<label data-tooltip="{{localize 'VAGABOND.MovementPhaseHint'}}">
<input type="checkbox" name="system.movement.phase" {{#if movement.phase}}checked{{/if}} />
{{localize "VAGABOND.Phase"}}
</label>
</div>
<div class="movement-field">
<label data-tooltip="{{localize 'VAGABOND.MovementSwimHint'}}">
<input type="checkbox" name="system.movement.swim" {{#if movement.swim}}checked{{/if}} />
{{localize "VAGABOND.Swim"}}
</label>
</div>
</div>
</div>
{{!-- Senses --}} {{!-- Senses --}}
{{#if hasSenses}} {{#if hasSenses}}
<div class="senses-row"> <div class="senses-row">