From d69f65c1b320884f765e2ec4dbf0ac0dbf01203d Mon Sep 17 00:00:00 2001 From: Owen Wu <1449069+yubing744@users.noreply.github.com> Date: Sun, 24 Nov 2024 14:44:28 +0800 Subject: [PATCH] Owen/game examples/rooch fish (#2938) * feat: add example rooch fish contact * feat: rooch fish add web * feat: add License for rooch fish files * feat: replace rooch fish player.move error * feat: fix move-constant-errors * feat: fix rooch_fish build warn * feat: fixbug for example test * feat: fix rooch fish init_test build warn * feat: fix dependency check issue * feat: upgrade cross-spawn version --- examples/rooch_fish/.gitignore | 1 + examples/rooch_fish/Makefile | 73 + examples/rooch_fish/Move.toml | 17 + examples/rooch_fish/README.md | 85 + examples/rooch_fish/docs/game_design.md | 127 + examples/rooch_fish/docs/tech_design.md | 445 +++ examples/rooch_fish/sources/fish.move | 300 ++ examples/rooch_fish/sources/food.move | 162 + examples/rooch_fish/sources/player.move | 404 +++ examples/rooch_fish/sources/pond.move | 1301 +++++++ examples/rooch_fish/sources/quad_tree.move | 410 +++ examples/rooch_fish/sources/rooch_fish.move | 213 ++ examples/rooch_fish/sources/simple_rng.move | 213 ++ examples/rooch_fish/sources/utils.move | 82 + .../tests/economic_system_test.move | 109 + .../rooch_fish/tests/fish_lifecycle_test.move | 74 + examples/rooch_fish/tests/init_test.move | 63 + examples/rooch_fish/web/.eslintrc.cjs | 18 + examples/rooch_fish/web/.gitignore | 24 + examples/rooch_fish/web/README.md | 78 + examples/rooch_fish/web/index.html | 29 + examples/rooch_fish/web/package.json | 49 + examples/rooch_fish/web/postcss.config.js | 10 + examples/rooch_fish/web/public/logo.svg | 3 + .../web/public/rooch_black_combine.svg | 3 + examples/rooch_fish/web/src/App.css | 10 + examples/rooch_fish/web/src/App.tsx | 344 ++ examples/rooch_fish/web/src/clients/index.ts | 4 + .../rooch_fish/web/src/clients/rooch/index.ts | 5 + .../web/src/clients/rooch/wsClient.ts | 27 + .../web/src/clients/rooch/wsTransport.ts | 131 + .../rooch_fish/web/src/components/fish.tsx | 41 + .../rooch_fish/web/src/components/food.tsx | 28 + examples/rooch_fish/web/src/config/index.ts | 341 ++ .../rooch_fish/web/src/constants/index.ts | 4 + .../web/src/constants/roochMutationKeys.ts | 21 + .../web/src/hooks/useFishController.ts | 163 + .../rooch_fish/web/src/hooks/useGameState.ts | 55 + .../web/src/hooks/useLatestTransaction.ts | 16 + .../web/src/hooks/usePlayerState.ts | 34 + .../rooch_fish/web/src/hooks/usePondState.ts | 150 + .../rooch_fish/web/src/hooks/useRccOwner.ts | 41 + .../web/src/hooks/useRoochFieldStates.ts | 57 + .../web/src/hooks/useRoochStates.ts | 45 + .../web/src/hooks/useRoochWSClient.ts | 17 + .../web/src/hooks/useRoochWSFieldStates.ts | 203 ++ .../rooch_fish/web/src/hooks/useSeqNumber.ts | 58 + .../src/hooks/useSignAndExecuteTransaction.ts | 96 + .../web/src/hooks/useTransactionDelay.ts | 82 + examples/rooch_fish/web/src/index.css | 26 + examples/rooch_fish/web/src/main.tsx | 59 + examples/rooch_fish/web/src/networks.ts | 18 + .../rooch_fish/web/src/scenes/debug_scene.tsx | 179 + .../rooch_fish/web/src/scenes/pond_scene.tsx | 392 ++ examples/rooch_fish/web/src/theme.ts | 58 + examples/rooch_fish/web/src/types/index.ts | 74 + examples/rooch_fish/web/src/utils.ts | 39 + examples/rooch_fish/web/src/utils/index.ts | 5 + .../rooch_fish/web/src/utils/rooch_client.ts | 197 + .../rooch_fish/web/src/utils/rooch_object.ts | 28 + examples/rooch_fish/web/src/vite-env.d.ts | 1 + examples/rooch_fish/web/tailwind.config.js | 26 + examples/rooch_fish/web/tsconfig.app.json | 35 + examples/rooch_fish/web/tsconfig.json | 15 + examples/rooch_fish/web/tsconfig.node.json | 17 + examples/rooch_fish/web/vite.config.ts | 13 + examples/rooch_fish/web/yarn.lock | 3170 +++++++++++++++++ 67 files changed, 10618 insertions(+) create mode 100644 examples/rooch_fish/.gitignore create mode 100644 examples/rooch_fish/Makefile create mode 100644 examples/rooch_fish/Move.toml create mode 100644 examples/rooch_fish/README.md create mode 100644 examples/rooch_fish/docs/game_design.md create mode 100644 examples/rooch_fish/docs/tech_design.md create mode 100644 examples/rooch_fish/sources/fish.move create mode 100644 examples/rooch_fish/sources/food.move create mode 100644 examples/rooch_fish/sources/player.move create mode 100644 examples/rooch_fish/sources/pond.move create mode 100644 examples/rooch_fish/sources/quad_tree.move create mode 100644 examples/rooch_fish/sources/rooch_fish.move create mode 100644 examples/rooch_fish/sources/simple_rng.move create mode 100644 examples/rooch_fish/sources/utils.move create mode 100644 examples/rooch_fish/tests/economic_system_test.move create mode 100644 examples/rooch_fish/tests/fish_lifecycle_test.move create mode 100644 examples/rooch_fish/tests/init_test.move create mode 100644 examples/rooch_fish/web/.eslintrc.cjs create mode 100644 examples/rooch_fish/web/.gitignore create mode 100644 examples/rooch_fish/web/README.md create mode 100644 examples/rooch_fish/web/index.html create mode 100644 examples/rooch_fish/web/package.json create mode 100644 examples/rooch_fish/web/postcss.config.js create mode 100644 examples/rooch_fish/web/public/logo.svg create mode 100644 examples/rooch_fish/web/public/rooch_black_combine.svg create mode 100644 examples/rooch_fish/web/src/App.css create mode 100644 examples/rooch_fish/web/src/App.tsx create mode 100644 examples/rooch_fish/web/src/clients/index.ts create mode 100644 examples/rooch_fish/web/src/clients/rooch/index.ts create mode 100644 examples/rooch_fish/web/src/clients/rooch/wsClient.ts create mode 100644 examples/rooch_fish/web/src/clients/rooch/wsTransport.ts create mode 100644 examples/rooch_fish/web/src/components/fish.tsx create mode 100644 examples/rooch_fish/web/src/components/food.tsx create mode 100644 examples/rooch_fish/web/src/config/index.ts create mode 100644 examples/rooch_fish/web/src/constants/index.ts create mode 100644 examples/rooch_fish/web/src/constants/roochMutationKeys.ts create mode 100644 examples/rooch_fish/web/src/hooks/useFishController.ts create mode 100644 examples/rooch_fish/web/src/hooks/useGameState.ts create mode 100644 examples/rooch_fish/web/src/hooks/useLatestTransaction.ts create mode 100644 examples/rooch_fish/web/src/hooks/usePlayerState.ts create mode 100644 examples/rooch_fish/web/src/hooks/usePondState.ts create mode 100644 examples/rooch_fish/web/src/hooks/useRccOwner.ts create mode 100644 examples/rooch_fish/web/src/hooks/useRoochFieldStates.ts create mode 100644 examples/rooch_fish/web/src/hooks/useRoochStates.ts create mode 100644 examples/rooch_fish/web/src/hooks/useRoochWSClient.ts create mode 100644 examples/rooch_fish/web/src/hooks/useRoochWSFieldStates.ts create mode 100644 examples/rooch_fish/web/src/hooks/useSeqNumber.ts create mode 100644 examples/rooch_fish/web/src/hooks/useSignAndExecuteTransaction.ts create mode 100644 examples/rooch_fish/web/src/hooks/useTransactionDelay.ts create mode 100644 examples/rooch_fish/web/src/index.css create mode 100644 examples/rooch_fish/web/src/main.tsx create mode 100644 examples/rooch_fish/web/src/networks.ts create mode 100644 examples/rooch_fish/web/src/scenes/debug_scene.tsx create mode 100644 examples/rooch_fish/web/src/scenes/pond_scene.tsx create mode 100644 examples/rooch_fish/web/src/theme.ts create mode 100644 examples/rooch_fish/web/src/types/index.ts create mode 100644 examples/rooch_fish/web/src/utils.ts create mode 100644 examples/rooch_fish/web/src/utils/index.ts create mode 100644 examples/rooch_fish/web/src/utils/rooch_client.ts create mode 100644 examples/rooch_fish/web/src/utils/rooch_object.ts create mode 100644 examples/rooch_fish/web/src/vite-env.d.ts create mode 100644 examples/rooch_fish/web/tailwind.config.js create mode 100644 examples/rooch_fish/web/tsconfig.app.json create mode 100644 examples/rooch_fish/web/tsconfig.json create mode 100644 examples/rooch_fish/web/tsconfig.node.json create mode 100644 examples/rooch_fish/web/vite.config.ts create mode 100644 examples/rooch_fish/web/yarn.lock diff --git a/examples/rooch_fish/.gitignore b/examples/rooch_fish/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/examples/rooch_fish/.gitignore @@ -0,0 +1 @@ +/build diff --git a/examples/rooch_fish/Makefile b/examples/rooch_fish/Makefile new file mode 100644 index 0000000000..a1806d5a8b --- /dev/null +++ b/examples/rooch_fish/Makefile @@ -0,0 +1,73 @@ +# Define variables +PACKAGE_NAME = rooch_fish +GAS_BUDGET = 1000 +PACKAGE_PATH = . +BUILD_DIR = $(PACKAGE_PATH)/build +SOURCES_DIR = $(PACKAGE_PATH)/sources +TESTS_DIR = $(PACKAGE_PATH)/tests + +# Default target +all: build + +# Build contract +build: + @echo "Building the Move package..." + rooch move build --path $(PACKAGE_PATH) --named-addresses rooch_fish=default --json + +# Publish contract +publish: + @echo "Publishing the Move package..." + rooch move publish --path $(PACKAGE_PATH) --named-addresses rooch_fish=default + +# Initialize game world +init-world: + @echo "Init RoochFish world..." + rooch move run --function 0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e8::rooch_fish::init_world --json + +# View game world +view-world: + rooch state --access-path /object/0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e8::rooch_fish::GameState + +# View game world +view-ponds: + rooch rpc request --method rooch_listFieldStates --params '["0xae308aa50bded0b341120431a9049ad1f151b345115a17bcefcad943505fa3b6", null, "8", {"decode": true, "showDisplay": true}]' --json + +# Purchase fish +purchase_fish: + rooch move run --function 0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e8::rooch_fish::purchase_fish --args object_id:0x5e89df84a672ea3697916f3a2a2ada4c63586db573b2e8af666da7d2b1084fd6 --args u64:0 --json + +# View purchased fish IDs +view_fish_ids: + rooch move view --function 0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e8::rooch_fish::get_pond_player_fish_ids --args object_id:0x5e89df84a672ea3697916f3a2a2ada4c63586db573b2e8af666da7d2b1084fd6 --args 0u64 --args 'address:default' + +# Move fish +move_fish: + rooch move run --function 0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e8::rooch_fish::move_fish --args object_id:0x5e89df84a672ea3697916f3a2a2ada4c63586db573b2e8af666da7d2b1084fd6 --args u64:0 --args u64:5 --args u8:0 --json --gas-profile + +# Debug contract +debug: + @echo "Running tests..." + rooch move test --path $(PACKAGE_PATH) --skip-fetch-latest-git-deps --ignore_compile_warnings --named-addresses rooch_fish=default pond + +# Test contract +test: + @echo "Running tests..." + rooch move test --path $(PACKAGE_PATH) --skip-fetch-latest-git-deps --ignore_compile_warnings --named-addresses rooch_fish=default + +# Clean build files +clean: + @echo "Cleaning build directory..." + rm -rf $(BUILD_DIR) + +# Help information +help: + @echo "Makefile for Sui Move project" + @echo "" + @echo "Usage:" + @echo " make build - Build the Move package" + @echo " make publish - Publish the Move package" + @echo " make test - Run tests" + @echo " make clean - Clean build directory" + @echo " make help - Show this help message" + +.PHONY: all build publish test clean help diff --git a/examples/rooch_fish/Move.toml b/examples/rooch_fish/Move.toml new file mode 100644 index 0000000000..20f1ca50a0 --- /dev/null +++ b/examples/rooch_fish/Move.toml @@ -0,0 +1,17 @@ +[package] +name = "rooch_fish" +version = "0.0.1" + +[dependencies] +MoveStdlib = { git = "https://github.com/rooch-network/rooch.git", subdir = "frameworks/move-stdlib", rev = "main" } +MoveosStdlib = { git = "https://github.com/rooch-network/rooch.git", subdir = "frameworks/moveos-stdlib", rev = "main" } +RoochFramework = { git = "https://github.com/rooch-network/rooch.git", subdir = "frameworks/rooch-framework", rev = "main" } + +[addresses] +rooch_fish = "_" +std = "0x1" +moveos_std = "0x2" +rooch_framework = "0x3" + +[dev-addresses] +rooch_fish = "0x42" \ No newline at end of file diff --git a/examples/rooch_fish/README.md b/examples/rooch_fish/README.md new file mode 100644 index 0000000000..0b45f23225 --- /dev/null +++ b/examples/rooch_fish/README.md @@ -0,0 +1,85 @@ +# RoochFish + +"Have You Caught Fish" is an open-source full-chain game based on the Rooch blockchain, where players can buy and control virtual fish in multiple dynamic fish ponds. The fish grow by eating other fish and food, and players can earn token rewards under specific conditions. The game introduces a stamina system, adding strategic depth and balance. The smart contract part of the project is open-source, aiming to provide developers and players with a transparent and scalable gaming environment. + +## Detailed Documentation + +- [Game Design Document](docs/game_design.md) - Detailed game mechanics, rules, and gameplay instructions +- [Technical Design Document](docs/tech_design.md) - Technical implementation and architecture design + +## Core Features + +### Multi-Pond System +- Multiple ponds with different sizes, capacities, and prices +- Each pond has a maximum fish size limit +- Specific exit areas for fish destruction and reward acquisition + +### Stamina System +- Each fish has a maximum stamina of 10 points +- Each move consumes 1 stamina point +- Stamina recovers 1 point per second +- Eating food or smaller fish immediately restores 10 stamina points + +### Economic System +- Use RGAS tokens to buy fish and feed food +- Rewards distribution upon fish destruction: + - 1% goes to the developer + - 20% goes to the player who fed the food + - The remaining reward goes to the fish owner + +### Game Mechanics +- New fish have a 1-minute protection period +- Fish can only eat smaller fish +- Fish "burst" and turn into food when they reach the maximum size limit +- Comprehensive collision detection system + +## Quick Start + +### Environment Setup + +1. **Install Rooch CLI**: + ```bash + # Refer to the Rooch official documentation to install the CLI + ``` + +### Using Makefile + +The project provides the following Makefile commands: + +```bash +make build # Build the contract +make publish # Publish the contract +make test # Run tests +make debug # Run a specific test +make clean # Clean build files +``` + +## Project Structure + +``` +rooch-fish/ +├── docs/ # Project documentation +│ ├── game_design.md # Game design document +│ └── tech_design.md # Technical design document +├── sources/ # Move contract source code +├── tests/ # Test files +├── build/ # Build output +├── Move.toml # Project configuration file +└── Makefile # Build script +``` + +## Strategic Tips + +- **Pond Selection**: Choose the appropriate pond based on strategy and funds +- **Stamina Management**: Plan movement paths wisely, rest in safe areas to recover stamina +- **Risk Assessment**: Balance the pursuit of food/smaller fish with stamina preservation +- **Optimal Timing**: Move fish to exit areas at the right time to claim rewards +- **Investment Balance**: Balance buying fish, feeding food, and claiming rewards + +## Future Plans + +- Introduce new pond types +- Implement special abilities for fish +- Add social features (team, guild) +- Host regular events and competitions +- Continuously optimize the stamina system balance diff --git a/examples/rooch_fish/docs/game_design.md b/examples/rooch_fish/docs/game_design.md new file mode 100644 index 0000000000..3979d8a0a1 --- /dev/null +++ b/examples/rooch_fish/docs/game_design.md @@ -0,0 +1,127 @@ +## Game Design: Have You Caught a Fish (Optimized Version) + +### 1. Core Concept + +"Have You Caught a Fish" is a multiplayer online game based on the Rooch blockchain, where players compete by buying and controlling virtual fish in multiple dynamic fish ponds. The game combines elements of growth, strategy, and economy. Players can grow by eating other fish or collecting food, and earn token rewards under specific conditions. Players can also feed fish ponds with food, influencing the game economy and potentially gaining profits. The game introduces a stamina system, adding depth and balance to the strategy. + +### 2. Game Mechanics and Rules + +#### 2.1 Fish Pond System + +- **Multi-Pond Design**: The game includes multiple fish ponds, each with different sizes, capacities, and purchase prices. +- **Pond Parameters**: Each pond has fixed width, height, maximum number of fish, and maximum amount of food. +- **Maximum Fish Size Limit**: Each pond sets a maximum fish size limit. When a fish reaches or exceeds this limit, it will "burst" and disappear, generating a certain amount of food (e.g., 10 pieces), which will be randomly distributed in the pond. +- **Exit Area**: Each pond has a specific exit area where fish can be destroyed for rewards. + +#### 2.2 Purchase and Generation + +- **Buying Fish**: Players use RGAS tokens to buy fish in specific ponds, with different purchase prices for each pond. +- **Random Generation**: New fish are randomly generated in the pond with a fixed initial size. +- **New Fish Protection Mechanism**: Newly born fish are protected for 1 minute and cannot be eaten by other fish. + +#### 2.3 Movement and Growth + +- **Movement Control**: + - Players can control the fish to move up, down, left, or right in the pond. + - Each movement consumes 1 stamina point. + - When stamina is 0, the fish cannot move. +- **Stamina System**: + - Each fish has a stamina attribute with a maximum value of 10 points. + - Stamina recovers 1 point per second. + - Stamina cannot exceed the maximum value of 10 points. + - Eating food or smaller fish immediately restores 10 stamina points. +- **Growth Mechanism**: + - Eating smaller fish increases size and immediately restores 10 stamina points. + - Eating food in the pond increases size and immediately restores 10 stamina points. + - When a fish reaches the maximum size limit, it will "burst" and turn into food. +- **Boundary Limits**: Fish movement is restricted by pond boundaries. + +#### 2.4 Feeding and Food System + +- **Feeding Food**: + - Players can use RGAS tokens to feed food into the pond. + - The amount of food fed affects the total amount of food in the pond. +- **Food Generation**: Food is generated at random positions in the pond, with a maximum quantity. + +#### 2.5 Interaction and Competition + +- **Eating Rules**: Fish can only eat smaller fish, which immediately restores 10 stamina points. +- **Food Consumption**: Eating food increases size and immediately restores 10 stamina points. +- **New Fish Protection**: New fish are protected for 1 minute after birth and cannot be eaten by other fish. +- **Collision Detection**: The system automatically detects collisions between fish and fish, fish and food, and processes the results. + +#### 2.6 Exit and Rewards + +- **Exit Mechanism**: Fish can move to a specific exit area in the pond to be destroyed. +- **Token Rewards**: + - When a fish is destroyed at the exit, the player receives RGAS token rewards based on the fish's size. + - Reward Distribution: + - **1%** of the reward is allocated to the developers for supporting ongoing development and maintenance. + - **20%** of the reward is allocated to players who fed food, based on the proportion of food fed by each player relative to the total food eaten by the fish. + - The remaining reward goes to the owner of the fish. + +#### 2.7 Economic System + +- **Global Economy**: The game maintains a global player list, recording total feeding amounts and player numbers. +- **Pond Economy**: Each pond has an independent economic system, including player lists and total feeding amounts. +- **Stamina Management**: Players need to manage their fish's stamina effectively, balancing movement and rest. + +### 3. Player Objectives and Strategies + +#### 3.1 Main Objectives + +- **Maximize Profits**: Grow large fish and destroy them at the right time to maximize RGAS token rewards. +- **Survival and Growth**: Survive and become the largest fish in a competitive environment. + +#### 3.2 Strategic Considerations + +- **Pond Selection**: Choose the appropriate pond based on your strategy and funds. +- **Investment Balance**: Balance buying fish, feeding food, and obtaining rewards. +- **Risk Management**: Avoid being eaten by larger fish during growth. +- **Timing**: Choose the right time to move fish to the exit area for rewards. +- **Maximum Size Management**: Decide whether to move fish to the exit area for tokens when approaching the maximum size, or risk further growth for more food. +- **Stamina Management**: Plan movement paths to avoid running out of stamina and being unable to evade danger or hunt. +- **Rest Strategy**: Rest fish in safe areas to recover stamina for future rapid movement. +- **Stamina Recovery Strategy**: Weigh the risk of eating food or smaller fish to quickly recover stamina. +- **Offense-Defense Balance**: Choose between chasing prey and conserving stamina, as successful hunting immediately restores stamina. +- **Risk Assessment**: Assess whether it's worth expending stamina to chase food or smaller fish, considering the immediate stamina recovery upon success. + +### 4. Technical Implementation + +#### 4.1 Smart Contracts + +- Use Move language to write smart contracts, implementing core logic for ponds, fish, and food. +- Implement key functions such as purchase, movement, growth, destruction, and feeding. +- Add stamina value to fish attributes and implement stamina consumption and recovery logic. +- Include stamina checks in the movement function to ensure movement only when there is enough stamina. +- Add immediate stamina recovery in the logic for eating food or smaller fish. +- Ensure stamina recovery does not exceed the maximum value of 10 points. +- Ensure contract security and efficiency. + +#### 4.2 Frontend Development + +- Develop an intuitive user interface to display multiple ponds and game status. +- Implement real-time updates and interaction functions. +- Display the current stamina value of each fish in the user interface. +- Implement visual feedback for stamina recovery, allowing players to clearly know when they can move again. +- Implement visual effects for rapid stamina recovery when eating food or smaller fish, allowing players to clearly perceive stamina changes. +- Consider adding a special animation or sound effect to emphasize rapid stamina recovery. +- Integrate blockchain wallets to simplify token operations. + +#### 4.3 Backend Services + +- Develop backend services to handle high-frequency updates and complex calculations. +- Implement game state synchronization and persistence. +- Implement a timer function for automatic stamina recovery. +- Ensure real-time synchronization and updating of stamina values. +- Optimize the stamina recovery calculation logic to balance normal and rapid recovery. +- Implement relevant data statistics, such as the number of times players recover stamina by eating food/smaller fish, for subsequent game balance adjustments. + +### 5. Future Extensions + +- Introduce new pond types to increase game diversity. +- Implement special abilities or attribute systems for fish. +- Add social features such as teaming and guilds. +- Host regular events or competitions to increase game fun. +- Adjust stamina system parameters based on player feedback and data analysis to optimize game balance. +- Consider introducing more stamina-related special items or skills to increase strategic depth. diff --git a/examples/rooch_fish/docs/tech_design.md b/examples/rooch_fish/docs/tech_design.md new file mode 100644 index 0000000000..53fd1b1abc --- /dev/null +++ b/examples/rooch_fish/docs/tech_design.md @@ -0,0 +1,445 @@ +# RoochFish Contract Technical Solution (Optimized Version) + +## 1. Introduction + +### 1.1 Project Background + +"RoochFish" is a multiplayer online game based on the Rooch blockchain platform. Players compete in multiple dynamic fish ponds by purchasing and controlling virtual fish. The game combines growth, strategy, and economic elements to provide players with an interesting and economically motivated gaming experience. The game introduces a physical system and a simplified food source tracking mechanism, which increases the strategic depth and game balance. + +### 1.2 Purpose of the Technical Solution + +This technical solution aims to describe the contract implementation of the RoochFish game in detail, including demand analysis, system design, and detailed implementation solutions. The solution will guide the development team to complete contract development efficiently and safely. + +## 2. Requirements Overview + +### 2.1 Functional Requirements + +- **Purchase and Generation of Fish**: Players use RGAS tokens to purchase fish, and the fish are randomly generated in the fish pond with an initial size and physical value. +- **Fish movement and stamina**: Players control the movement of fish, which consumes stamina and automatically recovers stamina every second. Fish cannot move when stamina is 0. +- **Fish growth**: Fish increase in size by eating fish or food smaller than themselves, and recover stamina immediately when eating food or small fish. +- **Fish destruction and rewards**: Fish can be moved to the exit location for destruction, and players receive RGAS token rewards based on the size of the fish. Fish will "die" when they reach their maximum size, and produce food. +- **Feeding food**: Players use RGAS tokens to feed food to the fish pond, and food is randomly generated in the fish pond. +- **Food source tracking**: Food records the player address of its source for reward distribution. +- **Reward distribution**: When a fish is destroyed or dies, rewards are distributed to the developer, the owner of the fish, and the player who is the source of the food according to a certain ratio. +- **New fish protection mechanism**: New fish cannot be eaten by other fish within 1 minute. + +### 2.2 Non-functional requirements + +- **Security**: The contract needs to prevent cheating and attacks to ensure the fairness of the game. + +- **Performance**: Optimize the efficiency of contract execution and reduce Gas consumption. + +- **Scalability**: The code design needs to be easy to expand and support future function additions. + +- **User experience**: Simplify the interaction process to ensure timely feedback and good experience. + +## 3. System analysis + +### 3.1 Role analysis + +- **Player**: Buy and control fish, feed food, and get rewards. + +- **Fish**: The main entity in the game, with attributes such as size, physical strength, and position. + +- **Food**: Items that fish can eat, increase the size of fish and restore physical strength. + +- **Fish pond**: The main scene of the game, accommodating fish and food. + +### 3.2 Key entities and attributes + +#### Fish + +- **id**: unique identifier +- **owner**: owner address +- **size**: size +- **position**: position (x, y) +- **stamina**: stamina value (0-10) +- **last_stamina_update**: timestamp of last stamina recovery +- **created_at**: creation timestamp +- **food_sources**: food source record (list) + +#### Food + +- **id**: unique identifier +- **size**: size +- **value**: value +- **position**: position (x, y) +- **feeder**: player address of food source + +#### Player + +- **address**: player address +- **feed_amount**: cumulative amount of food fed +- **reward**: cumulative reward + +#### Fish Pond + +- **fishes**: List of current fish + +- **foods**: List of current food + +- **max_fish_size**: Maximum size limit of fish + +## 4. System Design + +### 4.1 Module Structure + +- **RoochFish Main Module**: Main logic and entry function of the game. + +- **Fish Module**: Defines the structure and behavior of fish. + +- **Food Module**: Defines the structure and behavior of food. + +- **Pond Module**: Manages the state of the fish pond. + +- **Player Module**: Manages the player's feeding and reward records. + +- **Utils Tool Module**: Provides auxiliary functions, such as random number generation, time acquisition, etc. + +### 4.2 Data structure design #### Fish module ```move struct Fish has key, store { id: u64, owner: address, size: u64, position: (u64, u64), stamina: u8, last_stamina_update: u64, created_at: u64, food_sources: vector<(address, u64)>, } ``` #### Food module ```move struct Food has key, store { id: u64, size: u64, value: u64, position: (u64, u64), feeder: address, } ``` #### Player module ```move struct Player has key, store { address: address, feed_amount: u64, reward: u64, +} +``` + +#### Pond module + +```move +struct PondState has key, store { +fishes: vector>, +foods: vector>, +max_fish_size: u64, +} +``` + +### 4.3 Key algorithms and logic + +#### 4.3.1 Physical system + +- **Automatic recovery**: Fish recover 1 physical point per second, up to 10 points. + +- **Movement consumption**: Each move consumes 1 physical point, and insufficient physical strength cannot move. + +- **Instant recovery**: Eat food or small fish, and physical strength will be restored to 10 points immediately. + +#### 4.3.2 Movement logic + +- **Boundary detection**: Ensure that the movement of fish does not exceed the scope of the fish pond. + +- **Collision detection**: Detect collision with other fish or food. + +#### 4.3.3 Eating fish and eating food + +- **Eating fish conditions**: You can only eat fish smaller than yourself, and the target fish is not in the protection period. + +- **Eating food**: Increase the size of the fish and restore physical strength. + +- **Food source record**: Record the feeder address and value of the food for reward distribution. + +#### 4.3.4 Reward distribution + +- **When the fish is destroyed**: +- 1% reward to the developer. +- 79% reward to the owner of the fish. +- 20% is distributed to the feeder according to the contribution ratio based on the food source. +- **When the fish dies**: +- 1% reward to the developer. +- 20% is distributed to the feeder according to the contribution ratio based on the food source. +- 79% is converted into food, which brings benefits to the owner of the fish when eaten by other fish. + +## 5. Module Detailed Design + +### 5.1 RoochFish Main Module + +```move +module ::RoochFish { +public entry fun purchase_fish(ctx: &mut TxContext); +public entry fun move_fish(fish_id: u64, direction: u8, ctx: &mut TxContext); +public entry fun feed_food(amount: u64, ctx: &mut TxContext); +public entry fun destroy_fish(fish_id: u64, ctx: &mut TxContext); +} +``` + +#### Function Description + +- **purchase_fish**: The player purchases fish, generates new fish and adds them to the fish pond. +- **move_fish**: The player controls the movement of the fish, handles physical strength and collision logic. +- **feed_food**: The player feeds food, generates food and adds it to the fish pond. +- **destroy_fish**: Player destroys fish, collects rewards and handles distribution. + +### 5.2 Fish module + +```move +module ::Fish { +struct Fish has key, store { ... } + +public fun new(owner: address, id: u64, ctx: &mut TxContext): Object; + +public fun move(fish: &mut Fish, direction: u8, ctx: &mut TxContext); + +public fun grow(fish: &mut Fish, amount: u64, feeder: address); + +public fun is_protected(fish: &Fish, current_time: u64): bool; + +public fun auto_recover_stamina(fish: &mut Fish, current_time: u64); +} +``` + +#### Function description + +- **new**: Create a new fish and initialize attributes. + +- **move**: Handle the movement of fish and consume stamina. +- **grow**: The fish eats food or small fish, increases in size and recovers stamina, and records the source of food. +- **is_protected**: Determines whether the fish is in the protection period. +- **auto_recover_stamina**: Automatically recovers stamina. + +### 5.3 Food module + +```move +module ::Food { +struct Food has key, store { ... } + +public fun new(id: u64, feeder: address, size: u64, value: u64, ctx: &mut TxContext): Object; +} +``` + +#### Function description + +- **new**: Create new food, record the source and value. + +### 5.4 Pond module + +```move +module ::Pond { +struct PondState has key, store { ... } + +public fun add_fish(fish: Object); + +public fun remove_fish(fish_id: u64); + +public fun add_food(food: Object); + +public fun remove_food(food_id: u64); + +public fun get_state(): &mut PondState; +} +``` + +#### Function description + +- **add_fish**: Add fish to the pond. + +- **remove_fish**: Remove fish from the pond. + +- **add_food**: Add food to the pond. + +- **remove_food**: Remove food from the pond. + +- **get_state**: Get the current state of the pond. + +### 5.5 Player module + +```move +module ::Player { +struct Player has key, store { ... } + +public fun add_feed(address: address, amount: u64); + +public fun add_reward(address: address, amount: u64); + +public fun get_state(address: address): &mut Player; +} +``` + +#### Function description + +- **add_feed**: Record the player's feed amount. + +- **add_reward**: Add the player's reward. +- **get_state**: Get the player's status information. + +### 5.6 Utils Tool Module + +```move +module ::Utils { +public fun random_position(): (u64, u64); + +public fun current_timestamp(): u64; +} +``` + +#### Function Description + +- **random_position**: Generates random position coordinates. + +- **current_timestamp**: Gets the current timestamp. + +## 6. Implementation details + +### 6.1 Purchase fish + +```move +public entry fun purchase_fish(ctx: &mut TxContext) { +let sender = ctx.sender(); +// Deduct RGAS tokens +coin::burn(sender, purchase_amount); + +// Create a new fish +let fish_id = generate_unique_id(); +let position = Utils::random_position(); +let created_at = Utils::current_timestamp(); +let fish = Fish::new(sender, fish_id, ctx); + +// Add to the fish pond +Pond::add_fish(fish); +} +``` + +### 6.2 Move fish + +```move +public entry fun move_fish(fish_id: u64, direction: u8, ctx: &mut TxContext) { +let current_time = Utils::current_timestamp(); +let fish = Pond::get_fish_mut(fish_id); +// Verify ownership +assert!(fish.owner == ctx.sender(), ErrorFishNotOwned); +// Automatically recover stamina +Fish::auto_recover_stamina(&mut fish, current_time); +// Check stamina +assert!(fish.stamina >= 1, ErrorInsufficientStamina); +// Consume stamina +fish.stamina -= 1; +// Move fish +Fish::move(&mut fish, direction, ctx); +// Handle collisions +handle_collisions(&mut fish, ctx); +// Check if fish is full +if fish.size >= Pond::get_state().max_fish_size { +handle_overgrown_fish(&fish, ctx); +} +} +``` + +### 6.3 Feeding food + +```move +public entry fun feed_food(amount: u64, ctx: &mut TxContext) { +let feeder = ctx.sender(); +// Deduct RGAS tokens +coin::burn(feeder, amount); + +// Record feeding +Player::add_feed(feeder, amount); + +// Generate food +for _ in 0..amount { +let food_id = generate_unique_id(); +let food = Food::new(food_id, feeder, 1, 1, ctx); // Assume that the size and value of each unit of food is 1 +Pond::add_food(food); +} +} +``` + +### 6.4 Destroying fish + +```move +public entry fun destroy_fish(fish_id: u64, ctx: &mut TxContext) { +let fish = Pond::get_fish(fish_id); + +// Verify ownership and position +assert!(fish.owner == ctx.sender(), ErrorFishNotOwned); +assert!(is_at_exit(fish.position), ErrorNotAtExit); + +// Calculate rewards +let total_reward = calculate_reward(fish.size); + +// Distribute rewards +distribute_rewards(&fish, total_reward); + +// Remove fish +Pond::remove_fish(fish_id); +} +``` + +### 6.5 Dealing with overgrown fish + +```move +fun handle_overgrown_fish(fish: &Fish::Fish, ctx: &mut TxContext) { +let fish_size = fish.size; +let total_value = calculate_reward(fish_size); + +// Distribute rewards +distribute_rewards(fish, total_value); + +// Generate food +let remaining_value = total_value * 79 / 100; let food_value = remaining_value / 10; let food_size = fish_size / 10; for _ in 0..10 { let food_id = generate_unique_id(); let food = Food::new(food_id, fish.owner, food_size, food_value, ctx); Pond::add_food(food); } // remove fish Pond::remove_fish(fish.id); } ``` ### 6.6 Reward distribution ```move fun distribute_rewards(fish: &Fish::Fish, total_reward: u64) { let dev_reward = total_reward / 100; coin::mint(DEVELOPER_ADDRESS, dev_reward); let owner_reward = total_reward * 79/100; coin::mint(fish.owner, owner_reward); let feeder_reward = total_reward * 20 / 100; let total_food_value = 0u64; let mut contributions = Table::new(); for (feeder, value) in &fish.food_sources { total_food_value += *value; let entry = Table::get_mut_with_default(&mut contributions, *feeder, 0); *entry += *value; } if total_food_value > 0 { for (feeder, value) in Table::iter(&contributions) { let share = (*value * feeder_reward) / total_food_value; Player::add_reward(*feeder, share); } } +} +``` + +## 7. Test plan + +### 7.1 Test cases + +#### Use case 1: Stamina consumption and recovery + +- **Steps**: +1. Create a fish, the initial stamina should be 10. +2. Move the fish once, the stamina should be reduced by 1. +3. Wait for 1 second, the stamina should be increased by 1. + +- **Expected result**: Stamina is consumed and recovered correctly. + +#### Use case 2: Fish movement and collision + +- **Steps**: +1. Control the fish to move to the location with food. +2. Check the size and stamina of the fish. + +- **Expected result**: The size of the fish increases, and the stamina is restored to 10. + +#### Use case 3: Fish destruction and reward + +- **Steps**: +1. Control the fish to move to the exit location. +2. Destroy the fish, check the player's balance and reward record. + +- **Expected result**: Players get the correct rewards, and rewards for feeders are correctly distributed. + +#### Use case 4: Fish death processing + +- **Steps**: +1. Make the fish reach the maximum size limit. +2. Check whether the fish is overfed and whether food is generated. + +- **Expected result**: The fish is overfed, food is generated, and rewards are correctly distributed. + +### 7.2 Testing tools and environment + +- Use the test framework of the Move language to write unit tests. +- Simulate multiple scenarios to ensure that the logic is correct. + +## 8. Security considerations + +- **Permission control**: Ensure that only the owner of the fish can control and destroy the fish. + +- **Anti-cheating**: Prevent malicious players from tampering with key attributes such as physical strength and size. + +- **Randomness security**: Use secure random number generation to prevent position prediction. + +- **Prevent replay attacks**: Add transaction checks to prevent repeated operations. + +## 9. Performance optimization + +- **Data structure optimization**: Use efficient data structures (such as mappings and vectors) to manage fish and food. +- **Reduce storage usage**: Simplify data structure and reduce on-chain storage costs. +- **Batch operation**: Support batch feeding to reduce the number of transactions. + +## 10. Future expansion + +- **Multiple fish pond support**: Introduce fish ponds with different rules and rewards. +- **Special abilities**: Add special skills or attributes to fish. +- **Social functions**: Add player interaction, team formation and other functions. +- **Events and competitions**: Regularly hold in-game events to increase activity. + +## 11. Conclusion + +This technical solution describes the contract design and implementation method of the RoochFish game in detail, focusing on optimizing the food source tracking and reward distribution mechanism. By simplifying the data structure and logic, it not only ensures the fun and fairness of the game, but also reduces the complexity of implementation and maintenance. It is hoped that this solution can effectively guide development and create a popular blockchain game. \ No newline at end of file diff --git a/examples/rooch_fish/sources/fish.move b/examples/rooch_fish/sources/fish.move new file mode 100644 index 0000000000..a422eefb75 --- /dev/null +++ b/examples/rooch_fish/sources/fish.move @@ -0,0 +1,300 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +module rooch_fish::fish { + use moveos_std::signer; + use moveos_std::timestamp; + use moveos_std::simple_map::{Self, SimpleMap}; + + friend rooch_fish::rooch_fish; + friend rooch_fish::pond; + + const ErrorNotOwner: u64 = 1; + const ErrorInvalidDirection: u64 = 2; + const ErrorUnauthorizedCreation: u64 = 3; + + struct Fish has key, store { + id: u64, + owner: address, + size: u64, + x: u64, + y: u64, + food_contributors: SimpleMap, + total_food_consumed: u64, + created_at: u64, + } + + public(friend) fun create_fish(owner: address, id: u64, size: u64, x: u64, y: u64): Fish { + Fish { + id, + owner, + size, + x, + y, + food_contributors: simple_map::new(), + total_food_consumed: 0, + created_at: timestamp::now_milliseconds(), + } + } + + public(friend) fun move_fish(account: &signer, fish: &mut Fish, direction: u8) { + assert!(signer::address_of(account) == fish.owner, ErrorNotOwner); + assert!(direction <= 3, ErrorInvalidDirection); + + if (direction == 0) { + fish.y = fish.y + 1; + } else if (direction == 1) { + fish.x = fish.x + 1; + } else if (direction == 2) { + fish.y = fish.y - 1; + } else { + fish.x = fish.x - 1; + } + } + + #[test_only] + public(friend) fun move_fish_to_for_test(fish: &mut Fish, x: u64, y: u64) { + fish.x = x; + fish.y = y; + } + + public(friend) fun grow_fish(fish: &mut Fish, amount: u64) { + fish.size = fish.size + amount; + } + + public(friend) fun record_food_consumption(fish: &mut Fish, food_owner: address, food_size: u64) { + let current_amount = if (simple_map::contains_key(&fish.food_contributors, &food_owner)) { + *simple_map::borrow(&fish.food_contributors, &food_owner) + } else { + 0 + }; + + let new_amount = current_amount + food_size; + if (current_amount == 0) { + simple_map::add(&mut fish.food_contributors, food_owner, new_amount); + } else { + *simple_map::borrow_mut(&mut fish.food_contributors, &food_owner) = new_amount; + }; + + fish.total_food_consumed = fish.total_food_consumed + food_size; + } + + public fun get_food_contributors(fish: &Fish): vector
{ + simple_map::keys(&fish.food_contributors) + } + + public fun get_contributor_amount(fish: &Fish, owner: address): u64 { + if (simple_map::contains_key(&fish.food_contributors, &owner)) { + *simple_map::borrow(&fish.food_contributors, &owner) + } else { + 0 + } + } + + public fun get_total_food_consumed(fish: &Fish): u64 { + fish.total_food_consumed + } + + public fun is_protected(fish: &Fish): bool { + let protection_period = 60000; // 1 minute in milliseconds + let current_time = timestamp::now_milliseconds(); + current_time - fish.created_at < protection_period + } + + public fun get_created_at(fish: &Fish): u64 { + fish.created_at + } + + public(friend) fun drop_fish(fish: Fish) { + let Fish { + id: _, + owner: _, + size: _, + x: _, + y: _, + food_contributors: _, + total_food_consumed: _, + created_at: _, + } = fish; + } + + public fun get_fish_info(fish: &Fish): (u64, address, u64, u64, u64) { + (fish.id, fish.owner, fish.size, fish.x, fish.y) + } + + public fun get_id(fish: &Fish): u64 { + fish.id + } + + public fun get_owner(fish: &Fish): address { + fish.owner + } + + public fun get_size(fish: &Fish): u64 { + fish.size + } + + public fun get_x(fish: &Fish): u64 { + fish.x + } + + public fun get_y(fish: &Fish): u64 { + fish.y + } + + public fun get_position(fish: &Fish): (u64, u64) { + (fish.x, fish.y) + } + + #[test_only] + use std::vector; + + #[test] + fun test_create_fish() { + let owner = @0x1; + let fish = create_fish(owner, 1, 10, 5, 5); + let (id, fish_owner, size, x, y) = get_fish_info(&fish); + + assert!(id == 1, 1); + assert!(fish_owner == owner, 2); + assert!(size == 10, 3); + assert!(x == 5, 4); + assert!(y == 5, 5); + assert!(get_total_food_consumed(&fish) == 0, 6); + assert!(vector::length(&get_food_contributors(&fish)) == 0, 7); + + drop_fish(fish); + } + + #[test(owner = @0x42)] + fun test_move_fish(owner: signer) { + let owner_addr = signer::address_of(&owner); + let fish = create_fish(owner_addr, 1, 10, 5, 5); + + // Test all directions + move_fish(&owner, &mut fish, 0); // up + assert!(get_y(&fish) == 6, 1); + + move_fish(&owner, &mut fish, 1); // right + assert!(get_x(&fish) == 6, 2); + + move_fish(&owner, &mut fish, 2); // down + assert!(get_y(&fish) == 5, 3); + + move_fish(&owner, &mut fish, 3); // left + assert!(get_x(&fish) == 5, 4); + + drop_fish(fish); + } + + #[test(owner = @0x42)] + fun test_food_contribution(owner: signer) { + let owner_addr = signer::address_of(&owner); + let fish = create_fish(owner_addr, 1, 10, 5, 5); + + // Test single contribution + let food_owner = @0x101; + record_food_consumption(&mut fish, food_owner, 5); + assert!(get_contributor_amount(&fish, food_owner) == 5, 1); + assert!(get_total_food_consumed(&fish) == 5, 2); + + // Test multiple contributions from same owner + record_food_consumption(&mut fish, food_owner, 3); + assert!(get_contributor_amount(&fish, food_owner) == 8, 3); + assert!(get_total_food_consumed(&fish) == 8, 4); + + drop_fish(fish); + } + + #[test(owner = @0x42)] + fun test_multiple_contributors(owner: signer) { + let owner_addr = signer::address_of(&owner); + let fish = create_fish(owner_addr, 1, 10, 5, 5); + + let food_owner1 = @0x101; + let food_owner2 = @0x102; + let food_owner3 = @0x103; + + record_food_consumption(&mut fish, food_owner1, 5); + record_food_consumption(&mut fish, food_owner2, 10); + record_food_consumption(&mut fish, food_owner3, 15); + + assert!(get_total_food_consumed(&fish) == 30, 1); + assert!(get_contributor_amount(&fish, food_owner1) == 5, 2); + assert!(get_contributor_amount(&fish, food_owner2) == 10, 3); + assert!(get_contributor_amount(&fish, food_owner3) == 15, 4); + + let contributors = get_food_contributors(&fish); + assert!(vector::length(&contributors) == 3, 5); + assert!(vector::contains(&contributors, &food_owner1), 6); + assert!(vector::contains(&contributors, &food_owner2), 7); + assert!(vector::contains(&contributors, &food_owner3), 8); + + drop_fish(fish); + } + + #[test(owner = @0x42)] + fun test_grow_fish(owner: signer) { + let owner_addr = signer::address_of(&owner); + let fish = create_fish(owner_addr, 1, 10, 5, 5); + + grow_fish(&mut fish, 5); + assert!(get_size(&fish) == 15, 1); + + grow_fish(&mut fish, 10); + assert!(get_size(&fish) == 25, 2); + + drop_fish(fish); + } + + #[test(owner = @0x42, non_owner = @0x43)] + #[expected_failure(abort_code = ErrorNotOwner)] + fun test_move_fish_non_owner(owner: signer, non_owner: signer) { + let owner_addr = signer::address_of(&owner); + let fish = create_fish(owner_addr, 1, 10, 5, 5); + move_fish(&non_owner, &mut fish, 0); + drop_fish(fish); + } + + #[test(owner = @0x42)] + #[expected_failure(abort_code = ErrorInvalidDirection)] + fun test_move_fish_invalid_direction(owner: signer) { + let owner_addr = signer::address_of(&owner); + let fish = create_fish(owner_addr, 1, 10, 5, 5); + move_fish(&owner, &mut fish, 4); + drop_fish(fish); + } + + #[test] + fun test_non_contributor() { + let fish = create_fish(@0x1, 1, 10, 5, 5); + let non_contributor = @0x999; + + assert!(get_contributor_amount(&fish, non_contributor) == 0, 1); + assert!(!simple_map::contains_key(&fish.food_contributors, &non_contributor), 2); + + let contributors = get_food_contributors(&fish); + assert!(vector::length(&contributors) == 0, 3); + assert!(!vector::contains(&contributors, &non_contributor), 4); + + drop_fish(fish); + } + + #[test] + fun test_position_getters() { + let fish = create_fish(@0x1, 1, 10, 5, 5); + + assert!(get_x(&fish) == 5, 1); + assert!(get_y(&fish) == 5, 2); + + let (x, y) = get_position(&fish); + assert!(x == 5, 3); + assert!(y == 5, 4); + + move_fish_to_for_test(&mut fish, 10, 15); + assert!(get_x(&fish) == 10, 5); + assert!(get_y(&fish) == 15, 6); + + drop_fish(fish); + } +} diff --git a/examples/rooch_fish/sources/food.move b/examples/rooch_fish/sources/food.move new file mode 100644 index 0000000000..90c1bd4f2c --- /dev/null +++ b/examples/rooch_fish/sources/food.move @@ -0,0 +1,162 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +module rooch_fish::food { + use std::vector; + + friend rooch_fish::rooch_fish; + friend rooch_fish::pond; + + struct Food has key, store { + id: u64, + owner: address, + size: u64, + x: u64, + y: u64, + } + + public(friend) fun create_food(id: u64, owner: address, size: u64, x: u64, y: u64): Food { + Food { + id, + owner, + size, + x, + y, + } + } + + public(friend) fun create_multiple_food( + ids: vector, + owner: address, + sizes: vector, + xs: vector, + ys: vector + ): vector { + let len = vector::length(&ids); + assert!(len == vector::length(&sizes) && len == vector::length(&xs) && len == vector::length(&ys), 0); + + let foods = vector::empty(); + let i = 0; + while (i < len) { + let food = create_food( + *vector::borrow(&ids, i), + owner, + *vector::borrow(&sizes, i), + *vector::borrow(&xs, i), + *vector::borrow(&ys, i) + ); + vector::push_back(&mut foods, food); + i = i + 1; + }; + foods + } + + public fun get_id(food: &Food): u64 { + food.id + } + + public fun get_owner(food: &Food): address { + food.owner + } + + public fun get_size(food: &Food): u64 { + food.size + } + + public fun get_position(food: &Food): (u64, u64) { + (food.x, food.y) + } + + #[test_only] + public fun set_position_for_test(food: &mut Food, x: u64, y: u64) { + food.x = x; + food.y = y; + } + + public(friend) fun drop_food(food: Food) { + let Food { id: _, owner: _, size: _, x: _, y: _ } = food; + } + + #[test_only] + use moveos_std::signer; + + #[test] + fun test_create_food() { + let id = 1; + let owner = @0x42; + let size = 5; + let x = 10; + let y = 20; + + let food = create_food(id, owner, size, x, y); + + assert!(get_id(&food) == id, 1); + assert!(get_owner(&food) == owner, 2); + assert!(get_size(&food) == size, 3); + + let (food_x, food_y) = get_position(&food); + assert!(food_x == x, 4); + assert!(food_y == y, 5); + + drop_food(food); + } + + #[test] + fun test_create_multiple_food() { + let ids = vector[1, 2, 3]; + let owner = @0x42; + let sizes = vector[5, 10, 15]; + let xs = vector[10, 20, 30]; + let ys = vector[40, 50, 60]; + + let foods = create_multiple_food(ids, owner, sizes, xs, ys); + assert!(vector::length(&foods) == 3, 1); + + let food = vector::pop_back(&mut foods); + assert!(get_id(&food) == 3, 2); + assert!(get_owner(&food) == owner, 3); + assert!(get_size(&food) == 15, 4); + let (x, y) = get_position(&food); + assert!(x == 30 && y == 60, 5); + + drop_food(food); + vector::for_each(foods, |food| drop_food(food)); + } + + #[test(account = @0x42)] + fun test_food_ownership(account: signer) { + let owner = signer::address_of(&account); + let food = create_food(1, owner, 5, 10, 20); + + assert!(get_owner(&food) == owner, 1); + + drop_food(food); + } + + #[test] + fun test_position_setters() { + let food = create_food(1, @0x42, 5, 10, 20); + + set_position_for_test(&mut food, 30, 40); + let (x, y) = get_position(&food); + assert!(x == 30 && y == 40, 1); + + drop_food(food); + } + + #[test] + fun test_multiple_food_validation() { + let ids = vector[1, 2]; + let sizes = vector[5, 10, 15]; // Mismatched length + let xs = vector[10, 20]; + let ys = vector[40, 50]; + + assert!( + vector::length(&ids) != vector::length(&sizes) || + vector::length(&xs) != vector::length(&ys) || + vector::length(&ids) != vector::length(&xs), + 0 + ); + } +} + diff --git a/examples/rooch_fish/sources/player.move b/examples/rooch_fish/sources/player.move new file mode 100644 index 0000000000..cf27a5f486 --- /dev/null +++ b/examples/rooch_fish/sources/player.move @@ -0,0 +1,404 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +module rooch_fish::player { + use moveos_std::table::{Self, Table}; + use std::option::{Self, Option}; + use std::vector; + + friend rooch_fish::rooch_fish; + friend rooch_fish::pond; + + /// Error codes + const ErrorOverflow: u64 = 1001; + const ErrorInvalidPlayer: u64 = 1002; + const ErrorFishNotFound: u64 = 1003; + + /// Maximum value for u64 + const U64_MAX: u64 = 18446744073709551615; + + /// Maximum value for u256 + const U256_MAX: u256 = 115792089237316195423570985008687907853269984665640564039457584007913129639935; + + /// Represents the state of a player + struct PlayerState has key, store, copy, drop { + owner: address, + feed_amount: u256, + reward: u256, + fish_count: u64, + fish_ids: vector, + } + + /// Represents the list of all players + struct PlayerList has key, store { + players: Table, + total_feed: u256, + player_count: u64, + } + + /// Creates a new player list + public(friend) fun create_player_list(): PlayerList { + PlayerList { + players: table::new(), + total_feed: 0, + player_count: 0, + } + } + + /// Adds a fish for a player + /// Aborts if the addition would cause an overflow + public(friend) fun add_fish(player_list: &mut PlayerList, owner: address, fish_id: u64) { + let player_state = get_or_create_player_state(player_list, owner); + assert!(player_state.fish_count < U64_MAX, ErrorOverflow); + player_state.fish_count = player_state.fish_count + 1; + vector::push_back(&mut player_state.fish_ids, fish_id); + } + + /// Removes a fish from a player + /// Aborts if the player or fish doesn't exist + public(friend) fun remove_fish(player_list: &mut PlayerList, owner: address, fish_id: u64) { + let player_state = get_or_create_player_state(player_list, owner); + assert!(player_state.fish_count > 0, ErrorInvalidPlayer); + player_state.fish_count = player_state.fish_count - 1; + let (found, index) = vector::index_of(&player_state.fish_ids, &fish_id); + assert!(found, ErrorFishNotFound); + vector::remove(&mut player_state.fish_ids, index); + } + + /// Adds feed for a player + /// Aborts if the addition would cause an overflow + public(friend) fun add_feed(player_list: &mut PlayerList, owner: address, amount: u256) { + assert!(player_list.total_feed <= U256_MAX - amount, ErrorOverflow); + let player_state = get_or_create_player_state(player_list, owner); + assert!(player_state.feed_amount <= U256_MAX - amount, ErrorOverflow); + player_state.feed_amount = player_state.feed_amount + amount; + player_list.total_feed = player_list.total_feed + amount; + } + + /// Adds reward for a player + /// Aborts if the addition would cause an overflow + public(friend) fun add_reward(player_list: &mut PlayerList, owner: address, amount: u256) { + let player_state = get_or_create_player_state(player_list, owner); + assert!(player_state.reward <= U256_MAX - amount, ErrorOverflow); + player_state.reward = player_state.reward + amount; + } + + /// Gets the state of a player + /// Aborts if the player does not exist + public fun get_state(player_list: &PlayerList, owner: address): PlayerState { + assert!(player_exists(player_list, owner), ErrorInvalidPlayer); + *table::borrow(&player_list.players, owner) + } + + /// Safely gets the state of a player + /// Returns None if the player does not exist + public fun get_state_safe(player_list: &PlayerList, owner: address): Option { + if (player_exists(player_list, owner)) { + option::some(*table::borrow(&player_list.players, owner)) + } else { + option::none() + } + } + + /// Gets the feed amount of a player + public fun get_player_feed_amount(player_list: &PlayerList, owner: address): u256 { + assert!(player_exists(player_list, owner), ErrorInvalidPlayer); + table::borrow(&player_list.players, owner).feed_amount + } + + /// Gets the reward of a player + public fun get_player_reward(player_list: &PlayerList, owner: address): u256 { + assert!(player_exists(player_list, owner), ErrorInvalidPlayer); + table::borrow(&player_list.players, owner).reward + } + + /// Gets the fish count of a player + public fun get_player_fish_count(player_list: &PlayerList, owner: address): u64 { + assert!(player_exists(player_list, owner), ErrorInvalidPlayer); + table::borrow(&player_list.players, owner).fish_count + } + + /// Gets the fish IDs of a player + public fun get_player_fish_ids(player_list: &PlayerList, owner: address): vector { + assert!(player_exists(player_list, owner), ErrorInvalidPlayer); + *&table::borrow(&player_list.players, owner).fish_ids + } + + /// Checks if a player exists + public fun player_exists(player_list: &PlayerList, owner: address): bool { + table::contains(&player_list.players, owner) + } + + /// Gets or creates a player state + fun get_or_create_player_state(player_list: &mut PlayerList, owner: address): &mut PlayerState { + if (!table::contains(&player_list.players, owner)) { + let new_state = create_player_state(owner); + table::add(&mut player_list.players, owner, new_state); + player_list.player_count = player_list.player_count + 1; + }; + table::borrow_mut(&mut player_list.players, owner) + } + + /// Creates a new player state + fun create_player_state(owner: address): PlayerState { + PlayerState { + owner, + feed_amount: 0, + reward: 0, + fish_count: 0, + fish_ids: vector::empty(), + } + } + + /// Gets the total feed amount + public fun get_total_feed(player_list: &PlayerList): u256 { + player_list.total_feed + } + + /// Gets the total player count + public fun get_player_count(player_list: &PlayerList): u64 { + player_list.player_count + } + + /// Drops the player list + public fun drop_player_list(player_list: PlayerList) { + let PlayerList { players, total_feed: _, player_count: _ } = player_list; + table::drop(players); + } + + #[test] + fun test_create_player_list() { + let player_list = create_player_list(); + assert!(table::is_empty(&player_list.players), 1); + assert!(player_list.total_feed == 0, 2); + assert!(player_list.player_count == 0, 3); + + drop_player_list(player_list); + } + + #[test] + fun test_add_feed() { + let player_list = create_player_list(); + let owner = @0x1; + + add_feed(&mut player_list, owner, 100); + let state = get_state(&player_list, owner); + assert!(state.feed_amount == 100, 1); + assert!(state.reward == 0, 2); + assert!(get_total_feed(&player_list) == 100, 3); + assert!(get_player_count(&player_list) == 1, 4); + + add_feed(&mut player_list, owner, 50); + let state = get_state(&player_list, owner); + assert!(state.feed_amount == 150, 5); + assert!(state.reward == 0, 6); + assert!(get_total_feed(&player_list) == 150, 7); + assert!(get_player_count(&player_list) == 1, 8); + + let new_owner = @0x2; + add_feed(&mut player_list, new_owner, 75); + let state = get_state(&player_list, new_owner); + assert!(state.feed_amount == 75, 9); + assert!(state.reward == 0, 10); + assert!(get_total_feed(&player_list) == 225, 11); + assert!(get_player_count(&player_list) == 2, 12); + + drop_player_list(player_list); + } + + #[test] + fun test_add_reward() { + let player_list = create_player_list(); + let owner = @0x1; + + add_reward(&mut player_list, owner, 50); + let state = get_state(&player_list, owner); + assert!(state.feed_amount == 0, 1); + assert!(state.reward == 50, 2); + assert!(get_player_count(&player_list) == 1, 3); + + add_reward(&mut player_list, owner, 30); + let state = get_state(&player_list, owner); + assert!(state.feed_amount == 0, 4); + assert!(state.reward == 80, 5); + assert!(get_player_count(&player_list) == 1, 6); + + let new_owner = @0x2; + add_reward(&mut player_list, new_owner, 100); + let state = get_state(&player_list, new_owner); + assert!(state.feed_amount == 0, 7); + assert!(state.reward == 100, 8); + assert!(get_player_count(&player_list) == 2, 9); + + assert!(get_total_feed(&player_list) == 0, 10); + + drop_player_list(player_list); + } + + #[test] + fun test_get_state_existing_players() { + let player_list = create_player_list(); + let owner1 = @0x1; + let owner2 = @0x2; + + add_feed(&mut player_list, owner1, 100); + add_reward(&mut player_list, owner1, 50); + add_feed(&mut player_list, owner2, 200); + + let state1 = get_state(&player_list, owner1); + assert!(state1.owner == owner1, 1); + assert!(state1.feed_amount == 100, 2); + assert!(state1.reward == 50, 3); + + let state2 = get_state(&player_list, owner2); + assert!(state2.owner == owner2, 4); + assert!(state2.feed_amount == 200, 5); + assert!(state2.reward == 0, 6); + + assert!(get_player_count(&player_list) == 2, 7); + + drop_player_list(player_list); + } + + #[test] + #[expected_failure(abort_code = ErrorInvalidPlayer)] + fun test_get_state_non_existent_player() { + let player_list = create_player_list(); + let non_existent_owner = @0x3; + + let _ = get_state(&player_list, non_existent_owner); + + drop_player_list(player_list); + } + + #[test] + fun test_get_state_safe() { + let player_list = create_player_list(); + let owner = @0x1; + let non_existent_owner = @0x2; + + add_feed(&mut player_list, owner, 100); + + let state_option = get_state_safe(&player_list, owner); + assert!(option::is_some(&state_option), 1); + let state = option::extract(&mut state_option); + assert!(state.feed_amount == 100, 2); + + let state_option = get_state_safe(&player_list, non_existent_owner); + assert!(option::is_none(&state_option), 3); + + drop_player_list(player_list); + } + + #[test] + fun test_player_exists() { + let player_list = create_player_list(); + let owner = @0x1; + let non_existent_owner = @0x2; + + add_feed(&mut player_list, owner, 100); + + assert!(player_exists(&player_list, owner), 1); + assert!(!player_exists(&player_list, non_existent_owner), 2); + + drop_player_list(player_list); + } + + #[test] + #[expected_failure(abort_code = ErrorOverflow)] + fun test_add_feed_overflow() { + let player_list = create_player_list(); + let owner = @0x1; + + add_feed(&mut player_list, owner, U256_MAX); + add_feed(&mut player_list, owner, 1); + + drop_player_list(player_list); + } + + #[test] + #[expected_failure(abort_code = ErrorOverflow)] + fun test_add_reward_overflow() { + let player_list = create_player_list(); + let owner = @0x1; + + add_reward(&mut player_list, owner, U256_MAX); + add_reward(&mut player_list, owner, 1); + + drop_player_list(player_list); + } + + #[test] + fun test_add_and_remove_fish() { + let player_list = create_player_list(); + let owner = @0x1; + + add_fish(&mut player_list, owner, 1); + let state = get_state(&player_list, owner); + assert!(state.fish_count == 1, 1); + assert!(vector::length(&state.fish_ids) == 1, 2); + assert!(*vector::borrow(&state.fish_ids, 0) == 1, 3); + + add_fish(&mut player_list, owner, 2); + let state = get_state(&player_list, owner); + assert!(state.fish_count == 2, 4); + assert!(vector::length(&state.fish_ids) == 2, 5); + + remove_fish(&mut player_list, owner, 1); + let state = get_state(&player_list, owner); + assert!(state.fish_count == 1, 6); + assert!(vector::length(&state.fish_ids) == 1, 7); + assert!(*vector::borrow(&state.fish_ids, 0) == 2, 8); + + let fish_ids = get_player_fish_ids(&player_list, owner); + assert!(vector::length(&fish_ids) == 1, 9); + assert!(*vector::borrow(&fish_ids, 0) == 2, 10); + + drop_player_list(player_list); + } + + #[test] + #[expected_failure(abort_code = ErrorFishNotFound)] + fun test_remove_non_existent_fish() { + let player_list = create_player_list(); + let owner = @0x1; + + add_fish(&mut player_list, owner, 1); + remove_fish(&mut player_list, owner, 2); + + drop_player_list(player_list); + } + + #[test] + fun test_player_getters() { + let player_list = create_player_list(); + let owner = @0x1; + + add_feed(&mut player_list, owner, 100); + add_reward(&mut player_list, owner, 50); + add_fish(&mut player_list, owner, 1); + add_fish(&mut player_list, owner, 2); + + assert!(get_player_feed_amount(&player_list, owner) == 100, 2); + assert!(get_player_reward(&player_list, owner) == 50, 3); + assert!(get_player_fish_count(&player_list, owner) == 2, 4); + + let fish_ids = get_player_fish_ids(&player_list, owner); + assert!(vector::length(&fish_ids) == 2, 5); + assert!(*vector::borrow(&fish_ids, 0) == 1, 6); + assert!(*vector::borrow(&fish_ids, 1) == 2, 7); + + drop_player_list(player_list); + } + + #[test] + #[expected_failure(abort_code = ErrorInvalidPlayer)] + fun test_player_getters_invalid_player() { + let player_list = create_player_list(); + let non_existent_owner = @0x2; + + let _ = get_player_feed_amount(&player_list, non_existent_owner); + + drop_player_list(player_list); + } +} diff --git a/examples/rooch_fish/sources/pond.move b/examples/rooch_fish/sources/pond.move new file mode 100644 index 0000000000..a8951db1ff --- /dev/null +++ b/examples/rooch_fish/sources/pond.move @@ -0,0 +1,1301 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +module rooch_fish::pond { + use std::vector; + use std::u64; + + use moveos_std::object::{Self, Object}; + use moveos_std::signer; + use moveos_std::table::{Self, Table}; + use moveos_std::event; + + use rooch_framework::account_coin_store; + use rooch_framework::coin_store::{Self, CoinStore}; + use rooch_framework::gas_coin::{Self, RGas}; + + use rooch_fish::fish::{Self, Fish}; + use rooch_fish::food::{Self, Food}; + use rooch_fish::utils; + use rooch_fish::player::{Self, PlayerList}; + use rooch_fish::quad_tree; + + friend rooch_fish::rooch_fish; + + const ErrorInsufficientBalance: u64 = 1; + const ErrorFishNotFound: u64 = 2; + const ErrorFoodNotFound : u64 = 3; + const ErrorMaxFishCountReached : u64 = 4; + const ErrorMaxFoodCountReached: u64 = 5; + const ErrorUnauthorized: u64 = 6; + const ErrorFishNotInExitZone: u64 = 7; + const ErrorInvalidPosition: u64 = 8; + const ErrorFishOutOfBounds: u64 = 9; + const ErrorInsufficientTreasuryBalance: u64 = 10; + + const OBJECT_TYPE_FISH: u8 = 1; + const OBJECT_TYPE_FOOD: u8 = 2; + + const MAX_FOOD_PER_FEED: u64 = 20; + const FOOD_VALUE_RATIO: u64 = 10; + const MAX_FISH_SIZE: u64 = 100; + const BURST_FOOD_COUNT: u64 = 10; + + struct ExitZone has store, copy, drop { + x: u64, + y: u64, + radius: u64, + } + + struct Treasury has key, store { + coin_store: Object> + } + + struct PondState has key, store { + id: u64, + owner: address, + fishes: Table, + foods: Table, + exit_zones: vector, + quad_tree: quad_tree::QuadTree, + fish_count: u64, + food_count: u64, + width: u64, + height: u64, + purchase_amount: u256, + next_fish_id: u64, + next_food_id: u64, + max_fish_count: u64, + max_food_count: u64, + treasury: Treasury, + player_list: PlayerList, + } + + struct BurstEvent has copy, drop, store { + pond_id: u64, + fish_id: u64, + owner: address, + size: u64, + food_count: u64, + total_reward: u256 + } + + struct FishPurchasedEvent has copy, drop, store { + pond_id: u64, + fish_id: u64, + owner: address, + } + + struct FishMovedEvent has copy, drop, store { + pond_id: u64, + fish_id: u64, + new_x: u64, + new_y: u64, + } + + struct FishDestroyedEvent has copy, drop, store { + pond_id: u64, + fish_id: u64, + reward: u256, + } + + public(friend) fun create_pond( + id: u64, + owner: address, + width: u64, + height: u64, + purchase_amount: u256, + max_fish_count: u64, + max_food_count: u64 + ): Object { + let pond_state = PondState { + id, + owner, + fishes: table::new(), + foods: table::new(), + exit_zones: vector::empty(), + quad_tree: quad_tree::create_quad_tree(width, height), + fish_count: 0, + food_count: 0, + width, + height, + purchase_amount, + next_fish_id: 1, + next_food_id: 1, + max_fish_count, + max_food_count, + treasury: Treasury { coin_store: coin_store::create_coin_store() }, + player_list: player::create_player_list(), + }; + object::new(pond_state) + } + + public(friend) fun purchase_fish(pond_state: &mut PondState, account: &signer): u64 { + let account_addr = signer::address_of(account); + assert!(gas_coin::balance(account_addr) >= pond_state.purchase_amount, ErrorInsufficientBalance); + + let coin = account_coin_store::withdraw(account, pond_state.purchase_amount); + coin_store::deposit(&mut pond_state.treasury.coin_store, coin); + + let (x, y) = utils::random_position(0, pond_state.width, pond_state.height); + let fish_id = pond_state.next_fish_id; + pond_state.next_fish_id = pond_state.next_fish_id + 1; + let fish = fish::create_fish(account_addr, fish_id, 10, x, y); + add_fish(pond_state, fish); + + player::add_fish(&mut pond_state.player_list, account_addr, fish_id); + + event::emit(FishPurchasedEvent { pond_id: pond_state.id, fish_id, owner: account_addr }); + + fish_id + } + + public(friend) fun move_fish(pond_state: &mut PondState, account: &signer, fish_id: u64, direction: u8): (u64, u64) { + let account_addr = signer::address_of(account); + let fish = get_fish_mut(pond_state, fish_id); + assert!(fish::get_owner(fish) == account_addr, ErrorUnauthorized); + + let (old_x, old_y) = fish::get_position(fish); + fish::move_fish(account, fish, direction); + + let (new_x, new_y) = fish::get_position(fish); + assert!(new_x < pond_state.width && new_y < pond_state.height, ErrorFishOutOfBounds); + + // Update position in quad tree + quad_tree::update_object_position( + &mut pond_state.quad_tree, + fish_id, + OBJECT_TYPE_FISH, + old_x, + old_y, + new_x, + new_y + ); + + handle_collisions(pond_state, fish_id); + + event::emit(FishMovedEvent { + pond_id: pond_state.id, + fish_id, + new_x, + new_y + }); + + (new_x, new_y) + } + + public(friend) fun feed_food(pond_state: &mut PondState, account: &signer, count: u64) : u256 { + let account_addr = signer::address_of(account); + + // Ensure food count does not exceed limits + let actual_count = u64::min(count, MAX_FOOD_PER_FEED); + assert!(pond_state.food_count + actual_count <= pond_state.max_food_count, ErrorMaxFoodCountReached); + + // Calculate actual cost + let food_value = pond_state.purchase_amount / (FOOD_VALUE_RATIO as u256); + let total_cost = (actual_count as u256) * food_value; + + // Verify and transfer payment + assert!(gas_coin::balance(account_addr) >= total_cost, ErrorInsufficientBalance); + let coin = account_coin_store::withdraw(account, total_cost); + coin_store::deposit(&mut pond_state.treasury.coin_store, coin); + + // Create food objects + let i = 0; + while (i < actual_count) { + let (x, y) = utils::random_position(i * 2, pond_state.width, pond_state.height); + let food = food::create_food( + pond_state.next_food_id, + account_addr, + 1, + x, + y + ); + add_food(pond_state, food); + pond_state.next_food_id = pond_state.next_food_id + 1; + i = i + 1; + }; + + player::add_feed(&mut pond_state.player_list, account_addr, total_cost); + + total_cost + } + + public(friend) fun destroy_fish(pond_state: &mut PondState, account: &signer, fish_id: u64): u256 { + let account_addr = signer::address_of(account); + let fish = get_fish(pond_state, fish_id); + assert!(fish::get_owner(fish) == account_addr, ErrorUnauthorized); + assert!(is_fish_in_exit_zone(pond_state, fish), ErrorFishNotInExitZone); + + let removed_fish = remove_fish(pond_state, fish_id); + player::remove_fish(&mut pond_state.player_list, account_addr, fish_id); + + // Calculate total reward + let total_reward = calculate_reward(&removed_fish, pond_state); + + // 1% goes to developer + let dev_reward = total_reward / 100; + let dev_coin = coin_store::withdraw(&mut pond_state.treasury.coin_store, dev_reward); + account_coin_store::deposit(pond_state.owner, dev_coin); + + // 20% distributed proportionally among food contributors + let contributor_reward = (total_reward * 20) / 100; + let total_food = fish::get_total_food_consumed(&removed_fish); + + if (total_food > 0) { + let contributors = fish::get_food_contributors(&removed_fish); + let i = 0; + while (i < vector::length(&contributors)) { + let contributor = *vector::borrow(&contributors, i); + let amount = fish::get_contributor_amount(&removed_fish, contributor); + let reward = contributor_reward * (amount as u256) / (total_food as u256); + + if (reward > 0) { + let reward_coin = coin_store::withdraw(&mut pond_state.treasury.coin_store, reward); + account_coin_store::deposit(contributor, reward_coin); + }; + + i = i + 1; + }; + }; + + // Remaining 79% goes to fish owner + let owner_reward = total_reward - dev_reward - contributor_reward; + let owner_coin = coin_store::withdraw(&mut pond_state.treasury.coin_store, owner_reward); + account_coin_store::deposit(account_addr, owner_coin); + + event::emit(FishDestroyedEvent { + pond_id: pond_state.id, + fish_id, + reward: total_reward + }); + + fish::drop_fish(removed_fish); + + total_reward + } + + fun add_fish(pond_state: &mut PondState, fish: Fish) { + assert!(pond_state.fish_count < pond_state.max_fish_count, ErrorMaxFishCountReached ); + + let id = fish::get_id(&fish); + let (_, _, _, x, y) = fish::get_fish_info(&fish); + quad_tree::insert_object(&mut pond_state.quad_tree, id, OBJECT_TYPE_FISH, x, y); + + table::add(&mut pond_state.fishes, id, fish); + pond_state.fish_count = pond_state.fish_count + 1; + } + + fun remove_fish(pond_state: &mut PondState, fish_id: u64): Fish { + let fish = table::remove(&mut pond_state.fishes, fish_id); + let (_, _, _, x, y) = fish::get_fish_info(&fish); + quad_tree::remove_object(&mut pond_state.quad_tree, fish_id, OBJECT_TYPE_FISH, x, y); + + pond_state.fish_count = pond_state.fish_count - 1; + fish + } + + public fun get_fish(pond_state: &PondState, fish_id: u64): &Fish { + table::borrow(&pond_state.fishes, fish_id) + } + + fun get_fish_mut(pond_state: &mut PondState, fish_id: u64): &mut Fish { + table::borrow_mut(&mut pond_state.fishes, fish_id) + } + + fun add_food(pond_state: &mut PondState, food: Food) { + assert!(pond_state.food_count < pond_state.max_food_count, ErrorMaxFoodCountReached); + + let id = food::get_id(&food); + let (x, y) = food::get_position(&food); + quad_tree::insert_object(&mut pond_state.quad_tree, id, OBJECT_TYPE_FOOD, x, y); + + table::add(&mut pond_state.foods, id, food); + pond_state.food_count = pond_state.food_count + 1; + } + + fun remove_food(pond_state: &mut PondState, food_id: u64): Food { + let food = table::remove(&mut pond_state.foods, food_id); + let (x, y) = food::get_position(&food); + quad_tree::remove_object(&mut pond_state.quad_tree, food_id, OBJECT_TYPE_FOOD, x, y); + + pond_state.food_count = pond_state.food_count - 1; + food + } + + public fun get_food(pond_state: &PondState, food_id: u64): &Food { + table::borrow(&pond_state.foods, food_id) + } + + fun get_food_mut(pond_state: &mut PondState, food_id: u64): &mut Food { + table::borrow_mut(&mut pond_state.foods, food_id) + } + + fun handle_collisions(pond_state: &mut PondState, fish_id: u64) { + let fish = get_fish(pond_state, fish_id); + let fish_size = fish::get_size(fish); + let (fish_x, fish_y) = fish::get_position(fish); + + if (fish_size >= MAX_FISH_SIZE) { + process_burst(pond_state, fish_id); + return + }; + + let query_range = fish_size * 2; + let nearby_objects = quad_tree::query_range( + &pond_state.quad_tree, + utils::saturating_sub(fish_x, query_range), + utils::saturating_sub(fish_y, query_range), + query_range * 2, + query_range * 2, + ); + + let nearby_fish = vector::empty(); + let nearby_food = vector::empty(); + + let i = 0; + while (i < vector::length(&nearby_objects)) { + let object_entry = vector::borrow(&nearby_objects, i); + if (quad_tree::get_object_entry_type(object_entry) == OBJECT_TYPE_FISH && + quad_tree::get_object_entry_id(object_entry) != fish_id) { + vector::push_back(&mut nearby_fish, quad_tree::get_object_entry_id(object_entry)); + } else if (quad_tree::get_object_entry_type(object_entry) == OBJECT_TYPE_FOOD) { + vector::push_back(&mut nearby_food, quad_tree::get_object_entry_id(object_entry)); + }; + i = i + 1; + }; + + handle_food_collisions(pond_state, fish_id, fish_size, fish_x, fish_y, nearby_food); + handle_fish_collisions(pond_state, fish_id, fish_size, fish_x, fish_y, nearby_fish); + } + + + fun process_burst(pond_state: &mut PondState, fish_id: u64) { + let fish = remove_fish(pond_state, fish_id); + let owner = fish::get_owner(&fish); + let size = fish::get_size(&fish); + + // Calculate total reward + let total_reward = calculate_reward(&fish, pond_state); + + // Owner reward (1%) + let dev_reward = total_reward / 100; + let dev_coin = coin_store::withdraw(&mut pond_state.treasury.coin_store, dev_reward); + account_coin_store::deposit(pond_state.owner, dev_coin); + + // Contributor rewards (20%) + let contributor_reward = (total_reward * 20) / 100; + let total_food = fish::get_total_food_consumed(&fish); + + if (total_food > 0) { + let contributors = fish::get_food_contributors(&fish); + let i = 0; + while (i < vector::length(&contributors)) { + let contributor = *vector::borrow(&contributors, i); + let amount = fish::get_contributor_amount(&fish, contributor); + let reward = contributor_reward * (amount as u256) / (total_food as u256); + + if (reward > 0) { + let reward_coin = coin_store::withdraw(&mut pond_state.treasury.coin_store, reward); + account_coin_store::deposit(contributor, reward_coin); + }; + + i = i + 1; + }; + }; + + // Generate burst food + let food_size = size / BURST_FOOD_COUNT; + let i = 0; + while (i < BURST_FOOD_COUNT) { + let (x, y) = utils::random_position( + pond_state.next_food_id + i, + pond_state.width, + pond_state.height + ); + + let food = food::create_food( + pond_state.next_food_id, + owner, // Set burst fish owner as food owner + food_size, + x, + y + ); + add_food(pond_state, food); + pond_state.next_food_id = pond_state.next_food_id + 1; + i = i + 1; + }; + + event::emit(BurstEvent { + pond_id: pond_state.id, + fish_id, + owner, + size, + food_count: BURST_FOOD_COUNT, + total_reward + }); + + fish::drop_fish(fish); + } + + fun handle_food_collisions(pond_state: &mut PondState, fish_id: u64, fish_size: u64, fish_x: u64, fish_y: u64, nearby_food: vector) { + let growth_amount = 0u64; + let food_ids_to_remove = vector::empty(); + let food_owners = vector::empty
(); + let food_sizes = vector::empty(); + + // First pass: collect all foods to be eaten and their info + let i = 0; + while (i < vector::length(&nearby_food)) { + let food_id = *vector::borrow(&nearby_food, i); + + if (table::contains(&pond_state.foods, food_id)) { + let food = get_food(pond_state, food_id); + let (food_x, food_y) = food::get_position(food); + if (utils::calculate_distance(fish_x, fish_y, food_x, food_y) <= fish_size) { + growth_amount = growth_amount + food::get_size(food); + vector::push_back(&mut food_ids_to_remove, food_id); + vector::push_back(&mut food_owners, food::get_owner(food)); + vector::push_back(&mut food_sizes, food::get_size(food)); + }; + }; + + i = i + 1; + }; + + // Second pass: update fish + let fish_mut = get_fish_mut(pond_state, fish_id); + fish::grow_fish(fish_mut, growth_amount); + + let j = 0; + while (j < vector::length(&food_owners)) { + let food_owner = *vector::borrow(&food_owners, j); + let food_size = *vector::borrow(&food_sizes, j); + + fish::record_food_consumption(fish_mut, food_owner, food_size); + j = j + 1; + }; + + // Third pass: remove foods + let k = 0; + while (k < vector::length(&food_ids_to_remove)) { + let food_id = *vector::borrow(&food_ids_to_remove, k); + let food = remove_food(pond_state, food_id); + food::drop_food(food); + + k = k + 1; + }; + } + + fun handle_fish_collisions(pond_state: &mut PondState, fish_id: u64, fish_size: u64, fish_x: u64, fish_y: u64, nearby_fish: vector) { + let growth_amount = 0u64; + let fish_ids_to_remove = vector::empty(); + + let i = 0; + while (i < vector::length(&nearby_fish)) { + let other_fish_id = *vector::borrow(&nearby_fish, i); + + if (table::contains(&pond_state.fishes, other_fish_id)) { + let other_fish = get_fish(pond_state, other_fish_id); + let (other_x, other_y) = fish::get_position(other_fish); + let other_size = fish::get_size(other_fish); + + if (!fish::is_protected(other_fish) && + utils::calculate_distance(fish_x, fish_y, other_x, other_y) <= fish_size && fish_size > other_size) { + growth_amount = growth_amount + (other_size / 2); + vector::push_back(&mut fish_ids_to_remove, other_fish_id); + }; + }; + + i = i + 1; + }; + + let fish_mut = get_fish_mut(pond_state, fish_id); + fish::grow_fish(fish_mut, growth_amount); + + let j = 0; + while (j < vector::length(&fish_ids_to_remove)) { + let fish_id = *vector::borrow(&fish_ids_to_remove, j); + let fish_obj = remove_fish(pond_state, fish_id); + let owner = fish::get_owner(&fish_obj); + player::remove_fish(&mut pond_state.player_list, owner, fish_id); + fish::drop_fish(fish_obj); + j = j + 1; + }; + } + + fun calculate_reward(fish: &Fish, pond_state: &PondState): u256 { + let base_reward = (fish::get_size(fish) as u256); + base_reward * pond_state.purchase_amount / 100 + } + + public(friend) fun add_exit_zone(pond_state: &mut PondState, x: u64, y: u64, radius: u64) { + let exit_zone = ExitZone { x, y, radius }; + vector::push_back(&mut pond_state.exit_zones, exit_zone); + } + + public(friend) fun remove_exit_zone(pond_state: &mut PondState, index: u64) { + vector::swap_remove(&mut pond_state.exit_zones, index); + } + + public fun is_fish_in_exit_zone(pond_state: &PondState, fish: &Fish): bool { + let (fish_x, fish_y) = fish::get_position(fish); + let len = vector::length(&pond_state.exit_zones); + let i = 0; + while (i < len) { + let exit_zone = vector::borrow(&pond_state.exit_zones, i); + if (is_point_in_circle(fish_x, fish_y, exit_zone.x, exit_zone.y, exit_zone.radius)) { + return true + }; + i = i + 1; + }; + false + } + + fun is_point_in_circle(px: u64, py: u64, cx: u64, cy: u64, radius: u64): bool { + let dx = if (px > cx) { px - cx } else { cx - px }; + let dy = if (py > cy) { py - cy } else { cy - py }; + (dx * dx + dy * dy) <= (radius * radius) + } + + public fun get_pond_id(pond_state: &PondState): u64 { + pond_state.id + } + + public fun get_width(pond_state: &PondState): u64 { + pond_state.width + } + + public fun get_height(pond_state: &PondState): u64 { + pond_state.height + } + + public fun get_purchase_amount(pond_state: &PondState): u256 { + pond_state.purchase_amount + } + + public fun get_max_fish_count(pond_state: &PondState): u64 { + pond_state.max_fish_count + } + + public fun get_max_food_count(pond_state: &PondState): u64 { + pond_state.max_food_count + } + + public fun get_fish_count(pond_state: &PondState): u64 { + pond_state.fish_count + } + + public fun get_food_count(pond_state: &PondState): u64 { + pond_state.food_count + } + + public fun get_player_list(pond_state: &PondState): &PlayerList { + &pond_state.player_list + } + + public fun get_player_count(pond_state: &PondState): u64 { + player::get_player_count(&pond_state.player_list) + } + + public fun get_player_fish_ids(pond_state: &PondState, owner: address): vector { + player::get_player_fish_ids(&pond_state.player_list, owner) + } + + public fun get_total_feed(pond_state: &PondState): u256 { + player::get_total_feed(&pond_state.player_list) + } + + public fun get_max_food_per_feed(): u64 { + MAX_FOOD_PER_FEED + } + + public fun get_food_value_ratio(): u64 { + FOOD_VALUE_RATIO + } + + #[test_only] + public(friend) fun drop_pond(pond: Object) { + let PondState { + id: _, + owner: _, + fishes, + foods, + exit_zones, + quad_tree, + fish_count: _, + food_count: _, + width: _, + height: _, + purchase_amount: _, + next_fish_id: _, + next_food_id: _, + max_fish_count: _, + max_food_count: _, + treasury, + player_list + } = object::remove(pond); + + quad_tree::drop_quad_tree(quad_tree); + + while (!vector::is_empty(&exit_zones)) { + vector::pop_back(&mut exit_zones); + }; + vector::destroy_empty(exit_zones); + + table::drop_unchecked(fishes); + table::drop_unchecked(foods); + + let treasury_obj = object::new_named_object(treasury); + object::to_shared(treasury_obj); + + player::drop_player_list(player_list); + } + + #[test_only] + public(friend) fun move_fish_to_for_test(pond_state: &mut PondState, fish_id: u64, x: u64, y: u64) { + let fish = get_fish(pond_state, fish_id); + let (old_x, old_y) = fish::get_position(fish); + quad_tree::update_object_position( + &mut pond_state.quad_tree, + fish_id, + OBJECT_TYPE_FISH, + old_x, + old_y, + x, + y + ); + let fish = get_fish_mut(pond_state, fish_id); + fish::move_fish_to_for_test(fish, x, y); + } + + #[test_only] + public(friend) fun set_food_position_for_test(pond_state: &mut PondState, food_id: u64, x: u64, y: u64) { + let food = get_food(pond_state, food_id); + let (old_x, old_y) = food::get_position(food); + quad_tree::update_object_position( + &mut pond_state.quad_tree, + food_id, + OBJECT_TYPE_FOOD, + old_x, + old_y, + x, + y + ); + let food = get_food_mut(pond_state, food_id); + food::set_position_for_test(food, x, y); + } + + #[test_only] + public(friend) fun get_last_food_id(pond_state: &PondState): u64 { + pond_state.next_food_id - 1 + } + + #[test_only] + use rooch_framework::genesis; + + #[test_only] + use moveos_std::timestamp; + + #[test] + fun test_create_pond() { + genesis::init_for_test(); + + let id = 1; + let owner = @0x123; + let width = 100; + let height = 100; + let purchase_amount = 500; + let max_fish_count = 50; + let max_food_count = 30; + + let pond_obj = create_pond(id, owner, width, height, purchase_amount, max_fish_count, max_food_count); + let pond_state = object::borrow(&pond_obj); + + assert!(get_pond_id(pond_state) == id, 1); + assert!(get_width(pond_state) == width, 2); + assert!(get_height(pond_state) == height, 3); + assert!(get_purchase_amount(pond_state) == purchase_amount, 4); + assert!(get_max_fish_count(pond_state) == max_fish_count, 5); + assert!(get_max_food_count(pond_state) == max_food_count, 6); + assert!(get_fish_count(pond_state) == 0, 7); + assert!(get_food_count(pond_state) == 0, 8); + assert!(get_player_count(pond_state) == 0, 9); + assert!(get_total_feed(pond_state) == 0, 10); + + drop_pond(pond_obj); + } + + #[test(account = @0x42)] + fun test_fish_burst_mechanism(account: signer) { + genesis::init_for_test(); + + let account_addr = signer::address_of(&account); + gas_coin::faucet_for_test(account_addr, 1000000); + + let owner = @0x123; + let pond_obj = create_pond(1, owner, 100, 100, 500, 50, 30); + let pond_state = object::borrow_mut(&mut pond_obj); + + coin_store::deposit(&mut pond_state.treasury.coin_store, account_coin_store::withdraw(&account, 1000)); + + let initial_owner_balance = gas_coin::balance(owner); + + // Create a fish near max size + let fish_id = purchase_fish(pond_state, &account); + move_fish_to_for_test(pond_state, fish_id, 25, 25); + + // Grow fish to near burst size + let fish = get_fish_mut(pond_state, fish_id); + fish::grow_fish(fish, MAX_FISH_SIZE - 5); + + // Move fish to trigger burst + let (_, _) = move_fish(pond_state, &account, fish_id, 1); + + // Verify burst results + assert!(get_fish_count(pond_state) == 0, 1); + assert!(get_food_count(pond_state) == BURST_FOOD_COUNT, 2); + + // Verify owner received 1% reward + let final_owner_balance = gas_coin::balance(owner); + assert!(final_owner_balance > initial_owner_balance, 3); + + drop_pond(pond_obj); + } + + #[test(account = @0x42, food_owner1 = @0x43, food_owner2 = @0x44)] + fun test_food_contribution_tracking(account: signer, food_owner1: signer, food_owner2: signer) { + genesis::init_for_test(); + + let account_addr = signer::address_of(&account); + let food_owner1_addr = signer::address_of(&food_owner1); + let food_owner2_addr = signer::address_of(&food_owner2); + + // Set up accounts with gas + gas_coin::faucet_for_test(account_addr, 1000000); + gas_coin::faucet_for_test(food_owner1_addr, 1000000); + gas_coin::faucet_for_test(food_owner2_addr, 1000000); + + let pond_obj = create_pond(1, account_addr, 100, 100, 500, 50, 30); + let pond_state = object::borrow_mut(&mut pond_obj); + + coin_store::deposit(&mut pond_state.treasury.coin_store, account_coin_store::withdraw(&account, 10000)); + + // Create fish + let fish_id = purchase_fish(pond_state, &account); + move_fish_to_for_test(pond_state, fish_id, 25, 25); + + // Place food from different owners + feed_food(pond_state, &food_owner1, 1); + feed_food(pond_state, &food_owner2, 1); + + let food_id1 = get_last_food_id(pond_state) - 1; + let food_id2 = get_last_food_id(pond_state); + + // Position foods near fish + set_food_position_for_test(pond_state, food_id1, 26, 25); + set_food_position_for_test(pond_state, food_id2, 27, 25); + + // Move fish to eat both foods + move_fish(pond_state, &account, fish_id, 1); + move_fish(pond_state, &account, fish_id, 1); + + // Verify food contributions + let fish = get_fish(pond_state, fish_id); + assert!(fish::get_contributor_amount(fish, food_owner1_addr) > 0, 1); + assert!(fish::get_contributor_amount(fish, food_owner2_addr) > 0, 2); + assert!(fish::get_total_food_consumed(fish) > 0, 3); + + // Verify food was consumed + assert!(get_food_count(pond_state) == 0, 4); + + drop_pond(pond_obj); + } + + #[test(account = @0x42)] + fun test_reward_distribution(account: signer) { + genesis::init_for_test(); + + let account_addr = signer::address_of(&account); + let owner = @0x123; + gas_coin::faucet_for_test(account_addr, 1000000); + gas_coin::faucet_for_test(owner, 1000000); + + let pond_obj = create_pond(1, owner, 100, 100, 500, 50, 30); + let pond_state = object::borrow_mut(&mut pond_obj); + + // Add funds to treasury for rewards + coin_store::deposit(&mut pond_state.treasury.coin_store, account_coin_store::withdraw(&account, 10000)); + + let initial_owner_balance = gas_coin::balance(owner); + + // Create and grow fish + let fish_id = purchase_fish(pond_state, &account); + move_fish_to_for_test(pond_state, fish_id, 25, 25); + + let fish = get_fish_mut(pond_state, fish_id); + let fish_size = 100; + fish::grow_fish(fish, fish_size); + + // Calculate expected reward + let fish_final_size = fish::get_size(fish); + let total_reward = (fish_final_size as u256) * pond_state.purchase_amount / 100; + let expected_owner_reward = total_reward / 100; // 1% of total reward + + // Trigger burst + let (_, _) = move_fish(pond_state, &account, fish_id, 1); + + let final_owner_balance = gas_coin::balance(owner); + let actual_owner_reward = final_owner_balance - initial_owner_balance; + + // Verify owner got exactly 1% of fish's value + assert!(actual_owner_reward == expected_owner_reward, 1); + + drop_pond(pond_obj); + } + + #[test(account = @0x42)] + #[expected_failure(abort_code = ErrorMaxFishCountReached )] + fun test_max_fish_limit(account: signer) { + genesis::init_for_test(); + + let account_addr = signer::address_of(&account); + gas_coin::faucet_for_test(account_addr, 1000000); + + // Create pond with small max fish limit + let max_fish = 2; + let pond_obj = create_pond(1, account_addr, 100, 100, 500, max_fish, 30); + let pond_state = object::borrow_mut(&mut pond_obj); + + coin_store::deposit(&mut pond_state.treasury.coin_store, account_coin_store::withdraw(&account, 10000)); + + // Purchase fish until we hit the limit + purchase_fish(pond_state, &account); + assert!(get_fish_count(pond_state) == 1, 1); + + purchase_fish(pond_state, &account); + assert!(get_fish_count(pond_state) == 2, 2); + + // This should fail as we've reached the max fish limit + purchase_fish(pond_state, &account); + + drop_pond(pond_obj); + } + + #[test(account = @0x1)] + fun test_purchase_fish(account: signer) { + genesis::init_for_test(); + + let account_addr = signer::address_of(&account); + gas_coin::faucet_for_test(account_addr, 1000000); + + let pond_obj = create_pond(1, account_addr, 100, 100, 500, 50, 30); + let pond_state = object::borrow_mut(&mut pond_obj); + + coin_store::deposit(&mut pond_state.treasury.coin_store, account_coin_store::withdraw(&account, 1000)); + + let fish_id = purchase_fish(pond_state, &account); + assert!(get_fish_count(pond_state) == 1, 1); + assert!(fish::get_owner(get_fish(pond_state, fish_id)) == account_addr, 2); + + drop_pond(pond_obj); + } + + #[test(account = @0x1)] + fun test_move_fish(account: signer) { + genesis::init_for_test(); + + let account_addr = signer::address_of(&account); + gas_coin::faucet_for_test(account_addr, 1000000); + + let pond_obj = create_pond(1, account_addr, 100, 100, 500, 50, 30); + let pond_state = object::borrow_mut(&mut pond_obj); + + coin_store::deposit(&mut pond_state.treasury.coin_store, account_coin_store::withdraw(&account, 1000)); + + let fish_id = purchase_fish(pond_state, &account); + move_fish_to_for_test(pond_state, fish_id, 25, 25); + + let (new_x, new_y) = move_fish(pond_state, &account, fish_id, 1); + + let fish = get_fish(pond_state, fish_id); + let (fish_x, fish_y) = fish::get_position(fish); + assert!(fish_x == new_x && fish_y == new_y, 1); + + drop_pond(pond_obj); + } + + #[test(account = @0x1)] + fun test_feed_food(account: signer) { + genesis::init_for_test(); + + let account_addr = signer::address_of(&account); + gas_coin::faucet_for_test(account_addr, 1000000); + + let pond_obj = create_pond(1, account_addr, 100, 100, 500, 50, 300); + let pond_state = object::borrow_mut(&mut pond_obj); + + let food_value = 500 / FOOD_VALUE_RATIO; + let feed_count = MAX_FOOD_PER_FEED; + + let total_cost = feed_food(pond_state, &account, feed_count); + + assert!(get_food_count(pond_state) == feed_count, 1); + assert!(get_total_feed(pond_state) == (feed_count as u256) * (food_value as u256), 2); + + let large_feed_count = MAX_FOOD_PER_FEED * 2; + let second_total_cost = feed_food(pond_state, &account, large_feed_count); + + assert!(get_food_count(pond_state) == MAX_FOOD_PER_FEED * 2, 3); + assert!(get_total_feed(pond_state) == total_cost + second_total_cost, 4); + + drop_pond(pond_obj); + } + + #[test(account = @0x1)] + fun test_destroy_fish(account: signer) { + genesis::init_for_test(); + + let account_addr = signer::address_of(&account); + gas_coin::faucet_for_test(account_addr, 1000000); + + let pond_obj = create_pond(1, account_addr, 100, 100, 500, 50, 30); + let pond_state = object::borrow_mut(&mut pond_obj); + + coin_store::deposit(&mut pond_state.treasury.coin_store, account_coin_store::withdraw(&account, 10000)); + + let fish_id = purchase_fish(pond_state, &account); + move_fish_to_for_test(pond_state, fish_id, 25, 25); + + add_exit_zone(pond_state, 0, 0, 100); + + let reward = destroy_fish(pond_state, &account, fish_id); + assert!(reward > 0, 1); + assert!(get_fish_count(pond_state) == 0, 2); + + drop_pond(pond_obj); + } + + #[test] + fun test_exit_zones() { + genesis::init_for_test(); + + let owner = @0x123; + let pond_obj = create_pond(1, owner, 100, 100, 500, 50, 30); + let pond_state = object::borrow_mut(&mut pond_obj); + + add_exit_zone(pond_state, 10, 10, 5); + add_exit_zone(pond_state, 90, 90, 8); + + let fish1 = fish::create_fish(@0x1, 1, 10, 12, 12); + let fish2 = fish::create_fish(@0x2, 2, 15, 50, 50); + + assert!(is_fish_in_exit_zone(pond_state, &fish1), 1); + assert!(!is_fish_in_exit_zone(pond_state, &fish2), 2); + + remove_exit_zone(pond_state, 0); + assert!(!is_fish_in_exit_zone(pond_state, &fish1), 3); + + fish::drop_fish(fish1); + fish::drop_fish(fish2); + drop_pond(pond_obj); + } + + #[test(account = @0x42, other_account = @0x43)] + fun test_get_player_fish_ids(account: signer, other_account: signer) { + genesis::init_for_test(); + + let account_addr = signer::address_of(&account); + gas_coin::faucet_for_test(account_addr, 1000000); + + let pond_obj = create_pond(1, account_addr, 100, 100, 500, 50, 30); + let pond_state = object::borrow_mut(&mut pond_obj); + + coin_store::deposit(&mut pond_state.treasury.coin_store, account_coin_store::withdraw(&account, 10000)); + + let fish_id1 = purchase_fish(pond_state, &account); + let fish_id2 = purchase_fish(pond_state, &account); + let fish_id3 = purchase_fish(pond_state, &account); + + let fish_ids = get_player_fish_ids(pond_state, account_addr); + + assert!(vector::length(&fish_ids) == 3, 1); + assert!(vector::contains(&fish_ids, &fish_id1), 2); + assert!(vector::contains(&fish_ids, &fish_id2), 3); + assert!(vector::contains(&fish_ids, &fish_id3), 4); + + let other_account_addr = signer::address_of(&other_account); + gas_coin::faucet_for_test(other_account_addr, 1000000); + let other_fish_id = purchase_fish(pond_state, &other_account); + + let fish_ids = get_player_fish_ids(pond_state, account_addr); + + assert!(vector::length(&fish_ids) == 3, 5); + assert!(!vector::contains(&fish_ids, &other_fish_id), 6); + + drop_pond(pond_obj); + } + + #[test(account = @0x1)] + fun test_fish_eat_food_and_move(account: signer) { + genesis::init_for_test(); + + let account_addr = signer::address_of(&account); + gas_coin::faucet_for_test(account_addr, 1000000); + + let pond_obj = create_pond(1, account_addr, 100, 100, 500, 50, 30); + let pond_state = object::borrow_mut(&mut pond_obj); + + coin_store::deposit(&mut pond_state.treasury.coin_store, account_coin_store::withdraw(&account, 1000)); + + let fish_id = purchase_fish(pond_state, &account); + move_fish_to_for_test(pond_state, fish_id, 25, 25); + + let initial_fish_size = fish::get_size(get_fish(pond_state, fish_id)); + + feed_food(pond_state, &account, 1); + let food_id = get_last_food_id(pond_state); + set_food_position_for_test(pond_state, food_id, 25, 26); + + let initial_food_count = get_food_count(pond_state); + + move_fish(pond_state, &account, fish_id, 0); + + let fish = get_fish(pond_state, fish_id); + let final_fish_size = fish::get_size(fish); + let (fish_x, fish_y) = fish::get_position(fish); + let final_food_count = get_food_count(pond_state); + + assert!(final_fish_size > initial_fish_size, 1); + assert!(final_food_count < initial_food_count, 2); + assert!(fish_x == 25 && fish_y == 26, 3); + + move_fish(pond_state, &account, fish_id, 1); + let fish = get_fish(pond_state, fish_id); + let (fish_x, fish_y) = fish::get_position(fish); + + assert!(fish_x == 26 && fish_y == 26, 4); + + move_fish(pond_state, &account, fish_id, 2); + move_fish(pond_state, &account, fish_id, 3); + + let fish = get_fish(pond_state, fish_id); + let (fish_x, fish_y) = fish::get_position(fish); + + assert!(fish_x == 25 && fish_y == 25, 5); + + drop_pond(pond_obj); + } + + #[test(account = @0x1)] + #[expected_failure(abort_code = ErrorMaxFishCountReached )] + fun test_max_fish_count(account: signer) { + genesis::init_for_test(); + + let account_addr = signer::address_of(&account); + gas_coin::faucet_for_test(account_addr, 1000000); + + let pond_obj = create_pond(1, account_addr, 100, 100, 500, 2, 30); + let pond_state = object::borrow_mut(&mut pond_obj); + + coin_store::deposit(&mut pond_state.treasury.coin_store, account_coin_store::withdraw(&account, 10000)); + + purchase_fish(pond_state, &account); + purchase_fish(pond_state, &account); + purchase_fish(pond_state, &account); + + drop_pond(pond_obj); + } + + #[test(account = @0x1)] + #[expected_failure(abort_code = ErrorMaxFoodCountReached)] + fun test_max_food_count(account: signer) { + genesis::init_for_test(); + + let account_addr = signer::address_of(&account); + gas_coin::faucet_for_test(account_addr, 1000000); + + let pond_obj = create_pond(1, account_addr, 100, 100, 500, 50, 5); + let pond_state = object::borrow_mut(&mut pond_obj); + + feed_food(pond_state, &account, 10); + + drop_pond(pond_obj); + } + + #[test(account = @0x42, food_owner = @0x43)] + fun test_destroy_fish_reward_distribution(account: signer, food_owner: signer) { + genesis::init_for_test(); + + let account_addr = signer::address_of(&account); + let food_owner_addr = signer::address_of(&food_owner); + let owner = @0x123; + + // Set initial balances + gas_coin::faucet_for_test(account_addr, 1000000); + gas_coin::faucet_for_test(food_owner_addr, 1000000); + gas_coin::faucet_for_test(owner, 1000000); + + let pond_obj = create_pond(1, owner, 100, 100, 500, 50, 30); + let pond_state = object::borrow_mut(&mut pond_obj); + + // Add substantial funds to treasury for rewards + coin_store::deposit(&mut pond_state.treasury.coin_store, account_coin_store::withdraw(&account, 100000)); + + // Create and grow fish with significant size + let fish_id = purchase_fish(pond_state, &account); + move_fish_to_for_test(pond_state, fish_id, 25, 25); + + // Grow fish to increase reward value + let fish = get_fish_mut(pond_state, fish_id); + fish::grow_fish(fish, 50); + + // Add food contribution + feed_food(pond_state, &food_owner, 1); + let food_id = get_last_food_id(pond_state); + set_food_position_for_test(pond_state, food_id, 25, 26); + + // Move fish to eat food + move_fish(pond_state, &account, fish_id, 0); + + // Record balances before destroy + let initial_owner_balance = gas_coin::balance(owner); + let initial_food_owner_balance = gas_coin::balance(food_owner_addr); + let initial_fish_owner_balance = gas_coin::balance(account_addr); + + // Add exit zone and destroy fish + add_exit_zone(pond_state, 0, 0, 100); + let total_reward = destroy_fish(pond_state, &account, fish_id); + + // Calculate expected rewards + let dev_reward = total_reward / 100; // 1% + let contributor_reward = (total_reward * 20) / 100; // 20% + let owner_reward = total_reward - dev_reward - contributor_reward; // 79% + + // Verify final balances + let final_owner_balance = gas_coin::balance(owner); + let final_food_owner_balance = gas_coin::balance(food_owner_addr); + let final_fish_owner_balance = gas_coin::balance(account_addr); + + // Verify reward distributions + assert!((final_owner_balance > initial_owner_balance), 1); + assert!((final_owner_balance - initial_owner_balance) == dev_reward, 2); + + assert!((final_food_owner_balance > initial_food_owner_balance), 3); + assert!((final_food_owner_balance - initial_food_owner_balance) == contributor_reward, 4); + + assert!((final_fish_owner_balance > initial_fish_owner_balance), 5); + assert!((final_fish_owner_balance - initial_fish_owner_balance) == owner_reward, 6); + + // Verify total reward distribution + assert!(dev_reward + contributor_reward + owner_reward == total_reward, 7); + + drop_pond(pond_obj); + } + + #[test(account = @0x42)] + fun test_destroy_fish_no_contributors(account: signer) { + genesis::init_for_test(); + + let account_addr = signer::address_of(&account); + let owner = @0x123; + + // Set initial balances with large amounts + gas_coin::faucet_for_test(account_addr, 10000000); + gas_coin::faucet_for_test(owner, 10000000); + + let pond_obj = create_pond(1, owner, 100, 100, 500, 50, 30); + let pond_state = object::borrow_mut(&mut pond_obj); + + // Add substantial funds to treasury + coin_store::deposit(&mut pond_state.treasury.coin_store, account_coin_store::withdraw(&account, 1000000)); + + // Create and grow fish + let fish_id = purchase_fish(pond_state, &account); + move_fish_to_for_test(pond_state, fish_id, 25, 25); + + // Grow fish to increase reward value + let fish = get_fish_mut(pond_state, fish_id); + fish::grow_fish(fish, 50); + + // Add exit zone and record balances just before destroying fish + add_exit_zone(pond_state, 0, 0, 100); + let initial_owner_balance = gas_coin::balance(owner); + let initial_fish_owner_balance = gas_coin::balance(account_addr); + + // Destroy fish and get total reward + let total_reward = destroy_fish(pond_state, &account, fish_id); + + // Calculate expected rewards + let dev_reward = total_reward / 100; // 1% + let owner_reward = total_reward - dev_reward; // 99% (since no contributors) + + // Get final balances + let final_owner_balance = gas_coin::balance(owner); + let final_fish_owner_balance = gas_coin::balance(account_addr); + + // Calculate actual rewards received + let actual_dev_reward = final_owner_balance - initial_owner_balance; + let actual_owner_reward = final_fish_owner_balance - initial_fish_owner_balance; + + // Verify rewards - developer should get exactly 1% + assert!(actual_dev_reward == dev_reward, 1); + + // Verify that fish owner got remainder (allowing for minimal precision loss) + assert!(actual_owner_reward > 0, 2); + assert!(actual_owner_reward <= owner_reward, 3); // Should not exceed expected + + // The difference between expected and actual should be very small + let reward_difference = if (owner_reward > actual_owner_reward) { + owner_reward - actual_owner_reward + } else { + actual_owner_reward - owner_reward + }; + assert!(reward_difference < 100, 4); // Allow for small rounding differences + + // Total distributed rewards should match total_reward (within small margin) + let total_distributed = actual_dev_reward + actual_owner_reward; + let distribution_difference = if (total_reward > total_distributed) { + total_reward - total_distributed + } else { + total_distributed - total_reward + }; + assert!(distribution_difference < 100, 5); // Allow for small rounding differences + + drop_pond(pond_obj); + } + + #[test(account = @0x42)] + fun test_fish_protection(account: signer) { + genesis::init_for_test(); + + let account_addr = signer::address_of(&account); + gas_coin::faucet_for_test(account_addr, 1000000); + + let pond_obj = create_pond(1, account_addr, 100, 100, 500, 50, 30); + let pond_state = object::borrow_mut(&mut pond_obj); + + coin_store::deposit(&mut pond_state.treasury.coin_store, account_coin_store::withdraw(&account, 10000)); + + let predator_id = purchase_fish(pond_state, &account); + let prey_id = purchase_fish(pond_state, &account); + + let predator = get_fish_mut(pond_state, predator_id); + fish::grow_fish(predator, 50); + + move_fish_to_for_test(pond_state, predator_id, 25, 25); + move_fish_to_for_test(pond_state, prey_id, 26, 25); + + move_fish(pond_state, &account, predator_id, 1); + + assert!(table::contains(&pond_state.fishes, prey_id), 1); + + timestamp::fast_forward_seconds_for_test(61); + + move_fish(pond_state, &account, predator_id, 1); + + assert!(!table::contains(&pond_state.fishes, prey_id), 2); + + drop_pond(pond_obj); + } +} diff --git a/examples/rooch_fish/sources/quad_tree.move b/examples/rooch_fish/sources/quad_tree.move new file mode 100644 index 0000000000..dc6490c3a6 --- /dev/null +++ b/examples/rooch_fish/sources/quad_tree.move @@ -0,0 +1,410 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +module rooch_fish::quad_tree { + use std::vector; + use moveos_std::table::{Self, Table}; + + const MAX_DEPTH: u8 = 5; + const MAX_OBJECTS: u64 = 2; + + struct Point has copy, drop, store { + x: u64, + y: u64, + } + + struct Rectangle has copy, drop, store { + x: u64, + y: u64, + width: u64, + height: u64, + } + + struct ObjectEntry has copy, drop, store { + id: T, + object_type: u8, + x: u64, + y: u64, + } + + struct QuadTreeNode has copy, drop, store { + boundary: Rectangle, + objects: vector>, + is_divided: bool, + nw: u64, + ne: u64, + sw: u64, + se: u64, + } + + struct QuadTree has key, store { + nodes: Table>, + next_node_id: u64, + root: u64, + width: u64, + height: u64, + } + + public fun create_quad_tree(width: u64, height: u64): QuadTree { + let root_node = QuadTreeNode { + boundary: Rectangle { x: 0, y: 0, width, height }, + objects: vector::empty(), + is_divided: false, + nw: 0, ne: 0, sw: 0, se: 0, + }; + let nodes = table::new(); + table::add(&mut nodes, 0, root_node); + QuadTree { + nodes, + next_node_id: 1, + root: 0, + width, + height, + } + } + + public fun insert_object(tree: &mut QuadTree, id: T, object_type: u8, x: u64, y: u64) { + insert_object_recursive(tree, ObjectEntry { id, object_type, x, y }, 0, 0); + } + + fun insert_object_recursive( + tree: &mut QuadTree, + object: ObjectEntry, + node_id: u64, + depth: u8 + ) { + if (depth >= MAX_DEPTH) { + return + }; + + let node = table::borrow_mut(&mut tree.nodes, node_id); + if (!node.is_divided) { + if (vector::length(&node.objects) < MAX_OBJECTS) { + vector::push_back(&mut node.objects, object); + return + }; + subdivide(tree, node_id); + }; + + let node = table::borrow(&tree.nodes, node_id); + if (object.x < node.boundary.x + node.boundary.width / 2) { + if (object.y < node.boundary.y + node.boundary.height / 2) { + insert_object_recursive(tree, object, node.nw, depth + 1); + } else { + insert_object_recursive(tree, object, node.sw, depth + 1); + }; + } else { + if (object.y < node.boundary.y + node.boundary.height / 2) { + insert_object_recursive(tree, object, node.ne, depth + 1); + } else { + insert_object_recursive(tree, object, node.se, depth + 1); + }; + }; + } + + fun subdivide(tree: &mut QuadTree, node_id: u64) { + let node = table::borrow_mut(&mut tree.nodes, node_id); + if (node.is_divided) { + return + }; + + let x = node.boundary.x; + let y = node.boundary.y; + let w = node.boundary.width / 2; + let h = node.boundary.height / 2; + + let nw = tree.next_node_id; + tree.next_node_id = tree.next_node_id + 1; + let ne = tree.next_node_id; + tree.next_node_id = tree.next_node_id + 1; + let sw = tree.next_node_id; + tree.next_node_id = tree.next_node_id + 1; + let se = tree.next_node_id; + tree.next_node_id = tree.next_node_id + 1; + + table::add(&mut tree.nodes, nw, QuadTreeNode { + boundary: Rectangle { x, y, width: w, height: h }, + objects: vector::empty(), + is_divided: false, + nw: 0, ne: 0, sw: 0, se: 0, + }); + table::add(&mut tree.nodes, ne, QuadTreeNode { + boundary: Rectangle { x: x + w, y, width: w, height: h }, + objects: vector::empty(), + is_divided: false, + nw: 0, ne: 0, sw: 0, se: 0, + }); + table::add(&mut tree.nodes, sw, QuadTreeNode { + boundary: Rectangle { x, y: y + h, width: w, height: h }, + objects: vector::empty(), + is_divided: false, + nw: 0, ne: 0, sw: 0, se: 0, + }); + table::add(&mut tree.nodes, se, QuadTreeNode { + boundary: Rectangle { x: x + w, y: y + h, width: w, height: h }, + objects: vector::empty(), + is_divided: false, + nw: 0, ne: 0, sw: 0, se: 0, + }); + + let node = table::borrow_mut(&mut tree.nodes, node_id); + node.nw = nw; + node.ne = ne; + node.sw = sw; + node.se = se; + node.is_divided = true; + } + + public fun query_range( + tree: &QuadTree, + x: u64, + y: u64, + width: u64, + height: u64 + ): vector> { + let range = Rectangle { x, y, width, height }; + let result = vector::empty(); + query_range_recursive(tree, tree.root, &range, &mut result); + result + } + + fun query_range_recursive( + tree: &QuadTree, + node_id: u64, + range: &Rectangle, + result: &mut vector> + ) { + let node = table::borrow(&tree.nodes, node_id); + if (!intersects(&node.boundary, range)) { + return + }; + + let i = 0; + while (i < vector::length(&node.objects)) { + let object_entry = vector::borrow(&node.objects, i); + if (is_point_in_rectangle(object_entry.x, object_entry.y, range)) { + vector::push_back(result, *object_entry); + }; + i = i + 1; + }; + + if (node.is_divided) { + query_range_recursive(tree, node.nw, range, result); + query_range_recursive(tree, node.ne, range, result); + query_range_recursive(tree, node.sw, range, result); + query_range_recursive(tree, node.se, range, result); + }; + } + + fun intersects(r1: &Rectangle, r2: &Rectangle): bool { + !(r2.x > r1.x + r1.width || + r2.x + r2.width < r1.x || + r2.y > r1.y + r1.height || + r2.y + r2.height < r1.y) + } + + fun is_point_in_rectangle(x: u64, y: u64, rect: &Rectangle): bool { + x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height + } + + public fun remove_object(tree: &mut QuadTree, id: T, object_type: u8, x: u64, y: u64) { + remove_object_recursive(tree, id, object_type, x, y, 0); + } + + fun remove_object_recursive( + tree: &mut QuadTree, + id: T, + object_type: u8, + x: u64, + y: u64, + node_id: u64 + ) { + let node = table::borrow(&tree.nodes, node_id); + // First check if the point is within current node's boundary + if (!is_point_in_rectangle(x, y, &node.boundary)) { + return + }; + + // Check if we need to process current node + if (!node.is_divided) { + let node = table::borrow_mut(&mut tree.nodes, node_id); + let i = 0; + while (i < vector::length(&node.objects)) { + let object_entry = vector::borrow(&node.objects, i); + if (object_entry.id == id && object_entry.object_type == object_type && object_entry.x == x && object_entry.y == y) { + vector::remove(&mut node.objects, i); + return + }; + i = i + 1; + }; + return + }; + + // If node is divided, get quadrant information first + let mid_x = node.boundary.x + node.boundary.width / 2; + let mid_y = node.boundary.y + node.boundary.height / 2; + let nw = node.nw; + let ne = node.ne; + let sw = node.sw; + let se = node.se; + + // Search appropriate quadrant + if (x < mid_x) { + if (y < mid_y) { + remove_object_recursive(tree, id, object_type, x, y, nw); + } else { + remove_object_recursive(tree, id, object_type, x, y, sw); + }; + } else { + if (y < mid_y) { + remove_object_recursive(tree, id, object_type, x, y, ne); + } else { + remove_object_recursive(tree, id, object_type, x, y, se); + }; + }; + } + + + public fun update_object_position( + tree: &mut QuadTree, + id: T, + object_type: u8, + old_x: u64, + old_y: u64, + new_x: u64, + new_y: u64 + ) { + remove_object(tree, id, object_type, old_x, old_y); + insert_object(tree, id, object_type, new_x, new_y); + } + + public fun drop_quad_tree(tree: QuadTree) { + let QuadTree { nodes, next_node_id: _, root:_, width: _, height: _ } = tree; + table::drop(nodes); + } + + public fun get_object_entry_id(entry: &ObjectEntry): T { + entry.id + } + + public fun get_object_entry_type(entry: &ObjectEntry): u8 { + entry.object_type + } + + public fun get_object_entry_x(entry: &ObjectEntry): u64 { + entry.x + } + + public fun get_object_entry_y(entry: &ObjectEntry): u64 { + entry.y + } + + #[test] + fun test_create_quad_tree() { + let tree = create_quad_tree(100, 100); + drop_quad_tree(tree); + } + + #[test] + fun test_insert_and_query() { + let tree = create_quad_tree(100, 100); + + insert_object(&mut tree, 1, 1, 10, 10); + insert_object(&mut tree, 2, 1, 20, 20); + insert_object(&mut tree, 3, 1, 30, 30); + + let result = query_range(&tree, 0, 0, 50, 50); + assert!(vector::length(&result) == 3, 0); + + let result = query_range(&tree, 0, 0, 15, 15); + assert!(vector::length(&result) == 1, 1); + + let result = query_range(&tree, 5, 5, 10, 10); + assert!(vector::length(&result) == 1, 2); + + let result = query_range(&tree, 15, 15, 10, 10); + assert!(vector::length(&result) == 1, 3); + + let result = query_range(&tree, 25, 25, 10, 10); + assert!(vector::length(&result) == 1, 4); + + let result = query_range(&tree, 40, 40, 10, 10); + assert!(vector::length(&result) == 0, 5); + + drop_quad_tree(tree); + } + + #[test] + fun test_remove_object() { + let tree = create_quad_tree(100, 100); + + insert_object(&mut tree, 1, 1, 10, 10); + insert_object(&mut tree, 2, 1, 20, 20); + + let result = query_range(&tree, 0, 0, 50, 50); + assert!(vector::length(&result) == 2, 0); + + remove_object(&mut tree, 1, 1, 10, 10); + + let result = query_range(&tree, 0, 0, 50, 50); + assert!(vector::length(&result) == 1, 1); + + drop_quad_tree(tree); + } + + #[test] + fun test_update_object_position() { + let tree = create_quad_tree(100, 100); + + insert_object(&mut tree, 1, 1, 10, 10); + + let result = query_range(&tree, 0, 0, 15, 15); + assert!(vector::length(&result) == 1, 0); + + update_object_position(&mut tree, 1, 1, 10, 10, 50, 50); + + let result = query_range(&tree, 0, 0, 15, 15); + assert!(vector::length(&result) == 0, 1); + + let result = query_range(&tree, 45, 45, 10, 10); + assert!(vector::length(&result) == 1, 2); + + drop_quad_tree(tree); + } + + #[test] + fun test_get_object_entry_functions() { + let tree = create_quad_tree(100, 100); + + insert_object(&mut tree, 1, 2, 10, 10); + + let result = query_range(&tree, 0, 0, 15, 15); + assert!(vector::length(&result) == 1, 0); + + let entry = vector::borrow(&result, 0); + assert!(get_object_entry_id(entry) == 1, 1); + assert!(get_object_entry_type(entry) == 2, 2); + assert!(get_object_entry_x(entry) == 10, 3); + assert!(get_object_entry_y(entry) == 10, 4); + + drop_quad_tree(tree); + } + + #[test] + fun test_remove_object_at_boundaries() { + let tree = create_quad_tree(100, 100); + + // Insert objects at quadrant boundaries + insert_object(&mut tree, 1, 1, 50, 50); + + let result = query_range(&tree, 45, 45, 10, 10); + assert!(vector::length(&result) == 1, 0); + + remove_object(&mut tree, 1, 1, 50, 50); + + let result = query_range(&tree, 45, 45, 10, 10); + assert!(vector::length(&result) == 0, 1); + + drop_quad_tree(tree); + } +} diff --git a/examples/rooch_fish/sources/rooch_fish.move b/examples/rooch_fish/sources/rooch_fish.move new file mode 100644 index 0000000000..4a0b97a63d --- /dev/null +++ b/examples/rooch_fish/sources/rooch_fish.move @@ -0,0 +1,213 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +module rooch_fish::rooch_fish { + use std::vector; + use std::u256; + use moveos_std::object::{Self, Object}; + use moveos_std::signer; + use moveos_std::table::{Self, Table}; + use rooch_framework::gas_coin; + use rooch_fish::pond::{Self, PondState}; + use rooch_fish::player::{Self, PlayerList}; + + const ErrorInvalidPondID: u64 = 1; + const ErrorAlreadyInitialized: u64 = 2; + + struct PondConfig has copy, drop { + id: u64, + owner: address, + width: u64, + height: u64, + purchase_amount: u256, + max_fish_count: u64, + max_food_count: u64, + } + + struct GameState has key { + admin: address, + ponds: Table>, + player_list: PlayerList, + } + + public entry fun init_world(account: &signer) { + let admin = signer::address_of(account); + + let ponds = table::new(); + let unit = u256::pow(10, gas_coin::decimals() - 3); + + let pond_configs = vector[ + PondConfig { id: 0, owner: admin, width: 100, height: 100, purchase_amount: unit, max_fish_count: 100, max_food_count: 1000 }, + PondConfig { id: 1, owner: admin, width: 1000, height: 1000, purchase_amount: unit, max_fish_count: 1000, max_food_count: 10000 }, + PondConfig { id: 2, owner: admin, width: 10000, height: 10000, purchase_amount: unit, max_fish_count: 10000, max_food_count: 100000 }, + PondConfig { id: 3, owner: admin, width: 100000, height: 100000, purchase_amount: unit, max_fish_count: 100000, max_food_count: 1000000 }, + + PondConfig { id: 4, owner: admin, width: 1000, height: 1000, purchase_amount: unit * 10, max_fish_count: 1000, max_food_count: 10000 }, + PondConfig { id: 5, owner: admin, width: 1000, height: 1000, purchase_amount: unit * 100, max_fish_count: 1000, max_food_count: 10000 }, + PondConfig { id: 6, owner: admin, width: 1000, height: 1000, purchase_amount: unit * 1000, max_fish_count: 1000, max_food_count: 10000 }, + PondConfig { id: 7, owner: admin, width: 1000, height: 1000, purchase_amount: unit * 10000, max_fish_count: 1000, max_food_count: 10000 }, + ]; + + let i = 0; + while (i < vector::length(&pond_configs)) { + let config = vector::borrow(&pond_configs, i); + let pond_obj = pond::create_pond( + config.id, + config.owner, + config.width, + config.height, + (config.purchase_amount as u256), + config.max_fish_count, + config.max_food_count, + ); + + let pond_state = object::borrow_mut(&mut pond_obj); + pond::add_exit_zone(pond_state, config.width/2, config.height/2, 10); + + table::add(&mut ponds, config.id, pond_obj); + i = i + 1; + }; + + let player_list = player::create_player_list(); + + let game_state = GameState { + admin, + ponds, + player_list, + }; + + let game_state_obj = object::new_named_object(game_state); + object::to_shared(game_state_obj); + } + + public entry fun purchase_fish(account: &signer, game_state_obj: &mut Object, pond_id: u64) { + let game_state = object::borrow_mut(game_state_obj); + let account_addr = signer::address_of(account); + + let pond_obj = table::borrow_mut(&mut game_state.ponds, pond_id); + let pond_state = object::borrow_mut(pond_obj); + + let fish_id = pond::purchase_fish(pond_state, account); + player::add_fish(&mut game_state.player_list, account_addr, fish_id); + } + + public entry fun move_fish(account: &signer, game_state_obj: &mut Object, pond_id: u64, fish_id: u64, direction: u8) { + let game_state = object::borrow_mut(game_state_obj); + + let pond_obj = table::borrow_mut(&mut game_state.ponds, pond_id); + let pond_state = object::borrow_mut(pond_obj); + + pond::move_fish(pond_state, account, fish_id, direction); + } + + public entry fun feed_food(account: &signer, game_state_obj: &mut Object, pond_id: u64, count: u64) { + let game_state = object::borrow_mut(game_state_obj); + let account_addr = signer::address_of(account); + + let pond_obj = table::borrow_mut(&mut game_state.ponds, pond_id); + let pond_state = object::borrow_mut(pond_obj); + + let actual_cost = pond::feed_food(pond_state, account, count); + player::add_feed(&mut game_state.player_list, account_addr, actual_cost); + } + + public entry fun destroy_fish(account: &signer, game_state_obj: &mut Object, pond_id: u64, fish_id: u64) { + let game_state = object::borrow_mut(game_state_obj); + let account_addr = signer::address_of(account); + + let pond_obj = table::borrow_mut(&mut game_state.ponds, pond_id); + let pond_state = object::borrow_mut(pond_obj); + + let reward = pond::destroy_fish(pond_state, account, fish_id); + + let reward_amount = u256::divide_and_round_up(reward, u256::pow(10, gas_coin::decimals())); + player::add_reward(&mut game_state.player_list, account_addr, reward_amount); + } + + public fun get_pond_player_list(game_state_obj: &Object, pond_id: u64): &PlayerList { + let game_state = object::borrow(game_state_obj); + let pond_obj = table::borrow(&game_state.ponds, pond_id); + let pond_state = object::borrow(pond_obj); + pond::get_player_list(pond_state) + } + + public fun get_pond_player_count(game_state_obj: &Object, pond_id: u64): u64 { + let game_state = object::borrow(game_state_obj); + let pond_obj = table::borrow(&game_state.ponds, pond_id); + let pond_state = object::borrow(pond_obj); + pond::get_player_count(pond_state) + } + + public fun get_pond_total_feed(game_state_obj: &Object, pond_id: u64): u256 { + let game_state = object::borrow(game_state_obj); + let pond_obj = table::borrow(&game_state.ponds, pond_id); + let pond_state = object::borrow(pond_obj); + pond::get_total_feed(pond_state) + } + + public fun get_pond_player_fish_ids(game_state_obj: &Object, pond_id: u64, owner: address): vector { + let game_state = object::borrow(game_state_obj); + let pond_obj = table::borrow(&game_state.ponds, pond_id); + let pond_state = object::borrow(pond_obj); + pond::get_player_fish_ids(pond_state, owner) + } + + public fun get_global_player_list(game_state_obj: &Object): &PlayerList { + let game_state = object::borrow(game_state_obj); + &game_state.player_list + } + + public fun get_global_player_count(game_state_obj: &Object): u64 { + let game_state = object::borrow(game_state_obj); + player::get_player_count(&game_state.player_list) + } + + public fun get_global_total_feed(game_state_obj: &Object): u256 { + let game_state = object::borrow(game_state_obj); + player::get_total_feed(&game_state.player_list) + } + + public fun get_pond_count(game_state_obj: &Object): u64 { + let game_state = object::borrow(game_state_obj); + table::length(&game_state.ponds) + } + + public fun get_pond_info(game_state_obj: &Object, pond_id: u64): (u64, u64, u64, u256, u64, u64) { + let game_state = object::borrow(game_state_obj); + assert!(table::contains(&game_state.ponds, pond_id), ErrorInvalidPondID); + let pond_obj = table::borrow(&game_state.ponds, pond_id); + let pond_state = object::borrow(pond_obj); + ( + pond::get_width(pond_state), + pond::get_height(pond_state), + pond::get_max_fish_count(pond_state), + pond::get_purchase_amount(pond_state), + pond::get_max_food_per_feed(), + pond::get_food_value_ratio(), + ) + } + + #[test_only] + public fun set_fish_position_for_test(game_state_obj: &mut Object, pond_id: u64, fish_id: u64, x: u64, y: u64) { + let game_state = object::borrow_mut(game_state_obj); + let pond_obj = table::borrow_mut(&mut game_state.ponds, pond_id); + let pond_state = object::borrow_mut(pond_obj); + pond::move_fish_to_for_test(pond_state, fish_id, x, y); + } + + #[test_only] + public fun set_food_position_for_test(game_state_obj: &mut Object, pond_id: u64, food_id: u64, x: u64, y: u64) { + let game_state = object::borrow_mut(game_state_obj); + let pond_obj = table::borrow_mut(&mut game_state.ponds, pond_id); + let pond_state = object::borrow_mut(pond_obj); + pond::set_food_position_for_test(pond_state, food_id, x, y); + } + + #[test_only] + public fun get_last_food_id(game_state_obj: &Object, pond_id: u64): u64 { + let game_state = object::borrow(game_state_obj); + let pond_obj = table::borrow(&game_state.ponds, pond_id); + let pond_state = object::borrow(pond_obj); + pond::get_last_food_id(pond_state) + } +} diff --git a/examples/rooch_fish/sources/simple_rng.move b/examples/rooch_fish/sources/simple_rng.move new file mode 100644 index 0000000000..681a665922 --- /dev/null +++ b/examples/rooch_fish/sources/simple_rng.move @@ -0,0 +1,213 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +/// A simple random number generator in Move language. +module rooch_fish::simple_rng { + use moveos_std::tx_context; + use moveos_std::timestamp; + use moveos_std::bcs; + use std::vector; + use std::hash; + use std::option; + use rooch_framework::transaction::{Self, TransactionSequenceInfo}; + + const ErrorInvalidArg: u64 = 0; + const ErrorInvalidU64: u64 = 1; + const ErrorInvalidU128: u64 = 2; + const ErrorInvalidSeed: u64 = 3; + + fun seed(nonce: u64): vector { + let nonce_bytes = bcs::to_bytes(&nonce); + + let sequence_number = tx_context::sequence_number(); + let sequence_number_bytes = bcs::to_bytes(&sequence_number); + + let sender_addr = tx_context::sender(); + let sender_addr_bytes = bcs::to_bytes(&sender_addr); + + let timestamp_ms = timestamp::now_milliseconds(); + let timestamp_ms_bytes = bcs::to_bytes(×tamp_ms); + + let seed_bytes = vector::empty(); + + let tx_sequence_info_opt = tx_context::get_attribute(); + if (option::is_some(&tx_sequence_info_opt)) { + let tx_sequence_info = option::extract(&mut tx_sequence_info_opt); + let tx_accumulator_root = transaction::tx_accumulator_root(&tx_sequence_info); + let tx_accumulator_root_bytes = bcs::to_bytes(&tx_accumulator_root); + vector::append(&mut seed_bytes, tx_accumulator_root_bytes); + } else { + let tx_hash = tx_context::tx_hash(); + let tx_hash_bytes = bcs::to_bytes(&tx_hash); + vector::append(&mut seed_bytes, tx_hash_bytes); + }; + + vector::append(&mut seed_bytes, timestamp_ms_bytes); + vector::append(&mut seed_bytes, sender_addr_bytes); + vector::append(&mut seed_bytes, sequence_number_bytes); + vector::append(&mut seed_bytes, nonce_bytes); + + hash::sha3_256(seed_bytes) + } + + public fun bytes_to_u64(bytes: vector): u64 { + let value = 0u64; + let i = 0u64; + while (i < 8) { + value = value | ((*vector::borrow(&bytes, i) as u64) << ((8 * (7 - i)) as u8)); + i = i + 1; + }; + return value + } + + public fun bytes_to_u128(bytes: vector): u128 { + let value = 0u128; + let i = 0u64; + while (i < 16) { + value = value | ((*vector::borrow(&bytes, i) as u128) << ((8 * (15 - i)) as u8)); + i = i + 1; + }; + return value + } + + /// Generate a random u64 from seed + public fun rand_u64(nonce: u64): u64 { + let seed_bytes = seed(nonce); + bytes_to_u64(seed_bytes) + } + + /// Generate a random u128 from seed + public fun rand_u128(nonce: u64): u128 { + let seed_bytes = seed(nonce); + bytes_to_u128(seed_bytes) + } + + /// Generate a random integer range in [low, high) for u64. + public fun rand_u64_range(nonce: u64, low: u64, high: u64): u64 { + assert!(high > low, ErrorInvalidArg); + let value = rand_u64(nonce); + (value % (high - low)) + low + } + + /// Generate a random integer range in [low, high) for u128. + public fun rand_u128_range(nonce: u64, low: u128, high: u128): u128 { + assert!(high > low, ErrorInvalidArg); + let value = rand_u128(nonce); + (value % (high - low)) + low + } + + #[test] + fun test_bytes_to_u64() { + // binary: 01010001 11010011 10101111 11001100 11111101 00001001 10001110 11001101 + // bytes = [81, 211, 175, 204, 253, 9, 142, 205]; + let dec: u64 = 5896249632111562445; + + let bytes = vector::empty(); + vector::push_back(&mut bytes, 81); + vector::push_back(&mut bytes, 211); + vector::push_back(&mut bytes, 175); + vector::push_back(&mut bytes, 204); + vector::push_back(&mut bytes, 253); + vector::push_back(&mut bytes, 9); + vector::push_back(&mut bytes, 142); + vector::push_back(&mut bytes, 205); + + let value = bytes_to_u64(bytes); + assert!(value == dec, ErrorInvalidU64); + } + + #[test] + fun test_bytes_to_u128() { + // Example binary: 00000001 00100011 01000101 01100111 10001001 10101011 11001101 11101111 + // 00000000 00100010 01000100 01100110 10001000 10101010 11001100 11101110 + // bytes = [1, 35, 69, 103, 137, 171, 205, 239, 0, 34, 68, 102, 136, 170, 204, 238]; + let dec: u128 = 0x0123456789abcdef0022446688aaccee; + + let bytes = vector::empty(); + vector::push_back(&mut bytes, 1); + vector::push_back(&mut bytes, 35); + vector::push_back(&mut bytes, 69); + vector::push_back(&mut bytes, 103); + vector::push_back(&mut bytes, 137); + vector::push_back(&mut bytes, 171); + vector::push_back(&mut bytes, 205); + vector::push_back(&mut bytes, 239); + vector::push_back(&mut bytes, 0); + vector::push_back(&mut bytes, 34); + vector::push_back(&mut bytes, 68); + vector::push_back(&mut bytes, 102); + vector::push_back(&mut bytes, 136); + vector::push_back(&mut bytes, 170); + vector::push_back(&mut bytes, 204); + vector::push_back(&mut bytes, 238); + + let value = bytes_to_u128(bytes); + assert!(value == dec, ErrorInvalidU128); + } + + #[test] + fun test_generate_seed() { + // Test with nonce = 0 + let nonce = 0; + + // Mock sequence number + let sequence_number = 0; + let sequence_number_bytes = bcs::to_bytes(&sequence_number); + + // Mock sender address + let sender_addr = tx_context::sender(); + let sender_addr_bytes = bcs::to_bytes(&sender_addr); + + // Mock timestamp + let timestamp_ms = 0; + let timestamp_ms_bytes = bcs::to_bytes(×tamp_ms); + + // Mock tx hash + let tx_hash = tx_context::tx_hash(); + let tx_hash_bytes = bcs::to_bytes(&tx_hash); + + let nonce_bytes = bcs::to_bytes(&nonce); + + let expected_seed_bytes = vector::empty(); + vector::append(&mut expected_seed_bytes, tx_hash_bytes); + vector::append(&mut expected_seed_bytes, timestamp_ms_bytes); + vector::append(&mut expected_seed_bytes, sender_addr_bytes); + vector::append(&mut expected_seed_bytes, sequence_number_bytes); + vector::append(&mut expected_seed_bytes, nonce_bytes); + + let expected_seed = hash::sha3_256(expected_seed_bytes); + let seed_bytes = seed(nonce); + + assert!(seed_bytes == expected_seed, ErrorInvalidSeed); + } + + #[test] + fun test_rand_u64_range() { + let nonce = 0; + let low = 1; + let high = 101; + let value = rand_u64_range(nonce, low, high); + assert!(value >= low && value < high, ErrorInvalidArg); + } + + #[test] + fun test_rand_u128_range() { + let nonce = 0; + let low = 1; + let high = 101; + let value = rand_u128_range(nonce, low, high); + assert!(value >= low && value < high, ErrorInvalidArg); + } + + #[test] + #[expected_failure(abort_code = ErrorInvalidArg)] + fun test_rand_u64_range_invalid_range() { + rand_u64_range(0, 100, 50); + } + + #[test] + #[expected_failure(abort_code = ErrorInvalidArg)] + fun test_rand_u128_range_invalid_range() { + rand_u128_range(0, 100, 50); + } +} diff --git a/examples/rooch_fish/sources/utils.move b/examples/rooch_fish/sources/utils.move new file mode 100644 index 0000000000..4e4910df8d --- /dev/null +++ b/examples/rooch_fish/sources/utils.move @@ -0,0 +1,82 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +module rooch_fish::utils { + use rooch_fish::simple_rng; + + /// Generate a random position within the given bounds + public fun random_position(nonce: u64, max_x: u64, max_y: u64): (u64, u64) { + (random_u64(nonce, max_x), random_u64(nonce + 1, max_y)) + } + + /// Generate a random u64 number between 0 and max (inclusive) + public fun random_u64(nonce: u64, max: u64): u64 { + simple_rng::rand_u64_range(nonce, 0, max + 1) + } + + /// Calculate Manhattan distance between two points + public fun calculate_distance(x1: u64, y1: u64, x2: u64, y2: u64): u64 { + let dx = if (x1 > x2) { x1 - x2 } else { x2 - x1 }; + let dy = if (y1 > y2) { y1 - y2 } else { y2 - y1 }; + dx + dy + } + + /// Clamp a value between min and max + public fun clamp(value: u64, min: u64, max: u64): u64 { + if (value < min) min + else if (value > max) max + else value + } + + /// Check if a point is within a rectangle + public fun is_point_in_rect(x: u64, y: u64, rect_x: u64, rect_y: u64, rect_width: u64, rect_height: u64): bool { + x >= rect_x && x < rect_x + rect_width && y >= rect_y && y < rect_y + rect_height + } + + /// Performs saturating subtraction + public fun saturating_sub(x: u64, y: u64): u64 { + if (x < y) { + 0 + } else { + x - y + } + } + + #[test] + fun test_random_position() { + let (x, y) = random_position(0, 100, 100); + assert!(x <= 100 && y <= 100, 0); + } + + #[test] + fun test_random_u64() { + let r = random_u64(0, 10); + assert!(r <= 10, 0); + } + + #[test] + fun test_calculate_distance() { + assert!(calculate_distance(0, 0, 3, 4) == 7, 0); + } + + #[test] + fun test_clamp() { + assert!(clamp(5, 0, 10) == 5, 0); + assert!(clamp(15, 0, 10) == 10, 0); + assert!(clamp(0, 5, 10) == 5, 0); + } + + #[test] + fun test_is_point_in_rect() { + assert!(is_point_in_rect(5, 5, 0, 0, 10, 10), 0); + assert!(!is_point_in_rect(15, 15, 0, 0, 10, 10), 0); + } + + #[test] + fun test_saturating_sub() { + assert!(saturating_sub(10, 5) == 5, 0); + assert!(saturating_sub(5, 10) == 0, 1); + assert!(saturating_sub(0, 1) == 0, 2); + assert!(saturating_sub(18446744073709551615, 1) == 18446744073709551614, 3); // u64::MAX - 1 + } +} diff --git a/examples/rooch_fish/tests/economic_system_test.move b/examples/rooch_fish/tests/economic_system_test.move new file mode 100644 index 0000000000..70bda14819 --- /dev/null +++ b/examples/rooch_fish/tests/economic_system_test.move @@ -0,0 +1,109 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +module rooch_fish::economic_system_test { + use std::signer; + use std::vector; + use moveos_std::object; + + use rooch_framework::genesis; + use rooch_framework::gas_coin; + use rooch_fish::rooch_fish::{Self, GameState}; + use rooch_fish::player; + + const POND_ID_SMALL: u64 = 0; // Assuming pond 0 is the smallest + const POND_ID_LARGE: u64 = 7; // Assuming pond 7 is the largest + const INITIAL_BALANCE: u256 = 1000000000000; // 10000 RGAS + + #[test(admin = @rooch_fish, player1 = @0x42, player2 = @0x43)] + fun test_economic_system(admin: signer, player1: signer, player2: signer) { + // Initialize the test environment + genesis::init_for_test(); + rooch_fish::init_world(&admin); + + let player1_addr = signer::address_of(&player1); + let player2_addr = signer::address_of(&player2); + gas_coin::faucet_for_test(player1_addr, INITIAL_BALANCE); + gas_coin::faucet_for_test(player2_addr, INITIAL_BALANCE); + + // Get GameState object + let game_state_id = object::named_object_id(); + let game_state_obj = object::borrow_mut_object_shared(game_state_id); + + // Test purchase in different ponds + let initial_balance = gas_coin::balance(player1_addr); + rooch_fish::purchase_fish(&player1, game_state_obj, POND_ID_SMALL); + let cost_small = initial_balance - gas_coin::balance(player1_addr); + + let initial_balance = gas_coin::balance(player1_addr); + rooch_fish::purchase_fish(&player1, game_state_obj, POND_ID_LARGE); + let cost_large = initial_balance - gas_coin::balance(player1_addr); + + // Verify that larger pond costs more + assert!(cost_large > cost_small, 1); + + // Get fish IDs + let fish_ids_small = rooch_fish::get_pond_player_fish_ids(game_state_obj, POND_ID_SMALL, player1_addr); + let fish_ids_large = rooch_fish::get_pond_player_fish_ids(game_state_obj, POND_ID_LARGE, player1_addr); + let fish1_small = *vector::borrow(&fish_ids_small, 0); + let fish1_large = *vector::borrow(&fish_ids_large, 0); + + // Test feeding + let food_count = 5; + let initial_balance = gas_coin::balance(player1_addr); + rooch_fish::feed_food(&player1, game_state_obj, POND_ID_SMALL, food_count); + let feed_cost = initial_balance - gas_coin::balance(player1_addr); + + // Get pond info + let (_, _, _, purchase_amount, _, food_value_ratio) = rooch_fish::get_pond_info(game_state_obj, POND_ID_SMALL); + + // Calculate expected cost + let food_value = purchase_amount / (food_value_ratio as u256); + let expected_cost = (food_count as u256) * food_value; + + // Assert that the actual cost matches the expected cost + assert!(feed_cost == expected_cost, 2); + + // Test fish growth and reward + let food_id = rooch_fish::get_last_food_id(game_state_obj, POND_ID_SMALL); + rooch_fish::set_food_position_for_test(game_state_obj, POND_ID_SMALL, food_id, 26, 25); + rooch_fish::set_fish_position_for_test(game_state_obj, POND_ID_SMALL, fish1_small, 25, 25); + rooch_fish::move_fish(&player1, game_state_obj, POND_ID_SMALL, fish1_small, 1); + + rooch_fish::set_fish_position_for_test(game_state_obj, POND_ID_SMALL, fish1_small, 50, 50); + let initial_balance = gas_coin::balance(player1_addr); + rooch_fish::destroy_fish(&player1, game_state_obj, POND_ID_SMALL, fish1_small); + let reward_small = gas_coin::balance(player1_addr) - initial_balance; + + // Test reward in larger pond + rooch_fish::set_fish_position_for_test(game_state_obj, POND_ID_LARGE, fish1_large, 500, 500); + let initial_balance = gas_coin::balance(player1_addr); + rooch_fish::destroy_fish(&player1, game_state_obj, POND_ID_LARGE, fish1_large); + let reward_large = gas_coin::balance(player1_addr) - initial_balance; + + assert!(reward_large > reward_small, 3); + + // Test feeding distribution + rooch_fish::purchase_fish(&player2, game_state_obj, POND_ID_SMALL); + rooch_fish::feed_food(&player1, game_state_obj, POND_ID_SMALL, food_count); + + let player_list = rooch_fish::get_global_player_list(game_state_obj); + let player1_feed = player::get_player_feed_amount(player_list, player1_addr); + let player2_feed = player::get_player_feed_amount(player_list, player2_addr); + + // Get pond info for final verification + let (_, _, _, purchase_amount, _, food_value_ratio) = rooch_fish::get_pond_info(game_state_obj, POND_ID_SMALL); + let food_value = purchase_amount / (food_value_ratio as u256); + let expected_total_feed = (food_count as u256) * food_value * 2; // Player 1 fed twice + + assert!(player1_feed == expected_total_feed, 4); + assert!(player2_feed == 0, 5); + + // Clean up + let fish_ids_small = rooch_fish::get_pond_player_fish_ids(game_state_obj, POND_ID_SMALL, player2_addr); + let fish2_small = *vector::borrow(&fish_ids_small, 0); + rooch_fish::set_fish_position_for_test(game_state_obj, POND_ID_SMALL, fish2_small, 50, 50); + rooch_fish::destroy_fish(&player2, game_state_obj, POND_ID_SMALL, fish2_small); + } +} + diff --git a/examples/rooch_fish/tests/fish_lifecycle_test.move b/examples/rooch_fish/tests/fish_lifecycle_test.move new file mode 100644 index 0000000000..fcf1f040ad --- /dev/null +++ b/examples/rooch_fish/tests/fish_lifecycle_test.move @@ -0,0 +1,74 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +module rooch_fish::fish_lifecycle_test { + use std::signer; + use std::vector; + use moveos_std::object; + + use rooch_framework::genesis; + use rooch_framework::gas_coin; + + use rooch_fish::player; + use rooch_fish::rooch_fish::{Self, GameState}; + + const POND_ID: u64 = 0; + const INITIAL_BALANCE: u256 = 1000000000000; // 10000 RGAS + + #[test(admin = @rooch_fish, player = @0x42)] + fun test_fish_lifecycle(admin: signer, player: signer) { + // Initialize the test environment + genesis::init_for_test(); + rooch_fish::init_world(&admin); + + let player_addr = signer::address_of(&player); + gas_coin::faucet_for_test(player_addr, INITIAL_BALANCE); + + // Get GameState object + let game_state_id = object::named_object_id(); + let game_state_obj = object::borrow_mut_object_shared(game_state_id); + + // Purchase a fish + rooch_fish::purchase_fish(&player, game_state_obj, POND_ID); + + // Verify fish count increased + assert!(rooch_fish::get_pond_player_count(game_state_obj, POND_ID) == 1, 1); + + // Get the fish ID + let fish_ids = rooch_fish::get_pond_player_fish_ids(game_state_obj, POND_ID, player_addr); + assert!(vector::length(&fish_ids) == 1, 2); + let fish_id = *vector::borrow(&fish_ids, 0); + + // Set fish position for testing + rooch_fish::set_fish_position_for_test(game_state_obj, POND_ID, fish_id, 25, 25); + + // Move the fish + rooch_fish::move_fish(&player, game_state_obj, POND_ID, fish_id, 1); // Move right + + // Feed food + let food_count = 5; + rooch_fish::feed_food(&player, game_state_obj, POND_ID, food_count); + + // Get last food ID and set its position near the fish + let last_food_id = rooch_fish::get_last_food_id(game_state_obj, POND_ID); + rooch_fish::set_food_position_for_test(game_state_obj, POND_ID, last_food_id, 26, 25); + + // Verify total feed increased (exact amount depends on pond config) + assert!(rooch_fish::get_pond_total_feed(game_state_obj, POND_ID) > 0, 3); + + // Set fish position to exit zone for testing + rooch_fish::set_fish_position_for_test(game_state_obj, POND_ID, fish_id, 50, 50); + + // Destroy the fish + rooch_fish::destroy_fish(&player, game_state_obj, POND_ID, fish_id); + + // Verify fish count decreased + let fish_ids = rooch_fish::get_pond_player_fish_ids(game_state_obj, POND_ID, player_addr); + assert!(vector::length(&fish_ids) == 0, 4); + + // Verify player received rewards + let player_list = rooch_fish::get_global_player_list(game_state_obj); + let player_reward = player::get_player_reward(player_list, player_addr); + assert!(player_reward > 0, 5); + } +} diff --git a/examples/rooch_fish/tests/init_test.move b/examples/rooch_fish/tests/init_test.move new file mode 100644 index 0000000000..eb50cfad66 --- /dev/null +++ b/examples/rooch_fish/tests/init_test.move @@ -0,0 +1,63 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +module rooch_fish::init_test { + use std::vector; + use moveos_std::object; + use rooch_framework::genesis; + use rooch_fish::rooch_fish::{Self, GameState}; + + struct PondConfig has drop { + width: u64, + height: u64, + max_fish_count: u64, + purchase_amount: u64, + } + + #[test(admin = @rooch_fish)] + fun test_game_initialization(admin: signer) { + // Initialize the test environment + genesis::init_for_test(); + + // Initialize the game world + rooch_fish::init_world(&admin); + + // Get GameState object + let game_state_id = object::named_object_id(); + let game_state_obj = object::borrow_mut_object_shared(game_state_id); + + // Verify the number of ponds + assert!(rooch_fish::get_pond_count(game_state_obj) == 8, 0); + + // Define expected pond configurations + let expected_configs = vector[ + PondConfig { width: 100, height: 100, max_fish_count: 100, purchase_amount: 100000 }, + PondConfig { width: 1000, height: 1000, max_fish_count: 1000, purchase_amount: 100000 }, + PondConfig { width: 10000, height: 10000, max_fish_count: 10000, purchase_amount: 100000 }, + PondConfig { width: 100000, height: 100000, max_fish_count: 100000, purchase_amount: 100000 }, + PondConfig { width: 1000, height: 1000, max_fish_count: 1000, purchase_amount: 1000000 }, + PondConfig { width: 1000, height: 1000, max_fish_count: 1000, purchase_amount: 10000000 }, + PondConfig { width: 1000, height: 1000, max_fish_count: 1000, purchase_amount: 100000000 }, + PondConfig { width: 1000, height: 1000, max_fish_count: 1000, purchase_amount: 1000000000 }, + ]; + + // Verify each pond's configuration + let i = 0; + while (i < 8) { + let (width, height, max_fish_count, purchase_amount,_,_) = rooch_fish::get_pond_info(game_state_obj, (i as u64)); + let expected_config = vector::borrow(&expected_configs, i); + + assert!(width == expected_config.width, 1); + assert!(height == expected_config.height, 2); + assert!(max_fish_count == expected_config.max_fish_count, 3); + assert!(purchase_amount == (expected_config.purchase_amount as u256), 4); + + i = i + 1; + }; + + // Verify global player count and total feed + assert!(rooch_fish::get_global_player_count(game_state_obj) == 0, 5); + assert!(rooch_fish::get_global_total_feed(game_state_obj) == 0, 6); + } +} + diff --git a/examples/rooch_fish/web/.eslintrc.cjs b/examples/rooch_fish/web/.eslintrc.cjs new file mode 100644 index 0000000000..d6c9537953 --- /dev/null +++ b/examples/rooch_fish/web/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/examples/rooch_fish/web/.gitignore b/examples/rooch_fish/web/.gitignore new file mode 100644 index 0000000000..a547bf36d8 --- /dev/null +++ b/examples/rooch_fish/web/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/rooch_fish/web/README.md b/examples/rooch_fish/web/README.md new file mode 100644 index 0000000000..3b3b7d92d6 --- /dev/null +++ b/examples/rooch_fish/web/README.md @@ -0,0 +1,78 @@ +# Have You Fished? - Frontend + +Welcome to the frontend repository for "Have You Fished?", a multiplayer online game built on the Rooch blockchain. In this game, players compete by purchasing and controlling virtual fish in dynamic ponds, combining elements of growth, strategy, and economics. + +## Table of Contents + +- [Game Overview](#game-overview) +- [Features](#features) +- [Technologies Used](#technologies-used) +- [Getting Started](#getting-started) +- [Scripts](#scripts) +- [Contributing](#contributing) +- [License](#license) +- [Demo](#demo) + +## Game Overview + +"Have You Fished?" is a strategic game where players grow their fish by consuming smaller fish and collecting food. Players can earn RGAS tokens by achieving specific conditions, such as reaching the pond's exit. The game introduces a stamina system to add strategic depth and balance. + +### Key Mechanics + +- **Pond System**: Multiple ponds with varying sizes and capacities. +- **Fish Growth**: Consume smaller fish and food to grow. +- **Stamina System**: Manage stamina for movement and growth. +- **Feeding System**: Use RGAS tokens to feed ponds and influence the economy. +- **Exit and Rewards**: Earn tokens by reaching the pond's exit. + +## Features + +- **Dynamic Ponds**: Each pond has unique parameters and economic systems. +- **Interactive Gameplay**: Real-time control of fish movement and growth. +- **Blockchain Integration**: Utilizes Rooch blockchain for secure transactions. +- **Strategic Depth**: Balance between growth, stamina management, and economic investment. + +## Technologies Used + +- **React**: For building the user interface. +- **Vite**: For fast development and build processes. +- **TypeScript**: For type-safe JavaScript development. +- **MUI (Material-UI)**: For UI components and styling. +- **Rooch SDK**: For blockchain interactions. +- **Tailwind CSS**: For utility-first CSS styling. +- **React Query**: For data fetching and state management. +- **PixiJS**: As the game engine for rendering 2D graphics. + +## Getting Started + +To get a local copy up and running, follow these steps: + +### Prerequisites + +- Node.js and npm installed on your machine. + +### Installation + +1. Install dependencies: + ```bash + npm install + ``` + +2. Start the development server: + ```bash + npm run dev + ``` + +3. Open your browser and navigate to `http://localhost:3000` to see the application in action. + +## Scripts + +- `npm run dev`: Starts the development server. +- `npm run build`: Builds the application for production. +- `npm run lint`: Lints the codebase using ESLint. +- `npm run preview`: Previews the production build. + +## Demo + +Check out the live demo of the application on the test network: [Have You Fished? Demo](https://rooch-fish-web.vercel.app/) + diff --git a/examples/rooch_fish/web/index.html b/examples/rooch_fish/web/index.html new file mode 100644 index 0000000000..4a6e341a4e --- /dev/null +++ b/examples/rooch_fish/web/index.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + Rooch Fish + + +
+ + + diff --git a/examples/rooch_fish/web/package.json b/examples/rooch_fish/web/package.json new file mode 100644 index 0000000000..211f37c595 --- /dev/null +++ b/examples/rooch_fish/web/package.json @@ -0,0 +1,49 @@ +{ + "name": "rooch_fish_web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.5", + "@fontsource-variable/plus-jakarta-sans": "^5.0.21", + "@fontsource-variable/raleway": "^5.0.20", + "@mui/lab": "^5.0.0-alpha.172", + "@mui/material": "^5.16.4", + "@pixi/react": "7", + "@roochnetwork/rooch-sdk": "^0.2.1", + "@roochnetwork/rooch-sdk-kit": "^0.2.1", + "@tanstack/react-query": "^5.51.9", + "notistack": "^3.0.1", + "pixi.js": "7", + "react": "^18.3.1", + "react-countup": "^6.5.3", + "react-dom": "^18.3.1", + "react-gtm-module": "^2.0.11" + }, + "resolutions": { + "cross-spawn": "7.0.5" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@types/react-gtm-module": "^2.0.3", + "@typescript-eslint/eslint-plugin": "^7.13.1", + "@typescript-eslint/parser": "^7.13.1", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.19", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", + "postcss": "^8.4.39", + "tailwindcss": "^3.4.6", + "typescript": "^5.2.2", + "vite": "^5.3.1" + } +} diff --git a/examples/rooch_fish/web/postcss.config.js b/examples/rooch_fish/web/postcss.config.js new file mode 100644 index 0000000000..1a88bb2c53 --- /dev/null +++ b/examples/rooch_fish/web/postcss.config.js @@ -0,0 +1,10 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 +// Author: Jason Jo + +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/examples/rooch_fish/web/public/logo.svg b/examples/rooch_fish/web/public/logo.svg new file mode 100644 index 0000000000..7e9fbb2e79 --- /dev/null +++ b/examples/rooch_fish/web/public/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/rooch_fish/web/public/rooch_black_combine.svg b/examples/rooch_fish/web/public/rooch_black_combine.svg new file mode 100644 index 0000000000..4fae3f2d5e --- /dev/null +++ b/examples/rooch_fish/web/public/rooch_black_combine.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/rooch_fish/web/src/App.css b/examples/rooch_fish/web/src/App.css new file mode 100644 index 0000000000..064a8eabd5 --- /dev/null +++ b/examples/rooch_fish/web/src/App.css @@ -0,0 +1,10 @@ +/* Copyright (c) RoochNetwork */ +/* SPDX-License-Identifier: Apache-2.0 */ +/* Author: Jason Jo */ + +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} \ No newline at end of file diff --git a/examples/rooch_fish/web/src/App.tsx b/examples/rooch_fish/web/src/App.tsx new file mode 100644 index 0000000000..7bf501417c --- /dev/null +++ b/examples/rooch_fish/web/src/App.tsx @@ -0,0 +1,344 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 +// Author: Jason Jo + +import { config } from "./config"; +import { LoadingButton } from "@mui/lab"; +import { Button, Chip, Drawer, Stack, Typography } from "@mui/material"; +import { styled } from "@mui/material/styles"; +import { Args, Transaction } from "@roochnetwork/rooch-sdk"; +import { + UseSignAndExecuteTransaction, + useConnectWallet, + useCreateSessionKey, + useCurrentAddress, + useCurrentSession, + useRemoveSession, + useRoochClientQuery, + useWalletStore, + useWallets, +} from "@roochnetwork/rooch-sdk-kit"; +import { enqueueSnackbar } from "notistack"; +import { useState } from "react"; +import CountUp from "react-countup"; +import "./App.css"; +import { useRccOwner } from "./hooks/useRccOwner"; +import { fNumber, shortAddress } from "./utils"; +import { DebugScene } from './scenes/debug_scene' +import { PondScene } from './scenes/pond_scene'; +import { useGameState } from './hooks/useGameState'; +import { useLatestTransaction } from "./hooks/useLatestTransaction"; + +function getNextRewardClick(currentClicks: number): number { + const remainder = currentClicks % 21; + if (remainder === 0) { + return currentClicks + 21; + } else { + return currentClicks + (21 - remainder); + } +} + +const drawerWidth = 300; + +const Main = styled("main", { shouldForwardProp: (prop) => prop !== "open" })<{ + open?: boolean; +}>(({ theme, open }) => ({ + flexGrow: 1, + alignItems: "center", + padding: theme.spacing(3), + transition: theme.transitions.create("margin", { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + marginLeft: `${open ? drawerWidth : "0"}px`, + ...(open && { + transition: theme.transitions.create("margin", { + easing: theme.transitions.easing.easeOut, + duration: theme.transitions.duration.enteringScreen, + }), + }), +})); + +// Publish address of the counter contract +const counterAddress = + "0x872502737008ac71c4c008bb3846a688bfd9fa54c6724089ea51b72f813dc71e"; + +const roochCounterObject = + "0x24e093a6fa4698d1b6efd27ae9f1c21057b91bb9a2ef3c0fce2c94b44601764b"; + +const treasuryObject = + "0xe7beeda989fa0b2201c945310d533c82027c3270a39e2bcbaa65c4563210db82"; + +const treasuryOwnerAddress = + "rooch1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhxqaen"; + +const rgasCoinType = + "0x3::gas_coin::RGas"; + +export const rccCoinStoreType = + "0x3::coin_store::CoinStore<0x872502737008ac71c4c008bb3846a688bfd9fa54c6724089ea51b72f813dc71e::rooch_clicker_coin::RCC>"; + +/* +const fetchGameStateInfo = async (client: RoochClient) => { + const data = await client.getStates({ + accessPath: `/object/${config.gameStateObjectID}`, + stateOption: { + decode: true, + }, + }); + + return { result: data }; +}; +*/ + +function App() { + const wallets = useWallets(); + const currentAddress = useCurrentAddress(); + const sessionKey = useCurrentSession(); + const connectionStatus = useWalletStore((state) => state.connectionStatus); + const setWalletDisconnected = useWalletStore( + (state) => state.setWalletDisconnected + ); + const { mutateAsync: connectWallet } = useConnectWallet(); + + const { mutateAsync: createSessionKey } = useCreateSessionKey(); + const { mutateAsync: removeSessionKey } = useRemoveSession(); + const { mutateAsync: signAndExecuteTransaction } = + UseSignAndExecuteTransaction(); + + const { rccOwnerList } = useRccOwner(); + + const [showLeaderboard, setShowLeaderboard] = useState(false); + + console.log("currentAddress:", currentAddress?.genRoochAddress().toStr() || ""); + + const { data: RCCBalance, refetch: refetchRCCBalance } = useRoochClientQuery( + "getBalance", + { + owner: currentAddress?.genRoochAddress().toStr() || "", + coinType: rgasCoinType, + } + ); + + const [sessionLoading, setSessionLoading] = useState(false); + const [txnLoading, setTxnLoading] = useState(false); + const handlerCreateSessionKey = async () => { + if (sessionLoading) { + return; + } + setSessionLoading(true); + + const defaultScopes = [`${config.roochFishAddress}::*::*`]; + createSessionKey( + { + appName: "rooch_clicker", + appUrl: "http://localhost:5173", + maxInactiveInterval: 3600, + scopes: defaultScopes, + }, + { + onSuccess: (result) => { + console.log("session key", result); + }, + onError: (error) => { + if (String(error).includes("1004")) { + enqueueSnackbar("Insufficient gas, please claim gas first", { + variant: "warning", + action: ( + + + + ), + }); + } else { + enqueueSnackbar(String(error), { + variant: "warning", + }); + } + }, + } + ).finally(() => setSessionLoading(false)); + }; + + return ( + + + + + + + + + + + + + + + Rooch Fish |{" "} + {RCCBalance && ( + + Balance: {fNumber(RCCBalance.balance.toString(), 8)} RGas{" "} + ( Rooch Gas Coin ) + + )} + + {" "} + + {!sessionKey ? ( + { + handlerCreateSessionKey(); + }} + > + {connectionStatus !== "connected" + ? "Please connect wallet first" + : "Create Session Key"} + + ) : ( + + )} + + + + + + + Leaderboard + + + + {rccOwnerList + ?.filter((i) => i.owner !== treasuryOwnerAddress) + .sort((a, b) => { + return ( + Number((b.decoded_value?.value.balance as any).value.value) - + Number((a.decoded_value?.value.balance as any).value.value) + ); + }) + .map((item: any, i: number) => { + return ( + + + {shortAddress(item.owner_bitcoin_address, 6, 6)} + + + {fNumber( + Number( + (item.decoded_value?.value.balance as any).value.value + ),8 + )} + + + ); + })} + + +
+ {config.debug ? ( + + ):( + + )} +
+
+
+ ); +} + +export default App; diff --git a/examples/rooch_fish/web/src/clients/index.ts b/examples/rooch_fish/web/src/clients/index.ts new file mode 100644 index 0000000000..5851ab8568 --- /dev/null +++ b/examples/rooch_fish/web/src/clients/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +export * from "./rooch/index" \ No newline at end of file diff --git a/examples/rooch_fish/web/src/clients/rooch/index.ts b/examples/rooch_fish/web/src/clients/rooch/index.ts new file mode 100644 index 0000000000..cd3334ad26 --- /dev/null +++ b/examples/rooch_fish/web/src/clients/rooch/index.ts @@ -0,0 +1,5 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +export * from "./wsTransport" +export * from "./wsClient" \ No newline at end of file diff --git a/examples/rooch_fish/web/src/clients/rooch/wsClient.ts b/examples/rooch_fish/web/src/clients/rooch/wsClient.ts new file mode 100644 index 0000000000..0b08ba8e5b --- /dev/null +++ b/examples/rooch_fish/web/src/clients/rooch/wsClient.ts @@ -0,0 +1,27 @@ + +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +import { + RoochClientOptions, + RoochClient, + isRoochClient +} from "@roochnetwork/rooch-sdk"; +import { RoochWebSocketTransport} from "./wsTransport" + +export const DEFAULT_CREATE_WS_CLIENT = ( + _name: string, + config: RoochClientOptions | RoochClient, +) => { + if (isRoochClient(config)) { + return config + } + + config.transport = new RoochWebSocketTransport( + { + url: config.url!.toString(), + }, + ) + + return new RoochClient(config) +} \ No newline at end of file diff --git a/examples/rooch_fish/web/src/clients/rooch/wsTransport.ts b/examples/rooch_fish/web/src/clients/rooch/wsTransport.ts new file mode 100644 index 0000000000..0470ff0a47 --- /dev/null +++ b/examples/rooch_fish/web/src/clients/rooch/wsTransport.ts @@ -0,0 +1,131 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +import { + RoochTransport, + RoochHTTPTransportOptions, + RoochTransportRequestOptions, + JsonRpcError +} from "@roochnetwork/rooch-sdk"; + +interface WsRequest { + resolve: (value: any) => void + reject: (error: Error) => void + method: string + params: unknown[] + timestamp: number +} + +export class RoochWebSocketTransport implements RoochTransport { + #ws: WebSocket | null = null + #requestId = 0 + #options: RoochHTTPTransportOptions + #pendingRequests = new Map() + #reconnectAttempts = 0 + #maxReconnectAttempts = 5 + #reconnectDelay = 1000 + #connected = false + #connecting = false + + constructor(options: RoochHTTPTransportOptions) { + this.#options = options + this.connect() + } + + private connect() { + if (this.#connecting || this.#connected) return + + this.#connecting = true + const wsUrl = this.#options.url.replace(/^httpS/, 'wss') + this.#ws = new WebSocket(wsUrl) + + this.#ws.onopen = () => { + this.#connected = true + this.#connecting = false + this.#reconnectAttempts = 0 + } + + this.#ws.onclose = () => { + this.#connected = false + this.#ws = null + this.handleReconnect() + } + + this.#ws.onerror = (error) => { + console.error('WebSocket error:', error) + } + + this.#ws.onmessage = (event) => { + try { + const response = JSON.parse(event.data) + this.handleResponse(response) + } catch (error) { + console.error('Failed to parse WebSocket message:', error) + } + } + } + + private handleReconnect() { + if (this.#reconnectAttempts >= this.#maxReconnectAttempts) { + this.rejectAllPending(new Error('WebSocket connection failed')) + return + } + + this.#reconnectAttempts++ + setTimeout(() => this.connect(), this.#reconnectDelay * this.#reconnectAttempts) + } + + private handleResponse(response: any) { + const request = this.#pendingRequests.get(response.id) + if (!request) return + + this.#pendingRequests.delete(response.id) + + if ('error' in response && response.error != null) { + request.reject(new JsonRpcError(response.error.message, response.error.code)) + } else { + request.resolve(response.result) + } + } + + private rejectAllPending(error: Error) { + for (const request of this.#pendingRequests.values()) { + request.reject(error) + } + this.#pendingRequests.clear() + } + + async request(input: RoochTransportRequestOptions): Promise { + if (!this.#connected) { + throw new Error('WebSocket is not connected') + } + + return new Promise((resolve, reject) => { + const id = ++this.#requestId + const request: WsRequest = { + resolve, + reject, + method: input.method, + params: input.params, + timestamp: Date.now(), + } + + this.#pendingRequests.set(id, request) + this.#ws?.send( + JSON.stringify({ + jsonrpc: '2.0', + id, + method: input.method, + params: input.params, + }) + ) + }) + } + + disconnect() { + this.#ws?.close() + this.#connected = false + this.#connecting = false + this.rejectAllPending(new Error('WebSocket disconnected')) + } +} diff --git a/examples/rooch_fish/web/src/components/fish.tsx b/examples/rooch_fish/web/src/components/fish.tsx new file mode 100644 index 0000000000..35bb9467b6 --- /dev/null +++ b/examples/rooch_fish/web/src/components/fish.tsx @@ -0,0 +1,41 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +import { Container, Graphics } from '@pixi/react'; +import { useCallback } from 'react'; +import * as PIXI from 'pixi.js'; + +interface FishProps { + x: number; + y: number; + scale?: number; + rotation?: number; + color?: number; +} + +export const Fish = ({ x, y, scale = 1, rotation = 0, color = 0xFF6B6B}: FishProps) => { + const drawFish = useCallback((g: PIXI.Graphics) => { + g.clear(); + g.lineStyle(2, 0x000000, 1); + g.beginFill(color); + + // Almost circular body + g.moveTo(-15, 0); + g.bezierCurveTo(-15, -15, 15, -15, 15, 0); + g.bezierCurveTo(15, 15, -15, 15, -15, 0); + + // Small tail + g.moveTo(-15, 0); + g.lineTo(-22, -6); + g.lineTo(-22, 6); + g.lineTo(-15, 0); + + g.endFill(); + }, [color]); + + return ( + + + + ); +}; diff --git a/examples/rooch_fish/web/src/components/food.tsx b/examples/rooch_fish/web/src/components/food.tsx new file mode 100644 index 0000000000..6cbcac01ef --- /dev/null +++ b/examples/rooch_fish/web/src/components/food.tsx @@ -0,0 +1,28 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +import { Container, Graphics } from '@pixi/react'; +import { useCallback } from 'react'; +import * as PIXI from 'pixi.js'; + +interface FoodProps { + x: number; + y: number; + size?: number; + color?: number; +} + +export const Food = ({ x, y, size = 6, color = 0x00FF00 }: FoodProps) => { + const drawFood = useCallback((g: PIXI.Graphics) => { + g.clear(); + g.beginFill(color); + g.drawCircle(0, 0, size); + g.endFill(); + }, [color, size]); + + return ( + + + + ); +}; diff --git a/examples/rooch_fish/web/src/config/index.ts b/examples/rooch_fish/web/src/config/index.ts new file mode 100644 index 0000000000..43403578be --- /dev/null +++ b/examples/rooch_fish/web/src/config/index.ts @@ -0,0 +1,341 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +export const config = { + debug: false, + network: "testnet", // localnet, testnet + roochFishAddress: "0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e8", + gameStateObjectID: "0x3bbe1056fcfdfee92eae4d7045d9a231a3d5a4ca7ddd77e43d730bc80d8bc4d8", + ponds: { + 0: "0x4afb602f8bbbb516ef9c8dbcecb8d91bfb5ddb618a7505da0abb1904ebc1784b", + 1: "0x1fd948251d394db82f9d4a1e82118cf232c0f4f9c2c7ffa45ea9dffad28e9412", + 2: "0x676cb4ef030fd3cfc4a151a348e6c9bc987f97a11586dcee5660cd6bb1a2047c", + 3: "0x14b8edb38062592b47b6898e83c7b0df8f1d4439c0fb9d6728435ef870516b15", + 4: "0x2b3d3dc14e62060099e49a8c522b5698219795bf6a87d42624cc69eaf6263627", + 5: "0x7fe629442f293a0307c7cc6ae12edc0c60c4a7708c75a83b5f671ff03544e781", + 6: "0x855d118b9da7ac1f32922cf568f51d9df1e28cd0205d9f241ba0007c22fea5bb", + 7: "0xac8652636629f6bfe5ac6e102308bc032b01101243e50a3604c3d12955abf179" + } +} + +export type PondID = keyof typeof config.ponds; + +/* +testnet + +New modules: + 0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e8::simple_rng + 0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e8::pond + 0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e8::fish + 0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e8::player + 0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e8::quad_tree + 0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e8::rooch_fish + 0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e8::utils + 0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e8::food + +gameworld: +[ + { + "id": "0x3bbe1056fcfdfee92eae4d7045d9a231a3d5a4ca7ddd77e43d730bc80d8bc4d8", + "owner": "rooch1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhxqaen", + "owner_bitcoin_address": null, + "flag": 1, + "state_root": "0x5350415253455f4d45524b4c455f504c414345484f4c4445525f484153480000", + "size": "0", + "created_at": "1732199736531", + "updated_at": "1732199736531", + "object_type": "0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e8::rooch_fish::GameState", + "value": "0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e801ae308aa50bded0b341120431a9049ad1f151b345115a17bcefcad943505fa3b601ed4d53e3041bc78d727d34f30ccdaaa4227cadb87cd6ca6f6182673ba94b509c00000000000000000000000000000000000000000000000000000000000000000000000000000000", + "decoded_value": { + "abilities": 8, + "type": "0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e8::rooch_fish::GameState", + "value": { + "admin": "0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e8", + "player_list": { + "abilities": 12, + "type": "0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e8::player::PlayerList", + "value": { + "player_count": "0", + "players": { + "abilities": 4, + "type": "0x2::table::Table", + "value": { + "handle": { + "abilities": 12, + "type": "0x2::object::Object<0x2::table::TablePlaceholder>", + "value": { + "id": "0xed4d53e3041bc78d727d34f30ccdaaa4227cadb87cd6ca6f6182673ba94b509c" + } + } + } + }, + "total_feed": "0" + } + }, + "ponds": { + "abilities": 4, + "type": "0x2::table::Table>", + "value": { + "handle": { + "abilities": 12, + "type": "0x2::object::Object<0x2::table::TablePlaceholder>", + "value": { + "id": "0xae308aa50bded0b341120431a9049ad1f151b345115a17bcefcad943505fa3b6" + } + } + } + } + } + }, + "display_fields": null + } +] + +ponds: +{ + "data": [ + { + "field_key": "0x11228d102ec1ccd0e71d31a6115c33b886a4cd5cb48113db5c851c6d96e82b7f", + "state": { + "id": "0xae308aa50bded0b341120431a9049ad1f151b345115a17bcefcad943505fa3b611228d102ec1ccd0e71d31a6115c33b886a4cd5cb48113db5c851c6d96e82b7f", + "owner": "rooch1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhxqaen", + "owner_bitcoin_address": null, + "flag": 0, + "state_root": "0x5350415253455f4d45524b4c455f504c414345484f4c4445525f484153480000", + "size": "0", + "created_at": "1732199736531", + "updated_at": "1732199736531", + "object_type": "0x2::object::DynamicField>", + "value": "0x070000000000000001ac8652636629f6bfe5ac6e102308bc032b01101243e50a3604c3d12955abf179", + "decoded_value": { + "abilities": 12, + "type": "0x2::object::DynamicField>", + "value": { + "name": "7", + "value": { + "abilities": 12, + "type": "0x2::object::Object<0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e8::pond::PondState>", + "value": { + "id": "0xac8652636629f6bfe5ac6e102308bc032b01101243e50a3604c3d12955abf179" + } + } + } + }, + "display_fields": null + } + }, + { + "field_key": "0x14e41f37bef443dbf4163625571e6d035763b553cad649387b41be8f7c4c7569", + "state": { + "id": "0xae308aa50bded0b341120431a9049ad1f151b345115a17bcefcad943505fa3b614e41f37bef443dbf4163625571e6d035763b553cad649387b41be8f7c4c7569", + "owner": "rooch1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhxqaen", + "owner_bitcoin_address": null, + "flag": 0, + "state_root": "0x5350415253455f4d45524b4c455f504c414345484f4c4445525f484153480000", + "size": "0", + "created_at": "1732199736531", + "updated_at": "1732199736531", + "object_type": "0x2::object::DynamicField>", + "value": "0x020000000000000001676cb4ef030fd3cfc4a151a348e6c9bc987f97a11586dcee5660cd6bb1a2047c", + "decoded_value": { + "abilities": 12, + "type": "0x2::object::DynamicField>", + "value": { + "name": "2", + "value": { + "abilities": 12, + "type": "0x2::object::Object<0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e8::pond::PondState>", + "value": { + "id": "0x676cb4ef030fd3cfc4a151a348e6c9bc987f97a11586dcee5660cd6bb1a2047c" + } + } + } + }, + "display_fields": null + } + }, + { + "field_key": "0x28b56697e9fd6fbf8d6513b44cbeb793537ef7bd71dbe03c7a8d5cfc992ff407", + "state": { + "id": "0xae308aa50bded0b341120431a9049ad1f151b345115a17bcefcad943505fa3b628b56697e9fd6fbf8d6513b44cbeb793537ef7bd71dbe03c7a8d5cfc992ff407", + "owner": "rooch1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhxqaen", + "owner_bitcoin_address": null, + "flag": 0, + "state_root": "0x5350415253455f4d45524b4c455f504c414345484f4c4445525f484153480000", + "size": "0", + "created_at": "1732199736531", + "updated_at": "1732199736531", + "object_type": "0x2::object::DynamicField>", + "value": "0x0000000000000000014afb602f8bbbb516ef9c8dbcecb8d91bfb5ddb618a7505da0abb1904ebc1784b", + "decoded_value": { + "abilities": 12, + "type": "0x2::object::DynamicField>", + "value": { + "name": "0", + "value": { + "abilities": 12, + "type": "0x2::object::Object<0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e8::pond::PondState>", + "value": { + "id": "0x4afb602f8bbbb516ef9c8dbcecb8d91bfb5ddb618a7505da0abb1904ebc1784b" + } + } + } + }, + "display_fields": null + } + }, + { + "field_key": "0x2ce2e7bbb14b9b8d582aaf87c705f424e63373a1bb602deed89e14c95615e4b6", + "state": { + "id": "0xae308aa50bded0b341120431a9049ad1f151b345115a17bcefcad943505fa3b62ce2e7bbb14b9b8d582aaf87c705f424e63373a1bb602deed89e14c95615e4b6", + "owner": "rooch1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhxqaen", + "owner_bitcoin_address": null, + "flag": 0, + "state_root": "0x5350415253455f4d45524b4c455f504c414345484f4c4445525f484153480000", + "size": "0", + "created_at": "1732199736531", + "updated_at": "1732199736531", + "object_type": "0x2::object::DynamicField>", + "value": "0x0400000000000000012b3d3dc14e62060099e49a8c522b5698219795bf6a87d42624cc69eaf6263627", + "decoded_value": { + "abilities": 12, + "type": "0x2::object::DynamicField>", + "value": { + "name": "4", + "value": { + "abilities": 12, + "type": "0x2::object::Object<0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e8::pond::PondState>", + "value": { + "id": "0x2b3d3dc14e62060099e49a8c522b5698219795bf6a87d42624cc69eaf6263627" + } + } + } + }, + "display_fields": null + } + }, + { + "field_key": "0x3bc998c5c75500958ca88ca47f5b654ff2fa97da0c1539b6b4c4c8722975a80f", + "state": { + "id": "0xae308aa50bded0b341120431a9049ad1f151b345115a17bcefcad943505fa3b63bc998c5c75500958ca88ca47f5b654ff2fa97da0c1539b6b4c4c8722975a80f", + "owner": "rooch1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhxqaen", + "owner_bitcoin_address": null, + "flag": 0, + "state_root": "0x5350415253455f4d45524b4c455f504c414345484f4c4445525f484153480000", + "size": "0", + "created_at": "1732199736531", + "updated_at": "1732199736531", + "object_type": "0x2::object::DynamicField>", + "value": "0x060000000000000001855d118b9da7ac1f32922cf568f51d9df1e28cd0205d9f241ba0007c22fea5bb", + "decoded_value": { + "abilities": 12, + "type": "0x2::object::DynamicField>", + "value": { + "name": "6", + "value": { + "abilities": 12, + "type": "0x2::object::Object<0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e8::pond::PondState>", + "value": { + "id": "0x855d118b9da7ac1f32922cf568f51d9df1e28cd0205d9f241ba0007c22fea5bb" + } + } + } + }, + "display_fields": null + } + }, + { + "field_key": "0x5a473312d437bbf8c2690476f7f1df5040ff1f2398d19d7c039858dd3423bbbc", + "state": { + "id": "0xae308aa50bded0b341120431a9049ad1f151b345115a17bcefcad943505fa3b65a473312d437bbf8c2690476f7f1df5040ff1f2398d19d7c039858dd3423bbbc", + "owner": "rooch1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhxqaen", + "owner_bitcoin_address": null, + "flag": 0, + "state_root": "0x5350415253455f4d45524b4c455f504c414345484f4c4445525f484153480000", + "size": "0", + "created_at": "1732199736531", + "updated_at": "1732199736531", + "object_type": "0x2::object::DynamicField>", + "value": "0x0500000000000000017fe629442f293a0307c7cc6ae12edc0c60c4a7708c75a83b5f671ff03544e781", + "decoded_value": { + "abilities": 12, + "type": "0x2::object::DynamicField>", + "value": { + "name": "5", + "value": { + "abilities": 12, + "type": "0x2::object::Object<0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e8::pond::PondState>", + "value": { + "id": "0x7fe629442f293a0307c7cc6ae12edc0c60c4a7708c75a83b5f671ff03544e781" + } + } + } + }, + "display_fields": null + } + }, + { + "field_key": "0x7eb4036673c8611e43c3eff1202446612f22a4b3bac92b7e14c0562ade5f1a3f", + "state": { + "id": "0xae308aa50bded0b341120431a9049ad1f151b345115a17bcefcad943505fa3b67eb4036673c8611e43c3eff1202446612f22a4b3bac92b7e14c0562ade5f1a3f", + "owner": "rooch1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhxqaen", + "owner_bitcoin_address": null, + "flag": 0, + "state_root": "0x5350415253455f4d45524b4c455f504c414345484f4c4445525f484153480000", + "size": "0", + "created_at": "1732199736531", + "updated_at": "1732199736531", + "object_type": "0x2::object::DynamicField>", + "value": "0x0100000000000000011fd948251d394db82f9d4a1e82118cf232c0f4f9c2c7ffa45ea9dffad28e9412", + "decoded_value": { + "abilities": 12, + "type": "0x2::object::DynamicField>", + "value": { + "name": "1", + "value": { + "abilities": 12, + "type": "0x2::object::Object<0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e8::pond::PondState>", + "value": { + "id": "0x1fd948251d394db82f9d4a1e82118cf232c0f4f9c2c7ffa45ea9dffad28e9412" + } + } + } + }, + "display_fields": null + } + }, + { + "field_key": "0xf30b43fa7dbdf31380c5b3efb2e960569b6caa30e6ee302358fbd43a605a5dbc", + "state": { + "id": "0xae308aa50bded0b341120431a9049ad1f151b345115a17bcefcad943505fa3b6f30b43fa7dbdf31380c5b3efb2e960569b6caa30e6ee302358fbd43a605a5dbc", + "owner": "rooch1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhxqaen", + "owner_bitcoin_address": null, + "flag": 0, + "state_root": "0x5350415253455f4d45524b4c455f504c414345484f4c4445525f484153480000", + "size": "0", + "created_at": "1732199736531", + "updated_at": "1732199736531", + "object_type": "0x2::object::DynamicField>", + "value": "0x03000000000000000114b8edb38062592b47b6898e83c7b0df8f1d4439c0fb9d6728435ef870516b15", + "decoded_value": { + "abilities": 12, + "type": "0x2::object::DynamicField>", + "value": { + "name": "3", + "value": { + "abilities": 12, + "type": "0x2::object::Object<0xb38a327121ab8e9091a04377ec1e9af9ab4b801dbfb368f20fb0c080c763f7e8::pond::PondState>", + "value": { + "id": "0x14b8edb38062592b47b6898e83c7b0df8f1d4439c0fb9d6728435ef870516b15" + } + } + } + }, + "display_fields": null + } + } + ], + "next_cursor": "0xf30b43fa7dbdf31380c5b3efb2e960569b6caa30e6ee302358fbd43a605a5dbc", + "has_next_page": false +} +*/ \ No newline at end of file diff --git a/examples/rooch_fish/web/src/constants/index.ts b/examples/rooch_fish/web/src/constants/index.ts new file mode 100644 index 0000000000..288b183f17 --- /dev/null +++ b/examples/rooch_fish/web/src/constants/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +export * from "./roochMutationKeys" \ No newline at end of file diff --git a/examples/rooch_fish/web/src/constants/roochMutationKeys.ts b/examples/rooch_fish/web/src/constants/roochMutationKeys.ts new file mode 100644 index 0000000000..18f08e4644 --- /dev/null +++ b/examples/rooch_fish/web/src/constants/roochMutationKeys.ts @@ -0,0 +1,21 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +import { MutationKey } from '@tanstack/react-query' + +function formMutationKeyFn(baseEntity: string) { + return function mutationKeyFn(additionalKeys: MutationKey = []) { + return [{ ...roochMutationKeys.all, baseEntity }, ...additionalKeys] + } +} + +export const roochMutationKeys = { + all: { baseScope: 'rooch' }, + addNetwork: formMutationKeyFn('add-network'), + switchNetwork: formMutationKeyFn('switch-network'), + removeNetwork: formMutationKeyFn('remove-network'), + removeSession: formMutationKeyFn('remove-session'), + transferObject: formMutationKeyFn('transfer-object'), + transferCoin: formMutationKeyFn('transfer-coin'), + signAndExecuteTransaction: formMutationKeyFn('sign-and-execute-transaction'), +} diff --git a/examples/rooch_fish/web/src/hooks/useFishController.ts b/examples/rooch_fish/web/src/hooks/useFishController.ts new file mode 100644 index 0000000000..ae32c661e0 --- /dev/null +++ b/examples/rooch_fish/web/src/hooks/useFishController.ts @@ -0,0 +1,163 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +import { config } from "../config/index"; +import { useState, useEffect, useRef } from 'react'; +import { Args, Transaction } from "@roochnetwork/rooch-sdk"; +import { useSnackbar } from 'notistack'; +import { useSignAndExecuteTransaction } from "./useSignAndExecuteTransaction"; + +interface FishState { + x: number; + y: number; + rotation: number; + velocity: { + x: number; + y: number; + }; +} + +interface BoundaryProps { + minX: number; + maxX: number; + minY: number; + maxY: number; +} + +export const useFishController = (pondID:number, fishID: number, initialX: number, initialY: number, boundaries: BoundaryProps) => { + const [fishState, setFishState] = useState({ + x: initialX, + y: initialY, + rotation: 0, + velocity: { x: 0, y: 0 } + }); + + const isMoving = useRef(false); + const { enqueueSnackbar } = useSnackbar(); + + const { mutateAsync: signAndExecuteTransaction } = + useSignAndExecuteTransaction(); + + const handleFishMove = async (direction: number) => { + if (isMoving.current) { + return; + } + + isMoving.current = true; + console.log("move fish start, with direction:", direction, "fish_id:", fishID) + + setFishState(prev => ({ ...prev, error: undefined })); + + try { + const txn = new Transaction(); + txn.callFunction({ + address: config.roochFishAddress, + module: "rooch_fish", + function: "move_fish", + args: [ + Args.objectId(config.gameStateObjectID), + Args.u64(BigInt(pondID)), + Args.u64(BigInt(fishID)), + Args.u8(direction), + ], + }); + + (txn as any).data.maxGas = BigInt(50000000 * 10) + + const tx = await signAndExecuteTransaction({ transaction: txn }); + if (tx?.output?.status?.type != 'executed') { + const errorMsg = `Move failed: ${tx?.output?.status?.type}`; + console.error("move fish fail:", tx?.output?.status); + enqueueSnackbar(errorMsg, { + variant: "warning" + }); + return; + } + + console.log("move fish success, tx:", tx, "tx_order:", tx.sequence_info.tx_order); + } catch (error) { + console.error("move fish error:", error); + + if (String(error).includes("1004")) { + enqueueSnackbar("Insufficient gas, please claim gas first", { + variant: "warning" + }); + } else { + enqueueSnackbar(String(error), { + variant: "warning" + }); + } + } finally { + isMoving.current = false; + } + }; + + const speed = 5; + + useEffect(() => { + const keys = new Set(); + + const handleKeyDown = (e: KeyboardEvent) => { + keys.add(e.key); + }; + + const handleKeyUp = (e: KeyboardEvent) => { + keys.delete(e.key); + }; + + const updatePosition = () => { + setFishState(prev => { + let dx = 0; + let dy = 0; + + if (keys.has('ArrowLeft') || keys.has('a')) { + dx -= speed; + handleFishMove(3); + } + + if (keys.has('ArrowRight') || keys.has('d')) { + dx += speed; + handleFishMove(1); + } + + if (keys.has('ArrowUp') || keys.has('w')) { + dy -= speed; + handleFishMove(2); + } + + if (keys.has('ArrowDown') || keys.has('s')) { + dy += speed; + handleFishMove(0); + } + + let newX = Math.max(boundaries.minX, Math.min(boundaries.maxX, prev.x + dx)); + let newY = Math.max(boundaries.minY, Math.min(boundaries.maxY, prev.y + dy)); + + let newRotation = prev.rotation; + if (dx !== 0 || dy !== 0) { + newRotation = Math.atan2(dy, dx); + } + + return { + x: newX, + y: newY, + rotation: newRotation, + velocity: { x: dx, y: dy } + }; + }); + }; + + const gameLoop = setInterval(updatePosition, 30); + + window.addEventListener('keydown', handleKeyDown); + window.addEventListener('keyup', handleKeyUp); + + return () => { + clearInterval(gameLoop); + window.removeEventListener('keydown', handleKeyDown); + window.removeEventListener('keyup', handleKeyUp); + }; + }, [pondID, fishID, boundaries, speed]); + + return fishState; +}; diff --git a/examples/rooch_fish/web/src/hooks/useGameState.ts b/examples/rooch_fish/web/src/hooks/useGameState.ts new file mode 100644 index 0000000000..eee51be846 --- /dev/null +++ b/examples/rooch_fish/web/src/hooks/useGameState.ts @@ -0,0 +1,55 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +import { config } from "../config/index"; +import { useRoochClientQuery } from "@roochnetwork/rooch-sdk-kit"; +import { useQuery } from "@tanstack/react-query"; +import { useRoochClient } from "@roochnetwork/rooch-sdk-kit"; +import { listFieldStates } from "../utils/index" + +const extractObjectIds = (data: any[]) => { + const items = data.map(item => { + if(!item?.state?.decoded_value?.value?.value?.value?.id) { + console.log("Invalid item structure:", item); + return null; + } + + const name = item.state.decoded_value.value.name + const id = item.state.decoded_value.value.value.value.id; + return { + name, + id, + }; + }).filter(Boolean); // Remove null values + + return items; +}; + +export function useGameState() { + const client = useRoochClient(); + const { data: gameState, refetch: roochFishRefetch } = useRoochClientQuery( + "getStates", + { + accessPath: `/object/${config.gameStateObjectID}`, + stateOption: { + decode: true, + }, + }, + { refetchInterval: 3000 } + ); + + const pondHandleId = (gameState as any)?.[0]?.decoded_value?.value?.ponds?.value?.handle?.value?.id; + + const { data: pondsData } = useQuery({ + queryKey: ["listFieldStates", pondHandleId], + queryFn: async () => pondHandleId ? listFieldStates(client, pondHandleId) : null, + enabled: !!pondHandleId, + }); + + const finalGameState = gameState ? { + ...gameState, + ponds: extractObjectIds(pondsData ? pondsData.result : []) + } : null; + + return { data: finalGameState }; +} \ No newline at end of file diff --git a/examples/rooch_fish/web/src/hooks/useLatestTransaction.ts b/examples/rooch_fish/web/src/hooks/useLatestTransaction.ts new file mode 100644 index 0000000000..dd2879308e --- /dev/null +++ b/examples/rooch_fish/web/src/hooks/useLatestTransaction.ts @@ -0,0 +1,16 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +import { useRoochClient } from "@roochnetwork/rooch-sdk-kit"; +import { useQuery } from "@tanstack/react-query"; +import { getTransactionsByOrder } from "../utils/rooch_client" + +export function useLatestTransaction() { + const client = useRoochClient(); + const { data } = useQuery({ + queryKey: ["rooch_latest_tx"], + queryFn: async () => getTransactionsByOrder(client, null, 1, true), + }); + + return { tx: data?.result }; +} diff --git a/examples/rooch_fish/web/src/hooks/usePlayerState.ts b/examples/rooch_fish/web/src/hooks/usePlayerState.ts new file mode 100644 index 0000000000..0054f9c7f6 --- /dev/null +++ b/examples/rooch_fish/web/src/hooks/usePlayerState.ts @@ -0,0 +1,34 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +import { config } from "../config/index"; +import { useRoochClient, useCurrentAddress } from "@roochnetwork/rooch-sdk-kit"; +import { RoochClient, Args } from "@roochnetwork/rooch-sdk"; +import { useQuery } from "@tanstack/react-query"; + +const queryUserFishs = async (client: RoochClient, pondID: number, playerAddress: any) => { + const data = await client.executeViewFunction({ + target: `${config.roochFishAddress}::rooch_fish::get_pond_player_fish_ids`, + args: [ + Args.objectId(config.gameStateObjectID), + Args.u64(BigInt(pondID)), + Args.address(playerAddress.roochAddress) + ], + }); + + return { data }; +}; + +export function usePlayerState(pondID: number) { + const currentAddress = useCurrentAddress(); + const client = useRoochClient(); + + const { data } = useQuery({ + queryKey: ["query_user_fishs"], + queryFn: async () => queryUserFishs(client, pondID, currentAddress), + refetchInterval: 5000 + }); + + const fish_ids = (data?.data?.return_values?.[0]?.decoded_value ?? null) as Array; + return { fish_ids }; +} diff --git a/examples/rooch_fish/web/src/hooks/usePondState.ts b/examples/rooch_fish/web/src/hooks/usePondState.ts new file mode 100644 index 0000000000..e1b114dde4 --- /dev/null +++ b/examples/rooch_fish/web/src/hooks/usePondState.ts @@ -0,0 +1,150 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +import { config, PondID } from "../config/index"; +import { transformObject } from "../utils/rooch_object"; +import { bcs } from "@roochnetwork/rooch-sdk"; +import { useRoochClient } from "@roochnetwork/rooch-sdk-kit"; +import { useRoochState } from "./useRoochStates" +import { useRoochFieldStates } from "./useRoochFieldStates" +import { useRoochWSFieldStates } from "./useRoochWSFieldStates" + +const Fish = bcs.struct('Fish', { + id: bcs.u64(), + owner: bcs.Address, + size: bcs.u64(), + x: bcs.u64(), + y: bcs.u64(), +}); + +const Food = bcs.struct('Food', { + id: bcs.u64(), + owner: bcs.Address, + size: bcs.u64(), + x: bcs.u64(), + y: bcs.u64(), +}); + +const FishDynamicField = bcs.struct('DynamicField', { + name: bcs.u64(), + value: Fish, +}); + +const FoodDynamicField = bcs.struct('DynamicField', { + name: bcs.u64(), + value: Food, +}); + +interface FishData { + id: number; + owner: string; + size: number; + x: number; + y: number; +} + +interface FoodData { + id: number; + size: number; + x: number; + y: number; +} + +interface PondStateData { + width: number, + height: number, + fishes: { + handle: { + id: string; + }; + }; + foods: { + handle: { + id: string; + }; + }; + + exit_zones: any, +} + +interface DelayRecord { + timestamp: number; + delay: number; +} + +interface PondStateReturn { + data: PondStateData | null; + fishData: FishData[]; + foodData: FoodData[]; + getRecentDelays: any; +} + +export function usePondState(pondID: PondID): PondStateReturn { + const client = useRoochClient(); + + const { data, txOrder, refetch: roochFishRefetch } = useRoochState( + config.ponds[pondID], + { + refetchInterval: 60000, + } + ); + + const pondData = transformObject(data?data[0]:null) + + const fishTableHandleId = pondData?.fishes?.handle?.id; + const foodTableHandleId = pondData?.foods?.handle?.id; + + const { fields: fishData, getRecentDelays } = useRoochWSFieldStates(fishTableHandleId, FishDynamicField, { + refetchInterval: 10000, + diffInterval: 40, + }); + + const { fields: foodData } = useRoochFieldStates(foodTableHandleId, FoodDynamicField, { + refetchInterval: 5000, + }); + + const finalPondState = pondData ? { + ...pondData, + } : null; + + const finalFishData = transformFish(fishData); + const finalFoodData = transformFood(foodData); + + return { + data: finalPondState, + fishData: finalFishData, + foodData: finalFoodData, + getRecentDelays + }; +} + +function transformFish(fishData: Map): Array { + if (!fishData || !(fishData instanceof Map)) { + return []; + } + + return Array.from(fishData.values()).map(field => { + return { + id: field.value.id, + owner: field.value.owner, + size: field.value.size, + x: field.value.x, + y: field.value.y + }; + }); +} + +function transformFood(foodData: Map): Array { + if (!foodData || !(foodData instanceof Map)) { + return []; + } + + return Array.from(foodData.values()).map(field => { + return { + id: field.value.id, + size: field.value.size, + x: field.value.x, + y: field.value.y + }; + }); +} diff --git a/examples/rooch_fish/web/src/hooks/useRccOwner.ts b/examples/rooch_fish/web/src/hooks/useRccOwner.ts new file mode 100644 index 0000000000..931f336b28 --- /dev/null +++ b/examples/rooch_fish/web/src/hooks/useRccOwner.ts @@ -0,0 +1,41 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +import { useRoochClient } from "@roochnetwork/rooch-sdk-kit"; +import { rccCoinStoreType } from "../App"; +import { IndexerObjectStateView, RoochClient } from "@roochnetwork/rooch-sdk"; +import { useQuery } from "@tanstack/react-query"; + +const fetchAllOwner = async (client: RoochClient) => { + let result: IndexerObjectStateView[] = []; + let cursor = null; + // eslint-disable-next-line no-constant-condition + while (true) { + const data = await client.queryObjectStates({ + filter: { + object_type: rccCoinStoreType, + }, + queryOption: { + decode: true, + descending: false, + }, + cursor, + }); + + cursor = data.next_cursor ?? null; + result = result.concat(data.data); + if (!data.has_next_page) { + break; + } + } + return { result }; +}; + +export function useRccOwner() { + const client = useRoochClient(); + const { data } = useQuery({ + queryKey: ["rcc_owner_list"], + queryFn: async () => fetchAllOwner(client), + }); + return { rccOwnerList: data?.result }; +} diff --git a/examples/rooch_fish/web/src/hooks/useRoochFieldStates.ts b/examples/rooch_fish/web/src/hooks/useRoochFieldStates.ts new file mode 100644 index 0000000000..ae4702a1e9 --- /dev/null +++ b/examples/rooch_fish/web/src/hooks/useRoochFieldStates.ts @@ -0,0 +1,57 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +import { Buffer } from 'buffer'; +import { useState, useEffect, useRef } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { listFieldStates } from "../utils/index"; +import { useRoochClient } from "@roochnetwork/rooch-sdk-kit"; +import { BcsType } from "@roochnetwork/rooch-sdk"; + +export function useRoochFieldStates( + objectID: string, + fieldBcsType: BcsType, + opts: { refetchInterval: number } +) { + const client = useRoochClient(); + const [fields, setFields] = useState>(new Map); + + const { data: fieldStats } = useQuery({ + queryKey: ["listFieldStates"], + queryFn: async () => listFieldStates(client, objectID), + enabled: !!objectID, + refetchInterval: opts.refetchInterval, + }); + + const deserializeFieldState = (hexValue: string) => { + try { + const cleanHexValue = hexValue.startsWith('0x') ? hexValue.slice(2) : hexValue; + const buffer = Buffer.from(cleanHexValue, "hex"); + return fieldBcsType.parse(buffer); + } catch (error) { + console.error('BCS deserialization error:', error); + return null; + } + }; + + useEffect(() => { + if (fieldStats?.result) { + const newFields = new Map(); + + for (const item of fieldStats.result) { + if (item.state?.value) { + const deserializedValue = deserializeFieldState(item.state.value); + if (deserializedValue) { + newFields.set(item.field_key, deserializedValue); + } + } + } + + setFields(newFields); + } + }, [fieldStats]); + + return { + fields: fields, + }; +} diff --git a/examples/rooch_fish/web/src/hooks/useRoochStates.ts b/examples/rooch_fish/web/src/hooks/useRoochStates.ts new file mode 100644 index 0000000000..3f05fff8f4 --- /dev/null +++ b/examples/rooch_fish/web/src/hooks/useRoochStates.ts @@ -0,0 +1,45 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +import { useRoochClientQuery } from "@roochnetwork/rooch-sdk-kit"; +import { useQuery } from "@tanstack/react-query"; +import { useRoochClient } from "@roochnetwork/rooch-sdk-kit"; +import { getLatestTransaction } from "../utils/rooch_client" + +export function useRoochState(objectID: string, opts: any) { + const client = useRoochClient(); + + const { data: latestTxData } = useQuery({ + queryKey: ["rooch_latest_tx_for_use_rooch_states"], + queryFn: async () => getLatestTransaction(client), + enabled: !!objectID, + refetchInterval: opts.refetchInterval, + }); + + //console.log("useRoochState latestTxData:", latestTxData) + + const stateRoot = latestTxData?.execution_info?.state_root; + const txOrder = latestTxData?.transaction?.sequence_info.tx_order; + + const { data, refetch: refetch } = useRoochClientQuery( + "getStates", + { + accessPath: `/object/${objectID}`, + stateOption: { + decode: true, + stateRoot: stateRoot + } as any, + }, + { + enabled: !!stateRoot, + refetchInterval: opts.refetchInterval, + } + ); + + return { + data: data, + stateRoot: stateRoot, + txOrder: txOrder, + refetch: refetch, + }; +} diff --git a/examples/rooch_fish/web/src/hooks/useRoochWSClient.ts b/examples/rooch_fish/web/src/hooks/useRoochWSClient.ts new file mode 100644 index 0000000000..24f20f7a56 --- /dev/null +++ b/examples/rooch_fish/web/src/hooks/useRoochWSClient.ts @@ -0,0 +1,17 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +import { useMemo } from 'react' +import { DEFAULT_CREATE_WS_CLIENT } from '../clients/rooch/wsClient' +import { useRoochContext } from "@roochnetwork/rooch-sdk-kit"; + +export function useRoochWSClient() { + const { network: currentNetwork, networks } = useRoochContext() + + const client = useMemo(() => { + console.log("create rooch ws client") + return DEFAULT_CREATE_WS_CLIENT(currentNetwork, networks[currentNetwork]) + }, [currentNetwork, networks]) + + return client +} diff --git a/examples/rooch_fish/web/src/hooks/useRoochWSFieldStates.ts b/examples/rooch_fish/web/src/hooks/useRoochWSFieldStates.ts new file mode 100644 index 0000000000..6f53b3ba22 --- /dev/null +++ b/examples/rooch_fish/web/src/hooks/useRoochWSFieldStates.ts @@ -0,0 +1,203 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +import { Buffer } from 'buffer'; +import { useState, useEffect, useRef } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { getLatestTransaction } from "../utils/rooch_client"; +import { listFieldStates, syncStates } from "../utils/index"; +import { useRoochWSClient } from "./useRoochWSClient"; +import { BcsType } from "@roochnetwork/rooch-sdk"; +import { useTransactionDelay } from './useTransactionDelay'; + +interface RoochWSFieldStatesResult { + fields: Map; + stateRoot: string | undefined; + isLoading: boolean; + startTracking: (txOrder: string) => void; + recordTxConfirm: (tempId: string, txOrder: string) => void; + recordStateSync: (txOrder: string) => void; + getRecentDelays: () => any; +} + +export function useRoochWSFieldStates( + objectID: string, + fieldBcsType: BcsType, + opts: { + refetchInterval: number, + diffInterval: number + } +): RoochWSFieldStatesResult { + const client = useRoochWSClient(); + const [fields, setFields] = useState>(new Map); + const previousStateRootRef = useRef(); + const lastValidFieldsRef = useRef>(new Map()); + const isFetchingRef = useRef(false); + const processedTxOrdersRef = useRef>(new Set()); + + const { + startTracking, + recordTxConfirm, + recordStateSync, + getRecentDelays + } = useTransactionDelay(); + + const { data: latestTxData } = useQuery({ + queryKey: ["rooch_latest_tx_for_use_rooch_ws_field_states"], + queryFn: async () => getLatestTransaction(client), + enabled: !!objectID, + refetchInterval: opts.refetchInterval, + staleTime: opts.refetchInterval, + }); + + const stateRoot = latestTxData?.execution_info?.state_root; + const txOrder = latestTxData?.transaction?.sequence_info.tx_order; + const cursorRef = useRef(txOrder); + + const deserializeFieldState = (hexValue: string) => { + try { + const cleanHexValue = hexValue.startsWith('0x') ? hexValue.slice(2) : hexValue; + const buffer = Buffer.from(cleanHexValue, "hex"); + return fieldBcsType.parse(buffer); + } catch (error) { + console.error('BCS deserialization error:', error); + return null; + } + }; + + const updateFields = (newFields: Map, isFullData: boolean = false) => { + if (isFullData) { + // For full data fetch, always update regardless of size + lastValidFieldsRef.current = newFields; + setFields(newFields); + } else if (newFields.size > 0) { + // For diff updates, only update if there are changes + lastValidFieldsRef.current = newFields; + setFields(newFields); + } else { + setFields(lastValidFieldsRef.current); + } + }; + + // Add stateRoot validation to prevent unnecessary resets + useEffect(() => { + if (!stateRoot) return; + + // Only trigger full data fetch if stateRoot changed + if (previousStateRootRef.current && previousStateRootRef.current !== stateRoot) { + isFetchingRef.current = true; + fetchFullData().finally(() => { + isFetchingRef.current = false; + }); + } + previousStateRootRef.current = stateRoot; + }, [stateRoot]); + + const fetchFullData = async () => { + if (!objectID || !stateRoot || !client) return; + + try { + const fieldStats = await listFieldStates(client, objectID, stateRoot); + + if (fieldStats?.result) { + const newFields = new Map(); + + for (const item of fieldStats.result) { + if (item.state?.value) { + const deserializedValue = deserializeFieldState(item.state.value); + if (deserializedValue) { + newFields.set(item.field_key, deserializedValue); + } + } + } + + updateFields(newFields, true); + } + } catch (error) { + console.error("Error fetching field states:", error); + } + }; + + const fetchDiffData = async () => { + if (!objectID || !cursorRef.current || !client || isFetchingRef.current) return; + + try { + const fieldStats = await syncStates(client, objectID, cursorRef.current); + + for (const changeSet of fieldStats.result) { + if (BigInt(changeSet.tx_order) > BigInt(cursorRef.current)) { + cursorRef.current = (BigInt(changeSet.tx_order) + BigInt(1)).toString(); + } + + if (changeSet.state_change_set.changes.length === 0) continue; + + const newFields = new Map(lastValidFieldsRef.current); + + for (const change of changeSet.state_change_set.changes) { + for (const field of change.fields) { + const fieldKey = "0x" + field.metadata.id.slice(-64); + + if (field.value?.new || field.value?.modify) { + const value = field.value.new || field.value.modify; + const deserializedValue = deserializeFieldState(value); + if (deserializedValue) { + newFields.set(fieldKey, deserializedValue); + } + } else if (field.value?.delete) { + newFields.delete(fieldKey); + } + } + } + + updateFields(newFields); + + // Record state sync timing + if (txOrder && !processedTxOrdersRef.current.has(txOrder)) { + recordStateSync(txOrder); + processedTxOrdersRef.current.add(txOrder); + + // Clean up old processed tx orders (keep last 100) + if (processedTxOrdersRef.current.size > 100) { + const orders = Array.from(processedTxOrdersRef.current); + orders.slice(0, orders.length - 100).forEach(order => { + processedTxOrdersRef.current.delete(order); + }); + } + } + } + } catch (error) { + console.error("Error fetching diff states:", error); + } + }; + + useEffect(() => { + if (!objectID || !stateRoot || !txOrder) { + return; + } + + cursorRef.current = txOrder; + + // Initial fetch + if (!isFetchingRef.current) { + fetchFullData(); + } + + const intervalId = setInterval(fetchDiffData, opts.diffInterval); + + return () => { + clearInterval(intervalId); + }; + }, [stateRoot, txOrder, objectID, fieldBcsType, client, opts.diffInterval]); + + return { + fields: fields.size > 0 ? fields : lastValidFieldsRef.current, + stateRoot, + isLoading: isFetchingRef.current, + + // Export delay tracking functions + startTracking, + recordTxConfirm, + recordStateSync, + getRecentDelays, + }; +} diff --git a/examples/rooch_fish/web/src/hooks/useSeqNumber.ts b/examples/rooch_fish/web/src/hooks/useSeqNumber.ts new file mode 100644 index 0000000000..e8a512ce3d --- /dev/null +++ b/examples/rooch_fish/web/src/hooks/useSeqNumber.ts @@ -0,0 +1,58 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useCallback, useEffect, useRef } from "react"; +import { useRoochWSClient } from "./useRoochWSClient"; + +export function useSeqNumber(address: string) { + const client = useRoochWSClient(); + const queryClient = useQueryClient(); + const timerRef = useRef(); + + const queryKey = ["rooch_seq_number", address]; + + const { data: seqNumber } = useQuery({ + queryKey, + queryFn: async () => { + return client.getSequenceNumber(address); + }, + refetchOnWindowFocus: false, + }); + + const syncSeqNumber = useCallback(async () => { + const chainSeq = await client.getSequenceNumber(address); + console.log("syncSeqNumber:", chainSeq) + queryClient.setQueryData(queryKey, chainSeq); + }, [address, client, queryClient, queryKey]); + + const incrementLocal = useCallback(() => { + + + queryClient.setQueryData(queryKey, (old: bigint | undefined) => { + if (old === undefined) return 0n; + + console.log("incrementLocal:", old + 1n) + return old + 1n; + }); + + if (timerRef.current) { + clearInterval(timerRef.current); + } + + timerRef.current = setInterval(syncSeqNumber, 5000); + }, [queryClient, queryKey, syncSeqNumber]); + + useEffect(() => { + return () => { + if (timerRef.current) { + clearInterval(timerRef.current); + } + }; + }, []); + + return { + seqNumber: seqNumber ?? 0n, + incrementLocal, + }; +} diff --git a/examples/rooch_fish/web/src/hooks/useSignAndExecuteTransaction.ts b/examples/rooch_fish/web/src/hooks/useSignAndExecuteTransaction.ts new file mode 100644 index 0000000000..133326e998 --- /dev/null +++ b/examples/rooch_fish/web/src/hooks/useSignAndExecuteTransaction.ts @@ -0,0 +1,96 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +import { roochMutationKeys } from '../constants' +import { UseMutationOptions, UseMutationResult, useMutation } from '@tanstack/react-query' +import { Signer, Transaction, ExecuteTransactionResponseView } from '@roochnetwork/rooch-sdk' +import { useCurrentSession } from '@roochnetwork/rooch-sdk-kit' +import { useRoochWSClient } from "./useRoochWSClient" +import { signAndExecuteTransactionX } from "../utils/index" +import { useSeqNumber } from './useSeqNumber' +import { useTransactionDelay } from './useTransactionDelay' +import { useRef, useMemo } from 'react' + +type UseSignAndExecuteTransactionArgs = { + transaction: Transaction + signer?: Signer +} + +type UsesignAndExecuteTransactionResult = ExecuteTransactionResponseView + +type UsesignAndExecuteTransactionOptions = Omit< + UseMutationOptions< + UsesignAndExecuteTransactionResult, + Error, + UseSignAndExecuteTransactionArgs, + unknown + >, + 'mutationFn' +> + +export function useSignAndExecuteTransaction({ + mutationKey, + ...mutationOptions +}: UsesignAndExecuteTransactionOptions = {}): UseMutationResult< + UsesignAndExecuteTransactionResult, + Error, + UseSignAndExecuteTransactionArgs, + unknown +> & { + getRecentDelays: () => any[] + recordStateSync: (txOrder: string) => void +} { + const client = useRoochWSClient() + const session = useCurrentSession() + const lastTempIdRef = useRef() + + const sender = useMemo(() => { + if (session) { + return session.getRoochAddress().toHexAddress() + } + return '' + }, [session]) + + const { seqNumber, incrementLocal } = useSeqNumber(sender) + const { startTracking, recordTxConfirm, recordStateSync, getRecentDelays } = useTransactionDelay() + + const mutation = useMutation({ + mutationKey: roochMutationKeys.signAndExecuteTransaction(mutationKey), + mutationFn: async (args: any) => { + if (!session) { + throw Error('Create a session first') + } + + const tempId = startTracking() + lastTempIdRef.current = tempId + + const actualSigner = args.signer || session + const result = await signAndExecuteTransactionX({ + client: client, + transaction: args.transaction, + seqNumber: Number(seqNumber), + signer: actualSigner, + }) + + console.log("result.execution_info.status:", result.execution_info.status); + + if (result.execution_info.status.type !== 'executed') { + throw Error('transfer failed' + result.execution_info.status.type) + } + + if (result.sequence_info) { + recordTxConfirm(tempId, result.sequence_info.tx_order) + } + + incrementLocal() + return result + }, + ...mutationOptions, + }) + + return { + ...mutation, + getRecentDelays, + recordStateSync, + } +} \ No newline at end of file diff --git a/examples/rooch_fish/web/src/hooks/useTransactionDelay.ts b/examples/rooch_fish/web/src/hooks/useTransactionDelay.ts new file mode 100644 index 0000000000..f746604e08 --- /dev/null +++ b/examples/rooch_fish/web/src/hooks/useTransactionDelay.ts @@ -0,0 +1,82 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +import { useRef, useCallback } from 'react'; + +interface DelayRecord { + txOrder: string; + sendTime: number; + confirmTime: number; + syncTime: number; + totalDelay: number; + syncDelay: number; +} + +interface PendingTx { + tempId: string; + sendTime: number; + txOrder?: string; + confirmTime?: number; +} + +// Move these to module level +const delayRecords: DelayRecord[] = []; +const pendingTxs: Map = new Map(); +const txOrderMap: Map = new Map(); + +export function useTransactionDelay() { + const startTracking = useCallback(() => { + const tempId = crypto.randomUUID(); + pendingTxs.set(tempId, { + tempId, + sendTime: Date.now(), + }); + return tempId; + }, []); + + const recordTxConfirm = useCallback((tempId: string, txOrder: string) => { + const pending = pendingTxs.get(tempId); + if (pending) { + pending.txOrder = txOrder; + pending.confirmTime = Date.now(); + txOrderMap.set(txOrder, tempId); + } + }, []); + + const recordStateSync = useCallback((txOrder: string) => { + const tempId = txOrderMap.get(txOrder); + if (!tempId) return; + + const pending = pendingTxs.get(tempId); + if (!pending || !pending.confirmTime) return; + + const now = Date.now(); + const record: DelayRecord = { + txOrder, + sendTime: pending.sendTime, + confirmTime: pending.confirmTime, + syncTime: now, + totalDelay: now - pending.sendTime, + syncDelay: now - pending.confirmTime, + }; + + delayRecords.unshift(record); + if (delayRecords.length > 5) { + delayRecords.pop(); + } + + pendingTxs.delete(tempId); + txOrderMap.delete(txOrder); + }, []); + + const getRecentDelays = useCallback(() => { + return delayRecords; + }, []); + + return { + startTracking, + recordTxConfirm, + recordStateSync, + getRecentDelays, + }; +} \ No newline at end of file diff --git a/examples/rooch_fish/web/src/index.css b/examples/rooch_fish/web/src/index.css new file mode 100644 index 0000000000..fe9904c486 --- /dev/null +++ b/examples/rooch_fish/web/src/index.css @@ -0,0 +1,26 @@ +/* Copyright (c) RoochNetwork */ +/* SPDX-License-Identifier: Apache-2.0 */ +/* Author: Jason Jo */ + +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + font-family: 'Raleway Variable', var(--font-sans), ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: #0F172A; + background-color: #ffffff; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + background: linear-gradient(180deg, hsla(0, 0%, 100%, 0) 0, #fff 100vh), fixed 0 0 /20px 20px radial-gradient(#d1d1d1 1px, transparent 0), fixed 10px 10px /20px 20px radial-gradient(#d1d1d1 1px, transparent 0) +} \ No newline at end of file diff --git a/examples/rooch_fish/web/src/main.tsx b/examples/rooch_fish/web/src/main.tsx new file mode 100644 index 0000000000..690f3463d4 --- /dev/null +++ b/examples/rooch_fish/web/src/main.tsx @@ -0,0 +1,59 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 +// Author: Jason Jo + +import "@fontsource-variable/plus-jakarta-sans"; +import "@fontsource-variable/raleway"; +import { ThemeProvider, styled } from "@mui/material"; +import { RoochProvider, WalletProvider } from "@roochnetwork/rooch-sdk-kit"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { MaterialDesignContent, SnackbarProvider } from "notistack"; +import ReactDOM from "react-dom/client"; +import TagManager from "react-gtm-module"; +import App from "./App.tsx"; +import "./index.css"; + +import { config } from "./config/index" +import { networkConfig } from "./networks.ts"; +import { theme } from "./theme.ts"; + +const tagManagerArgs = { + gtmId: "G-8NGQS317V8", +}; + +TagManager.initialize(tagManagerArgs); + +const queryClient = new QueryClient(); + +// eslint-disable-next-line react-refresh/only-export-components +const StyledContent = styled(MaterialDesignContent)(() => ({ + "&.notistack-MuiContent": { + borderRadius: "12px", + }, + "&.notistack-MuiContent-success": { + borderRadius: "12px", + }, + "&.notistack-MuiContent-error": { + borderRadius: "12px", + }, +})); + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + + + + + + +); diff --git a/examples/rooch_fish/web/src/networks.ts b/examples/rooch_fish/web/src/networks.ts new file mode 100644 index 0000000000..c45b84a4d4 --- /dev/null +++ b/examples/rooch_fish/web/src/networks.ts @@ -0,0 +1,18 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 +// Author: Jason Jo + +import { getRoochNodeUrl } from "@roochnetwork/rooch-sdk"; +import { createNetworkConfig } from "@roochnetwork/rooch-sdk-kit"; + +const { networkConfig, useNetworkVariable, useNetworkVariables } = + createNetworkConfig({ + testnet: { + url: getRoochNodeUrl("testnet"), + }, + localnet: { + url: getRoochNodeUrl("localnet"), + }, + }); + +export { useNetworkVariable, useNetworkVariables, networkConfig }; diff --git a/examples/rooch_fish/web/src/scenes/debug_scene.tsx b/examples/rooch_fish/web/src/scenes/debug_scene.tsx new file mode 100644 index 0000000000..b1e0ec8a3f --- /dev/null +++ b/examples/rooch_fish/web/src/scenes/debug_scene.tsx @@ -0,0 +1,179 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +import { Buffer } from 'buffer'; +import { useEffect, useState, useRef } from 'react'; +import { Box, Button, Typography, AppBar, Toolbar } from '@mui/material'; +import { bcs } from "@roochnetwork/rooch-sdk"; +import { useSnackbar } from 'notistack'; +import { usePondState } from '../hooks/usePondState'; + +export const DebugScene = () => { + const { enqueueSnackbar } = useSnackbar(); + const [wsStatus, setWsStatus] = useState<'disconnected' | 'connecting' | 'connected'>('disconnected'); + const wsRef = useRef(null); + const requestIdRef = useRef(0); + const { data: pondState, fishData, foodData } = usePondState(0); + + console.log("pondState:", pondState); + console.log("fishData:", fishData); + console.log("foodData:", foodData); + + const testWebSocket = () => { + try { + setWsStatus('connecting'); + const ws = new WebSocket('wss://test-seed.rooch.network/'); // Replace with your WebSocket address + wsRef.current = ws; + + ws.onopen = () => { + setWsStatus('connected'); + enqueueSnackbar('WebSocket connected successfully!', { variant: 'success' }); + // Send a test message + ws.send('Hello Server!'); + }; + + ws.onmessage = (event) => { + enqueueSnackbar(`Received message: ${event.data}`, { variant: 'info' }); + }; + + ws.onerror = (error) => { + setWsStatus('disconnected'); + enqueueSnackbar('WebSocket connection error!', { variant: 'error' }); + console.error('WebSocket error:', error); + }; + + ws.onclose = () => { + setWsStatus('disconnected'); + wsRef.current = null; + enqueueSnackbar('WebSocket connection closed', { variant: 'warning' }); + }; + + } catch (error: any) { + setWsStatus('disconnected'); + wsRef.current = null; + enqueueSnackbar(`WebSocket error: ${error.message}`, { variant: 'error' }); + } + }; + + const closeWebSocket = () => { + if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { + wsRef.current.close(); + wsRef.current = null; + setWsStatus('disconnected'); + } + }; + + const sendSyncStatesRequest = () => { + if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) { + enqueueSnackbar('WebSocket not connected!', { variant: 'error' }); + return; + } + + const request = { + jsonrpc: "2.0", + id: requestIdRef.current++, + method: "rooch_syncStates", + params: [ + "all", + "89209125", + "100", + { + "decode": true, + "descending": false + } + ] + }; + + try { + wsRef.current.send(JSON.stringify(request)); + enqueueSnackbar('Sync states request sent!', { variant: 'success' }); + } catch (error: any) { + enqueueSnackbar(`Failed to send request: ${error.message}`, { variant: 'error' }); + } + }; + + // Close the connection when the component is unmounted + useEffect(() => { + return () => { + if (wsRef.current) { + wsRef.current.close(); + } + }; + }, []); + + const testBcsDeserialization = () => { + try { + // Define Fish structure + const Fish = bcs.struct('Fish', { + id: bcs.u64(), + owner: bcs.Address, // Move address is 32 bytes + size: bcs.u64(), + x: bcs.u64(), + y: bcs.u64(), + }); + + // Defining the DynamicField structure + const DynamicField = bcs.struct('DynamicField', { + name: bcs.u64(), + value: Fish, + }); + + const hexString = "09000000000000000900000000000000583c31fe2f5a549538f7fc039bf1569af1e20a45f159b11472a18a641549effd0c0000000000000015000000000000000c00000000000000"; + const buffer = Buffer.from(hexString, "hex"); + const deserializedData = DynamicField.parse(buffer); + console.log("deserializedData:", deserializedData) + } catch (error: any) { + console.error('BCS deserialization error:', error); + enqueueSnackbar(`BCS deserialization failed: ${error.message}`, { + variant: 'error' + }); + } + }; + + return ( + + + + + + + + + Status: {wsStatus} + + + + + ); +}; diff --git a/examples/rooch_fish/web/src/scenes/pond_scene.tsx b/examples/rooch_fish/web/src/scenes/pond_scene.tsx new file mode 100644 index 0000000000..2b09288432 --- /dev/null +++ b/examples/rooch_fish/web/src/scenes/pond_scene.tsx @@ -0,0 +1,392 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +import { Container, Graphics, Stage } from '@pixi/react'; +import { useMemo, useState } from 'react'; +import { BlurFilter, ColorMatrixFilter } from 'pixi.js'; +import { Box, Button, Paper, Typography, Grid, AppBar, Toolbar } from '@mui/material'; +import { useSnackbar } from 'notistack'; +import { Args, Transaction } from "@roochnetwork/rooch-sdk"; +import { + UseSignAndExecuteTransaction, +} from "@roochnetwork/rooch-sdk-kit"; +import { Fish } from '../components/fish'; +import { Food } from '../components/food'; +import { useFishController } from '../hooks/useFishController'; +import { usePondState } from '../hooks/usePondState'; +import { usePlayerState } from '../hooks/usePlayerState'; +import { config } from "../config/index"; +import { ExitZone } from '../types/index'; + +const RedColor = 0xFF6B6B +const BlueColor = 0x0000FF +const YellowColor = 0xFFFF00 +const GreenColor = 0x00FF00 + +export const PondScene = () => { + const { enqueueSnackbar } = useSnackbar(); + const [purchaseLoading, setPurchaseLoading] = useState(false); + const [feedLoading, setFeedLoading] = useState(false); + const [exitLoading, setExitLoading] = useState(false); + const { data: pondState, fishData, foodData, getRecentDelays } = usePondState(0); + const { fish_ids } = usePlayerState(0) + + const width = 800; + const height = 800; + + const scale = useMemo(() => { + if (!pondState) return 1; + + const horizontalScale = (width - 80) / pondState.width; + const verticalScale = (height - 80) / pondState.height; + + // Use the smaller scale to ensure pond fits within boundaries + return Math.min(horizontalScale, verticalScale); + }, [pondState, width, height]); + + const boundaries = useMemo(() => ({ + minX: 40, + maxX: width - 80, + minY: 40, + maxY: height - 80 + }), [width, height]); + + const waterFilters = useMemo(() => { + const blur = new BlurFilter(2); + const colorMatrix = new ColorMatrixFilter(); + + colorMatrix.brightness(1.1, true); + return [blur, colorMatrix]; + }, []); + + const foodItems = useMemo(() => { + const items = []; + const foodColors = [0x00FF00, 0xFFFF00, 0xFFA500]; + const margin = 40; + + for (let i = 0; i < 10; i++) { + items.push({ + x: margin + Math.random() * (width - margin * 2), + y: margin + Math.random() * (height - margin * 2), + size: 4 + Math.random() * 4, + color: foodColors[Math.floor(Math.random() * foodColors.length)] + }); + } + return items; + }, [width, height]); + + const playerFirstFish = useMemo(() => { + if (!fish_ids || !fishData || fish_ids.length === 0) return null; + return fishData.find((fish: any) => fish.id === fish_ids[0]); + }, [fish_ids, fishData]); + + useFishController(0, playerFirstFish ? parseInt((playerFirstFish as any).id) : 0, 100, 100, boundaries); + + const { mutateAsync: signAndExecuteTransaction } = + UseSignAndExecuteTransaction(); + + const handlePurchaseFish = async () => { + try { + setPurchaseLoading(true); + const txn = new Transaction(); + txn.callFunction({ + address: config.roochFishAddress, + module: "rooch_fish", + function: "purchase_fish", + args: [ + Args.objectId(config.gameStateObjectID), + Args.u64(BigInt(0)), // pond_id + ], + }); + + await signAndExecuteTransaction({ transaction: txn }); + setPurchaseLoading(false); + } catch (error) { + console.error(String(error)); + + if (String(error).includes("1004")) { + enqueueSnackbar("Insufficient gas, please claim gas first", { + variant: "warning" + }); + } else { + enqueueSnackbar(String(error), { + variant: "warning" + }); + } + } finally { + setPurchaseLoading(false); + } + }; + + const handleFeedPond = async () => { + try { + setFeedLoading(true); + const txn = new Transaction(); + txn.callFunction({ + address: config.roochFishAddress, + module: "rooch_fish", + function: "feed_food", + args: [ + Args.objectId(config.gameStateObjectID), + Args.u64(BigInt(0)), // pond_id + Args.u64(BigInt(10)), // count + ], + }); + + const tx = await signAndExecuteTransaction({ transaction: txn }); + if (tx?.output?.status?.type != 'executed') { + const errorMsg = `Feed food failed: ${tx?.output?.status?.type}`; + console.error("Feed food fail:", errorMsg); + enqueueSnackbar(errorMsg, { + variant: "warning" + }); + return; + } + + console.log("Feed food success!") + enqueueSnackbar("Successfully fed the pond!", { variant: "success" }); + } catch (error) { + console.error(String(error)); + enqueueSnackbar(String(error), { variant: "error" }); + } finally { + setFeedLoading(false); + } + }; + + const isInExitZone = (fishX: number, fishY: number): boolean => { + if (!pondState?.exit_zones) return false; + + return pondState.exit_zones.some((zone: ExitZone) => { + const distance = Math.sqrt( + Math.pow(fishX - zone.x, 2) + + Math.pow(fishY - zone.y, 2) + ); + return distance <= zone.radius; + }); + }; + + const handleExitPond = async () => { + if (!playerFirstFish) return; + + if (!isInExitZone(playerFirstFish.x, playerFirstFish.y)) { + enqueueSnackbar("Fish must be in an exit zone to leave the pond", { + variant: "warning" + }); + return; + } + + try { + setExitLoading(true); + const txn = new Transaction(); + txn.callFunction({ + address: config.roochFishAddress, + module: "rooch_fish", + function: "destroy_fish", + args: [ + Args.objectId(config.gameStateObjectID), + Args.u64(BigInt(0)), // pond_id + Args.u64(BigInt(playerFirstFish.id)), + ], + }); + + const tx = await signAndExecuteTransaction({ transaction: txn }); + if (tx?.output?.status?.type != 'executed') { + throw new Error(`Exit failed: ${tx?.output?.status?.type}`); + } + + enqueueSnackbar("Successfully exited the pond!", { + variant: "success" + }); + } catch (error) { + console.error(String(error)); + enqueueSnackbar(String(error), { + variant: "error" + }); + } finally { + setExitLoading(false); + } + }; + + // Add this utility function at the bottom of the file or in utils/time.ts + const getAverageDelay = (delays: any[]) => { + if (!delays || delays.length === 0) return null; + const sum = delays.reduce((acc, curr) => { + return { + totalDelay: acc.totalDelay + curr.totalDelay, + syncDelay: acc.syncDelay + curr.syncDelay + }; + }, { totalDelay: 0, syncDelay: 0 }); + + return { + confirmDelay: Math.round((sum.totalDelay - sum.syncDelay) / delays.length), + syncDelay: Math.round(sum.syncDelay / delays.length) + }; + }; + + return ( + + + + + + {playerFirstFish && ( + + )} + + + + + + + { + g.clear(); + g.beginFill(0x4FA4FF, 0.3); + g.drawRoundedRect(20, 20, width - 40, height - 40, 15); + g.endFill(); + }} + filters={waterFilters} + /> + + { + g.clear(); + g.lineStyle(4, 0x2980B9); + g.drawRoundedRect(20, 20, width - 40, height - 40, 15); + }} + /> + + + <> + {pondState?.exit_zones?.map((zone: ExitZone, index: number) => ( + { + g.clear(); + g.beginFill(GreenColor, 0.3); + g.drawCircle( + 40 + zone.x * scale, + 40 + zone.y * scale, + zone.radius * scale / 2 + ); + g.endFill(); + }} + /> + ))} + + {fishData && fishData.map((fishState: any, index: number) => ( + + ))} + + {foodData && foodData.map((food: any, index: number) => ( + + ))} + + + + + + {(!fish_ids || fish_ids.length==0) && ( + + + Welcome to the Pond! + + + You don't have a fish yet. Purchase one to start playing! + + + + )} + + + {playerFirstFish && ( + + + + + Position: ({Math.round(playerFirstFish.x)}, {Math.round(playerFirstFish.y)}) + + + + + Confirm: {(() => { + const delays = getRecentDelays(); + const avg = getAverageDelay(delays); + return avg ? `${avg.confirmDelay}ms` : 'N/A'; + })()} + + + + + Sync: {(() => { + const delays = getRecentDelays(); + const avg = getAverageDelay(delays); + return avg ? `${avg.syncDelay}ms` : 'N/A'; + })()} + + + + + )} + + ); +}; diff --git a/examples/rooch_fish/web/src/theme.ts b/examples/rooch_fish/web/src/theme.ts new file mode 100644 index 0000000000..2850c88d8d --- /dev/null +++ b/examples/rooch_fish/web/src/theme.ts @@ -0,0 +1,58 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 +// Author: Jason Jo + +import { createTheme } from "@mui/material"; + +export const theme = createTheme({ + palette: { + primary: { + main: "#0F172A", + }, + }, + typography: { + fontFamily: [ + "Raleway Variable", + "Montserrat", + "ui-sans-serif", + "system-ui", + "sans-serif", + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", + "Noto Color Emoji", + ].join(","), + allVariants: { + fontFamily: "inherit", + fontSize: undefined, + fontWeight: undefined, + textTransform: "unset", + margin: undefined, + }, + }, + shape: { + borderRadius: 12, + }, + components: { + MuiStack: { + defaultProps: { + direction: "row", + alignItems: "center", + }, + }, + MuiChip: { + defaultProps: { + sx: { + borderRadius: "12px", + }, + }, + }, + MuiButton: { + defaultProps: { + sx: { + boxShadow: "none", + }, + }, + }, + }, +}); diff --git a/examples/rooch_fish/web/src/types/index.ts b/examples/rooch_fish/web/src/types/index.ts new file mode 100644 index 0000000000..eb765bf874 --- /dev/null +++ b/examples/rooch_fish/web/src/types/index.ts @@ -0,0 +1,74 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +// Basic types for coordinates and dimensions +export type Point = { + x: number; + y: number; +}; + +// Fish type definition +export type Fish = { + id: number; + owner: string; // address as string + size: number; + x: number; + y: number; +}; + +// Food type definition +export type Food = { + id: number; + size: number; + x: number; + y: number; +}; + +// Player related types +export type PlayerState = { + owner: string; // address as string + feed_amount: string; // u256 as string + reward: string; // u256 as string + fish_count: number; + fish_ids: number[]; +}; + +export type PlayerList = { + players: Record; // address -> PlayerState mapping + total_feed: string; // u256 as string + player_count: number; +}; + +// ExitZone type definition +export type ExitZone = { + x: number; + y: number; + radius: number; +}; + +// Treasury type definition +export type Treasury = { + coin_store: { + value: string; // Using string for large numbers/addresses + }; +}; + +// Main PondState type definition +export type PondState = { + id: number; + fishes: Record; + foods: Record; + exit_zones: ExitZone[]; + quad_tree: any; // Define specific quad tree type if needed + fish_count: number; + food_count: number; + width: number; + height: number; + purchase_amount: string; // Using string for u256 + next_fish_id: number; + next_food_id: number; + max_fish_count: number; + max_food_count: number; + treasury: Treasury; + player_list: PlayerList; +}; diff --git a/examples/rooch_fish/web/src/utils.ts b/examples/rooch_fish/web/src/utils.ts new file mode 100644 index 0000000000..37afd850dd --- /dev/null +++ b/examples/rooch_fish/web/src/utils.ts @@ -0,0 +1,39 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 +// Author: Jason Jo + +export function shortAddress( + address: string | null | undefined, + start = 6, + end = 4 +): string { + try { + if (!address) { + return ""; + } + if (address.length <= start + end) { + return address; + } + return `${address.substring(0, start)}...${address.substring( + address.length - end, + address.length + )}`; + } catch (error) { + return ""; + } +} + +type InputValue = string | number | null; + +export function fNumber(inputValue: InputValue, decimals:number) { + if (inputValue === undefined) return ""; + + const number = Number(inputValue) / Math.pow(10, decimals); + + const fm = new Intl.NumberFormat("en-US", { + minimumFractionDigits: 0, + maximumFractionDigits: decimals, + }).format(number); + + return fm; +} diff --git a/examples/rooch_fish/web/src/utils/index.ts b/examples/rooch_fish/web/src/utils/index.ts new file mode 100644 index 0000000000..6e1d26fc35 --- /dev/null +++ b/examples/rooch_fish/web/src/utils/index.ts @@ -0,0 +1,5 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +export * from "./rooch_client" +export * from "./rooch_object" \ No newline at end of file diff --git a/examples/rooch_fish/web/src/utils/rooch_client.ts b/examples/rooch_fish/web/src/utils/rooch_client.ts new file mode 100644 index 0000000000..24558fca2a --- /dev/null +++ b/examples/rooch_fish/web/src/utils/rooch_client.ts @@ -0,0 +1,197 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +import { TransactionWithInfoView, RoochClient, Transaction, Bytes, Signer, ExecuteTransactionResponseView, str } from "@roochnetwork/rooch-sdk"; + +export const listFieldStates = async (client: RoochClient, object_id: string, stateRoot?: string | null) => { + try { + let result: any[] = []; + let cursor = null; + // eslint-disable-next-line no-constant-condition + while (true) { + try { + const data = await (client as any).transport.request({ + method: 'rooch_listFieldStates', + params: [ + object_id, + cursor, + "100", + { + decode: true, + stateRoot: stateRoot, + }, + ], + }) as any; + + //console.log("🚀 ~ file: listFieldStates ~ data:", data); + + if (!data) { + throw new Error('No data returned from listFieldStates request'); + } + + cursor = data.next_cursor ?? null; + result = result.concat(data.data || []); + + if (!data.has_next_page) { + break; + } + } catch (error) { + console.error('Error during listFieldStates pagination:', error); + break; // Break the loop on error + } + } + + return { result }; + } catch (error) { + console.error('Fatal error in listFieldStates:', error); + throw error; // Re-throw to be handled by React Query's error handling + } +}; + +export const syncStates = async (client: RoochClient, object_id: string, txOrder?: string | null) => { + try { + let result: any[] = []; + let cursor = null; + // eslint-disable-next-line no-constant-condition + while (true) { + try { + const data = await (client as any).transport.request({ + method: 'rooch_syncStates', + params: [ + { + object_i_d: object_id, + }, + txOrder, + "5", + { + decode: true, + descending: false, + }, + ], + }) as any; + + //console.log("🚀 ~ file: listFieldStates ~ data:", data); + + if (!data) { + throw new Error('No data returned from listFieldStates request'); + } + + cursor = data.next_cursor ?? null; + result = result.concat(data.data || []); + + if (!data.has_next_page) { + break; + } + } catch (error) { + console.error('Error during listFieldStates pagination:', error); + break; // Break the loop on error + } + } + + return { result, cursor }; + } catch (error) { + console.error('Fatal error in listFieldStates:', error); + throw error; // Re-throw to be handled by React Query's error handling + } +} + +export const getTransactionsByOrder = async (client: RoochClient, cursor: number | null, limit: number | null, descending_order: boolean) => { + try { + let result: TransactionWithInfoView[] = []; + // eslint-disable-next-line no-constant-condition + while (true) { + try { + const data = await (client as any).transport.request({ + method: 'rooch_getTransactionsByOrder', + params: [ + cursor, + limit, + descending_order, + ], + }) as any; + + //console.log("🚀 ~ file: listFieldStates ~ data:", data); + + if (!data) { + throw new Error('No data returned from listFieldStates request'); + } + + cursor = data.next_cursor ?? null; + result = result.concat(data.data || []); + + if (!data.has_next_page) { + break; + } + } catch (error) { + console.error('Error during listFieldStates pagination:', error); + break; // Break the loop on error + } + } + + return { result, cursor }; + } catch (error) { + console.error('Fatal error in listFieldStates:', error); + throw error; // Re-throw to be handled by React Query's error handling + } +}; + +export const getLatestTransaction = async (client: RoochClient) => { + try { + let cursor = null; + + while (true) { + const resp = await getTransactionsByOrder(client, cursor, 100, true) + //console.log("getLatestTransaction resp:", resp) + const txs = Array.from(resp.result || []).filter((item) => item.execution_info !== null); + cursor = resp.cursor; + + if (txs.length > 0) { + return txs[0] + } + + // Sleep for 1 second using Promise + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } catch (error) { + console.error('Fatal error in getLatestTransaction:', error); + throw error; // Re-throw to be handled by React Query's error handling + } +}; + +export const signAndExecuteTransactionX = async({ + client, + transaction, + seqNumber, + signer, + option = { withOutput: true }, +}: { + client: RoochClient + transaction: Transaction | Bytes + seqNumber: number + signer: Signer + option?: { + withOutput: boolean + } +}): Promise => { + let transactionHex: string + + if (transaction instanceof Uint8Array) { + transactionHex = str('hex', transaction) + } else { + let sender = signer.getRoochAddress().toHexAddress() + transaction.setChainId(await client.getChainId()) + transaction.setSeqNumber(BigInt(seqNumber)) + transaction.setSender(sender) + + const auth = await signer.signTransaction(transaction) + + transaction.setAuth(auth) + + transactionHex = `0x${transaction.encode().toHex()}` + } + + return await (client as any).transport.request({ + method: 'rooch_executeRawTransaction', + params: [transactionHex, option], + }) +} diff --git a/examples/rooch_fish/web/src/utils/rooch_object.ts b/examples/rooch_fish/web/src/utils/rooch_object.ts new file mode 100644 index 0000000000..a0e2796e87 --- /dev/null +++ b/examples/rooch_fish/web/src/utils/rooch_object.ts @@ -0,0 +1,28 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +export function transformObject(data: any): any { + if (data === null || typeof data !== 'object') { + return data; // 如果是基本类型,直接返回 + } + + if (data.decoded_value) { + return transformObject(data.decoded_value); + } + + if (Array.isArray(data)) { + return data.map(item => transformObject(item)); // 处理数组 + } + + const newObject: { [key: string]: any } = {}; + for (const key in data) { + if (data.hasOwnProperty(key)) { + if (key === 'value' && typeof data[key] === 'object') { + return transformObject(data[key]); // 直接返回 value 的内容 + } else if (key !== 'type') { // 只保留非 type 属性 + newObject[key] = transformObject(data[key]); // 递归处理对象属性 + } + } + } + return newObject; +} diff --git a/examples/rooch_fish/web/src/vite-env.d.ts b/examples/rooch_fish/web/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/rooch_fish/web/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/rooch_fish/web/tailwind.config.js b/examples/rooch_fish/web/tailwind.config.js new file mode 100644 index 0000000000..61edcdc5c8 --- /dev/null +++ b/examples/rooch_fish/web/tailwind.config.js @@ -0,0 +1,26 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 +// Author: Jason Jo + +/** @type {import('tailwindcss').Config} */ +export default { + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], + theme: { + fontFamily: { + sans: [ + "Plus Jakarta Sans Variable", + "Montserrat", + "ui-sans-serif", + "system-ui", + "sans-serif", + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", + "Noto Color Emoji", + ], + serif: ["Plus Jakarta Sans Variable"], + }, + extend: {}, + }, + plugins: [], +}; diff --git a/examples/rooch_fish/web/tsconfig.app.json b/examples/rooch_fish/web/tsconfig.app.json new file mode 100644 index 0000000000..9b946c8b32 --- /dev/null +++ b/examples/rooch_fish/web/tsconfig.app.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Module Resolution Helpers */ + "allowImportingTsExtensions": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, + + /* Linting */ + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/rooch_fish/web/tsconfig.json b/examples/rooch_fish/web/tsconfig.json new file mode 100644 index 0000000000..e9e2a6bf4f --- /dev/null +++ b/examples/rooch_fish/web/tsconfig.json @@ -0,0 +1,15 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 +// Author: Jason Jo + +{ + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/examples/rooch_fish/web/tsconfig.node.json b/examples/rooch_fish/web/tsconfig.node.json new file mode 100644 index 0000000000..05a1337a91 --- /dev/null +++ b/examples/rooch_fish/web/tsconfig.node.json @@ -0,0 +1,17 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 +// Author: Jason Jo + +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true, + "noEmit": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/rooch_fish/web/vite.config.ts b/examples/rooch_fish/web/vite.config.ts new file mode 100644 index 0000000000..c827e5859c --- /dev/null +++ b/examples/rooch_fish/web/vite.config.ts @@ -0,0 +1,13 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 +// Author: Jason Jo + +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig(({ mode }) => ({ + plugins: [react()], + esbuild: { + drop: mode === "production" ? ["console", "debugger"] : [], + }, +})); diff --git a/examples/rooch_fish/web/yarn.lock b/examples/rooch_fish/web/yarn.lock new file mode 100644 index 0000000000..79126ef4a5 --- /dev/null +++ b/examples/rooch_fish/web/yarn.lock @@ -0,0 +1,3170 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@alloc/quick-lru@^5.2.0": + version "5.2.0" + resolved "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" + integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== + +"@ampproject/remapping@^2.2.0": + version "2.3.0" + resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.9.tgz#895b6c7e04a7271a0cbfd575d2e8131751914cc7" + integrity sha512-z88xeGxnzehn2sqZ8UdGQEvYErF1odv2CftxInpSYJt6uHuPe9YjahKZITGs3l5LeI9d2ROG+obuDAoSlqbNfQ== + dependencies: + "@babel/highlight" "^7.25.9" + picocolors "^1.0.0" + +"@babel/compat-data@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.9.tgz#24b01c5db6a3ebf85661b4fb4a946a9bccc72ac8" + integrity sha512-yD+hEuJ/+wAJ4Ox2/rpNv5HIuPG82x3ZlQvYVn8iYCprdxzE7P1udpGF1jyjQVBU4dgznN+k2h103vxZ7NdPyw== + +"@babel/core@^7.25.2": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.25.9.tgz#855a4cddcec4158f3f7afadacdab2a7de8af7434" + integrity sha512-WYvQviPw+Qyib0v92AwNIrdLISTp7RfDkM7bPqBvpbnhY4wq8HvHBZREVdYDXk98C8BkOIVnHAY3yvj7AVISxQ== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.25.9" + "@babel/generator" "^7.25.9" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-module-transforms" "^7.25.9" + "@babel/helpers" "^7.25.9" + "@babel/parser" "^7.25.9" + "@babel/template" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.25.9.tgz#c7e828ebe0c2baba103b712924699c9e8a6e32f0" + integrity sha512-omlUGkr5EaoIJrhLf9CJ0TvjBRpd9+AXRG//0GEQ9THSo8wPiTlbpy1/Ow8ZTrbXpjd9FHXfbFQx32I04ht0FA== + dependencies: + "@babel/types" "^7.25.9" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^3.0.2" + +"@babel/helper-compilation-targets@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz#55af025ce365be3cdc0c1c1e56c6af617ce88875" + integrity sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ== + dependencies: + "@babel/compat-data" "^7.25.9" + "@babel/helper-validator-option" "^7.25.9" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715" + integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw== + dependencies: + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/helper-module-transforms@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.9.tgz#12e4fb2969197ef6d78ea8a2f24375ce85b425fb" + integrity sha512-TvLZY/F3+GvdRYFZFyxMvnsKi+4oJdgZzU3BoGN9Uc2d9C6zfNwJcKKhjqLAhK8i46mv93jsO74fDh3ih6rpHA== + dependencies: + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-simple-access" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/helper-plugin-utils@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz#9cbdd63a9443a2c92a725cca7ebca12cc8dd9f46" + integrity sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw== + +"@babel/helper-simple-access@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.9.tgz#6d51783299884a2c74618d6ef0f86820ec2e7739" + integrity sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q== + dependencies: + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/helper-string-parser@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + +"@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + +"@babel/helper-validator-option@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72" + integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw== + +"@babel/helpers@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.9.tgz#9e26aa6fbefdbca4f8c8a1d66dc6f1c00ddadb0a" + integrity sha512-oKWp3+usOJSzDZOucZUAMayhPz/xVjzymyDzUN8dk0Wd3RWMlGLXi07UCQ/CgQVb8LvXx3XBajJH4XGgkt7H7g== + dependencies: + "@babel/template" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/highlight@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.9.tgz#8141ce68fc73757946f983b343f1231f4691acc6" + integrity sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + chalk "^2.4.2" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.25.9.tgz#8fcaa079ac7458facfddc5cd705cc8005e4d3817" + integrity sha512-aI3jjAAO1fh7vY/pBGsn1i9LDbRP43+asrRlkPuTXW5yHXtd1NgTEMudbBoDDxrf1daEEfPJqR+JBMakzrR4Dg== + dependencies: + "@babel/types" "^7.25.9" + +"@babel/plugin-transform-react-jsx-self@^7.24.7": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz#c0b6cae9c1b73967f7f9eb2fca9536ba2fad2858" + integrity sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-react-jsx-source@^7.24.7": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz#4c6b8daa520b5f155b5fb55547d7c9fa91417503" + integrity sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.9.tgz#65884fd6dc255a775402cc1d9811082918f4bf00" + integrity sha512-4zpTHZ9Cm6L9L+uIqghQX8ZXg8HKFcjYO3qHoO8zTmRm6HQUJ8SSJ+KRvbMBZn0EGVlT4DRYeQ/6hjlyXBh+Kg== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/template@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016" + integrity sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg== + dependencies: + "@babel/code-frame" "^7.25.9" + "@babel/parser" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/traverse@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84" + integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw== + dependencies: + "@babel/code-frame" "^7.25.9" + "@babel/generator" "^7.25.9" + "@babel/parser" "^7.25.9" + "@babel/template" "^7.25.9" + "@babel/types" "^7.25.9" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.25.9.tgz#620f35ea1f4233df529ec9a2668d2db26574deee" + integrity sha512-OwS2CM5KocvQ/k7dFJa8i5bNGJP0hXWfVCfDkqRFP1IreH1JDC7wG6eCYCi0+McbfT8OR/kNqsI0UU0xP9H6PQ== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + +"@emotion/babel-plugin@^11.12.0": + version "11.12.0" + resolved "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz#7b43debb250c313101b3f885eba634f1d723fcc2" + integrity sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/runtime" "^7.18.3" + "@emotion/hash" "^0.9.2" + "@emotion/memoize" "^0.9.0" + "@emotion/serialize" "^1.2.0" + babel-plugin-macros "^3.1.0" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "4.2.0" + +"@emotion/cache@^11.11.0", "@emotion/cache@^11.13.0": + version "11.13.1" + resolved "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.1.tgz#fecfc54d51810beebf05bf2a161271a1a91895d7" + integrity sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw== + dependencies: + "@emotion/memoize" "^0.9.0" + "@emotion/sheet" "^1.4.0" + "@emotion/utils" "^1.4.0" + "@emotion/weak-memoize" "^0.4.0" + stylis "4.2.0" + +"@emotion/hash@^0.9.0", "@emotion/hash@^0.9.2": + version "0.9.2" + resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz#ff9221b9f58b4dfe61e619a7788734bd63f6898b" + integrity sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g== + +"@emotion/is-prop-valid@^1.3.0": + version "1.3.1" + resolved "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz#8d5cf1132f836d7adbe42cf0b49df7816fc88240" + integrity sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw== + dependencies: + "@emotion/memoize" "^0.9.0" + +"@emotion/memoize@^0.9.0": + version "0.9.0" + resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz#745969d649977776b43fc7648c556aaa462b4102" + integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ== + +"@emotion/react@^11.11.4": + version "11.13.3" + resolved "https://registry.npmjs.org/@emotion/react/-/react-11.13.3.tgz#a69d0de2a23f5b48e0acf210416638010e4bd2e4" + integrity sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.12.0" + "@emotion/cache" "^11.13.0" + "@emotion/serialize" "^1.3.1" + "@emotion/use-insertion-effect-with-fallbacks" "^1.1.0" + "@emotion/utils" "^1.4.0" + "@emotion/weak-memoize" "^0.4.0" + hoist-non-react-statics "^3.3.1" + +"@emotion/serialize@^1.2.0", "@emotion/serialize@^1.3.0", "@emotion/serialize@^1.3.1": + version "1.3.2" + resolved "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.2.tgz#e1c1a2e90708d5d85d81ccaee2dfeb3cc0cccf7a" + integrity sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA== + dependencies: + "@emotion/hash" "^0.9.2" + "@emotion/memoize" "^0.9.0" + "@emotion/unitless" "^0.10.0" + "@emotion/utils" "^1.4.1" + csstype "^3.0.2" + +"@emotion/sheet@^1.4.0": + version "1.4.0" + resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz#c9299c34d248bc26e82563735f78953d2efca83c" + integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg== + +"@emotion/styled@^11.11.5": + version "11.13.0" + resolved "https://registry.npmjs.org/@emotion/styled/-/styled-11.13.0.tgz#633fd700db701472c7a5dbef54d6f9834e9fb190" + integrity sha512-tkzkY7nQhW/zC4hztlwucpT8QEZ6eUzpXDRhww/Eej4tFfO0FxQYWRyg/c5CCXa4d/f174kqeXYjuQRnhzf6dA== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.12.0" + "@emotion/is-prop-valid" "^1.3.0" + "@emotion/serialize" "^1.3.0" + "@emotion/use-insertion-effect-with-fallbacks" "^1.1.0" + "@emotion/utils" "^1.4.0" + +"@emotion/unitless@^0.10.0": + version "0.10.0" + resolved "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz#2af2f7c7e5150f497bdabd848ce7b218a27cf745" + integrity sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg== + +"@emotion/use-insertion-effect-with-fallbacks@^1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz#1a818a0b2c481efba0cf34e5ab1e0cb2dcb9dfaf" + integrity sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw== + +"@emotion/utils@^1.4.0", "@emotion/utils@^1.4.1": + version "1.4.1" + resolved "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.1.tgz#b3adbb43de12ee2149541c4f1337d2eb7774f0ad" + integrity sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA== + +"@emotion/weak-memoize@^0.4.0": + version "0.4.0" + resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz#5e13fac887f08c44f76b0ccaf3370eb00fec9bb6" + integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== + +"@esbuild/aix-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" + integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== + +"@esbuild/android-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" + integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== + +"@esbuild/android-arm@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" + integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== + +"@esbuild/android-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" + integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== + +"@esbuild/darwin-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" + integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== + +"@esbuild/darwin-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" + integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== + +"@esbuild/freebsd-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" + integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== + +"@esbuild/freebsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" + integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== + +"@esbuild/linux-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" + integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== + +"@esbuild/linux-arm@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" + integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== + +"@esbuild/linux-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" + integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== + +"@esbuild/linux-loong64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" + integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== + +"@esbuild/linux-mips64el@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" + integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== + +"@esbuild/linux-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" + integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== + +"@esbuild/linux-riscv64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" + integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== + +"@esbuild/linux-s390x@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" + integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== + +"@esbuild/linux-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" + integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== + +"@esbuild/netbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" + integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== + +"@esbuild/openbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" + integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== + +"@esbuild/sunos-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" + integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== + +"@esbuild/win32-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" + integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== + +"@esbuild/win32-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" + integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== + +"@esbuild/win32-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" + integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== + +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": + version "4.4.0" + resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.6.1": + version "4.11.1" + resolved "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz#a547badfc719eb3e5f4b556325e542fbe9d7a18f" + integrity sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q== + +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.6.0" + globals "^13.19.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@8.57.1": + version "8.57.1" + resolved "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" + integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== + +"@floating-ui/core@^1.6.0": + version "1.6.8" + resolved "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz#aa43561be075815879305965020f492cdb43da12" + integrity sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA== + dependencies: + "@floating-ui/utils" "^0.2.8" + +"@floating-ui/dom@^1.0.0": + version "1.6.11" + resolved "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.11.tgz#8631857838d34ee5712339eb7cbdfb8ad34da723" + integrity sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ== + dependencies: + "@floating-ui/core" "^1.6.0" + "@floating-ui/utils" "^0.2.8" + +"@floating-ui/react-dom@^2.0.8": + version "2.1.2" + resolved "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz#a1349bbf6a0e5cb5ded55d023766f20a4d439a31" + integrity sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A== + dependencies: + "@floating-ui/dom" "^1.0.0" + +"@floating-ui/utils@^0.2.8": + version "0.2.8" + resolved "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz#21a907684723bbbaa5f0974cf7730bd797eb8e62" + integrity sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig== + +"@fontsource-variable/plus-jakarta-sans@^5.0.21": + version "5.1.0" + resolved "https://registry.npmjs.org/@fontsource-variable/plus-jakarta-sans/-/plus-jakarta-sans-5.1.0.tgz#9dd5aa72277c43d408f6e34cd658d823fbef1fe1" + integrity sha512-K7o2GO7/quVFFyfjTqYZu0ng4KJGf60KSAGbvUWwKuH+/Giyl6Qe/EqnGLcjrl9AmXZzTvtRB1xpkASn8FNFoQ== + +"@fontsource-variable/raleway@^5.0.20": + version "5.1.0" + resolved "https://registry.npmjs.org/@fontsource-variable/raleway/-/raleway-5.1.0.tgz#d3a3e7886ac10feab6425f2798f62f5f9334ea52" + integrity sha512-TtV3e3ZLfGu6XmqRDvZ0Q4cXlRs4Rwbxsc+Te3yMtJKRQgNS/OBRtLjztb3cCrZWTPvH7bKKWjtBlC8yuZWWVw== + +"@humanwhocodes/config-array@^0.13.0": + version "0.13.0" + resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" + integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== + dependencies: + "@humanwhocodes/object-schema" "^2.0.3" + debug "^4.3.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^2.0.3": + version "2.0.3" + resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@jridgewell/gen-mapping@^0.3.2", "@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@mui/base@5.0.0-beta.40": + version "5.0.0-beta.40" + resolved "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz#1f8a782f1fbf3f84a961e954c8176b187de3dae2" + integrity sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ== + dependencies: + "@babel/runtime" "^7.23.9" + "@floating-ui/react-dom" "^2.0.8" + "@mui/types" "^7.2.14" + "@mui/utils" "^5.15.14" + "@popperjs/core" "^2.11.8" + clsx "^2.1.0" + prop-types "^15.8.1" + +"@mui/core-downloads-tracker@^5.16.7": + version "5.16.7" + resolved "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.7.tgz#182a325a520f7ebd75de051fceabfc0314cfd004" + integrity sha512-RtsCt4Geed2/v74sbihWzzRs+HsIQCfclHeORh5Ynu2fS4icIKozcSubwuG7vtzq2uW3fOR1zITSP84TNt2GoQ== + +"@mui/lab@^5.0.0-alpha.172": + version "5.0.0-alpha.173" + resolved "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.173.tgz#a0f9696d93a765b48d69a7da5aaca0affa510ae8" + integrity sha512-Gt5zopIWwxDgGy/MXcp6GueD84xFFugFai4hYiXY0zowJpTVnIrTQCQXV004Q7rejJ7aaCntX9hpPJqCrioshA== + dependencies: + "@babel/runtime" "^7.23.9" + "@mui/base" "5.0.0-beta.40" + "@mui/system" "^5.16.5" + "@mui/types" "^7.2.15" + "@mui/utils" "^5.16.5" + clsx "^2.1.0" + prop-types "^15.8.1" + +"@mui/material@^5.16.4": + version "5.16.7" + resolved "https://registry.npmjs.org/@mui/material/-/material-5.16.7.tgz#6e814e2eefdaf065a769cecf549c3569e107a50b" + integrity sha512-cwwVQxBhK60OIOqZOVLFt55t01zmarKJiJUWbk0+8s/Ix5IaUzAShqlJchxsIQ4mSrWqgcKCCXKtIlG5H+/Jmg== + dependencies: + "@babel/runtime" "^7.23.9" + "@mui/core-downloads-tracker" "^5.16.7" + "@mui/system" "^5.16.7" + "@mui/types" "^7.2.15" + "@mui/utils" "^5.16.6" + "@popperjs/core" "^2.11.8" + "@types/react-transition-group" "^4.4.10" + clsx "^2.1.0" + csstype "^3.1.3" + prop-types "^15.8.1" + react-is "^18.3.1" + react-transition-group "^4.4.5" + +"@mui/private-theming@^5.16.6": + version "5.16.6" + resolved "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.6.tgz#547671e7ae3f86b68d1289a0b90af04dfcc1c8c9" + integrity sha512-rAk+Rh8Clg7Cd7shZhyt2HGTTE5wYKNSJ5sspf28Fqm/PZ69Er9o6KX25g03/FG2dfpg5GCwZh/xOojiTfm3hw== + dependencies: + "@babel/runtime" "^7.23.9" + "@mui/utils" "^5.16.6" + prop-types "^15.8.1" + +"@mui/styled-engine@^5.16.6": + version "5.16.6" + resolved "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.6.tgz#60110c106dd482dfdb7e2aa94fd6490a0a3f8852" + integrity sha512-zaThmS67ZmtHSWToTiHslbI8jwrmITcN93LQaR2lKArbvS7Z3iLkwRoiikNWutx9MBs8Q6okKvbZq1RQYB3v7g== + dependencies: + "@babel/runtime" "^7.23.9" + "@emotion/cache" "^11.11.0" + csstype "^3.1.3" + prop-types "^15.8.1" + +"@mui/system@^5.16.5", "@mui/system@^5.16.7": + version "5.16.7" + resolved "https://registry.npmjs.org/@mui/system/-/system-5.16.7.tgz#4583ca5bf3b38942e02c15a1e622ba869ac51393" + integrity sha512-Jncvs/r/d/itkxh7O7opOunTqbbSSzMTHzZkNLM+FjAOg+cYAZHrPDlYe1ZGKUYORwwb2XexlWnpZp0kZ4AHuA== + dependencies: + "@babel/runtime" "^7.23.9" + "@mui/private-theming" "^5.16.6" + "@mui/styled-engine" "^5.16.6" + "@mui/types" "^7.2.15" + "@mui/utils" "^5.16.6" + clsx "^2.1.0" + csstype "^3.1.3" + prop-types "^15.8.1" + +"@mui/types@^7.2.14", "@mui/types@^7.2.15": + version "7.2.18" + resolved "https://registry.npmjs.org/@mui/types/-/types-7.2.18.tgz#4b6385ed2f7828ef344113cdc339d6fdf8e4bc23" + integrity sha512-uvK9dWeyCJl/3ocVnTOS6nlji/Knj8/tVqVX03UVTpdmTJYu/s4jtDd9Kvv0nRGE0CUSNW1UYAci7PYypjealg== + +"@mui/utils@^5.15.14", "@mui/utils@^5.16.5", "@mui/utils@^5.16.6": + version "5.16.6" + resolved "https://registry.npmjs.org/@mui/utils/-/utils-5.16.6.tgz#905875bbc58d3dcc24531c3314a6807aba22a711" + integrity sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA== + dependencies: + "@babel/runtime" "^7.23.9" + "@mui/types" "^7.2.15" + "@types/prop-types" "^15.7.12" + clsx "^2.1.1" + prop-types "^15.8.1" + react-is "^18.3.1" + +"@mysten/bcs@1.0.4": + version "1.0.4" + resolved "https://registry.npmjs.org/@mysten/bcs/-/bcs-1.0.4.tgz#c8998ee7cb92d9e59e2fae735b69abe49d8037cb" + integrity sha512-6JoQi59GN/dVEBCNq8Rj4uOR0niDrJqDx/2gNQWXANwJakHIGH0AMniHrXP41B2dF+mZ3HVmh9Hi3otiEVQTrQ== + dependencies: + bs58 "^6.0.0" + +"@noble/curves@~1.6.0": + version "1.6.0" + resolved "https://registry.npmjs.org/@noble/curves/-/curves-1.6.0.tgz#be5296ebcd5a1730fccea4786d420f87abfeb40b" + integrity sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ== + dependencies: + "@noble/hashes" "1.5.0" + +"@noble/hashes@1.5.0", "@noble/hashes@^1.2.0", "@noble/hashes@~1.5.0": + version "1.5.0" + resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz#abadc5ca20332db2b1b2aa3e496e9af1213570b0" + integrity sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA== + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@pixi/accessibility@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/accessibility/-/accessibility-7.4.2.tgz#8c7105bd19b979330baea36cc09f7c367975812e" + integrity sha512-R6VEolm8uyy1FB1F2qaLKxVbzXAFTZCF2ka8fl9lsz7We6ZfO4QpXv9ur7DvzratjCQUQVCKo0/V7xL5q1EV/g== + +"@pixi/app@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/app/-/app-7.4.2.tgz#f1d9c9c52ff1d4133766590fbda677a23658fec7" + integrity sha512-ugkH3kOgjT8P1mTMY29yCOgEh+KuVMAn8uBxeY0aMqaUgIMysfpnFv+Aepp2CtvI9ygr22NC+OiKl+u+eEaQHw== + +"@pixi/assets@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/assets/-/assets-7.4.2.tgz#1df0c38c4fb64c391202cea8768e81eaa42de221" + integrity sha512-anxho59H9egZwoaEdM5aLvYyxoz6NCy3CaQIvNHD1bbGg8L16Ih0e26QSBR5fu53jl8OjT6M7s+p6n7uu4+fGA== + dependencies: + "@types/css-font-loading-module" "^0.0.12" + +"@pixi/color@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/color/-/color-7.4.2.tgz#d564a0b589166afb1c4a0e85c968b7375dbfc06f" + integrity sha512-av1LOvhHsiaW8+T4n/FgnOKHby55/w7VcA1HzPIHRBtEcsmxvSCDanT1HU2LslNhrxLPzyVx18nlmalOyt5OBg== + dependencies: + "@pixi/colord" "^2.9.6" + +"@pixi/colord@^2.9.6": + version "2.9.6" + resolved "https://registry.npmjs.org/@pixi/colord/-/colord-2.9.6.tgz#7e4e7851480da6fd3cef4e331f008d60be7e1204" + integrity sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA== + +"@pixi/compressed-textures@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/compressed-textures/-/compressed-textures-7.4.2.tgz#5c256d3a2071848e7503d499aae22684bbd69529" + integrity sha512-VJrt7el6O4ZJSWkeOGXwrhJaiLg1UBhHB3fj42VR4YloYkAxpfd9K6s6IcbcVz7n9L48APKBMgHyaB2pX2Ck/A== + +"@pixi/constants@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/constants/-/constants-7.4.2.tgz#e1b12316e1c637f3ebe80b599a707de4bdc4ab9b" + integrity sha512-N9vn6Wpz5WIQg7ugUg2+SdqD2u2+NM0QthE8YzLJ4tLH2Iz+/TrnPKUJzeyIqbg3sxJG5ZpGGPiacqIBpy1KyA== + +"@pixi/core@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/core/-/core-7.4.2.tgz#521de6fe99d48ffffef7e79d4dfde5c5055e8a13" + integrity sha512-UbMtgSEnyCOFPzbE6ThB9qopXxbZ5GCof2ArB4FXOC5Xi/83MOIIYg5kf5M8689C5HJMhg2SrJu3xLKppF+CMg== + dependencies: + "@pixi/color" "7.4.2" + "@pixi/constants" "7.4.2" + "@pixi/extensions" "7.4.2" + "@pixi/math" "7.4.2" + "@pixi/runner" "7.4.2" + "@pixi/settings" "7.4.2" + "@pixi/ticker" "7.4.2" + "@pixi/utils" "7.4.2" + +"@pixi/display@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/display/-/display-7.4.2.tgz#f0b7db822fba80cd2e8c28f6f489244360a578ba" + integrity sha512-DaD0J7gIlNlzO0Fdlby/0OH+tB5LtCY6rgFeCBKVDnzmn8wKW3zYZRenWBSFJ0Psx6vLqXYkSIM/rcokaKviIw== + +"@pixi/events@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/events/-/events-7.4.2.tgz#efd3361f7a78161aa0dffb484ea4ce3b7a0043c4" + integrity sha512-Jw/w57heZjzZShIXL0bxOvKB+XgGIevyezhGtfF2ZSzQoSBWo+Fj1uE0QwKd0RIaXegZw/DhSmiMJSbNmcjifA== + +"@pixi/extensions@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/extensions/-/extensions-7.4.2.tgz#4e939e3670ca66fdcd2590e8ab90a6a04a176dec" + integrity sha512-Hmx2+O0yZ8XIvgomHM9GZEGcy9S9Dd8flmtOK5Aa3fXs/8v7xD08+ANQpN9ZqWU2Xs+C6UBlpqlt2BWALvKKKA== + +"@pixi/extract@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/extract/-/extract-7.4.2.tgz#0e52446202bc8521ca9821a276e919020b0688f4" + integrity sha512-JOX27TRWjVEjauGBbF8PU7/g6LYXnivehdgqS5QlVDv1CNHTOrz/j3MdKcVWOhyZPbH5c9sh7lxyRxvd9AIuTQ== + +"@pixi/filter-alpha@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/filter-alpha/-/filter-alpha-7.4.2.tgz#0cd7bccadb91109cdb05679a17f8ba4d5281d0ec" + integrity sha512-9OsKJ+yvY2wIcQXwswj5HQBiwNGymwmqdxfp7mo+nZSBoDmxUqvMZzE9UNJ3eUlswuNvNRO8zNOsQvwdz7WFww== + +"@pixi/filter-blur@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/filter-blur/-/filter-blur-7.4.2.tgz#89458c467a553ec62ce799b4e68c6c574ba3a5bb" + integrity sha512-gOXBbIUx6CRZP1fmsis2wLzzSsofrqmIHhbf1gIkZMIQaLsc9T7brj+PaLTTiOiyJgnvGN5j20RZnkERWWKV0Q== + +"@pixi/filter-color-matrix@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/filter-color-matrix/-/filter-color-matrix-7.4.2.tgz#b792d6624a1b019edcc7f96e131078f8bc21c6d1" + integrity sha512-ykZiR59Gvj80UKs9qm7jeUTKvn+wWk6HBVJOmJbK9jFK5juakDWp7BbH26U78Q61EWj97kI1FdfcbMkuQ7rqkA== + +"@pixi/filter-displacement@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/filter-displacement/-/filter-displacement-7.4.2.tgz#e001f66c7f8bf02241c468cb33a01d21090ee09a" + integrity sha512-QS/eWp/ivsxef3xapNeGwpPX7vrqQQeo99Fux4k5zsvplnNEsf91t6QYJLG776AbZEu/qh8VYRBA5raIVY/REw== + +"@pixi/filter-fxaa@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/filter-fxaa/-/filter-fxaa-7.4.2.tgz#d11d6ee1fd098a8a6bee49b804d3b3ff087af1bf" + integrity sha512-U/ptJgDsfs/r8y2a6gCaiPfDu2IFAxpQ4wtfmBpz6vRhqeE4kI8yNIUx5dZbui57zlsJaW0BNacOQxHU0vLkyQ== + +"@pixi/filter-noise@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/filter-noise/-/filter-noise-7.4.2.tgz#99aebe14d0f52aaa8509e2531f1a4e658a702da9" + integrity sha512-Vy9ViBFhZEGh6xKkd3kFWErolZTwv1Y5Qb1bV7qPIYbvBECYsqzlR4uCrrjBV6KKm0PufpG/+NKC5vICZaqKzg== + +"@pixi/graphics@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/graphics/-/graphics-7.4.2.tgz#b4fcc39f6a0af4c74d66b3d45b6728c33467bc78" + integrity sha512-jH4/Tum2RqWzHGzvlwEr7HIVduoLO57Ze705N2zQPkUD57TInn5911aGUeoua7f/wK8cTLGzgB9BzSo2kTdcHw== + +"@pixi/math@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/math/-/math-7.4.2.tgz#473b400c89ac8fcdf6e09259ab097ce35cd8ac68" + integrity sha512-7jHmCQoYk6e0rfSKjdNFOPl0wCcdgoraxgteXJTTHv3r0bMNx2pHD9FJ0VvocEUG7XHfj55O3+u7yItOAx0JaQ== + +"@pixi/mesh-extras@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/mesh-extras/-/mesh-extras-7.4.2.tgz#92eb4f41fd08907f6118dbfcb4b65fc5642d14d3" + integrity sha512-vNR/7wjxjs7sv9fGoKkHyU91ZAD+7EnMHBS5F3CVISlOIFxLi96NNZCB81oUIdky/90pHw40johd/4izR5zTyw== + +"@pixi/mesh@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/mesh/-/mesh-7.4.2.tgz#f2095eb8ec792a102c76f18838f75d860bd42dff" + integrity sha512-mEkKyQvvMrYXC3pahvH5WBIKtrtB63WixRr91ANFI7zXD+ESG6Ap6XtxMCJmXDQPwBDNk7SWVMiCflYuchG7kA== + +"@pixi/mixin-cache-as-bitmap@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/mixin-cache-as-bitmap/-/mixin-cache-as-bitmap-7.4.2.tgz#288f9700a2bbb524798cea86b06bd1be36f90659" + integrity sha512-6dgthi2ruUT/lervSrFDQ7vXkEsHo6CxdgV7W/wNdW1dqgQlKfDvO6FhjXzyIMRLSooUf5FoeluVtfsjkUIYrw== + +"@pixi/mixin-get-child-by-name@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/mixin-get-child-by-name/-/mixin-get-child-by-name-7.4.2.tgz#d8671526637c09a87502466e27066049aebd240f" + integrity sha512-0Cfw8JpQhsixprxiYph4Lj+B5n83Kk4ftNMXgM5xtZz+tVLz5s91qR0MqcdzwTGTJ7utVygiGmS4/3EfR/duRQ== + +"@pixi/mixin-get-global-position@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/mixin-get-global-position/-/mixin-get-global-position-7.4.2.tgz#9dcc9f49fa8001103ceadb1061efe03a71761952" + integrity sha512-LcsahbVdX4DFS2IcGfNp4KaXuu7SjAwUp/flZSGIfstyKOKb5FWFgihtqcc9ZT4coyri3gs2JbILZub/zPZj1w== + +"@pixi/particle-container@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/particle-container/-/particle-container-7.4.2.tgz#f829caaa724be10b7e0a04fca89a68f64e80012a" + integrity sha512-B78Qq86kt0lEa5WtB2YFIm3+PjhKfw9La9R++GBSgABl+g13s2UaZ6BIPxvY3JxWMdxPm4iPrQPFX1QWRN68mw== + +"@pixi/prepare@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/prepare/-/prepare-7.4.2.tgz#36fa8f5a044e6c4f46e7d2df1bef82dce7d40455" + integrity sha512-PugyMzReCHXUzc3so9PPJj2OdHwibpUNWyqG4mWY2UUkb6c8NAGK1AnAPiscOvLilJcv/XQSFoNhX+N1jrvJEg== + +"@pixi/react@7": + version "7.1.2" + resolved "https://registry.npmjs.org/@pixi/react/-/react-7.1.2.tgz#f7491456e2176274e2536cb4de62fcdbc7aa42e8" + integrity sha512-ZhqeXcFCRfFYCvncGW6Bxc3PFCYN1PgpF60iZwQJA6/UD3DB70JQvtDkRnyQcy5yqsjNtdxS8HB42oNjQFnZrA== + dependencies: + lodash.isnil "4.0.0" + lodash.times "4.3.2" + performance-now "2.1.0" + prop-types "^15.8.1" + +"@pixi/runner@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/runner/-/runner-7.4.2.tgz#92b9748a2f85b9835bb36ce196a37215b8396392" + integrity sha512-LPBpwym4vdyyDY5ucF4INQccaGyxztERyLTY1YN6aqJyyMmnc7iqXlIKt+a0euMBtNoLoxy6MWMvIuZj0JfFPA== + +"@pixi/settings@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/settings/-/settings-7.4.2.tgz#36ebeec5f15bc9c538399e0692363be00a7bf28e" + integrity sha512-pMN+L6aWgvUbwhFIL/BTHKe2ShYGPZ8h9wlVBnFHMtUcJcFLMF1B3lzuvCayZRepOphs6RY0TqvnDvVb585JhQ== + dependencies: + "@pixi/constants" "7.4.2" + "@types/css-font-loading-module" "^0.0.12" + ismobilejs "^1.1.0" + +"@pixi/sprite-animated@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/sprite-animated/-/sprite-animated-7.4.2.tgz#c28a3b2a8ee986517657ad5a1406fdddd610f56b" + integrity sha512-QPT6yxCUGOBN+98H3pyIZ1ZO6Y7BN1o0Q2IMZEsD1rNfZJrTYS3Q8VlCG5t2YlFlcB8j5iBo24bZb6FUxLOmsQ== + +"@pixi/sprite-tiling@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/sprite-tiling/-/sprite-tiling-7.4.2.tgz#ba6e3c596cb6dadb21795153058a900dcb1cfd2b" + integrity sha512-Z8PP6ewy3nuDYL+NeEdltHAhuucVgia33uzAitvH3OqqRSx6a6YRBFbNLUM9Sx+fBO2Lk3PpV1g6QZX+NE5LOg== + +"@pixi/sprite@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/sprite/-/sprite-7.4.2.tgz#beed8c2fd2e00a02e340eaba9844b180bf1f1b24" + integrity sha512-Ccf/OVQsB+HQV0Fyf5lwD+jk1jeU7uSIqEjbxenNNssmEdB7S5qlkTBV2EJTHT83+T6Z9OMOHsreJZerydpjeg== + +"@pixi/spritesheet@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/spritesheet/-/spritesheet-7.4.2.tgz#e95ee2814fdf41a0028530d3f942a2bdb75b5543" + integrity sha512-YIvHdpXW+AYp8vD0NkjJmrdnVHTZKidCnx6k8ATSuuvCT6O5Tuh2N/Ul2oDj4/QaePy0lVhyhAbZpJW00Jr7mQ== + +"@pixi/text-bitmap@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/text-bitmap/-/text-bitmap-7.4.2.tgz#0027f50025bbb13953c829200c99b5a6e212dee7" + integrity sha512-lPBMJ83JnpFVL+6ckQ8KO8QmwdPm0z9Zs/M0NgFKH2F+BcjelRNnk80NI3O0qBDYSEDQIE+cFbKoZ213kf7zwA== + +"@pixi/text-html@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/text-html/-/text-html-7.4.2.tgz#843e153d1f8c09fdf18c2527baa9f778061e94d3" + integrity sha512-duOu8oDYeDNuyPozj2DAsQ5VZBbRiwIXy78Gn7H2pCiEAefw/Uv5jJYwdgneKME0e1tOxz1eOUGKPcI6IJnZjw== + +"@pixi/text@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/text/-/text-7.4.2.tgz#ee9f54083ecbf5b07d4410ede0cd005a312d2e71" + integrity sha512-rZZWpJNsIQ8WoCWrcVg8Gi6L/PDakB941clo6dO3XjoII2ucoOUcnpe5HIkudxi2xPvS/8Bfq990gFEx50TP5A== + +"@pixi/ticker@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/ticker/-/ticker-7.4.2.tgz#804994ddd0c771bdf938c9eeb8ad5ed4a28c0374" + integrity sha512-cAvxCh/KI6IW4m3tp2b+GQIf+DoSj9NNmPJmsOeEJ7LzvruG8Ps7SKI6CdjQob5WbceL1apBTDbqZ/f77hFDiQ== + dependencies: + "@pixi/extensions" "7.4.2" + "@pixi/settings" "7.4.2" + "@pixi/utils" "7.4.2" + +"@pixi/utils@7.4.2": + version "7.4.2" + resolved "https://registry.npmjs.org/@pixi/utils/-/utils-7.4.2.tgz#bbbe60eec3d91fa294a0c3bb6410be6a85eaaadc" + integrity sha512-aU/itcyMC4TxFbmdngmak6ey4kC5c16Y5ntIYob9QnjNAfD/7GTsYIBnP6FqEAyO1eq0MjkAALxdONuay1BG3g== + dependencies: + "@pixi/color" "7.4.2" + "@pixi/constants" "7.4.2" + "@pixi/settings" "7.4.2" + "@types/earcut" "^2.1.0" + earcut "^2.2.4" + eventemitter3 "^4.0.0" + url "^0.11.0" + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +"@popperjs/core@^2.11.8": + version "2.11.8" + resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" + integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== + +"@rollup/rollup-android-arm-eabi@4.24.0": + version "4.24.0" + resolved "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz#1661ff5ea9beb362795304cb916049aba7ac9c54" + integrity sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA== + +"@rollup/rollup-android-arm64@4.24.0": + version "4.24.0" + resolved "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz#2ffaa91f1b55a0082b8a722525741aadcbd3971e" + integrity sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA== + +"@rollup/rollup-darwin-arm64@4.24.0": + version "4.24.0" + resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz#627007221b24b8cc3063703eee0b9177edf49c1f" + integrity sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA== + +"@rollup/rollup-darwin-x64@4.24.0": + version "4.24.0" + resolved "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz#0605506142b9e796c370d59c5984ae95b9758724" + integrity sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ== + +"@rollup/rollup-linux-arm-gnueabihf@4.24.0": + version "4.24.0" + resolved "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz#62dfd196d4b10c0c2db833897164d2d319ee0cbb" + integrity sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA== + +"@rollup/rollup-linux-arm-musleabihf@4.24.0": + version "4.24.0" + resolved "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz#53ce72aeb982f1f34b58b380baafaf6a240fddb3" + integrity sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw== + +"@rollup/rollup-linux-arm64-gnu@4.24.0": + version "4.24.0" + resolved "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz#1632990f62a75c74f43e4b14ab3597d7ed416496" + integrity sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA== + +"@rollup/rollup-linux-arm64-musl@4.24.0": + version "4.24.0" + resolved "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz#8c03a996efb41e257b414b2e0560b7a21f2d9065" + integrity sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw== + +"@rollup/rollup-linux-powerpc64le-gnu@4.24.0": + version "4.24.0" + resolved "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz#5b98729628d5bcc8f7f37b58b04d6845f85c7b5d" + integrity sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw== + +"@rollup/rollup-linux-riscv64-gnu@4.24.0": + version "4.24.0" + resolved "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz#48e42e41f4cabf3573cfefcb448599c512e22983" + integrity sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg== + +"@rollup/rollup-linux-s390x-gnu@4.24.0": + version "4.24.0" + resolved "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz#e0b4f9a966872cb7d3e21b9e412a4b7efd7f0b58" + integrity sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g== + +"@rollup/rollup-linux-x64-gnu@4.24.0": + version "4.24.0" + resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz#78144741993100f47bd3da72fce215e077ae036b" + integrity sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A== + +"@rollup/rollup-linux-x64-musl@4.24.0": + version "4.24.0" + resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz#d9fe32971883cd1bd858336bd33a1c3ca6146127" + integrity sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ== + +"@rollup/rollup-win32-arm64-msvc@4.24.0": + version "4.24.0" + resolved "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz#71fa3ea369316db703a909c790743972e98afae5" + integrity sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ== + +"@rollup/rollup-win32-ia32-msvc@4.24.0": + version "4.24.0" + resolved "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz#653f5989a60658e17d7576a3996deb3902e342e2" + integrity sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ== + +"@rollup/rollup-win32-x64-msvc@4.24.0": + version "4.24.0" + resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz#0574d7e87b44ee8511d08cc7f914bcb802b70818" + integrity sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw== + +"@roochnetwork/rooch-sdk-kit@^0.2.1": + version "0.2.7" + resolved "https://registry.npmjs.org/@roochnetwork/rooch-sdk-kit/-/rooch-sdk-kit-0.2.7.tgz#b97d175ff48f2f42b4e38888eb8fd8dd6f072977" + integrity sha512-4RP41/3aAMxPHlMef9f8V45HRwXWW0M5fkWFCf12L5O1v0iaBxbL0O3pw8Lsu8ESd7BKdvk1ho5Qz7L+J/bcOg== + dependencies: + "@roochnetwork/rooch-sdk" "0.2.7" + "@vanilla-extract/css" "^1.15.5" + "@vanilla-extract/dynamic" "^2.1.2" + "@vanilla-extract/recipes" "^0.5.5" + clsx "^2.1.1" + zustand "^4.5.5" + +"@roochnetwork/rooch-sdk@0.2.7", "@roochnetwork/rooch-sdk@^0.2.1": + version "0.2.7" + resolved "https://registry.npmjs.org/@roochnetwork/rooch-sdk/-/rooch-sdk-0.2.7.tgz#c58dcc3a83ec8bbe587e3051a65e7825a73afeda" + integrity sha512-iQdoN06ijFLmdyPfOxgQGhcgl62vkhRKWUQ1fZO0+V2Kpu8wF4qN2Xg7nqZ8VCC5Kr1g9St2CmS60PXPjpCgPw== + dependencies: + "@mysten/bcs" "1.0.4" + "@noble/curves" "~1.6.0" + "@noble/hashes" "~1.5.0" + "@scure/base" "~1.1.6" + "@scure/bip32" "^1.5.0" + "@scure/bip39" "^1.4.0" + "@suchipi/femver" "^1.0.0" + bech32 "^2.0.0" + bs58check "4.0.0" + buffer "6.0.3" + tweetnacl "^1.0.3" + valibot "^0.41.0" + +"@scure/base@~1.1.6", "@scure/base@~1.1.7", "@scure/base@~1.1.8": + version "1.1.9" + resolved "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz#e5e142fbbfe251091f9c5f1dd4c834ac04c3dbd1" + integrity sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg== + +"@scure/bip32@^1.5.0": + version "1.5.0" + resolved "https://registry.npmjs.org/@scure/bip32/-/bip32-1.5.0.tgz#dd4a2e1b8a9da60e012e776d954c4186db6328e6" + integrity sha512-8EnFYkqEQdnkuGBVpCzKxyIwDCBLDVj3oiX0EKUFre/tOjL/Hqba1D6n/8RcmaQy4f95qQFrO2A8Sr6ybh4NRw== + dependencies: + "@noble/curves" "~1.6.0" + "@noble/hashes" "~1.5.0" + "@scure/base" "~1.1.7" + +"@scure/bip39@^1.4.0": + version "1.4.0" + resolved "https://registry.npmjs.org/@scure/bip39/-/bip39-1.4.0.tgz#664d4f851564e2e1d4bffa0339f9546ea55960a6" + integrity sha512-BEEm6p8IueV/ZTfQLp/0vhw4NPnT9oWf5+28nvmeUICjP99f4vr2d+qc7AVGDDtwRep6ifR43Yed9ERVmiITzw== + dependencies: + "@noble/hashes" "~1.5.0" + "@scure/base" "~1.1.8" + +"@suchipi/femver@^1.0.0": + version "1.0.0" + resolved "https://registry.npmjs.org/@suchipi/femver/-/femver-1.0.0.tgz#4909dcc069695e07bd23a64c4bfe411d11d9692f" + integrity sha512-bprE8+K5V+DPX7q2e2K57ImqNBdfGHDIWaGI5xHxZoxbKOuQZn4wzPiUxOAHnsUr3w3xHrWXwN7gnG/iIuEMIg== + +"@tanstack/query-core@5.59.16": + version "5.59.16" + resolved "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.16.tgz#aa4616e8a9c12afeef4cfbf3ed0f55f404d66e67" + integrity sha512-crHn+G3ltqb5JG0oUv6q+PMz1m1YkjpASrXTU+sYWW9pLk0t2GybUHNRqYPZWhxgjPaVGC4yp92gSFEJgYEsPw== + +"@tanstack/react-query@^5.51.9": + version "5.59.16" + resolved "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.59.16.tgz#1e701c6e6681965c04aa426df9da54b8edc6db1b" + integrity sha512-MuyWheG47h6ERd4PKQ6V8gDyBu3ThNG22e1fRVwvq6ap3EqsFhyuxCAwhNP/03m/mLg+DAb0upgbPaX6VB+CkQ== + dependencies: + "@tanstack/query-core" "5.59.16" + +"@types/babel__core@^7.20.5": + version "7.20.5" + resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" + integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.8" + resolved "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz#f836c61f48b1346e7d2b0d93c6dacc5b9535d3ab" + integrity sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.4" + resolved "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" + integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*": + version "7.20.6" + resolved "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz#8dc9f0ae0f202c08d8d4dab648912c8d6038e3f7" + integrity sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg== + dependencies: + "@babel/types" "^7.20.7" + +"@types/css-font-loading-module@^0.0.12": + version "0.0.12" + resolved "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz#65494833928823f998fbe8e86312821875d80db5" + integrity sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA== + +"@types/earcut@^2.1.0": + version "2.1.4" + resolved "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.4.tgz#5811d7d333048f5a7573b22ddc84923e69596da6" + integrity sha512-qp3m9PPz4gULB9MhjGID7wpo3gJ4bTGXm7ltNDsmOvsPduTeHp8wSW9YckBj3mljeOh4F0m2z/0JKAALRKbmLQ== + +"@types/estree@1.0.6": + version "1.0.6" + resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== + +"@types/parse-json@^4.0.0": + version "4.0.2" + resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" + integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== + +"@types/prop-types@*", "@types/prop-types@^15.7.12": + version "15.7.13" + resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz#2af91918ee12d9d32914feb13f5326658461b451" + integrity sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA== + +"@types/react-dom@^18.3.0": + version "18.3.1" + resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz#1e4654c08a9cdcfb6594c780ac59b55aad42fe07" + integrity sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ== + dependencies: + "@types/react" "*" + +"@types/react-gtm-module@^2.0.3": + version "2.0.4" + resolved "https://registry.npmjs.org/@types/react-gtm-module/-/react-gtm-module-2.0.4.tgz#27457712f7a17f44e752d2b7bd93e794a5fc6ff3" + integrity sha512-5wPMWsUE5AI6O0B0K1/zbs0rFHBKu+7NWXQwDXhqvA12ooLD6W1AYiWZqR4UiOd7ixZDV1H5Ys301zEsqyIfNg== + +"@types/react-transition-group@^4.4.10": + version "4.4.11" + resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz#d963253a611d757de01ebb241143b1017d5d63d5" + integrity sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@^18.3.3": + version "18.3.12" + resolved "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz#99419f182ccd69151813b7ee24b792fe08774f60" + integrity sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + +"@typescript-eslint/eslint-plugin@^7.13.1": + version "7.18.0" + resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz#b16d3cf3ee76bf572fdf511e79c248bdec619ea3" + integrity sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw== + dependencies: + "@eslint-community/regexpp" "^4.10.0" + "@typescript-eslint/scope-manager" "7.18.0" + "@typescript-eslint/type-utils" "7.18.0" + "@typescript-eslint/utils" "7.18.0" + "@typescript-eslint/visitor-keys" "7.18.0" + graphemer "^1.4.0" + ignore "^5.3.1" + natural-compare "^1.4.0" + ts-api-utils "^1.3.0" + +"@typescript-eslint/parser@^7.13.1": + version "7.18.0" + resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz#83928d0f1b7f4afa974098c64b5ce6f9051f96a0" + integrity sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg== + dependencies: + "@typescript-eslint/scope-manager" "7.18.0" + "@typescript-eslint/types" "7.18.0" + "@typescript-eslint/typescript-estree" "7.18.0" + "@typescript-eslint/visitor-keys" "7.18.0" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@7.18.0": + version "7.18.0" + resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz#c928e7a9fc2c0b3ed92ab3112c614d6bd9951c83" + integrity sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA== + dependencies: + "@typescript-eslint/types" "7.18.0" + "@typescript-eslint/visitor-keys" "7.18.0" + +"@typescript-eslint/type-utils@7.18.0": + version "7.18.0" + resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz#2165ffaee00b1fbbdd2d40aa85232dab6998f53b" + integrity sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA== + dependencies: + "@typescript-eslint/typescript-estree" "7.18.0" + "@typescript-eslint/utils" "7.18.0" + debug "^4.3.4" + ts-api-utils "^1.3.0" + +"@typescript-eslint/types@7.18.0": + version "7.18.0" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz#b90a57ccdea71797ffffa0321e744f379ec838c9" + integrity sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ== + +"@typescript-eslint/typescript-estree@7.18.0": + version "7.18.0" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz#b5868d486c51ce8f312309ba79bdb9f331b37931" + integrity sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA== + dependencies: + "@typescript-eslint/types" "7.18.0" + "@typescript-eslint/visitor-keys" "7.18.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^1.3.0" + +"@typescript-eslint/utils@7.18.0": + version "7.18.0" + resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz#bca01cde77f95fc6a8d5b0dbcbfb3d6ca4be451f" + integrity sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@typescript-eslint/scope-manager" "7.18.0" + "@typescript-eslint/types" "7.18.0" + "@typescript-eslint/typescript-estree" "7.18.0" + +"@typescript-eslint/visitor-keys@7.18.0": + version "7.18.0" + resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz#0564629b6124d67607378d0f0332a0495b25e7d7" + integrity sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg== + dependencies: + "@typescript-eslint/types" "7.18.0" + eslint-visitor-keys "^3.4.3" + +"@ungap/structured-clone@^1.2.0": + version "1.2.0" + resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + +"@vanilla-extract/css@^1.15.5": + version "1.16.0" + resolved "https://registry.npmjs.org/@vanilla-extract/css/-/css-1.16.0.tgz#d88276a7beae8953024b9b8dc4ae127dd6f9ab3a" + integrity sha512-05JTbvG1E0IrSZKZ5el2EM9CmAX0XSdsNY+d4aRZxDvYf3/hwxomvFFEz2b/awjgg9yTVHW83Wq19wE4OoTEMg== + dependencies: + "@emotion/hash" "^0.9.0" + "@vanilla-extract/private" "^1.0.6" + css-what "^6.1.0" + cssesc "^3.0.0" + csstype "^3.0.7" + dedent "^1.5.3" + deep-object-diff "^1.1.9" + deepmerge "^4.2.2" + lru-cache "^10.4.3" + media-query-parser "^2.0.2" + modern-ahocorasick "^1.0.0" + picocolors "^1.0.0" + +"@vanilla-extract/dynamic@^2.1.2": + version "2.1.2" + resolved "https://registry.npmjs.org/@vanilla-extract/dynamic/-/dynamic-2.1.2.tgz#b1d1c1e0e392934c5a3bbb53f99069a7721311ac" + integrity sha512-9BGMciD8rO1hdSPIAh1ntsG4LPD3IYKhywR7VOmmz9OO4Lx1hlwkSg3E6X07ujFx7YuBfx0GDQnApG9ESHvB2A== + dependencies: + "@vanilla-extract/private" "^1.0.6" + +"@vanilla-extract/private@^1.0.6": + version "1.0.6" + resolved "https://registry.npmjs.org/@vanilla-extract/private/-/private-1.0.6.tgz#f10bbf3189f7b827d0bd7f804a6219dd03ddbdd4" + integrity sha512-ytsG/JLweEjw7DBuZ/0JCN4WAQgM9erfSTdS1NQY778hFQSZ6cfCDEZZ0sgVm4k54uNz6ImKB33AYvSR//fjxw== + +"@vanilla-extract/recipes@^0.5.5": + version "0.5.5" + resolved "https://registry.npmjs.org/@vanilla-extract/recipes/-/recipes-0.5.5.tgz#da34e247be2c3d70e01ecfeb53310daadc608b74" + integrity sha512-VadU7+IFUwLNLMgks29AHav/K5h7DOEfTU91RItn5vwdPfzduodNg317YbgWCcpm7FSXkuR3B3X8ZOi95UOozA== + +"@vitejs/plugin-react@^4.3.1": + version "4.3.3" + resolved "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.3.tgz#28301ac6d7aaf20b73a418ee5c65b05519b4836c" + integrity sha512-NooDe9GpHGqNns1i8XDERg0Vsg5SSYRhRxxyTGogUdkdNt47jal+fbuYi+Yfq6pzRCKXyoPcWisfxE6RIM3GKA== + dependencies: + "@babel/core" "^7.25.2" + "@babel/plugin-transform-react-jsx-self" "^7.24.7" + "@babel/plugin-transform-react-jsx-source" "^7.24.7" + "@types/babel__core" "^7.20.5" + react-refresh "^0.14.2" + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.9.0: + version "8.13.0" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz#2a30d670818ad16ddd6a35d3842dacec9e5d7ca3" + integrity sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w== + +ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.1.0" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" + integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +any-promise@^1.0.0: + version "1.3.0" + resolved "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +arg@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" + integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +autoprefixer@^10.4.19: + version "10.4.20" + resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz#5caec14d43976ef42e32dcb4bd62878e96be5b3b" + integrity sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g== + dependencies: + browserslist "^4.23.3" + caniuse-lite "^1.0.30001646" + fraction.js "^4.3.7" + normalize-range "^0.1.2" + picocolors "^1.0.1" + postcss-value-parser "^4.2.0" + +babel-plugin-macros@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" + integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg== + dependencies: + "@babel/runtime" "^7.12.5" + cosmiconfig "^7.0.0" + resolve "^1.19.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base-x@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/base-x/-/base-x-5.0.0.tgz#6d835ceae379130e1a4cb846a70ac4746f28ea9b" + integrity sha512-sMW3VGSX1QWVFA6l8U62MLKz29rRfpTlYdCqLdpLo1/Yd4zZwSbnUaDfciIAowAqvq7YFnWq9hrhdg1KYgc1lQ== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bech32@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz#078d3686535075c8c79709f054b1b226a133b355" + integrity sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browserslist@^4.23.3, browserslist@^4.24.0: + version "4.24.2" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580" + integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg== + dependencies: + caniuse-lite "^1.0.30001669" + electron-to-chromium "^1.5.41" + node-releases "^2.0.18" + update-browserslist-db "^1.1.1" + +bs58@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz#a2cda0130558535dd281a2f8697df79caaf425d8" + integrity sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw== + dependencies: + base-x "^5.0.0" + +bs58check@4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/bs58check/-/bs58check-4.0.0.tgz#46cda52a5713b7542dcb78ec2efdf78f5bf1d23c" + integrity sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g== + dependencies: + "@noble/hashes" "^1.2.0" + bs58 "^6.0.0" + +buffer@6.0.3: + version "6.0.3" + resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase-css@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" + integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== + +caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001669: + version "1.0.30001669" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz#fda8f1d29a8bfdc42de0c170d7f34a9cf19ed7a3" + integrity sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w== + +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@^3.5.3: + version "3.6.0" + resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +clsx@^1.1.0: + version "1.2.1" + resolved "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" + integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== + +clsx@^2.1.0, clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +commander@^4.0.0: + version "4.1.1" + resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +convert-source-map@^1.5.0: + version "1.9.0" + resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +cosmiconfig@^7.0.0: + version "7.1.0" + resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" + integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + +countup.js@^2.8.0: + version "2.8.0" + resolved "https://registry.npmjs.org/countup.js/-/countup.js-2.8.0.tgz#64951f2df3ede28839413d654d8fef28251c32a8" + integrity sha512-f7xEhX0awl4NOElHulrl4XRfKoNH3rB+qfNSZZyjSZhaAoUk6elvhH+MNxMmlmuUJ2/QNTWPSA7U4mNtIAKljQ== + +cross-spawn@7.0.5, cross-spawn@^7.0.0, cross-spawn@^7.0.2: + version "7.0.5" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz#910aac880ff5243da96b728bc6521a5f6c2f2f82" + integrity sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +css-what@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +csstype@^3.0.2, csstype@^3.0.7, csstype@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + +debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.7" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + +dedent@^1.5.3: + version "1.5.3" + resolved "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz#99aee19eb9bae55a67327717b6e848d0bf777e5a" + integrity sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ== + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +deep-object-diff@^1.1.9: + version "1.1.9" + resolved "https://registry.npmjs.org/deep-object-diff/-/deep-object-diff-1.1.9.tgz#6df7ef035ad6a0caa44479c536ed7b02570f4595" + integrity sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA== + +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +didyoumean@^1.2.2: + version "1.2.2" + resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" + integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +dlv@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" + integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dom-helpers@^5.0.1: + version "5.2.1" + resolved "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + +earcut@^2.2.4: + version "2.2.4" + resolved "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz#6d02fd4d68160c114825d06890a92ecaae60343a" + integrity sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ== + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +electron-to-chromium@^1.5.41: + version "1.5.43" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.43.tgz#d9e69fc709ddebd521416de9d17cdef81d2d4718" + integrity sha512-NxnmFBHDl5Sachd2P46O7UJiMaMHMLSofoIWVJq3mj8NJgG0umiSeljAVP9lGzjI0UDLJJ5jjoGjcrB8RSbjLQ== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +esbuild@^0.21.3: + version "0.21.5" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" + integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== + optionalDependencies: + "@esbuild/aix-ppc64" "0.21.5" + "@esbuild/android-arm" "0.21.5" + "@esbuild/android-arm64" "0.21.5" + "@esbuild/android-x64" "0.21.5" + "@esbuild/darwin-arm64" "0.21.5" + "@esbuild/darwin-x64" "0.21.5" + "@esbuild/freebsd-arm64" "0.21.5" + "@esbuild/freebsd-x64" "0.21.5" + "@esbuild/linux-arm" "0.21.5" + "@esbuild/linux-arm64" "0.21.5" + "@esbuild/linux-ia32" "0.21.5" + "@esbuild/linux-loong64" "0.21.5" + "@esbuild/linux-mips64el" "0.21.5" + "@esbuild/linux-ppc64" "0.21.5" + "@esbuild/linux-riscv64" "0.21.5" + "@esbuild/linux-s390x" "0.21.5" + "@esbuild/linux-x64" "0.21.5" + "@esbuild/netbsd-x64" "0.21.5" + "@esbuild/openbsd-x64" "0.21.5" + "@esbuild/sunos-x64" "0.21.5" + "@esbuild/win32-arm64" "0.21.5" + "@esbuild/win32-ia32" "0.21.5" + "@esbuild/win32-x64" "0.21.5" + +escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-plugin-react-hooks@^4.6.2: + version "4.6.2" + resolved "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz#c829eb06c0e6f484b3fbb85a97e57784f328c596" + integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ== + +eslint-plugin-react-refresh@^0.4.7: + version "0.4.13" + resolved "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.13.tgz#ed7330da09b6192e6fa9b1b217ad979afbc898bf" + integrity sha512-f1EppwrpJRWmqDTyvAyomFVDYRtrS7iTEqv3nokETnMiMzs2SSTmKRTACce4O2p4jYyowiSMvpdwC/RLcMFhuQ== + +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint@^8.57.0: + version "8.57.1" + resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" + integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.57.1" + "@humanwhocodes/config-array" "^0.13.0" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + "@ungap/structured-clone" "^1.2.0" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" + esquery "^1.4.2" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + strip-ansi "^6.0.1" + text-table "^0.2.0" + +espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== + dependencies: + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" + +esquery@^1.4.2: + version "1.6.0" + resolved "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.9, fast-glob@^3.3.0: + version "3.3.2" + resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastq@^1.6.0: + version "1.17.1" + resolved "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== + dependencies: + reusify "^1.0.4" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" + integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.2.0" + resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.3" + rimraf "^3.0.2" + +flatted@^3.2.9: + version "3.3.1" + resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" + integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== + +foreground-child@^3.1.0: + version "3.3.0" + resolved "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" + integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + +fraction.js@^4.3.7: + version "4.3.7" + resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" + integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@^10.3.10: + version "10.4.5" + resolved "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^13.19.0: + version "13.24.0" + resolved "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== + dependencies: + type-fest "^0.20.2" + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +goober@^2.0.33: + version "2.1.16" + resolved "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz#7d548eb9b83ff0988d102be71f271ca8f9c82a95" + integrity sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g== + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +hasown@^2.0.0, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +hoist-non-react-statics@^3.3.1: + version "3.3.2" + resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore@^5.2.0, ignore@^5.3.1: + version "5.3.2" + resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.13.0: + version "2.15.1" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" + integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== + dependencies: + hasown "^2.0.2" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +ismobilejs@^1.1.0: + version "1.1.1" + resolved "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz#c56ca0ae8e52b24ca0f22ba5ef3215a2ddbbaa0e" + integrity sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw== + +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +jiti@^1.21.0: + version "1.21.6" + resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" + integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsesc@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" + integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lilconfig@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" + integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== + +lilconfig@^3.0.0: + version "3.1.2" + resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz#e4a7c3cb549e3a606c8dcc32e5ae1005e62c05cb" + integrity sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow== + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.isnil@4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz#49e28cd559013458c814c5479d3c663a21bfaa6c" + integrity sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng== + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.times@4.3.2: + version "4.3.2" + resolved "https://registry.npmjs.org/lodash.times/-/lodash.times-4.3.2.tgz#3e1f2565c431754d54ab57f2ed1741939285ca1d" + integrity sha512-FfaJzl0SA35CRPDh5SWe2BTght6y5KSK7yJv166qIp/8q7qOwBDCvuDZE2RUSMRpBkLF6rZKbLEUoTmaP3qg6A== + +loose-envify@^1.1.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lru-cache@^10.2.0, lru-cache@^10.4.3: + version "10.4.3" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +media-query-parser@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/media-query-parser/-/media-query-parser-2.0.2.tgz#ff79e56cee92615a304a1c2fa4f2bd056c0a1d29" + integrity sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w== + dependencies: + "@babel/runtime" "^7.12.5" + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4, micromatch@^4.0.5: + version "4.0.8" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + +modern-ahocorasick@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/modern-ahocorasick/-/modern-ahocorasick-1.0.1.tgz#dec373444f51b5458ac05216a8ec376e126dd283" + integrity sha512-yoe+JbhTClckZ67b2itRtistFKf8yPYelHLc7e5xAwtNAXxM6wJTUx2C7QeVSJFDzKT7bCIFyBVybPMKvmB9AA== + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +mz@^2.7.0: + version "2.7.0" + resolved "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +node-releases@^2.0.18: + version "2.0.18" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== + +notistack@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/notistack/-/notistack-3.0.1.tgz#daf59888ab7e2c30a1fa8f71f9cba2978773236e" + integrity sha512-ntVZXXgSQH5WYfyU+3HfcXuKaapzAJ8fBLQ/G618rn3yvSzEbnOB8ZSOwhX+dAORy/lw+GC2N061JA0+gYWTVA== + dependencies: + clsx "^1.1.0" + goober "^2.0.33" + +object-assign@^4.0.1, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== + +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.5" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +performance-now@2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== + +picocolors@^1.0.0, picocolors@^1.0.1, picocolors@^1.1.0: + version "1.1.1" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pify@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== + +pirates@^4.0.1: + version "4.0.6" + resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" + integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== + +pixi.js@7: + version "7.4.2" + resolved "https://registry.npmjs.org/pixi.js/-/pixi.js-7.4.2.tgz#d0378706f3db6df9e3f8917e22be04a99559995c" + integrity sha512-TifqgHGNofO7UCEbdZJOpUu7dUnpu4YZ0o76kfCqxDa4RS8ITc9zjECCbtalmuNXkVhSEZmBKQvE7qhHMqw/xg== + dependencies: + "@pixi/accessibility" "7.4.2" + "@pixi/app" "7.4.2" + "@pixi/assets" "7.4.2" + "@pixi/compressed-textures" "7.4.2" + "@pixi/core" "7.4.2" + "@pixi/display" "7.4.2" + "@pixi/events" "7.4.2" + "@pixi/extensions" "7.4.2" + "@pixi/extract" "7.4.2" + "@pixi/filter-alpha" "7.4.2" + "@pixi/filter-blur" "7.4.2" + "@pixi/filter-color-matrix" "7.4.2" + "@pixi/filter-displacement" "7.4.2" + "@pixi/filter-fxaa" "7.4.2" + "@pixi/filter-noise" "7.4.2" + "@pixi/graphics" "7.4.2" + "@pixi/mesh" "7.4.2" + "@pixi/mesh-extras" "7.4.2" + "@pixi/mixin-cache-as-bitmap" "7.4.2" + "@pixi/mixin-get-child-by-name" "7.4.2" + "@pixi/mixin-get-global-position" "7.4.2" + "@pixi/particle-container" "7.4.2" + "@pixi/prepare" "7.4.2" + "@pixi/sprite" "7.4.2" + "@pixi/sprite-animated" "7.4.2" + "@pixi/sprite-tiling" "7.4.2" + "@pixi/spritesheet" "7.4.2" + "@pixi/text" "7.4.2" + "@pixi/text-bitmap" "7.4.2" + "@pixi/text-html" "7.4.2" + +postcss-import@^15.1.0: + version "15.1.0" + resolved "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70" + integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew== + dependencies: + postcss-value-parser "^4.0.0" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-js@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz#61598186f3703bab052f1c4f7d805f3991bee9d2" + integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw== + dependencies: + camelcase-css "^2.0.1" + +postcss-load-config@^4.0.1: + version "4.0.2" + resolved "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz#7159dcf626118d33e299f485d6afe4aff7c4a3e3" + integrity sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ== + dependencies: + lilconfig "^3.0.0" + yaml "^2.3.4" + +postcss-nested@^6.0.1: + version "6.2.0" + resolved "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz#4c2d22ab5f20b9cb61e2c5c5915950784d068131" + integrity sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ== + dependencies: + postcss-selector-parser "^6.1.1" + +postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.1.1: + version "6.1.2" + resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de" + integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss@^8.4.23, postcss@^8.4.39, postcss@^8.4.43: + version "8.4.47" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365" + integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ== + dependencies: + nanoid "^3.3.7" + picocolors "^1.1.0" + source-map-js "^1.2.1" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prop-types@^15.6.2, prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +qs@^6.12.3: + version "6.13.0" + resolved "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +react-countup@^6.5.3: + version "6.5.3" + resolved "https://registry.npmjs.org/react-countup/-/react-countup-6.5.3.tgz#e892aa3eab2d6ba9c3cdba30bf4ed6764826d848" + integrity sha512-udnqVQitxC7QWADSPDOxVWULkLvKUWrDapn5i53HE4DPRVgs+Y5rr4bo25qEl8jSh+0l2cToJgGMx+clxPM3+w== + dependencies: + countup.js "^2.8.0" + +react-dom@^18.3.1: + version "18.3.1" + resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" + integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.2" + +react-gtm-module@^2.0.11: + version "2.0.11" + resolved "https://registry.npmjs.org/react-gtm-module/-/react-gtm-module-2.0.11.tgz#14484dac8257acd93614e347c32da9c5ac524206" + integrity sha512-8gyj4TTxeP7eEyc2QKawEuQoAZdjKvMY4pgWfycGmqGByhs17fR+zEBs0JUDq4US/l+vbTl+6zvUIx27iDo/Vw== + +react-is@^16.13.1, react-is@^16.7.0: + version "16.13.1" + resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-is@^18.3.1: + version "18.3.1" + resolved "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" + integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== + +react-refresh@^0.14.2: + version "0.14.2" + resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9" + integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA== + +react-transition-group@^4.4.5: + version "4.4.5" + resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + +react@^18.3.1: + version "18.3.1" + resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" + integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== + dependencies: + loose-envify "^1.1.0" + +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" + integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== + dependencies: + pify "^2.3.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve@^1.1.7, resolve@^1.19.0, resolve@^1.22.2: + version "1.22.8" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rollup@^4.20.0: + version "4.24.0" + resolved "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz#c14a3576f20622ea6a5c9cad7caca5e6e9555d05" + integrity sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg== + dependencies: + "@types/estree" "1.0.6" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.24.0" + "@rollup/rollup-android-arm64" "4.24.0" + "@rollup/rollup-darwin-arm64" "4.24.0" + "@rollup/rollup-darwin-x64" "4.24.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.24.0" + "@rollup/rollup-linux-arm-musleabihf" "4.24.0" + "@rollup/rollup-linux-arm64-gnu" "4.24.0" + "@rollup/rollup-linux-arm64-musl" "4.24.0" + "@rollup/rollup-linux-powerpc64le-gnu" "4.24.0" + "@rollup/rollup-linux-riscv64-gnu" "4.24.0" + "@rollup/rollup-linux-s390x-gnu" "4.24.0" + "@rollup/rollup-linux-x64-gnu" "4.24.0" + "@rollup/rollup-linux-x64-musl" "4.24.0" + "@rollup/rollup-win32-arm64-msvc" "4.24.0" + "@rollup/rollup-win32-ia32-msvc" "4.24.0" + "@rollup/rollup-win32-x64-msvc" "4.24.0" + fsevents "~2.3.2" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +scheduler@^0.23.2: + version "0.23.2" + resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" + integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== + dependencies: + loose-envify "^1.1.0" + +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.6.0: + version "7.6.3" + resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +source-map@^0.5.7: + version "0.5.7" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== + +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0: + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +stylis@4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" + integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== + +sucrase@^3.32.0: + version "3.35.0" + resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz#57f17a3d7e19b36d8995f06679d121be914ae263" + integrity sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA== + dependencies: + "@jridgewell/gen-mapping" "^0.3.2" + commander "^4.0.0" + glob "^10.3.10" + lines-and-columns "^1.1.6" + mz "^2.7.0" + pirates "^4.0.1" + ts-interface-checker "^0.1.9" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tailwindcss@^3.4.6: + version "3.4.14" + resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.14.tgz#6dd23a7f54ec197b19159e91e3bb1e55e7aa73ac" + integrity sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA== + dependencies: + "@alloc/quick-lru" "^5.2.0" + arg "^5.0.2" + chokidar "^3.5.3" + didyoumean "^1.2.2" + dlv "^1.1.3" + fast-glob "^3.3.0" + glob-parent "^6.0.2" + is-glob "^4.0.3" + jiti "^1.21.0" + lilconfig "^2.1.0" + micromatch "^4.0.5" + normalize-path "^3.0.0" + object-hash "^3.0.0" + picocolors "^1.0.0" + postcss "^8.4.23" + postcss-import "^15.1.0" + postcss-js "^4.0.1" + postcss-load-config "^4.0.1" + postcss-nested "^6.0.1" + postcss-selector-parser "^6.0.11" + resolve "^1.22.2" + sucrase "^3.32.0" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.1" + resolved "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" + integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== + dependencies: + any-promise "^1.0.0" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +ts-api-utils@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" + integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== + +ts-interface-checker@^0.1.9: + version "0.1.13" + resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" + integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== + +tweetnacl@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" + integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +typescript@^5.2.2: + version "5.6.3" + resolved "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b" + integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw== + +update-browserslist-db@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5" + integrity sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +url@^0.11.0: + version "0.11.4" + resolved "https://registry.npmjs.org/url/-/url-0.11.4.tgz#adca77b3562d56b72746e76b330b7f27b6721f3c" + integrity sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg== + dependencies: + punycode "^1.4.1" + qs "^6.12.3" + +use-sync-external-store@1.2.2: + version "1.2.2" + resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" + integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== + +util-deprecate@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +valibot@^0.41.0: + version "0.41.0" + resolved "https://registry.npmjs.org/valibot/-/valibot-0.41.0.tgz#5c2efd49c078e455f7862379365f6036f3cd9f96" + integrity sha512-igDBb8CTYr8YTQlOKgaN9nSS0Be7z+WRuaeYqGf3Cjz3aKmSnqEmYnkfVjzIuumGqfHpa3fLIvMEAfhrpqN8ng== + +vite@^5.3.1: + version "5.4.10" + resolved "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz#d358a7bd8beda6cf0f3b7a450a8c7693a4f80c18" + integrity sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ== + dependencies: + esbuild "^0.21.3" + postcss "^8.4.43" + rollup "^4.20.0" + optionalDependencies: + fsevents "~2.3.3" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yaml@^2.3.4: + version "2.6.0" + resolved "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz#14059ad9d0b1680d0f04d3a60fe00f3a857303c3" + integrity sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zustand@^4.5.5: + version "4.5.5" + resolved "https://registry.npmjs.org/zustand/-/zustand-4.5.5.tgz#f8c713041543715ec81a2adda0610e1dc82d4ad1" + integrity sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q== + dependencies: + use-sync-external-store "1.2.2"