Skip to content

Commit b9da1f3

Browse files
Move more control inside Next and improvements (#30)
* feat: integrate wallet using wagmi, move ticker, keymanager to next component * rm base path
1 parent 29f4bd0 commit b9da1f3

24 files changed

+12428
-2580
lines changed

game/next.config.mjs

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
/** @type {import('next').NextConfig} */
22
const nextConfig = {
3-
basePath: "/comets",
43
output: "export",
54
reactStrictMode: true,
5+
6+
webpack: (config) => {
7+
config.externals.push("pino-pretty", "lokijs", "encoding");
8+
return config;
9+
},
610
};
711

812
export default nextConfig;

game/package-lock.json

+12,094-2,343
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

game/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99
"lint": "next lint"
1010
},
1111
"dependencies": {
12+
"@tanstack/react-query": "^5.51.21",
1213
"hammerjs": "^2.0.8",
1314
"howler": "^2.2.4",
1415
"next": "14.2.5",
1516
"react": "^18",
1617
"react-dom": "^18",
17-
"viem": "^2.18.7"
18+
"viem": "^2.18.7",
19+
"wagmi": "^2.12.2"
1820
},
1921
"devDependencies": {
2022
"@types/hammerjs": "^2.0.45",

game/src/app/config.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { sepolia } from "viem/chains";
2+
import { createConfig, http } from "wagmi";
3+
import { injected } from "wagmi/connectors";
4+
5+
export const stackrDevnet = {
6+
...sepolia,
7+
name: "Stackr Devnet",
8+
rpcUrls: {
9+
default: {
10+
http: ["https://devnet.stf.xyz"],
11+
},
12+
},
13+
id: 69420,
14+
};
15+
16+
export function getConfig() {
17+
return createConfig({
18+
chains: [stackrDevnet],
19+
connectors: [injected({ shimDisconnect: true })],
20+
transports: {
21+
[stackrDevnet.id]: http(),
22+
},
23+
});
24+
}

game/src/app/globals.css

+5-5
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
}
1818

1919
@font-face {
20-
font-family: 'Hyperspace';
21-
src: url('../assets/Hyperspace.otf');
20+
font-family: "Hyperspace";
21+
src: url("../assets/Hyperspace.otf");
2222
}
2323

2424
body {
@@ -29,10 +29,10 @@ body {
2929
display: flex;
3030
justify-content: center;
3131
align-items: center;
32-
background-color: #525252;
32+
background-color: #000000;
33+
color: white;
3334
}
34-
Ø
35-
#canvas {
35+
Ø #canvas {
3636
vertical-align: middle;
3737
}
3838

game/src/app/layout.tsx

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { Navbar } from "@/components/navbar";
12
import type { Metadata } from "next";
23
import localFont from "next/font/local";
34
import "./globals.css";
4-
import { Navbar } from "@/components/navbar";
5+
import { Providers } from "./providers";
56

