diff --git a/assets/images/Sans titre.png b/assets/images/Sans titre.png deleted file mode 100644 index 8610bf2..0000000 Binary files a/assets/images/Sans titre.png and /dev/null differ diff --git a/assets/images/VERMINE 2047.webp b/assets/images/VERMINE 2047.webp new file mode 100644 index 0000000..a018d21 Binary files /dev/null and b/assets/images/VERMINE 2047.webp differ diff --git a/assets/images/parchment.jpg b/assets/images/parchment.jpg deleted file mode 100644 index 112b20d..0000000 Binary files a/assets/images/parchment.jpg and /dev/null differ diff --git a/assets/images/totem-logo.jpg b/assets/images/totem-logo.jpg deleted file mode 100644 index a570275..0000000 Binary files a/assets/images/totem-logo.jpg and /dev/null differ diff --git a/assets/images/recto.png b/assets/images/ui/box_background.png similarity index 100% rename from assets/images/recto.png rename to assets/images/ui/box_background.png diff --git a/module/documents/character.mjs b/module/documents/character.mjs new file mode 100644 index 0000000..82877dc --- /dev/null +++ b/module/documents/character.mjs @@ -0,0 +1,89 @@ +import { TotemActor } from "./actor.mjs"; +/** + * @extends {TotemActor} + */ +export class TotemCharacter extends TotemActor { + + /** @override */ + prepareData() { + // Prepare data for the actor. Calling the super version of this executes + // the following, in order: data reset (to clear active effects), + // prepareBaseData(), prepareEmbeddedDocuments() (including active effects), + // prepareDerivedData(). + super.prepareData(); + } + + /** @override */ + prepareBaseData() { + // Data modifications in this step occur before processing embedded + // documents or derived data. + } + + /** + * @override + * Augment the basic actor data with additional dynamic data. Typically, + * you'll want to handle most of your calculated/derived data in this step. + * Data calculated in this step should generally not exist in template.json + * (such as ability modifiers rather than ability scores) and should be + * available both inside and outside of character sheets (such as if an actor + * is queried and has a roll executed directly from it). + */ + prepareDerivedData() { + const actorData = this; + const systemData = actorData.system; + const flags = actorData.flags.totem || {}; + + // Make separate methods for each Actor type (character, npc, etc.) to keep + // things organized. + this._prepareCharacterData(actorData); + } + + /** + * Prepare Character type specific data + */ + _prepareCharacterData(actorData) { + if (actorData.type !== 'character') return; + + // Make modifications to data here. For example: + const systemData = actorData.system; + + // Loop through ability scores, and add their modifiers to our sheet output. + for (let [key, ability] of Object.entries(systemData.abilities)) { + // Calculate the modifier using d20 rules. + ability.mod = Math.floor((ability.value - 10) / 2); + } + } + + /** + * Override getRollData() that's supplied to rolls. + */ + getRollData() { + const data = super.getRollData(); + + // Prepare character roll data. + this._getCharacterRollData(data); + + return data; + } + + /** + * Prepare character roll data. + */ + _getCharacterRollData(data) { + if (this.type !== 'character') return; + + // Copy the ability scores to the top level, so that rolls can use + // formulas like `@str.mod + 4`. + if (data.abilities) { + for (let [k, v] of Object.entries(data.abilities)) { + data[k] = foundry.utils.deepClone(v); + } + } + + // Add level for easier access, or fall back to 0. + if (data.attributes.level) { + data.lvl = data.attributes.level.value ?? 0; + } + } + +} \ No newline at end of file diff --git a/module/documents/creature.mjs b/module/documents/creature.mjs new file mode 100644 index 0000000..383a6b9 --- /dev/null +++ b/module/documents/creature.mjs @@ -0,0 +1,76 @@ +import { TotemActor } from "./actor.mjs"; +/** + * Extend the base Actor document by defining a custom roll data structure which is ideal for the Simple system. + * @extends {TotemActor} + */ +export class TotemCreature extends TotemActor { + + /** @override */ + prepareData() { + // Prepare data for the actor. Calling the super version of this executes + // the following, in order: data reset (to clear active effects), + // prepareBaseData(), prepareEmbeddedDocuments() (including active effects), + // prepareDerivedData(). + super.prepareData(); + } + + /** @override */ + prepareBaseData() { + // Data modifications in this step occur before processing embedded + // documents or derived data. + } + + /** + * @override + * Augment the basic actor data with additional dynamic data. Typically, + * you'll want to handle most of your calculated/derived data in this step. + * Data calculated in this step should generally not exist in template.json + * (such as ability modifiers rather than ability scores) and should be + * available both inside and outside of character sheets (such as if an actor + * is queried and has a roll executed directly from it). + */ + prepareDerivedData() { + const actorData = this; + const systemData = actorData.system; + const flags = actorData.flags.totem || {}; + + // Make separate methods for each Actor type (character, npc, etc.) to keep + // things organized. + this._prepareNpcData(actorData); + } + + + + /** + * Prepare NPC type specific data. + */ + _prepareNpcData(actorData) { + if (actorData.type !== 'npc') return; + + // Make modifications to data here. For example: + const systemData = actorData.system; + systemData.xp = (systemData.cr * systemData.cr) * 100; + } + + /** + * Override getRollData() that's supplied to rolls. + */ + getRollData() { + const data = super.getRollData(); + + // Prepare character roll data. + this._getNpcRollData(data); + + return data; + } + + /** + * Prepare NPC roll data. + */ + _getNpcRollData(data) { + if (this.type !== 'creature') return; + + // Process additional NPC data here. + } + +} \ No newline at end of file diff --git a/module/documents/npc.mjs b/module/documents/npc.mjs new file mode 100644 index 0000000..ae5f97c --- /dev/null +++ b/module/documents/npc.mjs @@ -0,0 +1,76 @@ +import { TotemActor } from "./actor.mjs"; +/** + * Extend the base Actor document by defining a custom roll data structure which is ideal for the Simple system. + * @extends {TotemActor} + */ +export class TotemNpc extends TotemActor { + + /** @override */ + prepareData() { + // Prepare data for the actor. Calling the super version of this executes + // the following, in order: data reset (to clear active effects), + // prepareBaseData(), prepareEmbeddedDocuments() (including active effects), + // prepareDerivedData(). + super.prepareData(); + } + + /** @override */ + prepareBaseData() { + // Data modifications in this step occur before processing embedded + // documents or derived data. + } + + /** + * @override + * Augment the basic actor data with additional dynamic data. Typically, + * you'll want to handle most of your calculated/derived data in this step. + * Data calculated in this step should generally not exist in template.json + * (such as ability modifiers rather than ability scores) and should be + * available both inside and outside of character sheets (such as if an actor + * is queried and has a roll executed directly from it). + */ + prepareDerivedData() { + const actorData = this; + const systemData = actorData.system; + const flags = actorData.flags.totem || {}; + + // Make separate methods for each Actor type (character, npc, etc.) to keep + // things organized. + this._prepareNpcData(actorData); + } + + + + /** + * Prepare NPC type specific data. + */ + _prepareNpcData(actorData) { + if (actorData.type !== 'npc') return; + + // Make modifications to data here. For example: + const systemData = actorData.system; + systemData.xp = (systemData.cr * systemData.cr) * 100; + } + + /** + * Override getRollData() that's supplied to rolls. + */ + getRollData() { + const data = super.getRollData(); + + // Prepare character roll data. + this._getNpcRollData(data); + + return data; + } + + /** + * Prepare NPC roll data. + */ + _getNpcRollData(data) { + if (this.type !== 'npc') return; + + // Process additional NPC data here. + } + +} \ No newline at end of file diff --git a/module/helpers/config.mjs b/module/helpers/config.mjs deleted file mode 100644 index 95d8225..0000000 --- a/module/helpers/config.mjs +++ /dev/null @@ -1,23 +0,0 @@ -export const TOTEM = {}; - -/** - * The set of Ability Scores used within the sytem. - * @type {Object} - */ - TOTEM.abilities = { - "str": "TOTEM.AbilityStr", - "dex": "TOTEM.AbilityDex", - "con": "TOTEM.AbilityCon", - "int": "TOTEM.AbilityInt", - "wis": "TOTEM.AbilityWis", - "cha": "TOTEM.AbilityCha" -}; - -TOTEM.abilityAbbreviations = { - "str": "TOTEM.AbilityStrAbbr", - "dex": "TOTEM.AbilityDexAbbr", - "con": "TOTEM.AbilityConAbbr", - "int": "TOTEM.AbilityIntAbbr", - "wis": "TOTEM.AbilityWisAbbr", - "cha": "TOTEM.AbilityChaAbbr" -}; \ No newline at end of file diff --git a/module/helpers/effects.mjs b/module/helpers/effects.mjs deleted file mode 100644 index 8052b39..0000000 --- a/module/helpers/effects.mjs +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Manage Active Effect instances through the Actor Sheet via effect control buttons. - * @param {MouseEvent} event The left-click event on the effect control - * @param {Actor|Item} owner The owning document which manages this effect - */ - export function onManageActiveEffect(event, owner) { - event.preventDefault(); - const a = event.currentTarget; - const li = a.closest("li"); - const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null; - switch ( a.dataset.action ) { - case "create": - return owner.createEmbeddedDocuments("ActiveEffect", [{ - label: "New Effect", - icon: "icons/svg/aura.svg", - origin: owner.uuid, - "duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined, - disabled: li.dataset.effectType === "inactive" - }]); - case "edit": - return effect.sheet.render(true); - case "delete": - return effect.delete(); - case "toggle": - return effect.update({disabled: !effect.disabled}); - } -} - -/** - * Prepare the data structure for Active Effects which are currently applied to an Actor or Item. - * @param {ActiveEffect[]} effects The array of Active Effect instances to prepare sheet data for - * @return {object} Data for rendering - */ -export function prepareActiveEffectCategories(effects) { - - // Define effect header categories - const categories = { - temporary: { - type: "temporary", - label: "Temporary Effects", - effects: [] - }, - passive: { - type: "passive", - label: "Passive Effects", - effects: [] - }, - inactive: { - type: "inactive", - label: "Inactive Effects", - effects: [] - } - }; - - // Iterate over active effects, classifying them into categories - for ( let e of effects ) { - e._getSourceName(); // Trigger a lookup for the source name - if ( e.disabled ) categories.inactive.effects.push(e); - else if ( e.isTemporary ) categories.temporary.effects.push(e); - else categories.passive.effects.push(e); - } - return categories; -} \ No newline at end of file diff --git a/module/helpers/templates.mjs b/module/helpers/templates.mjs deleted file mode 100644 index 1e19ab0..0000000 --- a/module/helpers/templates.mjs +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Define a set of template paths to pre-load - * Pre-loaded templates are compiled and cached for fast access when rendering - * @return {Promise} - */ - export const preloadHandlebarsTemplates = async function() { - return loadTemplates([ - - // Actor partials. - "systems/totem/templates/actor/parts/actor-features.html", - "systems/totem/templates/actor/parts/actor-items.html", - "systems/totem/templates/actor/parts/actor-spells.html", - "systems/totem/templates/actor/parts/actor-effects.html", - ]); -}; diff --git a/module/sheets/actor-sheet.mjs b/module/sheets/actor-sheet.mjs index 5285cbd..53147b9 100644 --- a/module/sheets/actor-sheet.mjs +++ b/module/sheets/actor-sheet.mjs @@ -1,4 +1,4 @@ -import {onManageActiveEffect, prepareActiveEffectCategories} from "../helpers/effects.mjs"; +import {onManageActiveEffect, prepareActiveEffectCategories} from "../system/effects.mjs"; /** * Extend the basic ActorSheet with some very simple modifications diff --git a/module/sheets/character-sheet.mjs b/module/sheets/character-sheet.mjs new file mode 100644 index 0000000..b3d6006 --- /dev/null +++ b/module/sheets/character-sheet.mjs @@ -0,0 +1,231 @@ +import {onManageActiveEffect, prepareActiveEffectCategories} from "../system/effects.mjs"; +import { TotemActorSheet } from "./actor-sheet.mjs"; + +/** + * Extend the basic ActorSheet with some very simple modifications + * @extends {TotemActorSheet} + */ +export class TotemCharacterSheet extends TotemActorSheet { + + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + classes: ["totem", "sheet", "actor"], + template: "systems/totem/templates/actor/actor-sheet.html", + width: 600, + height: 600, + tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "features" }] + }); + } + + /** @override */ + get template() { + return `systems/totem/templates/actor/actor-${this.actor.type}-sheet.html`; + } + + /* -------------------------------------------- */ + + /** @override */ + getData() { + // Retrieve the data structure from the base sheet. You can inspect or log + // the context variable to see the structure, but some key properties for + // sheets are the actor object, the data object, whether or not it's + // editable, the items array, and the effects array. + const context = super.getData(); + + // Use a safe clone of the actor data for further operations. + const actorData = this.actor.toObject(false); + + // Add the actor's data to context.data for easier access, as well as flags. + context.system = actorData.system; + context.flags = actorData.flags; + context.config = CONFIG.TOTEM; + + // Prepare character data and items. + if (actorData.type == 'character') { + this._prepareItems(context); + this._prepareCharacterData(context); + } + + // Prepare NPC data and items. + if (actorData.type == 'npc') { + this._prepareItems(context); + } + + // Add roll data for TinyMCE editors. + context.rollData = context.actor.getRollData(); + + // Prepare active effects + context.effects = prepareActiveEffectCategories(this.actor.effects); + + return context; + } + + /** + * Organize and classify Items for Character sheets. + * + * @param {Object} actorData The actor to prepare. + * + * @return {undefined} + */ + _prepareCharacterData(context) { + // Handle ability scores. + for (let [k, v] of Object.entries(context.system.abilities)) { + v.label = game.i18n.localize(context.system.abilities[k].label) ?? k; + } + } + + /** + * Organize and classify Items for Character sheets. + * + * @param {Object} actorData The actor to prepare. + * + * @return {undefined} + */ + _prepareItems(context) { + // Initialize containers. + const gear = []; + const features = []; + const spells = { + 0: [], + 1: [], + 2: [], + 3: [], + 4: [], + 5: [], + 6: [], + 7: [], + 8: [], + 9: [] + }; + + // 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); + } + // Append to features. + else if (i.type === 'feature') { + features.push(i); + } + // Append to spells. + else if (i.type === 'spell') { + if (i.system.spellLevel != undefined) { + spells[i.system.spellLevel].push(i); + } + } + } + + // Assign and return + context.gear = gear; + context.features = features; + context.spells = spells; + } + + /* -------------------------------------------- */ + + /** @override */ + activateListeners(html) { + super.activateListeners(html); + + // Render the item sheet for viewing/editing prior to the editable check. + html.find('.item-edit').click(ev => { + const li = $(ev.currentTarget).parents(".item"); + const item = this.actor.items.get(li.data("itemId")); + item.sheet.render(true); + }); + + // ------------------------------------------------------------- + // Everything below here is only needed if the sheet is editable + if (!this.isEditable) return; + + // Add Inventory Item + html.find('.item-create').click(this._onItemCreate.bind(this)); + + // Delete Inventory Item + html.find('.item-delete').click(ev => { + const li = $(ev.currentTarget).parents(".item"); + const item = this.actor.items.get(li.data("itemId")); + item.delete(); + li.slideUp(200, () => this.render(false)); + }); + + // Active Effect management + html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.actor)); + + // Rollable abilities. + html.find('.rollable').click(this._onRoll.bind(this)); + + // Drag events for macros. + if (this.actor.isOwner) { + let handler = ev => this._onDragStart(ev); + html.find('li.item').each((i, li) => { + if (li.classList.contains("inventory-header")) return; + li.setAttribute("draggable", true); + li.addEventListener("dragstart", handler, false); + }); + } + } + + /** + * Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset + * @param {Event} event The originating click event + * @private + */ + async _onItemCreate(event) { + event.preventDefault(); + const header = event.currentTarget; + // Get the type of item to create. + const type = header.dataset.type; + // Grab any data associated with this control. + const data = duplicate(header.dataset); + // Initialize a default name. + const name = `New ${type.capitalize()}`; + // Prepare the item object. + const itemData = { + name: name, + type: type, + system: data + }; + // Remove the type from the dataset since it's in the itemData.type prop. + delete itemData.system["type"]; + + // Finally, create the item! + return await Item.create(itemData, {parent: this.actor}); + } + + /** + * Handle clickable rolls. + * @param {Event} event The originating click event + * @private + */ + _onRoll(event) { + event.preventDefault(); + const element = event.currentTarget; + const dataset = element.dataset; + + // Handle item rolls. + if (dataset.rollType) { + if (dataset.rollType == 'item') { + const itemId = element.closest('.item').dataset.itemId; + const item = this.actor.items.get(itemId); + if (item) return item.roll(); + } + } + + // Handle rolls that supply the formula directly. + if (dataset.roll) { + let label = dataset.label ? `[ability] ${dataset.label}` : ''; + let roll = new Roll(dataset.roll, this.actor.getRollData()); + roll.toMessage({ + speaker: ChatMessage.getSpeaker({ actor: this.actor }), + flavor: label, + rollMode: game.settings.get('core', 'rollMode'), + }); + return roll; + } + } + +} diff --git a/module/sheets/creature-sheet.mjs b/module/sheets/creature-sheet.mjs new file mode 100644 index 0000000..3d4e985 --- /dev/null +++ b/module/sheets/creature-sheet.mjs @@ -0,0 +1,231 @@ +import {onManageActiveEffect, prepareActiveEffectCategories} from "../system/effects.mjs"; +import { TotemActorSheet } from "./actor-sheet.mjs"; + +/** + * Extend the basic ActorSheet with some very simple modifications + * @extends {ActorSheet} + */ +export class TotemActorSheet extends ActorSheet { + + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + classes: ["totem", "sheet", "actor"], + template: "systems/totem/templates/actor/actor-sheet.html", + width: 600, + height: 600, + tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "features" }] + }); + } + + /** @override */ + get template() { + return `systems/totem/templates/actor/actor-${this.actor.type}-sheet.html`; + } + + /* -------------------------------------------- */ + + /** @override */ + getData() { + // Retrieve the data structure from the base sheet. You can inspect or log + // the context variable to see the structure, but some key properties for + // sheets are the actor object, the data object, whether or not it's + // editable, the items array, and the effects array. + const context = super.getData(); + + // Use a safe clone of the actor data for further operations. + const actorData = this.actor.toObject(false); + + // Add the actor's data to context.data for easier access, as well as flags. + context.system = actorData.system; + context.flags = actorData.flags; + context.config = CONFIG.TOTEM; + + // Prepare character data and items. + if (actorData.type == 'character') { + this._prepareItems(context); + this._prepareCharacterData(context); + } + + // Prepare NPC data and items. + if (actorData.type == 'npc') { + this._prepareItems(context); + } + + // Add roll data for TinyMCE editors. + context.rollData = context.actor.getRollData(); + + // Prepare active effects + context.effects = prepareActiveEffectCategories(this.actor.effects); + + return context; + } + + /** + * Organize and classify Items for Character sheets. + * + * @param {Object} actorData The actor to prepare. + * + * @return {undefined} + */ + _prepareCharacterData(context) { + // Handle ability scores. + for (let [k, v] of Object.entries(context.system.abilities)) { + v.label = game.i18n.localize(context.system.abilities[k].label) ?? k; + } + } + + /** + * Organize and classify Items for Character sheets. + * + * @param {Object} actorData The actor to prepare. + * + * @return {undefined} + */ + _prepareItems(context) { + // Initialize containers. + const gear = []; + const features = []; + const spells = { + 0: [], + 1: [], + 2: [], + 3: [], + 4: [], + 5: [], + 6: [], + 7: [], + 8: [], + 9: [] + }; + + // 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); + } + // Append to features. + else if (i.type === 'feature') { + features.push(i); + } + // Append to spells. + else if (i.type === 'spell') { + if (i.system.spellLevel != undefined) { + spells[i.system.spellLevel].push(i); + } + } + } + + // Assign and return + context.gear = gear; + context.features = features; + context.spells = spells; + } + + /* -------------------------------------------- */ + + /** @override */ + activateListeners(html) { + super.activateListeners(html); + + // Render the item sheet for viewing/editing prior to the editable check. + html.find('.item-edit').click(ev => { + const li = $(ev.currentTarget).parents(".item"); + const item = this.actor.items.get(li.data("itemId")); + item.sheet.render(true); + }); + + // ------------------------------------------------------------- + // Everything below here is only needed if the sheet is editable + if (!this.isEditable) return; + + // Add Inventory Item + html.find('.item-create').click(this._onItemCreate.bind(this)); + + // Delete Inventory Item + html.find('.item-delete').click(ev => { + const li = $(ev.currentTarget).parents(".item"); + const item = this.actor.items.get(li.data("itemId")); + item.delete(); + li.slideUp(200, () => this.render(false)); + }); + + // Active Effect management + html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.actor)); + + // Rollable abilities. + html.find('.rollable').click(this._onRoll.bind(this)); + + // Drag events for macros. + if (this.actor.isOwner) { + let handler = ev => this._onDragStart(ev); + html.find('li.item').each((i, li) => { + if (li.classList.contains("inventory-header")) return; + li.setAttribute("draggable", true); + li.addEventListener("dragstart", handler, false); + }); + } + } + + /** + * Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset + * @param {Event} event The originating click event + * @private + */ + async _onItemCreate(event) { + event.preventDefault(); + const header = event.currentTarget; + // Get the type of item to create. + const type = header.dataset.type; + // Grab any data associated with this control. + const data = duplicate(header.dataset); + // Initialize a default name. + const name = `New ${type.capitalize()}`; + // Prepare the item object. + const itemData = { + name: name, + type: type, + system: data + }; + // Remove the type from the dataset since it's in the itemData.type prop. + delete itemData.system["type"]; + + // Finally, create the item! + return await Item.create(itemData, {parent: this.actor}); + } + + /** + * Handle clickable rolls. + * @param {Event} event The originating click event + * @private + */ + _onRoll(event) { + event.preventDefault(); + const element = event.currentTarget; + const dataset = element.dataset; + + // Handle item rolls. + if (dataset.rollType) { + if (dataset.rollType == 'item') { + const itemId = element.closest('.item').dataset.itemId; + const item = this.actor.items.get(itemId); + if (item) return item.roll(); + } + } + + // Handle rolls that supply the formula directly. + if (dataset.roll) { + let label = dataset.label ? `[ability] ${dataset.label}` : ''; + let roll = new Roll(dataset.roll, this.actor.getRollData()); + roll.toMessage({ + speaker: ChatMessage.getSpeaker({ actor: this.actor }), + flavor: label, + rollMode: game.settings.get('core', 'rollMode'), + }); + return roll; + } + } + +} diff --git a/module/sheets/npc-sheet.mjs b/module/sheets/npc-sheet.mjs new file mode 100644 index 0000000..26fd747 --- /dev/null +++ b/module/sheets/npc-sheet.mjs @@ -0,0 +1,231 @@ +import {onManageActiveEffect, prepareActiveEffectCategories} from "../system/effects.mjs"; +import { TotemActorSheet } from "./actor-sheet.mjs"; + +/** + * Extend the basic ActorSheet with some very simple modifications + * @extends {TotemActorSheet} + */ +export class TotemNpcSheet extends TotemActorSheet { + + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + classes: ["totem", "sheet", "actor"], + template: "systems/totem/templates/actor/actor-sheet.html", + width: 600, + height: 600, + tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "features" }] + }); + } + + /** @override */ + get template() { + return `systems/totem/templates/actor/actor-${this.actor.type}-sheet.html`; + } + + /* -------------------------------------------- */ + + /** @override */ + getData() { + // Retrieve the data structure from the base sheet. You can inspect or log + // the context variable to see the structure, but some key properties for + // sheets are the actor object, the data object, whether or not it's + // editable, the items array, and the effects array. + const context = super.getData(); + + // Use a safe clone of the actor data for further operations. + const actorData = this.actor.toObject(false); + + // Add the actor's data to context.data for easier access, as well as flags. + context.system = actorData.system; + context.flags = actorData.flags; + context.config = CONFIG.TOTEM; + + // Prepare character data and items. + if (actorData.type == 'character') { + this._prepareItems(context); + this._prepareCharacterData(context); + } + + // Prepare NPC data and items. + if (actorData.type == 'npc') { + this._prepareItems(context); + } + + // Add roll data for TinyMCE editors. + context.rollData = context.actor.getRollData(); + + // Prepare active effects + context.effects = prepareActiveEffectCategories(this.actor.effects); + + return context; + } + + /** + * Organize and classify Items for Character sheets. + * + * @param {Object} actorData The actor to prepare. + * + * @return {undefined} + */ + _prepareCharacterData(context) { + // Handle ability scores. + for (let [k, v] of Object.entries(context.system.abilities)) { + v.label = game.i18n.localize(context.system.abilities[k].label) ?? k; + } + } + + /** + * Organize and classify Items for Character sheets. + * + * @param {Object} actorData The actor to prepare. + * + * @return {undefined} + */ + _prepareItems(context) { + // Initialize containers. + const gear = []; + const features = []; + const spells = { + 0: [], + 1: [], + 2: [], + 3: [], + 4: [], + 5: [], + 6: [], + 7: [], + 8: [], + 9: [] + }; + + // 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); + } + // Append to features. + else if (i.type === 'feature') { + features.push(i); + } + // Append to spells. + else if (i.type === 'spell') { + if (i.system.spellLevel != undefined) { + spells[i.system.spellLevel].push(i); + } + } + } + + // Assign and return + context.gear = gear; + context.features = features; + context.spells = spells; + } + + /* -------------------------------------------- */ + + /** @override */ + activateListeners(html) { + super.activateListeners(html); + + // Render the item sheet for viewing/editing prior to the editable check. + html.find('.item-edit').click(ev => { + const li = $(ev.currentTarget).parents(".item"); + const item = this.actor.items.get(li.data("itemId")); + item.sheet.render(true); + }); + + // ------------------------------------------------------------- + // Everything below here is only needed if the sheet is editable + if (!this.isEditable) return; + + // Add Inventory Item + html.find('.item-create').click(this._onItemCreate.bind(this)); + + // Delete Inventory Item + html.find('.item-delete').click(ev => { + const li = $(ev.currentTarget).parents(".item"); + const item = this.actor.items.get(li.data("itemId")); + item.delete(); + li.slideUp(200, () => this.render(false)); + }); + + // Active Effect management + html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.actor)); + + // Rollable abilities. + html.find('.rollable').click(this._onRoll.bind(this)); + + // Drag events for macros. + if (this.actor.isOwner) { + let handler = ev => this._onDragStart(ev); + html.find('li.item').each((i, li) => { + if (li.classList.contains("inventory-header")) return; + li.setAttribute("draggable", true); + li.addEventListener("dragstart", handler, false); + }); + } + } + + /** + * Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset + * @param {Event} event The originating click event + * @private + */ + async _onItemCreate(event) { + event.preventDefault(); + const header = event.currentTarget; + // Get the type of item to create. + const type = header.dataset.type; + // Grab any data associated with this control. + const data = duplicate(header.dataset); + // Initialize a default name. + const name = `New ${type.capitalize()}`; + // Prepare the item object. + const itemData = { + name: name, + type: type, + system: data + }; + // Remove the type from the dataset since it's in the itemData.type prop. + delete itemData.system["type"]; + + // Finally, create the item! + return await Item.create(itemData, {parent: this.actor}); + } + + /** + * Handle clickable rolls. + * @param {Event} event The originating click event + * @private + */ + _onRoll(event) { + event.preventDefault(); + const element = event.currentTarget; + const dataset = element.dataset; + + // Handle item rolls. + if (dataset.rollType) { + if (dataset.rollType == 'item') { + const itemId = element.closest('.item').dataset.itemId; + const item = this.actor.items.get(itemId); + if (item) return item.roll(); + } + } + + // Handle rolls that supply the formula directly. + if (dataset.roll) { + let label = dataset.label ? `[ability] ${dataset.label}` : ''; + let roll = new Roll(dataset.roll, this.actor.getRollData()); + roll.toMessage({ + speaker: ChatMessage.getSpeaker({ actor: this.actor }), + flavor: label, + rollMode: game.settings.get('core', 'rollMode'), + }); + return roll; + } + } + +} diff --git a/module/system/config.mjs b/module/system/config.mjs index a785687..c0be834 100644 --- a/module/system/config.mjs +++ b/module/system/config.mjs @@ -49,7 +49,7 @@ TOTEM.skillCategories = { "animal": { "label":"TOTEM.skillCategory.animal" }, - "machine": { + "tool": { "label":"TOTEM.skillCategory.machine" }, "weapon": { @@ -58,7 +58,7 @@ TOTEM.skillCategories = { "survival": { "label":"TOTEM.skillCategory.survival" }, - "earth": { + "world": { "label":"TOTEM.skillCategory.earth" } } \ No newline at end of file diff --git a/module/system/handlebars-manager.mjs b/module/system/handlebars-manager.mjs new file mode 100644 index 0000000..e906482 --- /dev/null +++ b/module/system/handlebars-manager.mjs @@ -0,0 +1,43 @@ +/** + * Define a set of template paths to pre-load + * Pre-loaded templates are compiled and cached for fast access when rendering + * @return {Promise} + */ + export const preloadHandlebarsTemplates = async function() { + return loadTemplates([ + + // Actor partials. + "systems/totem/templates/actor/parts/actor-traits.html", + "systems/totem/templates/actor/parts/actor-background.html", + "systems/totem/templates/actor/parts/actor-skills.html", + "systems/totem/templates/actor/parts/actor-items.html", + "systems/totem/templates/actor/parts/actor-effects.html", + ]); +}; + + +export const registerHandlebarsHelpers = function () { + Handlebars.registerHelper('concat', (...args) => args.slice(0, -1).join('')); + Handlebars.registerHelper('lower', e => e.toLocaleLowerCase()); + + Handlebars.registerHelper('toLowerCase', function(str) { + return str.toLowerCase(); + }); + + // Ifis not equal + Handlebars.registerHelper('ifne', function (v1, v2, options) { + if (v1 !== v2) return options.fn(this); + else return options.inverse(this); + }); + + // if equal + Handlebars.registerHelper('ife', function (v1, v2, options) { + if (v1 === v2) return options.fn(this); + else return options.inverse(this); + }); + // if equal + Handlebars.registerHelper('ifgt', function (v1, v2, options) { + if (v1 > v2) return options.fn(this); + else return options.inverse(this); + }); +} \ No newline at end of file diff --git a/module/system/helpers.mjs b/module/system/helpers.mjs deleted file mode 100644 index f3fa509..0000000 --- a/module/system/helpers.mjs +++ /dev/null @@ -1,25 +0,0 @@ -export const registerHandlebarsHelpers = function () { - Handlebars.registerHelper('concat', (...args) => args.slice(0, -1).join('')); - Handlebars.registerHelper('lower', e => e.toLocaleLowerCase()); - - Handlebars.registerHelper('toLowerCase', function(str) { - return str.toLowerCase(); - }); - - // Ifis not equal - Handlebars.registerHelper('ifne', function (v1, v2, options) { - if (v1 !== v2) return options.fn(this); - else return options.inverse(this); - }); - - // if equal - Handlebars.registerHelper('ife', function (v1, v2, options) { - if (v1 === v2) return options.fn(this); - else return options.inverse(this); - }); - // if equal - Handlebars.registerHelper('ifgt', function (v1, v2, options) { - if (v1 > v2) return options.fn(this); - else return options.inverse(this); - }); -} \ No newline at end of file diff --git a/module/system/templates.mjs b/module/system/templates.mjs deleted file mode 100644 index 9cf5964..0000000 --- a/module/system/templates.mjs +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Define a set of template paths to pre-load - * Pre-loaded templates are compiled and cached for fast access when rendering - * @return {Promise} - */ - export const preloadHandlebarsTemplates = async function() { - return loadTemplates([ - - // Actor partials. - "systems/totem/templates/actor/parts/actor-traits.html", - "systems/totem/templates/actor/parts/actor-background.html", - "systems/totem/templates/actor/parts/actor-skills.html", - "systems/totem/templates/actor/parts/actor-items.html", - "systems/totem/templates/actor/parts/actor-cephalie.html", - "systems/totem/templates/actor/parts/actor-effects.html", - ]); -}; diff --git a/module/totem.mjs b/module/totem.mjs index da1dbc3..96163de 100644 --- a/module/totem.mjs +++ b/module/totem.mjs @@ -1,15 +1,21 @@ -import { registerHandlebarsHelpers } from "./system/helpers.mjs"; import { registerHooks } from "./system/hooks.mjs"; import { registerSettings } from "./system/settings.mjs"; // Import document classes. import { TotemActor } from "./documents/actor.mjs"; +import { TotemCharacter } from "./documents/character.mjs"; +import { TotemNpc } from "./documents/npc.mjs"; +import { TotemCreature } from "./documents/creature.mjs"; + +import { TotemCharacterSheet } from "./sheets/character-sheet.mjs"; +import { TotemNpcSheet } from "./sheets/npc-sheet.mjs"; +import { TotemCreatureSheet } from "./sheets/creature-sheet.mjs"; + import { TotemItem } from "./documents/item.mjs"; -// Import sheet classes. -import { TotemActorSheet } from "./sheets/actor-sheet.mjs"; import { TotemItemSheet } from "./sheets/item-sheet.mjs"; + // Import helper/utility classes and constants. -import { preloadHandlebarsTemplates } from "./helpers/templates.mjs"; +import { preloadHandlebarsTemplates, registerHandlebarsHelpers } from "./system/handlebars-manager.mjs"; import { TOTEM } from "./system/config.mjs"; /* -------------------------------------------- */ @@ -21,7 +27,9 @@ Hooks.once('init', async function() { // Add utility classes to the global game object so that they're more easily // accessible in global contexts. game.totem = { - TotemActor, + TotemCharacter, + TotemNpc, + TotemCreature, TotemItem, rollItemMacro }; @@ -34,7 +42,7 @@ Hooks.once('init', async function() { * @type {String} */ CONFIG.Combat.initiative = { - formula: "1d20 + @abilities.dex.mod", + formula: "1d10 + @abilities.dex.mod", decimals: 2 }; @@ -44,7 +52,20 @@ Hooks.once('init', async function() { // Register sheet application classes Actors.unregisterSheet("core", ActorSheet); - Actors.registerSheet("totem", TotemActorSheet, { makeDefault: true }); + Actors.registerSheet('totem', TotemCharacterSheet, { + types: ['character'], + makeDefault: true, + }); + + Actors.registerSheet('totem', TotemNpcSheet, { + types: ['npc'], + makeDefault: true, + }); + + Actors.registerSheet('totem', TotemCreatureSheet, { + types: ['creature'], + makeDefault: true, + }); // Register vehicle Sheet Items.unregisterSheet("core", ItemSheet); Items.registerSheet("totem", TotemItemSheet, { makeDefault: true }); diff --git a/template.json b/template.json index 18ed818..6899e98 100644 --- a/template.json +++ b/template.json @@ -187,7 +187,7 @@ "value": 0, "min": 0, "max": 5, - "category": "machine", + "category": "tool", "rarity":2 }, "diy": { @@ -195,7 +195,7 @@ "value": 0, "min": 0, "max": 5, - "category": "machine", + "category": "tool", "rarity":0 }, "mecanical": { @@ -203,7 +203,7 @@ "value": 0, "min": 0, "max": 5, - "category": "machine", + "category": "tool", "rarity":2 }, "driving": { @@ -211,7 +211,7 @@ "value": 0, "min": 0, "max": 5, - "category": "machine", + "category": "tool", "rarity":1 }, "technology": { @@ -219,7 +219,7 @@ "value": 0, "min": 0, "max": 5, - "category": "machine", + "category": "tool", "rarity":2 }, "firearms": { @@ -307,7 +307,7 @@ "value": 0, "min": 0, "max": 5, - "category": "earth", + "category": "world", "rarity":0 }, "flora": { @@ -315,7 +315,7 @@ "value": 0, "min": 0, "max": 5, - "category": "earth", + "category": "world", "rarity":0 }, "road": { @@ -323,7 +323,7 @@ "value": 0, "min": 0, "max": 5, - "category": "earth", + "category": "world", "rarity":0 }, "toxics": { @@ -331,7 +331,7 @@ "value": 0, "min": 0, "max": 5, - "category": "earth", + "category": "world", "rarity":0 }, "remains": { @@ -339,7 +339,7 @@ "value": 0, "min": 0, "max": 5, - "category": "earth", + "category": "world", "rarity":0 } } @@ -394,7 +394,7 @@ "templates": ["base"], "quantity": 1, "weight": 0, - "formula": "d20 + @str.mod + ceil(@lvl / 2)" + "formula": "d10 + @str.mod + ceil(@lvl / 2)" }, "feature": { "templates": ["base"] diff --git a/templates/actor/actor-character-sheet.html b/templates/actor/actor-character-sheet.html index f8ec4ed..5a1d6fc 100644 --- a/templates/actor/actor-character-sheet.html +++ b/templates/actor/actor-character-sheet.html @@ -62,7 +62,7 @@ {{#ife skill.category sckey }}
- +
{{/ife}}