Properly implement sound and music volumes, debounce writes

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

@ -36,6 +36,7 @@
"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",
"debounce-promise": "^3.1.2",
"email-validator": "^2.0.4", "email-validator": "^2.0.4",
"eslint": "7.1.0", "eslint": "7.1.0",
"fastdom": "^1.0.8", "fastdom": "^1.0.8",

@ -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);
}
} }
} }

@ -34,19 +34,6 @@
&:hover { &:hover {
opacity: 0.8; 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 { &.save {
background-image: uiResource("icons/save.png"); background-image: uiResource("icons/save.png");

@ -22,11 +22,6 @@
@include S(border-radius, $globalBorderRadius); @include S(border-radius, $globalBorderRadius);
@include S(margin-bottom, 5px); @include S(margin-bottom, 5px);
label {
text-transform: uppercase;
@include Text;
}
.desc { .desc {
@include S(margin-top, 5px); @include S(margin-top, 5px);
@include SuperSmallText; @include SuperSmallText;
@ -37,6 +32,11 @@
display: grid; display: grid;
align-items: center; align-items: center;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
> label {
text-transform: uppercase;
@include Text;
}
} }
&.disabled { &.disabled {

@ -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)",

@ -12,6 +12,8 @@ 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 debounce = require("debounce-promise");
const logger = createLogger("read_write_proxy"); const logger = createLogger("read_write_proxy");
const salt = accessNestedPropertyReverse(globalConfig, ["file", "info"]); const salt = accessNestedPropertyReverse(globalConfig, ["file", "info"]);
@ -36,6 +38,11 @@ export class ReadWriteProxy {
); );
}); });
} }
/**
* Store a debounced handler to prevent double writes
*/
this.debouncedWrite = debounce(this.doWriteAsync.bind(this), 50);
} }
// -- Methods to override // -- Methods to override
@ -122,7 +129,8 @@ export class ReadWriteProxy {
} }
/** /**
* Writes the data asychronously, fails if verify() fails * Writes the data asychronously, fails if verify() fails.
* Debounces the operation by up to 50ms
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
writeAsync() { writeAsync() {
@ -133,6 +141,14 @@ export class ReadWriteProxy {
return Promise.reject(verifyResult.reason); return Promise.reject(verifyResult.reason);
} }
return this.debouncedWrite();
}
/**
* Actually writes the data asychronously
* @returns {Promise<void>}
*/
doWriteAsync() {
return asyncCompressor return asyncCompressor
.compressObjectAsync(this.currentData) .compressObjectAsync(this.currentData)
.then(compressed => { .then(compressed => {

@ -88,19 +88,13 @@ export class HUDGameMenu extends BaseHUDPart {
const menuButtons = makeDiv(this.element, null, ["menuButtons"]); 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.saveButton = makeDiv(menuButtons, null, ["button", "save", "animEven"]);
this.settingsButton = makeDiv(menuButtons, null, ["button", "settings"]); 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.saveButton, this.startSave);
this.trackClicks(this.settingsButton, this.openSettings); 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() { initialize() {
this.root.signals.gameSaved.add(this.onGameSaved, this); this.root.signals.gameSaved.add(this.onGameSaved, this);
} }
@ -111,7 +105,7 @@ export class HUDGameMenu extends BaseHUDPart {
// Update visibility of buttons // Update visibility of buttons
for (let i = 0; i < this.visibilityToUpdate.length; ++i) { for (let i = 0; i < this.visibilityToUpdate.length; ++i) {
const { button, condition, domAttach } = this.visibilityToUpdate[i]; const { condition, domAttach } = this.visibilityToUpdate[i];
domAttach.update(condition()); domAttach.update(condition());
} }
@ -172,17 +166,4 @@ export class HUDGameMenu extends BaseHUDPart {
openSettings() { openSettings() {
this.root.hud.parts.settingsMenu.show(); 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

@ -2740,6 +2740,11 @@ cyclist@^1.0.1:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
debounce-promise@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/debounce-promise/-/debounce-promise-3.1.2.tgz#320fb8c7d15a344455cd33cee5ab63530b6dc7c5"
integrity sha512-rZHcgBkbYavBeD9ej6sP56XfG53d51CD4dnaw989YX/nZ/ZJfgRx/9ePKmTNiUiyQvh4mtrMoS3OAWW+yoYtpg==
debug@2.6.9, debug@^2.2.0, debug@^2.3.3: debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
version "2.6.9" version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"

Loading…
Cancel
Save