diff --git a/dev/hooks/getHeaderControlsActorSheetV2.mjs b/dev/hooks/getHeaderControlsActorSheetV2.mjs new file mode 100644 index 0000000..07c78eb --- /dev/null +++ b/dev/hooks/getHeaderControlsActorSheetV2.mjs @@ -0,0 +1,23 @@ +/* +This hook exists to be able to change all of the actor-sheets to allow +them to have dev-mode controls in their header that are useful for +testing purposes. (This is particularly useful for testing embedded +items that are only allowed to exist on specific actor types) +*/ +Hooks.on(`getHeaderControlsActorSheetV2`, (app, controls) => { + if (!game.settings.get(`ripcrypt`, `devMode`)) { return } + + controls.push( + { + icon: `fa-solid fa-terminal`, + label: `Embed New Item (DEV)`, + action: `createItem`, + }, + { + label: `Make Global Reference`, + onClick: () => { + globalThis._doc = app.actor; + }, + }, + ); +}); diff --git a/module/hooks/hotReload.mjs b/dev/hooks/hotReload.mjs similarity index 87% rename from module/hooks/hotReload.mjs rename to dev/hooks/hotReload.mjs index 7ef784f..99f6a55 100644 --- a/module/hooks/hotReload.mjs +++ b/dev/hooks/hotReload.mjs @@ -1,4 +1,4 @@ -import { Logger } from "../utils/Logger.mjs"; +import { Logger } from "../../module/utils/Logger.mjs"; const loaders = { svg(data) { diff --git a/dev/main.mjs b/dev/main.mjs new file mode 100644 index 0000000..35dfa4c --- /dev/null +++ b/dev/main.mjs @@ -0,0 +1,3 @@ +// Hooks +import "./hooks/hotReload.mjs"; +import "./hooks/getHeaderControlsActorSheetV2.mjs"; diff --git a/eslint.config.mjs b/eslint.config.mjs index 4ef4c09..1d3bc7a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -74,7 +74,16 @@ export default [ "@stylistic/space-infix-ops": `warn`, "@stylistic/eol-last": `warn`, "@stylistic/operator-linebreak": [`warn`, `before`], - "@stylistic/indent": [`warn`, `tab`], + "@stylistic/indent": [ + `warn`, + `tab`, + { + SwitchCase: 1, + ignoredNodes: [ + `> .superClass`, + ], + }, + ], "@stylistic/brace-style": [`warn`, `stroustrup`, { "allowSingleLine": true }], "@stylistic/quotes": [`warn`, `backtick`, { "avoidEscape": true }], "@stylistic/comma-dangle": [`warn`, { arrays: `always-multiline`, objects: `always-multiline`, imports: `always-multiline`, exports: `always-multiline`, functions: `always-multiline` }], diff --git a/jsconfig.json b/jsconfig.json index 47b5b55..9841790 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -12,6 +12,7 @@ }, "include": [ "module/**/*", + "dev/**/*", "foundry/client/client.mjs", "foundry/client/global.d.mts", "foundry/common/primitives/global.d.mts" diff --git a/langs/en-ca.json b/langs/en-ca.json index a5d28cc..355793c 100644 --- a/langs/en-ca.json +++ b/langs/en-ca.json @@ -11,6 +11,7 @@ "good": "Good", "shield": "Shield", "skill": "Skill", + "trait": "Trait", "weapon": "Weapon" } }, @@ -173,6 +174,7 @@ "guts-value-readonly": "The current amount of guts the character has", "guts-max-readonly": "The maximum amount of guts the character can have" }, + "edit-description": "Edit Description", "traits-placeholder": "New Trait...", "short-range": "Short @RipCrypt.common.range", "long-range": "Long @RipCrypt.common.range", @@ -201,7 +203,8 @@ "invalid-socket": "Invalid socket data received, this means a module or system bug is present.", "unknown-socket-event": "An unknown socket event was received: {event}", "no-active-gm": "No active @USER.GM is logged in, you must wait for a @USER.GM to be active before you can do that.", - "malformed-socket-payload": "Socket event \"{event}\" received with malformed payload. Details: {details}" + "malformed-socket-payload": "Socket event \"{event}\" received with malformed payload. Details: {details}", + "invalid-parent-document": "Cannot create an item with type \"{itemType}\" on a parent document of type \"{parentType}\"" }, "warn": { "cannot-go-negative": "\"{name}\" is unable to be a negative number." diff --git a/module/Apps/ActorSheets/BookGeistSheet.mjs b/module/Apps/ActorSheets/BookGeistSheet.mjs new file mode 100644 index 0000000..eeeffdd --- /dev/null +++ b/module/Apps/ActorSheets/BookGeistSheet.mjs @@ -0,0 +1,217 @@ +import { filePath } from "../../consts.mjs"; +import { GenericAppMixin } from "../mixins/GenericApp.mjs"; +import { LaidOutAppMixin } from "../mixins/LaidOutAppMixin.mjs"; +import { localizer } from "../../utils/Localizer.mjs"; +import { editItemFromElement, deleteItemFromElement } from "../utils.mjs"; +import { gameTerms } from "../../gameTerms.mjs"; + +const { HandlebarsApplicationMixin } = foundry.applications.api; +const { ActorSheetV2 } = foundry.applications.sheets; +const { ContextMenu, TextEditor } = foundry.applications.ux; + +export class BookGeistSheet extends + LaidOutAppMixin( + GenericAppMixin( + HandlebarsApplicationMixin( + ActorSheetV2, +))) { + // #region Options + static DEFAULT_OPTIONS = { + classes: [ + `ripcrypt--actor`, + `BookGeistSheet`, + ], + position: { + width: `auto`, + height: `auto`, + }, + window: { + resizable: true, + }, + form: { + submitOnChange: true, + closeOnSubmite: false, + }, + }; + + static PARTS = { + layout: { + root: true, + template: filePath(`templates/Apps/BookGeistSheet/layout.hbs`), + }, + image: { template: filePath(`templates/Apps/BookGeistSheet/image.hbs`) }, + header: { template: filePath(`templates/Apps/BookGeistSheet/header.hbs`) }, + stats: { template: filePath(`templates/Apps/BookGeistSheet/stats.hbs`) }, + items: { template: filePath(`templates/Apps/BookGeistSheet/items.hbs`) }, + }; + // #endregion Options + + // #region Lifecycle + async _onRender() { + await super._onRender(); + + new ContextMenu.implementation( + this.element, + `[data-ctx-menu="item"]`, + [ + { + name: localizer(`RipCrypt.common.edit`), + condition: (el) => { + const itemId = el.dataset.itemId; + return itemId !== ``; + }, + callback: editItemFromElement, + }, + { + name: localizer(`RipCrypt.common.delete`), + condition: (el) => { + const itemId = el.dataset.itemId; + return itemId !== ``; + }, + callback: deleteItemFromElement, + }, + ], + { jQuery: false, fixed: true }, + ); + }; + // #endregion Lifecycle + + // #region Data Prep + async _preparePartContext(partID, ctx) { + + switch (partID) { + case `layout`: await this._prepareLayoutContext(ctx); break; + case `image`: await this._prepareImageContext(ctx); break; + case `header`: await this._prepareHeaderContext(ctx); break; + case `stats`: await this._prepareStatsContext(ctx); break; + case `items`: await this._prepareItemsContext(ctx); break; + }; + + return ctx; + }; + + async _prepareLayoutContext(ctx) { + ctx.imageVisible = true; + }; + + async _prepareImageContext(ctx) { + ctx.url = this.actor.img; + }; + + async _prepareHeaderContext(ctx) { + ctx.name = this.actor.name; + ctx.rank = this.actor.system.level.rank; + ctx.ranks = Object.values(gameTerms.Rank) + .map((value, index) => ({ + value, + label: index + })); + ctx.description = await TextEditor.implementation.enrichHTML(this.actor.system.description); + }; + + async _prepareStatsContext(ctx) { + const system = this.actor.system; + + const fate = system.fate; + if (fate) { + ctx.path = { + full: localizer(`RipCrypt.common.ordinals.${fate}.full`), + abbv: localizer(`RipCrypt.common.ordinals.${fate}.abbv`), + }; + } + else { + ctx.path = { + full: null, + abbv: localizer(`RipCrypt.common.empty`), + }; + }; + + Object.assign(ctx, system.ability); + ctx.guts = system.guts; + ctx.speed = system.speed; + }; + + async _prepareItemsContext(ctx) { + const armours = this.actor.system.equippedArmour; + const shield = this.actor.system.equippedShield; + + let defenses = []; + for (const [location, armour] of Object.entries(armours)) { + if (!armour) { continue } + const defense = { + name: localizer(`RipCrypt.common.anatomy.${location}`), + tooltip: null, + protection: 0, + shielded: false, + }; + if (armour) { + defense.armourUUID = armour.uuid; + defense.tooltip = armour.name, + defense.protection = armour.system.protection; + } + if (shield?.system.location.has(location)) { + defense.shieldUUID = shield.uuid; + defense.shielded = true; + defense.protection += shield.system.protection; + }; + if (defense.protection > 0) { + defenses.push(defense); + }; + }; + + ctx.defenses = defenses + ctx.traits = []; // Array<{name: string}> + + for (const item of this.actor.items) { + switch (item.type) { + case `weapon`: { + if (!item.system.equipped) { continue }; + ctx.attacks ??= []; + ctx.attacks.push(this._prepareWeapon(item)); + break; + }; + case `craft`: { + ctx.crafts ??= []; + ctx.crafts.push(this._prepareCraft(item)); + break; + }; + case `trait`: { + ctx.traits.push(this._prepareTrait(item)); + break; + }; + }; + }; + }; + + _prepareWeapon(weapon) { + const hasShortRange = weapon.system.range.short != null; + const hasLongRange = weapon.system.range.long != null; + const isRanged = hasShortRange || hasLongRange; + return { + uuid: weapon.uuid, + name: weapon.name, + damage: weapon.system.damage, + isRanged, + range: weapon.system.range, + }; + }; + + _prepareCraft(craft) { + return { + uuid: craft.uuid, + name: craft.name, + }; + }; + + _prepareTrait(trait) { + return { + uuid: trait.uuid, + name: trait.name, + description: trait.system.description, + }; + }; + // #endregion Data Prep + + // #region Actions + // #endregion Actions +}; diff --git a/module/Apps/ActorSheets/CombinedHeroSheet.mjs b/module/Apps/ActorSheets/CombinedHeroSheet.mjs index b81923a..25f01e4 100644 --- a/module/Apps/ActorSheets/CombinedHeroSheet.mjs +++ b/module/Apps/ActorSheets/CombinedHeroSheet.mjs @@ -1,6 +1,6 @@ import { CraftCardV1 } from "./CraftCardV1.mjs"; import { filePath } from "../../consts.mjs"; -import { GenericAppMixin } from "../GenericApp.mjs"; +import { GenericAppMixin } from "../mixins/GenericApp.mjs"; import { SkillsCardV1 } from "./SkillsCardV1.mjs"; import { StatsCardV1 } from "./StatsCardV1.mjs"; diff --git a/module/Apps/ActorSheets/CraftCardV1.mjs b/module/Apps/ActorSheets/CraftCardV1.mjs index 4a650df..560a08e 100644 --- a/module/Apps/ActorSheets/CraftCardV1.mjs +++ b/module/Apps/ActorSheets/CraftCardV1.mjs @@ -1,7 +1,7 @@ import { deleteItemFromElement, editItemFromElement } from "../utils.mjs"; import { documentSorter, filePath } from "../../consts.mjs"; import { gameTerms } from "../../gameTerms.mjs"; -import { GenericAppMixin } from "../GenericApp.mjs"; +import { GenericAppMixin } from "../mixins/GenericApp.mjs"; import { localizer } from "../../utils/Localizer.mjs"; import { Logger } from "../../utils/Logger.mjs"; diff --git a/module/Apps/ActorSheets/SkillsCardV1.mjs b/module/Apps/ActorSheets/SkillsCardV1.mjs index b0231f3..048536c 100644 --- a/module/Apps/ActorSheets/SkillsCardV1.mjs +++ b/module/Apps/ActorSheets/SkillsCardV1.mjs @@ -2,7 +2,7 @@ import { deleteItemFromElement, editItemFromElement } from "../utils.mjs"; import { documentSorter, filePath } from "../../consts.mjs"; import { AmmoTracker } from "../popovers/AmmoTracker.mjs"; import { gameTerms } from "../../gameTerms.mjs"; -import { GenericAppMixin } from "../GenericApp.mjs"; +import { GenericAppMixin } from "../mixins/GenericApp.mjs"; import { ItemFlags } from "../../flags/item.mjs"; import { localizer } from "../../utils/Localizer.mjs"; import { Logger } from "../../utils/Logger.mjs"; diff --git a/module/Apps/ActorSheets/StatsCardV1.mjs b/module/Apps/ActorSheets/StatsCardV1.mjs index cd44ead..8f05344 100644 --- a/module/Apps/ActorSheets/StatsCardV1.mjs +++ b/module/Apps/ActorSheets/StatsCardV1.mjs @@ -2,7 +2,7 @@ import { deleteItemFromElement, editItemFromElement } from "../utils.mjs"; import { DelveDiceHUD } from "../DelveDiceHUD.mjs"; import { filePath } from "../../consts.mjs"; import { gameTerms } from "../../gameTerms.mjs"; -import { GenericAppMixin } from "../GenericApp.mjs"; +import { GenericAppMixin } from "../mixins/GenericApp.mjs"; import { localizer } from "../../utils/Localizer.mjs"; import { Logger } from "../../utils/Logger.mjs"; diff --git a/module/Apps/DelveDiceHUD.mjs b/module/Apps/DelveDiceHUD.mjs index 9599fa1..0998668 100644 --- a/module/Apps/DelveDiceHUD.mjs +++ b/module/Apps/DelveDiceHUD.mjs @@ -143,7 +143,6 @@ export class DelveDiceHUD extends HandlebarsApplicationMixin(ApplicationV2) { }; }; - Logger.log(`${partId} Context`, ctx); return ctx; }; diff --git a/module/Apps/DicePool.mjs b/module/Apps/DicePool.mjs index 7418a3c..99eb70f 100644 --- a/module/Apps/DicePool.mjs +++ b/module/Apps/DicePool.mjs @@ -1,5 +1,5 @@ import { filePath } from "../consts.mjs"; -import { GenericAppMixin } from "./GenericApp.mjs"; +import { GenericAppMixin } from "./mixins/GenericApp.mjs"; import { localizer } from "../utils/Localizer.mjs"; import { Logger } from "../utils/Logger.mjs"; diff --git a/module/Apps/ItemSheets/AllItemSheetV1.mjs b/module/Apps/ItemSheets/AllItemSheetV1.mjs index 0608fde..6ec5fe5 100644 --- a/module/Apps/ItemSheets/AllItemSheetV1.mjs +++ b/module/Apps/ItemSheets/AllItemSheetV1.mjs @@ -1,5 +1,5 @@ import { filePath } from "../../consts.mjs"; -import { GenericAppMixin } from "../GenericApp.mjs"; +import { GenericAppMixin } from "../mixins/GenericApp.mjs"; import { Logger } from "../../utils/Logger.mjs"; const { HandlebarsApplicationMixin } = foundry.applications.api; diff --git a/module/Apps/ItemSheets/ArmourSheet.mjs b/module/Apps/ItemSheets/ArmourSheet.mjs index 7570413..bec8a14 100644 --- a/module/Apps/ItemSheets/ArmourSheet.mjs +++ b/module/Apps/ItemSheets/ArmourSheet.mjs @@ -1,6 +1,6 @@ import { filePath } from "../../consts.mjs"; import { gameTerms } from "../../gameTerms.mjs"; -import { GenericAppMixin } from "../GenericApp.mjs"; +import { GenericAppMixin } from "../mixins/GenericApp.mjs"; const { HandlebarsApplicationMixin } = foundry.applications.api; const { ItemSheetV2 } = foundry.applications.sheets; diff --git a/module/Apps/ItemSheets/TraitSheet.mjs b/module/Apps/ItemSheets/TraitSheet.mjs new file mode 100644 index 0000000..c835d01 --- /dev/null +++ b/module/Apps/ItemSheets/TraitSheet.mjs @@ -0,0 +1,53 @@ +import { filePath } from "../../consts.mjs"; +import { GenericAppMixin } from "../mixins/GenericApp.mjs"; + +const { HandlebarsApplicationMixin } = foundry.applications.api; +const { ItemSheetV2 } = foundry.applications.sheets; + +export class TraitSheet extends GenericAppMixin(HandlebarsApplicationMixin(ItemSheetV2)) { + // #region Options + static DEFAULT_OPTIONS = { + classes: [ + `ripcrypt--item`, + `TraitSheet`, + ], + position: { + width: `auto`, + height: `auto`, + }, + window: { + resizable: true, + }, + form: { + submitOnChange: true, + closeOnSubmit: false, + }, + }; + + static PARTS = { + content: { + template: filePath(`templates/Apps/TraitSheet/content.hbs`), + root: true, + }, + }; + // #endregion Options + + // #region Data Prep + async _prepareContext() { + const TextEditor = foundry.applications.ux.TextEditor.implementation; + const ctx = { + meta: { + idp: this.id, + }, + item: this.document, + enriched: { + system: { + description: await TextEditor.enrichHTML(this.document.system.description), + }, + }, + }; + + return ctx; + }; + // #endregion Data Prep +}; diff --git a/module/Apps/GenericApp.mjs b/module/Apps/mixins/GenericApp.mjs similarity index 76% rename from module/Apps/GenericApp.mjs rename to module/Apps/mixins/GenericApp.mjs index 0be30e4..ffb3666 100644 --- a/module/Apps/GenericApp.mjs +++ b/module/Apps/mixins/GenericApp.mjs @@ -1,7 +1,7 @@ -import { createItemFromElement, deleteItemFromElement, editItemFromElement, updateForeignDocumentFromEvent } from "./utils.mjs"; -import { DicePool } from "./DicePool.mjs"; -import { RichEditor } from "./RichEditor.mjs"; -import { toBoolean } from "../consts.mjs"; +import { createItemFromElement, deleteItemFromElement, editItemFromElement, updateForeignDocumentFromEvent } from "../utils.mjs"; +import { DicePool } from "../DicePool.mjs"; +import { RichEditor } from "../RichEditor.mjs"; +import { toBoolean } from "../../consts.mjs"; /** * A mixin that takes the class from HandlebarsApplicationMixin and combines it @@ -37,6 +37,8 @@ export function GenericAppMixin(HandlebarsApp) { _popoverManagers = new Map(); /** @type {Map} */ _hookIDs = new Map(); + /** @type {string | null} */ + #focus = null; // #endregion // #region Lifecycle @@ -53,6 +55,26 @@ export function GenericAppMixin(HandlebarsApp) { }; }; + /** + * @override + * This method overrides Foundry's default behaviour for caching the focused + * element so that it actually works when the application has a root partial + */ + async _preRender(...args) { + if (this.rendered) { + const target = this.element.querySelector(`:focus`); + if (target) { + if (target.id) { + this.#focus = `#${CSS.escape(target.id)}`; + } + else if (target.name) { + this.#focus = `${target.tagName}[name="${target.name}"]`; + }; + }; + }; + return super._preRender(...args); + }; + /** @override */ async _onRender(...args) { await super._onRender(...args); @@ -83,19 +105,30 @@ export function GenericAppMixin(HandlebarsApp) { }); }; - async _preparePartContext(partId, ctx, opts) { - ctx = await super._preparePartContext(partId, ctx, opts); - delete ctx.document; - delete ctx.fields; + /** + * @override + * This method overrides Foundry's default behaviour for caching the focused + * element so that it actually works when the application has a root partial + */ + async _postRender(...args) { + if (this.rendered) { + const target = this.element.querySelector(this.#focus); + target?.focus(); + }; + this.#focus = null; + return super._postRender(...args); + }; + + async _prepareContext(_options) { + const ctx = {}; ctx.meta ??= {}; - ctx.meta.idp = this.document?.uuid ?? this.id; + ctx.meta.idp = this.id; if (this.document) { ctx.meta.limited = this.document.limited; ctx.meta.editable = this.isEditable || game.user.isGM; ctx.meta.embedded = this.document.isEmbedded; }; - delete ctx.editable; return ctx; }; diff --git a/module/Apps/mixins/LaidOutAppMixin.mjs b/module/Apps/mixins/LaidOutAppMixin.mjs new file mode 100644 index 0000000..956630b --- /dev/null +++ b/module/Apps/mixins/LaidOutAppMixin.mjs @@ -0,0 +1,56 @@ +/** + * This mixin makes it so that we can provide a specific layout template without + * needing to reference each of the inner areas via a partial embedded in the root, + * enabling partial re-renders for parts of the sheet without losing advanced + * layout capabilities. + * + * @param {ReturnType} HandlebarsApp The mixin'd class from HAM to further mix + */ +export function LaidOutAppMixin(HandlebarsApp) { + class LaidOutApp extends HandlebarsApp { + #partDescriptors; + + /** + * This caches the part descriptors into this class of the heirarchy, because + * Foundry doesn't expose the partDescriptors from the HAM directly, so we + * inject a heirarchy call so that we can nab the pointer that Foundry has + * in the HAM so that we can also read/write it from this class. + */ + _configureRenderParts(options) { + const parts = super._configureRenderParts(options); + this.#partDescriptors = parts; + return parts; + }; + + /** + * @override + * This is essentially Foundry's HandlebarsApplicationMixin implementation, + * however if an existing part for non-root elements don't get concatenated + * into the DOM. + */ + _replaceHTML(result, content, options) { + const partInfo = this.#partDescriptors; + for ( const [partId, htmlElement] of Object.entries(result) ) { + const part = partInfo[partId]; + const priorElement = part.root ? content : content.querySelector(`[data-application-part="${partId}"]`); + const state = {}; + if ( priorElement ) { + this._preSyncPartState(partId, htmlElement, priorElement, state); + if ( part.root ) { + priorElement.replaceChildren(...htmlElement.children); + } + else { + priorElement.replaceWith(htmlElement); + } + this._syncPartState(partId, htmlElement, priorElement, state); + } + else { + continue; + }; + this._attachPartListeners(partId, htmlElement, options); + this.parts[partId] = htmlElement; + } + }; + }; + return LaidOutApp; +}; diff --git a/module/data/Actor/Geist.mjs b/module/data/Actor/Geist.mjs index 440392a..b78572c 100644 --- a/module/data/Actor/Geist.mjs +++ b/module/data/Actor/Geist.mjs @@ -1,3 +1,18 @@ import { EntityData } from "./Entity.mjs"; -export class GeistData extends EntityData {}; +const { fields } = foundry.data; + +export class GeistData extends EntityData { + static defineSchema() { + const schema = super.defineSchema(); + + schema.description = new fields.HTMLField({ + blank: true, + nullable: true, + trim: true, + initial: null, + }); + + return schema; + }; +}; diff --git a/module/data/Item/Ammo.mjs b/module/data/Item/Ammo.mjs index 0493eb1..135acf8 100644 --- a/module/data/Item/Ammo.mjs +++ b/module/data/Item/Ammo.mjs @@ -2,19 +2,6 @@ import { CommonItemData } from "./Common.mjs"; import { gameTerms } from "../../gameTerms.mjs"; export class AmmoData extends CommonItemData { - // MARK: Base Data - prepareBaseData() { - super.prepareBaseData(); - }; - - // MARK: Derived Data - prepareDerivedData() { - super.prepareDerivedData(); - }; - - // #region Getters - // #endregion - // #region Sheet Data getFormFields(_ctx) { const fields = [ diff --git a/module/data/Item/Common.mjs b/module/data/Item/Common.mjs index bf37c0e..f98dd5a 100644 --- a/module/data/Item/Common.mjs +++ b/module/data/Item/Common.mjs @@ -4,7 +4,7 @@ import { gameTerms } from "../../gameTerms.mjs"; const { fields } = foundry.data; export class CommonItemData extends foundry.abstract.TypeDataModel { - // MARK: Schema + // #region Schema static defineSchema() { return { quantity: requiredInteger({ min: 0, initial: 1 }), @@ -21,14 +21,5 @@ export class CommonItemData extends foundry.abstract.TypeDataModel { }), }; }; - - // MARK: Base Data - prepareBaseData() { - super.prepareBaseData(); - }; - - // MARK: Derived Data - prepareDerivedData() { - super.prepareDerivedData(); - }; + // #endregion Schema }; diff --git a/module/data/Item/Craft.mjs b/module/data/Item/Craft.mjs index 23aa9ba..b9a7d3e 100644 --- a/module/data/Item/Craft.mjs +++ b/module/data/Item/Craft.mjs @@ -21,19 +21,6 @@ export class CraftData extends SkillData { return schema; }; - // MARK: Base Data - prepareBaseData() { - super.prepareBaseData(); - }; - - // MARK: Derived Data - prepareDerivedData() { - super.prepareDerivedData(); - }; - - // #region Getters - // #endregion - // #region Sheet Data async getFormFields(_ctx) { const fields = [ diff --git a/module/data/Item/Good.mjs b/module/data/Item/Good.mjs index 9af246b..34c09b6 100644 --- a/module/data/Item/Good.mjs +++ b/module/data/Item/Good.mjs @@ -17,19 +17,6 @@ export class GoodData extends CommonItemData { return schema; }; - // MARK: Base Data - prepareBaseData() { - super.prepareBaseData(); - }; - - // MARK: Derived Data - prepareDerivedData() { - super.prepareDerivedData(); - }; - - // #region Getters - // #endregion - // #region Sheet Data async getFormFields(_ctx) { const fields = [ diff --git a/module/data/Item/Skill.mjs b/module/data/Item/Skill.mjs index 520a344..da03381 100644 --- a/module/data/Item/Skill.mjs +++ b/module/data/Item/Skill.mjs @@ -34,19 +34,6 @@ export class SkillData extends foundry.abstract.TypeDataModel { return schema; }; - // MARK: Base Data - prepareBaseData() { - super.prepareBaseData(); - }; - - // MARK: Derived Data - prepareDerivedData() { - super.prepareDerivedData(); - }; - - // #region Getters - // #endregion - // #region Sheet Data async getFormFields(_ctx) { const fields = [ diff --git a/module/data/Item/Trait.mjs b/module/data/Item/Trait.mjs new file mode 100644 index 0000000..067cb1c --- /dev/null +++ b/module/data/Item/Trait.mjs @@ -0,0 +1,25 @@ +import { localizer } from "../../utils/Localizer.mjs"; + +const { fields } = foundry.data; + +export class TraitData extends foundry.abstract.TypeDataModel { + // #region Schema + static defineSchema() { + return { + description: new fields.HTMLField({ blank: true, nullable: false, trim: true }), + }; + }; + // #endregion Schema + + // #region Lifecycle + async _preCreate() { + if (this.parent.isEmbedded && this.parent.parent.type !== `geist`) { + ui.notifications.error(localizer( + `RipCrypt.notifs.error.invalid-parent-document`, + { itemType: `trait`, parentType: this.parent.parent.type }, + )); + return false; + }; + }; + // #endregion +}; diff --git a/module/data/Item/Weapon.mjs b/module/data/Item/Weapon.mjs index ec35f29..c4db30f 100644 --- a/module/data/Item/Weapon.mjs +++ b/module/data/Item/Weapon.mjs @@ -49,10 +49,13 @@ export class WeaponData extends CommonItemData { async _preCreate(item, options) { const showEquipPrompt = options.showEquipPrompt ?? true; if (showEquipPrompt && this.parent.isEmbedded && this._canEquip()) { - const shouldEquip = await DialogV2.confirm({ + let shouldEquip = this.parent.parent.type === `geist`; + + shouldEquip ||= await DialogV2.confirm({ window: { title: `Equip Item?` }, content: `Do you want to equip ${item.name}?`, }); + if (shouldEquip) { this.updateSource({ "equipped": true }); }; diff --git a/module/handlebarHelpers/_index.mjs b/module/handlebarHelpers/_index.mjs index 9c8587d..2d1e881 100644 --- a/module/handlebarHelpers/_index.mjs +++ b/module/handlebarHelpers/_index.mjs @@ -9,5 +9,6 @@ export default { "rc-options": options, // #region Simple + "rc-ifOut": (v) => (v || ``), "rc-empty-state": (v) => v ?? localizer(`RipCrypt.common.empty`), }; diff --git a/module/hooks/init.mjs b/module/hooks/init.mjs index 28c4fd9..4b46bc0 100644 --- a/module/hooks/init.mjs +++ b/module/hooks/init.mjs @@ -1,6 +1,7 @@ // Applications import { AllItemSheetV1 } from "../Apps/ItemSheets/AllItemSheetV1.mjs"; import { ArmourSheet } from "../Apps/ItemSheets/ArmourSheet.mjs"; +import { BookGeistSheet } from "../Apps/ActorSheets/BookGeistSheet.mjs"; import { CombinedHeroSheet } from "../Apps/ActorSheets/CombinedHeroSheet.mjs"; import { CraftCardV1 } from "../Apps/ActorSheets/CraftCardV1.mjs"; import { DelveDiceHUD } from "../Apps/DelveDiceHUD.mjs"; @@ -17,6 +18,7 @@ import { GoodData } from "../data/Item/Good.mjs"; import { HeroData } from "../data/Actor/Hero.mjs"; import { ShieldData } from "../data/Item/Shield.mjs"; import { SkillData } from "../data/Item/Skill.mjs"; +import { TraitData } from "../data/Item/Trait.mjs"; import { WeaponData } from "../data/Item/Weapon.mjs"; // Class Overrides @@ -37,6 +39,7 @@ import { registerMetaSettings } from "../settings/metaSettings.mjs"; import { registerSockets } from "../sockets/_index.mjs"; import { registerUserSettings } from "../settings/userSettings.mjs"; import { registerWorldSettings } from "../settings/worldSettings.mjs"; +import { TraitSheet } from "../Apps/ItemSheets/TraitSheet.mjs"; const { Items, Actors } = foundry.documents.collections; @@ -62,6 +65,7 @@ Hooks.once(`init`, () => { CONFIG.Item.dataModels.good = GoodData; CONFIG.Item.dataModels.shield = ShieldData; CONFIG.Item.dataModels.skill = SkillData; + CONFIG.Item.dataModels.trait = TraitData; CONFIG.Item.dataModels.weapon = WeaponData; // #endregion @@ -87,22 +91,21 @@ Hooks.once(`init`, () => { label: `RipCrypt.sheet-names.StatsCardV1`, themes: StatsCardV1.themes, }); - Actors.registerSheet(game.system.id, StatsCardV1, { - makeDefault: true, - types: [`geist`], - label: `RipCrypt.sheet-names.StatsCardV1`, - themes: StatsCardV1.themes, - }); Actors.registerSheet(game.system.id, SkillsCardV1, { - types: [`hero`, `geist`], + types: [`hero`], label: `RipCrypt.sheet-names.SkillsCardV1`, themes: SkillsCardV1.themes, }); Actors.registerSheet(game.system.id, CraftCardV1, { - types: [`hero`, `geist`], + types: [`hero`], label: `RipCrypt.sheet-names.CraftCardV1`, themes: CraftCardV1.themes, }); + Actors.registerSheet(game.system.id, BookGeistSheet, { + types: [`geist`], + label: `RipCrypt.sheet-names.BookGeistSheet`, + themes: BookGeistSheet.themes, + }); // #endregion // #region Items @@ -118,8 +121,15 @@ Hooks.once(`init`, () => { label: `RipCrypt.sheet-names.ArmourSheet`, themes: ArmourSheet.themes, }); + Items.registerSheet(game.system.id, TraitSheet, { + makeDefault: true, + types: [`trait`], + label: `RipCrypt.sheet-names.TraitSheet`, + themes: TraitSheet.themes, + }); + Items.unregisterSheet(game.system.id, AllItemSheetV1, { - types: [`armour`, `shield`], + types: [`armour`, `shield`, `trait`], }); // #endregion // #endregion diff --git a/module/main.mjs b/module/main.mjs index 064d9f8..8c32f94 100644 --- a/module/main.mjs +++ b/module/main.mjs @@ -1,7 +1,6 @@ // Hooks import "./hooks/init.mjs"; import "./hooks/ready.mjs"; -import "./hooks/hotReload.mjs"; // Global API import "./api.mjs"; diff --git a/scripts/prepareManifest.mjs b/scripts/prepareManifest.mjs new file mode 100644 index 0000000..bfdbfba --- /dev/null +++ b/scripts/prepareManifest.mjs @@ -0,0 +1,34 @@ +/* +The intent of this script is to do all of the modifications of the +manifest file that we need to do in order to release the system. This +can include removing dev-only fields/attributes that end users will +never, and should never, care about nor need. +*/ +import { readFile, writeFile } from "fs/promises"; + +const MANIFEST_PATH = `system.json`; + +let manifest; +try { + manifest = JSON.parse(await readFile(MANIFEST_PATH, `utf-8`)); +} catch { + console.error(`Failed to parse manifest file.`); + process.exit(1); +}; + + +// Filter out dev-only resources +if (manifest.esmodules) { + manifest.esmodules = manifest.esmodules.filter( + filepath => !filepath.startsWith(`dev/`) + ); +}; + +// Remove dev flags +delete manifest.flags?.hotReload; + +if (Object.keys(manifest.flags).length === 0) { + delete manifest.flags; +}; + +await writeFile(MANIFEST_PATH, JSON.stringify(manifest, undefined, `\t`)); diff --git a/system.json b/system.json index d4b61ed..11a1cdd 100644 --- a/system.json +++ b/system.json @@ -12,7 +12,8 @@ { "name": "Oliver" } ], "esmodules": [ - "module/main.mjs" + "module/main.mjs", + "dev/main.mjs" ], "styles": [ { @@ -36,7 +37,7 @@ "flags": { "hotReload": { "extensions": ["css", "hbs", "json", "mjs", "svg"], - "paths": ["assets", "templates", "langs", "module"] + "paths": ["assets", "templates", "langs", "module", "dev"] } }, "documentTypes": { @@ -48,9 +49,22 @@ "ammo": {}, "armour": {}, "craft": {}, - "good": {}, + "good": { + "htmlFields": [ + "description" + ] + }, "shield": {}, - "skill": {}, + "skill": { + "htmlFields": [ + "description" + ] + }, + "trait": { + "htmlFields": [ + "description" + ] + }, "weapon": {} } }, diff --git a/templates/Apps/BookGeistSheet/header.hbs b/templates/Apps/BookGeistSheet/header.hbs new file mode 100644 index 0000000..fe198ef --- /dev/null +++ b/templates/Apps/BookGeistSheet/header.hbs @@ -0,0 +1,24 @@ +
+
+ +
+ + +
+ {{#if description}} +
+ {{{description}}} +
+ {{/if}} +
diff --git a/templates/Apps/BookGeistSheet/image.hbs b/templates/Apps/BookGeistSheet/image.hbs new file mode 100644 index 0000000..3edfe8e --- /dev/null +++ b/templates/Apps/BookGeistSheet/image.hbs @@ -0,0 +1,8 @@ +
+ +
diff --git a/templates/Apps/BookGeistSheet/items.hbs b/templates/Apps/BookGeistSheet/items.hbs new file mode 100644 index 0000000..dbe53b5 --- /dev/null +++ b/templates/Apps/BookGeistSheet/items.hbs @@ -0,0 +1,78 @@ +
+ {{#if attacks}} +
Attacks
+
+ {{#each attacks as |attack|}} +
+ {{attack.name}} + {{attack.damage}} + {{#if attack.isRanged}} + + ({{attack.range.short}} / {{attack.range.long}}) + + {{/if}} +
+ {{/each}} +
+ {{/if}} + {{#if crafts}} +
Craft
+
+ {{#each crafts as |craft|}} +
+ {{craft.name}} +
+ {{/each}} +
+ {{/if}} +
Defense
+
    + {{#each defenses as |defense|}} +
  • + {{defense.name}} ({{defense.protection}}{{#if defense.shielded}}, + + {{/if}}) +
  • + {{/each}} +
+
Traits
+
    + {{#each traits as |trait|}} +
  • + {{trait.name}} + {{#if trait.description}} + + {{/if}} +
  • + {{else}} + None + {{/each}} +
+
diff --git a/templates/Apps/BookGeistSheet/layout.hbs b/templates/Apps/BookGeistSheet/layout.hbs new file mode 100644 index 0000000..13e8fea --- /dev/null +++ b/templates/Apps/BookGeistSheet/layout.hbs @@ -0,0 +1,10 @@ +
+ {{#if imageVisible}} +
+ {{/if}} +
+
+
+
+
+
diff --git a/templates/Apps/BookGeistSheet/stats.hbs b/templates/Apps/BookGeistSheet/stats.hbs new file mode 100644 index 0000000..453ab1b --- /dev/null +++ b/templates/Apps/BookGeistSheet/stats.hbs @@ -0,0 +1,97 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + +
PathGritGaitGripGlimGutsMove
+ {{path.abbv}} + + {{#if meta.editable}} + + {{else if meta.limited}} + ??? + {{else}} + {{grit}} + {{/if}} + + {{#if meta.editable}} + + {{else if meta.limited}} + ??? + {{else}} + {{gait}} + {{/if}} + + {{#if meta.editable}} + + {{else if meta.limited}} + ??? + {{else}} + {{grip}} + {{/if}} + + {{#if meta.editable}} + + {{else if meta.limited}} + ??? + {{else}} + {{glim}} + {{/if}} + + {{#if meta.editable}} + + / {{guts.max}} + {{else if meta.limited}} + ??/?? + {{else}} + {{guts.value}}/{{guts.max}} + {{/if}} + {{speed.move}} / {{speed.run}}
+
diff --git a/templates/Apps/BookGeistSheet/style.css b/templates/Apps/BookGeistSheet/style.css new file mode 100644 index 0000000..4c8b2cf --- /dev/null +++ b/templates/Apps/BookGeistSheet/style.css @@ -0,0 +1,101 @@ +.BookGeistSheet { + > .window-content { + display: flex; + flex-direction: row; + gap: 4px; + padding: 8px; + color: var(--base-text); + background: var(--base-background); + } + + .info { + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 4px; + } + + .img-wrapper { + display: flex; + justify-content: center; + align-items: center; + flex-shrink: 0; + + img { + width: 150px; + height: 150px; + } + } + + .overview { + display: flex; + flex-direction: row; + gap: 4px; + + input { + width: 50%; + } + } + + table { + td { + border: 1px solid var(--accent-1); + text-align: center; + + input { + width: 30px; + background: unset; + text-align: center; + } + } + + thead td { + font-weight: bold; + border-top-width: 0; + + &:first-of-type, &:last-of-type { + border-left-width: 0; + border-right-width: 0; + } + } + + tbody tr { + td:first-of-type, td:last-of-type { + border-left-width: 0; + border-right-width: 0; + } + + &:last-of-type td { + border-bottom-width: 0; + } + } + + .alt { + background-color: var(--alt-row-background); + } + } + + .items { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 4fr); + grid-template-rows: repeat(3, auto); + gap: 2px; + + ul { + display: flex; + flex-direction: row; + gap: 4px; + list-style-type: none; + } + + li { + display: flex; + flex-direction: row; + align-items: center; + gap: 4px; + background-color: var(--accent-2); + border-radius: 4px; + padding: 2px 4px; + } + } +} diff --git a/templates/Apps/TraitSheet/content.hbs b/templates/Apps/TraitSheet/content.hbs new file mode 100644 index 0000000..7ede18c --- /dev/null +++ b/templates/Apps/TraitSheet/content.hbs @@ -0,0 +1,21 @@ +
+ + +
{{{ enriched.system.description }}}
+
diff --git a/templates/Apps/TraitSheet/style.css b/templates/Apps/TraitSheet/style.css new file mode 100644 index 0000000..adbf30c --- /dev/null +++ b/templates/Apps/TraitSheet/style.css @@ -0,0 +1,35 @@ +.ripcrypt.TraitSheet { + --input-underline: none; + max-width: 300px; + + > .window-content { + display: flex; + flex-direction: column; + gap: 8px; + padding: 4px; + color: var(--base-text); + background: var(--base-background); + } + + input { + border-radius: 4px; + padding: 2px 4px; + } + + .value { + background: var(--input-background); + color: var(--input-text); + padding: 4px; + + > :first-child { + margin-top: 0; + } + > :last-child { + margin-bottom: 0; + } + + &:empty { + display: none; + } + } +} diff --git a/templates/Apps/apps.css b/templates/Apps/apps.css index db88f7e..e620e85 100644 --- a/templates/Apps/apps.css +++ b/templates/Apps/apps.css @@ -1,3 +1,4 @@ +@import url("./common.css"); @import url("./AllItemSheetV1/style.css"); @import url("./CombinedHeroSheet/style.css"); @import url("./DelveDiceHUD/style.css"); @@ -7,24 +8,8 @@ @import url("./SkillsCardV1/style.css"); @import url("./RichEditor/style.css"); @import url("./ArmourSheet/style.css"); +@import url("./TraitSheet/style.css"); +@import url("./BookGeistSheet/style.css"); @import url("./popover.css"); @import url("./popovers/AmmoTracker/style.css"); - -.ripcrypt { - .window-content { - flex: initial; - padding: 0; - margin: 0; - } - - .StatsCardV1, - .SkillsCardV1, - .CraftCardV1 { - padding: 8px; - /* height: 270px; */ - width: 680px; - --col-gap: 2px; - --row-gap: 4px; - } -} diff --git a/templates/Apps/common.css b/templates/Apps/common.css new file mode 100644 index 0000000..379307a --- /dev/null +++ b/templates/Apps/common.css @@ -0,0 +1,17 @@ +.ripcrypt { + .window-content { + flex: initial; + padding: 0; + margin: 0; + } + + .StatsCardV1, + .SkillsCardV1, + .CraftCardV1 { + padding: 8px; + /* height: 270px; */ + width: 680px; + --col-gap: 2px; + --row-gap: 4px; + } +} diff --git a/templates/Apps/partials/item-header.hbs b/templates/Apps/partials/item-header.hbs index 300c166..358779d 100644 --- a/templates/Apps/partials/item-header.hbs +++ b/templates/Apps/partials/item-header.hbs @@ -2,7 +2,6 @@ Required parameters: "name" : the name of the item "system.quantity" : the quantity of the item - "meta.idp" : the ID Prefix for the application --}}
diff --git a/templates/css/elements/prose-mirror.css b/templates/css/elements/prose-mirror.css deleted file mode 100644 index 7aaa978..0000000 --- a/templates/css/elements/prose-mirror.css +++ /dev/null @@ -1,3 +0,0 @@ -.ripcrypt prose-mirror * { - all: revert-layer; -} diff --git a/templates/css/main.css b/templates/css/main.css index a8c65a3..8fc8421 100644 --- a/templates/css/main.css +++ b/templates/css/main.css @@ -26,4 +26,3 @@ @import url("../Apps/apps.css") layer(apps); /* Exceptions */ -@import url("./elements/prose-mirror.css") layer(exceptions);