diff --git a/src/bin/main.rs b/src/bin/main.rs index d493ec2..f43276e 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -74,6 +74,7 @@ fn main() { character.slot = 2; character.name = "ItemRefactor".into(); character.exp = 80000000; + character.char_class = elseware::entity::character::CharacterClass::HUcast; let character = entity_gateway.create_character(character).await.unwrap(); entity_gateway.set_character_meseta(&character.id, item::Meseta(999999)).await.unwrap(); entity_gateway.set_bank_meseta(&character.id, item::BankName("".into()), item::Meseta(999999)).await.unwrap(); @@ -110,11 +111,11 @@ fn main() { item::weapon::Weapon { weapon: item::weapon::WeaponType::Raygun, grind: 5, - special: Some(item::weapon::WeaponSpecial::Hell), - attrs: [Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Hit, value: 40}), + special: Some(item::weapon::WeaponSpecial::Kings), + attrs: [Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Hit, value: 100}), Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Dark, value: 30}), None,], - tekked: false, + tekked: true, } ), }).await.unwrap(); @@ -124,8 +125,8 @@ fn main() { item::weapon::Weapon { weapon: item::weapon::WeaponType::Handgun, grind: 5, - special: Some(item::weapon::WeaponSpecial::Charge), - attrs: [Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Hit, value: 40}), + special: Some(item::weapon::WeaponSpecial::Lords), + attrs: [Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Hit, value: 100}), Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Dark, value: 30}), None,], tekked: true, @@ -136,9 +137,9 @@ fn main() { NewItemEntity { item: ItemDetail::Weapon( item::weapon::Weapon { - weapon: item::weapon::WeaponType::Vjaya, + weapon: item::weapon::WeaponType::Ripper, grind: 5, - special: Some(item::weapon::WeaponSpecial::Charge), + special: Some(item::weapon::WeaponSpecial::Kings), attrs: [Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Hit, value: 40}), Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Dark, value: 100}), None,], @@ -152,7 +153,7 @@ fn main() { item::weapon::Weapon { weapon: item::weapon::WeaponType::Vulcan, grind: 5, - special: Some(item::weapon::WeaponSpecial::Charge), + special: Some(item::weapon::WeaponSpecial::Kings), attrs: [Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Hit, value: 100}), Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Dark, value: 100}), None,], diff --git a/src/entity/item/weapon.rs b/src/entity/item/weapon.rs index 11a8709..468f5dc 100644 --- a/src/entity/item/weapon.rs +++ b/src/entity/item/weapon.rs @@ -1421,6 +1421,80 @@ impl WeaponType { _ => Err(ItemParseError::InvalidWeaponType), } } + + pub fn special_penalty(&self) -> f32 { + match self { + WeaponType::Saber => 0.0, + WeaponType::Brand => 0.0, + WeaponType::Buster => 0.0, + WeaponType::Pallasch => 0.0, + WeaponType::Gladius => 0.0, + WeaponType::Handgun => 0.0, + WeaponType::Autogun => 0.0, + WeaponType::Lockgun => 0.0, + WeaponType::Railgun => 0.0, + WeaponType::Raygun => 0.0, + WeaponType::Rifle => 0.0, + WeaponType::Sniper => 0.0, + WeaponType::Blaster => 0.0, + WeaponType::Beam => 0.0, + WeaponType::Laser => 0.0, + WeaponType::Cane => 0.0, + WeaponType::Stick => 0.0, + WeaponType::Mace => 0.0, + WeaponType::Club => 0.0, + WeaponType::Rod => 0.0, + WeaponType::Pole => 0.0, + WeaponType::Pillar => 0.0, + WeaponType::Striker => 0.0, + WeaponType::Wand => 0.0, + WeaponType::Staff => 0.0, + WeaponType::Baton => 0.0, + WeaponType::Scepter => 0.0, + + WeaponType::Sword => 0.5, + WeaponType::Gigush => 0.5, + WeaponType::Breaker => 0.5, + WeaponType::Claymore => 0.5, + WeaponType::Calibur => 0.5, + WeaponType::FlowensSword => 0.5, + WeaponType::LastSurvivor => 0.5, + WeaponType::DragonSlayer => 0.5, + WeaponType::Dagger => 0.5, + WeaponType::Knife => 0.5, + WeaponType::Blade => 0.5, + WeaponType::Edge => 0.5, + WeaponType::Ripper => 0.5, + WeaponType::BladeDance => 0.5, + WeaponType::BloodyArt => 0.5, + WeaponType::CrossScar => 0.5, + WeaponType::ZeroDivide => 0.5, + WeaponType::TwoKamui => 0.5, + WeaponType::Partisan => 0.5, + WeaponType::Halbert => 0.5, + WeaponType::Glaive => 0.5, + WeaponType::Berdys => 0.5, + WeaponType::Gungnir => 0.5, + + WeaponType::Slicer => 0.6666, + WeaponType::Spinner => 0.6666, + WeaponType::Cutter => 0.6666, + WeaponType::Sawcer => 0.6666, + WeaponType::Diska => 0.6666, + WeaponType::Mechgun => 0.6666, + WeaponType::Assault => 0.6666, + WeaponType::Repeater => 0.6666, + WeaponType::Gatling => 0.6666, + WeaponType::Vulcan => 0.6666, + WeaponType::Shot => 0.6666, + WeaponType::Spread => 0.6666, + WeaponType::Cannon => 0.6666, + WeaponType::Launcher => 0.6666, + WeaponType::Arms => 0.6666, + + _ => 1.0, + } + } } diff --git a/src/ship/map/enemy.rs b/src/ship/map/enemy.rs index b5efd7c..c6719bf 100644 --- a/src/ship/map/enemy.rs +++ b/src/ship/map/enemy.rs @@ -118,6 +118,7 @@ pub struct MapEnemy { pub dropped_item: bool, pub gave_exp: bool, pub shiny: bool, + pub stolen_exp: [u32; 4], // tracks total amount of exp stolen by each player } impl MapEnemy { @@ -301,6 +302,7 @@ impl MapEnemy { gave_exp: false, player_hit: [false; 4], shiny: false, + stolen_exp: [0; 4], }) } @@ -313,6 +315,7 @@ impl MapEnemy { gave_exp: false, player_hit: [false; 4], shiny: false, + stolen_exp: [0; 4], } } @@ -366,5 +369,9 @@ impl MapEnemy { } self } + + pub fn steal_exp(&mut self, exp: u32, slot: usize) { + self.stolen_exp[slot] += exp + } } diff --git a/src/ship/map/maps.rs b/src/ship/map/maps.rs index 0743c01..d75229f 100644 --- a/src/ship/map/maps.rs +++ b/src/ship/map/maps.rs @@ -298,6 +298,10 @@ impl Maps { self.enemy_data[id].ok_or(MapsError::InvalidMonsterId(id)) } + pub fn mut_enemy_by_id(&mut self, id: usize) -> Option<&mut MapEnemy> { + self.enemy_data[id].as_mut() + } + pub fn object_by_id(&self, id: usize) -> Result { self.object_data[id].ok_or(MapsError::InvalidObjectId(id)) } diff --git a/src/ship/monster.rs b/src/ship/monster.rs index 1bdb5b7..8dab9f1 100644 --- a/src/ship/monster.rs +++ b/src/ship/monster.rs @@ -148,6 +148,44 @@ pub enum MonsterType { Kondrieu, } +impl MonsterType { + pub fn is_boss(&self) -> bool { + matches!(self, + MonsterType::Dragon | + MonsterType::DeRolLe | + MonsterType::DeRolLeBody | + MonsterType::DeRolLeMine | + MonsterType::VolOptPartA | + MonsterType::VolOptPillar | + MonsterType::VolOptMonitor | + MonsterType::VolOptAmp | + MonsterType::VolOptCore | + MonsterType::VolOptUnused | + MonsterType::VolOpt | + MonsterType::VolOptTrap | + MonsterType::DarkFalz | + MonsterType::DarkFalz1 | + MonsterType::DarkFalz2 | + MonsterType::DarkFalz3 | + MonsterType::Darvant | + MonsterType::UltDarvant | + MonsterType::Epsiguard | // TODO: is epsilon core a boss? + MonsterType::BarbaRay | + MonsterType::PigRay | + MonsterType::GolDragon | + MonsterType::GalGryphon | + MonsterType::OlgaFlow | + MonsterType::OlgaFlow1 | + MonsterType::OlgaFlow2 | + MonsterType::Gael | + MonsterType::Giel | + MonsterType::SaintMillion | + MonsterType::Shambertin | + MonsterType::Kondrieu + ) + } +} + #[derive(serde::Deserialize, Debug)] pub struct MonsterStats { diff --git a/src/ship/packet/handler/message.rs b/src/ship/packet/handler/message.rs index c39ba73..0ac24fc 100644 --- a/src/ship/packet/handler/message.rs +++ b/src/ship/packet/handler/message.rs @@ -3,9 +3,13 @@ use libpso::packet::messages::*; use crate::entity::gateway::EntityGateway; use crate::common::serverstate::ClientId; use crate::common::leveltable::CharacterLevelTable; +use crate::entity::item::ItemDetail; +use crate::entity::item::esweapon::{ESWeaponSpecial}; +use crate::entity::item::weapon::{WeaponSpecial}; +use crate::ship::map::MapsError; use crate::ship::ship::{SendShipPacket, ShipError, Rooms, Clients, ItemDropLocation}; use crate::ship::location::{ClientLocation, ClientLocationError}; -use crate::ship::items::{ItemManager, ClientItemId}; +use crate::ship::items::{ItemManager, ClientItemId, ItemManagerError}; use crate::ship::packet::builder; pub async fn request_exp(id: ClientId, @@ -398,3 +402,118 @@ where // TODO: send the packet to other clients Ok(Box::new(None.into_iter())) } + +// TODO: convenience function for giving exp and checking levelups (un-duplicate code here and `request_exp`) +// TODO: use real errors (Idunnoman) +// TODO: create InventoryError::CannotGetItemHandle or something +#[allow(clippy::too_many_arguments)] +pub async fn player_steals_exp (id: ClientId, + expsteal: &ExperienceSteal, + entity_gateway: &mut EG, + client_location: &ClientLocation, + clients: &mut Clients, + rooms: &mut Rooms, + item_manager: &mut ItemManager, + level_table: &CharacterLevelTable) + -> Result + Send>, anyhow::Error> +where + EG: EntityGateway +{ + let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?; + let area_client = client_location.get_local_client(id).map_err(|err| -> ClientLocationError { err.into() })?; + let room_id = client_location.get_room(id).map_err(|err| -> ClientLocationError { err.into() })?; + let room = rooms.get_mut(room_id.0) + .ok_or(ShipError::InvalidRoom(room_id.0 as u32))? + .as_mut() + .ok_or(ShipError::InvalidRoom(room_id.0 as u32))?; + + let monster = room.maps.mut_enemy_by_id(expsteal.enemy_id as usize).ok_or(MapsError::InvalidMonsterId(expsteal.enemy_id as usize))?; + if monster.monster.is_boss() { + Ok(Box::new(None.into_iter())) // should this be an error? + } else { + let monster_stats = room.monster_stats.get(&monster.monster).ok_or(ShipError::UnknownMonster(monster.monster))?; + + let remaining_exp = monster_stats.exp - monster.stolen_exp[area_client.local_client.id() as usize]; + if remaining_exp > 0 { + let char_special_modifier: f32 = if client.character.char_class.is_android() { + if room.mode.difficulty() == crate::ship::room::Difficulty::Ultimate { + 0.3 + } else { + 0.0 + } + } else { + 0.0 + }; + + let equipped_weapon_handle = item_manager + .get_character_inventory_mut(&client.character)? + .get_equipped_weapon_handle() + .ok_or(ItemManagerError::CannotGetIndividualItem)?; + + let equipped_weapon = &equipped_weapon_handle + .item() + .ok_or(ItemManagerError::Idunnoman)? + .individual() + .ok_or(ItemManagerError::Idunnoman)?.item; + + let special_exp_ratio: f32 = { + match equipped_weapon { + ItemDetail::Weapon(weapon) => match weapon.special { + Some(WeaponSpecial::Masters) => 0.08, + Some(WeaponSpecial::Lords) => 0.10, + Some(WeaponSpecial::Kings) => 0.12, + _ => 0.0, // TODO: error - stealing exp with wrong special + }, + ItemDetail::ESWeapon(esweapon) => match esweapon.special { + Some(ESWeaponSpecial::Kings) => 0.12, + _ => 0.0, // TODO: error - stealing exp with wrong special + }, + _ => 0.0, // TODO: error - stealing exp without a weapon!! + } + }; + + let weapon_special_reduction: f32 = { + match equipped_weapon { + ItemDetail::Weapon(weapon) => weapon.weapon.special_penalty(), + ItemDetail::ESWeapon(_esweapon) => 0.0, + _ => 1.0, // unreachable? + } + }; + + let exp_earned = std::cmp::min( + ((monster_stats.exp as f32 * (char_special_modifier + special_exp_ratio)).clamp(1.0, 80.0) * (1.0 - weapon_special_reduction)) as u32, + remaining_exp); + + monster.steal_exp(exp_earned, area_client.local_client.id() as usize); + + let clients_in_area = client_location.get_clients_in_room(room_id).map_err(|err| -> ClientLocationError { err.into() })?; + let gain_exp_pkt = builder::message::character_gained_exp(area_client, exp_earned); + let mut exp_pkts: Box + Send> = Box::new(clients_in_area.clone().into_iter() + .map(move |c| { + (c.client, SendShipPacket::Message(Message::new(GameMessage::GiveCharacterExp(gain_exp_pkt.clone())))) + })); + + let before_level = level_table.get_level_from_exp(client.character.char_class, client.character.exp); + let after_level = level_table.get_level_from_exp(client.character.char_class, client.character.exp + exp_earned); + let level_up = before_level != after_level; + + if level_up { + let (_, before_stats) = level_table.get_stats_from_exp(client.character.char_class, client.character.exp); + let (after_level, after_stats) = level_table.get_stats_from_exp(client.character.char_class, client.character.exp + exp_earned); + + let level_up_pkt = builder::message::character_leveled_up(area_client, after_level, before_stats, after_stats); + exp_pkts = Box::new(exp_pkts.chain(clients_in_area.into_iter() + .map(move |c| { + (c.client, SendShipPacket::Message(Message::new(GameMessage::PlayerLevelUp(level_up_pkt.clone())))) + }))) + } + + client.character.exp += exp_earned; + entity_gateway.save_character(&client.character).await?; + + Ok(exp_pkts) + } else { + Ok(Box::new(None.into_iter())) + } + } +} \ No newline at end of file diff --git a/src/ship/ship.rs b/src/ship/ship.rs index 4cf5e2a..a5a12f8 100644 --- a/src/ship/ship.rs +++ b/src/ship/ship.rs @@ -520,6 +520,10 @@ impl ShipServerState { GameMessage::PlayerSoldItem(player_sold_item) => { handler::message::player_sells_item(id, player_sold_item, &mut self.entity_gateway, &mut self.clients, &mut self.item_manager).await? }, + GameMessage::ExperienceSteal(exp_steal) => { + let block = self.blocks.with_client(id, &self.clients)?; + handler::message::player_steals_exp(id, exp_steal, &mut self.entity_gateway, &block.client_location, &mut self.clients, &mut block.rooms, &mut self.item_manager, &self.level_table).await? + }, _ => { let cmsg = msg.clone(); let block = self.blocks.with_client(id, &self.clients)?; diff --git a/tests/test_exp_gain.rs b/tests/test_exp_gain.rs index 0db6ba9..9a8decc 100644 --- a/tests/test_exp_gain.rs +++ b/tests/test_exp_gain.rs @@ -1,8 +1,11 @@ use elseware::common::serverstate::{ClientId, ServerState}; +use elseware::entity::character::{CharacterClass}; use elseware::entity::gateway::{EntityGateway, InMemoryGateway}; use elseware::common::leveltable::CharacterLevelTable; use elseware::ship::ship::{ShipServerState, SendShipPacket, RecvShipPacket}; use elseware::ship::monster::MonsterType; +use elseware::entity::item; +use elseware::ship::room::{Difficulty}; use libpso::packet::ship::*; use libpso::packet::messages::*; @@ -190,3 +193,722 @@ async fn test_one_character_gets_full_exp_and_other_attacker_gets_partial() { assert!(c1.character.exp == exp); assert!(c2.character.exp == (exp as f32 * 0.8) as u32); } + +#[async_std::test] +async fn test_exp_steal_min_1() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a", 1).await; + + let mut p1_inv = Vec::new(); + p1_inv.push(entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Raygun, + grind: 5, + special: Some(item::weapon::WeaponSpecial::Kings), + attrs: [Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Hit, value: 100}), + Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Dark, value: 30}), + None,], + tekked: true, + } + ), + }).await.unwrap()); + + let equipped = item::EquippedEntity { + weapon: Some(p1_inv[0].id), + armor: None, + shield: None, + unit: [None; 4], + mag: None, + }; + entity_gateway.set_character_equips(&char1.id, &equipped).await.unwrap(); + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + create_room(&mut ship, ClientId(1), "room", "").await; + + let enemy_id = { + let room = ship.blocks.0[0].rooms[0].as_ref().unwrap(); + let enemy_id = (0..).filter_map(|i| { + room.maps.enemy_by_id(i).ok().and_then(|enemy| { + if enemy.monster == MonsterType::Booma { + Some(i) + } + else { + None + } + }) + }).next().unwrap(); + enemy_id + }; + + ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::ExperienceSteal(ExperienceSteal{ + client: 0, + target: 0, + client2: enemy_id as u8, + target2: 16, + enemy_id: enemy_id as u16, + })))).await.unwrap().for_each(drop); + + let c1 = ship.clients.get(&ClientId(1)).unwrap(); + assert!(c1.character.exp == 1); +} + +#[async_std::test] +async fn test_exp_steal_max_80() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a", 1).await; + char1.exp = 80000000; + char1.char_class = CharacterClass::HUcast; + entity_gateway.save_character(&char1).await.unwrap(); + + let mut p1_inv = Vec::new(); + p1_inv.push(entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Raygun, + grind: 5, + special: Some(item::weapon::WeaponSpecial::Kings), + attrs: [Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Hit, value: 100}), + Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Dark, value: 30}), + None,], + tekked: true, + } + ), + }).await.unwrap()); + + let equipped = item::EquippedEntity { + weapon: Some(p1_inv[0].id), + armor: None, + shield: None, + unit: [None; 4], + mag: None, + }; + entity_gateway.set_character_equips(&char1.id, &equipped).await.unwrap(); + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + create_room_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::Ultimate).await; + + let enemy_id = { + let room = ship.blocks.0[0].rooms[0].as_ref().unwrap(); + let enemy_id = (0..).filter_map(|i| { + room.maps.enemy_by_id(i).ok().and_then(|enemy| { + if enemy.monster == MonsterType::Booma { + Some(i) + } + else { + None + } + }) + }).next().unwrap(); + enemy_id + }; + + ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::ExperienceSteal(ExperienceSteal{ + client: 0, + target: 0, + client2: enemy_id as u8, + target2: 16, + enemy_id: enemy_id as u16, + })))).await.unwrap().for_each(drop); + + let c1 = ship.clients.get(&ClientId(1)).unwrap(); + assert!(c1.character.exp == 80000080); +} + +#[async_std::test] +async fn test_exp_steal_android_boost_in_ultimate() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a", 1).await; + char1.exp = 80000000; + char1.char_class = CharacterClass::HUcast; + entity_gateway.save_character(&char1).await.unwrap(); + + let (_user2, mut char2) = new_user_character(&mut entity_gateway, "a2", "a", 1).await; + char2.exp = 80000000; + char2.char_class = CharacterClass::HUmar; + entity_gateway.save_character(&char2).await.unwrap(); + + let mut p1_inv = Vec::new(); + p1_inv.push(entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Raygun, + grind: 5, + special: Some(item::weapon::WeaponSpecial::Kings), + attrs: [Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Hit, value: 100}), + Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Dark, value: 30}), + None,], + tekked: true, + } + ), + }).await.unwrap()); + + let equipped = item::EquippedEntity { + weapon: Some(p1_inv[0].id), + armor: None, + shield: None, + unit: [None; 4], + mag: None, + }; + entity_gateway.set_character_equips(&char1.id, &equipped).await.unwrap(); + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); + + let mut p2_inv = Vec::new(); + p2_inv.push(entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Raygun, + grind: 5, + special: Some(item::weapon::WeaponSpecial::Kings), + attrs: [Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Hit, value: 100}), + Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Dark, value: 30}), + None,], + tekked: true, + } + ), + }).await.unwrap()); + + let equipped = item::EquippedEntity { + weapon: Some(p2_inv[0].id), + armor: None, + shield: None, + unit: [None; 4], + mag: None, + }; + entity_gateway.set_character_equips(&char2.id, &equipped).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(p2_inv)).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + create_room_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::Ultimate).await; + log_in_char(&mut ship, ClientId(2), "a2", "a").await; + join_lobby(&mut ship, ClientId(2)).await; + join_room(&mut ship, ClientId(2), 0).await; + + let enemy_id = { + let room = ship.blocks.0[0].rooms[0].as_ref().unwrap(); + let enemy_id = (0..).filter_map(|i| { + room.maps.enemy_by_id(i).ok().and_then(|enemy| { + if enemy.monster == MonsterType::Booma { + Some(i) + } + else { + None + } + }) + }).next().unwrap(); + enemy_id + }; + + ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::ExperienceSteal(ExperienceSteal{ + client: 0, + target: 0, + client2: enemy_id as u8, + target2: 16, + enemy_id: enemy_id as u16, + })))).await.unwrap().for_each(drop); + + ship.handle(ClientId(2), &RecvShipPacket::Message(Message::new(GameMessage::ExperienceSteal(ExperienceSteal{ + client: 0, + target: 0, + client2: enemy_id as u8, + target2: 16, + enemy_id: enemy_id as u16, + })))).await.unwrap().for_each(drop); + + let c1 = ship.clients.get(&ClientId(1)).unwrap(); + let c2 = ship.clients.get(&ClientId(2)).unwrap(); + assert!(c1.character.exp == 80000080); + assert!(c2.character.exp == 80000032); +} + +#[async_std::test] +async fn test_exp_steal_no_android_boost_in_vhard() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a", 1).await; + char1.exp = 80000000; + char1.char_class = CharacterClass::HUcast; + entity_gateway.save_character(&char1).await.unwrap(); + + let (_user2, mut char2) = new_user_character(&mut entity_gateway, "a2", "a", 1).await; + char2.exp = 80000000; + char2.char_class = CharacterClass::HUmar; + entity_gateway.save_character(&char2).await.unwrap(); + + let mut p1_inv = Vec::new(); + p1_inv.push(entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Raygun, + grind: 5, + special: Some(item::weapon::WeaponSpecial::Kings), + attrs: [Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Hit, value: 100}), + Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Dark, value: 30}), + None,], + tekked: true, + } + ), + }).await.unwrap()); + + let equipped = item::EquippedEntity { + weapon: Some(p1_inv[0].id), + armor: None, + shield: None, + unit: [None; 4], + mag: None, + }; + entity_gateway.set_character_equips(&char1.id, &equipped).await.unwrap(); + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); + + let mut p2_inv = Vec::new(); + p2_inv.push(entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Raygun, + grind: 5, + special: Some(item::weapon::WeaponSpecial::Kings), + attrs: [Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Hit, value: 100}), + Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Dark, value: 30}), + None,], + tekked: true, + } + ), + }).await.unwrap()); + + let equipped = item::EquippedEntity { + weapon: Some(p2_inv[0].id), + armor: None, + shield: None, + unit: [None; 4], + mag: None, + }; + entity_gateway.set_character_equips(&char2.id, &equipped).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(p2_inv)).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + create_room_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::VeryHard).await; + log_in_char(&mut ship, ClientId(2), "a2", "a").await; + join_lobby(&mut ship, ClientId(2)).await; + join_room(&mut ship, ClientId(2), 0).await; + + let enemy_id = { + let room = ship.blocks.0[0].rooms[0].as_ref().unwrap(); + let enemy_id = (0..).filter_map(|i| { + room.maps.enemy_by_id(i).ok().and_then(|enemy| { + if enemy.monster == MonsterType::Booma { + Some(i) + } + else { + None + } + }) + }).next().unwrap(); + enemy_id + }; + + ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::ExperienceSteal(ExperienceSteal{ + client: 0, + target: 0, + client2: enemy_id as u8, + target2: 16, + enemy_id: enemy_id as u16, + })))).await.unwrap().for_each(drop); + + ship.handle(ClientId(2), &RecvShipPacket::Message(Message::new(GameMessage::ExperienceSteal(ExperienceSteal{ + client: 0, + target: 0, + client2: enemy_id as u8, + target2: 16, + enemy_id: enemy_id as u16, + })))).await.unwrap().for_each(drop); + + let c1 = ship.clients.get(&ClientId(1)).unwrap(); + let c2 = ship.clients.get(&ClientId(2)).unwrap(); + assert!(c1.character.exp == 80000010); + assert!(c2.character.exp == 80000010); +} + +#[async_std::test] +async fn test_exp_steal_multihit_penalty() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a", 1).await; + char1.exp = 80000000; + char1.char_class = CharacterClass::HUcast; + entity_gateway.save_character(&char1).await.unwrap(); + + let mut p1_inv = Vec::new(); + p1_inv.push(entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Dagger, + grind: 5, + special: Some(item::weapon::WeaponSpecial::Kings), + attrs: [Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Hit, value: 100}), + Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Dark, value: 30}), + None,], + tekked: true, + } + ), + }).await.unwrap()); + + p1_inv.push(entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Mechgun, + grind: 5, + special: Some(item::weapon::WeaponSpecial::Kings), + attrs: [Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Hit, value: 100}), + Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Dark, value: 30}), + None,], + tekked: true, + } + ), + }).await.unwrap()); + + let equipped = item::EquippedEntity { + weapon: Some(p1_inv[0].id), + armor: None, + shield: None, + unit: [None; 4], + mag: None, + }; + entity_gateway.set_character_equips(&char1.id, &equipped).await.unwrap(); + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + create_room_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::Ultimate).await; + + let enemy_id = { + let room = ship.blocks.0[0].rooms[0].as_ref().unwrap(); + let enemy_id = (0..).filter_map(|i| { + room.maps.enemy_by_id(i).ok().and_then(|enemy| { + if enemy.monster == MonsterType::Booma { + Some(i) + } + else { + None + } + }) + }).next().unwrap(); + enemy_id + }; + + ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::ExperienceSteal(ExperienceSteal{ + client: 0, + target: 0, + client2: enemy_id as u8, + target2: 16, + enemy_id: enemy_id as u16, + })))).await.unwrap().for_each(drop); + + let c1 = ship.clients.get(&ClientId(1)).unwrap(); + assert!(c1.character.exp == 80000040); + + // change equipped item + ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerEquipItem(PlayerEquipItem { + client: 0, + target: 0, + item_id: 0x10001, + sub_menu: 9, + unknown1: 0, + })))).await.unwrap().for_each(drop); + + ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::ExperienceSteal(ExperienceSteal{ + client: 0, + target: 0, + client2: enemy_id as u8, + target2: 16, + enemy_id: enemy_id as u16, + })))).await.unwrap().for_each(drop); + + let c1 = ship.clients.get(&ClientId(1)).unwrap(); + assert!(c1.character.exp == 80000066); +} + +#[async_std::test] +async fn test_cannot_steal_exp_from_boss() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a", 1).await; + + let mut p1_inv = Vec::new(); + p1_inv.push(entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Raygun, + grind: 5, + special: Some(item::weapon::WeaponSpecial::Kings), + attrs: [Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Hit, value: 100}), + Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Dark, value: 30}), + None,], + tekked: true, + } + ), + }).await.unwrap()); + + let equipped = item::EquippedEntity { + weapon: Some(p1_inv[0].id), + armor: None, + shield: None, + unit: [None; 4], + mag: None, + }; + entity_gateway.set_character_equips(&char1.id, &equipped).await.unwrap(); + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + create_room(&mut ship, ClientId(1), "room", "").await; + + let enemy_id = { + let room = ship.blocks.0[0].rooms[0].as_ref().unwrap(); + let enemy_id = (0..).filter_map(|i| { + room.maps.enemy_by_id(i).ok().and_then(|enemy| { + if enemy.monster == MonsterType::Dragon { + Some(i) + } + else { + None + } + }) + }).next().unwrap(); + enemy_id + }; + + ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::ExperienceSteal(ExperienceSteal{ + client: 0, + target: 0, + client2: enemy_id as u8, + target2: 16, + enemy_id: enemy_id as u16, + })))).await.unwrap().for_each(drop); + + let c1 = ship.clients.get(&ClientId(1)).unwrap(); + assert!(c1.character.exp == 0); +} + +#[async_std::test] +async fn test_exp_steal_doesnt_exceed_100p() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a", 1).await; + + let mut p1_inv = Vec::new(); + p1_inv.push(entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Raygun, + grind: 5, + special: Some(item::weapon::WeaponSpecial::Kings), + attrs: [Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Hit, value: 100}), + Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Dark, value: 30}), + None,], + tekked: true, + } + ), + }).await.unwrap()); + + let equipped = item::EquippedEntity { + weapon: Some(p1_inv[0].id), + armor: None, + shield: None, + unit: [None; 4], + mag: None, + }; + entity_gateway.set_character_equips(&char1.id, &equipped).await.unwrap(); + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + create_room(&mut ship, ClientId(1), "room", "").await; + + let enemy_id = { + let room = ship.blocks.0[0].rooms[0].as_ref().unwrap(); + let enemy_id = (0..).filter_map(|i| { + room.maps.enemy_by_id(i).ok().and_then(|enemy| { + if enemy.monster == MonsterType::Booma { + Some(i) + } + else { + None + } + }) + }).next().unwrap(); + enemy_id + }; + + for _ in 0..10 { + ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::ExperienceSteal(ExperienceSteal{ + client: 0, + target: 0, + client2: enemy_id as u8, + target2: 16, + enemy_id: enemy_id as u16, + })))).await.unwrap().for_each(drop); + } + + let c1 = ship.clients.get(&ClientId(1)).unwrap(); + assert!(c1.character.exp == 5); +} + +#[async_std::test] +async fn test_each_client_can_steal_full_exp_from_same_enemy() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a", 1).await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a", 1).await; + entity_gateway.save_character(&char1).await.unwrap(); + entity_gateway.save_character(&char2).await.unwrap(); + + let mut p1_inv = Vec::new(); + p1_inv.push(entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Raygun, + grind: 5, + special: Some(item::weapon::WeaponSpecial::Kings), + attrs: [Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Hit, value: 100}), + Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Dark, value: 30}), + None,], + tekked: true, + } + ), + }).await.unwrap()); + + let equipped = item::EquippedEntity { + weapon: Some(p1_inv[0].id), + armor: None, + shield: None, + unit: [None; 4], + mag: None, + }; + entity_gateway.set_character_equips(&char1.id, &equipped).await.unwrap(); + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); + + let mut p2_inv = Vec::new(); + p2_inv.push(entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Raygun, + grind: 5, + special: Some(item::weapon::WeaponSpecial::Kings), + attrs: [Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Hit, value: 100}), + Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Dark, value: 30}), + None,], + tekked: true, + } + ), + }).await.unwrap()); + + let equipped = item::EquippedEntity { + weapon: Some(p2_inv[0].id), + armor: None, + shield: None, + unit: [None; 4], + mag: None, + }; + entity_gateway.set_character_equips(&char2.id, &equipped).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(p2_inv)).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + create_room_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::Normal).await; + log_in_char(&mut ship, ClientId(2), "a2", "a").await; + join_lobby(&mut ship, ClientId(2)).await; + join_room(&mut ship, ClientId(2), 0).await; + + let enemy_id = { + let room = ship.blocks.0[0].rooms[0].as_ref().unwrap(); + let enemy_id = (0..).filter_map(|i| { + room.maps.enemy_by_id(i).ok().and_then(|enemy| { + if enemy.monster == MonsterType::Booma { + Some(i) + } + else { + None + } + }) + }).next().unwrap(); + enemy_id + }; + + for _ in 0..10 { + ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::ExperienceSteal(ExperienceSteal{ + client: 0, + target: 0, + client2: enemy_id as u8, + target2: 16, + enemy_id: enemy_id as u16, + })))).await.unwrap().for_each(drop); + + ship.handle(ClientId(2), &RecvShipPacket::Message(Message::new(GameMessage::ExperienceSteal(ExperienceSteal{ + client: 0, + target: 0, + client2: enemy_id as u8, + target2: 16, + enemy_id: enemy_id as u16, + })))).await.unwrap().for_each(drop); + } + + let c1 = ship.clients.get(&ClientId(1)).unwrap(); + let c2 = ship.clients.get(&ClientId(2)).unwrap(); + assert!(c1.character.exp == 5); + assert!(c2.character.exp == 5); + +} +