RPG Maker MZ
Categorize Options v1.4.1

The options menu is pretty barebones, which isn't a bad thing, but it could be a lot better, especially if you plan on adding a lot more extra configuration options.

This plugin actually makes the first step, by categorizing the options menu and add some new features to this menu.

Download (94.68 kB, 8 times downloaded)

Demo (2.84 MB, 2 times downloaded)

Placement

Make sure to place this plugin below the plugins that this plugin requires, but above plugins that rely on this plugin.

Usage

This plugin is a complete overhaul of the Window_Options screen, and might not be compatible with other plugins that alter this object. Nevertheless attempts are made to keep compatibility as much as possible, which is why the original Window_Options object is kept. The same goes for Scene_Options. Instead, two new classes have been created, Window_OptionsExt and Scene_OptionsExt, which are both accessible from CXJ_MZ.CategorizeOptions.

For developers

For plugin developers, there are various hooks.

CXJ_MZ.CategorizeOptions.addOption(name, symbol, options = {})

This is the main portion of the plugin. It allows you to add a new option to the options menu.

By default, there are three types, category, boolean and volume. The category type allows you to create (sub)categories. The symbol defines the category identifier, which you can use with the category property of the options object.

In any other case, the symbol functions as the property selector of the ConfigManager. You can also select properties inside objects you've stored in the ConfigManager, by using a dot, in case you ever need to directly select a certain property.

Note that you are not limited to the the extra options listed, as some types have their own options.

Arguments:

{string|function} name The label. Can be a function that returns a string.
{string} symbol The symbol name.
{object} options

optional Extra options.

{boolean|function} enabled Whether the option is enabled or not.
{*} ext Additional data.
{string} type The option type. By default, category, boolean, volume, reset and cancel are enabled.
{string} category The category it should be added to. Defaults to '' (the root menu).
{number} index Where the option should be inserted.
{number} insertBefore The symbol name of the requested option. If defined, it will override the index.
{number} insertAfter The symbol name of the requested option. If defined, it will override the index.

CXJ_MZ.CatagorizeOptions.addSpacer(category = '', options = {})

A helper function that creates a spacer. Essentially it uses addOption to add an unselectable option, which acts as a spacer.

Arguments:

{string} category

optional The category it should be added to. Defaults to '' (the root menu).

{object} options

optional Extra options.

{string} symbol The symbol name. Only useful if you need to identify the spacer.
{boolean|function} enabled Whether the option is enabled or not.
{*} ext Additional data.
{number} height The height of the spacer. Defaults to the parameter value spacerHeight.
{number} index Where the option should be inserted.
{number} insertBefore The symbol name of the requested option. If defined, it will override the index.
{number} insertAfter The symbol name of the requested option. If defined, it will override the index.

CXJ_MZ.CatagorizeOptions.addFiller(category = '', options = {})

A helper function that creates a filler. It works similarly to addSpacer. When added on an option window screen that displays at full screen, it will try to fill up the remaining space, so that the entire window is essentially covered. This allows you to have options that are always displayed at the bottom. In fact, the default settings screens make use of this already.

Arguments:

{string} category

optional The category it should be added to. Defaults to '' (the root menu).

{object} options

optional Extra options.

{string} symbol The symbol name. Only useful if you need to identify the filler.
{boolean|function} enabled Whether the option is enabled or not.
{*} ext Additional data.
{number} weight When using multiple fillers, this allows you to define how big the filler is compared to others.
{number} index Where the option should be inserted.
{number} insertBefore The symbol name of the requested option. If defined, it will override the index.
{number} insertAfter The symbol name of the requested option. If defined, it will override the index.

CXJ_MZ.CategorizeOptions.modifyOption(symbol, category = '', options = {})

Allows you to modify an existing option.

Arguments:

{string} symbol The symbol name.
{string} category

optional The category it should be added to. Defaults to '' (the root menu).

{object} options

optional Extra options. Note that certain options cannot be set through this.

{boolean|function} enabled Whether the option is enabled or not.
{*} ext Additional data.

CXJ_MZ.CategorizeOptions.removeOption(symbol, category = '')

If you find that you want to remove certain options from your plugin, this will enable you to do so.

Do note that this will remove all instances of the option within the provided category. If no category is provided, it will pick the root menu.

Arguments:

{string} symbol The symbol name.
{string} category

optional The category it should be added to. Defaults to '' (the root menu).


CXJ_MZ.CategorizeOptions.addItemCallbacks(type, callbacks, theme = 'default')

In order to allow developers to easily add new option types, callbacks are implemented. It's also implemented in a way that you can actually override individual callbacks, or add new ones. By default, the following callbacks have been implemented:

  • render(index)
  • renderBackground(index)
  • ok(index)
  • change(index, forward)
  • getSize(index)
  • getSpacing(index)
  • getRect(index, rect)

There are already callbacks created for category, boolean, volume, reset and cancel, however, you can override these callbacks if necessary. In fact, you can replace individual callbacks without having to redefine the ones you want to keep. This is because internally, it deep merges the callback object.

While there technically aren't methods to remove a callback, simply setting a callback to null has the same effect. This also goes for themes, any missing callbacks will automatically inherit from the main.

Arguments:

{string} type The option type to add a callback for.
{object} callbacks The callbacks you want to store.
{string} theme optional The theme.

CXJ_MZ.CategorizeOptions.getItemCallbacks(type, callbackType = null, bindTo = null, ...theme)

This allows you to retrieve all callbacks of a certain type, or a specific callback.

Arguments:

{string} type The option type to retrieve the callbacks for.
{*} callbackType optional The type of callback to be retrieved. Leave null to get every callback.
{object} bindTo optional The object to bind to the callbacks.
{...string} theme optional The theme to use for the callbacks.

Returns:

An object containing all callbacks, the requested callback function if callbackType is not null, or null if the type doesn't have a callback or the callback function does not exist.


CXJ_MZ.CategorizeOptions.markTypeAsIgnore(type, ignore = true)

This allows you to mark an option type as ignore. This means that it will be skipped by the Reset to Default.

Arguments:

{string} type The option type to mark.
{boolean} ignore optional Whether to ignore the type or not.

CXJ_MZ.CategorizeOptions.registerScene(classObject, id, isPrimary = false)

This allows you to register an options scene.

As a game or plugin developer, you might find the need to replace the options scene entirely, in which case this will help you out.

By default, the original Scene_Options and CXJ_MZ.CategorizeOptions.Scene_OptionsExt are registered, as RMMZ.Default and CXJ_MZ.CategorizeOptions.Default respectively.

Arguments:

{object} classObject A class or an object prototype that defines an options scene.
{object} classObject An identifier, which can later be referenced in the optionsScene plugin parameter.
{boolean} isPrimary optional Whether or not to register the current scene as the primary scene. Will replace any previously set primary scene.

CXJ_MZ.CategorizeOptions.setGlobalTheme(theme = null)

This will set the global theme for your option types.

Arguments:

{string} theme optional The name of the theme.

This plugin overwrites default functionality. Make sure you check whether or not the plugin is compatible with other plugins by checking which functions they overwrite. Below is the list of methods it overwrites:

  • Window_Options (full replacement)
  • Scene_Options (full replacement)
  • Scene_Boot.prototype.initialize
  • Scene_Title.prototype.commandOptions
  • Scene_Menu.prototype.commandOptions
  • AudioManager.updateBufferParameters

Download Categorize Options v1.4 (93.33 kB, 5 times downloaded)

Download Demo v1.4 (2.84 MB, 2 times downloaded)

Download Categorize Options v1.3 (91.7 kB, 8 times downloaded)

Download Demo v1.3 (2.84 MB, 3 times downloaded)

Download Categorize Options v1.2 (83.96 kB, 12 times downloaded)

Download Demo v1.2 (2.84 MB, 3 times downloaded)

Download Categorize Options v1.1.1 (52.26 kB, 7 times downloaded)

Download Demo v1.1.1 (2.83 MB, 3 times downloaded)

Download Categorize Options v1.1 (51.08 kB, 138 times downloaded)

Download Categorize Options v1.0.1 (50.45 kB, 137 times downloaded)

Download Categorize Options v1.0 (48.45 kB, 149 times downloaded)

/******************************************************************************
 * CXJ_MZ_CategorizeOptions.js                                                *
 ******************************************************************************
 * By G.A.M. Kertopermono, a.k.a. GaryCXJk                                    *
 ******************************************************************************
 * License: MIT                                                               *
 ******************************************************************************
 * Copyright (c) 2020-2022, G.A.M. Kertopermono                               *
 *                                                                            *
 * Permission is hereby granted, free of charge, to any person obtaining a    *
 * copy of this software and associated documentation files (the "Software"), *
 * to deal in the Software without restriction, including without limitation  *
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,   *
 * and/or sell copies of the Software, and to permit persons to whom the      *
 * Software is furnished to do so, subject to the following conditions:       *
 *                                                                            *
 * The above copyright notice and this permission notice shall be included in *
 * all copies or substantial portions of the Software.                        *
 *                                                                            *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR *
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,   *
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL    *
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER *
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING    *
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER        *
 * DEALINGS IN THE SOFTWARE.                                                  *
 ******************************************************************************/

