Properly implement sound and music volumes, debounce writes

pull/655/head
tobspr 4 years ago
parent 50e40888fd
commit 6042fcba62

@ -1,99 +1,100 @@
{ {
"name": "shapez.io", "name": "shapez.io",
"version": "1.0.0", "version": "1.0.0",
"main": "index.js", "main": "index.js",
"repository": "https://github.com/tobspr/shapez.io", "repository": "https://github.com/tobspr/shapez.io",
"author": "Tobias Springer <tobias.springer1@gmail.com>", "author": "Tobias Springer <tobias.springer1@gmail.com>",
"license": "MIT", "license": "MIT",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "cd gulp && yarn gulp main.serveDev", "dev": "cd gulp && yarn gulp main.serveDev",
"tslint": "cd src/js && tsc", "tslint": "cd src/js && tsc",
"lint": "eslint src/js", "lint": "eslint src/js",
"prettier-all": "prettier --write src/**/*.* && prettier --write gulp/**/*.*", "prettier-all": "prettier --write src/**/*.* && prettier --write gulp/**/*.*",
"publishOnItchWindows": "butler push tmp_standalone_files/shapez.io-standalone-win32-x64 tobspr/shapezio:windows --userversion-file version", "publishOnItchWindows": "butler push tmp_standalone_files/shapez.io-standalone-win32-x64 tobspr/shapezio:windows --userversion-file version",
"publishOnItchLinux": "butler push tmp_standalone_files/shapez.io-standalone-linux-x64 tobspr/shapezio:linux --userversion-file version", "publishOnItchLinux": "butler push tmp_standalone_files/shapez.io-standalone-linux-x64 tobspr/shapezio:linux --userversion-file version",
"publishOnItch": "yarn publishOnItchWindows && yarn publishOnItchLinux", "publishOnItch": "yarn publishOnItchWindows && yarn publishOnItchLinux",
"publishOnSteam": "cd gulp/steampipe && ./upload.bat", "publishOnSteam": "cd gulp/steampipe && ./upload.bat",
"publishStandalone": "yarn publishOnItch && yarn publishOnSteam", "publishStandalone": "yarn publishOnItch && yarn publishOnSteam",
"publishWeb": "cd gulp && yarn main.deploy.prod", "publishWeb": "cd gulp && yarn main.deploy.prod",
"publish": "yarn publishStandalone && yarn publishWeb", "publish": "yarn publishStandalone && yarn publishWeb",
"syncTranslations": "node sync-translations.js" "syncTranslations": "node sync-translations.js"
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.5.4", "@babel/core": "^7.5.4",
"@babel/plugin-transform-block-scoping": "^7.4.4", "@babel/plugin-transform-block-scoping": "^7.4.4",
"@babel/plugin-transform-classes": "^7.5.5", "@babel/plugin-transform-classes": "^7.5.5",
"@babel/preset-env": "^7.5.4", "@babel/preset-env": "^7.5.4",
"@types/cordova": "^0.0.34", "@types/cordova": "^0.0.34",
"@types/filesystem": "^0.0.29", "@types/filesystem": "^0.0.29",
"ajv": "^6.10.2", "ajv": "^6.10.2",
"babel-loader": "^8.0.4", "babel-loader": "^8.0.4",
"circular-dependency-plugin": "^5.0.2", "circular-dependency-plugin": "^5.0.2",
"circular-json": "^0.5.9", "circular-json": "^0.5.9",
"clipboard-copy": "^3.1.0", "clipboard-copy": "^3.1.0",
"colors": "^1.3.3", "colors": "^1.3.3",
"core-js": "3", "core-js": "3",
"crc": "^3.8.0", "crc": "^3.8.0",
"cssnano-preset-advanced": "^4.0.7", "cssnano-preset-advanced": "^4.0.7",
"email-validator": "^2.0.4", "debounce-promise": "^3.1.2",
"eslint": "7.1.0", "email-validator": "^2.0.4",
"fastdom": "^1.0.8", "eslint": "7.1.0",
"flatted": "^2.0.1", "fastdom": "^1.0.8",
"howler": "^2.1.2", "flatted": "^2.0.1",
"html-loader": "^0.5.5", "howler": "^2.1.2",
"ignore-loader": "^0.1.2", "html-loader": "^0.5.5",
"logrocket": "^1.0.7", "ignore-loader": "^0.1.2",
"lz-string": "^1.4.4", "logrocket": "^1.0.7",
"markdown-loader": "^4.0.0", "lz-string": "^1.4.4",
"match-all": "^1.2.5", "markdown-loader": "^4.0.0",
"phonegap-plugin-mobile-accessibility": "^1.0.5", "match-all": "^1.2.5",
"promise-polyfill": "^8.1.0", "phonegap-plugin-mobile-accessibility": "^1.0.5",
"query-string": "^6.8.1", "promise-polyfill": "^8.1.0",
"rusha": "^0.8.13", "query-string": "^6.8.1",
"serialize-error": "^3.0.0", "rusha": "^0.8.13",
"strictdom": "^1.0.1", "serialize-error": "^3.0.0",
"string-replace-webpack-plugin": "^0.1.3", "strictdom": "^1.0.1",
"terser-webpack-plugin": "^1.1.0", "string-replace-webpack-plugin": "^0.1.3",
"typescript": "3.9.3", "terser-webpack-plugin": "^1.1.0",
"uglify-template-string-loader": "^1.1.0", "typescript": "3.9.3",
"unused-files-webpack-plugin": "^3.4.0", "uglify-template-string-loader": "^1.1.0",
"webpack": "^4.43.0", "unused-files-webpack-plugin": "^3.4.0",
"webpack-bundle-analyzer": "^3.0.3", "webpack": "^4.43.0",
"webpack-cli": "^3.1.0", "webpack-bundle-analyzer": "^3.0.3",
"webpack-deep-scope-plugin": "^1.6.0", "webpack-cli": "^3.1.0",
"webpack-plugin-replace": "^1.1.1", "webpack-deep-scope-plugin": "^1.6.0",
"webpack-strip-block": "^0.2.0", "webpack-plugin-replace": "^1.1.1",
"whatwg-fetch": "^3.0.0", "webpack-strip-block": "^0.2.0",
"worker-loader": "^2.0.0", "whatwg-fetch": "^3.0.0",
"yaml": "^1.10.0", "worker-loader": "^2.0.0",
"yawn-yaml": "^1.5.0" "yaml": "^1.10.0",
}, "yawn-yaml": "^1.5.0"
"devDependencies": { },
"@typescript-eslint/eslint-plugin": "3.0.1", "devDependencies": {
"@typescript-eslint/parser": "3.0.1", "@typescript-eslint/eslint-plugin": "3.0.1",
"autoprefixer": "^9.4.3", "@typescript-eslint/parser": "3.0.1",
"babel-plugin-closure-elimination": "^1.3.0", "autoprefixer": "^9.4.3",
"babel-plugin-console-source": "^2.0.2", "babel-plugin-closure-elimination": "^1.3.0",
"babel-plugin-danger-remove-unused-import": "^1.1.2", "babel-plugin-console-source": "^2.0.2",
"css-mqpacker": "^7.0.0", "babel-plugin-danger-remove-unused-import": "^1.1.2",
"cssnano": "^4.1.10", "css-mqpacker": "^7.0.0",
"eslint-config-prettier": "6.11.0", "cssnano": "^4.1.10",
"eslint-plugin-prettier": "3.1.3", "eslint-config-prettier": "6.11.0",
"faster.js": "^1.1.0", "eslint-plugin-prettier": "3.1.3",
"glob": "^7.1.3", "faster.js": "^1.1.0",
"imagemin-mozjpeg": "^8.0.0", "glob": "^7.1.3",
"imagemin-pngquant": "^8.0.0", "imagemin-mozjpeg": "^8.0.0",
"jimp": "^0.6.1", "imagemin-pngquant": "^8.0.0",
"js-yaml": "^3.13.1", "jimp": "^0.6.1",
"postcss-assets": "^5.0.0", "js-yaml": "^3.13.1",
"postcss-preset-env": "^6.5.0", "postcss-assets": "^5.0.0",
"postcss-round-subpixels": "^1.2.0", "postcss-preset-env": "^6.5.0",
"postcss-unprefix": "^2.1.3", "postcss-round-subpixels": "^1.2.0",
"prettier": "^2.0.4", "postcss-unprefix": "^2.1.3",
"sass-unused": "^0.3.0", "prettier": "^2.0.4",
"strip-json-comments": "^3.0.1", "sass-unused": "^0.3.0",
"trim": "^0.0.1", "strip-json-comments": "^3.0.1",
"yarn": "^1.22.4" "trim": "^0.0.1",
} "yarn": "^1.22.4"
} }
}

