diff --git a/src/app/duel/page.tsx b/src/app/duel/page.tsx index e27cade..40780ba 100644 --- a/src/app/duel/page.tsx +++ b/src/app/duel/page.tsx @@ -1,15 +1,8 @@ "use client"; -import { GameWrapper } from "@/components/game/game-wrapper"; -import { LoadingSpinner } from "@/components/ui/loading-spinner"; -import { useRouter, useSearchParams } from "next/navigation"; -import { Suspense, useEffect, useState } from "react"; import { ErrorBoundary } from "react-error-boundary"; -import { useDuelActions } from "@/stores/duel-store"; -import type { DecodedCombatResult } from "@/types/game.types"; -import type { Fighter } from "@/types/fighter-types"; -import { EventBus, GameEvents } from "@/game/EventBus"; -import { ResultsSummary } from "@/components/dialogs/results-summary"; +import dynamic from "next/dynamic"; +import { GameLoading } from "@/components/game/game-loading"; // Fallback component for when the game fails to load function GameErrorFallback() { @@ -33,88 +26,10 @@ function GameErrorFallback() { ); } -// Component that uses txId from URL -function DuelGame() { - const searchParams = useSearchParams(); - const txId = searchParams.get("txId") ?? undefined; - const router = useRouter(); - const { clearState } = useDuelActions(); - // State for Results Summary - const [player1, setPlayer1] = useState(null); - const [player2, setPlayer2] = useState(null); - const [duelResult, setDuelResult] = useState( - null, - ); - const [showResults, setShowResults] = useState(false); - useEffect(() => { - // Redirect if no transaction ID is provided - if (!txId) { - router.push("/"); - return; - } - - // Add this cleanup function - will run when component unmounts - return () => { - clearState(); // Clear duel state when leaving the page - }; - }, [txId, router, clearState]); - - // Effect for EventBus listeners - useEffect(() => { - // Handler for when initial duel data is loaded from Phaser - const handleDuelDataLoaded = (data: { - player1: Fighter; - player2: Fighter; - decodedCombatBytes: DecodedCombatResult; - }) => { - setPlayer1(data.player1); - setPlayer2(data.player2); - setDuelResult(data.decodedCombatBytes); // Store the full result data - }; - - // Handler for when the Phaser game signals the fight is over - const handleGameOver = () => { - setTimeout(() => { - setShowResults(true); // Trigger the results modal - }, 2000); - }; - - // Subscribe to events - EventBus.on(GameEvents.DUEL_DATA_LOADED, handleDuelDataLoaded); - EventBus.on(GameEvents.GAME_OVER, handleGameOver); - - // Cleanup listeners on component unmount - return () => { - EventBus.off(GameEvents.DUEL_DATA_LOADED, handleDuelDataLoaded); - EventBus.off(GameEvents.GAME_OVER, handleGameOver); - }; - }, []); // Changed dependency array back to empty - - const onDialogClose = () => { - setShowResults(false); - router.push("/"); - }; - - if (!txId) { - return ; - } - - return ( - <> - - - - - - ); -} +const DuelGame = dynamic(() => import("@/components/duel/duel-game"), { + ssr: false, + loading: () => , +}); export default function DuelPage() { return ( @@ -123,11 +38,9 @@ export default function DuelPage() {
{/* Game container */}
- } - > + - +
diff --git a/src/components/duel/duel-game.tsx b/src/components/duel/duel-game.tsx new file mode 100644 index 0000000..7f0cec4 --- /dev/null +++ b/src/components/duel/duel-game.tsx @@ -0,0 +1,124 @@ +"use client"; + +import { ErrorBoundary } from "react-error-boundary"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import { useDuelActions } from "@/stores/duel-store"; +import type { DecodedCombatResult } from "@/types/game.types"; +import type { Fighter } from "@/types/fighter-types"; +import { EventBus, GameEvents } from "@/game/EventBus"; +import { ResultsSummary } from "@/components/dialogs/results-summary"; +import { GameContainer } from "@/components/game/game-container"; +import { GameLoading } from "../game/game-loading"; + +// Fallback component for when the game fails to load +function GameErrorFallback() { + return ( +
+

