From 9f54e3f3eb6a8d6306cde664f227843627da1371 Mon Sep 17 00:00:00 2001
From: andy <andynewjack@protonmail.com>
Date: Sun, 15 May 2022 19:01:11 +0000
Subject: [PATCH] exp stela part 1

---
 src/bin/main.rs                    |  11 +--
 src/ship/packet/handler/message.rs | 103 ++++++++++++++++++++++++++++-
 src/ship/ship.rs                   |   4 ++
 3 files changed, 112 insertions(+), 6 deletions(-)

diff --git a/src/bin/main.rs b/src/bin/main.rs
index d493ec2..152b79f 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,
diff --git a/src/ship/packet/handler/message.rs b/src/ship/packet/handler/message.rs
index c39ba73..6f2997e 100644
--- a/src/ship/packet/handler/message.rs
+++ b/src/ship/packet/handler/message.rs
@@ -3,9 +3,12 @@ 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::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<EG: EntityGateway>(id: ClientId,
@@ -398,3 +401,101 @@ where
     // TODO: send the packet to other clients
     Ok(Box::new(None.into_iter()))
 }
+
+// TODO: multihit weapon penalty?
+// 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: reject bosses
+// TODO: use real errors (Idunnoman)
+// TODO: create InventoryError::CannotGetItemHandle or something
+pub async fn player_steals_exp<EG> (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<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + 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.enemy_by_id(expsteal.enemy_id as usize)?;
+    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
+        } else {
+            0.0
+        }
+    } else {
+        0.0
+    };
+
+     
+    let weapon_exp_ratio: f32 = {
+        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;
+        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 exp_gain = (monster_stats.exp as f32 * (char_special_modifier + weapon_exp_ratio)).clamp(1.0, 80.0) as u32;
+
+    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()))))
+        }));
+
+    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()
+            .map(move |c| {
+                (c.client, SendShipPacket::Message(Message::new(GameMessage::PlayerLevelUp(level_up_pkt.clone()))))
+            })))
+    }
+
+    client.character.exp += exp_gain;
+    entity_gateway.save_character(&client.character).await?;
+
+    Ok(exp_pkts)
+}
\ 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<EG: EntityGateway> ShipServerState<EG> {
             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)?;