Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"] }
Expand Down
260 changes: 226 additions & 34 deletions src/command/player.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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<AttributeType, u32> = 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<AttributeType, u32> = 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<Potion>; 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
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
}
}
}