🐅 Theme In-Game Editor for Arras.io 🐅

Modify the look and feel of your Arras.io game, while you're playing it!

  1. // ==UserScript==
  2. // @name 🐅 Theme In-Game Editor for Arras.io 🐅
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.7
  5. // @description Modify the look and feel of your Arras.io game, while you're playing it!
  6. // @author Road6943
  7. // @match *://arras.io/
  8. // @match *://arras.netlify.app/
  9. // @require https://cdn.jsdelivr.net/npm/vue@2.6.12
  10. // @resource VERTE_CSS https://cdn.jsdelivr.net/npm/verte@0.0.12/dist/verte.css
  11. // @require https://unpkg.com/prompt-boxes@2.0.6/src/js/prompt-boxes.js
  12. // @resource PROMPT_BOXES_CSS https://unpkg.com/prompt-boxes@2.0.6/src/css/prompt-boxes.css
  13. // @require https://unpkg.com/konva@4.0.0/konva.min.js
  14. // @require https://cdn.jsdelivr.net/npm/vue-konva@1.0.7/lib/vue-konva.min.js
  15. // @grant GM_getResourceText
  16. // @grant GM_addStyle
  17. // @grant GM_setClipboard
  18. // @grant GM_getValue
  19. // @grant GM_setValue
  20. // @grant GM_deleteValue
  21. // ==/UserScript==
  22.  
  23.  
  24. /* IMPORTANT NOTES: use single quotes (') for the majority of stuff as they won't interfere with
  25. ** either HTML's " double quotes " or the js string interpolation backticks ``
  26. ** Arras() function is what allows this whole thing to work -- gives current theme values and allows you to set new ones */
  27. var CONTAINER_ID = 'app';
  28. var CANVAS_ID = 'canvas';
  29. var LAUNCH_BTN_ID = 'launch-btn';
  30.  
  31. (function() {
  32. 'use strict';
  33.  
  34. // add these css files at the start so that the launch btn can be styled and positioned above canvas from the very start
  35. // Verte css stuff can't go here because it screws with the Arras landing page styling
  36. GM_addStyle( getUserscriptSpecificCSS() ); // positions items above canvas
  37. GM_addStyle( getAppCSS() ); // adds styling for the majority of the Vue app's ui
  38. //GM_addStyle( GM_getResourceText('PROMPT_BOXES_CSS') ); // (non-blocking dialogs)
  39.  
  40. var canvas = document.getElementById(CANVAS_ID);
  41.  
  42. // add a launch button to launch the main Vue instance, it should look identical to the toggle-btn
  43. // thanks to their shared .tiger-btn class
  44. canvas.insertAdjacentHTML('beforebegin', `
  45. <button id="${LAUNCH_BTN_ID}" class="tiger-btn">
  46. 🐅 Open 🐅
  47. </button>
  48. `);
  49.  
  50. document.getElementById(LAUNCH_BTN_ID).onclick = launchApp
  51. })();
  52.  
  53.  
  54. // launch the main editor app only if user is in-game (so that the themeColor stuff is actually availiable to grab)
  55. // also destroy the initial launch-btn at the end of this function because it is no longer needed and is replaced with toggle-btn inside the main Vue app
  56. function launchApp() {
  57. if (!userIsCurrentlyInGame()) {
  58. alert('You must be in-game to use this!');
  59. return;
  60. }
  61.  
  62. // remove the launch button, so that the toggle-btn in the Vue instance can take over
  63. // and also to prevent users from accidently creating multiple Vue instances through multiple clicks
  64. document.getElementById(LAUNCH_BTN_ID).remove();
  65.  
  66. // something in arras's default css styling screws with the top color picker
  67. // by making it be too wide and overflow from the color picker container
  68. // so this removes all existing css for just that top slider, so that only Verte's css can style it
  69. // the width:85% thing is something that I used before to make the slider look correct,
  70. // but it screwed with the functionality somewhat so I removed it
  71. /* Verte-related */
  72. GM_addStyle( '.verte-picker__slider { all: unset; /* width: 85%; */ }' );
  73.  
  74. // add verte css file (color picker styling)
  75. // Must go here (in-game only, never landing page) because it screws with the Arras landing page styling somehow
  76. GM_addStyle( GM_getResourceText("VERTE_CSS") );
  77.  
  78. // add css to make the Prompt Boxes non-blocking dialog toasts work
  79. GM_addStyle( GM_getResourceText('PROMPT_BOXES_CSS') );
  80.  
  81. var canvas = document.getElementById(CANVAS_ID);
  82. canvas.insertAdjacentHTML('beforebegin', getAppHTML());
  83. runAppJS();
  84. }
  85.  
  86.  
  87. // a little hack to detect if the user is currently in game or on the main landing page
  88. function userIsCurrentlyInGame() {
  89. // playerNameInput is disabled in-game, but enabled on the main landing page (because thats how players enter their name)
  90. //return document.getElementById("playerNameInput").hasAttribute("disabled");
  91.  
  92. // ^^ that no longer works, so heres a new hack:
  93. // Arras().themeColor is undefined on the landing page, but has a value in-game
  94. return (Arras().themeColor !== undefined)
  95. }
  96.  
  97. // this is css that allows the the userscript to properly show the editor above the game canvas
  98. // and anything else that's not used for the actual app functionality (anything that wouldn't go into a codepen of the app)
  99. function getUserscriptSpecificCSS() {
  100. return `
  101.  
  102. /* These position the launch button and editor div directly above the canvas */
  103. #${CONTAINER_ID}, #${LAUNCH_BTN_ID} {
  104. position: absolute;
  105. z-index: 2;
  106. }
  107. #${CANVAS_ID} {
  108. position: absolute;
  109. z-index: 1;
  110. }
  111.  
  112. `}
  113.  
  114. // paste the vue js html code into here, but NOT the script tag stuff or the style tag stuff
  115. function getAppHTML() {
  116. return `
  117. <!--
  118. This file contains the html (vue-template html) for the main editor
  119. The @keydown.stop stuff everywhere prevents typing into inputs in tiger from also affecting the game (e.g. when typing a theme name, you don't want presssing 'e' to also toggle autofire)
  120. -->
  121.  
  122. <div id="app"
  123. :style="[showApp ? {
  124. backgroundColor: '#0004', /* not a typo */
  125. height: '50%',
  126. width: '30%',
  127. overflow: 'hidden',
  128. } : {
  129. backgroundColor: 'transparent',
  130. height: '0%',
  131. width: '0%',
  132. }]"
  133. > <!-- app when open gets a bg color of #0001 (not a typo, it's pure black but with near-complete transparency) so that its styling like table borders appear no matter which color the map bg is -->
  134. <!--MOUSEOVER BUG FIX:
  135. When the container has a set size in a css file, your mouse inputs when over the app,
  136. even if its closed, do not register with the game */
  137. /* You need to have the container have 0% height & width when closed so the mouse can freely move around the top left corner
  138. This is why we use the v-bind:style for this -->
  139. <!-- overflow:hidden is to prevent editor from extending past the transparentish background of the app -->
  140.  
  141. <button id="toggle-btn" class="tiger-btn" @click="showApp = !showApp">
  142. 🐅 {{ showApp ? "Close" : "Open" }} 🐅
  143. </button>
  144. <button id="tab-btn" class="tiger-btn" @click="changeTab()" v-show="showApp">
  145. Change Tab
  146. </button>
  147. <br>
  148. <button id="save-btn" class="tiger-btn" v-show="showApp"
  149. @mousedown="indicateClicked('saveCurrentTheme') "
  150. @mouseup="saveCurrentTheme()"
  151. >
  152. Save Current Theme (Hold for 3s)
  153. </button>
  154.  
  155. <div id="editor" class="tab" v-show="showApp && currentTab === 'editor' ">
  156. <div v-for="(_, category) in config"
  157. v-if="category !== 'themeColor' "
  158. > <!-- handle themeColor separately -->
  159. <table>
  160. <thead>
  161. <th colspan="2" >
  162. Edit {{ category }}
  163. </th>
  164. </thead>
  165.  
  166. <!-- in Vue, key,val is reversed to val,key -->
  167. <tr v-for="(val,key) in config[category]"
  168. @change="console.log('Arras().' + category + '.' + key + ' is now ' + config[category][key])"
  169. >
  170. <td>
  171. {{ key }}
  172. </td>
  173.  
  174. <!-- use the type of the value to determine the type of input selector :: typeof won't work because Vue represents numbers as strings -->
  175. <td v-if="getType(val) === 'number' ">
  176. <input type="number" v-model.number="config[category][key]" @keydown.stop>
  177. </td>
  178.  
  179. <td v-else-if="getType(val) === 'boolean' ">
  180. <input type="checkbox" v-model="config[category][key]" >
  181. </td>
  182.  
  183. <td v-else>
  184. <!-- This means that the type of one of the properties was not recognized by this.getType() -->
  185. <strong>:( This is broken. Please ping Road#6943 in Discord to fix this. ):</strong>
  186. </td>
  187. </tr>
  188. </table>
  189. </div>
  190.  
  191. <div> <!-- themeColor is treated differently from other Arras() properties because color pickers need special attention (like the color descriptions, etc...) -->
  192. <table>
  193. <thead>
  194. <th colspan="3" >
  195. Edit themeColor
  196. </th>
  197. </thead>
  198.  
  199. <tr>
  200. <td>
  201. border
  202. </td>
  203. <td colspan="2" >
  204. <input type="number" v-model.number="config.themeColor.border" @keydown.stop
  205. @change="console.log('Arras().themeColor.border is now ' + config.themeColor.border)"
  206. >
  207. </td>
  208. </tr>
  209.  
  210. <tr v-for="[ description, colorName ] in colorDescriptions" >
  211.  
  212. <!-- A "dummy" column, so that the color picker doesn't flow out of the app -->
  213. <!-- Verte-related -->
  214. <td class="dummy-column">
  215. {{ colorName }}
  216. </td>
  217.  
  218. <td @keydown.stop> <!-- putting @keydown.stop in verte won't work, so this is the next best thing -->
  219. <!-- Why won't menuPosition="right" work!!! --> <!-- Verte-related -->
  220. <verte picker="square" model="hex" menuPosition="right"
  221. v-model="config.themeColor.table[ colorNames.indexOf(colorName) ]"
  222. @input=" console.log( colorName + ' is now ' + getHex(colorName) ) "
  223. ></verte>
  224. </td>
  225.  
  226. <td>
  227. {{ description }}
  228. </td>
  229. </tr>
  230. </table>
  231. </div>
  232.  
  233. <div id="special-color-buttons-container">
  234. <button class="tiger-btn" @click="randomizeColors">Randomize Colors</button>
  235. </div>
  236. </div>
  237.  
  238. <div id="extras" class="tab" v-show="showApp && currentTab === 'extras' ">
  239. <button id="feedback-btn" class="tiger-btn"
  240. onclick="window.open('https://forms.gle/M425vYsiBqZzjdRx6','_blank')"
  241. >
  242. Give Feedback! (Opens in New Tab)
  243. </button>
  244.  
  245. <table>
  246. <tr>
  247. <td>
  248. Theme Name:
  249. </td>
  250. <td>
  251. <input type="text" v-model="themeDetails.name"
  252. placeholder="Name" @keydown.stop
  253. >
  254. </td>
  255. </tr>
  256. <tr>
  257. <td>
  258. Theme Author:
  259. </td>
  260. <td>
  261. <input type="text" v-model="themeDetails.author"
  262. placeholder="Author" @keydown.stop
  263. >
  264. </td>
  265. </tr>
  266. <tr>
  267. <td>
  268. <textarea id="import-theme-textarea" v-model="importedTheme"
  269. placeholder="Enter theme" @keydown.stop
  270. ></textarea>
  271. </td>
  272. <td>
  273. <button class="tiger-btn" @click="indicateClicked('importTheme'); importTheme()">
  274. {{
  275. wasButtonClicked.importTheme
  276. ? 'Currently Importing Theme'
  277. : 'Import Theme (All Types Accepted)'
  278. }}
  279. </button>
  280. </td>
  281. </tr>
  282. <tr>
  283. <td>
  284. Best Option. Includes everything. Only works with Tiger (Theme In-Game Editor).
  285. </td>
  286. <td>
  287. <button class="tiger-btn" @click="indicateClicked('exportTiger'); exportTheme('TIGER_JSON')">
  288. {{
  289. wasButtonClicked.exportTiger
  290. ? 'Copied to clipboard!'
  291. : '🐅 Export Tiger Theme 🐅'
  292. }}
  293. </button>
  294. </td>
  295. </tr>
  296. <tr>
  297. <td>
  298. Only includes colors (and the border size). DOES NOT INCLUDE ANY OTHER VALUES SUCH AS FOR GRAPHICAL OR GUI PROPERTIES. However, it can be used without Tiger, by entering it into Arras.io's custom theme input.
  299. </td>
  300. <td>
  301. <button class="tiger-btn" @click="indicateClicked('exportBackwardsCompatible'); exportTheme('backwardsCompatible')">
  302. {{
  303. wasButtonClicked.exportBackwardsCompatible
  304. ? 'Copied to clipboard!'
  305. : 'Export Backwards-Compatible Theme'
  306. }}
  307. </button>
  308. </td>
  309. </tr>
  310. </table>
  311.  
  312. </div>
  313.  
  314. <div id="savedThemes" class="tab" v-show="showApp && currentTab === 'savedThemes' ">
  315. <table>
  316. <tr v-for="(theme,index) in savedThemes">
  317. <td class="theme-details-container" >
  318. {{ theme.themeDetails.name }}
  319. <br><br>
  320. by:
  321. <br>
  322. {{ theme.themeDetails.author }}
  323. <br><br>
  324. <button class="delete-btn tiger-btn"
  325. @mousedown="indicateClicked('deleteTheme')
  326. /* sets the starting timestamp */
  327. "
  328. @mouseup="deleteSavedTheme(index)
  329. /* only deletes if mouseup timestamp - mousedown timestamp >= 3 seconds, to prevent accidental deletion */
  330. "
  331. >
  332. Delete Theme<br>(Hold for 3s)
  333. </button>
  334. </td>
  335. <td class="theme-preview-container">
  336. <svg class="preview"
  337. :style="{
  338. backgroundColor: getHex('white', theme) ,
  339. stroke: getHex('black', theme) ,
  340. strokeWidth: 3 ,
  341. }"
  342. @click="applyTheme(theme)"
  343. >
  344. <rect class="barrelsAndRocks" x="50" y="40" rx="5" ry="5" width="35" height="20" stroke-width="3"
  345. :fill="getHex('grey', theme)" />
  346. <circle class="blueTeam" cx="50" cy="50" r="20" stroke-width="3"
  347. :fill="getHex('blue', theme)" />
  348. <rect class="barrelsAndRocks" x="215" y="40" rx="5" ry="5" width="35" height="20" stroke-width="3"
  349. :fill="getHex('grey', theme)" />
  350. <circle class="greenTeam" cx="250" cy="50" r="20" stroke-width="3"
  351. :fill="getHex('green', theme)" />
  352. <rect class="barrelsAndRocks" x="215" y="140" rx="5" ry="5" width="35" height="20" stroke-width="3"
  353. :fill="getHex('grey', theme)" />
  354. <circle class="magentaTeam" cx="250" cy="150" r="20" stroke-width="3"
  355. :fill="getHex('magenta', theme)" />
  356. <rect class="barrelsAndRocks" x="50" y="140" rx="5" ry="5" width="35" height="20" stroke-width="3"
  357. :fill="getHex('grey', theme)" />
  358. <circle class="redTeam" cx="50" cy="150" r="20" stroke-width="3"
  359. :fill="getHex('red', theme)" />
  360. <polygon class="triangle" points="65.5,100 100,80 100,120"
  361. :fill="getHex('orange', theme)" />
  362. <polygon class="square" points="230.5,85 230.5,115 200.5,115 200.5,85"
  363. :fill="getHex('gold', theme)" />
  364. <polygon class="pentagon" points="138,113 130.6,90.2 150,76.1 169.4,90.2 162,113"
  365. :fill="getHex('purple', theme)" />
  366. <polygon class="rock" class="barrelsAndRocks" points="142.1,53.7 131.15,42.75 131.15,27.25 142.1,16.3 157.6,16.1 168.55,27.25 168.55,42.75 157.6,53.7"
  367. :fill="getHex('grey', theme)" />
  368. <polygon class="crasher" points="150,130 140,147.32 160,147.32"
  369. :fill="getHex('pink', theme)" />
  370. <text x="87.5" y="180" class="gameText"
  371. :fill="getHex('guiwhite', theme)"
  372. >Click To Use</text>
  373. </svg>
  374. </td>
  375. </tr>
  376. </table>
  377. </div>
  378. </div>
  379.  
  380. `
  381. }
  382.  
  383. function getAppCSS() {
  384. return `
  385. /*
  386. This file contains the css (non-userscript specific or library-imported) that makes the editor appear and then look presentable
  387. */
  388.  
  389. /* These make sure the editor doesn't take up the whole screen
  390. ** and instead stays in a nice neat tiny box */
  391. html,body {
  392. height: 100%;
  393. width: 100%;
  394. }
  395.  
  396. /* DO NOT SET HEIGHT/WIDTH/OVERFLOW FOR app IN HERE (CSS FILE) */
  397. /* SET IT USING Vue's conditional :style binding */
  398. /* When the container has a set size, your mouse inputs when over the app,
  399. even if its closed, do not register with the game */
  400. /* You need to have the container have 0% height & width when closed so the mouse can freely move around the top left corner */
  401. #app .tab {
  402. height: 75%; /* To prevent bottom from extending past app */
  403. width: 100%;
  404. overflow: auto;
  405. }
  406.  
  407. /* dummy column adds space for the color picker to fully expand into */
  408. /* Verte-related */
  409. td.dummy-column {
  410. width: 100px;
  411. }
  412.  
  413. /* makes number/text inputs and textareas and editor buttons transparent */
  414. #app input[type="number"]:not(.verte__input),
  415. #app input[type="text"]:not(.verte__input),
  416. #app textarea,
  417. .tiger-btn {
  418. background-color:transparent;
  419. }
  420. /* adds outline to text so its visible against any background color, # of repeated shadows determines strength of outline */
  421. /* from https://stackoverflow.com/a/57465026 */
  422. /* also making all text bold and Ubuntu, so its easier to see */
  423. #app,
  424. #app input[type="number"]:not(.verte__input),
  425. #app input[type="text"]:not(.verte__input),
  426. #app textarea,
  427. .tiger-btn {
  428. text-shadow: 0 0 1px black, 0 0 1px black, 0 0 1px black, 0 0 1px black, 0 0 1px black, 0 0 1px black, 0 0 1px black, 0 0 1px black;
  429. color: white;
  430. font-weight: bold;
  431. /* no need to import Ubuntu font, Arras's default styling will take over in-game and provide it for free */
  432. }
  433.  
  434. /* makes the items more easy to visually separate from each other and the borders of the app */
  435. #app table,
  436. #app th,
  437. #app td {
  438. border: 1px solid white;
  439. border-collapse: collapse;
  440. padding: 10px;
  441. }
  442.  
  443. /* forces the number in number inputs to be close to its label on the left */
  444. #app input[type="number"] {
  445. text-align: left;
  446. }
  447.  
  448. /* force text inside toggle-btn to stay in 1 line */
  449. #toggle-btn {
  450. white-space: nowrap;
  451. }
  452.  
  453. /* make the tab changing and save theme buttons stay away from the open/close button */
  454. #tab-btn, #save-btn {
  455. float: right;
  456. }
  457. /* add some extra space below the #save-btn */
  458. #save-btn {
  459. margin-bottom: 10px;
  460. }
  461.  
  462. /* make extras tab table stay within app */
  463. #extras table {
  464. table-layout: fixed;
  465. }
  466. #extras textarea {
  467. width: 100%;
  468. }
  469.  
  470. /* make the entire svg theme preview show up
  471. ** and give it a "softer" look */
  472. #savedThemes svg {
  473. height: 250%;
  474. border-radius: 5%;
  475. }
  476. /* make the full svg preview show up instead of being cut off at the bottom */
  477. #savedThemes .theme-preview-container {
  478. height: 100px;
  479. /* Align svg to top of cell */
  480. vertical-align: top;
  481. }
  482.  
  483. /* make the svg text more visible and reflective of its in-game look */
  484. #savedThemes svg text {
  485. font-family: Ubuntu, sans-serif;
  486. font-size: 25px;
  487. font-weight: bold;
  488. stroke-width: 1;
  489. letter-spacing: -1.5px;
  490. }
  491.  
  492. /* add some space around the deleteTheme buttons,
  493. ** and make them half-transparent red
  494. */
  495. #savedThemes .delete-btn {
  496. margin: 10px;
  497. background-color: rgba(255, 0, 0, 0.7)
  498. }
  499.  
  500. #savedThemes .theme-details-container {
  501. text-align: center;
  502.  
  503. /* To prevent really long theme names or author names from
  504. fully displaying in one linemaking it impossible to scroll
  505. rightwards to the theme previews
  506. Instead, they are forced to wrap, which generates a really tall cell */
  507. word-break: break-all;
  508. min-width: 150px;
  509. }
  510.  
  511. /* main verte container
  512. -- add some space between its left and the #app container's left */
  513. /* Verte-related */
  514. [class="verte__menu-origin verte__menu-origin--bottom verte__menu-origin--active"] {
  515. margin-left: 25px;
  516. }
  517.  
  518. #special-color-buttons-container {
  519. text-align: center;
  520. margin-top: 10px;
  521. }
  522.  
  523. `
  524. }
  525.  
  526. // paste the vue js <script> js </script> code into herexs
  527. function runAppJS() {
  528.  
  529. /*
  530. This file contains the Vue.js code that runs the editor
  531. */
  532. 'use strict';
  533.  
  534. // initializing PromptBoxes (used for its toasts, which are non-blocking dialogs)
  535. var pb = new PromptBoxes({
  536. attrPrefix: 'pb',
  537. toasts: {
  538. direction: 'top', // Which direction to show the toast 'top' | 'bottom'
  539. max: 2, // The number of toasts that can be in the stack
  540. duration: 1000 * 3, // The time the toast appears (in milliseconds)
  541. showTimerBar: true, // Show timer bar countdown
  542. closeWithEscape: true, // Allow closing with escaping
  543. allowClose: true, // Whether to show a "x" to close the toast
  544. }
  545. });
  546.  
  547.  
  548. var app = new Vue({
  549. el: "#app",
  550.  
  551. components: { Verte }, /* Verte-related */
  552.  
  553. data: {
  554. showApp: true, // applies to the overall app (#app)
  555. currentTab: 'editor', // color pickers tab must be the initial one because otherwise the color pickers break
  556. // other options can be found in the changeTab() function
  557.  
  558. config: {}, // is filled with Arras(), and then stored currentTheme (config from previous session) on Vue instance creation
  559. // because this is linked directly to the game's Arras() obj, we don't need a watcher on config or a renderChange() function
  560. themeDetails: {
  561. name: "", // theme name
  562. author: "",
  563. },
  564. importedTheme: "",
  565.  
  566. // is synced with GM_ storage using a watcher :: each theme's unique key is its index in this array
  567. savedThemes: [],
  568.  
  569. // used to temporarily change button text after being clicked
  570. wasButtonClicked: {
  571. importTheme: false,
  572. exportTiger: false,
  573. exportBackwardsCompatible: false,
  574. },
  575.  
  576. // used to ensure user holds down btn for 3 seconds before the functioanlity actually happens
  577. // to prevent accidental stray clicks
  578. buttonClickStartTime: {
  579. deleteTheme: Infinity,
  580. saveCurrentTheme: Infinity,
  581. },
  582.  
  583. // colorNames is an array of the names of the colors in the array at Arras().themeColor.table, in the same order
  584. colorNames: ["teal","lgreen","orange","yellow","lavender","pink","vlgrey","lgrey","guiwhite","black","blue","green","red","gold","purple","magenta","grey","dgrey","white","guiblack"],
  585. // colorNames and colorDescriptions CANNOT be combined because the order for colorNames is a bad description order (you shouldn't put magenta far apart from blue/green/red, etc...)
  586. colorDescriptions: [
  587. /* "Utility" stuff, not sure how to describe these */
  588. /* Borders -- at top since the border size selector is right above the color selectors */
  589. ["Borders, Text Outlines, Health Bar Background"
  590. , "black"],
  591. ["Map Background"
  592. , "white"],
  593. ["Map Border, Grid"
  594. , "guiblack"],
  595. ["Text Color"
  596. , "guiwhite"],
  597. /* Teams */
  598. ["Your tank in FFA, Left team in 2TDM, Top left team in 4TDM"
  599. , "blue"],
  600. ["Enemy tanks in FFA, Bottom left team in 4TDM"
  601. , "red"],
  602. ["Right team in 2TDM, Top right team in 4TDM, Score Bar"
  603. , "green"],
  604. ["Bottom right team in 4TDM"
  605. , "magenta"],
  606. /* Shapes */
  607. ["Rocks, Barrels, Bar Backgrounds"
  608. , "grey"],
  609. ["Squares, Level Bar"
  610. , "gold"],
  611. ["Triangles"
  612. , "orange"],
  613. ["Pentagons, Pentagon Nest Background"
  614. , "purple"],
  615. ["Crashers"
  616. , "pink"],
  617. ["Eggs, Minimap Background, Invulnerability Flash"
  618. , "vlgrey"],
  619.  
  620. /* Bars under tanks/shapes/bosses/etc... */
  621. ["Main Health Bar, Rare Polygons"
  622. , "lgreen"],
  623. ["Shield/Regen Bar, Rare Polygons"
  624. , "teal"],
  625.  
  626. /* Extras */
  627. ["Arena Closers, Neutral Dominators"
  628. , "yellow"],
  629. ["Rogue Palisades"
  630. , "dgrey"],
  631. ["Unused"
  632. , "lavender"],
  633. ["Unused"
  634. , "lgrey"],
  635. ],
  636. },
  637.  
  638. methods: {
  639. // this is needed because vue converts everything into a string once you change a value
  640. // so the typeof operator won't work
  641. getType(val) {
  642. val = val.toString();
  643. if (val === "true" || val === "false") {
  644. return "boolean";
  645. }
  646. if (!isNaN(val)) {
  647. return "number";
  648. }
  649. if (val[0] === "#") {
  650. return "color";
  651. }
  652. },
  653.  
  654. // get the hexadecimal (#abc123) value for the given colorName in the themeColor table
  655. // second optional argument is a themeObj to use in case you want to get a hex for a different theme
  656. getHex(colorName, themeObj = this) {
  657. return themeObj.config.themeColor.table[ this.colorNames.indexOf(colorName) ];
  658. },
  659.  
  660. // move to next tab in tabs array, and then wrap back around to beginning
  661. changeTab() {
  662. var tabs = ['editor', 'extras', 'savedThemes'];
  663.  
  664. var currentTabIndex = tabs.indexOf( this.currentTab );
  665.  
  666. var newTabIndex = currentTabIndex + 1;
  667. if (newTabIndex === tabs.length) {
  668. newTabIndex = 0;
  669. }
  670.  
  671. this.currentTab = tabs[ newTabIndex ];
  672. },
  673.  
  674. // either changes the button's text for 3 seconds,
  675. // or starts a timer to ensure a button is held for 3 seconds before its functionality is run
  676. indicateClicked(btnName) {
  677. if (btnName === 'deleteTheme' || btnName === 'saveCurrentTheme') {
  678. pb.info('Hold button for 3 seconds to ' + btnName)
  679. // instead of a boolean, we want to use a numerical timestamp to make sure user holds click for 3 sec
  680. // to prevent stray click accidents
  681. this.buttonClickStartTime[btnName] = performance.now();
  682. }
  683. // button text depends on if true or false, this thus changes the button text for 3 seconds
  684. else {
  685. this.wasButtonClicked[btnName] = true;
  686. setTimeout(() => {
  687. this.wasButtonClicked[btnName] = false;
  688. }, 1000 * 3);
  689. }
  690. },
  691.  
  692. // export a theme as either a 'tiger' theme (using edn format) or 'arras' theme (json format, only contains themeColor changes)
  693. exportTheme(type) {
  694. var themeToExport = {};
  695.  
  696. // 'tiger' themes are purposefully incompatible with 'arras' themes because we don't want people who are not familiar with tiger
  697. // to become confused why a theme they got/found from someone else doesn't seem to work properly
  698. // (as the default arras custom theme input would only change colors and border, not any of the other graphical/gui properties)
  699. // tiger themes look like this:
  700. // TIGER_JSON{/* valid JSON */}
  701. // this way it'll be easy in the future if we want to add in extra theme types like TIGER_BASE64/* valid base64 */ or TIGER_XML</* valid XML */>
  702. if (type === 'TIGER_JSON') {
  703. themeToExport = {
  704. themeDetails: this.themeDetails,
  705. config: this.config,
  706. };
  707.  
  708. themeToExport = 'TIGER_JSON' + JSON.stringify(themeToExport);
  709. }
  710. else if (type === 'backwardsCompatible') {
  711. themeToExport = {
  712. name: this.themeDetails.name,
  713. author: this.themeDetails.author,
  714. content: {},
  715. };
  716.  
  717. for (var colorName of this.colorNames) {
  718. themeToExport.content[colorName] = this.getHex(colorName);
  719. }
  720.  
  721. themeToExport = JSON.stringify(themeToExport);
  722. }
  723. else {
  724. console.log('unsupported export theme type');
  725. return;
  726. }
  727.  
  728.  
  729. // copy to clipboard
  730. GM_setClipboard(themeToExport);
  731.  
  732. console.log('Exported the following theme:');
  733. console.log(themeToExport);
  734.  
  735. // use Prompt-Boxes library to notify user
  736. pb.success('Copied to Clipboard!');
  737. },
  738.  
  739. // supports both types of arras themes as well as the new TIGER_JSON theme type
  740. // this function only converts an imported theme string into a js object mirroring this.config/the game's Arras() object
  741. // but importTheme calls a different function (applyTheme) that will take in a theme obj and change the game's visual properties
  742. importTheme() {
  743. var themeToImport = this.importedTheme;
  744. themeToImport = themeToImport.trim();
  745.  
  746. if (themeToImport === '') {
  747. pb.error('Enter theme in box to the left');
  748. return;
  749. }
  750.  
  751. // Tiger themes start with TIGER, and then _<datatype>, e.g. TIGER_JSON{valid json here}
  752. if (themeToImport.startsWith('TIGER')) {
  753. if (themeToImport.startsWith('TIGER_JSON')) {
  754. // remove TIGER_JSON from start of string
  755. themeToImport = themeToImport.substring( 'TIGER_JSON'.length );
  756. themeToImport = JSON.parse(themeToImport);
  757. }
  758. else {
  759. console.log('invalid tiger theme format')
  760. }
  761. }
  762. // standard arras theme, either base64 or normal JSON
  763. // use functions provided by CX to handle these
  764. else {
  765. themeToImport = this.parseArrasTheme(themeToImport);
  766.  
  767. var newTheme = {
  768. themeDetails: {
  769. name: themeToImport.name,
  770. author: themeToImport.author,
  771. },
  772. config: {
  773. themeColor: {
  774. border: themeToImport.content.border,
  775. table: [],
  776. },
  777. },
  778. }
  779.  
  780. // add colors
  781. for (var colorName of this.colorNames) {
  782. var importedColorNameValue = themeToImport.content[colorName];
  783. (newTheme.config.themeColor.table).push( importedColorNameValue );
  784. }
  785.  
  786. // put the correctly formatted js object back into the themeToImport variable
  787. // to match the other branches of the if statement
  788. themeToImport = newTheme;
  789. }
  790.  
  791. /* At this point, themeToImport should contain a js object mirroring the structure of this.config */
  792. console.log('Imported Theme has been parsed as:');
  793. console.log(themeToImport)
  794.  
  795. // clear the import theme textarea
  796. this.importedTheme = '';
  797.  
  798. // use the js object to change the game's colors
  799. this.applyTheme(themeToImport);
  800. },
  801.  
  802. // takes in a themeObj, and changes the games visual properties using it
  803. // be careful not to simply assign this.config to a new object,
  804. // because that will remove it being a reference to the actual game's Arras() object
  805. // similarly, you can only directly change the atomic properties + arrays (not objects)
  806. applyTheme(themeObj) {
  807. // this function will create a new identical theme to remove reference to original theme object
  808. // this prevents future theme modifications from screwing with the original theme
  809. var newTheme = JSON.parse( JSON.stringify(themeObj) );
  810.  
  811. this.themeDetails = newTheme.themeDetails;
  812. for (var category in newTheme.config) {
  813. for (var property in newTheme.config[category]) {
  814. this.config[category][property] = newTheme.config[category][property];
  815. }
  816. }
  817.  
  818. pb.success('Theme Imported!');
  819. },
  820. // saves the current settings in the editor/extras as a new theme in savedThemes
  821. saveCurrentTheme() {
  822. // perforance.now uses milliseconds, so we divide by 1000 to convert to seconds
  823. var timeButtonWasHeldFor = (performance.now() - this.buttonClickStartTime.saveCurrentTheme) / 1000;
  824.  
  825. // button held for less than 3 sec
  826. if (timeButtonWasHeldFor < 3) {
  827. // clear the hold for 3 sec notification
  828. pb.clear();
  829. return;
  830. }
  831. // no author/name entered
  832. if (this.themeDetails.author.trim() === '' || this.themeDetails.name.trim() === '') {
  833. pb.error('Enter theme name and author!');
  834. return;
  835. }
  836. // actual theme saving stuff
  837. var themeToSave = {
  838. themeDetails: this.themeDetails,
  839. config: this.config,
  840. };
  841.  
  842. // create new identical theme to remove reference to the 'this' obj
  843. // so that future changes won't modify the theme being saved
  844. themeToSave = JSON.parse( JSON.stringify(themeToSave) );
  845.  
  846. // add new theme to start of the list
  847. this.savedThemes.unshift(themeToSave);
  848.  
  849. pb.success('Saved ' + themeToSave.themeDetails.name + ' by ' + themeToSave.themeDetails.author);
  850. },
  851. // delete a theme previously saved
  852. // only allow this function to complete if user holds down click for 3 seconds
  853. deleteSavedTheme(indexInSavedThemes) {
  854. // perforance.now uses milliseconds, so we divide by 1000 to convert to seconds
  855. var timeButtonWasHeldFor = (performance.now() - this.buttonClickStartTime.deleteTheme) / 1000;
  856. if (timeButtonWasHeldFor < 3) {
  857. // clear the hold click notification
  858. pb.clear();
  859. return;
  860. }
  861.  
  862. var deletedThemeDetails = this.savedThemes[indexInSavedThemes].themeDetails;
  863. // splice will do the actual deleting
  864. this.savedThemes.splice(indexInSavedThemes, 1);
  865. pb.success('Deleted ' + deletedThemeDetails.name + ' by ' + deletedThemeDetails.author);
  866. },
  867.  
  868.  
  869.  
  870. // Thank you to CX for providing this:
  871. // converts standard arras themes (BOTH base64 and JSON) into a JSON object,
  872. // in the same format as the standard arras JSON themes { name: "", author: "", content: {} }
  873. parseArrasTheme(string){
  874. // Compact Base64 Theme Format
  875. // - stored as a regular base64 string without trailing equal signs
  876. // name + \0 + author + \0 + border byte + (RGB colors)*
  877. try {
  878. let stripped = string.replace(/\s+/g, '')
  879. if (stripped.length % 4 == 2)
  880. stripped += '=='
  881. else if (stripped.length % 4 == 3)
  882. stripped += '='
  883. let data = atob(stripped)
  884. let name = 'Unknown Theme', author = ''
  885. let index = data.indexOf('\x00')
  886. if (index === -1) return null
  887. name = data.slice(0, index) || name
  888. data = data.slice(index + 1)
  889. index = data.indexOf('\x00')
  890. if (index === -1) return null
  891. author = data.slice(0, index) || author
  892. data = data.slice(index + 1)
  893. let border = data.charCodeAt(0) / 0xff
  894. data = data.slice(1)
  895. let paletteSize = Math.floor(data.length / 3)
  896. if (paletteSize < 2) return null
  897. let colorArray = []
  898. for (let i = 0; i < paletteSize; i++) {
  899. let red = data.charCodeAt(i * 3)
  900. let green = data.charCodeAt(i * 3 + 1)
  901. let blue = data.charCodeAt(i * 3 + 2)
  902. let color = (red << 16) | (green << 8) | blue
  903. colorArray.push('#' + color.toString(16).padStart(6, '0'))
  904. }
  905. let content = {
  906. teal: colorArray[0],
  907. lgreen: colorArray[1],
  908. orange: colorArray[2],
  909. yellow: colorArray[3],
  910. lavender: colorArray[4],
  911. pink: colorArray[5],
  912. vlgrey: colorArray[6],
  913. lgrey: colorArray[7],
  914. guiwhite: colorArray[8],
  915. black: colorArray[9],
  916. blue: colorArray[10],
  917. green: colorArray[11],
  918. red: colorArray[12],
  919. gold: colorArray[13],
  920. purple: colorArray[14],
  921. magenta: colorArray[15],
  922. grey: colorArray[16],
  923. dgrey: colorArray[17],
  924. white: colorArray[18],
  925. guiblack: colorArray[19],
  926. paletteSize,
  927. border,
  928. }
  929. return { name, author, content }
  930. } catch (e) {}
  931. try {
  932. let output = JSON.parse(string)
  933. if (typeof output !== 'object')
  934. return null
  935. let { name = 'Unknown Theme', author = '', content } = output
  936. for (let colorHex of [
  937. content.teal,
  938. content.lgreen,
  939. content.orange,
  940. content.yellow,
  941. content.lavender,
  942. content.pink,
  943. content.vlgrey,
  944. content.lgrey,
  945. content.guiwhite,
  946. content.black,
  947. content.blue,
  948. content.green,
  949. content.red,
  950. content.gold,
  951. content.purple,
  952. content.magenta,
  953. content.grey,
  954. content.dgrey,
  955. content.white,
  956. content.guiblack,
  957. ]) {
  958. if (!/^#[0-9a-fA-F]{6}$/.test(colorHex))
  959. return null
  960. }
  961. return {
  962. isJSON: true,
  963. name: (typeof name === 'string' && name) || 'Unknown Theme',
  964. author: (typeof author === 'string' && author) || '',
  965. content,
  966. }
  967. } catch (e) {}
  968. return null
  969. },
  970.  
  971.  
  972. // make the game colors random
  973. randomizeColors() {
  974. function getRandomColor() {
  975. const chars = "0123456789ABCDEF";
  976. let color = "#";
  977. for (let i = 0; i < 6; ++i) {
  978. let charIndex = Math.floor(Math.random() * 16);
  979. color += chars[charIndex];
  980. }
  981. return color;
  982. }
  983.  
  984. const numColors = this.colorNames.length;
  985. const newColors = [];
  986. for (let i = 0; i < numColors; i++) {
  987. newColors.push( getRandomColor() );
  988. }
  989. this.config.themeColor.table = newColors;
  990. console.log(`Game colors changed to [${newColors.join(", ")}]`);
  991. }
  992. },
  993.  
  994. // function run when Vue instance is first mounted onto the DOM
  995. // loads savedThemes from
  996. mounted() {
  997. // used in case it's users first time using tiger, which means they have no saved themes yet
  998. // see default theme authors at this link: https://discord.com/channels/372930441826533386/379175293149118465/398373439133712384
  999. var defaultSavedThemes = [
  1000. {"themeDetails":{"name":"Light Colors","author":"Neph"},"config":{"graphical":{"screenshotMode":false,"borderChunk":6,"barChunk":5,"compensationScale":1.114,"lowGraphics":false,"alphaAnimations":true,"inversedRender":true,"miterStars":true,"miter":false,"fontSizeOffset":0,"shieldbars":false,"renderGrid":true,"darkBorders":false,"neon":false,"coloredNest":false},"gui":{"enabled":true,"alcoveSize":200,"spacing":20},"themeColor":{"table":["#7adbbc","#b9e87e","#e7896d","#fdf380","#b58efd","#ef99c3","#e8ebf7","#aa9f9e","#ffffff","#484848","#3ca4cb","#8abc3f","#e03e41","#efc74b","#8d6adf","#cc669c","#a7a7af","#726f6f","#dbdbdb","#000000"],"border":0.5}}}
  1001. ,
  1002. {"themeDetails":{"name":"Dark Colors","author":"Neph"},"config":{"graphical":{"screenshotMode":false,"borderChunk":6,"barChunk":5,"compensationScale":1.114,"lowGraphics":false,"alphaAnimations":true,"inversedRender":true,"miterStars":true,"miter":false,"fontSizeOffset":0,"shieldbars":false,"renderGrid":true,"darkBorders":false,"neon":false,"coloredNest":false},"gui":{"enabled":true,"alcoveSize":200,"spacing":20},"themeColor":{"table":["#8975b7","#1ba01f","#c46748","#b2b224","#7d56c5","#b24fae","#1e1e1e","#3c3a3a","#000000","#e5e5e5","#379fc6","#30b53b","#ff6c6e","#ffc665","#9673e8","#c8679b","#635f5f","#73747a","#11110f","#ffffff"],"border":0.5}}}
  1003. ,
  1004. {"themeDetails":{"name":"Natural","author":"Neph"},"config":{"graphical":{"screenshotMode":false,"borderChunk":6,"barChunk":5,"compensationScale":1.114,"lowGraphics":false,"alphaAnimations":true,"inversedRender":true,"miterStars":true,"miter":false,"fontSizeOffset":0,"shieldbars":false,"renderGrid":true,"darkBorders":false,"neon":false,"coloredNest":false},"gui":{"enabled":true,"alcoveSize":200,"spacing":20},"themeColor":{"table":["#76c1bb","#aad35d","#e09545","#ffd993","#939fff","#d87fb2","#c4b6b6","#7f7f7f","#ffffff","#373834","#4f93b5","#00b659","#e14f65","#e5bf42","#8053a0","#b67caa","#998f8f","#494954","#a5b2a5","#000000"],"border":0.5}}}
  1005. ,
  1006. {"themeDetails":{"name":"Classic","author":"Neph"},"config":{"graphical":{"screenshotMode":false,"borderChunk":6,"barChunk":5,"compensationScale":1.114,"lowGraphics":false,"alphaAnimations":true,"inversedRender":true,"miterStars":true,"miter":false,"fontSizeOffset":0,"shieldbars":false,"renderGrid":true,"darkBorders":false,"neon":false,"coloredNest":false},"gui":{"enabled":true,"alcoveSize":200,"spacing":20},"themeColor":{"table":["#8efffb","#85e37d","#fc7676","#ffeb8e","#b58eff","#f177dd","#cdcdcd","#999999","#ffffff","#525252","#00b0e1","#00e06c","#f04f54","#ffe46b","#768cfc","#be7ff5","#999999","#545454","#c0c0c0","#000000"],"border":0.5}}}
  1007. ,
  1008. /* https://discord.com/channels/372930441826533386/380128318076223489/398339465166192641 */
  1009. {"themeDetails":{"name":"Forest","author":"Sterlon"},"config":{"graphical":{"screenshotMode":false,"borderChunk":6,"barChunk":5,"compensationScale":1.114,"lowGraphics":false,"alphaAnimations":true,"inversedRender":true,"miterStars":true,"miter":false,"fontSizeOffset":0,"shieldbars":false,"renderGrid":true,"darkBorders":false,"neon":false,"coloredNest":false},"gui":{"enabled":true,"alcoveSize":200,"spacing":20},"themeColor":{"table":["#884aa5","#8c9b3e","#d16a80","#97596d","#499855","#60294f","#ddc6b8","#7e949e","#ffffe8","#665750","#807bb6","#a1be55","#e5b05b","#ff4747","#bac674","#ba78d1","#998866","#529758","#7da060","#000000"],"border":0.5}}}
  1010. ,
  1011. /* https://discord.com/channels/372930441826533386/380128318076223489/398358280784838656 */
  1012. {"themeDetails":{"name":"Midnight","author":"uoiea"},"config":{"graphical":{"screenshotMode":false,"borderChunk":6,"barChunk":5,"compensationScale":1.114,"lowGraphics":false,"alphaAnimations":true,"inversedRender":true,"miterStars":true,"miter":false,"fontSizeOffset":0,"shieldbars":false,"renderGrid":true,"darkBorders":false,"neon":false,"coloredNest":false},"gui":{"enabled":true,"alcoveSize":200,"spacing":20},"themeColor":{"table":["#2b9098","#4baa5d","#345678","#cdc684","#89778e","#a85c90","#cccccc","#a7b2b7","#bac6ff","#091f28","#123455","#098765","#000013","#566381","#743784","#b29098","#555555","#649eb7","#444444","#000000"],"border":0.5}}}
  1013. ,
  1014. /* https://discord.com/channels/372930441826533386/380128318076223489/398368953921175553 */
  1015. {"themeDetails":{"name":"Snow","author":"Deolveopoler"},"config":{"graphical":{"screenshotMode":false,"borderChunk":6,"barChunk":5,"compensationScale":1.114,"lowGraphics":false,"alphaAnimations":true,"inversedRender":true,"miterStars":true,"miter":false,"fontSizeOffset":0,"shieldbars":false,"renderGrid":true,"darkBorders":false,"neon":false,"coloredNest":false},"gui":{"enabled":true,"alcoveSize":200,"spacing":20},"themeColor":{"table":["#89bfba","#b5d17d","#e5e0e0","#b5bbe5","#939fff","#646de5","#b2b2b2","#7f7f7f","#ffffff","#383835","#aeaeff","#aeffae","#ffaeae","#ffffff","#c3c3d8","#ffb5ff","#cccccc","#a0a0b2","#f2f2f2","#000000"],"border":0.5}}}
  1016. ,
  1017. /* https://discord.com/channels/372930441826533386/380128318076223489/398633942661595147 */
  1018. {"themeDetails":{"name":"Coral Reef","author":"Celesteα"},"config":{"graphical":{"screenshotMode":false,"borderChunk":6,"barChunk":5,"compensationScale":1.114,"lowGraphics":false,"alphaAnimations":true,"inversedRender":true,"miterStars":true,"miter":false,"fontSizeOffset":0,"shieldbars":false,"renderGrid":true,"darkBorders":false,"neon":false,"coloredNest":false},"gui":{"enabled":true,"alcoveSize":200,"spacing":20},"themeColor":{"table":["#76eec6","#41aa78","#ff7f50","#ffd250","#dc3388","#fa8072","#8b8886","#bfc1c2","#ffffff","#12466b","#4200ae","#0d6338","#dc4333","#fea904","#7b4bab","#5c246e","#656884","#d4d7d9","#3283bc","#000000"],"border":0.5}}}
  1019. ,
  1020. /* https://discord.com/channels/372930441826533386/380128318076223489/398638773392375819 */
  1021. {"themeDetails":{"name":"Badlands","author":"Incognious"},"config":{"graphical":{"screenshotMode":false,"borderChunk":6,"barChunk":5,"compensationScale":1.114,"lowGraphics":false,"alphaAnimations":true,"inversedRender":true,"miterStars":true,"miter":false,"fontSizeOffset":0,"shieldbars":false,"renderGrid":true,"darkBorders":false,"neon":false,"coloredNest":false},"gui":{"enabled":true,"alcoveSize":200,"spacing":20},"themeColor":{"table":["#f9cb9c","#f1c232","#38761d","#e69138","#b7b7b7","#78866b","#6aa84f","#b7b7b7","#a4c2f4","#000000","#0c5a9e","#6e8922","#5b0000","#783f04","#591c77","#20124d","#2f1c16","#999999","#543517","#ffffff"],"border":0.5}}}
  1022. ,
  1023. /* https://discord.com/channels/372930441826533386/380128318076223489/398631247879995414 */
  1024. {"themeDetails":{"name":"Bleach","author":"definitelynot."},"config":{"graphical":{"screenshotMode":false,"borderChunk":6,"barChunk":5,"compensationScale":1.114,"lowGraphics":false,"alphaAnimations":true,"inversedRender":true,"miterStars":true,"miter":false,"fontSizeOffset":0,"shieldbars":false,"renderGrid":true,"darkBorders":false,"neon":false,"coloredNest":false},"gui":{"enabled":true,"alcoveSize":200,"spacing":20},"themeColor":{"table":["#00ffff","#00ff00","#ff3200","#ffec00","#ff24a7","#ff3cbd","#fff186","#918181","#f1f1f1","#5f5f5f","#0025ff","#00ff00","#ff0000","#fffa23","#3100ff","#d4d3d3","#838383","#4c4c4c","#fffefe","#000000"],"border":0.5}}}
  1025. ,
  1026. /* https://discord.com/channels/372930441826533386/380128318076223489/398626876249210881 */
  1027. {"themeDetails":{"name":"Space","author":"Call"},"config":{"graphical":{"screenshotMode":false,"borderChunk":6,"barChunk":5,"compensationScale":1.114,"lowGraphics":false,"alphaAnimations":true,"inversedRender":true,"miterStars":true,"miter":false,"fontSizeOffset":0,"shieldbars":false,"renderGrid":true,"darkBorders":false,"neon":false,"coloredNest":false},"gui":{"enabled":true,"alcoveSize":200,"spacing":20},"themeColor":{"table":["#4788f3","#af1010","#ff0000","#82f850","#ffffff","#57006c","#ffffff","#272727","#000000","#7f7f7f","#0e1b92","#0aeb80","#c2b90a","#3e7e8c","#285911","#a9707e","#6f6a68","#2d0738","#000000","#ffffff"],"border":0.5}}}
  1028. ,
  1029. /* https://discord.com/channels/372930441826533386/380128318076223489/398635479022567435 */
  1030. {"themeDetails":{"name":"Nebula","author":"Deleted User"},"config":{"graphical":{"screenshotMode":false,"borderChunk":6,"barChunk":5,"compensationScale":1.114,"lowGraphics":false,"alphaAnimations":true,"inversedRender":true,"miterStars":true,"miter":false,"fontSizeOffset":0,"shieldbars":false,"renderGrid":true,"darkBorders":false,"neon":false,"coloredNest":false},"gui":{"enabled":true,"alcoveSize":200,"spacing":20},"themeColor":{"table":["#38b06e","#22882e","#d28e7f","#d5d879","#e084eb","#df3e3e","#f0f2cc","#7d7d7d","#c2c5ef","#161616","#9274e6","#89f470","#e08e5d","#ecdc58","#58cbec","#ea58ec","#7e5713","#303030","#555555","#ffffff"],"border":0.5}}}
  1031. ,
  1032. {"themeDetails":{"name":"Pumpkin Skeleton","author":"Road"},"config":{"graphical":{"screenshotMode":false,"borderChunk":6,"barChunk":5,"compensationScale":1.114,"lowGraphics":false,"alphaAnimations":true,"inversedRender":true,"miterStars":true,"miter":false,"fontSizeOffset":0,"shieldbars":false,"renderGrid":true,"darkBorders":false,"neon":false,"coloredNest":false},"gui":{"enabled":true,"alcoveSize":200,"spacing":20},"themeColor":{"table":["#721970","#ff6347","#1b713a","#fdf380","#941100","#194417","#1b713a","#aa9f9e","#fed8b1","#484848","#3ca4cb","#76eec6","#f04f54","#1b713a","#1b713a","#cc669c","#ffffff","#726f6f","#ff9b58","#000000"],"border":"3.3"}}} /* Its supposed to be in quotes (Pumpkin Skeleton border) */
  1033. ,
  1034. {"themeDetails":{"name":"Solarized Dark","author":"Road"},"config":{"graphical":{"screenshotMode":false,"borderChunk":6,"barChunk":5,"compensationScale":1.114,"lowGraphics":false,"alphaAnimations":true,"inversedRender":true,"miterStars":true,"miter":false,"fontSizeOffset":0,"shieldbars":false,"renderGrid":true,"darkBorders":false,"neon":false,"coloredNest":false},"gui":{"enabled":true,"alcoveSize":200,"spacing":20},"themeColor":{"table":["#b58900","#2aa198","#cb4b16","#657b83","#eee8d5","#d33682","#e0e2e4","#073642","#ffffff","#000000","#268bd2","#869600","#dc322f","#b58900","#678cb1","#a082bd","#839496","#073642","#002b36","#000000"],"border":0.5}}}
  1035. ,
  1036. {"themeDetails":{"name":"Desert","author":"Road"},"config":{"graphical":{"screenshotMode":false,"borderChunk":6,"barChunk":5,"compensationScale":1.114,"lowGraphics":false,"alphaAnimations":true,"inversedRender":true,"miterStars":true,"miter":false,"fontSizeOffset":0,"shieldbars":false,"renderGrid":true,"darkBorders":false,"neon":false,"coloredNest":false},"gui":{"enabled":true,"alcoveSize":200,"spacing":20},"themeColor":{"table":["#783b31","#f5deb3","#e17d70","#dfab79","#b9a9bb","#c1938e","#a88e80","#ccb78e","#ffffff","#555555","#007ba7","#2e8b57","#e44d2e","#ddcf70","#5b968f","#856088","#989b9d","#9e8171","#ceb385","#000000"],"border":0.5}}}
  1037. ,
  1038. {"themeDetails":{"name":"Eggplant","author":"Road"},"config":{"graphical":{"screenshotMode":false,"borderChunk":6,"barChunk":5,"compensationScale":1.114,"lowGraphics":false,"alphaAnimations":true,"inversedRender":true,"miterStars":true,"miter":false,"fontSizeOffset":0,"shieldbars":false,"renderGrid":true,"darkBorders":false,"neon":false,"coloredNest":false},"gui":{"enabled":true,"alcoveSize":200,"spacing":20},"themeColor":{"table":["#e96ba8","#78d4b6","#d6100f","#a39e9b","#e7e9db","#e96ba8","#8d8687","#2b1a29","#ffffff","#2b1a29","#06b6ef","#48b685","#ef6155","#f99b15","#815ba4","#fec418","#b9b6b0","#40113f","#50374d","#000000"],"border":0.5}}}
  1039. ,
  1040. {"themeDetails":{"name":"Gruvbox","author":"Road"},"config":{"graphical":{"screenshotMode":false,"borderChunk":6,"barChunk":5,"compensationScale":1.114,"lowGraphics":false,"alphaAnimations":true,"inversedRender":true,"miterStars":true,"miter":false,"fontSizeOffset":0,"shieldbars":false,"renderGrid":true,"darkBorders":false,"neon":false,"coloredNest":false},"gui":{"enabled":true,"alcoveSize":200,"spacing":20},"themeColor":{"table":["#83a598","#8ec07c","#d65d0e","#d79920","#d3869b","#d3869b","#bdae93","#aa9f9e","#ebddd2","#000000","#458588","#98971a","#cc241d","#d79920","#417b58","#b16186","#928374","#000000","#282828","#000000"],"border":"0.6"}}}
  1041. ,
  1042. {"themeDetails":{"name":"Depths","author":"Skrialik"},"config":{"graphical":{"screenshotMode":false,"borderChunk":6,"barChunk":5,"compensationScale":1.114,"lowGraphics":false,"alphaAnimations":true,"inversedRender":true,"miterStars":true,"miter":false,"fontSizeOffset":0,"shieldbars":false,"renderGrid":true,"darkBorders":false,"neon":false,"coloredNest":false},"gui":{"enabled":true,"alcoveSize":200,"spacing":20},"themeColor":{"table":["#fec700","#b51a00","#ffdbd8","#573400","#b58efd","#cde9b5","#cbf1ff","#aa9f9e","#ffffff","#000000","#002e7a","#375719","#000000","#fff2d5","#f4a4c0","#561029","#c1c1c1","#7a7a7a","#434343","#ffffff"],"border":0.7019607843137254}}}
  1043. ,
  1044.  
  1045. // Theme contest winners below, except Solarized Dark and Eggplant which were already included
  1046.  
  1047. {"themeDetails":{"name":"Retro","author":"Damocles"},"config":{"graphical":{"screenshotMode":false,"borderChunk":6,"compensationScale":1.114,"lowGraphics":false,"alphaAnimations":true,"inversedRender":true,"miterStars":true,"miter":false,"fontSizeOffset":0,"shieldbars":false,"renderGrid":true,"renderNames":true,"censorNames":false,"darkBorders":false,"neon":false,"alternateBorder":false,"coloredNest":false},"gui":{"enabled":true,"scale":1,"alcoveSize":200,"spacing":20,"leaderboard":true,"barChunk":5},"themeColor":{"table":["#ffff62","#06bf3b","#318e95","#937d00","#eee8d5","#e72c76","#c8d8e7","#073642","#ffffff","#000000","#2c3eb9","#efb209","#b91234","#82dfe4","#1e616a","#8b124c","#839496","#76b68b","#081e20","#000000"],"border":0.7529411764705882}}}
  1048. ,
  1049. {"themeDetails":{"name":"Pastel","author":"Damocles"},"config":{"graphical":{"screenshotMode":false,"borderChunk":6,"compensationScale":1.114,"lowGraphics":false,"alphaAnimations":true,"inversedRender":true,"miterStars":true,"miter":false,"fontSizeOffset":0,"shieldbars":false,"renderGrid":true,"renderNames":true,"censorNames":false,"darkBorders":false,"neon":false,"alternateBorder":false,"coloredNest":false},"gui":{"enabled":true,"scale":1,"alcoveSize":200,"spacing":20,"leaderboard":true,"barChunk":5},"themeColor":{"table":["#ffff98","#8affb2","#d8b384","#ffeb76","#eee8d5","#ff8dbd","#f3f0d7","#073642","#ffffb5","#675240","#397697","#3ff7a1","#f65f64","#d2b869","#81654a","#a75299","#c1c1c1","#8da996","#f5dba7","#000000"],"border":0.7529411764705882}}}
  1050. ,
  1051. {"themeDetails":{"name":"Discord","author":"Damocles"},"config":{"graphical":{"screenshotMode":false,"borderChunk":6,"compensationScale":1.114,"lowGraphics":false,"alphaAnimations":true,"inversedRender":true,"miterStars":true,"miter":false,"fontSizeOffset":0,"shieldbars":false,"renderGrid":true,"renderNames":true,"censorNames":false,"darkBorders":false,"neon":false,"alternateBorder":false,"coloredNest":false},"gui":{"enabled":true,"scale":1,"alcoveSize":200,"spacing":20,"leaderboard":true,"barChunk":5},"themeColor":{"table":["#d53f3f","#29b399","#ff2828","#ffeb8e","#b58eff","#ff68ff","#cdcdcd","#999999","#e0e0e0","#000000","#7289da","#43b581","#f04747","#ffe800","#5c79ff","#faa419","#999999","#545454","#1e2124","#36393e"],"border":0.7529411764705882}}}
  1052. ,
  1053. {"themeDetails":{"name":"WR Sheet Theme","author":"alettera"},"config":{"graphical":{"screenshotMode":false,"borderChunk":6,"compensationScale":1.114,"lowGraphics":false,"alphaAnimations":true,"inversedRender":true,"miterStars":true,"miter":false,"fontSizeOffset":0,"shieldbars":false,"renderGrid":true,"renderNames":true,"censorNames":false,"darkBorders":false,"neon":false,"alternateBorder":false,"coloredNest":false},"gui":{"enabled":true,"scale":1,"alcoveSize":200,"spacing":20,"leaderboard":true,"barChunk":5},"themeColor":{"table":["#fd9827","#689168","#bb8e75","#f5deba","#9e8171","#b35757","#eae0c9","#aa9f9e","#ffffff","#795548","#6bb2bf","#68c56c","#b86358","#d8bc67","#6e78aa","#846187","#868788","#726f6f","#cbb690","#000000"],"border":0.2980392156862745}}}
  1054. ,
  1055. {"themeDetails":{"name":"Descent","author":"Rog456"},"config":{"graphical":{"screenshotMode":false,"borderChunk":6,"compensationScale":1.114,"lowGraphics":false,"alphaAnimations":true,"inversedRender":true,"miterStars":true,"miter":false,"fontSizeOffset":0,"shieldbars":false,"renderGrid":true,"renderNames":true,"censorNames":false,"darkBorders":false,"neon":false,"alternateBorder":false,"coloredNest":false},"gui":{"enabled":true,"scale":1,"alcoveSize":200,"spacing":20,"leaderboard":true,"barChunk":5},"themeColor":{"table":["#95cecf","#ffa5ff","#bc8989","#ffffb2","#000000","#bd91c4","#7f7360","#000000","#ffffff","#000000","#ababab","#a69768","#545454","#fde7a5","#878777","#85a686","#c4bb9d","#2e2e32","#3b3b37","#000000"],"border":0}}}
  1056. ,
  1057. ];
  1058.  
  1059. defaultSavedThemes = JSON.stringify(defaultSavedThemes);
  1060.  
  1061. // localStorage returns a string, and I think GM_ storage does too?
  1062. var tigerSavedThemes = GM_getValue('tigerSavedThemes', defaultSavedThemes);
  1063. tigerSavedThemes = JSON.parse(tigerSavedThemes);
  1064.  
  1065. // load the stored themes into our savedThemes variable
  1066. this.savedThemes = tigerSavedThemes;
  1067.  
  1068. console.log('Retrieved the following themes from storage:');
  1069. console.log(tigerSavedThemes);
  1070.  
  1071. /*----------*/
  1072. // tigerCurrentTheme stuff starts here:
  1073.  
  1074. // first load the most up-to-date Arras() into config,
  1075. // in case new features were added recently that aren't in the saved currentTheme
  1076. this.config = Arras();
  1077.  
  1078. // Next, load the 'tigerCurrentTheme' if there is one, else use the current game colors
  1079. // this holds the theme that the user had loaded on their previous game session
  1080. var tigerCurrentTheme = GM_getValue( 'tigerCurrentTheme', JSON.stringify(Arras()) );
  1081. tigerCurrentTheme = JSON.parse(tigerCurrentTheme);
  1082.  
  1083. // do same for themeDetails
  1084. var defaultTigerCurrentThemeDetails = {name: '', author: ''};
  1085. defaultTigerCurrentThemeDetails = JSON.stringify(defaultTigerCurrentThemeDetails);
  1086. var tigerCurrentThemeDetails = GM_getValue( 'tigerCurrentThemeDetails', defaultTigerCurrentThemeDetails);
  1087. tigerCurrentThemeDetails = JSON.parse(tigerCurrentThemeDetails);
  1088.  
  1089. // this is to get the input to applyTheme into the right format so it will work properly
  1090. tigerCurrentTheme = {
  1091. themeDetails: tigerCurrentThemeDetails,
  1092. config: tigerCurrentTheme,
  1093. };
  1094.  
  1095. // finally apply the saved theme if there is one to the actual game colors (and this.config as well)
  1096. this.applyTheme(tigerCurrentTheme);
  1097.  
  1098. console.log('Retrieved and applied this current theme from storage:');
  1099. console.log(tigerCurrentTheme);
  1100. },
  1101.  
  1102. // store stuff to storage when they change
  1103. watch: {
  1104. // re-save this.savedThemes array to storage's tigerSavedThemes key whenever this.savedThemes changes
  1105. savedThemes(newVal) {
  1106. // save new savedThemes array to storage
  1107. GM_setValue( 'tigerSavedThemes', JSON.stringify(newVal) );
  1108.  
  1109. console.log('Saved the following savedThemes to storage:');
  1110. console.log(newVal);
  1111. },
  1112.  
  1113. // re-save the themeDetails whenever they change
  1114. // deep:true is because we want to detect if the object's keys change, not if
  1115. // the object itself is changed to be a reference to a different object
  1116. themeDetails: {
  1117. deep: true,
  1118. handler: function(newVal) {
  1119. // save new themeDetails object to storage
  1120. GM_setValue( 'tigerCurrentThemeDetails', JSON.stringify(newVal) );
  1121.  
  1122. console.log('Saved the following currentThemeDetails to storage:');
  1123. console.log(newVal);
  1124. },
  1125. },
  1126. },
  1127.  
  1128. // since config is so heavily nested, the deep:true stuff isn't working for it
  1129. // (in the userscript and game at least, the website seems to work fine for some reason (maybe because website's config isn't a reference to the actual game config object)??)
  1130. // so instead, we grab and store its value from the Arras() object right before the page unloads (this won't work for the website though because it's Arras() object doesn't change)
  1131. created() {
  1132. // need to use Arras() object because Vue instance won't exist at this point in time
  1133. window.addEventListener("beforeunload", function() {
  1134. GM_setValue('tigerCurrentTheme', JSON.stringify(Arras()) );
  1135.  
  1136. console.log('Saved the following currentTheme to storage:');
  1137. console.log( JSON.stringify(Arras()) );
  1138. });
  1139. },
  1140. });
  1141.  
  1142. }
  1143.  
  1144.