Properly implement sound and music volumes, debounce writes
This commit is contained in:
parent
50e40888fd
commit
6042fcba62
199
package.json
199
package.json
|
@ -1,99 +1,100 @@
|
|||
{
|
||||
"name": "shapez.io",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"repository": "https://github.com/tobspr/shapez.io",
|
||||
"author": "Tobias Springer <tobias.springer1@gmail.com>",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "cd gulp && yarn gulp main.serveDev",
|
||||
"tslint": "cd src/js && tsc",
|
||||
"lint": "eslint src/js",
|
||||
"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",
|
||||
"publishOnItchLinux": "butler push tmp_standalone_files/shapez.io-standalone-linux-x64 tobspr/shapezio:linux --userversion-file version",
|
||||
"publishOnItch": "yarn publishOnItchWindows && yarn publishOnItchLinux",
|
||||
"publishOnSteam": "cd gulp/steampipe && ./upload.bat",
|
||||
"publishStandalone": "yarn publishOnItch && yarn publishOnSteam",
|
||||
"publishWeb": "cd gulp && yarn main.deploy.prod",
|
||||
"publish": "yarn publishStandalone && yarn publishWeb",
|
||||
"syncTranslations": "node sync-translations.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.5.4",
|
||||
"@babel/plugin-transform-block-scoping": "^7.4.4",
|
||||
"@babel/plugin-transform-classes": "^7.5.5",
|
||||
"@babel/preset-env": "^7.5.4",
|
||||
"@types/cordova": "^0.0.34",
|
||||
"@types/filesystem": "^0.0.29",
|
||||
"ajv": "^6.10.2",
|
||||
"babel-loader": "^8.0.4",
|
||||
"circular-dependency-plugin": "^5.0.2",
|
||||
"circular-json": "^0.5.9",
|
||||
"clipboard-copy": "^3.1.0",
|
||||
"colors": "^1.3.3",
|
||||
"core-js": "3",
|
||||
"crc": "^3.8.0",
|
||||
"cssnano-preset-advanced": "^4.0.7",
|
||||
"email-validator": "^2.0.4",
|
||||
"eslint": "7.1.0",
|
||||
"fastdom": "^1.0.8",
|
||||
"flatted": "^2.0.1",
|
||||
"howler": "^2.1.2",
|
||||
"html-loader": "^0.5.5",
|
||||
"ignore-loader": "^0.1.2",
|
||||
"logrocket": "^1.0.7",
|
||||
"lz-string": "^1.4.4",
|
||||
"markdown-loader": "^4.0.0",
|
||||
"match-all": "^1.2.5",
|
||||
"phonegap-plugin-mobile-accessibility": "^1.0.5",
|
||||
"promise-polyfill": "^8.1.0",
|
||||
"query-string": "^6.8.1",
|
||||
"rusha": "^0.8.13",
|
||||
"serialize-error": "^3.0.0",
|
||||
"strictdom": "^1.0.1",
|
||||
"string-replace-webpack-plugin": "^0.1.3",
|
||||
"terser-webpack-plugin": "^1.1.0",
|
||||
"typescript": "3.9.3",
|
||||
"uglify-template-string-loader": "^1.1.0",
|
||||
"unused-files-webpack-plugin": "^3.4.0",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-bundle-analyzer": "^3.0.3",
|
||||
"webpack-cli": "^3.1.0",
|
||||
"webpack-deep-scope-plugin": "^1.6.0",
|
||||
"webpack-plugin-replace": "^1.1.1",
|
||||
"webpack-strip-block": "^0.2.0",
|
||||
"whatwg-fetch": "^3.0.0",
|
||||
"worker-loader": "^2.0.0",
|
||||
"yaml": "^1.10.0",
|
||||
"yawn-yaml": "^1.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "3.0.1",
|
||||
"@typescript-eslint/parser": "3.0.1",
|
||||
"autoprefixer": "^9.4.3",
|
||||
"babel-plugin-closure-elimination": "^1.3.0",
|
||||
"babel-plugin-console-source": "^2.0.2",
|
||||
"babel-plugin-danger-remove-unused-import": "^1.1.2",
|
||||
"css-mqpacker": "^7.0.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"eslint-config-prettier": "6.11.0",
|
||||
"eslint-plugin-prettier": "3.1.3",
|
||||
"faster.js": "^1.1.0",
|
||||
"glob": "^7.1.3",
|
||||
"imagemin-mozjpeg": "^8.0.0",
|
||||
"imagemin-pngquant": "^8.0.0",
|
||||
"jimp": "^0.6.1",
|
||||
"js-yaml": "^3.13.1",
|
||||
"postcss-assets": "^5.0.0",
|
||||
"postcss-preset-env": "^6.5.0",
|
||||
"postcss-round-subpixels": "^1.2.0",
|
||||
"postcss-unprefix": "^2.1.3",
|
||||
"prettier": "^2.0.4",
|
||||
"sass-unused": "^0.3.0",
|
||||
"strip-json-comments": "^3.0.1",
|
||||
"trim": "^0.0.1",
|
||||
"yarn": "^1.22.4"
|
||||
}
|
||||
}
|
||||
{
|
||||
"name": "shapez.io",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"repository": "https://github.com/tobspr/shapez.io",
|
||||
"author": "Tobias Springer <tobias.springer1@gmail.com>",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "cd gulp && yarn gulp main.serveDev",
|
||||
"tslint": "cd src/js && tsc",
|
||||
"lint": "eslint src/js",
|
||||
"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",
|
||||
"publishOnItchLinux": "butler push tmp_standalone_files/shapez.io-standalone-linux-x64 tobspr/shapezio:linux --userversion-file version",
|
||||
"publishOnItch": "yarn publishOnItchWindows && yarn publishOnItchLinux",
|
||||
"publishOnSteam": "cd gulp/steampipe && ./upload.bat",
|
||||
"publishStandalone": "yarn publishOnItch && yarn publishOnSteam",
|
||||
"publishWeb": "cd gulp && yarn main.deploy.prod",
|
||||
"publish": "yarn publishStandalone && yarn publishWeb",
|
||||
"syncTranslations": "node sync-translations.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.5.4",
|
||||
"@babel/plugin-transform-block-scoping": "^7.4.4",
|
||||
"@babel/plugin-transform-classes": "^7.5.5",
|
||||
"@babel/preset-env": "^7.5.4",
|
||||
"@types/cordova": "^0.0.34",
|
||||
"@types/filesystem": "^0.0.29",
|
||||
"ajv": "^6.10.2",
|
||||
"babel-loader": "^8.0.4",
|
||||
"circular-dependency-plugin": "^5.0.2",
|
||||
"circular-json": "^0.5.9",
|
||||
"clipboard-copy": "^3.1.0",
|
||||
"colors": "^1.3.3",
|
||||
"core-js": "3",
|
||||
"crc": "^3.8.0",
|
||||
"cssnano-preset-advanced": "^4.0.7",
|
||||
"debounce-promise": "^3.1.2",
|
||||
"email-validator": "^2.0.4",
|
||||
"eslint": "7.1.0",
|
||||
"fastdom": "^1.0.8",
|
||||
"flatted": "^2.0.1",
|
||||
"howler": "^2.1.2",
|
||||
"html-loader": "^0.5.5",
|
||||
"ignore-loader": "^0.1.2",
|
||||
"logrocket": "^1.0.7",
|
||||
"lz-string": "^1.4.4",
|
||||
"markdown-loader": "^4.0.0",
|
||||
"match-all": "^1.2.5",
|
||||
"phonegap-plugin-mobile-accessibility": "^1.0.5",
|
||||
"promise-polyfill": "^8.1.0",
|
||||
"query-string": "^6.8.1",
|
||||
"rusha": "^0.8.13",
|
||||
"serialize-error": "^3.0.0",
|
||||
"strictdom": "^1.0.1",
|
||||
"string-replace-webpack-plugin": "^0.1.3",
|
||||
"terser-webpack-plugin": "^1.1.0",
|
||||
"typescript": "3.9.3",
|
||||
"uglify-template-string-loader": "^1.1.0",
|
||||
"unused-files-webpack-plugin": "^3.4.0",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-bundle-analyzer": "^3.0.3",
|
||||
"webpack-cli": "^3.1.0",
|
||||
"webpack-deep-scope-plugin": "^1.6.0",
|
||||
"webpack-plugin-replace": "^1.1.1",
|
||||
"webpack-strip-block": "^0.2.0",
|
||||
"whatwg-fetch": "^3.0.0",
|
||||
"worker-loader": "^2.0.0",
|
||||
"yaml": "^1.10.0",
|
||||
"yawn-yaml": "^1.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "3.0.1",
|
||||
"@typescript-eslint/parser": "3.0.1",
|
||||
"autoprefixer": "^9.4.3",
|
||||
"babel-plugin-closure-elimination": "^1.3.0",
|
||||
"babel-plugin-console-source": "^2.0.2",
|
||||
"babel-plugin-danger-remove-unused-import": "^1.1.2",
|
||||
"css-mqpacker": "^7.0.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"eslint-config-prettier": "6.11.0",
|
||||
"eslint-plugin-prettier": "3.1.3",
|
||||
"faster.js": "^1.1.0",
|
||||
"glob": "^7.1.3",
|
||||
"imagemin-mozjpeg": "^8.0.0",
|
||||
"imagemin-pngquant": "^8.0.0",
|
||||
"jimp": "^0.6.1",
|
||||
"js-yaml": "^3.13.1",
|
||||
"postcss-assets": "^5.0.0",
|
||||
"postcss-preset-env": "^6.5.0",
|
||||
"postcss-round-subpixels": "^1.2.0",
|
||||
"postcss-unprefix": "^2.1.3",
|
||||
"prettier": "^2.0.4",
|
||||
"sass-unused": "^0.3.0",
|
||||
"strip-json-comments": "^3.0.1",
|
||||
"trim": "^0.0.1",
|
||||
"yarn": "^1.22.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -351,12 +351,12 @@ canvas {
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.pressed {
|
||||
.pressed:not(.noPressEffect) {
|
||||
transform: scale(0.98) !important;
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
.pressedSmallElement {
|
||||
.pressedSmallElement:not(.noPressEffect) {
|
||||
transform: scale(0.88) !important;
|
||||
animation: none !important;
|
||||
}
|
||||
|
@ -570,36 +570,46 @@ canvas {
|
|||
}
|
||||
}
|
||||
|
||||
.range {
|
||||
.rangeInputContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
label {
|
||||
@include S(margin-right, 5px);
|
||||
&,
|
||||
& * {
|
||||
@include PlainText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.range-input {
|
||||
input.rangeInput {
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
width: 100px;
|
||||
height: 10px;
|
||||
transform: translate(7px, 2px);
|
||||
@include S(width, 100px);
|
||||
@include S(height, 16px);
|
||||
|
||||
&::-webkit-slider-runnable-track {
|
||||
background-color: darken($mainBgColor, 3);
|
||||
color: darken($mainBgColor, 3);
|
||||
height: 16px;
|
||||
border-radius: 8px;
|
||||
// @include S(height, 16px);
|
||||
@include S(border-radius, 8px);
|
||||
}
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
box-shadow: inset 0 0 0 10px $themeColor;
|
||||
background-color: transparent;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
box-shadow: inset 0 0 0 D(10px) $themeColor;
|
||||
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 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
grid-auto-flow: column;
|
||||
|
||||
> .menuButtons {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
@include S(padding, 5px, 4px);
|
||||
justify-content: flex-end;
|
||||
@include S(margin-left, 20px);
|
||||
|
||||
> .button {
|
||||
@include S(width, 30px);
|
||||
@include S(height, 30px);
|
||||
display: inline-block;
|
||||
background: center center / 60% no-repeat;
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
transition: all 0.12s ease-in-out;
|
||||
transition-property: opacity, transform;
|
||||
will-change: opacity;
|
||||
opacity: 0.9;
|
||||
@include S(margin-left, 5px);
|
||||
position: relative;
|
||||
|
||||
@include IncreasedClickArea(0px);
|
||||
|
||||
@include DarkThemeInvert;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
&.music {
|
||||
background-image: uiResource("icons/music_on.png");
|
||||
&.muted {
|
||||
background-image: uiResource("icons/music_off.png");
|
||||
}
|
||||
}
|
||||
|
||||
&.sfx {
|
||||
background-image: uiResource("icons/sound_on.png");
|
||||
&.muted {
|
||||
background-image: uiResource("icons/sound_off.png");
|
||||
}
|
||||
}
|
||||
|
||||
&.save {
|
||||
background-image: uiResource("icons/save.png");
|
||||
@include MakeAnimationWrappedEvenOdd(0.5s ease-in-out) {
|
||||
0% {
|
||||
transform: scale(1, 1);
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: scale(1.5, 1.5) rotate(20deg);
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
85% {
|
||||
transform: scale(0.9, 0.9);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
90% {
|
||||
transform: scale(1.1, 1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.settings {
|
||||
background-image: uiResource("icons/settings.png");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttonContainer button {
|
||||
@include PlainText;
|
||||
color: #fff;
|
||||
border-color: rgba(0, 0, 0, 0.1);
|
||||
@include S(padding, 5px, 5px, 5px);
|
||||
|
||||
@include S(padding-left, 30px);
|
||||
@include S(margin-right, 3px);
|
||||
@include IncreasedClickArea(0px);
|
||||
@include ButtonText;
|
||||
@include S(min-height, 40px);
|
||||
transition: all 0.12s ease-in-out;
|
||||
transition-property: opacity, transform;
|
||||
display: inline-flex;
|
||||
background: center #{D(13px)} / #{D(20px)} no-repeat;
|
||||
background-color: $colorGreenBright;
|
||||
|
||||
&[data-button-id="shop"] {
|
||||
background-color: rgb(93, 103, 250);
|
||||
background-image: uiResource("icons/shop.png");
|
||||
background-size: #{D(18px)};
|
||||
}
|
||||
&[data-button-id="stats"] {
|
||||
background-color: rgb(85, 199, 138);
|
||||
background-image: uiResource("icons/statistics.png");
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.keybinding {
|
||||
border: 0;
|
||||
color: #fff;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
bottom: unset;
|
||||
background: transparent;
|
||||
@include S(top, 0px);
|
||||
right: unset;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
&:not(.hasBadge) .badge {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.hasBadge {
|
||||
transform-origin: 50% 0%;
|
||||
@include InlineAnimation(1s ease-in-out infinite) {
|
||||
50% {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
@include S(bottom, -8px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#ingame_HUD_GameMenu {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
grid-auto-flow: column;
|
||||
|
||||
> .menuButtons {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
@include S(padding, 5px, 4px);
|
||||
justify-content: flex-end;
|
||||
@include S(margin-left, 20px);
|
||||
|
||||
> .button {
|
||||
@include S(width, 30px);
|
||||
@include S(height, 30px);
|
||||
display: inline-block;
|
||||
background: center center / 60% no-repeat;
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
transition: all 0.12s ease-in-out;
|
||||
transition-property: opacity, transform;
|
||||
will-change: opacity;
|
||||
opacity: 0.9;
|
||||
@include S(margin-left, 5px);
|
||||
position: relative;
|
||||
|
||||
@include IncreasedClickArea(0px);
|
||||
|
||||
@include DarkThemeInvert;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&.save {
|
||||
background-image: uiResource("icons/save.png");
|
||||
@include MakeAnimationWrappedEvenOdd(0.5s ease-in-out) {
|
||||
0% {
|
||||
transform: scale(1, 1);
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: scale(1.5, 1.5) rotate(20deg);
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
85% {
|
||||
transform: scale(0.9, 0.9);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
90% {
|
||||
transform: scale(1.1, 1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.settings {
|
||||
background-image: uiResource("icons/settings.png");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttonContainer button {
|
||||
@include PlainText;
|
||||
color: #fff;
|
||||
border-color: rgba(0, 0, 0, 0.1);
|
||||
@include S(padding, 5px, 5px, 5px);
|
||||
|
||||
@include S(padding-left, 30px);
|
||||
@include S(margin-right, 3px);
|
||||
@include IncreasedClickArea(0px);
|
||||
@include ButtonText;
|
||||
@include S(min-height, 40px);
|
||||
transition: all 0.12s ease-in-out;
|
||||
transition-property: opacity, transform;
|
||||
display: inline-flex;
|
||||
background: center #{D(13px)} / #{D(20px)} no-repeat;
|
||||
background-color: $colorGreenBright;
|
||||
|
||||
&[data-button-id="shop"] {
|
||||
background-color: rgb(93, 103, 250);
|
||||
background-image: uiResource("icons/shop.png");
|
||||
background-size: #{D(18px)};
|
||||
}
|
||||
&[data-button-id="stats"] {
|
||||
background-color: rgb(85, 199, 138);
|
||||
background-image: uiResource("icons/statistics.png");
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.keybinding {
|
||||
border: 0;
|
||||
color: #fff;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
bottom: unset;
|
||||
background: transparent;
|
||||
@include S(top, 0px);
|
||||
right: unset;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
&:not(.hasBadge) .badge {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.hasBadge {
|
||||
transform-origin: 50% 0%;
|
||||
@include InlineAnimation(1s ease-in-out infinite) {
|
||||
50% {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
@include S(bottom, -8px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
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 {
|
||||
$colorCategoryButton: #eee;
|
||||
$colorCategoryButtonSelected: #5f748b;
|
||||
|
||||
.container .content {
|
||||
display: flex;
|
||||
overflow-y: scroll;
|
||||
|
||||
.categoryContainer {
|
||||
width: 100%;
|
||||
|
||||
.category {
|
||||
display: none;
|
||||
|
||||
&.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.setting {
|
||||
@include S(padding, 10px);
|
||||
background: #eeeff5;
|
||||
@include S(border-radius, $globalBorderRadius);
|
||||
@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;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
// opacity: 0.3;
|
||||
pointer-events: none;
|
||||
* {
|
||||
pointer-events: none !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
position: relative;
|
||||
.standaloneOnlyHint {
|
||||
@include PlainText;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: all;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(#fff, 0.5);
|
||||
text-transform: uppercase;
|
||||
color: $colorRedBright;
|
||||
}
|
||||
}
|
||||
|
||||
.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, $globalBorderRadius);
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@include S(min-width, 210px);
|
||||
@include S(max-width, 320px);
|
||||
width: 30%;
|
||||
height: 100%;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
@include S(margin-left, 20px);
|
||||
@include S(margin-right, 32px);
|
||||
|
||||
.other {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
button {
|
||||
@include S(margin-top, 4px);
|
||||
width: calc(100% - #{D(20px)});
|
||||
text-align: start;
|
||||
|
||||
&::after {
|
||||
content: unset;
|
||||
}
|
||||
}
|
||||
|
||||
button.categoryButton,
|
||||
button.about {
|
||||
background-color: $colorCategoryButton;
|
||||
color: #777a7f;
|
||||
|
||||
&.active {
|
||||
background-color: $colorCategoryButtonSelected;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.pressed {
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.versionbar {
|
||||
@include S(margin-top, 20px);
|
||||
@include SuperSmallText;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
grid-template-columns: 1fr auto;
|
||||
.buildVersion {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: #aaadaf;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include DarkThemeOverride {
|
||||
.container .content {
|
||||
.sidebar {
|
||||
button.categoryButton,
|
||||
button.about {
|
||||
background-color: #3f3f47;
|
||||
|
||||
&.active {
|
||||
background-color: $colorBlueBright;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.categoryContainer {
|
||||
.category {
|
||||
.setting {
|
||||
background: darken($darkModeGameBackground, 10);
|
||||
|
||||
.value.enum {
|
||||
// dirty but works
|
||||
filter: invert(0.78) sepia(40%) hue-rotate(190deg);
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.value.checkbox {
|
||||
background-color: #74767b;
|
||||
|
||||
&.checked {
|
||||
background-color: $colorBlueBright;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#state_SettingsState {
|
||||
$colorCategoryButton: #eee;
|
||||
$colorCategoryButtonSelected: #5f748b;
|
||||
|
||||
.container .content {
|
||||
display: flex;
|
||||
overflow-y: scroll;
|
||||
|
||||
.categoryContainer {
|
||||
width: 100%;
|
||||
|
||||
.category {
|
||||
display: none;
|
||||
|
||||
&.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.setting {
|
||||
@include S(padding, 10px);
|
||||
background: #eeeff5;
|
||||
@include S(border-radius, $globalBorderRadius);
|
||||
@include S(margin-bottom, 5px);
|
||||
|
||||
.desc {
|
||||
@include S(margin-top, 5px);
|
||||
@include SuperSmallText;
|
||||
color: #aaadb2;
|
||||
}
|
||||
|
||||
> .row {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
grid-template-columns: 1fr auto;
|
||||
|
||||
> label {
|
||||
text-transform: uppercase;
|
||||
@include Text;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
// opacity: 0.3;
|
||||
pointer-events: none;
|
||||
* {
|
||||
pointer-events: none !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
position: relative;
|
||||
.standaloneOnlyHint {
|
||||
@include PlainText;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: all;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(#fff, 0.5);
|
||||
text-transform: uppercase;
|
||||
color: $colorRedBright;
|
||||
}
|
||||
}
|
||||
|
||||
.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, $globalBorderRadius);
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@include S(min-width, 210px);
|
||||
@include S(max-width, 320px);
|
||||
width: 30%;
|
||||
height: 100%;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
@include S(margin-left, 20px);
|
||||
@include S(margin-right, 32px);
|
||||
|
||||
.other {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
button {
|
||||
@include S(margin-top, 4px);
|
||||
width: calc(100% - #{D(20px)});
|
||||
text-align: start;
|
||||
|
||||
&::after {
|
||||
content: unset;
|
||||
}
|
||||
}
|
||||
|
||||
button.categoryButton,
|
||||
button.about {
|
||||
background-color: $colorCategoryButton;
|
||||
color: #777a7f;
|
||||
|
||||
&.active {
|
||||
background-color: $colorCategoryButtonSelected;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.pressed {
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.versionbar {
|
||||
@include S(margin-top, 20px);
|
||||
@include SuperSmallText;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
grid-template-columns: 1fr auto;
|
||||
.buildVersion {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: #aaadaf;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include DarkThemeOverride {
|
||||
.container .content {
|
||||
.sidebar {
|
||||
button.categoryButton,
|
||||
button.about {
|
||||
background-color: #3f3f47;
|
||||
|
||||
&.active {
|
||||
background-color: $colorBlueBright;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.categoryContainer {
|
||||
.category {
|
||||
.setting {
|
||||
background: darken($darkModeGameBackground, 10);
|
||||
|
||||
.value.enum {
|
||||
// dirty but works
|
||||
filter: invert(0.78) sepia(40%) hue-rotate(190deg);
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.value.checkbox {
|
||||
background-color: #74767b;
|
||||
|
||||
&.checked {
|
||||
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)",
|
||||
"Updated and added new translations (Thanks to all contributors!)",
|
||||
"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)",
|
||||
"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)",
|
||||
|
|
|
@ -1,331 +1,347 @@
|
|||
/* typehints:start */
|
||||
import { Application } from "../application";
|
||||
/* typehints:end */
|
||||
|
||||
import { sha1, CRC_PREFIX, computeCrc } from "./sensitive_utils.encrypt";
|
||||
import { createLogger } from "./logging";
|
||||
import { FILE_NOT_FOUND } from "../platform/storage";
|
||||
import { accessNestedPropertyReverse } from "./utils";
|
||||
import { IS_DEBUG, globalConfig } from "./config";
|
||||
import { ExplainedResult } from "./explained_result";
|
||||
import { decompressX64, compressX64 } from "./lzstring";
|
||||
import { asyncCompressor, compressionPrefix } from "./async_compression";
|
||||
import { compressObject, decompressObject } from "../savegame/savegame_compressor";
|
||||
|
||||
const logger = createLogger("read_write_proxy");
|
||||
|
||||
const salt = accessNestedPropertyReverse(globalConfig, ["file", "info"]);
|
||||
|
||||
// Helper which only writes / reads if verify() works. Also performs migration
|
||||
export class ReadWriteProxy {
|
||||
constructor(app, filename) {
|
||||
/** @type {Application} */
|
||||
this.app = app;
|
||||
|
||||
this.filename = filename;
|
||||
|
||||
/** @type {object} */
|
||||
this.currentData = null;
|
||||
|
||||
// TODO: EXTREMELY HACKY! To verify we need to do this a step later
|
||||
if (G_IS_DEV && IS_DEBUG) {
|
||||
setTimeout(() => {
|
||||
assert(
|
||||
this.verify(this.getDefaultData()).result,
|
||||
"Verify() failed for default data: " + this.verify(this.getDefaultData()).reason
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// -- Methods to override
|
||||
|
||||
/** @returns {ExplainedResult} */
|
||||
verify(data) {
|
||||
abstract;
|
||||
return ExplainedResult.bad();
|
||||
}
|
||||
|
||||
// Should return the default data
|
||||
getDefaultData() {
|
||||
abstract;
|
||||
return {};
|
||||
}
|
||||
|
||||
// Should return the current version as an integer
|
||||
getCurrentVersion() {
|
||||
abstract;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Should migrate the data (Modify in place)
|
||||
/** @returns {ExplainedResult} */
|
||||
migrate(data) {
|
||||
abstract;
|
||||
return ExplainedResult.bad();
|
||||
}
|
||||
|
||||
// -- / Methods
|
||||
|
||||
// Resets whole data, returns promise
|
||||
resetEverythingAsync() {
|
||||
logger.warn("Reset data to default");
|
||||
this.currentData = this.getDefaultData();
|
||||
return this.writeAsync();
|
||||
}
|
||||
|
||||
getCurrentData() {
|
||||
return this.currentData;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} obj
|
||||
*/
|
||||
static serializeObject(obj) {
|
||||
const jsonString = JSON.stringify(compressObject(obj));
|
||||
const checksum = computeCrc(jsonString + salt);
|
||||
return compressionPrefix + compressX64(checksum + jsonString);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} text
|
||||
*/
|
||||
static deserializeObject(text) {
|
||||
const decompressed = decompressX64(text.substr(compressionPrefix.length));
|
||||
if (!decompressed) {
|
||||
// LZ string decompression failure
|
||||
throw new Error("bad-content / decompression-failed");
|
||||
}
|
||||
if (decompressed.length < 40) {
|
||||
// String too short
|
||||
throw new Error("bad-content / payload-too-small");
|
||||
}
|
||||
|
||||
// Compare stored checksum with actual checksum
|
||||
const checksum = decompressed.substring(0, 40);
|
||||
const jsonString = decompressed.substr(40);
|
||||
|
||||
const desiredChecksum = checksum.startsWith(CRC_PREFIX)
|
||||
? computeCrc(jsonString + salt)
|
||||
: sha1(jsonString + salt);
|
||||
|
||||
if (desiredChecksum !== checksum) {
|
||||
// Checksum mismatch
|
||||
throw new Error("bad-content / checksum-mismatch");
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(jsonString);
|
||||
const decoded = decompressObject(parsed);
|
||||
return decoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the data asychronously, fails if verify() fails
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
writeAsync() {
|
||||
const verifyResult = this.internalVerifyEntry(this.currentData);
|
||||
|
||||
if (!verifyResult.result) {
|
||||
logger.error("Tried to write invalid data to", this.filename, "reason:", verifyResult.reason);
|
||||
return Promise.reject(verifyResult.reason);
|
||||
}
|
||||
|
||||
return asyncCompressor
|
||||
.compressObjectAsync(this.currentData)
|
||||
.then(compressed => {
|
||||
return this.app.storage.writeFileAsync(this.filename, compressed);
|
||||
})
|
||||
.then(() => {
|
||||
logger.log("📄 Wrote", this.filename);
|
||||
})
|
||||
.catch(err => {
|
||||
logger.error("Failed to write", this.filename, ":", err);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
// Reads the data asynchronously, fails if verify() fails
|
||||
readAsync() {
|
||||
// Start read request
|
||||
return (
|
||||
this.app.storage
|
||||
.readFileAsync(this.filename)
|
||||
|
||||
// Check for errors during read
|
||||
.catch(err => {
|
||||
if (err === FILE_NOT_FOUND) {
|
||||
logger.log("File not found, using default data");
|
||||
|
||||
// File not found or unreadable, assume default file
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return Promise.reject("file-error: " + err);
|
||||
})
|
||||
|
||||
// Decrypt data (if its encrypted)
|
||||
// @ts-ignore
|
||||
.then(rawData => {
|
||||
if (rawData == null) {
|
||||
// So, the file has not been found, use default data
|
||||
return JSON.stringify(compressObject(this.getDefaultData()));
|
||||
}
|
||||
|
||||
if (rawData.startsWith(compressionPrefix)) {
|
||||
const decompressed = decompressX64(rawData.substr(compressionPrefix.length));
|
||||
if (!decompressed) {
|
||||
// LZ string decompression failure
|
||||
return Promise.reject("bad-content / decompression-failed");
|
||||
}
|
||||
if (decompressed.length < 40) {
|
||||
// String too short
|
||||
return Promise.reject("bad-content / payload-too-small");
|
||||
}
|
||||
|
||||
// Compare stored checksum with actual checksum
|
||||
const checksum = decompressed.substring(0, 40);
|
||||
const jsonString = decompressed.substr(40);
|
||||
|
||||
const desiredChecksum = checksum.startsWith(CRC_PREFIX)
|
||||
? computeCrc(jsonString + salt)
|
||||
: sha1(jsonString + salt);
|
||||
|
||||
if (desiredChecksum !== checksum) {
|
||||
// Checksum mismatch
|
||||
return Promise.reject(
|
||||
"bad-content / checksum-mismatch: " + desiredChecksum + " vs " + checksum
|
||||
);
|
||||
}
|
||||
return jsonString;
|
||||
} else {
|
||||
if (!G_IS_DEV) {
|
||||
return Promise.reject("bad-content / missing-compression");
|
||||
}
|
||||
}
|
||||
return rawData;
|
||||
})
|
||||
|
||||
// Parse JSON, this could throw but thats fine
|
||||
.then(res => {
|
||||
try {
|
||||
return JSON.parse(res);
|
||||
} catch (ex) {
|
||||
logger.error(
|
||||
"Failed to parse file content of",
|
||||
this.filename,
|
||||
":",
|
||||
ex,
|
||||
"(content was:",
|
||||
res,
|
||||
")"
|
||||
);
|
||||
throw new Error("invalid-serialized-data");
|
||||
}
|
||||
})
|
||||
|
||||
// Decompress
|
||||
.then(compressed => decompressObject(compressed))
|
||||
|
||||
// Verify basic structure
|
||||
.then(contents => {
|
||||
const result = this.internalVerifyBasicStructure(contents);
|
||||
if (!result.isGood()) {
|
||||
return Promise.reject("verify-failed: " + result.reason);
|
||||
}
|
||||
return contents;
|
||||
})
|
||||
|
||||
// Check version and migrate if required
|
||||
.then(contents => {
|
||||
if (contents.version > this.getCurrentVersion()) {
|
||||
return Promise.reject("stored-data-is-newer");
|
||||
}
|
||||
|
||||
if (contents.version < this.getCurrentVersion()) {
|
||||
logger.log(
|
||||
"Trying to migrate data object from version",
|
||||
contents.version,
|
||||
"to",
|
||||
this.getCurrentVersion()
|
||||
);
|
||||
const migrationResult = this.migrate(contents); // modify in place
|
||||
if (migrationResult.isBad()) {
|
||||
return Promise.reject("migration-failed: " + migrationResult.reason);
|
||||
}
|
||||
}
|
||||
return contents;
|
||||
})
|
||||
|
||||
// Verify
|
||||
.then(contents => {
|
||||
const verifyResult = this.internalVerifyEntry(contents);
|
||||
if (!verifyResult.result) {
|
||||
logger.error(
|
||||
"Read invalid data from",
|
||||
this.filename,
|
||||
"reason:",
|
||||
verifyResult.reason,
|
||||
"contents:",
|
||||
contents
|
||||
);
|
||||
return Promise.reject("invalid-data: " + verifyResult.reason);
|
||||
}
|
||||
return contents;
|
||||
})
|
||||
|
||||
// Store
|
||||
.then(contents => {
|
||||
this.currentData = contents;
|
||||
logger.log("📄 Read data with version", this.currentData.version, "from", this.filename);
|
||||
return contents;
|
||||
})
|
||||
|
||||
// Catchall
|
||||
.catch(err => {
|
||||
return Promise.reject("Failed to read " + this.filename + ": " + err);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the file
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
deleteAsync() {
|
||||
return this.app.storage.deleteFileAsync(this.filename);
|
||||
}
|
||||
|
||||
// Internal
|
||||
|
||||
/** @returns {ExplainedResult} */
|
||||
internalVerifyBasicStructure(data) {
|
||||
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()})`
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
/* typehints:start */
|
||||
import { Application } from "../application";
|
||||
/* typehints:end */
|
||||
|
||||
import { sha1, CRC_PREFIX, computeCrc } from "./sensitive_utils.encrypt";
|
||||
import { createLogger } from "./logging";
|
||||
import { FILE_NOT_FOUND } from "../platform/storage";
|
||||
import { accessNestedPropertyReverse } from "./utils";
|
||||
import { IS_DEBUG, globalConfig } from "./config";
|
||||
import { ExplainedResult } from "./explained_result";
|
||||
import { decompressX64, compressX64 } from "./lzstring";
|
||||
import { asyncCompressor, compressionPrefix } from "./async_compression";
|
||||
import { compressObject, decompressObject } from "../savegame/savegame_compressor";
|
||||
|
||||
const debounce = require("debounce-promise");
|
||||
|
||||
const logger = createLogger("read_write_proxy");
|
||||
|
||||
const salt = accessNestedPropertyReverse(globalConfig, ["file", "info"]);
|
||||
|
||||
// Helper which only writes / reads if verify() works. Also performs migration
|
||||
export class ReadWriteProxy {
|
||||
constructor(app, filename) {
|
||||
/** @type {Application} */
|
||||
this.app = app;
|
||||
|
||||
this.filename = filename;
|
||||
|
||||
/** @type {object} */
|
||||
this.currentData = null;
|
||||
|
||||
// TODO: EXTREMELY HACKY! To verify we need to do this a step later
|
||||
if (G_IS_DEV && IS_DEBUG) {
|
||||
setTimeout(() => {
|
||||
assert(
|
||||
this.verify(this.getDefaultData()).result,
|
||||
"Verify() failed for default data: " + this.verify(this.getDefaultData()).reason
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a debounced handler to prevent double writes
|
||||
*/
|
||||
this.debouncedWrite = debounce(this.doWriteAsync.bind(this), 50);
|
||||
}
|
||||
|
||||
// -- Methods to override
|
||||
|
||||
/** @returns {ExplainedResult} */
|
||||
verify(data) {
|
||||
abstract;
|
||||
return ExplainedResult.bad();
|
||||
}
|
||||
|
||||
// Should return the default data
|
||||
getDefaultData() {
|
||||
abstract;
|
||||
return {};
|
||||
}
|
||||
|
||||
// Should return the current version as an integer
|
||||
getCurrentVersion() {
|
||||
abstract;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Should migrate the data (Modify in place)
|
||||
/** @returns {ExplainedResult} */
|
||||
migrate(data) {
|
||||
abstract;
|
||||
return ExplainedResult.bad();
|
||||
}
|
||||
|
||||
// -- / Methods
|
||||
|
||||
// Resets whole data, returns promise
|
||||
resetEverythingAsync() {
|
||||
logger.warn("Reset data to default");
|
||||
this.currentData = this.getDefaultData();
|
||||
return this.writeAsync();
|
||||
}
|
||||
|
||||
getCurrentData() {
|
||||
return this.currentData;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} obj
|
||||
*/
|
||||
static serializeObject(obj) {
|
||||
const jsonString = JSON.stringify(compressObject(obj));
|
||||
const checksum = computeCrc(jsonString + salt);
|
||||
return compressionPrefix + compressX64(checksum + jsonString);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} text
|
||||
*/
|
||||
static deserializeObject(text) {
|
||||
const decompressed = decompressX64(text.substr(compressionPrefix.length));
|
||||
if (!decompressed) {
|
||||
// LZ string decompression failure
|
||||
throw new Error("bad-content / decompression-failed");
|
||||
}
|
||||
if (decompressed.length < 40) {
|
||||
// String too short
|
||||
throw new Error("bad-content / payload-too-small");
|
||||
}
|
||||
|
||||
// Compare stored checksum with actual checksum
|
||||
const checksum = decompressed.substring(0, 40);
|
||||
const jsonString = decompressed.substr(40);
|
||||
|
||||
const desiredChecksum = checksum.startsWith(CRC_PREFIX)
|
||||
? computeCrc(jsonString + salt)
|
||||
: sha1(jsonString + salt);
|
||||
|
||||
if (desiredChecksum !== checksum) {
|
||||
// Checksum mismatch
|
||||
throw new Error("bad-content / checksum-mismatch");
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(jsonString);
|
||||
const decoded = decompressObject(parsed);
|
||||
return decoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the data asychronously, fails if verify() fails.
|
||||
* Debounces the operation by up to 50ms
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
writeAsync() {
|
||||
const verifyResult = this.internalVerifyEntry(this.currentData);
|
||||
|
||||
if (!verifyResult.result) {
|
||||
logger.error("Tried to write invalid data to", this.filename, "reason:", verifyResult.reason);
|
||||
return Promise.reject(verifyResult.reason);
|
||||
}
|
||||
|
||||
return this.debouncedWrite();
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually writes the data asychronously
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
doWriteAsync() {
|
||||
return asyncCompressor
|
||||
.compressObjectAsync(this.currentData)
|
||||
.then(compressed => {
|
||||
return this.app.storage.writeFileAsync(this.filename, compressed);
|
||||
})
|
||||
.then(() => {
|
||||
logger.log("📄 Wrote", this.filename);
|
||||
})
|
||||
.catch(err => {
|
||||
logger.error("Failed to write", this.filename, ":", err);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
// Reads the data asynchronously, fails if verify() fails
|
||||
readAsync() {
|
||||
// Start read request
|
||||
return (
|
||||
this.app.storage
|
||||
.readFileAsync(this.filename)
|
||||
|
||||
// Check for errors during read
|
||||
.catch(err => {
|
||||
if (err === FILE_NOT_FOUND) {
|
||||
logger.log("File not found, using default data");
|
||||
|
||||
// File not found or unreadable, assume default file
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return Promise.reject("file-error: " + err);
|
||||
})
|
||||
|
||||
// Decrypt data (if its encrypted)
|
||||
// @ts-ignore
|
||||
.then(rawData => {
|
||||
if (rawData == null) {
|
||||
// So, the file has not been found, use default data
|
||||
return JSON.stringify(compressObject(this.getDefaultData()));
|
||||
}
|
||||
|
||||
if (rawData.startsWith(compressionPrefix)) {
|
||||
const decompressed = decompressX64(rawData.substr(compressionPrefix.length));
|
||||
if (!decompressed) {
|
||||
// LZ string decompression failure
|
||||
return Promise.reject("bad-content / decompression-failed");
|
||||
}
|
||||
if (decompressed.length < 40) {
|
||||
// String too short
|
||||
return Promise.reject("bad-content / payload-too-small");
|
||||
}
|
||||
|
||||
// Compare stored checksum with actual checksum
|
||||
const checksum = decompressed.substring(0, 40);
|
||||
const jsonString = decompressed.substr(40);
|
||||
|
||||
const desiredChecksum = checksum.startsWith(CRC_PREFIX)
|
||||
? computeCrc(jsonString + salt)
|
||||
: sha1(jsonString + salt);
|
||||
|
||||
if (desiredChecksum !== checksum) {
|
||||
// Checksum mismatch
|
||||
return Promise.reject(
|
||||
"bad-content / checksum-mismatch: " + desiredChecksum + " vs " + checksum
|
||||
);
|
||||
}
|
||||
return jsonString;
|
||||
} else {
|
||||
if (!G_IS_DEV) {
|
||||
return Promise.reject("bad-content / missing-compression");
|
||||
}
|
||||
}
|
||||
return rawData;
|
||||
})
|
||||
|
||||
// Parse JSON, this could throw but thats fine
|
||||
.then(res => {
|
||||
try {
|
||||
return JSON.parse(res);
|
||||
} catch (ex) {
|
||||
logger.error(
|
||||
"Failed to parse file content of",
|
||||
this.filename,
|
||||
":",
|
||||
ex,
|
||||
"(content was:",
|
||||
res,
|
||||
")"
|
||||
);
|
||||
throw new Error("invalid-serialized-data");
|
||||
}
|
||||
})
|
||||
|
||||
// Decompress
|
||||
.then(compressed => decompressObject(compressed))
|
||||
|
||||
// Verify basic structure
|
||||
.then(contents => {
|
||||
const result = this.internalVerifyBasicStructure(contents);
|
||||
if (!result.isGood()) {
|
||||
return Promise.reject("verify-failed: " + result.reason);
|
||||
}
|
||||
return contents;
|
||||
})
|
||||
|
||||
// Check version and migrate if required
|
||||
.then(contents => {
|
||||
if (contents.version > this.getCurrentVersion()) {
|
||||
return Promise.reject("stored-data-is-newer");
|
||||
}
|
||||
|
||||
if (contents.version < this.getCurrentVersion()) {
|
||||
logger.log(
|
||||
"Trying to migrate data object from version",
|
||||
contents.version,
|
||||
"to",
|
||||
this.getCurrentVersion()
|
||||
);
|
||||
const migrationResult = this.migrate(contents); // modify in place
|
||||
if (migrationResult.isBad()) {
|
||||
return Promise.reject("migration-failed: " + migrationResult.reason);
|
||||
}
|
||||
}
|
||||
return contents;
|
||||
})
|
||||
|
||||
// Verify
|
||||
.then(contents => {
|
||||
const verifyResult = this.internalVerifyEntry(contents);
|
||||
if (!verifyResult.result) {
|
||||
logger.error(
|
||||
"Read invalid data from",
|
||||
this.filename,
|
||||
"reason:",
|
||||
verifyResult.reason,
|
||||
"contents:",
|
||||
contents
|
||||
);
|
||||
return Promise.reject("invalid-data: " + verifyResult.reason);
|
||||
}
|
||||
return contents;
|
||||
})
|
||||
|
||||
// Store
|
||||
.then(contents => {
|
||||
this.currentData = contents;
|
||||
logger.log("📄 Read data with version", this.currentData.version, "from", this.filename);
|
||||
return contents;
|
||||
})
|
||||
|
||||
// Catchall
|
||||
.catch(err => {
|
||||
return Promise.reject("Failed to read " + this.filename + ": " + err);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the file
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
deleteAsync() {
|
||||
return this.app.storage.deleteFileAsync(this.filename);
|
||||
}
|
||||
|
||||
// Internal
|
||||
|
||||
/** @returns {ExplainedResult} */
|
||||
internalVerifyBasicStructure(data) {
|
||||
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()})`
|
||||
);
|
||||
}
|
||||
|
||||
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 { makeDiv } from "../../../core/utils";
|
||||
import { SOUNDS } from "../../../platform/sound";
|
||||
import { enumNotificationType } from "./notifications";
|
||||
import { T } from "../../../translations";
|
||||
import { KEYMAPPINGS } from "../../key_action_mapper";
|
||||
import { DynamicDomAttach } from "../dynamic_dom_attach";
|
||||
|
||||
export class HUDGameMenu extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
this.element = makeDiv(parent, "ingame_HUD_GameMenu");
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
id: "shop",
|
||||
label: "Upgrades",
|
||||
handler: () => this.root.hud.parts.shop.show(),
|
||||
keybinding: KEYMAPPINGS.ingame.menuOpenShop,
|
||||
badge: () => this.root.hubGoals.getAvailableUpgradeCount(),
|
||||
notification: /** @type {[string, enumNotificationType]} */ ([
|
||||
T.ingame.notifications.newUpgrade,
|
||||
enumNotificationType.upgrade,
|
||||
]),
|
||||
visible: () =>
|
||||
!this.root.app.settings.getAllSettings().offerHints || this.root.hubGoals.level >= 3,
|
||||
},
|
||||
{
|
||||
id: "stats",
|
||||
label: "Stats",
|
||||
handler: () => this.root.hud.parts.statistics.show(),
|
||||
keybinding: KEYMAPPINGS.ingame.menuOpenStats,
|
||||
visible: () =>
|
||||
!this.root.app.settings.getAllSettings().offerHints || this.root.hubGoals.level >= 3,
|
||||
},
|
||||
];
|
||||
|
||||
/** @type {Array<{
|
||||
* badge: function,
|
||||
* button: HTMLElement,
|
||||
* badgeElement: HTMLElement,
|
||||
* lastRenderAmount: number,
|
||||
* condition?: function,
|
||||
* notification: [string, enumNotificationType]
|
||||
* }>} */
|
||||
this.badgesToUpdate = [];
|
||||
|
||||
/** @type {Array<{
|
||||
* button: HTMLElement,
|
||||
* condition: function,
|
||||
* domAttach: DynamicDomAttach
|
||||
* }>} */
|
||||
this.visibilityToUpdate = [];
|
||||
|
||||
this.buttonsElement = makeDiv(this.element, null, ["buttonContainer"]);
|
||||
|
||||
buttons.forEach(({ id, label, handler, keybinding, badge, notification, visible }) => {
|
||||
const button = document.createElement("button");
|
||||
button.setAttribute("data-button-id", id);
|
||||
this.buttonsElement.appendChild(button);
|
||||
this.trackClicks(button, handler);
|
||||
|
||||
if (keybinding) {
|
||||
const binding = this.root.keyMapper.getBinding(keybinding);
|
||||
binding.add(handler);
|
||||
binding.appendLabelToElement(button);
|
||||
}
|
||||
|
||||
if (visible) {
|
||||
this.visibilityToUpdate.push({
|
||||
button,
|
||||
condition: visible,
|
||||
domAttach: new DynamicDomAttach(this.root, button),
|
||||
});
|
||||
}
|
||||
|
||||
if (badge) {
|
||||
const badgeElement = makeDiv(button, null, ["badge"]);
|
||||
this.badgesToUpdate.push({
|
||||
badge,
|
||||
lastRenderAmount: 0,
|
||||
button,
|
||||
badgeElement,
|
||||
notification,
|
||||
condition: visible,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const menuButtons = makeDiv(this.element, null, ["menuButtons"]);
|
||||
|
||||
this.musicButton = makeDiv(menuButtons, null, ["button", "music"]);
|
||||
this.sfxButton = makeDiv(menuButtons, null, ["button", "sfx"]);
|
||||
this.saveButton = makeDiv(menuButtons, null, ["button", "save", "animEven"]);
|
||||
this.settingsButton = makeDiv(menuButtons, null, ["button", "settings"]);
|
||||
|
||||
this.trackClicks(this.musicButton, this.toggleMusic);
|
||||
this.trackClicks(this.sfxButton, this.toggleSfx);
|
||||
this.trackClicks(this.saveButton, this.startSave);
|
||||
this.trackClicks(this.settingsButton, this.openSettings);
|
||||
|
||||
this.musicButton.classList.toggle("muted", this.root.app.settings.getAllSettings().musicMuted);
|
||||
this.sfxButton.classList.toggle("muted", this.root.app.settings.getAllSettings().soundsMuted);
|
||||
}
|
||||
initialize() {
|
||||
this.root.signals.gameSaved.add(this.onGameSaved, this);
|
||||
}
|
||||
|
||||
update() {
|
||||
let playSound = false;
|
||||
let notifications = new Set();
|
||||
|
||||
// Update visibility of buttons
|
||||
for (let i = 0; i < this.visibilityToUpdate.length; ++i) {
|
||||
const { button, condition, domAttach } = this.visibilityToUpdate[i];
|
||||
domAttach.update(condition());
|
||||
}
|
||||
|
||||
// Check for notifications and badges
|
||||
for (let i = 0; i < this.badgesToUpdate.length; ++i) {
|
||||
const {
|
||||
badge,
|
||||
button,
|
||||
badgeElement,
|
||||
lastRenderAmount,
|
||||
notification,
|
||||
condition,
|
||||
} = this.badgesToUpdate[i];
|
||||
|
||||
if (condition && !condition()) {
|
||||
// Do not show notifications for invisible buttons
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the amount shown differs from the one shown last frame
|
||||
const amount = badge();
|
||||
if (lastRenderAmount !== amount) {
|
||||
if (amount > 0) {
|
||||
badgeElement.innerText = amount;
|
||||
}
|
||||
// Check if the badge increased, if so play a notification
|
||||
if (amount > lastRenderAmount) {
|
||||
playSound = true;
|
||||
if (notification) {
|
||||
notifications.add(notification);
|
||||
}
|
||||
}
|
||||
|
||||
// Rerender notifications
|
||||
this.badgesToUpdate[i].lastRenderAmount = amount;
|
||||
button.classList.toggle("hasBadge", amount > 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (playSound) {
|
||||
this.root.soundProxy.playUi(SOUNDS.badgeNotification);
|
||||
}
|
||||
|
||||
notifications.forEach(([notification, type]) => {
|
||||
this.root.hud.signals.notification.dispatch(notification, type);
|
||||
});
|
||||
}
|
||||
|
||||
onGameSaved() {
|
||||
this.saveButton.classList.toggle("animEven");
|
||||
this.saveButton.classList.toggle("animOdd");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { SOUNDS } from "../../../platform/sound";
|
||||
import { enumNotificationType } from "./notifications";
|
||||
import { T } from "../../../translations";
|
||||
import { KEYMAPPINGS } from "../../key_action_mapper";
|
||||
import { DynamicDomAttach } from "../dynamic_dom_attach";
|
||||
|
||||
export class HUDGameMenu extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
this.element = makeDiv(parent, "ingame_HUD_GameMenu");
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
id: "shop",
|
||||
label: "Upgrades",
|
||||
handler: () => this.root.hud.parts.shop.show(),
|
||||
keybinding: KEYMAPPINGS.ingame.menuOpenShop,
|
||||
badge: () => this.root.hubGoals.getAvailableUpgradeCount(),
|
||||
notification: /** @type {[string, enumNotificationType]} */ ([
|
||||
T.ingame.notifications.newUpgrade,
|
||||
enumNotificationType.upgrade,
|
||||
]),
|
||||
visible: () =>
|
||||
!this.root.app.settings.getAllSettings().offerHints || this.root.hubGoals.level >= 3,
|
||||
},
|
||||
{
|
||||
id: "stats",
|
||||
label: "Stats",
|
||||
handler: () => this.root.hud.parts.statistics.show(),
|
||||
keybinding: KEYMAPPINGS.ingame.menuOpenStats,
|
||||
visible: () =>
|
||||
!this.root.app.settings.getAllSettings().offerHints || this.root.hubGoals.level >= 3,
|
||||
},
|
||||
];
|
||||
|
||||
/** @type {Array<{
|
||||
* badge: function,
|
||||
* button: HTMLElement,
|
||||
* badgeElement: HTMLElement,
|
||||
* lastRenderAmount: number,
|
||||
* condition?: function,
|
||||
* notification: [string, enumNotificationType]
|
||||
* }>} */
|
||||
this.badgesToUpdate = [];
|
||||
|
||||
/** @type {Array<{
|
||||
* button: HTMLElement,
|
||||
* condition: function,
|
||||
* domAttach: DynamicDomAttach
|
||||
* }>} */
|
||||
this.visibilityToUpdate = [];
|
||||
|
||||
this.buttonsElement = makeDiv(this.element, null, ["buttonContainer"]);
|
||||
|
||||
buttons.forEach(({ id, label, handler, keybinding, badge, notification, visible }) => {
|
||||
const button = document.createElement("button");
|
||||
button.setAttribute("data-button-id", id);
|
||||
this.buttonsElement.appendChild(button);
|
||||
this.trackClicks(button, handler);
|
||||
|
||||
if (keybinding) {
|
||||
const binding = this.root.keyMapper.getBinding(keybinding);
|
||||
binding.add(handler);
|
||||
binding.appendLabelToElement(button);
|
||||
}
|
||||
|
||||
if (visible) {
|
||||
this.visibilityToUpdate.push({
|
||||
button,
|
||||
condition: visible,
|
||||
domAttach: new DynamicDomAttach(this.root, button),
|
||||
});
|
||||
}
|
||||
|
||||
if (badge) {
|
||||
const badgeElement = makeDiv(button, null, ["badge"]);
|
||||
this.badgesToUpdate.push({
|
||||
badge,
|
||||
lastRenderAmount: 0,
|
||||
button,
|
||||
badgeElement,
|
||||
notification,
|
||||
condition: visible,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const menuButtons = makeDiv(this.element, null, ["menuButtons"]);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.root.signals.gameSaved.add(this.onGameSaved, this);
|
||||
}
|
||||
|
||||
update() {
|
||||
let playSound = false;
|
||||
let notifications = new Set();
|
||||
|
||||
// Update visibility of buttons
|
||||
for (let i = 0; i < this.visibilityToUpdate.length; ++i) {
|
||||
const { condition, domAttach } = this.visibilityToUpdate[i];
|
||||
domAttach.update(condition());
|
||||
}
|
||||
|
||||
// Check for notifications and badges
|
||||
for (let i = 0; i < this.badgesToUpdate.length; ++i) {
|
||||
const {
|
||||
badge,
|
||||
button,
|
||||
badgeElement,
|
||||
lastRenderAmount,
|
||||
notification,
|
||||
condition,
|
||||
} = this.badgesToUpdate[i];
|
||||
|
||||
if (condition && !condition()) {
|
||||
// Do not show notifications for invisible buttons
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the amount shown differs from the one shown last frame
|
||||
const amount = badge();
|
||||
if (lastRenderAmount !== amount) {
|
||||
if (amount > 0) {
|
||||
badgeElement.innerText = amount;
|
||||
}
|
||||
// Check if the badge increased, if so play a notification
|
||||
if (amount > lastRenderAmount) {
|
||||
playSound = true;
|
||||
if (notification) {
|
||||
notifications.add(notification);
|
||||
}
|
||||
}
|
||||
|
||||
// Rerender notifications
|
||||
this.badgesToUpdate[i].lastRenderAmount = amount;
|
||||
button.classList.toggle("hasBadge", amount > 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (playSound) {
|
||||
this.root.soundProxy.playUi(SOUNDS.badgeNotification);
|
||||
}
|
||||
|
||||
notifications.forEach(([notification, type]) => {
|
||||
this.root.hud.signals.notification.dispatch(notification, type);
|
||||
});
|
||||
}
|
||||
|
||||
onGameSaved() {
|
||||
this.saveButton.classList.toggle("animEven");
|
||||
this.saveButton.classList.toggle("animOdd");
|
||||
}
|
||||
|
||||
startSave() {
|
||||
this.root.gameState.doSave();
|
||||
}
|
||||
|
||||
openSettings() {
|
||||
this.root.hud.parts.settingsMenu.show();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -185,6 +185,9 @@ export class SoundImplBrowser extends SoundInterface {
|
|||
}
|
||||
|
||||
initialize() {
|
||||
// NOTICE: We override the initialize() method here with custom logic because
|
||||
// we have a sound sprites instance
|
||||
|
||||
this.sfxHandle = new SoundSpritesContainer();
|
||||
|
||||
// @ts-ignore
|
||||
|
@ -198,11 +201,11 @@ export class SoundImplBrowser extends SoundInterface {
|
|||
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.soundVolume = this.app.settings.getAllSettings().soundVolume;
|
||||
|
||||
if (G_IS_DEV && globalConfig.debug.disableMusic) {
|
||||
this.musicMuted = true;
|
||||
this.musicVolume = 0.0;
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
|
|
|
@ -103,9 +103,6 @@ export class SoundInterface {
|
|||
|
||||
this.pageIsVisible = true;
|
||||
|
||||
this.musicMuted = false;
|
||||
this.soundsMuted = false;
|
||||
|
||||
this.musicVolume = 1.0;
|
||||
this.soundVolume = 1.0;
|
||||
}
|
||||
|
@ -127,13 +124,11 @@ export class SoundInterface {
|
|||
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.soundVolume = this.app.settings.getAllSettings().soundVolume;
|
||||
|
||||
if (G_IS_DEV && globalConfig.debug.disableMusic) {
|
||||
this.musicMuted = true;
|
||||
this.musicVolume = 0.0;
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
|
@ -170,47 +165,6 @@ export class SoundInterface {
|
|||
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 {number}
|
||||
|
@ -254,7 +208,7 @@ export class SoundInterface {
|
|||
this.pageIsVisible = pageIsVisible;
|
||||
if (this.currentMusic) {
|
||||
if (pageIsVisible) {
|
||||
if (!this.currentMusic.isPlaying() && !this.musicMuted) {
|
||||
if (!this.currentMusic.isPlaying()) {
|
||||
this.currentMusic.play(this.musicVolume);
|
||||
}
|
||||
} else {
|
||||
|
@ -267,9 +221,6 @@ export class SoundInterface {
|
|||
* @param {string} key
|
||||
*/
|
||||
playUiSound(key) {
|
||||
if (this.soundsMuted) {
|
||||
return;
|
||||
}
|
||||
if (!this.sounds[key]) {
|
||||
logger.warn("Sound", key, "not found, probably not loaded yet");
|
||||
return;
|
||||
|
@ -288,7 +239,7 @@ export class SoundInterface {
|
|||
logger.warn("Music", key, "not found, probably not loaded yet");
|
||||
return;
|
||||
}
|
||||
if (!this.pageIsVisible || this.soundsMuted) {
|
||||
if (!this.pageIsVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -319,7 +270,7 @@ export class SoundInterface {
|
|||
this.currentMusic.stop();
|
||||
}
|
||||
this.currentMusic = music;
|
||||
if (music && this.pageIsVisible && !this.musicMuted) {
|
||||
if (music && this.pageIsVisible) {
|
||||
logger.log("Starting", this.currentMusic.key);
|
||||
music.play(this.musicVolume);
|
||||
}
|
||||
|
|
|
@ -159,29 +159,13 @@ export const allApplicationSettings = [
|
|||
(app, id) => app.updateAfterUiScaleChanged(),
|
||||
}),
|
||||
|
||||
new BoolSetting(
|
||||
"soundsMuted",
|
||||
enumCategories.general,
|
||||
/**
|
||||
* @param {Application} app
|
||||
*/
|
||||
(app, value) => app.sound.setSoundsMuted(value)
|
||||
),
|
||||
new RangeSetting(
|
||||
"soundVolume",
|
||||
enumCategories.general,
|
||||
/**
|
||||
* @param {Application} app
|
||||
*/
|
||||
(app, value) => app.sound.setSoundVolume(value / 100.0)
|
||||
),
|
||||
new BoolSetting(
|
||||
"musicMuted",
|
||||
enumCategories.general,
|
||||
/**
|
||||
* @param {Application} app
|
||||
*/
|
||||
(app, value) => app.sound.setMusicMuted(value)
|
||||
(app, value) => app.sound.setSoundVolume(value)
|
||||
),
|
||||
new RangeSetting(
|
||||
"musicVolume",
|
||||
|
@ -189,7 +173,7 @@ export const allApplicationSettings = [
|
|||
/**
|
||||
* @param {Application} app
|
||||
*/
|
||||
(app, value) => app.sound.setMusicVolume(value / 100.0)
|
||||
(app, value) => app.sound.setMusicVolume(value)
|
||||
),
|
||||
|
||||
new BoolSetting(
|
||||
|
@ -302,8 +286,6 @@ class SettingsStorage {
|
|||
this.uiScale = "regular";
|
||||
this.fullscreen = G_IS_STANDALONE;
|
||||
|
||||
this.soundsMuted = false;
|
||||
this.musicMuted = false;
|
||||
this.soundVolume = 1.0;
|
||||
this.musicVolume = 1.0;
|
||||
|
||||
|
@ -515,7 +497,17 @@ export class ApplicationSettings extends ReadWriteProxy {
|
|||
const setting = allApplicationSettings[i];
|
||||
const storedValue = settings[setting.id];
|
||||
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();
|
||||
|
@ -529,7 +521,7 @@ export class ApplicationSettings extends ReadWriteProxy {
|
|||
}
|
||||
|
||||
getCurrentVersion() {
|
||||
return 24;
|
||||
return 25;
|
||||
}
|
||||
|
||||
/** @param {{settings: SettingsStorage, version: number}} data */
|
||||
|
@ -633,12 +625,21 @@ export class ApplicationSettings extends ReadWriteProxy {
|
|||
}
|
||||
|
||||
if (data.version < 24) {
|
||||
data.settings.musicVolume = 1.0;
|
||||
data.settings.soundVolume = 1.0;
|
||||
data.settings.refreshRate = "60";
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -227,10 +227,10 @@ export class RangeSetting extends BaseSetting {
|
|||
category,
|
||||
changeCb = null,
|
||||
enabled = true,
|
||||
defaultValue = 100,
|
||||
defaultValue = 1.0,
|
||||
minValue = 0,
|
||||
maxValue = 100,
|
||||
stepSize = 1
|
||||
maxValue = 1.0,
|
||||
stepSize = 0.0001
|
||||
) {
|
||||
super(id, category, changeCb, enabled);
|
||||
|
||||
|
@ -247,9 +247,9 @@ export class RangeSetting extends BaseSetting {
|
|||
|
||||
<div class="row">
|
||||
<label>${T.settings.labels[this.id].title}</label>
|
||||
<div class="value range" data-setting="${this.id}">
|
||||
<label class="range-label">${this.defaultValue}</label>
|
||||
<input class="range-input" type="range" value="${this.defaultValue}" min="${
|
||||
<div class="value rangeInputContainer noPressEffect" data-setting="${this.id}">
|
||||
<label>${this.defaultValue}</label>
|
||||
<input class="rangeInput" type="range" value="${this.defaultValue}" min="${
|
||||
this.minValue
|
||||
}" max="${this.maxValue}" step="${this.stepSize}">
|
||||
</div>
|
||||
|
@ -265,33 +265,58 @@ export class RangeSetting extends BaseSetting {
|
|||
this.element = element;
|
||||
this.dialogs = dialogs;
|
||||
|
||||
this.element.querySelector(".range-input").addEventListener("input", () => {
|
||||
this.getRangeInputElement().addEventListener("input", () => {
|
||||
this.updateLabels();
|
||||
});
|
||||
|
||||
this.getRangeInputElement().addEventListener("change", () => {
|
||||
this.modify();
|
||||
});
|
||||
}
|
||||
|
||||
syncValueToElement() {
|
||||
const value = this.app.settings.getSetting(this.id);
|
||||
/** @type {HTMLInputElement} */
|
||||
const rangeInput = this.element.querySelector(".range-input"),
|
||||
rangeLabel = this.element.querySelector(".range-label");
|
||||
rangeInput.value = value;
|
||||
rangeLabel.innerHTML = value;
|
||||
this.setElementValue(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() {
|
||||
/** @type {HTMLInputElement} */
|
||||
const rangeInput = this.element.querySelector(".range-input");
|
||||
const newValue = Number(rangeInput.value);
|
||||
const rangeInput = this.getRangeInputElement();
|
||||
const newValue = Math.round(Number(rangeInput.value) * 100.0) / 100.0;
|
||||
this.app.settings.updateSetting(this.id, newValue);
|
||||
this.syncValueToElement();
|
||||
|
||||
console.log("SET", newValue);
|
||||
if (this.changeCb) {
|
||||
this.changeCb(this.app, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
validate(value) {
|
||||
return typeof value === "number";
|
||||
return typeof value === "number" && value >= this.minValue && value <= this.maxValue;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -728,6 +728,8 @@ settings:
|
|||
prod: Production
|
||||
buildDate: Built <at-date>
|
||||
|
||||
rangeSliderPercentage: <amount> %
|
||||
|
||||
labels:
|
||||
uiScale:
|
||||
title: Interface scale
|
||||
|
|
Loading…
Reference in New Issue