-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add phase management and basic login/lobby flow
- Loading branch information
1 parent
4fa59c5
commit 631d1ec
Showing
8 changed files
with
277 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters