diff --git a/packages/client/package.json b/packages/client/package.json index 7b6a7a5..4670174 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -38,6 +38,7 @@ "graphql-ws": "^5.15.0", "loadash": "^1.0.0", "lucide-react": "^0.292.0", + "micro-starknet": "^0.2.3", "mobx": "^6.9.0", "pathfinding": "^0.4.18", "phaser": "3.60.0-beta.14", diff --git a/packages/client/src/dojo/createClientComponents.ts b/packages/client/src/dojo/createClientComponents.ts index 2563f32..1db08d0 100644 --- a/packages/client/src/dojo/createClientComponents.ts +++ b/packages/client/src/dojo/createClientComponents.ts @@ -19,6 +19,7 @@ export function createClientComponents({ ...contractComponents, // create overridable component for optimistic rendering Player: overridableComponent(contractComponents.Player), + PlayerProfile: overridableComponent(contractComponents.PlayerProfile), Piece: overridableComponent(contractComponents.Piece), PlayerInvPiece: overridableComponent(contractComponents.PlayerInvPiece), Altar: overridableComponent(contractComponents.Altar), diff --git a/packages/client/src/dojo/createSystemCalls.ts b/packages/client/src/dojo/createSystemCalls.ts index df72b8c..3b2f113 100644 --- a/packages/client/src/dojo/createSystemCalls.ts +++ b/packages/client/src/dojo/createSystemCalls.ts @@ -1,6 +1,6 @@ import { ClientComponents } from "./createClientComponents"; import { IWorld } from "./generated/typescript/contracts.gen"; -import { Account } from "starknet"; +import { Account, RpcProvider } from "starknet"; import { getComponentValue, getComponentValueStrict, @@ -8,16 +8,19 @@ import { } from "@dojoengine/recs"; import { zeroEntity } from "../utils"; import { getEntityIdFromKeys } from "@dojoengine/utils"; -import { isBoolean, isEqual, isNull, isUndefined } from "lodash"; +import { isEqual } from "lodash"; import { PieceChange } from "./types"; import { processBattle } from "../phaser/systems/utils/processBattleLogs"; +import { opBuyHero } from "./opRender/opBuyHero"; +import { opSellHero } from "./opRender/opSellHero"; export type SystemCalls = ReturnType; export function createSystemCalls( { client }: { client: IWorld }, clientComponents: ClientComponents, - { GameStatus, LocalPiecesChangeTrack, Piece, LocalPiece }: ClientComponents + { GameStatus, LocalPiecesChangeTrack, Piece, LocalPiece }: ClientComponents, + rpcProvider: RpcProvider ) { const spawn = async (account: Account) => { try { @@ -126,16 +129,14 @@ export function createSystemCalls( altarSlot: number, invSlot: number ) => { - try { - return await client.home.buyHero({ - account, - altarSlot, - invSlot, - }); - } catch (e) { - console.error(e); - throw e; - } + await opBuyHero( + { client }, + clientComponents, + rpcProvider, + account, + altarSlot, + invSlot + ); }; const buyExp = async (account: Account) => { @@ -151,7 +152,13 @@ export function createSystemCalls( const sellHero = async (account: Account, gid: number) => { try { - return await client.home.sellHero({ account, gid }); + await opSellHero( + { client }, + clientComponents, + rpcProvider, + account, + gid + ); } catch (e) { console.error(e); throw e; diff --git a/packages/client/src/dojo/generated/setup.ts b/packages/client/src/dojo/generated/setup.ts index 4e9e8b0..f49e46c 100644 --- a/packages/client/src/dojo/generated/setup.ts +++ b/packages/client/src/dojo/generated/setup.ts @@ -16,6 +16,7 @@ import { import { createClient } from "graphql-ws"; import { GraphQLWsLink } from "@apollo/client/link/subscriptions"; import { getMainDefinition } from "@apollo/client/utilities"; +import { RpcProvider } from "starknet"; export type SetupResult = Awaited>; export async function setup({ @@ -29,6 +30,10 @@ export async function setup({ worldAddress: config.worldAddress, }); + const rpcProvider = new RpcProvider({ + nodeUrl: config.rpcUrl, + }); + // ws link const wsLink = new GraphQLWsLink( createClient({ @@ -99,7 +104,8 @@ export async function setup({ systemCalls: createSystemCalls( { client }, clientComponents, - clientComponents + clientComponents, + rpcProvider ), config, graphqlClient, diff --git a/packages/client/src/dojo/opRender/opBuyHero.ts b/packages/client/src/dojo/opRender/opBuyHero.ts index 422789c..c724172 100644 --- a/packages/client/src/dojo/opRender/opBuyHero.ts +++ b/packages/client/src/dojo/opRender/opBuyHero.ts @@ -1,22 +1,121 @@ -import { Account, AccountInterface } from "starknet"; +import { Account, RpcProvider } from "starknet"; import { IWorld } from "../generated/typescript/contracts.gen"; import { ClientComponents } from "../createClientComponents"; +import { getComponentValueStrict } from "@dojoengine/recs"; +import { getEntityIdFromKeys } from "@dojoengine/utils"; +import { poseidonHashMany } from "micro-starknet"; +import { logDebug } from "../../ui/lib/utils"; +import { uuid } from "@latticexyz/utils"; export const opBuyHero = async ( { client }: { client: IWorld }, - { Player, Altar, PlayerInvPiece }: ClientComponents, + { Player, Altar, Piece, PlayerInvPiece, PlayerProfile }: ClientComponents, + rpcProvider: RpcProvider, account: Account, altarSlot: number, invSlot: number ) => { + const playerEntity = getEntityIdFromKeys([BigInt(account.address)]); + const playerProfile = getComponentValueStrict(PlayerProfile, playerEntity); + const player = getComponentValueStrict(Player, playerEntity); + const altar = getComponentValueStrict(Altar, playerEntity); + const playerInvEntity = getEntityIdFromKeys([ + BigInt(account.address), + BigInt(invSlot), + ]); + + // check + if (player.coin <= 0 || player.inventoryCount >= 6) { + alert("cannot buy"); + return; + } + + playerProfile.pieceCounter += 1; + + const pieceGid = Number( + poseidonHashMany([ + BigInt(account.address), + BigInt(playerProfile.pieceCounter), + ]) & BigInt(0xffffffff) + ); + const pieceEntity = getEntityIdFromKeys([BigInt(pieceGid)]); + + const creatureId = Number(Object.entries(altar)[altarSlot][1]); + + player.coin -= 1; + player.inventoryCount += 1; + + logDebug(`generate gid ${pieceGid}`); + + const pieceOverUuid = uuid(); + Piece.addOverride(pieceOverUuid, { + entity: pieceEntity, + value: { + gid: pieceGid, + owner: BigInt(account.address), + idx: 0, + slot: invSlot, + level: 1, + creature_index: creatureId, + x: 0, + y: 0, + }, + }); + + // altar override + const altarOverride = uuid(); + const altarArray = Object.entries(altar); + altarArray[altarSlot][1] = 0; + Altar.addOverride(altarOverride, { + entity: playerEntity, + value: altarArray.reduce((accumulator, [key, value]) => { + accumulator[key] = value; + return accumulator; + }, {}), + }); + + // player inv piece override + const playerInvOverride = uuid(); + PlayerInvPiece.addOverride(playerInvOverride, { + entity: playerInvEntity, + value: { + owner: BigInt(account.address), + slot: invSlot, + gid: pieceGid, + }, + }); + + // player override + const playerOverride = uuid(); + Player.addOverride(playerOverride, { + entity: playerEntity, + value: player, + }); + + // player profile override + const playerProfileOverride = uuid(); + PlayerProfile.addOverride(playerProfileOverride, { + entity: playerEntity, + value: playerProfile, + }); + try { - return await client.home.buyHero({ + const tx = await client.home.buyHero({ account, altarSlot, invSlot, }); + await rpcProvider.waitForTransaction(tx.transaction_hash, { + retryInterval: 1000, + }); } catch (e) { console.error(e); throw e; + } finally { + Piece.removeOverride(pieceOverUuid); + Altar.removeOverride(altarOverride); + PlayerInvPiece.removeOverride(playerInvOverride); + Player.removeOverride(playerOverride); + PlayerProfile.removeOverride(playerProfileOverride); } }; diff --git a/packages/client/src/dojo/opRender/opSellHero.ts b/packages/client/src/dojo/opRender/opSellHero.ts new file mode 100644 index 0000000..12902b7 --- /dev/null +++ b/packages/client/src/dojo/opRender/opSellHero.ts @@ -0,0 +1,75 @@ +import { Account, RpcProvider } from "starknet"; +import { IWorld } from "../generated/typescript/contracts.gen"; +import { ClientComponents } from "../createClientComponents"; +import { getComponentValueStrict } from "@dojoengine/recs"; +import { getEntityIdFromKeys } from "@dojoengine/utils"; +import { uuid } from "@latticexyz/utils"; + +export const opSellHero = async ( + { client }: { client: IWorld }, + { Player, Piece, PlayerInvPiece }: ClientComponents, + rpcProvider: RpcProvider, + account: Account, + gid: number +) => { + const playerEntity = getEntityIdFromKeys([BigInt(account.address)]); + const player = getComponentValueStrict(Player, playerEntity); + const pieceEntity = getEntityIdFromKeys([BigInt(gid)]); + const piece = getComponentValueStrict(Piece, pieceEntity); + const playerInvEntity = getEntityIdFromKeys([ + BigInt(account.address), + BigInt(piece.slot), + ]); + const playerInvPiece = getComponentValueStrict( + PlayerInvPiece, + playerInvEntity + ); + + // check + player.coin += 1; + player.inventoryCount -= 1; + + const pieceOverUuid = uuid(); + Piece.addOverride(pieceOverUuid, { + entity: pieceEntity, + value: { + ...piece, + owner: 0n, + slot: 0, + }, + }); + + // player inv piece override + const playerInvOverride = uuid(); + PlayerInvPiece.addOverride(playerInvOverride, { + entity: playerInvEntity, + value: { + ...playerInvPiece, + gid: 0, + }, + }); + + // player override + const playerOverride = uuid(); + Player.addOverride(playerOverride, { + entity: playerEntity, + value: player, + }); + + try { + const tx = await client.home.sellHero({ + account, + gid, + }); + await rpcProvider.waitForTransaction(tx.transaction_hash, { + retryInterval: 1000, + }); + } catch (e) { + console.error(e); + throw e; + } finally { + Piece.removeOverride(pieceOverUuid); + PlayerInvPiece.removeOverride(playerInvOverride); + Player.removeOverride(playerOverride); + } +}; diff --git a/packages/client/src/dojo/setupNetwork.ts b/packages/client/src/dojo/setupNetwork.ts deleted file mode 100644 index 2df23d7..0000000 --- a/packages/client/src/dojo/setupNetwork.ts +++ /dev/null @@ -1,74 +0,0 @@ -// import { defineContractComponents } from "./generated/contractComponents"; -// import { world } from "./generated/world"; -// import { DojoProvider } from "@dojoengine/core"; -// import { Account, num } from "starknet"; -// import dev_manifest from "../../../contracts/target/dev/manifest.json"; -// import * as torii from "@dojoengine/torii-client"; -// import { createBurner } from "./createBurner"; -// import { ApolloClient, InMemoryCache } from "@apollo/client/core"; - -// export type SetupNetworkResult = Awaited>; - -// export async function setupNetwork() { -// const { -// VITE_PUBLIC_WORLD_ADDRESS, -// VITE_PUBLIC_NODE_URL, -// VITE_PUBLIC_TORII, -// } = import.meta.env; - -// const provider = new DojoProvider( -// VITE_PUBLIC_WORLD_ADDRESS, -// dev_manifest, -// VITE_PUBLIC_NODE_URL -// ); - -// const toriiClient = await torii.createClient([], { -// rpcUrl: VITE_PUBLIC_NODE_URL, -// toriiUrl: VITE_PUBLIC_TORII, -// worldAddress: VITE_PUBLIC_WORLD_ADDRESS, -// }); - -// const graphqlClient = new ApolloClient({ -// uri: `${VITE_PUBLIC_TORII}/graphql`, -// // temp disable cache -// cache: new InMemoryCache(), -// defaultOptions: { -// watchQuery: { -// fetchPolicy: "no-cache", -// errorPolicy: "ignore", -// }, -// query: { -// fetchPolicy: "no-cache", -// errorPolicy: "all", -// }, -// }, -// }); - -// const { account, burnerManager } = await createBurner(); - -// return { -// // dojo provider from core -// provider, - -// // recs world -// world, - -// toriiClient, -// graphqlClient, -// account, -// burnerManager, - -// // Define contract components for the world. -// contractComponents: defineContractComponents(world), - -// // Execute function. -// execute: async ( -// signer: Account, -// contract: string, -// system: string, -// call_data: num.BigNumberish[] -// ) => { -// return provider.execute(signer, contract, system, call_data); -// }, -// }; -// } diff --git a/packages/client/src/phaser/systems/utils/index.ts b/packages/client/src/phaser/systems/utils/index.ts index bb0f0fa..4b80e48 100644 --- a/packages/client/src/phaser/systems/utils/index.ts +++ b/packages/client/src/phaser/systems/utils/index.ts @@ -10,8 +10,8 @@ import { PhaserLayer } from "../.."; import { TILE_WIDTH } from "../../config/constants"; import { chainToWorldCoord, worldToChainCoord } from "./coorConvert"; import { zeroEntity } from "../../../utils"; -import { debug } from "../../../ui/lib/utils"; import { isEqual } from "lodash"; +import { logDebug } from "../../../ui/lib/utils"; export const utils = (layer: PhaserLayer) => { const { @@ -41,7 +41,7 @@ export const utils = (layer: PhaserLayer) => { if (isEqual(hero.position, { x: 0, y: 0 })) { return; } - debug(`removed piece, entity: ${entity} gid: ${gid}`); + logDebug(`removed piece, entity: ${entity} gid: ${gid}`); hero.despawn(); // hero.setComponent({ @@ -66,23 +66,25 @@ export const utils = (layer: PhaserLayer) => { ); if (!playerPiece) { - debug("no piece for ", playerAddr, index); + logDebug("no piece for ", playerAddr, index); return; } const entity = getEntityIdFromKeys([BigInt(playerPiece.gid)]); - debug(`try get piece ${playerAddr} ${index} ${playerPiece.gid}`); + logDebug(`try get piece ${playerAddr} ${index} ${playerPiece.gid}`); const piece = getComponentValue(LocalPiece, entity); if (!piece) { - throw Error(`try get piece ${playerAddr} ${index} ${playerPiece.gid}`); + throw Error( + `try get piece ${playerAddr} ${index} ${playerPiece.gid}` + ); } const isEnemy = BigInt(account.address) !== piece.owner; const { worldX, worldY } = chainToWorldCoord(piece.x, piece.y, isEnemy); - debug( + logDebug( `spawn ${playerAddr} ${index} ${piece.gid} at ${worldX}, ${worldY} ` ); @@ -130,7 +132,7 @@ export const utils = (layer: PhaserLayer) => { sprite.off("dragend"); sprite.on("dragend", (p: Phaser.Input.Pointer) => { - debug("drag end"); + logDebug("drag end"); sprite.clearTint(); // clear tint color // set dragging to false diff --git a/packages/client/src/ui/component/InvHero.tsx b/packages/client/src/ui/component/InvHero.tsx index 04410fe..7cd3467 100644 --- a/packages/client/src/ui/component/InvHero.tsx +++ b/packages/client/src/ui/component/InvHero.tsx @@ -13,7 +13,7 @@ import { getEntityIdFromKeys } from "@dojoengine/utils"; import { worldToChainCoord } from "../../phaser/systems/utils/coorConvert"; import { useComponentValue } from "@dojoengine/react"; import { zeroEntity } from "../../utils"; -import { logPlayerAction } from "../lib/utils"; +import { logDebug, logPlayerAction } from "../lib/utils"; export const InvHero = ({ id, @@ -158,7 +158,9 @@ export const InvHero = ({ getEntityIdFromKeys([BigInt(address), BigInt(id)]) ); - if (!invPiece || invPiece.gid !== 0) { + // logDebug(` ${invPiece}`); + + if (invPiece && invPiece.gid !== 0) { console.warn("slot occupied"); return; } @@ -365,7 +367,7 @@ export const InvHero = ({