From 8e097c9b2da4cc6e5ec55466b0fe4753a97fb569 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Sun, 14 Dec 2025 16:43:28 -0600 Subject: [PATCH] Implement character sheet foundation with ApplicationV2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- PROJECT_ROADMAP.json | 94 +-- lang/en.json | 86 ++- module/sheets/_module.mjs | 8 + module/sheets/base-actor-sheet.mjs | 716 +++++++++++++++++++++++ module/sheets/character-sheet.mjs | 367 ++++++++++++ module/sheets/npc-sheet.mjs | 279 +++++++++ module/vagabond.mjs | 64 ++- styles/scss/_variables.scss | 3 + styles/scss/components/_buttons.scss | 25 + styles/scss/sheets/_actor-sheet.scss | 735 +++++++++++++++++++++++- templates/actor/character-abilities.hbs | 121 ++++ templates/actor/character-biography.hbs | 86 +++ templates/actor/character-header.hbs | 118 ++++ templates/actor/character-inventory.hbs | 122 ++++ templates/actor/character-magic.hbs | 114 ++++ templates/actor/character-main.hbs | 110 ++++ templates/actor/npc-abilities.hbs | 45 ++ templates/actor/npc-actions.hbs | 80 +++ templates/actor/npc-header.hbs | 61 ++ templates/actor/npc-notes.hbs | 20 + templates/actor/npc-stats.hbs | 136 +++++ templates/actor/parts/tabs.hbs | 13 + 22 files changed, 3328 insertions(+), 75 deletions(-) create mode 100644 module/sheets/_module.mjs create mode 100644 module/sheets/base-actor-sheet.mjs create mode 100644 module/sheets/character-sheet.mjs create mode 100644 module/sheets/npc-sheet.mjs create mode 100644 templates/actor/character-abilities.hbs create mode 100644 templates/actor/character-biography.hbs create mode 100644 templates/actor/character-header.hbs create mode 100644 templates/actor/character-inventory.hbs create mode 100644 templates/actor/character-magic.hbs create mode 100644 templates/actor/character-main.hbs create mode 100644 templates/actor/npc-abilities.hbs create mode 100644 templates/actor/npc-actions.hbs create mode 100644 templates/actor/npc-header.hbs create mode 100644 templates/actor/npc-notes.hbs create mode 100644 templates/actor/npc-stats.hbs create mode 100644 templates/actor/parts/tabs.hbs diff --git a/PROJECT_ROADMAP.json b/PROJECT_ROADMAP.json index 61e0f0b..3a50baa 100644 --- a/PROJECT_ROADMAP.json +++ b/PROJECT_ROADMAP.json @@ -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." } ] }, diff --git a/lang/en.json b/lang/en.json index 933bb52..f529205 100644 --- a/lang/en.json +++ b/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}?" } diff --git a/module/sheets/_module.mjs b/module/sheets/_module.mjs new file mode 100644 index 0000000..8de2719 --- /dev/null +++ b/module/sheets/_module.mjs @@ -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"; diff --git a/module/sheets/base-actor-sheet.mjs b/module/sheets/base-actor-sheet.mjs new file mode 100644 index 0000000..2291bd0 --- /dev/null +++ b/module/sheets/base-actor-sheet.mjs @@ -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 }); + } +} diff --git a/module/sheets/character-sheet.mjs b/module/sheets/character-sheet.mjs new file mode 100644 index 0000000..8702b61 --- /dev/null +++ b/module/sheets/character-sheet.mjs @@ -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"); + } + } +} diff --git a/module/sheets/npc-sheet.mjs b/module/sheets/npc-sheet.mjs new file mode 100644 index 0000000..0f7c604 --- /dev/null +++ b/module/sheets/npc-sheet.mjs @@ -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 = ` +
+

${action.name}

+ ${action.description ? `

${action.description}

` : ""} +

+ Damage: [[/r ${action.damage}]] ${action.damageType} +

+ ${action.range ? `

Range: ${action.range}

