IntCyoaEnhancer

QoL improvements for CYOAs made in IntCyoaCreator

当前为 2022-01-22 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         IntCyoaEnhancer
// @namespace    https://agregen.gitlab.io/
// @version      0.2.5
// @description  QoL improvements for CYOAs made in IntCyoaCreator
// @author       agreg
// @license      MIT
// @match        https://*.neocities.org/*
// @icon         https://intcyoacreator.onrender.com/favicon.ico?
// @run-at       document-start
// @grant        unsafeWindow
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function() {
  'use strict';

  // overriding AJAX sender (before the page starts loading) to detect project.json download done at init time
  let init, enhance, _XHR = unsafeWindow.XMLHttpRequest;
  unsafeWindow.XMLHttpRequest = class XHR extends _XHR {
    constructor () {
      super();
      let _open = this.open;
      this.open = (...args) => {
        if ((`${args[0]}`.toUpperCase() === "GET") && (args[1] === "project.json")) {
          init(() => this.addEventListener('loadend', () => setTimeout(enhance)));
          // displaying loading indicator if not present already (as a mod)
          if (!document.getElementById('indicator')) {
            let _indicator = document.createElement('div'),  NBSP = '\xA0';
            _indicator.style = `position: fixed;  top: 0;  left: 0;  z-index: 1000`;
            document.body.prepend(_indicator);
            this.addEventListener('progress', e => {
              _indicator.innerText = NBSP + "Loading data: " + (!e.total ? `${(e.loaded/1024**2).toFixed(1)} MB` :
                                                                `${(100 * e.loaded / e.total).toFixed(2)}%`);
            });
            this.addEventListener('loadend', () => {_indicator.innerText = ""});
          }
        }
        return _open.apply(this, args);
      };
    }
  };

  init = (thunk=enhance) => {!init.done && (console.log("IntCyoaEnhancer!"),  init.done = true,  thunk())};
  document.addEventListener('readystatechange', () =>
    (document.readyState == 'complete') && ['activated', 'rows', 'pointTypes'].every(k => k in app.__vue__.$store.state.app) && init());

  enhance = () => {
    let _lazy = thunk => {let result, cached = false;  return () => (cached ? result : cached = true, result = thunk())};
    let _try = (thunk, fallback) => {try {return thunk()} catch (e) {console.error(e);  return fallback}};
    let _prompt = (message, value, thunk) => {let s = prompt(message, value);  return (s != null) && thunk(s)};
    let range = n => Array.from({length: n}, (_, i) => i);
    let times = (n, f) => range(n).forEach(f);

    // title & savestate are stored in URL hash
    let _hash = _try(() => `["${decodeURIComponent( location.hash.slice(1) )}"]`);  // it's a JSON array of 2 strings, without '["' & '"]' parts
    let $save = [],   [$title="", $saved=""] = _try(() => JSON.parse(_hash), []);
    let $updateUrl = ({title=$title, save=$save}={}) => {location.hash = JSON.stringify([title, $saved=save.join(",")]).slice(2, -2)};
    // app state accessors
    let $store = () => app.__vue__.$store,   $state = () => $store().state.app;
    let $pointTypes = () => $state().pointTypes,   $rows = () => $state().rows;
    let $items = _lazy(() => [].concat( ...$rows().map(row => row.objects) ));
    let $hiddenActive = _lazy(() => $items().filter(item => item.isSelectableMultiple || item.isImageUpload));
    let $itemsMap = _lazy((m = new Map()) => ($items().forEach(item => m.set(item.id, item)), m)),   $getItem = id => $itemsMap().get(id);
    try {$store()} catch (e) {throw Error("[IntCyoaEnhancer] Can't access app state!", {cause: e})}


    // logic taken from IntCyoaCreator as it appears to be hardwired into a UI component
    let _selectedMulti = (item, num) => {  // selecting a multi-value
      let counter = 0, sign = Math.sign(num);
      let _timesCmp = n => (sign < 0 ? item.numMultipleTimesMinus < n : item.numMultipleTimesPluss > n);
      let _useMulti = () => _timesCmp(counter) && (item.multipleUseVariable = counter += sign,  true);
      let _addPoints = () => $pointTypes().filter(points => points.id == item.multipleScoreId).every(points =>
        _timesCmp(points.startingSum) && (item.multipleUseVariable = points.startingSum += sign,  true));
      times(Math.abs(num), _ => {
        if ((item.isMultipleUseVariable ? _useMulti() : _addPoints()))
          item.scores.forEach(score => $pointTypes().filter(points => points.id == score.id)
                                                    .forEach(points => {points.startingSum -= sign * parseInt(score.value)}));
      });
    };
    let _loadSave = save => {  // applying a savestate
      let _isHidden = s => s.includes("/ON#") || s.includes("/IMG#");
      let tokens = save.split(','),  activated = tokens.filter(s => !_isHidden(s)),  hidden = tokens.filter(_isHidden);
      let _split = (sep, item, token, fn, [id, arg]=token.split(sep, 2)) => {(id == item.id) && fn(arg)};
      $store().commit({type: 'cleanActivated'});  // hopefully not broken…
      $items().forEach(item => {
        if (item.isSelectableMultiple)
          hidden.forEach(token => _split("/ON#", item, token, num => _selectedMulti(item, parseInt(num))));
        else if (item.isImageUpload)
          hidden.forEach(token => _split("/IMG#", item, token, img => {item.image = img.replaceAll("/CHAR#", ",")}));
      });
      //$store().commit({type: 'addNewActivatedArray', newActivated: activated});  // not all versions have this :-(
      let _activated = new Set(activated),  _isActivated = id => _activated.has(id);
      $state().activated = activated;
      $rows().forEach(row => {  // yes, four-level nested loop is how the app does everything
        row.isEditModeOn = false;
        delete row.allowedChoicesChange;  // bugfix: cleanActivated is supposed to do this… but it doesn't
        row.objects.filter(item => activated.includes(item.id)).forEach(item => {
          item.isActive = true;
          row.currentChoices += 1;
          item.scores.forEach(score => $pointTypes().filter(points => points.id == score.id).forEach(points => {
            if ((score.requireds.length <= 0) || $store().getters.checkRequireds(score)) {
              score.isActive = true;
              points.startingSum -= parseInt(score.value);
            }
          }));
        });
      });
    };
    // these are used for generating savestate
    let _isActive = item => item.isActive || (item.isImageUpload && item.image) || (item.isSelectableMultiple && (item.multipleUseVariable !== 0));
    let _activeId = item => (!_isActive(item) ? null : item.id + (item.isImageUpload        ? `/IMG#${item.image.replaceAll(",", "/CHAR#")}` :
                                                                  item.isSelectableMultiple ? `/ON#${item.multipleUseVariable}`              : ""));
    //let _activated = () => $items().map(_activeId).filter(Boolean);  // this is how the app calculates it (selection order seems to be ignored)

    let _hiddenActivated = () => $hiddenActive().filter(_isActive).map(item => item.id);  // images and multi-vals are excluded from state
    $store().watch(state => state.app.activated.filter(Boolean).concat( _hiddenActivated() ),  // activated is formed incorrectly and may contain ""
                   ids => {$save = ids.map($getItem).map(_activeId),  $updateUrl()});  // compared to the app """optimization""" this is blazing fast

    let _bugfix = () => {
      $rows().forEach(row => {delete row.allowedChoicesChange});  // This is a runtime variable, why is it exported?! It breaks reset!
    };

    // init && menu
    _bugfix();
    let _title = document.title;
    $title && (document.title = $title);
    $saved && confirm("Load state from URL?") && _loadSave($saved);
    GM_registerMenuCommand("Change webpage title", () =>
      _prompt("Change webpage title (empty to default)", $title||document.title, s => {document.title = ($title = s) || _title;  $updateUrl()}));
    GM_registerMenuCommand("Edit state", () =>
      _prompt("Edit state (empty to reset)", $saved, _loadSave));
  };
})();