/*:
 * @target MZ
 * @plugindesc Categorizes the option menu.
 * @author G.A.M. Kertopermono
 *
 * @help
 * ============================================================================
 * = About                                                                    =
 * ============================================================================
 *
 * The options menu is pretty barebones, which isn't a bad thing, but it could
 * be a lot better, especially if you plan on adding a lot more extra
 * configuration options.
 *
 * This plugin actually makes the first step, by categorizing the options menu
 * and add some new features to this menu.
 *
 * ============================================================================
 * = Requirements                                                             =
 * ============================================================================
 *
 * This plugin requires the following plugins to work:
 *
 * * CXJ_MZ.CoreEssentials: ^1.3
 *
 * ============================================================================
 * = Placement                                                                =
 * ============================================================================
 *
 * Make sure to place this plugin below the plugins that this plugin requires,
 * but above plugins that rely on this plugin.
 *
 * ============================================================================
 * = Usage                                                                    =
 * ============================================================================
 *
 * This plugin is a complete overhaul of the Window_Options screen, and might
 * not be compatible with other plugins that alter this object. Nevertheless
 * attempts are made to keep compatibility as much as possible, which is why
 * the original Window_Options object is kept. The same goes for Scene_Options.
 * Instead, two new classes have been created, Window_OptionsExt and
 * Scene_OptionsExt, which are both accessible from CXJ_MZ.CategorizeOptions.
 *
 * --------------
 * For developers
 * --------------
 *
 * For plugin developers, there are various hooks.
 *
 * ---
 *
 * CXJ_MZ.CategorizeOptions.addOption(name, symbol, options = {})
 *
 * This is the main portion of the plugin. It allows you to add a new option
 * to the options menu.
 *
 * By default, there are three types, category, boolean and volume. The
 * category type allows you to create (sub)categories. The symbol defines the
 * category identifier, which you can use with the category property of the
 * options object.
 *
 * In any other case, the symbol functions as the property selector of the
 * ConfigManager. You can also select properties inside objects you've
 * stored in the ConfigManager, by using a dot, in case you ever need to
 * directly select a certain property.
 *
 * Note that you are not limited to the the extra options listed, as some
 * types have their own options.
 *
 * Arguments:
 *
 * {string|function} name         - The label. Can be a function that returns
 *                                  a string.
 * {string} symbol                - The symbol name.
 * {object} options               - Extra options.
 *     {boolean|function} enabled - Whether the option is enabled or not.
 *     {*} ext                    - Additional data.
 *     {string} type              - The option type. By default, category,
 *                                  boolean, volume, reset and cancel are
 *                                  enabled.
 *     {string} category          - The category it should be added to.
 *                                  Defaults to '' (the root menu).
 *     {number} index             - Where the option should be inserted.
 *     {string} insertBefore      - The symbol name of the requested option.
 *                                  If defined, it will override the index.
 *     {string} insertAfter       - The symbol name of the requested option.
 *                                  If defined, it will override the index.
 *
 * ---
 *
 * CXJ_MZ.CatagorizeOptions.addSpacer(category = '', options = {})
 *
 * A helper function that creates a spacer. Essentially it uses addOption
 * to add an unselectable option, which acts as a spacer.
 *
 * Arguments:
 *
 * {string} category              - The category it should be added to.
 *                                  Defaults to '' (the root menu).
 * {object} options               - Extra options.
 *     {string} symbol            - The symbol name. Only useful if you
 *                                  need to identify the spacer.
 *     {boolean|function} enabled - Whether the option is enabled or not.
 *     {*} ext                    - Additional data.
 *     {number} height            - The height of the spacer. Defaults to
 *                                  the parameter value spacerHeight.
 *     {number} index             - Where the option should be inserted.
 *     {string} insertBefore      - The symbol name of the requested option.
 *                                  If defined, it will override the index.
 *     {string} insertAfter       - The symbol name of the requested option.
 *                                  If defined, it will override the index.
 *
 * ---
 *
 * CXJ_MZ.CatagorizeOptions.addSpacer(category = '', options = {})
 *
 * A helper function that creates a filler. It works similarly to addSpacer.
 * When added on an option window screen that displays at full screen, it
 * will try to fill up the remaining space, so that the entire window is
 * essentially covered. This allows you to have options that are always
 * displayed at the bottom. In fact, the default settings screens make use
 * of this already.
 *
 * Arguments:
 *
 * {string} category              - The category it should be added to.
 *                                  Defaults to '' (the root menu).
 * {object} options               - Extra options.
 *     {string} symbol            - The symbol name. Only useful if you
 *                                  need to identify the filler.
 *     {boolean|function} enabled - Whether the option is enabled or not.
 *     {*} ext                    - Additional data.
 *     {number} weight            - When using multiple fillers, this allows
 *                                  you to define how big the filler is
 *                                  compared to others.
 *     {number} index             - Where the option should be inserted.
 *     {string} insertBefore      - The symbol name of the requested option.
 *                                  If defined, it will override the index.
 *     {string} insertAfter       - The symbol name of the requested option.
 *                                  If defined, it will override the index.
 *
 * ---
 *
 * CXJ_MZ.CategorizeOptions.modifyOption(symbol, category = '', options = {})
 *
 * Allows you to modify an existing option.
 *
 * Arguments:
 *
 * {string} symbol                - The symbol name.
 * {string} category              - The category it belongs to. Defaults to
 *                                  '' (the root menu).
 * {object} options               - Extra options. Note that certain options
 *                                  cannot be set through this.
 *     {boolean|function} enabled - Whether the option is enabled or not.
 *     {*} ext                    - Additional data.
 *
 * ---
 *
 * CXJ_MZ.CategorizeOptions.removeOption(symbol, category = '')
 *
 * If you find that you want to remove certain options from your plugin, this
 * will enable you to do so.
 *
 * Do note that this will remove all instances of the option within the
 * provided category. If no category is provided, it will pick the root
 * menu.
 *
 * Arguments:
 *
 * {string} symbol   - The symbol name.
 * {string} category - The category it belongs to. Defaults to '' (the root
 *                     menu).
 *
 * ---
 *
 * CXJ_MZ.CategorizeOptions.addItemCallbacks(type, callbacks, theme = 'default')
 *
 * In order to allow developers to easily add new option types, callbacks
 * are implemented. It's also implemented in a way that you can actually
 * override individual callbacks, or add new ones. By default, the following
 * callbacks have been implemented:
 *
 * * render(index)
 * * renderBackground(index)
 * * ok(index)
 * * change(index, forward)
 * * getSize(index)
 * * getSpacing(index)
 * * getRect(index, rect)
 *
 * There are already callbacks created for category, boolean, volume, reset
 * and cancel, however, you can override these callbacks if necessary. In fact,
 * you can replace individual callbacks without having to redefine the ones you
 * want to keep. This is because internally, it deep merges the callback object.
 *
 * While there technically aren't methods to remove a callback, simply
 * setting a callback to null has the same effect. This also goes for themes,
 * any missing callbacks will automatically inherit from the main.
 *
 * Arguments:
 *
 * {string} type         - The option type to add a callback for.
 * {object} callbacks    - The callbacks you want to store.
 * {string} theme        - The theme.
 *
 * ---
 *
 * CXJ_MZ.CategorizeOptions.getItemCallbacks(type, callbackType = null, bindTo = null, ...theme)
 *
 * This allows you to retrieve all callbacks of a certain type, or a
 * specific callback.
 *
 * Arguments:
 *
 * {string} type     - The option type to retrieve the callbacks for.
 * {*} callbackType  - The type of callback to be retrieved. Leave
 *                     null to get every callback.
 * {object} bindTo   - The object to bind to the callbacks.
 * {...string} theme - The theme to use for the callbacks.
 *
 * Returns:
 *
 * An object containing all callbacks, the requested callback function if
 * callbackType is not null, or null if the type doesn't have a callback
 * or the callback function does not exist.
 *
 * ---
 *
 * CXJ_MZ.CategorizeOptions.markTypeAsIgnore(type, ignore = true)
 *
 * This allows you to mark an option type as ignore. This means that it
 * will be skipped by the Reset to Default.
 *
 * Arguments:
 *
 * {string} type    - The option type to mark.
 * {boolean} ignore - Whether to ignore the type or not.
 *
 * ---
 *
 * CXJ_MZ.CategorizeOptions.registerScene(classObject, id, isPrimary = false)
 *
 * This allows you to register an options scene.
 *
 * As a game or plugin developer, you might find the need to replace the
 * options scene entirely, in which case this will help you out.
 *
 * By default, the original Scene_Options and
 * CXJ_MZ.CategorizeOptions.Scene_OptionsExt are registered, as RMMZ.Default
 * and CXJ_MZ.CategorizeOptions.Default respectively.
 *
 * Arguments:
 *
 * {object} classObject - A class or an object prototype that defines an
 *                        options scene.
 * {string} id          - An identifier, which can later be referenced in the
 *                        optionsScene plugin parameter.
 * {boolean} isPrimary  - Whether or not to register the current scene as the
 *                        primary scene. Will replace any previously set
 *                        primary scene.
 *
 * ---
 *
 * CXJ_MZ.CategorizeOptions.setGlobalTheme(theme = null)
 *
 * This will set the global theme for your option types.
 *
 * Arguments:
 *
 * {string} theme - The name of the theme.
 *
 * ============================================================================
 * = Changelog                                                                =
 * ============================================================================
 *
 * 1.4.1 (2022-07-27)
 * ------------------
 *
 * * Added: Option to use rmmz-numberfont for numeric volume display.
 * * Fixed: Volume slider display default value didn't match the actual value.
 * * Fixed: Volume display and volume alignment weren't grouped properly.
 *
 * 1.4 (2022-07-18)
 * ----------------
 *
 * * Added: cursorUp and cursorDown command callbacks.
 * * Changed: itemRectWithPadding and itemLineRect now allow you to get the
 *   processed rectangle instead of just the raw rectangle.
 *
 * 1.3 (2022-07-14)
 * ----------------
 *
 * * Added: Spacers can now have a symbol.
 * * Added: New option type filler.
 * * Added: Minimum window height can now be set.
 * * Added: You can now add themes to option types.
 * * Changed: afterOk is now enabled for all option types.
 * * Changed: touchUI will automatically update the screen.
 * * Fixed: Parameter booleanMinimizeRect didn't have a default value or
 *   property type.
 *
 * 1.2 (2022-07-12)
 * ----------------
 *
 * * Added: Ability to use your own options scene.
 * * Added: Ability to register scenes and set a scene as primary.
 * * Added: New resetWithParent setting for category type options, which
 *   allows you to disable resetting the settings of a sub-category when the
 *   Reset to Default option is selected on the parent category.
 * * Added: Select options.
 * * Added: Method to set whether or not the back button should be visible.
 * * Added: Option to minimize the selection rectangle for various options.
 * * Added: Numeric display, without the percentage sign.
 * * Added: Volume slider can now also display a numeric or percentage.
 * * Fixed: Private functions weren't instantiated properly.
 * * Fixed: Invalid prop type used for 'text.back'.
 * * Fixed: Rectangle size was calculated before contents sprite was created,
 *   preventing you from calculating text sizes.
 * * Fixed: Scrolling doesn't work properly.
 * * Fixed: Scrolling doesn't render all visible items.
 *
 * 1.1.1 (2020-12-05)
 * ------------------
 *
 * * Fixed: Window width wasn't calculated properly with values lower than 0.
 * * Fixed: Parameter options couldn't go lower than 0.
 * * Fixed: Window visibility failed if there was a scroll.
 *
 * 1.1 (2020-12-01)
 * ----------------
 *
 * * Added CXJ_MZ.CategorizeOptions.markTypeAsIgnore.
 *
 * 1.0.1 (2020-12-01)
 * ------------------
 *
 * * Added insertBefore property for addOption.
 * * Added afterOk for category options.
 * * Added ignore support for reset, preventing options from getting reset.
 *
 * 1.0 (2020-11-30)
 * ----------------
 *
 * * Initial release
 *
 * ============================================================================
 * = Compatibility                                                            =
 * ============================================================================
 *
 * This plugin overwrites default functionality. Make sure you check whether or
 * not the plugin is compatible with other plugins by checking which functions
 * they overwrite. Below is the list of methods it overwrites:
 *
 * * Window_Options (full replacement)
 * * Scene_Options (full replacement)
 * * Scene_Boot.prototype.initialize
 * * Scene_Title.prototype.commandOptions
 * * Scene_Menu.prototype.commandOptions
 * * AudioManager.updateBufferParameters
 *
 * ============================================================================
 * = License                                                                  =
 * ============================================================================
 *
 * Copyright (c) 2020-2022, G.A.M. Kertopermono
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 * ============================================================================
 *
 * @param emptyMenu
 * @text Empty menu
 * @desc Start off with an empty options menu.
 * @type boolean
 * @default false
 * @on Yes
 * @off No
 *
 * @param touchUI
 * @text Touch UI
 * @desc Whether touch UI should be enabled by default or not.
 * @type select
 * @default null
 * @option Default
 * @value null
 * @option Yes
 * @value true
 * @option No
 * @value false
 *
 * @param hideTouchUI
 * @text Hide Touch UI option
 * @desc Whether or not to hide the touch UI option. Only applicable if Empty menu is turned off.
 * @type boolean
 * @default false
 * @on Yes
 * @off No
 *
 * @param windowWidth
 * @text Window width
 * @desc The width of the options window. Set 0 or lower to set the width based on the game screen size.
 * @type number
 * @default 400
 * @min -9999999
 *
 * @param minWindowHeight
 * @text Min. window height
 * @desc The minimum window height. Set 0 or lower to set the height based on the game screen size.
 * @type number
 * @default 1
 * @min -9999999
 *
 * @param maxWindowHeight
 * @text Max. window height
 * @desc The window height the window can grow to. Set 0 or lower to set the height based on the game screen size.
 * @type number
 * @default 0
 * @min -9999999
 *
 * @param spacerHeight
 * @text Spacer Height
 * @type number
 * @default 16
 * @min 0
 *
 * @param resetToDefaultSpacing
 * @text Reset to Default spacing
 * @desc The extra empty space above the Reset to Default button.
 * @type number
 * @default 16
 * @min 0
 *
 * @param booleanWrap
 * @text Wrap boolean options
 * @desc Boolean options always switch regardless of the direction pressed.
 * @type boolean
 * @default false
 * @on Yes
 * @off No
 *
 * @param booleanDisplay
 * @text Boolean display
 * @desc How the boolean options should be displayed.
 * @type select
 * @default toggle
 * @option Toggle
 * @value toggle
 * @option Side-by-side
 * @value sbs
 *
 * @param booleanMinimizeRect
 * @text Minimize select rectangle
 * @desc Whether to minimize the rectangle when this option is selected to just the settings part.
 * @type boolean
 * @default false
 * @on Yes
 * @off No
 *
 * @param selectWrap
 * @text Wrap select options
 * @desc Select options will wrap around.
 * @type boolean
 * @default false
 * @on Yes
 * @off No
 *
 * @param selectDisplay
 * @text Select display
 * @desc How the select options should be displayed.
 * @type select
 * @default toggle
 * @option Toggle
 * @value toggle
 * @option Side-by-side
 * @value sbs
 *
 * @param selectStretch
 * @text Stretch select options
 * @desc When options are displayed on a new line, whether or not to stretch the options.
 * @type select
 * @default true
 * @option Stretch
 * @value true
 * @option Don't stretch
 * @value false
 * @option Stretch (always appear on new line)
 * @value always
 * @option Don't stretch (always appear on new line)
 * @value never
 *
 * @param selectMinimizeRect
 * @text Minimize select rectangle
 * @desc Whether to minimize the rectangle when this option is selected to just the settings part.
 * @type boolean
 * @default false
 * @on Yes
 * @off No
 *
 * @param volumeDisplay
 * @text Volume display
 * @desc How the volume options should be displayed.
 * @type select
 * @default numeric
 * @option Numeric
 * @value numeric
 * @option Numeric (percentage)
 * @value percent
 * @option Slider
 * @value slider
 * @option Slider (below text)
 * @value sliderBelow
 * @option Slider (background)
 * @value sliderBG
 *
 * @param volumeNumberFont
 * @text Volume number font
 * @desc Use the number font for the numeric display?
 * @type boolean
 * @default false
 * @on Yes
 * @off No
 *
 * @param volumeMinimizeRect
 * @text Minimize volume rectangle
 * @desc Whether to minimize the rectangle when this option is selected to just the settings part.
 * @type boolean
 * @default false
 * @on Yes
 * @off No
 *
 * @param volumeStepSize
 * @text Volume step size
 * @type select
 * @default 20
 * @option 1
 * @value 1
 * @option 2
 * @value 2
 * @option 4
 * @value 4
 * @option 5
 * @value 5
 * @option 10
 * @value 10
 * @option 20
 * @value 20
 * @option 25
 * @value 25
 *
 * @param volumeSlider
 * @text Volume slider
 *
 * @param volumeSlider.width
 * @text Slider width
 * @type number
 * @parent volumeSlider
 * @default 120
 * @min -9999999
 *
 * @param volumeSlider.height
 * @text Slider height
 * @type number
 * @parent volumeSlider
 * @default 24
 *
 * @param volumeSlider.volumeDisplay
 * @text Volume display
 * @desc How the volume should be displayed if slider is activated.
 * @parent volumeSlider
 * @type select
 * @default None
 * @option None
 * @value None
 * @option Numeric
 * @value Numeric
 * @option Numeric (percentage)
 * @value Percent
 *
 * @param volumeSlider.volumeAlignment
 * @text Volume alignment
 * @desc How the volume display should be aligned.
 * @parent volumeSlider
 * @type select
 * @default default
 * @option Default
 * @value default
 * @option Before slider
 * @value Before
 * @option After slider
 * @value After
 * @option Left on slider
 * @value Left
 * @option Center on slider
 * @value Center
 * @option Right on slider
 * @value Right
 * @option Inline (only with sliders below text)
 * @value Inline
 *
 * @param volumeSlider.outlineColor
 * @text Outline color
 * @type struct<Color>
 * @parent volumeSlider
 * @default {"red":"0","green":"0","blue":"0","opacity":"1","systemColor":"-1"}
 *
 * @param volumeSlider.backgroundColor
 * @text Background color
 * @type struct<Color>
 * @parent volumeSlider
 * @default {"red":"0","green":"0","blue":"0","opacity":"1","systemColor":"19"}
 *
 * @param volumeSlider.fillColor1
 * @text Fill color 1
 * @type struct<Color>
 * @parent volumeSlider
 * @default {"red":"0","green":"0","blue":"0","opacity":"1","systemColor":"12"}
 *
 * @param volumeSlider.fillColor2
 * @text Fill color 2
 * @type struct<Color>
 * @parent volumeSlider
 * @default {"red":"0","green":"0","blue":"0","opacity":"1","systemColor":"4"}
 *
 * @param text
 * @text Text
 *
 * @param text.categoryGameplay
 * @text Category: Gameplay
 * @type string
 * @default Gameplay
 * @parent text
 *
 * @param text.categoryAudio
 * @text Category: Audio
 * @type string
 * @default Audio
 * @parent text
 *
 * @param text.optionOn
 * @text Option: ON
 * @type string
 * @default ON
 * @parent text
 *
 * @param text.optionOff
 * @text Option: OFF
 * @type string
 * @default OFF
 * @parent text
 *
 * @param text.audioMasterVolume
 * @text Audio: Master Volume
 * @type string
 * @default Master Volume
 * @parent text
 *
 * @param text.resetToDefault
 * @text General: Reset to Default
 * @type string
 * @default Reset to Default
 * @parent text
 *
 * @param text.back
 * @text General: Back
 * @type string
 * @default Back
 * @parent text
 *
 * @param theme
 * @text Theme
 * @desc The overall theme of each option type.
 * @type string
 * @default default
 *
 * @param optionsScene
 * @text Options Scene
 * @desc The class or object name of the options scene. If a scene is registered, you can use that ID.
 * @type string
 *
 */
