aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2024-08-21 02:46:06 +0900
committernsfisis <nsfisis@gmail.com>2024-08-21 02:46:06 +0900
commit5dba0da3efae63cab5313582a17f20dbb41c6450 (patch)
treef4227c503ce82ffeba84603f872f7237fbee8ea7
parent30e30c1d7db50f8146226c65b4eb8ee0f3d41a34 (diff)
downloadphperkaigi-2025-albatross-5dba0da3efae63cab5313582a17f20dbb41c6450.tar.gz
phperkaigi-2025-albatross-5dba0da3efae63cab5313582a17f20dbb41c6450.tar.zst
phperkaigi-2025-albatross-5dba0da3efae63cab5313582a17f20dbb41c6450.zip
feat(frontend): partially implement sound effect
-rw-r--r--frontend/app/.client/audio/AudioController.ts94
-rw-r--r--frontend/app/.client/audio/SoundEffect.ts47
-rw-r--r--frontend/app/components/GolfWatchApp.client.tsx16
-rw-r--r--frontend/app/components/GolfWatchAppWithAudioPlayRequest.client.tsx36
-rw-r--r--frontend/app/routes/golf.$gameId.watch.tsx6
5 files changed, 192 insertions, 7 deletions
diff --git a/frontend/app/.client/audio/AudioController.ts b/frontend/app/.client/audio/AudioController.ts
new file mode 100644
index 0000000..6ed6180
--- /dev/null
+++ b/frontend/app/.client/audio/AudioController.ts
@@ -0,0 +1,94 @@
+import { SoundEffect, getFileUrl } from "./SoundEffect";
+
+export class AudioController {
+ audioElements: Record<SoundEffect, HTMLAudioElement | null>;
+
+ constructor() {
+ this.audioElements = {
+ finish: null,
+ winner_1: null,
+ winner_2: null,
+ good_1: null,
+ good_2: null,
+ good_3: null,
+ good_4: null,
+ new_score_1: null,
+ new_score_2: null,
+ new_score_3: null,
+ compile_error_1: null,
+ compile_error_2: null,
+ };
+ }
+
+ loadAll(): Promise<void> {
+ return new Promise((resolve) => {
+ const files = Object.keys(this.audioElements).map(
+ (se) => [se as SoundEffect, getFileUrl(se as SoundEffect)] as const,
+ );
+ const totalCount = files.length;
+ let loadedCount = 0;
+
+ files.forEach(([se, fileUrl]) => {
+ const audio = new Audio(fileUrl);
+
+ audio.addEventListener(
+ "canplaythrough",
+ () => {
+ loadedCount++;
+ this.audioElements[se] = audio;
+ if (loadedCount === totalCount) {
+ resolve();
+ }
+ },
+ { once: true },
+ );
+
+ audio.addEventListener("error", () => {
+ console.log(`Failed to load audio file: ${fileUrl}`);
+ // Ignore the error and continue loading other files.
+ });
+ });
+ });
+ }
+
+ async playSoundEffect(soundEffect: SoundEffect): Promise<void> {
+ const audio = this.audioElements[soundEffect];
+ if (!audio) {
+ return;
+ }
+ audio.currentTime = 0;
+ await audio.play();
+ }
+
+ async playSoundEffectFinish(): Promise<void> {
+ await this.playSoundEffect("finish");
+ }
+
+ async playSoundEffectWinner(winner: 1 | 2): Promise<void> {
+ await this.playSoundEffect(`winner_${winner}`);
+ }
+
+ async playSoundEffectGood(): Promise<void> {
+ const variant = Math.floor(Math.random() * 4) + 1;
+ if (variant !== 1 && variant !== 2 && variant !== 3 && variant !== 4) {
+ return; // unreachable
+ }
+ return await this.playSoundEffect(`good_${variant}`);
+ }
+
+ async playSoundEffectNewScore(): Promise<void> {
+ const variant = Math.floor(Math.random() * 3) + 1;
+ if (variant !== 1 && variant !== 2 && variant !== 3) {
+ return; // unreachable
+ }
+ return await this.playSoundEffect(`new_score_${variant}`);
+ }
+
+ async playSoundEffectCompileError(): Promise<void> {
+ const variant = Math.floor(Math.random() * 2) + 1;
+ if (variant !== 1 && variant !== 2) {
+ return; // unreachable
+ }
+ return await this.playSoundEffect(`compile_error_${variant}`);
+ }
+}
diff --git a/frontend/app/.client/audio/SoundEffect.ts b/frontend/app/.client/audio/SoundEffect.ts
new file mode 100644
index 0000000..7e40da6
--- /dev/null
+++ b/frontend/app/.client/audio/SoundEffect.ts
@@ -0,0 +1,47 @@
+export type SoundEffect =
+ | "finish"
+ | "winner_1"
+ | "winner_2"
+ | "good_1"
+ | "good_2"
+ | "good_3"
+ | "good_4"
+ | "new_score_1"
+ | "new_score_2"
+ | "new_score_3"
+ | "compile_error_1"
+ | "compile_error_2";
+
+const BASE_URL =
+ process.env.NODE_ENV === "development"
+ ? `http://localhost:8002/iosdc-japan/2024/code-battle/files/audio`
+ : `/iosdc-japan/2024/code-battle/files/audio`;
+
+export function getFileUrl(soundEffect: SoundEffect): string {
+ switch (soundEffect) {
+ case "finish":
+ return `${BASE_URL}/EX_33.wav`;
+ case "winner_1":
+ return `${BASE_URL}/EX_34.wav`;
+ case "winner_2":
+ return `${BASE_URL}/EX_35.wav`;
+ case "good_1":
+ return `${BASE_URL}/EX_36.wav`;
+ case "good_2":
+ return `${BASE_URL}/EX_37.wav`;
+ case "good_3":
+ return `${BASE_URL}/EX_38.wav`;
+ case "good_4":
+ return `${BASE_URL}/EX_39.wav`;
+ case "new_score_1":
+ return `${BASE_URL}/EX_40.wav`;
+ case "new_score_2":
+ return `${BASE_URL}/EX_41.wav`;
+ case "new_score_3":
+ return `${BASE_URL}/EX_42.wav`;
+ case "compile_error_1":
+ return `${BASE_URL}/EX_43.wav`;
+ case "compile_error_2":
+ return `${BASE_URL}/EX_44.wav`;
+ }
+}
diff --git a/frontend/app/components/GolfWatchApp.client.tsx b/frontend/app/components/GolfWatchApp.client.tsx
index 22f87fa..d09a4ae 100644
--- a/frontend/app/components/GolfWatchApp.client.tsx
+++ b/frontend/app/components/GolfWatchApp.client.tsx
@@ -1,4 +1,5 @@
import { useEffect, useState } from "react";
+import { AudioController } from "../.client/audio/AudioController";
import type { components } from "../.server/api/schema";
import useWebSocket, { ReadyState } from "../hooks/useWebSocket";
import type { PlayerInfo } from "../models/PlayerInfo";
@@ -14,13 +15,17 @@ type Game = components["schemas"]["Game"];
type GameState = "connecting" | "waiting" | "starting" | "gaming" | "finished";
+export type Props = {
+ game: Game;
+ sockToken: string;
+ audioController: AudioController;
+};
+
export default function GolfWatchApp({
game,
sockToken,
-}: {
- game: Game;
- sockToken: string;
-}) {
+ audioController,
+}: Props) {
const socketUrl =
process.env.NODE_ENV === "development"
? `ws://localhost:8002/iosdc-japan/2024/code-battle/sock/golf/${game.game_id}/watch?token=${sockToken}`
@@ -53,6 +58,7 @@ export default function GolfWatchApp({
if (nowSec >= finishedAt) {
clearInterval(timer);
setGameState("finished");
+ audioController.playSoundEffectFinish();
} else {
setGameState("gaming");
}
@@ -65,7 +71,7 @@ export default function GolfWatchApp({
clearInterval(timer);
};
}
- }, [gameState, startedAt, game.duration_seconds]);
+ }, [gameState, startedAt, game.duration_seconds, audioController]);
const playerA = game.players[0];
const playerB = game.players[1];
diff --git a/frontend/app/components/GolfWatchAppWithAudioPlayRequest.client.tsx b/frontend/app/components/GolfWatchAppWithAudioPlayRequest.client.tsx
new file mode 100644
index 0000000..e299f4b
--- /dev/null
+++ b/frontend/app/components/GolfWatchAppWithAudioPlayRequest.client.tsx
@@ -0,0 +1,36 @@
+import { useState } from "react";
+import { AudioController } from "../.client/audio/AudioController";
+import GolfWatchApp, { type Props } from "./GolfWatchApp.client";
+
+export default function GolfWatchAppWithAudioPlayRequest({
+ game,
+ sockToken,
+}: Omit<Props, "audioController">) {
+ const [audioController, setAudioController] =
+ useState<AudioController | null>(null);
+ const audioPlayPermitted = audioController !== null;
+
+ if (audioPlayPermitted) {
+ return (
+ <GolfWatchApp
+ game={game}
+ sockToken={sockToken}
+ audioController={audioController}
+ />
+ );
+ } else {
+ return (
+ <div>
+ <button
+ onClick={async () => {
+ const audioController = new AudioController();
+ await audioController.loadAll();
+ setAudioController(audioController);
+ }}
+ >
+ Enable Audio Play
+ </button>
+ </div>
+ );
+ }
+}
diff --git a/frontend/app/routes/golf.$gameId.watch.tsx b/frontend/app/routes/golf.$gameId.watch.tsx
index 5edf92f..7e90b2d 100644
--- a/frontend/app/routes/golf.$gameId.watch.tsx
+++ b/frontend/app/routes/golf.$gameId.watch.tsx
@@ -3,7 +3,7 @@ import { useLoaderData } from "@remix-run/react";
import { ClientOnly } from "remix-utils/client-only";
import { apiGetGame, apiGetToken } from "../.server/api/client";
import { ensureUserLoggedIn } from "../.server/auth";
-import GolfWatchApp from "../components/GolfWatchApp.client";
+import GolfWatchAppWithAudioPlayRequest from "../components/GolfWatchAppWithAudioPlayRequest.client";
import GolfWatchAppConnecting from "../components/GolfWatchApps/GolfWatchAppConnecting";
export const meta: MetaFunction<typeof loader> = ({ data }) => [
@@ -41,7 +41,9 @@ export default function GolfWatch() {
return (
<ClientOnly fallback={<GolfWatchAppConnecting />}>
- {() => <GolfWatchApp game={game} sockToken={sockToken} />}
+ {() => (
+ <GolfWatchAppWithAudioPlayRequest game={game} sockToken={sockToken} />
+ )}
</ClientOnly>
);
}