Add sound effects, background music and a sound menu
Generate retro sound effects and music in the browser with the Web Audio API, so no audio files are needed. Any sound can still be replaced by a file. Audio (src/audio/): - 16 effects (move, rotate, hold, hard drop, lock, line clears, garbage, items, countdown, knockout, win/lose), triggered by comparing match snapshots, so the server is unchanged - Background music as a shuffled playlist of any number of tracks, either synth songs or audio files; 3 original songs included, tempo rises with the level - SoundPlayer interface, so the audio engine can be replaced - Audio files and music tracks are registered in theme/assets.ts (soundFiles, musicTracks) - All volume levels for developers are in config/sound.ts (master, sfx, music, per effect, per track, default slider positions) UI: - Speaker button fixed in the top right corner of every screen; opens an inline strip with a mute button and volume slider for SFX and music that doesn't cover the boards or pause the game - Leave button below it in the lobby and the game; in a running match it asks for a second click because leaving gives up the match - Lobby Music panel to switch single tracks on or off - Keys: M all sound on/off, B music on/off, N next track - Settings are saved in localStorage Docs: audio section in frontend.md, sound and music guide in extending.md, new keys in game-rules.md and the README.
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCurrentTrack, useSound } from '../audio/SoundContext';
|
||||
import { useGameSounds } from '../audio/useGameSounds';
|
||||
import { BoardCanvas } from '../components/BoardCanvas';
|
||||
import { ItemBar } from '../components/ItemBar';
|
||||
import { ItemFlashOverlay } from '../components/ItemFlashOverlay';
|
||||
import { Logo } from '../components/Logo';
|
||||
import { MiniBoard } from '../components/MiniBoard';
|
||||
import { PiecePreview } from '../components/PiecePreview';
|
||||
import { CornerControls } from '../components/CornerControls';
|
||||
import { useKeyboardInput } from '../input/useKeyboardInput';
|
||||
import type { Lobby, MatchState } from '../net/types';
|
||||
import type { InputAction, Lobby, MatchState } from '../net/types';
|
||||
import type { GameActions, ItemFlash } from '../net/useGameConnection';
|
||||
import { itemVisual } from '../theme/assets';
|
||||
import styles from './GamePage.module.css';
|
||||
@@ -56,9 +59,22 @@ export function GamePage({ match, lobby, playerId, flashes, actions }: Props) {
|
||||
[me, opponents, actions],
|
||||
);
|
||||
|
||||
const { player } = useSound();
|
||||
const track = useCurrentTrack();
|
||||
useGameSounds(match, playerId);
|
||||
|
||||
// A hard drop can't be told apart from a normal lock in the snapshots, so it sounds on input.
|
||||
const onAction = useCallback(
|
||||
(action: InputAction) => {
|
||||
if (action === 'HardDrop') player.play('hardDrop');
|
||||
actions.sendInput(action);
|
||||
},
|
||||
[actions, player],
|
||||
);
|
||||
|
||||
useKeyboardInput({
|
||||
enabled: !!me?.alive && match.phase !== 'finished',
|
||||
onAction: actions.sendInput,
|
||||
onAction,
|
||||
onTarget,
|
||||
});
|
||||
|
||||
@@ -70,8 +86,15 @@ export function GamePage({ match, lobby, playerId, flashes, actions }: Props) {
|
||||
<main className={styles.page}>
|
||||
<header className={styles.header}>
|
||||
<Logo small />
|
||||
<span className={styles.lobbyCode}>LOBBY {lobby.code}</span>
|
||||
<div className={styles.headerInfo}>
|
||||
{track && <span className={styles.track}>MUSIC: {track.title} (N)</span>}
|
||||
<span className={styles.lobbyCode}>LOBBY {lobby.code}</span>
|
||||
</div>
|
||||
</header>
|
||||
<CornerControls
|
||||
onLeave={() => void actions.leaveLobby()}
|
||||
leaveConfirm={!!me?.alive && match.phase !== 'finished'}
|
||||
/>
|
||||
|
||||
<div className={styles.arena}>
|
||||
{me ? (
|
||||
|
||||
Reference in New Issue
Block a user