All user data for FoundryVTT. Includes worlds, systems, modules, and any asset in the "foundryuserdata" directory. Does NOT include the FoundryVTT installation itself.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

331 lines
14 KiB

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(`<div class="searchbox"><input type="text" class="searchinput" placeholder="Drag and Drop an actor to add it to the list."></div>`);
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", $("<span />", { 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 = $(`
<li id="companion" class="companion-item" data-aid="${uuid}" data-elid="${randomID()}" draggable="true">
<div class="summon-btn">
<img class="actor-image" src="${actor.img}" alt="">
<div class="warpgate-btn" id="summon-companion" data-aid="${uuid}"></div>
</div>
<span class="actor-name">${actor.name}</span>
<div class="companion-number"><input type="number" min="1" max="99" class="fancy-input" step="1" id="companion-number-val" value="${data.number || 1}"></div>
<select class="anim-dropdown">
${this.getAnimations(data.animation)}
</select>
<i id="remove-companion" class="fas fa-trash"></i>
</li>
`);
// <i id="advanced-params" class="fas fa-edit"></i>
return $li;
}
getAnimations(anim) {
let animList = "";
for (let [group, animations] of Object.entries(AECONSTS.animations)) {
const localGroup = game.i18n.localize(`AE.groups.${group}`);
animList += `<optgroup label="${localGroup == `AE.groups.${group}` ? group : localGroup}">`;
for (let a of animations) {
animList += `<option value="${a.key}" ${a.key == anim ? "selected" : ""}>${a.name}</option>`;
}
animList += "</optgroup>";
}
return animList;
}
async wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async saveData() {
let data = [];
for (let companion of this.element.find(".companion-item")) {
data.push({
id: companion.dataset.aid,
animation: $(companion).find(".anim-dropdown").val(),
number: $(companion).find("#companion-number-val").val(),
});
}
this.actor && (this.actor.getFlag(AECONSTS.MN, "isLocal") || game.settings.get(AECONSTS.MN, "storeonactor")) ? this.actor.setFlag(AECONSTS.MN, "companions", data) : game.user.setFlag(AECONSTS.MN, "companions", data);
Hooks.callAll("automated-evocations.saveCompanionData", { actor: this.actor, data: data });
}
close(noSave = false) {
if (!noSave) this.saveData();
super.close();
}
}
class SimpleCompanionManager extends CompanionManager {
constructor(summonData,spellLevel,actor, range, image) {
super();
this.caster = actor;
this.summons = summonData;
this.spellLevel = spellLevel
this.range = range;
this.image = image;
}
async activateListeners(html) {
for (let summon of this.summons) {
this.element.find("#companion-list").append(await this.generateLi(summon));
}
html.on("click", "#summon-companion", this._onSummonCompanion.bind(this));
html.on("click", ".actor-name", this._onOpenSheet.bind(this));
}
_onDrop(event) {}
close() {
super.close(true);
}
}