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.

544 lines
20 KiB

1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
  1. import { i18n } from "./util.js";
  2. import { Avatar, ActorAvatar, TokenAvatar, CombatantAvatar } from "./consts.js";
  3. const rgb2hex = (rgb) =>
  4. `#${rgb
  5. .match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/)
  6. .slice(1)
  7. .map((n) => parseInt(n, 10).toString(16).padStart(2, "0"))
  8. .join("")}`;
  9. // second return value is whether the first value can be styled as pf2e-icon
  10. function getActionGlyph(actionCost) {
  11. if (actionCost === "1 to 3") return ["1 / 2 / 3", true];
  12. if (actionCost === "1 or 2") return ["1 / 2", true];
  13. if (actionCost === "2 or 3") return ["2 / 3", true];
  14. if (actionCost.type === "action") {
  15. return [actionCost.value, true];
  16. } else if (actionCost.type === "reaction") return ["R", true];
  17. else if (actionCost.type === "free") return ["F", true];
  18. else if (actionCost.length === 1) return [actionCost, true];
  19. else return [actionCost, false];
  20. }
  21. // Chat cards
  22. Hooks.on("renderChatMessage", (chatMessage, html, messageData) => {
  23. const isNarratorToolsMessage = chatMessage.flags["narrator-tools"];
  24. const isMLDRoundMarker = chatMessage.flags["monks-little-details"]?.roundmarker;
  25. const isMCDRoundMarker = chatMessage.flags["monks-combat-details"]?.roundmarker;
  26. const isRoundMarker = isMLDRoundMarker || isMCDRoundMarker;
  27. if (isNarratorToolsMessage || isRoundMarker) {
  28. return;
  29. }
  30. if (game.settings.get("pf2e-dorako-ux", "moving.restructure-card-info")) {
  31. let uuid = chatMessage?.flags?.pf2e?.origin?.uuid;
  32. if (uuid) {
  33. try {
  34. let origin = fromUuidSync(uuid);
  35. let actionCost = origin?.actionCost;
  36. if (actionCost) injectActionCost(html, actionCost);
  37. if (origin?.type === "spell") injectSpellInfo(html, origin);
  38. } catch (error) {
  39. // An error is thrown if the UUID is a reference to something that is not loaded, like an actor in a compendium.
  40. }
  41. }
  42. }
  43. injectSenderWrapper(html, messageData);
  44. injectMessageTag(html, messageData);
  45. injectWhisperParticipants(html, messageData);
  46. injectAuthorName(html, messageData);
  47. if (
  48. (game.settings.get("pf2e-dorako-ux", "avatar.hide-when-token-hidden") &&
  49. chatMessage.getFlag("pf2e-dorako-ux", "wasTokenHidden")) ||
  50. (game.settings.get("pf2e-dorako-ux", "avatar.hide-gm-avatar-when-secret") && !chatMessage.isContentVisible)
  51. ) {
  52. } else {
  53. injectAvatar(html, getAvatar(chatMessage));
  54. }
  55. moveFlavorTextToContents(html);
  56. });
  57. // Is damage roll
  58. Hooks.on("renderChatMessage", (chatMessage, html, messageData) => {
  59. if (!game.settings.get("pf2e-dorako-ux", "hiding.remove-attack-info-from-damage-roll-messages")) return;
  60. if (chatMessage?.isDamageRoll && chatMessage?.item?.type !== "spell") {
  61. html[0].classList.add("dorako-damage-roll");
  62. let flavor = html.find(".flavor-text");
  63. flavor.each(function () {
  64. $(this).contents().eq(1).wrap("<span/>");
  65. });
  66. }
  67. });
  68. // Is check roll
  69. Hooks.on("renderChatMessage", (chatMessage, html, messageData) => {
  70. if (!game.settings.get("pf2e-dorako-ux", "hiding.remove-attack-info-from-damage-roll-messages")) return;
  71. if (chatMessage?.isCheckRoll) {
  72. html.addClass("dorako-check-roll");
  73. }
  74. });
  75. // "Be last" magic trick. Should ensure that any other modules that modify, say, who spoke the message, have done so before you add the flags.
  76. Hooks.once("ready", () => {
  77. Hooks.on("preCreateChatMessage", (message) => {
  78. addAvatarsToFlags(message);
  79. message.updateSource({
  80. "flags.pf2e-dorako-ux.wasTokenHidden": message?.token?.hidden,
  81. });
  82. });
  83. Hooks.on("updateChatMessage", (message) => {
  84. addAvatarsToFlags(message);
  85. });
  86. Hooks.on("renderChatMessage", (app, html, data) => {
  87. const isKoboldWorksTurnAnnouncerMessage = app.flags["koboldworks-turn-announcer"];
  88. if (!isKoboldWorksTurnAnnouncerMessage) return;
  89. const avatar = html.find(".portrait");
  90. avatar.css("transform", `scale(${app.flags["pf2e-dorako-ux"]?.tokenAvatar.scale})`);
  91. avatar.css("flex", `0px 0px var(--avatar-size)`);
  92. avatar.css("height", `var(--avatar-size)`);
  93. avatar.css("width", `var(--avatar-size)`);
  94. });
  95. });
  96. function moveFlavorTextToContents(html) {
  97. let flavor = html.find(".flavor-text")[0];
  98. let contents = html.find(".message-content")[0];
  99. if (flavor) contents.prepend(flavor);
  100. }
  101. function injectSenderWrapper(html, messageData) {
  102. if (messageData.author === undefined) return;
  103. var target = html.find(".message-sender")[0];
  104. var wrapper = document.createElement("div");
  105. wrapper.classList.add("sender-wrapper");
  106. target.parentNode.insertBefore(wrapper, target);
  107. wrapper.appendChild(target);
  108. }
  109. function injectAvatar(html, avatar) {
  110. if (!avatar) return;
  111. let messageHeader = html.find(".message-header")[0];
  112. let portraitAndName = document.createElement("div");
  113. portraitAndName.classList.add("portrait-and-name");
  114. messageHeader.prepend(portraitAndName);
  115. let wrapper = document.createElement("div");
  116. wrapper.classList.add("portrait-wrapper");
  117. let portrait = document.createElement("img");
  118. portrait.classList.add("avatar");
  119. portrait.classList.add("portrait");
  120. wrapper.append(portrait);
  121. let senderWrapper = html.find(".sender-wrapper")[0];
  122. portraitAndName.append(senderWrapper);
  123. portraitAndName.prepend(wrapper);
  124. }
  125. function injectActionCost(html, actionCost) {
  126. if (!actionCost) return;
  127. const [actionGlyph, shouldBeStyled] = getActionGlyph(actionCost);
  128. if (!actionGlyph) return;
  129. // console.log("Injecting actionGlyph %s", actionGlyph);
  130. let messageHeader = html.find(".card-header")[0];
  131. let actionGlyphText = document.createElement("h3");
  132. html.find(".action-glyph")?.get(0).remove(); // Remove action-glyph added by system
  133. if (shouldBeStyled) actionGlyphText.classList.add("pf2-icon");
  134. actionGlyphText.textContent = actionGlyph;
  135. messageHeader.append(actionGlyphText);
  136. }
  137. function localizeComponent(componentKey) {
  138. if (componentKey === "focus") return i18n("PF2E.SpellComponentF");
  139. if (componentKey === "material") return i18n("PF2E.SpellComponentM");
  140. if (componentKey === "somatic") return i18n("PF2E.SpellComponentS");
  141. if (componentKey === "verbal") return i18n("PF2E.SpellComponentV");
  142. }
  143. function spellComponentsToText(components) {
  144. // console.log(components);
  145. const asArray = Object.entries(components);
  146. // console.log(asArray);
  147. const filtered = asArray.filter(([key, value]) => value);
  148. // console.log(filtered);
  149. const localized = filtered.map(([key, value]) => localizeComponent(key));
  150. // console.log(localized);
  151. const combined = localized.join(", ");
  152. return " " + combined.toLowerCase();
  153. }
  154. function injectSpellInfo(html, spell) {
  155. if (!spell) return;
  156. let messageHeader = html.find(".card-content")[0];
  157. let spellInfo = document.createElement("div");
  158. spellInfo.classList.add("spell-info");
  159. // console.log(spell);
  160. // Cast time + components
  161. let time = spell?.system?.time?.value;
  162. let components = spell?.system?.components;
  163. let elem = document.createElement("p");
  164. let castInfoLabel = document.createElement("strong");
  165. castInfoLabel.textContent = i18n("PF2E.CastLabel") + " ";
  166. let castTime = document.createElement("span");
  167. const [actionCost, shouldBeGlyph] = getActionGlyph(time);
  168. castTime.textContent = actionCost;
  169. if (shouldBeGlyph) castTime.classList.add("pf2-icon");
  170. let castComponents = document.createElement("span");
  171. // castComponents.textContent = spellComponentsToText(components);
  172. elem.append(castInfoLabel);
  173. elem.append(castTime);
  174. // elem.append(castComponents);
  175. spellInfo.append(elem);
  176. // Cost info (note: not cast time, material cost)
  177. let cost = spell?.system?.cost?.value;
  178. if (cost) {
  179. let elem = document.createElement("p");
  180. let label = document.createElement("strong");
  181. label.textContent = i18n("PF2E.SpellCostLabel") + " ";
  182. let value = document.createElement("span");
  183. value.textContent = cost;
  184. elem.append(label);
  185. elem.append(value);
  186. spellInfo.append(elem);
  187. }
  188. let secondarycasters = spell?.system?.secondarycasters?.value;
  189. if (secondarycasters) {
  190. let info = document.createElement("p");
  191. let label = document.createElement("strong");
  192. label.textContent = i18n("PF2E.SpellSecondaryCasters") + " ";
  193. let value = document.createElement("span");
  194. value.textContent = secondarycasters;
  195. info.append(label);
  196. info.append(value);
  197. spellInfo.append(info);
  198. }
  199. let primarycheck = spell?.system?.primarycheck?.value;
  200. if (primarycheck) {
  201. let info = document.createElement("p");
  202. let label = document.createElement("strong");
  203. label.textContent = i18n("PF2E.SpellPrimaryCheckLabel") + " ";
  204. let value = document.createElement("span");
  205. value.textContent = primarycheck;
  206. info.append(label);
  207. info.append(value);
  208. spellInfo.append(info);
  209. }
  210. let secondarycheck = spell?.system?.secondarycheck?.value;
  211. if (secondarycheck) {
  212. let info = document.createElement("p");
  213. let label = document.createElement("strong");
  214. label.textContent = i18n("PF2E.SpellSecondaryChecksLabel") + " ";
  215. let value = document.createElement("span");
  216. value.textContent = secondarycheck;
  217. info.append(label);
  218. info.append(value);
  219. spellInfo.append(info);
  220. }
  221. // Target info
  222. let target = spell?.system?.target?.value;
  223. if (target) {
  224. // console.log(target);
  225. let targetInfo = document.createElement("p");
  226. let targetInfoLabel = document.createElement("strong");
  227. targetInfoLabel.textContent = i18n("PF2E.SpellTargetLabel") + " ";
  228. let targetValue = document.createElement("span");
  229. targetValue.textContent = target;
  230. targetInfo.append(targetInfoLabel);
  231. targetInfo.append(targetValue);
  232. spellInfo.append(targetInfo);
  233. }
  234. // Range info
  235. let range = spell?.system?.range?.value;
  236. if (range) {
  237. // console.log(range);
  238. let rangeInfo = document.createElement("p");
  239. let rangeInfoLabel = document.createElement("strong");
  240. rangeInfoLabel.textContent = i18n("PF2E.SpellRangeLabel") + " ";
  241. let rangeValue = document.createElement("span");
  242. rangeValue.textContent = range;
  243. rangeInfo.append(rangeInfoLabel);
  244. rangeInfo.append(rangeValue);
  245. spellInfo.append(rangeInfo);
  246. }
  247. // Area info
  248. let area = spell?.system?.area?.value;
  249. if (area) {
  250. // console.log(area);
  251. let areaInfo = document.createElement("p");
  252. let areaInfoLabel = document.createElement("strong");
  253. areaInfoLabel.textContent = i18n("PF2E.AreaLabel") + " ";
  254. let areaType = spell?.system?.area?.type;
  255. let areaTypeLabel = areaType
  256. ? i18n("PF2E.AreaType" + areaType.charAt(0).toUpperCase() + areaType.slice(1)).toLowerCase()
  257. : "";
  258. let areaValue = document.createElement("span");
  259. areaValue.textContent = area + " " + i18n("PF2E.Foot").toLowerCase() + " " + areaTypeLabel;
  260. areaInfo.append(areaInfoLabel);
  261. areaInfo.append(areaValue);
  262. spellInfo.append(areaInfo);
  263. }
  264. // Duration info
  265. let duration = spell?.system?.duration?.value;
  266. if (duration) {
  267. // console.log(duration);
  268. let durationInfo = document.createElement("p");
  269. let durationInfoLabel = document.createElement("strong");
  270. durationInfoLabel.textContent = i18n("PF2E.SpellDurationLabel") + " ";
  271. let durationValue = document.createElement("span");
  272. durationValue.textContent = duration;
  273. durationInfo.append(durationInfoLabel);
  274. durationInfo.append(durationValue);
  275. spellInfo.append(durationInfo);
  276. }
  277. let hr = document.createElement("hr");
  278. // Heightening info
  279. let spellRightInfo = html.find(".card-header").find("h4")[0];
  280. let originalText = spellRightInfo.textContent;
  281. const [_, spellType, parsedLevel] = originalText.split(/(.*) (\d+)/);
  282. const baseLevel = spell?.baseLevel;
  283. const actualLevel = spell?.level;
  284. if (baseLevel != parsedLevel) {
  285. let heighteningInfo = document.createElement("h4");
  286. let spellTypeSpan = document.createElement("span");
  287. spellTypeSpan.textContent = spellType + " ";
  288. let originalLevel = document.createElement("s");
  289. originalLevel.textContent = baseLevel;
  290. let heightenedLevel = document.createElement("span");
  291. heightenedLevel.classList.add("heightened");
  292. heightenedLevel.textContent = " " + parsedLevel;
  293. heighteningInfo.append(spellTypeSpan);
  294. heighteningInfo.append(originalLevel);
  295. heighteningInfo.append(heightenedLevel);
  296. spellRightInfo.parentNode.replaceChild(heighteningInfo, spellRightInfo);
  297. }
  298. // Footer
  299. let footer = html.find(".card-footer")[0];
  300. if (footer) footer.classList.add("dorako-display-none");
  301. messageHeader.prepend(hr);
  302. messageHeader.prepend(spellInfo);
  303. }
  304. function injectAuthorName(html, messageData) {
  305. if (messageData.author === undefined) return;
  306. if (game.settings.get("pf2e-dorako-ux", "other.enable-player-tags")) {
  307. const messageSenderElem = html.find(".sender-wrapper");
  308. const playerName = messageData.author.name;
  309. const playerNameElem = document.createElement("span");
  310. playerNameElem.appendChild(document.createTextNode(playerName));
  311. playerNameElem.classList.add("player-name");
  312. playerNameElem.classList.add("header-meta");
  313. if (playerName === messageData.alias) {
  314. html.find(".message-sender").addClass("dorako-display-none");
  315. }
  316. messageSenderElem.append(playerNameElem);
  317. }
  318. }
  319. function injectMessageTag(html, messageData) {
  320. const setting = game.settings.get("pf2e-dorako-ux", "other.enable-rolltype-indication");
  321. if (setting == false) {
  322. return;
  323. }
  324. const messageMetadata = html.find(".message-metadata");
  325. const rolltype = $("<span>");
  326. rolltype.addClass("rolltype");
  327. rolltype.addClass("header-meta");
  328. const whisperTargets = messageData.message.whisper;
  329. const isBlind = messageData.message.blind;
  330. if (isBlind) rolltype.addClass("blind");
  331. const isWhisper = whisperTargets?.length > 0;
  332. if (isWhisper && !isBlind) rolltype.addClass("whisper");
  333. const isSelf = isWhisper && whisperTargets.length === 1 && whisperTargets[0] === messageData.message.user;
  334. const isRoll = messageData.message.rolls !== undefined;
  335. if (isBlind) {
  336. rolltype.text(i18n("pf2e-dorako-ux.text.secret"));
  337. messageMetadata.prepend(rolltype);
  338. } else if (isSelf && whisperTargets[0]) {
  339. rolltype.text(i18n("pf2e-dorako-ux.text.self-roll"));
  340. messageMetadata.prepend(rolltype);
  341. } else if (isRoll && isWhisper) {
  342. rolltype.text(i18n("pf2e-dorako-ux.text.gm-only"));
  343. messageMetadata.prepend(rolltype);
  344. } else if (isWhisper) {
  345. rolltype.text(i18n("pf2e-dorako-ux.text.whisper"));
  346. messageMetadata.prepend(rolltype);
  347. }
  348. if (game.settings.get("pf2e-dorako-ux", "moving.animate-messages")) {
  349. // Draw attention to direct whispers from players to GM
  350. const isGmSpeaker = game.users.get(messageData.message.user)?.isGM;
  351. const isGmTarget = game.users.get(whisperTargets?.[0])?.isGM;
  352. if (!(isBlind || isSelf) && isWhisper && !isGmSpeaker && isGmTarget) {
  353. html[0].classList.add("attention");
  354. }
  355. }
  356. }
  357. function injectWhisperParticipants(html, messageData) {
  358. const alias = messageData.alias;
  359. const author = messageData.author;
  360. const whisperTargets = messageData.message.whisper;
  361. const whisperTargetString = messageData.whisperTo;
  362. const whisperTargetIds = messageData.message.whisper;
  363. const isWhisper = whisperTargetIds?.length > 0 || false;
  364. const isRoll = messageData.message.rolls !== undefined;
  365. const isSelf =
  366. (isWhisper && whisperTargets.length === 1 && whisperTargets[0] === messageData.message.user) ||
  367. (isWhisper &&
  368. whisperTargets.length === 2 &&
  369. whisperTargets[0] === "null" &&
  370. whisperTargets[1] === messageData.message.user);
  371. const authorId = messageData.message.user;
  372. const userId = game.user.id;
  373. if (!isWhisper) return;
  374. if (userId !== authorId && !whisperTargetIds.includes(userId)) return;
  375. // remove the old whisper to content, if it exists
  376. html.find(".whisper-to").detach();
  377. // if this is a roll
  378. if (isRoll || isSelf) return;
  379. const messageHeader = html.find(".message-header");
  380. const whisperParticipants = $("<span>");
  381. whisperParticipants.addClass("whisper-to");
  382. const whisperFrom = $("<span>");
  383. const fromText = titleCase(i18n("pf2e-dorako-ux.text.from"));
  384. whisperFrom.text(`${fromText}: ${alias}`);
  385. whisperFrom.addClass("header-meta");
  386. const whisperTo = $("<span>");
  387. const toText = titleCase(i18n("pf2e-dorako-ux.text.to")).toLowerCase();
  388. whisperTo.text(`${toText}: ${whisperTargetString}`);
  389. whisperTo.addClass("header-meta");
  390. whisperParticipants.append(whisperFrom);
  391. whisperParticipants.append(whisperTo);
  392. messageHeader.append(whisperParticipants);
  393. }
  394. function addAvatarsToFlags(message) {
  395. let combatantImg =
  396. game.modules.get("combat-tracker-images")?.active && message.actor
  397. ? message.actor.getFlag("combat-tracker-images", "trackerImage")
  398. : null;
  399. let speaker = message.speaker;
  400. const token = game.scenes.get(speaker.scene)?.tokens.get(speaker.token);
  401. let tokenImg = token?.texture.src;
  402. const actor = game.actors.get(speaker.actor);
  403. let actorImg = actor?.img;
  404. let userImg = message.user?.avatar;
  405. let userAvatar = new Avatar(message.speaker.alias, userImg);
  406. let combatantAvatar = combatantImg ? new CombatantAvatar(message.speaker.alias, combatantImg) : null;
  407. let actorAvatar = actorImg ? new ActorAvatar(message.speaker.alias, actorImg) : null;
  408. let tokenAvatar = null;
  409. if (tokenImg) {
  410. tokenAvatar = new TokenAvatar(message.speaker.alias, tokenImg, token.texture.scaleX, actor.size == "sm");
  411. }
  412. message.updateSource({
  413. "flags.pf2e-dorako-ux.userAvatar": userAvatar,
  414. "flags.pf2e-dorako-ux.combatantAvatar": combatantAvatar,
  415. "flags.pf2e-dorako-ux.tokenAvatar": tokenAvatar,
  416. "flags.pf2e-dorako-ux.actorAvatar": actorAvatar,
  417. });
  418. }
  419. function getAvatar(message) {
  420. const source = game.settings.get("pf2e-dorako-ux", "avatar.source");
  421. if (source == "none") {
  422. return null;
  423. }
  424. let combatantAvatar = message.getFlag("pf2e-dorako-ux", "combatantAvatar");
  425. let tokenAvatar = message.getFlag("pf2e-dorako-ux", "tokenAvatar");
  426. let actorAvatar = message.getFlag("pf2e-dorako-ux", "actorAvatar");
  427. let userAvatar = game.settings.get("pf2e-dorako-ux", "avatar.use-user-avatar")
  428. ? message.getFlag("pf2e-dorako-ux", "userAvatar")
  429. : null;
  430. if (combatantAvatar) return combatantAvatar;
  431. if (
  432. game.settings.get("pf2e-dorako-ux", "avatar.hide-when-token-hidden") &&
  433. message.getFlag("pf2e-dorako-ux", "wasTokenHidden")
  434. ) {
  435. return null;
  436. }
  437. return source == "token" ? tokenAvatar || actorAvatar || userAvatar : actorAvatar || tokenAvatar || userAvatar;
  438. }
  439. // Add avatar if message contains avatar data
  440. Hooks.on("renderChatMessage", (message, b) => {
  441. let avatar = getAvatar(message);
  442. if (!avatar) return;
  443. let html = b[0];
  444. let avatarElem = html.getElementsByClassName("avatar")[0];
  445. if (!avatarElem) return;
  446. avatarElem.src = avatar.image;
  447. if (avatar.type == "token") {
  448. const smallScale = game.settings.get("pf2e-dorako-ux", "avatar.small-creature-token-avatar-size");
  449. let smallCorrection = avatar.isSmall ? 1.25 * smallScale : 1;
  450. avatarElem?.setAttribute("style", "transform: scale(" + Math.abs(avatar.scale) * smallCorrection + ")");
  451. }
  452. const portraitDegreeSetting = game.settings.get("pf2e-dorako-ux", "avatar.reacts-to-degree-of-success");
  453. if (portraitDegreeSetting && message.isContentVisible) {
  454. let outcome = message?.flags?.pf2e?.context?.outcome;
  455. if (outcome === undefined) return;
  456. if (outcome === "criticalFailure") {
  457. let wrapper = html.getElementsByClassName("portrait-wrapper")[0];
  458. wrapper?.setAttribute("style", "filter: saturate(0.2) drop-shadow(0px 0px 6px black)");
  459. } else if (outcome === "criticalSuccess") {
  460. let wrapper = html.getElementsByClassName("portrait-wrapper")[0];
  461. wrapper?.setAttribute("style", "filter: drop-shadow(0px 0px 6px lightgreen)");
  462. }
  463. }
  464. });
  465. // Add .spell to spells
  466. Hooks.on("renderChatMessage", (app, html, data) => {
  467. const item = app?.item;
  468. if (!item) return;
  469. if (!item.constructor.name.includes("SpellPF2e")) return;
  470. html[0].classList.add("spell");
  471. });