class CompanionManager extends FormApplication { constructor(actor) { super(); this.actor = actor; } static get defaultOptions() { return { ...super.defaultOptions, title: game.i18n.localize("AE.dialogs.companionManager.title"), id: "companionManager", template: `modules/automated-evocations/templates/companionmanager.hbs`, resizable: true, width: 300, height: window.innerHeight > 400 ? 400 : window.innerHeight - 100, dragDrop: [{ dragSelector: null, dropSelector: null }], }; } static get api() { return { dnd5e: { getSummonInfo(args, spellLevel) { const spellDC = args[0].assignedActor?.system.attributes.spelldc || 0; return { level: (args[0].spellLevel || spellLevel) - spellLevel, maxHP: args[0].assignedActor?.system.attributes.hp.max || 1, modifier: args[0].assignedActor?.system.abilities[args[0].assignedActor?.system.attributes.spellcasting]?.mod, dc: spellDC, attack: { ms: spellDC - 8 + args[0].assignedActor?.system.bonuses.msak.attack, rs: spellDC - 8 + args[0].assignedActor?.system.bonuses.rsak.attack, mw: args[0].assignedActor?.system.bonuses.mwak.attack, rw: args[0].assignedActor?.system.bonuses.rwak.attack, }, }; }, }, }; } getData() { return {}; } async activateListeners(html) { html.find("#companion-list").before(`
`); this.loadCompanions(); html.on("input", ".searchinput", this._onSearch.bind(this)); html.on("click", "#remove-companion", this._onRemoveCompanion.bind(this)); html.on("click", "#summon-companion", this._onSummonCompanion.bind(this)); html.on("click", ".actor-name", this._onOpenSheet.bind(this)); html.on("dragstart", "#companion", async (event) => { event.originalEvent.dataTransfer.setData("text/plain", event.currentTarget.dataset.elid); }); html.on("dragend", "#companion", async (event) => { event.originalEvent.dataTransfer.setData("text/plain", event.currentTarget.dataset.elid); }); } _onSearch(event) { const search = $(event.currentTarget).val(); this.element.find(".actor-name").each(function () { if ($(this).text().toLowerCase().includes(search.toLowerCase())) { $(this).parent().slideDown(200); } else { $(this).parent().slideUp(200); } }); } _canDragDrop() { return true; } _canDragStart() { return true; } async _onDrop(event) { let data; try { data = JSON.parse(event.dataTransfer.getData("text/plain")); } catch { data = event.dataTransfer.getData("text/plain"); } const li = this.element.find(`[data-elid="${data}"]`); if (li.length && !$(event.target).hasClass("nodrop")) { let target = $(event.target).closest("li"); if (target.length && target[0].dataset.elid != data) { $(li).remove(); target.before($(li)); } } if (!data.type === "Actor") return; //const actor = await fromUuid(data.uuid) this.element.find("#companion-list").append(await this.generateLi({ id: data.uuid })); this.saveData(); } async _onSummonCompanion(event) { this.minimize(); const animation = $(event.currentTarget.parentElement.parentElement).find(".anim-dropdown").val(); const aId = event.currentTarget.dataset.aid; const actor = game.actors.get(aId) || (await fromUuid(aId)); const duplicates = $(event.currentTarget.parentElement.parentElement).find("#companion-number-val").val(); const tokenData = await actor.getTokenData({ elevation: _token?.data?.elevation ?? 0 }); if (this.range) this.drawRange(this.range); const posData = await warpgate.crosshairs.show({ size: Math.max((Math.max(tokenData.width, tokenData.height) * (tokenData.texture.scaleX + tokenData.texture.scaleY)) / 2, 0.5), icon: "modules/automated-evocations/assets/black-hole-bolas.webp", label: "", }); this.clearRange(); if (!posData || posData.cancelled) { this.maximize(); return; } if (typeof AECONSTS.animationFunctions[animation].fn == "string") { this.evaluateExpression(game.macros.getName(AECONSTS.animationFunctions[animation].fn).command, posData, tokenData); } else { AECONSTS.animationFunctions[animation].fn(posData, tokenData); } await this.wait(AECONSTS.animationFunctions[animation].time); //get custom data macro const customTokenData = (await this.evaluateExpression(game.macros.getName(`AE_Companion_Macro(${actor.name})`)?.command, { summon: actor, spellLevel: this.spellLevel || 0, duplicates: duplicates, assignedActor: this.caster || game.user.character || _token.actor })) || {}; customTokenData.elevation = posData?.flags?.levels?.elevation ?? _token?.document?.elevation ?? 0; customTokenData.elevation = parseFloat(customTokenData.elevation); tokenData.elevation = customTokenData.elevation; let isCompendiumActor = false; if (!tokenData.actor) { isCompendiumActor = true; tokenData.updateSource({ actorId: Array.from(game.actors).find((a) => !a.prototypeToken?.actorLink).id }); } Hooks.on("preCreateToken", (tokenDoc, td) => { td ??= {}; td.elevation = customTokenData.elevation; tokenDoc.updateSource({ elevation: customTokenData.elevation }); }); Hooks.callAll("automated-evocations.preCreateToken", { tokenData: tokenData, customTokenData: customTokenData, posData: posData, actor: actor, spellLevel: this.spellLevel || 0, duplicates: duplicates, assignedActor: this.caster || game.user.character || _token.actor }); const tokens = await warpgate.spawnAt({ x: posData.x, y: posData.y }, tokenData, customTokenData || {}, {}, { duplicates }); if (tokens.length && isCompendiumActor) { for (const t of tokens) { const tokenDocument = canvas.tokens.get(t).document; await tokenDocument.update({ delta: actor.toObject() }); } } const postSummon = { assignedActor: this.caster || game?.user?.character || _token?.actor, spellLevel: this.spellLevel || 0, duplicates: duplicates, warpgateData: customTokenData || {}, summon: actor, tokenData: tokenData, posData: posData, summonedTokens: tokens, }; console.log("Automated Evocations Summoning:", postSummon); Hooks.callAll("automated-evocations.postSummon", postSummon); if (game.settings.get(AECONSTS.MN, "autoclose")) this.close(); else this.maximize(); } async drawRange() { const token = _token; if (!token) return; this.rangeData = { token, }; const rangeGraphics = new PIXI.Graphics(); rangeGraphics .lineStyle(1, 0x000000, 1) .beginFill(0x000000, 0.1) .drawCircle(0, 0, this.range * (canvas.scene.dimensions.size / canvas.scene.dimensions.distance)) .endFill(); if (this.image) { const texture = this.image ? await loadTexture(this.image) : null; //create a matrix so that the texture is centered and fills the circle const matrix = new PIXI.Matrix(); matrix.translate(-texture.width / 2, -texture.height / 2); matrix.scale((2 * this.range * (canvas.scene.dimensions.size / canvas.scene.dimensions.distance)) / texture.width, (2 * this.range * (canvas.scene.dimensions.size / canvas.scene.dimensions.distance)) / texture.height); rangeGraphics .beginTextureFill({ texture: texture, matrix, alpha: 0.3 }) .drawCircle(0, 0, this.range * (canvas.scene.dimensions.size / canvas.scene.dimensions.distance)) .endFill(); } rangeGraphics.position.set((canvas.scene.dimensions.size * token.document.width) / 2, (canvas.scene.dimensions.size * token.document.height) / 2); rangeGraphics.zIndex = -100; //add as first child so that it is behind the token token.addChildAt(rangeGraphics, 0); this.rangeData.graphics = rangeGraphics; } async clearRange() { if (!this.rangeData) return; this.rangeData.token.removeChild(this.rangeData.graphics); this.rangeData.graphics.destroy(); delete this.rangeData; } async evaluateExpression(expression, ...args) { if (!expression) return null; const AsyncFunction = async function () {}.constructor; const fn = new AsyncFunction("args", $("", { html: expression }).text()); try { return await fn(args); } catch (e) { ui.notifications.error("There was an error in your macro syntax. See the console (F12) for details"); console.error(e); return undefined; } } async _onRemoveCompanion(event) { Dialog.confirm({ title: game.i18n.localize("AE.dialogs.companionManager.confirm.title"), content: game.i18n.localize("AE.dialogs.companionManager.confirm.content"), yes: () => { event.currentTarget.parentElement.remove(); this.saveData(); }, no: () => {}, defaultYes: false, }); } async _onOpenSheet(event) { const actorId = event.currentTarget.parentElement.dataset.aid; const actor = game.actors.get(actorId) || (await fromUuid(actorId)); if (actor) { actor.sheet.render(true); } } async loadCompanions() { let data = this.actor && (this.actor.getFlag(AECONSTS.MN, "isLocal") || game.settings.get(AECONSTS.MN, "storeonactor")) ? this.actor.getFlag(AECONSTS.MN, "companions") || [] : game.user.getFlag(AECONSTS.MN, "companions"); if (data) { for (let companion of data) { this.element.find("#companion-list").append(await this.generateLi(companion)); } } } async generateLi(data) { const actor = game.actors.get(data.id) || game.actors.getName(data.id) || (await fromUuid(data.id)); if (!actor) return ""; const uuid = actor.uuid; const restricted = game.settings.get(AECONSTS.MN, "restrictOwned"); if (restricted && !actor.isOwner) return ""; let $li = $(`