diff --git a/.DS_Store b/.DS_Store
deleted file mode 100644
index b0006661c..000000000
Binary files a/.DS_Store and /dev/null differ
diff --git a/.github/workflows/STG-tamagotchi-battle-new.yml b/.github/workflows/STG-tamagotchi-battle-new.yml
index b7e60537a..3fa1ee256 100644
--- a/.github/workflows/STG-tamagotchi-battle-new.yml
+++ b/.github/workflows/STG-tamagotchi-battle-new.yml
@@ -5,7 +5,7 @@ on:
push:
branches: ["master", "main"]
paths:
- - frontend/apps/tamagotchi-battle-new/**
+ - frontend/apps/web3-warriors-battle/**
- frontend/packages/**
concurrency:
@@ -71,7 +71,7 @@ jobs:
- name: Build and push image
uses: docker/build-push-action@v5
with:
- file: frontend/apps/tamagotchi-battle-new/Dockerfile
+ file: frontend/apps/web3-warriors-battle/Dockerfile
push: true
tags: ${{ needs.prepair.outputs.image_name }}
build-args: |
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..4e52b6ca3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+# misc
+.DS_Store
diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock
index e5f871b1f..7459fb5c3 100644
--- a/contracts/Cargo.lock
+++ b/contracts/Cargo.lock
@@ -459,7 +459,7 @@ dependencies = [
"futures-lite",
"parking",
"polling",
- "rustix 0.38.37",
+ "rustix 0.38.38",
"slab",
"tracing",
"windows-sys 0.59.0",
@@ -511,7 +511,7 @@ dependencies = [
"cfg-if",
"event-listener 5.3.1",
"futures-lite",
- "rustix 0.38.37",
+ "rustix 0.38.38",
"tracing",
]
@@ -527,7 +527,7 @@ dependencies = [
"cfg-if",
"futures-core",
"futures-io",
- "rustix 0.38.37",
+ "rustix 0.38.38",
"signal-hook-registry",
"slab",
"windows-sys 0.59.0",
@@ -2502,9 +2502,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-lite"
-version = "2.3.0"
+version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5"
+checksum = "3f1fa2f9765705486b33fd2acf1577f8ec449c2ba1f318ae5447697b7c08d210"
dependencies = [
"fastrand",
"futures-core",
@@ -2573,21 +2573,25 @@ dependencies = [
name = "galactic-express"
version = "1.1.0"
dependencies = [
- "galactic-express-io",
+ "galactic-express-app",
"gear-wasm-builder",
- "gstd",
- "gtest",
- "num-traits",
+ "sails-client-gen",
+ "sails-idl-gen",
+ "sails-rs",
]
[[package]]
-name = "galactic-express-io"
+name = "galactic-express-app"
version = "1.1.0"
dependencies = [
- "gmeta",
+ "galactic-express",
+ "gclient",
+ "gear-core",
"gstd",
- "parity-scale-codec",
- "scale-info",
+ "gtest",
+ "num-traits",
+ "sails-rs",
+ "tokio",
]
[[package]]
@@ -3920,7 +3924,7 @@ dependencies = [
"http 1.1.0",
"jsonrpsee-core 0.23.2",
"pin-project",
- "rustls 0.23.15",
+ "rustls 0.23.16",
"rustls-pki-types",
"rustls-platform-verifier",
"soketto 0.8.0",
@@ -4185,9 +4189,9 @@ dependencies = [
[[package]]
name = "libm"
-version = "0.2.8"
+version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
+checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
[[package]]
name = "libredox"
@@ -4403,7 +4407,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2cffa4ad52c6f791f4f8b15f0c05f9824b2ced1160e88cc393d64fff9a8ac64"
dependencies = [
- "rustix 0.38.37",
+ "rustix 0.38.38",
]
[[package]]
@@ -5115,7 +5119,7 @@ dependencies = [
"concurrent-queue",
"hermit-abi 0.4.0",
"pin-project-lite",
- "rustix 0.38.37",
+ "rustix 0.38.38",
"tracing",
"windows-sys 0.59.0",
]
@@ -5714,9 +5718,9 @@ dependencies = [
[[package]]
name = "rustix"
-version = "0.38.37"
+version = "0.38.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
+checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a"
dependencies = [
"bitflags 2.6.0",
"errno",
@@ -5753,9 +5757,9 @@ dependencies = [
[[package]]
name = "rustls"
-version = "0.23.15"
+version = "0.23.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fbb44d7acc4e873d613422379f69f237a1b141928c02f6bc6ccfddddc2d7993"
+checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e"
dependencies = [
"log",
"once_cell",
@@ -5826,7 +5830,7 @@ dependencies = [
"jni",
"log",
"once_cell",
- "rustls 0.23.15",
+ "rustls 0.23.16",
"rustls-native-certs 0.7.3",
"rustls-platform-verifier-android",
"rustls-webpki 0.102.8",
@@ -6296,9 +6300,9 @@ dependencies = [
[[package]]
name = "serde"
-version = "1.0.213"
+version = "1.0.214"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1"
+checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
dependencies = [
"serde_derive",
]
@@ -6325,9 +6329,9 @@ dependencies = [
[[package]]
name = "serde_derive"
-version = "1.0.213"
+version = "1.0.214"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5"
+checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
dependencies = [
"proc-macro2",
"quote",
@@ -7524,7 +7528,7 @@ dependencies = [
"cfg-if",
"fastrand",
"once_cell",
- "rustix 0.38.37",
+ "rustix 0.38.38",
"windows-sys 0.59.0",
]
@@ -7745,7 +7749,7 @@ version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
dependencies = [
- "rustls 0.23.15",
+ "rustls 0.23.16",
"rustls-pki-types",
"tokio",
]
@@ -8763,7 +8767,7 @@ dependencies = [
"either",
"home",
"once_cell",
- "rustix 0.38.37",
+ "rustix 0.38.38",
]
[[package]]
diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml
index 093e9dd67..5721a5022 100644
--- a/contracts/Cargo.toml
+++ b/contracts/Cargo.toml
@@ -12,7 +12,7 @@ members = [
"car-races/car-3",
"car-races/wasm",
"concert/wasm",
- "galactic-express",
+ "galactic-express/wasm",
"multisig-wallet",
"multisig-wallet/state",
"oracle",
@@ -70,7 +70,6 @@ tamagotchi-battle-state.path = "tamagotchi-battle/state"
battleship-io.path = "battleship/io"
car-races-io.path = "car-races/io"
-galactic-express-io.path = "galactic-express/io"
multi-token-io.path = "multi-token/io"
multisig-wallet-io.path = "multisig-wallet/io"
ping-io.path = "ping/io"
diff --git a/contracts/battle/app/src/services/game/utils.rs b/contracts/battle/app/src/services/game/utils.rs
index 41ed18ce3..4a15c9f06 100644
--- a/contracts/battle/app/src/services/game/utils.rs
+++ b/contracts/battle/app/src/services/game/utils.rs
@@ -117,24 +117,6 @@ impl Battle {
}
}
- pub fn delete_players(&mut self, loser_1: &ActorId, loser_2: &ActorId, pair_id: u16) {
- let player_loser_1 = self
- .participants
- .remove(loser_1)
- .expect("The player must exist");
- let player_loser_2 = self
- .participants
- .remove(loser_2)
- .expect("The player must exist");
-
- self.defeated_participants.insert(*loser_1, player_loser_1);
- self.defeated_participants.insert(*loser_2, player_loser_2);
-
- self.pairs.remove(&pair_id);
- self.players_to_pairs.remove(loser_1);
- self.players_to_pairs.remove(loser_2);
- }
-
pub fn check_min_player_amount(&self) -> Result<(), BattleError> {
if self.participants.len() <= 1 {
return Err(BattleError::NotEnoughPlayers);
@@ -183,6 +165,7 @@ impl Battle {
self.pairs.insert(self.pair_id, pair);
self.players_to_pairs.insert(player.owner, self.pair_id);
self.waiting_player = Some((player.owner, self.pair_id));
+ self.pair_id += 1;
}
}
pub fn send_delayed_message_make_move_from_reservation(&mut self, time_for_move: u32) {
diff --git a/contracts/battle/tests/gtest.rs b/contracts/battle/tests/gtest.rs
index 28f2160ae..777d160b5 100644
--- a/contracts/battle/tests/gtest.rs
+++ b/contracts/battle/tests/gtest.rs
@@ -142,7 +142,7 @@ async fn test() {
println!("\n RES {:?}", result);
- make_move(&mut service_client, Move::Attack, USER_1, program_id)
+ make_move(&mut service_client, Move::Attack, USER_2, program_id)
.await
.unwrap();
@@ -170,7 +170,7 @@ async fn test() {
service_client
.start_next_fight()
- .with_args(GTestArgs::new(USER_2.into()))
+ .with_args(GTestArgs::new(USER_3.into()))
.send_recv(program_id)
.await
.unwrap();
diff --git a/contracts/galactic-express/.DS_Store b/contracts/galactic-express/.DS_Store
deleted file mode 100644
index 866c1fe2f..000000000
Binary files a/contracts/galactic-express/.DS_Store and /dev/null differ
diff --git a/contracts/galactic-express/.binpath b/contracts/galactic-express/.binpath
deleted file mode 100644
index b4c198035..000000000
--- a/contracts/galactic-express/.binpath
+++ /dev/null
@@ -1 +0,0 @@
-../target/wasm32-unknown-unknown/release/galactic_express
\ No newline at end of file
diff --git a/contracts/galactic-express/Cargo.toml b/contracts/galactic-express/Cargo.toml
deleted file mode 100644
index f73c4ed1c..000000000
--- a/contracts/galactic-express/Cargo.toml
+++ /dev/null
@@ -1,17 +0,0 @@
-[package]
-name = "galactic-express"
-version.workspace = true
-edition.workspace = true
-publish.workspace = true
-
-[dependencies]
-gstd.workspace = true
-galactic-express-io.workspace = true
-num-traits.workspace = true
-
-[dev-dependencies]
-gtest.workspace = true
-
-[build-dependencies]
-gear-wasm-builder.workspace = true
-galactic-express-io.workspace = true
diff --git a/contracts/galactic-express/README.md b/contracts/galactic-express/README.md
index 85949cbb0..f767052bb 100644
--- a/contracts/galactic-express/README.md
+++ b/contracts/galactic-express/README.md
@@ -1,6 +1,3 @@
-[![Open in Gitpod](https://img.shields.io/badge/Open_in-Gitpod-white?logo=gitpod)](https://gitpod.io/#FOLDER=galactic-express/https://github.com/gear-foundation/dapps)
-[![Docs](https://img.shields.io/github/actions/workflow/status/gear-foundation/dapps/contracts.yml?logo=rust&label=docs)](https://dapps.gear.rs/galactic_express_io)
-
# [Galactic Express](https://wiki.gear-tech.io/docs/examples/Gaming/galactic-express)
Galactic Express (GalEx) is a 100% on-chain PvE economic game.
@@ -16,7 +13,7 @@ cargo b -r -p "galactic-express"
### ✅ Testing
```sh
-cargo t -r -p "galactic-express"
+cargo t -r -p "galactic-express-app"
```
## Stages
diff --git a/contracts/galactic-express/app/Cargo.toml b/contracts/galactic-express/app/Cargo.toml
new file mode 100644
index 000000000..8ecca8b11
--- /dev/null
+++ b/contracts/galactic-express/app/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "galactic-express-app"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+
+[dependencies]
+gstd = { workspace = true, features = ["debug"] }
+sails-rs = { workspace = true, features = ["gtest"] }
+num-traits.workspace = true
+
+[dev-dependencies]
+gtest.workspace = true
+gstd.workspace = true
+gear-core.workspace = true
+gclient.workspace = true
+galactic-express = { path = "../wasm" }
+tokio = "1"
diff --git a/contracts/galactic-express/app/src/lib.rs b/contracts/galactic-express/app/src/lib.rs
new file mode 100644
index 000000000..52bbb4705
--- /dev/null
+++ b/contracts/galactic-express/app/src/lib.rs
@@ -0,0 +1,19 @@
+#![no_std]
+#![allow(clippy::new_without_default)]
+
+use sails_rs::prelude::*;
+mod services;
+use services::galactic_express::GameService;
+pub struct Program(());
+
+#[program]
+impl Program {
+ pub async fn new(dns_id_and_name: Option<(ActorId, String)>) -> Self {
+ GameService::init(dns_id_and_name).await;
+ Self(())
+ }
+
+ pub fn galactic_express(&self) -> GameService {
+ GameService::new()
+ }
+}
diff --git a/contracts/galactic-express/app/src/services/galactic_express/funcs.rs b/contracts/galactic-express/app/src/services/galactic_express/funcs.rs
new file mode 100644
index 000000000..bd6b6e3a8
--- /dev/null
+++ b/contracts/galactic-express/app/src/services/galactic_express/funcs.rs
@@ -0,0 +1,366 @@
+use crate::services::galactic_express::utils;
+use crate::services::galactic_express::{
+ Event, Game, GameError, HaltReason, Participant, Random, Results, Stage, Storage, Turn,
+ Weather, MAX_FUEL, MAX_PARTICIPANTS, MAX_PAYLOAD, PENALTY_LEVEL, REWARD, TURNS, TURN_ALTITUDE,
+};
+use sails_rs::{collections::HashMap, gstd::msg, prelude::*};
+
+pub fn create_new_session(storage: &mut Storage, name: String) -> Result {
+ let msg_src = msg::source();
+ let msg_value = msg::value();
+
+ if storage.player_to_game_id.contains_key(&msg_src) {
+ return Err(GameError::SeveralRegistrations);
+ }
+
+ let game = storage.games.entry(msg_src).or_insert_with(|| Game {
+ admin: msg_src,
+ admin_name: name,
+ bid: msg_value,
+ ..Default::default()
+ });
+
+ let stage = &mut game.stage;
+
+ match stage {
+ Stage::Registration(participants) => {
+ participants.clear();
+ }
+ Stage::Results { .. } => {
+ *stage = Stage::Registration(HashMap::::new())
+ }
+ }
+
+ let mut random = Random::new()?;
+
+ game.weather = match random.next() % (Weather::Tornado as u8 + 1) {
+ 0 => Weather::Clear,
+ 1 => Weather::Cloudy,
+ 2 => Weather::Rainy,
+ 3 => Weather::Stormy,
+ 4 => Weather::Thunder,
+ 5 => Weather::Tornado,
+ _ => unreachable!(),
+ };
+ game.altitude = random.generate(TURN_ALTITUDE.0, TURN_ALTITUDE.1) * TURNS as u16;
+ game.reward = random.generate(REWARD.0, REWARD.1);
+ storage.player_to_game_id.insert(msg_src, msg_src);
+
+ Ok(Event::NewSessionCreated {
+ altitude: game.altitude,
+ weather: game.weather,
+ reward: game.reward,
+ bid: msg_value,
+ })
+}
+
+pub fn cancel_game(storage: &mut Storage) -> Result {
+ let msg_src = msg::source();
+ let game = storage.games.get(&msg_src).ok_or(GameError::NoSuchGame)?;
+
+ match &game.stage {
+ Stage::Registration(players) => {
+ players.iter().for_each(|(id, _)| {
+ send_value(*id, game.bid);
+ storage.player_to_game_id.remove(id);
+ });
+ }
+ Stage::Results(results) => {
+ results.rankings.iter().for_each(|(id, _)| {
+ storage.player_to_game_id.remove(id);
+ });
+ }
+ }
+
+ storage.player_to_game_id.remove(&msg_src);
+ storage.games.remove(&msg_src);
+ Ok(Event::GameCanceled)
+}
+
+pub fn leave_game(storage: &mut Storage) -> Result {
+ let msg_src = msg::source();
+ storage.player_to_game_id.remove(&msg_src);
+ Ok(Event::GameLeft)
+}
+
+pub fn register(
+ storage: &mut Storage,
+ creator: ActorId,
+ participant: Participant,
+) -> Result {
+ let msg_source = msg::source();
+ let msg_value = msg::value();
+ let reply = register_for_session(storage, creator, participant, msg_source, msg_value);
+ if reply.is_err() {
+ send_value(msg_source, msg_value);
+ }
+ reply
+}
+
+fn register_for_session(
+ storage: &mut Storage,
+ creator: ActorId,
+ participant: Participant,
+ msg_source: ActorId,
+ msg_value: u128,
+) -> Result {
+ if storage.player_to_game_id.contains_key(&msg_source) {
+ return Err(GameError::SeveralRegistrations);
+ }
+
+ if let Some(game) = storage.games.get_mut(&creator) {
+ if msg_value != game.bid {
+ return Err(GameError::WrongBid);
+ }
+ if let Stage::Results(_) = game.stage {
+ return Err(GameError::SessionEnded);
+ }
+
+ let participants = game.stage.mut_participants()?;
+
+ if participants.contains_key(&msg_source) {
+ return Err(GameError::AlreadyRegistered);
+ }
+
+ if participants.len() >= MAX_PARTICIPANTS - 1 {
+ return Err(GameError::SessionFull);
+ }
+
+ participant.check()?;
+ participants.insert(msg_source, participant.clone());
+ storage.player_to_game_id.insert(msg_source, creator);
+
+ Ok(Event::Registered(msg_source, participant))
+ } else {
+ Err(GameError::NoSuchGame)
+ }
+}
+
+pub fn cancel_register(storage: &mut Storage) -> Result {
+ let msg_source = msg::source();
+
+ let creator = storage
+ .player_to_game_id
+ .get(&msg_source)
+ .ok_or(GameError::Unregistered)?;
+ let game = storage
+ .games
+ .get_mut(creator)
+ .ok_or(GameError::NoSuchGame)?;
+
+ if msg_source != game.admin {
+ let participants = game.stage.mut_participants()?;
+ if participants.contains_key(&msg_source) {
+ send_value(msg_source, game.bid);
+ participants.remove(&msg_source).expect("Critical error");
+ storage.player_to_game_id.remove(&msg_source);
+ } else {
+ return Err(GameError::NoSuchPlayer);
+ }
+ Ok(Event::RegistrationCanceled)
+ } else {
+ Err(GameError::NotForAdmin)
+ }
+}
+
+pub fn delete_player(storage: &mut Storage, player_id: ActorId) -> Result {
+ let msg_source = msg::source();
+
+ if let Some(game) = storage.games.get_mut(&msg_source) {
+ if let Stage::Results(_) = game.stage {
+ return Err(GameError::SessionEnded);
+ }
+
+ let participants = game.stage.mut_participants()?;
+
+ if participants.contains_key(&player_id) {
+ send_value(player_id, game.bid);
+ participants.remove(&player_id).expect("Critical error");
+ storage.player_to_game_id.remove(&player_id);
+ } else {
+ return Err(GameError::NoSuchPlayer);
+ }
+
+ Ok(Event::PlayerDeleted { player_id })
+ } else {
+ Err(GameError::NoSuchGame)
+ }
+}
+
+pub fn start_game(
+ storage: &mut Storage,
+ fuel_amount: u8,
+ payload_amount: u8,
+) -> Result {
+ let msg_source = msg::source();
+
+ let game = storage
+ .games
+ .get_mut(&msg_source)
+ .ok_or(GameError::NoSuchGame)?;
+
+ if fuel_amount > MAX_FUEL || payload_amount > MAX_PAYLOAD {
+ return Err(GameError::FuelOrPayloadOverload);
+ }
+ let participant = Participant {
+ id: msg_source,
+ name: game.admin_name.clone(),
+ fuel_amount,
+ payload_amount,
+ };
+
+ let participants = game.stage.mut_participants()?;
+
+ if participants.is_empty() {
+ return Err(GameError::NotEnoughParticipants);
+ }
+ participants.insert(msg_source, participant);
+
+ let mut random = Random::new()?;
+ let mut turns = HashMap::new();
+
+ for (actor, participant) in participants.into_iter() {
+ let mut actor_turns = Vec::with_capacity(TURNS);
+ let mut remaining_fuel = participant.fuel_amount;
+
+ for turn_index in 0..TURNS {
+ match turn(
+ turn_index,
+ remaining_fuel,
+ &mut random,
+ game.weather,
+ participant.payload_amount,
+ ) {
+ Ok(fuel_left) => {
+ remaining_fuel = fuel_left;
+
+ actor_turns.push(Turn::Alive {
+ fuel_left,
+ payload_amount: participant.payload_amount,
+ });
+ }
+ Err(halt_reason) => {
+ actor_turns.push(Turn::Destroyed(halt_reason));
+
+ break;
+ }
+ }
+ }
+
+ turns.insert(*actor, actor_turns);
+ }
+
+ let mut scores: Vec<(ActorId, u128)> = turns
+ .iter()
+ .map(|(actor, turns)| {
+ let last_turn = turns.last().expect("there must be at least 1 turn");
+
+ (
+ *actor,
+ match last_turn {
+ Turn::Alive {
+ fuel_left,
+ payload_amount,
+ } => (*payload_amount as u128 + *fuel_left as u128) * game.altitude as u128,
+ Turn::Destroyed(_) => 0,
+ },
+ )
+ })
+ .collect();
+
+ scores.sort_by(|(_, score_a), (_, score_b)| score_a.cmp(score_b));
+
+ let mut io_turns: Vec> = vec![vec![]; TURNS];
+
+ for (i, io_turn) in io_turns.iter_mut().enumerate().take(TURNS) {
+ for (actor, actor_turns) in &turns {
+ let turn = actor_turns
+ .get(i)
+ .unwrap_or_else(|| actor_turns.last().expect("There must be at least 1 turn"));
+ io_turn.push((*actor, *turn));
+ }
+ }
+
+ let max_value = scores.iter().map(|(_, value)| value).max().unwrap();
+ let winners: Vec<_> = scores
+ .iter()
+ .filter_map(|(actor_id, value)| {
+ if value == max_value {
+ Some(*actor_id)
+ } else {
+ None
+ }
+ })
+ .collect();
+ let prize = game.bid * scores.len() as u128 / winners.len() as u128;
+
+ if game.bid != 0 {
+ winners.iter().for_each(|id| {
+ send_value(*id, prize);
+ });
+ }
+ let participants = participants
+ .iter()
+ .map(|(id, participant)| (*id, participant.clone()))
+ .collect();
+
+ let results = Results {
+ turns: io_turns,
+ rankings: scores.clone(),
+ participants,
+ };
+ game.stage = Stage::Results(results.clone());
+
+ Ok(Event::GameFinished(results))
+}
+
+fn turn(
+ turn: usize,
+ remaining_fuel: u8,
+ random: &mut Random,
+ weather: Weather,
+ payload: u8,
+) -> Result {
+ let new_remaining_fuel =
+ match remaining_fuel.checked_sub((payload + 2 * weather as u8) / TURNS as u8) {
+ Some(actual_fuel) => actual_fuel,
+ None => return Err(HaltReason::FuelShortage),
+ };
+
+ match turn {
+ 0 => {
+ // values in "chance" are transmitted as percentages
+ if random.chance(3) {
+ return Err(HaltReason::EngineFailure);
+ }
+ // this trap for someone who specified a lot of fuel
+ if remaining_fuel >= PENALTY_LEVEL - 2 * weather as u8 && random.chance(10) {
+ return Err(HaltReason::FuelOverload);
+ }
+ }
+ 1 => {
+ // this trap for someone who specified a lot of payload
+ if payload >= PENALTY_LEVEL - 2 * weather as u8 && random.chance(10) {
+ return Err(HaltReason::PayloadOverload);
+ }
+
+ if random.chance(5 + weather as u8) {
+ return Err(HaltReason::SeparationFailure);
+ }
+ }
+ 2 => {
+ if random.chance(10 + weather as u8) {
+ return Err(HaltReason::AsteroidCollision);
+ }
+ }
+ _ => unreachable!(),
+ }
+
+ Ok(new_remaining_fuel)
+}
+
+fn send_value(destination: ActorId, value: u128) {
+ if value != 0 {
+ msg::send_with_gas(destination, "", 0, value).expect("Error in sending value");
+ }
+}
diff --git a/contracts/galactic-express/app/src/services/galactic_express/mod.rs b/contracts/galactic-express/app/src/services/galactic_express/mod.rs
new file mode 100644
index 000000000..7d9483b7d
--- /dev/null
+++ b/contracts/galactic-express/app/src/services/galactic_express/mod.rs
@@ -0,0 +1,194 @@
+use crate::services;
+use gstd::{exec, msg};
+use sails_rs::{collections::HashMap, gstd::service, prelude::*};
+mod funcs;
+pub mod utils;
+use utils::*;
+
+#[derive(Default)]
+pub struct Storage {
+ games: HashMap,
+ player_to_game_id: HashMap,
+ dns_info: Option<(ActorId, String)>,
+ admin: ActorId,
+}
+
+#[derive(Default)]
+pub struct Game {
+ admin: ActorId,
+ admin_name: String,
+ bid: u128,
+ altitude: u16,
+ weather: Weather,
+ reward: u128,
+ stage: Stage,
+}
+
+static mut STORAGE: Option = None;
+
+#[derive(Debug, Clone, Encode, Decode, TypeInfo, PartialEq, Eq)]
+#[codec(crate = sails_rs::scale_codec)]
+#[scale_info(crate = sails_rs::scale_info)]
+pub enum Event {
+ GameFinished(Results),
+ NewSessionCreated {
+ altitude: u16,
+ weather: Weather,
+ reward: u128,
+ bid: u128,
+ },
+ Registered(ActorId, Participant),
+ RegistrationCanceled,
+ PlayerDeleted {
+ player_id: ActorId,
+ },
+ GameCanceled,
+ GameLeft,
+ AdminChanged {
+ new_admin: ActorId,
+ },
+ Killed {
+ inheritor: ActorId,
+ },
+}
+
+#[derive(Clone)]
+pub struct GameService(());
+
+impl GameService {
+ pub async fn init(dns_id_and_name: Option<(ActorId, String)>) -> Self {
+ unsafe {
+ STORAGE = Some(Storage {
+ dns_info: dns_id_and_name.clone(),
+ admin: msg::source(),
+ ..Default::default()
+ });
+ }
+
+ if let Some((id, name)) = dns_id_and_name {
+ let request = [
+ "Dns".encode(),
+ "AddNewProgram".to_string().encode(),
+ (name, exec::program_id()).encode(),
+ ]
+ .concat();
+
+ msg::send_bytes_with_gas_for_reply(id, request, 5_000_000_000, 0, 0)
+ .expect("Error in sending message")
+ .await
+ .expect("Error in `AddNewProgram`");
+ }
+ Self(())
+ }
+ pub fn get_mut(&mut self) -> &'static mut Storage {
+ unsafe { STORAGE.as_mut().expect("Storage is not initialized") }
+ }
+ pub fn get(&self) -> &'static Storage {
+ unsafe { STORAGE.as_ref().expect("Storage is not initialized") }
+ }
+}
+
+#[service(events = Event)]
+impl GameService {
+ pub fn new() -> Self {
+ Self(())
+ }
+ pub fn create_new_session(&mut self, name: String) {
+ let storage = self.get_mut();
+ let event = services::utils::panicking(|| funcs::create_new_session(storage, name));
+ self.notify_on(event.clone()).expect("Notification Error");
+ }
+ pub fn cancel_game(&mut self) {
+ let storage = self.get_mut();
+ let event = services::utils::panicking(|| funcs::cancel_game(storage));
+ self.notify_on(event.clone()).expect("Notification Error");
+ }
+ pub fn leave_game(&mut self) {
+ let storage = self.get_mut();
+ let event = services::utils::panicking(|| funcs::leave_game(storage));
+ self.notify_on(event.clone()).expect("Notification Error");
+ }
+ pub fn register(&mut self, creator: ActorId, participant: Participant) {
+ let storage = self.get_mut();
+ let event = services::utils::panicking(|| funcs::register(storage, creator, participant));
+ self.notify_on(event.clone()).expect("Notification Error");
+ }
+ pub fn cancel_register(&mut self) {
+ let storage = self.get_mut();
+ let event = services::utils::panicking(|| funcs::cancel_register(storage));
+ self.notify_on(event.clone()).expect("Notification Error");
+ }
+ pub fn delete_player(&mut self, player_id: ActorId) {
+ let storage = self.get_mut();
+ let event = services::utils::panicking(|| funcs::delete_player(storage, player_id));
+ self.notify_on(event.clone()).expect("Notification Error");
+ }
+ pub fn start_game(&mut self, fuel_amount: u8, payload_amount: u8) {
+ let storage = self.get_mut();
+ let event =
+ services::utils::panicking(|| funcs::start_game(storage, fuel_amount, payload_amount));
+ self.notify_on(event.clone()).expect("Notification Error");
+ }
+ pub fn change_admin(&mut self, new_admin: ActorId) {
+ let storage = self.get_mut();
+ let msg_source = msg::source();
+ if storage.admin != msg_source {
+ services::utils::panic(GameError::DeniedAccess);
+ }
+ storage.admin = new_admin;
+ self.notify_on(Event::AdminChanged { new_admin })
+ .expect("Notification Error");
+ }
+ pub async fn kill(&mut self, inheritor: ActorId) {
+ let storage = self.get();
+ if storage.admin != msg::source() {
+ services::utils::panic(GameError::DeniedAccess);
+ }
+ if let Some((id, _name)) = &storage.dns_info {
+ let request = ["Dns".encode(), "DeleteMe".to_string().encode(), ().encode()].concat();
+
+ msg::send_bytes_with_gas_for_reply(*id, request, 5_000_000_000, 0, 0)
+ .expect("Error in sending message")
+ .await
+ .expect("Error in `AddNewProgram`");
+ }
+
+ self.notify_on(Event::Killed { inheritor })
+ .expect("Notification Error");
+ exec::exit(inheritor);
+ }
+ pub fn get_game(&self, player_id: ActorId) -> Option {
+ let storage = self.get();
+ storage
+ .player_to_game_id
+ .get(&player_id)
+ .and_then(|creator_id| storage.games.get(creator_id))
+ .map(|game| {
+ let stage = match &game.stage {
+ Stage::Registration(participants_data) => {
+ StageState::Registration(participants_data.clone().into_iter().collect())
+ }
+ Stage::Results(results) => StageState::Results(results.clone()),
+ };
+
+ GameState {
+ admin: game.admin,
+ admin_name: game.admin_name.clone(),
+ altitude: game.altitude,
+ weather: game.weather,
+ reward: game.reward,
+ stage,
+ bid: game.bid,
+ }
+ })
+ }
+ pub fn all(&self) -> State {
+ self.get().into()
+ }
+ pub fn admin(&self) -> &'static ActorId {
+ &self.get().admin
+ }
+ pub fn dns_info(&self) -> &'static Option<(ActorId, String)> {
+ &self.get().dns_info
+ }
+}
diff --git a/contracts/galactic-express/app/src/services/galactic_express/utils.rs b/contracts/galactic-express/app/src/services/galactic_express/utils.rs
new file mode 100644
index 000000000..788c81aed
--- /dev/null
+++ b/contracts/galactic-express/app/src/services/galactic_express/utils.rs
@@ -0,0 +1,252 @@
+use crate::services::galactic_express::Storage;
+use num_traits::FromBytes;
+use sails_rs::{
+ collections::HashMap,
+ errors::Error as GstdError,
+ gstd::exec,
+ ops::{Add, Rem, Sub},
+ prelude::*,
+};
+
+pub const MAX_PARTICIPANTS: usize = 4;
+pub const TURNS: usize = 3;
+
+/// Represents a range of the minimum & the maximum reward for a session.
+pub const REWARD: (u128, u128) = (80, 360);
+/// Represents a range of the minimum & the maximum turn altitude.
+pub const TURN_ALTITUDE: (u16, u16) = (500, 1_000);
+/// Dangerous level for high fuel and payload values
+/// This is to account for the scenario where a player specifies a significant amount of fuel
+/// or a large payload, resulting in a greater likelihood of mission failure.
+pub const PENALTY_LEVEL: u8 = 80;
+// maximum fuel value that can be entered by the user
+pub const MAX_FUEL: u8 = 100;
+// maximum payload value that can be entered by the user
+pub const MAX_PAYLOAD: u8 = 100;
+
+pub struct Random {
+ index: usize,
+ random: [u8; 32],
+}
+
+impl Random {
+ pub fn new() -> Result {
+ exec::random([0; 32])
+ .map(|(random, _)| Self { index: 0, random })
+ .map_err(|error| GstdError::from(error).into())
+ }
+
+ pub fn next(&mut self) -> u8 {
+ let next = *self
+ .random
+ .get(self.index)
+ .expect("index for the random array traversing must'n overflow");
+
+ self.index += 1;
+
+ next
+ }
+
+ pub fn generate(&mut self, min: T, max: T) -> T
+ where
+ T: FromBytes
+ + Add
+ + Sub
+ + Rem
+ + Copy,
+ {
+ min + T::from_le_bytes(&array::from_fn(|_| self.next())) % (max - min)
+ }
+
+ pub fn chance(&mut self, probability: u8) -> bool {
+ assert!(probability < 101, "probability can't be more than 100");
+
+ self.next() % 100 < probability
+ }
+}
+
+#[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Eq)]
+#[codec(crate = sails_rs::scale_codec)]
+#[scale_info(crate = sails_rs::scale_info)]
+pub enum GameError {
+ StateUninitaliazed,
+ GstdError(String),
+ SessionEnded,
+ FuelOrPayloadOverload,
+ SessionFull,
+ NotEnoughParticipants,
+ NoSuchGame,
+ WrongBid,
+ NoSuchPlayer,
+ Unregistered,
+ AlreadyRegistered,
+ SeveralRegistrations,
+ NotForAdmin,
+ DeniedAccess,
+}
+
+impl From for GameError {
+ fn from(error: GstdError) -> Self {
+ GameError::GstdError(error.to_string())
+ }
+}
+
+#[derive(Encode, Decode, TypeInfo, Default, Clone, Copy, Debug, PartialEq, Eq)]
+#[codec(crate = sails_rs::scale_codec)]
+#[scale_info(crate = sails_rs::scale_info)]
+pub enum Weather {
+ #[default]
+ Clear,
+ Cloudy,
+ Rainy,
+ Stormy,
+ Thunder,
+ Tornado,
+}
+
+pub enum Stage {
+ Registration(HashMap),
+ Results(Results),
+}
+
+impl Stage {
+ pub fn mut_participants(&mut self) -> Result<&mut HashMap, GameError> {
+ if let Stage::Registration(participants) = self {
+ Ok(participants)
+ } else {
+ Err(GameError::SessionEnded)
+ }
+ }
+}
+
+impl Default for Stage {
+ fn default() -> Self {
+ Self::Results(Results::default())
+ }
+}
+
+#[derive(Encode, Decode, TypeInfo, Default, Clone, Debug, PartialEq, Eq)]
+#[codec(crate = sails_rs::scale_codec)]
+#[scale_info(crate = sails_rs::scale_info)]
+pub struct Results {
+ pub turns: Vec>,
+ pub rankings: Vec<(ActorId, u128)>,
+ pub participants: Vec<(ActorId, Participant)>,
+}
+
+#[derive(Encode, Decode, TypeInfo, Clone, Debug, Default, PartialEq, Eq)]
+#[codec(crate = sails_rs::scale_codec)]
+#[scale_info(crate = sails_rs::scale_info)]
+pub struct Participant {
+ pub id: ActorId,
+ pub name: String,
+ pub fuel_amount: u8,
+ pub payload_amount: u8,
+}
+
+impl Participant {
+ pub fn check(&self) -> Result<(), GameError> {
+ if self.fuel_amount > MAX_FUEL || self.payload_amount > MAX_PAYLOAD {
+ Err(GameError::FuelOrPayloadOverload)
+ } else {
+ Ok(())
+ }
+ }
+}
+
+#[derive(Encode, Decode, TypeInfo, Clone, Copy, Debug, PartialEq, Eq)]
+#[codec(crate = sails_rs::scale_codec)]
+#[scale_info(crate = sails_rs::scale_info)]
+pub enum Turn {
+ Alive { fuel_left: u8, payload_amount: u8 },
+ Destroyed(HaltReason),
+}
+
+#[derive(Encode, Decode, TypeInfo, Clone, Copy, Debug, PartialEq, Eq)]
+#[codec(crate = sails_rs::scale_codec)]
+#[scale_info(crate = sails_rs::scale_info)]
+pub enum HaltReason {
+ PayloadOverload,
+ FuelOverload,
+ SeparationFailure,
+ AsteroidCollision,
+ FuelShortage,
+ EngineFailure,
+}
+
+#[derive(Encode, Decode, TypeInfo, Debug)]
+#[codec(crate = sails_rs::scale_codec)]
+#[scale_info(crate = sails_rs::scale_info)]
+pub struct State {
+ pub games: Vec<(ActorId, GameState)>,
+ pub player_to_game_id: Vec<(ActorId, ActorId)>,
+ pub dns_info: Option<(ActorId, String)>,
+ pub admin: ActorId,
+}
+
+#[derive(Encode, Decode, TypeInfo, Debug)]
+#[codec(crate = sails_rs::scale_codec)]
+#[scale_info(crate = sails_rs::scale_info)]
+pub struct GameState {
+ pub admin: ActorId,
+ pub admin_name: String,
+ pub altitude: u16,
+ pub weather: Weather,
+ pub reward: u128,
+ pub stage: StageState,
+ pub bid: u128,
+}
+
+#[derive(Encode, Decode, TypeInfo, Debug)]
+#[codec(crate = sails_rs::scale_codec)]
+#[scale_info(crate = sails_rs::scale_info)]
+pub enum StageState {
+ Registration(Vec<(ActorId, Participant)>),
+ Results(Results),
+}
+
+impl From<&Storage> for State {
+ fn from(value: &Storage) -> Self {
+ let Storage {
+ games,
+ player_to_game_id,
+ dns_info,
+ admin,
+ } = value;
+
+ let games = games
+ .iter()
+ .map(|(id, game)| {
+ let stage = match &game.stage {
+ Stage::Registration(participants_data) => StageState::Registration(
+ participants_data
+ .iter()
+ .map(|(actor_id, participant)| (*actor_id, participant.clone()))
+ .collect(),
+ ),
+ Stage::Results(results) => StageState::Results(results.clone()),
+ };
+
+ let game_state = GameState {
+ admin: game.admin,
+ admin_name: game.admin_name.clone(),
+ altitude: game.altitude,
+ weather: game.weather,
+ reward: game.reward,
+ stage,
+ bid: game.bid,
+ };
+ (*id, game_state)
+ })
+ .collect();
+
+ let player_to_game_id = player_to_game_id.iter().map(|(k, v)| (*k, *v)).collect();
+
+ Self {
+ games,
+ player_to_game_id,
+ dns_info: dns_info.clone(),
+ admin: *admin,
+ }
+ }
+}
diff --git a/contracts/galactic-express/app/src/services/mod.rs b/contracts/galactic-express/app/src/services/mod.rs
new file mode 100644
index 000000000..a7aedd83a
--- /dev/null
+++ b/contracts/galactic-express/app/src/services/mod.rs
@@ -0,0 +1,2 @@
+pub mod galactic_express;
+pub mod utils;
diff --git a/contracts/galactic-express/app/src/services/utils.rs b/contracts/galactic-express/app/src/services/utils.rs
new file mode 100644
index 000000000..9dee576e1
--- /dev/null
+++ b/contracts/galactic-express/app/src/services/utils.rs
@@ -0,0 +1,13 @@
+use core::fmt::Debug;
+use gstd::{ext, format};
+
+pub fn panicking Result>(f: F) -> T {
+ match f() {
+ Ok(v) => v,
+ Err(e) => panic(e),
+ }
+}
+
+pub fn panic(err: impl Debug) -> ! {
+ ext::panic(&format!("{err:?}"))
+}
diff --git a/contracts/galactic-express/app/tests/test.rs b/contracts/galactic-express/app/tests/test.rs
new file mode 100644
index 000000000..aca592b07
--- /dev/null
+++ b/contracts/galactic-express/app/tests/test.rs
@@ -0,0 +1,325 @@
+use galactic_express::{
+ traits::{GalacticExpress, GalacticExpressFactory},
+ GalacticExpress as GalacticExpressClient, GalacticExpressFactory as Factory, Participant,
+ StageState,
+};
+use gstd::errors::{ErrorReplyReason, SimpleExecutionError};
+use sails_rs::calls::*;
+use sails_rs::errors::{Error, RtlError};
+use sails_rs::gtest::{calls::*, System};
+
+pub const ADMIN: u64 = 10;
+pub const PLAYERS: [u64; 3] = [12, 13, 14];
+
+#[tokio::test]
+async fn test_play_game() {
+ let system = System::new();
+ system.init_logger();
+ system.mint_to(ADMIN, 100_000_000_000_000);
+ system.mint_to(PLAYERS[0], 100_000_000_000_000);
+ system.mint_to(PLAYERS[1], 100_000_000_000_000);
+ system.mint_to(PLAYERS[2], 100_000_000_000_000);
+ let program_space = GTestRemoting::new(system, ADMIN.into());
+ program_space.system().init_logger();
+ let code_id = program_space
+ .system()
+ .submit_code_file("../../target/wasm32-unknown-unknown/release/galactic_express.opt.wasm");
+
+ let galactic_express_factory = Factory::new(program_space.clone());
+ let galactic_express_id = galactic_express_factory
+ .new(None)
+ .send_recv(code_id, "123")
+ .await
+ .unwrap();
+
+ let mut client = GalacticExpressClient::new(program_space.clone());
+
+ let bid = 11_000_000_000_000;
+ program_space.system().mint_to(ADMIN, bid);
+
+ // create_new_session
+ client
+ .create_new_session("Game".to_string())
+ .with_value(bid)
+ .send_recv(galactic_express_id)
+ .await
+ .unwrap();
+ // check game state
+ let state = client.all().recv(galactic_express_id).await.unwrap();
+ assert!(!state.games.is_empty());
+ assert!(!state.player_to_game_id.is_empty());
+
+ // register
+ for player_id in PLAYERS {
+ let player = Participant {
+ id: player_id.into(),
+ name: "player".to_string(),
+ fuel_amount: 42,
+ payload_amount: 20,
+ };
+ program_space.system().mint_to(player_id, bid);
+
+ client
+ .register(ADMIN.into(), player)
+ .with_value(bid)
+ .with_args(GTestArgs::new(player_id.into()))
+ .send_recv(galactic_express_id)
+ .await
+ .unwrap();
+ }
+ // check game state
+ let state = client.all().recv(galactic_express_id).await.unwrap();
+ assert_eq!(state.player_to_game_id.len(), 4);
+ if let StageState::Registration(participants) = &state.games[0].1.stage {
+ assert_eq!(participants.len(), 3);
+ }
+
+ // start game
+ client
+ .start_game(42, 20)
+ .send_recv(galactic_express_id)
+ .await
+ .unwrap();
+
+ let state = client.all().recv(galactic_express_id).await.unwrap();
+ if let StageState::Results(results) = &state.games[0].1.stage {
+ assert_eq!(results.rankings.len(), 4);
+ }
+}
+
+#[tokio::test]
+async fn cancel_register_and_delete_player() {
+ let system = System::new();
+ system.init_logger();
+ system.mint_to(ADMIN, 100_000_000_000_000);
+ system.mint_to(PLAYERS[0], 100_000_000_000_000);
+ system.mint_to(PLAYERS[1], 100_000_000_000_000);
+ system.mint_to(PLAYERS[2], 100_000_000_000_000);
+ let program_space = GTestRemoting::new(system, ADMIN.into());
+ program_space.system().init_logger();
+ let code_id = program_space
+ .system()
+ .submit_code_file("../../target/wasm32-unknown-unknown/release/galactic_express.opt.wasm");
+
+ let galactic_express_factory = Factory::new(program_space.clone());
+ let galactic_express_id = galactic_express_factory
+ .new(None)
+ .send_recv(code_id, "123")
+ .await
+ .unwrap();
+
+ let mut client = GalacticExpressClient::new(program_space.clone());
+
+ let bid = 11_000_000_000_000;
+ program_space.system().mint_to(ADMIN, bid);
+
+ // create_new_session
+ client
+ .create_new_session("Game".to_string())
+ .with_value(bid)
+ .send_recv(galactic_express_id)
+ .await
+ .unwrap();
+ // check game state
+ let state = client.all().recv(galactic_express_id).await.unwrap();
+ assert!(!state.games.is_empty());
+ assert!(!state.player_to_game_id.is_empty());
+
+ // register
+ for player_id in PLAYERS {
+ let player = Participant {
+ id: player_id.into(),
+ name: "player".to_string(),
+ fuel_amount: 42,
+ payload_amount: 20,
+ };
+ program_space.system().mint_to(player_id, bid);
+
+ client
+ .register(ADMIN.into(), player)
+ .with_value(bid)
+ .with_args(GTestArgs::new(player_id.into()))
+ .send_recv(galactic_express_id)
+ .await
+ .unwrap();
+ }
+ // check game state
+ let state = client.all().recv(galactic_express_id).await.unwrap();
+ assert_eq!(state.player_to_game_id.len(), 4);
+ if let StageState::Registration(participants) = &state.games[0].1.stage {
+ assert_eq!(participants.len(), 3);
+ }
+
+ // cancel_register
+ client
+ .cancel_register()
+ .with_args(GTestArgs::new(PLAYERS[0].into()))
+ .send_recv(galactic_express_id)
+ .await
+ .unwrap();
+
+ // check game state
+ let state = client.all().recv(galactic_express_id).await.unwrap();
+ if let StageState::Registration(participants) = &state.games[0].1.stage {
+ assert_eq!(participants.len(), 2);
+ }
+ assert_eq!(state.player_to_game_id.len(), 3);
+
+ // delete_player
+ client
+ .delete_player(PLAYERS[1].into())
+ .send_recv(galactic_express_id)
+ .await
+ .unwrap();
+
+ // check game state
+ let state = client.all().recv(galactic_express_id).await.unwrap();
+ if let StageState::Registration(participants) = &state.games[0].1.stage {
+ assert_eq!(participants.len(), 1);
+ }
+ assert_eq!(state.player_to_game_id.len(), 2);
+}
+
+#[tokio::test]
+async fn errors() {
+ let system = System::new();
+ system.init_logger();
+ system.mint_to(ADMIN, 100_000_000_000_000);
+ system.mint_to(PLAYERS[0], 100_000_000_000_000);
+ system.mint_to(PLAYERS[1], 100_000_000_000_000);
+ system.mint_to(PLAYERS[2], 100_000_000_000_000);
+ let program_space = GTestRemoting::new(system, ADMIN.into());
+ program_space.system().init_logger();
+ let code_id = program_space
+ .system()
+ .submit_code_file("../../target/wasm32-unknown-unknown/release/galactic_express.opt.wasm");
+
+ let galactic_express_factory = Factory::new(program_space.clone());
+ let galactic_express_id = galactic_express_factory
+ .new(None)
+ .send_recv(code_id, "123")
+ .await
+ .unwrap();
+
+ let mut client = GalacticExpressClient::new(program_space.clone());
+
+ let bid = 11_000_000_000_000;
+ program_space.system().mint_to(ADMIN, bid);
+
+ let player = Participant {
+ id: ADMIN.into(),
+ name: "player".to_string(),
+ fuel_amount: 42,
+ payload_amount: 20,
+ };
+
+ let res = client
+ .register(ADMIN.into(), player)
+ .send_recv(galactic_express_id)
+ .await;
+
+ assert_error(&res, "NoSuchGame".to_string());
+
+ client
+ .create_new_session("Game".to_string())
+ .with_value(bid)
+ .send_recv(galactic_express_id)
+ .await
+ .unwrap();
+
+ let player = Participant {
+ id: ADMIN.into(),
+ name: "player".to_string(),
+ fuel_amount: 42,
+ payload_amount: 20,
+ };
+
+ let res = client
+ .register(ADMIN.into(), player)
+ .send_recv(galactic_express_id)
+ .await;
+
+ assert_error(&res, "SeveralRegistrations".to_string());
+
+ let res = client
+ .start_game(42, 20)
+ .with_args(GTestArgs::new(PLAYERS[0].into()))
+ .send_recv(galactic_express_id)
+ .await;
+
+ assert_error(&res, "NoSuchGame".to_string());
+
+ let res = client
+ .start_game(42, 20)
+ .send_recv(galactic_express_id)
+ .await;
+
+ assert_error(&res, "NotEnoughParticipants".to_string());
+
+ // register
+ for player_id in PLAYERS {
+ let player = Participant {
+ id: player_id.into(),
+ name: "player".to_string(),
+ fuel_amount: 42,
+ payload_amount: 20,
+ };
+ program_space.system().mint_to(player_id, bid);
+
+ client
+ .register(ADMIN.into(), player)
+ .with_value(bid)
+ .with_args(GTestArgs::new(player_id.into()))
+ .send_recv(galactic_express_id)
+ .await
+ .unwrap();
+ }
+
+ let res = client
+ .start_game(101, 100)
+ .send_recv(galactic_express_id)
+ .await;
+
+ assert_error(&res, "FuelOrPayloadOverload".to_string());
+
+ let res = client
+ .start_game(100, 101)
+ .send_recv(galactic_express_id)
+ .await;
+
+ assert_error(&res, "FuelOrPayloadOverload".to_string());
+
+ let res = client
+ .start_game(101, 101)
+ .send_recv(galactic_express_id)
+ .await;
+
+ assert_error(&res, "FuelOrPayloadOverload".to_string());
+
+ let player = Participant {
+ id: 100.into(),
+ name: "player".to_string(),
+ fuel_amount: 42,
+ payload_amount: 20,
+ };
+ program_space.system().mint_to(100, 100_000_000_000_000);
+
+ let res = client
+ .register(ADMIN.into(), player)
+ .with_value(bid)
+ .with_args(GTestArgs::new(100.into()))
+ .send_recv(galactic_express_id)
+ .await;
+
+ assert_error(&res, "SessionFull".to_string());
+}
+
+fn assert_error(res: &Result<(), Error>, error: String) {
+ assert!(matches!(
+ res,
+ Err(sails_rs::errors::Error::Rtl(RtlError::ReplyHasError(
+ ErrorReplyReason::Execution(SimpleExecutionError::UserspacePanic),
+ message
+ ))) if *message == "Panic occurred: ".to_string() + &error
+ ));
+}
diff --git a/contracts/galactic-express/build.rs b/contracts/galactic-express/build.rs
deleted file mode 100644
index 126c47cae..000000000
--- a/contracts/galactic-express/build.rs
+++ /dev/null
@@ -1,5 +0,0 @@
-use galactic_express_io::ContractMetadata;
-
-fn main() {
- gear_wasm_builder::build_with_metadata::();
-}
diff --git a/contracts/galactic-express/io/Cargo.toml b/contracts/galactic-express/io/Cargo.toml
deleted file mode 100644
index a0df31052..000000000
--- a/contracts/galactic-express/io/Cargo.toml
+++ /dev/null
@@ -1,11 +0,0 @@
-[package]
-name = "galactic-express-io"
-version.workspace = true
-edition.workspace = true
-publish.workspace = true
-
-[dependencies]
-gmeta.workspace = true
-gstd.workspace = true
-parity-scale-codec.workspace = true
-scale-info.workspace = true
diff --git a/contracts/galactic-express/io/src/lib.rs b/contracts/galactic-express/io/src/lib.rs
deleted file mode 100644
index 47f4ae9cc..000000000
--- a/contracts/galactic-express/io/src/lib.rs
+++ /dev/null
@@ -1,181 +0,0 @@
-#![no_std]
-
-use gmeta::{InOut, Metadata, Out};
-use gstd::{errors::Error as GstdError, prelude::*, ActorId};
-
-pub struct ContractMetadata;
-
-impl Metadata for ContractMetadata {
- type Init = Out>;
- type Handle = InOut>;
- type Reply = ();
- type Others = ();
- type Signal = ();
- type State = InOut;
-}
-
-pub const MAX_PARTICIPANTS: usize = 4;
-pub const TURNS: usize = 3;
-
-/// Represents a range of the minimum & the maximum reward for a session.
-pub const REWARD: (u128, u128) = (80, 360);
-/// Represents a range of the minimum & the maximum turn altitude.
-pub const TURN_ALTITUDE: (u16, u16) = (500, 1_000);
-/// Dangerous level for high fuel and payload values
-/// This is to account for the scenario where a player specifies a significant amount of fuel
-/// or a large payload, resulting in a greater likelihood of mission failure.
-pub const PENALTY_LEVEL: u8 = 80;
-// maximum fuel value that can be entered by the user
-pub const MAX_FUEL: u8 = 100;
-// maximum payload value that can be entered by the user
-pub const MAX_PAYLOAD: u8 = 100;
-
-#[derive(Encode, Decode, TypeInfo)]
-pub enum StateQuery {
- All,
- GetGame { player_id: ActorId },
-}
-
-#[derive(Encode, Decode, TypeInfo)]
-pub enum StateReply {
- All(State),
- Game(Option),
-}
-
-#[derive(Encode, Decode, TypeInfo, Debug)]
-pub struct State {
- pub games: Vec<(ActorId, GameState)>,
- pub player_to_game_id: Vec<(ActorId, ActorId)>,
-}
-
-#[derive(Encode, Decode, TypeInfo, Debug)]
-pub struct GameState {
- pub admin: ActorId,
- pub admin_name: String,
- pub altitude: u16,
- pub weather: Weather,
- pub reward: u128,
- pub stage: StageState,
- pub bid: u128,
-}
-
-#[derive(Encode, Decode, TypeInfo, Debug)]
-pub enum StageState {
- Registration(Vec<(ActorId, Participant)>),
- Results(Results),
-}
-
-#[derive(Encode, Decode, TypeInfo, Default, Clone, Debug, PartialEq, Eq)]
-pub struct Results {
- pub turns: Vec>,
- pub rankings: Vec<(ActorId, u128)>,
- pub participants: Vec<(ActorId, Participant)>,
-}
-
-#[derive(Encode, Decode, TypeInfo)]
-pub enum Action {
- CreateNewSession {
- name: String,
- },
- Register {
- creator: ActorId,
- participant: Participant,
- },
- CancelRegistration,
- DeletePlayer {
- player_id: ActorId,
- },
- CancelGame,
- LeaveGame,
- StartGame {
- fuel_amount: u8,
- payload_amount: u8,
- },
-}
-
-#[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Eq)]
-pub enum Event {
- GameFinished(Results),
- AdminChanged(ActorId, ActorId),
- NewSessionCreated {
- altitude: u16,
- weather: Weather,
- reward: u128,
- bid: u128,
- },
- Registered(ActorId, Participant),
- RegistrationCanceled,
- PlayerDeleted {
- player_id: ActorId,
- },
- GameCanceled,
- GameLeft,
-}
-
-#[derive(Encode, Decode, TypeInfo, Clone, Debug, Default, PartialEq, Eq)]
-pub struct Participant {
- pub id: ActorId,
- pub name: String,
- pub fuel_amount: u8,
- pub payload_amount: u8,
-}
-
-impl Participant {
- pub fn check(&self) -> Result<(), Error> {
- if self.fuel_amount > MAX_FUEL || self.payload_amount > MAX_PAYLOAD {
- Err(Error::FuelOrPayloadOverload)
- } else {
- Ok(())
- }
- }
-}
-
-#[derive(Encode, Decode, TypeInfo, Clone, Copy, Debug, PartialEq, Eq)]
-pub enum HaltReason {
- PayloadOverload,
- FuelOverload,
- SeparationFailure,
- AsteroidCollision,
- FuelShortage,
- EngineFailure,
-}
-
-#[derive(Encode, Decode, TypeInfo, Clone, Copy, Debug, PartialEq, Eq)]
-pub enum Turn {
- Alive { fuel_left: u8, payload_amount: u8 },
- Destroyed(HaltReason),
-}
-
-#[derive(Encode, Decode, TypeInfo, Default, Clone, Copy, Debug, PartialEq, Eq)]
-pub enum Weather {
- #[default]
- Clear,
- Cloudy,
- Rainy,
- Stormy,
- Thunder,
- Tornado,
-}
-
-#[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Eq)]
-pub enum Error {
- StateUninitaliazed,
- GstdError(String),
- SessionEnded,
- FuelOrPayloadOverload,
- SessionFull,
- NotEnoughParticipants,
- NoSuchGame,
- WrongBid,
- NoSuchPlayer,
- Unregistered,
- AlreadyRegistered,
- SeveralRegistrations,
- NotForAdmin,
-}
-
-impl From for Error {
- fn from(error: GstdError) -> Self {
- Error::GstdError(error.to_string())
- }
-}
diff --git a/contracts/galactic-express/src/lib.rs b/contracts/galactic-express/src/lib.rs
deleted file mode 100644
index 3350c60c3..000000000
--- a/contracts/galactic-express/src/lib.rs
+++ /dev/null
@@ -1,556 +0,0 @@
-#![no_std]
-
-use galactic_express_io::*;
-use gstd::{
- collections::HashMap,
- errors::Error as GstdError,
- exec, msg,
- ops::{Add, Rem, Sub},
- prelude::*,
- ActorId,
-};
-use num_traits::FromBytes;
-
-struct Random {
- index: usize,
- random: [u8; 32],
-}
-
-impl Random {
- fn new() -> Result {
- exec::random([0; 32])
- .map(|(random, _)| Self { index: 0, random })
- .map_err(|error| GstdError::from(error).into())
- }
-
- fn next(&mut self) -> u8 {
- let next = *self
- .random
- .get(self.index)
- .expect("index for the random array traversing must'n overflow");
-
- self.index += 1;
-
- next
- }
-
- fn generate(&mut self, min: T, max: T) -> T
- where
- T: FromBytes
- + Add
- + Sub
- + Rem
- + Copy,
- {
- min + T::from_le_bytes(&array::from_fn(|_| self.next())) % (max - min)
- }
-
- fn chance(&mut self, probability: u8) -> bool {
- assert!(probability < 101, "probability can't be more than 100");
-
- self.next() % 100 < probability
- }
-}
-
-static mut STATE: Option = None;
-
-fn state_mut() -> Result<&'static mut Contract, Error> {
- unsafe { STATE.as_mut().ok_or(Error::StateUninitaliazed) }
-}
-
-enum Stage {
- Registration(HashMap),
- Results(Results),
-}
-
-impl Stage {
- fn mut_participants(&mut self) -> Result<&mut HashMap, Error> {
- if let Stage::Registration(participants) = self {
- Ok(participants)
- } else {
- Err(Error::SessionEnded)
- }
- }
-}
-
-impl Default for Stage {
- fn default() -> Self {
- Self::Results(Results::default())
- }
-}
-
-#[derive(Default)]
-struct Contract {
- games: HashMap,
- player_to_game_id: HashMap,
-}
-
-#[derive(Default)]
-pub struct Game {
- admin: ActorId,
- admin_name: String,
- bid: u128,
- altitude: u16,
- weather: Weather,
- reward: u128,
- stage: Stage,
-}
-
-impl Contract {
- fn create_new_session(&mut self, name: String) -> Result {
- let msg_src = msg::source();
- let msg_value = msg::value();
-
- if self.player_to_game_id.contains_key(&msg_src) {
- return Err(Error::SeveralRegistrations);
- }
-
- let game = self.games.entry(msg_src).or_insert_with(|| Game {
- admin: msg_src,
- admin_name: name,
- bid: msg_value,
- ..Default::default()
- });
-
- let stage = &mut game.stage;
-
- match stage {
- Stage::Registration(participants) => {
- participants.clear();
- }
- Stage::Results { .. } => *stage = Stage::Registration(HashMap::new()),
- }
-
- let mut random = Random::new()?;
-
- game.weather = match random.next() % (Weather::Tornado as u8 + 1) {
- 0 => Weather::Clear,
- 1 => Weather::Cloudy,
- 2 => Weather::Rainy,
- 3 => Weather::Stormy,
- 4 => Weather::Thunder,
- 5 => Weather::Tornado,
- _ => unreachable!(),
- };
- game.altitude = random.generate(TURN_ALTITUDE.0, TURN_ALTITUDE.1) * TURNS as u16;
- game.reward = random.generate(REWARD.0, REWARD.1);
- self.player_to_game_id.insert(msg_src, msg_src);
-
- Ok(Event::NewSessionCreated {
- altitude: game.altitude,
- weather: game.weather,
- reward: game.reward,
- bid: msg_value,
- })
- }
-
- fn cancel_game(&mut self) -> Result {
- let msg_src = msg::source();
- let game = self.games.get(&msg_src).ok_or(Error::NoSuchGame)?;
-
- match &game.stage {
- Stage::Registration(players) => {
- players.iter().for_each(|(id, _)| {
- send_value(*id, game.bid);
- self.player_to_game_id.remove(id);
- });
- }
- Stage::Results(results) => {
- results.rankings.iter().for_each(|(id, _)| {
- self.player_to_game_id.remove(id);
- });
- }
- }
-
- self.player_to_game_id.remove(&msg_src);
- self.games.remove(&msg_src);
- Ok(Event::GameCanceled)
- }
-
- fn leave_game(&mut self) -> Result {
- let msg_src = msg::source();
- self.player_to_game_id.remove(&msg_src);
- Ok(Event::GameLeft)
- }
-
- fn register(
- &mut self,
- creator: ActorId,
- participant: Participant,
- msg_source: ActorId,
- msg_value: u128,
- ) -> Result {
- if self.player_to_game_id.contains_key(&msg_source) {
- return Err(Error::SeveralRegistrations);
- }
-
- if let Some(game) = self.games.get_mut(&creator) {
- if msg_value != game.bid {
- return Err(Error::WrongBid);
- }
- if let Stage::Results(_) = game.stage {
- return Err(Error::SessionEnded);
- }
-
- let participants = game.stage.mut_participants()?;
-
- if participants.contains_key(&msg_source) {
- return Err(Error::AlreadyRegistered);
- }
-
- if participants.len() >= MAX_PARTICIPANTS - 1 {
- return Err(Error::SessionFull);
- }
-
- participant.check()?;
- participants.insert(msg_source, participant.clone());
- self.player_to_game_id.insert(msg_source, creator);
-
- Ok(Event::Registered(msg_source, participant))
- } else {
- Err(Error::NoSuchGame)
- }
- }
-
- fn cancel_register(&mut self) -> Result {
- let msg_source = msg::source();
-
- let creator = self
- .player_to_game_id
- .get(&msg_source)
- .ok_or(Error::Unregistered)?;
- let game = self.games.get_mut(creator).ok_or(Error::NoSuchGame)?;
-
- if msg_source != game.admin {
- let participants = game.stage.mut_participants()?;
- if participants.contains_key(&msg_source) {
- send_value(msg_source, game.bid);
- participants.remove(&msg_source).expect("Critical error");
- self.player_to_game_id.remove(&msg_source);
- } else {
- return Err(Error::NoSuchPlayer);
- }
- Ok(Event::RegistrationCanceled)
- } else {
- Err(Error::NotForAdmin)
- }
- }
- fn delete_player(&mut self, player_id: ActorId) -> Result {
- let msg_source = msg::source();
-
- if let Some(game) = self.games.get_mut(&msg_source) {
- if let Stage::Results(_) = game.stage {
- return Err(Error::SessionEnded);
- }
-
- let participants = game.stage.mut_participants()?;
-
- if participants.contains_key(&player_id) {
- send_value(player_id, game.bid);
- participants.remove(&player_id).expect("Critical error");
- self.player_to_game_id.remove(&player_id);
- } else {
- return Err(Error::NoSuchPlayer);
- }
-
- Ok(Event::PlayerDeleted { player_id })
- } else {
- Err(Error::NoSuchGame)
- }
- }
-
- async fn start_game(&mut self, fuel_amount: u8, payload_amount: u8) -> Result {
- let msg_source = msg::source();
-
- let game = self.games.get_mut(&msg_source).ok_or(Error::NoSuchGame)?;
-
- if fuel_amount > MAX_FUEL || payload_amount > MAX_PAYLOAD {
- return Err(Error::FuelOrPayloadOverload);
- }
- let participant = Participant {
- id: msg_source,
- name: game.admin_name.clone(),
- fuel_amount,
- payload_amount,
- };
-
- let participants = game.stage.mut_participants()?;
-
- if participants.is_empty() {
- return Err(Error::NotEnoughParticipants);
- }
- participants.insert(msg_source, participant);
-
- let mut random = Random::new()?;
- let mut turns = HashMap::new();
-
- for (actor, participant) in participants.into_iter() {
- let mut actor_turns = Vec::with_capacity(TURNS);
- let mut remaining_fuel = participant.fuel_amount;
-
- for turn_index in 0..TURNS {
- match turn(
- turn_index,
- remaining_fuel,
- &mut random,
- game.weather,
- participant.payload_amount,
- ) {
- Ok(fuel_left) => {
- remaining_fuel = fuel_left;
-
- actor_turns.push(Turn::Alive {
- fuel_left,
- payload_amount: participant.payload_amount,
- });
- }
- Err(halt_reason) => {
- actor_turns.push(Turn::Destroyed(halt_reason));
-
- break;
- }
- }
- }
-
- turns.insert(*actor, actor_turns);
- }
-
- let mut scores: Vec<(ActorId, u128)> = turns
- .iter()
- .map(|(actor, turns)| {
- let last_turn = turns.last().expect("there must be at least 1 turn");
-
- (
- *actor,
- match last_turn {
- Turn::Alive {
- fuel_left,
- payload_amount,
- } => (*payload_amount as u128 + *fuel_left as u128) * game.altitude as u128,
- Turn::Destroyed(_) => 0,
- },
- )
- })
- .collect();
-
- scores.sort_by(|(_, score_a), (_, score_b)| score_a.cmp(score_b));
-
- let mut io_turns: Vec> = vec![vec![]; TURNS];
-
- for (i, io_turn) in io_turns.iter_mut().enumerate().take(TURNS) {
- for (actor, actor_turns) in &turns {
- let turn = actor_turns
- .get(i)
- .unwrap_or_else(|| actor_turns.last().expect("There must be at least 1 turn"));
- io_turn.push((*actor, *turn));
- }
- }
-
- let max_value = scores.iter().map(|(_, value)| value).max().unwrap();
- let winners: Vec<_> = scores
- .iter()
- .filter_map(|(actor_id, value)| {
- if value == max_value {
- Some(*actor_id)
- } else {
- None
- }
- })
- .collect();
- let prize = game.bid * scores.len() as u128 / winners.len() as u128;
-
- if game.bid != 0 {
- winners.iter().for_each(|id| {
- send_value(*id, prize);
- });
- }
- let participants = participants
- .iter()
- .map(|(id, participant)| (*id, participant.clone()))
- .collect();
-
- let results = Results {
- turns: io_turns,
- rankings: scores.clone(),
- participants,
- };
- game.stage = Stage::Results(results.clone());
-
- Ok(Event::GameFinished(results))
- }
-}
-
-fn turn(
- turn: usize,
- remaining_fuel: u8,
- random: &mut Random,
- weather: Weather,
- payload: u8,
-) -> Result {
- let new_remaining_fuel =
- match remaining_fuel.checked_sub((payload + 2 * weather as u8) / TURNS as u8) {
- Some(actual_fuel) => actual_fuel,
- None => return Err(HaltReason::FuelShortage),
- };
-
- match turn {
- 0 => {
- // values in "chance" are transmitted as percentages
- if random.chance(3) {
- return Err(HaltReason::EngineFailure);
- }
- // this trap for someone who specified a lot of fuel
- if remaining_fuel >= PENALTY_LEVEL - 2 * weather as u8 && random.chance(10) {
- return Err(HaltReason::FuelOverload);
- }
- }
- 1 => {
- // this trap for someone who specified a lot of payload
- if payload >= PENALTY_LEVEL - 2 * weather as u8 && random.chance(10) {
- return Err(HaltReason::PayloadOverload);
- }
-
- if random.chance(5 + weather as u8) {
- return Err(HaltReason::SeparationFailure);
- }
- }
- 2 => {
- if random.chance(10 + weather as u8) {
- return Err(HaltReason::AsteroidCollision);
- }
- }
- _ => unreachable!(),
- }
-
- Ok(new_remaining_fuel)
-}
-
-fn send_value(destination: ActorId, value: u128) {
- if value != 0 {
- msg::send_with_gas(destination, "", 0, value).expect("Error in sending value");
- }
-}
-
-#[no_mangle]
-extern fn init() {
- msg::reply(process_init(), 0).expect("failed to encode or reply from `main()`");
-}
-
-fn process_init() -> Result<(), Error> {
- unsafe {
- STATE = Some(Contract {
- ..Default::default()
- });
- }
-
- Ok(())
-}
-
-#[gstd::async_main]
-async fn main() {
- msg::reply(process_main().await, 0).expect("failed to encode or reply from `main()`");
-}
-
-async fn process_main() -> Result {
- let action = msg::load()?;
- let contract = state_mut()?;
-
- match action {
- Action::CreateNewSession { name } => contract.create_new_session(name),
- Action::Register {
- creator,
- participant,
- } => {
- let msg_source = msg::source();
- let msg_value = msg::value();
- let reply = contract.register(creator, participant, msg_source, msg_value);
- if reply.is_err() {
- send_value(msg_source, msg_value);
- }
- reply
- }
- Action::CancelRegistration => contract.cancel_register(),
- Action::DeletePlayer { player_id } => contract.delete_player(player_id),
- Action::CancelGame => contract.cancel_game(),
- Action::LeaveGame => contract.leave_game(),
- Action::StartGame {
- fuel_amount,
- payload_amount,
- } => contract.start_game(fuel_amount, payload_amount).await,
- }
-}
-
-#[no_mangle]
-extern fn state() {
- let state = unsafe { STATE.take().expect("Unexpected error in taking state") };
- let query: StateQuery = msg::load().expect("Unable to load the state query");
- let reply = match query {
- StateQuery::All => StateReply::All(state.into()),
- StateQuery::GetGame { player_id } => {
- let game_state = state
- .player_to_game_id
- .get(&player_id)
- .and_then(|creator_id| state.games.get(creator_id))
- .map(|game| {
- let stage = match &game.stage {
- Stage::Registration(participants_data) => StageState::Registration(
- participants_data.clone().into_iter().collect(),
- ),
- Stage::Results(results) => StageState::Results(results.clone()),
- };
-
- GameState {
- admin: game.admin,
- admin_name: game.admin_name.clone(),
- altitude: game.altitude,
- weather: game.weather,
- reward: game.reward,
- stage,
- bid: game.bid,
- }
- });
-
- StateReply::Game(game_state)
- }
- };
- msg::reply(reply, 0).expect("Unable to share the state");
-}
-
-impl From for State {
- fn from(value: Contract) -> Self {
- let Contract {
- games,
- player_to_game_id,
- } = value;
-
- let games = games
- .into_iter()
- .map(|(id, game)| {
- let stage = match game.stage {
- Stage::Registration(participants_data) => {
- StageState::Registration(participants_data.into_iter().collect())
- }
- Stage::Results(results) => StageState::Results(results),
- };
-
- let game_state = GameState {
- admin: game.admin,
- admin_name: game.admin_name.clone(),
- altitude: game.altitude,
- weather: game.weather,
- reward: game.reward,
- stage,
- bid: game.bid,
- };
- (id, game_state)
- })
- .collect();
-
- let player_to_game_id = player_to_game_id.into_iter().collect();
-
- Self {
- games,
- player_to_game_id,
- }
- }
-}
diff --git a/contracts/galactic-express/tests/test.rs b/contracts/galactic-express/tests/test.rs
deleted file mode 100644
index 1b2b8c3ed..000000000
--- a/contracts/galactic-express/tests/test.rs
+++ /dev/null
@@ -1,142 +0,0 @@
-// use utils::prelude::*;
-
-// mod utils;
-
-// #[test]
-// fn test() {
-// let system = utils::initialize_system();
-// let mut rockets = GalEx::initialize(&system, ADMIN);
-
-// let bid = 11_000_000_000_000;
-// system.mint_to(ADMIN, bid);
-// rockets
-// .create_new_session(ADMIN, "admin".to_string(), bid)
-// .succeed(0, 0);
-
-// for player_id in PLAYERS {
-// let player = Participant {
-// id: player_id.into(),
-// name: "player".to_string(),
-// fuel_amount: 42,
-// payload_amount: 20,
-// };
-// system.mint_to(player_id, bid);
-// rockets
-// .register(player_id, ADMIN.into(), player.clone(), bid)
-// .succeed((player_id, player), 0);
-// }
-
-// let state = rockets.state().expect("Unexpected invalid state.");
-
-// if let StageState::Registration(participants) = &state.games[0].1.stage {
-// assert_eq!(participants.len(), 3);
-// }
-
-// rockets
-// .start_game(ADMIN, 42, 20)
-// .succeed(PLAYERS.into_iter().chain(iter::once(ADMIN)).collect(), 3); // 3 since three players win and msg::send_with_gas is sent to them
-
-// let state = rockets.state().expect("Unexpected invalid state.");
-
-// if let StageState::Results(results) = &state.games[0].1.stage {
-// assert_eq!(results.rankings.len(), 4);
-// }
-// }
-
-// #[test]
-// fn cancel_register_and_delete_player() {
-// let system = utils::initialize_system();
-// let mut rockets = GalEx::initialize(&system, ADMIN);
-
-// let bid = 11_000_000_000_000;
-// system.mint_to(ADMIN, bid);
-// rockets
-// .create_new_session(ADMIN, "admin".to_string(), bid)
-// .succeed(0_u128, 0);
-
-// for player_id in PLAYERS {
-// let player = Participant {
-// id: player_id.into(),
-// name: "player".to_string(),
-// fuel_amount: 42,
-// payload_amount: 20,
-// };
-// system.mint_to(player_id, bid);
-// rockets
-// .register(player_id, ADMIN.into(), player.clone(), bid)
-// .succeed((player_id, player), 0);
-// }
-
-// let state = rockets.state().expect("Unexpected invalid state.");
-
-// if let StageState::Registration(participants) = &state.games[0].1.stage {
-// assert_eq!(participants.len(), 3);
-// }
-// assert_eq!(state.player_to_game_id.len(), 4);
-
-// drop(rockets.cancel_register(PLAYERS[0]));
-
-// let state = rockets.state().expect("Unexpected invalid state.");
-
-// if let StageState::Registration(participants) = &state.games[0].1.stage {
-// assert_eq!(participants.len(), 2);
-// }
-// assert_eq!(state.player_to_game_id.len(), 3);
-
-// drop(rockets.delete_player(ADMIN, PLAYERS[1].into()));
-
-// let state = rockets.state().expect("Unexpected invalid state.");
-
-// if let StageState::Registration(participants) = &state.games[0].1.stage {
-// assert_eq!(participants.len(), 1);
-// }
-// assert_eq!(state.player_to_game_id.len(), 2);
-// }
-
-// #[test]
-// fn errors() {
-// let system = utils::initialize_system();
-
-// let mut rockets = GalEx::initialize(&system, ADMIN);
-
-// rockets
-// .register(PLAYERS[0], ADMIN.into(), Default::default(), 0)
-// .failed(Error::NoSuchGame, 0);
-
-// rockets
-// .create_new_session(ADMIN, "admin".to_string(), 0)
-// .succeed(0, 0);
-
-// rockets
-// .register(ADMIN, ADMIN.into(), Default::default(), 0)
-// .failed(Error::SeveralRegistrations, 0);
-
-// rockets
-// .start_game(PLAYERS[0], 42, 20)
-// .failed(Error::NoSuchGame, 0);
-
-// rockets
-// .start_game(ADMIN, 42, 20)
-// .failed(Error::NotEnoughParticipants, 0);
-
-// for player in PLAYERS {
-// rockets
-// .register(player, ADMIN.into(), Default::default(), 0)
-// .succeed((player, Default::default()), 0);
-// }
-
-// rockets
-// .start_game(ADMIN, 101, 100)
-// .failed(Error::FuelOrPayloadOverload, 0);
-
-// rockets
-// .start_game(ADMIN, 100, 101)
-// .failed(Error::FuelOrPayloadOverload, 0);
-// rockets
-// .start_game(ADMIN, 101, 101)
-// .failed(Error::FuelOrPayloadOverload, 0);
-
-// rockets
-// .register(FOREIGN_USER, ADMIN.into(), Default::default(), 0)
-// .failed(Error::SessionFull, 0);
-// }
diff --git a/contracts/galactic-express/tests/utils/common.rs b/contracts/galactic-express/tests/utils/common.rs
deleted file mode 100644
index 7064af081..000000000
--- a/contracts/galactic-express/tests/utils/common.rs
+++ /dev/null
@@ -1,88 +0,0 @@
-use gstd::{fmt::Debug, marker::PhantomData, prelude::*};
-use gtest::{Log, RunResult as InnerRunResult, System};
-
-pub fn initialize_system() -> System {
- let system = System::new();
-
- system.init_logger();
-
- system
-}
-
-#[must_use]
-pub struct RunResult {
- pub result: InnerRunResult,
- check: fn(Event, Check) -> CheckResult,
- ghost_data: PhantomData<(Event, Error)>,
-}
-
-impl
- RunResult
-{
- pub fn new(result: InnerRunResult, check: fn(Event, Check) -> CheckResult) -> Self {
- Self {
- result,
- check,
- ghost_data: PhantomData,
- }
- }
-
- #[track_caller]
- pub fn failed(self, error: Error, index: usize) {
- assert_eq!(
- decode::>(&self.result, index).unwrap_err(),
- error
- );
- }
-
- #[track_caller]
- pub fn succeed(self, value: Check, index: usize) -> CheckResult {
- (self.check)(
- decode::>(&self.result, index).unwrap(),
- value,
- )
- }
-}
-
-#[must_use]
-pub struct InitResult {
- contract_instance: T,
- pub result: InnerRunResult,
- pub is_active: bool,
- ghost_data: PhantomData,
-}
-
-impl InitResult {
- pub fn new(contract_instance: T, result: InnerRunResult, is_active: bool) -> Self {
- Self {
- contract_instance,
- result,
- is_active,
- ghost_data: PhantomData,
- }
- }
-
- fn assert_contains(&self, payload: impl Encode) {
- assert_contains(&self.result, payload);
- }
-
- #[track_caller]
- pub fn succeed(self) -> T {
- assert!(self.is_active);
- self.assert_contains(Ok::<_, E>(()));
-
- self.contract_instance
- }
-}
-
-#[track_caller]
-fn assert_contains(result: &InnerRunResult, payload: impl Encode) {
- assert!(result.contains(&Log::builder().payload(payload)));
-}
-
-fn decode(result: &InnerRunResult, index: usize) -> T {
- match T::decode(&mut result.log()[index].payload()) {
- Ok(ok) => ok,
- Err(_) => std::panic!("{}", String::from_utf8_lossy(result.log()[0].payload())),
- }
-}
diff --git a/contracts/galactic-express/tests/utils/mod.rs b/contracts/galactic-express/tests/utils/mod.rs
deleted file mode 100644
index da31903ea..000000000
--- a/contracts/galactic-express/tests/utils/mod.rs
+++ /dev/null
@@ -1,147 +0,0 @@
-use common::{InitResult, RunResult};
-use galactic_express_io::*;
-use gstd::{collections::HashSet, prelude::*, ActorId};
-use gtest::{Program as InnerProgram, System};
-
-mod common;
-
-pub mod prelude;
-
-pub use common::initialize_system;
-
-pub const FOREIGN_USER: u64 = 1029384756123;
-pub const ADMIN: u64 = 10;
-pub const PLAYERS: [u64; 3] = [12, 13, 14];
-
-type GalExResult = RunResult;
-
-pub struct GalEx<'a>(InnerProgram<'a>);
-
-impl<'a> GalEx<'a> {
- pub fn initialize(system: &'a System, from: u64) -> Self {
- let program = InnerProgram::current(system);
-
- let result = program.send(from, 0);
- let is_active = system.is_active_program(program.id());
-
- InitResult::<_, Error>::new(Self(program), result, is_active).succeed()
- }
-
- pub fn create_new_session(
- &mut self,
- from: u64,
- name: String,
- bid: u128,
- ) -> GalExResult {
- RunResult::new(
- self.0
- .send_with_value(from, Action::CreateNewSession { name }, bid),
- |event, _id| {
- if let Event::NewSessionCreated {
- altitude, reward, ..
- } = event
- {
- assert!(((TURN_ALTITUDE.0 * (TURNS as u16))
- ..(TURN_ALTITUDE.1 * (TURNS as u16)))
- .contains(&altitude));
- assert!((REWARD.0..REWARD.1).contains(&reward));
- reward
- } else {
- unreachable!()
- }
- },
- )
- }
-
- pub fn register(
- &mut self,
- from: u64,
- creator: ActorId,
- participant: Participant,
- bid: u128,
- ) -> GalExResult<(u64, Participant)> {
- RunResult::new(
- self.0.send_with_value(
- from,
- Action::Register {
- creator,
- participant,
- },
- bid,
- ),
- |event, (actor, participant)| {
- assert_eq!(Event::Registered(actor.into(), participant), event)
- },
- )
- }
-
- pub fn cancel_register(&mut self, from: u64) -> GalExResult<(u64, Participant)> {
- RunResult::new(
- self.0.send(from, Action::CancelRegistration),
- |event, (_actor, _participant)| assert_eq!(Event::RegistrationCanceled, event),
- )
- }
-
- pub fn delete_player(
- &mut self,
- from: u64,
- player_id: ActorId,
- ) -> GalExResult<(u64, Participant)> {
- RunResult::new(
- self.0.send(from, Action::DeletePlayer { player_id }),
- |_, _| {},
- )
- }
-
- pub fn start_game(
- &mut self,
- from: u64,
- fuel_amount: u8,
- payload_amount: u8,
- ) -> GalExResult> {
- RunResult::new(
- self.0.send(
- from,
- Action::StartGame {
- fuel_amount,
- payload_amount,
- },
- ),
- |event, players| {
- if let Event::GameFinished(results) = event {
- assert!(results.turns.len() == TURNS);
- assert!(results.rankings.len() == MAX_PARTICIPANTS);
- assert!(results
- .turns
- .iter()
- .all(|players| players.len() == MAX_PARTICIPANTS));
-
- let players: HashSet = players.into_iter().map(|p| p.into()).collect();
-
- assert!(results
- .turns
- .iter()
- .map(|players| players
- .iter()
- .map(|(actor, _)| *actor)
- .collect::>())
- .all(|true_players| true_players == players));
- } else {
- unreachable!()
- }
- },
- )
- }
-
- pub fn state(&self) -> Option {
- let reply = self
- .0
- .read_state(StateQuery::All)
- .expect("Unexpected invalid state.");
- if let StateReply::All(state) = reply {
- Some(state)
- } else {
- None
- }
- }
-}
diff --git a/contracts/galactic-express/tests/utils/prelude.rs b/contracts/galactic-express/tests/utils/prelude.rs
deleted file mode 100644
index ed2994b73..000000000
--- a/contracts/galactic-express/tests/utils/prelude.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-pub use super::{GalEx, ADMIN, FOREIGN_USER, PLAYERS};
-pub use galactic_express_io::*;
-pub use gstd::prelude::*;
diff --git a/contracts/galactic-express/wasm/Cargo.toml b/contracts/galactic-express/wasm/Cargo.toml
new file mode 100644
index 000000000..eb0879e21
--- /dev/null
+++ b/contracts/galactic-express/wasm/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "galactic-express"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+
+[dependencies]
+sails-rs.workspace = true
+galactic-express-app = { path = "../app" }
+
+[build-dependencies]
+gear-wasm-builder.workspace = true
+sails-idl-gen.workspace = true
+sails-client-gen.workspace = true
+galactic-express-app = { path = "../app" }
+
+[lib]
+crate-type = ["rlib"]
+name = "galactic_express"
diff --git a/contracts/galactic-express/wasm/build.rs b/contracts/galactic-express/wasm/build.rs
new file mode 100644
index 000000000..511459d11
--- /dev/null
+++ b/contracts/galactic-express/wasm/build.rs
@@ -0,0 +1,20 @@
+use galactic_express_app::Program;
+use sails_client_gen::ClientGenerator;
+use sails_idl_gen::program;
+use std::{env, fs::File, path::PathBuf};
+
+fn main() {
+ gear_wasm_builder::build();
+
+ let manifest_dir_path = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
+
+ let idl_file_path = manifest_dir_path.join("galactic-express.idl");
+
+ let idl_file = File::create(idl_file_path.clone()).unwrap();
+
+ program::generate_idl::(idl_file).unwrap();
+
+ ClientGenerator::from_idl_path(&idl_file_path)
+ .generate_to(PathBuf::from(env::var("OUT_DIR").unwrap()).join("galactic_express_client.rs"))
+ .unwrap();
+}
diff --git a/contracts/galactic-express/wasm/galactic-express.idl b/contracts/galactic-express/wasm/galactic-express.idl
new file mode 100644
index 000000000..f2449f14c
--- /dev/null
+++ b/contracts/galactic-express/wasm/galactic-express.idl
@@ -0,0 +1,90 @@
+type Participant = struct {
+ id: actor_id,
+ name: str,
+ fuel_amount: u8,
+ payload_amount: u8,
+};
+
+type State = struct {
+ games: vec struct { actor_id, GameState },
+ player_to_game_id: vec struct { actor_id, actor_id },
+ dns_info: opt struct { actor_id, str },
+ admin: actor_id,
+};
+
+type GameState = struct {
+ admin: actor_id,
+ admin_name: str,
+ altitude: u16,
+ weather: Weather,
+ reward: u128,
+ stage: StageState,
+ bid: u128,
+};
+
+type Weather = enum {
+ Clear,
+ Cloudy,
+ Rainy,
+ Stormy,
+ Thunder,
+ Tornado,
+};
+
+type StageState = enum {
+ Registration: vec struct { actor_id, Participant },
+ Results: Results,
+};
+
+type Results = struct {
+ turns: vec vec struct { actor_id, Turn },
+ rankings: vec struct { actor_id, u128 },
+ participants: vec struct { actor_id, Participant },
+};
+
+type Turn = enum {
+ Alive: struct { fuel_left: u8, payload_amount: u8 },
+ Destroyed: HaltReason,
+};
+
+type HaltReason = enum {
+ PayloadOverload,
+ FuelOverload,
+ SeparationFailure,
+ AsteroidCollision,
+ FuelShortage,
+ EngineFailure,
+};
+
+constructor {
+ New : (dns_id_and_name: opt struct { actor_id, str });
+};
+
+service GalacticExpress {
+ CancelGame : () -> null;
+ CancelRegister : () -> null;
+ ChangeAdmin : (new_admin: actor_id) -> null;
+ CreateNewSession : (name: str) -> null;
+ DeletePlayer : (player_id: actor_id) -> null;
+ Kill : (inheritor: actor_id) -> null;
+ LeaveGame : () -> null;
+ Register : (creator: actor_id, participant: Participant) -> null;
+ StartGame : (fuel_amount: u8, payload_amount: u8) -> null;
+ query Admin : () -> actor_id;
+ query All : () -> State;
+ query DnsInfo : () -> opt struct { actor_id, str };
+ query GetGame : (player_id: actor_id) -> opt GameState;
+
+ events {
+ GameFinished: Results;
+ NewSessionCreated: struct { altitude: u16, weather: Weather, reward: u128, bid: u128 };
+ Registered: struct { actor_id, Participant };
+ RegistrationCanceled;
+ PlayerDeleted: struct { player_id: actor_id };
+ GameCanceled;
+ GameLeft;
+ AdminChanged: struct { new_admin: actor_id };
+ Killed: struct { inheritor: actor_id };
+ }
+};
+
diff --git a/contracts/galactic-express/wasm/src/lib.rs b/contracts/galactic-express/wasm/src/lib.rs
new file mode 100644
index 000000000..f26bdedc7
--- /dev/null
+++ b/contracts/galactic-express/wasm/src/lib.rs
@@ -0,0 +1,6 @@
+#![no_std]
+include!(concat!(env!("OUT_DIR"), "/galactic_express_client.rs"));
+include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs"));
+
+#[cfg(target_arch = "wasm32")]
+pub use galactic_express_app::wasm::*;
diff --git a/contracts/tic-tac-toe/app/Cargo.toml b/contracts/tic-tac-toe/app/Cargo.toml
index 8bf0aa440..19678eba7 100644
--- a/contracts/tic-tac-toe/app/Cargo.toml
+++ b/contracts/tic-tac-toe/app/Cargo.toml
@@ -7,7 +7,6 @@ license.workspace = true
[dependencies]
gstd = { workspace = true, features = ["debug"] }
sails-rs = { workspace = true, features = ["gtest"] }
-
schnorrkel.workspace = true
[dev-dependencies]
diff --git a/contracts/tic-tac-toe/app/src/services/game/funcs.rs b/contracts/tic-tac-toe/app/src/services/game/funcs.rs
index e647bb341..6eeb5335b 100644
--- a/contracts/tic-tac-toe/app/src/services/game/funcs.rs
+++ b/contracts/tic-tac-toe/app/src/services/game/funcs.rs
@@ -2,8 +2,7 @@ use crate::services::game::{
Config, Event, GameError, GameInstance, GameResult, Mark, Storage, VICTORIES,
};
use crate::services::session::utils::{ActionsForSession, SessionData};
-use gstd::{collections::HashMap, exec, msg};
-use sails_rs::prelude::*;
+use sails_rs::{prelude::*, collections::HashMap, gstd::{exec, msg}};
pub fn start_game(
storage: &mut Storage,
diff --git a/contracts/vara-man/app/src/services/game/funcs.rs b/contracts/vara-man/app/src/services/game/funcs.rs
index e048cb7db..87eacc554 100644
--- a/contracts/vara-man/app/src/services/game/funcs.rs
+++ b/contracts/vara-man/app/src/services/game/funcs.rs
@@ -3,8 +3,7 @@ use crate::services::game::{
MAX_PARTICIPANTS,
};
use crate::services::session::utils::{ActionsForSession, SessionData};
-use gstd::{collections::HashMap, exec, msg, prelude::*, ActorId};
-use sails_rs::U256;
+use sails_rs::{U256, gstd::{exec, msg}, prelude::*, collections::HashMap, ActorId};
pub fn create_new_tournament(
storage: &mut GameStorage,
diff --git a/contracts/vara-man/app/src/services/game/mod.rs b/contracts/vara-man/app/src/services/game/mod.rs
index c198a051d..2a1eeaa16 100644
--- a/contracts/vara-man/app/src/services/game/mod.rs
+++ b/contracts/vara-man/app/src/services/game/mod.rs
@@ -1,7 +1,6 @@
use super::session::Storage as SessionStorage;
use crate::services;
-use gstd::{collections::HashMap, exec, msg, String};
-use sails_rs::{gstd::service, prelude::*};
+use sails_rs::{gstd::{service, exec, msg}, prelude::*, collections::HashMap};
mod funcs;
pub mod utils;
use utils::*;
diff --git a/frontend/apps/battleship-zk/src/features/multiplayer/components/create-game-form/create-game-form.tsx b/frontend/apps/battleship-zk/src/features/multiplayer/components/create-game-form/create-game-form.tsx
index 8ec7c1c37..383b7a7b7 100644
--- a/frontend/apps/battleship-zk/src/features/multiplayer/components/create-game-form/create-game-form.tsx
+++ b/frontend/apps/battleship-zk/src/features/multiplayer/components/create-game-form/create-game-form.tsx
@@ -12,10 +12,6 @@ import { useMultiplayerGame } from '../../hooks';
import { useCreateGameMessage } from '../../sails/messages';
import styles from './CreateGameForm.module.scss';
-export interface ContractFormValues {
- [key: string]: string;
-}
-
type CreateFormValues = {
fee: number;
name: string;
diff --git a/frontend/apps/battleship-zk/src/features/multiplayer/components/join-game-form/join-game-form.tsx b/frontend/apps/battleship-zk/src/features/multiplayer/components/join-game-form/join-game-form.tsx
index 8e9741e64..2d2eacff5 100644
--- a/frontend/apps/battleship-zk/src/features/multiplayer/components/join-game-form/join-game-form.tsx
+++ b/frontend/apps/battleship-zk/src/features/multiplayer/components/join-game-form/join-game-form.tsx
@@ -16,10 +16,6 @@ import { useJoinGameMessage } from '../../sails/messages';
import { useMultiGameQuery } from '../../sails/queries';
import styles from './JoinGameForm.module.scss';
-export interface ContractFormValues {
- [key: string]: string;
-}
-
type Props = {
onCancel: () => void;
};
diff --git a/frontend/apps/galactic-express/src/app/hooks/index.ts b/frontend/apps/galactic-express/src/app/hooks/index.ts
new file mode 100644
index 000000000..e4b56d017
--- /dev/null
+++ b/frontend/apps/galactic-express/src/app/hooks/index.ts
@@ -0,0 +1,2 @@
+export { usePending } from './use-pending';
+export * from './use-sign-and-send';
diff --git a/frontend/apps/galactic-express/src/app/hooks/use-pending.ts b/frontend/apps/galactic-express/src/app/hooks/use-pending.ts
new file mode 100644
index 000000000..0ebee7ba0
--- /dev/null
+++ b/frontend/apps/galactic-express/src/app/hooks/use-pending.ts
@@ -0,0 +1,8 @@
+import { useAtom } from 'jotai';
+import { IS_LOADING } from 'atoms';
+
+export function usePending() {
+ const [pending, setPending] = useAtom(IS_LOADING);
+
+ return { pending, setPending };
+}
diff --git a/frontend/apps/galactic-express/src/app/hooks/use-sign-and-send.ts b/frontend/apps/galactic-express/src/app/hooks/use-sign-and-send.ts
new file mode 100644
index 000000000..e7d779465
--- /dev/null
+++ b/frontend/apps/galactic-express/src/app/hooks/use-sign-and-send.ts
@@ -0,0 +1,44 @@
+import { usePending } from './';
+import { useCheckBalance } from '@dapps-frontend/hooks';
+import { useAlert } from '@gear-js/react-hooks';
+import { GenericTransactionReturn, TransactionReturn } from '@gear-js/react-hooks/dist/esm/hooks/sails/types';
+
+export type Options = {
+ onSuccess?: () => void;
+ onError?: (error?: Error) => void;
+};
+
+export const useSignAndSend = () => {
+ const { checkBalance } = useCheckBalance();
+ const { setPending } = usePending();
+ const alert = useAlert();
+
+ const signAndSend = async (
+ transaction: TransactionReturn<() => GenericTransactionReturn>,
+ options?: Options,
+ ) => {
+ const { onSuccess, onError } = options || {};
+ const calculatedGas = Number(transaction.extrinsic.args[2].toString());
+ checkBalance(
+ calculatedGas,
+ async () => {
+ try {
+ const { response } = await transaction.signAndSend();
+ await response();
+ onSuccess?.();
+ setPending(false);
+ } catch (e) {
+ onError?.(e as Error);
+ setPending(false);
+ console.error(e);
+ if (typeof e === 'string') {
+ alert.error(e);
+ }
+ }
+ },
+ onError,
+ );
+ };
+
+ return { signAndSend };
+};
diff --git a/frontend/apps/galactic-express/src/app/utils/index.ts b/frontend/apps/galactic-express/src/app/utils/index.ts
new file mode 100644
index 000000000..15858e186
--- /dev/null
+++ b/frontend/apps/galactic-express/src/app/utils/index.ts
@@ -0,0 +1 @@
+export * from './sails';
diff --git a/frontend/apps/galactic-express/src/app/utils/sails/events/index.ts b/frontend/apps/galactic-express/src/app/utils/sails/events/index.ts
new file mode 100644
index 000000000..2224d3720
--- /dev/null
+++ b/frontend/apps/galactic-express/src/app/utils/sails/events/index.ts
@@ -0,0 +1,2 @@
+export { useEventGameCanceledSubscription } from './use-event-game-canceled-subscription';
+export { useEventPlayerDeletedSubscription } from './use-event-player-deleted-subscription';
diff --git a/frontend/apps/galactic-express/src/app/utils/sails/events/use-event-game-canceled-subscription.ts b/frontend/apps/galactic-express/src/app/utils/sails/events/use-event-game-canceled-subscription.ts
new file mode 100644
index 000000000..0c520a067
--- /dev/null
+++ b/frontend/apps/galactic-express/src/app/utils/sails/events/use-event-game-canceled-subscription.ts
@@ -0,0 +1,22 @@
+import { useProgramEvent } from '@gear-js/react-hooks';
+import { useProgram } from 'app/utils';
+import { REGISTRATION_STATUS } from 'atoms';
+import { useSetAtom } from 'jotai';
+
+export function useEventGameCanceledSubscription(isUserAdmin: boolean) {
+ const program = useProgram();
+ const setRegistrationStatus = useSetAtom(REGISTRATION_STATUS);
+
+ const onData = () => {
+ if (!isUserAdmin) {
+ setRegistrationStatus('GameCanceled');
+ }
+ };
+
+ useProgramEvent({
+ program,
+ serviceName: 'galacticExpress',
+ functionName: 'subscribeToGameCanceledEvent',
+ onData,
+ });
+}
diff --git a/frontend/apps/galactic-express/src/app/utils/sails/events/use-event-player-deleted-subscription.ts b/frontend/apps/galactic-express/src/app/utils/sails/events/use-event-player-deleted-subscription.ts
new file mode 100644
index 000000000..402ae382d
--- /dev/null
+++ b/frontend/apps/galactic-express/src/app/utils/sails/events/use-event-player-deleted-subscription.ts
@@ -0,0 +1,24 @@
+import { HexString } from '@gear-js/api';
+import { useProgramEvent, useAccount } from '@gear-js/react-hooks';
+import { useProgram } from 'app/utils';
+import { REGISTRATION_STATUS } from 'atoms';
+import { useSetAtom } from 'jotai';
+
+export function useEventPlayerDeletedSubscription() {
+ const program = useProgram();
+ const { account } = useAccount();
+ const setRegistrationStatus = useSetAtom(REGISTRATION_STATUS);
+
+ const onData = ({ player_id }: { player_id: HexString }) => {
+ if (account?.decodedAddress === player_id) {
+ setRegistrationStatus('PlayerRemoved');
+ }
+ };
+
+ useProgramEvent({
+ program,
+ serviceName: 'galacticExpress',
+ functionName: 'subscribeToPlayerDeletedEvent',
+ onData,
+ });
+}
diff --git a/frontend/apps/galactic-express/src/app/utils/sails/galactic-express.idl b/frontend/apps/galactic-express/src/app/utils/sails/galactic-express.idl
new file mode 100644
index 000000000..f2449f14c
--- /dev/null
+++ b/frontend/apps/galactic-express/src/app/utils/sails/galactic-express.idl
@@ -0,0 +1,90 @@
+type Participant = struct {
+ id: actor_id,
+ name: str,
+ fuel_amount: u8,
+ payload_amount: u8,
+};
+
+type State = struct {
+ games: vec struct { actor_id, GameState },
+ player_to_game_id: vec struct { actor_id, actor_id },
+ dns_info: opt struct { actor_id, str },
+ admin: actor_id,
+};
+
+type GameState = struct {
+ admin: actor_id,
+ admin_name: str,
+ altitude: u16,
+ weather: Weather,
+ reward: u128,
+ stage: StageState,
+ bid: u128,
+};
+
+type Weather = enum {
+ Clear,
+ Cloudy,
+ Rainy,
+ Stormy,
+ Thunder,
+ Tornado,
+};
+
+type StageState = enum {
+ Registration: vec struct { actor_id, Participant },
+ Results: Results,
+};
+
+type Results = struct {
+ turns: vec vec struct { actor_id, Turn },
+ rankings: vec struct { actor_id, u128 },
+ participants: vec struct { actor_id, Participant },
+};
+
+type Turn = enum {
+ Alive: struct { fuel_left: u8, payload_amount: u8 },
+ Destroyed: HaltReason,
+};
+
+type HaltReason = enum {
+ PayloadOverload,
+ FuelOverload,
+ SeparationFailure,
+ AsteroidCollision,
+ FuelShortage,
+ EngineFailure,
+};
+
+constructor {
+ New : (dns_id_and_name: opt struct { actor_id, str });
+};
+
+service GalacticExpress {
+ CancelGame : () -> null;
+ CancelRegister : () -> null;
+ ChangeAdmin : (new_admin: actor_id) -> null;
+ CreateNewSession : (name: str) -> null;
+ DeletePlayer : (player_id: actor_id) -> null;
+ Kill : (inheritor: actor_id) -> null;
+ LeaveGame : () -> null;
+ Register : (creator: actor_id, participant: Participant) -> null;
+ StartGame : (fuel_amount: u8, payload_amount: u8) -> null;
+ query Admin : () -> actor_id;
+ query All : () -> State;
+ query DnsInfo : () -> opt struct { actor_id, str };
+ query GetGame : (player_id: actor_id) -> opt GameState;
+
+ events {
+ GameFinished: Results;
+ NewSessionCreated: struct { altitude: u16, weather: Weather, reward: u128, bid: u128 };
+ Registered: struct { actor_id, Participant };
+ RegistrationCanceled;
+ PlayerDeleted: struct { player_id: actor_id };
+ GameCanceled;
+ GameLeft;
+ AdminChanged: struct { new_admin: actor_id };
+ Killed: struct { inheritor: actor_id };
+ }
+};
+
diff --git a/frontend/apps/galactic-express/src/app/utils/sails/index.ts b/frontend/apps/galactic-express/src/app/utils/sails/index.ts
new file mode 100644
index 000000000..052b4eecf
--- /dev/null
+++ b/frontend/apps/galactic-express/src/app/utils/sails/index.ts
@@ -0,0 +1,5 @@
+export * from './sails';
+export * from './lib';
+export * from './events';
+export * from './queries';
+export * from './messages';
diff --git a/frontend/apps/galactic-express/src/app/utils/sails/lib.ts b/frontend/apps/galactic-express/src/app/utils/sails/lib.ts
new file mode 100644
index 000000000..9ad8d332e
--- /dev/null
+++ b/frontend/apps/galactic-express/src/app/utils/sails/lib.ts
@@ -0,0 +1,487 @@
+import { TransactionBuilder, getServiceNamePrefix, getFnNamePrefix, ZERO_ADDRESS } from 'sails-js';
+import { GearApi, HexString, decodeAddress } from '@gear-js/api';
+import { TypeRegistry } from '@polkadot/types';
+
+type ActorId = HexString;
+
+export interface Participant {
+ id: ActorId;
+ name: string;
+ fuel_amount: number;
+ payload_amount: number;
+}
+
+export interface State {
+ games: Array<[ActorId, GameState]>;
+ player_to_game_id: Array<[ActorId, ActorId]>;
+ dns_info: [ActorId, string] | null;
+ admin: ActorId;
+}
+
+export interface GameState {
+ admin: ActorId;
+ admin_name: string;
+ altitude: number;
+ weather: Weather;
+ reward: number | string | bigint;
+ stage: StageState;
+ bid: number | string | bigint;
+}
+
+export type Weather = 'clear' | 'cloudy' | 'rainy' | 'stormy' | 'thunder' | 'tornado';
+
+export type StageState = { registration: Array<[ActorId, Participant]> } | { results: Results };
+
+export interface Results {
+ turns: Array>;
+ rankings: Array<[ActorId, number | string | bigint]>;
+ participants: Array<[ActorId, Participant]>;
+}
+
+export type Turn = { alive: { fuel_left: number; payload_amount: number } } | { destroyed: HaltReason };
+
+export type HaltReason =
+ | 'payloadOverload'
+ | 'fuelOverload'
+ | 'separationFailure'
+ | 'asteroidCollision'
+ | 'fuelShortage'
+ | 'engineFailure';
+
+export class Program {
+ public readonly registry: TypeRegistry;
+ public readonly galacticExpress: GalacticExpress;
+
+ constructor(public api: GearApi, public programId?: `0x${string}`) {
+ const types: Record = {
+ Participant: { id: '[u8;32]', name: 'String', fuel_amount: 'u8', payload_amount: 'u8' },
+ State: {
+ games: 'Vec<([u8;32], GameState)>',
+ player_to_game_id: 'Vec<([u8;32], [u8;32])>',
+ dns_info: 'Option<([u8;32], String)>',
+ admin: '[u8;32]',
+ },
+ GameState: {
+ admin: '[u8;32]',
+ admin_name: 'String',
+ altitude: 'u16',
+ weather: 'Weather',
+ reward: 'u128',
+ stage: 'StageState',
+ bid: 'u128',
+ },
+ Weather: { _enum: ['Clear', 'Cloudy', 'Rainy', 'Stormy', 'Thunder', 'Tornado'] },
+ StageState: { _enum: { Registration: 'Vec<([u8;32], Participant)>', Results: 'Results' } },
+ Results: {
+ turns: 'Vec>',
+ rankings: 'Vec<([u8;32], u128)>',
+ participants: 'Vec<([u8;32], Participant)>',
+ },
+ Turn: { _enum: { Alive: { fuel_left: 'u8', payload_amount: 'u8' }, Destroyed: 'HaltReason' } },
+ HaltReason: {
+ _enum: [
+ 'PayloadOverload',
+ 'FuelOverload',
+ 'SeparationFailure',
+ 'AsteroidCollision',
+ 'FuelShortage',
+ 'EngineFailure',
+ ],
+ },
+ };
+
+ this.registry = new TypeRegistry();
+ this.registry.setKnownTypes({ types });
+ this.registry.register(types);
+
+ this.galacticExpress = new GalacticExpress(this);
+ }
+
+ newCtorFromCode(code: Uint8Array | Buffer, dns_id_and_name: [ActorId, string] | null): TransactionBuilder {
+ const builder = new TransactionBuilder(
+ this.api,
+ this.registry,
+ 'upload_program',
+ ['New', dns_id_and_name],
+ '(String, Option<([u8;32], String)>)',
+ 'String',
+ code,
+ );
+
+ this.programId = builder.programId;
+ return builder;
+ }
+
+ newCtorFromCodeId(codeId: `0x${string}`, dns_id_and_name: [ActorId, string] | null) {
+ const builder = new TransactionBuilder(
+ this.api,
+ this.registry,
+ 'create_program',
+ ['New', dns_id_and_name],
+ '(String, Option<([u8;32], String)>)',
+ 'String',
+ codeId,
+ );
+
+ this.programId = builder.programId;
+ return builder;
+ }
+}
+
+export class GalacticExpress {
+ constructor(private _program: Program) {}
+
+ public cancelGame(): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['GalacticExpress', 'CancelGame'],
+ '(String, String)',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public cancelRegister(): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['GalacticExpress', 'CancelRegister'],
+ '(String, String)',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public changeAdmin(new_admin: ActorId): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['GalacticExpress', 'ChangeAdmin', new_admin],
+ '(String, String, [u8;32])',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public createNewSession(name: string): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['GalacticExpress', 'CreateNewSession', name],
+ '(String, String, String)',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public deletePlayer(player_id: ActorId): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['GalacticExpress', 'DeletePlayer', player_id],
+ '(String, String, [u8;32])',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public kill(inheritor: ActorId): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['GalacticExpress', 'Kill', inheritor],
+ '(String, String, [u8;32])',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public leaveGame(): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['GalacticExpress', 'LeaveGame'],
+ '(String, String)',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public register(creator: ActorId, participant: Participant): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['GalacticExpress', 'Register', creator, participant],
+ '(String, String, [u8;32], Participant)',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public startGame(fuel_amount: number, payload_amount: number): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['GalacticExpress', 'StartGame', fuel_amount, payload_amount],
+ '(String, String, u8, u8)',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public async admin(
+ originAddress?: string,
+ value?: number | string | bigint,
+ atBlock?: `0x${string}`,
+ ): Promise {
+ const payload = this._program.registry.createType('(String, String)', ['GalacticExpress', 'Admin']).toHex();
+ const reply = await this._program.api.message.calculateReply({
+ destination: this._program.programId!,
+ origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS,
+ payload,
+ value: value || 0,
+ gasLimit: this._program.api.blockGasLimit.toBigInt(),
+ at: atBlock,
+ });
+ if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString());
+ const result = this._program.registry.createType('(String, String, [u8;32])', reply.payload);
+ return result[2].toJSON() as unknown as ActorId;
+ }
+
+ public async all(originAddress?: string, value?: number | string | bigint, atBlock?: `0x${string}`): Promise {
+ const payload = this._program.registry.createType('(String, String)', ['GalacticExpress', 'All']).toHex();
+ const reply = await this._program.api.message.calculateReply({
+ destination: this._program.programId!,
+ origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS,
+ payload,
+ value: value || 0,
+ gasLimit: this._program.api.blockGasLimit.toBigInt(),
+ at: atBlock,
+ });
+ if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString());
+ const result = this._program.registry.createType('(String, String, State)', reply.payload);
+ return result[2].toJSON() as unknown as State;
+ }
+
+ public async dnsInfo(
+ originAddress?: string,
+ value?: number | string | bigint,
+ atBlock?: `0x${string}`,
+ ): Promise<[ActorId, string] | null> {
+ const payload = this._program.registry.createType('(String, String)', ['GalacticExpress', 'DnsInfo']).toHex();
+ const reply = await this._program.api.message.calculateReply({
+ destination: this._program.programId!,
+ origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS,
+ payload,
+ value: value || 0,
+ gasLimit: this._program.api.blockGasLimit.toBigInt(),
+ at: atBlock,
+ });
+ if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString());
+ const result = this._program.registry.createType('(String, String, Option<([u8;32], String)>)', reply.payload);
+ return result[2].toJSON() as unknown as [ActorId, string] | null;
+ }
+
+ public async getGame(
+ player_id: ActorId,
+ originAddress?: string,
+ value?: number | string | bigint,
+ atBlock?: `0x${string}`,
+ ): Promise {
+ const payload = this._program.registry
+ .createType('(String, String, [u8;32])', ['GalacticExpress', 'GetGame', player_id])
+ .toHex();
+ const reply = await this._program.api.message.calculateReply({
+ destination: this._program.programId!,
+ origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS,
+ payload,
+ value: value || 0,
+ gasLimit: this._program.api.blockGasLimit.toBigInt(),
+ at: atBlock,
+ });
+ if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString());
+ const result = this._program.registry.createType('(String, String, Option)', reply.payload);
+ return result[2].toJSON() as unknown as GameState | null;
+ }
+
+ public subscribeToGameFinishedEvent(callback: (data: Results) => void | Promise): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'GalacticExpress' && getFnNamePrefix(payload) === 'GameFinished') {
+ callback(
+ this._program.registry
+ .createType('(String, String, Results)', message.payload)[2]
+ .toJSON() as unknown as Results,
+ );
+ }
+ });
+ }
+
+ public subscribeToNewSessionCreatedEvent(
+ callback: (data: {
+ altitude: number;
+ weather: Weather;
+ reward: number | string | bigint;
+ bid: number | string | bigint;
+ }) => void | Promise,
+ ): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'GalacticExpress' && getFnNamePrefix(payload) === 'NewSessionCreated') {
+ callback(
+ this._program.registry
+ .createType(
+ '(String, String, {"altitude":"u16","weather":"Weather","reward":"u128","bid":"u128"})',
+ message.payload,
+ )[2]
+ .toJSON() as unknown as {
+ altitude: number;
+ weather: Weather;
+ reward: number | string | bigint;
+ bid: number | string | bigint;
+ },
+ );
+ }
+ });
+ }
+
+ public subscribeToRegisteredEvent(
+ callback: (data: [ActorId, Participant]) => void | Promise,
+ ): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'GalacticExpress' && getFnNamePrefix(payload) === 'Registered') {
+ callback(
+ this._program.registry
+ .createType('(String, String, ([u8;32], Participant))', message.payload)[2]
+ .toJSON() as unknown as [ActorId, Participant],
+ );
+ }
+ });
+ }
+
+ public subscribeToRegistrationCanceledEvent(callback: (data: null) => void | Promise): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'GalacticExpress' && getFnNamePrefix(payload) === 'RegistrationCanceled') {
+ callback(null);
+ }
+ });
+ }
+
+ public subscribeToPlayerDeletedEvent(
+ callback: (data: { player_id: ActorId }) => void | Promise,
+ ): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'GalacticExpress' && getFnNamePrefix(payload) === 'PlayerDeleted') {
+ callback(
+ this._program.registry
+ .createType('(String, String, {"player_id":"[u8;32]"})', message.payload)[2]
+ .toJSON() as unknown as { player_id: ActorId },
+ );
+ }
+ });
+ }
+
+ public subscribeToGameCanceledEvent(callback: (data: null) => void | Promise): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'GalacticExpress' && getFnNamePrefix(payload) === 'GameCanceled') {
+ callback(null);
+ }
+ });
+ }
+
+ public subscribeToGameLeftEvent(callback: (data: null) => void | Promise): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'GalacticExpress' && getFnNamePrefix(payload) === 'GameLeft') {
+ callback(null);
+ }
+ });
+ }
+
+ public subscribeToAdminChangedEvent(
+ callback: (data: { new_admin: ActorId }) => void | Promise,
+ ): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'GalacticExpress' && getFnNamePrefix(payload) === 'AdminChanged') {
+ callback(
+ this._program.registry
+ .createType('(String, String, {"new_admin":"[u8;32]"})', message.payload)[2]
+ .toJSON() as unknown as { new_admin: ActorId },
+ );
+ }
+ });
+ }
+
+ public subscribeToKilledEvent(callback: (data: { inheritor: ActorId }) => void | Promise): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'GalacticExpress' && getFnNamePrefix(payload) === 'Killed') {
+ callback(
+ this._program.registry
+ .createType('(String, String, {"inheritor":"[u8;32]"})', message.payload)[2]
+ .toJSON() as unknown as { inheritor: ActorId },
+ );
+ }
+ });
+ }
+}
diff --git a/frontend/apps/galactic-express/src/app/utils/sails/messages/index.ts b/frontend/apps/galactic-express/src/app/utils/sails/messages/index.ts
new file mode 100644
index 000000000..5f9274801
--- /dev/null
+++ b/frontend/apps/galactic-express/src/app/utils/sails/messages/index.ts
@@ -0,0 +1,7 @@
+export { useCancelGameMessage } from './use-cancel-game-message';
+export { useCancelRegisterMessage } from './use-cancel-register-message';
+export { useCreateNewSessionMessage } from './use-create-new-session-message';
+export { useDeletePlayerMessage } from './use-delete-player-message';
+export { useRegisterMessage } from './use-register-message';
+export { useStartGameMessage } from './use-start-game-message';
+export { useLeaveGameMessage } from './use-leave-game-message';
diff --git a/frontend/apps/galactic-express/src/app/utils/sails/messages/use-cancel-game-message.ts b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-cancel-game-message.ts
new file mode 100644
index 000000000..cedd312bb
--- /dev/null
+++ b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-cancel-game-message.ts
@@ -0,0 +1,23 @@
+import { usePrepareProgramTransaction } from '@gear-js/react-hooks';
+import { useProgram } from 'app/utils';
+import { Options, useSignAndSend } from 'app/hooks';
+
+export const useCancelGameMessage = () => {
+ const program = useProgram();
+ const { prepareTransactionAsync } = usePrepareProgramTransaction({
+ program,
+ serviceName: 'galacticExpress',
+ functionName: 'cancelGame',
+ });
+ const { signAndSend } = useSignAndSend();
+
+ const cancelGameMessage = async (options: Options) => {
+ const { transaction } = await prepareTransactionAsync({
+ args: [],
+ gasLimit: { increaseGas: 10 },
+ });
+ signAndSend(transaction, options);
+ };
+
+ return { cancelGameMessage };
+};
diff --git a/frontend/apps/galactic-express/src/app/utils/sails/messages/use-cancel-register-message.ts b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-cancel-register-message.ts
new file mode 100644
index 000000000..d204b9b12
--- /dev/null
+++ b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-cancel-register-message.ts
@@ -0,0 +1,23 @@
+import { usePrepareProgramTransaction } from '@gear-js/react-hooks';
+import { useProgram } from 'app/utils';
+import { Options, useSignAndSend } from 'app/hooks';
+
+export const useCancelRegisterMessage = () => {
+ const program = useProgram();
+ const { prepareTransactionAsync } = usePrepareProgramTransaction({
+ program,
+ serviceName: 'galacticExpress',
+ functionName: 'cancelRegister',
+ });
+ const { signAndSend } = useSignAndSend();
+
+ const cancelRegisterMessage = async (options: Options) => {
+ const { transaction } = await prepareTransactionAsync({
+ args: [],
+ gasLimit: { increaseGas: 10 },
+ });
+ signAndSend(transaction, options);
+ };
+
+ return { cancelRegisterMessage };
+};
diff --git a/frontend/apps/galactic-express/src/app/utils/sails/messages/use-create-new-session-message.ts b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-create-new-session-message.ts
new file mode 100644
index 000000000..2870d14c2
--- /dev/null
+++ b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-create-new-session-message.ts
@@ -0,0 +1,29 @@
+import { usePrepareProgramTransaction } from '@gear-js/react-hooks';
+import { useProgram } from 'app/utils';
+import { Options, useSignAndSend } from 'app/hooks';
+
+type Params = {
+ name: string;
+ value?: bigint;
+};
+
+export const useCreateNewSessionMessage = () => {
+ const program = useProgram();
+ const { prepareTransactionAsync } = usePrepareProgramTransaction({
+ program,
+ serviceName: 'galacticExpress',
+ functionName: 'createNewSession',
+ });
+ const { signAndSend } = useSignAndSend();
+
+ const createNewSessionMessage = async ({ value, name }: Params, options: Options) => {
+ const { transaction } = await prepareTransactionAsync({
+ args: [name],
+ gasLimit: { increaseGas: 10 },
+ value,
+ });
+ signAndSend(transaction, options);
+ };
+
+ return { createNewSessionMessage };
+};
diff --git a/frontend/apps/galactic-express/src/app/utils/sails/messages/use-delete-player-message.ts b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-delete-player-message.ts
new file mode 100644
index 000000000..e63fd1e34
--- /dev/null
+++ b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-delete-player-message.ts
@@ -0,0 +1,28 @@
+import { usePrepareProgramTransaction } from '@gear-js/react-hooks';
+import { useProgram } from 'app/utils';
+import { Options, useSignAndSend } from 'app/hooks';
+import { HexString } from '@gear-js/api';
+
+type Params = {
+ playerId: HexString;
+};
+
+export const useDeletePlayerMessage = () => {
+ const program = useProgram();
+ const { prepareTransactionAsync } = usePrepareProgramTransaction({
+ program,
+ serviceName: 'galacticExpress',
+ functionName: 'deletePlayer',
+ });
+ const { signAndSend } = useSignAndSend();
+
+ const deletePlayerMessage = async ({ playerId }: Params, options?: Options) => {
+ const { transaction } = await prepareTransactionAsync({
+ args: [playerId],
+ gasLimit: { increaseGas: 10 },
+ });
+ signAndSend(transaction, options);
+ };
+
+ return { deletePlayerMessage };
+};
diff --git a/frontend/apps/galactic-express/src/app/utils/sails/messages/use-leave-game-message.ts b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-leave-game-message.ts
new file mode 100644
index 000000000..cfe672b74
--- /dev/null
+++ b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-leave-game-message.ts
@@ -0,0 +1,23 @@
+import { usePrepareProgramTransaction } from '@gear-js/react-hooks';
+import { useProgram } from 'app/utils';
+import { Options, useSignAndSend } from 'app/hooks';
+
+export const useLeaveGameMessage = () => {
+ const program = useProgram();
+ const { prepareTransactionAsync } = usePrepareProgramTransaction({
+ program,
+ serviceName: 'galacticExpress',
+ functionName: 'leaveGame',
+ });
+ const { signAndSend } = useSignAndSend();
+
+ const leaveGameMessage = async (options: Options) => {
+ const { transaction } = await prepareTransactionAsync({
+ args: [],
+ gasLimit: { increaseGas: 10 },
+ });
+ signAndSend(transaction, options);
+ };
+
+ return { leaveGameMessage };
+};
diff --git a/frontend/apps/galactic-express/src/app/utils/sails/messages/use-register-message.ts b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-register-message.ts
new file mode 100644
index 000000000..046b8bda8
--- /dev/null
+++ b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-register-message.ts
@@ -0,0 +1,36 @@
+import { usePrepareProgramTransaction } from '@gear-js/react-hooks';
+import { Participant, useProgram } from 'app/utils';
+import { Options, useSignAndSend } from 'app/hooks';
+import { HexString } from '@gear-js/api';
+
+type Params = {
+ creator: HexString;
+ participant: Participant;
+ value?: bigint;
+};
+
+export const useRegisterMessage = () => {
+ const program = useProgram();
+ const { prepareTransactionAsync } = usePrepareProgramTransaction({
+ program,
+ serviceName: 'galacticExpress',
+ functionName: 'register',
+ });
+ const { signAndSend } = useSignAndSend();
+
+ const registerMessage = async ({ value, creator, participant }: Params, options?: Options) => {
+ try {
+ const { transaction } = await prepareTransactionAsync({
+ args: [creator, participant],
+ gasLimit: { increaseGas: 10 },
+ value,
+ });
+ signAndSend(transaction, options);
+ } catch (error) {
+ console.error(error);
+ options?.onError?.(error as Error);
+ }
+ };
+
+ return { registerMessage };
+};
diff --git a/frontend/apps/galactic-express/src/app/utils/sails/messages/use-start-game-message.ts b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-start-game-message.ts
new file mode 100644
index 000000000..2fd211364
--- /dev/null
+++ b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-start-game-message.ts
@@ -0,0 +1,33 @@
+import { usePrepareProgramTransaction } from '@gear-js/react-hooks';
+import { useProgram } from 'app/utils';
+import { Options, useSignAndSend } from 'app/hooks';
+
+type Params = {
+ fuel: number;
+ payload: number;
+};
+
+export const useStartGameMessage = () => {
+ const program = useProgram();
+ const { prepareTransactionAsync } = usePrepareProgramTransaction({
+ program,
+ serviceName: 'galacticExpress',
+ functionName: 'startGame',
+ });
+ const { signAndSend } = useSignAndSend();
+
+ const startGameMessage = async ({ fuel, payload }: Params, options?: Options) => {
+ try {
+ const { transaction } = await prepareTransactionAsync({
+ args: [fuel, payload],
+ gasLimit: { increaseGas: 20 },
+ });
+ signAndSend(transaction, options);
+ } catch (error) {
+ console.error(error);
+ options?.onError?.(error as Error);
+ }
+ };
+
+ return { startGameMessage };
+};
diff --git a/frontend/apps/galactic-express/src/app/utils/sails/queries/index.ts b/frontend/apps/galactic-express/src/app/utils/sails/queries/index.ts
new file mode 100644
index 000000000..6a6dcd850
--- /dev/null
+++ b/frontend/apps/galactic-express/src/app/utils/sails/queries/index.ts
@@ -0,0 +1 @@
+export { useGetGameQuery } from './use-get-game-query';
diff --git a/frontend/apps/galactic-express/src/app/utils/sails/queries/use-get-game-query.ts b/frontend/apps/galactic-express/src/app/utils/sails/queries/use-get-game-query.ts
new file mode 100644
index 000000000..58ab816c7
--- /dev/null
+++ b/frontend/apps/galactic-express/src/app/utils/sails/queries/use-get-game-query.ts
@@ -0,0 +1,19 @@
+import { useProgram } from 'app/utils';
+import { useAccount, useProgramQuery } from '@gear-js/react-hooks';
+import { HexString } from '@gear-js/api';
+
+export const useGetGameQuery = (gameAddress?: HexString) => {
+ const program = useProgram();
+ const { account } = useAccount();
+
+ const { data, refetch, isFetching, error } = useProgramQuery({
+ program,
+ serviceName: 'galacticExpress',
+ functionName: 'getGame',
+ args: [gameAddress || '0x'],
+ query: { enabled: account && gameAddress ? undefined : false },
+ watch: true,
+ });
+
+ return { game: data, isFetching, refetch, error };
+};
diff --git a/frontend/apps/galactic-express/src/app/utils/sails/sails.tsx b/frontend/apps/galactic-express/src/app/utils/sails/sails.tsx
new file mode 100644
index 000000000..b3da2e51f
--- /dev/null
+++ b/frontend/apps/galactic-express/src/app/utils/sails/sails.tsx
@@ -0,0 +1,12 @@
+import { useProgram as useGearJsProgram } from '@gear-js/react-hooks';
+import { Program } from 'app/utils';
+import { useDnsProgramIds } from '@dapps-frontend/hooks';
+
+const useProgram = () => {
+ const { programId } = useDnsProgramIds();
+ const { data: program } = useGearJsProgram({ library: Program, id: programId });
+
+ return program;
+};
+
+export { useProgram };
diff --git a/frontend/apps/galactic-express/src/assets/meta/galactic_express_meta.txt b/frontend/apps/galactic-express/src/assets/meta/galactic_express_meta.txt
deleted file mode 100644
index 1f3960521..000000000
--- a/frontend/apps/galactic-express/src/assets/meta/galactic_express_meta.txt
+++ /dev/null
@@ -1 +0,0 @@
-0002000001000000000105000000010a000000000000000119000000011a00000035268c000418526573756c740804540104044501080108084f6b040004000000000c457272040008000001000004000004000008084c67616c61637469635f657870726573735f696f144572726f72000138485374617465556e696e6974616c69617a656400000024477374644572726f7204000c0118537472696e670001003053657373696f6e456e646564000200544675656c4f725061796c6f61644f7665726c6f61640003002c53657373696f6e46756c6c000400544e6f74456e6f7567685061727469636970616e74730005002454784d616e61676572040010015c5472616e73616374696f6e4d616e616765724572726f72000600284e6f5375636847616d650007002057726f6e67426964000800304e6f53756368506c6179657200090030556e72656769737465726564000a0044416c726561647952656769737465726564000b00505365766572616c526567697374726174696f6e73000c002c4e6f74466f7241646d696e000d00000c0000050200100c20676561725f6c69622874785f6d616e616765725c5472616e73616374696f6e4d616e616765724572726f7200010c4c5472616e73616374696f6e4e6f74466f756e64000000404d69736d617463686564547844617461000100204f766572666c6f770002000014084c67616c61637469635f657870726573735f696f18416374696f6e00011c404372656174654e657753657373696f6e0401106e616d650c0118537472696e6700000020526567697374657208011c63726561746f7218011c4163746f72496400012c7061727469636970616e7424012c5061727469636970616e740001004843616e63656c526567697374726174696f6e0002003044656c657465506c61796572040124706c617965725f696418011c4163746f7249640003002843616e63656c47616d65000400244c6561766547616d6500050024537461727447616d6508012c6675656c5f616d6f756e7420010875380001387061796c6f61645f616d6f756e742001087538000600001810106773746418636f6d6d6f6e287072696d6974697665731c4163746f724964000004001c01205b75383b2033325d00001c00000320000000200020000005030024084c67616c61637469635f657870726573735f696f2c5061727469636970616e740000100108696418011c4163746f7249640001106e616d650c0118537472696e6700012c6675656c5f616d6f756e7420010875380001387061796c6f61645f616d6f756e7420010875380000280418526573756c74080454012c044501080108084f6b04002c000000000c45727204000800000100002c084c67616c61637469635f657870726573735f696f144576656e740001203041646d696e4368616e676564080018011c4163746f724964000018011c4163746f724964000000284e657753657373696f6e100120616c74697475646530010c75313600011c7765617468657234011c576561746865720001187265776172643801107531323800010c626964380110753132380001002852656769737465726564080018011c4163746f724964000024012c5061727469636970616e740002004843616e63656c526567697374726174696f6e00030034506c6179657244656c65746564040124706c617965725f696418011c4163746f7249640004003047616d6543616e63656c65640005003047616d6546696e697368656404003c011c526573756c74730006002047616d654c6566740007000030000005040034084c67616c61637469635f657870726573735f696f1c5765617468657200011814436c65617200000018436c6f756479000100145261696e790002001853746f726d790003001c5468756e6465720004001c546f726e61646f000500003800000507003c084c67616c61637469635f657870726573735f696f1c526573756c747300000c01147475726e734001645665633c5665633c284163746f7249642c205475726e293e3e00012072616e6b696e67735401505665633c284163746f7249642c2075313238293e0001307061727469636970616e74735c016c5665633c284163746f7249642c205061727469636970616e74293e00004000000244004400000248004800000408184c004c084c67616c61637469635f657870726573735f696f105475726e00010814416c6976650801246675656c5f6c65667420010875380001387061796c6f61645f616d6f756e7420010875380000002444657374726f796564040050012848616c74526561736f6e0001000050084c67616c61637469635f657870726573735f696f2848616c74526561736f6e0001183c5061796c6f61644f7665726c6f6164000000304675656c4f7665726c6f61640001004453657061726174696f6e4661696c7572650002004441737465726f6964436f6c6c6973696f6e000300304675656c53686f727461676500040034456e67696e654661696c7572650005000054000002580058000004081838005c0000026000600000040818240064084c67616c61637469635f657870726573735f696f28537461746551756572790001080c416c6c0000001c47657447616d65040124706c617965725f696418011c4163746f7249640001000068084c67616c61637469635f657870726573735f696f2853746174655265706c790001080c416c6c04006c011453746174650000001047616d6504008801444f7074696f6e3c47616d6553746174653e000100006c084c67616c61637469635f657870726573735f696f145374617465000008011467616d65737001645665633c284163746f7249642c2047616d655374617465293e000144706c617965725f746f5f67616d655f696480015c5665633c284163746f7249642c204163746f724964293e0000700000027400740000040818780078084c67616c61637469635f657870726573735f696f2447616d65537461746500001c011461646d696e18011c4163746f72496400012861646d696e5f6e616d650c0118537472696e67000120616c74697475646530010c75313600011c7765617468657234011c576561746865720001187265776172643801107531323800011473746167657c01285374616765537461746500010c6269643801107531323800007c084c67616c61637469635f657870726573735f696f285374616765537461746500010830526567697374726174696f6e04005c016c5665633c284163746f7249642c205061727469636970616e74293e0000001c526573756c747304003c011c526573756c74730001000080000002840084000004081818008804184f7074696f6e04045401780108104e6f6e6500000010536f6d650400780000010000
\ No newline at end of file
diff --git a/frontend/apps/galactic-express/src/atoms.ts b/frontend/apps/galactic-express/src/atoms.ts
index a7b5877ab..e3247e81b 100644
--- a/frontend/apps/galactic-express/src/atoms.ts
+++ b/frontend/apps/galactic-express/src/atoms.ts
@@ -1,7 +1,8 @@
import { atom } from 'jotai';
import { RegistrationStatus } from 'features/session/types';
+import { HexString } from '@gear-js/api';
-export const CURRENT_GAME_ATOM = atom('');
+export const CURRENT_GAME_ATOM = atom(null);
export const PLAYER_NAME_ATOM = atom(null);
diff --git a/frontend/apps/galactic-express/src/components/layout/header/Header.tsx b/frontend/apps/galactic-express/src/components/layout/header/Header.tsx
index abcdba82e..77b2929cf 100644
--- a/frontend/apps/galactic-express/src/components/layout/header/Header.tsx
+++ b/frontend/apps/galactic-express/src/components/layout/header/Header.tsx
@@ -14,8 +14,8 @@ function Header() {
const { admin, stage } = state || {};
const isUserAdmin = admin === account?.decodedAddress;
- const isRegistration = Object.keys(stage || {})[0] === 'Registration';
- const participants = stage?.Registration || stage?.Results?.participants;
+ const isRegistration = stage && 'registration' in stage;
+ const participants = isRegistration ? stage.registration : [];
return (
}
className={{ header: styles.header, content: styles.container }}>
- {isUserAdmin && isRegistration && }
+ {isUserAdmin && isRegistration && }
);
}
diff --git a/frontend/apps/galactic-express/src/features/session/api.ts b/frontend/apps/galactic-express/src/features/session/api.ts
deleted file mode 100644
index 0f58154f3..000000000
--- a/frontend/apps/galactic-express/src/features/session/api.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import metaTxt from 'assets/meta/galactic_express_meta.txt';
-import { useProgramMetadata } from 'hooks';
-
-function useEscrowMetadata() {
- return useProgramMetadata(metaTxt);
-}
-
-export { useEscrowMetadata };
diff --git a/frontend/apps/galactic-express/src/features/session/components/cancel-game-button/CancelGameButton.tsx b/frontend/apps/galactic-express/src/features/session/components/cancel-game-button/CancelGameButton.tsx
index 998137c82..68e7b4128 100644
--- a/frontend/apps/galactic-express/src/features/session/components/cancel-game-button/CancelGameButton.tsx
+++ b/frontend/apps/galactic-express/src/features/session/components/cancel-game-button/CancelGameButton.tsx
@@ -1,11 +1,11 @@
+import clsx from 'clsx';
import { ReactComponent as CrossIconSVG } from 'assets/images/icons/cross-icon.svg';
import { useAtom, useSetAtom } from 'jotai';
import { Button } from '@gear-js/vara-ui';
import { useAccount } from '@gear-js/react-hooks';
-import { useLaunchMessage } from 'features/session/hooks';
import { Participant } from 'features/session/types';
import { IS_LOADING, REGISTRATION_STATUS } from 'atoms';
-import clsx from 'clsx';
+import { useCancelGameMessage, useCancelRegisterMessage } from 'app/utils';
import styles from './CancelGameButton.module.scss';
type Props = {
@@ -14,11 +14,13 @@ type Props = {
};
function CancelGameButton({ isAdmin, participants }: Props) {
- const { meta: isMeta, message: sendMessage } = useLaunchMessage();
const setRegistrationStatus = useSetAtom(REGISTRATION_STATUS);
const [isLoading, setIsLoading] = useAtom(IS_LOADING);
const { account } = useAccount();
+ const { cancelGameMessage } = useCancelGameMessage();
+ const { cancelRegisterMessage } = useCancelRegisterMessage();
+
const isRegistered = account?.decodedAddress
? participants.map((participant) => participant[0]).includes(account.decodedAddress)
: false;
@@ -27,7 +29,7 @@ function CancelGameButton({ isAdmin, participants }: Props) {
setIsLoading(false);
};
- const onInBlock = () => {
+ const onSuccess = () => {
setIsLoading(false);
setRegistrationStatus('registration');
};
@@ -35,23 +37,10 @@ function CancelGameButton({ isAdmin, participants }: Props) {
const handleClick = () => {
setIsLoading(true);
if (isAdmin) {
- sendMessage({
- payload: {
- CancelGame: null,
- },
- onError,
- onInBlock,
- });
+ cancelGameMessage({ onError, onSuccess });
}
-
if (!isAdmin && isRegistered) {
- sendMessage({
- payload: {
- CancelRegistration: null,
- },
- onError,
- onInBlock,
- });
+ cancelRegisterMessage({ onError, onSuccess });
}
};
diff --git a/frontend/apps/galactic-express/src/features/session/components/form/Form.tsx b/frontend/apps/galactic-express/src/features/session/components/form/Form.tsx
index 4902533e9..59a2e92f7 100644
--- a/frontend/apps/galactic-express/src/features/session/components/form/Form.tsx
+++ b/frontend/apps/galactic-express/src/features/session/components/form/Form.tsx
@@ -1,17 +1,18 @@
import { useAtomValue, useSetAtom, useAtom } from 'jotai';
import { CURRENT_GAME_ATOM, IS_LOADING, PLAYER_NAME_ATOM } from 'atoms';
-import { useAccount, withoutCommas } from '@gear-js/react-hooks';
+import { useAccount } from '@gear-js/react-hooks';
import { Button } from '@gear-js/ui';
import { useForm } from '@mantine/form';
import { Card } from 'components';
-import { ChangeEvent, Dispatch, SetStateAction, useState } from 'react';
+import { ChangeEvent, Dispatch, SetStateAction } from 'react';
import { RegistrationStatus } from 'features/session/types';
import { ReactComponent as RocketSVG } from '../../assets/rocket.svg';
import { INITIAL_VALUES, VALIDATE, WEATHERS } from '../../consts';
-import { useLaunchMessage } from '../../hooks';
import { Range } from '../range';
import { Probability } from '../probability';
import styles from './Form.module.scss';
+import { useStartGameMessage, useRegisterMessage } from 'app/utils';
+import { getPanicType } from 'utils';
type Props = {
weather: string;
@@ -30,10 +31,11 @@ function Form({ weather, bid, isAdmin, setRegistrationStatus }: Props) {
});
const playerName = useAtomValue(PLAYER_NAME_ATOM);
const currentGameAddress = useAtomValue(CURRENT_GAME_ATOM);
+ const { startGameMessage } = useStartGameMessage();
+ const { registerMessage } = useRegisterMessage();
- const { fuel, payload } = values;
-
- const { meta, message: sendMessage } = useLaunchMessage();
+ const fuel = Number(values.fuel);
+ const payload = Number(values.payload);
const handleNumberInputChange = ({ target }: ChangeEvent) => {
const value = +target.value;
@@ -52,29 +54,45 @@ function Form({ weather, bid, isAdmin, setRegistrationStatus }: Props) {
});
const handleSubmit = () => {
- if (!isAdmin && meta && account?.decodedAddress) {
+ if (!isAdmin && account?.decodedAddress && currentGameAddress && playerName) {
setIsLoading(true);
- sendMessage({
- payload: {
- Register: {
- creator: currentGameAddress,
- participant: { fuel_amount: fuel, payload_amount: payload, name: playerName, id: account.decodedAddress },
- },
- },
- value: Number(withoutCommas(bid || '')),
- onSuccess: () => {
- setRegistrationStatus('success');
- setCurrentGame('');
- setIsLoading(false);
+
+ registerMessage(
+ {
+ creator: currentGameAddress,
+ participant: { fuel_amount: fuel, payload_amount: payload, name: playerName, id: account.decodedAddress },
+ value: bid ? BigInt(bid) : undefined,
},
- onError: () => {
- setIsLoading(false);
+ {
+ onSuccess: () => {
+ setRegistrationStatus('success');
+ setCurrentGame(null);
+ setIsLoading(false);
+ },
+ onError: (error) => {
+ setIsLoading(false);
+
+ const panicType = getPanicType(error);
+ if (panicType === 'SessionFull') {
+ setRegistrationStatus('MaximumPlayersReached');
+ }
+ },
},
- });
+ );
}
- if (isAdmin && meta) {
- sendMessage({ payload: { StartGame: { fuel_amount: fuel, payload_amount: payload } } });
+ if (isAdmin) {
+ startGameMessage(
+ { fuel: fuel, payload: payload },
+ {
+ onError: (error) => {
+ const panicType = getPanicType(error);
+ if (panicType === 'NotEnoughParticipants') {
+ setRegistrationStatus(panicType);
+ }
+ },
+ },
+ );
}
};
diff --git a/frontend/apps/galactic-express/src/features/session/components/game-found-modal/GameFoundModal.tsx b/frontend/apps/galactic-express/src/features/session/components/game-found-modal/GameFoundModal.tsx
index 77e10617a..46bdb8c60 100644
--- a/frontend/apps/galactic-express/src/features/session/components/game-found-modal/GameFoundModal.tsx
+++ b/frontend/apps/galactic-express/src/features/session/components/game-found-modal/GameFoundModal.tsx
@@ -77,7 +77,7 @@ function GameFoundModal({ entryFee, players, gasAmount, onSubmit, onClose }: Pro
{items.map((item) => (
-
+
{item.name}
{item.value}
diff --git a/frontend/apps/galactic-express/src/features/session/components/participants-table/ParticipantsTable.tsx b/frontend/apps/galactic-express/src/features/session/components/participants-table/ParticipantsTable.tsx
index 9452bf493..921576e40 100644
--- a/frontend/apps/galactic-express/src/features/session/components/participants-table/ParticipantsTable.tsx
+++ b/frontend/apps/galactic-express/src/features/session/components/participants-table/ParticipantsTable.tsx
@@ -1,8 +1,9 @@
import { Fragment } from 'react';
import { cx } from 'utils';
import { shortenString } from 'features/session/utils';
+import { decodeAddress } from '@gear-js/api';
import { Button } from '@gear-js/vara-ui';
-import { useLaunchMessage } from 'features/session/hooks';
+import { useDeletePlayerMessage } from 'app/utils';
import styles from './ParticipantsTable.module.scss';
interface TableData {
@@ -18,8 +19,7 @@ type Props = {
};
function ParticipantsTable({ data, userAddress, isUserAdmin }: Props) {
- const { meta: isMeta, message: sendMessage } = useLaunchMessage();
-
+ const { deletePlayerMessage } = useDeletePlayerMessage();
const isYourAddress = (address: string) => address === userAddress;
const modifiedData: TableData[] = [
@@ -28,13 +28,7 @@ function ParticipantsTable({ data, userAddress, isUserAdmin }: Props) {
];
const handleDeletePlayer = (playerId: string) => {
- sendMessage({
- payload: {
- DeletePlayer: {
- playerId,
- },
- },
- });
+ deletePlayerMessage({ playerId: decodeAddress(playerId) });
};
return (
diff --git a/frontend/apps/galactic-express/src/features/session/components/session-passed-info/SessionPassedInfo.tsx b/frontend/apps/galactic-express/src/features/session/components/session-passed-info/SessionPassedInfo.tsx
index efc009c5b..18b028149 100644
--- a/frontend/apps/galactic-express/src/features/session/components/session-passed-info/SessionPassedInfo.tsx
+++ b/frontend/apps/galactic-express/src/features/session/components/session-passed-info/SessionPassedInfo.tsx
@@ -7,7 +7,7 @@ function SessionPassedInfo() {
const setCurrentGame = useSetAtom(CURRENT_GAME_ATOM);
const handleClick = () => {
- setCurrentGame('');
+ setCurrentGame(null);
};
return (
diff --git a/frontend/apps/galactic-express/src/features/session/components/session/Session.module.scss b/frontend/apps/galactic-express/src/features/session/components/session/Session.module.scss
index c2e40b286..5cd21ff44 100644
--- a/frontend/apps/galactic-express/src/features/session/components/session/Session.module.scss
+++ b/frontend/apps/galactic-express/src/features/session/components/session/Session.module.scss
@@ -13,6 +13,7 @@
align-items: center;
white-space: nowrap;
border-bottom: 1px solid rgba(#8c8b90, 0.2);
+ max-width: 562px;
.heading {
font-weight: 300;
@@ -104,9 +105,9 @@
}
.courtainGreen {
- background: radial-gradient(circle, rgba(2,0,36,0) 0%, rgba(111,207,151,0.3) 0%, rgba(0,0,0,0) 75%);
+ background: radial-gradient(circle, rgba(2, 0, 36, 0) 0%, rgba(111, 207, 151, 0.3) 0%, rgba(0, 0, 0, 0) 75%);
}
.courtainRed {
- background: radial-gradient(circle, rgba(2,0,36,0) 0%, rgba(235,87,87,0.3) 0%, rgba(0,0,0,0) 75%);
-}
\ No newline at end of file
+ background: radial-gradient(circle, rgba(2, 0, 36, 0) 0%, rgba(235, 87, 87, 0.3) 0%, rgba(0, 0, 0, 0) 75%);
+}
diff --git a/frontend/apps/galactic-express/src/features/session/components/session/Session.tsx b/frontend/apps/galactic-express/src/features/session/components/session/Session.tsx
index 9dfffab50..ce62d97f6 100644
--- a/frontend/apps/galactic-express/src/features/session/components/session/Session.tsx
+++ b/frontend/apps/galactic-express/src/features/session/components/session/Session.tsx
@@ -23,7 +23,7 @@ type Props = {
};
function Session({ session, turns, rankings, userId, participants, admin }: Props) {
- const { altitude, weather, reward, sessionId: id } = session;
+ const { altitude, weather, reward } = session;
const roundsCount = turns.length;
const [roundIndex, setRoundIndex] = useState(0);
@@ -36,29 +36,25 @@ function Session({ session, turns, rankings, userId, participants, admin }: Prop
const firstPage = () => setRoundIndex(0);
const lastPage = () => setRoundIndex(roundsCount - 1);
- const defineFuelLeftFormat = (isAlive: boolean, fuelLeft: string) => {
- if (isAlive && fuelLeft) {
- return fuelLeft !== '0' ? fuelLeft : '1';
- }
-
- return ' - ';
+ const defineFuelLeftFormat = (isAlive: boolean, fuelLeft: number) => {
+ return isAlive && fuelLeft ? String(fuelLeft) : ' - ';
};
const getEvents = (): Event[] =>
turns[roundIndex]
.slice()
- .sort((a: TurnParticipant, b: TurnParticipant) => {
+ .sort((a, b) => {
const indexA = participants.findIndex((p) => p[0] === a[0]);
const indexB = participants.findIndex((p) => p[0] === b[0]);
return indexA - indexB;
})
?.map((participantInfo) => {
- const isAlive = Object.keys(participantInfo[1])[0] === 'Alive';
+ const isAlive = 'alive' in participantInfo[1];
const firstDeadRound = turns.findIndex((turn) => {
const part = turn.find((participant) => participant[0] === participantInfo[0]) || [];
- return Object.keys(part[1] || {})[0] !== 'Alive';
+ return Object.keys(part[1] || {})[0] !== 'alive';
});
return {
@@ -66,8 +62,11 @@ function Session({ session, turns, rankings, userId, participants, admin }: Prop
name: participants.find((part) => part[0] === participantInfo[0])?.[1].name,
deadRound: !isAlive,
firstDeadRound,
- fuelLeft: defineFuelLeftFormat(isAlive, participantInfo[1]?.Alive?.fuelLeft),
- payload: isAlive ? participantInfo[1].Alive.payloadAmount : ' - ',
+ fuelLeft: defineFuelLeftFormat(
+ isAlive,
+ 'alive' in participantInfo[1] ? participantInfo[1]?.alive?.fuel_left : 0,
+ ),
+ payload: 'alive' in participantInfo[1] ? String(participantInfo[1].alive.payload_amount) : ' - ',
lastAltitude: String(
Math.round(
Number(withoutCommas(altitude)) /
@@ -76,11 +75,12 @@ function Session({ session, turns, rankings, userId, participants, admin }: Prop
: roundsCount - roundNumber + 1),
),
),
+ haltReason: 'alive' in participantInfo[1] ? null : participantInfo[1].destroyed,
};
});
const getFeedItems = () =>
- getEvents()?.map(({ participant, payload, lastAltitude, fuelLeft, deadRound }, index) => (
+ getEvents()?.map(({ participant, payload, lastAltitude, fuelLeft, deadRound, haltReason }, index) => (
{getVaraAddress(participant)}
@@ -93,16 +93,18 @@ function Session({ session, turns, rankings, userId, participants, admin }: Prop
{lastAltitude},
Payload:
{payload},
+
Halt:
+
{haltReason || 'null'},
));
const sortRanks = () => {
- const isAllZeros = rankings.every((rank) => rank[1] === '0');
+ const isAllZeros = rankings.every((rank) => rank[1] === 0);
const sortedRanks = isAllZeros
? []
- : rankings.sort((rankA, rankB) => (Number(withoutCommas(rankA[1])) < Number(withoutCommas(rankB[1])) ? 1 : -1));
+ : rankings.sort((rankA, rankB) => (Number(rankA[1]) < Number(rankB[1]) ? 1 : -1));
return sortedRanks;
};
@@ -117,7 +119,7 @@ function Session({ session, turns, rankings, userId, participants, admin }: Prop
return {
isUserWinner: winners.map((item) => item[0]).includes(userId || '0x'),
- userRank: sortedRanks.find((item) => item[0] === userId)?.[1] || '',
+ userRank: sortedRanks.find((item) => item[0] === userId)?.[1] || '0',
winners,
};
};
@@ -126,6 +128,15 @@ function Session({ session, turns, rankings, userId, participants, admin }: Prop
return (
+
item[0]).includes(userId || '0x')
+ ? styles.courtainGreen
+ : styles.courtainRed,
+ )}
+ />
+
);
diff --git a/frontend/apps/galactic-express/src/features/session/components/table/Table.module.scss b/frontend/apps/galactic-express/src/features/session/components/table/Table.module.scss
index ea285a293..530187e4d 100644
--- a/frontend/apps/galactic-express/src/features/session/components/table/Table.module.scss
+++ b/frontend/apps/galactic-express/src/features/session/components/table/Table.module.scss
@@ -6,7 +6,7 @@ $borderRadius: 8px;
.table {
display: grid;
- grid-template-columns: 1fr repeat(5, max-content);
+ grid-template-columns: 1fr repeat(6, max-content);
text-align: center;
div {
@@ -25,34 +25,34 @@ $borderRadius: 8px;
background-color: rgba(255, 255, 255, 0.04);
// first row
- &:nth-child(n + 6):nth-child(-n + 12) {
+ &:nth-child(n + 7):nth-child(-n + 14) {
border-top: $tableBorder;
}
// last row
- &:nth-last-child(-n + 6) {
+ &:nth-last-child(-n + 7) {
border-bottom: $tableBorder;
}
// not last column
- &:not(:nth-child(6n)) {
+ &:not(:nth-child(7n)) {
border-right: $cellBorder;
}
// not last row
- &:not(:nth-last-child(-n + 6)) {
+ &:not(:nth-last-child(-n + 7)) {
border-bottom: $cellBorder;
}
- &:nth-child(7) {
+ &:nth-child(8) {
border-top-left-radius: $borderRadius;
}
- &:nth-child(12) {
+ &:nth-child(14) {
border-top-right-radius: $borderRadius;
}
- &:nth-last-child(6) {
+ &:nth-last-child(7) {
border-bottom-left-radius: $borderRadius;
}
diff --git a/frontend/apps/galactic-express/src/features/session/components/table/Table.tsx b/frontend/apps/galactic-express/src/features/session/components/table/Table.tsx
index 550b01080..378224596 100644
--- a/frontend/apps/galactic-express/src/features/session/components/table/Table.tsx
+++ b/frontend/apps/galactic-express/src/features/session/components/table/Table.tsx
@@ -22,7 +22,7 @@ function Table({ data, userId }: Props) {
));
const getBody = () =>
- data?.map(({ participant, name, deadRound, fuelLeft, lastAltitude, payload }, index) => (
+ data?.map(({ participant, name, deadRound, fuelLeft, lastAltitude, payload, haltReason }, index) => (
{deadRound ? : }
{fuelLeft}
{lastAltitude}
- {payload}
+ {payload}
+ {haltReason || ' - '}
));
diff --git a/frontend/apps/galactic-express/src/features/session/components/win-status/WinStatus.tsx b/frontend/apps/galactic-express/src/features/session/components/win-status/WinStatus.tsx
index 0575a0d50..c5a1b28b0 100644
--- a/frontend/apps/galactic-express/src/features/session/components/win-status/WinStatus.tsx
+++ b/frontend/apps/galactic-express/src/features/session/components/win-status/WinStatus.tsx
@@ -3,9 +3,9 @@ import { cx } from 'utils';
import { REGISTRATION_STATUS } from 'atoms';
import { getVaraAddress, useAccount } from '@gear-js/react-hooks';
import { Button } from '@gear-js/ui';
-import { useLaunchMessage } from 'features/session/hooks';
import { shortenString } from 'features/session/utils';
import { RankWithName } from 'features/session/types';
+import { useCancelGameMessage, useLeaveGameMessage } from 'app/utils';
import styles from './WinStatus.module.scss';
type Props = {
@@ -16,25 +16,22 @@ type Props = {
};
function WinStatus({ type, userRank, winners, admin }: Props) {
- const { meta, message: sendNewSessionMessage } = useLaunchMessage();
const setRegistrationStatus = useSetAtom(REGISTRATION_STATUS);
+ const { cancelGameMessage } = useCancelGameMessage();
+ const { leaveGameMessage } = useLeaveGameMessage();
const { account } = useAccount();
const isAdmin = admin === account?.decodedAddress;
- const onInBlock = () => {
+ const onSuccess = () => {
setRegistrationStatus('registration');
};
const handleCreateNewSession = () => {
- if (!meta) {
- return;
- }
-
if (isAdmin) {
- sendNewSessionMessage({ payload: { CancelGame: null }, onInBlock });
+ cancelGameMessage({ onSuccess });
} else {
- sendNewSessionMessage({ payload: { LeaveGame: null }, onInBlock });
+ leaveGameMessage({ onSuccess });
}
};
@@ -51,7 +48,7 @@ function WinStatus({ type, userRank, winners, admin }: Props) {
Winners:{' '}
{winners.map((item) => (
-
+
{item[2] || shortenString(getVaraAddress(item[0]), 6)}
))}
diff --git a/frontend/apps/galactic-express/src/features/session/consts.ts b/frontend/apps/galactic-express/src/features/session/consts.ts
index aa187af00..b53718527 100644
--- a/frontend/apps/galactic-express/src/features/session/consts.ts
+++ b/frontend/apps/galactic-express/src/features/session/consts.ts
@@ -48,7 +48,7 @@ const VALIDATE = {
fuel: isGreaterThanZero,
};
-const TABLE_HEADINGS = ['Player', 'Name', 'Alive', 'Fuel Left', 'Altitude', 'Payload'];
+const TABLE_HEADINGS = ['Player', 'Name', 'Alive', 'Fuel Left', 'Altitude', 'Payload', 'Halt'];
const PLAYER_COLORS = ['#eb5757', '#f2c94c', '#2f80ed', '#9b51e0'];
diff --git a/frontend/apps/galactic-express/src/features/session/hooks.ts b/frontend/apps/galactic-express/src/features/session/hooks.ts
index 86dd7bec2..a5441064e 100644
--- a/frontend/apps/galactic-express/src/features/session/hooks.ts
+++ b/frontend/apps/galactic-express/src/features/session/hooks.ts
@@ -1,35 +1,14 @@
-import { useMemo } from 'react';
-import { useAccount, useReadFullState, useSendMessageWithGas } from '@gear-js/react-hooks';
-import { HexString } from '@gear-js/api';
-import metaTxt from 'assets/meta/galactic_express_meta.txt';
-import { useProgramMetadata } from 'hooks';
-import { ADDRESS } from 'consts';
+import { useAccount } from '@gear-js/react-hooks';
import { useAtomValue } from 'jotai';
import { CURRENT_GAME_ATOM } from 'atoms';
-import { useDnsProgramIds } from '@dapps-frontend/hooks';
-import { LaunchState } from './types';
+import { useGetGameQuery } from 'app/utils';
function useLaunchState() {
const { account } = useAccount();
- const { programId } = useDnsProgramIds();
const currentGame = useAtomValue(CURRENT_GAME_ATOM);
- const meta = useProgramMetadata(metaTxt);
- const payload = useMemo(
- () => ({ GetGame: { playerId: currentGame || account?.decodedAddress } }),
- [currentGame, account?.decodedAddress],
- );
+ const { game } = useGetGameQuery(currentGame || account?.decodedAddress);
- const { state } = useReadFullState(programId, meta, payload);
-
- return state?.Game;
+ return game;
}
-
-function useLaunchMessage() {
- const { programId } = useDnsProgramIds();
- const meta = useProgramMetadata(metaTxt);
-
- return { meta: !!meta, message: useSendMessageWithGas(programId, meta, { isMaxGasLimit: true }) };
-}
-
-export { useLaunchState, useLaunchMessage };
+export { useLaunchState };
diff --git a/frontend/apps/galactic-express/src/features/session/types.ts b/frontend/apps/galactic-express/src/features/session/types.ts
index b5117b38b..a98fa8b9e 100644
--- a/frontend/apps/galactic-express/src/features/session/types.ts
+++ b/frontend/apps/galactic-express/src/features/session/types.ts
@@ -1,25 +1,13 @@
import { HexString } from '@polkadot/util/types';
-
-type Strategy = {
- name: string;
- fuelAmount: string;
- payloadAmount: string;
-};
+import { HaltReason, Participant as ProgramParticipant, Turn } from 'app/utils';
type Session = {
altitude: string;
weather: string;
reward: string;
- sessionId: string;
};
-type Participant = [HexString, Strategy];
-
-type Results = {
- turns: Turns;
- rankings: Rank[];
- participants: Participant[];
-};
+type Participant = [HexString, ProgramParticipant];
type Event = {
participant: HexString;
@@ -29,50 +17,16 @@ type Event = {
fuelLeft: string;
lastAltitude: string;
payload: string;
+ haltReason: HaltReason | null;
};
-type Rank = [HexString, string];
+type Rank = [HexString, number | string | bigint];
type RankWithName = [`0x${string}`, string, string];
-type State = {
- admin: HexString;
- stage: {
- Registration: Participant[];
- Results: Results;
- };
- master: string;
- altitude: string;
- weather: string;
- reward: string;
- sessionId: string;
- bid: string;
- adminName: string;
-};
-
-type LaunchState = {
- Game: State;
-};
-
-type TurnParticipant = [
- HexString,
- {
- Alive: {
- fuelLeft: string;
- payloadAmount: string;
- };
- },
-];
-
-type Turn = TurnParticipant[];
+type TurnParticipant = [HexString, Turn];
-type Turns = Turn[];
-
-type PlayerStatus = 'Finished' | 'Registered' | null;
-
-type PlayerInfo = {
- PlayerInfo: PlayerStatus;
-};
+type Turns = TurnParticipant[][];
type RegistrationStatus =
| 'registration'
@@ -83,17 +37,4 @@ type RegistrationStatus =
| 'PlayerRemoved'
| 'GameCanceled';
-export type {
- LaunchState,
- State,
- Event,
- Participant,
- Turns,
- Rank,
- TurnParticipant,
- Session,
- PlayerStatus,
- PlayerInfo,
- RankWithName,
- RegistrationStatus,
-};
+export type { Event, Participant, Turns, Rank, TurnParticipant, Session, RankWithName, RegistrationStatus };
diff --git a/frontend/apps/galactic-express/src/features/welcome/components/enter-contract-address/RequestGame.tsx b/frontend/apps/galactic-express/src/features/welcome/components/enter-contract-address/RequestGame.tsx
index 6ecafdc96..e237dab04 100644
--- a/frontend/apps/galactic-express/src/features/welcome/components/enter-contract-address/RequestGame.tsx
+++ b/frontend/apps/galactic-express/src/features/welcome/components/enter-contract-address/RequestGame.tsx
@@ -1,30 +1,22 @@
import { useEffect, useState } from 'react';
import { Wallet } from '@dapps-frontend/ui';
import { Button } from '@gear-js/vara-ui';
-import { useDnsProgramIds } from '@dapps-frontend/hooks';
import { cx } from 'utils';
import { ReactComponent as VaraSVG } from 'assets/images/icons/vara-coin.svg';
import { ReactComponent as TVaraSVG } from 'assets/images/icons/tvara-coin.svg';
import { useSetAtom, useAtom } from 'jotai';
import { CURRENT_GAME_ATOM, IS_LOADING, PLAYER_NAME_ATOM, REGISTRATION_STATUS } from 'atoms';
-import { useLaunchMessage } from 'features/session/hooks';
-import metaTxt from 'assets/meta/galactic_express_meta.txt';
-import { useAccount, useApi, useBalanceFormat, withoutCommas } from '@gear-js/react-hooks';
+import { useAccount, useApi, useBalanceFormat } from '@gear-js/react-hooks';
import { TextField } from 'components/layout/TextField';
import { isNotEmpty, useForm } from '@mantine/form';
import { HexString, decodeAddress } from '@gear-js/api';
import { GameFoundModal } from 'features/session/components/game-found-modal';
-import { useProgramMetadata } from 'hooks';
-import { LaunchState } from 'features/session/types';
import { JoinModalFormValues } from 'features/session/components/game-found-modal/GameFoundModal';
import { TextModal } from 'features/session/components/game-not-found-modal';
import { GameIntro } from '../game-intro';
+import { GameState, useGetGameQuery, useCreateNewSessionMessage } from 'app/utils';
import styles from './RequestGame.module.scss';
-export interface ContractFormValues {
- [key: string]: string;
-}
-
type Status = 'creating' | 'joining' | null;
type CreateFormValues = {
@@ -39,12 +31,11 @@ type JoinFormValues = {
function RequestGame() {
const { account } = useAccount();
const { api } = useApi();
- const { programId } = useDnsProgramIds();
const { getFormattedBalanceValue, getChainBalanceValue } = useBalanceFormat();
- const [foundState, setFoundState] = useState(null);
- const { message: sendNewSessionMessage } = useLaunchMessage();
- const meta = useProgramMetadata(metaTxt);
+ const { createNewSessionMessage } = useCreateNewSessionMessage();
+
+ const [foundState, setFoundState] = useState(null);
const setCurrentGame = useSetAtom(CURRENT_GAME_ATOM);
const setPlayerName = useSetAtom(PLAYER_NAME_ATOM);
const setRegistrationStatus = useSetAtom(REGISTRATION_STATUS);
@@ -67,7 +58,7 @@ function RequestGame() {
},
});
- const joinForm = useForm({
+ const joinForm = useForm({
initialValues: {
address: undefined,
},
@@ -78,7 +69,9 @@ function RequestGame() {
const { errors: createErrors, getInputProps: getCreateInputProps, onSubmit: onCreateSubmit } = createForm;
- const { errors: joinErrors, getInputProps: getJoinInputProps, onSubmit: onJoinSubmit } = joinForm;
+ const { errors: joinErrors, getInputProps: getJoinInputProps, onSubmit: onJoinSubmit, values } = joinForm;
+
+ const { refetch } = useGetGameQuery(values.address?.length === 49 ? decodeAddress(values.address) : undefined);
const handleSetStatus = (newStatus: Status) => {
setStatus(newStatus);
@@ -92,25 +85,11 @@ function RequestGame() {
if (!account?.decodedAddress) {
return;
}
-
- const payload = {
- CreateNewSession: {
- name: values.name,
- },
- };
-
setIsLoading(true);
- sendNewSessionMessage({
- payload,
- value: getChainBalanceValue(values.fee).toFixed(),
- onSuccess: () => {
- setIsLoading(false);
- },
- onError: () => {
- console.log('error');
- setIsLoading(false);
- },
- });
+ createNewSessionMessage(
+ { name: values.name, value: BigInt(getChainBalanceValue(values.fee).toFixed()) },
+ { onSuccess: () => setIsLoading(false), onError: () => setIsLoading(false) },
+ );
};
const handleOpenJoinSessionModal = async (values: JoinFormValues) => {
@@ -118,21 +97,11 @@ function RequestGame() {
return;
}
- const payload = { GetGame: { playerId: decodeAddress(values.address || '') } };
-
try {
- const res = await api?.programState.read(
- {
- programId,
- payload,
- },
- meta,
- );
-
- const state = (await res?.toHuman()) as LaunchState;
+ const { data } = await refetch();
- if (state?.Game) {
- setFoundState(state);
+ if (data) {
+ setFoundState(data);
setFoundGame(decodeAddress(values.address || ''));
setIsJoinSessionModalShown(true);
return;
@@ -252,8 +221,10 @@ function RequestGame() {
)}
{isJoinSessionModalShown && (
();
-
- useEffect(() => {
- fetch(source)
- .then((response) => response.text())
- .then((raw) => ProgramMetadata.from(`0x${raw}`))
- .then((result) => setMetadata(result))
- .catch(({ message }: Error) => alert.error(message));
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- return metadata;
-}
-
-function useStateMetadata(source: string) {
- const alert = useAlert();
-
- const [stateMetadata, setStateMetadata] = useState();
-
- useEffect(() => {
- fetch(source)
- .then((response) => response.arrayBuffer())
- .then((arrayBuffer) => Buffer.from(arrayBuffer))
- .then((buffer) => getStateMetadata(buffer))
- .then((result) => setStateMetadata(result))
- .catch(({ message }: Error) => alert.error(message));
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- return stateMetadata;
-}
-
-function useReadState({ programId, meta, payload }: { programId?: HexString; meta: string; payload?: AnyJson }) {
- const metadata = useProgramMetadata(meta);
- return useReadFullState(programId, metadata, payload);
-}
-
-export { useProgramMetadata, useStateMetadata, useReadState };
diff --git a/frontend/apps/galactic-express/src/hooks/index.ts b/frontend/apps/galactic-express/src/hooks/index.ts
deleted file mode 100644
index 2785a21b4..000000000
--- a/frontend/apps/galactic-express/src/hooks/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { useProgramMetadata, useStateMetadata, useReadState } from './api';
-
-export { useProgramMetadata, useStateMetadata, useReadState };
diff --git a/frontend/apps/galactic-express/src/pages/home/Home.tsx b/frontend/apps/galactic-express/src/pages/home/Home.tsx
index 5734a1281..21887d486 100644
--- a/frontend/apps/galactic-express/src/pages/home/Home.tsx
+++ b/frontend/apps/galactic-express/src/pages/home/Home.tsx
@@ -14,13 +14,16 @@ function Home() {
const [isGameCancelledModalOpen, setIsGameCancelledModalOpen] = useState(false);
const { account } = useAccount();
const state = useLaunchState();
- const { admin, stage, sessionId, altitude, weather, reward, bid, adminName } = state || {};
+ const { admin, stage, altitude, weather, reward, bid, admin_name } = state || {};
- const isSessionEnded = Object.keys(stage || {})[0] === 'Results';
+ const isSessionEnded = stage && 'results' in stage;
- const rankings = stage?.Results?.rankings;
- const turns = stage?.Results?.turns;
- const participants = stage?.Registration || stage?.Results?.participants;
+ const rankings = isSessionEnded ? stage.results.rankings : [];
+ const turns = isSessionEnded ? stage.results.turns : [];
+
+ const registrationParticipants = stage && 'registration' in stage && stage.registration;
+ const resultsParticipants = isSessionEnded && stage.results.participants;
+ const participants = registrationParticipants || resultsParticipants || [];
const isUserAdmin = admin === account?.decodedAddress;
@@ -51,17 +54,16 @@ function Home() {
<>
{!isSessionEnded && (
)}
@@ -70,14 +72,13 @@ function Home() {
{rankings?.map((item) => item[0]).includes(account?.decodedAddress || '0x') ? (
diff --git a/frontend/apps/galactic-express/src/utils/index.ts b/frontend/apps/galactic-express/src/utils/index.ts
index 0ec1b2c0a..643201ddc 100644
--- a/frontend/apps/galactic-express/src/utils/index.ts
+++ b/frontend/apps/galactic-express/src/utils/index.ts
@@ -52,3 +52,14 @@ export const cx = (...styles: string[]) => clsx(...styles);
export const isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent,
);
+
+export const getPanicType = (error: unknown) => {
+ if (error instanceof Error) {
+ const errorWords = error?.message?.replaceAll("'", '').trim().split(' ');
+ const panicType = errorWords[errorWords.length - 1];
+
+ return panicType;
+ }
+
+ return null;
+};
diff --git a/frontend/apps/syndote/src/pages/welcome/components/create-game-form/CreateGameForm.tsx b/frontend/apps/syndote/src/pages/welcome/components/create-game-form/CreateGameForm.tsx
index 0108ce9f0..e5e8cfd7f 100644
--- a/frontend/apps/syndote/src/pages/welcome/components/create-game-form/CreateGameForm.tsx
+++ b/frontend/apps/syndote/src/pages/welcome/components/create-game-form/CreateGameForm.tsx
@@ -10,10 +10,6 @@ import { isNotEmpty, useForm } from '@mantine/form';
import { useSyndoteMessage } from 'hooks/metadata';
import styles from './CreateGameForm.module.scss';
-export interface ContractFormValues {
- [key: string]: string;
-}
-
type CreateFormValues = {
fee: number;
name: string;
diff --git a/frontend/apps/syndote/src/pages/welcome/components/join-game-form/JoinGameForm.tsx b/frontend/apps/syndote/src/pages/welcome/components/join-game-form/JoinGameForm.tsx
index 64fce0c31..bb55cfd78 100644
--- a/frontend/apps/syndote/src/pages/welcome/components/join-game-form/JoinGameForm.tsx
+++ b/frontend/apps/syndote/src/pages/welcome/components/join-game-form/JoinGameForm.tsx
@@ -15,10 +15,6 @@ import { TextModal } from 'pages/home/text-modal';
import styles from './JoinGameForm.module.scss';
import { GameSessionState, State } from 'types';
-export interface ContractFormValues {
- [key: string]: string;
-}
-
type Props = {
onCancel: () => void;
};
diff --git a/frontend/apps/syndote/src/pages/welcome/components/request-game/RequestGame.tsx b/frontend/apps/syndote/src/pages/welcome/components/request-game/RequestGame.tsx
index 3c43c4f44..bc0b4a411 100644
--- a/frontend/apps/syndote/src/pages/welcome/components/request-game/RequestGame.tsx
+++ b/frontend/apps/syndote/src/pages/welcome/components/request-game/RequestGame.tsx
@@ -9,10 +9,6 @@ import styles from './RequestGame.module.scss';
import { CreateGameForm } from '../create-game-form';
import { JoinGameForm } from '../join-game-form';
-export interface ContractFormValues {
- [key: string]: string;
-}
-
type Status = 'creating' | 'joining' | null;
function RequestGame() {
diff --git a/frontend/apps/varatube/src/App.tsx b/frontend/apps/varatube/src/App.tsx
index bda83bc2c..0d419fb4c 100644
--- a/frontend/apps/varatube/src/App.tsx
+++ b/frontend/apps/varatube/src/App.tsx
@@ -3,17 +3,17 @@ import { Footer } from '@dapps-frontend/ui';
import { Routing } from 'pages';
import { Header, ApiLoader } from 'components';
import { withProviders } from 'hocs';
-import { useProgramState } from 'hooks/api';
import 'simplebar-react/dist/simplebar.min.css';
import 'App.scss';
import '@gear-js/vara-ui/dist/style.css';
+import { useGetSubscriberQuery } from 'app/utils';
function Component() {
const { isApiReady } = useApi();
const { isAccountReady } = useAccount();
- const { isSubscriptionsStateRead } = useProgramState();
+ const { isFetched } = useGetSubscriberQuery();
- const isAppReady = isApiReady && isAccountReady && isSubscriptionsStateRead;
+ const isAppReady = isApiReady && isAccountReady && isFetched;
return (
<>
diff --git a/frontend/apps/varatube/src/app/utils/index.ts b/frontend/apps/varatube/src/app/utils/index.ts
new file mode 100644
index 000000000..15858e186
--- /dev/null
+++ b/frontend/apps/varatube/src/app/utils/index.ts
@@ -0,0 +1 @@
+export * from './sails';
diff --git a/frontend/apps/varatube/src/app/utils/sails/extended_vft.idl b/frontend/apps/varatube/src/app/utils/sails/extended_vft.idl
new file mode 100644
index 000000000..55310739f
--- /dev/null
+++ b/frontend/apps/varatube/src/app/utils/sails/extended_vft.idl
@@ -0,0 +1,34 @@
+constructor {
+ New : (name: str, symbol: str, decimals: u8);
+};
+
+service Vft {
+ Burn : (from: actor_id, value: u256) -> bool;
+ GrantAdminRole : (to: actor_id) -> null;
+ GrantBurnerRole : (to: actor_id) -> null;
+ GrantMinterRole : (to: actor_id) -> null;
+ Mint : (to: actor_id, value: u256) -> bool;
+ RevokeAdminRole : (from: actor_id) -> null;
+ RevokeBurnerRole : (from: actor_id) -> null;
+ RevokeMinterRole : (from: actor_id) -> null;
+ Approve : (spender: actor_id, value: u256) -> bool;
+ Transfer : (to: actor_id, value: u256) -> bool;
+ TransferFrom : (from: actor_id, to: actor_id, value: u256) -> bool;
+ query Admins : () -> vec actor_id;
+ query Burners : () -> vec actor_id;
+ query Minters : () -> vec actor_id;
+ query Allowance : (owner: actor_id, spender: actor_id) -> u256;
+ query BalanceOf : (account: actor_id) -> u256;
+ query Decimals : () -> u8;
+ query Name : () -> str;
+ query Symbol : () -> str;
+ query TotalSupply : () -> u256;
+
+ events {
+ Minted: struct { to: actor_id, value: u256 };
+ Burned: struct { from: actor_id, value: u256 };
+ Approval: struct { owner: actor_id, spender: actor_id, value: u256 };
+ Transfer: struct { from: actor_id, to: actor_id, value: u256 };
+ }
+};
+
diff --git a/frontend/apps/varatube/src/app/utils/sails/extended_vft.ts b/frontend/apps/varatube/src/app/utils/sails/extended_vft.ts
new file mode 100644
index 000000000..5ef447721
--- /dev/null
+++ b/frontend/apps/varatube/src/app/utils/sails/extended_vft.ts
@@ -0,0 +1,451 @@
+import { GearApi, decodeAddress } from '@gear-js/api';
+import { TypeRegistry } from '@polkadot/types';
+import { TransactionBuilder, getServiceNamePrefix, getFnNamePrefix, ZERO_ADDRESS } from 'sails-js';
+
+type ActorId = string;
+
+export class Program {
+ public readonly registry: TypeRegistry;
+ public readonly vft: Vft;
+
+ constructor(public api: GearApi, public programId?: `0x${string}`) {
+ const types: Record = {};
+
+ this.registry = new TypeRegistry();
+ this.registry.setKnownTypes({ types });
+ this.registry.register(types);
+
+ this.vft = new Vft(this);
+ }
+
+ newCtorFromCode(code: Uint8Array | Buffer, name: string, symbol: string, decimals: number): TransactionBuilder {
+ const builder = new TransactionBuilder(
+ this.api,
+ this.registry,
+ 'upload_program',
+ ['New', name, symbol, decimals],
+ '(String, String, String, u8)',
+ 'String',
+ code,
+ );
+
+ this.programId = builder.programId;
+ return builder;
+ }
+
+ newCtorFromCodeId(codeId: `0x${string}`, name: string, symbol: string, decimals: number) {
+ const builder = new TransactionBuilder(
+ this.api,
+ this.registry,
+ 'create_program',
+ ['New', name, symbol, decimals],
+ '(String, String, String, u8)',
+ 'String',
+ codeId,
+ );
+
+ this.programId = builder.programId;
+ return builder;
+ }
+}
+
+export class Vft {
+ constructor(private _program: Program) {}
+
+ public burn(from: ActorId, value: number | string | bigint): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Vft', 'Burn', from, value],
+ '(String, String, [u8;32], U256)',
+ 'bool',
+ this._program.programId,
+ );
+ }
+
+ public grantAdminRole(to: ActorId): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Vft', 'GrantAdminRole', to],
+ '(String, String, [u8;32])',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public grantBurnerRole(to: ActorId): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Vft', 'GrantBurnerRole', to],
+ '(String, String, [u8;32])',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public grantMinterRole(to: ActorId): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Vft', 'GrantMinterRole', to],
+ '(String, String, [u8;32])',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public mint(to: ActorId, value: number | string | bigint): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Vft', 'Mint', to, value],
+ '(String, String, [u8;32], U256)',
+ 'bool',
+ this._program.programId,
+ );
+ }
+
+ public revokeAdminRole(from: ActorId): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Vft', 'RevokeAdminRole', from],
+ '(String, String, [u8;32])',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public revokeBurnerRole(from: ActorId): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Vft', 'RevokeBurnerRole', from],
+ '(String, String, [u8;32])',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public revokeMinterRole(from: ActorId): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Vft', 'RevokeMinterRole', from],
+ '(String, String, [u8;32])',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public approve(spender: ActorId, value: number | string | bigint): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Vft', 'Approve', spender, value],
+ '(String, String, [u8;32], U256)',
+ 'bool',
+ this._program.programId,
+ );
+ }
+
+ public transfer(to: ActorId, value: number | string | bigint): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Vft', 'Transfer', to, value],
+ '(String, String, [u8;32], U256)',
+ 'bool',
+ this._program.programId,
+ );
+ }
+
+ public transferFrom(from: ActorId, to: ActorId, value: number | string | bigint): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Vft', 'TransferFrom', from, to, value],
+ '(String, String, [u8;32], [u8;32], U256)',
+ 'bool',
+ this._program.programId,
+ );
+ }
+
+ public async admins(
+ originAddress?: string,
+ value?: number | string | bigint,
+ atBlock?: `0x${string}`,
+ ): Promise> {
+ const payload = this._program.registry.createType('(String, String)', ['Vft', 'Admins']).toHex();
+ const reply = await this._program.api.message.calculateReply({
+ destination: this._program.programId!,
+ origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS,
+ payload,
+ value: value || 0,
+ gasLimit: this._program.api.blockGasLimit.toBigInt(),
+ at: atBlock,
+ });
+ if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString());
+ const result = this._program.registry.createType('(String, String, Vec<[u8;32]>)', reply.payload);
+ return result[2].toJSON() as unknown as Array;
+ }
+
+ public async burners(
+ originAddress?: string,
+ value?: number | string | bigint,
+ atBlock?: `0x${string}`,
+ ): Promise> {
+ const payload = this._program.registry.createType('(String, String)', ['Vft', 'Burners']).toHex();
+ const reply = await this._program.api.message.calculateReply({
+ destination: this._program.programId!,
+ origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS,
+ payload,
+ value: value || 0,
+ gasLimit: this._program.api.blockGasLimit.toBigInt(),
+ at: atBlock,
+ });
+ if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString());
+ const result = this._program.registry.createType('(String, String, Vec<[u8;32]>)', reply.payload);
+ return result[2].toJSON() as unknown as Array;
+ }
+
+ public async minters(
+ originAddress?: string,
+ value?: number | string | bigint,
+ atBlock?: `0x${string}`,
+ ): Promise> {
+ const payload = this._program.registry.createType('(String, String)', ['Vft', 'Minters']).toHex();
+ const reply = await this._program.api.message.calculateReply({
+ destination: this._program.programId!,
+ origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS,
+ payload,
+ value: value || 0,
+ gasLimit: this._program.api.blockGasLimit.toBigInt(),
+ at: atBlock,
+ });
+ if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString());
+ const result = this._program.registry.createType('(String, String, Vec<[u8;32]>)', reply.payload);
+ return result[2].toJSON() as unknown as Array;
+ }
+
+ public async allowance(
+ owner: ActorId,
+ spender: ActorId,
+ originAddress?: string,
+ value?: number | string | bigint,
+ atBlock?: `0x${string}`,
+ ): Promise {
+ const payload = this._program.registry
+ .createType('(String, String, [u8;32], [u8;32])', ['Vft', 'Allowance', owner, spender])
+ .toHex();
+ const reply = await this._program.api.message.calculateReply({
+ destination: this._program.programId!,
+ origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS,
+ payload,
+ value: value || 0,
+ gasLimit: this._program.api.blockGasLimit.toBigInt(),
+ at: atBlock,
+ });
+ if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString());
+ const result = this._program.registry.createType('(String, String, U256)', reply.payload);
+ return result[2].toBigInt() as unknown as bigint;
+ }
+
+ public async balanceOf(
+ account: ActorId,
+ originAddress?: string,
+ value?: number | string | bigint,
+ atBlock?: `0x${string}`,
+ ): Promise {
+ const payload = this._program.registry
+ .createType('(String, String, [u8;32])', ['Vft', 'BalanceOf', account])
+ .toHex();
+ const reply = await this._program.api.message.calculateReply({
+ destination: this._program.programId!,
+ origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS,
+ payload,
+ value: value || 0,
+ gasLimit: this._program.api.blockGasLimit.toBigInt(),
+ at: atBlock,
+ });
+ if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString());
+ const result = this._program.registry.createType('(String, String, U256)', reply.payload);
+ return result[2].toBigInt() as unknown as bigint;
+ }
+
+ public async decimals(
+ originAddress?: string,
+ value?: number | string | bigint,
+ atBlock?: `0x${string}`,
+ ): Promise {
+ const payload = this._program.registry.createType('(String, String)', ['Vft', 'Decimals']).toHex();
+ const reply = await this._program.api.message.calculateReply({
+ destination: this._program.programId!,
+ origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS,
+ payload,
+ value: value || 0,
+ gasLimit: this._program.api.blockGasLimit.toBigInt(),
+ at: atBlock,
+ });
+ if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString());
+ const result = this._program.registry.createType('(String, String, u8)', reply.payload);
+ return result[2].toNumber() as unknown as number;
+ }
+
+ public async name(
+ originAddress?: string,
+ value?: number | string | bigint,
+ atBlock?: `0x${string}`,
+ ): Promise {
+ const payload = this._program.registry.createType('(String, String)', ['Vft', 'Name']).toHex();
+ const reply = await this._program.api.message.calculateReply({
+ destination: this._program.programId!,
+ origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS,
+ payload,
+ value: value || 0,
+ gasLimit: this._program.api.blockGasLimit.toBigInt(),
+ at: atBlock,
+ });
+ if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString());
+ const result = this._program.registry.createType('(String, String, String)', reply.payload);
+ return result[2].toString() as unknown as string;
+ }
+
+ public async symbol(
+ originAddress?: string,
+ value?: number | string | bigint,
+ atBlock?: `0x${string}`,
+ ): Promise {
+ const payload = this._program.registry.createType('(String, String)', ['Vft', 'Symbol']).toHex();
+ const reply = await this._program.api.message.calculateReply({
+ destination: this._program.programId!,
+ origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS,
+ payload,
+ value: value || 0,
+ gasLimit: this._program.api.blockGasLimit.toBigInt(),
+ at: atBlock,
+ });
+ if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString());
+ const result = this._program.registry.createType('(String, String, String)', reply.payload);
+ return result[2].toString() as unknown as string;
+ }
+
+ public async totalSupply(
+ originAddress?: string,
+ value?: number | string | bigint,
+ atBlock?: `0x${string}`,
+ ): Promise {
+ const payload = this._program.registry.createType('(String, String)', ['Vft', 'TotalSupply']).toHex();
+ const reply = await this._program.api.message.calculateReply({
+ destination: this._program.programId!,
+ origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS,
+ payload,
+ value: value || 0,
+ gasLimit: this._program.api.blockGasLimit.toBigInt(),
+ at: atBlock,
+ });
+ if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString());
+ const result = this._program.registry.createType('(String, String, U256)', reply.payload);
+ return result[2].toBigInt() as unknown as bigint;
+ }
+
+ public subscribeToMintedEvent(
+ callback: (data: { to: ActorId; value: number | string | bigint }) => void | Promise,
+ ): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Vft' && getFnNamePrefix(payload) === 'Minted') {
+ callback(
+ this._program.registry
+ .createType('(String, String, {"to":"[u8;32]","value":"U256"})', message.payload)[2]
+ .toJSON() as unknown as { to: ActorId; value: number | string | bigint },
+ );
+ }
+ });
+ }
+
+ public subscribeToBurnedEvent(
+ callback: (data: { from: ActorId; value: number | string | bigint }) => void | Promise,
+ ): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Vft' && getFnNamePrefix(payload) === 'Burned') {
+ callback(
+ this._program.registry
+ .createType('(String, String, {"from":"[u8;32]","value":"U256"})', message.payload)[2]
+ .toJSON() as unknown as { from: ActorId; value: number | string | bigint },
+ );
+ }
+ });
+ }
+
+ public subscribeToApprovalEvent(
+ callback: (data: { owner: ActorId; spender: ActorId; value: number | string | bigint }) => void | Promise,
+ ): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Vft' && getFnNamePrefix(payload) === 'Approval') {
+ callback(
+ this._program.registry
+ .createType('(String, String, {"owner":"[u8;32]","spender":"[u8;32]","value":"U256"})', message.payload)[2]
+ .toJSON() as unknown as { owner: ActorId; spender: ActorId; value: number | string | bigint },
+ );
+ }
+ });
+ }
+
+ public subscribeToTransferEvent(
+ callback: (data: { from: ActorId; to: ActorId; value: number | string | bigint }) => void | Promise,
+ ): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Vft' && getFnNamePrefix(payload) === 'Transfer') {
+ callback(
+ this._program.registry
+ .createType('(String, String, {"from":"[u8;32]","to":"[u8;32]","value":"U256"})', message.payload)[2]
+ .toJSON() as unknown as { from: ActorId; to: ActorId; value: number | string | bigint },
+ );
+ }
+ });
+ }
+}
diff --git a/frontend/apps/varatube/src/app/utils/sails/index.ts b/frontend/apps/varatube/src/app/utils/sails/index.ts
new file mode 100644
index 000000000..e6bda9d41
--- /dev/null
+++ b/frontend/apps/varatube/src/app/utils/sails/index.ts
@@ -0,0 +1,3 @@
+export * from './sails';
+export * from './queries';
+export * from './messages';
diff --git a/frontend/apps/varatube/src/app/utils/sails/messages/index.ts b/frontend/apps/varatube/src/app/utils/sails/messages/index.ts
new file mode 100644
index 000000000..ac7f7bf51
--- /dev/null
+++ b/frontend/apps/varatube/src/app/utils/sails/messages/index.ts
@@ -0,0 +1,2 @@
+export { useCancelSubscriptionMessage } from './use-cancel-register-message';
+export { useRegisterSubscriptionMessage } from './use-register-subscription-message';
diff --git a/frontend/apps/varatube/src/app/utils/sails/messages/use-approve-message.ts b/frontend/apps/varatube/src/app/utils/sails/messages/use-approve-message.ts
new file mode 100644
index 000000000..4eca9b1e5
--- /dev/null
+++ b/frontend/apps/varatube/src/app/utils/sails/messages/use-approve-message.ts
@@ -0,0 +1,30 @@
+import { usePrepareProgramTransaction } from '@gear-js/react-hooks';
+import { Options, useSignAndSend } from 'hooks/use-sign-and-send';
+import { useVftProgram } from '../sails';
+
+type Params = {
+ spender: string;
+ value: number | string | bigint;
+};
+
+export const useApproveMessage = () => {
+ const program = useVftProgram();
+ const { prepareTransactionAsync } = usePrepareProgramTransaction({
+ program,
+ serviceName: 'vft',
+ functionName: 'approve',
+ });
+ const { signAndSend } = useSignAndSend();
+
+ const approveMessage = async ({ spender, value }: Params, options: Options) => {
+ const { transaction } = await prepareTransactionAsync({
+ args: [spender, value],
+
+ gasLimit: { increaseGas: 20 },
+ });
+ console.log('SIGN APPROOVE');
+ signAndSend(transaction, options);
+ };
+
+ return { approveMessage };
+};
diff --git a/frontend/apps/varatube/src/app/utils/sails/messages/use-cancel-register-message.ts b/frontend/apps/varatube/src/app/utils/sails/messages/use-cancel-register-message.ts
new file mode 100644
index 000000000..af3718739
--- /dev/null
+++ b/frontend/apps/varatube/src/app/utils/sails/messages/use-cancel-register-message.ts
@@ -0,0 +1,24 @@
+import { usePrepareProgramTransaction } from '@gear-js/react-hooks';
+import { Options, useSignAndSend } from 'hooks/use-sign-and-send';
+import { useVaratubeProgram } from '../sails';
+
+export const useCancelSubscriptionMessage = () => {
+ const program = useVaratubeProgram();
+ const { prepareTransactionAsync } = usePrepareProgramTransaction({
+ program,
+ serviceName: 'varatube',
+ functionName: 'cancelSubscription',
+ });
+
+ const { signAndSend } = useSignAndSend();
+
+ const cancelSubscriptionMessage = async (options: Options) => {
+ const { transaction } = await prepareTransactionAsync({
+ args: [],
+ gasLimit: { increaseGas: 20 },
+ });
+ signAndSend(transaction, options);
+ };
+
+ return { cancelSubscriptionMessage };
+};
diff --git a/frontend/apps/varatube/src/app/utils/sails/messages/use-register-subscription-message.ts b/frontend/apps/varatube/src/app/utils/sails/messages/use-register-subscription-message.ts
new file mode 100644
index 000000000..57649c59c
--- /dev/null
+++ b/frontend/apps/varatube/src/app/utils/sails/messages/use-register-subscription-message.ts
@@ -0,0 +1,30 @@
+import { usePrepareProgramTransaction } from '@gear-js/react-hooks';
+import { Options, useSignAndSend } from 'hooks/use-sign-and-send';
+import { useVaratubeProgram } from '../sails';
+import { Period } from '../varatube';
+
+type Params = {
+ period: Period;
+ currency_id: `0x${string}`;
+ with_renewal: boolean;
+};
+
+export const useRegisterSubscriptionMessage = () => {
+ const program = useVaratubeProgram();
+ const { prepareTransactionAsync } = usePrepareProgramTransaction({
+ program,
+ serviceName: 'varatube',
+ functionName: 'registerSubscription',
+ });
+ const { signAndSend } = useSignAndSend();
+
+ const registerSubscriptionMessage = async ({ period, currency_id, with_renewal }: Params, options: Options) => {
+ const { transaction } = await prepareTransactionAsync({
+ args: [period, currency_id, with_renewal],
+ gasLimit: { increaseGas: 20 },
+ });
+ signAndSend(transaction, options);
+ };
+
+ return { registerSubscriptionMessage };
+};
diff --git a/frontend/apps/varatube/src/app/utils/sails/queries/index.ts b/frontend/apps/varatube/src/app/utils/sails/queries/index.ts
new file mode 100644
index 000000000..aeab04022
--- /dev/null
+++ b/frontend/apps/varatube/src/app/utils/sails/queries/index.ts
@@ -0,0 +1,2 @@
+export { useGetSubscriberQuery } from './use-get-subscriber-query';
+export { useBalanceOfQuery } from './use-balance-of-query';
diff --git a/frontend/apps/varatube/src/app/utils/sails/queries/use-balance-of-query.ts b/frontend/apps/varatube/src/app/utils/sails/queries/use-balance-of-query.ts
new file mode 100644
index 000000000..294935e7a
--- /dev/null
+++ b/frontend/apps/varatube/src/app/utils/sails/queries/use-balance-of-query.ts
@@ -0,0 +1,17 @@
+import { useVftProgram } from '../sails';
+import { useAccount, useProgramQuery } from '@gear-js/react-hooks';
+
+export const useBalanceOfQuery = () => {
+ const program = useVftProgram();
+ const { account } = useAccount();
+
+ const { data, refetch, isFetching, error } = useProgramQuery({
+ program,
+ serviceName: 'vft',
+ functionName: 'balanceOf',
+ args: [account?.decodedAddress || ''],
+ query: { enabled: account ? undefined : false },
+ });
+
+ return { balance: data, isFetching, refetch, error };
+};
diff --git a/frontend/apps/varatube/src/app/utils/sails/queries/use-currencies-query.ts b/frontend/apps/varatube/src/app/utils/sails/queries/use-currencies-query.ts
new file mode 100644
index 000000000..7ebb75d5c
--- /dev/null
+++ b/frontend/apps/varatube/src/app/utils/sails/queries/use-currencies-query.ts
@@ -0,0 +1,15 @@
+import { useVaratubeProgram } from '../sails';
+import { useProgramQuery } from '@gear-js/react-hooks';
+
+export const useCurrenciesQuery = () => {
+ const program = useVaratubeProgram();
+
+ const { data, refetch, isFetching, error } = useProgramQuery({
+ program,
+ serviceName: 'varatube',
+ functionName: 'currencies',
+ args: [],
+ });
+
+ return { currencies: data, isFetching, refetch, error };
+};
diff --git a/frontend/apps/varatube/src/app/utils/sails/queries/use-get-subscriber-query.ts b/frontend/apps/varatube/src/app/utils/sails/queries/use-get-subscriber-query.ts
new file mode 100644
index 000000000..053766508
--- /dev/null
+++ b/frontend/apps/varatube/src/app/utils/sails/queries/use-get-subscriber-query.ts
@@ -0,0 +1,19 @@
+import { useVaratubeProgram } from '../sails';
+import { useAccount, useProgramQuery } from '@gear-js/react-hooks';
+
+export const useGetSubscriberQuery = () => {
+ const program = useVaratubeProgram();
+ const { account } = useAccount();
+
+ const { data, refetch, isFetching, error, isFetched } = useProgramQuery({
+ program,
+ serviceName: 'varatube',
+ functionName: 'allSubscriptions',
+ args: [],
+ query: { enabled: true },
+ });
+
+ const subscriber = data?.find(([address]) => account?.decodedAddress === address)?.[1];
+
+ return { subscriber, isFetching, isFetched, refetch, error };
+};
diff --git a/frontend/apps/varatube/src/app/utils/sails/sails.ts b/frontend/apps/varatube/src/app/utils/sails/sails.ts
new file mode 100644
index 000000000..9aa59b1cf
--- /dev/null
+++ b/frontend/apps/varatube/src/app/utils/sails/sails.ts
@@ -0,0 +1,24 @@
+import { useProgram as useGearJsProgram } from '@gear-js/react-hooks';
+import { Program as VaratubeProgram } from './varatube';
+import { Program as VftProgram } from './extended_vft';
+import { ADDRESS } from 'consts';
+
+const useVaratubeProgram = () => {
+ const { data: program } = useGearJsProgram({
+ library: VaratubeProgram,
+ id: ADDRESS.CONTRACT,
+ });
+
+ return program;
+};
+
+const useVftProgram = () => {
+ const { data: program } = useGearJsProgram({
+ library: VftProgram,
+ id: ADDRESS.FT_CONTRACT,
+ });
+
+ return program;
+};
+
+export { useVaratubeProgram, useVftProgram };
diff --git a/frontend/apps/varatube/src/app/utils/sails/varatube.idl b/frontend/apps/varatube/src/app/utils/sails/varatube.idl
new file mode 100644
index 000000000..8989d6b7b
--- /dev/null
+++ b/frontend/apps/varatube/src/app/utils/sails/varatube.idl
@@ -0,0 +1,65 @@
+type Config = struct {
+ gas_for_token_transfer: u64,
+ gas_to_start_subscription_update: u64,
+ block_duration: u32,
+ min_gas_limit: u64,
+};
+
+type Period = enum {
+ Year,
+ NineMonths,
+ SixMonths,
+ ThreeMonths,
+ Month,
+};
+
+type SubscriberDataState = struct {
+ is_active: bool,
+ start_date: u64,
+ start_block: u32,
+ end_date: u64,
+ end_block: u32,
+ period: Period,
+ will_renew: bool,
+ price: u128,
+};
+
+type SubscriberData = struct {
+ currency_id: actor_id,
+ period: Period,
+
+ subscription_start: opt struct { u64, u32 },
+
+ renewal_date: opt struct { u64, u32 },
+};
+
+constructor {
+ New : (config: Config, dns_id_and_name: opt struct { actor_id, str });
+};
+
+service Varatube {
+ AddTokenData : (token_id: actor_id, price: u128) -> null;
+ CancelSubscription : () -> null;
+ Kill : (inheritor: actor_id) -> null;
+ ManagePendingSubscription : (enable: bool) -> null;
+ RegisterSubscription : (period: Period, currency_id: actor_id, with_renewal: bool) -> null;
+ UpdateConfig : (gas_for_token_transfer: opt u64, gas_to_start_subscription_update: opt u64, block_duration: opt u32) -> null;
+ UpdateSubscription : (subscriber: actor_id) -> null;
+ query Admins : () -> vec actor_id;
+ query AllSubscriptions : () -> vec struct { actor_id, SubscriberDataState };
+ query Config : () -> Config;
+ query Currencies : () -> vec struct { actor_id, u128 };
+ query GetSubscriber : (account: actor_id) -> opt SubscriberData;
+ query Subscribers : () -> vec struct { actor_id, SubscriberData };
+
+ events {
+ SubscriptionRegistered;
+ SubscriptionUpdated;
+ SubscriptionCancelled;
+ PendingSubscriptionManaged;
+ PaymentAdded;
+ ConfigUpdated;
+ Killed: struct { inheritor: actor_id };
+ }
+};
+
diff --git a/frontend/apps/varatube/src/app/utils/sails/varatube.ts b/frontend/apps/varatube/src/app/utils/sails/varatube.ts
new file mode 100644
index 000000000..1e06922f9
--- /dev/null
+++ b/frontend/apps/varatube/src/app/utils/sails/varatube.ts
@@ -0,0 +1,421 @@
+import { TransactionBuilder, getServiceNamePrefix, getFnNamePrefix, ZERO_ADDRESS } from 'sails-js';
+import { GearApi, decodeAddress } from '@gear-js/api';
+import { TypeRegistry } from '@polkadot/types';
+
+type ActorId = string;
+
+export interface Config {
+ gas_for_token_transfer: number | string | bigint;
+ gas_to_start_subscription_update: number | string | bigint;
+ block_duration: number;
+ min_gas_limit: number | string | bigint;
+}
+
+export type Period = 'year' | 'nineMonths' | 'sixMonths' | 'threeMonths' | 'month';
+
+export interface SubscriberDataState {
+ is_active: boolean;
+ start_date: number | string | bigint;
+ start_block: number;
+ end_date: number | string | bigint;
+ end_block: number;
+ period: Period;
+ will_renew: boolean;
+ price: number | string | bigint;
+}
+
+export interface SubscriberData {
+ currency_id: ActorId;
+ period: Period;
+ subscription_start: [number | string | bigint, number] | null;
+ renewal_date: [number | string | bigint, number] | null;
+}
+
+export class Program {
+ public readonly registry: TypeRegistry;
+ public readonly varatube: Varatube;
+
+ constructor(public api: GearApi, public programId?: `0x${string}`) {
+ const types: Record = {
+ Config: {
+ gas_for_token_transfer: 'u64',
+ gas_to_start_subscription_update: 'u64',
+ block_duration: 'u32',
+ min_gas_limit: 'u64',
+ },
+ Period: { _enum: ['Year', 'NineMonths', 'SixMonths', 'ThreeMonths', 'Month'] },
+ SubscriberDataState: {
+ is_active: 'bool',
+ start_date: 'u64',
+ start_block: 'u32',
+ end_date: 'u64',
+ end_block: 'u32',
+ period: 'Period',
+ will_renew: 'bool',
+ price: 'u128',
+ },
+ SubscriberData: {
+ currency_id: '[u8;32]',
+ period: 'Period',
+ subscription_start: 'Option<(u64, u32)>',
+ renewal_date: 'Option<(u64, u32)>',
+ },
+ };
+
+ this.registry = new TypeRegistry();
+ this.registry.setKnownTypes({ types });
+ this.registry.register(types);
+
+ this.varatube = new Varatube(this);
+ }
+
+ newCtorFromCode(
+ code: Uint8Array | Buffer,
+ config: Config,
+ dns_id_and_name: [ActorId, string] | null,
+ ): TransactionBuilder {
+ const builder = new TransactionBuilder(
+ this.api,
+ this.registry,
+ 'upload_program',
+ ['New', config, dns_id_and_name],
+ '(String, Config, Option<([u8;32], String)>)',
+ 'String',
+ code,
+ );
+
+ this.programId = builder.programId;
+ return builder;
+ }
+
+ newCtorFromCodeId(codeId: `0x${string}`, config: Config, dns_id_and_name: [ActorId, string] | null) {
+ const builder = new TransactionBuilder(
+ this.api,
+ this.registry,
+ 'create_program',
+ ['New', config, dns_id_and_name],
+ '(String, Config, Option<([u8;32], String)>)',
+ 'String',
+ codeId,
+ );
+
+ this.programId = builder.programId;
+ return builder;
+ }
+}
+
+export class Varatube {
+ constructor(private _program: Program) {}
+
+ public addTokenData(token_id: ActorId, price: number | string | bigint): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Varatube', 'AddTokenData', token_id, price],
+ '(String, String, [u8;32], u128)',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public cancelSubscription(): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Varatube', 'CancelSubscription'],
+ '(String, String)',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public kill(inheritor: ActorId): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Varatube', 'Kill', inheritor],
+ '(String, String, [u8;32])',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public managePendingSubscription(enable: boolean): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Varatube', 'ManagePendingSubscription', enable],
+ '(String, String, bool)',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public registerSubscription(period: Period, currency_id: ActorId, with_renewal: boolean): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Varatube', 'RegisterSubscription', period, currency_id, with_renewal],
+ '(String, String, Period, [u8;32], bool)',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public updateConfig(
+ gas_for_token_transfer: number | string | bigint | null,
+ gas_to_start_subscription_update: number | string | bigint | null,
+ block_duration: number | null,
+ ): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Varatube', 'UpdateConfig', gas_for_token_transfer, gas_to_start_subscription_update, block_duration],
+ '(String, String, Option, Option, Option)',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public updateSubscription(subscriber: ActorId): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Varatube', 'UpdateSubscription', subscriber],
+ '(String, String, [u8;32])',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public async admins(
+ originAddress?: string,
+ value?: number | string | bigint,
+ atBlock?: `0x${string}`,
+ ): Promise> {
+ const payload = this._program.registry.createType('(String, String)', ['Varatube', 'Admins']).toHex();
+ const reply = await this._program.api.message.calculateReply({
+ destination: this._program.programId!,
+ origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS,
+ payload,
+ value: value || 0,
+ gasLimit: this._program.api.blockGasLimit.toBigInt(),
+ at: atBlock,
+ });
+ if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString());
+ const result = this._program.registry.createType('(String, String, Vec<[u8;32]>)', reply.payload);
+ return result[2].toJSON() as unknown as Array;
+ }
+
+ public async allSubscriptions(
+ originAddress?: string,
+ value?: number | string | bigint,
+ atBlock?: `0x${string}`,
+ ): Promise> {
+ const payload = this._program.registry.createType('(String, String)', ['Varatube', 'AllSubscriptions']).toHex();
+ const reply = await this._program.api.message.calculateReply({
+ destination: this._program.programId!,
+ origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS,
+ payload,
+ value: value || 0,
+ gasLimit: this._program.api.blockGasLimit.toBigInt(),
+ at: atBlock,
+ });
+ if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString());
+ const result = this._program.registry.createType(
+ '(String, String, Vec<([u8;32], SubscriberDataState)>)',
+ reply.payload,
+ );
+ return result[2].toJSON() as unknown as Array<[ActorId, SubscriberDataState]>;
+ }
+
+ public async config(
+ originAddress?: string,
+ value?: number | string | bigint,
+ atBlock?: `0x${string}`,
+ ): Promise {
+ const payload = this._program.registry.createType('(String, String)', ['Varatube', 'Config']).toHex();
+ const reply = await this._program.api.message.calculateReply({
+ destination: this._program.programId!,
+ origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS,
+ payload,
+ value: value || 0,
+ gasLimit: this._program.api.blockGasLimit.toBigInt(),
+ at: atBlock,
+ });
+ if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString());
+ const result = this._program.registry.createType('(String, String, Config)', reply.payload);
+ return result[2].toJSON() as unknown as Config;
+ }
+
+ public async currencies(
+ originAddress?: string,
+ value?: number | string | bigint,
+ atBlock?: `0x${string}`,
+ ): Promise> {
+ const payload = this._program.registry.createType('(String, String)', ['Varatube', 'Currencies']).toHex();
+ const reply = await this._program.api.message.calculateReply({
+ destination: this._program.programId!,
+ origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS,
+ payload,
+ value: value || 0,
+ gasLimit: this._program.api.blockGasLimit.toBigInt(),
+ at: atBlock,
+ });
+ if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString());
+ const result = this._program.registry.createType('(String, String, Vec<([u8;32], u128)>)', reply.payload);
+ return result[2].toJSON() as unknown as Array<[ActorId, number | string | bigint]>;
+ }
+
+ public async getSubscriber(
+ account: ActorId,
+ originAddress?: string,
+ value?: number | string | bigint,
+ atBlock?: `0x${string}`,
+ ): Promise {
+ const payload = this._program.registry
+ .createType('(String, String, [u8;32])', ['Varatube', 'GetSubscriber', account])
+ .toHex();
+ const reply = await this._program.api.message.calculateReply({
+ destination: this._program.programId!,
+ origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS,
+ payload,
+ value: value || 0,
+ gasLimit: this._program.api.blockGasLimit.toBigInt(),
+ at: atBlock,
+ });
+ if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString());
+ const result = this._program.registry.createType('(String, String, Option)', reply.payload);
+ return result[2].toJSON() as unknown as SubscriberData | null;
+ }
+
+ public async subscribers(
+ originAddress?: string,
+ value?: number | string | bigint,
+ atBlock?: `0x${string}`,
+ ): Promise> {
+ const payload = this._program.registry.createType('(String, String)', ['Varatube', 'Subscribers']).toHex();
+ const reply = await this._program.api.message.calculateReply({
+ destination: this._program.programId!,
+ origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS,
+ payload,
+ value: value || 0,
+ gasLimit: this._program.api.blockGasLimit.toBigInt(),
+ at: atBlock,
+ });
+ if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString());
+ const result = this._program.registry.createType('(String, String, Vec<([u8;32], SubscriberData)>)', reply.payload);
+ return result[2].toJSON() as unknown as Array<[ActorId, SubscriberData]>;
+ }
+
+ public subscribeToSubscriptionRegisteredEvent(callback: (data: null) => void | Promise): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Varatube' && getFnNamePrefix(payload) === 'SubscriptionRegistered') {
+ callback(null);
+ }
+ });
+ }
+
+ public subscribeToSubscriptionUpdatedEvent(callback: (data: null) => void | Promise): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Varatube' && getFnNamePrefix(payload) === 'SubscriptionUpdated') {
+ callback(null);
+ }
+ });
+ }
+
+ public subscribeToSubscriptionCancelledEvent(callback: (data: null) => void | Promise): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Varatube' && getFnNamePrefix(payload) === 'SubscriptionCancelled') {
+ callback(null);
+ }
+ });
+ }
+
+ public subscribeToPendingSubscriptionManagedEvent(
+ callback: (data: null) => void | Promise,
+ ): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Varatube' && getFnNamePrefix(payload) === 'PendingSubscriptionManaged') {
+ callback(null);
+ }
+ });
+ }
+
+ public subscribeToPaymentAddedEvent(callback: (data: null) => void | Promise): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Varatube' && getFnNamePrefix(payload) === 'PaymentAdded') {
+ callback(null);
+ }
+ });
+ }
+
+ public subscribeToConfigUpdatedEvent(callback: (data: null) => void | Promise): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Varatube' && getFnNamePrefix(payload) === 'ConfigUpdated') {
+ callback(null);
+ }
+ });
+ }
+
+ public subscribeToKilledEvent(callback: (data: { inheritor: ActorId }) => void | Promise): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Varatube' && getFnNamePrefix(payload) === 'Killed') {
+ callback(
+ this._program.registry
+ .createType('(String, String, {"inheritor":"[u8;32]"})', message.payload)[2]
+ .toJSON() as unknown as { inheritor: ActorId },
+ );
+ }
+ });
+ }
+}
diff --git a/frontend/apps/varatube/src/assets/state/ft_meta.txt b/frontend/apps/varatube/src/assets/state/ft_meta.txt
deleted file mode 100644
index 92bac072d..000000000
--- a/frontend/apps/varatube/src/assets/state/ft_meta.txt
+++ /dev/null
@@ -1 +0,0 @@
-0002000100000000000103000000010700000000000000000108000000690c3400084466756e6769626c655f746f6b656e5f696f28496e6974436f6e66696700000c01106e616d65040118537472696e6700011873796d626f6c040118537472696e67000120646563696d616c73080108753800000400000502000800000503000c084466756e6769626c655f746f6b656e5f696f204654416374696f6e000118104d696e74040010011075313238000000104275726e040010011075313238000100205472616e736665720c011066726f6d14011c4163746f724964000108746f14011c4163746f724964000118616d6f756e74100110753132380002001c417070726f7665080108746f14011c4163746f724964000118616d6f756e74100110753132380003002c546f74616c537570706c790004002442616c616e63654f66040014011c4163746f724964000500001000000507001410106773746418636f6d6d6f6e287072696d6974697665731c4163746f724964000004001801205b75383b2033325d0000180000032000000008001c084466756e6769626c655f746f6b656e5f696f1c46544576656e74000110205472616e736665720c011066726f6d14011c4163746f724964000108746f14011c4163746f724964000118616d6f756e74100110753132380000001c417070726f76650c011066726f6d14011c4163746f724964000108746f14011c4163746f724964000118616d6f756e74100110753132380001002c546f74616c537570706c790400100110753132380002001c42616c616e63650400100110753132380003000020084466756e6769626c655f746f6b656e5f696f3c496f46756e6769626c65546f6b656e00001801106e616d65040118537472696e6700011873796d626f6c040118537472696e67000130746f74616c5f737570706c791001107531323800012062616c616e6365732401505665633c284163746f7249642c2075313238293e000128616c6c6f77616e6365732c01905665633c284163746f7249642c205665633c284163746f7249642c2075313238293e293e000120646563696d616c730801087538000024000002280028000004081410002c00000230003000000408142400
\ No newline at end of file
diff --git a/frontend/apps/varatube/src/assets/state/varatube_meta.txt b/frontend/apps/varatube/src/assets/state/varatube_meta.txt
deleted file mode 100644
index 33aa84eee..000000000
--- a/frontend/apps/varatube/src/assets/state/varatube_meta.txt
+++ /dev/null
@@ -1 +0,0 @@
-0002000100000000000103000000010c00000000000000010f0000000110000000891b6c00082c76617261747562655f696f18436f6e66696700000c01586761735f666f725f746f6b656e5f7472616e7366657204010c75363400014c6761735f666f725f64656c617965645f6d736704010c753634000138626c6f636b5f6475726174696f6e08010c75333200000400000506000800000505000c082c76617261747562655f696f1c416374696f6e73000118505265676973746572537562736372697074696f6e0c012c63757272656e63795f696410011c4163746f724964000118706572696f641c0118506572696f64000130776974685f72656e6577616c200110626f6f6c00000048557064617465537562736372697074696f6e0401287375627363726962657210011c4163746f7249640001004843616e63656c537562736372697074696f6e000200644d616e61676550656e64696e67537562736372697074696f6e040118656e61626c65200110626f6f6c00030030416464546f6b656e44617461080120746f6b656e5f696410011c4163746f7249640001147072696365240114507269636500040030557064617465436f6e6669670c01586761735f666f725f746f6b656e5f7472616e7366657228012c4f7074696f6e3c7536343e00014c6761735f666f725f64656c617965645f6d736728012c4f7074696f6e3c7536343e000138626c6f636b5f6475726174696f6e2c012c4f7074696f6e3c7533323e000500001010106773746418636f6d6d6f6e287072696d6974697665731c4163746f724964000004001401205b75383b2033325d0000140000032000000018001800000503001c082c76617261747562655f696f18506572696f640001141059656172000000284e696e654d6f6e746873000100245369784d6f6e7468730002002c54687265654d6f6e746873000300144d6f6e7468000400002000000500002400000507002804184f7074696f6e04045401040108104e6f6e6500000010536f6d6504000400000100002c04184f7074696f6e04045401080108104e6f6e6500000010536f6d650400080000010000300418526573756c740804540134044501380108084f6b040034000000000c457272040038000001000034082c76617261747562655f696f145265706c7900011858537562736372697074696f6e526567697374657265640000004c537562736372697074696f6e5570646174656400010054537562736372697074696f6e43616e63656c6c65640002006850656e64696e67537562736372697074696f6e4d616e61676564000300305061796d656e74416464656400040034436f6e666967557064617465640005000038082c76617261747562655f696f144572726f72000124604163636f756e74416c726561647952656769737465726564000000844572726f72496e53656e64696e674d7367546f5472616e73666572546f6b656e73000100784572726f72496e526563656976696e675265706c7946726f6d546f6b656e000200704572726f72447572696e6753656e64696e6744656c617965644d73670003004c4163636f756e74446f65734e6f7445786973740004003857726f6e674d7367536f7572636500050064556e726567697374657265645061796d656e744d6574686f6400060060537562736372697074696f6e49734e6f7450656e64696e67000700204e6f7441646d696e000800003c082c76617261747562655f696f28537461746551756572790001101841646d696e730000002843757272656e636965730001002c537562736372696265727300020018436f6e6669670003000040082c76617261747562655f696f2853746174655265706c790001101841646d696e7304004401305665633c4163746f7249643e0000002843757272656e63696573040048016042547265654d61703c4163746f7249642c2050726963653e0001002c5375627363726962657273040054018442547265654d61703c4163746f7249642c2053756273637269626572446174613e00020018436f6e6669670400000118436f6e6669670003000044000002100048042042547265654d617008044b0110045601240004004c0000004c0000025000500000040810240054042042547265654d617008044b0110045601580004006400000058082c76617261747562655f696f385375627363726962657244617461000010012c63757272656e63795f696410011c4163746f724964000118706572696f641c0118506572696f64000148737562736372697074696f6e5f73746172745c01484f7074696f6e3c287536342c20753332293e00013072656e6577616c5f646174655c01484f7074696f6e3c287536342c20753332293e00005c04184f7074696f6e04045401600108104e6f6e6500000010536f6d65040060000001000060000004080408006400000268006800000408105800
\ No newline at end of file
diff --git a/frontend/apps/varatube/src/atoms.ts b/frontend/apps/varatube/src/atoms.ts
deleted file mode 100644
index eed4334a6..000000000
--- a/frontend/apps/varatube/src/atoms.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { atom } from 'jotai';
-import { FullSubState, State } from 'types';
-
-const STATE_ATOM = atom(null);
-
-const IS_STATE_READ_ATOM = atom(false);
-
-const IS_AVAILABLE_BALANCE_READY = atom(false);
-
-const AVAILABLE_BALANCE = atom(undefined);
-
-export { STATE_ATOM, IS_STATE_READ_ATOM, IS_AVAILABLE_BALANCE_READY, AVAILABLE_BALANCE };
diff --git a/frontend/apps/varatube/src/components/layout/header/Header.tsx b/frontend/apps/varatube/src/components/layout/header/Header.tsx
index 9bc6477dd..de68917bb 100644
--- a/frontend/apps/varatube/src/components/layout/header/Header.tsx
+++ b/frontend/apps/varatube/src/components/layout/header/Header.tsx
@@ -3,11 +3,11 @@ import clsx from 'clsx';
import { Link } from 'react-router-dom';
import { MenuHandler, Header as CommonHeader } from '@dapps-frontend/ui';
import logo from 'assets/images/logo.png';
-import { useFTBalance } from 'hooks/api';
import styles from './Header.module.scss';
+import { useBalanceOfQuery } from 'app/utils';
function Header() {
- const tokens = useFTBalance();
+ const { balance } = useBalanceOfQuery();
return (
- {tokens && (
+ {balance && (
- Tokens: {tokens}
+ Tokens: {String(balance)}
)}
>
diff --git a/frontend/apps/varatube/src/components/layout/on-login/OnLogin.tsx b/frontend/apps/varatube/src/components/layout/on-login/OnLogin.tsx
index f4f442be3..02f8fd93b 100644
--- a/frontend/apps/varatube/src/components/layout/on-login/OnLogin.tsx
+++ b/frontend/apps/varatube/src/components/layout/on-login/OnLogin.tsx
@@ -1,6 +1,5 @@
-import { useAccount } from '@gear-js/react-hooks';
+import { useGetSubscriberQuery } from 'app/utils/sails/queries';
import { Loader } from 'components';
-import { useProgramState } from 'hooks/api';
import { ReactNode } from 'react';
@@ -9,19 +8,9 @@ type Props = {
};
function OnLogin({ children }: Props) {
- const { account } = useAccount();
- const { decodedAddress } = account || {};
+ const { subscriber, isFetched } = useGetSubscriberQuery();
- const { subscriptionsState, isSubscriptionsStateRead } = useProgramState();
- const subscription = subscriptionsState && decodedAddress ? subscriptionsState[decodedAddress] : undefined;
-
- return isSubscriptionsStateRead ? (
- <>
- {subscription && children} {!subscription &&
}
- >
- ) : (
-
- );
+ return !isFetched ? <>{subscriber ? children :
}> : ;
}
export { OnLogin };
diff --git a/frontend/apps/varatube/src/components/modals/purchase-subscription-modal/PurchaseSubscriptionModal.tsx b/frontend/apps/varatube/src/components/modals/purchase-subscription-modal/PurchaseSubscriptionModal.tsx
index 3e732e21e..ae8d65108 100644
--- a/frontend/apps/varatube/src/components/modals/purchase-subscription-modal/PurchaseSubscriptionModal.tsx
+++ b/frontend/apps/varatube/src/components/modals/purchase-subscription-modal/PurchaseSubscriptionModal.tsx
@@ -3,11 +3,10 @@ import { useForm as useMantineForm } from '@mantine/form';
import { UseFormInput } from '@mantine/form/lib/use-form';
import { ChangeEvent } from 'react';
import styles from './PurchaseSubscriptionModal.module.scss';
-import { periods } from 'consts';
+import { initialValues, periods } from 'consts';
+import { FormValues } from 'types';
-const initialValues = { isRenewal: true, period: periods[0].value };
-
-type Props = { disabledSubmitButton: boolean; close: () => void; onSubmit: (values: typeof initialValues) => void };
+type Props = { disabledSubmitButton: boolean; close: () => void; onSubmit: (values: FormValues) => void };
const useForm = (input: UseFormInput>) => {
const form = useMantineForm(input);
diff --git a/frontend/apps/varatube/src/consts.ts b/frontend/apps/varatube/src/consts.ts
index 86e37f9ea..5befaf27a 100644
--- a/frontend/apps/varatube/src/consts.ts
+++ b/frontend/apps/varatube/src/consts.ts
@@ -20,6 +20,8 @@ const periods = [
{ label: '1 month', value: 'Month', rate: 1 },
];
+const initialValues = { isRenewal: true, period: periods[0].value };
+
const VOUCHER_MIN_LIMIT = 18;
-export { ADDRESS, LOCAL_STORAGE, periods, VOUCHER_MIN_LIMIT };
+export { ADDRESS, LOCAL_STORAGE, periods, initialValues, VOUCHER_MIN_LIMIT };
diff --git a/frontend/apps/varatube/src/hooks/api.ts b/frontend/apps/varatube/src/hooks/api.ts
deleted file mode 100644
index 7635fccfd..000000000
--- a/frontend/apps/varatube/src/hooks/api.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { useAtom } from 'jotai';
-import { useAccount, useApi, useReadFullState, useSendMessage, useSendMessageWithGas } from '@gear-js/react-hooks';
-import { HexString } from '@polkadot/util/types';
-import varatubeMeta from 'assets/state/varatube_meta.txt';
-import ftMeta from 'assets/state/ft_meta.txt';
-import { ADDRESS } from 'consts';
-import { useProgramMetadata } from './metadata';
-import { useCallback, useEffect } from 'react';
-import { IS_STATE_READ_ATOM, STATE_ATOM } from 'atoms';
-import { FullSubState } from 'types';
-
-function useSubscriptionMeta() {
- return useProgramMetadata(varatubeMeta);
-}
-
-function useFTMeta() {
- return useProgramMetadata(ftMeta);
-}
-
-function useSubscriptionsMessage() {
- const metadata = useSubscriptionMeta();
-
- return useSendMessage(ADDRESS.CONTRACT, metadata);
-}
-
-function useFTMessage() {
- const metadata = useFTMeta();
-
- return useSendMessageWithGas(ADDRESS.FT_CONTRACT, metadata, { isMaxGasLimit: true });
-}
-
-type FTState = { balances: [[HexString, string]] };
-
-function useFTBalance() {
- const { account } = useAccount();
- const { decodedAddress } = account || {};
-
- const meta = useProgramMetadata(ftMeta);
- const { state } = useReadFullState(ADDRESS.FT_CONTRACT, meta, '0x');
-
- const balances = state?.balances;
- const userBalanceEntity = balances?.find(([address]) => address === decodedAddress);
- const [, balance] = userBalanceEntity || [];
-
- return balance;
-}
-
-function useProgramState() {
- const meta = useSubscriptionMeta();
- const { api } = useApi();
- const programId = ADDRESS.CONTRACT;
- const [data, setData] = useAtom(STATE_ATOM);
- const [isStateRead, setIsStateRead] = useAtom(IS_STATE_READ_ATOM);
-
- const triggerState = useCallback(() => {
- if (!api || !meta || !programId) return;
-
- const payload = {
- Subscribers: null,
- };
-
- api.programState
- .read({ programId, payload }, meta)
- .then((codec) => codec.toHuman())
- .then((state: any) => {
- setData(state);
- setIsStateRead(true);
- });
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [api, meta, programId, setData]);
-
- useEffect(() => {
- if (!isStateRead) {
- triggerState();
- }
- }, [isStateRead, triggerState]);
-
- const state = {
- subscriptionsState: data?.Subscribers || null,
- isSubscriptionsStateRead: isStateRead,
- updateState: triggerState,
- };
-
- return state;
-}
-
-export { useSubscriptionsMessage, useFTBalance, useFTMessage, useProgramState };
diff --git a/frontend/apps/varatube/src/hooks/index.ts b/frontend/apps/varatube/src/hooks/index.ts
index 66c40f5b4..ed1dd62f8 100644
--- a/frontend/apps/varatube/src/hooks/index.ts
+++ b/frontend/apps/varatube/src/hooks/index.ts
@@ -1,26 +1,20 @@
-import { useAccount } from '@gear-js/react-hooks';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
-import { useProgramState, useSubscriptionsMessage } from './api';
+import { useGetSubscriberQuery } from 'app/utils';
function useSubscription() {
const navigate = useNavigate();
- const { account } = useAccount();
- const { decodedAddress } = account || {};
-
- const { subscriptionsState, isSubscriptionsStateRead } = useProgramState();
-
- const subscription = subscriptionsState && decodedAddress ? subscriptionsState[decodedAddress] : undefined;
+ const { subscriber, isFetched } = useGetSubscriberQuery();
useEffect(() => {
- if (isSubscriptionsStateRead && !subscription) {
+ if (isFetched && !subscriber) {
navigate('/subscription');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [isSubscriptionsStateRead, subscription, account]);
+ }, [isFetched, subscriber]);
- return isSubscriptionsStateRead;
+ return Boolean(subscriber);
}
-export { useSubscriptionsMessage, useSubscription };
+export { useSubscription };
diff --git a/frontend/apps/varatube/src/hooks/metadata.ts b/frontend/apps/varatube/src/hooks/metadata.ts
deleted file mode 100644
index 183cea697..000000000
--- a/frontend/apps/varatube/src/hooks/metadata.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { useEffect, useState } from 'react';
-import { useAlert } from '@gear-js/react-hooks';
-import { getStateMetadata, ProgramMetadata, StateMetadata } from '@gear-js/api';
-import { HexString } from '@polkadot/util/types';
-
-function useProgramMetadata(source: string) {
- const alert = useAlert();
-
- const [metadata, setMetadata] = useState();
-
- useEffect(() => {
- fetch(source)
- .then((response) => response.text())
- .then((raw) => `0x${raw}` as HexString)
- .then((metaHex) => ProgramMetadata.from(metaHex))
- .then((result) => setMetadata(result))
- .catch(({ message }: Error) => alert.error(message));
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- return metadata;
-}
-
-function useStateMetadata(wasm: Buffer | undefined) {
- const alert = useAlert();
-
- const [stateMetadata, setStateMetadata] = useState();
-
- useEffect(() => {
- if (!wasm) return;
-
- getStateMetadata(wasm)
- .then((result) => setStateMetadata(result))
- .catch(({ message }: Error) => alert.error(message));
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [wasm]);
-
- return stateMetadata;
-}
-
-export { useProgramMetadata, useStateMetadata };
diff --git a/frontend/apps/varatube/src/hooks/use-sign-and-send.ts b/frontend/apps/varatube/src/hooks/use-sign-and-send.ts
new file mode 100644
index 000000000..1719b7df9
--- /dev/null
+++ b/frontend/apps/varatube/src/hooks/use-sign-and-send.ts
@@ -0,0 +1,40 @@
+import { useCheckBalance } from '@dapps-frontend/hooks';
+import { useAlert } from '@gear-js/react-hooks';
+import { GenericTransactionReturn, TransactionReturn } from '@gear-js/react-hooks/dist/esm/hooks/sails/types';
+
+export type Options = {
+ onSuccess?: (result: T) => void;
+ onError?: () => void;
+};
+
+export const useSignAndSend = () => {
+ const { checkBalance } = useCheckBalance();
+ const alert = useAlert();
+
+ const signAndSend = async (
+ transaction: TransactionReturn<() => GenericTransactionReturn>,
+ options?: Options,
+ ) => {
+ const { onSuccess, onError } = options || {};
+ const calculatedGas = Number(transaction.extrinsic.args[2].toString());
+ checkBalance(
+ calculatedGas,
+ async () => {
+ try {
+ const { response } = await transaction.signAndSend();
+ const result = await response();
+ onSuccess?.(result);
+ } catch (e) {
+ onError?.();
+ console.error(e);
+ if (typeof e === 'string') {
+ alert.error(e);
+ }
+ }
+ },
+ onError,
+ );
+ };
+
+ return { signAndSend };
+};
diff --git a/frontend/apps/varatube/src/pages/subscription/Subscription.tsx b/frontend/apps/varatube/src/pages/subscription/Subscription.tsx
index 5cd7caf60..21ca3656d 100644
--- a/frontend/apps/varatube/src/pages/subscription/Subscription.tsx
+++ b/frontend/apps/varatube/src/pages/subscription/Subscription.tsx
@@ -1,43 +1,44 @@
-import { useAccount, useAlert, useApi, useBalanceFormat, withoutCommas } from '@gear-js/react-hooks';
+import { useAlert, useApi } from '@gear-js/react-hooks';
import { Button, checkboxStyles } from '@gear-js/ui';
import { useState } from 'react';
import { Heading, Loader, PurchaseSubscriptionModal } from 'components';
-import { useSubscriptionsMessage } from 'hooks';
-import { useHandleCalculateGas, useCheckBalance } from '@dapps-frontend/hooks';
-import varatubeMeta from 'assets/state/varatube_meta.txt';
import pic from 'assets/images/pic.png';
import clsx from 'clsx';
import { ADDRESS, periods } from 'consts';
import styles from './Subscription.module.scss';
import { PurchaseSubscriptionApproveModal } from 'components/modals/purchase-subscription-approve-modal';
-import { InitialValues } from 'types';
-import { useFTBalance, useFTMessage, useProgramState } from 'hooks/api';
-import { useProgramMetadata } from 'hooks/metadata';
+import { FormValues } from 'types';
+import {
+ useBalanceOfQuery,
+ useCancelSubscriptionMessage,
+ useGetSubscriberQuery,
+ useRegisterSubscriptionMessage,
+} from 'app/utils';
+import { useApproveMessage } from 'app/utils/sails/messages/use-approve-message';
+import { Period } from 'app/utils/sails/varatube';
+import { useCurrenciesQuery } from 'app/utils/sails/queries/use-currencies-query';
function Subscription() {
- const amount = 10000;
- const { account } = useAccount();
+ const { currencies } = useCurrenciesQuery();
+ const amount = currencies?.[0][1] ? Number(currencies[0][1]) : null;
+
const alert = useAlert();
const { api } = useApi();
- const tokens = useFTBalance();
- const { decodedAddress } = account || {};
- const [valuesToTransfer, setValuesToTransfer] = useState(null);
- const { subscriptionsState, isSubscriptionsStateRead, updateState } = useProgramState();
- const varatubeMetadata = useProgramMetadata(varatubeMeta);
- const calculateGas = useHandleCalculateGas(ADDRESS.CONTRACT, varatubeMetadata);
- const { checkBalance } = useCheckBalance();
- const subscription = subscriptionsState && decodedAddress ? subscriptionsState[decodedAddress] : undefined;
+ const { balance, refetch: refetchBalance } = useBalanceOfQuery();
+ const [valuesToTransfer, setValuesToTransfer] = useState(null);
+
+ const { subscriber, isFetching, refetch } = useGetSubscriberQuery();
+
const [isSubscribing, setIsSubscribing] = useState(false);
- const { period, price, willRenew, subscriptionStart, subscriptionEnd } = subscription || {};
- const [startDateTimestamp] = subscriptionStart || [];
- const [endDateTimestamp] = subscriptionEnd || [];
+ const { period, start_date, end_date, will_renew } = subscriber || {};
- const startDate = startDateTimestamp ? new Date(+withoutCommas(startDateTimestamp)).toLocaleString() : '';
- const endDate = endDateTimestamp ? new Date(+withoutCommas(endDateTimestamp)).toLocaleString() : '';
+ const startDate = start_date ? new Date(Number(start_date)).toLocaleString() : '';
+ const endDate = end_date ? new Date(Number(end_date)).toLocaleString() : '';
- const sendMessage = useSubscriptionsMessage();
- const sendFTMessage = useFTMessage();
+ const { registerSubscriptionMessage } = useRegisterSubscriptionMessage();
+ const { cancelSubscriptionMessage } = useCancelSubscriptionMessage();
+ const { approveMessage } = useApproveMessage();
const [isModalOpen, setIsModalOpen] = useState(false);
const [isApproveModalOpen, setIsApproveModalOpen] = useState(false);
@@ -49,29 +50,15 @@ function Subscription() {
const openApproveModal = () => setIsApproveModalOpen(true);
const cancelSubscription = () => {
- const payload = { CancelSubscription: null };
-
- calculateGas(payload)
- .then((res) => res.toHuman())
- .then(({ min_limit }) => {
- const minLimit = withoutCommas(min_limit as string);
- const gasLimit = Math.floor(Number(minLimit) + Number(minLimit) * 0.2);
-
- checkBalance(gasLimit, () => {
- sendMessage({
- payload,
- gasLimit,
- onSuccess: () => {
- updateState();
- alert.success('Unsubscribed successfully');
- },
- });
- });
- })
- .catch((error) => {
- console.log(error);
+ cancelSubscriptionMessage({
+ onSuccess: () => {
+ refetch();
+ alert.success('Unsubscribed successfully');
+ },
+ onError: () => {
alert.error('Gas calculation error');
- });
+ },
+ });
};
const clearValues = () => {
@@ -79,42 +66,31 @@ function Subscription() {
};
const findSelectedPeriodRate = (period: string) => periods.find((item) => item.value === period)?.rate || 1;
+ const price = period && amount ? String(findSelectedPeriodRate(period) * amount) : null;
const purchaseSubscription = () => {
if (valuesToTransfer) {
- const payload = {
- RegisterSubscription: {
+ registerSubscriptionMessage(
+ {
currency_id: ADDRESS.FT_CONTRACT,
- period: { [valuesToTransfer.period]: null },
+ period: valuesToTransfer.period as Period,
with_renewal: valuesToTransfer.isRenewal,
},
- };
-
- calculateGas(payload)
- .then((res) => res.toHuman())
- .then(({ min_limit }) => {
- const minLimit = withoutCommas(min_limit as string);
- const gasLimit = Math.floor(Number(minLimit) + Number(minLimit) * 0.2);
-
- checkBalance(gasLimit, () => {
- sendMessage({
- payload,
- gasLimit,
- onSuccess: () => {
- closeModal();
- clearValues();
- updateState();
- setIsSubscribing(false);
- alert.success('Subscribed successfully');
- },
- });
- });
- })
- .catch((error) => {
- console.log(error);
- alert.error('Gas calculation error');
- setIsSubscribing(false);
- });
+ {
+ onSuccess: () => {
+ closeModal();
+ clearValues();
+ setIsSubscribing(false);
+ alert.success('Subscribed successfully');
+ refetch();
+ refetchBalance();
+ },
+ onError: () => {
+ alert.error('Gas calculation error');
+ setIsSubscribing(false);
+ },
+ },
+ );
}
};
@@ -128,7 +104,7 @@ function Subscription() {
return;
}
- if (!tokens || Number(withoutCommas(tokens)) < findSelectedPeriodRate(valuesToTransfer?.period) * amount) {
+ if (!amount || !balance || Number(balance) < findSelectedPeriodRate(valuesToTransfer?.period) * amount) {
alert.error(`You don't have enough tokens to subscribe`);
clearValues();
closeApproveModal();
@@ -138,17 +114,16 @@ function Subscription() {
setIsSubscribing(true);
- checkBalance(api?.blockGasLimit.toNumber(), () => {
- const amountToTransfer = findSelectedPeriodRate(valuesToTransfer.period) * amount;
- if (amountToTransfer)
- sendFTMessage({
- payload: {
- Approve: {
- to: ADDRESS.CONTRACT,
- amount: String(findSelectedPeriodRate(valuesToTransfer.period) * amount),
- },
- },
+ const amountToTransfer = findSelectedPeriodRate(valuesToTransfer.period) * amount;
+ if (amountToTransfer) {
+ approveMessage(
+ {
+ spender: ADDRESS.CONTRACT,
+ value: amountToTransfer,
+ },
+ {
onSuccess: () => {
+ console.log('onSuccess');
closeApproveModal();
purchaseSubscription();
},
@@ -159,18 +134,19 @@ function Subscription() {
setIsSubscribing(false);
alert.error('Some error has occured');
},
- });
- });
+ },
+ );
+ }
};
return (
<>
- {isSubscriptionsStateRead ? (
+ {!isFetching && amount ? (
<>
- {subscription ? (
+ {subscriber ? (
<>
-
+ {will_renew &&
}
>
) : (
<>
@@ -234,7 +210,7 @@ function Subscription() {
onSubmit={saveSubscriptionValues}
/>
)}
- {isApproveModalOpen && valuesToTransfer && (
+ {isApproveModalOpen && valuesToTransfer && amount && (
+
+
+
+
+
+
+
+
+
+## Description
+
+React application of Web3 Warriors Battle based on [Rust smart-contract](https://github.com/gear-foundation/dapps/tree/master/contracts/battle).
+
+## Getting started
+
+### Install packages:
+
+```sh
+yarn install
+```
+
+### Declare environment variables:
+
+Create `.env` file, `.env.example` will let you know what variables are expected.
+
+In order for all features to work as expected, the node and it's runtime version should be chosen based on the current `@gear-js/api` version.
+
+In case of issues with the application, try to switch to another network or run your own local node and specify its address in the .env file. When applicable, make sure the smart contract(s) wasm files are uploaded and running in this network accordingly.
+
+### Run the app:
+
+```sh
+yarn dev
+```
diff --git a/frontend/apps/web3-warriors-battle/index.html b/frontend/apps/web3-warriors-battle/index.html
new file mode 100644
index 000000000..aa5428a02
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/index.html
@@ -0,0 +1,24 @@
+
+
+
+
+ Web3 Warriors Battle
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ You need to enable JavaScript to run this app.
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/package.json b/frontend/apps/web3-warriors-battle/package.json
new file mode 100644
index 000000000..ff83bb7ed
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/package.json
@@ -0,0 +1,74 @@
+{
+ "name": "web3-warriors-battle",
+ "private": true,
+ "version": "1.0.2",
+ "type": "module",
+ "scripts": {
+ "start": "yarn build:packages && vite --open",
+ "build": "yarn build:packages && tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@dapps-frontend/error-tracking": "workspace:*",
+ "@dapps-frontend/hooks": "workspace:*",
+ "@dapps-frontend/ui": "workspace:*",
+ "@gear-js/api": "0.38.1",
+ "@gear-js/react-hooks": "0.13.0",
+ "@mantine/form": "6.0.15",
+ "@polkadot/api": "11.0.2",
+ "@polkadot/types": "11.0.2",
+ "@polkadot/util": "12.3.2",
+ "@polkadot/util-crypto": "12.6.2",
+ "@radix-ui/react-dialog": "1.0.4",
+ "@radix-ui/react-scroll-area": "1.0.4",
+ "@tanstack/react-query": "5.29.0",
+ "@types/node": "18.16.19",
+ "@types/react": "18.2.33",
+ "@types/react-dom": "18.2.14",
+ "assert": "2.0.0",
+ "buffer": "6.0.3",
+ "class-variance-authority": "0.6.1",
+ "clsx": "1.2.1",
+ "framer-motion": "10.16.2",
+ "jotai": "2.2.1",
+ "react": "18.2.0",
+ "react-countdown": "2.3.5",
+ "react-dom": "18.2.0",
+ "react-router-dom": "6.10.0",
+ "react-transition-group": "4.4.5",
+ "sails-js": "0.1.8",
+ "sass": "1.62.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react-swc": "3.3.2",
+ "autoprefixer": "10.4.15",
+ "eslint": "8.48.0",
+ "eslint-config-react-app": "7.0.1",
+ "postcss": "8.4.29",
+ "prettier": "3.0.3",
+ "rollup-plugin-visualizer": "5.9.2",
+ "tailwindcss": "3.3.3",
+ "typescript": "4.9.5",
+ "vite": "4.4.9",
+ "vite-plugin-eslint": "1.8.1",
+ "vite-plugin-node-polyfills": "0.17.0",
+ "vite-plugin-svgr": "3.2.0"
+ },
+ "browserslist": {
+ "production": [
+ "chrome >= 67",
+ "edge >= 79",
+ "firefox >= 68",
+ "opera >= 54",
+ "safari >= 14"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "lint-staged": {
+ "./**/*.{js,css,ts,tsx}": "eslint --fix"
+ }
+}
diff --git a/frontend/apps/web3-warriors-battle/postcss.config.js b/frontend/apps/web3-warriors-battle/postcss.config.js
new file mode 100644
index 000000000..2aa7205d4
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/frontend/apps/web3-warriors-battle/public/favicons/android-chrome-192x192.png b/frontend/apps/web3-warriors-battle/public/favicons/android-chrome-192x192.png
new file mode 100644
index 000000000..35aae1e5a
Binary files /dev/null and b/frontend/apps/web3-warriors-battle/public/favicons/android-chrome-192x192.png differ
diff --git a/frontend/apps/web3-warriors-battle/public/favicons/android-chrome-512x512.png b/frontend/apps/web3-warriors-battle/public/favicons/android-chrome-512x512.png
new file mode 100644
index 000000000..51dc2932c
Binary files /dev/null and b/frontend/apps/web3-warriors-battle/public/favicons/android-chrome-512x512.png differ
diff --git a/frontend/apps/web3-warriors-battle/public/favicons/apple-touch-icon.png b/frontend/apps/web3-warriors-battle/public/favicons/apple-touch-icon.png
new file mode 100644
index 000000000..0dc8e417f
Binary files /dev/null and b/frontend/apps/web3-warriors-battle/public/favicons/apple-touch-icon.png differ
diff --git a/frontend/apps/web3-warriors-battle/public/favicons/browserconfig.xml b/frontend/apps/web3-warriors-battle/public/favicons/browserconfig.xml
new file mode 100644
index 000000000..5aecc916b
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/public/favicons/browserconfig.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+ #00aba9
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/public/favicons/favicon-16x16.png b/frontend/apps/web3-warriors-battle/public/favicons/favicon-16x16.png
new file mode 100644
index 000000000..5189aa279
Binary files /dev/null and b/frontend/apps/web3-warriors-battle/public/favicons/favicon-16x16.png differ
diff --git a/frontend/apps/web3-warriors-battle/public/favicons/favicon-32x32.png b/frontend/apps/web3-warriors-battle/public/favicons/favicon-32x32.png
new file mode 100644
index 000000000..883e02ba8
Binary files /dev/null and b/frontend/apps/web3-warriors-battle/public/favicons/favicon-32x32.png differ
diff --git a/frontend/apps/web3-warriors-battle/public/favicons/favicon.ico b/frontend/apps/web3-warriors-battle/public/favicons/favicon.ico
new file mode 100644
index 000000000..663c05fc6
Binary files /dev/null and b/frontend/apps/web3-warriors-battle/public/favicons/favicon.ico differ
diff --git a/frontend/apps/web3-warriors-battle/public/favicons/mstile-144x144.png b/frontend/apps/web3-warriors-battle/public/favicons/mstile-144x144.png
new file mode 100644
index 000000000..cc0de5f49
Binary files /dev/null and b/frontend/apps/web3-warriors-battle/public/favicons/mstile-144x144.png differ
diff --git a/frontend/apps/web3-warriors-battle/public/favicons/mstile-150x150.png b/frontend/apps/web3-warriors-battle/public/favicons/mstile-150x150.png
new file mode 100644
index 000000000..fe8440e95
Binary files /dev/null and b/frontend/apps/web3-warriors-battle/public/favicons/mstile-150x150.png differ
diff --git a/frontend/apps/web3-warriors-battle/public/favicons/mstile-310x150.png b/frontend/apps/web3-warriors-battle/public/favicons/mstile-310x150.png
new file mode 100644
index 000000000..82a7676b5
Binary files /dev/null and b/frontend/apps/web3-warriors-battle/public/favicons/mstile-310x150.png differ
diff --git a/frontend/apps/web3-warriors-battle/public/favicons/mstile-310x310.png b/frontend/apps/web3-warriors-battle/public/favicons/mstile-310x310.png
new file mode 100644
index 000000000..20b2084c1
Binary files /dev/null and b/frontend/apps/web3-warriors-battle/public/favicons/mstile-310x310.png differ
diff --git a/frontend/apps/web3-warriors-battle/public/favicons/mstile-70x70.png b/frontend/apps/web3-warriors-battle/public/favicons/mstile-70x70.png
new file mode 100644
index 000000000..8fc7f1435
Binary files /dev/null and b/frontend/apps/web3-warriors-battle/public/favicons/mstile-70x70.png differ
diff --git a/frontend/apps/web3-warriors-battle/public/favicons/safari-pinned-tab.svg b/frontend/apps/web3-warriors-battle/public/favicons/safari-pinned-tab.svg
new file mode 100644
index 000000000..ae140daef
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/public/favicons/safari-pinned-tab.svg
@@ -0,0 +1,18 @@
+
+
+
+
+Created by potrace 1.14, written by Peter Selinger 2001-2017
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/public/favicons/site.webmanifest b/frontend/apps/web3-warriors-battle/public/favicons/site.webmanifest
new file mode 100644
index 000000000..ff1c8aa06
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/public/favicons/site.webmanifest
@@ -0,0 +1,19 @@
+{
+ "name": "Web3 Warriors Battle",
+ "short_name": "Web3 Warriors Battle",
+ "icons": [
+ {
+ "src": "/favicons/android-chrome-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/favicons/android-chrome-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ],
+ "theme_color": "#ffffff",
+ "background_color": "#ffffff",
+ "display": "standalone"
+}
\ No newline at end of file
diff --git a/frontend/apps/web3-warriors-battle/public/fonts/Anuphan-Bold.woff2 b/frontend/apps/web3-warriors-battle/public/fonts/Anuphan-Bold.woff2
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/apps/web3-warriors-battle/public/fonts/Anuphan-ExtraLight.woff2 b/frontend/apps/web3-warriors-battle/public/fonts/Anuphan-ExtraLight.woff2
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/apps/web3-warriors-battle/public/fonts/Anuphan-Light.woff2 b/frontend/apps/web3-warriors-battle/public/fonts/Anuphan-Light.woff2
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/apps/web3-warriors-battle/public/fonts/Anuphan-Medium.woff2 b/frontend/apps/web3-warriors-battle/public/fonts/Anuphan-Medium.woff2
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/apps/web3-warriors-battle/public/fonts/Anuphan-Regular.woff2 b/frontend/apps/web3-warriors-battle/public/fonts/Anuphan-Regular.woff2
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/apps/web3-warriors-battle/public/fonts/Anuphan-SemiBold.woff2 b/frontend/apps/web3-warriors-battle/public/fonts/Anuphan-SemiBold.woff2
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/apps/web3-warriors-battle/public/fonts/Anuphan-Thin.woff2 b/frontend/apps/web3-warriors-battle/public/fonts/Anuphan-Thin.woff2
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/apps/web3-warriors-battle/public/fonts/anuphan-variable.woff2 b/frontend/apps/web3-warriors-battle/public/fonts/anuphan-variable.woff2
new file mode 100644
index 000000000..9a02514d4
Binary files /dev/null and b/frontend/apps/web3-warriors-battle/public/fonts/anuphan-variable.woff2 differ
diff --git a/frontend/apps/web3-warriors-battle/public/sprites/icons.svg b/frontend/apps/web3-warriors-battle/public/sprites/icons.svg
new file mode 100644
index 000000000..52935f368
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/public/sprites/icons.svg
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/app.scss b/frontend/apps/web3-warriors-battle/src/app.scss
new file mode 100644
index 000000000..56d29507e
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app.scss
@@ -0,0 +1,2 @@
+@import '@/assets/styles';
+@import 'global.css';
diff --git a/frontend/apps/web3-warriors-battle/src/app.tsx b/frontend/apps/web3-warriors-battle/src/app.tsx
new file mode 100644
index 000000000..b7d9fb035
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app.tsx
@@ -0,0 +1,30 @@
+import './app.scss';
+import { withProviders } from '@/app/hocs';
+import { Loader, LoadingError, MainLayout } from '@/components';
+import '@gear-js/vara-ui/dist/style.css';
+import { Routing } from '@/pages';
+import { useMyBattleQuery, useProgram } from './app/utils';
+
+function Component() {
+ const program = useProgram();
+
+ const { error } = useMyBattleQuery();
+ const isGameReady = !!program;
+
+ return (
+
+ {!!error && (
+
+ Error in the Game contract :(
+
+ Error message: {error.message}
+
+
+ )}
+ {!error && isGameReady && }
+ {!error && !isGameReady && }
+
+ );
+}
+
+export const App = withProviders(Component);
diff --git a/frontend/apps/web3-warriors-battle/src/app/consts.ts b/frontend/apps/web3-warriors-battle/src/app/consts.ts
new file mode 100644
index 000000000..1b7188eb6
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/consts.ts
@@ -0,0 +1,24 @@
+export const ACCOUNT_ID_LOCAL_STORAGE_KEY = 'account';
+
+export const ADDRESS = {
+ NODE: import.meta.env.VITE_NODE_ADDRESS,
+ DNS_API_URL: import.meta.env.VITE_DNS_API_URL,
+ DNS_NAME: import.meta.env.VITE_DNS_NAME,
+ SENTRY_DSN: import.meta.env.VITE_SENTRY_DSN_TTT,
+};
+
+export const MAX_PLAYERS_COUNT = 50;
+
+export const ROUTES = {
+ HOME: '/',
+ IMPORT_CHARACTER: '/import-character',
+ GENERATE_CHARACTER: '/generate-character',
+ CREATE_GAME: '/create',
+ FIND_GAME: '/find',
+ WAITING: '/waiting',
+
+ GAME: '/game',
+ ONBOARDING: '/onboarding',
+
+ NOTFOUND: '*',
+};
diff --git a/frontend/apps/web3-warriors-battle/src/app/hocs/index.tsx b/frontend/apps/web3-warriors-battle/src/app/hocs/index.tsx
new file mode 100644
index 000000000..6470b246c
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/hocs/index.tsx
@@ -0,0 +1,46 @@
+import {
+ ApiProvider as GearApiProvider,
+ AlertProvider as GearAlertProvider,
+ AccountProvider as GearAccountProvider,
+ ProviderProps,
+} from '@gear-js/react-hooks';
+import { ComponentType } from 'react';
+import { BrowserRouter } from 'react-router-dom';
+
+import { DnsProvider as SharedDnsProvider } from '@dapps-frontend/hooks';
+
+import { ADDRESS } from '@/app/consts';
+import { Alert, alertStyles } from '@/components/ui/alert';
+import { QueryProvider } from './query-provider';
+
+function ApiProvider({ children }: ProviderProps) {
+ return {children} ;
+}
+
+function AccountProvider({ children }: ProviderProps) {
+ return {children} ;
+}
+
+function AlertProvider({ children }: ProviderProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+function DnsProvider({ children }: ProviderProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+const providers = [BrowserRouter, ApiProvider, AccountProvider, AlertProvider, DnsProvider, QueryProvider];
+
+function withProviders(Component: ComponentType) {
+ return () => providers.reduceRight((children, Provider) => {children} , );
+}
+
+export { withProviders };
diff --git a/frontend/apps/web3-warriors-battle/src/app/hocs/query-provider.tsx b/frontend/apps/web3-warriors-battle/src/app/hocs/query-provider.tsx
new file mode 100644
index 000000000..82ae550f8
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/hocs/query-provider.tsx
@@ -0,0 +1,23 @@
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { ReactNode } from 'react';
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ gcTime: 0,
+ staleTime: Infinity,
+ refetchOnWindowFocus: false,
+ retry: false,
+ },
+ },
+});
+
+type Props = {
+ children: ReactNode;
+};
+
+const QueryProvider = ({ children }: Props) => (
+ {children}
+);
+
+export { QueryProvider };
diff --git a/frontend/apps/web3-warriors-battle/src/app/hooks/use-is-app-ready.ts b/frontend/apps/web3-warriors-battle/src/app/hooks/use-is-app-ready.ts
new file mode 100644
index 000000000..060094a6e
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/hooks/use-is-app-ready.ts
@@ -0,0 +1,29 @@
+import { atom, useAtomValue, useSetAtom } from 'jotai';
+import { useAccount, useApi } from '@gear-js/react-hooks';
+import { useAccountAvailableBalance, useAccountAvailableBalanceSync } from '@/features/account-available-balance/hooks';
+import { useEffect } from 'react';
+import { useAuth } from '@/features/auth';
+
+const isAppReadyAtom = atom(false);
+
+export function useIsAppReady() {
+ const isAppReady = useAtomValue(isAppReadyAtom);
+ const setIsAppReady = useSetAtom(isAppReadyAtom);
+
+ return { isAppReady, setIsAppReady };
+}
+
+export function useIsAppReadySync() {
+ const { isApiReady } = useApi();
+ const { isAccountReady } = useAccount();
+ const { isAvailableBalanceReady } = useAccountAvailableBalance();
+ const { isAuthReady } = useAuth();
+
+ const { setIsAppReady } = useIsAppReady();
+
+ useAccountAvailableBalanceSync();
+ useEffect(() => {
+ setIsAppReady(isApiReady && isAccountReady && isAvailableBalanceReady && isAuthReady);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isAccountReady, isApiReady, isAvailableBalanceReady, isAuthReady]);
+}
diff --git a/frontend/apps/web3-warriors-battle/src/app/hooks/use-sign-and-send.ts b/frontend/apps/web3-warriors-battle/src/app/hooks/use-sign-and-send.ts
new file mode 100644
index 000000000..884998ad2
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/hooks/use-sign-and-send.ts
@@ -0,0 +1,44 @@
+import { usePending } from '@/features/game/hooks';
+import { useCheckBalance } from '@dapps-frontend/hooks';
+import { useAlert } from '@gear-js/react-hooks';
+import { GenericTransactionReturn, TransactionReturn } from '@gear-js/react-hooks/dist/esm/hooks/sails/types';
+
+export type Options = {
+ onSuccess?: () => void;
+ onError?: () => void;
+};
+
+export const useSignAndSend = () => {
+ const { checkBalance } = useCheckBalance();
+ const { setPending } = usePending();
+ const alert = useAlert();
+
+ const signAndSend = async (
+ transaction: TransactionReturn<() => GenericTransactionReturn>,
+ options?: Options,
+ ) => {
+ const { onSuccess, onError } = options || {};
+ const calculatedGas = Number(transaction.extrinsic.args[2].toString());
+ checkBalance(
+ calculatedGas,
+ async () => {
+ try {
+ const { response } = await transaction.signAndSend();
+ await response();
+ onSuccess?.();
+ setPending(false);
+ } catch (e) {
+ onError?.();
+ setPending(false);
+ console.error(e);
+ if (typeof e === 'string') {
+ alert.error(e);
+ }
+ }
+ },
+ onError,
+ );
+ };
+
+ return { signAndSend };
+};
diff --git a/frontend/apps/web3-warriors-battle/src/app/types.ts b/frontend/apps/web3-warriors-battle/src/app/types.ts
new file mode 100644
index 000000000..0c3956f2f
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/types.ts
@@ -0,0 +1,33 @@
+import { FC, PropsWithChildren, SVGProps } from 'react';
+
+export type BaseComponentProps = PropsWithChildren & {
+ className?: string;
+};
+
+export type PickPartial = T | Pick;
+
+// in case Object.entries return value is immutable
+// ref: https://stackoverflow.com/a/60142095
+export type Entries = {
+ [K in keyof T]: [K, T[K]];
+}[keyof T][];
+
+export type SVGComponent = FC<
+ SVGProps & {
+ title?: string | undefined;
+ }
+>;
+
+export type ArrayElement = ArrayType extends readonly (infer ElementType)[]
+ ? ElementType
+ : never;
+
+export type ContractError = {
+ message?: string;
+};
+
+declare global {
+ interface Window {
+ walletExtension?: { isNovaWallet: boolean };
+ }
+}
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/index.ts b/frontend/apps/web3-warriors-battle/src/app/utils/index.ts
new file mode 100644
index 000000000..45bb01ca6
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/index.ts
@@ -0,0 +1,49 @@
+import { AlertContainerFactory } from '@gear-js/react-hooks';
+
+export * from './sails';
+
+export const copyToClipboard = async ({
+ alert,
+ value,
+ successfulText,
+}: {
+ alert?: AlertContainerFactory;
+ value: string;
+ successfulText?: string;
+}) => {
+ const onSuccess = () => {
+ if (alert) {
+ alert.success(successfulText || 'Copied');
+ }
+ };
+ const onError = () => {
+ if (alert) {
+ alert.error('Copy error');
+ }
+ };
+
+ function unsecuredCopyToClipboard(text: string) {
+ const textArea = document.createElement('textarea');
+ textArea.value = text;
+ document.body.appendChild(textArea);
+ textArea.focus();
+ textArea.select();
+ try {
+ document.execCommand('copy');
+ onSuccess();
+ } catch (err) {
+ console.error('Unable to copy to clipboard', err);
+ onError();
+ }
+ document.body.removeChild(textArea);
+ }
+
+ if (window.isSecureContext && navigator.clipboard) {
+ navigator.clipboard
+ .writeText(value)
+ .then(() => onSuccess())
+ .catch(() => onError());
+ } else {
+ unsecuredCopyToClipboard(value);
+ }
+};
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/sails/battle.idl b/frontend/apps/web3-warriors-battle/src/app/utils/sails/battle.idl
new file mode 100644
index 000000000..f8bea9070
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/sails/battle.idl
@@ -0,0 +1,126 @@
+type Config = struct {
+ health: u16,
+ max_participants: u8,
+ attack_range: struct { u16, u16 },
+ defence_range: struct { u16, u16 },
+ dodge_range: struct { u16, u16 },
+ available_points: u16,
+ time_for_move_in_blocks: u32,
+ block_duration_ms: u32,
+ gas_for_create_warrior: u64,
+ gas_to_cancel_the_battle: u64,
+ time_to_cancel_the_battle: u32,
+ reservation_amount: u64,
+ reservation_time: u32,
+};
+
+type Appearance = struct {
+ head_index: u16,
+ hat_index: u16,
+ body_index: u16,
+ accessory_index: u16,
+ body_color: str,
+ back_color: str,
+};
+
+type Move = enum {
+ Attack,
+ Reflect,
+ Ultimate,
+};
+
+type BattleState = struct {
+ admin: actor_id,
+ battle_name: str,
+ time_creation: u64,
+ bid: u128,
+ participants: vec struct { actor_id, Player },
+ defeated_participants: vec struct { actor_id, Player },
+ state: State,
+ pairs: vec struct { u16, Pair },
+ players_to_pairs: vec struct { actor_id, u16 },
+ waiting_player: opt struct { actor_id, u16 },
+ pair_id: u16,
+ reservation: vec struct { actor_id, ReservationId },
+};
+
+type Player = struct {
+ warrior_id: opt actor_id,
+ owner: actor_id,
+ user_name: str,
+ player_settings: PlayerSettings,
+ appearance: Appearance,
+ number_of_victories: u8,
+ ultimate_reload: u8,
+ reflect_reload: u8,
+};
+
+type PlayerSettings = struct {
+ health: u16,
+ attack: u16,
+ defence: u16,
+ dodge: u16,
+};
+
+type State = enum {
+ Registration,
+ Started,
+ GameIsOver: struct { winners: struct { actor_id, opt actor_id } },
+};
+
+type Pair = struct {
+ player_1: actor_id,
+ player_2: actor_id,
+ action: opt struct { actor_id, Move },
+ round: u8,
+ round_start_time: u64,
+};
+
+type ReservationId = struct {
+ [u8, 32],
+};
+
+constructor {
+ New : (config: Config);
+};
+
+service Battle {
+ AddAdmin : (new_admin: actor_id) -> null;
+ AutomaticMove : (player_id: actor_id, number_of_victories: u8, round: u8) -> null;
+ CancelRegister : () -> null;
+ CancelTournament : () -> null;
+ ChangeConfig : (config: Config) -> null;
+ CreateNewBattle : (battle_name: str, user_name: str, warrior_id: opt actor_id, appearance: opt Appearance, attack: u16, defence: u16, dodge: u16) -> null;
+ DelayedCancelTournament : (game_id: actor_id, time_creation: u64) -> null;
+ DeletePlayer : (player_id: actor_id) -> null;
+ ExitGame : () -> null;
+ MakeMove : (warrior_move: Move) -> null;
+ Register : (game_id: actor_id, warrior_id: opt actor_id, appearance: opt Appearance, user_name: str, attack: u16, defence: u16, dodge: u16) -> null;
+ StartBattle : () -> null;
+ StartNextFight : () -> null;
+ query Admins : () -> vec actor_id;
+ query Config : () -> Config;
+ query GetBattle : (game_id: actor_id) -> opt BattleState;
+ query GetMyBattle : () -> opt BattleState;
+
+ events {
+ NewBattleCreated: struct { battle_id: actor_id, bid: u128 };
+ PlayerRegistered: struct { admin_id: actor_id, user_name: str, bid: u128 };
+ RegisterCanceled: struct { player_id: actor_id };
+ BattleCanceled: struct { game_id: actor_id };
+ BattleStarted;
+ MoveMade;
+ BattleFinished: struct { winner: actor_id };
+ PairChecked: struct { game_id: actor_id, pair_id: u8, round: u8 };
+ FirstRoundChecked: struct { game_id: actor_id, wave: u8 };
+ NextBattleStarted;
+ EnemyWaiting;
+ WarriorGenerated: struct { address: actor_id };
+ AdminAdded: struct { new_admin: actor_id };
+ ConfigChanged: struct { config: Config };
+ GameLeft;
+ RoundAction: struct { round: u8, player_1: struct { actor_id, Move, u16 }, player_2: struct { actor_id, Move, u16 } };
+ AutomaticMoveMade;
+ }
+};
+
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/sails/battle.ts b/frontend/apps/web3-warriors-battle/src/app/utils/sails/battle.ts
new file mode 100644
index 000000000..0e8e2beb5
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/sails/battle.ts
@@ -0,0 +1,755 @@
+import { TransactionBuilder, getServiceNamePrefix, getFnNamePrefix, ZERO_ADDRESS } from 'sails-js';
+import { GearApi, decodeAddress } from '@gear-js/api';
+import { TypeRegistry } from '@polkadot/types';
+
+type ActorId = string;
+
+export interface Config {
+ health: number;
+ max_participants: number;
+ attack_range: [number, number];
+ defence_range: [number, number];
+ dodge_range: [number, number];
+ available_points: number;
+ time_for_move_in_blocks: number;
+ block_duration_ms: number;
+ gas_for_create_warrior: number | string | bigint;
+ gas_to_cancel_the_battle: number | string | bigint;
+ time_to_cancel_the_battle: number;
+ reservation_amount: number | string | bigint;
+ reservation_time: number;
+}
+
+export interface Appearance {
+ head_index: number;
+ hat_index: number;
+ body_index: number;
+ accessory_index: number;
+ body_color: string;
+ back_color: string;
+}
+
+export type Move = 'Attack' | 'Reflect' | 'Ultimate';
+
+export interface BattleState {
+ admin: ActorId;
+ battle_name: string;
+ time_creation: number | string | bigint;
+ bid: number | string | bigint;
+ participants: Array<[ActorId, Player]>;
+ defeated_participants: Array<[ActorId, Player]>;
+ state: State;
+ pairs: Array<[number, Pair]>;
+ players_to_pairs: Array<[ActorId, number]>;
+ waiting_player: [ActorId, number] | null;
+ pair_id: number;
+ reservation: Array<[ActorId, ReservationId]>;
+}
+
+export interface Player {
+ warrior_id: ActorId | null;
+ owner: ActorId;
+ user_name: string;
+ player_settings: PlayerSettings;
+ appearance: Appearance;
+ number_of_victories: number;
+ ultimate_reload: number;
+ reflect_reload: number;
+}
+
+export interface PlayerSettings {
+ health: number;
+ attack: number;
+ defence: number;
+ dodge: number;
+}
+
+export type State = { registration: null } | { started: null } | { gameIsOver: { winners: [ActorId, ActorId | null] } };
+
+export interface Pair {
+ player_1: ActorId;
+ player_2: ActorId;
+ action: [ActorId, Move] | null;
+ round: number;
+ round_start_time: number | string | bigint;
+}
+
+export type ReservationId = [Array];
+
+export class Program {
+ public readonly registry: TypeRegistry;
+ public readonly battle: Battle;
+
+ constructor(
+ public api: GearApi,
+ public programId?: `0x${string}`,
+ ) {
+ const types: Record = {
+ Config: {
+ health: 'u16',
+ max_participants: 'u8',
+ attack_range: '(u16, u16)',
+ defence_range: '(u16, u16)',
+ dodge_range: '(u16, u16)',
+ available_points: 'u16',
+ time_for_move_in_blocks: 'u32',
+ block_duration_ms: 'u32',
+ gas_for_create_warrior: 'u64',
+ gas_to_cancel_the_battle: 'u64',
+ time_to_cancel_the_battle: 'u32',
+ reservation_amount: 'u64',
+ reservation_time: 'u32',
+ },
+ Appearance: {
+ head_index: 'u16',
+ hat_index: 'u16',
+ body_index: 'u16',
+ accessory_index: 'u16',
+ body_color: 'String',
+ back_color: 'String',
+ },
+ Move: { _enum: ['Attack', 'Reflect', 'Ultimate'] },
+ BattleState: {
+ admin: '[u8;32]',
+ battle_name: 'String',
+ time_creation: 'u64',
+ bid: 'u128',
+ participants: 'Vec<([u8;32], Player)>',
+ defeated_participants: 'Vec<([u8;32], Player)>',
+ state: 'State',
+ pairs: 'Vec<(u16, Pair)>',
+ players_to_pairs: 'Vec<([u8;32], u16)>',
+ waiting_player: 'Option<([u8;32], u16)>',
+ pair_id: 'u16',
+ reservation: 'Vec<([u8;32], ReservationId)>',
+ },
+ Player: {
+ warrior_id: 'Option<[u8;32]>',
+ owner: '[u8;32]',
+ user_name: 'String',
+ player_settings: 'PlayerSettings',
+ appearance: 'Appearance',
+ number_of_victories: 'u8',
+ ultimate_reload: 'u8',
+ reflect_reload: 'u8',
+ },
+ PlayerSettings: { health: 'u16', attack: 'u16', defence: 'u16', dodge: 'u16' },
+ State: {
+ _enum: { Registration: 'Null', Started: 'Null', GameIsOver: { winners: '([u8;32], Option<[u8;32]>)' } },
+ },
+ Pair: {
+ player_1: '[u8;32]',
+ player_2: '[u8;32]',
+ action: 'Option<([u8;32], Move)>',
+ round: 'u8',
+ round_start_time: 'u64',
+ },
+ ReservationId: '([u8; 32])',
+ };
+
+ this.registry = new TypeRegistry();
+ this.registry.setKnownTypes({ types });
+ this.registry.register(types);
+
+ this.battle = new Battle(this);
+ }
+
+ newCtorFromCode(code: Uint8Array | Buffer, config: Config): TransactionBuilder {
+ const builder = new TransactionBuilder(
+ this.api,
+ this.registry,
+ 'upload_program',
+ ['New', config],
+ '(String, Config)',
+ 'String',
+ code,
+ );
+
+ this.programId = builder.programId;
+ return builder;
+ }
+
+ newCtorFromCodeId(codeId: `0x${string}`, config: Config) {
+ const builder = new TransactionBuilder(
+ this.api,
+ this.registry,
+ 'create_program',
+ ['New', config],
+ '(String, Config)',
+ 'String',
+ codeId,
+ );
+
+ this.programId = builder.programId;
+ return builder;
+ }
+}
+
+export class Battle {
+ constructor(private _program: Program) {}
+
+ public addAdmin(new_admin: ActorId): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Battle', 'AddAdmin', new_admin],
+ '(String, String, [u8;32])',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public automaticMove(player_id: ActorId, number_of_victories: number, round: number): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Battle', 'AutomaticMove', player_id, number_of_victories, round],
+ '(String, String, [u8;32], u8, u8)',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public cancelRegister(): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Battle', 'CancelRegister'],
+ '(String, String)',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public cancelTournament(): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Battle', 'CancelTournament'],
+ '(String, String)',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public changeConfig(config: Config): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Battle', 'ChangeConfig', config],
+ '(String, String, Config)',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public createNewBattle(
+ battle_name: string,
+ user_name: string,
+ warrior_id: ActorId | null,
+ appearance: Appearance | null,
+ attack: number,
+ defence: number,
+ dodge: number,
+ ): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Battle', 'CreateNewBattle', battle_name, user_name, warrior_id, appearance, attack, defence, dodge],
+ '(String, String, String, String, Option<[u8;32]>, Option, u16, u16, u16)',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public delayedCancelTournament(game_id: ActorId, time_creation: number | string | bigint): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Battle', 'DelayedCancelTournament', game_id, time_creation],
+ '(String, String, [u8;32], u64)',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public deletePlayer(player_id: ActorId): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Battle', 'DeletePlayer', player_id],
+ '(String, String, [u8;32])',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public exitGame(): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Battle', 'ExitGame'],
+ '(String, String)',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public makeMove(warrior_move: Move): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Battle', 'MakeMove', warrior_move],
+ '(String, String, Move)',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public register(
+ game_id: ActorId,
+ warrior_id: ActorId | null,
+ appearance: Appearance | null,
+ user_name: string,
+ attack: number,
+ defence: number,
+ dodge: number,
+ ): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Battle', 'Register', game_id, warrior_id, appearance, user_name, attack, defence, dodge],
+ '(String, String, [u8;32], Option<[u8;32]>, Option, String, u16, u16, u16)',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public startBattle(): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Battle', 'StartBattle'],
+ '(String, String)',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public startNextFight(): TransactionBuilder {
+ if (!this._program.programId) throw new Error('Program ID is not set');
+ return new TransactionBuilder(
+ this._program.api,
+ this._program.registry,
+ 'send_message',
+ ['Battle', 'StartNextFight'],
+ '(String, String)',
+ 'Null',
+ this._program.programId,
+ );
+ }
+
+ public async admins(
+ originAddress?: string,
+ value?: number | string | bigint,
+ atBlock?: `0x${string}`,
+ ): Promise> {
+ const payload = this._program.registry.createType('(String, String)', ['Battle', 'Admins']).toHex();
+ const reply = await this._program.api.message.calculateReply({
+ destination: this._program.programId!,
+ origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS,
+ payload,
+ value: value || 0,
+ gasLimit: this._program.api.blockGasLimit.toBigInt(),
+ at: atBlock,
+ });
+ if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString());
+ const result = this._program.registry.createType('(String, String, Vec<[u8;32]>)', reply.payload);
+ return result[2].toJSON() as unknown as Array;
+ }
+
+ public async config(
+ originAddress?: string,
+ value?: number | string | bigint,
+ atBlock?: `0x${string}`,
+ ): Promise {
+ const payload = this._program.registry.createType('(String, String)', ['Battle', 'Config']).toHex();
+ const reply = await this._program.api.message.calculateReply({
+ destination: this._program.programId!,
+ origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS,
+ payload,
+ value: value || 0,
+ gasLimit: this._program.api.blockGasLimit.toBigInt(),
+ at: atBlock,
+ });
+ if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString());
+ const result = this._program.registry.createType('(String, String, Config)', reply.payload);
+ return result[2].toJSON() as unknown as Config;
+ }
+
+ public async getBattle(
+ game_id: ActorId,
+ originAddress?: string,
+ value?: number | string | bigint,
+ atBlock?: `0x${string}`,
+ ): Promise {
+ const payload = this._program.registry
+ .createType('(String, String, [u8;32])', ['Battle', 'GetBattle', game_id])
+ .toHex();
+ const reply = await this._program.api.message.calculateReply({
+ destination: this._program.programId!,
+ origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS,
+ payload,
+ value: value || 0,
+ gasLimit: this._program.api.blockGasLimit.toBigInt(),
+ at: atBlock,
+ });
+ if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString());
+ const result = this._program.registry.createType('(String, String, Option)', reply.payload);
+ return result[2].toJSON() as unknown as BattleState | null;
+ }
+
+ public async getMyBattle(
+ originAddress?: string,
+ value?: number | string | bigint,
+ atBlock?: `0x${string}`,
+ ): Promise {
+ const payload = this._program.registry.createType('(String, String)', ['Battle', 'GetMyBattle']).toHex();
+ const reply = await this._program.api.message.calculateReply({
+ destination: this._program.programId!,
+ origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS,
+ payload,
+ value: value || 0,
+ gasLimit: this._program.api.blockGasLimit.toBigInt(),
+ at: atBlock,
+ });
+ if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString());
+ const result = this._program.registry.createType('(String, String, Option)', reply.payload);
+ return result[2].toJSON() as unknown as BattleState | null;
+ }
+
+ public subscribeToNewBattleCreatedEvent(
+ callback: (data: { battle_id: ActorId; bid: number | string | bigint }) => void | Promise,
+ ): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Battle' && getFnNamePrefix(payload) === 'NewBattleCreated') {
+ callback(
+ this._program.registry
+ .createType('(String, String, {"battle_id":"[u8;32]","bid":"u128"})', message.payload)[2]
+ .toJSON() as unknown as { battle_id: ActorId; bid: number | string | bigint },
+ );
+ }
+ });
+ }
+
+ public subscribeToPlayerRegisteredEvent(
+ callback: (data: { admin_id: ActorId; user_name: string; bid: number | string | bigint }) => void | Promise,
+ ): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Battle' && getFnNamePrefix(payload) === 'PlayerRegistered') {
+ callback(
+ this._program.registry
+ .createType(
+ '(String, String, {"admin_id":"[u8;32]","user_name":"String","bid":"u128"})',
+ message.payload,
+ )[2]
+ .toJSON() as unknown as { admin_id: ActorId; user_name: string; bid: number | string | bigint },
+ );
+ }
+ });
+ }
+
+ public subscribeToRegisterCanceledEvent(
+ callback: (data: { player_id: ActorId }) => void | Promise,
+ ): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Battle' && getFnNamePrefix(payload) === 'RegisterCanceled') {
+ callback(
+ this._program.registry
+ .createType('(String, String, {"player_id":"[u8;32]"})', message.payload)[2]
+ .toJSON() as unknown as { player_id: ActorId },
+ );
+ }
+ });
+ }
+
+ public subscribeToBattleCanceledEvent(
+ callback: (data: { game_id: ActorId }) => void | Promise,
+ ): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Battle' && getFnNamePrefix(payload) === 'BattleCanceled') {
+ callback(
+ this._program.registry
+ .createType('(String, String, {"game_id":"[u8;32]"})', message.payload)[2]
+ .toJSON() as unknown as { game_id: ActorId },
+ );
+ }
+ });
+ }
+
+ public subscribeToBattleStartedEvent(callback: (data: null) => void | Promise): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Battle' && getFnNamePrefix(payload) === 'BattleStarted') {
+ callback(null);
+ }
+ });
+ }
+
+ public subscribeToMoveMadeEvent(callback: (data: null) => void | Promise): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Battle' && getFnNamePrefix(payload) === 'MoveMade') {
+ callback(null);
+ }
+ });
+ }
+
+ public subscribeToBattleFinishedEvent(
+ callback: (data: { winner: ActorId }) => void | Promise,
+ ): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Battle' && getFnNamePrefix(payload) === 'BattleFinished') {
+ callback(
+ this._program.registry
+ .createType('(String, String, {"winner":"[u8;32]"})', message.payload)[2]
+ .toJSON() as unknown as { winner: ActorId },
+ );
+ }
+ });
+ }
+
+ public subscribeToPairCheckedEvent(
+ callback: (data: { game_id: ActorId; pair_id: number; round: number }) => void | Promise,
+ ): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Battle' && getFnNamePrefix(payload) === 'PairChecked') {
+ callback(
+ this._program.registry
+ .createType('(String, String, {"game_id":"[u8;32]","pair_id":"u8","round":"u8"})', message.payload)[2]
+ .toJSON() as unknown as { game_id: ActorId; pair_id: number; round: number },
+ );
+ }
+ });
+ }
+
+ public subscribeToFirstRoundCheckedEvent(
+ callback: (data: { game_id: ActorId; wave: number }) => void | Promise,
+ ): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Battle' && getFnNamePrefix(payload) === 'FirstRoundChecked') {
+ callback(
+ this._program.registry
+ .createType('(String, String, {"game_id":"[u8;32]","wave":"u8"})', message.payload)[2]
+ .toJSON() as unknown as { game_id: ActorId; wave: number },
+ );
+ }
+ });
+ }
+
+ public subscribeToNextBattleStartedEvent(callback: (data: null) => void | Promise): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Battle' && getFnNamePrefix(payload) === 'NextBattleStarted') {
+ callback(null);
+ }
+ });
+ }
+
+ public subscribeToEnemyWaitingEvent(callback: (data: null) => void | Promise): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Battle' && getFnNamePrefix(payload) === 'EnemyWaiting') {
+ callback(null);
+ }
+ });
+ }
+
+ public subscribeToWarriorGeneratedEvent(
+ callback: (data: { address: ActorId }) => void | Promise,
+ ): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Battle' && getFnNamePrefix(payload) === 'WarriorGenerated') {
+ callback(
+ this._program.registry
+ .createType('(String, String, {"address":"[u8;32]"})', message.payload)[2]
+ .toJSON() as unknown as { address: ActorId },
+ );
+ }
+ });
+ }
+
+ public subscribeToAdminAddedEvent(
+ callback: (data: { new_admin: ActorId }) => void | Promise,
+ ): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Battle' && getFnNamePrefix(payload) === 'AdminAdded') {
+ callback(
+ this._program.registry
+ .createType('(String, String, {"new_admin":"[u8;32]"})', message.payload)[2]
+ .toJSON() as unknown as { new_admin: ActorId },
+ );
+ }
+ });
+ }
+
+ public subscribeToConfigChangedEvent(
+ callback: (data: { config: Config }) => void | Promise,
+ ): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Battle' && getFnNamePrefix(payload) === 'ConfigChanged') {
+ callback(
+ this._program.registry
+ .createType('(String, String, {"config":"Config"})', message.payload)[2]
+ .toJSON() as unknown as { config: Config },
+ );
+ }
+ });
+ }
+
+ public subscribeToGameLeftEvent(callback: (data: null) => void | Promise): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Battle' && getFnNamePrefix(payload) === 'GameLeft') {
+ callback(null);
+ }
+ });
+ }
+
+ public subscribeToRoundActionEvent(
+ callback: (data: {
+ round: number;
+ player_1: [ActorId, Move, number];
+ player_2: [ActorId, Move, number];
+ }) => void | Promise,
+ ): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Battle' && getFnNamePrefix(payload) === 'RoundAction') {
+ callback(
+ this._program.registry
+ .createType(
+ '(String, String, {"round":"u8","player_1":"([u8;32], Move, u16)","player_2":"([u8;32], Move, u16)"})',
+ message.payload,
+ )[2]
+ .toJSON() as unknown as {
+ round: number;
+ player_1: [ActorId, Move, number];
+ player_2: [ActorId, Move, number];
+ },
+ );
+ }
+ });
+ }
+
+ public subscribeToAutomaticMoveMadeEvent(callback: (data: null) => void | Promise): Promise<() => void> {
+ return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => {
+ if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) {
+ return;
+ }
+
+ const payload = message.payload.toHex();
+ if (getServiceNamePrefix(payload) === 'Battle' && getFnNamePrefix(payload) === 'AutomaticMoveMade') {
+ callback(null);
+ }
+ });
+ }
+}
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/sails/events/index.ts b/frontend/apps/web3-warriors-battle/src/app/utils/sails/events/index.ts
new file mode 100644
index 000000000..2f90a06c8
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/sails/events/index.ts
@@ -0,0 +1,2 @@
+export { useEventRoundActionSubscription } from './use-event-round-action-subscription';
+export { useEventBattleCanceledSubscription } from './use-event-battle-canceled-subscription';
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/sails/events/use-event-battle-canceled-subscription.ts b/frontend/apps/web3-warriors-battle/src/app/utils/sails/events/use-event-battle-canceled-subscription.ts
new file mode 100644
index 000000000..13a66cd24
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/sails/events/use-event-battle-canceled-subscription.ts
@@ -0,0 +1,38 @@
+import { useAtom } from 'jotai';
+import { useProgramEvent, useAccount } from '@gear-js/react-hooks';
+import { useProgram } from '@/app/utils';
+import { isBattleCanceledAtom } from '@/features/game/store';
+
+export function useEventBattleCanceledSubscription(currentGameId?: string) {
+ const program = useProgram();
+ const [isBattleCanceled, setIsBattleCanceled] = useAtom(isBattleCanceledAtom);
+ const { account } = useAccount();
+
+ const onBattleCanceled = ({ game_id }: { game_id: string }) => {
+ if (currentGameId === game_id && account?.decodedAddress !== currentGameId) {
+ setIsBattleCanceled(true);
+ }
+ };
+
+ const onRegisterCanceled = ({ player_id }: { player_id: string }) => {
+ if (account?.decodedAddress === player_id) {
+ setIsBattleCanceled(true);
+ }
+ };
+
+ useProgramEvent({
+ program,
+ serviceName: 'battle',
+ functionName: 'subscribeToBattleCanceledEvent',
+ onData: onBattleCanceled,
+ });
+
+ useProgramEvent({
+ program,
+ serviceName: 'battle',
+ functionName: 'subscribeToRegisterCanceledEvent',
+ onData: onRegisterCanceled,
+ });
+
+ return { isBattleCanceled, setIsBattleCanceled };
+}
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/sails/events/use-event-round-action-subscription.ts b/frontend/apps/web3-warriors-battle/src/app/utils/sails/events/use-event-round-action-subscription.ts
new file mode 100644
index 000000000..7b6c71287
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/sails/events/use-event-round-action-subscription.ts
@@ -0,0 +1,42 @@
+import { useAccount, useProgramEvent } from '@gear-js/react-hooks';
+import { Move, Pair, useProgram } from '@/app/utils';
+import { useRef, useState } from 'react';
+
+type RoundData = {
+ round: number;
+ player_1: [string, Move, number];
+ player_2: [string, Move, number];
+};
+
+export function useEventRoundActionSubscription(pair: Pair) {
+ const program = useProgram();
+ const { account } = useAccount();
+ const [lastMoves, setLastMoves] = useState<{ moves: [Move, Move]; newHealth: [number, number] } | null>(null);
+
+ const resetLastMoves = () => setLastMoves(null);
+
+ const roundRef = useRef(null);
+
+ const onData = ({ round, player_1, player_2 }: RoundData) => {
+ const players = [pair.player_1, pair.player_2];
+
+ if (players.includes(player_1[0]) && players.includes(player_2[0]) && account && roundRef.current !== round) {
+ roundRef.current = round;
+ const isMyBattle = players.includes(account.decodedAddress);
+ const isMatchPlayers = isMyBattle ? account.decodedAddress === player_1[0] : pair.player_1 === player_1[0];
+ const myData = isMatchPlayers ? player_1 : player_2;
+ const opponentsData = isMatchPlayers ? player_2 : player_1;
+
+ setLastMoves({ moves: [myData[1], opponentsData[1]], newHealth: [myData[2], opponentsData[2]] });
+ }
+ };
+
+ useProgramEvent({
+ program,
+ serviceName: 'battle',
+ functionName: 'subscribeToRoundActionEvent',
+ onData,
+ });
+
+ return { lastMoves, resetLastMoves };
+}
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/sails/index.ts b/frontend/apps/web3-warriors-battle/src/app/utils/sails/index.ts
new file mode 100644
index 000000000..bbdc3e257
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/sails/index.ts
@@ -0,0 +1,4 @@
+export * from './sails';
+export * from './battle';
+export * from './queries';
+export * from './messages';
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/index.ts b/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/index.ts
new file mode 100644
index 000000000..6a5d81ceb
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/index.ts
@@ -0,0 +1,9 @@
+export { useCreateNewBattleMessage } from './use-create-new-battle-message';
+export { useRegisterMessage } from './use-register-message';
+export { useCancelRegisterMessage } from './use-cancel-register-message';
+export { useCancelTournamentMessage } from './use-cancel-tournament-message';
+export { useStartBattleMessage } from './use-start-battle-message';
+export { useMakeMoveMessage } from './use-make-move-message';
+export { useStartNextFightMessage } from './use-start-next-fight-message';
+export { useExitGameMessage } from './use-exit-game-message';
+export { useDeletePlayerMessage } from './use-delete-player-message';
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-cancel-register-message.ts b/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-cancel-register-message.ts
new file mode 100644
index 000000000..d4b42b0f9
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-cancel-register-message.ts
@@ -0,0 +1,23 @@
+import { usePrepareProgramTransaction } from '@gear-js/react-hooks';
+import { useProgram } from '@/app/utils';
+import { Options, useSignAndSend } from '@/app/hooks/use-sign-and-send';
+
+export const useCancelRegisterMessage = () => {
+ const program = useProgram();
+ const { prepareTransactionAsync } = usePrepareProgramTransaction({
+ program,
+ serviceName: 'battle',
+ functionName: 'cancelRegister',
+ });
+ const { signAndSend } = useSignAndSend();
+
+ const cancelRegisterMessage = async (options: Options) => {
+ const { transaction } = await prepareTransactionAsync({
+ args: [],
+ gasLimit: { increaseGas: 10 },
+ });
+ signAndSend(transaction, options);
+ };
+
+ return { cancelRegisterMessage };
+};
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-cancel-tournament-message.ts b/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-cancel-tournament-message.ts
new file mode 100644
index 000000000..b11f2b700
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-cancel-tournament-message.ts
@@ -0,0 +1,26 @@
+import { usePrepareProgramTransaction } from '@gear-js/react-hooks';
+import { useProgram } from '@/app/utils';
+import { Options, useSignAndSend } from '@/app/hooks/use-sign-and-send';
+import { usePending } from '@/features/game/hooks';
+
+export const useCancelTournamentMessage = () => {
+ const program = useProgram();
+ const { prepareTransactionAsync } = usePrepareProgramTransaction({
+ program,
+ serviceName: 'battle',
+ functionName: 'cancelTournament',
+ });
+ const { signAndSend } = useSignAndSend();
+ const { setPending } = usePending();
+
+ const cancelTournamentMessage = async (options: Options) => {
+ setPending(true);
+ const { transaction } = await prepareTransactionAsync({
+ args: [],
+ gasLimit: { increaseGas: 10 },
+ });
+ signAndSend(transaction, options);
+ };
+
+ return { cancelTournamentMessage };
+};
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-create-new-battle-message.ts b/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-create-new-battle-message.ts
new file mode 100644
index 000000000..c3d5b5e5e
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-create-new-battle-message.ts
@@ -0,0 +1,36 @@
+import { usePrepareProgramTransaction } from '@gear-js/react-hooks';
+import { Appearance, useProgram } from '@/app/utils';
+import { Options, useSignAndSend } from '@/app/hooks/use-sign-and-send';
+
+type CreateNewBattleParams = {
+ value: bigint;
+ name: string;
+ tournamentName: string;
+ warriorId: string | null;
+ appearance: Appearance | null;
+ attack: number;
+ defence: number;
+ dodge: number;
+};
+
+export const useCreateNewBattleMessage = () => {
+ const program = useProgram();
+ const { prepareTransactionAsync } = usePrepareProgramTransaction({
+ program,
+ serviceName: 'battle',
+ functionName: 'createNewBattle',
+ });
+ const { signAndSend } = useSignAndSend();
+
+ const createNewBattleMessage = async (params: CreateNewBattleParams, options: Options) => {
+ const { value, tournamentName, name, warriorId, appearance, attack, defence, dodge } = params;
+ const { transaction } = await prepareTransactionAsync({
+ args: [tournamentName, name, warriorId, appearance, attack, defence, dodge],
+ gasLimit: { increaseGas: 10 },
+ value,
+ });
+ signAndSend(transaction, options);
+ };
+
+ return { createNewBattleMessage };
+};
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-delete-player-message.ts b/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-delete-player-message.ts
new file mode 100644
index 000000000..4ee2bc598
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-delete-player-message.ts
@@ -0,0 +1,23 @@
+import { usePrepareProgramTransaction } from '@gear-js/react-hooks';
+import { useProgram } from '@/app/utils';
+import { Options, useSignAndSend } from '@/app/hooks/use-sign-and-send';
+
+export const useDeletePlayerMessage = () => {
+ const program = useProgram();
+ const { prepareTransactionAsync } = usePrepareProgramTransaction({
+ program,
+ serviceName: 'battle',
+ functionName: 'deletePlayer',
+ });
+ const { signAndSend } = useSignAndSend();
+
+ const deletePlayerMessage = async (playerId: string, options?: Options) => {
+ const { transaction } = await prepareTransactionAsync({
+ args: [playerId],
+ gasLimit: { increaseGas: 10 },
+ });
+ signAndSend(transaction, options);
+ };
+
+ return { deletePlayerMessage };
+};
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-exit-game-message.ts b/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-exit-game-message.ts
new file mode 100644
index 000000000..63442b9b4
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-exit-game-message.ts
@@ -0,0 +1,23 @@
+import { usePrepareProgramTransaction } from '@gear-js/react-hooks';
+import { useProgram } from '@/app/utils';
+import { Options, useSignAndSend } from '@/app/hooks/use-sign-and-send';
+
+export const useExitGameMessage = () => {
+ const program = useProgram();
+ const { prepareTransactionAsync } = usePrepareProgramTransaction({
+ program,
+ serviceName: 'battle',
+ functionName: 'exitGame',
+ });
+ const { signAndSend } = useSignAndSend();
+
+ const exitGameMessage = async (options?: Options) => {
+ const { transaction } = await prepareTransactionAsync({
+ args: [],
+ gasLimit: { increaseGas: 10 },
+ });
+ signAndSend(transaction, options);
+ };
+
+ return { exitGameMessage };
+};
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-make-move-message.ts b/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-make-move-message.ts
new file mode 100644
index 000000000..a0c7638ba
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-make-move-message.ts
@@ -0,0 +1,23 @@
+import { usePrepareProgramTransaction } from '@gear-js/react-hooks';
+import { Move, useProgram } from '@/app/utils';
+import { Options, useSignAndSend } from '@/app/hooks/use-sign-and-send';
+
+export const useMakeMoveMessage = () => {
+ const program = useProgram();
+ const { prepareTransactionAsync } = usePrepareProgramTransaction({
+ program,
+ serviceName: 'battle',
+ functionName: 'makeMove',
+ });
+ const { signAndSend } = useSignAndSend();
+
+ const makeMoveMessage = async (move: Move, options?: Options) => {
+ const { transaction } = await prepareTransactionAsync({
+ args: [move],
+ gasLimit: { increaseGas: 10 },
+ });
+ signAndSend(transaction, options);
+ };
+
+ return { makeMoveMessage };
+};
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-register-message.ts b/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-register-message.ts
new file mode 100644
index 000000000..71896c0ab
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-register-message.ts
@@ -0,0 +1,36 @@
+import { usePrepareProgramTransaction } from '@gear-js/react-hooks';
+import { Appearance, useProgram } from '@/app/utils';
+import { Options, useSignAndSend } from '@/app/hooks/use-sign-and-send';
+
+type RegisterParams = {
+ value: bigint;
+ gameId: `0x${string}`;
+ name: string;
+ warriorId: string | null;
+ appearance: Appearance | null;
+ attack: number;
+ defence: number;
+ dodge: number;
+};
+
+export const useRegisterMessage = () => {
+ const program = useProgram();
+ const { prepareTransactionAsync } = usePrepareProgramTransaction({
+ program,
+ serviceName: 'battle',
+ functionName: 'register',
+ });
+ const { signAndSend } = useSignAndSend();
+
+ const registerMessage = async (params: RegisterParams, options: Options) => {
+ const { value, gameId, name, warriorId, appearance, attack, defence, dodge } = params;
+ const { transaction } = await prepareTransactionAsync({
+ args: [gameId, warriorId, appearance, name, attack, defence, dodge],
+ gasLimit: { increaseGas: 10 },
+ value,
+ });
+ signAndSend(transaction, options);
+ };
+
+ return { registerMessage };
+};
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-start-battle-message.ts b/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-start-battle-message.ts
new file mode 100644
index 000000000..e576b635b
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-start-battle-message.ts
@@ -0,0 +1,27 @@
+import { usePrepareProgramTransaction } from '@gear-js/react-hooks';
+import { useProgram } from '@/app/utils';
+import { Options, useSignAndSend } from '@/app/hooks/use-sign-and-send';
+import { usePending } from '@/features/game/hooks';
+
+export const useStartBattleMessage = () => {
+ const program = useProgram();
+ const { prepareTransactionAsync } = usePrepareProgramTransaction({
+ program,
+ serviceName: 'battle',
+ functionName: 'startBattle',
+ });
+ const { signAndSend } = useSignAndSend();
+ const { setPending } = usePending();
+
+ const startBattleMessage = async (options?: Options) => {
+ setPending(true);
+
+ const { transaction } = await prepareTransactionAsync({
+ args: [],
+ gasLimit: { increaseGas: 10 },
+ });
+ signAndSend(transaction, options);
+ };
+
+ return { startBattleMessage };
+};
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-start-next-fight-message.ts b/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-start-next-fight-message.ts
new file mode 100644
index 000000000..d1a88ddc4
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/sails/messages/use-start-next-fight-message.ts
@@ -0,0 +1,26 @@
+import { usePrepareProgramTransaction } from '@gear-js/react-hooks';
+import { useProgram } from '@/app/utils';
+import { Options, useSignAndSend } from '@/app/hooks/use-sign-and-send';
+import { usePending } from '@/features/game/hooks';
+
+export const useStartNextFightMessage = () => {
+ const program = useProgram();
+ const { prepareTransactionAsync } = usePrepareProgramTransaction({
+ program,
+ serviceName: 'battle',
+ functionName: 'startNextFight',
+ });
+ const { signAndSend } = useSignAndSend();
+ const { setPending } = usePending();
+
+ const startNextFightMessage = async (options?: Options) => {
+ setPending(true);
+ const { transaction } = await prepareTransactionAsync({
+ args: [],
+ gasLimit: { increaseGas: 10 },
+ });
+ signAndSend(transaction, options);
+ };
+
+ return { startNextFightMessage };
+};
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/sails/queries/index.ts b/frontend/apps/web3-warriors-battle/src/app/utils/sails/queries/index.ts
new file mode 100644
index 000000000..045e8896e
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/sails/queries/index.ts
@@ -0,0 +1,4 @@
+export { useAdminsQuery } from './use-admins-query';
+export { useConfigQuery } from './use-config-query';
+export { useMyBattleQuery } from './use-my-battle-query';
+export { useBattleQuery } from './use-battle-query';
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/sails/queries/use-admins-query.ts b/frontend/apps/web3-warriors-battle/src/app/utils/sails/queries/use-admins-query.ts
new file mode 100644
index 000000000..fc3695d24
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/sails/queries/use-admins-query.ts
@@ -0,0 +1,17 @@
+import { useProgram } from '@/app/utils';
+import { useAccount, useProgramQuery } from '@gear-js/react-hooks';
+
+export const useAdminsQuery = () => {
+ const program = useProgram();
+ const { account } = useAccount();
+
+ const { data, refetch, isFetching, error } = useProgramQuery({
+ program,
+ serviceName: 'battle',
+ functionName: 'admins',
+ args: [],
+ query: { enabled: account ? undefined : false },
+ });
+
+ return { admins: data, isFetching, refetch, error };
+};
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/sails/queries/use-battle-query.ts b/frontend/apps/web3-warriors-battle/src/app/utils/sails/queries/use-battle-query.ts
new file mode 100644
index 000000000..a376b2f50
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/sails/queries/use-battle-query.ts
@@ -0,0 +1,16 @@
+import { useProgram } from '@/app/utils';
+import { useProgramQuery } from '@gear-js/react-hooks';
+
+export const useBattleQuery = (gameAddress: string) => {
+ const program = useProgram();
+
+ const { data, refetch, isFetching, error } = useProgramQuery({
+ program,
+ serviceName: 'battle',
+ functionName: 'getBattle',
+ args: [gameAddress],
+ query: { enabled: false },
+ });
+
+ return { battleState: data, isFetching, refetch, error };
+};
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/sails/queries/use-config-query.ts b/frontend/apps/web3-warriors-battle/src/app/utils/sails/queries/use-config-query.ts
new file mode 100644
index 000000000..4ead8a72c
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/sails/queries/use-config-query.ts
@@ -0,0 +1,17 @@
+import { useProgram } from '@/app/utils';
+import { useAccount, useProgramQuery } from '@gear-js/react-hooks';
+
+export const useConfigQuery = () => {
+ const program = useProgram();
+ const { account } = useAccount();
+
+ const { data, refetch, isFetching, error } = useProgramQuery({
+ program,
+ serviceName: 'battle',
+ functionName: 'config',
+ args: [],
+ query: { enabled: account ? undefined : false },
+ });
+
+ return { config: data, isFetching, refetch, error };
+};
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/sails/queries/use-get-appearance-query.ts b/frontend/apps/web3-warriors-battle/src/app/utils/sails/queries/use-get-appearance-query.ts
new file mode 100644
index 000000000..fa31409fc
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/sails/queries/use-get-appearance-query.ts
@@ -0,0 +1,18 @@
+import { useAccount, useProgramQuery } from '@gear-js/react-hooks';
+import { useWarriorProgram } from '../warrior-programm';
+
+export const useGetAppearanceQuery = (programId: string) => {
+ const program = useWarriorProgram(programId);
+ const { account } = useAccount();
+
+ const { data, refetch, isFetching, error } = useProgramQuery({
+ program,
+ serviceName: 'warrior',
+ functionName: 'getAppearance',
+ args: [],
+ query: { enabled: account ? undefined : false },
+ watch: account ? true : false,
+ });
+
+ return { appearance: data, isFetching, refetch, error };
+};
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/sails/queries/use-my-battle-query.ts b/frontend/apps/web3-warriors-battle/src/app/utils/sails/queries/use-my-battle-query.ts
new file mode 100644
index 000000000..ebdec2b95
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/sails/queries/use-my-battle-query.ts
@@ -0,0 +1,18 @@
+import { useProgram } from '@/app/utils';
+import { useAccount, useProgramQuery } from '@gear-js/react-hooks';
+
+export const useMyBattleQuery = () => {
+ const program = useProgram();
+ const { account } = useAccount();
+
+ const { data, refetch, isFetching, error } = useProgramQuery({
+ program,
+ serviceName: 'battle',
+ functionName: 'getMyBattle',
+ args: [],
+ query: { enabled: account ? undefined : false },
+ watch: account ? true : false,
+ });
+
+ return { battleState: data, isFetching, refetch, error };
+};
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/sails/sails.tsx b/frontend/apps/web3-warriors-battle/src/app/utils/sails/sails.tsx
new file mode 100644
index 000000000..2cffde777
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/sails/sails.tsx
@@ -0,0 +1,12 @@
+import { useProgram as useGearJsProgram } from '@gear-js/react-hooks';
+import { Program } from '@/app/utils';
+import { useDnsProgramIds } from '@dapps-frontend/hooks';
+
+const useProgram = () => {
+ const { programId } = useDnsProgramIds();
+ const { data: program } = useGearJsProgram({ library: Program, id: programId });
+
+ return program;
+};
+
+export { useProgram };
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/sails/warrior-programm.tsx b/frontend/apps/web3-warriors-battle/src/app/utils/sails/warrior-programm.tsx
new file mode 100644
index 000000000..ded6aa387
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/sails/warrior-programm.tsx
@@ -0,0 +1,11 @@
+import { useProgram as useGearJsProgram } from '@gear-js/react-hooks';
+import { WarriorProgram } from './warrior';
+
+const useWarriorProgram = (programId: string) => {
+ const enabled = programId.startsWith('0x') && programId.length === 66 ? true : false;
+ const { data: program } = useGearJsProgram({ library: WarriorProgram, id: programId as `0x${string}`, query: { enabled } });
+
+ return program;
+};
+
+export { useWarriorProgram };
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/sails/warrior.idl b/frontend/apps/web3-warriors-battle/src/app/utils/sails/warrior.idl
new file mode 100644
index 000000000..ce8651b09
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/sails/warrior.idl
@@ -0,0 +1,18 @@
+type Appearance = struct {
+ head_index: u16,
+ hat_index: u16,
+ body_index: u16,
+ accessory_index: u16,
+ body_color: str,
+ back_color: str,
+};
+
+constructor {
+ New : ();
+};
+
+service Warrior {
+ query GetAppearance : () -> Appearance;
+ query GetOwner : () -> actor_id;
+};
+
diff --git a/frontend/apps/web3-warriors-battle/src/app/utils/sails/warrior.ts b/frontend/apps/web3-warriors-battle/src/app/utils/sails/warrior.ts
new file mode 100644
index 000000000..e9f509626
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/app/utils/sails/warrior.ts
@@ -0,0 +1,93 @@
+import { GearApi, decodeAddress } from '@gear-js/api';
+import { TypeRegistry } from '@polkadot/types';
+import { TransactionBuilder, ActorId, ZERO_ADDRESS } from 'sails-js';
+
+export interface Appearance {
+ head_index: number;
+ hat_index: number;
+ body_index: number;
+ accessory_index: number;
+ body_color: string;
+ back_color: string;
+}
+
+export class WarriorProgram {
+ public readonly registry: TypeRegistry;
+ public readonly warrior: Warrior;
+
+ constructor(public api: GearApi, public programId?: `0x${string}`) {
+ const types: Record = {
+ Appearance: {"head_index":"u16","hat_index":"u16","body_index":"u16","accessory_index":"u16","body_color":"String","back_color":"String"},
+ }
+
+ this.registry = new TypeRegistry();
+ this.registry.setKnownTypes({ types });
+ this.registry.register(types);
+
+ this.warrior = new Warrior(this);
+ }
+
+ newCtorFromCode(code: Uint8Array | Buffer): TransactionBuilder {
+ const builder = new TransactionBuilder(
+ this.api,
+ this.registry,
+ 'upload_program',
+ 'New',
+ 'String',
+ 'String',
+ code,
+ );
+
+ this.programId = builder.programId;
+ return builder;
+ }
+
+ newCtorFromCodeId(codeId: `0x${string}`) {
+ const builder = new TransactionBuilder(
+ this.api,
+ this.registry,
+ 'create_program',
+ 'New',
+ 'String',
+ 'String',
+ codeId,
+ );
+
+ this.programId = builder.programId;
+ return builder;
+ }
+}
+
+export class Warrior {
+ constructor(private _program: WarriorProgram) {}
+
+ public async getAppearance(originAddress?: string, value?: number | string | bigint, atBlock?: `0x${string}`): Promise {
+ const payload = this._program.registry.createType('(String, String)', ['Warrior', 'GetAppearance']).toHex();
+ const reply = await this._program.api.message.calculateReply({
+ destination: this._program.programId!,
+ origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS,
+ payload,
+ value: value || 0,
+ gasLimit: this._program.api.blockGasLimit.toBigInt(),
+ at: atBlock,
+ });
+ if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString());
+ const result = this._program.registry.createType('(String, String, Appearance)', reply.payload);
+ return result[2].toJSON() as unknown as Appearance;
+ }
+
+ public async getOwner(originAddress?: string, value?: number | string | bigint, atBlock?: `0x${string}`): Promise {
+ const payload = this._program.registry.createType('(String, String)', ['Warrior', 'GetOwner']).toHex();
+ const reply = await this._program.api.message.calculateReply({
+ destination: this._program.programId!,
+ origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS,
+ payload,
+ value: value || 0,
+ gasLimit: this._program.api.blockGasLimit.toBigInt(),
+ at: atBlock,
+ });
+ if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString());
+ const result = this._program.registry.createType('(String, String, [u8;32])', reply.payload);
+ return result[2].toJSON() as unknown as ActorId;
+ }
+}
\ No newline at end of file
diff --git a/frontend/apps/web3-warriors-battle/src/assets/images/icons/arrow-right.svg b/frontend/apps/web3-warriors-battle/src/assets/images/icons/arrow-right.svg
new file mode 100644
index 000000000..12e1fc00f
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/images/icons/arrow-right.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/assets/images/icons/burger-menu.svg b/frontend/apps/web3-warriors-battle/src/assets/images/icons/burger-menu.svg
new file mode 100644
index 000000000..e0aeb3426
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/images/icons/burger-menu.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/assets/images/icons/caret-down.svg b/frontend/apps/web3-warriors-battle/src/assets/images/icons/caret-down.svg
new file mode 100644
index 000000000..7cc1f0b2b
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/images/icons/caret-down.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/assets/images/icons/chevron-down.svg b/frontend/apps/web3-warriors-battle/src/assets/images/icons/chevron-down.svg
new file mode 100644
index 000000000..b6f35713f
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/images/icons/chevron-down.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/assets/images/icons/chevron-left.svg b/frontend/apps/web3-warriors-battle/src/assets/images/icons/chevron-left.svg
new file mode 100644
index 000000000..f35573311
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/images/icons/chevron-left.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/assets/images/icons/chevron-right.svg b/frontend/apps/web3-warriors-battle/src/assets/images/icons/chevron-right.svg
new file mode 100644
index 000000000..9404e32bb
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/images/icons/chevron-right.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/assets/images/icons/chevrons-left.svg b/frontend/apps/web3-warriors-battle/src/assets/images/icons/chevrons-left.svg
new file mode 100644
index 000000000..a8fea8d72
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/images/icons/chevrons-left.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/assets/images/icons/chevrons-right.svg b/frontend/apps/web3-warriors-battle/src/assets/images/icons/chevrons-right.svg
new file mode 100644
index 000000000..b01a5a7c1
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/images/icons/chevrons-right.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/assets/images/icons/cross.svg b/frontend/apps/web3-warriors-battle/src/assets/images/icons/cross.svg
new file mode 100644
index 000000000..dc8f8e1fc
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/images/icons/cross.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/assets/images/icons/discord.svg b/frontend/apps/web3-warriors-battle/src/assets/images/icons/discord.svg
new file mode 100644
index 000000000..edfc883f8
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/images/icons/discord.svg
@@ -0,0 +1,4 @@
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/assets/images/icons/gear-logo.svg b/frontend/apps/web3-warriors-battle/src/assets/images/icons/gear-logo.svg
new file mode 100644
index 000000000..49aa60865
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/images/icons/gear-logo.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/assets/images/icons/gear.svg b/frontend/apps/web3-warriors-battle/src/assets/images/icons/gear.svg
new file mode 100644
index 000000000..f9113ea5c
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/images/icons/gear.svg
@@ -0,0 +1,4 @@
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/assets/images/icons/github.svg b/frontend/apps/web3-warriors-battle/src/assets/images/icons/github.svg
new file mode 100644
index 000000000..68b456739
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/images/icons/github.svg
@@ -0,0 +1,4 @@
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/assets/images/icons/magic-line.svg b/frontend/apps/web3-warriors-battle/src/assets/images/icons/magic-line.svg
new file mode 100644
index 000000000..510560099
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/images/icons/magic-line.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/assets/images/icons/medium.svg b/frontend/apps/web3-warriors-battle/src/assets/images/icons/medium.svg
new file mode 100644
index 000000000..af8375726
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/images/icons/medium.svg
@@ -0,0 +1,4 @@
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/assets/images/icons/search.svg b/frontend/apps/web3-warriors-battle/src/assets/images/icons/search.svg
new file mode 100644
index 000000000..f5820d16b
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/images/icons/search.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/assets/images/icons/star.svg b/frontend/apps/web3-warriors-battle/src/assets/images/icons/star.svg
new file mode 100644
index 000000000..0515c08f1
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/images/icons/star.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/assets/images/icons/tvara-coin.svg b/frontend/apps/web3-warriors-battle/src/assets/images/icons/tvara-coin.svg
new file mode 100644
index 000000000..ec6036faa
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/images/icons/tvara-coin.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/assets/images/icons/twitter.svg b/frontend/apps/web3-warriors-battle/src/assets/images/icons/twitter.svg
new file mode 100644
index 000000000..9187632cc
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/images/icons/twitter.svg
@@ -0,0 +1,4 @@
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/assets/images/icons/user.svg b/frontend/apps/web3-warriors-battle/src/assets/images/icons/user.svg
new file mode 100644
index 000000000..794b70cf8
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/images/icons/user.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/assets/images/icons/vara-coin.svg b/frontend/apps/web3-warriors-battle/src/assets/images/icons/vara-coin.svg
new file mode 100644
index 000000000..0d0467d2f
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/images/icons/vara-coin.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/assets/images/icons/vara-logo.svg b/frontend/apps/web3-warriors-battle/src/assets/images/icons/vara-logo.svg
new file mode 100644
index 000000000..67452d7d9
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/images/icons/vara-logo.svg
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/assets/images/icons/vara-sign.svg b/frontend/apps/web3-warriors-battle/src/assets/images/icons/vara-sign.svg
new file mode 100644
index 000000000..29c4466cc
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/images/icons/vara-sign.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/assets/images/index.ts b/frontend/apps/web3-warriors-battle/src/assets/images/index.ts
new file mode 100644
index 000000000..7a3379d36
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/images/index.ts
@@ -0,0 +1,20 @@
+export { ReactComponent as CrossIcon } from './icons/cross.svg';
+export { ReactComponent as BurgerMenuIcon } from './icons/burger-menu.svg';
+export { ReactComponent as GearIcon } from './icons/gear.svg';
+export { ReactComponent as TwitterIcon } from './icons/twitter.svg';
+export { ReactComponent as GithubIcon } from './icons/github.svg';
+export { ReactComponent as DiscordIcon } from './icons/discord.svg';
+export { ReactComponent as MediumIcon } from './icons/medium.svg';
+export { ReactComponent as VaraLogoIcon } from './icons/vara-logo.svg';
+export { ReactComponent as VaraSignIcon } from './icons/vara-sign.svg';
+export { ReactComponent as GearLogoIcon } from './icons/gear-logo.svg';
+export { ReactComponent as ChevronDown } from './icons/chevron-down.svg';
+export { ReactComponent as ChevronLeft } from './icons/chevron-left.svg';
+export { ReactComponent as ChevronsLeft } from './icons/chevrons-left.svg';
+export { ReactComponent as ChevronRight } from './icons/chevron-right.svg';
+export { ReactComponent as ChevronsRight } from './icons/chevrons-right.svg';
+export { ReactComponent as CaretDown } from './icons/caret-down.svg';
+export { ReactComponent as VaraCoinIcon } from './icons/vara-coin.svg';
+export { ReactComponent as TVaraCoinIcon } from './icons/tvara-coin.svg';
+export { ReactComponent as BonusPointsIcon } from './icons/star.svg';
+export { ReactComponent as UserIcon } from './icons/user.svg';
diff --git a/frontend/apps/web3-warriors-battle/src/assets/styles/common.scss b/frontend/apps/web3-warriors-battle/src/assets/styles/common.scss
new file mode 100644
index 000000000..538c40f3b
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/styles/common.scss
@@ -0,0 +1,40 @@
+body {
+ font-family: 'Anuphan', sans-serif;
+ color: #000;
+ background-color: #fff;
+ font-variant-numeric: lining-nums proportional-nums;
+
+ @supports not (font-variation-settings: 'wdth' 115) {
+ & {
+ font-family: 'Anuphan-Fallback', sans-serif;
+ }
+ }
+
+ &.modal-open {
+ overflow-y: hidden;
+ }
+}
+
+#root {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ max-width: 100vw;
+
+ @supports (height: 1svh) {
+ min-height: 100svh;
+ }
+}
+
+main {
+ position: relative; // for loaders
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ padding: 0 0 32px 0;
+ overflow-x: hidden;
+
+ @media screen and (max-width: 767px) {
+ padding: 0;
+ }
+}
diff --git a/frontend/apps/web3-warriors-battle/src/assets/styles/fonts.scss b/frontend/apps/web3-warriors-battle/src/assets/styles/fonts.scss
new file mode 100644
index 000000000..62e199de7
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/styles/fonts.scss
@@ -0,0 +1,57 @@
+@font-face {
+ font-family: 'Anuphan-Fallback';
+ font-weight: 100;
+ src: url('/fonts/Anuphan-Thin.woff2') format('woff2');
+ font-display: swap;
+}
+
+@font-face {
+ font-family: 'Anuphan-Fallback';
+ font-weight: 200;
+ src: url('/fonts/Anuphan-ExtraLight.woff2') format('woff2');
+ font-display: swap;
+}
+
+@font-face {
+ font-family: 'Anuphan-Fallback';
+ font-weight: 300;
+ src: url('/fonts/Anuphan-Light.woff2') format('woff2');
+ font-display: swap;
+}
+
+@font-face {
+ font-family: 'Anuphan-Fallback';
+ font-weight: 400;
+ src: url('/fonts/Anuphan-Regular.woff2') format('woff2');
+ font-display: swap;
+}
+
+@font-face {
+ font-family: 'Anuphan-Fallback';
+ font-weight: 500;
+ src: url('/fonts/Anuphan-Medium.woff2') format('woff2');
+ font-display: swap;
+}
+
+@font-face {
+ font-family: 'Anuphan-Fallback';
+ font-weight: 600;
+ src: url('/fonts/Anuphan-SemiBold.woff2') format('woff2');
+ font-display: swap;
+}
+
+@font-face {
+ font-family: 'Anuphan-Fallback';
+ font-weight: 700;
+ src: url('/fonts/Anuphan-Bold.woff2') format('woff2');
+ font-display: swap;
+}
+
+@font-face {
+ font-family: 'Anuphan';
+ src: url('/fonts/anuphan-variable.woff2') format('woff2-variations');
+ font-weight: 100 700;
+ font-stretch: 75% 125%;
+ font-style: normal;
+ font-display: swap;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/assets/styles/index.scss b/frontend/apps/web3-warriors-battle/src/assets/styles/index.scss
new file mode 100644
index 000000000..0b0cb0733
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/styles/index.scss
@@ -0,0 +1,2 @@
+@import 'fonts';
+@import 'common';
diff --git a/frontend/apps/web3-warriors-battle/src/assets/styles/utils.scss b/frontend/apps/web3-warriors-battle/src/assets/styles/utils.scss
new file mode 100644
index 000000000..7f104576a
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/assets/styles/utils.scss
@@ -0,0 +1,19 @@
+%absoluteCenter {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+
+@mixin gap($value, $direction: bottom) {
+ $margin: 'margin-#{$direction}';
+
+ > *:not(:last-child) {
+ #{$margin}: $value;
+ }
+}
+
+@mixin textOverflow {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/index.ts b/frontend/apps/web3-warriors-battle/src/components/index.ts
new file mode 100644
index 000000000..db43f1a11
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/index.ts
@@ -0,0 +1,7 @@
+export { Header, NotAuthorized, NotFound, MainLayout } from './layout';
+export { ApiLoader, Loader, LoadingError } from './loaders';
+export { Modal } from './ui/modal';
+export { Card } from './ui/card';
+export { CardButton } from './ui/card-button';
+export { Text } from './ui/text';
+export { Segmented } from './ui/segmented';
diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/game-details/GameDetails.module.scss b/frontend/apps/web3-warriors-battle/src/components/layout/game-details/GameDetails.module.scss
new file mode 100644
index 000000000..179efe2fb
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/layout/game-details/GameDetails.module.scss
@@ -0,0 +1,29 @@
+.info {
+ width: 100%;
+ padding: 16px;
+ display: grid;
+ grid-template-rows: auto;
+ background: #F7F9FA;
+ color: #000000;
+ gap: 13px;
+ border-radius: 15px;
+}
+
+.item {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+
+ .itemName {
+ font-size: 14px;
+ font-weight: 400;
+ text-align: start;
+ }
+
+ .itemValue {
+ font-size: 14px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+}
\ No newline at end of file
diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/game-details/GameDetails.tsx b/frontend/apps/web3-warriors-battle/src/components/layout/game-details/GameDetails.tsx
new file mode 100644
index 000000000..09876bff8
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/layout/game-details/GameDetails.tsx
@@ -0,0 +1,29 @@
+import clsx from 'clsx';
+import styles from './GameDetails.module.scss';
+
+type Props = {
+ items: {
+ name: JSX.Element | string;
+ value: JSX.Element;
+ key: string;
+ }[];
+ className?: {
+ container?: string;
+ item?: string;
+ };
+};
+
+function GameDetails({ items, className }: Props) {
+ return (
+
+ {items.map((item) => (
+
+ {item.name}
+ {item.value}
+
+ ))}
+
+ );
+}
+
+export { GameDetails };
diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/game-details/index.ts b/frontend/apps/web3-warriors-battle/src/components/layout/game-details/index.ts
new file mode 100644
index 000000000..942e752a7
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/layout/game-details/index.ts
@@ -0,0 +1,3 @@
+import { GameDetails } from './GameDetails';
+
+export { GameDetails };
diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/header/header.module.scss b/frontend/apps/web3-warriors-battle/src/components/layout/header/header.module.scss
new file mode 100644
index 000000000..920212198
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/layout/header/header.module.scss
@@ -0,0 +1,62 @@
+.header {
+ position: relative;
+ z-index: 9;
+ padding: 20px;
+
+ &__container {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+ &__logoWrapper {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ max-width: 672px;
+ margin-right: 24px;
+ }
+
+ &__tutorial {
+ @media screen and (max-width: 767px) {
+ display: none !important;
+ }
+ }
+
+ &__logo {
+ flex-shrink: 0;
+
+ &--center {
+ @media screen and (max-width: 767px) {
+ margin: 0 auto;
+ }
+ }
+ }
+
+ &__wallet {
+ @media screen and (max-width: 767px) {
+ display: none;
+ }
+ }
+}
+
+.wallet > button {
+ font-size: 16px;
+ padding-right: 22px;
+ padding-left: 22px;
+}
+
+.mobile_balance {
+ display: none;
+
+ @media screen and (max-width: 767px) {
+ display: block;
+ display: flex;
+ margin-right: 14px;
+ }
+}
+
+.menu_wrapper {
+ display: flex;
+ flex-direction: row-reverse;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/header/header.tsx b/frontend/apps/web3-warriors-battle/src/components/layout/header/header.tsx
new file mode 100644
index 000000000..2966efbc3
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/layout/header/header.tsx
@@ -0,0 +1,31 @@
+import { Logo } from './logo';
+import styles from './header.module.scss';
+import { Header as CommonHeader, MenuHandler } from '@dapps-frontend/ui';
+import clsx from 'clsx';
+import { useAccount } from '@gear-js/react-hooks';
+import { GameButton } from '@/features/game/components/game-button';
+import { useNavigate } from 'react-router-dom';
+import { ROUTES } from '@/app/consts';
+
+export function Header() {
+ const { account } = useAccount();
+ const navigate = useNavigate();
+
+ return (
+
+
+ navigate(ROUTES.ONBOARDING)}
+ />
+
+ }
+ className={{ header: styles.header, content: styles.header__container }}
+ menu={ }
+ />
+ );
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/header/index.ts b/frontend/apps/web3-warriors-battle/src/components/layout/header/index.ts
new file mode 100644
index 000000000..ddd972315
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/layout/header/index.ts
@@ -0,0 +1 @@
+export { Header } from './header';
diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/header/logo/index.ts b/frontend/apps/web3-warriors-battle/src/components/layout/header/logo/index.ts
new file mode 100644
index 000000000..cfdf7a76b
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/layout/header/logo/index.ts
@@ -0,0 +1 @@
+export { Logo } from './logo';
diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/header/logo/logo.module.scss b/frontend/apps/web3-warriors-battle/src/components/layout/header/logo/logo.module.scss
new file mode 100644
index 000000000..b9def072c
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/layout/header/logo/logo.module.scss
@@ -0,0 +1,43 @@
+.link {
+ display: inline-flex;
+ transition: opacity 300ms ease;
+
+ @media screen and (max-width: 767px) {
+ position: relative;
+ }
+
+ &:not(.active):hover {
+ opacity: 0.7;
+ }
+
+ .title {
+ --gradient-to: #0ed3a3;
+
+ align-self: flex-start;
+ margin-top: -4px;
+ font-size: 20px;
+ line-height: 24px;
+ white-space: nowrap;
+ user-select: none;
+
+ @media screen and (max-width: 767px) {
+ display: none;
+ position: absolute;
+ left: 100%;
+ font-size: 10px;
+ line-height: 18px;
+ }
+ }
+}
+
+.logo {
+ width: 100%;
+ height: 100%;
+ max-height: 60px;
+ max-width: 92px;
+
+ @media screen and (max-width: 767px) {
+ max-height: 40px;
+ max-width: 62px;
+ }
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/header/logo/logo.tsx b/frontend/apps/web3-warriors-battle/src/components/layout/header/logo/logo.tsx
new file mode 100644
index 000000000..44e54e9dc
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/layout/header/logo/logo.tsx
@@ -0,0 +1,20 @@
+import { NavLink } from 'react-router-dom';
+import clsx from 'clsx';
+import styles from './logo.module.scss';
+import { ROUTES } from '@/app/consts';
+import { TextGradient } from '@/components/ui/text-gradient';
+import { Sprite } from '@/components/ui/sprite';
+import type { BaseComponentProps } from '@/app/types';
+
+type LogoProps = BaseComponentProps & {
+ label?: string;
+};
+
+export function Logo({ className, label }: LogoProps) {
+ return (
+ clsx(styles.link, isActive && styles.active, className)}>
+
+ {label && {label} }
+
+ );
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/index.ts b/frontend/apps/web3-warriors-battle/src/components/layout/index.ts
new file mode 100644
index 000000000..89f86633f
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/layout/index.ts
@@ -0,0 +1,6 @@
+export { Header } from './header';
+export { MainLayout } from './main-layout';
+export { NotFound } from './not-found';
+export { NotAuthorized } from './not-authorized';
+export { VaraIcon } from './vara-svg';
+export { GameDetails } from './game-details';
diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/main-layout.tsx b/frontend/apps/web3-warriors-battle/src/components/layout/main-layout.tsx
new file mode 100644
index 000000000..4fa43d4a7
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/layout/main-layout.tsx
@@ -0,0 +1,29 @@
+import { PropsWithChildren } from 'react';
+import { Footer } from '@dapps-frontend/ui';
+import { ApiLoader, Header } from '@/components';
+import { useIsAppReady, useIsAppReadySync } from '@/app/hooks/use-is-app-ready';
+import { useAuthSync } from '@/features/auth/hooks';
+import { Container } from '../ui/container';
+
+type MainLayoutProps = PropsWithChildren;
+
+export function MainLayout({ children }: MainLayoutProps) {
+ const { isAppReady } = useIsAppReady();
+
+ useIsAppReadySync();
+ useAuthSync();
+
+ return (
+ <>
+
+
+ {!isAppReady && }
+ {isAppReady && children}
+
+
+
+
+
+ >
+ );
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/not-authorized/index.ts b/frontend/apps/web3-warriors-battle/src/components/layout/not-authorized/index.ts
new file mode 100644
index 000000000..291b5c1f8
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/layout/not-authorized/index.ts
@@ -0,0 +1 @@
+export { NotAuthorized } from './not-authorized';
diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/not-authorized/not-authorized.module.scss b/frontend/apps/web3-warriors-battle/src/components/layout/not-authorized/not-authorized.module.scss
new file mode 100644
index 000000000..88504f226
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/layout/not-authorized/not-authorized.module.scss
@@ -0,0 +1,4 @@
+.card {
+ position: absolute;
+ top: 156px;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/not-authorized/not-authorized.tsx b/frontend/apps/web3-warriors-battle/src/components/layout/not-authorized/not-authorized.tsx
new file mode 100644
index 000000000..e922b6779
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/layout/not-authorized/not-authorized.tsx
@@ -0,0 +1,18 @@
+import { Background } from '@/features/game/components';
+import { Card } from '@/components/ui/card';
+import { Wallet } from '@dapps-frontend/ui';
+import styles from './not-authorized.module.scss';
+
+export function NotAuthorized() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/not-found/assets/images/404.jpg b/frontend/apps/web3-warriors-battle/src/components/layout/not-found/assets/images/404.jpg
new file mode 100644
index 000000000..06ba8cf4d
Binary files /dev/null and b/frontend/apps/web3-warriors-battle/src/components/layout/not-found/assets/images/404.jpg differ
diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/not-found/assets/images/404.webp b/frontend/apps/web3-warriors-battle/src/components/layout/not-found/assets/images/404.webp
new file mode 100644
index 000000000..4e0ad8d26
Binary files /dev/null and b/frontend/apps/web3-warriors-battle/src/components/layout/not-found/assets/images/404.webp differ
diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/not-found/index.ts b/frontend/apps/web3-warriors-battle/src/components/layout/not-found/index.ts
new file mode 100644
index 000000000..4324bddd0
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/layout/not-found/index.ts
@@ -0,0 +1 @@
+export { NotFound } from './not-found';
diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/not-found/not-found.module.scss b/frontend/apps/web3-warriors-battle/src/components/layout/not-found/not-found.module.scss
new file mode 100644
index 000000000..89b11e695
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/layout/not-found/not-found.module.scss
@@ -0,0 +1,46 @@
+@use '@/utils' as *;
+
+.container {
+ flex: 1;
+ display: grid;
+ @include gap(60px);
+ width: 100%;
+}
+
+.content {
+ display: grid;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ grid-gap: 48px;
+ grid-template-rows: 1fr auto auto;
+ height: 100%;
+}
+
+.image {
+ position: relative;
+ width: 100%;
+ height: 100%;
+
+ picture {
+ position: absolute;
+ display: flex;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ inset: 0;
+ }
+
+ img {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ overflow: visible;
+ }
+}
+
+.header {
+ @include gap(24px);
+ text-align: center;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/not-found/not-found.tsx b/frontend/apps/web3-warriors-battle/src/components/layout/not-found/not-found.tsx
new file mode 100644
index 000000000..3cc8b59d7
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/layout/not-found/not-found.tsx
@@ -0,0 +1,29 @@
+import styles from './not-found.module.scss';
+import { Link } from 'react-router-dom';
+import { buttonVariants } from '@/components/ui/button/button';
+import { Heading } from '@/components/ui/heading';
+import { ROUTES } from '@/app/consts';
+import ImageWebp from './assets/images/404.webp';
+import ImageBase from './assets/images/404.jpg';
+
+export function NotFound() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ Page not found
+
+
+ Back To Home
+
+
+
+ );
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/vara-svg/VaraIcon.tsx b/frontend/apps/web3-warriors-battle/src/components/layout/vara-svg/VaraIcon.tsx
new file mode 100644
index 000000000..cbd7580fe
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/layout/vara-svg/VaraIcon.tsx
@@ -0,0 +1,19 @@
+import { ReactComponent as VaraSVG } from '@/assets/images/icons/vara-coin.svg';
+import { ReactComponent as TVaraSVG } from '@/assets/images/icons/tvara-coin.svg';
+import { useApi } from '@gear-js/react-hooks';
+
+type VaraIconProps = {
+ className?: string;
+};
+
+function VaraIcon({ className }: VaraIconProps) {
+ const { api } = useApi();
+
+ return api?.registry.chainTokens[0].toLowerCase() === 'vara' ? (
+
+ ) : (
+
+ );
+}
+
+export { VaraIcon };
diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/vara-svg/index.ts b/frontend/apps/web3-warriors-battle/src/components/layout/vara-svg/index.ts
new file mode 100644
index 000000000..728e1e28c
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/layout/vara-svg/index.ts
@@ -0,0 +1,3 @@
+import { VaraIcon } from './VaraIcon';
+
+export { VaraIcon };
diff --git a/frontend/apps/web3-warriors-battle/src/components/loaders/api-loader/ApiLoader.module.scss b/frontend/apps/web3-warriors-battle/src/components/loaders/api-loader/ApiLoader.module.scss
new file mode 100644
index 000000000..3fc8b585c
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/loaders/api-loader/ApiLoader.module.scss
@@ -0,0 +1,24 @@
+@use '@/utils' as *;
+
+.loader {
+ @include absoluteCenter;
+ font-size: 21px;
+
+ &::after {
+ content: '.';
+ width: 1em;
+ display: inline-block;
+ text-align: left;
+ animation: ellipsis 1.25s infinite;
+ }
+}
+
+@keyframes ellipsis {
+ 33% {
+ content: '..';
+ }
+
+ 66% {
+ content: '...';
+ }
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/loaders/api-loader/ApiLoader.tsx b/frontend/apps/web3-warriors-battle/src/components/loaders/api-loader/ApiLoader.tsx
new file mode 100644
index 000000000..a17476d74
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/loaders/api-loader/ApiLoader.tsx
@@ -0,0 +1,7 @@
+import styles from './ApiLoader.module.scss';
+
+function ApiLoader() {
+ return Initializing API
;
+}
+
+export { ApiLoader };
diff --git a/frontend/apps/web3-warriors-battle/src/components/loaders/api-loader/index.ts b/frontend/apps/web3-warriors-battle/src/components/loaders/api-loader/index.ts
new file mode 100644
index 000000000..81cf28034
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/loaders/api-loader/index.ts
@@ -0,0 +1,3 @@
+import { ApiLoader } from './ApiLoader';
+
+export { ApiLoader };
diff --git a/frontend/apps/web3-warriors-battle/src/components/loaders/index.ts b/frontend/apps/web3-warriors-battle/src/components/loaders/index.ts
new file mode 100644
index 000000000..42537ef8d
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/loaders/index.ts
@@ -0,0 +1,3 @@
+export { ApiLoader } from './api-loader';
+export { Loader } from './loader';
+export { LoadingError } from './loading-error';
diff --git a/frontend/apps/web3-warriors-battle/src/components/loaders/loader/Loader.module.scss b/frontend/apps/web3-warriors-battle/src/components/loaders/loader/Loader.module.scss
new file mode 100644
index 000000000..642e55020
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/loaders/loader/Loader.module.scss
@@ -0,0 +1,32 @@
+@use '@/utils' as *;
+
+.loader {
+ @include absoluteCenter;
+ display: flex;
+ align-items: center;
+
+ &::before,
+ &::after {
+ content: '';
+ background: url(@/assets/images/icons/gear.svg) center/cover no-repeat;
+ animation: spin linear infinite;
+ }
+
+ &::before {
+ @include square(50px);
+ animation-duration: 4s;
+ }
+
+ &::after {
+ @include square(25px);
+ margin-left: -6px;
+ animation-duration: 2s;
+ animation-direction: reverse;
+ }
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/loaders/loader/Loader.tsx b/frontend/apps/web3-warriors-battle/src/components/loaders/loader/Loader.tsx
new file mode 100644
index 000000000..c6f45e44a
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/loaders/loader/Loader.tsx
@@ -0,0 +1,5 @@
+import styles from './Loader.module.scss';
+
+export function Loader() {
+ return
;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/loaders/loader/index.ts b/frontend/apps/web3-warriors-battle/src/components/loaders/loader/index.ts
new file mode 100644
index 000000000..ba8bc1d7f
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/loaders/loader/index.ts
@@ -0,0 +1,3 @@
+import { Loader } from './Loader';
+
+export { Loader };
diff --git a/frontend/apps/web3-warriors-battle/src/components/loaders/loading-error/index.ts b/frontend/apps/web3-warriors-battle/src/components/loaders/loading-error/index.ts
new file mode 100644
index 000000000..d0ec66cd2
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/loaders/loading-error/index.ts
@@ -0,0 +1 @@
+export { LoadingError } from './loading-error';
diff --git a/frontend/apps/web3-warriors-battle/src/components/loaders/loading-error/loading-error.module.scss b/frontend/apps/web3-warriors-battle/src/components/loaders/loading-error/loading-error.module.scss
new file mode 100644
index 000000000..1886c42b8
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/loaders/loading-error/loading-error.module.scss
@@ -0,0 +1,18 @@
+.box {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ flex: 1;
+ font-size: 21px;
+ text-align: center;
+
+ pre {
+ margin-top: 12px;
+ max-width: 500px;
+ font-size: 16px;
+ text-align: left;
+ color: #777777;
+ white-space: normal;
+ }
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/loaders/loading-error/loading-error.tsx b/frontend/apps/web3-warriors-battle/src/components/loaders/loading-error/loading-error.tsx
new file mode 100644
index 000000000..193a7d356
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/loaders/loading-error/loading-error.tsx
@@ -0,0 +1,9 @@
+import styles from './loading-error.module.scss';
+import { Container } from '@/components/ui/container';
+import { BaseComponentProps } from '@/app/types';
+
+type LoadingErrorProps = BaseComponentProps & {};
+
+export function LoadingError({ children }: LoadingErrorProps) {
+ return {children} ;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/alert/alert.module.scss b/frontend/apps/web3-warriors-battle/src/components/ui/alert/alert.module.scss
new file mode 100644
index 000000000..c219da5fd
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/alert/alert.module.scss
@@ -0,0 +1,99 @@
+.root {
+ position: fixed;
+ right: 20px;
+ bottom: 20px;
+ z-index: 20;
+ width: 100%;
+ max-width: min(480px, calc(100vw - 32px));
+}
+
+.alert {
+ position: relative;
+ display: grid;
+ grid-gap: 8px;
+ padding: 24px 48px 24px 24px;
+ color: #000;
+ background-color: #fff;
+ border-radius: 4px;
+ box-shadow: 0 10px 20px 0 rgba(91, 91, 91, 0.15);
+}
+
+.header {
+ display: flex;
+ align-items: center;
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 20px;
+ text-transform: capitalize;
+
+ &::before {
+ content: '';
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ margin-right: 10px;
+ background-size: cover;
+ background-repeat: no-repeat;
+ }
+
+ &.info {
+ &::before {
+ background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0_180_426)'%3E%3Cpath d='M16.6716 0.0294991H3.35792C2.9058 -0.0350076 2.44488 0.00661504 2.01163 0.151072C1.57839 0.295529 1.18472 0.538855 0.861786 0.861786C0.538855 1.18472 0.295529 1.57839 0.151072 2.01163C0.00661504 2.44488 -0.0350076 2.9058 0.0294991 3.35792V20L3.35792 16.6716H16.6716C17.1237 16.7361 17.5846 16.6945 18.0179 16.55C18.4511 16.4056 18.8448 16.1622 19.1677 15.8393C19.4906 15.5164 19.734 15.1227 19.8784 14.6895C20.0229 14.2562 20.0645 13.7953 20 13.3432V3.35792C20.0645 2.9058 20.0229 2.44488 19.8784 2.01163C19.734 1.57839 19.4906 1.18472 19.1677 0.861786C18.8448 0.538855 18.4511 0.295529 18.0179 0.151072C17.5846 0.00661504 17.1237 -0.0350076 16.6716 0.0294991ZM10.8469 11.679C10.8469 11.8996 10.7592 12.1113 10.6031 12.2673C10.4471 12.4234 10.2354 12.5111 10.0147 12.5111C9.79406 12.5111 9.58241 12.4234 9.42636 12.2673C9.27031 12.1113 9.18265 11.8996 9.18265 11.679V8.35054C9.18265 8.12985 9.27031 7.9182 9.42636 7.76216C9.58241 7.60611 9.79406 7.51844 10.0147 7.51844C10.2354 7.51844 10.4471 7.60611 10.6031 7.76216C10.7592 7.9182 10.8469 8.12985 10.8469 8.35054V11.679ZM10.0369 6.1316C9.74269 6.13307 9.45991 6.01759 9.2508 5.81056C9.04169 5.60354 8.92339 5.32192 8.92192 5.02767C8.92045 4.73342 9.03593 4.45064 9.24295 4.24153C9.44998 4.03242 9.73159 3.91412 10.0258 3.91265H10.0369C10.3312 3.91265 10.6134 4.02954 10.8215 4.23761C11.0295 4.44568 11.1464 4.72787 11.1464 5.02212C11.1464 5.31637 11.0295 5.59857 10.8215 5.80664C10.6134 6.01471 10.3312 6.1316 10.0369 6.1316Z' fill='black'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_180_426'%3E%3Crect width='20' height='20' fill='white'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E%0A");
+ }
+ }
+
+ &.success {
+ color: #0ed3a3;
+
+ &::before {
+ background-image: url("data:image/svg+xml,%3Csvg width='17' height='17' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8.5.5a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-.96 11.465L3.728 8.152l.904-.904 2.908 2.907 4.827-4.827.905.904-5.732 5.733Z' fill='%230ed3a3'/%3E%3C/svg%3E");
+ }
+ }
+
+ &.error {
+ color: #ff0101;
+
+ &::before {
+ background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0_180_8)'%3E%3Cpath d='M19.351 8.39432L11.5666 0.609894C11.1482 0.191345 10.5917 -0.0390625 10 -0.0390625C9.40827 -0.0390625 8.85178 0.191345 8.43338 0.609894L0.648956 8.39432C0.23056 8.81271 0 9.36905 0 9.96094C0 10.5528 0.23056 11.1092 0.648956 11.5276L8.43338 19.312C8.85178 19.7305 9.40827 19.9609 10 19.9609C10.5917 19.9609 11.1482 19.7305 11.5666 19.312L19.3509 11.5276C19.7694 11.1092 20 10.5528 20 9.96094C20 9.36905 19.7694 8.81271 19.351 8.39432ZM10.6012 13.9815C10.5992 14.0002 10.5965 14.0193 10.5925 14.0381C10.5887 14.0569 10.584 14.0756 10.5785 14.0941C10.573 14.1121 10.5664 14.1304 10.5589 14.148C10.5519 14.1655 10.5434 14.1832 10.5347 14.2C10.5258 14.2168 10.5156 14.2332 10.5051 14.2493C10.4944 14.2653 10.4831 14.2805 10.471 14.2954C10.4588 14.3102 10.4456 14.3246 10.4323 14.3384C10.4187 14.3517 10.4042 14.3649 10.3893 14.377C10.3745 14.3892 10.3592 14.4005 10.3432 14.411C10.3271 14.4215 10.3107 14.4318 10.2939 14.4408C10.2771 14.4493 10.2596 14.4576 10.2419 14.465C10.2243 14.4724 10.206 14.4791 10.188 14.4846C10.1695 14.4901 10.1509 14.4946 10.1321 14.4986C10.1134 14.5021 10.0941 14.5053 10.0754 14.5071C10.0563 14.5091 10.0368 14.5099 10.0175 14.5099C9.99848 14.5099 9.97925 14.5091 9.96002 14.5071C9.94095 14.5053 9.92188 14.5021 9.90296 14.4986C9.88434 14.4946 9.86557 14.4901 9.84757 14.4846C9.8291 14.4791 9.81079 14.4724 9.79324 14.465C9.77554 14.4576 9.7583 14.4493 9.74167 14.4408C9.72473 14.4318 9.70795 14.4215 9.69193 14.411C9.67636 14.4005 9.66065 14.3892 9.64584 14.377C9.63089 14.3649 9.61655 14.3517 9.60312 14.3384C9.58954 14.3246 9.57657 14.3102 9.56406 14.2954C9.55231 14.2805 9.54071 14.2653 9.53003 14.2493C9.5195 14.2332 9.50974 14.2168 9.50073 14.2C9.49188 14.1832 9.48364 14.1655 9.47617 14.148C9.46915 14.1304 9.46243 14.1121 9.45694 14.0941C9.45114 14.0756 9.44641 14.0569 9.4429 14.0381C9.43909 14.0193 9.43634 14.0002 9.43436 13.9815C9.43237 13.9623 9.43131 13.9427 9.43131 13.9236C9.43131 13.9044 9.43237 13.8853 9.43436 13.8661C9.43634 13.847 9.43909 13.8278 9.4429 13.8091C9.44641 13.7903 9.45114 13.7715 9.45694 13.7537C9.46243 13.7352 9.46915 13.7172 9.47617 13.6992C9.48364 13.6816 9.49173 13.6644 9.50073 13.6476C9.50974 13.6308 9.5195 13.614 9.53003 13.5985C9.54071 13.5825 9.55231 13.5667 9.56406 13.5519C9.57657 13.537 9.58954 13.5226 9.60312 13.5089C9.61655 13.4956 9.63089 13.4827 9.64584 13.4702C9.66065 13.4584 9.67621 13.4468 9.69193 13.4361C9.70795 13.4256 9.72473 13.4158 9.74167 13.4068C9.75845 13.3978 9.77554 13.3897 9.79324 13.3823C9.81079 13.3748 9.8291 13.3685 9.84757 13.363C9.86557 13.3572 9.88419 13.3525 9.90296 13.349C9.92172 13.3452 9.94095 13.342 9.96002 13.3405C9.99802 13.3365 10.0371 13.3365 10.0754 13.3405C10.0941 13.342 10.1134 13.3452 10.1321 13.349C10.1508 13.3525 10.1695 13.3572 10.188 13.363C10.206 13.3685 10.2243 13.3748 10.2419 13.3823C10.2596 13.3897 10.2771 13.3978 10.2939 13.4068C10.3107 13.4158 10.3271 13.4256 10.3432 13.4361C10.3592 13.4468 10.3745 13.4584 10.3893 13.4702C10.4042 13.4827 10.4187 13.4956 10.4323 13.5089C10.4456 13.5226 10.459 13.537 10.471 13.5519C10.4831 13.5667 10.4944 13.5825 10.5051 13.5985C10.5156 13.614 10.5257 13.6308 10.5347 13.6476C10.5434 13.6644 10.5519 13.6816 10.5589 13.6992C10.5664 13.7172 10.573 13.7352 10.5785 13.7537C10.584 13.7715 10.5887 13.7903 10.5925 13.8091C10.5965 13.8278 10.5992 13.847 10.6012 13.8661C10.6032 13.8853 10.6039 13.9044 10.6039 13.9236C10.6039 13.9427 10.6032 13.9623 10.6012 13.9815ZM10.6039 12.0865C10.6039 12.4103 10.3415 12.6729 10.0177 12.6729C9.69391 12.6729 9.43131 12.4103 9.43131 12.0865V6.00464C9.43131 5.68085 9.69391 5.41824 10.0177 5.41824C10.3415 5.41824 10.6039 5.68085 10.6039 6.00464V12.0865Z' fill='%23FF0101'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_180_8'%3E%3Crect width='20' height='20' fill='white'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E%0A");
+ }
+ }
+
+ &.loading {
+ color: #0e8ed3;
+
+ &::before {
+ background-image: url("data:image/svg+xml,%3Csvg width='30' height='30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M15 2.5c3.393 0 6.474 1.361 8.729 3.564l1.418-1.418a.498.498 0 0 1 .834.216l2 7a.5.5 0 0 1-.618.618l-7-1.999a.5.5 0 0 1-.216-.835L21.6 8.192A9.446 9.446 0 0 0 15 5.5c-5.239 0-9.5 4.261-9.5 9.5 0 5.238 4.261 9.5 9.5 9.5 5.238 0 9.5-4.262 9.5-9.5 0-.248-.018-.492-.038-.736l3.035.875C27.422 21.968 21.846 27.5 15 27.5 8.107 27.5 2.5 21.892 2.5 15 2.5 8.107 8.107 2.5 15 2.5Z' fill='%230e8ed3'/%3E%3C/svg%3E");
+ animation: rotating 1s linear infinite;
+ }
+ }
+}
+
+.button {
+ position: absolute;
+ top: 18px;
+ right: 18px;
+ margin-left: auto;
+ transition: color 350ms ease;
+
+ &:hover {
+ color: #777777;
+ }
+}
+
+.body {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding-top: 4px;
+ padding-bottom: 6px;
+ font-size: 14px;
+ line-height: 20px;
+}
+
+@keyframes rotating {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/alert/alert.tsx b/frontend/apps/web3-warriors-battle/src/components/ui/alert/alert.tsx
new file mode 100644
index 000000000..1e8ca9530
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/alert/alert.tsx
@@ -0,0 +1,23 @@
+import clsx from 'clsx';
+import { AlertProps } from './alert.types';
+import styles from './alert.module.scss';
+import { Button } from '../button';
+import { Sprite } from '@/components/ui/sprite';
+
+export function Alert({ alert, close }: AlertProps) {
+ const { content, options } = alert;
+ const { type, title, style, isClosed } = options;
+
+ return (
+
+
{title || type}
+
{content}
+ {isClosed && (
+
+
+
+ )}
+
+ );
+}
+export { styles as alertStyles };
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/alert/alert.types.ts b/frontend/apps/web3-warriors-battle/src/components/ui/alert/alert.types.ts
new file mode 100644
index 000000000..2e5f9d150
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/alert/alert.types.ts
@@ -0,0 +1,20 @@
+import { CSSProperties, ReactNode } from 'react';
+
+type Options = {
+ type: 'info' | 'error' | 'loading' | 'success';
+ style?: CSSProperties;
+ title?: string;
+ timeout?: number;
+ isClosed?: boolean;
+};
+
+type Alert = {
+ id: string;
+ content: ReactNode;
+ options: Options;
+};
+
+export type AlertProps = {
+ alert: Alert;
+ close: () => void;
+};
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/alert/index.ts b/frontend/apps/web3-warriors-battle/src/components/ui/alert/index.ts
new file mode 100644
index 000000000..d1c70adb7
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/alert/index.ts
@@ -0,0 +1,2 @@
+export { Alert, alertStyles } from './alert';
+export type { AlertProps } from './alert.types';
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/balance/Balance.module.scss b/frontend/apps/web3-warriors-battle/src/components/ui/balance/Balance.module.scss
new file mode 100644
index 000000000..02573d5cd
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/balance/Balance.module.scss
@@ -0,0 +1,40 @@
+@use '@/utils' as *;
+
+.wrapper {
+ display: flex;
+ align-items: center;
+ @include gap(10px, right);
+
+ svg {
+ width: 24px;
+ height: 24px;
+ }
+}
+
+.balance {
+ display: block;
+ line-height: 1;
+}
+
+.amount {
+ font-weight: 700;
+ font-size: 18px;
+ line-height: 20px;
+ letter-spacing: -0.02em;
+}
+
+.small {
+ font-size: 10px;
+ font-weight: 500;
+ line-height: 1.4;
+ color: rgba(#000, 0.6);
+}
+
+.decimal {
+ margin-left: 1px;
+}
+
+.unit {
+ margin-left: 4px;
+ text-transform: uppercase;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/balance/Balance.tsx b/frontend/apps/web3-warriors-battle/src/components/ui/balance/Balance.tsx
new file mode 100644
index 000000000..56890085f
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/balance/Balance.tsx
@@ -0,0 +1,30 @@
+import styles from './Balance.module.scss';
+import clsx from 'clsx';
+import type { BaseComponentProps } from '@/app/types';
+import { Sprite } from '@/components/ui/sprite';
+
+type Props = BaseComponentProps & {
+ icon: string;
+ value: string;
+ decimal?: string;
+ unit?: string;
+};
+
+type HOCProps = Omit;
+
+export function Balance({ icon, value, decimal, unit, className }: Props) {
+ return (
+
+
+
+
+ {decimal && {`.${decimal}`} }
+ {unit && {unit} }
+
+
+ );
+}
+
+export function PointsBalance({ value, unit = 'PPV', className }: HOCProps) {
+ return ;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/balance/index.ts b/frontend/apps/web3-warriors-battle/src/components/ui/balance/index.ts
new file mode 100644
index 000000000..9e1fccf25
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/balance/index.ts
@@ -0,0 +1,3 @@
+import { PointsBalance } from './Balance';
+
+export { PointsBalance };
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/button/button.tsx b/frontend/apps/web3-warriors-battle/src/components/ui/button/button.tsx
new file mode 100644
index 000000000..c20c841e8
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/button/button.tsx
@@ -0,0 +1,74 @@
+import { cva, type VariantProps } from 'class-variance-authority';
+import { ButtonHTMLAttributes, forwardRef } from 'react';
+import styles from './buttons.module.scss';
+
+export const buttonVariants = cva('', {
+ variants: {
+ variant: {
+ primary: styles.primary,
+ white: styles.white,
+ black: styles.black,
+ outline: styles.outline,
+ text: styles.text,
+ },
+ size: {
+ sm: styles.sm,
+ md: styles.md,
+ },
+ width: {
+ normal: '',
+ full: styles.block,
+ },
+ state: {
+ normal: '',
+ loading: styles.loading,
+ },
+ },
+ // compoundVariants: [{ variant: 'primary', size: 'medium', className: styles.primaryMedium }],
+ defaultVariants: {
+ variant: 'primary',
+ size: 'md',
+ state: 'normal',
+ width: 'normal',
+ },
+});
+
+export interface ButtonProps extends ButtonHTMLAttributes, VariantProps {
+ isLoading?: boolean;
+}
+
+export const Button = forwardRef(
+ ({ children, className, variant, size, state, isLoading, width, disabled, ...props }, ref) => {
+ return (
+
+ {isLoading && (
+
+
+
+ )}
+ {children}
+
+ );
+ },
+);
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/button/buttons.module.scss b/frontend/apps/web3-warriors-battle/src/components/ui/button/buttons.module.scss
new file mode 100644
index 000000000..c794d37b4
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/button/buttons.module.scss
@@ -0,0 +1,209 @@
+@use '@/utils' as *;
+
+.base {
+ --btn-text-color: #000;
+
+ font-weight: 700;
+ transition-property: background-color, border-color, color;
+ transition-duration: 300ms;
+ transition-timing-function: ease;
+ white-space: nowrap;
+ user-select: none;
+ touch-action: none;
+
+ &:disabled,
+ &[aria-disabled='true'] {
+ pointer-events: auto;
+ cursor: not-allowed;
+ }
+}
+
+a.base[aria-disabled='true'] {
+ pointer-events: none;
+}
+
+.btn {
+ --btn-bg-color: #64ffdb;
+ --btn-border-color: var(--btn-bg-color);
+ --btn-radius: 2px;
+
+ @extend .base;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--btn-text-color);
+ border-radius: var(--btn-radius);
+ border: 2px solid var(--btn-border-color);
+ background-color: var(--btn-bg-color);
+ @include gap(10px, right);
+ letter-spacing: 0.03em;
+}
+
+.md {
+ padding: 10px 30px;
+ font-size: 18px;
+ line-height: 26px;
+}
+
+.sm {
+ padding: 8px 14px;
+ font-size: 16px;
+ line-height: 20px;
+}
+
+.primary {
+ @extend .btn;
+ --btn-bg-color: #64ffdb;
+ --btn-border-color: var(--btn-bg-color);
+ --btn-text-color: #000;
+ position: relative;
+ z-index: 0;
+ background: transparent;
+ border-color: transparent;
+
+ &::before,
+ &::after {
+ --btn-bg-color: #91fcdd;
+ content: '';
+ position: absolute;
+ top: -2px;
+ left: -2px;
+ right: -2px;
+ bottom: -2px;
+ z-index: -1;
+ border-radius: var(--btn-radius);
+ }
+
+ &::before {
+ background: linear-gradient(270deg, #64ffdb 0%, #00ffc4 100%);
+ transition: opacity 350ms ease;
+ }
+
+ &::after {
+ opacity: 0;
+ background-color: var(--btn-bg-color);
+ transition: opacity 350ms ease, background-color 350ms ease;
+ }
+
+ &:not([disabled]) {
+ &:hover,
+ &:focus-visible {
+ &::after {
+ --btn-bg-color: #91fcdd;
+ opacity: 1;
+ }
+ }
+
+ &:active {
+ &::after {
+ --btn-bg-color: #0ed3a3;
+ opacity: 1;
+ }
+ }
+ }
+
+ &[aria-disabled='true'],
+ &:disabled {
+ &::before {
+ background: linear-gradient(270deg, rgba(100, 255, 219, 0.07) 0%, rgba(0, 255, 196, 0.07) 100%);
+ }
+ }
+
+ &.loading {
+ --btn-text-color: #474747;
+ opacity: 100%;
+ }
+}
+
+.black {
+ @extend .btn;
+ --btn-bg-color: #000;
+ --btn-text-color: #fff;
+
+ &:not(:disabled):not([aria-disabled='true']) {
+ &:hover,
+ &:focus-visible {
+ --btn-bg-color: #404040;
+ }
+
+ &:active {
+ --btn-bg-color: #262626;
+ }
+ }
+}
+
+.white {
+ @extend .btn;
+ --btn-bg-color: #fff;
+ --btn-text-color: #000;
+
+ &:not(:disabled):not([aria-disabled='true']) {
+ &:hover,
+ &:focus-visible {
+ --btn-bg-color: #d4d4d4;
+ }
+
+ &:active {
+ --btn-bg-color: #d4d4d4;
+ }
+ }
+}
+
+.outline {
+ @extend .btn;
+ --btn-bg-color: transparent;
+ --btn-border-color: #000;
+ --btn-text-color: #000;
+
+ &:not(:disabled):not([aria-disabled='true']) {
+ &:hover,
+ &:focus-visible {
+ --btn-bg-color: #000;
+ --btn-text-color: #fff;
+ }
+
+ &:active {
+ --btn-bg-color: #404040;
+ --btn-text-color: #fff;
+ --btn-border-color: var(--btn-bg-color);
+ }
+ }
+}
+
+.text {
+ @extend .base;
+ padding: 0;
+ background-color: unset;
+ border: unset;
+
+ &.loading {
+ --btn-text-color: #474747;
+ opacity: 100%;
+ }
+}
+
+.loader {
+ animation: spin 1s linear infinite;
+
+ @keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+ }
+}
+
+.loading {
+ opacity: 70%;
+
+ svg {
+ display: inline-block;
+ margin-right: 8px;
+ }
+}
+
+.block {
+ width: 100%;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/button/index.ts b/frontend/apps/web3-warriors-battle/src/components/ui/button/index.ts
new file mode 100644
index 000000000..6658ca0cb
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/button/index.ts
@@ -0,0 +1,2 @@
+export { Button, buttonVariants } from './button';
+export type { ButtonProps } from './button';
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/card-button/card-button.module.scss b/frontend/apps/web3-warriors-battle/src/components/ui/card-button/card-button.module.scss
new file mode 100644
index 000000000..0a51517fd
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/card-button/card-button.module.scss
@@ -0,0 +1,36 @@
+.card {
+ display: flex;
+ padding: 20px 24px;
+ gap: 16px;
+ border-radius: 12px;
+ border: 2px solid #000000;
+ background: #ffffff;
+ box-shadow: 0px 4px 100px 0px #cacaca33;
+ justify-content: space-between;
+ align-items: center;
+ cursor: pointer;
+
+ &:hover {
+ background-color: #f5f5f5;
+ transition: all 0.3s ease;
+ }
+}
+
+.title {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 10px;
+ line-height: 20px;
+}
+
+.disabled {
+ pointer-events: none;
+ background: #f6f6f6;
+ border: 2px solid #00000033;
+ color: #0000004d;
+
+ .title {
+ color: #00000033;
+ }
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/card-button/card-button.tsx b/frontend/apps/web3-warriors-battle/src/components/ui/card-button/card-button.tsx
new file mode 100644
index 000000000..62bb3926f
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/card-button/card-button.tsx
@@ -0,0 +1,32 @@
+import clsx from 'clsx';
+import { BaseComponentProps } from '@/app/types';
+import { ArrowRightIcon } from '@/features/game/assets/images';
+import { Text } from '../text';
+import styles from './card-button.module.scss';
+
+type CardButtonProps = BaseComponentProps & {
+ onClick: React.MouseEventHandler;
+ title: string;
+ description: string;
+ icon: React.ReactNode;
+ disabled?: boolean;
+};
+
+export function CardButton({ onClick, icon, title, className, description, disabled }: CardButtonProps) {
+ return (
+
+
+
+ {icon}
+
+ {title}
+
+
+
+ {description}
+
+
+
+
+ );
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/card-button/index.ts b/frontend/apps/web3-warriors-battle/src/components/ui/card-button/index.ts
new file mode 100644
index 000000000..049909aac
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/card-button/index.ts
@@ -0,0 +1 @@
+export { CardButton } from './card-button';
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/card/card.module.scss b/frontend/apps/web3-warriors-battle/src/components/ui/card/card.module.scss
new file mode 100644
index 000000000..102b233f9
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/card/card.module.scss
@@ -0,0 +1,83 @@
+.card {
+ display: flex;
+ border-radius: 8px;
+ border: 2px solid #000000;
+ background: #ffffff;
+ overflow: hidden;
+}
+
+.leftSide {
+ display: flex;
+ flex: 1;
+ align-items: center;
+ justify-content: flex-start;
+ flex-direction: column;
+ padding: 40px;
+ gap: 48px;
+ border-radius: 8px;
+}
+.sm .leftSide {
+ gap: 24px;
+}
+
+.md .leftSide {
+ gap: 28px;
+}
+
+.rightSide {
+ background: #4b4b55;
+ border-left: 2px solid #000000;
+ width: 386px;
+ padding: 32px 40px;
+}
+
+h2 {
+ &.lg {
+ font-size: 40px;
+ font-weight: 700;
+ line-height: 48px;
+ }
+
+ &.md {
+ font-size: 32px;
+ font-weight: 700;
+ line-height: 38px;
+ }
+
+ &.sm {
+ font-size: 24px;
+ font-weight: 700;
+ line-height: 28.8px;
+ }
+}
+
+.header {
+ width: 100%;
+}
+
+.center {
+ text-align: center;
+}
+
+.left {
+ text-align: left;
+}
+
+.description {
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 24px;
+ letter-spacing: 0.25px;
+
+ &.lg {
+ margin-top: 16px;
+ }
+
+ &.md {
+ margin-top: 8px;
+ }
+
+ &.sm {
+ margin-top: 8px;
+ }
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/card/card.tsx b/frontend/apps/web3-warriors-battle/src/components/ui/card/card.tsx
new file mode 100644
index 000000000..1c9ec8be7
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/card/card.tsx
@@ -0,0 +1,49 @@
+import { cva, VariantProps } from 'class-variance-authority';
+import clsx from 'clsx';
+import { BaseComponentProps } from '@/app/types';
+import { Text } from '../text';
+import styles from './card.module.scss';
+
+export const cardVariants = cva('', {
+ variants: {
+ size: {
+ sm: styles.sm,
+ md: styles.md,
+ lg: styles.lg,
+ },
+ align: {
+ left: styles.left,
+ center: styles.center,
+ },
+ },
+ defaultVariants: {
+ size: 'md',
+ align: 'center',
+ },
+});
+
+type CardProps = BaseComponentProps &
+ VariantProps & {
+ title?: string;
+ description?: string;
+ rightSideSlot?: React.ReactNode;
+ };
+
+export function Card({ children, title, align, className, size, description, rightSideSlot }: CardProps) {
+ return (
+
+
+
+ {title &&
{title} }
+ {description && (
+
+ {description}
+
+ )}
+
+ {children}
+
+ {rightSideSlot &&
{rightSideSlot}
}
+
+ );
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/card/index.ts b/frontend/apps/web3-warriors-battle/src/components/ui/card/index.ts
new file mode 100644
index 000000000..48b675389
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/card/index.ts
@@ -0,0 +1 @@
+export { Card } from './card';
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/container/container.module.scss b/frontend/apps/web3-warriors-battle/src/components/ui/container/container.module.scss
new file mode 100644
index 000000000..c0b0c0ec8
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/container/container.module.scss
@@ -0,0 +1,13 @@
+.container {
+ --container-padding: 20px;
+ width: 100%;
+ max-width: 1240px;
+ padding-right: var(--container-padding);
+ padding-left: var(--container-padding);
+ margin-right: auto;
+ margin-left: auto;
+
+ @media screen and (max-width: 767px) {
+ --container-padding: 16px;
+ }
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/container/container.tsx b/frontend/apps/web3-warriors-battle/src/components/ui/container/container.tsx
new file mode 100644
index 000000000..9f67301cb
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/container/container.tsx
@@ -0,0 +1,9 @@
+import styles from './container.module.scss';
+import clsx from 'clsx';
+import { BaseComponentProps } from '@/app/types';
+
+type ContainerProps = BaseComponentProps & {};
+
+export function Container({ children, className }: ContainerProps) {
+ return {children}
;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/container/index.ts b/frontend/apps/web3-warriors-battle/src/components/ui/container/index.ts
new file mode 100644
index 000000000..7872edd0e
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/container/index.ts
@@ -0,0 +1 @@
+export { Container } from './container';
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/heading/Heading.module.scss b/frontend/apps/web3-warriors-battle/src/components/ui/heading/Heading.module.scss
new file mode 100644
index 000000000..2bad4fb06
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/heading/Heading.module.scss
@@ -0,0 +1,40 @@
+.xs {
+ font-size: 24px;
+ line-height: 38px;
+}
+
+.sm {
+ font-size: 32px;
+ line-height: 42px;
+}
+
+.md {
+ font-size: 40px;
+ line-height: 56px;
+}
+
+.lg {
+ font-size: 64px;
+ line-height: 72px;
+}
+
+.xl {
+ font-size: 72px;
+ line-height: 86px;
+}
+
+.normal {
+ font-weight: 400;
+}
+
+.medium {
+ font-weight: 500;
+}
+
+.semibold {
+ font-weight: 600;
+}
+
+.bold {
+ font-weight: 700;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/heading/Heading.tsx b/frontend/apps/web3-warriors-battle/src/components/ui/heading/Heading.tsx
new file mode 100644
index 000000000..dc07c4c7b
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/heading/Heading.tsx
@@ -0,0 +1,35 @@
+import styles from './Heading.module.scss';
+import { cva, VariantProps } from 'class-variance-authority';
+import { BaseHTMLAttributes } from 'react';
+
+export const headingVariants = cva('', {
+ variants: {
+ size: {
+ xs: styles.xs,
+ sm: styles.sm,
+ md: styles.md,
+ lg: styles.lg,
+ xl: styles.xl,
+ },
+ weight: {
+ normal: styles.normal,
+ medium: styles.medium,
+ semibold: styles.semibold,
+ bold: styles.bold,
+ },
+ },
+ defaultVariants: {
+ size: 'md',
+ weight: 'medium',
+ },
+});
+
+export interface HeadingProps extends BaseHTMLAttributes, VariantProps {}
+
+export function Heading({ children, className, size, weight, ...props }: HeadingProps) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/heading/index.ts b/frontend/apps/web3-warriors-battle/src/components/ui/heading/index.ts
new file mode 100644
index 000000000..b2f13992e
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/heading/index.ts
@@ -0,0 +1,2 @@
+export { Heading, headingVariants } from './Heading';
+export type { HeadingProps } from './Heading';
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/modal/Modal.module.scss b/frontend/apps/web3-warriors-battle/src/components/ui/modal/Modal.module.scss
new file mode 100644
index 000000000..44cf06f7f
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/modal/Modal.module.scss
@@ -0,0 +1,76 @@
+.modal {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ padding: 0;
+ background: none;
+
+ &::backdrop {
+ background-color: rgba(0, 0, 0, 0.2);
+ backdrop-filter: blur(10px);
+ }
+}
+
+.wrapper {
+ max-width: 100%;
+ padding: 32px 24px 32px 32px;
+ border-radius: 15px;
+ background-color: #f6f8f8;
+}
+
+.header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+}
+
+.titleContainer {
+ display: flex;
+ flex-direction: column;
+}
+
+.title {
+ font-size: 32px;
+ font-weight: 700;
+ line-height: 120%;
+ letter-spacing: 0.02em;
+ color: #222424;
+ margin-bottom: 8px;
+}
+
+.sm .title {
+ font-size: 24px;
+ line-height: 28.8px;
+ letter-spacing: 0.02em;
+}
+
+.description {
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 24px;
+ letter-spacing: 0.25px;
+ color: #555756;
+ margin-bottom: 28px;
+}
+
+.modal-close {
+ position: relative;
+ bottom: 2px;
+ left: 5px;
+ margin-left: auto;
+ transition: color 350ms ease;
+
+ &:hover {
+ color: #777777;
+ }
+}
+
+.buttons {
+ display: flex;
+ gap: 24px;
+ width: 100%;
+ & > * {
+ flex: 1;
+ }
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/modal/Modal.tsx b/frontend/apps/web3-warriors-battle/src/components/ui/modal/Modal.tsx
new file mode 100644
index 000000000..0d79f78ad
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/modal/Modal.tsx
@@ -0,0 +1,96 @@
+import { VariantProps, cva } from 'class-variance-authority';
+import clsx from 'clsx';
+import { MouseEvent, useEffect, useRef } from 'react';
+import { motion } from 'framer-motion';
+import { variantsOverlay, variantsPanel } from '@/components/ui/modal/modal.variants';
+import { Button } from '../button';
+import { Sprite } from '@/components/ui/sprite';
+import type { BaseComponentProps } from '@/app/types';
+import styles from './Modal.module.scss';
+
+const variants = cva('', {
+ variants: { size: { md: styles.md, sm: styles.sm } },
+ defaultVariants: { size: 'md' },
+});
+
+type Props = BaseComponentProps &
+ VariantProps & {
+ title: string;
+ description?: string;
+ onClose: () => void;
+ buttons?: React.ReactNode;
+ modalClassName?: string;
+ closeOnBackdrop?: boolean;
+ };
+
+export function Modal({
+ title,
+ description,
+ buttons,
+ onClose,
+ className,
+ modalClassName,
+ closeOnBackdrop = true,
+ size,
+ children,
+}: Props) {
+ const ref = useRef(null);
+
+ const disableScroll = () => document.body.classList.add('modal-open');
+ const enableScroll = () => document.body.classList.remove('modal-open');
+
+ const open = () => {
+ ref.current?.showModal();
+ disableScroll();
+ };
+
+ const close = () => {
+ ref.current?.close();
+ enableScroll();
+ };
+
+ useEffect(() => {
+ open();
+
+ return () => close();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const handleClick = ({ target }: MouseEvent) => {
+ const isBackdropClick = target === ref.current;
+
+ if (isBackdropClick && closeOnBackdrop) onClose();
+ };
+
+ return (
+
+
+
+
+
{title}
+ {description &&
{description}
}
+
+
+
+
+
+
+ {children}
+
+ {buttons && {buttons}
}
+
+
+ );
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/modal/index.ts b/frontend/apps/web3-warriors-battle/src/components/ui/modal/index.ts
new file mode 100644
index 000000000..67fcb47bf
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/modal/index.ts
@@ -0,0 +1,3 @@
+import { Modal } from './Modal';
+
+export { Modal };
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/modal/modal.variants.ts b/frontend/apps/web3-warriors-battle/src/components/ui/modal/modal.variants.ts
new file mode 100644
index 000000000..c56cf20fd
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/modal/modal.variants.ts
@@ -0,0 +1,39 @@
+import { Variants } from 'framer-motion';
+
+export const variantsOverlay: Variants = {
+ closed: {
+ opacity: 0,
+ transition: {
+ // delay: 0.15,
+ duration: 0.3,
+ ease: 'easeIn',
+ },
+ },
+ open: {
+ opacity: 1,
+ transition: {
+ duration: 0.2,
+ ease: 'easeOut',
+ },
+ },
+};
+export const variantsPanel: Variants = {
+ closed: {
+ y: 'var(--y-closed, 0)',
+ opacity: 'var(--opacity-closed)',
+ scale: 'var(--scale-closed, 1)',
+ transition: {
+ duration: 0.3,
+ ease: 'easeIn',
+ },
+ },
+ open: {
+ y: 'var(--y-open, 0)',
+ opacity: 'var(--opacity-open)',
+ scale: 'var(--scale-open, 1)',
+ transition: {
+ // delay: 0.15,
+ duration: 0.2,
+ },
+ },
+};
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/scroll-area/index.ts b/frontend/apps/web3-warriors-battle/src/components/ui/scroll-area/index.ts
new file mode 100644
index 000000000..ca6bd6b1c
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/scroll-area/index.ts
@@ -0,0 +1 @@
+export { ScrollArea, ScrollBar } from './scroll-area';
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/scroll-area/scroll-area.module.scss b/frontend/apps/web3-warriors-battle/src/components/ui/scroll-area/scroll-area.module.scss
new file mode 100644
index 000000000..5669ded0f
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/scroll-area/scroll-area.module.scss
@@ -0,0 +1,67 @@
+.root {
+ position: relative;
+ display: flex;
+ overflow: hidden;
+}
+
+.viewport {
+ flex: 1;
+ //grow h-full w-full rounded-[inherit]
+}
+
+.scrollbar {
+ display: flex;
+ touch-action: none;
+ user-select: none;
+ transition-property: background-color;
+ transition-timing-function: ease;
+ transition-duration: 300ms;
+
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.2);
+ }
+
+ &.vertical {
+ height: 100%;
+ width: 1px;
+ margin-right: 1px;
+ }
+
+ &.horizontal {
+ height: 1px;
+ margin-top: 1px;
+ width: 100%;
+ }
+}
+
+.thumb {
+ position: relative;
+ flex: 1;
+ margin: 0 -1px;
+ border-radius: 2px;
+ transition-property: background-color;
+ transition-timing-function: ease;
+ transition-duration: 300ms;
+ background-color: rgba(0, 0, 0, 0.7);
+
+ &:hover {
+ background-color: rgba(0, 0, 0, 1);
+ }
+}
+
+.top {
+ background: linear-gradient(0deg, rgba(255, 255, 255, 0) 0%, #ffffff 78.26%);
+ top: -10px;
+}
+
+.bottom {
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, #ffffff 78.26%);
+ bottom: -10px;
+}
+
+.gradient {
+ position: absolute;
+ left: 0;
+ height: 46px;
+ width: 100%;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/scroll-area/scroll-area.tsx b/frontend/apps/web3-warriors-battle/src/components/ui/scroll-area/scroll-area.tsx
new file mode 100644
index 000000000..e72381de7
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/scroll-area/scroll-area.tsx
@@ -0,0 +1,39 @@
+import * as React from 'react';
+import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
+import clsx from 'clsx';
+import styles from './scroll-area.module.scss';
+
+const ScrollArea = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+ {children}
+
+
+
+
+));
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
+
+const ScrollBar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = 'vertical', ...props }, ref) => (
+
+ {/* */}
+
+));
+ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
+
+export { ScrollArea, ScrollBar };
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/segmented/index.ts b/frontend/apps/web3-warriors-battle/src/components/ui/segmented/index.ts
new file mode 100644
index 000000000..24832a4b3
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/segmented/index.ts
@@ -0,0 +1 @@
+export { Segmented } from './segmented';
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/segmented/segmented.module.scss b/frontend/apps/web3-warriors-battle/src/components/ui/segmented/segmented.module.scss
new file mode 100644
index 000000000..f73b2b5a0
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/segmented/segmented.module.scss
@@ -0,0 +1,29 @@
+.wrapper {
+ width: 583px;
+ height: 52px;
+ padding: 4px;
+ gap: 1px;
+ border-radius: 12px;
+ background: #eceded;
+ display: flex;
+ // font-family: Geist;
+ font-size: 16px;
+ font-weight: 600;
+ line-height: 24px;
+ text-align: center;
+ color: #58696e;
+}
+
+.option {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background-color 0.3s;
+ border-radius: 10px;
+}
+
+.selected {
+ background: #ffffff;
+ box-shadow: 0px 0.4px 1.2px 0px #00000033;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/segmented/segmented.tsx b/frontend/apps/web3-warriors-battle/src/components/ui/segmented/segmented.tsx
new file mode 100644
index 000000000..4685617a0
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/segmented/segmented.tsx
@@ -0,0 +1,32 @@
+import clsx from 'clsx';
+import styles from './segmented.module.scss';
+import { HTMLAttributes } from 'react';
+
+type SegmentedOption = {
+ label: React.ReactNode;
+ value: string;
+};
+
+type SegmentedProps = HTMLAttributes & {
+ options: SegmentedOption[];
+ onChange: (value: string) => void;
+ value: string;
+};
+
+const Segmented = ({ className, options, onChange, value }: SegmentedProps) => {
+ return (
+
+ {options.map((option) => (
+ onChange(option.value)}>
+ {option.label}
+
+ ))}
+
+ );
+};
+
+export { Segmented };
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/sprite.tsx b/frontend/apps/web3-warriors-battle/src/components/ui/sprite.tsx
new file mode 100644
index 000000000..7ca020dee
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/sprite.tsx
@@ -0,0 +1,15 @@
+import { FC, SVGProps } from 'react';
+
+type IconProps = SVGProps & {
+ name: string;
+ section?: string;
+ size?: number;
+};
+
+export const Sprite: FC = ({ name, className, section = 'icons', size, ...props }) => {
+ return (
+
+
+
+ );
+};
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/table/index.ts b/frontend/apps/web3-warriors-battle/src/components/ui/table/index.ts
new file mode 100644
index 000000000..e28a5bf2d
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/table/index.ts
@@ -0,0 +1 @@
+export { Table, TableBody, TableCell, TableFooter, TableCaption, TableHead, TableHeader, TableRow } from './table';
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/table/table.module.scss b/frontend/apps/web3-warriors-battle/src/components/ui/table/table.module.scss
new file mode 100644
index 000000000..21996f406
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/table/table.module.scss
@@ -0,0 +1,61 @@
+.table {
+ width: 100%;
+ max-width: calc(100vw - 40px);
+ overflow-x: auto;
+
+ table {
+ width: 100%;
+ color: #202020;
+ font-size: 14px;
+ font-style: normal;
+ line-height: 20px;
+ }
+
+ thead {
+ //'[&_tr]:border-b'
+ th {
+ color: #fff;
+ background-color: #000;
+ white-space: nowrap;
+ }
+ }
+
+ th,
+ td {
+ text-align: left;
+ // [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px];
+ font-weight: inherit;
+ vertical-align: middle;
+ line-height: 1;
+ }
+
+ th {
+ padding: 10px;
+ }
+
+ tr {
+ > *:first-child {
+ padding-left: 20px;
+ }
+ > *:last-child {
+ padding-right: 20px;
+ }
+ }
+
+ td {
+ //'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
+ padding: 14px 10px;
+ }
+
+ tbody {
+ //'[&_tr:last-child]:border-0'
+ }
+
+ tfoot {
+ //'bg-primary font-medium text-primary-foreground'
+ }
+
+ caption {
+ //'mt-4 text-sm text-muted-foreground',
+ }
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/table/table.tsx b/frontend/apps/web3-warriors-battle/src/components/ui/table/table.tsx
new file mode 100644
index 000000000..9888f318a
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/table/table.tsx
@@ -0,0 +1,46 @@
+import * as React from 'react';
+import styles from './table.module.scss';
+
+const Table = React.forwardRef>(({ ...props }, ref) => (
+
+));
+Table.displayName = 'Table';
+
+const TableHeader = React.forwardRef>(
+ ({ ...props }, ref) => ,
+);
+TableHeader.displayName = 'TableHeader';
+
+const TableBody = React.forwardRef>(
+ ({ ...props }, ref) => ,
+);
+TableBody.displayName = 'TableBody';
+
+const TableFooter = React.forwardRef>(
+ ({ className, ...props }, ref) => ,
+);
+TableFooter.displayName = 'TableFooter';
+
+const TableRow = React.forwardRef>(
+ ({ ...props }, ref) => ,
+);
+TableRow.displayName = 'TableRow';
+
+const TableHead = React.forwardRef>(
+ ({ ...props }, ref) => ,
+);
+TableHead.displayName = 'TableHead';
+
+const TableCell = React.forwardRef>(
+ ({ ...props }, ref) => ,
+);
+TableCell.displayName = 'TableCell';
+
+const TableCaption = React.forwardRef>(
+ ({ ...props }, ref) => ,
+);
+TableCaption.displayName = 'TableCaption';
+
+export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/text-gradient/index.ts b/frontend/apps/web3-warriors-battle/src/components/ui/text-gradient/index.ts
new file mode 100644
index 000000000..46799f9c5
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/text-gradient/index.ts
@@ -0,0 +1,3 @@
+import { TextGradient } from './text-gradient';
+
+export { TextGradient };
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/text-gradient/text-gradient.module.scss b/frontend/apps/web3-warriors-battle/src/components/ui/text-gradient/text-gradient.module.scss
new file mode 100644
index 000000000..f88ef6adb
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/text-gradient/text-gradient.module.scss
@@ -0,0 +1,8 @@
+.gradient {
+ --color-from: #2b2b2b;
+ --color-to: #00ffc4;
+
+ background: linear-gradient(90deg, var(--color-from) 33.39%, var(--color-to) 77.42%);
+ background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/text-gradient/text-gradient.tsx b/frontend/apps/web3-warriors-battle/src/components/ui/text-gradient/text-gradient.tsx
new file mode 100644
index 000000000..2737b185f
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/text-gradient/text-gradient.tsx
@@ -0,0 +1,6 @@
+import styles from './text-gradient.module.scss';
+import clsx from 'clsx';
+
+export function TextGradient({ children, className }: React.PropsWithChildren & { className?: string }) {
+ return {children} ;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/text/index.ts b/frontend/apps/web3-warriors-battle/src/components/ui/text/index.ts
new file mode 100644
index 000000000..8347e8fe5
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/text/index.ts
@@ -0,0 +1,2 @@
+export { Text, textVariants } from './text';
+export type { TextProps } from './text';
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/text/text.module.scss b/frontend/apps/web3-warriors-battle/src/components/ui/text/text.module.scss
new file mode 100644
index 000000000..61d2caa7d
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/text/text.module.scss
@@ -0,0 +1,40 @@
+.xs {
+ font-size: 12px;
+ line-height: 16px;
+}
+
+.sm {
+ font-size: 14px;
+ line-height: 20px;
+}
+
+.md {
+ font-size: 16px;
+ line-height: 23px;
+}
+
+.lg {
+ font-size: 18px;
+ line-height: 25px;
+}
+
+.xl {
+ font-size: 20px;
+ line-height: 28px;
+}
+
+.normal {
+ font-weight: 400;
+}
+
+.medium {
+ font-weight: 500;
+}
+
+.semibold {
+ font-weight: 600;
+}
+
+.bold {
+ font-weight: 700;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/components/ui/text/text.tsx b/frontend/apps/web3-warriors-battle/src/components/ui/text/text.tsx
new file mode 100644
index 000000000..939ff34fa
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/components/ui/text/text.tsx
@@ -0,0 +1,35 @@
+import { cva, type VariantProps } from 'class-variance-authority';
+import { BaseHTMLAttributes } from 'react';
+import styles from './text.module.scss';
+
+export const textVariants = cva('', {
+ variants: {
+ size: {
+ xs: styles.xs,
+ sm: styles.sm,
+ md: styles.md,
+ lg: styles.lg,
+ xl: styles.xl,
+ },
+ weight: {
+ normal: styles.normal,
+ medium: styles.medium,
+ semibold: styles.semibold,
+ bold: styles.bold,
+ },
+ },
+ defaultVariants: {
+ size: 'md',
+ weight: 'normal',
+ },
+});
+
+export interface TextProps extends BaseHTMLAttributes, VariantProps {}
+
+export function Text({ children, className, size, weight, ...props }: TextProps) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/frontend/apps/web3-warriors-battle/src/env.d.ts b/frontend/apps/web3-warriors-battle/src/env.d.ts
new file mode 100644
index 000000000..e3cc1c7d5
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/env.d.ts
@@ -0,0 +1,13 @@
+interface ImportMetaEnv {
+ readonly VITE_NODE_ADDRESS: string;
+ readonly VITE_AUTH_API_ADDRESS: string;
+ readonly VITE_DNS_API_URL: string;
+ readonly VITE_DNS_NAME: string;
+ readonly VITE_DEFAULT_NODES_URL: string;
+ readonly VITE_STAGING_NODES_URL: string;
+ readonly VITE_GTM_ID_TTT: string;
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/account-available-balance/consts.ts b/frontend/apps/web3-warriors-battle/src/features/account-available-balance/consts.ts
new file mode 100644
index 000000000..6f32bee2b
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/account-available-balance/consts.ts
@@ -0,0 +1,6 @@
+import { atom } from 'jotai';
+
+export const IS_AVAILABLE_BALANCE_READY = atom(false);
+export const AVAILABLE_BALANCE = atom(
+ undefined,
+);
diff --git a/frontend/apps/web3-warriors-battle/src/features/account-available-balance/hooks.ts b/frontend/apps/web3-warriors-battle/src/features/account-available-balance/hooks.ts
new file mode 100644
index 000000000..d27082a0d
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/account-available-balance/hooks.ts
@@ -0,0 +1,69 @@
+import { useEffect } from 'react';
+import { useAtomValue, useSetAtom } from 'jotai';
+import { useAccount, useApi, useBalance } from '@gear-js/react-hooks';
+import { CreateType } from '@gear-js/api';
+import { formatBalance } from '@polkadot/util';
+import { ISystemAccount } from './types';
+import { AVAILABLE_BALANCE, IS_AVAILABLE_BALANCE_READY } from './consts';
+
+export function useAccountAvailableBalance() {
+ const isAvailableBalanceReady = useAtomValue(IS_AVAILABLE_BALANCE_READY);
+ const availableBalance = useAtomValue(AVAILABLE_BALANCE);
+ const setAvailableBalance = useSetAtom(AVAILABLE_BALANCE);
+ return { isAvailableBalanceReady, availableBalance, setAvailableBalance };
+}
+
+export function useAccountAvailableBalanceSync() {
+ const { isAccountReady, account } = useAccount();
+ const { api, isApiReady } = useApi();
+ const { balance, isBalanceReady } = useBalance(account?.decodedAddress || '');
+
+ const isReady = useAtomValue(IS_AVAILABLE_BALANCE_READY);
+ const setIsReady = useSetAtom(IS_AVAILABLE_BALANCE_READY);
+ const setAvailableBalance = useSetAtom(AVAILABLE_BALANCE);
+
+ useEffect(() => {
+ if (!api || !isApiReady || !isAccountReady) return;
+
+ if (account && balance) {
+ api.query.system.account(account.decodedAddress).then((res) => {
+ const systemAccount = res.toJSON() as ISystemAccount;
+ const total = balance.toString();
+ const fee = CreateType.create('u128', systemAccount.data.feeFrozen).toString();
+
+ const getBalance = (b: string) => () => {
+ const [unit] = api.registry.chainTokens;
+ const [decimals] = api.registry.chainDecimals;
+
+ const existentialDeposit = formatBalance(api.existentialDeposit, {
+ decimals,
+ forceUnit: unit,
+ withSiFull: false,
+ withSi: false,
+ withUnit: unit,
+ withZero: false,
+ });
+
+ const value = formatBalance(b.toString(), {
+ decimals,
+ forceUnit: unit,
+ withSiFull: false,
+ withSi: false,
+ withUnit: unit,
+ });
+
+ // `${value.slice(0, -2)}`
+ return { value, unit, existentialDeposit };
+ };
+
+ setAvailableBalance(getBalance(`${+total - +fee}`));
+
+ if (!isReady) {
+ setIsReady(true);
+ }
+ });
+ } else {
+ setIsReady(true);
+ }
+ }, [account, api, isAccountReady, isApiReady, isReady, setAvailableBalance, setIsReady, balance, isBalanceReady]);
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/account-available-balance/types.ts b/frontend/apps/web3-warriors-battle/src/features/account-available-balance/types.ts
new file mode 100644
index 000000000..1abf4da8d
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/account-available-balance/types.ts
@@ -0,0 +1,14 @@
+import type { HexString } from '@polkadot/util/types';
+
+export type ISystemAccount = {
+ consumers: number; // 0
+ data: {
+ feeFrozen: number | HexString; // 0
+ free: number | HexString; // '0x...'
+ miscFrozen: number | HexString; // 0
+ reserved: number | HexString; // 8327965542000
+ };
+ nonce: number; // 94
+ providers: number; // 1
+ sufficients: number; // 0
+};
diff --git a/frontend/apps/web3-warriors-battle/src/features/auth/consts.ts b/frontend/apps/web3-warriors-battle/src/features/auth/consts.ts
new file mode 100644
index 000000000..27ddbcfa5
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/auth/consts.ts
@@ -0,0 +1,8 @@
+import { atom } from 'jotai';
+
+const AUTH_MESSAGE = 'VARA';
+
+const AUTH_TOKEN_ATOM = atom('');
+const IS_AUTH_READY_ATOM = atom(false);
+
+export { AUTH_MESSAGE, AUTH_TOKEN_ATOM, IS_AUTH_READY_ATOM };
diff --git a/frontend/apps/web3-warriors-battle/src/features/auth/hooks.ts b/frontend/apps/web3-warriors-battle/src/features/auth/hooks.ts
new file mode 100644
index 000000000..7c893c921
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/auth/hooks.ts
@@ -0,0 +1,44 @@
+import { useAtom } from 'jotai';
+import { Account, useAccount } from '@gear-js/react-hooks';
+import { IS_AUTH_READY_ATOM } from './consts';
+import { useWallet } from '@/features/wallet/hooks';
+import { useEffect } from 'react';
+
+export function useAuth() {
+ const [isAuthReady, setIsAuthReady] = useAtom(IS_AUTH_READY_ATOM);
+
+ const { login, logout } = useAccount();
+ const { resetWalletId } = useWallet();
+
+ const signOut = () => {
+ logout();
+ resetWalletId();
+ };
+
+ const auth = async () => {
+ setIsAuthReady(true);
+ };
+
+ const signIn = async (_account: Account) => {
+ await login(_account);
+ };
+
+ return { signIn, signOut, auth, isAuthReady };
+}
+
+function useAuthSync() {
+ const { isAccountReady, account } = useAccount();
+ const { auth } = useAuth();
+
+ useEffect(() => {
+ if (!isAccountReady) {
+ return;
+ }
+
+ auth();
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isAccountReady, account?.decodedAddress]);
+}
+
+export { useAuthSync };
diff --git a/frontend/apps/web3-warriors-battle/src/features/auth/index.ts b/frontend/apps/web3-warriors-battle/src/features/auth/index.ts
new file mode 100644
index 000000000..4830b0318
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/auth/index.ts
@@ -0,0 +1 @@
+export { useAuth } from './hooks';
diff --git a/frontend/apps/web3-warriors-battle/src/features/auth/types.ts b/frontend/apps/web3-warriors-battle/src/features/auth/types.ts
new file mode 100644
index 000000000..1face70d7
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/auth/types.ts
@@ -0,0 +1,35 @@
+export type SignInResponse = {
+ accessToken: string;
+ discord: string | null;
+ username: string;
+};
+
+export type ShareLinkResponse = {
+ link: string;
+ registeredUserCount: number;
+ remainingUsersToInvite: number;
+};
+
+export type LinkResponse = ShareLinkResponse & {
+ expired?: boolean;
+ freeze?: number;
+ message?: string;
+};
+
+export type AuthResponse = {
+ success: true;
+ content: {
+ user: {
+ address: string;
+ activities: {
+ staked: boolean;
+ raced: boolean;
+ tictactoe: boolean;
+ };
+ };
+ };
+};
+
+export type AvailableTokensResponse = {
+ result: number;
+};
diff --git a/frontend/apps/web3-warriors-battle/src/features/auth/utils.ts b/frontend/apps/web3-warriors-battle/src/features/auth/utils.ts
new file mode 100644
index 000000000..cef936fd0
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/auth/utils.ts
@@ -0,0 +1,3 @@
+export function trimEndSlash(url: string): string {
+ return url?.endsWith('/') ? url.slice(0, -1) : url;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/background.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/background.svg
new file mode 100644
index 000000000..61dab03a3
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/background.svg
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-1.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-1.svg
new file mode 100644
index 000000000..1444b63fd
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-1.svg
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-10.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-10.svg
new file mode 100644
index 000000000..cc191a5b1
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-10.svg
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-11.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-11.svg
new file mode 100644
index 000000000..61b7c15f1
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-11.svg
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-2.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-2.svg
new file mode 100644
index 000000000..c406fa35a
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-2.svg
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-3.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-3.svg
new file mode 100644
index 000000000..b431fc9c4
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-3.svg
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-4.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-4.svg
new file mode 100644
index 000000000..654e25cac
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-4.svg
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-5.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-5.svg
new file mode 100644
index 000000000..73501d7dd
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-5.svg
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-6.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-6.svg
new file mode 100644
index 000000000..4a6122fe1
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-6.svg
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-7.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-7.svg
new file mode 100644
index 000000000..721a85292
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-7.svg
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-8.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-8.svg
new file mode 100644
index 000000000..f037ac1b7
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-8.svg
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-9.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-9.svg
new file mode 100644
index 000000000..e1f11856e
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/accessories-9.svg
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/body-1.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/body-1.svg
new file mode 100644
index 000000000..f58f5f942
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/body-1.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/body-2.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/body-2.svg
new file mode 100644
index 000000000..56c6995ab
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/body-2.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/body-3.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/body-3.svg
new file mode 100644
index 000000000..b2c045de2
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/body-3.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/body-4.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/body-4.svg
new file mode 100644
index 000000000..59ac50767
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/body-4.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/body-5.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/body-5.svg
new file mode 100644
index 000000000..c60767477
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/body-5.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/body-6.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/body-6.svg
new file mode 100644
index 000000000..f2fea814d
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/body-6.svg
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-1.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-1.svg
new file mode 100644
index 000000000..08db6da8e
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-1.svg
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-2.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-2.svg
new file mode 100644
index 000000000..92bd06c95
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-2.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-3.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-3.svg
new file mode 100644
index 000000000..4aa891df3
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-3.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-4.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-4.svg
new file mode 100644
index 000000000..01049376c
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-4.svg
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-5.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-5.svg
new file mode 100644
index 000000000..edcb2cbc1
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-5.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-6.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-6.svg
new file mode 100644
index 000000000..979e5a8ec
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-6.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-7.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-7.svg
new file mode 100644
index 000000000..fd15c014d
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-7.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-8.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-8.svg
new file mode 100644
index 000000000..c98bfd102
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-8.svg
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-9.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-9.svg
new file mode 100644
index 000000000..dc2c7a01f
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/hat-9.svg
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/head-1.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/head-1.svg
new file mode 100644
index 000000000..65a9fb247
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/head-1.svg
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/head-2.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/head-2.svg
new file mode 100644
index 000000000..4e71eb2dd
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/head-2.svg
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/head-3.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/head-3.svg
new file mode 100644
index 000000000..66bf9035e
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/head-3.svg
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/head-4.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/head-4.svg
new file mode 100644
index 000000000..f7ac853ca
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/head-4.svg
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/head-5.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/head-5.svg
new file mode 100644
index 000000000..6b759b85e
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/head-5.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/index.ts b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/index.ts
new file mode 100644
index 000000000..3423479e9
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/index.ts
@@ -0,0 +1,2 @@
+export { ReactComponent as BodyColor } from './static/body-color.svg';
+export { ReactComponent as BackColor } from './static/back-color.svg';
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/static/back-color.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/static/back-color.svg
new file mode 100644
index 000000000..3d1ac8438
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/static/back-color.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/static/body-color.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/static/body-color.svg
new file mode 100644
index 000000000..28031093a
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/character/static/body-color.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/admin.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/admin.svg
new file mode 100644
index 000000000..49b74d659
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/admin.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/arrow-right.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/arrow-right.svg
new file mode 100644
index 000000000..a12a6334d
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/arrow-right.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/attack-button.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/attack-button.svg
new file mode 100644
index 000000000..1e175276f
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/attack-button.svg
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/attack.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/attack.svg
new file mode 100644
index 000000000..e595dda33
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/attack.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/caret-right.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/caret-right.svg
new file mode 100644
index 000000000..cb86e3e70
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/caret-right.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/code-slash.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/code-slash.svg
new file mode 100644
index 000000000..4de041985
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/code-slash.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/copy.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/copy.svg
new file mode 100644
index 000000000..723898ca3
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/copy.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/cup-star.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/cup-star.svg
new file mode 100644
index 000000000..e58b7e765
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/cup-star.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/defence-button.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/defence-button.svg
new file mode 100644
index 000000000..8792d3a02
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/defence-button.svg
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/defence.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/defence.svg
new file mode 100644
index 000000000..996340cdb
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/defence.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/dodge.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/dodge.svg
new file mode 100644
index 000000000..325d7bc36
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/dodge.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/filled-cross.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/filled-cross.svg
new file mode 100644
index 000000000..184cb9b39
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/filled-cross.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/game-button-glow.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/game-button-glow.svg
new file mode 100644
index 000000000..c2fd3764e
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/game-button-glow.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/health.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/health.svg
new file mode 100644
index 000000000..df4e3a411
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/health.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/info.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/info.svg
new file mode 100644
index 000000000..ee9fb59cb
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/info.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/loader.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/loader.svg
new file mode 100644
index 000000000..2a26c8e8e
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/loader.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/magic-line.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/magic-line.svg
new file mode 100644
index 000000000..510560099
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/magic-line.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/search.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/search.svg
new file mode 100644
index 000000000..4d7b2462f
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/search.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/skull-big.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/skull-big.svg
new file mode 100644
index 000000000..96bf3b223
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/skull-big.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/ultimate-button.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/ultimate-button.svg
new file mode 100644
index 000000000..c0b9c3cf8
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/ultimate-button.svg
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/user-skull.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/user-skull.svg
new file mode 100644
index 000000000..1912459d7
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/user-skull.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/user-smile.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/user-smile.svg
new file mode 100644
index 000000000..e02f04a23
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/icons/user-smile.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/index.ts b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/index.ts
new file mode 100644
index 000000000..6c80e2098
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/index.ts
@@ -0,0 +1,27 @@
+import backgroundSvg from './background.svg';
+import timerLightSvg from './timer-light.svg';
+export { timerLightSvg };
+export { backgroundSvg };
+export { ReactComponent as AdminIcon } from './icons/admin.svg';
+export { ReactComponent as SearchIcon } from './icons/search.svg';
+export { ReactComponent as CodeSlashIcon } from './icons/code-slash.svg';
+export { ReactComponent as ArrowRightIcon } from './icons/arrow-right.svg';
+export { ReactComponent as MagicLineIcon } from './icons/magic-line.svg';
+export { ReactComponent as HealthIcon } from './icons/health.svg';
+export { ReactComponent as AttackIcon } from './icons/attack.svg';
+export { ReactComponent as DefenceIcon } from './icons/defence.svg';
+export { ReactComponent as AttackButtonIcon } from './icons/attack-button.svg';
+export { ReactComponent as DefenceButtonIcon } from './icons/defence-button.svg';
+export { ReactComponent as UltimateButtonIcon } from './icons/ultimate-button.svg';
+export { ReactComponent as DodgeIcon } from './icons/dodge.svg';
+export { ReactComponent as CopyIcon } from './icons/copy.svg';
+export { ReactComponent as InfoIcon } from './icons/info.svg';
+export { ReactComponent as GameButtonGlowSvg } from './icons/game-button-glow.svg';
+export { ReactComponent as LoaderIcon } from './icons/loader.svg';
+export { ReactComponent as UserSmileIcon } from './icons/user-smile.svg';
+export { ReactComponent as UserSkullIcon } from './icons/user-skull.svg';
+export { ReactComponent as SkullBigIcon } from './icons/skull-big.svg';
+export { ReactComponent as CupStarIcon } from './icons/cup-star.svg';
+export { ReactComponent as CaretRightIcon } from './icons/caret-right.svg';
+export { ReactComponent as FilledCrossIcon } from './icons/filled-cross.svg';
+export * from './character';
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/timer-dark.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/timer-dark.svg
new file mode 100644
index 000000000..a56d6fff2
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/timer-dark.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/assets/images/timer-light.svg b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/timer-light.svg
new file mode 100644
index 000000000..1d27521e4
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/assets/images/timer-light.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/animations/fireball/fireball.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/animations/fireball/fireball.tsx
new file mode 100644
index 000000000..4abfc1b4d
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/animations/fireball/fireball.tsx
@@ -0,0 +1,219 @@
+import { BattleHistory } from '@/features/game/types';
+import React, { useRef, useEffect } from 'react';
+
+interface Particle {
+ index: number;
+ x: number;
+ y: number;
+ r: number;
+ o: number;
+ c: string;
+ xv: number;
+ yv: number;
+ rv: number;
+ ov: number;
+}
+
+interface Fireball {
+ index: number;
+ x: number;
+ y: number;
+ xv: number;
+ yv: number;
+ life: number;
+ reflectLeft?: number;
+ reflectRight?: number;
+}
+
+type FireballCanvasProps = {
+ lastTurnHistory: BattleHistory;
+};
+
+export const FireballCanvas: React.FC = ({ lastTurnHistory }) => {
+ const canvasRef = useRef(null);
+ const fireballs = useRef<{ [key: number]: Fireball }>({});
+ const particles = useRef<{ [key: number]: Particle }>({});
+ const nextFireballIndex = useRef(0);
+ const nextParticleIndex = useRef(0);
+ const o = useRef({ x: 0, y: 0 });
+ const edge = useRef({ top: 0, right: 0, bottom: 0, left: 0 });
+
+ const gravity = 0.025;
+ const particleColor = '#f73';
+ const radius = 11;
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ const ctx = canvas?.getContext('2d');
+
+ if (!canvas || !ctx) return;
+
+ const resize = () => {
+ const width = (canvas.width = canvas.clientWidth);
+ const height = (canvas.height = canvas.clientHeight);
+ o.current = { x: Math.floor(width / 2), y: Math.floor(height / 2) };
+ edge.current = {
+ top: -o.current.y,
+ right: width - o.current.x,
+ bottom: height - o.current.y,
+ left: -o.current.x,
+ };
+ };
+
+ resize();
+ window.addEventListener('resize', resize);
+
+ const newParticle = (
+ x: number,
+ y: number,
+ r: number,
+ o: number,
+ c: string,
+ xv: number,
+ yv: number,
+ rv: number,
+ ov: number,
+ ) => {
+ const index = ++nextParticleIndex.current;
+ particles.current[index] = { index, x, y, r, o, c, xv, yv, rv, ov };
+ };
+
+ const loop = () => {
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
+ ctx.globalCompositeOperation = 'source-over';
+ ctx.globalAlpha = 1;
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+ ctx.translate(o.current.x, o.current.y);
+
+ ctx.globalCompositeOperation = 'lighter';
+ for (const i in particles.current) {
+ const p = particles.current[i];
+ ctx.beginPath();
+ ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
+ ctx.globalAlpha = p.o;
+ ctx.fillStyle = p.c;
+ ctx.fill();
+ }
+
+ for (const i in particles.current) {
+ const p = particles.current[i];
+ p.x += p.xv;
+ p.y += p.yv;
+ p.r += p.rv;
+ p.o += p.ov;
+ if (p.r < 0 || p.o < 0) delete particles.current[p.index];
+ }
+
+ for (const i in fireballs.current) {
+ const f = fireballs.current[i];
+ const numParticles = Math.max(Math.sqrt(f.xv * f.xv + f.yv * f.yv) / 5, 1);
+ const numParticlesInt = Math.ceil(numParticles);
+ const numParticlesDif = numParticles / numParticlesInt;
+
+ for (let j = 0; j < numParticlesInt; j++) {
+ newParticle(
+ f.x - (f.xv * j) / numParticlesInt,
+ f.y - (f.yv * j) / numParticlesInt,
+ radius,
+ numParticlesDif,
+ particleColor,
+ Math.random() * 0.6 - 0.3,
+ Math.random() * 0.6 - 0.3,
+ -0.3,
+ -0.05 * numParticlesDif,
+ );
+ }
+
+ f.x += f.xv;
+ f.y += f.yv;
+ f.yv += gravity;
+
+ let boundary;
+ if (f.y < (boundary = edge.current.top + 7)) {
+ f.y = boundary;
+ f.yv *= -1;
+ } else if (f.y > (boundary = edge.current.bottom - 7)) {
+ f.y = boundary;
+ f.yv *= -1;
+ }
+ if (f.x > (boundary = f.reflectLeft || edge.current.right - 7)) {
+ f.x = boundary;
+ f.xv *= -1.5;
+ f.yv *= -1;
+ } else if (f.x < (boundary = f.reflectRight || edge.current.left + 7)) {
+ f.x = boundary;
+ f.xv *= -1.5;
+ f.yv *= -1;
+ }
+
+ if (--f.life < 0) delete fireballs.current[f.index];
+ }
+
+ requestAnimationFrame(loop);
+ };
+
+ loop();
+
+ return () => {
+ window.removeEventListener('resize', resize);
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const launchFireball = (
+ x: number,
+ y: number,
+ xv: number,
+ yv: number,
+ life: number,
+ reflectLeft?: number,
+ reflectRight?: number,
+ ) => {
+ fireballs.current[++nextFireballIndex.current] = {
+ index: nextFireballIndex.current,
+ x,
+ y,
+ xv,
+ yv,
+ life,
+ reflectLeft,
+ reflectRight,
+ };
+ };
+
+ useEffect(() => {
+ const playerAttack = ['Attack', 'Ultimate'].includes(lastTurnHistory.player.action || '');
+ const opponentAttack = ['Attack', 'Ultimate'].includes(lastTurnHistory.opponent.action || '');
+
+ if (playerAttack) {
+ if (lastTurnHistory.opponent.isDodged) {
+ launchFireball(-250, 200, 2.46, -3.03, 400);
+ } else {
+ if (lastTurnHistory.opponent.action === 'Reflect') {
+ launchFireball(-250, 200, 2.46, -3.03, 350, 230);
+ } else {
+ launchFireball(-250, 200, 2.46, -3.03, 230);
+ }
+ }
+ }
+ if (opponentAttack) {
+ if (lastTurnHistory.player.isDodged) {
+ launchFireball(300, 200, -2.46, -3.03, 410);
+ } else {
+ if (lastTurnHistory.player.action === 'Reflect') {
+ launchFireball(300, 200, -2.46, -3.03, 350, undefined, -180);
+ } else {
+ launchFireball(300, 200, -2.46, -3.03, 250);
+ }
+ }
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+
+
+
+ );
+};
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/animations/index.ts b/frontend/apps/web3-warriors-battle/src/features/game/components/animations/index.ts
new file mode 100644
index 000000000..184db0d72
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/animations/index.ts
@@ -0,0 +1,2 @@
+export { FireballCanvas } from './fireball/fireball';
+export { SphereAnimation } from './sphere/sphere';
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/animations/sphere/sphere.module.scss b/frontend/apps/web3-warriors-battle/src/features/game/components/animations/sphere/sphere.module.scss
new file mode 100644
index 000000000..4f00b0f0c
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/animations/sphere/sphere.module.scss
@@ -0,0 +1,22 @@
+.attack {
+ display: block;
+ border-radius: 50%;
+ mix-blend-mode: overlay;
+ background-color: #676767;
+}
+
+.ultimate {
+ display: block;
+ border-radius: 50%;
+ box-shadow: 0px 4px 76px 0px #00ffc4 inset;
+ background: #00ffc41a;
+ mix-blend-mode: plus-lighter;
+}
+
+.reflect {
+ display: block;
+ border-radius: 50%;
+ box-shadow: 0px 28px 100px 0px #16981b inset;
+ background: radial-gradient(60.28% 60.28% at 85.56% 31.67%, #56750a 0%, rgba(36, 49, 3, 0.1) 100%);
+ mix-blend-mode: plus-lighter;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/animations/sphere/sphere.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/animations/sphere/sphere.tsx
new file mode 100644
index 000000000..bf8ca7c28
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/animations/sphere/sphere.tsx
@@ -0,0 +1,129 @@
+import clsx from 'clsx';
+import React, { useEffect, useRef } from 'react';
+import styles from './sphere.module.scss';
+import { Move } from '@/app/utils';
+
+const PARTICLE_RADIUS = 20;
+
+interface Particle {
+ speed: { x: number; y: number };
+ location: { x: number; y: number };
+ radius: number;
+ life: number;
+ remaining_life: number;
+ r: number;
+ g: number;
+ b: number;
+ opacity?: number;
+}
+
+class ParticleFlame implements Particle {
+ speed;
+ location;
+ radius;
+ life;
+ remaining_life;
+ r;
+ g;
+ b;
+
+ constructor(W: number, H: number, flamewidth: number, type: Move) {
+ this.speed = { x: -2.5 + Math.random() * 5, y: -15 + Math.random() * 10 };
+ const locmin = W / 2 - flamewidth / 2;
+ const locmax = W / 2 + flamewidth / 2;
+ this.location = { x: Math.random() * (locmax - locmin) + locmin, y: H };
+
+ this.radius = Math.random() * PARTICLE_RADIUS + PARTICLE_RADIUS;
+ this.life = 10 + Math.random() * 10;
+ this.remaining_life = this.life;
+
+ if (type === 'Attack') {
+ this.r = 255;
+ this.g = Math.round(Math.random() * 90 + 100);
+ this.b = Math.round(Math.random() * 20 + 10);
+ } else {
+ this.r = Math.round(Math.random() * 30 + 215);
+ this.g = Math.round(Math.random() * 20 + 215);
+ this.b = 255;
+ }
+ }
+}
+
+type FireAnimationProps = {
+ type?: Move | null;
+ className?: string;
+};
+
+export const SphereAnimation: React.FC = ({ type, className }) => {
+ const canvasRef = useRef(null);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return;
+
+ const W = 360;
+ const H = 360;
+ canvas.width = W;
+ canvas.height = H;
+
+ if (!type || type === 'Reflect') {
+ ctx.clearRect(0, 0, W, H);
+ return;
+ }
+
+ const particles: Particle[] = [];
+ const particle_count = 100;
+ const flamewidth = 360;
+
+ for (let i = 0; i < particle_count; i++) {
+ particles.push(new ParticleFlame(W, H, flamewidth, type));
+ }
+
+ function drawFlames() {
+ if (!ctx || !type) return;
+ ctx.globalCompositeOperation = 'source-over';
+ ctx.clearRect(0, 0, W, H);
+ ctx.globalCompositeOperation = 'lighter';
+
+ for (let i = 0; i < particles.length; i++) {
+ const p = particles[i];
+ ctx.beginPath();
+ p.opacity = Math.round((p.remaining_life / p.life) * 100) / 100;
+ const gradient = ctx.createRadialGradient(p.location.x, p.location.y, 0, p.location.x, p.location.y, p.radius);
+ gradient.addColorStop(0, `rgba(${p.r}, ${p.g}, ${p.b}, ${p.opacity})`);
+ gradient.addColorStop(0.5, `rgba(${p.r}, ${p.g}, ${p.b}, ${p.opacity})`);
+ gradient.addColorStop(1, `rgba(${p.r}, ${p.g}, ${p.b}, 0)`);
+ ctx.fillStyle = gradient;
+ ctx.arc(p.location.x, p.location.y, p.radius, Math.PI * 2, 0, false);
+ ctx.fill();
+
+ p.remaining_life--;
+ p.radius--;
+ p.location.x += p.speed.x;
+ p.location.y += p.speed.y;
+
+ if (p.remaining_life < 0 || p.radius < 0) {
+ particles[i] = new ParticleFlame(W, H, flamewidth, type);
+ }
+ }
+ }
+
+ const interval = setInterval(drawFlames, 33);
+
+ return () => clearInterval(interval);
+ }, [type]);
+
+ return (
+
+ );
+};
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/avatar/avatar.module.scss b/frontend/apps/web3-warriors-battle/src/features/game/components/avatar/avatar.module.scss
new file mode 100644
index 000000000..8a7b3244d
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/avatar/avatar.module.scss
@@ -0,0 +1,35 @@
+.container {
+ height: 100%;
+}
+
+.wrapper {
+ position: relative;
+ width: 80px;
+ height: 100%;
+ overflow: hidden;
+ display: flex;
+ align-items: flex-start;
+ justify-content: center;
+ & > svg {
+ position: absolute;
+ top: 0;
+ left: -9px;
+ width: 100px;
+ height: 98px;
+ }
+}
+
+.md {
+ &.container {
+ width: 100%;
+ }
+ .wrapper {
+ height: 80%;
+ width: 100%;
+ }
+ svg {
+ width: 124px;
+ height: 163px;
+ left: -3px;
+ }
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/avatar/avatar.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/avatar/avatar.tsx
new file mode 100644
index 000000000..afef21d3c
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/avatar/avatar.tsx
@@ -0,0 +1,32 @@
+import styles from './avatar.module.scss';
+import { VariantProps, cva } from 'class-variance-authority';
+import { CharacterView } from '../character/character';
+import { getLazySvg } from '../../utils';
+import { Suspense, memo } from 'react';
+import { BodyColor } from '../../assets/images';
+
+export const variants = cva('', {
+ variants: { size: { md: styles.md, sm: styles.sm } },
+ defaultVariants: { size: 'md' },
+});
+
+type AvatarProps = VariantProps & CharacterView;
+
+export const Avatar = memo((props: AvatarProps) => {
+ const { size, hat_index, head_index, body_color } = props;
+
+ const Hat = getLazySvg('hat', hat_index);
+ const Head = getLazySvg('head', head_index);
+
+ return (
+
+
+
+ );
+});
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/avatar/index.ts b/frontend/apps/web3-warriors-battle/src/features/game/components/avatar/index.ts
new file mode 100644
index 000000000..c082aa971
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/avatar/index.ts
@@ -0,0 +1 @@
+export { Avatar } from './avatar';
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/background/background.module.scss b/frontend/apps/web3-warriors-battle/src/features/game/components/background/background.module.scss
new file mode 100644
index 000000000..df2bfd401
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/background/background.module.scss
@@ -0,0 +1,25 @@
+@use '@/utils' as *;
+
+.content {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ flex-direction: column;
+ height: 100%;
+ min-height: 600px;
+ width: 100%;
+ max-width: 1440px;
+ margin: 0 auto;
+ border-bottom: 3px solid #000;
+}
+
+.image {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ max-height: 600px;
+ object-fit: cover;
+ overflow: visible;
+ z-index: -1;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/background/background.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/background/background.tsx
new file mode 100644
index 000000000..ec17f1651
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/background/background.tsx
@@ -0,0 +1,17 @@
+import { PropsWithChildren } from 'react';
+import styles from './background.module.scss';
+import { backgroundSvg } from '../../assets/images';
+import clsx from 'clsx';
+
+type BackgroundProps = PropsWithChildren & {
+ className?: string;
+};
+
+export function Background({ children, className }: BackgroundProps) {
+ return (
+
+
+ {children}
+
+ );
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/background/index.ts b/frontend/apps/web3-warriors-battle/src/features/game/components/background/index.ts
new file mode 100644
index 000000000..e0e570763
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/background/index.ts
@@ -0,0 +1 @@
+export { Background } from './background';
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/battle-card/battle-card.module.scss b/frontend/apps/web3-warriors-battle/src/features/game/components/battle-card/battle-card.module.scss
new file mode 100644
index 000000000..9a56bf36b
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/battle-card/battle-card.module.scss
@@ -0,0 +1,91 @@
+.wrapper {
+ background: #fff;
+
+ display: flex;
+ gap: 16px;
+ width: 456px;
+ border: 1.5px solid #0b0b0b;
+
+ &.left {
+ padding: 9px 21px 9px 23px;
+ border-radius: 10px 0 0 10px;
+ border-right: 0.75px solid #0b0b0b;
+ }
+ &.right {
+ padding: 9px 23px 9px 21px;
+ border-radius: 0 10px 10px 0;
+ border-left: 0.75px solid #0b0b0b;
+ }
+}
+
+.info {
+ flex: 1;
+}
+
+.header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 4px;
+}
+
+.winsCount {
+ height: 26px;
+ padding: 5px 8px;
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 7px;
+ background: #00ffc412;
+ color: #0ed3a3;
+}
+
+.healthCount {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ width: 72px;
+ margin-right: 16px;
+}
+
+.stats {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ height: 17px;
+ margin-top: 12px;
+ margin-bottom: 4px;
+}
+
+.stat {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ width: 60px;
+}
+
+.attackStat {
+ width: 52px;
+}
+
+.icon {
+ color: #cfcfcf;
+}
+
+.attack {
+ color: #db4242;
+}
+
+.reflect {
+ color: #9cc53c;
+}
+
+.dodgeStat {
+ gap: 2px;
+ width: 91px;
+}
+
+.dodge {
+ color: #ffad33;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/battle-card/battle-card.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/battle-card/battle-card.tsx
new file mode 100644
index 000000000..692dccbdf
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/battle-card/battle-card.tsx
@@ -0,0 +1,67 @@
+import { VariantProps, cva } from 'class-variance-authority';
+import clsx from 'clsx';
+import { Text } from '@/components';
+import { AttackIcon, CupStarIcon, DefenceIcon, DodgeIcon, HealthIcon } from '../../assets/images';
+import { Avatar } from '../avatar';
+import { CharacterView } from '../character/character';
+import styles from './battle-card.module.scss';
+import { PlayerSettings } from '@/app/utils';
+
+export const variants = cva('', {
+ variants: { align: { left: styles.left, right: styles.right } },
+ defaultVariants: { align: 'left' },
+});
+
+type BattleCardProps = PlayerSettings &
+ VariantProps
& {
+ name: string;
+ winsCount?: number;
+ characterView: CharacterView;
+ };
+
+const BattleCard = ({ align, name, attack, health, defence, dodge, winsCount = 0, characterView }: BattleCardProps) => {
+ return (
+
+
+
+
+
{name}
+
+
+
+ {winsCount}
+
+
+
+
+
+
+
+ {health}
+
+
+
+
+
+
+ {defence}%
+
+
+
+
+
+ {dodge}%
+
+
+
+
+
+ );
+};
+
+export { BattleCard };
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/battle-card/index.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/battle-card/index.tsx
new file mode 100644
index 000000000..2346fdbaa
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/battle-card/index.tsx
@@ -0,0 +1 @@
+export { BattleCard } from './battle-card';
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/battle-history-card/battle-history-card.module.scss b/frontend/apps/web3-warriors-battle/src/features/game/components/battle-history-card/battle-history-card.module.scss
new file mode 100644
index 000000000..fb6add9fe
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/battle-history-card/battle-history-card.module.scss
@@ -0,0 +1,136 @@
+.wrapper {
+ background: #fff;
+
+ width: 456px;
+ border: 1.5px solid #0b0b0b;
+
+ &.left {
+ padding: 11px 21px 12px 23px;
+ border-radius: 10px 0 0 10px;
+ border-right: 0.75px solid #0b0b0b;
+ }
+ &.right {
+ padding: 11px 23px 12px 21px;
+ border-radius: 0 10px 10px 0;
+ border-left: 0.75px solid #0b0b0b;
+ }
+}
+
+.header {
+ display: flex;
+ justify-content: space-between;
+}
+
+.action-Attack {
+ color: #ca3d3d;
+ font-weight: 700;
+ text-transform: capitalize;
+}
+
+.action-Ultimate {
+ color: #0ed3a3;
+ font-weight: 700;
+ text-transform: capitalize;
+}
+
+.action-Reflect {
+ color: #7ea732;
+ font-weight: 700;
+ text-transform: capitalize;
+}
+
+.healthRow {
+ display: flex;
+}
+
+.healthCount {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ width: 72px;
+ margin-right: 16px;
+}
+
+.receivedDamage {
+ color: #ca445f;
+}
+
+.recivedText {
+ line-height: 22px;
+ margin-left: auto;
+}
+
+.stats {
+ display: flex;
+ align-items: center;
+ gap: 24px;
+ height: 22px;
+ margin-top: 2px;
+}
+
+.stat {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ width: 53px;
+
+ &:first-child {
+ width: 63px;
+ }
+}
+
+.icon {
+ color: #cfcfcf;
+}
+
+.attack {
+ color: #db4242;
+}
+
+.ultimate {
+ color: #00ffc4;
+}
+
+.reflect {
+ color: #9cc53c;
+}
+
+.dodgeStat {
+ gap: 2px;
+ width: 91px;
+}
+
+.dodge {
+ color: #ffad33;
+}
+
+.dodged {
+ color: #a5a5a5;
+ flex: 1;
+ text-align: right;
+ line-height: 22px;
+}
+
+.healthCount {
+ display: flex;
+ align-items: center;
+ gap: 9px;
+}
+
+.cross {
+ position: absolute;
+ top: -13px;
+ right: -11px;
+ background: #fff;
+ border: 2px solid #000000;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 32px;
+ width: 32px;
+ svg {
+ height: 18px;
+ width: 18px;
+ }
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/battle-history-card/battle-history-card.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/battle-history-card/battle-history-card.tsx
new file mode 100644
index 000000000..7912d387b
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/battle-history-card/battle-history-card.tsx
@@ -0,0 +1,100 @@
+import clsx from 'clsx';
+import { Text } from '@/components';
+import { AttackIcon, DefenceIcon, DodgeIcon, HealthIcon } from '../../assets/images';
+import { HealthIndicator } from '../health-indicator';
+import { VariantProps, cva } from 'class-variance-authority';
+import { CrossIcon } from '@/assets/images';
+import { Move } from '@/app/utils';
+import { PlayerState } from '../../types';
+import { PlayerStatus } from '../player-status/player-status';
+import styles from './battle-history-card.module.scss';
+
+export const variants = cva('', {
+ variants: { align: { left: styles.left, right: styles.right } },
+ defaultVariants: { align: 'left' },
+});
+
+type BattleHistoryCardProps = Omit &
+ VariantProps & {
+ onClose?: () => void;
+ action: Move | null;
+ };
+
+const BattleHistoryCard = ({
+ align,
+ action,
+ attack,
+ health,
+ defence,
+ dodge,
+ isDodged,
+ receivedDamage,
+ name,
+ onClose,
+}: BattleHistoryCardProps) => {
+ const isAlive = health > 0;
+
+ return (
+
+
+
+ {action ? (
+ <>
+ {name} uses {action}
+ >
+ ) : (
+ {name}
+ )}
+
+
+
+
+
+
+
+
+ {health} {receivedDamage > 0 && (-{receivedDamage}) }
+
+
+
+
+ {Boolean(receivedDamage) &&
received {receivedDamage} damage }
+
+
+
+
+
+ {attack} {action === 'Ultimate' && '(x2)'}
+
+
+
+
+
+ {defence}%
+
+
+
+
+
+ {dodge}% chance
+
+
+ {isDodged &&
Dodged }
+
+
+ {onClose && (
+
+
+
+ )}
+
+ );
+};
+
+export { BattleHistoryCard };
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/battle-history-card/index.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/battle-history-card/index.tsx
new file mode 100644
index 000000000..fa6e2e821
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/battle-history-card/index.tsx
@@ -0,0 +1 @@
+export { BattleHistoryCard } from './battle-history-card';
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/battle-history-sync/battle-history-sync.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/battle-history-sync/battle-history-sync.tsx
new file mode 100644
index 000000000..29f7d0084
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/battle-history-sync/battle-history-sync.tsx
@@ -0,0 +1,7 @@
+import { UsePrepareBattleHistoryParams, usePrepareBattleHistory } from '../../hooks';
+
+export const BattleHistorySinc = (props: UsePrepareBattleHistoryParams) => {
+ usePrepareBattleHistory(props);
+
+ return null;
+};
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/battle-history-sync/index.ts b/frontend/apps/web3-warriors-battle/src/features/game/components/battle-history-sync/index.ts
new file mode 100644
index 000000000..5855bc7b3
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/battle-history-sync/index.ts
@@ -0,0 +1 @@
+export { BattleHistorySinc } from './battle-history-sync';
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/character-stats-form/character-stats-form.module.scss b/frontend/apps/web3-warriors-battle/src/features/game/components/character-stats-form/character-stats-form.module.scss
new file mode 100644
index 000000000..a0e7b1621
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/character-stats-form/character-stats-form.module.scss
@@ -0,0 +1,147 @@
+.container {
+ color: #ffffffcc;
+}
+
+.stats {
+ margin-top: 40px;
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+}
+
+.row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ height: 22px;
+ > :first-child {
+ color: #00ffc4;
+ }
+
+ button {
+ color: #00ffc4;
+ &:disabled {
+ color: #58696e;
+ opacity: 1;
+ }
+ }
+}
+
+.title {
+ color: #ffffff;
+ margin-bottom: 8px;
+}
+
+.text {
+ text-transform: capitalize;
+ margin-left: 8px;
+ flex: 1;
+ color: #ffffff;
+ text-align: left;
+}
+
+.description {
+ margin-top: 4px;
+}
+
+.points {
+ font-weight: 700;
+ color: #00ffc4;
+}
+
+.max {
+ color: #00ffc4;
+ margin-left: 8px;
+ font-weight: 500;
+ font-size: 12px;
+}
+
+.arrowLeft {
+ transform: rotate(180deg);
+}
+
+.input {
+ position: relative;
+ width: 38px;
+ height: 22px;
+
+ input {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 38px;
+ height: 22px;
+ color: #ffffff;
+ background: transparent;
+ font-size: 14px;
+ font-weight: 700;
+ line-height: 16.8px;
+ letter-spacing: 0.02em;
+ text-align: center;
+
+ &:focus {
+ outline: none;
+ }
+ }
+
+ /* remove input arrows */
+ /* Chrome, Safari, Edge, Opera */
+ & input::-webkit-outer-spin-button,
+ & input::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+
+ /* Firefox */
+ & input[type='number'] {
+ -moz-appearance: textfield;
+ }
+}
+
+.border {
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ right: 0px;
+ bottom: 0px;
+ background: transparent;
+ pointer-events: none;
+}
+
+.input:before,
+.input:after,
+.border:before,
+.border:after {
+ position: absolute;
+ pointer-events: none;
+ width: 9px;
+ height: 9px;
+ content: '';
+ border-color: #58696e;
+ border-style: solid;
+ border-radius: 1px;
+}
+
+.input:before {
+ left: 0px;
+ top: 0px;
+ border-width: 1.5px 0 0 1.5px;
+}
+
+.input:after {
+ right: 0px;
+ top: 0px;
+ border-width: 1.5px 1.5px 0 0;
+}
+
+.border:before {
+ right: 0px;
+ bottom: 0px;
+ border-width: 0 1.5px 1.5px 0;
+}
+
+.border:after {
+ left: 0px;
+ bottom: 0px;
+ border-width: 0 0 1.5px 1.5px;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/character-stats-form/character-stats-from.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/character-stats-form/character-stats-from.tsx
new file mode 100644
index 000000000..f5683eef0
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/character-stats-form/character-stats-from.tsx
@@ -0,0 +1,152 @@
+import { useEffect } from 'react';
+import { Button } from '@gear-js/vara-ui';
+import { useForm } from '@mantine/form';
+import { Text } from '@/components';
+import { Heading } from '@/components/ui/heading';
+import { AttackIcon, CaretRightIcon, DefenceIcon, DodgeIcon } from '../../assets/images';
+import { CharacterStatsFormValues } from '../../types';
+import { characterStatsStorage } from '../../store';
+import styles from './character-stats-form.module.scss';
+
+type Stats = 'attack' | 'defence' | 'dodge';
+
+type CharacterStats = {
+ icon: React.ReactNode;
+ name: Stats;
+ description: string;
+ maxCount: number;
+ minCount: number;
+ percentPerPoint?: number;
+};
+
+const charStats: CharacterStats[] = [
+ {
+ icon: ,
+ name: 'attack',
+ description: 'The strength of the damage you do to the opponent.',
+ maxCount: 20,
+ minCount: 10,
+ },
+ {
+ icon: ,
+ name: 'defence',
+ description: "Reflects the opponent's attack back to them. Each point equals 10%.",
+ percentPerPoint: 10,
+ maxCount: 10,
+ minCount: 0,
+ },
+ {
+ icon: ,
+ name: 'dodge',
+ description: 'The chance to fully evade opponent’s attack. Each point increases chance by 4%.',
+ percentPerPoint: 4,
+ maxCount: 10,
+ minCount: 0,
+ },
+];
+
+type CharacterStatsFormProps = {
+ onValuesChange?: (values: CharacterStatsFormValues, isValid: boolean) => void;
+};
+
+export const CharacterStatsForm = ({ onValuesChange }: CharacterStatsFormProps) => {
+ const statsForm = useForm({
+ initialValues: characterStatsStorage.get() || {
+ attack: 10,
+ defence: 0,
+ dodge: 0,
+ },
+ transformValues: (values) => {
+ return {
+ attack: Math.min(values.attack, charStats[0].maxCount),
+ defence: Math.min(values.defence, charStats[1].maxCount),
+ dodge: Math.min(values.dodge, charStats[2].maxCount),
+ };
+ },
+ });
+
+ const { getInputProps, setFieldValue, values } = statsForm;
+ const initialPoints = 10;
+ const availablePoints = 20 + initialPoints - values.attack - values.defence - values.dodge;
+ const displayedAvailablePoints = Math.min(20, Math.max(availablePoints, 0));
+
+ useEffect(() => {
+ const isValid = availablePoints === 0;
+ onValuesChange?.(values, isValid);
+ }, [values, availablePoints, onValuesChange]);
+
+ const drawRow = ({ icon, name, percentPerPoint, maxCount, minCount, description }: CharacterStats) => {
+ const getValidCount = (count: number) => {
+ const negative = Math.min(availablePoints, 0);
+ return Math.max(minCount, Math.min(Number(count) + negative, maxCount));
+ };
+
+ const value = values[name];
+
+ return (
+
+
+ {icon}
+
+ {name} {percentPerPoint && ({percentPerPoint * Math.min(value, maxCount)}%) }:
+
+
+
setFieldValue(name, value - 1)}
+ />
+
+
{
+ setFieldValue(name, getValidCount(Number(event.currentTarget.value)));
+ }}
+ />
+
+
+ setFieldValue(name, value + 1)}
+ disabled={values[name] >= maxCount || availablePoints === 0}
+ />
+
+ setFieldValue(name, Math.min(maxCount, value + availablePoints))}>
+
+ {maxCount} max
+
+
+
+
+ {description}
+
+
+ );
+ };
+
+ return (
+
+
+ Set Character's Attributes
+
+
+
+ {displayedAvailablePoints} points are available to distribute.
+
+
+
+
+ );
+};
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/character-stats-form/index.ts b/frontend/apps/web3-warriors-battle/src/features/game/components/character-stats-form/index.ts
new file mode 100644
index 000000000..d0c3b7ab5
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/character-stats-form/index.ts
@@ -0,0 +1 @@
+export { CharacterStatsForm } from './character-stats-from';
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/character-stats/character-stats.module.scss b/frontend/apps/web3-warriors-battle/src/features/game/components/character-stats/character-stats.module.scss
new file mode 100644
index 000000000..588e3b267
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/character-stats/character-stats.module.scss
@@ -0,0 +1,163 @@
+.container {
+ position: absolute;
+ top: 48px;
+ display: flex;
+ height: 120px;
+ width: 458px;
+ align-self: normal;
+ color: #fff;
+ &.left {
+ left: 120px;
+ }
+
+ &.right {
+ right: 120px;
+ }
+}
+
+.top {
+ position: absolute;
+ width: 340px;
+ height: 68px;
+ top: 8px;
+
+ .vector {
+ position: absolute;
+ top: 0;
+ color: #4b4b55;
+ z-index: -1;
+ }
+}
+
+.left .top {
+ left: 0;
+}
+.right .top {
+ right: 0;
+}
+
+.right .vector {
+ transform: scaleX(-1);
+}
+
+.bottom {
+ position: absolute;
+
+ width: 340px;
+ height: 68px;
+
+ &.left {
+ left: 28px;
+ }
+ &.right {
+ right: 28px;
+ }
+
+ top: 39.5px;
+ .vector {
+ position: absolute;
+ color: #3d3d47;
+ z-index: -1;
+ }
+}
+
+.name {
+ text-align: right;
+ margin: 8px 19px;
+ height: 22px;
+ line-height: 22px;
+ display: flex;
+ gap: 8px;
+ color: #fff;
+ justify-content: flex-end;
+}
+
+.right .name {
+ justify-content: flex-start;
+}
+
+.health {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 2px 19px;
+ width: 287px;
+}
+.left .health {
+ text-align: left;
+ margin-left: auto;
+}
+.right .health {
+ text-align: left;
+ margin-right: auto;
+}
+
+.stats {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin: 46.5px 44px 0 auto;
+ width: 249px;
+}
+
+.stat {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.attack {
+ color: #db4242;
+}
+
+.defence {
+ color: #9cc53c;
+}
+
+.dodgeStat {
+ gap: 4px;
+}
+
+.dodge {
+ color: #ffad33;
+}
+
+.healthCount {
+ display: flex;
+ align-items: center;
+ gap: 9px;
+}
+
+.avatar {
+ position: absolute;
+ top: 0;
+ height: 120px;
+ width: 120px;
+ background: #595964;
+ border: 1.5px solid #0b0b0b;
+ border-radius: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &.left {
+ right: 0;
+ }
+
+ &.right {
+ left: 0;
+ }
+}
+
+.active {
+ background: #00ffc4;
+ box-shadow:
+ 0px 2.4px 4.8px 0px #00ffc466,
+ 0px 0.6px 1.8px 0px #00000033 inset;
+}
+
+.defeated {
+ .stat,
+ .healthCount {
+ opacity: 0.2;
+ }
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/character-stats/character-stats.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/character-stats/character-stats.tsx
new file mode 100644
index 000000000..c3eadb671
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/character-stats/character-stats.tsx
@@ -0,0 +1,86 @@
+import clsx from 'clsx';
+import { VariantProps, cva } from 'class-variance-authority';
+import { Text } from '@/components';
+import { AttackIcon, DefenceIcon, DodgeIcon, HealthIcon, SkullBigIcon } from '../../assets/images';
+import { ReactComponent as VectorIcon } from './vector.svg';
+import { HealthIndicator } from '../health-indicator';
+import { Avatar } from '../avatar';
+import { CharacterView } from '../character/character';
+import { PlayerSettings } from '@/app/utils';
+import styles from './character-stats.module.scss';
+
+export const variants = cva('', {
+ variants: { align: { left: styles.left, right: styles.right }, status: { defeated: styles.defeated, alive: null } },
+ defaultVariants: { align: 'left', status: 'alive' },
+});
+
+type CharacterStatsProps = VariantProps &
+ PlayerSettings & {
+ name: string;
+ characterView: CharacterView;
+ isActive?: boolean;
+ className?: string;
+ };
+
+export const CharacterStats = ({
+ className,
+ align,
+ name,
+ attack,
+ health,
+ defence,
+ dodge,
+ characterView,
+ isActive = false,
+}: CharacterStatsProps) => {
+ const status = health === 0 ? 'defeated' : 'alive';
+
+ return (
+
+
+
+
+
+
+
+
+ {defence}%
+
+
+
+
+
+ {dodge}% chance
+
+
+
+
+
+
+
+ {name}
+
+ {status === 'defeated' && }
+
+
+
+
+
+
+ );
+};
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/character-stats/index.ts b/frontend/apps/web3-warriors-battle/src/features/game/components/character-stats/index.ts
new file mode 100644
index 000000000..8fe3bdfb4
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/character-stats/index.ts
@@ -0,0 +1 @@
+export { CharacterStats } from './character-stats';
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/character-stats/vector.svg b/frontend/apps/web3-warriors-battle/src/features/game/components/character-stats/vector.svg
new file mode 100644
index 000000000..837368d6b
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/character-stats/vector.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/character/character.module.scss b/frontend/apps/web3-warriors-battle/src/features/game/components/character/character.module.scss
new file mode 100644
index 000000000..1061c4beb
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/character/character.module.scss
@@ -0,0 +1,71 @@
+.container {
+ position: relative;
+ height: 340px;
+ width: 340px;
+
+ & > svg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 340px;
+ width: 340px;
+ }
+}
+
+.sm {
+ height: 279px;
+ width: 279px;
+ & > svg {
+ height: 279px;
+ width: 279px;
+ }
+}
+
+.loader {
+ position: absolute;
+ top: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ > svg {
+ animation: rotateSpinner 2.5s linear infinite;
+ height: 48px;
+ width: 48px;
+ }
+}
+
+.loaderBackground {
+ background: #ffffff80;
+ backdrop-filter: blur(12px);
+}
+
+.loaderText {
+ margin-top: 6.5px;
+ color: #000000;
+}
+
+.fallback {
+ &::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(white 10%, rgba(255, 255, 255, 0) 5%),
+ linear-gradient(90deg, white 10%, rgba(255, 255, 255, 0) 5%),
+ linear-gradient(270deg, white 10%, rgba(255, 255, 255, 0) 5%),
+ linear-gradient(0deg, white 10%, rgba(255, 255, 255, 0) 5%);
+ pointer-events: none;
+ border-radius: 50%;
+ }
+}
+
+@keyframes rotateSpinner {
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/character/character.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/character/character.tsx
new file mode 100644
index 000000000..bf73d2c11
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/character/character.tsx
@@ -0,0 +1,74 @@
+import clsx from 'clsx';
+import React, { Suspense, memo } from 'react';
+import { BackColor, BodyColor, LoaderIcon } from '../../assets/images';
+import { getLazySvg } from '../../utils';
+import { Text } from '@/components';
+import styles from './character.module.scss';
+import { Appearance } from '@/app/utils';
+
+export type CharacterView = Appearance;
+
+type FixedLengthArray = [T, ...T[]] & { length: L };
+export type AppearanceIdentifiers = FixedLengthArray;
+
+type CharacterProps = CharacterView & {
+ fallback?: React.ReactNode;
+ withSpiner?: boolean;
+ loaderBackground?: boolean;
+ size?: 'md' | 'sm';
+};
+
+export const Character = memo(
+ (props: CharacterProps) => {
+ const {
+ accessory_index,
+ body_index,
+ hat_index,
+ head_index,
+ body_color,
+ back_color,
+ fallback,
+ withSpiner = true,
+ loaderBackground = false,
+ size = 'md',
+ } = props;
+
+ const Hat = getLazySvg('hat', hat_index);
+ const Head = getLazySvg('head', head_index);
+ const Body = getLazySvg('body', body_index);
+ const Accessory = getLazySvg('accessories', accessory_index);
+
+ return (
+
+
+ {fallback && {fallback}
}
+ {withSpiner && (
+
+
+
+ Please wait
+
+
+ )}
+ >
+ }>
+
+
+
+
+
+
+
+
+ );
+ },
+ (prev, next) =>
+ prev.accessory_index === next.accessory_index &&
+ prev.back_color === next.back_color &&
+ prev.body_color === next.body_color &&
+ prev.body_index === next.body_index &&
+ prev.hat_index === next.hat_index &&
+ prev.head_index === next.head_index,
+);
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/character/index.ts b/frontend/apps/web3-warriors-battle/src/features/game/components/character/index.ts
new file mode 100644
index 000000000..b9c01d601
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/character/index.ts
@@ -0,0 +1 @@
+export { Character } from './character';
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/game-button/game-button.module.scss b/frontend/apps/web3-warriors-battle/src/features/game/components/game-button/game-button.module.scss
new file mode 100644
index 000000000..c6de78d88
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/game-button/game-button.module.scss
@@ -0,0 +1,267 @@
+.outer {
+ position: relative;
+ padding: 7px 8px;
+ border-radius: 12px;
+ width: 210px;
+ height: 70px;
+ overflow: hidden;
+
+ & > ::after {
+ content: '';
+ transition: all 0.3s ease;
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ }
+
+ &:disabled > ::after {
+ background: #000000cc;
+ }
+
+ &:hover:not(:disabled) > ::after {
+ background: #ffffff2d;
+ }
+
+ &.red {
+ background: linear-gradient(180deg, #d44f4f 0%, #8f3030 20.83%, #591414 94.27%);
+ }
+ &.green {
+ background: linear-gradient(180deg, #b0d44f 0%, #628f30 20.83%, #365914 94.27%);
+ }
+ &.cyan {
+ background: linear-gradient(180deg, #00ffc4 0%, #0ed3a3 20.83%, #145951 94.27%);
+ }
+ &.black {
+ background: linear-gradient(180deg, #4b4b55 0%, #464650 20.5%, #35353d 94.27%);
+ width: 144px;
+ height: 44px;
+ padding: 4px 5.5px 5px;
+ border-radius: 6px;
+ }
+}
+
+.inner {
+ transition: all 2s;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 16px;
+ border-radius: 8px;
+ color: #ddd;
+
+ width: 100%;
+ height: 100%;
+
+ box-shadow: 0px 1px 3px 0px #ffffff40 inset;
+}
+.red .inner {
+ background: linear-gradient(180deg, #ca3d3d 0%, #852626 100%);
+}
+.green .inner {
+ background: linear-gradient(180deg, #a1ca3d 0%, #5c8526 100%);
+}
+.cyan .inner {
+ background: linear-gradient(180deg, #00ffc4 0%, #008e6b 100%);
+}
+.black .inner {
+ background: linear-gradient(180deg, #494953 0%, #4a4a53 100%);
+ border-radius: 4px;
+}
+
+.pending {
+ pointer-events: none;
+ & .inner {
+ > svg:first-child {
+ animation: rotateSpinner 2.5s linear infinite;
+ }
+ }
+ &.red .inner {
+ background: #852626;
+ color: #ca3d3d;
+ }
+ &.green .inner {
+ background: #5c8526;
+ color: #a1ca3d;
+ }
+ &.cyan .inner {
+ background: #008e6b;
+ color: #03f8bf;
+ }
+}
+
+.text {
+ position: relative;
+ font-size: 23px;
+ font-weight: 700;
+ line-height: 29.9px;
+ letter-spacing: -0.025em;
+
+ // color: #fcd1d1;
+ color: transparent; /* Чтобы градиент применялся только к тексту */
+
+ // text-shadow: 0px 1px 0px #553211;
+ // text-shadow: 5px 5px #558ABB;\
+}
+.red .text {
+ background: linear-gradient(180deg, #ffffff 0%, #fcd1d1 100%);
+ background-clip: text;
+ -webkit-background-clip: text;
+}
+.green .text {
+ background: linear-gradient(180deg, #fffdeb 0%, #fffada 100%);
+ background-clip: text;
+ -webkit-background-clip: text;
+}
+
+.cyan .text {
+ background: linear-gradient(180deg, #ffffff 0%, #a7f0e7 100%);
+ background-clip: text;
+ -webkit-background-clip: text;
+}
+.black .text {
+ background: linear-gradient(180deg, #ffffff 0%, #c9f5f0 100%);
+ background-clip: text;
+ -webkit-background-clip: text;
+ font-size: 16px;
+ line-height: 20.8px;
+}
+
+.blocked {
+ .text {
+ font-size: 18px;
+ line-height: 23.4px;
+ }
+
+ .shadow {
+ top: 24px;
+ }
+
+ .stroke {
+ position: absolute;
+ top: 23px;
+ }
+}
+
+.stroke {
+ position: absolute;
+ top: 20px;
+}
+
+.red .stroke {
+ -webkit-text-stroke: 3px #551111;
+}
+.green .stroke {
+ -webkit-text-stroke: 3px #3c5511;
+}
+.cyan .stroke {
+ -webkit-text-stroke: 3px #11554d;
+}
+.black .stroke {
+ -webkit-text-stroke: 2px #0b0b0b;
+ top: 11px;
+}
+
+.shadow {
+ position: absolute;
+ top: 21px;
+}
+
+.red .shadow {
+ -webkit-text-stroke: 3px #553211;
+}
+.green .shadow {
+ -webkit-text-stroke: 3px #3c5511;
+}
+.cyan .shadow {
+ -webkit-text-stroke: 3px #3c5511;
+}
+.black .shadow {
+ -webkit-text-stroke: 2px #35353e;
+ top: 12px;
+}
+
+.glow {
+ position: absolute;
+ top: 12.5px;
+ right: 12.24px;
+}
+
+.black .glow {
+ top: 5px;
+ right: 5px;
+ transform: scale(0.6);
+}
+
+.blackAnimation {
+ border-radius: 50%;
+ position: absolute;
+ animation: glare 3s linear infinite;
+}
+
+.outer:hover {
+ .blackAnimation {
+ display: none;
+ }
+}
+
+@keyframes glare {
+ 0%,
+ 40% {
+ width: 65.95px;
+ height: 72px;
+ top: -50px;
+ left: 15.1px;
+ opacity: 0;
+ transform: rotate(-25.46deg);
+ background: linear-gradient(244.48deg, rgba(74, 74, 84, 0.25) 24.28%, rgba(210, 210, 225, 0.175) 85.2%);
+ }
+ 50% {
+ width: 65.95px;
+ height: 72px;
+ top: -24.41px;
+ left: 15.1px;
+ opacity: 0.1;
+ transform: rotate(-25.46deg);
+ }
+ 65% {
+ width: 115.69px;
+ height: 79.66px;
+ top: -6.18px;
+ left: -19.71px;
+ opacity: 1;
+ transform: rotate(10.12deg);
+ background: linear-gradient(
+ 228.67deg,
+ rgba(74, 74, 84, 0.25) 12.63%,
+ rgba(210, 210, 225, 0.125) 52.79%,
+ rgba(74, 74, 84, 0.25) 97.08%
+ );
+ }
+ 80% {
+ width: 227.45px;
+ height: 93.86px;
+ top: -8.55px;
+ left: -29.85px;
+ opacity: 1;
+ }
+ 100% {
+ width: 187.45px;
+ height: 93.86px;
+ top: -8.55px;
+ left: 20px;
+ opacity: 0;
+ transform: rotate(10.12deg);
+ background: linear-gradient(
+ 228.67deg,
+ rgba(74, 74, 84, 0.25) 12.63%,
+ rgba(210, 210, 225, 0.125) 52.79%,
+ rgba(74, 74, 84, 0.25) 97.08%
+ );
+ }
+}
+
+@keyframes rotateSpinner {
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/game-button/game-button.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/game-button/game-button.tsx
new file mode 100644
index 000000000..3ab8cb61f
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/game-button/game-button.tsx
@@ -0,0 +1,60 @@
+import { VariantProps, cva } from 'class-variance-authority';
+import clsx from 'clsx';
+import { ButtonHTMLAttributes } from 'react';
+import { GameButtonGlowSvg, LoaderIcon } from '../../assets/images';
+import styles from './game-button.module.scss';
+
+const variants = cva('', {
+ variants: { color: { red: styles.red, green: styles.green, cyan: styles.cyan, black: styles.black } },
+ defaultVariants: { color: 'red' },
+});
+
+type GameButtonProps = ButtonHTMLAttributes
&
+ VariantProps & {
+ text: string;
+ icon?: React.ReactNode;
+ turnsBlocked?: number;
+ pending?: boolean;
+ };
+
+export const GameButton = ({
+ color,
+ text,
+ icon,
+ className,
+ turnsBlocked,
+ disabled,
+ pending,
+ ...restProps
+}: GameButtonProps) => {
+ const isDisabled = Boolean(turnsBlocked) || disabled;
+ const displayedText = turnsBlocked ? `Blocked for ${turnsBlocked} turns` : text;
+
+ return (
+
+
+ {pending ? (
+
+ ) : (
+ <>
+ {!turnsBlocked && icon}
+
+ {displayedText}
+ {displayedText}
+ {displayedText}
+
+ >
+ )}
+
+
+ {color === 'black' &&
}
+
+ );
+};
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/game-button/index.ts b/frontend/apps/web3-warriors-battle/src/features/game/components/game-button/index.ts
new file mode 100644
index 000000000..17a4b4a57
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/game-button/index.ts
@@ -0,0 +1 @@
+export { GameButton } from './game-button';
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/game-over-card/game-over-card.module.scss b/frontend/apps/web3-warriors-battle/src/features/game/components/game-over-card/game-over-card.module.scss
new file mode 100644
index 000000000..9e2bc973b
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/game-over-card/game-over-card.module.scss
@@ -0,0 +1,54 @@
+.backdrop {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ backdrop-filter: grayscale(100%);
+}
+
+.card {
+ position: absolute;
+ left: 0;
+ right: 0;
+ margin: 0 auto;
+ top: 217px;
+ width: 546px;
+ > div {
+ padding: 32px;
+ }
+ gap: 28px;
+ h2 {
+ font-size: 32px;
+ line-height: 38px;
+ }
+}
+
+.prize {
+ margin-top: 4px;
+ background: #f7f9fa;
+ border-radius: 4px;
+ height: 64px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+}
+
+.icon {
+ margin-left: 16px;
+ margin-right: 10px;
+ height: 32px;
+ width: 32px;
+}
+
+.result {
+ position: absolute;
+ top: 486px;
+ text-transform: uppercase;
+ text-align: center;
+ width: 100%;
+ font-size: 56px;
+ line-height: 67px;
+ font-weight: 700;
+ color: #ffffff;
+ -webkit-text-stroke: 2px #000000;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/game-over-card/game-over-card.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/game-over-card/game-over-card.tsx
new file mode 100644
index 000000000..ac0f049e9
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/game-over-card/game-over-card.tsx
@@ -0,0 +1,94 @@
+import clsx from 'clsx';
+import { useAtomValue } from 'jotai';
+import { Card, Text } from '@/components';
+import { VaraIcon } from '@/components/layout';
+import { useAccount, useBalanceFormat } from '@gear-js/react-hooks';
+import { Player, State } from '@/app/utils';
+import { battleHistoryAtom, currentPlayersAtom } from '../../store';
+import styles from './game-over-card.module.scss';
+
+type GameOverCardProps = {
+ bid: number;
+ totalParticipants: number;
+ state: State;
+ participantsMap: Record;
+ isAlive: boolean;
+ isShowOtherBattle: boolean;
+ className?: string;
+};
+
+const GameOverCard = ({
+ bid,
+ className,
+ state,
+ totalParticipants,
+ participantsMap,
+ isAlive,
+ isShowOtherBattle,
+}: GameOverCardProps) => {
+ const { account } = useAccount();
+ const currentPlayers = useAtomValue(currentPlayersAtom);
+ const battleHistory = useAtomValue(battleHistoryAtom);
+ const { getFormattedBalanceValue } = useBalanceFormat();
+ const isTournamentOver = 'gameIsOver' in state;
+ const prizeValue = Number(getFormattedBalanceValue(Number(bid) || 0)) * totalParticipants;
+ const isCurrentDraw =
+ !isTournamentOver && battleHistory?.[0].player.health === 0 && battleHistory?.[0].opponent.health === 0;
+
+ const isTournamentDraw = isTournamentOver && state.gameIsOver.winners[1];
+
+ const getMyResultStatus = () => {
+ if (!account) return null;
+ if (isCurrentDraw || (isTournamentDraw && state.gameIsOver.winners.includes(account.decodedAddress)))
+ return 'It’s a draw';
+ if (!isAlive && (!isShowOtherBattle || isTournamentOver)) return 'You lose';
+ if (isTournamentOver && state.gameIsOver.winners[0] === account.decodedAddress) return 'You win';
+ return null;
+ };
+
+ const myResultStatus = getMyResultStatus();
+
+ const getDesctiptionText = () => {
+ if (!isTournamentOver) {
+ const winnersName =
+ currentPlayers?.player.player_settings.health === 0
+ ? currentPlayers?.opponent.user_name
+ : currentPlayers?.player.user_name;
+ return `${
+ winnersName || 'Player 2'
+ } wins! Now you can watch other players' battles. Choose any battle from the list below.`;
+ }
+
+ const firstTournamentWinnerName = participantsMap[state.gameIsOver.winners[0]].user_name;
+ if (isTournamentDraw && state.gameIsOver.winners[1]) {
+ const secondTournamentWinnerName = participantsMap[state.gameIsOver.winners[1]].user_name;
+ return `${firstTournamentWinnerName} and ${secondTournamentWinnerName} ended in a draw!`;
+ } else {
+ return `${firstTournamentWinnerName} wins!`;
+ }
+ };
+
+ return (
+ myResultStatus && (
+
+ {!isCurrentDraw && (
+
+ {isTournamentOver && (
+
+ Winner prize:
+
+
+ {isTournamentDraw ? prizeValue / 2 : prizeValue} VARA
+
+
+ )}
+
+ )}
+
+
{myResultStatus}
+
+ )
+ );
+};
+
+export { GameOverCard };
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/game-over-card/index.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/game-over-card/index.tsx
new file mode 100644
index 000000000..d690d594f
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/game-over-card/index.tsx
@@ -0,0 +1 @@
+export { GameOverCard } from './game-over-card';
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/game-spinner/game-spinner.module.scss b/frontend/apps/web3-warriors-battle/src/features/game/components/game-spinner/game-spinner.module.scss
new file mode 100644
index 000000000..b40bf7108
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/game-spinner/game-spinner.module.scss
@@ -0,0 +1,29 @@
+.wrapper {
+ position: absolute;
+ top: 248px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ width: 222px;
+ height: 222px;
+ background: #4b4b55;
+ border: 1.5px solid #0b0b0b;
+ color: #ffffff;
+ border-radius: 20px;
+ padding: 40px;
+ text-align: center;
+
+ > svg {
+ animation: rotateSpinner 2.5s linear infinite;
+ height: 48px;
+ width: 48px;
+ margin-bottom: 7px;
+ }
+}
+
+@keyframes rotateSpinner {
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/game-spinner/game-spinner.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/game-spinner/game-spinner.tsx
new file mode 100644
index 000000000..4d81cd927
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/game-spinner/game-spinner.tsx
@@ -0,0 +1,18 @@
+import { LoaderIcon } from '../../assets/images';
+import { Text } from '@/components';
+import styles from './game-spinner.module.scss';
+
+type GameSpinnerProps = {
+ text: string;
+};
+
+export const GameSpinner = ({ text }: GameSpinnerProps) => {
+ return (
+
+
+
+ {text}
+
+
+ );
+};
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/game-spinner/index.ts b/frontend/apps/web3-warriors-battle/src/features/game/components/game-spinner/index.ts
new file mode 100644
index 000000000..0aab58c4c
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/game-spinner/index.ts
@@ -0,0 +1 @@
+export { GameSpinner } from './game-spinner';
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/health-indicator/health-indicator.module.scss b/frontend/apps/web3-warriors-battle/src/features/game/components/health-indicator/health-indicator.module.scss
new file mode 100644
index 000000000..8a1527b62
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/health-indicator/health-indicator.module.scss
@@ -0,0 +1,66 @@
+.wrapper {
+ display: flex;
+ align-items: center;
+}
+
+.segment {
+ position: relative;
+ height: 16px;
+ width: 13.5px;
+ border-radius: 6px;
+ &:not(:first-child) {
+ margin-left: 6px;
+ }
+
+ &.small {
+ width: 6px;
+ height: 14px;
+
+ &:not(:first-child) {
+ margin-left: 3px;
+ }
+ }
+}
+
+.inner {
+ position: relative;
+ height: 100%;
+ width: 100%;
+ border-radius: 4px;
+ background-color: #0b0b0b;
+ overflow: hidden;
+}
+
+.small .inner {
+ background-color: #cfcfcf;
+ box-shadow: 0px 0.6px 1.8px 0px #00000033 inset;
+}
+
+.health {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ background-color: #00ffc4;
+ box-shadow: 0px 0.6px 1.8px 0px #00000033 inset;
+}
+
+.damage {
+ position: absolute;
+ top: 0;
+ height: 100%;
+ background-color: #ca445f;
+}
+
+.shadow {
+ position: absolute;
+ top: 0;
+ width: 100%;
+ height: 14px;
+ border-radius: 4px;
+ box-shadow: 0px 4.4px 4.8px 0px #00ffc466;
+}
+
+.small .shadow {
+ box-shadow: 0px 3px 4.8px 0px #00ffc466;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/health-indicator/health-indicator.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/health-indicator/health-indicator.tsx
new file mode 100644
index 000000000..6f2c89095
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/health-indicator/health-indicator.tsx
@@ -0,0 +1,57 @@
+import clsx from 'clsx';
+import styles from './health-indicator.module.scss';
+
+type HealthIndicatorProps = {
+ health: number;
+ maxHealth?: number;
+ prevHealth?: number;
+ size?: 'md' | 'sm';
+};
+
+const HealthIndicator = ({ health, maxHealth = 100, prevHealth, size = 'md' }: HealthIndicatorProps) => {
+ const segments = 10;
+ const healthPerSegment = maxHealth / segments;
+
+ const healthSegments = Array.from({ length: segments }, (_, i) => {
+ const segmentHealthStart = i * healthPerSegment;
+ const segmentHealthEnd = (i + 1) * healthPerSegment;
+
+ const isCurrentPartiallyFilled = health < segmentHealthEnd && health > segmentHealthStart;
+ const isPrevPartiallyFilled = prevHealth && prevHealth < segmentHealthEnd && prevHealth > segmentHealthStart;
+
+ const currentFillPercent = isCurrentPartiallyFilled
+ ? ((health - segmentHealthStart) / healthPerSegment) * 100
+ : health >= segmentHealthEnd
+ ? 100
+ : 0;
+
+ const damageFillPercent = isPrevPartiallyFilled
+ ? ((prevHealth - segmentHealthStart) / healthPerSegment) * 100 - currentFillPercent
+ : prevHealth && prevHealth >= segmentHealthEnd && health < segmentHealthEnd
+ ? 100 - currentFillPercent
+ : 0;
+
+ return (
+
+
+
+
+ {damageFillPercent > 0 && (
+
+ )}
+
+
+
+ );
+ });
+
+ return {healthSegments}
;
+};
+
+export { HealthIndicator };
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/health-indicator/index.ts b/frontend/apps/web3-warriors-battle/src/features/game/components/health-indicator/index.ts
new file mode 100644
index 000000000..59b724f6c
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/health-indicator/index.ts
@@ -0,0 +1 @@
+export { HealthIndicator } from './health-indicator';
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/index.ts b/frontend/apps/web3-warriors-battle/src/features/game/components/index.ts
new file mode 100644
index 000000000..c679b6494
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/index.ts
@@ -0,0 +1,20 @@
+export { Avatar } from './avatar';
+export { FireballCanvas, SphereAnimation } from './animations';
+export { Background } from './background';
+export { Character } from './character';
+export { CharacterStats } from './character-stats';
+export { CharacterStatsForm } from './character-stats-form';
+export { WaitList } from './wait-list';
+export { List } from './list';
+export { GameButton } from './game-button';
+export { GameOverCard } from './game-over-card';
+export { BattleCard } from './battle-card';
+export { HealthIndicator } from './health-indicator';
+export { PlayerStatus } from './player-status';
+export { PlayersList } from './players-list';
+export { GameSpinner } from './game-spinner';
+export { BattleTabs } from './sections/battle-tabs';
+export { Timer } from './timer';
+export * from './battle-history-card';
+export { BattleHistorySinc } from './battle-history-sync';
+export { GameCanceledModal } from './modals/game-canceled-modal';
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/list/index.ts b/frontend/apps/web3-warriors-battle/src/features/game/components/list/index.ts
new file mode 100644
index 000000000..53821c4bd
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/list/index.ts
@@ -0,0 +1 @@
+export { List } from './list';
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/list/list.module.scss b/frontend/apps/web3-warriors-battle/src/features/game/components/list/list.module.scss
new file mode 100644
index 000000000..1a315542b
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/list/list.module.scss
@@ -0,0 +1,8 @@
+.wrapper {
+ padding: 20px;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ background: #eceded;
+ border-radius: 20px;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/list/list.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/list/list.tsx
new file mode 100644
index 000000000..a8ddcc126
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/list/list.tsx
@@ -0,0 +1,27 @@
+import clsx from 'clsx';
+import { Button } from '@gear-js/vara-ui';
+import { BaseComponentProps } from '@/app/types';
+import styles from './list.module.scss';
+import { useState } from 'react';
+
+type ListProps = BaseComponentProps & {
+ items: React.ReactNode[];
+ maxLength: number;
+};
+
+const List = ({ items, className, maxLength, ...restProps }: ListProps) => {
+ const [showAll, setShowAll] = useState(false);
+
+ const displayedItems = showAll ? items : items.slice(0, maxLength);
+
+ return (
+
+ {displayedItems}
+ {items.length > maxLength && !showAll && (
+ setShowAll(true)} />
+ )}
+
+ );
+};
+
+export { List };
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/modals/game-canceled-modal/game-canceled-modal.module.scss b/frontend/apps/web3-warriors-battle/src/features/game/components/modals/game-canceled-modal/game-canceled-modal.module.scss
new file mode 100644
index 000000000..73614fb5c
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/modals/game-canceled-modal/game-canceled-modal.module.scss
@@ -0,0 +1,10 @@
+.modal {
+ width: 650px;
+ & > div > div {
+ width: 540px;
+ }
+}
+
+.button {
+ width: 227.5px;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/modals/game-canceled-modal/game-canceled-modal.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/modals/game-canceled-modal/game-canceled-modal.tsx
new file mode 100644
index 000000000..d6af25b20
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/modals/game-canceled-modal/game-canceled-modal.tsx
@@ -0,0 +1,26 @@
+import { Button } from '@gear-js/vara-ui';
+import { useSetAtom } from 'jotai';
+import { useNavigate } from 'react-router-dom';
+import { Modal } from '@/components';
+import { ROUTES } from '@/app/consts';
+import { isBattleCanceledAtom } from '@/features/game/store';
+import styles from './game-canceled-modal.module.scss';
+
+export const GameCanceledModal = () => {
+ const navigate = useNavigate();
+ const setIsCanceled = useSetAtom(isBattleCanceledAtom);
+ const onClose = () => {
+ setIsCanceled(false);
+ navigate(ROUTES.HOME);
+ };
+
+ return (
+
+
+
+ );
+};
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/modals/game-canceled-modal/index.ts b/frontend/apps/web3-warriors-battle/src/features/game/components/modals/game-canceled-modal/index.ts
new file mode 100644
index 000000000..d6be8f95b
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/modals/game-canceled-modal/index.ts
@@ -0,0 +1 @@
+export { GameCanceledModal } from './game-canceled-modal';
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/modals/game-found-modal/game-found-modal.module.scss b/frontend/apps/web3-warriors-battle/src/features/game/components/modals/game-found-modal/game-found-modal.module.scss
new file mode 100644
index 000000000..cbb5c85a8
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/modals/game-found-modal/game-found-modal.module.scss
@@ -0,0 +1,33 @@
+.modal {
+ max-width: 668px;
+}
+
+.container {
+ display: flex;
+ flex-direction: column;
+ gap: 28px;
+}
+
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 28px;
+}
+
+.buttons {
+ display: flex;
+ gap: 24px;
+}
+
+.button {
+ width: 100%;
+}
+
+.modalWrapper {
+ background: #fff;
+ border-radius: 8px 8px 0px 0px;
+}
+
+.mainText {
+ opacity: 0.7;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/modals/game-found-modal/game-found-modal.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/modals/game-found-modal/game-found-modal.tsx
new file mode 100644
index 000000000..19438e379
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/modals/game-found-modal/game-found-modal.tsx
@@ -0,0 +1,89 @@
+import { useApi } from '@gear-js/react-hooks';
+import { Button, Input } from '@gear-js/vara-ui';
+import { Modal } from '@/components/ui/modal';
+import { ReactComponent as VaraSVG } from '@/assets/images/icons/vara-coin.svg';
+import { ReactComponent as TVaraSVG } from '@/assets/images/icons/tvara-coin.svg';
+import { isNotEmpty, useForm } from '@mantine/form';
+import { GameDetails } from '@/components/layout';
+import { UserIcon } from '@/assets/images';
+import styles from './game-found-modal.module.scss';
+import { MAX_PLAYERS_COUNT } from '@/app/consts';
+import { usePending } from '@/features/game/hooks';
+
+type Props = {
+ entryFee: number | string;
+ participantsCount: number;
+ onSubmit: (values: JoinModalFormValues) => Promise;
+ onClose: () => void;
+};
+
+export type JoinModalFormValues = {
+ name: string;
+};
+
+function GameFoundModal({ entryFee, participantsCount, onSubmit, onClose }: Props) {
+ const { api } = useApi();
+ const { pending } = usePending();
+
+ const VaraSvg = api?.registry.chainTokens[0].toLowerCase() === 'vara' ? : ;
+
+ const items = [
+ {
+ name: 'Entry fee',
+ value: (
+ <>
+ {VaraSvg} {entryFee} VARA
+ >
+ ),
+ key: '1',
+ },
+ {
+ name: 'Players already joined the game',
+ value: (
+ <>
+ {participantsCount} / {MAX_PLAYERS_COUNT}
+ >
+ ),
+ key: '2',
+ },
+ ];
+
+ const joinForm = useForm({
+ initialValues: {
+ name: '',
+ },
+ validate: {
+ name: isNotEmpty(`Name shouldn't be empty`),
+ },
+ });
+
+ const { errors: joinErrors, getInputProps: getJoinInputProps, onSubmit: onJoinSubmit } = joinForm;
+
+ const handleJoinSession = async (values: JoinModalFormValues) => {
+ await onSubmit(values);
+ };
+
+ return (
+
+
+
+
+ );
+}
+
+export { GameFoundModal };
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/modals/game-found-modal/index.ts b/frontend/apps/web3-warriors-battle/src/features/game/components/modals/game-found-modal/index.ts
new file mode 100644
index 000000000..1dbc42c9e
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/modals/game-found-modal/index.ts
@@ -0,0 +1,5 @@
+import { GameFoundModal, JoinModalFormValues } from './game-found-modal';
+
+export { GameFoundModal };
+
+export type { JoinModalFormValues };
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/player-status/index.ts b/frontend/apps/web3-warriors-battle/src/features/game/components/player-status/index.ts
new file mode 100644
index 000000000..29a707150
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/player-status/index.ts
@@ -0,0 +1 @@
+export { PlayerStatus } from './player-status';
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/player-status/player-status.module.scss b/frontend/apps/web3-warriors-battle/src/features/game/components/player-status/player-status.module.scss
new file mode 100644
index 000000000..39ecabe7c
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/player-status/player-status.module.scss
@@ -0,0 +1,21 @@
+.status {
+ height: 26px;
+ padding: 2px 9px;
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+}
+
+.alive {
+ color: #0ed3a3;
+ background: #00ffc412;
+ width: 77px;
+}
+
+.defeat {
+ color: #ca445f;
+ background: #ca445f12;
+ width: 109px;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/player-status/player-status.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/player-status/player-status.tsx
new file mode 100644
index 000000000..f4903f8fa
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/player-status/player-status.tsx
@@ -0,0 +1,20 @@
+import { Text } from '@/components';
+import { SkullBigIcon } from '../../assets/images';
+import clsx from 'clsx';
+
+import styles from './player-status.module.scss';
+
+type PlayerStatusProps = {
+ isAlive: boolean;
+};
+
+const PlayerStatus = ({ isAlive }: PlayerStatusProps) => {
+ return (
+
+ {!isAlive && }
+ {isAlive ? 'Alive' : 'Defeated'}
+
+ );
+};
+
+export { PlayerStatus };
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/players-list/index.ts b/frontend/apps/web3-warriors-battle/src/features/game/components/players-list/index.ts
new file mode 100644
index 000000000..bfab0de89
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/players-list/index.ts
@@ -0,0 +1 @@
+export { PlayersList } from './players-list';
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/players-list/players-list.module.scss b/frontend/apps/web3-warriors-battle/src/features/game/components/players-list/players-list.module.scss
new file mode 100644
index 000000000..f68e89c95
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/players-list/players-list.module.scss
@@ -0,0 +1,60 @@
+.wrapper {
+ width: 576px;
+ display: flex;
+ flex-direction: column;
+}
+
+.title {
+ text-align: center;
+ margin-bottom: 16px;
+}
+
+.list {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.statusWrapper {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+}
+
+.item {
+ display: grid;
+ grid-template-columns: 17px 1fr 200px;
+ align-items: center;
+ gap: 12px;
+ padding: 11px 12px;
+ border-radius: 8px;
+ border: 1px solid #00000012;
+
+ &:first-child {
+ margin-top: 16px;
+ }
+
+ &:last-child {
+ margin-bottom: 16px;
+ }
+
+ &:not(:first-child) {
+ margin-top: 4px;
+ }
+
+ &.my {
+ background: #00ffc4;
+ }
+}
+
+.number {
+ width: 17px;
+ opacity: 0.5;
+}
+
+.icon {
+ margin-left: 14px;
+ margin-right: 8px;
+ height: 32px;
+ width: 32px;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/players-list/players-list.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/players-list/players-list.tsx
new file mode 100644
index 000000000..d3569f934
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/players-list/players-list.tsx
@@ -0,0 +1,67 @@
+import clsx from 'clsx';
+import { Button } from '@gear-js/vara-ui';
+import { useAccount, useBalanceFormat } from '@gear-js/react-hooks';
+import { useState } from 'react';
+import { BaseComponentProps } from '@/app/types';
+import { Text } from '@/components';
+import { VaraIcon } from '@/components/layout';
+import { Heading } from '@/components/ui/heading';
+import { PlayerStatus } from '../player-status/player-status';
+import styles from './players-list.module.scss';
+
+type Item = {
+ name: string;
+ status: 'defeated' | 'alive';
+ address: string;
+};
+
+type PlayersListProps = BaseComponentProps & {
+ items: Item[];
+ bid: number;
+ tournamentName: string;
+};
+
+const PlayersList = ({ items, className, bid, tournamentName, ...restProps }: PlayersListProps) => {
+ const [showAll, setShowAll] = useState(false);
+ const { account } = useAccount();
+ const { getFormattedBalanceValue } = useBalanceFormat();
+ const maxLength = 10;
+ const displayedItems = showAll ? items : items.slice(0, maxLength);
+
+ return (
+
+
+ {tournamentName}
+
+
+ {displayedItems.map(({ name, status, address }, index) => {
+ const isMy = address === account?.decodedAddress;
+
+ return (
+
+
+ {index + 1}
+
+
+ {name}
+
+
+
+
+
+
+ {getFormattedBalanceValue(bid).toFixed(2)}
+
+
+
+ );
+ })}
+
+ {items.length > maxLength && !showAll && (
+
setShowAll(true)} />
+ )}
+
+ );
+};
+
+export { PlayersList };
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/sections/battle-tabs/battle-tabs.module.scss b/frontend/apps/web3-warriors-battle/src/features/game/components/sections/battle-tabs/battle-tabs.module.scss
new file mode 100644
index 000000000..aa58569bb
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/sections/battle-tabs/battle-tabs.module.scss
@@ -0,0 +1,49 @@
+.tabs {
+ position: absolute;
+ top: 684px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ &.defeated {
+ top: 642px;
+ }
+}
+
+.list {
+ margin-top: 30px;
+}
+
+.playersList {
+ margin-top: 43px;
+}
+
+.switcher {
+ margin-top: 31px;
+ display: flex;
+ gap: 10px;
+ align-items: center;
+ justify-content: center;
+}
+
+.historyItem {
+ display: flex;
+ cursor: pointer;
+
+ &.disabled {
+ cursor: auto;
+ pointer-events: none;
+ }
+}
+
+.players {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ > div {
+ gap: 8;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ }
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/sections/battle-tabs/battle-tabs.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/sections/battle-tabs/battle-tabs.tsx
new file mode 100644
index 000000000..be7d17fbd
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/sections/battle-tabs/battle-tabs.tsx
@@ -0,0 +1,190 @@
+import clsx from 'clsx';
+import { useEffect, useState } from 'react';
+import { useAtom, useAtomValue, useSetAtom } from 'jotai';
+
+import { Switcher } from '@dapps-frontend/ui';
+import { BattleHistoryCard, BattleCard, PlayersList, List } from '@/features/game/components';
+import { Segmented, Text } from '@/components';
+import { BattleState, Player } from '@/app/utils';
+import { UserSkullIcon, UserSmileIcon } from '@/features/game/assets/images';
+import { PlayerStatus } from '@/features/game/types';
+import { battleHistoryAtom, currentPlayersAtom, otherPairBattleWatchAtom } from '@/features/game/store';
+import styles from './battle-tabs.module.scss';
+
+type Tabs = 'players' | 'history';
+
+type BattleTabsProps = {
+ battleState: BattleState;
+ participantsMap: Record;
+ isAlive: boolean;
+};
+
+export const BattleTabs = ({ battleState, participantsMap, isAlive }: BattleTabsProps) => {
+ const { participants, defeated_participants, battle_name, state } = battleState;
+ const [selectedTab, setSelectedTab] = useState('players');
+ const [showCurrentBattle, setShowCurrentBattle] = useState(true);
+
+ const [battleHistory, setBattleHistory] = useAtom(battleHistoryAtom);
+ const currentPlayers = useAtomValue(currentPlayersAtom);
+ const setOtherPairBattleWatch = useSetAtom(otherPairBattleWatchAtom);
+
+ const isTournamentOver = 'gameIsOver' in state;
+
+ useEffect(() => {
+ if (!isAlive && !isTournamentOver) {
+ setSelectedTab('history');
+ setShowCurrentBattle(false);
+ }
+ }, [isAlive, isTournamentOver]);
+
+ const alivePlayersListItems = participants.map(([address, { user_name }]) => ({
+ name: user_name,
+ status: 'alive' as PlayerStatus,
+ address,
+ }));
+ const defeatedPlayersListItems = defeated_participants.map(([address, { user_name }]) => ({
+ name: user_name,
+ status: 'defeated' as PlayerStatus,
+ address,
+ }));
+
+ const playersListItems = [...alivePlayersListItems, ...defeatedPlayersListItems];
+
+ const segmentedOptions = [
+ {
+ label: (
+
+
Players:
+
+ {participants.length}
+
+
+ {defeated_participants.length}
+
+
+ ),
+ value: 'players',
+ },
+ {
+ label: 'Battle History ',
+ value: 'history',
+ },
+ ];
+ return (
+
+
setSelectedTab(value as Tabs)} />
+
+ {selectedTab === 'players' && (
+
+ )}
+
+ {selectedTab === 'history' && (
+ <>
+
+ setShowCurrentBattle(isChecked)}
+ />
+ Show current battle
+
+ {showCurrentBattle ? (
+ {
+ return (
+
+
+
+
+ );
+ })) ||
+ []
+ }
+ />
+ ) : (
+ {
+ const player1 = participantsMap[player_1];
+ const player2 = participantsMap[player_2];
+ const disabled = isAlive || !player2;
+ return (
+ {
+ if (disabled) {
+ return;
+ }
+
+ setOtherPairBattleWatch(key);
+ setShowCurrentBattle(true);
+ setBattleHistory([
+ {
+ player: {
+ action: null,
+ health: player1.player_settings.health,
+ isDodged: false,
+ receivedDamage: 0,
+ },
+ opponent: {
+ action: null,
+ health: player1.player_settings.health,
+ isDodged: false,
+ receivedDamage: 0,
+ },
+ },
+ ]);
+ }}>
+
+ {player2 && (
+
+ )}
+
+ );
+ })
+ : [
+
+ There are no other battles now
+
,
+ ]
+ }
+ />
+ )}
+ >
+ )}
+
+ );
+};
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/sections/battle-tabs/index.ts b/frontend/apps/web3-warriors-battle/src/features/game/components/sections/battle-tabs/index.ts
new file mode 100644
index 000000000..60b6cd1db
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/sections/battle-tabs/index.ts
@@ -0,0 +1 @@
+export { BattleTabs } from './battle-tabs';
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/sections/create-game-form/create-game-form.module.scss b/frontend/apps/web3-warriors-battle/src/features/game/components/sections/create-game-form/create-game-form.module.scss
new file mode 100644
index 000000000..9b75e00ee
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/sections/create-game-form/create-game-form.module.scss
@@ -0,0 +1,34 @@
+@use '@/assets/styles/utils' as *;
+@use '@gear-js/ui/breakpoints' as *;
+
+.card {
+ gap: 32px;
+ margin-top: 40px;
+}
+
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ width: 100%;
+}
+
+.formRow {
+ display: flex;
+ gap: 24px;
+ @include sm {
+ flex-direction: column;
+ }
+ > div {
+ flex: 1;
+ }
+}
+
+.buttons {
+ display: flex;
+ gap: 24px;
+ width: 100%;
+ & > * {
+ flex: 1;
+ }
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/sections/create-game-form/create-game-form.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/sections/create-game-form/create-game-form.tsx
new file mode 100644
index 000000000..658196b4d
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/sections/create-game-form/create-game-form.tsx
@@ -0,0 +1,139 @@
+import { useEffect } from 'react';
+import { useAccount, useApi, useBalanceFormat } from '@gear-js/react-hooks';
+import { useAtomValue } from 'jotai';
+import { useNavigate } from 'react-router-dom';
+import { Input, Button } from '@gear-js/vara-ui';
+import { isNotEmpty, useForm } from '@mantine/form';
+import { VaraIcon } from '@/components/layout';
+import { usePending } from '@/features/game/hooks';
+import { Background } from '../../background';
+import { Card } from '@/components';
+import { characterAppearanceAtom, characterStatsStorage, warriorIdStorage } from '@/features/game/store';
+import { useCreateNewBattleMessage } from '@/app/utils';
+import { ROUTES } from '@/app/consts';
+import styles from './create-game-form.module.scss';
+
+type CreateGameFormValues = {
+ fee: number;
+ name: string;
+ tournamentName: string;
+};
+
+function CreateGameForm() {
+ const navigate = useNavigate();
+ const { account } = useAccount();
+ const { api } = useApi();
+ const { getFormattedBalanceValue } = useBalanceFormat();
+
+ const appearance = useAtomValue(characterAppearanceAtom);
+ const characterStats = characterStatsStorage.get();
+ const warriorId = warriorIdStorage.get();
+
+ useEffect(() => {
+ if (!appearance || !characterStats) {
+ navigate(-1);
+ }
+ }, [appearance, characterStats, navigate]);
+
+ const { createNewBattleMessage } = useCreateNewBattleMessage();
+ const { pending, setPending } = usePending();
+ const existentialDeposit = Number(getFormattedBalanceValue(api?.existentialDeposit.toNumber() || 0).toFixed());
+ const { getChainBalanceValue } = useBalanceFormat();
+
+ const createForm = useForm({
+ initialValues: {
+ fee: 0,
+ name: '',
+ tournamentName: '',
+ },
+ validate: {
+ fee: (value) =>
+ Number(value) < existentialDeposit + 5 && Number(value) !== 0
+ ? `value must be more than ${existentialDeposit + 5} or 0`
+ : null,
+ name: isNotEmpty(`Name shouldn't be empty`),
+ tournamentName: isNotEmpty(`Tournament name shouldn't be empty`),
+ },
+ });
+
+ const { getInputProps: getCreateInputProps, onSubmit: onCreateSubmit } = createForm;
+
+ const handleCreateSession = async (values: CreateGameFormValues) => {
+ if (!account?.decodedAddress || !appearance || !characterStats) {
+ return;
+ }
+ const { attack, defence, dodge } = characterStats;
+ const { name, tournamentName } = values;
+ const fee = BigInt(getChainBalanceValue(values.fee).toFixed());
+ setPending(true);
+ await createNewBattleMessage(
+ { value: fee, name, tournamentName, appearance, attack, defence, dodge, warriorId },
+ {
+ onSuccess: () => {
+ setPending(false);
+ navigate(ROUTES.WAITING);
+ },
+ onError: () => {
+ setPending(false);
+ },
+ },
+ );
+ };
+
+ return (
+
+
+
+
+
+ navigate(-1)}
+ />
+
+
+
+
+ );
+}
+
+export { CreateGameForm };
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/sections/create-game-form/index.ts b/frontend/apps/web3-warriors-battle/src/features/game/components/sections/create-game-form/index.ts
new file mode 100644
index 000000000..4cf31f31a
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/sections/create-game-form/index.ts
@@ -0,0 +1,3 @@
+import { CreateGameForm } from './create-game-form';
+
+export { CreateGameForm };
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/sections/find-game-form/find-game-form.module.scss b/frontend/apps/web3-warriors-battle/src/features/game/components/sections/find-game-form/find-game-form.module.scss
new file mode 100644
index 000000000..af791e9b7
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/sections/find-game-form/find-game-form.module.scss
@@ -0,0 +1,34 @@
+@use '@/assets/styles/utils' as *;
+
+.card {
+ margin-top: 100px;
+ width: 668px;
+ max-width: 100%;
+}
+
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ width: 100%;
+}
+
+.buttons {
+ display: flex;
+ gap: 24px;
+ width: 100%;
+ & > * {
+ flex: 1;
+ }
+}
+
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 48px;
+ width: 100%;
+}
+
+.gameNotFoundModal {
+ max-width: 508px;
+}
\ No newline at end of file
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/sections/find-game-form/find-game-form.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/sections/find-game-form/find-game-form.tsx
new file mode 100644
index 000000000..3b2a364b9
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/sections/find-game-form/find-game-form.tsx
@@ -0,0 +1,147 @@
+import { useEffect, useState } from 'react';
+import { useAtomValue } from 'jotai';
+import { useNavigate } from 'react-router-dom';
+import { Button, Input } from '@gear-js/vara-ui';
+import { decodeAddress } from '@gear-js/api';
+import { useAccount, useBalanceFormat, withoutCommas } from '@gear-js/react-hooks';
+import { isNotEmpty, useForm } from '@mantine/form';
+import { HexString } from '@gear-js/api';
+
+import { GameFoundModal, JoinModalFormValues } from '../../modals/game-found-modal';
+import { Card, Modal } from '@/components';
+import { BattleState, useBattleQuery, useRegisterMessage } from '@/app/utils';
+import { usePending } from '@/features/game/hooks';
+import { ROUTES } from '@/app/consts';
+import { characterAppearanceAtom, characterStatsStorage, warriorIdStorage } from '@/features/game/store';
+import styles from './find-game-form.module.scss';
+
+type FindGameFormValues = {
+ address: HexString | undefined;
+};
+
+function FindGameForm() {
+ const navigate = useNavigate();
+ const { account } = useAccount();
+ const { getFormattedBalanceValue } = useBalanceFormat();
+ const { registerMessage } = useRegisterMessage();
+
+ const [foundState, setFoundState] = useState(null);
+
+ const { pending, setPending } = usePending();
+ const [isJoinSessionModalShown, setIsJoinSessionModalShown] = useState(false);
+ const [gameNotFoundModal, setGameNotFoundModal] = useState(false);
+
+ const joinForm = useForm({
+ initialValues: {
+ address: undefined,
+ },
+ validate: {
+ address: isNotEmpty(`Address shouldn't be empty`),
+ },
+ });
+
+ const { errors: joinErrors, getInputProps: getJoinInputProps, onSubmit: onJoinSubmit, values } = joinForm;
+
+ const { refetch } = useBattleQuery(values.address?.length === 49 ? decodeAddress(values.address) : '');
+ const appearance = useAtomValue(characterAppearanceAtom);
+ const characterStats = characterStatsStorage.get();
+ const warriorId = warriorIdStorage.get();
+
+ useEffect(() => {
+ if (!appearance || !characterStats) {
+ navigate(ROUTES.HOME);
+ }
+ }, [appearance, characterStats, navigate]);
+
+ const handleCloseFoundModal = () => {
+ setIsJoinSessionModalShown(false);
+ };
+
+ const handleOpenJoinSessionModal = async (values: FindGameFormValues) => {
+ if (!account?.decodedAddress || !values.address) {
+ return;
+ }
+
+ try {
+ const response = await refetch();
+ const { data } = response;
+
+ if (data?.state && 'registration' in data.state) {
+ setFoundState(data);
+ setIsJoinSessionModalShown(true);
+ return;
+ }
+
+ setGameNotFoundModal(true);
+ } catch (err: any) {
+ setGameNotFoundModal(true);
+ }
+ };
+
+ const handleJoinSession = async (values: JoinModalFormValues) => {
+ if (foundState && account && appearance && characterStats) {
+ setPending(true);
+ const gameId = decodeAddress(foundState.admin);
+ const { attack, defence, dodge } = characterStats;
+ const { name } = values;
+ registerMessage(
+ { value: BigInt(foundState.bid), name, appearance, attack, defence, dodge, warriorId, gameId },
+ {
+ onSuccess: () => {
+ setPending(false);
+ navigate(ROUTES.WAITING);
+ },
+ onError: () => setPending(false),
+ },
+ );
+ }
+ };
+
+ const handleCloseNotFoundModal = () => {
+ setGameNotFoundModal(false);
+ };
+
+ return (
+
+
+
+ {isJoinSessionModalShown && foundState && (
+
+ )}
+ {gameNotFoundModal && (
+ }
+ />
+ )}
+
+ );
+}
+
+export { FindGameForm };
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/sections/find-game-form/index.ts b/frontend/apps/web3-warriors-battle/src/features/game/components/sections/find-game-form/index.ts
new file mode 100644
index 000000000..b2ba0737d
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/sections/find-game-form/index.ts
@@ -0,0 +1,3 @@
+import { FindGameForm } from './find-game-form';
+
+export { FindGameForm };
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/timer/index.ts b/frontend/apps/web3-warriors-battle/src/features/game/components/timer/index.ts
new file mode 100644
index 000000000..8f24c927b
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/timer/index.ts
@@ -0,0 +1,3 @@
+import { Timer } from './timer';
+
+export { Timer };
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/timer/timer.module.scss b/frontend/apps/web3-warriors-battle/src/features/game/components/timer/timer.module.scss
new file mode 100644
index 000000000..d0b7f8967
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/timer/timer.module.scss
@@ -0,0 +1,39 @@
+.red {
+ color: #dd1f1f;
+}
+
+.container {
+ position: absolute;
+ top: 45px;
+ color: #ffffff;
+ text-align: center;
+ width: 226px;
+}
+
+.light {
+ position: absolute;
+ top: -85px;
+ left: -38px;
+ width: 300px;
+ height: 300px;
+
+ background-image: url(../../assets/images/timer-light.svg);
+
+ z-index: -1;
+}
+.dark {
+ background-image: url(../../assets/images/timer-dark.svg);
+}
+
+.time {
+ font-size: 72px;
+ font-weight: 700;
+ line-height: 1;
+ letter-spacing: 0.04em;
+ margin: 12px 0 4px;
+}
+
+.title {
+ margin-bottom: 21px;
+ text-transform: uppercase;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/timer/timer.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/timer/timer.tsx
new file mode 100644
index 000000000..b02684057
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/timer/timer.tsx
@@ -0,0 +1,38 @@
+import clsx from 'clsx';
+import { Text } from '@/components';
+import styles from './timer.module.scss';
+import { UseTimerParams, useTimer } from '../../hooks';
+
+type Props = UseTimerParams & {
+ isYourTurn?: boolean;
+};
+
+export function Timer({ remainingTime, shouldGoOn, isYourTurn }: Props) {
+ const formattedTimeLeft = useTimer({ remainingTime, shouldGoOn });
+
+ return (
+
+
+
{formattedTimeLeft}
+
+ {isYourTurn ? (
+ <>
+
+ YOUR TURN
+
+
+
+ If you don’t act in 60 seconds,
+
+
+ “Attack” will be chosen automatically.
+
+ >
+ ) : (
+
+ Waiting for opponent’s turn.
+
+ )}
+
+ );
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/wait-list/index.ts b/frontend/apps/web3-warriors-battle/src/features/game/components/wait-list/index.ts
new file mode 100644
index 000000000..ffc8b5e4a
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/wait-list/index.ts
@@ -0,0 +1 @@
+export { WaitList } from './wait-list';
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/wait-list/wait-list.module.scss b/frontend/apps/web3-warriors-battle/src/features/game/components/wait-list/wait-list.module.scss
new file mode 100644
index 000000000..a49f2b5d5
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/wait-list/wait-list.module.scss
@@ -0,0 +1,52 @@
+.list {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 420px;
+}
+
+.item {
+ display: grid;
+ grid-template-columns: 17px 1fr 180px;
+ gap: 12px;
+ padding: 15.5px 12px;
+ border-radius: 8px;
+ border: 1px solid #00000012;
+ text-align: left;
+
+ &:first-child {
+ margin-top: 16px;
+ }
+
+ &:last-child {
+ margin-bottom: 16px;
+ }
+
+ &:not(:first-child) {
+ margin-top: 4px;
+ }
+
+ &.my {
+ background: #00ffc4;
+ }
+
+ &.admin {
+ grid-template-columns: 28px 17px 1fr 180px;
+ }
+}
+
+.cross {
+ margin-right: 4px;
+}
+
+.number {
+ width: 17px;
+ opacity: 0.5;
+}
+
+.addressWrapper {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ color: #58696e;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/components/wait-list/wait-list.tsx b/frontend/apps/web3-warriors-battle/src/features/game/components/wait-list/wait-list.tsx
new file mode 100644
index 000000000..c1abab70d
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/components/wait-list/wait-list.tsx
@@ -0,0 +1,83 @@
+import { useEffect, useRef } from 'react';
+import clsx from 'clsx';
+import { stringShorten } from '@polkadot/util';
+import { Button } from '@gear-js/vara-ui';
+import { Text } from '@/components';
+import { copyToClipboard, useDeletePlayerMessage } from '@/app/utils';
+import { getVaraAddress, useAccount, useAlert } from '@gear-js/react-hooks';
+import { CopyIcon, FilledCrossIcon } from '../../assets/images';
+import { ScrollArea } from '@/components/ui/scroll-area';
+import styles from './wait-list.module.scss';
+
+type WaitListItem = {
+ name: string;
+ address: string;
+};
+
+type WaitListProps = {
+ items: WaitListItem[];
+ isAdmin: boolean;
+};
+
+const WaitList = ({ items, isAdmin }: WaitListProps) => {
+ const alert = useAlert();
+ const { account } = useAccount();
+ const { deletePlayerMessage } = useDeletePlayerMessage();
+
+ const handleCopyAddress = (value: string) => {
+ copyToClipboard({ alert, value });
+ };
+
+ const myItemRef = useRef(null);
+
+ useEffect(() => {
+ myItemRef.current?.scrollIntoView({ behavior: 'smooth' });
+ }, []);
+
+ return (
+
+ {items.map(({ name, address }, index) => {
+ const isMy = address === account?.decodedAddress;
+
+ return (
+
+ {isAdmin && (
+ <>
+ {!isMy ? (
+
deletePlayerMessage(address)}
+ color="transparent"
+ />
+ ) : (
+
+ )}
+ >
+ )}
+
+ {index + 1}
+
+
+ {name}
+
+
+
+ {stringShorten(getVaraAddress(address), 8)}
+
+ handleCopyAddress(getVaraAddress(address))}>
+
+
+ );
+ })}
+
+ );
+};
+
+export { WaitList };
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/consts.ts b/frontend/apps/web3-warriors-battle/src/features/game/consts.ts
new file mode 100644
index 000000000..3e6f6ccec
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/consts.ts
@@ -0,0 +1,56 @@
+import { AssetsCount, PlayerState } from './types';
+
+export const TIME_LEFT_GAP = 1;
+
+export const MAX_HEALTH = 100;
+export const assetsCount: AssetsCount = {
+ hat: 9,
+ head: 5,
+ body: 6,
+ accessories: 11,
+};
+
+export const body_colors = ['#C9B7FE', '#FEC9B7'];
+export const back_colors = ['#616161', '#FFD700', '#FF69B4', '#49F2C9'];
+
+export const mockCharacterView = {
+ hat_index: 6,
+ head_index: 1,
+ body_index: 0,
+ accessory_index: 2,
+ body_color: body_colors[0],
+ back_color: back_colors[2],
+};
+
+export const mockCharacterView2 = {
+ hat_index: 7,
+ head_index: 1,
+ body_index: 1,
+ accessory_index: 3,
+ body_color: body_colors[1],
+ back_color: back_colors[1],
+};
+
+const mockPlayer1: PlayerState = {
+ name: 'Player name 1',
+ health: 100,
+ attack: 30,
+ defence: 8,
+ dodge: 4,
+ action: 'Attack',
+ isDodged: true,
+ receivedDamage: 0,
+};
+
+const mockPlayer2: PlayerState = {
+ name: 'Player name 2',
+ health: 100,
+ attack: 10,
+ defence: 13,
+ dodge: 5,
+ action: 'Reflect',
+ isDodged: false,
+ receivedDamage: 0,
+};
+
+export { mockPlayer1, mockPlayer2 };
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/hooks.ts b/frontend/apps/web3-warriors-battle/src/features/game/hooks.ts
new file mode 100644
index 000000000..2ffb57bc4
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/hooks.ts
@@ -0,0 +1,199 @@
+import { useEffect, useRef, useState } from 'react';
+import { atom, useAtom, useSetAtom } from 'jotai';
+import { BattleHistory } from './types';
+import { useEventRoundActionSubscription } from '@/app/utils/sails/events';
+import { BattleState, Pair, Player } from '@/app/utils';
+import {
+ battleHistoryAtom,
+ battleHistoryStorage,
+ characterStatsStorage,
+ currentPlayersAtom,
+ currentPlayersStorage,
+ isBattleCanceledAtom,
+ otherPairBattleWatchAtom,
+ warriorIdStorage,
+} from './store';
+import { MAX_HEALTH, TIME_LEFT_GAP } from './consts';
+import { useAccount } from '@gear-js/react-hooks';
+
+const pendingAtom = atom(false);
+
+export function usePending() {
+ const [pending, setPending] = useAtom(pendingAtom);
+
+ return { pending, setPending };
+}
+
+export function useResetGameState() {
+ const setBattleHistory = useSetAtom(battleHistoryAtom);
+ const setOtherPairBattleWatch = useSetAtom(otherPairBattleWatchAtom);
+ const setIsBattleCanceledAtom = useSetAtom(isBattleCanceledAtom);
+ useEffect(() => {
+ setBattleHistory(null);
+ battleHistoryStorage.set(null);
+ setOtherPairBattleWatch(null);
+ setIsBattleCanceledAtom(false);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+}
+
+export function useResetCharacterStats() {
+ useEffect(() => {
+ characterStatsStorage.set(null);
+ warriorIdStorage.set(null);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+}
+
+export type UsePrepareBattleHistoryParams = {
+ pair: Pair;
+ player: Player;
+ opponent: Player | null;
+ turnEndCallback: () => void;
+};
+
+export function usePrepareBattleHistory({ pair, player, opponent, turnEndCallback }: UsePrepareBattleHistoryParams) {
+ const setBattleHistory = useSetAtom(battleHistoryAtom);
+ const { lastMoves, resetLastMoves } = useEventRoundActionSubscription(pair);
+ const setCurrentPlayers = useSetAtom(currentPlayersAtom);
+
+ const myFullDefence = player?.player_settings.defence === 100;
+ const opponentFullDefence = opponent?.player_settings.defence === 100;
+
+ useEffect(() => {
+ if (lastMoves && opponent) {
+ const [myMove, opponentsMove] = lastMoves.moves;
+ const [myHealth, opponentsHealth] = lastMoves.newHealth;
+
+ setBattleHistory((prev) => {
+ const myReceivedDamage = (prev?.[0].player.health ?? MAX_HEALTH) - myHealth;
+ const opponentsReceivedDamage = (prev?.[0].opponent.health ?? MAX_HEALTH) - opponentsHealth;
+ const isBothUseReflect = myMove === 'Reflect' && opponentsMove === 'Reflect';
+ const meReflectAll = myMove === 'Reflect' && myFullDefence;
+ const opponentReflectAll = opponentsMove === 'Reflect' && opponentFullDefence;
+
+ const newHistory: BattleHistory = {
+ player: {
+ action: myMove,
+ isDodged: myReceivedDamage === 0 && !isBothUseReflect && !meReflectAll,
+ receivedDamage: myReceivedDamage,
+ health: myHealth,
+ },
+ opponent: {
+ action: opponentsMove,
+ isDodged: opponentsReceivedDamage === 0 && !isBothUseReflect && !opponentReflectAll,
+ receivedDamage: opponentsReceivedDamage,
+ health: opponentsHealth,
+ },
+ };
+
+ const next = prev ? [newHistory, ...prev] : [newHistory];
+ battleHistoryStorage.set(next);
+ return next;
+ });
+
+ setCurrentPlayers((prev) => {
+ if (prev) {
+ const newPlayers = { ...prev };
+ newPlayers.player.player_settings.health = myHealth;
+ newPlayers.opponent.player_settings.health = opponentsHealth;
+ currentPlayersStorage.set(newPlayers);
+ return newPlayers;
+ }
+ return prev;
+ });
+
+ turnEndCallback();
+ resetLastMoves();
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [lastMoves, myFullDefence, opponentFullDefence]);
+}
+
+export type UseTimerParams = {
+ remainingTime: string | number | bigint | null | undefined;
+ shouldGoOn?: boolean;
+};
+
+export function useTimer({ remainingTime, shouldGoOn = true }: UseTimerParams) {
+ const [timeLeft, setTimeLeft] = useState(null);
+ const startTimeRef = useRef(null);
+
+ useEffect(() => {
+ if (remainingTime === undefined) {
+ setTimeLeft(null);
+ startTimeRef.current = null;
+ } else if (remainingTime === 0) {
+ setTimeLeft(0);
+ } else {
+ const updateTimer = () => {
+ if (!shouldGoOn) {
+ return;
+ }
+ const currentTime = new Date().getTime();
+ if (startTimeRef.current === null) {
+ startTimeRef.current = currentTime;
+ }
+ const timeLeftMilliseconds =
+ Number(remainingTime) + (startTimeRef.current || currentTime) - currentTime - TIME_LEFT_GAP;
+
+ setTimeLeft(Math.max(timeLeftMilliseconds, 0));
+ };
+
+ const timerInterval = setInterval(updateTimer, 1000);
+
+ return () => {
+ clearInterval(timerInterval);
+ };
+ }
+ }, [shouldGoOn, remainingTime]);
+
+ const displayedTime = timeLeft ?? (remainingTime ? Math.max(Number(remainingTime), 0) : null);
+ const formattedTimeLeft = displayedTime !== null ? Math.round(displayedTime / 1000) : '';
+
+ return formattedTimeLeft;
+}
+
+export function useParticipants(battleState?: BattleState | null) {
+ const { account } = useAccount();
+ const [otherPairBattleWatch] = useAtom(otherPairBattleWatchAtom);
+ const [currentPlayers, setCurrentPlayers] = useAtom(currentPlayersAtom);
+
+ const { pairs, players_to_pairs } = battleState || {};
+ const pairId =
+ otherPairBattleWatch ?? players_to_pairs?.find(([address]) => account?.decodedAddress === address)?.[1];
+ const pair = pairs?.find(([number]) => pairId === number)?.[1];
+
+ const { participants, defeated_participants } = battleState || {};
+
+ const { player_1, player_2 } = pair || {};
+ const isAlive = Boolean(participants?.some(([address]) => address === account?.decodedAddress));
+
+ const allParticipants = participants && defeated_participants ? [...participants, ...defeated_participants] : [];
+
+ const participantsMap = allParticipants.reduce(
+ (acc, [key, player]) => {
+ acc[key] = player;
+ return acc;
+ },
+ {} as Record,
+ );
+
+ const playerAddress = otherPairBattleWatch !== null ? player_1 : account?.decodedAddress;
+ const opponentsAddress = playerAddress === player_1 ? player_2 : player_1;
+
+ const player = playerAddress ? participantsMap[playerAddress] : null;
+ const opponent = opponentsAddress ? participantsMap[opponentsAddress] : null;
+
+ const hasPlayer = Boolean(player);
+ const hasOpponent = Boolean(opponent);
+
+ useEffect(() => {
+ if (player && opponent) {
+ setCurrentPlayers({ player, opponent });
+ currentPlayersStorage.set({ player, opponent });
+ }
+ }, [player, opponent, setCurrentPlayers]);
+
+ return { participantsMap, allParticipants, hasPlayer, hasOpponent, isAlive, pair, currentPlayers };
+}
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/store.ts b/frontend/apps/web3-warriors-battle/src/features/game/store.ts
new file mode 100644
index 000000000..e0130a85e
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/store.ts
@@ -0,0 +1,30 @@
+import { atom } from 'jotai';
+import { BattleHistory, CharacterStatsFormValues, CurrentPlayers } from './types';
+import { Appearance } from '@/app/utils';
+
+const getStorage = (key: string) => ({
+ set: (item: T | null) => localStorage.setItem(key, JSON.stringify(item)),
+ get: (): T | null => {
+ const value = localStorage.getItem(key);
+ return value ? JSON.parse(value) : null;
+ },
+});
+
+const CHARACTER_APPEARANCE_LOCAL_KEY = 'character-appearance';
+const CHARACTER_STATS_LOCAL_KEY = 'character-stats';
+const WARRIOR_ID_LOCAL_KEY = 'warrior-id';
+const BATTLE_HISTORY_LOCAL_KEY = 'battle-history';
+const CURRENT_PLAYERS_LOCAL_KEY = 'current-players';
+
+export const characterAppearanceStorage = getStorage(CHARACTER_APPEARANCE_LOCAL_KEY);
+export const characterStatsStorage = getStorage(CHARACTER_STATS_LOCAL_KEY);
+export const warriorIdStorage = getStorage<`0x${string}`>(WARRIOR_ID_LOCAL_KEY);
+export const battleHistoryStorage = getStorage(BATTLE_HISTORY_LOCAL_KEY);
+export const currentPlayersStorage = getStorage(CURRENT_PLAYERS_LOCAL_KEY);
+
+export const characterAppearanceAtom = atom(characterAppearanceStorage.get());
+export const battleHistoryAtom = atom(battleHistoryStorage.get());
+export const currentPlayersAtom = atom(currentPlayersStorage.get());
+
+export const otherPairBattleWatchAtom = atom(null);
+export const isBattleCanceledAtom = atom(false);
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/types.ts b/frontend/apps/web3-warriors-battle/src/features/game/types.ts
new file mode 100644
index 000000000..622cb30ee
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/types.ts
@@ -0,0 +1,40 @@
+import { Move, Player } from '@/app/utils';
+
+export type AssetType = 'head' | 'hat' | 'body' | 'accessories';
+export type AssetsCount = Record;
+
+export type PlayerStatus = 'defeated' | 'alive';
+
+export type PlayerState = {
+ name: string;
+ health: number;
+ attack: number;
+ defence: number;
+ dodge: number;
+ action: Move;
+ isDodged: boolean;
+ receivedDamage: number;
+};
+
+export type CharacterStatsFormValues = {
+ attack: number;
+ defence: number;
+ dodge: number;
+};
+
+type BattleHistoryItem = {
+ action: Move | null;
+ receivedDamage: number;
+ health: number;
+ isDodged: boolean;
+};
+
+export type BattleHistory = {
+ player: BattleHistoryItem;
+ opponent: BattleHistoryItem;
+};
+
+export type CurrentPlayers = {
+ player: Player;
+ opponent: Player;
+};
diff --git a/frontend/apps/web3-warriors-battle/src/features/game/utils.tsx b/frontend/apps/web3-warriors-battle/src/features/game/utils.tsx
new file mode 100644
index 000000000..649513af4
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/game/utils.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+import { AssetType } from './types';
+import { assetsCount, back_colors, body_colors } from './consts';
+import { CharacterView } from './components/character/character';
+
+export const getLazySvg = (assetType: AssetType, index: number) => {
+ const assetNumber = index > 0 ? (index % assetsCount[assetType]) + 1 : 1;
+
+ return React.lazy(() =>
+ import(`./assets/images/character/${assetType}-${assetNumber}.svg`).then((module) => ({
+ default: module.ReactComponent,
+ })),
+ );
+};
+
+export const getRandomNumber = (maxNumber: number) => Math.floor(Math.random() * maxNumber);
+
+export const generateRandomCharacterView = (): CharacterView => ({
+ hat_index: getRandomNumber(assetsCount.hat),
+ head_index: getRandomNumber(assetsCount.head),
+ body_index: getRandomNumber(assetsCount.body),
+ accessory_index: getRandomNumber(assetsCount.accessories),
+ body_color: body_colors[getRandomNumber(body_colors.length)],
+ back_color: back_colors[getRandomNumber(back_colors.length)],
+});
diff --git a/frontend/apps/web3-warriors-battle/src/features/wallet/assets/images/icons/copy.svg b/frontend/apps/web3-warriors-battle/src/features/wallet/assets/images/icons/copy.svg
new file mode 100644
index 000000000..35e5778d8
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/wallet/assets/images/icons/copy.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/wallet/assets/images/icons/edit.svg b/frontend/apps/web3-warriors-battle/src/features/wallet/assets/images/icons/edit.svg
new file mode 100644
index 000000000..18cbd708c
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/wallet/assets/images/icons/edit.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/wallet/assets/images/icons/enkrypt.svg b/frontend/apps/web3-warriors-battle/src/features/wallet/assets/images/icons/enkrypt.svg
new file mode 100644
index 000000000..77ecb8336
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/wallet/assets/images/icons/enkrypt.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/wallet/assets/images/icons/exit.svg b/frontend/apps/web3-warriors-battle/src/features/wallet/assets/images/icons/exit.svg
new file mode 100644
index 000000000..3e1eecafd
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/wallet/assets/images/icons/exit.svg
@@ -0,0 +1,5 @@
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/wallet/assets/images/icons/nova.svg b/frontend/apps/web3-warriors-battle/src/features/wallet/assets/images/icons/nova.svg
new file mode 100644
index 000000000..155e05b61
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/wallet/assets/images/icons/nova.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/wallet/assets/images/icons/polkadot.svg b/frontend/apps/web3-warriors-battle/src/features/wallet/assets/images/icons/polkadot.svg
new file mode 100644
index 000000000..62946202c
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/wallet/assets/images/icons/polkadot.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/wallet/assets/images/icons/subwallet.svg b/frontend/apps/web3-warriors-battle/src/features/wallet/assets/images/icons/subwallet.svg
new file mode 100644
index 000000000..b9a16e354
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/wallet/assets/images/icons/subwallet.svg
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/wallet/assets/images/icons/talisman.svg b/frontend/apps/web3-warriors-battle/src/features/wallet/assets/images/icons/talisman.svg
new file mode 100644
index 000000000..86e00c754
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/wallet/assets/images/icons/talisman.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/apps/web3-warriors-battle/src/features/wallet/assets/index.ts b/frontend/apps/web3-warriors-battle/src/features/wallet/assets/index.ts
new file mode 100644
index 000000000..9193af9e6
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/wallet/assets/index.ts
@@ -0,0 +1,8 @@
+export { ReactComponent as PolkadotSVG } from './images/icons/polkadot.svg';
+export { ReactComponent as SubWalletSVG } from './images/icons/subwallet.svg';
+export { ReactComponent as TalismanSVG } from './images/icons/talisman.svg';
+export { ReactComponent as EnkryptSVG } from './images/icons/enkrypt.svg';
+export { ReactComponent as NovaIcon } from './images/icons/nova.svg';
+export { ReactComponent as ExitIcon } from './images/icons/exit.svg';
+export { ReactComponent as EditIcon } from './images/icons/edit.svg';
+export { ReactComponent as CopyIcon } from './images/icons/copy.svg';
diff --git a/frontend/apps/web3-warriors-battle/src/features/wallet/consts.ts b/frontend/apps/web3-warriors-battle/src/features/wallet/consts.ts
new file mode 100644
index 000000000..efacb47ce
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/wallet/consts.ts
@@ -0,0 +1,22 @@
+import { EnkryptSVG, NovaIcon, PolkadotSVG, SubWalletSVG, TalismanSVG } from './assets';
+import { IWalletExtensionContent, IWalletId } from '@/features/wallet/types';
+
+export const WALLET_ID_LOCAL_STORAGE_KEY = 'wallet';
+
+export const isNovaWallet = !!window?.walletExtension?.isNovaWallet;
+
+export const WALLET = isNovaWallet
+ ? {
+ 'polkadot-js': { name: 'Nova Wallet', SVG: NovaIcon },
+ 'subwallet-js': { name: 'SubWallet', SVG: SubWalletSVG },
+ }
+ : {
+ 'polkadot-js': { name: 'Polkadot JS', SVG: PolkadotSVG },
+ 'subwallet-js': { name: 'SubWallet', SVG: SubWalletSVG },
+ talisman: { name: 'Talisman', SVG: TalismanSVG },
+ enkrypt: { name: 'Enkrypt', SVG: EnkryptSVG },
+ };
+
+export type Wallets = [IWalletId, IWalletExtensionContent][];
+
+export const WALLETS = Object.entries(WALLET) as Wallets;
diff --git a/frontend/apps/web3-warriors-battle/src/features/wallet/hooks.ts b/frontend/apps/web3-warriors-battle/src/features/wallet/hooks.ts
new file mode 100644
index 000000000..059beea50
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/wallet/hooks.ts
@@ -0,0 +1,24 @@
+import { useAccount } from '@gear-js/react-hooks';
+import { useEffect, useState } from 'react';
+import { WALLET } from './consts';
+import { IWalletId } from './types';
+
+function useWallet() {
+ const { wallets, account } = useAccount();
+
+ const defaultWalletId = account?.meta.source as IWalletId | undefined;
+ const [walletId, setWalletId] = useState(defaultWalletId);
+
+ const wallet = walletId ? WALLET[walletId] : undefined;
+ const walletAccounts = wallets && walletId ? wallets[walletId].accounts : undefined;
+
+ useEffect(() => {
+ setWalletId(defaultWalletId);
+ }, [defaultWalletId]);
+
+ const resetWalletId = () => setWalletId(undefined);
+
+ return { wallet, walletId, walletAccounts, setWalletId, resetWalletId };
+}
+
+export { useWallet };
diff --git a/frontend/apps/web3-warriors-battle/src/features/wallet/types.ts b/frontend/apps/web3-warriors-battle/src/features/wallet/types.ts
new file mode 100644
index 000000000..c868a0f75
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/features/wallet/types.ts
@@ -0,0 +1,6 @@
+import { WALLET } from './consts';
+import type { SVGComponent } from '@/app/types';
+
+export type IWalletId = keyof typeof WALLET;
+
+export type IWalletExtensionContent = { name: string; SVG: SVGComponent };
diff --git a/frontend/apps/web3-warriors-battle/src/global.css b/frontend/apps/web3-warriors-battle/src/global.css
new file mode 100644
index 000000000..7e69d086d
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/global.css
@@ -0,0 +1,43 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@keyframes dialog-overlay-show {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes dialog-overlay-hide {
+ from {
+ opacity: 1;
+ }
+ to {
+ opacity: 0;
+ }
+}
+
+@keyframes dialog-content-show {
+ from {
+ opacity: 0;
+ transform: translate(-50%, -50%) scale(0.95);
+ }
+ to {
+ opacity: 1;
+ transform: translate(-50%, -50%) scale(1);
+ }
+}
+
+@keyframes dialog-content-hide {
+ from {
+ opacity: 1;
+ transform: translate(-50%, -50%) scale(1);
+ }
+ to {
+ opacity: 0;
+ transform: translate(-50%, -50%) scale(0.95);
+ }
+}
diff --git a/frontend/apps/web3-warriors-battle/src/global.d.ts b/frontend/apps/web3-warriors-battle/src/global.d.ts
new file mode 100644
index 000000000..cf8024678
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/global.d.ts
@@ -0,0 +1,11 @@
+// declaring .wasm, since TS doesn't support experimental modules
+// source: https://github.com/microsoft/TypeScript/issues/31713
+declare module '*.wasm' {
+ const value: string;
+ export default value;
+}
+
+declare module '*.txt' {
+ const value: string;
+ export default value;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/main.tsx b/frontend/apps/web3-warriors-battle/src/main.tsx
new file mode 100644
index 000000000..762bd4c09
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/main.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { initErrorTracking, logPublicEnvs, initAnalytics } from '@dapps-frontend/error-tracking';
+import { App } from './app';
+
+initAnalytics();
+initErrorTracking();
+logPublicEnvs();
+
+ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
+
+
+ ,
+);
diff --git a/frontend/apps/web3-warriors-battle/src/pages/create-game.module.scss b/frontend/apps/web3-warriors-battle/src/pages/create-game.module.scss
new file mode 100644
index 000000000..ca31d2b14
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/pages/create-game.module.scss
@@ -0,0 +1,34 @@
+.character {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ height: 295px;
+ gap: 16px;
+ & > div {
+ flex: 1;
+ }
+}
+
+.card {
+ margin-top: 32px;
+ width: 686px;
+ padding-bottom: 8px;
+}
+
+.container {
+ margin-top: 48px;
+ width: 1000px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.buttons {
+ display: flex;
+ gap: 16px;
+ & > div {
+ flex: 1;
+ }
+}
diff --git a/frontend/apps/web3-warriors-battle/src/pages/create-game.tsx b/frontend/apps/web3-warriors-battle/src/pages/create-game.tsx
new file mode 100644
index 000000000..54c955b38
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/pages/create-game.tsx
@@ -0,0 +1,12 @@
+import { Background } from '../features/game/components/background';
+import { CreateGameForm } from '@/features/game/components/sections/create-game-form';
+
+export default function CreateGame() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/frontend/apps/web3-warriors-battle/src/pages/find-game.module.scss b/frontend/apps/web3-warriors-battle/src/pages/find-game.module.scss
new file mode 100644
index 000000000..ca31d2b14
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/pages/find-game.module.scss
@@ -0,0 +1,34 @@
+.character {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ height: 295px;
+ gap: 16px;
+ & > div {
+ flex: 1;
+ }
+}
+
+.card {
+ margin-top: 32px;
+ width: 686px;
+ padding-bottom: 8px;
+}
+
+.container {
+ margin-top: 48px;
+ width: 1000px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.buttons {
+ display: flex;
+ gap: 16px;
+ & > div {
+ flex: 1;
+ }
+}
diff --git a/frontend/apps/web3-warriors-battle/src/pages/find-game.tsx b/frontend/apps/web3-warriors-battle/src/pages/find-game.tsx
new file mode 100644
index 000000000..73046fc0a
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/pages/find-game.tsx
@@ -0,0 +1,10 @@
+import { Background } from '../features/game/components/background';
+import { FindGameForm } from '@/features/game/components/sections/find-game-form';
+
+export default function FindGame() {
+ return (
+
+
+
+ );
+}
diff --git a/frontend/apps/web3-warriors-battle/src/pages/game.module.scss b/frontend/apps/web3-warriors-battle/src/pages/game.module.scss
new file mode 100644
index 000000000..5d2fb2e89
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/pages/game.module.scss
@@ -0,0 +1,84 @@
+.character {
+ position: absolute;
+ width: 340px;
+
+ &.left {
+ left: 210px;
+ top: 214px;
+ }
+ &.right {
+ right: 187px;
+ top: 222px;
+ }
+}
+
+.fireSphere {
+ position: absolute;
+ left: 0px;
+ top: -26px;
+ height: 100%;
+ width: 100%;
+}
+
+.redButton {
+ background: #eb5757;
+ color: #ffffff;
+ &:hover {
+ background: #eb5757;
+ }
+}
+
+.buttons {
+ display: flex;
+ position: absolute;
+ top: 566px;
+ gap: 24px;
+}
+
+.exit {
+ position: absolute;
+ top: 692px;
+ left: calc(50% + 364px);
+ height: 40px;
+ width: 93px;
+ &.defeated {
+ top: 650px;
+ }
+}
+
+.cancelTournament {
+ position: absolute;
+ top: 688px;
+ left: calc(50% + 433px);
+ height: 44px;
+ width: 168px;
+ font-size: 14px;
+ &.defeated {
+ top: 646px;
+ }
+}
+
+.historyItem {
+ display: flex;
+}
+
+.endTurnHistory {
+ position: absolute;
+ top: 441px;
+}
+
+.nextButton {
+ width: 579px;
+ z-index: 1;
+ position: absolute;
+ top: 589px;
+}
+
+.gameOver {
+ position: absolute;
+ top: 0px;
+}
+
+.cancelTournamentModal {
+ width: 650px;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/pages/game.tsx b/frontend/apps/web3-warriors-battle/src/pages/game.tsx
new file mode 100644
index 000000000..6661e9911
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/pages/game.tsx
@@ -0,0 +1,310 @@
+import clsx from 'clsx';
+import { useAtom, useAtomValue, useSetAtom } from 'jotai';
+import { useNavigate } from 'react-router-dom';
+import { Button } from '@gear-js/vara-ui';
+import { useAccount } from '@gear-js/react-hooks';
+
+import {
+ Background,
+ BattleTabs,
+ BattleHistorySinc,
+ Character,
+ CharacterStats,
+ BattleHistoryCard,
+ GameButton,
+ GameOverCard,
+ Timer,
+ SphereAnimation,
+ FireballCanvas,
+ GameSpinner,
+} from '@/features/game/components';
+import { AttackButtonIcon, DefenceButtonIcon, UltimateButtonIcon } from '@/features/game/assets/images';
+import { useEffect, useState } from 'react';
+import { Loader, Modal } from '@/components';
+import { ExitIcon } from '@/features/wallet/assets';
+import {
+ Move,
+ useCancelTournamentMessage,
+ useConfigQuery,
+ useExitGameMessage,
+ useMakeMoveMessage,
+ useMyBattleQuery,
+ useStartNextFightMessage,
+} from '@/app/utils';
+import { ROUTES } from '@/app/consts';
+import {
+ battleHistoryAtom,
+ battleHistoryStorage,
+ currentPlayersAtom,
+ currentPlayersStorage,
+ otherPairBattleWatchAtom,
+} from '@/features/game/store';
+import { useParticipants, usePending } from '@/features/game/hooks';
+import styles from './game.module.scss';
+
+export default function GamePage() {
+ const navigate = useNavigate();
+ const { account } = useAccount();
+ const { battleState, isFetching } = useMyBattleQuery();
+ const { pending } = usePending();
+ const { config } = useConfigQuery();
+ const { cancelTournamentMessage } = useCancelTournamentMessage();
+ const { startNextFightMessage } = useStartNextFightMessage();
+ const { makeMoveMessage } = useMakeMoveMessage();
+ const { exitGameMessage } = useExitGameMessage();
+
+ const [isOpenCancelTournamentModal, setIsOpenCancelTournamentModal] = useState(false);
+
+ const [tappedButton, setTappedButton] = useState(null);
+ const [showAnimation, setShowAnimation] = useState(false);
+
+ const battleHistory = useAtomValue(battleHistoryAtom);
+ const lastTurnHistory = battleHistory?.[0];
+
+ const [isShowTurnEndCard, setIsShowTurnEndCard] = useState(false);
+
+ const [otherPairBattleWatch] = useAtom(otherPairBattleWatchAtom);
+ const isShowOtherBattle = Boolean(battleState?.pairs.some(([pairId]) => pairId === otherPairBattleWatch));
+
+ const { allParticipants, isAlive, hasPlayer, hasOpponent, participantsMap, pair, currentPlayers } =
+ useParticipants(battleState);
+
+ const { player, opponent } = currentPlayers || {};
+
+ const { admin, state, waiting_player, bid } = battleState || {};
+
+ useEffect(() => {
+ if (!isFetching && !battleState) {
+ navigate(ROUTES.HOME);
+ }
+ }, [isFetching, battleState, navigate]);
+
+ const turnEndCallback = () => {
+ setIsShowTurnEndCard(true);
+ setTappedButton(null);
+ setShowAnimation(true);
+ setTimeout(() => {
+ setShowAnimation(false);
+ }, 3000);
+ };
+
+ const setBattleHistory = useSetAtom(battleHistoryAtom);
+ const setCurrentPlayers = useSetAtom(currentPlayersAtom);
+
+ if (!battleState || !config || !state || !account) {
+ return ;
+ }
+
+ const showStartNextBattle = !hasOpponent && waiting_player?.[0] !== account.decodedAddress && isAlive;
+ const showWaitingForOpponent = waiting_player?.[0] === account.decodedAddress;
+ const isAdmin = account.decodedAddress === admin;
+ const isTournamentOver = 'gameIsOver' in state;
+ const isCurrentDraw =
+ !isTournamentOver && battleHistory?.[0].player.health === 0 && battleHistory?.[0].opponent.health === 0;
+
+ const onAttackClick = () => {
+ setTappedButton('Attack');
+ makeMoveMessage('Attack', { onError: () => setTappedButton(null) });
+ };
+ const onReflectClick = () => {
+ setTappedButton('Reflect');
+ makeMoveMessage('Reflect', { onError: () => setTappedButton(null) });
+ };
+ const onUltimateClick = () => {
+ setTappedButton('Ultimate');
+ makeMoveMessage('Ultimate', { onError: () => setTappedButton(null) });
+ };
+
+ const { round_start_time } = pair || {};
+ const roundDuration = config.time_for_move_in_blocks * config.block_duration_ms;
+ const timeLeft = round_start_time ? Number(round_start_time) + roundDuration - Date.now() : null;
+
+ const onCancelTournament = () => {
+ cancelTournamentMessage({ onSuccess: () => navigate(ROUTES.HOME) });
+ };
+
+ const onExitGame = () => {
+ exitGameMessage({
+ onSuccess: () => navigate(ROUTES.HOME),
+ });
+ };
+
+ return (
+ <>
+
+ {player && (
+ <>
+
+ {(player.player_settings.health !== 0 || isCurrentDraw) && (
+
+
+
+
+
+ )}
+ >
+ )}
+
+ {hasPlayer && hasOpponent && !showAnimation && (
+
+ )}
+
+ {opponent && (
+ <>
+
+ {(opponent.player_settings.health !== 0 || isCurrentDraw) && (
+
+
+
+ {showAnimation && (
+
+ )}
+
+ )}
+ >
+ )}
+
+ {lastTurnHistory && showAnimation && }
+
+ {showWaitingForOpponent ||
+ (hasOpponent && hasPlayer && player && opponent && !isTournamentOver && !isShowOtherBattle && (
+
+ }
+ pending={tappedButton === 'Attack' || pending}
+ disabled={showWaitingForOpponent || !!(tappedButton && tappedButton !== 'Attack')}
+ />
+ }
+ pending={tappedButton === 'Reflect' || pending}
+ turnsBlocked={player.reflect_reload}
+ disabled={showWaitingForOpponent || !!(tappedButton && tappedButton !== 'Reflect')}
+ />
+ }
+ pending={tappedButton === 'Ultimate' || pending}
+ turnsBlocked={player.ultimate_reload}
+ disabled={showWaitingForOpponent || !!(tappedButton && tappedButton !== 'Ultimate')}
+ />
+
+ ))}
+
+ {showStartNextBattle && !isTournamentOver && !isShowOtherBattle && (
+ {
+ setBattleHistory(null);
+ battleHistoryStorage.set(null);
+ setCurrentPlayers(null);
+ currentPlayersStorage.set(null);
+ startNextFightMessage();
+ }}
+ disabled={pending}
+ />
+ )}
+
+ {showWaitingForOpponent && }
+
+ {player && opponent && pair && (
+
+ )}
+
+ {isShowTurnEndCard &&
+ lastTurnHistory &&
+ player &&
+ opponent &&
+ !isTournamentOver &&
+ !isCurrentDraw &&
+ isAlive && (
+
+
+ setIsShowTurnEndCard(false)}
+ />
+
+ )}
+
+
+
+ {isAdmin ? (
+ (isTournamentOver ? onCancelTournament() : setIsOpenCancelTournamentModal(true))}
+ disabled={pending}
+ />
+ ) : (
+
+ )}
+
+
+
+ {isOpenCancelTournamentModal && (
+ setIsOpenCancelTournamentModal(false)}
+ buttons={
+ <>
+
+ setIsOpenCancelTournamentModal(false)}
+ disabled={pending}
+ />
+ >
+ }
+ />
+ )}
+
+ >
+ );
+}
diff --git a/frontend/apps/web3-warriors-battle/src/pages/generate-character.module.scss b/frontend/apps/web3-warriors-battle/src/pages/generate-character.module.scss
new file mode 100644
index 000000000..42234bf55
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/pages/generate-character.module.scss
@@ -0,0 +1,50 @@
+.character {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 295px;
+}
+
+.card {
+ position: absolute;
+ top: 158px;
+ width: 668px;
+
+ height: 472px;
+
+ &Filled {
+ top: 56px;
+ width: 881px;
+
+ & > :first-child {
+ gap: 24px;
+ padding-top: 32px;
+ padding-bottom: 0;
+ gap: 0px;
+ }
+ }
+}
+
+.container {
+ position: absolute;
+ top: 556px;
+ width: 1000px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.buttons {
+ display: flex;
+ gap: 16px;
+ & > div {
+ flex: 1;
+ }
+}
+
+.generate {
+ width: 306px;
+}
\ No newline at end of file
diff --git a/frontend/apps/web3-warriors-battle/src/pages/generate-character.tsx b/frontend/apps/web3-warriors-battle/src/pages/generate-character.tsx
new file mode 100644
index 000000000..9a0655f54
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/pages/generate-character.tsx
@@ -0,0 +1,97 @@
+import { Button } from '@gear-js/vara-ui';
+import clsx from 'clsx';
+import { useSetAtom } from 'jotai';
+import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+import { Card, CardButton } from '@/components';
+import { Background, CharacterStatsForm, Character } from '@/features/game/components';
+import { AdminIcon, SearchIcon } from '@/features/game/assets/images';
+import { CharacterView } from '@/features/game/components/character/character';
+import { generateRandomCharacterView } from '@/features/game/utils';
+import { ROUTES } from '@/app/consts';
+
+import {
+ characterAppearanceAtom,
+ characterAppearanceStorage,
+ characterStatsStorage,
+ warriorIdStorage,
+} from '@/features/game/store';
+import { CharacterStatsFormValues } from '@/features/game/types';
+import styles from './generate-character.module.scss';
+
+export default function GenerateCharacter() {
+ const navigate = useNavigate();
+ const [characterView, setCharacterView] = useState(
+ characterAppearanceStorage.get() || generateRandomCharacterView(),
+ );
+ const [prevCharacterView, setPrevCharacterView] = useState(generateRandomCharacterView());
+ const [characterStats, setCharacterStats] = useState(characterStatsStorage.get());
+ const [isNextDisabled, setIsNextDisabled] = useState(true);
+
+ const generate = () => {
+ setPrevCharacterView(characterView);
+ setCharacterView(generateRandomCharacterView());
+ };
+
+ const setCharacterAppearance = useSetAtom(characterAppearanceAtom);
+
+ const onNextClick = (to: string) => {
+ if (!characterStats) return;
+ setCharacterAppearance(characterView);
+ characterStatsStorage.set(characterStats);
+ characterAppearanceStorage.set(characterView);
+ warriorIdStorage.set(null);
+ navigate(to);
+ };
+
+ return (
+ <>
+
+ {
+ setCharacterStats(stats);
+ setIsNextDisabled(!isValid);
+ }}
+ />
+ }>
+
+ }
+ />
+
+
+
+
+
+
onNextClick(ROUTES.FIND_GAME)}
+ icon={ }
+ title="Find a private game"
+ description="To find the game, you need to enter the administrator's address."
+ disabled={isNextDisabled}
+ />
+ onNextClick(ROUTES.CREATE_GAME)}
+ icon={ }
+ title="Create a game in administrator mode"
+ description="Create a game and specify your participation rules."
+ disabled={isNextDisabled}
+ />
+
+
navigate(-1)} />
+
+
+ >
+ );
+}
diff --git a/frontend/apps/web3-warriors-battle/src/pages/home.module.scss b/frontend/apps/web3-warriors-battle/src/pages/home.module.scss
new file mode 100644
index 000000000..25e1ed269
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/pages/home.module.scss
@@ -0,0 +1,11 @@
+.card {
+ margin-top: 92px;
+}
+
+.container {
+ width: 620px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding: 0 22px;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/pages/home.tsx b/frontend/apps/web3-warriors-battle/src/pages/home.tsx
new file mode 100644
index 000000000..1f0b23677
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/pages/home.tsx
@@ -0,0 +1,35 @@
+import { useNavigate } from 'react-router-dom';
+import { Card } from '@/components';
+import { ROUTES } from '@/app/consts';
+import { Background } from '@/features/game/components';
+import { CardButton } from '@/components/ui/card-button';
+import { CodeSlashIcon, MagicLineIcon } from '@/features/game/assets/images';
+import { useResetCharacterStats } from '@/features/game/hooks';
+
+import styles from './home.module.scss';
+
+export default function Home() {
+ const navigate = useNavigate();
+ useResetCharacterStats();
+
+ return (
+
+
+
+ navigate(ROUTES.IMPORT_CHARACTER)}
+ icon={ }
+ title="Import Character from Program"
+ description="Enter the program ID to view your character."
+ />
+ navigate(ROUTES.GENERATE_CHARACTER)}
+ icon={ }
+ title="Generate Character Without a Code"
+ description="Simply generate a random appearance and attributes."
+ />
+
+
+
+ );
+}
diff --git a/frontend/apps/web3-warriors-battle/src/pages/import-character.module.scss b/frontend/apps/web3-warriors-battle/src/pages/import-character.module.scss
new file mode 100644
index 000000000..4613459ee
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/pages/import-character.module.scss
@@ -0,0 +1,46 @@
+.character {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 295px;
+ gap: 16px;
+}
+
+.card {
+ position: absolute;
+ top: 158px;
+ width: 668px;
+ max-height: 478px;
+ text-align: left;
+
+ &Filled {
+ top: 32px;
+ width: 881px;
+
+ & > :first-child {
+ gap: 24px;
+ padding-top: 32px;
+ padding-bottom: 0;
+ }
+ }
+}
+
+.container {
+ position: absolute;
+ top: 556px;
+ width: 1000px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.buttons {
+ display: flex;
+ gap: 16px;
+ & > div {
+ flex: 1;
+ }
+}
diff --git a/frontend/apps/web3-warriors-battle/src/pages/import-character.tsx b/frontend/apps/web3-warriors-battle/src/pages/import-character.tsx
new file mode 100644
index 000000000..7de8947d7
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/pages/import-character.tsx
@@ -0,0 +1,101 @@
+import clsx from 'clsx';
+import { useSetAtom } from 'jotai';
+import { Input, Button } from '@gear-js/vara-ui';
+import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+import { Card, CardButton } from '@/components';
+import { Background, CharacterStatsForm, Character } from '@/features/game/components';
+import { AdminIcon, SearchIcon } from '@/features/game/assets/images';
+import { ROUTES } from '@/app/consts';
+import { useGetAppearanceQuery } from '@/app/utils/sails/queries/use-get-appearance-query';
+import { CharacterStatsFormValues } from '@/features/game/types';
+import {
+ characterAppearanceAtom,
+ characterAppearanceStorage,
+ characterStatsStorage,
+ warriorIdStorage,
+} from '@/features/game/store';
+
+import styles from './import-character.module.scss';
+
+export default function ImportCharacter() {
+ const navigate = useNavigate();
+ const [address, setAddress] = useState('');
+
+ const { appearance, error } = useGetAppearanceQuery(address);
+ const [characterStats, setCharacterStats] = useState(characterStatsStorage.get());
+ const [isNextDisabled, setIsNextDisabled] = useState(true);
+
+ const isCharacterFound = Boolean(appearance);
+
+ const setCharacterAppearance = useSetAtom(characterAppearanceAtom);
+
+ const onNextClick = (to: string) => {
+ if (!characterStats || !appearance) return;
+ const { attack, defence, dodge } = characterStats;
+
+ setCharacterAppearance(appearance);
+ warriorIdStorage.set(address as `0x${string}`);
+ characterAppearanceStorage.set(appearance);
+ characterStatsStorage.set({ attack, defence, dodge });
+ navigate(to);
+ };
+
+ return (
+ <>
+
+ {
+ setCharacterStats(stats);
+ setIsNextDisabled(!isValid);
+ }}
+ />
+ )
+ }>
+ setAddress(e.target.value.trim())}
+ error={error && 'The program must include the "Warrior" service and the "getAppearance" function.'}
+ />
+ {isCharacterFound && appearance && (
+
+
+
+ )}
+
+
+
+
onNextClick(ROUTES.FIND_GAME)}
+ icon={ }
+ title="Find a private game"
+ description="To find the game, you need to enter the administrator's address."
+ disabled={!isCharacterFound || isNextDisabled}
+ />
+ onNextClick(ROUTES.CREATE_GAME)}
+ icon={ }
+ title="Create a game in administrator mode"
+ description="Create a game and specify your participation rules."
+ disabled={!isCharacterFound || isNextDisabled}
+ />
+
+
navigate(ROUTES.HOME)} />
+
+
+ >
+ );
+}
diff --git a/frontend/apps/web3-warriors-battle/src/pages/index.tsx b/frontend/apps/web3-warriors-battle/src/pages/index.tsx
new file mode 100644
index 000000000..51c9bc2f4
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/pages/index.tsx
@@ -0,0 +1,71 @@
+import { Suspense, useEffect } from 'react';
+import { Route, useNavigate, useLocation } from 'react-router-dom';
+import { ErrorTrackingRoutes } from '@dapps-frontend/error-tracking';
+import { ROUTES } from '@/app/consts';
+import { Loader, NotAuthorized } from '@/components';
+import { useAccount } from '@gear-js/react-hooks';
+
+import Home from './home';
+import { useMyBattleQuery } from '@/app/utils';
+import ImportCharacterPage from './import-character';
+import GenerateCharacterPage from './generate-character';
+import CreateGamePage from './create-game';
+import FindGamePage from './find-game';
+import NotFoundPage from './not-found';
+import WaitingPage from './waiting';
+import GamePage from './game';
+import OnboardingPage from './onboarding';
+
+const routes = [
+ { path: ROUTES.HOME, Page: Home },
+ { path: ROUTES.NOTFOUND, Page: NotFoundPage },
+ { path: ROUTES.IMPORT_CHARACTER, Page: ImportCharacterPage },
+ { path: ROUTES.GENERATE_CHARACTER, Page: GenerateCharacterPage },
+ { path: ROUTES.CREATE_GAME, Page: CreateGamePage },
+ { path: ROUTES.FIND_GAME, Page: FindGamePage },
+ { path: ROUTES.WAITING, Page: WaitingPage },
+ { path: ROUTES.GAME, Page: GamePage },
+ { path: ROUTES.ONBOARDING, Page: OnboardingPage },
+];
+
+export function Routing() {
+ const { account } = useAccount();
+
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ const { battleState, isFetching } = useMyBattleQuery();
+
+ useEffect(() => {
+ if (battleState && !isFetching) {
+ const { state } = battleState;
+ if ('registration' in state && location.pathname !== ROUTES.WAITING && location.pathname !== ROUTES.ONBOARDING) {
+ navigate(ROUTES.WAITING);
+ }
+ if ('started' in state && location.pathname !== ROUTES.GAME && location.pathname !== ROUTES.ONBOARDING) {
+ navigate(ROUTES.GAME);
+ }
+ if ('gameIsOver' in state && location.pathname !== ROUTES.GAME && location.pathname !== ROUTES.ONBOARDING) {
+ navigate(ROUTES.GAME);
+ }
+ }
+ }, [battleState, isFetching, navigate, location.pathname]);
+
+ return account ? (
+
+ {routes.map(({ path, Page }) => (
+ }>
+
+
+ }
+ />
+ ))}
+
+ ) : (
+
+ );
+}
diff --git a/frontend/apps/web3-warriors-battle/src/pages/not-found.tsx b/frontend/apps/web3-warriors-battle/src/pages/not-found.tsx
new file mode 100644
index 000000000..67861f09c
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/pages/not-found.tsx
@@ -0,0 +1,5 @@
+import { NotFound } from '@/components/layout/not-found';
+
+export default function NotFoundPage() {
+ return ;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/pages/onboarding.module.scss b/frontend/apps/web3-warriors-battle/src/pages/onboarding.module.scss
new file mode 100644
index 000000000..3c45b914f
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/pages/onboarding.module.scss
@@ -0,0 +1,152 @@
+.character {
+ position: absolute;
+ width: 340px;
+
+ &.left {
+ left: calc(50% - 512px);
+ top: 214px;
+ }
+ &.right {
+ right: calc(50% - 512px);
+ top: 222px;
+ }
+}
+
+.redButton {
+ background: #eb5757;
+ color: #ffffff;
+ &:hover {
+ background: #eb5757;
+ }
+}
+
+.attack {
+ color: #bb3939;
+ font-weight: 600;
+}
+.reflect {
+ color: #7aa330;
+ font-weight: 600;
+}
+
+.dodge {
+ color: #fba21c;
+ font-weight: 600;
+}
+
+.ultimate {
+ color: #01ae84;
+ font-weight: 600;
+}
+
+.exit {
+ position: absolute;
+ top: 692px;
+ right: 263px;
+ height: 40px;
+ width: 93px;
+}
+
+.cancelTournament {
+ position: absolute;
+ top: 688px;
+ right: 137.5px;
+ height: 44px;
+ width: 168px;
+ font-size: 14px;
+}
+
+.players {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ > div {
+ gap: 8;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ }
+}
+
+.modal {
+ position: relative;
+ max-width: 670px;
+ padding: 32px 40px;
+ > p {
+ margin-top: 12px;
+ }
+}
+
+.modalButtons {
+ display: flex;
+ align-items: center;
+ top: 566px;
+ gap: 16px;
+ margin-top: 32px;
+ > button {
+ flex: 1;
+ }
+ > :last-child {
+ margin-left: 8px;
+ }
+}
+
+.blur-backdrop {
+ position: fixed;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ pointer-events: none;
+ z-index: 20;
+
+ background-color: #000000a6;
+ backdrop-filter: blur(8px);
+}
+
+.characterCircle {
+ position: relative;
+ top: 305px;
+ right: 366px;
+ overflow: hidden;
+ margin: 0 auto;
+ height: 354px;
+ width: 354px;
+ border-radius: 50%;
+ > :first-child {
+ left: 354px;
+ top: -204px;
+ > :last-child {
+ left: -346px;
+ top: 210px;
+ }
+ }
+}
+
+.backdrop {
+ align-items: flex-end;
+ padding: 29px;
+ height: 100%;
+
+ &::backdrop {
+ background-color: transparent;
+ backdrop-filter: none;
+ }
+}
+
+.gameButtons {
+ position: absolute;
+ display: flex;
+ top: -126px;
+ left: -4px;
+ gap: 24px;
+}
+
+.highlighted {
+ z-index: 30;
+}
+
+.segmented {
+ position: absolute;
+ top: -106px;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/pages/onboarding.tsx b/frontend/apps/web3-warriors-battle/src/pages/onboarding.tsx
new file mode 100644
index 000000000..bd42cf789
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/pages/onboarding.tsx
@@ -0,0 +1,191 @@
+import clsx from 'clsx';
+import { useNavigate } from 'react-router-dom';
+import { Button } from '@gear-js/vara-ui';
+
+import { Background } from '@/features/game/components';
+import { Character } from '@/features/game/components/character';
+import { CharacterStats } from '@/features/game/components/character-stats';
+import {
+ AttackButtonIcon,
+ DefenceButtonIcon,
+ UltimateButtonIcon,
+ UserSkullIcon,
+ UserSmileIcon,
+} from '@/features/game/assets/images';
+import { GameButton, Timer } from '@/features/game/components';
+import { useEffect, useState } from 'react';
+import { Modal, Segmented, Text } from '@/components';
+import { mockCharacterView, mockCharacterView2, mockPlayer1, mockPlayer2 } from '@/features/game/consts';
+import { characterAppearanceStorage, characterStatsStorage } from '@/features/game/store';
+import styles from './onboarding.module.scss';
+
+const steps = [
+ {
+ title: 'This is your character',
+ children: (
+
+ You will attack and defend against the opponent. The goal is to defeat as many opponents as possible. The player
+ who defeats all others wins the game.
+
+ ),
+ },
+ {
+ title: 'How to play',
+ children: (
+ <>
+
+ Press Attack to deal damage to your opponent. The more attack points
+ you have, the harder you'll hit.
+
+
+ Reflect returns a part of the enemy's attack damage equal to your
+ defenсe. It can only be used once every few turns.
+
+
+ Ultimate doubles your attack damage and is also active once every few
+ turns.
+
+ >
+ ),
+ },
+ {
+ title: 'Players stats overview',
+ children: (
+ <>
+
+ Attack points show the strength of the damage you deal to the opponent.
+
+
+ Defence indicates how efficiently opponent's attack can be reflected.
+
+
+ Dodge chance: the probability of fully evading the opponent’s attack.
+
+
+ You can also track the damage dealt to you by the opponent, which will affect your{' '}
+ Health stat.
+
+ If it's your turn, your character's image will be highlighted in green.
+ >
+ ),
+ },
+ {
+ title: 'Players list and battle history',
+ children: (
+
+ At the bottom of the screen, you can view the list of active and eliminated players, as well as the history of
+ the battle with all the moves you and your opponent made.
+
+ ),
+ },
+];
+
+export default function OnboardingPage() {
+ const navigate = useNavigate();
+ const characterView = characterAppearanceStorage.get() || mockCharacterView;
+ const characterStats = characterStatsStorage.get();
+
+ useEffect(() => {
+ window.scrollTo({ top: 0 });
+ }, []);
+
+ const onClose = () => {
+ navigate(-1);
+ };
+
+ const [step, setStep] = useState(0);
+
+ const timeLeft = 12000;
+
+ const segmentedOptions = [
+ {
+ label: (
+
+
Players:
+
+ 12
+
+
+ 36
+
+
+ ),
+ value: 'players',
+ },
+ {
+ label: 'Battle History ',
+ value: 'history',
+ },
+ ];
+
+ const { title, children } = steps[step];
+
+ return (
+ <>
+
+
+
+
+
+
+ { }
+
+
+
+
+
+
+
+ {children}
+
+ {step !== 0 && setStep(step - 1)} />}
+ {step === steps.length - 1 ? (
+
+ ) : (
+ setStep(step + 1)} />
+ )}
+
+ {step + 1}/{steps.length}
+
+
+
+ {step === 1 && (
+
+ } />
+ } />
+ } />
+
+ )}
+ {step === 3 && (
+ {}} />
+ )}
+
+
+
+
+ {step === 0 && (
+
+ )}
+
+ >
+ );
+}
diff --git a/frontend/apps/web3-warriors-battle/src/pages/waiting.module.scss b/frontend/apps/web3-warriors-battle/src/pages/waiting.module.scss
new file mode 100644
index 000000000..02bf9a216
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/pages/waiting.module.scss
@@ -0,0 +1,67 @@
+.background {
+ margin-bottom: 180px;
+}
+
+.card {
+ position: absolute;
+ width: 620px;
+ top: 48px;
+ right: 120px;
+}
+
+.character {
+ position: absolute;
+ width: 620px;
+ top: 258px;
+ left: 211px;
+}
+
+.tutorial {
+ position: absolute;
+ width: 526px;
+ top: 524px;
+ left: 120px;
+}
+
+.redButton {
+ background: #eb5757;
+ color: #ffffff;
+ &:hover {
+ background: #eb5757;
+ }
+}
+
+.buttons {
+ display: flex;
+ gap: 24px;
+ width: 100%;
+ & > * {
+ flex: 1;
+ }
+}
+
+.tutorialButton {
+ max-width: 258px;
+}
+
+.info {
+ display: flex;
+ gap: 6px;
+ color: #58696eb2;
+}
+
+.footer {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 16px;
+}
+
+.leaveModal {
+ width: 508px;
+}
+
+.cancelTournamentModal {
+ width: 650px;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/pages/waiting.tsx b/frontend/apps/web3-warriors-battle/src/pages/waiting.tsx
new file mode 100644
index 000000000..de97017d8
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/pages/waiting.tsx
@@ -0,0 +1,179 @@
+import { Button } from '@gear-js/vara-ui';
+import { useAccount } from '@gear-js/react-hooks';
+import { useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+import { Card, Loader, Text, Modal } from '@/components';
+import { Background, GameCanceledModal, WaitList } from '@/features/game/components';
+import { Character } from '@/features/game/components/character';
+import { CharacterStats } from '@/features/game/components/character-stats';
+import { InfoIcon } from '@/features/game/assets/images';
+import { ROUTES } from '@/app/consts';
+import {
+ useMyBattleQuery,
+ useCancelRegisterMessage,
+ useCancelTournamentMessage,
+ useStartBattleMessage,
+} from '@/app/utils';
+import { usePending, useResetGameState } from '@/features/game/hooks';
+import { useEventBattleCanceledSubscription } from '@/app/utils/sails/events';
+import styles from './waiting.module.scss';
+
+export default function WaitingPage() {
+ const navigate = useNavigate();
+ const { account } = useAccount();
+
+ const { battleState, isFetching } = useMyBattleQuery();
+ const { cancelTournamentMessage } = useCancelTournamentMessage();
+ const { cancelRegisterMessage } = useCancelRegisterMessage();
+ const { startBattleMessage } = useStartBattleMessage();
+ const { isBattleCanceled } = useEventBattleCanceledSubscription(battleState?.admin);
+
+ const [isOpenLeaveModal, setIsOpenLeaveModal] = useState(false);
+ const [isOpenCancelTournamentModal, setIsOpenCancelTournamentModal] = useState(false);
+
+ useResetGameState();
+
+ useEffect(() => {
+ if (!isFetching && !battleState && !isBattleCanceled) {
+ navigate(ROUTES.HOME);
+ }
+ }, [isFetching, battleState, isBattleCanceled, navigate]);
+
+ const { pending } = usePending();
+
+ if (isBattleCanceled) {
+ return ;
+ }
+
+ if (!battleState || !account) {
+ return ;
+ }
+
+ const { participants, battle_name, admin } = battleState;
+ const player = participants.find(([address]) => address === account.decodedAddress)?.[1];
+
+ if (!player) {
+ return Character not found
;
+ }
+
+ const { appearance, player_settings, user_name } = player;
+
+ const items = participants?.map(([address, { user_name }]) => ({ name: user_name, address }));
+ const isAdmin = account.decodedAddress === admin;
+
+ const onStartTournament = () => {
+ startBattleMessage({ onSuccess: () => navigate(ROUTES.GAME) });
+ };
+
+ const onCancelTournament = () => {
+ cancelTournamentMessage({
+ onSuccess: () => navigate(ROUTES.HOME),
+ onError: () => setIsOpenCancelTournamentModal(false),
+ });
+ };
+
+ const onLeaveGame = () => {
+ cancelRegisterMessage({ onSuccess: () => navigate(ROUTES.HOME), onError: () => setIsOpenLeaveModal(false) });
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ {isAdmin ? (
+ <>
+ setIsOpenCancelTournamentModal(true)}
+ disabled={pending}
+ />
+
+ >
+ ) : (
+ setIsOpenLeaveModal(true)}
+ disabled={pending}
+ />
+ )}
+
+
+
+ To change your character, you need to leave the game.
+
+
+
+
+
+
+ navigate(ROUTES.ONBOARDING)}
+ className={styles.tutorialButton}
+ disabled={pending}
+ />
+
+
+
+ {isOpenLeaveModal && (
+ setIsOpenLeaveModal(false)}
+ buttons={
+ <>
+ setIsOpenLeaveModal(false)} />
+
+ >
+ }
+ />
+ )}
+
+ {isOpenCancelTournamentModal && (
+ setIsOpenCancelTournamentModal(false)}
+ buttons={
+ <>
+
+ setIsOpenCancelTournamentModal(false)}
+ />
+ >
+ }
+ />
+ )}
+
+ >
+ );
+}
diff --git a/frontend/apps/web3-warriors-battle/src/utils/_breakpoints.scss b/frontend/apps/web3-warriors-battle/src/utils/_breakpoints.scss
new file mode 100644
index 000000000..1ae722281
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/utils/_breakpoints.scss
@@ -0,0 +1,30 @@
+$xl: 1200px;
+$lg: 992px;
+$md: 768px;
+$sm: 576px;
+
+@function getMaxWidth($width) {
+ @return 'max-width: #{$width - 1}';
+}
+
+@mixin breakpoint($width) {
+ @media screen and (getMaxWidth($width)) {
+ @content;
+ }
+}
+
+@mixin xl {
+ @include breakpoint($xl);
+}
+
+@mixin lg {
+ @include breakpoint($lg);
+}
+
+@mixin md {
+ @include breakpoint($md);
+}
+
+@mixin sm {
+ @include breakpoint($sm);
+}
diff --git a/frontend/apps/web3-warriors-battle/src/utils/_index.scss b/frontend/apps/web3-warriors-battle/src/utils/_index.scss
new file mode 100644
index 000000000..173a09c9d
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/utils/_index.scss
@@ -0,0 +1,3 @@
+@forward 'breakpoints';
+@forward 'mixins';
+@forward 'variables';
diff --git a/frontend/apps/web3-warriors-battle/src/utils/_mixins.scss b/frontend/apps/web3-warriors-battle/src/utils/_mixins.scss
new file mode 100644
index 000000000..0f6551ae6
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/utils/_mixins.scss
@@ -0,0 +1,76 @@
+@use 'variables' as *;
+
+@mixin gap($value, $direction: bottom) {
+ $margin: 'margin-#{$direction}';
+
+ > *:not(:last-child) {
+ #{$margin}: $value;
+ }
+}
+
+@mixin transition($prop: all) {
+ transition: 0.25s $prop;
+}
+
+@mixin square($size) {
+ width: $size;
+ height: $size;
+}
+
+@mixin circle($size) {
+ @include square($size);
+ border-radius: 50%;
+}
+
+@mixin flexCenter {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+@mixin svgColor($color) {
+ path {
+ fill: $color;
+ }
+}
+
+@mixin absoluteCenter {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+
+@mixin textOverflow {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+@mixin flex-gap($gap: 1.25em) {
+ $gap-half: calc(#{$gap} * 0.5);
+
+ @if type-of($gap) == 'number' {
+ $gap-half: $gap * 0.5;
+ } @else if type-of($gap) == 'list' {
+ $gap-half: ();
+ @each $value in $gap {
+ $value-half: calc(#{$value} * 0.5);
+ @if type-of($value) == 'number' {
+ $value-half: $value * 0.5;
+ }
+ $gap-half: append($gap-half, $value-half);
+ }
+ }
+
+ @include gap($gap-half, right);
+
+ @supports selector(:first-child) {
+ gap: $gap;
+
+ @include gap(0, right);
+ }
+}
+
+@mixin loaderWrapper {
+ position: relative;
+}
diff --git a/frontend/apps/web3-warriors-battle/src/utils/_variables.scss b/frontend/apps/web3-warriors-battle/src/utils/_variables.scss
new file mode 100644
index 000000000..aa5da9ae7
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/utils/_variables.scss
@@ -0,0 +1,7 @@
+$headerPadding: 20px;
+
+$inputWidth: 480px;
+
+$xCardPadding: 32px;
+$yCardPadding: 24px;
+$cardPadding: $yCardPadding $xCardPadding;
diff --git a/frontend/apps/web3-warriors-battle/src/vite-env.d.ts b/frontend/apps/web3-warriors-battle/src/vite-env.d.ts
new file mode 100644
index 000000000..11f02fe2a
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/frontend/apps/web3-warriors-battle/tailwind.config.js b/frontend/apps/web3-warriors-battle/tailwind.config.js
new file mode 100644
index 000000000..9204845a2
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/tailwind.config.js
@@ -0,0 +1,9 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
+ theme: {},
+ corePlugins: {
+ container: false,
+ },
+ plugins: [],
+};
diff --git a/frontend/apps/web3-warriors-battle/tsconfig.json b/frontend/apps/web3-warriors-battle/tsconfig.json
new file mode 100644
index 000000000..3edc4146d
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/tsconfig.json
@@ -0,0 +1,32 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "paths": {
+ "@/*": ["./src/*"]
+ // "@/app/*": ["./src/app/*"],
+ // "@/assets/*": ["./src/assets/*"],
+ // "@/components/*": ["./src/components/*"]
+ },
+ "types": ["vite-plugin-svgr/client"]
+ },
+ "include": ["src"],
+ "references": [
+ {
+ "path": "./tsconfig.node.json"
+ }
+ ]
+}
diff --git a/frontend/apps/web3-warriors-battle/tsconfig.node.json b/frontend/apps/web3-warriors-battle/tsconfig.node.json
new file mode 100644
index 000000000..9d31e2aed
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/tsconfig.node.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/frontend/apps/web3-warriors-battle/vite.config.ts b/frontend/apps/web3-warriors-battle/vite.config.ts
new file mode 100644
index 000000000..f024e6212
--- /dev/null
+++ b/frontend/apps/web3-warriors-battle/vite.config.ts
@@ -0,0 +1,31 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react-swc';
+import path from 'path';
+import { nodePolyfills } from 'vite-plugin-node-polyfills';
+import eslint from 'vite-plugin-eslint';
+import svgr from 'vite-plugin-svgr';
+
+// https://vitejs.dev/config/
+export default defineConfig(({ mode }) => {
+ return {
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, 'src'),
+ },
+ },
+ server: {
+ host: '0.0.0.0',
+ port: 3000,
+ },
+ preview: {
+ port: 3000,
+ host: true,
+ },
+ build: {
+ outDir: 'build',
+ sourcemap: true,
+ },
+ plugins: [svgr(), react(), nodePolyfills(), eslint()],
+ assetsInclude: ['**/*.wasm?inline', '**/*.txt?inline'],
+ };
+});
diff --git a/frontend/dev/tamagotchi-battle-frontend-testnet/src/features/battle-tamagotchi/types/battles.ts b/frontend/dev/tamagotchi-battle-frontend-testnet/src/features/battle-tamagotchi/types/battles.ts
index d6eb01636..ae355cd3b 100644
--- a/frontend/dev/tamagotchi-battle-frontend-testnet/src/features/battle-tamagotchi/types/battles.ts
+++ b/frontend/dev/tamagotchi-battle-frontend-testnet/src/features/battle-tamagotchi/types/battles.ts
@@ -60,7 +60,7 @@ export type BattleHero = {
hero_id: HexString
name: String
date_of_birth: string
- defense: string
+ defence: string
power: string
health: string
color: String
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 2cf343b64..96a924fa0 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -1993,6 +1993,7 @@ __metadata:
peerDependencies:
"@gear-js/api": 0.38.1
"@gear-js/react-hooks": 0.13.0
+ "@tanstack/react-query": 5.29.0
react: 18.2.0
react-dom: 18.2.0
languageName: unknown
@@ -23681,6 +23682,55 @@ __metadata:
languageName: node
linkType: hard
+"tamagotchi-battle-new@workspace:apps/tamagotchi-battle-new":
+ version: 0.0.0-use.local
+ resolution: "tamagotchi-battle-new@workspace:apps/tamagotchi-battle-new"
+ dependencies:
+ "@dapps-frontend/error-tracking": "workspace:*"
+ "@dapps-frontend/hooks": "workspace:*"
+ "@dapps-frontend/ui": "workspace:*"
+ "@gear-js/api": "npm:0.38.1"
+ "@gear-js/react-hooks": "npm:0.13.0"
+ "@mantine/form": "npm:6.0.15"
+ "@polkadot/api": "npm:11.0.2"
+ "@polkadot/types": "npm:11.0.2"
+ "@polkadot/util": "npm:12.3.2"
+ "@polkadot/util-crypto": "npm:12.6.2"
+ "@radix-ui/react-dialog": "npm:1.0.4"
+ "@radix-ui/react-scroll-area": "npm:1.0.4"
+ "@tanstack/react-query": "npm:5.29.0"
+ "@types/node": "npm:18.16.19"
+ "@types/react": "npm:18.2.33"
+ "@types/react-dom": "npm:18.2.14"
+ "@vitejs/plugin-react-swc": "npm:3.3.2"
+ assert: "npm:2.0.0"
+ autoprefixer: "npm:10.4.15"
+ buffer: "npm:6.0.3"
+ class-variance-authority: "npm:0.6.1"
+ clsx: "npm:1.2.1"
+ eslint: "npm:8.48.0"
+ eslint-config-react-app: "npm:7.0.1"
+ framer-motion: "npm:10.16.2"
+ jotai: "npm:2.2.1"
+ postcss: "npm:8.4.29"
+ prettier: "npm:3.0.3"
+ react: "npm:18.2.0"
+ react-countdown: "npm:2.3.5"
+ react-dom: "npm:18.2.0"
+ react-router-dom: "npm:6.10.0"
+ react-transition-group: "npm:4.4.5"
+ rollup-plugin-visualizer: "npm:5.9.2"
+ sails-js: "npm:0.1.8"
+ sass: "npm:1.62.0"
+ tailwindcss: "npm:3.3.3"
+ typescript: "npm:4.9.5"
+ vite: "npm:4.4.9"
+ vite-plugin-eslint: "npm:1.8.1"
+ vite-plugin-node-polyfills: "npm:0.17.0"
+ vite-plugin-svgr: "npm:3.2.0"
+ languageName: unknown
+ linkType: soft
+
"tamagotchi-battle@workspace:apps/tamagotchi-battle":
version: 0.0.0-use.local
resolution: "tamagotchi-battle@workspace:apps/tamagotchi-battle"