create-mud 2.2.17-e45e3751c0ea10c7b1f0088d674121419b0d0acb → 2.2.17-f1d5432fe2c9abb19fa378f790217e67d6dd8504
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/react/mprocs.yaml +9 -1
- package/templates/react/packages/client/index.html +2 -2
- package/templates/react/packages/client/package.json +17 -9
- package/templates/react/packages/client/postcss.config.cjs +6 -0
- package/templates/react/packages/client/src/App.tsx +41 -100
- package/templates/react/packages/client/src/Providers.tsx +35 -0
- package/templates/react/packages/client/src/common.ts +26 -0
- package/templates/react/packages/client/src/game/GameMap.tsx +102 -0
- package/templates/react/packages/client/src/game/useKeyboardMovement.ts +26 -0
- package/templates/react/packages/client/src/index.tsx +17 -32
- package/templates/react/packages/client/src/mud/Explorer.tsx +32 -0
- package/templates/react/packages/client/src/mud/Synced.tsx +14 -0
- package/templates/react/packages/client/src/mud/stash.ts +4 -0
- package/templates/react/packages/client/src/mud/useSyncStatus.ts +21 -0
- package/templates/react/packages/client/src/mud/useWorldContract.ts +44 -0
- package/templates/react/packages/client/src/ui/AsyncButton.tsx +41 -0
- package/templates/react/packages/client/src/ui/ErrorFallback.tsx +58 -0
- package/templates/react/packages/client/src/ui/icons/ArrowDownIcon.tsx +22 -0
- package/templates/react/packages/client/src/ui/icons/MUDIcon.tsx +25 -0
- package/templates/react/packages/client/src/wagmiConfig.ts +49 -0
- package/templates/react/packages/client/tailwind.config.ts +10 -0
- package/templates/react/packages/client/tsconfig.json +1 -1
- package/templates/react/packages/client/vite.config.ts +2 -7
- package/templates/react/packages/contracts/.env +1 -1
- package/templates/react/packages/contracts/mud.config.ts +13 -8
- package/templates/react/packages/contracts/out/IWorld.sol/IWorld.abi.json +2021 -0
- package/templates/react/packages/contracts/package.json +1 -0
- package/templates/react/packages/contracts/script/PostDeploy.s.sol +1 -9
- package/templates/react/packages/contracts/src/MoveSystem.sol +26 -0
- package/templates/react/packages/contracts/src/codegen/common.sol +11 -0
- package/templates/react/packages/contracts/src/codegen/index.sol +1 -1
- package/templates/react/packages/contracts/src/codegen/tables/Position.sol +318 -0
- package/templates/{react-ecs/packages/contracts/src/codegen/world/IIncrementSystem.sol → react/packages/contracts/src/codegen/world/IMoveSystem.sol} +5 -3
- package/templates/react/packages/contracts/src/codegen/world/IWorld.sol +2 -2
- package/templates/react/packages/contracts/test/MoveTest.t.sol +25 -0
- package/templates/react/packages/contracts/test/WorldTest.t.sol +16 -0
- package/templates/react/packages/contracts/worlds.json +1 -1
- package/templates/react-ecs/mprocs.yaml +9 -1
- package/templates/react-ecs/package.json +1 -2
- package/templates/react-ecs/packages/client/index.html +2 -2
- package/templates/react-ecs/packages/client/package.json +16 -9
- package/templates/react-ecs/packages/client/postcss.config.cjs +6 -0
- package/templates/react-ecs/packages/client/src/App.tsx +66 -21
- package/templates/react-ecs/packages/client/src/Providers.tsx +29 -0
- package/templates/react-ecs/packages/client/src/common.ts +27 -0
- package/templates/react-ecs/packages/client/src/game/GameMap.tsx +112 -0
- package/templates/react-ecs/packages/client/src/game/useKeyboardMovement.ts +26 -0
- package/templates/react-ecs/packages/client/src/index.tsx +17 -32
- package/templates/react-ecs/packages/client/src/mud/Explorer.tsx +32 -0
- package/templates/react-ecs/packages/client/src/mud/Synced.tsx +14 -0
- package/templates/react-ecs/packages/client/src/mud/recs.ts +6 -0
- package/templates/react-ecs/packages/client/src/mud/useSyncStatus.ts +17 -0
- package/templates/react-ecs/packages/client/src/mud/useWorldContract.ts +44 -0
- package/templates/react-ecs/packages/client/src/ui/AsyncButton.tsx +41 -0
- package/templates/react-ecs/packages/client/src/ui/ErrorFallback.tsx +58 -0
- package/templates/react-ecs/packages/client/src/ui/icons/ArrowDownIcon.tsx +22 -0
- package/templates/react-ecs/packages/client/src/ui/icons/MUDIcon.tsx +25 -0
- package/templates/react-ecs/packages/client/src/wagmiConfig.ts +49 -0
- package/templates/react-ecs/packages/client/tailwind.config.ts +10 -0
- package/templates/react-ecs/packages/client/tsconfig.json +1 -1
- package/templates/react-ecs/packages/client/vite.config.ts +2 -7
- package/templates/react-ecs/packages/contracts/.env +1 -1
- package/templates/react-ecs/packages/contracts/mud.config.ts +18 -6
- package/templates/react-ecs/packages/contracts/out/IWorld.sol/IWorld.abi.json +2039 -0
- package/templates/react-ecs/packages/contracts/package.json +1 -0
- package/templates/react-ecs/packages/contracts/script/PostDeploy.s.sol +1 -5
- package/templates/react-ecs/packages/contracts/src/Entity.sol +20 -0
- package/templates/react-ecs/packages/contracts/src/MoveSystem.sol +30 -0
- package/templates/react-ecs/packages/contracts/src/SpawnSystem.sol +18 -0
- package/templates/react-ecs/packages/contracts/src/codegen/common.sol +11 -0
- package/templates/react-ecs/packages/contracts/src/codegen/index.sol +3 -1
- package/templates/react-ecs/packages/contracts/src/codegen/tables/{Counter.sol → EntityCount.sol} +35 -35
- package/templates/react-ecs/packages/contracts/src/codegen/tables/Owner.sol +202 -0
- package/templates/react-ecs/packages/contracts/src/codegen/tables/Position.sol +321 -0
- package/templates/{react/packages/contracts/src/codegen/world/ITasksSystem.sol → react-ecs/packages/contracts/src/codegen/world/IMoveSystem.sol} +6 -9
- package/templates/react-ecs/packages/contracts/src/codegen/world/ISpawnSystem.sol +15 -0
- package/templates/react-ecs/packages/contracts/src/codegen/world/IWorld.sol +3 -2
- package/templates/react-ecs/packages/contracts/src/createEntity.sol +11 -0
- package/templates/react-ecs/packages/contracts/test/MoveTest.t.sol +39 -0
- package/templates/react-ecs/packages/contracts/test/WorldTest.t.sol +16 -0
- package/templates/react-ecs/packages/contracts/worlds.json +1 -1
- package/templates/react/packages/client/src/MUDContext.tsx +0 -21
- package/templates/react/packages/client/src/mud/createSystemCalls.ts +0 -56
- package/templates/react/packages/client/src/mud/getNetworkConfig.ts +0 -76
- package/templates/react/packages/client/src/mud/setup.ts +0 -18
- package/templates/react/packages/client/src/mud/setupNetwork.ts +0 -101
- package/templates/react/packages/client/src/mud/supportedChains.ts +0 -20
- package/templates/react/packages/contracts/src/codegen/tables/Tasks.sol +0 -522
- package/templates/react/packages/contracts/src/systems/TasksSystem.sol +0 -24
- package/templates/react/packages/contracts/test/TasksTest.t.sol +0 -30
- package/templates/react-ecs/packages/client/src/MUDContext.tsx +0 -21
- package/templates/react-ecs/packages/client/src/mud/createClientComponents.ts +0 -21
- package/templates/react-ecs/packages/client/src/mud/createSystemCalls.ts +0 -51
- package/templates/react-ecs/packages/client/src/mud/getNetworkConfig.ts +0 -77
- package/templates/react-ecs/packages/client/src/mud/setup.ts +0 -21
- package/templates/react-ecs/packages/client/src/mud/setupNetwork.ts +0 -106
- package/templates/react-ecs/packages/client/src/mud/supportedChains.ts +0 -20
- package/templates/react-ecs/packages/client/src/mud/world.ts +0 -3
- package/templates/react-ecs/packages/contracts/src/systems/IncrementSystem.sol +0 -14
- package/templates/react-ecs/packages/contracts/test/CounterTest.t.sol +0 -31
|
@@ -1,29 +1,74 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import { AccountButton } from "@latticexyz/entrykit/internal";
|
|
2
|
+
import { Direction, Entity } from "./common";
|
|
3
|
+
import mudConfig from "contracts/mud.config";
|
|
4
|
+
import { useMemo } from "react";
|
|
5
|
+
import { GameMap } from "./game/GameMap";
|
|
6
|
+
import { useWorldContract } from "./mud/useWorldContract";
|
|
7
|
+
import { Synced } from "./mud/Synced";
|
|
8
|
+
import { useSync } from "@latticexyz/store-sync/react";
|
|
9
|
+
import { components } from "./mud/recs";
|
|
10
|
+
import { useEntityQuery } from "@latticexyz/react";
|
|
11
|
+
import { Has, getComponentValueStrict } from "@latticexyz/recs";
|
|
12
|
+
import { Address } from "viem";
|
|
4
13
|
|
|
5
|
-
export
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
14
|
+
export function App() {
|
|
15
|
+
const playerEntities = useEntityQuery([Has(components.Owner), Has(components.Position)]);
|
|
16
|
+
const players = useMemo(
|
|
17
|
+
() =>
|
|
18
|
+
playerEntities.map((entity) => {
|
|
19
|
+
const owner = getComponentValueStrict(components.Owner, entity);
|
|
20
|
+
const position = getComponentValueStrict(components.Position, entity);
|
|
21
|
+
return {
|
|
22
|
+
entity: entity as Entity,
|
|
23
|
+
owner: owner.owner as Address,
|
|
24
|
+
x: position.x,
|
|
25
|
+
y: position.y,
|
|
26
|
+
};
|
|
27
|
+
}),
|
|
28
|
+
[playerEntities],
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const sync = useSync();
|
|
32
|
+
const worldContract = useWorldContract();
|
|
33
|
+
|
|
34
|
+
const onMove = useMemo(
|
|
35
|
+
() =>
|
|
36
|
+
sync.data && worldContract
|
|
37
|
+
? async (entity: Entity, direction: Direction) => {
|
|
38
|
+
const tx = await worldContract.write.app__move([entity, mudConfig.enums.Direction.indexOf(direction)]);
|
|
39
|
+
await sync.data.waitForTransaction(tx);
|
|
40
|
+
}
|
|
41
|
+
: undefined,
|
|
42
|
+
[sync.data, worldContract],
|
|
43
|
+
);
|
|
10
44
|
|
|
11
|
-
const
|
|
45
|
+
const onSpawn = useMemo(
|
|
46
|
+
() =>
|
|
47
|
+
sync.data && worldContract
|
|
48
|
+
? async () => {
|
|
49
|
+
const tx = await worldContract.write.app__spawn();
|
|
50
|
+
await sync.data.waitForTransaction(tx);
|
|
51
|
+
}
|
|
52
|
+
: undefined,
|
|
53
|
+
[sync.data, worldContract],
|
|
54
|
+
);
|
|
12
55
|
|
|
13
56
|
return (
|
|
14
57
|
<>
|
|
15
|
-
<div>
|
|
16
|
-
|
|
58
|
+
<div className="fixed inset-0 grid place-items-center p-4">
|
|
59
|
+
<Synced
|
|
60
|
+
fallback={({ message, percentage }) => (
|
|
61
|
+
<div className="tabular-nums">
|
|
62
|
+
{message} ({percentage.toFixed(1)}%)…
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
<GameMap players={players} onMove={onMove} onSpawn={onSpawn} />
|
|
67
|
+
</Synced>
|
|
68
|
+
</div>
|
|
69
|
+
<div className="fixed top-2 right-2">
|
|
70
|
+
<AccountButton />
|
|
17
71
|
</div>
|
|
18
|
-
<button
|
|
19
|
-
type="button"
|
|
20
|
-
onClick={async (event) => {
|
|
21
|
-
event.preventDefault();
|
|
22
|
-
console.log("new counter value:", await increment());
|
|
23
|
-
}}
|
|
24
|
-
>
|
|
25
|
-
Increment
|
|
26
|
-
</button>
|
|
27
72
|
</>
|
|
28
73
|
);
|
|
29
|
-
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { WagmiProvider } from "wagmi";
|
|
2
|
+
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { ReactNode } from "react";
|
|
4
|
+
import { SyncProvider } from "@latticexyz/store-sync/react";
|
|
5
|
+
import { defineConfig, EntryKitProvider } from "@latticexyz/entrykit/internal";
|
|
6
|
+
import { wagmiConfig } from "./wagmiConfig";
|
|
7
|
+
import { chainId, getWorldAddress, startBlock } from "./common";
|
|
8
|
+
import { syncAdapter } from "./mud/recs";
|
|
9
|
+
|
|
10
|
+
const queryClient = new QueryClient();
|
|
11
|
+
|
|
12
|
+
export type Props = {
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function Providers({ children }: Props) {
|
|
17
|
+
const worldAddress = getWorldAddress();
|
|
18
|
+
return (
|
|
19
|
+
<WagmiProvider config={wagmiConfig}>
|
|
20
|
+
<QueryClientProvider client={queryClient}>
|
|
21
|
+
<EntryKitProvider config={defineConfig({ chainId, worldAddress })}>
|
|
22
|
+
<SyncProvider chainId={chainId} address={worldAddress} startBlock={startBlock} adapter={syncAdapter}>
|
|
23
|
+
{children}
|
|
24
|
+
</SyncProvider>
|
|
25
|
+
</EntryKitProvider>
|
|
26
|
+
</QueryClientProvider>
|
|
27
|
+
</WagmiProvider>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import mudConfig from "contracts/mud.config";
|
|
2
|
+
import { chains } from "./wagmiConfig";
|
|
3
|
+
import { Chain, Hex } from "viem";
|
|
4
|
+
|
|
5
|
+
export const chainId = import.meta.env.CHAIN_ID;
|
|
6
|
+
export const worldAddress = import.meta.env.WORLD_ADDRESS;
|
|
7
|
+
export const startBlock = import.meta.env.START_BLOCK;
|
|
8
|
+
|
|
9
|
+
export const url = new URL(window.location.href);
|
|
10
|
+
|
|
11
|
+
export type Entity = Hex;
|
|
12
|
+
export type Direction = (typeof mudConfig.enums.Direction)[number];
|
|
13
|
+
|
|
14
|
+
export function getWorldAddress() {
|
|
15
|
+
if (!worldAddress) {
|
|
16
|
+
throw new Error("No world address configured. Is the world still deploying?");
|
|
17
|
+
}
|
|
18
|
+
return worldAddress;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getChain(): Chain {
|
|
22
|
+
const chain = chains.find((c) => c.id === chainId);
|
|
23
|
+
if (!chain) {
|
|
24
|
+
throw new Error(`No chain configured for chain ID ${chainId}.`);
|
|
25
|
+
}
|
|
26
|
+
return chain;
|
|
27
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { serialize, useAccount } from "wagmi";
|
|
2
|
+
import { useKeyboardMovement } from "./useKeyboardMovement";
|
|
3
|
+
import { Address, Hex, hexToBigInt, keccak256 } from "viem";
|
|
4
|
+
import { ArrowDownIcon } from "../ui/icons/ArrowDownIcon";
|
|
5
|
+
import { twMerge } from "tailwind-merge";
|
|
6
|
+
import { Direction, Entity } from "../common";
|
|
7
|
+
import mudConfig from "contracts/mud.config";
|
|
8
|
+
import { AsyncButton } from "../ui/AsyncButton";
|
|
9
|
+
import { useAccountModal } from "@latticexyz/entrykit/internal";
|
|
10
|
+
import { useMemo } from "react";
|
|
11
|
+
|
|
12
|
+
export type Props = {
|
|
13
|
+
readonly players?: {
|
|
14
|
+
readonly entity: Entity;
|
|
15
|
+
readonly owner: Address;
|
|
16
|
+
readonly x: number;
|
|
17
|
+
readonly y: number;
|
|
18
|
+
}[];
|
|
19
|
+
readonly onMove?: (entity: Entity, direction: Direction) => Promise<void>;
|
|
20
|
+
readonly onSpawn?: () => Promise<void>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const size = 40;
|
|
24
|
+
const scale = 100 / size;
|
|
25
|
+
|
|
26
|
+
function getColorAngle(seed: Hex) {
|
|
27
|
+
return Number(hexToBigInt(keccak256(seed)) % 360n);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const rotateClassName = {
|
|
31
|
+
North: "rotate-0",
|
|
32
|
+
East: "rotate-90",
|
|
33
|
+
South: "rotate-180",
|
|
34
|
+
West: "-rotate-90",
|
|
35
|
+
} as const satisfies Record<Direction, `${"" | "-"}rotate-${number}`>;
|
|
36
|
+
|
|
37
|
+
export function GameMap({ players = [], onMove, onSpawn }: Props) {
|
|
38
|
+
const { openAccountModal } = useAccountModal();
|
|
39
|
+
const { address: userAddress } = useAccount();
|
|
40
|
+
|
|
41
|
+
const currentPlayer = players.find((player) => player.owner.toLowerCase() === userAddress?.toLowerCase());
|
|
42
|
+
|
|
43
|
+
useKeyboardMovement(
|
|
44
|
+
useMemo(
|
|
45
|
+
() => (onMove && currentPlayer ? (direction: Direction) => onMove(currentPlayer.entity, direction) : undefined),
|
|
46
|
+
[currentPlayer, onMove],
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className="aspect-square w-full max-w-[40rem]">
|
|
52
|
+
<div className="relative w-full h-full border-8 border-black/10">
|
|
53
|
+
{currentPlayer && onMove
|
|
54
|
+
? mudConfig.enums.Direction.map((direction) => (
|
|
55
|
+
<button
|
|
56
|
+
key={direction}
|
|
57
|
+
title={`Move ${direction.toLowerCase()}`}
|
|
58
|
+
className={twMerge(
|
|
59
|
+
"outline-0 absolute inset-0 cursor-pointer grid p-4",
|
|
60
|
+
rotateClassName[direction],
|
|
61
|
+
"transition bg-gradient-to-t from-transparent via-transparent to-blue-50 text-blue-400 opacity-0 hover:opacity-40 active:opacity-100",
|
|
62
|
+
)}
|
|
63
|
+
style={{ clipPath: "polygon(0% 0%, 100% 0%, 50% 50%)" }}
|
|
64
|
+
onClick={() => onMove(currentPlayer.entity, direction)}
|
|
65
|
+
>
|
|
66
|
+
<ArrowDownIcon className="rotate-180 text-4xl self-start justify-self-center" />
|
|
67
|
+
</button>
|
|
68
|
+
))
|
|
69
|
+
: null}
|
|
70
|
+
|
|
71
|
+
{players.map((player) => (
|
|
72
|
+
<div
|
|
73
|
+
key={player.entity}
|
|
74
|
+
className="absolute bg-current"
|
|
75
|
+
style={{
|
|
76
|
+
color: `hwb(${getColorAngle(player.owner)} 40% 20%)`,
|
|
77
|
+
width: `${scale}%`,
|
|
78
|
+
height: `${scale}%`,
|
|
79
|
+
left: `${((((player.x + size / 2) % size) + size) % size) * scale}%`,
|
|
80
|
+
top: `${((size - ((player.y + size / 2) % size)) % size) * scale}%`,
|
|
81
|
+
}}
|
|
82
|
+
title={serialize(player, null, 2)}
|
|
83
|
+
>
|
|
84
|
+
{player === currentPlayer ? <div className="w-full h-full bg-current animate-ping opacity-50" /> : null}
|
|
85
|
+
</div>
|
|
86
|
+
))}
|
|
87
|
+
|
|
88
|
+
{!currentPlayer ? (
|
|
89
|
+
onSpawn ? (
|
|
90
|
+
<div className="absolute inset-0 grid place-items-center">
|
|
91
|
+
<AsyncButton
|
|
92
|
+
className="group outline-0 p-4 border-4 border-green-400 transition ring-green-300 hover:ring-4 active:scale-95 rounded-lg text-lg font-medium aria-busy:pointer-events-none aria-busy:animate-pulse"
|
|
93
|
+
onClick={() => onSpawn()}
|
|
94
|
+
>
|
|
95
|
+
Spawn<span className="hidden group-aria-busy:inline">ing…</span>
|
|
96
|
+
</AsyncButton>
|
|
97
|
+
</div>
|
|
98
|
+
) : (
|
|
99
|
+
<div className="absolute inset-0 grid place-items-center">
|
|
100
|
+
<button
|
|
101
|
+
className="group outline-0 p-4 border-4 border-green-400 transition ring-green-300 hover:ring-4 active:scale-95 rounded-lg text-lg font-medium aria-busy:pointer-events-none aria-busy:animate-pulse"
|
|
102
|
+
onClick={openAccountModal}
|
|
103
|
+
>
|
|
104
|
+
Sign in to play
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
)
|
|
108
|
+
) : null}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { Direction } from "../common";
|
|
3
|
+
|
|
4
|
+
const keys = new Map<KeyboardEvent["key"], Direction>([
|
|
5
|
+
["ArrowUp", "North"],
|
|
6
|
+
["ArrowRight", "East"],
|
|
7
|
+
["ArrowDown", "South"],
|
|
8
|
+
["ArrowLeft", "West"],
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
export const useKeyboardMovement = (move: undefined | ((direction: Direction) => void)) => {
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!move) return;
|
|
14
|
+
|
|
15
|
+
const listener = (event: KeyboardEvent) => {
|
|
16
|
+
const direction = keys.get(event.key);
|
|
17
|
+
if (direction == null) return;
|
|
18
|
+
|
|
19
|
+
event.preventDefault();
|
|
20
|
+
move(direction);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
window.addEventListener("keydown", listener);
|
|
24
|
+
return () => window.removeEventListener("keydown", listener);
|
|
25
|
+
}, [move]);
|
|
26
|
+
};
|
|
@@ -1,34 +1,19 @@
|
|
|
1
|
-
import
|
|
1
|
+
import "tailwindcss/tailwind.css";
|
|
2
|
+
import { StrictMode } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { Providers } from "./Providers";
|
|
2
5
|
import { App } from "./App";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
+
import { Explorer } from "./mud/Explorer";
|
|
7
|
+
import { ErrorBoundary } from "react-error-boundary";
|
|
8
|
+
import { ErrorFallback } from "./ui/ErrorFallback";
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
// https://vitejs.dev/guide/env-and-mode.html
|
|
20
|
-
if (import.meta.env.DEV) {
|
|
21
|
-
const { mount: mountDevTools } = await import("@latticexyz/dev-tools");
|
|
22
|
-
mountDevTools({
|
|
23
|
-
config: mudConfig,
|
|
24
|
-
publicClient: result.network.publicClient,
|
|
25
|
-
walletClient: result.network.walletClient,
|
|
26
|
-
latestBlock$: result.network.latestBlock$,
|
|
27
|
-
storedBlockLogs$: result.network.storedBlockLogs$,
|
|
28
|
-
worldAddress: result.network.worldContract.address,
|
|
29
|
-
worldAbi: result.network.worldContract.abi,
|
|
30
|
-
write$: result.network.write$,
|
|
31
|
-
recsWorld: result.network.world,
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
});
|
|
10
|
+
createRoot(document.getElementById("react-root")!).render(
|
|
11
|
+
<StrictMode>
|
|
12
|
+
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
|
13
|
+
<Providers>
|
|
14
|
+
<App />
|
|
15
|
+
<Explorer />
|
|
16
|
+
</Providers>
|
|
17
|
+
</ErrorBoundary>
|
|
18
|
+
</StrictMode>,
|
|
19
|
+
);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { getChain, getWorldAddress } from "../common";
|
|
3
|
+
import { MUDIcon } from "../ui/icons/MUDIcon";
|
|
4
|
+
|
|
5
|
+
export function Explorer() {
|
|
6
|
+
const [open, setOpen] = useState(false);
|
|
7
|
+
|
|
8
|
+
const chain = getChain();
|
|
9
|
+
const worldAddress = getWorldAddress();
|
|
10
|
+
|
|
11
|
+
const explorerUrl = chain.blockExplorers?.worldsExplorer?.url;
|
|
12
|
+
if (!explorerUrl) return null;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="fixed bottom-0 inset-x-0 flex flex-col opacity-80 transition hover:opacity-100">
|
|
16
|
+
<button
|
|
17
|
+
type="button"
|
|
18
|
+
onClick={() => setOpen(!open)}
|
|
19
|
+
className="outline-none flex justify-end gap-2 p-2 font-medium leading-none text-black"
|
|
20
|
+
>
|
|
21
|
+
{open ? (
|
|
22
|
+
<>Close</>
|
|
23
|
+
) : (
|
|
24
|
+
<>
|
|
25
|
+
Explore <MUDIcon className="text-orange-500" />
|
|
26
|
+
</>
|
|
27
|
+
)}
|
|
28
|
+
</button>
|
|
29
|
+
{open ? <iframe src={`${explorerUrl}/${worldAddress}`} className="bg-black h-[50vh]" /> : null}
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import { useSyncStatus } from "./useSyncStatus";
|
|
3
|
+
import { ComponentValue } from "@latticexyz/recs";
|
|
4
|
+
import { components } from "./recs";
|
|
5
|
+
|
|
6
|
+
export type Props = {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
fallback?: (props: ComponentValue<(typeof components)["SyncProgress"]["schema"]>) => ReactNode;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function Synced({ children, fallback }: Props) {
|
|
12
|
+
const status = useSyncStatus();
|
|
13
|
+
return status.isLive ? children : fallback?.(status);
|
|
14
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createWorld } from "@latticexyz/recs";
|
|
2
|
+
import { createSyncAdapter } from "@latticexyz/store-sync/recs";
|
|
3
|
+
import config from "contracts/mud.config";
|
|
4
|
+
|
|
5
|
+
export const world = createWorld();
|
|
6
|
+
export const { syncAdapter, components } = createSyncAdapter({ world, config });
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { initialProgress } from "@latticexyz/store-sync/internal";
|
|
2
|
+
import { SyncStep } from "@latticexyz/store-sync";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { useComponentValue } from "@latticexyz/react";
|
|
5
|
+
import { singletonEntity } from "@latticexyz/store-sync/recs";
|
|
6
|
+
import { components } from "./recs";
|
|
7
|
+
|
|
8
|
+
export function useSyncStatus() {
|
|
9
|
+
const progress = useComponentValue(components.SyncProgress, singletonEntity, initialProgress);
|
|
10
|
+
return useMemo(
|
|
11
|
+
() => ({
|
|
12
|
+
...progress,
|
|
13
|
+
isLive: progress.step === SyncStep.LIVE,
|
|
14
|
+
}),
|
|
15
|
+
[progress],
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useClient } from "wagmi";
|
|
2
|
+
import { chainId, getWorldAddress } from "../common";
|
|
3
|
+
import { Account, Chain, Client, GetContractReturnType, Transport, getContract } from "viem";
|
|
4
|
+
import { useQuery } from "@tanstack/react-query";
|
|
5
|
+
import { useSessionClient } from "@latticexyz/entrykit/internal";
|
|
6
|
+
import { observer } from "@latticexyz/explorer/observer";
|
|
7
|
+
import worldAbi from "contracts/out/IWorld.sol/IWorld.abi.json";
|
|
8
|
+
|
|
9
|
+
export function useWorldContract():
|
|
10
|
+
| GetContractReturnType<
|
|
11
|
+
typeof worldAbi,
|
|
12
|
+
{
|
|
13
|
+
public: Client<Transport, Chain>;
|
|
14
|
+
wallet: Client<Transport, Chain, Account>;
|
|
15
|
+
}
|
|
16
|
+
>
|
|
17
|
+
| undefined {
|
|
18
|
+
const client = useClient({ chainId });
|
|
19
|
+
const { data: sessionClient } = useSessionClient();
|
|
20
|
+
|
|
21
|
+
const { data: worldContract } = useQuery({
|
|
22
|
+
queryKey: ["worldContract", client?.uid, sessionClient?.uid],
|
|
23
|
+
queryFn: () => {
|
|
24
|
+
if (!client || !sessionClient) {
|
|
25
|
+
throw new Error("Not connected.");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return getContract({
|
|
29
|
+
abi: worldAbi,
|
|
30
|
+
address: getWorldAddress(),
|
|
31
|
+
client: {
|
|
32
|
+
public: client,
|
|
33
|
+
wallet: sessionClient.extend(observer()),
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
staleTime: Infinity,
|
|
38
|
+
refetchOnMount: false,
|
|
39
|
+
refetchOnReconnect: false,
|
|
40
|
+
refetchOnWindowFocus: false,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return worldContract;
|
|
44
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { DetailedHTMLProps, ButtonHTMLAttributes, useState, useRef, useCallback, MouseEventHandler } from "react";
|
|
2
|
+
|
|
3
|
+
export type AsyncButtonProps = {
|
|
4
|
+
pending?: boolean;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type Props = AsyncButtonProps & DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>;
|
|
8
|
+
|
|
9
|
+
export const AsyncButton = ({ pending, type, disabled, onClick, ...props }: Props) => {
|
|
10
|
+
// TODO: move all this logic into a hook so we can wrap other event handlers
|
|
11
|
+
|
|
12
|
+
const promiseRef = useRef<Promise<unknown>>();
|
|
13
|
+
const [promisePending, setPromisePending] = useState<true | undefined>(undefined);
|
|
14
|
+
|
|
15
|
+
const asyncOnClick = useCallback<MouseEventHandler<HTMLButtonElement>>(
|
|
16
|
+
(...args) => {
|
|
17
|
+
if (!onClick) return;
|
|
18
|
+
const result = onClick(...args);
|
|
19
|
+
const promise = Promise.resolve(result);
|
|
20
|
+
promiseRef.current = promise;
|
|
21
|
+
setPromisePending(true);
|
|
22
|
+
promise.finally(() => {
|
|
23
|
+
if (promiseRef.current === promise) {
|
|
24
|
+
setPromisePending(undefined);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
[onClick],
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<button
|
|
33
|
+
type={type || "button"}
|
|
34
|
+
aria-busy={pending || promisePending}
|
|
35
|
+
aria-disabled={disabled}
|
|
36
|
+
disabled={disabled || pending || promisePending}
|
|
37
|
+
onClick={onClick ? asyncOnClick : undefined}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { wait } from "@latticexyz/common/utils";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { FallbackProps } from "react-error-boundary";
|
|
4
|
+
|
|
5
|
+
export function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
|
|
6
|
+
const when = new Date();
|
|
7
|
+
const isMounted = useRef(false);
|
|
8
|
+
const [retries, setRetries] = useState(1);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
isMounted.current = true;
|
|
12
|
+
return () => {
|
|
13
|
+
isMounted.current = false;
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="fixed inset-0 overflow-auto bg-red-50">
|
|
19
|
+
<div className="w-full max-w-screen-md mx-auto py-16 px-8 space-y-12">
|
|
20
|
+
<h1 className="text-4xl font-black text-red-500">Oops! It broke :(</h1>
|
|
21
|
+
<div className="space-y-6">
|
|
22
|
+
<div className="relative">
|
|
23
|
+
<div className="p-6 bg-red-100 border-l-8 -ml-[8px] border-red-500 font-semibold whitespace-pre-wrap">
|
|
24
|
+
{error instanceof Error ? error.message : String(error)}
|
|
25
|
+
</div>
|
|
26
|
+
{error instanceof Error && error.stack ? (
|
|
27
|
+
<div className="p-6 bg-white font-mono text-sm overflow-auto whitespace-pre">{error.stack}</div>
|
|
28
|
+
) : null}
|
|
29
|
+
<div className="absolute top-full right-0 text-sm text-stone-400" title={when.toISOString()}>
|
|
30
|
+
{when.toLocaleString()}
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
{retries > 0 ? (
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
className="group bg-red-500 text-white px-4 py-2 rounded-md hover:bg-red-600 active:bg-red-700 transition aria-busy:pointer-events-none aria-busy:animate-pulse"
|
|
38
|
+
onClick={async (event) => {
|
|
39
|
+
// if we retry and the same error occurs, it'll look like the button click did nothing
|
|
40
|
+
// so we'll fake a pending state here to give users an indication something is happening
|
|
41
|
+
event.currentTarget.ariaBusy = "true";
|
|
42
|
+
await wait(1000);
|
|
43
|
+
resetErrorBoundary();
|
|
44
|
+
if (isMounted.current) {
|
|
45
|
+
setRetries((value) => value - 1);
|
|
46
|
+
event.currentTarget.ariaBusy = null;
|
|
47
|
+
}
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
<span className="group-aria-busy:hidden">Retry?</span>
|
|
51
|
+
<span className="hidden group-aria-busy:inline">Retrying…</span>
|
|
52
|
+
</button>
|
|
53
|
+
) : null}
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { DetailedHTMLProps, SVGAttributes } from "react";
|
|
2
|
+
import { twMerge } from "tailwind-merge";
|
|
3
|
+
|
|
4
|
+
export type Props = DetailedHTMLProps<SVGAttributes<SVGSVGElement>, SVGSVGElement>;
|
|
5
|
+
|
|
6
|
+
export function ArrowDownIcon({ className, ...props }: Props) {
|
|
7
|
+
return (
|
|
8
|
+
<svg
|
|
9
|
+
className={twMerge("h-[1em] w-[1em]", className)}
|
|
10
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
11
|
+
viewBox="0 0 16 16"
|
|
12
|
+
fill="currentColor"
|
|
13
|
+
{...props}
|
|
14
|
+
>
|
|
15
|
+
<path
|
|
16
|
+
fillRule="evenodd"
|
|
17
|
+
d="M8 2a.75.75 0 0 1 .75.75v8.69l3.22-3.22a.75.75 0 1 1 1.06 1.06l-4.5 4.5a.75.75 0 0 1-1.06 0l-4.5-4.5a.75.75 0 0 1 1.06-1.06l3.22 3.22V2.75A.75.75 0 0 1 8 2Z"
|
|
18
|
+
clipRule="evenodd"
|
|
19
|
+
/>
|
|
20
|
+
</svg>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { DetailedHTMLProps, SVGAttributes } from "react";
|
|
2
|
+
import { twMerge } from "tailwind-merge";
|
|
3
|
+
|
|
4
|
+
export type Props = DetailedHTMLProps<SVGAttributes<SVGSVGElement>, SVGSVGElement>;
|
|
5
|
+
|
|
6
|
+
export function MUDIcon({ className, ...props }: Props) {
|
|
7
|
+
return (
|
|
8
|
+
<svg
|
|
9
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
10
|
+
viewBox="0 0 8 8"
|
|
11
|
+
fill="currentColor"
|
|
12
|
+
shapeRendering="crispEdges"
|
|
13
|
+
className={twMerge("-my-[0.125em] h-[1.25em] w-[1.25em]", className)}
|
|
14
|
+
{...props}
|
|
15
|
+
>
|
|
16
|
+
{/* eslint-disable-next-line max-len */}
|
|
17
|
+
<path d="M0 0h1v1H0zm0 1h1v1H0zm0 1h1v1H0zm0 1h1v1H0zm0 1h1v1H0zm0 1h1v1H0zm0 1h1v1H0zm0 1h1v1H0zm1 0h1v1H1zm1 0h1v1H2zm1 0h1v1H3zm1 0h1v1H4zm1 0h1v1H5zm2-1h1v1H7zm0 1h1v1H7zM6 7h1v1H6zm1-2h1v1H7zm0-1h1v1H7zm0-1h1v1H7z" />
|
|
18
|
+
<path
|
|
19
|
+
d="M2 2h1v1H2zm0 1h1v1H2zm0 1h1v1H2zm0 1h1v1H2zm1-3h1v1H3zm1 0h1v1H4zm1 0h1v1H5zm0 1h1v1H5zm0 1h1v1H5zm0 1h1v1H5zM4 5h1v1H4zM3 5h1v1H3z"
|
|
20
|
+
opacity=".5"
|
|
21
|
+
/>
|
|
22
|
+
<path d="M7 2h1v1H7zm0-1h1v1H7zM1 0h1v1H1zm1 0h1v1H2zm1 0h1v1H3zm1 0h1v1H4zm1 0h1v1H5zm1 0h1v1H6zm1 0h1v1H7z" />
|
|
23
|
+
</svg>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Chain, http, webSocket } from "viem";
|
|
2
|
+
import { anvil } from "viem/chains";
|
|
3
|
+
import { createWagmiConfig } from "@latticexyz/entrykit/internal";
|
|
4
|
+
import { rhodolite, garnet, redstone } from "@latticexyz/common/chains";
|
|
5
|
+
import { chainId } from "./common";
|
|
6
|
+
|
|
7
|
+
export const chains = [
|
|
8
|
+
redstone,
|
|
9
|
+
garnet,
|
|
10
|
+
rhodolite,
|
|
11
|
+
{
|
|
12
|
+
...anvil,
|
|
13
|
+
contracts: {
|
|
14
|
+
...anvil.contracts,
|
|
15
|
+
paymaster: {
|
|
16
|
+
address: "0xf03E61E7421c43D9068Ca562882E98d1be0a6b6e",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
blockExplorers: {
|
|
20
|
+
default: {} as never,
|
|
21
|
+
worldsExplorer: {
|
|
22
|
+
name: "MUD Worlds Explorer",
|
|
23
|
+
url: "http://localhost:13690/anvil/worlds",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
] as const satisfies Chain[];
|
|
28
|
+
|
|
29
|
+
export const transports = {
|
|
30
|
+
[anvil.id]: webSocket(),
|
|
31
|
+
[garnet.id]: http(),
|
|
32
|
+
[rhodolite.id]: http(),
|
|
33
|
+
[redstone.id]: http(),
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
export const wagmiConfig = createWagmiConfig({
|
|
37
|
+
chainId,
|
|
38
|
+
// TODO: swap this with another default project ID or leave empty
|
|
39
|
+
walletConnectProjectId: "14ce88fdbc0f9c294e26ec9b4d848e44",
|
|
40
|
+
appName: document.title,
|
|
41
|
+
chains,
|
|
42
|
+
transports,
|
|
43
|
+
pollingInterval: {
|
|
44
|
+
[anvil.id]: 2000,
|
|
45
|
+
[garnet.id]: 2000,
|
|
46
|
+
[rhodolite.id]: 2000,
|
|
47
|
+
[redstone.id]: 2000,
|
|
48
|
+
},
|
|
49
|
+
});
|