IntCyoaEnhancer

QoL improvements for CYOAs made in IntCyoaCreator

目前为 2022-01-22 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name IntCyoaEnhancer
  3. // @namespace https://agregen.gitlab.io/
  4. // @version 0.3
  5. // @description QoL improvements for CYOAs made in IntCyoaCreator
  6. // @author agreg
  7. // @license MIT
  8. // @match https://*.neocities.org/*
  9. // @icon https://intcyoacreator.onrender.com/favicon.ico?
  10. // @run-at document-start
  11. // @require https://unpkg.com/mreframe/dist/mreframe.js
  12. // @grant unsafeWindow
  13. // @grant GM_registerMenuCommand
  14. // @grant GM_addStyle
  15. // ==/UserScript==
  16.  
  17. (function() {
  18. 'use strict';
  19.  
  20. // overriding AJAX sender (before the page starts loading) to detect project.json download done at init time
  21. let init, enhance, _XHR = unsafeWindow.XMLHttpRequest;
  22. unsafeWindow.XMLHttpRequest = class XHR extends _XHR {
  23. constructor () {
  24. super();
  25. let _open = this.open;
  26. this.open = (...args) => {
  27. if ((`${args[0]}`.toUpperCase() === "GET") && (args[1] === "project.json")) {
  28. init(() => this.addEventListener('loadend', () => setTimeout(enhance)));
  29. // displaying loading indicator if not present already (as a mod)
  30. if (!document.getElementById('indicator')) {
  31. let _indicator = document.createElement('div'), NBSP = '\xA0';
  32. _indicator.style = `position: fixed; top: 0; left: 0; z-index: 1000`;
  33. document.body.prepend(_indicator);
  34. this.addEventListener('progress', e => {
  35. _indicator.innerText = NBSP + "Loading data: " + (!e.total ? `${(e.loaded/1024**2).toFixed(1)} MB` :
  36. `${(100 * e.loaded / e.total).toFixed(2)}%`);
  37. });
  38. this.addEventListener('loadend', () => {_indicator.innerText = ""});
  39. }
  40. }
  41. return _open.apply(this, args);
  42. };
  43. }
  44. };
  45.  
  46. init = (thunk=enhance) => {!init.done && (console.log("IntCyoaEnhancer!"), init.done = true, thunk())};
  47. document.addEventListener('readystatechange', () =>
  48. (document.readyState == 'complete') && ['activated', 'rows', 'pointTypes'].every(k => k in app.__vue__.$store.state.app) && init());
  49.  
  50. enhance = () => {
  51. let _lazy = thunk => {let result, cached = false; return () => (cached ? result : cached = true, result = thunk())};
  52. let _try = (thunk, fallback) => {try {return thunk()} catch (e) {console.error(e); return fallback}};
  53. let _prompt = (message, value, thunk) => {let s = prompt(message, value); return (s != null) && thunk(s)};
  54. let range = n => Array.from({length: n}, (_, i) => i);
  55. let times = (n, f) => range(n).forEach(f);
  56.  
  57. // title & savestate are stored in URL hash
  58. let _hash = _try(() => `["${decodeURIComponent( location.hash.slice(1) )}"]`); // it's a JSON array of 2 strings, without '["' & '"]' parts
  59. let $save = [], [$title="", $saved=""] = _try(() => JSON.parse(_hash), []);
  60. let $updateUrl = ({title=$title, save=$save}={}) => {location.hash = JSON.stringify([title, $saved=save.join(",")]).slice(2, -2)};
  61. // app state accessors
  62. let $store = () => app.__vue__.$store, $state = () => $store().state.app;
  63. let $pointTypes = () => $state().pointTypes, $rows = () => $state().rows;
  64. let $items = _lazy(() => [].concat( ...$rows().map(row => row.objects) ));
  65. let $hiddenActive = _lazy(() => $items().filter(item => item.isSelectableMultiple || item.isImageUpload));
  66. let $itemsMap = _lazy((m = new Map()) => ($items().forEach(item => m.set(item.id, item)), m)), $getItem = id => $itemsMap().get(id);
  67. try {$store()} catch (e) {throw Error("[IntCyoaEnhancer] Can't access app state!", {cause: e})}
  68.  
  69. // logic taken from IntCyoaCreator as it appears to be hardwired into a UI component
  70. let _selectedMulti = (item, num) => { // selecting a multi-value
  71. let counter = 0, sign = Math.sign(num);
  72. let _timesCmp = n => (sign < 0 ? item.numMultipleTimesMinus < n : item.numMultipleTimesPluss > n);
  73. let _useMulti = () => _timesCmp(counter) && (item.multipleUseVariable = counter += sign, true);
  74. let _addPoints = () => $pointTypes().filter(points => points.id == item.multipleScoreId).every(points =>
  75. _timesCmp(points.startingSum) && (item.multipleUseVariable = points.startingSum += sign, true));
  76. times(Math.abs(num), _ => {
  77. if ((item.isMultipleUseVariable ? _useMulti() : _addPoints()))
  78. item.scores.forEach(score => $pointTypes().filter(points => points.id == score.id)
  79. .forEach(points => {points.startingSum -= sign * parseInt(score.value)}));
  80. });
  81. };
  82. let _loadSave = save => { // applying a savestate
  83. let _isHidden = s => s.includes("/ON#") || s.includes("/IMG#");
  84. let tokens = save.split(','), activated = tokens.filter(s => !_isHidden(s)), hidden = tokens.filter(_isHidden);
  85. let _split = (sep, item, token, fn, [id, arg]=token.split(sep, 2)) => {(id == item.id) && fn(arg)};
  86. $store().commit({type: 'cleanActivated'}); // hopefully not broken…
  87. $items().forEach(item => {
  88. if (item.isSelectableMultiple)
  89. hidden.forEach(token => _split("/ON#", item, token, num => _selectedMulti(item, parseInt(num))));
  90. else if (item.isImageUpload)
  91. hidden.forEach(token => _split("/IMG#", item, token, img => {item.image = img.replaceAll("/CHAR#", ",")}));
  92. });
  93. //$store().commit({type: 'addNewActivatedArray', newActivated: activated}); // not all versions have this :-(
  94. let _activated = new Set(activated), _isActivated = id => _activated.has(id);
  95. $state().activated = activated;
  96. $rows().forEach(row => { // yes, four-level nested loop is how the app does everything
  97. row.isEditModeOn = false;
  98. delete row.allowedChoicesChange; // bugfix: cleanActivated is supposed to do this… but it doesn't
  99. row.objects.filter(item => activated.includes(item.id)).forEach(item => {
  100. item.isActive = true;
  101. row.currentChoices += 1;
  102. item.scores.forEach(score => $pointTypes().filter(points => points.id == score.id).forEach(points => {
  103. if ((score.requireds.length <= 0) || $store().getters.checkRequireds(score)) {
  104. score.isActive = true;
  105. points.startingSum -= parseInt(score.value);
  106. }
  107. }));
  108. });
  109. });
  110. };
  111. // these are used for generating savestate
  112. let _isActive = item => item.isActive || (item.isImageUpload && item.image) || (item.isSelectableMultiple && (item.multipleUseVariable !== 0));
  113. let _activeId = item => (!_isActive(item) ? null : item.id + (item.isImageUpload ? `/IMG#${item.image.replaceAll(",", "/CHAR#")}` :
  114. item.isSelectableMultiple ? `/ON#${item.multipleUseVariable}` : ""));
  115. //let _activated = () => $items().map(_activeId).filter(Boolean); // this is how the app calculates it (selection order seems to be ignored)
  116.  
  117. let $hiddenActivated = () => $hiddenActive().filter(_isActive).map(item => item.id); // images and multi-vals are excluded from state
  118. $store().watch(state => state.app.activated.filter(Boolean).concat( $hiddenActivated() ), // activated is formed incorrectly and may contain ""
  119. ids => {$save = ids.map($getItem).map(_activeId), $updateUrl()}); // compared to the app """optimization""" this is blazing fast
  120.  
  121. // debug functions for console
  122. let $activated = () => $state().activated, $clone = x => JSON.parse(JSON.stringify(x));
  123. let $dbg = {$store, $state, $pointTypes, $rows, $items, $getItem, $activated, $hiddenActivated, $clone};
  124. Object.assign(unsafeWindow, {$dbg}, $dbg);
  125.  
  126. let _bugfix = () => {
  127. $rows().forEach(row => {delete row.allowedChoicesChange}); // This is a runtime variable, why is it exported?! It breaks reset!
  128. };
  129.  
  130. // init && menu
  131. _bugfix();
  132. let _title = document.title;
  133. $title && (document.title = $title);
  134. $saved && confirm("Load state from URL?") && _loadSave($saved);
  135. GM_registerMenuCommand("Change webpage title", () =>
  136. _prompt("Change webpage title (empty to default)", $title||document.title, s => {document.title = ($title = s) || _title; $updateUrl()}));
  137. GM_registerMenuCommand("Edit state", () =>
  138. _prompt("Edit state (empty to reset)", $saved, _loadSave));
  139.  
  140. GM_registerMenuCommand("Cheat engine", function $cheat() {
  141. if (!$cheat.toggle) {
  142. const {reFrame: rf, reagent: r, util: {getIn, update, assocIn, merge, entries}} = require('mreframe');
  143. let updateIn = (o, path, f, ...args) => assocIn(o, path, f(getIn(o, path), ...args));
  144. const _ID = 'CHEAT', ID = '#'+_ID, _scroll = (s, bg='lightgrey', thumb='grey', wk='::-webkit-scrollbar') =>
  145. `${s} {scrollbar-width:thin; scrollbar-color:${thumb} ${bg}} ${s}${wk} {width:6px; height:6px; background:${bg}} ${s}${wk}-thumb {background:${thumb}}`;
  146. GM_addStyle(`${ID} {position:fixed; bottom:0; left:0; z-index:1000; color:var(--light); background:var(--gray-dark); opacity:.75} ${ID}:hover {opacity:1}
  147. ${ID} .-frame {max-height:100vh; display:flex; flex-direction:column} ${ID} .-scrollbox {overflow-y:auto} ${_scroll(ID+" .-scrollbox")}
  148. ${ID} h3 {text-align:center} ${ID} table.-points td, ${ID} .-cheats {padding:.5ex} ${ID} .-row {display:flex; flex-direction:row}
  149. ${ID} button {background-color:var(--secondary); border-style:outset; border-radius:1em}
  150. ${ID} td.-minus button, ${ID} tr.-minus :is(.-point-name, .-point-value) {background-color:var(--danger)}
  151. ${ID} td.-plus button, ${ID} tr.-plus :is(.-point-name, .-point-value) {background-color:var(--purple)}
  152. ${ID} button.-cheats {background: var(--cyan)}`);
  153. $cheat.UI = Object.assign(document.createElement('div'), {id: _ID});
  154. document.body.append($cheat.UI);
  155. $cheat.toggle = () => rf.disp(['toggle-ui']);
  156.  
  157. let _points = pointTypes => pointTypes.map(points => [points.id, points.name, points.beforeText, points.startingSum]);
  158. $store().watch(state => _points(state.app.pointTypes), points => rf.disp(['cache-points', points]));
  159.  
  160. rf.regEventDb('init-db', () => ({
  161. show: false,
  162. points: {},
  163. cache: {points: []},
  164. }));
  165. rf.regEventDb('toggle-ui', db => update(db, 'show', x => !x));
  166. rf.regEventFx('point-add!', ({db}, [_, id, n]) => ({db: updateIn(db, ['points', id], x => (x||0)+n),
  167. points: [{id, add: n}]}));
  168. rf.regEventFx('reset-cheats!', ({db}) => ({db: merge(db, {points: {}}),
  169. points: entries(db.points).map(([id, n]) => ({id, add: -n}))}));
  170. rf.regEventDb('cache-points', (db, [_, points]) => assocIn(db, ['cache', 'points'], points));
  171.  
  172. rf.regFx('points', changes => changes.forEach(({id, add}) => {$pointTypes().find(x => x.id == id).startingSum += add}));
  173.  
  174. rf.regSub('show', getIn);
  175. rf.regSub('points', getIn);
  176. rf.regSub('cache', getIn);
  177. rf.regSub('cheating?', db => true);
  178. rf.regSub('points*', ([_, id]) => rf.subscribe(['points', id]), n => n||0);
  179. let _change = n => (!n ? "" : `${n < 0 ? n : '+'+n}`);
  180. rf.regSub('point-show', ([_, id]) => rf.subscribe(['points', id]), _change);
  181. rf.regSub('point-changes', '<-', ['cache', 'points'], '<-', ['points'], ([points, o]) =>
  182. points.filter(([id]) => o[id]).map(([id, name, show]) => [`[${id}] ` + (show||`(${name})`), o[id]]));
  183. rf.regSub('tooltip', '<-', ['point-changes'], changes =>
  184. changes.map(([points, change]) => `${points} ${_change(change)}`).join("\n"));
  185. rf.regSub('cheating?', '<-', ['point-changes'], changes => changes.length > 0);
  186.  
  187. let PointAdd = id => n => ['button', {onclick: () => rf.disp(['point-add!', id, n])}, (n > 0) && '+', n];
  188. let Points = () => ['table.-points', ...rf.dsub(['cache', 'points']).map(([id, name, show, amount]) =>
  189. ['tr', {class: [{1: '-plus', '-1': '-minus'}[Math.sign(rf.dsub(['points*', id]))]],
  190. title: rf.dsub(['point-show', id])},
  191. ['td.-minus', ...[-100, -10, -1].map( PointAdd(id) )],
  192. ['td.-point-name', "[", ['tt', id], "]", ['br'], show||['em', "<untitled>"], ['br'], `(${name})`],
  193. ['td.-point-value', amount],
  194. ['td.-plus', ...[+100, +10, +1].map( PointAdd(id) )]])];
  195. let Frame = (...body) => ['.-frame',
  196. ['h3', {title: rf.dsub(['tooltip'])}, "Points"],
  197. ['.-scrollbox', ...body],
  198. ['div.-row', {title: rf.dsub(['tooltip'])},
  199. ['button', {onclick: $cheat}, (rf.dsub(['cheating?']) ? "< HIDE" : "× CLOSE")],
  200. rf.dsub(['cheating?']) && ['button', {onclick: () => rf.disp(['reset-cheats!'])}, "RESET"]]];
  201. let UI = () => (rf.dsub(['show']) ? [Frame, [Points]] :
  202. rf.dsub(['cheating?']) && ['button.-cheats', {onclick: $cheat, title: rf.dsub(['tooltip'])}, " Cheats: on "]);
  203.  
  204. rf.dispatchSync(['init-db']);
  205. rf.disp(['cache-points', _points( $pointTypes() )]);
  206. r.render([UI], $cheat.UI);
  207. }
  208. $cheat.toggle();
  209. });
  210. };
  211. })();