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/packet/handler/message.rs b/src/ship/packet/handler/message.rs index 352fc94..fe47942 100644 --- a/src/ship/packet/handler/message.rs +++ b/src/ship/packet/handler/message.rs @@ -6,6 +6,7 @@ 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, ItemManagerError}; @@ -402,8 +403,6 @@ where Ok(Box::new(None.into_iter())) } -// TODO: restrict stealable exp to 100% -// TODO: track stealable exp per client // 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 @@ -427,84 +426,94 @@ where .as_mut() .ok_or(ShipError::InvalidRoom(room_id.0 as u32))?; - let monster = room.maps.enemy_by_id(expsteal.enemy_id as usize)?; + 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 monster_stats = room.monster_stats.get(&monster.monster).ok_or(ShipError::UnknownMonster(monster.monster))?; - let char_special_modifier: f32 = if client.character.char_class.is_android() { - if room.mode.difficulty() == crate::ship::room::Difficulty::Ultimate { - 0.3 + let remaining_exp = monster_stats.exp - monster.stolen_exp[area_client.local_client.id() as usize]; + if remaining_exp <= 0 { + Ok(Box::new(None.into_iter())) + } else { + 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 - } - } 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 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); + println!("monster info: {:?}", monster); - let weapon_special_reduction: f32 = { - match equipped_weapon { - ItemDetail::Weapon(weapon) => weapon.weapon.special_penalty(), - ItemDetail::ESWeapon(_esweapon) => 0.0, - _ => 0.0, - } - }; + 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 exp_gain = ((monster_stats.exp as f32 * (char_special_modifier + special_exp_ratio)).clamp(1.0, 80.0) * (1.0 - weapon_special_reduction)) as u32; + 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; - 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_gain); - 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())))) - })); + 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 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_gain); - let level_up = before_level != after_level; + 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())))) + }))) + } - 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_gain); + client.character.exp += exp_earned; + entity_gateway.save_character(&client.character).await?; - 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())))) - }))) + Ok(exp_pkts) } - - client.character.exp += exp_gain; - entity_gateway.save_character(&client.character).await?; - - Ok(exp_pkts) } } \ No newline at end of file diff --git a/tests/test_exp_gain.rs b/tests/test_exp_gain.rs index fe31773..bb0b875 100644 --- a/tests/test_exp_gain.rs +++ b/tests/test_exp_gain.rs @@ -448,7 +448,7 @@ async fn test_exp_steal_android_boost_in_ultimate() { #[async_std::test] async fn test_exp_steal_no_android_boost_in_vhard() { - let mut entity_gateway = InMemoryGateway::default(); + let mut entity_gateway = InMemoryGateway::default(); let (_user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a", 1).await; char1.exp = 80000000; @@ -735,11 +735,183 @@ async fn test_cannot_steal_exp_from_boss() { #[async_std::test] async fn test_exp_steal_doesnt_exceed_100p() { - assert!(false) + 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() { - assert!(false) + 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(); + println!("c1 exp: {:?}, c2 exp: {:?}", c1.character.exp, c2.character.exp); + assert!(c1.character.exp == 5); + assert!(c2.character.exp == 5); + }