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.

310 lines
11 KiB

1 year ago
  1. class CompanionManager extends FormApplication {
  2. constructor(actor) {
  3. super();
  4. this.actor = actor;
  5. }
  6. static get defaultOptions() {
  7. return {
  8. ...super.defaultOptions,
  9. title: game.i18n.localize("AE.dialogs.companionManager.title"),
  10. id: "companionManager",
  11. template: `modules/automated-evocations/templates/companionmanager.hbs`,
  12. resizable: true,
  13. width: 300,
  14. height: window.innerHeight > 400 ? 400 : window.innerHeight - 100,
  15. dragDrop: [{ dragSelector: null, dropSelector: null }],
  16. };
  17. }
  18. static get api() {
  19. return {
  20. dnd5e: {
  21. getSummonInfo(args, spellLevel) {
  22. const spellDC = (args[0].assignedActor?.system.attributes.spelldc) || 0;
  23. return {
  24. level: (args[0].spellLevel || spellLevel) - spellLevel,
  25. maxHP: args[0].assignedActor?.system.attributes.hp.max || 1,
  26. modifier: args[0].assignedActor?.system.abilities[args[0].assignedActor?.system.attributes.spellcasting]?.mod,
  27. dc: spellDC,
  28. attack: {
  29. ms: spellDC - 8 + args[0].assignedActor?.system.bonuses.msak.attack,
  30. rs: spellDC - 8 + args[0].assignedActor?.system.bonuses.rsak.attack,
  31. mw: args[0].assignedActor?.system.bonuses.mwak.attack,
  32. rw: args[0].assignedActor?.system.bonuses.rwak.attack,
  33. }
  34. }
  35. }
  36. }
  37. }
  38. }
  39. getData() {
  40. return {};
  41. }
  42. async activateListeners(html) {
  43. 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>`)
  44. this.loadCompanions();
  45. html.on("input", ".searchinput", this._onSearch.bind(this));
  46. html.on("click", "#remove-companion", this._onRemoveCompanion.bind(this));
  47. html.on("click", "#summon-companion", this._onSummonCompanion.bind(this));
  48. html.on("click", ".actor-name", this._onOpenSheet.bind(this));
  49. html.on("dragstart", "#companion", async (event) => {
  50. event.originalEvent.dataTransfer.setData(
  51. "text/plain",
  52. event.currentTarget.dataset.elid
  53. );
  54. });
  55. html.on("dragend", "#companion", async (event) => {
  56. event.originalEvent.dataTransfer.setData(
  57. "text/plain",
  58. event.currentTarget.dataset.elid
  59. );
  60. });
  61. }
  62. _onSearch(event) {
  63. const search = $(event.currentTarget).val();
  64. this.element.find(".actor-name").each(function() {
  65. if ($(this).text().toLowerCase().includes(search.toLowerCase())) {
  66. $(this).parent().slideDown(200);
  67. } else {
  68. $(this).parent().slideUp(200);
  69. }
  70. });
  71. }
  72. _canDragDrop() {
  73. return true;
  74. }
  75. _canDragStart() {
  76. return true;
  77. }
  78. async _onDrop(event) {
  79. let data;
  80. try {
  81. data = JSON.parse(event.dataTransfer.getData("text/plain"));
  82. } catch {
  83. data = event.dataTransfer.getData("text/plain");
  84. }
  85. const li = this.element.find(`[data-elid="${data}"]`);
  86. if (li.length && !$(event.target).hasClass("nodrop")) {
  87. let target = $(event.target).closest("li");
  88. if (target.length && target[0].dataset.elid != data) {
  89. $(li).remove();
  90. target.before($(li));
  91. }
  92. }
  93. if (!data.type === "Actor") return;
  94. //const actor = await fromUuid(data.uuid)
  95. this.element
  96. .find("#companion-list")
  97. .append(await this.generateLi({ id: data.uuid }));
  98. this.saveData();
  99. }
  100. async _onSummonCompanion(event) {
  101. this.minimize();
  102. const animation = $(event.currentTarget.parentElement.parentElement)
  103. .find(".anim-dropdown")
  104. .val();
  105. const aId = event.currentTarget.dataset.aid;
  106. const actor = game.actors.get(aId) || await fromUuid(aId);
  107. const duplicates = $(event.currentTarget.parentElement.parentElement)
  108. .find("#companion-number-val")
  109. .val();
  110. const tokenData = await actor.getTokenData({elevation: _token?.data?.elevation ?? 0});
  111. const posData = await warpgate.crosshairs.show({
  112. size: Math.max(Math.max(tokenData.width,tokenData.height)*(tokenData.texture.scaleX + tokenData.texture.scaleY)/2, 0.5),
  113. icon: "modules/automated-evocations/assets/black-hole-bolas.webp",
  114. label: "",
  115. });
  116. if (!posData || posData.cancelled) {
  117. this.maximize();
  118. return;
  119. }
  120. if (typeof AECONSTS.animationFunctions[animation].fn == "string") {
  121. this.evaluateExpression(game.macros.getName(AECONSTS.animationFunctions[animation].fn).command, posData, tokenData);
  122. }else{
  123. AECONSTS.animationFunctions[animation].fn(posData, tokenData);
  124. }
  125. await this.wait(AECONSTS.animationFunctions[animation].time);
  126. //get custom data macro
  127. 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}) || {};
  128. customTokenData.elevation = posData?.flags?.levels?.elevation ?? _token?.document?.elevation ?? 0;
  129. customTokenData.elevation = parseFloat(customTokenData.elevation);
  130. tokenData.elevation = customTokenData.elevation;
  131. let isCompendiumActor = false;
  132. if (!tokenData.actor) {
  133. isCompendiumActor = true;
  134. tokenData.updateSource({actorId: Array.from(game.actors).find(a => !a.prototypeToken?.actorLink).id})
  135. }
  136. Hooks.on("preCreateToken", (tokenDoc, td) => {
  137. td ??= {};
  138. td.elevation = customTokenData.elevation;
  139. tokenDoc.updateSource({elevation: customTokenData.elevation});
  140. });
  141. const tokens = await warpgate.spawnAt(
  142. { x: posData.x, y: posData.y},
  143. tokenData,
  144. customTokenData || {},
  145. {},
  146. { duplicates }
  147. );
  148. if (tokens.length && isCompendiumActor) {
  149. for (const t of tokens) {
  150. const tokenDocument = canvas.tokens.get(t).document;
  151. await tokenDocument.update({delta: actor.toObject()});
  152. }
  153. }
  154. console.log("Automated Evocations Summoning:", {
  155. assignedActor: this.caster || game?.user?.character || _token?.actor,
  156. spellLevel: this.spellLevel || 0,
  157. duplicates: duplicates,
  158. warpgateData: customTokenData || {},
  159. summon: actor,
  160. tokenData: tokenData,
  161. posData: posData,
  162. })
  163. if(game.settings.get(AECONSTS.MN, "autoclose")) this.close();
  164. else this.maximize();
  165. }
  166. async evaluateExpression(expression, ...args) {
  167. if (!expression) return null;
  168. const AsyncFunction = (async function () {}).constructor;
  169. const fn = new AsyncFunction("args" ,$("<span />", { html: expression }).text());
  170. try {
  171. return await fn(args);
  172. } catch(e) {
  173. ui.notifications.error("There was an error in your macro syntax. See the console (F12) for details");
  174. console.error(e);
  175. return undefined;
  176. }
  177. }
  178. async _onRemoveCompanion(event) {
  179. Dialog.confirm({
  180. title: game.i18n.localize("AE.dialogs.companionManager.confirm.title"),
  181. content: game.i18n.localize(
  182. "AE.dialogs.companionManager.confirm.content"
  183. ),
  184. yes: () => {
  185. event.currentTarget.parentElement.remove();
  186. this.saveData();
  187. },
  188. no: () => {},
  189. defaultYes: false,
  190. });
  191. }
  192. async _onOpenSheet(event) {
  193. const actorId = event.currentTarget.parentElement.dataset.aid;
  194. const actor = game.actors.get(actorId) || await fromUuid(actorId);
  195. if (actor) {
  196. actor.sheet.render(true);
  197. }
  198. }
  199. async loadCompanions() {
  200. 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");
  201. if (data) {
  202. for (let companion of data) {
  203. this.element.find("#companion-list").append(await this.generateLi(companion));
  204. }
  205. }
  206. }
  207. async generateLi(data) {
  208. const actor = game.actors.get(data.id) || game.actors.getName(data.id) || await fromUuid(data.id);
  209. if (!actor) return "";
  210. const uuid = actor.uuid;
  211. const restricted = game.settings.get(AECONSTS.MN, "restrictOwned")
  212. if(restricted && !actor.isOwner) return "";
  213. let $li = $(`
  214. <li id="companion" class="companion-item" data-aid="${
  215. uuid
  216. }" data-elid="${randomID()}" draggable="true">
  217. <div class="summon-btn">
  218. <img class="actor-image" src="${actor.img}" alt="">
  219. <div class="warpgate-btn" id="summon-companion" data-aid="${uuid}"></div>
  220. </div>
  221. <span class="actor-name">${actor.name}</span>
  222. <div class="companion-number"><input type="number" min="1" max="99" class="fancy-input" step="1" id="companion-number-val" value="${
  223. data.number || 1
  224. }"></div>
  225. <select class="anim-dropdown">
  226. ${this.getAnimations(data.animation)}
  227. </select>
  228. <i id="remove-companion" class="fas fa-trash"></i>
  229. </li>
  230. `);
  231. // <i id="advanced-params" class="fas fa-edit"></i>
  232. return $li;
  233. }
  234. getAnimations(anim) {
  235. let animList = "";
  236. for (let [group, animations] of Object.entries(AECONSTS.animations)) {
  237. const localGroup = game.i18n.localize(`AE.groups.${group}`)
  238. animList+=`<optgroup label="${localGroup == `AE.groups.${group}` ? group : localGroup}">`;
  239. for (let a of animations) {
  240. animList += `<option value="${a.key}" ${
  241. a.key == anim ? "selected" : ""
  242. }>${a.name}</option>`;
  243. }
  244. animList += "</optgroup>";
  245. }
  246. return animList;
  247. }
  248. async wait(ms) {
  249. return new Promise((resolve) => setTimeout(resolve, ms));
  250. }
  251. async saveData() {
  252. let data = [];
  253. for (let companion of this.element.find(".companion-item")) {
  254. data.push({
  255. id: companion.dataset.aid,
  256. animation: $(companion).find(".anim-dropdown").val(),
  257. number: $(companion).find("#companion-number-val").val(),
  258. });
  259. }
  260. 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);
  261. }
  262. close(noSave = false) {
  263. if (!noSave) this.saveData();
  264. super.close();
  265. }
  266. }
  267. class SimpleCompanionManager extends CompanionManager {
  268. constructor(summonData,spellLevel,actor) {
  269. super();
  270. this.caster = actor;
  271. this.summons = summonData;
  272. this.spellLevel = spellLevel
  273. }
  274. async activateListeners(html) {
  275. for (let summon of this.summons) {
  276. this.element.find("#companion-list").append(await this.generateLi(summon));
  277. }
  278. html.on("click", "#summon-companion", this._onSummonCompanion.bind(this));
  279. html.on("click", ".actor-name", this._onOpenSheet.bind(this));
  280. }
  281. _onDrop(event) {}
  282. close() {
  283. super.close(true);
  284. }
  285. }