67
export const metadata: Metadata = {
78
title: "Comets",
@@ -18,10 +19,12 @@ export default function RootLayout({
1819
return (
1920
<html lang="en">
2021
<body className={Hyperspace.className}>
21-
<div className="flex flex-col w-full h-full">
22-
<Navbar />
23-
<div className="flex-1 content-center self-center">{children}</div>
24-
</div>
22+
<Providers>
23+
<div className="flex flex-col w-full h-full">
24+
<Navbar />
25+
<div className="flex-1 content-center self-center">{children}</div>
26+
</div>
27+
</Providers>
2528
</body>
2629
</html>
2730
);

game/src/app/page.tsx

+38-10
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,59 @@
11
"use client";
2+
import { Button } from "@/components/button";
23
import { Game } from "@/components/game";
34
import { fetchLeaderboard, fetchMruInfo } from "@/rpc/api";
45
import { useEffect, useState } from "react";
6+
import { createWalletClient, custom } from "viem";
7+
import { addChain } from "viem/actions";
8+
import { useAccount, useConnect } from "wagmi";
9+
import { stackrDevnet } from "./config";
10+
11+
export default function Main() {
12+
const { isConnected, isConnecting } = useAccount();
13+
const { connectors, connect } = useConnect();
514

6-
export default function Comets() {
715
const [isLoading, setLoading] = useState(true);
816

917
useEffect(() => {
1018
const setupGame = async () => {
1119
setLoading(true);
1220
await Promise.all([fetchMruInfo(), fetchLeaderboard()]);
21+
const connector = connectors[0];
22+
await connector.getProvider();
1323
setLoading(false);
1424
};
1525
setupGame();
16-
}, []);
26+
}, [connectors]);
27+
28+
const connectWallet = async () => {
29+
const connector = connectors[0];
30+
const chainId = await connector.getChainId();
31+
const walletClient = createWalletClient({
32+
transport: custom(window.ethereum),
33+
});
34+
if (chainId !== stackrDevnet.id) {
35+
try {
36+
await walletClient.switchChain({ id: stackrDevnet.id });
37+
} catch (e) {
38+
console.log(e);
39+
await addChain(walletClient, { chain: stackrDevnet });
40+
}
41+
}
42+
connect({ connector });
43+
};
1744

18-
if (isLoading) {
45+
const renderContinueButton = () => {
1946
return (
20-
<div className="text-3xl text-white w-full text-center">
21-
Loading Game...
47+
<div className="flex flex-col justify-center">
48+
<Button onClick={connectWallet}>Connect Wallet</Button>
49+
<div className="text-center mt-4">To play game</div>
2250
</div>
2351
);
52+
};
53+
54+
if (isLoading || isConnecting) {
55+
return <div className="text-3xl w-full text-center">Loading Game...</div>;
2456
}
2557

26-
return (
27-
<main>
28-
<Game />
29-
</main>
30-
);
58+
return <main>{isConnected ? <Game /> : renderContinueButton()}</main>;
3159
}

game/src/app/providers.tsx

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"use client";
2+
3+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4+
import { type ReactNode, useState } from "react";
5+
import { WagmiProvider } from "wagmi";
6+
7+
import { getConfig } from "./config";
8+
9+
type Props = {
10+
children: ReactNode;
11+
};
12+
13+
export function Providers({ children }: Props) {
14+
const [config] = useState(() => getConfig());
15+
const [queryClient] = useState(() => new QueryClient());
16+
17+
return (
18+
<WagmiProvider config={config}>
19+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
20+
</WagmiProvider>
21+
);
22+
}

game/src/app/useAction.tsx

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { submitAction } from "@/rpc/api";
2+
import { getFromStore, StorageKey } from "@/rpc/storage";
3+
import { getAddress } from "viem";
4+
import { useAccount, useSignTypedData } from "wagmi";
5+
6+
export const useAction = () => {
7+
const { address } = useAccount();
8+
const { signTypedDataAsync } = useSignTypedData();
9+
10+
if (!address) {
11+
throw new Error("No address found");
12+
}
13+
14+
const mruInfo = getFromStore(StorageKey.MRU_INFO);
15+
const { domain, schemas } = mruInfo;
16+
const msgSender = getAddress(address);
17+
18+
const submit = async (transition: string, inputs: any) => {
19+
let signature;
20+
try {
21+
signature = await signTypedDataAsync({
22+
domain,
23+
primaryType: schemas[transition].primaryType,
24+
types: schemas[transition].types,
25+
message: inputs,
26+
account: msgSender,
27+
});
28+
} catch (e) {
29+
console.error("Error signing message", e);
30+
return;
31+
}
32+
33+
return submitAction(transition, {
34+
inputs,
35+
signature,
36+
msgSender,
37+
});
38+
};
39+
40+
return { submit };
41+
};

game/src/components/button.tsx

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
interface Props {
2+
children: React.ReactNode;
3+
onClick: () => void;
4+
}
5+
6+
export const Button: React.FC<Props> = ({ children, onClick }) => {
7+
return (
8+
<button
9+
className="select-none p-2 px-4 border-2 cursor-pointer shadow-sm hover:bg-gray-800 hover:border-gray-400"
10+
onClick={onClick}
11+
>
12+
{children}
13+
</button>
14+
);
15+
};

game/src/components/game.tsx

+71-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,80 @@
11
"use client";
2+
import { useAction } from "@/app/useAction";
23
import { Comets } from "@/core/comets";
4+
import { KeyManager } from "@/core/keys";
35
import { init } from "@/core/loop";
4-
import { useEffect } from "react";
6+
import { TickRecorder } from "@/core/tickRecorder";
7+
import {
8+
addToStore,
9+
getFromStore,
10+
removeFromStore,
11+
StorageKey,
12+
} from "@/rpc/storage";
13+
import { useEffect, useRef } from "react";
514

15+
const LOOP_INTERVAL = 1000;
616
export const Game = () => {
17+
const { submit } = useAction();
18+
const tickRecorder = useRef<TickRecorder>();
19+
const isStarting = useRef<boolean>(false);
20+
const isEnding = useRef<boolean>(false);
21+
const game = useRef<Comets>();
22+
23+
const handleEndGame = async (score: number) => {
24+
if (isEnding.current) {
25+
return;
26+
}
27+
28+
isEnding.current = true;
29+
try {
30+
await submit("endGame", {
31+
gameId: getFromStore(StorageKey.GAME_ID),
32+
timestamp: Date.now(),
33+
score,
34+
gameInputs: tickRecorder.current?.serializedTicks(),
35+
});
36+
} catch (e: unknown) {
37+
console.error("Error sending ticks", (e as Error).message);
38+
} finally {
39+
removeFromStore(StorageKey.GAME_ID);
40+
game.current?.switchToMainPage();
41+
isEnding.current = false;
42+
}
43+
};
44+
45+
const handleStartGame = async () => {
46+
if (isStarting.current) {
47+
return;
48+
}
49+
50+
isStarting.current = true;
51+
const res = await submit("startGame", {
52+
timestamp: Date.now(),
53+
});
54+
const gameId = res.logs[0].value;
55+
console.debug("Game started", gameId);
56+
addToStore(StorageKey.GAME_ID, gameId);
57+
isStarting.current = false;
58+
return gameId;
59+
};
60+
61+
const createGame = () => {
62+
const km = new KeyManager();
63+
const tr = new TickRecorder(km);
64+
tickRecorder.current = tr;
65+
66+
const comets = new Comets(km, tr, handleEndGame, handleStartGame);
67+
game.current = comets;
68+
};
69+
770
useEffect(() => {
8-
const game = new Comets();
71+
createGame();
72+
973
const tick = setTimeout(() => {
10-
init(game);
11-
}, 1000);
74+
if (game.current) {
75+
init(game.current);
76+
}
77+
}, LOOP_INTERVAL);
1278

1379
return () => {
1480
if (tick) {
@@ -19,7 +85,7 @@ export const Game = () => {
1985

2086
return (
2187
<div id="game">
22-
<canvas id="canvas"></canvas>
88+
<canvas id="canvas" className="border-4 rounded-sm"></canvas>
2389
</div>
2490
);
2591
};

game/src/components/navbar.tsx

+19-11
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,30 @@
11
"use client";
2-
import { getWalletClient } from "@/rpc/wallet";
3-
import { useEffect, useState } from "react";
2+
import { stackrDevnet } from "@/app/config";
3+
import { formatAddress } from "@/core/highScoreMode";
4+
import { useEffect } from "react";
5+
import { useAccount, useDisconnect } from "wagmi";
6+
import { Button } from "./button";
47

58
export const Navbar = () => {
6-
const [user, setUser] = useState<string | null>(null);
9+
const { address, chainId } = useAccount();
10+
const { disconnect } = useDisconnect();
711

812
useEffect(() => {
9-
(async () => {
10-
const client = await getWalletClient();
11-
setUser(client?.account?.address || "...");
12-
})();
13-
}, []);
13+
if (chainId !== stackrDevnet.id) {
14+
disconnect();
15+
}
16+
}, [chainId]);
1417

1518
return (
16-
<nav className="p-6 text-white">
19+
<nav className="px-12 py-4">
1720
<div className="flex justify-between items-center">
18-
<div className="text-2xl">Comets</div>
19-
<div>{!user ? <button>Connect Wallet</button> : user}</div>
21+
<div className="text-2xl p-2 px-4 select-none">Comets</div>
22+
{!!address && (
23+
<div className="flex gap-4 text-center items-center">
24+
<div>{formatAddress(address)}</div>
25+
<Button onClick={() => disconnect()}>Disconnect</Button>
26+
</div>
27+
)}
2028
</div>
2129
</nav>
2230
);

0 commit comments

Comments
 (0)