` : ""} +
+ `; + + 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 }); + } +} diff --git a/module/vagabond.mjs b/module/vagabond.mjs index 47a0d07..4884993 100644 --- a/module/vagabond.mjs +++ b/module/vagabond.mjs @@ -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(); }); /* -------------------------------------------- */ diff --git a/styles/scss/_variables.scss b/styles/scss/_variables.scss index 714a89e..5997b6d 100644 --- a/styles/scss/_variables.scss +++ b/styles/scss/_variables.scss @@ -100,3 +100,6 @@ $z-fixed: 300; $z-modal-backdrop: 400; $z-modal: 500; $z-tooltip: 600; + +// Breakpoints for responsive layouts +$breakpoint-narrow: 700px; diff --git a/styles/scss/components/_buttons.scss b/styles/scss/components/_buttons.scss index aeb0185..fbe6d8c 100644 --- a/styles/scss/components/_buttons.scss +++ b/styles/scss/components/_buttons.scss @@ -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; diff --git a/styles/scss/sheets/_actor-sheet.scss b/styles/scss/sheets/_actor-sheet.scss index 206f724..45e123d 100644 --- a/styles/scss/sheets/_actor-sheet.scss +++ b/styles/scss/sheets/_actor-sheet.scss @@ -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; diff --git a/templates/actor/character-abilities.hbs b/templates/actor/character-abilities.hbs new file mode 100644 index 0000000..1e2f1f2 --- /dev/null +++ b/templates/actor/character-abilities.hbs @@ -0,0 +1,121 @@ +{{!-- Character Sheet Abilities Tab --}} +
+ {{!-- Ancestry Section --}} + {{#if ancestry}} +
+

{{localize "VAGABOND.Ancestry"}}

+
+
+ {{ancestry.name}} +
+ {{ancestry.name}} + {{ancestry.system.beingType}} + {{ancestry.system.size}} +
+
+ {{#if ancestry.system.traits.length}} +
+ {{#each ancestry.system.traits}} +
+ {{this.name}}: {{{this.description}}} +
+ {{/each}} +
+ {{/if}} +
+
+ {{/if}} + + {{!-- Class Features Section --}} +
+

{{localize "VAGABOND.Features"}}

+
    + {{#each items.features}} +
  • + {{this.name}} +
    + {{this.name}} + {{#if this.system.sourceClass}} + {{this.system.sourceClass}} Lv{{this.system.levelGained}} + {{/if}} +
    +
    {{{this.system.description}}}
    + +
  • + {{else}} +
  • {{localize "VAGABOND.NoFeatures"}}
  • + {{/each}} +
+
+ + {{!-- Perks Section --}} +
+
+

{{localize "VAGABOND.Perks"}}

+ +
+
    + {{#each items.perks}} +
  • + {{this.name}} +
    + {{this.name}} + {{#if this.system.prerequisites}} + {{this.system.prerequisites}} + {{/if}} +
    +
    {{{this.system.description}}}
    + +
  • + {{else}} +
  • {{localize "VAGABOND.NoPerks"}}
  • + {{/each}} +
+
+ + {{!-- Active Effects Section --}} +
+

{{localize "VAGABOND.ActiveEffects"}}

+ + {{#if effects.temporary.length}} +
+

{{localize "VAGABOND.TemporaryEffects"}}

+
    + {{#each effects.temporary}} +
  • + {{this.name}} + {{this.name}} + {{#if this.duration.remaining}} + {{this.duration.remaining}} rounds + {{/if}} +
  • + {{/each}} +
+
+ {{/if}} + + {{#if effects.passive.length}} +
+

{{localize "VAGABOND.PassiveEffects"}}

+
    + {{#each effects.passive}} +
  • + {{this.name}} + {{this.name}} + {{this.parent.name}} +
  • + {{/each}} +
+
+ {{/if}} +
+
diff --git a/templates/actor/character-biography.hbs b/templates/actor/character-biography.hbs new file mode 100644 index 0000000..b18960e --- /dev/null +++ b/templates/actor/character-biography.hbs @@ -0,0 +1,86 @@ +{{!-- Character Sheet Biography Tab --}} +
+ {{!-- Character Details --}} +
+

{{localize "VAGABOND.CharacterDetails"}}

+
+
+ + +
+ +
+ + +
+
+
+ + {{!-- Languages --}} +
+

{{localize "VAGABOND.Languages"}}

+
+ {{#each system.languages}} + {{this}} + {{else}} + {{localize "VAGABOND.NoLanguages"}} + {{/each}} +
+
+ + {{!-- Senses --}} +
+

{{localize "VAGABOND.Senses"}}

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

{{localize "VAGABOND.Biography"}}

+
+ +
+
+ + {{!-- Notes --}} +
+

{{localize "VAGABOND.Notes"}}

+
+ +
+
+
diff --git a/templates/actor/character-header.hbs b/templates/actor/character-header.hbs new file mode 100644 index 0000000..2e9d321 --- /dev/null +++ b/templates/actor/character-header.hbs @@ -0,0 +1,118 @@ +{{!-- Character Sheet Header --}} +
+
+ {{actor.name}} +
+ +
+

+ +

+ +
+
+ + {{ancestryName}} +
+ +
+ + {{className}} +
+ +
+ + +
+ +
+ + +
+
+
+ +
+
+ {{!-- HP Bar --}} +
+ +
+
+
+
+ + / + {{resources.hp.max}} +
+
+
+ + +
+
+
+ + {{!-- Mana Bar --}} + {{#if resources.mana.max}} +
+ +
+
+
+
+ + / + {{resources.mana.max}} +
+
+
+ + +
+
+
+ {{/if}} +
+ +
+
+ + + / + {{resources.luck.max}} +
+ +
+ + + / + 5 +
+ +
+ + {{system.armor}} +
+ +
+ + {{speed.walk}} + ft +
+
+
+
diff --git a/templates/actor/character-inventory.hbs b/templates/actor/character-inventory.hbs new file mode 100644 index 0000000..95f6c0b --- /dev/null +++ b/templates/actor/character-inventory.hbs @@ -0,0 +1,122 @@ +{{!-- Character Sheet Inventory Tab --}} +
+ {{!-- Item Slots Header --}} +
+
+ {{localize "VAGABOND.ItemSlots"}}: + {{itemSlots.used}} + / + {{itemSlots.max}} + {{#if itemSlots.overburdened}} + + + {{localize "VAGABOND.Overburdened"}} + + {{/if}} +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {{!-- Weapons Section --}} +
+
+

{{localize "VAGABOND.Weapons"}}

+ +
+
    + {{#each items.weapons}} +
  • + {{this.name}} + {{this.name}} + {{this.system.damage}} + {{this.system.slots}} slot{{#if (gt this.system.slots 1)}}s{{/if}} + + +
  • + {{else}} +
  • {{localize "VAGABOND.NoWeapons"}}
  • + {{/each}} +
+
+ + {{!-- Armor Section --}} +
+
+

{{localize "VAGABOND.Armor"}}

+ +
+
    + {{#each items.armor}} +
  • + {{this.name}} + {{this.name}} + +{{this.system.armorValue}} Armor + {{this.system.slots}} slot{{#if (gt this.system.slots 1)}}s{{/if}} + + +
  • + {{else}} +
  • {{localize "VAGABOND.NoArmor"}}
  • + {{/each}} +
+
+ + {{!-- Equipment Section --}} +
+
+

{{localize "VAGABOND.Equipment"}}

+ +
+
    + {{#each items.equipment}} +
  • + {{this.name}} + {{this.name}} + x{{this.system.quantity}} + {{this.system.slots}} slot{{#if (gt this.system.slots 1)}}s{{/if}} + +
  • + {{else}} +
  • {{localize "VAGABOND.NoEquipment"}}
  • + {{/each}} +
+
+
diff --git a/templates/actor/character-magic.hbs b/templates/actor/character-magic.hbs new file mode 100644 index 0000000..e5244e1 --- /dev/null +++ b/templates/actor/character-magic.hbs @@ -0,0 +1,114 @@ +{{!-- Character Sheet Magic Tab --}} +
+ {{!-- Mana Display --}} +
+
+ +
+ + / + {{resources.mana.max}} +
+ {{#if resources.mana.castingMax}} +
+ {{localize "VAGABOND.CastingMax"}}: + {{resources.mana.castingMax}} +
+ {{/if}} +
+ + {{!-- Focus Status --}} + {{#if hasFocus}} +
+

{{localize "VAGABOND.FocusActive"}}

+
    + {{#each focus.active}} +
  • + {{this.spellName}} + {{#if this.target}} + on {{this.target}} + {{/if}} + {{#if this.manaCostPerRound}} + {{this.manaCostPerRound}} Mana/round + {{/if}} +
  • + {{/each}} +
+
+ {{/if}} +
+ + {{!-- Known Spells Section --}} +
+
+

{{localize "VAGABOND.KnownSpells"}}

+ +
+ +
    + {{#each items.spells}} +
  • + {{this.name}} +
    + {{this.name}} + {{this.system.damageType}} +
    +
    + {{#if this.system.baseDamage}} + + + {{this.system.baseDamage}} + + {{/if}} + {{#if this.system.baseEffect}} + + + Effect + + {{/if}} +
    +
    + + +
    +
  • + {{else}} +
  • {{localize "VAGABOND.NoSpells"}}
  • + {{/each}} +
+
+ + {{!-- Spell Casting Reference --}} +
+

{{localize "VAGABOND.SpellcastingReference"}}

+
+
+

{{localize "VAGABOND.Delivery"}}

+
    +
  • Touch/Remote/Imbue: 0 Mana
  • +
  • Cube: +1 Mana
  • +
  • Aura/Cone/Glyph/Line/Sphere: +2 Mana
  • +
+
+
+

{{localize "VAGABOND.Duration"}}

+
    +
  • Instant: Immediate effect
  • +
  • Focus: Maintained (1 Mana/round vs unwilling)
  • +
  • Continual: Lasts until conditions met
  • +
+
+
+
+
diff --git a/templates/actor/character-main.hbs b/templates/actor/character-main.hbs new file mode 100644 index 0000000..cd4c025 --- /dev/null +++ b/templates/actor/character-main.hbs @@ -0,0 +1,110 @@ +{{!-- Character Sheet Main Tab --}} +
+
+ {{!-- Left Column: Stats --}} +
+

{{localize "VAGABOND.Stats"}}

+
+ {{#each stats}} +
+ + +
+ {{/each}} +
+
+ + {{!-- Center Column: Saves & Skills --}} +
+ {{!-- Saves Section --}} +
+

{{localize "VAGABOND.Saves"}}

+
+ {{#each saves}} +
+ {{localize this.label}} + ({{this.stats}}) + {{this.difficulty}} + +
+ {{/each}} +
+
+ + {{!-- Skills Section --}} +
+

{{localize "VAGABOND.Skills"}}

+
+ {{#each skills}} +
+ + {{localize this.label}} + ({{this.statAbbr}}) + {{this.difficulty}} + {{#if this.hasCritBonus}} + + {{this.critThreshold}} + + {{/if}} + +
+ {{/each}} +
+
+
+ + {{!-- Right Column: Attacks & Weapons --}} +
+ {{!-- Attack Skills --}} +
+

{{localize "VAGABOND.Attacks"}}

+
+ {{#each attackSkills}} +
+ {{localize this.label}} + ({{this.statAbbr}}) + {{this.difficulty}} + {{#if this.hasCritBonus}} + + {{this.critThreshold}} + + {{/if}} +
+ {{/each}} +
+
+ + {{!-- Equipped Weapons --}} +
+

{{localize "VAGABOND.EquippedWeapons"}}

+
    + {{#each items.weapons}} + {{#if this.system.equipped}} +
  • + {{this.name}} + {{this.system.damage}} + +
  • + {{/if}} + {{/each}} +
+
+
+
+
diff --git a/templates/actor/npc-abilities.hbs b/templates/actor/npc-abilities.hbs new file mode 100644 index 0000000..c5163f4 --- /dev/null +++ b/templates/actor/npc-abilities.hbs @@ -0,0 +1,45 @@ +{{!-- NPC Sheet Abilities Section --}} +
+
+

{{localize "VAGABOND.Abilities"}}

+ +
+ +
    + {{#each abilities}} +
  • +
    + +
    + + +
    +
    + +
    + +
    +
  • + {{else}} +
  • +

    {{localize "VAGABOND.NoAbilities"}}

    + +
  • + {{/each}} +
+
diff --git a/templates/actor/npc-actions.hbs b/templates/actor/npc-actions.hbs new file mode 100644 index 0000000..d66c3d9 --- /dev/null +++ b/templates/actor/npc-actions.hbs @@ -0,0 +1,80 @@ +{{!-- NPC Sheet Actions Section --}} +
+
+

{{localize "VAGABOND.Actions"}}

+ +
+ +
    + {{#each actions}} +
  • +
    + +
    + + +
    +
    + +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    +
    + +
    + +
    +
  • + {{else}} +
  • +

    {{localize "VAGABOND.NoActions"}}

    + +
  • + {{/each}} +
+
diff --git a/templates/actor/npc-header.hbs b/templates/actor/npc-header.hbs new file mode 100644 index 0000000..3778e92 --- /dev/null +++ b/templates/actor/npc-header.hbs @@ -0,0 +1,61 @@ +{{!-- NPC Sheet Header --}} +
+
+ {{actor.name}} +
+ +
+

+ +

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+
+
+ +
+
+ +
+
+
+ + / + +
+
+
+ + {{#if moraleStatus.broken}} +
+ + {{localize "VAGABOND.MoraleBroken"}} +
+ {{/if}} +
+
diff --git a/templates/actor/npc-notes.hbs b/templates/actor/npc-notes.hbs new file mode 100644 index 0000000..f9232bf --- /dev/null +++ b/templates/actor/npc-notes.hbs @@ -0,0 +1,20 @@ +{{!-- NPC Sheet Notes Section --}} +
+ {{!-- Loot --}} +
+

{{localize "VAGABOND.Loot"}}

+
+ +
+
+ + {{!-- GM Notes --}} +
+

{{localize "VAGABOND.GMNotes"}}

+
+ +
+
+
diff --git a/templates/actor/npc-stats.hbs b/templates/actor/npc-stats.hbs new file mode 100644 index 0000000..b0bd5e8 --- /dev/null +++ b/templates/actor/npc-stats.hbs @@ -0,0 +1,136 @@ +{{!-- NPC Sheet Stats Section --}} +
+
+ {{!-- Zone --}} +
+ + +

{{zoneBehavior}}

+
+ + {{!-- Speed --}} +
+ +
+
+ + + ft +
+ {{#if speed.hasSpecialMovement}} + {{#if speed.fly}} +
+ + + ft +
+ {{/if}} + {{#if speed.swim}} +
+ + + ft +
+ {{/if}} + {{#if speed.climb}} +
+ + + ft +
+ {{/if}} + {{/if}} +
+
+ + {{!-- Size & Type --}} +
+
+ + +
+
+ + +
+
+ + {{!-- Appearing --}} +
+ + +
+
+ + {{!-- Senses --}} + {{#if hasSenses}} +
+ + {{#if senses.darksight}} + {{localize "VAGABOND.Darksight"}} + {{/if}} + {{#if senses.blindsight}} + {{localize "VAGABOND.Blindsight"}} {{senses.blindsight}} ft + {{/if}} + {{#if senses.tremorsense}} + {{localize "VAGABOND.Tremorsense"}} {{senses.tremorsense}} ft + {{/if}} +
+ {{/if}} + + {{!-- Damage Modifiers --}} + {{#if hasDamageModifiers}} +
+ {{#if immunities.length}} +
+ +
+ {{#each immunities}} + {{this}} + {{/each}} +
+
+ {{/if}} + + {{#if resistances.length}} +
+ +
+ {{#each resistances}} + {{this}} + {{/each}} +
+
+ {{/if}} + + {{#if weaknesses.length}} +
+ +
+ {{#each weaknesses}} + {{this}} + {{/each}} +
+
+ {{/if}} +
+ {{/if}} +
diff --git a/templates/actor/parts/tabs.hbs b/templates/actor/parts/tabs.hbs new file mode 100644 index 0000000..752c3d8 --- /dev/null +++ b/templates/actor/parts/tabs.hbs @@ -0,0 +1,13 @@ +{{!-- Sheet Tabs Navigation --}} +{{#if tabs.length}} + +{{/if}}