diff --git a/Cargo.lock b/Cargo.lock index 720d5f7..3db65f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1363,7 +1363,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -2143,7 +2143,6 @@ dependencies = [ [[package]] name = "sf-api" version = "0.2.0" -source = "git+https://github.com/the-marenga/sf-api.git?branch=simulation#cdacb44cba9c1c517e1738a04ba41ca8160a064d" dependencies = [ "aho-corasick", "base64", @@ -2171,6 +2170,7 @@ dependencies = [ "axum-server", "base64", "clap", + "enum-map", "fastrand", "log", "num-derive", diff --git a/Cargo.toml b/Cargo.toml index 6e637e2..115a23c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ axum = "0.7.9" axum-server = { version = "0.7.1", features = ["tls-rustls"] } base64 = "0.22.1" clap = { version = "4.5.21", features = ["derive"] } +enum-map = "2.7" fastrand = "2.2.0" log = "0.4.22" num-derive = "0.4.2" @@ -22,8 +23,12 @@ reqwest = { version = "0.12.9", features = [ ] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -sf-api = { git = "https://github.com/the-marenga/sf-api.git", branch = "simulation", version = "0.2.0", default-features = false, features = [ - "serde", +#sf-api = { git = "https://github.com/GutZuFusss/sf-api.git", branch = "feature/pvp", version = "0.2.0", default-features = false, features = [ +# "serde", +#] } +# sf-api via path +sf-api = { path = "../sf-api", default-features = false, features = [ + "serde", ] } sha1 = "0.10.6" sqlx = { version = "0.8.2", features = ["runtime-tokio", "sqlite"] } diff --git a/src/command/player.rs b/src/command/player.rs index 9874df3..4cb2de5 100644 --- a/src/command/player.rs +++ b/src/command/player.rs @@ -1,11 +1,20 @@ use std::fmt::Write; +use enum_map::{enum_map, EnumMap}; use fastrand::Rng; use log::error; use num_traits::FromPrimitive; use sf_api::{ - gamestate::character::{Gender, Race}, + command::AttributeType, + gamestate::{ + character::{Class, Gender, Race}, + items::Potion, + }, misc::from_sf_string, + simulate::{ + AttackType, Battle, BattleEvent, BattleFighter, BattleLogger, + BattleSide, BattleTeam, ClassEffect, UpgradeableFighter, + }, }; use sqlx::Sqlite; @@ -795,26 +804,66 @@ pub(crate) async fn player_arena_fight( let fighters = [session.player_id, enemy_id]; - let starting_hp = 10_000; + let mut battle_fighters = Vec::with_capacity(2); for pid in fighters { let fighter = sqlx::query!( - "SELECT name, portrait.*, a.*, level, class, race, gender + "SELECT name, portrait.*, a.*, ab.strength AS strengthb, + ab.dexterity AS dexterityb, ab.intelligence AS \ + intelligenceb, + ab.stamina AS staminab, ab.luck AS luckb, level, class, \ + race, + gender FROM character c NATURAL JOIN portrait - JOIN attributes a on a.id = c.attributes + JOIN attributes a ON a.id = c.attributes + JOIN attributes ab ON ab.id = c.attributes_bought WHERE pid = $1", pid ) .fetch_one(db) .await?; - // Player a info + let attr: EnumMap = enum_map! { + AttributeType::Strength => fighter.strength as u32, + AttributeType::Dexterity => fighter.dexterity as u32, + AttributeType::Intelligence => fighter.intelligence as u32, + AttributeType::Constitution => fighter.stamina as u32, + AttributeType::Luck => fighter.luck as u32, + }; + + let attr_bought: EnumMap = enum_map! { + AttributeType::Strength => fighter.strengthb as u32, + AttributeType::Dexterity => fighter.dexterityb as u32, + AttributeType::Intelligence => fighter.intelligenceb as u32, + AttributeType::Constitution => fighter.staminab as u32, + AttributeType::Luck => fighter.luckb as u32, + }; + + let potions: [Option; 3] = Default::default(); + + let upgradeable_fighter = UpgradeableFighter { + is_companion: false, + level: fighter.level as u16, + class: Class::from_i64(fighter.class - 1).unwrap(), + attribute_basis: attr, + _attributes_bought: attr_bought, + pet_attribute_bonus_perc: EnumMap::default(), // TODO + equipment: Default::default(), // TODO + active_potions: potions, // TODO + portal_hp_bonus: 0, // TODO + portal_dmg_bonus: 0, // TODO + }; + + let bf = BattleFighter::from_upgradeable(&upgradeable_fighter); + battle_fighters.push(bf.clone()); + + // Player info resp.add_val(fighter.pid); resp.add_str(&fighter.name); resp.add_val(fighter.level); - resp.add_val(starting_hp); // TODO: Calc their hp - resp.add_val(starting_hp); // TODO: Calc their hp + resp.add_val(bf.max_hp); + resp.add_val(bf.max_hp); resp.add_val(fighter.strength); // str resp.add_val(fighter.dexterity); // dex resp.add_val(fighter.intelligence); // int @@ -854,42 +903,31 @@ pub(crate) async fn player_arena_fight( resp.add_key("fight.r"); - let mut player_a_hp = starting_hp; - let mut player_b_hp = starting_hp; + let mut bf_left = [battle_fighters.get(0).unwrap().clone()]; + let mut bf_right = [battle_fighters.get(1).unwrap().clone()]; + let mut battle: Battle = Battle::new(&mut bf_left, &mut bf_right); + let mut logger = MyCustomLogger::new(resp, [fighters[0], fighters[1]]); + + battle.simulate(&mut logger); + resp = logger.response; + + let left_hp = match battle.left.current() { + Some(f) => f.current_hp, + None => 0, + }; - let mut rng = fastrand::Rng::new(); - for round in 0.. { - let (pid, our_hp, enemy_hp) = if round % 2 == 0 { - (fighters[0], &mut player_a_hp, &mut player_b_hp) - } else { - (fighters[1], &mut player_b_hp, &mut player_a_hp) - }; - *enemy_hp -= rng.i32(2_000..=4_000); - resp.add_val(pid); - resp.add_val(0); - resp.add_val(1); // Attack type (weapon, catapult, etc.) - resp.add_val(0); // Enemy reaction (repelled/dodged) - resp.add_val(0); - resp.add_val(*our_hp); - resp.add_val(*enemy_hp); - resp.add_val(0); - resp.add_val(0); - if player_a_hp <= 0 || player_b_hp <= 0 { - break; - } - } resp.add_key("winnerid"); - resp.add_val(if player_a_hp > 0 { + resp.add_val(if left_hp > 0 { fighters[0] } else { fighters[1] }); resp.add_key("fightresult.battlereward"); - resp.add_val((player_a_hp > 0) as i32); // have we won? + resp.add_val((left_hp > 0) as i32); // have we won? resp.add_val(1); resp.add_val(0); // silver - resp.add_val(1000); // xp won - resp.add_val(100); // mushrooms + resp.add_val(0); // xp won + resp.add_val(if left_hp > 0 { 1337 } else { 0 }); // mushrooms resp.add_val(0); // honor won resp.add_val(0); resp.add_val(2); // rank pre @@ -900,3 +938,157 @@ pub(crate) async fn player_arena_fight( } resp.build() } + +struct MyCustomLogger { + response: ResponseBuilder, + fighter_ids: [i64; 2], + player_turn: usize, + msg_skill_type: i64, + msg_attack_type: i64, + msg_enemy_reaction: i64, +} + +impl MyCustomLogger { + fn new(response: ResponseBuilder, fighter_ids: [i64; 2]) -> Self { + Self { + response, + fighter_ids, + player_turn: 0, + msg_skill_type: 0, + msg_attack_type: 0, + msg_enemy_reaction: 0, + } + } + + fn add_attack( + &mut self, + from: Option<&BattleFighter>, + to: Option<&BattleFighter>, + ) { + let from_hp = match from { + Some(f) => f.current_hp, + None => 0, + }; + let to_hp = match to { + Some(f) => f.current_hp, + None => 0, + }; + println!( + "From {}: {:?}, To: {:?}", + self.fighter_ids[self.player_turn], from_hp, to_hp + ); + + if from.unwrap().class == Class::Druid { + match from.unwrap().class_effect { + ClassEffect::Druid { bear: true, .. } => { + self.msg_skill_type = 11; + self.msg_attack_type = 1; + } + _ => { + self.msg_skill_type = 10; + } + } + } + self.response.add_val(self.fighter_ids[self.player_turn]); + self.response.add_val(self.msg_skill_type); // or maybe smth like "strength of attack" + self.response.add_val(self.msg_attack_type); // Attack type (normal=0, crit=1, catapult, etc.) + self.response.add_val(self.msg_enemy_reaction); // Enemy reaction (repelled/dodged) + self.response.add_val(0); + self.response.add_val(from_hp); // Attacker hp + self.response.add_val(to_hp); // Defender hp + self.response.add_val(0); + self.response.add_val(0); + + self.msg_skill_type = 0; + self.msg_attack_type = 0; + self.msg_enemy_reaction = 0; + } +} + +impl BattleLogger for MyCustomLogger { + fn log(&mut self, event: BattleEvent) { + match event { + BattleEvent::TurnUpdate(side) => { + println!("#### Turn update ####"); + self.player_turn = if side == BattleSide::Left { 0 } else { 1 }; + } + BattleEvent::BattleEnd(b, side) => {} + BattleEvent::Attack(from, to, attack_type) => { + println!( + "Attack (from {:?} to {:?} (Attack-Type: {:?})", + from.class, to.class, attack_type + ); + if attack_type == AttackType::Swoop { + self.msg_attack_type = 13; + } + // nothing to do, 0 is default & we dont wanna override crits + } + BattleEvent::Dodged(from, to) => { + println!("Dodged (from {:?} to {:?})", from.class, to.class); + self.msg_enemy_reaction = 4; + self.add_attack(Some(from), Some(to)); + } + BattleEvent::Blocked(from, to) => { + println!("Blocked (from {:?} to {:?})", from.class, to.class); + self.msg_enemy_reaction = 2; + self.add_attack(Some(from), Some(to)); + } + BattleEvent::Crit(from, to) => { + println!("Crit (from {:?} to {:?})", from.class, to.class); + self.msg_attack_type = 1; + } + BattleEvent::DamageReceived(from, to, dmg) => { + println!( + "Damage received (from {:?} to {:?} (dmg: {:?})", + from.class, to.class, dmg + ); + self.add_attack(Some(from), Some(to)); + } + BattleEvent::DemonHunterRevived(from, to) => { + println!( + "Demon Hunter Revived (from {:?} to {:?})", + from.class, to.class + ); + } + BattleEvent::CometRepelled(from, to) => { + println!( + "Comet Repelled (from {:?} to {:?})", + from.class, to.class + ); + self.msg_enemy_reaction = 3; + self.add_attack(Some(from), Some(to)); + } + BattleEvent::CometAttack(from, to) => { + println!( + "Comet Attack (from {:?} to {:?})", + from.class, to.class + ); + self.msg_attack_type = 10; + } + BattleEvent::MinionSpawned(from, to, minion) => { + println!( + "Minion Spawned (from {:?} to {:?} (minion: {:?})", + from.class, to.class, minion + ); + } + BattleEvent::MinionSkeletonRevived(from, to) => { + println!( + "Minion Skeleton Revived (from {:?} to {:?})", + from.class, to.class + ); + } + BattleEvent::BardPlay(from, to, quality) => { + println!( + "Bard Play (from {:?} to {:?} (quality: {:?})", + from.class, to.class, quality + ); + } + BattleEvent::FighterDefeat(b, side) => { + println!("Fighter Defeat (side: {:?})", side); + } + _ => { + // log::error!("Unknown event: {:?}\n\nOccured during battle {:?}\n\nBattle log: {:?}", e, battle, logger.0); + } + } + } +}