diff --git a/PROJECT_ROADMAP.json b/PROJECT_ROADMAP.json index 3a50baa..4599036 100644 --- a/PROJECT_ROADMAP.json +++ b/PROJECT_ROADMAP.json @@ -382,7 +382,7 @@ "tested": false, "priority": "critical", "dependencies": ["2.2"], - "notes": "Uses Foundry v13 ApplicationV2 with HandlebarsApplicationMixin. Includes action handlers, drag-drop, item management." + "notes": "Uses Foundry v13 ApplicationV2 with HandlebarsApplicationMixin. Includes action handlers, drag-drop, item management. Added _cleanupInactiveTabs() to fix ApplicationV2 part stacking issue with tabs." }, { "id": "3.2", @@ -472,7 +472,7 @@ "tested": false, "priority": "high", "dependencies": ["3.2"], - "notes": "Abilities tab with ancestry, features, perks, and active effects display." + "notes": "Abilities tab with ancestry, features, perks, and active effects display. Perk prerequisites use getPrerequisiteString() from data model for proper formatting." }, { "id": "3.11", @@ -663,7 +663,8 @@ "completed": false, "tested": false, "priority": "high", - "dependencies": ["5.1", "5.2", "5.3", "3.2"] + "dependencies": ["5.1", "5.2", "5.3", "3.2"], + "notes": "In progress. Completed: Header, Main tab (stats, combat, saves, skills, attacks), Inventory tab (inline header with item slots/currencies, item lists), Abilities tab (ancestry, features, perks with prerequisite display, active effects). Remaining: Magic tab, Biography tab." }, { "id": "5.5", diff --git a/lang/en.json b/lang/en.json index f529205..bebfd9b 100644 --- a/lang/en.json +++ b/lang/en.json @@ -331,9 +331,12 @@ "VAGABOND.Languages": "Languages", "VAGABOND.NoLanguages": "No languages", "VAGABOND.Senses": "Senses", - "VAGABOND.Darksight": "Darksight", + "VAGABOND.Allsight": "Allsight", "VAGABOND.Blindsight": "Blindsight", - "VAGABOND.Tremorsense": "Tremorsense", + "VAGABOND.Darkvision": "Darkvision", + "VAGABOND.Echolocation": "Echolocation", + "VAGABOND.Seismicsense": "Seismicsense", + "VAGABOND.Telepathy": "Telepathy", "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 b1e63d2..3f114d9 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -419,17 +419,14 @@ export default class CharacterData extends VagabondActorBase { statIncreasesByLevel: new fields.ObjectField({ initial: {} }), // { "2": "might", "4": "dexterity" } }), - // Senses and vision types + // Senses - binary toggles, may be granted by perks/traits senses: new fields.SchemaField({ - darksight: new fields.BooleanField({ initial: false }), // Dwarf, Goblin, Orc, Infravision perk - blindsight: new fields.NumberField({ integer: true, initial: 0, min: 0 }), // Range in feet - tremorsense: new fields.NumberField({ integer: true, initial: 0, min: 0 }), // Range in feet - // Special vision abilities from perks/ancestry - specialVision: new fields.SchemaField({ - elvenEyes: new fields.BooleanField({ initial: false }), // Favor on sight Detect - witchsight: new fields.BooleanField({ initial: false }), // See Invisible, Favor vs illusions - sixthSense: new fields.BooleanField({ initial: false }), // Ignore Blinded for sight-based checks - }), + allsight: new fields.BooleanField({ initial: false }), + blindsight: new fields.BooleanField({ initial: false }), + darkvision: new fields.BooleanField({ initial: false }), // Dwarf, Goblin, Orc, Infravision perk + echolocation: new fields.BooleanField({ initial: false }), + seismicsense: new fields.BooleanField({ initial: false }), + telepathy: new fields.BooleanField({ initial: false }), }), // Known languages diff --git a/module/data/actor/npc.mjs b/module/data/actor/npc.mjs index d0bdba6..47587fc 100644 --- a/module/data/actor/npc.mjs +++ b/module/data/actor/npc.mjs @@ -113,11 +113,14 @@ export default class NPCData extends VagabondActorBase { // Being type (for targeting by certain effects) beingType: new fields.StringField({ initial: "beast" }), - // Senses (vision types) + // Senses - binary toggles, may be granted by abilities/traits senses: new fields.SchemaField({ - darksight: new fields.BooleanField({ initial: false }), - blindsight: new fields.NumberField({ integer: true, initial: 0, min: 0 }), // Range in feet - tremorsense: new fields.NumberField({ integer: true, initial: 0, min: 0 }), // Range in feet + allsight: new fields.BooleanField({ initial: false }), + blindsight: new fields.BooleanField({ initial: false }), + darkvision: new fields.BooleanField({ initial: false }), + echolocation: new fields.BooleanField({ initial: false }), + seismicsense: new fields.BooleanField({ initial: false }), + telepathy: new fields.BooleanField({ initial: false }), }), // Movement speed diff --git a/module/sheets/npc-sheet.mjs b/module/sheets/npc-sheet.mjs index 0f7c604..107e776 100644 --- a/module/sheets/npc-sheet.mjs +++ b/module/sheets/npc-sheet.mjs @@ -119,7 +119,12 @@ export default class VagabondNPCSheet extends VagabondActorSheet { // Senses context.senses = system.senses; context.hasSenses = - system.senses.darksight || system.senses.blindsight > 0 || system.senses.tremorsense > 0; + system.senses.allsight || + system.senses.blindsight || + system.senses.darkvision || + system.senses.echolocation || + system.senses.seismicsense || + system.senses.telepathy; // Damage modifiers context.immunities = system.immunities || []; diff --git a/module/tests/actor.test.mjs b/module/tests/actor.test.mjs index f2cb15b..56c47c0 100644 --- a/module/tests/actor.test.mjs +++ b/module/tests/actor.test.mjs @@ -664,19 +664,19 @@ export function registerActorTests(quenchRunner) { describe("NPC Senses", () => { it("tracks vision types for NPCs", async () => { /** - * Senses determine what an NPC can perceive: - * darksight = see in darkness, blindsight/tremorsense = range in feet + * Senses are binary toggles that determine what an NPC can perceive: + * allsight, blindsight, darkvision, echolocation, seismicsense, telepathy */ - expect(testNPC.system.senses.darksight).to.equal(false); - expect(testNPC.system.senses.blindsight).to.equal(0); + expect(testNPC.system.senses.darkvision).to.equal(false); + expect(testNPC.system.senses.blindsight).to.equal(false); await testNPC.update({ - "system.senses.darksight": true, - "system.senses.blindsight": 30, + "system.senses.darkvision": true, + "system.senses.blindsight": true, }); - expect(testNPC.system.senses.darksight).to.equal(true); - expect(testNPC.system.senses.blindsight).to.equal(30); + expect(testNPC.system.senses.darkvision).to.equal(true); + expect(testNPC.system.senses.blindsight).to.equal(true); }); }); diff --git a/styles/scss/sheets/_actor-sheet.scss b/styles/scss/sheets/_actor-sheet.scss index 80d9e6e..b9fc54a 100644 --- a/styles/scss/sheets/_actor-sheet.scss +++ b/styles/scss/sheets/_actor-sheet.scss @@ -1379,6 +1379,581 @@ } } } + + // ---------------------------------------- + // Magic Tab Layout + // ---------------------------------------- + .magic-tab { + display: flex; + flex-direction: column; + gap: $spacing-4; + } + + // Magic Header (Mana & Focus) + .magic-header { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: $spacing-4; + padding-bottom: $spacing-3; + margin-bottom: $spacing-2; + border-bottom: 2px solid $color-border; + } + + // Mana Display - matches inventory item-slots format + .mana-display { + display: flex; + flex-direction: row; + align-items: center; + gap: $spacing-2; + margin-right: auto; + + > label { + font-size: $font-size-xs; + font-weight: $font-weight-semibold; + text-transform: uppercase; + letter-spacing: 0.05em; + color: $color-text-secondary; + } + + .mana-values { + display: flex; + align-items: baseline; + gap: 2px; + font-family: $font-family-header; + font-size: $font-size-lg; + + input { + width: 40px; + padding: 2px 4px; + font-family: $font-family-header; + font-size: $font-size-lg; + font-weight: $font-weight-bold; + text-align: center; + color: $color-text-primary; + background-color: $color-parchment-light; + border: 1px solid $color-border; + border-radius: $radius-sm; + + &:focus { + outline: none; + border-color: $color-accent-primary; + } + } + + .separator { + color: $color-text-muted; + } + + .max { + color: $color-text-secondary; + } + } + + .casting-max { + display: flex; + align-items: center; + gap: $spacing-2; + margin-left: $spacing-3; + padding-left: $spacing-3; + border-left: 1px solid $color-border-light; + font-size: $font-size-sm; + + .label { + color: $color-text-muted; + } + + .value { + font-weight: $font-weight-bold; + color: $color-info; + } + } + } + + // Focus Display + .focus-display { + flex: 1; + min-width: 200px; + padding: $spacing-3; + background-color: rgba($color-warning, 0.1); + border: 1px solid rgba($color-warning, 0.3); + border-radius: $radius-md; + + h3 { + margin: 0 0 $spacing-2 0; + font-family: $font-family-header; + font-size: $font-size-base; + font-weight: $font-weight-bold; + color: $color-warning; + text-transform: uppercase; + letter-spacing: 0.05em; + } + + .focus-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: $spacing-1; + } + + .focus-item { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: $spacing-2; + padding: $spacing-2; + background-color: $color-parchment-light; + border-radius: $radius-sm; + + .focus-spell { + font-weight: $font-weight-semibold; + color: $color-text-primary; + } + + .focus-target { + font-size: $font-size-sm; + color: $color-text-secondary; + } + + .focus-cost { + font-size: $font-size-xs; + padding: $spacing-1 $spacing-2; + background-color: rgba($color-info, 0.1); + border-radius: $radius-sm; + color: $color-info; + margin-left: auto; + } + } + } + + // Spells Section + .spells-section { + .section-header-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: $spacing-2; + + .section-header { + margin: 0; + padding-bottom: 0; + border-bottom: none; + } + + .item-create { + @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; + } + } + } + } + + // Spell List + .spell-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: $spacing-2; + } + + .spell-item { + display: grid; + grid-template-columns: auto 1fr auto auto; + gap: $spacing-3; + align-items: center; + padding: $spacing-3; + background-color: $color-parchment-light; + border: 1px solid $color-border-light; + border-radius: $radius-md; + transition: all $transition-fast; + + &:hover { + background-color: $color-parchment; + border-color: $color-border; + } + + &.empty { + display: flex; + justify-content: center; + padding: $spacing-4; + font-style: italic; + color: $color-text-muted; + } + + .spell-img { + width: 40px; + height: 40px; + object-fit: cover; + border: 1px solid $color-border; + border-radius: $radius-md; + background-color: $color-parchment; + } + + .spell-info { + display: flex; + flex-direction: column; + gap: 2px; + + .spell-name { + font-weight: $font-weight-semibold; + color: $color-text-primary; + cursor: pointer; + + &:hover { + color: $color-accent-primary; + text-decoration: underline; + } + } + + .spell-type { + font-size: $font-size-xs; + color: $color-text-secondary; + text-transform: capitalize; + } + } + + .spell-details { + display: flex; + align-items: center; + gap: $spacing-2; + + .spell-damage { + display: flex; + align-items: center; + gap: $spacing-1; + padding: $spacing-1 $spacing-2; + background-color: rgba($color-danger, 0.1); + border-radius: $radius-sm; + font-family: $font-family-mono; + font-size: $font-size-sm; + font-weight: $font-weight-semibold; + color: $color-danger; + + i { + font-size: $font-size-xs; + } + } + + .spell-effect { + display: flex; + align-items: center; + gap: $spacing-1; + padding: $spacing-1 $spacing-2; + background-color: rgba($color-info, 0.1); + border-radius: $radius-sm; + font-size: $font-size-sm; + color: $color-info; + cursor: help; + + i { + font-size: $font-size-xs; + } + } + } + + .spell-actions { + display: flex; + align-items: center; + gap: $spacing-2; + + .spell-cast { + display: flex; + align-items: center; + gap: $spacing-1; + padding: $spacing-2 $spacing-3; + background-color: $color-info; + border: none; + border-radius: $radius-md; + font-size: $font-size-sm; + font-weight: $font-weight-semibold; + color: white; + cursor: pointer; + transition: all $transition-fast; + + &:hover { + background-color: darken($color-info, 10%); + } + + i { + font-size: $font-size-sm; + } + } + + .item-delete { + @include flex-center; + width: 28px; + height: 28px; + 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-sm; + } + } + } + } + + // Casting Reference + .casting-reference { + padding: $spacing-3; + background-color: $color-parchment-light; + border: 1px solid $color-border-light; + border-radius: $radius-md; + + > h3 { + margin: 0 0 $spacing-3 0; + font-family: $font-family-header; + font-size: $font-size-base; + font-weight: $font-weight-bold; + color: $color-text-secondary; + text-transform: uppercase; + letter-spacing: 0.05em; + padding-bottom: $spacing-2; + border-bottom: 1px solid $color-border-light; + } + + .reference-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: $spacing-4; + } + + .reference-section { + h4 { + margin: 0 0 $spacing-2 0; + font-size: $font-size-sm; + font-weight: $font-weight-semibold; + color: $color-text-primary; + } + + ul { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: $spacing-1; + + li { + font-size: $font-size-sm; + color: $color-text-secondary; + line-height: $line-height-normal; + + strong { + color: $color-text-primary; + } + } + } + } + } + + // Responsive magic layout + @container sheet-content (max-width: 500px) { + .magic-header { + flex-direction: column; + } + + .spell-item { + grid-template-columns: auto 1fr; + grid-template-rows: auto auto; + + .spell-img { + grid-row: span 2; + } + + .spell-details { + grid-column: 2; + justify-content: flex-start; + } + + .spell-actions { + grid-column: 1 / -1; + justify-content: flex-end; + } + } + + .casting-reference .reference-grid { + grid-template-columns: 1fr; + } + } + + // ---------------------------------------- + // Biography Tab Layout + // ---------------------------------------- + .biography-tab { + display: flex; + flex-direction: column; + gap: $spacing-4; + } + + .biography-section { + // Hide languages section - not yet implemented + &.languages { + display: none; + } + } + + // Character Details (Size, Being Type) + .details-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: $spacing-3; + + .detail-field { + 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; + } + + select { + @include input-base; + padding: $spacing-2; + font-size: $font-size-base; + cursor: pointer; + + &:focus { + outline: none; + border-color: $color-accent-primary; + } + } + } + } + + // Senses Section + .senses-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: $spacing-3; + + .sense-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 { + .editor-container { + background-color: $color-parchment-light; + border: 1px solid $color-border; + border-radius: $radius-md; + overflow: hidden; + } + + textarea { + width: 100%; + min-height: 150px; + padding: $spacing-3; + font-family: $font-family-body; + font-size: $font-size-sm; + line-height: $line-height-relaxed; + color: $color-text-primary; + background-color: transparent; + border: none; + resize: vertical; + + &:focus { + outline: none; + } + + &::placeholder { + color: $color-text-muted; + font-style: italic; + } + } + } + + // Biography gets more height + .biography-text textarea { + min-height: 200px; + } + + // Responsive biography layout + @container sheet-content (max-width: 500px) { + .details-grid { + grid-template-columns: 1fr; + } + + .senses-grid { + grid-template-columns: repeat(2, 1fr); + } + } } // ========================================== diff --git a/templates/actor/character-biography.hbs b/templates/actor/character-biography.hbs index b18960e..61434df 100644 --- a/templates/actor/character-biography.hbs +++ b/templates/actor/character-biography.hbs @@ -46,22 +46,45 @@