diff --git a/assets/js/Common/VolumeControl.js b/assets/js/Common/VolumeControl.js new file mode 100644 index 00000000..a9947604 --- /dev/null +++ b/assets/js/Common/VolumeControl.js @@ -0,0 +1,87 @@ +import {html} from 'uhtml/node.js' +import * as scriptune from 'https://cdn.jsdelivr.net/gh/marein/js-scriptune@main/src/scriptune.js' + +customElements.define('volume-control', class extends HTMLElement { + connectedCallback() { + this.append(html` + + + + `); + + this._storageKey = this.getAttribute('storage-key') || 'volume-control'; + this._volume = this._volume; + this._abortController = new AbortController(); + + this._control.addEventListener('input', this._onVolumeChange.bind(this)); + } + + /** + * @param {Number} value + */ + set _volume(value) { + localStorage.setItem(this._storageKey, value); + this._control.value = value; + this._updateElements(); + scriptune.setMasterVolume(value); + } + + get _volume() { + return Math.max(0, Math.min(parseFloat(localStorage.getItem(this._storageKey) || 1), 1)); + } + + _updateElements() { + this._output.innerText = `${this._volume * 100}%`; + this._iconHighVolume.classList.toggle('d-none', this._volume <= 0.6); + this._iconLowVolume.classList.toggle('d-none', this._volume === 0 || this._volume > 0.6); + this._iconMute.classList.toggle('d-none', this._volume !== 0); + } + + _onVolumeChange(event) { + this._volume = parseFloat(event.target.value); + + this._abortController.abort(); + this._abortController = new AbortController(); + scriptune.play('C4:s', {signal: this._abortController.signal}); + } +}); diff --git a/assets/js/ConnectFour/Game.js b/assets/js/ConnectFour/Game.js index 912085f0..e0fca582 100644 --- a/assets/js/ConnectFour/Game.js +++ b/assets/js/ConnectFour/Game.js @@ -3,6 +3,21 @@ import {Game as GameModel} from './Model/Game.js' import {html} from 'uhtml/node.js' import * as sse from '../Common/EventSource.js' +function play(sheet) { + import('https://cdn.jsdelivr.net/gh/marein/js-scriptune@main/src/scriptune.js') + .then(m => m.play(sheet)); +} + +const sounds = { + error: () => play(`-:s F2:s C2:e`), + move: () => play(`#BPM 300\nC4:s C5:s`), + next: () => sounds.move(), + previous: () => play(`#BPM 300\nC5:s C4:s`), + win: () => play(`-:s C4:s E4:s G4:s C5:e G4:s C5:e`), + loss: () => play(`#BPM 180\n-:s C4:s E4:s G4:s C5:e -:s C5:s -:s C5:s -:e C1:h`), + join: () => play(`C4:s E4:s G4:s C5:e`) +}; + customElements.define('connect-four-game', class extends HTMLElement { connectedCallback() { this._sseAbortController = new AbortController(); @@ -155,10 +170,13 @@ customElements.define('connect-four-game', class extends HTMLElement { const field = this._lastFieldInColumn(event.target.closest('[data-column]').dataset.column); if (!field) { + sounds.error(); this._isMoveInProgress = false; return; } + sounds.move(); + const eventOptions = { bubbles: true, detail: { @@ -181,6 +199,7 @@ customElements.define('connect-four-game', class extends HTMLElement { .catch(() => { if (!this._game.hasPendingMove(eventOptions.detail)) return; + sounds.error(); this.dispatchEvent(new CustomEvent('ConnectFour.PlayerMovedFailed', eventOptions)); }) .finally(() => this._isMoveInProgress = false); @@ -202,6 +221,7 @@ customElements.define('connect-four-game', class extends HTMLElement { _onPlayerJoined = event => { if (event.detail.gameId !== this._game.gameId) return; + sounds.join(); this._game.redPlayerId = event.detail.redPlayerId; this._game.yellowPlayerId = event.detail.yellowPlayerId; this._changeCurrentPlayer(event.detail.redPlayerId); @@ -212,6 +232,7 @@ customElements.define('connect-four-game', class extends HTMLElement { this._changeCurrentPlayer(event.detail.nextPlayerId); if (this._game.hasPendingMove(event.detail)) this._removePendingToken(); if (this._game.hasMove(event.detail)) return; + if (event.detail.playerId !== this._playerId) sounds.move(); if (!event.detail.pending) this._isMoveInProgress = false; if (!this._followMovesButton || this._followMovesButton.disabled === true) this._numberOfCurrentMoveInView++; @@ -231,20 +252,37 @@ customElements.define('connect-four-game', class extends HTMLElement { _onGameWon = event => { if (event.detail.gameId !== this._game.gameId) return; + if (event.detail.loserId === this._playerId) sounds.loss(); + if (event.detail.winnerId === this._playerId) sounds.win(); this._game.winningSequences = event.detail.winningSequences; this._showWinningSequences(); this._changeCurrentPlayer(''); this._forceFollowMovesAnimation = this._numberOfCurrentMoveInView !== this._game.numberOfMoves(); } - _onGameFinished = event => { + _onGameDrawn = event => { + if (event.detail.gameId !== this._game.gameId) return; + sounds.win(); + this._changeCurrentPlayer(''); + } + + _onGameResigned = event => { + if (event.detail.gameId !== this._game.gameId) return; + if (event.detail.resignedPlayerId === this._playerId) sounds.loss(); + if (event.detail.opponentPlayerId === this._playerId) sounds.win(); + this._changeCurrentPlayer(''); + } + + _onGameAborted = event => { if (event.detail.gameId !== this._game.gameId) return; + sounds.error(); this._changeCurrentPlayer(''); } _onPreviousMoveClick(event) { event.preventDefault(); + sounds.previous(); this._numberOfCurrentMoveInView--; this._showMovesUpTo(this._numberOfCurrentMoveInView); } @@ -252,6 +290,7 @@ customElements.define('connect-four-game', class extends HTMLElement { _onNextMoveClick(event) { event.preventDefault(); + sounds.next(); this._numberOfCurrentMoveInView++; this._showMovesUpTo(this._numberOfCurrentMoveInView); } @@ -259,6 +298,7 @@ customElements.define('connect-four-game', class extends HTMLElement { _onFollowMovesClick(event) { event.preventDefault(); + sounds.next(); this._numberOfCurrentMoveInView = this._game.numberOfMoves(); this._showMovesUpTo(this._numberOfCurrentMoveInView); } @@ -283,9 +323,9 @@ customElements.define('connect-four-game', class extends HTMLElement { 'ConnectFour.PlayerJoined': this._onPlayerJoined, 'ConnectFour.PlayerMoved': this._onPlayerMoved, 'ConnectFour.GameWon': this._onGameWon, - 'ConnectFour.GameDrawn': this._onGameFinished, - 'ConnectFour.GameAborted': this._onGameFinished, - 'ConnectFour.GameResigned': this._onGameFinished + 'ConnectFour.GameDrawn': this._onGameDrawn, + 'ConnectFour.GameAborted': this._onGameAborted, + 'ConnectFour.GameResigned': this._onGameResigned }, this._sseAbortController.signal); } }); diff --git a/config/importmap.php b/config/importmap.php index e421873e..8a004b1b 100644 --- a/config/importmap.php +++ b/config/importmap.php @@ -18,6 +18,7 @@ $map = [ 'app' => ['path' => 'js/app.js', 'entrypoint' => true], 'notification-list' => ['path' => 'js/Common/NotificationList.js'], + 'volume-control' => ['path' => 'js/Common/VolumeControl.js'], 'event-source-status' => ['path' => 'js/Common/EventSourceStatus.js'], 'confirmation-button' => ['path' => 'js/Common/ConfirmationButton.js'], 'uhtml/node.js' => ['version' => '4.7.0'], diff --git a/templates/layout/condensed.html.twig b/templates/layout/condensed.html.twig index e80430cc..abc24ae6 100644 --- a/templates/layout/condensed.html.twig +++ b/templates/layout/condensed.html.twig @@ -14,6 +14,7 @@