Style Magic and Biography tabs, update senses system

Magic tab:
- Mana display matching inventory header format
- Focus status panel with active spell tracking
- Spell list with damage/effect badges and cast buttons
- Spellcasting reference guide with delivery/duration costs

Biography tab:
- Character details section with Size and Being Type dropdowns
- Senses as 3-column grid of boolean checkboxes
- Biography and Notes textareas with proper styling
- Languages section hidden (not yet implemented)

Senses system overhaul:
- Changed from mixed boolean/number to all boolean toggles
- Renamed darksight to darkvision
- Added: allsight, echolocation, seismicsense, telepathy
- Removed: tremorsense (not in system)
- Updated both character and NPC data models
- Updated NPC sheet template and hasSenses logic

Also updated PROJECT_ROADMAP.json with styling progress notes.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2025-12-14 22:36:11 -06:00
parent 10963403e9
commit 15fd9f684f
9 changed files with 660 additions and 44 deletions

View File

@ -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",

View File

@ -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...",

View File

@ -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

View File

@ -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

View File

@ -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 || [];

View File

@ -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);
});
});

View File

@ -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);
}
}
}
// ==========================================

View File

@ -46,22 +46,45 @@
<div class="senses-grid">
<div class="sense-field">
<label>
<input type="checkbox" name="system.senses.darksight"
{{#if system.senses.darksight}}checked{{/if}} />
{{localize "VAGABOND.Darksight"}}
<input type="checkbox" name="system.senses.allsight"
{{#if system.senses.allsight}}checked{{/if}} />
{{localize "VAGABOND.Allsight"}}
</label>
</div>
<div class="sense-field">
<label>{{localize "VAGABOND.Blindsight"}}</label>
<input type="number" name="system.senses.blindsight"
value="{{system.senses.blindsight}}" min="0" />
<span class="unit">ft</span>
<label>
<input type="checkbox" name="system.senses.blindsight"
{{#if system.senses.blindsight}}checked{{/if}} />
{{localize "VAGABOND.Blindsight"}}
</label>
</div>
<div class="sense-field">
<label>{{localize "VAGABOND.Tremorsense"}}</label>
<input type="number" name="system.senses.tremorsense"
value="{{system.senses.tremorsense}}" min="0" />
<span class="unit">ft</span>
<label>
<input type="checkbox" name="system.senses.darkvision"
{{#if system.senses.darkvision}}checked{{/if}} />
{{localize "VAGABOND.Darkvision"}}
</label>
</div>
<div class="sense-field">
<label>
<input type="checkbox" name="system.senses.echolocation"
{{#if system.senses.echolocation}}checked{{/if}} />
{{localize "VAGABOND.Echolocation"}}
</label>
</div>
<div class="sense-field">
<label>
<input type="checkbox" name="system.senses.seismicsense"
{{#if system.senses.seismicsense}}checked{{/if}} />
{{localize "VAGABOND.Seismicsense"}}
</label>
</div>
<div class="sense-field">
<label>
<input type="checkbox" name="system.senses.telepathy"
{{#if system.senses.telepathy}}checked{{/if}} />
{{localize "VAGABOND.Telepathy"}}
</label>
</div>
</div>
</div>

View File

@ -84,14 +84,23 @@
{{#if hasSenses}}
<div class="senses-row">
<label>{{localize "VAGABOND.Senses"}}:</label>
{{#if senses.darksight}}
<span class="sense-tag">{{localize "VAGABOND.Darksight"}}</span>
{{#if senses.allsight}}
<span class="sense-tag">{{localize "VAGABOND.Allsight"}}</span>
{{/if}}
{{#if senses.blindsight}}
<span class="sense-tag">{{localize "VAGABOND.Blindsight"}} {{senses.blindsight}} ft</span>
<span class="sense-tag">{{localize "VAGABOND.Blindsight"}}</span>
{{/if}}
{{#if senses.tremorsense}}
<span class="sense-tag">{{localize "VAGABOND.Tremorsense"}} {{senses.tremorsense}} ft</span>
{{#if senses.darkvision}}
<span class="sense-tag">{{localize "VAGABOND.Darkvision"}}</span>
{{/if}}
{{#if senses.echolocation}}
<span class="sense-tag">{{localize "VAGABOND.Echolocation"}}</span>
{{/if}}
{{#if senses.seismicsense}}
<span class="sense-tag">{{localize "VAGABOND.Seismicsense"}}</span>
{{/if}}
{{#if senses.telepathy}}
<span class="sense-tag">{{localize "VAGABOND.Telepathy"}}</span>
{{/if}}
</div>
{{/if}}