1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2024-10-27 20:34:29 +00:00

Add settings

This commit is contained in:
tobspr 2020-05-17 12:46:51 +02:00
parent a70a937302
commit 53386e1cd9
17 changed files with 470 additions and 262 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 B

View File

@ -519,14 +519,14 @@ canvas {
.checkbox { .checkbox {
$bgColor: darken($mainBgColor, 0); $bgColor: darken($mainBgColor, 0);
background-color: $bgColor; background-color: $bgColor;
@include S(width, 45px); @include S(width, 35px);
@include S(height, 20px); @include S(height, 17px);
display: flex; display: flex;
@include S(padding, 3px); @include S(padding, 3px);
box-sizing: content-box; box-sizing: content-box;
cursor: pointer; cursor: pointer;
pointer-events: all; pointer-events: all;
transition: opacity 0.2s ease-in-out, background-color 0.4s ease-in-out, box-shadow 0.4s ease-in-out !important; transition: opacity 0.2s ease-in-out, background-color 0.3s ease-in-out, box-shadow 0.4s ease-in-out !important;
position: relative; position: relative;
@include BorderRadius(20px); @include BorderRadius(20px);
@include IncreasedClickArea(10px); @include IncreasedClickArea(10px);
@ -535,9 +535,13 @@ canvas {
opacity: 0.2; opacity: 0.2;
} }
&:hover {
background-color: darken($bgColor, 5);
}
.knob { .knob {
@include S(width, 20px); @include S(width, 20px);
@include S(height, 20px); @include S(height, 17px);
display: inline-block; display: inline-block;
transition: margin-left 0.4s ease-in-out !important; transition: margin-left 0.4s ease-in-out !important;
background: #fff; background: #fff;
@ -550,7 +554,11 @@ canvas {
background-color: $themeColor; background-color: $themeColor;
@include BoxShadow3D($themeColor, $size: 2px); @include BoxShadow3D($themeColor, $size: 2px);
.knob { .knob {
@include S(margin-left, 25px); @include S(margin-left, 15px);
}
&:hover {
background-color: lighten($themeColor, 15);
} }
} }
} }

View File

@ -49,6 +49,20 @@
@include S(padding, 12px); @include S(padding, 12px);
pointer-events: all; pointer-events: all;
&.optionChooserDialog {
.optionParent {
display: grid;
@include S(grid-gap, 5px);
grid-template-columns: 1fr 1fr;
.option {
pointer-events: all;
cursor: pointer;
@include S(padding, 10px);
background: #eee;
}
}
}
> .title { > .title {
@include Heading; @include Heading;
margin: 0; margin: 0;

View File

@ -23,6 +23,7 @@
@import "states/preload"; @import "states/preload";
@import "states/main_menu"; @import "states/main_menu";
@import "states/ingame"; @import "states/ingame";
@import "states/settings";
@import "ingame_hud/buildings_toolbar"; @import "ingame_hud/buildings_toolbar";
@import "ingame_hud/building_placer"; @import "ingame_hud/building_placer";

View File

@ -0,0 +1,57 @@
#state_SettingsState {
.content {
.categoryLabel {
display: block;
text-transform: uppercase;
@include S(margin-top, 15px);
@include S(margin-bottom, 15px);
@include Heading;
}
.setting {
@include S(padding, 10px);
background: #eee;
@include S(border-radius, 2px);
@include S(margin-bottom, 5px);
label {
text-transform: uppercase;
@include Text;
}
.desc {
@include S(margin-top, 5px);
@include SuperSmallText;
color: #aaadb2;
}
> .row {
display: grid;
align-items: center;
grid-template-columns: 1fr auto;
}
.value.enum {
background: #fff;
@include PlainText;
display: flex;
align-items: flex-start;
pointer-events: all;
cursor: pointer;
justify-content: center;
@include S(min-width, 100px);
@include S(border-radius, 2px);
@include S(padding, 4px);
@include S(padding-right, 15px);
background: #fff uiResource("icons/enum_selector.png") calc(100% - #{D(5px)})
calc(50% + #{D(1px)}) / #{D(15px)} no-repeat;
transition: background-color 0.12s ease-in-out;
&:hover {
background-color: #fafafa;
}
}
}
}
}

View File

@ -5,263 +5,39 @@
align-items: center; align-items: center;
$padding: 15px; $padding: 15px;
.bottomPoppingInNotification { .headerBar,
position: absolute; .content {
left: 50%; @include S(width, 500px);
text-align: center;
@include BoxShadow3D(mix(lighten($mainBgColor, 12), $colorRedBright, 50%));
@include S(padding, 10px);
max-width: #{D(280px)};
@include S(bottom, 30px);
box-sizing: border-box;
width: 100%;
@include PlainText;
@include BorderRadius(4px);
$baseTransform: translateX(-50%);
transform-origin: 0% 100%;
transform: translateY(500%);
opacity: 0;
display: block;
@include InlineAnimation(5s ease-in-out) {
0% {
opacity: 0;
transform: scale(0) skew(5deg, 5deg) translateY(100%) $baseTransform;
}
8% {
transform: scale(1.05) translateY(-2%) $baseTransform;
}
12% {
transform: scale(1) $baseTransform;
opacity: 1;
}
97% {
transform: scale(1) $baseTransform;
opacity: 1;
}
100% {
opacity: 0;
transform: scale(0) skew(5deg, 5deg) translateY(100%) $baseTransform;
}
}
}
.widthKeeper {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
box-sizing: content-box;
@include S(max-width, 1000px);
@include StyleAtHeight(800px) {
@include S(padding-top, 30px);
} }
.headerBar { .headerBar {
display: flex; display: flex;
@include VerticalStyle {
// margin-top: 1px;
}
// margin-bottom: 15px;
padding: $padding;
$h: 25px;
@include S(min-height, $h);
@include S(max-height, $h);
align-items: center; align-items: center;
justify-content: center;
position: relative;
z-index: 50;
background: transparent;
@include S(padding-top, $padding);
@include S(padding-left, $padding);
@include S(padding-right, $padding);
background-size: calc(100% - #{D(6px)}) 100%;
$paddingBottom: 20px;
@include S(padding-bottom, $paddingBottom);
@include S(margin-bottom, -$h - $padding - $paddingBottom);
h1 { h1 {
// text-align: center;
cursor: pointer;
// transform-origin: 0px 50%;
pointer-events: all;
@include S(padding, 5px, 0px, 5px, 30px);
@include S(left, -2px);
@include S(min-width, 100px);
position: relative;
@include IncreasedClickArea(25px);
text-transform: uppercase;
// background: uiResource("back_arrow.png") center center no-repeat;
@include S(background-position-x, -3px);
@include S(background-size, 25px, 25px);
// Due to back button
color: $text3dColor;
@include TextShadow3D($borderColor: #18151d);
@include SuperHeading; @include SuperHeading;
@include StyleBelowWidth(380px) {
@include Heading;
}
}
.grow {
flex-grow: 1;
}
}
.container {
text-align: left;
flex-direction: column;
pointer-events: all;
box-sizing: border-box;
z-index: 25;
position: relative;
@include S(padding-left, 0px);
@include S(padding-right, 0px);
height: 100%;
@include SupportsAndroidNotchQuery {
height: calc(
100% - constant(safe-area-inset-top) - constant(safe-area-inset-bottom) -
var(--notch-inset-top) - var(--notch-inset-bottom)
);
}
@include SupportsiOsNotchQuery {
height: calc(
100% - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) -
var(--notch-inset-top) - var(--notch-inset-bottom)
);
}
.loadingIndicator {
display: none;
}
.errorIndicator {
display: none;
flex-direction: column;
text-align: center;
.errorInner {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
@include S(max-width, 350px);
strong {
$col: #ff4564;
@include TextShadow3D($col);
@include Heading;
}
i {
@include PlainText;
color: #888;
@include S(margin-top, 10px);
display: inline-block;
}
}
}
.loadingIndicator,
.errorIndicator {
box-sizing: border-box;
justify-content: center;
align-items: center;
height: 100%;
@include S(padding, 30px);
}
// Loading state
&.loading {
.mainContent {
animation: none;
display: none !important;
}
.loadingIndicator {
display: flex;
}
}
// Error state
&.errored {
.mainContent {
animation: none;
display: none !important;
}
.errorIndicator {
animation: none;
display: flex;
}
}
}
.mainContent {
overflow-y: auto !important;
overflow-x: hidden;
@include S(padding, $padding);
height: 100%;
width: 100%;
box-sizing: border-box;
@include InlineAnimation(0.4s ease-in-out) {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.category_label {
display: block;
@include S(margin-top, 40px);
text-transform: uppercase; text-transform: uppercase;
&:first-child { color: #333438;
margin-top: 0 !important;
} }
@include S(margin-bottom, 16px); .backButton {
@include Heading; @include S(width, 30px);
@include S(height, 30px);
@include TextShadow3D(#68a1bb, $borderColor: #141718); @include S(margin-right, 10px);
@include S(margin-left, -5px);
background: uiResource("icons/state_back_button.png") center center / 70% no-repeat;
} }
.cardbox { @include S(margin-bottom, 20px);
@include S(padding, 20px, 15px);
$cardBg: lighten($mainBgColor, 9);
background: $cardBg;
margin-bottom: 15px;
@include S(margin-bottom, 15px);
@include BorderRadius(4px);
@include S(padding-bottom, 14px);
@include BoxShadow3D($cardBg);
&:last-child {
border-bottom: 0;
}
}
}
} }
&.hasTitle { .content {
.mainContent { background: #fff;
@include S(padding-top, 70px, $important: true); @include S(border-radius, 2px);
} @include S(padding, 10px);
max-height: calc(80vh - #{D(60px)});
overflow-y: auto;
box-sizing: border-box;
pointer-events: all;
} }
} }

View File

@ -1,3 +1,5 @@
$globalBorderRadius: 0px;
// When to reduce control elements size for small devices // When to reduce control elements size for small devices
$layoutExpandMinWidth: 340px; $layoutExpandMinWidth: 340px;

View File

@ -31,6 +31,7 @@ import { ShapezGameAnalytics } from "./platform/browser/game_analytics";
import { queryParamOptions } from "./core/query_parameters"; import { queryParamOptions } from "./core/query_parameters";
import { NoGameAnalytics } from "./platform/browser/no_game_analytics"; import { NoGameAnalytics } from "./platform/browser/no_game_analytics";
import { StorageImplBrowserIndexedDB } from "./platform/browser/storage_indexed_db"; import { StorageImplBrowserIndexedDB } from "./platform/browser/storage_indexed_db";
import { SettingsState } from "./states/settings";
const logger = createLogger("application"); const logger = createLogger("application");
@ -142,7 +143,7 @@ export class Application {
*/ */
registerStates() { registerStates() {
/** @type {Array<typeof GameState>} */ /** @type {Array<typeof GameState>} */
const states = [PreloadState, MainMenuState, InGameState]; const states = [PreloadState, MainMenuState, InGameState, SettingsState];
for (let i = 0; i < states.length; ++i) { for (let i = 0; i < states.length; ++i) {
this.stateMgr.register(states[i]); this.stateMgr.register(states[i]);

View File

@ -90,7 +90,7 @@ export const globalConfig = {
allBuildingsUnlocked: true, allBuildingsUnlocked: true,
upgradesNoCost: true, upgradesNoCost: true,
disableUnlockDialog: false, disableUnlockDialog: false,
testTranslations: true, // testTranslations: true,
/* dev:end */ /* dev:end */
}, },

View File

@ -0,0 +1,153 @@
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
import { GameState } from "./game_state";
import { T } from "../translations";
/**
* Baseclass for all game states which are structured similary: A header with back button + some
* scrollable content.
*/
export class TextualGameState extends GameState {
///// INTERFACE ////
/**
* Should return the states inner html. If not overriden, will create a scrollable container
* with the content of getMainContentHTML()
* @returns {string}
*/
getInnerHTML() {
return `
<div class="content mainContent">
${this.getMainContentHTML()}
</div>
`;
}
/**
* Should return the states HTML content.
*/
getMainContentHTML() {
return "";
}
/**
* Should return the title of the game state. If null, no title and back button will
* get created
* @returns {string|null}
*/
getStateHeaderTitle() {
return null;
}
/////////////
/**
* Back button handler, can be overridden. Per default it goes back to the main menu,
* or if coming from the game it moves back to the game again.
*/
onBackButton() {
if (this.backToStateId) {
this.moveToState(this.backToStateId, this.backToStatePayload);
} else {
this.moveToState(this.getDefaultPreviousState());
}
}
/**
* Returns the default state to go back to
*/
getDefaultPreviousState() {
return "MainMenuState";
}
/**
* Goes to a new state, telling him to go back to this state later
* @param {string} stateId
*/
moveToStateAddGoBack(stateId) {
this.moveToState(stateId, {
backToStateId: this.key,
backToStatePayload: {
backToStateId: this.backToStateId,
backToStatePayload: this.backToStatePayload,
},
});
}
/**
* Removes all click detectors, except the one on the back button. Useful when regenerating
* content.
*/
clearClickDetectorsExceptHeader() {
for (let i = 0; i < this.clickDetectors.length; ++i) {
const detector = this.clickDetectors[i];
if (detector.element === this.headerElement) {
continue;
}
detector.cleanup();
this.clickDetectors.splice(i, 1);
i -= 1;
}
}
/**
* Overrides the GameState implementation to provide our own html
*/
internalGetFullHtml() {
let headerHtml = "";
if (this.getStateHeaderTitle()) {
headerHtml = `
<div class="headerBar">
<button class="backButton"></button>
<h1>${this.getStateHeaderTitle()}</h1>
</div>`;
}
return `
${headerHtml}
<div class="container">
${this.getInnerHTML()}
</div>
`;
}
//// INTERNALS /////
/**
* Overrides the GameState leave callback to cleanup stuff
*/
internalLeaveCallback() {
super.internalLeaveCallback();
this.dialogs.cleanup();
}
/**
* Overrides the GameState enter callback to setup required stuff
* @param {any} payload
*/
internalEnterCallback(payload) {
super.internalEnterCallback(payload, false);
if (payload.backToStateId) {
this.backToStateId = payload.backToStateId;
this.backToStatePayload = payload.backToStatePayload;
}
this.htmlElement.classList.add("textualState");
if (this.getStateHeaderTitle()) {
this.htmlElement.classList.add("hasTitle");
}
this.containerElement = this.htmlElement.querySelector(".widthKeeper .container");
this.headerElement = this.htmlElement.querySelector(".headerBar > .backButton");
if (this.headerElement) {
this.trackClicks(this.headerElement, this.onBackButton);
}
this.dialogs = new HUDModalDialogs(null, this.app);
const dialogsElement = document.body.querySelector(".modalDialogParent");
this.dialogs.initializeToElement(dialogsElement);
this.onEnter(payload);
}
}

View File

@ -25,6 +25,10 @@ export class HUDSettingsMenu extends BaseHUDPart {
title: T.ingame.settingsMenu.buttons.continue, title: T.ingame.settingsMenu.buttons.continue,
action: () => this.close(), action: () => this.close(),
}, },
{
title: T.ingame.settingsMenu.buttons.settings,
action: () => this.goToSettings(),
},
{ {
title: T.ingame.settingsMenu.buttons.menu, title: T.ingame.settingsMenu.buttons.menu,
action: () => this.returnToMenu(), action: () => this.returnToMenu(),
@ -47,6 +51,10 @@ export class HUDSettingsMenu extends BaseHUDPart {
this.root.gameState.goBackToMenu(); this.root.gameState.goBackToMenu();
} }
goToSettings() {
this.root.gameState.goToSettings();
}
shouldPauseGame() { shouldPauseGame() {
return this.visible; return this.visible;
} }

View File

@ -3,6 +3,7 @@ import { Application } from "../application";
/* typehints:end */ /* typehints:end */
import { createLogger } from "../core/logging"; import { createLogger } from "../core/logging";
import { T } from "../translations";
const logger = createLogger("setting_types"); const logger = createLogger("setting_types");
@ -63,8 +64,8 @@ export class BaseSetting {
showRestartRequiredDialog() { showRestartRequiredDialog() {
const { restart } = this.dialogs.showInfo( const { restart } = this.dialogs.showInfo(
"Restart required", T.dialogs.restartRequired.title,
"You need to restart the game to apply the settings.", T.dialogs.restartRequired.text,
this.app.platformWrapper.getSupportsRestart() ? ["later:grey", "restart:misc"] : ["ok:good"] this.app.platformWrapper.getSupportsRestart() ? ["later:grey", "restart:misc"] : ["ok:good"]
); );
if (restart) { if (restart) {
@ -113,11 +114,11 @@ export class EnumSetting extends BaseSetting {
return ` return `
<div class="setting cardbox ${this.enabled ? "enabled" : "disabled"}"> <div class="setting cardbox ${this.enabled ? "enabled" : "disabled"}">
<div class="row"> <div class="row">
<label>TODO: SETTING TITLE</label> <label>${T.settings.labels[this.id].title}</label>
<div class="value enum" data-setting="${this.id}"></div> <div class="value enum" data-setting="${this.id}"></div>
</div> </div>
<div class="desc"> <div class="desc">
TODO: SETTING DESC ${T.settings.labels[this.id].description}
</div> </div>
</div>`; </div>`;
} }
@ -153,7 +154,7 @@ export class EnumSetting extends BaseSetting {
} }
modify() { modify() {
const { optionSelected } = this.dialogs.showOptionChooser("TODO: SETTING TITLE", { const { optionSelected } = this.dialogs.showOptionChooser(T.settings.labels[this.id].title, {
active: this.app.settings.getSetting(this.id), active: this.app.settings.getSetting(this.id),
options: this.options.map(option => ({ options: this.options.map(option => ({
value: this.valueGetter(option), value: this.valueGetter(option),
@ -186,13 +187,13 @@ export class BoolSetting extends BaseSetting {
return ` return `
<div class="setting cardbox ${this.enabled ? "enabled" : "disabled"}"> <div class="setting cardbox ${this.enabled ? "enabled" : "disabled"}">
<div class="row"> <div class="row">
<label>TODO: SETTING TITLE</label> <label>${T.settings.labels[this.id].title}</label>
<div class="value checkbox checked" data-setting="${this.id}"> <div class="value checkbox checked" data-setting="${this.id}">
<span class="knob"></span> <span class="knob"></span>
</div> </div>
</div> </div>
<div class="desc"> <div class="desc">
TODO: SETTING DESC ${T.settings.labels[this.id].description}
</div> </div>
</div>`; </div>`;
} }

View File

@ -144,6 +144,13 @@ export class InGameState extends GameState {
this.saveThenGoToState("MainMenuState"); this.saveThenGoToState("MainMenuState");
} }
/**
* Goes back to the settings state
*/
goToSettings() {
this.saveThenGoToState("SettingsState");
}
/** /**
* Moves to a state outside of the game * Moves to a state outside of the game
* @param {string} stateId * @param {string} stateId

View File

@ -285,6 +285,12 @@ export class MainMenuState extends GameState {
const savegame = this.app.savegameMgr.createNewSavegame(); const savegame = this.app.savegameMgr.createNewSavegame();
this.app.analytics.trackUiClick("startgame"); this.app.analytics.trackUiClick("startgame");
if (G_IS_DEV) {
// TODO
this.moveToState("SettingsState");
}
this.moveToState("InGameState", { this.moveToState("InGameState", {
savegame, savegame,
}); });

127
src/js/states/settings.js Normal file
View File

@ -0,0 +1,127 @@
import { TextualGameState } from "../core/textual_game_state";
import { formatSecondsToTimeAgo } from "../core/utils";
import { allApplicationSettings } from "../profile/application_settings";
import { T } from "../translations";
export class SettingsState extends TextualGameState {
constructor() {
super("SettingsState");
}
getStateHeaderTitle() {
return T.settings.title;
}
getMainContentHTML() {
return `
<div class="upperLinks">
${
this.app.platformWrapper.getSupportsKeyboard()
? `
<button class="styledButton editKeybindings">KEYBINDING TODO</button>
`
: ""
}
<button class="styledButton changelog">CHANGELOG TODO</button>
</div>
${this.getSettingsHtml()}
<div class="versionbar">
<div class="buildVersion">${T.global.loading} ...</div>
<button class="styledButton copyright">COPYRIGHT TODO</button>
</div>
`;
}
getSettingsHtml() {
let lastCategory = null;
let html = "";
for (let i = 0; i < allApplicationSettings.length; ++i) {
const setting = allApplicationSettings[i];
if (setting.categoryId !== lastCategory) {
lastCategory = setting.categoryId;
if (i !== 0) {
html += "</div>";
}
html += `<strong class="categoryLabel">${T.settings.categories[lastCategory]}</strong>`;
html += "<div class='settingsContainer'>";
}
html += setting.getHtml();
}
if (lastCategory) {
html += "</div>";
}
return html;
}
renderBuildText() {
const labelVersion = this.htmlElement.querySelector(".buildVersion");
const lastBuildMs = new Date().getTime() - G_BUILD_TIME;
const lastBuildText = formatSecondsToTimeAgo(lastBuildMs / 1000.0);
const version = T.settings.versionBadges[G_APP_ENVIRONMENT];
labelVersion.innerHTML = `
<span class='version'>
${G_BUILD_VERSION} @ ${version} @ ${G_BUILD_COMMIT_HASH}
</span>
<span class='buildTime'>
${T.settings.buildDate.replace("<at-date>", lastBuildText)}<br />
</span>`;
}
onEnter(payload) {
this.renderBuildText();
this.trackClicks(this.htmlElement.querySelector(".copyright"), this.onCopyrightClicked, {
preventDefault: false,
});
this.trackClicks(this.htmlElement.querySelector(".changelog"), this.onChangelogClicked, {
preventDefault: false,
});
const keybindingsButton = this.htmlElement.querySelector(".editKeybindings");
if (keybindingsButton) {
this.trackClicks(keybindingsButton, this.onKeybindingsClicked, { preventDefault: false });
}
this.initSettings();
}
initSettings() {
allApplicationSettings.forEach(setting => {
const element = this.htmlElement.querySelector("[data-setting='" + setting.id + "']");
setting.bind(this.app, element, this.dialogs);
setting.syncValueToElement();
this.trackClicks(
element,
() => {
setting.modify();
},
{ preventDefault: false }
);
});
}
onCopyrightClicked() {
// this.moveToStateAddGoBack("CopyrightState");
}
onChangelogClicked() {
// this.moveToStateAddGoBack("ChangelogState");
}
onKeybindingsClicked() {
// this.moveToStateAddGoBack("KeybindingsState");
}
}

View File

@ -21,6 +21,7 @@
global: global:
loading: Loading loading: Loading
error: Error
# How big numbers are rendered, e.g. "10,000" # How big numbers are rendered, e.g. "10,000"
thousandsDivider: "," thousandsDivider: ","
@ -77,6 +78,8 @@ dialogs:
ok: OK ok: OK
delete: Delete delete: Delete
cancel: Cancel cancel: Cancel
later: Later
restart: Restart
importSavegameError: importSavegameError:
title: Import Error title: Import Error
@ -103,6 +106,11 @@ dialogs:
text: >- text: >-
Failed to delete the savegame: Failed to delete the savegame:
restartRequired:
title: Restart required
text: >-
You need to restart the game to apply the settings.
ingame: ingame:
# This is shown in the top left corner and displays useful keybindings in # This is shown in the top left corner and displays useful keybindings in
# every situation # every situation
@ -186,6 +194,7 @@ ingame:
buttons: buttons:
continue: Continue continue: Continue
settings: Settings
menu: Return to menu menu: Return to menu
# All shop upgrades # All shop upgrades
@ -257,3 +266,41 @@ storyRewards:
# Special reward, which is shown when there is no reward actually # Special reward, which is shown when there is no reward actually
no_reward: Next level no_reward: Next level
settings:
title: Settings
categories:
game: Game
app: Application
versionBadges:
dev: Development
staging: Staging
prod: Production
buildDate: Built <at-date>
labels:
uiScale:
title: Interface scale
description: >-
Changes the size of the user interface. The interface will still scale based on your device resolution, but this setting controls the amount of scale.
fullscreen:
title: Fullscreen
description: >-
It is recommended to play the game in fullscreen to get the best experience. Only available in the standalone.
theme:
title: Interface theme
description: >-
Choose the interface theme which also affects the game. Notice that everything except the default theme may lead to graphical issues.
soundsMuted:
title: Mute Sounds
description: >-
If enabled, mutes all sound effects.
musicMuted:
title: Mute Music
description: >-
If enabled, mutes all music.