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.

241 lines
9.9 KiB

  1. /**
  2. * Registers core commands for autocompletion.
  3. * @private
  4. */
  5. export const registerCoreCommands = function () {
  6. if (!game.settings.get("_chatcommands", "includeCoreCommands")) return;
  7. const commands = game.chatCommands;
  8. registerMessageCommands(commands);
  9. registerRollCommands(commands);
  10. }
  11. /**
  12. * Registers core commands for modifying messages.
  13. * @param {ChatCommands} commands The game's chat command API instance.
  14. * @private
  15. */
  16. function registerMessageCommands(commands) {
  17. commands.register({
  18. name: "/ic",
  19. module: "core",
  20. icon: "<i class='fas fa-id-badge'></i>",
  21. description: game.i18n.localize("_chatcommands.coreCommands.ic")
  22. });
  23. commands.register({
  24. name: "/ooc",
  25. module: "core",
  26. icon: "<i class='fas fa-chalkboard-user'></i>",
  27. description: game.i18n.localize("_chatcommands.coreCommands.ooc")
  28. });
  29. commands.register({
  30. name: "/emote",
  31. module: "core",
  32. aliases: ["/em", "/me"],
  33. icon: "<i class='fas fa-address-card'></i>",
  34. description: game.i18n.localize("_chatcommands.coreCommands.emote")
  35. });
  36. commands.register({
  37. name: "/whisper",
  38. module: "core",
  39. aliases: ["/w", "@"],
  40. icon: "<i class='fas fa-message'></i>",
  41. description: game.i18n.localize("_chatcommands.coreCommands.whisper.description"),
  42. autocompleteCallback: game.modules.get("autocomplete-whisper")?.active ? () => [] : completeWhisper
  43. });
  44. }
  45. /**
  46. * Creates entries for completing user names and suggests bracket syntax as needed.
  47. * @param {AutocompleteMenu} menu The menu that initiated the completion process.
  48. * @param {string} alias The alias of the command.
  49. * @param {string} parameters The parameters of the command.
  50. * @returns {HTMLElement[]} The HTML elements containing syntax and recent roll entries.
  51. * @private
  52. */
  53. function completeWhisper(menu, alias, parameters) {
  54. let userNames = ["GM", "Players"].concat(Array.from(game.users.values()).map(u => u.name));
  55. const included = [];
  56. const suggested = [];
  57. const candidates = parameters.split(',');
  58. let match = false;
  59. for (let i = 0; i < candidates.length; i++) {
  60. // Strip whitespace, brackets and capitalization.
  61. const name = candidates[i].toLowerCase().replace(/^[\s\[]*|[\s\]]*$/g, "");
  62. match = false;
  63. // Check all users that haven't already been added.
  64. for (let userName of userNames.filter(n => !included.includes(n))) {
  65. if (userName.toLowerCase() === name) {
  66. // Exact match, include the user.
  67. match = true;
  68. included.push(userName);
  69. break;
  70. } else if (i === candidates.length - 1 && (name === "" || userName.toLowerCase().includes(name))) {
  71. // Last entry may not have exact match, suggest the user if the name could match the input.
  72. match = true;
  73. suggested.push(userName);
  74. }
  75. }
  76. // Stop searching if any input doesn't have a match (which likely means a comma within the message).
  77. if (!match) break;
  78. }
  79. // Not all candidates have a match, close the menu.
  80. if (!match) return [];
  81. // There are no suggestions, check if the paramters still need syntax adjustments.
  82. const needsRedirect = alias === "@";
  83. if (!suggested.length) {
  84. const needsBrackets = included.length > 1 || included[0]?.includes(' ');
  85. const hasBrackets = parameters.startsWith('[') && parameters.trimEnd().endsWith(']');
  86. if (included.length && (needsRedirect || (hasBrackets !== needsBrackets))) {
  87. // Suggest /w without displaying a menu.
  88. menu.suggest(needsBrackets ? `/w [${included.join(", ")}]` : "/w " + included.join(", "));
  89. }
  90. return [];
  91. }
  92. let prefix = needsRedirect ? "/w " : alias + " "; // Redirect @ to /w
  93. if (included.length) {
  94. // There already is a user in the parameters, suggest multi user syntax.
  95. return suggested.map(n => createUserElement(`${prefix}[${included.join(", ")}, ${n}]`, n));
  96. } else {
  97. // We don't have a user yet, suggest single user syntax.
  98. return suggested.map(n => {
  99. const completedName = n.includes(' ') ? `[${n}]` : n; // Handle user names with spaces.
  100. return createUserElement(prefix + completedName, n);
  101. });
  102. }
  103. }
  104. /**
  105. * Creates a command element for the given user's name. This may add additional information when the name is a
  106. * special string rather than a user's name, e.g. "GM" or "Players".
  107. * @param {string} command The command to complete when selecting the user.
  108. * @param {string} name The name of the user.
  109. * @returns {HTMLElement} An HTML element containing the command entry for the user.
  110. * @private
  111. */
  112. function createUserElement(command, name) {
  113. let content = name;
  114. if (name === "GM") {
  115. const gmLocal = game.i18n.localize("_chatcommands.coreCommands.whisper.gm");
  116. if (gmLocal !== name) content += ` (${gmLocal})`;
  117. content += ` <span class="notes">${game.users.filter(u => u.isGM).map(u => u.name).join(", ")}</span>`;
  118. } else if (name === "Players") {
  119. const playerLocal = game.i18n.localize("_chatcommands.coreCommands.whisper.players");
  120. if (playerLocal !== name) content += ` (${playerLocal})`;
  121. content += ` <span class="notes">${game.users.filter(u => !u.isGM).map(u => u.name).join(", ")}</span>`;
  122. }
  123. return game.chatCommands.createCommandElement(command, content);
  124. }
  125. /**
  126. * Registers hooks and core commands for displaying roll syntax and recent rolls.
  127. * @param {ChatCommands} commands The game's chat command API instance.
  128. * @private
  129. */
  130. function registerRollCommands(commands) {
  131. // Register setting to store recent rolls per user.
  132. game.settings.register("_chatcommands", "recentRolls", {
  133. name: "Recent dice rolls",
  134. scope: 'client',
  135. config: false,
  136. type: Array,
  137. default: []
  138. });
  139. // Register hook to track recent rolls for core commands.
  140. Hooks.on("invokeChatCommand", (_, command, parameters) => {
  141. if (!command.name.endsWith("roll") || command.module !== "core") return;
  142. const recentRolls = game.settings.get("_chatcommands", "recentRolls");
  143. const existingRoll = recentRolls.indexOf(parameters);
  144. if (existingRoll !== -1) recentRolls.splice(existingRoll, 1);
  145. recentRolls.unshift(parameters);
  146. game.settings.set("_chatcommands", "recentRolls", recentRolls.slice(0, 5));
  147. });
  148. commands.register({
  149. name: "/roll",
  150. module: "core",
  151. aliases: ["/r"],
  152. icon: "<i class='fas fa-dice'></i>",
  153. description: game.i18n.localize("_chatcommands.coreCommands.roll.basic"),
  154. autocompleteCallback: completeDice,
  155. closeOnComplete: false
  156. });
  157. commands.register({
  158. name: "/gmroll",
  159. module: "core",
  160. aliases: ["/gmr"],
  161. icon: "<i class='fas fa-dice-two'></i>",
  162. description: game.i18n.localize("_chatcommands.coreCommands.roll.gm"),
  163. autocompleteCallback: completeDice,
  164. closeOnComplete: false
  165. });
  166. commands.register({
  167. name: "/blindroll",
  168. module: "core",
  169. aliases: ["/broll", "/br"],
  170. icon: "<i class='fas fa-eye-slash'></i>",
  171. description: game.i18n.localize("_chatcommands.coreCommands.roll.blind"),
  172. autocompleteCallback: completeDice,
  173. closeOnComplete: false
  174. });
  175. commands.register({
  176. name: "/selfroll",
  177. module: "core",
  178. aliases: ["/sr"],
  179. icon: "<i class='fas fa-dice-one'></i>",
  180. description: game.i18n.localize("_chatcommands.coreCommands.roll.self"),
  181. autocompleteCallback: completeDice,
  182. closeOnComplete: false
  183. });
  184. commands.register({
  185. name: "/publicroll",
  186. module: "core",
  187. aliases: ["/pr"],
  188. icon: "<i class='fas fa-dice-five'></i>",
  189. description: game.i18n.localize("_chatcommands.coreCommands.roll.public"),
  190. autocompleteCallback: completeDice,
  191. closeOnComplete: false
  192. });
  193. }
  194. /**
  195. * Creates a set of menu entries to display roll syntax information and suggest recent rolls. Some info entries may
  196. * be skipped if the menu doesn't have enough space.
  197. * @param {AutocompleteMenu} menu The menu that initiated the completion process.
  198. * @param {string} alias The alias of the command.
  199. * @param {string} parameters The parameters of the command.
  200. * @returns {HTMLElement[]} The HTML elements containing syntax and recent roll entries.
  201. * @private
  202. */
  203. function completeDice(menu, alias, parameters) {
  204. const commands = game.chatCommands;
  205. const recentRolls = game.settings.get("_chatcommands", "recentRolls")
  206. .slice()
  207. .filter(r => r.includes(parameters))
  208. .map(r => commands.createCommandElement(alias + " " + r, r));
  209. let info;
  210. if (menu.maxEntries >= 10) {
  211. info = [
  212. commands.createInfoElement(game.i18n.localize("_chatcommands.coreCommands.roll.simpleInfo")),
  213. commands.createInfoElement(game.i18n.localize("_chatcommands.coreCommands.roll.descriptionInfo")),
  214. commands.createInfoElement(game.i18n.localize("_chatcommands.coreCommands.roll.modifierInfo")),
  215. commands.createInfoElement(game.i18n.localize("_chatcommands.coreCommands.roll.advancedInfo"))
  216. ];
  217. if (!recentRolls.length) return info;
  218. info.push(commands.createInfoElement(`<hr><p class="notes">
  219. ${game.i18n.localize("_chatcommands.coreCommands.roll.recent")}</p>`));
  220. } else if (menu.maxEntries > 5) {
  221. info = [commands.createInfoElement(game.i18n.localize("_chatcommands.coreCommands.roll.simpleInfo"))];
  222. } else {
  223. return recentRolls;
  224. }
  225. return info.concat(recentRolls);
  226. }