expsteal #107
| @ -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 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -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<MapObject, MapsError> { | ||||
|         self.object_data[id].ok_or(MapsError::InvalidObjectId(id)) | ||||
|     } | ||||
|  | ||||
| @ -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_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 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 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, | ||||
|                 _ => 0.0, | ||||
|             } | ||||
|         }; | ||||
|             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_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 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); | ||||
| 
 | ||||
|         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<dyn Iterator<Item = _> + Send> = Box::new(clients_in_area.clone().into_iter() | ||||
|             .map(move |c| { | ||||
|                 (c.client, SendShipPacket::Message(Message::new(GameMessage::GiveCharacterExp(gain_exp_pkt.clone())))) | ||||
|             })); | ||||
|             monster.steal_exp(exp_earned, area_client.local_client.id() as usize); | ||||
|             println!("monster info: {:?}", monster); | ||||
| 
 | ||||
|         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; | ||||
| 
 | ||||
|         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); | ||||
| 
 | ||||
|             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() | ||||
|             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<dyn Iterator<Item = _> + Send> = Box::new(clients_in_area.clone().into_iter() | ||||
|                 .map(move |c| { | ||||
|                     (c.client, SendShipPacket::Message(Message::new(GameMessage::PlayerLevelUp(level_up_pkt.clone())))) | ||||
|                 }))) | ||||
|                     (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) | ||||
|         } | ||||
| 
 | ||||
|         client.character.exp += exp_gain; | ||||
|         entity_gateway.save_character(&client.character).await?; | ||||
| 
 | ||||
|         Ok(exp_pkts) | ||||
|     } | ||||
| } | ||||
| @ -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); | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user