Skip to content

Commit

Permalink
add phase management and basic login/lobby flow
Browse files Browse the repository at this point in the history
  • Loading branch information
willemolding committed Dec 27, 2023
1 parent 4fa59c5 commit 631d1ec
Show file tree
Hide file tree
Showing 8 changed files with 277 additions and 35 deletions.
4 changes: 2 additions & 2 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import './App.css';
import { useEffect } from 'react';
import { useNetworkLayer } from './hooks/useNetworkLayer';
import { store } from "./store/store";
import { useStore } from "./store/store";
import { UI } from './ui';

import { ToastContainer } from 'react-toastify';
Expand All @@ -16,7 +16,7 @@ function App() {

console.log("Setting network layer");

store.setState({ networkLayer });
useStore.setState({ networkLayer });

}, [networkLayer]);

Expand Down
4 changes: 2 additions & 2 deletions client/src/hooks/useDojo.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Account, RpcProvider } from "starknet";
import { NetworkLayer } from "../dojo/createNetworkLayer";
import { store } from "../store/store";
import { useStore } from "../store/store";
import { useBurner } from "@dojoengine/create-burner";

export type UIStore = ReturnType<typeof useDojo>;

export const useDojo = () => {
const { networkLayer } = store();
const { networkLayer } = useStore();

const provider = new RpcProvider({
nodeUrl: import.meta.env.VITE_PUBLIC_NODE_URL,
Expand Down
174 changes: 174 additions & 0 deletions client/src/spellcrafterContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { createContext, useContext, useState, useEffect } from 'react';
import { EntityIndex } from "@latticexyz/recs";
import { useDojo } from './hooks/useDojo';
import { useEntityQuery, useComponentValue } from '@latticexyz/react';
import { getEntityIdFromKeys } from '@dojoengine/utils';
import { setComponent, HasValue } from '@latticexyz/recs';
import { SpellStats, Region, POLAR_STAT_MIDPOINT } from './dojo/gameConfig';
import cardDefs from './generated/cards.json';

type GameActions = {
// create a new game and set it to be active
newGame: () => Promise<void>,
// call to play a card in the current active game
interact: (cardId: number) => Promise<void>,
// make a forage action in t
forage: (region: number) => Promise<void>,
}

type GameStats = {
chaos: number | undefined,
power: number | undefined,
hotCold: number | undefined,
lightDark: number | undefined,
barriers: number | undefined,
}

type SpellcrafterContext = {
// All games that belong to the current account as set by the dojo context
games: Array<EntityIndex>,
// currently selected game
activeGame: EntityIndex | undefined,
// change which came is currently active
setActiveGame: (gameId: EntityIndex) => void,
// the game stats for this current game
stats: GameStats | null,
// The cards that the player is holding in this game [card_id, number_owned]
cards: Array<[number, number]>,
// call these functions to perform actions in the current game
actions: GameActions,
}

const SpellcrafterContext = createContext<SpellcrafterContext | null>(null);

export const SpellcrafterProvider = ({ children }: { children: React.ReactNode }) => {
const currentValue = useContext(SpellcrafterContext);
if (currentValue) throw new Error("SpellcrafterProvider can only be used once");

// use value to produce a new SpellcrafterContext
const dojo = useDojo();
if (!dojo) throw new Error("SpellcrafterProvider must be used inside a DojoProvider");

const {
setup: {
systemCalls: { newGame, interact, forage },
components: { ValueInGame, Owner },
network: { graphSdk }
},
account: { account }
} = dojo;

// state held in this context
const [activeGame, setActiveGame] = useState<EntityIndex | undefined>(undefined);
const games = useEntityQuery([HasValue(Owner, { address: account.address })])

const fetchGames = async (address: string) => {
const { data: { ownerComponents } } = await graphSdk.getPlayersGames({ address: address });
ownerComponents?.edges?.forEach((entity) => {
let keys = entity?.node?.entity?.keys
const entityIndex = getEntityIdFromKeys(keys as any);
entity?.node?.entity?.components?.forEach((component) => {
switch (component?.__typename) {
case "Owner":
setComponent(Owner, entityIndex, { address: component?.address })
break;
default:
break;
}
})
})
}

// repopulate the games list when the account changes
useEffect(() => {
if (!account.address) {
setActiveGame(undefined);
return;
}
fetchGames(account.address);
}, [account.address]);

// update when the games list changes
useEffect(() => {
setActiveGame(games[games.length - 1]);
}, [games])

// use a graphql query to bootstrap the game data store when the active game changes
useEffect(() => {
if (!activeGame) return;
const fetchStats = async () => {
const { data: { valueingameComponents } } = await graphSdk.getGameValues({ game_id: "0x" + Number(activeGame).toString(16) });
valueingameComponents?.edges?.forEach((entity) => {
let keys = entity?.node?.entity?.keys?.map((key) => BigInt(key!))
const entityIndex = getEntityIdFromKeys(keys as any);
entity?.node?.entity?.components?.forEach((component) => {
switch (component?.__typename) {
case "ValueInGame":
setComponent(ValueInGame, entityIndex, { value: component?.value })
break;
default:
break;
}
})
})
}
fetchStats();
}, [activeGame]);

const actions = {
newGame: async () => {
if (!account.address) throw new Error("No active account");
await newGame(account);
// TODO: This is a huge hack and depends on waiting for the indexer
// For some reason the entities produces from the chain events are not triggering
// the useEntityQuery hook to update. Need to figure that out so things work properly
setTimeout(() => {
fetchGames(account.address)
}, 2000)
},
interact: async (cardId: number) => {
if (!activeGame) throw new Error("No active game");
await interact(account, activeGame, cardId as EntityIndex)
},
forage: async (region: Region) => {
if (!activeGame) throw new Error("No active game");
await forage(account, activeGame, region as EntityIndex)
}
}

// hook to bind to a ValueInGame for the current game
const useGameValue = (valueId: number): number | undefined => {
return useComponentValue(ValueInGame, getEntityIdFromKeys([BigInt(valueId), BigInt(parseInt((activeGame || -1).toString()!))]))?.value
}

const cards = cardDefs.map((def): [number, number] => {
return [parseInt(def.card_id), useGameValue(parseInt(def.card_id)) || 0]
}).filter(([_, count]) => count)

const contextValue: SpellcrafterContext = {
games,
activeGame,
setActiveGame,
stats: {
chaos: useGameValue(SpellStats.Chaos),
power: useGameValue(SpellStats.Power),
barriers: useGameValue(SpellStats.Barriers),
hotCold: (useGameValue(SpellStats.HotCold) || POLAR_STAT_MIDPOINT) - POLAR_STAT_MIDPOINT,
lightDark: (useGameValue(SpellStats.LightDark) || POLAR_STAT_MIDPOINT) - POLAR_STAT_MIDPOINT,
},
cards,
actions
}

return (
<SpellcrafterContext.Provider value={contextValue}>
{children}
</SpellcrafterContext.Provider>
)
}

export const useSpellcrafter = () => {
const value = useContext(SpellcrafterContext);
if (!value) throw new Error("Must be used within a SpellCraftProvider");
return value;
};
6 changes: 5 additions & 1 deletion client/src/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ import { NetworkLayer } from "../dojo/createNetworkLayer";

export type Store = {
networkLayer: NetworkLayer | null;
currentGameId: string | null;
setCurrentGameId: (currentGameId: string | null) => void;
};

export const store = create<Store>(() => ({
export const useStore = create<Store>((set) => ({
networkLayer: null,
currentGameId: null,
setCurrentGameId: (currentGameId: string | null) => set(() => ({ currentGameId })),
}));

4 changes: 2 additions & 2 deletions client/src/ui/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { store } from "../store/store";
import { useStore } from "../store/store";
import { Wrapper } from "./wrapper";

import { PhaseManager} from "./phaseManager";

export const UI = () => {

const layers = store((state) => {
const layers = useStore((state) => {
return {
networkLayer: state.networkLayer,
};
Expand Down
34 changes: 34 additions & 0 deletions client/src/ui/pages/gamePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React, { useEffect, useState } from "react";

import { ClickWrapper } from "../clickWrapper";
import { Phase } from "../phaseManager";
import { useDojo } from "../../hooks/useDojo";
import { Account, num } from "starknet";
import { padHex } from "../../utils";
import { useStore } from "../../store/store";


export const GamePage: React.FC = () => {

//for now we use a burner account
const {
account: { account },
networkLayer: {
systemCalls: { create_game },
network: { graphSdk }
},
} = useDojo();

const currentGameId = useStore((state) => state.currentGameId);

return (
<ClickWrapper className="centered-div" style={{ display: "flex", justifyContent: "center", alignItems: "center", flexDirection: "column", gap: "20px" }}>


<div>
Now playing {currentGameId}
</div>

</ClickWrapper>
);
};
82 changes: 55 additions & 27 deletions client/src/ui/pages/lobbyPage.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,78 @@
import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";

import { ClickWrapper } from "../clickWrapper";
import { Phase } from "../phaseManager";
import { useDojo } from "../../hooks/useDojo";
import { truncateString } from "../../utils";
import { Account, num } from "starknet";
import { padHex } from "../../utils";
import { useStore } from "../../store/store";

interface LobbyPageProps {
setUIState: React.Dispatch<Phase>;
setUIState: React.Dispatch<Phase>;
}

export const LobbyPage: React.FC<LobbyPageProps> = ({ setUIState }) => {

//for now we use a burner account
const {
account: { account },
networkLayer: {
systemCalls: { create_game },
network: { graphSdk }
},
} = useDojo();

useEffect(() => {
const fetchPlayerGames = async () => {
const response = await graphSdk().getPlayersGames({ address: padHex(account.address) });
console.log(response.data);
//for now we use a burner account
const {
account: { account },
networkLayer: {
systemCalls: { create_game },
network: { graphSdk }
},
} = useDojo();

const [games, setGames] = useState<Array<string>>([]);

useEffect(() => {
const fetchPlayerGames = async () => {
const { data } = await graphSdk().getPlayersGames({ address: padHex(account.address) });

const gameIds: Array<string> = [];
data.ownerModels?.edges?.forEach((edge) => {
edge?.node?.entity?.models?.forEach((model) => {
switch (model?.__typename) {
case "Owner":
gameIds.push(model?.entity_id);
break;
default:
break;
}
})
})
console.log(gameIds);
setGames(gameIds)
}

fetchPlayerGames();
}, [graphSdk, account]);


const newGame = async (account: Account) => {
//create a new game by sending a transaction
const game = await create_game(account);
}

fetchPlayerGames();
}, [graphSdk, account]);

const setCurrentGameId = useStore((state) => state.setCurrentGameId);

const newGame = async (account: Account) => {
//create a new game by sending a transaction
const game = await create_game(account);
}
const setGameAndPlay = (gameId: string) => {
setCurrentGameId(gameId);
setUIState(Phase.GAME);
}

return (
<ClickWrapper className="centered-div" style={{ display: "flex", justifyContent: "center", alignItems: "center", flexDirection: "column", gap: "20px" }}>

return (
<ClickWrapper className="centered-div" style={{ display: "flex", justifyContent: "center", alignItems: "center", flexDirection: "column", gap: "20px" }}>
{games.map((gameId, index) => {
return (<div key={index} className="global-button-style" style={{ fontSize: "2.4cqw", padding: "5px 10px", fontFamily: "OL", fontWeight: "100" }} onClick={() => { setGameAndPlay(gameId) }}>
Play {gameId}
</div>)
})}

<div className="global-button-style" style={{ fontSize: "2.4cqw", padding: "5px 10px", fontFamily: "OL", fontWeight: "100" }} onClick={() => { newGame(account)}}>
Create Game
New Game
</div>

</ClickWrapper>
);
);
};
4 changes: 3 additions & 1 deletion client/src/ui/phaseManager.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
//libs
import React, { useState, useEffect } from "react";
import { useState } from "react";

import { LoginPage } from "./pages/loginPage";
import { LobbyPage } from "./pages/lobbyPage";
import { GamePage } from "./pages/gamePage";

export enum Phase {
LOGIN,
Expand All @@ -21,6 +22,7 @@ export const PhaseManager = () => {
<>
{phase === Phase.LOGIN && <LoginPage setUIState={setUIState}/>}
{phase === Phase.LOBBY &&<LobbyPage setUIState={setUIState}/>}
{phase === Phase.GAME &&<GamePage/>}
</>
);
};
Expand Down

0 comments on commit 631d1ec

Please sign in to comment.