diff --git a/lang/en.json b/lang/en.json index c68e18e..38e14e2 100644 --- a/lang/en.json +++ b/lang/en.json @@ -29,6 +29,14 @@ "TYPES.Item.floor": "Floor", "TYPES.Item.wall": "Wall", + "BASICFANTASYRPG.Tab.cargo": "Cargo", + "BASICFANTASYRPG.Tab.combat": "Combat", + "BASICFANTASYRPG.Tab.description": "Description", + "BASICFANTASYRPG.Tab.features": "Special Abilities", + "BASICFANTASYRPG.Tab.floors": "Floors & Walls", + "BASICFANTASYRPG.Tab.items": "Equipment", + "BASICFANTASYRPG.Tab.spells": "Spells", + "BASICFANTASYRPG.TabCargo": "Cargo", "BASICFANTASYRPG.TabCombat": "Combat", "BASICFANTASYRPG.TabDescription": "Description", diff --git a/lang/fr.json b/lang/fr.json index 2be7934..b5c4bc7 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -37,6 +37,14 @@ "BASICFANTASYRPG.TabItems": "Équipement", "BASICFANTASYRPG.TabSpells": "Sortilèges", + "BASICFANTASYRPG.Tab.cargo": "Cargo", + "BASICFANTASYRPG.Tab.combat": "Combat", + "BASICFANTASYRPG.Tab.description": "Description", + "BASICFANTASYRPG.Tab.features": "Capacité spéciale", + "BASICFANTASYRPG.Tab.floors": "Sols & Murs", + "BASICFANTASYRPG.Tab.items": "Équipement", + "BASICFANTASYRPG.Tab.spells": "Sortilèges", + "BASICFANTASYRPG.AbilityStr": "Force", "BASICFANTASYRPG.AbilityCon": "Constitution", "BASICFANTASYRPG.AbilityDex": "Dextérité", diff --git a/module/basicfantasyrpg.mjs b/module/basicfantasyrpg.mjs index 017f2ef..ddb1b66 100644 --- a/module/basicfantasyrpg.mjs +++ b/module/basicfantasyrpg.mjs @@ -4,6 +4,8 @@ import { BasicFantasyRPGItem } from './documents/item.mjs'; // Import sheet classes. import { BasicFantasyRPGActorSheet } from './sheets/actor-sheet.mjs'; import { BasicFantasyRPGItemSheet } from './sheets/item-sheet.mjs'; +import { CharacterSheet } from './sheets/character-sheet.mjs'; + // Import helper/utility classes and constants. import { preloadHandlebarsTemplates } from './helpers/templates.mjs'; import { BASICFANTASYRPG } from './helpers/config.mjs'; @@ -40,7 +42,13 @@ Hooks.once('init', async function() { // Register sheet application classes Actors.unregisterSheet('core', ActorSheet); - Actors.registerSheet('basicfantasyrpg', BasicFantasyRPGActorSheet, { makeDefault: true }); + Actors.registerSheet('basicfantasyrpg', BasicFantasyRPGActorSheet, + { makeDefault: true } + ); + Actors.registerSheet('basicfantasyrpg', + CharacterSheet, + { types: ['character'], makeDefault: true, label: "Character Sheet V2"} + ); Items.unregisterSheet('core', ItemSheet); Items.registerSheet('basicfantasyrpg', BasicFantasyRPGItemSheet, { makeDefault: true }); @@ -94,7 +102,7 @@ Handlebars.registerHelper('toLowerCase', function(str) { }); Handlebars.registerHelper('selected', function(value) { - return Boolean(value) ? "selected" : ""; + return value ? "selected" : ""; }); Handlebars.registerPartial('iconDamage', ``); diff --git a/module/sheets/base-actor-sheet.mjs b/module/sheets/base-actor-sheet.mjs new file mode 100644 index 0000000..369ff3b --- /dev/null +++ b/module/sheets/base-actor-sheet.mjs @@ -0,0 +1,436 @@ +import { successChatMessage } from "../helpers/chat.mjs"; +import { onManageActiveEffect, prepareActiveEffectCategories } from "../helpers/effects.mjs"; + +const { HandlebarsApplicationMixin } = foundry.applications.api; +const { ActorSheetV2 } = foundry.applications.sheets; + +/** + * Base Actor Sheet class for Basic Fantasy RPG + * Contains shared functionality for all actor types + * @extends {ActorSheetV2} + */ +export class BaseActorSheet extends HandlebarsApplicationMixin(ActorSheetV2) { + static DEFAULT_OPTIONS = { + classes: ["basicfantasyrpg", "sheet", "actor", "themed", "theme-light"], + position: { + width: 600, + height: 600, + }, + window: { + resizable: true, + }, + form: { + handler: BaseActorSheet.#onSubmitDocumentForm, + submitOnChange: true, + }, + }; + + /** @override */ + async _prepareContext(options) { + const context = await super._prepareContext(options); + const TextEditor = foundry.applications.ux.TextEditor.implementation; + + // Add the actor's basic data to the context + context.actor = this.document; + context.system = this.document.system; + context.name = this.document.name; + context.data = context.system; + context.items = this.document.items.contents; + + // biography editor + context.enrichedBiography = await TextEditor.enrichHTML(this.document.system.biography, { + async: true, + }); + + this._prepareItems(context); + + return context; + } + + /** + * Handle form submission for the actor sheet + * @param {SubmitEvent} event The form submission event + * @param {HTMLFormElement} form The submitted form + * @param {FormDataExtended} formData The form data + * @returns {Promise} + */ + static async #onSubmitDocumentForm(event, form, formData) { + const updates = foundry.utils.expandObject(formData.object); + return this.document.update(updates); + } + + /** + * Organize and classify Items for Actor sheets. + * + * @param {Object} actorData The actor to prepare. + * + * @return {undefined} + */ + _prepareItems(context) { + // Initialize containers. + const gear = []; + const weapons = []; + const armors = []; + const spells = { + 1: [], + 2: [], + 3: [], + 4: [], + 5: [], + 6: [], + }; + const features = []; + const floors = []; + const walls = []; + + // Define an object to store carried weight. + let carriedWeight = { + value: 0, + _addWeight(moreWeight, quantity) { + if (!quantity || quantity === "" || Number.isNaN(quantity) || quantity < 0) { + return; // check we have a valid quantity, and do nothing if we do not + } + let q = Math.floor(quantity / 20); + if (!Number.isNaN(parseFloat(moreWeight))) { + this.value += parseFloat(moreWeight) * quantity; + } else if (moreWeight === "*" && q > 0) { + // '*' is gold pieces + this.value += q; + } + }, + }; + + // Iterate through items, allocating to containers + for (let i of context.items) { + i.img = i.img || DEFAULT_TOKEN; + // Append to gear. + if (i.type === "item") { + gear.push(i); + carriedWeight._addWeight(i.system.weight.value, i.system.quantity.value); + } else if (i.type === "weapon") { + // Append to weapons. + weapons.push(i); + carriedWeight._addWeight(i.system.weight.value, 1); // Weapons are always quantity 1 + } else if (i.type === "armor") { + // Append to armors. + armors.push(i); + carriedWeight._addWeight(i.system.weight.value, 1); // Armor is always quantity 1 + } else if (i.type === "spell") { + // Append to spells. + if (i.system.spellLevel.value !== undefined) { + spells[i.system.spellLevel.value].push(i); + } + } else if (i.type === "feature") { + // Append to features. + features.push(i); + } else if (i.type === "floor") { + // Append to floors for strongholds. + floors.push(i); + } else if (i.type === "wall") { + // Append to walls for strongholds. + if (i.system.floor.value !== undefined) { + if (!walls[i.system.floor.value]) walls[i.system.floor.value] = []; + walls[i.system.floor.value].push(i); + } + } + } + + // Iterate through money, add to carried weight + if (context.data.money) { + let gp = Number(context.data.money.gp.value); + gp += context.data.money.pp.value; + gp += context.data.money.ep.value; + gp += context.data.money.sp.value; + gp += context.data.money.cp.value; + carriedWeight._addWeight("*", gp); // '*' will calculate GP weight + } + + // Assign and return + context.gear = gear; + context.weapons = weapons; + context.armors = armors; + context.spells = spells; + context.features = features; + context.floors = floors; + context.walls = walls; + context.carriedWeight = Math.floor(carriedWeight.value); // we discard fractions of weight when we update the sheet + } + + _onRender(context, options) { + super._onRender(context, options); + + // Remove old listeners first to prevent duplicates + if (this._boundClickHandler) { + this.element.removeEventListener("click", this._boundClickHandler); + } + + // Add click listener with proper binding + this._boundClickHandler = this._onSheetClick.bind(this); + this.element.addEventListener("click", this._boundClickHandler); + + // Drag and drop for macros - always refresh since items may have changed + if (this.document.isOwner) { + this._setupDragAndDrop(); + } + } + + /** + * Handle click events using event delegation + * @param {Event} event The click event + */ + _onSheetClick(event) { + const target = event.target; + // const control = target.closest('.item-control, .rollable, input[name="rangeBonus"]'); + // Handle item editing first (always available) + const editControl = target.closest(".item-edit"); + if (editControl) { + event.preventDefault(); + this._onItemEdit(event); + return; + } + + // Everything below here is only needed if the sheet is editable + if (!this.isEditable) return; + + const control = target.closest('.item-control, .rollable, input[name="rangeBonus"]'); + + if (!control) return; + + // Item creation + if (control.classList.contains("item-create")) { + event.preventDefault(); + this._onItemCreate(event); + } + // Item deletion + else if (control.classList.contains("item-delete")) { + event.preventDefault(); + this._onItemDelete(event); + } + // Spell preparation + else if (control.classList.contains("spell-prepare")) { + event.preventDefault(); + this._onSpellPrepare(event); + } + // Quantity adjustment + else if (control.classList.contains("quantity")) { + event.preventDefault(); + this._onQuantityAdjust(event); + } + // Active effect management + else if (control.classList.contains("effect-control")) { + event.preventDefault(); + onManageActiveEffect(event, this.document); + } + // Rollable abilities + else if (control.classList.contains("rollable")) { + event.preventDefault(); + this._onRoll(event); + } + // Siege engine range bonus (specific to siege engines) + else if (control.matches('input[name="rangeBonus"]') && this.document.type === "siegeEngine") { + this.document.update({ "system.rangeBonus.value": Number(control.value) }); + } + } + + /** + * Handle item editing + * @param {Event} event The click event + */ + _onItemEdit(event) { + const li = event.target.closest(".item"); + if (!li?.dataset.itemId) return; + + const item = this.document.items.get(li.dataset.itemId); + if (item) { + item.sheet.render(true); + } + } + + /** + * Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset + * @param {Event} event The originating click event + */ + async _onItemCreate(event) { + const header = event.target.closest(".item-create"); + const type = header.dataset.type; + const data = foundry.utils.duplicate(header.dataset); + + if (type === "spell") { + data.spellLevel = { + value: data.spellLevelValue, + }; + delete data.spellLevelValue; + } else if (type === "wall") { + data.floor = { + value: data.floorNumber, + }; + delete data.floorNumber; + } + + const name = `New ${type.capitalize()}`; + const itemData = { + name: name, + type: type, + system: data, + }; + delete itemData.system["type"]; + + return await Item.create(itemData, { parent: this.document }); + } + + /** + * Handle item deletion with animation + * @param {Event} event The click event + */ + async _onItemDelete(event) { + const li = event.target.closest(".item"); + if (!li) return; + + const item = this.document.items.get(li.dataset.itemId); + if (!item) return; + + // Add deletion animation class + li.classList.add('item-deleting'); + + // Listen for animation end + const handleAnimationEnd = async () => { + li.removeEventListener('animationend', handleAnimationEnd); + await item.delete(); + // Sheet will auto-render due to document change + }; + + li.addEventListener('animationend', handleAnimationEnd); + } + + /** + * Handle spell preparation adjustments + * @param {Event} event The click event + */ + _onSpellPrepare(event) { + const change = event.target.closest(".spell-prepare").dataset.change; + if (!parseInt(change)) return; + + const li = event.target.closest(".item"); + if (!li) return; + + const item = this.document.items.get(li.dataset.itemId); + if (!item) return; + + const newValue = item.system.prepared.value + parseInt(change); + item.update({ "system.prepared.value": newValue }); + } + + /** + * Handle quantity adjustments + * @param {Event} event The click event + */ + _onQuantityAdjust(event) { + const change = event.target.closest(".quantity").dataset.change; + if (!parseInt(change)) return; + + const li = event.target.closest(".item"); + if (!li) return; + + const item = this.document.items.get(li.dataset.itemId); + if (!item) return; + + const newValue = item.system.quantity.value + parseInt(change); + item.update({ "system.quantity.value": newValue }); + } + + /** + * Handle clickable rolls + * @param {Event} event The originating click event + */ + async _onRoll(event) { + const element = event.target.closest(".rollable"); + const dataset = element.dataset; + + if (dataset.rollType) { + // Handle weapon rolls + if (dataset.rollType === "weapon") { + const itemId = element.closest(".item").dataset.itemId; + const item = this.document.items.get(itemId); + let label = dataset.label + ? `${game.i18n.localize("BASICFANTASYRPG.Roll")}: ${dataset.label}` + : `${game.i18n.localize("BASICFANTASYRPG.Roll")}: ${dataset.attack.capitalize()} attack with ${item.name}`; + + let rollFormula = "d20+@ab"; + if (this.document.type === "character") { + if (dataset.attack === "melee") { + rollFormula += "+@str.bonus"; + } else if (dataset.attack === "ranged") { + rollFormula += "+@dex.bonus"; + } + } + rollFormula += "+" + item.system.bonusAb.value; + + let roll = new Roll(rollFormula, this.document.getRollData()); + roll.toMessage({ + speaker: ChatMessage.getSpeaker({ actor: this.document }), + flavor: label, + rollMode: game.settings.get("core", "rollMode"), + }); + return roll; + } + + // Handle item rolls + if (dataset.rollType === "item") { + const itemId = element.closest(".item").dataset.itemId; + const item = this.document.items.get(itemId); + if (item) return item.roll(); + } + } + + // Handle rolls that supply the formula directly + if (dataset.roll) { + let label = dataset.label + ? `${game.i18n.localize("BASICFANTASYRPG.Roll")}: ${dataset.label}` + : ""; + let roll = new Roll(dataset.roll, this.document.getRollData()); + await roll.roll(); + label += successChatMessage(roll.total, dataset.targetNumber, dataset.rollUnder); + roll.toMessage({ + speaker: ChatMessage.getSpeaker({ actor: this.document }), + flavor: label, + rollMode: game.settings.get("core", "rollMode"), + }); + return roll; + } + } + + /** + * Setup drag and drop functionality for items + */ + _setupDragAndDrop() { + const handler = (ev) => this._onDragStart(ev); + this.element.querySelectorAll("li.item").forEach((li) => { + if (li.classList.contains("inventory-header")) return; + if (li.getAttribute("draggable") === "true") return; + li.setAttribute("draggable", true); + li.addEventListener("dragstart", handler, false); + }); + } + + /** + * Handle beginning of drag workflows + * @param {Event} event The drag start event + */ + _onDragStart(event) { + const li = event.currentTarget; + if (event.target.classList.contains("content-link")) return; + + let dragData = null; + + // Owned Items + if (li.dataset.itemId) { + const item = this.document.items.get(li.dataset.itemId); + dragData = item.toDragData(); + } + + if (!dragData) return; + + // Set data transfer + event.dataTransfer.setData("text/plain", JSON.stringify(dragData)); + } +} diff --git a/module/sheets/character-sheet.mjs b/module/sheets/character-sheet.mjs new file mode 100644 index 0000000..84f0d68 --- /dev/null +++ b/module/sheets/character-sheet.mjs @@ -0,0 +1,84 @@ +import { BaseActorSheet } from "./base-actor-sheet.mjs"; + +/** + * Character Sheet for Basic Fantasy RPG + * Extends BaseActorSheet with character-specific functionality + * @extends {BaseActorSheet} + */ +export class CharacterSheet extends BaseActorSheet { + static DEFAULT_OPTIONS = { + ...BaseActorSheet.DEFAULT_OPTIONS, + classes: [...BaseActorSheet.DEFAULT_OPTIONS.classes, "character"], + window: { + ...BaseActorSheet.DEFAULT_OPTIONS.window, + title: "Character", + }, + }; + + static TABS = { + primary: { + tabs: [{ id: "combat" }, { id: "description"}, { id: "items" }, { id: "spells"}, { id: "features" }], + labelPrefix: "BASICFANTASYRPG.Tab", + initial: "combat", + }, + }; + + static PARTS = { + header: { + template: "systems/basicfantasyrpg/templates/actor/character.hbs", + }, + tabs: { + // Foundry-provided generic template + template: 'templates/generic/tab-navigation.hbs', + }, + combat: { + template: "systems/basicfantasyrpg/templates/actor/parts/combat.hbs", + }, + description: { + template: "systems/basicfantasyrpg/templates/actor/parts/description.hbs", + }, + items: { + template: "systems/basicfantasyrpg/templates/actor/parts/items.hbs", + }, + spells: { + template: "systems/basicfantasyrpg/templates/actor/parts/spells.hbs", + }, + features: { + template: "systems/basicfantasyrpg/templates/actor/parts/features.hbs", + } + }; + + + /** @override */ + _onRender(context, options) { + super._onRender(context, options); + + // Add character-specific rendering logic + console.log("Character Sheet rendered for:", context.name); + } + + /** @override */ + async _prepareContext(options) { + const context = await super._prepareContext(options); + + context.tabs = this._prepareTabs("primary"); + + // Handle saves. + for (let [k, v] of Object.entries(context.data.saves)) { + v.label = game.i18n.localize(CONFIG.BASICFANTASYRPG.saves[k]) ?? k; + } + + // Handle ability scores. + for (let [k, v] of Object.entries(context.data.abilities)) { + v.label = game.i18n.localize(CONFIG.BASICFANTASYRPG.abilities[k]) ?? k; + } + + // Handle money. + for (let [k, v] of Object.entries(context.data.money)) { + v.label = game.i18n.localize(CONFIG.BASICFANTASYRPG.money[k]) ?? k; + } + + console.log("Available context data:", context); + return context; + } +} diff --git a/styles/basicfantasyrpg.css b/styles/basicfantasyrpg.css index 1cfff7a..a92027b 100644 --- a/styles/basicfantasyrpg.css +++ b/styles/basicfantasyrpg.css @@ -3,55 +3,60 @@ font-family: 'Soutane'; src: url('soutane-webfont.eot'); src: url('soutane-webfont.eot?#iefix') format('embedded-opentype'), - url('soutane-webfont.woff') format('woff'), url('soutane-webfont.ttf') format('truetype'), - url('soutane-webfont.svg#soutaneregular') format('svg'); + url('soutane-webfont.svg#soutaneregular') format('svg'), + url('soutane-webfont.woff') format('woff'); font-weight: normal; font-style: normal; + font-display: swap; } @font-face { font-family: 'Soutane'; src: url('soutanebold-webfont.eot'); src: url('soutanebold-webfont.eot?#iefix') format('embedded-opentype'), - url('soutanebold-webfont.woff') format('woff'), url('soutanebold-webfont.ttf') format('truetype'), - url('soutanebold-webfont.svg#soutanebold') format('svg'); + url('soutanebold-webfont.svg#soutanebold') format('svg'), + url('soutanebold-webfont.woff') format('woff'); font-weight: bold; font-style: normal; + font-display: swap; } @font-face { font-family: 'Soutane'; src: url('soutanebolditalic-webfont.eot'); src: url('soutanebolditalic-webfont.eot?#iefix') format('embedded-opentype'), - url('soutanebolditalic-webfont.woff') format('woff'), url('soutanebolditalic-webfont.ttf') format('truetype'), - url('soutanebolditalic-webfont.svg#soutanebold_italic') format('svg'); + url('soutanebolditalic-webfont.svg#soutanebold_italic') format('svg'), + url('soutanebolditalic-webfont.woff') format('woff'); font-weight: bold; font-style: italic; + font-display: swap; } @font-face { font-family: 'Soutane'; src: url('soutaneitalic-webfont.eot'); src: url('soutaneitalic-webfont.eot?#iefix') format('embedded-opentype'), - url('soutaneitalic-webfont.woff') format('woff'), url('soutaneitalic-webfont.ttf') format('truetype'), - url('soutaneitalic-webfont.svg#soutaneitalic') format('svg'); + url('soutaneitalic-webfont.svg#soutaneitalic') format('svg'), + url('soutaneitalic-webfont.woff') format('woff'); font-weight: normal; font-style: italic; + font-display: swap; } @font-face { font-family: 'SoutaneBlack'; src: url('soutaneblack-webfont.eot'); src: url('soutaneblack-webfont.eot?#iefix') format('embedded-opentype'), - url('soutaneblack-webfont.woff') format('woff'), url('soutaneblack-webfont.ttf') format('truetype'), - url('soutaneblack-webfont.svg#soutaneblackregular') format('svg'); + url('soutaneblack-webfont.svg#soutaneblackregular') format('svg'), + url('soutaneblack-webfont.woff') format('woff'); font-weight: normal; font-style: normal; + font-display: swap; } @font-face { @@ -98,6 +103,13 @@ font-style: italic; } +:root { + --font-primary: "Soutane"; + --color-border-light-primary: #b7b7af; /* neutral light grey-brown */ + --color-border-light-highlight: #ccc5b9; /* warm light beige */ +} + + /* Global styles */ .game { font-family: "Soutane", "Century Gothic", "TeX Gyre Adventor", var(--font-primary); @@ -416,12 +428,16 @@ padding: 0px; margin: 5px 0; border-bottom: 0; + line-height: 48px; + font-size: 28px; + font-weight: 400; } .basicfantasyrpg .sheet-header h1.charname input { width: 100%; height: 100%; margin: 0; + font-family: "Soutane", "Century Gothic", "TeX Gyre Adventor", var(--font-primary); } .basicfantasyrpg .sheet-tabs { @@ -442,6 +458,10 @@ max-height: calc(100% - 53px); } +.basicfantasyrpg.sheet .tab-container { + display: flex; +} + .basicfantasyrpg .editor-content { font-family: "Soutane", "Century Gothic", "TeX Gyre Adventor", var(--font-primary); font-size: 18px; @@ -541,6 +561,9 @@ margin: 0; white-space: nowrap; overflow-x: hidden; + font-size: 13px; + font-family: "Soutane", "Century Gothic", "TeX Gyre Adventor", var(--font-primary); + font-weight: 400; } .basicfantasyrpg .items-list .item-controls { @@ -642,3 +665,19 @@ .basicfantasyrpg .effects .item .effect-controls { border: none; } + +/* Item deletion animation */ +.basicfantasyrpg .items-list .item.item-deleting { + animation: itemDeleteFade 0.15s ease-out forwards; +} + +@keyframes itemDeleteFade { + 0% { + opacity: 1; + transform: translateX(0); + } + 100% { + opacity: 0; + transform: translateX(-30px); + } +} diff --git a/templates/actor/actor-character-sheet.html b/templates/actor/actor-character-sheet.html index 0cc102e..00037ee 100644 --- a/templates/actor/actor-character-sheet.html +++ b/templates/actor/actor-character-sheet.html @@ -1,4 +1,4 @@ - + {{!-- Sheet Header --}} @@ -105,5 +105,5 @@ - + diff --git a/templates/actor/actor-monster-sheet.html b/templates/actor/actor-monster-sheet.html index b9887cf..68b547f 100644 --- a/templates/actor/actor-monster-sheet.html +++ b/templates/actor/actor-monster-sheet.html @@ -1,4 +1,4 @@ - + {{!-- Sheet Header --}} @@ -104,4 +104,4 @@ - + diff --git a/templates/actor/character.hbs b/templates/actor/character.hbs new file mode 100644 index 0000000..a3e6d85 --- /dev/null +++ b/templates/actor/character.hbs @@ -0,0 +1,49 @@ + + + {{!-- Sheet Header --}} + + + + + + + + + + + + + {{localize data.hitPoints.label}} + + + / + + + + + + + {{localize data.class.label}} / {{localize data.level.label}} + + + + + + + + + + {{localize data.xp.abbr}} / {{localize 'BASICFANTASYRPG.NextLevel'}} + + + / + + + + + + + + + + \ No newline at end of file diff --git a/templates/actor/parts/combat.hbs b/templates/actor/parts/combat.hbs new file mode 100644 index 0000000..81da101 --- /dev/null +++ b/templates/actor/parts/combat.hbs @@ -0,0 +1,120 @@ + + + + + + + + {{#each data.abilities as |ability key|}} + + {{ability.label}} + + {{numberFormat ability.bonus decimals=0 sign=true}} + + {{/each}} + + + + {{#each data.saves as |save key|}} + + {{save.label}} + + + {{/each}} + + + + + + + + + {{localize data.armorClass.abbr}} + + + + + + + {{localize data.attackBonus.abbr}} + + + + + + + {{localize data.move.label}} + + + + + + + {{localize data.initBonus.label}} + + + + + + + + + + {{localize 'ITEM.TypeWeapon'}} + {{localize 'BASICFANTASYRPG.Attack'}} / {{localize 'BASICFANTASYRPG.DamageAbbr'}} + + {{localize 'BASICFANTASYRPG.Add'}} + + + {{#each weapons as |weapon id|}} + + + + + + {{weapon.name}} + + + {{> iconMelee}} + {{> iconRanged}} + / + {{> iconDamage}} + + + + + + + {{/each}} + + + + + {{localize 'ITEM.TypeArmor'}} + {{localize 'BASICFANTASYRPG.ArmorClass'}} + + {{localize 'BASICFANTASYRPG.Add'}} + + + {{#each armors as |armor id|}} + + + + + + {{armor.name}} + + {{localize armor.system.armorClass.abbr}} {{armor.system.armorClass.value}} + + + + + + {{/each}} + + + + \ No newline at end of file diff --git a/templates/actor/parts/description.hbs b/templates/actor/parts/description.hbs new file mode 100644 index 0000000..be70ad6 --- /dev/null +++ b/templates/actor/parts/description.hbs @@ -0,0 +1,38 @@ + + + + + + {{localize data.race.label}} + + + + + + + {{localize data.sex.label}} + + + + + + + {{localize data.age.label}} + + + + + + + {{ uuid }} + + + + + diff --git a/templates/actor/parts/features.hbs b/templates/actor/parts/features.hbs new file mode 100644 index 0000000..3c973fb --- /dev/null +++ b/templates/actor/parts/features.hbs @@ -0,0 +1,28 @@ + + + + {{localize 'ITEM.TypeFeature'}} + {{localize 'BASICFANTASYRPG.Formula'}} + + {{localize 'BASICFANTASYRPG.Add'}} + + + {{#each features as |item id|}} + + + + + + {{item.name}} + + {{item.system.formula.value}}{{#if item.system.targetNumber.value}} ({{#if item.system.rollUnder.value}}≤{{else}}≥{{/if}} {{item.system.targetNumber.value}}){{/if}} + + + + + + {{/each}} + + \ No newline at end of file diff --git a/templates/actor/parts/items.hbs b/templates/actor/parts/items.hbs new file mode 100644 index 0000000..a922480 --- /dev/null +++ b/templates/actor/parts/items.hbs @@ -0,0 +1,74 @@ + + + {{#if (eq actor.type 'character')}} + + + + {{data.money.pp.label}} + + + + + + + {{data.money.gp.label}} + + + + + + + {{data.money.ep.label}} + + + + + + + {{data.money.sp.label}} + + + + + + + {{data.money.cp.label}} + + + + + + + {{/if}} + + + + {{localizeItemNameForActor actor.type}} + {{localize 'BASICFANTASYRPG.CarriedWeight'}}: {{carriedWeight}} {{localize 'BASICFANTASYRPG.PoundsAbbr'}} + + {{localize 'BASICFANTASYRPG.Add'}} + + + {{#each gear as |item id|}} + + + + + + {{item.system.quantity.value}} {{item.name}} + + + {{item.system.weight.value}} {{localize 'BASICFANTASYRPG.PoundsAbbr'}} + {{item.system.price.value}} + + + + + + + {{/each}} + + \ No newline at end of file diff --git a/templates/actor/parts/spells.hbs b/templates/actor/parts/spells.hbs new file mode 100644 index 0000000..07a4994 --- /dev/null +++ b/templates/actor/parts/spells.hbs @@ -0,0 +1,48 @@ + + + + {{#each data.spellsPerLevel.value as |spellsPerLevel level|}} + + {{level}} + + + + + {{/each}} + + + + + {{#each spells as |spells spellLevel|}} + + {{localize 'BASICFANTASYRPG.SpellLevel'}} {{spellLevel}} {{localize 'BASICFANTASYRPG.Spells'}} + {{localize 'BASICFANTASYRPG.Prepared'}} + + {{localize 'BASICFANTASYRPG.Add'}} + + + {{#each spells as |item id|}} + + + + + + {{item.name}} + + + + {{item.system.prepared.value}} + + + + + + + + {{/each}} + {{/each}} + + + \ No newline at end of file