Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 8 additions & 95 deletions src/app/duel/page.tsx
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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<Fighter | null>(null);
const [player2, setPlayer2] = useState<Fighter | null>(null);
const [duelResult, setDuelResult] = useState<DecodedCombatResult | null>(
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 <LoadingSpinner size="lg" text="Loading game..." />;
}

return (
<>
<ErrorBoundary FallbackComponent={GameErrorFallback}>
<GameWrapper />
</ErrorBoundary>
<ResultsSummary
isOpen={showResults}
onClose={onDialogClose}
result={duelResult}
player1={player1}
player2={player2}
txId={txId}
/>
</>
);
}
const DuelGame = dynamic(() => import("@/components/duel/duel-game"), {
ssr: false,
loading: () => <GameLoading />,
});

export default function DuelPage() {
return (
Expand All @@ -123,11 +38,9 @@ export default function DuelPage() {
<div className="flex-1 bg-opacity-70 rounded-lg overflow-hidden border border-yellow-600/20 shadow-lg items-center justify-center flex p-4">
{/* Game container */}
<div className="flex items-center justify-center flex-1 z-10">
<Suspense
fallback={<LoadingSpinner size="lg" text="Loading game..." />}
>
<ErrorBoundary FallbackComponent={GameErrorFallback}>
<DuelGame />
</Suspense>
</ErrorBoundary>
</div>
</div>
</main>
Expand Down
124 changes: 124 additions & 0 deletions src/components/duel/duel-game.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="w-full h-full flex flex-col items-center justify-center bg-stone-900/90 p-6 text-center">
<h2 className="text-2xl font-bold text-yellow-400 mb-4">
Could not load game
</h2>
<p className="text-stone-200 mb-6">
There was an error loading the game. This could be due to browser
compatibility issues or missing assets.
</p>
<button
type="button"
className="bg-gradient-to-r from-amber-700 to-yellow-600 hover:from-amber-600 hover:to-yellow-500 text-stone-100 px-4 py-2 rounded"
onClick={() => window?.location?.reload()}
>
Try Again
</button>
</div>
);
}

// 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<Fighter | null>(null);
const [player2, setPlayer2] = useState<Fighter | null>(null);
const [duelResult, setDuelResult] = useState<DecodedCombatResult | null>(
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 (
<>
<ErrorBoundary FallbackComponent={GameErrorFallback}>
<GameContainer />
</ErrorBoundary>
<ResultsSummary
isOpen={showResults}
onClose={onDialogClose}
result={duelResult}
player1={player1}
player2={player2}
txId={txId}
/>
</>
);
}
29 changes: 29 additions & 0 deletions src/components/game/game-container.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="relative w-full flex justify-center">
{/* Always render the game wrapper */}
<div className="w-full">
<GameWrapper />
</div>

{/* Overlay with animated loader */}
</div>
);
}
27 changes: 27 additions & 0 deletions src/components/game/game-loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use client";

import { motion } from "framer-motion";
import { LoadingSpinner } from "@/components/ui/loading-spinner";

export function GameLoading() {
return (
<div className="w-full flex justify-center items-center">
<div
className="relative bg-black w-full overflow-hidden rounded-md flex items-center justify-center"
style={{
maxWidth: "960px",
aspectRatio: "16/9",
}}
>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
className="flex flex-col items-center justify-center text-center"
>
<LoadingSpinner size="lg" text="Loading game..." />
</motion.div>
</div>
</div>
);
}
8 changes: 6 additions & 2 deletions src/components/game/game-wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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");
Expand Down
1 change: 1 addition & 0 deletions src/game/EventBus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
2 changes: 1 addition & 1 deletion src/game/config/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/game/scenes/Boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/game/scenes/FightScene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
});
});
Expand Down
4 changes: 2 additions & 2 deletions src/game/scenes/Preloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.");
Expand Down
Loading