use crate::ship::items::ClientItemId; use std::collections::{HashMap, BTreeMap}; use thiserror::Error; use crate::entity::gateway::EntityGateway; use crate::entity::character::{CharacterEntity, CharacterEntityId}; use crate::entity::item::{ItemDetail, ItemLocation, BankName}; use crate::entity::item::{Meseta, NewItemEntity}; use crate::entity::item::tool::{Tool, ToolType}; use crate::ship::map::MapArea; use crate::ship::ship::ItemDropLocation; use crate::ship::drops::{ItemDrop, ItemDropType}; use crate::ship::location::{AreaClient, RoomId}; use crate::ship::items::bank::*; use crate::ship::items::floor::*; use crate::ship::items::inventory::*; use crate::ship::items::use_tool; pub enum TriggerCreateItem { Yes, No } #[derive(Error, Debug)] #[error("")] pub enum ItemManagerError { EntityGatewayError, NoSuchItemId(ClientItemId), NoCharacter(CharacterEntityId), CouldNotAddToInventory(ClientItemId), //ItemBelongsToOtherPlayer, Idunnoman, CouldNotSplitItem(ClientItemId), CouldNotDropMeseta, InvalidBankName(BankName), NotEnoughTools(Tool, usize, usize), // have, expected InventoryItemConsumeError(#[from] InventoryItemConsumeError), BankFull, WrongItemType(ClientItemId), } pub struct ItemManager { id_counter: u32, character_inventory: HashMap, //character_bank: HashMap>, character_bank: HashMap, character_floor: HashMap, character_room: HashMap, room_floor: HashMap, room_item_id_counter: HashMap ClientItemId + Send>>, } impl ItemManager { pub fn new() -> ItemManager { ItemManager { id_counter: 0, character_inventory: HashMap::new(), character_bank: HashMap::new(), character_floor: HashMap::new(), character_room: HashMap::new(), room_floor: HashMap::new(), room_item_id_counter: HashMap::new(), } } pub fn next_global_item_id(&mut self) -> ClientItemId { self.id_counter += 1; ClientItemId(self.id_counter) } // TODO: Result pub async fn load_character(&mut self, entity_gateway: &mut EG, character: &CharacterEntity) { let items = entity_gateway.get_items_by_character(&character).await; let inventory_items = items.clone().into_iter() .filter_map(|item| { match item.location { ItemLocation::Inventory{slot, equipped, ..} => Some((item.id, item.item, slot, equipped)), _ => None, } }) .fold(BTreeMap::new(), |mut acc, (id, item, slot, equipped)| { if item.is_stackable() { if let ItemDetail::Tool(tool) = item { let inventory_item = acc.entry(slot).or_insert(InventoryItem::Stacked(StackedInventoryItem { entity_ids: Vec::new(), item_id: self.next_global_item_id(), tool: tool, })); if let InventoryItem::Stacked(ref mut stacked_inventory_item) = inventory_item { stacked_inventory_item.entity_ids.push(id); } } } else { acc.insert(slot, InventoryItem::Individual(IndividualInventoryItem { entity_id: id, item_id: self.next_global_item_id(), item: item, equipped: equipped, })); } acc }); // TODO: not using BankName anymore, clean this up let mut bank_items = items.into_iter() .filter_map(|item| { match item.location { ItemLocation::Bank{name, ..} => Some((item.id, item.item, name)), _ => None, } }) .fold(BTreeMap::new(), |mut acc, (id, item, name)| { acc.entry(name).or_insert(Vec::new()).push((id, item)); acc }) .into_iter() .map(|(bank_name, bank_items)| { let stacked_bank_items = bank_items.into_iter() .fold(Vec::new(), |mut acc, (id, bank_item)| { if bank_item.is_stackable() { let existing_item = acc.iter_mut() .find(|item| { if let (BankItem::Stacked(stacked_bank_item), &ItemDetail::Tool(ref tool)) = (item, &bank_item) { stacked_bank_item.tool == *tool } else { false } }); match existing_item { Some(item) => { if let BankItem::Stacked(ref mut stacked_bank_item) = item { stacked_bank_item.entity_ids.push(id); } } None => { if let ItemDetail::Tool(tool) = bank_item { acc.push(BankItem::Stacked(StackedBankItem { entity_ids: vec![id], item_id: self.next_global_item_id(), tool: tool, })); } }, } } else { acc.push(BankItem::Individual(IndividualBankItem { entity_id: id, item_id: self.next_global_item_id(), item: bank_item, })); } acc }); (bank_name, CharacterBank::new(stacked_bank_items)) }) .collect::>(); let inventory = CharacterInventory::new(inventory_items.into_iter().map(|(_k, v)| v).take(30).collect()); self.character_inventory.insert(character.id, inventory); self.character_bank.insert(character.id, bank_items.remove(&BankName("".to_string())).unwrap_or(CharacterBank::new(Vec::new()))); } pub fn add_character_to_room(&mut self, room_id: RoomId, character: &CharacterEntity, area_client: AreaClient) { let base_inventory_id = ((area_client.local_client.id() as u32) << 21) | 0x10000; let inventory = self.character_inventory.get_mut(&character.id).unwrap(); inventory.initialize_item_ids(base_inventory_id); let base_bank_id = ((area_client.local_client.id() as u32) << 21) | 0x20000; let default_bank = self.character_bank.get_mut(&character.id);//.unwrap().get_mut(&BankName("".to_string())); match default_bank { Some(default_bank) => { default_bank.initialize_item_ids(base_bank_id); }, None => {}, } self.character_room.insert(character.id, room_id); self.character_floor.insert(character.id, RoomFloorItems::new()); self.room_floor.entry(room_id).or_insert(RoomFloorItems::new()); let mut inc = 0xF0000000; self.room_item_id_counter.entry(room_id).or_insert(Box::new(move || { inc += 1; ClientItemId(inc) })); } pub fn get_character_inventory(&self, character: &CharacterEntity) -> Result<&CharacterInventory, ItemManagerError> { Ok(self.character_inventory.get(&character.id) .ok_or(ItemManagerError::NoCharacter(character.id))?) } pub fn get_character_bank(&self, character: &CharacterEntity) -> Result<&CharacterBank, ItemManagerError> { Ok(self.character_bank .get(&character.id) .ok_or(ItemManagerError::NoCharacter(character.id))?) //.get(&BankName("".to_string())) //.ok_or(ItemManagerError::InvalidBankName(BankName("".to_string())))?) } /*pub fn get_character_bank_mut(&mut self, character: &CharacterEntity) -> Result<&CharacterBank, ItemManagerError> { Ok(self.character_bank .get_mut(&character.id) .ok_or(ItemManagerError::NoCharacter(character.id))? .entry(BankName("".to_string())) .or_insert(CharacterBank::new(Vec::new()))) //.ok_or(ItemManagerError::InvalidBankName(BankName("".to_string())))?) }*/ pub fn remove_character_from_room(&mut self, character: &CharacterEntity) { self.character_inventory.remove(&character.id); self.character_floor.remove(&character.id); self.character_room.remove(&character.id) .as_ref() .map(|room| { if self.character_room.iter().find(|(_, r)| *r == room).is_none() { self.room_floor.remove(room); } }); } pub fn get_floor_item_by_id(&self, character: &CharacterEntity, item_id: ClientItemId) -> Result<&FloorItem, ItemManagerError> { let local_floor = self.character_floor.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; let room = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; let shared_floor = self.room_floor.get(room).ok_or(ItemManagerError::NoCharacter(character.id))?; local_floor.get_item_by_id(item_id) .or_else(|| { shared_floor.get_item_by_id(item_id) }) .ok_or(ItemManagerError::NoSuchItemId(item_id)) } pub async fn character_picks_up_item(&mut self, entity_gateway: &mut EG, character: &mut CharacterEntity, item_id: ClientItemId) -> Result { let local_floor = self.character_floor.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; let room_id = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; let shared_floor = self.room_floor.get_mut(&room_id).ok_or(ItemManagerError::NoCharacter(character.id))?; let floor_item = local_floor.get_item_handle_by_id(item_id) .or_else(|| { shared_floor.get_item_handle_by_id(item_id) }) .ok_or(ItemManagerError::NoSuchItemId(item_id))?; let trigger_create_item = match floor_item.item() { Some(FloorItem::Individual(individual_floor_item)) => { let new_inventory_item = inventory.pick_up_individual_floor_item(&individual_floor_item); match new_inventory_item { Some((new_inventory_item, slot)) => { entity_gateway.change_item_location( &new_inventory_item.entity_id, ItemLocation::Inventory { character_id: character.id, slot: slot.0, equipped: false, } ).await; if let Some(_) = new_inventory_item.mag() { entity_gateway.change_mag_owner(&new_inventory_item.entity_id, character).await; } }, None => { return Err(ItemManagerError::CouldNotAddToInventory(item_id)); }, } TriggerCreateItem::Yes }, Some(FloorItem::Stacked(stacked_floor_item)) => { let new_inventory_item = inventory.pick_up_stacked_floor_item(&stacked_floor_item); match new_inventory_item { Some((new_inventory_item, slot)) => { for entity_id in &new_inventory_item.entity_ids { entity_gateway.change_item_location( &entity_id, ItemLocation::Inventory { character_id: character.id, slot: slot.0, equipped: false, } ).await; } if stacked_floor_item.count() != new_inventory_item.count() { TriggerCreateItem::No } else { TriggerCreateItem::Yes } }, None => { return Err(ItemManagerError::CouldNotAddToInventory(item_id)); } } }, Some(FloorItem::Meseta(meseta_floor_item)) => { if character.meseta >= 999999 { return Err(ItemManagerError::CouldNotAddToInventory(item_id)); } character.meseta = std::cmp::min(character.meseta + meseta_floor_item.meseta.0, 999999); entity_gateway.save_character(&character).await; TriggerCreateItem::No }, None => { return Err(ItemManagerError::CouldNotAddToInventory(item_id)); } }; floor_item.remove_from_floor(); Ok(trigger_create_item) } pub async fn enemy_drop_item_on_local_floor(&mut self, entity_gateway: &mut EG, character: &CharacterEntity, item_drop: ItemDrop) -> Result<&FloorItem, ItemManagerError> { let room_id = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; enum ItemOrMeseta { Individual(ItemDetail), Stacked(Tool), Meseta(Meseta) } let item = match item_drop.item { ItemDropType::Weapon(w) => ItemOrMeseta::Individual(ItemDetail::Weapon(w)), ItemDropType::Armor(w) => ItemOrMeseta::Individual(ItemDetail::Armor(w)), ItemDropType::Shield(w) => ItemOrMeseta::Individual(ItemDetail::Shield(w)), ItemDropType::Unit(w) => ItemOrMeseta::Individual(ItemDetail::Unit(w)), ItemDropType::TechniqueDisk(w) => ItemOrMeseta::Individual(ItemDetail::TechniqueDisk(w)), ItemDropType::Mag(w) => ItemOrMeseta::Individual(ItemDetail::Mag(w)), //ItemDropType::IndividualTool(t) => ItemOrMeseta::Individual(ItemDetail::Tool(t)), //ItemDropType::StackedTool(t, _) => ItemOrMeseta::Stacked(t), ItemDropType::Tool(t) if t.tool.is_stackable() => ItemOrMeseta::Stacked(t), ItemDropType::Tool(t) if !t.tool.is_stackable() => ItemOrMeseta::Individual(ItemDetail::Tool(t)), ItemDropType::Meseta(m) => ItemOrMeseta::Meseta(Meseta(m)), _ => unreachable!() // rust isnt smart enough to see that the conditional on tool catches everything }; let item_id = self.room_item_id_counter.get_mut(room_id).ok_or(ItemManagerError::NoCharacter(character.id))?(); let floor_item = match item { ItemOrMeseta::Individual(item_detail) => { let entity = entity_gateway.create_item(NewItemEntity { item: item_detail.clone(), location: ItemLocation::LocalFloor { character_id: character.id, map_area: item_drop.map_area, x: item_drop.x, y: item_drop.y, z: item_drop.z, } }).await.ok_or(ItemManagerError::EntityGatewayError)?; FloorItem::Individual(IndividualFloorItem { entity_id: entity.id, item_id: item_id, item: item_detail, map_area: item_drop.map_area, x: item_drop.x, y: item_drop.y, z: item_drop.z, }) }, ItemOrMeseta::Stacked(tool) => { let entity = entity_gateway.create_item(NewItemEntity { item: ItemDetail::Tool(tool), location: ItemLocation::LocalFloor { character_id: character.id, map_area: item_drop.map_area, x: item_drop.x, y: item_drop.y, z: item_drop.z, } }).await.ok_or(ItemManagerError::EntityGatewayError)?; FloorItem::Stacked(StackedFloorItem { entity_ids: vec![entity.id], item_id: item_id, tool: tool, map_area: item_drop.map_area, x: item_drop.x, y: item_drop.y, z: item_drop.z, }) }, ItemOrMeseta::Meseta(meseta) => { FloorItem::Meseta(MesetaFloorItem { item_id: item_id, meseta: meseta, map_area: item_drop.map_area, x: item_drop.x, y: item_drop.y, z: item_drop.z, }) }, }; self.character_floor.entry(character.id).or_insert(RoomFloorItems::new()).add_item(floor_item); // TODO: make these real errors self.character_floor.get(&character.id).ok_or(ItemManagerError::Idunnoman)?.get_item_by_id(item_id).ok_or(ItemManagerError::Idunnoman) } pub async fn player_drop_item_on_shared_floor(&mut self, entity_gateway: &mut EG, character: &CharacterEntity, //inventory_item: InventoryItem, item_id: ClientItemId, item_drop_location: (MapArea, f32, f32, f32)) -> Result<(), ItemManagerError> { let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; let room_id = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; let shared_floor = self.room_floor.get_mut(&room_id).ok_or(ItemManagerError::NoCharacter(character.id))?; let dropped_inventory_item = inventory.take_item_by_id(item_id).ok_or(ItemManagerError::NoSuchItemId(item_id))?; match dropped_inventory_item { InventoryItem::Individual(individual_inventory_item) => { let individual_floor_item = shared_floor.drop_individual_inventory_item(individual_inventory_item, item_drop_location); entity_gateway.change_item_location( &individual_floor_item.entity_id, ItemLocation::SharedFloor { map_area: item_drop_location.0, x: item_drop_location.1, y: item_drop_location.2, z: item_drop_location.3, } ).await; }, InventoryItem::Stacked(stacked_inventory_item) => { let stacked_floor_item = shared_floor.drop_stacked_inventory_item(stacked_inventory_item, item_drop_location); for entity_id in &stacked_floor_item.entity_ids { entity_gateway.change_item_location( entity_id, ItemLocation::SharedFloor { map_area: item_drop_location.0, x: item_drop_location.1, y: item_drop_location.2, z: item_drop_location.3, } ).await; } }, } Ok(()) } pub async fn player_drops_meseta_on_shared_floor(&mut self, entity_gateway: &mut EG, character: &mut CharacterEntity, drop_location: ItemDropLocation, amount: u32) -> Result { let room_id = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; let shared_floor = self.room_floor.get_mut(&room_id).ok_or(ItemManagerError::NoCharacter(character.id))?; if character.meseta < amount { return Err(ItemManagerError::CouldNotDropMeseta) } character.meseta -= amount; entity_gateway.save_character(&character).await; let item_id = self.room_item_id_counter.get_mut(room_id).ok_or(ItemManagerError::NoCharacter(character.id))?(); let floor_item = FloorItem::Meseta(MesetaFloorItem { item_id: item_id, meseta: Meseta(amount), map_area: drop_location.map_area, x: drop_location.x, y: 0.0, z: drop_location.z, }); shared_floor.add_item(floor_item.clone()); Ok(floor_item) } pub async fn player_drops_partial_stack_on_shared_floor(&mut self, entity_gateway: &mut EG, character: &CharacterEntity, //inventory_item: InventoryItem, item_id: ClientItemId, drop_location: ItemDropLocation, amount: usize) -> Result<&StackedFloorItem, ItemManagerError> { let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; let room_id = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; let shared_floor = self.room_floor.get_mut(&room_id).ok_or(ItemManagerError::NoCharacter(character.id))?; let item_to_split = inventory.get_item_handle_by_id(item_id).ok_or(ItemManagerError::NoSuchItemId(item_id))?; let new_item_id = self.room_item_id_counter.get_mut(room_id).ok_or(ItemManagerError::NoCharacter(character.id))?(); let stacked_floor_item = shared_floor.drop_partial_stacked_inventory_item(item_to_split, amount, new_item_id, (drop_location.map_area, drop_location.x, 0.0, drop_location.z)) .ok_or(ItemManagerError::CouldNotSplitItem(item_id))?; for entity_id in &stacked_floor_item.entity_ids { entity_gateway.change_item_location( entity_id, ItemLocation::SharedFloor { map_area: drop_location.map_area, x: drop_location.x, y: 0.0, z: drop_location.z, } ).await; } Ok(stacked_floor_item) } pub async fn player_consumes_tool(&mut self, entity_gateway: &mut EG, character: &CharacterEntity, item_id: ClientItemId, amount: usize) -> Result { let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; let used_item = inventory.get_item_handle_by_id(item_id).ok_or(ItemManagerError::NoSuchItemId(item_id))?; let consumed_item = used_item.consume(amount)?; for entity_id in consumed_item.entity_ids() { entity_gateway.change_item_location(&entity_id, ItemLocation::Consumed).await; } Ok(consumed_item) } pub async fn player_deposits_item(&mut self, entity_gateway: &mut EG, character: &CharacterEntity, item_id: ClientItemId, amount: usize) -> Result<(), ItemManagerError> { let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; let bank = self.character_bank .get_mut(&character.id) .ok_or(ItemManagerError::NoCharacter(character.id))?; let item_to_deposit = inventory.get_item_handle_by_id(item_id).ok_or(ItemManagerError::NoSuchItemId(item_id))?; let bank_item = bank.deposit_item(item_to_deposit, amount).ok_or(ItemManagerError::Idunnoman)?; match bank_item { BankItem::Individual(individual_bank_item) => { entity_gateway.change_item_location(&individual_bank_item.entity_id, ItemLocation::Bank { character_id: character.id, name: BankName("".to_string()) }).await; }, BankItem::Stacked(stacked_bank_item) => { for entity_id in &stacked_bank_item.entity_ids { entity_gateway.change_item_location(entity_id, ItemLocation::Bank { character_id: character.id, name: BankName("".to_string()) }).await; } } } Ok(()) } pub async fn player_withdraws_item(&mut self, entity_gateway: &mut EG, character: &CharacterEntity, item_id: ClientItemId, amount: usize) -> Result<&InventoryItem, ItemManagerError> { let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; let bank = self.character_bank .get_mut(&character.id) .ok_or(ItemManagerError::NoCharacter(character.id))?; let item_to_withdraw = bank.get_item_handle_by_id(item_id).ok_or(ItemManagerError::NoSuchItemId(item_id))?; let inventory_item = inventory.withdraw_item(item_to_withdraw, amount).ok_or(ItemManagerError::Idunnoman)?; match inventory_item { (InventoryItem::Individual(individual_inventory_item), slot) => { entity_gateway.change_item_location(&individual_inventory_item.entity_id, ItemLocation::Inventory { character_id: character.id, slot: slot, equipped: false, }).await; }, (InventoryItem::Stacked(stacked_inventory_item), slot) => { for entity_id in &stacked_inventory_item.entity_ids { entity_gateway.change_item_location(entity_id, ItemLocation::Inventory { character_id: character.id, slot: slot, equipped: false, }).await; } } } Ok(inventory_item.0) } pub async fn player_feeds_mag_item(&mut self, entity_gateway: &mut EG, character: &CharacterEntity, mag_id: ClientItemId, tool_id: ClientItemId) -> Result<(), ItemManagerError> { let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; let consumed_tool = { let item_to_feed = inventory.get_item_handle_by_id(tool_id).ok_or(ItemManagerError::NoSuchItemId(tool_id))?; item_to_feed.consume(1)? }; let mut mag_handle = inventory.get_item_handle_by_id(mag_id).ok_or(ItemManagerError::NoSuchItemId(mag_id))?; let individual_item = mag_handle.item_mut() .ok_or(ItemManagerError::NoSuchItemId(mag_id))? .individual() .ok_or(ItemManagerError::WrongItemType(mag_id))?; let mag = individual_item .mag_mut() .ok_or(ItemManagerError::WrongItemType(mag_id))?; let consumed_tool_type = match &consumed_tool { ConsumedItem::Stacked(stacked_consumed_item) => stacked_consumed_item.tool.tool, _ => return Err(ItemManagerError::WrongItemType(tool_id)) }; mag.feed(consumed_tool_type); for entity_id in consumed_tool.entity_ids() { entity_gateway.feed_mag(&individual_item.entity_id, &entity_id).await; entity_gateway.change_item_location(&entity_id, ItemLocation::FedToMag { mag: individual_item.entity_id, }).await; } Ok(()) } pub async fn use_item(&mut self, used_item: ConsumedItem, entity_gateway: &mut EG, character: &mut CharacterEntity) -> Result<(), ItemManagerError> { let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; match &used_item.item() { ItemDetail::Weapon(_w) => { // something like when items are used to combine/transform them? //_ => {} }, ItemDetail::Tool(t) => { match t.tool { ToolType::PowerMaterial => { use_tool::power_material(entity_gateway, character).await; }, ToolType::MindMaterial => { use_tool::mind_material(entity_gateway, character).await; }, ToolType::EvadeMaterial => { use_tool::evade_material(entity_gateway, character).await; }, ToolType::DefMaterial => { use_tool::def_material(entity_gateway, character).await; }, ToolType::LuckMaterial => { use_tool::luck_material(entity_gateway, character).await; }, ToolType::HpMaterial => { use_tool::hp_material(entity_gateway, character).await; }, ToolType::TpMaterial => { use_tool::tp_material(entity_gateway, character).await; }, ToolType::CellOfMag502 => { use_tool::cell_of_mag_502(entity_gateway, &used_item, inventory).await; }, _ => {} } } _ => {} } Ok(()) } }