React Client
Run the dev server
From the root of the project, run:
Easy method:
./scripts/client.sh
Manual method:
pnpm i && pnpm dev
The client requires environment variables to be set. See the Environment Variables page for more information.
Project Structure
Elements
- Small reusable react UI components
Components
- Stateful react components composed of Elements. This is where all the onchain state should be stored.
Modules
- Collection of components composed into Modules
Containers
- Locations where Modules are composed into a full page
Layouts
- Collection of containers composed into a full layout
useDojo Hook
The useDojo
hook is a core part of the Dojo framework that provides access to essential game functionality, account
management, and network interactions.
Usage
import { useDojo } from "@/hooks/context/DojoContext";
function GameComponent() {
const { account, network, masterAccount } = useDojo();
// ... your game logic
}
Features
Account Management
The hook provides comprehensive wallet and account management capabilities:
- Burner Wallet Creation: Create temporary wallets for players
- Account Selection: Switch between different burner wallets
- Account Listing: View all available burner wallets
- Master Account Access: Access to the game's master account
- Deployment Status: Track wallet deployment status
Network Integration
Access to network configuration and setup results for interacting with the Starknet blockchain.
Game Setup
Provides access to all initialized game systems, components, and world configurations.
Requirements
The hook must be used within a DojoProvider
component and requires the following environment variables:
VITE_PUBLIC_MASTER_ADDRESS
VITE_PUBLIC_MASTER_PRIVATE_KEY
VITE_PUBLIC_ACCOUNT_CLASS_HASH
Return Value
The hook returns an object containing:
setup
: Complete Dojo context including game systems and componentsaccount
: Account management functions and statenetwork
: Network configuration and setup resultsmasterAccount
: The master account instance
Example
function GameUI() {
const { account } = useDojo();
return (
<div>
<p>Current Account: {account.account.address}</p>
</div>
);
}
Eternum provider
Wrapper around the DojoProvider, which itself is a wrapper around a generic Starknet provider, an API to easy interact with your contract’s API via the RPC. Each layer of abstraction adds its own set of functionalities, with the Eternum Provider mainly providing easy access to all the system calls that our systems expose (create_army, etc…)
Offchain messages
You can use offchain messages to store information in the indexer, but it is not persisted onchain. Some examples of use cases for this are in-game messages. Refer to this code for an example of how to use offchain messages
State Management with Recs
Recs allow you to query the state of the world and subscribe to changes in the world. It is the recommended way to manage state in the client.
const structureAtPosition = runQuery([HasValue(Position, { x, y }), Has(Structure)]);
This line of code will run a query against the local state of the browser and return all the structures that are at the
position { x, y }
. This does not execute any call to Torii as the local state is already synced with the latest
updates.
This part of the code in client/src/dojo/setup.ts
is where the local state is initialized and synced.
Subscribing to changes
To subscribe to changes in the world, you can use the useEntityQuery
hook. This hook will return the entity that
matches the query and update it if it changes.
const allRealms = useEntityQuery([Has(Realm)]);
Getting the value of your components
After you have run a query, you can get the value of your components using the getComponentValue
function.
const realm = getComponentValue(Realm, entityId);
The entityId is the poseidon hash of all the keys of the component you want to get. This can be an easier way to get your component than using a query if you already know all the keys.
Getting a component from an entity
If you have an entityId, you can get a component from it using the getComponent
function.
const realm = getComponentValue(Realm, getEntityIdFromKeys([entityId]));
The previous line of code is equivalent to:
const entity = runQuery([Has(Realm), HasValue(Keys, [entityId])]);
const realm = getComponentValue(Realm, Array.from(entity)[0]);
Extending the client
Adding a new component
You will need to add your component to the contractComponents.ts
file. This will ensure that the component is synced
with the state of the world and will provide the types for the component.
Adding a new system
You will need to add your system logic to the sdk/packages/eternum/src/provider/index.ts
file and then use it in the
createSystemCalls.ts
file. This will ensure that the system is called with the correct arguments and will provide the
types for the system.
Optimistic updates
You can use optimistic updates to update the local state without waiting for the transaction to be included in a block. This is useful to provide a better user experience by updating the UI immediately.
private _optimisticDestroy = (entityId: ID, col: number, row: number) => {
const overrideId = uuid();
const realmPosition = getComponentValue(this.setup.components.Position, getEntityIdFromKeys([BigInt(entityId)]));
const { x: outercol, y: outerrow } = realmPosition || { x: 0, y: 0 };
const entity = getEntityIdFromKeys([outercol, outerrow, col, row].map((v) => BigInt(v)));
this.setup.components.Building.addOverride(overrideId, {
entity,
value: {
outer_col: outercol,
outer_row: outerrow,
inner_col: col,
inner_row: row,
category: "None",
produced_resource_type: 0,
bonus_percent: 0,
entity_id: 0,
outer_entity_id: 0,
},
});
return overrideId;
};
You can use the uuid
function to generate a unique overrideId. Remember to return the overrideId so you can later
delete it after the transaction is included in a block. It's good practice to remove the override, however because of a
delay between the transaction being included in a block and Torii syncing, you might have a split second where the
override is removed and the Recs being updated. This will cause your component (i.e. a building) to appear then
disappear for a second before reappearing.