Implement character sheet foundation with ApplicationV2
Add actor sheet implementation using Foundry VTT v13 ApplicationV2 API: - Base actor sheet class with tab navigation, drag-drop, scroll preservation - Character sheet with header, main tab (stats, saves, skills, attacks) - NPC sheet structure (templates only, styling pending) - Resource bars with fill effect and backdrop pills for legibility - Responsive layout using CSS Container Queries - Fix for ApplicationV2 tab switching (cleanup stale parts) 🤖 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
f6948dc7d6
commit
8e097c9b2d
@ -342,19 +342,21 @@
|
||||
"id": "2.12",
|
||||
"name": "Implement class feature automation",
|
||||
"description": "When class item added to character, apply appropriate Active Effects for current level; update on level change",
|
||||
"completed": false,
|
||||
"tested": false,
|
||||
"completed": true,
|
||||
"tested": true,
|
||||
"priority": "high",
|
||||
"dependencies": ["2.2", "2.3", "1.8", "1.15"]
|
||||
"dependencies": ["2.2", "2.3", "1.8", "1.15"],
|
||||
"notes": "Implemented via _onCreate/_preDelete lifecycle methods and updateActor hook. Made applyClassFeatures() idempotent. Commit 8afcf8c."
|
||||
},
|
||||
{
|
||||
"id": "2.13",
|
||||
"name": "Implement morale check system (NPC)",
|
||||
"description": "2d6 vs Morale roll, triggered manually or via hooks on death/half HP",
|
||||
"completed": false,
|
||||
"tested": false,
|
||||
"completed": true,
|
||||
"tested": true,
|
||||
"priority": "medium",
|
||||
"dependencies": ["2.4", "1.3"]
|
||||
"dependencies": ["2.4", "1.3"],
|
||||
"notes": "rollMorale() for individual, rollGroupMorale() for lowest in selection, auto-prompt at half HP, chat button handler, macro created. Commit f6948dc."
|
||||
},
|
||||
{
|
||||
"id": "2.14",
|
||||
@ -376,145 +378,161 @@
|
||||
"id": "3.1",
|
||||
"name": "Create base VagabondActorSheet class",
|
||||
"description": "Extended ActorSheet with common methods, tab handling, drag-drop, context menus",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "critical",
|
||||
"dependencies": ["2.2"]
|
||||
"dependencies": ["2.2"],
|
||||
"notes": "Uses Foundry v13 ApplicationV2 with HandlebarsApplicationMixin. Includes action handlers, drag-drop, item management."
|
||||
},
|
||||
{
|
||||
"id": "3.2",
|
||||
"name": "Design Character sheet layout (HTML/Handlebars)",
|
||||
"description": "Match official Hero Record: Stats column, HP/Armor/Fatigue, Speed section, Saves, Skills grid, Attacks, Inventory, Abilities, Magic",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "critical",
|
||||
"dependencies": ["3.1"]
|
||||
"dependencies": ["3.1"],
|
||||
"notes": "Tabbed layout with Main, Inventory, Abilities, Magic, Biography tabs. Templates in templates/actor/."
|
||||
},
|
||||
{
|
||||
"id": "3.3",
|
||||
"name": "Implement Character sheet - Header section",
|
||||
"description": "Name, Ancestry, Level, Class, XP, Size, Being Type fields",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "critical",
|
||||
"dependencies": ["3.2"]
|
||||
"dependencies": ["3.2"],
|
||||
"notes": "Header includes portrait, name, ancestry/class display, level/XP inputs, HP/Mana bars, secondary resources."
|
||||
},
|
||||
{
|
||||
"id": "3.4",
|
||||
"name": "Implement Character sheet - Stats section",
|
||||
"description": "Six stats display with large numbers matching official sheet aesthetic",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "critical",
|
||||
"dependencies": ["3.2"]
|
||||
"dependencies": ["3.2"],
|
||||
"notes": "Stats grid on Main tab with editable values and stat abbreviations."
|
||||
},
|
||||
{
|
||||
"id": "3.5",
|
||||
"name": "Implement Character sheet - Combat section",
|
||||
"description": "HP (current/max), Armor, Fatigue, Speed (base/bonus/crawl/travel), Current Luck",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "critical",
|
||||
"dependencies": ["3.2"]
|
||||
"dependencies": ["3.2"],
|
||||
"notes": "Resource bars in header, secondary resources (Luck, Fatigue, Armor, Speed) displayed."
|
||||
},
|
||||
{
|
||||
"id": "3.6",
|
||||
"name": "Implement Character sheet - Saves section",
|
||||
"description": "Reflex, Endure, Will with calculated difficulties, clickable for rolls",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "critical",
|
||||
"dependencies": ["3.2", "2.7"]
|
||||
"dependencies": ["3.2", "2.7"],
|
||||
"notes": "Saves grid on Main tab with stat formulas and clickable roll action."
|
||||
},
|
||||
{
|
||||
"id": "3.7",
|
||||
"name": "Implement Character sheet - Skills section",
|
||||
"description": "12 skills grid with trained checkboxes, stat associations, difficulty numbers, clickable for rolls",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "critical",
|
||||
"dependencies": ["3.2", "2.5"]
|
||||
"dependencies": ["3.2", "2.5"],
|
||||
"notes": "Skills grid with trained toggle, stat abbreviation, difficulty, crit threshold display."
|
||||
},
|
||||
{
|
||||
"id": "3.8",
|
||||
"name": "Implement Character sheet - Attacks section",
|
||||
"description": "Weapon attack skills (Melee/Brawl/Ranged/Finesse) with difficulties, equipped weapons list with roll buttons",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "critical",
|
||||
"dependencies": ["3.2", "2.6"]
|
||||
"dependencies": ["3.2", "2.6"],
|
||||
"notes": "Attack skills and equipped weapons on Main tab with roll buttons."
|
||||
},
|
||||
{
|
||||
"id": "3.9",
|
||||
"name": "Implement Character sheet - Inventory tab",
|
||||
"description": "Item list with slots display, wealth tracking (G/S/C), occupied/max/bonus slots, drag-drop support",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "high",
|
||||
"dependencies": ["3.2", "2.11"]
|
||||
"dependencies": ["3.2", "2.11"],
|
||||
"notes": "Inventory tab with weapons, armor, equipment sections, slot tracking, wealth inputs."
|
||||
},
|
||||
{
|
||||
"id": "3.10",
|
||||
"name": "Implement Character sheet - Abilities tab",
|
||||
"description": "List of Features, Perks, Ancestry traits; expandable descriptions; usage tracking for limited abilities",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "high",
|
||||
"dependencies": ["3.2"]
|
||||
"dependencies": ["3.2"],
|
||||
"notes": "Abilities tab with ancestry, features, perks, and active effects display."
|
||||
},
|
||||
{
|
||||
"id": "3.11",
|
||||
"name": "Implement Character sheet - Magic tab",
|
||||
"description": "Mana (current/max/casting max), known spells list with cast buttons, focus indicator",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "high",
|
||||
"dependencies": ["3.2", "2.8"]
|
||||
"dependencies": ["3.2", "2.8"],
|
||||
"notes": "Magic tab with mana display, focus tracking, spell list with cast buttons, reference guide."
|
||||
},
|
||||
{
|
||||
"id": "3.12",
|
||||
"name": "Implement Character sheet - Biography tab",
|
||||
"description": "Rich text editor for character background, notes, bonds",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "medium",
|
||||
"dependencies": ["3.2"]
|
||||
"dependencies": ["3.2"],
|
||||
"notes": "Biography tab with character details, languages, senses, biography and notes textareas."
|
||||
},
|
||||
{
|
||||
"id": "3.13",
|
||||
"name": "Create NPC/Monster sheet layout",
|
||||
"description": "Compact stat block format: HD, HP, TL, Zone, Morale, Armor, Immunities, Weaknesses, Actions, Abilities",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "high",
|
||||
"dependencies": ["3.1", "1.3"]
|
||||
"dependencies": ["3.1", "1.3"],
|
||||
"notes": "NPC sheet with header, stats, actions, abilities, and notes sections (no tabs)."
|
||||
},
|
||||
{
|
||||
"id": "3.14",
|
||||
"name": "Implement NPC sheet - Stat block section",
|
||||
"description": "Display all combat-relevant stats in traditional TTRPG stat block format",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "high",
|
||||
"dependencies": ["3.13"]
|
||||
"dependencies": ["3.13"],
|
||||
"notes": "Stats section with zone, speed, size, being type, senses, damage modifiers."
|
||||
},
|
||||
{
|
||||
"id": "3.15",
|
||||
"name": "Implement NPC sheet - Actions section",
|
||||
"description": "List of attack actions with clickable roll buttons, damage dice display",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "high",
|
||||
"dependencies": ["3.13", "2.4"]
|
||||
"dependencies": ["3.13", "2.4"],
|
||||
"notes": "Dynamic actions list with add/delete, editable fields, roll buttons."
|
||||
},
|
||||
{
|
||||
"id": "3.16",
|
||||
"name": "Implement NPC sheet - Morale button",
|
||||
"description": "Clickable morale check with result interpretation",
|
||||
"completed": false,
|
||||
"completed": true,
|
||||
"tested": false,
|
||||
"priority": "medium",
|
||||
"dependencies": ["3.13", "2.13"]
|
||||
"dependencies": ["3.13", "2.13"],
|
||||
"notes": "Morale roll button in header, disabled when morale broken."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
86
lang/en.json
86
lang/en.json
@ -279,5 +279,89 @@
|
||||
"VAGABOND.InsufficientManaShort": "Insufficient Mana",
|
||||
"VAGABOND.CastSuccess": "Cast Success!",
|
||||
"VAGABOND.CastFailed": "Cast Failed",
|
||||
"VAGABOND.CriticalCast": "Critical Cast!"
|
||||
"VAGABOND.CriticalCast": "Critical Cast!",
|
||||
|
||||
"VAGABOND.TabMain": "Main",
|
||||
"VAGABOND.TabInventory": "Inventory",
|
||||
"VAGABOND.TabAbilities": "Abilities",
|
||||
"VAGABOND.TabMagic": "Magic",
|
||||
"VAGABOND.TabBiography": "Biography",
|
||||
|
||||
"VAGABOND.CharacterName": "Character Name",
|
||||
"VAGABOND.NPCName": "NPC Name",
|
||||
"VAGABOND.ChangePortrait": "Change Portrait",
|
||||
|
||||
"VAGABOND.ResourceHP": "HP",
|
||||
"VAGABOND.ResourceMana": "Mana",
|
||||
"VAGABOND.ResourceLuck": "Luck",
|
||||
"VAGABOND.ResourceFatigue": "Fatigue",
|
||||
"VAGABOND.Heal": "Heal",
|
||||
|
||||
"VAGABOND.Overburdened": "Overburdened!",
|
||||
"VAGABOND.Weapons": "Weapons",
|
||||
"VAGABOND.Equipment": "Equipment",
|
||||
"VAGABOND.EquippedWeapons": "Equipped Weapons",
|
||||
|
||||
"VAGABOND.RollSkill": "Roll Skill",
|
||||
"VAGABOND.ToggleTrained": "Toggle Trained",
|
||||
"VAGABOND.ToggleEquipped": "Toggle Equipped",
|
||||
"VAGABOND.DeleteItem": "Delete Item",
|
||||
"VAGABOND.CreateWeapon": "Create Weapon",
|
||||
"VAGABOND.CreateArmor": "Create Armor",
|
||||
"VAGABOND.CreateEquipment": "Create Equipment",
|
||||
"VAGABOND.CreatePerk": "Create Perk",
|
||||
"VAGABOND.NoWeapons": "No weapons",
|
||||
"VAGABOND.NoArmor": "No armor",
|
||||
"VAGABOND.NoEquipment": "No equipment",
|
||||
"VAGABOND.NoFeatures": "No features",
|
||||
"VAGABOND.NoPerks": "No perks",
|
||||
"VAGABOND.NoSpells": "No spells known",
|
||||
|
||||
"VAGABOND.ActiveEffects": "Active Effects",
|
||||
"VAGABOND.TemporaryEffects": "Temporary Effects",
|
||||
"VAGABOND.PassiveEffects": "Passive Effects",
|
||||
|
||||
"VAGABOND.KnownSpells": "Known Spells",
|
||||
"VAGABOND.FocusActive": "Active Focus",
|
||||
"VAGABOND.SpellcastingReference": "Spellcasting Reference",
|
||||
|
||||
"VAGABOND.CharacterDetails": "Character Details",
|
||||
"VAGABOND.Size": "Size",
|
||||
"VAGABOND.BeingType": "Being Type",
|
||||
"VAGABOND.Languages": "Languages",
|
||||
"VAGABOND.NoLanguages": "No languages",
|
||||
"VAGABOND.Senses": "Senses",
|
||||
"VAGABOND.Darksight": "Darksight",
|
||||
"VAGABOND.Blindsight": "Blindsight",
|
||||
"VAGABOND.Tremorsense": "Tremorsense",
|
||||
"VAGABOND.BiographyPlaceholder": "Enter character background...",
|
||||
"VAGABOND.Notes": "Notes",
|
||||
"VAGABOND.NotesPlaceholder": "Enter notes...",
|
||||
|
||||
"VAGABOND.RollMorale": "Roll Morale",
|
||||
"VAGABOND.MoraleBroken": "Morale Broken!",
|
||||
"VAGABOND.Immunities": "Immunities",
|
||||
"VAGABOND.Resistances": "Resistances",
|
||||
"VAGABOND.Weaknesses": "Weaknesses",
|
||||
"VAGABOND.AddAction": "Add Action",
|
||||
"VAGABOND.DeleteAction": "Delete Action",
|
||||
"VAGABOND.RollAction": "Roll Action",
|
||||
"VAGABOND.ActionName": "Action Name",
|
||||
"VAGABOND.ActionDescription": "Action description...",
|
||||
"VAGABOND.NoActions": "No actions defined",
|
||||
"VAGABOND.AddAbility": "Add Ability",
|
||||
"VAGABOND.DeleteAbility": "Delete Ability",
|
||||
"VAGABOND.AbilityName": "Ability Name",
|
||||
"VAGABOND.AbilityDescription": "Ability description...",
|
||||
"VAGABOND.NoAbilities": "No abilities defined",
|
||||
"VAGABOND.Passive": "Passive",
|
||||
"VAGABOND.Range": "Range",
|
||||
"VAGABOND.Loot": "Loot",
|
||||
"VAGABOND.LootPlaceholder": "Describe loot and treasure...",
|
||||
"VAGABOND.GMNotes": "GM Notes",
|
||||
"VAGABOND.GMNotesPlaceholder": "Notes for the GM...",
|
||||
|
||||
"VAGABOND.ItemNew": "New {type}",
|
||||
"VAGABOND.ItemDeleteTitle": "Delete {name}",
|
||||
"VAGABOND.ItemDeleteConfirm": "Are you sure you want to delete {name}?"
|
||||
}
|
||||
|
||||
8
module/sheets/_module.mjs
Normal file
8
module/sheets/_module.mjs
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Sheet Classes Module
|
||||
* Exports all actor and item sheet classes for the Vagabond RPG system.
|
||||
*/
|
||||
|
||||
export { default as VagabondActorSheet } from "./base-actor-sheet.mjs";
|
||||
export { default as VagabondCharacterSheet } from "./character-sheet.mjs";
|
||||
export { default as VagabondNPCSheet } from "./npc-sheet.mjs";
|
||||
716
module/sheets/base-actor-sheet.mjs
Normal file
716
module/sheets/base-actor-sheet.mjs
Normal file
@ -0,0 +1,716 @@
|
||||
/**
|
||||
* Base Actor Sheet for Vagabond RPG
|
||||
*
|
||||
* Provides common functionality for all actor sheets:
|
||||
* - Tab navigation
|
||||
* - Drag-and-drop handling
|
||||
* - Item management (create, edit, delete)
|
||||
* - Context menus
|
||||
* - Roll integration
|
||||
*
|
||||
* Uses Foundry VTT v13 ActorSheetV2 API with HandlebarsApplicationMixin.
|
||||
*
|
||||
* @extends ActorSheetV2
|
||||
* @mixes HandlebarsApplicationMixin
|
||||
*/
|
||||
|
||||
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
const { ActorSheetV2 } = foundry.applications.sheets;
|
||||
|
||||
export default class VagabondActorSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||
/**
|
||||
* @param {Object} options - Application options
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
super(options);
|
||||
|
||||
// Active tab tracking
|
||||
this._activeTab = "main";
|
||||
|
||||
// Scroll position preservation
|
||||
this._scrollPositions = {};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Static Properties */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
id: "vagabond-actor-sheet-{id}",
|
||||
classes: ["vagabond", "sheet", "actor"],
|
||||
tag: "form",
|
||||
window: {
|
||||
title: "VAGABOND.ActorSheet",
|
||||
icon: "fa-solid fa-user",
|
||||
resizable: true,
|
||||
},
|
||||
position: {
|
||||
width: 720,
|
||||
height: 800,
|
||||
},
|
||||
form: {
|
||||
handler: VagabondActorSheet.#onFormSubmit,
|
||||
submitOnChange: true,
|
||||
closeOnSubmit: false,
|
||||
},
|
||||
actions: {
|
||||
rollSkill: VagabondActorSheet.#onRollSkill,
|
||||
rollSave: VagabondActorSheet.#onRollSave,
|
||||
rollAttack: VagabondActorSheet.#onRollAttack,
|
||||
castSpell: VagabondActorSheet.#onCastSpell,
|
||||
itemEdit: VagabondActorSheet.#onItemEdit,
|
||||
itemDelete: VagabondActorSheet.#onItemDelete,
|
||||
itemCreate: VagabondActorSheet.#onItemCreate,
|
||||
itemToggleEquipped: VagabondActorSheet.#onItemToggleEquipped,
|
||||
changeTab: VagabondActorSheet.#onChangeTab,
|
||||
modifyResource: VagabondActorSheet.#onModifyResource,
|
||||
toggleTrained: VagabondActorSheet.#onToggleTrained,
|
||||
},
|
||||
};
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
header: {
|
||||
template: "systems/vagabond/templates/actor/parts/header.hbs",
|
||||
},
|
||||
tabs: {
|
||||
template: "systems/vagabond/templates/actor/parts/tabs.hbs",
|
||||
},
|
||||
body: {
|
||||
template: "systems/vagabond/templates/actor/parts/body.hbs",
|
||||
},
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Getters */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Convenient alias for the actor document.
|
||||
* @returns {VagabondActor}
|
||||
*/
|
||||
get actor() {
|
||||
return this.document;
|
||||
}
|
||||
|
||||
/** @override */
|
||||
get title() {
|
||||
return this.document.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available tabs for this sheet.
|
||||
* Subclasses should override to define their tabs.
|
||||
* @returns {Object[]} Array of tab definitions
|
||||
*/
|
||||
get tabs() {
|
||||
return [
|
||||
{ id: "main", label: "VAGABOND.TabMain", icon: "fa-solid fa-user" },
|
||||
{ id: "inventory", label: "VAGABOND.TabInventory", icon: "fa-solid fa-suitcase" },
|
||||
{ id: "abilities", label: "VAGABOND.TabAbilities", icon: "fa-solid fa-star" },
|
||||
{ id: "biography", label: "VAGABOND.TabBiography", icon: "fa-solid fa-book" },
|
||||
];
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Data Preparation */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _prepareContext(options) {
|
||||
const context = await super._prepareContext(options);
|
||||
|
||||
// Basic actor data
|
||||
context.actor = this.actor;
|
||||
context.system = this.actor.system;
|
||||
context.source = this.actor.toObject().system;
|
||||
context.items = this._prepareItems();
|
||||
context.effects = this._prepareActiveEffects();
|
||||
|
||||
// Sheet state
|
||||
context.activeTab = this._activeTab;
|
||||
context.tabs = this.tabs.map((tab) => ({
|
||||
...tab,
|
||||
active: tab.id === this._activeTab,
|
||||
cssClass: tab.id === this._activeTab ? "active" : "",
|
||||
}));
|
||||
|
||||
// Roll data for formulas in templates
|
||||
context.rollData = this.actor.getRollData();
|
||||
|
||||
// Editable state
|
||||
context.editable = this.isEditable;
|
||||
context.owner = this.actor.isOwner;
|
||||
context.limited = this.actor.limited;
|
||||
|
||||
// System configuration
|
||||
context.config = CONFIG.VAGABOND;
|
||||
|
||||
// Type-specific context
|
||||
await this._prepareTypeContext(context, options);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare type-specific context data.
|
||||
* Subclasses should override this.
|
||||
*
|
||||
* @param {Object} context - The context object to augment
|
||||
* @param {Object} options - Render options
|
||||
* @protected
|
||||
*/
|
||||
async _prepareTypeContext(_context, _options) {
|
||||
// Override in subclasses
|
||||
}
|
||||
|
||||
/**
|
||||
* Organize and classify items for the sheet.
|
||||
*
|
||||
* @returns {Object} Categorized items
|
||||
* @protected
|
||||
*/
|
||||
_prepareItems() {
|
||||
const items = {
|
||||
weapons: [],
|
||||
armor: [],
|
||||
equipment: [],
|
||||
spells: [],
|
||||
features: [],
|
||||
perks: [],
|
||||
classes: [],
|
||||
ancestry: null,
|
||||
};
|
||||
|
||||
for (const item of this.actor.items) {
|
||||
// Set common properties
|
||||
item.system.isEquipped = item.system.equipped ?? false;
|
||||
|
||||
switch (item.type) {
|
||||
case "weapon":
|
||||
items.weapons.push(item);
|
||||
break;
|
||||
case "armor":
|
||||
items.armor.push(item);
|
||||
break;
|
||||
case "equipment":
|
||||
items.equipment.push(item);
|
||||
break;
|
||||
case "spell":
|
||||
items.spells.push(item);
|
||||
break;
|
||||
case "feature":
|
||||
items.features.push(item);
|
||||
break;
|
||||
case "perk":
|
||||
items.perks.push(item);
|
||||
break;
|
||||
case "class":
|
||||
items.classes.push(item);
|
||||
break;
|
||||
case "ancestry":
|
||||
items.ancestry = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort items by name
|
||||
for (const category of Object.keys(items)) {
|
||||
if (Array.isArray(items[category])) {
|
||||
items[category].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare active effects for display.
|
||||
*
|
||||
* @returns {Object} Categorized effects
|
||||
* @protected
|
||||
*/
|
||||
_prepareActiveEffects() {
|
||||
const effects = {
|
||||
temporary: [],
|
||||
passive: [],
|
||||
inactive: [],
|
||||
};
|
||||
|
||||
for (const effect of this.actor.effects) {
|
||||
if (effect.disabled) {
|
||||
effects.inactive.push(effect);
|
||||
} else if (effect.isTemporary) {
|
||||
effects.temporary.push(effect);
|
||||
} else {
|
||||
effects.passive.push(effect);
|
||||
}
|
||||
}
|
||||
|
||||
return effects;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Rendering */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_preRender(context, options) {
|
||||
super._preRender(context, options);
|
||||
|
||||
// Save scroll positions before re-render
|
||||
this._saveScrollPositions();
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_onRender(context, options) {
|
||||
super._onRender(context, options);
|
||||
|
||||
// Remove stale tab content (ApplicationV2 appends parts without removing old ones)
|
||||
this._cleanupInactiveTabs();
|
||||
|
||||
// Restore scroll positions after re-render
|
||||
this._restoreScrollPositions();
|
||||
|
||||
// Set up drag-and-drop for items
|
||||
this._setupDragDrop();
|
||||
|
||||
// Initialize any content-editable fields
|
||||
this._initializeEditors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove tab content sections that don't match the active tab.
|
||||
* ApplicationV2's parts rendering appends new parts without removing old ones,
|
||||
* so we need to clean up inactive tabs after each render.
|
||||
* @protected
|
||||
*/
|
||||
_cleanupInactiveTabs() {
|
||||
if (!this.element) return;
|
||||
|
||||
const activeTabClass = `${this._activeTab}-tab`;
|
||||
const tabContents = this.element.querySelectorAll(".tab-content");
|
||||
|
||||
for (const tabContent of tabContents) {
|
||||
// Check if this tab content matches the active tab
|
||||
if (!tabContent.classList.contains(activeTabClass)) {
|
||||
tabContent.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save scroll positions of scrollable elements before re-render.
|
||||
* @protected
|
||||
*/
|
||||
_saveScrollPositions() {
|
||||
if (!this.element) return;
|
||||
|
||||
// Save main window scroll
|
||||
const windowEl = this.element.querySelector(".window-content");
|
||||
if (windowEl) {
|
||||
this._scrollPositions.window = windowEl.scrollTop;
|
||||
}
|
||||
|
||||
// Save tab content scroll
|
||||
const tabContent = this.element.querySelector(".tab-content");
|
||||
if (tabContent) {
|
||||
this._scrollPositions.tabContent = tabContent.scrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore scroll positions of scrollable elements after re-render.
|
||||
* @protected
|
||||
*/
|
||||
_restoreScrollPositions() {
|
||||
if (!this.element) return;
|
||||
|
||||
// Restore main window scroll
|
||||
const windowEl = this.element.querySelector(".window-content");
|
||||
if (windowEl && this._scrollPositions.window !== undefined) {
|
||||
windowEl.scrollTop = this._scrollPositions.window;
|
||||
}
|
||||
|
||||
// Restore tab content scroll
|
||||
const tabContent = this.element.querySelector(".tab-content");
|
||||
if (tabContent && this._scrollPositions.tabContent !== undefined) {
|
||||
tabContent.scrollTop = this._scrollPositions.tabContent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up drag-and-drop handlers.
|
||||
* @protected
|
||||
*/
|
||||
_setupDragDrop() {
|
||||
// Enable dragging items from the sheet
|
||||
const draggables = this.element.querySelectorAll("[data-item-id]");
|
||||
for (const el of draggables) {
|
||||
el.setAttribute("draggable", "true");
|
||||
el.addEventListener("dragstart", this._onDragStart.bind(this));
|
||||
}
|
||||
|
||||
// Enable dropping items onto the sheet
|
||||
this.element.addEventListener("dragover", this._onDragOver.bind(this));
|
||||
this.element.addEventListener("drop", this._onDrop.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize rich text editors.
|
||||
* @protected
|
||||
*/
|
||||
_initializeEditors() {
|
||||
// TinyMCE or ProseMirror editors would be initialized here
|
||||
// For now, we use simple textareas
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Drag and Drop */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle drag start for items.
|
||||
* @param {DragEvent} event
|
||||
* @protected
|
||||
*/
|
||||
_onDragStart(event) {
|
||||
const itemId = event.currentTarget.dataset.itemId;
|
||||
const item = this.actor.items.get(itemId);
|
||||
if (!item) return;
|
||||
|
||||
const dragData = {
|
||||
type: "Item",
|
||||
uuid: item.uuid,
|
||||
};
|
||||
|
||||
event.dataTransfer.setData("text/plain", JSON.stringify(dragData));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle drag over.
|
||||
* @param {DragEvent} event
|
||||
* @protected
|
||||
*/
|
||||
_onDragOver(event) {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle drop onto the sheet.
|
||||
* @param {DragEvent} event
|
||||
* @protected
|
||||
*/
|
||||
async _onDrop(event) {
|
||||
event.preventDefault();
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(event.dataTransfer.getData("text/plain"));
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle different drop types
|
||||
switch (data.type) {
|
||||
case "Item":
|
||||
return this._onDropItem(event, data);
|
||||
case "ActiveEffect":
|
||||
return this._onDropActiveEffect(event, data);
|
||||
case "Actor":
|
||||
return this._onDropActor(event, data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle dropping an Item onto the sheet.
|
||||
* @param {DragEvent} event
|
||||
* @param {Object} data
|
||||
* @protected
|
||||
*/
|
||||
async _onDropItem(event, data) {
|
||||
if (!this.actor.isOwner) return;
|
||||
|
||||
const item = await Item.fromDropData(data);
|
||||
if (!item) return;
|
||||
|
||||
// If the item is from this actor, it's a sort operation
|
||||
if (item.parent === this.actor) {
|
||||
return this._onSortItem(event, item);
|
||||
}
|
||||
|
||||
// Create the item on this actor
|
||||
return this._onDropItemCreate(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle creating an Item from a drop.
|
||||
* @param {VagabondItem} item
|
||||
* @protected
|
||||
*/
|
||||
async _onDropItemCreate(item) {
|
||||
const itemData = item.toObject();
|
||||
|
||||
// Special handling for ancestry (only one allowed)
|
||||
if (item.type === "ancestry") {
|
||||
const existingAncestry = this.actor.items.find((i) => i.type === "ancestry");
|
||||
if (existingAncestry) {
|
||||
await existingAncestry.delete();
|
||||
}
|
||||
}
|
||||
|
||||
return this.actor.createEmbeddedDocuments("Item", [itemData]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle sorting items within the sheet.
|
||||
* @param {DragEvent} event
|
||||
* @param {VagabondItem} item
|
||||
* @protected
|
||||
*/
|
||||
async _onSortItem(event, item) {
|
||||
// Get the drop target
|
||||
const dropTarget = event.target.closest("[data-item-id]");
|
||||
if (!dropTarget) return;
|
||||
|
||||
const targetId = dropTarget.dataset.itemId;
|
||||
if (targetId === item.id) return;
|
||||
|
||||
const target = this.actor.items.get(targetId);
|
||||
if (!target || target.type !== item.type) return;
|
||||
|
||||
// Perform the sort
|
||||
const siblings = this.actor.items.filter((i) => i.type === item.type && i.id !== item.id);
|
||||
const sortUpdates = foundry.utils.SortingHelpers.performIntegerSort(item, {
|
||||
target,
|
||||
siblings,
|
||||
});
|
||||
|
||||
const updateData = sortUpdates.map((u) => ({
|
||||
_id: u.target.id,
|
||||
sort: u.update.sort,
|
||||
}));
|
||||
|
||||
return this.actor.updateEmbeddedDocuments("Item", updateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle dropping an Active Effect.
|
||||
* @param {DragEvent} event
|
||||
* @param {Object} data
|
||||
* @protected
|
||||
*/
|
||||
async _onDropActiveEffect(event, data) {
|
||||
const effect = await ActiveEffect.fromDropData(data);
|
||||
if (!effect) return;
|
||||
|
||||
if (effect.parent === this.actor) {
|
||||
return; // No-op for effects already on this actor
|
||||
}
|
||||
|
||||
return this.actor.createEmbeddedDocuments("ActiveEffect", [effect.toObject()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle dropping an Actor (e.g., for summoning).
|
||||
* @param {DragEvent} event
|
||||
* @param {Object} data
|
||||
* @protected
|
||||
*/
|
||||
async _onDropActor(_event, _data) {
|
||||
// Override in subclasses if needed (e.g., for companion/summon tracking)
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Action Handlers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle form submission.
|
||||
* @param {Event} event
|
||||
* @param {HTMLFormElement} form
|
||||
* @param {FormDataExtended} formData
|
||||
*/
|
||||
static async #onFormSubmit(event, form, formData) {
|
||||
const sheet = this;
|
||||
const updateData = foundry.utils.expandObject(formData.object);
|
||||
await sheet.actor.update(updateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle skill roll action.
|
||||
* @param {PointerEvent} event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static async #onRollSkill(event, target) {
|
||||
event.preventDefault();
|
||||
const skillId = target.dataset.skill;
|
||||
if (!skillId) return;
|
||||
|
||||
const { SkillCheckDialog } = game.vagabond.applications;
|
||||
await SkillCheckDialog.prompt(this.actor, skillId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle save roll action.
|
||||
* @param {PointerEvent} event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static async #onRollSave(event, target) {
|
||||
event.preventDefault();
|
||||
const saveType = target.dataset.save;
|
||||
if (!saveType) return;
|
||||
|
||||
const { SaveRollDialog } = game.vagabond.applications;
|
||||
await SaveRollDialog.prompt(this.actor, saveType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle attack roll action.
|
||||
* @param {PointerEvent} event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static async #onRollAttack(event, target) {
|
||||
event.preventDefault();
|
||||
const weaponId = target.dataset.weaponId;
|
||||
|
||||
const { AttackRollDialog } = game.vagabond.applications;
|
||||
await AttackRollDialog.prompt(this.actor, { weaponId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle spell cast action.
|
||||
* @param {PointerEvent} event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static async #onCastSpell(event, target) {
|
||||
event.preventDefault();
|
||||
const spellId = target.dataset.spellId;
|
||||
if (!spellId) return;
|
||||
|
||||
const spell = this.actor.items.get(spellId);
|
||||
if (!spell) return;
|
||||
|
||||
const { SpellCastDialog } = game.vagabond.applications;
|
||||
await SpellCastDialog.prompt(this.actor, { spell });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle item edit action.
|
||||
* @param {PointerEvent} event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static async #onItemEdit(event, target) {
|
||||
event.preventDefault();
|
||||
const itemId = target.closest("[data-item-id]")?.dataset.itemId;
|
||||
if (!itemId) return;
|
||||
|
||||
const item = this.actor.items.get(itemId);
|
||||
if (!item) return;
|
||||
|
||||
item.sheet.render(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle item delete action.
|
||||
* @param {PointerEvent} event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static async #onItemDelete(event, target) {
|
||||
event.preventDefault();
|
||||
const itemId = target.closest("[data-item-id]")?.dataset.itemId;
|
||||
if (!itemId) return;
|
||||
|
||||
const item = this.actor.items.get(itemId);
|
||||
if (!item) return;
|
||||
|
||||
// Confirm deletion
|
||||
const confirmed = await Dialog.confirm({
|
||||
title: game.i18n.format("VAGABOND.ItemDeleteTitle", { name: item.name }),
|
||||
content: game.i18n.format("VAGABOND.ItemDeleteConfirm", { name: item.name }),
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
await item.delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle item create action.
|
||||
* @param {PointerEvent} event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static async #onItemCreate(event, target) {
|
||||
event.preventDefault();
|
||||
const type = target.dataset.type;
|
||||
if (!type) return;
|
||||
|
||||
const itemData = {
|
||||
name: game.i18n.format("VAGABOND.ItemNew", { type }),
|
||||
type,
|
||||
};
|
||||
|
||||
const [item] = await this.actor.createEmbeddedDocuments("Item", [itemData]);
|
||||
item?.sheet.render(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle item equipped toggle.
|
||||
* @param {PointerEvent} event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static async #onItemToggleEquipped(event, target) {
|
||||
event.preventDefault();
|
||||
const itemId = target.closest("[data-item-id]")?.dataset.itemId;
|
||||
if (!itemId) return;
|
||||
|
||||
const item = this.actor.items.get(itemId);
|
||||
if (!item) return;
|
||||
|
||||
await item.update({ "system.equipped": !item.system.equipped });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle tab change action.
|
||||
* @param {PointerEvent} event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static async #onChangeTab(event, target) {
|
||||
event.preventDefault();
|
||||
const tab = target.dataset.tab;
|
||||
if (!tab) return;
|
||||
|
||||
this._activeTab = tab;
|
||||
this.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle resource modification.
|
||||
* @param {PointerEvent} event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static async #onModifyResource(event, target) {
|
||||
event.preventDefault();
|
||||
const resource = target.dataset.resource;
|
||||
const delta = parseInt(target.dataset.delta, 10);
|
||||
if (!resource || isNaN(delta)) return;
|
||||
|
||||
await this.actor.modifyResource(resource, delta);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle skill trained toggle.
|
||||
* @param {PointerEvent} event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static async #onToggleTrained(event, target) {
|
||||
event.preventDefault();
|
||||
const skillId = target.dataset.skill;
|
||||
if (!skillId) return;
|
||||
|
||||
const currentValue = this.actor.system.skills[skillId]?.trained ?? false;
|
||||
await this.actor.update({ [`system.skills.${skillId}.trained`]: !currentValue });
|
||||
}
|
||||
}
|
||||
367
module/sheets/character-sheet.mjs
Normal file
367
module/sheets/character-sheet.mjs
Normal file
@ -0,0 +1,367 @@
|
||||
/**
|
||||
* Character Sheet for Vagabond RPG
|
||||
*
|
||||
* Extended sheet for player characters with:
|
||||
* - Stats section with all six attributes
|
||||
* - Combat section (HP, Armor, Fatigue, Speed)
|
||||
* - Saves section (Reflex, Endure, Will)
|
||||
* - Skills section (12 skills with trained/difficulty)
|
||||
* - Attacks section (weapons and attack skills)
|
||||
* - Inventory tab
|
||||
* - Abilities tab (features, perks, ancestry)
|
||||
* - Magic tab (mana, spells, focus)
|
||||
* - Biography tab
|
||||
*
|
||||
* @extends VagabondActorSheet
|
||||
*/
|
||||
|
||||
import VagabondActorSheet from "./base-actor-sheet.mjs";
|
||||
|
||||
export default class VagabondCharacterSheet extends VagabondActorSheet {
|
||||
/* -------------------------------------------- */
|
||||
/* Static Properties */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = foundry.utils.mergeObject(
|
||||
super.DEFAULT_OPTIONS,
|
||||
{
|
||||
classes: ["vagabond", "sheet", "actor", "character"],
|
||||
position: {
|
||||
width: 750,
|
||||
height: 850,
|
||||
},
|
||||
},
|
||||
{ inplace: false }
|
||||
);
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
header: {
|
||||
template: "systems/vagabond/templates/actor/character-header.hbs",
|
||||
},
|
||||
tabs: {
|
||||
template: "systems/vagabond/templates/actor/parts/tabs.hbs",
|
||||
},
|
||||
main: {
|
||||
template: "systems/vagabond/templates/actor/character-main.hbs",
|
||||
},
|
||||
inventory: {
|
||||
template: "systems/vagabond/templates/actor/character-inventory.hbs",
|
||||
},
|
||||
abilities: {
|
||||
template: "systems/vagabond/templates/actor/character-abilities.hbs",
|
||||
},
|
||||
magic: {
|
||||
template: "systems/vagabond/templates/actor/character-magic.hbs",
|
||||
},
|
||||
biography: {
|
||||
template: "systems/vagabond/templates/actor/character-biography.hbs",
|
||||
},
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Getters */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
get tabs() {
|
||||
return [
|
||||
{ id: "main", label: "VAGABOND.TabMain", icon: "fa-solid fa-user" },
|
||||
{ id: "inventory", label: "VAGABOND.TabInventory", icon: "fa-solid fa-suitcase" },
|
||||
{ id: "abilities", label: "VAGABOND.TabAbilities", icon: "fa-solid fa-star" },
|
||||
{ id: "magic", label: "VAGABOND.TabMagic", icon: "fa-solid fa-wand-sparkles" },
|
||||
{ id: "biography", label: "VAGABOND.TabBiography", icon: "fa-solid fa-book" },
|
||||
];
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Data Preparation */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _prepareTypeContext(context, _options) {
|
||||
// Stats with labels
|
||||
context.stats = this._prepareStats();
|
||||
|
||||
// Skills organized by associated stat
|
||||
context.skills = this._prepareSkills();
|
||||
|
||||
// Saves with calculated difficulties
|
||||
context.saves = this._prepareSaves();
|
||||
|
||||
// Attack skills
|
||||
context.attackSkills = this._prepareAttackSkills();
|
||||
|
||||
// Resources with display data
|
||||
context.resources = this._prepareResources();
|
||||
|
||||
// Speed display
|
||||
context.speed = this._prepareSpeed();
|
||||
|
||||
// Wealth display
|
||||
context.wealth = this.actor.system.wealth;
|
||||
|
||||
// Item slots
|
||||
context.itemSlots = this.actor.system.itemSlots;
|
||||
|
||||
// Character details
|
||||
context.details = this.actor.system.details;
|
||||
context.sizeOptions = CONFIG.VAGABOND?.sizes || {};
|
||||
context.beingTypeOptions = CONFIG.VAGABOND?.beingTypes || {};
|
||||
|
||||
// Focus tracking
|
||||
context.focus = this.actor.system.focus;
|
||||
context.hasFocus = this.actor.system.focus?.active?.length > 0;
|
||||
|
||||
// Class and ancestry info
|
||||
context.ancestry = context.items.ancestry;
|
||||
context.classes = context.items.classes;
|
||||
context.className = context.items.classes[0]?.name || "None";
|
||||
context.ancestryName = context.items.ancestry?.name || "None";
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare stats for display with labels and colors.
|
||||
* @returns {Object}
|
||||
* @private
|
||||
*/
|
||||
_prepareStats() {
|
||||
const system = this.actor.system;
|
||||
const stats = {};
|
||||
|
||||
const statConfig = {
|
||||
might: { label: "VAGABOND.StatMight", abbr: "MIT", color: "stat-might" },
|
||||
dexterity: { label: "VAGABOND.StatDexterity", abbr: "DEX", color: "stat-dexterity" },
|
||||
awareness: { label: "VAGABOND.StatAwareness", abbr: "AWR", color: "stat-awareness" },
|
||||
reason: { label: "VAGABOND.StatReason", abbr: "RSN", color: "stat-reason" },
|
||||
presence: { label: "VAGABOND.StatPresence", abbr: "PRS", color: "stat-presence" },
|
||||
luck: { label: "VAGABOND.StatLuck", abbr: "LUK", color: "stat-luck" },
|
||||
};
|
||||
|
||||
for (const [key, config] of Object.entries(statConfig)) {
|
||||
stats[key] = {
|
||||
...config,
|
||||
value: system.stats[key].value,
|
||||
path: `system.stats.${key}.value`,
|
||||
};
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare skills for display with associated stats and difficulties.
|
||||
* @returns {Object}
|
||||
* @private
|
||||
*/
|
||||
_prepareSkills() {
|
||||
const system = this.actor.system;
|
||||
const skillConfig = CONFIG.VAGABOND?.skills || {};
|
||||
const skills = {};
|
||||
|
||||
for (const [skillId, config] of Object.entries(skillConfig)) {
|
||||
const skillData = system.skills[skillId];
|
||||
if (!skillData) continue;
|
||||
|
||||
skills[skillId] = {
|
||||
id: skillId,
|
||||
label: config.label || skillId,
|
||||
stat: config.stat,
|
||||
statAbbr: this._getStatAbbr(config.stat),
|
||||
trained: skillData.trained,
|
||||
difficulty: skillData.difficulty,
|
||||
critThreshold: skillData.critThreshold,
|
||||
hasCritBonus: skillData.critThreshold < 20,
|
||||
};
|
||||
}
|
||||
|
||||
return skills;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare saves for display.
|
||||
* @returns {Object}
|
||||
* @private
|
||||
*/
|
||||
_prepareSaves() {
|
||||
const system = this.actor.system;
|
||||
|
||||
return {
|
||||
reflex: {
|
||||
id: "reflex",
|
||||
label: "VAGABOND.SaveReflex",
|
||||
stats: "DEX + AWR",
|
||||
difficulty: system.saves.reflex.difficulty,
|
||||
bonus: system.saves.reflex.bonus,
|
||||
},
|
||||
endure: {
|
||||
id: "endure",
|
||||
label: "VAGABOND.SaveEndure",
|
||||
stats: "MIT + MIT",
|
||||
difficulty: system.saves.endure.difficulty,
|
||||
bonus: system.saves.endure.bonus,
|
||||
},
|
||||
will: {
|
||||
id: "will",
|
||||
label: "VAGABOND.SaveWill",
|
||||
stats: "RSN + PRS",
|
||||
difficulty: system.saves.will.difficulty,
|
||||
bonus: system.saves.will.bonus,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare attack skills for display.
|
||||
* @returns {Object}
|
||||
* @private
|
||||
*/
|
||||
_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: "dexterity" },
|
||||
finesse: { label: "VAGABOND.AttackFinesse", stat: "dexterity" },
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
return attacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare resources for display.
|
||||
* @returns {Object}
|
||||
* @private
|
||||
*/
|
||||
_prepareResources() {
|
||||
const system = this.actor.system;
|
||||
|
||||
return {
|
||||
hp: {
|
||||
label: "VAGABOND.ResourceHP",
|
||||
value: system.resources.hp.value,
|
||||
max: system.resources.hp.max,
|
||||
percent: Math.round((system.resources.hp.value / system.resources.hp.max) * 100) || 0,
|
||||
color: this._getResourceColor(system.resources.hp.value, system.resources.hp.max),
|
||||
},
|
||||
mana: {
|
||||
label: "VAGABOND.ResourceMana",
|
||||
value: system.resources.mana.value,
|
||||
max: system.resources.mana.max,
|
||||
castingMax: system.resources.mana.castingMax,
|
||||
percent: Math.round((system.resources.mana.value / system.resources.mana.max) * 100) || 0,
|
||||
},
|
||||
luck: {
|
||||
label: "VAGABOND.ResourceLuck",
|
||||
value: system.resources.luck.value,
|
||||
max: system.resources.luck.max,
|
||||
},
|
||||
fatigue: {
|
||||
label: "VAGABOND.ResourceFatigue",
|
||||
value: system.resources.fatigue.value,
|
||||
max: 5,
|
||||
isDangerous: system.resources.fatigue.value >= 4,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare speed display.
|
||||
* @returns {Object}
|
||||
* @private
|
||||
*/
|
||||
_prepareSpeed() {
|
||||
const system = this.actor.system;
|
||||
|
||||
return {
|
||||
walk: system.speed.walk,
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stat abbreviation.
|
||||
* @param {string} stat
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
_getStatAbbr(stat) {
|
||||
const abbrs = {
|
||||
might: "MIT",
|
||||
dexterity: "DEX",
|
||||
awareness: "AWR",
|
||||
reason: "RSN",
|
||||
presence: "PRS",
|
||||
luck: "LUK",
|
||||
};
|
||||
return abbrs[stat] || stat.toUpperCase().slice(0, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color class for resource bar based on percentage.
|
||||
* @param {number} value
|
||||
* @param {number} max
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
_getResourceColor(value, max) {
|
||||
const percent = (value / max) * 100;
|
||||
if (percent <= 25) return "critical";
|
||||
if (percent <= 50) return "warning";
|
||||
return "healthy";
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Rendering */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _preparePartContext(partId, context, options) {
|
||||
context = await super._preparePartContext(partId, context, options);
|
||||
|
||||
// Only render the active tab's content
|
||||
if (["main", "inventory", "abilities", "magic", "biography"].includes(partId)) {
|
||||
context.isActiveTab = partId === this._activeTab;
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_configureRenderOptions(options) {
|
||||
super._configureRenderOptions(options);
|
||||
|
||||
// Always render header and tabs
|
||||
options.parts = ["header", "tabs"];
|
||||
|
||||
// Add the active tab's part
|
||||
if (this._activeTab && VagabondCharacterSheet.PARTS[this._activeTab]) {
|
||||
options.parts.push(this._activeTab);
|
||||
} else {
|
||||
options.parts.push("main");
|
||||
}
|
||||
}
|
||||
}
|
||||
279
module/sheets/npc-sheet.mjs
Normal file
279
module/sheets/npc-sheet.mjs
Normal file
@ -0,0 +1,279 @@
|
||||
/**
|
||||
* NPC/Monster Sheet for Vagabond RPG
|
||||
*
|
||||
* Compact stat block format sheet for NPCs and monsters:
|
||||
* - Header with HD, HP, TL, Zone
|
||||
* - Combat stats (Armor, Morale, Speed)
|
||||
* - Immunities/Weaknesses/Resistances
|
||||
* - Actions list with attack buttons
|
||||
* - Abilities list
|
||||
* - GM notes
|
||||
*
|
||||
* @extends VagabondActorSheet
|
||||
*/
|
||||
|
||||
import VagabondActorSheet from "./base-actor-sheet.mjs";
|
||||
|
||||
export default class VagabondNPCSheet extends VagabondActorSheet {
|
||||
/* -------------------------------------------- */
|
||||
/* Static Properties */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = foundry.utils.mergeObject(
|
||||
super.DEFAULT_OPTIONS,
|
||||
{
|
||||
classes: ["vagabond", "sheet", "actor", "npc"],
|
||||
position: {
|
||||
width: 520,
|
||||
height: 600,
|
||||
},
|
||||
actions: {
|
||||
...VagabondActorSheet.DEFAULT_OPTIONS.actions,
|
||||
rollMorale: VagabondNPCSheet.#onRollMorale,
|
||||
rollAction: VagabondNPCSheet.#onRollAction,
|
||||
addAction: VagabondNPCSheet.#onAddAction,
|
||||
deleteAction: VagabondNPCSheet.#onDeleteAction,
|
||||
addAbility: VagabondNPCSheet.#onAddAbility,
|
||||
deleteAbility: VagabondNPCSheet.#onDeleteAbility,
|
||||
},
|
||||
},
|
||||
{ inplace: false }
|
||||
);
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
header: {
|
||||
template: "systems/vagabond/templates/actor/npc-header.hbs",
|
||||
},
|
||||
stats: {
|
||||
template: "systems/vagabond/templates/actor/npc-stats.hbs",
|
||||
},
|
||||
actions: {
|
||||
template: "systems/vagabond/templates/actor/npc-actions.hbs",
|
||||
},
|
||||
abilities: {
|
||||
template: "systems/vagabond/templates/actor/npc-abilities.hbs",
|
||||
},
|
||||
notes: {
|
||||
template: "systems/vagabond/templates/actor/npc-notes.hbs",
|
||||
},
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Getters */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
get tabs() {
|
||||
// NPC sheets don't use tabs - all content on one page
|
||||
return [];
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Data Preparation */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _prepareTypeContext(context, _options) {
|
||||
const system = this.actor.system;
|
||||
|
||||
// Core combat stats
|
||||
context.hd = system.hd;
|
||||
context.hp = {
|
||||
value: system.hp.value,
|
||||
max: system.hp.max,
|
||||
percent: Math.round((system.hp.value / system.hp.max) * 100) || 0,
|
||||
isHalf: system.hp.value <= Math.floor(system.hp.max / 2),
|
||||
isDead: system.hp.value <= 0,
|
||||
};
|
||||
context.tl = system.tl;
|
||||
context.armor = system.armor;
|
||||
context.morale = system.morale;
|
||||
context.moraleStatus = system.moraleStatus;
|
||||
|
||||
// Zone with behavior hint
|
||||
context.zone = system.zone;
|
||||
context.zoneBehavior = system.getZoneBehavior?.() || "";
|
||||
context.zoneOptions = {
|
||||
frontline: "VAGABOND.ZoneFrontline",
|
||||
midline: "VAGABOND.ZoneMidline",
|
||||
backline: "VAGABOND.ZoneBackline",
|
||||
};
|
||||
|
||||
// Size and being type
|
||||
context.size = system.size;
|
||||
context.beingType = system.beingType;
|
||||
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,
|
||||
};
|
||||
|
||||
// Senses
|
||||
context.senses = system.senses;
|
||||
context.hasSenses =
|
||||
system.senses.darksight || system.senses.blindsight > 0 || system.senses.tremorsense > 0;
|
||||
|
||||
// Damage modifiers
|
||||
context.immunities = system.immunities || [];
|
||||
context.weaknesses = system.weaknesses || [];
|
||||
context.resistances = system.resistances || [];
|
||||
context.hasDamageModifiers =
|
||||
context.immunities.length > 0 ||
|
||||
context.weaknesses.length > 0 ||
|
||||
context.resistances.length > 0;
|
||||
|
||||
// Actions with index for editing
|
||||
context.actions = (system.actions || []).map((action, index) => ({
|
||||
...action,
|
||||
index,
|
||||
}));
|
||||
context.hasActions = context.actions.length > 0;
|
||||
|
||||
// Abilities with index for editing
|
||||
context.abilities = (system.abilities || []).map((ability, index) => ({
|
||||
...ability,
|
||||
index,
|
||||
}));
|
||||
context.hasAbilities = context.abilities.length > 0;
|
||||
|
||||
// Appearing (encounter numbers)
|
||||
context.appearing = system.appearing;
|
||||
|
||||
// Loot and GM notes
|
||||
context.loot = system.loot;
|
||||
context.gmNotes = system.gmNotes;
|
||||
|
||||
// Damage type options for actions
|
||||
context.damageTypeOptions = CONFIG.VAGABOND?.damageTypes || {};
|
||||
context.attackTypeOptions = {
|
||||
melee: "VAGABOND.AttackMelee",
|
||||
ranged: "VAGABOND.AttackRanged",
|
||||
};
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_configureRenderOptions(options) {
|
||||
super._configureRenderOptions(options);
|
||||
|
||||
// NPC sheets render all parts (no tabs)
|
||||
options.parts = ["header", "stats", "actions", "abilities", "notes"];
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Action Handlers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle morale roll.
|
||||
* @param {PointerEvent} event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static async #onRollMorale(event, _target) {
|
||||
event.preventDefault();
|
||||
await this.actor.rollMorale({ trigger: "manual" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle action roll (attack).
|
||||
* @param {PointerEvent} event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static async #onRollAction(event, target) {
|
||||
event.preventDefault();
|
||||
const actionIndex = parseInt(target.dataset.actionIndex, 10);
|
||||
const action = this.actor.system.actions[actionIndex];
|
||||
if (!action) return;
|
||||
|
||||
// Roll the damage for this action
|
||||
const roll = await new Roll(action.damage).evaluate();
|
||||
|
||||
// Create chat message
|
||||
const content = `
|
||||
<div class="vagabond npc-action">
|
||||
<h3>${action.name}</h3>
|
||||
${action.description ? `<p class="description">${action.description}</p>` : ""}
|
||||
<p class="damage">
|
||||
<strong>Damage:</strong> [[/r ${action.damage}]] ${action.damageType}
|
||||
</p>
|
||||
${action.range ? `<p class="range"><strong>Range:</strong> ${action.range}</p>` : ""}
|
||||
</div>
|
||||
`;
|
||||
|
||||
await ChatMessage.create({
|
||||
speaker: ChatMessage.getSpeaker({ actor: this.actor }),
|
||||
content,
|
||||
rolls: [roll],
|
||||
sound: CONFIG.sounds.dice,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle adding a new action.
|
||||
* @param {PointerEvent} event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static async #onAddAction(event, _target) {
|
||||
event.preventDefault();
|
||||
const actions = [...(this.actor.system.actions || [])];
|
||||
actions.push({
|
||||
name: "New Action",
|
||||
description: "",
|
||||
attackType: "melee",
|
||||
damage: "1d6",
|
||||
damageType: "blunt",
|
||||
range: "",
|
||||
properties: [],
|
||||
});
|
||||
await this.actor.update({ "system.actions": actions });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle deleting an action.
|
||||
* @param {PointerEvent} event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static async #onDeleteAction(event, target) {
|
||||
event.preventDefault();
|
||||
const actionIndex = parseInt(target.dataset.actionIndex, 10);
|
||||
const actions = [...(this.actor.system.actions || [])];
|
||||
actions.splice(actionIndex, 1);
|
||||
await this.actor.update({ "system.actions": actions });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle adding a new ability.
|
||||
* @param {PointerEvent} event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static async #onAddAbility(event, _target) {
|
||||
event.preventDefault();
|
||||
const abilities = [...(this.actor.system.abilities || [])];
|
||||
abilities.push({
|
||||
name: "New Ability",
|
||||
description: "",
|
||||
passive: true,
|
||||
});
|
||||
await this.actor.update({ "system.abilities": abilities });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle deleting an ability.
|
||||
* @param {PointerEvent} event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static async #onDeleteAbility(event, target) {
|
||||
event.preventDefault();
|
||||
const abilityIndex = parseInt(target.dataset.abilityIndex, 10);
|
||||
const abilities = [...(this.actor.system.abilities || [])];
|
||||
abilities.splice(abilityIndex, 1);
|
||||
await this.actor.update({ "system.abilities": abilities });
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,8 @@
|
||||
* @module vagabond
|
||||
*/
|
||||
|
||||
/* global Actors */
|
||||
|
||||
// Import configuration
|
||||
import { VAGABOND } from "./helpers/config.mjs";
|
||||
|
||||
@ -33,11 +35,7 @@ import {
|
||||
} from "./applications/_module.mjs";
|
||||
|
||||
// Import sheet classes
|
||||
// import { VagabondCharacterSheet } from "./sheets/actor-sheet.mjs";
|
||||
// import { VagabondItemSheet } from "./sheets/item-sheet.mjs";
|
||||
|
||||
// Import helper functions
|
||||
// import { preloadHandlebarsTemplates } from "./helpers/templates.mjs";
|
||||
import { VagabondActorSheet, VagabondCharacterSheet, VagabondNPCSheet } from "./sheets/_module.mjs";
|
||||
|
||||
// Import test registration (for Quench)
|
||||
import { registerQuenchTests } from "./tests/quench-init.mjs";
|
||||
@ -46,10 +44,35 @@ import { registerQuenchTests } from "./tests/quench-init.mjs";
|
||||
/* Foundry VTT Initialization */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Preload Handlebars templates.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function preloadHandlebarsTemplates() {
|
||||
const templatePaths = [
|
||||
// Character sheet parts
|
||||
"systems/vagabond/templates/actor/character-header.hbs",
|
||||
"systems/vagabond/templates/actor/character-main.hbs",
|
||||
"systems/vagabond/templates/actor/character-inventory.hbs",
|
||||
"systems/vagabond/templates/actor/character-abilities.hbs",
|
||||
"systems/vagabond/templates/actor/character-magic.hbs",
|
||||
"systems/vagabond/templates/actor/character-biography.hbs",
|
||||
"systems/vagabond/templates/actor/parts/tabs.hbs",
|
||||
// NPC sheet parts
|
||||
"systems/vagabond/templates/actor/npc-header.hbs",
|
||||
"systems/vagabond/templates/actor/npc-stats.hbs",
|
||||
"systems/vagabond/templates/actor/npc-actions.hbs",
|
||||
"systems/vagabond/templates/actor/npc-abilities.hbs",
|
||||
"systems/vagabond/templates/actor/npc-notes.hbs",
|
||||
];
|
||||
|
||||
return loadTemplates(templatePaths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Init hook - runs once when Foundry initializes
|
||||
*/
|
||||
Hooks.once("init", () => {
|
||||
Hooks.once("init", async () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("Vagabond RPG | Initializing Vagabond RPG System");
|
||||
|
||||
@ -66,6 +89,11 @@ Hooks.once("init", () => {
|
||||
SpellCastDialog,
|
||||
FavorHinderDebug,
|
||||
},
|
||||
sheets: {
|
||||
VagabondActorSheet,
|
||||
VagabondCharacterSheet,
|
||||
VagabondNPCSheet,
|
||||
},
|
||||
};
|
||||
|
||||
// Register Actor data models
|
||||
@ -90,22 +118,28 @@ Hooks.once("init", () => {
|
||||
CONFIG.Actor.documentClass = VagabondActor;
|
||||
CONFIG.Item.documentClass = VagabondItem;
|
||||
|
||||
// Register sheet application classes (TODO: Phase 3-4)
|
||||
// Actors.unregisterSheet("core", ActorSheet);
|
||||
// Actors.registerSheet("vagabond", VagabondCharacterSheet, {
|
||||
// types: ["character"],
|
||||
// makeDefault: true,
|
||||
// label: "VAGABOND.SheetCharacter"
|
||||
// });
|
||||
// Register Actor sheet classes
|
||||
Actors.unregisterSheet("core", ActorSheet);
|
||||
Actors.registerSheet("vagabond", VagabondCharacterSheet, {
|
||||
types: ["character"],
|
||||
makeDefault: true,
|
||||
label: "VAGABOND.SheetCharacter",
|
||||
});
|
||||
Actors.registerSheet("vagabond", VagabondNPCSheet, {
|
||||
types: ["npc"],
|
||||
makeDefault: true,
|
||||
label: "VAGABOND.SheetNPC",
|
||||
});
|
||||
|
||||
// Register Item sheet classes (TODO: Phase 4)
|
||||
// Items.unregisterSheet("core", ItemSheet);
|
||||
// Items.registerSheet("vagabond", VagabondItemSheet, {
|
||||
// makeDefault: true,
|
||||
// label: "VAGABOND.SheetItem"
|
||||
// });
|
||||
|
||||
// Preload Handlebars templates (TODO: Phase 3)
|
||||
// return preloadHandlebarsTemplates();
|
||||
// Preload Handlebars templates
|
||||
await preloadHandlebarsTemplates();
|
||||
});
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
@ -100,3 +100,6 @@ $z-fixed: 300;
|
||||
$z-modal-backdrop: 400;
|
||||
$z-modal: 500;
|
||||
$z-tooltip: 600;
|
||||
|
||||
// Breakpoints for responsive layouts
|
||||
$breakpoint-narrow: 700px;
|
||||
|
||||
@ -33,9 +33,11 @@
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
color: $color-text-primary;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $color-parchment-dark;
|
||||
color: $color-accent-primary;
|
||||
}
|
||||
|
||||
i {
|
||||
@ -43,6 +45,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Small icon button (for +/- resource buttons)
|
||||
.btn-icon-sm {
|
||||
@include button-base;
|
||||
@include flex-center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
background-color: $color-parchment;
|
||||
border: 1px solid $color-border;
|
||||
color: $color-text-primary;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $color-parchment-dark;
|
||||
border-color: $color-accent-primary;
|
||||
color: $color-accent-primary;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 10px;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
// Rollable button (for dice rolls)
|
||||
.btn-roll {
|
||||
@include button-base;
|
||||
|
||||
@ -1,44 +1,418 @@
|
||||
// Vagabond RPG - Actor Sheet Styles
|
||||
// ==================================
|
||||
|
||||
// Placeholder - will be expanded in Phase 3
|
||||
// Base actor sheet styles
|
||||
.vagabond.sheet.actor {
|
||||
min-width: 600px;
|
||||
min-height: 500px;
|
||||
|
||||
// ==========================================
|
||||
// HEADER - Three Column Layout
|
||||
// ==========================================
|
||||
.sheet-header {
|
||||
@include flex-between;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
gap: $spacing-4;
|
||||
padding: $spacing-4;
|
||||
background-color: $color-parchment-dark;
|
||||
border-bottom: 2px solid $color-border;
|
||||
|
||||
.profile-img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
border: 2px solid $color-border;
|
||||
border-radius: $radius-md;
|
||||
// Left column - Portrait
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
.profile-img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
border: none; // Remove border, container provides it
|
||||
border-radius: $radius-md;
|
||||
cursor: pointer;
|
||||
transition: opacity $transition-fast;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-fields {
|
||||
// Center column - Name and details
|
||||
.header-center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-2;
|
||||
min-width: 200px;
|
||||
|
||||
.actor-name {
|
||||
margin: 0;
|
||||
|
||||
input {
|
||||
@include input-base;
|
||||
width: 100%;
|
||||
font-family: $font-family-header;
|
||||
font-size: $font-size-xl;
|
||||
font-weight: $font-weight-bold;
|
||||
padding: $spacing-2;
|
||||
}
|
||||
}
|
||||
|
||||
.header-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: $spacing-2;
|
||||
|
||||
.header-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-1;
|
||||
|
||||
label {
|
||||
font-size: $font-size-xs;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $color-text-secondary;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: $font-size-sm;
|
||||
color: $color-text-primary;
|
||||
}
|
||||
|
||||
input {
|
||||
@include input-base;
|
||||
padding: $spacing-1 $spacing-2;
|
||||
font-size: $font-size-sm;
|
||||
|
||||
&[type="number"] {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Right column - Resources
|
||||
.header-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-3;
|
||||
min-width: 200px;
|
||||
max-width: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// RESOURCE BARS (HP, Mana)
|
||||
// ==========================================
|
||||
.resource-bars {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-2;
|
||||
}
|
||||
|
||||
.resource-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-1;
|
||||
|
||||
> label {
|
||||
font-size: $font-size-xs;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $color-text-secondary;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.bar-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-2;
|
||||
}
|
||||
|
||||
// The meter container
|
||||
.bar-container {
|
||||
flex: 1;
|
||||
margin-left: $spacing-4;
|
||||
position: relative;
|
||||
height: 28px;
|
||||
background-color: $color-parchment-darker;
|
||||
border: 1px solid $color-border;
|
||||
border-radius: $radius-md;
|
||||
overflow: hidden;
|
||||
box-shadow: inset 0 0 8px rgba(0, 0, 0, 0.15);
|
||||
|
||||
// The fill bar
|
||||
.bar-fill {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
border-radius: $radius-md 0 0 $radius-md;
|
||||
transition: width $transition-base;
|
||||
}
|
||||
|
||||
// Values overlay - backdrop pill for legibility
|
||||
.bar-values {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $spacing-1;
|
||||
padding: $spacing-1 $spacing-2;
|
||||
background: rgba($color-parchment-light, 0.85);
|
||||
border-radius: $radius-full;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $color-text-primary;
|
||||
|
||||
input {
|
||||
width: 32px;
|
||||
padding: 2px 4px;
|
||||
font-size: $font-size-sm;
|
||||
font-weight: $font-weight-bold;
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid $color-border-light;
|
||||
border-radius: $radius-sm;
|
||||
|
||||
&:focus {
|
||||
background: white;
|
||||
outline: none;
|
||||
border-color: $color-accent-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
color: $color-text-secondary;
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
|
||||
.max {
|
||||
font-size: $font-size-sm;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resource +/- buttons
|
||||
.resource-buttons {
|
||||
display: flex;
|
||||
gap: $spacing-1;
|
||||
|
||||
button {
|
||||
@include button-base;
|
||||
@include flex-center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
background-color: $color-parchment;
|
||||
border: 1px solid $color-border;
|
||||
color: $color-text-primary;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $color-parchment-dark;
|
||||
border-color: $color-accent-primary;
|
||||
color: $color-accent-primary;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 10px;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HP-specific colors
|
||||
&.hp {
|
||||
.bar-fill {
|
||||
background: linear-gradient(to bottom, $color-danger, darken($color-danger, 10%));
|
||||
}
|
||||
|
||||
&.healthy .bar-fill {
|
||||
background: linear-gradient(to bottom, $color-success, darken($color-success, 10%));
|
||||
}
|
||||
|
||||
&.warning .bar-fill {
|
||||
background: linear-gradient(to bottom, $color-warning, darken($color-warning, 10%));
|
||||
}
|
||||
|
||||
&.critical .bar-fill {
|
||||
background: linear-gradient(to bottom, $color-danger, darken($color-danger, 15%));
|
||||
}
|
||||
}
|
||||
|
||||
// Mana-specific colors
|
||||
&.mana {
|
||||
.bar-fill {
|
||||
background: linear-gradient(to bottom, $color-info, darken($color-info, 10%));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// SECONDARY RESOURCES (Luck, Fatigue, etc.)
|
||||
// ==========================================
|
||||
.secondary-resources {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: $spacing-2;
|
||||
}
|
||||
|
||||
.resource {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-1;
|
||||
padding: $spacing-1 $spacing-2;
|
||||
background-color: $color-parchment;
|
||||
border: 1px solid $color-border-light;
|
||||
border-radius: $radius-md;
|
||||
font-size: $font-size-sm;
|
||||
|
||||
label {
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $color-text-secondary;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-weight: $font-weight-bold;
|
||||
color: $color-text-primary;
|
||||
}
|
||||
|
||||
.separator {
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.max {
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: $font-size-xs;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 36px;
|
||||
padding: $spacing-1;
|
||||
font-size: $font-size-sm;
|
||||
font-weight: $font-weight-bold;
|
||||
text-align: center;
|
||||
background: $color-parchment-light;
|
||||
border: 1px solid $color-border-light;
|
||||
border-radius: $radius-sm;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $color-accent-primary;
|
||||
}
|
||||
}
|
||||
|
||||
// Danger state for fatigue
|
||||
&.danger {
|
||||
background-color: rgba($color-danger, 0.1);
|
||||
border-color: $color-danger;
|
||||
|
||||
label,
|
||||
.value {
|
||||
color: $color-danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Character sheet specific
|
||||
// ==========================================
|
||||
// CHARACTER SHEET SPECIFIC
|
||||
// ==========================================
|
||||
.vagabond.sheet.actor.character {
|
||||
// Stats column (left side, matching official sheet)
|
||||
.stats-column {
|
||||
@include flex-column;
|
||||
gap: $spacing-3;
|
||||
// ----------------------------------------
|
||||
// Tab Content Area
|
||||
// ----------------------------------------
|
||||
.tab-content {
|
||||
padding: $spacing-4;
|
||||
background-color: $color-parchment-dark;
|
||||
border-right: 2px solid $color-border;
|
||||
background-color: $color-parchment;
|
||||
overflow-y: auto;
|
||||
@include custom-scrollbar;
|
||||
|
||||
// Enable container queries for responsive layout based on sheet width
|
||||
container-type: inline-size;
|
||||
container-name: sheet-content;
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
// Section Headers
|
||||
// ----------------------------------------
|
||||
.section-header {
|
||||
font-family: $font-family-header;
|
||||
font-size: $font-size-lg;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $color-text-primary;
|
||||
margin: 0 0 $spacing-3 0;
|
||||
padding-bottom: $spacing-2;
|
||||
border-bottom: 2px solid $color-border;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.subsection-header {
|
||||
font-family: $font-family-header;
|
||||
font-size: $font-size-base;
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $color-text-secondary;
|
||||
margin: $spacing-3 0 $spacing-2 0;
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
// Main Tab Layout
|
||||
// ----------------------------------------
|
||||
.main-tab {
|
||||
.main-grid {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
gap: $spacing-4;
|
||||
|
||||
// Responsive: stack vertically on narrow container
|
||||
@container sheet-content (max-width: #{$breakpoint-narrow}) {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto auto auto;
|
||||
|
||||
.stats-column {
|
||||
order: 1;
|
||||
}
|
||||
.center-column {
|
||||
order: 2;
|
||||
}
|
||||
.attacks-column {
|
||||
order: 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
// Stats Column (Left)
|
||||
// ----------------------------------------
|
||||
.stats-column {
|
||||
min-width: 120px;
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: $spacing-2;
|
||||
}
|
||||
|
||||
.stat-block {
|
||||
text-align: center;
|
||||
@include flex-column;
|
||||
align-items: center;
|
||||
padding: $spacing-2;
|
||||
background-color: $color-parchment-light;
|
||||
border: 1px solid $color-border-light;
|
||||
border-radius: $radius-md;
|
||||
|
||||
.stat-label {
|
||||
font-family: $font-family-header;
|
||||
@ -51,14 +425,333 @@
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
@include stat-badge;
|
||||
margin: 0 auto;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
padding: 0;
|
||||
font-family: $font-family-header;
|
||||
font-size: $font-size-2xl;
|
||||
font-weight: $font-weight-bold;
|
||||
text-align: center;
|
||||
color: $color-text-primary;
|
||||
background-color: $color-parchment;
|
||||
border: 2px solid $color-border;
|
||||
border-radius: $radius-md;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $color-accent-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive: horizontal stats on narrow container
|
||||
@container sheet-content (max-width: #{$breakpoint-narrow}) {
|
||||
min-width: 0;
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: $spacing-1;
|
||||
}
|
||||
|
||||
.stat-block {
|
||||
padding: $spacing-1;
|
||||
|
||||
.stat-value {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: $font-size-xl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
// Center Column (Saves & Skills)
|
||||
// ----------------------------------------
|
||||
.center-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-4;
|
||||
}
|
||||
|
||||
// Saves Section
|
||||
.saves-section {
|
||||
.saves-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-2;
|
||||
}
|
||||
|
||||
.save-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-2;
|
||||
padding: $spacing-2 $spacing-3;
|
||||
background-color: $color-parchment-light;
|
||||
border: 1px solid $color-border-light;
|
||||
border-radius: $radius-md;
|
||||
cursor: pointer;
|
||||
transition: all $transition-fast;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-parchment-dark;
|
||||
border-color: $color-accent-primary;
|
||||
|
||||
.roll-icon {
|
||||
color: $color-accent-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.save-label {
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $color-text-primary;
|
||||
}
|
||||
|
||||
.save-stats {
|
||||
font-size: $font-size-sm;
|
||||
color: $color-text-secondary;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.save-difficulty {
|
||||
font-family: $font-family-header;
|
||||
font-size: $font-size-lg;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $color-text-primary;
|
||||
min-width: 28px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.roll-icon {
|
||||
color: $color-text-muted;
|
||||
transition: color $transition-fast;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skills Section
|
||||
.skills-section {
|
||||
.skills-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: $spacing-2;
|
||||
}
|
||||
|
||||
.skill-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-2;
|
||||
padding: $spacing-1 $spacing-2;
|
||||
background-color: $color-parchment-light;
|
||||
border: 1px solid $color-border-light;
|
||||
border-radius: $radius-md;
|
||||
transition: all $transition-fast;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-parchment-dark;
|
||||
border-color: $color-border;
|
||||
}
|
||||
|
||||
&.trained {
|
||||
background-color: rgba($color-success, 0.1);
|
||||
border-color: rgba($color-success, 0.3);
|
||||
|
||||
.skill-trained-toggle {
|
||||
color: $color-success;
|
||||
border-color: $color-success;
|
||||
}
|
||||
}
|
||||
|
||||
.skill-trained-toggle {
|
||||
@include flex-center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 1px solid $color-border;
|
||||
border-radius: $radius-sm;
|
||||
color: $color-text-muted;
|
||||
cursor: pointer;
|
||||
transition: all $transition-fast;
|
||||
|
||||
&:hover {
|
||||
border-color: $color-accent-primary;
|
||||
color: $color-accent-primary;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.skill-name {
|
||||
font-weight: $font-weight-medium;
|
||||
color: $color-text-primary;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.skill-stat {
|
||||
font-size: $font-size-xs;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.skill-difficulty {
|
||||
font-family: $font-family-header;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $color-text-primary;
|
||||
min-width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.skill-crit {
|
||||
font-size: $font-size-xs;
|
||||
color: $color-warning;
|
||||
|
||||
i {
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.skill-roll-btn {
|
||||
@include flex-center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: $color-text-muted;
|
||||
cursor: pointer;
|
||||
transition: color $transition-fast;
|
||||
|
||||
&:hover {
|
||||
color: $color-accent-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
// Attacks Column (Right)
|
||||
// ----------------------------------------
|
||||
.attacks-column {
|
||||
min-width: 180px;
|
||||
|
||||
.attack-skills-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-2;
|
||||
}
|
||||
|
||||
// Responsive: horizontal attacks on narrow container
|
||||
@container sheet-content (max-width: #{$breakpoint-narrow}) {
|
||||
min-width: 0;
|
||||
|
||||
.attack-skills-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: $spacing-2;
|
||||
}
|
||||
}
|
||||
|
||||
.attack-skill-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-2;
|
||||
padding: $spacing-2 $spacing-3;
|
||||
background-color: $color-parchment-light;
|
||||
border: 1px solid $color-border-light;
|
||||
border-radius: $radius-md;
|
||||
cursor: pointer;
|
||||
transition: all $transition-fast;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-parchment-dark;
|
||||
border-color: $color-accent-primary;
|
||||
}
|
||||
|
||||
.attack-name {
|
||||
font-weight: $font-weight-semibold;
|
||||
color: $color-text-primary;
|
||||
}
|
||||
|
||||
.attack-stat {
|
||||
font-size: $font-size-xs;
|
||||
color: $color-text-secondary;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.attack-difficulty {
|
||||
font-family: $font-family-header;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $color-text-primary;
|
||||
}
|
||||
|
||||
.attack-crit {
|
||||
font-size: $font-size-xs;
|
||||
color: $color-warning;
|
||||
}
|
||||
}
|
||||
|
||||
// Equipped Weapons List
|
||||
.weapon-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.weapon-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-2;
|
||||
padding: $spacing-2;
|
||||
border-bottom: 1px solid $color-border-light;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.weapon-name {
|
||||
flex: 1;
|
||||
font-weight: $font-weight-medium;
|
||||
color: $color-text-primary;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: $color-accent-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.weapon-damage {
|
||||
font-family: $font-family-mono;
|
||||
font-size: $font-size-sm;
|
||||
color: $color-danger;
|
||||
}
|
||||
|
||||
.weapon-roll {
|
||||
@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-accent-highlight;
|
||||
border-color: $color-accent-primary;
|
||||
color: $color-accent-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NPC/Monster sheet specific
|
||||
// ==========================================
|
||||
// NPC/MONSTER SHEET SPECIFIC
|
||||
// ==========================================
|
||||
.vagabond.sheet.actor.npc {
|
||||
min-width: 400px;
|
||||
|
||||
|
||||
121
templates/actor/character-abilities.hbs
Normal file
121
templates/actor/character-abilities.hbs
Normal file
@ -0,0 +1,121 @@
|
||||
{{!-- Character Sheet Abilities Tab --}}
|
||||
<section class="sheet-body tab-content abilities-tab">
|
||||
{{!-- Ancestry Section --}}
|
||||
{{#if ancestry}}
|
||||
<div class="abilities-section ancestry">
|
||||
<h2 class="section-header">{{localize "VAGABOND.Ancestry"}}</h2>
|
||||
<div class="ancestry-block" data-item-id="{{ancestry.id}}">
|
||||
<div class="ancestry-header">
|
||||
<img class="ancestry-img" src="{{ancestry.img}}" alt="{{ancestry.name}}" />
|
||||
<div class="ancestry-info">
|
||||
<span class="ancestry-name" data-action="itemEdit">{{ancestry.name}}</span>
|
||||
<span class="ancestry-being-type">{{ancestry.system.beingType}}</span>
|
||||
<span class="ancestry-size">{{ancestry.system.size}}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{#if ancestry.system.traits.length}}
|
||||
<div class="ancestry-traits">
|
||||
{{#each ancestry.system.traits}}
|
||||
<div class="trait">
|
||||
<strong>{{this.name}}:</strong> {{{this.description}}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{!-- Class Features Section --}}
|
||||
<div class="abilities-section features">
|
||||
<h2 class="section-header">{{localize "VAGABOND.Features"}}</h2>
|
||||
<ul class="ability-list">
|
||||
{{#each items.features}}
|
||||
<li class="ability-item" data-item-id="{{this.id}}">
|
||||
<img class="ability-img" src="{{this.img}}" alt="{{this.name}}" />
|
||||
<div class="ability-info">
|
||||
<span class="ability-name" data-action="itemEdit">{{this.name}}</span>
|
||||
{{#if this.system.sourceClass}}
|
||||
<span class="ability-source">{{this.system.sourceClass}} Lv{{this.system.levelGained}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="ability-description">{{{this.system.description}}}</div>
|
||||
<button type="button" class="item-delete" data-action="itemDelete"
|
||||
data-tooltip="{{localize 'VAGABOND.DeleteItem'}}">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</li>
|
||||
{{else}}
|
||||
<li class="ability-item empty">{{localize "VAGABOND.NoFeatures"}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{{!-- Perks Section --}}
|
||||
<div class="abilities-section perks">
|
||||
<div class="section-header-row">
|
||||
<h2 class="section-header">{{localize "VAGABOND.Perks"}}</h2>
|
||||
<button type="button" class="item-create" data-action="itemCreate" data-type="perk"
|
||||
data-tooltip="{{localize 'VAGABOND.CreatePerk'}}">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
<ul class="ability-list">
|
||||
{{#each items.perks}}
|
||||
<li class="ability-item" data-item-id="{{this.id}}">
|
||||
<img class="ability-img" src="{{this.img}}" alt="{{this.name}}" />
|
||||
<div class="ability-info">
|
||||
<span class="ability-name" data-action="itemEdit">{{this.name}}</span>
|
||||
{{#if this.system.prerequisites}}
|
||||
<span class="ability-prereqs">{{this.system.prerequisites}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="ability-description">{{{this.system.description}}}</div>
|
||||
<button type="button" class="item-delete" data-action="itemDelete"
|
||||
data-tooltip="{{localize 'VAGABOND.DeleteItem'}}">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</li>
|
||||
{{else}}
|
||||
<li class="ability-item empty">{{localize "VAGABOND.NoPerks"}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{{!-- Active Effects Section --}}
|
||||
<div class="abilities-section effects">
|
||||
<h2 class="section-header">{{localize "VAGABOND.ActiveEffects"}}</h2>
|
||||
|
||||
{{#if effects.temporary.length}}
|
||||
<div class="effects-group temporary">
|
||||
<h3 class="subsection-header">{{localize "VAGABOND.TemporaryEffects"}}</h3>
|
||||
<ul class="effect-list">
|
||||
{{#each effects.temporary}}
|
||||
<li class="effect-item">
|
||||
<img class="effect-icon" src="{{this.icon}}" alt="{{this.name}}" />
|
||||
<span class="effect-name">{{this.name}}</span>
|
||||
{{#if this.duration.remaining}}
|
||||
<span class="effect-duration">{{this.duration.remaining}} rounds</span>
|
||||
{{/if}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if effects.passive.length}}
|
||||
<div class="effects-group passive">
|
||||
<h3 class="subsection-header">{{localize "VAGABOND.PassiveEffects"}}</h3>
|
||||
<ul class="effect-list">
|
||||
{{#each effects.passive}}
|
||||
<li class="effect-item">
|
||||
<img class="effect-icon" src="{{this.icon}}" alt="{{this.name}}" />
|
||||
<span class="effect-name">{{this.name}}</span>
|
||||
<span class="effect-source">{{this.parent.name}}</span>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</section>
|
||||
86
templates/actor/character-biography.hbs
Normal file
86
templates/actor/character-biography.hbs
Normal file
@ -0,0 +1,86 @@
|
||||
{{!-- Character Sheet Biography Tab --}}
|
||||
<section class="sheet-body tab-content biography-tab">
|
||||
{{!-- Character Details --}}
|
||||
<div class="biography-section details">
|
||||
<h2 class="section-header">{{localize "VAGABOND.CharacterDetails"}}</h2>
|
||||
<div class="details-grid">
|
||||
<div class="detail-field">
|
||||
<label>{{localize "VAGABOND.Size"}}</label>
|
||||
<select name="system.details.size">
|
||||
{{#each sizeOptions}}
|
||||
<option value="{{@key}}" {{#if (eq @key ../details.size)}}selected{{/if}}>
|
||||
{{localize this}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="detail-field">
|
||||
<label>{{localize "VAGABOND.BeingType"}}</label>
|
||||
<select name="system.details.beingType">
|
||||
{{#each beingTypeOptions}}
|
||||
<option value="{{@key}}" {{#if (eq @key ../details.beingType)}}selected{{/if}}>
|
||||
{{localize this}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Languages --}}
|
||||
<div class="biography-section languages">
|
||||
<h2 class="section-header">{{localize "VAGABOND.Languages"}}</h2>
|
||||
<div class="languages-list">
|
||||
{{#each system.languages}}
|
||||
<span class="language-tag">{{this}}</span>
|
||||
{{else}}
|
||||
<span class="no-languages">{{localize "VAGABOND.NoLanguages"}}</span>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Senses --}}
|
||||
<div class="biography-section senses">
|
||||
<h2 class="section-header">{{localize "VAGABOND.Senses"}}</h2>
|
||||
<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"}}
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Biography Text --}}
|
||||
<div class="biography-section biography-text">
|
||||
<h2 class="section-header">{{localize "VAGABOND.Biography"}}</h2>
|
||||
<div class="editor-container">
|
||||
<textarea class="biography-editor" name="system.biography"
|
||||
placeholder="{{localize 'VAGABOND.BiographyPlaceholder'}}">{{system.biography}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Notes --}}
|
||||
<div class="biography-section notes">
|
||||
<h2 class="section-header">{{localize "VAGABOND.Notes"}}</h2>
|
||||
<div class="editor-container">
|
||||
<textarea class="notes-editor" name="system.notes"
|
||||
placeholder="{{localize 'VAGABOND.NotesPlaceholder'}}">{{system.notes}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
118
templates/actor/character-header.hbs
Normal file
118
templates/actor/character-header.hbs
Normal file
@ -0,0 +1,118 @@
|
||||
{{!-- Character Sheet Header --}}
|
||||
<header class="sheet-header">
|
||||
<div class="header-left">
|
||||
<img class="profile-img" src="{{actor.img}}" alt="{{actor.name}}"
|
||||
data-edit="img" data-tooltip="VAGABOND.ChangePortrait" />
|
||||
</div>
|
||||
|
||||
<div class="header-center">
|
||||
<h1 class="actor-name">
|
||||
<input type="text" name="name" value="{{actor.name}}" placeholder="{{localize 'VAGABOND.CharacterName'}}" />
|
||||
</h1>
|
||||
|
||||
<div class="header-fields">
|
||||
<div class="header-field">
|
||||
<label>{{localize "VAGABOND.Ancestry"}}</label>
|
||||
<span class="ancestry-name">{{ancestryName}}</span>
|
||||
</div>
|
||||
|
||||
<div class="header-field">
|
||||
<label>{{localize "VAGABOND.Class"}}</label>
|
||||
<span class="class-name">{{className}}</span>
|
||||
</div>
|
||||
|
||||
<div class="header-field">
|
||||
<label>{{localize "VAGABOND.Level"}}</label>
|
||||
<input type="number" name="system.level" value="{{system.level}}" min="1" max="10" />
|
||||
</div>
|
||||
|
||||
<div class="header-field">
|
||||
<label>{{localize "VAGABOND.XP"}}</label>
|
||||
<input type="number" name="system.xp" value="{{system.xp}}" min="0" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<div class="resource-bars">
|
||||
{{!-- HP Bar --}}
|
||||
<div class="resource-bar hp {{resources.hp.color}}">
|
||||
<label>{{localize resources.hp.label}}</label>
|
||||
<div class="bar-row">
|
||||
<div class="bar-container">
|
||||
<div class="bar-fill" style="width: {{resources.hp.percent}}%"></div>
|
||||
<div class="bar-values">
|
||||
<input type="number" name="system.resources.hp.value" value="{{resources.hp.value}}" min="0" />
|
||||
<span class="separator">/</span>
|
||||
<span class="max">{{resources.hp.max}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-buttons">
|
||||
<button type="button" data-action="modifyResource" data-resource="hp" data-delta="-1"
|
||||
data-tooltip="{{localize 'VAGABOND.Damage'}}">
|
||||
<i class="fa-solid fa-minus"></i>
|
||||
</button>
|
||||
<button type="button" data-action="modifyResource" data-resource="hp" data-delta="1"
|
||||
data-tooltip="{{localize 'VAGABOND.Heal'}}">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Mana Bar --}}
|
||||
{{#if resources.mana.max}}
|
||||
<div class="resource-bar mana">
|
||||
<label>{{localize resources.mana.label}}</label>
|
||||
<div class="bar-row">
|
||||
<div class="bar-container">
|
||||
<div class="bar-fill" style="width: {{resources.mana.percent}}%"></div>
|
||||
<div class="bar-values">
|
||||
<input type="number" name="system.resources.mana.value" value="{{resources.mana.value}}" min="0" />
|
||||
<span class="separator">/</span>
|
||||
<span class="max">{{resources.mana.max}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-buttons">
|
||||
<button type="button" data-action="modifyResource" data-resource="mana" data-delta="-1">
|
||||
<i class="fa-solid fa-minus"></i>
|
||||
</button>
|
||||
<button type="button" data-action="modifyResource" data-resource="mana" data-delta="1">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="secondary-resources">
|
||||
<div class="resource luck">
|
||||
<label>{{localize resources.luck.label}}</label>
|
||||
<input type="number" name="system.resources.luck.value"
|
||||
value="{{resources.luck.value}}" min="0" max="{{resources.luck.max}}" />
|
||||
<span class="separator">/</span>
|
||||
<span class="max">{{resources.luck.max}}</span>
|
||||
</div>
|
||||
|
||||
<div class="resource fatigue {{#if resources.fatigue.isDangerous}}danger{{/if}}">
|
||||
<label>{{localize resources.fatigue.label}}</label>
|
||||
<input type="number" name="system.resources.fatigue.value"
|
||||
value="{{resources.fatigue.value}}" min="0" max="5" />
|
||||
<span class="separator">/</span>
|
||||
<span class="max">5</span>
|
||||
</div>
|
||||
|
||||
<div class="resource armor">
|
||||
<label>{{localize "VAGABOND.Armor"}}</label>
|
||||
<span class="value">{{system.armor}}</span>
|
||||
</div>
|
||||
|
||||
<div class="resource speed">
|
||||
<label>{{localize "VAGABOND.Speed"}}</label>
|
||||
<span class="value">{{speed.walk}}</span>
|
||||
<span class="unit">ft</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
122
templates/actor/character-inventory.hbs
Normal file
122
templates/actor/character-inventory.hbs
Normal file
@ -0,0 +1,122 @@
|
||||
{{!-- Character Sheet Inventory Tab --}}
|
||||
<section class="sheet-body tab-content inventory-tab">
|
||||
{{!-- Item Slots Header --}}
|
||||
<div class="inventory-header">
|
||||
<div class="item-slots {{#if itemSlots.overburdened}}overburdened{{/if}}">
|
||||
<span class="slots-label">{{localize "VAGABOND.ItemSlots"}}:</span>
|
||||
<span class="slots-used">{{itemSlots.used}}</span>
|
||||
<span class="slots-separator">/</span>
|
||||
<span class="slots-max">{{itemSlots.max}}</span>
|
||||
{{#if itemSlots.overburdened}}
|
||||
<span class="overburdened-warning">
|
||||
<i class="fa-solid fa-exclamation-triangle"></i>
|
||||
{{localize "VAGABOND.Overburdened"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="wealth">
|
||||
<div class="currency gold">
|
||||
<input type="number" name="system.wealth.gold" value="{{wealth.gold}}" min="0" />
|
||||
<label>{{localize "VAGABOND.Gold"}}</label>
|
||||
</div>
|
||||
<div class="currency silver">
|
||||
<input type="number" name="system.wealth.silver" value="{{wealth.silver}}" min="0" />
|
||||
<label>{{localize "VAGABOND.Silver"}}</label>
|
||||
</div>
|
||||
<div class="currency copper">
|
||||
<input type="number" name="system.wealth.copper" value="{{wealth.copper}}" min="0" />
|
||||
<label>{{localize "VAGABOND.Copper"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Weapons Section --}}
|
||||
<div class="inventory-section weapons">
|
||||
<div class="section-header-row">
|
||||
<h2 class="section-header">{{localize "VAGABOND.Weapons"}}</h2>
|
||||
<button type="button" class="item-create" data-action="itemCreate" data-type="weapon"
|
||||
data-tooltip="{{localize 'VAGABOND.CreateWeapon'}}">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
<ul class="item-list">
|
||||
{{#each items.weapons}}
|
||||
<li class="item-row" data-item-id="{{this.id}}">
|
||||
<img class="item-img" src="{{this.img}}" alt="{{this.name}}" />
|
||||
<span class="item-name" data-action="itemEdit">{{this.name}}</span>
|
||||
<span class="item-damage">{{this.system.damage}}</span>
|
||||
<span class="item-slots">{{this.system.slots}} slot{{#if (gt this.system.slots 1)}}s{{/if}}</span>
|
||||
<button type="button" class="item-equipped {{#if this.system.equipped}}active{{/if}}"
|
||||
data-action="itemToggleEquipped" data-tooltip="{{localize 'VAGABOND.ToggleEquipped'}}">
|
||||
<i class="fa-solid fa-shield-halved"></i>
|
||||
</button>
|
||||
<button type="button" class="item-delete" data-action="itemDelete"
|
||||
data-tooltip="{{localize 'VAGABOND.DeleteItem'}}">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</li>
|
||||
{{else}}
|
||||
<li class="item-row empty">{{localize "VAGABOND.NoWeapons"}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{{!-- Armor Section --}}
|
||||
<div class="inventory-section armor">
|
||||
<div class="section-header-row">
|
||||
<h2 class="section-header">{{localize "VAGABOND.Armor"}}</h2>
|
||||
<button type="button" class="item-create" data-action="itemCreate" data-type="armor"
|
||||
data-tooltip="{{localize 'VAGABOND.CreateArmor'}}">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
<ul class="item-list">
|
||||
{{#each items.armor}}
|
||||
<li class="item-row" data-item-id="{{this.id}}">
|
||||
<img class="item-img" src="{{this.img}}" alt="{{this.name}}" />
|
||||
<span class="item-name" data-action="itemEdit">{{this.name}}</span>
|
||||
<span class="item-armor-value">+{{this.system.armorValue}} Armor</span>
|
||||
<span class="item-slots">{{this.system.slots}} slot{{#if (gt this.system.slots 1)}}s{{/if}}</span>
|
||||
<button type="button" class="item-equipped {{#if this.system.equipped}}active{{/if}}"
|
||||
data-action="itemToggleEquipped" data-tooltip="{{localize 'VAGABOND.ToggleEquipped'}}">
|
||||
<i class="fa-solid fa-shield-halved"></i>
|
||||
</button>
|
||||
<button type="button" class="item-delete" data-action="itemDelete"
|
||||
data-tooltip="{{localize 'VAGABOND.DeleteItem'}}">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</li>
|
||||
{{else}}
|
||||
<li class="item-row empty">{{localize "VAGABOND.NoArmor"}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{{!-- Equipment Section --}}
|
||||
<div class="inventory-section equipment">
|
||||
<div class="section-header-row">
|
||||
<h2 class="section-header">{{localize "VAGABOND.Equipment"}}</h2>
|
||||
<button type="button" class="item-create" data-action="itemCreate" data-type="equipment"
|
||||
data-tooltip="{{localize 'VAGABOND.CreateEquipment'}}">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
<ul class="item-list">
|
||||
{{#each items.equipment}}
|
||||
<li class="item-row" data-item-id="{{this.id}}">
|
||||
<img class="item-img" src="{{this.img}}" alt="{{this.name}}" />
|
||||
<span class="item-name" data-action="itemEdit">{{this.name}}</span>
|
||||
<span class="item-quantity">x{{this.system.quantity}}</span>
|
||||
<span class="item-slots">{{this.system.slots}} slot{{#if (gt this.system.slots 1)}}s{{/if}}</span>
|
||||
<button type="button" class="item-delete" data-action="itemDelete"
|
||||
data-tooltip="{{localize 'VAGABOND.DeleteItem'}}">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</li>
|
||||
{{else}}
|
||||
<li class="item-row empty">{{localize "VAGABOND.NoEquipment"}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
114
templates/actor/character-magic.hbs
Normal file
114
templates/actor/character-magic.hbs
Normal file
@ -0,0 +1,114 @@
|
||||
{{!-- Character Sheet Magic Tab --}}
|
||||
<section class="sheet-body tab-content magic-tab">
|
||||
{{!-- Mana Display --}}
|
||||
<div class="magic-header">
|
||||
<div class="mana-display">
|
||||
<label>{{localize "VAGABOND.Mana"}}</label>
|
||||
<div class="mana-values">
|
||||
<input type="number" name="system.resources.mana.value"
|
||||
value="{{resources.mana.value}}" min="0" max="{{resources.mana.max}}" />
|
||||
<span class="separator">/</span>
|
||||
<span class="max">{{resources.mana.max}}</span>
|
||||
</div>
|
||||
{{#if resources.mana.castingMax}}
|
||||
<div class="casting-max">
|
||||
<span class="label">{{localize "VAGABOND.CastingMax"}}:</span>
|
||||
<span class="value">{{resources.mana.castingMax}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{!-- Focus Status --}}
|
||||
{{#if hasFocus}}
|
||||
<div class="focus-display">
|
||||
<h3>{{localize "VAGABOND.FocusActive"}}</h3>
|
||||
<ul class="focus-list">
|
||||
{{#each focus.active}}
|
||||
<li class="focus-item">
|
||||
<span class="focus-spell">{{this.spellName}}</span>
|
||||
{{#if this.target}}
|
||||
<span class="focus-target">on {{this.target}}</span>
|
||||
{{/if}}
|
||||
{{#if this.manaCostPerRound}}
|
||||
<span class="focus-cost">{{this.manaCostPerRound}} Mana/round</span>
|
||||
{{/if}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{!-- Known Spells Section --}}
|
||||
<div class="spells-section">
|
||||
<div class="section-header-row">
|
||||
<h2 class="section-header">{{localize "VAGABOND.KnownSpells"}}</h2>
|
||||
<button type="button" class="item-create" data-action="itemCreate" data-type="spell"
|
||||
data-tooltip="{{localize 'VAGABOND.CreateSpell'}}">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul class="spell-list">
|
||||
{{#each items.spells}}
|
||||
<li class="spell-item" data-item-id="{{this.id}}">
|
||||
<img class="spell-img" src="{{this.img}}" alt="{{this.name}}" />
|
||||
<div class="spell-info">
|
||||
<span class="spell-name" data-action="itemEdit">{{this.name}}</span>
|
||||
<span class="spell-type">{{this.system.damageType}}</span>
|
||||
</div>
|
||||
<div class="spell-details">
|
||||
{{#if this.system.baseDamage}}
|
||||
<span class="spell-damage">
|
||||
<i class="fa-solid fa-burst"></i>
|
||||
{{this.system.baseDamage}}
|
||||
</span>
|
||||
{{/if}}
|
||||
{{#if this.system.baseEffect}}
|
||||
<span class="spell-effect" data-tooltip="{{this.system.baseEffect}}">
|
||||
<i class="fa-solid fa-wand-sparkles"></i>
|
||||
Effect
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="spell-actions">
|
||||
<button type="button" class="spell-cast" data-action="castSpell" data-spell-id="{{this.id}}"
|
||||
data-tooltip="{{localize 'VAGABOND.CastSpell'}}">
|
||||
<i class="fa-solid fa-magic"></i>
|
||||
{{localize "VAGABOND.Cast"}}
|
||||
</button>
|
||||
<button type="button" class="item-delete" data-action="itemDelete"
|
||||
data-tooltip="{{localize 'VAGABOND.DeleteItem'}}">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
{{else}}
|
||||
<li class="spell-item empty">{{localize "VAGABOND.NoSpells"}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{{!-- Spell Casting Reference --}}
|
||||
<div class="casting-reference">
|
||||
<h3>{{localize "VAGABOND.SpellcastingReference"}}</h3>
|
||||
<div class="reference-grid">
|
||||
<div class="reference-section delivery">
|
||||
<h4>{{localize "VAGABOND.Delivery"}}</h4>
|
||||
<ul>
|
||||
<li><strong>Touch/Remote/Imbue:</strong> 0 Mana</li>
|
||||
<li><strong>Cube:</strong> +1 Mana</li>
|
||||
<li><strong>Aura/Cone/Glyph/Line/Sphere:</strong> +2 Mana</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="reference-section duration">
|
||||
<h4>{{localize "VAGABOND.Duration"}}</h4>
|
||||
<ul>
|
||||
<li><strong>Instant:</strong> Immediate effect</li>
|
||||
<li><strong>Focus:</strong> Maintained (1 Mana/round vs unwilling)</li>
|
||||
<li><strong>Continual:</strong> Lasts until conditions met</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
110
templates/actor/character-main.hbs
Normal file
110
templates/actor/character-main.hbs
Normal file
@ -0,0 +1,110 @@
|
||||
{{!-- Character Sheet Main Tab --}}
|
||||
<section class="sheet-body tab-content main-tab">
|
||||
<div class="main-grid">
|
||||
{{!-- Left Column: Stats --}}
|
||||
<div class="stats-column">
|
||||
<h2 class="section-header">{{localize "VAGABOND.Stats"}}</h2>
|
||||
<div class="stats-grid">
|
||||
{{#each stats}}
|
||||
<div class="stat-block {{this.color}}">
|
||||
<label class="stat-label" data-tooltip="{{localize this.label}}">{{this.abbr}}</label>
|
||||
<input type="number" class="stat-value" name="{{this.path}}"
|
||||
value="{{this.value}}" min="1" max="10" />
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Center Column: Saves & Skills --}}
|
||||
<div class="center-column">
|
||||
{{!-- Saves Section --}}
|
||||
<div class="saves-section">
|
||||
<h2 class="section-header">{{localize "VAGABOND.Saves"}}</h2>
|
||||
<div class="saves-list">
|
||||
{{#each saves}}
|
||||
<div class="save-row" data-action="rollSave" data-save="{{this.id}}">
|
||||
<span class="save-label">{{localize this.label}}</span>
|
||||
<span class="save-stats">({{this.stats}})</span>
|
||||
<span class="save-difficulty">{{this.difficulty}}</span>
|
||||
<i class="fa-solid fa-dice-d20 roll-icon"></i>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Skills Section --}}
|
||||
<div class="skills-section">
|
||||
<h2 class="section-header">{{localize "VAGABOND.Skills"}}</h2>
|
||||
<div class="skills-grid">
|
||||
{{#each skills}}
|
||||
<div class="skill-row {{#if this.trained}}trained{{/if}}" data-skill="{{this.id}}">
|
||||
<button type="button" class="skill-trained-toggle"
|
||||
data-action="toggleTrained" data-skill="{{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="skill-name">{{localize this.label}}</span>
|
||||
<span class="skill-stat">({{this.statAbbr}})</span>
|
||||
<span class="skill-difficulty">{{this.difficulty}}</span>
|
||||
{{#if this.hasCritBonus}}
|
||||
<span class="skill-crit" data-tooltip="{{localize 'VAGABOND.CritThreshold'}}">
|
||||
<i class="fa-solid fa-star"></i>{{this.critThreshold}}
|
||||
</span>
|
||||
{{/if}}
|
||||
<button type="button" class="skill-roll-btn"
|
||||
data-action="rollSkill" data-skill="{{this.id}}"
|
||||
data-tooltip="{{localize 'VAGABOND.RollSkill'}}">
|
||||
<i class="fa-solid fa-dice-d20"></i>
|
||||
</button>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Right Column: Attacks & Weapons --}}
|
||||
<div class="attacks-column">
|
||||
{{!-- Attack Skills --}}
|
||||
<div class="attack-skills-section">
|
||||
<h2 class="section-header">{{localize "VAGABOND.Attacks"}}</h2>
|
||||
<div class="attack-skills-grid">
|
||||
{{#each attackSkills}}
|
||||
<div class="attack-skill-row" data-action="rollAttack" data-attack-skill="{{this.id}}">
|
||||
<span class="attack-name">{{localize this.label}}</span>
|
||||
<span class="attack-stat">({{this.statAbbr}})</span>
|
||||
<span class="attack-difficulty">{{this.difficulty}}</span>
|
||||
{{#if this.hasCritBonus}}
|
||||
<span class="attack-crit">
|
||||
<i class="fa-solid fa-star"></i>{{this.critThreshold}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Equipped Weapons --}}
|
||||
<div class="equipped-weapons-section">
|
||||
<h3 class="subsection-header">{{localize "VAGABOND.EquippedWeapons"}}</h3>
|
||||
<ul class="weapon-list">
|
||||
{{#each items.weapons}}
|
||||
{{#if this.system.equipped}}
|
||||
<li class="weapon-item" data-item-id="{{this.id}}">
|
||||
<span class="weapon-name" data-action="itemEdit">{{this.name}}</span>
|
||||
<span class="weapon-damage">{{this.system.damage}}</span>
|
||||
<button type="button" class="weapon-roll" data-action="rollAttack"
|
||||
data-weapon-id="{{this.id}}" data-tooltip="{{localize 'VAGABOND.RollAttack'}}">
|
||||
<i class="fa-solid fa-dice-d20"></i>
|
||||
</button>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
45
templates/actor/npc-abilities.hbs
Normal file
45
templates/actor/npc-abilities.hbs
Normal file
@ -0,0 +1,45 @@
|
||||
{{!-- NPC Sheet Abilities Section --}}
|
||||
<section class="npc-abilities">
|
||||
<div class="section-header-row">
|
||||
<h2 class="section-header">{{localize "VAGABOND.Abilities"}}</h2>
|
||||
<button type="button" class="ability-add" data-action="addAbility"
|
||||
data-tooltip="{{localize 'VAGABOND.AddAbility'}}">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul class="ability-list">
|
||||
{{#each abilities}}
|
||||
<li class="ability-item" data-ability-index="{{this.index}}">
|
||||
<div class="ability-header">
|
||||
<input type="text" class="ability-name" name="system.abilities.{{this.index}}.name"
|
||||
value="{{this.name}}" placeholder="{{localize 'VAGABOND.AbilityName'}}" />
|
||||
<div class="ability-controls">
|
||||
<label class="ability-passive">
|
||||
<input type="checkbox" name="system.abilities.{{this.index}}.passive"
|
||||
{{#if this.passive}}checked{{/if}} />
|
||||
{{localize "VAGABOND.Passive"}}
|
||||
</label>
|
||||
<button type="button" class="ability-delete" data-action="deleteAbility"
|
||||
data-ability-index="{{this.index}}" data-tooltip="{{localize 'VAGABOND.DeleteAbility'}}">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ability-description">
|
||||
<textarea name="system.abilities.{{this.index}}.description"
|
||||
placeholder="{{localize 'VAGABOND.AbilityDescription'}}">{{this.description}}</textarea>
|
||||
</div>
|
||||
</li>
|
||||
{{else}}
|
||||
<li class="ability-item empty">
|
||||
<p>{{localize "VAGABOND.NoAbilities"}}</p>
|
||||
<button type="button" data-action="addAbility">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
{{localize "VAGABOND.AddAbility"}}
|
||||
</button>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</section>
|
||||
80
templates/actor/npc-actions.hbs
Normal file
80
templates/actor/npc-actions.hbs
Normal file
@ -0,0 +1,80 @@
|
||||
{{!-- NPC Sheet Actions Section --}}
|
||||
<section class="npc-actions">
|
||||
<div class="section-header-row">
|
||||
<h2 class="section-header">{{localize "VAGABOND.Actions"}}</h2>
|
||||
<button type="button" class="action-add" data-action="addAction"
|
||||
data-tooltip="{{localize 'VAGABOND.AddAction'}}">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul class="action-list">
|
||||
{{#each actions}}
|
||||
<li class="action-item" data-action-index="{{this.index}}">
|
||||
<div class="action-header">
|
||||
<input type="text" class="action-name" name="system.actions.{{this.index}}.name"
|
||||
value="{{this.name}}" placeholder="{{localize 'VAGABOND.ActionName'}}" />
|
||||
<div class="action-controls">
|
||||
<button type="button" class="action-roll" data-action="rollAction"
|
||||
data-action-index="{{this.index}}" data-tooltip="{{localize 'VAGABOND.RollAction'}}">
|
||||
<i class="fa-solid fa-dice-d20"></i>
|
||||
</button>
|
||||
<button type="button" class="action-delete" data-action="deleteAction"
|
||||
data-action-index="{{this.index}}" data-tooltip="{{localize 'VAGABOND.DeleteAction'}}">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-details">
|
||||
<div class="action-field attack-type">
|
||||
<label>{{localize "VAGABOND.AttackType"}}</label>
|
||||
<select name="system.actions.{{this.index}}.attackType">
|
||||
{{#each ../attackTypeOptions}}
|
||||
<option value="{{@key}}" {{#if (eq @key ../this.attackType)}}selected{{/if}}>
|
||||
{{localize this}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="action-field damage">
|
||||
<label>{{localize "VAGABOND.Damage"}}</label>
|
||||
<input type="text" name="system.actions.{{this.index}}.damage"
|
||||
value="{{this.damage}}" placeholder="1d6" />
|
||||
</div>
|
||||
|
||||
<div class="action-field damage-type">
|
||||
<label>{{localize "VAGABOND.DamageType"}}</label>
|
||||
<select name="system.actions.{{this.index}}.damageType">
|
||||
{{#each ../damageTypeOptions}}
|
||||
<option value="{{@key}}" {{#if (eq @key ../this.damageType)}}selected{{/if}}>
|
||||
{{localize this}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="action-field range">
|
||||
<label>{{localize "VAGABOND.Range"}}</label>
|
||||
<input type="text" name="system.actions.{{this.index}}.range"
|
||||
value="{{this.range}}" placeholder="60 ft" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-description">
|
||||
<textarea name="system.actions.{{this.index}}.description"
|
||||
placeholder="{{localize 'VAGABOND.ActionDescription'}}">{{this.description}}</textarea>
|
||||
</div>
|
||||
</li>
|
||||
{{else}}
|
||||
<li class="action-item empty">
|
||||
<p>{{localize "VAGABOND.NoActions"}}</p>
|
||||
<button type="button" data-action="addAction">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
{{localize "VAGABOND.AddAction"}}
|
||||
</button>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</section>
|
||||
61
templates/actor/npc-header.hbs
Normal file
61
templates/actor/npc-header.hbs
Normal file
@ -0,0 +1,61 @@
|
||||
{{!-- NPC Sheet Header --}}
|
||||
<header class="sheet-header npc-header">
|
||||
<div class="header-portrait">
|
||||
<img class="profile-img" src="{{actor.img}}" alt="{{actor.name}}"
|
||||
data-edit="img" data-tooltip="VAGABOND.ChangePortrait" />
|
||||
</div>
|
||||
|
||||
<div class="header-info">
|
||||
<h1 class="actor-name">
|
||||
<input type="text" name="name" value="{{actor.name}}" placeholder="{{localize 'VAGABOND.NPCName'}}" />
|
||||
</h1>
|
||||
|
||||
<div class="header-stats">
|
||||
<div class="stat-box hd">
|
||||
<label>HD</label>
|
||||
<input type="number" name="system.hd" value="{{hd}}" min="0" />
|
||||
</div>
|
||||
|
||||
<div class="stat-box tl">
|
||||
<label>TL</label>
|
||||
<input type="number" name="system.tl" value="{{tl}}" min="0" step="0.1" />
|
||||
</div>
|
||||
|
||||
<div class="stat-box armor">
|
||||
<label>{{localize "VAGABOND.Armor"}}</label>
|
||||
<input type="number" name="system.armor" value="{{armor}}" min="0" />
|
||||
</div>
|
||||
|
||||
<div class="stat-box morale">
|
||||
<label>{{localize "VAGABOND.Morale"}}</label>
|
||||
<input type="number" name="system.morale" value="{{morale}}" min="2" max="12" />
|
||||
<button type="button" class="morale-roll" data-action="rollMorale"
|
||||
data-tooltip="{{localize 'VAGABOND.RollMorale'}}"
|
||||
{{#if moraleStatus.broken}}disabled{{/if}}>
|
||||
<i class="fa-solid fa-dice"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-hp">
|
||||
<div class="hp-bar {{#if hp.isDead}}dead{{else if hp.isHalf}}half{{/if}}">
|
||||
<label>HP</label>
|
||||
<div class="bar-container">
|
||||
<div class="bar-fill" style="width: {{hp.percent}}%"></div>
|
||||
<div class="bar-values">
|
||||
<input type="number" name="system.hp.value" value="{{hp.value}}" min="0" />
|
||||
<span class="separator">/</span>
|
||||
<input type="number" name="system.hp.max" value="{{hp.max}}" min="1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if moraleStatus.broken}}
|
||||
<div class="morale-broken">
|
||||
<i class="fa-solid fa-flag"></i>
|
||||
{{localize "VAGABOND.MoraleBroken"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</header>
|
||||
20
templates/actor/npc-notes.hbs
Normal file
20
templates/actor/npc-notes.hbs
Normal file
@ -0,0 +1,20 @@
|
||||
{{!-- NPC Sheet Notes Section --}}
|
||||
<section class="npc-notes">
|
||||
{{!-- Loot --}}
|
||||
<div class="notes-section loot">
|
||||
<h2 class="section-header">{{localize "VAGABOND.Loot"}}</h2>
|
||||
<div class="editor-container">
|
||||
<textarea name="system.loot"
|
||||
placeholder="{{localize 'VAGABOND.LootPlaceholder'}}">{{loot}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- GM Notes --}}
|
||||
<div class="notes-section gm-notes">
|
||||
<h2 class="section-header">{{localize "VAGABOND.GMNotes"}}</h2>
|
||||
<div class="editor-container">
|
||||
<textarea name="system.gmNotes"
|
||||
placeholder="{{localize 'VAGABOND.GMNotesPlaceholder'}}">{{gmNotes}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
136
templates/actor/npc-stats.hbs
Normal file
136
templates/actor/npc-stats.hbs
Normal file
@ -0,0 +1,136 @@
|
||||
{{!-- NPC Sheet Stats Section --}}
|
||||
<section class="npc-stats">
|
||||
<div class="stats-row">
|
||||
{{!-- Zone --}}
|
||||
<div class="stat-group zone">
|
||||
<label>{{localize "VAGABOND.Zone"}}</label>
|
||||
<select name="system.zone">
|
||||
{{#each zoneOptions}}
|
||||
<option value="{{@key}}" {{#if (eq @key ../zone)}}selected{{/if}}>
|
||||
{{localize this}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
<p class="zone-hint">{{zoneBehavior}}</p>
|
||||
</div>
|
||||
|
||||
{{!-- Speed --}}
|
||||
<div class="stat-group speed">
|
||||
<label>{{localize "VAGABOND.Speed"}}</label>
|
||||
<div class="speed-values">
|
||||
<div class="speed-item walk">
|
||||
<i class="fa-solid fa-shoe-prints"></i>
|
||||
<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>
|
||||
|
||||
{{!-- Size & Type --}}
|
||||
<div class="stat-group type">
|
||||
<div class="type-field">
|
||||
<label>{{localize "VAGABOND.Size"}}</label>
|
||||
<select name="system.size">
|
||||
{{#each sizeOptions}}
|
||||
<option value="{{@key}}" {{#if (eq @key ../size)}}selected{{/if}}>
|
||||
{{localize this}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="type-field">
|
||||
<label>{{localize "VAGABOND.BeingType"}}</label>
|
||||
<select name="system.beingType">
|
||||
{{#each beingTypeOptions}}
|
||||
<option value="{{@key}}" {{#if (eq @key ../beingType)}}selected{{/if}}>
|
||||
{{localize this}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Appearing --}}
|
||||
<div class="stat-group appearing">
|
||||
<label>{{localize "VAGABOND.Appearing"}}</label>
|
||||
<input type="text" name="system.appearing" value="{{appearing}}" placeholder="1d6" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Senses --}}
|
||||
{{#if hasSenses}}
|
||||
<div class="senses-row">
|
||||
<label>{{localize "VAGABOND.Senses"}}:</label>
|
||||
{{#if senses.darksight}}
|
||||
<span class="sense-tag">{{localize "VAGABOND.Darksight"}}</span>
|
||||
{{/if}}
|
||||
{{#if senses.blindsight}}
|
||||
<span class="sense-tag">{{localize "VAGABOND.Blindsight"}} {{senses.blindsight}} ft</span>
|
||||
{{/if}}
|
||||
{{#if senses.tremorsense}}
|
||||
<span class="sense-tag">{{localize "VAGABOND.Tremorsense"}} {{senses.tremorsense}} ft</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{!-- Damage Modifiers --}}
|
||||
{{#if hasDamageModifiers}}
|
||||
<div class="damage-modifiers">
|
||||
{{#if immunities.length}}
|
||||
<div class="modifier-row immunities">
|
||||
<label>{{localize "VAGABOND.Immunities"}}:</label>
|
||||
<div class="modifier-tags">
|
||||
{{#each immunities}}
|
||||
<span class="modifier-tag immune">{{this}}</span>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if resistances.length}}
|
||||
<div class="modifier-row resistances">
|
||||
<label>{{localize "VAGABOND.Resistances"}}:</label>
|
||||
<div class="modifier-tags">
|
||||
{{#each resistances}}
|
||||
<span class="modifier-tag resist">{{this}}</span>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if weaknesses.length}}
|
||||
<div class="modifier-row weaknesses">
|
||||
<label>{{localize "VAGABOND.Weaknesses"}}:</label>
|
||||
<div class="modifier-tags">
|
||||
{{#each weaknesses}}
|
||||
<span class="modifier-tag weak">{{this}}</span>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</section>
|
||||
13
templates/actor/parts/tabs.hbs
Normal file
13
templates/actor/parts/tabs.hbs
Normal file
@ -0,0 +1,13 @@
|
||||
{{!-- Sheet Tabs Navigation --}}
|
||||
{{#if tabs.length}}
|
||||
<nav class="sheet-tabs">
|
||||
{{#each tabs}}
|
||||
<button type="button" class="tab-button {{this.cssClass}}"
|
||||
data-action="changeTab" data-tab="{{this.id}}"
|
||||
data-tooltip="{{localize this.label}}">
|
||||
<i class="{{this.icon}}"></i>
|
||||
<span class="tab-label">{{localize this.label}}</span>
|
||||
</button>
|
||||
{{/each}}
|
||||
</nav>
|
||||
{{/if}}
|
||||
Loading…
Reference in New Issue
Block a user