/*~struct~Color:
 * @param red
 * @text Red
 * @type number
 * @min 0
 * @max 255
 * @decimals 0
 *
 * @param green
 * @text Green
 * @type number
 * @min 0
 * @max 255
 * @decimals 0
 *
 * @param blue
 * @text Blue
 * @type number
 * @min 0
 * @max 255
 * @decimals 0
 *
 * @param opacity
 * @text Opacity
 * @type number
 * @min 0
 * @max 1
 * @decimals 2
 *
 * @param systemColor
 * @text System color
 * @desc If set to a positive integer, use the window system color instead.
 * @type number
 * @default -1
 * @min -1
 * @max 31
 */

(() => {
  window.CXJ_MZ = window.CXJ_MZ || {};
  const {
    CXJ_MZ
  } = window;
  CXJ_MZ.CategorizeOptions = CXJ_MZ.CategorizeOptions || {};
  CXJ_MZ.CategorizeOptions.version = '1.4.1';

  if (!CXJ_MZ.CoreEssentials) {
    throw new Error('CoreEssentials has not been initialized. Make sure you load CoreEssentials before this plugin.');
  }

  if (!CXJ_MZ.CoreEssentials.isVersion('CXJ_MZ.CoreEssentials', '1.3.1')) {
    throw new Error('The correct version of CoreEssentials has not been loaded (required version: 1.3.1).');
  }

  const {
    CoreEssentials,
    CategorizeOptions,
  } = CXJ_MZ;

  const pluginName = 'CXJ_MZ_CategorizeOptions';

  /* ------------------------------------------------------------------------
   * - Default parameters                                                   -
   * ------------------------------------------------------------------------
   */

  // The Color struct is defined here to make it easier to reuse during
  // parameter conversion.
  const colorStruct = {
    red: 'number',
    green: 'number',
    blue: 'number',
    opacity: 'number',
    systemColor: 'number',
  };

  const parameters = CoreEssentials.getParameters(pluginName, {
    emptyMenu: false,
    touchUI: null,
    hideTouchUI: false,
    windowWidth: 400,
    minWindowHeight: 1,
    maxWindowHeight: 0,
    spacerHeight: 16,
    resetToDefaultSpacing: 16,
    booleanWrap: false,
    booleanDisplay: 'toggle',
    booleanMinimizeRect: false,
    selectWrap: false,
    selectDisplay: 'toggle',
    selectStretch: 'true',
    selectMinimizeRect: false,
    volumeDisplay: 'numeric',
    volumeNumberFont: false,
    volumeMinimizeRect: false,
    volumeStepSize: 20,
    'volumeSlider.width': 120,
    'volumeSlider.height': 120,
    'volumeSlider.volumeDisplay': 'None',
    'volumeSlider.volumeAlignment': 'Default',
    'volumeSlider.outlineColor': {
      red: 0,
      green: 0,
      blue: 0,
      opacity: 1,
      systemColor: -1,
    },
    'volumeSlider.backgroundColor': {
      red: 0,
      green: 0,
      blue: 0,
      opacity: 1,
      systemColor: 19,
    },
    'volumeSlider.fillColor1': {
      red: 0,
      green: 0,
      blue: 0,
      opacity: 1,
      systemColor: 12,
    },
    'volumeSlider.fillColor2': {
      red: 0,
      green: 0,
      blue: 0,
      opacity: 1,
      systemColor: 4,
    },
    'text.categoryGameplay': 'Gameplay',
    'text.categoryAudio': 'Audio',
    'text.optionOn': 'ON',
    'text.optionOff': 'OFF',
    'text.audioMasterVolume': 'Master Volume',
    'text.resetToDefault': 'Reset to Default',
    'text.back': 'Back',
    theme: 'default',
    optionsScene: '',
  }, {
    emptyMenu: 'boolean',
    touchUI: 'literal',
    hideTouchUI: 'boolean',
    windowWidth: 'number',
    minWindowHeight: 'number',
    maxWindowHeight: 'number',
    spacerHeight: 'number',
    resetToDefaultSpacing: 'number',
    booleanWrap: 'boolean',
    booleanDisplay: 'text',
    booleanMinimizeRect: 'boolean',
    selectWrap: 'boolean',
    selectDisplay: 'text',
    selectStretch: 'text',
    selectMinimizeRect: 'boolean',
    volumeDisplay: 'text',
    volumeNumberFont: 'boolean',
    volumeMinimizeRect: 'boolean',
    volumeStepSize: 'number',
    'volumeSlider.width': 'number',
    'volumeSlider.height': 'number',
    'volumeSlider.volumeDisplay': 'text',
    'volumeSlider.volumeAlignment': 'text',
    'volumeSlider.outlineColor': ['object', colorStruct],
    'volumeSlider.backgroundColor1': ['object', colorStruct],
    'volumeSlider.backgroundColor2': ['object', colorStruct],
    'volumeSlider.fillColor1': ['object', colorStruct],
    'volumeSlider.fillColor2': ['object', colorStruct],
    'text.categoryGameplay': 'text',
    'text.categoryAudio': 'text',
    'text.optionOn': 'text',
    'text.optionOff': 'text',
    'text.audioMasterVolume': 'text',
    'text.resetToDefault': 'text',
    'text.back': 'text',
    theme: 'text',
    optionsScene: 'text',
  });

  /* ------------------------------------------------------------------------
   * - Private variables                                                    -
   * ------------------------------------------------------------------------
   */

  const categoryOptions = {};
  const typeCallbacks = {};
  const typeIgnore = {};

  // These will only be used if the LocalizationHelper plugin is loaded.
  const paramTextRemap = {
    optionOn: 'options.general.on',
    optionOff: 'options.general.off',
    resetToDefault: 'options.general.resetToDefault',
    categoryGameplay: 'options.category.gameplay',
    categoryAudio: 'options.category.audio',
    audioMasterVolume: 'options.audio.masterVolume',
    back: 'general.back',
  };

  let hasLocalizationHelper = null;

  const registeredScenes = {};

  let primaryScene = null;

  let globalTheme = 'default';

  let id = 1;

  /* --------------------------------------------------------------------------
   * - Private functions                                                      -
   * -                                                                        -
   * - These are helper functions that aren't meant to be used outside the    -
   * - plugin.                                                                -
   * --------------------------------------------------------------------------
   */

  /**
   * Retrieves a string from the TextManager.
   * @param {string} symbol - The symbol of the option.
   * @returns {string} The requested text string.
   */
  const fromTextManager = (symbol) => TextManager[symbol];

  /**
   * Retrieves a localized string from the parameters.
   */
  const getLocalizedText = (key) => {
    if (hasLocalizationHelper === null) {
      hasLocalizationHelper = CXJ_MZ.CoreEssentials.isVersion('CXJ_MZ.LocalizationHelper', '1.0');
    }
    if (hasLocalizationHelper) {
      return CXJ_MZ.LocalizationHelper.line(paramTextRemap[key] || key);
    }
    return '';
  };

  /**
   * Retrieves a string from the parameters.
   * @param {string} key => The string key.
   * @returns {string} The requested text string.
   */
  const getText = (key) => getLocalizedText(key) || parameters[`text.${key}`] || '';

  /**
   * Converts the given color values to a valid color.
   * @param {object} color - A color object, as defined in the parameter struct.
   * @returns {string} A valid color string.
   */
  const colorToRgba = (color) => (
    color.systemColor > -1
    ? ColorManager.textColor(color.systemColor)
    : `rgba(${color.red}, ${color.green}, ${color.blue}, ${color.opacity})`
  );

  const getOptionsScene = () => {
    if (parameters.optionsScene) {
      const { optionsScene } = parameters;
      if (registeredScenes[optionsScene]) {
        return registeredScenes[optionsScene];
      }
      const foundScene = CoreEssentials.findObject(optionsScene);
      if (foundScene) {
        return foundScene;
      }
    }
    return primaryScene;
  }

  const getId = () => {
    const currentId = id;
    id+= 1;
    return currentId;
  };

  /* --------------------------------------------------------------------------
   * - Plugin methods                                                         -
   * --------------------------------------------------------------------------
   */

  /**
   *
   * @param {string|function} name - The label. Can be a function that returns
   * a string.
   * @param {string} symbol - The symbol name.
   * @param {object} options - Extra options.
   */
  CategorizeOptions.addOption = (name, symbol, options = {}) => {
    // Let's destructure the options parameter.
    const {
      enabled = true,
      ext = null,
      type = '',
      category = '',
      index = null,
      insertBefore = null,
      insertAfter = null,
      spacing = 0,
      visible = null,
      ...extraData
    } = options;

    // Now create the data variable.
    const data = {
      ...extraData,
      name,
      symbol,
      enabled,
      ext,
      type,
      spacing,
      visible,
    };

    // Ensure that the category array exists.
    categoryOptions[category] = categoryOptions[category] || [];

    let idx = index;

    if (insertBefore || insertAfter) {
      categoryOptions[category].every((item, oIdx) => {
        if (insertBefore && item.symbol === insertBefore) {
          idx = oIdx;
          return false;
        } else if (insertAfter && item.symbol === insertAfter) {
          idx = oIdx + 1;
          return false;
        }
        return true;
      });
    }

    // Now, depending on what the value of index is, insert the data
    // somewhere in the array.
    if (idx === null || Number.isNaN(+idx) || idx > categoryOptions[category].length) {
      categoryOptions[category].push(data);
    } else if (idx === 0) {
      categoryOptions[category].unshift(data);
    } else {
      categoryOptions[category].splice(idx, 0, data);
    }
  };

  /**
   * Adds a spacer.
   * @param {string} category
   * @param {object} options
   */
  CategorizeOptions.addSpacer = (category = '', options = {}) => {
    const { symbol = `spacer${getId()}` } = options;
    CategorizeOptions.addOption('', symbol, {
      ...options,
      category,
      type: 'spacer',
    });
  }

  /**
   * Adds a filler.
   * @param {string} category
   * @param {object} options
   */
  CategorizeOptions.addFiller = (category = '', options = {}) => {
    const { symbol = `filler${getId()}` } = options;
    CategorizeOptions.addOption('', symbol, {
      ...options,
      category,
      type: 'filler',
    });
  }

  /**
   * Modifies an existing option.
   * @param {string} symbol
   * @param {string} category
   * @param {object} options
   */
  CategorizeOptions.modifyOption = (symbol, category = '', options = {}) => {
    // Let's destructure the options parameter. We want to filter out the data we don't want
    // to merge with the original options.
    const {
      symbol: _symbol = '',
      category: _category = '',
      index: _index = null,
      insertBefore: _insertBefore = null,
      insertAfter: _insertAfter = null,
      ...data
    } = options;

    if (categoryOptions[category]) {
      const option = categoryOptions[category].find((option) => option.symbol === symbol);
      if (option) {
        Object.assign(option, data);
      }
    }
  };

  /**
   * Permanently removes an option from a category.
   *
   * This method will actually remove all instances of the option. Since options should
   * always have a unique symbol, this is an intended feature.
   *
   * @param {string} symbol
   * @param {string} category
   */
  CategorizeOptions.removeOption = (symbol, category = '') => {
    if (categoryOptions[category]) {
      let index;
      while ((index = categoryOptions[category].findIndex((option) => option.symbol === symbol)) > -1) {
        categoryOptions[category].splice(index, 1);
      }
    }
  };

  /**
   * Add callbacks for certain option types.
   * @param {string} type - The option type to add a callback for.
   * @param {object} callbacks - The callbacks you want to store.
   */
  CategorizeOptions.addItemCallbacks = (type, callbacks, theme = 'default') => {
    typeCallbacks[type] = typeCallbacks[type] ?? {};
    typeCallbacks[type][theme] = {
      ...(typeCallbacks[type] || {}),
      ...callbacks,
    };
  }

  /**
   * Retrieves the stored callbacks.
   * @param {string} type - The option type to retrieve the callbacks for.
   * @param {*} callbackType - The type of callback to be retrieved. Leave
   * null to get every callback.
   * @param {object} bindTo - The object to bind to the callbacks.
   * @param {...string} theme - The theme to use for the callbacks.
   * @returns {object|function} An object containing all callbacks, the
   * requested callback function if callbackType is not null, or null if
   * the type doesn't have a callback or the callback function does not
   * exist.
   */
  CategorizeOptions.getItemCallbacks = (type, callbackType = null, bindTo = null, ...theme) => {
    // If the option type does not exist at all, return null.
    const callbackThemes = typeCallbacks[type] ?? null;
    if (!callbackThemes) {
      return null;
    }
    const allThemes = [...theme];
    if (globalTheme !== null) {
      allThemes.push(globalTheme);
    }
    const themeCallbacks = allThemes.reduce((prevTheme, currentTheme) => prevTheme ?? callbackThemes[currentTheme] ?? null, null);
    const defaultThemeCallbacks = callbackThemes.default ?? null;
    if (!themeCallbacks && !defaultThemeCallbacks) {
      return null;
    }

    // We don't want the picked theme to inherit from the global theme set.
    const inheritThemes = [];

    let currentThemeCallbacks = themeCallbacks ?? {};
    while (currentThemeCallbacks.parentTheme) {
      currentThemeCallbacks = callbackThemes[currentThemeCallbacks.parentTheme] ?? {};
      inheritThemes.push(currentThemeCallbacks);
    }

    const callbacks = CoreEssentials.deepMerge({}, themeCallbacks ?? {}, ...inheritThemes, themeCallbacks ?? {});

    // If there's no callback type requested, return the entire object.
    if (!callbackType) {
      if (bindTo) {
        return Object.keys(callbacks).reduce((returnCallbacks, callbackProperty) => {
          returnCallbacks[callbackProperty] = callbacks[callbackProperty].bind(bindTo);
          return returnCallbacks;
        }, {});
      }
      return callbacks;
    }

    // If an array is supplied, select all requested callback types.
    if (Array.isArray(callbackType)) {
      const retCallbacks = {};

      callbackType.forEach((cbType) => {
        if (callbacks[cbType]) {
          retCallbacks[cbType] = bindTo ? callbacks[cbType].bind(bindTo) : callbacks[cbType];
        }
      });

      return retCallbacks;
    }

    const returnCallback = callbacks[callbackType] || null;
    return returnCallback && bindTo ? returnCallback.bind(bindTo) : returnCallback;
  }

  CategorizeOptions.markTypeAsIgnore = (type, ignore = true) => {
    typeIgnore[type] = ignore;
  };

  CategorizeOptions.registerScene = (classObject, id, isPrimary = false) => {
    registeredScenes[id] = classObject;
    if (isPrimary) {
      primaryScene = classObject;
    }
  };

  CategorizeOptions.setGlobalTheme = (theme = null) => {
    globalTheme = theme ?? parameters.theme;
  }

  // To make it easier to create the options, destructure the CategorizeOptions
  // object.
  const {
    addOption,
    addFiller,
    addItemCallbacks,
    getItemCallbacks,
    markTypeAsIgnore,
  } = CategorizeOptions;

  if (!parameters.emptyMenu) {
    // Bind functions for common strings.
    const textResetToDefault = getText.bind(null, 'resetToDefault');
    const textBack = getText.bind(null, 'back');
    const backVisible = function(category) {
      return this.backVisible(category);
    }
    const handleRefresh = function() {
      this.callHandler('refresh');
    }

    // Add root categories.
    addOption(getText.bind(null, 'categoryGameplay'), 'gameplay', { type: 'category' });
    addOption(getText.bind(null, 'categoryAudio'),    'audio',    { type: 'category' });
    addFiller('', { symbol: 'endFiller' });
    addOption(textResetToDefault,                     'reset',    { type: 'reset' });
    addOption(textBack,                               'cancel',   { type: 'cancel', visible: backVisible });

    // Add for category Gameplay.
    addOption(fromTextManager,    'alwaysDash',      { type: 'boolean', category: 'gameplay' });
    addOption(fromTextManager,    'commandRemember', { type: 'boolean', category: 'gameplay' });
    addOption(fromTextManager,    'touchUI',         { type: 'boolean', category: 'gameplay', visible: () => !parameters.hideTouchUI || ConfigManager.touchUI, afterOk: handleRefresh });
    addFiller('gameplay', { symbol: 'endFiller' });
    addOption(textResetToDefault, 'reset',           { type: 'reset', category: 'gameplay' });
    addOption(textBack,           'cancel',          { type: 'cancel', category: 'gameplay', visible: backVisible });

    // Add for category Audio.
    addOption(getText.bind(null, 'audioMasterVolume'), 'masterVolume', { type: 'volume', category: 'audio' });
    addOption(fromTextManager,                         'bgmVolume',    { type: 'volume', category: 'audio' });
    addOption(fromTextManager,                         'bgsVolume',    { type: 'volume', category: 'audio' });
    addOption(fromTextManager,                         'meVolume',     { type: 'volume', category: 'audio' });
    addOption(fromTextManager,                         'seVolume',     { type: 'volume', category: 'audio' });
    addFiller('volume', { symbol: 'endFiller' });
    addOption(textResetToDefault,                      'reset',        { type: 'reset', category: 'audio' });
    addOption(textBack,                                'cancel',       { type: 'cancel', category: 'audio', visible: backVisible });
  }

  /**
   * A simple renderer. Mainly for options that have no value attached.
   * @param {number} index - The option index.
   */
  addItemCallbacks.renderSimple = function(index) {
    // We want to simplify rendering this item.
    const rect = this.itemLineRect(index);
    const {
      textColor = null,
      outlineColor = null,
    } = this.itemData(index, ['textColor', 'outlineColor']);
    this.resetTextColor();
    this.changePaintOpacity(this.isCommandEnabled(index));
    if (textColor) {
      this.changeTextColor(Number.isNaN(+textColor) ? textColor : ColorManager.textColor(textColor));
    }
    if (outlineColor) {
      this.changeOutlineColor(outlineColor === true ? ColorManager.outlineColor() : outlineColor);
    }
    this.drawText(this.commandName(index), rect.x, rect.y, rect.width, 'left', index);
  };

  // Option type: Category
  addItemCallbacks('category', {
    render: addItemCallbacks.renderSimple,
    ok: function(index) {
      const symbol = this.commandSymbol(index);
      this.setCategory(symbol);
      this.playOkSound();
    },
    // We want to disable the changing action, since it's not relevant.
    change: () => {},
  });

  // Option type: Boolean
  addItemCallbacks('boolean', {
    render: function(index) {
      const itemData = this.itemData(index);
      const {
        textColor,
        outlineColor,
      } = itemData;
      const title = this.commandName(index);
      const rect = this.itemLineRect(index);
      const display = itemData.display ?? parameters.booleanDisplay;
      let statusWidth = this.statusWidth();
      const optionOn = (typeof itemData.labelOn === 'function' ? itemData.labelOn.call(this) : itemData.labelOn) ?? getText('optionOn');
      const optionOff = (typeof itemData.labelOff === 'function' ? itemData.labelOn.call(this) : itemData.labelOff) ?? getText('optionOff');
      /*
      If the display type is side-by-side, calculate the width based on
      the string length of both options. We'll add four times the item
      padding, twice for each item, since we want to pad on both sides.
      */
      if (display === 'sbs') {
        statusWidth = 4 * this.itemPadding()
          + this.contents.measureTextWidth(optionOn)
          + this.contents.measureTextWidth(optionOff);
      }
      const titleWidth = rect.width - statusWidth;
      this.resetTextColor();
      this.changePaintOpacity(this.isCommandEnabled(index));
      if (textColor) {
        this.changeTextColor(Number.isNaN(+textColor) ? textColor : ColorManager.textColor(textColor));
      }
      if (outlineColor) {
        this.changeOutlineColor(outlineColor === true ? ColorManager.outlineColor() : outlineColor);
      }
      this.drawText(title, rect.x, rect.y, titleWidth, "left", index);
      this.resetTextColor();
      // Let's draw the strings side by side.
      if (display === 'sbs') {
        // First, get the value. This value gets used to determine which option gets grayed out.
        const symbol = this.commandSymbol(index);
        const value = this.getConfigValue(symbol);

        // Next get the individual width of both strings.
        const offWidth = this.contents.measureTextWidth(optionOff);
        const onWidth = this.contents.measureTextWidth(optionOn);

        // First, draw the Off option.
        this.changePaintOpacity(!value);
        this.drawText(optionOff, rect.x + titleWidth + this.itemPadding() * 2, rect.y, offWidth, "right", index);

        // Next, draw the On option.
        this.changePaintOpacity(value);
        this.drawText(optionOn, rect.x + titleWidth + this.itemPadding() * 4 + offWidth, rect.y, onWidth, "right", index);
      } else {
        // The original method of drawing.
        const status = this.statusText(index);
        this.drawText(status, rect.x + titleWidth, rect.y, statusWidth, "right", index);
      }
    },
    change: function(index, forward) {
      const symbol = this.commandSymbol(index);
      let value = forward;
      // If boolean wrap is enabled, the direction keys act as if you're
      // selecting the option (it doesn't matter which side you press).
      if (this.itemData(index, 'wrap') ?? parameters.booleanWrap) {
        value = !this.getConfigValue(symbol);
      }
      this.changeValue(symbol, value);
    },
    getRect(index, rect) {
      const itemData = this.itemData(index);
      const minimizeRect = itemData.minimizeRect ?? parameters.selectMinimizeRect;
      if (minimizeRect) {
        const optionOn = (typeof itemData.labelOn === 'function' ? itemData.labelOn.call(this) : itemData.labelOn) ?? getText('optionOn');
        const optionOff = (typeof itemData.labelOff === 'function' ? itemData.labelOn.call(this) : itemData.labelOff) ?? getText('optionOff');

        const offWidth = this.contents.measureTextWidth(optionOff);
        const onWidth = this.contents.measureTextWidth(optionOn);

        const totalWidth = (itemData.display === 'sbs' ? offWidth + onWidth + this.itemPadding() * 2 : Math.max(offWidth, onWidth)) + 2 * this.itemPadding();
        rect.x+= rect.width - totalWidth;
        rect.width = totalWidth;
      }
      return rect;
    },
  });

  // Option type: Select
  addItemCallbacks('select', {
    render: function(index) {
      // Get all the callbacks needed for the rendering.
      const {
        getOptions,
        calculateOptionWidth,
        getInitialOptionIndex,
      } = this.commandCallbacks(index, ['getOptions', 'calculateOptionWidth', 'getInitialOptionIndex']);
      const data = this.itemData(index);
      const title = this.commandName(index);
      const rect = this.itemLineRect(index);
      const display = data.display ?? parameters.selectDisplay;
      const stretch = data.stretch ?? ['true', 'always'].includes(parameters.selectStretch);
      const options = getOptions(index);
      const totalWidth = calculateOptionWidth(index, options);
      const titleWidth = this.contents.measureTextWidth(title);
      // Calculate whether the options should appear on a new line or not.
      const newLine = (
        (data.newLine ?? ['always', 'never'].includes(parameters.selectStretch))
        || (totalWidth + titleWidth) > rect.width
      );
      let selectState = this.getOptionState(index) ?? null;
      if (!selectState) {
        selectState = {
          optionIndex: getInitialOptionIndex(index),
          forward: true,
          x: 0,
        };
        this.setOptionState(index, selectState);
      }
      this.resetTextColor();
      this.changePaintOpacity(this.isCommandEnabled(index));
      this.drawText(title, rect.x, rect.y, titleWidth, "left", newLine ? null : index);
      const start = {
        x: rect.x + (newLine ? 0 : rect.width),
        y: newLine ? rect.y + this.lineHeight() : rect.y,
      };
      if (display === 'sbs') {
        if (newLine) {
          const actualTotalWidth = totalWidth - this.itemPadding();
          let spacing = this.itemPadding() * 2;
          if (stretch && actualTotalWidth <= rect.width) {
            const trimmedWidth = actualTotalWidth - (2 * (options.length - 1) * this.itemPadding());
            spacing = (rect.width - trimmedWidth) / (options.length - 1);
          }
          if (actualTotalWidth > rect.width) {
            const optionBound = options.slice(0, selectState.optionIndex + 1).reduce((prevState, option, index) => {
              prevState.left = prevState.right + (index ? spacing : 0);
              prevState.right = prevState.left + option.width;
              return prevState;
            }, {left: 0, right: 0});
            const viewBound = {left: selectState.x, right: selectState.x + rect.width};

            if (viewBound.right < optionBound.right) {
              selectState.x = optionBound.right - rect.width;
              viewBound.left+= optionBound.right - viewBound.right;
              viewBound.right = optionBound.right;
            }
            if (viewBound.left > optionBound.left) {
              selectState.x = optionBound.left;
            }
            start.x-= selectState.x;
          }
          const context = this.contents.context;
          context.save();
          context.beginPath();
          context.rect(rect.x, start.y, rect.width, this.lineHeight());
          context.clip();
          options.forEach((option, index) => {
            const isSelected = index === selectState.optionIndex;
            if (start.x + rect.width >= 0 && start.x <= rect.width) {
              this.changePaintOpacity(isSelected);
              this.drawText(option.name, Math.round(start.x), start.y, option.width, "left", null);
            }
            start.x+= option.width + spacing;
          });
          context.restore();
        } else {
          start.x-= totalWidth;

          options.forEach((option, index) => {
            const isSelected = index === selectState.optionIndex;
            this.changePaintOpacity(isSelected);
            this.drawText(option.name, start.x + this.itemPadding(), start.y, option.width, "left", null);
            start.x+= option.width + (2 * this.itemPadding());
          });
        }
      } else {
        const option = options[selectState.optionIndex];
        if (!newLine) {
          start.x-= option.width;
        }
        this.drawText(option.name, start.x, start.y, Math.min(option.width, rect.width), "right", newLine ? null : index);
      }
    },
    change: function(index, forward, wrap = null) {
      const symbol = this.commandSymbol(index);
      const options = this.commandCallbacks(index, 'getOptions')(index);
      const selectState = this.getOptionState(index) ?? {
        optionIndex: this.commandCallbacks(index, 'getInitialOptionIndex')(index),
        x: 0,
      };
      const { optionIndex } = selectState;
      const doWrap = wrap ?? this.itemData(index, 'wrap') ?? parameters.selectWrap;
      const indexMod = (forward ? 1 : -1);
      const newIndex = (
        doWrap
        ? (options.length + optionIndex + indexMod) % options.length
        : Math.min(options.length - 1, Math.max(0, optionIndex + indexMod))
      );
      this.setOptionState(index, {
        ...selectState,
        optionIndex: newIndex,
        forward,
      });
      this.changeValue(symbol, options[newIndex].value);
    },
    ok: function(index) {
      this.commandCallbacks(index, 'change')(index, true, true);
    },
    getSize(index) {
      const calculateOptionWidth = this.commandCallbacks(index, 'calculateOptionWidth');
      const title = this.commandName(index);
      const rectWidth = this.itemWidth() - this.colSpacing();
      const totalWidth = calculateOptionWidth(index);
      const titleWidth = this.contents.measureTextWidth(title);
      if (
        (this.itemData(index, 'newLine') ?? ['always', 'never'].includes(parameters.selectStretch))
        || (totalWidth + titleWidth) > rectWidth
      ) {
        return this.lineHeight() * 2;
      }
      return this.lineHeight();
    },
    getRect(index, rect) {
      const itemData = this.itemData(index);
      const minimizeRect = itemData.minimizeRect ?? parameters.selectMinimizeRect;
      if (minimizeRect) {
        const calculateOptionWidth = this.commandCallbacks(index, 'calculateOptionWidth');
        const title = this.commandName(index);
        const rectWidth = this.itemWidth() - this.colSpacing();
        const totalWidth = calculateOptionWidth(index);
        const titleWidth = this.contents.measureTextWidth(title);
        if (
          (this.itemData(index, 'newLine') ?? ['always', 'never'].includes(parameters.selectStretch))
          || (totalWidth + titleWidth) > rectWidth
        ) {
          rect.y+= this.lineHeight();
          rect.height/= 2;
        } else {
          rect.x+= rect.width - (totalWidth + this.itemPadding());
          rect.width = totalWidth + this.itemPadding();
        }
      }
      return rect;
    },
    getOptions: function(index) {
      const options = this.itemData(index, 'options') ?? [];
      return options.map(({ name = '', symbol = '', value = null, ...option }) => {
        const returnOption = {
          ...option,
          symbol,
          value: value ?? symbol,
          name: typeof label === 'function' ? name(symbol) : name,
        };
        returnOption.width = this.contents.measureTextWidth(returnOption.name);
        return returnOption;
      });
    },
    calculateOptionWidth: function(index, options = null) {
      const display = this.itemData(index, 'display') ?? parameters.selectDisplay;
      const realOptions = (
        options
        ? options
        : this.commandCallbacks(index, 'getOptions')(index)
      );
      return realOptions.reduce((previousTotal, option, index) => {
        if (display === 'sbs') {
          return previousTotal + option.width + (index ? 2 : 1) * this.itemPadding();
        }
        return Math.max(previousTotal, option.width + this.itemPadding());
      }, 0);
    },
    /**
     * Gets the initial option index.
     *
     * This will search the initial index for the selected option, based on the value stored
     * in the configuration.
     *
     * @param {number} index
     * @returns {number}
     */
    getInitialOptionIndex: function(index) {
      const symbol = this.commandSymbol(index);
      const value = this.getConfigValue(symbol) ?? null;
      const options = this.commandCallbacks(index, 'getOptions')(index);
      return (
        value === 0
        ? 0
        : Math.max(0, options.findIndex((potentialOption) => potentialOption.value === value))
      );
    }
  });

  addItemCallbacks('volume', {
    render: function(index) {
      const title = this.commandName(index);
      const rect = this.itemLineRect(index);
      const symbol = this.commandSymbol(index);
      const value = this.getConfigValue(symbol);
      const {
        display,
        sliderType,
        textDisplay,
        textAlignment,
        valueWidth,
        statusWidth,
        titleWidth,
        sliderWidth,
        sliderHeight,
      } = this.commandCallbacks(index, 'getDisplayProperties')(index);
      const currentFontFace = this.contents.fontFace;

      this.resetTextColor();
      this.changePaintOpacity(this.isCommandEnabled(index));
      this.drawText(title, rect.x, rect.y, titleWidth, "left", sliderType === 'sliderBelow' ? null : index);
      if (display !== 'sliderBG') {
        const status = textDisplay === 'percent' ? this.statusText(index) : value;
        if (display.startsWith('slider') && !display.startsWith('sliderBG')) {
          // Get the colors.
          const sliderOutline = colorToRgba(parameters['volumeSlider.outlineColor']);
          const sliderBg = colorToRgba(parameters['volumeSlider.backgroundColor']);
          const sliderFill1 = colorToRgba(parameters['volumeSlider.fillColor1']);
          const sliderFill2 = colorToRgba(parameters['volumeSlider.fillColor2']);

          // Now calculate how full the bar should be.
          const volumeFill = Math.round((value / 100) * sliderWidth);

          // Now calculate the exact coordinates where the top left of the bar should be.
          let sliderX = rect.x + titleWidth + 2 * this.itemPadding();
          let sliderY = rect.y + (rect.height - sliderHeight) / 2;

          if (display.startsWith('sliderBelow')) {
            sliderX = rect.x + rect.width - sliderWidth;
            sliderY = rect.y + this.itemHeight();
          }
          if (textAlignment === 'after') {
            sliderX-= valueWidth + 2 * this.itemPadding();
          }

          // If the value is higher than 0, draw the filled bar.
          if (value > 0) {
            this.contents.gradientFillRect(sliderX, sliderY, volumeFill, sliderHeight, sliderFill1, sliderFill2);
          }

          // If the value is lower than 100, draw the remainder.
          if (value < 100) {
            this.contents.fillRect(sliderX + volumeFill, sliderY, sliderWidth - volumeFill, sliderHeight, sliderBg);
          }

          // Finally, draw the outline.
          this.contents.strokeRect(sliderX, sliderY, sliderWidth, sliderHeight, sliderOutline);

          if (textDisplay) {
            this.resetTextColor();
            this.changePaintOpacity(this.isCommandEnabled(index));
            let textX = rect.x + rect.width - valueWidth;
            let textY = display.startsWith('sliderBelow') ? rect.y + (rect.height) / 2 : rect.y;
            let drawIndex = display.startsWith('sliderBelow') ? null : index;
            switch (textAlignment) {
              case 'before':
                textX = sliderX - valueWidth - 2 * this.itemPadding();
                break;
              case 'inline':
                textY = rect.y;
                break;
              case 'left':
                textX = sliderX + this.itemPadding();
                if (textX + valueWidth > sliderX - this.itemPadding()) {
                  textX = sliderX + (sliderWidth - valueWidth) / 2;
                }
                break;
              case 'right':
                textX-= this.itemPadding();
                if (textX < sliderX + this.itemPadding()) {
                  textX = sliderX + (sliderWidth - valueWidth) / 2;
                }
                break;
              case 'center':
                textX = sliderX + (sliderWidth - valueWidth) / 2;
                break;
              case 'after':
              default:
                break;
            }
            if (parameters.volumeNumberFont) {
              this.contents.fontFace = 'rmmz-numberfont';
            }
            this.drawText(status, textX, textY, valueWidth, 'right', drawIndex);
            if (parameters.volumeNumberFont) {
              this.contents.fontFace = currentFontFace;
            }
          }
        } else {
          const currentFontFace = this.contents.fontFace;
          if (parameters.volumeNumberFont) {
            this.contents.fontFace = 'rmmz-numberfont';
          }
          this.drawText(status, rect.x + titleWidth, rect.y, statusWidth, "right", index);
          if (parameters.volumeNumberFont) {
            this.contents.fontFace = currentFontFace;
          }
        }
      }
    },
    renderBackground(index) {
      const display = this.itemData(index, 'display') ?? parameters.volumeDisplay;
      if (!display.startsWith('sliderBG')) {
        this.drawItemBackground(index, false);
      } else {
        const symbol = this.commandSymbol(index);
        const value = this.getConfigValue(symbol);
        const rect = this.itemRect(index);
        const volumeFill = Math.round((value / 100) * rect.width);
        const c1 = ColorManager.itemBackColor1();
        const c2 = ColorManager.itemBackColor2();
        const sliderFill1 = colorToRgba(parameters['volumeSlider.fillColor1']);
        const sliderFill2 = colorToRgba(parameters['volumeSlider.fillColor2']);
        const {
          x,
          y,
          width: w,
          height: h,
        } = rect;
        if (value > 0) {
          this.contentsBack.gradientFillRect(x, y, volumeFill, h, sliderFill1, sliderFill2, true);
        }
        if (value < 100) {
          this.contentsBack.gradientFillRect(x + volumeFill, y, w - volumeFill, h, c1, c2, true);
        }
        this.contentsBack.strokeRect(x, y, w, h, c1);
      }
    },
    getSize(index) {
      const {
        sliderHeight,
        sliderType,
      } = this.commandCallbacks(index, 'getBaseDisplayProperties')(index);
      if (sliderType === 'sliderBelow') {
        const textHeight = this.itemHeight();
        return textHeight + sliderHeight + 8;
      }
      return this.lineHeight();
    },
    getRect(index, rect) {
      const {
        sliderWidth,
        sliderType,
        textAlignment,
        minimizeRect,
        valueWidth,
      } = this.commandCallbacks(index, 'getDisplayProperties')(index);
      if (minimizeRect) {
        const minX = rect.x;
        const maxWidth = rect.width;
        const statusWidth = 2 * this.itemPadding() + sliderWidth;
        const titleWidth = rect.width - statusWidth;
        rect.x+= titleWidth;
        rect.width-= titleWidth;
        switch (sliderType) {
          case 'slider':
            if (textAlignment === 'after') {
              rect.x-= valueWidth + 2 * this.itemPadding();
            }
            break;
          case 'sliderBelow': {
            const textHeight = this.itemHeight() - 8;
            rect.y+= textHeight;
            rect.height-= textHeight;
            if (textAlignment === 'after') {
              rect.x = minX;
            }
            break;
          }
          default:
            break;
        }
        rect.x = Math.max(rect.x, minX);
        rect.width = Math.min(rect.width, maxWidth);
      }
      return rect;
    },
    getDisplayProperties(index) {
      const title = this.commandName(index);
      const rect = this.itemLineRect(index);
      const baseDisplayProperties = this.commandCallbacks(index, 'getBaseDisplayProperties')(index);
      const {
        display,
        sliderType,
        textDisplay,
        textAlignment,
        valueWidth,
      } = baseDisplayProperties;
      let {
        statusWidth,
        sliderWidth,
      } = baseDisplayProperties;
      if (sliderWidth <= 0) {
        sliderWidth = rect.width + sliderWidth;
      }
      let actualTitleWidth = 0;
      if (sliderType !== 'sliderBelow') {
        actualTitleWidth+= this.contents.measureTextWidth(title) + (2 * this.itemPadding());
      }
      if (textDisplay && ['before', 'after'].includes(textAlignment)) {
        actualTitleWidth+= valueWidth + (2 * this.itemPadding());
      }
      sliderWidth = Math.min(rect.width - actualTitleWidth, sliderWidth);

      if (sliderType === 'slider') {
        statusWidth = 2 * this.itemPadding() + sliderWidth;
      } else if (sliderType === 'sliderBelow' || display === 'sliderBG') {
        statusWidth = 0;
      }
      const titleWidth = rect.width - statusWidth;

      return {
        ...baseDisplayProperties,
        statusWidth,
        sliderWidth,
        titleWidth,
      };
    },
    getBaseDisplayProperties(index) {
      const itemData = this.itemData(index);
      const display = itemData.display ?? parameters.volumeDisplay;
      const sliderType = (display.match(/^slider(Below|BG)?/) ?? [])[0] ?? null;
      let textDisplay = itemData.volumeDisplay ?? parameters['volumeSlider.volumeDisplay'] ?? 'None';
      if (textDisplay === 'None') {
        textDisplay = (
          display.match(/(Numeric|Percent)$/) ?? [])[1] ?? (
            (
              {
                numeric: 'Numeric',
                percent: 'Percent',
              }
            )[display] ?? null
        );
      }
      textDisplay = textDisplay ? textDisplay.toLowerCase() : null;
      let textAlignment = itemData.volumeAlignment ?? parameters['volumeSlider.volumeAlignment'] ?? 'Default';
      if (textAlignment === 'Default' || (textAlignment === 'Inline' && sliderType !== 'sliderBelow')) {
        textAlignment = (
          display.match(/^slider(?:Below)?(Before|Left|Center|Right|After)(?:Numeric|Percent)$/)
          ?? display.match(/^sliderBelow(Inline)(?:Numeric|Percent)$/)
          ?? []
        )[1] ?? (
          display.match(/^sliderBelow/)
          ? 'Inline'
          : 'Before'
        );
      }
      textAlignment = textAlignment.toLowerCase();
      const statusWidth = this.statusWidth();
      const currentFontFace = this.contents.fontFace;
      if (parameters.volumeNumberFont) {
        this.contents.fontFace = 'rmmz-numberfont';
      }
      const valueWidth = this.contents.measureTextWidth(`000${textDisplay === 'percent' ? '%' : ''}`);
      if (parameters.volumeNumberFont) {
        this.contents.fontFace = currentFontFace;
      }
      const sliderWidth = itemData.sliderWidth ?? parameters['volumeSlider.width'];
      const sliderHeight = itemData.sliderHeight ?? parameters['volumeSlider.height'];
      const minimizeRect = itemData.minimizeRect ?? parameters.volumeMinimizeRect;

      return {
        display,
        sliderType,
        textDisplay,
        textAlignment,
        statusWidth,
        valueWidth,
        sliderWidth,
        sliderHeight,
        minimizeRect,
      };
    }
  });

  addItemCallbacks('reset', {
    render: addItemCallbacks.renderSimple,
    ok: function() {
      const { defaults } = ConfigManager;
      const categoryData = this.getCategoryData();

      while (categoryData.length) {
        const data = categoryData.shift();

        const {
          type,
          symbol,
          ignore = false,
          resetWithParent = true,
        } = data;

        if (type === 'category' && resetWithParent) {
          categoryData.push(...this.getCategoryData(symbol));
        } else if (!ignore || typeIgnore[type]) {
          const defaultValue = CoreEssentials.findObject(symbol, defaults);
          this.setConfigValue(symbol, defaultValue);
        }
      }
      this.refreshCategory();
      this.playOkSound();
    },
    // We want to disable the changing action, since it's not relevant.
    change: () => {},
    getSpacing: () => parameters.resetToDefaultSpacing,
  });

  markTypeAsIgnore('reset');

  // Option type: Cancel
  addItemCallbacks('cancel', {
    render: addItemCallbacks.renderSimple,
    ok: function() {
      this.processCancel();
    },
    // We want to disable the changing action, since it's not relevant.
    change: () => {},
  });

  markTypeAsIgnore('cancel');

  // Option type: spacer
  addItemCallbacks('spacer', {
    getAttributes: () => ({
      selectable: false,
    }),
    render: () => {},
    renderBackground: () => {},
    getSize() {
      return -2 * this.rowSpacing();
    },
    getSpacing(index) {
      return this.itemData(index, 'height') ?? parameters.spacerHeight;
    },
  });

  markTypeAsIgnore('spacer');

  // Option type: label
  addItemCallbacks('label', {
    getAttributes: () => ({
      selectable: false,
    }),
    render: addItemCallbacks.renderSimple,
    getRect(_index, rect) {
      rect.x-= this.colSpacing() / 2;
      rect.width+= this.colSpacing();
      return rect;
    },
    getSize() {
      return this.lineHeight() - 2 * this.rowSpacing();
    },
    renderBackground: () => {},
  });

  markTypeAsIgnore('label');

  addItemCallbacks('filler', {
    getAttributes: () => ({
      selectable: false,
    }),
    render: () => {},
    renderBackground: () => {},
    getSize() {
      return -2 * this.rowSpacing();
    },
    getSpacing(index, totalHeight) {
      if (totalHeight) {
        return 0;
      }
      const commandType = this.commandType(index);
      const getWeight = this.commandCallbacks(index, 'getWeight');
      const weight = getWeight(index);
      let totalWeight = 0;
      for (let idx = 0; idx < this.maxItems(); idx += 1) {
        if (this.commandType(idx) === commandType) {
          totalWeight+= getWeight(idx);
        }
      }
      const totalItemHeight = this.getTotalItemHeight();
      return Math.max(0, this.innerHeight - totalItemHeight) * (weight / totalWeight);
    },
    getWeight(index) {
      return this.itemData(index, 'weight') ?? 1;
    }
  });

  (() => {
    // Let's first redefine ConfigManager.touchUI (if set in parameters).
    if (parameters.touchUI !== null) {
      ConfigManager.touchUI = parameters.touchUI;
    }

    //-----------------------------------------------------------------------------
    // Window_OptionsExt
    //
    // The window for changing various settings on the options screen.

    class Window_OptionsExt extends Window_Options {
      _buttonAreaHeight = 0;
      _windowTheme = null;

      /**
       * Gets run when the window gets initialized.
       * @param {string} category - The category.
       * @param {string} parent - The parent category.
       */
      initialize(rect, category = '', parent = null) {
        this._category = category;
        this._listData = this.getVisibleCategoryData();
        this._parent = parent ? [ (Array.isArray(parent) ? parent : [ parent, 0]) ] : [];
        this._optionStates = [];
        // We'll need to initialize the window before we can begin to size it, since
        // the contents sprite hasn't been loaded yet.
        super.initialize(rect);
        // Next, resize the window.
        this.refreshCategory();
        this.forceSelect(0);
      }

      getTotalItemHeight() {
        return this._listData.reduce((totalHeight, _option, index) => (
          totalHeight + this.itemHeight(index) + this.itemSpacing(index, true)
        ), 0);
      }

      /**
       * Gets the window rectangle based on the current category.
       */
      getWindowRect() {
        const buttonAreaMargin = (ConfigManager.touchUI ? this._buttonAreaHeight : 0);
        const boxHeight = Graphics.boxHeight - buttonAreaMargin;
        let windowHeight = $gameSystem.windowPadding() * 2 + this.getTotalItemHeight();
        let minWindowHeight = (parameters.minWindowHeight ?? 1);
        if (minWindowHeight <= 0) {
          minWindowHeight = boxHeight + minWindowHeight;
        }
        let maxWindowHeight = (parameters.maxWindowHeight ?? 0);
        if (maxWindowHeight <= 0) {
          maxWindowHeight = boxHeight + maxWindowHeight;
        }
        windowHeight = Math.min(maxWindowHeight, Math.max(minWindowHeight, windowHeight));

        const windowWidth = parameters.windowWidth > 0 ? parameters.windowWidth : Graphics.boxWidth + parameters.windowWidth;
        const windowX = (Graphics.boxWidth - windowWidth) / 2;
        const windowY = (boxHeight - windowHeight) / 2 + buttonAreaMargin;
        return new Rectangle(windowX, windowY, windowWidth, windowHeight);
      }

      /**
       * Adds items from the selected category.
       * @param {string} category - The category to add items from.
       */
      addCategory(category = null) {
        // Get every option of the current category.
        const categoryData = this.getVisibleCategoryData(category);

        // Iterate through each option.
        categoryData.forEach((data) => {
          // Destructure the option data.
          const {
            name,
            symbol,
            enabled,
            ext,
          } = data;

          // If name is a function, run the function, otherwise, use name as label.
          const label = typeof name === 'function' ? name(symbol) : name;

          this.addCommand(label, symbol, enabled, ext, null);
        });
      }

      /**
       * Checks whether the current category window has a parent.
       */
      hasParentCategory() {
        return !!this._parent.length;
      }

      /**
       * Gets the current category.
       */
      getCategory() {
        return this._category;
      }

      /**
       * Gets the data of the current category.
       * @param {string} category - The category you want to retrieve the data for.
       * @param {boolean} getRaw - Whether or not you want to get the raw data.
       */
      getCategoryData(category = null, getRaw = false) {
        // If no category has been set, use the current category.
        const currentCategory = category !== null ? category : this._category;

        // Get every option of the current category.
        const categoryData = categoryOptions[currentCategory];

        if (getRaw) {
          return categoryData;
        }
        return CoreEssentials.copyArray(categoryData);
      }

      /**
       * Gets the visible data of the current category.
       * @param {string} category - The category you want to retrieve the data for.
       */
      getVisibleCategoryData(category = null) {
        return this.getCategoryData(category).filter((data) => {
          if (data.visible === null) {
            return true;
          }
          if (typeof data.visible === 'function') {
            return data.visible.call(this, category);
          }
          return !!data.visible;
        });
      }

      /**
       * Sets the window to go up one category.
       */
      popCategory() {
        const parent = this._parent.pop();
        this._category = parent[0];
        this.refreshCategory();
        this.forceSelect(parent[1]);
      }

      /**
       * Switches to a different category.
       * @param {string} category - The category you want to switch to.
       */
      setCategory(category) {
        this._parent.push([this._category, this.index()]);
        this._category = category;
        this._optionStates = [];
        this.refreshCategory();
        this.forceSelect(0);
      }

      /**
       * Refreshes the category data.
       */
      refreshCategory() {
        this._listData = this.getVisibleCategoryData();
        this.clearCommandList();
        this.makeCommandList();
        const rect = this.getWindowRect();
        this.move(rect.x, rect.y, rect.width, rect.height);
        this.createContents();
        Window_Selectable.prototype.refresh.call(this);
      }

      /**
       * Gets the overall height of the current category.
       */
      overallHeight() {
        // First store the maximum amount of columns.
        const maxCols = this.maxCols();

        // Next, create an array representing the columns.
        const cols = [];

        // Now, for each item height we'll add it to the corresponding column.
        for (let idx = 0; idx < this._listData.length; idx++) {
          cols[idx % maxCols] = (cols[idx % maxCols] || 0) + this.itemHeight(idx) + this.itemSpacing(idx);
        }

        // Finally, return the largest number in the array.
        return Math.max(...cols);
      }

      cursorDown(wrap) {
        const index = this.index();
        const maxItems = this.maxItems();
        const maxCols = this.maxCols();
        let newIndex = index;
        let oldIndex;
        let wrapStartIndex = null;
        let wrapIndex = null;
        do {
          oldIndex = newIndex;
          const cursorDown = this.commandCallbacks(oldIndex, 'cursorDown');
          if (oldIndex < maxItems - maxCols || (wrap && maxCols === 1)) {
            newIndex = null;
            if (cursorDown) {
              const wrapInfo = {
                wrapStartIndex,
                wrapIndex,
              };
              newIndex = cursorDown(oldIndex, wrap, wrapInfo);
              ({wrapStartIndex = null, wrapIndex = null} = wrapInfo);
            }
            if (newIndex === null) {
              newIndex = (oldIndex + maxCols) % maxItems;
            }
            if (wrapStartIndex !== null && wrapIndex !== null) {
              if (newIndex === wrapStartIndex) {
                newIndex = wrapIndex;
              }
            }
          }
          if (oldIndex === newIndex) {
            newIndex = index;
            break;
          }
          const { selectable = true } = this.itemAttributes(newIndex);
          if (selectable) {
            break;
          }
        } while (index !== newIndex);
        if (index !== newIndex) {
          this.smoothSelect(newIndex);
        }
      }

      cursorUp(wrap) {
        const index = Math.max(0, this.index());
        const maxItems = this.maxItems();
        const maxCols = this.maxCols();
        let newIndex = index;
        let oldIndex;
        let wrapStartIndex = null;
        let wrapIndex = null;
        do {
          oldIndex = newIndex;
          const cursorUp = this.commandCallbacks(oldIndex, 'cursorUp');
          if (oldIndex >= maxCols || (wrap && maxCols === 1)) {
            newIndex = null;
            if (cursorUp) {
              const wrapInfo = {
                wrapStartIndex,
                wrapIndex,
              };
              newIndex = cursorUp(oldIndex, wrap, wrapInfo);
              ({wrapStartIndex = null, wrapIndex = null} = wrapInfo);
            }
            if (newIndex === null) {
              newIndex = (oldIndex - maxCols + maxItems) % maxItems;
            }
            if (wrapStartIndex !== null && wrapIndex !== null) {
              if (newIndex === wrapStartIndex) {
                newIndex = wrapIndex;
              }
            }
          }
          if (oldIndex === newIndex) {
            newIndex = index;
            break;
          }
          const { selectable = true } = this.itemAttributes(newIndex);
          if (selectable) {
            break;
          }
        } while (index !== newIndex);
        if (index !== newIndex) {
          this.smoothSelect(newIndex);
        }
      }

      onTouchSelect(trigger) {
        this._doubleTouch = false;
        if (this.isCursorMovable()) {
          const lastIndex = this.index();
          const hitIndex = this.hitIndex();
          if (hitIndex >= 0) {
            const { selectable = true } = this.itemAttributes(hitIndex);
            if (!selectable) {
              return;
            }
            if (hitIndex === this.index()) {
              this._doubleTouch = true;
            }
            this.select(hitIndex);
          }
          if (trigger && this.index() !== lastIndex) {
            this.playCursorSound();
          }
        }
      }

      /**
       * Creates the command list.
       */
      makeCommandList() {
        this.addCategory();
      }

      /**
       * @deprecated 1.0 - All is handled through makeCommandList.
       */
      addGeneralOptions() {
        this.addCategory('gameplay');
      }

      /**
       * @deprecated 1.0 - All is handled through makeCommandList.
       */
      addVolumeOptions() {
        this.addCategory('audio');
      };

      /**
       * Checks the command type.
       * @param {number} index - The index.
       * @return The command type.
       */
      commandType(index) {
        return this._listData[index].type;
      }

      /**
       * Retrieves the callbacks of the requested option.
       * @param {number} index - The index.
       * @param {*} callbackType - The callback type.
       */
      commandCallbacks(index, callbackType = null) {
        const type = this.commandType(index);
        const themes = this.itemThemes(index);
        return getItemCallbacks(type, callbackType, this, ...themes);
      }

      /**
       * Gets the rectangle of the current item.
       * @param {number} index - The index.
       */
      itemRect(index) {
        // Retrieve the getRect callback.
        const getRect = this.commandCallbacks(index, 'getRect');

        const rect = this.itemRectRaw(index);

        // If getRect has been defined, run the callback, otherwise, just return
        // the Rectangle object.
        if (getRect) {
          return getRect(index, rect);
        }
        return rect;
      }

      /**
       * Gets the rectangle of the current item, without modification.
       * @param {number} index - The index.
       */
      itemRectRaw(index) {
        const maxCols = this.maxCols();
        const itemWidth = this.itemWidth();
        const itemHeight = this.itemHeight(index);
        const colSpacing = this.colSpacing();
        const rowSpacing = this.rowSpacing();
        const col = index % maxCols;
        const x = col * itemWidth + colSpacing / 2 - this.scrollBaseX();

        // We'll iterate through every item before the currently requested one,
        // to calculate the y-coordinate.

        // First, set the initial y-coordinate. This would be the y-coordinate
        // if this item was the first item.
        let y = rowSpacing / 2 - this.scrollBaseY() + this.itemSpacing(index);

        // Now, for each item prior to this one, let's calculate the height and
        // add it to the y-coordinate.
        for (let idx = 0; idx < index; idx++) {
          // If there is more than one column, we'll have to ignore those items
          // that aren't in the same column as the current one.
          if (idx % maxCols === col) {
            y+= this.itemHeight(idx) + this.itemSpacing(idx);
          }
        }
        const width = itemWidth - colSpacing;
        const height = itemHeight - rowSpacing;

        return new Rectangle(x, y, width, height);

      }

      /**
       * Retrieves the item rectangle with padding.
       * @param {number} index
       * @returns
       */
      itemRectWithPadding(index, raw = true) {
        // Replace the regular itemRect with itemRectRaw.
        const rect = raw ? this.itemRectRaw(index) : this.itemRect(index);
        const padding = this.itemPadding();
        rect.x += padding;
        rect.width -= padding * 2;
        return rect;
      };

      itemLineRect(index, raw = true) {
        const rect = this.itemRectWithPadding(index, raw);
        const padding = (rect.height - this.lineHeight(index)) / 2;
        rect.y += padding;
        rect.height -= padding * 2;
        return rect;
      }

      clearItem(index) {
        const rect = this.itemRectRaw(index);
        this.contents.clearRect(rect.x, rect.y, rect.width, rect.height);
        this.contentsBack.clearRect(rect.x, rect.y, rect.width, rect.height);
        const clearItem = this.commandCallbacks(index, 'clearItem');
        if (clearItem) {
          clearItem(index);
        }
      };

      /**
       * Retrieves the line height.
       * @param {number} index - The option index.
       */
      lineHeight(index = null) {
        // If an index is set, try to retrieve the current option's size.
        if (index !== null) {
          const getSize = this.commandCallbacks(index, 'getSize');

          if (getSize) {
            return getSize(index);
          }
        }
        return super.lineHeight();
      }

      itemHeight(index = null) {
        if (index === null) {
          return super.itemHeight();
        }
        return this.lineHeight(index) + 8;
      }

      itemSpacing(index, totalHeight = false) {
        if (this._listData[index] && this._listData[index].spacing) {
          return this._listData[index].spacing;
        }
        const getSpacing = this.commandCallbacks(index, 'getSpacing');

        if (getSpacing) {
          return getSpacing(index, totalHeight);
        }

        return 0;
      }

      itemData(index, property = null) {
        /*
        We'll destructure the data, this way we can exclude parameters
        that don't need to be returned. They're also being aliased to
        avoid some possible issues with eslint.
        */
        const {
          name: _name,
          symbol: _symbol,
          enabled: _enabled,
          ext: _ext,
          ...data
        } = this._listData[index];
        // If property is set, return just the property value.
        if (property !== null) {
          if (Array.isArray(property)) {
            return property.reduce((returnData, prop) => {
              returnData[prop] = data[prop] ?? null;
              return returnData;
            }, {});
          }
          return data[property] ?? null;
        }
        return data;
      }

      itemAttributes(index) {
        const getAttributes = this.commandCallbacks(index, 'getAttributes');
        if (getAttributes) {
          return getAttributes(index);
        }
        return {};
      }

      itemThemes(index) {
        const themes = [];
        const theme = this.itemData(index, 'theme') ?? null;
        if (theme !== null) {
          themes.push(theme);
        }
        if (this._windowTheme !== null) {
          themes.push(this._windowTheme);
        }
        return themes;
      }

      ensureCursorVisible = function(smooth, index = null) {
        const idx = index || this._index;
        if (this._cursorAll) {
          this.scrollTo(0, 0);
        } else if (this.innerHeight > 0 && this.row() >= 0) {
          const rowSpacing = this.rowSpacing();
          const offsetTop = rowSpacing / 2;
          const scrollY = this.scrollY();
          const rect = this.itemRectRaw(idx);
          const itemTop = rect.y - offsetTop + this.scrollBaseY() - this.itemSpacing(idx);
          const itemBottom = itemTop + this.itemHeight(idx);
          const scrollMin = itemBottom - this.innerHeight;
          if (scrollY > itemTop) {
              if (smooth) {
                this.smoothScrollTo(0, itemTop);
              } else {
                this.scrollTo(0, itemTop);
              }
          } else if (scrollY < scrollMin) {
            if (smooth) {
              this.smoothScrollTo(0, scrollMin);
            } else {
              this.scrollTo(0, scrollMin);
            }
          }
        }
      };

      drawAllItems() {
        const rowSpacing = this.rowSpacing();
        let y = rowSpacing / 2 - this.scrollBaseY();
        for (let index = 0; index < this.maxItems(); index++) {
          const yBottom = y + this.itemHeight(index) + this.itemSpacing(index);
          if (yBottom >= 0 && y <= this.innerHeight) {
            this.drawItemBackground(index);
            this.drawItem(index);
          }
          if (y > this.innerHeight) {
            break;
          }
          y = yBottom;
        }
      }

      drawItem(index) {
        const renderCallback = this.commandCallbacks(index, 'render');

        if (renderCallback) {
          renderCallback(index);
        } else {
          super.drawItem(index);
        }
      }

      drawItemBackground(index, runCallback = true) {
        const renderBG = this.commandCallbacks(index, 'renderBackground');

        if (renderBG && runCallback) {
          renderBG(index);
        } else {
          super.drawItemBackground(index);
        }
      }

      drawText(text, x, y, maxWidth, align, index = null, lineHeight = null) {
        this.contents.drawText(text, x, y, maxWidth, lineHeight === null ? this.lineHeight(index) : lineHeight, align);
      }

      processOk() {
        const index = this.index();

        const okCallback = this.commandCallbacks(index, 'ok');

        if (okCallback) {
          okCallback(index);
        } else {
          super.processOk();
        }
        if (this.isOpenAndActive()) {
          const afterOk = this.itemData(index, 'afterOk');
          if (afterOk) {
            afterOk.call(this, index);
          }
        }
      }

      cursorRight() {
        if (!this.cursorItemChange(true)) {
          super.cursorRight();
        }
      }

      cursorLeft() {
        if (!this.cursorItemChange(false)) {
          super.cursorLeft();
        }
      }

      cursorItemChange(forward) {
        const index = this.index();

        const changeCallback = this.commandCallbacks(index, 'change');

        if (changeCallback) {
          changeCallback(index, forward);
          return true;
        }
        return false;
      }

      /**
       * Retrieves the proper text string for boolean values.
       * @param {boolean} value - Whether the option is on or off.
       */
      booleanStatusText(value) {
        return getText(value ? 'optionOn' : 'optionOff');
      }

      /**
       * Defines the step size for volume.
       * @returns The step size for volume configuration.
       */
      volumeOffset() {
        return parameters.volumeStepSize || 20;
      }

      /**
       * Retrieves a config value.
       * @param {string} symbol - The name of the config setting.
       */
      getConfigValue(symbol) {
        return CoreEssentials.findObject(symbol, ConfigManager);
      }

      /**
       * Sets a config value.
       * @param {string} symbol - The name of the config setting.
       * @param {*} value - The value that needs to be stored.
       */
      setConfigValue(symbol, value) {
        // We'll set ConfigManager as the root object. This so that we can also
        // target nested objects later if needed.
        let rootObject = ConfigManager;

        // We'll also set the property name to the symbol.
        let prop = symbol;

        // If symbol contains a dot, we can assume it's a nested object that
        // needs to be targeted.
        if (symbol.includes('.')) {
          // Split the symbol to an array, then pop the last element. This last
          // element will be the new prop.
          const symbolSegs = symbol.split('.');
          prop = symbolSegs.pop();

          // Using the remaining segments, find the proper object. This will be the
          // rootObject.
          rootObject = CoreEssentials.findObject(symbolSegs.join('.'), ConfigManager);
        }

        // Finally, set the value to the object's property.
        rootObject[prop] = value;
      };

      /**
       * Allows you to get a previously set state for the selected index.
       * @param {number} index
       * @returns {*}
       */
      getOptionState(index) {
        return this._optionStates[index] ?? null;
      }

      /**
       * Allows you to set a state for the selected index.
       * @param {number} index
       * @param {*} state
       */
      setOptionState(index, state) {
        this._optionStates[index] = state;
      };

      setButtonAreaHeight(height) {
        this._buttonAreaHeight = height;
      }

      setWindowTheme(theme) {
        this._windowTheme = theme;
      }

      /**
       * Checks whether or not the back button should be visible for the current category.
       * @param {string} category
       * @returns {boolean}
       */
      backVisible(category) {
        return true;
      }
    }

    CategorizeOptions.Window_OptionsExt = Window_OptionsExt;

    //-----------------------------------------------------------------------------
    // Scene_OptionsExt
    //
    // The scene class of the options screen.

    class Scene_OptionsExt extends Scene_Options {
      createOptionsWindow() {
        // Window_OptionsExt is being used instead of the regular Window_Options.
        this._optionsWindow = new Window_OptionsExt(this.optionsWindowRect());
        this._optionsWindow.setButtonAreaHeight(this.buttonAreaHeight());
        this._optionsWindow.setHandler("cancel", this.onCancel.bind(this));
        this._optionsWindow.setHandler("refresh", this.refreshWindows(this));
        this.addWindow(this._optionsWindow);

        this.refreshWindows();
      }

      onCancel() {
        // This fix makes sure that the options window only closes if it's on
        // the root category. Otherwise, go one category up.
        if (this._optionsWindow.hasParentCategory()) {
          this._optionsWindow.popCategory();
          this._optionsWindow.activate();
        } else {
          this.popScene();
        }
      }

      handleUI() {
        if (ConfigManager.touchUI) {
          if (this.needsCancelButton() && !this._cancelButton) {
            this.createCancelButton();
          }
        }
        if (this._cancelButton) {
          this._cancelButton.visible = ConfigManager.touchUI;
        }
      }

      refreshWindows() {
        this.handleUI();
        this._optionsWindow.refreshCategory();
      }
    }

    CategorizeOptions.Scene_OptionsExt = Scene_OptionsExt;
    CategorizeOptions.registerScene(Scene_Options, 'RMMZ.Default');
    CategorizeOptions.registerScene(Scene_OptionsExt, 'CXJ_MZ.CategorizeOptions.Default', true);

    /* --------------------------------------------------------------------
     * - Scene_Boot.prototype.initialize (Override)                       -
     * --------------------------------------------------------------------
     */

    CoreEssentials.registerFunctionExtension('Scene_Boot.prototype.initialize', function() {
      ConfigManager.defaults = ConfigManager.makeData();
    });

    /* --------------------------------------------------------------------
     * - Scene_Title.prototype.commandOptions (Override)                  -
     * --------------------------------------------------------------------
     */

    Scene_Title.prototype.commandOptions = function() {
      this._commandWindow.close();
      SceneManager.push(getOptionsScene());
    };

    /* --------------------------------------------------------------------
     * - Scene_Menu.prototype.commandOptions (Override)                   -
     * --------------------------------------------------------------------
     */

    Scene_Menu.prototype.commandOptions = function() {
      SceneManager.push(getOptionsScene());
    };

    // This manages the general volume.
    AudioManager._masterVolume = 100;

    /* --------------------------------------------------------------------
     * - AudioManager.masterVolume (New)                                  -
     * --------------------------------------------------------------------
     */
    Object.defineProperty(AudioManager, "masterVolume", {
      get: function() {
          return this._masterVolume;
      },
      set: function(value) {
          this._masterVolume = value;
          this.updateBgmParameters(this._currentBgm);
          this.updateBgsParameters(this._currentBgs);
          this.updateMeParameters(this._currentMe);
      },
      configurable: true
    });

    /* --------------------------------------------------------------------
     * - AudioManager.updateBufferParameters (Override)                   -
     * --------------------------------------------------------------------
     */

    CoreEssentials.registerFunctionExtension('AudioManager.updateBufferParameters', function(buffer, _configVolume, audio) {
      if (buffer && audio) {
        buffer.volume*= (this._masterVolume / 100);
      }
    });

    /* --------------------------------------------------------------------
     * - ConfigManager.masterVolume (New)                                 -
     * --------------------------------------------------------------------
     */
    CoreEssentials.addConfig('masterVolume', 'volume', {
      get: function() {
          return AudioManager.masterVolume;
      },
      set: function(value) {
        AudioManager.masterVolume = value;
      },
    });
  })();
})();
                                

Creator: GaryCXJk

Release date: 2020-11-30

Last updated: 2022-07-18

Downloads: 153

License: The MIT License

Requirements: