Guest User

Untitled

a guest
May 24th, 2018
1,028
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 388.96 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.7.3
  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. BASE_SITE_URL = "https://thegiddylimit.github.io/"; // FIXME restore when the main site is back up/automate this
  20.  
  21. SCRIPT_EXTENSIONS = [];
  22.  
  23. CONFIG_OPTIONS = {
  24. interface: {
  25. _name: "Interface",
  26. showCustomArtPreview: {
  27. name: "Show Custom Art Previews",
  28. default: true,
  29. _type: "boolean"
  30. }
  31. }
  32. };
  33.  
  34. addConfigOptions = function (category, options) {
  35. if (!CONFIG_OPTIONS[category]) CONFIG_OPTIONS[category] = options;
  36. else CONFIG_OPTIONS[category] = Object.assign(CONFIG_OPTIONS[category], options);
  37. };
  38.  
  39. OBJECT_DEFINE_PROPERTY = Object.defineProperty;
  40. Object.defineProperty = function (obj, prop, vals) {
  41. try {
  42. if (prop === "largefeats" || prop === "xlfeats") {
  43. vals.value = true;
  44. }
  45. OBJECT_DEFINE_PROPERTY(obj, prop, vals);
  46. } catch (e) {
  47. console.log("failed to define property:");
  48. console.log(e);
  49. console.log(obj, prop, vals);
  50. }
  51. };
  52.  
  53. UPPER_CANVAS_MOUSEDOWN = null;
  54. EventTarget.prototype.addEventListenerBase = EventTarget.prototype.addEventListener;
  55. EventTarget.prototype.addEventListener = function(type, listener, options, ...others) {
  56. if (type === "mousedown" && this === d20.engine.uppercanvas) UPPER_CANVAS_MOUSEDOWN = listener;
  57. this.addEventListenerBase(type, listener, options, ...others);
  58. };
  59.  
  60. const betteR205etools = function () {
  61. const DATA_URL = BASE_SITE_URL + "data/";
  62. const JS_URL = BASE_SITE_URL + "js/";
  63. const IMG_URL = BASE_SITE_URL + "img/";
  64.  
  65. const SPELL_DATA_DIR = `${DATA_URL}spells/`;
  66. const SPELL_META_URL = `${SPELL_DATA_DIR}roll20.json`;
  67. const MONSTER_DATA_DIR = `${DATA_URL}bestiary/`;
  68. const ADVENTURE_DATA_DIR = `${DATA_URL}adventure/`;
  69.  
  70. const ITEM_DATA_URL = `${DATA_URL}items.json`;
  71. const FEAT_DATA_URL = `${DATA_URL}feats.json`;
  72. const PSIONIC_DATA_URL = `${DATA_URL}psionics.json`;
  73. const OBJECT_DATA_URL = `${DATA_URL}objects.json`;
  74. const CLASS_DATA_URL = `${DATA_URL}classes.json`;
  75. const BACKGROUND_DATA_URL = `${DATA_URL}backgrounds.json`;
  76. const RACE_DATA_URL = `${DATA_URL}races.json`;
  77.  
  78. const HOMEBREW_REPO_URL = `https://api.github.com/repos/TheGiddyLimit/homebrew/`;
  79. // 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
  80. // embed shitty OAth2 details here to enable 5k/hour requests per IP (sending them with requests to the API relaxes the limit)
  81. // naturally these are client-visible and should not be used to secure anything
  82. const HOMEBREW_CLIENT_ID = `67e57877469da38a85a7`;
  83. const HOMEBREW_CLIENT_SECRET = `c00dede21ca63a855abcd9a113415e840aca3f92`;
  84.  
  85. const REQUIRED_PROPS = {
  86. "monster": [
  87. "ac",
  88. "alignment",
  89. "cha",
  90. "con",
  91. "cr",
  92. "dex",
  93. "hp",
  94. "int",
  95. "name",
  96. "passive",
  97. "size",
  98. "source",
  99. "speed",
  100. "str",
  101. "type",
  102. "wis"
  103. ],
  104. "spell": [
  105. "name",
  106. "level",
  107. "school",
  108. "time",
  109. "range",
  110. "components",
  111. "duration",
  112. "classes",
  113. "entries",
  114. "source"
  115. ],
  116. "item": [
  117. "name",
  118. "rarity",
  119. "source"
  120. ],
  121. "psionic": [
  122. "name",
  123. "source",
  124. "type"
  125. ],
  126. "feat": [
  127. "name",
  128. "source",
  129. "entries"
  130. ],
  131. "object": [
  132. "name",
  133. "source",
  134. "size",
  135. "type",
  136. "ac",
  137. "hp",
  138. "immune",
  139. "entries"
  140. ],
  141. "class": [
  142. "name",
  143. "source",
  144. "hd",
  145. "proficiency",
  146. "classTableGroups",
  147. "startingProficiencies",
  148. "startingEquipment",
  149. "classFeatures",
  150. "subclassTitle",
  151. "subclasses"
  152. ],
  153. "subclass": [
  154.  
  155. ],
  156. "background": [
  157. "name",
  158. "source",
  159. "skillProficiencies",
  160. "entries"
  161. ],
  162. "race": [
  163. "name",
  164. "source"
  165. ]
  166. };
  167.  
  168. let spellDataUrls = {};
  169. let spellMetaData = {};
  170. let monsterDataUrls = {};
  171. let monsterFluffDataUrls = {};
  172. let monsterFluffData = {};
  173. let adventureMetadata = {};
  174.  
  175. // build a big dictionary of sheet properties to be used as reference throughout // TODO use these as reference throughout
  176. function SheetAttribute (name, ogl, shaped) {
  177. this.name = name;
  178. this.ogl = ogl;
  179. this.shaped = shaped;
  180. }
  181.  
  182. NPC_SHEET_ATTRIBUTES = {};
  183. // these (other than the name, which is for display only) are all lowercased; any comparison should be lowercased
  184. NPC_SHEET_ATTRIBUTES["empty"] = new SheetAttribute("--Empty--", "", "");
  185. // TODO: implement custom entry (enable textarea)
  186. //NPC_SHEET_ATTRIBUTES["custom"] = new SheetAttribute("-Custom-", "-Custom-", "-Custom-");
  187. NPC_SHEET_ATTRIBUTES["npc_hpbase"] = new SheetAttribute("Avg HP", "npc_hpbase", "npc_hpbase");
  188. NPC_SHEET_ATTRIBUTES["npc_ac"] = new SheetAttribute("AC", "npc_ac", "ac");
  189. NPC_SHEET_ATTRIBUTES["passive"] = new SheetAttribute("Passive Perception", "passive", "passive");
  190. NPC_SHEET_ATTRIBUTES["npc_hpformula"] = new SheetAttribute("HP Formula", "npc_hpformula", "npc_hpformula");
  191. NPC_SHEET_ATTRIBUTES["npc_speed"] = new SheetAttribute("Speed", "npc_speed", "npc_speed");
  192. NPC_SHEET_ATTRIBUTES["spell_save_dc"] = new SheetAttribute("Spell Save DC", "spell_save_dc", "spell_save_DC");
  193. NPC_SHEET_ATTRIBUTES["npc_legendary_actions"] = new SheetAttribute("Legendary Actions", "npc_legendary_actions", "npc_legendary_actions");
  194. NPC_SHEET_ATTRIBUTES["npc_challenge"] = new SheetAttribute("CR", "npc_challenge", "challenge");
  195.  
  196. PC_SHEET_ATTRIBUTES = {};
  197. PC_SHEET_ATTRIBUTES["empty"] = new SheetAttribute("--Default--", "", "");
  198. PC_SHEET_ATTRIBUTES["hp"] = new SheetAttribute("Current HP", "hp", "HP");
  199. PC_SHEET_ATTRIBUTES["ac"] = new SheetAttribute("AC", "ac", "ac"); // TODO check shaped
  200. PC_SHEET_ATTRIBUTES["passive_wisdom"] = new SheetAttribute("Passive Perception", "passive_wisdom", "passive_wisdom"); // TODO check shaped
  201. PC_SHEET_ATTRIBUTES["speed"] = new SheetAttribute("Speed", "speed", "speed"); // TODO check shaped
  202. PC_SHEET_ATTRIBUTES["spell_save_dc"] = new SheetAttribute("Spell Save DC", "spell_save_dc", "spell_save_dc"); // TODO check shaped
  203.  
  204. addConfigOptions("token", {
  205. "_name": "Tokens",
  206. "bar1": {
  207. "name": "Bar 1 (NPC)",
  208. "default": "npc_hpbase",
  209. "_type": "_SHEET_ATTRIBUTE"
  210. },
  211. "bar1_pc": {
  212. "name": "Bar 1 (PC)",
  213. "default": "",
  214. "_type": "_SHEET_ATTRIBUTE_PC"
  215. },
  216. "bar1_max": {
  217. "name": "Set Bar 1 Max",
  218. "default": true,
  219. "_type": "boolean"
  220. },
  221. "bar1_reveal": {
  222. "name": "Reveal Bar 1",
  223. "default": false,
  224. "_type": "boolean"
  225. },
  226. "bar2": {
  227. "name": "Bar 2 (NPC)",
  228. "default": "npc_ac",
  229. "_type": "_SHEET_ATTRIBUTE"
  230. },
  231. "bar2_pc": {
  232. "name": "Bar 2 (PC)",
  233. "default": "",
  234. "_type": "_SHEET_ATTRIBUTE_PC"
  235. },
  236. "bar2_max": {
  237. "name": "Set Bar 2 Max",
  238. "default": false,
  239. "_type": "boolean"
  240. },
  241. "bar2_reveal": {
  242. "name": "Reveal Bar 2",
  243. "default": false,
  244. "_type": "boolean"
  245. },
  246. "bar3": {
  247. "name": "Bar 3 (NPC)",
  248. "default": "passive",
  249. "_type": "_SHEET_ATTRIBUTE"
  250. },
  251. "bar3_pc": {
  252. "name": "Bar 3 (PC)",
  253. "default": "",
  254. "_type": "_SHEET_ATTRIBUTE_PC"
  255. },
  256. "bar3_max": {
  257. "name": "Set Bar 3 Max",
  258. "default": false,
  259. "_type": "boolean"
  260. },
  261. "bar3_reveal": {
  262. "name": "Reveal Bar 3",
  263. "default": false,
  264. "_type": "boolean"
  265. },
  266. "rollHP": {
  267. "name": "Roll Token HP",
  268. "default": false,
  269. "_type": "boolean"
  270. },
  271. "maximiseHp": {
  272. "name": "Maximise Token HP",
  273. "default": false,
  274. "_type": "boolean"
  275. },
  276. "name": {
  277. "name": "Show Nameplate",
  278. "default": true,
  279. "_type": "boolean"
  280. },
  281. "name_reveal": {
  282. "name": "Reveal Nameplate",
  283. "default": false,
  284. "_type": "boolean"
  285. },
  286. "tokenactions": {
  287. "name": "Add TokenAction Macros on Import (Actions)",
  288. "default": true,
  289. "_type": "boolean"
  290. },
  291. "tokenactionsTraits": {
  292. "name": "Add TokenAction Macros on Import (Traits)",
  293. "default": true,
  294. "_type": "boolean"
  295. },
  296. "tokenactionsSkillsSaves": {
  297. "name": "Add TokenAction Macros on Import (Skills, Checks, and Saves)",
  298. "default": true,
  299. "_type": "boolean"
  300. },
  301. "tokenactionsSpells": {
  302. "name": "Add TokenAction Macros on Import (Spells)",
  303. "default": true,
  304. "_type": "boolean"
  305. },
  306. "namesuffix": {
  307. "name": "Append Text to Names on Import",
  308. "default": "",
  309. "_type": "String"
  310. }
  311. });
  312. addConfigOptions("import", {
  313. "_name": "Import",
  314. "importIntervalHandout": {
  315. "name": "Rest Time between Each Handout (msec)",
  316. "default": 100,
  317. "_type": "integer"
  318. },
  319. "importIntervalCharacter": {
  320. "name": "Rest Time between Each Character (msec)",
  321. "default": 2500,
  322. "_type": "integer"
  323. },
  324. "importFluffAs": {
  325. "name": "Import Creature Fluff As...",
  326. "default": "Bio",
  327. "_type": "_enum",
  328. "_values": ["Bio", "GM Notes"]
  329. },
  330. "whispermode": {
  331. "name": "Sheet Whisper Mode on Import",
  332. "default": "Toggle (Default GM)",
  333. "_type": "_WHISPERMODE"
  334. },
  335. "advantagemode": {
  336. "name": "Sheet Advantage Mode on Import",
  337. "default": "Toggle (Default Advantage)",
  338. "_type": "_ADVANTAGEMODE"
  339. },
  340. "damagemode": {
  341. "name": "Sheet Auto Roll Damage Mode on Import",
  342. "default": "Auto Roll",
  343. "_type": "_DAMAGEMODE"
  344. },
  345. "skipSenses": {
  346. "name": "Skip Importing Creature Senses",
  347. "default": false,
  348. "_type": "boolean"
  349. },
  350. });
  351. addConfigOptions("interface", {
  352. "_name": "Interface",
  353. "customTracker": {
  354. "name": "Add Additional Info to Tracker",
  355. "default": true,
  356. "_type": "boolean"
  357. },
  358. "trackerCol1": {
  359. "name": "Tracker Column 1",
  360. "default": "HP",
  361. "_type": "_FORMULA"
  362. },
  363. "trackerCol2": {
  364. "name": "Tracker Column 2",
  365. "default": "AC",
  366. "_type": "_FORMULA"
  367. },
  368. "trackerCol3": {
  369. "name": "Tracker Column 3",
  370. "default": "PP",
  371. "_type": "_FORMULA"
  372. },
  373. "minifyTracker": {
  374. "name": "Shrink Initiative Tracker Text",
  375. "default": false,
  376. "_type": "boolean"
  377. },
  378. "showDifficulty": {
  379. "name": "Show Difficulty in Tracker",
  380. "default": true,
  381. "_type": "boolean"
  382. },
  383. "emoji": {
  384. "name": "Add Emoji Replacement to Chat",
  385. "default": true,
  386. "_type": "boolean"
  387. },
  388. "showCustomArtPreview": {
  389. "name": "Show Custom Art Previews",
  390. "default": true,
  391. "_type": "boolean"
  392. }
  393. });
  394.  
  395. d20plus.sheet = "ogl";
  396. d20plus.remaining = 0;
  397. d20plus.initiative = {};
  398. d20plus.importer = {};
  399. d20plus.monsters = {};
  400. d20plus.spells = {};
  401. d20plus.psionics = {};
  402. d20plus.items = {};
  403. d20plus.feats = {};
  404. d20plus.races = {};
  405. d20plus.objects = {};
  406. d20plus.classes = {};
  407. d20plus.subclasses = {};
  408. d20plus.backgrounds = {};
  409. d20plus.adventures = {};
  410.  
  411. d20plus.advantageModes = ["Toggle (Default Advantage)", "Toggle", "Toggle (Default Disadvantage)", "Always", "Query", "Never"];
  412. d20plus.whisperModes = ["Toggle (Default GM)", "Toggle (Default Public)", "Always", "Query", "Never"];
  413. d20plus.damageModes = ["Auto Roll", "Don't Auto Roll"];
  414.  
  415. d20plus.formulas = {
  416. _options: ["--Empty--", "AC", "HP", "PP"],
  417. "ogl": {
  418. "cr": "@{npc_challenge}",
  419. "ac": "@{ac}",
  420. "npcac": "@{npc_ac}",
  421. "hp": "@{hp}",
  422. "pp": "@{passive_wisdom}",
  423. "macro": ""
  424. },
  425. "community": {
  426. "cr": "@{npc_challenge}",
  427. "ac": "@{AC}",
  428. "npcac": "@{AC}",
  429. "hp": "@{HP}",
  430. "pp": "10 + @{perception}",
  431. "macro": ""
  432. },
  433. "shaped": {
  434. "cr": "@{challenge}",
  435. "ac": "@{AC}",
  436. "npcac": "@{AC}",
  437. "hp": "@{HP}",
  438. "pp": "@{repeating_skill_$11_passive}",
  439. "macro": "shaped_statblock"
  440. }
  441. };
  442.  
  443. d20plus.scripts.push({name: "5etoolsutils", url: `${JS_URL}utils.js`});
  444. d20plus.scripts.push({name: "5etoolsrender", url: `${JS_URL}entryrender.js`});
  445.  
  446. d20plus.json = [
  447. {name: "spell index", url: `${SPELL_DATA_DIR}index.json`},
  448. {name: "spell metadata", url: SPELL_META_URL},
  449. {name: "bestiary index", url: `${MONSTER_DATA_DIR}index.json`},
  450. {name: "bestiary fluff index", url: `${MONSTER_DATA_DIR}fluff-index.json`},
  451. {name: "adventures index", url: `${DATA_URL}adventures.json`},
  452. {name: "basic items", url: `${DATA_URL}basicitems.json`}
  453. ];
  454.  
  455. // add JSON index/metadata
  456. d20plus.addJson = function (onLoadFunction) {
  457. d20plus.log("Load JSON");
  458. const onEachLoadFunction = function (name, url, data) {
  459. if (name === "spell index") spellDataUrls = data;
  460. else if (name === "spell metadata") spellMetaData = data;
  461. else if (name === "bestiary index") monsterDataUrls = data;
  462. else if (name === "bestiary fluff index") monsterFluffDataUrls = data;
  463. else if (name === "adventures index") adventureMetadata = data;
  464. else if (name === "basic items") {
  465. data.itemProperty.forEach(p => EntryRenderer.item._addProperty(p));
  466. data.itemType.forEach(t => EntryRenderer.item._addType(t));
  467. }
  468. else throw new Error(`Unhandled data from JSON ${name} (${url})`);
  469.  
  470. d20plus.log(`JSON [${name}] Loaded`);
  471. };
  472. d20plus.chainLoad(d20plus.json, 0, onEachLoadFunction, onLoadFunction);
  473. };
  474.  
  475. d20plus.handleConfigChange = function () {
  476. if (window.is_gm) {
  477. d20plus.log("Applying config");
  478. d20plus.setInitiativeShrink(d20plus.getCfgVal("interface", "minifyTracker"));
  479. d20.Campaign.initiativewindow.rebuildInitiativeList();
  480. d20plus.updateDifficulty();
  481. if (d20plus.art.refreshList) d20plus.art.refreshList();
  482. }
  483. };
  484.  
  485. // get the user config'd token HP bar
  486. d20plus.getCfgHpBarNumber = function () {
  487. const bars = [
  488. d20plus.getCfgVal("token", "bar1"),
  489. d20plus.getCfgVal("token", "bar2"),
  490. d20plus.getCfgVal("token", "bar3")
  491. ];
  492. return bars[0] === "npc_hpbase" ? 1 : bars[1] === "npc_hpbase" ? 2 : bars[2] === "npc_hpbase" ? 3 : null;
  493. };
  494.  
  495. d20plus.importer._playerImports = {};
  496. d20plus.importer.storePlayerImport = function (id, data) {
  497. d20plus.importer._playerImports[id] = data;
  498. };
  499.  
  500. d20plus.importer.retreivePlayerImport = function (id) {
  501. return d20plus.importer._playerImports[id];
  502. };
  503.  
  504. d20plus.importer.clearPlayerImport = function () {
  505. d20plus.importer._playerImports = {};
  506. };
  507.  
  508. d20plus.importer.addMeta = function (meta) {
  509. if (!meta) return;
  510. BrewUtil._sourceCache = BrewUtil._sourceCache || {};
  511. if (meta.sources) {
  512. meta.sources.forEach(src => {
  513. BrewUtil._sourceCache[src.json] = {abbreviation: src.abbreviation, full: src.full};
  514. })
  515. }
  516. };
  517.  
  518. // Page fully loaded and visible
  519. d20plus.Init = function () {
  520. d20plus.log("Init (v" + d20plus.version + ")");
  521. d20plus.settingsHtmlHeader = `<hr><h3>betteR20-5etools v${d20plus.version}</h3>`;
  522. d20plus.addAllCss();
  523. if (window.is_gm) {
  524. d20plus.log("Is GM");
  525. d20plus.enhancePageSelector();
  526. }
  527. else d20plus.log("Not GM. Some functionality will be unavailable.");
  528. d20plus.setSheet();
  529. d20plus.addScripts(d20plus.onScriptLoad);
  530. };
  531.  
  532. // continue init once JSON loads
  533. d20plus.onScriptLoad = function () {
  534. d20plus.addJson(d20plus.onJsonLoad);
  535. };
  536.  
  537. // continue init once scripts load
  538. d20plus.onJsonLoad = function () {
  539. IS_ROLL20 = true; // global variable from 5etools' utils.js
  540. BrewUtil._buildSourceCache = function () {
  541. // no-op when building source cache; we'll handle this elsewhere
  542. BrewUtil._sourceCache = BrewUtil._sourceCache || {};
  543. };
  544. EntryRenderer.getDefaultRenderer().setBaseUrl(BASE_SITE_URL);
  545. if (window.is_gm) d20plus.loadConfig(d20plus.onConfigLoad);
  546. else d20plus.onConfigLoad();
  547. };
  548.  
  549. // continue more init after config loaded
  550. d20plus.onConfigLoad = function () {
  551. if (window.is_gm) d20plus.loadArt(d20plus.onArtLoad);
  552. else d20plus.onArtLoad();
  553. };
  554.  
  555. // continue more init after art loaded
  556. d20plus.onArtLoad = function () {
  557. d20plus.bindDropLocations();
  558. d20plus.addHtmlHeader();
  559. d20plus.addCustomHTML();
  560. d20plus.addHtmlFooter();
  561. d20plus.enhanceMarkdown();
  562. d20plus.addProFeatures();
  563. d20plus.initArtFromUrlButtons();
  564. if (window.is_gm) {
  565. d20plus.addJournalCommands();
  566. d20plus.addSelectedTokenCommands();
  567. d20.Campaign.pages.each(d20plus.bindGraphics);
  568. d20.Campaign.activePage().collection.on("add", d20plus.bindGraphics);
  569. d20plus.addCustomArtSearch();
  570. d20plus.handleConfigChange();
  571. d20plus.addTokenHover();
  572. } else {
  573. d20plus.startPlayerConfigHandler();
  574. }
  575. d20plus.enhanceStatusEffects();
  576. d20plus.enhanceMeasureTool();
  577. d20plus.enhanceSnap();
  578. d20plus.enhanceChat();
  579. d20plus.log("All systems operational");
  580. d20plus.chatTag(`betteR20-5etools v${d20plus.version}`);
  581. };
  582.  
  583. // Bind Graphics Add on page
  584. d20plus.bindGraphics = function (page) {
  585. d20plus.log("Bind Graphics");
  586. try {
  587. if (page.get("archived") === false) {
  588. page.thegraphics.on("add", function (e) {
  589. var character = e.character;
  590. if (character) {
  591. var npc = character.attribs.find(function (a) {
  592. return a.get("name").toLowerCase() == "npc";
  593. });
  594. var isNPC = npc ? parseInt(npc.get("current")) : 0;
  595. // Set bars if configured to do so
  596. var barsList = ["bar1", "bar2", "bar3"];
  597. $.each(barsList, (i, barName) => {
  598. // PC config keys are suffixed "_pc"
  599. const confVal = d20plus.getCfgVal("token", `${barName}${isNPC ? "" : "_pc"}`);
  600. if (confVal) {
  601. const charAttr = character.attribs.find(a => a.get("name").toLowerCase() == confVal);
  602. if (charAttr) {
  603. e.attributes[barName + "_value"] = charAttr.get("current");
  604. if (d20plus.hasCfgVal("token", barName + "_max")) {
  605. if (d20plus.getCfgVal("token", barName + "_max") && !isNPC && confVal === "hp") { // player HP is current; need to set max to max
  606. e.attributes[barName + "_max"] = charAttr.get("max");
  607. } else {
  608. if (isNPC) {
  609. // TODO: Setting a value to empty/null does not overwrite existing values on the token.
  610. // setting a specific value does. Must figure this out.
  611. e.attributes[barName + "_max"] = d20plus.getCfgVal("token", barName + "_max") ? charAttr.get("current") : "";
  612. } else {
  613. // preserve default token for player tokens
  614. if (d20plus.getCfgVal("token", barName + "_max")) {
  615. e.attributes[barName + "_max"] = charAttr.get("current");
  616. }
  617. }
  618. }
  619. }
  620. if (d20plus.hasCfgVal("token", barName + "_reveal")) {
  621. e.attributes["showplayers_" + barName] = d20plus.getCfgVal("token", barName + "_reveal");
  622. }
  623. }
  624. }
  625. });
  626.  
  627. // NPC-only settings
  628. if (isNPC) {
  629. // Set Nametag
  630. if (d20plus.hasCfgVal("token", "name")) {
  631. e.attributes["showname"] = d20plus.getCfgVal("token", "name");
  632. if (d20plus.hasCfgVal("token", "name_reveal")) {
  633. e.attributes["showplayers_name"] = d20plus.getCfgVal("token", "name_reveal");
  634. }
  635. }
  636.  
  637. // Roll HP
  638. // TODO: npc_hpbase appears to be hardcoded here? Refactor for NPC_SHEET_ATTRIBUTES?
  639. if ((d20plus.getCfgVal("token", "rollHP") || d20plus.getCfgVal("token", "maximiseHp")) && d20plus.getCfgKey("token", "npc_hpbase")) {
  640. var hpf = character.attribs.find(function (a) {
  641. return a.get("name").toLowerCase() == NPC_SHEET_ATTRIBUTES["npc_hpformula"][d20plus.sheet];
  642. });
  643. var barName = d20plus.getCfgKey("token", "npc_hpbase");
  644. var hpformula = hpf.get("current");
  645.  
  646. if (hpformula && hpf) {
  647. if (d20plus.getCfgVal("token", "maximiseHp")) {
  648. const maxSum = hpformula.replace("d", "*");
  649. try {
  650. const max = eval(maxSum);
  651. if (!isNaN(max)) {
  652. e.attributes[barName + "_value"] = max;
  653. e.attributes[barName + "_max"] = max;
  654. }
  655. } catch (error) {
  656. d20plus.log("Error Maximising HP");
  657. console.log(error);
  658. }
  659. } else {
  660. d20plus.randomRoll(hpformula, function (result) {
  661. e.attributes[barName + "_value"] = result.total;
  662. e.attributes[barName + "_max"] = result.total;
  663. d20plus.log("Rolled HP for [" + character.get("name") + "]");
  664. }, function (error) {
  665. d20plus.log("Error Rolling HP Dice");
  666. console.log(error);
  667. });
  668. }
  669. }
  670. }
  671. }
  672. }
  673. });
  674. }
  675. } catch (e) {
  676. console.log("D20Plus bindGraphics Exception", e);
  677. console.log("PAGE", page);
  678. }
  679. };
  680.  
  681. // bind token HP to initiative tracker window HP field
  682. d20plus.bindToken = function (token) {
  683. function getInitTrackerToken () {
  684. const $window = $("#initiativewindow");
  685. if (!$window.length) return [];
  686. return $window.find(`li.token`).filter((i, e) => {
  687. return $(e).data("tokenid") === token.id;
  688. });
  689. }
  690.  
  691. const $initToken = getInitTrackerToken();
  692. if (!$initToken.length) return;
  693. const $iptHp = $initToken.find(`.hp.editable`);
  694. const npcFlag = token.character ? token.character.attribs.find((a) => {
  695. return a.get("name").toLowerCase() === "npc";
  696. }) : null;
  697. // if there's a HP column enabled
  698. if ($iptHp.length) {
  699. let toBind;
  700. if (!token.character || npcFlag && npcFlag.get("current") == "1") {
  701. const hpBar = d20plus.getCfgHpBarNumber();
  702. // and a HP bar chosen
  703. if (hpBar) {
  704. $iptHp.text(token.attributes[`bar${hpBar}_value`])
  705. }
  706.  
  707. toBind = (token, changes) => {
  708. const $initToken = getInitTrackerToken();
  709. if (!$initToken.length) return;
  710. const $iptHp = $initToken.find(`.hp.editable`);
  711. const hpBar = d20plus.getCfgHpBarNumber();
  712.  
  713. if ($iptHp && hpBar) {
  714. if (changes.changes[`bar${hpBar}_value`]) {
  715. $iptHp.text(token.changed[`bar${hpBar}_value`]);
  716. }
  717. }
  718. };
  719. } else {
  720. toBind = (token, changes) => {
  721. const $initToken = getInitTrackerToken();
  722. if (!$initToken.length) return;
  723. const $iptHp = $initToken.find(`.hp.editable`);
  724. if ($iptHp) {
  725. $iptHp.text(token.character.autoCalcFormula(d20plus.formulas[d20plus.sheet].hp));
  726. }
  727. }
  728. }
  729. // clean up old handler
  730. if (d20plus.tokenBindings[token.id]) token.off("change", d20plus.tokenBindings[token.id]);
  731. // add new handler
  732. d20plus.tokenBindings[token.id] = toBind;
  733. token.on("change", toBind);
  734. }
  735. };
  736. d20plus.tokenBindings = {};
  737.  
  738. // Determine difficulty of current encounter (iniativewindow)
  739. d20plus.getDifficulty = function () {
  740. var difficulty = "Unknown";
  741. var partyXPThreshold = [0, 0, 0, 0];
  742. var players = [];
  743. var npcs = [];
  744. try {
  745. $.each(d20.Campaign.initiativewindow.cleanList(), function (i, v) {
  746. var page = d20.Campaign.pages.get(v._pageid);
  747. if (page) {
  748. var token = page.thegraphics.get(v.id);
  749. if (token) {
  750. var char = token.character;
  751. if (char) {
  752. var npc = char.attribs.find(function (a) {
  753. return a.get("name").toLowerCase() === "npc";
  754. });
  755. if (npc && npc.get("current") === "1") {
  756. npcs.push(char);
  757. } else {
  758. var level = char.attribs.find(function (a) {
  759. return a.get("name").toLowerCase() === "level";
  760. });
  761. // Can't determine difficulty without level
  762. if (!level || partyXPThreshold === null) {
  763. partyXPThreshold = null;
  764. return;
  765. }
  766. // Total party threshold
  767. for (i = 0; i < partyXPThreshold.length; i++) partyXPThreshold[i] += Parser.levelToXpThreshold(level.get("current"))[i];
  768. players.push(players.length + 1);
  769. }
  770. }
  771. }
  772. }
  773. });
  774. if (!players.length) return difficulty;
  775. // If a player doesn't have level set, fail out.
  776. if (partyXPThreshold !== null) {
  777. var len = npcs.length;
  778. var multiplier = 0;
  779. var adjustedxp = 0;
  780. var xp = 0;
  781. var index = 0;
  782. // Adjust for number of monsters
  783. if (len < 2) index = 0;
  784. else if (len < 3) index = 1;
  785. else if (len < 7) index = 2;
  786. else if (len < 11) index = 3;
  787. else if (len < 15) index = 4;
  788. else
  789. index = 5;
  790. // Adjust for smaller parties
  791. if (players.length < 3) index++;
  792. // Set multiplier
  793. multiplier = d20plus.multipliers[index];
  794. // Total monster xp
  795. $.each(npcs, function (i, v) {
  796. var cr = v.attribs.find(function (a) {
  797. return a.get("name").toLowerCase() === "npc_challenge";
  798. });
  799. if (cr) xp += parseInt(Parser.crToXp(cr.get("current")));
  800. });
  801. // Encounter's adjusted xp
  802. adjustedxp = xp * multiplier;
  803. console.log("Party XP Threshold", partyXPThreshold);
  804. console.log("Adjusted XP", adjustedxp);
  805. // Determine difficulty
  806. if (adjustedxp < partyXPThreshold[0]) difficulty = "Trivial";
  807. else if (adjustedxp < partyXPThreshold[1]) difficulty = "Easy";
  808. else if (adjustedxp < partyXPThreshold[2]) difficulty = "Medium";
  809. else if (adjustedxp < partyXPThreshold[3]) difficulty = "Hard";
  810. else
  811. difficulty = "Deadly";
  812. }
  813. } catch (e) {
  814. console.log("D20Plus getDifficulty Exception", e);
  815. }
  816. return difficulty;
  817. };
  818.  
  819. /**
  820. * Takes a path made up of strings and arrays of strings, and turns it into one flat array of strings
  821. */
  822. d20plus.importer.getCleanPath = function (...path) {
  823. const clean = [];
  824. getStrings(clean, path);
  825. return clean.map(s => s.trim()).filter(s => s);
  826.  
  827. function getStrings (stack, toProc) {
  828. toProc.forEach(tp => {
  829. if (typeof tp === "string") {
  830. stack.push(tp);
  831. } else if (tp instanceof Array) {
  832. getStrings(stack, tp);
  833. } else {
  834. throw new Error("Object in path was not a string or an array")
  835. }
  836. });
  837. }
  838. };
  839.  
  840. d20plus.importer.makeDirTree = function (...path) {
  841. const parts = d20plus.importer.getCleanPath(path);
  842. // path e.g. d20plus.importer.makeDirTree("Spells", "Cantrips", "1")
  843. // roll20 allows a max directory depth of 4 :joy: (5, but the 5th level is unusable)
  844. if (parts.length > 4) throw new Error("Max directory depth exceeded! The maximum is 4.")
  845.  
  846. const madeSoFar = [];
  847.  
  848. const root = {i: d20plus.getJournalFolderObj()};
  849.  
  850. // roll20 folder management is dumb, so just pick the first folder with the right name if there's multiple
  851. let curDir = root;
  852. parts.forEach(toMake => {
  853. const existing = curDir.i.find((it) => {
  854. // n is folder name (only folders have the n property)
  855. return it.n && it.n === toMake && it.i;
  856. });
  857. if (!existing) {
  858. if (curDir.id) {
  859. d20.journal.addFolderToFolderStructure(toMake, curDir.id);
  860. } else {
  861. // root has no id
  862. d20.journal.addFolderToFolderStructure(toMake);
  863. }
  864. }
  865. d20.journal.refreshJournalList();
  866. madeSoFar.push(toMake);
  867.  
  868. // we have to save -> reread the entire directory JSON -> walk back to where we were
  869. let nextDir = {i: JSON.parse(d20.Campaign.get("journalfolder"))};
  870. madeSoFar.forEach(f => {
  871. nextDir = nextDir.i.find(dir => dir.n && (dir.n.toLowerCase() === f.toLowerCase()));
  872. });
  873.  
  874. curDir = nextDir;
  875. });
  876. return curDir;
  877. };
  878.  
  879. d20plus.importer.recursiveRemoveDirById = function (folderId, withConfirmation) {
  880. if (!withConfirmation || confirm("Are you sure you want to delete this folder, and everything in it? This cannot be undone.")) {
  881. const folder = $(`[data-globalfolderid='${folderId}']`);
  882. if (folder.length) {
  883. d20plus.log("Nuking folder...");
  884. const childItems = folder.find("[data-itemid]").each((i, e) => {
  885. const $e = $(e);
  886. const itemId = $e.attr("data-itemid");
  887. let toDel = d20.Campaign.handouts.get(itemId);
  888. toDel || (toDel = d20.Campaign.characters.get(itemId));
  889. if (toDel) toDel.destroy();
  890. });
  891. const childFolders = folder.find(`[data-globalfolderid]`).remove();
  892. folder.remove();
  893. $("#journalfolderroot").trigger("change");
  894. }
  895. }
  896. };
  897.  
  898. d20plus.importer.removeDirByPath = function (...path) {
  899. path = d20plus.importer.getCleanPath(path);
  900. return d20plus.importer._checkOrRemoveDirByPath(true, path);
  901. };
  902.  
  903. d20plus.importer.checkDirExistsByPath = function (...path) {
  904. path = d20plus.importer.getCleanPath(path);
  905. return d20plus.importer._checkOrRemoveDirByPath(false, path);
  906. };
  907.  
  908. d20plus.importer._checkOrRemoveDirByPath = function (doDelete, path) {
  909. const parts = d20plus.importer.getCleanPath(path);
  910.  
  911. const root = {i: d20plus.getJournalFolderObj()};
  912.  
  913. let curDir = root;
  914. for (let i = 0; i < parts.length; ++i) {
  915. const p = parts[i];
  916. let lastId;
  917. const existing = curDir.i.find((it) => {
  918. lastId = it.id;
  919. // n is folder name (only folders have the n property)
  920. return it.n && it.n === p;
  921. });
  922. if (!existing) return false;
  923. curDir = existing;
  924. if (i === parts.length - 1) {
  925. d20plus.importer.recursiveRemoveDirById(lastId, false);
  926. return true;
  927. }
  928. }
  929. };
  930.  
  931. d20plus.importer.removeFileByPath = function (...path) {
  932. path = d20plus.importer.getCleanPath(path);
  933. return d20plus.importer._checkOrRemoveFileByPath(true, path);
  934. };
  935.  
  936. d20plus.importer.checkFileExistsByPath = function (...path) {
  937. path = d20plus.importer.getCleanPath(path);
  938. return d20plus.importer._checkOrRemoveFileByPath(false, path);
  939. };
  940.  
  941. d20plus.importer._checkOrRemoveFileByPath = function (doDelete, path) {
  942. const parts = d20plus.importer.getCleanPath(path);
  943.  
  944. const root = {i: d20plus.getJournalFolderObj()};
  945.  
  946. let curDir = root;
  947. for (let i = 0; i < parts.length; ++i) {
  948. const p = parts[i];
  949. let lastId;
  950. const existing = curDir.i.find((it) => {
  951. if (i === parts.length - 1) {
  952. // for the last item, check handouts/characters to see if the match it (which could be a string ID)
  953. const char = d20.Campaign.characters.get(it);
  954. const handout = d20.Campaign.handouts.get(it);
  955. if ((char && char.get("name") === p) || (handout && handout.get("name") === p)) {
  956. lastId = it;
  957. return true;
  958. }
  959. } else {
  960. lastId = it.id;
  961. // n is folder name (only folders have the n property)
  962. return it.n && it.n === p;
  963. }
  964. return false;
  965. });
  966. if (!existing) return false;
  967. curDir = existing;
  968. if (i === parts.length - 1) {
  969. if (doDelete) {
  970. // on the last item, delete
  971. let toDel = d20.Campaign.handouts.get(lastId);
  972. toDel || (toDel = d20.Campaign.characters.get(lastId))
  973. if (toDel) toDel.destroy();
  974. }
  975. return true;
  976. }
  977. }
  978. return false;
  979. };
  980.  
  981. d20plus.formSrcUrl = function (dataDir, fileName) {
  982. return dataDir + fileName;
  983. };
  984.  
  985. d20plus.addCustomHTML = function () {
  986. function populateDropdown (dropdownId, inputFieldId, baseUrl, srcUrlObject, defaultSel, homebrewDir) {
  987. const defaultUrl = d20plus.formSrcUrl(baseUrl, srcUrlObject[defaultSel]);
  988. $(inputFieldId).val(defaultUrl);
  989. const dropdown = $(dropdownId);
  990. $.each(Object.keys(srcUrlObject), function (i, src) {
  991. dropdown.append($('<option>', {
  992. value: d20plus.formSrcUrl(baseUrl, srcUrlObject[src]),
  993. text: Parser.sourceJsonToFullCompactPrefix(src)
  994. }));
  995. });
  996. dropdown.append($('<option>', {
  997. value: "",
  998. text: "Custom"
  999. }));
  1000.  
  1001. const brewUrl = `${HOMEBREW_REPO_URL}contents/${homebrewDir}${d20plus.getAntiCacheSuffix()}&client_id=${HOMEBREW_CLIENT_ID}&client_secret=${HOMEBREW_CLIENT_SECRET}`;
  1002. DataUtil.loadJSON(brewUrl, (data, debugUrl) => {
  1003. if (data.message) console.error(debugUrl, data.message);
  1004. data.forEach(it => {
  1005. dropdown.append($('<option>', {
  1006. value: `${it.download_url}${d20plus.getAntiCacheSuffix()}`,
  1007. text: `Homebrew: ${it.name.trim().replace(/\.json$/i, "")}`
  1008. }));
  1009. });
  1010. }, brewUrl);
  1011.  
  1012. dropdown.val(defaultUrl);
  1013. dropdown.change(function () {
  1014. $(inputFieldId).val(this.value);
  1015. });
  1016. }
  1017.  
  1018. function populateBasicDropdown (dropdownId, inputFieldId, defaultSel, homebrewDir, addForPlayers) {
  1019. function doPopulate (dropdownId, inputFieldId) {
  1020. const $sel = $(dropdownId);
  1021. if (defaultSel) {
  1022. $(inputFieldId).val(defaultSel);
  1023. $sel.append($('<option>', {
  1024. value: defaultSel,
  1025. text: "Official Sources"
  1026. }));
  1027. }
  1028. $sel.append($('<option>', {
  1029. value: "",
  1030. text: "Custom"
  1031. }));
  1032.  
  1033. const brewUrl = `${HOMEBREW_REPO_URL}contents/${homebrewDir}${d20plus.getAntiCacheSuffix()}&client_id=${HOMEBREW_CLIENT_ID}&client_secret=${HOMEBREW_CLIENT_SECRET}`;
  1034. DataUtil.loadJSON(brewUrl, (data, debugUrl) => {
  1035. if (data.message) console.error(debugUrl, data.message);
  1036. data.forEach(it => {
  1037. $sel.append($('<option>', {
  1038. value: `${it.download_url}${d20plus.getAntiCacheSuffix()}`,
  1039. text: `Homebrew: ${it.name.trim().replace(/\.json$/i, "")}`
  1040. }));
  1041. });
  1042. }, brewUrl);
  1043.  
  1044. $sel.val(defaultSel);
  1045. $sel.change(function () {
  1046. $(inputFieldId).val(this.value);
  1047. });
  1048. }
  1049.  
  1050. doPopulate(dropdownId, inputFieldId, defaultSel, homebrewDir);
  1051. if (addForPlayers) doPopulate(`${dropdownId}-player`, `${inputFieldId}-player`, defaultSel, homebrewDir);
  1052. }
  1053.  
  1054. const $body = $("body");
  1055. if (window.is_gm) {
  1056. const $wrpSettings = $(`#betteR20-settings`);
  1057.  
  1058. $wrpSettings.append(d20plus.settingsHtmlImportHeader);
  1059. $wrpSettings.append(d20plus.settingsHtmlSelector);
  1060. $wrpSettings.append(d20plus.settingsHtmlPtMonsters);
  1061. $wrpSettings.append(d20plus.settingsHtmlPtItems);
  1062. $wrpSettings.append(d20plus.settingsHtmlPtSpells);
  1063. $wrpSettings.append(d20plus.settingsHtmlPtPsionics);
  1064. $wrpSettings.append(d20plus.settingsHtmlPtRaces);
  1065. $wrpSettings.append(d20plus.settingsHtmlPtFeats);
  1066. $wrpSettings.append(d20plus.settingsHtmlPtObjects);
  1067. $wrpSettings.append(d20plus.settingsHtmlPtClasses);
  1068. $wrpSettings.append(d20plus.settingsHtmlPtSubclasses);
  1069. $wrpSettings.append(d20plus.settingsHtmlPtBackgrounds);
  1070. $wrpSettings.append(d20plus.settingsHtmlPtAdventures);
  1071. $wrpSettings.append(d20plus.settingsHtmlPtImportFooter);
  1072.  
  1073. $("#mysettings > .content a#button-monsters-load").on(window.mousedowntype, d20plus.monsters.button);
  1074. $("#mysettings > .content a#button-monsters-load-all").on(window.mousedowntype, d20plus.monsters.buttonAll);
  1075. $("#mysettings > .content a#import-objects-load").on(window.mousedowntype, d20plus.objects.button);
  1076. $("#mysettings > .content a#button-adventures-load").on(window.mousedowntype, d20plus.adventures.button);
  1077.  
  1078. $("#mysettings > .content a#bind-drop-locations").on(window.mousedowntype, d20plus.bindDropLocations);
  1079. $("#initiativewindow .characterlist").before(d20plus.initiativeHeaders);
  1080.  
  1081. d20plus.setTurnOrderTemplate();
  1082. d20.Campaign.initiativewindow.rebuildInitiativeList();
  1083. d20plus.hpAllowEdit();
  1084. d20.Campaign.initiativewindow.model.on("change:turnorder", function () {
  1085. d20plus.updateDifficulty();
  1086. });
  1087. d20plus.updateDifficulty();
  1088.  
  1089. populateDropdown("#button-monsters-select", "#import-monster-url", MONSTER_DATA_DIR, monsterDataUrls, "MM", "creature");
  1090. populateBasicDropdown("#button-objects-select", "#import-objects-url", OBJECT_DATA_URL, "object");
  1091.  
  1092. populateAdventuresDropdown();
  1093.  
  1094. function populateAdventuresDropdown () {
  1095. const defaultAdvUrl = d20plus.formSrcUrl(ADVENTURE_DATA_DIR, "adventure-lmop.json");
  1096. const $iptUrl = $("#import-adventures-url");
  1097. $iptUrl.val(defaultAdvUrl);
  1098. $iptUrl.data("id", "lmop");
  1099. const $sel = $("#button-adventures-select");
  1100. adventureMetadata.adventure.forEach(a => {
  1101. $sel.append($('<option>', {
  1102. value: d20plus.formSrcUrl(ADVENTURE_DATA_DIR, `adventure-${a.id.toLowerCase()}.json|${a.id}`),
  1103. text: a.name
  1104. }));
  1105. });
  1106. $sel.append($('<option>', {
  1107. value: "",
  1108. text: "Custom"
  1109. }));
  1110. $sel.val(defaultAdvUrl);
  1111. $sel.change(() => {
  1112. const [url, id] = $sel.val().split("|");
  1113. $($iptUrl).val(url);
  1114. $iptUrl.data("id", id);
  1115. });
  1116. }
  1117.  
  1118. // import
  1119. $("a#button-spells-load").on(window.mousedowntype, () => d20plus.spells.button());
  1120. $("a#button-spells-load-all").on(window.mousedowntype, () => d20plus.spells.buttonAll());
  1121. $("a#import-psionics-load").on(window.mousedowntype, () => d20plus.psionics.button());
  1122. $("a#import-items-load").on(window.mousedowntype, () => d20plus.items.button());
  1123. $("a#import-races-load").on(window.mousedowntype, () => d20plus.races.button());
  1124. $("a#import-feats-load").on(window.mousedowntype, () => d20plus.feats.button());
  1125. $("a#import-classes-load").on(window.mousedowntype, () => d20plus.classes.button());
  1126. $("a#import-subclasses-load").on(window.mousedowntype, () => d20plus.subclasses.button());
  1127. $("a#import-backgrounds-load").on(window.mousedowntype, () => d20plus.backgrounds.button());
  1128. $("select#import-mode-select").on("change", () => d20plus.importer.importModeSwitch());
  1129. } else {
  1130. // player-only HTML if required
  1131. }
  1132.  
  1133. $body.append(d20plus.playerImportHtml);
  1134. const $winPlayer = $("#d20plus-playerimport");
  1135. const $appTo = $winPlayer.find(`.append-target`);
  1136. $appTo.append(d20plus.settingsHtmlSelectorPlayer);
  1137. $appTo.append(d20plus.settingsHtmlPtItemsPlayer);
  1138. $appTo.append(d20plus.settingsHtmlPtSpellsPlayer);
  1139. $appTo.append(d20plus.settingsHtmlPtPsionicsPlayer);
  1140. $appTo.append(d20plus.settingsHtmlPtRacesPlayer);
  1141. $appTo.append(d20plus.settingsHtmlPtFeatsPlayer);
  1142. $appTo.append(d20plus.settingsHtmlPtClassesPlayer);
  1143. $appTo.append(d20plus.settingsHtmlPtSubclassesPlayer);
  1144. $appTo.append(d20plus.settingsHtmlPtBackgroundsPlayer);
  1145.  
  1146. $winPlayer.dialog({
  1147. autoOpen: false,
  1148. resizable: true,
  1149. width: 800,
  1150. height: 650,
  1151. });
  1152.  
  1153. const $wrpPlayerImport = $(`
  1154. <div style="padding: 0 10px">
  1155. <div style="clear: both"></div>
  1156. </div>`);
  1157. 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>`)
  1158. .on("click", () => {
  1159. $winPlayer.dialog("open");
  1160. });
  1161. $wrpPlayerImport.prepend($btnPlayerImport);
  1162. $(`#journal`).prepend($wrpPlayerImport);
  1163.  
  1164. // SHARED WINDOWS/BUTTONS
  1165. // import
  1166. $("a#button-spells-load-player").on(window.mousedowntype, () => d20plus.spells.button(true));
  1167. $("a#button-spells-load-all-player").on(window.mousedowntype, () => d20plus.spells.buttonAll(true));
  1168. $("a#import-psionics-load-player").on(window.mousedowntype, () => d20plus.psionics.button(true));
  1169. $("a#import-items-load-player").on(window.mousedowntype, () => d20plus.items.button(true));
  1170. $("a#import-races-load-player").on(window.mousedowntype, () => d20plus.races.button(true));
  1171. $("a#import-feats-load-player").on(window.mousedowntype, () => d20plus.feats.button(true));
  1172. $("a#import-classes-load-player").on(window.mousedowntype, () => d20plus.classes.button(true));
  1173. $("a#import-subclasses-load-player").on(window.mousedowntype, () => d20plus.subclasses.button(true));
  1174. $("a#import-backgrounds-load-player").on(window.mousedowntype, () => d20plus.backgrounds.button(true));
  1175. $("select#import-mode-select-player").on("change", () => d20plus.importer.importModeSwitch());
  1176.  
  1177. $body.append(d20plus.importDialogHtml);
  1178. $body.append(d20plus.importListHTML);
  1179. $body.append(d20plus.importListPropsHTML);
  1180. $("#d20plus-import").dialog({
  1181. autoOpen: false,
  1182. resizable: false
  1183. });
  1184. $("#d20plus-importlist").dialog({
  1185. autoOpen: false,
  1186. resizable: true,
  1187. width: 1000,
  1188. height: 700
  1189. });
  1190. $("#d20plus-import-props").dialog({
  1191. autoOpen: false,
  1192. resizable: true,
  1193. width: 300,
  1194. height: 600
  1195. });
  1196.  
  1197. populateDropdown("#button-spell-select", "#import-spell-url", SPELL_DATA_DIR, spellDataUrls, "PHB", "spell");
  1198. populateDropdown("#button-spell-select-player", "#import-spell-url-player", SPELL_DATA_DIR, spellDataUrls, "PHB", "spell");
  1199.  
  1200. populateBasicDropdown("#button-items-select", "#import-items-url", ITEM_DATA_URL, "item", true);
  1201. populateBasicDropdown("#button-psionics-select", "#import-psionics-url", PSIONIC_DATA_URL, "psionic", true);
  1202. populateBasicDropdown("#button-feats-select", "#import-feats-url", FEAT_DATA_URL, "feat", true);
  1203. populateBasicDropdown("#button-races-select", "#import-races-url", RACE_DATA_URL, "race", true);
  1204. populateBasicDropdown("#button-classes-select", "#import-classes-url", CLASS_DATA_URL, "class", true);
  1205. populateBasicDropdown("#button-subclasses-select", "#import-subclasses-url", "", "subclass", true);
  1206. populateBasicDropdown("#button-backgrounds-select", "#import-backgrounds-url", BACKGROUND_DATA_URL, "background", true);
  1207.  
  1208. // bind tokens button
  1209. 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>`);
  1210. altBindButton.on("click", function () {
  1211. d20plus.bindDropLocations();
  1212. });
  1213.  
  1214. if (window.is_gm) {
  1215. const $addPoint = $(`#journal button.btn.superadd`);
  1216. altBindButton.css("margin-right", "5px");
  1217. $addPoint.after(altBindButton);
  1218. } else {
  1219. altBindButton.css("margin-top", "5px");
  1220. const $wrprControls = $(`#search-wrp-controls`);
  1221. $wrprControls.append(altBindButton);
  1222. }
  1223. $("#journal btn#bind-drop-locations").on(window.mousedowntype, d20plus.bindDropLocations);
  1224. };
  1225.  
  1226. d20plus.updateDifficulty = function () {
  1227. if (!$("div#initiativewindow").parent().is("body")) {
  1228. var $span = $("div#initiativewindow").parent().find(".ui-dialog-buttonpane > span.difficulty");
  1229. var $btnpane = $("div#initiativewindow").parent().find(".ui-dialog-buttonpane");
  1230. if (!$span.length) {
  1231. $btnpane.prepend(d20plus.difficultyHtml);
  1232. $span = $("div#initiativewindow").parent().find(".ui-dialog-buttonpane > span.difficulty");
  1233. }
  1234. if (d20plus.getCfgVal("interface", "showDifficulty")) {
  1235. $span.text("Difficulty: " + d20plus.getDifficulty());
  1236. $span.show();
  1237. } else {
  1238. $span.hide();
  1239. }
  1240. }
  1241. };
  1242.  
  1243. // bind tokens to the initiative tracker
  1244. d20plus.bindTokens = function () {
  1245. // Gets a list of all the tokens on the current page:
  1246. const curTokens = d20.Campaign.pages.get(d20.Campaign.activePage()).thegraphics.toArray();
  1247. curTokens.forEach(t => {
  1248. d20plus.bindToken(t);
  1249. });
  1250. };
  1251.  
  1252. // bind drop locations on sheet to accept custom handouts
  1253. d20plus.bindDropLocations = function () {
  1254. if (window.is_gm) {
  1255. // Bind Spells and Items, add compendium-item to each of them
  1256. var journalFolder = d20.Campaign.get("journalfolder");
  1257. if (journalFolder === "") {
  1258. d20.journal.addFolderToFolderStructure("Spells");
  1259. d20.journal.addFolderToFolderStructure("Psionics");
  1260. d20.journal.addFolderToFolderStructure("Items");
  1261. d20.journal.addFolderToFolderStructure("Feats");
  1262. d20.journal.addFolderToFolderStructure("Classes");
  1263. d20.journal.addFolderToFolderStructure("Subclasses");
  1264. d20.journal.addFolderToFolderStructure("Backgrounds");
  1265. d20.journal.addFolderToFolderStructure("Races");
  1266. d20.journal.refreshJournalList();
  1267. journalFolder = d20.Campaign.get("journalfolder");
  1268. }
  1269. }
  1270.  
  1271. function addClasses (folderName) {
  1272. $(`#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");
  1273. }
  1274.  
  1275. addClasses("Spells");
  1276. addClasses("Psionics");
  1277. addClasses("Items");
  1278. addClasses("Feats");
  1279. addClasses("Classes");
  1280. addClasses("Subclasses");
  1281. addClasses("Backgrounds");
  1282. addClasses("Races");
  1283.  
  1284. // if player, force-enable dragging
  1285. if (!window.is_gm) {
  1286. $(`.Vetools-draggable`).draggable({
  1287. revert: true,
  1288. distance: 10,
  1289. revertDuration: 0,
  1290. helper: "clone",
  1291. handle: ".namecontainer",
  1292. appendTo: "body",
  1293. scroll: true,
  1294. start: function () {
  1295. $("#journalfolderroot").addClass("externaldrag")
  1296. },
  1297. stop: function () {
  1298. $("#journalfolderroot").removeClass("externaldrag")
  1299. }
  1300. });
  1301. }
  1302.  
  1303. d20.Campaign.characters.models.each(function (v, i) {
  1304. v.view.rebindCompendiumDropTargets = function () {
  1305. // ready character sheet for draggable
  1306. $(".sheet-compendium-drop-target").each(function () {
  1307. $(this).droppable({
  1308. hoverClass: "dropping",
  1309. tolerance: "pointer",
  1310. activeClass: "active-drop-target",
  1311. accept: ".compendium-item",
  1312. drop: function (t, i) {
  1313. var characterid = $(".characterdialog").has(t.target).attr("data-characterid");
  1314. var character = d20.Campaign.characters.get(characterid).view;
  1315. var inputData;
  1316. const $hlpr = $(i.helper[0]);
  1317.  
  1318. let doSetDirty = true;
  1319.  
  1320. if ($hlpr.hasClass("handout")) {
  1321. console.log("Handout item dropped onto target!");
  1322. t.originalEvent.dropHandled = !0;
  1323.  
  1324. if ($hlpr.hasClass(`player-imported`)) {
  1325. const data = d20plus.importer.retreivePlayerImport($hlpr.attr("data-playerimportid"));
  1326. handleData(data);
  1327. } else {
  1328. var id = $hlpr.attr("data-itemid");
  1329. var handout = d20.Campaign.handouts.get(id);
  1330. console.log(character);
  1331. var data = "";
  1332. if (window.is_gm) {
  1333. handout._getLatestBlob("gmnotes", function (gmnotes) {
  1334. data = gmnotes;
  1335. handout.updateBlobs({gmnotes: gmnotes});
  1336. handleData(JSON.parse(data));
  1337. });
  1338. } else {
  1339. handout._getLatestBlob("notes", function (notes) {
  1340. data = $(notes).filter("del").html();
  1341. handleData(JSON.parse(data));
  1342. });
  1343. }
  1344. }
  1345.  
  1346. function handleData (data) {
  1347. const extraDirty = [];
  1348.  
  1349. // TODO remove Feat workaround when roll20 supports feat drag-n-drop properly
  1350. if (data.data.Category === "Feats") {
  1351. const rowId = d20plus.generateRowId();
  1352. character.model.attribs.create({
  1353. "name": `repeating_traits_${rowId}_options-flag`,
  1354. "current": "0"
  1355. });
  1356.  
  1357. character.model.attribs.create({
  1358. "name": `repeating_traits_${rowId}_name`,
  1359. "current": data.name
  1360. });
  1361.  
  1362. character.model.attribs.create({
  1363. "name": `repeating_traits_${rowId}_description`,
  1364. "current": data.Vetoolscontent
  1365. });
  1366.  
  1367. character.model.attribs.create({
  1368. "name": `repeating_traits_${rowId}_source`,
  1369. "current": "Feat"
  1370. });
  1371.  
  1372. character.model.view._updateSheetValues();
  1373. const dirty = [];
  1374. $.each(d20.journal.customSheets.attrDeps, function (i, v) {
  1375. dirty.push(i);
  1376. });
  1377. d20.journal.notifyWorkersOfAttrChanges(character.model.view.model.id, dirty, true);
  1378. } else if (data.data.Category === "Backgrounds") { // TODO remove Background workaround when roll20 supports background drag-n-drop properly
  1379. const bg = data.Vetoolscontent;
  1380.  
  1381. const renderer = new EntryRenderer();
  1382. renderer.setBaseUrl(BASE_SITE_URL);
  1383. const renderStack = [];
  1384. let feature;
  1385. bg.entries.forEach(e => {
  1386. if (e.name && e.name.includes("Feature:")) {
  1387. feature = JSON.parse(JSON.stringify(e));
  1388. feature.name = feature.name.replace("Feature:", "").trim();
  1389. }
  1390. });
  1391. if (feature) renderer.recursiveEntryRender({entries: feature.entries}, renderStack);
  1392.  
  1393. d20plus.importer.addOrUpdateAttr(character.model, "background", bg.name);
  1394.  
  1395. const fRowId = d20plus.generateRowId();
  1396. character.model.attribs.create({
  1397. name: `repeating_traits_${fRowId}_name`,
  1398. current: bg.name
  1399. });
  1400. character.model.attribs.create({
  1401. name: `repeating_traits_${fRowId}_source`,
  1402. current: "Background"
  1403. });
  1404. character.model.attribs.create({
  1405. name: `repeating_traits_${fRowId}_source_type`,
  1406. current: bg.name
  1407. });
  1408. if (renderStack.length) {
  1409. character.model.attribs.create({
  1410. name: `repeating_traits_${fRowId}_description`,
  1411. current: d20plus.importer.getCleanText(renderStack.join(""))
  1412. });
  1413. }
  1414. character.model.attribs.create({
  1415. name: `repeating_traits_${fRowId}_options-flag`,
  1416. current: "0"
  1417. });
  1418.  
  1419. if (bg.skillProficiencies) {
  1420. const skills = bg.skillProficiencies.split(",").map(s => s.toLowerCase().trim().replace(/ /g, "_"));
  1421. skills.forEach(s => {
  1422. d20plus.importer.addOrUpdateAttr(character.model, `${s}_prof`, `(@{pb}*@{${s}_type})`);
  1423. });
  1424. }
  1425. } else if (data.data.Category === "Races") { // TODO remove Race workaround when roll20 supports background drag-n-drop properly
  1426. const race = data.Vetoolscontent;
  1427.  
  1428. d20plus.importer.addOrUpdateAttr(character.model, `race`, race.name);
  1429. d20plus.importer.addOrUpdateAttr(character.model, `speed`, Parser.getSpeedString(race));
  1430. race.entries.forEach(e => {
  1431. const renderer = new EntryRenderer();
  1432. renderer.setBaseUrl(BASE_SITE_URL);
  1433. const renderStack = [];
  1434. renderer.recursiveEntryRender({entries: e.entries}, renderStack);
  1435.  
  1436. const fRowId = d20plus.generateRowId();
  1437. character.model.attribs.create({
  1438. name: `repeating_traits_${fRowId}_name`,
  1439. current: e.name
  1440. });
  1441. character.model.attribs.create({
  1442. name: `repeating_traits_${fRowId}_source`,
  1443. current: "Race"
  1444. });
  1445. character.model.attribs.create({
  1446. name: `repeating_traits_${fRowId}_source_type`,
  1447. current: race.name
  1448. });
  1449. character.model.attribs.create({
  1450. name: `repeating_traits_${fRowId}_description`,
  1451. current: d20plus.importer.getCleanText(renderStack.join(""))
  1452. });
  1453. character.model.attribs.create({
  1454. name: `repeating_traits_${fRowId}_options-flag`,
  1455. current: "0"
  1456. });
  1457. });
  1458. } else if (data.data.Category === "Classes") {
  1459. let level = prompt("What level?", "1");
  1460. if (level && level.trim()) {
  1461. level = Number(level);
  1462. if (level) {
  1463. if (level < 0 || level > 20) {
  1464. alert("Please enter a number between one and 20!");
  1465. return;
  1466. }
  1467.  
  1468. const clss = data.Vetoolscontent;
  1469.  
  1470. // --- these don't work
  1471. // d20plus.importer.addOrUpdateAttr(character.model, "class", data.name);
  1472. // d20plus.importer.addOrUpdateAttr(character.model, "level", level);
  1473. // d20plus.importer.addOrUpdateAttr(character.model, "base_level", String(level));
  1474.  
  1475. // operation "kitchen sink"
  1476. setTimeout(() => {
  1477. d20plus.importer.addOrUpdateAttr(character.model, "pb", d20plus.getProfBonusFromLevel(Number(level)));
  1478. // try to set level -- none of these actually work lol
  1479. d20plus.importer.addOrUpdateAttr(character.model, "level", level);
  1480. d20plus.importer.addOrUpdateAttr(character.model, "base_level", String(level));
  1481. character.$charsheet.find(`.sheet-pc .sheet-core input[name=attr_base_level]`)
  1482. .val(String(level))
  1483. .text(String(level))
  1484. .trigger("change");
  1485. // hack to set class
  1486. character.$charsheet.find(`.sheet-pc .sheet-core select[name=attr_class]`).val(data.name).trigger("change");
  1487. character.model.persisted = false;
  1488. extraDirty.add("level", "base_level", "pb");
  1489. }, 500);
  1490.  
  1491. const renderer = new EntryRenderer();
  1492. renderer.setBaseUrl(BASE_SITE_URL);
  1493. for (let i = 0; i < level; i++) {
  1494. const lvlFeatureList = clss.classFeatures[i];
  1495. for (let j = 0; j < lvlFeatureList.length; j++) {
  1496. const feature = lvlFeatureList[j];
  1497. // don't add "you gain a subclass feature" or ASI's
  1498. if (!feature.gainSubclassFeature && feature.name !== "Ability Score Improvement") {
  1499. const renderStack = [];
  1500. renderer.recursiveEntryRender({entries: feature.entries}, renderStack);
  1501.  
  1502. const fRowId = d20plus.generateRowId();
  1503. character.model.attribs.create({
  1504. name: `repeating_traits_${fRowId}_name`,
  1505. current: feature.name
  1506. });
  1507. character.model.attribs.create({
  1508. name: `repeating_traits_${fRowId}_source`,
  1509. current: "Class"
  1510. });
  1511. character.model.attribs.create({
  1512. name: `repeating_traits_${fRowId}_source_type`,
  1513. current: `${clss.name} ${i + 1}`
  1514. });
  1515. character.model.attribs.create({
  1516. name: `repeating_traits_${fRowId}_description`,
  1517. current: d20plus.importer.getCleanText(renderStack.join(""))
  1518. });
  1519. character.model.attribs.create({
  1520. name: `repeating_traits_${fRowId}_options-flag`,
  1521. current: "0"
  1522. });
  1523. }
  1524. }
  1525. }
  1526. }
  1527. }
  1528. } else if (data.data.Category === "Subclasses") {
  1529. const sc = data.Vetoolscontent;
  1530. let maxIndex = sc.subclassFeatures.length;
  1531. const gainLevels = [];
  1532. // _gainAtLevels should be a 20-length array of booleans
  1533. if (sc._gainAtLevels) {
  1534. maxIndex = 0;
  1535.  
  1536. let level = prompt("What level?", "1");
  1537. if (level && level.trim()) {
  1538. level = Number(level);
  1539. if (level) {
  1540. if (level < 0 || level > 20) {
  1541. alert("Please enter a number between one and 20!");
  1542. return;
  1543. }
  1544.  
  1545. for (let i = 0; i < level; i++) {
  1546. if (sc._gainAtLevels[i]) {
  1547. maxIndex++;
  1548. gainLevels.push(i + 1);
  1549. }
  1550. }
  1551. }
  1552. } else {
  1553. return;
  1554. }
  1555. }
  1556.  
  1557. if (maxIndex === 0) return;
  1558.  
  1559. const renderer = new EntryRenderer();
  1560. renderer.setBaseUrl(BASE_SITE_URL);
  1561. let firstFeatures = true;
  1562. for (let i = 0; i < maxIndex; i++) {
  1563. const lvlFeatureList = sc.subclassFeatures[i];
  1564. for (let j = 0; j < lvlFeatureList.length; j++) {
  1565. const featureCpy = JSON.parse(JSON.stringify(lvlFeatureList[j]));
  1566. let feature = lvlFeatureList[j];
  1567.  
  1568. try {
  1569. while (!feature.name || (feature[0] && !feature[0].name)) {
  1570. if (feature.entries && feature.entries.name) {
  1571. feature = feature.entries;
  1572. continue;
  1573. } else if (feature.entries[0] && feature.entries[0].name) {
  1574. feature = feature.entries[0];
  1575. continue;
  1576. } else {
  1577. feature = feature.entries;
  1578. }
  1579.  
  1580. if (!feature) {
  1581. // in case something goes wrong, reset break the loop
  1582. feature = featureCpy;
  1583. break;
  1584. }
  1585. }
  1586. } catch (e) {
  1587. console.error("Failed to find feature");
  1588. // in case something goes _really_ wrong, reset
  1589. feature = featureCpy;
  1590. }
  1591.  
  1592. // for the first batch of subclass features, try to split them up
  1593. if (firstFeatures && feature.name && feature.entries) {
  1594. const subFeatures = [];
  1595. const baseFeatures = feature.entries.filter(f => {
  1596. if (f.name && f.type === "entries") {
  1597. subFeatures.push(f);
  1598. return false;
  1599. } else return true;
  1600. });
  1601. addFeatureToSheet({name: feature.name, type: feature.type, entries: baseFeatures});
  1602. subFeatures.forEach(sf => {
  1603. addFeatureToSheet(sf);
  1604. })
  1605. } else {
  1606. addFeatureToSheet(feature);
  1607. }
  1608.  
  1609. function addFeatureToSheet (feature) {
  1610. const renderStack = [];
  1611. renderer.recursiveEntryRender({entries: feature.entries}, renderStack);
  1612.  
  1613. const fRowId = d20plus.generateRowId();
  1614. character.model.attribs.create({
  1615. name: `repeating_traits_${fRowId}_name`,
  1616. current: feature.name
  1617. });
  1618. character.model.attribs.create({
  1619. name: `repeating_traits_${fRowId}_source`,
  1620. current: "Class"
  1621. });
  1622. character.model.attribs.create({
  1623. name: `repeating_traits_${fRowId}_source_type`,
  1624. current: `${sc.class} (${sc.name} ${gainLevels[i]})`
  1625. });
  1626. character.model.attribs.create({
  1627. name: `repeating_traits_${fRowId}_description`,
  1628. current: d20plus.importer.getCleanText(renderStack.join(""))
  1629. });
  1630. character.model.attribs.create({
  1631. name: `repeating_traits_${fRowId}_options-flag`,
  1632. current: "0"
  1633. });
  1634. }
  1635.  
  1636. firstFeatures = false;
  1637. }
  1638. }
  1639. } else if (data.data.Category === "Psionics") {
  1640. function makeSpellTrait (level, rowId, propName, content) {
  1641. character.model.attribs.create({
  1642. "name": `repeating_spell-${level}_${rowId}_${propName}`,
  1643. "current": `${content}`
  1644. });
  1645. }
  1646.  
  1647. // disable all components
  1648. function noComponents (level, rowId, hasM) {
  1649. makeSpellTrait(level, rowId, "spellcomp_v", 0);
  1650. makeSpellTrait(level, rowId, "spellcomp_s", 0);
  1651. if (!hasM) {
  1652. makeSpellTrait(level, rowId, "spellcomp_m", 0);
  1653. }
  1654. makeSpellTrait(level, rowId, "options-flag", 0);
  1655. }
  1656.  
  1657. const renderer = new EntryRenderer();
  1658. renderer.setBaseUrl(BASE_SITE_URL);
  1659.  
  1660. if (data.type === "D") {
  1661. const rowId = d20plus.generateRowId();
  1662.  
  1663. // make focus
  1664. const focusLevel = "cantrip";
  1665. makeSpellTrait(focusLevel, rowId, "spelllevel", "cantrip");
  1666. makeSpellTrait(focusLevel, rowId, "spellname", `${data.name} Focus`);
  1667. makeSpellTrait(focusLevel, rowId, "spelldescription", data.focus);
  1668. makeSpellTrait(focusLevel, rowId, "spellcastingtime", "1 bonus action");
  1669. noComponents(focusLevel, rowId);
  1670.  
  1671. data.modes.forEach(m => {
  1672. if (m.submodes) {
  1673. m.submodes.forEach(sm => {
  1674. const rowId = d20plus.generateRowId();
  1675. const smLevel = sm.cost.min;
  1676. makeSpellTrait(smLevel, rowId, "spelllevel", smLevel);
  1677. makeSpellTrait(smLevel, rowId, "spellname", `${m.name} (${sm.name})`);
  1678. const renderStack = [];
  1679. renderer.recursiveEntryRender({entries: sm.entries}, renderStack, 3);
  1680. makeSpellTrait(smLevel, rowId, "spelldescription", d20plus.importer.getCleanText(renderStack.join("")));
  1681. const costStr = sm.cost.min === sm.cost.max ? sm.cost.min : `${sm.cost.min}-${sm.cost.max}`;
  1682. makeSpellTrait(smLevel, rowId, "spellcomp_materials", `${costStr} psi points`);
  1683. noComponents(smLevel, rowId, true);
  1684. });
  1685. } else {
  1686. const rowId = d20plus.generateRowId();
  1687. const mLevel = m.cost.min;
  1688. makeSpellTrait(mLevel, rowId, "spelllevel", mLevel);
  1689. makeSpellTrait(mLevel, rowId, "spellname", `${m.name}`);
  1690. const renderStack = [];
  1691. renderer.recursiveEntryRender({entries: m.entries}, renderStack, 3);
  1692. makeSpellTrait(mLevel, rowId, "spelldescription", `Psionic Discipline mode\n\n${d20plus.importer.getCleanText(renderStack.join(""))}`);
  1693. const costStr = m.cost.min === m.cost.max ? m.cost.min : `${m.cost.min}-${m.cost.max}`;
  1694. makeSpellTrait(mLevel, rowId, "spellcomp_materials", `${costStr} psi points`);
  1695. if (m.concentration) {
  1696. makeSpellTrait(mLevel, rowId, "spellduration", `${m.concentration.duration} ${m.concentration.unit}`);
  1697. makeSpellTrait(mLevel, rowId, "spellconcentration", "Yes");
  1698. }
  1699. noComponents(mLevel, rowId, true);
  1700. }
  1701. });
  1702. } else {
  1703. const rowId = d20plus.generateRowId();
  1704. const level = "cantrip";
  1705. makeSpellTrait(level, rowId, "spelllevel", "cantrip");
  1706. makeSpellTrait(level, rowId, "spellname", data.name);
  1707. makeSpellTrait(level, rowId, "spelldescription", `Psionic Talent\n\n${d20plus.importer.getCleanText(EntryRenderer.psionic.getTalentText(data, renderer))}`);
  1708. noComponents(level, rowId, false);
  1709. }
  1710. } else {
  1711. function doDefaultDrop (n, outerI) {
  1712. const e = character;
  1713. var i = $(outerI.helper[0]).attr("data-pagename");
  1714.  
  1715. // BEGIN ROLL20 CODE
  1716. var r = n.data;
  1717. r.Name = n.name,
  1718. r.uniqueName = i,
  1719. r.Content = n.content;
  1720. var o = $(t.target);
  1721. o.find("*[accept]").each(function() {
  1722. var t = $(this)
  1723. , n = t.attr("accept");
  1724. 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() {
  1725. var e = $(this);
  1726. (e.attr("value") === r[n] || e.text() === r[n]) && e.attr("selected", "selected")
  1727. }) : $(this).val(r[n]),
  1728. e.saveSheetValues(this))
  1729. })
  1730. // END ROLL20 CODE
  1731. }
  1732. doDefaultDrop(data, i);
  1733. doSetDirty = false;
  1734. }
  1735.  
  1736. if (doSetDirty) {
  1737. character.model.view._updateSheetValues();
  1738. const dirty = [];
  1739. extraDirty.forEach(ed => {
  1740. dirty.push(ed);
  1741. });
  1742. $.each(d20.journal.customSheets.attrDeps, function (i, v) {
  1743. dirty.push(i);
  1744. });
  1745. d20.journal.notifyWorkersOfAttrChanges(character.model.view.model.id, dirty, true);
  1746. }
  1747. }
  1748. } else {
  1749. // rename some variables...
  1750. const e = character;
  1751. const n = i;
  1752.  
  1753. // original roll20 code
  1754. console.log("Compendium item dropped onto target!");
  1755. t.originalEvent.dropHandled = !0;
  1756. window.wantsToReceiveDrop(this, t, function () {
  1757. var i = $(n.helper[0]).attr("data-pagename");
  1758. console.log(d20.compendium.compendiumBase + "compendium/" + COMPENDIUM_BOOK_NAME + "/" + i + ".json?plaintext=true"),
  1759. $.get(d20.compendium.compendiumBase + "compendium/" + COMPENDIUM_BOOK_NAME + "/" + i + ".json?plaintext=true", function (n) {
  1760. var r = n.data;
  1761. r.Name = n.name,
  1762. r.uniqueName = i,
  1763. r.Content = n.content;
  1764. var o = $(t.target);
  1765. o.find("*[accept]").each(function () {
  1766. var t = $(this)
  1767. , n = t.attr("accept");
  1768. 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 () {
  1769. var e = $(this);
  1770. (e.attr("value") === r[n] || e.text() === r[n]) && e.attr("selected", "selected")
  1771. }) : $(this).val(r[n]),
  1772. e.saveSheetValues(this))
  1773. })
  1774. })
  1775. });
  1776. }
  1777. }
  1778. });
  1779. });
  1780. };
  1781. });
  1782. };
  1783.  
  1784. d20plus.getProfBonusFromLevel = function (level) {
  1785. if (level < 5) return "2";
  1786. if (level < 9) return "3";
  1787. if (level < 13) return "4";
  1788. if (level < 17) return "5";
  1789. return "6";
  1790. };
  1791.  
  1792. d20plus.monsters._groupOptions = ["Type", "Type (with tags)", "CR", "Alphabetical", "Source"];
  1793. d20plus.monsters._listCols = ["name", "type", "cr", "source"];
  1794. d20plus.monsters._listItemBuilder = (it) => `
  1795. <span class="name col-4" title="name">${it.name}</span>
  1796. <span class="type col-4" title="type">TYP[${Parser.monTypeToFullObj(it.type).asText.uppercaseFirst()}]</span>
  1797. <span class="cr col-2" title="cr">${it.cr === undefined ? "CR[Unknown]" : `CR[${(it.cr.cr || it.cr)}]`}</span>
  1798. <span title="source (Full: ${Parser.sourceJsonToFull(it.source)})" class="source">SRC[${Parser.sourceJsonToAbv(it.source)}]</span>`;
  1799. d20plus.monsters._listIndexConverter = (m) => {
  1800. m.__pType = m.__pType || Parser.monTypeToFullObj(m.type).type; // only filter using primary type
  1801. return {
  1802. name: m.name.toLowerCase(),
  1803. type: m.__pType.toLowerCase(),
  1804. cr: m.cr === undefined ? "unknown" : (m.cr.cr || m.cr).toLowerCase(),
  1805. source: Parser.sourceJsonToAbv(m.source).toLowerCase()
  1806. };
  1807. };
  1808. // Import Monsters button was clicked
  1809. d20plus.monsters.button = function () {
  1810. function loadData (url) {
  1811. DataUtil.loadJSON(url, (data) => {
  1812. d20plus.importer.addMeta(data._meta);
  1813. d20plus.importer.showImportList(
  1814. "monster",
  1815. data.monster,
  1816. d20plus.monsters.handoutBuilder,
  1817. {
  1818. groupOptions: d20plus.monsters._groupOptions,
  1819. listItemBuilder: d20plus.monsters._listItemBuilder,
  1820. listIndex: d20plus.monsters._listCols,
  1821. listIndexConverter: d20plus.monsters._listIndexConverter
  1822. }
  1823. );
  1824. });
  1825. }
  1826.  
  1827. const url = $("#import-monster-url").val();
  1828. if (url && url.trim()) {
  1829. // ugly hack to pre-load fluff
  1830. const fileName = url.split("/").reverse()[0];
  1831. const src = Object.keys(monsterDataUrls).find(k => monsterDataUrls[k] === fileName);
  1832. if (src && monsterFluffDataUrls[src]) {
  1833. const fluffUrl = d20plus.monsters.formMonsterUrl(monsterFluffDataUrls[src]);
  1834. DataUtil.loadJSON(fluffUrl, (data) => {
  1835. monsterFluffData[src] = data;
  1836. loadData(url);
  1837. });
  1838. } else {
  1839. loadData(url);
  1840. }
  1841. }
  1842. };
  1843.  
  1844. // Import All Monsters button was clicked
  1845. d20plus.monsters.buttonAll = function () {
  1846. function loadData () {
  1847. const toLoad = Object.keys(monsterDataUrls).filter(src => !isNonstandardSource(src)).map(src => d20plus.monsters.formMonsterUrl(monsterDataUrls[src]));
  1848. if (toLoad.length) {
  1849. DataUtil.multiLoadJSON(
  1850. toLoad.map(url => ({url})),
  1851. () => {},
  1852. (dataStack) => {
  1853. let toAdd = [];
  1854. dataStack.forEach(d => toAdd = toAdd.concat(d.monster));
  1855. d20plus.importer.showImportList(
  1856. "monster",
  1857. toAdd,
  1858. d20plus.monsters.handoutBuilder,
  1859. {
  1860. groupOptions: d20plus.monsters._groupOptions,
  1861. listItemBuilder: d20plus.monsters._listItemBuilder,
  1862. listIndex: d20plus.monsters._listCols,
  1863. listIndexConverter: d20plus.monsters._listIndexConverter
  1864. }
  1865. );
  1866. }
  1867. );
  1868. }
  1869. }
  1870.  
  1871. // preload fluff if available
  1872. const toLoadFluff = Object.keys(monsterFluffDataUrls)
  1873. .filter(src => !isNonstandardSource(src))
  1874. .map(src => ({url: d20plus.monsters.formMonsterUrl(monsterFluffDataUrls[src]), src}));
  1875. if (toLoadFluff.length) {
  1876. DataUtil.multiLoadJSON(
  1877. toLoadFluff,
  1878. (tl, data) => {
  1879. monsterFluffData[tl.src] = data;
  1880. },
  1881. () => {
  1882. loadData();
  1883. }
  1884. );
  1885. } else {
  1886. loadData();
  1887. }
  1888. };
  1889.  
  1890. d20plus.monsters.formMonsterUrl = function (fileName) {
  1891. return d20plus.formSrcUrl(MONSTER_DATA_DIR, fileName);
  1892. };
  1893.  
  1894. d20plus.importer.getSetAvatarImage = function (character, avatar) {
  1895. character.attributes.avatar = avatar;
  1896. var tokensize = 1;
  1897. if (character.size === "L") tokensize = 2;
  1898. if (character.size === "H") tokensize = 3;
  1899. if (character.size === "G") tokensize = 4;
  1900. var lightradius = 5;
  1901. if (character.senses && character.senses.toLowerCase().match(/(darkvision|blindsight|tremorsense|truesight)/)) lightradius = Math.max.apply(Math, character.senses.match(/\d+/g));
  1902. var lightmin = 0;
  1903. if (character.senses && character.senses.toLowerCase().match(/(blindsight|tremorsense|truesight)/)) lightmin = lightradius;
  1904. const nameSuffix = d20plus.getCfgVal("token", "namesuffix");
  1905. var defaulttoken = {
  1906. represents: character.id,
  1907. name: `${character.name}${nameSuffix ? ` ${nameSuffix}` : ""}`,
  1908. imgsrc: avatar,
  1909. width: 70 * tokensize,
  1910. height: 70 * tokensize
  1911. };
  1912. if (!d20plus.getCfgVal("import", "skipSenses")) {
  1913. defaulttoken.light_hassight = true;
  1914. defaulttoken.light_radius = lightradius;
  1915. defaulttoken.light_dimradius = lightmin;
  1916. }
  1917.  
  1918. character.updateBlobs({avatar: avatar, defaulttoken: JSON.stringify(defaulttoken)});
  1919. character.save({defaulttoken: (new Date()).getTime()});
  1920. };
  1921.  
  1922. d20plus.importer.addAction = function (character, name, text, index) {
  1923. if (d20plus.getCfgVal("token", "tokenactions")) {
  1924. character.abilities.create({
  1925. name: index + ": " + name,
  1926. istokenaction: true,
  1927. action: d20plus.actionMacroAction(index)
  1928. });
  1929. }
  1930.  
  1931. var newRowId = d20plus.generateRowId();
  1932. var actiontext = text;
  1933. var action_desc = actiontext; // required for later reduction of information dump.
  1934. var rollbase = d20plus.importer.rollbase;
  1935. // attack parsing
  1936. if (actiontext.indexOf(" Attack:") > -1) {
  1937. var attacktype = "";
  1938. var attacktype2 = "";
  1939. if (actiontext.indexOf(" Weapon Attack:") > -1) {
  1940. attacktype = actiontext.split(" Weapon Attack:")[0];
  1941. attacktype2 = " Weapon Attack:";
  1942. } else if (actiontext.indexOf(" Spell Attack:") > -1) {
  1943. attacktype = actiontext.split(" Spell Attack:")[0];
  1944. attacktype2 = " Spell Attack:";
  1945. }
  1946. var attackrange = "";
  1947. var rangetype = "";
  1948. if (attacktype.indexOf("Melee") > -1) {
  1949. attackrange = (actiontext.match(/reach (.*?),/) || ["", ""])[1];
  1950. rangetype = "Reach";
  1951. } else {
  1952. attackrange = (actiontext.match(/range (.*?),/) || ["", ""])[1];
  1953. rangetype = "Range";
  1954. }
  1955. var tohit = (actiontext.match(/\+(.*?) to hit/) || ["", ""])[1];
  1956. var damage = "";
  1957. var damagetype = "";
  1958. var damage2 = "";
  1959. var damagetype2 = "";
  1960. var onhit = "";
  1961. damageregex = /\d+ \((\d+d\d+\s?(?:\+|-)?\s?\d*)\) (\S+ )?damage/g;
  1962. damagesearches = damageregex.exec(actiontext);
  1963. if (damagesearches) {
  1964. onhit = damagesearches[0];
  1965. damage = damagesearches[1];
  1966. damagetype = (damagesearches[2] != null) ? damagesearches[2].trim() : "";
  1967. damagesearches = damageregex.exec(actiontext);
  1968. if (damagesearches) {
  1969. onhit += " plus " + damagesearches[0];
  1970. damage2 = damagesearches[1];
  1971. damagetype2 = (damagesearches[2] != null) ? damagesearches[2].trim() : "";
  1972. }
  1973. }
  1974. onhit = onhit.trim();
  1975. var attacktarget = (actiontext.match(/\.,(?!.*\.,)(.*)\. Hit:/) || ["", ""])[1];
  1976. // Cut the information dump in the description
  1977. var atk_desc_simple_regex = /Hit: \d+ \((\d+d\d+\s?(?:\+|-)?\s?\d*)\) (\S+ )?damage\.(.*)/g;
  1978. var atk_desc_complex_regex = /(Hit:.*)/g;
  1979. // is it a simple attack (just 1 damage type)?
  1980. var match_simple_atk = atk_desc_simple_regex.exec(actiontext);
  1981. if (match_simple_atk != null) {
  1982. //if yes, then only display special effects, if any
  1983. action_desc = match_simple_atk[3].trim();
  1984. } else {
  1985. //if not, simply cut everything before "Hit:" so there are no details lost.
  1986. var match_compl_atk = atk_desc_complex_regex.exec(actiontext);
  1987. if (match_compl_atk != null) action_desc = match_compl_atk[1].trim();
  1988. }
  1989. var tohitrange = "+" + tohit + ", " + rangetype + " " + attackrange + ", " + attacktarget + ".";
  1990. var damageflags = `{{damage=1}} {{dmg1flag=1}}${damage2 ? ` {{dmg2flag=1}}` : ""}`
  1991. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_name", current: name});
  1992. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_flag", current: "on"});
  1993. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_npc_options-flag", current: "0"});
  1994. character.attribs.create({
  1995. name: "repeating_npcaction_" + newRowId + "_attack_display_flag",
  1996. current: "{{attack=1}}"
  1997. });
  1998. character.attribs.create({
  1999. name: "repeating_npcaction_" + newRowId + "_attack_options",
  2000. current: "{{attack=1}}"
  2001. });
  2002. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_tohit", current: tohit});
  2003. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_damage", current: damage});
  2004. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_crit", current: damage});
  2005. character.attribs.create({
  2006. name: "repeating_npcaction_" + newRowId + "_attack_damagetype",
  2007. current: damagetype
  2008. });
  2009. if (damage2) {
  2010. character.attribs.create({
  2011. name: "repeating_npcaction_" + newRowId + "_attack_damage2",
  2012. current: damage2
  2013. });
  2014. character.attribs.create({
  2015. name: "repeating_npcaction_" + newRowId + "_attack_crit2",
  2016. current: damage2
  2017. });
  2018. character.attribs.create({
  2019. name: "repeating_npcaction_" + newRowId + "_attack_damagetype2",
  2020. current: damagetype2
  2021. });
  2022. }
  2023. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_name_display", current: name});
  2024. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_rollbase", current: rollbase});
  2025. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_type", current: attacktype});
  2026. character.attribs.create({
  2027. name: "repeating_npcaction_" + newRowId + "_attack_type_display",
  2028. current: attacktype + attacktype2
  2029. });
  2030. character.attribs.create({
  2031. name: "repeating_npcaction_" + newRowId + "_attack_tohitrange",
  2032. current: tohitrange
  2033. });
  2034. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_range", current: attackrange});
  2035. character.attribs.create({
  2036. name: "repeating_npcaction_" + newRowId + "_attack_target",
  2037. current: attacktarget
  2038. });
  2039. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_damage_flag", current: damageflags});
  2040. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_onhit", current: onhit});
  2041. } else {
  2042. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_name", current: name});
  2043. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_npc_options-flag", current: 0});
  2044. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_rollbase", current: rollbase});
  2045. character.attribs.create({name: "repeating_npcaction_" + newRowId + "_name_display", current: name});
  2046. }
  2047. var descriptionFlag = Math.max(Math.ceil(text.length / 57), 1);
  2048. character.attribs.create({
  2049. name: "repeating_npcaction_" + newRowId + "_description",
  2050. current: action_desc
  2051. });
  2052. character.attribs.create({
  2053. name: "repeating_npcaction_" + newRowId + "_description_flag",
  2054. current: descriptionFlag
  2055. });
  2056. };
  2057.  
  2058. // Create monster character from js data object
  2059. d20plus.monsters.handoutBuilder = function (data, overwrite, inJournals, folderName, saveIdsTo) {
  2060. // make dir
  2061. const folder = d20plus.importer.makeDirTree(`Monsters`, folderName);
  2062. const path = ["Monsters", folderName, data.name];
  2063.  
  2064. // handle duplicates/overwrites
  2065. if (!d20plus.importer._checkHandleDuplicate(path, overwrite)) return;
  2066.  
  2067. const name = data.name;
  2068. const pType = Parser.monTypeToFullObj(data.type);
  2069.  
  2070. const renderer = new EntryRenderer();
  2071. renderer.setBaseUrl(BASE_SITE_URL);
  2072.  
  2073. // get fluff, if available
  2074. const includedFluff = data.fluff;
  2075. let renderFluff = null;
  2076. // prefer fluff directly attached to the creature
  2077. if (includedFluff) {
  2078. if (includedFluff.entries) {
  2079. const depth = includedFluff.entries.type === "section" ? -1 : 2;
  2080. renderFluff = renderer.renderEntry(includedFluff.entries, depth);
  2081. }
  2082. } else {
  2083. const fluffData = monsterFluffData[data.source] ? monsterFluffData[data.source] : null;
  2084. const fluff = fluffData ? monsterFluffData[data.source].monster.find(it => it.name === data.name) : null;
  2085. if (fluff) {
  2086. if (fluff._copy) {
  2087. const cpy = fluffData.monster.find(it => fluff._copy.name === it.name);
  2088. // preserve these
  2089. const name = fluff.name;
  2090. const src = fluff.source;
  2091. const images = fluff.images;
  2092. Object.assign(fluff, cpy);
  2093. fluff.name = name;
  2094. fluff.source = src;
  2095. if (images) fluff.images = images;
  2096. delete fluff._copy;
  2097. }
  2098.  
  2099. if (fluff._appendCopy) {
  2100. const cpy = fluffData.monster.find(it => fluff._appendCopy.name === it.name);
  2101. if (cpy.images) {
  2102. if (!fluff.images) fluff.images = cpy.images;
  2103. else fluff.images = fluff.images.concat(cpy.images);
  2104. }
  2105. if (cpy.entries) {
  2106. if (!fluff.entries) fluff.entries = cpy.entries;
  2107. else fluff.entries.entries = fluff.entries.entries.concat(cpy.entries.entries);
  2108. }
  2109. delete fluff._appendCopy;
  2110. }
  2111.  
  2112. if (fluff.entries) {
  2113. const depth = fluff.entries.type === "section" ? -1 : 2;
  2114. renderFluff = renderer.renderEntry(fluff.entries, depth);
  2115. }
  2116. }
  2117. }
  2118.  
  2119. d20.Campaign.characters.create(
  2120. {
  2121. name: name,
  2122. tags: d20plus.importer.getTagString([
  2123. pType.type,
  2124. ...pType.tags,
  2125. `cr ${(data.cr ? (data.cr.cr || data.cr) : "").replace(/\//g, " over ")}` || "unknown cr",
  2126. Parser.sourceJsonToFull(data.source)
  2127. ], "monsters")
  2128. },
  2129. {
  2130. success: function (character) {
  2131. if (saveIdsTo) saveIdsTo[UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_BESTIARY](data)] = {name: data.name, source: data.source, type: "character", roll20Id: character.id};
  2132. /* OGL Sheet */
  2133. try {
  2134. const type = Parser.monTypeToFullObj(data.type).asText;
  2135. const source = Parser.sourceJsonToAbv(data.source);
  2136. const avatar = data.tokenURL || `${IMG_URL}${source}/${name.replace(/"/g, "")}.png`;
  2137. character.size = data.size;
  2138. character.name = name;
  2139. character.senses = data.senses;
  2140. character.hp = data.hp.average || 0;
  2141. $.ajax({
  2142. url: avatar,
  2143. type: 'HEAD',
  2144. error: function () {
  2145. d20plus.importer.getSetAvatarImage(character, `${IMG_URL}blank.png`);
  2146. },
  2147. success: function () {
  2148. d20plus.importer.getSetAvatarImage(character, avatar);
  2149. }
  2150. });
  2151. var ac = data.ac.match(/^\d+/);
  2152. var actype = /\(([^)]+)\)/.exec(data.ac);
  2153. var hp = data.hp.average || 0;
  2154. var hpformula = data.hp.formula;
  2155. var passive = data.passive != null ? data.passive : "";
  2156. var passiveStr = passive !== "" ? "passive Perception " + passive : "";
  2157. var senses = data.senses || "";
  2158. var sensesStr = senses !== "" ? senses + ", " + passiveStr : passiveStr;
  2159. var size = d20plus.getSizeString(data.size || "");
  2160. var alignment = data.alignment ? Parser.alignmentListToFull(data.alignment).toLowerCase() : "(Unknown Alignment)";
  2161. var cr = data.cr ? (data.cr.cr || data.cr) : "";
  2162. var xp = Parser.crToXp(cr);
  2163. character.attribs.create({name: "npc", current: 1});
  2164. character.attribs.create({name: "npc_toggle", current: 1});
  2165. character.attribs.create({name: "npc_options-flag", current: 0});
  2166. character.attribs.create({name: "wtype", current: d20plus.importer.getDesiredWhisperType()});
  2167. character.attribs.create({name: "rtype", current: d20plus.importer.getDesiredRollType()});
  2168. character.attribs.create({
  2169. name: "advantagetoggle",
  2170. current: d20plus.importer.getDesiredAdvantageToggle()
  2171. });
  2172. character.attribs.create({
  2173. name: "whispertoggle",
  2174. current: d20plus.importer.getDesiredWhisperToggle()
  2175. });
  2176. character.attribs.create({name: "dtype", current: d20plus.importer.getDesiredDamageType()});
  2177. character.attribs.create({name: "npc_name", current: name});
  2178. character.attribs.create({name: "npc_size", current: size});
  2179. character.attribs.create({name: "type", current: type});
  2180. character.attribs.create({name: "npc_type", current: size + " " + type + ", " + alignment});
  2181. character.attribs.create({name: "npc_alignment", current: alignment});
  2182. character.attribs.create({name: "npc_ac", current: ac != null ? ac[0] : ""});
  2183. character.attribs.create({name: "npc_actype", current: actype != null ? actype[1] || "" : ""});
  2184. character.attribs.create({name: "npc_hpbase", current: hp != null ? hp : ""});
  2185. character.attribs.create({
  2186. name: "npc_hpformula",
  2187. current: hpformula != null ? hpformula || "" : ""
  2188. });
  2189. const parsedSpeed = Parser.getSpeedString(data);
  2190. data.npc_speed = parsedSpeed;
  2191. if (d20plus.sheet === "shaped") {
  2192. data.npc_speed = data.npc_speed.toLowerCase();
  2193. var match = data.npc_speed.match(/^\s*(\d+)\s?(ft\.?|m\.?)/);
  2194. if (match && match[1]) {
  2195. data.speed = match[1] + ' ' + match[2];
  2196. character.attribs.create({name: "speed", current: match[1] + ' ' + match[2]});
  2197. }
  2198. data.npc_speed = parsedSpeed;
  2199. var regex = /(burrow|climb|fly|swim)\s+(\d+)\s?(ft\.?|m\.?)/g;
  2200. var speeds = void 0;
  2201. while ((speeds = regex.exec(data.npc_speed)) !== null) character.attribs.create({
  2202. name: "speed_" + speeds[1],
  2203. current: speeds[2] + ' ' + speeds[3]
  2204. });
  2205. if (data.npc_speed && data.npc_speed.includes('hover')) character.attribs.create({
  2206. name: "speed_fly_hover",
  2207. current: 1
  2208. });
  2209. data.npc_speed = '';
  2210. }
  2211.  
  2212. function calcMod (score) {
  2213. return Math.floor((Number(score) - 10) / 2);
  2214. }
  2215.  
  2216. character.attribs.create({name: "npc_speed", current: parsedSpeed != null ? parsedSpeed : ""});
  2217. character.attribs.create({name: "strength", current: data.str});
  2218. character.attribs.create({name: "strength_base", current: data.str});
  2219. character.attribs.create({name: "strength_mod", current: calcMod(data.str)});
  2220.  
  2221. character.attribs.create({name: "dexterity", current: data.dex});
  2222. character.attribs.create({name: "dexterity_base", current: data.dex});
  2223. character.attribs.create({name: "dexterity_mod", current: calcMod(data.dex)});
  2224.  
  2225. character.attribs.create({name: "constitution", current: data.con});
  2226. character.attribs.create({name: "constitution_base", current: data.con});
  2227. character.attribs.create({name: "constitution_mod", current: calcMod(data.con)});
  2228.  
  2229. character.attribs.create({name: "intelligence", current: data.int});
  2230. character.attribs.create({name: "intelligence_base", current: data.int});
  2231. character.attribs.create({name: "intelligence_mod", current: calcMod(data.int)});
  2232.  
  2233. character.attribs.create({name: "wisdom", current: data.wis});
  2234. character.attribs.create({name: "wisdom_base", current: data.wis});
  2235. character.attribs.create({name: "wisdom_mod", current: calcMod(data.wis)});
  2236.  
  2237. character.attribs.create({name: "charisma", current: data.cha});
  2238. character.attribs.create({name: "charisma_base", current: data.cha});
  2239. character.attribs.create({name: "charisma_mod", current: calcMod(data.cha)});
  2240.  
  2241. character.attribs.create({name: "passive", current: passive});
  2242. character.attribs.create({
  2243. name: "npc_languages",
  2244. current: data.languages != null ? data.languages : ""
  2245. });
  2246. character.attribs.create({name: "npc_challenge", current: cr.cr || cr});
  2247. character.attribs.create({name: "npc_xp", current: xp});
  2248. character.attribs.create({
  2249. name: "npc_vulnerabilities",
  2250. current: data.vulnerable != null ? Parser.monImmResToFull(data.vulnerable) : ""
  2251. });
  2252. character.attribs.create({
  2253. name: "damage_vulnerabilities",
  2254. current: data.vulnerable != null ? Parser.monImmResToFull(data.vulnerable) : ""
  2255. });
  2256. character.attribs.create({
  2257. name: "npc_resistances",
  2258. current: data.resist != null ? Parser.monImmResToFull(data.resist) : ""
  2259. });
  2260. character.attribs.create({
  2261. name: "damage_resistances",
  2262. current: data.resist != null ? Parser.monImmResToFull(data.resist) : ""
  2263. });
  2264. character.attribs.create({name: "npc_immunities", current: data.immune != null ? Parser.monImmResToFull(data.immune) : ""});
  2265. character.attribs.create({
  2266. name: "damage_immunities",
  2267. current: data.immune != null ? Parser.monImmResToFull(data.immune) : ""
  2268. });
  2269. character.attribs.create({
  2270. name: "npc_condition_immunities",
  2271. current: data.conditionImmune != null ? Parser.monCondImmToFull(data.conditionImmune) : ""
  2272. });
  2273. character.attribs.create({
  2274. name: "damage_condition_immunities",
  2275. current: data.conditionImmune != null ? Parser.monCondImmToFull(data.conditionImmune) : ""
  2276. });
  2277. character.attribs.create({name: "npc_senses", current: sensesStr});
  2278.  
  2279. // add Tokenaction Macros
  2280. if (d20plus.getCfgVal("token", "tokenactionsSkillsSaves")) {
  2281. character.abilities.create({
  2282. name: "Perception",
  2283. istokenaction: true,
  2284. action: d20plus.actionMacroPerception
  2285. });
  2286. character.abilities.create({
  2287. name: "Init",
  2288. istokenaction: true,
  2289. action: d20plus.actionMacroInit
  2290. });
  2291. character.abilities.create({
  2292. name: "DR/Immunities",
  2293. istokenaction: true,
  2294. action: d20plus.actionMacroDrImmunities
  2295. });
  2296. character.abilities.create({
  2297. name: "Stats",
  2298. istokenaction: true,
  2299. action: d20plus.actionMacroStats
  2300. });
  2301. character.abilities.create({
  2302. name: "Saves",
  2303. istokenaction: true,
  2304. action: d20plus.actionMacroSaves
  2305. });
  2306. character.abilities.create({
  2307. name: "Skill-Check",
  2308. istokenaction: true,
  2309. action: d20plus.actionMacroSkillCheck
  2310. });
  2311. character.abilities.create({
  2312. name: "Ability-Check",
  2313. istokenaction: true,
  2314. action: d20plus.actionMacroAbilityCheck
  2315. });
  2316. }
  2317.  
  2318. if (data.save != null) {
  2319. character.attribs.create({name: "npc_saving_flag", current: 1});
  2320. Object.keys(data.save).forEach(k => {
  2321. character.attribs.create({
  2322. name: "npc_" + k + "_save_base",
  2323. current: data.save[k]
  2324. });
  2325. character.attribs.create({
  2326. name: k + "_saving_throw_proficient",
  2327. current: data.save[k]
  2328. });
  2329. });
  2330. }
  2331. if (data.skill != null) {
  2332. const skills = data.skill;
  2333. const skillsString = Object.keys(skills).map(function (k) {
  2334. return k.uppercaseFirst() + ' ' + skills[k];
  2335. }).join(', ');
  2336. character.attribs.create({name: "npc_skills_flag", current: 1});
  2337. character.attribs.create({name: "npc_skills", current: skillsString});
  2338.  
  2339. // Shaped Sheet currently doesn't correctly load NPC Skills
  2340. // This adds a visual representation as a Trait for reference
  2341. if (d20plus.sheet === "shaped") {
  2342. var newRowId = d20plus.generateRowId();
  2343. character.attribs.create({
  2344. name: "repeating_npctrait_" + newRowId + "_name",
  2345. current: "NPC Skills"
  2346. });
  2347. character.attribs.create({
  2348. name: "repeating_npctrait_" + newRowId + "_desc",
  2349. current: skillsString
  2350. });
  2351. }
  2352.  
  2353. $.each(skills, function (k, v) {
  2354. const cleanSKill = $.trim(k).toLowerCase().replace(/ /g, "_");
  2355. const cleanBonus = parseInt($.trim(v)) || 0;
  2356. character.attribs.create({
  2357. name: "npc_" + cleanSKill + "_base",
  2358. current: parseInt($.trim(v)) || 0
  2359. });
  2360. character.attribs.create({
  2361. name: "npc_" + cleanSKill + "_base",
  2362. current: cleanBonus
  2363. });
  2364. character.attribs.create({
  2365. name: "npc_" + cleanSKill,
  2366. current: cleanBonus
  2367. });
  2368. });
  2369. }
  2370. if (data.spellcasting) {
  2371. // make it a spellcaster
  2372. character.attribs.create({name: `npcspellcastingflag`, current: "1"});
  2373.  
  2374. // figure out the casting ability or spell DC
  2375. let spellDc;
  2376. let spellAbility;
  2377. let casterLevel;
  2378. for (const sc of data.spellcasting) {
  2379. const abils = [];
  2380. const toCheck = sc.headerEntries.join("");
  2381.  
  2382. // use the first ability/DC we find, since roll20 doesn't support multiple
  2383. const abM = /(strength|constitution|dexterity|intelligence|wisdom|charisma)/i.exec(toCheck);
  2384. const dcM = /DC (\d+)/i.exec(toCheck);
  2385. const lvlM = /(\d+)(st|nd|rd|th).level\s+spellcaster/i.exec(toCheck);
  2386. if (dcM) {
  2387. spellDc = dcM[1];
  2388. }
  2389. if (lvlM) {
  2390. casterLevel = lvlM[1];
  2391. }
  2392. if (abM) {
  2393. spellAbility = abM[1];
  2394. }
  2395. }
  2396. // delay these, does nothing otherwise (getting overwritten by turning on npc_spellcasting after, perhaps?)
  2397. // if (spellDc) character.attribs.create({name: `spell_save_dc`, current: spellDc});
  2398. // if (spellAbility) character.attribs.create({name: "spellcasting_ability", current: `@{${spellAbility.toLowerCase()}_mod}+`})
  2399. // if (casterLevel) character.attribs.create({name: "caster_level", current: casterLevel})
  2400. const charInterval = d20plus.getCfgVal("import", "importIntervalCharacter") || d20plus.getCfgDefaultVal("import", "importIntervalCharacter");
  2401. const spAbilsDelayMs = Math.max(350, Math.floor(charInterval / 5));
  2402. console.log(`Spellcasting import interval: ${spAbilsDelayMs} ms`);
  2403. setTimeout(() => {
  2404. if (spellDc) {
  2405. d20plus.importer.addOrUpdateAttr(character, "spell_save_dc", spellDc);
  2406. }
  2407. if (spellAbility) {
  2408. d20plus.importer.addOrUpdateAttr(character, "spellcasting_ability", `@{${spellAbility.toLowerCase()}_mod}+`);
  2409. }
  2410. if (casterLevel) {
  2411. d20plus.importer.addOrUpdateAttr(character, "caster_level", casterLevel);
  2412. d20plus.importer.addOrUpdateAttr(character, "level", Number(casterLevel));
  2413. }
  2414. }, spAbilsDelayMs);
  2415.  
  2416. // set spellcaster class, since this seems to reliably set spellcasting ability
  2417. if (spellAbility == "Intelligence") d20plus.importer.addOrUpdateAttr(character, "class", `Wizard`);
  2418. if (spellAbility == "Wisdom") d20plus.importer.addOrUpdateAttr(character, "class", `Cleric`);
  2419. if (spellAbility == "Charisma") d20plus.importer.addOrUpdateAttr(character, "class", `Bard`);
  2420.  
  2421. // add the spellcasting text
  2422. const newRowId = d20plus.generateRowId();
  2423. const spellTrait = EntryRenderer.monster.getSpellcastingRenderedTraits(data, renderer).map(it => it.rendered).filter(it => it).join("");
  2424. const cleanDescription = d20plus.importer.getCleanText(spellTrait);
  2425. character.attribs.create({
  2426. name: `repeating_npctrait_${newRowId}_name`,
  2427. current: "Spellcasting"
  2428. });
  2429. character.attribs.create({
  2430. name: `repeating_npctrait_${newRowId}_desc`,
  2431. current: cleanDescription
  2432. });
  2433.  
  2434. // begin building a spells macro
  2435. const tokenActionStack = [cleanDescription];
  2436.  
  2437. // add the spells
  2438. const allSpells = [];
  2439. data.spellcasting.forEach(sc => {
  2440. const toAdd = ["constant", "will", "rest", "daily", "weekly"];
  2441. toAdd.forEach(k => {
  2442. if (sc[k]) {
  2443. Object.values(sc[k]).forEach(spOrSpArr => {
  2444. if (spOrSpArr instanceof Array) {
  2445. Array.prototype.push.apply(allSpells, spOrSpArr);
  2446. } else {
  2447. allSpells.push(spOrSpArr);
  2448. }
  2449. });
  2450. }
  2451. });
  2452. if (sc.spells) {
  2453. Object.keys(sc.spells).forEach(lvl => {
  2454. // delayed creation of spell slots, once it's a spellcaster
  2455. setTimeout(() => {
  2456. if (sc.spells[lvl].slots) {
  2457. const slotName = `lvl${lvl}_slots_total`;
  2458. d20plus.importer.addOrUpdateAttr(character, slotName, String(sc.spells[lvl].slots));
  2459. }
  2460. }, spAbilsDelayMs);
  2461.  
  2462. if (sc.spells[lvl].spells) {
  2463. Array.prototype.push.apply(allSpells, sc.spells[lvl].spells);
  2464. }
  2465. });
  2466. }
  2467. });
  2468.  
  2469. // render sheet
  2470. character.view.render();
  2471.  
  2472. // add spells to sheet
  2473. const toAdd = [];
  2474. allSpells.forEach(sp => {
  2475. const tagSplit = EntryRenderer.splitByTags(sp);
  2476. tagSplit.forEach(s => {
  2477. if (!s || !s.trim()) return;
  2478. if (s.charAt(0) === "@") {
  2479. const [tag, text] = EntryRenderer.splitFirstSpace(s);
  2480. if (tag === "@spell") {
  2481. toAdd.push(text);
  2482. }
  2483. }
  2484. });
  2485. });
  2486.  
  2487. const addMacroIndex = toAdd.length - 1;
  2488. toAdd.forEach((text, i) => {
  2489. let [name, source] = text.split("|");
  2490. if (!source) source = "PHB";
  2491. const rawUrl = spellDataUrls[Object.keys(spellDataUrls).find(src => source.toLowerCase() === src.toLowerCase())];
  2492. const url = d20plus.spells.formSpellUrl(rawUrl);
  2493. // the JSON gets cached by the script, so this is fine
  2494. DataUtil.loadJSON(url, (data) => {
  2495. const spell = data.spell.find(spell => spell.name.toLowerCase() === name.toLowerCase());
  2496.  
  2497. const [notecontents, gmnotes] = d20plus.spells._getHandoutData(spell);
  2498.  
  2499. addSpell(JSON.parse(gmnotes), spell, i, addMacroIndex);
  2500. });
  2501. });
  2502.  
  2503. function addSpell (sp, VeSp, index, addMacroIndex) {
  2504. const rId = d20plus.generateRowId();
  2505. const lvl = sp.data.Level === "0" ? "cantrip" : sp.data.Level;
  2506. const base = `repeating_spell-${lvl}_${rId}_`;
  2507.  
  2508. makeAttrib("spelllevel", lvl);
  2509. makeAttrib("spellname", sp.name);
  2510. makeAttrib("spellschool", sp.data.School);
  2511. makeAttrib("spellcastingtime", sp.data["Casting Time"]); // spaces in property names l m a o
  2512. makeAttrib("spellrange", sp.data.Range);
  2513. makeAttrib("spelltarget", sp.data.Target);
  2514. makeAttrib("spellcomp_v", Number(!!VeSp.components.v));
  2515. makeAttrib("spellcomp_s", Number(!!VeSp.components.s));
  2516. makeAttrib("spellcomp_materials", sp.data.Material);
  2517. if (!sp.data.Material && !VeSp.components.m) makeAttrib("spellcomp_m", "0");
  2518. makeAttrib("spellconcentration", sp.data.Concentration)
  2519. makeAttrib("spellduration", sp.data.Duration);
  2520. makeAttrib("spelldamage", sp.data.Damage);
  2521. makeAttrib("spelldamagetype", sp.data["Damage Type"]);
  2522. makeAttrib("spellsave", sp.data.Save);
  2523. makeAttrib("spellsavesuccess", sp.data["Save Success"]);
  2524. makeAttrib("spellhldie", sp.data["Higher Spell Slot Dice"]);
  2525. makeAttrib("spellhldietype", sp.data["Higher Spell Slot Die"]);
  2526. const [text, hlText] = sp.content.split("\n\nAt Higher Levels:")
  2527. makeAttrib("spelldescription", addInlineRollers(text));
  2528. makeAttrib("spellathigherlevels", addInlineRollers(hlText));
  2529. makeAttrib("options-flag", "0");
  2530.  
  2531. // TODO reverse engineer/add the other ~20 attributes needed to make this work (see `enableSpellattackHack()`)
  2532. if (sp.content.toLowerCase().includes("ranged spell attack")) {
  2533. makeAttrib("spelloutput", "ATTACK");
  2534. makeAttrib("spellattack", "Ranged");
  2535. } else if (sp.content.toLowerCase().includes("melee spell attack")) {
  2536. makeAttrib("spelloutput", "ATTACK");
  2537. makeAttrib("spellattack", "Melee");
  2538. } else if (sp.data.Damage) {
  2539. makeAttrib("spelloutput", "ATTACK");
  2540. makeAttrib("spellattack", "None");
  2541. }
  2542.  
  2543. tokenActionStack.push(`[${sp.name}](~selected|${base}spell)`);
  2544.  
  2545. if (index === addMacroIndex) {
  2546. if (d20plus.getCfgVal("token", "tokenactionsSpells")) {
  2547. character.abilities.create({
  2548. name: "Spells",
  2549. istokenaction: true,
  2550. action: `/w gm @{selected|wtype}&{template:npcaction} {{name=@{selected|npc_name}}} {{rname=Spellcasting}} {{description=${tokenActionStack.join("")}}}`
  2551. });
  2552. }
  2553. enableSpellattackHack();
  2554. }
  2555.  
  2556. function enableSpellattackHack () {
  2557. // temporary(?) hack to avoid creating all the properties manually
  2558. setTimeout(() => {
  2559. const $sel = character.view.$charsheet.find(`select[name=attr_spelloutput]`).filter((i, ele) => {
  2560. return $(ele).val() === "ATTACK";
  2561. });
  2562. setTimeout(() => {
  2563. $sel.val("SPELLCARD").trigger("change")
  2564. setTimeout(() => {
  2565. $sel.val("ATTACK").trigger("change");
  2566. }, spAbilsDelayMs);
  2567. }, spAbilsDelayMs);
  2568. }, spAbilsDelayMs);
  2569. }
  2570.  
  2571. function makeAttrib (name, current) {
  2572. if (current !== undefined && current !== null) character.attribs.create({
  2573. name: `${base}${name}`,
  2574. current: current
  2575. });
  2576. }
  2577.  
  2578. function addInlineRollers (text) {
  2579. if (!text) return text;
  2580. return text.replace(DICE_REGEX, (match) => {
  2581. return `[[${match}]]`;
  2582. });
  2583. }
  2584. }
  2585. }
  2586. if (data.trait) {
  2587. $.each(data.trait, function (i, v) {
  2588. var newRowId = d20plus.generateRowId();
  2589. character.attribs.create({
  2590. name: "repeating_npctrait_" + newRowId + "_name",
  2591. current: v.name
  2592. });
  2593.  
  2594. if (d20plus.getCfgVal("token", "tokenactionsTraits")) {
  2595. const offsetIndex = data.spellcasting ? 1 + i : i;
  2596. character.abilities.create({
  2597. name: "Trait" + offsetIndex + ": " + v.name,
  2598. istokenaction: true,
  2599. action: d20plus.actionMacroTrait(offsetIndex)
  2600. });
  2601. }
  2602.  
  2603. var text = d20plus.importer.getCleanText(renderer.renderEntry({entries: v.entries}, 1));
  2604. character.attribs.create({name: "repeating_npctrait_" + newRowId + "_desc", current: text});
  2605. });
  2606. }
  2607. if (data.action) {
  2608. $.each(data.action, function (i, v) {
  2609. var text = d20plus.importer.getCleanText(renderer.renderEntry({entries: v.entries}, 1));
  2610. d20plus.importer.addAction(character, v.name, text, i);
  2611. });
  2612. }
  2613. if (data.reaction) {
  2614. character.attribs.create({name: "reaction_flag", current: 1});
  2615. character.attribs.create({name: "npcreactionsflag", current: 1});
  2616. $.each(data.reaction, function (i, v) {
  2617. var newRowId = d20plus.generateRowId();
  2618. var text = "";
  2619. character.attribs.create({
  2620. name: "repeating_npcreaction_" + newRowId + "_name",
  2621. current: v.name
  2622. });
  2623.  
  2624. // roll20 only supports a single reaction, so only use the first
  2625. if (d20plus.getCfgVal("token", "tokenactions") && i === 0) {
  2626. character.abilities.create({
  2627. name: "Reaction: " + v.name,
  2628. istokenaction: true,
  2629. action: d20plus.actionMacroReaction
  2630. });
  2631. }
  2632.  
  2633. var text = d20plus.importer.getCleanText(renderer.renderEntry({entries: v.entries}, 1));
  2634. character.attribs.create({
  2635. name: "repeating_npcreaction_" + newRowId + "_desc",
  2636. current: text
  2637. });
  2638. character.attribs.create({
  2639. name: "repeating_npcreaction_" + newRowId + "_description",
  2640. current: text
  2641. });
  2642. });
  2643. }
  2644. if (data.legendary) {
  2645. character.attribs.create({name: "legendary_flag", current: "1"});
  2646. let legendaryActions = data.legendaryActions || 3;
  2647. character.attribs.create({name: "npc_legendary_actions", current: legendaryActions.toString()});
  2648. let tokenactiontext = "";
  2649. $.each(data.legendary, function (i, v) {
  2650. var newRowId = d20plus.generateRowId();
  2651.  
  2652. if (d20plus.getCfgVal("token", "tokenactions")) {
  2653. tokenactiontext += "[" + v.name + "](~selected|repeating_npcaction-l_$" + i + "_npc_action)\n\r";
  2654. }
  2655.  
  2656. var rollbase = d20plus.importer.rollbase;
  2657. if (v.attack != null) {
  2658. if (!(v.attack instanceof Array)) {
  2659. var tmp = v.attack;
  2660. v.attack = [];
  2661. v.attack.push(tmp);
  2662. }
  2663. $.each(v.attack, function (z, x) {
  2664. if (!x) return;
  2665. var attack = x.split("|");
  2666. var name = "";
  2667. if (v.attack.length > 1)
  2668. name = (attack[0] == v.name) ? v.name : v.name + " - " + attack[0] + "";
  2669. else
  2670. name = v.name;
  2671. var onhit = "";
  2672. var damagetype = "";
  2673. if (attack.length == 2) {
  2674. damage = "" + attack[1];
  2675. tohit = "";
  2676. } else {
  2677. damage = "" + attack[2];
  2678. tohit = attack[1] || 0;
  2679. }
  2680. character.attribs.create({
  2681. name: "repeating_npcaction-l_" + newRowId + "_name",
  2682. current: name
  2683. });
  2684. character.attribs.create({
  2685. name: "repeating_npcaction-l_" + newRowId + "_attack_flag",
  2686. current: "on"
  2687. });
  2688. character.attribs.create({
  2689. name: "repeating_npcaction-l_" + newRowId + "_npc_options-flag",
  2690. current: 0
  2691. });
  2692. character.attribs.create({
  2693. name: "repeating_npcaction-l_" + newRowId + "_attack_display_flag",
  2694. current: "{{attack=1}}"
  2695. });
  2696. character.attribs.create({
  2697. name: "repeating_npcaction-l_" + newRowId + "_attack_options",
  2698. current: "{{attack=1}}"
  2699. });
  2700. character.attribs.create({
  2701. name: "repeating_npcaction-l_" + newRowId + "_attack_tohit",
  2702. current: tohit
  2703. });
  2704. character.attribs.create({
  2705. name: "repeating_npcaction-l_" + newRowId + "_attack_damage",
  2706. current: damage
  2707. });
  2708. character.attribs.create({
  2709. name: "repeating_npcaction-l_" + newRowId + "_name_display",
  2710. current: name
  2711. });
  2712. character.attribs.create({
  2713. name: "repeating_npcaction-l_" + newRowId + "_rollbase",
  2714. current: rollbase
  2715. });
  2716. character.attribs.create({
  2717. name: "repeating_npcaction-l_" + newRowId + "_attack_type",
  2718. current: ""
  2719. });
  2720. character.attribs.create({
  2721. name: "repeating_npcaction-l_" + newRowId + "_attack_tohitrange",
  2722. current: ""
  2723. });
  2724. character.attribs.create({
  2725. name: "repeating_npcaction-l_" + newRowId + "_damage_flag",
  2726. current: "{{damage=1}} {{dmg1flag=1}} {{dmg2flag=1}}"
  2727. });
  2728. if (damage !== "") {
  2729. damage1 = damage.replace(/\s/g, "").split(/d|(?=\+|-)/g);
  2730. if (damage1[1])
  2731. damage1[1] = damage1[1].replace(/[^0-9-+]/g, "");
  2732. damage2 = isNaN(eval(damage1[1])) === false ? eval(damage1[1]) : 0;
  2733. if (damage1.length < 2) {
  2734. onhit = onhit + damage1[0] + " (" + damage + ")" + damagetype + " damage";
  2735. } else if (damage1.length < 3) {
  2736. onhit = onhit + Math.floor(damage1[0] * ((damage2 / 2) + 0.5)) + " (" + damage + ")" + damagetype + " damage";
  2737. } else {
  2738. onhit = onhit + (Math.floor(damage1[0] * ((damage2 / 2) + 0.5)) + parseInt(damage1[2], 10)) + " (" + damage + ")" + damagetype + " damage";
  2739. }
  2740. }
  2741. character.attribs.create({
  2742. name: "repeating_npcaction-l_" + newRowId + "_attack_onhit",
  2743. current: onhit
  2744. });
  2745. });
  2746. } else {
  2747. character.attribs.create({
  2748. name: "repeating_npcaction-l_" + newRowId + "_name",
  2749. current: v.name
  2750. });
  2751. character.attribs.create({
  2752. name: "repeating_npcaction-l_" + newRowId + "_npc_options-flag",
  2753. current: 0
  2754. });
  2755. character.attribs.create({
  2756. name: "repeating_npcaction-l_" + newRowId + "_rollbase",
  2757. current: rollbase
  2758. });
  2759. character.attribs.create({
  2760. name: "repeating_npcaction-l_" + newRowId + "_name_display",
  2761. current: v.name
  2762. });
  2763. }
  2764.  
  2765. var text = d20plus.importer.getCleanText(renderer.renderEntry({entries: v.entries}, 1));
  2766. var descriptionFlag = Math.max(Math.ceil(text.length / 57), 1);
  2767. character.attribs.create({
  2768. name: "repeating_npcaction-l_" + newRowId + "_description",
  2769. current: text
  2770. });
  2771. character.attribs.create({
  2772. name: "repeating_npcaction-l_" + newRowId + "_description_flag",
  2773. current: descriptionFlag
  2774. });
  2775. });
  2776. if (d20plus.getCfgVal("token", "tokenactions")) {
  2777. character.abilities.create({
  2778. name: "Legendary Actions",
  2779. istokenaction: true,
  2780. action: d20plus.actionMacroLegendary(tokenactiontext)
  2781. });
  2782. }
  2783. }
  2784. character.view._updateSheetValues();
  2785. var dirty = [];
  2786. $.each(d20.journal.customSheets.attrDeps, function (i, v) {
  2787. dirty.push(i);
  2788. });
  2789. d20.journal.notifyWorkersOfAttrChanges(character.view.model.id, dirty, true);
  2790.  
  2791.  
  2792. if (renderFluff) {
  2793. setTimeout(() => {
  2794. const fluffAs = d20plus.getCfgVal("import", "importFluffAs") || d20plus.getCfgDefaultVal("import", "importFluffAs");
  2795. let k = fluffAs === "Bio"? "bio" : "gmnotes";
  2796. character.updateBlobs({
  2797. [k]: Markdown.parse(renderFluff)
  2798. });
  2799. character.save({
  2800. [k]: (new Date).getTime()
  2801. });
  2802. }, 500);
  2803. }
  2804. } catch (e) {
  2805. d20plus.log("Error loading [" + name + "]");
  2806. d20plus.addImportError(name);
  2807. console.log(data);
  2808. console.log(e);
  2809. }
  2810. /* end OGL Sheet */
  2811. d20.journal.addItemToFolderStructure(character.id, folder.id);
  2812. }
  2813. });
  2814. };
  2815.  
  2816. d20plus.importer.findAttrId = function (character, attrName) {
  2817. const found = character.attribs.toJSON().find(a => a.name === attrName);
  2818. return found ? found.id : undefined;
  2819. };
  2820.  
  2821. d20plus.importer.addOrUpdateAttr = function (character, attrName, value) {
  2822. const id = d20plus.importer.findAttrId(character, attrName);
  2823. if (id) {
  2824. character.attribs.get(id).set("current", value);
  2825. } else {
  2826. character.attribs.create({
  2827. "name": attrName,
  2828. "current": value
  2829. });
  2830. }
  2831. };
  2832.  
  2833. // Import dialog showing names of monsters failed to import
  2834. d20plus.addImportError = function (name) {
  2835. var $span = $("#import-errors");
  2836. if ($span.text() == "0") {
  2837. $span.text(name);
  2838. } else {
  2839. $span.text($span.text() + ", " + name);
  2840. }
  2841. };
  2842.  
  2843. // Get NPC size from chr
  2844. d20plus.getSizeString = function (chr) {
  2845. const result = Parser.sizeAbvToFull(chr);
  2846. return result ? result : "(Unknown Size)";
  2847. };
  2848.  
  2849. // Create editable HP variable and autocalculate + or -
  2850. d20plus.hpAllowEdit = function () {
  2851. $("#initiativewindow").on(window.mousedowntype, ".hp.editable", function () {
  2852. if ($(this).find("input").length > 0) return void $(this).find("input").focus();
  2853. var val = $.trim($(this).text());
  2854. const $span = $(this);
  2855. $span.html(`<input type='text' value='${val}'/>`);
  2856. const $ipt = $(this).find("input");
  2857. $ipt[0].focus();
  2858. });
  2859. $("#initiativewindow").on("keydown", ".hp.editable", function (event) {
  2860. if (event.which == 13) {
  2861. const $span = $(this);
  2862. const $ipt = $span.find("input");
  2863. if (!$ipt.length) return;
  2864.  
  2865. var el, token, id, char, hp,
  2866. val = $.trim($ipt.val());
  2867.  
  2868. // roll20 token modification supports plus/minus for a single integer; mimic this
  2869. const m = /^((\d+)?([+-]))?(\d+)$/.exec(val);
  2870. if (m) {
  2871. let op = null;
  2872. if (m[3]) {
  2873. op = m[3] === "+" ? "ADD" : "SUB";
  2874. }
  2875. const base = m[2] ? eval(m[0]) : null;
  2876. const mod = Number(m[4]);
  2877.  
  2878. el = $(this).parents("li.token");
  2879. id = el.data("tokenid");
  2880. token = d20.Campaign.pages.get(d20.Campaign.activePage()).thegraphics.get(id);
  2881. char = token.character;
  2882.  
  2883. npc = char.attribs ? char.attribs.find(function (a) {
  2884. return a.get("name").toLowerCase() === "npc";
  2885. }) : null;
  2886. let total;
  2887. // char.attribs doesn't exist for generico tokens, in this case stick stuff in an appropriate bar
  2888. if (!char.attribs || npc && npc.get("current") == "1") {
  2889. const hpBar = d20plus.getCfgHpBarNumber();
  2890. if (hpBar) {
  2891. total;
  2892. if (base !== null) {
  2893. total = base;
  2894. } else if (op) {
  2895. const curr = token.attributes[`bar${hpBar}_value`];
  2896. if (op === "ADD") total = curr + mod;
  2897. else total = curr - mod;
  2898. } else {
  2899. total = mod;
  2900. }
  2901. token.attributes[`bar${hpBar}_value`] = total;
  2902. }
  2903. } else {
  2904. hp = char.attribs.find(function (a) {
  2905. return a.get("name").toLowerCase() === "hp";
  2906. });
  2907. if (hp) {
  2908. total;
  2909. if (base !== null) {
  2910. total = base;
  2911. } else if (op) {
  2912. if (op === "ADD") total = hp.attributes.current + mod;
  2913. else total = hp.attributes.current - mod;
  2914. } else {
  2915. total = mod;
  2916. }
  2917. hp.syncedSave({current: total});
  2918. } else {
  2919. total;
  2920. if (base !== null) {
  2921. total = base;
  2922. } else if (op) {
  2923. if (op === "ADD") total = mod;
  2924. else total = 0 - mod;
  2925. } else {
  2926. total = mod;
  2927. }
  2928. char.attribs.create({name: "hp", current: total});
  2929. }
  2930. }
  2931. // convert the field back to text
  2932. $span.html(total);
  2933. }
  2934. d20.Campaign.initiativewindow.rebuildInitiativeList();
  2935. }
  2936. });
  2937. };
  2938.  
  2939. // Change character sheet formulas
  2940. d20plus.setSheet = function () {
  2941. d20plus.sheet = "ogl";
  2942. if (window.is_gm && (!d20.journal.customSheets || !d20.journal.customSheets)) {
  2943. d20.textchat.incoming(false, ({
  2944. who: "system",
  2945. type: "system",
  2946. content: `<span style="color: red;">5etoolsR20: no character sheet selected! Exiting...</span>`
  2947. }));
  2948. throw new Error("No character sheet selected!");
  2949. }
  2950. if (d20.journal.customSheets.layouthtml.indexOf("shaped_d20") > 0) d20plus.sheet = "shaped";
  2951. if (d20.journal.customSheets.layouthtml.indexOf("DnD5e_Character_Sheet") > 0) d20plus.sheet = "community";
  2952. d20plus.log("Switched Character Sheet Template to " + d20plus.sheet);
  2953. };
  2954.  
  2955. // Return Initiative Tracker template with formulas
  2956. d20plus.initErrorHandler = null;
  2957. d20plus.setTurnOrderTemplate = function () {
  2958. if (!d20plus.turnOrderCachedFunction) {
  2959. d20plus.turnOrderCachedFunction = d20.Campaign.initiativewindow.rebuildInitiativeList;
  2960. d20plus.turnOrderCachedTemplate = $("#tmpl_initiativecharacter").clone();
  2961. }
  2962.  
  2963. d20.Campaign.initiativewindow.rebuildInitiativeList = function () {
  2964. var html = d20plus.initiativeTemplate;
  2965. var columnsAdded = [];
  2966. $(".tracker-header-extra-columns").empty();
  2967.  
  2968. const cols = [
  2969. d20plus.getCfgVal("interface", "trackerCol1"),
  2970. d20plus.getCfgVal("interface", "trackerCol2"),
  2971. d20plus.getCfgVal("interface", "trackerCol3")
  2972. ];
  2973.  
  2974. const headerStack = [];
  2975. const replaceStack = [
  2976. // this is hidden by CSS
  2977. `<span class='cr' alt='CR' title='CR'>
  2978. <$ if(npc && npc.get("current") == "1") { $>
  2979. <$!char.attribs.find(function(e) { return e.get("name").toLowerCase() === "npc_challenge" }).get("current")$>
  2980. <$ } $>
  2981. </span>`
  2982. ];
  2983. cols.forEach((c, i) => {
  2984. switch (c) {
  2985. case "HP": {
  2986. const hpBar = d20plus.getCfgHpBarNumber();
  2987. replaceStack.push(`
  2988. <span class='hp editable tracker-col' alt='HP' title='HP'>
  2989. <$ if(npc && npc.get("current") == "1") { $>
  2990. ${hpBar ? `<$!token.attributes.bar${hpBar}_value$>` : ""}
  2991. <$ } else if (typeof char !== "undefined" && char && typeof char.autoCalcFormula !== "undefined") { $>
  2992. <$!char.autoCalcFormula('${d20plus.formulas[d20plus.sheet].hp}')$>
  2993. <$ } else { $>
  2994. <$!"\u2014"$>
  2995. <$ } $>
  2996. </span>
  2997. `);
  2998. headerStack.push(`<span class='tracker-col'>HP</span>`);
  2999. break;
  3000. }
  3001. case "AC": {
  3002. replaceStack.push(`
  3003. <span class='ac tracker-col' alt='AC' title='AC'>
  3004. <$ if(npc && npc.get("current") == "1" && typeof char !== "undefined" && char && typeof char.autoCalcFormula !== "undefined") { $>
  3005. <$!char.autoCalcFormula('${d20plus.formulas[d20plus.sheet].npcac}')$>
  3006. <$ } else if (typeof char !== "undefined" && char && typeof char.autoCalcFormula !== "undefined") { $>
  3007. <$!char.autoCalcFormula('${d20plus.formulas[d20plus.sheet].ac}')$>
  3008. <$ } else { $>
  3009. <$!"\u2014"$>
  3010. <$ } $>
  3011. </span>
  3012. `);
  3013. headerStack.push(`<span class='tracker-col'>AC</span>`);
  3014. break;
  3015. }
  3016. case "PP": {
  3017. replaceStack.push(`
  3018. <$ var passive = (typeof char !== "undefined" && char && typeof char.autoCalcFormula !== "undefined") ? (char.autoCalcFormula('@{passive}') || char.autoCalcFormula('${d20plus.formulas[d20plus.sheet].pp}')) : "\u2014"; $>
  3019. <span class='pp tracker-col' alt='Passive Perception' title='Passive Perception'><$!passive$></span>
  3020. `);
  3021. headerStack.push(`<span class='tracker-col'>PP</span>`);
  3022. break;
  3023. }
  3024. default: {
  3025. replaceStack.push(`<span class="tracker-col"/>`);
  3026. headerStack.push(`<span class="tracker-col"/>`);
  3027. }
  3028. }
  3029. });
  3030.  
  3031. console.log("use custom tracker val was ", d20plus.getCfgVal("interface", "customTracker"))
  3032. if (d20plus.getCfgVal("interface", "customTracker")) {
  3033. const $header = $(".tracker-header-extra-columns");
  3034. // prepend/reverse used since tracker gets populated in right-to-left order
  3035. headerStack.forEach(h => $header.prepend(h))
  3036. html = html.replace(`<!--5ETOOLS_REPLACE_TARGET-->`, replaceStack.reverse().join(" \n"));
  3037. }
  3038.  
  3039. $("#tmpl_initiativecharacter").replaceWith(html);
  3040.  
  3041. // Hack to catch errors, part 1
  3042. const startTime = (new Date).getTime();
  3043.  
  3044. var results = d20plus.turnOrderCachedFunction.apply(this, []);
  3045. setTimeout(function () {
  3046. $(".initmacrobutton").unbind("click");
  3047. $(".initmacrobutton").bind("click", function () {
  3048. console.log("Macro button clicked");
  3049. tokenid = $(this).parent().parent().data("tokenid");
  3050. var token, char;
  3051. var page = d20.Campaign.activePage();
  3052. if (page) token = page.thegraphics.get(tokenid);
  3053. if (token) char = token.character;
  3054. if (char) {
  3055. char.view.showDialog();
  3056. // d20.textchat.doChatInput(`%{` + char.id + `|` + d20plus.formulas[d20plus.sheet]["macro"] + `}`)
  3057. }
  3058. });
  3059.  
  3060. d20plus.bindTokens();
  3061. }, 100);
  3062.  
  3063. // Hack to catch errors, part 2
  3064. if (d20plus.initErrorHandler) {
  3065. window.removeEventListener("error", d20plus.initErrorHandler);
  3066. }
  3067. d20plus.initErrorHandler = function (event) {
  3068. // if we see an error within 250 msec of trying to override the initiative window...
  3069. if (((new Date).getTime() - startTime) < 250) {
  3070. d20plus.log("ERROR: failed to populate custom initiative tracker, restoring default...");
  3071. // restore the default functionality
  3072. $("#tmpl_initiativecharacter").replaceWith(d20plus.turnOrderCachedTemplate);
  3073. return d20plus.turnOrderCachedFunction();
  3074. }
  3075. };
  3076. window.addEventListener("error", d20plus.initErrorHandler);
  3077. return results;
  3078. };
  3079. };
  3080.  
  3081. d20plus.importer.makePlayerDraggable = function (importId, name) {
  3082. const $appTo = $(`#d20plus-playerimport`).find(`.Vetools-player-imported`);
  3083. const $li = $(`
  3084. <li class="journalitem dd-item handout ui-draggable compendium-item Vetools-draggable player-imported" data-playerimportid="${importId}">
  3085. <div class="dd-handle dd-sortablehandle">Drag</div>
  3086. <div class="dd-content">
  3087. <div class="token"><img src="/images/handout.png" draggable="false"></div>
  3088. <div class="name">
  3089. <div class="namecontainer">${name}</div>
  3090. </div>
  3091. </div>
  3092. </li>
  3093. `);
  3094. $li.draggable({
  3095. revert: true,
  3096. distance: 10,
  3097. revertDuration: 0,
  3098. helper: "clone",
  3099. handle: ".namecontainer",
  3100. appendTo: "body",
  3101. scroll: true,
  3102. start: function () {
  3103. console.log("drag start")
  3104. },
  3105. stop: function () {
  3106. console.log("drag stop")
  3107. }
  3108. });
  3109. $appTo.prepend($li);
  3110. };
  3111.  
  3112. d20plus.spells.formSpellUrl = function (fileName) {
  3113. return d20plus.formSrcUrl(SPELL_DATA_DIR, fileName);
  3114. };
  3115.  
  3116. d20plus.spells._groupOptions = ["Level", "Spell Points", "Alphabetical", "Source"];
  3117. d20plus.spells._listCols = ["name", "class", "level", "source"];
  3118. d20plus.spells._listItemBuilder = (it) => `
  3119. <span class="name col-4" title="name">${it.name}</span>
  3120. <span class="class col-3" title="class">${it.classes.fromClassList.map(c => `CLS[${c.name}]`).join(", ")}</span>
  3121. <span class="level col-3" title="level">LVL[${Parser.spLevelToFull(it.level)}]</span>
  3122. <span title="source (Full: ${Parser.sourceJsonToFull(it.source)})" class="source col-2">SRC[${Parser.sourceJsonToAbv(it.source)}]</span>`;
  3123. d20plus.spells._listIndexConverter = (sp) => {
  3124. return {
  3125. name: sp.name.toLowerCase(),
  3126. class: sp.classes.fromClassList.map(c => c.name.toLowerCase()),
  3127. level: Parser.spLevelToFull(sp.level).toLowerCase(),
  3128. source: Parser.sourceJsonToAbv(sp.source).toLowerCase()
  3129. };
  3130. };
  3131. // Import Spells button was clicked
  3132. d20plus.spells.button = function (forcePlayer) {
  3133. const playerMode = forcePlayer || !window.is_gm;
  3134. const url = playerMode ? $("#import-spell-url-player").val() : $("#import-spell-url").val();
  3135. if (url && url.trim()) {
  3136. const handoutBuilder = playerMode ? d20plus.spells.playerImportBuilder : d20plus.spells.handoutBuilder;
  3137.  
  3138. DataUtil.loadJSON(url, (data) => {
  3139. d20plus.importer.addMeta(data._meta);
  3140. d20plus.importer.showImportList(
  3141. "spell",
  3142. data.spell,
  3143. handoutBuilder,
  3144. {
  3145. groupOptions: d20plus.spells._groupOptions,
  3146. forcePlayer,
  3147. listItemBuilder: d20plus.spells._listItemBuilder,
  3148. listIndex: d20plus.spells._listCols,
  3149. listIndexConverter: d20plus.spells._listIndexConverter
  3150. }
  3151. );
  3152. });
  3153. }
  3154. };
  3155.  
  3156. // Import All Spells button was clicked
  3157. d20plus.spells.buttonAll = function (forcePlayer) {
  3158. const toLoad = Object.keys(spellDataUrls).filter(src => !isNonstandardSource(src)).map(src => d20plus.spells.formSpellUrl(spellDataUrls[src]));
  3159.  
  3160. if (toLoad.length) {
  3161. const handoutBuilder = !forcePlayer && window.is_gm ? d20plus.spells.handoutBuilder : d20plus.spells.playerImportBuilder;
  3162.  
  3163. DataUtil.multiLoadJSON(toLoad.map(url => ({url: url})), () => {
  3164. }, (dataStack) => {
  3165. let toAdd = [];
  3166. dataStack.forEach(d => toAdd = toAdd.concat(d.spell));
  3167. d20plus.importer.showImportList(
  3168. "spell",
  3169. toAdd,
  3170. handoutBuilder,
  3171. {
  3172. groupOptions: d20plus.spells._groupOptions,
  3173. forcePlayer,
  3174. listItemBuilder: d20plus.spells._listItemBuilder,
  3175. listIndex: d20plus.spells._listCols,
  3176. listIndexConverter: d20plus.spells._listIndexConverter
  3177. }
  3178. );
  3179. });
  3180. }
  3181. };
  3182.  
  3183. d20plus.importer.getTagString = function (data, prefix) {
  3184. return JSON.stringify(data.map(d => `${prefix}-${Parser.stringToSlug(d.toString())}`).concat([prefix]));
  3185. };
  3186.  
  3187. // Create spell handout from js data object
  3188. d20plus.spells.handoutBuilder = function (data, overwrite, inJournals, folderName, saveIdsTo, builderOptions) {
  3189. // make dir
  3190. const folder = d20plus.importer.makeDirTree(`Spells`, folderName);
  3191. const path = ["Spells", folderName, data.name];
  3192.  
  3193. // handle duplicates/overwrites
  3194. if (!d20plus.importer._checkHandleDuplicate(path, overwrite)) return;
  3195.  
  3196. const name = data.name;
  3197. // build spell handout
  3198. d20.Campaign.handouts.create({
  3199. name: name,
  3200. tags: d20plus.importer.getTagString([
  3201. Parser.spSchoolAbvToFull(data.school),
  3202. Parser.spLevelToFull(data.level),
  3203. ...data.classes.fromClassList.map(c => c.name),
  3204. Parser.sourceJsonToFull(data.source)
  3205. ], "spell")
  3206. }, {
  3207. success: function (handout) {
  3208. if (saveIdsTo) saveIdsTo[UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_SPELLS](data)] = {name: data.name, source: data.source, type: "handout", roll20Id: handout.id};
  3209.  
  3210. const [notecontents, gmnotes] = d20plus.spells._getHandoutData(data, builderOptions);
  3211.  
  3212. console.log(notecontents);
  3213. handout.updateBlobs({notes: notecontents, gmnotes: gmnotes});
  3214. handout.save({notes: (new Date).getTime(), inplayerjournals: inJournals});
  3215. d20.journal.addItemToFolderStructure(handout.id, folder.id);
  3216. }
  3217. });
  3218. };
  3219.  
  3220. d20plus.spells.playerImportBuilder = function (data) {
  3221. const [notecontents, gmnotes] = d20plus.spells._getHandoutData(data);
  3222.  
  3223. const importId = d20plus.generateRowId();
  3224. d20plus.importer.storePlayerImport(importId, JSON.parse(gmnotes));
  3225. d20plus.importer.makePlayerDraggable(importId, data.name);
  3226. };
  3227.  
  3228. d20plus.spells._getHandoutData = function (data, builderOptions) {
  3229. builderOptions = builderOptions || {};
  3230. // merge in roll20 metadata, if available
  3231. const spellMeta = spellMetaData.spell.find(sp => sp.name.toLowerCase() === data.name.toLowerCase() && sp.source.toLowerCase() === data.source.toLowerCase());
  3232. if (spellMeta) {
  3233. data.roll20 = spellMeta.data;
  3234. }
  3235.  
  3236. if (!data.school) data.school = "A";
  3237. if (!data.range) data.range = "Self";
  3238. if (!data.duration) data.duration = "Instantaneous";
  3239. if (!data.components) data.components = "";
  3240. if (!data.time) data.components = "1 action";
  3241.  
  3242. const r20Data = {};
  3243. if (data.roll20) Object.assign(r20Data, data.roll20);
  3244. Object.assign(
  3245. r20Data,
  3246. {
  3247. "Level": builderOptions.isSpellPoints ? String(Math.min(9, d20plus.spells.spLevelToSpellPoints(data.level))) : String(data.level),
  3248. "Range": Parser.spRangeToFull(data.range),
  3249. "School": Parser.spSchoolAbvToFull(data.school),
  3250. "Source": "5etoolsR20",
  3251. "Classes": d20plus.importer.getCleanText(Parser.spClassesToFull(data.classes)),
  3252. "Category": "Spells",
  3253. "Duration": Parser.spDurationToFull(data.duration),
  3254. "Material": "",
  3255. "Components": parseComponents(data.components),
  3256. "Casting Time": Parser.spTimeListToFull(data.time)
  3257. }
  3258. );
  3259.  
  3260. var r20json = {
  3261. name: data.name,
  3262. content: "",
  3263. htmlcontent: "",
  3264. data: r20Data
  3265. };
  3266. if (data.components.m && data.components.m.length) r20json.data["Material"] = data.components.m;
  3267. if (data.meta) {
  3268. if (data.meta.ritual) r20json.data["Ritual"] = "Yes";
  3269. }
  3270. if (data.duration.filter(d => d.concentration).length > 0) {
  3271. r20json.data["Concentration"] = "Yes";
  3272. }
  3273. var notecontents = "";
  3274. var gmnotes = "";
  3275. notecontents += `<p><h3>${data.name}</h3>
  3276. <em>${Parser.spLevelSchoolMetaToFull(data.level, data.school, data.meta)}${builderOptions.isSpellPoints && data.level ? ` (${d20plus.spells.spLevelToSpellPoints(data.level)} spell points)` : ""}</em></p><p>
  3277. <strong>Casting Time:</strong> ${Parser.spTimeListToFull(data.time)}<br>
  3278. <strong>Range:</strong> ${Parser.spRangeToFull(data.range)}<br>
  3279. <strong>Components:</strong> ${Parser.spComponentsToFull(data.components)}<br>
  3280. <strong>Duration:</strong> ${Parser.spDurationToFull(data.duration)}<br>
  3281. </p>`;
  3282. const renderer = new EntryRenderer();
  3283. const renderStack = [];
  3284. const entryList = {type: "entries", entries: data.entries};
  3285. renderer.setBaseUrl(BASE_SITE_URL);
  3286. renderer.recursiveEntryRender(entryList, renderStack, 1);
  3287. r20json.content = d20plus.importer.getCleanText(renderStack.join(" "));
  3288. notecontents += renderStack.join("");
  3289. if (data.entriesHigherLevel) {
  3290. const hLevelRenderStack = [];
  3291. const higherLevelsEntryList = {type: "entries", entries: data.entriesHigherLevel};
  3292. renderer.recursiveEntryRender(higherLevelsEntryList, hLevelRenderStack, 2);
  3293. r20json.content += "\n\nAt Higher Levels: " + d20plus.importer.getCleanText(hLevelRenderStack.join(" ").replace("At Higher Levels.", ""));
  3294. notecontents += hLevelRenderStack.join("");
  3295. }
  3296. notecontents += `<p><strong>Classes:</strong> ${Parser.spClassesToFull(data.classes)}</p>`;
  3297. gmnotes = JSON.stringify(r20json);
  3298. notecontents += `<del>${gmnotes}</del>`;
  3299.  
  3300. return [notecontents, gmnotes];
  3301. };
  3302.  
  3303. // parse spell components
  3304. function parseComponents (components) {
  3305. const out = [];
  3306. if (components.v) out.push("V");
  3307. if (components.s) out.push("S");
  3308. if (components.m) out.push("M");
  3309. return out.join(" ");
  3310. }
  3311.  
  3312. d20plus.items._groupOptions = ["Type", "Rarity", "Alphabetical", "Source"];
  3313. d20plus.items._listCols = ["name", "type", "rarity", "source"];
  3314. d20plus.items._listItemBuilder = (it) => {
  3315. if (!it._isEnhanced) EntryRenderer.item.enhanceItem(it);
  3316.  
  3317. return `
  3318. <span class="name col-3" title="name">${it.name}</span>
  3319. <span class="type col-5" title="type">${it.typeText.split(",").map(t => `TYP[${t.trim()}]`).join(", ")}</span>
  3320. <span class="rarity col-2" title="rarity">RAR[${it.rarity}]</span>
  3321. <span title="source (Full: ${Parser.sourceJsonToFull(it.source)})" class="source col-2">SRC[${Parser.sourceJsonToAbv(it.source)}]</span>`;
  3322. };
  3323. d20plus.items._listIndexConverter = (it) => {
  3324. if (!it._isEnhanced) EntryRenderer.item.enhanceItem(it);
  3325. return {
  3326. name: it.name.toLowerCase(),
  3327. type: it.typeText.toLowerCase().split(","),
  3328. rarity: it.rarity.toLowerCase(),
  3329. source: Parser.sourceJsonToAbv(it.source).toLowerCase()
  3330. };
  3331. };
  3332. // Import Items button was clicked
  3333. d20plus.items.button = function (forcePlayer) {
  3334. const playerMode = forcePlayer || !window.is_gm;
  3335. const url = playerMode ? $("#import-items-url-player").val() : $("#import-items-url").val();
  3336. if (url && url.trim()) {
  3337. const handoutBuilder = playerMode ? d20plus.items.playerImportBuilder : d20plus.items.handoutBuilder;
  3338.  
  3339. if (url.trim() === "https://5etools.com/data/items.json") {
  3340. EntryRenderer.item.buildList((itemList) => {
  3341. d20plus.importer.showImportList(
  3342. "item",
  3343. itemList,
  3344. handoutBuilder,
  3345. {
  3346. groupOptions: d20plus.items._groupOptions,
  3347. forcePlayer,
  3348. listItemBuilder: d20plus.items._listItemBuilder,
  3349. listIndex: d20plus.items._listCols,
  3350. listIndexConverter: d20plus.items._listIndexConverter
  3351. }
  3352. );
  3353. },
  3354. {
  3355. items: "https://5etools.com/data/items.json",
  3356. basicitems: "https://5etools.com/data/basicitems.json",
  3357. magicvariants: "https://5etools.com/data/magicvariants.json"
  3358. });
  3359. } else {
  3360. // for non-standard URLs, do a generic import
  3361. DataUtil.loadJSON(url, (data) => {
  3362. d20plus.importer.addMeta(data._meta);
  3363. d20plus.importer.showImportList(
  3364. "item",
  3365. data.item,
  3366. handoutBuilder,
  3367. {
  3368. groupOptions: d20plus.items._groupOptions,
  3369. forcePlayer,
  3370. listItemBuilder: d20plus.items._listItemBuilder,
  3371. listIndex: d20plus.items._listCols,
  3372. listIndexConverter: d20plus.items._listIndexConverter
  3373. }
  3374. );
  3375. });
  3376. }
  3377. }
  3378. };
  3379.  
  3380. // Import individual items
  3381. d20plus.items.handoutBuilder = function (data, overwrite, inJournals, folderName, saveIdsTo) {
  3382. // make dir
  3383. const folder = d20plus.importer.makeDirTree(`Items`, folderName);
  3384. const path = ["Items", folderName, data.name];
  3385.  
  3386. // handle duplicates/overwrites
  3387. if (!d20plus.importer._checkHandleDuplicate(path, overwrite)) return;
  3388.  
  3389. const name = data.name;
  3390.  
  3391. if (!data._isEnhanced) EntryRenderer.item.enhanceItem(data); // for homebrew items
  3392.  
  3393. // build item handout
  3394. d20.Campaign.handouts.create({
  3395. name: name,
  3396. tags: d20plus.importer.getTagString([
  3397. `rarity ${data.rarity}`,
  3398. ...data.procType,
  3399. Parser.sourceJsonToFull(data.source)
  3400. ], "items")
  3401. }, {
  3402. success: function (handout) {
  3403. if (saveIdsTo) saveIdsTo[UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ITEMS](data)] = {name: data.name, source: data.source, type: "handout", roll20Id: handout.id};
  3404.  
  3405. const [notecontents, gmnotes] = d20plus.items._getHandoutData(data);
  3406.  
  3407. handout.updateBlobs({notes: notecontents, gmnotes: gmnotes});
  3408. handout.save({
  3409. notes: (new Date).getTime(),
  3410. inplayerjournals: inJournals
  3411. });
  3412. d20.journal.addItemToFolderStructure(handout.id, folder.id);
  3413. }
  3414. });
  3415. };
  3416.  
  3417. d20plus.items.playerImportBuilder = function (data) {
  3418. const [notecontents, gmnotes] = d20plus.items._getHandoutData(data);
  3419.  
  3420. const importId = d20plus.generateRowId();
  3421. d20plus.importer.storePlayerImport(importId, JSON.parse(gmnotes));
  3422. d20plus.importer.makePlayerDraggable(importId, data.name);
  3423. };
  3424.  
  3425. d20plus.items._getHandoutData = function (data) {
  3426. var notecontents = "";
  3427. roll20Data = {
  3428. name: data.name,
  3429. data: {
  3430. Category: "Items"
  3431. }
  3432. };
  3433. const typeArray = [];
  3434. if (data.wondrous) typeArray.push("Wondrous Item");
  3435. if (data.technology) typeArray.push(data.technology);
  3436. if (data.age) typeArray.push(data.age);
  3437. if (data.weaponCategory) typeArray.push(data.weaponCategory + " Weapon");
  3438. var type = data.type;
  3439. if (data.type) {
  3440. const fullType = d20plus.items.parseType(data.type);
  3441. typeArray.push(fullType);
  3442. roll20Data.data["Item Type"] = fullType;
  3443. } else if (data.typeText) {
  3444. roll20Data.data["Item Type"] = data.typeText;
  3445. }
  3446. var typestring = typeArray.join(", ");
  3447. var damage = "";
  3448. if (data.dmg1 && data.dmgType) damage = data.dmg1 + " " + Parser.dmgTypeToFull(data.dmgType);
  3449. var armorclass = "";
  3450. if (type === "S") armorclass = "+" + data.ac;
  3451. if (type === "LA") armorclass = data.ac + " + Dex";
  3452. if (type === "MA") armorclass = data.ac + " + Dex (max 2)";
  3453. if (type === "HA") armorclass = data.ac;
  3454. var properties = "";
  3455. if (data.property) {
  3456. var propertieslist = data.property;
  3457. for (var i = 0; i < propertieslist.length; i++) {
  3458. var a = d20plus.items.parseProperty(propertieslist[i]);
  3459. var b = propertieslist[i];
  3460. if (b === "V") a = a + " (" + data.dmg2 + ")";
  3461. if (b === "T" || b === "A") a = a + " (" + data.range + "ft.)";
  3462. if (b === "RLD") a = a + " (" + data.reload + " shots)";
  3463. if (i > 0) a = ", " + a;
  3464. properties += a;
  3465. }
  3466. }
  3467. var reqAttune = data.reqAttune;
  3468. var attunementstring = "";
  3469. if (reqAttune) {
  3470. if (reqAttune === "YES") {
  3471. attunementstring = " (Requires Attunement)";
  3472. } else if (reqAttune === "OPTIONAL") {
  3473. attunementstring = " (Attunement Optional)";
  3474. } else {
  3475. reqAttune = " (Requires Attunement " + reqAttune + ")";
  3476. }
  3477. }
  3478. notecontents += `<p><h3>${data.name}</h3></p><em>${typestring}`;
  3479. if (data.tier) notecontents += ", " + data.tier;
  3480. var rarity = data.rarity;
  3481. var ismagicitem = (rarity !== "None" && rarity !== "Unknown");
  3482. if (ismagicitem) notecontents += ", " + rarity;
  3483. if (attunementstring) notecontents += attunementstring;
  3484. notecontents += `</em>`;
  3485. if (damage) notecontents += `<p><strong>Damage: </strong>${damage}</p>`;
  3486. if (properties) {
  3487. notecontents += `<p><strong>Properties: </strong>${properties}</p>`;
  3488. roll20Data.data.Properties = properties;
  3489. }
  3490. if (armorclass) {
  3491. notecontents += `<p><strong>Armor Class: </strong>${armorclass}</p>`;
  3492. roll20Data.data.AC = String(data.ac);
  3493. }
  3494. if (data.weight) {
  3495. notecontents += `<p><strong>Weight: </strong>${data.weight} lbs.</p>`;
  3496. roll20Data.data.Weight = String(data.weight);
  3497. }
  3498. var itemtext = data.entries ? data.entries : "";
  3499. const renderer = new EntryRenderer();
  3500. const renderStack = [];
  3501. const entryList = {type: "entries", entries: data.entries};
  3502. renderer.setBaseUrl(BASE_SITE_URL);
  3503. renderer.recursiveEntryRender(entryList, renderStack, 1);
  3504. var textstring = renderStack.join("");
  3505. if (textstring) {
  3506. notecontents += `<hr>`;
  3507. notecontents += textstring;
  3508. }
  3509.  
  3510. if (data.range) {
  3511. roll20Data.data.Range = data.range;
  3512. }
  3513. if (data.dmg1 && data.dmgType) {
  3514. roll20Data.data.Damage = data.dmg1;
  3515. roll20Data.data["Damage Type"] = Parser.dmgTypeToFull(data.dmgType);
  3516. }
  3517. if (textstring.trim()) {
  3518. roll20Data.content = d20plus.importer.getCleanText(textstring);
  3519. roll20Data.htmlcontent = roll20Data.content;
  3520. }
  3521. if (data.stealth) {
  3522. roll20Data.data.Stealth = "Disadvantage";
  3523. }
  3524. // roll20Data.data.Duration = "1 Minute"; // used by e.g. poison; not show in sheet
  3525. // roll20Data.data.Save = "Constitution"; // used by e.g. poison, ball bearings; not shown in sheet
  3526. // 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
  3527. // roll20Data.data["Item Rarity"] = "Wondrous"; // used by Iron Bands of Binding... and nothing else?; not shown in sheet
  3528. if (data.reqAttune === "YES") {
  3529. roll20Data.data["Requires Attunement"] = "Yes";
  3530. } else {
  3531. roll20Data.data["Requires Attunement"] = "No";
  3532. }
  3533. // TODO handle other magic versions
  3534. // roll20Data.data.Modifiers = ... ; // this can be a variety of things, and is a comma separated list
  3535. // some examples, that are currently handled:
  3536. // "Ranged Attacks +3, Ranged Damage +3"
  3537. // "Ac +2"
  3538. // "Spell Attack +2"
  3539. // "Saving Throws +1"
  3540. // "AC +15, Spell Attack +2, Spell DC +2"
  3541. // ...and some examples, that are not:
  3542. // "Constitution +2"
  3543. // "Strength: 21"
  3544. if (data.modifier) {
  3545. const allModifiers = data.modifier.filter(m => m.__text).map(m => m.__text.split(" ").map(s => s.uppercaseFirst()).join(" ")).join(", ");
  3546. roll20Data.data.Modifiers = allModifiers;
  3547. }
  3548.  
  3549. const gmnotes = JSON.stringify(roll20Data);
  3550.  
  3551. return [notecontents, gmnotes];
  3552. };
  3553.  
  3554. d20plus.items.parseType = function (type) {
  3555. const result = Parser.itemTypeToAbv(type);
  3556. return result ? result : "n/a";
  3557. };
  3558.  
  3559. d20plus.items.parseDamageType = function (damagetype) {
  3560. const result = Parser.dmgTypeToFull(damagetype);
  3561. return result ? result : false;
  3562. };
  3563.  
  3564. d20plus.items.parseProperty = function (property) {
  3565. if (property === "A") return "ammunition";
  3566. if (property === "AF") return "ammunition";
  3567. if (property === "BF") return "burst fire";
  3568. if (property === "F") return "finesse";
  3569. if (property === "H") return "heavy";
  3570. if (property === "L") return "light";
  3571. if (property === "LD") return "loading";
  3572. if (property === "R") return "reach";
  3573. if (property === "RLD") return "reload";
  3574. if (property === "S") return "special";
  3575. if (property === "T") return "thrown";
  3576. if (property === "2H") return "two-handed";
  3577. if (property === "V") return "versatile";
  3578. return "n/a";
  3579. };
  3580.  
  3581. d20plus.psionics._groupOptions = ["Alphabetical", "Order", "Source"];
  3582. d20plus.psionics._listCols = ["name", "order", "source"];
  3583. d20plus.psionics._listItemBuilder = (it) => `
  3584. <span class="name col-6">${it.name}</span>
  3585. <span class="order col-4">ORD[${it.order || "None"}]</span>
  3586. <span title="${Parser.sourceJsonToFull(it.source)}" class="source col-2">SRC[${Parser.sourceJsonToAbv(it.source)}]</span>`;
  3587. d20plus.psionics._listIndexConverter = (p) => {
  3588. return {
  3589. name: p.name.toLowerCase(),
  3590. order: (p.order || "none").toLowerCase(),
  3591. source: Parser.sourceJsonToAbv(p.source).toLowerCase()
  3592. };
  3593. };
  3594. // Import Psionics button was clicked
  3595. d20plus.psionics.button = function (forcePlayer) {
  3596. const playerMode = forcePlayer || !window.is_gm;
  3597. const url = playerMode ? $("#import-psionics-url-player").val() : $("#import-psionics-url").val();
  3598. if (url && url.trim()) {
  3599. const handoutBuilder = playerMode ? d20plus.psionics.playerImportBuilder : d20plus.psionics.handoutBuilder;
  3600.  
  3601. DataUtil.loadJSON(url, (data) => {
  3602. d20plus.importer.addMeta(data._meta);
  3603. d20plus.importer.showImportList(
  3604. "psionic",
  3605. data.psionic,
  3606. handoutBuilder,
  3607. {
  3608. groupOptions: d20plus.psionics._groupOptions,
  3609. forcePlayer,
  3610. listItemBuilder: d20plus.psionics._listItemBuilder,
  3611. listIndex: d20plus.psionics._listCols,
  3612. listIndexConverter: d20plus.psionics._listIndexConverter
  3613. }
  3614. );
  3615. });
  3616. }
  3617. };
  3618.  
  3619. d20plus.psionics.handoutBuilder = function (data, overwrite, inJournals, folderName, saveIdsTo) {
  3620. // make dir
  3621. const folder = d20plus.importer.makeDirTree(`Psionics`, folderName);
  3622. const path = ["Psionics", folderName, data.name];
  3623.  
  3624. // handle duplicates/overwrites
  3625. if (!d20plus.importer._checkHandleDuplicate(path, overwrite)) return;
  3626.  
  3627. const name = data.name;
  3628. d20.Campaign.handouts.create({
  3629. name: name,
  3630. tags: d20plus.importer.getTagString([
  3631. Parser.psiTypeToFull(data.type),
  3632. data.order || "orderless",
  3633. Parser.sourceJsonToFull(data.source)
  3634. ], "psionics")
  3635. }, {
  3636. success: function (handout) {
  3637. if (saveIdsTo) saveIdsTo[UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_PSIONICS](data)] = {name: data.name, source: data.source, type: "handout", roll20Id: handout.id};
  3638.  
  3639. const [noteContents, gmNotes] = d20plus.psionics._getHandoutData(data);
  3640.  
  3641. handout.updateBlobs({notes: noteContents, gmnotes: gmNotes});
  3642. handout.save({notes: (new Date).getTime(), inplayerjournals: inJournals});
  3643. d20.journal.addItemToFolderStructure(handout.id, folder.id);
  3644. }
  3645. });
  3646. };
  3647.  
  3648. d20plus.psionics.playerImportBuilder = function (data) {
  3649. const [notecontents, gmnotes] = d20plus.psionics._getHandoutData(data);
  3650.  
  3651. const importId = d20plus.generateRowId();
  3652. d20plus.importer.storePlayerImport(importId, JSON.parse(gmnotes));
  3653. d20plus.importer.makePlayerDraggable(importId, data.name);
  3654. };
  3655.  
  3656. d20plus.psionics._getHandoutData = function (data) {
  3657. function renderTalent () {
  3658. const renderStack = [];
  3659. renderer.recursiveEntryRender(({entries: data.entries, type: "entries"}), renderStack);
  3660. return renderStack.join(" ");
  3661. }
  3662.  
  3663. const renderer = new EntryRenderer();
  3664. renderer.setBaseUrl(BASE_SITE_URL);
  3665. data.data = {
  3666. Category: "Psionics"
  3667. };
  3668. const gmNotes = JSON.stringify(data);
  3669.  
  3670. const baseNoteContents = `
  3671. <h3>${data.name}</h3>
  3672. <p><em>${data.type === "D" ? `${data.order} ${Parser.psiTypeToFull(data.type)}` : `${Parser.psiTypeToFull(data.type)}`}</em></p>
  3673. ${data.type === "D" ? `${EntryRenderer.psionic.getDisciplineText(data, renderer)}` : `${renderTalent()}`}
  3674. `;
  3675.  
  3676. const noteContents = `${baseNoteContents}<br><del>${gmNotes}</del>`;
  3677.  
  3678. return [noteContents, gmNotes];
  3679. };
  3680.  
  3681. // Import Races button was clicked
  3682. d20plus.races.button = function (forcePlayer) {
  3683. const playerMode = forcePlayer || !window.is_gm;
  3684. const url = playerMode ? $("#import-races-url-player").val() : $("#import-races-url").val();
  3685. if (url && url.trim()) {
  3686. const handoutBuilder = playerMode ? d20plus.races.playerImportBuilder : d20plus.races.handoutBuilder;
  3687.  
  3688. DataUtil.loadJSON(url, (data) => {
  3689. d20plus.importer.addMeta(data._meta);
  3690. d20plus.importer.showImportList(
  3691. "race",
  3692. EntryRenderer.race.mergeSubraces(data.race),
  3693. handoutBuilder,
  3694. {
  3695. forcePlayer
  3696. }
  3697. );
  3698. });
  3699. }
  3700. };
  3701.  
  3702. d20plus.races.handoutBuilder = function (data, overwrite, inJournals, folderName, saveIdsTo) {
  3703. // make dir
  3704. const folder = d20plus.importer.makeDirTree(`Races`, folderName);
  3705. const path = ["Races", folderName, data.name];
  3706.  
  3707. // handle duplicates/overwrites
  3708. if (!d20plus.importer._checkHandleDuplicate(path, overwrite)) return;
  3709.  
  3710. const name = data.name;
  3711. d20.Campaign.handouts.create({
  3712. name: name,
  3713. tags: d20plus.importer.getTagString([
  3714. Parser.sizeAbvToFull(data.size),
  3715. Parser.sourceJsonToFull(data.source)
  3716. ], "races")
  3717. }, {
  3718. success: function (handout) {
  3719. if (saveIdsTo) saveIdsTo[UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_RACES](data)] = {name: data.name, source: data.source, type: "handout", roll20Id: handout.id};
  3720.  
  3721. const [noteContents, gmNotes] = d20plus.races._getHandoutData(data);
  3722.  
  3723. handout.updateBlobs({notes: noteContents, gmnotes: gmNotes});
  3724. handout.save({notes: (new Date).getTime(), inplayerjournals: inJournals});
  3725. d20.journal.addItemToFolderStructure(handout.id, folder.id);
  3726. }
  3727. });
  3728. };
  3729.  
  3730. d20plus.races.playerImportBuilder = function (data) {
  3731. const [notecontents, gmnotes] = d20plus.races._getHandoutData(data);
  3732.  
  3733. const importId = d20plus.generateRowId();
  3734. d20plus.importer.storePlayerImport(importId, JSON.parse(gmnotes));
  3735. d20plus.importer.makePlayerDraggable(importId, data.name);
  3736. };
  3737.  
  3738. d20plus.races._getHandoutData = function (data) {
  3739. const renderer = new EntryRenderer();
  3740. renderer.setBaseUrl(BASE_SITE_URL);
  3741.  
  3742. // TODO
  3743. const renderStack = [];
  3744. const ability = utils_getAbilityData(data.ability);
  3745. renderStack.push(`
  3746. <h3>${data.name}</h3>
  3747. <p>
  3748. <strong>Ability Scores:</strong> ${ability.asText}<br>
  3749. <strong>Size:</strong> ${Parser.sizeAbvToFull(data.size)}<br>
  3750. <strong>Speed:</strong> ${Parser.getSpeedString(data)}<br>
  3751. </p>
  3752. `);
  3753. renderer.recursiveEntryRender({entries: data.entries}, renderStack, 1);
  3754. const rendered = renderStack.join("");
  3755.  
  3756. const r20json = {
  3757. "name": data.name,
  3758. "Vetoolscontent": data,
  3759. "data": {
  3760. "Category": "Races"
  3761. }
  3762. };
  3763. const gmNotes = JSON.stringify(r20json);
  3764. const noteContents = `${rendered}\n\n<del>${gmNotes}</del>`;
  3765.  
  3766. return [noteContents, gmNotes];
  3767. };
  3768.  
  3769. // Import Feats button was clicked
  3770. d20plus.feats.button = function (forcePlayer) {
  3771. const playerMode = forcePlayer || !window.is_gm;
  3772. const url = playerMode ? $("#import-feats-url-player").val() : $("#import-feats-url").val();
  3773. if (url && url.trim()) {
  3774. const handoutBuilder = playerMode ? d20plus.feats.playerImportBuilder : d20plus.feats.handoutBuilder;
  3775.  
  3776. DataUtil.loadJSON(url, (data) => {
  3777. d20plus.importer.addMeta(data._meta);
  3778. d20plus.importer.showImportList(
  3779. "feat",
  3780. data.feat,
  3781. handoutBuilder,
  3782. {
  3783. forcePlayer
  3784. }
  3785. );
  3786. });
  3787. }
  3788. };
  3789.  
  3790. d20plus.feats.handoutBuilder = function (data, overwrite, inJournals, folderName, saveIdsTo) {
  3791. // make dir
  3792. const folder = d20plus.importer.makeDirTree(`Feats`, folderName);
  3793. const path = ["Feats", folderName, data.name];
  3794.  
  3795. // handle duplicates/overwrites
  3796. if (!d20plus.importer._checkHandleDuplicate(path, overwrite)) return;
  3797.  
  3798. const name = data.name;
  3799. d20.Campaign.handouts.create({
  3800. name: name,
  3801. tags: d20plus.importer.getTagString([
  3802. Parser.sourceJsonToFull(data.source)
  3803. ], "feats")
  3804. }, {
  3805. success: function (handout) {
  3806. if (saveIdsTo) saveIdsTo[UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_FEATS](data)] = {name: data.name, source: data.source, type: "handout", roll20Id: handout.id};
  3807.  
  3808. const [noteContents, gmNotes] = d20plus.feats._getHandoutData(data);
  3809.  
  3810. handout.updateBlobs({notes: noteContents, gmnotes: gmNotes});
  3811. handout.save({notes: (new Date).getTime(), inplayerjournals: inJournals});
  3812. d20.journal.addItemToFolderStructure(handout.id, folder.id);
  3813. }
  3814. });
  3815. };
  3816.  
  3817. d20plus.feats.playerImportBuilder = function (data) {
  3818. const [notecontents, gmnotes] = d20plus.feats._getHandoutData(data);
  3819.  
  3820. const importId = d20plus.generateRowId();
  3821. d20plus.importer.storePlayerImport(importId, JSON.parse(gmnotes));
  3822. d20plus.importer.makePlayerDraggable(importId, data.name);
  3823. };
  3824.  
  3825. d20plus.feats._getHandoutData = function (data) {
  3826. const renderer = new EntryRenderer();
  3827. renderer.setBaseUrl(BASE_SITE_URL);
  3828. const prerequisite = EntryRenderer.feat.getPrerequisiteText(data.prerequisite);
  3829. EntryRenderer.feat.mergeAbilityIncrease(data);
  3830.  
  3831. const renderStack = [];
  3832. renderer.recursiveEntryRender({entries: data.entries}, renderStack, 2);
  3833. const rendered = renderStack.join("");
  3834.  
  3835. const r20json = {
  3836. "name": data.name,
  3837. "content": `${prerequisite ? `**Prerequisite**: ${prerequisite}\n\n` : ""}${$(rendered).text()}`,
  3838. "Vetoolscontent": d20plus.importer.getCleanText(rendered),
  3839. "htmlcontent": "",
  3840. "data": {
  3841. "Category": "Feats"
  3842. }
  3843. };
  3844. const gmNotes = JSON.stringify(r20json);
  3845.  
  3846. const baseNoteContents = `${prerequisite ? `<p><i>Prerequisite: ${prerequisite}.</i></p> ` : ""}${rendered}`;
  3847. const noteContents = `${baseNoteContents}<del>${gmNotes}</del>`;
  3848.  
  3849. return [noteContents, gmNotes];
  3850. };
  3851.  
  3852. // Import Object button was clicked
  3853. d20plus.objects.button = function () {
  3854. const url = $("#import-objects-url").val();
  3855. if (url && url.trim()) {
  3856. DataUtil.loadJSON(url, (data) => {
  3857. d20plus.importer.addMeta(data._meta);
  3858. d20plus.importer.showImportList(
  3859. "object",
  3860. data.object,
  3861. d20plus.objects.handoutBuilder
  3862. );
  3863. });
  3864. }
  3865. };
  3866.  
  3867. d20plus.objects.handoutBuilder = function (data, overwrite, inJournals, folderName, saveIdsTo) {
  3868. // make dir
  3869. const folder = d20plus.importer.makeDirTree(`Objects`, folderName);
  3870. const path = ["Objects", folderName, data.name];
  3871.  
  3872. // handle duplicates/overwrites
  3873. if (!d20plus.importer._checkHandleDuplicate(path, overwrite)) return;
  3874.  
  3875. const name = data.name;
  3876. d20.Campaign.characters.create(
  3877. {
  3878. name: name,
  3879. tags: d20plus.importer.getTagString([
  3880. Parser.sizeAbvToFull(data.size),
  3881. Parser.sourceJsonToFull(data.source)
  3882. ], "objects")
  3883. },
  3884. {
  3885. success: function (character) {
  3886. if (saveIdsTo) saveIdsTo[UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_OBJECTS](data)] = {name: data.name, source: data.source, type: "character", roll20Id: character.id};
  3887.  
  3888. try {
  3889. const avatar = data.tokenURL || `${IMG_URL}objects/${name}.png`;
  3890. character.size = data.size;
  3891. character.name = name;
  3892. character.senses = data.senses;
  3893. character.hp = data.hp;
  3894. $.ajax({
  3895. url: avatar,
  3896. type: 'HEAD',
  3897. error: function () {
  3898. d20plus.importer.getSetAvatarImage(character, `${IMG_URL}blank.png`);
  3899. },
  3900. success: function () {
  3901. d20plus.importer.getSetAvatarImage(character, avatar);
  3902. }
  3903. });
  3904. const ac = data.ac.match(/^\d+/);
  3905. const size = Parser.sizeAbvToFull(data.size);
  3906. character.attribs.create({name: "npc", current: 1});
  3907. character.attribs.create({name: "npc_toggle", current: 1});
  3908. character.attribs.create({name: "npc_options-flag", current: 0});
  3909. character.attribs.create({name: "wtype", current: d20plus.importer.getDesiredWhisperType()});
  3910. character.attribs.create({name: "rtype", current: d20plus.importer.getDesiredRollType()});
  3911. character.attribs.create({
  3912. name: "advantagetoggle",
  3913. current: d20plus.importer.getDesiredAdvantageToggle()
  3914. });
  3915. character.attribs.create({
  3916. name: "whispertoggle",
  3917. current: d20plus.importer.getDesiredWhisperToggle()
  3918. });
  3919. character.attribs.create({name: "dtype", current: d20plus.importer.getDesiredDamageType()});
  3920. character.attribs.create({name: "npc_name", current: name});
  3921. character.attribs.create({name: "npc_size", current: size});
  3922. character.attribs.create({name: "type", current: data.type});
  3923. character.attribs.create({name: "npc_type", current: `${size} ${data.type}`});
  3924. character.attribs.create({name: "npc_ac", current: ac != null ? ac[0] : ""});
  3925. character.attribs.create({name: "npc_actype", current: ""});
  3926. character.attribs.create({name: "npc_hpbase", current: data.hp});
  3927. character.attribs.create({name: "npc_hpformula", current: data.hp ? `${data.hp}d1` : ""});
  3928.  
  3929. character.attribs.create({name: "npc_immunities", current: data.immune ? data.immune : ""});
  3930. character.attribs.create({name: "damage_immunities", current: data.immune ? data.immune : ""});
  3931.  
  3932. //Should only be one entry for objects
  3933. if (data.entries != null) {
  3934. character.attribs.create({name: "repeating_npctrait_0_name", current: name});
  3935. character.attribs.create({name: "repeating_npctrait_0_desc", current: data.entries});
  3936. if (d20plus.getCfgVal("token", "tokenactionsTraits")) {
  3937. character.abilities.create({
  3938. name: "Information: " + name,
  3939. istokenaction: true,
  3940. action: d20plus.actionMacroTrait(0)
  3941. });
  3942. }
  3943. }
  3944.  
  3945. const renderer = new EntryRenderer();
  3946. renderer.setBaseUrl(BASE_SITE_URL);
  3947. if (data.actionEntries) {
  3948. data.actionEntries.forEach((e, i) => {
  3949. const renderStack = [];
  3950. renderer.recursiveEntryRender({entries: e.entries}, renderStack, 2);
  3951. const actionText = d20plus.importer.getCleanText(renderStack.join(""));
  3952. d20plus.importer.addAction(character, e.name, actionText, i);
  3953. });
  3954. }
  3955.  
  3956. character.view._updateSheetValues();
  3957. var dirty = [];
  3958. $.each(d20.journal.customSheets.attrDeps, function (i, v) {
  3959. dirty.push(i);
  3960. });
  3961. d20.journal.notifyWorkersOfAttrChanges(character.view.model.id, dirty, true);
  3962.  
  3963. if (data.entries) {
  3964. const bio = renderer.renderEntry({type: "entries", entries: data.entries});
  3965.  
  3966. setTimeout(() => {
  3967. const fluffAs = d20plus.getCfgVal("import", "importFluffAs") || d20plus.getCfgDefaultVal("import", "importFluffAs");
  3968. let k = fluffAs === "Bio"? "bio" : "gmnotes";
  3969. character.updateBlobs({
  3970. [k]: Markdown.parse(bio)
  3971. });
  3972. character.save({
  3973. [k]: (new Date).getTime()
  3974. });
  3975. }, 500);
  3976. }
  3977. } catch (e) {
  3978. d20plus.log(`Error loading [${name}]`);
  3979. d20plus.addImportError(name);
  3980. console.log(data);
  3981. console.log(e);
  3982. }
  3983. d20.journal.addItemToFolderStructure(character.id, folder.id);
  3984. }
  3985. });
  3986. };
  3987.  
  3988. // version from previous scripts. Might be useless now?
  3989. 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}";
  3990. // from OGL sheet, Jan 2018
  3991. 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}";
  3992.  
  3993. d20plus.importer.getDesiredRollType = function () {
  3994. // rtype
  3995. const toggle = "@{advantagetoggle}";
  3996. const never = "{{normal=1}} {{r2=[[0d20";
  3997. const always = "{{always=1}} {{r2=[[@{d20}";
  3998. 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}}";
  3999. const desired = d20plus.getCfgVal("import", "advantagemode");
  4000. if (desired) {
  4001. switch (desired) {
  4002. case "Toggle (Default Advantage)":
  4003. case "Toggle":
  4004. case "Toggle (Default Disadvantage)":
  4005. return toggle;
  4006. case "Always":
  4007. return always;
  4008. case "Query":
  4009. return query;
  4010. case "Never":
  4011. return never;
  4012. }
  4013. } else {
  4014. return toggle;
  4015. }
  4016. };
  4017.  
  4018. d20plus.importer.getDesiredAdvantageToggle = function () {
  4019. // advantagetoggle
  4020. const advantage = "{{query=1}} {{advantage=1}} {{r2=[[@{d20}";
  4021. const disadvantage = "{{query=1}} {{disadvantage=1}} {{r2=[[@{d20}";
  4022. const desired = d20plus.getCfgVal("import", "advantagemode");
  4023. const neither = "";
  4024. if (desired) {
  4025. switch (desired) {
  4026. case "Toggle (Default Advantage)":
  4027. return advantage;
  4028. case "Toggle (Default Disadvantage)":
  4029. return desired;
  4030. case "Toggle":
  4031. case "Always":
  4032. case "Query":
  4033. case "Never":
  4034. return neither;
  4035. }
  4036. } else {
  4037. return neither;
  4038. }
  4039. };
  4040.  
  4041. d20plus.importer.getDesiredWhisperType = function () {
  4042. // wtype
  4043. const toggle = "@{whispertoggle}";
  4044. const never = " ";
  4045. const always = "/w gm ";
  4046. const query = "?{Whisper?|Public Roll,|Whisper Roll,/w gm }";
  4047. const desired = d20plus.getCfgVal("import", "whispermode");
  4048. if (desired) {
  4049. switch (desired) {
  4050. case "Toggle (Default GM)":
  4051. case "Toggle (Default Public)":
  4052. return toggle;
  4053. case "Always":
  4054. return always;
  4055. case "Query":
  4056. return query;
  4057. case "Never":
  4058. return never;
  4059. }
  4060. } else {
  4061. return toggle;
  4062. }
  4063. };
  4064.  
  4065. d20plus.importer.getDesiredWhisperToggle = function () {
  4066. // whispertoggle
  4067. const gm = "/w gm ";
  4068. const pblic = " ";
  4069. const desired = d20plus.getCfgVal("import", "whispermode");
  4070. if (desired) {
  4071. switch (desired) {
  4072. case "Toggle (Default GM)":
  4073. return gm;
  4074. case "Toggle (Default Public)":
  4075. return pblic;
  4076. case "Always":
  4077. return "";
  4078. case "Query":
  4079. return "";
  4080. case "Never":
  4081. return "";
  4082. }
  4083. } else {
  4084. return gm;
  4085. }
  4086. };
  4087.  
  4088. d20plus.importer.getDesiredDamageType = function () {
  4089. // dtype
  4090. const on = "full";
  4091. const off = "pick";
  4092. const desired = d20plus.getCfgVal("import", "damagemode");
  4093. if (desired) {
  4094. switch (desired) {
  4095. case "Auto Roll":
  4096. return on;
  4097. case "Don't Auto Roll":
  4098. return off;
  4099. }
  4100. } else {
  4101. return on;
  4102. }
  4103. };
  4104.  
  4105. // Import Classes button was clicked
  4106. d20plus.classes.button = function (forcePlayer) {
  4107. const playerMode = forcePlayer || !window.is_gm;
  4108. const url = playerMode ? $("#import-classes-url-player").val() : $("#import-classes-url").val();
  4109. if (url && url.trim()) {
  4110. const handoutBuilder = playerMode ? d20plus.classes.playerImportBuilder : d20plus.classes.handoutBuilder;
  4111.  
  4112. DataUtil.loadJSON(url, (data) => {
  4113. d20plus.importer.addMeta(data._meta);
  4114. d20plus.importer.showImportList(
  4115. "class",
  4116. data.class,
  4117. handoutBuilder,
  4118. {
  4119. forcePlayer
  4120. }
  4121. );
  4122. });
  4123. }
  4124. };
  4125.  
  4126. d20plus.classes.handoutBuilder = function (data, overwrite, inJournals, folderName, saveIdsTo) {
  4127. // make dir
  4128. const folder = d20plus.importer.makeDirTree(`Classes`, folderName);
  4129. const path = ["Classes", folderName, data.name];
  4130.  
  4131. // handle duplicates/overwrites
  4132. if (!d20plus.importer._checkHandleDuplicate(path, overwrite)) return;
  4133.  
  4134. const name = data.name;
  4135. d20.Campaign.handouts.create({
  4136. name: name,
  4137. tags: d20plus.importer.getTagString([
  4138. Parser.sourceJsonToFull(data.source)
  4139. ], "classes")
  4140. }, {
  4141. success: function (handout) {
  4142. if (saveIdsTo) saveIdsTo[UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES](data)] = {name: data.name, source: data.source, type: "handout", roll20Id: handout.id};
  4143.  
  4144. const [noteContents, gmNotes] = d20plus.classes._getHandoutData(data);
  4145.  
  4146. handout.updateBlobs({notes: noteContents, gmnotes: gmNotes});
  4147. handout.save({notes: (new Date).getTime(), inplayerjournals: inJournals});
  4148. d20.journal.addItemToFolderStructure(handout.id, folder.id);
  4149. }
  4150. });
  4151.  
  4152. d20plus.classes._handleSubclasses(data, overwrite, inJournals, folderName);
  4153. };
  4154.  
  4155. d20plus.classes._handleSubclasses = function (data, overwrite, inJournals, outerFolderName, forcePlayer) {
  4156. const playerMode = forcePlayer || !window.is_gm;
  4157. // import subclasses
  4158. if (data.subclasses) {
  4159. const allSubclasses = (data.source && isNonstandardSource(data.source)) || !window.confirm(`${data.name} subclasses: import published only?`);
  4160.  
  4161. const gainFeatureArray = [];
  4162. outer: for (let i = 0; i < 20; i++) {
  4163. const lvlFeatureList = data.classFeatures[i];
  4164. for (let j = 0; j < lvlFeatureList.length; j++) {
  4165. const feature = lvlFeatureList[j];
  4166. if (feature.gainSubclassFeature) {
  4167. gainFeatureArray.push(true);
  4168. continue outer;
  4169. }
  4170. }
  4171. gainFeatureArray.push(false);
  4172. }
  4173.  
  4174. data.subclasses.forEach(sc => {
  4175. if (!allSubclasses && isNonstandardSource(sc.source)) return;
  4176.  
  4177. sc.class = data.name;
  4178. sc._gainAtLevels = gainFeatureArray;
  4179. if (playerMode) {
  4180. d20plus.subclasses.playerImportBuilder(sc);
  4181. } else {
  4182. const folderName = d20plus.importer._getHandoutPath("subclass", sc, "Class");
  4183. const path = [folderName];
  4184. if (outerFolderName) path.push(sc.source || data.source); // if it wasn't None, group by source
  4185. d20plus.subclasses.handoutBuilder(sc, overwrite, inJournals, path);
  4186. }
  4187. });
  4188. }
  4189. };
  4190.  
  4191. d20plus.classes.playerImportBuilder = function (data) {
  4192. const [notecontents, gmnotes] = d20plus.classes._getHandoutData(data);
  4193.  
  4194. const importId = d20plus.generateRowId();
  4195. d20plus.importer.storePlayerImport(importId, JSON.parse(gmnotes));
  4196. d20plus.importer.makePlayerDraggable(importId, data.name);
  4197.  
  4198. d20plus.classes._handleSubclasses(data, false, false, null, true);
  4199. };
  4200.  
  4201. d20plus.classes._getHandoutData = function (data) {
  4202. const renderer = new EntryRenderer();
  4203. renderer.setBaseUrl(BASE_SITE_URL);
  4204.  
  4205. const renderStack = [];
  4206. // make a copy of the data to modify
  4207. const curClass = JSON.parse(JSON.stringify(data));
  4208. // render the class text
  4209. for (let i = 0; i < 20; i++) {
  4210. const lvlFeatureList = curClass.classFeatures[i];
  4211. for (let j = 0; j < lvlFeatureList.length; j++) {
  4212. const feature = lvlFeatureList[j];
  4213. renderer.recursiveEntryRender(feature, renderStack);
  4214. }
  4215. }
  4216. const rendered = renderStack.join("");
  4217.  
  4218. const r20json = {
  4219. "name": data.name,
  4220. "Vetoolscontent": data,
  4221. "data": {
  4222. "Category": "Classes"
  4223. }
  4224. };
  4225. const gmNotes = JSON.stringify(r20json);
  4226. const noteContents = `${rendered}\n\n<del>${gmNotes}</del>`;
  4227.  
  4228. return [noteContents, gmNotes];
  4229. };
  4230.  
  4231. d20plus.subclasses._groupOptions = ["Class", "Alphabetical", "Source"];
  4232. d20plus.subclasses._listCols = ["name", "class", "source"];
  4233. d20plus.subclasses._listItemBuilder = (it) => `
  4234. <span class="name col-6">${it.name}</span>
  4235. <span class="class col-4">CLS[${it.class}]</span>
  4236. <span title="${Parser.sourceJsonToFull(it.source)}" class="source col-2">SRC[${Parser.sourceJsonToAbv(it.source)}]</span>`;
  4237. d20plus.subclasses._listIndexConverter = (sc) => {
  4238. return {
  4239. name: sc.name.toLowerCase(),
  4240. class: sc.class.toLowerCase(),
  4241. source: Parser.sourceJsonToAbv(sc.source).toLowerCase()
  4242. };
  4243. };
  4244. // Import Subclasses button was clicked
  4245. d20plus.subclasses.button = function (forcePlayer) {
  4246. const playerMode = forcePlayer || !window.is_gm;
  4247. const url = playerMode ? $("#import-subclasses-url-player").val() : $("#import-subclasses-url").val();
  4248. if (url && url.trim()) {
  4249. const handoutBuilder = playerMode ? d20plus.subclasses.playerImportBuilder : d20plus.subclasses.handoutBuilder;
  4250.  
  4251. DataUtil.loadJSON(url, (data) => {
  4252. d20plus.importer.addMeta(data._meta);
  4253. d20plus.importer.showImportList(
  4254. "subclass",
  4255. data.subclass,
  4256. handoutBuilder,
  4257. {
  4258. groupOptions: d20plus.subclasses._groupOptions,
  4259. forcePlayer,
  4260. listItemBuilder: d20plus.subclasses._listItemBuilder,
  4261. listIndex: d20plus.subclasses._listCols,
  4262. listIndexConverter: d20plus.subclasses._listIndexConverter
  4263. }
  4264. );
  4265. });
  4266. }
  4267. };
  4268.  
  4269. d20plus.subclasses.handoutBuilder = function (data, overwrite, inJournals, folderName, saveIdsTo) {
  4270. // make dir
  4271. const folder = d20plus.importer.makeDirTree(`Subclasses`, folderName);
  4272. const path = ["Sublasses", folderName, data.name];
  4273.  
  4274. // handle duplicates/overwrites
  4275. if (!d20plus.importer._checkHandleDuplicate(path, overwrite)) return;
  4276.  
  4277. const name = `${data.shortName} (${data.class})`;
  4278. d20.Campaign.handouts.create({
  4279. name: name,
  4280. tags: d20plus.importer.getTagString([
  4281. data.class,
  4282. Parser.sourceJsonToFull(data.source)
  4283. ], "subclasses")
  4284. }, {
  4285. success: function (handout) {
  4286. if (saveIdsTo) saveIdsTo[UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES](data)] = {name: data.name, source: data.source, type: "handout", roll20Id: handout.id};
  4287.  
  4288. const [noteContents, gmNotes] = d20plus.subclasses._getHandoutData(data);
  4289.  
  4290. handout.updateBlobs({notes: noteContents, gmnotes: gmNotes});
  4291. handout.save({notes: (new Date).getTime(), inplayerjournals: inJournals});
  4292. d20.journal.addItemToFolderStructure(handout.id, folder.id);
  4293. }
  4294. });
  4295. };
  4296.  
  4297. d20plus.subclasses.playerImportBuilder = function (data) {
  4298. const [notecontents, gmnotes] = d20plus.subclasses._getHandoutData(data);
  4299.  
  4300. const importId = d20plus.generateRowId();
  4301. d20plus.importer.storePlayerImport(importId, JSON.parse(gmnotes));
  4302. const name = `${data.class ? `${data.class} \u2014 ` : ""}${data.name}`;
  4303. d20plus.importer.makePlayerDraggable(importId, name);
  4304. };
  4305.  
  4306. d20plus.subclasses._getHandoutData = function (data) {
  4307. const renderer = new EntryRenderer();
  4308. renderer.setBaseUrl(BASE_SITE_URL);
  4309.  
  4310. const renderStack = [];
  4311.  
  4312. data.subclassFeatures.forEach(lvl => {
  4313. lvl.forEach(f => {
  4314. renderer.recursiveEntryRender(f, renderStack);
  4315. });
  4316. });
  4317.  
  4318. const rendered = renderStack.join("");
  4319.  
  4320. const r20json = {
  4321. "name": data.name,
  4322. "Vetoolscontent": data,
  4323. "data": {
  4324. "Category": "Subclasses"
  4325. }
  4326. };
  4327. const gmNotes = JSON.stringify(r20json);
  4328. const noteContents = `${rendered}\n\n<del>${gmNotes}</del>`;
  4329.  
  4330. return [noteContents, gmNotes];
  4331. };
  4332.  
  4333. d20plus.backgrounds.button = function (forcePlayer) {
  4334. const playerMode = forcePlayer || !window.is_gm;
  4335. const url = playerMode ? $("#import-backgrounds-url-player").val() : $("#import-backgrounds-url").val();
  4336. if (url && url.trim()) {
  4337. const handoutBuilder = playerMode ? d20plus.backgrounds.playerImportBuilder : d20plus.backgrounds.handoutBuilder;
  4338.  
  4339. DataUtil.loadJSON(url, (data) => {
  4340. d20plus.importer.addMeta(data._meta);
  4341. d20plus.importer.showImportList(
  4342. "background",
  4343. data.background,
  4344. handoutBuilder,
  4345. {
  4346. forcePlayer
  4347. }
  4348. );
  4349. });
  4350. }
  4351. };
  4352.  
  4353. d20plus.backgrounds.handoutBuilder = function (data, overwrite, inJournals, folderName, saveIdsTo) {
  4354. // make dir
  4355. const folder = d20plus.importer.makeDirTree(`Backgrounds`, folderName);
  4356. const path = ["Backgrounds", folderName, data.name];
  4357.  
  4358. // handle duplicates/overwrites
  4359. if (!d20plus.importer._checkHandleDuplicate(path, overwrite)) return;
  4360.  
  4361. const name = data.name;
  4362. d20.Campaign.handouts.create({
  4363. name: name,
  4364. tags: d20plus.importer.getTagString([
  4365. Parser.sourceJsonToFull(data.source)
  4366. ], "backgrounds")
  4367. }, {
  4368. success: function (handout) {
  4369. if (saveIdsTo) saveIdsTo[UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_BACKGROUNDS](data)] = {name: data.name, source: data.source, type: "handout", roll20Id: handout.id};
  4370.  
  4371. const [noteContents, gmNotes] = d20plus.backgrounds._getHandoutData(data);
  4372.  
  4373. handout.updateBlobs({notes: noteContents, gmnotes: gmNotes});
  4374. handout.save({notes: (new Date).getTime(), inplayerjournals: inJournals});
  4375. d20.journal.addItemToFolderStructure(handout.id, folder.id);
  4376. }
  4377. });
  4378. };
  4379.  
  4380. d20plus.backgrounds.playerImportBuilder = function (data) {
  4381. const [notecontents, gmnotes] = d20plus.backgrounds._getHandoutData(data);
  4382.  
  4383. const importId = d20plus.generateRowId();
  4384. d20plus.importer.storePlayerImport(importId, JSON.parse(gmnotes));
  4385. d20plus.importer.makePlayerDraggable(importId, data.name);
  4386. };
  4387.  
  4388. d20plus.backgrounds._getHandoutData = function (data) {
  4389. const renderer = new EntryRenderer();
  4390. renderer.setBaseUrl(BASE_SITE_URL);
  4391.  
  4392. const renderStack = [];
  4393.  
  4394. renderer.recursiveEntryRender({entries: data.entries}, renderStack, 1);
  4395.  
  4396. const rendered = renderStack.join("");
  4397.  
  4398. const r20json = {
  4399. "name": data.name,
  4400. "Vetoolscontent": data,
  4401. "data": {
  4402. "Category": "Backgrounds"
  4403. }
  4404. };
  4405. const gmNotes = JSON.stringify(r20json);
  4406. const noteContents = `${rendered}\n\n<del>${gmNotes}</del>`;
  4407.  
  4408. return [noteContents, gmNotes];
  4409. };
  4410.  
  4411. // Import Adventures button was clicked
  4412. d20plus.adventures.button = function () {
  4413. const url = $("#import-adventures-url").val();
  4414. if (url !== null) d20plus.adventures.load(url);
  4415. };
  4416.  
  4417. d20plus.importer.importModeSwitch = function () {
  4418. d20plus.importer.clearPlayerImport();
  4419. const $winPlayer = $(`#d20plus-playerimport`).find(`.append-list-journal`).empty();
  4420.  
  4421. $(`.importer-section`).hide();
  4422. const toShow = $(`#import-mode-select`).val();
  4423. $(`#betteR20-settings`).find(`.importer-section[data-import-group="${toShow}"]`).show();
  4424. const toShowPlayer = $(`#import-mode-select-player`).val();
  4425. $(`#d20plus-playerimport`).find(`.importer-section[data-import-group="${toShowPlayer}"]`).show();
  4426. };
  4427.  
  4428. d20plus.importer.showImportList = function (dataType, dataArray, handoutBuilder, options) {
  4429. if (!options) options = {};
  4430. /*
  4431. options = {
  4432. groupOptions: ["Source", "CR", "Alphabetical", "Type"],
  4433. forcePlayer: true,
  4434. callback: () => console.log("hello world"),
  4435. saveIdsTo: {}, // object to receive IDs of created handouts/creatures
  4436. // these three generally used together
  4437. 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>`,
  4438. listIndex: ["name", "source"],
  4439. listIndexConverter: (mon) => {
  4440. name: mon.name.toLowerCase(),
  4441. source: Parser.sourceJsonToAbv(m.source).toLowerCase() // everything is assumed to be lowercase
  4442. };
  4443. }
  4444. */
  4445. $("a.ui-tabs-anchor[href='#journal']").trigger("click");
  4446.  
  4447. if (!window.is_gm || options.forcePlayer) {
  4448. d20plus.importer.clearPlayerImport();
  4449. const $winPlayer = $(`#d20plus-playerimport`);
  4450. const $appPlayer = $winPlayer.find(`.append-list-journal`);
  4451. $appPlayer.empty();
  4452. $appPlayer.append(`<ol class="dd-list Vetools-player-imported" style="max-width: 95%;"/>`);
  4453. }
  4454.  
  4455. // sort data
  4456. dataArray.sort((a, b) => SortUtil.ascSort(a.name, b.name));
  4457.  
  4458. // collect available properties
  4459. const propSet = {}; // represent this as an object instead of a set, to maintain some semblance of ordering
  4460. dataArray.map(it => Object.keys(it)).forEach(keys => keys.forEach(k => propSet[k] = true));
  4461.  
  4462. // build checkbox list
  4463. const $list = $("#import-list .list");
  4464. $list.html("");
  4465. dataArray.forEach((it, i) => {
  4466. if (it.noDisplay) return;
  4467.  
  4468. const inner = options.listItemBuilder
  4469. ? options.listItemBuilder(it)
  4470. : `<span class="name col-10">${it.name}</span><span class="source" title="${Parser.sourceJsonToFull(it.source)}">${Parser.sourceJsonToAbv(it.source)}</span>`;
  4471.  
  4472. $list.append(`
  4473. <label class="import-cb-label" data-listid="${i}">
  4474. <input type="checkbox">
  4475. ${inner}
  4476. </label>
  4477. `);
  4478. });
  4479.  
  4480. // init list library
  4481. const importList = new List("import-list", {
  4482. valueNames: options.listIndex || ["name"]
  4483. });
  4484.  
  4485. // reset the UI and add handlers
  4486. $(`#import-list > .search`).val("");
  4487. importList.search("");
  4488. $("#import-options label").hide();
  4489. $("#import-overwrite").parent().show();
  4490. $("#import-showplayers").parent().show();
  4491. $("#organize-by").parent().show();
  4492. $("#d20plus-importlist").dialog("open");
  4493.  
  4494. $("#d20plus-importlist button").unbind("click");
  4495.  
  4496. $("#importlist-selectall").bind("click", () => {
  4497. d20plus.importer._importSelectAll(importList);
  4498. });
  4499. $("#importlist-deselectall").bind("click", () => {
  4500. d20plus.importer._importDeselectAll(importList);
  4501. });
  4502. $("#importlist-selectvis").bind("click", () => {
  4503. d20plus.importer._importSelectVisible(importList);
  4504. });
  4505. $("#importlist-deselectvis").bind("click", () => {
  4506. d20plus.importer._importDeselectVisible(importList);
  4507. });
  4508.  
  4509. $("#importlist-selectall-published").bind("click", () => {
  4510. d20plus.importer._importSelectPublished(importList);
  4511. });
  4512.  
  4513. if (options.listIndexConverter) {
  4514. const $iptFilter = $(`#import-list-filter`).show();
  4515. $(`#import-list-filter-help`).show();
  4516. $iptFilter.off("keydown").off("keyup");
  4517. d20plus.importer.addListFilter($iptFilter, dataArray, importList, options.listIndexConverter);
  4518. } else {
  4519. $(`#import-list-filter`).hide();
  4520. $(`#import-list-filter-help`).hide();
  4521. }
  4522.  
  4523. const excludedProps = new Set();
  4524. const $winProps = $("#d20plus-import-props");
  4525. $winProps.find(`button`).bind("click", () => {
  4526. excludedProps.clear();
  4527. $winProps.find(`.prop-row`).each((i, ele) => {
  4528. if (!$(ele).find(`input`).prop("checked")) excludedProps.add($(ele).find(`span`).text());
  4529. });
  4530. });
  4531. const $btnProps = $(`#save-import-props`);
  4532. $btnProps.bind("click", () => {
  4533. $winProps.dialog("close");
  4534. });
  4535. const $props = $winProps.find(`.select-props`);
  4536. $props.empty();
  4537. $(`#import-open-props`).bind("click", () => {
  4538. Object.keys(propSet).forEach(p => {
  4539. const req = REQUIRED_PROPS[dataType] && REQUIRED_PROPS[dataType].includes(p);
  4540. $props.append(`
  4541. <label style="display: block; ${req ? "color: red;" : ""}" class="prop-row">
  4542. <input type="checkbox" checked="true">
  4543. <span>${p}</span>
  4544. </label>
  4545. `)
  4546. });
  4547. $winProps.dialog("open");
  4548. });
  4549.  
  4550. const $selGroupBy = $(`#organize-by`);
  4551. $selGroupBy.html("");
  4552. options.groupOptions = (options.groupOptions || ["Alphabetical", "Source"]).concat(["None"]);
  4553. options.groupOptions.forEach(g => {
  4554. $selGroupBy.append(`<option value="${g}">${g}</option>`);
  4555. });
  4556.  
  4557. $("#d20plus-importlist button#importstart").bind("click", function () {
  4558. $("#d20plus-importlist").dialog("close");
  4559. const overwrite = $("#import-overwrite").prop("checked");
  4560. const inJournals = $("#import-showplayers").prop("checked") ? "all" : "";
  4561. const groupBy = $(`#organize-by`).val();
  4562.  
  4563. // build list of items to process
  4564. const importQueue = [];
  4565. importList.items.forEach((e) => {
  4566. if ($(e.elm).find("input").prop("checked")) {
  4567. const dataIndex = parseInt($(e.elm).data("listid"));
  4568. const it = dataArray[dataIndex];
  4569. importQueue.push(it);
  4570. }
  4571. });
  4572.  
  4573. const $stsName = $("#import-name");
  4574. const $stsRemain = $("#import-remaining");
  4575. let remaining = importQueue.length;
  4576. let interval;
  4577. if (dataType === "monster" || dataType === "object") {
  4578. interval = d20plus.getCfgVal("import", "importIntervalCharacter") || d20plus.getCfgDefaultVal("import", "importIntervalCharacter");
  4579. } else {
  4580. interval = d20plus.getCfgVal("import", "importIntervalHandout") || d20plus.getCfgDefaultVal("import", "importIntervalHandout");
  4581. }
  4582.  
  4583. let cancelWorker = false;
  4584. const $btnCancel = $(`#importcancel`);
  4585. $btnCancel.off("click");
  4586. $btnCancel.on("click", () => {
  4587. handleWorkerComplete();
  4588. cancelWorker = true;
  4589. });
  4590.  
  4591. // start worker to process list
  4592. $("#d20plus-import").dialog("open");
  4593.  
  4594. // run one immediately
  4595. let worker;
  4596. workerFn();
  4597. worker = setInterval(() => {
  4598. workerFn();
  4599. }, interval);
  4600.  
  4601. function workerFn () {
  4602. if (!importQueue.length) {
  4603. handleWorkerComplete();
  4604. return;
  4605. }
  4606. if (cancelWorker) {
  4607. return;
  4608. }
  4609.  
  4610. // pull items out the queue in LIFO order, for journal ordering (last created will be at the top)
  4611. let it = importQueue.pop();
  4612. it.name = it.name || "(Unknown)";
  4613.  
  4614. $stsName.text(it.name);
  4615. $stsRemain.text(remaining--);
  4616.  
  4617. if (excludedProps.size) {
  4618. it = JSON.parse(JSON.stringify(it));
  4619. [...excludedProps].forEach(k => delete it[k]);
  4620. }
  4621.  
  4622. if (!window.is_gm || options.forcePlayer) {
  4623. handoutBuilder(it);
  4624. } else {
  4625. const folderName = groupBy === "None" ? "" : d20plus.importer._getHandoutPath(dataType, it, groupBy);
  4626. const builderOptions = {};
  4627. if (dataType === "spell" && groupBy === "Spell Points") builderOptions.isSpellPoints = true;
  4628. handoutBuilder(it, overwrite, inJournals, folderName, options.saveIdsTo, builderOptions);
  4629. }
  4630. }
  4631.  
  4632. function handleWorkerComplete () {
  4633. if (worker) clearInterval(worker);
  4634. if (cancelWorker) {
  4635. $stsName.text("Import cancelled");
  4636. if (~$stsRemain.text().indexOf("(cancelled)")) $stsRemain.text(`${$stsRemain.text()} (cancelled)`);
  4637. d20plus.log(`Import cancelled`);
  4638. setTimeout(() => {
  4639. d20plus.bindDropLocations();
  4640. }, 250);
  4641. } else {
  4642. $stsName.text("Import complete");
  4643. $stsRemain.text("0");
  4644. d20plus.log(`Import complete`);
  4645. setTimeout(() => {
  4646. d20plus.bindDropLocations();
  4647. }, 250);
  4648. if (options.callback) options.callback();
  4649. }
  4650. }
  4651. });
  4652. };
  4653.  
  4654. d20plus.spells.spLevelToSpellPoints = function (level) {
  4655. switch (level) {
  4656. case 1:
  4657. return 2;
  4658. case 2:
  4659. return 3;
  4660. case 3:
  4661. return 5;
  4662. case 4:
  4663. return 6;
  4664. case 5:
  4665. return 7;
  4666. case 6:
  4667. return 8;
  4668. case 7:
  4669. return 10;
  4670. case 8:
  4671. return 11;
  4672. case 9:
  4673. return 13;
  4674. case 0:
  4675. default:
  4676. return 0;
  4677. }
  4678. };
  4679.  
  4680. d20plus.importer._getHandoutPath = function (dataType, it, groupBy) {
  4681. switch (dataType) {
  4682. case "monster": {
  4683. let folderName;
  4684. switch (groupBy) {
  4685. case "Source":
  4686. folderName = Parser.sourceJsonToFull(it.source);
  4687. break;
  4688. case "CR":
  4689. folderName = it.cr ? (it.cr.cr || it.cr) : "Unknown";
  4690. break;
  4691. case "Alphabetical":
  4692. folderName = it.name[0].uppercaseFirst();
  4693. break;
  4694. case "Type (with tags)":
  4695. folderName = Parser.monTypeToFullObj(it.type).asText.uppercaseFirst();
  4696. break;
  4697. case "Type":
  4698. default:
  4699. folderName = Parser.monTypeToFullObj(it.type).type.uppercaseFirst();
  4700. break;
  4701. }
  4702. return folderName;
  4703. }
  4704. case "spell": {
  4705. let folderName;
  4706. switch (groupBy) {
  4707. case "Source":
  4708. folderName = Parser.sourceJsonToFull(it.source);
  4709. break;
  4710. case "Alphabetical":
  4711. folderName = it.name[0].uppercaseFirst();
  4712. break;
  4713. case "Spell Points":
  4714. folderName = `${d20plus.spells.spLevelToSpellPoints(it.level)} spell points`;
  4715. break;
  4716. case "Level":
  4717. default:
  4718. folderName = `${Parser.spLevelToFull(it.level)}${it.level ? " level" : ""}`;
  4719. break;
  4720. }
  4721. return folderName;
  4722. }
  4723. case "item": {
  4724. let folderName;
  4725. switch (groupBy) {
  4726. case "Source":
  4727. folderName = Parser.sourceJsonToFull(it.source);
  4728. break;
  4729. case "Rarity":
  4730. folderName = it.rarity;
  4731. break;
  4732. case "Alphabetical":
  4733. folderName = it.name[0].uppercaseFirst();
  4734. break;
  4735. case "Type":
  4736. default:
  4737. if (it.type) {
  4738. folderName = Parser.itemTypeToAbv(it.type);
  4739. } else if (it.typeText) {
  4740. folderName = it.typeText;
  4741. } else {
  4742. folderName = "Unknown";
  4743. }
  4744. break;
  4745. }
  4746. return folderName;
  4747. }
  4748. case "psionic": {
  4749. let folderName;
  4750. switch (groupBy) {
  4751. case "Source":
  4752. folderName = Parser.sourceJsonToFull(it.source);
  4753. break;
  4754. case "Order":
  4755. folderName = Parser.psiOrderToFull(it.order);
  4756. break;
  4757. case "Alphabetical":
  4758. default:
  4759. folderName = it.name[0].uppercaseFirst();
  4760. break;
  4761. }
  4762. return folderName;
  4763. }
  4764. case "feat": {
  4765. let folderName;
  4766. switch (groupBy) {
  4767. case "Source":
  4768. folderName = Parser.sourceJsonToFull(it.source);
  4769. break;
  4770. case "Alphabetical":
  4771. default:
  4772. folderName = it.name[0].uppercaseFirst();
  4773. break;
  4774. }
  4775. return folderName;
  4776. }
  4777. case "object": {
  4778. let folderName;
  4779. switch (groupBy) {
  4780. case "Source":
  4781. folderName = Parser.sourceJsonToFull(it.source);
  4782. break;
  4783. case "Alphabetical":
  4784. default:
  4785. folderName = it.name[0].uppercaseFirst();
  4786. break;
  4787. }
  4788. return folderName;
  4789. }
  4790. case "class": {
  4791. let folderName;
  4792. switch (groupBy) {
  4793. case "Source":
  4794. folderName = Parser.sourceJsonToFull(it.source);
  4795. break;
  4796. case "Alphabetical":
  4797. default:
  4798. folderName = it.name[0].uppercaseFirst();
  4799. break;
  4800. }
  4801. return folderName;
  4802. }
  4803. case "subclass": {
  4804. let folderName;
  4805. switch (groupBy) {
  4806. case "Source":
  4807. folderName = Parser.sourceJsonToFull(it.source);
  4808. break;
  4809. case "Alphabetical":
  4810. folderName = it.name[0].uppercaseFirst();
  4811. break;
  4812. case "Class":
  4813. default:
  4814. folderName = it.class;
  4815. }
  4816. return folderName;
  4817. }
  4818. case "background": {
  4819. let folderName;
  4820. switch (groupBy) {
  4821. case "Source":
  4822. folderName = Parser.sourceJsonToFull(it.source);
  4823. break;
  4824. case "Alphabetical":
  4825. default:
  4826. folderName = it.name[0].uppercaseFirst();
  4827. break;
  4828. }
  4829. return folderName;
  4830. }
  4831. case "race": {
  4832. let folderName;
  4833. switch (groupBy) {
  4834. case "Source":
  4835. folderName = Parser.sourceJsonToFull(it.source);
  4836. break;
  4837. case "Alphabetical":
  4838. default:
  4839. folderName = it.name[0].uppercaseFirst();
  4840. break;
  4841. }
  4842. return folderName;
  4843. }
  4844. default:
  4845. throw new Error(`Unknown import type '${dataType}'`);
  4846. }
  4847. };
  4848.  
  4849. d20plus.importer._checkHandleDuplicate = function (path, overwrite) {
  4850. const dupe = d20plus.importer.checkFileExistsByPath(path);
  4851. if (dupe && !overwrite) return false;
  4852. else if (dupe) d20plus.importer.removeFileByPath(path);
  4853. return true;
  4854. };
  4855.  
  4856. d20plus.importer._importToggleSelectAll = function (importList, selectAllCb) {
  4857. const $sa = $(selectAllCb);
  4858. importList.items.forEach(i => Array.prototype.forEach.call(i.elm.children, (e) => {
  4859. if (e.tagName === "INPUT") {
  4860. $(e).prop("checked", $sa.prop("checked"));
  4861. }
  4862. }));
  4863. };
  4864.  
  4865. d20plus.importer._importSelectAll = function (importList) {
  4866. importList.items.forEach(i => Array.prototype.forEach.call(i.elm.children, (e) => {
  4867. if (e.tagName === "INPUT") {
  4868. $(e).prop("checked", true);
  4869. }
  4870. }));
  4871. };
  4872.  
  4873. d20plus.importer._importSelectVisible = function (importList) {
  4874. importList.visibleItems.forEach(i => Array.prototype.forEach.call(i.elm.children, (e) => {
  4875. if (e.tagName === "INPUT") {
  4876. $(e).prop("checked", true);
  4877. }
  4878. }));
  4879. };
  4880.  
  4881. d20plus.importer._importDeselectAll = function (importList) {
  4882. importList.items.forEach(i => Array.prototype.forEach.call(i.elm.children, (e) => {
  4883. if (e.tagName === "INPUT") {
  4884. $(e).prop("checked", false);
  4885. }
  4886. }));
  4887. };
  4888.  
  4889. d20plus.importer._importDeselectVisible = function (importList) {
  4890. importList.visibleItems.forEach(i => Array.prototype.forEach.call(i.elm.children, (e) => {
  4891. if (e.tagName === "INPUT") {
  4892. $(e).prop("checked", false);
  4893. }
  4894. }));
  4895. };
  4896.  
  4897. d20plus.importer._importSelectPublished = function (importList) {
  4898. function setSelection (i, setTo) {
  4899. Array.prototype.forEach.call(i.elm.children, (e) => {
  4900. if (e.tagName === "INPUT") {
  4901. $(e).prop("checked", setTo);
  4902. }
  4903. })
  4904. }
  4905.  
  4906. importList.items.forEach(i => {
  4907. if (isNonstandardSource(i.values().source)) {
  4908. setSelection(i, false);
  4909. } else {
  4910. setSelection(i, true);
  4911. }
  4912.  
  4913. });
  4914. };
  4915.  
  4916. // Fetch adventure data from file
  4917. d20plus.adventures.load = function (url) {
  4918. $("a.ui-tabs-anchor[href='#journal']").trigger("click");
  4919. $.ajax({
  4920. type: "GET",
  4921. url: url,
  4922. dataType: "text",
  4923. success: function (data) {
  4924. data = JSON.parse(data);
  4925.  
  4926. function isPart (e) {
  4927. return typeof e === "string" || typeof e === "object" && (e.type !== "entries");
  4928. }
  4929.  
  4930. // open progress window
  4931. $("#d20plus-import").dialog("open");
  4932. $("#import-remaining").text("Initialising...");
  4933.  
  4934. // get metadata
  4935. const adMeta = adventureMetadata.adventure.find(a => a.id.toLowerCase() === $("#import-adventures-url").data("id").toLowerCase())
  4936.  
  4937. const addQueue = [];
  4938. const sections = JSON.parse(JSON.stringify(data.data));
  4939. const adDir = `${Parser.sourceJsonToFull(adMeta.id)}`;
  4940. sections.forEach((s, i) => {
  4941. if (i >= adMeta.contents.length) return;
  4942.  
  4943. const chapterDir = [adDir, adMeta.contents[i].name];
  4944.  
  4945. const introEntries = [];
  4946. if (s.entries && s.entries.length && isPart(s.entries[0])) {
  4947. while (isPart(s.entries[0])) {
  4948. introEntries.push(s.entries[0]);
  4949. s.entries.shift();
  4950. }
  4951. }
  4952. addQueue.push({
  4953. dir: chapterDir,
  4954. type: "entries",
  4955. name: s.name,
  4956. entries: introEntries,
  4957. });
  4958.  
  4959. // compact entries into layers
  4960. front = null;
  4961. let tempStack = [];
  4962. let textIndex = 1;
  4963. while ((front = s.entries.shift())) {
  4964. if (isPart(front)) {
  4965. tempStack.push(front);
  4966. } else {
  4967. if (tempStack.length) {
  4968. addQueue.push({
  4969. dir: chapterDir,
  4970. type: "entries",
  4971. name: `Text ${textIndex++}`,
  4972. entries: tempStack
  4973. });
  4974. tempStack = [];
  4975. }
  4976. front.dir = chapterDir;
  4977. addQueue.push(front);
  4978. }
  4979. }
  4980. });
  4981.  
  4982. const renderer = new EntryRenderer();
  4983. renderer.setBaseUrl(BASE_SITE_URL);
  4984.  
  4985. const $stsName = $("#import-name");
  4986. const $stsRemain = $("#import-remaining");
  4987. const interval = d20plus.getCfgVal("import", "importIntervalHandout") || d20plus.getCfgDefaultVal("import", "importIntervalHandout");
  4988.  
  4989. ////////////////////////////////////////////////////////////////////////////////////////////////////////
  4990. EntryRenderer.getDefaultRenderer().setBaseUrl(BASE_SITE_URL);
  4991. // pre-import tags
  4992. const tags = {};
  4993. renderer.doExportTags(tags);
  4994. addQueue.forEach(entry => {
  4995. renderer.recursiveEntryRender(entry, []);
  4996. });
  4997.  
  4998. // storage for returned handout/character IDs
  4999. const RETURNED_IDS = {};
  5000.  
  5001. // monsters
  5002. const preMonsters = Object.keys(tags)
  5003. .filter(k => tags[k].page === "bestiary.html")
  5004. .map(k => tags[k]);
  5005. if (confirm("Import creatures from this adventure?")) doPreImport(preMonsters, showMonsterImport);
  5006. else doItemImport();
  5007.  
  5008. function showMonsterImport (toImport) {
  5009. d20plus.log(`Displaying monster import list for [${adMeta.name}]`);
  5010. d20plus.importer.showImportList(
  5011. "monster",
  5012. toImport.filter(it => it),
  5013. d20plus.monsters.handoutBuilder,
  5014. {
  5015. groupOptions: d20plus.monsters._groupOptions,
  5016. saveIdsTo: RETURNED_IDS,
  5017. callback: doItemImport,
  5018. listItemBuilder: d20plus.monsters._listItemBuilder,
  5019. listIndex: d20plus.monsters._listCols,
  5020. listIndexConverter: d20plus.monsters._listIndexConverter
  5021. }
  5022. );
  5023. }
  5024.  
  5025. // items
  5026. function doItemImport () {
  5027. const preItems = Object.keys(tags)
  5028. .filter(k => tags[k].page === "items.html")
  5029. .map(k => tags[k]);
  5030. if (confirm("Import items from this adventure?")) doPreImport(preItems, showItemImport);
  5031. else doMainImport();
  5032. }
  5033.  
  5034. function showItemImport (toImport) {
  5035. d20plus.log(`Displaying item import list for [${adMeta.name}]`);
  5036. d20plus.importer.showImportList(
  5037. "item",
  5038. toImport.filter(it => it),
  5039. d20plus.items.handoutBuilder,
  5040. {
  5041. groupOptions: d20plus.items._groupOptions,
  5042. saveIdsTo: RETURNED_IDS,
  5043. callback: doMainImport,
  5044. listItemBuilder: d20plus.items._listItemBuilder,
  5045. listIndex: d20plus.items._listCols,
  5046. listIndexConverter: d20plus.items._listIndexConverter
  5047. }
  5048. );
  5049. }
  5050.  
  5051. function doPreImport (asTags, callback) {
  5052. const tmp = [];
  5053. let cachedCount = asTags.length;
  5054. asTags.forEach(it => {
  5055. try {
  5056. EntryRenderer.hover._doFillThenCall(
  5057. it.page,
  5058. it.source,
  5059. it.hash,
  5060. () => {
  5061. tmp.push(EntryRenderer.hover._getFromCache(it.page, it.source, it.hash));
  5062. cachedCount--;
  5063. if (cachedCount <= 0) callback(tmp);
  5064. }
  5065. );
  5066. } catch (x) {
  5067. console.log(x);
  5068. cachedCount--;
  5069. if (cachedCount <= 0) callback(tmp);
  5070. }
  5071. });
  5072. }
  5073. ////////////////////////////////////////////////////////////////////////////////////////////////////////
  5074. function doMainImport () {
  5075. // pass in any created handouts/characters to use for links in the renderer
  5076. renderer.setRoll20Ids(RETURNED_IDS);
  5077.  
  5078. let cancelWorker = false;
  5079. const $btnCancel = $(`#importcancel`);
  5080. $btnCancel.off("click");
  5081. $btnCancel.on("click", () => {
  5082. cancelWorker = true;
  5083. });
  5084.  
  5085. let remaining = addQueue.length;
  5086.  
  5087. d20plus.log(`Running import of [${adMeta.name}] with ${interval} ms delay between each handout create`);
  5088. let lastId = null;
  5089. let lastName = null;
  5090.  
  5091. const worker = setInterval(() => {
  5092. if (!addQueue.length || cancelWorker) {
  5093. clearInterval(worker);
  5094. $stsName.text("DONE!");
  5095. $stsRemain.text("0");
  5096. d20plus.log(`Finished import of [${adMeta.name}]`);
  5097. renderer.resetRoll20Ids();
  5098. return;
  5099. }
  5100.  
  5101. // pull items out the queue in LIFO order, for journal ordering (last created will be at the top)
  5102. const entry = addQueue.pop();
  5103. entry.name = entry.name || "(Unknown)";
  5104. entry.name = d20plus.importer.getCleanText(renderer.renderEntry(entry.name));
  5105. $stsName.text(entry.name);
  5106. $stsRemain.text(remaining--);
  5107. const folder = d20plus.importer.makeDirTree(entry.dir);
  5108.  
  5109. d20.Campaign.handouts.create({
  5110. name: entry.name
  5111. }, {
  5112. success: function (handout) {
  5113. const renderStack = [];
  5114. renderer.recursiveEntryRender(entry, renderStack);
  5115. if (lastId && lastName) renderStack.push(`<br><p>Next handout: <a href="http://journal.roll20.net/handout/${lastId}">${lastName}</a></p>`);
  5116. const rendered = renderStack.join("");
  5117.  
  5118. lastId = handout.id;
  5119. lastName = entry.name;
  5120. handout.updateBlobs({notes: rendered});
  5121. handout.save({notes: (new Date).getTime(), inplayerjournals: ""});
  5122. d20.journal.addItemToFolderStructure(handout.id, folder.id);
  5123. }
  5124. });
  5125. }, interval);
  5126. }
  5127. }
  5128. });
  5129. };
  5130.  
  5131. d20plus.importer.getCleanText = function (str) {
  5132. const check = jQuery.parseHTML(str);
  5133. if (check.length === 1 && check[0].constructor === Text) {
  5134. return str;
  5135. }
  5136. const $ele = $(str);
  5137. $ele.find("p, li, br").append("\n\n");
  5138. return $ele.text().replace(/[ ]+/g, " ");
  5139.  
  5140. /* version which preserves images, and converts dice
  5141. const IMG_TAG = "R20IMGTAG";
  5142. let imgIndex = 0;
  5143. const imgStack = [];
  5144. str.replace(/(<img.*>)/, (match) => {
  5145. imgStack.push(match);
  5146. return ` ${IMG_TAG}_${imgIndex++} `;
  5147. });
  5148. const $ele = $(str);
  5149. $ele.find("p, li, br").append("\n\n");
  5150. let out = $ele.text();
  5151. out = out.replace(DICE_REGEX, (match) => {
  5152. return `[[${match}]]`;
  5153. });
  5154. return out.replace(/R20IMGTAG_(\d+)/, (match, g1) => {
  5155. return imgStack[Number(g1)];
  5156. });
  5157. */
  5158. };
  5159.  
  5160. d20plus.miniInitStyle = `
  5161. #initiativewindow button.initmacrobutton {
  5162. padding: 1px 4px;
  5163. }
  5164.  
  5165. #initiativewindow input {
  5166. font-size: 8px;
  5167. }
  5168.  
  5169. #initiativewindow ul li span.name {
  5170. font-size: 13px;
  5171. padding-top: 0;
  5172. padding-left: 4px;
  5173. margin-top: -3px;
  5174. }
  5175.  
  5176. #initiativewindow ul li img {
  5177. min-height: 15px;
  5178. max-height: 15px;
  5179. }
  5180.  
  5181. #initiativewindow ul li {
  5182. min-height: 15px;
  5183. }
  5184.  
  5185. #initiativewindow div.header span.initiative,
  5186. #initiativewindow ul li span.initiative,
  5187. #initiativewindow ul li span.tracker-col,
  5188. #initiativewindow div.header span.tracker-col,
  5189. #initiativewindow div.header span.initmacro,
  5190. #initiativewindow ul li span.initmacro {
  5191. font-size: 10px;
  5192. font-weight: bold;
  5193. text-align: right;
  5194. float: right;
  5195. padding: 0 5px;
  5196. width: 7%;
  5197. min-height: 20px;
  5198. display: block;
  5199. overflow: hidden;
  5200. }
  5201.  
  5202. #initiativewindow ul li .controls {
  5203. padding: 0 3px;
  5204. }
  5205. `;
  5206.  
  5207. d20plus.setInitiativeShrink = function (doShrink) {
  5208. const customStyle = $(`#dynamicStyle`);
  5209. if (doShrink) {
  5210. customStyle.html(d20plus.miniInitStyle);
  5211. } else {
  5212. customStyle.html("");
  5213. }
  5214. };
  5215.  
  5216. d20plus.difficultyHtml = `<span class="difficulty" style="position: absolute"></span>`;
  5217.  
  5218. d20plus.multipliers = [1, 1.5, 2, 2.5, 3, 4, 5];
  5219.  
  5220. d20plus.playerImportHtml = `<div id="d20plus-playerimport" title="Temporary Import">
  5221. <div class="append-target">
  5222. <!-- populate with js -->
  5223. </div>
  5224. <div class="append-list-journal" style="max-height: 400px; overflow-y: auto;">
  5225. <!-- populate with js -->
  5226. </div>
  5227. <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>
  5228. </div>`;
  5229.  
  5230. d20plus.importListHTML = `<div id="d20plus-importlist" title="Import..." style="width: 1000px;">
  5231. <p style="display: flex">
  5232. <button type="button" id="importlist-selectall" class="btn" style="margin: 0 2px;"><span>Select All</span></button>
  5233. <button type="button" id="importlist-deselectall" class="btn" style="margin: 0 2px;"><span>Deselect All</span></button>
  5234. <button type="button" id="importlist-selectvis" class="btn" style="margin: 0 2px;"><span>Select Visible</span></button>
  5235. <button type="button" id="importlist-deselectvis" class="btn" style="margin: 0 2px;"><span>Deselect Visible</span></button>
  5236. <span style="width:1px;background: #bbb;height: 26px;margin: 2px;"></span>
  5237. <button type="button" id="importlist-selectall-published" class="btn" style="margin: 0 2px;"><span>Select All Published</span></button>
  5238. </p>
  5239. <p>
  5240. <span id="import-list">
  5241. <input class="search" autocomplete="off" placeholder="Search list...">
  5242. <input type="search" id="import-list-filter" class="filter" placeholder="Filter...">
  5243. <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>
  5244. <br>
  5245. <span class="list" style="max-height: 400px; overflow-y: scroll; overflow-x: hidden; display: block; margin-top: 1em; transform: translateZ(0);"></span>
  5246. </span>
  5247. </p>
  5248. <p id="import-options">
  5249. <label style="display: inline-block">Group Handouts By... <select id="organize-by"></select></label>
  5250. <button type="button" id="import-open-props" class="btn" role="button" aria-disabled="false" style="padding: 3px; display: inline-block;">Select Properties</button>
  5251. <label>Make handouts visible to all players? <input type="checkbox" title="Make items visible to all players" id="import-showplayers" checked></label>
  5252. <label>Overwrite existing? <input type="checkbox" title="Overwrite existing" id="import-overwrite"></label>
  5253. </p>
  5254. <button type="button" id="importstart" class="btn" role="button" aria-disabled="false">
  5255. <span>Start Import</span>
  5256. </button>
  5257. </div>`;
  5258.  
  5259. d20plus.importListPropsHTML = `<div id="d20plus-import-props" title="Choose Properties to Import">
  5260. <div class="select-props" style="max-height: 400px; overflow-y: scroll; transform: translateZ(0)">
  5261. <!-- populate with JS -->
  5262. </div>
  5263. <p>
  5264. Warning: this feature is highly experimental, and disabling <span style="color: red;">properties which are assumed to always exist</span> is not recommended.
  5265. <br>
  5266. <button type="button" id="save-import-props" class="btn" role="button" aria-disabled="false">Save</button>
  5267. </p>
  5268. </div>`;
  5269.  
  5270. d20plus.importDialogHtml = `<div id="d20plus-import" title="Importing...">
  5271. <p>
  5272. <h3 id="import-name"></h3>
  5273. </p>
  5274. <span id="import-remaining"></span> remaining
  5275. <p>
  5276. Errors: <span id="import-errors">0</span>
  5277. </p>
  5278. <p>
  5279. <button type="button" id="importcancel" alt="Cancel" title="Cancel Import" class="btn" role="button" aria-disabled="false">
  5280. <span>Cancel</span>
  5281. </button>
  5282. </p>
  5283. </div>`;
  5284.  
  5285. d20plus.settingsHtmlImportHeader = `
  5286. <h4>Import By Category</h4>
  5287. <p><small><i>We strongly recommend the OGL sheet for importing. You can switch afterwards.</i></small></p>
  5288. `;
  5289. d20plus.settingsHtmlSelector = `
  5290. <select id="import-mode-select">
  5291. <option value="none" disabled selected>Select category...</option>
  5292. <option value="monster">Monsters</option>
  5293. <option value="spell">Spells</option>
  5294. <option value="item">Items</option>
  5295. <option value="psionic">Psionics</option>
  5296. <option value="race">Races</option>
  5297. <option value="feat">Feats</option>
  5298. <option value="object">Objects</option>
  5299. <option value="class">Classes</option>
  5300. <option value="subclass">Subclasses</option>
  5301. <option value="background">Backgrounds</option>
  5302. <option value="adventure">Adventures</option>
  5303. </select>
  5304. `;
  5305. d20plus.settingsHtmlSelectorPlayer = `
  5306. <select id="import-mode-select-player">
  5307. <option value="none" disabled selected>Select category...</option>
  5308. <option value="spell">Spells</option>
  5309. <option value="item">Items</option>
  5310. <option value="psionic">Psionics</option>
  5311. <option value="race">Races</option>
  5312. <option value="feat">Feats</option>
  5313. <option value="class">Classes</option>
  5314. <option value="subclass">Subclasses</option>
  5315. <option value="background">Backgrounds</option>
  5316. </select>
  5317. `;
  5318. d20plus.settingsHtmlPtMonsters = `
  5319. <div class="importer-section" data-import-group="monster">
  5320. <h4>Monster Importing</h4>
  5321. <label for="import-monster-url">Monster Data URL:</label>
  5322. <select id="button-monsters-select">
  5323. <!-- populate with JS-->
  5324. </select>
  5325. <input type="text" id="import-monster-url">
  5326. <p><a class="btn" href="#" id="button-monsters-load">Import Monsters</a></p>
  5327. <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>
  5328. <p>
  5329. WARNING: Importing huge numbers of character sheets slows the game down. We recommend you import them as needed.<br>
  5330. The "Import Monsters From All Sources" button presents a list containing monsters from official sources only.<br>
  5331. To import from third-party sources, either individually select one available in the list or enter a custom URL, and "Import Monsters."
  5332. </p>
  5333. </div>
  5334. `;
  5335.  
  5336. d20plus.settingsHtmlPtItems = `
  5337. <div class="importer-section" data-import-group="item">
  5338. <h4>Item Importing</h4>
  5339. <label for="import-items-url">Item Data URL:</label>
  5340. <select id="button-items-select"><!-- populate with JS--></select>
  5341. <input type="text" id="import-items-url">
  5342. <a class="btn" href="#" id="import-items-load">Import Items</a>
  5343. </div>
  5344. `;
  5345.  
  5346. d20plus.settingsHtmlPtItemsPlayer = `
  5347. <div class="importer-section" data-import-group="item">
  5348. <h4>Item Importing</h4>
  5349. <label for="import-items-url-player">Item Data URL:</label>
  5350. <select id="button-items-select-player"><!-- populate with JS--></select>
  5351. <input type="text" id="import-items-url-player">
  5352. <a class="btn" href="#" id="import-items-load-player">Import Items</a>
  5353. </div>
  5354. `;
  5355.  
  5356. d20plus.settingsHtmlPtSpells = `
  5357. <div class="importer-section" data-import-group="spell">
  5358. <h4>Spell Importing</h4>
  5359. <label for="import-spell-url">Spell Data URL:</label>
  5360. <select id="button-spell-select">
  5361. <!-- populate with JS-->
  5362. </select>
  5363. <input type="text" id="import-spell-url">
  5364. <p><a class="btn" href="#" id="button-spells-load">Import Spells</a><p/>
  5365. <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>
  5366. <p>
  5367. The "Import Spells From All Sources" button presents a list containing spells from official sources only.<br>
  5368. To import from third-party sources, either individually select one available in the list or enter a custom URL, and "Import Spells."
  5369. </p>
  5370. </div>
  5371. `;
  5372.  
  5373. d20plus.settingsHtmlPtSpellsPlayer = `
  5374. <div class="importer-section" data-import-group="spell">
  5375. <h4>Spell Importing</h4>
  5376. <label for="import-spell-url-player">Spell Data URL:</label>
  5377. <select id="button-spell-select-player">
  5378. <!-- populate with JS-->
  5379. </select>
  5380. <input type="text" id="import-spell-url-player">
  5381. <p><a class="btn" href="#" id="button-spells-load-player">Import Spells</a><p/>
  5382. <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>
  5383. <p>
  5384. The "Import Spells From All Sources" button presents a list containing spells from official sources only.<br>
  5385. To import from third-party sources, either individually select one available in the list or enter a custom URL, and "Import Spells."
  5386. </p>
  5387. </div>
  5388. `;
  5389.  
  5390. d20plus.settingsHtmlPtPsionics = `
  5391. <div class="importer-section" data-import-group="psionic">
  5392. <h4>Psionic Importing</h4>
  5393. <label for="import-psionics-url">Psionics Data URL:</label>
  5394. <select id="button-psionics-select"><!-- populate with JS--></select>
  5395. <input type="text" id="import-psionics-url">
  5396. <a class="btn" href="#" id="import-psionics-load">Import Psionics</a>
  5397. </div>
  5398. `;
  5399.  
  5400. d20plus.settingsHtmlPtPsionicsPlayer = `
  5401. <div class="importer-section" data-import-group="psionic">
  5402. <h4>Psionic Importing</h4>
  5403. <label for="import-psionics-url-player">Psionics Data URL:</label>
  5404. <select id="button-psionics-select-player"><!-- populate with JS--></select>
  5405. <input type="text" id="import-psionics-url-player">
  5406. <a class="btn" href="#" id="import-psionics-load-player">Import Psionics</a>
  5407. </div>
  5408. `;
  5409.  
  5410. d20plus.settingsHtmlPtFeats = `
  5411. <div class="importer-section" data-import-group="feat">
  5412. <h4>Feat Importing</h4>
  5413. <label for="import-feats-url">Feat Data URL:</label>
  5414. <select id="button-feats-select"><!-- populate with JS--></select>
  5415. <input type="text" id="import-feats-url">
  5416. <a class="btn" href="#" id="import-feats-load">Import Feats</a>
  5417. </div>
  5418. `;
  5419.  
  5420. d20plus.settingsHtmlPtFeatsPlayer = `
  5421. <div class="importer-section" data-import-group="feat">
  5422. <h4>Feat Importing</h4>
  5423. <label for="import-feats-url-player">Feat Data URL:</label>
  5424. <select id="button-feats-select-player"><!-- populate with JS--></select>
  5425. <input type="text" id="import-feats-url-player">
  5426. <a class="btn" href="#" id="import-feats-load-player">Import Feats</a>
  5427. </div>
  5428. `;
  5429.  
  5430. d20plus.settingsHtmlPtObjects = `
  5431. <div class="importer-section" data-import-group="object">
  5432. <h4>Object Importing</h4>
  5433. <label for="import-objects-url">Object Data URL:</label>
  5434. <select id="button-objects-select"><!-- populate with JS--></select>
  5435. <input type="text" id="import-objects-url">
  5436. <a class="btn" href="#" id="import-objects-load">Import Objects</a>
  5437. </div>
  5438. `;
  5439.  
  5440. d20plus.settingsHtmlPtRaces = `
  5441. <div class="importer-section" data-import-group="race">
  5442. <h4>Race Importing</h4>
  5443. <label for="import-races-url">Race Data URL:</label>
  5444. <select id="button-races-select"><!-- populate with JS--></select>
  5445. <input type="text" id="import-races-url">
  5446. <a class="btn" href="#" id="import-races-load">Import Races</a>
  5447. </div>
  5448. `;
  5449.  
  5450. d20plus.settingsHtmlPtRacesPlayer = `
  5451. <div class="importer-section" data-import-group="race">
  5452. <h4>Race Importing</h4>
  5453. <label for="import-races-url-player">Race Data URL:</label>
  5454. <select id="button-races-select-player"><!-- populate with JS--></select>
  5455. <input type="text" id="import-races-url-player">
  5456. <a class="btn" href="#" id="import-races-load-player">Import Races</a>
  5457. </div>
  5458. `;
  5459.  
  5460. d20plus.settingsHtmlPtClasses = `
  5461. <div class="importer-section" data-import-group="class">
  5462. <h4>Class Importing</h4>
  5463. <label for="import-classes-url">Class Data URL:</label>
  5464. <select id="button-classes-select"><!-- populate with JS--></select>
  5465. <input type="text" id="import-classes-url">
  5466. <a class="btn" href="#" id="import-classes-load">Import Classes</a>
  5467. </div>
  5468. `;
  5469.  
  5470. d20plus.settingsHtmlPtClassesPlayer = `
  5471. <div class="importer-section" data-import-group="class">
  5472. <h4>Class Importing</h4>
  5473. <label for="import-classes-url-player">Class Data URL:</label>
  5474. <select id="button-classes-select-player"><!-- populate with JS--></select>
  5475. <input type="text" id="import-classes-url-player">
  5476. <a class="btn" href="#" id="import-classes-load-player">Import Classes</a>
  5477. </div>
  5478. `;
  5479.  
  5480. d20plus.settingsHtmlPtSubclasses = `
  5481. <div class="importer-section" data-import-group="subclass">
  5482. <h4>Subclass Importing</h4>
  5483. <label for="import-subclasses-url">Subclass Data URL:</label>
  5484. <select id="button-subclasses-select"><!-- populate with JS--></select>
  5485. <input type="text" id="import-subclasses-url">
  5486. <a class="btn" href="#" id="import-subclasses-load">Import Subclasses</a>
  5487. <p>
  5488. <b>Default subclasses are imported as part of Classes import. This can be used to load homebrew classes.</b>
  5489. </p>
  5490. </div>
  5491. `;
  5492.  
  5493. d20plus.settingsHtmlPtSubclassesPlayer = `
  5494. <div class="importer-section" data-import-group="subclass">
  5495. <h4>Subclass Importing</h4>
  5496. <label for="import-subclasses-url-player">Subclass Data URL:</label>
  5497. <select id="button-subclasses-select-player"><!-- populate with JS--></select>
  5498. <input type="text" id="import-subclasses-url-player">
  5499. <a class="btn" href="#" id="import-subclasses-load-player">Import Subclasses</a>
  5500. <p>
  5501. <b>Default subclasses are imported as part of Classes import. This can be used to load homebrew classes.</b>
  5502. </p>
  5503. </div>
  5504. `;
  5505.  
  5506. d20plus.settingsHtmlPtBackgrounds = `
  5507. <div class="importer-section" data-import-group="background">
  5508. <h4>Background Importing</h4>
  5509. <label for="import-backgrounds-url">Background Data URL:</label>
  5510. <select id="button-backgrounds-select"><!-- populate with JS--></select>
  5511. <input type="text" id="import-backgrounds-url">
  5512. <a class="btn" href="#" id="import-backgrounds-load">Import Backgrounds</a>
  5513. </div>
  5514. `;
  5515.  
  5516. d20plus.settingsHtmlPtBackgroundsPlayer = `
  5517. <div class="importer-section" data-import-group="background">
  5518. <h4>Background Importing</h4>
  5519. <label for="import-backgrounds-url-player">Background Data URL:</label>
  5520. <select id="button-backgrounds-select-player"><!-- populate with JS--></select>
  5521. <input type="text" id="import-backgrounds-url-player">
  5522. <a class="btn" href="#" id="import-backgrounds-load-player">Import Backgrounds</a>
  5523. </div>
  5524. `;
  5525.  
  5526. d20plus.settingsHtmlPtAdventures = `
  5527. <div class="importer-section" data-import-group="adventure">
  5528. <h4>Adventure Importing</h4>
  5529. <label for="import-adventures-url">Adventure Data URL:</label>
  5530. <select id="button-adventures-select">
  5531. <!-- populate with JS-->
  5532. </select>
  5533. <input type="text" id="import-adventures-url">
  5534. <p><a class="btn" href="#" id="button-adventures-load">Import Adventure</a><p/>
  5535. </div>
  5536. `;
  5537.  
  5538. d20plus.settingsHtmlPtImportFooter = `
  5539. <br>
  5540. <a class="btn bind-drop-locations" href="#" id="bind-drop-locations" style="margin-top: 3px;">Bind Drag-n-Drop</a>
  5541. <p><strong>Readme</strong></p>
  5542. <p>
  5543. You can drag-and-drop imported handouts to character sheets.<br>
  5544. If a handout is glowing green in the journal, it's draggable. This breaks when Roll20 decides to hard-refresh the journal.<br>
  5545. To restore this functionality, press the "Bind Drag-n-Drop" button.<br>
  5546. <i>Note: to drag a handout to a character sheet, you need to drag the name, and not the handout icon.</i>
  5547. </p>
  5548. `;
  5549.  
  5550. d20plus.cssRules = d20plus.cssRules.concat([
  5551. {
  5552. s: ".no-shrink",
  5553. r: "flex-shrink: 0;"
  5554. },
  5555. {
  5556. s: "#initiativewindow ul li span.initiative,#initiativewindow ul li span.tracker-col,#initiativewindow ul li span.initmacro",
  5557. r: "font-size: 25px;font-weight: bold;text-align: right;float: right;padding: 2px 5px;width: 10%;min-height: 20px;display: block;"
  5558. },
  5559. {
  5560. s: "#initiativewindow ul li span.editable input",
  5561. r: "width: 100%; box-sizing: border-box;height: 100%;"
  5562. },
  5563. {
  5564. s: "#initiativewindow div.header",
  5565. r: "height: 30px;"
  5566. },
  5567. {
  5568. s: "#initiativewindow div.header span",
  5569. r: "cursor: default;font-size: 15px;font-weight: bold;text-align: right;float: right;width: 10%;min-height: 20px;padding: 5px;"
  5570. },
  5571. {
  5572. s: ".ui-dialog-buttonpane span.difficulty",
  5573. r: "display: inline-block;padding: 5px 4px 6px;margin: .5em .4em .5em 0;font-size: 18px;"
  5574. },
  5575. {
  5576. s: ".ui-dialog-buttonpane.buttonpane-absolute-position",
  5577. r: "position: absolute;bottom: 0;box-sizing: border-box;width: 100%;"
  5578. },
  5579. {
  5580. s: ".ui-dialog.dialog-collapsed .ui-dialog-buttonpane",
  5581. r: "position: initial;"
  5582. },
  5583. {
  5584. s: ".token .cr,.header .cr",
  5585. r: "display: none!important;"
  5586. },
  5587. {
  5588. s: "li.handout.compendium-item .namecontainer",
  5589. r: "box-shadow: inset 0px 0px 25px 2px rgb(195, 239, 184);"
  5590. },
  5591. {
  5592. s: ".bind-drop-locations:active",
  5593. r: "box-shadow: inset 0px 0px 25px 2px rgb(195, 239, 184);"
  5594. },
  5595. {
  5596. s: "del",
  5597. r: "display: none;"
  5598. },
  5599. {
  5600. s: ".import-cb-label",
  5601. r: "display: block; margin-right: -13px !important;"
  5602. },
  5603. {
  5604. s: ".import-cb-label span",
  5605. r: "display: inline-block; overflow: hidden; max-height: 18px; letter-spacing: -1px; font-size: 12px;"
  5606. },
  5607. {
  5608. s: ".import-cb-label .source",
  5609. r: "width: calc(16.667% - 28px);'"
  5610. },
  5611. {
  5612. s: ".importer-section",
  5613. r: "display: none;"
  5614. },
  5615. {
  5616. s: ".userscript-entry-title",
  5617. r: "font-weight: bold;"
  5618. },
  5619. {
  5620. s: ".userscript-statsBlockHead > .userscript-entry-title",
  5621. r: "font-weight: bold; font-size: 1.5em;"
  5622. },
  5623. {
  5624. s: ".userscript-statsBlockHead > .userscript-statsBlockSubHead > .userscript-entry-title",
  5625. r: "font-weight: bold; font-size: 1.3em;"
  5626. },
  5627. {
  5628. s: ".userscript-statsInlineHead > .userscript-entry-title, .userscript-statsInlineHeadSubVariant > .userscript-entry-title",
  5629. r: "font-style: italic"
  5630. },
  5631. {
  5632. s: ".userscript-statsBlockInsetReadaloud",
  5633. r: "background: #cbd6c688 !important"
  5634. },
  5635. // some generic width stuff
  5636. {
  5637. s: ".col-1",
  5638. r: "width: 8.333%;"
  5639. },
  5640. {
  5641. s: ".col-2",
  5642. r: "width: 16.666%;"
  5643. },
  5644. {
  5645. s: ".col-3",
  5646. r: "width: 25%;"
  5647. },
  5648. {
  5649. s: ".col-4",
  5650. r: "width: 33.333%;"
  5651. },
  5652. {
  5653. s: ".col-5",
  5654. r: "width: 41.667%;"
  5655. },
  5656. {
  5657. s: ".col-6",
  5658. r: "width: 50%;"
  5659. },
  5660. {
  5661. s: ".col-7",
  5662. r: "width: 58.333%;"
  5663. },
  5664. {
  5665. s: ".col-8",
  5666. r: "width: 66.667%;"
  5667. },
  5668. {
  5669. s: ".col-9",
  5670. r: "width: 75%;"
  5671. },
  5672. {
  5673. s: ".col-10",
  5674. r: "width: 83.333%;"
  5675. },
  5676. {
  5677. s: ".col-11",
  5678. r: "width: 91.667%;"
  5679. },
  5680. {
  5681. s: ".col-12",
  5682. r: "width: 100%;"
  5683. },
  5684. ]);
  5685.  
  5686. d20plus.tools = d20plus.tools.concat([
  5687. {
  5688. name: "Shapeshifter Token Builder",
  5689. desc: "Build a rollable table and related token to represent a shapeshifting creature.",
  5690. html: `
  5691. <div id="d20plus-shapeshiftbuild" title="Shapeshifter Token Builder">
  5692. <div id="shapeshiftbuild-list">
  5693. <input type="search" class="search" placeholder="Search creatures...">
  5694. <input type="search" class="filter" placeholder="Filter...">
  5695. <span title="Filter format example: 'cr:1/4; cr:1/2; type:beast; source:MM'" style="cursor: help;">[?]</span>
  5696. <div class="list" style="transform: translateZ(0); max-height: 490px; overflow-y: scroll; overflow-x: hidden;"><i>Loading...</i></div>
  5697. </div>
  5698. <br>
  5699. <input id="shapeshift-name" placeholder="Table name">
  5700. <button class="btn">Create Table</button>
  5701. </div>
  5702. `,
  5703. dialogFn: () => {
  5704. $("#d20plus-shapeshiftbuild").dialog({
  5705. autoOpen: false,
  5706. resizable: true,
  5707. width: 800,
  5708. height: 650,
  5709. });
  5710. },
  5711. openFn: () => {
  5712. const $win = $("#d20plus-shapeshiftbuild");
  5713. $win.dialog("open");
  5714.  
  5715. const toLoad = Object.keys(monsterDataUrls).map(src => d20plus.monsters.formMonsterUrl(monsterDataUrls[src]));
  5716.  
  5717. const $fltr = $win.find(`.filter`);
  5718. $fltr.off("keydown").off("keyup");
  5719. $win.find(`button`).off("click");
  5720.  
  5721. const $lst = $win.find(`.list`);
  5722. let tokenList;
  5723.  
  5724. DataUtil.multiLoadJSON(
  5725. toLoad.map(url => ({url})),
  5726. () => {},
  5727. (dataStack) => {
  5728. $lst.empty();
  5729.  
  5730. let toShow = [];
  5731. dataStack.forEach(d => toShow = toShow.concat(d.monster));
  5732. toShow = toShow.sort((a, b) => SortUtil.ascSort(a.name, b.name));
  5733.  
  5734. let tmp = "";
  5735. toShow.forEach((m, i) => {
  5736. m.__pType = Parser.monTypeToFullObj(m.type).asText;
  5737.  
  5738. tmp += `
  5739. <label class="import-cb-label" data-listid="${i}">
  5740. <input type="checkbox">
  5741. <span class="name col-4">${m.name}</span>
  5742. <span class="type col-4">TYP[${m.__pType.uppercaseFirst()}]</span>
  5743. <span class="cr col-2">${m.cr === undefined ? "CR[Unknown]" : `CR[${(m.cr.cr || m.cr)}]`}</span>
  5744. <span title="${Parser.sourceJsonToFull(m.source)}" class="source">SRC[${Parser.sourceJsonToAbv(m.source)}]</span>
  5745. </label>
  5746. `;
  5747. });
  5748. $lst.html(tmp);
  5749. tmp = null;
  5750.  
  5751. tokenList = new List("shapeshiftbuild-list", {
  5752. valueNames: ["name", "type", "cr", "source"]
  5753. });
  5754.  
  5755. d20plus.importer.addListFilter($fltr, toShow, tokenList, d20plus.monsters._listIndexConverter);
  5756.  
  5757. $win.find(`button`).on("click", () => {
  5758. console.log("Assembling creature list");
  5759. if (tokenList) {
  5760. $("a.ui-tabs-anchor[href='#deckstables']").trigger("click");
  5761.  
  5762. const sel = tokenList.items
  5763. .filter(it => $(it.elm).find(`input`).prop("checked"))
  5764. .map(it => toShow[$(it.elm).attr("data-listid")]);
  5765.  
  5766. const id = d20.Campaign.rollabletables.create().id;
  5767. const table = d20.Campaign.rollabletables.get(id);
  5768. table.set("name", $(`#shapeshift-name`).val().trim() || "Shapeshifter");
  5769. table.save();
  5770. sel.forEach(m => {
  5771. const item = table.tableitems.create();
  5772. item.set("name", m.name);
  5773. const avatar = m.tokenURL || `${IMG_URL}${Parser.sourceJsonToAbv(m.source)}/${m.name.replace(/"/g, "")}.png`;
  5774. item.set("avatar", avatar);
  5775. item.save();
  5776. });
  5777. table.save();
  5778. d20.rollabletables.refreshTablesList();
  5779. alert("Created table!")
  5780. }
  5781. });
  5782. }
  5783. );
  5784. }
  5785. },
  5786. {
  5787. name: "Pauper's Character Vault",
  5788. desc: "Dump characters to JSON, or import dumped characters.",
  5789. html: `
  5790. <div id="d20plus-paupervault" title="Pauper's Character Vault">
  5791. <p>
  5792. This experimental tool allows you to download characters as JSON, to later upload to other games.
  5793. </p>
  5794. <select style="margin-bottom: 0;"></select> <button class="btn download">Download</button>
  5795. <hr>
  5796. <button class="btn upload">Upload</button><input accept=".json" type="file" style="position: absolute; left: -9999px;"> (Previously Download-ed files only)
  5797. </div>
  5798. `,
  5799. dialogFn: () => {
  5800. $("#d20plus-paupervault").dialog({
  5801. autoOpen: false,
  5802. resizable: true,
  5803. width: 400,
  5804. height: 250,
  5805. });
  5806. },
  5807. openFn: () => {
  5808. const $win = $("#d20plus-paupervault");
  5809. $win.dialog("open");
  5810.  
  5811. const $selChar = $win.find(`select`);
  5812.  
  5813. $selChar.append(d20.Campaign.characters.toJSON().sort((a, b) => SortUtil.ascSort(a.name, b.name)).map(c => {
  5814. return `<option value="${c.id}">${c.name || `(Unnamed; ID ${c.id})`}</option>`
  5815. }).join(""));
  5816.  
  5817. const $btnDl = $win.find(`.download`);
  5818. $btnDl.off("click");
  5819. $btnDl.on("click", () => {
  5820. const id = $selChar.val();
  5821. const rawChar = d20.Campaign.characters.get(id);
  5822. const char = rawChar.toJSON();
  5823. char.attribs = rawChar.attribs.toJSON();
  5824. DataUtil.userDownload(char.name.replace(/[^0-9A-Za-z -_()\[\]{}]/, "_"), JSON.stringify({
  5825. char
  5826. }, null, "\t"));
  5827. });
  5828.  
  5829. const $btnUl = $win.find(`.upload`);
  5830. $btnUl.off("click");
  5831. $btnUl.on("click", () => {
  5832. const $iptFile = $win.find(`input[type="file"]`);
  5833.  
  5834. const input = $iptFile[0];
  5835.  
  5836. const reader = new FileReader();
  5837. reader.onload = () => {
  5838. $("a.ui-tabs-anchor[href='#journal']").trigger("click");
  5839.  
  5840. try {
  5841. const text = reader.result;
  5842. const json = JSON.parse(text);
  5843.  
  5844. if (!json.char) {
  5845. window.alert("Failed to import character! See the log for details.");
  5846. console.error(`No "char" attribute found in parsed JSON!`);
  5847. return;
  5848. }
  5849. const char = json.char;
  5850.  
  5851. const newId = d20plus.generateRowId();
  5852. d20.Campaign.characters.create(
  5853. {
  5854. ...char,
  5855. id: newId
  5856. },
  5857. {
  5858. success: function (character) {
  5859. try {
  5860. character.attribs.reset();
  5861. if (!char.attribs) {
  5862. window.alert(`Warning: Uploaded character had no "attribs" attribute. The character sheet will contain no data.`);
  5863. return;
  5864. }
  5865. const toSave = char.attribs.map(a => character.attribs.push(a));
  5866. toSave.forEach(s => s.syncedSave());
  5867. } catch (e) {
  5868. window.alert("Failed to import character! See the log for details.");
  5869. console.error(e);
  5870. }
  5871. }
  5872. }
  5873. );
  5874. } catch (e) {
  5875. console.error(e);
  5876. window.alert("Failed to load file! See the log for details.")
  5877. }
  5878. };
  5879. input.onchange = function () {
  5880. reader.readAsText(input.files[0]);
  5881. };
  5882.  
  5883. $iptFile.click();
  5884. });
  5885. }
  5886. }
  5887. ]);
  5888.  
  5889. // caller should run `$iptFilter.off("keydown").off("keyup");` before calling this
  5890. d20plus.importer.addListFilter = function ($iptFilter, dataList, listObj, listIndexConverter) {
  5891. $iptFilter.val("");
  5892. const TYPE_TIMEOUT_MS = 100;
  5893. let typeTimer;
  5894. $iptFilter.on("keyup", () => {
  5895. clearTimeout(typeTimer);
  5896. typeTimer = setTimeout(() => {
  5897. const exps = $iptFilter.val().split(";");
  5898. const filters = exps.map(it => it.trim())
  5899. .filter(it => it)
  5900. .map(it => it.toLowerCase().split(":"))
  5901. .filter(it => it.length === 2)
  5902. .map(it => ({field: it[0], value: it[1]}));
  5903. const grouped = [];
  5904. filters.forEach(f => {
  5905. const existing = grouped.find(it => it.field === f.field);
  5906. if (existing) existing.values.push(f.value);
  5907. else grouped.push({field: f.field, values: [f.value]})
  5908. });
  5909.  
  5910. listObj.filter((item) => {
  5911. const it = dataList[$(item.elm).attr("data-listid")];
  5912. it._filterVs = it._filterVs || listIndexConverter(it);
  5913. return !grouped.find(f => {
  5914. if (it._filterVs[f.field]) {
  5915. if (it._filterVs[f.field] instanceof Array) {
  5916. return !(it._filterVs[f.field].find(v => f.values.includes(v)));
  5917. } else {
  5918. return !f.values.includes(it._filterVs[f.field])
  5919. }
  5920. }
  5921. return false;
  5922. });
  5923. });
  5924. }, TYPE_TIMEOUT_MS);
  5925. });
  5926. $iptFilter.on("keydown", () => {
  5927. clearTimeout(typeTimer);
  5928. });
  5929. };
  5930.  
  5931. d20plus.initiativeHeaders = `<div class="header">
  5932. <span class="ui-button-text initmacro">Sheet</span>
  5933. <span class="initiative" alt="Initiative" title="Initiative">Init</span>
  5934. <span class="cr" alt="CR" title="CR">CR</span>
  5935. <div class="tracker-header-extra-columns"></div>
  5936. </div>`;
  5937.  
  5938. d20plus.initiativeTemplate = `<script id="tmpl_initiativecharacter" type="text/html">
  5939. <![CDATA[
  5940. <li class='token <$ if (this.layer === "gmlayer") { $>gmlayer<$ } $>' data-tokenid='<$!this.id$>' data-currentindex='<$!this.idx$>'>
  5941. <$ var token = d20.Campaign.pages.get(d20.Campaign.activePage()).thegraphics.get(this.id); $>
  5942. <$ var char = (token) ? token.character : null; $>
  5943. <span alt='Sheet Macro' title='Sheet Macro' class='initmacro'>
  5944. <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'>
  5945. <span class='ui-button-text'>N</span>
  5946. </button>
  5947. </span>
  5948. <span alt='Initiative' title='Initiative' class='initiative <$ if (this.iseditable) { $>editable<$ } $>'>
  5949. <$!this.pr$>
  5950. </span>
  5951. <$ if (char) { $>
  5952. <$ var npc = char.attribs ? char.attribs.find(function(a){return a.get("name").toLowerCase() == "npc" }) : null; $>
  5953. <$ } $>
  5954. <div class="tracker-extra-columns">
  5955. <!--5ETOOLS_REPLACE_TARGET-->
  5956. </div>
  5957. <$ if (this.avatar) { $><img src='<$!this.avatar$>' /><$ } $>
  5958. <span class='name'><$!this.name$></span>
  5959. <div class='clear' style='height: 0px;'></div>
  5960. <div class='controls'>
  5961. <span class='pictos remove'>#</span>
  5962. </div>
  5963. </li>
  5964. ]]>
  5965. </script>`;
  5966.  
  5967. d20plus.actionMacroPerception = "%{Selected|npc_perception} /w gm &{template:default} {{name=Senses}} /w gm @{Selected|npc_senses} ";
  5968. d20plus.actionMacroInit = "%{selected|npc_init}";
  5969. 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}}} ";
  5970. 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)}}";
  5971. 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}}} ";
  5972. 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}}} ";
  5973. 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}}} ";
  5974.  
  5975. d20plus.actionMacroTrait = function (index) {
  5976. return "/w gm &{template:npcaction} {{name=@{selected|npc_name}}} {{rname=@{selected|repeating_npctrait_$" + index + "_name}}} {{description=@{selected|repeating_npctrait_$" + index + "_desc} }}";
  5977. };
  5978.  
  5979. d20plus.actionMacroAction = function (index) {
  5980. return "%{selected|repeating_npcaction_$" + index + "_npc_action}";
  5981. };
  5982.  
  5983. d20plus.actionMacroReaction = "/w gm &{template:npcaction} {{name=@{selected|npc_name}}} {{rname=@{selected|repeating_npcreaction_$0_name}}} {{description=@{selected|repeating_npcreaction_$0_desc} }} ";
  5984.  
  5985. d20plus.actionMacroLegendary = function (tokenactiontext) {
  5986. 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 + "}} ";
  5987. }
  5988. };
  5989.  
  5990. SCRIPT_EXTENSIONS.push(betteR205etools);
  5991.  
  5992. const betteR20Emoji = function () {
  5993. d20plus.chat = {};
  5994.  
  5995. d20plus.chat.emojiIndex = {
  5996. joy: !0,
  5997. heart: !0,
  5998. heart_eyes: !0,
  5999. sob: !0,
  6000. blush: !0,
  6001. unamused: !0,
  6002. kissing_heart: !0,
  6003. two_hearts: !0,
  6004. weary: !0,
  6005. ok_hand: !0,
  6006. pensive: !0,
  6007. smirk: !0,
  6008. grin: !0,
  6009. recycle: !0,
  6010. wink: !0,
  6011. thumbsup: !0,
  6012. pray: !0,
  6013. relieved: !0,
  6014. notes: !0,
  6015. flushed: !0,
  6016. raised_hands: !0,
  6017. see_no_evil: !0,
  6018. cry: !0,
  6019. sunglasses: !0,
  6020. v: !0,
  6021. eyes: !0,
  6022. sweat_smile: !0,
  6023. sparkles: !0,
  6024. sleeping: !0,
  6025. smile: !0,
  6026. purple_heart: !0,
  6027. broken_heart: !0,
  6028. expressionless: !0,
  6029. sparkling_heart: !0,
  6030. blue_heart: !0,
  6031. confused: !0,
  6032. information_desk_person: !0,
  6033. stuck_out_tongue_winking_eye: !0,
  6034. disappointed: !0,
  6035. yum: !0,
  6036. neutral_face: !0,
  6037. sleepy: !0,
  6038. clap: !0,
  6039. cupid: !0,
  6040. heartpulse: !0,
  6041. revolving_hearts: !0,
  6042. arrow_left: !0,
  6043. speak_no_evil: !0,
  6044. kiss: !0,
  6045. point_right: !0,
  6046. cherry_blossom: !0,
  6047. scream: !0,
  6048. fire: !0,
  6049. rage: !0,
  6050. smiley: !0,
  6051. tada: !0,
  6052. tired_face: !0,
  6053. camera: !0,
  6054. rose: !0,
  6055. stuck_out_tongue_closed_eyes: !0,
  6056. muscle: !0,
  6057. skull: !0,
  6058. sunny: !0,
  6059. yellow_heart: !0,
  6060. triumph: !0,
  6061. new_moon_with_face: !0,
  6062. laughing: !0,
  6063. sweat: !0,
  6064. point_left: !0,
  6065. heavy_check_mark: !0,
  6066. heart_eyes_cat: !0,
  6067. grinning: !0,
  6068. mask: !0,
  6069. green_heart: !0,
  6070. wave: !0,
  6071. persevere: !0,
  6072. heartbeat: !0,
  6073. arrow_forward: !0,
  6074. arrow_backward: !0,
  6075. arrow_right_hook: !0,
  6076. leftwards_arrow_with_hook: !0,
  6077. crown: !0,
  6078. kissing_closed_eyes: !0,
  6079. stuck_out_tongue: !0,
  6080. disappointed_relieved: !0,
  6081. innocent: !0,
  6082. headphones: !0,
  6083. white_check_mark: !0,
  6084. confounded: !0,
  6085. arrow_right: !0,
  6086. angry: !0,
  6087. grimacing: !0,
  6088. star2: !0,
  6089. gun: !0,
  6090. raising_hand: !0,
  6091. thumbsdown: !0,
  6092. dancer: !0,
  6093. musical_note: !0,
  6094. no_mouth: !0,
  6095. dizzy: !0,
  6096. fist: !0,
  6097. point_down: !0,
  6098. red_circle: !0,
  6099. no_good: !0,
  6100. boom: !0,
  6101. thought_balloon: !0,
  6102. tongue: !0,
  6103. poop: !0,
  6104. cold_sweat: !0,
  6105. gem: !0,
  6106. ok_woman: !0,
  6107. pizza: !0,
  6108. joy_cat: !0,
  6109. sun_with_face: !0,
  6110. leaves: !0,
  6111. sweat_drops: !0,
  6112. penguin: !0,
  6113. zzz: !0,
  6114. walking: !0,
  6115. airplane: !0,
  6116. balloon: !0,
  6117. star: !0,
  6118. ribbon: !0,
  6119. ballot_box_with_check: !0,
  6120. worried: !0,
  6121. underage: !0,
  6122. fearful: !0,
  6123. four_leaf_clover: !0,
  6124. hibiscus: !0,
  6125. microphone: !0,
  6126. open_hands: !0,
  6127. ghost: !0,
  6128. palm_tree: !0,
  6129. bangbang: !0,
  6130. nail_care: !0,
  6131. x: !0,
  6132. alien: !0,
  6133. bow: !0,
  6134. cloud: !0,
  6135. soccer: !0,
  6136. angel: !0,
  6137. dancers: !0,
  6138. exclamation: !0,
  6139. snowflake: !0,
  6140. point_up: !0,
  6141. kissing_smiling_eyes: !0,
  6142. rainbow: !0,
  6143. crescent_moon: !0,
  6144. heart_decoration: !0,
  6145. gift_heart: !0,
  6146. gift: !0,
  6147. beers: !0,
  6148. anguished: !0,
  6149. earth_africa: !0,
  6150. movie_camera: !0,
  6151. anchor: !0,
  6152. zap: !0,
  6153. heavy_multiplication_x: !0,
  6154. runner: !0,
  6155. sunflower: !0,
  6156. earth_americas: !0,
  6157. bouquet: !0,
  6158. dog: !0,
  6159. moneybag: !0,
  6160. herb: !0,
  6161. couple: !0,
  6162. fallen_leaf: !0,
  6163. tulip: !0,
  6164. birthday: !0,
  6165. cat: !0,
  6166. coffee: !0,
  6167. dizzy_face: !0,
  6168. point_up_2: !0,
  6169. open_mouth: !0,
  6170. hushed: !0,
  6171. basketball: !0,
  6172. christmas_tree: !0,
  6173. ring: !0,
  6174. full_moon_with_face: !0,
  6175. astonished: !0,
  6176. two_women_holding_hands: !0,
  6177. money_with_wings: !0,
  6178. crying_cat_face: !0,
  6179. hear_no_evil: !0,
  6180. dash: !0,
  6181. cactus: !0,
  6182. hotsprings: !0,
  6183. telephone: !0,
  6184. maple_leaf: !0,
  6185. princess: !0,
  6186. massage: !0,
  6187. love_letter: !0,
  6188. trophy: !0,
  6189. person_frowning: !0,
  6190. us: !0,
  6191. confetti_ball: !0,
  6192. blossom: !0,
  6193. lips: !0,
  6194. fries: !0,
  6195. doughnut: !0,
  6196. frowning: !0,
  6197. ocean: !0,
  6198. bomb: !0,
  6199. ok: !0,
  6200. cyclone: !0,
  6201. rocket: !0,
  6202. umbrella: !0,
  6203. couplekiss: !0,
  6204. couple_with_heart: !0,
  6205. lollipop: !0,
  6206. clapper: !0,
  6207. pig: !0,
  6208. smiling_imp: !0,
  6209. imp: !0,
  6210. bee: !0,
  6211. kissing_cat: !0,
  6212. anger: !0,
  6213. musical_score: !0,
  6214. santa: !0,
  6215. earth_asia: !0,
  6216. football: !0,
  6217. guitar: !0,
  6218. panda_face: !0,
  6219. speech_balloon: !0,
  6220. strawberry: !0,
  6221. smirk_cat: !0,
  6222. banana: !0,
  6223. watermelon: !0,
  6224. snowman: !0,
  6225. smile_cat: !0,
  6226. top: !0,
  6227. eggplant: !0,
  6228. crystal_ball: !0,
  6229. fork_and_knife: !0,
  6230. calling: !0,
  6231. iphone: !0,
  6232. partly_sunny: !0,
  6233. warning: !0,
  6234. scream_cat: !0,
  6235. small_orange_diamond: !0,
  6236. baby: !0,
  6237. feet: !0,
  6238. footprints: !0,
  6239. beer: !0,
  6240. wine_glass: !0,
  6241. o: !0,
  6242. video_camera: !0,
  6243. rabbit: !0,
  6244. tropical_drink: !0,
  6245. smoking: !0,
  6246. space_invader: !0,
  6247. peach: !0,
  6248. snake: !0,
  6249. turtle: !0,
  6250. cherries: !0,
  6251. kissing: !0,
  6252. frog: !0,
  6253. milky_way: !0,
  6254. rotating_light: !0,
  6255. hatching_chick: !0,
  6256. closed_book: !0,
  6257. candy: !0,
  6258. hamburger: !0,
  6259. bear: !0,
  6260. tiger: !0,
  6261. fast_forward: !0,
  6262. icecream: !0,
  6263. pineapple: !0,
  6264. ear_of_rice: !0,
  6265. syringe: !0,
  6266. put_litter_in_its_place: !0,
  6267. chocolate_bar: !0,
  6268. black_small_square: !0,
  6269. tv: !0,
  6270. pill: !0,
  6271. octopus: !0,
  6272. jack_o_lantern: !0,
  6273. grapes: !0,
  6274. smiley_cat: !0,
  6275. cd: !0,
  6276. cocktail: !0,
  6277. cake: !0,
  6278. video_game: !0,
  6279. arrow_down: !0,
  6280. no_entry_sign: !0,
  6281. lipstick: !0,
  6282. whale: !0,
  6283. cookie: !0,
  6284. dolphin: !0,
  6285. loud_sound: !0,
  6286. man: !0,
  6287. hatched_chick: !0,
  6288. monkey: !0,
  6289. books: !0,
  6290. japanese_ogre: !0,
  6291. guardsman: !0,
  6292. loudspeaker: !0,
  6293. scissors: !0,
  6294. girl: !0,
  6295. mortar_board: !0,
  6296. fr: !0,
  6297. baseball: !0,
  6298. vertical_traffic_light: !0,
  6299. woman: !0,
  6300. fireworks: !0,
  6301. stars: !0,
  6302. sos: !0,
  6303. mushroom: !0,
  6304. pouting_cat: !0,
  6305. left_luggage: !0,
  6306. high_heel: !0,
  6307. dart: !0,
  6308. swimmer: !0,
  6309. key: !0,
  6310. bikini: !0,
  6311. family: !0,
  6312. pencil2: !0,
  6313. elephant: !0,
  6314. droplet: !0,
  6315. seedling: !0,
  6316. apple: !0,
  6317. cool: !0,
  6318. telephone_receiver: !0,
  6319. dollar: !0,
  6320. house_with_garden: !0,
  6321. book: !0,
  6322. haircut: !0,
  6323. computer: !0,
  6324. bulb: !0,
  6325. question: !0,
  6326. back: !0,
  6327. boy: !0,
  6328. closed_lock_with_key: !0,
  6329. person_with_pouting_face: !0,
  6330. tangerine: !0,
  6331. sunrise: !0,
  6332. poultry_leg: !0,
  6333. blue_circle: !0,
  6334. oncoming_automobile: !0,
  6335. shaved_ice: !0,
  6336. bird: !0,
  6337. first_quarter_moon_with_face: !0,
  6338. eyeglasses: !0,
  6339. goat: !0,
  6340. night_with_stars: !0,
  6341. older_woman: !0,
  6342. black_circle: !0,
  6343. new_moon: !0,
  6344. two_men_holding_hands: !0,
  6345. white_circle: !0,
  6346. customs: !0,
  6347. tropical_fish: !0,
  6348. house: !0,
  6349. arrows_clockwise: !0,
  6350. last_quarter_moon_with_face: !0,
  6351. round_pushpin: !0,
  6352. full_moon: !0,
  6353. athletic_shoe: !0,
  6354. lemon: !0,
  6355. baby_bottle: !0,
  6356. spaghetti: !0,
  6357. wind_chime: !0,
  6358. fish_cake: !0,
  6359. evergreen_tree: !0,
  6360. up: !0,
  6361. arrow_up: !0,
  6362. arrow_upper_right: !0,
  6363. arrow_lower_right: !0,
  6364. arrow_lower_left: !0,
  6365. performing_arts: !0,
  6366. nose: !0,
  6367. pig_nose: !0,
  6368. fish: !0,
  6369. man_with_turban: !0,
  6370. koala: !0,
  6371. ear: !0,
  6372. eight_spoked_asterisk: !0,
  6373. small_blue_diamond: !0,
  6374. shower: !0,
  6375. bug: !0,
  6376. ramen: !0,
  6377. tophat: !0,
  6378. bride_with_veil: !0,
  6379. fuelpump: !0,
  6380. checkered_flag: !0,
  6381. horse: !0,
  6382. watch: !0,
  6383. monkey_face: !0,
  6384. baby_symbol: !0,
  6385. new: !0,
  6386. free: !0,
  6387. sparkler: !0,
  6388. corn: !0,
  6389. tennis: !0,
  6390. alarm_clock: !0,
  6391. battery: !0,
  6392. grey_exclamation: !0,
  6393. wolf: !0,
  6394. moyai: !0,
  6395. cow: !0,
  6396. mega: !0,
  6397. older_man: !0,
  6398. dress: !0,
  6399. link: !0,
  6400. chicken: !0,
  6401. whale2: !0,
  6402. arrow_upper_left: !0,
  6403. deciduous_tree: !0,
  6404. bento: !0,
  6405. pushpin: !0,
  6406. soon: !0,
  6407. repeat: !0,
  6408. dragon: !0,
  6409. hamster: !0,
  6410. golf: !0,
  6411. surfer: !0,
  6412. mouse: !0,
  6413. waxing_crescent_moon: !0,
  6414. blue_car: !0,
  6415. a: !0,
  6416. interrobang: !0,
  6417. u5272: !0,
  6418. electric_plug: !0,
  6419. first_quarter_moon: !0,
  6420. cancer: !0,
  6421. trident: !0,
  6422. bread: !0,
  6423. cop: !0,
  6424. tea: !0,
  6425. fishing_pole_and_fish: !0,
  6426. bike: !0,
  6427. rice: !0,
  6428. radio: !0,
  6429. baby_chick: !0,
  6430. arrow_heading_down: !0,
  6431. waning_crescent_moon: !0,
  6432. arrow_up_down: !0,
  6433. last_quarter_moon: !0,
  6434. radio_button: !0,
  6435. sheep: !0,
  6436. person_with_blond_hair: !0,
  6437. waning_gibbous_moon: !0,
  6438. lock: !0,
  6439. green_apple: !0,
  6440. japanese_goblin: !0,
  6441. curly_loop: !0,
  6442. triangular_flag_on_post: !0,
  6443. arrows_counterclockwise: !0,
  6444. racehorse: !0,
  6445. fried_shrimp: !0,
  6446. sunrise_over_mountains: !0,
  6447. volcano: !0,
  6448. rooster: !0,
  6449. inbox_tray: !0,
  6450. wedding: !0,
  6451. sushi: !0,
  6452. wavy_dash: !0,
  6453. ice_cream: !0,
  6454. rewind: !0,
  6455. tomato: !0,
  6456. rabbit2: !0,
  6457. eight_pointed_black_star: !0,
  6458. small_red_triangle: !0,
  6459. high_brightness: !0,
  6460. heavy_plus_sign: !0,
  6461. man_with_gua_pi_mao: !0,
  6462. convenience_store: !0,
  6463. busts_in_silhouette: !0,
  6464. beetle: !0,
  6465. small_red_triangle_down: !0,
  6466. arrow_heading_up: !0,
  6467. name_badge: !0,
  6468. bath: !0,
  6469. no_entry: !0,
  6470. crocodile: !0,
  6471. dog2: !0,
  6472. cat2: !0,
  6473. hammer: !0,
  6474. meat_on_bone: !0,
  6475. shell: !0,
  6476. sparkle: !0,
  6477. b: !0,
  6478. m: !0,
  6479. poodle: !0,
  6480. aquarius: !0,
  6481. stew: !0,
  6482. jeans: !0,
  6483. honey_pot: !0,
  6484. musical_keyboard: !0,
  6485. unlock: !0,
  6486. black_nib: !0,
  6487. statue_of_liberty: !0,
  6488. heavy_dollar_sign: !0,
  6489. snowboarder: !0,
  6490. white_flower: !0,
  6491. necktie: !0,
  6492. diamond_shape_with_a_dot_inside: !0,
  6493. aries: !0,
  6494. womens: !0,
  6495. ant: !0,
  6496. scorpius: !0,
  6497. city_sunset: !0,
  6498. hourglass_flowing_sand: !0,
  6499. o2: !0,
  6500. dragon_face: !0,
  6501. snail: !0,
  6502. dvd: !0,
  6503. shirt: !0,
  6504. game_die: !0,
  6505. heavy_minus_sign: !0,
  6506. dolls: !0,
  6507. sagittarius: !0,
  6508. "8ball": !0,
  6509. bus: !0,
  6510. custard: !0,
  6511. crossed_flags: !0,
  6512. part_alternation_mark: !0,
  6513. camel: !0,
  6514. curry: !0,
  6515. steam_locomotive: !0,
  6516. hospital: !0,
  6517. large_blue_diamond: !0,
  6518. tanabata_tree: !0,
  6519. bell: !0,
  6520. leo: !0,
  6521. gemini: !0,
  6522. pear: !0,
  6523. large_orange_diamond: !0,
  6524. taurus: !0,
  6525. globe_with_meridians: !0,
  6526. door: !0,
  6527. clock6: !0,
  6528. oncoming_police_car: !0,
  6529. envelope_with_arrow: !0,
  6530. closed_umbrella: !0,
  6531. saxophone: !0,
  6532. church: !0,
  6533. bicyclist: !0,
  6534. pisces: !0,
  6535. dango: !0,
  6536. capricorn: !0,
  6537. office: !0,
  6538. rowboat: !0,
  6539. womans_hat: !0,
  6540. mans_shoe: !0,
  6541. love_hotel: !0,
  6542. mount_fuji: !0,
  6543. dromedary_camel: !0,
  6544. handbag: !0,
  6545. hourglass: !0,
  6546. negative_squared_cross_mark: !0,
  6547. trumpet: !0,
  6548. school: !0,
  6549. cow2: !0,
  6550. construction_worker: !0,
  6551. toilet: !0,
  6552. pig2: !0,
  6553. grey_question: !0,
  6554. beginner: !0,
  6555. violin: !0,
  6556. on: !0,
  6557. credit_card: !0,
  6558. id: !0,
  6559. secret: !0,
  6560. ferris_wheel: !0,
  6561. bowling: !0,
  6562. libra: !0,
  6563. virgo: !0,
  6564. barber: !0,
  6565. purse: !0,
  6566. roller_coaster: !0,
  6567. rat: !0,
  6568. date: !0,
  6569. rugby_football: !0,
  6570. ram: !0,
  6571. arrow_up_small: !0,
  6572. black_square_button: !0,
  6573. mobile_phone_off: !0,
  6574. tokyo_tower: !0,
  6575. congratulations: !0,
  6576. kimono: !0,
  6577. ship: !0,
  6578. mag_right: !0,
  6579. mag: !0,
  6580. fire_engine: !0,
  6581. clock1130: !0,
  6582. police_car: !0,
  6583. black_joker: !0,
  6584. bridge_at_night: !0,
  6585. package: !0,
  6586. oncoming_taxi: !0,
  6587. calendar: !0,
  6588. horse_racing: !0,
  6589. tiger2: !0,
  6590. boot: !0,
  6591. ambulance: !0,
  6592. white_square_button: !0,
  6593. boar: !0,
  6594. school_satchel: !0,
  6595. loop: !0,
  6596. pound: !0,
  6597. information_source: !0,
  6598. ox: !0,
  6599. rice_ball: !0,
  6600. vs: !0,
  6601. end: !0,
  6602. parking: !0,
  6603. sandal: !0,
  6604. tent: !0,
  6605. seat: !0,
  6606. taxi: !0,
  6607. black_medium_small_square: !0,
  6608. briefcase: !0,
  6609. newspaper: !0,
  6610. circus_tent: !0,
  6611. six_pointed_star: !0,
  6612. mens: !0,
  6613. european_castle: !0,
  6614. flashlight: !0,
  6615. foggy: !0,
  6616. arrow_double_up: !0,
  6617. bamboo: !0,
  6618. ticket: !0,
  6619. helicopter: !0,
  6620. minidisc: !0,
  6621. oncoming_bus: !0,
  6622. melon: !0,
  6623. white_small_square: !0,
  6624. european_post_office: !0,
  6625. keycap_ten: !0,
  6626. notebook: !0,
  6627. no_bell: !0,
  6628. oden: !0,
  6629. flags: !0,
  6630. carousel_horse: !0,
  6631. blowfish: !0,
  6632. chart_with_upwards_trend: !0,
  6633. sweet_potato: !0,
  6634. ski: !0,
  6635. clock12: !0,
  6636. signal_strength: !0,
  6637. construction: !0,
  6638. black_medium_square: !0,
  6639. satellite: !0,
  6640. euro: !0,
  6641. womans_clothes: !0,
  6642. ledger: !0,
  6643. leopard: !0,
  6644. low_brightness: !0,
  6645. clock3: !0,
  6646. department_store: !0,
  6647. truck: !0,
  6648. sake: !0,
  6649. railway_car: !0,
  6650. speedboat: !0,
  6651. vhs: !0,
  6652. clock1: !0,
  6653. arrow_double_down: !0,
  6654. water_buffalo: !0,
  6655. arrow_down_small: !0,
  6656. yen: !0,
  6657. mute: !0,
  6658. running_shirt_with_sash: !0,
  6659. white_large_square: !0,
  6660. wheelchair: !0,
  6661. clock2: !0,
  6662. paperclip: !0,
  6663. atm: !0,
  6664. cinema: !0,
  6665. telescope: !0,
  6666. rice_scene: !0,
  6667. blue_book: !0,
  6668. white_medium_square: !0,
  6669. postbox: !0,
  6670. "e-mail": !0,
  6671. mouse2: !0,
  6672. bullettrain_side: !0,
  6673. ideograph_advantage: !0,
  6674. nut_and_bolt: !0,
  6675. ng: !0,
  6676. hotel: !0,
  6677. wc: !0,
  6678. izakaya_lantern: !0,
  6679. repeat_one: !0,
  6680. mailbox_with_mail: !0,
  6681. chart_with_downwards_trend: !0,
  6682. green_book: !0,
  6683. tractor: !0,
  6684. fountain: !0,
  6685. metro: !0,
  6686. clipboard: !0,
  6687. no_mobile_phones: !0,
  6688. clock4: !0,
  6689. no_smoking: !0,
  6690. black_large_square: !0,
  6691. slot_machine: !0,
  6692. clock5: !0,
  6693. bathtub: !0,
  6694. scroll: !0,
  6695. station: !0,
  6696. rice_cracker: !0,
  6697. bank: !0,
  6698. wrench: !0,
  6699. u6307: !0,
  6700. articulated_lorry: !0,
  6701. page_facing_up: !0,
  6702. ophiuchus: !0,
  6703. bar_chart: !0,
  6704. no_pedestrians: !0,
  6705. vibration_mode: !0,
  6706. clock10: !0,
  6707. clock9: !0,
  6708. bullettrain_front: !0,
  6709. minibus: !0,
  6710. tram: !0,
  6711. clock8: !0,
  6712. u7a7a: !0,
  6713. traffic_light: !0,
  6714. mountain_bicyclist: !0,
  6715. microscope: !0,
  6716. japanese_castle: !0,
  6717. bookmark: !0,
  6718. bookmark_tabs: !0,
  6719. pouch: !0,
  6720. ab: !0,
  6721. page_with_curl: !0,
  6722. flower_playing_cards: !0,
  6723. clock11: !0,
  6724. fax: !0,
  6725. clock7: !0,
  6726. white_medium_small_square: !0,
  6727. currency_exchange: !0,
  6728. sound: !0,
  6729. chart: !0,
  6730. cl: !0,
  6731. floppy_disk: !0,
  6732. post_office: !0,
  6733. speaker: !0,
  6734. japan: !0,
  6735. u55b6: !0,
  6736. mahjong: !0,
  6737. incoming_envelope: !0,
  6738. orange_book: !0,
  6739. restroom: !0,
  6740. u7121: !0,
  6741. u6709: !0,
  6742. triangular_ruler: !0,
  6743. train: !0,
  6744. u7533: !0,
  6745. trolleybus: !0,
  6746. u6708: !0,
  6747. notebook_with_decorative_cover: !0,
  6748. u7981: !0,
  6749. u6e80: !0,
  6750. postal_horn: !0,
  6751. factory: !0,
  6752. children_crossing: !0,
  6753. train2: !0,
  6754. straight_ruler: !0,
  6755. pager: !0,
  6756. accept: !0,
  6757. u5408: !0,
  6758. lock_with_ink_pen: !0,
  6759. clock130: !0,
  6760. sa: !0,
  6761. outbox_tray: !0,
  6762. twisted_rightwards_arrows: !0,
  6763. mailbox: !0,
  6764. light_rail: !0,
  6765. clock930: !0,
  6766. busstop: !0,
  6767. open_file_folder: !0,
  6768. file_folder: !0,
  6769. potable_water: !0,
  6770. card_index: !0,
  6771. clock230: !0,
  6772. monorail: !0,
  6773. clock1230: !0,
  6774. clock1030: !0,
  6775. abc: !0,
  6776. mailbox_closed: !0,
  6777. clock430: !0,
  6778. mountain_railway: !0,
  6779. do_not_litter: !0,
  6780. clock330: !0,
  6781. heavy_division_sign: !0,
  6782. clock730: !0,
  6783. clock530: !0,
  6784. capital_abcd: !0,
  6785. mailbox_with_no_mail: !0,
  6786. symbols: !0,
  6787. aerial_tramway: !0,
  6788. clock830: !0,
  6789. clock630: !0,
  6790. abcd: !0,
  6791. mountain_cableway: !0,
  6792. koko: !0,
  6793. passport_control: !0,
  6794. "non-potable_water": !0,
  6795. suspension_railway: !0,
  6796. baggage_claim: !0,
  6797. no_bicycles: !0,
  6798. skull_crossbones: !0,
  6799. hugging: !0,
  6800. thinking: !0,
  6801. nerd: !0,
  6802. zipper_mouth: !0,
  6803. rolling_eyes: !0,
  6804. upside_down: !0,
  6805. slight_smile: !0,
  6806. middle_finger: !0,
  6807. writing_hand: !0,
  6808. dark_sunglasses: !0,
  6809. eye: !0,
  6810. man_in_suit: !0,
  6811. golfer: !0,
  6812. heart_exclamation: !0,
  6813. star_of_david: !0,
  6814. cross: !0,
  6815. "fleur-de-lis": !0,
  6816. atom: !0,
  6817. wheel_of_dharma: !0,
  6818. yin_yang: !0,
  6819. peace: !0,
  6820. star_and_crescent: !0,
  6821. orthodox_cross: !0,
  6822. biohazard: !0,
  6823. radioactive: !0,
  6824. place_of_worship: !0,
  6825. anger_right: !0,
  6826. menorah: !0,
  6827. om_symbol: !0,
  6828. coffin: !0,
  6829. gear: !0,
  6830. alembic: !0,
  6831. scales: !0,
  6832. crossed_swords: !0,
  6833. keyboard: !0,
  6834. shield: !0,
  6835. bed: !0,
  6836. shopping_bags: !0,
  6837. sleeping_accommodation: !0,
  6838. ballot_box: !0,
  6839. compression: !0,
  6840. wastebasket: !0,
  6841. file_cabinet: !0,
  6842. trackball: !0,
  6843. printer: !0,
  6844. joystick: !0,
  6845. hole: !0,
  6846. candle: !0,
  6847. prayer_beads: !0,
  6848. camera_with_flash: !0,
  6849. amphora: !0,
  6850. label: !0,
  6851. flag_black: !0,
  6852. flag_white: !0,
  6853. film_frames: !0,
  6854. control_knobs: !0,
  6855. level_slider: !0,
  6856. thermometer: !0,
  6857. airplane_arriving: !0,
  6858. airplane_departure: !0,
  6859. railway_track: !0,
  6860. motorway: !0,
  6861. synagogue: !0,
  6862. mosque: !0,
  6863. kaaba: !0,
  6864. stadium: !0,
  6865. desert: !0,
  6866. classical_building: !0,
  6867. cityscape: !0,
  6868. camping: !0,
  6869. bow_and_arrow: !0,
  6870. rosette: !0,
  6871. volleyball: !0,
  6872. medal: !0,
  6873. reminder_ribbon: !0,
  6874. popcorn: !0,
  6875. champagne: !0,
  6876. hot_pepper: !0,
  6877. burrito: !0,
  6878. taco: !0,
  6879. hotdog: !0,
  6880. shamrock: !0,
  6881. comet: !0,
  6882. turkey: !0,
  6883. scorpion: !0,
  6884. lion_face: !0,
  6885. crab: !0,
  6886. spider_web: !0,
  6887. spider: !0,
  6888. chipmunk: !0,
  6889. wind_blowing_face: !0,
  6890. fog: !0,
  6891. play_pause: !0,
  6892. track_previous: !0,
  6893. track_next: !0,
  6894. beach_umbrella: !0,
  6895. chains: !0,
  6896. pick: !0,
  6897. stopwatch: !0,
  6898. ferry: !0,
  6899. mountain: !0,
  6900. shinto_shrine: !0,
  6901. ice_skate: !0,
  6902. skier: !0,
  6903. flag_ac: !0,
  6904. flag_ad: !0,
  6905. flag_ae: !0,
  6906. flag_af: !0,
  6907. flag_ag: !0,
  6908. flag_ai: !0,
  6909. flag_al: !0,
  6910. flag_am: !0,
  6911. "flag-ao": !0,
  6912. "flag-aq": !0,
  6913. "flag-ar": !0,
  6914. "flag-as": !0,
  6915. "flag-at": !0,
  6916. "flag-au": !0,
  6917. "flag-aw": !0,
  6918. "flag-ax": !0,
  6919. "flag-az": !0,
  6920. "flag-ba": !0,
  6921. "flag-bb": !0,
  6922. "flag-bd": !0,
  6923. "flag-be": !0,
  6924. "flag-bf": !0,
  6925. "flag-bg": !0,
  6926. "flag-bh": !0,
  6927. "flag-bi": !0,
  6928. "flag-bj": !0,
  6929. "flag-bl": !0,
  6930. "flag-bm": !0,
  6931. "flag-bn": !0,
  6932. "flag-bo": !0,
  6933. "flag-bq": !0,
  6934. "flag-br": !0,
  6935. "flag-bs": !0,
  6936. "flag-bt": !0,
  6937. "flag-bv": !0,
  6938. "flag-bw": !0,
  6939. "flag-by": !0,
  6940. "flag-bz": !0,
  6941. "flag-ca": !0,
  6942. "flag-cc": !0,
  6943. "flag-cd": !0,
  6944. "flag-cf": !0,
  6945. "flag-cg": !0,
  6946. "flag-ch": !0,
  6947. "flag-ci": !0,
  6948. "flag-ck": !0,
  6949. "flag-cl": !0,
  6950. "flag-cm": !0,
  6951. "flag-cn": !0,
  6952. "flag-co": !0,
  6953. "flag-cp": !0,
  6954. "flag-cr": !0,
  6955. "flag-cu": !0,
  6956. "flag-cv": !0,
  6957. "flag-cw": !0,
  6958. "flag-cx": !0,
  6959. "flag-cy": !0,
  6960. "flag-cz": !0,
  6961. "flag-de": !0,
  6962. "flag-dg": !0,
  6963. "flag-dj": !0,
  6964. "flag-dk": !0,
  6965. "flag-dm": !0,
  6966. "flag-do": !0,
  6967. "flag-dz": !0,
  6968. "flag-ea": !0,
  6969. "flag-ec": !0,
  6970. "flag-ee": !0,
  6971. "flag-eg": !0,
  6972. "flag-eh": !0,
  6973. "flag-er": !0,
  6974. "flag-es": !0,
  6975. "flag-et": !0,
  6976. "flag-eu": !0,
  6977. "flag-fi": !0,
  6978. "flag-fj": !0,
  6979. "flag-fk": !0,
  6980. "flag-fm": !0,
  6981. "flag-fo": !0,
  6982. "flag-fr": !0,
  6983. "flag-ga": !0,
  6984. "flag-gb": !0,
  6985. "flag-gd": !0,
  6986. "flag-ge": !0,
  6987. "flag-gf": !0,
  6988. "flag-gg": !0,
  6989. "flag-gh": !0,
  6990. "flag-gi": !0,
  6991. "flag-gl": !0,
  6992. "flag-gm": !0,
  6993. "flag-gn": !0,
  6994. "flag-gp": !0,
  6995. "flag-gq": !0,
  6996. "flag-gr": !0,
  6997. "flag-gs": !0,
  6998. "flag-gt": !0,
  6999. "flag-gu": !0,
  7000. "flag-gw": !0,
  7001. "flag-gy": !0,
  7002. "flag-hk": !0,
  7003. "flag-hm": !0,
  7004. "flag-hn": !0,
  7005. "flag-hr": !0,
  7006. "flag-ht": !0,
  7007. "flag-hu": !0,
  7008. "flag-ic": !0,
  7009. "flag-id": !0,
  7010. "flag-ie": !0,
  7011. "flag-il": !0,
  7012. "flag-im": !0,
  7013. "flag-in": !0,
  7014. "flag-io": !0,
  7015. "flag-iq": !0,
  7016. "flag-ir": !0,
  7017. "flag-is": !0,
  7018. "flag-it": !0,
  7019. "flag-je": !0,
  7020. "flag-jm": !0,
  7021. "flag-jo": !0,
  7022. "flag-jp": !0,
  7023. "flag-ke": !0,
  7024. "flag-kg": !0,
  7025. "flag-kh": !0,
  7026. "flag-ki": !0,
  7027. "flag-km": !0,
  7028. "flag-kn": !0,
  7029. "flag-kp": !0,
  7030. "flag-kr": !0,
  7031. "flag-kw": !0,
  7032. "flag-ky": !0,
  7033. "flag-kz": !0,
  7034. "flag-la": !0,
  7035. "flag-lb": !0,
  7036. "flag-lc": !0,
  7037. "flag-li": !0,
  7038. "flag-lk": !0,
  7039. "flag-lr": !0,
  7040. "flag-ls": !0,
  7041. "flag-lt": !0,
  7042. "flag-lu": !0,
  7043. "flag-lv": !0,
  7044. "flag-ly": !0,
  7045. "flag-ma": !0,
  7046. "flag-mc": !0,
  7047. "flag-md": !0,
  7048. "flag-me": !0,
  7049. "flag-mf": !0,
  7050. "flag-mg": !0,
  7051. "flag-mh": !0,
  7052. "flag-mk": !0,
  7053. "flag-ml": !0,
  7054. "flag-mm": !0,
  7055. "flag-mn": !0,
  7056. "flag-mo": !0,
  7057. "flag-mp": !0,
  7058. "flag-mq": !0,
  7059. "flag-mr": !0,
  7060. "flag-ms": !0,
  7061. "flag-mt": !0,
  7062. "flag-mu": !0,
  7063. "flag-mv": !0,
  7064. "flag-mw": !0,
  7065. "flag-mx": !0,
  7066. "flag-my": !0,
  7067. "flag-mz": !0,
  7068. "flag-na": !0,
  7069. "flag-nc": !0,
  7070. "flag-ne": !0,
  7071. "flag-nf": !0,
  7072. "flag-ng": !0,
  7073. "flag-ni": !0,
  7074. "flag-nl": !0,
  7075. "flag-no": !0,
  7076. "flag-np": !0,
  7077. "flag-nr": !0,
  7078. "flag-nu": !0,
  7079. "flag-nz": !0,
  7080. "flag-om": !0,
  7081. "flag-pa": !0,
  7082. "flag-pe": !0,
  7083. "flag-pf": !0,
  7084. "flag-pg": !0,
  7085. "flag-ph": !0,
  7086. "flag-pk": !0,
  7087. "flag-pl": !0,
  7088. "flag-pm": !0,
  7089. "flag-pn": !0,
  7090. "flag-pr": !0,
  7091. "flag-ps": !0,
  7092. "flag-pt": !0,
  7093. "flag-pw": !0,
  7094. "flag-py": !0,
  7095. "flag-qa": !0,
  7096. "flag-re": !0,
  7097. "flag-ro": !0,
  7098. "flag-rs": !0,
  7099. "flag-ru": !0,
  7100. "flag-rw": !0,
  7101. "flag-sa": !0,
  7102. "flag-sb": !0,
  7103. "flag-sc": !0,
  7104. "flag-sd": !0,
  7105. "flag-se": !0,
  7106. "flag-sg": !0,
  7107. "flag-sh": !0,
  7108. "flag-si": !0,
  7109. "flag-sj": !0,
  7110. "flag-sk": !0,
  7111. "flag-sl": !0,
  7112. "flag-sm": !0,
  7113. "flag-sn": !0,
  7114. "flag-so": !0,
  7115. "flag-sr": !0,
  7116. "flag-ss": !0,
  7117. "flag-st": !0,
  7118. "flag-sv": !0,
  7119. "flag-sx": !0,
  7120. "flag-sy": !0,
  7121. "flag-sz": !0,
  7122. "flag-ta": !0,
  7123. "flag-tc": !0,
  7124. "flag-td": !0,
  7125. "flag-tf": !0,
  7126. "flag-tg": !0,
  7127. "flag-th": !0,
  7128. "flag-tj": !0,
  7129. "flag-tk": !0,
  7130. "flag-tl": !0,
  7131. "flag-tm": !0,
  7132. "flag-tn": !0,
  7133. "flag-to": !0,
  7134. "flag-tr": !0,
  7135. "flag-tt": !0,
  7136. "flag-tv": !0,
  7137. "flag-tw": !0,
  7138. "flag-tz": !0,
  7139. "flag-ua": !0,
  7140. "flag-ug": !0,
  7141. "flag-um": !0,
  7142. "flag-us": !0,
  7143. "flag-uy": !0,
  7144. "flag-uz": !0,
  7145. "flag-va": !0,
  7146. "flag-vc": !0,
  7147. "flag-ve": !0,
  7148. "flag-vg": !0,
  7149. "flag-vi": !0,
  7150. "flag-vn": !0,
  7151. flag_vu: !0,
  7152. flag_wf: !0,
  7153. flag_ws: !0,
  7154. flag_xk: !0,
  7155. flag_ye: !0,
  7156. flag_yt: !0,
  7157. flag_za: !0,
  7158. flag_zm: !0,
  7159. flag_zw: !0,
  7160. black_heart: !0,
  7161. speech_left: !0,
  7162. egg: !0,
  7163. octagonal_sign: !0,
  7164. spades: !0,
  7165. hearts: !0,
  7166. diamonds: !0,
  7167. clubs: !0,
  7168. drum: !0,
  7169. left_right_arrow: !0,
  7170. tm: !0,
  7171. "100": !0
  7172. };
  7173.  
  7174. addConfigOptions(
  7175. "interface", {
  7176. _name: "Interface",
  7177. emoji: {
  7178. name: "Add Emoji Replacement to Chat",
  7179. default: true,
  7180. _type: "boolean"
  7181. }
  7182. }
  7183. );
  7184.  
  7185. d20plus.enhanceChat = () => {
  7186. d20plus.log("Enhancing chat");
  7187. const tc = d20.textchat.$textarea;
  7188. $("#textchat-input").off("click", "button")
  7189. $("#textchat-input").on("click", "button", function () {
  7190. if (!window.is_gm || d20plus.getCfgVal("interface", "emoji")) {
  7191. tc.val(tc.val().replace(/(:\w*?:)/g, (m0, m1) => {
  7192. const clean = m1.replace(/:/g, "");
  7193. return d20plus.chat.emojiIndex && d20plus.chat.emojiIndex[clean] ? `[${clean}](https://github.com/TheGiddyLimit/emoji-dump/raw/master/out/${clean}.png)` : m1;
  7194. }));
  7195. }
  7196. const toSend = $.trim(tc.val());
  7197. d20.textchat.doChatInput(toSend);
  7198. tc.val("").focus();
  7199. });
  7200. };
  7201. };
  7202.  
  7203. SCRIPT_EXTENSIONS.push(betteR20Emoji);
  7204.  
  7205. var betteR20Base = function () {
  7206. addConfigOptions("token", {
  7207. "_name": "Tokens",
  7208. "enhanceStatus": {
  7209. "name": "Use Custom Status Icons",
  7210. "default": true,
  7211. "_type": "boolean"
  7212. },
  7213. "statusSheetUrl": {
  7214. "name": `Custom Status Spritesheet Url (<a style="color: blue" href="https://app.roll20.net/images/statussheet.png" target="_blank">Original</a>)`,
  7215. "default": "https://raw.githubusercontent.com/TheGiddyLimit/5etoolsR20/master/img/statussheet.png",
  7216. "_type": "String"
  7217. },
  7218. "statusSheetSmallUrl": {
  7219. "name": `Custom Status Spritesheet (Small) Url (<a style="color: blue" href="https://app.roll20.net/images/statussheet_small.png" target="_blank">Original</a>)`,
  7220. "default": "https://raw.githubusercontent.com/TheGiddyLimit/5etoolsR20/master/img/statussheet_small.png",
  7221. "_type": "String"
  7222. }
  7223. }
  7224. );
  7225. addConfigOptions("canvas", {
  7226. "_name": "Canvas",
  7227. "halfGridSnap": {
  7228. "name": "Snap to Half-Grid",
  7229. "default": false,
  7230. "_type": "boolean"
  7231. },
  7232. "scaleNamesStatuses": {
  7233. "name": "Scaled Names and Status Icons",
  7234. "default": true,
  7235. "_type": "boolean"
  7236. }
  7237. }
  7238. );
  7239.  
  7240. const d20plus = {
  7241. // EXTERNAL SCRIPTS ////////////////////////////////////////////////////////////////////////////////////////////
  7242. scriptsLoaded: false,
  7243. scripts: [
  7244. {name: "listjs", url: "https://raw.githubusercontent.com/javve/list.js/v1.5.0/dist/list.min.js"}
  7245. ],
  7246.  
  7247. addScripts: (onLoadFunction) => {
  7248. d20plus.log("Add JS");
  7249. const onEachLoadFunction = function (name, url, js) {
  7250. try {
  7251. window.eval(js);
  7252. d20plus.log(`JS [${name}] Loaded`);
  7253. } catch (e) {
  7254. d20plus.log(`Error loading ${name}`);
  7255. }
  7256. };
  7257. d20plus.chainLoad(d20plus.scripts, 0, onEachLoadFunction, onLoadFunction);
  7258. },
  7259.  
  7260. chainLoad: (toLoads, index, onEachLoadFunction, onFinalLoadFunction) => {
  7261. const toLoad = toLoads[index];
  7262. // on loading the last item, run onLoadFunction
  7263. let retries = 3;
  7264. function withRetries () {
  7265. $.ajax({
  7266. type: "GET",
  7267. url: toLoad.url + d20plus.getAntiCacheSuffix() + retries,
  7268. success: function (data) {
  7269. if (index === toLoads.length - 1) {
  7270. onEachLoadFunction(toLoad.name, toLoad.url, data);
  7271. onFinalLoadFunction();
  7272. } else {
  7273. onEachLoadFunction(toLoad.name, toLoad.url, data);
  7274. d20plus.chainLoad(toLoads, index + 1, onEachLoadFunction, onFinalLoadFunction);
  7275. }
  7276. },
  7277. error: function (resp, qq, pp) {
  7278. if (resp && resp.status === 500 && retries-- > 0) {
  7279. console.error(resp, qq, pp);
  7280. d20plus.log(`Error loading ${toLoad.name}; retrying`);
  7281. setTimeout(() => {
  7282. withRetries();
  7283. }, 500);
  7284. } else {
  7285. console.error(resp, qq, pp);
  7286. d20plus.log(`Error loading ${toLoad.name}`);
  7287. }
  7288. }
  7289. });
  7290. }
  7291. withRetries();
  7292. },
  7293.  
  7294. // UTILITIES ///////////////////////////////////////////////////////////////////////////////////////////////////
  7295. log: (arg) => {
  7296. console.log("%cD20Plus > ", "color: #3076b9; font-size: large", arg);
  7297. },
  7298.  
  7299. ascSort: (a, b) => {
  7300. if (b === a) return 0;
  7301. return b < a ? 1 : -1;
  7302. },
  7303.  
  7304. chatTag: (message) => {
  7305. d20.textchat.incoming(false, ({
  7306. who: "system",
  7307. type: "system",
  7308. 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;">
  7309. ${message}
  7310. <br>
  7311. <br>
  7312. Need help? Join our <a style="color: white;" href="https://discord.gg/Zk34CUZ">Discord</a>.
  7313. <br>
  7314. <br>
  7315. <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>
  7316. </span>`
  7317. }));
  7318. },
  7319.  
  7320. addCSS: (sheet, selector, rules) => {
  7321. const index = sheet.cssRules.length;
  7322. if ("insertRule" in sheet) {
  7323. sheet.insertRule(selector + "{" + rules + "}", index);
  7324. } else if ("addRule" in sheet) {
  7325. sheet.addRule(selector, rules, index);
  7326. }
  7327. },
  7328.  
  7329. addAllCss: () => {
  7330. d20plus.log("Add CSS");
  7331. const targetSheet = window.document.styleSheets[window.document.styleSheets.length - 1];
  7332. _.each(d20plus.baseCssRules, function (r) {
  7333. d20plus.addCSS(targetSheet, r.s, r.r);
  7334. });
  7335. if (!window.is_gm) {
  7336. _.each(d20plus.baseCssRulesPlayer, function (r) {
  7337. d20plus.addCSS(targetSheet, r.s, r.r);
  7338. });
  7339. }
  7340. _.each(d20plus.cssRules, function (r) {
  7341. d20plus.addCSS(targetSheet, r.s, r.r);
  7342. });
  7343. },
  7344.  
  7345. getAntiCacheSuffix: () => {
  7346. return "?" + (new Date()).getTime();
  7347. },
  7348.  
  7349. generateRowId: () => {
  7350. return window.generateUUID().replace(/_/g, "Z");
  7351. },
  7352.  
  7353. randomRoll: (roll, success, error) => {
  7354. d20.textchat.diceengine.process(roll, success, error);
  7355. },
  7356.  
  7357. randomInt: (int) => {
  7358. // Return random integer between [0,int)
  7359. return d20.textchat.diceengine.random(int);
  7360. },
  7361.  
  7362. getJournalFolderObj: () => {
  7363. d20.journal.refreshJournalList();
  7364. let journalFolder = d20.Campaign.get("journalfolder");
  7365. if (journalFolder === "") {
  7366. d20.journal.addFolderToFolderStructure("Characters");
  7367. d20.journal.refreshJournalList();
  7368. journalFolder = d20.Campaign.get("journalfolder");
  7369. }
  7370. return JSON.parse(journalFolder);
  7371. },
  7372.  
  7373. getCleanText: (str) => {
  7374. const check = jQuery.parseHTML(str);
  7375. if (check.length === 1 && check[0].constructor === Text) {
  7376. return str;
  7377. }
  7378. const $ele = $(str);
  7379. $ele.find("p, li, br").append("\n\n");
  7380. return $ele.text().replace(/[ ]+/g, " ");
  7381. },
  7382.  
  7383. // CONFIG //////////////////////////////////////////////////////////////////////////////////////////////////////
  7384. config: {},
  7385.  
  7386. loadConfigFailed: false,
  7387.  
  7388. loadConfig: (nextFn) => {
  7389. d20plus.log("Reading Config");
  7390. let configHandout = d20plus.getConfigHandout();
  7391.  
  7392. if (!configHandout) {
  7393. d20plus.log("No config found! Initialising new config...");
  7394. d20plus.makeDefaultConfig(doLoad);
  7395. } else {
  7396. doLoad();
  7397. }
  7398.  
  7399. function doLoad () {
  7400. configHandout = d20plus.getConfigHandout();
  7401. if (configHandout) {
  7402. configHandout.view.render();
  7403. configHandout._getLatestBlob("gmnotes", function (gmnotes) {
  7404. try {
  7405. const decoded = decodeURIComponent(gmnotes);
  7406.  
  7407. d20plus.config = JSON.parse(decoded);
  7408.  
  7409. d20plus.log("Config Loaded:");
  7410. d20plus.log(d20plus.config);
  7411. nextFn();
  7412. } catch (e) {
  7413. if (!d20plus.loadConfigFailed) {
  7414. // prevent infinite loops
  7415. d20plus.loadConfigFailed = true;
  7416.  
  7417. d20plus.log("Corrupted config! Rebuilding...");
  7418. d20plus.makeDefaultConfig(() => {
  7419. d20plus.loadConfig(nextFn)
  7420. });
  7421. } else {
  7422. // if the config fails, continue to load anyway
  7423. nextFn();
  7424. }
  7425. }
  7426. });
  7427. } else {
  7428. d20plus.log("Failed to create config handout!");
  7429. nextFn();
  7430. }
  7431. }
  7432. },
  7433.  
  7434. makeDefaultConfig: (nextFn) => {
  7435. d20.Campaign.handouts.create({
  7436. name: CONFIG_HANDOUT
  7437. }, {
  7438. success: function (handout) {
  7439. 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.";
  7440.  
  7441. // default settings
  7442. // token settings mimic official content; other settings as vanilla as possible
  7443. const gmnotes = JSON.stringify(d20plus.getDefaultConfig());
  7444.  
  7445. handout.updateBlobs({notes: notecontents, gmnotes: gmnotes});
  7446. handout.save({notes: (new Date).getTime(), inplayerjournals: ""});
  7447.  
  7448. if (nextFn) nextFn();
  7449. }
  7450. });
  7451. },
  7452.  
  7453. getConfigHandout: () => {
  7454. d20plus.getJournalFolderObj(); // ensure journal init
  7455.  
  7456. return d20.Campaign.handouts.models.find(function (handout) {
  7457. return handout.attributes.name === CONFIG_HANDOUT;
  7458. });
  7459. },
  7460.  
  7461. getCfgKey: (group, val) => {
  7462. if (val === undefined || d20plus.config[group] === undefined) return undefined;
  7463. const gr = d20plus.config[group];
  7464. for (const key of Object.keys(d20plus.config[group])) {
  7465. if (gr[key] !== undefined && gr[key] === val) {
  7466. return key;
  7467. }
  7468. }
  7469. return undefined;
  7470. },
  7471.  
  7472. getRawCfgVal: (group, key) => {
  7473. if (d20plus.config[group] === undefined) return undefined;
  7474. if (d20plus.config[group][key] === undefined) return undefined;
  7475. return d20plus.config[group][key];
  7476. },
  7477.  
  7478. getCfgVal: (group, key) => {
  7479. if (d20plus.config[group] === undefined) return undefined;
  7480. if (d20plus.config[group][key] === undefined) return undefined;
  7481. if (CONFIG_OPTIONS[group][key]._type === "_SHEET_ATTRIBUTE") {
  7482. if (!NPC_SHEET_ATTRIBUTES[d20plus.config[group][key]]) return undefined;
  7483. return NPC_SHEET_ATTRIBUTES[d20plus.config[group][key]][d20plus.sheet];
  7484. }
  7485. if (CONFIG_OPTIONS[group][key]._type === "_SHEET_ATTRIBUTE_PC") {
  7486. if (!PC_SHEET_ATTRIBUTES[d20plus.config[group][key]]) return undefined;
  7487. return PC_SHEET_ATTRIBUTES[d20plus.config[group][key]][d20plus.sheet];
  7488. }
  7489. return d20plus.config[group][key];
  7490. },
  7491.  
  7492. getCfgDefaultVal: (group, key) => {
  7493. if (CONFIG_OPTIONS[group] === undefined) return undefined;
  7494. if (CONFIG_OPTIONS[group][key] === undefined) return undefined;
  7495. return CONFIG_OPTIONS[group][key].default
  7496. },
  7497.  
  7498. getCfgEnumVals: (group, key) => {
  7499. if (CONFIG_OPTIONS[group] === undefined) return undefined;
  7500. if (CONFIG_OPTIONS[group][key] === undefined) return undefined;
  7501. return CONFIG_OPTIONS[group][key]._values
  7502. },
  7503.  
  7504. getDefaultConfig: () => {
  7505. const outCpy = {};
  7506. $.each(CONFIG_OPTIONS, (sectK, sect) => {
  7507. outCpy[sectK] = outCpy[sectK] || {};
  7508. $.each(sect, (k, data) => {
  7509. if (!k.startsWith("_")) {
  7510. outCpy[sectK][k] = data.default;
  7511. }
  7512. });
  7513. });
  7514. return outCpy;
  7515. },
  7516.  
  7517. // Helpful for checking if a boolean option is set even if false
  7518. hasCfgVal: (group, key) => {
  7519. if (d20plus.config[group] === undefined) return undefined;
  7520. return d20plus.config[group][key] !== undefined;
  7521. },
  7522.  
  7523. setCfgVal: (group, key, val) => {
  7524. if (d20plus.config[group] === undefined) d20plus.config[group] = {};
  7525. d20plus.config[group][key] = val;
  7526. },
  7527.  
  7528. makeTabPane: ($addTo, headers, content) => {
  7529. if (headers.length !== content.length) throw new Error("Tab header and content length were not equal!");
  7530.  
  7531. if ($addTo.attr("hastabs") !== "YES") {
  7532. const $tabBar = $(`<ul class="nav nav-tabs"/>`);
  7533.  
  7534. const tabList = [];
  7535. const paneList = [];
  7536. const $tabPanes = $(`<div class="tabcontent"/>`);
  7537.  
  7538. $.each(content, (i, e) => {
  7539. const toAdd = $(`<div class="plustab${i} tab-pane" ${i === 0 ? "" : `style="display: none"`}/>`);
  7540. toAdd.append(e);
  7541. paneList[i] = toAdd;
  7542. $tabPanes.append(toAdd);
  7543. });
  7544.  
  7545. $.each(headers, (i, e) => {
  7546. const toAdd = $(`<li ${i === 0 ? `class="active"` : ""}><a data-tab="plustab${i}" href="#">${e}</a></li>`).on("click", () => {
  7547. paneList.forEach((p, i2) => {
  7548. if (i2 === i) {
  7549. tabList[i2].addClass("active");
  7550. paneList[i2].show();
  7551. } else {
  7552. tabList[i2].removeClass("active");
  7553. paneList[i2].hide();
  7554. }
  7555. });
  7556. });
  7557. tabList[i] = (toAdd);
  7558. $tabBar.append(toAdd);
  7559. });
  7560.  
  7561. $addTo
  7562. .append($tabBar)
  7563. .append($tabPanes);
  7564.  
  7565. $addTo.attr("hastabs", "YES");
  7566. }
  7567. },
  7568.  
  7569. openConfigEditor: () => {
  7570. const cEdit = $("#d20plus-configeditor");
  7571. cEdit.dialog("open");
  7572.  
  7573. if (cEdit.attr("hastabs") !== "YES") {
  7574. cEdit.attr("hastabs", "YES");
  7575. const appendTo = $(`<div/>`);
  7576. cEdit.prepend(appendTo);
  7577.  
  7578. const configFields = {};
  7579.  
  7580. const sortedKeys = Object.keys(CONFIG_OPTIONS).sort((a, b) => d20plus.ascSort(CONFIG_OPTIONS[a]._name, CONFIG_OPTIONS[b]._name));
  7581. const tabList = sortedKeys.map(k => CONFIG_OPTIONS[k]._name);
  7582. const contentList = sortedKeys.map(k => makeTab(k));
  7583.  
  7584. function makeTab (cfgK) {
  7585. const cfgGroup = CONFIG_OPTIONS[cfgK];
  7586. configFields[cfgK] = {};
  7587.  
  7588. const content = $(`
  7589. <div class="config-table-wrapper">
  7590. <table class="config-table">
  7591. <thead><tr><th>Property</th><th>Value</th></tr></thead>
  7592. <tbody></tbody>
  7593. </table>
  7594. </div>
  7595. `);
  7596. const tbody = content.find(`tbody`);
  7597.  
  7598. const sortedTabKeys = Object.keys(cfgGroup).filter(k => !k.startsWith("_"));
  7599. sortedTabKeys.forEach((grpK, idx) => {
  7600. const prop = cfgGroup[grpK];
  7601.  
  7602. // IDs only used for label linking
  7603. const toAdd = $(`<tr><td><label for="conf_field_${idx}" class="config-name">${prop.name}</label></td></tr>`);
  7604.  
  7605. // 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.
  7606. switch (prop._type) {
  7607. case "boolean": {
  7608. const field = $(`<input type="checkbox" id="conf_field_${idx}" ${d20plus.getCfgVal(cfgK, grpK) ? `checked` : ""}>`);
  7609.  
  7610. configFields[cfgK][grpK] = () => {
  7611. return field.prop("checked")
  7612. };
  7613.  
  7614. const td = $(`<td/>`).append(field);
  7615. toAdd.append(td);
  7616. break;
  7617. }
  7618. case "String": {
  7619. const curr = d20plus.getCfgVal(cfgK, grpK) || "";
  7620. const def = d20plus.getCfgDefaultVal(cfgK, grpK) || "";
  7621. const field = $(`<input id="conf_field_${idx}" value="${curr}" ${def ? `placeholder="Default: ${def}"` : ""}>`);
  7622.  
  7623. configFields[cfgK][grpK] = () => {
  7624. return field.val() ? field.val().trim() : "";
  7625. };
  7626.  
  7627. const td = $(`<td/>`).append(field);
  7628. toAdd.append(td);
  7629. break;
  7630. }
  7631. case "_SHEET_ATTRIBUTE_PC":
  7632. case "_SHEET_ATTRIBUTE": {
  7633. const DICT = prop._type === "_SHEET_ATTRIBUTE" ? NPC_SHEET_ATTRIBUTES : PC_SHEET_ATTRIBUTES;
  7634. const sortedNpcsAttKeys = Object.keys(DICT).sort((at1, at2) => d20plus.ascSort(DICT[at1].name, DICT[at2].name));
  7635. 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>`);
  7636. const cur = d20plus.getCfgVal(cfgK, grpK);
  7637. if (cur !== undefined) {
  7638. field.val(cur);
  7639. }
  7640.  
  7641. configFields[cfgK][grpK] = () => {
  7642. return field.val()
  7643. };
  7644.  
  7645. const td = $(`<td/>`).append(field);
  7646. toAdd.append(td);
  7647. break;
  7648. }
  7649. case "integer": {
  7650. const def = d20plus.getCfgDefaultVal(cfgK, grpK);
  7651. const field = $(`<input id="conf_field_${idx}" type="number" value="${d20plus.getCfgVal(cfgK, grpK)}" ${def != null ? `placeholder="Default: ${def}"` : ""}>`);
  7652.  
  7653. configFields[cfgK][grpK] = () => {
  7654. return Number(field.val());
  7655. };
  7656.  
  7657. const td = $(`<td/>`).append(field);
  7658. toAdd.append(td);
  7659. break;
  7660. }
  7661. case "_FORMULA": {
  7662. 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>`);
  7663.  
  7664. const cur = d20plus.getCfgVal(cfgK, grpK);
  7665. if (cur !== undefined) {
  7666. $field.val(cur);
  7667. }
  7668.  
  7669. configFields[cfgK][grpK] = () => {
  7670. return $field.val();
  7671. };
  7672.  
  7673. const td = $(`<td/>`).append($field);
  7674. toAdd.append(td);
  7675. break;
  7676. }
  7677. case "_WHISPERMODE": {
  7678. const $field = $(`<select id="conf_field_${idx}" class="cfg_grp_${cfgK}" data-item="${grpK}">${d20plus.whisperModes.map(mode => `<option value="${mode}">${mode}</option>`)}</select>`);
  7679.  
  7680. const cur = d20plus.getCfgVal(cfgK, grpK);
  7681. if (cur !== undefined) {
  7682. $field.val(cur);
  7683. }
  7684.  
  7685. configFields[cfgK][grpK] = () => {
  7686. return $field.val();
  7687. };
  7688.  
  7689. const td = $(`<td/>`).append($field);
  7690. toAdd.append(td);
  7691. break;
  7692. }
  7693. case "_ADVANTAGEMODE": {
  7694. const $field = $(`<select id="conf_field_${idx}" class="cfg_grp_${cfgK}" data-item="${grpK}">${d20plus.advantageModes.map(mode => `<option value="${mode}">${mode}</option>`)}</select>`);
  7695.  
  7696. const cur = d20plus.getCfgVal(cfgK, grpK);
  7697. if (cur !== undefined) {
  7698. $field.val(cur);
  7699. }
  7700.  
  7701. configFields[cfgK][grpK] = () => {
  7702. return $field.val();
  7703. };
  7704.  
  7705. const td = $(`<td/>`).append($field);
  7706. toAdd.append(td);
  7707. break;
  7708. }
  7709. case "_DAMAGEMODE": {
  7710. const $field = $(`<select id="conf_field_${idx}" class="cfg_grp_${cfgK}" data-item="${grpK}">${d20plus.damageModes.map(mode => `<option value="${mode}">${mode}</option>`)}</select>`);
  7711.  
  7712. const cur = d20plus.getCfgVal(cfgK, grpK);
  7713. if (cur !== undefined) {
  7714. $field.val(cur);
  7715. }
  7716.  
  7717. configFields[cfgK][grpK] = () => {
  7718. return $field.val();
  7719. };
  7720.  
  7721. const td = $(`<td/>`).append($field);
  7722. toAdd.append(td);
  7723. break;
  7724. }
  7725. case "_enum": { // for generic String enums not covered above
  7726. 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>`);
  7727.  
  7728. const cur = d20plus.getCfgVal(cfgK, grpK);
  7729. if (cur !== undefined) {
  7730. $field.val(cur);
  7731. } else {
  7732. const def = d20plus.getCfgDefaultVal(cfgK, grpK);
  7733. if (def !== undefined) {
  7734. $field.val(def);
  7735. }
  7736. }
  7737.  
  7738. configFields[cfgK][grpK] = () => {
  7739. return $field.val();
  7740. };
  7741.  
  7742. const td = $(`<td/>`).append($field);
  7743. toAdd.append(td);
  7744. break;
  7745. }
  7746. }
  7747. tbody.append(toAdd);
  7748. });
  7749.  
  7750. return content;
  7751. }
  7752.  
  7753. d20plus.makeTabPane(
  7754. appendTo,
  7755. tabList,
  7756. contentList
  7757. );
  7758.  
  7759. const saveButton = $(`#configsave`);
  7760. saveButton.unbind("click");
  7761. saveButton.bind("click", () => {
  7762. let handout = d20plus.getConfigHandout();
  7763. if (!handout) {
  7764. d20plus.makeDefaultConfig(doSave);
  7765. } else {
  7766. doSave();
  7767. }
  7768.  
  7769. function doSave () {
  7770. $.each(configFields, (cfgK, grp) => {
  7771. $.each(grp, (grpK, grpVField) => {
  7772. d20plus.setCfgVal(cfgK, grpK, grpVField());
  7773. })
  7774. });
  7775.  
  7776. const gmnotes = JSON.stringify(d20plus.config).replace(/%/g, "%25");
  7777. handout.updateBlobs({gmnotes: gmnotes});
  7778. handout.save({notes: (new Date).getTime()});
  7779.  
  7780. d20plus.log("Saved config");
  7781.  
  7782. d20plus.baseHandleConfigChange();
  7783. if (d20plus.handleConfigChange) d20plus.handleConfigChange();
  7784. }
  7785. });
  7786. }
  7787. },
  7788.  
  7789. _handleStatusTokenConfigChange: () => {
  7790. if (window.is_gm) {
  7791. if (d20plus.getCfgVal("token", "enhanceStatus")) {
  7792. const sheetUrl = d20plus.getCfgVal("token", "statusSheetUrl") || d20plus.getCfgDefaultVal("token", "statusSheetUrl");
  7793. const sheetSmallUrl = d20plus.getCfgVal("token", "statusSheetSmallUrl") || d20plus.getCfgDefaultVal("token", "statusSheetSmallUrl");
  7794.  
  7795. window.Campaign && window.Campaign.save({
  7796. "bR20cfg_statussheet": sheetUrl,
  7797. "bR20cfg_statussheet_small": sheetSmallUrl
  7798. });
  7799.  
  7800. d20.token_editor.statussheet.src = sheetUrl;
  7801. d20.token_editor.statussheet_small.src = sheetSmallUrl;
  7802. d20plus._removeStatusEffectEntries(); // clean up any old data
  7803. d20plus._addStatusEffectEntries();
  7804. } else {
  7805. window.Campaign && window.Campaign.save({
  7806. "bR20cfg_statussheet": "",
  7807. "bR20cfg_statussheet_small": ""
  7808. });
  7809.  
  7810. d20.token_editor.statussheet.src = "/images/statussheet.png";
  7811. d20.token_editor.statussheet_small.src = "/images/statussheet_small.png";
  7812. d20plus._removeStatusEffectEntries();
  7813. }
  7814. } else {
  7815. if (window.Campaign && window.Campaign.attributes && window.Campaign.attributes.bR20cfg_statussheet && window.Campaign.attributes.bR20cfg_statussheet_small) {
  7816. d20.token_editor.statussheet.src = window.Campaign.attributes.bR20cfg_statussheet;
  7817. d20.token_editor.statussheet_small.src = window.Campaign.attributes.bR20cfg_statussheet_small;
  7818. d20plus._addStatusEffectEntries();
  7819. } else {
  7820. d20.token_editor.statussheet.src = "/images/statussheet.png";
  7821. d20.token_editor.statussheet_small.src = "/images/statussheet_small.png";
  7822. d20plus._removeStatusEffectEntries();
  7823. }
  7824. }
  7825. },
  7826.  
  7827. baseHandleConfigChange: () => {
  7828. d20plus._handleStatusTokenConfigChange();
  7829. },
  7830.  
  7831. startPlayerConfigHandler: () => {
  7832. function handlePlayerCfg () {
  7833. d20plus.baseHandleConfigChange();
  7834. if (d20plus.handleConfigChange) d20plus.handleConfigChange();
  7835. }
  7836.  
  7837. // every 5 seconds, poll and apply any config changes the GM might have made
  7838. if (!window.is_gm) {
  7839. setInterval(() => {
  7840. handlePlayerCfg();
  7841. }, 5000);
  7842. }
  7843. handlePlayerCfg();
  7844. },
  7845.  
  7846. // SETTINGS TOOLS //////////////////////////////////////////////////////////////////////////////////////////////
  7847. tools: [
  7848. {
  7849. name: "Journal Cleaner",
  7850. desc: "Quickly select and delete journal items from the root folder, useful for cleaning up loose items after deleting a folder.",
  7851. html: `
  7852. <div id="d20plus-quickdelete" title="Journal Root Cleaner">
  7853. <p>A list of characters and handouts in the journal folder root, which allows them to be quickly deleted.</p>
  7854. <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>
  7855. <div id="delete-list-container">
  7856. <input class="search" autocomplete="off" placeholder="Search list..." style="width: 100%;">
  7857. <br><br>
  7858. <ul class="list deletelist" style="max-height: 600px; overflow-y: scroll; display: block; margin: 0;"></ul>
  7859. </div>
  7860. </div>;
  7861. `,
  7862. dialogFn: () => {
  7863. $("#d20plus-quickdelete").dialog({
  7864. autoOpen: false,
  7865. resizable: true,
  7866. width: 800,
  7867. height: 650,
  7868. });
  7869. },
  7870. openFn: () => {
  7871. const $win = $("#d20plus-quickdelete");
  7872. $win.dialog("open");
  7873.  
  7874. const journal = d20plus.getJournalFolderObj();
  7875. const rootItems = [];
  7876. journal.forEach(it => {
  7877. if (it.i) return; // skip folders
  7878. const handout = d20.Campaign.handouts.get(it);
  7879. if (handout && (handout.get("name") === CONFIG_HANDOUT || handout.get("name") === ART_HANDOUT)) return; // skip 5etools handouts
  7880. const character = d20.Campaign.characters.get(it);
  7881. if (handout) rootItems.push({type: "handouts", id: it, name: handout.get("name")});
  7882. if (character) rootItems.push({type: "characters", id: it, name: character.get("name")});
  7883. });
  7884.  
  7885. const $delList = $win.find(`.list`);
  7886. $delList.empty();
  7887.  
  7888. rootItems.forEach((it, i) => {
  7889. $delList.append(`
  7890. <label class="import-cb-label">
  7891. <input type="checkbox" data-listid="${i}">
  7892. <span class="name">${it.name}</span>
  7893. </label>
  7894. `);
  7895. });
  7896.  
  7897. // init list library
  7898. const delList = new List("delete-list-container", {
  7899. valueNames: ["name"],
  7900. listClass: "deletelist"
  7901. });
  7902.  
  7903. const $cbAll = $("#deletelist-selectall");
  7904. $cbAll.unbind("click");
  7905. $cbAll.prop("checked", false);
  7906. $cbAll.bind("click", function () {
  7907. d20plus.importer._importToggleSelectAll(delList, $cbAll);
  7908. });
  7909.  
  7910. const $btnDel = $(`#quickdelete-btn-submit`);
  7911. $btnDel.off("click");
  7912. $btnDel.on("click", () => {
  7913. if (confirm("Delete selected?")) {
  7914. delList.items.forEach(it => Array.prototype.forEach.call(it.elm.children, (e) => {
  7915. const $e = $(e);
  7916. if ($e.is("input") && $e.prop("checked")) {
  7917. const dataIndex = parseInt($e.data("listid"));
  7918. const toDel = rootItems[dataIndex];
  7919. d20.Campaign[toDel.type].get(toDel.id).destroy();
  7920. }
  7921. }));
  7922. $win.dialog("close");
  7923. $("#journalfolderroot").trigger("change");
  7924. }
  7925. });
  7926. }
  7927. },
  7928. {
  7929. name: "SVG Draw",
  7930. desc: "Paste SVG data as text to automatically draw the paths.",
  7931. html: `
  7932. <div id="d20plus-svgdraw" title="SVG Drawing Tool">
  7933. <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>
  7934. <p>Line width (px; default values are 1, 3, 5, 8, 14): <input name="stroke-width" placeholder="5" value="5" type="number"></p>
  7935. <textarea rows="10" cols="100" placeholder="Paste SVG data here"></textarea>
  7936. <br>
  7937. <button class="btn">Draw</button>
  7938. </div>
  7939. `,
  7940. dialogFn: () => {
  7941. $("#d20plus-svgdraw").dialog({
  7942. autoOpen: false,
  7943. resizable: true,
  7944. width: 800,
  7945. height: 650,
  7946. });
  7947. },
  7948. openFn: () => {
  7949. // adapted from `d20.engine.finishCurrentPolygon`
  7950. function addShape(path, pathStroke, strokeWidth) {
  7951. let i = d20.engine.convertAbsolutePathStringtoFabric(path);
  7952. i = _.extend(i, {
  7953. strokeWidth: strokeWidth,
  7954. fill: "transparent",
  7955. stroke: pathStroke,
  7956. path: JSON.parse(i.path)
  7957. });
  7958. d20.Campaign.activePage().addPath(i);
  7959. d20.engine.debounced_renderTop();
  7960. }
  7961.  
  7962. const $win = $("#d20plus-svgdraw");
  7963. $win.dialog("open");
  7964.  
  7965. $win.find(`button`).off("click").on("click", () => {
  7966. d20plus.log("Drawing paths");
  7967. const input = $win.find(`textarea`).val();
  7968. const svg = $.parseXML(input);
  7969.  
  7970. const toDraw = $(svg).find("path").map((i, e) => {
  7971. const $e = $(e);
  7972. return {stroke: $e.attr("stroke") || "black", d: $e.attr("d")}
  7973. }).get();
  7974.  
  7975. const strokeWidth = Math.max(1, Number($win.find(`input[name="stroke-width"]`).val()));
  7976.  
  7977. toDraw.forEach(it => {
  7978. addShape(it.d, it.stroke, strokeWidth)
  7979. });
  7980. });
  7981. }
  7982. },
  7983. {
  7984. name: "Multi-Whisper",
  7985. desc: "Send whispers to multiple players ",
  7986. html: `
  7987. <div id="d20plus-whispers" title="Multi-Whisper Tool">
  7988. <div>
  7989. <button class="btn toggle-dc">Show Disconnected Players</button>
  7990. <button class="btn send-all">Send All Messages</button>
  7991. </div>
  7992. <hr>
  7993. <div class="messages" style="max-height: 600px; overflow-y: auto; overflow-x: hidden; transform: translateZ(0)">
  7994. <!-- populate with JS -->
  7995. </div>
  7996. </div>
  7997. `,
  7998. dialogFn: () => {
  7999. $("#d20plus-whispers").dialog({
  8000. autoOpen: false,
  8001. resizable: true,
  8002. width: 1000,
  8003. height: 760,
  8004. });
  8005. },
  8006. openFn: () => {
  8007. $("a.ui-tabs-anchor[href='#textchat']").trigger("click");
  8008.  
  8009. const $win = $("#d20plus-whispers");
  8010. $win.dialog("open");
  8011.  
  8012. const $btnToggleDc = $win.find(`.toggle-dc`).off("click").text("Show Disconnected Players");
  8013. const $btnSendAll = $win.find(`.send-all`).off("click");
  8014.  
  8015. const $pnlMessages = $win.find(`.messages`).empty();
  8016. const players = d20.Campaign.players.toJSON();
  8017. players.forEach((p, i) => {
  8018. const $btnSend = $(`<button class="btn send">Send</button>`).on("click", function () {
  8019. const $btn = $(this);
  8020. const $wrp = $btn.closest(`.wrp-message`);
  8021. const toMsg = $wrp.find(`input[data-player-id]:checked`).filter(":visible").map((ii, e) => $(e).attr("data-player-id")).get();
  8022. const content = $wrp.find(`.message`).val().trim();
  8023. toMsg.forEach(targetId => {
  8024. d20.textchat.doChatInput(`/w ${d20.Campaign.players.get(targetId).get("displayname").split(" ")[0]} ${content}`);
  8025.  
  8026. // This only posts to local player's chat, sadly
  8027. // d20.textchat.incoming(
  8028. // false,
  8029. // {
  8030. // avatar: `/users/avatar/${window.currentPlayer.get("d20userid")}/30`,
  8031. // who: d20.textchat.$speakingas.find("option:first-child").text(),
  8032. // type: "whisper",
  8033. // content: content,
  8034. // playerid: window.currentPlayer.id,
  8035. // id: d20plus.generateRowId(),
  8036. // target: targetId,
  8037. // target_name: d20.Campaign.players.get(targetId).get("displayname") || ""
  8038. // }
  8039. // );
  8040. })
  8041. });
  8042.  
  8043. $pnlMessages.append($(`
  8044. <div ${p.online || `style="display: none;"`} data-online="${p.online}" class="wrp-message">
  8045. <div>
  8046. ${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("")}
  8047. </div>
  8048. <textarea style="display: block; width: 95%;" placeholder="Enter whisper" class="message"></textarea>
  8049. </div>
  8050. `).append($btnSend).append(`<hr>`));
  8051. });
  8052.  
  8053. $btnToggleDc.on("click", () => {
  8054. $btnToggleDc.text($btnToggleDc.text().startsWith("Show") ? "Hide Disconnected Players" : "Show Disconnected Players");
  8055. $pnlMessages.find(`[data-online="false"]`).toggle();
  8056. });
  8057.  
  8058. $btnSendAll.on("click", () => {
  8059. $pnlMessages.find(`button.send`).click();
  8060. });
  8061. }
  8062. },
  8063. {
  8064. name: "Table Importer",
  8065. desc: "Import TableExport data",
  8066. html: `
  8067. <div id="d20plus-tables" title="Table Importer">
  8068. <div id="table-list">
  8069. <input type="search" class="search" placeholder="Search tables...">
  8070. <div class="list" style="transform: translateZ(0); max-height: 490px; overflow-y: scroll; overflow-x: hidden;"><i>Loading...</i></div>
  8071. </div>
  8072. <br>
  8073. <button class="btn start-import">Import</button>
  8074. </div>
  8075. `,
  8076. dialogFn: () => {
  8077. $("#d20plus-tables").dialog({
  8078. autoOpen: false,
  8079. resizable: true,
  8080. width: 800,
  8081. height: 650,
  8082. });
  8083. },
  8084. openFn: () => {
  8085. const $win = $("#d20plus-tables");
  8086. $win.dialog("open");
  8087.  
  8088. const $btnImport = $win.find(`.start-import`).off("click");
  8089.  
  8090. const url = `${BASE_SITE_URL}/data/roll20-tables.json`;
  8091. DataUtil.loadJSON(url, (data) => {
  8092. const $lst = $win.find(`.list`);
  8093.  
  8094. const tables = data.table.sort((a, b) => SortUtil.ascSort(a.name, b.name));
  8095. let tmp = "";
  8096. tables.forEach((t, i) => {
  8097. tmp += `
  8098. <label class="import-cb-label" data-listid="${i}">
  8099. <input type="checkbox">
  8100. <span class="name col-10">${t.name}</span>
  8101. <span title="${t.source ? Parser.sourceJsonToFull(t.source) : "Unknown Source"}" class="source">SRC[${t.source ? Parser.sourceJsonToAbv(t.source) : "UNK"}]</span>
  8102. </label>
  8103. `;
  8104. });
  8105. $lst.html(tmp);
  8106. tmp = null;
  8107.  
  8108. const tableList = new List("table-list", {
  8109. valueNames: ["name", "source"]
  8110. });
  8111.  
  8112. $btnImport.on("click", () => {
  8113. $("a.ui-tabs-anchor[href='#deckstables']").trigger("click");
  8114. const sel = tableList.items
  8115. .filter(it => $(it.elm).find(`input`).prop("checked"))
  8116. .map(it => tables[$(it.elm).attr("data-listid")]);
  8117.  
  8118. sel.forEach(t => {
  8119. const r20t = d20.Campaign.rollabletables.create({
  8120. name: t.name.replace(/\s+/g, "-"),
  8121. showplayers: t.isShown,
  8122. id: d20plus.generateRowId()
  8123. });
  8124.  
  8125. r20t.tableitems.reset(t.items.map(i => {
  8126. const out = {
  8127. id: d20plus.generateRowId(),
  8128. name: i.row
  8129. };
  8130. if (i.weight !== undefined) out.weight = i.weight;
  8131. if (i.avatar) out.avatar = i.avatar;
  8132. return out;
  8133. }))
  8134. })
  8135. });
  8136. });
  8137. }
  8138. },
  8139.  
  8140. {
  8141. name: "Token Avatar URL Fixer",
  8142. desc: "Change the root URL for tokens en-masse.",
  8143. html: `
  8144. <div id="d20plus-avatar-fixer" title="Avatar Fixer">
  8145. <p><b>Warning:</b> this thing doesn't really work.</p>
  8146. <p>Current URLs (view only): <select class="view-only"></select></p>
  8147. <p><label>Replace:<br><input name="search" value="https://5etools.com/"></label></p>
  8148. <p><label>With:<br><input name="replace" value="https://thegiddylimit.github.io/"></label></p>
  8149. <p><button class="btn">Go!</button></p>
  8150. </div>
  8151. `,
  8152. dialogFn: () => {
  8153. $("#d20plus-avatar-fixer").dialog({
  8154. autoOpen: false,
  8155. resizable: true,
  8156. width: 400,
  8157. height: 400,
  8158. });
  8159. },
  8160. openFn: () => {
  8161. function replaceAll (str, search, replacement) {
  8162. return str.split(search).join(replacement);
  8163. }
  8164.  
  8165. const $win = $("#d20plus-avatar-fixer");
  8166. $win.dialog("open");
  8167.  
  8168. const $selView = $win.find(`.view-only`);
  8169. const toView = [];
  8170. d20.Campaign.characters.toJSON().forEach(c => {
  8171. if (c.avatar && c.avatar.trim()) {
  8172. toView.push(c.avatar);
  8173. }
  8174. });
  8175. toView.sort(SortUtil.ascSort).forEach(url => $selView.append(`<option disabled>${url}</option>`));
  8176.  
  8177. const $btnGo = $win.find(`button`).off("click");
  8178. $btnGo.on("click", () => {
  8179. let count = 0;
  8180. $("a.ui-tabs-anchor[href='#journal']").trigger("click");
  8181.  
  8182. const search = $win.find(`[name="search"]`).val();
  8183. const replace = $win.find(`[name="replace"]`).val();
  8184.  
  8185. d20.Campaign.characters.toJSON().forEach(c => {
  8186. const id = c.id;
  8187.  
  8188. const realC = d20.Campaign.characters.get(id);
  8189.  
  8190. const curr = realC.get("avatar");
  8191. let toSave = false;
  8192. if (curr.includes(search)) {
  8193. count++;
  8194. realC.set("avatar", replaceAll(curr, search, replace));
  8195. toSave = true;
  8196. }
  8197. if (realC.get("defaulttoken")) {
  8198. realC._getLatestBlob("defaulttoken", (bl) => {
  8199. if (bl && bl.imgsrc && bl.imgsrc.includes(search)) {
  8200. count++;
  8201. realC.updateBlobs({imgsrc: replaceAll(bl.imgsrc, search, replace)});
  8202. toSave = true;
  8203. }
  8204. });
  8205. }
  8206. if (toSave) {
  8207. realC.save();
  8208. }
  8209. });
  8210. window.alert(`Replaced ${count} item${count === 0 || count > 1 ? "s" : ""}.`)
  8211. });
  8212. }
  8213. }
  8214. ],
  8215.  
  8216. addTools: () => {
  8217. const $body = $(`body`);
  8218. const $tools = $(`#d20-tools-list`);
  8219. const $toolsList = $tools.find(`.tools-list`);
  8220. d20plus.tools.forEach(t => {
  8221. $body.append(t.html); // add HTML
  8222. t.dialogFn(); // init window
  8223. // add tool row
  8224. const $wrp = $(`<div class="tool-row"/>`);
  8225. $wrp.append(`<p style="width: 20%;">${t.name}</p>`);
  8226. $wrp.append(`<p style="width: 60%;">${t.desc}</p>`);
  8227. $(`<a style="width: 15%;" class="btn" href="#">Open</a>`).on(mousedowntype, () => {
  8228. t.openFn();
  8229. $tools.dialog("close");
  8230. }).appendTo($wrp);
  8231. $toolsList.append($wrp);
  8232. });
  8233.  
  8234. $tools.dialog({
  8235. autoOpen: false,
  8236. resizable: true,
  8237. width: 800,
  8238. height: 650,
  8239. });
  8240. $(`#button-view-tools`).on(mousedowntype, () => {
  8241. $tools.dialog("open");
  8242. });
  8243. },
  8244.  
  8245. // ART /////////////////////////////////////////////////////////////////////////////////////////////////////////////
  8246. art: {
  8247. button: () => {
  8248. // add external art button was clicked
  8249. const $art = $("#d20plus-artfolder");
  8250. $art.dialog("open");
  8251. const $artList = $art.find(`.list`);
  8252. $artList.empty();
  8253.  
  8254. if (d20plus.art.custom) {
  8255. d20plus.art.custom.forEach(a => {
  8256. const $liArt = getArtLi(a.name, a.url);
  8257. $artList.append($liArt);
  8258. });
  8259. }
  8260.  
  8261. // init list library
  8262. const artList = new List("art-list-container", {
  8263. valueNames: ["name"],
  8264. listClass: "artlist"
  8265. });
  8266.  
  8267. const $btnAdd = $(`#art-list-add-btn`);
  8268. const $iptAddName = $(`#art-list-add-name`);
  8269. const $iptAddUrl = $(`#art-list-add-url`);
  8270. $btnAdd.off("click");
  8271. $btnAdd.on("click", () => {
  8272. const name = $iptAddName.val().trim();
  8273. const url = $iptAddUrl.val().trim();
  8274. if (!name || !url) {
  8275. alert("Missing required fields!")
  8276. } else {
  8277. artList.search();
  8278. artList.filter();
  8279. const $liArt = getArtLi(name, url);
  8280. $artList.append($liArt);
  8281. refreshCustomArtList();
  8282. }
  8283. });
  8284.  
  8285. const $btnMassAdd = $(`#art-list-multi-add-btn`);
  8286. $btnMassAdd.off("click");
  8287. $btnMassAdd.on("click", () => {
  8288. $("#d20plus-artmassadd").dialog("open");
  8289. const $btnMassAddSubmit = $(`#art-list-multi-add-btn-submit`);
  8290. $btnMassAddSubmit.off("click");
  8291. $btnMassAddSubmit.on("click", () => {
  8292. artList.search();
  8293. artList.filter();
  8294. const $iptUrls = $(`#art-list-multi-add-area`);
  8295. const massUrls = $iptUrls.val();
  8296. const spl = massUrls.split("\n").map(s => s.trim()).filter(s => s);
  8297. if (!spl.length) return;
  8298. else {
  8299. const delim = "---";
  8300. const toAdd = [];
  8301. for (const s of spl) {
  8302. if (!s.includes(delim)) {
  8303. alert(`Badly formatted line: ${s}`)
  8304. return;
  8305. } else {
  8306. const parts = s.split(delim);
  8307. if (parts.length !== 2) {
  8308. alert(`Badly formatted line: ${s}`)
  8309. return;
  8310. } else {
  8311. toAdd.push({
  8312. name: parts[0],
  8313. url: parts[1]
  8314. });
  8315. }
  8316. }
  8317. }
  8318. toAdd.forEach(a => {
  8319. $artList.append(getArtLi(a.name, a.url));
  8320. });
  8321. refreshCustomArtList();
  8322. $("#d20plus-artmassadd").dialog("close");
  8323. }
  8324. });
  8325. });
  8326.  
  8327. makeDraggables();
  8328. d20plus.art.refreshList = refreshCustomArtList;
  8329.  
  8330. function getArtLi (name, url) {
  8331. const showImage = d20plus.getCfgVal("interface", "showCustomArtPreview");
  8332. const $liArt = $(`
  8333. <li class="dd-item library-item draggableresult Vetools-draggable-art ui-draggable" data-fullsizeurl="${url}">
  8334. ${showImage ? `<img src="${url}" style="width: 30px; max-height: 30px; display: inline-block" draggable="false">` : ""}
  8335. <div class="dd-content name" style="display: inline-block; width: 35%;" data-url="${url}">${name}</div>
  8336. <a href="${url}"><span class="url" style="display: inline-block; width: ${showImage ? "40%" : "55%"};">${url}</span></a>
  8337. </li>
  8338. `);
  8339. if (!showImage) {
  8340. $liArt.on("mousedown", () => {
  8341. const $loader = $(`<div class="temp-warning">Loading image - don't drop yet!</div>`);
  8342. const $img = $(`<img src="${url}" style="width: 30px; max-height: 30px; display: none">`);
  8343. if (!$img.prop("complete")) {
  8344. $(`body`).append($loader);
  8345. $img.on("load", () => {
  8346. $loader.remove();
  8347. });
  8348. $loader.append($img);
  8349. }
  8350. });
  8351. }
  8352.  
  8353. const $btnDel = $(`<span class="delete btn btn-danger"><span class="pictos">#</span></span>`).on("click", () => {
  8354. $liArt.remove();
  8355. refreshCustomArtList();
  8356. });
  8357. $liArt.append($btnDel);
  8358. return $liArt;
  8359. }
  8360.  
  8361. function refreshCustomArtList () {
  8362. artList.reIndex();
  8363. const custom = [];
  8364. artList.items.forEach(i => {
  8365. const $ele = $(i.elm);
  8366. custom.push({
  8367. name: $ele.find(`.name`).text(),
  8368. url: $ele.find(`.url`).text()
  8369. });
  8370. });
  8371. d20plus.art.custom = custom;
  8372. makeDraggables();
  8373. saveToHandout();
  8374. }
  8375.  
  8376. function makeDraggables () {
  8377. $(`.Vetools-draggable-art`).draggable({
  8378. handle: ".dd-content",
  8379. revert: true,
  8380. revertDuration: 0,
  8381. helper: "clone",
  8382. appendTo: "body"
  8383. })
  8384. }
  8385.  
  8386. function saveToHandout () {
  8387. const handout = d20plus.getArtHandout();
  8388. if (!handout) {
  8389. d20.Campaign.handouts.create({
  8390. name: ART_HANDOUT
  8391. }, {
  8392. success: function (handout) {
  8393. notecontents = "This handout is used to store custom art URLs."
  8394.  
  8395. const gmnotes = JSON.stringify(d20plus.art.custom);
  8396. handout.updateBlobs({notes: notecontents, gmnotes: gmnotes});
  8397. handout.save({notes: (new Date).getTime(), inplayerjournals: ""});
  8398. }
  8399. });
  8400. } else {
  8401. const gmnotes = JSON.stringify(d20plus.art.custom);
  8402. handout.updateBlobs({gmnotes: gmnotes});
  8403. handout.save({notes: (new Date).getTime()});
  8404. }
  8405. }
  8406. },
  8407.  
  8408. // TODO load a decent default art library from somewhere
  8409. default: [
  8410. {
  8411. name: "Phoenix",
  8412. url: "http://www.discgolfbirmingham.com/wordpress/wp-content/uploads/2014/04/phoenix-rising.jpg"
  8413. }
  8414. ]
  8415. },
  8416.  
  8417. getArtHandout: () => {
  8418. return d20.Campaign.handouts.models.find((handout) => {
  8419. return handout.attributes.name === ART_HANDOUT;
  8420. });
  8421. },
  8422.  
  8423. loadArt: (nextFn) => {
  8424. d20plus.log("Loading custom art");
  8425. const handout = d20plus.getArtHandout();
  8426. if (handout) {
  8427. handout.view.render();
  8428. handout._getLatestBlob("gmnotes", function (gmnotes) {
  8429. const decoded = decodeURIComponent(gmnotes);
  8430. try {
  8431. d20plus.art.custom = JSON.parse(decoded);
  8432. nextFn();
  8433. } catch (e) {
  8434. nextFn();
  8435. }
  8436. });
  8437. } else {
  8438. nextFn();
  8439. }
  8440. },
  8441.  
  8442. addCustomArtSearch: () => {
  8443. d20plus.log("Add custom art search");
  8444. const $afterTo = $(`#libraryresults`);
  8445. $afterTo.after(d20plus.artListHTML);
  8446.  
  8447. const $olNone = $(`#image-search-none`);
  8448. const $olHasResults = $(`#image-search-has-results`);
  8449.  
  8450. const $olArt = $(`#custom-art-results`);
  8451. const $srchImages = $(`#imagedialog .searchbox input.keywords`);
  8452. $srchImages.on("keyup", () => {
  8453. $olArt.empty();
  8454. const searched = $srchImages.val().trim().toLowerCase();
  8455. if (searched.length === 0) {
  8456. $olNone.show();
  8457. $olHasResults.hide();
  8458. return;
  8459. }
  8460.  
  8461. let toShow = d20plus.art.default.filter(a => a.name.toLowerCase().includes(searched));
  8462. if (d20plus.art.custom) toShow = toShow.concat(d20plus.art.custom.filter(a => a.name.toLowerCase().includes(searched)));
  8463.  
  8464. if (!toShow.length) {
  8465. $olNone.show();
  8466. $olHasResults.hide();
  8467. } else {
  8468. $olNone.hide();
  8469. $olHasResults.show();
  8470.  
  8471. toShow.forEach(a => {
  8472. $olArt.append(`
  8473. <li class="dd-item library-item draggableresult Vetoolsresult ui-draggable" data-fullsizeurl="${a.url}">
  8474. <div class="dd-content">
  8475. <div class="token"><img src="${a.url}" draggable="false"></div>
  8476. <div class="name">
  8477. <div class="namecontainer"><a href="${a.url}" rel="external">${a.name}</a></div>
  8478. </div>
  8479. </div>
  8480. </li>
  8481. `);
  8482. });
  8483. }
  8484.  
  8485. $("#imagedialog #Vetoolsresults .draggableresult").draggable({
  8486. handle: ".dd-content",
  8487. revert: true,
  8488. revertDuration: 0,
  8489. helper: "clone",
  8490. appendTo: "body"
  8491. }).addTouch();
  8492. });
  8493. },
  8494.  
  8495. initArtFromUrlButtons: () => {
  8496. d20plus.log("Add direct URL art buttons");
  8497. $("#tmpl_charactereditor").replaceWith(d20plus.template_charactereditor);
  8498. $("#tmpl_handouteditor").replaceWith(d20plus.template_handouteditor);
  8499.  
  8500. $(`.character-image-by-url`).live("click", function () {
  8501. const cId = $(this).closest(`[data-characterid]`).attr(`data-characterid`);
  8502. const url = window.prompt("Enter a URL", "https://example.com/pic.png");
  8503. if (url) {
  8504. d20.Campaign.characters.get(cId).set("avatar", url);
  8505. }
  8506. });
  8507.  
  8508. $(`.handout-image-by-url`).live("click", function () {
  8509. const hId = $(this).closest(`[data-handoutid]`).attr(`data-handoutid`);
  8510. const url = window.prompt("Enter a URL", "https://example.com/pic.png");
  8511. if (url) {
  8512. d20.Campaign.handouts.get(hId).set("avatar", url);
  8513. }
  8514. });
  8515. },
  8516.  
  8517. // UI ENHANCEMENTS /////////////////////////////////////////////////////////////////////////////////////////////////
  8518.  
  8519. addProFeatures: () => {
  8520. d20plus.log("Add Pro features");
  8521.  
  8522. // modified to allow players to use the FX tool, and to keep current colour selections when switching tool
  8523. // BEGIN ROLL20 CODE
  8524. function setMode (e) {
  8525. d20plus.log("Setting mode " + e);
  8526. // BEGIN MOD
  8527. // "text" === e || "rect" === e || "polygon" === e || "path" === e || "pan" === e || "select" === e || "targeting" === e || "measure" === e || window.is_gm || (e = "select"),
  8528. // END MOD
  8529. "text" == e ? $("#editor").addClass("texteditmode") : $("#editor").removeClass("texteditmode"),
  8530. $("#floatingtoolbar li").removeClass("activebutton"),
  8531. $("#" + e).addClass("activebutton"),
  8532. "fog" == e.substring(0, 3) && $("#fogcontrols").addClass("activebutton"),
  8533. "rect" == e && ($("#drawingtools").addClass("activebutton"),
  8534. $("#drawingtools").removeClass("text path polygon").addClass("rect")),
  8535. "text" == e && ($("#drawingtools").addClass("activebutton"),
  8536. $("#drawingtools").removeClass("rect path polygon").addClass("text")),
  8537. "path" == e && $("#drawingtools").addClass("activebutton").removeClass("text rect polygon").addClass("path"),
  8538. "polygon" == e ? $("#drawingtools").addClass("activebutton").removeClass("text rect path").addClass("polygon") : d20.engine.finishCurrentPolygon(),
  8539. "pan" !== e && "select" !== e && d20.engine.unselect(),
  8540. "pan" == e ? ($("#select").addClass("pan").removeClass("select").addClass("activebutton"),
  8541. d20.token_editor.removeRadialMenu(),
  8542. $("#editor-wrapper").addClass("panning")) : $("#editor-wrapper").removeClass("panning"),
  8543. "select" == e && $("#select").addClass("select").removeClass("pan").addClass("activebutton"),
  8544. $("#floatingtoolbar .mode").hide(),
  8545. ("text" == e || "select" == e) && $("#floatingtoolbar ." + e).show(),
  8546. "gridalign" == e ? $("#gridaligninstructions").show() : "gridalign" === d20.engine.mode && $("#gridaligninstructions").hide(),
  8547. "targeting" === e ? ($("#targetinginstructions").show(),
  8548. $("#upperCanvas").addClass("targeting"),
  8549. d20.engine.canvas.hoverCursor = "crosshair") : "targeting" === d20.engine.mode && ($("#targetinginstructions").hide(),
  8550. $("#upperCanvas").removeClass("targeting"),
  8551. d20.engine.nextTargetCallback && _.defer(function () {
  8552. d20.engine.nextTargetCallback && d20.engine.nextTargetCallback(!1)
  8553. }),
  8554. d20.engine.canvas.hoverCursor = "move"),
  8555. console.log("Switch mode to " + e),
  8556. // BEGIN MOD
  8557. d20.engine.mode = e;
  8558. d20.engine.canvas.isDrawingMode = "path" == e ? !0 : !1;
  8559. if ("text" == e || "path" == e || "rect" == e || "polygon" == e || "fxtools" == e) {
  8560. $("#secondary-toolbar").show();
  8561. $("#secondary-toolbar .mode").hide();
  8562. $("#secondary-toolbar ." + e).show();
  8563. ("path" == e || "rect" == e || "polygon" == e) && ("" === $("#path_strokecolor").val() && ($("#path_strokecolor").val("#000000").trigger("change-silent"),
  8564. $("#path_fillcolor").val("transparent").trigger("change-silent")),
  8565. d20.engine.canvas.freeDrawingBrush.color = $("#path_strokecolor").val(),
  8566. d20.engine.canvas.freeDrawingBrush.fill = $("#path_fillcolor").val() || "transparent",
  8567. $("#path_width").trigger("change")),
  8568. "fxtools" == e && "" === $("#fxtools_color").val() && $("#fxtools_color").val("#a61c00").trigger("change-silent"),
  8569. $("#floatingtoolbar").trigger("blur")
  8570. } else {
  8571. $("#secondary-toolbar").hide();
  8572. $("#floatingtoolbar").trigger("blur");
  8573. }
  8574. // END MOD
  8575. // END ROLL20 CODE
  8576. }
  8577.  
  8578. d20plus.setMode = setMode;
  8579.  
  8580. // rebind buttons with new setMode
  8581. const $drawTools = $("#drawingtools");
  8582. const $rect = $drawTools.find(".chooserect");
  8583. const $path = $drawTools.find(".choosepath");
  8584. const $poly = $drawTools.find(".choosepolygon");
  8585. $drawTools.unbind(clicktype).bind(clicktype, () => {
  8586. $(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")
  8587. });
  8588. $rect.unbind(clicktype).bind(clicktype, () => {
  8589. setMode("rect");
  8590. return false;
  8591. });
  8592. $path.unbind(clicktype).bind(clicktype, () => {
  8593. setMode("path");
  8594. return false;
  8595. });
  8596. $poly.unbind(clicktype).bind(clicktype, () => {
  8597. setMode("polygon");
  8598. return false;
  8599. });
  8600. $("#rect").unbind(clicktype).bind(clicktype, () => setMode("rect"));
  8601. $("#path").unbind(clicktype).bind(clicktype, () => setMode("path"));
  8602.  
  8603. if (!$(`#fxtools`).length) {
  8604. const $fxMode = $(`<li id="fxtools"/>`).append(`<span class="pictos">e</span>`);
  8605. $fxMode.on("click", () => {
  8606. d20plus.setMode("fxtools");
  8607. });
  8608. $(`#drawingtools`).after($fxMode);
  8609. }
  8610.  
  8611. if (window.is_gm) {
  8612. // add lighting layer tool
  8613. if (!$(`#editinglayer .choosewalls`).length) {
  8614. $(`#editinglayer .choosegmlayer`).after(`<li class="choosewalls"><span class="pictostwo">r</span> Dynamic Lighting</li>`);
  8615. }
  8616.  
  8617. // ensure tokens have editable sight
  8618. $("#tmpl_tokeneditor").replaceWith(d20plus.template_TokenEditor);
  8619. // show dynamic lighting/etc page settings
  8620. $("#tmpl_pagesettings").replaceWith(d20plus.template_pageSettings);
  8621. $("#page-toolbar").on("mousedown", ".settings", function () {
  8622. var e = d20.Campaign.pages.get($(this).parents(".availablepage").attr("data-pageid"));
  8623. e.view._template = $.jqotec("#tmpl_pagesettings");
  8624. });
  8625. }
  8626. },
  8627.  
  8628. enhanceMeasureTool: () => {
  8629. d20plus.log("Enhance Measure tool");
  8630. // ROLL20 CODE
  8631. var T = function (e, t, n, i, r, o) {
  8632. var a = d20.engine.getDistanceInScale({
  8633. x: t.x,
  8634. y: t.y
  8635. }, {
  8636. x: t.to_x,
  8637. y: t.to_y
  8638. }, o)
  8639. , s = a[0];
  8640. void 0 !== r && (s = Math.round(10 * (s + r)) / 10);
  8641. var l = s + "" + d20.Campaign.activePage().get("scale_units");
  8642. if (e.strokeStyle = t.color,
  8643. n) {
  8644. // BEGIN MOD
  8645. var fontSize = (1 / d20.engine.canvasZoom) * 12;
  8646. e.font = fontSize + "pt Arial Black";
  8647. var c = e.measureText(l);
  8648. e.fillStyle = "rgba(255,255,255,0.75)";
  8649. e.beginPath();
  8650. e.rect(t.to_x - 35, t.to_y - (23 + fontSize), c.width + 10, (10 + fontSize));
  8651. e.closePath();
  8652. e.fill();
  8653. // END MOD
  8654. }
  8655. e.beginPath();
  8656. var u = 15
  8657. , d = Math.atan2(t.to_y - t.y, t.to_x - t.x);
  8658. return e.moveTo(t.x, t.y),
  8659. e.lineTo(t.to_x, t.to_y),
  8660. (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)),
  8661. e.moveTo(t.to_x, t.to_y),
  8662. e.lineTo(t.to_x - u * Math.cos(d + Math.PI / 6), t.to_y - u * Math.sin(d + Math.PI / 6))),
  8663. e.closePath(),
  8664. e.stroke(),
  8665. "nub" === i && (e.beginPath(),
  8666. e.arc(t.to_x, t.to_y, 7, 0, 2 * Math.PI, !0),
  8667. e.closePath(),
  8668. e.fillStyle = e.strokeStyle,
  8669. e.fill()),
  8670. n && (e.fillStyle = "rgba(0,0,0,1)",
  8671. e.fillText(l, t.to_x - 30, t.to_y - 20)),
  8672. a
  8673. };
  8674. d20.engine.drawMeasurements = function (e) {
  8675. e.globalCompositeOperation = "source-over",
  8676. e.lineWidth = 3,
  8677. e.globalAlpha = 1,
  8678. _.each(d20.engine.measurements, function (t) {
  8679. if (t.pageid === d20.Campaign.activePage().id) {
  8680. var n = _.clone(t)
  8681. , i = d20.Campaign.players.get(n.player);
  8682. n.color = i.get("color"),
  8683. n.to_x = n.to_x - d20.engine.currentCanvasOffset[0],
  8684. n.to_y = n.to_y - d20.engine.currentCanvasOffset[1],
  8685. n.x = n.x - d20.engine.currentCanvasOffset[0],
  8686. n.y = n.y - d20.engine.currentCanvasOffset[1],
  8687. T(e, n, !0, !0)
  8688. }
  8689. })
  8690. }
  8691. // END ROLL20 CODE
  8692. },
  8693.  
  8694. _addStatusEffectEntries: () => {
  8695. const sheetUrl = window.is_gm ? d20plus.getCfgVal("token", "statusSheetUrl") || d20plus.getCfgDefaultVal("token", "statusSheetUrl"): window.Campaign.attributes.bR20cfg_statussheet;
  8696.  
  8697. const temp = new Image();
  8698. temp.onload = () => {
  8699. const xSize = 34;
  8700. const iMin = 47;
  8701. // const iMax = 101;
  8702. const iMax = Math.ceil(temp.width / xSize); // round the last one up to a full image
  8703. for (let i = iMin; i < iMax; ++i) {
  8704. d20.token_editor.statusmarkers["5etools_" + (i - iMin)] = String(i * xSize);
  8705. }
  8706. };
  8707. temp.src = sheetUrl;
  8708.  
  8709. $(`#5etools-status-css`).html(`#radial-menu .markermenu .markericon {
  8710. background-image: url(${sheetUrl});
  8711. }`);
  8712. },
  8713.  
  8714. _removeStatusEffectEntries: () => {
  8715. $(`#5etools-status-css`).html("");
  8716. Object.keys(d20.token_editor.statusmarkers).filter(k => k.startsWith("5etools_")).forEach(k => delete d20.token_editor.statusmarkers[k]);
  8717. },
  8718.  
  8719. enhanceStatusEffects: () => {
  8720. d20plus.log("Enhance status effects");
  8721. $(`head`).append(`<style id="5etools-status-css"/>`);
  8722. d20plus._handleStatusTokenConfigChange();
  8723.  
  8724. function overwriteStatusEffects () {
  8725. d20.engine.canvasDirty = true;
  8726. d20.engine.canvasTopDirty = true;
  8727. d20.engine.canvas._objects.forEach(it => {
  8728. // avoid adding it to any objects that wouldn't have it to begin with
  8729. if (!it.model || !it.model.view || !it.model.view.updateBackdrops) return;
  8730.  
  8731. it.model.view.updateBackdrops = function (e) {
  8732. 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) {
  8733. // BEGIN MOD
  8734. const scaleFact = (d20plus.getCfgVal("canvas", "scaleNamesStatuses") && d20.Campaign.activePage().get("snapping_increment"))
  8735. ? d20.Campaign.activePage().get("snapping_increment")
  8736. : 1;
  8737. // END MOD
  8738. var t = this.model.collection.page
  8739. , n = e || d20.engine.canvas.getContext();
  8740. n.save(),
  8741. (this.graphic.get("flipX") || this.graphic.get("flipY")) && n.scale(this.graphic.get("flipX") ? -1 : 1, this.graphic.get("flipY") ? -1 : 1);
  8742. var i = this
  8743. , r = Math.floor(this.graphic.get("width") / 2)
  8744. , o = Math.floor(this.graphic.get("height") / 2)
  8745. , a = (parseFloat(t.get("scale_number")),
  8746. this.model.get("statusmarkers").split(","));
  8747. -1 !== a.indexOf("dead") && (n.strokeStyle = "rgba(189,13,13,0.60)",
  8748. n.lineWidth = 10,
  8749. n.beginPath(),
  8750. n.moveTo(-r + 7, -o + 15),
  8751. n.lineTo(r - 7, o - 5),
  8752. n.moveTo(r - 7, -o + 15),
  8753. n.lineTo(-r + 7, o - 5),
  8754. n.closePath(),
  8755. n.stroke()),
  8756. n.rotate(-this.graphic.get("angle") * Math.PI / 180),
  8757. n.strokeStyle = "rgba(0,0,0,0.65)",
  8758. n.lineWidth = 1;
  8759. var s = 0
  8760. , l = i.model.get("bar1_value")
  8761. , c = i.model.get("bar1_max");
  8762. if ("" != c && (window.is_gm || this.model.get("showplayers_bar1") || this.model.currentPlayerControls() && this.model.get("playersedit_bar1"))) {
  8763. var u = parseInt(l, 10) / parseInt(c, 10)
  8764. , d = -o - 20 + 0;
  8765. n.fillStyle = "rgba(" + d20.Campaign.tokendisplay.bar1_rgb + ",0.75)",
  8766. n.beginPath(),
  8767. n.rect(-r + 3, d, Math.floor((2 * r - 6) * u), 8),
  8768. n.closePath(),
  8769. n.fill(),
  8770. n.beginPath(),
  8771. n.rect(-r + 3, d, 2 * r - 6, 8),
  8772. n.closePath(),
  8773. n.stroke(),
  8774. s++
  8775. }
  8776. var l = i.model.get("bar2_value")
  8777. , c = i.model.get("bar2_max");
  8778. if ("" != c && (window.is_gm || this.model.get("showplayers_bar2") || this.model.currentPlayerControls() && this.model.get("playersedit_bar2"))) {
  8779. var u = parseInt(l, 10) / parseInt(c, 10)
  8780. , d = -o - 20 + 12;
  8781. n.fillStyle = "rgba(" + d20.Campaign.tokendisplay.bar2_rgb + ",0.75)",
  8782. n.beginPath(),
  8783. n.rect(-r + 3, d, Math.floor((2 * r - 6) * u), 8),
  8784. n.closePath(),
  8785. n.fill(),
  8786. n.beginPath(),
  8787. n.rect(-r + 3, d, 2 * r - 6, 8),
  8788. n.closePath(),
  8789. n.stroke(),
  8790. s++
  8791. }
  8792. var l = i.model.get("bar3_value")
  8793. , c = i.model.get("bar3_max");
  8794. if ("" != c && (window.is_gm || this.model.get("showplayers_bar3") || this.model.currentPlayerControls() && this.model.get("playersedit_bar3"))) {
  8795. var u = parseInt(l, 10) / parseInt(c, 10)
  8796. , d = -o - 20 + 24;
  8797. n.fillStyle = "rgba(" + d20.Campaign.tokendisplay.bar3_rgb + ",0.75)",
  8798. n.beginPath(),
  8799. n.rect(-r + 3, d, Math.floor((2 * r - 6) * u), 8),
  8800. n.closePath(),
  8801. n.fill(),
  8802. n.beginPath(),
  8803. n.rect(-r + 3, d, 2 * r - 6, 8),
  8804. n.closePath(),
  8805. n.stroke()
  8806. }
  8807. var h, p, g = 1, f = !1;
  8808. switch (d20.Campaign.get("markers_position")) {
  8809. case "bottom":
  8810. h = o - 10,
  8811. p = r;
  8812. break;
  8813. case "left":
  8814. h = -o - 10,
  8815. p = -r,
  8816. f = !0;
  8817. break;
  8818. case "right":
  8819. h = -o - 10,
  8820. p = r - 18,
  8821. f = !0;
  8822. break;
  8823. default:
  8824. h = -o + 10,
  8825. p = r
  8826. }
  8827. // BEGIN MOD
  8828. n.strokeStyle = "white";
  8829. n.lineWidth = 3 * scaleFact;
  8830. const scaledFont = 14 * scaleFact;
  8831. n.font = "bold " + scaledFont + "px Arial";
  8832. // END MOD
  8833. _.each(a, function (e) {
  8834. var t = d20.token_editor.statusmarkers[e.split("@")[0]];
  8835. if (!t)
  8836. return !0;
  8837. if ("dead" === e)
  8838. return !0;
  8839. var i = 0;
  8840. if (g--,
  8841. "#" === t.substring(0, 1))
  8842. n.fillStyle = t,
  8843. n.beginPath(),
  8844. f ? h += 16 : p -= 16,
  8845. n.arc(p + 8, f ? h + 4 : h, 6, 0, 2 * Math.PI, !0),
  8846. n.closePath(),
  8847. n.stroke(),
  8848. n.fill(),
  8849. i = f ? 10 : 4;
  8850. else {
  8851. // BEGIN MOD
  8852. if (!d20.token_editor.statussheet_ready) return;
  8853. const scaledWH = 21 * scaleFact;
  8854. const scaledOffset = 22 * scaleFact;
  8855. f ? h += scaledOffset : p -= scaledOffset;
  8856.  
  8857. if (d20.engine.canvasZoom <= 1) {
  8858. n.drawImage(d20.token_editor.statussheet_small, parseInt(t, 10), 0, 21, 21, p, h - 9, scaledWH, scaledWH);
  8859. } else {
  8860. n.drawImage(d20.token_editor.statussheet, parseInt(t, 10), 0, 24, 24, p, h - 9, scaledWH, scaledWH)
  8861. }
  8862.  
  8863. i = f ? 14 : 12;
  8864. i *= scaleFact;
  8865. // END MOD
  8866. }
  8867. if (-1 !== e.indexOf("@")) {
  8868. var r = e.split("@")[1];
  8869. // BEGIN MOD
  8870. // bing backtick to "clear counter"
  8871. if (r === "`") return;
  8872. n.fillStyle = "rgb(222,31,31)";
  8873. var o = f ? 9 : 14;
  8874. o *= scaleFact;
  8875. o -= (14 - (scaleFact * 14));
  8876. n.strokeText(r + "", p + i, h + o);
  8877. n.fillText(r + "", p + i, h + o);
  8878. // END MOD
  8879. }
  8880. });
  8881. var m = i.model.get("name");
  8882. if ("" != m && 1 == this.model.get("showname") && (window.is_gm || this.model.get("showplayers_name") || this.model.currentPlayerControls() && this.model.get("playersedit_name"))) {
  8883. n.textAlign = "center";
  8884. // BEGIN MOD
  8885. var y = 14 * scaleFact;
  8886. const scaledY = 22 * scaleFact;
  8887. const scaled6 = 6 * scaleFact;
  8888. const scaled8 = 8 * scaleFact;
  8889. n.font = "bold " + y + "px Arial";
  8890. var v = n.measureText(m).width;
  8891. n.fillStyle = "rgba(255,255,255,0.50)";
  8892. n.fillRect(-1 * Math.floor((v + scaled6) / 2), o + scaled8, v + scaled6, y + scaled6);
  8893. n.fillStyle = "rgb(0,0,0)";
  8894. n.fillText(m + "", 0, o + scaledY, v);
  8895. // END MOD
  8896. }
  8897. n.restore()
  8898. }
  8899. }
  8900. });
  8901. }
  8902.  
  8903. overwriteStatusEffects();
  8904.  
  8905. d20.engine.canvas.off("object:added");
  8906. d20.engine.canvas.on("object:added", overwriteStatusEffects);
  8907.  
  8908. // the holy trinity
  8909. // d20.engine.canvas.on("object:removed", () => console.log("added"));
  8910. // d20.engine.canvas.on("object:removed", () => console.log("removed"));
  8911. // d20.engine.canvas.on("object:modified", () => console.log("modified"));
  8912.  
  8913. $(document).off("mouseenter", ".markermenu");
  8914. $(document).on("mouseenter", ".markermenu", function () {
  8915. var e = this;
  8916. $(this).on("mouseover.statusiconhover", ".statusicon", function () {
  8917. a = $(this).attr("data-action-type").replace("toggle_status_", "")
  8918. }),
  8919. $(document).on("keypress.statusnum", function (t) {
  8920. // BEGIN MOD // TODO see if this clashes with keyboard shortcuts
  8921. if ("dead" !== a && currentcontexttarget) {
  8922. // END MOD
  8923. var n = String.fromCharCode(t.which)
  8924. ,
  8925. i = "" == currentcontexttarget.model.get("statusmarkers") ? [] : currentcontexttarget.model.get("statusmarkers").split(",")
  8926. , r = (_.map(i, function (e) {
  8927. return e.split("@")[0]
  8928. }),
  8929. !1);
  8930. i = _.map(i, function (e) {
  8931. return e.split("@")[0] == a ? (r = !0,
  8932. a + "@" + n) : e
  8933. }),
  8934. r || ($(e).find(".statusicon[data-action-type=toggle_status_" + a + "]").addClass("active"),
  8935. i.push(a + "@" + n)),
  8936. currentcontexttarget.model.save({
  8937. statusmarkers: i.join(",")
  8938. })
  8939. }
  8940. })
  8941. })
  8942. },
  8943.  
  8944. enhancePageSelector: () => {
  8945. d20plus.log("Enhancing page selector");
  8946. var updatePageOrder = function () {
  8947. d20plus.log("Saving page order...");
  8948. var pos = 0;
  8949. $("#page-toolbar .pages .chooseablepage").each(function () {
  8950. var page = d20.Campaign.pages.get($(this).attr("data-pageid"));
  8951. page && page.save({
  8952. placement: pos
  8953. });
  8954. pos++;
  8955. });
  8956. d20.pagetoolbar.noReload = false;
  8957. d20.pagetoolbar.refreshPageListing();
  8958. }
  8959.  
  8960. function overwriteDraggables () {
  8961. // make them draggable on both axes
  8962. $("#page-toolbar .pages").sortable("destroy");
  8963. $("#page-toolbar .pages").sortable({
  8964. items: "> .chooseablepage",
  8965. start: function () {
  8966. d20.pagetoolbar.noReload = true;
  8967. },
  8968. stop: function () {
  8969. updatePageOrder()
  8970. },
  8971. distance: 15
  8972. }).addTouch();
  8973. $("#page-toolbar .playerbookmark").draggable("destroy");
  8974. $("#page-toolbar .playerbookmark").draggable({
  8975. revert: "invalid",
  8976. appendTo: "#page-toolbar",
  8977. helper: "original"
  8978. }).addTouch();
  8979. $("#page-toolbar .playerspecificbookmark").draggable("destroy");
  8980. $("#page-toolbar .playerspecificbookmark").draggable({
  8981. revert: "invalid",
  8982. appendTo: "#page-toolbar",
  8983. helper: "original"
  8984. }).addTouch();
  8985. }
  8986.  
  8987. overwriteDraggables();
  8988. $(`#page-toolbar`).css("top", "calc(-90vh + 40px)");
  8989.  
  8990. const originalFn = d20.pagetoolbar.refreshPageListing;
  8991. d20.pagetoolbar.refreshPageListing = () => {
  8992. originalFn();
  8993. // original function is debounced at 100ms, so debounce this at 110ms and hope for the best
  8994. _.debounce(() => {
  8995. overwriteDraggables();
  8996. }, 110)();
  8997. }
  8998. },
  8999.  
  9000. initQuickSearch: ($iptSearch, $outSearch) => {
  9001. $iptSearch.on("keyup", () => {
  9002. const searchVal = ($iptSearch.val() || "").trim();
  9003. $outSearch.empty();
  9004. if (searchVal.length <= 2) return; // ignore 2 characters or less, for performance reasons
  9005. const found = $(`#journal .content`).find(`li[data-itemid]`).filter((i, ele) => {
  9006. const $ele = $(ele);
  9007. return $ele.find(`.name`).text().trim().toLowerCase().includes(searchVal.toLowerCase());
  9008. });
  9009. if (found.length) {
  9010. $outSearch.append(`<p><b>Search results:</b></p>`);
  9011. const $outList = $(`<ol class="dd-list Vetools-search-results"/>`);
  9012. $outSearch.append($outList);
  9013. found.clone().addClass("Vetools-draggable").appendTo($outList);
  9014. $outSearch.append(`<hr>`);
  9015. $(`.Vetools-search-results .Vetools-draggable`).draggable({
  9016. revert: true,
  9017. distance: 10,
  9018. revertDuration: 0,
  9019. helper: "clone",
  9020. handle: ".namecontainer",
  9021. appendTo: "body",
  9022. scroll: true,
  9023. start: function () {
  9024. $("#journalfolderroot").addClass("externaldrag")
  9025. },
  9026. stop: function () {
  9027. $("#journalfolderroot").removeClass("externaldrag")
  9028. }
  9029. });
  9030. }
  9031. });
  9032. },
  9033.  
  9034. addSelectedTokenCommands: () => {
  9035. d20plus.log("Add token rightclick commands");
  9036. $("#tmpl_actions_menu").replaceWith(d20plus.template_actionsMenu);
  9037.  
  9038. // BEGIN ROLL20 CODE
  9039. var e, t = !1, n = [];
  9040. var i = function() {
  9041. t && (t.remove(),
  9042. t = !1),
  9043. e && clearTimeout(e)
  9044. };
  9045. var r = function (r) {
  9046. var o, a;
  9047. r.changedTouches && r.changedTouches.length > 0 ? (o = r.changedTouches[0].pageX,
  9048. a = r.changedTouches[0].pageY) : (o = r.pageX,
  9049. a = r.pageY),
  9050. i(),
  9051. n = [];
  9052. for (var s = [], l = d20.engine.selected(), c = 0; c < l.length; c++)
  9053. n.push(l[c]),
  9054. s.push(l[c].type);
  9055. if (s = _.uniq(s),
  9056. n.length > 0)
  9057. if (1 == s.length) {
  9058. var u = n[0];
  9059. t = $("image" == u.type && 0 == u.model.get("isdrawing") ? $("#tmpl_actions_menu").jqote(u.model) : $("#tmpl_actions_menu").jqote(u.model))
  9060. } else {
  9061. var u = n[0];
  9062. t = $($("#tmpl_actions_menu").jqote(u.model))
  9063. }
  9064. else
  9065. t = $($("#tmpl_actions_menu").jqote({}));
  9066. if (!window.is_gm && t[0].lastElementChild.childElementCount < 1)
  9067. return !1;
  9068. t.appendTo("body");
  9069. var d = t.height()
  9070. , h = t.width()
  9071. , p = {};
  9072. return p.top = a > $("#editor-wrapper").height() - $("#playerzone").height() - d - 100 ? a - d + "px" : a + "px",
  9073. p.left = o > $("#editor-wrapper").width() - h ? o + 10 - h + "px" : o + 10 + "px",
  9074. t.css(p),
  9075. $(".actions_menu").bind("mousedown mouseup touchstart", function(e) {
  9076. e.stopPropagation()
  9077. }),
  9078. $(".actions_menu ul > li").bind("mouseover touchend", function() {
  9079. if (e && (clearTimeout(e),
  9080. e = !1),
  9081. $(this).parents(".hasSub").length > 0)
  9082. ;
  9083. else if ($(this).hasClass("hasSub")) {
  9084. $(".actions_menu").css({
  9085. width: "215px",
  9086. height: "250px"
  9087. });
  9088. var t = this;
  9089. _.defer(function() {
  9090. $(".actions_menu ul.submenu").hide(),
  9091. $(t).find("ul.submenu:hidden").show()
  9092. })
  9093. } else
  9094. $(".actions_menu ul.submenu").hide()
  9095. }),
  9096. $(".actions_menu ul.submenu").live("mouseover", function() {
  9097. e && (clearTimeout(e),
  9098. e = !1)
  9099. }),
  9100. $(".actions_menu, .actions_menu ul.submenu").live("mouseleave", function() {
  9101. e || (e = setTimeout(function() {
  9102. $(".actions_menu ul.submenu").hide(),
  9103. $(".actions_menu").css("width", "100px").css("height", "auto"),
  9104. e = !1
  9105. }, 500))
  9106. }),
  9107. $(".actions_menu li").on(clicktype, function() {
  9108. var e = $(this).attr("data-action-type");
  9109. if (null != e) {
  9110. if ("copy" == e)
  9111. d20.clipboard.doCopy(),
  9112. i();
  9113. else if ("paste" == e)
  9114. d20.clipboard.doPaste(),
  9115. i();
  9116. else if ("delete" == e) {
  9117. var t = d20.engine.selected();
  9118. d20.engine.canvas.deactivateAllWithDispatch();
  9119. for (var r = 0; r < t.length; r++)
  9120. t[r].model.destroy();
  9121. i()
  9122. } else if ("undo" == e)
  9123. d20.undo && d20.undo.doUndo(),
  9124. i();
  9125. else if ("tofront" == e)
  9126. d20.engine.canvas.getActiveGroup() && d20.engine.unselect(),
  9127. _.each(n, function(e) {
  9128. d20.engine.canvas.bringToFront(e)
  9129. }),
  9130. d20.Campaign.activePage().debounced_recordZIndexes(),
  9131. i();
  9132. else if ("toback" == e)
  9133. d20.engine.canvas.getActiveGroup() && d20.engine.unselect(),
  9134. _.each(n, function(e) {
  9135. d20.engine.canvas.sendToBack(e)
  9136. }),
  9137. d20.Campaign.activePage().debounced_recordZIndexes(),
  9138. i();
  9139. else if (-1 !== e.indexOf("tolayer_")) {
  9140. d20.engine.unselect();
  9141. var o = e.replace("tolayer_", "");
  9142. _.each(n, function(e) {
  9143. e.model.save({
  9144. layer: o
  9145. })
  9146. }),
  9147. i(),
  9148. d20.token_editor.removeRadialMenu()
  9149. } else if ("addturn" == e)
  9150. _.each(n, function(e) {
  9151. d20.Campaign.initiativewindow.addTokenToList(e.model.id)
  9152. }),
  9153. i(),
  9154. d20.tutorial && d20.tutorial.active && $(document.body).trigger("addedTurn");
  9155. else if ("group" == e) {
  9156. var a = [];
  9157. d20.engine.unselect(),
  9158. _.each(n, function(e) {
  9159. a.push(e.model.id)
  9160. }),
  9161. _.each(n, function(e) {
  9162. e.model.addToGroup(a)
  9163. }),
  9164. i();
  9165. var s = n[0];
  9166. d20.engine.select(s)
  9167. } else if ("ungroup" == e)
  9168. d20.engine.unselect(),
  9169. _.each(n, function(e) {
  9170. e.model.clearGroup()
  9171. }),
  9172. d20.token_editor.removeRadialMenu(),
  9173. i();
  9174. else if ("toggledrawing" == e)
  9175. d20.engine.unselect(),
  9176. _.each(n, function(e) {
  9177. e.model.set({
  9178. isdrawing: !e.model.get("isdrawing")
  9179. }).save()
  9180. }),
  9181. i(),
  9182. d20.token_editor.removeRadialMenu();
  9183. else if ("toggleflipv" == e)
  9184. d20.engine.unselect(),
  9185. _.each(n, function(e) {
  9186. e.model.set({
  9187. flipv: !e.model.get("flipv")
  9188. }).save()
  9189. }),
  9190. i(),
  9191. d20.token_editor.removeRadialMenu();
  9192. else if ("togglefliph" == e)
  9193. d20.engine.unselect(),
  9194. _.each(n, function(e) {
  9195. e.model.set({
  9196. fliph: !e.model.get("fliph")
  9197. }).save()
  9198. }),
  9199. i(),
  9200. d20.token_editor.removeRadialMenu();
  9201. else if ("takecard" == e)
  9202. d20.engine.canvas.getActiveGroup() && d20.engine.unselect(),
  9203. _.each(n, function(e) {
  9204. var t = d20.decks.cardByID(e.model.get("cardid"));
  9205. if (e.model.get("isdrawing") === !1)
  9206. var n = {
  9207. bar1_value: e.model.get("bar1_value"),
  9208. bar1_max: e.model.get("bar1_max"),
  9209. bar2_value: e.model.get("bar2_value"),
  9210. bar2_max: e.model.get("bar2_max"),
  9211. bar3_value: e.model.get("bar3_value"),
  9212. bar3_max: e.model.get("bar3_max")
  9213. };
  9214. d20.Campaign.hands.addCardToHandForPlayer(t, window.currentPlayer, n ? n : void 0),
  9215. _.defer(function() {
  9216. e.model.destroy()
  9217. })
  9218. }),
  9219. d20.engine.unselect(),
  9220. i();
  9221. else if ("flipcard" == e)
  9222. d20.engine.canvas.getActiveGroup() && d20.engine.unselect(),
  9223. _.each(n, function(e) {
  9224. var t = e.model.get("sides").split("|")
  9225. , n = e.model.get("currentSide")
  9226. , i = n + 1;
  9227. i > t.length - 1 && (i = 0),
  9228. e.model.set({
  9229. currentSide: i,
  9230. imgsrc: unescape(t[i])
  9231. }).save()
  9232. }),
  9233. i();
  9234. else if ("setdimensions" == e) {
  9235. var l = n[0]
  9236. , c = $($("#tmpl_setdimensions").jqote()).dialog({
  9237. title: "Set Dimensions",
  9238. width: 325,
  9239. height: 225,
  9240. buttons: {
  9241. Set: function() {
  9242. var e, t;
  9243. "pixels" == c.find(".dimtype").val() ? (e = parseInt(c.find("input.width").val(), 10),
  9244. t = parseInt(c.find("input.height").val(), 10)) : (e = parseFloat(c.find("input.width").val()) * window.dpi,
  9245. t = parseFloat(c.find("input.height").val()) * window.dpi),
  9246. l.model.save({
  9247. width: e,
  9248. height: t
  9249. }),
  9250. c.off("change"),
  9251. c.dialog("destroy").remove()
  9252. },
  9253. Cancel: function() {
  9254. c.off("change"),
  9255. c.dialog("destroy").remove()
  9256. }
  9257. },
  9258. beforeClose: function() {
  9259. c.off("change"),
  9260. c.dialog("destroy").remove()
  9261. }
  9262. });
  9263. c.on("change", ".dimtype", function() {
  9264. "pixels" == $(this).val() ? (c.find("input.width").val(Math.round(l.get("width"))),
  9265. c.find("input.height").val(Math.round(l.get("height")))) : (c.find("input.width").val(l.get("width") / window.dpi),
  9266. c.find("input.height").val(l.get("height") / window.dpi))
  9267. }),
  9268. c.find(".dimtype").trigger("change"),
  9269. i()
  9270. } else if ("aligntogrid" == e)
  9271. if (0 === d20.Campaign.activePage().get("snapping_increment")) {
  9272. i();
  9273. var u = $($("#tmpl_grid-disabled").jqote(h)).dialog({
  9274. title: "Grid Off",
  9275. buttons: {
  9276. Ok: function() {
  9277. u.off("change"),
  9278. u.dialog("destroy").remove()
  9279. }
  9280. },
  9281. beforeClose: function() {
  9282. u.off("change"),
  9283. u.dialog("destroy").remove()
  9284. }
  9285. })
  9286. } else
  9287. d20.engine.gridaligner.target = n[0],
  9288. d20plus.setMode("gridalign"),
  9289. i();
  9290. else if ("side_random" == e) {
  9291. d20.engine.canvas.getActiveGroup() && d20.engine.unselect();
  9292. var d = [];
  9293. _.each(n, function(e) {
  9294. if (e.model && "" != e.model.get("sides")) {
  9295. var t = e.model.get("sides").split("|")
  9296. , n = t.length
  9297. , i = d20.textchat.diceengine.random(n);
  9298. e.model.save({
  9299. currentSide: i,
  9300. imgsrc: unescape(t[i])
  9301. }),
  9302. d.push(t[i])
  9303. }
  9304. }),
  9305. d20.textchat.rawChatInput({
  9306. type: "tokenroll",
  9307. content: d.join("|")
  9308. }),
  9309. i()
  9310. } else if ("side_choose" == e) {
  9311. var l = n[0]
  9312. , h = l.model.toJSON()
  9313. , p = h.currentSide;
  9314. h.sidesarray = h.sides.split("|");
  9315. var c = $($("#tmpl_chooseside").jqote(h)).dialog({
  9316. title: "Choose Side",
  9317. width: 325,
  9318. height: 225,
  9319. buttons: {
  9320. Choose: function() {
  9321. d20.engine.canvas.getActiveGroup() && d20.engine.unselect(),
  9322. l.model.save({
  9323. currentSide: p,
  9324. imgsrc: unescape(h.sidesarray[p])
  9325. }),
  9326. l = null,
  9327. h = null,
  9328. c.off("slide"),
  9329. c.dialog("destroy").remove()
  9330. },
  9331. Cancel: function() {
  9332. l = null,
  9333. h = null,
  9334. c.off("slide"),
  9335. c.dialog("destroy").remove()
  9336. }
  9337. },
  9338. beforeClose: function() {
  9339. l = null,
  9340. h = null,
  9341. c.off("slide"),
  9342. c.dialog("destroy").remove()
  9343. }
  9344. });
  9345. c.find(".sideslider").slider({
  9346. min: 0,
  9347. max: h.sidesarray.length - 1,
  9348. step: 1,
  9349. value: h.currentSide
  9350. }),
  9351. c.on("slide", function(e, t) {
  9352. t.value != p && (p = t.value,
  9353. c.find(".sidechoices .sidechoice").hide().eq(t.value).show())
  9354. }),
  9355. c.find(".sidechoices .sidechoice").hide().eq(h.currentSide).show(),
  9356. i()
  9357. }
  9358. // BEGIN MOD
  9359. if ("rollsaves" === e) {
  9360. const sel = d20.engine.selected();
  9361.  
  9362. const options = ["str", "dex", "con", "int", "wis", "cha"].map(it => `<option value='${it}'>${Parser.attAbvToFull(it)}</option>`);
  9363. 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>");
  9364. dialog.dialog({
  9365. title: "Input Value",
  9366. beforeClose: function() {
  9367. return false;
  9368. },
  9369. buttons: {
  9370. Submit: function() {
  9371. const val = Parser.attAbvToFull(dialog.find("select").val());
  9372. console.log(val);
  9373. d20.engine.unselect();
  9374. sel.forEach(it => {
  9375. d20.engine.select(it);
  9376. 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}]]}}`;
  9377. d20.textchat.doChatInput(toRoll);
  9378. d20.engine.unselect();
  9379. });
  9380.  
  9381. dialog.off();
  9382. dialog.dialog("destroy").remove();
  9383. d20.textchat.$textarea.focus();
  9384. },
  9385. Cancel: function() {
  9386. dialog.off();
  9387. dialog.dialog("destroy").remove();
  9388. }
  9389. }
  9390. });
  9391.  
  9392. i();
  9393. } else if ("rollinit" === e) {
  9394. const sel = d20.engine.selected();
  9395. d20.engine.unselect();
  9396. sel.forEach(it => {
  9397. d20.engine.select(it);
  9398. 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}}`;
  9399. d20.textchat.doChatInput(toRoll);
  9400. d20.engine.unselect();
  9401. });
  9402. i();
  9403. }
  9404. // END MOD
  9405. return !1
  9406. }
  9407. }),
  9408. !1
  9409. };
  9410. // END ROLL20 CODE
  9411. d20.token_editor.showContextMenu = r;
  9412. d20.token_editor.closeContextMenu = i;
  9413. $(`#editor-wrapper`).on("click", d20.token_editor.closeContextMenu);
  9414. },
  9415.  
  9416. enhanceSnap: () => {
  9417. /**
  9418. * Dumb variable names copy-pasted from uglified code
  9419. * @param c x co-ord
  9420. * @param u y c-ord
  9421. * @returns {*[]} 2-len array; [0] = x and [1] = y
  9422. */
  9423. function getClosestHexPoint (c, u) {
  9424. function getEuclidDist (x1, y1, x2, y2) {
  9425. return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
  9426. }
  9427.  
  9428. const hx = d20.canvas_overlay.activeHexGrid.GetHexAt({
  9429. X: c,
  9430. Y: u
  9431. });
  9432.  
  9433. let minDist = 1000000;
  9434. let minPoint = [c, u];
  9435.  
  9436. function checkDist(x1, y1) {
  9437. const dist = getEuclidDist(x1, y1, c, u);
  9438. if (dist < minDist) {
  9439. minDist = dist;
  9440. minPoint = [x1, y1];
  9441. }
  9442. }
  9443. hx.Points.forEach(pt => {
  9444. checkDist(pt.X, pt.Y);
  9445. });
  9446. checkDist(hx.MidPoint.X, hx.MidPoint.Y);
  9447.  
  9448. return minPoint;
  9449. }
  9450.  
  9451. // BEGIN ROLL20 CODE
  9452. const M = function(e) {
  9453. //BEGIN MOD
  9454. var t = d20.engine.canvas;
  9455. var s = $("#editor-wrapper");
  9456. // END MOD
  9457. var n, r;
  9458. if (d20.tddice && d20.tddice.handleInteraction && d20.tddice.handleInteraction(),
  9459. e.touches) {
  9460. if ("pan" == d20.engine.mode)
  9461. return;
  9462. e.touches.length > 1 && (R = d20.engine.mode,
  9463. d20.engine.mode = "pan",
  9464. d20.engine.leftMouseIsDown = !0),
  9465. d20.engine.lastTouchStarted = (new Date).getTime(),
  9466. n = e.touches[0].pageX,
  9467. r = e.touches[0].pageY,
  9468. e.preventDefault()
  9469. } else
  9470. n = e.pageX,
  9471. r = e.pageY;
  9472. for (var o = d20.engine.showLastPaths.length; o--; )
  9473. "selected" == d20.engine.showLastPaths[o].type && d20.engine.showLastPaths.splice(o, 1);
  9474. d20.engine.handleMetaKeys(e),
  9475. ("select" == d20.engine.mode || "path" == d20.engine.mode) && t.__onMouseDown(e),
  9476. (1 == e.which || e.touches && 1 == e.touches.length) && (d20.engine.leftMouseIsDown = !0);
  9477. var a = Math.floor(n / d20.engine.canvasZoom + d20.engine.currentCanvasOffset[0] - d20.engine.paddingOffset[0] / d20.engine.canvasZoom)
  9478. , l = Math.floor(r / d20.engine.canvasZoom + d20.engine.currentCanvasOffset[1] - d20.engine.paddingOffset[1] / d20.engine.canvasZoom);
  9479. if (d20.engine.lastMousePos = [a, l],
  9480. !d20.engine.leftMouseIsDown || "fog-reveal" != d20.engine.mode && "fog-hide" != d20.engine.mode && "gridalign" != d20.engine.mode) {
  9481. if (d20.engine.leftMouseIsDown && "fog-polygonreveal" == d20.engine.mode) {
  9482. // BEGIN MOD
  9483. var c = a;
  9484. var u = l;
  9485. if (0 != d20.engine.snapTo && (e.shiftKey && !d20.Campaign.activePage().get("adv_fow_enabled") || !e.shiftKey && d20.Campaign.activePage().get("adv_fow_enabled"))) {
  9486. if ("square" == d20.Campaign.activePage().get("grid_type")) {
  9487. c = d20.engine.snapToIncrement(c, d20.engine.snapTo);
  9488. u = d20.engine.snapToIncrement(u, d20.engine.snapTo);
  9489. } else {
  9490. const minPoint = getClosestHexPoint(c, u);
  9491. c = minPoint[0];
  9492. u = minPoint[1];
  9493. }
  9494. }
  9495. 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) {
  9496. d20.engine.fog.points.push([d20.engine.fog.points[0][0], d20.engine.fog.points[0][1]]);
  9497. d20.engine.finishPolygonReveal();
  9498. } else {
  9499. d20.engine.fog.points.push([c, u]);
  9500. }
  9501. d20.engine.drawOverlays();
  9502. // END MOD
  9503. } else if (d20.engine.leftMouseIsDown && "measure" == d20.engine.mode) {
  9504. if (d20.engine.measure.down[0] = a,
  9505. d20.engine.measure.down[1] = l,
  9506. 0 != d20.engine.snapTo && !e.altKey)
  9507. if ("square" == d20.Campaign.activePage().get("grid_type"))
  9508. 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),
  9509. 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);
  9510. else {
  9511. var d = d20.canvas_overlay.activeHexGrid.GetHexAt({
  9512. X: d20.engine.measure.down[0],
  9513. Y: d20.engine.measure.down[1]
  9514. });
  9515. d20.engine.measure.down[1] = d.MidPoint.Y,
  9516. d20.engine.measure.down[0] = d.MidPoint.X
  9517. }
  9518. } else if (d20.engine.leftMouseIsDown && "fxtools" == d20.engine.mode)
  9519. d20.engine.fx.current || (d20.engine.fx.current = d20.fx.handleClick(a, l));
  9520. else if (d20.engine.leftMouseIsDown && "text" == d20.engine.mode) {
  9521. var h = {
  9522. fontFamily: $("#font-family").val(),
  9523. fontSize: $("#font-size").val(),
  9524. fill: $("#font-color").val(),
  9525. text: "",
  9526. left: a,
  9527. top: l
  9528. }
  9529. , p = d20.Campaign.activePage().addText(h);
  9530. _.defer(function() {
  9531. d20.engine.editText(p.view.graphic, h.top, h.left),
  9532. setTimeout(function() {
  9533. $(".texteditor").focus()
  9534. }, 300)
  9535. })
  9536. } else if (d20.engine.leftMouseIsDown && "rect" == d20.engine.mode) {
  9537. var g = parseInt($("#path_width").val(), 10)
  9538. , f = d20.engine.drawshape.shape = {
  9539. strokewidth: g,
  9540. x: 0,
  9541. y: 0,
  9542. width: 10,
  9543. height: 10,
  9544. type: e.altKey ? "circle" : "rect"
  9545. }
  9546. , c = a
  9547. , u = l;
  9548. 0 != d20.engine.snapTo && e.shiftKey && (c = d20.engine.snapToIncrement(c, d20.engine.snapTo),
  9549. u = d20.engine.snapToIncrement(u, d20.engine.snapTo)),
  9550. f.x = c,
  9551. f.y = u,
  9552. f.fill = $("#path_fillcolor").val(),
  9553. f.stroke = $("#path_strokecolor").val(),
  9554. d20.engine.drawshape.start = [n + d20.engine.currentCanvasOffset[0] - d20.engine.paddingOffset[0], r + d20.engine.currentCanvasOffset[1] - d20.engine.paddingOffset[1]],
  9555. d20.engine.renderTop()
  9556. } else if (d20.engine.leftMouseIsDown && "polygon" == d20.engine.mode) {
  9557. if (d20.engine.drawshape.shape)
  9558. var f = d20.engine.drawshape.shape;
  9559. else {
  9560. var g = parseInt($("#path_width").val(), 10)
  9561. , f = d20.engine.drawshape.shape = {
  9562. strokewidth: g,
  9563. points: [],
  9564. type: "polygon"
  9565. };
  9566. f.fill = $("#path_fillcolor").val(),
  9567. f.stroke = $("#path_strokecolor").val()
  9568. }
  9569. // BEGIN MOD
  9570. var c = a;
  9571. var u = l;
  9572. if (0 != d20.engine.snapTo && e.shiftKey) {
  9573. if ("square" == d20.Campaign.activePage().get("grid_type")) {
  9574. c = d20.engine.snapToIncrement(c, d20.engine.snapTo);
  9575. u = d20.engine.snapToIncrement(u, d20.engine.snapTo);
  9576. } else {
  9577. const minPoint = getClosestHexPoint(c, u);
  9578. c = minPoint[0];
  9579. u = minPoint[1];
  9580. }
  9581. }
  9582. if (f.points.length > 0 && Math.abs(f.points[0][0] - c) + Math.abs(f.points[0][1] - u) < 15) {
  9583. f.points.push([f.points[0][0], f.points[0][1]]);
  9584. if (f.points.length > 2) {
  9585. f.points.push([f.points[1][0], f.points[1][1]]);
  9586. }
  9587. d20.engine.finishCurrentPolygon();
  9588. } else {
  9589. f.points.push([c, u]);
  9590. }
  9591. d20.engine.debounced_renderTop();
  9592. // END MOD
  9593. } else if (d20.engine.leftMouseIsDown && "targeting" === d20.engine.mode) {
  9594. var m = d20.engine.canvas.findTarget(e, !0, !0);
  9595. return void (void 0 !== m && "image" === m.type && m.model && d20.engine.nextTargetCallback(m))
  9596. }
  9597. } else
  9598. d20.engine.fog.down[0] = a,
  9599. d20.engine.fog.down[1] = l,
  9600. 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),
  9601. 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),
  9602. d20.engine.fog.down[1] = d20.engine.snapToIncrement(d20.engine.fog.down[1], d20.engine.snapTo)));
  9603. if (window.currentPlayer && d20.engine.leftMouseIsDown && "select" == d20.engine.mode) {
  9604. if (d20.engine.pings[window.currentPlayer.id] && d20.engine.pings[window.currentPlayer.id].radius > 20)
  9605. return;
  9606. var y = a
  9607. , v = l
  9608. , b = {
  9609. left: y,
  9610. top: v,
  9611. radius: -5,
  9612. player: window.currentPlayer.id,
  9613. pageid: d20.Campaign.activePage().id,
  9614. currentLayer: window.currentEditingLayer
  9615. };
  9616. window.is_gm && e.shiftKey && (b.scrollto = !0),
  9617. d20.engine.pings[window.currentPlayer.id] = b,
  9618. d20.engine.pinging = {
  9619. downx: n,
  9620. downy: r
  9621. },
  9622. d20.engine.renderTop()
  9623. }
  9624. 3 == e.which && (d20.engine.rightMouseIsDown = !0),
  9625. 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()],
  9626. d20.engine.pan.panXY = [n, r],
  9627. d20.engine.pan.panning = !0) : d20.engine.pan.panning = !1,
  9628. // BEGIN MOD
  9629. $(`#upperCanvas`).hasClass("hasfocus") || $(`#upperCanvas`).focus()
  9630. // END MOD
  9631. };
  9632. // END ROLL20 CODE
  9633.  
  9634. if (UPPER_CANVAS_MOUSEDOWN) {
  9635. d20plus.log("Enhancing hex snap");
  9636. d20.engine.uppercanvas.removeEventListener("mousedown", UPPER_CANVAS_MOUSEDOWN);
  9637. d20.engine.uppercanvas.addEventListener("mousedown", M);
  9638. }
  9639.  
  9640. // add half-grid snap
  9641. d20.engine.snapToIncrement = function(e, t) {
  9642. if (d20plus.getCfgVal("canvas", "halfGridSnap")) {
  9643. t = t / 2;
  9644. }
  9645. return t * Math.round(e / t);
  9646. }
  9647. },
  9648.  
  9649. _getHoverGmNoteStr: (str) => {
  9650. const $initial = $(`<div>${str}</div>`);
  9651.  
  9652. let stack = "";
  9653. function recurse ($ele, depth, listDepth, inTable, inTd) {
  9654. const nodes = $ele.contents().get();
  9655.  
  9656. nodes.forEach(n => {
  9657. if (n.nodeType === 3) { // text node
  9658. const pre = listDepth ? ` ${"-".repeat(listDepth)}` : "";
  9659. const doLineEnd = !inTd;
  9660. stack += `${pre}${n.data}${doLineEnd ? "\n" : ""}`;
  9661. } else {
  9662. const $n = $(n);
  9663.  
  9664. if ($n.is("br")) {
  9665. stack += "\n";
  9666. return;
  9667. }
  9668.  
  9669. let nxtInTable = inTable;
  9670. if ($n.is("table")) {
  9671. nxtInTable = true;
  9672. }
  9673.  
  9674. if ($n.is("tr")) {
  9675. stack += "----------\n"
  9676. }
  9677.  
  9678. let nxtInTd = inTd;
  9679. if ($n.is("td")) {
  9680. stack += "|";
  9681. nxtInTd = true;
  9682. }
  9683.  
  9684. let nxtListDepth = listDepth;
  9685. if ($n.is("ul") || $n.is("li")) {
  9686. nxtListDepth = listDepth ? listDepth + 1 : 1;
  9687. }
  9688.  
  9689. recurse($(n), depth + 1, nxtListDepth, nxtInTable, nxtInTd);
  9690.  
  9691. // end TRs with a newline
  9692. if ($n.is("tr")) {
  9693. stack += "\n";
  9694. }
  9695. }
  9696. })
  9697.  
  9698. }
  9699.  
  9700. recurse($initial, 0, 0, false, false);
  9701.  
  9702. return stack;
  9703. },
  9704. _tokenHover: null,
  9705. _drawTokenHover: () => {
  9706. if (!d20plus._tokenHover || !d20plus._tokenHover.text) return;
  9707.  
  9708. const pt = d20plus._tokenHover.pt;
  9709. let txt;
  9710. try {
  9711. txt = d20plus._getHoverGmNoteStr(unescape(d20plus._tokenHover.text));
  9712. } catch (e) {
  9713. txt = "[Error - could not read GM notes - try re-save]"
  9714. }
  9715.  
  9716. function wrapText (context, text, x, y, maxWidth, lineHeight, doDraw) {
  9717. const words = text.replace(/\n/g, " \n ").split(/[ ]+/);
  9718. let line = '';
  9719.  
  9720. for(let n = 0; n < words.length; n++) {
  9721. if (words[n] === "\n") {
  9722. if (doDraw) context.fillText(line.trim(), x, y);
  9723. line = '';
  9724. y += lineHeight;
  9725. }
  9726. const testLine = line + words[n] + ' ';
  9727. const metrics = context.measureText(testLine);
  9728. const testWidth = metrics.width;
  9729. if (testWidth > maxWidth && n > 0) {
  9730. if (doDraw) context.fillText(line.trim(), x, y);
  9731. line = words[n] + ' ';
  9732. y += lineHeight;
  9733. }
  9734. else {
  9735. line = testLine;
  9736. }
  9737. }
  9738. if (doDraw) context.fillText(line.trim(), x, y);
  9739. return y;
  9740. }
  9741.  
  9742. const ctx = d20.engine.canvas.contextTop || d20.engine.canvas.contextContainer;
  9743.  
  9744. const scaleFact = (1 / d20.engine.canvasZoom);
  9745. const xOffset = pt.x > (d20.engine.canvasWidth / 2) ? -300 * scaleFact : 0;
  9746. const fontSize = scaleFact * 12;
  9747. const lineHeight = scaleFact * 18;
  9748. ctx.font = fontSize + "pt Arial Black";
  9749.  
  9750. const finalY = wrapText(ctx, txt, pt.x + xOffset, pt.y, 300 * scaleFact, lineHeight, false);
  9751.  
  9752. ctx.fillStyle = "rgba(255,255,255,0.75)";
  9753. ctx.beginPath();
  9754. ctx.rect(pt.x - (10 * scaleFact) + xOffset, pt.y - lineHeight, 320 * scaleFact, (finalY - pt.y) + (lineHeight + fontSize));
  9755. ctx.closePath();
  9756. ctx.fill();
  9757.  
  9758. ctx.fillStyle = "rgba(0,0,0,1)";
  9759. wrapText(ctx, txt, pt.x + xOffset, pt.y, 300 * scaleFact, lineHeight, true);
  9760. },
  9761. addTokenHover: () => {
  9762. // BEGIN ROLL20 CODE
  9763. d20.engine.drawOverlaysTop = function(e) {
  9764. e.globalCompositeOperation = "lighter";
  9765. d20.fx.render(e);
  9766. e.globalCompositeOperation = "source-over";
  9767. d20.engine.redrawSightTokens(e);
  9768. d20.engine.drawShadowMovements(e);
  9769. d20.engine.drawMeasurements(e);
  9770. d20.engine.drawPings(e);
  9771. d20.engine.drawInProgressDrawings(e);
  9772.  
  9773. // BEGIN MOD
  9774. d20plus._drawTokenHover();
  9775. // END MOD
  9776. };
  9777. // END ROLL20 CODE
  9778.  
  9779. // store data for the rendering function to access
  9780. d20.engine.canvas.on("mouse:move", (data, ...others) => {
  9781. if (data.target && data.target.model && data.e.shiftKey) {
  9782. d20.engine.renderTop();
  9783. const gmNotes = data.target.model.get("gmnotes");
  9784. const pt = d20.engine.canvas.getPointer(data.e);
  9785. pt.x -= d20.engine.currentCanvasOffset[0];
  9786. pt.y -= d20.engine.currentCanvasOffset[1];
  9787. d20plus._tokenHover = {
  9788. pt: pt,
  9789. text: gmNotes
  9790. }
  9791. } else {
  9792. if (d20plus._tokenHover) d20.engine.renderTop();
  9793. d20plus._tokenHover = null;
  9794. }
  9795. })
  9796. },
  9797.  
  9798. enhanceMarkdown: () => {
  9799. const OUT_STRIKE = "<span style='text-decoration: line-through'>$1</span>";
  9800.  
  9801. // BEGIN ROLL20 CODE
  9802. window.Markdown.parse = function(e) {
  9803. {
  9804. var t = e
  9805. , n = []
  9806. , i = [];
  9807. -1 != t.indexOf("\r\n") ? "\r\n" : -1 != t.indexOf("\n") ? "\n" : ""
  9808. }
  9809. return t = t.replace(/{{{([\s\S]*?)}}}/g, function(e) {
  9810. return n.push(e.substring(3, e.length - 3)),
  9811. "{{{}}}"
  9812. }),
  9813. t = t.replace(new RegExp("<pre>([\\s\\S]*?)</pre>","gi"), function(e) {
  9814. return i.push(e.substring(5, e.length - 6)),
  9815. "<pre></pre>"
  9816. }),
  9817. // BEGIN MOD
  9818. t = t.replace(/~~(.*?)~~/g, OUT_STRIKE),
  9819. // END MOD
  9820. t = t.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>"),
  9821. t = t.replace(/\*(.*?)\*/g, "<em>$1</em>"),
  9822. t = t.replace(/``(.*?)``/g, "<code>$1</code>"),
  9823. t = t.replace(/\[([^\]]+)\]\(([^)]+(\.png|\.gif|\.jpg|\.jpeg))\)/g, '<a href="$2"><img src="$2" alt="$1" /></a>'),
  9824. t = t.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>'),
  9825. t = t.replace(new RegExp("<pre></pre>","g"), function() {
  9826. return "<pre>" + i.shift() + "</pre>"
  9827. }),
  9828. t = t.replace(/{{{}}}/g, function() {
  9829. return n.shift()
  9830. })
  9831. }
  9832. // END ROLL20 CODE
  9833.  
  9834. // after a short delay, replace any old content in the chat
  9835. setTimeout(() => {
  9836. $(`.message`).each(function () {
  9837. $(this).html($(this).html().replace(/~~(.*?)~~/g, OUT_STRIKE))
  9838. })
  9839. }, 2500);
  9840. },
  9841.  
  9842. // JOURNAL UI //////////////////////////////////////////////////////////////////////////////////////////////////////
  9843.  
  9844. lastClickedFolderId: null,
  9845.  
  9846. addJournalCommands: () => {
  9847. // Create new Journal commands
  9848. // stash the folder ID of the last folder clicked
  9849. $("#journalfolderroot").on("contextmenu", ".dd-content", function (e) {
  9850. if ($(this).parent().hasClass("dd-folder")) {
  9851. const lastClicked = $(this).parent();
  9852. d20plus.lastClickedFolderId = lastClicked.attr("data-globalfolderid");
  9853. }
  9854.  
  9855.  
  9856. if ($(this).parent().hasClass("character")) {
  9857. $(`.Vetools-make-tokenactions`).show();
  9858. } else {
  9859. $(`.Vetools-make-tokenactions`).hide();
  9860. }
  9861. });
  9862.  
  9863. var first = $("#journalitemmenu ul li").first();
  9864. // "Make Tokenactions" option
  9865. first.after(`<li class="Vetools-make-tokenactions" data-action-type="additem">Make Tokenactions</li>`);
  9866. $("#journalitemmenu ul").on(window.mousedowntype, "li[data-action-type=additem]", function () {
  9867. var id = $currentItemTarget.attr("data-itemid");
  9868. var character = d20.Campaign.characters.get(id);
  9869. d20plus.log("Making Token Actions..");
  9870. if (character) {
  9871. var npc = character.attribs.find(function (a) {
  9872. return a.get("name").toLowerCase() == "npc";
  9873. });
  9874. var isNPC = npc ? parseInt(npc.get("current")) : 0;
  9875. if (isNPC) {
  9876. //Npc specific tokenactions
  9877. character.abilities.create({
  9878. name: "Perception",
  9879. istokenaction: true,
  9880. action: d20plus.actionMacroPerception
  9881. });
  9882. character.abilities.create({
  9883. name: "DR/Immunities",
  9884. istokenaction: true,
  9885. action: d20plus.actionMacroDrImmunities
  9886. });
  9887. character.abilities.create({
  9888. name: "Stats",
  9889. istokenaction: true,
  9890. action: d20plus.actionMacroStats
  9891. });
  9892. character.abilities.create({
  9893. name: "Saves",
  9894. istokenaction: true,
  9895. action: d20plus.actionMacroSaves
  9896. });
  9897. character.abilities.create({
  9898. name: "Skill-Check",
  9899. istokenaction: true,
  9900. action: d20plus.actionMacroSkillCheck
  9901. });
  9902. character.abilities.create({
  9903. name: "Ability-Check",
  9904. istokenaction: true,
  9905. action: d20plus.actionMacroAbilityCheck
  9906. });
  9907. } else {
  9908. //player specific tokenactions
  9909. //@{selected|repeating_attack_$0_atkname}
  9910. character.abilities.create({
  9911. name: "Attack 1",
  9912. istokenaction: true,
  9913. action: "%{selected|repeating_attack_$0_attack}"
  9914. });
  9915. character.abilities.create({
  9916. name: "Attack 2",
  9917. istokenaction: true,
  9918. action: "%{selected|repeating_attack_$1_attack}"
  9919. });
  9920. character.abilities.create({
  9921. name: "Attack 3",
  9922. istokenaction: true,
  9923. action: "%{selected|repeating_attack_$2_attack}"
  9924. });
  9925. character.abilities.create({
  9926. name: "Tool 1",
  9927. istokenaction: true,
  9928. action: "%{selected|repeating_tool_$0_tool}"
  9929. });
  9930. //" + character.get("name") + "
  9931. character.abilities.create({
  9932. name: "Whisper GM",
  9933. istokenaction: true,
  9934. action: "/w gm ?{Message to whisper the GM?}"
  9935. });
  9936. character.abilities.create({
  9937. name: "Favorite Spells",
  9938. istokenaction: true,
  9939. 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)}}"
  9940. });
  9941. character.abilities.create({
  9942. name: "Dual Attack",
  9943. istokenaction: false,
  9944. action: "%{selected|repeating_attack_$0_attack}\n\r%{selected|repeating_attack_$0_attack}"
  9945. });
  9946. character.abilities.create({
  9947. name: "Saves",
  9948. istokenaction: true,
  9949. 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"
  9950. });
  9951. character.abilities.create({
  9952. name: "Skill-Check",
  9953. istokenaction: true,
  9954. 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}"
  9955. });
  9956. }
  9957. //for everyone
  9958. character.abilities.create({
  9959. name: "Initiative",
  9960. istokenaction: true,
  9961. action: d20plus.actionMacroInit
  9962. });
  9963. }
  9964. });
  9965.  
  9966. // "Duplicate" option
  9967. first.after("<li data-action-type=\"cloneitem\">Duplicate</li>");
  9968. first.after("<li style=\"height: 10px;\">&nbsp;</li>");
  9969. $("#journalitemmenu ul").on(window.mousedowntype, "li[data-action-type=cloneitem]", function () {
  9970. var id = $currentItemTarget.attr("data-itemid");
  9971. var character = d20.Campaign.characters.get(id);
  9972. var handout = d20.Campaign.handouts.get(id);
  9973. d20plus.log("Duplicating..");
  9974. if (character) {
  9975. character.editview.render();
  9976. character.editview.$el.find("button.duplicate").trigger("click");
  9977. }
  9978. if (handout) {
  9979. handout.view.render();
  9980. var json = handout.toJSON();
  9981. delete json.id;
  9982. json.name = "Copy of " + json.name;
  9983. handout.collection.create(json, {
  9984. success: function (h) {
  9985. handout._getLatestBlob("gmnotes", function (gmnotes) {
  9986. h.updateBlobs({gmnotes: gmnotes});
  9987. });
  9988. handout._getLatestBlob("notes", function (notes) {
  9989. h.updateBlobs({notes: notes});
  9990. });
  9991. }
  9992. });
  9993. }
  9994. });
  9995.  
  9996. // New command on FOLDERS
  9997. var last = $("#journalmenu ul li").last();
  9998. last.after("<li style=\"background-color: #FA5050; color: white;\" data-action-type=\"fulldelete\">Delete Folder + Contents</li>");
  9999. $("#journalmenu ul").on(window.mousedowntype, "li[data-action-type=fulldelete]", function () {
  10000. d20plus.importer.recursiveRemoveDirById(d20plus.lastClickedFolderId, true);
  10001. d20plus.lastClickedFolderId = null;
  10002. $("#journalmenu").hide();
  10003. });
  10004. },
  10005.  
  10006. // CSS /////////////////////////////////////////////////////////////////////////////////////////////////////////////
  10007. baseCssRules: [
  10008. // generic
  10009. {
  10010. s: ".display-inline-block",
  10011. r: "display: inline-block;"
  10012. },
  10013. // page view enhancement
  10014. {
  10015. s: "#page-toolbar",
  10016. r: "height: calc(90vh - 40px);"
  10017. },
  10018. {
  10019. s: "#page-toolbar .container",
  10020. r: "height: 100%; white-space: normal;"
  10021. },
  10022. {
  10023. s: "#page-toolbar .pages .availablepage",
  10024. r: "width: 100px; height: 100px;"
  10025. },
  10026. {
  10027. s: "#page-toolbar .pages .availablepage img.pagethumb",
  10028. r: "max-width: 60px; max-height: 60px;"
  10029. },
  10030. {
  10031. s: "#page-toolbar .pages .availablepage span",
  10032. r: "bottom: 1px;"
  10033. },
  10034. // search
  10035. {
  10036. s: ".Vetoolsresult",
  10037. r: "background: #ff8080;"
  10038. },
  10039. // config editor
  10040. {
  10041. s: "div.config-table-wrapper",
  10042. r: "min-height: 200px; width: 100%; height: 100%; max-height: 460px; overflow-y: auto; transform: translateZ(0);"
  10043. },
  10044. {
  10045. s: "table.config-table",
  10046. r: "width: 100%; table-layout: fixed;"
  10047. },
  10048. {
  10049. s: "table.config-table tbody tr:nth-child(odd)",
  10050. r: "background-color: #f8f8f8;"
  10051. },
  10052. {
  10053. s: "table.config-table tbody td > *",
  10054. r: "vertical-align: middle; margin: 0;"
  10055. },
  10056. {
  10057. s: ".config-name",
  10058. r: "display: inline-block; line-height: 35px; width: 100%;"
  10059. },
  10060. // tool list
  10061. {
  10062. s: ".tools-list",
  10063. r: "max-height: 70vh;"
  10064. },
  10065. {
  10066. s: ".tool-row",
  10067. r: "min-height: 40px; display: flex; flex-direction: row; align-items: center;"
  10068. },
  10069. {
  10070. s: ".tool-row:nth-child(odd)",
  10071. r: "background-color: #f0f0f0;"
  10072. },
  10073. {
  10074. s: ".tool-row > *",
  10075. r: "flex-shrink: 0;"
  10076. },
  10077. // warning overlay
  10078. {
  10079. s: "temp-warning",
  10080. 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;"
  10081. }
  10082. ],
  10083.  
  10084. baseCssRulesPlayer: [
  10085. {
  10086. s: ".player-hidden",
  10087. r: "display: none !important;"
  10088. }
  10089. ],
  10090.  
  10091. cssRules: [], // other scripts should populate this
  10092.  
  10093. // HTML AND TEMPLATES //////////////////////////////////////////////////////////////////////////////////////////////
  10094.  
  10095. addHtmlHeader: () => {
  10096. d20plus.log("Add HTML");
  10097. const $body = $("body");
  10098.  
  10099. const $wrpSettings = $(`<div id="betteR20-settings"/>`);
  10100. $("#mysettings > .content").children("hr").first().before($wrpSettings);
  10101.  
  10102. $wrpSettings.append(d20plus.settingsHtmlHeader);
  10103. if (window.is_gm) {
  10104. $(`#imagedialog`).find(`.searchbox`).find(`.tabcontainer`).first().after(d20plus.artTabHtml);
  10105. $(`a#button-add-external-art`).on(window.mousedowntype, d20plus.art.button);
  10106.  
  10107. $body.append(d20plus.configEditorHTML);
  10108. $body.append(d20plus.addArtHTML);
  10109. $body.append(d20plus.addArtMassAdderHTML);
  10110. $body.append(d20plus.toolsListHtml);
  10111. const $cfgEditor = $("#d20plus-configeditor");
  10112. $cfgEditor.dialog({
  10113. autoOpen: false,
  10114. resizable: true,
  10115. width: 800,
  10116. height: 650,
  10117. });
  10118. $cfgEditor.parent().append(d20plus.configEditorButtonBarHTML);
  10119. $("#d20plus-artfolder").dialog({
  10120. autoOpen: false,
  10121. resizable: true,
  10122. width: 800,
  10123. height: 400,
  10124. });
  10125. $("#d20plus-artmassadd").dialog({
  10126. autoOpen: false,
  10127. resizable: true,
  10128. width: 800,
  10129. height: 400,
  10130. });
  10131. }
  10132.  
  10133. // shared GM/player conent
  10134. // quick search box
  10135. const $iptSearch = $(`<input id="player-search" class="ui-autocomplete-input" autocomplete="off" placeholder="Quick search by name...">`);
  10136. const $wrprResults = $(`<div id="player-search-results" class="content searchbox"/>`);
  10137.  
  10138. if (window.is_gm) {
  10139. $iptSearch.css("width", "calc(100% - 5px)");
  10140. const $addPoint = $("#journal").find("button.btn.superadd");
  10141. $addPoint.after($wrprResults);
  10142. $addPoint.after(`<br>`);
  10143. $addPoint.after($iptSearch);
  10144. $addPoint.after(`<br><br>`);
  10145. } else {
  10146. const $wrprControls = $(`<div class="content searchbox" id="search-wrp-controls"/>`);
  10147. $(`#journal .content`).before($wrprControls).before($wrprResults);
  10148. $iptSearch.css("max-width", "calc(100% - 140px)");
  10149. $wrprControls.append($iptSearch);
  10150. }
  10151. d20plus.initQuickSearch($iptSearch, $wrprResults);
  10152. },
  10153.  
  10154. addHtmlFooter: () => {
  10155. const $wrpSettings = $(`#betteR20-settings`);
  10156. $wrpSettings.append(d20plus.settingsHtmlPtFooter);
  10157.  
  10158. $("#mysettings > .content a#button-edit-config").on(window.mousedowntype, d20plus.openConfigEditor);
  10159. d20plus.addTools();
  10160. },
  10161.  
  10162. settingsHtmlPtFooter:
  10163. `<p>
  10164. <a class="btn player-hidden" href="#" id="button-edit-config" style="margin-top: 3px;">Edit Config</a>
  10165. </p>
  10166. <p>
  10167. For help, advice, and updates, <a href="https://discord.gg/Zk34CUZ" target="_blank" style="color: #08c;">join our Discord!</a>
  10168. </p>
  10169. <p>
  10170. <a class="btn player-hidden" href="#" id="button-view-tools" style="margin-top: 3px;">Open Tools List</a>
  10171. </p>
  10172. <style id="dynamicStyle"></style>
  10173. `,
  10174.  
  10175. artTabHtml: `
  10176. <p><a class="btn" href="#" id="button-add-external-art">Manage External Art</a></p>
  10177. `,
  10178.  
  10179. addArtHTML: `
  10180. <div id="d20plus-artfolder" title="External Art" style="position: relative">
  10181. <p>Add external images by URL. Any direct link to an image should work.</p>
  10182. <p>
  10183. <input placeholder="Name*" id="art-list-add-name">
  10184. <input placeholder="URL*" id="art-list-add-url">
  10185. <a class="btn" href="#" id="art-list-add-btn">Add URL</a>
  10186. <a class="btn" href="#" id="art-list-multi-add-btn">Add Multiple URLs...</a>
  10187. <p/>
  10188. <hr>
  10189. <div id="art-list-container">
  10190. <input class="search" autocomplete="off" placeholder="Search list..." style="width: 100%;">
  10191. <br>
  10192. <p>
  10193. <span style="display: inline-block; width: 40%; font-weight: bold;">Name</span>
  10194. <span style="display: inline-block; font-weight: bold;">URL</span>
  10195. </p>
  10196. <ul class="list artlist" style="max-height: 600px; overflow-y: scroll; display: block; margin: 0;"></ul>
  10197. </div>
  10198. </div>`,
  10199.  
  10200. addArtMassAdderHTML: `
  10201. <div id="d20plus-artmassadd" title="Mass Add Art URLs">
  10202. <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>
  10203. <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>
  10204. </div>`,
  10205.  
  10206. artListHTML: `
  10207. <div id="Vetoolsresults">
  10208. <ol class="dd-list" id="image-search-none"><div class="alert white">No results found in 5etools for those keywords.</div></ol>
  10209.  
  10210. <ol class="dd-list" id="image-search-has-results">
  10211. <li class="dd-item dd-folder Vetoolsresult">
  10212. <div class="dd-content">
  10213. <div class="folder-title">From 5etools</div>
  10214. </div>
  10215.  
  10216. <ol class="dd-list Vetoolsresultfolder" id="custom-art-results"></ol>
  10217. </li>
  10218. </ol>
  10219. </div>`,
  10220.  
  10221. configEditorHTML: `
  10222. <div id="d20plus-configeditor" title="Config Editor" style="position: relative">
  10223. <!-- populate with js -->
  10224. </div>`,
  10225.  
  10226. configEditorButtonBarHTML: `
  10227. <div class="ui-dialog-buttonpane ui-widget-content ui-helper-clearfix">
  10228. <div class="ui-dialog-buttonset">
  10229. <button type="button" id="configsave" alt="Save" title="Save Config" class="btn" role="button" aria-disabled="false">
  10230. <span>Save</span>
  10231. </button>
  10232. </div>
  10233. </div>
  10234. `,
  10235.  
  10236. toolsListHtml: `
  10237. <div id="d20-tools-list" title="Tools List" style="position: relative">
  10238. <div class="tools-list">
  10239. <!-- populate with js -->
  10240. </div>
  10241. </div>
  10242. `,
  10243.  
  10244. template_TokenEditor: `
  10245. <script id='tmpl_tokeneditor' type='text/html'>
  10246. <div class='dialog largedialog tokeneditor' style='display: block;'>
  10247. <ul class='nav nav-tabs'>
  10248. <li class='active'>
  10249. <a data-tab='basic' href='javascript:void(0);'>Basic</a>
  10250. </li>
  10251. <li>
  10252. <a data-tab='advanced' href='javascript:void(0);'>Advanced</a>
  10253. </li>
  10254. </ul>
  10255. <div class='tab-content'>
  10256. <div class='basic tab-pane'>
  10257. <div style='float: left; width: 300px;'>
  10258. <div style='float: right; margin-right: 85px; font-size: 1.2em; position: relative; top: -4px; cursor: help;'>
  10259. <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>
  10260. </div>
  10261. <label>Represents Character</label>
  10262. <select class='represents'>
  10263. <option value=''>None/Generic Token</option>
  10264. <$ _.each(window.Campaign.activeCharacters(), function(char) { $>
  10265. <option value="<$!char.id$>"><$!char.get("name")$></option>
  10266. <$ }); $>
  10267. </select>
  10268. <div class='clear'></div>
  10269. <div style='float: right; margin-right: 75px;'>
  10270. <label>
  10271. <input class='showname' type='checkbox' value='1'>
  10272. Show nameplate?
  10273. </label>
  10274. </div>
  10275. <label>Name</label>
  10276. <input class='name' style='width: 210px;' type='text'>
  10277. <div class='clear'></div>
  10278. <label>Controlled By</label>
  10279. <$ if(this.character) { $>
  10280. <p>(Determined by Character settings)</p>
  10281. <$ } else { $>
  10282. <select class='controlledby chosen' multiple='true'>
  10283. <option value='all'>All Players</option>
  10284. <$ window.Campaign.players.each(function(player) { $>
  10285. <option value="<$!player.id$>"><$!player.get("displayname")$></option>
  10286. <$ }); $>
  10287. </select>
  10288. <$ } $>
  10289. <div class='clear' style='height: 10px;'></div>
  10290. <label>
  10291. Tint Color
  10292. </label>
  10293. <input class='tint_color colorpicker' type='text'>
  10294. <div class='clear'></div>
  10295. </div>
  10296. <div style='float: left; width: 300px;'>
  10297. <label>
  10298. <span class='bar_color_indicator' style='background-color: <$!window.Campaign.get('bar1_color')$>'></span>
  10299. Bar 1
  10300. </label>
  10301. <div class='clear' style='height: 1px;'></div>
  10302. <div class='inlineinputs' style='margin-top: 5px; margin-bottom: 5px;'>
  10303. <input class='bar1_value' type='text'>
  10304. /
  10305. <input class='bar1_max' type='text'>
  10306. <$ if(this.character) { $>
  10307. <div style='float: right;'>
  10308. <select class='bar1_link' style='width: 125px;'>
  10309. <option value=''>None</option>
  10310. <$ _.each(this.tokensettingsview.availAttribs(), function(attrib) { $>
  10311. <option value="<$!attrib.id$>"><$!attrib.name$>
  10312. <$ }); $>
  10313. </select>
  10314. <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>
  10315. </div>
  10316. <$ } $>
  10317. </div>
  10318. <span style='color: #888;'>(Leave blank for no bar)</span>
  10319. <div class='clear'></div>
  10320. <label>
  10321. <span class='bar_color_indicator' style='background-color: <$!window.Campaign.get('bar2_color')$>'></span>
  10322. Bar 2
  10323. </label>
  10324. <div class='inlineinputs' style='margin-top: 5px; margin-bottom: 5px;'>
  10325. <input class='bar2_value' type='text'>
  10326. /
  10327. <input class='bar2_max' type='text'>
  10328. <$ if(this.character) { $>
  10329. <div style='float: right; margin-right: 30px;'>
  10330. <select class='bar2_link' style='width: 125px;'>
  10331. <option value=''>None</option>
  10332. <$ _.each(this.tokensettingsview.availAttribs(), function(attrib) { $>
  10333. <option value="<$!attrib.id$>"><$!attrib.name$>
  10334. <$ }); $>
  10335. </select>
  10336. </div>
  10337. <$ } $>
  10338. </div>
  10339. <span style='color: #888;'>(Leave blank for no bar)</span>
  10340. <div class='clear'></div>
  10341. <label>
  10342. <span class='bar_color_indicator' style='background-color: <$!window.Campaign.get('bar3_color')$>'></span>
  10343. Bar 3
  10344. </label>
  10345. <div class='inlineinputs' style='margin-top: 5px; margin-bottom: 5px;'>
  10346. <input class='bar3_value' type='text'>
  10347. /
  10348. <input class='bar3_max' type='text'>
  10349. <$ if(this.character) { $>
  10350. <div style='float: right; margin-right: 30px;'>
  10351. <select class='bar3_link' style='width: 125px;'>
  10352. <option value=''>None</option>
  10353. <$ _.each(this.tokensettingsview.availAttribs(), function(attrib) { $>
  10354. <option value="<$!attrib.id$>"><$!attrib.name$>
  10355. <$ }); $>
  10356. </select>
  10357. </div>
  10358. <$ } $>
  10359. </div>
  10360. <span style='color: #888;'>(Leave blank for no bar)</span>
  10361. <div class='clear' style='height: 10px;'></div>
  10362. <div style='float: left; width: 130px;'>
  10363. <div style='float: right;'>
  10364. <label>
  10365. <input class='aura1_square' type='checkbox'>
  10366. Square
  10367. </label>
  10368. </div>
  10369. <label>
  10370. Aura 1
  10371. </label>
  10372. <div class='inlineinputs' style='margin-top: 5px;'>
  10373. <input class='aura1_radius' type='text'>
  10374. <$!window.Campaign.activePage().get("scale_units")$>.
  10375. <input class='aura1_color colorpicker' type='text'>
  10376. </div>
  10377. </div>
  10378. <div style='float: left; width: 130px; margin-left: 20px;'>
  10379. <div style='float: right;'>
  10380. <label>
  10381. <input class='aura2_square' type='checkbox'>
  10382. Square
  10383. </label>
  10384. </div>
  10385. <label>
  10386. Aura 2
  10387. </label>
  10388. <div class='inlineinputs' style='margin-top: 5px;'>
  10389. <input class='aura2_radius' type='text'>
  10390. <$!window.Campaign.activePage().get("scale_units")$>.
  10391. <input class='aura2_color colorpicker' type='text'>
  10392. </div>
  10393. </div>
  10394. <div class='clear'></div>
  10395. </div>
  10396. <div class='clear'></div>
  10397. <hr>
  10398. <h4>
  10399. GM Notes
  10400. <span style='font-weight: regular; font-size: 0.9em;'>(Only visible to GMs)</span>
  10401. </h4>
  10402. <textarea class='gmnotes'></textarea>
  10403. <div class='clear'></div>
  10404. <label>&nbsp;</label>
  10405. </div>
  10406. <div class='advanced tab-pane'>
  10407. <div class='row-fluid'>
  10408. <div class='span6'>
  10409. <h4>Player Permissions</h4>
  10410. <div style='margin-left: 5px;'>
  10411. <div class='inlineinputs'>
  10412. <label style='width: 40px;'>Name</label>
  10413. <label>
  10414. <input class='showplayers_name' type='checkbox'>
  10415. See
  10416. </label>
  10417. <label>
  10418. <input class='playersedit_name' type='checkbox'>
  10419. Edit
  10420. </label>
  10421. </div>
  10422. <div class='clear' style='height: 5px;'></div>
  10423. <div class='inlineinputs'>
  10424. <label style='width: 40px;'>Bar 1</label>
  10425. <label>
  10426. <input class='showplayers_bar1' type='checkbox'>
  10427. See
  10428. </label>
  10429. <label>
  10430. <input class='playersedit_bar1' type='checkbox'>
  10431. Edit
  10432. </label>
  10433. </div>
  10434. <div class='clear' style='height: 5px;'></div>
  10435. <div class='inlineinputs'>
  10436. <label style='width: 40px;'>Bar 2</label>
  10437. <label>
  10438. <input class='showplayers_bar2' type='checkbox'>
  10439. See
  10440. </label>
  10441. <label>
  10442. <input class='playersedit_bar2' type='checkbox'>
  10443. Edit
  10444. </label>
  10445. </div>
  10446. <div class='clear' style='height: 5px;'></div>
  10447. <div class='inlineinputs'>
  10448. <label style='width: 40px;'>Bar 3</label>
  10449. <label>
  10450. <input class='showplayers_bar3' type='checkbox'>
  10451. See
  10452. </label>
  10453. <label>
  10454. <input class='playersedit_bar3' type='checkbox'>
  10455. Edit
  10456. </label>
  10457. </div>
  10458. <div class='clear' style='height: 5px;'></div>
  10459. <div class='inlineinputs'>
  10460. <label style='width: 40px;'>Aura 1</label>
  10461. <label>
  10462. <input class='showplayers_aura1' type='checkbox'>
  10463. See
  10464. </label>
  10465. <label>
  10466. <input class='playersedit_aura1' type='checkbox'>
  10467. Edit
  10468. </label>
  10469. </div>
  10470. <div class='clear' style='height: 5px;'></div>
  10471. <div class='inlineinputs'>
  10472. <label style='width: 40px;'>Aura 2</label>
  10473. <label>
  10474. <input class='showplayers_aura2' type='checkbox'>
  10475. See
  10476. </label>
  10477. <label>
  10478. <input class='playersedit_aura2' type='checkbox'>
  10479. Edit
  10480. </label>
  10481. </div>
  10482. <div class='clear' style='height: 10px;'></div>
  10483. <small style='text-align: left; font-size: 0.9em;'>
  10484. See: All Players can view
  10485. <br>
  10486. Edit: Controlling players can view and change
  10487. </small>
  10488. </div>
  10489. <div class='clear'></div>
  10490. </div>
  10491. <div class='span6'>
  10492. <h4>Emits Light</h4>
  10493. <div class='inlineinputs' style='margin-top: 5px; margin-bottom: 5px;'>
  10494. <input class='light_radius' type='text'>
  10495. <$!window.Campaign.activePage().get("scale_units")$>.
  10496. <input class='light_dimradius' type='text'>
  10497. <$!window.Campaign.activePage().get("scale_units")$>.
  10498. <input class='light_angle' placeholder='360' type='text'>
  10499. <span style='font-size: 2.0em;'>&deg;</span>
  10500. </div>
  10501. <span style='color: #888; padding-left: 5px;'>Light Radius / (optional) Start of Dim / Angle</span>
  10502. <div class='inlineinputs' style='margin-top: 5px;'>
  10503. <label style='margin-left: 7px;'>
  10504. <input class='light_otherplayers' type='checkbox'>
  10505. All Players See Light
  10506. </label>
  10507. </div>
  10508. <div class='inlineinputs' style='margin-top: 2px;'>
  10509. <label style='margin-left: 7px;'>
  10510. <input class='light_hassight' type='checkbox'>
  10511. Has Sight
  10512. </label>
  10513. <span style="margin-left: 9px; margin-right: 28px;">/</span>
  10514. Angle:
  10515. <input class='light_losangle' placeholder='360' type='text'>
  10516. <span style='font-size: 2.0em;'>&deg;</span>
  10517. </div>
  10518. <div class='inlineinputs' style='margin-left: 90px; margin-top: 5px;'>
  10519. <span style="margin-left: 8px; margin-right: 12px;">/</span>
  10520. Multiplyer:
  10521. <input class='light_multiplier' placeholder='1.0' style='margin-right: 10px;' type='text'>x</input>
  10522. </div>
  10523. <h4>Advanced Fog of War</h4>
  10524. <div class='inlineinputs' style='margin-top: 5px; margin-bottom: 5px;'>
  10525. <input class='advfow_viewdistance' type='text'>
  10526. <$!window.Campaign.activePage().get("scale_units")$>.
  10527. </div>
  10528. <span style='color: #888; padding-left: 5px;'>View Distance</span>
  10529. <!-- %h4 -->
  10530. <!-- Token Actions -->
  10531. <!-- %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") ? -->
  10532. <!-- %p -->
  10533. <!-- %strong Add New Token Action: -->
  10534. <!-- %br -->
  10535. <!-- %select.chosen(placeholder="Choose from the list...") -->
  10536. <!-- %option(value="") Choose from the list... -->
  10537. <!-- <$ if(this.character) { $> -->
  10538. <!-- <optgroup label="Abilities"> -->
  10539. <!-- <$ this.character.abilities.each(function(abil) { $> -->
  10540. <!-- <option value="ability|<$!abil.get('id')$>"><$!abil.get('name')$></option> -->
  10541. <!-- <$ }); $> -->
  10542. <!-- </optgroup> -->
  10543. <!-- <$ } $> -->
  10544. </div>
  10545. </div>
  10546. </div>
  10547. </div>
  10548. </div>
  10549. </script>
  10550. `,
  10551.  
  10552. template_pageSettings: `
  10553. <script id="tmpl_pagesettings" type="text/html">
  10554. <label style='padding-top: 4px;'>
  10555. <strong>Page Size</strong>
  10556. </label>
  10557. <input type="number" class="width" style="width: 50px;" value="<$!this.model.get("width")$>" />
  10558. un. by
  10559. <input type="number" class="height" style="width: 50px; margin-left: 5px;" value="<$!this.model.get("height")$>" />
  10560. un.
  10561. <small style='display: block; font-size: 0.9em; margin-left: 110px;'>width by height, 1 unit = 70 pixels</small>
  10562. <div class='clear' style='height: 15px;'></div>
  10563. <label style='margin-left: 55px; position: relative; top: 6px;'><strong>Scale:</strong> 1 unit =</label>
  10564. <input type="number" class="scale_number" style="width: 35px;" value="<$!this.model.get("scale_number")$>" />
  10565. <select class='scale_units' style='width: 50px; position: relative; top: 2px;'>
  10566. <option value='ft'>ft.</option>
  10567. <option value='m'>m.</option>
  10568. <option value='km'>km.</option>
  10569. <option value='mi'>mi.</option>
  10570. <option value='in'>in.</option>
  10571. <option value='cm'>cm.</option>
  10572. <option value='un'>un.</option>
  10573. <option value='hex'>hex</option>
  10574. <option value='sq.'>sq.</option>
  10575. </select>
  10576. <div class='clear' style='height: 15px;'></div>
  10577. <label>
  10578. <strong>Background</strong>
  10579. </label>
  10580. <input class='pagebackground' type='text'>
  10581. <hr>
  10582. <label style='position: relative; top: 8px;'>
  10583. <strong>Grid</strong>
  10584. </label>
  10585. <label class='checkbox'>
  10586. <input class='gridenabled' type='checkbox' value='1'>
  10587. Enabled, Size:
  10588. </label>
  10589. <input type="number" class="snappingincrement" style="width: 35px;" value="<$!this.model.get("snapping_increment")$>" /> units
  10590. <div class='clear' style='height: 7px;'></div>
  10591. <label style='margin-left: 55px; position: relative; top: 4px;'>
  10592. <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>
  10593. Diagonals
  10594. </label>
  10595. <select class='diagonaltype' style='width: 100px;'>
  10596. <option value="foure" <$ if(this.model.get("diagonaltype") == "foure") { $>selected<$ } $> >D&D 4E Compatible (Default)</option>
  10597. <option value="threefive" <$ if(this.model.get("diagonaltype") == "threefive") { $>selected<$ } $> >Pathfinder/3.5E Compatible</option>
  10598. <option value="pythagorean" <$ if(this.model.get("diagonaltype") == "pythagorean") { $>selected<$ } $> >Euclidean</option>
  10599. <option value="manhattan" <$ if(this.model.get("diagonaltype") == "manhattan") { $>selected<$ } $> >Manhattan</option>
  10600. </select>
  10601. <div class='clear' style='height: 7px;'></div>
  10602. <label style='margin-left: 55px; position: relative; top: 4px;'>Type</label>
  10603. <select class='gridtype' style='width: 100px;'>
  10604. <option value="square" <$ if(this.model.get("grid_type") == "square") { $>selected<$ } $> >Square</option>
  10605. <option value="hex" <$ if(this.model.get("grid_type") == "hex") { $>selected<$ } $> >Hex (V)</option>
  10606. <option value="hexr" <$ if(this.model.get("grid_type") == "hexr") { $>selected<$ } $> >Hex (H)</option>
  10607. </select>
  10608. <div class='clear' style='height: 2px;'></div>
  10609. <label class='checkbox' style='margin-left: 130px;'>
  10610. <input class='gridlabels' type='checkbox' value='1'>&nbsp; Show Labels (Hex Only)</input>
  10611. </label>
  10612. <div class='clear' style='height: 10px;'></div>
  10613. <label style='margin-left: 55px;'>Color</label>
  10614. <input class='gridcolor' type='text'>
  10615. <div class='clear' style='height: 7px;'></div>
  10616. <label style='margin-left: 55px;'>Opacity</label>
  10617. <div class='gridopacity'></div>
  10618. <div class='clear' style='height: 10px'></div>
  10619. <hr>
  10620. <label style='position: relative; top: -2px;'>
  10621. <strong>Fog of War</strong>
  10622. </label>
  10623. <label class='checkbox'>
  10624. <input class='darknessenabled' type='checkbox' value='1'>&nbsp; Enabled</input>
  10625. </label>
  10626. <hr>
  10627. <strong style="display: block;"><i>Requires a paid subscription or all players to use a betteR20 script</i></strong>
  10628. <label style='position: relative; top: 3px; width: 85px; padding-left: 15px;'>
  10629. <strong>Advanced Fog of War</strong>
  10630. </label>
  10631. <label class='checkbox'>
  10632. <input class='advancedfowenabled showtip' style='margin-top: 8px; margin-bottom: 8px;' type='checkbox' value='1'>&nbsp; Enabled</input>
  10633. </label>
  10634. <span class='no_grid' style='display: none;'>
  10635. , Size:
  10636. <input type="number" class="advancedfowgridsize" style="width: 30px;" value="<$!this.model.get("adv_fow_grid_size")$>" /> units
  10637. </span>
  10638. <br>
  10639. <label class='checkbox'>
  10640. <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>
  10641. </label>
  10642. <br>
  10643. <label class='checkbox' style='margin-left: 110px;'>
  10644. <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>
  10645. </label>
  10646. <br>
  10647. <br>
  10648. <label style='position: relative; top: -2px;'>
  10649. <strong>Dynamic Lighting</strong>
  10650. </label>
  10651. <label class='checkbox'>
  10652. <input class='lightingenabled showtip' type='checkbox' value='1'>&nbsp; Enabled</input>
  10653. </label>
  10654. <br>
  10655. <label class='checkbox'>
  10656. <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>
  10657. </label>
  10658. <br>
  10659. <br>
  10660. <label class='checkbox' style='margin-left: 110px;'>
  10661. <input class='lightingupdate' type='checkbox' value='1'>&nbsp; Only Update on Drop</input>
  10662. </label>
  10663. <br>
  10664. <label class='checkbox' style='margin-left: 110px;'>
  10665. <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>
  10666. </label>
  10667. <br>
  10668. <label class='checkbox' style='margin-left: 110px;'>
  10669. <input class='lightglobalillum' title='Instead of darkness show light in all places players can see.' type='checkbox' value='1'>&nbsp; Global Illumination</input>
  10670. </label>
  10671. <hr>
  10672. <label style='font-weight: bold;'>GM Opacity</label>
  10673. <div class='fogopacity'></div>
  10674. <div class='clear'></div>
  10675. <hr>
  10676. <label style='font-weight: bold;'>Play on Load</label>
  10677. <select class='pagejukeboxtrigger' style='width: 180px;'></select>
  10678. <div class='clear'></div>
  10679. <hr>
  10680. <button class='delete btn btn-danger' style='float: right;'>
  10681. Delete Page
  10682. </button>
  10683. <button class='archive btn'>
  10684. Archive Page
  10685. </button>
  10686. <div class='clear'></div>
  10687. </script>
  10688. `,
  10689.  
  10690. template_actionsMenu: `
  10691. <script id='tmpl_actions_menu' type='text/html'>
  10692. <div class='actions_menu d20contextmenu'>
  10693. <ul>
  10694. <$ if(this.view && this.view.graphic.type == "image" && this.get("cardid") !== "") { $>
  10695. <li class='head hasSub' data-action-type='takecard'>Take Card</li>
  10696. <li class='head hasSub' data-action-type='flipcard'>Flip Card</li>
  10697. <$ } $>
  10698. <$ if(window.is_gm) { $>
  10699. <$ if(this.view && this.get("isdrawing") === false && window.currentEditingLayer != "map") { $>
  10700. <!-- BEGIN MOD -->
  10701. <li class='head hasSub' data-action-type='rollinit'>Roll Initiative</li>
  10702. <li class='head hasSub' data-action-type='rollsaves'>Roll Save</li>
  10703. <!-- END MOD -->
  10704. <li class='head hasSub' data-action-type='addturn'>Add Turn</li>
  10705. <$ } $>
  10706. <li class='head'>Edit</li>
  10707. <$ if(this.view) { $>
  10708. <li data-action-type='delete'>Delete</li>
  10709. <li data-action-type='copy'>Copy</li>
  10710. <$ } $>
  10711. <li data-action-type='paste'>Paste</li>
  10712. <li data-action-type='undo'>Undo</li>
  10713. <$ if(this.view) { $>
  10714. <li data-action-type='tofront'>To Front</li>
  10715. <li data-action-type='toback'>To Back</li>
  10716. <li class='head hasSub' data-menuname='advanced'>
  10717. Advanced &raquo;
  10718. <ul class='submenu' data-menuname='advanced'>
  10719. <li data-action-type='group'>Group</li>
  10720. <li data-action-type='ungroup'>Ungroup</li>
  10721. <$ if(this.get("type") == "image") { $>
  10722. <li class="<$ if (this && this.get("isdrawing")) { $>active<$ } $>" data-action-type="toggledrawing">Is Drawing</li>
  10723. <li class="<$ if (this && this.get("fliph")) { $>active<$ } $>" data-action-type="togglefliph">Flip Horizontal</li>
  10724. <li class="<$ if (this && this.get("flipv")) { $>active<$ } $>" data-action-type="toggleflipv">Flip Vertical</li>
  10725. <li data-action-type='setdimensions'>Set Dimensions</li>
  10726. <$ if(window.currentEditingLayer == "map") { $>
  10727. <li data-action-type='aligntogrid'>Align to Grid</li>
  10728. <$ } $>
  10729. <$ } $>
  10730. </ul>
  10731. </li>
  10732. <li class='head hasSub' data-menuname='positioning'>
  10733. Layer &raquo;
  10734. <ul class='submenu' data-menuname='positioning'>
  10735. <li data-action-type="tolayer_map" class='<$ if(this && this.get("layer") == "map") { $>active<$ } $>'>Map Layer</li>
  10736. <li data-action-type="tolayer_objects" class='<$ if(this && this.get("layer") == "objects") { $>active<$ } $>'>Token Layer</li>
  10737. <li data-action-type="tolayer_gmlayer" class='<$ if(this && this.get("layer") == "gmlayer") { $>active<$ } $>'>GM Layer</li>
  10738. <li data-action-type="tolayer_walls" class='<$ if(this && this.get("layer") == "walls") { $>active<$ } $>'>Lighting Layer (will not block LoS)</li>
  10739. </ul>
  10740. </li>
  10741. <$ } $>
  10742. <$ } $>
  10743. <$ if(this.view && this.get("sides") !== "" && this.get("cardid") === "") { $>
  10744. <li class='head hasSub' data-menuname='mutliside'>
  10745. Multi-Sided &raquo;
  10746. <ul class='submenu' data-menuname='multiside'>
  10747. <li data-action-type='side_random'>Random Side</li>
  10748. <li data-action-type='side_choose'>Choose Side</li>
  10749. </ul>
  10750. </li>
  10751. <$ } $>
  10752. </ul>
  10753. </div>
  10754. </script>
  10755. `,
  10756.  
  10757. template_charactereditor: `
  10758. <script id='tmpl_charactereditor' type='text/html'>
  10759. <div class='dialog largedialog charactereditor' style='display: block;'>
  10760. <!-- %ul.nav.nav-tabs -->
  10761. <!-- %li.active -->
  10762. <!-- %a(href="javascript:void(0);" data-tab="bioinfo") Bio & Info -->
  10763. <!-- %li -->
  10764. <!-- %a(href="javascript:void(0);" data-tab="attributesabilities") Attributes & Abilities -->
  10765. <div class='tab-content'>
  10766. <div class='bioinfo tab-pane'>
  10767. <div class='row-fluid'>
  10768. <div class='span5'>
  10769. <label>
  10770. <strong>Avatar</strong>
  10771. </label>
  10772. <$ if(true) { $>
  10773. <div class="avatar dropbox <$! this.get("avatar") != "" ? "filled" : "" $>" style="width: 95%;">
  10774. <div class="status"></div>
  10775. <div class="inner">
  10776. <$ if(this.get("avatar") == "") { $>
  10777. <h4 style="padding-bottom: 0px; marigin-bottom: 0px; color: #777;">Drop a file<small>(JPG, PNG, GIF)</small></h4>
  10778. <br /> or
  10779. <button class="btn">Choose a file...</button>
  10780. <input class="manual" type="file" />
  10781. <$ } else { $>
  10782. <img src="<$!this.get("avatar")$>" draggable="false" />
  10783. <div class='remove'><a href='#'>Remove</a></div>
  10784. <$ } $>
  10785. </div>
  10786. </div>
  10787. <$ } else { $>
  10788. <div class='avatar'>
  10789. <$ if(this.get("avatar") != "") { $>
  10790. <img src="<$!this.get("avatar")$>" draggable="false" />
  10791. <$ } $>
  10792. </div>
  10793. <$ } $>
  10794. <div class='clear'></div>
  10795. <!-- BEGIN MOD -->
  10796. <button class="btn character-image-by-url">Set Image from URL</button>
  10797. <div class='clear'></div>
  10798. <!-- END MOD -->
  10799. <$ if (window.is_gm) { $>
  10800. <label>
  10801. <strong>Default Token (Optional)</strong>
  10802. </label>
  10803. <div class="defaulttoken tokenslot <$! this.get("defaulttoken") !== "" ? "filled" : "" $> style="width: 95%;">
  10804. <$ if(this.get("defaulttoken") !== "") { $>
  10805. <img src="" draggable="false" />
  10806. <div class="remove"><a href="#">Remove</a></div>
  10807. <$ } else { $>
  10808. <button class="btn">Use Selected Token</button>
  10809. <small>Select a token on the tabletop to use as the Default Token</small>
  10810. <$ } $>
  10811. </div>
  10812. <$ } $>
  10813. </div>
  10814. <div class='span7'>
  10815. <label>
  10816. <strong>Name</strong>
  10817. </label>
  10818. <input class='name' type='text'>
  10819. <div class='clear'></div>
  10820. <$ if(window.is_gm) { $>
  10821. <label>
  10822. <strong>In Player's Journals</strong>
  10823. </label>
  10824. <select class='inplayerjournals chosen' multiple='true' style='width: 100%;'>
  10825. <option value="all">All Players</option>
  10826. <$ window.Campaign.players.each(function(player) { $>
  10827. <option value="<$!player.id$>"><$!player.get("displayname")$></option>
  10828. <$ }); $>
  10829. </select>
  10830. <div class='clear'></div>
  10831. <label>
  10832. <strong>Can Be Edited &amp; Controlled By</strong>
  10833. </label>
  10834. <select class='controlledby chosen' multiple='true' style='width: 100%;'>
  10835. <option value="all">All Players</option>
  10836. <$ window.Campaign.players.each(function(player) { $>
  10837. <option value="<$!player.id$>"><$!player.get("displayname")$></option>
  10838. <$ }); $>
  10839. </select>
  10840. <div class='clear'></div>
  10841. <label>
  10842. <strong>Tags</strong>
  10843. </label>
  10844. <input class='tags'>
  10845. <div class='clear'></div>
  10846. <hr>
  10847. <button class='delete btn btn-danger' style='float: right;'>
  10848. Delete
  10849. </button>
  10850. <button class='duplicate btn' style='margin-right: 10px;'>
  10851. Duplicate
  10852. </button>
  10853. <button class='archive btn'>
  10854. <$ if(this.get("archived")) { $>Restore from Archive<$ } else { $>Archive<$ } $>
  10855. </button>
  10856. <div class='clear'></div>
  10857. <$ } $>
  10858. <div class='clear'></div>
  10859. </div>
  10860. </div>
  10861. <div class='row-fluid'>
  10862. <div class='span12'>
  10863. <hr>
  10864. <label>
  10865. <strong>Bio & Info</strong>
  10866. </label>
  10867. <textarea class='bio'></textarea>
  10868. <div class='clear'></div>
  10869. <$ if(window.is_gm) { $>
  10870. <label>
  10871. <strong>GM Notes (Only visible to GM)</strong>
  10872. </label>
  10873. <textarea class='gmnotes'></textarea>
  10874. <div class='clear'></div>
  10875. <$ } $>
  10876. </div>
  10877. </div>
  10878. </div>
  10879. </div>
  10880. </div>
  10881. </script>
  10882. `,
  10883.  
  10884. template_handouteditor: `
  10885. <script id='tmpl_handouteditor' type='text/html'>
  10886. <div class='dialog largedialog handouteditor' style='display: block;'>
  10887. <div class='row-fluid'>
  10888. <div class='span12'>
  10889. <label>
  10890. <strong>Name</strong>
  10891. </label>
  10892. <input class='name' type='text'>
  10893. <div class='clear'></div>
  10894. <$ if (window.is_gm) { $>
  10895. <label>
  10896. <strong>In Player's Journals</strong>
  10897. </label>
  10898. <select class='inplayerjournals chosen' multiple='true' style='width: 100%;'>
  10899. <option value="all">All Players</option>
  10900. <$ window.Campaign.players.each(function(player) { $>
  10901. <option value="<$!player.id$>"><$!player.get("displayname")$></option>
  10902. <$ }); $>
  10903. </select>
  10904. <div class='clear'></div>
  10905. <label>
  10906. <strong>Can Be Edited By</strong>
  10907. </label>
  10908. <select class='controlledby chosen' multiple='true' style='width: 100%;'>
  10909. <option value="all">All Players</option>
  10910. <$ window.Campaign.players.each(function(player) { $>
  10911. <option value="<$!player.id$>"><$!player.get("displayname")$></option>
  10912. <$ }); $>
  10913. </select>
  10914. <div class='clear'></div>
  10915. <label>
  10916. <strong>Tags</strong>
  10917. </label>
  10918. <input class='tags'>
  10919. <div class='clear'></div>
  10920. <$ } $>
  10921. </div>
  10922. </div>
  10923. <div class='row-fluid'>
  10924. <div class='span12'>
  10925. <div class="avatar dropbox <$! this.get("avatar") != "" ? "filled" : "" $>">
  10926. <div class="status"></div>
  10927. <div class="inner">
  10928. <$ if(this.get("avatar") == "") { $>
  10929. <h4 style="padding-bottom: 0px; marigin-bottom: 0px; color: #777;">Drop a file</h4>
  10930. <br /> or
  10931. <button class="btn">Choose a file...</button>
  10932. <input class="manual" type="file" />
  10933. <$ } else { $>
  10934. <img src="<$!this.get("avatar")$>" />
  10935. <div class='remove'><a href='#'>Remove</a></div>
  10936. <$ } $>
  10937. </div>
  10938. </div>
  10939. <div class='clear'></div>
  10940. </div>
  10941. </div>
  10942. <!-- BEGIN MOD -->
  10943. <div class='row-fluid'>
  10944. <button class="btn handout-image-by-url">Set Image from URL</button>
  10945. <div class='clear'></div>
  10946. </div>
  10947. <!-- END MOD -->
  10948. <div class='row-fluid'>
  10949. <div class='span12'>
  10950. <label>
  10951. <strong>Description & Notes</strong>
  10952. </label>
  10953. <textarea class='notes'></textarea>
  10954. <div class='clear'></div>
  10955. <$ if(window.is_gm) { $>
  10956. <label>
  10957. <strong>GM Notes (Only visible to GM)</strong>
  10958. </label>
  10959. <textarea class='gmnotes'></textarea>
  10960. <div class='clear'></div>
  10961. <hr>
  10962. <button class='delete btn btn-danger' style='float: right;'>
  10963. Delete Handout
  10964. </button>
  10965. <button class='archive btn'>
  10966. <$ if(this.get("archived")) { $>Restore Handout from Archive<$ } else { $>Archive Handout<$ } $>
  10967. </button>
  10968. <div class='clear'></div>
  10969. <$ } $>
  10970. </div>
  10971. </div>
  10972. </div>
  10973. </script>
  10974. `,
  10975. };
  10976. };
  10977.  
  10978. const D20plus = function (version) {
  10979. d20plus.version = version;
  10980.  
  10981. // Window loaded
  10982. window.onload = function () {
  10983. window.unwatch("d20");
  10984. const checkLoaded = setInterval(function () {
  10985. if (!$("#loading-overlay").is(":visible")) {
  10986. clearInterval(checkLoaded);
  10987. d20plus.Init();
  10988. }
  10989. }, 1000);
  10990. };
  10991.  
  10992. /* object.watch polyfill by Eli Grey, http://eligrey.com */
  10993. if (!Object.prototype.watch) {
  10994. Object.defineProperty(Object.prototype, "watch", {
  10995. enumerable: false,
  10996. configurable: true,
  10997. writable: false,
  10998. value: function (prop, handler) {
  10999. var
  11000. oldval = this[prop],
  11001. newval = oldval,
  11002. getter = function () {
  11003. return newval;
  11004. },
  11005. setter = function (val) {
  11006. oldval = newval;
  11007. return (newval = handler.call(this, prop, oldval, val));
  11008. };
  11009. if (delete this[prop]) {
  11010. Object.defineProperty(this, prop, {
  11011. get: getter,
  11012. set: setter,
  11013. enumerable: true,
  11014. configurable: true
  11015. });
  11016. }
  11017. }
  11018. });
  11019. }
  11020. if (!Object.prototype.unwatch) {
  11021. Object.defineProperty(Object.prototype, "unwatch", {
  11022. enumerable: false,
  11023. configurable: true,
  11024. writable: false,
  11025. value: function (prop) {
  11026. var val = this[prop];
  11027. delete this[prop];
  11028. this[prop] = val;
  11029. }
  11030. });
  11031. }
  11032. /* end object.watch polyfill */
  11033.  
  11034. window.d20ext = {};
  11035. window.watch("d20ext", function (id, oldValue, newValue) {
  11036. d20plus.log("Set Development");
  11037. newValue.environment = "development";
  11038. Object.defineProperty(newValue, 'seenad', {
  11039. value: true
  11040. });
  11041. return newValue;
  11042. });
  11043. window.d20 = {};
  11044. window.watch("d20", function (id, oldValue, newValue) {
  11045. d20plus.log("Obtained d20 variable");
  11046. window.unwatch("d20ext");
  11047. window.d20ext.environment = "production";
  11048. newValue.environment = "production";
  11049. return newValue;
  11050. });
  11051. window.d20plus = d20plus;
  11052. d20plus.log("Injected");
  11053. };
  11054.  
  11055. document.addEventListener("DOMContentLoaded", function(event) {
  11056. // do some template injection
  11057. $("#tmpl_charactereditor").html($(d20plus.template_charactereditor).html());
  11058. $("#tmpl_handouteditor").html($(d20plus.template_handouteditor).html());
  11059. });
  11060.  
  11061. // if we are the topmost frame, inject
  11062. if (window.top === window.self) {
  11063. function strip (str) {
  11064. return str.substring(str.indexOf("\n") + 1, str.lastIndexOf("\n")) + "\n";
  11065. }
  11066.  
  11067. let stack = "function (version) {\n";
  11068. stack += strip(betteR20Base.toString());
  11069.  
  11070. for (let i = 0; i < SCRIPT_EXTENSIONS.length; ++i) {
  11071. stack += strip(SCRIPT_EXTENSIONS[i].toString())
  11072. }
  11073. stack += strip(D20plus.toString());
  11074.  
  11075. stack += "\n}";
  11076. unsafeWindow.eval("(" + stack + ")('" + GM_info.script.version + "')");
  11077. }
Add Comment
Please, Sign In to add comment