@ -351,12 +351,12 @@ canvas {
box-sizing: border-box; box-sizing: border-box;
} }
.pressed { .pressed:not(.noPressEffect) {
transform: scale(0.98) !important; transform: scale(0.98) !important;
animation: none !important; animation: none !important;
} }
.pressedSmallElement { .pressedSmallElement:not(.noPressEffect) {
transform: scale(0.88) !important; transform: scale(0.88) !important;
animation: none !important; animation: none !important;
} }
@ -570,36 +570,46 @@ canvas {
} }
} }
.range { .rangeInputContainer {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
label {
@include S(margin-right, 5px);
&,
& * {
@include PlainText;
}
}
} }
.range-input { input.rangeInput {
cursor: pointer; cursor: pointer;
background-color: transparent; background-color: transparent;
width: 100px; @include S(width, 100px);
height: 10px; @include S(height, 16px);
transform: translate(7px, 2px);
&::-webkit-slider-runnable-track { &::-webkit-slider-runnable-track {
background-color: darken($mainBgColor, 3); background-color: darken($mainBgColor, 3);
color: darken($mainBgColor, 3); color: darken($mainBgColor, 3);
height: 16px; // @include S(height, 16px);
border-radius: 8px; @include S(border-radius, 8px);
} }
&::-webkit-slider-thumb { &::-webkit-slider-thumb {
appearance: none; appearance: none;
-webkit-appearance: none; -webkit-appearance: none;
box-shadow: inset 0 0 0 10px $themeColor; box-shadow: inset 0 0 0 D(10px) $themeColor;
background-color: transparent;
width: 20px;
height: 20px;
border-radius: 50%; border-radius: 50%;
transition: 0.3s;
transition: box-shadow 0.3s;
} }
&:hover::-webkit-slider-thumb {
box-shadow: inset 0 0 0 10px lighten($themeColor, 15); &:hover {
&::-webkit-slider-thumb {
box-shadow: inset 0 0 0 D(10px) lighten($themeColor, 15);
}
} }
} }

@ -1,160 +1,147 @@
#ingame_HUD_GameMenu { #ingame_HUD_GameMenu {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
display: flex; display: flex;
grid-auto-flow: column; grid-auto-flow: column;
> .menuButtons { > .menuButtons {
position: relative; position: relative;
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
@include S(padding, 5px, 4px); @include S(padding, 5px, 4px);
justify-content: flex-end; justify-content: flex-end;
@include S(margin-left, 20px); @include S(margin-left, 20px);
> .button { > .button {
@include S(width, 30px); @include S(width, 30px);
@include S(height, 30px); @include S(height, 30px);
display: inline-block; display: inline-block;
background: center center / 60% no-repeat; background: center center / 60% no-repeat;
pointer-events: all; pointer-events: all;
cursor: pointer; cursor: pointer;
transition: all 0.12s ease-in-out; transition: all 0.12s ease-in-out;
transition-property: opacity, transform; transition-property: opacity, transform;
will-change: opacity; will-change: opacity;
opacity: 0.9; opacity: 0.9;
@include S(margin-left, 5px); @include S(margin-left, 5px);
position: relative; position: relative;
@include IncreasedClickArea(0px); @include IncreasedClickArea(0px);
@include DarkThemeInvert; @include DarkThemeInvert;
&:hover { &:hover {
opacity: 0.8; opacity: 0.8;
} }
&.music {
background-image: uiResource("icons/music_on.png"); &.save {
&.muted { background-image: uiResource("icons/save.png");
background-image: uiResource("icons/music_off.png"); @include MakeAnimationWrappedEvenOdd(0.5s ease-in-out) {
} 0% {
} transform: scale(1, 1);
}
&.sfx {
background-image: uiResource("icons/sound_on.png"); 70% {
&.muted { transform: scale(1.5, 1.5) rotate(20deg);
background-image: uiResource("icons/sound_off.png"); opacity: 0.2;
} }
}
85% {
&.save { transform: scale(0.9, 0.9);
background-image: uiResource("icons/save.png"); opacity: 1;
@include MakeAnimationWrappedEvenOdd(0.5s ease-in-out) { }
0% {
transform: scale(1, 1); 90% {
} transform: scale(1.1, 1.1);
}
70% { }
transform: scale(1.5, 1.5) rotate(20deg); }
opacity: 0.2;
} &.settings {
background-image: uiResource("icons/settings.png");
85% { }
transform: scale(0.9, 0.9); }
opacity: 1; }
}
.buttonContainer button {
90% { @include PlainText;
transform: scale(1.1, 1.1); color: #fff;
} border-color: rgba(0, 0, 0, 0.1);
} @include S(padding, 5px, 5px, 5px);
}
@include S(padding-left, 30px);
&.settings { @include S(margin-right, 3px);
background-image: uiResource("icons/settings.png"); @include IncreasedClickArea(0px);
} @include ButtonText;
} @include S(min-height, 40px);
} transition: all 0.12s ease-in-out;
transition-property: opacity, transform;
.buttonContainer button { display: inline-flex;
@include PlainText; background: center #{D(13px)} / #{D(20px)} no-repeat;
color: #fff; background-color: $colorGreenBright;
border-color: rgba(0, 0, 0, 0.1);
@include S(padding, 5px, 5px, 5px); &[data-button-id="shop"] {
background-color: rgb(93, 103, 250);
@include S(padding-left, 30px); background-image: uiResource("icons/shop.png");
@include S(margin-right, 3px); background-size: #{D(18px)};
@include IncreasedClickArea(0px); }
@include ButtonText; &[data-button-id="stats"] {
@include S(min-height, 40px); background-color: rgb(85, 199, 138);
transition: all 0.12s ease-in-out; background-image: uiResource("icons/statistics.png");
transition-property: opacity, transform; }
display: inline-flex;
background: center #{D(13px)} / #{D(20px)} no-repeat; &:hover {
background-color: $colorGreenBright; opacity: 0.9;
}
&[data-button-id="shop"] {
background-color: rgb(93, 103, 250); .keybinding {
background-image: uiResource("icons/shop.png"); border: 0;
background-size: #{D(18px)}; color: #fff;
} border-top-left-radius: 0;
&[data-button-id="stats"] { border-top-right-radius: 0;
background-color: rgb(85, 199, 138); bottom: unset;
background-image: uiResource("icons/statistics.png"); background: transparent;
} @include S(top, 0px);
right: unset;
&:hover { left: 50%;
opacity: 0.9; transform: translateX(-50%);
} }
.keybinding { &:not(.hasBadge) .badge {
border: 0; display: none;
color: #fff; }
border-top-left-radius: 0;
border-top-right-radius: 0; &.hasBadge {
bottom: unset; transform-origin: 50% 0%;
background: transparent; @include InlineAnimation(1s ease-in-out infinite) {
@include S(top, 0px); 50% {
right: unset; transform: scale(1.02);
left: 50%; }
transform: translateX(-50%); }
}
.badge {
&:not(.hasBadge) .badge { position: absolute;
display: none; @include S(bottom, -8px);
} left: 50%;
transform: translateX(-50%);
&.hasBadge {
transform-origin: 50% 0%; background: #333;
@include InlineAnimation(1s ease-in-out infinite) { @include PlainText;
50% { display: flex;
transform: scale(1.02); justify-content: center;
} align-items: center;
} @include S(min-width, 5px);
@include S(height, 10px);
.badge { @include S(padding, 1px, 3px, 2px);
position: absolute; @include S(border-radius, $globalBorderRadius);
@include S(bottom, -8px); border: #{D(1px)} solid #fff;
left: 50%; @include InlineAnimation(1s ease-in-out infinite) {
transform: translateX(-50%); 50% {
transform: translateX(-50%) scale(1.05);
background: #333; }
@include PlainText; }
display: flex; }
justify-content: center; }
align-items: center; }
@include S(min-width, 5px); }
@include S(height, 10px);
@include S(padding, 1px, 3px, 2px);
@include S(border-radius, $globalBorderRadius);
border: #{D(1px)} solid #fff;
@include InlineAnimation(1s ease-in-out infinite) {
50% {
transform: translateX(-50%) scale(1.05);
}
}
}
}
}
}

