diff --git a/src/bin/main.rs b/src/bin/main.rs index e4d7ee9..91f0815 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -211,18 +211,10 @@ fn main() { }).await.unwrap(); entity_gateway.use_mag_cell(&item5_m.id, &cell.id).await.unwrap(); - entity_gateway.create_item( + let item6_1 = entity_gateway.create_item( NewItemEntity { - item: ItemDetail::Weapon( - item::weapon::Weapon { - weapon: item::weapon::WeaponType::Autogun, - grind: 5, - special: Some(item::weapon::WeaponSpecial::Hell), - attrs: [Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Hit, value: 70}), - Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Dark, value: 80}), - None,], - tekked: false, - } + item: ItemDetail::ESWeapon( + item::esweapon::ESWeapon::new(item::esweapon::ESWeaponType::Saber) ), }).await.unwrap(); let item7_a = entity_gateway.create_item( @@ -230,8 +222,8 @@ fn main() { item: ItemDetail::Armor( item::armor::Armor { armor: item::armor::ArmorType::Frame, - dfp: 0, - evp: 0, + dfp: 2, + evp: 2, slots: 4, } ), @@ -242,8 +234,8 @@ fn main() { item: ItemDetail::Shield( item::shield::Shield { shield: item::shield::ShieldType::Barrier, - dfp: 0, - evp: 0, + dfp: 5, + evp: 5, } ), } @@ -253,7 +245,7 @@ fn main() { item: ItemDetail::Unit( item::unit::Unit { unit: item::unit::UnitType::PriestMind, - modifier: Some(item::unit::UnitModifier::Minus), + modifier: Some(item::unit::UnitModifier::PlusPlus), } ), } @@ -263,7 +255,7 @@ fn main() { item: ItemDetail::Unit( item::unit::Unit { unit: item::unit::UnitType::PriestMind, - modifier: Some(item::unit::UnitModifier::Minus), + modifier: Some(item::unit::UnitModifier::Plus), } ), } @@ -283,7 +275,7 @@ fn main() { item: ItemDetail::Unit( item::unit::Unit { unit: item::unit::UnitType::PriestMind, - modifier: Some(item::unit::UnitModifier::Minus), + modifier: Some(item::unit::UnitModifier::MinusMinus), } ), } @@ -295,6 +287,20 @@ fn main() { ), } ).await.unwrap(); + let item14 = entity_gateway.create_item( + NewItemEntity { + item: ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Vulcan, + grind: 5, + special: Some(item::weapon::WeaponSpecial::Charge), + attrs: [Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Hit, value: 100}), + Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Dark, value: 100}), + Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Native, value: 100}),], + tekked: true, + } + ), + }).await.unwrap(); let monomates = futures::future::join_all((0..6).map(|_| { let mut entity_gateway = entity_gateway.clone(); @@ -319,7 +325,7 @@ fn main() { }; entity_gateway.set_character_equips(&character.id, &equipped).await.unwrap(); - let inventory = item::InventoryEntity::new(vec![InventoryItemEntity::from(item0), item1.into(), item2_w.into(), item3.into(), item4.into(), item5_m.into(), item6.into(), item7_a.into(), item8_s.into(), item9_u0.into(), item10_u1.into(), item11_u2.into(), item12_u3.into(), item13.into(), monomates.into()]); + let inventory = item::InventoryEntity::new(vec![InventoryItemEntity::from(item0), item1.into(), item2_w.into(), item3.into(), item4.into(), item5_m.into(), item6.into(), item6_1.into(), item7_a.into(), item8_s.into(), item9_u0.into(), item10_u1.into(), item11_u2.into(), item12_u3.into(), item13.into(), item14.into(), monomates.into()]); entity_gateway.set_character_inventory(&character.id, &inventory).await.unwrap(); entity_gateway.set_character_bank(&character.id, &item::BankEntity::default(), item::BankName("".into())).await.unwrap(); } diff --git a/src/entity/gateway/postgres/postgres.rs b/src/entity/gateway/postgres/postgres.rs index 2fefb03..bbf116a 100644 --- a/src/entity/gateway/postgres/postgres.rs +++ b/src/entity/gateway/postgres/postgres.rs @@ -147,13 +147,13 @@ impl EntityGateway for PostgresGateway { let new_settings = sqlx::query_as::<_, PgUserSettings>("insert into user_settings (user_account, blocked_users, key_config, joystick_config, option_flags, shortcuts, symbol_chats, team_name) values ($1, $2, $3, $4, $5, $6, $7, $8) returning *;") .bind(settings.user_id.0) - .bind(settings.settings.blocked_users.to_vec().into_iter().map(|i| i.to_le_bytes().to_vec()).flatten().collect::>()) + .bind(settings.settings.blocked_users.iter().copied().flat_map(|i| i.to_le_bytes().to_vec()).collect::>()) .bind(settings.settings.key_config.to_vec()) .bind(settings.settings.joystick_config.to_vec()) .bind(settings.settings.option_flags as i32) .bind(settings.settings.shortcuts.to_vec()) .bind(settings.settings.symbol_chats.to_vec()) - .bind(settings.settings.team_name.to_vec().into_iter().map(|i| i.to_le_bytes().to_vec()).flatten().collect::>()) + .bind(settings.settings.team_name.iter().copied().flat_map(|i| i.to_le_bytes().to_vec()).collect::>()) .fetch_one(&self.pool).await?; Ok(new_settings.into()) } @@ -167,13 +167,13 @@ impl EntityGateway for PostgresGateway { async fn save_user_settings(&mut self, settings: &UserSettingsEntity) -> Result<(), GatewayError> { sqlx::query("update user_settings set blocked_users=$1, key_config=$2, joystick_config=$3, option_flags=$4, shortcuts=$5, symbol_chats=$6, team_name=$7 where id=$8") - .bind(settings.settings.blocked_users.to_vec().into_iter().map(|i| i.to_le_bytes().to_vec()).flatten().collect::>()) + .bind(settings.settings.blocked_users.iter().copied().flat_map(|i| i.to_le_bytes().to_vec()).collect::>()) .bind(&settings.settings.key_config.to_vec()) .bind(&settings.settings.joystick_config.to_vec()) .bind(&settings.settings.option_flags) .bind(&settings.settings.shortcuts.to_vec()) .bind(&settings.settings.symbol_chats.to_vec()) - .bind(settings.settings.team_name.to_vec().into_iter().map(|i| i.to_le_bytes().to_vec()).flatten().collect::>()) + .bind(settings.settings.team_name.iter().copied().flat_map(|i| i.to_le_bytes().to_vec()).collect::>()) .bind(&settings.id.0) .fetch_one(&self.pool).await?; Ok(()) diff --git a/src/entity/item/armor.rs b/src/entity/item/armor.rs index 6d2489d..451852b 100644 --- a/src/entity/item/armor.rs +++ b/src/entity/item/armor.rs @@ -297,7 +297,7 @@ pub enum ArmorModifier { } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct Armor { pub armor: ArmorType, pub dfp: u8, @@ -329,4 +329,74 @@ impl Armor { Err(ItemParseError::InvalidArmorBytes) // TODO: error handling if wrong bytes are given } } + + pub fn is_rare_item(self) -> bool { + matches!( + self.armor, + ArmorType::HunterField + | ArmorType::RangerField + | ArmorType::ForceField + | ArmorType::RevivalGarment + | ArmorType::SpiritGarment + | ArmorType::StinkFrame + | ArmorType::DPartsVer101 + | ArmorType::DPartsVer210 + | ArmorType::ParasiteWearDeRol + | ArmorType::ParasiteWearNelgal + | ArmorType::ParasiteWearVajulla + | ArmorType::SensePlate + | ArmorType::GravitonPlate + | ArmorType::AttributePlate + | ArmorType::FlowensFrame + | ArmorType::CustomFrameVerOo + | ArmorType::DbsArmor + | ArmorType::GuardWave + | ArmorType::DfField + | ArmorType::LuminousField + | ArmorType::ChuChuFever + | ArmorType::LoveHeart + | ArmorType::FlameGarment + | ArmorType::VirusArmorLafuteria + | ArmorType::BrightnessCircle + | ArmorType::AuraField + | ArmorType::ElectroFrame + | ArmorType::SacredCloth + | ArmorType::SmokingPlate + | ArmorType::StarCuirass + | ArmorType::BlackHoundCuirass + | ArmorType::MorningPrayer + | ArmorType::BlackOdoshiDomaru + | ArmorType::RedOdoshiDomaru + | ArmorType::BlackOdoshiRedNimaidou + | ArmorType::BlueOdoshiVioletNimaidou + | ArmorType::DirtyLifejacket + | ArmorType::KroesSweater + | ArmorType::WeddingDress + | ArmorType::SonicteamArmor + | ArmorType::RedCoat + | ArmorType::Thirteen + | ArmorType::MotherGarb + | ArmorType::MotherGarbPlus + | ArmorType::DressPlate + | ArmorType::Sweetheart + | ArmorType::IgnitionCloak + | ArmorType::CongealCloak + | ArmorType::TempestCloak + | ArmorType::CursedCloak + | ArmorType::SelectCloak + | ArmorType::SpiritCuirass + | ArmorType::RevivalCuriass + | ArmorType::AllianceUniform + | ArmorType::OfficerUniform + | ArmorType::CommanderUniform + | ArmorType::CrimsonCoat + | ArmorType::InfantryGear + | ArmorType::LieutenantGear + | ArmorType::InfantryMantle + | ArmorType::LieutenantMantle + | ArmorType::UnionField + | ArmorType::SamuraiArmor + | ArmorType::StealthSuit + ) + } } diff --git a/src/entity/item/esweapon.rs b/src/entity/item/esweapon.rs index 908b026..3a88ccd 100644 --- a/src/entity/item/esweapon.rs +++ b/src/entity/item/esweapon.rs @@ -253,6 +253,11 @@ impl ESWeapon { name, } } + + // TODO: this isn't even needed. all sranks are rare and only sell for 10? meseta in the shop + pub fn is_rare_item(self) -> bool { + true + } } #[cfg(test)] diff --git a/src/entity/item/mag.rs b/src/entity/item/mag.rs index e75656e..64efc2e 100644 --- a/src/entity/item/mag.rs +++ b/src/entity/item/mag.rs @@ -1099,6 +1099,53 @@ impl Mag { MagCell::LibertaKit => MagType::Agastya, } } + + // TODO: is this even needed? mags are not shop sellable...yet + pub fn is_rare_item(self) -> bool { + matches!( + self.mag, + MagType::Pitri + | MagType::Soniti + | MagType::Preta + | MagType::Churel + | MagType::Robochao + | MagType::OpaOpa + | MagType::Pian + | MagType::Chao + | MagType::ChuChu + | MagType::KapuKapu + | MagType::AngelsWing + | MagType::DevilsWing + | MagType::Elenor + | MagType::MarkIII + | MagType::MasterSystem + | MagType::Genesis + | MagType::SegaSaturn + | MagType::Dreamcast + | MagType::Hamburger + | MagType::PanzersTail + | MagType::DevilsTail + | MagType::Deva + | MagType::Rati + | MagType::Savitri + | MagType::Rukmin + | MagType::Pushan + | MagType::Diwari + | MagType::Sato + | MagType::Bhima + | MagType::Nidra + | MagType::GeungSi + | MagType::Tellusis + | MagType::StrikerUnit + | MagType::Pioneer + | MagType::Puyo + | MagType::Moro + | MagType::Rappy + | MagType::Yahoo + | MagType::GaelGiel + | MagType::Agastya + ) + } } diff --git a/src/entity/item/mod.rs b/src/entity/item/mod.rs index c083d7e..c840034 100644 --- a/src/entity/item/mod.rs +++ b/src/entity/item/mod.rs @@ -202,6 +202,7 @@ impl std::convert::From> for InventoryItemEntity { } impl InventoryItemEntity { + #[must_use] pub fn map_individual ItemEntity>(self, func: F) -> InventoryItemEntity { match self { InventoryItemEntity::Individual(item) => InventoryItemEntity::Individual(func(item)), diff --git a/src/entity/item/shield.rs b/src/entity/item/shield.rs index 5784fd4..5c42598 100644 --- a/src/entity/item/shield.rs +++ b/src/entity/item/shield.rs @@ -548,4 +548,154 @@ impl Shield { Err(ItemParseError::InvalidShieldBytes) // TODO: error handling if wrong bytes are given } } + + pub fn is_rare_item(self) -> bool { + matches!( + self.shield, + ShieldType::InvisibleGuard + | ShieldType::SacredGuard + | ShieldType::SPartsVer116 + | ShieldType::SPartsVer201 + | ShieldType::LightRelief + | ShieldType::ShieldOfDelsaber + | ShieldType::ForceWall + | ShieldType::RangerWall + | ShieldType::HunterWall + | ShieldType::AttributeWall + | ShieldType::SecretGear + | ShieldType::CombatGear + | ShieldType::ProtoRegeneGear + | ShieldType::RegenerateGear + | ShieldType::RegeneGearAdv + | ShieldType::FlowensShield + | ShieldType::CustomBarrierVerOo + | ShieldType::DbsShield + | ShieldType::RedRing + | ShieldType::TripolicShield + | ShieldType::StandstillShield + | ShieldType::SafetyHeart + | ShieldType::KasamiBracer + | ShieldType::GodsShieldSuzaku + | ShieldType::GodsShieldGenbu + | ShieldType::GodsShieldByakko + | ShieldType::GodsShieldSeiryu + | ShieldType::HuntersShell + | ShieldType::RicosGlasses + | ShieldType::RicosEarring + | ShieldType::BlueRing + | ShieldType::Barrier2 + | ShieldType::SecureFeet + | ShieldType::Barrier3 + | ShieldType::Barrier4 + | ShieldType::Barrier5 + | ShieldType::Barrier6 + | ShieldType::RestaMerge + | ShieldType::AntiMerge + | ShieldType::ShiftaMerge + | ShieldType::DebandMerge + | ShieldType::FoieMerge + | ShieldType::GifoieMerge + | ShieldType::RafoieMerge + | ShieldType::RedMerge + | ShieldType::BartaMerge + | ShieldType::GibartaMerge + | ShieldType::RabartaMerge + | ShieldType::BlueMerge + | ShieldType::ZondeMerge + | ShieldType::GizondeMerge + | ShieldType::RazondeMerge + | ShieldType::YellowMerge + | ShieldType::RecoveryBarrier + | ShieldType::AssistBarrier + | ShieldType::RedBarrier + | ShieldType::BlueBarrier + | ShieldType::YellowBarrier + | ShieldType::WeaponsGoldShield + | ShieldType::BlackGear + | ShieldType::WorksGuard + | ShieldType::RagolRing + | ShieldType::BlueRing2 + | ShieldType::BlueRing3 + | ShieldType::BlueRing4 + | ShieldType::BlueRing5 + | ShieldType::BlueRing6 + | ShieldType::BlueRing7 + | ShieldType::BlueRing8 + | ShieldType::BlueRing9 + | ShieldType::GreenRing + | ShieldType::GreenRing2 + | ShieldType::GreenRing3 + | ShieldType::GreenRing4 + | ShieldType::GreenRing5 + | ShieldType::GreenRing6 + | ShieldType::GreenRing7 + | ShieldType::GreenRing8 + | ShieldType::YellowRing + | ShieldType::YellowRing2 + | ShieldType::YellowRing3 + | ShieldType::YellowRing4 + | ShieldType::YellowRing5 + | ShieldType::YellowRing6 + | ShieldType::YellowRing7 + | ShieldType::YellowRing8 + | ShieldType::PurpleRing + | ShieldType::PurpleRing2 + | ShieldType::PurpleRing3 + | ShieldType::PurpleRing4 + | ShieldType::PurpleRing5 + | ShieldType::PurpleRing6 + | ShieldType::PurpleRing7 + | ShieldType::PurpleRing8 + | ShieldType::WhiteRing + | ShieldType::WhiteRing2 + | ShieldType::WhiteRing3 + | ShieldType::WhiteRing4 + | ShieldType::WhiteRing5 + | ShieldType::WhiteRing6 + | ShieldType::WhiteRing7 + | ShieldType::WhiteRing8 + | ShieldType::BlackRing + | ShieldType::BlackRing2 + | ShieldType::BlackRing3 + | ShieldType::BlackRing4 + | ShieldType::BlackRing5 + | ShieldType::BlackRing6 + | ShieldType::BlackRing7 + | ShieldType::BlackRing8 + | ShieldType::WeaponsSilverShield + | ShieldType::WeaponsCopperShield + | ShieldType::Gratia + | ShieldType::TripolicReflector + | ShieldType::StrikerPlus + | ShieldType::RegenerateGearBP + | ShieldType::Rupika + | ShieldType::YataMirror + | ShieldType::BunnyEars + | ShieldType::CatEars + | ShieldType::ThreeSeals + | ShieldType::GodsShieldKouryu + | ShieldType::DfShield + | ShieldType::FromTheDepths + | ShieldType::DeRolLeShield + | ShieldType::HoneycombReflector + | ShieldType::Epsiguard + | ShieldType::AngelRing + | ShieldType::UnionGuard + | ShieldType::UnionGuard2 + | ShieldType::UnionGuard3 + | ShieldType::UnionGuard4 + | ShieldType::StinkShield + | ShieldType::Unknownb + | ShieldType::Genpei + | ShieldType::Genpei2 + | ShieldType::Genpei3 + | ShieldType::Genpei4 + | ShieldType::Genpei5 + | ShieldType::Genpei6 + | ShieldType::Genpei7 + | ShieldType::Genpei8 + | ShieldType::Genpei9 + | ShieldType::Genpei10 + ) + } } diff --git a/src/entity/item/tool.rs b/src/entity/item/tool.rs index 054a922..f24f596 100644 --- a/src/entity/item/tool.rs +++ b/src/entity/item/tool.rs @@ -680,4 +680,174 @@ impl Tool { pub fn max_stack(&self) -> usize { self.tool.max_stack() } + + pub fn is_rare_item(self) -> bool { + matches!( + self.tool, + ToolType::CellOfMag502 + | ToolType::CellOfMag213 + | ToolType::PartsOfRobochao + | ToolType::HeartOfOpaOpa + | ToolType::HeartOfPian + | ToolType::HeartOfChao + | ToolType::SorcerersRightArm + | ToolType::SBeatsArms + | ToolType::PArmsArms + | ToolType::DelsabersRightArm + | ToolType::BringersRightArm + | ToolType::DelsabersLeftArm + | ToolType::SRedsArms + | ToolType::DragonsClaw + | ToolType::HildebearsHead + | ToolType::HildebluesHead + | ToolType::PartsOfBaranz + | ToolType::BelrasRightArm + | ToolType::GiGuesBody + | ToolType::SinowBerillsArms + | ToolType::GrassAssassinsArms + | ToolType::BoomasRightArm + | ToolType::GoboomasRightArm + | ToolType::GigoboomasRightArm + | ToolType::GalGryphonsWing + | ToolType::RappysWing + | ToolType::CladdingOfEpsilon + | ToolType::DeRolLeShell + | ToolType::BerillPhoton + | ToolType::ParasiticGeneFlow + | ToolType::MagicStoneIritista + | ToolType::BlueBlackStone + | ToolType::Syncesta + | ToolType::MagicWater + | ToolType::ParasiticCellTypeD + | ToolType::MagicRockHeartKey + | ToolType::MagicRockMoola + | ToolType::StarAmplifier + | ToolType::BookOfHitogata + | ToolType::HeartOfChuChu + | ToolType::PartsOfEggBlaster + | ToolType::HeartOfAngel + | ToolType::HeartOfDevil + | ToolType::KitOfHamburger + | ToolType::PanthersSpirit + | ToolType::KitOfMark3 + | ToolType::KitOfMasterSystem + | ToolType::KitOfGenesis + | ToolType::KitOfSegaSaturn + | ToolType::KitOfDreamcast + | ToolType::AmplifierOfResta + | ToolType::AmplifierOfAnti + | ToolType::AmplifierOfShifta + | ToolType::AmplifierOfDeband + | ToolType::AmplifierOfFoie + | ToolType::AmplifierOfGifoie + | ToolType::AmplifierOfRafoie + | ToolType::AmplifierOfBarta + | ToolType::AmplifierOfGibarta + | ToolType::AmplifierOfRabarta + | ToolType::AmplifierOfZonde + | ToolType::AmplifierOfGizonde + | ToolType::AmplifierOfRazonde + | ToolType::AmplifierOfRed + | ToolType::AmplifierOfBlue + | ToolType::AmplifierOfYellow + | ToolType::HeartOfKapuKapu + | ToolType::PhotonBooster + | ToolType::Addslot + | ToolType::PhotonDrop + | ToolType::PhotonSphere + | ToolType::PhotonCrystal + | ToolType::SecretTicket + | ToolType::PhotonTicket + | ToolType::BookOfKatana1 + | ToolType::BookOfKatana2 + | ToolType::BookOfKatana3 + | ToolType::WeaponsBronzeBadge + | ToolType::WeaponsSilverBadge + | ToolType::WeaponsGoldBadge + | ToolType::WeaponsCrystalBadge + | ToolType::WeaponsSteelBadge + | ToolType::WeaponsAluminumBadge + | ToolType::WeaponsLeatherBadge + | ToolType::WeaponsBoneBadge + | ToolType::LetterOfAppreciation + | ToolType::ItemTicket + | ToolType::ValentinesChocolate + | ToolType::NewYearsCard + | ToolType::ChristmasCard + | ToolType::BirthdayCard + | ToolType::ProofOfSonicTeam + | ToolType::SpecialEventTicket + | ToolType::FlowerBouquet + | ToolType::Cake + | ToolType::Accessories + | ToolType::MrNakasBusinessCard + | ToolType::Present + | ToolType::Chocolate + | ToolType::Candy + | ToolType::Cake2 + | ToolType::WeaponsSilverBadge2 + | ToolType::WeaponsGoldBadge2 + | ToolType::WeaponsCrystalBadge2 + | ToolType::WeaponsSteelBadge2 + | ToolType::WeaponsAluminumBadge2 + | ToolType::WeaponsLeatherBadge2 + | ToolType::WeaponsBoneBadge2 + | ToolType::Bouquet + | ToolType::Decoction + | ToolType::ChristmasPresent + | ToolType::EasterEgg + | ToolType::JackOLantern + | ToolType::DiskVol1WeddingMarch + | ToolType::DiskVol2DayLight + | ToolType::DiskVol3BurningRangers + | ToolType::DiskVol4OpenYourHeart + | ToolType::DiskVol5LiveLearn + | ToolType::DiskVol6Nights + | ToolType::DiskVol7EndingThemePianoVer + | ToolType::DiskVol8HeartToHeart + | ToolType::DiskVol9StrangeBlue + | ToolType::DiskVol10ReunionSystem + | ToolType::DiskVol11Pinnacles + | ToolType::DiskVol12FightInsideTheSpaceship + | ToolType::HuntersReport + | ToolType::HuntersReport2 + | ToolType::HuntersReport3 + | ToolType::HuntersReport4 + | ToolType::HuntersReport5 + | ToolType::Tablet + | ToolType::Unknown2 + | ToolType::DragonScale + | ToolType::HeavenStrikerCoat + | ToolType::PioneerParts + | ToolType::AmitiesMemo + | ToolType::HeartOfMorolian + | ToolType::RappysBeak + | ToolType::YahoosEngine + | ToolType::DPhotonCore + | ToolType::LibertaKit + | ToolType::CellOfMag0503 + | ToolType::CellOfMag0504 + | ToolType::CellOfMag0505 + | ToolType::CellOfMag0506 + | ToolType::CellOfMag0507 + | ToolType::TeamPoints500 + | ToolType::TeamPoints1000 + | ToolType::TeamPoints5000 + | ToolType::TeamPoints10000 + ) + } + + // TODO: do we actually need this function? + pub fn is_material(self) -> bool { + matches!( + self.tool, + ToolType::PowerMaterial + | ToolType::MindMaterial + | ToolType::EvadeMaterial + | ToolType::HpMaterial + | ToolType::TpMaterial + | ToolType::DefMaterial + | ToolType::LuckMaterial + ) + } } diff --git a/src/entity/item/unit.rs b/src/entity/item/unit.rs index df0b611..371f450 100644 --- a/src/entity/item/unit.rs +++ b/src/entity/item/unit.rs @@ -384,4 +384,76 @@ impl Unit { Err(ItemParseError::InvalidUnitBytes) // TODO: error handling if wrong bytes are given } } + + pub fn is_rare_item(self) -> bool { + matches!( + self.unit, + UnitType::GodPower + | UnitType::GodMind + | UnitType::GodArm + | UnitType::GodLegs + | UnitType::GodHp + | UnitType::GodTp + | UnitType::GodBody + | UnitType::GodLuck + | UnitType::HeroAbility + | UnitType::GodAbility + | UnitType::AllResist + | UnitType::SuperResist + | UnitType::PerfectResist + | UnitType::HpRevival + | UnitType::TpRevival + | UnitType::PbAmplifier + | UnitType::PbGenerate + | UnitType::PbCreate + | UnitType::DevilTechnique + | UnitType::GodTechnique + | UnitType::DevilBattle + | UnitType::GodBattle + | UnitType::CurePoison + | UnitType::CureParalysis + | UnitType::CureSlow + | UnitType::CureConfuse + | UnitType::CureFreeze + | UnitType::CureShock + | UnitType::YasakaniMagatama + | UnitType::V101 + | UnitType::V501 + | UnitType::V502 + | UnitType::V801 + | UnitType::Limiter + | UnitType::Adept + | UnitType::SwordsmanLore + | UnitType::ProofOfSwordSaint + | UnitType::Smartlink + | UnitType::DivineProtection + | UnitType::HeavenlyBattle + | UnitType::HeavenlyPower + | UnitType::HeavenlyMind + | UnitType::HeavenlyArms + | UnitType::HeavenlyLegs + | UnitType::HeavenlyBody + | UnitType::HeavenlyLuck + | UnitType::HeavenlyAbility + | UnitType::CenturionAbility + | UnitType::FriendRing + | UnitType::HeavenlyHp + | UnitType::HeavenlyTp + | UnitType::HeavenlyResist + | UnitType::HeavenlyTechnique + | UnitType::HpRessurection + | UnitType::TpRessurection + | UnitType::PbIncrease + ) + } + + pub fn modifier_stars(&self) -> i8 { + match self.modifier { + Some(UnitModifier::PlusPlus) => 1, + Some(UnitModifier::Plus) => 1, + Some(UnitModifier::Minus) => -1, + Some(UnitModifier::MinusMinus) => -1, + _ => 0, + } + } } diff --git a/src/entity/item/weapon.rs b/src/entity/item/weapon.rs index 6473dd8..5527f61 100644 --- a/src/entity/item/weapon.rs +++ b/src/entity/item/weapon.rs @@ -93,6 +93,8 @@ impl WeaponSpecial { pub fn value(&self) -> u8 { *self as u8 } + + #[must_use] pub fn rank_up(&self) -> WeaponSpecial { match self { WeaponSpecial::Draw => WeaponSpecial::Drain, @@ -138,6 +140,7 @@ impl WeaponSpecial { } } + #[must_use] pub fn rank_down(&self) -> WeaponSpecial { match self { WeaponSpecial::Draw => WeaponSpecial::Draw, @@ -1454,7 +1457,7 @@ pub enum WeaponModifier { }, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct Weapon { pub weapon: WeaponType, pub special: Option, @@ -1584,6 +1587,68 @@ impl Weapon { Err(ItemParseError::InvalidWeaponBytes) // TODO: error handling if wrong bytes are given } } -} - + // TODO: invert this? ie: handgun, saber, dagger etc. => false, _ => true? + pub fn is_rare_item(self) -> bool { + !matches!( + self.weapon, + WeaponType::Saber + | WeaponType::Brand + | WeaponType::Buster + | WeaponType::Pallasch + | WeaponType::Gladius + | WeaponType::Sword + | WeaponType::Gigush + | WeaponType::Breaker + | WeaponType::Claymore + | WeaponType::Calibur + | WeaponType::Dagger + | WeaponType::Knife + | WeaponType::Blade + | WeaponType::Edge + | WeaponType::Ripper + | WeaponType::Partisan + | WeaponType::Halbert + | WeaponType::Glaive + | WeaponType::Berdys + | WeaponType::Gungnir + | WeaponType::Slicer + | WeaponType::Spinner + | WeaponType::Cutter + | WeaponType::Sawcer + | WeaponType::Diska + | WeaponType::Handgun + | WeaponType::Autogun + | WeaponType::Lockgun + | WeaponType::Railgun + | WeaponType::Raygun + | WeaponType::Rifle + | WeaponType::Sniper + | WeaponType::Blaster + | WeaponType::Beam + | WeaponType::Laser + | WeaponType::Mechgun + | WeaponType::Assault + | WeaponType::Repeater + | WeaponType::Gatling + | WeaponType::Vulcan + | WeaponType::Shot + | WeaponType::Spread + | WeaponType::Cannon + | WeaponType::Launcher + | WeaponType::Arms + | WeaponType::Cane + | WeaponType::Stick + | WeaponType::Mace + | WeaponType::Club + | WeaponType::Rod + | WeaponType::Pole + | WeaponType::Pillar + | WeaponType::Striker + | WeaponType::Wand + | WeaponType::Staff + | WeaponType::Baton + | WeaponType::Scepter + ) + } +} diff --git a/src/login/login.rs b/src/login/login.rs index 617fad2..4181136 100644 --- a/src/login/login.rs +++ b/src/login/login.rs @@ -232,7 +232,7 @@ mod test { async fn save_user(&mut self, _user: &UserAccountEntity) -> Result<(), GatewayError> { Ok(()) } - }; + } let mut server = LoginServerState::new(TestData {}, "127.0.0.1".parse().unwrap()); diff --git a/src/ship/character.rs b/src/ship/character.rs index b1235b1..e5d4429 100644 --- a/src/ship/character.rs +++ b/src/ship/character.rs @@ -14,6 +14,7 @@ pub struct CharacterBytesBuilder<'a> { } impl<'a> CharacterBytesBuilder<'a> { + #[must_use] pub fn character(self, character: &'a CharacterEntity) -> CharacterBytesBuilder<'a> { CharacterBytesBuilder { character: Some(character), @@ -21,6 +22,7 @@ impl<'a> CharacterBytesBuilder<'a> { } } + #[must_use] pub fn stats(self, stats: &'a CharacterStats) -> CharacterBytesBuilder<'a> { CharacterBytesBuilder { stats: Some(stats), @@ -28,6 +30,7 @@ impl<'a> CharacterBytesBuilder<'a> { } } + #[must_use] pub fn level(self, level: u32) -> CharacterBytesBuilder<'a> { CharacterBytesBuilder { level: Some(level), @@ -35,6 +38,7 @@ impl<'a> CharacterBytesBuilder<'a> { } } + #[must_use] pub fn meseta(self, meseta: Meseta) -> CharacterBytesBuilder<'a> { CharacterBytesBuilder { meseta: Some(meseta), @@ -95,6 +99,7 @@ pub struct FullCharacterBytesBuilder<'a> { } impl<'a> FullCharacterBytesBuilder<'a> { + #[must_use] pub fn character(self, character: &'a CharacterEntity) -> FullCharacterBytesBuilder<'a> { FullCharacterBytesBuilder { character: Some(character), @@ -102,6 +107,7 @@ impl<'a> FullCharacterBytesBuilder<'a> { } } + #[must_use] pub fn stats(self, stats: &'a CharacterStats) -> FullCharacterBytesBuilder<'a> { FullCharacterBytesBuilder { stats: Some(stats), @@ -109,6 +115,7 @@ impl<'a> FullCharacterBytesBuilder<'a> { } } + #[must_use] pub fn level(self, level: u32) -> FullCharacterBytesBuilder<'a> { FullCharacterBytesBuilder { level: Some(level), @@ -116,6 +123,7 @@ impl<'a> FullCharacterBytesBuilder<'a> { } } + #[must_use] pub fn meseta(self, meseta: Meseta) -> FullCharacterBytesBuilder<'a> { FullCharacterBytesBuilder { meseta: Some(meseta), @@ -123,6 +131,7 @@ impl<'a> FullCharacterBytesBuilder<'a> { } } + #[must_use] pub fn inventory(self, inventory: &'a CharacterInventory) -> FullCharacterBytesBuilder<'a> { FullCharacterBytesBuilder { inventory: Some(inventory), @@ -130,6 +139,7 @@ impl<'a> FullCharacterBytesBuilder<'a> { } } + #[must_use] pub fn bank(self, bank: &'a CharacterBank) -> FullCharacterBytesBuilder<'a> { FullCharacterBytesBuilder { bank: Some(bank), @@ -137,6 +147,7 @@ impl<'a> FullCharacterBytesBuilder<'a> { } } + #[must_use] pub fn key_config(self, key_config: &'a [u8; 0x16C]) -> FullCharacterBytesBuilder<'a> { FullCharacterBytesBuilder { key_config: Some(key_config), @@ -144,6 +155,7 @@ impl<'a> FullCharacterBytesBuilder<'a> { } } + #[must_use] pub fn joystick_config(self, joystick_config: &'a [u8; 0x38]) -> FullCharacterBytesBuilder<'a> { FullCharacterBytesBuilder { joystick_config: Some(joystick_config), @@ -151,6 +163,7 @@ impl<'a> FullCharacterBytesBuilder<'a> { } } + #[must_use] pub fn symbol_chat(self, symbol_chat: &'a [u8; 1248]) -> FullCharacterBytesBuilder<'a> { FullCharacterBytesBuilder { symbol_chat: Some(symbol_chat), @@ -158,6 +171,7 @@ impl<'a> FullCharacterBytesBuilder<'a> { } } + #[must_use] pub fn tech_menu(self, tech_menu: &'a [u8; 40]) -> FullCharacterBytesBuilder<'a> { FullCharacterBytesBuilder { tech_menu: Some(tech_menu), @@ -165,6 +179,7 @@ impl<'a> FullCharacterBytesBuilder<'a> { } } + #[must_use] pub fn option_flags(self, option_flags: u32) -> FullCharacterBytesBuilder<'a> { FullCharacterBytesBuilder { option_flags: Some(option_flags), diff --git a/src/ship/items/inventory.rs b/src/ship/items/inventory.rs index 7906ee4..da4c4b0 100644 --- a/src/ship/items/inventory.rs +++ b/src/ship/items/inventory.rs @@ -1,13 +1,14 @@ use std::cmp::Ordering; use thiserror::Error; -use libpso::character::character;//::InventoryItem; +use libpso::character::character; use crate::entity::character::CharacterEntityId; use crate::entity::item::{ItemEntityId, ItemDetail, ItemEntity, ItemType, InventoryEntity, InventoryItemEntity, EquippedEntity}; -use crate::entity::item::tool::Tool; +use crate::entity::item::tool::{Tool, ToolType}; use crate::entity::item::mag::Mag; use crate::entity::item::weapon::Weapon; -use crate::ship::items::{ClientItemId, BankItem, BankItemHandle}; +use crate::ship::items::{ClientItemId, BankItem, BankItemHandle, ItemManagerError}; use crate::ship::items::floor::{IndividualFloorItem, StackedFloorItem}; +use crate::ship::shops::{ShopItem, ArmorShopItem, ToolShopItem, WeaponShopItem}; const INVENTORY_CAPACITY: usize = 30; @@ -239,6 +240,62 @@ impl InventoryItem { } } + pub fn get_sell_price(&self) -> Result { + match self { + InventoryItem::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(ItemManagerError::ItemNotSellable(self.clone())) + }, + ItemDetail::ESWeapon(_e) => { + Ok(10u32) + }, + } + }, + // the number of stacked items sold is handled by the caller. this is just the price of 1 + InventoryItem::Stacked(stacked_item) => { + Ok((ToolShopItem::from(&stacked_item.tool).price() / 8) as u32) + }, + } + } + pub fn stacked(&self) -> Option<&StackedInventoryItem> { match self { InventoryItem::Stacked(ref stacked_inventory_item) => Some(stacked_inventory_item), diff --git a/src/ship/items/manager.rs b/src/ship/items/manager.rs index 2af3cdc..950e088 100644 --- a/src/ship/items/manager.rs +++ b/src/ship/items/manager.rs @@ -1,5 +1,6 @@ use crate::ship::items::ClientItemId; use std::collections::HashMap; +use std::cmp::Ordering; use std::cell::RefCell; use thiserror::Error; use crate::entity::gateway::{EntityGateway, GatewayError}; @@ -72,6 +73,12 @@ pub enum ItemManagerError { GatewayError(#[from] GatewayError), #[error("stacked item")] StackedItemError(Vec), + #[error("item not sellable")] + ItemNotSellable(InventoryItem), + #[error("wallet full")] + WalletFull, + #[error("invalid sale")] + InvalidSale, ItemTransactionAction(Box), #[error("invalid trade")] InvalidTrade, @@ -887,6 +894,48 @@ impl ItemManager { Ok(inventory_item) } + pub async fn player_sells_item(&mut self, + entity_gateway: &mut EG, + character: &mut CharacterEntity, + item_id: ClientItemId, + amount: usize) + -> Result<(), anyhow::Error> { + let character_meseta = self.get_character_meseta(&character.id)?.0; + let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; + let sold_item_handle = inventory.get_item_handle_by_id(item_id).ok_or(ItemManagerError::NoSuchItemId(item_id))?; + if let Some(item_sold) = sold_item_handle.item() { + let unit_price = item_sold.get_sell_price()?; { + let total_sale = unit_price * amount as u32; + if character_meseta + total_sale <= 999999 { + match item_sold { + InventoryItem::Individual(i) => { + entity_gateway.add_item_note(&i.entity_id, ItemNote::SoldToShop).await?; + inventory.remove_by_id(item_id).ok_or(ItemManagerError::NoSuchItemId(item_id))?; + }, + InventoryItem::Stacked(s) => { + match amount.cmp(&s.count()) { + Ordering::Less | Ordering::Equal => { + sold_item_handle.consume(amount)?; + }, + Ordering::Greater => return Err(ItemManagerError::InvalidSale.into()), + }; + }, + } + entity_gateway.set_character_inventory(&character.id, &inventory.as_inventory_entity(&character.id)).await?; + let character_meseta = self.get_character_meseta_mut(&character.id)?; + character_meseta.0 += total_sale; + entity_gateway.set_character_meseta(&character.id, *character_meseta).await?; + } + else { + return Err(ItemManagerError::WalletFull.into()) + } + } + } else { + return Err(ItemManagerError::ItemIdNotInInventory(item_id).into()) + } + Ok(()) + } + // TODO: check if slot exists before putting units into it pub async fn player_equips_item(&mut self, entity_gateway: &mut EG, @@ -944,10 +993,9 @@ impl ItemManager { .ok_or(ItemManagerError::WrongItemType(item_id))?; let entity_id = individual.entity_id; - let mut weapon = individual + let mut weapon = *individual .weapon() - .ok_or(ItemManagerError::WrongItemType(item_id))? - .clone(); + .ok_or(ItemManagerError::WrongItemType(item_id))?; weapon.apply_modifier(&tek); entity_gateway.add_weapon_modifier(&entity_id, tek).await?; @@ -955,7 +1003,7 @@ impl ItemManager { inventory.add_item(InventoryItem::Individual(IndividualInventoryItem { entity_id, item_id, - item: ItemDetail::Weapon(weapon.clone()), + item: ItemDetail::Weapon(weapon), })); entity_gateway.set_character_inventory(&character.id, &inventory.as_inventory_entity(&character.id)).await?; @@ -1018,9 +1066,9 @@ impl ItemManager { TradeItem::Stacked(item_id, amount) => { let stacked_inventory_item = src_inventory .get_item_by_id(*item_id) - .ok_or_else(|| TradeError::InvalidItemId(*item_id))? + .ok_or(TradeError::InvalidItemId(*item_id))? .stacked() - .ok_or_else(|| TradeError::InvalidItemId(*item_id))?; + .ok_or(TradeError::InvalidItemId(*item_id))?; match dest_inventory.space_for_stacked_item(&stacked_inventory_item.tool, *amount) { SpaceForStack::Yes(YesThereIsSpace::ExistingStack) => { Ok(acc) diff --git a/src/ship/items/mod.rs b/src/ship/items/mod.rs index 0d76eb3..8a917a8 100644 --- a/src/ship/items/mod.rs +++ b/src/ship/items/mod.rs @@ -1,7 +1,7 @@ mod bank; mod floor; pub mod inventory; -mod manager; +pub mod manager; pub mod transaction; pub mod use_tool; use serde::{Serialize, Deserialize}; diff --git a/src/ship/map/area.rs b/src/ship/map/area.rs index 031337f..33b51aa 100644 --- a/src/ship/map/area.rs +++ b/src/ship/map/area.rs @@ -341,6 +341,7 @@ pub struct MapAreaLookupBuilder { } impl MapAreaLookupBuilder { + #[must_use] pub fn add(mut self, value: u16, map_area: MapArea) -> MapAreaLookupBuilder { self.map_areas.insert(value, map_area); self diff --git a/src/ship/map/enemy.rs b/src/ship/map/enemy.rs index 6cbe07e..b5efd7c 100644 --- a/src/ship/map/enemy.rs +++ b/src/ship/map/enemy.rs @@ -316,6 +316,7 @@ impl MapEnemy { } } + #[must_use] pub fn set_shiny(self) -> MapEnemy { MapEnemy { shiny: true, @@ -337,6 +338,7 @@ impl MapEnemy { TODO: distinguish between a `random` rare monster and a `set/guaranteed` rare monster? (does any acceptable quest even have this?) guaranteed rare monsters don't count towards the limit */ + #[must_use] pub fn set_rare_appearance(self) -> MapEnemy { match (self.monster, self.map_area.to_episode()) { (MonsterType::RagRappy, Episode::One) => {MapEnemy {monster: MonsterType::AlRappy, shiny:true, ..self}}, @@ -357,6 +359,7 @@ impl MapEnemy { } // in theory this should only be called on monsters we know can have rare types + #[must_use] pub fn roll_appearance_for_mission(self, rare_monster_table: &RareMonsterAppearTable) -> MapEnemy { if rare_monster_table.roll_appearance(&self.monster) { return self.set_rare_appearance() diff --git a/src/ship/map/maps.rs b/src/ship/map/maps.rs index 1f85124..0743c01 100644 --- a/src/ship/map/maps.rs +++ b/src/ship/map/maps.rs @@ -285,9 +285,9 @@ impl Maps { enemy_data }), object_data: map_variants.iter() - .map(|map_variant| { + .flat_map(|map_variant| { objects_from_map_data(map_variant.obj_file().into(), &room_mode.episode(), &map_variant.map) - }).flatten().collect(), + }).collect(), map_variants, }; maps.roll_monster_appearance(rare_monster_table); diff --git a/src/ship/packet/handler/direct_message.rs b/src/ship/packet/handler/direct_message.rs index ea6aff4..a417c6f 100644 --- a/src/ship/packet/handler/direct_message.rs +++ b/src/ship/packet/handler/direct_message.rs @@ -81,9 +81,9 @@ where { let room_id = client_location.get_room(id).map_err(|err| -> ClientLocationError { err.into() })?; let room = rooms.get_mut(room_id.0) - .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))? + .ok_or(ShipError::InvalidRoom(room_id.0 as u32))? .as_mut() - .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?; + .ok_or(ShipError::InvalidRoom(room_id.0 as u32))?; let monster = room.maps.enemy_by_id(request_item.enemy_id as usize)?; if monster.dropped_item { @@ -187,9 +187,9 @@ EG: EntityGateway { let room_id = client_location.get_room(id).map_err(|err| -> ClientLocationError { err.into() })?; let room = rooms.get_mut(room_id.0) - .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))? + .ok_or(ShipError::InvalidRoom(room_id.0 as u32))? .as_mut() - .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?; + .ok_or(ShipError::InvalidRoom(room_id.0 as u32))?; let box_object = room.maps.object_by_id(box_drop_request.object_id as usize)?; if box_object.dropped_item { @@ -297,13 +297,12 @@ where }; Ok(Box::new(other_clients_in_area.into_iter() - .map(move |c| { + .flat_map(move |c| { bank_action_pkts.clone().into_iter() .map(move |pkt| { (c.client, pkt) }) }) - .flatten() )) } @@ -319,9 +318,9 @@ pub async fn shop_request(id: ClientId, let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?; let room_id = client_location.get_room(id).map_err(|err| -> ClientLocationError { err.into() })?; let room = rooms.get(room_id.0) - .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))? + .ok_or(ShipError::InvalidRoom(room_id.0 as u32))? .as_ref() - .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?; + .ok_or(ShipError::InvalidRoom(room_id.0 as u32))?; let level = level_table.get_level_from_exp(client.character.char_class, client.character.exp) as usize; let shop_list = match shop_request.shop_type { SHOP_OPTION_WEAPON => { @@ -381,11 +380,11 @@ where }; let character_meseta = item_manager.get_character_meseta_mut(&client.character.id)?; - if character_meseta.0 < item.price() as u32 { + if character_meseta.0 < (item.price() * buy_item.amount as usize) as u32 { return Err(ShipError::ShopError.into()) } - character_meseta.0 -= item.price() as u32; + character_meseta.0 -= (item.price() * buy_item.amount as usize) as u32; entity_gateway.set_character_meseta(&client.character.id, *character_meseta).await?; let inventory_item = item_manager.player_buys_item(entity_gateway, &client.character, item, ClientItemId(buy_item.item_id), buy_item.amount as usize).await?; @@ -446,11 +445,10 @@ where let inventory = item_manager.get_character_inventory(&client.character)?; let item = inventory.get_item_by_id(ClientItemId(tek_request.item_id)) .ok_or(ItemManagerError::WrongItemType(ClientItemId(tek_request.item_id)))?; - let mut weapon = item.individual() + let mut weapon = *item.individual() .ok_or(ItemManagerError::WrongItemType(ClientItemId(tek_request.item_id)))? .weapon() - .ok_or(ItemManagerError::WrongItemType(ClientItemId(tek_request.item_id)))? - .clone(); + .ok_or(ItemManagerError::WrongItemType(ClientItemId(tek_request.item_id)))?; weapon.apply_modifier(&item::weapon::WeaponModifier::Tekked { special: special_mod, diff --git a/src/ship/packet/handler/message.rs b/src/ship/packet/handler/message.rs index 19c8281..c39ba73 100644 --- a/src/ship/packet/handler/message.rs +++ b/src/ship/packet/handler/message.rs @@ -20,9 +20,9 @@ pub async fn request_exp(id: ClientId, 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_else(|| ShipError::InvalidRoom(room_id.0 as u32))? + .ok_or(ShipError::InvalidRoom(room_id.0 as u32))? .as_mut() - .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?; + .ok_or(ShipError::InvalidRoom(room_id.0 as u32))?; let monster = room.maps.enemy_by_id(request_exp.enemy_id as usize)?; let monster_stats = room.monster_stats.get(&monster.monster).ok_or(ShipError::UnknownMonster(monster.monster))?; @@ -76,9 +76,9 @@ where let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?; let room_id = client_location.get_room(id).map_err(|err| -> ClientLocationError { err.into() })?; let room = rooms.get_mut(room_id.0) - .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))? + .ok_or(ShipError::InvalidRoom(room_id.0 as u32))? .as_mut() - .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?; + .ok_or(ShipError::InvalidRoom(room_id.0 as u32))?; let area = room.map_areas.get_area_map(player_drop_item.map_area)?; item_manager.player_drop_item_on_shared_floor(entity_gateway, &client.character, ClientItemId(player_drop_item.item_id), (*area, player_drop_item.x, player_drop_item.y, player_drop_item.z)).await?; let clients_in_area = client_location.get_clients_in_room(room_id).map_err(|err| -> ClientLocationError { err.into() })?; @@ -99,9 +99,9 @@ pub fn drop_coordinates(id: ClientId, let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?; let room_id = client_location.get_room(id).map_err(|err| -> ClientLocationError { err.into() })?; let room = rooms.get(room_id.0) - .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))? + .ok_or(ShipError::InvalidRoom(room_id.0 as u32))? .as_ref() - .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?; + .ok_or(ShipError::InvalidRoom(room_id.0 as u32))?; client.item_drop_location = Some(ItemDropLocation { map_area: *room.map_areas.get_area_map(drop_coordinates.map_area)?, @@ -110,7 +110,7 @@ pub fn drop_coordinates(id: ClientId, item_id: ClientItemId(drop_coordinates.item_id), }); - Ok(Box::new(None.into_iter())) + Ok(Box::new(None.into_iter())) // TODO: do we need to send a packet here? } pub async fn no_longer_has_item(id: ClientId, @@ -140,7 +140,7 @@ where let clients_in_area = client_location.get_clients_in_room(room_id).map_err(|err| -> ClientLocationError { err.into() })?; Ok(Box::new(clients_in_area.into_iter() - .map(move |c| { + .flat_map(move |c| { std::iter::once((c.client, SendShipPacket::Message(Message::new(GameMessage::DropSplitStack(dropped_meseta_pkt.clone()))))) .chain( if c.client != id { @@ -153,7 +153,6 @@ where } ) }) - .flatten() )) } else { @@ -191,9 +190,9 @@ pub fn update_player_position(id: ClientId, let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?; if let Ok(room_id) = client_location.get_room(id).map_err(|err| -> ClientLocationError { err.into() }) { let room = rooms.get(room_id.0) - .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))? + .ok_or(ShipError::InvalidRoom(room_id.0 as u32))? .as_ref() - .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?; + .ok_or(ShipError::InvalidRoom(room_id.0 as u32))?; match &message.msg { GameMessage::PlayerChangedMap(p) => { @@ -291,7 +290,7 @@ where let item_used_type = item_manager.player_consumes_tool(entity_gateway, &mut client.character, ClientItemId(player_use_tool.item_id), 1).await?; item_manager.use_item(item_used_type, entity_gateway, &mut client.character).await?; - Ok(Box::new(None.into_iter())) + Ok(Box::new(None.into_iter())) // TODO: should probably tell other players we used an item } pub async fn player_used_medical_center(id: ClientId, @@ -353,7 +352,7 @@ where 0 }; item_manager.player_equips_item(entity_gateway, &client.character, ClientItemId(pkt.item_id), equip_slot).await?; - Ok(Box::new(None.into_iter())) + Ok(Box::new(None.into_iter())) // TODO: tell other players you equipped an item } pub async fn player_unequips_item(id: ClientId, @@ -367,7 +366,7 @@ where { let client = clients.get(&id).ok_or(ShipError::ClientNotFound(id))?; item_manager.player_unequips_item(entity_gateway, &client.character, ClientItemId(pkt.item_id)).await?; - Ok(Box::new(None.into_iter())) + Ok(Box::new(None.into_iter())) // TODO: tell other players if you unequip an item } pub async fn player_sorts_items(id: ClientId, @@ -381,5 +380,21 @@ where { let client = clients.get(&id).ok_or(ShipError::ClientNotFound(id))?; item_manager.player_sorts_items(entity_gateway, &client.character, pkt.item_ids).await?; - Ok(Box::new(None.into_iter())) // Do clients care about the order of other clients items? + Ok(Box::new(None.into_iter())) // TODO: clients probably care about each others item orders +} + +pub async fn player_sells_item (id: ClientId, + sold_item: &PlayerSoldItem, + entity_gateway: &mut EG, + // client_location: &ClientLocation, + clients: &mut Clients, + item_manager: &mut ItemManager) + -> Result + Send>, anyhow::Error> +where + EG: EntityGateway +{ + let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?; + item_manager.player_sells_item(entity_gateway, &mut client.character, ClientItemId(sold_item.item_id), sold_item.amount as usize).await?; + // TODO: send the packet to other clients + Ok(Box::new(None.into_iter())) } diff --git a/src/ship/packet/handler/quest.rs b/src/ship/packet/handler/quest.rs index 3c4e7b4..550df18 100644 --- a/src/ship/packet/handler/quest.rs +++ b/src/ship/packet/handler/quest.rs @@ -81,8 +81,8 @@ pub fn player_chose_quest(id: ClientId, questmenuselect: &QuestMenuSelect, quest let room_id = client_location.get_room(id).map_err(|err| -> ClientLocationError { err.into() })?; let room = rooms.get_mut(room_id.0) - .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?.as_mut() - .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?; + .ok_or(ShipError::InvalidRoom(room_id.0 as u32))?.as_mut() + .ok_or(ShipError::InvalidRoom(room_id.0 as u32))?; room.maps.set_quest_data(quest.enemies.clone(), quest.objects.clone(), &room.rare_monster_table); room.map_areas = quest.map_areas.clone(); @@ -95,9 +95,9 @@ pub fn player_chose_quest(id: ClientId, questmenuselect: &QuestMenuSelect, quest client.done_loading_quest = false; } }); - Ok(Box::new(area_clients.into_iter().map(move |c| { + Ok(Box::new(area_clients.into_iter().flat_map(move |c| { vec![(c.client, SendShipPacket::QuestHeader(bin.clone())), (c.client, SendShipPacket::QuestHeader(dat.clone()))] - }).flatten())) + }))) } pub fn quest_file_request(id: ClientId, quest_file_request: &QuestFileRequest, quests: &QuestList) -> Result + Send>, ShipError> { diff --git a/src/ship/packet/handler/room.rs b/src/ship/packet/handler/room.rs index 279d53b..ba82b2b 100644 --- a/src/ship/packet/handler/room.rs +++ b/src/ship/packet/handler/room.rs @@ -157,10 +157,9 @@ pub fn done_bursting(id: ClientId, }; } let area_client = client_location.get_local_client(id).unwrap(); // TODO: unwrap - let mut result: Box + Send> = Box::new( client_location.get_client_neighbors(id).unwrap().into_iter() // TODO: unwrap - .map(move |client| { + .flat_map(move |client| { vec![ (client.client, SendShipPacket::Message(Message::new(GameMessage::BurstDone(BurstDone { client: area_client.local_client.id(), @@ -168,7 +167,6 @@ pub fn done_bursting(id: ClientId, })))), ] }) - .flatten() ); // TODO: check how often `done_bursting` is called. ie: make sure it's only used when joining a room and not each time a player warps in a pipe diff --git a/src/ship/packet/handler/trade.rs b/src/ship/packet/handler/trade.rs index 7792b4e..040bc29 100644 --- a/src/ship/packet/handler/trade.rs +++ b/src/ship/packet/handler/trade.rs @@ -481,7 +481,7 @@ where let clients_in_room = client_location.get_all_clients_by_client(id)?; let traded_item_packets = traded_items .into_iter() - .map(|item| { + .flat_map(|item| { match item.item_detail { ItemToTradeDetail::Individual(item_detail) => { [ @@ -503,8 +503,7 @@ where }, } }) - .flatten() - .map(move |packet| { + .flat_map(move |packet| { clients_in_room .clone() .into_iter() @@ -521,8 +520,7 @@ where _ => Some((client.client, SendShipPacket::Message(Message::new(packet.clone())))) } }) - }) - .flatten(); + }); let close_trade = vec![ (this.client(), SendShipPacket::TradeSuccessful(TradeSuccessful::default())), (other.client(), SendShipPacket::TradeSuccessful(TradeSuccessful::default())) diff --git a/src/ship/ship.rs b/src/ship/ship.rs index ccf95ac..d784677 100644 --- a/src/ship/ship.rs +++ b/src/ship/ship.rs @@ -357,31 +357,37 @@ impl Default for ShipServerStateBuilder { } impl ShipServerStateBuilder { + #[must_use] pub fn gateway(mut self, entity_gateway: EG) -> ShipServerStateBuilder { self.entity_gateway = Some(entity_gateway); self } + #[must_use] pub fn name(mut self, name: String) -> ShipServerStateBuilder { self.name = Some(name); self } + #[must_use] pub fn ip(mut self, ip: Ipv4Addr) -> ShipServerStateBuilder { self.ip = Some(ip); self } + #[must_use] pub fn port(mut self, port: u16) -> ShipServerStateBuilder { self.port = Some(port); self } + #[must_use] pub fn auth_token(mut self, auth_token: AuthToken) -> ShipServerStateBuilder { self.auth_token = Some(auth_token); self } + #[must_use] pub fn blocks(mut self, num_blocks: usize) -> ShipServerStateBuilder { self.num_blocks = num_blocks; self @@ -510,6 +516,9 @@ impl ShipServerState { GameMessage::SortItems(sort_items) => { handler::message::player_sorts_items(id, sort_items, &mut self.entity_gateway, &self.clients, &mut self.item_manager).await? }, + 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? + }, _ => { let cmsg = msg.clone(); let block = self.blocks.with_client(id, &self.clients)?; diff --git a/src/ship/shops/armor.rs b/src/ship/shops/armor.rs index 789db56..b85fe12 100644 --- a/src/ship/shops/armor.rs +++ b/src/ship/shops/armor.rs @@ -12,47 +12,55 @@ use crate::entity::item::unit::{Unit, UnitType}; use crate::ship::shops::ShopItem; use crate::ship::item_stats::{ARMOR_STATS, SHIELD_STATS, UNIT_STATS}; +// #[derive(Debug)] +// pub enum ArmorShopItem { +// Frame(ArmorType, usize), // slots +// Barrier(ShieldType), +// Unit(UnitType), +// } + #[derive(Debug)] pub enum ArmorShopItem { - Frame(ArmorType, usize), - Barrier(ShieldType), - Unit(UnitType), + Frame(Armor), // slots + Barrier(Shield), + Unit(Unit), } const ARMOR_MULTIPLIER: f32 = 0.799_999_95; const SHIELD_MULTIPLIER: f32 = 1.5; const UNIT_MULTIPLIER: f32 = 1000.0; +// TODO: reduce the number of type casts? impl ShopItem for ArmorShopItem { fn price(&self) -> usize { match self { - ArmorShopItem::Frame(frame, slot) => { - ARMOR_STATS.get(frame) + ArmorShopItem::Frame(frame) => { + ARMOR_STATS.get(&frame.armor) .map(|frame_stats| { - let mut price = (frame_stats.dfp + frame_stats.evp) as f32; + let mut price = (frame_stats.dfp + frame_stats.evp + frame.dfp as i32 + frame.evp as i32) as f32; price *= price; price /= ARMOR_MULTIPLIER; - price += 70.0 * frame_stats.level_req as f32; - price += 70.0 * frame_stats.level_req as f32 * *slot as f32; + price += 70.0 * (frame_stats.level_req + 1) as f32; + price += 70.0 * (frame_stats.level_req + 1) as f32 * frame.slots as f32; price as usize }) .unwrap_or(0xFFFF) }, ArmorShopItem::Barrier(barrier) => { - SHIELD_STATS.get(barrier) + SHIELD_STATS.get(&barrier.shield) .map(|barrier_stats| { - let mut price = (barrier_stats.dfp + barrier_stats.evp) as f32; + let mut price = (barrier_stats.dfp + barrier_stats.evp + barrier.dfp as i32 + barrier.evp as i32) as f32; price *= price; price /= SHIELD_MULTIPLIER; - price += 70.0 * barrier_stats.level_req as f32; + price += 70.0 * (barrier_stats.level_req + 1) as f32; price as usize }) .unwrap_or(0xFFFF) }, ArmorShopItem::Unit(unit) => { - UNIT_STATS.get(unit) + UNIT_STATS.get(&unit.unit) .map(|unit_stats| { - (unit_stats.stars as f32 * UNIT_MULTIPLIER) as usize + ((unit_stats.stars as f32 + unit.modifier_stars() as f32) * UNIT_MULTIPLIER) as usize }) .unwrap_or(0xFFFF) } @@ -65,24 +73,24 @@ impl ShopItem for ArmorShopItem { fn as_item(&self) -> ItemDetail { match self { - ArmorShopItem::Frame(frame, slot) => { + ArmorShopItem::Frame(frame) => { ItemDetail::Armor(Armor { - armor: *frame, + armor: frame.armor, dfp: 0, evp: 0, - slots: *slot as u8, + slots: frame.slots as u8, }) }, ArmorShopItem::Barrier(barrier) => { ItemDetail::Shield(Shield { - shield: *barrier, + shield: barrier.shield, dfp: 0, evp: 0, }) }, ArmorShopItem::Unit(unit) => { ItemDetail::Unit(Unit { - unit: *unit, + unit: unit.unit, modifier: None, }) }, @@ -90,6 +98,24 @@ impl ShopItem for ArmorShopItem { } } +impl From<&Armor> for ArmorShopItem { + fn from(armor: &Armor) -> ArmorShopItem { + ArmorShopItem::Frame(*armor) + } +} + +impl From<&Shield> for ArmorShopItem { + fn from(shield: &Shield) -> ArmorShopItem { + ArmorShopItem::Barrier(*shield) + } +} + +impl From<&Unit> for ArmorShopItem { + fn from(unit: &Unit) -> ArmorShopItem { + ArmorShopItem::Unit(*unit) + } +} + #[derive(Debug, Deserialize, Clone)] struct FrameTierItem { @@ -262,7 +288,12 @@ impl ArmorShop { let frame_detail = tier.item.get(frame_choice.sample(&mut self.rng)).unwrap(); let slot = self.frame.slot_rate.get(slot_choice.sample(&mut self.rng)).unwrap(); - ArmorShopItem::Frame(frame_detail.item, slot.slot) + ArmorShopItem::Frame(Armor { + armor: frame_detail.item, + dfp: 0, + evp: 0, + slots: slot.slot as u8, + }) }) .collect() } @@ -279,7 +310,11 @@ impl ArmorShop { .map(|_| { let barrier_detail = tier.item.get(barrier_choice.sample(&mut self.rng)).unwrap(); - ArmorShopItem::Barrier(barrier_detail.item) + ArmorShopItem::Barrier(Shield { + shield: barrier_detail.item, + dfp: 0, + evp: 0, + }) }) .collect() } @@ -295,7 +330,10 @@ impl ArmorShop { .map(|_| { let unit_detail = tier.item.get(unit_choice.sample(&mut self.rng)).unwrap(); - ArmorShopItem::Unit(unit_detail.item) + ArmorShopItem::Unit(Unit { + unit: unit_detail.item, + modifier: None, + }) }) .collect() }) diff --git a/src/ship/shops/tool.rs b/src/ship/shops/tool.rs index d3a8e4b..b5f9718 100644 --- a/src/ship/shops/tool.rs +++ b/src/ship/shops/tool.rs @@ -96,6 +96,18 @@ impl ShopItem for ToolShopItem { } } +impl From<&Tool> for ToolShopItem { + fn from(tool: &Tool) -> ToolShopItem { + ToolShopItem::Tool(tool.tool) + } +} + +impl From<&TechniqueDisk> for ToolShopItem { + fn from(techdisk: &TechniqueDisk) -> ToolShopItem { + ToolShopItem::Tech(*techdisk) + } +} + #[derive(Debug, Deserialize)] struct ToolTable(Vec); diff --git a/src/ship/shops/weapon.rs b/src/ship/shops/weapon.rs index 0e2e8e4..14509c8 100644 --- a/src/ship/shops/weapon.rs +++ b/src/ship/shops/weapon.rs @@ -27,7 +27,7 @@ pub struct WeaponShopItem { weapon: WeaponType, special: Option, grind: usize, - attributes: [Option; 2], + attributes: [Option; 3], } impl PartialEq for WeaponShopItem { @@ -53,6 +53,17 @@ impl PartialOrd for WeaponShopItem { } } +impl From<&Weapon> for WeaponShopItem { + fn from(weapon: &Weapon) -> WeaponShopItem { + WeaponShopItem { + weapon: weapon.weapon, + special: weapon.special, + grind: weapon.grind as usize, + attributes: weapon.attrs, + } + } +} + fn special_stars(special: &WeaponSpecial) -> usize { match special { @@ -120,7 +131,6 @@ impl ShopItem for WeaponShopItem { }).unwrap_or(0.0); price += special * special * 1000.0; - price as usize }) .unwrap_or(0xFFFF) @@ -143,6 +153,17 @@ impl ShopItem for WeaponShopItem { } } +impl WeaponShopItem { + pub fn weapon_from_item(w: &Weapon) -> WeaponShopItem { + WeaponShopItem { + weapon: w.weapon, + special: w.special, + grind: w.grind as usize, + attributes: w.attrs, + } + } +} + #[derive(Debug, Deserialize)] struct WeaponTableTierEntry { @@ -517,7 +538,7 @@ impl WeaponShop { weapon, grind, special, - attributes: [attr1, attr2], + attributes: [attr1, attr2, None], } } diff --git a/src/ship/trade.rs b/src/ship/trade.rs index 9f600a8..39d7d0e 100644 --- a/src/ship/trade.rs +++ b/src/ship/trade.rs @@ -107,7 +107,7 @@ impl TradeState { where F: Fn(&mut ClientTradeState, &mut ClientTradeState) -> T { - let mut c1 = self.trades.get(client).ok_or_else(|| TradeStateError::ClientNotInTrade(*client))?.borrow_mut(); + let mut c1 = self.trades.get(client).ok_or(TradeStateError::ClientNotInTrade(*client))?.borrow_mut(); let mut c2 = self.trades.get(&c1.other_client).ok_or(TradeStateError::ClientNotInTrade(c1.other_client))?.borrow_mut(); // sanity check diff --git a/tests/test_shops.rs b/tests/test_shops.rs index 4b9c275..27d7f06 100644 --- a/tests/test_shops.rs +++ b/tests/test_shops.rs @@ -3,6 +3,7 @@ use elseware::entity::gateway::{EntityGateway, InMemoryGateway}; use elseware::entity::item; use elseware::ship::ship::{ShipServerState, RecvShipPacket, SendShipPacket}; use elseware::ship::room::Difficulty; +use elseware::ship::items::manager::ItemManagerError; use libpso::packet::ship::*; use libpso::packet::messages::*; @@ -208,7 +209,7 @@ async fn test_player_buys_multiple_from_tool_shop() { })))).await.unwrap().for_each(drop); let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); - assert!(c1_meseta.0 < 999999); + assert_eq!(c1_meseta.0, 999749); let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); assert_eq!(p1_items.items.len(), 1); p1_items.items[0].with_stacked(|item| { @@ -255,7 +256,47 @@ async fn test_player_buys_from_armor_shop() { } #[async_std::test] -async fn test_player_sells_to_shop() { +async fn test_player_sells_3_attr_weapon_to_shop() { + let mut entity_gateway = InMemoryGateway::default(); + + let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").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::Vulcan, + grind: 5, + special: Some(item::weapon::WeaponSpecial::Charge), + attrs: [Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Hit, value: 100}), + Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Dark, value: 100}), + Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Native, value: 100}),], + tekked: true, + } + ), + }).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; + + ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerSoldItem(PlayerSoldItem { + client: 0, + target: 0, + item_id: 0x10000, + amount: 1, + })))).await.unwrap().for_each(drop); + + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 4406); } #[async_std::test] @@ -574,3 +615,535 @@ async fn test_units_disappear_from_shop_when_bought() { }).unwrap(); }).unwrap(); } + +#[async_std::test] +async fn test_player_sells_untekked_weapon() { + let mut entity_gateway = InMemoryGateway::default(); + + let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").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::Vulcan, + grind: 5, + special: Some(item::weapon::WeaponSpecial::Charge), + attrs: [Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Hit, value: 100}), + Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Dark, value: 100}), + Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Native, value: 100}),], + tekked: false, + } + ), + }).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; + + ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerSoldItem(PlayerSoldItem { + client: 0, + target: 0, + item_id: 0x10000, + amount: 1, + })))).await.unwrap().for_each(drop); + + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 1); +} + +#[async_std::test] +async fn test_player_sells_rare_item() { + let mut entity_gateway = InMemoryGateway::default(); + + let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").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::DarkFlow, + grind: 5, + special: None, + attrs: [Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Hit, value: 100}), + Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Dark, value: 100}), + Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Native, value: 100}),], + tekked: true, + } + ), + }).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; + + ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerSoldItem(PlayerSoldItem { + client: 0, + target: 0, + item_id: 0x10000, + amount: 1, + })))).await.unwrap().for_each(drop); + + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 10); +} + +#[async_std::test] +async fn test_player_sells_partial_photon_drop_stack() { + let mut entity_gateway = InMemoryGateway::default(); + + let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + + let mut p1_inv = Vec::new(); + + let mut photon_drops = Vec::new(); + for _ in 0..7usize { + photon_drops.push(entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::PhotonDrop, + } + ), + }).await.unwrap()); + } + + p1_inv.push(item::InventoryItemEntity::Stacked(photon_drops)); + + 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; + + ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerSoldItem(PlayerSoldItem { + client: 0, + target: 0, + item_id: 0x10000, + amount: 3, + })))).await.unwrap().for_each(drop); + + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 3000); +} + +#[async_std::test] +async fn test_player_sells_basic_frame() { + let mut entity_gateway = InMemoryGateway::default(); + + let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + + let mut p1_inv = Vec::new(); + + p1_inv.push(entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Armor( + item::armor::Armor { + armor: item::armor::ArmorType::Frame, + dfp: 0, + evp: 0, + slots: 0, + } + ), + }).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; + + ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerSoldItem(PlayerSoldItem { + client: 0, + target: 0, + item_id: 0x10000, + amount: 1, + })))).await.unwrap().for_each(drop); + + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 24); +} + +#[async_std::test] +async fn test_player_sells_max_frame() { + let mut entity_gateway = InMemoryGateway::default(); + + let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + + let mut p1_inv = Vec::new(); + + p1_inv.push(entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Armor( + item::armor::Armor { + armor: item::armor::ArmorType::Frame, + dfp: 2, + evp: 2, + slots: 4, + } + ), + }).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; + + ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerSoldItem(PlayerSoldItem { + client: 0, + target: 0, + item_id: 0x10000, + amount: 1, + })))).await.unwrap().for_each(drop); + + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 74); +} + +#[async_std::test] +async fn test_player_sells_basic_barrier() { + let mut entity_gateway = InMemoryGateway::default(); + + let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + + let mut p1_inv = Vec::new(); + + p1_inv.push(entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Shield( + item::shield::Shield { + shield: item::shield::ShieldType::Barrier, + dfp: 0, + evp: 0, + } + ), + }).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; + + ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerSoldItem(PlayerSoldItem { + client: 0, + target: 0, + item_id: 0x10000, + amount: 1, + })))).await.unwrap().for_each(drop); + + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 69); +} + +#[async_std::test] +async fn test_player_sells_max_barrier() { + let mut entity_gateway = InMemoryGateway::default(); + + let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + + let mut p1_inv = Vec::new(); + + p1_inv.push(entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Shield( + item::shield::Shield { + shield: item::shield::ShieldType::Barrier, + dfp: 5, + evp: 5, + } + ), + }).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; + + ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerSoldItem(PlayerSoldItem { + client: 0, + target: 0, + item_id: 0x10000, + amount: 1, + })))).await.unwrap().for_each(drop); + + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 122); +} + +#[async_std::test] +async fn test_player_sells_1_star_minusminus_unit() { + let mut entity_gateway = InMemoryGateway::default(); + + let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + + let mut p1_inv = Vec::new(); + + p1_inv.push(entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Unit( + item::unit::Unit { + unit: item::unit::UnitType::PriestMind, + modifier: Some(item::unit::UnitModifier::MinusMinus), + } + ), + }).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; + + ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerSoldItem(PlayerSoldItem { + client: 0, + target: 0, + item_id: 0x10000, + amount: 1, + })))).await.unwrap().for_each(drop); + + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 125); +} + +#[async_std::test] +async fn test_player_sells_5_star_plusplus_unit() { + let mut entity_gateway = InMemoryGateway::default(); + + let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + + let mut p1_inv = Vec::new(); + + p1_inv.push(entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Unit( + item::unit::Unit { + unit: item::unit::UnitType::GeneralHp, + modifier: Some(item::unit::UnitModifier::PlusPlus), + } + ), + }).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; + + ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerSoldItem(PlayerSoldItem { + client: 0, + target: 0, + item_id: 0x10000, + amount: 1, + })))).await.unwrap().for_each(drop); + + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 625); +} + +#[async_std::test] +async fn test_player_sells_rare_frame() { + let mut entity_gateway = InMemoryGateway::default(); + + let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + + let mut p1_inv = Vec::new(); + + p1_inv.push(entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Armor( + item::armor::Armor { + armor: item::armor::ArmorType::StinkFrame, + dfp: 10, + evp: 20, + slots: 3, + } + ), + }).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; + + ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerSoldItem(PlayerSoldItem { + client: 0, + target: 0, + item_id: 0x10000, + amount: 1, + })))).await.unwrap().for_each(drop); + + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 10); +} + +#[async_std::test] +async fn test_player_sells_rare_barrier() { + let mut entity_gateway = InMemoryGateway::default(); + + let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + + let mut p1_inv = Vec::new(); + + p1_inv.push(entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Shield( + item::shield::Shield { + shield: item::shield::ShieldType::RedRing, + dfp: 10, + evp: 20, + } + ), + }).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; + + ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerSoldItem(PlayerSoldItem { + client: 0, + target: 0, + item_id: 0x10000, + amount: 1, + })))).await.unwrap().for_each(drop); + + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 10); +} + +#[async_std::test] +async fn test_player_sells_rare_unit() { + let mut entity_gateway = InMemoryGateway::default(); + + let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + + let mut p1_inv = Vec::new(); + + p1_inv.push(entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Unit( + item::unit::Unit { + unit: item::unit::UnitType::V101, + modifier: None, + } + ), + }).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; + + ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerSoldItem(PlayerSoldItem { + client: 0, + target: 0, + item_id: 0x10000, + amount: 1, + })))).await.unwrap().for_each(drop); + + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 10); +} + +#[async_std::test] +async fn test_player_cant_sell_if_meseta_would_go_over_max() { + let mut entity_gateway = InMemoryGateway::default(); + + let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + entity_gateway.set_character_meseta(&char1.id, item::Meseta(999995)).await.unwrap(); + + let mut p1_inv = Vec::new(); + + p1_inv.push(entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Unit( + item::unit::Unit { + unit: item::unit::UnitType::V101, + modifier: None, + } + ), + }).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 ack = ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerSoldItem(PlayerSoldItem { + client: 0, + target: 0, + item_id: 0x10000, + amount: 1, + })))).await.err().unwrap(); + assert!(matches!(ack.downcast::().unwrap(), ItemManagerError::WalletFull)); + + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 999995); +}