diff --git a/langs/en-ca.2.json b/langs/en-ca.2.json index 12ebfc1..a6015ed 100644 --- a/langs/en-ca.2.json +++ b/langs/en-ca.2.json @@ -51,6 +51,9 @@ "skill-roll-locked": "@dotdungeon.trainingLevel.locked" } } + }, + "default": { + "name": "(Unnamed @TYPES.{document}.{type})" } }, "TYPES": { @@ -67,7 +70,7 @@ "pet": "Pet", "structure": "Structure", "service": "Service", - "materials": "Materials", + "material": "Materials", "legendaryItem": "Legendary Item", "spell": "Spell", "untyped": "Custom" diff --git a/module/config.mjs b/module/config.mjs index 51e77b7..591fd84 100644 --- a/module/config.mjs +++ b/module/config.mjs @@ -54,7 +54,7 @@ export const localizerConfig = { }; export const itemFilters = [ - `materials`, + `material`, `untyped`, `aspect`, `weapon`, diff --git a/module/documents/Actor/GenericActor.mjs b/module/documents/Actor/GenericActor.mjs new file mode 100644 index 0000000..0964ad4 --- /dev/null +++ b/module/documents/Actor/GenericActor.mjs @@ -0,0 +1,102 @@ +import { localizer } from "../../utils/localizer.mjs"; + +export class DotDungeonActor extends Actor { + /** @type {any} */ + system; + + async openEmbeddedSheet($event) { + const data = $event.target.dataset; + let item = await fromUuid(data.embeddedEdit); + item?.sheet.render(true); + }; + + async createEmbeddedItem(defaults, opts = {}) { + let items = await this.createEmbeddedDocuments(`Item`, defaults); + if (items.length == 0) { + throw new Error(`Failed to create any items`); + }; + this.sheet.render(); + if ( + game.settings.get(`dotdungeon`, `openEmbeddedOnCreate`) + && !opts.overrideSheetOpen + ) { + for (const item of items) { + item.sheet.render(true); + }; + }; + }; + + async genericEmbeddedCreate($event) { + const data = $event.currentTarget.dataset; + if (!this[`createCustom${data.embeddedCreate}`]) { + this.createEmbeddedItem({ + type: data.embeddedCreate, + name: localizer( + `dotdungeon.default.name`, + { document: `Actor`, type: data.embeddedCreate } + ), + }); + } else { + this[`createCustom${data.embeddedCreate}`]($event); + }; + }; + + async genericEmbeddedUpdate($event) { + const target = $event.delegateTarget; + const data = target.dataset; + const item = await fromUuid(data.embeddedId); + + let value = target.value; + switch (target.type) { + case "checkbox": value = target.checked; break; + }; + + await item?.update({ [data.embeddedUpdate]: value }); + }; + + async genericEmbeddedDelete($event) { + let data = $event.currentTarget.dataset; + let item = await fromUuid(data.embeddedId); + + if (!item) { + ui.notifications.error( + `dotdungeon.notification.error.item-not-found`, + { console: false } + ); + return; + }; + + Dialog.confirm({ + title: game.i18n.format( + `dotdungeon.dialogs.${item.type}.delete.title`, + item + ), + content: game.i18n.format( + `dotdungeon.dialogs.${item.type}.delete.content`, + item + ), + yes: () => { + item.delete(); + }, + defaultYes: false, + }); + }; + + async genericSendToChat($event) { + const data = $event.currentTarget.dataset; + const type = data.messageType; + if (this[`send${type}ToChat`]) { + return await this[`send${type}ToChat`]($event); + }; + if (!data.messageContent) { + console.warn(`.dungeon | Tried to send a chat message with no content`); + return; + }; + let message = await ChatMessage.create({ + content: data.messageContent, + flavor: data.messageFlavor, + speaker: { actor: this.actor }, + }); + message.render(); + }; +}; diff --git a/module/documents/Actor/Handler.mjs b/module/documents/Actor/Handler.mjs deleted file mode 100644 index 06df8fe..0000000 --- a/module/documents/Actor/Handler.mjs +++ /dev/null @@ -1,104 +0,0 @@ -import PlayerActor from "./Player.mjs"; -import MobActor from "./Mob.mjs"; -import SyncActor from "./Sync.mjs"; - -/** @extends {Actor} */ -export class ActorHandler extends Actor { - proxyTargets = { - player: PlayerActor, - mob: MobActor, - sync: SyncActor, - }; - - constructor(data, ctx) { - super(data, ctx); - }; - - /** @type {class|undefined} */ - get fn() { - return this.proxyTargets[this.type]; - }; - - async proxyFunction(funcName, ...args) { - if (!this.fn?.[funcName]) return; - return await this.fn?.[funcName].bind(this)(...args); - }; - - async openEmbeddedSheet($event) { - if (this.fn?.openEmbeddedSheet) { - this.fn.openEmbeddedSheet.bind(this)($event); - } else { - const data = $event.target.dataset; - let item = await fromUuid(data.embeddedEdit); - item?.sheet.render(true); - }; - }; - - async genericEmbeddedUpdate($event) { - if (this.fn?.genericEmbeddedUpdate) { - return this.fn.genericEmbeddedUpdate.bind(this)($event); - }; - const target = $event.delegateTarget; - const data = target.dataset; - const item = await fromUuid(data.embeddedId); - - let value = target.value; - switch (target.type) { - case "checkbox": value = target.checked; break; - }; - - await item?.update({ [data.embeddedUpdate]: value }); - }; - - async genericEmbeddedDelete($event) { - if (!this.fn?.genericEmbeddedDelete) return; - this.fn.genericEmbeddedDelete.bind(this)($event); - }; - - async genericEmbeddedCreate($event) { - const data = $event.currentTarget.dataset; - if (!this.fn?.[`createCustom${data.embeddedCreate}`]) return; - this.fn?.[`createCustom${data.embeddedCreate}`].bind(this)($event); - }; - - async genericSendToChat($event) { - const data = $event.currentTarget.dataset; - const type = data.messageType; - if (this.fn?.[`send${type}ToChat`]) { - return await this.fn?.[`send${type}ToChat`].bind(this)($event); - }; - if (!data.messageContent) { - console.warn(`.dungeon | Tried to send a chat message with no content`); - return; - }; - let message = await ChatMessage.create({ - content: data.messageContent, - flavor: data.messageFlavor, - speaker: { actor: this.actor } - }); - message.render(); - }; - - /** - * @param {ItemHandler} item - * @returns {boolean} true to allow the document to be embedded - */ - async preItemEmbed(item) { - let type = item.type[0].toUpperCase() + item.type.slice(1); - if (this.fn?.[`pre${type}Embed`]) { - return await this.fn?.[`pre${type}Embed`].bind(this)(item); - }; - return true; - }; - - _preUpdate(...args) { - return this.proxyFunction("_preUpdate", ...args); - }; - - getRollData() { - if (!this.fn?.getRollData) return {}; - return this.fn?.getRollData.bind(this)(); - }; - - useRestDie() {return this.proxyFunction("useRestDie")}; -}; diff --git a/module/documents/Actor/Mob.mjs b/module/documents/Actor/Mob.mjs index 16f397f..cca7e96 100644 --- a/module/documents/Actor/Mob.mjs +++ b/module/documents/Actor/Mob.mjs @@ -1,11 +1,10 @@ -/** @this {Actor} */ -function getRollData() { - const data = { - initiative: this.system.initiative ?? 0, - }; - return data; -}; +import { DotDungeonActor } from "./GenericActor.mjs"; -export default { - getRollData, +export class Mob extends DotDungeonActor { + getRollData() { + const data = { + initiative: this.system.initiative ?? 0, + }; + return data; + }; }; diff --git a/module/documents/Actor/Player.mjs b/module/documents/Actor/Player.mjs index 0cafed5..b5abdfd 100644 --- a/module/documents/Actor/Player.mjs +++ b/module/documents/Actor/Player.mjs @@ -1,155 +1,68 @@ -import { ItemHandler } from "../Item/Handler.mjs"; +import { DotDungeonActor } from "./GenericActor.mjs"; +import { DotDungeonItem } from "../Item/GenericItem.mjs"; -/** @this {Actor} */ -async function genericEmbeddedDelete($event) { - let data = $event.currentTarget.dataset; - let item = await fromUuid(data.embeddedId); +export class Player extends DotDungeonActor { - if (!item) { - ui.notifications.error( - `dotdungeon.notification.error.item-not-found`, - { console: false } + async createCustomPet() { + const body = new URLSearchParams({ + number: 1, + animal: `Cat`, + "X-Requested-With": "fetch" + }); + const r = await fetch( + `https://randommer.io/pet-names`, + { + method: "POST", + body + } ); - return; + await this.createEmbeddedItem([{ + type: `pet`, + name: (await r.json())[0] ?? game.i18n.localize(`dotdungeon.defaults.pet.name`), + }]); }; - Dialog.confirm({ - title: game.i18n.format( - `dotdungeon.dialogs.${item.type}.delete.title`, - item - ), - content: game.i18n.format( - `dotdungeon.dialogs.${item.type}.delete.content`, - item - ), - yes: () => { - item.delete(); - }, - defaultYes: false, - }); -}; - -/** @this {Actor} */ -async function createCustomItem(defaults, opts = {}) { - let items = await this.createEmbeddedDocuments(`Item`, defaults); - if (items.length == 0) { - throw new Error(); + get atAspectLimit() { + let limit = game.settings.get(`dotdungeon`, `aspectLimit`); + return this.itemTypes.aspect.length >= limit; }; - this.sheet.render(); - if ( - game.settings.get(`dotdungeon`, `openEmbeddedOnCreate`) - && !opts.overrideSheetOpen - ) { - for (const item of items) { - item.sheet.render(true); + + async preAspectEmbed(item) { + if (this.atAspectLimit) { + ui.notifications.error( + game.i18n.format( + `dotdungeon.notification.error.aspect-limit-reached`, + { limit: game.settings.get(`dotdungeon`, `aspectLimit`) } + ), + { console: false } + ); + return false; }; }; -}; -/** @this {Actor} */ -async function createCustomUntyped() { - await createCustomItem.bind(this)([{ - type: `untyped`, - name: game.i18n.format(`dotdungeon.defaults.untyped.name`), - }]); -}; + /** + * @param {DotDungeonItem} item + */ + async preUntypedEmbed(item) { + let inventoryItem = this.itemTypes.untyped.find(i => i.name === item.name); + if (inventoryItem) { + inventoryItem.update({"system.quantity": inventoryItem.system.quantity + 1}); + ui.notifications.info( + game.i18n.format( + `dotdungeon.notification.info.increased-item-quantity`, + { name: inventoryItem.name } + ), + { console: false } + ); + return false; + }; + }; -/** @this {Actor} */ -async function createCustomAspect() { - await createCustomItem.bind(this)([{ - type: `aspect`, - name: game.i18n.format(`dotdungeon.defaults.aspect.name`), - }]); -}; - -/** @this {Actor} */ -async function createCustomSpell() { - await createCustomItem.bind(this)([{ - type: `spell`, - name: game.i18n.format(`dotdungeon.defaults.spell.name`), - }]); -}; - -/** @this {Actor} */ -async function createCustomPet() { - const body = new URLSearchParams({ - number: 1, - animal: `Cat`, - "X-Requested-With": "fetch" - }) - const r = await fetch( - `https://randommer.io/pet-names`, - { - method: "POST", - body - } - ); - await createCustomItem.bind(this)([{ - type: `pet`, - name: (await r.json())[0] ?? game.i18n.localize(`dotdungeon.defaults.pet.name`), - }]); -}; - -/** @this {Actor} */ -async function atAspectLimit() { - let limit = game.settings.get(`dotdungeon`, `aspectLimit`); - return this.itemTypes.aspect.length >= limit; -}; - -/** - * @param {ItemHandler} item - * @this {Actor} - */ -async function preAspectEmbed(item) { - if (await atAspectLimit.bind(this)()) { - ui.notifications.error( - game.i18n.format( - `dotdungeon.notification.error.aspect-limit-reached`, - { limit: game.settings.get(`dotdungeon`, `aspectLimit`) } - ), - { console: false } - ); - return false; + getRollData() { + const data = { + initiative: this.system.stats.hands ?? 0, + stats: this.system.stats, + }; + return data; }; }; - -/** - * @param {ItemHandler} item - * @this {Actor} - */ -async function preUntypedEmbed(item) { - let inventoryItem = this.itemTypes.untyped.find(i => i.name === item.name); - if (inventoryItem) { - inventoryItem.update({"system.quantity": inventoryItem.system.quantity + 1}); - ui.notifications.info( - game.i18n.format( - `dotdungeon.notification.info.increased-item-quantity`, - { name: inventoryItem.name } - ), - { console: false } - ); - return false; - }; -}; - -/** @this {Actor} */ -function getRollData() { - const data = { - initiative: this.system.stats.hands ?? 0, - stats: this.system.stats, - }; - return data; -}; - -export default { - atAspectLimit, - createCustomItem, - createCustomUntyped, - createCustomAspect, - createCustomSpell, - createCustomPet, - genericEmbeddedDelete, - preAspectEmbed, - preUntypedEmbed, - getRollData, -}; diff --git a/module/documents/Actor/Sync.mjs b/module/documents/Actor/Sync.mjs index caa5a5d..1542b3e 100644 --- a/module/documents/Actor/Sync.mjs +++ b/module/documents/Actor/Sync.mjs @@ -1,60 +1,55 @@ -import { syncMilestones, syncDice } from "../../config.mjs"; +import { DotDungeonActor } from "./GenericActor.mjs"; -/** @this {Actor} */ -async function useRestDie() { - let addToSync = await (new Roll(syncDice)).evaluate(); - await addToSync.toMessage({ - speaker: ChatMessage.getSpeaker({ actor: this.actor }), - flavor: `Sync Restoration`, - }); - this.update({ - "system.rest_dice": this.system.rest_dice - 1, - "system.value": this.system.value + addToSync.total, - }); -}; +export class Sync extends DotDungeonActor { + async useRestDie() { + let addToSync = await (new Roll(syncDice)).evaluate(); + await addToSync.toMessage({ + speaker: ChatMessage.getSpeaker({ actor: this.actor }), + flavor: `Sync Restoration`, + }); + this.update({ + "system.rest_dice": this.system.rest_dice - 1, + "system.value": this.system.value + addToSync.total, + }); + }; -/** @this {Actor} */ -async function _preUpdate(data, options) { - if (options.diff) { - if (data.system?.value != null) { - let currentSync = this.system.value; - let newSync = data.system.value; + async _preUpdate(data, options) { + if (options.diff) { + if (data.system?.value != null) { + let currentSync = this.system.value; + let newSync = data.system.value; - let minSync = Math.min(currentSync, newSync); - let maxSync = Math.max(currentSync, newSync); - let milestones = syncMilestones.filter( - m => minSync < m.value && m.value <= maxSync - ); + let minSync = Math.min(currentSync, newSync); + let maxSync = Math.max(currentSync, newSync); + let milestones = syncMilestones.filter( + m => minSync < m.value && m.value <= maxSync + ); - if (milestones.length > 0) data.system.rest_dice ??= this.system.rest_dice; + if (milestones.length > 0) data.system.rest_dice ??= this.system.rest_dice; - for (const milestone of milestones) { - // Damage - if (newSync < currentSync) { - if (!this.system.milestones_hit.has(milestone.value)) { - data.system.rest_dice += 1; - this.system.milestones_hit.add(milestone.value); - }; - } + for (const milestone of milestones) { + // Damage + if (newSync < currentSync) { + if (!this.system.milestones_hit.has(milestone.value)) { + data.system.rest_dice += 1; + this.system.milestones_hit.add(milestone.value); + }; + } - // Healing - else if (newSync > currentSync) { - if ( - this.system.milestones_hit.has(milestone.value) - && milestone.andReturn - && milestone.value <= newSync - ) { - this.system.milestones_hit.delete(milestone.value); + // Healing + else if (newSync > currentSync) { + if ( + this.system.milestones_hit.has(milestone.value) + && milestone.andReturn + && milestone.value <= newSync + ) { + this.system.milestones_hit.delete(milestone.value); + }; }; }; - }; - data.system.milestones_hit = [ ...this.system.milestones_hit ]; + data.system.milestones_hit = [ ...this.system.milestones_hit ]; + }; }; }; }; - -export default { - _preUpdate, - useRestDie, -}; diff --git a/module/documents/Actor/_proxy.mjs b/module/documents/Actor/_proxy.mjs new file mode 100644 index 0000000..ae555e2 --- /dev/null +++ b/module/documents/Actor/_proxy.mjs @@ -0,0 +1,46 @@ +import { DotDungeonActor } from "./GenericActor.mjs"; +import { Player } from "./Player.mjs"; +import { Sync } from "./Sync.mjs"; +import { Mob } from "./Mob.mjs"; + +const classes = { + player: Player, + mob: Mob, + sync: Sync, +}; + +export const ActorProxy = new Proxy(function () {}, { + construct(target, args) { + const [data] = args; + + if (!classes.hasOwnProperty(data.type)) { + return new DotDungeonActor(...args); + } + + return new classes[data.type](...args); + }, + get(target, prop, receiver) { + console.log(prop) + if (["create", "createDocuments"].includes(prop)) { + return function (data, options) { + if (data.constructor === Array) { + return data.map(i => ItemProxy.create(i, options)) + } + + if (!classes.hasOwnProperty(data.type)) { + return DotDungeonActor.create(data, options); + } + + return classes[data.type].create(data, options) + }; + }; + + if (prop == Symbol.hasInstance) { + return function (instance) { + return Object.values(classes).some(i => instance instanceof i) + }; + }; + + return DotDungeonActor[prop]; + }, +}); diff --git a/module/documents/Item/Aspect.mjs b/module/documents/Item/Aspect.mjs index bf6e764..07698a9 100644 --- a/module/documents/Item/Aspect.mjs +++ b/module/documents/Item/Aspect.mjs @@ -1,10 +1,9 @@ -/** @this {ItemHandler} */ -async function _preCreate(_data, _options, _user) { - if (this.isEmbedded) { - return await this.actor?.preItemEmbed(this); - }; -}; +import { DotDungeonItem } from "./GenericItem.mjs"; -export default { - _preCreate, +export class Aspect extends DotDungeonItem { + async _preCreate() { + if (this.isEmbedded) { + return await this.actor?.preItemEmbed(this); + }; + } }; diff --git a/module/documents/Item/GenericItem.mjs b/module/documents/Item/GenericItem.mjs new file mode 100644 index 0000000..1869073 --- /dev/null +++ b/module/documents/Item/GenericItem.mjs @@ -0,0 +1 @@ +export class DotDungeonItem extends Item {}; diff --git a/module/documents/Item/Handler.mjs b/module/documents/Item/Handler.mjs deleted file mode 100644 index dbb6903..0000000 --- a/module/documents/Item/Handler.mjs +++ /dev/null @@ -1,30 +0,0 @@ -import AspectItem from "./Aspect.mjs"; -import SpellItem from "./Spell.mjs"; - -/** @extends {Item} */ -export class ItemHandler extends Item { - proxyTargets = { - aspect: AspectItem, - spell: SpellItem, - }; - - constructor(data, ctx) { - super(data, ctx); - }; - - /** @type {class|undefined} */ - get fn() { - return this.proxyTargets[this.type]; - }; - - async proxyFunction(funcName, ...args) { - if (!this.fn?.[funcName]) return; - return await this.fn?.[funcName].bind(this)(...args); - }; - - async _preCreate(...args) { - if (this.fn?._preCreate) return this.fn?._preCreate.bind(this)(...args); - if (this.isEmbedded) return await this.actor?.preItemEmbed(this); - return; - }; -}; diff --git a/module/documents/Item/Spell.mjs b/module/documents/Item/Spell.mjs deleted file mode 100644 index ff8b4c5..0000000 --- a/module/documents/Item/Spell.mjs +++ /dev/null @@ -1 +0,0 @@ -export default {}; diff --git a/module/documents/Item/_proxy.mjs b/module/documents/Item/_proxy.mjs new file mode 100644 index 0000000..b9677b3 --- /dev/null +++ b/module/documents/Item/_proxy.mjs @@ -0,0 +1,42 @@ +import { Aspect } from "./Aspect.mjs"; +import { DotDungeonItem } from "./GenericItem.mjs"; + +const classes = { + aspect: Aspect, +}; + +export const ItemProxy = new Proxy(function () {}, { + construct(target, args) { + const [data] = args; + + if (!classes.hasOwnProperty(data.type)) { + return new DotDungeonItem(...args); + } + + return new classes[data.type](...args); + }, + get(target, prop, receiver) { + console.log(prop) + if (["create", "createDocuments"].includes(prop)) { + return function (data, options) { + if (data.constructor === Array) { + return data.map(i => ItemProxy.create(i, options)) + } + + if (!classes.hasOwnProperty(data.type)) { + return DotDungeonItem.create(data, options); + } + + return classes[data.type].create(data, options) + }; + }; + + if (prop == Symbol.hasInstance) { + return function (instance) { + return Object.values(classes).some(i => instance instanceof i) + }; + }; + + return DotDungeonItem[prop]; + }, +}); diff --git a/module/dotdungeon.mjs b/module/dotdungeon.mjs index 606307a..02c323c 100644 --- a/module/dotdungeon.mjs +++ b/module/dotdungeon.mjs @@ -8,8 +8,8 @@ import { SyncData } from "./models/Actor/Sync.mjs"; import { MobData } from "./models/Actor/Mob.mjs"; // Main Documents -import { ActorHandler } from "./documents/Actor/Handler.mjs"; -import { ItemHandler } from "./documents/Item/Handler.mjs"; +import { ActorProxy } from "./documents/Actor/_proxy.mjs"; +import { ItemProxy } from "./documents/Item/_proxy.mjs"; // Item Sheets import { UntypedItemSheet } from "./sheets/Items/UntypedItemSheet.mjs"; @@ -46,8 +46,8 @@ Hooks.once(`init`, async () => { CONFIG.Item.dataModels.aspect = AspectItemData; CONFIG.Item.dataModels.spell = SpellItemData; CONFIG.Item.dataModels.pet = PetItemData; - CONFIG.Actor.documentClass = ActorHandler; - CONFIG.Item.documentClass = ItemHandler; + CONFIG.Actor.documentClass = ActorProxy; + CONFIG.Item.documentClass = ItemProxy; CONFIG.DOTDUNGEON = DOTDUNGEON; diff --git a/module/sheets/Actors/PC/PlayerSheetV2.mjs b/module/sheets/Actors/PC/PlayerSheetV2.mjs index 1a7f8c9..5b6ec37 100644 --- a/module/sheets/Actors/PC/PlayerSheetV2.mjs +++ b/module/sheets/Actors/PC/PlayerSheetV2.mjs @@ -2,8 +2,13 @@ import { GenericActorSheet } from "../../GenericActorSheet.mjs"; import DOTDUNGEON from "../../../config.mjs"; import { localizer } from "../../../utils/localizer.mjs"; import { modifierToString } from "../../../utils/modifierToString.mjs"; +import { Player } from "../../../documents/Actor2/Player.mjs"; export class PlayerSheetv2 extends GenericActorSheet { + + /** @type {Player | null} */ + actor; + static get defaultOptions() { let opts = mergeObject( super.defaultOptions, @@ -61,7 +66,7 @@ export class PlayerSheetv2 extends GenericActorSheet { ctx.computed = { canChangeGroup: ctx.settings.playersCanChangeGroup || ctx.isGM, - canAddAspect: !await actor.proxyFunction.bind(actor)(`atAspectLimit`), + canAddAspect: !this.actor.atAspectLimit, stats: this.#statData, itemFilters: this.#itemFilters, noItemTypesVisible: this._itemTypesHidden.size === DOTDUNGEON.itemFilters.length, diff --git a/module/sheets/MVPPCSheet.mjs b/module/sheets/MVPPCSheet.mjs index 7c48f1c..4d5e3ad 100644 --- a/module/sheets/MVPPCSheet.mjs +++ b/module/sheets/MVPPCSheet.mjs @@ -35,7 +35,7 @@ export class MVPPCSheet extends GenericActorSheet { ctx.computed = { canChangeGroup: ctx.settings.playersCanChangeGroup || ctx.isGM, - canAddAspect: !await actor.proxyFunction.bind(actor)(`atAspectLimit`), + canAddAspect: !this.actor.atAspectLimit, }; return ctx; diff --git a/template.json b/template.json index d52fb28..2954976 100644 --- a/template.json +++ b/template.json @@ -9,6 +9,7 @@ "Item": { "types": [ "untyped", + "material", "aspect", "weapon", "armour",