@ -1,188 +1,188 @@
#state_SettingsState { #state_SettingsState {
$colorCategoryButton: #eee; $colorCategoryButton: #eee;
$colorCategoryButtonSelected: #5f748b; $colorCategoryButtonSelected: #5f748b;
.container .content { .container .content {
display: flex; display: flex;
overflow-y: scroll; overflow-y: scroll;
.categoryContainer { .categoryContainer {
width: 100%; width: 100%;
.category { .category {
display: none; display: none;
&.active { &.active {
display: block; display: block;
} }
.setting { .setting {
@include S(padding, 10px); @include S(padding, 10px);
background: #eeeff5; background: #eeeff5;
@include S(border-radius, $globalBorderRadius); @include S(border-radius, $globalBorderRadius);
@include S(margin-bottom, 5px); @include S(margin-bottom, 5px);
label { .desc {
text-transform: uppercase; @include S(margin-top, 5px);
@include Text; @include SuperSmallText;
} color: #aaadb2;
}
.desc {
@include S(margin-top, 5px); > .row {
@include SuperSmallText; display: grid;
color: #aaadb2; align-items: center;
} grid-template-columns: 1fr auto;
> .row { > label {
display: grid; text-transform: uppercase;
align-items: center; @include Text;
grid-template-columns: 1fr auto; }
} }
&.disabled { &.disabled {
// opacity: 0.3; // opacity: 0.3;
pointer-events: none; pointer-events: none;
* { * {
pointer-events: none !important; pointer-events: none !important;
cursor: default !important; cursor: default !important;
} }
position: relative; position: relative;
.standaloneOnlyHint { .standaloneOnlyHint {
@include PlainText; @include PlainText;
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
pointer-events: all; pointer-events: all;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: rgba(#fff, 0.5); background: rgba(#fff, 0.5);
text-transform: uppercase; text-transform: uppercase;
color: $colorRedBright; color: $colorRedBright;
} }
} }
.value.enum { .value.enum {
background: #fff; background: #fff;
@include PlainText; @include PlainText;
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
pointer-events: all; pointer-events: all;
cursor: pointer; cursor: pointer;
justify-content: center; justify-content: center;
@include S(min-width, 100px); @include S(min-width, 100px);
@include S(border-radius, $globalBorderRadius); @include S(border-radius, $globalBorderRadius);
@include S(padding, 4px); @include S(padding, 4px);
@include S(padding-right, 15px); @include S(padding-right, 15px);
background: #fff uiResource("icons/enum_selector.png") calc(100% - #{D(5px)}) background: #fff uiResource("icons/enum_selector.png") calc(100% - #{D(5px)})
calc(50% + #{D(1px)}) / #{D(15px)} no-repeat; calc(50% + #{D(1px)}) / #{D(15px)} no-repeat;
transition: background-color 0.12s ease-in-out; transition: background-color 0.12s ease-in-out;
&:hover { &:hover {
background-color: #fafafa; background-color: #fafafa;
} }
} }
} }
} }
} }
.sidebar { .sidebar {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@include S(min-width, 210px); @include S(min-width, 210px);
@include S(max-width, 320px); @include S(max-width, 320px);
width: 30%; width: 30%;
height: 100%; height: 100%;
position: sticky; position: sticky;
top: 0; top: 0;
@include S(margin-left, 20px); @include S(margin-left, 20px);
@include S(margin-right, 32px); @include S(margin-right, 32px);
.other { .other {
margin-top: auto; margin-top: auto;
} }
button { button {
@include S(margin-top, 4px); @include S(margin-top, 4px);
width: calc(100% - #{D(20px)}); width: calc(100% - #{D(20px)});
text-align: start; text-align: start;
&::after { &::after {
content: unset; content: unset;
} }
} }
button.categoryButton, button.categoryButton,
button.about { button.about {
background-color: $colorCategoryButton; background-color: $colorCategoryButton;
color: #777a7f; color: #777a7f;
&.active { &.active {
background-color: $colorCategoryButtonSelected; background-color: $colorCategoryButtonSelected;
color: #fff; color: #fff;
&:hover { &:hover {
opacity: 1; opacity: 1;
} }
} }
&.pressed { &.pressed {
transform: none !important; transform: none !important;
} }
} }
.versionbar { .versionbar {
@include S(margin-top, 20px); @include S(margin-top, 20px);
@include SuperSmallText; @include SuperSmallText;
display: grid; display: grid;
align-items: center; align-items: center;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
.buildVersion { .buildVersion {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
color: #aaadaf; color: #aaadaf;
} }
} }
} }
} }
@include DarkThemeOverride { @include DarkThemeOverride {
.container .content { .container .content {
.sidebar { .sidebar {
button.categoryButton, button.categoryButton,
button.about { button.about {
background-color: #3f3f47; background-color: #3f3f47;
&.active { &.active {
background-color: $colorBlueBright; background-color: $colorBlueBright;
} }
} }
} }
.categoryContainer { .categoryContainer {
.category { .category {
.setting { .setting {
background: darken($darkModeGameBackground, 10); background: darken($darkModeGameBackground, 10);
.value.enum { .value.enum {
// dirty but works // dirty but works
filter: invert(0.78) sepia(40%) hue-rotate(190deg); filter: invert(0.78) sepia(40%) hue-rotate(190deg);
color: #222; color: #222;
} }
.value.checkbox { .value.checkbox {
background-color: #74767b; background-color: #74767b;
&.checked { &.checked {
background-color: $colorBlueBright; background-color: $colorBlueBright;
} }
} }
} }
} }
} }
} }
} }
} }

@ -18,6 +18,7 @@ export const CHANGELOG = [
"Tier 2 tunnels are now 9 tiles wide, so the gap between is 8 tiles (double the tier 1 range)", "Tier 2 tunnels are now 9 tiles wide, so the gap between is 8 tiles (double the tier 1 range)",
"Updated and added new translations (Thanks to all contributors!)", "Updated and added new translations (Thanks to all contributors!)",
"Added setting to be able to delete buildings while placing (inspired by hexy)", "Added setting to be able to delete buildings while placing (inspired by hexy)",
"You can now adjust the sound and music volumes! (inspired by Yoshie2000)",
"Mark pinned shapes in statistics dialog and show them first (inspired by davidburhans)", "Mark pinned shapes in statistics dialog and show them first (inspired by davidburhans)",
"Added setting to show chunk borders", "Added setting to show chunk borders",
"Quad painters have been reworked! They now are integrated with the wires, and only paint the shape when the value is 1 (inspired by dengr1605)", "Quad painters have been reworked! They now are integrated with the wires, and only paint the shape when the value is 1 (inspired by dengr1605)",

@ -1,331 +1,347 @@
/* typehints:start */ /* typehints:start */
import { Application } from "../application"; import { Application } from "../application";
/* typehints:end */ /* typehints:end */
import { sha1, CRC_PREFIX, computeCrc } from "./sensitive_utils.encrypt"; import { sha1, CRC_PREFIX, computeCrc } from "./sensitive_utils.encrypt";
import { createLogger } from "./logging"; import { createLogger } from "./logging";
import { FILE_NOT_FOUND } from "../platform/storage"; import { FILE_NOT_FOUND } from "../platform/storage";
import { accessNestedPropertyReverse } from "./utils"; import { accessNestedPropertyReverse } from "./utils";
import { IS_DEBUG, globalConfig } from "./config"; import { IS_DEBUG, globalConfig } from "./config";
import { ExplainedResult } from "./explained_result"; import { ExplainedResult } from "./explained_result";
import { decompressX64, compressX64 } from "./lzstring"; import { decompressX64, compressX64 } from "./lzstring";
import { asyncCompressor, compressionPrefix } from "./async_compression"; import { asyncCompressor, compressionPrefix } from "./async_compression";
import { compressObject, decompressObject } from "../savegame/savegame_compressor"; import { compressObject, decompressObject } from "../savegame/savegame_compressor";
const logger = createLogger("read_write_proxy"); const debounce = require("debounce-promise");
const salt = accessNestedPropertyReverse(globalConfig, ["file", "info"]); const logger = createLogger("read_write_proxy");
// Helper which only writes / reads if verify() works. Also performs migration const salt = accessNestedPropertyReverse(globalConfig, ["file", "info"]);
export class ReadWriteProxy {
constructor(app, filename) { // Helper which only writes / reads if verify() works. Also performs migration
/** @type {Application} */ export class ReadWriteProxy {
this.app = app; constructor(app, filename) {
/** @type {Application} */
this.filename = filename; this.app = app;
/** @type {object} */ this.filename = filename;
this.currentData = null;
/** @type {object} */
// TODO: EXTREMELY HACKY! To verify we need to do this a step later this.currentData = null;
if (G_IS_DEV && IS_DEBUG) {
setTimeout(() => { // TODO: EXTREMELY HACKY! To verify we need to do this a step later
assert( if (G_IS_DEV && IS_DEBUG) {
this.verify(this.getDefaultData()).result, setTimeout(() => {
"Verify() failed for default data: " + this.verify(this.getDefaultData()).reason assert(
); this.verify(this.getDefaultData()).result,
}); "Verify() failed for default data: " + this.verify(this.getDefaultData()).reason
} );
} });
}
// -- Methods to override
/**
/** @returns {ExplainedResult} */ * Store a debounced handler to prevent double writes
verify(data) { */
abstract; this.debouncedWrite = debounce(this.doWriteAsync.bind(this), 50);
return ExplainedResult.bad(); }
}
// -- Methods to override
// Should return the default data
getDefaultData() { /** @returns {ExplainedResult} */
abstract; verify(data) {
return {}; abstract;
} return ExplainedResult.bad();
}
// Should return the current version as an integer
getCurrentVersion() { // Should return the default data
abstract; getDefaultData() {
return 0; abstract;
} return {};
}
// Should migrate the data (Modify in place)
/** @returns {ExplainedResult} */ // Should return the current version as an integer
migrate(data) { getCurrentVersion() {
abstract; abstract;
return ExplainedResult.bad(); return 0;
} }
// -- / Methods // Should migrate the data (Modify in place)
/** @returns {ExplainedResult} */
// Resets whole data, returns promise migrate(data) {
resetEverythingAsync() { abstract;
logger.warn("Reset data to default"); return ExplainedResult.bad();
this.currentData = this.getDefaultData(); }
return this.writeAsync();
} // -- / Methods
getCurrentData() { // Resets whole data, returns promise
return this.currentData; resetEverythingAsync() {
} logger.warn("Reset data to default");
this.currentData = this.getDefaultData();
/** return this.writeAsync();
* }
* @param {object} obj
*/ getCurrentData() {
static serializeObject(obj) { return this.currentData;
const jsonString = JSON.stringify(compressObject(obj)); }
const checksum = computeCrc(jsonString + salt);
return compressionPrefix + compressX64(checksum + jsonString); /**
} *
* @param {object} obj
/** */
* static serializeObject(obj) {
* @param {object} text const jsonString = JSON.stringify(compressObject(obj));
*/ const checksum = computeCrc(jsonString + salt);
static deserializeObject(text) { return compressionPrefix + compressX64(checksum + jsonString);
const decompressed = decompressX64(text.substr(compressionPrefix.length)); }
if (!decompressed) {
// LZ string decompression failure /**
throw new Error("bad-content / decompression-failed"); *
} * @param {object} text
if (decompressed.length < 40) { */
// String too short static deserializeObject(text) {
throw new Error("bad-content / payload-too-small"); const decompressed = decompressX64(text.substr(compressionPrefix.length));
} if (!decompressed) {
// LZ string decompression failure
// Compare stored checksum with actual checksum throw new Error("bad-content / decompression-failed");
const checksum = decompressed.substring(0, 40); }
const jsonString = decompressed.substr(40); if (decompressed.length < 40) {
// String too short
const desiredChecksum = checksum.startsWith(CRC_PREFIX) throw new Error("bad-content / payload-too-small");
? computeCrc(jsonString + salt) }
: sha1(jsonString + salt);
// Compare stored checksum with actual checksum
if (desiredChecksum !== checksum) { const checksum = decompressed.substring(0, 40);
// Checksum mismatch const jsonString = decompressed.substr(40);
throw new Error("bad-content / checksum-mismatch");
} const desiredChecksum = checksum.startsWith(CRC_PREFIX)
? computeCrc(jsonString + salt)
const parsed = JSON.parse(jsonString); : sha1(jsonString + salt);
const decoded = decompressObject(parsed);
return decoded; if (desiredChecksum !== checksum) {
} // Checksum mismatch
throw new Error("bad-content / checksum-mismatch");
/** }
* Writes the data asychronously, fails if verify() fails
* @returns {Promise<void>} const parsed = JSON.parse(jsonString);
*/ const decoded = decompressObject(parsed);
writeAsync() { return decoded;
const verifyResult = this.internalVerifyEntry(this.currentData); }
if (!verifyResult.result) { /**
logger.error("Tried to write invalid data to", this.filename, "reason:", verifyResult.reason); * Writes the data asychronously, fails if verify() fails.
return Promise.reject(verifyResult.reason); * Debounces the operation by up to 50ms
} * @returns {Promise<void>}
*/
return asyncCompressor writeAsync() {
.compressObjectAsync(this.currentData) const verifyResult = this.internalVerifyEntry(this.currentData);
.then(compressed => {
return this.app.storage.writeFileAsync(this.filename, compressed); if (!verifyResult.result) {
}) logger.error("Tried to write invalid data to", this.filename, "reason:", verifyResult.reason);
.then(() => { return Promise.reject(verifyResult.reason);
logger.log("📄 Wrote", this.filename); }
})
.catch(err => { return this.debouncedWrite();
logger.error("Failed to write", this.filename, ":", err); }
throw err;
}); /**
} * Actually writes the data asychronously
* @returns {Promise<void>}
// Reads the data asynchronously, fails if verify() fails */
readAsync() { doWriteAsync() {
// Start read request return asyncCompressor
return ( .compressObjectAsync(this.currentData)
this.app.storage .then(compressed => {
.readFileAsync(this.filename) return this.app.storage.writeFileAsync(this.filename, compressed);
})
// Check for errors during read .then(() => {
.catch(err => { logger.log("📄 Wrote", this.filename);
if (err === FILE_NOT_FOUND) { })
logger.log("File not found, using default data"); .catch(err => {
logger.error("Failed to write", this.filename, ":", err);
// File not found or unreadable, assume default file throw err;
return Promise.resolve(null); });
} }
return Promise.reject("file-error: " + err); // Reads the data asynchronously, fails if verify() fails
}) readAsync() {
// Start read request
// Decrypt data (if its encrypted) return (
// @ts-ignore this.app.storage
.then(rawData => { .readFileAsync(this.filename)
if (rawData == null) {
// So, the file has not been found, use default data // Check for errors during read
return JSON.stringify(compressObject(this.getDefaultData())); .catch(err => {
} if (err === FILE_NOT_FOUND) {
logger.log("File not found, using default data");
if (rawData.startsWith(compressionPrefix)) {
const decompressed = decompressX64(rawData.substr(compressionPrefix.length)); // File not found or unreadable, assume default file
if (!decompressed) { return Promise.resolve(null);
// LZ string decompression failure }
return Promise.reject("bad-content / decompression-failed");
} return Promise.reject("file-error: " + err);
if (decompressed.length < 40) { })
// String too short
return Promise.reject("bad-content / payload-too-small"); // Decrypt data (if its encrypted)
} // @ts-ignore
.then(rawData => {
// Compare stored checksum with actual checksum if (rawData == null) {
const checksum = decompressed.substring(0, 40); // So, the file has not been found, use default data
const jsonString = decompressed.substr(40); return JSON.stringify(compressObject(this.getDefaultData()));
}
const desiredChecksum = checksum.startsWith(CRC_PREFIX)
? computeCrc(jsonString + salt) if (rawData.startsWith(compressionPrefix)) {
: sha1(jsonString + salt); const decompressed = decompressX64(rawData.substr(compressionPrefix.length));
if (!decompressed) {
if (desiredChecksum !== checksum) { // LZ string decompression failure
// Checksum mismatch return Promise.reject("bad-content / decompression-failed");
return Promise.reject( }
"bad-content / checksum-mismatch: " + desiredChecksum + " vs " + checksum if (decompressed.length < 40) {
); // String too short
} return Promise.reject("bad-content / payload-too-small");
return jsonString; }
} else {
if (!G_IS_DEV) { // Compare stored checksum with actual checksum
return Promise.reject("bad-content / missing-compression"); const checksum = decompressed.substring(0, 40);
} const jsonString = decompressed.substr(40);
}
return rawData; const desiredChecksum = checksum.startsWith(CRC_PREFIX)
}) ? computeCrc(jsonString + salt)
: sha1(jsonString + salt);
// Parse JSON, this could throw but thats fine
.then(res => { if (desiredChecksum !== checksum) {
try { // Checksum mismatch
return JSON.parse(res); return Promise.reject(
} catch (ex) { "bad-content / checksum-mismatch: " + desiredChecksum + " vs " + checksum
logger.error( );
"Failed to parse file content of", }
this.filename, return jsonString;
":", } else {
ex, if (!G_IS_DEV) {
"(content was:", return Promise.reject("bad-content / missing-compression");
res, }
")" }
); return rawData;
throw new Error("invalid-serialized-data"); })
}
}) // Parse JSON, this could throw but thats fine
.then(res => {
// Decompress try {
.then(compressed => decompressObject(compressed)) return JSON.parse(res);
} catch (ex) {
// Verify basic structure logger.error(
.then(contents => { "Failed to parse file content of",
const result = this.internalVerifyBasicStructure(contents); this.filename,
if (!result.isGood()) { ":",
return Promise.reject("verify-failed: " + result.reason); ex,
} "(content was:",
return contents; res,
}) ")"
);
// Check version and migrate if required throw new Error("invalid-serialized-data");
.then(contents => { }
if (contents.version > this.getCurrentVersion()) { })
return Promise.reject("stored-data-is-newer");
} // Decompress
.then(compressed => decompressObject(compressed))
if (contents.version < this.getCurrentVersion()) {
logger.log( // Verify basic structure
"Trying to migrate data object from version", .then(contents => {
contents.version, const result = this.internalVerifyBasicStructure(contents);
"to", if (!result.isGood()) {
this.getCurrentVersion() return Promise.reject("verify-failed: " + result.reason);
); }
const migrationResult = this.migrate(contents); // modify in place return contents;
if (migrationResult.isBad()) { })
return Promise.reject("migration-failed: " + migrationResult.reason);
} // Check version and migrate if required
} .then(contents => {
return contents; if (contents.version > this.getCurrentVersion()) {
}) return Promise.reject("stored-data-is-newer");
}
// Verify
.then(contents => { if (contents.version < this.getCurrentVersion()) {
const verifyResult = this.internalVerifyEntry(contents); logger.log(
if (!verifyResult.result) { "Trying to migrate data object from version",
logger.error( contents.version,
"Read invalid data from", "to",
this.filename, this.getCurrentVersion()
"reason:", );
verifyResult.reason, const migrationResult = this.migrate(contents); // modify in place
"contents:", if (migrationResult.isBad()) {
contents return Promise.reject("migration-failed: " + migrationResult.reason);
); }
return Promise.reject("invalid-data: " + verifyResult.reason); }
} return contents;
return contents; })
})
// Verify
// Store .then(contents => {
.then(contents => { const verifyResult = this.internalVerifyEntry(contents);
this.currentData = contents; if (!verifyResult.result) {
logger.log("📄 Read data with version", this.currentData.version, "from", this.filename); logger.error(
return contents; "Read invalid data from",
}) this.filename,
"reason:",
// Catchall verifyResult.reason,
.catch(err => { "contents:",
return Promise.reject("Failed to read " + this.filename + ": " + err); contents
}) );
); return Promise.reject("invalid-data: " + verifyResult.reason);
} }
return contents;
/** })
* Deletes the file
* @returns {Promise<void>} // Store
*/ .then(contents => {
deleteAsync() { this.currentData = contents;
return this.app.storage.deleteFileAsync(this.filename); logger.log("📄 Read data with version", this.currentData.version, "from", this.filename);
} return contents;
})
// Internal
// Catchall
/** @returns {ExplainedResult} */ .catch(err => {
internalVerifyBasicStructure(data) { return Promise.reject("Failed to read " + this.filename + ": " + err);
if (!data) { })
return ExplainedResult.bad("Data is empty"); );
} }
if (!Number.isInteger(data.version) || data.version < 0) {
return ExplainedResult.bad( /**
`Data has invalid version: ${data.version} (expected ${this.getCurrentVersion()})` * Deletes the file
); * @returns {Promise<void>}
} */
deleteAsync() {
return ExplainedResult.good(); return this.app.storage.deleteFileAsync(this.filename);
} }
/** @returns {ExplainedResult} */ // Internal
internalVerifyEntry(data) {
if (data.version !== this.getCurrentVersion()) { /** @returns {ExplainedResult} */
return ExplainedResult.bad( internalVerifyBasicStructure(data) {
"Version mismatch, got " + data.version + " and expected " + this.getCurrentVersion() if (!data) {
); return ExplainedResult.bad("Data is empty");
} }
if (!Number.isInteger(data.version) || data.version < 0) {
const verifyStructureError = this.internalVerifyBasicStructure(data); return ExplainedResult.bad(
if (!verifyStructureError.isGood()) { `Data has invalid version: ${data.version} (expected ${this.getCurrentVersion()})`
return verifyStructureError; );
} }
return this.verify(data);
} return ExplainedResult.good();
} }
/** @returns {ExplainedResult} */
internalVerifyEntry(data) {
if (data.version !== this.getCurrentVersion()) {
return ExplainedResult.bad(
"Version mismatch, got " + data.version + " and expected " + this.getCurrentVersion()
);
}
const verifyStructureError = this.internalVerifyBasicStructure(data);
if (!verifyStructureError.isGood()) {
return verifyStructureError;
}
return this.verify(data);
}
}

@ -1,188 +1,169 @@
import { BaseHUDPart } from "../base_hud_part"; import { BaseHUDPart } from "../base_hud_part";
import { makeDiv } from "../../../core/utils"; import { makeDiv } from "../../../core/utils";
import { SOUNDS } from "../../../platform/sound"; import { SOUNDS } from "../../../platform/sound";
import { enumNotificationType } from "./notifications"; import { enumNotificationType } from "./notifications";
import { T } from "../../../translations"; import { T } from "../../../translations";
import { KEYMAPPINGS } from "../../key_action_mapper"; import { KEYMAPPINGS } from "../../key_action_mapper";
import { DynamicDomAttach } from "../dynamic_dom_attach"; import { DynamicDomAttach } from "../dynamic_dom_attach";
export class HUDGameMenu extends BaseHUDPart { export class HUDGameMenu extends BaseHUDPart {
createElements(parent) { createElements(parent) {
this.element = makeDiv(parent, "ingame_HUD_GameMenu"); this.element = makeDiv(parent, "ingame_HUD_GameMenu");
const buttons = [ const buttons = [
{ {
id: "shop", id: "shop",
label: "Upgrades", label: "Upgrades",
handler: () => this.root.hud.parts.shop.show(), handler: () => this.root.hud.parts.shop.show(),
keybinding: KEYMAPPINGS.ingame.menuOpenShop, keybinding: KEYMAPPINGS.ingame.menuOpenShop,
badge: () => this.root.hubGoals.getAvailableUpgradeCount(), badge: () => this.root.hubGoals.getAvailableUpgradeCount(),
notification: /** @type {[string, enumNotificationType]} */ ([ notification: /** @type {[string, enumNotificationType]} */ ([
T.ingame.notifications.newUpgrade, T.ingame.notifications.newUpgrade,
enumNotificationType.upgrade, enumNotificationType.upgrade,
]), ]),
visible: () => visible: () =>
!this.root.app.settings.getAllSettings().offerHints || this.root.hubGoals.level >= 3, !this.root.app.settings.getAllSettings().offerHints || this.root.hubGoals.level >= 3,
}, },
{ {
id: "stats", id: "stats",
label: "Stats", label: "Stats",
handler: () => this.root.hud.parts.statistics.show(), handler: () => this.root.hud.parts.statistics.show(),
keybinding: KEYMAPPINGS.ingame.menuOpenStats, keybinding: KEYMAPPINGS.ingame.menuOpenStats,
visible: () => visible: () =>
!this.root.app.settings.getAllSettings().offerHints || this.root.hubGoals.level >= 3, !this.root.app.settings.getAllSettings().offerHints || this.root.hubGoals.level >= 3,
}, },
]; ];
/** @type {Array<{ /** @type {Array<{
* badge: function, * badge: function,
* button: HTMLElement, * button: HTMLElement,
* badgeElement: HTMLElement, * badgeElement: HTMLElement,
* lastRenderAmount: number, * lastRenderAmount: number,
* condition?: function, * condition?: function,
* notification: [string, enumNotificationType] * notification: [string, enumNotificationType]
* }>} */ * }>} */
this.badgesToUpdate = []; this.badgesToUpdate = [];
/** @type {Array<{ /** @type {Array<{
* button: HTMLElement, * button: HTMLElement,
* condition: function, * condition: function,
* domAttach: DynamicDomAttach * domAttach: DynamicDomAttach
* }>} */ * }>} */
this.visibilityToUpdate = []; this.visibilityToUpdate = [];
this.buttonsElement = makeDiv(this.element, null, ["buttonContainer"]); this.buttonsElement = makeDiv(this.element, null, ["buttonContainer"]);
buttons.forEach(({ id, label, handler, keybinding, badge, notification, visible }) => { buttons.forEach(({ id, label, handler, keybinding, badge, notification, visible }) => {
const button = document.createElement("button"); const button = document.createElement("button");
button.setAttribute("data-button-id", id); button.setAttribute("data-button-id", id);
this.buttonsElement.appendChild(button); this.buttonsElement.appendChild(button);
this.trackClicks(button, handler); this.trackClicks(button, handler);
if (keybinding) { if (keybinding) {
const binding = this.root.keyMapper.getBinding(keybinding); const binding = this.root.keyMapper.getBinding(keybinding);
binding.add(handler); binding.add(handler);
binding.appendLabelToElement(button); binding.appendLabelToElement(button);
} }
if (visible) { if (visible) {
this.visibilityToUpdate.push({ this.visibilityToUpdate.push({
button, button,
condition: visible, condition: visible,
domAttach: new DynamicDomAttach(this.root, button), domAttach: new DynamicDomAttach(this.root, button),
}); });
} }
if (badge) { if (badge) {
const badgeElement = makeDiv(button, null, ["badge"]); const badgeElement = makeDiv(button, null, ["badge"]);
this.badgesToUpdate.push({ this.badgesToUpdate.push({
badge, badge,
lastRenderAmount: 0, lastRenderAmount: 0,
button, button,
badgeElement, badgeElement,
notification, notification,
condition: visible, condition: visible,
}); });
} }
}); });
const menuButtons = makeDiv(this.element, null, ["menuButtons"]); const menuButtons = makeDiv(this.element, null, ["menuButtons"]);
this.musicButton = makeDiv(menuButtons, null, ["button", "music"]); this.saveButton = makeDiv(menuButtons, null, ["button", "save", "animEven"]);
this.sfxButton = makeDiv(menuButtons, null, ["button", "sfx"]); this.settingsButton = makeDiv(menuButtons, null, ["button", "settings"]);
this.saveButton = makeDiv(menuButtons, null, ["button", "save", "animEven"]);
this.settingsButton = makeDiv(menuButtons, null, ["button", "settings"]); this.trackClicks(this.saveButton, this.startSave);
this.trackClicks(this.settingsButton, this.openSettings);
this.trackClicks(this.musicButton, this.toggleMusic); }
this.trackClicks(this.sfxButton, this.toggleSfx);
this.trackClicks(this.saveButton, this.startSave); initialize() {
this.trackClicks(this.settingsButton, this.openSettings); this.root.signals.gameSaved.add(this.onGameSaved, this);
}
this.musicButton.classList.toggle("muted", this.root.app.settings.getAllSettings().musicMuted);
this.sfxButton.classList.toggle("muted", this.root.app.settings.getAllSettings().soundsMuted); update() {
} let playSound = false;
initialize() { let notifications = new Set();
this.root.signals.gameSaved.add(this.onGameSaved, this);
} // Update visibility of buttons
for (let i = 0; i < this.visibilityToUpdate.length; ++i) {
update() { const { condition, domAttach } = this.visibilityToUpdate[i];
let playSound = false; domAttach.update(condition());
let notifications = new Set(); }
// Update visibility of buttons // Check for notifications and badges
for (let i = 0; i < this.visibilityToUpdate.length; ++i) { for (let i = 0; i < this.badgesToUpdate.length; ++i) {
const { button, condition, domAttach } = this.visibilityToUpdate[i]; const {
domAttach.update(condition()); badge,
} button,
badgeElement,
// Check for notifications and badges lastRenderAmount,
for (let i = 0; i < this.badgesToUpdate.length; ++i) { notification,
const { condition,
badge, } = this.badgesToUpdate[i];
button,
badgeElement, if (condition && !condition()) {
lastRenderAmount, // Do not show notifications for invisible buttons
notification, continue;
condition, }
} = this.badgesToUpdate[i];
// Check if the amount shown differs from the one shown last frame
if (condition && !condition()) { const amount = badge();
// Do not show notifications for invisible buttons if (lastRenderAmount !== amount) {
continue; if (amount > 0) {
} badgeElement.innerText = amount;
}
// Check if the amount shown differs from the one shown last frame // Check if the badge increased, if so play a notification
const amount = badge(); if (amount > lastRenderAmount) {
if (lastRenderAmount !== amount) { playSound = true;
if (amount > 0) { if (notification) {
badgeElement.innerText = amount; notifications.add(notification);
} }
// Check if the badge increased, if so play a notification }
if (amount > lastRenderAmount) {
playSound = true; // Rerender notifications
if (notification) { this.badgesToUpdate[i].lastRenderAmount = amount;
notifications.add(notification); button.classList.toggle("hasBadge", amount > 0);
} }
} }
// Rerender notifications if (playSound) {
this.badgesToUpdate[i].lastRenderAmount = amount; this.root.soundProxy.playUi(SOUNDS.badgeNotification);
button.classList.toggle("hasBadge", amount > 0); }
}
} notifications.forEach(([notification, type]) => {
this.root.hud.signals.notification.dispatch(notification, type);
if (playSound) { });
this.root.soundProxy.playUi(SOUNDS.badgeNotification); }
}
onGameSaved() {
notifications.forEach(([notification, type]) => { this.saveButton.classList.toggle("animEven");
this.root.hud.signals.notification.dispatch(notification, type); this.saveButton.classList.toggle("animOdd");
}); }
}
startSave() {
onGameSaved() { this.root.gameState.doSave();
this.saveButton.classList.toggle("animEven"); }
this.saveButton.classList.toggle("animOdd");
} openSettings() {
this.root.hud.parts.settingsMenu.show();
startSave() { }
this.root.gameState.doSave(); }
}
openSettings() {
this.root.hud.parts.settingsMenu.show();
}
toggleMusic() {
const newValue = !this.root.app.settings.getAllSettings().musicMuted;
this.root.app.settings.updateSetting("musicMuted", newValue);
this.musicButton.classList.toggle("muted", newValue);
}
toggleSfx() {
const newValue = !this.root.app.settings.getAllSettings().soundsMuted;
this.root.app.settings.updateSetting("soundsMuted", newValue);
this.sfxButton.classList.toggle("muted", newValue);
}
}

