diff --git a/src/bin/main.rs b/src/bin/main.rs index a9ed329..ee9aa8c 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -350,6 +350,23 @@ 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, + } + ), + location: ItemLocation::Inventory { + character_id: character.id, + } + }).await.unwrap(); let equipped = item::EquippedEntity { weapon: Some(item2_w.id), @@ -360,7 +377,7 @@ fn main() { }; entity_gateway.set_character_equips(&character.id, &equipped).await.unwrap(); - let inventory = item::InventoryEntity::new(vec![item0, item1, item2_w, item3, item4, item5_m, item6, item7_a, item8_s, item9_u0, item10_u1, item11_u2, item12_u3, item13]); + let inventory = item::InventoryEntity::new(vec![item0, item1, item2_w, item3, item4, item5_m, item6, item7_a, item8_s, item9_u0, item10_u1, item11_u2, item12_u3, item13, item14]); 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/models.rs b/src/entity/gateway/postgres/models.rs index 02bd750..5f90986 100644 --- a/src/entity/gateway/postgres/models.rs +++ b/src/entity/gateway/postgres/models.rs @@ -597,6 +597,7 @@ pub enum PgItemLocationDetail { mag: u32, }, Shop, + SoldToShop, } impl From<ItemLocation> for PgItemLocationDetail { @@ -609,6 +610,7 @@ impl From<ItemLocation> for PgItemLocationDetail { ItemLocation::Consumed => PgItemLocationDetail::Consumed, ItemLocation::FedToMag{mag} => PgItemLocationDetail::FedToMag{mag: mag.0}, ItemLocation::Shop => PgItemLocationDetail::Shop, + ItemLocation::SoldToShop => PgItemLocationDetail::SoldToShop, } } } @@ -623,6 +625,7 @@ impl From<PgItemLocationDetail> for ItemLocation { PgItemLocationDetail::Consumed => ItemLocation::Consumed, PgItemLocationDetail::FedToMag{mag} => ItemLocation::FedToMag{mag: ItemEntityId(mag)}, PgItemLocationDetail::Shop => ItemLocation::Shop, + PgItemLocationDetail::SoldToShop => ItemLocation::SoldToShop, } } } 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 4f36983..087ee88 100644 --- a/src/entity/item/mag.rs +++ b/src/entity/item/mag.rs @@ -1098,6 +1098,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 9959f54..8e8c6d4 100644 --- a/src/entity/item/mod.rs +++ b/src/entity/item/mod.rs @@ -47,6 +47,7 @@ pub enum ItemLocation { mag: ItemEntityId, }, Shop, + SoldToShop, /*Destroyed { // marks an item that has been consumed in some way }, 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..cc973ed 100644 --- a/src/entity/item/unit.rs +++ b/src/entity/item/unit.rs @@ -384,4 +384,66 @@ 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 + ) + } } diff --git a/src/entity/item/weapon.rs b/src/entity/item/weapon.rs index 6473dd8..9f4ea27 100644 --- a/src/entity/item/weapon.rs +++ b/src/entity/item/weapon.rs @@ -1454,7 +1454,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<WeaponSpecial>, @@ -1584,6 +1584,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/ship/items/inventory.rs b/src/ship/items/inventory.rs index 9589849..7369c40 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, ItemLocation, 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; @@ -220,6 +221,62 @@ impl InventoryItem { _ => None } } + + pub fn get_sell_price(&self) -> Result<u32, ItemManagerError> { + 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) + }, + } + } } diff --git a/src/ship/items/manager.rs b/src/ship/items/manager.rs index 441c85b..f22c3a5 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 thiserror::Error; use crate::entity::gateway::EntityGateway; use crate::entity::character::{CharacterEntity, CharacterEntityId, TechLevel}; @@ -55,6 +56,9 @@ pub enum ItemManagerError { NoArmorEquipped, GatewayError(#[from] crate::entity::gateway::GatewayError), StackedItemError(Vec<ItemEntity>), + ItemNotSellable(InventoryItem), + WalletFull, + InvalidSale, } pub struct ItemManager { @@ -844,6 +848,45 @@ impl ItemManager { Ok(inventory_item) } + pub async fn player_sells_item<EG: EntityGateway>(&mut self, + entity_gateway: &mut EG, + character: &mut CharacterEntity, + item_id: ClientItemId, + amount: usize) + -> Result<(), anyhow::Error> { + 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 { + character.meseta += total_sale; + match item_sold { + InventoryItem::Individual(i) => { + entity_gateway.change_item_location(&i.entity_id, ItemLocation::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?; + entity_gateway.save_character(character).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<EG: EntityGateway>(&mut self, entity_gateway: &mut EG, @@ -901,10 +944,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?; @@ -912,7 +954,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?; diff --git a/src/ship/packet/handler/direct_message.rs b/src/ship/packet/handler/direct_message.rs index b2cb864..f17f1a1 100644 --- a/src/ship/packet/handler/direct_message.rs +++ b/src/ship/packet/handler/direct_message.rs @@ -370,11 +370,12 @@ where } }; - if client.character.meseta < item.price() as u32 { + // TODO: stop giving away wares for free + if client.character.meseta < (item.price() * buy_item.amount as usize) as u32 { return Err(ShipError::ShopError.into()) } - client.character.meseta -= item.price() as u32; + client.character.meseta -= (item.price() * buy_item.amount as usize) as u32; entity_gateway.save_character(&client.character).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?; @@ -435,11 +436,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 39d3717..1ded48e 100644 --- a/src/ship/packet/handler/message.rs +++ b/src/ship/packet/handler/message.rs @@ -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<EG>(id: ClientId, @@ -254,7 +254,7 @@ where if client.character.meseta >= charge.meseta { client.character.meseta -= charge.meseta; entity_gateway.save_character(&client.character).await?; - Ok(Box::new(None.into_iter())) + Ok(Box::new(None.into_iter())) // TODO: should probably tell other players we used charge and lost money? } else { Err(ShipError::NotEnoughMeseta(id, client.character.meseta).into()) } @@ -274,7 +274,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<EG>(id: ClientId, @@ -333,7 +333,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<EG>(id: ClientId, @@ -347,7 +347,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<EG>(id: ClientId, @@ -361,5 +361,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<EG> (id: ClientId, + sold_item: &PlayerSoldItem, + entity_gateway: &mut EG, + // client_location: &ClientLocation, + clients: &mut Clients, + item_manager: &mut ItemManager) + -> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + 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/ship.rs b/src/ship/ship.rs index 8091647..ae9255d 100644 --- a/src/ship/ship.rs +++ b/src/ship/ship.rs @@ -485,6 +485,9 @@ impl<EG: EntityGateway> ShipServerState<EG> { 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..83afca2 100644 --- a/src/ship/shops/armor.rs +++ b/src/ship/shops/armor.rs @@ -14,7 +14,7 @@ use crate::ship::item_stats::{ARMOR_STATS, SHIELD_STATS, UNIT_STATS}; #[derive(Debug)] pub enum ArmorShopItem { - Frame(ArmorType, usize), + Frame(ArmorType, usize), // slots Barrier(ShieldType), Unit(UnitType), } @@ -90,6 +90,38 @@ impl ShopItem for ArmorShopItem { } } +// impl ArmorShopItem { +// pub fn armor_from_item(a: &Armor) -> ArmorShopItem { +// ArmorShopItem::Frame(a.armor, a.slots as usize) +// } + +// pub fn shield_from_item(s: &Shield) -> ArmorShopItem { +// ArmorShopItem::Barrier(s.shield) +// } + +// pub fn unit_from_item(u: &Unit) -> ArmorShopItem { +// ArmorShopItem::Unit(u.unit) +// } +// } + +impl From<&Armor> for ArmorShopItem { + fn from(armor: &Armor) -> ArmorShopItem { + ArmorShopItem::Frame(armor.armor, armor.slots as usize) + } +} + +impl From<&Shield> for ArmorShopItem { + fn from(shield: &Shield) -> ArmorShopItem { + ArmorShopItem::Barrier(shield.shield) + } +} + +impl From<&Unit> for ArmorShopItem { + fn from(unit: &Unit) -> ArmorShopItem { + ArmorShopItem::Unit(unit.unit) + } +} + #[derive(Debug, Deserialize, Clone)] struct FrameTierItem { diff --git a/src/ship/shops/tool.rs b/src/ship/shops/tool.rs index d3a8e4b..2a025c6 100644 --- a/src/ship/shops/tool.rs +++ b/src/ship/shops/tool.rs @@ -96,6 +96,28 @@ impl ShopItem for ToolShopItem { } } +// impl ToolShopItem { +// pub fn tool_from_item(t: &Tool) -> ToolShopItem { +// ToolShopItem::Tool(t.tool) +// } + +// pub fn tech_from_item(d: &TechniqueDisk) -> ToolShopItem { +// ToolShopItem::Tech(*d) +// } +// } + +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<ToolType>); diff --git a/src/ship/shops/weapon.rs b/src/ship/shops/weapon.rs index 6818f23..1f53cf7 100644 --- a/src/ship/shops/weapon.rs +++ b/src/ship/shops/weapon.rs @@ -27,7 +27,7 @@ pub struct WeaponShopItem { weapon: WeaponType, special: Option<WeaponSpecial>, grind: usize, - attributes: [Option<WeaponAttribute>; 2], + attributes: [Option<WeaponAttribute>; 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 { @@ -515,7 +536,7 @@ impl<R: Rng + SeedableRng> WeaponShop<R> { weapon, grind, special, - attributes: [attr1, attr2], + attributes: [attr1, attr2, None], } } diff --git a/tests/test_shops.rs b/tests/test_shops.rs index ade8ccf..831109c 100644 --- a/tests/test_shops.rs +++ b/tests/test_shops.rs @@ -211,7 +211,7 @@ async fn test_player_buys_multiple_from_tool_shop() { let characters1 = entity_gateway.get_characters_by_user(&user1).await.unwrap(); let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap(); - assert!(c1.meseta < 999999); + assert_eq!(c1.meseta, 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| { @@ -259,7 +259,51 @@ 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, + } + ), + location: item::ItemLocation::Inventory { + character_id: char1.id, + } + }).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_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::Ultimate).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 characters1 = entity_gateway.get_characters_by_user(&user1).await.unwrap(); + let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap(); + assert_eq!(c1.meseta, 4406); } #[async_std::test] @@ -583,3 +627,146 @@ async fn test_units_disappear_from_shop_when_bought() { }).unwrap(); }).unwrap(); } + +#[async_std::test] +async fn test_selling_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, + } + ), + location: item::ItemLocation::Inventory { + character_id: char1.id, + } + }).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_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::Ultimate).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 characters1 = entity_gateway.get_characters_by_user(&user1).await.unwrap(); + let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap(); + assert_eq!(c1.meseta, 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, + } + ), + location: item::ItemLocation::Inventory { + character_id: char1.id, + } + }).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_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::Ultimate).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 characters1 = entity_gateway.get_characters_by_user(&user1).await.unwrap(); + let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap(); + assert_eq!(c1.meseta, 10); +} + +#[async_std::test] +async fn test_player_sells_photon_drops() { + 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, + } + ), + location: item::ItemLocation::Inventory { + character_id: char1.id, + } + }).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_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::Ultimate).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 characters1 = entity_gateway.get_characters_by_user(&user1).await.unwrap(); + let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap(); + assert_eq!(c1.meseta, 3000); +} \ No newline at end of file