From 694b11f42352681d5f2287cf709af6e4e6a65f24 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Mon, 15 Dec 2025 00:16:44 -0600 Subject: [PATCH] Implement movement capability system with boolean toggles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- lang/en.json | 13 + module/data/actor/character.mjs | 22 +- module/data/actor/npc.mjs | 16 +- module/sheets/npc-sheet.mjs | 43 +- styles/scss/sheets/_actor-sheet.scss | 1035 ++++++++++++++++++++++- templates/actor/character-biography.hbs | 42 + templates/actor/npc-stats.hbs | 74 +- 7 files changed, 1181 insertions(+), 64 deletions(-) diff --git a/lang/en.json b/lang/en.json index bebfd9b..e013d73 100644 --- a/lang/en.json +++ b/lang/en.json @@ -181,6 +181,7 @@ "VAGABOND.Zone": "Zone", "VAGABOND.Morale": "Morale", "VAGABOND.Appearing": "# Appearing", + "VAGABOND.RollAppearing": "Roll # Appearing", "VAGABOND.Immune": "Immune", "VAGABOND.Weak": "Weak", "VAGABOND.Actions": "Actions", @@ -337,6 +338,18 @@ "VAGABOND.Echolocation": "Echolocation", "VAGABOND.Seismicsense": "Seismicsense", "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.Notes": "Notes", "VAGABOND.NotesPlaceholder": "Enter notes...", diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 3f114d9..0b80517 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -313,22 +313,24 @@ export default class CharacterData extends VagabondActorBase { overburdened: new fields.BooleanField({ initial: false }), }), - // Movement speeds (multiple types like NPCs) + // Movement speed - base walking speed plus bonus 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 }), - // 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: 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: new fields.SchemaField({ reflex: new fields.SchemaField({ diff --git a/module/data/actor/npc.mjs b/module/data/actor/npc.mjs index 47587fc..85cb5b3 100644 --- a/module/data/actor/npc.mjs +++ b/module/data/actor/npc.mjs @@ -123,12 +123,20 @@ export default class NPCData extends VagabondActorBase { 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({ 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 diff --git a/module/sheets/npc-sheet.mjs b/module/sheets/npc-sheet.mjs index 107e776..1168877 100644 --- a/module/sheets/npc-sheet.mjs +++ b/module/sheets/npc-sheet.mjs @@ -32,6 +32,7 @@ export default class VagabondNPCSheet extends VagabondActorSheet { ...VagabondActorSheet.DEFAULT_OPTIONS.actions, rollMorale: VagabondNPCSheet.#onRollMorale, rollAction: VagabondNPCSheet.#onRollAction, + rollAppearing: VagabondNPCSheet.#onRollAppearing, addAction: VagabondNPCSheet.#onAddAction, deleteAction: VagabondNPCSheet.#onDeleteAction, addAbility: VagabondNPCSheet.#onAddAbility, @@ -107,14 +108,17 @@ export default class VagabondNPCSheet extends VagabondActorSheet { context.sizeOptions = CONFIG.VAGABOND?.sizes || {}; context.beingTypeOptions = CONFIG.VAGABOND?.beingTypes || {}; - // Speed - context.speed = { - walk: system.speed.value, - fly: system.speed.fly, - swim: system.speed.swim, - climb: system.speed.climb, - hasSpecialMovement: system.speed.fly > 0 || system.speed.swim > 0 || system.speed.climb > 0, - }; + // Speed (base value only) + context.speed = system.speed.value; + + // Movement capabilities (boolean toggles) + context.movement = system.movement; + context.hasMovement = + system.movement.climb || + system.movement.cling || + system.movement.fly || + system.movement.phase || + system.movement.swim; // 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: `${this.actor.name} - # Appearing`, + }); + } + /** * Handle adding a new action. * @param {PointerEvent} event diff --git a/styles/scss/sheets/_actor-sheet.scss b/styles/scss/sheets/_actor-sheet.scss index b9fc54a..b0d49df 100644 --- a/styles/scss/sheets/_actor-sheet.scss +++ b/styles/scss/sheets/_actor-sheet.scss @@ -1906,6 +1906,54 @@ } } + // Movement Section (same pattern as Senses) + .movement-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: $spacing-3; + + .movement-field { + label { + display: flex; + align-items: center; + gap: $spacing-2; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $color-text-primary; + cursor: pointer; + padding: $spacing-2; + background-color: $color-parchment-light; + border: 1px solid $color-border-light; + border-radius: $radius-sm; + transition: all $transition-fast; + + &:hover { + background-color: $color-parchment; + border-color: $color-border; + } + + input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; + accent-color: $color-accent-primary; + + &:checked + span, + &:checked ~ span { + color: $color-accent-primary; + font-weight: $font-weight-semibold; + } + } + } + + // Style when checkbox is checked + &:has(input:checked) label { + background-color: rgba($color-success, 0.1); + border-color: rgba($color-success, 0.3); + } + } + } + // Biography & Notes Text Areas .biography-text, .notes { @@ -1950,7 +1998,8 @@ grid-template-columns: 1fr; } - .senses-grid { + .senses-grid, + .movement-grid { grid-template-columns: repeat(2, 1fr); } } @@ -1960,24 +2009,986 @@ // NPC/MONSTER SHEET SPECIFIC // ========================================== .vagabond.sheet.actor.npc { - min-width: 400px; + min-width: 450px; + min-height: 500px; - .stat-block-display { - @include panel; - padding: $spacing-4; + // ---------------------------------------- + // NPC Header + // ---------------------------------------- + .npc-header { + display: grid; + grid-template-columns: auto 1fr auto; + gap: $spacing-3; + padding: $spacing-3; + background-color: $color-parchment-dark; + border-bottom: 2px solid $color-border; - .stat-line { - @include flex-between; - padding: $spacing-1 0; - border-bottom: 1px solid $color-border-light; + .header-portrait { + .profile-img { + width: 80px; + height: 80px; + object-fit: cover; + border: 2px solid $color-border; + border-radius: $radius-md; + cursor: pointer; + transition: border-color $transition-fast; - &:last-child { - border-bottom: none; + &:hover { + border-color: $color-accent-primary; + } + } + } + + .header-info { + display: flex; + flex-direction: column; + gap: $spacing-2; + + .actor-name { + margin: 0; + + input { + width: 100%; + font-family: $font-family-header; + font-size: $font-size-xl; + font-weight: $font-weight-bold; + color: $color-text-primary; + background: transparent; + border: none; + border-bottom: 1px solid transparent; + padding: $spacing-1 0; + + &:hover, + &:focus { + border-bottom-color: $color-border; + outline: none; + } + } } - .label { + .header-stats { + display: flex; + flex-wrap: wrap; + gap: $spacing-2; + + .stat-box { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + padding: $spacing-2; + background-color: $color-parchment-light; + border: 1px solid $color-border; + border-radius: $radius-sm; + min-width: 50px; + + label { + font-size: $font-size-xs; + font-weight: $font-weight-semibold; + text-transform: uppercase; + color: $color-text-secondary; + } + + input { + width: 40px; + font-family: $font-family-header; + font-size: $font-size-lg; + font-weight: $font-weight-bold; + text-align: center; + color: $color-text-primary; + background: transparent; + border: none; + + &:focus { + outline: none; + background-color: white; + border-radius: $radius-sm; + } + } + + &.morale { + flex-direction: row; + gap: $spacing-2; + padding: $spacing-1 $spacing-2; + + label { + margin-right: auto; + } + + .morale-roll { + @include flex-center; + width: 24px; + height: 24px; + padding: 0; + background-color: $color-parchment; + border: 1px solid $color-border; + border-radius: $radius-sm; + color: $color-text-primary; + cursor: pointer; + transition: all $transition-fast; + + &:hover:not(:disabled) { + background-color: $color-accent-primary; + border-color: $color-accent-primary; + color: white; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + i { + font-size: $font-size-xs; + } + } + } + } + } + } + + .header-hp { + display: flex; + flex-direction: column; + gap: $spacing-2; + min-width: 120px; + + .hp-bar { + display: flex; + flex-direction: column; + gap: $spacing-1; + + > label { + font-size: $font-size-xs; + font-weight: $font-weight-semibold; + text-transform: uppercase; + color: $color-text-secondary; + } + + .bar-container { + position: relative; + height: 32px; + background-color: $color-parchment-light; + border: 2px solid $color-border; + border-radius: $radius-md; + overflow: hidden; + + .bar-fill { + position: absolute; + top: 0; + left: 0; + height: 100%; + background: linear-gradient(to bottom, $color-success, darken($color-success, 10%)); + transition: width 0.3s ease; + } + + .bar-values { + position: relative; + display: flex; + align-items: center; + justify-content: center; + height: 100%; + z-index: 1; + + input { + width: 36px; + font-family: $font-family-header; + font-size: $font-size-base; + font-weight: $font-weight-bold; + text-align: center; + color: $color-text-primary; + background: transparent; + border: none; + + &:focus { + outline: none; + background-color: rgba(white, 0.8); + border-radius: $radius-sm; + } + } + + .separator { + color: $color-text-secondary; + margin: 0 2px; + } + } + } + + // HP state colors + &.half .bar-fill { + background: linear-gradient(to bottom, $color-warning, darken($color-warning, 10%)); + } + + &.dead .bar-fill { + background: linear-gradient(to bottom, $color-danger, darken($color-danger, 10%)); + } + } + + .morale-broken { + display: flex; + align-items: center; + gap: $spacing-1; + padding: $spacing-1 $spacing-2; + background-color: rgba($color-danger, 0.1); + border: 1px solid rgba($color-danger, 0.3); + border-radius: $radius-sm; + font-size: $font-size-xs; font-weight: $font-weight-semibold; + color: $color-danger; + + i { + font-size: $font-size-sm; + } } } } + + // ---------------------------------------- + // NPC Body / Content + // ---------------------------------------- + .sheet-body { + display: flex; + flex-direction: column; + gap: $spacing-3; + padding: $spacing-3; + overflow-y: auto; + flex: 1; + min-height: 0; + } + + // ---------------------------------------- + // NPC Stats Section + // ---------------------------------------- + .npc-stats { + display: flex; + flex-direction: column; + gap: $spacing-3; + padding: $spacing-3; + background-color: $color-parchment-light; + border: 1px solid $color-border; + border-radius: $radius-md; + + .stats-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: $spacing-3; + } + + .stat-group { + display: flex; + flex-direction: column; + gap: $spacing-1; + + > label { + font-size: $font-size-xs; + font-weight: $font-weight-semibold; + text-transform: uppercase; + letter-spacing: 0.05em; + color: $color-text-secondary; + + // Rollable labels (e.g., Appearing) + &.rollable { + display: inline-flex; + align-items: center; + gap: $spacing-1; + cursor: pointer; + transition: color $transition-fast; + + &:hover { + color: $color-accent-primary; + + i { + color: $color-accent-primary; + } + } + + i { + font-size: $font-size-xs; + color: $color-text-muted; + transition: color $transition-fast; + } + } + } + + select, + input[type="text"], + input[type="number"] { + @include input-base; + padding: $spacing-1 $spacing-2; + font-size: $font-size-sm; + } + + .zone-hint { + margin: 0; + font-size: $font-size-xs; + font-style: italic; + color: $color-text-muted; + line-height: $line-height-tight; + } + } + + .stat-group.speed { + .speed-value { + display: flex; + align-items: center; + gap: $spacing-1; + + input { + width: 50px; + padding: $spacing-1 $spacing-2; + font-size: $font-size-base; + font-weight: $font-weight-semibold; + text-align: center; + background-color: $color-parchment; + border: 1px solid $color-border-light; + border-radius: $radius-sm; + + &:focus { + outline: none; + background-color: white; + border-color: $color-accent-primary; + } + } + + .unit { + font-size: $font-size-sm; + color: $color-text-muted; + } + } + } + + // Movement Section (uses same styling as biography movement-grid) + .biography-section.movement { + padding: $spacing-2 0; + border-top: 1px solid $color-border-light; + + .section-header { + font-family: $font-family-header; + font-size: $font-size-sm; + font-weight: $font-weight-bold; + color: $color-text-secondary; + margin: 0 0 $spacing-2 0; + padding: 0; + border: none; + text-transform: uppercase; + letter-spacing: 0.05em; + } + + .movement-grid { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: $spacing-2; + } + + .movement-field label { + display: flex; + align-items: center; + gap: $spacing-2; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $color-text-primary; + cursor: pointer; + padding: $spacing-2; + background-color: $color-parchment-light; + border: 1px solid $color-border-light; + border-radius: $radius-sm; + transition: all $transition-fast; + + &:hover { + background-color: $color-parchment; + border-color: $color-border; + } + + input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; + accent-color: $color-accent-primary; + } + } + + .movement-field:has(input:checked) label { + background-color: rgba($color-success, 0.1); + border-color: rgba($color-success, 0.3); + } + } + + .stat-group.type { + .type-field { + display: flex; + flex-direction: column; + gap: 2px; + margin-bottom: $spacing-2; + + &:last-child { + margin-bottom: 0; + } + + label { + font-size: $font-size-xs; + font-weight: $font-weight-semibold; + text-transform: uppercase; + color: $color-text-secondary; + } + } + } + + // Senses Row + .senses-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: $spacing-2; + padding-top: $spacing-2; + border-top: 1px solid $color-border-light; + + > label { + font-size: $font-size-sm; + font-weight: $font-weight-semibold; + color: $color-text-secondary; + } + + .sense-tag { + padding: $spacing-1 $spacing-2; + font-size: $font-size-xs; + font-weight: $font-weight-medium; + color: $color-info; + background-color: rgba($color-info, 0.1); + border: 1px solid rgba($color-info, 0.3); + border-radius: $radius-sm; + } + } + + // Damage Modifiers + .damage-modifiers { + display: flex; + flex-direction: column; + gap: $spacing-2; + padding-top: $spacing-2; + border-top: 1px solid $color-border-light; + + .modifier-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: $spacing-2; + + > label { + font-size: $font-size-sm; + font-weight: $font-weight-semibold; + color: $color-text-secondary; + min-width: 80px; + } + + .modifier-tags { + display: flex; + flex-wrap: wrap; + gap: $spacing-1; + } + + .modifier-tag { + padding: $spacing-1 $spacing-2; + font-size: $font-size-xs; + font-weight: $font-weight-medium; + border-radius: $radius-sm; + text-transform: capitalize; + + &.immune { + color: darken($color-info, 20%); + background-color: rgba($color-info, 0.15); + border: 1px solid rgba($color-info, 0.3); + } + + &.resist { + color: darken($color-success, 20%); + background-color: rgba($color-success, 0.15); + border: 1px solid rgba($color-success, 0.3); + } + + &.weak { + color: darken($color-danger, 10%); + background-color: rgba($color-danger, 0.15); + border: 1px solid rgba($color-danger, 0.3); + } + } + } + } + } + + // ---------------------------------------- + // NPC Actions Section + // ---------------------------------------- + .npc-actions { + .section-header-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: $spacing-2; + + .section-header { + margin: 0; + font-family: $font-family-header; + font-size: $font-size-base; + font-weight: $font-weight-bold; + color: $color-text-primary; + text-transform: uppercase; + letter-spacing: 0.05em; + } + + .action-add { + @include flex-center; + width: 28px; + height: 28px; + padding: 0; + background-color: $color-parchment; + border: 1px solid $color-border; + border-radius: $radius-md; + color: $color-text-primary; + cursor: pointer; + transition: all $transition-fast; + + &:hover { + background-color: $color-success; + border-color: $color-success; + color: white; + } + + i { + font-size: $font-size-sm; + } + } + } + + .action-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: $spacing-2; + } + + .action-item { + padding: $spacing-3; + background-color: $color-parchment-light; + border: 1px solid $color-border; + border-radius: $radius-md; + + &.empty { + display: flex; + flex-direction: column; + align-items: center; + gap: $spacing-2; + padding: $spacing-4; + + p { + margin: 0; + font-style: italic; + color: $color-text-muted; + } + + button { + display: flex; + align-items: center; + gap: $spacing-1; + padding: $spacing-2 $spacing-3; + background-color: $color-parchment; + border: 1px solid $color-border; + border-radius: $radius-md; + font-size: $font-size-sm; + color: $color-text-primary; + cursor: pointer; + transition: all $transition-fast; + + &:hover { + background-color: $color-success; + border-color: $color-success; + color: white; + } + } + } + + .action-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: $spacing-2; + margin-bottom: $spacing-2; + + .action-name { + flex: 1; + font-family: $font-family-header; + font-size: $font-size-base; + font-weight: $font-weight-semibold; + color: $color-text-primary; + background: transparent; + border: none; + border-bottom: 1px solid transparent; + padding: $spacing-1 0; + + &:hover, + &:focus { + border-bottom-color: $color-border; + outline: none; + } + } + + .action-controls { + display: flex; + gap: $spacing-1; + + button { + @include flex-center; + width: 28px; + height: 28px; + padding: 0; + background-color: $color-parchment; + border: 1px solid $color-border; + border-radius: $radius-sm; + color: $color-text-primary; + cursor: pointer; + transition: all $transition-fast; + + &.action-roll:hover { + background-color: $color-accent-primary; + border-color: $color-accent-primary; + color: white; + } + + &.action-delete:hover { + background-color: rgba($color-danger, 0.1); + border-color: $color-danger; + color: $color-danger; + } + + i { + font-size: $font-size-sm; + } + } + } + } + + .action-details { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); + gap: $spacing-2; + margin-bottom: $spacing-2; + + .action-field { + display: flex; + flex-direction: column; + gap: 2px; + + label { + font-size: $font-size-xs; + font-weight: $font-weight-semibold; + text-transform: uppercase; + color: $color-text-secondary; + } + + input, + select { + @include input-base; + padding: $spacing-1 $spacing-2; + font-size: $font-size-sm; + } + } + } + + .action-description { + textarea { + width: 100%; + min-height: 60px; + padding: $spacing-2; + font-size: $font-size-sm; + line-height: $line-height-relaxed; + color: $color-text-primary; + background-color: $color-parchment; + border: 1px solid $color-border-light; + border-radius: $radius-sm; + resize: vertical; + + &:focus { + outline: none; + border-color: $color-accent-primary; + } + + &::placeholder { + color: $color-text-muted; + font-style: italic; + } + } + } + } + } + + // ---------------------------------------- + // NPC Abilities Section + // ---------------------------------------- + .npc-abilities { + .section-header-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: $spacing-2; + + .section-header { + margin: 0; + font-family: $font-family-header; + font-size: $font-size-base; + font-weight: $font-weight-bold; + color: $color-text-primary; + text-transform: uppercase; + letter-spacing: 0.05em; + } + + .ability-add { + @include flex-center; + width: 28px; + height: 28px; + padding: 0; + background-color: $color-parchment; + border: 1px solid $color-border; + border-radius: $radius-md; + color: $color-text-primary; + cursor: pointer; + transition: all $transition-fast; + + &:hover { + background-color: $color-success; + border-color: $color-success; + color: white; + } + + i { + font-size: $font-size-sm; + } + } + } + + .ability-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: $spacing-2; + } + + .ability-item { + padding: $spacing-3; + background-color: $color-parchment-light; + border: 1px solid $color-border; + border-radius: $radius-md; + + &.empty { + display: flex; + flex-direction: column; + align-items: center; + gap: $spacing-2; + padding: $spacing-4; + + p { + margin: 0; + font-style: italic; + color: $color-text-muted; + } + + button { + display: flex; + align-items: center; + gap: $spacing-1; + padding: $spacing-2 $spacing-3; + background-color: $color-parchment; + border: 1px solid $color-border; + border-radius: $radius-md; + font-size: $font-size-sm; + color: $color-text-primary; + cursor: pointer; + transition: all $transition-fast; + + &:hover { + background-color: $color-success; + border-color: $color-success; + color: white; + } + } + } + + .ability-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: $spacing-2; + margin-bottom: $spacing-2; + + .ability-name { + flex: 1; + font-family: $font-family-header; + font-size: $font-size-base; + font-weight: $font-weight-semibold; + color: $color-text-primary; + background: transparent; + border: none; + border-bottom: 1px solid transparent; + padding: $spacing-1 0; + + &:hover, + &:focus { + border-bottom-color: $color-border; + outline: none; + } + } + + .ability-controls { + display: flex; + align-items: center; + gap: $spacing-2; + + .ability-passive { + display: flex; + align-items: center; + gap: $spacing-1; + font-size: $font-size-xs; + color: $color-text-secondary; + cursor: pointer; + + input[type="checkbox"] { + width: 14px; + height: 14px; + cursor: pointer; + accent-color: $color-accent-primary; + } + } + + .ability-delete { + @include flex-center; + width: 24px; + height: 24px; + padding: 0; + background-color: transparent; + border: 1px solid $color-border-light; + border-radius: $radius-sm; + color: $color-text-muted; + cursor: pointer; + transition: all $transition-fast; + + &:hover { + background-color: rgba($color-danger, 0.1); + border-color: $color-danger; + color: $color-danger; + } + + i { + font-size: $font-size-xs; + } + } + } + } + + .ability-description { + textarea { + width: 100%; + min-height: 60px; + padding: $spacing-2; + font-size: $font-size-sm; + line-height: $line-height-relaxed; + color: $color-text-primary; + background-color: $color-parchment; + border: 1px solid $color-border-light; + border-radius: $radius-sm; + resize: vertical; + + &:focus { + outline: none; + border-color: $color-accent-primary; + } + + &::placeholder { + color: $color-text-muted; + font-style: italic; + } + } + } + } + } + + // ---------------------------------------- + // NPC Notes Section + // ---------------------------------------- + .npc-notes { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: $spacing-3; + + .notes-section { + display: flex; + flex-direction: column; + gap: $spacing-2; + + .section-header { + margin: 0; + font-family: $font-family-header; + font-size: $font-size-sm; + font-weight: $font-weight-bold; + color: $color-text-secondary; + text-transform: uppercase; + letter-spacing: 0.05em; + } + + .editor-container { + flex: 1; + + textarea { + width: 100%; + min-height: 100px; + height: 100%; + padding: $spacing-2; + font-size: $font-size-sm; + line-height: $line-height-relaxed; + color: $color-text-primary; + background-color: $color-parchment-light; + border: 1px solid $color-border; + border-radius: $radius-md; + resize: vertical; + + &:focus { + outline: none; + border-color: $color-accent-primary; + } + + &::placeholder { + color: $color-text-muted; + font-style: italic; + } + } + } + } + } + + // ---------------------------------------- + // Responsive NPC Layout + // ---------------------------------------- + @media (max-width: 550px) { + .npc-header { + grid-template-columns: auto 1fr; + grid-template-rows: auto auto; + + .header-portrait { + grid-row: span 2; + } + + .header-hp { + grid-column: 2; + } + } + + .npc-notes { + grid-template-columns: 1fr; + } + + .npc-actions .action-item .action-details { + grid-template-columns: 1fr 1fr; + } + } } diff --git a/templates/actor/character-biography.hbs b/templates/actor/character-biography.hbs index 61434df..aeb6455 100644 --- a/templates/actor/character-biography.hbs +++ b/templates/actor/character-biography.hbs @@ -89,6 +89,48 @@ + {{!-- Movement Capabilities --}} +
+

{{localize "VAGABOND.Movement"}}

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ {{!-- Biography Text --}}

{{localize "VAGABOND.Biography"}}

diff --git a/templates/actor/npc-stats.hbs b/templates/actor/npc-stats.hbs index 301adce..0a87ce3 100644 --- a/templates/actor/npc-stats.hbs +++ b/templates/actor/npc-stats.hbs @@ -17,35 +17,9 @@ {{!-- Speed --}}
-
-
- - - ft -
- {{#if speed.hasSpecialMovement}} - {{#if speed.fly}} -
- - - ft -
- {{/if}} - {{#if speed.swim}} -
- - - ft -
- {{/if}} - {{#if speed.climb}} -
- - - ft -
- {{/if}} - {{/if}} +
+ + ft
@@ -75,11 +49,51 @@ {{!-- Appearing --}}
- +
+ {{!-- Movement Types --}} +
+

{{localize "VAGABOND.Movement"}}

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ {{!-- Senses --}} {{#if hasSenses}}