@ -185,6 +185,9 @@ export class SoundImplBrowser extends SoundInterface {
} }
initialize() { initialize() {
// NOTICE: We override the initialize() method here with custom logic because
// we have a sound sprites instance
this.sfxHandle = new SoundSpritesContainer(); this.sfxHandle = new SoundSpritesContainer();
// @ts-ignore // @ts-ignore
@ -198,11 +201,11 @@ export class SoundImplBrowser extends SoundInterface {
this.music[musicPath] = music; this.music[musicPath] = music;
} }
this.musicMuted = this.app.settings.getAllSettings().musicMuted; this.musicVolume = this.app.settings.getAllSettings().musicVolume;
this.soundsMuted = this.app.settings.getAllSettings().soundsMuted; this.soundVolume = this.app.settings.getAllSettings().soundVolume;
if (G_IS_DEV && globalConfig.debug.disableMusic) { if (G_IS_DEV && globalConfig.debug.disableMusic) {
this.musicMuted = true; this.musicVolume = 0.0;
} }
return Promise.resolve(); return Promise.resolve();

@ -103,9 +103,6 @@ export class SoundInterface {
this.pageIsVisible = true; this.pageIsVisible = true;
this.musicMuted = false;
this.soundsMuted = false;
this.musicVolume = 1.0; this.musicVolume = 1.0;
this.soundVolume = 1.0; this.soundVolume = 1.0;
} }
@ -127,13 +124,11 @@ export class SoundInterface {
this.music[musicPath] = music; this.music[musicPath] = music;
} }
this.musicMuted = this.app.settings.getAllSettings().musicMuted;
this.soundsMuted = this.app.settings.getAllSettings().soundsMuted;
this.musicVolume = this.app.settings.getAllSettings().musicVolume; this.musicVolume = this.app.settings.getAllSettings().musicVolume;
this.soundVolume = this.app.settings.getAllSettings().soundVolume; this.soundVolume = this.app.settings.getAllSettings().soundVolume;
if (G_IS_DEV && globalConfig.debug.disableMusic) { if (G_IS_DEV && globalConfig.debug.disableMusic) {
this.musicMuted = true; this.musicVolume = 0.0;
} }
return Promise.resolve(); return Promise.resolve();
@ -170,47 +165,6 @@ export class SoundInterface {
return Promise.all(...promises); return Promise.all(...promises);
} }
/**
* Returns if the music is muted
* @returns {boolean}
*/
getMusicMuted() {
return this.musicMuted;
}
/**
* Returns if sounds are muted
* @returns {boolean}
*/
getSoundsMuted() {
return this.soundsMuted;
}
/**
* Sets if the music is muted
* @param {boolean} muted
*/
setMusicMuted(muted) {
this.musicMuted = muted;
if (this.musicMuted) {
if (this.currentMusic) {
this.currentMusic.stop();
}
} else {
if (this.currentMusic) {
this.currentMusic.play(this.musicVolume);
}
}
}
/**
* Sets if the sounds are muted
* @param {boolean} muted
*/
setSoundsMuted(muted) {
this.soundsMuted = muted;
}
/** /**
* Returns the music volume * Returns the music volume
* @returns {number} * @returns {number}
@ -254,7 +208,7 @@ export class SoundInterface {
this.pageIsVisible = pageIsVisible; this.pageIsVisible = pageIsVisible;
if (this.currentMusic) { if (this.currentMusic) {
if (pageIsVisible) { if (pageIsVisible) {
if (!this.currentMusic.isPlaying() && !this.musicMuted) { if (!this.currentMusic.isPlaying()) {
this.currentMusic.play(this.musicVolume); this.currentMusic.play(this.musicVolume);
} }
} else { } else {
@ -267,9 +221,6 @@ export class SoundInterface {
* @param {string} key * @param {string} key
*/ */
playUiSound(key) { playUiSound(key) {
if (this.soundsMuted) {
return;
}
if (!this.sounds[key]) { if (!this.sounds[key]) {
logger.warn("Sound", key, "not found, probably not loaded yet"); logger.warn("Sound", key, "not found, probably not loaded yet");
return; return;
@ -288,7 +239,7 @@ export class SoundInterface {
logger.warn("Music", key, "not found, probably not loaded yet"); logger.warn("Music", key, "not found, probably not loaded yet");
return; return;
} }
if (!this.pageIsVisible || this.soundsMuted) { if (!this.pageIsVisible) {
return; return;
} }
@ -319,7 +270,7 @@ export class SoundInterface {
this.currentMusic.stop(); this.currentMusic.stop();
} }
this.currentMusic = music; this.currentMusic = music;
if (music && this.pageIsVisible && !this.musicMuted) { if (music && this.pageIsVisible) {
logger.log("Starting", this.currentMusic.key); logger.log("Starting", this.currentMusic.key);
music.play(this.musicVolume); music.play(this.musicVolume);
} }

@ -159,29 +159,13 @@ export const allApplicationSettings = [
(app, id) => app.updateAfterUiScaleChanged(), (app, id) => app.updateAfterUiScaleChanged(),
}), }),
new BoolSetting(
"soundsMuted",
enumCategories.general,
/**
* @param {Application} app
*/
(app, value) => app.sound.setSoundsMuted(value)
),
new RangeSetting( new RangeSetting(
"soundVolume", "soundVolume",
enumCategories.general, enumCategories.general,
/** /**
* @param {Application} app * @param {Application} app
*/ */
(app, value) => app.sound.setSoundVolume(value / 100.0) (app, value) => app.sound.setSoundVolume(value)
),
new BoolSetting(
"musicMuted",
enumCategories.general,
/**
* @param {Application} app
*/
(app, value) => app.sound.setMusicMuted(value)
), ),
new RangeSetting( new RangeSetting(
"musicVolume", "musicVolume",
@ -189,7 +173,7 @@ export const allApplicationSettings = [
/** /**
* @param {Application} app * @param {Application} app
*/ */
(app, value) => app.sound.setMusicVolume(value / 100.0) (app, value) => app.sound.setMusicVolume(value)
), ),
new BoolSetting( new BoolSetting(
@ -302,8 +286,6 @@ class SettingsStorage {
this.uiScale = "regular"; this.uiScale = "regular";
this.fullscreen = G_IS_STANDALONE; this.fullscreen = G_IS_STANDALONE;
this.soundsMuted = false;
this.musicMuted = false;
this.soundVolume = 1.0; this.soundVolume = 1.0;
this.musicVolume = 1.0; this.musicVolume = 1.0;
@ -515,7 +497,17 @@ export class ApplicationSettings extends ReadWriteProxy {
const setting = allApplicationSettings[i]; const setting = allApplicationSettings[i];
const storedValue = settings[setting.id]; const storedValue = settings[setting.id];
if (!setting.validate(storedValue)) { if (!setting.validate(storedValue)) {
return ExplainedResult.bad("Bad setting value for " + setting.id + ": " + storedValue); return ExplainedResult.bad(
"Bad setting value for " +
setting.id +
": " +
storedValue +
" @ settings version " +
data.version +
" (latest is " +
this.getCurrentVersion() +
")"
);
} }
} }
return ExplainedResult.good(); return ExplainedResult.good();
@ -529,7 +521,7 @@ export class ApplicationSettings extends ReadWriteProxy {
} }
getCurrentVersion() { getCurrentVersion() {
return 24; return 25;
} }
/** @param {{settings: SettingsStorage, version: number}} data */ /** @param {{settings: SettingsStorage, version: number}} data */
@ -633,12 +625,21 @@ export class ApplicationSettings extends ReadWriteProxy {
} }
if (data.version < 24) { if (data.version < 24) {
data.settings.musicVolume = 1.0;
data.settings.soundVolume = 1.0;
data.settings.refreshRate = "60"; data.settings.refreshRate = "60";
data.version = 24; data.version = 24;
} }
if (data.version < 25) {
data.settings.musicVolume = 0.5;
data.settings.soundVolume = 0.5;
// @ts-ignore
delete data.settings.musicMuted;
// @ts-ignore
delete data.settings.soundsMuted;
data.version = 25;
}
return ExplainedResult.good(); return ExplainedResult.good();
} }
} }

