use std::cmp::Ordering; use libpso::character::character; use crate::ship::items::ClientItemId; use crate::entity::item::{Meseta, ItemEntityId, ItemDetail, ItemEntity, InventoryEntity, InventoryItemEntity, EquippedEntity}; use std::future::Future; use crate::entity::character::CharacterEntityId; use crate::entity::item::tool::ToolType; use crate::entity::item::mag::Mag; use crate::entity::item::weapon::Weapon; use crate::ship::items::{ClientItemId, BankItem, BankItemHandle, ItemManagerError}; use crate::entity::item::unit::Unit; use crate::ship::items::floor::{IndividualFloorItem, StackedFloorItem}; use crate::ship::shops::{ShopItem, ArmorShopItem, ToolShopItem, WeaponShopItem}; use crate::ship::items::state::ItemStateError; use crate::ship::items::state::{IndividualItemDetail, StackedItemDetail, AddItemResult}; use crate::ship::items::floor::{FloorItem, FloorItemDetail}; #[derive(Clone, Debug)] pub enum InventoryItemDetail { Individual(IndividualItemDetail), Stacked(StackedItemDetail), } impl InventoryItemDetail { // TODO: rename as_stacked for consistency pub fn stacked(&self) -> Option<&StackedItemDetail> { match self { InventoryItemDetail::Stacked(sitem) => Some(sitem), _ => None, } } // TODO: rename as_stacked_mut for consistency pub fn stacked_mut(&mut self) -> Option<&mut StackedItemDetail> { match self { InventoryItemDetail::Stacked(sitem) => Some(sitem), _ => None, } } pub fn as_individual(&self) -> Option<&IndividualItemDetail> { match self { InventoryItemDetail::Individual(iitem) => Some(iitem), _ => None, } } pub fn as_individual_mut(&mut self) -> Option<&mut IndividualItemDetail> { match self { InventoryItemDetail::Individual(iitem) => Some(iitem), _ => None, } } pub fn as_client_bytes(&self) -> [u8; 16] { match self { InventoryItemDetail::Individual(item) => { item.as_client_bytes() }, InventoryItemDetail::Stacked(item) => { item.tool.as_stacked_bytes(item.entity_ids.len()) }, } } // TODO: this should probably go somewhere a bit more fundamental like ItemDetail pub fn sell_price(&self) -> Result { match self { InventoryItemDetail::Individual(individual_item) => { match &individual_item.item { // TODO: can wrapped items be sold? ItemDetail::Weapon(w) => { if !w.tekked { return Ok(1u32) } if w.is_rare_item() { return Ok(10u32) } Ok((WeaponShopItem::from(w).price() / 8) as u32) }, ItemDetail::Armor(a) => { if a.is_rare_item() { return Ok(10u32) } Ok((ArmorShopItem::from(a).price() / 8) as u32) }, ItemDetail::Shield(s) => { if s.is_rare_item() { return Ok(10u32) } Ok((ArmorShopItem::from(s).price() / 8) as u32) }, ItemDetail::Unit(u) => { if u.is_rare_item() { return Ok(10u32) } Ok((ArmorShopItem::from(u).price() / 8) as u32) }, ItemDetail::Tool(t) => { if !matches!(t.tool, ToolType::PhotonDrop | ToolType::PhotonSphere | ToolType::PhotonCrystal) && t.is_rare_item() { return Ok(10u32) } Ok((ToolShopItem::from(t).price() / 8) as u32) }, ItemDetail::TechniqueDisk(d) => { Ok((ToolShopItem::from(d).price() / 8) as u32) }, ItemDetail::Mag(_m) => { Err(ItemStateError::ItemNotSellable) }, ItemDetail::ESWeapon(_e) => { Ok(10u32) }, } }, // the number of stacked items sold is handled by the caller. this is just the price of 1 InventoryItemDetail::Stacked(stacked_item) => { Ok(((ToolShopItem::from(&stacked_item.tool).price() / 8) as u32) * stacked_item.count() as u32) }, } } } #[derive(Clone, Debug)] pub struct InventoryItem { pub item_id: ClientItemId, pub item: InventoryItemDetail, } impl InventoryItem { pub async fn with_entity_id(&self, mut param: T, mut func: F) -> Result where F: FnMut(T, ItemEntityId) -> Fut, Fut: Future>, { match &self.item { InventoryItemDetail::Individual(individual_item) => { param = func(param, individual_item.entity_id).await?; }, InventoryItemDetail::Stacked(stacked_item) => { for entity_id in &stacked_item.entity_ids { param = func(param, *entity_id).await?; } } } Ok(param) } pub async fn with_mag(&self, mut param: T, mut func: F) -> Result where F: FnMut(T, ItemEntityId, Mag) -> Fut, Fut: Future>, { if let InventoryItemDetail::Individual(individual_item) = &self.item { if let ItemDetail::Mag(mag) = &individual_item.item { param = func(param, individual_item.entity_id, mag.clone()).await?; } } Ok(param) } } #[derive(Clone, Debug)] pub struct Inventory(Vec); impl Inventory { pub fn new(items: Vec) -> Inventory { Inventory(items) } } #[derive(thiserror::Error, Debug)] pub enum InventoryError { #[error("inventory full")] InventoryFull, #[error("stack full")] StackFull, #[error("meseta full")] MesetaFull, } #[derive(Clone)] pub struct InventoryState { pub character_id: CharacterEntityId, pub item_id_counter: u32, pub inventory: Inventory, pub equipped: EquippedEntity, pub meseta: Meseta, } impl InventoryState { pub fn initialize_item_ids(&mut self, base_item_id: u32) { for (i, item) in self.inventory.0.iter_mut().enumerate() { item.item_id = ClientItemId(base_item_id + i as u32); } self.item_id_counter = base_item_id + self.inventory.0.len() as u32 + 1; } pub fn new_item_id(&mut self) -> ClientItemId { self.item_id_counter += 1; ClientItemId(self.item_id_counter) } pub fn count(&self) -> usize { self.inventory.0.len() } pub fn add_floor_item(&mut self, item: FloorItem) -> Result { match item.item { FloorItemDetail::Individual(iitem) => { if self.inventory.0.len() >= 30 { Err(InventoryError::InventoryFull) } else { self.inventory.0.push(InventoryItem { item_id: item.item_id, item: InventoryItemDetail::Individual(iitem) }); Ok(AddItemResult::NewItem) } }, FloorItemDetail::Stacked(sitem) => { let existing_stack = self.inventory.0 .iter_mut() .filter_map(|item| item.item.stacked_mut()) .find(|item| { item.tool == sitem.tool }); match existing_stack { Some(existing_stack) => { if existing_stack.entity_ids.len() + sitem.entity_ids.len() > sitem.tool.max_stack() { Err(InventoryError::StackFull) } else { existing_stack.entity_ids.append(&mut sitem.entity_ids.clone()); Ok(AddItemResult::AddToStack) } }, None => { if self.inventory.0.len() >= 30 { Err(InventoryError::InventoryFull) } else { self.inventory.0.push(InventoryItem { item_id: item.item_id, item: InventoryItemDetail::Stacked(sitem) }); Ok(AddItemResult::NewItem) } } } }, FloorItemDetail::Meseta(meseta) => { if self.meseta == Meseta(999999) { Err(InventoryError::MesetaFull) } else { self.meseta.0 = std::cmp::min(self.meseta.0 + meseta.0, 999999); Ok(AddItemResult::Meseta) } }, } } pub fn add_item(&mut self, item: InventoryItem) -> Result<(AddItemResult, InventoryItem), InventoryError> { match &item.item { InventoryItemDetail::Individual(_) => { if self.inventory.0.len() >= 30 { Err(InventoryError::InventoryFull) } else { self.inventory.0.push(item); Ok(( AddItemResult::NewItem, self.inventory.0 .last() .unwrap() .clone() )) } }, InventoryItemDetail::Stacked(sitem) => { let existing_stack = self.inventory.0 .iter_mut() .filter_map(|item| item.item.stacked_mut()) .find(|item| { item.tool == sitem.tool }); match existing_stack { Some(existing_stack) => { if existing_stack.entity_ids.len() + sitem.entity_ids.len() > sitem.tool.max_stack() { Err(InventoryError::StackFull) } else { existing_stack.entity_ids.append(&mut sitem.entity_ids.clone()); Ok(( AddItemResult::AddToStack, self.inventory.0[self.inventory.0 .iter() .filter_map(|item| item.item.stacked()) .position(|item| item.tool == sitem.tool) .unwrap()] .clone() )) } }, None => { if self.inventory.0.len() >= 30 { Err(InventoryError::InventoryFull) } else { self.inventory.0.push(item); Ok(( AddItemResult::NewItem, self.inventory.0 .last() .unwrap() .clone() )) } } } } } } pub fn take_item(&mut self, item_id: &ClientItemId, amount: u32) -> Option { let idx = self.inventory.0 .iter() .position(|i| i.item_id == *item_id)?; match &mut self.inventory.0[idx].item { InventoryItemDetail::Individual(_individual_item) => { Some(self.inventory.0.remove(idx)) }, InventoryItemDetail::Stacked(stacked_item) => { let remove_all = (amount == 0) || match stacked_item.entity_ids.len().cmp(&(amount as usize)) { Ordering::Equal => true, Ordering::Greater => false, Ordering::Less => return None, }; if remove_all { Some(self.inventory.0.remove(idx)) } else { let entity_ids = stacked_item.entity_ids.drain(..(amount as usize)).collect(); self.item_id_counter += 1; Some(InventoryItem { item_id: ClientItemId(self.item_id_counter), item: InventoryItemDetail::Stacked(StackedItemDetail { entity_ids, tool: stacked_item.tool, })}) } } } } // TODO: rename get_item_by_client_id pub fn get_by_client_id(&self, item_id: &ClientItemId) -> Option<&InventoryItem> { self.inventory.0 .iter() .find(|i| i.item_id == *item_id) } pub fn get_by_client_id_mut(&mut self, item_id: &ClientItemId) -> Option<&mut InventoryItem> { self.inventory.0 .iter_mut() .find(|i| i.item_id == *item_id) } pub fn add_meseta(&mut self, amount: u32) -> Result<(), ItemStateError> { if self.meseta.0 == 999999 { return Err(ItemStateError::FullOfMeseta) } self.meseta.0 = std::cmp::min(self.meseta.0 + amount, 999999); Ok(()) } pub fn add_meseta_no_overflow(&mut self, amount: u32) -> Result<(), ItemStateError> { if self.meseta.0 + amount > 999999 { return Err(ItemStateError::FullOfMeseta) } self.meseta.0 += amount; Ok(()) } pub fn remove_meseta(&mut self, amount: u32) -> Result<(), ItemStateError> { if amount > self.meseta.0 { return Err(ItemStateError::InvalidMesetaRemoval(amount)) } self.meseta.0 -= amount; Ok(()) } pub fn equip(&mut self, item_id: &ClientItemId, equip_slot: u8) { for item in &self.inventory.0 { if let InventoryItemDetail::Individual(inventory_item) = &item.item { if item.item_id == *item_id { match inventory_item.item { ItemDetail::Weapon(_) => self.equipped.weapon = Some(inventory_item.entity_id), ItemDetail::Armor(_) => self.equipped.armor = Some(inventory_item.entity_id), ItemDetail::Shield(_) => self.equipped.shield = Some(inventory_item.entity_id), ItemDetail::Unit(_) => { if let Some(unit) = self.equipped.unit.get_mut(equip_slot as usize) { *unit = Some(inventory_item.entity_id) } } ItemDetail::Mag(_) => self.equipped.mag = Some(inventory_item.entity_id), _ => {} } } } } } pub fn unequip(&mut self, item_id: &ClientItemId) { for item in &self.inventory.0 { if let InventoryItemDetail::Individual(inventory_item) = &item.item { if item.item_id == *item_id { match inventory_item.item { ItemDetail::Weapon(_) => self.equipped.weapon = None, ItemDetail::Armor(_) => { self.equipped.armor = None; self.equipped.unit = [None; 4]; } ItemDetail::Shield(_) => self.equipped.shield = None, ItemDetail::Unit(_) => { for unit in self.equipped.unit.iter_mut() { if *unit == Some(inventory_item.entity_id) { *unit = None } } } ItemDetail::Mag(_) => self.equipped.mag = Some(inventory_item.entity_id), _ => {} } } } } } pub fn equipped_mag_mut(&mut self) -> Option<(ItemEntityId, &mut Mag)> { let mag_id = self.equipped.mag?; self.inventory.0 .iter_mut() .filter_map(|i| { let individual = i.item.as_individual_mut()?; let entity_id = individual.entity_id; Some((entity_id, individual.as_mag_mut()?)) }) .find(|(entity_id, _)| *entity_id == mag_id) } pub fn sort(&mut self, item_ids: &[ClientItemId]) { self.inventory.0.sort_by(|a, b| { let a_index = item_ids.iter().position(|item_id| *item_id == a.item_id); let b_index = item_ids.iter().position(|item_id| *item_id == b.item_id); match (a_index, b_index) { (Some(a_index), Some(b_index)) => { a_index.cmp(&b_index) }, _ => Ordering::Equal } }); } pub fn as_inventory_entity(&self, _character_id: &CharacterEntityId) -> InventoryEntity { InventoryEntity { items: self.inventory.0.iter() .map(|item| { match &item.item { InventoryItemDetail::Individual(item) => { InventoryItemEntity::Individual(ItemEntity { id: item.entity_id, item: item.item.clone(), }) }, InventoryItemDetail::Stacked(items) => { InventoryItemEntity::Stacked(items.entity_ids.iter() .map(|id| { ItemEntity { id: *id, item: ItemDetail::Tool(items.tool) } }) .collect()) }, } }) .collect() } } pub fn as_equipped_entity(&self) -> EquippedEntity { self.equipped.clone() } pub fn as_client_inventory_items(&self) -> [character::InventoryItem; 30] { self.inventory.0.iter() .enumerate() .fold([character::InventoryItem::default(); 30], |mut inventory, (slot, item)| { let bytes = item.item.as_client_bytes(); inventory[slot].data1.copy_from_slice(&bytes[0..12]); inventory[slot].data2.copy_from_slice(&bytes[12..16]); inventory[slot].item_id = item.item_id.0; inventory[slot].equipped = 0; inventory[slot].flags = 0; if let InventoryItemDetail::Individual(individual_item) = &item.item { if self.equipped.is_equipped(&individual_item.entity_id) { if let ItemDetail::Unit(_) = individual_item.item { inventory[slot].data1[4] = self.equipped.unit.iter() .enumerate() .find(|(_, u_id)| **u_id == Some(individual_item.entity_id)) .map(|(a, _)| a) .unwrap_or(0) as u8 } inventory[slot].equipped = 1; inventory[slot].flags |= 8; } } inventory }) } }