diff --git a/bun.lockb b/bun.lockb index d6464aef..e7a70ba0 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/examples/emojiman/.github/mark-dark.svg b/examples/emojiman/.github/mark-dark.svg new file mode 100644 index 00000000..4fb9b267 --- /dev/null +++ b/examples/emojiman/.github/mark-dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/examples/emojiman/.github/mark-light.svg b/examples/emojiman/.github/mark-light.svg new file mode 100644 index 00000000..01cb677a --- /dev/null +++ b/examples/emojiman/.github/mark-light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/examples/emojiman/.github/workflows/test.yaml b/examples/emojiman/.github/workflows/test.yaml new file mode 100644 index 00000000..9cf3da81 --- /dev/null +++ b/examples/emojiman/.github/workflows/test.yaml @@ -0,0 +1,16 @@ +name: CI + +on: + push: + pull_request: + +jobs: + sozo-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: curl -L https://install.dojoengine.org | bash + - run: /home/runner/.config/.dojo/bin/dojoup -v v0.3.2 + - run: | + /home/runner/.config/.dojo/bin/sozo build + /home/runner/.config/.dojo/bin/sozo test diff --git a/examples/emojiman/.gitignore b/examples/emojiman/.gitignore new file mode 100644 index 00000000..eb5a316c --- /dev/null +++ b/examples/emojiman/.gitignore @@ -0,0 +1 @@ +target diff --git a/examples/emojiman/.vscode/settings.json b/examples/emojiman/.vscode/settings.json new file mode 100644 index 00000000..47c48a34 --- /dev/null +++ b/examples/emojiman/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cairo1.languageServerPath": "$HOME/.dojo/bin/dojo-language-server", + "cairo1.enableLanguageServer": true, + "cairo1.enableScarb": false +} \ No newline at end of file diff --git a/examples/emojiman/LICENSE b/examples/emojiman/LICENSE new file mode 100644 index 00000000..d29b8597 --- /dev/null +++ b/examples/emojiman/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Dojo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/emojiman/README.md b/examples/emojiman/README.md new file mode 100644 index 00000000..95f3cc6b --- /dev/null +++ b/examples/emojiman/README.md @@ -0,0 +1,29 @@ + + + Dojo logo + + +![Turn based](https://img.shields.io/badge/Turn_based-8A2BE2) +![Rock paper scissors](https://img.shields.io/badge/Rock_Paper_Scissors-blue) +# Emojiman contracts + +Player can be of type of rock/paper/scissors. +Players spawn on a 50 x 50 grid. Make 3 moves per turn. And try to kill as many as they can before getting killed. A kill happens when two players land on the same tile, regular RPS mechanics. Two players of same type can exist on the same tile. + +## Data + +* Player ID +* Position +* Energy +* Type +* ContractAddress + +## Actions + +* Spawn + - Assigns a player ID and spawns the contract address. +* Move + - Submit 3 directions you want to move to. +* Tick + - Runs the game turn processing all submitted moves. + - Kills the players of different type on same tile. diff --git a/examples/emojiman/Scarb.lock b/examples/emojiman/Scarb.lock new file mode 100644 index 00000000..4ece2a45 --- /dev/null +++ b/examples/emojiman/Scarb.lock @@ -0,0 +1,21 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "dojo" +version = "0.3.4" +source = "git+https://github.com/dojoengine/dojo?tag=v0.3.4#a3140d88b08b79c5ff2261c1db81bafe80b5cc91" +dependencies = [ + "dojo_plugin", +] + +[[package]] +name = "dojo_examples" +version = "0.3.7" +dependencies = [ + "dojo", +] + +[[package]] +name = "dojo_plugin" +version = "0.3.6" diff --git a/examples/emojiman/Scarb.toml b/examples/emojiman/Scarb.toml new file mode 100644 index 00000000..32d2b60f --- /dev/null +++ b/examples/emojiman/Scarb.toml @@ -0,0 +1,21 @@ +[package] +cairo-version = "2.2.0" +name = "dojo_examples" +version = "0.3.7" + +[cairo] +sierra-replace-ids = true + +[dependencies] +dojo = { git = "https://github.com/dojoengine/dojo", tag = "v0.3.4" } + +[[target.dojo]] + +[tool.dojo] +initializer_class_hash = "0xbeef" + +[tool.dojo.env] +rpc_url = "http://localhost:5050/" +# Default account for katana with seed = 0 +account_address = "0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973" +private_key = "0x1800000000300000180000000000030000000000003006001800006600" diff --git a/examples/emojiman/scripts/default_auth.sh b/examples/emojiman/scripts/default_auth.sh new file mode 100644 index 00000000..db23d253 --- /dev/null +++ b/examples/emojiman/scripts/default_auth.sh @@ -0,0 +1,24 @@ +#!/bin/bash +set -euo pipefail +pushd $(dirname "$0")/.. + +export RPC_URL="http://localhost:5050"; + +export WORLD_ADDRESS=$(cat ./target/dev/manifest.json | jq -r '.world.address') + +export ACTIONS_ADDRESS=$(cat ./target/dev/manifest.json | jq -r '.contracts[] | select(.name == "actions" ).address') + +echo "---------------------------------------------------------------------------" +echo world : $WORLD_ADDRESS +echo " " +echo actions : $ACTIONS_ADDRESS +echo "---------------------------------------------------------------------------" + +# enable system -> component authorizations +COMPONENTS=("Position" "MovesQueue" "RPSType" "GameData" "PlayerID" "Energy" "PlayerAddress" "PlayerAtPosition" ) + +for component in ${COMPONENTS[@]}; do + sozo auth writer $component $ACTIONS_ADDRESS --world $WORLD_ADDRESS --rpc-url $RPC_URL +done + +echo "Default authorizations have been successfully set." \ No newline at end of file diff --git a/examples/emojiman/src/actions.cairo b/examples/emojiman/src/actions.cairo new file mode 100644 index 00000000..7324cd83 --- /dev/null +++ b/examples/emojiman/src/actions.cairo @@ -0,0 +1,391 @@ +use dojo_examples::models::{Direction, RPS}; + +const INITIAL_ENERGY: u8 = 255; +const RENEWED_ENERGY: u8 = 3; +const MOVE_ENERGY_COST: u8 = 1; +const X_RANGE: u128 = 50; // These need to be u128 +const Y_RANGE: u128 = 50; // These need to be u128 +const X_ORIGIN: u8 = 100; // These can be same as position +const Y_ORIGIN: u8 = 100; // These can be same as position + +// define the interface +#[starknet::interface] +trait IActions { + fn spawn(self: @TContractState, rps: RPS); + fn move(self: @TContractState, dir: Direction); + fn tick(self: @TContractState); +} + +// dojo decorator +#[dojo::contract] +mod actions { + use starknet::{ContractAddress, get_caller_address}; + use debug::PrintTrait; + use dojo_examples::models::{ + GAME_DATA_KEY, GameData, Direction, RPS, RPSPrintImpl, Vec2, Position, PlayerAtPosition, + RPSType, Energy, PlayerID, PlayerAddress + }; + use dojo_examples::utils::next_position; + use super::{ + INITIAL_ENERGY, RENEWED_ENERGY, MOVE_ENERGY_COST, X_RANGE, Y_RANGE, X_ORIGIN, Y_ORIGIN, + IActions + }; + use integer::{u128s_from_felt252, U128sFromFelt252Result, u128_safe_divmod}; + + // region player id assignment + fn assign_player_id(world: IWorldDispatcher, num_players: u8, player: ContractAddress) -> u8 { + let id = num_players; + set!(world, (PlayerID { player, id }, PlayerAddress { player, id })); + return id; + } + // endregion player id assignment + + // region player position + fn clear_player_at_position(world: IWorldDispatcher, x: u8, y: u8) { + // Set no player at position + set!(world, (PlayerAtPosition { x, y, id: 0 })); + } + + fn player_at_position(world: IWorldDispatcher, x: u8, y: u8) -> u8 { + get!(world, (x, y), (PlayerAtPosition)).id + } + // endregion player position + + // region game ops + fn player_position_and_energy(world: IWorldDispatcher, id: u8, x: u8, y: u8, amt: u8) { + set!(world, (PlayerAtPosition { x, y, id }, Position { x, y, id }, Energy { id, amt },)); + } + + fn player_dead(world: IWorldDispatcher, id: u8) { + let pos = get!(world, id, (Position)); + let empty_player = starknet::contract_address_const::<0>(); + + let player = get!(world, id, (PlayerAddress)).player; + // Remove player address and ID mappings + set!(world, (PlayerID { player, id: 0 })); + + // Empty player address for id + player_position_and_energy(world, id, 0, 0, 0); + } + + // panics if players are of same type (move cancelled) + // if the player dies returns false + // if the player kills the other player returns true + fn encounter(world: IWorldDispatcher, player: u8, adversary: u8) -> bool { + let adv_type = get!(world, adversary, (RPSType)).rps; + let ply_type = get!(world, player, (RPSType)).rps; + if encounter_type(ply_type, adv_type) { + // adversary dies + player_dead(world, adversary); + true + } else { + // player dies + player_dead(world, player); + false + } + } + + fn encounter_type(ply_type: RPS, adv_type: RPS) -> bool { + assert(adv_type != ply_type, 'occupied by same type'); + if (ply_type == RPS::Rock && adv_type == RPS::Scissors) + || (ply_type == RPS::Paper && adv_type == RPS::Rock) + || (ply_type == RPS::Scissors && adv_type == RPS::Paper) { + return true; + } + false + } + + fn spawn_coords(world: IWorldDispatcher, player: felt252, mut salt: felt252) -> (u8, u8) { + let mut x = 10; + let mut y = 10; + loop { + let hash = pedersen::pedersen(player, salt); + let rnd_seed = match u128s_from_felt252(hash) { + U128sFromFelt252Result::Narrow(low) => low, + U128sFromFelt252Result::Wide((high, low)) => low, + }; + let (rnd_seed, x_) = u128_safe_divmod(rnd_seed, X_RANGE.try_into().unwrap()); + let (rnd_seed, y_) = u128_safe_divmod(rnd_seed, Y_RANGE.try_into().unwrap()); + let x_: felt252 = x_.into(); + let y_: felt252 = y_.into(); + + x = X_ORIGIN + x_.try_into().unwrap(); + y = Y_ORIGIN + y_.try_into().unwrap(); + let occupied = player_at_position(world, x, y); + if occupied == 0 { + break; + } else { + salt += 1; // Try new salt + } + }; + (x, y) + } + // endregion game ops + + // impl: implement functions specified in trait + #[external(v0)] + impl ActionsImpl of IActions { + // Spawns the player on to the map + fn spawn(self: @ContractState, rps: RPS) { + let world = self.world_dispatcher.read(); + let player = get_caller_address(); + + let mut game_data = get!(world, GAME_DATA_KEY, (GameData)); + game_data.number_of_players += 1; + let number_of_players = game_data.number_of_players; // id starts at 1 + set!(world, (game_data)); + + let id = assign_player_id(world, number_of_players, player); + + set!(world, (RPSType { id, rps })); + + let (x, y) = spawn_coords(world, player.into(), id.into()); // Pick randomly + player_position_and_energy(world, id, x, y, INITIAL_ENERGY); + } + + // Queues move for player to be processed later + fn move(self: @ContractState, dir: Direction) { + let world = self.world_dispatcher.read(); + let player = get_caller_address(); + + // player id + let id = get!(world, player, (PlayerID)).id; + + let (pos, energy) = get!(world, id, (Position, Energy)); + + assert(energy.amt >= MOVE_ENERGY_COST, 'Not enough energy'); + + // Clear old position + clear_player_at_position(world, pos.x, pos.y); + + let Position{id, x, y } = next_position(pos, dir); + + let adversary = player_at_position(world, x, y); + if 0 == adversary { + // Empty cell, move + player_position_and_energy(world, id, x, y, energy.amt - MOVE_ENERGY_COST); + } else { + if encounter(world, id, adversary) { + // Move the player + player_position_and_energy(world, id, x, y, energy.amt - MOVE_ENERGY_COST); + } + } + } + + // Process player move queues + // @TODO do the killing + // @TODO update player entities + // @TODO keep score + fn tick(self: @ContractState) {} + } +} + +#[cfg(test)] +mod tests { + use starknet::class_hash::Felt252TryIntoClassHash; + use starknet::ContractAddress; + use debug::PrintTrait; + + // import world dispatcher + use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + + // import test utils + use dojo::test_utils::{spawn_test_world, deploy_contract}; + + // import models + use dojo_examples::models::{ + position, player_at_position, rps_type, energy, player_id, player_address, + }; + use dojo_examples::models::{ + Position, RPSType, Energy, Direction, RPS, RPSPrintImpl, Vec2, PlayerAtPosition, PlayerID, + PlayerAddress, + }; + + // import actions + use super::{actions, IActionsDispatcher, IActionsDispatcherTrait}; + use super::{INITIAL_ENERGY, RENEWED_ENERGY, MOVE_ENERGY_COST}; + + fn init() -> (ContractAddress, IWorldDispatcher, IActionsDispatcher) { + let caller = starknet::contract_address_const::<'jon'>(); + // This sets caller for current function, but not passed to called contract functions + starknet::testing::set_caller_address(caller); + // This sets caller for called contract functions. + starknet::testing::set_contract_address(caller); + // models + let mut models = array![ + player_at_position::TEST_CLASS_HASH, + position::TEST_CLASS_HASH, + energy::TEST_CLASS_HASH, + rps_type::TEST_CLASS_HASH, + player_id::TEST_CLASS_HASH, + player_address::TEST_CLASS_HASH, + ]; + + // deploy world with models + let world = spawn_test_world(models); + + // deploy systems contract + let contract_address = world + .deploy_contract('actions', actions::TEST_CLASS_HASH.try_into().unwrap()); + let actions = IActionsDispatcher { contract_address }; + (caller, world, actions) + } + + #[test] + #[available_gas(30000000)] + fn spawn_test() { + let (caller, world, actions_) = init(); + + actions_.spawn(RPS::Rock); + + // Get player ID + let player_id = get!(world, caller, (PlayerID)).id; + assert(1 == player_id, 'incorrect id'); + + // Get player from id + let (position, rps_type, energy) = get!(world, player_id, (Position, RPSType, Energy)); + assert(0 < position.x, 'incorrect position.x'); + assert(0 < position.y, 'incorrect position.y'); + assert(RPS::Rock == rps_type.rps, 'incorrect rps'); + assert(INITIAL_ENERGY == energy.amt, 'incorrect energy'); + } + + #[test] + #[available_gas(30000000)] + fn dead_test() { + let (caller, world, actions_) = init(); + + actions_.spawn(RPS::Rock); + // Get player ID + let player_id = get!(world, caller, (PlayerID)).id; + actions::player_dead(world, player_id); + + // Get player from id + let (position, rps_type, energy) = get!(world, player_id, (Position, RPSType, Energy)); + assert(0 == position.x, 'incorrect position.x'); + assert(0 == position.y, 'incorrect position.y'); + assert(0 == energy.amt, 'incorrect energy'); + } + + #[test] + #[available_gas(30000000)] + fn random_spawn_test() { + let (caller, world, actions_) = init(); + + actions_.spawn(RPS::Rock); + // Get player ID + let pos_p1 = get!(world, get!(world, caller, (PlayerID)).id, (Position)); + + let caller = starknet::contract_address_const::<'jim'>(); + starknet::testing::set_contract_address(caller); + actions_.spawn(RPS::Rock); + // Get player ID + let pos_p2 = get!(world, get!(world, caller, (PlayerID)).id, (Position)); + + assert(pos_p1.x != pos_p2.x, 'spawn pos.x same'); + assert(pos_p1.y != pos_p2.y, 'spawn pos.x same'); + } + + #[test] + #[available_gas(30000000)] + fn random_duplicate_spawn_test() { + let (caller, world, actions_) = init(); + + let id = 16; + let (x, y) = actions::spawn_coords(world, caller.into(), id); + + // Simulate player #5 on that location + set!(world, (PlayerAtPosition { x, y, id: 5 })); + + let (x_, y_) = actions::spawn_coords(world, caller.into(), id); + + assert(x != x_, 'spawn pos.x same'); + assert(y != y_, 'spawn pos.x same'); + } + + #[test] + #[available_gas(30000000)] + fn moves_test() { + let (caller, world, actions_) = init(); + + actions_.spawn(RPS::Rock); + + // Get player ID + let player_id = get!(world, caller, (PlayerID)).id; + assert(1 == player_id, 'incorrect id'); + + let (spawn_pos, spawn_energy) = get!(world, player_id, (Position, Energy)); + + actions_.move(Direction::Up); + // Get player from id + let (pos, energy) = get!(world, player_id, (Position, Energy)); + + assert(energy.amt == spawn_energy.amt - MOVE_ENERGY_COST, 'incorrect energy'); + assert(spawn_pos.x == pos.x, 'incorrect position.x'); + assert(spawn_pos.y - 1 == pos.y, 'incorrect position.y'); + } + + #[test] + #[available_gas(30000000)] + fn player_at_position_test() { + let (caller, world, actions_) = init(); + + actions_.spawn(RPS::Rock); + + // Get player ID + let player_id = get!(world, caller, (PlayerID)).id; + + // Get player position + let Position{x, y, id } = get!(world, player_id, Position); + + // Player should be at position + assert(actions::player_at_position(world, x, y) == player_id, 'player should be at pos'); + + // Player moves + actions_.move(Direction::Up); + + // Player shouldn't be at old position + assert(actions::player_at_position(world, x, y) == 0, 'player should not be at pos'); + + // Get new player position + let Position{x, y, id } = get!(world, player_id, Position); + + // Player should be at new position + assert(actions::player_at_position(world, x, y) == player_id, 'player should be at pos'); + } + + use RPS::{Rock, Paper, Scissors}; + + #[test] + #[available_gas(30000000)] + fn encounter_test() { + let (caller, world, actions_) = init(); + assert(false == actions::encounter_type(RPS::Rock, RPS::Paper), 'R v P should lose'); + assert(true == actions::encounter_type(RPS::Rock, RPS::Scissors), 'R v S should win'); + assert(false == actions::encounter_type(RPS::Scissors, RPS::Rock), 'S v R should lose'); + assert(true == actions::encounter_type(RPS::Scissors, RPS::Paper), 'S v P should win'); + assert(false == actions::encounter_type(RPS::Paper, RPS::Scissors), 'P v S should lose'); + assert(true == actions::encounter_type(RPS::Paper, RPS::Rock), 'P v R should win'); + } + + #[test] + #[available_gas(2000000)] + #[should_panic()] + fn encounter_rock_tie_panic() { + actions::encounter_type(RPS::Rock, RPS::Rock); + } + + #[test] + #[available_gas(2000000)] + #[should_panic()] + fn encounter_paper_tie_panic() { + actions::encounter_type(RPS::Paper, RPS::Paper); + } + + #[test] + #[available_gas(2000000)] + #[should_panic()] + fn encounter_scissor_tie_panic() { + actions::encounter_type(RPS::Scissors, RPS::Scissors); + } +} diff --git a/examples/emojiman/src/lib.cairo b/examples/emojiman/src/lib.cairo new file mode 100644 index 00000000..9f927ca6 --- /dev/null +++ b/examples/emojiman/src/lib.cairo @@ -0,0 +1,4 @@ +mod actions; +mod models; +mod utils; + diff --git a/examples/emojiman/src/models.cairo b/examples/emojiman/src/models.cairo new file mode 100644 index 00000000..38c305c6 --- /dev/null +++ b/examples/emojiman/src/models.cairo @@ -0,0 +1,101 @@ +use starknet::ContractAddress; +use debug::PrintTrait; + +#[derive(Serde, Copy, Drop, Introspect)] +enum Direction { + None, + Left, + Right, + Up, + Down, +} + +impl DirectionIntoFelt252 of Into { + fn into(self: Direction) -> felt252 { + match self { + Direction::None(()) => 0, + Direction::Left(()) => 1, + Direction::Right(()) => 2, + Direction::Up(()) => 3, + Direction::Down(()) => 4, + } + } +} + +#[derive(Serde, Copy, Drop, PartialEq, Introspect)] +enum RPS { + Rock, + Paper, + Scissors +} + +impl RPSPrintImpl of PrintTrait { + fn print(self: RPS) { + match self { + RPS::Rock => 'Rock'.print(), + RPS::Paper => 'Paper'.print(), + RPS::Scissors => 'Scissors'.print(), + } + } +} + +const GAME_DATA_KEY: felt252 = 'game'; + +#[derive(Copy, Drop, Serde, Introspect)] +struct Vec2 { + x: u32, + y: u32 +} + +#[derive(Model, Copy, Drop, Serde)] +struct PlayerAtPosition { + #[key] + x: u8, + #[key] + y: u8, + id: u8, +} + +#[derive(Model, Copy, Drop, Serde)] +struct Position { + #[key] + id: u8, + x: u8, + y: u8 +} + +#[derive(Model, Copy, Drop, Serde)] +struct RPSType { + #[key] + id: u8, + rps: RPS, +} + +#[derive(Model, Copy, Drop, Serde)] +struct Energy { + #[key] + id: u8, + amt: u8, +} + +#[derive(Model, Copy, Drop, Serde)] +struct PlayerID { + #[key] + player: ContractAddress, + id: u8, +} + +#[derive(Model, Copy, Drop, Serde)] +struct PlayerAddress { + #[key] + id: u8, + player: ContractAddress, +} + +#[derive(Model, Copy, Drop, Serde)] +struct GameData { + #[key] + game: felt252, // Always 'game' + number_of_players: u8, + available_ids: u256, // Packed u8s? +} diff --git a/examples/emojiman/src/utils.cairo b/examples/emojiman/src/utils.cairo new file mode 100644 index 00000000..823b1501 --- /dev/null +++ b/examples/emojiman/src/utils.cairo @@ -0,0 +1,13 @@ +use dojo_examples::models::{Position, Direction}; + +fn next_position(mut position: Position, direction: Direction) -> Position { + match direction { + Direction::None(()) => { return position; }, + Direction::Left(()) => { position.x -= 1; }, + Direction::Right(()) => { position.x += 1; }, + Direction::Up(()) => { position.y -= 1; }, + Direction::Down(()) => { position.y += 1; }, + }; + + position +} diff --git a/examples/react-phaser-example/components.json b/examples/react-phaser-example/components.json new file mode 100644 index 00000000..2a12988a --- /dev/null +++ b/examples/react-phaser-example/components.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "stone", + "cssVariables": true + }, + "aliases": { + "components": "src/components", + "utils": "src/lib/utils" + } +} diff --git a/examples/react-phaser-example/package.json b/examples/react-phaser-example/package.json index 833f1333..696991a0 100644 --- a/examples/react-phaser-example/package.json +++ b/examples/react-phaser-example/package.json @@ -9,21 +9,25 @@ "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", "codegen": "graphql-codegen", - "components": "npx @dojoengine/core ../dojo-starter/target/dev/manifest.json src/dojo/contractComponents.ts http://localhost:5050 0x534692277764b04cfc469858891b825c799d1da550d2509fdd5be2f32abdaa0" + "components": "npx @dojoengine/core ../emojiman/target/dev/manifest.json src/dojo/contractComponents.ts http://localhost:5050 0x534692277764b04cfc469858891b825c799d1da550d2509fdd5be2f32abdaa0" }, "dependencies": { "@dojoengine/core": "link:dojo-packages/packages/core", "@dojoengine/create-burner": "link:dojo-packages/packages/create-burner", - "@dojoengine/torii-client": "link:dojo-packages/packages/torii-client", - "@dojoengine/utils": "link:dojo-packages/packages/utils", "@dojoengine/react": "link:dojo-packages/packages/react", "@dojoengine/recs": "0.1.35", - "@latticexyz/utils": "^2.0.0-next.11", + "@dojoengine/torii-client": "link:dojo-packages/packages/torii-client", + "@dojoengine/utils": "link:dojo-packages/packages/utils", "@latticexyz/phaserx": "^2.0.0-next.11", + "@latticexyz/utils": "^2.0.0-next.11", + "@radix-ui/react-slot": "^1.0.2", + "class-variance-authority": "^0.7.0", + "clsx": "^2.0.0", "ethers": "^5.7.2", "events": "^3.3.0", "graphql": "^16.7.1", "graphql-request": "^6.1.0", + "lucide-react": "^0.292.0", "mobx": "^6.9.0", "phaser": "3.60.0-beta.14", "proxy-deep": "^3.1.1", @@ -33,16 +37,18 @@ "simplex-noise": "^4.0.1", "starknet": "^5.19.5", "styled-components": "^6.0.7", - "zustand": "^4.4.1", + "tailwind-merge": "^2.0.0", + "tailwindcss-animate": "^1.0.7", "vite-plugin-top-level-await": "^1.3.1", - "vite-plugin-wasm": "^3.2.2" + "vite-plugin-wasm": "^3.2.2", + "zustand": "^4.4.1" }, "devDependencies": { "@graphql-codegen/cli": "^5.0.0", "@graphql-codegen/typescript": "^4.0.1", "@graphql-codegen/typescript-graphql-request": "^5.0.0", "@graphql-codegen/typescript-operations": "^4.0.1", - "@types/node": "^20.4.8", + "@types/node": "^20.9.0", "@types/react": "^18.0.37", "@types/react-dom": "^18.0.11", "@typescript-eslint/eslint-plugin": "^5.59.0", diff --git a/examples/react-phaser-example/src/App.tsx b/examples/react-phaser-example/src/App.tsx index 40982626..22b8d0f7 100644 --- a/examples/react-phaser-example/src/App.tsx +++ b/examples/react-phaser-example/src/App.tsx @@ -18,7 +18,6 @@ function App() { return (
-
); diff --git a/examples/react-phaser-example/src/artTypes/world.ts b/examples/react-phaser-example/src/artTypes/world.ts deleted file mode 100644 index 1e819ca2..00000000 --- a/examples/react-phaser-example/src/artTypes/world.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -export enum Tileset { - Grass = 0, - Mountains = 1, - Forest = 2, -} -export enum TileAnimationKey { } -export const TileAnimations: { [key in TileAnimationKey]: number[] } = {}; \ No newline at end of file diff --git a/examples/react-phaser-example/src/assets/atlases/atlas.json b/examples/react-phaser-example/src/assets/atlases/atlas.json index 1052ddd4..d47553c1 100644 --- a/examples/react-phaser-example/src/assets/atlases/atlas.json +++ b/examples/react-phaser-example/src/assets/atlases/atlas.json @@ -5,7 +5,7 @@ }, "textures": [ { - "image": "atlas.png?timestamp=1678825051140", + "image": "../texture.png?timestamp=1678825051140", "format": "RGBA8888", "size": { "w": 2048, @@ -14,7 +14,7 @@ "scale": 1, "frames": [ { - "filename": "sprites/soldier/idle/0.png", + "filename": "sprites/paper/0.png", "rotated": false, "trimmed": false, "sourceSize": { @@ -35,7 +35,7 @@ } }, { - "filename": "sprites/soldier/idle/1.png", + "filename": "sprites/rock/0.png", "rotated": false, "trimmed": false, "sourceSize": { @@ -56,7 +56,7 @@ } }, { - "filename": "sprites/soldier/idle/2.png", + "filename": "sprites/scissors/0.png", "rotated": false, "trimmed": false, "sourceSize": { @@ -70,34 +70,13 @@ "h": 32 }, "frame": { - "x": 0, - "y": 32, - "w": 32, - "h": 32 - } - }, - { - "filename": "sprites/soldier/idle/3.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 0, + "x": 64, "y": 0, "w": 32, "h": 32 - }, - "frame": { - "x": 32, - "y": 32, - "w": 32, - "h": 32 } } ] } ] -} \ No newline at end of file +} diff --git a/examples/react-phaser-example/src/assets/paper.png b/examples/react-phaser-example/src/assets/paper.png new file mode 100644 index 00000000..864042a2 Binary files /dev/null and b/examples/react-phaser-example/src/assets/paper.png differ diff --git a/examples/react-phaser-example/src/assets/react.svg b/examples/react-phaser-example/src/assets/react.svg deleted file mode 100644 index 6c87de9b..00000000 --- a/examples/react-phaser-example/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/react-phaser-example/src/assets/rock.png b/examples/react-phaser-example/src/assets/rock.png new file mode 100644 index 00000000..93303300 Binary files /dev/null and b/examples/react-phaser-example/src/assets/rock.png differ diff --git a/examples/react-phaser-example/src/assets/scissors.png b/examples/react-phaser-example/src/assets/scissors.png new file mode 100644 index 00000000..284c1c01 Binary files /dev/null and b/examples/react-phaser-example/src/assets/scissors.png differ diff --git a/examples/react-phaser-example/src/assets/texture.json b/examples/react-phaser-example/src/assets/texture.json new file mode 100644 index 00000000..e90d4312 --- /dev/null +++ b/examples/react-phaser-example/src/assets/texture.json @@ -0,0 +1,83 @@ +{ + "textures": [ + { + "image": "texture.png", + "format": "RGBA8888", + "size": { + "w": 96, + "h": 32 + }, + "scale": 1, + "frames": [ + { + "filename": "paper.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + }, + "frame": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "filename": "rock.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + }, + "frame": { + "x": 32, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "filename": "scissors.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + }, + "frame": { + "x": 64, + "y": 0, + "w": 32, + "h": 32 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:7dd38248733509cfafbacea47809c895:4ba9ad81aa06f2955894ea13212a26b7:88db1022cd6b660616541d2788fcd4f7$" + } +} diff --git a/examples/react-phaser-example/src/assets/texture.png b/examples/react-phaser-example/src/assets/texture.png new file mode 100644 index 00000000..9e519a9b Binary files /dev/null and b/examples/react-phaser-example/src/assets/texture.png differ diff --git a/examples/react-phaser-example/src/assets/tilesets/land.png b/examples/react-phaser-example/src/assets/tilesets/land.png new file mode 100644 index 00000000..4e3af1c1 Binary files /dev/null and b/examples/react-phaser-example/src/assets/tilesets/land.png differ diff --git a/examples/react-phaser-example/src/assets/world.ts b/examples/react-phaser-example/src/assets/world.ts new file mode 100644 index 00000000..67a4155c --- /dev/null +++ b/examples/react-phaser-example/src/assets/world.ts @@ -0,0 +1,8 @@ +export enum Tileset { + Land = 0, + Forest = 1, + Sea = 2, + Desert = 3, +} +export enum TileAnimationKey {} +export const TileAnimations: { [key in TileAnimationKey]: number[] } = {}; diff --git a/examples/react-phaser-example/src/components/ui/button.tsx b/examples/react-phaser-example/src/components/ui/button.tsx new file mode 100644 index 00000000..e15750fe --- /dev/null +++ b/examples/react-phaser-example/src/components/ui/button.tsx @@ -0,0 +1,57 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "../../lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + } +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/examples/react-phaser-example/src/dojo/contractComponents.ts b/examples/react-phaser-example/src/dojo/contractComponents.ts index 12e2cf77..08ea2b7b 100644 --- a/examples/react-phaser-example/src/dojo/contractComponents.ts +++ b/examples/react-phaser-example/src/dojo/contractComponents.ts @@ -4,33 +4,90 @@ import { defineComponent, Type as RecsType, World } from "@dojoengine/recs"; export function defineContractComponents(world: World) { return { - Moves: (() => { + Energy: (() => { return defineComponent( world, + { id: RecsType.Number, amt: RecsType.Number }, { - player: RecsType.BigInt, - remaining: RecsType.Number, - last_direction: RecsType.Number, + metadata: { + name: "Energy", + types: [], + }, + } + ); + })(), + GameData: (() => { + return defineComponent( + world, + { + game: RecsType.Number, + number_of_players: RecsType.Number, + available_ids: RecsType.NumberArray, }, { metadata: { - name: "Moves", - types: ["Direction"], + name: "GameData", + types: [], }, } ); })(), - Position: (() => { + PlayerAddress: (() => { return defineComponent( world, + { id: RecsType.Number, player: RecsType.String }, { - player: RecsType.BigInt, - vec: { x: RecsType.Number, y: RecsType.Number }, - }, + metadata: { + name: "PlayerAddress", + types: [], + }, + } + ); + })(), + PlayerAtPosition: (() => { + return defineComponent( + world, + { x: RecsType.Number, y: RecsType.Number, id: RecsType.Number }, + { + metadata: { + name: "PlayerAtPosition", + types: [], + }, + } + ); + })(), + PlayerID: (() => { + return defineComponent( + world, + { player: RecsType.String, id: RecsType.Number }, + { + metadata: { + name: "PlayerID", + types: [], + }, + } + ); + })(), + Position: (() => { + return defineComponent( + world, + { id: RecsType.Number, x: RecsType.Number, y: RecsType.Number }, { metadata: { name: "Position", - types: ["Vec2"], + types: [], + }, + } + ); + })(), + RPSType: (() => { + return defineComponent( + world, + { id: RecsType.Number, rps: RecsType.Number }, + { + metadata: { + name: "RPSType", + types: ["Direction"], }, } ); diff --git a/examples/react-phaser-example/src/dojo/createBurner.ts b/examples/react-phaser-example/src/dojo/createBurner.ts new file mode 100644 index 00000000..a753ec0f --- /dev/null +++ b/examples/react-phaser-example/src/dojo/createBurner.ts @@ -0,0 +1,29 @@ +import { BurnerManager } from "@dojoengine/create-burner"; +import { Account, RpcProvider } from "starknet"; + +export const createBurner = async () => { + const rpcProvider = new RpcProvider({ + nodeUrl: import.meta.env.VITE_PUBLIC_NODE_URL!, + }); + + const masterAccount = new Account( + rpcProvider, + import.meta.env.VITE_PUBLIC_MASTER_ADDRESS!, + import.meta.env.VITE_PUBLIC_MASTER_PRIVATE_KEY! + ); + + const burnerManager = new BurnerManager({ + masterAccount, + accountClassHash: import.meta.env.VITE_PUBLIC_ACCOUNT_CLASS_HASH!, + rpcProvider, + }); + + burnerManager.create(); + + burnerManager.init(); + + return { + account: burnerManager.account as Account, + burnerManager, + }; +}; diff --git a/examples/react-phaser-example/src/dojo/createClientComponents.ts b/examples/react-phaser-example/src/dojo/createClientComponents.ts index fba197bc..12ef0456 100644 --- a/examples/react-phaser-example/src/dojo/createClientComponents.ts +++ b/examples/react-phaser-example/src/dojo/createClientComponents.ts @@ -1,13 +1,13 @@ import { overridableComponent } from "@dojoengine/recs"; import { SetupNetworkResult } from "./setupNetwork"; - export type ClientComponents = ReturnType; -export function createClientComponents({ contractComponents }: SetupNetworkResult) { +export function createClientComponents({ + contractComponents, +}: SetupNetworkResult) { return { ...contractComponents, Position: overridableComponent(contractComponents.Position), - Moves: overridableComponent(contractComponents.Moves), }; -} \ No newline at end of file +} diff --git a/examples/react-phaser-example/src/dojo/createNetworkLayer.ts b/examples/react-phaser-example/src/dojo/createNetworkLayer.ts index 5d6a3f34..484d71fa 100644 --- a/examples/react-phaser-example/src/dojo/createNetworkLayer.ts +++ b/examples/react-phaser-example/src/dojo/createNetworkLayer.ts @@ -1,53 +1,48 @@ import { world } from "./world"; import { setup } from "./setup"; -import { Account, RpcProvider } from "starknet"; -import { BurnerManager } from "@dojoengine/create-burner"; -import { SyncManager } from "@dojoengine/react"; +import { Account } from "starknet"; +import { createSyncManager } from "@dojoengine/react"; export type NetworkLayer = Awaited>; export const createNetworkLayer = async () => { const { components, systemCalls, network } = await setup(); - const rpcProvider = new RpcProvider({ - nodeUrl: import.meta.env.VITE_PUBLIC_NODE_URL!, - }); - - const masterAccount = new Account( - rpcProvider, - import.meta.env.VITE_PUBLIC_MASTER_ADDRESS!, - import.meta.env.VITE_PUBLIC_MASTER_PRIVATE_KEY! - ); - - const burnerManager = new BurnerManager({ - masterAccount, - accountClassHash: import.meta.env.VITE_PUBLIC_ACCOUNT_CLASS_HASH!, - rpcProvider, - }); - - // TODO: Currently if you change wallets in the UI, phaser will not update. - burnerManager.init(); - - if (burnerManager.account) { - // sync manager to active address - new SyncManager(network.torii_client, [ - { + const initial_sync = () => { + const models: any = []; + + for (let i = 1; i <= 20; i++) { + models.push({ model: network.contractComponents.Position, - keys: [burnerManager.account?.address], - }, - { - model: network.contractComponents.Moves as any, - keys: [burnerManager.account?.address], - }, - ]); - } + keys: [i.toString()], + }); + models.push({ + model: network.contractComponents.RPSType, + keys: [i.toString()], + }); + models.push({ + model: network.contractComponents.PlayerID, + keys: [i.toString()], + }); + models.push({ + model: network.contractComponents.Energy, + keys: [i.toString()], + }); + } + + return models; + }; + + const { sync } = createSyncManager(network.torii_client, initial_sync()); + + sync(); return { world, components, systemCalls, network, - account: burnerManager.account as Account, - burnerManager, + account: network.burnerManager.account as Account, + burnerManage: network.burnerManager, }; }; diff --git a/examples/react-phaser-example/src/dojo/createSystemCalls.ts b/examples/react-phaser-example/src/dojo/createSystemCalls.ts index 5ad180cd..12e61f0a 100644 --- a/examples/react-phaser-example/src/dojo/createSystemCalls.ts +++ b/examples/react-phaser-example/src/dojo/createSystemCalls.ts @@ -1,10 +1,11 @@ import { SetupNetworkResult } from "./setupNetwork"; -import { Account, AccountInterface } from "starknet"; +import { Account } from "starknet"; import { Entity, getComponentValue } from "@dojoengine/recs"; import { uuid } from "@latticexyz/utils"; import { ClientComponents } from "./createClientComponents"; -import { Direction, updatePositionWithDirection } from "../utils"; +import { Direction, updatePositionWithDirection } from "./utils"; import { getEvents, setComponentsFromEvents } from "@dojoengine/utils"; +import { RPSSprites } from "../phaser/config/constants"; export type SystemCalls = ReturnType; @@ -12,42 +13,45 @@ export interface SystemSigner { signer: Account; } +export interface SpawnSystemProps extends SystemSigner { + rps: RPSSprites; +} + export interface MoveSystemProps extends SystemSigner { direction: Direction; } export function createSystemCalls( { execute, contractComponents }: SetupNetworkResult, - { Position, Moves }: ClientComponents + { Position, PlayerID }: ClientComponents ) { - const spawn = async (props: SystemSigner) => { - const signer = props.signer; - - console.log("spawn", signer.address); - const entityId = signer.address.toString() as Entity; - - const positionId = uuid(); - Position.addOverride(positionId, { - entity: entityId, - value: { player: BigInt(entityId), vec: { x: 10, y: 10 } }, - }); - - const movesId = uuid(); - Moves.addOverride(movesId, { - entity: entityId, - value: { - player: BigInt(entityId), - remaining: 100, - last_direction: 0, - }, - }); + const spawn = async (props: SpawnSystemProps) => { + const { signer, rps } = props; + + // const entityId = signer.address.toString() as Entity; + + // const positionId = uuid(); + // Position.addOverride(positionId, { + // entity: entityId, + // value: { player: BigInt(entityId), vec: { x: 10, y: 10 } }, + // }); + + // const movesId = uuid(); + // Moves.addOverride(movesId, { + // entity: entityId, + // value: { + // player: BigInt(entityId), + // remaining: 100, + // last_direction: 0, + // }, + // }); try { const { transaction_hash } = await execute( signer, "actions", "spawn", - [] + [rps] ); setComponentsFromEvents( @@ -60,41 +64,44 @@ export function createSystemCalls( ); } catch (e) { console.log(e); - Position.removeOverride(positionId); - Moves.removeOverride(movesId); + // Position.removeOverride(positionId); + // Moves.removeOverride(movesId); } finally { - Position.removeOverride(positionId); - Moves.removeOverride(movesId); + // Position.removeOverride(positionId); + // Moves.removeOverride(movesId); } }; + // NOTE: Optimistic updates only work when 1 owner enttiy exists const move = async (props: MoveSystemProps) => { const { signer, direction } = props; - const entityId = signer.address.toString() as Entity; - - const positionId = uuid(); - Position.addOverride(positionId, { - entity: entityId, - value: { - player: BigInt(entityId), - vec: updatePositionWithDirection( - direction, - // currently recs does not support nested values so we use any here - getComponentValue(Position, entityId) as any - ).vec, - }, - }); - - const movesId = uuid(); - Moves.addOverride(movesId, { - entity: entityId, - value: { - player: BigInt(entityId), - remaining: - (getComponentValue(Moves, entityId)?.remaining || 0) - 1, - }, - }); + console.log(signer); + + const playerId = getComponentValue( + PlayerID, + signer.address.toString() as Entity + ); + + // const currentPosition = getComponentValue( + // Position, + // playerId?.id.toString() as Entity + // ) || { x: 0, y: 0 }; + + // const newPosition = updatePositionWithDirection(direction, { + // x: currentPosition["x"], + // y: currentPosition["y"], + // }); + + // const positionId = uuid(); + // Position.addOverride(positionId, { + // entity: playerId?.id.toString() as Entity, + // value: { + // id: 1, + // x: newPosition["x"], + // y: newPosition["y"], + // }, + // }); try { const { transaction_hash } = await execute( @@ -104,21 +111,27 @@ export function createSystemCalls( [direction] ); - setComponentsFromEvents( - contractComponents, - getEvents( - await signer.waitForTransaction(transaction_hash, { - retryInterval: 100, - }) - ) + console.log( + await signer.waitForTransaction(transaction_hash, { + retryInterval: 100, + }) ); + + // setComponentsFromEvents( + // contractComponents, + // getEvents( + // await signer.waitForTransaction(transaction_hash, { + // retryInterval: 100, + // }) + // ) + // ); } catch (e) { console.log(e); - Position.removeOverride(positionId); - Moves.removeOverride(movesId); + // Position.removeOverride(positionId); + // Moves.removeOverride(movesId); } finally { - Position.removeOverride(positionId); - Moves.removeOverride(movesId); + // Position.removeOverride(positionId); + // Moves.removeOverride(movesId); } }; diff --git a/examples/react-phaser-example/src/dojo/setupNetwork.ts b/examples/react-phaser-example/src/dojo/setupNetwork.ts index d466fc66..16e5e345 100644 --- a/examples/react-phaser-example/src/dojo/setupNetwork.ts +++ b/examples/react-phaser-example/src/dojo/setupNetwork.ts @@ -2,13 +2,13 @@ import { defineContractComponents } from "./contractComponents"; import { world } from "./world"; import { RPCProvider } from "@dojoengine/core"; import { Account, num } from "starknet"; -import manifest from "../../../dojo-starter/target/dev/manifest.json"; +import manifest from "../../../emojiman/target/dev/manifest.json"; import * as torii from "@dojoengine/torii-client"; +import { createBurner } from "./createBurner"; export type SetupNetworkResult = Awaited>; export async function setupNetwork() { - // Extract environment variables for better readability. const { VITE_PUBLIC_WORLD_ADDRESS, VITE_PUBLIC_NODE_URL, @@ -28,11 +28,15 @@ export async function setupNetwork() { worldAddress: VITE_PUBLIC_WORLD_ADDRESS, }); + const { account, burnerManager } = await createBurner(); + // Return the setup object. return { provider, world, torii_client, + account, + burnerManager, // Define contract components for the world. contractComponents: defineContractComponents(world), @@ -46,5 +50,7 @@ export async function setupNetwork() { ) => { return provider.execute(signer, contract, system, call_data); }, + + // sync, }; } diff --git a/examples/react-phaser-example/src/utils/index.ts b/examples/react-phaser-example/src/dojo/utils/index.ts similarity index 75% rename from examples/react-phaser-example/src/utils/index.ts rename to examples/react-phaser-example/src/dojo/utils/index.ts index 1ec8b000..20888569 100644 --- a/examples/react-phaser-example/src/utils/index.ts +++ b/examples/react-phaser-example/src/dojo/utils/index.ts @@ -7,20 +7,20 @@ export enum Direction { export function updatePositionWithDirection( direction: Direction, - value: { vec: { x: number; y: number } } + value: { x: number; y: number } ) { switch (direction) { case Direction.Left: - value.vec.x--; + value.x--; break; case Direction.Right: - value.vec.x++; + value.x++; break; case Direction.Up: - value.vec.y--; + value.y--; break; case Direction.Down: - value.vec.y++; + value.y++; break; default: throw new Error("Invalid direction provided"); diff --git a/examples/react-phaser-example/src/hooks/useDojo.tsx b/examples/react-phaser-example/src/hooks/useDojo.tsx index 9528d619..e7c993b6 100644 --- a/examples/react-phaser-example/src/hooks/useDojo.tsx +++ b/examples/react-phaser-example/src/hooks/useDojo.tsx @@ -4,8 +4,6 @@ import { PhaserLayer } from "../phaser"; import { store } from "../store/store"; import { useBurnerManager } from "@dojoengine/create-burner"; -export type UIStore = ReturnType; - export const useDojo = () => { const { networkLayer, phaserLayer } = store(); @@ -15,7 +13,7 @@ export const useDojo = () => { const { account, get, create, select, list, isDeploying, clear } = useBurnerManager({ - burnerManager: networkLayer.burnerManager, + burnerManager: networkLayer.burnerManage, }); return { diff --git a/examples/react-phaser-example/src/hooks/useNetworkLayer.tsx b/examples/react-phaser-example/src/hooks/useNetworkLayer.tsx index 796598b5..78728f68 100644 --- a/examples/react-phaser-example/src/hooks/useNetworkLayer.tsx +++ b/examples/react-phaser-example/src/hooks/useNetworkLayer.tsx @@ -9,9 +9,11 @@ export const useNetworkLayer = () => { useEffect(() => { return () => { - networkLayerPromise.then((networkLayer) => networkLayer.world.dispose()); + networkLayerPromise.then((networkLayer) => + networkLayer.world.dispose() + ); }; }, [networkLayerPromise]); return usePromiseValue(networkLayerPromise); -}; \ No newline at end of file +}; diff --git a/examples/react-phaser-example/src/hooks/usePhaserLayer.tsx b/examples/react-phaser-example/src/hooks/usePhaserLayer.tsx index 0877c572..7f7525b9 100644 --- a/examples/react-phaser-example/src/hooks/usePhaserLayer.tsx +++ b/examples/react-phaser-example/src/hooks/usePhaserLayer.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { createPhaserLayer } from "../phaser"; import { NetworkLayer } from "../dojo/createNetworkLayer"; -import { phaserConfig } from "../phaser/configurePhaser"; +import { phaserConfig } from "../phaser/config/configurePhaser"; import { usePromiseValue } from "./usePromiseValue"; type Props = { @@ -17,7 +17,6 @@ const createContainer = () => { return container; }; - export const usePhaserLayer = ({ networkLayer }: Props) => { const parentRef = useRef(null); const [{ width, height }, setSize] = useState({ width: 0, height: 0 }); @@ -43,13 +42,13 @@ export const usePhaserLayer = ({ networkLayer }: Props) => { }, }), }; - - // We don't want width/height to recreate phaser layer, so we ignore linter }, [networkLayer]); useEffect(() => { return () => { - phaserLayerPromise?.then((phaserLayer) => phaserLayer.world.dispose()); + phaserLayerPromise?.then((phaserLayer) => + phaserLayer.world.dispose() + ); container?.remove(); }; }, [container, phaserLayerPromise]); @@ -71,4 +70,4 @@ export const usePhaserLayer = ({ networkLayer }: Props) => { ); return useMemo(() => ({ ref, phaserLayer }), [ref, phaserLayer]); -} \ No newline at end of file +}; diff --git a/examples/react-phaser-example/src/index.css b/examples/react-phaser-example/src/index.css index bd6213e1..e4e24946 100644 --- a/examples/react-phaser-example/src/index.css +++ b/examples/react-phaser-example/src/index.css @@ -1,3 +1,76 @@ @tailwind base; @tailwind components; -@tailwind utilities; \ No newline at end of file +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 20 14.3% 4.1%; + + --card: 0 0% 100%; + --card-foreground: 20 14.3% 4.1%; + + --popover: 0 0% 100%; + --popover-foreground: 20 14.3% 4.1%; + + --primary: 24 9.8% 10%; + --primary-foreground: 60 9.1% 97.8%; + + --secondary: 60 4.8% 95.9%; + --secondary-foreground: 24 9.8% 10%; + + --muted: 60 4.8% 95.9%; + --muted-foreground: 25 5.3% 44.7%; + + --accent: 60 4.8% 95.9%; + --accent-foreground: 24 9.8% 10%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 60 9.1% 97.8%; + + --border: 20 5.9% 90%; + --input: 20 5.9% 90%; + --ring: 20 14.3% 4.1%; + + --radius: 0.5rem; + } + + .dark { + --background: 20 14.3% 4.1%; + --foreground: 60 9.1% 97.8%; + + --card: 20 14.3% 4.1%; + --card-foreground: 60 9.1% 97.8%; + + --popover: 20 14.3% 4.1%; + --popover-foreground: 60 9.1% 97.8%; + + --primary: 60 9.1% 97.8%; + --primary-foreground: 24 9.8% 10%; + + --secondary: 12 6.5% 15.1%; + --secondary-foreground: 60 9.1% 97.8%; + + --muted: 12 6.5% 15.1%; + --muted-foreground: 24 5.4% 63.9%; + + --accent: 12 6.5% 15.1%; + --accent-foreground: 60 9.1% 97.8%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 60 9.1% 97.8%; + + --border: 12 6.5% 15.1%; + --input: 12 6.5% 15.1%; + --ring: 24 5.7% 82.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} \ No newline at end of file diff --git a/examples/react-phaser-example/src/lib/utils.ts b/examples/react-phaser-example/src/lib/utils.ts new file mode 100644 index 00000000..ec79801f --- /dev/null +++ b/examples/react-phaser-example/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/examples/react-phaser-example/src/phaser/configurePhaser.ts b/examples/react-phaser-example/src/phaser/config/configurePhaser.ts similarity index 70% rename from examples/react-phaser-example/src/phaser/configurePhaser.ts rename to examples/react-phaser-example/src/phaser/config/configurePhaser.ts index 7c46f9ab..d4656d73 100644 --- a/examples/react-phaser-example/src/phaser/configurePhaser.ts +++ b/examples/react-phaser-example/src/phaser/config/configurePhaser.ts @@ -5,8 +5,8 @@ import { defineMapConfig, defineCameraConfig, } from "@latticexyz/phaserx"; -import worldTileset from "../assets/tilesets/world.png"; -import { TileAnimations, Tileset } from "../artTypes/world"; +import landTileset from "../../assets/tilesets/land.png"; +import { TileAnimations, Tileset } from "../../assets/world"; import { Sprites, Assets, @@ -23,7 +23,7 @@ const mainMap = defineMapConfig({ chunkSize: TILE_WIDTH * 64, // tile size * tile amount tileWidth: TILE_WIDTH, tileHeight: TILE_HEIGHT, - backgroundTile: [Tileset.Grass], + backgroundTile: [Tileset.Land], animationInterval: ANIMATION_INTERVAL, tileAnimations: TileAnimations, layers: { @@ -42,7 +42,7 @@ export const phaserConfig = { [Assets.Tileset]: { type: AssetType.Image, key: Assets.Tileset, - path: worldTileset, + path: landTileset, }, [Assets.MainAtlas]: { type: AssetType.MultiAtlas, @@ -65,13 +65,33 @@ export const phaserConfig = { }, animations: [ { - key: Animations.SwordsmanIdle, + key: Animations.RockIdle, assetKey: Assets.MainAtlas, startFrame: 0, - endFrame: 3, + endFrame: 0, frameRate: 6, repeat: -1, - prefix: "sprites/soldier/idle/", + prefix: "sprites/rock/", + suffix: ".png", + }, + { + key: Animations.ScissorsIdle, + assetKey: Assets.MainAtlas, + startFrame: 0, + endFrame: 0, + frameRate: 6, + repeat: -1, + prefix: "sprites/scissors/", + suffix: ".png", + }, + { + key: Animations.PaperIdle, + assetKey: Assets.MainAtlas, + startFrame: 0, + endFrame: 0, + frameRate: 6, + repeat: -1, + prefix: "sprites/paper/", suffix: ".png", }, ], diff --git a/examples/react-phaser-example/src/phaser/constants.ts b/examples/react-phaser-example/src/phaser/config/constants.ts similarity index 74% rename from examples/react-phaser-example/src/phaser/constants.ts rename to examples/react-phaser-example/src/phaser/config/constants.ts index 149cb9e1..26638e01 100644 --- a/examples/react-phaser-example/src/phaser/constants.ts +++ b/examples/react-phaser-example/src/phaser/config/constants.ts @@ -7,12 +7,21 @@ export enum Maps { } export enum Animations { - SwordsmanIdle = "SwordsmanIdle", + RockIdle = "RockIdle", + PaperIdle = "PaperIdle", + ScissorsIdle = "ScissorsIdle", } + export enum Sprites { Soldier, } +export enum RPSSprites { + Rock, + Paper, + Scissors, +} + export enum Assets { MainAtlas = "MainAtlas", Tileset = "Tileset", diff --git a/examples/react-phaser-example/src/phaser/index.ts b/examples/react-phaser-example/src/phaser/index.ts index 9637c8c7..8ce5f158 100644 --- a/examples/react-phaser-example/src/phaser/index.ts +++ b/examples/react-phaser-example/src/phaser/index.ts @@ -20,8 +20,8 @@ export const createPhaserLayer = async ( const { camera } = scenes.Main; - camera.phaserCamera.setBounds(-1000, -1000, 2000, 2000); - camera.phaserCamera.centerOn(0, 0); + camera.phaserCamera.setBounds(0, 0, 5000, 5000); + camera.phaserCamera.centerOn(1500, 1500); const components = {}; diff --git a/examples/react-phaser-example/src/phaser/phaserLayer.tsx b/examples/react-phaser-example/src/phaser/phaserLayer.tsx index 1d8b850c..fc221b78 100644 --- a/examples/react-phaser-example/src/phaser/phaserLayer.tsx +++ b/examples/react-phaser-example/src/phaser/phaserLayer.tsx @@ -1,15 +1,14 @@ import { useEffect } from "react"; import { NetworkLayer } from "../dojo/createNetworkLayer"; -import { store } from "../store/store"; +import { store, useUIStore } from "../store/store"; import { usePhaserLayer } from "../hooks/usePhaserLayer"; type Props = { networkLayer: NetworkLayer | null; }; -// TODO: this is where we need to set the burner account from local storage. export const PhaserLayer = ({ networkLayer }: Props) => { - + const loggedIn = useUIStore((state: any) => state.loggedIn); const { phaserLayer, ref } = usePhaserLayer({ networkLayer }); useEffect(() => { @@ -18,7 +17,7 @@ export const PhaserLayer = ({ networkLayer }: Props) => { console.log("Setting phaser layer"); } - }, [phaserLayer]); + }, [phaserLayer, loggedIn]); return (
{ }} /> ); -}; \ No newline at end of file +}; diff --git a/examples/react-phaser-example/src/phaser/systems/camera.ts b/examples/react-phaser-example/src/phaser/systems/camera.ts new file mode 100644 index 00000000..597326c9 --- /dev/null +++ b/examples/react-phaser-example/src/phaser/systems/camera.ts @@ -0,0 +1,13 @@ +import { PhaserLayer } from ".."; + +export const camera = (layer: PhaserLayer) => { + const { + scenes: { + Main: { + camera: { phaserCamera }, + }, + }, + } = layer; + + phaserCamera.centerOn(0, 0); +}; diff --git a/examples/react-phaser-example/src/phaser/systems/controls.ts b/examples/react-phaser-example/src/phaser/systems/controls.ts index 7f0cf57b..d7518530 100644 --- a/examples/react-phaser-example/src/phaser/systems/controls.ts +++ b/examples/react-phaser-example/src/phaser/systems/controls.ts @@ -1,5 +1,5 @@ import { PhaserLayer } from ".."; -import { Direction } from "../../utils"; +import { Direction } from "../../dojo/utils"; export const controls = (layer: PhaserLayer) => { const { diff --git a/examples/react-phaser-example/src/phaser/systems/mapSystem.ts b/examples/react-phaser-example/src/phaser/systems/mapSystem.ts index 551f0a1c..eb979d97 100644 --- a/examples/react-phaser-example/src/phaser/systems/mapSystem.ts +++ b/examples/react-phaser-example/src/phaser/systems/mapSystem.ts @@ -1,4 +1,4 @@ -import { Tileset } from "../../artTypes/world"; +import { Tileset } from "../../assets/world"; import { PhaserLayer } from ".."; import { createNoise2D } from "simplex-noise"; @@ -20,12 +20,18 @@ export function mapSystem(layer: PhaserLayer) { const coord = { x, y }; const seed = noise(x, y); - putTileAt(coord, Tileset.Grass, "Background"); - - if (seed >= 0.45) { - putTileAt(coord, Tileset.Mountains, "Foreground"); - } else if (seed < -0.45) { + if (seed > 0.9) { + // This would be the highest 'elevation' + putTileAt(coord, Tileset.Desert, "Foreground"); + } else if (seed > 0.3) { + // Even lower, could be fields or plains + putTileAt(coord, Tileset.Sea, "Foreground"); + } else if (seed > 0.1) { + // Close to water level, might be beach putTileAt(coord, Tileset.Forest, "Foreground"); + } else { + // Below a certain threshold, it is sea + putTileAt(coord, Tileset.Land, "Foreground"); } } } diff --git a/examples/react-phaser-example/src/phaser/systems/move.ts b/examples/react-phaser-example/src/phaser/systems/move.ts index e1ea335d..4fd4e62c 100644 --- a/examples/react-phaser-example/src/phaser/systems/move.ts +++ b/examples/react-phaser-example/src/phaser/systems/move.ts @@ -1,54 +1,89 @@ -import { Entity, Has, defineEnterSystem, defineSystem, getComponentValueStrict } from "@dojoengine/recs"; +import { + Entity, + Has, + defineEnterSystem, + defineSystem, + getComponentValueStrict, + getComponentValue, +} from "@dojoengine/recs"; import { PhaserLayer } from ".."; import { tileCoordToPixelCoord } from "@latticexyz/phaserx"; -import { Animations, TILE_HEIGHT, TILE_WIDTH } from "../constants"; +import { + Animations, + RPSSprites, + TILE_HEIGHT, + TILE_WIDTH, +} from "../config/constants"; export const move = (layer: PhaserLayer) => { - const { world, scenes: { Main: { objectPool, camera }, }, networkLayer: { - components: { Position } + components: { Position, RPSType, PlayerID }, }, } = layer; - defineEnterSystem(world, [Has(Position)], ({ entity }: any) => { - const playerObj = objectPool.get(entity.toString(), "Sprite"); + defineEnterSystem( + world, + [Has(Position), Has(RPSType)], + ({ entity }: any) => { + const playerObj = objectPool.get(entity.toString(), "Sprite"); - console.log(playerObj) + const type = getComponentValue( + RPSType, + entity.toString() as Entity + ); - playerObj.setComponent({ - id: 'animation', - once: (sprite: any) => { + console.log("defineEnterSystem", type); - console.log(sprite) - sprite.play(Animations.SwordsmanIdle); - } - }); - }); + let animation = Animations.RockIdle; - defineSystem(world, [Has(Position)], ({ entity }: any) => { + switch (type?.rps) { + case RPSSprites.Rock: + animation = Animations.RockIdle; + break; + case RPSSprites.Paper: + animation = Animations.PaperIdle; + break; + case RPSSprites.Scissors: + animation = Animations.ScissorsIdle; + break; + } - console.log(entity) + playerObj.setComponent({ + id: "animation", + once: (sprite: any) => { + sprite.play(animation); + }, + }); + } + ); - const position = getComponentValueStrict(Position, entity.toString() as Entity); + defineSystem(world, [Has(Position)], ({ entity }: any) => { + const position = getComponentValueStrict( + Position, + entity.toString() as Entity + ); - const offsetPosition = { x: position?.vec.x, y: position?.vec.y }; + const offsetPosition = { x: position?.x, y: position?.y }; - const pixelPosition = tileCoordToPixelCoord(offsetPosition, TILE_WIDTH, TILE_HEIGHT); + const pixelPosition = tileCoordToPixelCoord( + offsetPosition, + TILE_WIDTH, + TILE_HEIGHT + ); - const player = objectPool.get(entity, "Sprite") + const player = objectPool.get(entity, "Sprite"); player.setComponent({ - id: 'position', + id: "position", once: (sprite: any) => { sprite.setPosition(pixelPosition?.x, pixelPosition?.y); camera.centerOn(pixelPosition?.x, pixelPosition?.y); - } - }) - + }, + }); }); -}; \ No newline at end of file +}; diff --git a/examples/react-phaser-example/src/phaser/systems/registerSystems.ts b/examples/react-phaser-example/src/phaser/systems/registerSystems.ts index 7e9a95da..95df0c48 100644 --- a/examples/react-phaser-example/src/phaser/systems/registerSystems.ts +++ b/examples/react-phaser-example/src/phaser/systems/registerSystems.ts @@ -1,12 +1,12 @@ import { PhaserLayer } from ".."; import { move } from "./move"; -import { spawn } from "./spawn"; import { controls } from "./controls"; import { mapSystem } from "./mapSystem"; +import { camera } from "./camera"; export const registerSystems = (layer: PhaserLayer) => { move(layer); - spawn(layer); controls(layer); - mapSystem(layer) -}; \ No newline at end of file + mapSystem(layer); + camera(layer); +}; diff --git a/examples/react-phaser-example/src/phaser/systems/spawn.ts b/examples/react-phaser-example/src/phaser/systems/spawn.ts deleted file mode 100644 index 88f77fa3..00000000 --- a/examples/react-phaser-example/src/phaser/systems/spawn.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { PhaserLayer } from ".."; - -export const spawn = (layer: PhaserLayer) => { - const { - scenes: { - Main: { input }, - }, - networkLayer: { - systemCalls: { spawn }, - account, - }, - } = layer; - - input.onKeyPress( - (keys) => keys.has("SPACE"), - () => { - spawn({ signer: account }); - } - ); -}; diff --git a/examples/react-phaser-example/src/store/store.ts b/examples/react-phaser-example/src/store/store.ts index 05794064..c677b9de 100644 --- a/examples/react-phaser-example/src/store/store.ts +++ b/examples/react-phaser-example/src/store/store.ts @@ -9,6 +9,10 @@ export type Store = { export const store = create(() => ({ networkLayer: null, - phaserLayer: null + phaserLayer: null, })); +export const useUIStore = create((set) => ({ + loggedIn: false, + setLoggedIn: () => set(() => ({ loggedIn: true })), +})); diff --git a/examples/react-phaser-example/src/ui/CreateAccount.tsx b/examples/react-phaser-example/src/ui/CreateAccount.tsx new file mode 100644 index 00000000..d07fc24b --- /dev/null +++ b/examples/react-phaser-example/src/ui/CreateAccount.tsx @@ -0,0 +1,21 @@ +import { useUIStore } from "../store/store"; +import { WalletConnect } from "./WalletConnect"; + +export const CreateAccount = () => { + const loggedIn = useUIStore((state: any) => state.loggedIn); + return ( +
+
+

RPS

+
Eat to survive
+
+ +
+
+
+ ); +}; diff --git a/examples/react-phaser-example/src/ui/GameState.tsx b/examples/react-phaser-example/src/ui/GameState.tsx new file mode 100644 index 00000000..e69de29b diff --git a/examples/react-phaser-example/src/ui/Spawn.tsx b/examples/react-phaser-example/src/ui/Spawn.tsx new file mode 100644 index 00000000..8a19e6ec --- /dev/null +++ b/examples/react-phaser-example/src/ui/Spawn.tsx @@ -0,0 +1,66 @@ +import { useDojo } from "../hooks/useDojo"; +import { RPSSprites } from "../phaser/config/constants"; +import { ClickWrapper } from "./ClickWrapper"; +import { Button } from "../components/ui/button"; +import { useUIStore } from "../store/store"; +import { useEffect } from "react"; + +export const Spawn = () => { + const setLoggedIn = useUIStore((state: any) => state.setLoggedIn); + const { + account: { account }, + systemCalls: { spawn }, + } = useDojo(); + + // useEffect(() => { + // if (isDeploying) { + // return; + // } + + // if (account) { + // return; + // } + + // (async () => { + // const accounts = await list(); + + // if (accounts.length === 0) { + // await create(); + // } else { + // await select(accounts[0].address); + // } + // })(); + // }, [account]); + + if (!account) { + return
Deploying...
; + } + + return ( + +
+ {Object.keys(RPSSprites) + .filter((key) => isNaN(Number(key))) + .map((key) => ( +
+ +
+ ))} +
+
+ ); +}; diff --git a/examples/react-phaser-example/src/ui/WalletConnect.tsx b/examples/react-phaser-example/src/ui/WalletConnect.tsx new file mode 100644 index 00000000..a4b3ed7b --- /dev/null +++ b/examples/react-phaser-example/src/ui/WalletConnect.tsx @@ -0,0 +1,14 @@ +import { ClickWrapper } from "./ClickWrapper"; +import { Spawn } from "./Spawn"; + +export const WalletConnect = () => { + return ( + +
+
+ +
+
+
+ ); +}; diff --git a/examples/react-phaser-example/src/ui/index.tsx b/examples/react-phaser-example/src/ui/index.tsx index 438b6022..8702ba96 100644 --- a/examples/react-phaser-example/src/ui/index.tsx +++ b/examples/react-phaser-example/src/ui/index.tsx @@ -1,7 +1,5 @@ -import styled from "styled-components"; import { store } from "../store/store"; -import { Wrapper } from "./wrapper"; -import { SpawnBtn } from "./spawnbtn"; +import { CreateAccount } from "./CreateAccount"; export const UI = () => { const layers = store((state) => { @@ -14,10 +12,8 @@ export const UI = () => { if (!layers.networkLayer || !layers.phaserLayer) return <>; return ( - -
- -
-
+
+ +
); }; diff --git a/examples/react-phaser-example/src/ui/spawnbtn.tsx b/examples/react-phaser-example/src/ui/spawnbtn.tsx deleted file mode 100644 index 9befcaf9..00000000 --- a/examples/react-phaser-example/src/ui/spawnbtn.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { useDojo } from "../hooks/useDojo"; -import { ClickWrapper } from "./clickWrapper"; - -export const SpawnBtn = () => { - const { - account: { account, create, isDeploying, select, list, clear }, - systemCalls: { spawn }, - } = useDojo(); - - return ( - -
-
- - -
- -
-
signer:
- - -
-
- -
-
-
- ); -}; diff --git a/examples/react-phaser-example/src/ui/wrapper.tsx b/examples/react-phaser-example/src/ui/wrapper.tsx deleted file mode 100644 index 8f145635..00000000 --- a/examples/react-phaser-example/src/ui/wrapper.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import styled from "styled-components"; - -export const Wrapper = styled.div` - position: absolute; - inset: 0; - pointer-events: none; -`; \ No newline at end of file diff --git a/examples/react-phaser-example/tailwind.config.js b/examples/react-phaser-example/tailwind.config.js index 40eda665..0377ea1d 100644 --- a/examples/react-phaser-example/tailwind.config.js +++ b/examples/react-phaser-example/tailwind.config.js @@ -1,8 +1,76 @@ /** @type {import('tailwindcss').Config} */ -export default { - content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], - theme: { - extend: {}, +module.exports = { + darkMode: ["class"], + content: [ + './pages/**/*.{ts,tsx}', + './components/**/*.{ts,tsx}', + './app/**/*.{ts,tsx}', + './src/**/*.{ts,tsx}', + ], + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, }, - plugins: [], -}; + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "accordion-down": { + from: { height: 0 }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: 0 }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, + }, + }, + plugins: [require("tailwindcss-animate")], +} \ No newline at end of file diff --git a/examples/react-phaser-example/tsconfig.json b/examples/react-phaser-example/tsconfig.json index 22a01748..a056980a 100644 --- a/examples/react-phaser-example/tsconfig.json +++ b/examples/react-phaser-example/tsconfig.json @@ -26,6 +26,12 @@ "include": [ "src" ], + "baseUrl": ".", + "paths": { + "@/*": [ + "./src/*" + ] + }, "references": [ { "path": "./tsconfig.node.json" diff --git a/examples/react-phaser-example/vite.config.ts b/examples/react-phaser-example/vite.config.ts index a77004fe..36a2dcd8 100644 --- a/examples/react-phaser-example/vite.config.ts +++ b/examples/react-phaser-example/vite.config.ts @@ -1,3 +1,4 @@ +import path from "path"; import { defineConfig } from "vite"; import wasm from "vite-plugin-wasm"; import react from "@vitejs/plugin-react"; @@ -6,4 +7,9 @@ import topLevelAwait from "vite-plugin-top-level-await"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react(), wasm(), topLevelAwait()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, }); diff --git a/package.json b/package.json index 7487fbbb..1329e5a3 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,28 @@ { - "name": "dojo-packages", - "version": "1.0.0", - "workspaces": [ - "examples/react-app", - "examples/react-phaser-example", - "packages/create-burner", - "packages/core", - "packages/utils", - "packages/torii-client", - "packages/torii-wasm", - "packages/react" - ], - "devDependencies": { - "prettier": "^3.0.3" - }, - "scripts": { - "build-core": "bun run --cwd packages/core build", - "build-create-burner": "bun run --cwd packages/create-burner build", - "build-utils": "bun run --cwd packages/utils build", - "build-torii-client": "bun run --cwd packages/torii-client build", - "build-torii-wasm": "bun run --cwd packages/torii-wasm build", - "build-react": "bun run --cwd packages/react build", - "build-phaser": "bun run --cwd examples/react-phaser-example build", - "build": "bun run build-core && bun run build-create-burner && bun run build-utils && bun run build-torii-client && bun run build-torii-wasm", - "clean": "rm -rf node_modules packages/create-burner/node_modules packages/core/node_modules packages/utils/node_modules packages/torii-client/node_modules packages/torii-wasm/node_modules packages/react/node_modules bun.lockb packages/create-burner/bun.lockb packages/core/bun.lockb packages/utils/bun.lockb packages/torii-client/bun.lockb packages/torii-wasm/bun.lockb packages/react/bun.lockb && rm -rf examples/react-app/node_modules examples/react-app/bun.lockb examples/react-phaser-example/node_modules examples/react-phaser-example/bun.lockb" - } + "name": "dojo-packages", + "version": "1.0.0", + "workspaces": [ + "examples/react-app", + "examples/react-phaser-example", + "packages/create-burner", + "packages/core", + "packages/utils", + "packages/torii-client", + "packages/torii-wasm", + "packages/react" + ], + "devDependencies": { + "prettier": "^3.0.3" + }, + "scripts": { + "build-core": "bun run --cwd packages/core build", + "build-create-burner": "bun run --cwd packages/create-burner build", + "build-utils": "bun run --cwd packages/utils build", + "build-torii-client": "bun run --cwd packages/torii-client build", + "build-torii-wasm": "bun run --cwd packages/torii-wasm build", + "build-react": "bun run --cwd packages/react build", + "build-phaser": "bun run --cwd examples/react-phaser-example build", + "build": "bun run build-core && bun run build-create-burner && bun run build-utils && bun run build react && bun run build-torii-wasm && bun run build-torii-client", + "clean": "rm -rf node_modules packages/create-burner/node_modules packages/core/node_modules packages/utils/node_modules packages/torii-client/node_modules packages/torii-wasm/node_modules packages/react/node_modules bun.lockb packages/create-burner/bun.lockb packages/core/bun.lockb packages/utils/bun.lockb packages/torii-client/bun.lockb packages/torii-wasm/bun.lockb packages/react/bun.lockb && rm -rf examples/react-app/node_modules examples/react-app/bun.lockb examples/react-phaser-example/node_modules examples/react-phaser-example/bun.lockb" + } } diff --git a/packages/react/src/syncManager/index.ts b/packages/react/src/syncManager/index.ts index c019356a..13653309 100644 --- a/packages/react/src/syncManager/index.ts +++ b/packages/react/src/syncManager/index.ts @@ -1 +1,3 @@ export { SyncManager } from "./syncManager"; +export { SubscribeManager } from "./subscribeManager"; +export { createSyncManager } from "./syncFunction"; diff --git a/packages/react/src/syncManager/subscribeManager.ts b/packages/react/src/syncManager/subscribeManager.ts new file mode 100644 index 00000000..cd73c983 --- /dev/null +++ b/packages/react/src/syncManager/subscribeManager.ts @@ -0,0 +1,59 @@ +import { Client } from "@dojoengine/torii-client"; +import { + Component, + Schema, + Metadata, + Entity, + setComponent, + ComponentValue, +} from "@dojoengine/recs"; +import { getEntityIdFromKeys } from "@dojoengine/utils"; +import { convertValues } from "../utils"; + +type ModelEntry = { + model: Component; + keys: any[]; +}; + +export class SubscribeManager { + private client: Client; + private modelEntries: ModelEntry[]; + + constructor(client: Client, modelEntries: ModelEntry[]) { + this.client = client; + this.modelEntries = modelEntries; + this.modelEntries.forEach((entry) => this.subscribeToModel(entry)); + } + + private async setModelValue(modelEntry: ModelEntry): Promise { + const { model, keys } = modelEntry; + const componentName = model.metadata?.name; + const keysToStrings = keys.map((key) => key.toString()); + const entityIndex: Entity | string = + keys.length === 1 ? keys[0].toString() : getEntityIdFromKeys(keys); + + try { + const modelValue = await this.client.getModelValue( + componentName! as string, + keysToStrings + ); + setComponent( + model, + entityIndex as Entity, + convertValues(model.schema, modelValue) as ComponentValue + ); + } catch (error) { + console.error("Failed to fetch or set model value:", error); + } + } + + private subscribeToModel(modelEntry: ModelEntry): void { + this.client.onSyncEntityChange( + { + model: modelEntry.model.metadata?.name! as string, + keys: modelEntry.keys.map((k) => k.toString()), + }, + () => this.setModelValue(modelEntry) + ); + } +} diff --git a/packages/react/src/syncManager/syncFunction.ts b/packages/react/src/syncManager/syncFunction.ts new file mode 100644 index 00000000..e508d9b4 --- /dev/null +++ b/packages/react/src/syncManager/syncFunction.ts @@ -0,0 +1,109 @@ +import { Client } from "@dojoengine/torii-client"; +import { + Component, + Schema, + Metadata, + Entity, + setComponent, +} from "@dojoengine/recs"; +import { getEntityIdFromKeys } from "@dojoengine/utils"; +import { convertValues } from "../utils"; + +type ModelEntry = { + model: Component; + keys: any[]; +}; + +export function createSyncManager( + client: Client, + modelEntries: ModelEntry[] +) { + async function fetchAndSetModelValue( + modelEntry: ModelEntry + ): Promise { + const { model, keys } = modelEntry; + const componentName = model.metadata?.name as string; + const keysToStrings = keys.map((key) => key.toString()); + const entityIndex: Entity = + keys.length === 1 ? keys[0].toString() : getEntityIdFromKeys(keys); + + try { + const modelValue = await client.getModelValue( + componentName, + keysToStrings + ); + console.log("sync modelValue", modelValue); + + const convertedValue = convertValues(model.schema, modelValue); + + console.log(convertedValue); + setComponent(model, entityIndex, convertedValue as any); + } catch (error) { + console.error("Failed to fetch or set model value:", error); + } + } + + function sync() { + modelEntries.forEach((modelEntry) => { + fetchAndSetModelValue(modelEntry); + client.addEntitiesToSync([ + { + model: modelEntry.model.metadata?.name as string, + keys: modelEntry.keys.map((k) => k.toString()), + }, + ]); + client.onSyncEntityChange( + { + model: modelEntry.model.metadata?.name! as string, + keys: modelEntry.keys.map((k) => k.toString()), + }, + () => { + client + .getModelValue( + modelEntry.model.metadata?.name! as string, + modelEntry.keys.map((k) => k.toString()) + ) + .then((modelValue) => { + console.log("ohayo", modelValue); + + const convertedValue = convertValues( + modelEntry.model.schema, + modelValue + ); + + const entityIndex: Entity = + modelEntry.keys.length === 1 + ? modelEntry.keys[0].toString() + : getEntityIdFromKeys(modelEntry.keys); + + setComponent( + modelEntry.model, + entityIndex, + convertedValue as any + ); + }); + } + ); + }); + } + + function cleanup() { + modelEntries.forEach((modelEntry) => { + client + .removeEntitiesToSync([ + { + model: modelEntry.model.metadata?.name as string, + keys: modelEntry.keys.map((k) => k.toString()), + }, + ]) + .catch((error) => { + console.error( + "Failed to remove entities on cleanup", + error + ); + }); + }); + } + + return { cleanup, sync }; +} diff --git a/packages/react/src/syncManager/syncManager.ts b/packages/react/src/syncManager/syncManager.ts index 4940a03b..9b5ac23b 100644 --- a/packages/react/src/syncManager/syncManager.ts +++ b/packages/react/src/syncManager/syncManager.ts @@ -9,7 +9,6 @@ import { import { getEntityIdFromKeys } from "@dojoengine/utils"; import { convertValues } from "../utils"; -// type KeyType = any; // Replace 'any' with your actual key type type ModelEntry = { model: Component; keys: any[]; @@ -44,6 +43,10 @@ export class SyncManager { componentName, keysToStrings ); + // TODO: + + console.log("modelValue", modelValue); + const convertedValue = convertValues(model.schema, modelValue); setComponent(model, entityIndex, convertedValue as any); } diff --git a/packages/react/src/utils.ts b/packages/react/src/utils.ts index 01a53fed..ca586229 100644 --- a/packages/react/src/utils.ts +++ b/packages/react/src/utils.ts @@ -14,9 +14,26 @@ export function convertValues(schema: Schema, values: any) { acc[key] = convertValues(schemaType, value); } else { // Otherwise, convert the value based on the schema type - // @ts-ignore - acc[key] = - schemaType === RecsType.BigInt ? BigInt(value) : Number(value); + + if (value == "Rock") { + // @ts-ignore + acc[key] = + schemaType === RecsType.BigInt ? BigInt(0) : Number(0); + } else if (value == "Paper") { + // @ts-ignore + acc[key] = + schemaType === RecsType.BigInt ? BigInt(1) : Number(1); + } else if (value == "Scissors") { + // @ts-ignore + acc[key] = + schemaType === RecsType.BigInt ? BigInt(2) : Number(2); + } else { + // @ts-ignore + acc[key] = + schemaType === RecsType.BigInt + ? BigInt(value) + : Number(value); + } } return acc; }, {}); diff --git a/packages/torii-wasm/crate/Cargo.lock b/packages/torii-wasm/crate/Cargo.lock index 89c883fc..ed05c3ff 100644 --- a/packages/torii-wasm/crate/Cargo.lock +++ b/packages/torii-wasm/crate/Cargo.lock @@ -1183,8 +1183,8 @@ dependencies = [ [[package]] name = "dojo-types" -version = "0.3.3" -source = "git+https://github.com/dojoengine/dojo?branch=main#29846bd8048dfc63ce170fc8ea86717b9a81738f" +version = "0.3.9" +source = "git+https://github.com/dojoengine/dojo?tag=v0.3.9#015e1d2027cef343aca6655f95cd6668305ca272" dependencies = [ "crypto-bigint", "hex", @@ -1199,8 +1199,8 @@ dependencies = [ [[package]] name = "dojo-world" -version = "0.3.3" -source = "git+https://github.com/dojoengine/dojo?branch=main#29846bd8048dfc63ce170fc8ea86717b9a81738f" +version = "0.3.9" +source = "git+https://github.com/dojoengine/dojo?tag=v0.3.9#015e1d2027cef343aca6655f95cd6668305ca272" dependencies = [ "anyhow", "async-trait", @@ -1693,6 +1693,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" + [[package]] name = "httparse" version = "1.8.0" @@ -4026,6 +4032,39 @@ dependencies = [ "syn 2.0.38", ] +[[package]] +name = "tonic-reflection" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fa37c513df1339d197f4ba21d28c918b9ef1ac1768265f11ecb6b7f1cba1b76" +dependencies = [ + "prost 0.12.1", + "prost-types 0.12.1", + "tokio", + "tokio-stream", + "tonic 0.10.2", +] + +[[package]] +name = "tonic-web" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fddb2a37b247e6adcb9f239f4e5cefdcc5ed526141a416b943929f13aea2cce" +dependencies = [ + "base64 0.21.5", + "bytes", + "http", + "http-body", + "hyper", + "pin-project", + "tokio-stream", + "tonic 0.10.2", + "tower-http", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tonic-web-wasm-client" version = "0.4.0" @@ -4052,8 +4091,8 @@ dependencies = [ [[package]] name = "torii-client" -version = "0.3.3" -source = "git+https://github.com/dojoengine/dojo?branch=main#29846bd8048dfc63ce170fc8ea86717b9a81738f" +version = "0.3.9" +source = "git+https://github.com/dojoengine/dojo?tag=v0.3.9#015e1d2027cef343aca6655f95cd6668305ca272" dependencies = [ "async-trait", "crypto-bigint", @@ -4104,8 +4143,8 @@ dependencies = [ [[package]] name = "torii-grpc" -version = "0.3.3" -source = "git+https://github.com/dojoengine/dojo?branch=main#29846bd8048dfc63ce170fc8ea86717b9a81738f" +version = "0.3.9" +source = "git+https://github.com/dojoengine/dojo?tag=v0.3.9#015e1d2027cef343aca6655f95cd6668305ca272" dependencies = [ "bytes", "dojo-types", @@ -4131,6 +4170,8 @@ dependencies = [ "tonic 0.9.2", "tonic-build 0.10.2", "tonic-build 0.9.2", + "tonic-reflection", + "tonic-web", "tonic-web-wasm-client", "tower", "tracing", @@ -4157,6 +4198,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" +dependencies = [ + "bitflags 2.4.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "pin-project-lite", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.2" @@ -4470,9 +4529,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.65" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/packages/torii-wasm/crate/Cargo.toml b/packages/torii-wasm/crate/Cargo.toml index fe03cb06..486b119f 100644 --- a/packages/torii-wasm/crate/Cargo.toml +++ b/packages/torii-wasm/crate/Cargo.toml @@ -23,13 +23,13 @@ tokio = { version = "1.32.0", default-features = false, features = [ "rt" ] } url = "2.4.0" # Dojo -dojo-types = { git = "https://github.com/dojoengine/dojo", branch = "main" } -torii-client = { git = "https://github.com/dojoengine/dojo", branch = "main" } +dojo-types = { git = "https://github.com/dojoengine/dojo", tag = "v0.3.9" } +torii-client = { git = "https://github.com/dojoengine/dojo", tag = "v0.3.9" } # WASM js-sys = "0.3.64" serde-wasm-bindgen = "0.6.0" -wasm-bindgen = "0.2.87" +wasm-bindgen = "=0.2.88" wasm-bindgen-futures = "0.4.37" web-sys = { version = "0.3.4", features = [ 'MessageEvent', 'Window', 'Worker', 'WorkerGlobalScope', 'console' ] } @@ -47,7 +47,7 @@ incremental = true opt-level = 1 [dev-dependencies] -wasm-bindgen-test = "0.3.0" +wasm-bindgen-test = "=0.3.38" [features] console-error-panic = [ "dep:console_error_panic_hook" ] diff --git a/packages/torii-wasm/crate/src/lib.rs b/packages/torii-wasm/crate/src/lib.rs index 7857c8ef..62b19e17 100644 --- a/packages/torii-wasm/crate/src/lib.rs +++ b/packages/torii-wasm/crate/src/lib.rs @@ -2,16 +2,16 @@ use std::str::FromStr; -use dojo_types::schema::EntityModel; +use dojo_types::schema::{Clause, EntityQuery, KeysClause}; use futures::StreamExt; use starknet::core::types::FieldElement; use starknet::core::utils::cairo_short_string_to_felt; -use types::{ClientConfig, IEntityModel}; use wasm_bindgen::prelude::*; mod types; mod utils; +use types::{ClientConfig, EntityModel, IEntityModel}; use utils::parse_ty_as_json_str; type JsFieldElement = JsValue; @@ -50,9 +50,9 @@ impl Client { match self .inner - .entity(&EntityModel { - keys, + .entity(&EntityQuery { model: model.to_string(), + clause: Clause::Keys(KeysClause { keys }), }) .await { @@ -73,7 +73,7 @@ impl Client { let entities = entities .into_iter() - .map(|e| serde_wasm_bindgen::from_value(e.into())) + .map(|e| TryInto::::try_into(e)) .collect::, _>>()?; self.inner @@ -95,7 +95,7 @@ impl Client { let entities = entities .into_iter() - .map(|e| serde_wasm_bindgen::from_value(e.into())) + .map(|e| TryInto::::try_into(e)) .collect::, _>>()?; self.inner @@ -114,8 +114,7 @@ impl Client { #[cfg(feature = "console-error-panic")] console_error_panic_hook::set_once(); - let entity = - serde_wasm_bindgen::from_value::(entity.into())?; + let entity = serde_wasm_bindgen::from_value::(entity.into())?; let model = cairo_short_string_to_felt(&entity.model).expect("invalid model name"); let mut rcv = self .inner @@ -151,7 +150,7 @@ pub async fn create_client( let entities = initialEntitiesToSync .into_iter() - .map(|e| serde_wasm_bindgen::from_value(e.into())) + .map(|e| TryInto::::try_into(e)) .collect::, _>>()?; let world_address = FieldElement::from_str(&world_address) diff --git a/packages/torii-wasm/crate/src/types.rs b/packages/torii-wasm/crate/src/types.rs index 4871113f..27cd7359 100644 --- a/packages/torii-wasm/crate/src/types.rs +++ b/packages/torii-wasm/crate/src/types.rs @@ -1,7 +1,27 @@ +use dojo_types::schema::EntityQuery; use serde::{Deserialize, Serialize}; +use starknet::core::types::FieldElement; use tsify::Tsify; use wasm_bindgen::prelude::wasm_bindgen; +// TODO: remove this in favour of the new EntityQuery +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct EntityModel { + pub model: String, + pub keys: Vec, +} + +impl From for dojo_types::schema::EntityQuery { + fn from(value: EntityModel) -> Self { + Self { + model: value.model, + clause: dojo_types::schema::Clause::Keys(dojo_types::schema::KeysClause { + keys: value.keys, + }), + } + } +} + #[wasm_bindgen(typescript_custom_section)] pub const ENTITY_MODEL_STR: &'static str = r#" export interface EntityModel { @@ -16,6 +36,13 @@ extern "C" { pub type IEntityModel; } +impl TryFrom for EntityQuery { + type Error = serde_wasm_bindgen::Error; + fn try_from(value: IEntityModel) -> Result { + serde_wasm_bindgen::from_value::(value.into()).map(|e| e.into()) + } +} + #[derive(Tsify, Serialize, Deserialize)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct ClientConfig { @@ -26,3 +53,29 @@ pub struct ClientConfig { #[serde(rename = "worldAddress")] pub world_address: String, } + +#[cfg(test)] +mod test { + + use starknet::macros::felt; + + use super::*; + + #[test] + fn convert_entity_model_to_query() { + let entity_model = EntityModel { + model: "Position".into(), + keys: vec![felt!("0x1"), felt!("0x2")], + }; + + let entity_query: EntityQuery = entity_model.try_into().unwrap(); + + assert_eq!(entity_query.model, "Position"); + assert_eq!( + entity_query.clause, + dojo_types::schema::Clause::Keys(dojo_types::schema::KeysClause { + keys: vec![felt!("0x1"), felt!("0x2")], + }) + ); + } +} diff --git a/packages/utils/src/utils/index.ts b/packages/utils/src/utils/index.ts index d389c5dd..e0756070 100644 --- a/packages/utils/src/utils/index.ts +++ b/packages/utils/src/utils/index.ts @@ -258,3 +258,14 @@ export function parseComponentValueFromGraphQLEntity( return value; } } + +export function shortenHex(hexString: string, numDigits = 6) { + if (hexString?.length <= numDigits) { + return hexString; + } + + const halfDigits = Math.floor(numDigits / 2); + const firstHalf = hexString.slice(0, halfDigits); + const secondHalf = hexString.slice(-halfDigits); + return `${firstHalf}...${secondHalf}`; +}