@ -227,10 +227,10 @@ export class RangeSetting extends BaseSetting {
category, category,
changeCb = null, changeCb = null,
enabled = true, enabled = true,
defaultValue = 100, defaultValue = 1.0,
minValue = 0, minValue = 0,
maxValue = 100, maxValue = 1.0,
stepSize = 1 stepSize = 0.0001
) { ) {
super(id, category, changeCb, enabled); super(id, category, changeCb, enabled);
@ -247,9 +247,9 @@ export class RangeSetting extends BaseSetting {
<div class="row"> <div class="row">
<label>${T.settings.labels[this.id].title}</label> <label>${T.settings.labels[this.id].title}</label>
<div class="value range" data-setting="${this.id}"> <div class="value rangeInputContainer noPressEffect" data-setting="${this.id}">
<label class="range-label">${this.defaultValue}</label> <label>${this.defaultValue}</label>
<input class="range-input" type="range" value="${this.defaultValue}" min="${ <input class="rangeInput" type="range" value="${this.defaultValue}" min="${
this.minValue this.minValue
}" max="${this.maxValue}" step="${this.stepSize}"> }" max="${this.maxValue}" step="${this.stepSize}">
</div> </div>
@ -265,33 +265,58 @@ export class RangeSetting extends BaseSetting {
this.element = element; this.element = element;
this.dialogs = dialogs; this.dialogs = dialogs;
this.element.querySelector(".range-input").addEventListener("input", () => { this.getRangeInputElement().addEventListener("input", () => {
this.updateLabels();
});
this.getRangeInputElement().addEventListener("change", () => {
this.modify(); this.modify();
}); });
} }
syncValueToElement() { syncValueToElement() {
const value = this.app.settings.getSetting(this.id); const value = this.app.settings.getSetting(this.id);
/** @type {HTMLInputElement} */ this.setElementValue(value);
const rangeInput = this.element.querySelector(".range-input"), }
rangeLabel = this.element.querySelector(".range-label");
rangeInput.value = value; /**
rangeLabel.innerHTML = value; * Sets the elements value to the given value
* @param {number} value
*/
setElementValue(value) {
const rangeInput = this.getRangeInputElement();
const rangeLabel = this.element.querySelector("label");
rangeInput.value = String(value);
rangeLabel.innerHTML = T.settings.rangeSliderPercentage.replace(
"<amount>",
String(Math.round(value * 100.0))
);
}
updateLabels() {
const value = Number(this.getRangeInputElement().value);
this.setElementValue(value);
}
/**
* @returns {HTMLInputElement}
*/
getRangeInputElement() {
return this.element.querySelector("input.rangeInput");
} }
modify() { modify() {
/** @type {HTMLInputElement} */ const rangeInput = this.getRangeInputElement();
const rangeInput = this.element.querySelector(".range-input"); const newValue = Math.round(Number(rangeInput.value) * 100.0) / 100.0;
const newValue = Number(rangeInput.value);
this.app.settings.updateSetting(this.id, newValue); this.app.settings.updateSetting(this.id, newValue);
this.syncValueToElement(); this.syncValueToElement();
console.log("SET", newValue);
if (this.changeCb) { if (this.changeCb) {
this.changeCb(this.app, newValue); this.changeCb(this.app, newValue);
} }
} }
validate(value) { validate(value) {
return typeof value === "number"; return typeof value === "number" && value >= this.minValue && value <= this.maxValue;
} }
} }

@ -728,6 +728,8 @@ settings:
prod: Production prod: Production
buildDate: Built <at-date> buildDate: Built <at-date>
rangeSliderPercentage: <amount> %
labels: labels:
uiScale: uiScale:
title: Interface scale title: Interface scale

17271
yarn.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save