Guest User

better20 v1.6.2 -- backup edition

a guest
May 20th, 2018
1,187
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 383.33 KB | None | 0 0
  1. // ==UserScript==
  2. // @name betteR20-5etools
  3. // @namespace https://rem.uz/
  4. // @license MIT (https://opensource.org/licenses/MIT)
  5. // @version 1.6.2
  6. // @updateURL https://get.5etools.com/script/betteR20-5etools.user.js
  7. // @downloadURL https://get.5etools.com/script/betteR20-5etools.user.js
  8. // @description Enhance your Roll20 experience
  9. // @author 5egmegaanon/astranauta/MrLabRat/TheGiddyLimit/DBAWiseMan/BDeveau/Remuz/Callador Julaan/Erogroth
  10. // @match https://app.roll20.net/editor/
  11. // @grant unsafeWindow
  12. // @run-at document-start
  13. // ==/UserScript==
  14.  
  15.  
  16. ART_HANDOUT = "betteR20-art";
  17. CONFIG_HANDOUT = "betteR20-config";
  18.  
  19. SCRIPT_EXTENSIONS = [];
  20.  
  21. CONFIG_OPTIONS = {
  22. interface: {
  23. _name: "Interface",
  24. showCustomArtPreview: {
  25. name: "Show Custom Art Previews",
  26. default: true,
  27. _type: "boolean"
  28. }
  29. }
  30. };
  31.  
  32. addConfigOptions = function (category, options) {
  33. if (!CONFIG_OPTIONS[category]) CONFIG_OPTIONS[category] = options;
  34. else CONFIG_OPTIONS[category] = Object.assign(CONFIG_OPTIONS[category], options);
  35. };
  36.  
  37. OBJECT_DEFINE_PROPERTY = Object.defineProperty;
  38. Object.defineProperty = function (obj, prop, vals) {
  39. try {
  40. if (prop === "largefeats" || prop === "xlfeats") {
  41. vals.value = true;
  42. }
  43. OBJECT_DEFINE_PROPERTY(obj, prop, vals);
  44. } catch (e) {
  45. console.log("failed to define property:");
  46. console.log(e);
  47. console.log(obj, prop, vals);
  48. }
  49. };
  50.  
  51. UPPER_CANVAS_MOUSEDOWN = null;
  52. EventTarget.prototype.addEventListenerBase = EventTarget.prototype.addEventListener;
  53. EventTarget.prototype.addEventListener = function(type, listener, options, ...others) {
  54. if (type === "mousedown" && this === d20.engine.uppercanvas) UPPER_CANVAS_MOUSEDOWN = listener;
  55. this.addEventListenerBase(type, listener, options, ...others);
  56. };
  57.  
  58. const betteR205etools = function () {
  59. const BASE_SITE_URL = "https://thegiddylimit.github.io/";
  60. const DATA_URL = BASE_SITE_URL + "data/";
  61. const JS_URL = BASE_SITE_URL + "js/";
  62. const IMG_URL = BASE_SITE_URL + "img/";
  63.  
  64. const SPELL_DATA_DIR = `${DATA_URL}spells/`;
  65. const SPELL_META_URL = `${SPELL_DATA_DIR}roll20.json`;
  66. const MONSTER_DATA_DIR = `${DATA_URL}bestiary/`;
  67. const ADVENTURE_DATA_DIR = `${DATA_URL}adventure/`;
  68.  
  69. const ITEM_DATA_URL = `${DATA_URL}items.json`;
  70. const FEAT_DATA_URL = `${DATA_URL}feats.json`;
  71. const PSIONIC_DATA_URL = `${DATA_URL}psionics.json`;
  72. const OBJECT_DATA_URL = `${DATA_URL}objects.json`;
  73. const CLASS_DATA_URL = `${DATA_URL}classes.json`;
  74. const BACKGROUND_DATA_URL = `${DATA_URL}backgrounds.json`;
  75. const RACE_DATA_URL = `${DATA_URL}races.json`;
  76.  
  77. const HOMEBREW_REPO_URL = `https://api.github.com/repos/TheGiddyLimit/homebrew/`;
  78. // the GitHub API has a 60 requests/hour limit per IP which we quickly hit if the user refreshes their Roll20 a couple of times
  79. // embed shitty OAth2 details here to enable 5k/hour requests per IP (sending them with requests to the API relaxes the limit)
  80. // naturally these are client-visible and should not be used to secure anything
  81. const HOMEBREW_CLIENT_ID = `67e57877469da38a85a7`;
  82. const HOMEBREW_CLIENT_SECRET = `c00dede21ca63a855abcd9a113415e840aca3f92`;
  83.  
  84. const REQUIRED_PROPS = {
  85. "monster": [
  86. "ac",
  87. "alignment",
  88. "cha",
  89. "con",
  90. "cr",
  91. "dex",
  92. "hp",
  93. "int",
  94. "name",
  95. "passive",
  96. "size",
  97. "source",
  98. "speed",
  99. "str",
  100. "type",
  101. "wis"
  102. ],
  103. "spell": [
  104. "name",
  105. "level",
  106. "school",
  107. "time",
  108. "range",
  109. "components",
  110. "duration",
  111. "classes",
  112. "entries",
  113. "source"
  114. ],
  115. "item": [
  116. "name",
  117. "rarity",
  118. "source"
  119. ],
  120. "psionic": [
  121. "name",
  122. "source",
  123. "type"
  124. ],
  125. "feat": [
  126. "name",
  127. "source",
  128. "entries"
  129. ],
  130. "object": [
  131. "name",
  132. "source",
  133. "size",
  134. "type",
  135. "ac",
  136. "hp",
  137. "immune",
  138. "entries"
  139. ],
  140. "class": [
  141. "name",
  142. "source",
  143. "hd",
  144. "proficiency",
  145. "classTableGroups",
  146. "startingProficiencies",
  147. "startingEquipment",
  148. "classFeatures",
  149. "subclassTitle",
  150. "subclasses"
  151. ],
  152. "subclass": [
  153.  
  154. ],
  155. "background": [
  156. "name",
  157. "source",
  158. "skillProficiencies",
  159. "entries"
  160. ],
  161. "race": [
  162. "name",
  163. "source"
  164. ]
  165. };
  166.  
  167. let spellDataUrls = {};
  168. let spellMetaData = {};
  169. let monsterDataUrls = {};
  170. let monsterFluffDataUrls = {};
  171. let monsterFluffData = {};
  172. let adventureMetadata = {};
  173.  
  174. // build a big dictionary of sheet properties to be used as reference throughout // TODO use these as reference throughout
  175. function SheetAttribute (name, ogl, shaped) {
  176. this.name = name;
  177. this.ogl = ogl;
  178. this.shaped = shaped;
  179. }
  180.  
  181. NPC_SHEET_ATTRIBUTES = {};
  182. // these (other than the name, which is for display only) are all lowercased; any comparison should be lowercased
  183. NPC_SHEET_ATTRIBUTES["empty"] = new SheetAttribute("--Empty--", "", "");
  184. // TODO: implement custom entry (enable textarea)
  185. //NPC_SHEET_ATTRIBUTES["custom"] = new SheetAttribute("-Custom-", "-Custom-", "-Custom-");
  186. NPC_SHEET_ATTRIBUTES["npc_hpbase"] = new SheetAttribute("Avg HP", "npc_hpbase", "npc_hpbase");
  187. NPC_SHEET_ATTRIBUTES["npc_ac"] = new SheetAttribute("AC", "npc_ac", "ac");
  188. NPC_SHEET_ATTRIBUTES["passive"] = new SheetAttribute("Passive Perception", "passive", "passive");
  189. NPC_SHEET_ATTRIBUTES["npc_hpformula"] = new SheetAttribute("HP Formula", "npc_hpformula", "npc_hpformula");
  190. NPC_SHEET_ATTRIBUTES["npc_speed"] = new SheetAttribute("Speed", "npc_speed", "npc_speed");
  191. NPC_SHEET_ATTRIBUTES["spell_save_dc"] = new SheetAttribute("Spell Save DC", "spell_save_dc", "spell_save_DC");
  192. NPC_SHEET_ATTRIBUTES["npc_legendary_actions"] = new SheetAttribute("Legendary Actions", "npc_legendary_actions", "npc_legendary_actions");
  193. NPC_SHEET_ATTRIBUTES["npc_challenge"] = new SheetAttribute("CR", "npc_challenge", "challenge");
  194.  
  195. PC_SHEET_ATTRIBUTES = {};
  196. PC_SHEET_ATTRIBUTES["empty"] = new SheetAttribute("--Default--", "", "");
  197. PC_SHEET_ATTRIBUTES["hp"] = new SheetAttribute("Current HP", "hp", "HP");
  198. PC_SHEET_ATTRIBUTES["ac"] = new SheetAttribute("AC", "ac", "ac"); // TODO check shaped
  199. PC_SHEET_ATTRIBUTES["passive_wisdom"] = new SheetAttribute("Passive Perception", "passive_wisdom", "passive_wisdom"); // TODO check shaped
  200. PC_SHEET_ATTRIBUTES["speed"] = new SheetAttribute("Speed", "speed", "speed"); // TODO check shaped
  201. PC_SHEET_ATTRIBUTES["spell_save_dc"] = new SheetAttribute("Spell Save DC", "spell_save_dc", "spell_save_dc"); // TODO check shaped
  202.  
  203. addConfigOptions("token", {
  204. "_name": "Tokens",
  205. "bar1": {
  206. "name": "Bar 1 (NPC)",
  207. "default": "npc_hpbase",
  208. "_type": "_SHEET_ATTRIBUTE"
  209. },
  210. "bar1_pc": {
  211. "name": "Bar 1 (PC)",
  212. "default": "",
  213. "_type": "_SHEET_ATTRIBUTE_PC"
  214. },
  215. "bar1_max": {
  216. "name": "Set Bar 1 Max",
  217. "default": true,
  218. "_type": "boolean"
  219. },
  220. "bar1_reveal": {
  221. "name": "Reveal Bar 1",
  222. "default": false,
  223. "_type": "boolean"
  224. },
  225. "bar2": {
  226. "name": "Bar 2 (NPC)",
  227. "default": "npc_ac",
  228. "_type": "_SHEET_ATTRIBUTE"
  229. },
  230. "bar2_pc": {
  231. "name": "Bar 2 (PC)",
  232. "default": "",
  233. "_type": "_SHEET_ATTRIBUTE_PC"
  234. },
  235. "bar2_max": {
  236. "name": "Set Bar 2 Max",
  237. "default": false,
  238. "_type": "boolean"
  239. },
  240. "bar2_reveal": {
  241. "name": "Reveal Bar 2",
  242. "default": false,
  243. "_type": "boolean"
  244. },
  245. "bar3": {
  246. "name": "Bar 3 (NPC)",
  247. "default": "passive",
  248. "_type": "_SHEET_ATTRIBUTE"
  249. },
  250. "bar3_pc": {
  251. "name": "Bar 3 (PC)",
  252. "default": "",
  253. "_type": "_SHEET_ATTRIBUTE_PC"
  254. },
  255. "bar3_max": {
  256. "name": "Set Bar 3 Max",
  257. "default": false,
  258. "_type": "boolean"
  259. },
  260. "bar3_reveal": {
  261. "name": "Reveal Bar 3",
  262. "default": false,
  263. "_type": "boolean"
  264. },
  265. "rollHP": {
  266. "name": "Roll Token HP",
  267. "default": false,
  268. "_type": "boolean"
  269. },
  270. "maximiseHp": {
  271. "name": "Maximise Token HP",
  272. "default": false,
  273. "_type": "boolean"
  274. },
  275. "name": {
  276. "name": "Show Nameplate",
  277. "default": true,
  278. "_type": "boolean"
  279. },
  280. "name_reveal": {
  281. "name": "Reveal Nameplate",
  282. "default": false,
  283. "_type": "boolean"
  284. },
  285. "tokenactions": {
  286. "name": "Add TokenAction Macros on Import",
  287. "default": true,
  288. "_type": "boolean"
  289. },
  290. "namesuffix": {
  291. "name": "Append Text to Names on Import",
  292. "default": "",
  293. "_type": "String"
  294. }
  295. });
  296. addConfigOptions("import", {
  297. "_name": "Import",
  298. "importIntervalHandout": {
  299. "name": "Rest Time between Each Handout (msec)",
  300. "default": 100,
  301. "_type": "integer"
  302. },
  303. "importIntervalCharacter": {
  304. "name": "Rest Time between Each Character (msec)",
  305. "default": 2500,
  306. "_type": "integer"
  307. },
  308. "importFluffAs": {
  309. "name": "Import Creature Fluff As...",
  310. "default": "Bio",
  311. "_type": "_enum",
  312. "_values": ["Bio", "GM Notes"]
  313. },
  314. "whispermode": {
  315. "name": "Sheet Whisper Mode on Import",
  316. "default": "Toggle (Default GM)",
  317. "_type": "_WHISPERMODE"
  318. },
  319. "advantagemode": {
  320. "name": "Sheet Advantage Mode on Import",
  321. "default": "Toggle (Default Advantage)",
  322. "_type": "_ADVANTAGEMODE"
  323. },
  324. "damagemode": {
  325. "name": "Sheet Auto Roll Damage Mode on Import",
  326. "default": "Auto Roll",
  327. "_type": "_DAMAGEMODE"
  328. },
  329. "skipSenses": {
  330. "name": "Skip Importing Creature Senses",
  331. "default": false,
  332. "_type": "boolean"
  333. },
  334. });
  335. addConfigOptions("interface", {
  336. "_name": "Interface",
  337. "customTracker": {
  338. "name": "Add Additional Info to Tracker",
  339. "default": true,
  340. "_type": "boolean"
  341. },
  342. "trackerCol1": {
  343. "name": "Tracker Column 1",
  344. "default": "HP",
  345. "_type": "_FORMULA"
  346. },
  347. "trackerCol2": {
  348. "name": "Tracker Column 2",
  349. "default": "AC",
  350. "_type": "_FORMULA"
  351. },
  352. "trackerCol3": {
  353. "name": "Tracker Column 3",
  354. "default": "PP",
  355. "_type": "_FORMULA"
  356. },
  357. "minifyTracker": {
  358. "name": "Shrink Initiative Tracker Text",
  359. "default": false,
  360. "_type": "boolean"
  361. },
  362. "showDifficulty": {
  363. "name": "Show Difficulty in Tracker",
  364. "default": true,
  365. "_type": "boolean"
  366. },
  367. "emoji": {
  368. "name": "Add Emoji Replacement to Chat",
  369. "default": true,
  370. "_type": "boolean"
  371. },
  372. "showCustomArtPreview": {
  373. "name": "Show Custom Art Previews",
  374. "default": true,
  375. "_type": "boolean"
  376. }
  377. });
  378.  
  379. d20plus.sheet = "ogl";
  380. d20plus.remaining = 0;
  381. d20plus.initiative = {};
  382. d20plus.importer = {};
  383. d20plus.monsters = {};
  384. d20plus.spells = {};
  385. d20plus.psionics = {};
  386. d20plus.items = {};
  387. d20plus.feats = {};
  388. d20plus.races = {};
  389. d20plus.objects = {};
  390. d20plus.classes = {};
  391. d20plus.subclasses = {};
  392. d20plus.backgrounds = {};
  393. d20plus.adventures = {};
  394.  
  395. d20plus.advantageModes = ["Toggle (Default Advantage)", "Toggle", "Toggle (Default Disadvantage)", "Always", "Query", "Never"];
  396. d20plus.whisperModes = ["Toggle (Default GM)", "Toggle (Default Public)", "Always", "Query", "Never"];
  397. d20plus.damageModes = ["Auto Roll", "Don't Auto Roll"];
  398.  
  399. d20plus.formulas = {
  400. _options: ["--Empty--", "AC", "HP", "PP"],
  401. "ogl": {
  402. "cr": "@{npc_challenge}",
  403. "ac": "@{ac}",
  404. "npcac": "@{npc_ac}",
  405. "hp": "@{hp}",
  406. "pp": "@{passive_wisdom}",
  407. "macro": ""
  408. },
  409. "community": {
  410. "cr": "@{npc_challenge}",
  411. "ac": "@{AC}",
  412. "npcac": "@{AC}",
  413. "hp": "@{HP}",
  414. "pp": "10 + @{perception}",
  415. "macro": ""
  416. },
  417. "shaped": {
  418. "cr": "@{challenge}",
  419. "ac": "@{AC}",
  420. "npcac": "@{AC}",
  421. "hp": "@{HP}",
  422. "pp": "@{repeating_skill_$11_passive}",
  423. "macro": "shaped_statblock"
  424. }
  425. };
  426.  
  427. d20plus.scripts.push({name: "5etoolsutils", url: `${JS_URL}utils.js`});
  428. d20plus.scripts.push({name: "5etoolsrender", url: `${JS_URL}entryrender.js`});
  429.  
  430. d20plus.json = [
  431. {name: "spell index", url: `${SPELL_DATA_DIR}index.json`},
  432. {name: "spell metadata", url: SPELL_META_URL},
  433. {name: "bestiary index", url: `${MONSTER_DATA_DIR}index.json`},
  434. {name: "bestiary fluff index", url: `${MONSTER_DATA_DIR}fluff-index.json`},
  435. {name: "adventures index", url: `${DATA_URL}adventures.json`}
  436. ];
  437.  
  438. // add JSON index/metadata
  439. d20plus.addJson = function (onLoadFunction) {
  440. d20plus.log("Load JSON");
  441. const onEachLoadFunction = function (name, url, data) {
  442. if (name === "spell index") spellDataUrls = data;
  443. else if (name === "spell metadata") spellMetaData = data;
  444. else if (name === "bestiary index") monsterDataUrls = data;
  445. else if (name === "bestiary fluff index") monsterFluffDataUrls = data;
  446. else if (name === "adventures index") adventureMetadata = data;
  447. else throw new Error(`Unhandled data from JSON ${name} (${url})`);
  448.  
  449. d20plus.log(`JSON [${name}] Loaded`);
  450. };
  451. d20plus.chainLoad(d20plus.json, 0, onEachLoadFunction, onLoadFunction);
  452. };
  453.  
  454. d20plus.handleConfigChange = function () {
  455. if (window.is_gm) {
  456. d20plus.log("Applying config");
  457. d20plus.setInitiativeShrink(d20plus.getCfgVal("interface", "minifyTracker"));
  458. d20.Campaign.initiativewindow.rebuildInitiativeList();
  459. d20plus.updateDifficulty();
  460. if (d20plus.art.refreshList) d20plus.art.refreshList();
  461. }
  462. };
  463.  
  464. // get the user config'd token HP bar
  465. d20plus.getCfgHpBarNumber = function () {
  466. const bars = [
  467. d20plus.getCfgVal("token", "bar1"),
  468. d20plus.getCfgVal("token", "bar2"),
  469. d20plus.getCfgVal("token", "bar3")
  470. ];
  471. return bars[0] === "npc_hpbase" ? 1 : bars[1] === "npc_hpbase" ? 2 : bars[2] === "npc_hpbase" ? 3 : null;
  472. };
  473.  
  474. d20plus.importer._playerImports = {};
  475. d20plus.importer.storePlayerImport = function (id, data) {
  476. d20plus.importer._playerImports[id] = data;
  477. };
  478.  
  479. d20plus.importer.retreivePlayerImport = function (id) {
  480. return d20plus.importer._playerImports[id];
  481. };
  482.  
  483. d20plus.importer.clearPlayerImport = function () {
  484. d20plus.importer._playerImports = {};
  485. };
  486.  
  487. d20plus.importer.addMeta = function (meta) {
  488. if (!meta) return;
  489. BrewUtil._sourceCache = BrewUtil._sourceCache || {};
  490. if (meta.sources) {
  491. meta.sources.forEach(src => {
  492. BrewUtil._sourceCache[src.json] = {abbreviation: src.abbreviation, full: src.full};
  493. })
  494. }
  495. };
  496.  
  497. // Page fully loaded and visible
  498. d20plus.Init = function () {
  499. d20plus.log("Init (v" + d20plus.version + ")");
  500. d20plus.settingsHtmlHeader = `<hr><h3>betteR20-5etools v${d20plus.version}</h3>`;
  501. d20plus.addAllCss();
  502. if (window.is_gm) {
  503. d20plus.log("Is GM");
  504. d20plus.enhancePageSelector();
  505. }
  506. else d20plus.log("Not GM. Some functionality will be unavailable.");
  507. d20plus.setSheet();
  508. d20plus.addJson(d20plus.onJsonLoad);
  509. };
  510.  
  511. // continue init once JSON loads
  512. d20plus.onJsonLoad = function () {
  513. d20plus.addScripts(d20plus.onScriptLoad);
  514. };
  515.  
  516. // continue init once scripts load
  517. d20plus.onScriptLoad = function () {
  518. IS_ROLL20 = true; // global variable from 5etools' utils.js
  519. BrewUtil._buildSourceCache = function () {
  520. // no-op when building source cache; we'll handle this elsewhere
  521. BrewUtil._sourceCache = BrewUtil._sourceCache || {};
  522. };
  523. EntryRenderer.getDefaultRenderer().setBaseUrl(BASE_SITE_URL);
  524. if (window.is_gm) d20plus.loadConfig(d20plus.onConfigLoad);
  525. else d20plus.onConfigLoad();
  526. };
  527.  
  528. // continue more init after config loaded
  529. d20plus.onConfigLoad = function () {
  530. if (window.is_gm) d20plus.loadArt(d20plus.onArtLoad);
  531. else d20plus.onArtLoad();
  532. };
  533.  
  534. // continue more init after art loaded
  535. d20plus.onArtLoad = function () {
  536. d20plus.bindDropLocations();
  537. d20plus.addHtmlHeader();
  538. d20plus.addCustomHTML();
  539. d20plus.addHtmlFooter();
  540. d20plus.enhanceMarkdown();
  541. d20plus.addProFeatures();
  542. d20plus.initArtFromUrlButtons();
  543. if (window.is_gm) {
  544. d20plus.addJournalCommands();
  545. d20plus.addSelectedTokenCommands();
  546. d20.Campaign.pages.each(d20plus.bindGraphics);
  547. d20.Campaign.activePage().collection.on("add", d20plus.bindGraphics);
  548. d20plus.addCustomArtSearch();
  549. d20plus.handleConfigChange();
  550. d20plus.addTokenHover();
  551. } else {
  552. d20plus.startPlayerConfigHandler();
  553. }
  554. d20plus.enhanceStatusEffects();
  555. d20plus.enhanceMeasureTool();
  556. d20plus.enhanceSnap();
  557. d20plus.enhanceChat();
  558. d20plus.log("All systems operational");
  559. d20plus.chatTag(`betteR20-5etools v${d20plus.version}`);
  560. };
  561.  
  562. // Bind Graphics Add on page
  563. d20plus.bindGraphics = function (page) {
  564. d20plus.log("Bind Graphics");
  565. try {
  566. if (page.get("archived") === false) {
  567. page.thegraphics.on("add", function (e) {
  568. var character = e.character;
  569. if (character) {
  570. var npc = character.attribs.find(function (a) {
  571. return a.get("name").toLowerCase() == "npc";
  572. });
  573. var isNPC = npc ? parseInt(npc.get("current")) : 0;
  574. // Set bars if configured to do so
  575. var barsList = ["bar1", "bar2", "bar3"];
  576. $.each(barsList, (i, barName) => {
  577. // PC config keys are suffixed "_pc"
  578. const confVal = d20plus.getCfgVal("token", `${barName}${isNPC ? "" : "_pc"}`);
  579. if (confVal) {
  580. const charAttr = character.attribs.find(a => a.get("name").toLowerCase() == confVal);
  581. if (charAttr) {
  582. e.attributes[barName + "_value"] = charAttr.get("current");
  583. if (d20plus.hasCfgVal("token", barName + "_max")) {
  584. if (d20plus.getCfgVal("token", barName + "_max") && !isNPC && confVal === "hp") { // player HP is current; need to set max to max
  585. e.attributes[barName + "_max"] = charAttr.get("max");
  586. } else {
  587. if (isNPC) {
  588. // TODO: Setting a value to empty/null does not overwrite existing values on the token.
  589. // setting a specific value does. Must figure this out.
  590. e.attributes[barName + "_max"] = d20plus.getCfgVal("token", barName + "_max") ? charAttr.get("current") : "";
  591. } else {
  592. // preserve default token for player tokens
  593. if (d20plus.getCfgVal("token", barName + "_max")) {
  594. e.attributes[barName + "_max"] = charAttr.get("current");
  595. }
  596. }
  597. }
  598. }
  599. if (d20plus.hasCfgVal("token", barName + "_reveal")) {
  600. e.attributes["showplayers_" + barName] = d20plus.getCfgVal("token", barName + "_reveal");
  601. }
  602. }
  603. }
  604. });
  605.  
  606. // NPC-only settings
  607. if (isNPC) {
  608. // Set Nametag
  609. if (d20plus.hasCfgVal("token", "name")) {
  610. e.attributes["showname"] = d20plus.getCfgVal("token", "name");
  611. if (d20plus.hasCfgVal("token", "name_reveal")) {
  612. e.attributes["showplayers_name"] = d20plus.getCfgVal("token", "name_reveal");
  613. }
  614. }
  615.  
  616. // Roll HP
  617. // TODO: npc_hpbase appears to be hardcoded here? Refactor for NPC_SHEET_ATTRIBUTES?
  618. if ((d20plus.getCfgVal("token", "rollHP") || d20plus.getCfgVal("token", "maximiseHp")) && d20plus.getCfgKey("token", "npc_hpbase")) {
  619. var hpf = character.attribs.find(function (a) {
  620. return a.get("name").toLowerCase() == NPC_SHEET_ATTRIBUTES["npc_hpformula"][d20plus.sheet];
  621. });
  622. var barName = d20plus.getCfgKey("token", "npc_hpbase");
  623. var hpformula = hpf.get("current");
  624.  
  625. if (hpformula && hpf) {
  626. if (d20plus.getCfgVal("token", "maximiseHp")) {
  627. const maxSum = hpformula.replace("d", "*");
  628. try {
  629. const max = eval(maxSum);
  630. if (!isNaN(max)) {
  631. e.attributes[barName + "_value"] = max;
  632. e.attributes[barName + "_max"] = max;
  633. }
  634. } catch (error) {
  635. d20plus.log("Error Maximising HP");
  636. console.log(error);
  637. }
  638. } else {
  639. d20plus.randomRoll(hpformula, function (result) {
  640. e.attributes[barName + "_value"] = result.total;
  641. e.attributes[barName + "_max"] = result.total;
  642. d20plus.log("Rolled HP for [" + character.get("name") + "]");
  643. }, function (error) {
  644. d20plus.log("Error Rolling HP Dice");
  645. console.log(error);
  646. });
  647. }
  648. }
  649. }
  650. }
  651. }
  652. });
  653. }
  654. } catch (e) {
  655. console.log("D20Plus bindGraphics Exception", e);
  656. console.log("PAGE", page);
  657. }
  658. };
  659.  
  660. // bind token HP to initiative tracker window HP field
  661. d20plus.bindToken = function (token) {
  662. function getInitTrackerToken () {
  663. const $window = $("#initiativewindow");
  664. if (!$window.length) return [];
  665. return $window.find(`li.token`).filter((i, e) => {
  666. return $(e).data("tokenid") === token.id;
  667. });
  668. }
  669.  
  670. const $initToken = getInitTrackerToken();
  671. if (!$initToken.length) return;
  672. const $iptHp = $initToken.find(`.hp.editable`);
  673. const npcFlag = token.character ? token.character.attribs.find((a) => {
  674. return a.get("name").toLowerCase() === "npc";
  675. }) : null;
  676. // if there's a HP column enabled
  677. if ($iptHp.length) {
  678. let toBind;
  679. if (!token.character || npcFlag && npcFlag.get("current") == "1") {
  680. const hpBar = d20plus.getCfgHpBarNumber();
  681. // and a HP bar chosen
  682. if (hpBar) {
  683. $iptHp.text(token.attributes[`bar${hpBar}_value`])
  684. }
  685.  
  686. toBind = (token, changes) => {
  687. const $initToken = getInitTrackerToken();
  688. if (!$initToken.length) return;
  689. const $iptHp = $initToken.find(`.hp.editable`);
  690. const hpBar = d20plus.getCfgHpBarNumber();
  691.  
  692. if ($iptHp && hpBar) {
  693. if (changes.changes[`bar${hpBar}_value`]) {
  694. $iptHp.text(token.changed[`bar${hpBar}_value`]);
  695. }
  696. }
  697. };
  698. } else {
  699. toBind = (token, changes) => {
  700. const $initToken = getInitTrackerToken();
  701. if (!$initToken.length) return;
  702. const $iptHp = $initToken.find(`.hp.editable`);
  703. if ($iptHp) {
  704. $iptHp.text(token.character.autoCalcFormula(d20plus.formulas[d20plus.sheet].hp));
  705. }
  706. }
  707. }
  708. // clean up old handler
  709. if (d20plus.tokenBindings[token.id]) token.off("change", d20plus.tokenBindings[token.id]);
  710. // add new handler
  711. d20plus.tokenBindings[token.id] = toBind;
  712. token.on("change", toBind);
  713. }
  714. };
  715. d20plus.tokenBindings = {};
  716.  
  717. // Determine difficulty of current encounter (iniativewindow)
  718. d20plus.getDifficulty = function () {
  719. var difficulty = "Unknown";
  720. var partyXPThreshold = [0, 0, 0, 0];
  721. var players = [];
  722. var npcs = [];
  723. try {
  724. $.each(d20.Campaign.initiativewindow.cleanList(), function (i, v) {
  725. var page = d20.Campaign.pages.get(v._pageid);
  726. if (page) {
  727. var token = page.thegraphics.get(v.id);
  728. if (token) {
  729. var char = token.character;
  730. if (char) {
  731. var npc = char.attribs.find(function (a) {
  732. return a.get("name").toLowerCase() === "npc";
  733. });
  734. if (npc && npc.get("current") === "1") {
  735. npcs.push(char);
  736. } else {
  737. var level = char.attribs.find(function (a) {
  738. return a.get("name").toLowerCase() === "level";
  739. });
  740. // Can't determine difficulty without level
  741. if (!level || partyXPThreshold === null) {
  742. partyXPThreshold = null;
  743. return;
  744. }
  745. // Total party threshold
  746. for (i = 0; i < partyXPThreshold.length; i++) partyXPThreshold[i] += Parser.levelToXpThreshold(level.get("current"))[i];
  747. players.push(players.length + 1);
  748. }
  749. }
  750. }
  751. }
  752. });
  753. if (!players.length) return difficulty;
  754. // If a player doesn't have level set, fail out.
  755. if (partyXPThreshold !== null) {
  756. var len = npcs.length;
  757. var multiplier = 0;
  758. var adjustedxp = 0;
  759. var xp = 0;
  760. var index = 0;
  761. // Adjust for number of monsters
  762. if (len < 2) index = 0;
  763. else if (len < 3) index = 1;
  764. else if (len < 7) index = 2;
  765. else if (len < 11) index = 3;
  766. else if (len < 15) index = 4;
  767. else
  768. index = 5;
  769. // Adjust for smaller parties
  770. if (players.length < 3) index++;
  771. // Set multiplier
  772. multiplier = d20plus.multipliers[index];
  773. // Total monster xp
  774. $.each(npcs, function (i, v) {
  775. var cr = v.attribs.find(function (a) {
  776. return a.get("name").toLowerCase() === "npc_challenge";
  777. });
  778. if (cr) xp += parseInt(Parser.crToXp(cr.get("current")));
  779. });
  780. // Encounter's adjusted xp
  781. adjustedxp = xp * multiplier;
  782. console.log("Party XP Threshold", partyXPThreshold);
  783. console.log("Adjusted XP", adjustedxp);
  784. // Determine difficulty
  785. if (adjustedxp < partyXPThreshold[0]) difficulty = "Trivial";
  786. else if (adjustedxp < partyXPThreshold[1]) difficulty = "Easy";
  787. else if (adjustedxp < partyXPThreshold[2]) difficulty = "Medium";
  788. else if (adjustedxp < partyXPThreshold[3]) difficulty = "Hard";
  789. else
  790. difficulty = "Deadly";
  791. }
  792. } catch (e) {
  793. console.log("D20Plus getDifficulty Exception", e);
  794. }
  795. return difficulty;
  796. };
  797.  
  798. /**
  799. * Takes a path made up of strings and arrays of strings, and turns it into one flat array of strings
  800. */
  801. d20plus.importer.getCleanPath = function (...path) {
  802. const clean = [];
  803. getStrings(clean, path);
  804. return clean.map(s => s.trim()).filter(s => s);
  805.  
  806. function getStrings (stack, toProc) {
  807. toProc.forEach(tp => {
  808. if (typeof tp === "string") {
  809. stack.push(tp);
  810. } else if (tp instanceof Array) {
  811. getStrings(stack, tp);
  812. } else {
  813. throw new Error("Object in path was not a string or an array")
  814. }
  815. });
  816. }
  817. };
  818.  
  819. d20plus.importer.makeDirTree = function (...path) {
  820. const parts = d20plus.importer.getCleanPath(path);
  821. // path e.g. d20plus.importer.makeDirTree("Spells", "Cantrips", "1")
  822. // roll20 allows a max directory depth of 4 :joy: (5, but the 5th level is unusable)
  823. if (parts.length > 4) throw new Error("Max directory depth exceeded! The maximum is 4.")
  824.  
  825. const madeSoFar = [];
  826.  
  827. const root = {i: d20plus.getJournalFolderObj()};
  828.  
  829. // roll20 folder management is dumb, so just pick the first folder with the right name if there's multiple
  830. let curDir = root;
  831. parts.forEach(toMake => {
  832. const existing = curDir.i.find((it) => {
  833. // n is folder name (only folders have the n property)
  834. return it.n && it.n === toMake && it.i;
  835. });
  836. if (!existing) {
  837. if (curDir.id) {
  838. d20.journal.addFolderToFolderStructure(toMake, curDir.id);
  839. } else {
  840. // root has no id
  841. d20.journal.addFolderToFolderStructure(toMake);
  842. }
  843. }
  844. d20.journal.refreshJournalList();
  845. madeSoFar.push(toMake);
  846.  
  847. // we have to save -> reread the entire directory JSON -> walk back to where we were
  848. let nextDir = {i: JSON.parse(d20.Campaign.get("journalfolder"))};
  849. madeSoFar.forEach(f => {
  850. nextDir = nextDir.i.find(dir => dir.n && (dir.n.toLowerCase() === f.toLowerCase()));
  851. });
  852.  
  853. curDir = nextDir;
  854. });
  855. return curDir;
  856. };
  857.  
  858. d20plus.importer.recursiveRemoveDirById = function (folderId, withConfirmation) {
  859. if (!withConfirmation || confirm("Are you sure you want to delete this folder, and everything in it? This cannot be undone.")) {
  860. const folder = $(`[data-globalfolderid='${folderId}']`);
  861. if (folder.length) {
  862. d20plus.log("Nuking folder...");
  863. const childItems = folder.find("[data-itemid]").each((i, e) => {
  864. const $e = $(e);
  865. const itemId = $e.attr("data-itemid");
  866. let toDel = d20.Campaign.handouts.get(itemId);
  867. toDel || (toDel = d20.Campaign.characters.get(itemId));
  868. if (toDel) toDel.destroy();
  869. });
  870. const childFolders = folder.find(`[data-globalfolderid]`).remove();
  871. folder.remove();
  872. $("#journalfolderroot").trigger("change");
  873. }
  874. }
  875. };
  876.  
  877. d20plus.importer.removeDirByPath = function (...path) {
  878. path = d20plus.importer.getCleanPath(path);
  879. return d20plus.importer._checkOrRemoveDirByPath(true, path);
  880. };
  881.  
  882. d20plus.importer.checkDirExistsByPath = function (...path) {
  883. path = d20plus.importer.getCleanPath(path);
  884. return d20plus.importer._checkOrRemoveDirByPath(false, path);
  885. };
  886.  
  887. d20plus.importer._checkOrRemoveDirByPath = function (doDelete, path) {
  888. const parts = d20plus.importer.getCleanPath(path);
  889.  
  890. const root = {i: d20plus.getJournalFolderObj()};
  891.  
  892. let curDir = root;
  893. for (let i = 0; i < parts.length; ++i) {
  894. const p = parts[i];
  895. let lastId;
  896. const existing = curDir.i.find((it) => {
  897. lastId = it.id;
  898. // n is folder name (only folders have the n property)
  899. return it.n && it.n === p;
  900. });
  901. if (!existing) return false;
  902. curDir = existing;
  903. if (i === parts.length - 1) {
  904. d20plus.importer.recursiveRemoveDirById(lastId, false);
  905. return true;
  906. }
  907. }
  908. };
  909.  
  910. d20plus.importer.removeFileByPath = function (...path) {
  911. path = d20plus.importer.getCleanPath(path);
  912. return d20plus.importer._checkOrRemoveFileByPath(true, path);
  913. };
  914.  
  915. d20plus.importer.checkFileExistsByPath = function (...path) {
  916. path = d20plus.importer.getCleanPath(path);
  917. return d20plus.importer._checkOrRemoveFileByPath(false, path);
  918. };
  919.  
  920. d20plus.importer._checkOrRemoveFileByPath = function (doDelete, path) {
  921. const parts = d20plus.importer.getCleanPath(path);
  922.  
  923. const root = {i: d20plus.getJournalFolderObj()};
  924.  
  925. let curDir = root;
  926. for (let i = 0; i < parts.length; ++i) {
  927. const p = parts[i];
  928. let lastId;
  929. const existing = curDir.i.find((it) => {
  930. if (i === parts.length - 1) {
  931. // for the last item, check handouts/characters to see if the match it (which could be a string ID)
  932. const char = d20.Campaign.characters.get(it);
  933. const handout = d20.Campaign.handouts.get(it);
  934. if ((char && char.get("name") === p) || (handout && handout.get("name") === p)) {
  935. lastId = it;
  936. return true;
  937. }
  938. } else {
  939. lastId = it.id;
  940. // n is folder name (only folders have the n property)
  941. return it.n && it.n === p;
  942. }
  943. return false;
  944. });
  945. if (!existing) return false;
  946. curDir = existing;
  947. if (i === parts.length - 1) {
  948. if (doDelete) {
  949. // on the last item, delete
  950. let toDel = d20.Campaign.handouts.get(lastId);
  951. toDel || (toDel = d20.Campaign.characters.get(lastId))
  952. if (toDel) toDel.destroy();
  953. }
  954. return true;
  955. }
  956. }
  957. return false;
  958. };
  959.  
  960. d20plus.formSrcUrl = function (dataDir, fileName) {
  961. return dataDir + fileName;
  962. };
  963.  
  964. d20plus.addCustomHTML = function () {
  965. function populateDropdown (dropdownId, inputFieldId, baseUrl, srcUrlObject, defaultSel, homebrewDir) {
  966. const defaultUrl = d20plus.formSrcUrl(baseUrl, srcUrlObject[defaultSel]);
  967. $(inputFieldId).val(defaultUrl);
  968. const dropdown = $(dropdownId);
  969. $.each(Object.keys(srcUrlObject), function (i, src) {
  970. dropdown.append($('<option>', {
  971. value: d20plus.formSrcUrl(baseUrl, srcUrlObject[src]),
  972. text: Parser.sourceJsonToFullCompactPrefix(src)
  973. }));
  974. });
  975. dropdown.append($('<option>', {
  976. value: "",
  977. text: "Custom"
  978. }));
  979.  
  980. const brewUrl = `${HOMEBREW_REPO_URL}contents/${homebrewDir}${d20plus.getAntiCacheSuffix()}&client_id=${HOMEBREW_CLIENT_ID}&client_secret=${HOMEBREW_CLIENT_SECRET}`;
  981. DataUtil.loadJSON(brewUrl, (data, debugUrl) => {
  982. if (data.message) console.error(debugUrl, data.message);
  983. data.forEach(it => {
  984. dropdown.append($('<option>', {
  985. value: `${it.download_url}${d20plus.getAntiCacheSuffix()}`,
  986. text: `Homebrew: ${it.name.trim().replace(/\.json$/i, "")}`
  987. }));
  988. });
  989. }, brewUrl);
  990.  
  991. dropdown.val(defaultUrl);
  992. dropdown.change(function () {
  993. $(inputFieldId).val(this.value);
  994. });
  995. }
  996.  
  997. function populateBasicDropdown (dropdownId, inputFieldId, defaultSel, homebrewDir, addForPlayers) {
  998. function doPopulate (dropdownId, inputFieldId) {
  999. const $sel = $(dropdownId);
  1000. if (defaultSel) {
  1001. $(inputFieldId).val(defaultSel);
  1002. $sel.append($('<option>', {
  1003. value: defaultSel,
  1004. text: "Official Sources"
  1005. }));
  1006. }
  1007. $sel.append($('<option>', {
  1008. value: "",
  1009. text: "Custom"
  1010. }));
  1011.  
  1012. const brewUrl = `${HOMEBREW_REPO_URL}contents/${homebrewDir}${d20plus.getAntiCacheSuffix()}&client_id=${HOMEBREW_CLIENT_ID}&client_secret=${HOMEBREW_CLIENT_SECRET}`;
  1013. DataUtil.loadJSON(brewUrl, (data, debugUrl) => {
  1014. if (data.message) console.error(debugUrl, data.message);
  1015. data.forEach(it => {
  1016. $sel.append($('<option>', {
  1017. value: `${it.download_url}${d20plus.getAntiCacheSuffix()}`,
  1018. text: `Homebrew: ${it.name.trim().replace(/\.json$/i, "")}`
  1019. }));
  1020. });
  1021. }, brewUrl);
  1022.  
  1023. $sel.val(defaultSel);
  1024. $sel.change(function () {
  1025. $(inputFieldId).val(this.value);
  1026. });
  1027. }
  1028.  
  1029. doPopulate(dropdownId, inputFieldId, defaultSel, homebrewDir);
  1030. if (addForPlayers) doPopulate(`${dropdownId}-player`, `${inputFieldId}-player`, defaultSel, homebrewDir);
  1031. }
  1032.  
  1033. const $body = $("body");
  1034. if (window.is_gm) {
  1035. const $wrpSettings = $(`#betteR20-settings`);
  1036.  
  1037. $wrpSettings.append(d20plus.settingsHtmlImportHeader);
  1038. $wrpSettings.append(d20plus.settingsHtmlSelector);
  1039. $wrpSettings.append(d20plus.settingsHtmlPtMonsters);
  1040. $wrpSettings.append(d20plus.settingsHtmlPtItems);
  1041. $wrpSettings.append(d20plus.settingsHtmlPtSpells);
  1042. $wrpSettings.append(d20plus.settingsHtmlPtPsionics);
  1043. $wrpSettings.append(d20plus.settingsHtmlPtRaces);
  1044. $wrpSettings.append(d20plus.settingsHtmlPtFeats);
  1045. $wrpSettings.append(d20plus.settingsHtmlPtObjects);
  1046. $wrpSettings.append(d20plus.settingsHtmlPtClasses);
  1047. $wrpSettings.append(d20plus.settingsHtmlPtSubclasses);
  1048. $wrpSettings.append(d20plus.settingsHtmlPtBackgrounds);
  1049. $wrpSettings.append(d20plus.settingsHtmlPtAdventures);
  1050. $wrpSettings.append(d20plus.settingsHtmlPtImportFooter);
  1051.  
  1052. $("#mysettings > .content a#button-monsters-load").on(window.mousedowntype, d20plus.monsters.button);
  1053. $("#mysettings > .content a#button-monsters-load-all").on(window.mousedowntype, d20plus.monsters.buttonAll);
  1054. $("#mysettings > .content a#import-objects-load").on(window.mousedowntype, d20plus.objects.button);
  1055. $("#mysettings > .content a#button-adventures-load").on(window.mousedowntype, d20plus.adventures.button);
  1056.  
  1057. $("#mysettings > .content a#bind-drop-locations").on(window.mousedowntype, d20plus.bindDropLocations);
  1058. $("#initiativewindow .characterlist").before(d20plus.initiativeHeaders);
  1059.  
  1060. d20plus.setTurnOrderTemplate();
  1061. d20.Campaign.initiativewindow.rebuildInitiativeList();
  1062. d20plus.hpAllowEdit();
  1063. d20.Campaign.initiativewindow.model.on("change:turnorder", function () {
  1064. d20plus.updateDifficulty();
  1065. });
  1066. d20plus.updateDifficulty();
  1067.  
  1068. populateDropdown("#button-monsters-select", "#import-monster-url", MONSTER_DATA_DIR, monsterDataUrls, "MM", "creature");
  1069. populateBasicDropdown("#button-objects-select", "#import-objects-url", OBJECT_DATA_URL, "object");
  1070.  
  1071. populateAdventuresDropdown();
  1072.  
  1073. function populateAdventuresDropdown () {
  1074. const defaultAdvUrl = d20plus.formSrcUrl(ADVENTURE_DATA_DIR, "adventure-lmop.json");
  1075. const $iptUrl = $("#import-adventures-url");
  1076. $iptUrl.val(defaultAdvUrl);
  1077. $iptUrl.data("id", "lmop");
  1078. const $sel = $("#button-adventures-select");
  1079. adventureMetadata.adventure.forEach(a => {
  1080. $sel.append($('<option>', {
  1081. value: d20plus.formSrcUrl(ADVENTURE_DATA_DIR, `adventure-${a.id.toLowerCase()}.json|${a.id}`),
  1082. text: a.name
  1083. }));
  1084. });
  1085. $sel.append($('<option>', {
  1086. value: "",
  1087. text: "Custom"
  1088. }));
  1089. $sel.val(defaultAdvUrl);
  1090. $sel.change(() => {
  1091. const [url, id] = $sel.val().split("|");
  1092. $($iptUrl).val(url);
  1093. $iptUrl.data("id", id);
  1094. });
  1095. }
  1096.  
  1097. // import
  1098. $("a#button-spells-load").on(window.mousedowntype, () => d20plus.spells.button());
  1099. $("a#button-spells-load-all").on(window.mousedowntype, () => d20plus.spells.buttonAll());
  1100. $("a#import-psionics-load").on(window.mousedowntype, () => d20plus.psionics.button());
  1101. $("a#import-items-load").on(window.mousedowntype, () => d20plus.items.button());
  1102. $("a#import-races-load").on(window.mousedowntype, () => d20plus.races.button());
  1103. $("a#import-feats-load").on(window.mousedowntype, () => d20plus.feats.button());
  1104. $("a#import-classes-load").on(window.mousedowntype, () => d20plus.classes.button());
  1105. $("a#import-subclasses-load").on(window.mousedowntype, () => d20plus.subclasses.button());
  1106. $("a#import-backgrounds-load").on(window.mousedowntype, () => d20plus.backgrounds.button());
  1107. $("select#import-mode-select").on("change", () => d20plus.importer.importModeSwitch());
  1108. } else {
  1109. // player-only HTML if required
  1110. }
  1111.  
  1112. $body.append(d20plus.playerImportHtml);
  1113. const $winPlayer = $("#d20plus-playerimport");
  1114. const $appTo = $winPlayer.find(`.append-target`);
  1115. $appTo.append(d20plus.settingsHtmlSelectorPlayer);
  1116. $appTo.append(d20plus.settingsHtmlPtItemsPlayer);
  1117. $appTo.append(d20plus.settingsHtmlPtSpellsPlayer);
  1118. $appTo.append(d20plus.settingsHtmlPtPsionicsPlayer);
  1119. $appTo.append(d20plus.settingsHtmlPtRacesPlayer);
  1120. $appTo.append(d20plus.settingsHtmlPtFeatsPlayer);
  1121. $appTo.append(d20plus.settingsHtmlPtClassesPlayer);
  1122. $appTo.append(d20plus.settingsHtmlPtSubclassesPlayer);
  1123. $appTo.append(d20plus.settingsHtmlPtBackgroundsPlayer);
  1124.  
  1125. $winPlayer.dialog({
  1126. autoOpen: false,
  1127. resizable: true,
  1128. width: 800,
  1129. height: 650,
  1130. });
  1131.  
  1132. const $wrpPlayerImport = $(`
  1133. <div style="padding: 0 10px">
  1134. <div style="clear: both"></div>
  1135. </div>`);
  1136. const $btnPlayerImport = $(`<button class="btn" href="#" title="A tool to import temporary copies of various things, which can be drag-and-dropped to character sheets." style="margin-top: 5px">Temp Import Spells, Items, Classes,...</button>`)
  1137. .on("click", () => {
  1138. $winPlayer.dialog("open");
  1139. });
  1140. $wrpPlayerImport.prepend($btnPlayerImport);
  1141. $(`#journal`).prepend($wrpPlayerImport);
  1142.  
  1143. // SHARED WINDOWS/BUTTONS
  1144. // import
  1145. $("a#button-spells-load-player").on(window.mousedowntype, () => d20plus.spells.button(true));
  1146. $("a#button-spells-load-all-player").on(window.mousedowntype, () => d20plus.spells.buttonAll(true));
  1147. $("a#import-psionics-load-player").on(window.mousedowntype, () => d20plus.psionics.button(true));
  1148. $("a#import-items-load-player").on(window.mousedowntype, () => d20plus.items.button(true));
  1149. $("a#import-races-load-player").on(window.mousedowntype, () => d20plus.races.button(true));
  1150. $("a#import-feats-load-player").on(window.mousedowntype, () => d20plus.feats.button(true));
  1151. $("a#import-classes-load-player").on(window.mousedowntype, () => d20plus.classes.button(true));
  1152. $("a#import-subclasses-load-player").on(window.mousedowntype, () => d20plus.subclasses.button(true));
  1153. $("a#import-backgrounds-load-player").on(window.mousedowntype, () => d20plus.backgrounds.button(true));
  1154. $("select#import-mode-select-player").on("change", () => d20plus.importer.importModeSwitch());
  1155.  
  1156. $body.append(d20plus.importDialogHtml);
  1157. $body.append(d20plus.importListHTML);
  1158. $body.append(d20plus.importListPropsHTML);
  1159. $("#d20plus-import").dialog({
  1160. autoOpen: false,
  1161. resizable: false
  1162. });
  1163. $("#d20plus-importlist").dialog({
  1164. autoOpen: false,
  1165. resizable: true,
  1166. width: 1000,
  1167. height: 700
  1168. });
  1169. $("#d20plus-import-props").dialog({
  1170. autoOpen: false,
  1171. resizable: true,
  1172. width: 300,
  1173. height: 600
  1174. });
  1175.  
  1176. populateDropdown("#button-spell-select", "#import-spell-url", SPELL_DATA_DIR, spellDataUrls, "PHB", "spell");
  1177. populateDropdown("#button-spell-select-player", "#import-spell-url-player", SPELL_DATA_DIR, spellDataUrls, "PHB", "spell");
  1178.  
  1179. populateBasicDropdown("#button-items-select", "#import-items-url", ITEM_DATA_URL, "item", true);
  1180. populateBasicDropdown("#button-psionics-select", "#import-psionics-url", PSIONIC_DATA_URL, "psionic", true);
  1181. populateBasicDropdown("#button-feats-select", "#import-feats-url", FEAT_DATA_URL, "feat", true);
  1182. populateBasicDropdown("#button-races-select", "#import-races-url", RACE_DATA_URL, "race", true);
  1183. populateBasicDropdown("#button-classes-select", "#import-classes-url", CLASS_DATA_URL, "class", true);
  1184. populateBasicDropdown("#button-subclasses-select", "#import-subclasses-url", "", "subclass", true);
  1185. populateBasicDropdown("#button-backgrounds-select", "#import-backgrounds-url", BACKGROUND_DATA_URL, "background", true);
  1186.  
  1187. // bind tokens button
  1188. const altBindButton = $(`<button id="bind-drop-locations-alt" class="btn bind-drop-locations" href="#" title="Bind drop locations and handouts">Bind Drag-n-Drop</button>`);
  1189. altBindButton.on("click", function () {
  1190. d20plus.bindDropLocations();
  1191. });
  1192.  
  1193. if (window.is_gm) {
  1194. const $addPoint = $(`#journal button.btn.superadd`);
  1195. altBindButton.css("margin-right", "5px");
  1196. $addPoint.after(altBindButton);
  1197. } else {
  1198. altBindButton.css("margin-top", "5px");
  1199. const $wrprControls = $(`#search-wrp-controls`);
  1200. $wrprControls.append(altBindButton);
  1201. }
  1202. $("#journal btn#bind-drop-locations").on(window.mousedowntype, d20plus.bindDropLocations);
  1203. };
  1204.  
  1205. d20plus.updateDifficulty = function () {
  1206. if (!$("div#initiativewindow").parent().is("body")) {
  1207. var $span = $("div#initiativewindow").parent().find(".ui-dialog-buttonpane > span.difficulty");
  1208. var $btnpane = $("div#initiativewindow").parent().find(".ui-dialog-buttonpane");
  1209. if (!$span.length) {
  1210. $btnpane.prepend(d20plus.difficultyHtml);
  1211. $span = $("div#initiativewindow").parent().find(".ui-dialog-buttonpane > span.difficulty");
  1212. }
  1213. if (d20plus.getCfgVal("interface", "showDifficulty")) {
  1214. $span.text("Difficulty: " + d20plus.getDifficulty());
  1215. $span.show();
  1216. } else {
  1217. $span.hide();
  1218. }
  1219. }
  1220. };
  1221.  
  1222. // bind tokens to the initiative tracker
  1223. d20plus.bindTokens = function () {
  1224. // Gets a list of all the tokens on the current page:
  1225. const curTokens = d20.Campaign.pages.get(d20.Campaign.activePage()).thegraphics.toArray();
  1226. curTokens.forEach(t => {
  1227. d20plus.bindToken(t);
  1228. });
  1229. };
  1230.  
  1231. // bind drop locations on sheet to accept custom handouts
  1232. d20plus.bindDropLocations = function () {
  1233. if (window.is_gm) {
  1234. // Bind Spells and Items, add compendium-item to each of them
  1235. var journalFolder = d20.Campaign.get("journalfolder");
  1236. if (journalFolder === "") {
  1237. d20.journal.addFolderToFolderStructure("Spells");
  1238. d20.journal.addFolderToFolderStructure("Psionics");
  1239. d20.journal.addFolderToFolderStructure("Items");
  1240. d20.journal.addFolderToFolderStructure("Feats");
  1241. d20.journal.addFolderToFolderStructure("Classes");
  1242. d20.journal.addFolderToFolderStructure("Subclasses");
  1243. d20.journal.addFolderToFolderStructure("Backgrounds");
  1244. d20.journal.addFolderToFolderStructure("Races");
  1245. d20.journal.refreshJournalList();
  1246. journalFolder = d20.Campaign.get("journalfolder");
  1247. }
  1248. }
  1249.  
  1250. function addClasses (folderName) {
  1251. $(`#journalfolderroot > ol.dd-list > li.dd-folder > div.dd-content:contains(${folderName})`).parent().find("ol li[data-itemid]").addClass("compendium-item").addClass("ui-draggable").addClass("Vetools-draggable");
  1252. }
  1253.  
  1254. addClasses("Spells");
  1255. addClasses("Psionics");
  1256. addClasses("Items");
  1257. addClasses("Feats");
  1258. addClasses("Classes");
  1259. addClasses("Subclasses");
  1260. addClasses("Backgrounds");
  1261. addClasses("Races");
  1262.  
  1263. // if player, force-enable dragging
  1264. if (!window.is_gm) {
  1265. $(`.Vetools-draggable`).draggable({
  1266. revert: true,
  1267. distance: 10,
  1268. revertDuration: 0,
  1269. helper: "clone",
  1270. handle: ".namecontainer",
  1271. appendTo: "body",
  1272. scroll: true,
  1273. start: function () {
  1274. $("#journalfolderroot").addClass("externaldrag")
  1275. },
  1276. stop: function () {
  1277. $("#journalfolderroot").removeClass("externaldrag")
  1278. }
  1279. });
  1280. }
  1281.  
  1282. d20.Campaign.characters.models.each(function (v, i) {
  1283. v.view.rebindCompendiumDropTargets = function () {
  1284. // ready character sheet for draggable
  1285. $(".sheet-compendium-drop-target").each(function () {
  1286. $(this).droppable({
  1287. hoverClass: "dropping",
  1288. tolerance: "pointer",
  1289. activeClass: "active-drop-target",
  1290. accept: ".compendium-item",
  1291. drop: function (t, i) {
  1292. var characterid = $(".characterdialog").has(t.target).attr("data-characterid");
  1293. var character = d20.Campaign.characters.get(characterid).view;
  1294. var inputData;
  1295. const $hlpr = $(i.helper[0]);
  1296.  
  1297. let doSetDirty = true;
  1298.  
  1299. if ($hlpr.hasClass("handout")) {
  1300. console.log("Handout item dropped onto target!");
  1301. t.originalEvent.dropHandled = !0;
  1302.  
  1303. if ($hlpr.hasClass(`player-imported`)) {
  1304. const data = d20plus.importer.retreivePlayerImport($hlpr.attr("data-playerimportid"));
  1305. handleData(data);
  1306. } else {
  1307. var id = $hlpr.attr("data-itemid");
  1308. var handout = d20.Campaign.handouts.get(id);
  1309. console.log(character);
  1310. var data = "";
  1311. if (window.is_gm) {
  1312. handout._getLatestBlob("gmnotes", function (gmnotes) {
  1313. data = gmnotes;
  1314. handout.updateBlobs({gmnotes: gmnotes});
  1315. handleData(JSON.parse(data));
  1316. });
  1317. } else {
  1318. handout._getLatestBlob("notes", function (notes) {
  1319. data = $(notes).filter("del").html();
  1320. handleData(JSON.parse(data));
  1321. });
  1322. }
  1323. }
  1324.  
  1325. function handleData (data) {
  1326. const extraDirty = [];
  1327.  
  1328. // TODO remove Feat workaround when roll20 supports feat drag-n-drop properly
  1329. if (data.data.Category === "Feats") {
  1330. const rowId = d20plus.generateRowId();
  1331. character.model.attribs.create({
  1332. "name": `repeating_traits_${rowId}_options-flag`,
  1333. "current": "0"
  1334. });
  1335.  
  1336. character.model.attribs.create({
  1337. "name": `repeating_traits_${rowId}_name`,
  1338. "current": data.name
  1339. });
  1340.  
  1341. character.model.attribs.create({
  1342. "name": `repeating_traits_${rowId}_description`,
  1343. "current": data.Vetoolscontent
  1344. });
  1345.  
  1346. character.model.attribs.create({
  1347. "name": `repeating_traits_${rowId}_source`,
  1348. "current": "Feat"
  1349. });
  1350.  
  1351. character.model.view._updateSheetValues();
  1352. const dirty = [];
  1353. $.each(d20.journal.customSheets.attrDeps, function (i, v) {
  1354. dirty.push(i);
  1355. });
  1356. d20.journal.notifyWorkersOfAttrChanges(character.model.view.model.id, dirty, true);
  1357. } else if (data.data.Category === "Backgrounds") { // TODO remove Background workaround when roll20 supports background drag-n-drop properly
  1358. const bg = data.Vetoolscontent;
  1359.  
  1360. const renderer = new EntryRenderer();
  1361. renderer.setBaseUrl(BASE_SITE_URL);
  1362. const renderStack = [];
  1363. let feature;
  1364. bg.entries.forEach(e => {
  1365. if (e.name && e.name.includes("Feature:")) {
  1366. feature = JSON.parse(JSON.stringify(e));
  1367. feature.name = feature.name.replace("Feature:", "").trim();
  1368. }
  1369. });
  1370. if (feature) renderer.recursiveEntryRender({entries: feature.entries}, renderStack);
  1371.  
  1372. d20plus.importer.addOrUpdateAttr(character.model, "background", bg.name);
  1373.  
  1374. const fRowId = d20plus.generateRowId();
  1375. character.model.attribs.create({
  1376. name: `repeating_traits_${fRowId}_name`,
  1377. current: bg.name
  1378. });
  1379. character.model.attribs.create({
  1380. name: `repeating_traits_${fRowId}_source`,
  1381. current: "Background"
  1382. });
  1383. character.model.attribs.create({
  1384. name: `repeating_traits_${fRowId}_source_type`,
  1385. current: bg.name
  1386. });
  1387. if (renderStack.length) {
  1388. character.model.attribs.create({
  1389. name: `repeating_traits_${fRowId}_description`,
  1390. current: d20plus.importer.getCleanText(renderStack.join(""))
  1391. });
  1392. }
  1393. character.model.attribs.create({
  1394. name: `repeating_traits_${fRowId}_options-flag`,
  1395. current: "0"
  1396. });
  1397.  
  1398. if (bg.skillProficiencies) {
  1399. const skills = bg.skillProficiencies.split(",").map(s => s.toLowerCase().trim().replace(/ /g, "_"));
  1400. skills.forEach(s => {
  1401. d20plus.importer.addOrUpdateAttr(character.model, `${s}_prof`, `(@{pb}*@{${s}_type})`);
  1402. });
  1403. }
  1404. } else if (data.data.Category === "Races") { // TODO remove Race workaround when roll20 supports background drag-n-drop properly
  1405. const race = data.Vetoolscontent;
  1406.  
  1407. d20plus.importer.addOrUpdateAttr(character.model, `race`, race.name);
  1408. d20plus.importer.addOrUpdateAttr(character.model, `speed`, Parser.getSpeedString(race));
  1409. race.entries.forEach(e => {
  1410. const renderer = new EntryRenderer();
  1411. renderer.setBaseUrl(BASE_SITE_URL);
  1412. const renderStack = [];
  1413. renderer.recursiveEntryRender({entries: e.entries}, renderStack);
  1414.  
  1415. const fRowId = d20plus.generateRowId();
  1416. character.model.attribs.create({
  1417. name: `repeating_traits_${fRowId}_name`,
  1418. current: e.name
  1419. });
  1420. character.model.attribs.create({
  1421. name: `repeating_traits_${fRowId}_source`,
  1422. current: "Race"
  1423. });
  1424. character.model.attribs.create({
  1425. name: `repeating_traits_${fRowId}_source_type`,
  1426. current: race.name
  1427. });
  1428. character.model.attribs.create({
  1429. name: `repeating_traits_${fRowId}_description`,
  1430. current: d20plus.importer.getCleanText(renderStack.join(""))
  1431. });
  1432. character.model.attribs.create({
  1433. name: `repeating_traits_${fRowId}_options-flag`,
  1434. current: "0"
  1435. });
  1436. });
  1437. } else if (data.data.Category === "Classes") {
  1438. let level = prompt("What level?", "1");
  1439. if (level && level.trim()) {
  1440. level = Number(level);
  1441. if (level) {
  1442. if (level < 0 || level > 20) {
  1443. alert("Please enter a number between one and 20!");
  1444. return;
  1445. }
  1446.  
  1447. const clss = data.Vetoolscontent;
  1448.  
  1449. // --- these don't work
  1450. // d20plus.importer.addOrUpdateAttr(character.model, "class", data.name);
  1451. // d20plus.importer.addOrUpdateAttr(character.model, "level", level);
  1452. // d20plus.importer.addOrUpdateAttr(character.model, "base_level", String(level));
  1453.  
  1454. // operation "kitchen sink"
  1455. setTimeout(() => {
  1456. d20plus.importer.addOrUpdateAttr(character.model, "pb", d20plus.getProfBonusFromLevel(Number(level)));
  1457. // try to set level -- none of these actually work lol
  1458. d20plus.importer.addOrUpdateAttr(character.model, "level", level);
  1459. d20plus.importer.addOrUpdateAttr(character.model, "base_level", String(level));
  1460. character.$charsheet.find(`.sheet-pc .sheet-core input[name=attr_base_level]`)
  1461. .val(String(level))
  1462. .text(String(level))
  1463. .trigger("change");
  1464. // hack to set class
  1465. character.$charsheet.find(`.sheet-pc .sheet-core select[name=attr_class]`).val(data.name).trigger("change");
  1466. character.model.persisted = false;
  1467. extraDirty.add("level", "base_level", "pb");
  1468. }, 500);
  1469.  
  1470. const renderer = new EntryRenderer();
  1471. renderer.setBaseUrl(BASE_SITE_URL);
  1472. for (let i = 0; i < level; i++) {
  1473. const lvlFeatureList = clss.classFeatures[i];
  1474. for (let j = 0; j < lvlFeatureList.length; j++) {
  1475. const feature = lvlFeatureList[j];
  1476. // don't add "you gain a subclass feature" or ASI's
  1477. if (!feature.gainSubclassFeature && feature.name !== "Ability Score Improvement") {
  1478. const renderStack = [];
  1479. renderer.recursiveEntryRender({entries: feature.entries}, renderStack);
  1480.  
  1481. const fRowId = d20plus.generateRowId();
  1482. character.model.attribs.create({
  1483. name: `repeating_traits_${fRowId}_name`,
  1484. current: feature.name
  1485. });
  1486. character.model.attribs.create({
  1487. name: `repeating_traits_${fRowId}_source`,
  1488. current: "Class"
  1489. });
  1490. character.model.attribs.create({
  1491. name: `repeating_traits_${fRowId}_source_type`,
  1492. current: `${clss.name} ${i + 1}`
  1493. });
  1494. character.model.attribs.create({
  1495. name: `repeating_traits_${fRowId}_description`,
  1496. current: d20plus.importer.getCleanText(renderStack.join(""))
  1497. });
  1498. character.model.attribs.create({
  1499. name: `repeating_traits_${fRowId}_options-flag`,
  1500. current: "0"
  1501. });
  1502. }
  1503. }
  1504. }
  1505. }
  1506. }
  1507. } else if (data.data.Category === "Subclasses") {
  1508. const sc = data.Vetoolscontent;
  1509. let maxIndex = sc.subclassFeatures.length;
  1510. const gainLevels = [];
  1511. // _gainAtLevels should be a 20-length array of booleans
  1512. if (sc._gainAtLevels) {
  1513. maxIndex = 0;
  1514.  
  1515. let level = prompt("What level?", "1");
  1516. if (level && level.trim()) {
  1517. level = Number(level);
  1518. if (level) {
  1519. if (level < 0 || level > 20) {
  1520. alert("Please enter a number between one and 20!");
  1521. return;
  1522. }
  1523.  
  1524. for (let i = 0; i < level; i++) {
  1525. if (sc._gainAtLevels[i]) {
  1526. maxIndex++;
  1527. gainLevels.push(i + 1);
  1528. }
  1529. }
  1530. }
  1531. } else {
  1532. return;
  1533. }
  1534. }
  1535.  
  1536. if (maxIndex === 0) return;
  1537.  
  1538. const renderer = new EntryRenderer();
  1539. renderer.setBaseUrl(BASE_SITE_URL);
  1540. let firstFeatures = true;
  1541. for (let i = 0; i < maxIndex; i++) {
  1542. const lvlFeatureList = sc.subclassFeatures[i];
  1543. for (let j = 0; j < lvlFeatureList.length; j++) {
  1544. const featureCpy = JSON.parse(JSON.stringify(lvlFeatureList[j]));
  1545. let feature = lvlFeatureList[j];
  1546.  
  1547. try {
  1548. while (!feature.name || (feature[0] && !feature[0].name)) {
  1549. if (feature.entries && feature.entries.name) {
  1550. feature = feature.entries;
  1551. continue;
  1552. } else if (feature.entries[0] && feature.entries[0].name) {
  1553. feature = feature.entries[0];
  1554. continue;
  1555. } else {
  1556. feature = feature.entries;
  1557. }
  1558.  
  1559. if (!feature) {
  1560. // in case something goes wrong, reset break the loop
  1561. feature = featureCpy;
  1562. break;
  1563. }
  1564. }
  1565. } catch (e) {
  1566. console.error("Failed to find feature");
  1567. // in case something goes _really_ wrong, reset
  1568. feature = featureCpy;
  1569. }
  1570.  
  1571. // for the first batch of subclass features, try to split them up
  1572. if (firstFeatures && feature.name && feature.entries) {
  1573. const subFeatures = [];
  1574. const baseFeatures = feature.entries.filter(f => {
  1575. if (f.name && f.type === "entries") {
  1576. subFeatures.push(f);
  1577. return false;
  1578. } else return true;
  1579. });
  1580. addFeatureToSheet({name: feature.name, type: feature.type, entries: baseFeatures});
  1581. subFeatures.forEach(sf => {
  1582. addFeatureToSheet(sf);
  1583. })
  1584. } else {
  1585. addFeatureToSheet(feature);
  1586. }
  1587.  
  1588. function addFeatureToSheet (feature) {
  1589. const renderStack = [];
  1590. renderer.recursiveEntryRender({entries: feature.entries}, renderStack);
  1591.  
  1592. const fRowId = d20plus.generateRowId();
  1593. character.model.attribs.create({
  1594. name: `repeating_traits_${fRowId}_name`,
  1595. current: feature.name
  1596. });
  1597. character.model.attribs.create({
  1598. name: `repeating_traits_${fRowId}_source`,
  1599. current: "Class"
  1600. });
  1601. character.model.attribs.create({
  1602. name: `repeating_traits_${fRowId}_source_type`,
  1603. current: `${sc.class} (${sc.name} ${gainLevels[i]})`
  1604. });
  1605. character.model.attribs.create({
  1606. name: `repeating_traits_${fRowId}_description`,
  1607. current: d20plus.importer.getCleanText(renderStack.join(""))
  1608. });
  1609. character.model.attribs.create({
  1610. name: `repeating_traits_${fRowId}_options-flag`,
  1611. current: "0"
  1612. });
  1613. }
  1614.  
  1615. firstFeatures = false;
  1616. }
  1617. }
  1618. } else if (data.data.Category === "Psionics") {
  1619. function makeSpellTrait (level, rowId, propName, content) {
  1620. character.model.attribs.create({
  1621. "name": `repeating_spell-${level}_${rowId}_${propName}`,
  1622. "current": `${content}`
  1623. });
  1624. }
  1625.  
  1626. // disable all components
  1627. function noComponents (level, rowId, hasM) {
  1628. makeSpellTrait(level, rowId, "spellcomp_v", 0);
  1629. makeSpellTrait(level, rowId, "spellcomp_s", 0);
  1630. if (!hasM) {
  1631. makeSpellTrait(level, rowId, "spellcomp_m", 0);
  1632. }
  1633. makeSpellTrait(level, rowId, "options-flag", 0);
  1634. }
  1635.  
  1636. const renderer = new EntryRenderer();
  1637. renderer.setBaseUrl(BASE_SITE_URL);
  1638.  
  1639. if (data.type === "D") {
  1640. const rowId = d20plus.generateRowId();
  1641.  
  1642. // make focus
  1643. const focusLevel = "cantrip";
  1644. makeSpellTrait(focusLevel, rowId, "spelllevel", "cantrip");
  1645. makeSpellTrait(focusLevel, rowId, "spellname", `${data.name} Focus`);
  1646. makeSpellTrait(focusLevel, rowId, "spelldescription", data.focus);
  1647. makeSpellTrait(focusLevel, rowId, "spellcastingtime", "1 bonus action");
  1648. noComponents(focusLevel, rowId);
  1649.  
  1650. data.modes.forEach(m => {
  1651. if (m.submodes) {
  1652. m.submodes.forEach(sm => {
  1653. const rowId = d20plus.generateRowId();
  1654. const smLevel = sm.cost.min;
  1655. makeSpellTrait(smLevel, rowId, "spelllevel", smLevel);
  1656. makeSpellTrait(smLevel, rowId, "spellname", `${m.name} (${sm.name})`);
  1657. const renderStack = [];
  1658. renderer.recursiveEntryRender({entries: sm.entries}, renderStack, 3);
  1659. makeSpellTrait(smLevel, rowId, "spelldescription", d20plus.importer.getCleanText(renderStack.join("")));
  1660. const costStr = sm.cost.min === sm.cost.max ? sm.cost.min : `${sm.cost.min}-${sm.cost.max}`;
  1661. makeSpellTrait(smLevel, rowId, "spellcomp_materials", `${costStr} psi points`);
  1662. noComponents(smLevel, rowId, true);
  1663. });
  1664. } else {
  1665. const rowId = d20plus.generateRowId();
  1666. const mLevel = m.cost.min;
  1667. makeSpellTrait(mLevel, rowId, "spelllevel", mLevel);
  1668. makeSpellTrait(mLevel, rowId, "spellname", `${m.name}`);
  1669. const renderStack = [];
  1670. renderer.recursiveEntryRender({entries: m.entries}, renderStack, 3);
  1671. makeSpellTrait(mLevel, rowId, "spelldescription", `Psionic Discipline mode\n\n${d20plus.importer.getCleanText(renderStack.join(""))}`);
  1672. const costStr = m.cost.min === m.cost.max ? m.cost.min : `${m.cost.min}-${m.cost.max}`;
  1673. makeSpellTrait(mLevel, rowId, "spellcomp_materials", `${costStr} psi points`);
  1674. if (m.concentration) {
  1675. makeSpellTrait(mLevel, rowId, "spellduration", `${m.concentration.duration} ${m.concentration.unit}`);
  1676. makeSpellTrait(mLevel, rowId, "spellconcentration", "Yes");
  1677. }
  1678. noComponents(mLevel, rowId, true);
  1679. }
  1680. });
  1681. } else {
  1682. const rowId = d20plus.generateRowId();
  1683. const level = "cantrip";
  1684. makeSpellTrait(level, rowId, "spelllevel", "cantrip");
  1685. makeSpellTrait(level, rowId, "spellname", data.name);
  1686. makeSpellTrait(level, rowId, "spelldescription", `Psionic Talent\n\n${d20plus.importer.getCleanText(EntryRenderer.psionic.getTalentText(data, renderer))}`);
  1687. noComponents(level, rowId, false);
  1688. }
  1689. } else {
  1690. function doDefaultDrop (n, outerI) {
  1691. const e = character;
  1692. var i = $(outerI.helper[0]).attr("data-pagename");
  1693.  
  1694. // BEGIN ROLL20 CODE
  1695. var r = n.data;
  1696. r.Name = n.name,
  1697. r.uniqueName = i,
  1698. r.Content = n.content;
  1699. var o = $(t.target);
  1700. o.find("*[accept]").each(function() {
  1701. var t = $(this)
  1702. , n = t.attr("accept");
  1703. r[n] && ("input" === t[0].tagName.toLowerCase() && "checkbox" === t.attr("type") ? t.attr("value") == r[n] ? t.attr("checked", "checked") : t.removeAttr("checked") : "input" === t[0].tagName.toLowerCase() && "radio" === t.attr("type") ? t.attr("value") == r[n] ? t.attr("checked", "checked") : t.removeAttr("checked") : "select" === t[0].tagName.toLowerCase() ? t.find("option").each(function() {
  1704. var e = $(this);
  1705. (e.attr("value") === r[n] || e.text() === r[n]) && e.attr("selected", "selected")
  1706. }) : $(this).val(r[n]),
  1707. e.saveSheetValues(this))
  1708. })
  1709. // END ROLL20 CODE
  1710. }
  1711. doDefaultDrop(data, i);
  1712. doSetDirty = false;
  1713. }
  1714.  
  1715. if (doSetDirty) {
  1716. character.model.view._updateSheetValues();
  1717. const dirty = [];
  1718. extraDirty.forEach(ed => {
  1719. dirty.push(ed);
  1720. });
  1721. $.each(d20.journal.customSheets.attrDeps, function (i, v) {
  1722. dirty.push(i);
  1723. });
  1724. d20.journal.notifyWorkersOfAttrChanges(character.model.view.model.id, dirty, true);
  1725. }
  1726. }
  1727. } else {
  1728. // rename some variables...
  1729. const e = character;
  1730. const n = i;
  1731.  
  1732. // original roll20 code
  1733. console.log("Compendium item dropped onto target!");
  1734. t.originalEvent.dropHandled = !0;
  1735. window.wantsToReceiveDrop(this, t, function () {
  1736. var i = $(n.helper[0]).attr("data-pagename");
  1737. console.log(d20.compendium.compendiumBase + "compendium/" + COMPENDIUM_BOOK_NAME + "/" + i + ".json?plaintext=true"),
  1738. $.get(d20.compendium.compendiumBase + "compendium/" + COMPENDIUM_BOOK_NAME + "/" + i + ".json?plaintext=true", function (n) {
  1739. var r = n.data;
  1740. r.Name = n.name,
  1741. r.uniqueName = i,
  1742. r.Content = n.content;
  1743. var o = $(t.target);
  1744. o.find("*[accept]").each(function () {
  1745. var t = $(this)
  1746. , n = t.attr("accept");
  1747. r[n] && ("input" === t[0].tagName.toLowerCase() && "checkbox" === t.attr("type") ? t.attr("value") == r[n] ? t.attr("checked", "checked") : t.removeAttr("checked") : "input" === t[0].tagName.toLowerCase() && "radio" === t.attr("type") ? t.attr("value") == r[n] ? t.attr("checked", "checked") : t.removeAttr("checked") : "select" === t[0].tagName.toLowerCase() ? t.find("option").each(function () {
  1748. var e = $(this);
  1749. (e.attr("value") === r[n] || e.text() === r[n]) && e.attr("selected", "selected")
  1750. }) : $(this).val(r[n]),
  1751. e.saveSheetValues(this))
  1752. })
  1753. })
  1754. });
  1755. }
  1756. }
  1757. });
  1758. });
  1759. };
  1760. });
  1761. };
  1762.  
  1763. d20plus.getProfBonusFromLevel = function (level) {
  1764. if (level < 5) return "2";
  1765. if (level < 9) return "3";
  1766. if (level < 13) return "4";
  1767. if (level < 17) return "5";
  1768. return "6";
  1769. };
  1770.  
  1771. d20plus.monsters._groupOptions = ["Type", "CR", "Alphabetical", "Source"];
  1772. d20plus.monsters._listCols = ["name", "type", "cr", "source"];
  1773. d20plus.monsters._listItemBuilder = (it) => `
  1774. <span class="name col-4" title="name">${it.name}</span>
  1775. <span class="type col-4" title="type">TYP[${Parser.monTypeToFullObj(it.type).asText.uppercaseFirst()}]</span>
  1776. <span class="cr col-2" title="cr">${it.cr === undefined ? "CR[Unknown]" : `CR[${(it.cr.cr || it.cr)}]`}</span>
  1777. <span title="source (Full: ${Parser.sourceJsonToFull(it.source)})" class="source">SRC[${Parser.sourceJsonToAbv(it.source)}]</span>`;
  1778. d20plus.monsters._listIndexConverter = (m) => {
  1779. m.__pType = m.__pType || Parser.monTypeToFullObj(m.type).type; // only filter using primary type
  1780. return {
  1781. name: m.name.toLowerCase(),
  1782. type: m.__pType.toLowerCase(),
  1783. cr: m.cr === undefined ? "unknown" : (m.cr.cr || m.cr).toLowerCase(),
  1784. source: Parser.sourceJsonToAbv(m.source).toLowerCase()
  1785. };
  1786. };
  1787. // Import Monsters button was clicked
  1788. d20plus.monsters.button = function () {
  1789. function loadData (url) {
  1790. DataUtil.loadJSON(url, (data) => {
  1791. d20plus.importer.addMeta(data._meta);
  1792. d20plus.importer.showImportList(
  1793. "monster",
  1794. data.monster,
  1795. d20plus.monsters.handoutBuilder,
  1796. {
  1797. groupOptions: d20plus.monsters._groupOptions,
  1798. listItemBuilder: d20plus.monsters._listItemBuilder,
  1799. listIndex: d20plus.monsters._listCols,
  1800. listIndexConverter: d20plus.monsters._listIndexConverter
  1801. }
  1802. );
  1803. });
  1804. }
  1805.  
  1806. const url = $("#import-monster-url").val();
  1807. if (url && url.trim()) {
  1808. // ugly hack to pre-load fluff
  1809. const fileName = url.split("/").reverse()[0];
  1810. const src = Object.keys(monsterDataUrls).find(k => monsterDataUrls[k] === fileName);
  1811. if (src && monsterFluffDataUrls[src]) {
  1812. const fluffUrl = d20plus.monsters.formMonsterUrl(monsterFluffDataUrls[src]);
  1813. DataUtil.loadJSON(fluffUrl, (data) => {
  1814. monsterFluffData[src] = data;
  1815. loadData(url);
  1816. });
  1817. } else {
  1818. loadData(url);
  1819. }
  1820. }
  1821. };
  1822.  
  1823. // Import All Monsters button was clicked
  1824. d20plus.monsters.buttonAll = function () {
  1825. function loadData () {
  1826. const toLoad = Object.keys(monsterDataUrls).filter(src => !isNonstandardSource(src)).map(src => d20plus.monsters.formMonsterUrl(monsterDataUrls[src]));
  1827. if (toLoad.length) {
  1828. DataUtil.multiLoadJSON(
  1829. toLoad.map(url => ({url})),
  1830. () => {},
  1831. (dataStack) => {
  1832. let toAdd = [];
  1833. dataStack.forEach(d => toAdd = toAdd.concat(d.monster));
  1834. d20plus.importer.showImportList(
  1835. "monster",
  1836. toAdd,
  1837. d20plus.monsters.handoutBuilder,
  1838. {
  1839. groupOptions: d20plus.monsters._groupOptions,
  1840. listItemBuilder: d20plus.monsters._listItemBuilder,
  1841. listIndex: d20plus.monsters._listCols,
  1842. listIndexConverter: d20plus.monsters._listIndexConverter
  1843. }
  1844. );
  1845. }
  1846. );
  1847. }
  1848. }
  1849.  
  1850. // preload fluff if available
  1851. const toLoadFluff = Object.keys(monsterFluffDataUrls)
  1852. .filter(src => !isNonstandardSource(src))
  1853. .map(src => ({url: d20plus.monsters.formMonsterUrl(monsterFluffDataUrls[src]), src}));
  1854. if (toLoadFluff.length) {
  1855. DataUtil.multiLoadJSON(
  1856. toLoadFluff,
  1857. (tl, data) => {
  1858. monsterFluffData[tl.src] = data;
  1859. },
  1860. () => {
  1861. loadData();
  1862. }
  1863. );
  1864. } else {
  1865. loadData();
  1866. }
  1867. };
  1868.  
  1869. d20plus.monsters.formMonsterUrl = function (fileName) {
  1870. return d20plus.formSrcUrl(MONSTER_DATA_DIR, fileName);
  1871. };
  1872.  
  1873. d20plus.importer.getSetAvatarImage = function (character, avatar) {
  1874. character.attributes.avatar = avatar;
  1875. var tokensize = 1;
  1876. if (character.size === "L") tokensize = 2;
  1877. if (character.size === "H") tokensize = 3;
  1878. if (character.size === "G") tokensize = 4;
  1879. var lightradius = 5;
  1880. if (character.senses && character.senses.toLowerCase().match(/(darkvision|blindsight|tremorsense|truesight)/)) lightradius = Math.max.apply(Math, character.senses.match(/\d+/g));
  1881. var lightmin = 0;
  1882. if (character.senses && character.senses.toLowerCase().match(/(blindsight|tremorsense|truesight)/)) lightmin = lightradius;
  1883. const nameSuffix = d20plus.getCfgVal("token", "namesuffix");
  1884. var defaulttoken = {
  1885. represents: character.id,
  1886. name: `${character.name}${nameSuffix ? ` ${nameSuffix}` : ""}`,
  1887. imgsrc: avatar,
  1888. width: 70 * tokensize,
  1889. height: 70 * tokensize
  1890. };
  1891. if (!d20plus.getCfgVal("import", "skipSenses")) {
  1892. defaulttoken.light_hassight = true;
  1893. defaulttoken.light_radius = lightradius;
  1894. defaulttoken.light_dimradius = lightmin;
  1895. }
  1896.  
  1897. character.updateBlobs({avatar: avatar, defaulttoken: JSON.stringify(defaulttoken)});
  1898. character.save({defaulttoken: (new Date()).getTime()});
  1899. };
  1900.  
  1901. d20plus.importer.addAction = function (character, name, text, index) {
  1902. if (d20plus.getCfgVal("token", "tokenactions")) {
  1903. character.abilities.create({
  1904. name: index + ": " + name,
  1905. istokenaction: true,
  1906. action: d20plus.actionMacroAction(index)
  1907. });
  1908. }
  1909.  
  1910. var newRowId = d20plus.generateRowId();
  1911. var actiontext = text;
  1912. var action_desc = actiontext; // required for later reduction of information dump.
  1913. var rollbase = d20plus.importer.rollbase;
  1914. // attack parsing
  1915. if (actiontext.indexOf(" Attack:") > -1) {
  1916. var attacktype = "";
  1917. var attacktype2 = "";
  1918. if (actiontext.indexOf(" Weapon Attack:") > -1) {
  1919. attacktype = actiontext.split(" Weapon Attack:")[0];
  1920. attacktype2 = " Weapon Attack:";
  1921. } else if (actiontext.indexOf(" Spell Attack:") > -1) {
  1922. attacktype = actiontext.split(" Spell Attack:")[0];
  1923. attacktype2 = " Spell Attack:";
  1924. }
  1925. var attackrange = "";
  1926. var rangetype = "";
  1927. if (attacktype.indexOf("Melee") > -1) {
  1928. attackrange = (actiontext.match(/reach (.*?),/) || ["", ""])[1];
  1929. rangetype = "Reach";
  1930. } else {
  1931. attackrange = (actiontext.match(/range (.*?),/) || ["", ""])[1];
  1932. rangetype = "Range";
  1933. }
  1934. var tohit = (actiontext.match(/\+(.*?) to hit/) || ["", ""])[1];
  1935. var damage = "";
  1936. var damagetype = "";
  1937. var damage2 = "";
  1938. var damagetype2 = "";
  1939. var onhit = "";
  1940. damageregex = /\d+ \((\d+d\d+\s?(?:\+|-)?\s?\d*)\) (\S+ )?damage/g;
  1941. damagesearches = damageregex.exec(actiontext);
  1942. if (damagesearches) {
  1943. onhit = damagesearches[0];
  1944. damage = damagesearches[1];
  1945. damagetype = (damagesearches[2] != null) ? damagesearches[2].trim() : "";
  1946. damagesearches = damageregex.exec(actiontext);
  1947. if (damagesearches) {
  1948. onhit += " plus " + damagesearches[0];
  1949. damage2 = damagesearches[1];
  1950. damagetype2 = (damagesearches[2] != null) ? damagesearches[2].trim() : "";
  1951. }
  1952. }
  1953. onhit = onhit.trim();
  1954. var attacktarget = (actiontext.match(/\.,(?!.*\.,)(.*)\. Hit:/) || ["", ""])[1];
  1955. // Cut the information dump in the description
  1956. var atk_desc_simple_regex = /Hit: \d+ \((\d+d\d+\s?(?:\+|-)?\s?\d*)\) (\S+ )?damage\.(.*)/g;
  1957. var atk_desc_complex_regex = /(Hit:.*)/g;
  1958. // is it a simple attack (just 1 damage type)?
  1959. var match_simple_atk = atk_desc_simple_regex.exec(actiontext);
  1960. if (match_simple_atk != null) {
  1961. //if yes, then only display special effects, if any
  1962. action_desc = match_simple_atk[3].trim();
  1963. } else {
  1964. //if not, simply cut everything before "Hit:" so there are no details lost.
  1965. var match_compl_atk = atk_desc_complex_regex.exec(actiontext);
  1966. if (match_compl_atk != null) action_desc = match_compl_atk[1].trim();
  1967. }
  1968. var tohitrange = "+" + tohit + ", " + rangetype + " " + attackrange + ", " + attacktarget + ".";
  1969. var damageflags = `{{damage=1}} {{dmg1flag=1}}${damage2 ? ` {{dmg2flag=1}}` : ""}`
  1970. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_name", current: name});
  1971. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_flag", current: "on"});
  1972. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_npc_options-flag", current: "0"});
  1973. character.attribs.create({
  1974. name: "repeating_npcaction_" + newRowId + "_attack_display_flag",
  1975. current: "{{attack=1}}"
  1976. });
  1977. character.attribs.create({
  1978. name: "repeating_npcaction_" + newRowId + "_attack_options",
  1979. current: "{{attack=1}}"
  1980. });
  1981. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_tohit", current: tohit});
  1982. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_damage", current: damage});
  1983. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_crit", current: damage});
  1984. character.attribs.create({
  1985. name: "repeating_npcaction_" + newRowId + "_attack_damagetype",
  1986. current: damagetype
  1987. });
  1988. if (damage2) {
  1989. character.attribs.create({
  1990. name: "repeating_npcaction_" + newRowId + "_attack_damage2",
  1991. current: damage2
  1992. });
  1993. character.attribs.create({
  1994. name: "repeating_npcaction_" + newRowId + "_attack_crit2",
  1995. current: damage2
  1996. });
  1997. character.attribs.create({
  1998. name: "repeating_npcaction_" + newRowId + "_attack_damagetype2",
  1999. current: damagetype2
  2000. });
  2001. }
  2002. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_name_display", current: name});
  2003. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_rollbase", current: rollbase});
  2004. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_type", current: attacktype});
  2005. character.attribs.create({
  2006. name: "repeating_npcaction_" + newRowId + "_attack_type_display",
  2007. current: attacktype + attacktype2
  2008. });
  2009. character.attribs.create({
  2010. name: "repeating_npcaction_" + newRowId + "_attack_tohitrange",
  2011. current: tohitrange
  2012. });
  2013. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_range", current: attackrange});
  2014. character.attribs.create({
  2015. name: "repeating_npcaction_" + newRowId + "_attack_target",
  2016. current: attacktarget
  2017. });
  2018. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_damage_flag", current: damageflags});
  2019. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_onhit", current: onhit});
  2020. } else {
  2021. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_name", current: name});
  2022. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_npc_options-flag", current: 0});
  2023. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_rollbase", current: rollbase});
  2024. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_name_display", current: name});
  2025. }
  2026. var descriptionFlag = Math.max(Math.ceil(text.length / 57), 1);
  2027. character.attribs.create({
  2028. name: "repeating_npcaction_" + newRowId + "_description",
  2029. current: action_desc
  2030. });
  2031. character.attribs.create({
  2032. name: "repeating_npcaction_" + newRowId + "_description_flag",
  2033. current: descriptionFlag
  2034. });
  2035. };
  2036.  
  2037. // Create monster character from js data object
  2038. d20plus.monsters.handoutBuilder = function (data, overwrite, inJournals, folderName, saveIdsTo) {
  2039. // make dir
  2040. const folder = d20plus.importer.makeDirTree(`Monsters`, folderName);
  2041. const path = ["Monsters", folderName, data.name];
  2042.  
  2043. // handle duplicates/overwrites
  2044. if (!d20plus.importer._checkHandleDuplicate(path, overwrite)) return;
  2045.  
  2046. const name = data.name;
  2047. const pType = Parser.monTypeToFullObj(data.type);
  2048.  
  2049. const renderer = new EntryRenderer();
  2050. renderer.setBaseUrl(BASE_SITE_URL);
  2051.  
  2052. // get fluff, if available
  2053. const includedFluff = data.fluff;
  2054. let renderFluff = null;
  2055. // prefer fluff directly attached to the creature
  2056. if (includedFluff) {
  2057. if (includedFluff.entries) {
  2058. const depth = includedFluff.entries.type === "section" ? -1 : 2;
  2059. renderFluff = renderer.renderEntry(includedFluff.entries, depth);
  2060. }
  2061. } else {
  2062. const fluffData = monsterFluffData[data.source] ? monsterFluffData[data.source] : null;
  2063. const fluff = fluffData ? monsterFluffData[data.source].monster.find(it => it.name === data.name) : null;
  2064. if (fluff) {
  2065. if (fluff._copy) {
  2066. const cpy = fluffData.monster.find(it => fluff._copy.name === it.name);
  2067. // preserve these
  2068. const name = fluff.name;
  2069. const src = fluff.source;
  2070. const images = fluff.images;
  2071. Object.assign(fluff, cpy);
  2072. fluff.name = name;
  2073. fluff.source = src;
  2074. if (images) fluff.images = images;
  2075. delete fluff._copy;
  2076. }
  2077.  
  2078. if (fluff._appendCopy) {
  2079. const cpy = fluffData.monster.find(it => fluff._appendCopy.name === it.name);
  2080. if (cpy.images) {
  2081. if (!fluff.images) fluff.images = cpy.images;
  2082. else fluff.images = fluff.images.concat(cpy.images);
  2083. }
  2084. if (cpy.entries) {
  2085. if (!fluff.entries) fluff.entries = cpy.entries;
  2086. else fluff.entries.entries = fluff.entries.entries.concat(cpy.entries.entries);
  2087. }
  2088. delete fluff._appendCopy;
  2089. }
  2090.  
  2091. if (fluff.entries) {
  2092. const depth = fluff.entries.type === "section" ? -1 : 2;
  2093. renderFluff = renderer.renderEntry(fluff.entries, depth);
  2094. }
  2095. }
  2096. }
  2097.  
  2098. d20.Campaign.characters.create(
  2099. {
  2100. name: name,
  2101. tags: d20plus.importer.getTagString([
  2102. pType.type,
  2103. ...pType.tags,
  2104. `cr ${(data.cr ? (data.cr.cr || data.cr) : "").replace(/\//g, " over ")}` || "unknown cr",
  2105. Parser.sourceJsonToFull(data.source)
  2106. ], "monsters")
  2107. },
  2108. {
  2109. success: function (character) {
  2110. if (saveIdsTo) saveIdsTo[UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_BESTIARY](data)] = {name: data.name, source: data.source, type: "character", roll20Id: character.id};
  2111. /* OGL Sheet */
  2112. try {
  2113. const type = Parser.monTypeToFullObj(data.type).asText;
  2114. const source = Parser.sourceJsonToAbv(data.source);
  2115. const avatar = data.tokenURL || `${IMG_URL}${source}/${name.replace(/"/g, "")}.png`;
  2116. character.size = data.size;
  2117. character.name = name;
  2118. character.senses = data.senses;
  2119. character.hp = data.hp.average || 0;
  2120. $.ajax({
  2121. url: avatar,
  2122. type: 'HEAD',
  2123. error: function () {
  2124. d20plus.importer.getSetAvatarImage(character, `${IMG_URL}blank.png`);
  2125. },
  2126. success: function () {
  2127. d20plus.importer.getSetAvatarImage(character, avatar);
  2128. }
  2129. });
  2130. var ac = data.ac.match(/^\d+/);
  2131. var actype = /\(([^)]+)\)/.exec(data.ac);
  2132. var hp = data.hp.average || 0;
  2133. var hpformula = data.hp.formula;
  2134. var passive = data.passive != null ? data.passive : "";
  2135. var passiveStr = passive !== "" ? "passive Perception " + passive : "";
  2136. var senses = data.senses || "";
  2137. var sensesStr = senses !== "" ? senses + ", " + passiveStr : passiveStr;
  2138. var size = d20plus.getSizeString(data.size || "");
  2139. var alignment = data.alignment ? Parser.alignmentListToFull(data.alignment).toLowerCase() : "(Unknown Alignment)";
  2140. var cr = data.cr ? (data.cr.cr || data.cr) : "";
  2141. var xp = Parser.crToXp(cr);
  2142. character.attribs.create({name: "npc", current: 1});
  2143. character.attribs.create({name: "npc_toggle", current: 1});
  2144. character.attribs.create({name: "npc_options-flag", current: 0});
  2145. character.attribs.create({name: "wtype", current: d20plus.importer.getDesiredWhisperType()});
  2146. character.attribs.create({name: "rtype", current: d20plus.importer.getDesiredRollType()});
  2147. character.attribs.create({
  2148. name: "advantagetoggle",
  2149. current: d20plus.importer.getDesiredAdvantageToggle()
  2150. });
  2151. character.attribs.create({
  2152. name: "whispertoggle",
  2153. current: d20plus.importer.getDesiredWhisperToggle()
  2154. });
  2155. character.attribs.create({name: "dtype", current: d20plus.importer.getDesiredDamageType()});
  2156. character.attribs.create({name: "npc_name", current: name});
  2157. character.attribs.create({name: "npc_size", current: size});
  2158. character.attribs.create({name: "type", current: type});
  2159. character.attribs.create({name: "npc_type", current: size + " " + type + ", " + alignment});
  2160. character.attribs.create({name: "npc_alignment", current: alignment});
  2161. character.attribs.create({name: "npc_ac", current: ac != null ? ac[0] : ""});
  2162. character.attribs.create({name: "npc_actype", current: actype != null ? actype[1] || "" : ""});
  2163. character.attribs.create({name: "npc_hpbase", current: hp != null ? hp : ""});
  2164. character.attribs.create({
  2165. name: "npc_hpformula",
  2166. current: hpformula != null ? hpformula || "" : ""
  2167. });
  2168. const parsedSpeed = Parser.getSpeedString(data);
  2169. data.npc_speed = parsedSpeed;
  2170. if (d20plus.sheet === "shaped") {
  2171. data.npc_speed = data.npc_speed.toLowerCase();
  2172. var match = data.npc_speed.match(/^\s*(\d+)\s?(ft\.?|m\.?)/);
  2173. if (match && match[1]) {
  2174. data.speed = match[1] + ' ' + match[2];
  2175. character.attribs.create({name: "speed", current: match[1] + ' ' + match[2]});
  2176. }
  2177. data.npc_speed = parsedSpeed;
  2178. var regex = /(burrow|climb|fly|swim)\s+(\d+)\s?(ft\.?|m\.?)/g;
  2179. var speeds = void 0;
  2180. while ((speeds = regex.exec(data.npc_speed)) !== null) character.attribs.create({
  2181. name: "speed_" + speeds[1],
  2182. current: speeds[2] + ' ' + speeds[3]
  2183. });
  2184. if (data.npc_speed && data.npc_speed.includes('hover')) character.attribs.create({
  2185. name: "speed_fly_hover",
  2186. current: 1
  2187. });
  2188. data.npc_speed = '';
  2189. }
  2190.  
  2191. function calcMod (score) {
  2192. return Math.floor((Number(score) - 10) / 2);
  2193. }
  2194.  
  2195. character.attribs.create({name: "npc_speed", current: parsedSpeed != null ? parsedSpeed : ""});
  2196. character.attribs.create({name: "strength", current: data.str});
  2197. character.attribs.create({name: "strength_base", current: data.str});
  2198. character.attribs.create({name: "strength_mod", current: calcMod(data.str)});
  2199.  
  2200. character.attribs.create({name: "dexterity", current: data.dex});
  2201. character.attribs.create({name: "dexterity_base", current: data.dex});
  2202. character.attribs.create({name: "dexterity_mod", current: calcMod(data.dex)});
  2203.  
  2204. character.attribs.create({name: "constitution", current: data.con});
  2205. character.attribs.create({name: "constitution_base", current: data.con});
  2206. character.attribs.create({name: "constitution_mod", current: calcMod(data.con)});
  2207.  
  2208. character.attribs.create({name: "intelligence", current: data.int});
  2209. character.attribs.create({name: "intelligence_base", current: data.int});
  2210. character.attribs.create({name: "intelligence_mod", current: calcMod(data.int)});
  2211.  
  2212. character.attribs.create({name: "wisdom", current: data.wis});
  2213. character.attribs.create({name: "wisdom_base", current: data.wis});
  2214. character.attribs.create({name: "wisdom_mod", current: calcMod(data.wis)});
  2215.  
  2216. character.attribs.create({name: "charisma", current: data.cha});
  2217. character.attribs.create({name: "charisma_base", current: data.cha});
  2218. character.attribs.create({name: "charisma_mod", current: calcMod(data.cha)});
  2219.  
  2220. character.attribs.create({name: "passive", current: passive});
  2221. character.attribs.create({
  2222. name: "npc_languages",
  2223. current: data.languages != null ? data.languages : ""
  2224. });
  2225. character.attribs.create({name: "npc_challenge", current: cr.cr || cr});
  2226. character.attribs.create({name: "npc_xp", current: xp});
  2227. character.attribs.create({
  2228. name: "npc_vulnerabilities",
  2229. current: data.vulnerable != null ? Parser.monImmResToFull(data.vulnerable) : ""
  2230. });
  2231. character.attribs.create({
  2232. name: "damage_vulnerabilities",
  2233. current: data.vulnerable != null ? Parser.monImmResToFull(data.vulnerable) : ""
  2234. });
  2235. character.attribs.create({
  2236. name: "npc_resistances",
  2237. current: data.resist != null ? Parser.monImmResToFull(data.resist) : ""
  2238. });
  2239. character.attribs.create({
  2240. name: "damage_resistances",
  2241. current: data.resist != null ? Parser.monImmResToFull(data.resist) : ""
  2242. });
  2243. character.attribs.create({name: "npc_immunities", current: data.immune != null ? Parser.monImmResToFull(data.immune) : ""});
  2244. character.attribs.create({
  2245. name: "damage_immunities",
  2246. current: data.immune != null ? Parser.monImmResToFull(data.immune) : ""
  2247. });
  2248. character.attribs.create({
  2249. name: "npc_condition_immunities",
  2250. current: data.conditionImmune != null ? Parser.monCondImmToFull(data.conditionImmune) : ""
  2251. });
  2252. character.attribs.create({
  2253. name: "damage_condition_immunities",
  2254. current: data.conditionImmune != null ? Parser.monCondImmToFull(data.conditionImmune) : ""
  2255. });
  2256. character.attribs.create({name: "npc_senses", current: sensesStr});
  2257.  
  2258. // add Tokenaction Macros
  2259. if (d20plus.getCfgVal("token", "tokenactions")) {
  2260. character.abilities.create({
  2261. name: "Perception",
  2262. istokenaction: true,
  2263. action: d20plus.actionMacroPerception
  2264. });
  2265. character.abilities.create({
  2266. name: "Init",
  2267. istokenaction: true,
  2268. action: d20plus.actionMacroInit
  2269. });
  2270. character.abilities.create({
  2271. name: "DR/Immunities",
  2272. istokenaction: true,
  2273. action: d20plus.actionMacroDrImmunities
  2274. });
  2275. character.abilities.create({
  2276. name: "Stats",
  2277. istokenaction: true,
  2278. action: d20plus.actionMacroStats
  2279. });
  2280. character.abilities.create({
  2281. name: "Saves",
  2282. istokenaction: true,
  2283. action: d20plus.actionMacroSaves
  2284. });
  2285. character.abilities.create({
  2286. name: "Skill-Check",
  2287. istokenaction: true,
  2288. action: d20plus.actionMacroSkillCheck
  2289. });
  2290. character.abilities.create({
  2291. name: "Ability-Check",
  2292. istokenaction: true,
  2293. action: d20plus.actionMacroAbilityCheck
  2294. });
  2295. }
  2296.  
  2297. if (data.save != null) {
  2298. character.attribs.create({name: "npc_saving_flag", current: 1});
  2299. Object.keys(data.save).forEach(k => {
  2300. character.attribs.create({
  2301. name: "npc_" + k + "_save_base",
  2302. current: data.save[k]
  2303. });
  2304. character.attribs.create({
  2305. name: k + "_saving_throw_proficient",
  2306. current: data.save[k]
  2307. });
  2308. });
  2309. }
  2310. if (data.skill != null) {
  2311. const skills = data.skill;
  2312. const skillsString = Object.keys(skills).map(function (k) {
  2313. return k.uppercaseFirst() + ' ' + skills[k];
  2314. }).join(', ');
  2315. character.attribs.create({name: "npc_skills_flag", current: 1});
  2316. character.attribs.create({name: "npc_skills", current: skillsString});
  2317.  
  2318. // Shaped Sheet currently doesn't correctly load NPC Skills
  2319. // This adds a visual representation as a Trait for reference
  2320. if (d20plus.sheet === "shaped") {
  2321. var newRowId = d20plus.generateRowId();
  2322. character.attribs.create({
  2323. name: "repeating_npctrait_" + newRowId + "_name",
  2324. current: "NPC Skills"
  2325. });
  2326. character.attribs.create({
  2327. name: "repeating_npctrait_" + newRowId + "_desc",
  2328. current: skillsString
  2329. });
  2330. }
  2331.  
  2332. $.each(skills, function (k, v) {
  2333. const cleanSKill = $.trim(k).toLowerCase().replace(/ /g, "_");
  2334. const cleanBonus = parseInt($.trim(v)) || 0;
  2335. character.attribs.create({
  2336. name: "npc_" + cleanSKill + "_base",
  2337. current: parseInt($.trim(v)) || 0
  2338. });
  2339. character.attribs.create({
  2340. name: "npc_" + cleanSKill + "_base",
  2341. current: cleanBonus
  2342. });
  2343. character.attribs.create({
  2344. name: "npc_" + cleanSKill,
  2345. current: cleanBonus
  2346. });
  2347. });
  2348. }
  2349. if (data.spellcasting) {
  2350. // make it a spellcaster
  2351. character.attribs.create({name: `npcspellcastingflag`, current: "1"});
  2352.  
  2353. // figure out the casting ability or spell DC
  2354. let spellDc;
  2355. let spellAbility;
  2356. let casterLevel;
  2357. for (const sc of data.spellcasting) {
  2358. const abils = [];
  2359. const toCheck = sc.headerEntries.join("");
  2360.  
  2361. // use the first ability/DC we find, since roll20 doesn't support multiple
  2362. const abM = /(strength|constitution|dexterity|intelligence|wisdom|charisma)/i.exec(toCheck);
  2363. const dcM = /DC (\d+)/i.exec(toCheck);
  2364. const lvlM = /(\d+)(st|nd|rd|th).level\s+spellcaster/i.exec(toCheck);
  2365. if (dcM) {
  2366. spellDc = dcM[1];
  2367. }
  2368. if (lvlM) {
  2369. casterLevel = lvlM[1];
  2370. }
  2371. if (abM) {
  2372. spellAbility = abM[1];
  2373. }
  2374. }
  2375. // delay these, does nothing otherwise (getting overwritten by turning on npc_spellcasting after, perhaps?)
  2376. // if (spellDc) character.attribs.create({name: `spell_save_dc`, current: spellDc});
  2377. // if (spellAbility) character.attribs.create({name: "spellcasting_ability", current: `@{${spellAbility.toLowerCase()}_mod}+`})
  2378. // if (casterLevel) character.attribs.create({name: "caster_level", current: casterLevel})
  2379. const charInterval = d20plus.getCfgVal("import", "importIntervalCharacter") || d20plus.getCfgDefaultVal("import", "importIntervalCharacter");
  2380. const spAbilsDelayMs = Math.max(350, Math.floor(charInterval / 5));
  2381. console.log(`Spellcasting import interval: ${spAbilsDelayMs} ms`);
  2382. setTimeout(() => {
  2383. if (spellDc) {
  2384. d20plus.importer.addOrUpdateAttr(character, "spell_save_dc", spellDc);
  2385. }
  2386. if (spellAbility) {
  2387. d20plus.importer.addOrUpdateAttr(character, "spellcasting_ability", `@{${spellAbility.toLowerCase()}_mod}+`);
  2388. }
  2389. if (casterLevel) {
  2390. d20plus.importer.addOrUpdateAttr(character, "caster_level", casterLevel);
  2391. d20plus.importer.addOrUpdateAttr(character, "level", Number(casterLevel));
  2392. }
  2393. }, spAbilsDelayMs);
  2394.  
  2395. // set spellcaster class, since this seems to reliably set spellcasting ability
  2396. if (spellAbility == "Intelligence") d20plus.importer.addOrUpdateAttr(character, "class", `Wizard`);
  2397. if (spellAbility == "Wisdom") d20plus.importer.addOrUpdateAttr(character, "class", `Cleric`);
  2398. if (spellAbility == "Charisma") d20plus.importer.addOrUpdateAttr(character, "class", `Bard`);
  2399.  
  2400. // add the spellcasting text
  2401. const newRowId = d20plus.generateRowId();
  2402. const spellTrait = EntryRenderer.monster.getSpellcastingRenderedTraits(data, renderer).map(it => it.rendered).filter(it => it).join("");
  2403. const cleanDescription = d20plus.importer.getCleanText(spellTrait);
  2404. character.attribs.create({
  2405. name: `repeating_npctrait_${newRowId}_name`,
  2406. current: "Spellcasting"
  2407. });
  2408. character.attribs.create({
  2409. name: `repeating_npctrait_${newRowId}_desc`,
  2410. current: cleanDescription
  2411. });
  2412.  
  2413. // begin building a spells macro
  2414. const tokenActionStack = [cleanDescription];
  2415.  
  2416. // add the spells
  2417. const allSpells = [];
  2418. data.spellcasting.forEach(sc => {
  2419. const toAdd = ["constant", "will", "rest", "daily", "weekly"];
  2420. toAdd.forEach(k => {
  2421. if (sc[k]) {
  2422. Object.values(sc[k]).forEach(spOrSpArr => {
  2423. if (spOrSpArr instanceof Array) {
  2424. Array.prototype.push.apply(allSpells, spOrSpArr);
  2425. } else {
  2426. allSpells.push(spOrSpArr);
  2427. }
  2428. });
  2429. }
  2430. });
  2431. if (sc.spells) {
  2432. Object.keys(sc.spells).forEach(lvl => {
  2433. // delayed creation of spell slots, once it's a spellcaster
  2434. setTimeout(() => {
  2435. if (sc.spells[lvl].slots) {
  2436. const slotName = `lvl${lvl}_slots_total`;
  2437. d20plus.importer.addOrUpdateAttr(character, slotName, String(sc.spells[lvl].slots));
  2438. }
  2439. }, spAbilsDelayMs);
  2440.  
  2441. if (sc.spells[lvl].spells) {
  2442. Array.prototype.push.apply(allSpells, sc.spells[lvl].spells);
  2443. }
  2444. });
  2445. }
  2446. });
  2447.  
  2448. // render sheet
  2449. character.view.render();
  2450.  
  2451. // add spells to sheet
  2452. const toAdd = [];
  2453. allSpells.forEach(sp => {
  2454. const tagSplit = EntryRenderer.splitByTags(sp);
  2455. tagSplit.forEach(s => {
  2456. if (!s || !s.trim()) return;
  2457. if (s.charAt(0) === "@") {
  2458. const [tag, text] = EntryRenderer.splitFirstSpace(s);
  2459. if (tag === "@spell") {
  2460. toAdd.push(text);
  2461. }
  2462. }
  2463. });
  2464. });
  2465.  
  2466. const addMacroIndex = toAdd.length - 1;
  2467. toAdd.forEach((text, i) => {
  2468. let [name, source] = text.split("|");
  2469. if (!source) source = "PHB";
  2470. const rawUrl = spellDataUrls[Object.keys(spellDataUrls).find(src => source.toLowerCase() === src.toLowerCase())];
  2471. const url = d20plus.spells.formSpellUrl(rawUrl);
  2472. // the JSON gets cached by the script, so this is fine
  2473. DataUtil.loadJSON(url, (data) => {
  2474. const spell = data.spell.find(spell => spell.name.toLowerCase() === name.toLowerCase());
  2475.  
  2476. const [notecontents, gmnotes] = d20plus.spells._getHandoutData(spell);
  2477.  
  2478. addSpell(JSON.parse(gmnotes), spell, i, addMacroIndex);
  2479. });
  2480. });
  2481.  
  2482. function addSpell (sp, VeSp, index, addMacroIndex) {
  2483. const rId = d20plus.generateRowId();
  2484. const lvl = sp.data.Level === "0" ? "cantrip" : sp.data.Level;
  2485. const base = `repeating_spell-${lvl}_${rId}_`;
  2486.  
  2487. makeAttrib("spelllevel", lvl);
  2488. makeAttrib("spellname", sp.name);
  2489. makeAttrib("spellschool", sp.data.School);
  2490. makeAttrib("spellcastingtime", sp.data["Casting Time"]); // spaces in property names l m a o
  2491. makeAttrib("spellrange", sp.data.Range);
  2492. makeAttrib("spelltarget", sp.data.Target);
  2493. makeAttrib("spellcomp_v", Number(!!VeSp.components.v));
  2494. makeAttrib("spellcomp_s", Number(!!VeSp.components.s));
  2495. makeAttrib("spellcomp_materials", sp.data.Material);
  2496. if (!sp.data.Material && !VeSp.components.m) makeAttrib("spellcomp_m", "0");
  2497. makeAttrib("spellconcentration", sp.data.Concentration)
  2498. makeAttrib("spellduration", sp.data.Duration);
  2499. makeAttrib("spelldamage", sp.data.Damage);
  2500. makeAttrib("spelldamagetype", sp.data["Damage Type"]);
  2501. makeAttrib("spellsave", sp.data.Save);
  2502. makeAttrib("spellsavesuccess", sp.data["Save Success"]);
  2503. makeAttrib("spellhldie", sp.data["Higher Spell Slot Dice"]);
  2504. makeAttrib("spellhldietype", sp.data["Higher Spell Slot Die"]);
  2505. const [text, hlText] = sp.content.split("\n\nAt Higher Levels:")
  2506. makeAttrib("spelldescription", addInlineRollers(text));
  2507. makeAttrib("spellathigherlevels", addInlineRollers(hlText));
  2508. makeAttrib("options-flag", "0");
  2509.  
  2510. // TODO reverse engineer/add the other ~20 attributes needed to make this work (see `enableSpellattackHack()`)
  2511. if (sp.content.toLowerCase().includes("ranged spell attack")) {
  2512. makeAttrib("spelloutput", "ATTACK");
  2513. makeAttrib("spellattack", "Ranged");
  2514. } else if (sp.content.toLowerCase().includes("melee spell attack")) {
  2515. makeAttrib("spelloutput", "ATTACK");
  2516. makeAttrib("spellattack", "Melee");
  2517. } else if (sp.data.Damage) {
  2518. makeAttrib("spelloutput", "ATTACK");
  2519. makeAttrib("spellattack", "None");
  2520. }
  2521.  
  2522. tokenActionStack.push(`[${sp.name}](~selected|${base}spell)`);
  2523.  
  2524. if (index === addMacroIndex) {
  2525. if (d20plus.getCfgVal("token", "tokenactions")) {
  2526. character.abilities.create({
  2527. name: "Spells",
  2528. istokenaction: true,
  2529. action: `/w gm @{selected|wtype}&{template:npcaction} {{name=@{selected|npc_name}}} {{rname=Spellcasting}} {{description=${tokenActionStack.join("")}}}`
  2530. });
  2531. }
  2532. enableSpellattackHack();
  2533. }
  2534.  
  2535. function enableSpellattackHack () {
  2536. // temporary(?) hack to avoid creating all the properties manually
  2537. setTimeout(() => {
  2538. const $sel = character.view.$charsheet.find(`select[name=attr_spelloutput]`).filter((i, ele) => {
  2539. return $(ele).val() === "ATTACK";
  2540. });
  2541. setTimeout(() => {
  2542. $sel.val("SPELLCARD").trigger("change")
  2543. setTimeout(() => {
  2544. $sel.val("ATTACK").trigger("change");
  2545. }, spAbilsDelayMs);
  2546. }, spAbilsDelayMs);
  2547. }, spAbilsDelayMs);
  2548. }
  2549.  
  2550. function makeAttrib (name, current) {
  2551. if (current !== undefined && current !== null) character.attribs.create({
  2552. name: `${base}${name}`,
  2553. current: current
  2554. });
  2555. }
  2556.  
  2557. function addInlineRollers (text) {
  2558. if (!text) return text;
  2559. return text.replace(DICE_REGEX, (match) => {
  2560. return `[[${match}]]`;
  2561. });
  2562. }
  2563. }
  2564. }
  2565. if (data.trait) {
  2566. $.each(data.trait, function (i, v) {
  2567. var newRowId = d20plus.generateRowId();
  2568. character.attribs.create({
  2569. name: "repeating_npctrait_" + newRowId + "_name",
  2570. current: v.name
  2571. });
  2572.  
  2573. if (d20plus.getCfgVal("token", "tokenactions")) {
  2574. const offsetIndex = data.spellcasting ? 1 + i : i;
  2575. character.abilities.create({
  2576. name: "Trait" + offsetIndex + ": " + v.name,
  2577. istokenaction: true,
  2578. action: d20plus.actionMacroTrait(offsetIndex)
  2579. });
  2580. }
  2581.  
  2582. var text = d20plus.importer.getCleanText(renderer.renderEntry({entries: v.entries}, 1));
  2583. character.attribs.create({name: "repeating_npctrait_" + newRowId + "_desc", current: text});
  2584. });
  2585. }
  2586. if (data.action) {
  2587. $.each(data.action, function (i, v) {
  2588. var text = d20plus.importer.getCleanText(renderer.renderEntry({entries: v.entries}, 1));
  2589. d20plus.importer.addAction(character, v.name, text, i);
  2590. });
  2591. }
  2592. if (data.reaction) {
  2593. character.attribs.create({name: "reaction_flag", current: 1});
  2594. character.attribs.create({name: "npcreactionsflag", current: 1});
  2595. $.each(data.reaction, function (i, v) {
  2596. var newRowId = d20plus.generateRowId();
  2597. var text = "";
  2598. character.attribs.create({
  2599. name: "repeating_npcreaction_" + newRowId + "_name",
  2600. current: v.name
  2601. });
  2602.  
  2603. // roll20 only supports a single reaction, so only use the first
  2604. if (d20plus.getCfgVal("token", "tokenactions") && i === 0) {
  2605. character.abilities.create({
  2606. name: "Reaction: " + v.name,
  2607. istokenaction: true,
  2608. action: d20plus.actionMacroReaction
  2609. });
  2610. }
  2611.  
  2612. var text = d20plus.importer.getCleanText(renderer.renderEntry({entries: v.entries}, 1));
  2613. character.attribs.create({
  2614. name: "repeating_npcreaction_" + newRowId + "_desc",
  2615. current: text
  2616. });
  2617. character.attribs.create({
  2618. name: "repeating_npcreaction_" + newRowId + "_description",
  2619. current: text
  2620. });
  2621. });
  2622. }
  2623. if (data.legendary) {
  2624. character.attribs.create({name: "legendary_flag", current: "1"});
  2625. let legendaryActions = data.legendaryActions || 3;
  2626. character.attribs.create({name: "npc_legendary_actions", current: legendaryActions.toString()});
  2627. let tokenactiontext = "";
  2628. $.each(data.legendary, function (i, v) {
  2629. var newRowId = d20plus.generateRowId();
  2630.  
  2631. if (d20plus.getCfgVal("token", "tokenactions")) {
  2632. tokenactiontext += "[" + v.name + "](~selected|repeating_npcaction-l_$" + i + "_npc_action)\n\r";
  2633. }
  2634.  
  2635. var rollbase = d20plus.importer.rollbase;
  2636. if (v.attack != null) {
  2637. if (!(v.attack instanceof Array)) {
  2638. var tmp = v.attack;
  2639. v.attack = [];
  2640. v.attack.push(tmp);
  2641. }
  2642. $.each(v.attack, function (z, x) {
  2643. if (!x) return;
  2644. var attack = x.split("|");
  2645. var name = "";
  2646. if (v.attack.length > 1)
  2647. name = (attack[0] == v.name) ? v.name : v.name + " - " + attack[0] + "";
  2648. else
  2649. name = v.name;
  2650. var onhit = "";
  2651. var damagetype = "";
  2652. if (attack.length == 2) {
  2653. damage = "" + attack[1];
  2654. tohit = "";
  2655. } else {
  2656. damage = "" + attack[2];
  2657. tohit = attack[1] || 0;
  2658. }
  2659. character.attribs.create({
  2660. name: "repeating_npcaction-l_" + newRowId + "_name",
  2661. current: name
  2662. });
  2663. character.attribs.create({
  2664. name: "repeating_npcaction-l_" + newRowId + "_attack_flag",
  2665. current: "on"
  2666. });
  2667. character.attribs.create({
  2668. name: "repeating_npcaction-l_" + newRowId + "_npc_options-flag",
  2669. current: 0
  2670. });
  2671. character.attribs.create({
  2672. name: "repeating_npcaction-l_" + newRowId + "_attack_display_flag",
  2673. current: "{{attack=1}}"
  2674. });
  2675. character.attribs.create({
  2676. name: "repeating_npcaction-l_" + newRowId + "_attack_options",
  2677. current: "{{attack=1}}"
  2678. });
  2679. character.attribs.create({
  2680. name: "repeating_npcaction-l_" + newRowId + "_attack_tohit",
  2681. current: tohit
  2682. });
  2683. character.attribs.create({
  2684. name: "repeating_npcaction-l_" + newRowId + "_attack_damage",
  2685. current: damage
  2686. });
  2687. character.attribs.create({
  2688. name: "repeating_npcaction-l_" + newRowId + "_name_display",
  2689. current: name
  2690. });
  2691. character.attribs.create({
  2692. name: "repeating_npcaction-l_" + newRowId + "_rollbase",
  2693. current: rollbase
  2694. });
  2695. character.attribs.create({
  2696. name: "repeating_npcaction-l_" + newRowId + "_attack_type",
  2697. current: ""
  2698. });
  2699. character.attribs.create({
  2700. name: "repeating_npcaction-l_" + newRowId + "_attack_tohitrange",
  2701. current: ""
  2702. });
  2703. character.attribs.create({
  2704. name: "repeating_npcaction-l_" + newRowId + "_damage_flag",
  2705. current: "{{damage=1}} {{dmg1flag=1}} {{dmg2flag=1}}"
  2706. });
  2707. if (damage !== "") {
  2708. damage1 = damage.replace(/\s/g, "").split(/d|(?=\+|-)/g);
  2709. if (damage1[1])
  2710. damage1[1] = damage1[1].replace(/[^0-9-+]/g, "");
  2711. damage2 = isNaN(eval(damage1[1])) === false ? eval(damage1[1]) : 0;
  2712. if (damage1.length < 2) {
  2713. onhit = onhit + damage1[0] + " (" + damage + ")" + damagetype + " damage";
  2714. } else if (damage1.length < 3) {
  2715. onhit = onhit + Math.floor(damage1[0] * ((damage2 / 2) + 0.5)) + " (" + damage + ")" + damagetype + " damage";
  2716. } else {
  2717. onhit = onhit + (Math.floor(damage1[0] * ((damage2 / 2) + 0.5)) + parseInt(damage1[2], 10)) + " (" + damage + ")" + damagetype + " damage";
  2718. }
  2719. }
  2720. character.attribs.create({
  2721. name: "repeating_npcaction-l_" + newRowId + "_attack_onhit",
  2722. current: onhit
  2723. });
  2724. });
  2725. } else {
  2726. character.attribs.create({
  2727. name: "repeating_npcaction-l_" + newRowId + "_name",
  2728. current: v.name
  2729. });
  2730. character.attribs.create({
  2731. name: "repeating_npcaction-l_" + newRowId + "_npc_options-flag",
  2732. current: 0
  2733. });
  2734. character.attribs.create({
  2735. name: "repeating_npcaction-l_" + newRowId + "_rollbase",
  2736. current: rollbase
  2737. });
  2738. character.attribs.create({
  2739. name: "repeating_npcaction-l_" + newRowId + "_name_display",
  2740. current: v.name
  2741. });
  2742. }
  2743.  
  2744. var text = d20plus.importer.getCleanText(renderer.renderEntry({entries: v.entries}, 1));
  2745. var descriptionFlag = Math.max(Math.ceil(text.length / 57), 1);
  2746. character.attribs.create({
  2747. name: "repeating_npcaction-l_" + newRowId + "_description",
  2748. current: text
  2749. });
  2750. character.attribs.create({
  2751. name: "repeating_npcaction-l_" + newRowId + "_description_flag",
  2752. current: descriptionFlag
  2753. });
  2754. });
  2755. if (d20plus.getCfgVal("token", "tokenactions")) {
  2756. character.abilities.create({
  2757. name: "Legendary Actions",
  2758. istokenaction: true,
  2759. action: d20plus.actionMacroLegendary(tokenactiontext)
  2760. });
  2761. }
  2762. }
  2763. character.view._updateSheetValues();
  2764. var dirty = [];
  2765. $.each(d20.journal.customSheets.attrDeps, function (i, v) {
  2766. dirty.push(i);
  2767. });
  2768. d20.journal.notifyWorkersOfAttrChanges(character.view.model.id, dirty, true);
  2769.  
  2770.  
  2771. if (renderFluff) {
  2772. setTimeout(() => {
  2773. const fluffAs = d20plus.getCfgVal("import", "importFluffAs") || d20plus.getCfgDefaultVal("import", "importFluffAs");
  2774. let k = fluffAs === "Bio"? "bio" : "gmnotes";
  2775. character.updateBlobs({
  2776. [k]: Markdown.parse(renderFluff)
  2777. });
  2778. character.save({
  2779. [k]: (new Date).getTime()
  2780. });
  2781. }, 500);
  2782. }
  2783. } catch (e) {
  2784. d20plus.log("Error loading [" + name + "]");
  2785. d20plus.addImportError(name);
  2786. console.log(data);
  2787. console.log(e);
  2788. }
  2789. /* end OGL Sheet */
  2790. d20.journal.addItemToFolderStructure(character.id, folder.id);
  2791. }
  2792. });
  2793. };
  2794.  
  2795. d20plus.importer.findAttrId = function (character, attrName) {
  2796. const found = character.attribs.toJSON().find(a => a.name === attrName);
  2797. return found ? found.id : undefined;
  2798. };
  2799.  
  2800. d20plus.importer.addOrUpdateAttr = function (character, attrName, value) {
  2801. const id = d20plus.importer.findAttrId(character, attrName);
  2802. if (id) {
  2803. character.attribs.get(id).set("current", value);
  2804. } else {
  2805. character.attribs.create({
  2806. "name": attrName,
  2807. "current": value
  2808. });
  2809. }
  2810. };
  2811.  
  2812. // Import dialog showing names of monsters failed to import
  2813. d20plus.addImportError = function (name) {
  2814. var $span = $("#import-errors");
  2815. if ($span.text() == "0") {
  2816. $span.text(name);
  2817. } else {
  2818. $span.text($span.text() + ", " + name);
  2819. }
  2820. };
  2821.  
  2822. // Get NPC size from chr
  2823. d20plus.getSizeString = function (chr) {
  2824. const result = Parser.sizeAbvToFull(chr);
  2825. return result ? result : "(Unknown Size)";
  2826. };
  2827.  
  2828. // Create editable HP variable and autocalculate + or -
  2829. d20plus.hpAllowEdit = function () {
  2830. $("#initiativewindow").on(window.mousedowntype, ".hp.editable", function () {
  2831. if ($(this).find("input").length > 0) return void $(this).find("input").focus();
  2832. var val = $.trim($(this).text());
  2833. const $span = $(this);
  2834. $span.html(`<input type='text' value='${val}'/>`);
  2835. const $ipt = $(this).find("input");
  2836. $ipt[0].focus();
  2837. });
  2838. $("#initiativewindow").on("keydown", ".hp.editable", function (event) {
  2839. if (event.which == 13) {
  2840. const $span = $(this);
  2841. const $ipt = $span.find("input");
  2842. if (!$ipt.length) return;
  2843.  
  2844. var el, token, id, char, hp,
  2845. val = $.trim($ipt.val());
  2846.  
  2847. // roll20 token modification supports plus/minus for a single integer; mimic this
  2848. const m = /^((\d+)?([+-]))?(\d+)$/.exec(val);
  2849. if (m) {
  2850. let op = null;
  2851. if (m[3]) {
  2852. op = m[3] === "+" ? "ADD" : "SUB";
  2853. }
  2854. const base = m[2] ? eval(m[0]) : null;
  2855. const mod = Number(m[4]);
  2856.  
  2857. el = $(this).parents("li.token");
  2858. id = el.data("tokenid");
  2859. token = d20.Campaign.pages.get(d20.Campaign.activePage()).thegraphics.get(id);
  2860. char = token.character;
  2861.  
  2862. npc = char.attribs ? char.attribs.find(function (a) {
  2863. return a.get("name").toLowerCase() === "npc";
  2864. }) : null;
  2865. let total;
  2866. // char.attribs doesn't exist for generico tokens, in this case stick stuff in an appropriate bar
  2867. if (!char.attribs || npc && npc.get("current") == "1") {
  2868. const hpBar = d20plus.getCfgHpBarNumber();
  2869. if (hpBar) {
  2870. total;
  2871. if (base !== null) {
  2872. total = base;
  2873. } else if (op) {
  2874. const curr = token.attributes[`bar${hpBar}_value`];
  2875. if (op === "ADD") total = curr + mod;
  2876. else total = curr - mod;
  2877. } else {
  2878. total = mod;
  2879. }
  2880. token.attributes[`bar${hpBar}_value`] = total;
  2881. }
  2882. } else {
  2883. hp = char.attribs.find(function (a) {
  2884. return a.get("name").toLowerCase() === "hp";
  2885. });
  2886. if (hp) {
  2887. total;
  2888. if (base !== null) {
  2889. total = base;
  2890. } else if (op) {
  2891. if (op === "ADD") total = hp.attributes.current + mod;
  2892. else total = hp.attributes.current - mod;
  2893. } else {
  2894. total = mod;
  2895. }
  2896. hp.syncedSave({current: total});
  2897. } else {
  2898. total;
  2899. if (base !== null) {
  2900. total = base;
  2901. } else if (op) {
  2902. if (op === "ADD") total = mod;
  2903. else total = 0 - mod;
  2904. } else {
  2905. total = mod;
  2906. }
  2907. char.attribs.create({name: "hp", current: total});
  2908. }
  2909. }
  2910. // convert the field back to text
  2911. $span.html(total);
  2912. }
  2913. d20.Campaign.initiativewindow.rebuildInitiativeList();
  2914. }
  2915. });
  2916. };
  2917.  
  2918. // Change character sheet formulas
  2919. d20plus.setSheet = function () {
  2920. d20plus.sheet = "ogl";
  2921. if (window.is_gm && (!d20.journal.customSheets || !d20.journal.customSheets)) {
  2922. d20.textchat.incoming(false, ({
  2923. who: "system",
  2924. type: "system",
  2925. content: `<span style="color: red;">5etoolsR20: no character sheet selected! Exiting...</span>`
  2926. }));
  2927. throw new Error("No character sheet selected!");
  2928. }
  2929. if (d20.journal.customSheets.layouthtml.indexOf("shaped_d20") > 0) d20plus.sheet = "shaped";
  2930. if (d20.journal.customSheets.layouthtml.indexOf("DnD5e_Character_Sheet") > 0) d20plus.sheet = "community";
  2931. d20plus.log("Switched Character Sheet Template to " + d20plus.sheet);
  2932. };
  2933.  
  2934. // Return Initiative Tracker template with formulas
  2935. d20plus.initErrorHandler = null;
  2936. d20plus.setTurnOrderTemplate = function () {
  2937. if (!d20plus.turnOrderCachedFunction) {
  2938. d20plus.turnOrderCachedFunction = d20.Campaign.initiativewindow.rebuildInitiativeList;
  2939. d20plus.turnOrderCachedTemplate = $("#tmpl_initiativecharacter").clone();
  2940. }
  2941.  
  2942. d20.Campaign.initiativewindow.rebuildInitiativeList = function () {
  2943. var html = d20plus.initiativeTemplate;
  2944. var columnsAdded = [];
  2945. $(".tracker-header-extra-columns").empty();
  2946.  
  2947. const cols = [
  2948. d20plus.getCfgVal("interface", "trackerCol1"),
  2949. d20plus.getCfgVal("interface", "trackerCol2"),
  2950. d20plus.getCfgVal("interface", "trackerCol3")
  2951. ];
  2952.  
  2953. const headerStack = [];
  2954. const replaceStack = [
  2955. // this is hidden by CSS
  2956. `<span class='cr' alt='CR' title='CR'>
  2957. <$ if(npc && npc.get("current") == "1") { $>
  2958. <$!char.attribs.find(function(e) { return e.get("name").toLowerCase() === "npc_challenge" }).get("current")$>
  2959. <$ } $>
  2960. </span>`
  2961. ];
  2962. cols.forEach((c, i) => {
  2963. switch (c) {
  2964. case "HP": {
  2965. const hpBar = d20plus.getCfgHpBarNumber();
  2966. replaceStack.push(`
  2967. <span class='hp editable tracker-col' alt='HP' title='HP'>
  2968. <$ if(npc && npc.get("current") == "1") { $>
  2969. ${hpBar ? `<$!token.attributes.bar${hpBar}_value$>` : ""}
  2970. <$ } else if (typeof char !== "undefined" && char && typeof char.autoCalcFormula !== "undefined") { $>
  2971. <$!char.autoCalcFormula('${d20plus.formulas[d20plus.sheet].hp}')$>
  2972. <$ } else { $>
  2973. <$!"\u2014"$>
  2974. <$ } $>
  2975. </span>
  2976. `);
  2977. headerStack.push(`<span class='tracker-col'>HP</span>`);
  2978. break;
  2979. }
  2980. case "AC": {
  2981. replaceStack.push(`
  2982. <span class='ac tracker-col' alt='AC' title='AC'>
  2983. <$ if(npc && npc.get("current") == "1" && typeof char !== "undefined" && char && typeof char.autoCalcFormula !== "undefined") { $>
  2984. <$!char.autoCalcFormula('${d20plus.formulas[d20plus.sheet].npcac}')$>
  2985. <$ } else if (typeof char !== "undefined" && char && typeof char.autoCalcFormula !== "undefined") { $>
  2986. <$!char.autoCalcFormula('${d20plus.formulas[d20plus.sheet].ac}')$>
  2987. <$ } else { $>
  2988. <$!"\u2014"$>
  2989. <$ } $>
  2990. </span>
  2991. `);
  2992. headerStack.push(`<span class='tracker-col'>AC</span>`);
  2993. break;
  2994. }
  2995. case "PP": {
  2996. replaceStack.push(`
  2997. <$ var passive = (typeof char !== "undefined" && char && typeof char.autoCalcFormula !== "undefined") ? (char.autoCalcFormula('@{passive}') || char.autoCalcFormula('${d20plus.formulas[d20plus.sheet].pp}')) : "\u2014"; $>
  2998. <span class='pp tracker-col' alt='Passive Perception' title='Passive Perception'><$!passive$></span>
  2999. `);
  3000. headerStack.push(`<span class='tracker-col'>PP</span>`);
  3001. break;
  3002. }
  3003. default: {
  3004. replaceStack.push(`<span class="tracker-col"/>`);
  3005. headerStack.push(`<span class="tracker-col"/>`);
  3006. }
  3007. }
  3008. });
  3009.  
  3010. console.log("use custom tracker val was ", d20plus.getCfgVal("interface", "customTracker"))
  3011. if (d20plus.getCfgVal("interface", "customTracker")) {
  3012. const $header = $(".tracker-header-extra-columns");
  3013. // prepend/reverse used since tracker gets populated in right-to-left order
  3014. headerStack.forEach(h => $header.prepend(h))
  3015. html = html.replace(`<!--5ETOOLS_REPLACE_TARGET-->`, replaceStack.reverse().join(" \n"));
  3016. }
  3017.  
  3018. $("#tmpl_initiativecharacter").replaceWith(html);
  3019.  
  3020. // Hack to catch errors, part 1
  3021. const startTime = (new Date).getTime();
  3022.  
  3023. var results = d20plus.turnOrderCachedFunction.apply(this, []);
  3024. setTimeout(function () {
  3025. $(".initmacrobutton").unbind("click");
  3026. $(".initmacrobutton").bind("click", function () {
  3027. console.log("Macro button clicked");
  3028. tokenid = $(this).parent().parent().data("tokenid");
  3029. var token, char;
  3030. var page = d20.Campaign.activePage();
  3031. if (page) token = page.thegraphics.get(tokenid);
  3032. if (token) char = token.character;
  3033. if (char) {
  3034. char.view.showDialog();
  3035. // d20.textchat.doChatInput(`%{` + char.id + `|` + d20plus.formulas[d20plus.sheet]["macro"] + `}`)
  3036. }
  3037. });
  3038.  
  3039. d20plus.bindTokens();
  3040. }, 100);
  3041.  
  3042. // Hack to catch errors, part 2
  3043. if (d20plus.initErrorHandler) {
  3044. window.removeEventListener("error", d20plus.initErrorHandler);
  3045. }
  3046. d20plus.initErrorHandler = function (event) {
  3047. // if we see an error within 250 msec of trying to override the initiative window...
  3048. if (((new Date).getTime() - startTime) < 250) {
  3049. d20plus.log("ERROR: failed to populate custom initiative tracker, restoring default...");
  3050. // restore the default functionality
  3051. $("#tmpl_initiativecharacter").replaceWith(d20plus.turnOrderCachedTemplate);
  3052. return d20plus.turnOrderCachedFunction();
  3053. }
  3054. };
  3055. window.addEventListener("error", d20plus.initErrorHandler);
  3056. return results;
  3057. };
  3058. };
  3059.  
  3060. d20plus.importer.makePlayerDraggable = function (importId, name) {
  3061. const $appTo = $(`#d20plus-playerimport`).find(`.Vetools-player-imported`);
  3062. const $li = $(`
  3063. <li class="journalitem dd-item handout ui-draggable compendium-item Vetools-draggable player-imported" data-playerimportid="${importId}">
  3064. <div class="dd-handle dd-sortablehandle">Drag</div>
  3065. <div class="dd-content">
  3066. <div class="token"><img src="/images/handout.png" draggable="false"></div>
  3067. <div class="name">
  3068. <div class="namecontainer">${name}</div>
  3069. </div>
  3070. </div>
  3071. </li>
  3072. `);
  3073. $li.draggable({
  3074. revert: true,
  3075. distance: 10,
  3076. revertDuration: 0,
  3077. helper: "clone",
  3078. handle: ".namecontainer",
  3079. appendTo: "body",
  3080. scroll: true,
  3081. start: function () {
  3082. console.log("drag start")
  3083. },
  3084. stop: function () {
  3085. console.log("drag stop")
  3086. }
  3087. });
  3088. $appTo.prepend($li);
  3089. };
  3090.  
  3091. d20plus.spells.formSpellUrl = function (fileName) {
  3092. return d20plus.formSrcUrl(SPELL_DATA_DIR, fileName);
  3093. };
  3094.  
  3095. d20plus.spells._groupOptions = ["Level", "Spell Points", "Alphabetical", "Source"];
  3096. d20plus.spells._listCols = ["name", "class", "level", "source"];
  3097. d20plus.spells._listItemBuilder = (it) => `
  3098. <span class="name col-4" title="name">${it.name}</span>
  3099. <span class="class col-3" title="class">${it.classes.fromClassList.map(c => `CLS[${c.name}]`).join(", ")}</span>
  3100. <span class="level col-3" title="level">LVL[${Parser.spLevelToFull(it.level)}]</span>
  3101. <span title="source (Full: ${Parser.sourceJsonToFull(it.source)})" class="source col-2">SRC[${Parser.sourceJsonToAbv(it.source)}]</span>`;
  3102. d20plus.spells._listIndexConverter = (sp) => {
  3103. return {
  3104. name: sp.name.toLowerCase(),
  3105. class: sp.classes.fromClassList.map(c => c.name.toLowerCase()),
  3106. level: Parser.spLevelToFull(sp.level).toLowerCase(),
  3107. source: Parser.sourceJsonToAbv(sp.source).toLowerCase()
  3108. };
  3109. };
  3110. // Import Spells button was clicked
  3111. d20plus.spells.button = function (forcePlayer) {
  3112. const playerMode = forcePlayer || !window.is_gm;
  3113. const url = playerMode ? $("#import-spell-url-player").val() : $("#import-spell-url").val();
  3114. if (url && url.trim()) {
  3115. const handoutBuilder = playerMode ? d20plus.spells.playerImportBuilder : d20plus.spells.handoutBuilder;
  3116.  
  3117. DataUtil.loadJSON(url, (data) => {
  3118. d20plus.importer.addMeta(data._meta);
  3119. d20plus.importer.showImportList(
  3120. "spell",
  3121. data.spell,
  3122. handoutBuilder,
  3123. {
  3124. groupOptions: d20plus.spells._groupOptions,
  3125. forcePlayer,
  3126. listItemBuilder: d20plus.spells._listItemBuilder,
  3127. listIndex: d20plus.spells._listCols,
  3128. listIndexConverter: d20plus.spells._listIndexConverter
  3129. }
  3130. );
  3131. });
  3132. }
  3133. };
  3134.  
  3135. // Import All Spells button was clicked
  3136. d20plus.spells.buttonAll = function (forcePlayer) {
  3137. const toLoad = Object.keys(spellDataUrls).filter(src => !isNonstandardSource(src)).map(src => d20plus.spells.formSpellUrl(spellDataUrls[src]));
  3138.  
  3139. if (toLoad.length) {
  3140. const handoutBuilder = !forcePlayer && window.is_gm ? d20plus.spells.handoutBuilder : d20plus.spells.playerImportBuilder;
  3141.  
  3142. DataUtil.multiLoadJSON(toLoad.map(url => ({url: url})), () => {
  3143. }, (dataStack) => {
  3144. let toAdd = [];
  3145. dataStack.forEach(d => toAdd = toAdd.concat(d.spell));
  3146. d20plus.importer.showImportList(
  3147. "spell",
  3148. toAdd,
  3149. handoutBuilder,
  3150. {
  3151. groupOptions: d20plus.spells._groupOptions,
  3152. forcePlayer,
  3153. listItemBuilder: d20plus.spells._listItemBuilder,
  3154. listIndex: d20plus.spells._listCols,
  3155. listIndexConverter: d20plus.spells._listIndexConverter
  3156. }
  3157. );
  3158. });
  3159. }
  3160. };
  3161.  
  3162. d20plus.importer.getTagString = function (data, prefix) {
  3163. return JSON.stringify(data.map(d => `${prefix}-${Parser.stringToSlug(d.toString())}`).concat([prefix]));
  3164. };
  3165.  
  3166. // Create spell handout from js data object
  3167. d20plus.spells.handoutBuilder = function (data, overwrite, inJournals, folderName, saveIdsTo, builderOptions) {
  3168. // make dir
  3169. const folder = d20plus.importer.makeDirTree(`Spells`, folderName);
  3170. const path = ["Spells", folderName, data.name];
  3171.  
  3172. // handle duplicates/overwrites
  3173. if (!d20plus.importer._checkHandleDuplicate(path, overwrite)) return;
  3174.  
  3175. const name = data.name;
  3176. // build spell handout
  3177. d20.Campaign.handouts.create({
  3178. name: name,
  3179. tags: d20plus.importer.getTagString([
  3180. Parser.spSchoolAbvToFull(data.school),
  3181. Parser.spLevelToFull(data.level),
  3182. ...data.classes.fromClassList.map(c => c.name),
  3183. Parser.sourceJsonToFull(data.source)
  3184. ], "spell")
  3185. }, {
  3186. success: function (handout) {
  3187. if (saveIdsTo) saveIdsTo[UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_SPELLS](data)] = {name: data.name, source: data.source, type: "handout", roll20Id: handout.id};
  3188.  
  3189. const [notecontents, gmnotes] = d20plus.spells._getHandoutData(data, builderOptions);
  3190.  
  3191. console.log(notecontents);
  3192. handout.updateBlobs({notes: notecontents, gmnotes: gmnotes});
  3193. handout.save({notes: (new Date).getTime(), inplayerjournals: inJournals});
  3194. d20.journal.addItemToFolderStructure(handout.id, folder.id);
  3195. }
  3196. });
  3197. };
  3198.  
  3199. d20plus.spells.playerImportBuilder = function (data) {
  3200. const [notecontents, gmnotes] = d20plus.spells._getHandoutData(data);
  3201.  
  3202. const importId = d20plus.generateRowId();
  3203. d20plus.importer.storePlayerImport(importId, JSON.parse(gmnotes));
  3204. d20plus.importer.makePlayerDraggable(importId, data.name);
  3205. };
  3206.  
  3207. d20plus.spells._getHandoutData = function (data, builderOptions) {
  3208. builderOptions = builderOptions || {};
  3209. // merge in roll20 metadata, if available
  3210. const spellMeta = spellMetaData.spell.find(sp => sp.name.toLowerCase() === data.name.toLowerCase() && sp.source.toLowerCase() === data.source.toLowerCase());
  3211. if (spellMeta) {
  3212. data.roll20 = spellMeta.data;
  3213. }
  3214.  
  3215. if (!data.school) data.school = "A";
  3216. if (!data.range) data.range = "Self";
  3217. if (!data.duration) data.duration = "Instantaneous";
  3218. if (!data.components) data.components = "";
  3219. if (!data.time) data.components = "1 action";
  3220.  
  3221. const r20Data = {};
  3222. if (data.roll20) Object.assign(r20Data, data.roll20);
  3223. Object.assign(
  3224. r20Data,
  3225. {
  3226. "Level": builderOptions.isSpellPoints ? String(Math.min(9, d20plus.spells.spLevelToSpellPoints(data.level))) : String(data.level),
  3227. "Range": Parser.spRangeToFull(data.range),
  3228. "School": Parser.spSchoolAbvToFull(data.school),
  3229. "Source": "5etoolsR20",
  3230. "Classes": d20plus.importer.getCleanText(Parser.spClassesToFull(data.classes)),
  3231. "Category": "Spells",
  3232. "Duration": Parser.spDurationToFull(data.duration),
  3233. "Material": "",
  3234. "Components": parseComponents(data.components),
  3235. "Casting Time": Parser.spTimeListToFull(data.time)
  3236. }
  3237. );
  3238.  
  3239. var r20json = {
  3240. name: data.name,
  3241. content: "",
  3242. htmlcontent: "",
  3243. data: r20Data
  3244. };
  3245. if (data.components.m && data.components.m.length) r20json.data["Material"] = data.components.m;
  3246. if (data.meta) {
  3247. if (data.meta.ritual) r20json.data["Ritual"] = "Yes";
  3248. }
  3249. if (data.duration.filter(d => d.concentration).length > 0) {
  3250. r20json.data["Concentration"] = "Yes";
  3251. }
  3252. var notecontents = "";
  3253. var gmnotes = "";
  3254. notecontents += `<p><h3>${data.name}</h3>
  3255. <em>${Parser.spLevelSchoolMetaToFull(data.level, data.school, data.meta)}${builderOptions.isSpellPoints && data.level ? ` (${d20plus.spells.spLevelToSpellPoints(data.level)} spell points)` : ""}</em></p><p>
  3256. <strong>Casting Time:</strong> ${Parser.spTimeListToFull(data.time)}<br>
  3257. <strong>Range:</strong> ${Parser.spRangeToFull(data.range)}<br>
  3258. <strong>Components:</strong> ${Parser.spComponentsToFull(data.components)}<br>
  3259. <strong>Duration:</strong> ${Parser.spDurationToFull(data.duration)}<br>
  3260. </p>`;
  3261. const renderer = new EntryRenderer();
  3262. const renderStack = [];
  3263. const entryList = {type: "entries", entries: data.entries};
  3264. renderer.setBaseUrl(BASE_SITE_URL);
  3265. renderer.recursiveEntryRender(entryList, renderStack, 1);
  3266. r20json.content = d20plus.importer.getCleanText(renderStack.join(" "));
  3267. notecontents += renderStack.join("");
  3268. if (data.entriesHigherLevel) {
  3269. const hLevelRenderStack = [];
  3270. const higherLevelsEntryList = {type: "entries", entries: data.entriesHigherLevel};
  3271. renderer.recursiveEntryRender(higherLevelsEntryList, hLevelRenderStack, 2);
  3272. r20json.content += "\n\nAt Higher Levels: " + d20plus.importer.getCleanText(hLevelRenderStack.join(" ").replace("At Higher Levels.", ""));
  3273. notecontents += hLevelRenderStack.join("");
  3274. }
  3275. notecontents += `<p><strong>Classes:</strong> ${Parser.spClassesToFull(data.classes)}</p>`;
  3276. gmnotes = JSON.stringify(r20json);
  3277. notecontents += `<del>${gmnotes}</del>`;
  3278.  
  3279. return [notecontents, gmnotes];
  3280. };
  3281.  
  3282. // parse spell components
  3283. function parseComponents (components) {
  3284. const out = [];
  3285. if (components.v) out.push("V");
  3286. if (components.s) out.push("S");
  3287. if (components.m) out.push("M");
  3288. return out.join(" ");
  3289. }
  3290.  
  3291. d20plus.items._groupOptions = ["Type", "Rarity", "Alphabetical", "Source"];
  3292. d20plus.items._listCols = ["name", "type", "rarity", "source"];
  3293. d20plus.items._listItemBuilder = (it) => {
  3294. if (!it._isEnhanced) EntryRenderer.item.enhanceItem(it);
  3295.  
  3296. return `
  3297. <span class="name col-3" title="name">${it.name}</span>
  3298. <span class="type col-5" title="type">${it.typeText.split(",").map(t => `TYP[${t.trim()}]`).join(", ")}</span>
  3299. <span class="rarity col-2" title="rarity">RAR[${it.rarity}]</span>
  3300. <span title="source (Full: ${Parser.sourceJsonToFull(it.source)})" class="source col-2">SRC[${Parser.sourceJsonToAbv(it.source)}]</span>`;
  3301. };
  3302. d20plus.items._listIndexConverter = (it) => {
  3303. if (!it._isEnhanced) EntryRenderer.item.enhanceItem(it);
  3304. return {
  3305. name: it.name.toLowerCase(),
  3306. type: it.typeText.toLowerCase().split(","),
  3307. rarity: it.rarity.toLowerCase(),
  3308. source: Parser.sourceJsonToAbv(it.source).toLowerCase()
  3309. };
  3310. };
  3311. // Import Items button was clicked
  3312. d20plus.items.button = function (forcePlayer) {
  3313. const playerMode = forcePlayer || !window.is_gm;
  3314. const url = playerMode ? $("#import-items-url-player").val() : $("#import-items-url").val();
  3315. if (url && url.trim()) {
  3316. const handoutBuilder = playerMode ? d20plus.items.playerImportBuilder : d20plus.items.handoutBuilder;
  3317.  
  3318. if (url.trim() === "https://5etools.com/data/items.json") {
  3319. EntryRenderer.item.buildList((itemList) => {
  3320. d20plus.importer.showImportList(
  3321. "item",
  3322. itemList,
  3323. handoutBuilder,
  3324. {
  3325. groupOptions: d20plus.items._groupOptions,
  3326. forcePlayer,
  3327. listItemBuilder: d20plus.items._listItemBuilder,
  3328. listIndex: d20plus.items._listCols,
  3329. listIndexConverter: d20plus.items._listIndexConverter
  3330. }
  3331. );
  3332. },
  3333. {
  3334. items: "https://5etools.com/data/items.json",
  3335. basicitems: "https://5etools.com/data/basicitems.json",
  3336. magicvariants: "https://5etools.com/data/magicvariants.json"
  3337. });
  3338. } else {
  3339. // for non-standard URLs, do a generic import
  3340. DataUtil.loadJSON(url, (data) => {
  3341. d20plus.importer.addMeta(data._meta);
  3342. d20plus.importer.showImportList(
  3343. "item",
  3344. data.item,
  3345. handoutBuilder,
  3346. {
  3347. groupOptions: d20plus.items._groupOptions,
  3348. forcePlayer,
  3349. listItemBuilder: d20plus.items._listItemBuilder,
  3350. listIndex: d20plus.items._listCols,
  3351. listIndexConverter: d20plus.items._listIndexConverter
  3352. }
  3353. );
  3354. });
  3355. }
  3356. }
  3357. };
  3358.  
  3359. // Import individual items
  3360. d20plus.items.handoutBuilder = function (data, overwrite, inJournals, folderName, saveIdsTo) {
  3361. // make dir
  3362. const folder = d20plus.importer.makeDirTree(`Items`, folderName);
  3363. const path = ["Items", folderName, data.name];
  3364.  
  3365. // handle duplicates/overwrites
  3366. if (!d20plus.importer._checkHandleDuplicate(path, overwrite)) return;
  3367.  
  3368. const name = data.name;
  3369.  
  3370. if (!data._isEnhanced) EntryRenderer.item.enhanceItem(data); // for homebrew items
  3371.  
  3372. // build item handout
  3373. d20.Campaign.handouts.create({
  3374. name: name,
  3375. tags: d20plus.importer.getTagString([
  3376. `rarity ${data.rarity}`,
  3377. ...data.procType,
  3378. Parser.sourceJsonToFull(data.source)
  3379. ], "items")
  3380. }, {
  3381. success: function (handout) {
  3382. if (saveIdsTo) saveIdsTo[UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ITEMS](data)] = {name: data.name, source: data.source, type: "handout", roll20Id: handout.id};
  3383.  
  3384. const [notecontents, gmnotes] = d20plus.items._getHandoutData(data);
  3385.  
  3386. handout.updateBlobs({notes: notecontents, gmnotes: gmnotes});
  3387. handout.save({
  3388. notes: (new Date).getTime(),
  3389. inplayerjournals: inJournals
  3390. });
  3391. d20.journal.addItemToFolderStructure(handout.id, folder.id);
  3392. }
  3393. });
  3394. };
  3395.  
  3396. d20plus.items.playerImportBuilder = function (data) {
  3397. const [notecontents, gmnotes] = d20plus.items._getHandoutData(data);
  3398.  
  3399. const importId = d20plus.generateRowId();
  3400. d20plus.importer.storePlayerImport(importId, JSON.parse(gmnotes));
  3401. d20plus.importer.makePlayerDraggable(importId, data.name);
  3402. };
  3403.  
  3404. d20plus.items._getHandoutData = function (data) {
  3405. var notecontents = "";
  3406. roll20Data = {
  3407. name: data.name,
  3408. data: {
  3409. Category: "Items"
  3410. }
  3411. };
  3412. const typeArray = [];
  3413. if (data.wondrous) typeArray.push("Wondrous Item");
  3414. if (data.technology) typeArray.push(data.technology);
  3415. if (data.age) typeArray.push(data.age);
  3416. if (data.weaponCategory) typeArray.push(data.weaponCategory + " Weapon");
  3417. var type = data.type;
  3418. if (data.type) {
  3419. const fullType = d20plus.items.parseType(data.type);
  3420. typeArray.push(fullType);
  3421. roll20Data.data["Item Type"] = fullType;
  3422. } else if (data.typeText) {
  3423. roll20Data.data["Item Type"] = data.typeText;
  3424. }
  3425. var typestring = typeArray.join(", ");
  3426. var damage = "";
  3427. if (data.dmg1 && data.dmgType) damage = data.dmg1 + " " + Parser.dmgTypeToFull(data.dmgType);
  3428. var armorclass = "";
  3429. if (type === "S") armorclass = "+" + data.ac;
  3430. if (type === "LA") armorclass = data.ac + " + Dex";
  3431. if (type === "MA") armorclass = data.ac + " + Dex (max 2)";
  3432. if (type === "HA") armorclass = data.ac;
  3433. var properties = "";
  3434. if (data.property) {
  3435. var propertieslist = data.property;
  3436. for (var i = 0; i < propertieslist.length; i++) {
  3437. var a = d20plus.items.parseProperty(propertieslist[i]);
  3438. var b = propertieslist[i];
  3439. if (b === "V") a = a + " (" + data.dmg2 + ")";
  3440. if (b === "T" || b === "A") a = a + " (" + data.range + "ft.)";
  3441. if (b === "RLD") a = a + " (" + data.reload + " shots)";
  3442. if (i > 0) a = ", " + a;
  3443. properties += a;
  3444. }
  3445. }
  3446. var reqAttune = data.reqAttune;
  3447. var attunementstring = "";
  3448. if (reqAttune) {
  3449. if (reqAttune === "YES") {
  3450. attunementstring = " (Requires Attunement)";
  3451. } else if (reqAttune === "OPTIONAL") {
  3452. attunementstring = " (Attunement Optional)";
  3453. } else {
  3454. reqAttune = " (Requires Attunement " + reqAttune + ")";
  3455. }
  3456. }
  3457. notecontents += `<p><h3>${data.name}</h3></p><em>${typestring}`;
  3458. if (data.tier) notecontents += ", " + data.tier;
  3459. var rarity = data.rarity;
  3460. var ismagicitem = (rarity !== "None" && rarity !== "Unknown");
  3461. if (ismagicitem) notecontents += ", " + rarity;
  3462. if (attunementstring) notecontents += attunementstring;
  3463. notecontents += `</em>`;
  3464. if (damage) notecontents += `<p><strong>Damage: </strong>${damage}</p>`;
  3465. if (properties) {
  3466. notecontents += `<p><strong>Properties: </strong>${properties}</p>`;
  3467. roll20Data.data.Properties = properties;
  3468. }
  3469. if (armorclass) {
  3470. notecontents += `<p><strong>Armor Class: </strong>${armorclass}</p>`;
  3471. roll20Data.data.AC = String(data.ac);
  3472. }
  3473. if (data.weight) {
  3474. notecontents += `<p><strong>Weight: </strong>${data.weight} lbs.</p>`;
  3475. roll20Data.data.Weight = String(data.weight);
  3476. }
  3477. var itemtext = data.entries ? data.entries : "";
  3478. const renderer = new EntryRenderer();
  3479. const renderStack = [];
  3480. const entryList = {type: "entries", entries: data.entries};
  3481. renderer.setBaseUrl(BASE_SITE_URL);
  3482. renderer.recursiveEntryRender(entryList, renderStack, 1);
  3483. var textstring = renderStack.join("");
  3484. if (textstring) {
  3485. notecontents += `<hr>`;
  3486. notecontents += textstring;
  3487. }
  3488.  
  3489. if (data.range) {
  3490. roll20Data.data.Range = data.range;
  3491. }
  3492. if (data.dmg1 && data.dmgType) {
  3493. roll20Data.data.Damage = data.dmg1;
  3494. roll20Data.data["Damage Type"] = Parser.dmgTypeToFull(data.dmgType);
  3495. }
  3496. if (textstring.trim()) {
  3497. roll20Data.content = d20plus.importer.getCleanText(textstring);
  3498. roll20Data.htmlcontent = roll20Data.content;
  3499. }
  3500. if (data.stealth) {
  3501. roll20Data.data.Stealth = "Disadvantage";
  3502. }
  3503. // roll20Data.data.Duration = "1 Minute"; // used by e.g. poison; not show in sheet
  3504. // roll20Data.data.Save = "Constitution"; // used by e.g. poison, ball bearings; not shown in sheet
  3505. // roll20Data.data.Target = "Each creature in a 10-foot square centered on a point within range"; // used by e.g. ball bearings; not shown in sheet
  3506. // roll20Data.data["Item Rarity"] = "Wondrous"; // used by Iron Bands of Binding... and nothing else?; not shown in sheet
  3507. if (data.reqAttune === "YES") {
  3508. roll20Data.data["Requires Attunement"] = "Yes";
  3509. } else {
  3510. roll20Data.data["Requires Attunement"] = "No";
  3511. }
  3512. // TODO handle other magic versions
  3513. // roll20Data.data.Modifiers = ... ; // this can be a variety of things, and is a comma separated list
  3514. // some examples, that are currently handled:
  3515. // "Ranged Attacks +3, Ranged Damage +3"
  3516. // "Ac +2"
  3517. // "Spell Attack +2"
  3518. // "Saving Throws +1"
  3519. // "AC +15, Spell Attack +2, Spell DC +2"
  3520. // ...and some examples, that are not:
  3521. // "Constitution +2"
  3522. // "Strength: 21"
  3523. if (data.modifier) {
  3524. const allModifiers = data.modifier.filter(m => m.__text).map(m => m.__text.split(" ").map(s => s.uppercaseFirst()).join(" ")).join(", ");
  3525. roll20Data.data.Modifiers = allModifiers;
  3526. }
  3527.  
  3528. const gmnotes = JSON.stringify(roll20Data);
  3529.  
  3530. return [notecontents, gmnotes];
  3531. };
  3532.  
  3533. d20plus.items.parseType = function (type) {
  3534. const result = Parser.itemTypeToAbv(type);
  3535. return result ? result : "n/a";
  3536. };
  3537.  
  3538. d20plus.items.parseDamageType = function (damagetype) {
  3539. const result = Parser.dmgTypeToFull(damagetype);
  3540. return result ? result : false;
  3541. };
  3542.  
  3543. d20plus.items.parseProperty = function (property) {
  3544. if (property === "A") return "ammunition";
  3545. if (property === "AF") return "ammunition";
  3546. if (property === "BF") return "burst fire";
  3547. if (property === "F") return "finesse";
  3548. if (property === "H") return "heavy";
  3549. if (property === "L") return "light";
  3550. if (property === "LD") return "loading";
  3551. if (property === "R") return "reach";
  3552. if (property === "RLD") return "reload";
  3553. if (property === "S") return "special";
  3554. if (property === "T") return "thrown";
  3555. if (property === "2H") return "two-handed";
  3556. if (property === "V") return "versatile";
  3557. return "n/a";
  3558. };
  3559.  
  3560. d20plus.psionics._groupOptions = ["Alphabetical", "Order", "Source"];
  3561. d20plus.psionics._listCols = ["name", "order", "source"];
  3562. d20plus.psionics._listItemBuilder = (it) => `
  3563. <span class="name col-6">${it.name}</span>
  3564. <span class="order col-4">ORD[${it.order || "None"}]</span>
  3565. <span title="${Parser.sourceJsonToFull(it.source)}" class="source col-2">SRC[${Parser.sourceJsonToAbv(it.source)}]</span>`;
  3566. d20plus.psionics._listIndexConverter = (p) => {
  3567. return {
  3568. name: p.name.toLowerCase(),
  3569. order: (p.order || "none").toLowerCase(),
  3570. source: Parser.sourceJsonToAbv(p.source).toLowerCase()
  3571. };
  3572. };
  3573. // Import Psionics button was clicked
  3574. d20plus.psionics.button = function (forcePlayer) {
  3575. const playerMode = forcePlayer || !window.is_gm;
  3576. const url = playerMode ? $("#import-psionics-url-player").val() : $("#import-psionics-url").val();
  3577. if (url && url.trim()) {
  3578. const handoutBuilder = playerMode ? d20plus.psionics.playerImportBuilder : d20plus.psionics.handoutBuilder;
  3579.  
  3580. DataUtil.loadJSON(url, (data) => {
  3581. d20plus.importer.addMeta(data._meta);
  3582. d20plus.importer.showImportList(
  3583. "psionic",
  3584. data.psionic,
  3585. handoutBuilder,
  3586. {
  3587. groupOptions: d20plus.psionics._groupOptions,
  3588. forcePlayer,
  3589. listItemBuilder: d20plus.psionics._listItemBuilder,
  3590. listIndex: d20plus.psionics._listCols,
  3591. listIndexConverter: d20plus.psionics._listIndexConverter
  3592. }
  3593. );
  3594. });
  3595. }
  3596. };
  3597.  
  3598. d20plus.psionics.handoutBuilder = function (data, overwrite, inJournals, folderName, saveIdsTo) {
  3599. // make dir
  3600. const folder = d20plus.importer.makeDirTree(`Psionics`, folderName);
  3601. const path = ["Psionics", folderName, data.name];
  3602.  
  3603. // handle duplicates/overwrites
  3604. if (!d20plus.importer._checkHandleDuplicate(path, overwrite)) return;
  3605.  
  3606. const name = data.name;
  3607. d20.Campaign.handouts.create({
  3608. name: name,
  3609. tags: d20plus.importer.getTagString([
  3610. Parser.psiTypeToFull(data.type),
  3611. data.order || "orderless",
  3612. Parser.sourceJsonToFull(data.source)
  3613. ], "psionics")
  3614. }, {
  3615. success: function (handout) {
  3616. if (saveIdsTo) saveIdsTo[UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_PSIONICS](data)] = {name: data.name, source: data.source, type: "handout", roll20Id: handout.id};
  3617.  
  3618. const [noteContents, gmNotes] = d20plus.psionics._getHandoutData(data);
  3619.  
  3620. handout.updateBlobs({notes: noteContents, gmnotes: gmNotes});
  3621. handout.save({notes: (new Date).getTime(), inplayerjournals: inJournals});
  3622. d20.journal.addItemToFolderStructure(handout.id, folder.id);
  3623. }
  3624. });
  3625. };
  3626.  
  3627. d20plus.psionics.playerImportBuilder = function (data) {
  3628. const [notecontents, gmnotes] = d20plus.psionics._getHandoutData(data);
  3629.  
  3630. const importId = d20plus.generateRowId();
  3631. d20plus.importer.storePlayerImport(importId, JSON.parse(gmnotes));
  3632. d20plus.importer.makePlayerDraggable(importId, data.name);
  3633. };
  3634.  
  3635. d20plus.psionics._getHandoutData = function (data) {
  3636. function renderTalent () {
  3637. const renderStack = [];
  3638. renderer.recursiveEntryRender(({entries: data.entries, type: "entries"}), renderStack);
  3639. return renderStack.join(" ");
  3640. }
  3641.  
  3642. const renderer = new EntryRenderer();
  3643. renderer.setBaseUrl(BASE_SITE_URL);
  3644. data.data = {
  3645. Category: "Psionics"
  3646. };
  3647. const gmNotes = JSON.stringify(data);
  3648.  
  3649. const baseNoteContents = `
  3650. <h3>${data.name}</h3>
  3651. <p><em>${data.type === "D" ? `${data.order} ${Parser.psiTypeToFull(data.type)}` : `${Parser.psiTypeToFull(data.type)}`}</em></p>
  3652. ${data.type === "D" ? `${EntryRenderer.psionic.getDisciplineText(data, renderer)}` : `${renderTalent()}`}
  3653. `;
  3654.  
  3655. const noteContents = `${baseNoteContents}<br><del>${gmNotes}</del>`;
  3656.  
  3657. return [noteContents, gmNotes];
  3658. };
  3659.  
  3660. // Import Races button was clicked
  3661. d20plus.races.button = function (forcePlayer) {
  3662. const playerMode = forcePlayer || !window.is_gm;
  3663. const url = playerMode ? $("#import-races-url-player").val() : $("#import-races-url").val();
  3664. if (url && url.trim()) {
  3665. const handoutBuilder = playerMode ? d20plus.races.playerImportBuilder : d20plus.races.handoutBuilder;
  3666.  
  3667. DataUtil.loadJSON(url, (data) => {
  3668. d20plus.importer.addMeta(data._meta);
  3669. d20plus.importer.showImportList(
  3670. "race",
  3671. EntryRenderer.race.mergeSubraces(data.race),
  3672. handoutBuilder,
  3673. {
  3674. forcePlayer
  3675. }
  3676. );
  3677. });
  3678. }
  3679. };
  3680.  
  3681. d20plus.races.handoutBuilder = function (data, overwrite, inJournals, folderName, saveIdsTo) {
  3682. // make dir
  3683. const folder = d20plus.importer.makeDirTree(`Races`, folderName);
  3684. const path = ["Races", folderName, data.name];
  3685.  
  3686. // handle duplicates/overwrites
  3687. if (!d20plus.importer._checkHandleDuplicate(path, overwrite)) return;
  3688.  
  3689. const name = data.name;
  3690. d20.Campaign.handouts.create({
  3691. name: name,
  3692. tags: d20plus.importer.getTagString([
  3693. Parser.sizeAbvToFull(data.size),
  3694. Parser.sourceJsonToFull(data.source)
  3695. ], "races")
  3696. }, {
  3697. success: function (handout) {
  3698. if (saveIdsTo) saveIdsTo[UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_RACES](data)] = {name: data.name, source: data.source, type: "handout", roll20Id: handout.id};
  3699.  
  3700. const [noteContents, gmNotes] = d20plus.races._getHandoutData(data);
  3701.  
  3702. handout.updateBlobs({notes: noteContents, gmnotes: gmNotes});
  3703. handout.save({notes: (new Date).getTime(), inplayerjournals: inJournals});
  3704. d20.journal.addItemToFolderStructure(handout.id, folder.id);
  3705. }
  3706. });
  3707. };
  3708.  
  3709. d20plus.races.playerImportBuilder = function (data) {
  3710. const [notecontents, gmnotes] = d20plus.races._getHandoutData(data);
  3711.  
  3712. const importId = d20plus.generateRowId();
  3713. d20plus.importer.storePlayerImport(importId, JSON.parse(gmnotes));
  3714. d20plus.importer.makePlayerDraggable(importId, data.name);
  3715. };
  3716.  
  3717. d20plus.races._getHandoutData = function (data) {
  3718. const renderer = new EntryRenderer();
  3719. renderer.setBaseUrl(BASE_SITE_URL);
  3720.  
  3721. // TODO
  3722. const renderStack = [];
  3723. const ability = utils_getAbilityData(data.ability);
  3724. renderStack.push(`
  3725. <h3>${data.name}</h3>
  3726. <p>
  3727. <strong>Ability Scores:</strong> ${ability.asText}<br>
  3728. <strong>Size:</strong> ${Parser.sizeAbvToFull(data.size)}<br>
  3729. <strong>Speed:</strong> ${Parser.getSpeedString(data)}<br>
  3730. </p>
  3731. `);
  3732. renderer.recursiveEntryRender({entries: data.entries}, renderStack, 1);
  3733. const rendered = renderStack.join("");
  3734.  
  3735. const r20json = {
  3736. "name": data.name,
  3737. "Vetoolscontent": data,
  3738. "data": {
  3739. "Category": "Races"
  3740. }
  3741. };
  3742. const gmNotes = JSON.stringify(r20json);
  3743. const noteContents = `${rendered}\n\n<del>${gmNotes}</del>`;
  3744.  
  3745. return [noteContents, gmNotes];
  3746. };
  3747.  
  3748. // Import Feats button was clicked
  3749. d20plus.feats.button = function (forcePlayer) {
  3750. const playerMode = forcePlayer || !window.is_gm;
  3751. const url = playerMode ? $("#import-feats-url-player").val() : $("#import-feats-url").val();
  3752. if (url && url.trim()) {
  3753. const handoutBuilder = playerMode ? d20plus.feats.playerImportBuilder : d20plus.feats.handoutBuilder;
  3754.  
  3755. DataUtil.loadJSON(url, (data) => {
  3756. d20plus.importer.addMeta(data._meta);
  3757. d20plus.importer.showImportList(
  3758. "feat",
  3759. data.feat,
  3760. handoutBuilder,
  3761. {
  3762. forcePlayer
  3763. }
  3764. );
  3765. });
  3766. }
  3767. };
  3768.  
  3769. d20plus.feats.handoutBuilder = function (data, overwrite, inJournals, folderName, saveIdsTo) {
  3770. // make dir
  3771. const folder = d20plus.importer.makeDirTree(`Feats`, folderName);
  3772. const path = ["Feats", folderName, data.name];
  3773.  
  3774. // handle duplicates/overwrites
  3775. if (!d20plus.importer._checkHandleDuplicate(path, overwrite)) return;
  3776.  
  3777. const name = data.name;
  3778. d20.Campaign.handouts.create({
  3779. name: name,
  3780. tags: d20plus.importer.getTagString([
  3781. Parser.sourceJsonToFull(data.source)
  3782. ], "feats")
  3783. }, {
  3784. success: function (handout) {
  3785. if (saveIdsTo) saveIdsTo[UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_FEATS](data)] = {name: data.name, source: data.source, type: "handout", roll20Id: handout.id};
  3786.  
  3787. const [noteContents, gmNotes] = d20plus.feats._getHandoutData(data);
  3788.  
  3789. handout.updateBlobs({notes: noteContents, gmnotes: gmNotes});
  3790. handout.save({notes: (new Date).getTime(), inplayerjournals: inJournals});
  3791. d20.journal.addItemToFolderStructure(handout.id, folder.id);
  3792. }
  3793. });
  3794. };
  3795.  
  3796. d20plus.feats.playerImportBuilder = function (data) {
  3797. const [notecontents, gmnotes] = d20plus.feats._getHandoutData(data);
  3798.  
  3799. const importId = d20plus.generateRowId();
  3800. d20plus.importer.storePlayerImport(importId, JSON.parse(gmnotes));
  3801. d20plus.importer.makePlayerDraggable(importId, data.name);
  3802. };
  3803.  
  3804. d20plus.feats._getHandoutData = function (data) {
  3805. const renderer = new EntryRenderer();
  3806. renderer.setBaseUrl(BASE_SITE_URL);
  3807. const prerequisite = EntryRenderer.feat.getPrerequisiteText(data.prerequisite);
  3808. EntryRenderer.feat.mergeAbilityIncrease(data);
  3809.  
  3810. const renderStack = [];
  3811. renderer.recursiveEntryRender({entries: data.entries}, renderStack, 2);
  3812. const rendered = renderStack.join("");
  3813.  
  3814. const r20json = {
  3815. "name": data.name,
  3816. "content": `${prerequisite ? `**Prerequisite**: ${prerequisite}\n\n` : ""}${$(rendered).text()}`,
  3817. "Vetoolscontent": d20plus.importer.getCleanText(rendered),
  3818. "htmlcontent": "",
  3819. "data": {
  3820. "Category": "Feats"
  3821. }
  3822. };
  3823. const gmNotes = JSON.stringify(r20json);
  3824.  
  3825. const baseNoteContents = `${prerequisite ? `<p><i>Prerequisite: ${prerequisite}.</i></p> ` : ""}${rendered}`;
  3826. const noteContents = `${baseNoteContents}<del>${gmNotes}</del>`;
  3827.  
  3828. return [noteContents, gmNotes];
  3829. };
  3830.  
  3831. // Import Object button was clicked
  3832. d20plus.objects.button = function () {
  3833. const url = $("#import-objects-url").val();
  3834. if (url && url.trim()) {
  3835. DataUtil.loadJSON(url, (data) => {
  3836. d20plus.importer.addMeta(data._meta);
  3837. d20plus.importer.showImportList(
  3838. "object",
  3839. data.object,
  3840. d20plus.objects.handoutBuilder
  3841. );
  3842. });
  3843. }
  3844. };
  3845.  
  3846. d20plus.objects.handoutBuilder = function (data, overwrite, inJournals, folderName, saveIdsTo) {
  3847. // make dir
  3848. const folder = d20plus.importer.makeDirTree(`Objects`, folderName);
  3849. const path = ["Objects", folderName, data.name];
  3850.  
  3851. // handle duplicates/overwrites
  3852. if (!d20plus.importer._checkHandleDuplicate(path, overwrite)) return;
  3853.  
  3854. const name = data.name;
  3855. d20.Campaign.characters.create(
  3856. {
  3857. name: name,
  3858. tags: d20plus.importer.getTagString([
  3859. Parser.sizeAbvToFull(data.size),
  3860. Parser.sourceJsonToFull(data.source)
  3861. ], "objects")
  3862. },
  3863. {
  3864. success: function (character) {
  3865. if (saveIdsTo) saveIdsTo[UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_OBJECTS](data)] = {name: data.name, source: data.source, type: "character", roll20Id: character.id};
  3866.  
  3867. try {
  3868. const avatar = data.tokenURL || `${IMG_URL}objects/${name}.png`;
  3869. character.size = data.size;
  3870. character.name = name;
  3871. character.senses = data.senses;
  3872. character.hp = data.hp;
  3873. $.ajax({
  3874. url: avatar,
  3875. type: 'HEAD',
  3876. error: function () {
  3877. d20plus.importer.getSetAvatarImage(character, `${IMG_URL}blank.png`);
  3878. },
  3879. success: function () {
  3880. d20plus.importer.getSetAvatarImage(character, avatar);
  3881. }
  3882. });
  3883. const ac = data.ac.match(/^\d+/);
  3884. const size = Parser.sizeAbvToFull(data.size);
  3885. character.attribs.create({name: "npc", current: 1});
  3886. character.attribs.create({name: "npc_toggle", current: 1});
  3887. character.attribs.create({name: "npc_options-flag", current: 0});
  3888. character.attribs.create({name: "wtype", current: d20plus.importer.getDesiredWhisperType()});
  3889. character.attribs.create({name: "rtype", current: d20plus.importer.getDesiredRollType()});
  3890. character.attribs.create({
  3891. name: "advantagetoggle",
  3892. current: d20plus.importer.getDesiredAdvantageToggle()
  3893. });
  3894. character.attribs.create({
  3895. name: "whispertoggle",
  3896. current: d20plus.importer.getDesiredWhisperToggle()
  3897. });
  3898. character.attribs.create({name: "dtype", current: d20plus.importer.getDesiredDamageType()});
  3899. character.attribs.create({name: "npc_name", current: name});
  3900. character.attribs.create({name: "npc_size", current: size});
  3901. character.attribs.create({name: "type", current: data.type});
  3902. character.attribs.create({name: "npc_type", current: `${size} ${data.type}`});
  3903. character.attribs.create({name: "npc_ac", current: ac != null ? ac[0] : ""});
  3904. character.attribs.create({name: "npc_actype", current: ""});
  3905. character.attribs.create({name: "npc_hpbase", current: data.hp});
  3906. character.attribs.create({name: "npc_hpformula", current: data.hp ? `${data.hp}d1` : ""});
  3907.  
  3908. character.attribs.create({name: "npc_immunities", current: data.immune ? data.immune : ""});
  3909. character.attribs.create({name: "damage_immunities", current: data.immune ? data.immune : ""});
  3910.  
  3911. //Should only be one entry for objects
  3912. if (data.entries != null) {
  3913. character.attribs.create({name: "repeating_npctrait_0_name", current: name});
  3914. character.attribs.create({name: "repeating_npctrait_0_desc", current: data.entries});
  3915. if (d20plus.getCfgVal("token", "tokenactions")) {
  3916. character.abilities.create({
  3917. name: "Information: " + name,
  3918. istokenaction: true,
  3919. action: d20plus.actionMacroTrait(0)
  3920. });
  3921. }
  3922. }
  3923.  
  3924. const renderer = new EntryRenderer();
  3925. renderer.setBaseUrl(BASE_SITE_URL);
  3926. if (data.actionEntries) {
  3927. data.actionEntries.forEach((e, i) => {
  3928. const renderStack = [];
  3929. renderer.recursiveEntryRender({entries: e.entries}, renderStack, 2);
  3930. const actionText = d20plus.importer.getCleanText(renderStack.join(""));
  3931. d20plus.importer.addAction(character, e.name, actionText, i);
  3932. });
  3933. }
  3934.  
  3935. character.view._updateSheetValues();
  3936. var dirty = [];
  3937. $.each(d20.journal.customSheets.attrDeps, function (i, v) {
  3938. dirty.push(i);
  3939. });
  3940. d20.journal.notifyWorkersOfAttrChanges(character.view.model.id, dirty, true);
  3941.  
  3942. if (data.entries) {
  3943. const bio = renderer.renderEntry({type: "entries", entries: data.entries});
  3944.  
  3945. setTimeout(() => {
  3946. const fluffAs = d20plus.getCfgVal("import", "importFluffAs") || d20plus.getCfgDefaultVal("import", "importFluffAs");
  3947. let k = fluffAs === "Bio"? "bio" : "gmnotes";
  3948. character.updateBlobs({
  3949. [k]: Markdown.parse(bio)
  3950. });
  3951. character.save({
  3952. [k]: (new Date).getTime()
  3953. });
  3954. }, 500);
  3955. }
  3956. } catch (e) {
  3957. d20plus.log(`Error loading [${name}]`);
  3958. d20plus.addImportError(name);
  3959. console.log(data);
  3960. console.log(e);
  3961. }
  3962. d20.journal.addItemToFolderStructure(character.id, folder.id);
  3963. }
  3964. });
  3965. };
  3966.  
  3967. // version from previous scripts. Might be useless now?
  3968. d20plus.importer.rollbaseOld = "@{wtype}&{template:npcaction} @{attack_display_flag} @{damage_flag} {{name=@{npc_name}}} {{rname=@{name}}} {{r1=[[1d20+(@{attack_tohit}+0)]]}} @{rtype}+(@{attack_tohit}+0)]]}} {{dmg1=[[@{attack_damage}+0]]}} {{dmg1type=@{attack_damagetype}}} {{dmg2=[[@{attack_damage2}+0]]}} {{dmg2type=@{attack_damagetype2}}} {{crit1=[[@{attack_crit}+0]]}} {{crit2=[[@{attack_crit2}+0]]}} {{description=@{description}}} @{charname_output}";
  3969. // from OGL sheet, Jan 2018
  3970. d20plus.importer.rollbase = "@{wtype}&{template:npcaction} {{attack=1}} @{damage_flag} @{npc_name_flag} {{rname=@{name}}} {{r1=[[@{d20}+(@{attack_tohit}+0)]]}} @{rtype}+(@{attack_tohit}+0)]]}} {{dmg1=[[@{attack_damage}+0]]}} {{dmg1type=@{attack_damagetype}}} {{dmg2=[[@{attack_damage2}+0]]}} {{dmg2type=@{attack_damagetype2}}} {{crit1=[[@{attack_crit}+0]]}} {{crit2=[[@{attack_crit2}+0]]}} {{description=@{show_desc}}} @{charname_output}";
  3971.  
  3972. d20plus.importer.getDesiredRollType = function () {
  3973. // rtype
  3974. const toggle = "@{advantagetoggle}";
  3975. const never = "{{normal=1}} {{r2=[[0d20";
  3976. const always = "{{always=1}} {{r2=[[@{d20}";
  3977. const query = "{{query=1}} ?{Advantage?|Normal Roll,&#123&#123normal=1&#125&#125 &#123&#123r2=[[0d20|Advantage,&#123&#123advantage=1&#125&#125 &#123&#123r2=[[@{d20}|Disadvantage,&#123&#123disadvantage=1&#125&#125 &#123&#123r2=[[@{d20}}";
  3978. const desired = d20plus.getCfgVal("import", "advantagemode");
  3979. if (desired) {
  3980. switch (desired) {
  3981. case "Toggle (Default Advantage)":
  3982. case "Toggle":
  3983. case "Toggle (Default Disadvantage)":
  3984. return toggle;
  3985. case "Always":
  3986. return always;
  3987. case "Query":
  3988. return query;
  3989. case "Never":
  3990. return never;
  3991. }
  3992. } else {
  3993. return toggle;
  3994. }
  3995. };
  3996.  
  3997. d20plus.importer.getDesiredAdvantageToggle = function () {
  3998. // advantagetoggle
  3999. const advantage = "{{query=1}} {{advantage=1}} {{r2=[[@{d20}";
  4000. const disadvantage = "{{query=1}} {{disadvantage=1}} {{r2=[[@{d20}";
  4001. const desired = d20plus.getCfgVal("import", "advantagemode");
  4002. const neither = "";
  4003. if (desired) {
  4004. switch (desired) {
  4005. case "Toggle (Default Advantage)":
  4006. return advantage;
  4007. case "Toggle (Default Disadvantage)":
  4008. return desired;
  4009. case "Toggle":
  4010. case "Always":
  4011. case "Query":
  4012. case "Never":
  4013. return neither;
  4014. }
  4015. } else {
  4016. return neither;
  4017. }
  4018. };
  4019.  
  4020. d20plus.importer.getDesiredWhisperType = function () {
  4021. // wtype
  4022. const toggle = "@{whispertoggle}";
  4023. const never = " ";
  4024. const always = "/w gm ";
  4025. const query = "?{Whisper?|Public Roll,|Whisper Roll,/w gm }";
  4026. const desired = d20plus.getCfgVal("import", "whispermode");
  4027. if (desired) {
  4028. switch (desired) {
  4029. case "Toggle (Default GM)":
  4030. case "Toggle (Default Public)":
  4031. return toggle;
  4032. case "Always":
  4033. return always;
  4034. case "Query":
  4035. return query;
  4036. case "Never":
  4037. return never;
  4038. }
  4039. } else {
  4040. return toggle;
  4041. }
  4042. };
  4043.  
  4044. d20plus.importer.getDesiredWhisperToggle = function () {
  4045. // whispertoggle
  4046. const gm = "/w gm ";
  4047. const pblic = " ";
  4048. const desired = d20plus.getCfgVal("import", "whispermode");
  4049. if (desired) {
  4050. switch (desired) {
  4051. case "Toggle (Default GM)":
  4052. return gm;
  4053. case "Toggle (Default Public)":
  4054. return pblic;
  4055. case "Always":
  4056. return "";
  4057. case "Query":
  4058. return "";
  4059. case "Never":
  4060. return "";
  4061. }
  4062. } else {
  4063. return gm;
  4064. }
  4065. };
  4066.  
  4067. d20plus.importer.getDesiredDamageType = function () {
  4068. // dtype
  4069. const on = "full";
  4070. const off = "pick";
  4071. const desired = d20plus.getCfgVal("import", "damagemode");
  4072. if (desired) {
  4073. switch (desired) {
  4074. case "Auto Roll":
  4075. return on;
  4076. case "Don't Auto Roll":
  4077. return off;
  4078. }
  4079. } else {
  4080. return on;
  4081. }
  4082. };
  4083.  
  4084. // Import Classes button was clicked
  4085. d20plus.classes.button = function (forcePlayer) {
  4086. const playerMode = forcePlayer || !window.is_gm;
  4087. const url = playerMode ? $("#import-classes-url-player").val() : $("#import-classes-url").val();
  4088. if (url && url.trim()) {
  4089. const handoutBuilder = playerMode ? d20plus.classes.playerImportBuilder : d20plus.classes.handoutBuilder;
  4090.  
  4091. DataUtil.loadJSON(url, (data) => {
  4092. d20plus.importer.addMeta(data._meta);
  4093. d20plus.importer.showImportList(
  4094. "class",
  4095. data.class,
  4096. handoutBuilder,
  4097. {
  4098. forcePlayer
  4099. }
  4100. );
  4101. });
  4102. }
  4103. };
  4104.  
  4105. d20plus.classes.handoutBuilder = function (data, overwrite, inJournals, folderName, saveIdsTo) {
  4106. // make dir
  4107. const folder = d20plus.importer.makeDirTree(`Classes`, folderName);
  4108. const path = ["Classes", folderName, data.name];
  4109.  
  4110. // handle duplicates/overwrites
  4111. if (!d20plus.importer._checkHandleDuplicate(path, overwrite)) return;
  4112.  
  4113. const name = data.name;
  4114. d20.Campaign.handouts.create({
  4115. name: name,
  4116. tags: d20plus.importer.getTagString([
  4117. Parser.sourceJsonToFull(data.source)
  4118. ], "classes")
  4119. }, {
  4120. success: function (handout) {
  4121. if (saveIdsTo) saveIdsTo[UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES](data)] = {name: data.name, source: data.source, type: "handout", roll20Id: handout.id};
  4122.  
  4123. const [noteContents, gmNotes] = d20plus.classes._getHandoutData(data);
  4124.  
  4125. handout.updateBlobs({notes: noteContents, gmnotes: gmNotes});
  4126. handout.save({notes: (new Date).getTime(), inplayerjournals: inJournals});
  4127. d20.journal.addItemToFolderStructure(handout.id, folder.id);
  4128. }
  4129. });
  4130.  
  4131. d20plus.classes._handleSubclasses(data, overwrite, inJournals, folderName);
  4132. };
  4133.  
  4134. d20plus.classes._handleSubclasses = function (data, overwrite, inJournals, outerFolderName, forcePlayer) {
  4135. const playerMode = forcePlayer || !window.is_gm;
  4136. // import subclasses
  4137. if (data.subclasses) {
  4138. const allSubclasses = (data.source && isNonstandardSource(data.source)) || !window.confirm(`${data.name} subclasses: import published only?`);
  4139.  
  4140. const gainFeatureArray = [];
  4141. outer: for (let i = 0; i < 20; i++) {
  4142. const lvlFeatureList = data.classFeatures[i];
  4143. for (let j = 0; j < lvlFeatureList.length; j++) {
  4144. const feature = lvlFeatureList[j];
  4145. if (feature.gainSubclassFeature) {
  4146. gainFeatureArray.push(true);
  4147. continue outer;
  4148. }
  4149. }
  4150. gainFeatureArray.push(false);
  4151. }
  4152.  
  4153. data.subclasses.forEach(sc => {
  4154. if (!allSubclasses && isNonstandardSource(sc.source)) return;
  4155.  
  4156. sc.class = data.name;
  4157. sc._gainAtLevels = gainFeatureArray;
  4158. if (playerMode) {
  4159. d20plus.subclasses.playerImportBuilder(sc);
  4160. } else {
  4161. const folderName = d20plus.importer._getHandoutPath("subclass", sc, "Class");
  4162. const path = [folderName];
  4163. if (outerFolderName) path.push(sc.source || data.source); // if it wasn't None, group by source
  4164. d20plus.subclasses.handoutBuilder(sc, overwrite, inJournals, path);
  4165. }
  4166. });
  4167. }
  4168. };
  4169.  
  4170. d20plus.classes.playerImportBuilder = function (data) {
  4171. const [notecontents, gmnotes] = d20plus.classes._getHandoutData(data);
  4172.  
  4173. const importId = d20plus.generateRowId();
  4174. d20plus.importer.storePlayerImport(importId, JSON.parse(gmnotes));
  4175. d20plus.importer.makePlayerDraggable(importId, data.name);
  4176.  
  4177. d20plus.classes._handleSubclasses(data, false, false, null, true);
  4178. };
  4179.  
  4180. d20plus.classes._getHandoutData = function (data) {
  4181. const renderer = new EntryRenderer();
  4182. renderer.setBaseUrl(BASE_SITE_URL);
  4183.  
  4184. const renderStack = [];
  4185. // make a copy of the data to modify
  4186. const curClass = JSON.parse(JSON.stringify(data));
  4187. // render the class text
  4188. for (let i = 0; i < 20; i++) {
  4189. const lvlFeatureList = curClass.classFeatures[i];
  4190. for (let j = 0; j < lvlFeatureList.length; j++) {
  4191. const feature = lvlFeatureList[j];
  4192. renderer.recursiveEntryRender(feature, renderStack);
  4193. }
  4194. }
  4195. const rendered = renderStack.join("");
  4196.  
  4197. const r20json = {
  4198. "name": data.name,
  4199. "Vetoolscontent": data,
  4200. "data": {
  4201. "Category": "Classes"
  4202. }
  4203. };
  4204. const gmNotes = JSON.stringify(r20json);
  4205. const noteContents = `${rendered}\n\n<del>${gmNotes}</del>`;
  4206.  
  4207. return [noteContents, gmNotes];
  4208. };
  4209.  
  4210. d20plus.subclasses._groupOptions = ["Class", "Alphabetical", "Source"];
  4211. d20plus.subclasses._listCols = ["name", "class", "source"];
  4212. d20plus.subclasses._listItemBuilder = (it) => `
  4213. <span class="name col-6">${it.name}</span>
  4214. <span class="class col-4">CLS[${it.class}]</span>
  4215. <span title="${Parser.sourceJsonToFull(it.source)}" class="source col-2">SRC[${Parser.sourceJsonToAbv(it.source)}]</span>`;
  4216. d20plus.subclasses._listIndexConverter = (sc) => {
  4217. return {
  4218. name: sc.name.toLowerCase(),
  4219. class: sc.class.toLowerCase(),
  4220. source: Parser.sourceJsonToAbv(sc.source).toLowerCase()
  4221. };
  4222. };
  4223. // Import Subclasses button was clicked
  4224. d20plus.subclasses.button = function (forcePlayer) {
  4225. const playerMode = forcePlayer || !window.is_gm;
  4226. const url = playerMode ? $("#import-subclasses-url-player").val() : $("#import-subclasses-url").val();
  4227. if (url && url.trim()) {
  4228. const handoutBuilder = playerMode ? d20plus.subclasses.playerImportBuilder : d20plus.subclasses.handoutBuilder;
  4229.  
  4230. DataUtil.loadJSON(url, (data) => {
  4231. d20plus.importer.addMeta(data._meta);
  4232. d20plus.importer.showImportList(
  4233. "subclass",
  4234. data.subclass,
  4235. handoutBuilder,
  4236. {
  4237. groupOptions: d20plus.subclasses._groupOptions,
  4238. forcePlayer,
  4239. listItemBuilder: d20plus.subclasses._listItemBuilder,
  4240. listIndex: d20plus.subclasses._listCols,
  4241. listIndexConverter: d20plus.subclasses._listIndexConverter
  4242. }
  4243. );
  4244. });
  4245. }
  4246. };
  4247.  
  4248. d20plus.subclasses.handoutBuilder = function (data, overwrite, inJournals, folderName, saveIdsTo) {
  4249. // make dir
  4250. const folder = d20plus.importer.makeDirTree(`Subclasses`, folderName);
  4251. const path = ["Sublasses", folderName, data.name];
  4252.  
  4253. // handle duplicates/overwrites
  4254. if (!d20plus.importer._checkHandleDuplicate(path, overwrite)) return;
  4255.  
  4256. const name = `${data.shortName} (${data.class})`;
  4257. d20.Campaign.handouts.create({
  4258. name: name,
  4259. tags: d20plus.importer.getTagString([
  4260. data.class,
  4261. Parser.sourceJsonToFull(data.source)
  4262. ], "subclasses")
  4263. }, {
  4264. success: function (handout) {
  4265. if (saveIdsTo) saveIdsTo[UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES](data)] = {name: data.name, source: data.source, type: "handout", roll20Id: handout.id};
  4266.  
  4267. const [noteContents, gmNotes] = d20plus.subclasses._getHandoutData(data);
  4268.  
  4269. handout.updateBlobs({notes: noteContents, gmnotes: gmNotes});
  4270. handout.save({notes: (new Date).getTime(), inplayerjournals: inJournals});
  4271. d20.journal.addItemToFolderStructure(handout.id, folder.id);
  4272. }
  4273. });
  4274. };
  4275.  
  4276. d20plus.subclasses.playerImportBuilder = function (data) {
  4277. const [notecontents, gmnotes] = d20plus.subclasses._getHandoutData(data);
  4278.  
  4279. const importId = d20plus.generateRowId();
  4280. d20plus.importer.storePlayerImport(importId, JSON.parse(gmnotes));
  4281. const name = `${data.class ? `${data.class} \u2014 ` : ""}${data.name}`;
  4282. d20plus.importer.makePlayerDraggable(importId, name);
  4283. };
  4284.  
  4285. d20plus.subclasses._getHandoutData = function (data) {
  4286. const renderer = new EntryRenderer();
  4287. renderer.setBaseUrl(BASE_SITE_URL);
  4288.  
  4289. const renderStack = [];
  4290.  
  4291. data.subclassFeatures.forEach(lvl => {
  4292. lvl.forEach(f => {
  4293. renderer.recursiveEntryRender(f, renderStack);
  4294. });
  4295. });
  4296.  
  4297. const rendered = renderStack.join("");
  4298.  
  4299. const r20json = {
  4300. "name": data.name,
  4301. "Vetoolscontent": data,
  4302. "data": {
  4303. "Category": "Subclasses"
  4304. }
  4305. };
  4306. const gmNotes = JSON.stringify(r20json);
  4307. const noteContents = `${rendered}\n\n<del>${gmNotes}</del>`;
  4308.  
  4309. return [noteContents, gmNotes];
  4310. };
  4311.  
  4312. d20plus.backgrounds.button = function (forcePlayer) {
  4313. const playerMode = forcePlayer || !window.is_gm;
  4314. const url = playerMode ? $("#import-backgrounds-url-player").val() : $("#import-backgrounds-url").val();
  4315. if (url && url.trim()) {
  4316. const handoutBuilder = playerMode ? d20plus.backgrounds.playerImportBuilder : d20plus.backgrounds.handoutBuilder;
  4317.  
  4318. DataUtil.loadJSON(url, (data) => {
  4319. d20plus.importer.addMeta(data._meta);
  4320. d20plus.importer.showImportList(
  4321. "background",
  4322. data.background,
  4323. handoutBuilder,
  4324. {
  4325. forcePlayer
  4326. }
  4327. );
  4328. });
  4329. }
  4330. };
  4331.  
  4332. d20plus.backgrounds.handoutBuilder = function (data, overwrite, inJournals, folderName, saveIdsTo) {
  4333. // make dir
  4334. const folder = d20plus.importer.makeDirTree(`Backgrounds`, folderName);
  4335. const path = ["Backgrounds", folderName, data.name];
  4336.  
  4337. // handle duplicates/overwrites
  4338. if (!d20plus.importer._checkHandleDuplicate(path, overwrite)) return;
  4339.  
  4340. const name = data.name;
  4341. d20.Campaign.handouts.create({
  4342. name: name,
  4343. tags: d20plus.importer.getTagString([
  4344. Parser.sourceJsonToFull(data.source)
  4345. ], "backgrounds")
  4346. }, {
  4347. success: function (handout) {
  4348. if (saveIdsTo) saveIdsTo[UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_BACKGROUNDS](data)] = {name: data.name, source: data.source, type: "handout", roll20Id: handout.id};
  4349.  
  4350. const [noteContents, gmNotes] = d20plus.backgrounds._getHandoutData(data);
  4351.  
  4352. handout.updateBlobs({notes: noteContents, gmnotes: gmNotes});
  4353. handout.save({notes: (new Date).getTime(), inplayerjournals: inJournals});
  4354. d20.journal.addItemToFolderStructure(handout.id, folder.id);
  4355. }
  4356. });
  4357. };
  4358.  
  4359. d20plus.backgrounds.playerImportBuilder = function (data) {
  4360. const [notecontents, gmnotes] = d20plus.backgrounds._getHandoutData(data);
  4361.  
  4362. const importId = d20plus.generateRowId();
  4363. d20plus.importer.storePlayerImport(importId, JSON.parse(gmnotes));
  4364. d20plus.importer.makePlayerDraggable(importId, data.name);
  4365. };
  4366.  
  4367. d20plus.backgrounds._getHandoutData = function (data) {
  4368. const renderer = new EntryRenderer();
  4369. renderer.setBaseUrl(BASE_SITE_URL);
  4370.  
  4371. const renderStack = [];
  4372.  
  4373. renderer.recursiveEntryRender({entries: data.entries}, renderStack, 1);
  4374.  
  4375. const rendered = renderStack.join("");
  4376.  
  4377. const r20json = {
  4378. "name": data.name,
  4379. "Vetoolscontent": data,
  4380. "data": {
  4381. "Category": "Backgrounds"
  4382. }
  4383. };
  4384. const gmNotes = JSON.stringify(r20json);
  4385. const noteContents = `${rendered}\n\n<del>${gmNotes}</del>`;
  4386.  
  4387. return [noteContents, gmNotes];
  4388. };
  4389.  
  4390. // Import Adventures button was clicked
  4391. d20plus.adventures.button = function () {
  4392. const url = $("#import-adventures-url").val();
  4393. if (url !== null) d20plus.adventures.load(url);
  4394. };
  4395.  
  4396. d20plus.importer.importModeSwitch = function () {
  4397. d20plus.importer.clearPlayerImport();
  4398. const $winPlayer = $(`#d20plus-playerimport`).find(`.append-list-journal`).empty();
  4399.  
  4400. $(`.importer-section`).hide();
  4401. const toShow = $(`#import-mode-select`).val();
  4402. $(`#betteR20-settings`).find(`.importer-section[data-import-group="${toShow}"]`).show();
  4403. const toShowPlayer = $(`#import-mode-select-player`).val();
  4404. $(`#d20plus-playerimport`).find(`.importer-section[data-import-group="${toShowPlayer}"]`).show();
  4405. };
  4406.  
  4407. d20plus.importer.showImportList = function (dataType, dataArray, handoutBuilder, options) {
  4408. if (!options) options = {};
  4409. /*
  4410. options = {
  4411. groupOptions: ["Source", "CR", "Alphabetical", "Type"],
  4412. forcePlayer: true,
  4413. callback: () => console.log("hello world"),
  4414. saveIdsTo: {}, // object to receive IDs of created handouts/creatures
  4415. // these three generally used together
  4416. listItemBuilder: (it) => `<span class="name col-8">${it.name}</span><span title="${Parser.sourceJsonToFull(it.source)}" class="source col-4">${it.cr ? `(CR ${it.cr.cr || it.cr}) ` : ""}(${Parser.sourceJsonToAbv(it.source)})</span>`,
  4417. listIndex: ["name", "source"],
  4418. listIndexConverter: (mon) => {
  4419. name: mon.name.toLowerCase(),
  4420. source: Parser.sourceJsonToAbv(m.source).toLowerCase() // everything is assumed to be lowercase
  4421. };
  4422. }
  4423. */
  4424. $("a.ui-tabs-anchor[href='#journal']").trigger("click");
  4425.  
  4426. if (!window.is_gm || options.forcePlayer) {
  4427. d20plus.importer.clearPlayerImport();
  4428. const $winPlayer = $(`#d20plus-playerimport`);
  4429. const $appPlayer = $winPlayer.find(`.append-list-journal`);
  4430. $appPlayer.empty();
  4431. $appPlayer.append(`<ol class="dd-list Vetools-player-imported" style="max-width: 95%;"/>`);
  4432. }
  4433.  
  4434. // sort data
  4435. dataArray.sort((a, b) => SortUtil.ascSort(a.name, b.name));
  4436.  
  4437. // collect available properties
  4438. const propSet = {}; // represent this as an object instead of a set, to maintain some semblance of ordering
  4439. dataArray.map(it => Object.keys(it)).forEach(keys => keys.forEach(k => propSet[k] = true));
  4440.  
  4441. // build checkbox list
  4442. const $list = $("#import-list .list");
  4443. $list.html("");
  4444. dataArray.forEach((it, i) => {
  4445. if (it.noDisplay) return;
  4446.  
  4447. const inner = options.listItemBuilder
  4448. ? options.listItemBuilder(it)
  4449. : `<span class="name col-10">${it.name}</span><span class="source" title="${Parser.sourceJsonToFull(it.source)}">${Parser.sourceJsonToAbv(it.source)}</span>`;
  4450.  
  4451. $list.append(`
  4452. <label class="import-cb-label" data-listid="${i}">
  4453. <input type="checkbox">
  4454. ${inner}
  4455. </label>
  4456. `);
  4457. });
  4458.  
  4459. // init list library
  4460. const importList = new List("import-list", {
  4461. valueNames: options.listIndex || ["name"]
  4462. });
  4463.  
  4464. // reset the UI and add handlers
  4465. $(`#import-list > .search`).val("");
  4466. importList.search("");
  4467. $("#import-options label").hide();
  4468. $("#import-overwrite").parent().show();
  4469. $("#import-showplayers").parent().show();
  4470. $("#organize-by").parent().show();
  4471. $("#d20plus-importlist").dialog("open");
  4472.  
  4473. $("#d20plus-importlist button").unbind("click");
  4474.  
  4475. $("#importlist-selectall").bind("click", () => {
  4476. d20plus.importer._importSelectAll(importList);
  4477. });
  4478. $("#importlist-deselectall").bind("click", () => {
  4479. d20plus.importer._importDeselectAll(importList);
  4480. });
  4481. $("#importlist-selectvis").bind("click", () => {
  4482. d20plus.importer._importSelectVisible(importList);
  4483. });
  4484. $("#importlist-deselectvis").bind("click", () => {
  4485. d20plus.importer._importDeselectVisible(importList);
  4486. });
  4487.  
  4488. $("#importlist-selectall-published").bind("click", () => {
  4489. d20plus.importer._importSelectPublished(importList);
  4490. });
  4491.  
  4492. if (options.listIndexConverter) {
  4493. const $iptFilter = $(`#import-list-filter`).show();
  4494. $(`#import-list-filter-help`).show();
  4495. $iptFilter.off("keydown").off("keyup");
  4496. d20plus.importer.addListFilter($iptFilter, dataArray, importList, options.listIndexConverter);
  4497. } else {
  4498. $(`#import-list-filter`).hide();
  4499. $(`#import-list-filter-help`).hide();
  4500. }
  4501.  
  4502. const excludedProps = new Set();
  4503. const $winProps = $("#d20plus-import-props");
  4504. $winProps.find(`button`).bind("click", () => {
  4505. excludedProps.clear();
  4506. $winProps.find(`.prop-row`).each((i, ele) => {
  4507. if (!$(ele).find(`input`).prop("checked")) excludedProps.add($(ele).find(`span`).text());
  4508. });
  4509. });
  4510. const $btnProps = $(`#save-import-props`);
  4511. $btnProps.bind("click", () => {
  4512. $winProps.dialog("close");
  4513. });
  4514. const $props = $winProps.find(`.select-props`);
  4515. $props.empty();
  4516. $(`#import-open-props`).bind("click", () => {
  4517. Object.keys(propSet).forEach(p => {
  4518. const req = REQUIRED_PROPS[dataType] && REQUIRED_PROPS[dataType].includes(p);
  4519. $props.append(`
  4520. <label style="display: block; ${req ? "color: red;" : ""}" class="prop-row">
  4521. <input type="checkbox" checked="true">
  4522. <span>${p}</span>
  4523. </label>
  4524. `)
  4525. });
  4526. $winProps.dialog("open");
  4527. });
  4528.  
  4529. const $selGroupBy = $(`#organize-by`);
  4530. $selGroupBy.html("");
  4531. options.groupOptions = (options.groupOptions || ["Alphabetical", "Source"]).concat(["None"]);
  4532. options.groupOptions.forEach(g => {
  4533. $selGroupBy.append(`<option value="${g}">${g}</option>`);
  4534. });
  4535.  
  4536. $("#d20plus-importlist button#importstart").bind("click", function () {
  4537. $("#d20plus-importlist").dialog("close");
  4538. const overwrite = $("#import-overwrite").prop("checked");
  4539. const inJournals = $("#import-showplayers").prop("checked") ? "all" : "";
  4540. const groupBy = $(`#organize-by`).val();
  4541.  
  4542. // build list of items to process
  4543. const importQueue = [];
  4544. importList.items.forEach((e) => {
  4545. if ($(e.elm).find("input").prop("checked")) {
  4546. const dataIndex = parseInt($(e.elm).data("listid"));
  4547. const it = dataArray[dataIndex];
  4548. importQueue.push(it);
  4549. }
  4550. });
  4551.  
  4552. const $stsName = $("#import-name");
  4553. const $stsRemain = $("#import-remaining");
  4554. let remaining = importQueue.length;
  4555. let interval;
  4556. if (dataType === "monster" || dataType === "object") {
  4557. interval = d20plus.getCfgVal("import", "importIntervalCharacter") || d20plus.getCfgDefaultVal("import", "importIntervalCharacter");
  4558. } else {
  4559. interval = d20plus.getCfgVal("import", "importIntervalHandout") || d20plus.getCfgDefaultVal("import", "importIntervalHandout");
  4560. }
  4561.  
  4562. let cancelWorker = false;
  4563. const $btnCancel = $(`#importcancel`);
  4564. $btnCancel.off("click");
  4565. $btnCancel.on("click", () => {
  4566. handleWorkerComplete();
  4567. cancelWorker = true;
  4568. });
  4569.  
  4570. // start worker to process list
  4571. $("#d20plus-import").dialog("open");
  4572.  
  4573. // run one immediately
  4574. let worker;
  4575. workerFn();
  4576. worker = setInterval(() => {
  4577. workerFn();
  4578. }, interval);
  4579.  
  4580. function workerFn () {
  4581. if (!importQueue.length) {
  4582. handleWorkerComplete();
  4583. return;
  4584. }
  4585. if (cancelWorker) {
  4586. return;
  4587. }
  4588.  
  4589. // pull items out the queue in LIFO order, for journal ordering (last created will be at the top)
  4590. let it = importQueue.pop();
  4591. it.name = it.name || "(Unknown)";
  4592.  
  4593. $stsName.text(it.name);
  4594. $stsRemain.text(remaining--);
  4595.  
  4596. if (excludedProps.size) {
  4597. it = JSON.parse(JSON.stringify(it));
  4598. [...excludedProps].forEach(k => delete it[k]);
  4599. }
  4600.  
  4601. if (!window.is_gm || options.forcePlayer) {
  4602. handoutBuilder(it);
  4603. } else {
  4604. const folderName = groupBy === "None" ? "" : d20plus.importer._getHandoutPath(dataType, it, groupBy);
  4605. const builderOptions = {};
  4606. if (dataType === "spell" && groupBy === "Spell Points") builderOptions.isSpellPoints = true;
  4607. handoutBuilder(it, overwrite, inJournals, folderName, options.saveIdsTo, builderOptions);
  4608. }
  4609. }
  4610.  
  4611. function handleWorkerComplete () {
  4612. if (worker) clearInterval(worker);
  4613. if (cancelWorker) {
  4614. $stsName.text("Import cancelled");
  4615. if (~$stsRemain.text().indexOf("(cancelled)")) $stsRemain.text(`${$stsRemain.text()} (cancelled)`);
  4616. d20plus.log(`Import cancelled`);
  4617. setTimeout(() => {
  4618. d20plus.bindDropLocations();
  4619. }, 250);
  4620. } else {
  4621. $stsName.text("Import complete");
  4622. $stsRemain.text("0");
  4623. d20plus.log(`Import complete`);
  4624. setTimeout(() => {
  4625. d20plus.bindDropLocations();
  4626. }, 250);
  4627. if (options.callback) options.callback();
  4628. }
  4629. }
  4630. });
  4631. };
  4632.  
  4633. d20plus.spells.spLevelToSpellPoints = function (level) {
  4634. switch (level) {
  4635. case 1:
  4636. return 2;
  4637. case 2:
  4638. return 3;
  4639. case 3:
  4640. return 5;
  4641. case 4:
  4642. return 6;
  4643. case 5:
  4644. return 7;
  4645. case 6:
  4646. return 8;
  4647. case 7:
  4648. return 10;
  4649. case 8:
  4650. return 11;
  4651. case 9:
  4652. return 13;
  4653. case 0:
  4654. default:
  4655. return 0;
  4656. }
  4657. };
  4658.  
  4659. d20plus.importer._getHandoutPath = function (dataType, it, groupBy) {
  4660. switch (dataType) {
  4661. case "monster": {
  4662. let folderName;
  4663. switch (groupBy) {
  4664. case "Source":
  4665. folderName = Parser.sourceJsonToFull(it.source);
  4666. break;
  4667. case "CR":
  4668. folderName = it.cr ? (it.cr.cr || it.cr) : "Unknown";
  4669. break;
  4670. case "Alphabetical":
  4671. folderName = it.name[0].uppercaseFirst();
  4672. break;
  4673. case "Type":
  4674. default:
  4675. folderName = Parser.monTypeToFullObj(it.type).type.uppercaseFirst();
  4676. break;
  4677. }
  4678. return folderName;
  4679. }
  4680. case "spell": {
  4681. let folderName;
  4682. switch (groupBy) {
  4683. case "Source":
  4684. folderName = Parser.sourceJsonToFull(it.source);
  4685. break;
  4686. case "Alphabetical":
  4687. folderName = it.name[0].uppercaseFirst();
  4688. break;
  4689. case "Spell Points":
  4690. folderName = `${d20plus.spells.spLevelToSpellPoints(it.level)} spell points`;
  4691. break;
  4692. case "Level":
  4693. default:
  4694. folderName = `${Parser.spLevelToFull(it.level)}${it.level ? " level" : ""}`;
  4695. break;
  4696. }
  4697. return folderName;
  4698. }
  4699. case "item": {
  4700. let folderName;
  4701. switch (groupBy) {
  4702. case "Source":
  4703. folderName = Parser.sourceJsonToFull(it.source);
  4704. break;
  4705. case "Rarity":
  4706. folderName = it.rarity;
  4707. break;
  4708. case "Alphabetical":
  4709. folderName = it.name[0].uppercaseFirst();
  4710. break;
  4711. case "Type":
  4712. default:
  4713. if (it.type) {
  4714. folderName = Parser.itemTypeToAbv(it.type);
  4715. } else if (it.typeText) {
  4716. folderName = it.typeText;
  4717. } else {
  4718. folderName = "Unknown";
  4719. }
  4720. break;
  4721. }
  4722. return folderName;
  4723. }
  4724. case "psionic": {
  4725. let folderName;
  4726. switch (groupBy) {
  4727. case "Source":
  4728. folderName = Parser.sourceJsonToFull(it.source);
  4729. break;
  4730. case "Order":
  4731. folderName = Parser.psiOrderToFull(it.order);
  4732. break;
  4733. case "Alphabetical":
  4734. default:
  4735. folderName = it.name[0].uppercaseFirst();
  4736. break;
  4737. }
  4738. return folderName;
  4739. }
  4740. case "feat": {
  4741. let folderName;
  4742. switch (groupBy) {
  4743. case "Source":
  4744. folderName = Parser.sourceJsonToFull(it.source);
  4745. break;
  4746. case "Alphabetical":
  4747. default:
  4748. folderName = it.name[0].uppercaseFirst();
  4749. break;
  4750. }
  4751. return folderName;
  4752. }
  4753. case "object": {
  4754. let folderName;
  4755. switch (groupBy) {
  4756. case "Source":
  4757. folderName = Parser.sourceJsonToFull(it.source);
  4758. break;
  4759. case "Alphabetical":
  4760. default:
  4761. folderName = it.name[0].uppercaseFirst();
  4762. break;
  4763. }
  4764. return folderName;
  4765. }
  4766. case "class": {
  4767. let folderName;
  4768. switch (groupBy) {
  4769. case "Source":
  4770. folderName = Parser.sourceJsonToFull(it.source);
  4771. break;
  4772. case "Alphabetical":
  4773. default:
  4774. folderName = it.name[0].uppercaseFirst();
  4775. break;
  4776. }
  4777. return folderName;
  4778. }
  4779. case "subclass": {
  4780. let folderName;
  4781. switch (groupBy) {
  4782. case "Source":
  4783. folderName = Parser.sourceJsonToFull(it.source);
  4784. break;
  4785. case "Alphabetical":
  4786. folderName = it.name[0].uppercaseFirst();
  4787. break;
  4788. case "Class":
  4789. default:
  4790. folderName = it.class;
  4791. }
  4792. return folderName;
  4793. }
  4794. case "background": {
  4795. let folderName;
  4796. switch (groupBy) {
  4797. case "Source":
  4798. folderName = Parser.sourceJsonToFull(it.source);
  4799. break;
  4800. case "Alphabetical":
  4801. default:
  4802. folderName = it.name[0].uppercaseFirst();
  4803. break;
  4804. }
  4805. return folderName;
  4806. }
  4807. case "race": {
  4808. let folderName;
  4809. switch (groupBy) {
  4810. case "Source":
  4811. folderName = Parser.sourceJsonToFull(it.source);
  4812. break;
  4813. case "Alphabetical":
  4814. default:
  4815. folderName = it.name[0].uppercaseFirst();
  4816. break;
  4817. }
  4818. return folderName;
  4819. }
  4820. default:
  4821. throw new Error(`Unknown import type '${dataType}'`);
  4822. }
  4823. };
  4824.  
  4825. d20plus.importer._checkHandleDuplicate = function (path, overwrite) {
  4826. const dupe = d20plus.importer.checkFileExistsByPath(path);
  4827. if (dupe && !overwrite) return false;
  4828. else if (dupe) d20plus.importer.removeFileByPath(path);
  4829. return true;
  4830. };
  4831.  
  4832. d20plus.importer._importToggleSelectAll = function (importList, selectAllCb) {
  4833. const $sa = $(selectAllCb);
  4834. importList.items.forEach(i => Array.prototype.forEach.call(i.elm.children, (e) => {
  4835. if (e.tagName === "INPUT") {
  4836. $(e).prop("checked", $sa.prop("checked"));
  4837. }
  4838. }));
  4839. };
  4840.  
  4841. d20plus.importer._importSelectAll = function (importList) {
  4842. importList.items.forEach(i => Array.prototype.forEach.call(i.elm.children, (e) => {
  4843. if (e.tagName === "INPUT") {
  4844. $(e).prop("checked", true);
  4845. }
  4846. }));
  4847. };
  4848.  
  4849. d20plus.importer._importSelectVisible = function (importList) {
  4850. importList.visibleItems.forEach(i => Array.prototype.forEach.call(i.elm.children, (e) => {
  4851. if (e.tagName === "INPUT") {
  4852. $(e).prop("checked", true);
  4853. }
  4854. }));
  4855. };
  4856.  
  4857. d20plus.importer._importDeselectAll = function (importList) {
  4858. importList.items.forEach(i => Array.prototype.forEach.call(i.elm.children, (e) => {
  4859. if (e.tagName === "INPUT") {
  4860. $(e).prop("checked", false);
  4861. }
  4862. }));
  4863. };
  4864.  
  4865. d20plus.importer._importDeselectVisible = function (importList) {
  4866. importList.visibleItems.forEach(i => Array.prototype.forEach.call(i.elm.children, (e) => {
  4867. if (e.tagName === "INPUT") {
  4868. $(e).prop("checked", false);
  4869. }
  4870. }));
  4871. };
  4872.  
  4873. d20plus.importer._importSelectPublished = function (importList) {
  4874. function setSelection (i, setTo) {
  4875. Array.prototype.forEach.call(i.elm.children, (e) => {
  4876. if (e.tagName === "INPUT") {
  4877. $(e).prop("checked", setTo);
  4878. }
  4879. })
  4880. }
  4881.  
  4882. importList.items.forEach(i => {
  4883. if (isNonstandardSource(i.values().source)) {
  4884. setSelection(i, false);
  4885. } else {
  4886. setSelection(i, true);
  4887. }
  4888.  
  4889. });
  4890. };
  4891.  
  4892. // Fetch adventure data from file
  4893. d20plus.adventures.load = function (url) {
  4894. $("a.ui-tabs-anchor[href='#journal']").trigger("click");
  4895. $.ajax({
  4896. type: "GET",
  4897. url: url,
  4898. dataType: "text",
  4899. success: function (data) {
  4900. data = JSON.parse(data);
  4901.  
  4902. function isPart (e) {
  4903. return typeof e === "string" || typeof e === "object" && (e.type !== "entries");
  4904. }
  4905.  
  4906. // open progress window
  4907. $("#d20plus-import").dialog("open");
  4908. $("#import-remaining").text("Initialising...");
  4909.  
  4910. // get metadata
  4911. const adMeta = adventureMetadata.adventure.find(a => a.id.toLowerCase() === $("#import-adventures-url").data("id").toLowerCase())
  4912.  
  4913. const addQueue = [];
  4914. const sections = JSON.parse(JSON.stringify(data.data));
  4915. const adDir = `${Parser.sourceJsonToFull(adMeta.id)}`;
  4916. sections.forEach((s, i) => {
  4917. if (i >= adMeta.contents.length) return;
  4918.  
  4919. const chapterDir = [adDir, adMeta.contents[i].name];
  4920.  
  4921. const introEntries = [];
  4922. if (s.entries && s.entries.length && isPart(s.entries[0])) {
  4923. while (isPart(s.entries[0])) {
  4924. introEntries.push(s.entries[0]);
  4925. s.entries.shift();
  4926. }
  4927. }
  4928. addQueue.push({
  4929. dir: chapterDir,
  4930. type: "entries",
  4931. name: s.name,
  4932. entries: introEntries,
  4933. });
  4934.  
  4935. // compact entries into layers
  4936. front = null;
  4937. let tempStack = [];
  4938. let textIndex = 1;
  4939. while ((front = s.entries.shift())) {
  4940. if (isPart(front)) {
  4941. tempStack.push(front);
  4942. } else {
  4943. if (tempStack.length) {
  4944. addQueue.push({
  4945. dir: chapterDir,
  4946. type: "entries",
  4947. name: `Text ${textIndex++}`,
  4948. entries: tempStack
  4949. });
  4950. tempStack = [];
  4951. }
  4952. front.dir = chapterDir;
  4953. addQueue.push(front);
  4954. }
  4955. }
  4956. });
  4957.  
  4958. const renderer = new EntryRenderer();
  4959. renderer.setBaseUrl(BASE_SITE_URL);
  4960.  
  4961. const $stsName = $("#import-name");
  4962. const $stsRemain = $("#import-remaining");
  4963. const interval = d20plus.getCfgVal("import", "importIntervalHandout") || d20plus.getCfgDefaultVal("import", "importIntervalHandout");
  4964.  
  4965. ////////////////////////////////////////////////////////////////////////////////////////////////////////
  4966. EntryRenderer.getDefaultRenderer().setBaseUrl(BASE_SITE_URL);
  4967. // pre-import tags
  4968. const tags = {};
  4969. renderer.doExportTags(tags);
  4970. addQueue.forEach(entry => {
  4971. renderer.recursiveEntryRender(entry, []);
  4972. });
  4973.  
  4974. // storage for returned handout/character IDs
  4975. const RETURNED_IDS = {};
  4976.  
  4977. // monsters
  4978. const preMonsters = Object.keys(tags)
  4979. .filter(k => tags[k].page === "bestiary.html")
  4980. .map(k => tags[k]);
  4981. if (confirm("Import creatures from this adventure?")) doPreImport(preMonsters, showMonsterImport);
  4982. else doItemImport();
  4983.  
  4984. function showMonsterImport (toImport) {
  4985. d20plus.log(`Displaying monster import list for [${adMeta.name}]`);
  4986. d20plus.importer.showImportList(
  4987. "monster",
  4988. toImport.filter(it => it),
  4989. d20plus.monsters.handoutBuilder,
  4990. {
  4991. groupOptions: d20plus.monsters._groupOptions,
  4992. saveIdsTo: RETURNED_IDS,
  4993. callback: doItemImport,
  4994. listItemBuilder: d20plus.monsters._listItemBuilder,
  4995. listIndex: d20plus.monsters._listCols,
  4996. listIndexConverter: d20plus.monsters._listIndexConverter
  4997. }
  4998. );
  4999. }
  5000.  
  5001. // items
  5002. function doItemImport () {
  5003. const preItems = Object.keys(tags)
  5004. .filter(k => tags[k].page === "items.html")
  5005. .map(k => tags[k]);
  5006. if (confirm("Import items from this adventure?")) doPreImport(preItems, showItemImport);
  5007. else doMainImport();
  5008. }
  5009.  
  5010. function showItemImport (toImport) {
  5011. d20plus.log(`Displaying item import list for [${adMeta.name}]`);
  5012. d20plus.importer.showImportList(
  5013. "item",
  5014. toImport.filter(it => it),
  5015. d20plus.items.handoutBuilder,
  5016. {
  5017. groupOptions: d20plus.items._groupOptions,
  5018. saveIdsTo: RETURNED_IDS,
  5019. callback: doMainImport,
  5020. listItemBuilder: d20plus.items._listItemBuilder,
  5021. listIndex: d20plus.items._listCols,
  5022. listIndexConverter: d20plus.items._listIndexConverter
  5023. }
  5024. );
  5025. }
  5026.  
  5027. function doPreImport (asTags, callback) {
  5028. const tmp = [];
  5029. let cachedCount = asTags.length;
  5030. asTags.forEach(it => {
  5031. try {
  5032. EntryRenderer.hover._doFillThenCall(
  5033. it.page,
  5034. it.source,
  5035. it.hash,
  5036. () => {
  5037. tmp.push(EntryRenderer.hover._getFromCache(it.page, it.source, it.hash));
  5038. cachedCount--;
  5039. if (cachedCount <= 0) callback(tmp);
  5040. }
  5041. );
  5042. } catch (x) {
  5043. console.log(x);
  5044. cachedCount--;
  5045. if (cachedCount <= 0) callback(tmp);
  5046. }
  5047. });
  5048. }
  5049. ////////////////////////////////////////////////////////////////////////////////////////////////////////
  5050. function doMainImport () {
  5051. // pass in any created handouts/characters to use for links in the renderer
  5052. renderer.setRoll20Ids(RETURNED_IDS);
  5053.  
  5054. let cancelWorker = false;
  5055. const $btnCancel = $(`#importcancel`);
  5056. $btnCancel.off("click");
  5057. $btnCancel.on("click", () => {
  5058. cancelWorker = true;
  5059. });
  5060.  
  5061. let remaining = addQueue.length;
  5062.  
  5063. d20plus.log(`Running import of [${adMeta.name}] with ${interval} ms delay between each handout create`);
  5064. let lastId = null;
  5065. let lastName = null;
  5066.  
  5067. const worker = setInterval(() => {
  5068. if (!addQueue.length || cancelWorker) {
  5069. clearInterval(worker);
  5070. $stsName.text("DONE!");
  5071. $stsRemain.text("0");
  5072. d20plus.log(`Finished import of [${adMeta.name}]`);
  5073. renderer.resetRoll20Ids();
  5074. return;
  5075. }
  5076.  
  5077. // pull items out the queue in LIFO order, for journal ordering (last created will be at the top)
  5078. const entry = addQueue.pop();
  5079. entry.name = entry.name || "(Unknown)";
  5080. entry.name = d20plus.importer.getCleanText(renderer.renderEntry(entry.name));
  5081. $stsName.text(entry.name);
  5082. $stsRemain.text(remaining--);
  5083. const folder = d20plus.importer.makeDirTree(entry.dir);
  5084.  
  5085. d20.Campaign.handouts.create({
  5086. name: entry.name
  5087. }, {
  5088. success: function (handout) {
  5089. const renderStack = [];
  5090. renderer.recursiveEntryRender(entry, renderStack);
  5091. if (lastId && lastName) renderStack.push(`<br><p>Next handout: <a href="http://journal.roll20.net/handout/${lastId}">${lastName}</a></p>`);
  5092. const rendered = renderStack.join("");
  5093.  
  5094. lastId = handout.id;
  5095. lastName = entry.name;
  5096. handout.updateBlobs({notes: rendered});
  5097. handout.save({notes: (new Date).getTime(), inplayerjournals: ""});
  5098. d20.journal.addItemToFolderStructure(handout.id, folder.id);
  5099. }
  5100. });
  5101. }, interval);
  5102. }
  5103. }
  5104. });
  5105. };
  5106.  
  5107. d20plus.importer.getCleanText = function (str) {
  5108. const check = jQuery.parseHTML(str);
  5109. if (check.length === 1 && check[0].constructor === Text) {
  5110. return str;
  5111. }
  5112. const $ele = $(str);
  5113. $ele.find("p, li, br").append("\n\n");
  5114. return $ele.text().replace(/[ ]+/g, " ");
  5115.  
  5116. /* version which preserves images, and converts dice
  5117. const IMG_TAG = "R20IMGTAG";
  5118. let imgIndex = 0;
  5119. const imgStack = [];
  5120. str.replace(/(<img.*>)/, (match) => {
  5121. imgStack.push(match);
  5122. return ` ${IMG_TAG}_${imgIndex++} `;
  5123. });
  5124. const $ele = $(str);
  5125. $ele.find("p, li, br").append("\n\n");
  5126. let out = $ele.text();
  5127. out = out.replace(DICE_REGEX, (match) => {
  5128. return `[[${match}]]`;
  5129. });
  5130. return out.replace(/R20IMGTAG_(\d+)/, (match, g1) => {
  5131. return imgStack[Number(g1)];
  5132. });
  5133. */
  5134. };
  5135.  
  5136. d20plus.miniInitStyle = `
  5137. #initiativewindow button.initmacrobutton {
  5138. padding: 1px 4px;
  5139. }
  5140.  
  5141. #initiativewindow input {
  5142. font-size: 8px;
  5143. }
  5144.  
  5145. #initiativewindow ul li span.name {
  5146. font-size: 13px;
  5147. padding-top: 0;
  5148. padding-left: 4px;
  5149. margin-top: -3px;
  5150. }
  5151.  
  5152. #initiativewindow ul li img {
  5153. min-height: 15px;
  5154. max-height: 15px;
  5155. }
  5156.  
  5157. #initiativewindow ul li {
  5158. min-height: 15px;
  5159. }
  5160.  
  5161. #initiativewindow div.header span.initiative,
  5162. #initiativewindow ul li span.initiative,
  5163. #initiativewindow ul li span.tracker-col,
  5164. #initiativewindow div.header span.tracker-col,
  5165. #initiativewindow div.header span.initmacro,
  5166. #initiativewindow ul li span.initmacro {
  5167. font-size: 10px;
  5168. font-weight: bold;
  5169. text-align: right;
  5170. float: right;
  5171. padding: 0 5px;
  5172. width: 7%;
  5173. min-height: 20px;
  5174. display: block;
  5175. overflow: hidden;
  5176. }
  5177.  
  5178. #initiativewindow ul li .controls {
  5179. padding: 0 3px;
  5180. }
  5181. `;
  5182.  
  5183. d20plus.setInitiativeShrink = function (doShrink) {
  5184. const customStyle = $(`#dynamicStyle`);
  5185. if (doShrink) {
  5186. customStyle.html(d20plus.miniInitStyle);
  5187. } else {
  5188. customStyle.html("");
  5189. }
  5190. };
  5191.  
  5192. d20plus.difficultyHtml = `<span class="difficulty" style="position: absolute"></span>`;
  5193.  
  5194. d20plus.multipliers = [1, 1.5, 2, 2.5, 3, 4, 5];
  5195.  
  5196. d20plus.playerImportHtml = `<div id="d20plus-playerimport" title="Temporary Import">
  5197. <div class="append-target">
  5198. <!-- populate with js -->
  5199. </div>
  5200. <div class="append-list-journal" style="max-height: 400px; overflow-y: auto;">
  5201. <!-- populate with js -->
  5202. </div>
  5203. <p><i>Player-imported items are temporary, as players can't make handouts. GMs may also use this functionality to avoid cluttering the journal. Once imported, items can be drag-dropped to character sheets.</i></p>
  5204. </div>`;
  5205.  
  5206. d20plus.importListHTML = `<div id="d20plus-importlist" title="Import..." style="width: 1000px;">
  5207. <p style="display: flex">
  5208. <button type="button" id="importlist-selectall" class="btn" style="margin: 0 2px;"><span>Select All</span></button>
  5209. <button type="button" id="importlist-deselectall" class="btn" style="margin: 0 2px;"><span>Deselect All</span></button>
  5210. <button type="button" id="importlist-selectvis" class="btn" style="margin: 0 2px;"><span>Select Visible</span></button>
  5211. <button type="button" id="importlist-deselectvis" class="btn" style="margin: 0 2px;"><span>Deselect Visible</span></button>
  5212. <span style="width:1px;background: #bbb;height: 26px;margin: 2px;"></span>
  5213. <button type="button" id="importlist-selectall-published" class="btn" style="margin: 0 2px;"><span>Select All Published</span></button>
  5214. </p>
  5215. <p>
  5216. <span id="import-list">
  5217. <input class="search" autocomplete="off" placeholder="Search list...">
  5218. <input type="search" id="import-list-filter" class="filter" placeholder="Filter...">
  5219. <span id ="import-list-filter-help" title="Filter format example: 'cr:1/4; cr:1/2; type:beast; source:MM' -- hover over the columns to see the filterable name." style="cursor: help;">[?]</span>
  5220. <br>
  5221. <span class="list" style="max-height: 400px; overflow-y: scroll; overflow-x: hidden; display: block; margin-top: 1em; transform: translateZ(0);"></span>
  5222. </span>
  5223. </p>
  5224. <p id="import-options">
  5225. <label style="display: inline-block">Group Handouts By... <select id="organize-by"></select></label>
  5226. <button type="button" id="import-open-props" class="btn" role="button" aria-disabled="false" style="padding: 3px; display: inline-block;">Select Properties</button>
  5227. <label>Make handouts visible to all players? <input type="checkbox" title="Make items visible to all players" id="import-showplayers" checked></label>
  5228. <label>Overwrite existing? <input type="checkbox" title="Overwrite existing" id="import-overwrite"></label>
  5229. </p>
  5230. <button type="button" id="importstart" class="btn" role="button" aria-disabled="false">
  5231. <span>Start Import</span>
  5232. </button>
  5233. </div>`;
  5234.  
  5235. d20plus.importListPropsHTML = `<div id="d20plus-import-props" title="Choose Properties to Import">
  5236. <div class="select-props" style="max-height: 400px; overflow-y: scroll; transform: translateZ(0)">
  5237. <!-- populate with JS -->
  5238. </div>
  5239. <p>
  5240. Warning: this feature is highly experimental, and disabling <span style="color: red;">properties which are assumed to always exist</span> is not recommended.
  5241. <br>
  5242. <button type="button" id="save-import-props" class="btn" role="button" aria-disabled="false">Save</button>
  5243. </p>
  5244. </div>`;
  5245.  
  5246. d20plus.importDialogHtml = `<div id="d20plus-import" title="Importing...">
  5247. <p>
  5248. <h3 id="import-name"></h3>
  5249. </p>
  5250. <span id="import-remaining"></span> remaining
  5251. <p>
  5252. Errors: <span id="import-errors">0</span>
  5253. </p>
  5254. <p>
  5255. <button type="button" id="importcancel" alt="Cancel" title="Cancel Import" class="btn" role="button" aria-disabled="false">
  5256. <span>Cancel</span>
  5257. </button>
  5258. </p>
  5259. </div>`;
  5260.  
  5261. d20plus.settingsHtmlImportHeader = `
  5262. <h4>Import By Category</h4>
  5263. <p><small><i>We strongly recommend the OGL sheet for importing. You can switch afterwards.</i></small></p>
  5264. `;
  5265. d20plus.settingsHtmlSelector = `
  5266. <select id="import-mode-select">
  5267. <option value="none" disabled selected>Select category...</option>
  5268. <option value="monster">Monsters</option>
  5269. <option value="spell">Spells</option>
  5270. <option value="item">Items</option>
  5271. <option value="psionic">Psionics</option>
  5272. <option value="race">Races</option>
  5273. <option value="feat">Feats</option>
  5274. <option value="object">Objects</option>
  5275. <option value="class">Classes</option>
  5276. <option value="subclass">Subclasses</option>
  5277. <option value="background">Backgrounds</option>
  5278. <option value="adventure">Adventures</option>
  5279. </select>
  5280. `;
  5281. d20plus.settingsHtmlSelectorPlayer = `
  5282. <select id="import-mode-select-player">
  5283. <option value="none" disabled selected>Select category...</option>
  5284. <option value="spell">Spells</option>
  5285. <option value="item">Items</option>
  5286. <option value="psionic">Psionics</option>
  5287. <option value="race">Races</option>
  5288. <option value="feat">Feats</option>
  5289. <option value="class">Classes</option>
  5290. <option value="subclass">Subclasses</option>
  5291. <option value="background">Backgrounds</option>
  5292. </select>
  5293. `;
  5294. d20plus.settingsHtmlPtMonsters = `
  5295. <div class="importer-section" data-import-group="monster">
  5296. <h4>Monster Importing</h4>
  5297. <label for="import-monster-url">Monster Data URL:</label>
  5298. <select id="button-monsters-select">
  5299. <!-- populate with JS-->
  5300. </select>
  5301. <input type="text" id="import-monster-url">
  5302. <p><a class="btn" href="#" id="button-monsters-load">Import Monsters</a></p>
  5303. <p><a class="btn" href="#" id="button-monsters-load-all" title="Standard sources only; no third-party or UA">Import Monsters From All Sources</a></p>
  5304. <p>
  5305. WARNING: Importing huge numbers of character sheets slows the game down. We recommend you import them as needed.<br>
  5306. The "Import Monsters From All Sources" button presents a list containing monsters from official sources only.<br>
  5307. To import from third-party sources, either individually select one available in the list or enter a custom URL, and "Import Monsters."
  5308. </p>
  5309. </div>
  5310. `;
  5311.  
  5312. d20plus.settingsHtmlPtItems = `
  5313. <div class="importer-section" data-import-group="item">
  5314. <h4>Item Importing</h4>
  5315. <label for="import-items-url">Item Data URL:</label>
  5316. <select id="button-items-select"><!-- populate with JS--></select>
  5317. <input type="text" id="import-items-url">
  5318. <a class="btn" href="#" id="import-items-load">Import Items</a>
  5319. </div>
  5320. `;
  5321.  
  5322. d20plus.settingsHtmlPtItemsPlayer = `
  5323. <div class="importer-section" data-import-group="item">
  5324. <h4>Item Importing</h4>
  5325. <label for="import-items-url-player">Item Data URL:</label>
  5326. <select id="button-items-select-player"><!-- populate with JS--></select>
  5327. <input type="text" id="import-items-url-player">
  5328. <a class="btn" href="#" id="import-items-load-player">Import Items</a>
  5329. </div>
  5330. `;
  5331.  
  5332. d20plus.settingsHtmlPtSpells = `
  5333. <div class="importer-section" data-import-group="spell">
  5334. <h4>Spell Importing</h4>
  5335. <label for="import-spell-url">Spell Data URL:</label>
  5336. <select id="button-spell-select">
  5337. <!-- populate with JS-->
  5338. </select>
  5339. <input type="text" id="import-spell-url">
  5340. <p><a class="btn" href="#" id="button-spells-load">Import Spells</a><p/>
  5341. <p><a class="btn" href="#" id="button-spells-load-all" title="Standard sources only; no third-party or UA">Import Spells From All Sources</a></p>
  5342. <p>
  5343. The "Import Spells From All Sources" button presents a list containing spells from official sources only.<br>
  5344. To import from third-party sources, either individually select one available in the list or enter a custom URL, and "Import Spells."
  5345. </p>
  5346. </div>
  5347. `;
  5348.  
  5349. d20plus.settingsHtmlPtSpellsPlayer = `
  5350. <div class="importer-section" data-import-group="spell">
  5351. <h4>Spell Importing</h4>
  5352. <label for="import-spell-url-player">Spell Data URL:</label>
  5353. <select id="button-spell-select-player">
  5354. <!-- populate with JS-->
  5355. </select>
  5356. <input type="text" id="import-spell-url-player">
  5357. <p><a class="btn" href="#" id="button-spells-load-player">Import Spells</a><p/>
  5358. <p><a class="btn" href="#" id="button-spells-load-all-player" title="Standard sources only; no third-party or UA">Import Spells From All Sources</a></p>
  5359. <p>
  5360. The "Import Spells From All Sources" button presents a list containing spells from official sources only.<br>
  5361. To import from third-party sources, either individually select one available in the list or enter a custom URL, and "Import Spells."
  5362. </p>
  5363. </div>
  5364. `;
  5365.  
  5366. d20plus.settingsHtmlPtPsionics = `
  5367. <div class="importer-section" data-import-group="psionic">
  5368. <h4>Psionic Importing</h4>
  5369. <label for="import-psionics-url">Psionics Data URL:</label>
  5370. <select id="button-psionics-select"><!-- populate with JS--></select>
  5371. <input type="text" id="import-psionics-url">
  5372. <a class="btn" href="#" id="import-psionics-load">Import Psionics</a>
  5373. </div>
  5374. `;
  5375.  
  5376. d20plus.settingsHtmlPtPsionicsPlayer = `
  5377. <div class="importer-section" data-import-group="psionic">
  5378. <h4>Psionic Importing</h4>
  5379. <label for="import-psionics-url-player">Psionics Data URL:</label>
  5380. <select id="button-psionics-select-player"><!-- populate with JS--></select>
  5381. <input type="text" id="import-psionics-url-player">
  5382. <a class="btn" href="#" id="import-psionics-load-player">Import Psionics</a>
  5383. </div>
  5384. `;
  5385.  
  5386. d20plus.settingsHtmlPtFeats = `
  5387. <div class="importer-section" data-import-group="feat">
  5388. <h4>Feat Importing</h4>
  5389. <label for="import-feats-url">Feat Data URL:</label>
  5390. <select id="button-feats-select"><!-- populate with JS--></select>
  5391. <input type="text" id="import-feats-url">
  5392. <a class="btn" href="#" id="import-feats-load">Import Feats</a>
  5393. </div>
  5394. `;
  5395.  
  5396. d20plus.settingsHtmlPtFeatsPlayer = `
  5397. <div class="importer-section" data-import-group="feat">
  5398. <h4>Feat Importing</h4>
  5399. <label for="import-feats-url-player">Feat Data URL:</label>
  5400. <select id="button-feats-select-player"><!-- populate with JS--></select>
  5401. <input type="text" id="import-feats-url-player">
  5402. <a class="btn" href="#" id="import-feats-load-player">Import Feats</a>
  5403. </div>
  5404. `;
  5405.  
  5406. d20plus.settingsHtmlPtObjects = `
  5407. <div class="importer-section" data-import-group="object">
  5408. <h4>Object Importing</h4>
  5409. <label for="import-objects-url">Object Data URL:</label>
  5410. <select id="button-objects-select"><!-- populate with JS--></select>
  5411. <input type="text" id="import-objects-url">
  5412. <a class="btn" href="#" id="import-objects-load">Import Objects</a>
  5413. </div>
  5414. `;
  5415.  
  5416. d20plus.settingsHtmlPtRaces = `
  5417. <div class="importer-section" data-import-group="race">
  5418. <h4>Race Importing</h4>
  5419. <label for="import-races-url">Race Data URL:</label>
  5420. <select id="button-races-select"><!-- populate with JS--></select>
  5421. <input type="text" id="import-races-url">
  5422. <a class="btn" href="#" id="import-races-load">Import Races</a>
  5423. </div>
  5424. `;
  5425.  
  5426. d20plus.settingsHtmlPtRacesPlayer = `
  5427. <div class="importer-section" data-import-group="race">
  5428. <h4>Race Importing</h4>
  5429. <label for="import-races-url-player">Race Data URL:</label>
  5430. <select id="button-races-select-player"><!-- populate with JS--></select>
  5431. <input type="text" id="import-races-url-player">
  5432. <a class="btn" href="#" id="import-races-load-player">Import Races</a>
  5433. </div>
  5434. `;
  5435.  
  5436. d20plus.settingsHtmlPtClasses = `
  5437. <div class="importer-section" data-import-group="class">
  5438. <h4>Class Importing</h4>
  5439. <label for="import-classes-url">Class Data URL:</label>
  5440. <select id="button-classes-select"><!-- populate with JS--></select>
  5441. <input type="text" id="import-classes-url">
  5442. <a class="btn" href="#" id="import-classes-load">Import Classes</a>
  5443. </div>
  5444. `;
  5445.  
  5446. d20plus.settingsHtmlPtClassesPlayer = `
  5447. <div class="importer-section" data-import-group="class">
  5448. <h4>Class Importing</h4>
  5449. <label for="import-classes-url-player">Class Data URL:</label>
  5450. <select id="button-classes-select-player"><!-- populate with JS--></select>
  5451. <input type="text" id="import-classes-url-player">
  5452. <a class="btn" href="#" id="import-classes-load-player">Import Classes</a>
  5453. </div>
  5454. `;
  5455.  
  5456. d20plus.settingsHtmlPtSubclasses = `
  5457. <div class="importer-section" data-import-group="subclass">
  5458. <h4>Subclass Importing</h4>
  5459. <label for="import-subclasses-url">Subclass Data URL:</label>
  5460. <select id="button-subclasses-select"><!-- populate with JS--></select>
  5461. <input type="text" id="import-subclasses-url">
  5462. <a class="btn" href="#" id="import-subclasses-load">Import Subclasses</a>
  5463. <p>
  5464. <b>Default subclasses are imported as part of Classes import. This can be used to load homebrew classes.</b>
  5465. </p>
  5466. </div>
  5467. `;
  5468.  
  5469. d20plus.settingsHtmlPtSubclassesPlayer = `
  5470. <div class="importer-section" data-import-group="subclass">
  5471. <h4>Subclass Importing</h4>
  5472. <label for="import-subclasses-url-player">Subclass Data URL:</label>
  5473. <select id="button-subclasses-select-player"><!-- populate with JS--></select>
  5474. <input type="text" id="import-subclasses-url-player">
  5475. <a class="btn" href="#" id="import-subclasses-load-player">Import Subclasses</a>
  5476. <p>
  5477. <b>Default subclasses are imported as part of Classes import. This can be used to load homebrew classes.</b>
  5478. </p>
  5479. </div>
  5480. `;
  5481.  
  5482. d20plus.settingsHtmlPtBackgrounds = `
  5483. <div class="importer-section" data-import-group="background">
  5484. <h4>Background Importing</h4>
  5485. <label for="import-backgrounds-url">Background Data URL:</label>
  5486. <select id="button-backgrounds-select"><!-- populate with JS--></select>
  5487. <input type="text" id="import-backgrounds-url">
  5488. <a class="btn" href="#" id="import-backgrounds-load">Import Backgrounds</a>
  5489. </div>
  5490. `;
  5491.  
  5492. d20plus.settingsHtmlPtBackgroundsPlayer = `
  5493. <div class="importer-section" data-import-group="background">
  5494. <h4>Background Importing</h4>
  5495. <label for="import-backgrounds-url-player">Background Data URL:</label>
  5496. <select id="button-backgrounds-select-player"><!-- populate with JS--></select>
  5497. <input type="text" id="import-backgrounds-url-player">
  5498. <a class="btn" href="#" id="import-backgrounds-load-player">Import Backgrounds</a>
  5499. </div>
  5500. `;
  5501.  
  5502. d20plus.settingsHtmlPtAdventures = `
  5503. <div class="importer-section" data-import-group="adventure">
  5504. <h4>Adventure Importing</h4>
  5505. <label for="import-adventures-url">Adventure Data URL:</label>
  5506. <select id="button-adventures-select">
  5507. <!-- populate with JS-->
  5508. </select>
  5509. <input type="text" id="import-adventures-url">
  5510. <p><a class="btn" href="#" id="button-adventures-load">Import Adventure</a><p/>
  5511. </div>
  5512. `;
  5513.  
  5514. d20plus.settingsHtmlPtImportFooter = `
  5515. <br>
  5516. <a class="btn bind-drop-locations" href="#" id="bind-drop-locations" style="margin-top: 3px;">Bind Drag-n-Drop</a>
  5517. <p><strong>Readme</strong></p>
  5518. <p>
  5519. You can drag-and-drop imported handouts to character sheets.<br>
  5520. If a handout is glowing green in the journal, it's draggable. This breaks when Roll20 decides to hard-refresh the journal.<br>
  5521. To restore this functionality, press the "Bind Drag-n-Drop" button.<br>
  5522. <i>Note: to drag a handout to a character sheet, you need to drag the name, and not the handout icon.</i>
  5523. </p>
  5524. `;
  5525.  
  5526. d20plus.cssRules = d20plus.cssRules.concat([
  5527. {
  5528. s: ".no-shrink",
  5529. r: "flex-shrink: 0;"
  5530. },
  5531. {
  5532. s: "#initiativewindow ul li span.initiative,#initiativewindow ul li span.tracker-col,#initiativewindow ul li span.initmacro",
  5533. r: "font-size: 25px;font-weight: bold;text-align: right;float: right;padding: 2px 5px;width: 10%;min-height: 20px;display: block;"
  5534. },
  5535. {
  5536. s: "#initiativewindow ul li span.editable input",
  5537. r: "width: 100%; box-sizing: border-box;height: 100%;"
  5538. },
  5539. {
  5540. s: "#initiativewindow div.header",
  5541. r: "height: 30px;"
  5542. },
  5543. {
  5544. s: "#initiativewindow div.header span",
  5545. r: "cursor: default;font-size: 15px;font-weight: bold;text-align: right;float: right;width: 10%;min-height: 20px;padding: 5px;"
  5546. },
  5547. {
  5548. s: ".ui-dialog-buttonpane span.difficulty",
  5549. r: "display: inline-block;padding: 5px 4px 6px;margin: .5em .4em .5em 0;font-size: 18px;"
  5550. },
  5551. {
  5552. s: ".ui-dialog-buttonpane.buttonpane-absolute-position",
  5553. r: "position: absolute;bottom: 0;box-sizing: border-box;width: 100%;"
  5554. },
  5555. {
  5556. s: ".ui-dialog.dialog-collapsed .ui-dialog-buttonpane",
  5557. r: "position: initial;"
  5558. },
  5559. {
  5560. s: ".token .cr,.header .cr",
  5561. r: "display: none!important;"
  5562. },
  5563. {
  5564. s: "li.handout.compendium-item .namecontainer",
  5565. r: "box-shadow: inset 0px 0px 25px 2px rgb(195, 239, 184);"
  5566. },
  5567. {
  5568. s: ".bind-drop-locations:active",
  5569. r: "box-shadow: inset 0px 0px 25px 2px rgb(195, 239, 184);"
  5570. },
  5571. {
  5572. s: "del",
  5573. r: "display: none;"
  5574. },
  5575. {
  5576. s: ".import-cb-label",
  5577. r: "display: block; margin-right: -13px !important;"
  5578. },
  5579. {
  5580. s: ".import-cb-label span",
  5581. r: "display: inline-block; overflow: hidden; max-height: 18px; letter-spacing: -1px; font-size: 12px;"
  5582. },
  5583. {
  5584. s: ".import-cb-label .source",
  5585. r: "width: calc(16.667% - 28px);'"
  5586. },
  5587. {
  5588. s: ".importer-section",
  5589. r: "display: none;"
  5590. },
  5591. {
  5592. s: ".userscript-entry-title",
  5593. r: "font-weight: bold;"
  5594. },
  5595. {
  5596. s: ".userscript-statsBlockHead > .userscript-entry-title",
  5597. r: "font-weight: bold; font-size: 1.5em;"
  5598. },
  5599. {
  5600. s: ".userscript-statsBlockHead > .userscript-statsBlockSubHead > .userscript-entry-title",
  5601. r: "font-weight: bold; font-size: 1.3em;"
  5602. },
  5603. {
  5604. s: ".userscript-statsInlineHead > .userscript-entry-title, .userscript-statsInlineHeadSubVariant > .userscript-entry-title",
  5605. r: "font-style: italic"
  5606. },
  5607. {
  5608. s: ".userscript-statsBlockInsetReadaloud",
  5609. r: "background: #cbd6c688 !important"
  5610. },
  5611. // some generic width stuff
  5612. {
  5613. s: ".col-1",
  5614. r: "width: 8.333%;"
  5615. },
  5616. {
  5617. s: ".col-2",
  5618. r: "width: 16.666%;"
  5619. },
  5620. {
  5621. s: ".col-3",
  5622. r: "width: 25%;"
  5623. },
  5624. {
  5625. s: ".col-4",
  5626. r: "width: 33.333%;"
  5627. },
  5628. {
  5629. s: ".col-5",
  5630. r: "width: 41.667%;"
  5631. },
  5632. {
  5633. s: ".col-6",
  5634. r: "width: 50%;"
  5635. },
  5636. {
  5637. s: ".col-7",
  5638. r: "width: 58.333%;"
  5639. },
  5640. {
  5641. s: ".col-8",
  5642. r: "width: 66.667%;"
  5643. },
  5644. {
  5645. s: ".col-9",
  5646. r: "width: 75%;"
  5647. },
  5648. {
  5649. s: ".col-10",
  5650. r: "width: 83.333%;"
  5651. },
  5652. {
  5653. s: ".col-11",
  5654. r: "width: 91.667%;"
  5655. },
  5656. {
  5657. s: ".col-12",
  5658. r: "width: 100%;"
  5659. },
  5660. ]);
  5661.  
  5662. d20plus.tools = d20plus.tools.concat([
  5663. {
  5664. name: "Shapeshifter Token Builder",
  5665. desc: "Build a rollable table and related token to represent a shapeshifting creature.",
  5666. html: `
  5667. <div id="d20plus-shapeshiftbuild" title="Shapeshifter Token Builder">
  5668. <div id="shapeshiftbuild-list">
  5669. <input type="search" class="search" placeholder="Search creatures...">
  5670. <input type="search" class="filter" placeholder="Filter...">
  5671. <span title="Filter format example: 'cr:1/4; cr:1/2; type:beast; source:MM'" style="cursor: help;">[?]</span>
  5672. <div class="list" style="transform: translateZ(0); max-height: 490px; overflow-y: scroll; overflow-x: hidden;"><i>Loading...</i></div>
  5673. </div>
  5674. <br>
  5675. <input id="shapeshift-name" placeholder="Table name">
  5676. <button class="btn">Create Table</button>
  5677. </div>
  5678. `,
  5679. dialogFn: () => {
  5680. $("#d20plus-shapeshiftbuild").dialog({
  5681. autoOpen: false,
  5682. resizable: true,
  5683. width: 800,
  5684. height: 650,
  5685. });
  5686. },
  5687. openFn: () => {
  5688. const $win = $("#d20plus-shapeshiftbuild");
  5689. $win.dialog("open");
  5690.  
  5691. const toLoad = Object.keys(monsterDataUrls).map(src => d20plus.monsters.formMonsterUrl(monsterDataUrls[src]));
  5692.  
  5693. const $fltr = $win.find(`.filter`);
  5694. $fltr.off("keydown").off("keyup");
  5695. $win.find(`button`).off("click");
  5696.  
  5697. const $lst = $win.find(`.list`);
  5698. let tokenList;
  5699.  
  5700. DataUtil.multiLoadJSON(
  5701. toLoad.map(url => ({url})),
  5702. () => {},
  5703. (dataStack) => {
  5704. $lst.empty();
  5705.  
  5706. let toShow = [];
  5707. dataStack.forEach(d => toShow = toShow.concat(d.monster));
  5708. toShow = toShow.sort((a, b) => SortUtil.ascSort(a.name, b.name));
  5709.  
  5710. let tmp = "";
  5711. toShow.forEach((m, i) => {
  5712. m.__pType = Parser.monTypeToFullObj(m.type).asText;
  5713.  
  5714. tmp += `
  5715. <label class="import-cb-label" data-listid="${i}">
  5716. <input type="checkbox">
  5717. <span class="name col-4">${m.name}</span>
  5718. <span class="type col-4">TYP[${m.__pType.uppercaseFirst()}]</span>
  5719. <span class="cr col-2">${m.cr === undefined ? "CR[Unknown]" : `CR[${(m.cr.cr || m.cr)}]`}</span>
  5720. <span title="${Parser.sourceJsonToFull(m.source)}" class="source">SRC[${Parser.sourceJsonToAbv(m.source)}]</span>
  5721. </label>
  5722. `;
  5723. });
  5724. $lst.html(tmp);
  5725. tmp = null;
  5726.  
  5727. tokenList = new List("shapeshiftbuild-list", {
  5728. valueNames: ["name", "type", "cr", "source"]
  5729. });
  5730.  
  5731. d20plus.importer.addListFilter($fltr, toShow, tokenList, d20plus.monsters._listIndexConverter);
  5732.  
  5733. $win.find(`button`).on("click", () => {
  5734. console.log("Assembling creature list");
  5735. if (tokenList) {
  5736. $("a.ui-tabs-anchor[href='#deckstables']").trigger("click");
  5737.  
  5738. const sel = tokenList.items
  5739. .filter(it => $(it.elm).find(`input`).prop("checked"))
  5740. .map(it => toShow[$(it.elm).attr("data-listid")]);
  5741.  
  5742. const id = d20.Campaign.rollabletables.create().id;
  5743. const table = d20.Campaign.rollabletables.get(id);
  5744. table.set("name", $(`#shapeshift-name`).val().trim() || "Shapeshifter");
  5745. table.save();
  5746. sel.forEach(m => {
  5747. const item = table.tableitems.create();
  5748. item.set("name", m.name);
  5749. const avatar = m.tokenURL || `${IMG_URL}${Parser.sourceJsonToAbv(m.source)}/${m.name.replace(/"/g, "")}.png`;
  5750. item.set("avatar", avatar);
  5751. item.save();
  5752. });
  5753. table.save();
  5754. d20.rollabletables.refreshTablesList();
  5755. alert("Created table!")
  5756. }
  5757. });
  5758. }
  5759. );
  5760. }
  5761. },
  5762. {
  5763. name: "Pauper's Character Vault",
  5764. desc: "Dump characters to JSON, or import dumped characters.",
  5765. html: `
  5766. <div id="d20plus-paupervault" title="Pauper's Character Vault">
  5767. <p>
  5768. This experimental tool allows you to download characters as JSON, to later upload to other games.
  5769. </p>
  5770. <select style="margin-bottom: 0;"></select> <button class="btn download">Download</button>
  5771. <hr>
  5772. <button class="btn upload">Upload</button><input accept=".json" type="file" style="position: absolute; left: -9999px;"> (Previously Download-ed files only)
  5773. </div>
  5774. `,
  5775. dialogFn: () => {
  5776. $("#d20plus-paupervault").dialog({
  5777. autoOpen: false,
  5778. resizable: true,
  5779. width: 400,
  5780. height: 250,
  5781. });
  5782. },
  5783. openFn: () => {
  5784. const $win = $("#d20plus-paupervault");
  5785. $win.dialog("open");
  5786.  
  5787. const $selChar = $win.find(`select`);
  5788.  
  5789. $selChar.append(d20.Campaign.characters.toJSON().sort((a, b) => SortUtil.ascSort(a.name, b.name)).map(c => {
  5790. return `<option value="${c.id}">${c.name || `(Unnamed; ID ${c.id})`}</option>`
  5791. }).join(""));
  5792.  
  5793. const $btnDl = $win.find(`.download`);
  5794. $btnDl.off("click");
  5795. $btnDl.on("click", () => {
  5796. const id = $selChar.val();
  5797. const rawChar = d20.Campaign.characters.get(id);
  5798. const char = rawChar.toJSON();
  5799. char.attribs = rawChar.attribs.toJSON();
  5800. DataUtil.userDownload(char.name.replace(/[^0-9A-Za-z -_()\[\]{}]/, "_"), JSON.stringify({
  5801. char
  5802. }, null, "\t"));
  5803. });
  5804.  
  5805. const $btnUl = $win.find(`.upload`);
  5806. $btnUl.off("click");
  5807. $btnUl.on("click", () => {
  5808. const $iptFile = $win.find(`input[type="file"]`);
  5809.  
  5810. const input = $iptFile[0];
  5811.  
  5812. const reader = new FileReader();
  5813. reader.onload = () => {
  5814. $("a.ui-tabs-anchor[href='#journal']").trigger("click");
  5815.  
  5816. try {
  5817. const text = reader.result;
  5818. const json = JSON.parse(text);
  5819.  
  5820. if (!json.char) {
  5821. window.alert("Failed to import character! See the log for details.");
  5822. console.error(`No "char" attribute found in parsed JSON!`);
  5823. return;
  5824. }
  5825. const char = json.char;
  5826.  
  5827. const newId = d20plus.generateRowId();
  5828. d20.Campaign.characters.create(
  5829. {
  5830. ...char,
  5831. id: newId
  5832. },
  5833. {
  5834. success: function (character) {
  5835. try {
  5836. character.attribs.reset();
  5837. if (!char.attribs) {
  5838. window.alert(`Warning: Uploaded character had no "attribs" attribute. The character sheet will contain no data.`);
  5839. return;
  5840. }
  5841. const toSave = char.attribs.map(a => character.attribs.push(a));
  5842. toSave.forEach(s => s.syncedSave());
  5843. } catch (e) {
  5844. window.alert("Failed to import character! See the log for details.");
  5845. console.error(e);
  5846. }
  5847. }
  5848. }
  5849. );
  5850. } catch (e) {
  5851. console.error(e);
  5852. window.alert("Failed to load file! See the log for details.")
  5853. }
  5854. };
  5855. input.onchange = function () {
  5856. reader.readAsText(input.files[0]);
  5857. };
  5858.  
  5859. $iptFile.click();
  5860. });
  5861. }
  5862. }
  5863. ]);
  5864.  
  5865. // caller should run `$iptFilter.off("keydown").off("keyup");` before calling this
  5866. d20plus.importer.addListFilter = function ($iptFilter, dataList, listObj, listIndexConverter) {
  5867. $iptFilter.val("");
  5868. const TYPE_TIMEOUT_MS = 100;
  5869. let typeTimer;
  5870. $iptFilter.on("keyup", () => {
  5871. clearTimeout(typeTimer);
  5872. typeTimer = setTimeout(() => {
  5873. const exps = $iptFilter.val().split(";");
  5874. const filters = exps.map(it => it.trim())
  5875. .filter(it => it)
  5876. .map(it => it.toLowerCase().split(":"))
  5877. .filter(it => it.length === 2)
  5878. .map(it => ({field: it[0], value: it[1]}));
  5879. const grouped = [];
  5880. filters.forEach(f => {
  5881. const existing = grouped.find(it => it.field === f.field);
  5882. if (existing) existing.values.push(f.value);
  5883. else grouped.push({field: f.field, values: [f.value]})
  5884. });
  5885.  
  5886. listObj.filter((item) => {
  5887. const it = dataList[$(item.elm).attr("data-listid")];
  5888. it._filterVs = it._filterVs || listIndexConverter(it);
  5889. return !grouped.find(f => {
  5890. if (it._filterVs[f.field]) {
  5891. if (it._filterVs[f.field] instanceof Array) {
  5892. return !(it._filterVs[f.field].find(v => f.values.includes(v)));
  5893. } else {
  5894. return !f.values.includes(it._filterVs[f.field])
  5895. }
  5896. }
  5897. return false;
  5898. });
  5899. });
  5900. }, TYPE_TIMEOUT_MS);
  5901. });
  5902. $iptFilter.on("keydown", () => {
  5903. clearTimeout(typeTimer);
  5904. });
  5905. };
  5906.  
  5907. d20plus.initiativeHeaders = `<div class="header">
  5908. <span class="ui-button-text initmacro">Sheet</span>
  5909. <span class="initiative" alt="Initiative" title="Initiative">Init</span>
  5910. <span class="cr" alt="CR" title="CR">CR</span>
  5911. <div class="tracker-header-extra-columns"></div>
  5912. </div>`;
  5913.  
  5914. d20plus.initiativeTemplate = `<script id="tmpl_initiativecharacter" type="text/html">
  5915. <![CDATA[
  5916. <li class='token <$ if (this.layer === "gmlayer") { $>gmlayer<$ } $>' data-tokenid='<$!this.id$>' data-currentindex='<$!this.idx$>'>
  5917. <$ var token = d20.Campaign.pages.get(d20.Campaign.activePage()).thegraphics.get(this.id); $>
  5918. <$ var char = (token) ? token.character : null; $>
  5919. <span alt='Sheet Macro' title='Sheet Macro' class='initmacro'>
  5920. <button type='button' class='initmacrobutton ui-button ui-widget ui-state-default ui-corner-all ui-button-text-only pictos' role='button' aria-disabled='false'>
  5921. <span class='ui-button-text'>N</span>
  5922. </button>
  5923. </span>
  5924. <span alt='Initiative' title='Initiative' class='initiative <$ if (this.iseditable) { $>editable<$ } $>'>
  5925. <$!this.pr$>
  5926. </span>
  5927. <$ if (char) { $>
  5928. <$ var npc = char.attribs ? char.attribs.find(function(a){return a.get("name").toLowerCase() == "npc" }) : null; $>
  5929. <$ } $>
  5930. <div class="tracker-extra-columns">
  5931. <!--5ETOOLS_REPLACE_TARGET-->
  5932. </div>
  5933. <$ if (this.avatar) { $><img src='<$!this.avatar$>' /><$ } $>
  5934. <span class='name'><$!this.name$></span>
  5935. <div class='clear' style='height: 0px;'></div>
  5936. <div class='controls'>
  5937. <span class='pictos remove'>#</span>
  5938. </div>
  5939. </li>
  5940. ]]>
  5941. </script>`;
  5942.  
  5943. d20plus.actionMacroPerception = "%{Selected|npc_perception} /w gm &{template:default} {{name=Senses}} /w gm @{Selected|npc_senses} ";
  5944. d20plus.actionMacroInit = "%{selected|npc_init}";
  5945. d20plus.actionMacroDrImmunities = "/w gm &{template:default} {{name=DR/Immunities}} {{Damage Resistance= @{selected|npc_resistances}}} {{Damage Vulnerability= @{selected|npc_vulnerabilities}}} {{Damage Immunity= @{selected|npc_immunities}}} {{Condition Immunity= @{selected|npc_condition_immunities}}} ";
  5946. d20plus.actionMacroStats = "/w gm &{template:default} {{name=Stats}} {{Armor Class= @{selected|npc_AC}}} {{Hit Dice= @{selected|npc_hpformula}}} {{Speed= @{selected|npc_speed}}} {{Skills= @{selected|npc_skills}}} {{Senses= @{selected|npc_senses}}} {{Languages= @{selected|npc_languages}}} {{Challenge= @{selected|npc_challenge}(@{selected|npc_xp}xp)}}";
  5947. d20plus.actionMacroSaves = "/w gm &{template:simple}{{always=1}}?{Saving Throw?|STR,{{rname=Strength Save&#125;&#125;{{mod=@{npc_str_save}&#125;&#125; {{r1=[[1d20+@{npc_str_save}]]&#125;&#125;{{r2=[[1d20+@{npc_str_save}]]&#125;&#125;|DEX,{{rname=Dexterity Save&#125;&#125;{{mod=@{npc_dex_save}&#125;&#125; {{r1=[[1d20+@{npc_dex_save}]]&#125;&#125;{{r2=[[1d20+@{npc_dex_save}]]&#125;&#125;|CON,{{rname=Constitution Save&#125;&#125;{{mod=@{npc_con_save}&#125;&#125; {{r1=[[1d20+@{npc_con_save}]]&#125;&#125;{{r2=[[1d20+@{npc_con_save}]]&#125;&#125;|INT,{{rname=Intelligence Save&#125;&#125;{{mod=@{npc_int_save}&#125;&#125; {{r1=[[1d20+@{npc_int_save}]]&#125;&#125;{{r2=[[1d20+@{npc_int_save}]]&#125;&#125;|WIS,{{rname=Wisdom Save&#125;&#125;{{mod=@{npc_wis_save}&#125;&#125; {{r1=[[1d20+@{npc_wis_save}]]&#125;&#125;{{r2=[[1d20+@{npc_wis_save}]]&#125;&#125;|CHA,{{rname=Charisma Save&#125;&#125;{{mod=@{npc_cha_save}&#125;&#125; {{r1=[[1d20+@{npc_cha_save}]]&#125;&#125;{{r2=[[1d20+@{npc_cha_save}]]&#125;&#125;}{{charname=@{character_name}}} ";
  5948. d20plus.actionMacroSkillCheck = "/w gm &{template:simple}{{always=1}}?{Ability?|Acrobatics,{{rname=Acrobatics&#125;&#125;{{mod=@{npc_acrobatics}&#125;&#125; {{r1=[[1d20+@{npc_acrobatics}]]&#125;&#125;{{r2=[[1d20+@{npc_acrobatics}]]&#125;&#125;|Animal Handling,{{rname=Animal Handling&#125;&#125;{{mod=@{npc_animal_handling}&#125;&#125; {{r1=[[1d20+@{npc_animal_handling}]]&#125;&#125;{{r2=[[1d20+@{npc_animal_handling}]]&#125;&#125;|Arcana,{{rname=Arcana&#125;&#125;{{mod=@{npc_arcana}&#125;&#125; {{r1=[[1d20+@{npc_arcana}]]&#125;&#125;{{r2=[[1d20+@{npc_arcana}]]&#125;&#125;|Athletics,{{rname=Athletics&#125;&#125;{{mod=@{npc_athletics}&#125;&#125; {{r1=[[1d20+@{npc_athletics}]]&#125;&#125;{{r2=[[1d20+@{npc_athletics}]]&#125;&#125;|Deception,{{rname=Deception&#125;&#125;{{mod=@{npc_deception}&#125;&#125; {{r1=[[1d20+@{npc_deception}]]&#125;&#125;{{r2=[[1d20+@{npc_deception}]]&#125;&#125;|History,{{rname=History&#125;&#125;{{mod=@{npc_history}&#125;&#125; {{r1=[[1d20+@{npc_history}]]&#125;&#125;{{r2=[[1d20+@{npc_history}]]&#125;&#125;|Insight,{{rname=Insight&#125;&#125;{{mod=@{npc_insight}&#125;&#125; {{r1=[[1d20+@{npc_insight}]]&#125;&#125;{{r2=[[1d20+@{npc_insight}]]&#125;&#125;|Intimidation,{{rname=Intimidation&#125;&#125;{{mod=@{npc_intimidation}&#125;&#125; {{r1=[[1d20+@{npc_intimidation}]]&#125;&#125;{{r2=[[1d20+@{npc_intimidation}]]&#125;&#125;|Investigation,{{rname=Investigation&#125;&#125;{{mod=@{npc_investigation}&#125;&#125; {{r1=[[1d20+@{npc_investigation}]]&#125;&#125;{{r2=[[1d20+@{npc_investigation}]]&#125;&#125;|Medicine,{{rname=Medicine&#125;&#125;{{mod=@{npc_medicine}&#125;&#125; {{r1=[[1d20+@{npc_medicine}]]&#125;&#125;{{r2=[[1d20+@{npc_medicine}]]&#125;&#125;|Nature,{{rname=Nature&#125;&#125;{{mod=@{npc_nature}&#125;&#125; {{r1=[[1d20+@{npc_nature}]]&#125;&#125;{{r2=[[1d20+@{npc_nature}]]&#125;&#125;|Perception,{{rname=Perception&#125;&#125;{{mod=@{npc_perception}&#125;&#125; {{r1=[[1d20+@{npc_perception}]]&#125;&#125;{{r2=[[1d20+@{npc_perception}]]&#125;&#125;|Performance,{{rname=Performance&#125;&#125;{{mod=@{npc_performance}&#125;&#125; {{r1=[[1d20+@{npc_performance}]]&#125;&#125;{{r2=[[1d20+@{npc_performance}]]&#125;&#125;|Persuasion,{{rname=Persuasion&#125;&#125;{{mod=@{npc_persuasion}&#125;&#125; {{r1=[[1d20+@{npc_persuasion}]]&#125;&#125;{{r2=[[1d20+@{npc_persuasion}]]&#125;&#125;|Religion,{{rname=Religion&#125;&#125;{{mod=@{npc_religion}&#125;&#125; {{r1=[[1d20+@{npc_religion}]]&#125;&#125;{{r2=[[1d20+@{npc_religion}]]&#125;&#125;|Sleight of Hand,{{rname=Sleight of Hand&#125;&#125;{{mod=@{npc_sleight_of_hand}&#125;&#125; {{r1=[[1d20+@{npc_sleight_of_hand}]]&#125;&#125;{{r2=[[1d20+@{npc_sleight_of_hand}]]&#125;&#125;|Stealth,{{rname=Stealth&#125;&#125;{{mod=@{npc_stealth}&#125;&#125; {{r1=[[1d20+@{npc_stealth}]]&#125;&#125;{{r2=[[1d20+@{npc_stealth}]]&#125;&#125;|Survival,{{rname=Survival&#125;&#125;{{mod=@{npc_survival}&#125;&#125; {{r1=[[1d20+@{npc_survival}]]&#125;&#125;{{r2=[[1d20+@{npc_survival}]]&#125;&#125;}{{charname=@{character_name}}} ";
  5949. d20plus.actionMacroAbilityCheck = "/w gm &{template:simple}{{always=1}}?{Ability?|STR,{{rname=Strength&#125;&#125;{{mod=@{strength_mod}&#125;&#125; {{r1=[[1d20+@{strength_mod}]]&#125;&#125;{{r2=[[1d20+@{strength_mod}]]&#125;&#125;|DEX,{{rname=Dexterity&#125;&#125;{{mod=@{dexterity_mod}&#125;&#125; {{r1=[[1d20+@{dexterity_mod}]]&#125;&#125;{{r2=[[1d20+@{dexterity_mod}]]&#125;&#125;|CON,{{rname=Constitution&#125;&#125;{{mod=@{constitution_mod}&#125;&#125; {{r1=[[1d20+@{constitution_mod}]]&#125;&#125;{{r2=[[1d20+@{constitution_mod}]]&#125;&#125;|INT,{{rname=Intelligence&#125;&#125;{{mod=@{intelligence_mod}&#125;&#125; {{r1=[[1d20+@{intelligence_mod}]]&#125;&#125;{{r2=[[1d20+@{intelligence_mod}]]&#125;&#125;|WIS,{{rname=Wisdom&#125;&#125;{{mod=@{wisdom_mod}&#125;&#125; {{r1=[[1d20+@{wisdom_mod}]]&#125;&#125;{{r2=[[1d20+@{wisdom_mod}]]&#125;&#125;|CHA,{{rname=Charisma&#125;&#125;{{mod=@{charisma_mod}&#125;&#125; {{r1=[[1d20+@{charisma_mod}]]&#125;&#125;{{r2=[[1d20+@{charisma_mod}]]&#125;&#125;}{{charname=@{character_name}}} ";
  5950.  
  5951. d20plus.actionMacroTrait = function (index) {
  5952. return "/w gm &{template:npcaction} {{name=@{selected|npc_name}}} {{rname=@{selected|repeating_npctrait_$" + index + "_name}}} {{description=@{selected|repeating_npctrait_$" + index + "_desc} }}";
  5953. };
  5954.  
  5955. d20plus.actionMacroAction = function (index) {
  5956. return "%{selected|repeating_npcaction_$" + index + "_npc_action}";
  5957. };
  5958.  
  5959. d20plus.actionMacroReaction = "/w gm &{template:npcaction} {{name=@{selected|npc_name}}} {{rname=@{selected|repeating_npcreaction_$0_name}}} {{description=@{selected|repeating_npcreaction_$0_desc} }} ";
  5960.  
  5961. d20plus.actionMacroLegendary = function (tokenactiontext) {
  5962. return "/w gm @{selected|wtype}&{template:npcaction} {{name=@{selected|npc_name}}} {{rname=Legendary Actions}} {{description=The @{selected|npc_name} can take @{selected|npc_legendary_actions} legendary actions, choosing from the options below. Only one legendary option can be used at a time and only at the end of another creature's turn. The @{selected|npc_name} regains spent legendary actions at the start of its turn.\n\r" + tokenactiontext + "}} ";
  5963. }
  5964. };
  5965.  
  5966. SCRIPT_EXTENSIONS.push(betteR205etools);
  5967.  
  5968. const betteR20Emoji = function () {
  5969. d20plus.chat = {};
  5970.  
  5971. d20plus.chat.emojiIndex = {
  5972. joy: !0,
  5973. heart: !0,
  5974. heart_eyes: !0,
  5975. sob: !0,
  5976. blush: !0,
  5977. unamused: !0,
  5978. kissing_heart: !0,
  5979. two_hearts: !0,
  5980. weary: !0,
  5981. ok_hand: !0,
  5982. pensive: !0,
  5983. smirk: !0,
  5984. grin: !0,
  5985. recycle: !0,
  5986. wink: !0,
  5987. thumbsup: !0,
  5988. pray: !0,
  5989. relieved: !0,
  5990. notes: !0,
  5991. flushed: !0,
  5992. raised_hands: !0,
  5993. see_no_evil: !0,
  5994. cry: !0,
  5995. sunglasses: !0,
  5996. v: !0,
  5997. eyes: !0,
  5998. sweat_smile: !0,
  5999. sparkles: !0,
  6000. sleeping: !0,
  6001. smile: !0,
  6002. purple_heart: !0,
  6003. broken_heart: !0,
  6004. expressionless: !0,
  6005. sparkling_heart: !0,
  6006. blue_heart: !0,
  6007. confused: !0,
  6008. information_desk_person: !0,
  6009. stuck_out_tongue_winking_eye: !0,
  6010. disappointed: !0,
  6011. yum: !0,
  6012. neutral_face: !0,
  6013. sleepy: !0,
  6014. clap: !0,
  6015. cupid: !0,
  6016. heartpulse: !0,
  6017. revolving_hearts: !0,
  6018. arrow_left: !0,
  6019. speak_no_evil: !0,
  6020. kiss: !0,
  6021. point_right: !0,
  6022. cherry_blossom: !0,
  6023. scream: !0,
  6024. fire: !0,
  6025. rage: !0,
  6026. smiley: !0,
  6027. tada: !0,
  6028. tired_face: !0,
  6029. camera: !0,
  6030. rose: !0,
  6031. stuck_out_tongue_closed_eyes: !0,
  6032. muscle: !0,
  6033. skull: !0,
  6034. sunny: !0,
  6035. yellow_heart: !0,
  6036. triumph: !0,
  6037. new_moon_with_face: !0,
  6038. laughing: !0,
  6039. sweat: !0,
  6040. point_left: !0,
  6041. heavy_check_mark: !0,
  6042. heart_eyes_cat: !0,
  6043. grinning: !0,
  6044. mask: !0,
  6045. green_heart: !0,
  6046. wave: !0,
  6047. persevere: !0,
  6048. heartbeat: !0,
  6049. arrow_forward: !0,
  6050. arrow_backward: !0,
  6051. arrow_right_hook: !0,
  6052. leftwards_arrow_with_hook: !0,
  6053. crown: !0,
  6054. kissing_closed_eyes: !0,
  6055. stuck_out_tongue: !0,
  6056. disappointed_relieved: !0,
  6057. innocent: !0,
  6058. headphones: !0,
  6059. white_check_mark: !0,
  6060. confounded: !0,
  6061. arrow_right: !0,
  6062. angry: !0,
  6063. grimacing: !0,
  6064. star2: !0,
  6065. gun: !0,
  6066. raising_hand: !0,
  6067. thumbsdown: !0,
  6068. dancer: !0,
  6069. musical_note: !0,
  6070. no_mouth: !0,
  6071. dizzy: !0,
  6072. fist: !0,
  6073. point_down: !0,
  6074. red_circle: !0,
  6075. no_good: !0,
  6076. boom: !0,
  6077. thought_balloon: !0,
  6078. tongue: !0,
  6079. poop: !0,
  6080. cold_sweat: !0,
  6081. gem: !0,
  6082. ok_woman: !0,
  6083. pizza: !0,
  6084. joy_cat: !0,
  6085. sun_with_face: !0,
  6086. leaves: !0,
  6087. sweat_drops: !0,
  6088. penguin: !0,
  6089. zzz: !0,
  6090. walking: !0,
  6091. airplane: !0,
  6092. balloon: !0,
  6093. star: !0,
  6094. ribbon: !0,
  6095. ballot_box_with_check: !0,
  6096. worried: !0,
  6097. underage: !0,
  6098. fearful: !0,
  6099. four_leaf_clover: !0,
  6100. hibiscus: !0,
  6101. microphone: !0,
  6102. open_hands: !0,
  6103. ghost: !0,
  6104. palm_tree: !0,
  6105. bangbang: !0,
  6106. nail_care: !0,
  6107. x: !0,
  6108. alien: !0,
  6109. bow: !0,
  6110. cloud: !0,
  6111. soccer: !0,
  6112. angel: !0,
  6113. dancers: !0,
  6114. exclamation: !0,
  6115. snowflake: !0,
  6116. point_up: !0,
  6117. kissing_smiling_eyes: !0,
  6118. rainbow: !0,
  6119. crescent_moon: !0,
  6120. heart_decoration: !0,
  6121. gift_heart: !0,
  6122. gift: !0,
  6123. beers: !0,
  6124. anguished: !0,
  6125. earth_africa: !0,
  6126. movie_camera: !0,
  6127. anchor: !0,
  6128. zap: !0,
  6129. heavy_multiplication_x: !0,
  6130. runner: !0,
  6131. sunflower: !0,
  6132. earth_americas: !0,
  6133. bouquet: !0,
  6134. dog: !0,
  6135. moneybag: !0,
  6136. herb: !0,
  6137. couple: !0,
  6138. fallen_leaf: !0,
  6139. tulip: !0,
  6140. birthday: !0,
  6141. cat: !0,
  6142. coffee: !0,
  6143. dizzy_face: !0,
  6144. point_up_2: !0,
  6145. open_mouth: !0,
  6146. hushed: !0,
  6147. basketball: !0,
  6148. christmas_tree: !0,
  6149. ring: !0,
  6150. full_moon_with_face: !0,
  6151. astonished: !0,
  6152. two_women_holding_hands: !0,
  6153. money_with_wings: !0,
  6154. crying_cat_face: !0,
  6155. hear_no_evil: !0,
  6156. dash: !0,
  6157. cactus: !0,
  6158. hotsprings: !0,
  6159. telephone: !0,
  6160. maple_leaf: !0,
  6161. princess: !0,
  6162. massage: !0,
  6163. love_letter: !0,
  6164. trophy: !0,
  6165. person_frowning: !0,
  6166. us: !0,
  6167. confetti_ball: !0,
  6168. blossom: !0,
  6169. lips: !0,
  6170. fries: !0,
  6171. doughnut: !0,
  6172. frowning: !0,
  6173. ocean: !0,
  6174. bomb: !0,
  6175. ok: !0,
  6176. cyclone: !0,
  6177. rocket: !0,
  6178. umbrella: !0,
  6179. couplekiss: !0,
  6180. couple_with_heart: !0,
  6181. lollipop: !0,
  6182. clapper: !0,
  6183. pig: !0,
  6184. smiling_imp: !0,
  6185. imp: !0,
  6186. bee: !0,
  6187. kissing_cat: !0,
  6188. anger: !0,
  6189. musical_score: !0,
  6190. santa: !0,
  6191. earth_asia: !0,
  6192. football: !0,
  6193. guitar: !0,
  6194. panda_face: !0,
  6195. speech_balloon: !0,
  6196. strawberry: !0,
  6197. smirk_cat: !0,
  6198. banana: !0,
  6199. watermelon: !0,
  6200. snowman: !0,
  6201. smile_cat: !0,
  6202. top: !0,
  6203. eggplant: !0,
  6204. crystal_ball: !0,
  6205. fork_and_knife: !0,
  6206. calling: !0,
  6207. iphone: !0,
  6208. partly_sunny: !0,
  6209. warning: !0,
  6210. scream_cat: !0,
  6211. small_orange_diamond: !0,
  6212. baby: !0,
  6213. feet: !0,
  6214. footprints: !0,
  6215. beer: !0,
  6216. wine_glass: !0,
  6217. o: !0,
  6218. video_camera: !0,
  6219. rabbit: !0,
  6220. tropical_drink: !0,
  6221. smoking: !0,
  6222. space_invader: !0,
  6223. peach: !0,
  6224. snake: !0,
  6225. turtle: !0,
  6226. cherries: !0,
  6227. kissing: !0,
  6228. frog: !0,
  6229. milky_way: !0,
  6230. rotating_light: !0,
  6231. hatching_chick: !0,
  6232. closed_book: !0,
  6233. candy: !0,
  6234. hamburger: !0,
  6235. bear: !0,
  6236. tiger: !0,
  6237. fast_forward: !0,
  6238. icecream: !0,
  6239. pineapple: !0,
  6240. ear_of_rice: !0,
  6241. syringe: !0,
  6242. put_litter_in_its_place: !0,
  6243. chocolate_bar: !0,
  6244. black_small_square: !0,
  6245. tv: !0,
  6246. pill: !0,
  6247. octopus: !0,
  6248. jack_o_lantern: !0,
  6249. grapes: !0,
  6250. smiley_cat: !0,
  6251. cd: !0,
  6252. cocktail: !0,
  6253. cake: !0,
  6254. video_game: !0,
  6255. arrow_down: !0,
  6256. no_entry_sign: !0,
  6257. lipstick: !0,
  6258. whale: !0,
  6259. cookie: !0,
  6260. dolphin: !0,
  6261. loud_sound: !0,
  6262. man: !0,
  6263. hatched_chick: !0,
  6264. monkey: !0,
  6265. books: !0,
  6266. japanese_ogre: !0,
  6267. guardsman: !0,
  6268. loudspeaker: !0,
  6269. scissors: !0,
  6270. girl: !0,
  6271. mortar_board: !0,
  6272. fr: !0,
  6273. baseball: !0,
  6274. vertical_traffic_light: !0,
  6275. woman: !0,
  6276. fireworks: !0,
  6277. stars: !0,
  6278. sos: !0,
  6279. mushroom: !0,
  6280. pouting_cat: !0,
  6281. left_luggage: !0,
  6282. high_heel: !0,
  6283. dart: !0,
  6284. swimmer: !0,
  6285. key: !0,
  6286. bikini: !0,
  6287. family: !0,
  6288. pencil2: !0,
  6289. elephant: !0,
  6290. droplet: !0,
  6291. seedling: !0,
  6292. apple: !0,
  6293. cool: !0,
  6294. telephone_receiver: !0,
  6295. dollar: !0,
  6296. house_with_garden: !0,
  6297. book: !0,
  6298. haircut: !0,
  6299. computer: !0,
  6300. bulb: !0,
  6301. question: !0,
  6302. back: !0,
  6303. boy: !0,
  6304. closed_lock_with_key: !0,
  6305. person_with_pouting_face: !0,
  6306. tangerine: !0,
  6307. sunrise: !0,
  6308. poultry_leg: !0,
  6309. blue_circle: !0,
  6310. oncoming_automobile: !0,
  6311. shaved_ice: !0,
  6312. bird: !0,
  6313. first_quarter_moon_with_face: !0,
  6314. eyeglasses: !0,
  6315. goat: !0,
  6316. night_with_stars: !0,
  6317. older_woman: !0,
  6318. black_circle: !0,
  6319. new_moon: !0,
  6320. two_men_holding_hands: !0,
  6321. white_circle: !0,
  6322. customs: !0,
  6323. tropical_fish: !0,
  6324. house: !0,
  6325. arrows_clockwise: !0,
  6326. last_quarter_moon_with_face: !0,
  6327. round_pushpin: !0,
  6328. full_moon: !0,
  6329. athletic_shoe: !0,
  6330. lemon: !0,
  6331. baby_bottle: !0,
  6332. spaghetti: !0,
  6333. wind_chime: !0,
  6334. fish_cake: !0,
  6335. evergreen_tree: !0,
  6336. up: !0,
  6337. arrow_up: !0,
  6338. arrow_upper_right: !0,
  6339. arrow_lower_right: !0,
  6340. arrow_lower_left: !0,
  6341. performing_arts: !0,
  6342. nose: !0,
  6343. pig_nose: !0,
  6344. fish: !0,
  6345. man_with_turban: !0,
  6346. koala: !0,
  6347. ear: !0,
  6348. eight_spoked_asterisk: !0,
  6349. small_blue_diamond: !0,
  6350. shower: !0,
  6351. bug: !0,
  6352. ramen: !0,
  6353. tophat: !0,
  6354. bride_with_veil: !0,
  6355. fuelpump: !0,
  6356. checkered_flag: !0,
  6357. horse: !0,
  6358. watch: !0,
  6359. monkey_face: !0,
  6360. baby_symbol: !0,
  6361. new: !0,
  6362. free: !0,
  6363. sparkler: !0,
  6364. corn: !0,
  6365. tennis: !0,
  6366. alarm_clock: !0,
  6367. battery: !0,
  6368. grey_exclamation: !0,
  6369. wolf: !0,
  6370. moyai: !0,
  6371. cow: !0,
  6372. mega: !0,
  6373. older_man: !0,
  6374. dress: !0,
  6375. link: !0,
  6376. chicken: !0,
  6377. whale2: !0,
  6378. arrow_upper_left: !0,
  6379. deciduous_tree: !0,
  6380. bento: !0,
  6381. pushpin: !0,
  6382. soon: !0,
  6383. repeat: !0,
  6384. dragon: !0,
  6385. hamster: !0,
  6386. golf: !0,
  6387. surfer: !0,
  6388. mouse: !0,
  6389. waxing_crescent_moon: !0,
  6390. blue_car: !0,
  6391. a: !0,
  6392. interrobang: !0,
  6393. u5272: !0,
  6394. electric_plug: !0,
  6395. first_quarter_moon: !0,
  6396. cancer: !0,
  6397. trident: !0,
  6398. bread: !0,
  6399. cop: !0,
  6400. tea: !0,
  6401. fishing_pole_and_fish: !0,
  6402. bike: !0,
  6403. rice: !0,
  6404. radio: !0,
  6405. baby_chick: !0,
  6406. arrow_heading_down: !0,
  6407. waning_crescent_moon: !0,
  6408. arrow_up_down: !0,
  6409. last_quarter_moon: !0,
  6410. radio_button: !0,
  6411. sheep: !0,
  6412. person_with_blond_hair: !0,
  6413. waning_gibbous_moon: !0,
  6414. lock: !0,
  6415. green_apple: !0,
  6416. japanese_goblin: !0,
  6417. curly_loop: !0,
  6418. triangular_flag_on_post: !0,
  6419. arrows_counterclockwise: !0,
  6420. racehorse: !0,
  6421. fried_shrimp: !0,
  6422. sunrise_over_mountains: !0,
  6423. volcano: !0,
  6424. rooster: !0,
  6425. inbox_tray: !0,
  6426. wedding: !0,
  6427. sushi: !0,
  6428. wavy_dash: !0,
  6429. ice_cream: !0,
  6430. rewind: !0,
  6431. tomato: !0,
  6432. rabbit2: !0,
  6433. eight_pointed_black_star: !0,
  6434. small_red_triangle: !0,
  6435. high_brightness: !0,
  6436. heavy_plus_sign: !0,
  6437. man_with_gua_pi_mao: !0,
  6438. convenience_store: !0,
  6439. busts_in_silhouette: !0,
  6440. beetle: !0,
  6441. small_red_triangle_down: !0,
  6442. arrow_heading_up: !0,
  6443. name_badge: !0,
  6444. bath: !0,
  6445. no_entry: !0,
  6446. crocodile: !0,
  6447. dog2: !0,
  6448. cat2: !0,
  6449. hammer: !0,
  6450. meat_on_bone: !0,
  6451. shell: !0,
  6452. sparkle: !0,
  6453. b: !0,
  6454. m: !0,
  6455. poodle: !0,
  6456. aquarius: !0,
  6457. stew: !0,
  6458. jeans: !0,
  6459. honey_pot: !0,
  6460. musical_keyboard: !0,
  6461. unlock: !0,
  6462. black_nib: !0,
  6463. statue_of_liberty: !0,
  6464. heavy_dollar_sign: !0,
  6465. snowboarder: !0,
  6466. white_flower: !0,
  6467. necktie: !0,
  6468. diamond_shape_with_a_dot_inside: !0,
  6469. aries: !0,
  6470. womens: !0,
  6471. ant: !0,
  6472. scorpius: !0,
  6473. city_sunset: !0,
  6474. hourglass_flowing_sand: !0,
  6475. o2: !0,
  6476. dragon_face: !0,
  6477. snail: !0,
  6478. dvd: !0,
  6479. shirt: !0,
  6480. game_die: !0,
  6481. heavy_minus_sign: !0,
  6482. dolls: !0,
  6483. sagittarius: !0,
  6484. "8ball": !0,
  6485. bus: !0,
  6486. custard: !0,
  6487. crossed_flags: !0,
  6488. part_alternation_mark: !0,
  6489. camel: !0,
  6490. curry: !0,
  6491. steam_locomotive: !0,
  6492. hospital: !0,
  6493. large_blue_diamond: !0,
  6494. tanabata_tree: !0,
  6495. bell: !0,
  6496. leo: !0,
  6497. gemini: !0,
  6498. pear: !0,
  6499. large_orange_diamond: !0,
  6500. taurus: !0,
  6501. globe_with_meridians: !0,
  6502. door: !0,
  6503. clock6: !0,
  6504. oncoming_police_car: !0,
  6505. envelope_with_arrow: !0,
  6506. closed_umbrella: !0,
  6507. saxophone: !0,
  6508. church: !0,
  6509. bicyclist: !0,
  6510. pisces: !0,
  6511. dango: !0,
  6512. capricorn: !0,
  6513. office: !0,
  6514. rowboat: !0,
  6515. womans_hat: !0,
  6516. mans_shoe: !0,
  6517. love_hotel: !0,
  6518. mount_fuji: !0,
  6519. dromedary_camel: !0,
  6520. handbag: !0,
  6521. hourglass: !0,
  6522. negative_squared_cross_mark: !0,
  6523. trumpet: !0,
  6524. school: !0,
  6525. cow2: !0,
  6526. construction_worker: !0,
  6527. toilet: !0,
  6528. pig2: !0,
  6529. grey_question: !0,
  6530. beginner: !0,
  6531. violin: !0,
  6532. on: !0,
  6533. credit_card: !0,
  6534. id: !0,
  6535. secret: !0,
  6536. ferris_wheel: !0,
  6537. bowling: !0,
  6538. libra: !0,
  6539. virgo: !0,
  6540. barber: !0,
  6541. purse: !0,
  6542. roller_coaster: !0,
  6543. rat: !0,
  6544. date: !0,
  6545. rugby_football: !0,
  6546. ram: !0,
  6547. arrow_up_small: !0,
  6548. black_square_button: !0,
  6549. mobile_phone_off: !0,
  6550. tokyo_tower: !0,
  6551. congratulations: !0,
  6552. kimono: !0,
  6553. ship: !0,
  6554. mag_right: !0,
  6555. mag: !0,
  6556. fire_engine: !0,
  6557. clock1130: !0,
  6558. police_car: !0,
  6559. black_joker: !0,
  6560. bridge_at_night: !0,
  6561. package: !0,
  6562. oncoming_taxi: !0,
  6563. calendar: !0,
  6564. horse_racing: !0,
  6565. tiger2: !0,
  6566. boot: !0,
  6567. ambulance: !0,
  6568. white_square_button: !0,
  6569. boar: !0,
  6570. school_satchel: !0,
  6571. loop: !0,
  6572. pound: !0,
  6573. information_source: !0,
  6574. ox: !0,
  6575. rice_ball: !0,
  6576. vs: !0,
  6577. end: !0,
  6578. parking: !0,
  6579. sandal: !0,
  6580. tent: !0,
  6581. seat: !0,
  6582. taxi: !0,
  6583. black_medium_small_square: !0,
  6584. briefcase: !0,
  6585. newspaper: !0,
  6586. circus_tent: !0,
  6587. six_pointed_star: !0,
  6588. mens: !0,
  6589. european_castle: !0,
  6590. flashlight: !0,
  6591. foggy: !0,
  6592. arrow_double_up: !0,
  6593. bamboo: !0,
  6594. ticket: !0,
  6595. helicopter: !0,
  6596. minidisc: !0,
  6597. oncoming_bus: !0,
  6598. melon: !0,
  6599. white_small_square: !0,
  6600. european_post_office: !0,
  6601. keycap_ten: !0,
  6602. notebook: !0,
  6603. no_bell: !0,
  6604. oden: !0,
  6605. flags: !0,
  6606. carousel_horse: !0,
  6607. blowfish: !0,
  6608. chart_with_upwards_trend: !0,
  6609. sweet_potato: !0,
  6610. ski: !0,
  6611. clock12: !0,
  6612. signal_strength: !0,
  6613. construction: !0,
  6614. black_medium_square: !0,
  6615. satellite: !0,
  6616. euro: !0,
  6617. womans_clothes: !0,
  6618. ledger: !0,
  6619. leopard: !0,
  6620. low_brightness: !0,
  6621. clock3: !0,
  6622. department_store: !0,
  6623. truck: !0,
  6624. sake: !0,
  6625. railway_car: !0,
  6626. speedboat: !0,
  6627. vhs: !0,
  6628. clock1: !0,
  6629. arrow_double_down: !0,
  6630. water_buffalo: !0,
  6631. arrow_down_small: !0,
  6632. yen: !0,
  6633. mute: !0,
  6634. running_shirt_with_sash: !0,
  6635. white_large_square: !0,
  6636. wheelchair: !0,
  6637. clock2: !0,
  6638. paperclip: !0,
  6639. atm: !0,
  6640. cinema: !0,
  6641. telescope: !0,
  6642. rice_scene: !0,
  6643. blue_book: !0,
  6644. white_medium_square: !0,
  6645. postbox: !0,
  6646. "e-mail": !0,
  6647. mouse2: !0,
  6648. bullettrain_side: !0,
  6649. ideograph_advantage: !0,
  6650. nut_and_bolt: !0,
  6651. ng: !0,
  6652. hotel: !0,
  6653. wc: !0,
  6654. izakaya_lantern: !0,
  6655. repeat_one: !0,
  6656. mailbox_with_mail: !0,
  6657. chart_with_downwards_trend: !0,
  6658. green_book: !0,
  6659. tractor: !0,
  6660. fountain: !0,
  6661. metro: !0,
  6662. clipboard: !0,
  6663. no_mobile_phones: !0,
  6664. clock4: !0,
  6665. no_smoking: !0,
  6666. black_large_square: !0,
  6667. slot_machine: !0,
  6668. clock5: !0,
  6669. bathtub: !0,
  6670. scroll: !0,
  6671. station: !0,
  6672. rice_cracker: !0,
  6673. bank: !0,
  6674. wrench: !0,
  6675. u6307: !0,
  6676. articulated_lorry: !0,
  6677. page_facing_up: !0,
  6678. ophiuchus: !0,
  6679. bar_chart: !0,
  6680. no_pedestrians: !0,
  6681. vibration_mode: !0,
  6682. clock10: !0,
  6683. clock9: !0,
  6684. bullettrain_front: !0,
  6685. minibus: !0,
  6686. tram: !0,
  6687. clock8: !0,
  6688. u7a7a: !0,
  6689. traffic_light: !0,
  6690. mountain_bicyclist: !0,
  6691. microscope: !0,
  6692. japanese_castle: !0,
  6693. bookmark: !0,
  6694. bookmark_tabs: !0,
  6695. pouch: !0,
  6696. ab: !0,
  6697. page_with_curl: !0,
  6698. flower_playing_cards: !0,
  6699. clock11: !0,
  6700. fax: !0,
  6701. clock7: !0,
  6702. white_medium_small_square: !0,
  6703. currency_exchange: !0,
  6704. sound: !0,
  6705. chart: !0,
  6706. cl: !0,
  6707. floppy_disk: !0,
  6708. post_office: !0,
  6709. speaker: !0,
  6710. japan: !0,
  6711. u55b6: !0,
  6712. mahjong: !0,
  6713. incoming_envelope: !0,
  6714. orange_book: !0,
  6715. restroom: !0,
  6716. u7121: !0,
  6717. u6709: !0,
  6718. triangular_ruler: !0,
  6719. train: !0,
  6720. u7533: !0,
  6721. trolleybus: !0,
  6722. u6708: !0,
  6723. notebook_with_decorative_cover: !0,
  6724. u7981: !0,
  6725. u6e80: !0,
  6726. postal_horn: !0,
  6727. factory: !0,
  6728. children_crossing: !0,
  6729. train2: !0,
  6730. straight_ruler: !0,
  6731. pager: !0,
  6732. accept: !0,
  6733. u5408: !0,
  6734. lock_with_ink_pen: !0,
  6735. clock130: !0,
  6736. sa: !0,
  6737. outbox_tray: !0,
  6738. twisted_rightwards_arrows: !0,
  6739. mailbox: !0,
  6740. light_rail: !0,
  6741. clock930: !0,
  6742. busstop: !0,
  6743. open_file_folder: !0,
  6744. file_folder: !0,
  6745. potable_water: !0,
  6746. card_index: !0,
  6747. clock230: !0,
  6748. monorail: !0,
  6749. clock1230: !0,
  6750. clock1030: !0,
  6751. abc: !0,
  6752. mailbox_closed: !0,
  6753. clock430: !0,
  6754. mountain_railway: !0,
  6755. do_not_litter: !0,
  6756. clock330: !0,
  6757. heavy_division_sign: !0,
  6758. clock730: !0,
  6759. clock530: !0,
  6760. capital_abcd: !0,
  6761. mailbox_with_no_mail: !0,
  6762. symbols: !0,
  6763. aerial_tramway: !0,
  6764. clock830: !0,
  6765. clock630: !0,
  6766. abcd: !0,
  6767. mountain_cableway: !0,
  6768. koko: !0,
  6769. passport_control: !0,
  6770. "non-potable_water": !0,
  6771. suspension_railway: !0,
  6772. baggage_claim: !0,
  6773. no_bicycles: !0,
  6774. skull_crossbones: !0,
  6775. hugging: !0,
  6776. thinking: !0,
  6777. nerd: !0,
  6778. zipper_mouth: !0,
  6779. rolling_eyes: !0,
  6780. upside_down: !0,
  6781. slight_smile: !0,
  6782. middle_finger: !0,
  6783. writing_hand: !0,
  6784. dark_sunglasses: !0,
  6785. eye: !0,
  6786. man_in_suit: !0,
  6787. golfer: !0,
  6788. heart_exclamation: !0,
  6789. star_of_david: !0,
  6790. cross: !0,
  6791. "fleur-de-lis": !0,
  6792. atom: !0,
  6793. wheel_of_dharma: !0,
  6794. yin_yang: !0,
  6795. peace: !0,
  6796. star_and_crescent: !0,
  6797. orthodox_cross: !0,
  6798. biohazard: !0,
  6799. radioactive: !0,
  6800. place_of_worship: !0,
  6801. anger_right: !0,
  6802. menorah: !0,
  6803. om_symbol: !0,
  6804. coffin: !0,
  6805. gear: !0,
  6806. alembic: !0,
  6807. scales: !0,
  6808. crossed_swords: !0,
  6809. keyboard: !0,
  6810. shield: !0,
  6811. bed: !0,
  6812. shopping_bags: !0,
  6813. sleeping_accommodation: !0,
  6814. ballot_box: !0,
  6815. compression: !0,
  6816. wastebasket: !0,
  6817. file_cabinet: !0,
  6818. trackball: !0,
  6819. printer: !0,
  6820. joystick: !0,
  6821. hole: !0,
  6822. candle: !0,
  6823. prayer_beads: !0,
  6824. camera_with_flash: !0,
  6825. amphora: !0,
  6826. label: !0,
  6827. flag_black: !0,
  6828. flag_white: !0,
  6829. film_frames: !0,
  6830. control_knobs: !0,
  6831. level_slider: !0,
  6832. thermometer: !0,
  6833. airplane_arriving: !0,
  6834. airplane_departure: !0,
  6835. railway_track: !0,
  6836. motorway: !0,
  6837. synagogue: !0,
  6838. mosque: !0,
  6839. kaaba: !0,
  6840. stadium: !0,
  6841. desert: !0,
  6842. classical_building: !0,
  6843. cityscape: !0,
  6844. camping: !0,
  6845. bow_and_arrow: !0,
  6846. rosette: !0,
  6847. volleyball: !0,
  6848. medal: !0,
  6849. reminder_ribbon: !0,
  6850. popcorn: !0,
  6851. champagne: !0,
  6852. hot_pepper: !0,
  6853. burrito: !0,
  6854. taco: !0,
  6855. hotdog: !0,
  6856. shamrock: !0,
  6857. comet: !0,
  6858. turkey: !0,
  6859. scorpion: !0,
  6860. lion_face: !0,
  6861. crab: !0,
  6862. spider_web: !0,
  6863. spider: !0,
  6864. chipmunk: !0,
  6865. wind_blowing_face: !0,
  6866. fog: !0,
  6867. play_pause: !0,
  6868. track_previous: !0,
  6869. track_next: !0,
  6870. beach_umbrella: !0,
  6871. chains: !0,
  6872. pick: !0,
  6873. stopwatch: !0,
  6874. ferry: !0,
  6875. mountain: !0,
  6876. shinto_shrine: !0,
  6877. ice_skate: !0,
  6878. skier: !0,
  6879. flag_ac: !0,
  6880. flag_ad: !0,
  6881. flag_ae: !0,
  6882. flag_af: !0,
  6883. flag_ag: !0,
  6884. flag_ai: !0,
  6885. flag_al: !0,
  6886. flag_am: !0,
  6887. "flag-ao": !0,
  6888. "flag-aq": !0,
  6889. "flag-ar": !0,
  6890. "flag-as": !0,
  6891. "flag-at": !0,
  6892. "flag-au": !0,
  6893. "flag-aw": !0,
  6894. "flag-ax": !0,
  6895. "flag-az": !0,
  6896. "flag-ba": !0,
  6897. "flag-bb": !0,
  6898. "flag-bd": !0,
  6899. "flag-be": !0,
  6900. "flag-bf": !0,
  6901. "flag-bg": !0,
  6902. "flag-bh": !0,
  6903. "flag-bi": !0,
  6904. "flag-bj": !0,
  6905. "flag-bl": !0,
  6906. "flag-bm": !0,
  6907. "flag-bn": !0,
  6908. "flag-bo": !0,
  6909. "flag-bq": !0,
  6910. "flag-br": !0,
  6911. "flag-bs": !0,
  6912. "flag-bt": !0,
  6913. "flag-bv": !0,
  6914. "flag-bw": !0,
  6915. "flag-by": !0,
  6916. "flag-bz": !0,
  6917. "flag-ca": !0,
  6918. "flag-cc": !0,
  6919. "flag-cd": !0,
  6920. "flag-cf": !0,
  6921. "flag-cg": !0,
  6922. "flag-ch": !0,
  6923. "flag-ci": !0,
  6924. "flag-ck": !0,
  6925. "flag-cl": !0,
  6926. "flag-cm": !0,
  6927. "flag-cn": !0,
  6928. "flag-co": !0,
  6929. "flag-cp": !0,
  6930. "flag-cr": !0,
  6931. "flag-cu": !0,
  6932. "flag-cv": !0,
  6933. "flag-cw": !0,
  6934. "flag-cx": !0,
  6935. "flag-cy": !0,
  6936. "flag-cz": !0,
  6937. "flag-de": !0,
  6938. "flag-dg": !0,
  6939. "flag-dj": !0,
  6940. "flag-dk": !0,
  6941. "flag-dm": !0,
  6942. "flag-do": !0,
  6943. "flag-dz": !0,
  6944. "flag-ea": !0,
  6945. "flag-ec": !0,
  6946. "flag-ee": !0,
  6947. "flag-eg": !0,
  6948. "flag-eh": !0,
  6949. "flag-er": !0,
  6950. "flag-es": !0,
  6951. "flag-et": !0,
  6952. "flag-eu": !0,
  6953. "flag-fi": !0,
  6954. "flag-fj": !0,
  6955. "flag-fk": !0,
  6956. "flag-fm": !0,
  6957. "flag-fo": !0,
  6958. "flag-fr": !0,
  6959. "flag-ga": !0,
  6960. "flag-gb": !0,
  6961. "flag-gd": !0,
  6962. "flag-ge": !0,
  6963. "flag-gf": !0,
  6964. "flag-gg": !0,
  6965. "flag-gh": !0,
  6966. "flag-gi": !0,
  6967. "flag-gl": !0,
  6968. "flag-gm": !0,
  6969. "flag-gn": !0,
  6970. "flag-gp": !0,
  6971. "flag-gq": !0,
  6972. "flag-gr": !0,
  6973. "flag-gs": !0,
  6974. "flag-gt": !0,
  6975. "flag-gu": !0,
  6976. "flag-gw": !0,
  6977. "flag-gy": !0,
  6978. "flag-hk": !0,
  6979. "flag-hm": !0,
  6980. "flag-hn": !0,
  6981. "flag-hr": !0,
  6982. "flag-ht": !0,
  6983. "flag-hu": !0,
  6984. "flag-ic": !0,
  6985. "flag-id": !0,
  6986. "flag-ie": !0,
  6987. "flag-il": !0,
  6988. "flag-im": !0,
  6989. "flag-in": !0,
  6990. "flag-io": !0,
  6991. "flag-iq": !0,
  6992. "flag-ir": !0,
  6993. "flag-is": !0,
  6994. "flag-it": !0,
  6995. "flag-je": !0,
  6996. "flag-jm": !0,
  6997. "flag-jo": !0,
  6998. "flag-jp": !0,
  6999. "flag-ke": !0,
  7000. "flag-kg": !0,
  7001. "flag-kh": !0,
  7002. "flag-ki": !0,
  7003. "flag-km": !0,
  7004. "flag-kn": !0,
  7005. "flag-kp": !0,
  7006. "flag-kr": !0,
  7007. "flag-kw": !0,
  7008. "flag-ky": !0,
  7009. "flag-kz": !0,
  7010. "flag-la": !0,
  7011. "flag-lb": !0,
  7012. "flag-lc": !0,
  7013. "flag-li": !0,
  7014. "flag-lk": !0,
  7015. "flag-lr": !0,
  7016. "flag-ls": !0,
  7017. "flag-lt": !0,
  7018. "flag-lu": !0,
  7019. "flag-lv": !0,
  7020. "flag-ly": !0,
  7021. "flag-ma": !0,
  7022. "flag-mc": !0,
  7023. "flag-md": !0,
  7024. "flag-me": !0,
  7025. "flag-mf": !0,
  7026. "flag-mg": !0,
  7027. "flag-mh": !0,
  7028. "flag-mk": !0,
  7029. "flag-ml": !0,
  7030. "flag-mm": !0,
  7031. "flag-mn": !0,
  7032. "flag-mo": !0,
  7033. "flag-mp": !0,
  7034. "flag-mq": !0,
  7035. "flag-mr": !0,
  7036. "flag-ms": !0,
  7037. "flag-mt": !0,
  7038. "flag-mu": !0,
  7039. "flag-mv": !0,
  7040. "flag-mw": !0,
  7041. "flag-mx": !0,
  7042. "flag-my": !0,
  7043. "flag-mz": !0,
  7044. "flag-na": !0,
  7045. "flag-nc": !0,
  7046. "flag-ne": !0,
  7047. "flag-nf": !0,
  7048. "flag-ng": !0,
  7049. "flag-ni": !0,
  7050. "flag-nl": !0,
  7051. "flag-no": !0,
  7052. "flag-np": !0,
  7053. "flag-nr": !0,
  7054. "flag-nu": !0,
  7055. "flag-nz": !0,
  7056. "flag-om": !0,
  7057. "flag-pa": !0,
  7058. "flag-pe": !0,
  7059. "flag-pf": !0,
  7060. "flag-pg": !0,
  7061. "flag-ph": !0,
  7062. "flag-pk": !0,
  7063. "flag-pl": !0,
  7064. "flag-pm": !0,
  7065. "flag-pn": !0,
  7066. "flag-pr": !0,
  7067. "flag-ps": !0,
  7068. "flag-pt": !0,
  7069. "flag-pw": !0,
  7070. "flag-py": !0,
  7071. "flag-qa": !0,
  7072. "flag-re": !0,
  7073. "flag-ro": !0,
  7074. "flag-rs": !0,
  7075. "flag-ru": !0,
  7076. "flag-rw": !0,
  7077. "flag-sa": !0,
  7078. "flag-sb": !0,
  7079. "flag-sc": !0,
  7080. "flag-sd": !0,
  7081. "flag-se": !0,
  7082. "flag-sg": !0,
  7083. "flag-sh": !0,
  7084. "flag-si": !0,
  7085. "flag-sj": !0,
  7086. "flag-sk": !0,
  7087. "flag-sl": !0,
  7088. "flag-sm": !0,
  7089. "flag-sn": !0,
  7090. "flag-so": !0,
  7091. "flag-sr": !0,
  7092. "flag-ss": !0,
  7093. "flag-st": !0,
  7094. "flag-sv": !0,
  7095. "flag-sx": !0,
  7096. "flag-sy": !0,
  7097. "flag-sz": !0,
  7098. "flag-ta": !0,
  7099. "flag-tc": !0,
  7100. "flag-td": !0,
  7101. "flag-tf": !0,
  7102. "flag-tg": !0,
  7103. "flag-th": !0,
  7104. "flag-tj": !0,
  7105. "flag-tk": !0,
  7106. "flag-tl": !0,
  7107. "flag-tm": !0,
  7108. "flag-tn": !0,
  7109. "flag-to": !0,
  7110. "flag-tr": !0,
  7111. "flag-tt": !0,
  7112. "flag-tv": !0,
  7113. "flag-tw": !0,
  7114. "flag-tz": !0,
  7115. "flag-ua": !0,
  7116. "flag-ug": !0,
  7117. "flag-um": !0,
  7118. "flag-us": !0,
  7119. "flag-uy": !0,
  7120. "flag-uz": !0,
  7121. "flag-va": !0,
  7122. "flag-vc": !0,
  7123. "flag-ve": !0,
  7124. "flag-vg": !0,
  7125. "flag-vi": !0,
  7126. "flag-vn": !0,
  7127. flag_vu: !0,
  7128. flag_wf: !0,
  7129. flag_ws: !0,
  7130. flag_xk: !0,
  7131. flag_ye: !0,
  7132. flag_yt: !0,
  7133. flag_za: !0,
  7134. flag_zm: !0,
  7135. flag_zw: !0,
  7136. black_heart: !0,
  7137. speech_left: !0,
  7138. egg: !0,
  7139. octagonal_sign: !0,
  7140. spades: !0,
  7141. hearts: !0,
  7142. diamonds: !0,
  7143. clubs: !0,
  7144. drum: !0,
  7145. left_right_arrow: !0,
  7146. tm: !0,
  7147. "100": !0
  7148. };
  7149.  
  7150. addConfigOptions(
  7151. "interface", {
  7152. _name: "Interface",
  7153. emoji: {
  7154. name: "Add Emoji Replacement to Chat",
  7155. default: true,
  7156. _type: "boolean"
  7157. }
  7158. }
  7159. );
  7160.  
  7161. d20plus.enhanceChat = () => {
  7162. d20plus.log("Enhancing chat");
  7163. const tc = d20.textchat.$textarea;
  7164. $("#textchat-input").off("click", "button")
  7165. $("#textchat-input").on("click", "button", function () {
  7166. if (!window.is_gm || d20plus.getCfgVal("interface", "emoji")) {
  7167. tc.val(tc.val().replace(/(:\w*?:)/g, (m0, m1) => {
  7168. const clean = m1.replace(/:/g, "");
  7169. return d20plus.chat.emojiIndex && d20plus.chat.emojiIndex[clean] ? `[${clean}](https://github.com/TheGiddyLimit/emoji-dump/raw/master/out/${clean}.png)` : m1;
  7170. }));
  7171. }
  7172. const toSend = $.trim(tc.val());
  7173. d20.textchat.doChatInput(toSend);
  7174. tc.val("").focus();
  7175. });
  7176. };
  7177. };
  7178.  
  7179. SCRIPT_EXTENSIONS.push(betteR20Emoji);
  7180.  
  7181. var betteR20Base = function () {
  7182. addConfigOptions("token", {
  7183. "_name": "Tokens",
  7184. "enhanceStatus": {
  7185. "name": "Use Custom Status Icons",
  7186. "default": true,
  7187. "_type": "boolean"
  7188. },
  7189. "statusSheetUrl": {
  7190. "name": `Custom Status Spritesheet Url (<a style="color: blue" href="https://app.roll20.net/images/statussheet.png" target="_blank">Original</a>)`,
  7191. "default": "https://raw.githubusercontent.com/TheGiddyLimit/5etoolsR20/master/img/statussheet.png",
  7192. "_type": "String"
  7193. },
  7194. "statusSheetSmallUrl": {
  7195. "name": `Custom Status Spritesheet (Small) Url (<a style="color: blue" href="https://app.roll20.net/images/statussheet_small.png" target="_blank">Original</a>)`,
  7196. "default": "https://raw.githubusercontent.com/TheGiddyLimit/5etoolsR20/master/img/statussheet_small.png",
  7197. "_type": "String"
  7198. }
  7199. }
  7200. );
  7201. addConfigOptions("canvas", {
  7202. "_name": "Canvas",
  7203. "halfGridSnap": {
  7204. "name": "Snap to Half-Grid",
  7205. "default": false,
  7206. "_type": "boolean"
  7207. },
  7208. "scaleNamesStatuses": {
  7209. "name": "Scaled Names and Status Icons",
  7210. "default": true,
  7211. "_type": "boolean"
  7212. }
  7213. }
  7214. );
  7215.  
  7216. const d20plus = {
  7217. // EXTERNAL SCRIPTS ////////////////////////////////////////////////////////////////////////////////////////////
  7218. scriptsLoaded: false,
  7219. scripts: [
  7220. {name: "listjs", url: "https://raw.githubusercontent.com/javve/list.js/v1.5.0/dist/list.min.js"}
  7221. ],
  7222.  
  7223. addScripts: (onLoadFunction) => {
  7224. d20plus.log("Add JS");
  7225. const onEachLoadFunction = function (name, url, js) {
  7226. try {
  7227. window.eval(js);
  7228. d20plus.log(`JS [${name}] Loaded`);
  7229. } catch (e) {
  7230. d20plus.log(`Error loading ${name}`);
  7231. }
  7232. };
  7233. d20plus.chainLoad(d20plus.scripts, 0, onEachLoadFunction, onLoadFunction);
  7234. },
  7235.  
  7236. chainLoad: (toLoads, index, onEachLoadFunction, onFinalLoadFunction) => {
  7237. const toLoad = toLoads[index];
  7238. // on loading the last item, run onLoadFunction
  7239. let retries = 3;
  7240. function withRetries () {
  7241. $.ajax({
  7242. type: "GET",
  7243. url: toLoad.url + d20plus.getAntiCacheSuffix() + retries,
  7244. success: function (data) {
  7245. if (index === toLoads.length - 1) {
  7246. onEachLoadFunction(toLoad.name, toLoad.url, data);
  7247. onFinalLoadFunction();
  7248. } else {
  7249. onEachLoadFunction(toLoad.name, toLoad.url, data);
  7250. d20plus.chainLoad(toLoads, index + 1, onEachLoadFunction, onFinalLoadFunction);
  7251. }
  7252. },
  7253. error: function (resp, qq, pp) {
  7254. if (resp && resp.status === 500 && retries-- > 0) {
  7255. console.error(resp, qq, pp);
  7256. d20plus.log(`Error loading ${toLoad.name}; retrying`);
  7257. setTimeout(() => {
  7258. withRetries();
  7259. }, 500);
  7260. } else {
  7261. console.error(resp, qq, pp);
  7262. d20plus.log(`Error loading ${toLoad.name}`);
  7263. }
  7264. }
  7265. });
  7266. }
  7267. withRetries();
  7268. },
  7269.  
  7270. // UTILITIES ///////////////////////////////////////////////////////////////////////////////////////////////////
  7271. log: (arg) => {
  7272. console.log("%cD20Plus > ", "color: #3076b9; font-size: large", arg);
  7273. },
  7274.  
  7275. ascSort: (a, b) => {
  7276. if (b === a) return 0;
  7277. return b < a ? 1 : -1;
  7278. },
  7279.  
  7280. chatTag: (message) => {
  7281. d20.textchat.incoming(false, ({
  7282. who: "system",
  7283. type: "system",
  7284. content: `<span style="margin-left: -45px; margin-right: -5px; margin-bottom: -7px; display: inline-block; font-weight: bold; font-family: 'Lucida Console', Monaco, monospace; color: #20C20E; background: black; padding: 3px;">
  7285. ${message}
  7286. <br>
  7287. <br>
  7288. Need help? Join our <a style="color: white;" href="https://discord.gg/Zk34CUZ">Discord</a>.
  7289. <br>
  7290. <br>
  7291. <span title="You'd think this would be obvious.">Please DO NOT post about this script or any related content in official channels, such as the Roll20 forums.</span>
  7292. </span>`
  7293. }));
  7294. },
  7295.  
  7296. addCSS: (sheet, selector, rules) => {
  7297. const index = sheet.cssRules.length;
  7298. if ("insertRule" in sheet) {
  7299. sheet.insertRule(selector + "{" + rules + "}", index);
  7300. } else if ("addRule" in sheet) {
  7301. sheet.addRule(selector, rules, index);
  7302. }
  7303. },
  7304.  
  7305. addAllCss: () => {
  7306. d20plus.log("Add CSS");
  7307. const targetSheet = window.document.styleSheets[window.document.styleSheets.length - 1];
  7308. _.each(d20plus.baseCssRules, function (r) {
  7309. d20plus.addCSS(targetSheet, r.s, r.r);
  7310. });
  7311. if (!window.is_gm) {
  7312. _.each(d20plus.baseCssRulesPlayer, function (r) {
  7313. d20plus.addCSS(targetSheet, r.s, r.r);
  7314. });
  7315. }
  7316. _.each(d20plus.cssRules, function (r) {
  7317. d20plus.addCSS(targetSheet, r.s, r.r);
  7318. });
  7319. },
  7320.  
  7321. getAntiCacheSuffix: () => {
  7322. return "?" + (new Date()).getTime();
  7323. },
  7324.  
  7325. generateRowId: () => {
  7326. return window.generateUUID().replace(/_/g, "Z");
  7327. },
  7328.  
  7329. randomRoll: (roll, success, error) => {
  7330. d20.textchat.diceengine.process(roll, success, error);
  7331. },
  7332.  
  7333. randomInt: (int) => {
  7334. // Return random integer between [0,int)
  7335. return d20.textchat.diceengine.random(int);
  7336. },
  7337.  
  7338. getJournalFolderObj: () => {
  7339. d20.journal.refreshJournalList();
  7340. let journalFolder = d20.Campaign.get("journalfolder");
  7341. if (journalFolder === "") {
  7342. d20.journal.addFolderToFolderStructure("Characters");
  7343. d20.journal.refreshJournalList();
  7344. journalFolder = d20.Campaign.get("journalfolder");
  7345. }
  7346. return JSON.parse(journalFolder);
  7347. },
  7348.  
  7349. getCleanText: (str) => {
  7350. const check = jQuery.parseHTML(str);
  7351. if (check.length === 1 && check[0].constructor === Text) {
  7352. return str;
  7353. }
  7354. const $ele = $(str);
  7355. $ele.find("p, li, br").append("\n\n");
  7356. return $ele.text().replace(/[ ]+/g, " ");
  7357. },
  7358.  
  7359. // CONFIG //////////////////////////////////////////////////////////////////////////////////////////////////////
  7360. config: {},
  7361.  
  7362. loadConfigFailed: false,
  7363.  
  7364. loadConfig: (nextFn) => {
  7365. d20plus.log("Reading Config");
  7366. let configHandout = d20plus.getConfigHandout();
  7367.  
  7368. if (!configHandout) {
  7369. d20plus.log("No config found! Initialising new config...");
  7370. d20plus.makeDefaultConfig(doLoad);
  7371. } else {
  7372. doLoad();
  7373. }
  7374.  
  7375. function doLoad () {
  7376. configHandout = d20plus.getConfigHandout();
  7377. if (configHandout) {
  7378. configHandout.view.render();
  7379. configHandout._getLatestBlob("gmnotes", function (gmnotes) {
  7380. try {
  7381. const decoded = decodeURIComponent(gmnotes);
  7382.  
  7383. d20plus.config = JSON.parse(decoded);
  7384.  
  7385. d20plus.log("Config Loaded:");
  7386. d20plus.log(d20plus.config);
  7387. nextFn();
  7388. } catch (e) {
  7389. if (!d20plus.loadConfigFailed) {
  7390. // prevent infinite loops
  7391. d20plus.loadConfigFailed = true;
  7392.  
  7393. d20plus.log("Corrupted config! Rebuilding...");
  7394. d20plus.makeDefaultConfig(() => {
  7395. d20plus.loadConfig(nextFn)
  7396. });
  7397. } else {
  7398. // if the config fails, continue to load anyway
  7399. nextFn();
  7400. }
  7401. }
  7402. });
  7403. } else {
  7404. d20plus.log("Failed to create config handout!");
  7405. nextFn();
  7406. }
  7407. }
  7408. },
  7409.  
  7410. makeDefaultConfig: (nextFn) => {
  7411. d20.Campaign.handouts.create({
  7412. name: CONFIG_HANDOUT
  7413. }, {
  7414. success: function (handout) {
  7415. notecontents = "The GM notes contain config options saved between sessions. If you want to wipe your saved settings, delete this handout and reload roll20. If you want to edit your settings, click the \"Edit Config\" button in the <b>Settings</b> (cog) panel.";
  7416.  
  7417. // default settings
  7418. // token settings mimic official content; other settings as vanilla as possible
  7419. const gmnotes = JSON.stringify(d20plus.getDefaultConfig());
  7420.  
  7421. handout.updateBlobs({notes: notecontents, gmnotes: gmnotes});
  7422. handout.save({notes: (new Date).getTime(), inplayerjournals: ""});
  7423.  
  7424. if (nextFn) nextFn();
  7425. }
  7426. });
  7427. },
  7428.  
  7429. getConfigHandout: () => {
  7430. d20plus.getJournalFolderObj(); // ensure journal init
  7431.  
  7432. return d20.Campaign.handouts.models.find(function (handout) {
  7433. return handout.attributes.name === CONFIG_HANDOUT;
  7434. });
  7435. },
  7436.  
  7437. getCfgKey: (group, val) => {
  7438. if (val === undefined || d20plus.config[group] === undefined) return undefined;
  7439. const gr = d20plus.config[group];
  7440. for (const key of Object.keys(d20plus.config[group])) {
  7441. if (gr[key] !== undefined && gr[key] === val) {
  7442. return key;
  7443. }
  7444. }
  7445. return undefined;
  7446. },
  7447.  
  7448. getRawCfgVal: (group, key) => {
  7449. if (d20plus.config[group] === undefined) return undefined;
  7450. if (d20plus.config[group][key] === undefined) return undefined;
  7451. return d20plus.config[group][key];
  7452. },
  7453.  
  7454. getCfgVal: (group, key) => {
  7455. if (d20plus.config[group] === undefined) return undefined;
  7456. if (d20plus.config[group][key] === undefined) return undefined;
  7457. if (CONFIG_OPTIONS[group][key]._type === "_SHEET_ATTRIBUTE") {
  7458. if (!NPC_SHEET_ATTRIBUTES[d20plus.config[group][key]]) return undefined;
  7459. return NPC_SHEET_ATTRIBUTES[d20plus.config[group][key]][d20plus.sheet];
  7460. }
  7461. if (CONFIG_OPTIONS[group][key]._type === "_SHEET_ATTRIBUTE_PC") {
  7462. if (!PC_SHEET_ATTRIBUTES[d20plus.config[group][key]]) return undefined;
  7463. return PC_SHEET_ATTRIBUTES[d20plus.config[group][key]][d20plus.sheet];
  7464. }
  7465. return d20plus.config[group][key];
  7466. },
  7467.  
  7468. getCfgDefaultVal: (group, key) => {
  7469. if (CONFIG_OPTIONS[group] === undefined) return undefined;
  7470. if (CONFIG_OPTIONS[group][key] === undefined) return undefined;
  7471. return CONFIG_OPTIONS[group][key].default
  7472. },
  7473.  
  7474. getCfgEnumVals: (group, key) => {
  7475. if (CONFIG_OPTIONS[group] === undefined) return undefined;
  7476. if (CONFIG_OPTIONS[group][key] === undefined) return undefined;
  7477. return CONFIG_OPTIONS[group][key]._values
  7478. },
  7479.  
  7480. getDefaultConfig: () => {
  7481. const outCpy = {};
  7482. $.each(CONFIG_OPTIONS, (sectK, sect) => {
  7483. outCpy[sectK] = outCpy[sectK] || {};
  7484. $.each(sect, (k, data) => {
  7485. if (!k.startsWith("_")) {
  7486. outCpy[sectK][k] = data.default;
  7487. }
  7488. });
  7489. });
  7490. return outCpy;
  7491. },
  7492.  
  7493. // Helpful for checking if a boolean option is set even if false
  7494. hasCfgVal: (group, key) => {
  7495. if (d20plus.config[group] === undefined) return undefined;
  7496. return d20plus.config[group][key] !== undefined;
  7497. },
  7498.  
  7499. setCfgVal: (group, key, val) => {
  7500. if (d20plus.config[group] === undefined) d20plus.config[group] = {};
  7501. d20plus.config[group][key] = val;
  7502. },
  7503.  
  7504. makeTabPane: ($addTo, headers, content) => {
  7505. if (headers.length !== content.length) throw new Error("Tab header and content length were not equal!");
  7506.  
  7507. if ($addTo.attr("hastabs") !== "YES") {
  7508. const $tabBar = $(`<ul class="nav nav-tabs"/>`);
  7509.  
  7510. const tabList = [];
  7511. const paneList = [];
  7512. const $tabPanes = $(`<div class="tabcontent"/>`);
  7513.  
  7514. $.each(content, (i, e) => {
  7515. const toAdd = $(`<div class="plustab${i} tab-pane" ${i === 0 ? "" : `style="display: none"`}/>`);
  7516. toAdd.append(e);
  7517. paneList[i] = toAdd;
  7518. $tabPanes.append(toAdd);
  7519. });
  7520.  
  7521. $.each(headers, (i, e) => {
  7522. const toAdd = $(`<li ${i === 0 ? `class="active"` : ""}><a data-tab="plustab${i}" href="#">${e}</a></li>`).on("click", () => {
  7523. paneList.forEach((p, i2) => {
  7524. if (i2 === i) {
  7525. tabList[i2].addClass("active");
  7526. paneList[i2].show();
  7527. } else {
  7528. tabList[i2].removeClass("active");
  7529. paneList[i2].hide();
  7530. }
  7531. });
  7532. });
  7533. tabList[i] = (toAdd);
  7534. $tabBar.append(toAdd);
  7535. });
  7536.  
  7537. $addTo
  7538. .append($tabBar)
  7539. .append($tabPanes);
  7540.  
  7541. $addTo.attr("hastabs", "YES");
  7542. }
  7543. },
  7544.  
  7545. openConfigEditor: () => {
  7546. const cEdit = $("#d20plus-configeditor");
  7547. cEdit.dialog("open");
  7548.  
  7549. if (cEdit.attr("hastabs") !== "YES") {
  7550. cEdit.attr("hastabs", "YES");
  7551. const appendTo = $(`<div/>`);
  7552. cEdit.prepend(appendTo);
  7553.  
  7554. const configFields = {};
  7555.  
  7556. const sortedKeys = Object.keys(CONFIG_OPTIONS).sort((a, b) => d20plus.ascSort(CONFIG_OPTIONS[a]._name, CONFIG_OPTIONS[b]._name));
  7557. const tabList = sortedKeys.map(k => CONFIG_OPTIONS[k]._name);
  7558. const contentList = sortedKeys.map(k => makeTab(k));
  7559.  
  7560. function makeTab (cfgK) {
  7561. const cfgGroup = CONFIG_OPTIONS[cfgK];
  7562. configFields[cfgK] = {};
  7563.  
  7564. const content = $(`
  7565. <div class="config-table-wrapper">
  7566. <table class="config-table">
  7567. <thead><tr><th>Property</th><th>Value</th></tr></thead>
  7568. <tbody></tbody>
  7569. </table>
  7570. </div>
  7571. `);
  7572. const tbody = content.find(`tbody`);
  7573.  
  7574. const sortedTabKeys = Object.keys(cfgGroup).filter(k => !k.startsWith("_"));
  7575. sortedTabKeys.forEach((grpK, idx) => {
  7576. const prop = cfgGroup[grpK];
  7577.  
  7578. // IDs only used for label linking
  7579. const toAdd = $(`<tr><td><label for="conf_field_${idx}" class="config-name">${prop.name}</label></td></tr>`);
  7580.  
  7581. // Each config `_type` should have a case here. Each case should add a function to the map [configFields:[cfgK:grpK]]. These functions should return the value of the input.
  7582. switch (prop._type) {
  7583. case "boolean": {
  7584. const field = $(`<input type="checkbox" id="conf_field_${idx}" ${d20plus.getCfgVal(cfgK, grpK) ? `checked` : ""}>`);
  7585.  
  7586. configFields[cfgK][grpK] = () => {
  7587. return field.prop("checked")
  7588. };
  7589.  
  7590. const td = $(`<td/>`).append(field);
  7591. toAdd.append(td);
  7592. break;
  7593. }
  7594. case "String": {
  7595. const curr = d20plus.getCfgVal(cfgK, grpK) || "";
  7596. const def = d20plus.getCfgDefaultVal(cfgK, grpK) || "";
  7597. const field = $(`<input id="conf_field_${idx}" value="${curr}" ${def ? `placeholder="Default: ${def}"` : ""}>`);
  7598.  
  7599. configFields[cfgK][grpK] = () => {
  7600. return field.val() ? field.val().trim() : "";
  7601. };
  7602.  
  7603. const td = $(`<td/>`).append(field);
  7604. toAdd.append(td);
  7605. break;
  7606. }
  7607. case "_SHEET_ATTRIBUTE_PC":
  7608. case "_SHEET_ATTRIBUTE": {
  7609. const DICT = prop._type === "_SHEET_ATTRIBUTE" ? NPC_SHEET_ATTRIBUTES : PC_SHEET_ATTRIBUTES;
  7610. const sortedNpcsAttKeys = Object.keys(DICT).sort((at1, at2) => d20plus.ascSort(DICT[at1].name, DICT[at2].name));
  7611. const field = $(`<select id="conf_field_${idx}" class="cfg_grp_${cfgK}" data-item="${grpK}">${sortedNpcsAttKeys.map(npcK => `<option value="${npcK}">${DICT[npcK].name}</option>`)}</select>`);
  7612. const cur = d20plus.getCfgVal(cfgK, grpK);
  7613. if (cur !== undefined) {
  7614. field.val(cur);
  7615. }
  7616.  
  7617. configFields[cfgK][grpK] = () => {
  7618. return field.val()
  7619. };
  7620.  
  7621. const td = $(`<td/>`).append(field);
  7622. toAdd.append(td);
  7623. break;
  7624. }
  7625. case "integer": {
  7626. const def = d20plus.getCfgDefaultVal(cfgK, grpK);
  7627. const field = $(`<input id="conf_field_${idx}" type="number" value="${d20plus.getCfgVal(cfgK, grpK)}" ${def != null ? `placeholder="Default: ${def}"` : ""}>`);
  7628.  
  7629. configFields[cfgK][grpK] = () => {
  7630. return Number(field.val());
  7631. };
  7632.  
  7633. const td = $(`<td/>`).append(field);
  7634. toAdd.append(td);
  7635. break;
  7636. }
  7637. case "_FORMULA": {
  7638. const $field = $(`<select id="conf_field_${idx}" class="cfg_grp_${cfgK}" data-item="${grpK}">${d20plus.formulas._options.sort().map(opt => `<option value="${opt}">${opt}</option>`)}</select>`);
  7639.  
  7640. const cur = d20plus.getCfgVal(cfgK, grpK);
  7641. if (cur !== undefined) {
  7642. $field.val(cur);
  7643. }
  7644.  
  7645. configFields[cfgK][grpK] = () => {
  7646. return $field.val();
  7647. };
  7648.  
  7649. const td = $(`<td/>`).append($field);
  7650. toAdd.append(td);
  7651. break;
  7652. }
  7653. case "_WHISPERMODE": {
  7654. const $field = $(`<select id="conf_field_${idx}" class="cfg_grp_${cfgK}" data-item="${grpK}">${d20plus.whisperModes.map(mode => `<option value="${mode}">${mode}</option>`)}</select>`);
  7655.  
  7656. const cur = d20plus.getCfgVal(cfgK, grpK);
  7657. if (cur !== undefined) {
  7658. $field.val(cur);
  7659. }
  7660.  
  7661. configFields[cfgK][grpK] = () => {
  7662. return $field.val();
  7663. };
  7664.  
  7665. const td = $(`<td/>`).append($field);
  7666. toAdd.append(td);
  7667. break;
  7668. }
  7669. case "_ADVANTAGEMODE": {
  7670. const $field = $(`<select id="conf_field_${idx}" class="cfg_grp_${cfgK}" data-item="${grpK}">${d20plus.advantageModes.map(mode => `<option value="${mode}">${mode}</option>`)}</select>`);
  7671.  
  7672. const cur = d20plus.getCfgVal(cfgK, grpK);
  7673. if (cur !== undefined) {
  7674. $field.val(cur);
  7675. }
  7676.  
  7677. configFields[cfgK][grpK] = () => {
  7678. return $field.val();
  7679. };
  7680.  
  7681. const td = $(`<td/>`).append($field);
  7682. toAdd.append(td);
  7683. break;
  7684. }
  7685. case "_DAMAGEMODE": {
  7686. const $field = $(`<select id="conf_field_${idx}" class="cfg_grp_${cfgK}" data-item="${grpK}">${d20plus.damageModes.map(mode => `<option value="${mode}">${mode}</option>`)}</select>`);
  7687.  
  7688. const cur = d20plus.getCfgVal(cfgK, grpK);
  7689. if (cur !== undefined) {
  7690. $field.val(cur);
  7691. }
  7692.  
  7693. configFields[cfgK][grpK] = () => {
  7694. return $field.val();
  7695. };
  7696.  
  7697. const td = $(`<td/>`).append($field);
  7698. toAdd.append(td);
  7699. break;
  7700. }
  7701. case "_enum": { // for generic String enums not covered above
  7702. const $field = $(`<select id="conf_field_${idx}" class="cfg_grp_${cfgK}" data-item="${grpK}">${d20plus.getCfgEnumVals(cfgK, grpK).map(it => `<option value="${it}">${it}</option>`)}</select>`);
  7703.  
  7704. const cur = d20plus.getCfgVal(cfgK, grpK);
  7705. if (cur !== undefined) {
  7706. $field.val(cur);
  7707. } else {
  7708. const def = d20plus.getCfgDefaultVal(cfgK, grpK);
  7709. if (def !== undefined) {
  7710. $field.val(def);
  7711. }
  7712. }
  7713.  
  7714. configFields[cfgK][grpK] = () => {
  7715. return $field.val();
  7716. };
  7717.  
  7718. const td = $(`<td/>`).append($field);
  7719. toAdd.append(td);
  7720. break;
  7721. }
  7722. }
  7723. tbody.append(toAdd);
  7724. });
  7725.  
  7726. return content;
  7727. }
  7728.  
  7729. d20plus.makeTabPane(
  7730. appendTo,
  7731. tabList,
  7732. contentList
  7733. );
  7734.  
  7735. const saveButton = $(`#configsave`);
  7736. saveButton.unbind("click");
  7737. saveButton.bind("click", () => {
  7738. let handout = d20plus.getConfigHandout();
  7739. if (!handout) {
  7740. d20plus.makeDefaultConfig(doSave);
  7741. } else {
  7742. doSave();
  7743. }
  7744.  
  7745. function doSave () {
  7746. $.each(configFields, (cfgK, grp) => {
  7747. $.each(grp, (grpK, grpVField) => {
  7748. d20plus.setCfgVal(cfgK, grpK, grpVField());
  7749. })
  7750. });
  7751.  
  7752. const gmnotes = JSON.stringify(d20plus.config).replace(/%/g, "%25");
  7753. handout.updateBlobs({gmnotes: gmnotes});
  7754. handout.save({notes: (new Date).getTime()});
  7755.  
  7756. d20plus.log("Saved config");
  7757.  
  7758. d20plus.baseHandleConfigChange();
  7759. if (d20plus.handleConfigChange) d20plus.handleConfigChange();
  7760. }
  7761. });
  7762. }
  7763. },
  7764.  
  7765. _handleStatusTokenConfigChange: () => {
  7766. if (window.is_gm) {
  7767. if (d20plus.getCfgVal("token", "enhanceStatus")) {
  7768. const sheetUrl = d20plus.getCfgVal("token", "statusSheetUrl") || d20plus.getCfgDefaultVal("token", "statusSheetUrl");
  7769. const sheetSmallUrl = d20plus.getCfgVal("token", "statusSheetSmallUrl") || d20plus.getCfgDefaultVal("token", "statusSheetSmallUrl");
  7770.  
  7771. window.Campaign && window.Campaign.save({
  7772. "bR20cfg_statussheet": sheetUrl,
  7773. "bR20cfg_statussheet_small": sheetSmallUrl
  7774. });
  7775.  
  7776. d20.token_editor.statussheet.src = sheetUrl;
  7777. d20.token_editor.statussheet_small.src = sheetSmallUrl;
  7778. d20plus._removeStatusEffectEntries(); // clean up any old data
  7779. d20plus._addStatusEffectEntries();
  7780. } else {
  7781. window.Campaign && window.Campaign.save({
  7782. "bR20cfg_statussheet": "",
  7783. "bR20cfg_statussheet_small": ""
  7784. });
  7785.  
  7786. d20.token_editor.statussheet.src = "/images/statussheet.png";
  7787. d20.token_editor.statussheet_small.src = "/images/statussheet_small.png";
  7788. d20plus._removeStatusEffectEntries();
  7789. }
  7790. } else {
  7791. if (window.Campaign && window.Campaign.attributes && window.Campaign.attributes.bR20cfg_statussheet && window.Campaign.attributes.bR20cfg_statussheet_small) {
  7792. d20.token_editor.statussheet.src = window.Campaign.attributes.bR20cfg_statussheet;
  7793. d20.token_editor.statussheet_small.src = window.Campaign.attributes.bR20cfg_statussheet_small;
  7794. d20plus._addStatusEffectEntries();
  7795. } else {
  7796. d20.token_editor.statussheet.src = "/images/statussheet.png";
  7797. d20.token_editor.statussheet_small.src = "/images/statussheet_small.png";
  7798. d20plus._removeStatusEffectEntries();
  7799. }
  7800. }
  7801. },
  7802.  
  7803. baseHandleConfigChange: () => {
  7804. d20plus._handleStatusTokenConfigChange();
  7805. },
  7806.  
  7807. startPlayerConfigHandler: () => {
  7808. function handlePlayerCfg () {
  7809. d20plus.baseHandleConfigChange();
  7810. if (d20plus.handleConfigChange) d20plus.handleConfigChange();
  7811. }
  7812.  
  7813. // every 5 seconds, poll and apply any config changes the GM might have made
  7814. if (!window.is_gm) {
  7815. setInterval(() => {
  7816. handlePlayerCfg();
  7817. }, 5000);
  7818. }
  7819. handlePlayerCfg();
  7820. },
  7821.  
  7822. // SETTINGS TOOLS //////////////////////////////////////////////////////////////////////////////////////////////
  7823. tools: [
  7824. {
  7825. name: "Journal Cleaner",
  7826. desc: "Quickly select and delete journal items from the root folder, useful for cleaning up loose items after deleting a folder.",
  7827. html: `
  7828. <div id="d20plus-quickdelete" title="Journal Root Cleaner">
  7829. <p>A list of characters and handouts in the journal folder root, which allows them to be quickly deleted.</p>
  7830. <p style="display: flex; justify-content: space-between"><label><input type="checkbox" title="Select all" id="deletelist-selectall"> Select All</label> <a class="btn" href="#" id="quickdelete-btn-submit">Delete Selected</a></p>
  7831. <div id="delete-list-container">
  7832. <input class="search" autocomplete="off" placeholder="Search list..." style="width: 100%;">
  7833. <br><br>
  7834. <ul class="list deletelist" style="max-height: 600px; overflow-y: scroll; display: block; margin: 0;"></ul>
  7835. </div>
  7836. </div>;
  7837. `,
  7838. dialogFn: () => {
  7839. $("#d20plus-quickdelete").dialog({
  7840. autoOpen: false,
  7841. resizable: true,
  7842. width: 800,
  7843. height: 650,
  7844. });
  7845. },
  7846. openFn: () => {
  7847. const $win = $("#d20plus-quickdelete");
  7848. $win.dialog("open");
  7849.  
  7850. const journal = d20plus.getJournalFolderObj();
  7851. const rootItems = [];
  7852. journal.forEach(it => {
  7853. if (it.i) return; // skip folders
  7854. const handout = d20.Campaign.handouts.get(it);
  7855. if (handout && (handout.get("name") === CONFIG_HANDOUT || handout.get("name") === ART_HANDOUT)) return; // skip 5etools handouts
  7856. const character = d20.Campaign.characters.get(it);
  7857. if (handout) rootItems.push({type: "handouts", id: it, name: handout.get("name")});
  7858. if (character) rootItems.push({type: "characters", id: it, name: character.get("name")});
  7859. });
  7860.  
  7861. const $delList = $win.find(`.list`);
  7862. $delList.empty();
  7863.  
  7864. rootItems.forEach((it, i) => {
  7865. $delList.append(`
  7866. <label class="import-cb-label">
  7867. <input type="checkbox" data-listid="${i}">
  7868. <span class="name">${it.name}</span>
  7869. </label>
  7870. `);
  7871. });
  7872.  
  7873. // init list library
  7874. const delList = new List("delete-list-container", {
  7875. valueNames: ["name"],
  7876. listClass: "deletelist"
  7877. });
  7878.  
  7879. const $cbAll = $("#deletelist-selectall");
  7880. $cbAll.unbind("click");
  7881. $cbAll.prop("checked", false);
  7882. $cbAll.bind("click", function () {
  7883. d20plus.importer._importToggleSelectAll(delList, $cbAll);
  7884. });
  7885.  
  7886. const $btnDel = $(`#quickdelete-btn-submit`);
  7887. $btnDel.off("click");
  7888. $btnDel.on("click", () => {
  7889. if (confirm("Delete selected?")) {
  7890. delList.items.forEach(it => Array.prototype.forEach.call(it.elm.children, (e) => {
  7891. const $e = $(e);
  7892. if ($e.is("input") && $e.prop("checked")) {
  7893. const dataIndex = parseInt($e.data("listid"));
  7894. const toDel = rootItems[dataIndex];
  7895. d20.Campaign[toDel.type].get(toDel.id).destroy();
  7896. }
  7897. }));
  7898. $win.dialog("close");
  7899. $("#journalfolderroot").trigger("change");
  7900. }
  7901. });
  7902. }
  7903. },
  7904. {
  7905. name: "SVG Draw",
  7906. desc: "Paste SVG data as text to automatically draw the paths.",
  7907. html: `
  7908. <div id="d20plus-svgdraw" title="SVG Drawing Tool">
  7909. <p>Paste SVG data as text to automatically draw any included &lt;path&gt;s. Draws to the current layer, in the top-left corner, with no scaling. Takes colour information from &quot;stroke&quot; attributes.</p>
  7910. <p>Line width (px; default values are 1, 3, 5, 8, 14): <input name="stroke-width" placeholder="5" value="5" type="number"></p>
  7911. <textarea rows="10" cols="100" placeholder="Paste SVG data here"></textarea>
  7912. <br>
  7913. <button class="btn">Draw</button>
  7914. </div>
  7915. `,
  7916. dialogFn: () => {
  7917. $("#d20plus-svgdraw").dialog({
  7918. autoOpen: false,
  7919. resizable: true,
  7920. width: 800,
  7921. height: 650,
  7922. });
  7923. },
  7924. openFn: () => {
  7925. // adapted from `d20.engine.finishCurrentPolygon`
  7926. function addShape(path, pathStroke, strokeWidth) {
  7927. let i = d20.engine.convertAbsolutePathStringtoFabric(path);
  7928. i = _.extend(i, {
  7929. strokeWidth: strokeWidth,
  7930. fill: "transparent",
  7931. stroke: pathStroke,
  7932. path: JSON.parse(i.path)
  7933. });
  7934. d20.Campaign.activePage().addPath(i);
  7935. d20.engine.debounced_renderTop();
  7936. }
  7937.  
  7938. const $win = $("#d20plus-svgdraw");
  7939. $win.dialog("open");
  7940.  
  7941. $win.find(`button`).off("click").on("click", () => {
  7942. d20plus.log("Drawing paths");
  7943. const input = $win.find(`textarea`).val();
  7944. const svg = $.parseXML(input);
  7945.  
  7946. const toDraw = $(svg).find("path").map((i, e) => {
  7947. const $e = $(e);
  7948. return {stroke: $e.attr("stroke") || "black", d: $e.attr("d")}
  7949. }).get();
  7950.  
  7951. const strokeWidth = Math.max(1, Number($win.find(`input[name="stroke-width"]`).val()));
  7952.  
  7953. toDraw.forEach(it => {
  7954. addShape(it.d, it.stroke, strokeWidth)
  7955. });
  7956. });
  7957. }
  7958. },
  7959. {
  7960. name: "Multi-Whisper",
  7961. desc: "Send whispers to multiple players ",
  7962. html: `
  7963. <div id="d20plus-whispers" title="Multi-Whisper Tool">
  7964. <div>
  7965. <button class="btn toggle-dc">Show Disconnected Players</button>
  7966. <button class="btn send-all">Send All Messages</button>
  7967. </div>
  7968. <hr>
  7969. <div class="messages" style="max-height: 600px; overflow-y: auto; overflow-x: hidden; transform: translateZ(0)">
  7970. <!-- populate with JS -->
  7971. </div>
  7972. </div>
  7973. `,
  7974. dialogFn: () => {
  7975. $("#d20plus-whispers").dialog({
  7976. autoOpen: false,
  7977. resizable: true,
  7978. width: 1000,
  7979. height: 760,
  7980. });
  7981. },
  7982. openFn: () => {
  7983. $("a.ui-tabs-anchor[href='#textchat']").trigger("click");
  7984.  
  7985. const $win = $("#d20plus-whispers");
  7986. $win.dialog("open");
  7987.  
  7988. const $btnToggleDc = $win.find(`.toggle-dc`).off("click").text("Show Disconnected Players");
  7989. const $btnSendAll = $win.find(`.send-all`).off("click");
  7990.  
  7991. const $pnlMessages = $win.find(`.messages`).empty();
  7992. const players = d20.Campaign.players.toJSON();
  7993. players.forEach((p, i) => {
  7994. const $btnSend = $(`<button class="btn send">Send</button>`).on("click", function () {
  7995. const $btn = $(this);
  7996. const $wrp = $btn.closest(`.wrp-message`);
  7997. const toMsg = $wrp.find(`input[data-player-id]:checked`).filter(":visible").map((ii, e) => $(e).attr("data-player-id")).get();
  7998. const content = $wrp.find(`.message`).val().trim();
  7999. toMsg.forEach(targetId => {
  8000. d20.textchat.doChatInput(`/w ${d20.Campaign.players.get(targetId).get("displayname").split(" ")[0]} ${content}`);
  8001.  
  8002. // This only posts to local player's chat, sadly
  8003. // d20.textchat.incoming(
  8004. // false,
  8005. // {
  8006. // avatar: `/users/avatar/${window.currentPlayer.get("d20userid")}/30`,
  8007. // who: d20.textchat.$speakingas.find("option:first-child").text(),
  8008. // type: "whisper",
  8009. // content: content,
  8010. // playerid: window.currentPlayer.id,
  8011. // id: d20plus.generateRowId(),
  8012. // target: targetId,
  8013. // target_name: d20.Campaign.players.get(targetId).get("displayname") || ""
  8014. // }
  8015. // );
  8016. })
  8017. });
  8018.  
  8019. $pnlMessages.append($(`
  8020. <div ${p.online || `style="display: none;"`} data-online="${p.online}" class="wrp-message">
  8021. <div>
  8022. ${players.map((pp, ii) => `<label style="margin-right: 10px; ${pp.online || ` display: none;`}" data-online="${pp.online}" class="display-inline-block">${pp.displayname} <input data-player-id="${pp.id}" type="checkbox" ${i === ii ? `checked="true"` : ""}></label>`).join("")}
  8023. </div>
  8024. <textarea style="display: block; width: 95%;" placeholder="Enter whisper" class="message"></textarea>
  8025. </div>
  8026. `).append($btnSend).append(`<hr>`));
  8027. });
  8028.  
  8029. $btnToggleDc.on("click", () => {
  8030. $btnToggleDc.text($btnToggleDc.text().startsWith("Show") ? "Hide Disconnected Players" : "Show Disconnected Players");
  8031. $pnlMessages.find(`[data-online="false"]`).toggle();
  8032. });
  8033.  
  8034. $btnSendAll.on("click", () => {
  8035. $pnlMessages.find(`button.send`).click();
  8036. });
  8037. }
  8038. }
  8039. ],
  8040.  
  8041. addTools: () => {
  8042. const $body = $(`body`);
  8043. const $tools = $(`#d20-tools-list`);
  8044. const $toolsList = $tools.find(`.tools-list`);
  8045. d20plus.tools.forEach(t => {
  8046. $body.append(t.html); // add HTML
  8047. t.dialogFn(); // init window
  8048. // add tool row
  8049. const $wrp = $(`<div class="tool-row"/>`);
  8050. $wrp.append(`<p style="width: 20%;">${t.name}</p>`);
  8051. $wrp.append(`<p style="width: 60%;">${t.desc}</p>`);
  8052. $(`<a style="width: 15%;" class="btn" href="#">Open</a>`).on(mousedowntype, () => {
  8053. t.openFn();
  8054. $tools.dialog("close");
  8055. }).appendTo($wrp);
  8056. $toolsList.append($wrp);
  8057. });
  8058.  
  8059. $tools.dialog({
  8060. autoOpen: false,
  8061. resizable: true,
  8062. width: 800,
  8063. height: 650,
  8064. });
  8065. $(`#button-view-tools`).on(mousedowntype, () => {
  8066. $tools.dialog("open");
  8067. });
  8068. },
  8069.  
  8070. // ART /////////////////////////////////////////////////////////////////////////////////////////////////////////////
  8071. art: {
  8072. button: () => {
  8073. // add external art button was clicked
  8074. const $art = $("#d20plus-artfolder");
  8075. $art.dialog("open");
  8076. const $artList = $art.find(`.list`);
  8077. $artList.empty();
  8078.  
  8079. if (d20plus.art.custom) {
  8080. d20plus.art.custom.forEach(a => {
  8081. const $liArt = getArtLi(a.name, a.url);
  8082. $artList.append($liArt);
  8083. });
  8084. }
  8085.  
  8086. // init list library
  8087. const artList = new List("art-list-container", {
  8088. valueNames: ["name"],
  8089. listClass: "artlist"
  8090. });
  8091.  
  8092. const $btnAdd = $(`#art-list-add-btn`);
  8093. const $iptAddName = $(`#art-list-add-name`);
  8094. const $iptAddUrl = $(`#art-list-add-url`);
  8095. $btnAdd.off("click");
  8096. $btnAdd.on("click", () => {
  8097. const name = $iptAddName.val().trim();
  8098. const url = $iptAddUrl.val().trim();
  8099. if (!name || !url) {
  8100. alert("Missing required fields!")
  8101. } else {
  8102. artList.search();
  8103. artList.filter();
  8104. const $liArt = getArtLi(name, url);
  8105. $artList.append($liArt);
  8106. refreshCustomArtList();
  8107. }
  8108. });
  8109.  
  8110. const $btnMassAdd = $(`#art-list-multi-add-btn`);
  8111. $btnMassAdd.off("click");
  8112. $btnMassAdd.on("click", () => {
  8113. $("#d20plus-artmassadd").dialog("open");
  8114. const $btnMassAddSubmit = $(`#art-list-multi-add-btn-submit`);
  8115. $btnMassAddSubmit.off("click");
  8116. $btnMassAddSubmit.on("click", () => {
  8117. artList.search();
  8118. artList.filter();
  8119. const $iptUrls = $(`#art-list-multi-add-area`);
  8120. const massUrls = $iptUrls.val();
  8121. const spl = massUrls.split("\n").map(s => s.trim()).filter(s => s);
  8122. if (!spl.length) return;
  8123. else {
  8124. const delim = "---";
  8125. const toAdd = [];
  8126. for (const s of spl) {
  8127. if (!s.includes(delim)) {
  8128. alert(`Badly formatted line: ${s}`)
  8129. return;
  8130. } else {
  8131. const parts = s.split(delim);
  8132. if (parts.length !== 2) {
  8133. alert(`Badly formatted line: ${s}`)
  8134. return;
  8135. } else {
  8136. toAdd.push({
  8137. name: parts[0],
  8138. url: parts[1]
  8139. });
  8140. }
  8141. }
  8142. }
  8143. toAdd.forEach(a => {
  8144. $artList.append(getArtLi(a.name, a.url));
  8145. });
  8146. refreshCustomArtList();
  8147. $("#d20plus-artmassadd").dialog("close");
  8148. }
  8149. });
  8150. });
  8151.  
  8152. makeDraggables();
  8153. d20plus.art.refreshList = refreshCustomArtList;
  8154.  
  8155. function getArtLi (name, url) {
  8156. const showImage = d20plus.getCfgVal("interface", "showCustomArtPreview");
  8157. const $liArt = $(`
  8158. <li class="dd-item library-item draggableresult Vetools-draggable-art ui-draggable" data-fullsizeurl="${url}">
  8159. ${showImage ? `<img src="${url}" style="width: 30px; max-height: 30px; display: inline-block" draggable="false">` : ""}
  8160. <div class="dd-content name" style="display: inline-block; width: 35%;" data-url="${url}">${name}</div>
  8161. <a href="${url}"><span class="url" style="display: inline-block; width: ${showImage ? "40%" : "55%"};">${url}</span></a>
  8162. </li>
  8163. `);
  8164. if (!showImage) {
  8165. $liArt.on("mousedown", () => {
  8166. const $loader = $(`<div class="temp-warning">Loading image - don't drop yet!</div>`);
  8167. const $img = $(`<img src="${url}" style="width: 30px; max-height: 30px; display: none">`);
  8168. if (!$img.prop("complete")) {
  8169. $(`body`).append($loader);
  8170. $img.on("load", () => {
  8171. $loader.remove();
  8172. });
  8173. $loader.append($img);
  8174. }
  8175. });
  8176. }
  8177.  
  8178. const $btnDel = $(`<span class="delete btn btn-danger"><span class="pictos">#</span></span>`).on("click", () => {
  8179. $liArt.remove();
  8180. refreshCustomArtList();
  8181. });
  8182. $liArt.append($btnDel);
  8183. return $liArt;
  8184. }
  8185.  
  8186. function refreshCustomArtList () {
  8187. artList.reIndex();
  8188. const custom = [];
  8189. artList.items.forEach(i => {
  8190. const $ele = $(i.elm);
  8191. custom.push({
  8192. name: $ele.find(`.name`).text(),
  8193. url: $ele.find(`.url`).text()
  8194. });
  8195. });
  8196. d20plus.art.custom = custom;
  8197. makeDraggables();
  8198. saveToHandout();
  8199. }
  8200.  
  8201. function makeDraggables () {
  8202. $(`.Vetools-draggable-art`).draggable({
  8203. handle: ".dd-content",
  8204. revert: true,
  8205. revertDuration: 0,
  8206. helper: "clone",
  8207. appendTo: "body"
  8208. })
  8209. }
  8210.  
  8211. function saveToHandout () {
  8212. const handout = d20plus.getArtHandout();
  8213. if (!handout) {
  8214. d20.Campaign.handouts.create({
  8215. name: ART_HANDOUT
  8216. }, {
  8217. success: function (handout) {
  8218. notecontents = "This handout is used to store custom art URLs."
  8219.  
  8220. const gmnotes = JSON.stringify(d20plus.art.custom);
  8221. handout.updateBlobs({notes: notecontents, gmnotes: gmnotes});
  8222. handout.save({notes: (new Date).getTime(), inplayerjournals: ""});
  8223. }
  8224. });
  8225. } else {
  8226. const gmnotes = JSON.stringify(d20plus.art.custom);
  8227. handout.updateBlobs({gmnotes: gmnotes});
  8228. handout.save({notes: (new Date).getTime()});
  8229. }
  8230. }
  8231. },
  8232.  
  8233. // TODO load a decent default art library from somewhere
  8234. default: [
  8235. {
  8236. name: "Phoenix",
  8237. url: "http://www.discgolfbirmingham.com/wordpress/wp-content/uploads/2014/04/phoenix-rising.jpg"
  8238. }
  8239. ]
  8240. },
  8241.  
  8242. getArtHandout: () => {
  8243. return d20.Campaign.handouts.models.find((handout) => {
  8244. return handout.attributes.name === ART_HANDOUT;
  8245. });
  8246. },
  8247.  
  8248. loadArt: (nextFn) => {
  8249. d20plus.log("Loading custom art");
  8250. const handout = d20plus.getArtHandout();
  8251. if (handout) {
  8252. handout.view.render();
  8253. handout._getLatestBlob("gmnotes", function (gmnotes) {
  8254. const decoded = decodeURIComponent(gmnotes);
  8255. try {
  8256. d20plus.art.custom = JSON.parse(decoded);
  8257. nextFn();
  8258. } catch (e) {
  8259. nextFn();
  8260. }
  8261. });
  8262. } else {
  8263. nextFn();
  8264. }
  8265. },
  8266.  
  8267. addCustomArtSearch: () => {
  8268. d20plus.log("Add custom art search");
  8269. const $afterTo = $(`#libraryresults`);
  8270. $afterTo.after(d20plus.artListHTML);
  8271.  
  8272. const $olNone = $(`#image-search-none`);
  8273. const $olHasResults = $(`#image-search-has-results`);
  8274.  
  8275. const $olArt = $(`#custom-art-results`);
  8276. const $srchImages = $(`#imagedialog .searchbox input.keywords`);
  8277. $srchImages.on("keyup", () => {
  8278. $olArt.empty();
  8279. const searched = $srchImages.val().trim().toLowerCase();
  8280. if (searched.length === 0) {
  8281. $olNone.show();
  8282. $olHasResults.hide();
  8283. return;
  8284. }
  8285.  
  8286. let toShow = d20plus.art.default.filter(a => a.name.toLowerCase().includes(searched));
  8287. if (d20plus.art.custom) toShow = toShow.concat(d20plus.art.custom.filter(a => a.name.toLowerCase().includes(searched)));
  8288.  
  8289. if (!toShow.length) {
  8290. $olNone.show();
  8291. $olHasResults.hide();
  8292. } else {
  8293. $olNone.hide();
  8294. $olHasResults.show();
  8295.  
  8296. toShow.forEach(a => {
  8297. $olArt.append(`
  8298. <li class="dd-item library-item draggableresult Vetoolsresult ui-draggable" data-fullsizeurl="${a.url}">
  8299. <div class="dd-content">
  8300. <div class="token"><img src="${a.url}" draggable="false"></div>
  8301. <div class="name">
  8302. <div class="namecontainer"><a href="${a.url}" rel="external">${a.name}</a></div>
  8303. </div>
  8304. </div>
  8305. </li>
  8306. `);
  8307. });
  8308. }
  8309.  
  8310. $("#imagedialog #Vetoolsresults .draggableresult").draggable({
  8311. handle: ".dd-content",
  8312. revert: true,
  8313. revertDuration: 0,
  8314. helper: "clone",
  8315. appendTo: "body"
  8316. }).addTouch();
  8317. });
  8318. },
  8319.  
  8320. initArtFromUrlButtons: () => {
  8321. d20plus.log("Add direct URL art buttons");
  8322. $("#tmpl_charactereditor").replaceWith(d20plus.template_charactereditor);
  8323. $("#tmpl_handouteditor").replaceWith(d20plus.template_handouteditor);
  8324.  
  8325. $(`.character-image-by-url`).live("click", function () {
  8326. const cId = $(this).closest(`[data-characterid]`).attr(`data-characterid`);
  8327. const url = window.prompt("Enter a URL", "https://example.com/pic.png");
  8328. if (url) {
  8329. d20.Campaign.characters.get(cId).set("avatar", url);
  8330. }
  8331. });
  8332.  
  8333. $(`.handout-image-by-url`).live("click", function () {
  8334. const hId = $(this).closest(`[data-handoutid]`).attr(`data-handoutid`);
  8335. const url = window.prompt("Enter a URL", "https://example.com/pic.png");
  8336. if (url) {
  8337. d20.Campaign.handouts.get(hId).set("avatar", url);
  8338. }
  8339. });
  8340. },
  8341.  
  8342. // UI ENHANCEMENTS /////////////////////////////////////////////////////////////////////////////////////////////////
  8343.  
  8344. addProFeatures: () => {
  8345. d20plus.log("Add Pro features");
  8346.  
  8347. // modified to allow players to use the FX tool, and to keep current colour selections when switching tool
  8348. // BEGIN ROLL20 CODE
  8349. function setMode (e) {
  8350. d20plus.log("Setting mode " + e);
  8351. // BEGIN MOD
  8352. // "text" === e || "rect" === e || "polygon" === e || "path" === e || "pan" === e || "select" === e || "targeting" === e || "measure" === e || window.is_gm || (e = "select"),
  8353. // END MOD
  8354. "text" == e ? $("#editor").addClass("texteditmode") : $("#editor").removeClass("texteditmode"),
  8355. $("#floatingtoolbar li").removeClass("activebutton"),
  8356. $("#" + e).addClass("activebutton"),
  8357. "fog" == e.substring(0, 3) && $("#fogcontrols").addClass("activebutton"),
  8358. "rect" == e && ($("#drawingtools").addClass("activebutton"),
  8359. $("#drawingtools").removeClass("text path polygon").addClass("rect")),
  8360. "text" == e && ($("#drawingtools").addClass("activebutton"),
  8361. $("#drawingtools").removeClass("rect path polygon").addClass("text")),
  8362. "path" == e && $("#drawingtools").addClass("activebutton").removeClass("text rect polygon").addClass("path"),
  8363. "polygon" == e ? $("#drawingtools").addClass("activebutton").removeClass("text rect path").addClass("polygon") : d20.engine.finishCurrentPolygon(),
  8364. "pan" !== e && "select" !== e && d20.engine.unselect(),
  8365. "pan" == e ? ($("#select").addClass("pan").removeClass("select").addClass("activebutton"),
  8366. d20.token_editor.removeRadialMenu(),
  8367. $("#editor-wrapper").addClass("panning")) : $("#editor-wrapper").removeClass("panning"),
  8368. "select" == e && $("#select").addClass("select").removeClass("pan").addClass("activebutton"),
  8369. $("#floatingtoolbar .mode").hide(),
  8370. ("text" == e || "select" == e) && $("#floatingtoolbar ." + e).show(),
  8371. "gridalign" == e ? $("#gridaligninstructions").show() : "gridalign" === d20.engine.mode && $("#gridaligninstructions").hide(),
  8372. "targeting" === e ? ($("#targetinginstructions").show(),
  8373. $("#upperCanvas").addClass("targeting"),
  8374. d20.engine.canvas.hoverCursor = "crosshair") : "targeting" === d20.engine.mode && ($("#targetinginstructions").hide(),
  8375. $("#upperCanvas").removeClass("targeting"),
  8376. d20.engine.nextTargetCallback && _.defer(function () {
  8377. d20.engine.nextTargetCallback && d20.engine.nextTargetCallback(!1)
  8378. }),
  8379. d20.engine.canvas.hoverCursor = "move"),
  8380. console.log("Switch mode to " + e),
  8381. // BEGIN MOD
  8382. d20.engine.mode = e;
  8383. d20.engine.canvas.isDrawingMode = "path" == e ? !0 : !1;
  8384. if ("text" == e || "path" == e || "rect" == e || "polygon" == e || "fxtools" == e) {
  8385. $("#secondary-toolbar").show();
  8386. $("#secondary-toolbar .mode").hide();
  8387. $("#secondary-toolbar ." + e).show();
  8388. ("path" == e || "rect" == e || "polygon" == e) && ("" === $("#path_strokecolor").val() && ($("#path_strokecolor").val("#000000").trigger("change-silent"),
  8389. $("#path_fillcolor").val("transparent").trigger("change-silent")),
  8390. d20.engine.canvas.freeDrawingBrush.color = $("#path_strokecolor").val(),
  8391. d20.engine.canvas.freeDrawingBrush.fill = $("#path_fillcolor").val() || "transparent",
  8392. $("#path_width").trigger("change")),
  8393. "fxtools" == e && "" === $("#fxtools_color").val() && $("#fxtools_color").val("#a61c00").trigger("change-silent"),
  8394. $("#floatingtoolbar").trigger("blur")
  8395. } else {
  8396. $("#secondary-toolbar").hide();
  8397. $("#floatingtoolbar").trigger("blur");
  8398. }
  8399. // END MOD
  8400. // END ROLL20 CODE
  8401. }
  8402.  
  8403. d20plus.setMode = setMode;
  8404.  
  8405. // rebind buttons with new setMode
  8406. const $drawTools = $("#drawingtools");
  8407. const $rect = $drawTools.find(".chooserect");
  8408. const $path = $drawTools.find(".choosepath");
  8409. const $poly = $drawTools.find(".choosepolygon");
  8410. $drawTools.unbind(clicktype).bind(clicktype, () => {
  8411. $(this).hasClass("rect") ? setMode("rect") : $(this).hasClass("text") ? setMode("text") : $(this).hasClass("path") ? setMode("path") : $(this).hasClass("drawselect") ? setMode("drawselect") : $(this).hasClass("polygon") && setMode("polygon")
  8412. });
  8413. $rect.unbind(clicktype).bind(clicktype, () => {
  8414. setMode("rect");
  8415. return false;
  8416. });
  8417. $path.unbind(clicktype).bind(clicktype, () => {
  8418. setMode("path");
  8419. return false;
  8420. });
  8421. $poly.unbind(clicktype).bind(clicktype, () => {
  8422. setMode("polygon");
  8423. return false;
  8424. });
  8425. $("#rect").unbind(clicktype).bind(clicktype, () => setMode("rect"));
  8426. $("#path").unbind(clicktype).bind(clicktype, () => setMode("path"));
  8427.  
  8428. if (!$(`#fxtools`).length) {
  8429. const $fxMode = $(`<li id="fxtools"/>`).append(`<span class="pictos">e</span>`);
  8430. $fxMode.on("click", () => {
  8431. d20plus.setMode("fxtools");
  8432. });
  8433. $(`#drawingtools`).after($fxMode);
  8434. }
  8435.  
  8436. if (window.is_gm) {
  8437. // add lighting layer tool
  8438. if (!$(`#editinglayer .choosewalls`).length) {
  8439. $(`#editinglayer .choosegmlayer`).after(`<li class="choosewalls"><span class="pictostwo">r</span> Dynamic Lighting</li>`);
  8440. }
  8441.  
  8442. // ensure tokens have editable sight
  8443. $("#tmpl_tokeneditor").replaceWith(d20plus.template_TokenEditor);
  8444. // show dynamic lighting/etc page settings
  8445. $("#tmpl_pagesettings").replaceWith(d20plus.template_pageSettings);
  8446. $("#page-toolbar").on("mousedown", ".settings", function () {
  8447. var e = d20.Campaign.pages.get($(this).parents(".availablepage").attr("data-pageid"));
  8448. e.view._template = $.jqotec("#tmpl_pagesettings");
  8449. });
  8450. }
  8451. },
  8452.  
  8453. enhanceMeasureTool: () => {
  8454. d20plus.log("Enhance Measure tool");
  8455. // ROLL20 CODE
  8456. var T = function (e, t, n, i, r, o) {
  8457. var a = d20.engine.getDistanceInScale({
  8458. x: t.x,
  8459. y: t.y
  8460. }, {
  8461. x: t.to_x,
  8462. y: t.to_y
  8463. }, o)
  8464. , s = a[0];
  8465. void 0 !== r && (s = Math.round(10 * (s + r)) / 10);
  8466. var l = s + "" + d20.Campaign.activePage().get("scale_units");
  8467. if (e.strokeStyle = t.color,
  8468. n) {
  8469. // BEGIN MOD
  8470. var fontSize = (1 / d20.engine.canvasZoom) * 12;
  8471. e.font = fontSize + "pt Arial Black";
  8472. var c = e.measureText(l);
  8473. e.fillStyle = "rgba(255,255,255,0.75)";
  8474. e.beginPath();
  8475. e.rect(t.to_x - 35, t.to_y - (23 + fontSize), c.width + 10, (10 + fontSize));
  8476. e.closePath();
  8477. e.fill();
  8478. // END MOD
  8479. }
  8480. e.beginPath();
  8481. var u = 15
  8482. , d = Math.atan2(t.to_y - t.y, t.to_x - t.x);
  8483. return e.moveTo(t.x, t.y),
  8484. e.lineTo(t.to_x, t.to_y),
  8485. (i === !0 || "arrow" === i) && (e.lineTo(t.to_x - u * Math.cos(d - Math.PI / 6), t.to_y - u * Math.sin(d - Math.PI / 6)),
  8486. e.moveTo(t.to_x, t.to_y),
  8487. e.lineTo(t.to_x - u * Math.cos(d + Math.PI / 6), t.to_y - u * Math.sin(d + Math.PI / 6))),
  8488. e.closePath(),
  8489. e.stroke(),
  8490. "nub" === i && (e.beginPath(),
  8491. e.arc(t.to_x, t.to_y, 7, 0, 2 * Math.PI, !0),
  8492. e.closePath(),
  8493. e.fillStyle = e.strokeStyle,
  8494. e.fill()),
  8495. n && (e.fillStyle = "rgba(0,0,0,1)",
  8496. e.fillText(l, t.to_x - 30, t.to_y - 20)),
  8497. a
  8498. };
  8499. d20.engine.drawMeasurements = function (e) {
  8500. e.globalCompositeOperation = "source-over",
  8501. e.lineWidth = 3,
  8502. e.globalAlpha = 1,
  8503. _.each(d20.engine.measurements, function (t) {
  8504. if (t.pageid === d20.Campaign.activePage().id) {
  8505. var n = _.clone(t)
  8506. , i = d20.Campaign.players.get(n.player);
  8507. n.color = i.get("color"),
  8508. n.to_x = n.to_x - d20.engine.currentCanvasOffset[0],
  8509. n.to_y = n.to_y - d20.engine.currentCanvasOffset[1],
  8510. n.x = n.x - d20.engine.currentCanvasOffset[0],
  8511. n.y = n.y - d20.engine.currentCanvasOffset[1],
  8512. T(e, n, !0, !0)
  8513. }
  8514. })
  8515. }
  8516. // END ROLL20 CODE
  8517. },
  8518.  
  8519. _addStatusEffectEntries: () => {
  8520. const sheetUrl = window.is_gm ? d20plus.getCfgVal("token", "statusSheetUrl") || d20plus.getCfgDefaultVal("token", "statusSheetUrl"): window.Campaign.attributes.bR20cfg_statussheet;
  8521.  
  8522. const temp = new Image();
  8523. temp.onload = () => {
  8524. const xSize = 34;
  8525. const iMin = 47;
  8526. // const iMax = 101;
  8527. const iMax = Math.ceil(temp.width / xSize); // round the last one up to a full image
  8528. for (let i = iMin; i < iMax; ++i) {
  8529. d20.token_editor.statusmarkers["5etools_" + (i - iMin)] = String(i * xSize);
  8530. }
  8531. };
  8532. temp.src = sheetUrl;
  8533.  
  8534. $(`#5etools-status-css`).html(`#radial-menu .markermenu .markericon {
  8535. background-image: url(${sheetUrl});
  8536. }`);
  8537. },
  8538.  
  8539. _removeStatusEffectEntries: () => {
  8540. $(`#5etools-status-css`).html("");
  8541. Object.keys(d20.token_editor.statusmarkers).filter(k => k.startsWith("5etools_")).forEach(k => delete d20.token_editor.statusmarkers[k]);
  8542. },
  8543.  
  8544. enhanceStatusEffects: () => {
  8545. d20plus.log("Enhance status effects");
  8546. $(`head`).append(`<style id="5etools-status-css"/>`);
  8547. d20plus._handleStatusTokenConfigChange();
  8548.  
  8549. function overwriteStatusEffects () {
  8550. d20.engine.canvasDirty = true;
  8551. d20.engine.canvasTopDirty = true;
  8552. d20.engine.canvas._objects.forEach(it => {
  8553. // avoid adding it to any objects that wouldn't have it to begin with
  8554. if (!it.model || !it.model.view || !it.model.view.updateBackdrops) return;
  8555.  
  8556. it.model.view.updateBackdrops = function (e) {
  8557. if (!this.nohud && ("objects" == this.model.get("layer") || "gmlayer" == this.model.get("layer")) && "image" == this.model.get("type") && this.model && this.model.collection && this.graphic) {
  8558. // BEGIN MOD
  8559. const scaleFact = (d20plus.getCfgVal("canvas", "scaleNamesStatuses") && d20.Campaign.activePage().get("snapping_increment"))
  8560. ? d20.Campaign.activePage().get("snapping_increment")
  8561. : 1;
  8562. // END MOD
  8563. var t = this.model.collection.page
  8564. , n = e || d20.engine.canvas.getContext();
  8565. n.save(),
  8566. (this.graphic.get("flipX") || this.graphic.get("flipY")) && n.scale(this.graphic.get("flipX") ? -1 : 1, this.graphic.get("flipY") ? -1 : 1);
  8567. var i = this
  8568. , r = Math.floor(this.graphic.get("width") / 2)
  8569. , o = Math.floor(this.graphic.get("height") / 2)
  8570. , a = (parseFloat(t.get("scale_number")),
  8571. this.model.get("statusmarkers").split(","));
  8572. -1 !== a.indexOf("dead") && (n.strokeStyle = "rgba(189,13,13,0.60)",
  8573. n.lineWidth = 10,
  8574. n.beginPath(),
  8575. n.moveTo(-r + 7, -o + 15),
  8576. n.lineTo(r - 7, o - 5),
  8577. n.moveTo(r - 7, -o + 15),
  8578. n.lineTo(-r + 7, o - 5),
  8579. n.closePath(),
  8580. n.stroke()),
  8581. n.rotate(-this.graphic.get("angle") * Math.PI / 180),
  8582. n.strokeStyle = "rgba(0,0,0,0.65)",
  8583. n.lineWidth = 1;
  8584. var s = 0
  8585. , l = i.model.get("bar1_value")
  8586. , c = i.model.get("bar1_max");
  8587. if ("" != c && (window.is_gm || this.model.get("showplayers_bar1") || this.model.currentPlayerControls() && this.model.get("playersedit_bar1"))) {
  8588. var u = parseInt(l, 10) / parseInt(c, 10)
  8589. , d = -o - 20 + 0;
  8590. n.fillStyle = "rgba(" + d20.Campaign.tokendisplay.bar1_rgb + ",0.75)",
  8591. n.beginPath(),
  8592. n.rect(-r + 3, d, Math.floor((2 * r - 6) * u), 8),
  8593. n.closePath(),
  8594. n.fill(),
  8595. n.beginPath(),
  8596. n.rect(-r + 3, d, 2 * r - 6, 8),
  8597. n.closePath(),
  8598. n.stroke(),
  8599. s++
  8600. }
  8601. var l = i.model.get("bar2_value")
  8602. , c = i.model.get("bar2_max");
  8603. if ("" != c && (window.is_gm || this.model.get("showplayers_bar2") || this.model.currentPlayerControls() && this.model.get("playersedit_bar2"))) {
  8604. var u = parseInt(l, 10) / parseInt(c, 10)
  8605. , d = -o - 20 + 12;
  8606. n.fillStyle = "rgba(" + d20.Campaign.tokendisplay.bar2_rgb + ",0.75)",
  8607. n.beginPath(),
  8608. n.rect(-r + 3, d, Math.floor((2 * r - 6) * u), 8),
  8609. n.closePath(),
  8610. n.fill(),
  8611. n.beginPath(),
  8612. n.rect(-r + 3, d, 2 * r - 6, 8),
  8613. n.closePath(),
  8614. n.stroke(),
  8615. s++
  8616. }
  8617. var l = i.model.get("bar3_value")
  8618. , c = i.model.get("bar3_max");
  8619. if ("" != c && (window.is_gm || this.model.get("showplayers_bar3") || this.model.currentPlayerControls() && this.model.get("playersedit_bar3"))) {
  8620. var u = parseInt(l, 10) / parseInt(c, 10)
  8621. , d = -o - 20 + 24;
  8622. n.fillStyle = "rgba(" + d20.Campaign.tokendisplay.bar3_rgb + ",0.75)",
  8623. n.beginPath(),
  8624. n.rect(-r + 3, d, Math.floor((2 * r - 6) * u), 8),
  8625. n.closePath(),
  8626. n.fill(),
  8627. n.beginPath(),
  8628. n.rect(-r + 3, d, 2 * r - 6, 8),
  8629. n.closePath(),
  8630. n.stroke()
  8631. }
  8632. var h, p, g = 1, f = !1;
  8633. switch (d20.Campaign.get("markers_position")) {
  8634. case "bottom":
  8635. h = o - 10,
  8636. p = r;
  8637. break;
  8638. case "left":
  8639. h = -o - 10,
  8640. p = -r,
  8641. f = !0;
  8642. break;
  8643. case "right":
  8644. h = -o - 10,
  8645. p = r - 18,
  8646. f = !0;
  8647. break;
  8648. default:
  8649. h = -o + 10,
  8650. p = r
  8651. }
  8652. // BEGIN MOD
  8653. n.strokeStyle = "white";
  8654. n.lineWidth = 3 * scaleFact;
  8655. const scaledFont = 14 * scaleFact;
  8656. n.font = "bold " + scaledFont + "px Arial";
  8657. // END MOD
  8658. _.each(a, function (e) {
  8659. var t = d20.token_editor.statusmarkers[e.split("@")[0]];
  8660. if (!t)
  8661. return !0;
  8662. if ("dead" === e)
  8663. return !0;
  8664. var i = 0;
  8665. if (g--,
  8666. "#" === t.substring(0, 1))
  8667. n.fillStyle = t,
  8668. n.beginPath(),
  8669. f ? h += 16 : p -= 16,
  8670. n.arc(p + 8, f ? h + 4 : h, 6, 0, 2 * Math.PI, !0),
  8671. n.closePath(),
  8672. n.stroke(),
  8673. n.fill(),
  8674. i = f ? 10 : 4;
  8675. else {
  8676. // BEGIN MOD
  8677. if (!d20.token_editor.statussheet_ready) return;
  8678. const scaledWH = 21 * scaleFact;
  8679. const scaledOffset = 22 * scaleFact;
  8680. f ? h += scaledOffset : p -= scaledOffset;
  8681.  
  8682. if (d20.engine.canvasZoom <= 1) {
  8683. n.drawImage(d20.token_editor.statussheet_small, parseInt(t, 10), 0, 21, 21, p, h - 9, scaledWH, scaledWH);
  8684. } else {
  8685. n.drawImage(d20.token_editor.statussheet, parseInt(t, 10), 0, 24, 24, p, h - 9, scaledWH, scaledWH)
  8686. }
  8687.  
  8688. i = f ? 14 : 12;
  8689. i *= scaleFact;
  8690. // END MOD
  8691. }
  8692. if (-1 !== e.indexOf("@")) {
  8693. var r = e.split("@")[1];
  8694. // BEGIN MOD
  8695. // bing backtick to "clear counter"
  8696. if (r === "`") return;
  8697. n.fillStyle = "rgb(222,31,31)";
  8698. var o = f ? 9 : 14;
  8699. o *= scaleFact;
  8700. o -= (14 - (scaleFact * 14));
  8701. n.strokeText(r + "", p + i, h + o);
  8702. n.fillText(r + "", p + i, h + o);
  8703. // END MOD
  8704. }
  8705. });
  8706. var m = i.model.get("name");
  8707. if ("" != m && 1 == this.model.get("showname") && (window.is_gm || this.model.get("showplayers_name") || this.model.currentPlayerControls() && this.model.get("playersedit_name"))) {
  8708. n.textAlign = "center";
  8709. // BEGIN MOD
  8710. var y = 14 * scaleFact;
  8711. const scaledY = 22 * scaleFact;
  8712. const scaled6 = 6 * scaleFact;
  8713. const scaled8 = 8 * scaleFact;
  8714. n.font = "bold " + y + "px Arial";
  8715. var v = n.measureText(m).width;
  8716. n.fillStyle = "rgba(255,255,255,0.50)";
  8717. n.fillRect(-1 * Math.floor((v + scaled6) / 2), o + scaled8, v + scaled6, y + scaled6);
  8718. n.fillStyle = "rgb(0,0,0)";
  8719. n.fillText(m + "", 0, o + scaledY, v);
  8720. // END MOD
  8721. }
  8722. n.restore()
  8723. }
  8724. }
  8725. });
  8726. }
  8727.  
  8728. overwriteStatusEffects();
  8729.  
  8730. d20.engine.canvas.off("object:added");
  8731. d20.engine.canvas.on("object:added", overwriteStatusEffects);
  8732.  
  8733. // the holy trinity
  8734. // d20.engine.canvas.on("object:removed", () => console.log("added"));
  8735. // d20.engine.canvas.on("object:removed", () => console.log("removed"));
  8736. // d20.engine.canvas.on("object:modified", () => console.log("modified"));
  8737.  
  8738. $(document).off("mouseenter", ".markermenu");
  8739. $(document).on("mouseenter", ".markermenu", function () {
  8740. var e = this;
  8741. $(this).on("mouseover.statusiconhover", ".statusicon", function () {
  8742. a = $(this).attr("data-action-type").replace("toggle_status_", "")
  8743. }),
  8744. $(document).on("keypress.statusnum", function (t) {
  8745. // BEGIN MOD // TODO see if this clashes with keyboard shortcuts
  8746. if ("dead" !== a && currentcontexttarget) {
  8747. // END MOD
  8748. var n = String.fromCharCode(t.which)
  8749. ,
  8750. i = "" == currentcontexttarget.model.get("statusmarkers") ? [] : currentcontexttarget.model.get("statusmarkers").split(",")
  8751. , r = (_.map(i, function (e) {
  8752. return e.split("@")[0]
  8753. }),
  8754. !1);
  8755. i = _.map(i, function (e) {
  8756. return e.split("@")[0] == a ? (r = !0,
  8757. a + "@" + n) : e
  8758. }),
  8759. r || ($(e).find(".statusicon[data-action-type=toggle_status_" + a + "]").addClass("active"),
  8760. i.push(a + "@" + n)),
  8761. currentcontexttarget.model.save({
  8762. statusmarkers: i.join(",")
  8763. })
  8764. }
  8765. })
  8766. })
  8767. },
  8768.  
  8769. enhancePageSelector: () => {
  8770. d20plus.log("Enhancing page selector");
  8771. var updatePageOrder = function () {
  8772. d20plus.log("Saving page order...");
  8773. var pos = 0;
  8774. $("#page-toolbar .pages .chooseablepage").each(function () {
  8775. var page = d20.Campaign.pages.get($(this).attr("data-pageid"));
  8776. page && page.save({
  8777. placement: pos
  8778. });
  8779. pos++;
  8780. });
  8781. d20.pagetoolbar.noReload = false;
  8782. d20.pagetoolbar.refreshPageListing();
  8783. }
  8784.  
  8785. function overwriteDraggables () {
  8786. // make them draggable on both axes
  8787. $("#page-toolbar .pages").sortable("destroy");
  8788. $("#page-toolbar .pages").sortable({
  8789. items: "> .chooseablepage",
  8790. start: function () {
  8791. d20.pagetoolbar.noReload = true;
  8792. },
  8793. stop: function () {
  8794. updatePageOrder()
  8795. },
  8796. distance: 15
  8797. }).addTouch();
  8798. $("#page-toolbar .playerbookmark").draggable("destroy");
  8799. $("#page-toolbar .playerbookmark").draggable({
  8800. revert: "invalid",
  8801. appendTo: "#page-toolbar",
  8802. helper: "original"
  8803. }).addTouch();
  8804. $("#page-toolbar .playerspecificbookmark").draggable("destroy");
  8805. $("#page-toolbar .playerspecificbookmark").draggable({
  8806. revert: "invalid",
  8807. appendTo: "#page-toolbar",
  8808. helper: "original"
  8809. }).addTouch();
  8810. }
  8811.  
  8812. overwriteDraggables();
  8813. $(`#page-toolbar`).css("top", "calc(-90vh + 40px)");
  8814.  
  8815. const originalFn = d20.pagetoolbar.refreshPageListing;
  8816. d20.pagetoolbar.refreshPageListing = () => {
  8817. originalFn();
  8818. // original function is debounced at 100ms, so debounce this at 110ms and hope for the best
  8819. _.debounce(() => {
  8820. overwriteDraggables();
  8821. }, 110)();
  8822. }
  8823. },
  8824.  
  8825. initQuickSearch: ($iptSearch, $outSearch) => {
  8826. $iptSearch.on("keyup", () => {
  8827. const searchVal = ($iptSearch.val() || "").trim();
  8828. $outSearch.empty();
  8829. if (searchVal.length <= 2) return; // ignore 2 characters or less, for performance reasons
  8830. const found = $(`#journal .content`).find(`li[data-itemid]`).filter((i, ele) => {
  8831. const $ele = $(ele);
  8832. return $ele.find(`.name`).text().trim().toLowerCase().includes(searchVal.toLowerCase());
  8833. });
  8834. if (found.length) {
  8835. $outSearch.append(`<p><b>Search results:</b></p>`);
  8836. const $outList = $(`<ol class="dd-list Vetools-search-results"/>`);
  8837. $outSearch.append($outList);
  8838. found.clone().addClass("Vetools-draggable").appendTo($outList);
  8839. $outSearch.append(`<hr>`);
  8840. $(`.Vetools-search-results .Vetools-draggable`).draggable({
  8841. revert: true,
  8842. distance: 10,
  8843. revertDuration: 0,
  8844. helper: "clone",
  8845. handle: ".namecontainer",
  8846. appendTo: "body",
  8847. scroll: true,
  8848. start: function () {
  8849. $("#journalfolderroot").addClass("externaldrag")
  8850. },
  8851. stop: function () {
  8852. $("#journalfolderroot").removeClass("externaldrag")
  8853. }
  8854. });
  8855. }
  8856. });
  8857. },
  8858.  
  8859. addSelectedTokenCommands: () => {
  8860. d20plus.log("Add token rightclick commands");
  8861. $("#tmpl_actions_menu").replaceWith(d20plus.template_actionsMenu);
  8862.  
  8863. // BEGIN ROLL20 CODE
  8864. var e, t = !1, n = [];
  8865. var i = function() {
  8866. t && (t.remove(),
  8867. t = !1),
  8868. e && clearTimeout(e)
  8869. };
  8870. var r = function (r) {
  8871. var o, a;
  8872. r.changedTouches && r.changedTouches.length > 0 ? (o = r.changedTouches[0].pageX,
  8873. a = r.changedTouches[0].pageY) : (o = r.pageX,
  8874. a = r.pageY),
  8875. i(),
  8876. n = [];
  8877. for (var s = [], l = d20.engine.selected(), c = 0; c < l.length; c++)
  8878. n.push(l[c]),
  8879. s.push(l[c].type);
  8880. if (s = _.uniq(s),
  8881. n.length > 0)
  8882. if (1 == s.length) {
  8883. var u = n[0];
  8884. t = $("image" == u.type && 0 == u.model.get("isdrawing") ? $("#tmpl_actions_menu").jqote(u.model) : $("#tmpl_actions_menu").jqote(u.model))
  8885. } else {
  8886. var u = n[0];
  8887. t = $($("#tmpl_actions_menu").jqote(u.model))
  8888. }
  8889. else
  8890. t = $($("#tmpl_actions_menu").jqote({}));
  8891. if (!window.is_gm && t[0].lastElementChild.childElementCount < 1)
  8892. return !1;
  8893. t.appendTo("body");
  8894. var d = t.height()
  8895. , h = t.width()
  8896. , p = {};
  8897. return p.top = a > $("#editor-wrapper").height() - $("#playerzone").height() - d - 100 ? a - d + "px" : a + "px",
  8898. p.left = o > $("#editor-wrapper").width() - h ? o + 10 - h + "px" : o + 10 + "px",
  8899. t.css(p),
  8900. $(".actions_menu").bind("mousedown mouseup touchstart", function(e) {
  8901. e.stopPropagation()
  8902. }),
  8903. $(".actions_menu ul > li").bind("mouseover touchend", function() {
  8904. if (e && (clearTimeout(e),
  8905. e = !1),
  8906. $(this).parents(".hasSub").length > 0)
  8907. ;
  8908. else if ($(this).hasClass("hasSub")) {
  8909. $(".actions_menu").css({
  8910. width: "215px",
  8911. height: "250px"
  8912. });
  8913. var t = this;
  8914. _.defer(function() {
  8915. $(".actions_menu ul.submenu").hide(),
  8916. $(t).find("ul.submenu:hidden").show()
  8917. })
  8918. } else
  8919. $(".actions_menu ul.submenu").hide()
  8920. }),
  8921. $(".actions_menu ul.submenu").live("mouseover", function() {
  8922. e && (clearTimeout(e),
  8923. e = !1)
  8924. }),
  8925. $(".actions_menu, .actions_menu ul.submenu").live("mouseleave", function() {
  8926. e || (e = setTimeout(function() {
  8927. $(".actions_menu ul.submenu").hide(),
  8928. $(".actions_menu").css("width", "100px").css("height", "auto"),
  8929. e = !1
  8930. }, 500))
  8931. }),
  8932. $(".actions_menu li").on(clicktype, function() {
  8933. var e = $(this).attr("data-action-type");
  8934. if (null != e) {
  8935. if ("copy" == e)
  8936. d20.clipboard.doCopy(),
  8937. i();
  8938. else if ("paste" == e)
  8939. d20.clipboard.doPaste(),
  8940. i();
  8941. else if ("delete" == e) {
  8942. var t = d20.engine.selected();
  8943. d20.engine.canvas.deactivateAllWithDispatch();
  8944. for (var r = 0; r < t.length; r++)
  8945. t[r].model.destroy();
  8946. i()
  8947. } else if ("undo" == e)
  8948. d20.undo && d20.undo.doUndo(),
  8949. i();
  8950. else if ("tofront" == e)
  8951. d20.engine.canvas.getActiveGroup() && d20.engine.unselect(),
  8952. _.each(n, function(e) {
  8953. d20.engine.canvas.bringToFront(e)
  8954. }),
  8955. d20.Campaign.activePage().debounced_recordZIndexes(),
  8956. i();
  8957. else if ("toback" == e)
  8958. d20.engine.canvas.getActiveGroup() && d20.engine.unselect(),
  8959. _.each(n, function(e) {
  8960. d20.engine.canvas.sendToBack(e)
  8961. }),
  8962. d20.Campaign.activePage().debounced_recordZIndexes(),
  8963. i();
  8964. else if (-1 !== e.indexOf("tolayer_")) {
  8965. d20.engine.unselect();
  8966. var o = e.replace("tolayer_", "");
  8967. _.each(n, function(e) {
  8968. e.model.save({
  8969. layer: o
  8970. })
  8971. }),
  8972. i(),
  8973. d20.token_editor.removeRadialMenu()
  8974. } else if ("addturn" == e)
  8975. _.each(n, function(e) {
  8976. d20.Campaign.initiativewindow.addTokenToList(e.model.id)
  8977. }),
  8978. i(),
  8979. d20.tutorial && d20.tutorial.active && $(document.body).trigger("addedTurn");
  8980. else if ("group" == e) {
  8981. var a = [];
  8982. d20.engine.unselect(),
  8983. _.each(n, function(e) {
  8984. a.push(e.model.id)
  8985. }),
  8986. _.each(n, function(e) {
  8987. e.model.addToGroup(a)
  8988. }),
  8989. i();
  8990. var s = n[0];
  8991. d20.engine.select(s)
  8992. } else if ("ungroup" == e)
  8993. d20.engine.unselect(),
  8994. _.each(n, function(e) {
  8995. e.model.clearGroup()
  8996. }),
  8997. d20.token_editor.removeRadialMenu(),
  8998. i();
  8999. else if ("toggledrawing" == e)
  9000. d20.engine.unselect(),
  9001. _.each(n, function(e) {
  9002. e.model.set({
  9003. isdrawing: !e.model.get("isdrawing")
  9004. }).save()
  9005. }),
  9006. i(),
  9007. d20.token_editor.removeRadialMenu();
  9008. else if ("toggleflipv" == e)
  9009. d20.engine.unselect(),
  9010. _.each(n, function(e) {
  9011. e.model.set({
  9012. flipv: !e.model.get("flipv")
  9013. }).save()
  9014. }),
  9015. i(),
  9016. d20.token_editor.removeRadialMenu();
  9017. else if ("togglefliph" == e)
  9018. d20.engine.unselect(),
  9019. _.each(n, function(e) {
  9020. e.model.set({
  9021. fliph: !e.model.get("fliph")
  9022. }).save()
  9023. }),
  9024. i(),
  9025. d20.token_editor.removeRadialMenu();
  9026. else if ("takecard" == e)
  9027. d20.engine.canvas.getActiveGroup() && d20.engine.unselect(),
  9028. _.each(n, function(e) {
  9029. var t = d20.decks.cardByID(e.model.get("cardid"));
  9030. if (e.model.get("isdrawing") === !1)
  9031. var n = {
  9032. bar1_value: e.model.get("bar1_value"),
  9033. bar1_max: e.model.get("bar1_max"),
  9034. bar2_value: e.model.get("bar2_value"),
  9035. bar2_max: e.model.get("bar2_max"),
  9036. bar3_value: e.model.get("bar3_value"),
  9037. bar3_max: e.model.get("bar3_max")
  9038. };
  9039. d20.Campaign.hands.addCardToHandForPlayer(t, window.currentPlayer, n ? n : void 0),
  9040. _.defer(function() {
  9041. e.model.destroy()
  9042. })
  9043. }),
  9044. d20.engine.unselect(),
  9045. i();
  9046. else if ("flipcard" == e)
  9047. d20.engine.canvas.getActiveGroup() && d20.engine.unselect(),
  9048. _.each(n, function(e) {
  9049. var t = e.model.get("sides").split("|")
  9050. , n = e.model.get("currentSide")
  9051. , i = n + 1;
  9052. i > t.length - 1 && (i = 0),
  9053. e.model.set({
  9054. currentSide: i,
  9055. imgsrc: unescape(t[i])
  9056. }).save()
  9057. }),
  9058. i();
  9059. else if ("setdimensions" == e) {
  9060. var l = n[0]
  9061. , c = $($("#tmpl_setdimensions").jqote()).dialog({
  9062. title: "Set Dimensions",
  9063. width: 325,
  9064. height: 225,
  9065. buttons: {
  9066. Set: function() {
  9067. var e, t;
  9068. "pixels" == c.find(".dimtype").val() ? (e = parseInt(c.find("input.width").val(), 10),
  9069. t = parseInt(c.find("input.height").val(), 10)) : (e = parseFloat(c.find("input.width").val()) * window.dpi,
  9070. t = parseFloat(c.find("input.height").val()) * window.dpi),
  9071. l.model.save({
  9072. width: e,
  9073. height: t
  9074. }),
  9075. c.off("change"),
  9076. c.dialog("destroy").remove()
  9077. },
  9078. Cancel: function() {
  9079. c.off("change"),
  9080. c.dialog("destroy").remove()
  9081. }
  9082. },
  9083. beforeClose: function() {
  9084. c.off("change"),
  9085. c.dialog("destroy").remove()
  9086. }
  9087. });
  9088. c.on("change", ".dimtype", function() {
  9089. "pixels" == $(this).val() ? (c.find("input.width").val(Math.round(l.get("width"))),
  9090. c.find("input.height").val(Math.round(l.get("height")))) : (c.find("input.width").val(l.get("width") / window.dpi),
  9091. c.find("input.height").val(l.get("height") / window.dpi))
  9092. }),
  9093. c.find(".dimtype").trigger("change"),
  9094. i()
  9095. } else if ("aligntogrid" == e)
  9096. if (0 === d20.Campaign.activePage().get("snapping_increment")) {
  9097. i();
  9098. var u = $($("#tmpl_grid-disabled").jqote(h)).dialog({
  9099. title: "Grid Off",
  9100. buttons: {
  9101. Ok: function() {
  9102. u.off("change"),
  9103. u.dialog("destroy").remove()
  9104. }
  9105. },
  9106. beforeClose: function() {
  9107. u.off("change"),
  9108. u.dialog("destroy").remove()
  9109. }
  9110. })
  9111. } else
  9112. d20.engine.gridaligner.target = n[0],
  9113. d20plus.setMode("gridalign"),
  9114. i();
  9115. else if ("side_random" == e) {
  9116. d20.engine.canvas.getActiveGroup() && d20.engine.unselect();
  9117. var d = [];
  9118. _.each(n, function(e) {
  9119. if (e.model && "" != e.model.get("sides")) {
  9120. var t = e.model.get("sides").split("|")
  9121. , n = t.length
  9122. , i = d20.textchat.diceengine.random(n);
  9123. e.model.save({
  9124. currentSide: i,
  9125. imgsrc: unescape(t[i])
  9126. }),
  9127. d.push(t[i])
  9128. }
  9129. }),
  9130. d20.textchat.rawChatInput({
  9131. type: "tokenroll",
  9132. content: d.join("|")
  9133. }),
  9134. i()
  9135. } else if ("side_choose" == e) {
  9136. var l = n[0]
  9137. , h = l.model.toJSON()
  9138. , p = h.currentSide;
  9139. h.sidesarray = h.sides.split("|");
  9140. var c = $($("#tmpl_chooseside").jqote(h)).dialog({
  9141. title: "Choose Side",
  9142. width: 325,
  9143. height: 225,
  9144. buttons: {
  9145. Choose: function() {
  9146. d20.engine.canvas.getActiveGroup() && d20.engine.unselect(),
  9147. l.model.save({
  9148. currentSide: p,
  9149. imgsrc: unescape(h.sidesarray[p])
  9150. }),
  9151. l = null,
  9152. h = null,
  9153. c.off("slide"),
  9154. c.dialog("destroy").remove()
  9155. },
  9156. Cancel: function() {
  9157. l = null,
  9158. h = null,
  9159. c.off("slide"),
  9160. c.dialog("destroy").remove()
  9161. }
  9162. },
  9163. beforeClose: function() {
  9164. l = null,
  9165. h = null,
  9166. c.off("slide"),
  9167. c.dialog("destroy").remove()
  9168. }
  9169. });
  9170. c.find(".sideslider").slider({
  9171. min: 0,
  9172. max: h.sidesarray.length - 1,
  9173. step: 1,
  9174. value: h.currentSide
  9175. }),
  9176. c.on("slide", function(e, t) {
  9177. t.value != p && (p = t.value,
  9178. c.find(".sidechoices .sidechoice").hide().eq(t.value).show())
  9179. }),
  9180. c.find(".sidechoices .sidechoice").hide().eq(h.currentSide).show(),
  9181. i()
  9182. }
  9183. // BEGIN MOD
  9184. if ("rollsaves" === e) {
  9185. const sel = d20.engine.selected();
  9186.  
  9187. const options = ["str", "dex", "con", "int", "wis", "cha"].map(it => `<option value='${it}'>${Parser.attAbvToFull(it)}</option>`);
  9188. const dialog= $("<div><p style='font-size: 1.15em;'><strong>" + d20.utils.strip_tags("Select Save") + ":</strong> <select style='width: 150px; margin-left: 5px;'>" + options.join("") + "</select></p></div>");
  9189. dialog.dialog({
  9190. title: "Input Value",
  9191. beforeClose: function() {
  9192. return false;
  9193. },
  9194. buttons: {
  9195. Submit: function() {
  9196. const val = Parser.attAbvToFull(dialog.find("select").val());
  9197. console.log(val);
  9198. d20.engine.unselect();
  9199. sel.forEach(it => {
  9200. d20.engine.select(it);
  9201. const toRoll = `@{selected|wtype} &{template:simple} {{charname=@{selected|token_name}}} {{always=1}} {{rname=${val} Save}} {{mod=@{selected|${val.toLowerCase()}_save_bonus}}} {{r1=[[1d20+@{selected|${val.toLowerCase()}_save_bonus}]]}} {{r2=[[1d20+@{selected|${val.toLowerCase()}_save_bonus}]]}}`;
  9202. d20.textchat.doChatInput(toRoll);
  9203. d20.engine.unselect();
  9204. });
  9205.  
  9206. dialog.off();
  9207. dialog.dialog("destroy").remove();
  9208. d20.textchat.$textarea.focus();
  9209. },
  9210. Cancel: function() {
  9211. dialog.off();
  9212. dialog.dialog("destroy").remove();
  9213. }
  9214. }
  9215. });
  9216.  
  9217. i();
  9218. } else if ("rollinit" === e) {
  9219. const sel = d20.engine.selected();
  9220. d20.engine.unselect();
  9221. sel.forEach(it => {
  9222. d20.engine.select(it);
  9223. const toRoll = `@{selected|wtype} &{template:simple} {{rname=Initiative}} {{charname=@{selected|token_name}}} {{mod=[[@{selected|initiative_bonus}]]}} {{r1=[[@{selected|d20}+@{selected|dexterity_mod} &{tracker}]]}}{{normal=1}}`;
  9224. d20.textchat.doChatInput(toRoll);
  9225. d20.engine.unselect();
  9226. });
  9227. i();
  9228. }
  9229. // END MOD
  9230. return !1
  9231. }
  9232. }),
  9233. !1
  9234. };
  9235. // END ROLL20 CODE
  9236. d20.token_editor.showContextMenu = r;
  9237. d20.token_editor.closeContextMenu = i;
  9238. $(`#editor-wrapper`).on("click", d20.token_editor.closeContextMenu);
  9239. },
  9240.  
  9241. enhanceSnap: () => {
  9242. /**
  9243. * Dumb variable names copy-pasted from uglified code
  9244. * @param c x co-ord
  9245. * @param u y c-ord
  9246. * @returns {*[]} 2-len array; [0] = x and [1] = y
  9247. */
  9248. function getClosestHexPoint (c, u) {
  9249. function getEuclidDist (x1, y1, x2, y2) {
  9250. return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
  9251. }
  9252.  
  9253. const hx = d20.canvas_overlay.activeHexGrid.GetHexAt({
  9254. X: c,
  9255. Y: u
  9256. });
  9257.  
  9258. let minDist = 1000000;
  9259. let minPoint = [c, u];
  9260.  
  9261. function checkDist(x1, y1) {
  9262. const dist = getEuclidDist(x1, y1, c, u);
  9263. if (dist < minDist) {
  9264. minDist = dist;
  9265. minPoint = [x1, y1];
  9266. }
  9267. }
  9268. hx.Points.forEach(pt => {
  9269. checkDist(pt.X, pt.Y);
  9270. });
  9271. checkDist(hx.MidPoint.X, hx.MidPoint.Y);
  9272.  
  9273. return minPoint;
  9274. }
  9275.  
  9276. // BEGIN ROLL20 CODE
  9277. const M = function(e) {
  9278. //BEGIN MOD
  9279. var t = d20.engine.canvas;
  9280. var s = $("#editor-wrapper");
  9281. // END MOD
  9282. var n, r;
  9283. if (d20.tddice && d20.tddice.handleInteraction && d20.tddice.handleInteraction(),
  9284. e.touches) {
  9285. if ("pan" == d20.engine.mode)
  9286. return;
  9287. e.touches.length > 1 && (R = d20.engine.mode,
  9288. d20.engine.mode = "pan",
  9289. d20.engine.leftMouseIsDown = !0),
  9290. d20.engine.lastTouchStarted = (new Date).getTime(),
  9291. n = e.touches[0].pageX,
  9292. r = e.touches[0].pageY,
  9293. e.preventDefault()
  9294. } else
  9295. n = e.pageX,
  9296. r = e.pageY;
  9297. for (var o = d20.engine.showLastPaths.length; o--; )
  9298. "selected" == d20.engine.showLastPaths[o].type && d20.engine.showLastPaths.splice(o, 1);
  9299. d20.engine.handleMetaKeys(e),
  9300. ("select" == d20.engine.mode || "path" == d20.engine.mode) && t.__onMouseDown(e),
  9301. (1 == e.which || e.touches && 1 == e.touches.length) && (d20.engine.leftMouseIsDown = !0);
  9302. var a = Math.floor(n / d20.engine.canvasZoom + d20.engine.currentCanvasOffset[0] - d20.engine.paddingOffset[0] / d20.engine.canvasZoom)
  9303. , l = Math.floor(r / d20.engine.canvasZoom + d20.engine.currentCanvasOffset[1] - d20.engine.paddingOffset[1] / d20.engine.canvasZoom);
  9304. if (d20.engine.lastMousePos = [a, l],
  9305. !d20.engine.leftMouseIsDown || "fog-reveal" != d20.engine.mode && "fog-hide" != d20.engine.mode && "gridalign" != d20.engine.mode) {
  9306. if (d20.engine.leftMouseIsDown && "fog-polygonreveal" == d20.engine.mode) {
  9307. // BEGIN MOD
  9308. var c = a;
  9309. var u = l;
  9310. if (0 != d20.engine.snapTo && (e.shiftKey && !d20.Campaign.activePage().get("adv_fow_enabled") || !e.shiftKey && d20.Campaign.activePage().get("adv_fow_enabled"))) {
  9311. if ("square" == d20.Campaign.activePage().get("grid_type")) {
  9312. c = d20.engine.snapToIncrement(c, d20.engine.snapTo);
  9313. u = d20.engine.snapToIncrement(u, d20.engine.snapTo);
  9314. } else {
  9315. const minPoint = getClosestHexPoint(c, u);
  9316. c = minPoint[0];
  9317. u = minPoint[1];
  9318. }
  9319. }
  9320. if (d20.engine.fog.points.length > 0 && Math.abs(d20.engine.fog.points[0][0] - c) + Math.abs(d20.engine.fog.points[0][1] - u) < 15) {
  9321. d20.engine.fog.points.push([d20.engine.fog.points[0][0], d20.engine.fog.points[0][1]]);
  9322. d20.engine.finishPolygonReveal();
  9323. } else {
  9324. d20.engine.fog.points.push([c, u]);
  9325. }
  9326. d20.engine.drawOverlays();
  9327. // END MOD
  9328. } else if (d20.engine.leftMouseIsDown && "measure" == d20.engine.mode) {
  9329. if (d20.engine.measure.down[0] = a,
  9330. d20.engine.measure.down[1] = l,
  9331. 0 != d20.engine.snapTo && !e.altKey)
  9332. if ("square" == d20.Campaign.activePage().get("grid_type"))
  9333. d20.engine.measure.down[1] = d20.engine.snapToIncrement(d20.engine.measure.down[1] + Math.floor(d20.engine.snapTo / 2), d20.engine.snapTo) - Math.floor(d20.engine.snapTo / 2),
  9334. d20.engine.measure.down[0] = d20.engine.snapToIncrement(d20.engine.measure.down[0] + Math.floor(d20.engine.snapTo / 2), d20.engine.snapTo) - Math.floor(d20.engine.snapTo / 2);
  9335. else {
  9336. var d = d20.canvas_overlay.activeHexGrid.GetHexAt({
  9337. X: d20.engine.measure.down[0],
  9338. Y: d20.engine.measure.down[1]
  9339. });
  9340. d20.engine.measure.down[1] = d.MidPoint.Y,
  9341. d20.engine.measure.down[0] = d.MidPoint.X
  9342. }
  9343. } else if (d20.engine.leftMouseIsDown && "fxtools" == d20.engine.mode)
  9344. d20.engine.fx.current || (d20.engine.fx.current = d20.fx.handleClick(a, l));
  9345. else if (d20.engine.leftMouseIsDown && "text" == d20.engine.mode) {
  9346. var h = {
  9347. fontFamily: $("#font-family").val(),
  9348. fontSize: $("#font-size").val(),
  9349. fill: $("#font-color").val(),
  9350. text: "",
  9351. left: a,
  9352. top: l
  9353. }
  9354. , p = d20.Campaign.activePage().addText(h);
  9355. _.defer(function() {
  9356. d20.engine.editText(p.view.graphic, h.top, h.left),
  9357. setTimeout(function() {
  9358. $(".texteditor").focus()
  9359. }, 300)
  9360. })
  9361. } else if (d20.engine.leftMouseIsDown && "rect" == d20.engine.mode) {
  9362. var g = parseInt($("#path_width").val(), 10)
  9363. , f = d20.engine.drawshape.shape = {
  9364. strokewidth: g,
  9365. x: 0,
  9366. y: 0,
  9367. width: 10,
  9368. height: 10,
  9369. type: e.altKey ? "circle" : "rect"
  9370. }
  9371. , c = a
  9372. , u = l;
  9373. 0 != d20.engine.snapTo && e.shiftKey && (c = d20.engine.snapToIncrement(c, d20.engine.snapTo),
  9374. u = d20.engine.snapToIncrement(u, d20.engine.snapTo)),
  9375. f.x = c,
  9376. f.y = u,
  9377. f.fill = $("#path_fillcolor").val(),
  9378. f.stroke = $("#path_strokecolor").val(),
  9379. d20.engine.drawshape.start = [n + d20.engine.currentCanvasOffset[0] - d20.engine.paddingOffset[0], r + d20.engine.currentCanvasOffset[1] - d20.engine.paddingOffset[1]],
  9380. d20.engine.renderTop()
  9381. } else if (d20.engine.leftMouseIsDown && "polygon" == d20.engine.mode) {
  9382. if (d20.engine.drawshape.shape)
  9383. var f = d20.engine.drawshape.shape;
  9384. else {
  9385. var g = parseInt($("#path_width").val(), 10)
  9386. , f = d20.engine.drawshape.shape = {
  9387. strokewidth: g,
  9388. points: [],
  9389. type: "polygon"
  9390. };
  9391. f.fill = $("#path_fillcolor").val(),
  9392. f.stroke = $("#path_strokecolor").val()
  9393. }
  9394. // BEGIN MOD
  9395. var c = a;
  9396. var u = l;
  9397. if (0 != d20.engine.snapTo && e.shiftKey) {
  9398. if ("square" == d20.Campaign.activePage().get("grid_type")) {
  9399. c = d20.engine.snapToIncrement(c, d20.engine.snapTo);
  9400. u = d20.engine.snapToIncrement(u, d20.engine.snapTo);
  9401. } else {
  9402. const minPoint = getClosestHexPoint(c, u);
  9403. c = minPoint[0];
  9404. u = minPoint[1];
  9405. }
  9406. }
  9407. if (f.points.length > 0 && Math.abs(f.points[0][0] - c) + Math.abs(f.points[0][1] - u) < 15) {
  9408. f.points.push([f.points[0][0], f.points[0][1]]);
  9409. if (f.points.length > 2) {
  9410. f.points.push([f.points[1][0], f.points[1][1]]);
  9411. }
  9412. d20.engine.finishCurrentPolygon();
  9413. } else {
  9414. f.points.push([c, u]);
  9415. }
  9416. d20.engine.debounced_renderTop();
  9417. // END MOD
  9418. } else if (d20.engine.leftMouseIsDown && "targeting" === d20.engine.mode) {
  9419. var m = d20.engine.canvas.findTarget(e, !0, !0);
  9420. return void (void 0 !== m && "image" === m.type && m.model && d20.engine.nextTargetCallback(m))
  9421. }
  9422. } else
  9423. d20.engine.fog.down[0] = a,
  9424. d20.engine.fog.down[1] = l,
  9425. 0 != d20.engine.snapTo && "square" == d20.Campaign.activePage().get("grid_type") && ("gridalign" == d20.engine.mode ? e.shiftKey && (d20.engine.fog.down[0] = d20.engine.snapToIncrement(d20.engine.fog.down[0], d20.engine.snapTo),
  9426. d20.engine.fog.down[1] = d20.engine.snapToIncrement(d20.engine.fog.down[1], d20.engine.snapTo)) : (e.shiftKey && !d20.Campaign.activePage().get("adv_fow_enabled") || !e.shiftKey && d20.Campaign.activePage().get("adv_fow_enabled")) && (d20.engine.fog.down[0] = d20.engine.snapToIncrement(d20.engine.fog.down[0], d20.engine.snapTo),
  9427. d20.engine.fog.down[1] = d20.engine.snapToIncrement(d20.engine.fog.down[1], d20.engine.snapTo)));
  9428. if (window.currentPlayer && d20.engine.leftMouseIsDown && "select" == d20.engine.mode) {
  9429. if (d20.engine.pings[window.currentPlayer.id] && d20.engine.pings[window.currentPlayer.id].radius > 20)
  9430. return;
  9431. var y = a
  9432. , v = l
  9433. , b = {
  9434. left: y,
  9435. top: v,
  9436. radius: -5,
  9437. player: window.currentPlayer.id,
  9438. pageid: d20.Campaign.activePage().id,
  9439. currentLayer: window.currentEditingLayer
  9440. };
  9441. window.is_gm && e.shiftKey && (b.scrollto = !0),
  9442. d20.engine.pings[window.currentPlayer.id] = b,
  9443. d20.engine.pinging = {
  9444. downx: n,
  9445. downy: r
  9446. },
  9447. d20.engine.renderTop()
  9448. }
  9449. 3 == e.which && (d20.engine.rightMouseIsDown = !0),
  9450. d20.engine.rightMouseIsDown && ("select" == d20.engine.mode || "path" == d20.engine.mode || "text" == d20.engine.mode) || d20.engine.leftMouseIsDown && "pan" == d20.engine.mode ? (d20.engine.pan.beginPos = [s.scrollLeft(), s.scrollTop()],
  9451. d20.engine.pan.panXY = [n, r],
  9452. d20.engine.pan.panning = !0) : d20.engine.pan.panning = !1,
  9453. // BEGIN MOD
  9454. $(`#upperCanvas`).hasClass("hasfocus") || $(`#upperCanvas`).focus()
  9455. // END MOD
  9456. };
  9457. // END ROLL20 CODE
  9458.  
  9459. if (UPPER_CANVAS_MOUSEDOWN) {
  9460. d20plus.log("Enhancing hex snap");
  9461. d20.engine.uppercanvas.removeEventListener("mousedown", UPPER_CANVAS_MOUSEDOWN);
  9462. d20.engine.uppercanvas.addEventListener("mousedown", M);
  9463. }
  9464.  
  9465. // add half-grid snap
  9466. d20.engine.snapToIncrement = function(e, t) {
  9467. if (d20plus.getCfgVal("canvas", "halfGridSnap")) {
  9468. t = t / 2;
  9469. }
  9470. return t * Math.round(e / t);
  9471. }
  9472. },
  9473.  
  9474. _getHoverGmNoteStr: (str) => {
  9475. const $initial = $(`<div>${str}</div>`);
  9476.  
  9477. let stack = "";
  9478. function recurse ($ele, depth, listDepth, inTable, inTd) {
  9479. const nodes = $ele.contents().get();
  9480.  
  9481. nodes.forEach(n => {
  9482. if (n.nodeType === 3) { // text node
  9483. const pre = listDepth ? ` ${"-".repeat(listDepth)}` : "";
  9484. const doLineEnd = !inTd;
  9485. stack += `${pre}${n.data}${doLineEnd ? "\n" : ""}`;
  9486. } else {
  9487. const $n = $(n);
  9488.  
  9489. if ($n.is("br")) {
  9490. stack += "\n";
  9491. return;
  9492. }
  9493.  
  9494. let nxtInTable = inTable;
  9495. if ($n.is("table")) {
  9496. nxtInTable = true;
  9497. }
  9498.  
  9499. if ($n.is("tr")) {
  9500. stack += "----------\n"
  9501. }
  9502.  
  9503. let nxtInTd = inTd;
  9504. if ($n.is("td")) {
  9505. stack += "|";
  9506. nxtInTd = true;
  9507. }
  9508.  
  9509. let nxtListDepth = listDepth;
  9510. if ($n.is("ul") || $n.is("li")) {
  9511. nxtListDepth = listDepth ? listDepth + 1 : 1;
  9512. }
  9513.  
  9514. recurse($(n), depth + 1, nxtListDepth, nxtInTable, nxtInTd);
  9515.  
  9516. // end TRs with a newline
  9517. if ($n.is("tr")) {
  9518. stack += "\n";
  9519. }
  9520. }
  9521. })
  9522.  
  9523. }
  9524.  
  9525. recurse($initial, 0, 0, false, false);
  9526.  
  9527. return stack;
  9528. },
  9529. _tokenHover: null,
  9530. _drawTokenHover: () => {
  9531. if (!d20plus._tokenHover || !d20plus._tokenHover.text) return;
  9532.  
  9533. const pt = d20plus._tokenHover.pt;
  9534. let txt;
  9535. try {
  9536. txt = d20plus._getHoverGmNoteStr(unescape(d20plus._tokenHover.text));
  9537. } catch (e) {
  9538. txt = "[Error - could not read GM notes - try re-save]"
  9539. }
  9540.  
  9541. function wrapText (context, text, x, y, maxWidth, lineHeight, doDraw) {
  9542. const words = text.replace(/\n/g, " \n ").split(/[ ]+/);
  9543. let line = '';
  9544.  
  9545. for(let n = 0; n < words.length; n++) {
  9546. if (words[n] === "\n") {
  9547. if (doDraw) context.fillText(line.trim(), x, y);
  9548. line = '';
  9549. y += lineHeight;
  9550. }
  9551. const testLine = line + words[n] + ' ';
  9552. const metrics = context.measureText(testLine);
  9553. const testWidth = metrics.width;
  9554. if (testWidth > maxWidth && n > 0) {
  9555. if (doDraw) context.fillText(line.trim(), x, y);
  9556. line = words[n] + ' ';
  9557. y += lineHeight;
  9558. }
  9559. else {
  9560. line = testLine;
  9561. }
  9562. }
  9563. if (doDraw) context.fillText(line.trim(), x, y);
  9564. return y;
  9565. }
  9566.  
  9567. const ctx = d20.engine.canvas.contextTop || d20.engine.canvas.contextContainer;
  9568.  
  9569. const scaleFact = (1 / d20.engine.canvasZoom);
  9570. const xOffset = pt.x > (d20.engine.canvasWidth / 2) ? -300 * scaleFact : 0;
  9571. const fontSize = scaleFact * 12;
  9572. const lineHeight = scaleFact * 18;
  9573. ctx.font = fontSize + "pt Arial Black";
  9574.  
  9575. const finalY = wrapText(ctx, txt, pt.x + xOffset, pt.y, 300 * scaleFact, lineHeight, false);
  9576.  
  9577. ctx.fillStyle = "rgba(255,255,255,0.75)";
  9578. ctx.beginPath();
  9579. ctx.rect(pt.x - (10 * scaleFact) + xOffset, pt.y - lineHeight, 320 * scaleFact, (finalY - pt.y) + (lineHeight + fontSize));
  9580. ctx.closePath();
  9581. ctx.fill();
  9582.  
  9583. ctx.fillStyle = "rgba(0,0,0,1)";
  9584. wrapText(ctx, txt, pt.x + xOffset, pt.y, 300 * scaleFact, lineHeight, true);
  9585. },
  9586. addTokenHover: () => {
  9587. // BEGIN ROLL20 CODE
  9588. d20.engine.drawOverlaysTop = function(e) {
  9589. e.globalCompositeOperation = "lighter";
  9590. d20.fx.render(e);
  9591. e.globalCompositeOperation = "source-over";
  9592. d20.engine.redrawSightTokens(e);
  9593. d20.engine.drawShadowMovements(e);
  9594. d20.engine.drawMeasurements(e);
  9595. d20.engine.drawPings(e);
  9596. d20.engine.drawInProgressDrawings(e);
  9597.  
  9598. // BEGIN MOD
  9599. d20plus._drawTokenHover();
  9600. // END MOD
  9601. };
  9602. // END ROLL20 CODE
  9603.  
  9604. // store data for the rendering function to access
  9605. d20.engine.canvas.on("mouse:move", (data, ...others) => {
  9606. if (data.target && data.target.model && data.e.shiftKey) {
  9607. d20.engine.renderTop();
  9608. const gmNotes = data.target.model.get("gmnotes");
  9609. const pt = d20.engine.canvas.getPointer(data.e);
  9610. pt.x -= d20.engine.currentCanvasOffset[0];
  9611. pt.y -= d20.engine.currentCanvasOffset[1];
  9612. d20plus._tokenHover = {
  9613. pt: pt,
  9614. text: gmNotes
  9615. }
  9616. } else {
  9617. if (d20plus._tokenHover) d20.engine.renderTop();
  9618. d20plus._tokenHover = null;
  9619. }
  9620. })
  9621. },
  9622.  
  9623. enhanceMarkdown: () => {
  9624. const OUT_STRIKE = "<span style='text-decoration: line-through'>$1</span>";
  9625.  
  9626. // BEGIN ROLL20 CODE
  9627. window.Markdown.parse = function(e) {
  9628. {
  9629. var t = e
  9630. , n = []
  9631. , i = [];
  9632. -1 != t.indexOf("\r\n") ? "\r\n" : -1 != t.indexOf("\n") ? "\n" : ""
  9633. }
  9634. return t = t.replace(/{{{([\s\S]*?)}}}/g, function(e) {
  9635. return n.push(e.substring(3, e.length - 3)),
  9636. "{{{}}}"
  9637. }),
  9638. t = t.replace(new RegExp("<pre>([\\s\\S]*?)</pre>","gi"), function(e) {
  9639. return i.push(e.substring(5, e.length - 6)),
  9640. "<pre></pre>"
  9641. }),
  9642. // BEGIN MOD
  9643. t = t.replace(/~~(.*?)~~/g, OUT_STRIKE),
  9644. // END MOD
  9645. t = t.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>"),
  9646. t = t.replace(/\*(.*?)\*/g, "<em>$1</em>"),
  9647. t = t.replace(/``(.*?)``/g, "<code>$1</code>"),
  9648. t = t.replace(/\[([^\]]+)\]\(([^)]+(\.png|\.gif|\.jpg|\.jpeg))\)/g, '<a href="$2"><img src="$2" alt="$1" /></a>'),
  9649. t = t.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>'),
  9650. t = t.replace(new RegExp("<pre></pre>","g"), function() {
  9651. return "<pre>" + i.shift() + "</pre>"
  9652. }),
  9653. t = t.replace(/{{{}}}/g, function() {
  9654. return n.shift()
  9655. })
  9656. }
  9657. // END ROLL20 CODE
  9658.  
  9659. // after a short delay, replace any old content in the chat
  9660. setTimeout(() => {
  9661. $(`.message`).each(function () {
  9662. $(this).html($(this).html().replace(/~~(.*?)~~/g, OUT_STRIKE))
  9663. })
  9664. }, 2500);
  9665. },
  9666.  
  9667. // JOURNAL UI //////////////////////////////////////////////////////////////////////////////////////////////////////
  9668.  
  9669. lastClickedFolderId: null,
  9670.  
  9671. addJournalCommands: () => {
  9672. // Create new Journal commands
  9673. // stash the folder ID of the last folder clicked
  9674. $("#journalfolderroot").on("contextmenu", ".dd-content", function (e) {
  9675. if ($(this).parent().hasClass("dd-folder")) {
  9676. const lastClicked = $(this).parent();
  9677. d20plus.lastClickedFolderId = lastClicked.attr("data-globalfolderid");
  9678. }
  9679.  
  9680.  
  9681. if ($(this).parent().hasClass("character")) {
  9682. $(`.Vetools-make-tokenactions`).show();
  9683. } else {
  9684. $(`.Vetools-make-tokenactions`).hide();
  9685. }
  9686. });
  9687.  
  9688. var first = $("#journalitemmenu ul li").first();
  9689. // "Make Tokenactions" option
  9690. first.after(`<li class="Vetools-make-tokenactions" data-action-type="additem">Make Tokenactions</li>`);
  9691. $("#journalitemmenu ul").on(window.mousedowntype, "li[data-action-type=additem]", function () {
  9692. var id = $currentItemTarget.attr("data-itemid");
  9693. var character = d20.Campaign.characters.get(id);
  9694. d20plus.log("Making Token Actions..");
  9695. if (character) {
  9696. var npc = character.attribs.find(function (a) {
  9697. return a.get("name").toLowerCase() == "npc";
  9698. });
  9699. var isNPC = npc ? parseInt(npc.get("current")) : 0;
  9700. if (isNPC) {
  9701. //Npc specific tokenactions
  9702. character.abilities.create({
  9703. name: "Perception",
  9704. istokenaction: true,
  9705. action: d20plus.actionMacroPerception
  9706. });
  9707. character.abilities.create({
  9708. name: "DR/Immunities",
  9709. istokenaction: true,
  9710. action: d20plus.actionMacroDrImmunities
  9711. });
  9712. character.abilities.create({
  9713. name: "Stats",
  9714. istokenaction: true,
  9715. action: d20plus.actionMacroStats
  9716. });
  9717. character.abilities.create({
  9718. name: "Saves",
  9719. istokenaction: true,
  9720. action: d20plus.actionMacroSaves
  9721. });
  9722. character.abilities.create({
  9723. name: "Skill-Check",
  9724. istokenaction: true,
  9725. action: d20plus.actionMacroSkillCheck
  9726. });
  9727. character.abilities.create({
  9728. name: "Ability-Check",
  9729. istokenaction: true,
  9730. action: d20plus.actionMacroAbilityCheck
  9731. });
  9732. } else {
  9733. //player specific tokenactions
  9734. //@{selected|repeating_attack_$0_atkname}
  9735. character.abilities.create({
  9736. name: "Attack 1",
  9737. istokenaction: true,
  9738. action: "%{selected|repeating_attack_$0_attack}"
  9739. });
  9740. character.abilities.create({
  9741. name: "Attack 2",
  9742. istokenaction: true,
  9743. action: "%{selected|repeating_attack_$1_attack}"
  9744. });
  9745. character.abilities.create({
  9746. name: "Attack 3",
  9747. istokenaction: true,
  9748. action: "%{selected|repeating_attack_$2_attack}"
  9749. });
  9750. character.abilities.create({
  9751. name: "Tool 1",
  9752. istokenaction: true,
  9753. action: "%{selected|repeating_tool_$0_tool}"
  9754. });
  9755. //" + character.get("name") + "
  9756. character.abilities.create({
  9757. name: "Whisper GM",
  9758. istokenaction: true,
  9759. action: "/w gm ?{Message to whisper the GM?}"
  9760. });
  9761. character.abilities.create({
  9762. name: "Favorite Spells",
  9763. istokenaction: true,
  9764. action: "/w @{character_name} &{template:npcaction} {{rname=Favorite Spells}} {{description=Favorite Spells are the first spells in each level of your spellbook.\n\r[Cantrip](~selected|repeating_spell-cantrip_$0_spell)\n[1st Level](~selected|repeating_spell-1_$0_spell)\n\r[2nd Level](~selected|repeating_spell-2_$0_spell)\n\r[3rd Level](~selected|repeating_spell-3_$0_spell)\n\r[4th Level](~selected|repeating_spell-4_$0_spell)\n\r[5th Level](~selected|repeating_spell-5_$0_spell)}}"
  9765. });
  9766. character.abilities.create({
  9767. name: "Dual Attack",
  9768. istokenaction: false,
  9769. action: "%{selected|repeating_attack_$0_attack}\n\r%{selected|repeating_attack_$0_attack}"
  9770. });
  9771. character.abilities.create({
  9772. name: "Saves",
  9773. istokenaction: true,
  9774. action: "@{selected|wtype}&{template:simple} @{selected|rtype}?{Save|Strength, +@{selected|strength_save_bonus}@{selected|pbd_safe}]]&#125;&#125; {{rname=Strength Save&#125;&#125 {{mod=@{selected|strength_save_bonus}&#125;&#125; {{r1=[[@{selected|d20}+@{selected|strength_save_bonus}@{selected|pbd_safe}]]&#125;&#125; |Dexterity, +@{selected|dexterity_save_bonus}@{selected|pbd_safe}]]&#125;&#125; {{rname=Dexterity Save&#125;&#125 {{mod=@{selected|dexterity_save_bonus}&#125;&#125; {{r1=[[@{selected|d20}+@{selected|dexterity_save_bonus}@{selected|pbd_safe}]]&#125;&#125; |Constitution, +@{selected|constitution_save_bonus}@{selected|pbd_safe}]]&#125;&#125; {{rname=Constitution Save&#125;&#125 {{mod=@{selected|constitution_save_bonus}&#125;&#125; {{r1=[[@{selected|d20}+@{selected|constitution_save_bonus}@{selected|pbd_safe}]]&#125;&#125; |Intelligence, +@{selected|intelligence_save_bonus}@{selected|pbd_safe}]]&#125;&#125; {{rname=Intelligence Save&#125;&#125 {{mod=@{selected|intelligence_save_bonus}&#125;&#125; {{r1=[[@{selected|d20}+@{selected|intelligence_save_bonus}@{selected|pbd_safe}]]&#125;&#125; |Wisdom, +@{selected|wisdom_save_bonus}@{selected|pbd_safe}]]&#125;&#125; {{rname=Wisdom Save&#125;&#125 {{mod=@{selected|wisdom_save_bonus}&#125;&#125; {{r1=[[@{selected|d20}+@{selected|wisdom_save_bonus}@{selected|pbd_safe}]]&#125;&#125; |Charisma, +@{selected|charisma_save_bonus}@{selected|pbd_safe}]]&#125;&#125; {{rname=Charisma Save&#125;&#125 {{mod=@{selected|charisma_save_bonus}&#125;&#125; {{r1=[[@{selected|d20}+@{selected|charisma_save_bonus}@{selected|pbd_safe}]]&#125;&#125;}@{selected|global_save_mod}@{selected|charname_output"
  9775. });
  9776. character.abilities.create({
  9777. name: "Skill-Check",
  9778. istokenaction: true,
  9779. action: "@{selected|wtype}&{template:simple} @{selected|rtype}?{Ability|Acrobatics, +@{selected|acrobatics_bonus}@{selected|pbd_safe} ]]&#125;&#125; {{rname=Acrobatics&#125;&#125; {{mod=@{selected|acrobatics_bonus}&#125;&#125; {{r1=[[ @{selected|d20} + @{selected|acrobatics_bonus}@{selected|pbd_safe} ]]&#125;&#125; |Animal Handling, +@{selected|animal_handling_bonus}@{selected|pbd_safe} ]]&#125;&#125; {{rname=Animal Handling&#125;&#125; {{mod=@{selected|animal_handling_bonus}&#125;&#125; {{r1=[[ @{selected|d20} + @{selected|animal_handling_bonus}@{selected|pbd_safe} ]]&#125;&#125; |Arcana, +@{selected|arcana_bonus}@{selected|pbd_safe} ]]&#125;&#125; {{rname=Arcana&#125;&#125; {{mod=@{selected|arcana_bonus}&#125;&#125; {{r1=[[ @{selected|d20} + @{selected|arcana_bonus}@{selected|pbd_safe} ]]&#125;&#125; |Athletics, +@{selected|athletics_bonus}@{selected|pbd_safe} ]]&#125;&#125; {{rname=Athletics&#125;&#125; {{mod=@{selected|athletics_bonus}&#125;&#125; {{r1=[[ @{selected|d20} + @{selected|athletics_bonus}@{selected|pbd_safe} ]]&#125;&#125; |Deception, +@{selected|deception_bonus}@{selected|pbd_safe} ]]&#125;&#125; {{rname=Deception&#125;&#125; {{mod=@{selected|deception_bonus}&#125;&#125; {{r1=[[ @{selected|d20} + @{selected|deception_bonus}@{selected|pbd_safe} ]]&#125;&#125; |History, +@{selected|history_bonus}@{selected|pbd_safe} ]]&#125;&#125; {{rname=History&#125;&#125; {{mod=@{selected|history_bonus}&#125;&#125; {{r1=[[ @{selected|d20} + @{selected|history_bonus}@{selected|pbd_safe} ]]&#125;&#125; |Insight, +@{selected|insight_bonus}@{selected|pbd_safe} ]]&#125;&#125; {{rname=Insight&#125;&#125; {{mod=@{selected|insight_bonus}&#125;&#125; {{r1=[[ @{selected|d20} + @{selected|insight_bonus}@{selected|pbd_safe} ]]&#125;&#125; |Intimidation, +@{selected|intimidation_bonus}@{selected|pbd_safe} ]]&#125;&#125; {{rname=Intimidation&#125;&#125; {{mod=@{selected|intimidation_bonus}&#125;&#125; {{r1=[[ @{selected|d20} + @{selected|intimidation_bonus}@{selected|pbd_safe} ]]&#125;&#125; |Investigation, +@{selected|investigation_bonus}@{selected|pbd_safe} ]]&#125;&#125; {{rname=Investigation&#125;&#125; {{mod=@{selected|investigation_bonus}&#125;&#125; {{r1=[[ @{selected|d20} + @{selected|investigation_bonus}@{selected|pbd_safe} ]]&#125;&#125; |Medicine, +@{selected|medicine_bonus}@{selected|pbd_safe} ]]&#125;&#125; {{rname=Medicine&#125;&#125; {{mod=@{selected|medicine_bonus}&#125;&#125; {{r1=[[ @{selected|d20} + @{selected|medicine_bonus}@{selected|pbd_safe} ]]&#125;&#125; |Nature, +@{selected|nature_bonus}@{selected|pbd_safe} ]]&#125;&#125; {{rname=Nature&#125;&#125; {{mod=@{selected|nature_bonus}&#125;&#125; {{r1=[[ @{selected|d20} + @{selected|nature_bonus}@{selected|pbd_safe} ]]&#125;&#125; |Perception, +@{selected|perception_bonus}@{selected|pbd_safe} ]]&#125;&#125; {{rname=Perception&#125;&#125; {{mod=@{selected|perception_bonus}&#125;&#125; {{r1=[[ @{selected|d20} + @{selected|perception_bonus}@{selected|pbd_safe} ]]&#125;&#125; |Performance, +@{selected|performance_bonus}@{selected|pbd_safe} ]]&#125;&#125; {{rname=Performance&#125;&#125; {{mod=@{selected|performance_bonus}&#125;&#125; {{r1=[[ @{selected|d20} + @{selected|performance_bonus}@{selected|pbd_safe} ]]&#125;&#125; |Persuasion, +@{selected|persuasion_bonus}@{selected|pbd_safe} ]]&#125;&#125; {{rname=Persuasion&#125;&#125; {{mod=@{selected|persuasion_bonus}&#125;&#125; {{r1=[[ @{selected|d20} + @{selected|persuasion_bonus}@{selected|pbd_safe} ]]&#125;&#125; |Religion, +@{selected|religion_bonus}@{selected|pbd_safe} ]]&#125;&#125; {{rname=Religion&#125;&#125; {{mod=@{selected|religion_bonus}&#125;&#125; {{r1=[[ @{selected|d20} + @{selected|religion_bonus}@{selected|pbd_safe} ]]&#125;&#125; |Sleight of Hand, +@{selected|sleight_of_hand_bonus}@{selected|pbd_safe} ]]&#125;&#125; {{rname=Sleight of Hand&#125;&#125; {{mod=@{selected|sleight_of_hand_bonus}&#125;&#125; {{r1=[[ @{selected|d20} + @{selected|sleight_of_hand_bonus}@{selected|pbd_safe} ]]&#125;&#125; |Stealth, +@{selected|stealth_bonus}@{selected|pbd_safe} ]]&#125;&#125; {{rname=Stealth&#125;&#125; {{mod=@{selected|stealth_bonus}&#125;&#125; {{r1=[[ @{selected|d20} + @{selected|stealth_bonus}@{selected|pbd_safe} ]]&#125;&#125; |Survival, +@{selected|survival_bonus}@{selected|pbd_safe} ]]&#125;&#125; {{rname=Survival&#125;&#125; {{mod=@{selected|survival_bonus}&#125;&#125; {{r1=[[ @{selected|d20} + @{selected|survival_bonus}@{selected|pbd_safe} ]]&#125;&#125; |Strength, +@{selected|strength_mod}@{selected|jack_attr}[STR]]]&#125;&#125; {{rname=Strength&#125;&#125; {{mod=@{selected|strength_mod}@{selected|jack_bonus}&#125;&#125; {{r1=[[ @{selected|d20} + @{selected|strength_mod}@{selected|jack_attr}[STR]]]&#125;&#125; |Dexterity, +@{selected|dexterity_mod}@{selected|jack_attr}[DEX]]]&#125;&#125; {{rname=Dexterity&#125;&#125; {{mod=@{selected|dexterity_mod}@{selected|jack_bonus}&#125;&#125; {{r1=[[ @{selected|d20} + @{selected|dexterity_mod}@{selected|jack_attr}[DEX]]]&#125;&#125; |Constitution, +@{selected|constitution_mod}@{selected|jack_attr}[CON]]]&#125;&#125; {{rname=Constitution&#125;&#125; {{mod=@{selected|constitution_mod}@{selected|jack_bonus}&#125;&#125; {{r1=[[ @{selected|d20} + @{selected|constitution_mod}@{selected|jack_attr}[CON]]]&#125;&#125; |Intelligence, +@{selected|intelligence_mod}@{selected|jack_attr}[INT]]]&#125;&#125; {{rname=Intelligence&#125;&#125; {{mod=@{selected|intelligence_mod}@{selected|jack_bonus}&#125;&#125; {{r1=[[ @{selected|d20} + @{selected|intelligence_mod}@{selected|jack_attr}[INT]]]&#125;&#125; |Wisdom, +@{selected|wisdom_mod}@{selected|jack_attr}[WIS]]]&#125;&#125; {{rname=Wisdom&#125;&#125; {{mod=@{selected|wisdom_mod}@{selected|jack_bonus}&#125;&#125; {{r1=[[ @{selected|d20} + @{selected|wisdom_mod}@{selected|jack_attr}[WIS]]]&#125;&#125; |Charisma, +@{selected|charisma_mod}@{selected|jack_attr}[CHA]]]&#125;&#125; {{rname=Charisma&#125;&#125; {{mod=@{selected|charisma_mod}@{selected|jack_bonus}&#125;&#125; {{r1=[[ @{selected|d20} + @{selected|charisma_mod}@{selected|jack_attr}[CHA]]]&#125;&#125; } @{selected|global_skill_mod} @{selected|charname_output}"
  9780. });
  9781. }
  9782. //for everyone
  9783. character.abilities.create({
  9784. name: "Initiative",
  9785. istokenaction: true,
  9786. action: d20plus.actionMacroInit
  9787. });
  9788. }
  9789. });
  9790.  
  9791. // "Duplicate" option
  9792. first.after("<li data-action-type=\"cloneitem\">Duplicate</li>");
  9793. first.after("<li style=\"height: 10px;\">&nbsp;</li>");
  9794. $("#journalitemmenu ul").on(window.mousedowntype, "li[data-action-type=cloneitem]", function () {
  9795. var id = $currentItemTarget.attr("data-itemid");
  9796. var character = d20.Campaign.characters.get(id);
  9797. var handout = d20.Campaign.handouts.get(id);
  9798. d20plus.log("Duplicating..");
  9799. if (character) {
  9800. character.editview.render();
  9801. character.editview.$el.find("button.duplicate").trigger("click");
  9802. }
  9803. if (handout) {
  9804. handout.view.render();
  9805. var json = handout.toJSON();
  9806. delete json.id;
  9807. json.name = "Copy of " + json.name;
  9808. handout.collection.create(json, {
  9809. success: function (h) {
  9810. handout._getLatestBlob("gmnotes", function (gmnotes) {
  9811. h.updateBlobs({gmnotes: gmnotes});
  9812. });
  9813. handout._getLatestBlob("notes", function (notes) {
  9814. h.updateBlobs({notes: notes});
  9815. });
  9816. }
  9817. });
  9818. }
  9819. });
  9820.  
  9821. // New command on FOLDERS
  9822. var last = $("#journalmenu ul li").last();
  9823. last.after("<li style=\"background-color: #FA5050; color: white;\" data-action-type=\"fulldelete\">Delete Folder + Contents</li>");
  9824. $("#journalmenu ul").on(window.mousedowntype, "li[data-action-type=fulldelete]", function () {
  9825. d20plus.importer.recursiveRemoveDirById(d20plus.lastClickedFolderId, true);
  9826. d20plus.lastClickedFolderId = null;
  9827. $("#journalmenu").hide();
  9828. });
  9829. },
  9830.  
  9831. // CSS /////////////////////////////////////////////////////////////////////////////////////////////////////////////
  9832. baseCssRules: [
  9833. // generic
  9834. {
  9835. s: ".display-inline-block",
  9836. r: "display: inline-block;"
  9837. },
  9838. // page view enhancement
  9839. {
  9840. s: "#page-toolbar",
  9841. r: "height: calc(90vh - 40px);"
  9842. },
  9843. {
  9844. s: "#page-toolbar .container",
  9845. r: "height: 100%; white-space: normal;"
  9846. },
  9847. {
  9848. s: "#page-toolbar .pages .availablepage",
  9849. r: "width: 100px; height: 100px;"
  9850. },
  9851. {
  9852. s: "#page-toolbar .pages .availablepage img.pagethumb",
  9853. r: "max-width: 60px; max-height: 60px;"
  9854. },
  9855. {
  9856. s: "#page-toolbar .pages .availablepage span",
  9857. r: "bottom: 1px;"
  9858. },
  9859. // search
  9860. {
  9861. s: ".Vetoolsresult",
  9862. r: "background: #ff8080;"
  9863. },
  9864. // config editor
  9865. {
  9866. s: "div.config-table-wrapper",
  9867. r: "min-height: 200px; width: 100%; height: 100%; max-height: 460px; overflow-y: auto; transform: translateZ(0);"
  9868. },
  9869. {
  9870. s: "table.config-table",
  9871. r: "width: 100%; table-layout: fixed;"
  9872. },
  9873. {
  9874. s: "table.config-table tbody tr:nth-child(odd)",
  9875. r: "background-color: #f8f8f8;"
  9876. },
  9877. {
  9878. s: "table.config-table tbody td > *",
  9879. r: "vertical-align: middle; margin: 0;"
  9880. },
  9881. {
  9882. s: ".config-name",
  9883. r: "display: inline-block; line-height: 35px; width: 100%;"
  9884. },
  9885. // tool list
  9886. {
  9887. s: ".tools-list",
  9888. r: "max-height: 70vh;"
  9889. },
  9890. {
  9891. s: ".tool-row",
  9892. r: "min-height: 40px; display: flex; flex-direction: row; align-items: center;"
  9893. },
  9894. {
  9895. s: ".tool-row:nth-child(odd)",
  9896. r: "background-color: #f0f0f0;"
  9897. },
  9898. {
  9899. s: ".tool-row > *",
  9900. r: "flex-shrink: 0;"
  9901. },
  9902. // warning overlay
  9903. {
  9904. s: "temp-warning",
  9905. r: "position: fixed; top: 12px; left: calc(50vw - 200px); z-index: 10000; width: 320px; background: transparent; color: red; font-weight: bold; font-size: 150%; font-variant: small-caps; border: 1px solid red; padding: 4px; text-align: center; border-radius: 4px;"
  9906. }
  9907. ],
  9908.  
  9909. baseCssRulesPlayer: [
  9910. {
  9911. s: ".player-hidden",
  9912. r: "display: none !important;"
  9913. }
  9914. ],
  9915.  
  9916. cssRules: [], // other scripts should populate this
  9917.  
  9918. // HTML AND TEMPLATES //////////////////////////////////////////////////////////////////////////////////////////////
  9919.  
  9920. addHtmlHeader: () => {
  9921. d20plus.log("Add HTML");
  9922. const $body = $("body");
  9923.  
  9924. const $wrpSettings = $(`<div id="betteR20-settings"/>`);
  9925. $("#mysettings > .content").children("hr").first().before($wrpSettings);
  9926.  
  9927. $wrpSettings.append(d20plus.settingsHtmlHeader);
  9928. if (window.is_gm) {
  9929. $(`#imagedialog`).find(`.searchbox`).find(`.tabcontainer`).first().after(d20plus.artTabHtml);
  9930. $(`a#button-add-external-art`).on(window.mousedowntype, d20plus.art.button);
  9931.  
  9932. $body.append(d20plus.configEditorHTML);
  9933. $body.append(d20plus.addArtHTML);
  9934. $body.append(d20plus.addArtMassAdderHTML);
  9935. $body.append(d20plus.toolsListHtml);
  9936. const $cfgEditor = $("#d20plus-configeditor");
  9937. $cfgEditor.dialog({
  9938. autoOpen: false,
  9939. resizable: true,
  9940. width: 800,
  9941. height: 650,
  9942. });
  9943. $cfgEditor.parent().append(d20plus.configEditorButtonBarHTML);
  9944. $("#d20plus-artfolder").dialog({
  9945. autoOpen: false,
  9946. resizable: true,
  9947. width: 800,
  9948. height: 400,
  9949. });
  9950. $("#d20plus-artmassadd").dialog({
  9951. autoOpen: false,
  9952. resizable: true,
  9953. width: 800,
  9954. height: 400,
  9955. });
  9956. }
  9957.  
  9958. // shared GM/player conent
  9959. // quick search box
  9960. const $iptSearch = $(`<input id="player-search" class="ui-autocomplete-input" autocomplete="off" placeholder="Quick search by name...">`);
  9961. const $wrprResults = $(`<div id="player-search-results" class="content searchbox"/>`);
  9962.  
  9963. if (window.is_gm) {
  9964. $iptSearch.css("width", "calc(100% - 5px)");
  9965. const $addPoint = $("#journal").find("button.btn.superadd");
  9966. $addPoint.after($wrprResults);
  9967. $addPoint.after(`<br>`);
  9968. $addPoint.after($iptSearch);
  9969. $addPoint.after(`<br><br>`);
  9970. } else {
  9971. const $wrprControls = $(`<div class="content searchbox" id="search-wrp-controls"/>`);
  9972. $(`#journal .content`).before($wrprControls).before($wrprResults);
  9973. $iptSearch.css("max-width", "calc(100% - 140px)");
  9974. $wrprControls.append($iptSearch);
  9975. }
  9976. d20plus.initQuickSearch($iptSearch, $wrprResults);
  9977. },
  9978.  
  9979. addHtmlFooter: () => {
  9980. const $wrpSettings = $(`#betteR20-settings`);
  9981. $wrpSettings.append(d20plus.settingsHtmlPtFooter);
  9982.  
  9983. $("#mysettings > .content a#button-edit-config").on(window.mousedowntype, d20plus.openConfigEditor);
  9984. d20plus.addTools();
  9985. },
  9986.  
  9987. settingsHtmlPtFooter:
  9988. `<p>
  9989. <a class="btn player-hidden" href="#" id="button-edit-config" style="margin-top: 3px;">Edit Config</a>
  9990. </p>
  9991. <p>
  9992. For help, advice, and updates, <a href="https://discord.gg/Zk34CUZ" target="_blank" style="color: #08c;">join our Discord!</a>
  9993. </p>
  9994. <p>
  9995. <a class="btn player-hidden" href="#" id="button-view-tools" style="margin-top: 3px;">Open Tools List</a>
  9996. </p>
  9997. <style id="dynamicStyle"></style>
  9998. `,
  9999.  
  10000. artTabHtml: `
  10001. <p><a class="btn" href="#" id="button-add-external-art">Manage External Art</a></p>
  10002. `,
  10003.  
  10004. addArtHTML: `
  10005. <div id="d20plus-artfolder" title="External Art" style="position: relative">
  10006. <p>Add external images by URL. Any direct link to an image should work.</p>
  10007. <p>
  10008. <input placeholder="Name*" id="art-list-add-name">
  10009. <input placeholder="URL*" id="art-list-add-url">
  10010. <a class="btn" href="#" id="art-list-add-btn">Add URL</a>
  10011. <a class="btn" href="#" id="art-list-multi-add-btn">Add Multiple URLs...</a>
  10012. <p/>
  10013. <hr>
  10014. <div id="art-list-container">
  10015. <input class="search" autocomplete="off" placeholder="Search list..." style="width: 100%;">
  10016. <br>
  10017. <p>
  10018. <span style="display: inline-block; width: 40%; font-weight: bold;">Name</span>
  10019. <span style="display: inline-block; font-weight: bold;">URL</span>
  10020. </p>
  10021. <ul class="list artlist" style="max-height: 600px; overflow-y: scroll; display: block; margin: 0;"></ul>
  10022. </div>
  10023. </div>`,
  10024.  
  10025. addArtMassAdderHTML: `
  10026. <div id="d20plus-artmassadd" title="Mass Add Art URLs">
  10027. <p>One entry per line; entry format: <b>[name]---[URL (direct link to image)]</b> <a class="btn" href="#" id="art-list-multi-add-btn-submit">Add URLs</a></p>
  10028. <p><textarea id="art-list-multi-add-area" style="width: 100%; height: 100%; min-height: 500px;" placeholder="My Image---http://pics.me/img1.png"></textarea></p>
  10029. </div>`,
  10030.  
  10031. artListHTML: `
  10032. <div id="Vetoolsresults">
  10033. <ol class="dd-list" id="image-search-none"><div class="alert white">No results found in 5etools for those keywords.</div></ol>
  10034.  
  10035. <ol class="dd-list" id="image-search-has-results">
  10036. <li class="dd-item dd-folder Vetoolsresult">
  10037. <div class="dd-content">
  10038. <div class="folder-title">From 5etools</div>
  10039. </div>
  10040.  
  10041. <ol class="dd-list Vetoolsresultfolder" id="custom-art-results"></ol>
  10042. </li>
  10043. </ol>
  10044. </div>`,
  10045.  
  10046. configEditorHTML: `
  10047. <div id="d20plus-configeditor" title="Config Editor" style="position: relative">
  10048. <!-- populate with js -->
  10049. </div>`,
  10050.  
  10051. configEditorButtonBarHTML: `
  10052. <div class="ui-dialog-buttonpane ui-widget-content ui-helper-clearfix">
  10053. <div class="ui-dialog-buttonset">
  10054. <button type="button" id="configsave" alt="Save" title="Save Config" class="btn" role="button" aria-disabled="false">
  10055. <span>Save</span>
  10056. </button>
  10057. </div>
  10058. </div>
  10059. `,
  10060.  
  10061. toolsListHtml: `
  10062. <div id="d20-tools-list" title="Tools List" style="position: relative">
  10063. <div class="tools-list">
  10064. <!-- populate with js -->
  10065. </div>
  10066. </div>
  10067. `,
  10068.  
  10069. template_TokenEditor: `
  10070. <script id='tmpl_tokeneditor' type='text/html'>
  10071. <div class='dialog largedialog tokeneditor' style='display: block;'>
  10072. <ul class='nav nav-tabs'>
  10073. <li class='active'>
  10074. <a data-tab='basic' href='javascript:void(0);'>Basic</a>
  10075. </li>
  10076. <li>
  10077. <a data-tab='advanced' href='javascript:void(0);'>Advanced</a>
  10078. </li>
  10079. </ul>
  10080. <div class='tab-content'>
  10081. <div class='basic tab-pane'>
  10082. <div style='float: left; width: 300px;'>
  10083. <div style='float: right; margin-right: 85px; font-size: 1.2em; position: relative; top: -4px; cursor: help;'>
  10084. <a class='showtip pictos' title="You can choose to have the token represent a Character from the Journal. If you do, the token's name, controlling players, and bar values will be based on the Character. Most times you'll just leave this set to None/Generic.">?</a>
  10085. </div>
  10086. <label>Represents Character</label>
  10087. <select class='represents'>
  10088. <option value=''>None/Generic Token</option>
  10089. <$ _.each(window.Campaign.activeCharacters(), function(char) { $>
  10090. <option value="<$!char.id$>"><$!char.get("name")$></option>
  10091. <$ }); $>
  10092. </select>
  10093. <div class='clear'></div>
  10094. <div style='float: right; margin-right: 75px;'>
  10095. <label>
  10096. <input class='showname' type='checkbox' value='1'>
  10097. Show nameplate?
  10098. </label>
  10099. </div>
  10100. <label>Name</label>
  10101. <input class='name' style='width: 210px;' type='text'>
  10102. <div class='clear'></div>
  10103. <label>Controlled By</label>
  10104. <$ if(this.character) { $>
  10105. <p>(Determined by Character settings)</p>
  10106. <$ } else { $>
  10107. <select class='controlledby chosen' multiple='true'>
  10108. <option value='all'>All Players</option>
  10109. <$ window.Campaign.players.each(function(player) { $>
  10110. <option value="<$!player.id$>"><$!player.get("displayname")$></option>
  10111. <$ }); $>
  10112. </select>
  10113. <$ } $>
  10114. <div class='clear' style='height: 10px;'></div>
  10115. <label>
  10116. Tint Color
  10117. </label>
  10118. <input class='tint_color colorpicker' type='text'>
  10119. <div class='clear'></div>
  10120. </div>
  10121. <div style='float: left; width: 300px;'>
  10122. <label>
  10123. <span class='bar_color_indicator' style='background-color: <$!window.Campaign.get('bar1_color')$>'></span>
  10124. Bar 1
  10125. </label>
  10126. <div class='clear' style='height: 1px;'></div>
  10127. <div class='inlineinputs' style='margin-top: 5px; margin-bottom: 5px;'>
  10128. <input class='bar1_value' type='text'>
  10129. /
  10130. <input class='bar1_max' type='text'>
  10131. <$ if(this.character) { $>
  10132. <div style='float: right;'>
  10133. <select class='bar1_link' style='width: 125px;'>
  10134. <option value=''>None</option>
  10135. <$ _.each(this.tokensettingsview.availAttribs(), function(attrib) { $>
  10136. <option value="<$!attrib.id$>"><$!attrib.name$>
  10137. <$ }); $>
  10138. </select>
  10139. <a class='pictos showtip' style='font-size: 1.2em; position: relative; top: -5px; margin-left: 10px; cursor: help;' title='You can choose an Attribute from the Character this token represents. The values for this bar will be synced to the values of that Attribute.'>?</a>
  10140. </div>
  10141. <$ } $>
  10142. </div>
  10143. <span style='color: #888;'>(Leave blank for no bar)</span>
  10144. <div class='clear'></div>
  10145. <label>
  10146. <span class='bar_color_indicator' style='background-color: <$!window.Campaign.get('bar2_color')$>'></span>
  10147. Bar 2
  10148. </label>
  10149. <div class='inlineinputs' style='margin-top: 5px; margin-bottom: 5px;'>
  10150. <input class='bar2_value' type='text'>
  10151. /
  10152. <input class='bar2_max' type='text'>
  10153. <$ if(this.character) { $>
  10154. <div style='float: right; margin-right: 30px;'>
  10155. <select class='bar2_link' style='width: 125px;'>
  10156. <option value=''>None</option>
  10157. <$ _.each(this.tokensettingsview.availAttribs(), function(attrib) { $>
  10158. <option value="<$!attrib.id$>"><$!attrib.name$>
  10159. <$ }); $>
  10160. </select>
  10161. </div>
  10162. <$ } $>
  10163. </div>
  10164. <span style='color: #888;'>(Leave blank for no bar)</span>
  10165. <div class='clear'></div>
  10166. <label>
  10167. <span class='bar_color_indicator' style='background-color: <$!window.Campaign.get('bar3_color')$>'></span>
  10168. Bar 3
  10169. </label>
  10170. <div class='inlineinputs' style='margin-top: 5px; margin-bottom: 5px;'>
  10171. <input class='bar3_value' type='text'>
  10172. /
  10173. <input class='bar3_max' type='text'>
  10174. <$ if(this.character) { $>
  10175. <div style='float: right; margin-right: 30px;'>
  10176. <select class='bar3_link' style='width: 125px;'>
  10177. <option value=''>None</option>
  10178. <$ _.each(this.tokensettingsview.availAttribs(), function(attrib) { $>
  10179. <option value="<$!attrib.id$>"><$!attrib.name$>
  10180. <$ }); $>
  10181. </select>
  10182. </div>
  10183. <$ } $>
  10184. </div>
  10185. <span style='color: #888;'>(Leave blank for no bar)</span>
  10186. <div class='clear' style='height: 10px;'></div>
  10187. <div style='float: left; width: 130px;'>
  10188. <div style='float: right;'>
  10189. <label>
  10190. <input class='aura1_square' type='checkbox'>
  10191. Square
  10192. </label>
  10193. </div>
  10194. <label>
  10195. Aura 1
  10196. </label>
  10197. <div class='inlineinputs' style='margin-top: 5px;'>
  10198. <input class='aura1_radius' type='text'>
  10199. <$!window.Campaign.activePage().get("scale_units")$>.
  10200. <input class='aura1_color colorpicker' type='text'>
  10201. </div>
  10202. </div>
  10203. <div style='float: left; width: 130px; margin-left: 20px;'>
  10204. <div style='float: right;'>
  10205. <label>
  10206. <input class='aura2_square' type='checkbox'>
  10207. Square
  10208. </label>
  10209. </div>
  10210. <label>
  10211. Aura 2
  10212. </label>
  10213. <div class='inlineinputs' style='margin-top: 5px;'>
  10214. <input class='aura2_radius' type='text'>
  10215. <$!window.Campaign.activePage().get("scale_units")$>.
  10216. <input class='aura2_color colorpicker' type='text'>
  10217. </div>
  10218. </div>
  10219. <div class='clear'></div>
  10220. </div>
  10221. <div class='clear'></div>
  10222. <hr>
  10223. <h4>
  10224. GM Notes
  10225. <span style='font-weight: regular; font-size: 0.9em;'>(Only visible to GMs)</span>
  10226. </h4>
  10227. <textarea class='gmnotes'></textarea>
  10228. <div class='clear'></div>
  10229. <label>&nbsp;</label>
  10230. </div>
  10231. <div class='advanced tab-pane'>
  10232. <div class='row-fluid'>
  10233. <div class='span6'>
  10234. <h4>Player Permissions</h4>
  10235. <div style='margin-left: 5px;'>
  10236. <div class='inlineinputs'>
  10237. <label style='width: 40px;'>Name</label>
  10238. <label>
  10239. <input class='showplayers_name' type='checkbox'>
  10240. See
  10241. </label>
  10242. <label>
  10243. <input class='playersedit_name' type='checkbox'>
  10244. Edit
  10245. </label>
  10246. </div>
  10247. <div class='clear' style='height: 5px;'></div>
  10248. <div class='inlineinputs'>
  10249. <label style='width: 40px;'>Bar 1</label>
  10250. <label>
  10251. <input class='showplayers_bar1' type='checkbox'>
  10252. See
  10253. </label>
  10254. <label>
  10255. <input class='playersedit_bar1' type='checkbox'>
  10256. Edit
  10257. </label>
  10258. </div>
  10259. <div class='clear' style='height: 5px;'></div>
  10260. <div class='inlineinputs'>
  10261. <label style='width: 40px;'>Bar 2</label>
  10262. <label>
  10263. <input class='showplayers_bar2' type='checkbox'>
  10264. See
  10265. </label>
  10266. <label>
  10267. <input class='playersedit_bar2' type='checkbox'>
  10268. Edit
  10269. </label>
  10270. </div>
  10271. <div class='clear' style='height: 5px;'></div>
  10272. <div class='inlineinputs'>
  10273. <label style='width: 40px;'>Bar 3</label>
  10274. <label>
  10275. <input class='showplayers_bar3' type='checkbox'>
  10276. See
  10277. </label>
  10278. <label>
  10279. <input class='playersedit_bar3' type='checkbox'>
  10280. Edit
  10281. </label>
  10282. </div>
  10283. <div class='clear' style='height: 5px;'></div>
  10284. <div class='inlineinputs'>
  10285. <label style='width: 40px;'>Aura 1</label>
  10286. <label>
  10287. <input class='showplayers_aura1' type='checkbox'>
  10288. See
  10289. </label>
  10290. <label>
  10291. <input class='playersedit_aura1' type='checkbox'>
  10292. Edit
  10293. </label>
  10294. </div>
  10295. <div class='clear' style='height: 5px;'></div>
  10296. <div class='inlineinputs'>
  10297. <label style='width: 40px;'>Aura 2</label>
  10298. <label>
  10299. <input class='showplayers_aura2' type='checkbox'>
  10300. See
  10301. </label>
  10302. <label>
  10303. <input class='playersedit_aura2' type='checkbox'>
  10304. Edit
  10305. </label>
  10306. </div>
  10307. <div class='clear' style='height: 10px;'></div>
  10308. <small style='text-align: left; font-size: 0.9em;'>
  10309. See: All Players can view
  10310. <br>
  10311. Edit: Controlling players can view and change
  10312. </small>
  10313. </div>
  10314. <div class='clear'></div>
  10315. </div>
  10316. <div class='span6'>
  10317. <h4>Emits Light</h4>
  10318. <div class='inlineinputs' style='margin-top: 5px; margin-bottom: 5px;'>
  10319. <input class='light_radius' type='text'>
  10320. <$!window.Campaign.activePage().get("scale_units")$>.
  10321. <input class='light_dimradius' type='text'>
  10322. <$!window.Campaign.activePage().get("scale_units")$>.
  10323. <input class='light_angle' placeholder='360' type='text'>
  10324. <span style='font-size: 2.0em;'>&deg;</span>
  10325. </div>
  10326. <span style='color: #888; padding-left: 5px;'>Light Radius / (optional) Start of Dim / Angle</span>
  10327. <div class='inlineinputs' style='margin-top: 5px;'>
  10328. <label style='margin-left: 7px;'>
  10329. <input class='light_otherplayers' type='checkbox'>
  10330. All Players See Light
  10331. </label>
  10332. </div>
  10333. <div class='inlineinputs' style='margin-top: 2px;'>
  10334. <label style='margin-left: 7px;'>
  10335. <input class='light_hassight' type='checkbox'>
  10336. Has Sight
  10337. </label>
  10338. <span style="margin-left: 9px; margin-right: 28px;">/</span>
  10339. Angle:
  10340. <input class='light_losangle' placeholder='360' type='text'>
  10341. <span style='font-size: 2.0em;'>&deg;</span>
  10342. </div>
  10343. <div class='inlineinputs' style='margin-left: 90px; margin-top: 5px;'>
  10344. <span style="margin-left: 8px; margin-right: 12px;">/</span>
  10345. Multiplyer:
  10346. <input class='light_multiplier' placeholder='1.0' style='margin-right: 10px;' type='text'>x</input>
  10347. </div>
  10348. <h4>Advanced Fog of War</h4>
  10349. <div class='inlineinputs' style='margin-top: 5px; margin-bottom: 5px;'>
  10350. <input class='advfow_viewdistance' type='text'>
  10351. <$!window.Campaign.activePage().get("scale_units")$>.
  10352. </div>
  10353. <span style='color: #888; padding-left: 5px;'>View Distance</span>
  10354. <!-- %h4 -->
  10355. <!-- Token Actions -->
  10356. <!-- %a.pictos.showtip(style="margin-left: 15px; cursor: help; font-size: 1.1em; position: relative; top: -2px;" title="Choose from Macros and Abilities of linked Character to show when token is selected") ? -->
  10357. <!-- %p -->
  10358. <!-- %strong Add New Token Action: -->
  10359. <!-- %br -->
  10360. <!-- %select.chosen(placeholder="Choose from the list...") -->
  10361. <!-- %option(value="") Choose from the list... -->
  10362. <!-- <$ if(this.character) { $> -->
  10363. <!-- <optgroup label="Abilities"> -->
  10364. <!-- <$ this.character.abilities.each(function(abil) { $> -->
  10365. <!-- <option value="ability|<$!abil.get('id')$>"><$!abil.get('name')$></option> -->
  10366. <!-- <$ }); $> -->
  10367. <!-- </optgroup> -->
  10368. <!-- <$ } $> -->
  10369. </div>
  10370. </div>
  10371. </div>
  10372. </div>
  10373. </div>
  10374. </script>
  10375. `,
  10376.  
  10377. template_pageSettings: `
  10378. <script id="tmpl_pagesettings" type="text/html">
  10379. <label style='padding-top: 4px;'>
  10380. <strong>Page Size</strong>
  10381. </label>
  10382. <input type="number" class="width" style="width: 50px;" value="<$!this.model.get("width")$>" />
  10383. un. by
  10384. <input type="number" class="height" style="width: 50px; margin-left: 5px;" value="<$!this.model.get("height")$>" />
  10385. un.
  10386. <small style='display: block; font-size: 0.9em; margin-left: 110px;'>width by height, 1 unit = 70 pixels</small>
  10387. <div class='clear' style='height: 15px;'></div>
  10388. <label style='margin-left: 55px; position: relative; top: 6px;'><strong>Scale:</strong> 1 unit =</label>
  10389. <input type="number" class="scale_number" style="width: 35px;" value="<$!this.model.get("scale_number")$>" />
  10390. <select class='scale_units' style='width: 50px; position: relative; top: 2px;'>
  10391. <option value='ft'>ft.</option>
  10392. <option value='m'>m.</option>
  10393. <option value='km'>km.</option>
  10394. <option value='mi'>mi.</option>
  10395. <option value='in'>in.</option>
  10396. <option value='cm'>cm.</option>
  10397. <option value='un'>un.</option>
  10398. <option value='hex'>hex</option>
  10399. <option value='sq.'>sq.</option>
  10400. </select>
  10401. <div class='clear' style='height: 15px;'></div>
  10402. <label>
  10403. <strong>Background</strong>
  10404. </label>
  10405. <input class='pagebackground' type='text'>
  10406. <hr>
  10407. <label style='position: relative; top: 8px;'>
  10408. <strong>Grid</strong>
  10409. </label>
  10410. <label class='checkbox'>
  10411. <input class='gridenabled' type='checkbox' value='1'>
  10412. Enabled, Size:
  10413. </label>
  10414. <input type="number" class="snappingincrement" style="width: 35px;" value="<$!this.model.get("snapping_increment")$>" /> units
  10415. <div class='clear' style='height: 7px;'></div>
  10416. <label style='margin-left: 55px; position: relative; top: 4px;'>
  10417. <a class='showtip pictos' title='Type of formula to use for calculating distances when using the measurement tool. Note: does not apply to Hex grids.'>?</a>
  10418. Diagonals
  10419. </label>
  10420. <select class='diagonaltype' style='width: 100px;'>
  10421. <option value="foure" <$ if(this.model.get("diagonaltype") == "foure") { $>selected<$ } $> >D&D 4E Compatible (Default)</option>
  10422. <option value="threefive" <$ if(this.model.get("diagonaltype") == "threefive") { $>selected<$ } $> >Pathfinder/3.5E Compatible</option>
  10423. <option value="pythagorean" <$ if(this.model.get("diagonaltype") == "pythagorean") { $>selected<$ } $> >Euclidean</option>
  10424. <option value="manhattan" <$ if(this.model.get("diagonaltype") == "manhattan") { $>selected<$ } $> >Manhattan</option>
  10425. </select>
  10426. <div class='clear' style='height: 7px;'></div>
  10427. <label style='margin-left: 55px; position: relative; top: 4px;'>Type</label>
  10428. <select class='gridtype' style='width: 100px;'>
  10429. <option value="square" <$ if(this.model.get("grid_type") == "square") { $>selected<$ } $> >Square</option>
  10430. <option value="hex" <$ if(this.model.get("grid_type") == "hex") { $>selected<$ } $> >Hex (V)</option>
  10431. <option value="hexr" <$ if(this.model.get("grid_type") == "hexr") { $>selected<$ } $> >Hex (H)</option>
  10432. </select>
  10433. <div class='clear' style='height: 2px;'></div>
  10434. <label class='checkbox' style='margin-left: 130px;'>
  10435. <input class='gridlabels' type='checkbox' value='1'>&nbsp; Show Labels (Hex Only)</input>
  10436. </label>
  10437. <div class='clear' style='height: 10px;'></div>
  10438. <label style='margin-left: 55px;'>Color</label>
  10439. <input class='gridcolor' type='text'>
  10440. <div class='clear' style='height: 7px;'></div>
  10441. <label style='margin-left: 55px;'>Opacity</label>
  10442. <div class='gridopacity'></div>
  10443. <div class='clear' style='height: 10px'></div>
  10444. <hr>
  10445. <label style='position: relative; top: -2px;'>
  10446. <strong>Fog of War</strong>
  10447. </label>
  10448. <label class='checkbox'>
  10449. <input class='darknessenabled' type='checkbox' value='1'>&nbsp; Enabled</input>
  10450. </label>
  10451. <hr>
  10452. <strong style="display: block;"><i>Requires a paid subscription or all players to use a betteR20 script</i></strong>
  10453. <label style='position: relative; top: 3px; width: 85px; padding-left: 15px;'>
  10454. <strong>Advanced Fog of War</strong>
  10455. </label>
  10456. <label class='checkbox'>
  10457. <input class='advancedfowenabled showtip' style='margin-top: 8px; margin-bottom: 8px;' type='checkbox' value='1'>&nbsp; Enabled</input>
  10458. </label>
  10459. <span class='no_grid' style='display: none;'>
  10460. , Size:
  10461. <input type="number" class="advancedfowgridsize" style="width: 30px;" value="<$!this.model.get("adv_fow_grid_size")$>" /> units
  10462. </span>
  10463. <br>
  10464. <label class='checkbox'>
  10465. <input class='advancedfowshowgrid showtip' title='By default the Advanced Fog of War hides the map grid anywhere revealed but the player can no longer see because of Dynamic Lighting. This option makes the grid always visible.' type='checkbox' value='1'>&nbsp; Show Grid</input>
  10466. </label>
  10467. <br>
  10468. <label class='checkbox' style='margin-left: 110px;'>
  10469. <input class='dimlightreveals showtip' title='By default the Advanced Fog of War will not be permanently revealed by Dynamic Lighting that is not bright. This option allows dim lighting to also reveal the fog.' type='checkbox' value='1'>&nbsp; Dim Light Reveals</input>
  10470. </label>
  10471. <br>
  10472. <br>
  10473. <label style='position: relative; top: -2px;'>
  10474. <strong>Dynamic Lighting</strong>
  10475. </label>
  10476. <label class='checkbox'>
  10477. <input class='lightingenabled showtip' type='checkbox' value='1'>&nbsp; Enabled</input>
  10478. </label>
  10479. <br>
  10480. <label class='checkbox'>
  10481. <input class='lightenforcelos showtip' title="Player's line of sight set by what tokens they can control." type='checkbox' value='1'>&nbsp; Enforce Line of Sight</input>
  10482. </label>
  10483. <br>
  10484. <br>
  10485. <label class='checkbox' style='margin-left: 110px;'>
  10486. <input class='lightingupdate' type='checkbox' value='1'>&nbsp; Only Update on Drop</input>
  10487. </label>
  10488. <br>
  10489. <label class='checkbox' style='margin-left: 110px;'>
  10490. <input class='lightrestrictmove' title="Don't allow player tokens to move through Dynamic Lighting walls. Can be enabled even if lighting is not used." type='checkbox' value='1'>&nbsp; Restrict Movement</input>
  10491. </label>
  10492. <br>
  10493. <label class='checkbox' style='margin-left: 110px;'>
  10494. <input class='lightglobalillum' title='Instead of darkness show light in all places players can see.' type='checkbox' value='1'>&nbsp; Global Illumination</input>
  10495. </label>
  10496. <hr>
  10497. <label style='font-weight: bold;'>GM Opacity</label>
  10498. <div class='fogopacity'></div>
  10499. <div class='clear'></div>
  10500. <hr>
  10501. <label style='font-weight: bold;'>Play on Load</label>
  10502. <select class='pagejukeboxtrigger' style='width: 180px;'></select>
  10503. <div class='clear'></div>
  10504. <hr>
  10505. <button class='delete btn btn-danger' style='float: right;'>
  10506. Delete Page
  10507. </button>
  10508. <button class='archive btn'>
  10509. Archive Page
  10510. </button>
  10511. <div class='clear'></div>
  10512. </script>
  10513. `,
  10514.  
  10515. template_actionsMenu: `
  10516. <script id='tmpl_actions_menu' type='text/html'>
  10517. <div class='actions_menu d20contextmenu'>
  10518. <ul>
  10519. <$ if(this.view && this.view.graphic.type == "image" && this.get("cardid") !== "") { $>
  10520. <li class='head hasSub' data-action-type='takecard'>Take Card</li>
  10521. <li class='head hasSub' data-action-type='flipcard'>Flip Card</li>
  10522. <$ } $>
  10523. <$ if(window.is_gm) { $>
  10524. <$ if(this.view && this.get("isdrawing") === false && window.currentEditingLayer != "map") { $>
  10525. <!-- BEGIN MOD -->
  10526. <li class='head hasSub' data-action-type='rollinit'>Roll Initiative</li>
  10527. <li class='head hasSub' data-action-type='rollsaves'>Roll Save</li>
  10528. <!-- END MOD -->
  10529. <li class='head hasSub' data-action-type='addturn'>Add Turn</li>
  10530. <$ } $>
  10531. <li class='head'>Edit</li>
  10532. <$ if(this.view) { $>
  10533. <li data-action-type='delete'>Delete</li>
  10534. <li data-action-type='copy'>Copy</li>
  10535. <$ } $>
  10536. <li data-action-type='paste'>Paste</li>
  10537. <li data-action-type='undo'>Undo</li>
  10538. <$ if(this.view) { $>
  10539. <li data-action-type='tofront'>To Front</li>
  10540. <li data-action-type='toback'>To Back</li>
  10541. <li class='head hasSub' data-menuname='advanced'>
  10542. Advanced &raquo;
  10543. <ul class='submenu' data-menuname='advanced'>
  10544. <li data-action-type='group'>Group</li>
  10545. <li data-action-type='ungroup'>Ungroup</li>
  10546. <$ if(this.get("type") == "image") { $>
  10547. <li class="<$ if (this && this.get("isdrawing")) { $>active<$ } $>" data-action-type="toggledrawing">Is Drawing</li>
  10548. <li class="<$ if (this && this.get("fliph")) { $>active<$ } $>" data-action-type="togglefliph">Flip Horizontal</li>
  10549. <li class="<$ if (this && this.get("flipv")) { $>active<$ } $>" data-action-type="toggleflipv">Flip Vertical</li>
  10550. <li data-action-type='setdimensions'>Set Dimensions</li>
  10551. <$ if(window.currentEditingLayer == "map") { $>
  10552. <li data-action-type='aligntogrid'>Align to Grid</li>
  10553. <$ } $>
  10554. <$ } $>
  10555. </ul>
  10556. </li>
  10557. <li class='head hasSub' data-menuname='positioning'>
  10558. Layer &raquo;
  10559. <ul class='submenu' data-menuname='positioning'>
  10560. <li data-action-type="tolayer_map" class='<$ if(this && this.get("layer") == "map") { $>active<$ } $>'>Map Layer</li>
  10561. <li data-action-type="tolayer_objects" class='<$ if(this && this.get("layer") == "objects") { $>active<$ } $>'>Token Layer</li>
  10562. <li data-action-type="tolayer_gmlayer" class='<$ if(this && this.get("layer") == "gmlayer") { $>active<$ } $>'>GM Layer</li>
  10563. <li data-action-type="tolayer_walls" class='<$ if(this && this.get("layer") == "walls") { $>active<$ } $>'>Lighting Layer (will not block LoS)</li>
  10564. </ul>
  10565. </li>
  10566. <$ } $>
  10567. <$ } $>
  10568. <$ if(this.view && this.get("sides") !== "" && this.get("cardid") === "") { $>
  10569. <li class='head hasSub' data-menuname='mutliside'>
  10570. Multi-Sided &raquo;
  10571. <ul class='submenu' data-menuname='multiside'>
  10572. <li data-action-type='side_random'>Random Side</li>
  10573. <li data-action-type='side_choose'>Choose Side</li>
  10574. </ul>
  10575. </li>
  10576. <$ } $>
  10577. </ul>
  10578. </div>
  10579. </script>
  10580. `,
  10581.  
  10582. template_charactereditor: `
  10583. <script id='tmpl_charactereditor' type='text/html'>
  10584. <div class='dialog largedialog charactereditor' style='display: block;'>
  10585. <!-- %ul.nav.nav-tabs -->
  10586. <!-- %li.active -->
  10587. <!-- %a(href="javascript:void(0);" data-tab="bioinfo") Bio & Info -->
  10588. <!-- %li -->
  10589. <!-- %a(href="javascript:void(0);" data-tab="attributesabilities") Attributes & Abilities -->
  10590. <div class='tab-content'>
  10591. <div class='bioinfo tab-pane'>
  10592. <div class='row-fluid'>
  10593. <div class='span5'>
  10594. <label>
  10595. <strong>Avatar</strong>
  10596. </label>
  10597. <$ if(true) { $>
  10598. <div class="avatar dropbox <$! this.get("avatar") != "" ? "filled" : "" $>" style="width: 95%;">
  10599. <div class="status"></div>
  10600. <div class="inner">
  10601. <$ if(this.get("avatar") == "") { $>
  10602. <h4 style="padding-bottom: 0px; marigin-bottom: 0px; color: #777;">Drop a file<small>(JPG, PNG, GIF)</small></h4>
  10603. <br /> or
  10604. <button class="btn">Choose a file...</button>
  10605. <input class="manual" type="file" />
  10606. <$ } else { $>
  10607. <img src="<$!this.get("avatar")$>" draggable="false" />
  10608. <div class='remove'><a href='#'>Remove</a></div>
  10609. <$ } $>
  10610. </div>
  10611. </div>
  10612. <$ } else { $>
  10613. <div class='avatar'>
  10614. <$ if(this.get("avatar") != "") { $>
  10615. <img src="<$!this.get("avatar")$>" draggable="false" />
  10616. <$ } $>
  10617. </div>
  10618. <$ } $>
  10619. <div class='clear'></div>
  10620. <!-- BEGIN MOD -->
  10621. <button class="btn character-image-by-url">Set Image from URL</button>
  10622. <div class='clear'></div>
  10623. <!-- END MOD -->
  10624. <$ if (window.is_gm) { $>
  10625. <label>
  10626. <strong>Default Token (Optional)</strong>
  10627. </label>
  10628. <div class="defaulttoken tokenslot <$! this.get("defaulttoken") !== "" ? "filled" : "" $> style="width: 95%;">
  10629. <$ if(this.get("defaulttoken") !== "") { $>
  10630. <img src="" draggable="false" />
  10631. <div class="remove"><a href="#">Remove</a></div>
  10632. <$ } else { $>
  10633. <button class="btn">Use Selected Token</button>
  10634. <small>Select a token on the tabletop to use as the Default Token</small>
  10635. <$ } $>
  10636. </div>
  10637. <$ } $>
  10638. </div>
  10639. <div class='span7'>
  10640. <label>
  10641. <strong>Name</strong>
  10642. </label>
  10643. <input class='name' type='text'>
  10644. <div class='clear'></div>
  10645. <$ if(window.is_gm) { $>
  10646. <label>
  10647. <strong>In Player's Journals</strong>
  10648. </label>
  10649. <select class='inplayerjournals chosen' multiple='true' style='width: 100%;'>
  10650. <option value="all">All Players</option>
  10651. <$ window.Campaign.players.each(function(player) { $>
  10652. <option value="<$!player.id$>"><$!player.get("displayname")$></option>
  10653. <$ }); $>
  10654. </select>
  10655. <div class='clear'></div>
  10656. <label>
  10657. <strong>Can Be Edited &amp; Controlled By</strong>
  10658. </label>
  10659. <select class='controlledby chosen' multiple='true' style='width: 100%;'>
  10660. <option value="all">All Players</option>
  10661. <$ window.Campaign.players.each(function(player) { $>
  10662. <option value="<$!player.id$>"><$!player.get("displayname")$></option>
  10663. <$ }); $>
  10664. </select>
  10665. <div class='clear'></div>
  10666. <label>
  10667. <strong>Tags</strong>
  10668. </label>
  10669. <input class='tags'>
  10670. <div class='clear'></div>
  10671. <hr>
  10672. <button class='delete btn btn-danger' style='float: right;'>
  10673. Delete
  10674. </button>
  10675. <button class='duplicate btn' style='margin-right: 10px;'>
  10676. Duplicate
  10677. </button>
  10678. <button class='archive btn'>
  10679. <$ if(this.get("archived")) { $>Restore from Archive<$ } else { $>Archive<$ } $>
  10680. </button>
  10681. <div class='clear'></div>
  10682. <$ } $>
  10683. <div class='clear'></div>
  10684. </div>
  10685. </div>
  10686. <div class='row-fluid'>
  10687. <div class='span12'>
  10688. <hr>
  10689. <label>
  10690. <strong>Bio & Info</strong>
  10691. </label>
  10692. <textarea class='bio'></textarea>
  10693. <div class='clear'></div>
  10694. <$ if(window.is_gm) { $>
  10695. <label>
  10696. <strong>GM Notes (Only visible to GM)</strong>
  10697. </label>
  10698. <textarea class='gmnotes'></textarea>
  10699. <div class='clear'></div>
  10700. <$ } $>
  10701. </div>
  10702. </div>
  10703. </div>
  10704. </div>
  10705. </div>
  10706. </script>
  10707. `,
  10708.  
  10709. template_handouteditor: `
  10710. <script id='tmpl_handouteditor' type='text/html'>
  10711. <div class='dialog largedialog handouteditor' style='display: block;'>
  10712. <div class='row-fluid'>
  10713. <div class='span12'>
  10714. <label>
  10715. <strong>Name</strong>
  10716. </label>
  10717. <input class='name' type='text'>
  10718. <div class='clear'></div>
  10719. <$ if (window.is_gm) { $>
  10720. <label>
  10721. <strong>In Player's Journals</strong>
  10722. </label>
  10723. <select class='inplayerjournals chosen' multiple='true' style='width: 100%;'>
  10724. <option value="all">All Players</option>
  10725. <$ window.Campaign.players.each(function(player) { $>
  10726. <option value="<$!player.id$>"><$!player.get("displayname")$></option>
  10727. <$ }); $>
  10728. </select>
  10729. <div class='clear'></div>
  10730. <label>
  10731. <strong>Can Be Edited By</strong>
  10732. </label>
  10733. <select class='controlledby chosen' multiple='true' style='width: 100%;'>
  10734. <option value="all">All Players</option>
  10735. <$ window.Campaign.players.each(function(player) { $>
  10736. <option value="<$!player.id$>"><$!player.get("displayname")$></option>
  10737. <$ }); $>
  10738. </select>
  10739. <div class='clear'></div>
  10740. <label>
  10741. <strong>Tags</strong>
  10742. </label>
  10743. <input class='tags'>
  10744. <div class='clear'></div>
  10745. <$ } $>
  10746. </div>
  10747. </div>
  10748. <div class='row-fluid'>
  10749. <div class='span12'>
  10750. <div class="avatar dropbox <$! this.get("avatar") != "" ? "filled" : "" $>">
  10751. <div class="status"></div>
  10752. <div class="inner">
  10753. <$ if(this.get("avatar") == "") { $>
  10754. <h4 style="padding-bottom: 0px; marigin-bottom: 0px; color: #777;">Drop a file</h4>
  10755. <br /> or
  10756. <button class="btn">Choose a file...</button>
  10757. <input class="manual" type="file" />
  10758. <$ } else { $>
  10759. <img src="<$!this.get("avatar")$>" />
  10760. <div class='remove'><a href='#'>Remove</a></div>
  10761. <$ } $>
  10762. </div>
  10763. </div>
  10764. <div class='clear'></div>
  10765. </div>
  10766. </div>
  10767. <!-- BEGIN MOD -->
  10768. <div class='row-fluid'>
  10769. <button class="btn handout-image-by-url">Set Image from URL</button>
  10770. <div class='clear'></div>
  10771. </div>
  10772. <!-- END MOD -->
  10773. <div class='row-fluid'>
  10774. <div class='span12'>
  10775. <label>
  10776. <strong>Description & Notes</strong>
  10777. </label>
  10778. <textarea class='notes'></textarea>
  10779. <div class='clear'></div>
  10780. <$ if(window.is_gm) { $>
  10781. <label>
  10782. <strong>GM Notes (Only visible to GM)</strong>
  10783. </label>
  10784. <textarea class='gmnotes'></textarea>
  10785. <div class='clear'></div>
  10786. <hr>
  10787. <button class='delete btn btn-danger' style='float: right;'>
  10788. Delete Handout
  10789. </button>
  10790. <button class='archive btn'>
  10791. <$ if(this.get("archived")) { $>Restore Handout from Archive<$ } else { $>Archive Handout<$ } $>
  10792. </button>
  10793. <div class='clear'></div>
  10794. <$ } $>
  10795. </div>
  10796. </div>
  10797. </div>
  10798. </script>
  10799. `,
  10800. };
  10801. };
  10802.  
  10803. const D20plus = function (version) {
  10804. d20plus.version = version;
  10805.  
  10806. // Window loaded
  10807. window.onload = function () {
  10808. window.unwatch("d20");
  10809. const checkLoaded = setInterval(function () {
  10810. if (!$("#loading-overlay").is(":visible")) {
  10811. clearInterval(checkLoaded);
  10812. d20plus.Init();
  10813. }
  10814. }, 1000);
  10815. };
  10816.  
  10817. /* object.watch polyfill by Eli Grey, http://eligrey.com */
  10818. if (!Object.prototype.watch) {
  10819. Object.defineProperty(Object.prototype, "watch", {
  10820. enumerable: false,
  10821. configurable: true,
  10822. writable: false,
  10823. value: function (prop, handler) {
  10824. var
  10825. oldval = this[prop],
  10826. newval = oldval,
  10827. getter = function () {
  10828. return newval;
  10829. },
  10830. setter = function (val) {
  10831. oldval = newval;
  10832. return (newval = handler.call(this, prop, oldval, val));
  10833. };
  10834. if (delete this[prop]) {
  10835. Object.defineProperty(this, prop, {
  10836. get: getter,
  10837. set: setter,
  10838. enumerable: true,
  10839. configurable: true
  10840. });
  10841. }
  10842. }
  10843. });
  10844. }
  10845. if (!Object.prototype.unwatch) {
  10846. Object.defineProperty(Object.prototype, "unwatch", {
  10847. enumerable: false,
  10848. configurable: true,
  10849. writable: false,
  10850. value: function (prop) {
  10851. var val = this[prop];
  10852. delete this[prop];
  10853. this[prop] = val;
  10854. }
  10855. });
  10856. }
  10857. /* end object.watch polyfill */
  10858.  
  10859. window.d20ext = {};
  10860. window.watch("d20ext", function (id, oldValue, newValue) {
  10861. d20plus.log("Set Development");
  10862. newValue.environment = "development";
  10863. Object.defineProperty(newValue, 'seenad', {
  10864. value: true
  10865. });
  10866. return newValue;
  10867. });
  10868. window.d20 = {};
  10869. window.watch("d20", function (id, oldValue, newValue) {
  10870. d20plus.log("Obtained d20 variable");
  10871. window.unwatch("d20ext");
  10872. window.d20ext.environment = "production";
  10873. newValue.environment = "production";
  10874. return newValue;
  10875. });
  10876. window.d20plus = d20plus;
  10877. d20plus.log("Injected");
  10878. };
  10879.  
  10880. document.addEventListener("DOMContentLoaded", function(event) {
  10881. // do some template injection
  10882. $("#tmpl_charactereditor").html($(d20plus.template_charactereditor).html());
  10883. $("#tmpl_handouteditor").html($(d20plus.template_handouteditor).html());
  10884. });
  10885.  
  10886. // if we are the topmost frame, inject
  10887. if (window.top === window.self) {
  10888. function strip (str) {
  10889. return str.substring(str.indexOf("\n") + 1, str.lastIndexOf("\n")) + "\n";
  10890. }
  10891.  
  10892. let stack = "function (version) {\n";
  10893. stack += strip(betteR20Base.toString());
  10894.  
  10895. for (let i = 0; i < SCRIPT_EXTENSIONS.length; ++i) {
  10896. stack += strip(SCRIPT_EXTENSIONS[i].toString())
  10897. }
  10898. stack += strip(D20plus.toString());
  10899.  
  10900. stack += "\n}";
  10901. unsafeWindow.eval("(" + stack + ")('" + GM_info.script.version + "')");
  10902. }
Add Comment
Please, Sign In to add comment