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.

290 lines
9.9 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. async _onDrop(event) {
  73. let data;
  74. try {
  75. data = JSON.parse(event.dataTransfer.getData("text/plain"));
  76. } catch {
  77. data = event.dataTransfer.getData("text/plain");
  78. }
  79. const li = this.element.find(`[data-elid="${data}"]`);
  80. if (li.length && !$(event.target).hasClass("nodrop")) {
  81. let target = $(event.target).closest("li");
  82. if (target.length && target[0].dataset.elid != data) {
  83. $(li).remove();
  84. target.before($(li));
  85. }
  86. }
  87. if (!data.type === "Actor") return;
  88. const actor = await fromUuid(data.uuid)
  89. this.element
  90. .find("#companion-list")
  91. .append(this.generateLi({ id: actor.id }));
  92. this.saveData();
  93. }
  94. async _onSummonCompanion(event) {
  95. this.minimize();
  96. const animation = $(event.currentTarget.parentElement.parentElement)
  97. .find(".anim-dropdown")
  98. .val();
  99. const aId = event.currentTarget.dataset.aid;
  100. const actor = game.actors.get(aId);
  101. const duplicates = $(event.currentTarget.parentElement.parentElement)
  102. .find("#companion-number-val")
  103. .val();
  104. const tokenData = await actor.getTokenData({elevation: _token?.data?.elevation ?? 0});
  105. const posData = await warpgate.crosshairs.show({
  106. size: Math.max(Math.max(tokenData.width,tokenData.height)*(tokenData.texture.scaleX + tokenData.texture.scaleY)/2, 0.5),
  107. icon: "modules/automated-evocations/assets/black-hole-bolas.webp",
  108. label: "",
  109. });
  110. if (!posData || posData.cancelled) {
  111. this.maximize();
  112. return;
  113. }
  114. if (typeof AECONSTS.animationFunctions[animation].fn == "string") {
  115. this.evaluateExpression(game.macros.getName(AECONSTS.animationFunctions[animation].fn).command, posData, tokenData);
  116. }else{
  117. AECONSTS.animationFunctions[animation].fn(posData, tokenData);
  118. }
  119. await this.wait(AECONSTS.animationFunctions[animation].time);
  120. //get custom data macro
  121. 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}) || {};
  122. customTokenData.elevation = posData?.flags?.levels?.elevation ?? _token?.document?.elevation ?? 0;
  123. customTokenData.elevation = parseFloat(customTokenData.elevation);
  124. tokenData.elevation = customTokenData.elevation;
  125. Hooks.on("preCreateToken", (tokenDoc, td) => {
  126. td ??= {};
  127. td.elevation = customTokenData.elevation;
  128. tokenDoc.updateSource({elevation: customTokenData.elevation});
  129. });
  130. warpgate.spawnAt(
  131. { x: posData.x, y: posData.y},
  132. tokenData,
  133. customTokenData || {},
  134. {},
  135. { duplicates }
  136. );
  137. console.log("Automated Evocations Summoning:", {
  138. assignedActor: this.caster || game?.user?.character || _token?.actor,
  139. spellLevel: this.spellLevel || 0,
  140. duplicates: duplicates,
  141. warpgateData: customTokenData || {},
  142. summon: actor,
  143. tokenData: tokenData,
  144. posData: posData,
  145. })
  146. if(game.settings.get(AECONSTS.MN, "autoclose")) this.close();
  147. else this.maximize();
  148. }
  149. async evaluateExpression(expression, ...args) {
  150. if (!expression) return null;
  151. const AsyncFunction = (async function () {}).constructor;
  152. const fn = new AsyncFunction("args" ,$("<span />", { html: expression }).text());
  153. try {
  154. return await fn(args);
  155. } catch(e) {
  156. ui.notifications.error("There was an error in your macro syntax. See the console (F12) for details");
  157. console.error(e);
  158. return undefined;
  159. }
  160. }
  161. async _onRemoveCompanion(event) {
  162. Dialog.confirm({
  163. title: game.i18n.localize("AE.dialogs.companionManager.confirm.title"),
  164. content: game.i18n.localize(
  165. "AE.dialogs.companionManager.confirm.content"
  166. ),
  167. yes: () => {
  168. event.currentTarget.parentElement.remove();
  169. this.saveData();
  170. },
  171. no: () => {},
  172. defaultYes: false,
  173. });
  174. }
  175. async _onOpenSheet(event) {
  176. const actorId = event.currentTarget.parentElement.dataset.aid;
  177. const actor = game.actors.get(actorId);
  178. if (actor) {
  179. actor.sheet.render(true);
  180. }
  181. }
  182. async loadCompanions() {
  183. 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");
  184. if (data) {
  185. for (let companion of data) {
  186. this.element.find("#companion-list").append(this.generateLi(companion));
  187. }
  188. }
  189. }
  190. generateLi(data) {
  191. const actor = game.actors.get(data.id) || game.actors.getName(data.id);
  192. if (!actor) return "";
  193. const restricted = game.settings.get(AECONSTS.MN, "restrictOwned")
  194. if(restricted && !actor.isOwner) return "";
  195. let $li = $(`
  196. <li id="companion" class="companion-item" data-aid="${
  197. actor.id
  198. }" data-elid="${randomID()}" draggable="true">
  199. <div class="summon-btn">
  200. <img class="actor-image" src="${actor.img}" alt="">
  201. <div class="warpgate-btn" id="summon-companion" data-aid="${actor.id}"></div>
  202. </div>
  203. <span class="actor-name">${actor.name}</span>
  204. <div class="companion-number"><input type="number" min="1" max="99" class="fancy-input" step="1" id="companion-number-val" value="${
  205. data.number || 1
  206. }"></div>
  207. <select class="anim-dropdown">
  208. ${this.getAnimations(data.animation)}
  209. </select>
  210. <i id="remove-companion" class="fas fa-trash"></i>
  211. </li>
  212. `);
  213. // <i id="advanced-params" class="fas fa-edit"></i>
  214. return $li;
  215. }
  216. getAnimations(anim) {
  217. let animList = "";
  218. for (let [group, animations] of Object.entries(AECONSTS.animations)) {
  219. const localGroup = game.i18n.localize(`AE.groups.${group}`)
  220. animList+=`<optgroup label="${localGroup == `AE.groups.${group}` ? group : localGroup}">`;
  221. for (let a of animations) {
  222. animList += `<option value="${a.key}" ${
  223. a.key == anim ? "selected" : ""
  224. }>${a.name}</option>`;
  225. }
  226. animList += "</optgroup>";
  227. }
  228. return animList;
  229. }
  230. async wait(ms) {
  231. return new Promise((resolve) => setTimeout(resolve, ms));
  232. }
  233. async saveData() {
  234. let data = [];
  235. for (let companion of this.element.find(".companion-item")) {
  236. data.push({
  237. id: companion.dataset.aid,
  238. animation: $(companion).find(".anim-dropdown").val(),
  239. number: $(companion).find("#companion-number-val").val(),
  240. });
  241. }
  242. 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);
  243. }
  244. close(noSave = false) {
  245. if (!noSave) this.saveData();
  246. super.close();
  247. }
  248. }
  249. class SimpleCompanionManager extends CompanionManager {
  250. constructor(summonData,spellLevel,actor) {
  251. super();
  252. this.caster = actor;
  253. this.summons = summonData;
  254. this.spellLevel = spellLevel
  255. }
  256. async activateListeners(html) {
  257. for (let summon of this.summons) {
  258. this.element.find("#companion-list").append(this.generateLi(summon));
  259. }
  260. html.on("click", "#summon-companion", this._onSummonCompanion.bind(this));
  261. html.on("click", ".actor-name", this._onOpenSheet.bind(this));
  262. }
  263. _onDrop(event) {}
  264. close() {
  265. super.close(true);
  266. }
  267. }