+ Could not load game +

+

+ There was an error loading the game. This could be due to browser + compatibility issues or missing assets. +

+ +
+ ); +} + +// Component that uses txId from URL +export default function DuelGame() { + const searchParams = useSearchParams(); + const txId = searchParams.get("txId") ?? undefined; + const router = useRouter(); + const { clearState } = useDuelActions(); + // State for Results Summary + const [player1, setPlayer1] = useState(null); + const [player2, setPlayer2] = useState(null); + const [duelResult, setDuelResult] = useState( + null, + ); + const [showResults, setShowResults] = useState(false); + + useEffect(() => { + // Redirect if no transaction ID is provided + if (!txId && typeof window !== "undefined") { + router.push("/"); + return; + } + + // Cleanup function: only runs if the redirect above didn't happen + return () => { + if (txId) { + clearState(); + } + }; + }, [txId, router, clearState]); + + // Effect for EventBus listeners + useEffect(() => { + if (typeof window === "undefined" || !EventBus) { + return; + } + + // Handler for when initial duel data is loaded from Phaser + const handleDuelDataLoaded = (data: { + player1: Fighter; + player2: Fighter; + decodedCombatBytes: DecodedCombatResult; + }) => { + setPlayer1(data.player1); + setPlayer2(data.player2); + setDuelResult(data.decodedCombatBytes); // Store the full result data + }; + + // Handler for when the Phaser game signals the fight is over + const handleGameOver = () => { + setTimeout(() => { + setShowResults(true); // Trigger the results modal + }, 2000); + }; + + // Subscribe to events + EventBus?.on(GameEvents.DUEL_DATA_LOADED, handleDuelDataLoaded); + EventBus?.on(GameEvents.GAME_OVER, handleGameOver); + + // Cleanup listeners on component unmount + return () => { + EventBus?.off(GameEvents.DUEL_DATA_LOADED, handleDuelDataLoaded); + EventBus?.off(GameEvents.GAME_OVER, handleGameOver); + }; + }, []); + + const onDialogClose = () => { + setShowResults(false); + router.push("/"); + }; + + if (!txId) { + return null; + } + + return ( + <> + + + + + + ); +} diff --git a/src/components/game/game-container.tsx b/src/components/game/game-container.tsx new file mode 100644 index 0000000..e65fefd --- /dev/null +++ b/src/components/game/game-container.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { GameWrapper } from "@/components/game/game-wrapper"; +import { LoadingSpinner } from "@/components/ui/loading-spinner"; + +export function GameContainer() { + const [showLoader, setShowLoader] = useState(true); + + useEffect(() => { + const timer = setTimeout(() => { + setShowLoader(false); + }, 2500); + + return () => clearTimeout(timer); + }, []); + + return ( +
+ {/* Always render the game wrapper */} +
+ +
+ + {/* Overlay with animated loader */} +
+ ); +} diff --git a/src/components/game/game-loading.tsx b/src/components/game/game-loading.tsx new file mode 100644 index 0000000..d539105 --- /dev/null +++ b/src/components/game/game-loading.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { motion } from "framer-motion"; +import { LoadingSpinner } from "@/components/ui/loading-spinner"; + +export function GameLoading() { + return ( +
+
+ + + +
+
+ ); +} diff --git a/src/components/game/game-wrapper.tsx b/src/components/game/game-wrapper.tsx index dcf99fb..828b872 100644 --- a/src/components/game/game-wrapper.tsx +++ b/src/components/game/game-wrapper.tsx @@ -19,9 +19,10 @@ interface GameWrapperProps { // player2Id?: string; player1?: Fighter; // txId?: string; + onPhaserLoaded?: () => void; } -export function GameWrapper({ player1 }: GameWrapperProps) { +export function GameWrapper({ player1, onPhaserLoaded }: GameWrapperProps) { const [isClient, setIsClient] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); const [isMuted, setIsMuted] = useState(false); @@ -32,7 +33,10 @@ export function GameWrapper({ player1 }: GameWrapperProps) { useEffect(() => { setIsClient(true); - }, []); + if (onPhaserLoaded) { + onPhaserLoaded(); + } + }, [onPhaserLoaded]); const fixCanvasSize = useCallback(() => { const canvas = containerRef.current?.querySelector("canvas"); diff --git a/src/game/EventBus.ts b/src/game/EventBus.ts index 029b5d8..f3d3737 100644 --- a/src/game/EventBus.ts +++ b/src/game/EventBus.ts @@ -16,4 +16,5 @@ export enum GameEvents { PLAYER_HEALED = "player-healed", GAME_OVER = "game-over", DUEL_DATA_LOADED = "duel-data-loaded", + LOADING_UI_READY = "loading-ui-ready", } diff --git a/src/game/config/main.ts b/src/game/config/main.ts index 7618fa0..62a1f41 100644 --- a/src/game/config/main.ts +++ b/src/game/config/main.ts @@ -21,7 +21,7 @@ export const gameData = { // Find out more information about the Game Config at: // https://newdocs.phaser.io/docs/3.70.0/Phaser.Types.Core.GameConfig -const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent); +const isIOS = typeof navigator !== 'undefined' && /iPhone|iPad|iPod/i.test(navigator.userAgent); const config: Phaser.Types.Core.GameConfig = { type: isIOS ? Phaser.CANVAS : Phaser.AUTO, diff --git a/src/game/scenes/Boot.ts b/src/game/scenes/Boot.ts index c1182f2..06a21cf 100644 --- a/src/game/scenes/Boot.ts +++ b/src/game/scenes/Boot.ts @@ -13,7 +13,7 @@ export class Boot extends Phaser.Scene { .text( this.cameras.main.width / 2, this.cameras.main.height / 2, - "Initializing...", + "", // TODO: This is the line which causes and intermediary loading screen between react and phaser handoff { fontFamily: "Arial", fontSize: "24px", diff --git a/src/game/scenes/FightScene.ts b/src/game/scenes/FightScene.ts index 33474b2..636462c 100644 --- a/src/game/scenes/FightScene.ts +++ b/src/game/scenes/FightScene.ts @@ -1235,7 +1235,7 @@ export class FightScene extends Scene { onComplete: () => { // After walking away, start taunt sequence this.playTauntSequence(winner, isPlayer2); - EventBus.emit(GameEvents.GAME_OVER); + EventBus?.emit(GameEvents.GAME_OVER); }, }); }); diff --git a/src/game/scenes/Preloader.ts b/src/game/scenes/Preloader.ts index dc9bcf2..542885e 100644 --- a/src/game/scenes/Preloader.ts +++ b/src/game/scenes/Preloader.ts @@ -118,7 +118,7 @@ export class Preloader extends Scene { this.events.emit("status-update", "Finalizing..."); // Emit the duel data loaded event for the /duel page - EventBus.emit(GameEvents.DUEL_DATA_LOADED, { + EventBus?.emit(GameEvents.DUEL_DATA_LOADED, { player1: this.player1, player2: this.player2, decodedCombatBytes: this.decodedCombatBytes, @@ -152,7 +152,7 @@ export class Preloader extends Scene { this.scene.start("FightScene", sceneData); // Let React know that the scene is ready - EventBus.emit("current-scene-ready", this); + EventBus?.emit("current-scene-ready", this); } catch (error) { console.error("Error starting fight:", error); this.loadingUI.showError("Error starting game. Please try again."); diff --git a/src/game/ui/LoadingUI.ts b/src/game/ui/LoadingUI.ts index 008abec..be4a08f 100644 --- a/src/game/ui/LoadingUI.ts +++ b/src/game/ui/LoadingUI.ts @@ -1,5 +1,6 @@ import type { Scene } from "phaser"; import { LoadingProgressManager } from "./LoadingProgressManager"; +import { EventBus, GameEvents } from "../EventBus"; export class LoadingUI { private scene: Scene; @@ -17,6 +18,10 @@ export class LoadingUI { }); this.create(); + + // Emit that the loading UI is ready + EventBus?.emit(GameEvents.LOADING_UI_READY); + this.setupListeners(); this.configureLoadingStages(); }