diff --git a/.cargo/config b/.cargo/config index 9884717112..f588f639ff 100644 --- a/.cargo/config +++ b/.cargo/config @@ -5,7 +5,7 @@ rustflags = [ [alias] generate = "run --package tools --" -test-server = "-Zpackage-features run --bin veloren-server-cli --no-default-features" +test-server = "-Zpackage-features run --bin veloren-server-cli --no-default-features -- -b" tracy-server = "-Zunstable-options -Zpackage-features run --bin veloren-server-cli --no-default-features --features tracy,simd --profile no_overflow" test-voxygen = "-Zpackage-features run --bin veloren-voxygen --no-default-features --features gl,simd" tracy-voxygen = "-Zunstable-options -Zpackage-features run --bin veloren-voxygen --no-default-features --features tracy,gl,simd --profile no_overflow" diff --git a/CHANGELOG.md b/CHANGELOG.md index 60cb12e115..29f595209b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial support for alternate style keyboards - Flying birds travel the world - Plugin system now based on Wasmer 1.0.0 +- Added 4x Bag loadout slots, used for upgrading inventory space +- Added an additional Ring loadout slot +- The inventory can now be expanded to fill the whole window +- Added /dropall admin command (drops all inventory items on the ground) ### Changed @@ -26,10 +30,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Glider can now be deployed even when not on the ground - Gliding now has an energy cost for strenuous maneuvers based on lift - Translations are now folders with multiple files instead of a huge single file +- Default inventory slots reduced to 18 - existing characters given 3x 6-slot bags as compensation +- Protection rating was moved to the top left of the loadout view ### Removed - SSAAx4 option +- The Stats button and associated screen were removed ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 7f31cb6e6a..bdbf7973ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6089,6 +6089,7 @@ version = "0.8.0" dependencies = [ "authc", "chrono", + "const-tweaker", "crossbeam-channel 0.5.0", "diesel", "diesel_migrations", @@ -6179,6 +6180,7 @@ dependencies = [ "image", "inline_tweak", "itertools", + "lazy_static", "native-dialog", "num 0.3.1", "old_school_gfx_glutin_ext", diff --git a/assets/common/cave_scatter.ron b/assets/common/cave_scatter.ron index cd00eb2b47..13e3ccf291 100644 --- a/assets/common/cave_scatter.ron +++ b/assets/common/cave_scatter.ron @@ -4,7 +4,7 @@ (110, Stones), (150, ShortGrass), (120, CaveMushroom), - (2, ShinyGem), - (2, Chest), + (4, ShinyGem), + (5, Chest), (15, Crate), ] diff --git a/assets/common/items/armor/back/backpack_0.ron b/assets/common/items/armor/back/backpack_0.ron index 145163ba89..0b623b4fcc 100644 --- a/assets/common/items/armor/back/backpack_0.ron +++ b/assets/common/items/armor/back/backpack_0.ron @@ -9,4 +9,5 @@ ItemDef( ) ), quality: High, + slots: 18, ) diff --git a/assets/common/items/armor/bag/heavy_seabag.ron b/assets/common/items/armor/bag/heavy_seabag.ron new file mode 100644 index 0000000000..d679a2070f --- /dev/null +++ b/assets/common/items/armor/bag/heavy_seabag.ron @@ -0,0 +1,12 @@ +ItemDef( + name: "Heavy Seabag", + description: "Commonly used by sailors.", + kind: Armor( + ( + kind: Bag("BluePouch"), + stats: (protection: Normal(0.0)), + ) + ), + quality: Moderate, + slots: 14, +) diff --git a/assets/common/items/armor/bag/knitted_red_pouch.ron b/assets/common/items/armor/bag/knitted_red_pouch.ron new file mode 100644 index 0000000000..a3829eee99 --- /dev/null +++ b/assets/common/items/armor/bag/knitted_red_pouch.ron @@ -0,0 +1,12 @@ +ItemDef( + name: "Knitted Red Pouch", + description: "Made from some patches of dyed cloth.", + kind: Armor( + ( + kind: Bag("RedSmall"), + stats: (protection: Normal(0.0)), + ) + ), + quality: Moderate, + slots: 9, +) diff --git a/assets/common/items/armor/bag/liana_kit.ron b/assets/common/items/armor/bag/liana_kit.ron new file mode 100644 index 0000000000..6bebf02b17 --- /dev/null +++ b/assets/common/items/armor/bag/liana_kit.ron @@ -0,0 +1,12 @@ +ItemDef( + name: "Liana Kit", + description: "Woven from dried lianas.", + kind: Armor( + ( + kind: Bag("GreenMid"), + stats: (protection: Normal(0.0)), + ) + ), + quality: Moderate, + slots: 12, +) diff --git a/assets/common/items/armor/bag/mindflayer_spellbag.ron b/assets/common/items/armor/bag/mindflayer_spellbag.ron new file mode 100644 index 0000000000..c9962ce616 --- /dev/null +++ b/assets/common/items/armor/bag/mindflayer_spellbag.ron @@ -0,0 +1,12 @@ +ItemDef( + name: "Mindflayer Spellbag", + description: "You can almost feel the Mindflayer's\nevil presence flowing through the fabric.", + kind: Armor( + ( + kind: Bag("PurpleSkull"), + stats: (protection: Normal(0.0)), + ) + ), + quality: Epic, + slots: 27, +) diff --git a/assets/common/items/armor/bag/reliable_backpack.ron b/assets/common/items/armor/bag/reliable_backpack.ron new file mode 100644 index 0000000000..732bb75e69 --- /dev/null +++ b/assets/common/items/armor/bag/reliable_backpack.ron @@ -0,0 +1,12 @@ +ItemDef( + name: "Reliable Backpack", + description: "It will never give you up.", + kind: Armor( + ( + kind: Bag("LeatherLarge"), + stats: (protection: Normal(0.0)), + ) + ), + quality: High, + slots: 16, +) diff --git a/assets/common/items/armor/bag/soulkeeper_cursed.ron b/assets/common/items/armor/bag/soulkeeper_cursed.ron new file mode 100644 index 0000000000..237296d588 --- /dev/null +++ b/assets/common/items/armor/bag/soulkeeper_cursed.ron @@ -0,0 +1,12 @@ +ItemDef( + name: "Cursed Soulkeeper", + description: "WIP", + kind: Armor( + ( + kind: Bag("RedFace"), + stats: (protection: Normal(0.0)), + ) + ), + quality: Legendary, + slots: 36, +) diff --git a/assets/common/items/armor/bag/soulkeeper_pure.ron b/assets/common/items/armor/bag/soulkeeper_pure.ron new file mode 100644 index 0000000000..7cb1b49c62 --- /dev/null +++ b/assets/common/items/armor/bag/soulkeeper_pure.ron @@ -0,0 +1,12 @@ +ItemDef( + name: "Purified Soulkeeper", + description: "WIP", + kind: Armor( + ( + kind: Bag("BlueFace"), + stats: (protection: Normal(0.0)), + ) + ), + quality: Legendary, + slots: 36, +) diff --git a/assets/common/items/armor/bag/sturdy_red_backpack.ron b/assets/common/items/armor/bag/sturdy_red_backpack.ron new file mode 100644 index 0000000000..e46a915ef4 --- /dev/null +++ b/assets/common/items/armor/bag/sturdy_red_backpack.ron @@ -0,0 +1,12 @@ +ItemDef( + name: "Sturdy Red Backpack", + description: "Made from some patches of dyed cloth.", + kind: Armor( + ( + kind: Bag("RedLarge"), + stats: (protection: Normal(0.0)), + ) + ), + quality: High, + slots: 18, +) diff --git a/assets/common/items/armor/bag/tiny_leather_pouch.ron b/assets/common/items/armor/bag/tiny_leather_pouch.ron new file mode 100644 index 0000000000..21d195fdca --- /dev/null +++ b/assets/common/items/armor/bag/tiny_leather_pouch.ron @@ -0,0 +1,12 @@ +ItemDef( + name: "Tiny Leather Pouch", + description: "Made from a few patches of leather.", + kind: Armor( + ( + kind: Bag("LeatherSmall"), + stats: (protection: Normal(0.0)), + ) + ), + quality: Common, + slots: 6, +) diff --git a/assets/common/items/armor/bag/tiny_red_pouch.ron b/assets/common/items/armor/bag/tiny_red_pouch.ron new file mode 100644 index 0000000000..e8b944c994 --- /dev/null +++ b/assets/common/items/armor/bag/tiny_red_pouch.ron @@ -0,0 +1,12 @@ +ItemDef( + name: "Tiny Red Pouch", + description: "Made from a single patch of dyed cloth.", + kind: Armor( + ( + kind: Bag("RedTiny"), + stats: (protection: Normal(0.0)), + ) + ), + quality: Common, + slots: 3, +) diff --git a/assets/common/items/armor/bag/troll_hide_pack.ron b/assets/common/items/armor/bag/troll_hide_pack.ron new file mode 100644 index 0000000000..242a32dc4d --- /dev/null +++ b/assets/common/items/armor/bag/troll_hide_pack.ron @@ -0,0 +1,12 @@ +ItemDef( + name: "Trollhide Pack", + description: "Trolls were definitely hurt\nin the making of this.", + kind: Armor( + ( + kind: Bag("GreenLarge"), + stats: (protection: Normal(0.0)), + ) + ), + quality: High, + slots: 18, +) diff --git a/assets/common/items/armor/bag/woven_red_bag.ron b/assets/common/items/armor/bag/woven_red_bag.ron new file mode 100644 index 0000000000..fea605d7e3 --- /dev/null +++ b/assets/common/items/armor/bag/woven_red_bag.ron @@ -0,0 +1,12 @@ +ItemDef( + name: "Woven Red Bag", + description: "Made from some patches of dyed cloth.", + kind: Armor( + ( + kind: Bag("RedMed"), + stats: (protection: Normal(0.0)), + ) + ), + quality: Moderate, + slots: 15, +) diff --git a/assets/common/items/crafting_ing/cloth_scraps_red.ron b/assets/common/items/crafting_ing/cloth_scraps_red.ron new file mode 100644 index 0000000000..4fa8452d4e --- /dev/null +++ b/assets/common/items/crafting_ing/cloth_scraps_red.ron @@ -0,0 +1,8 @@ +ItemDef( + name: "Red Cloth Scraps", + description: "Dyed red with flower pigments.", + kind: Ingredient( + kind: "ClothScrapsRed", + ), + quality: Common, +) diff --git a/assets/common/items/crafting_ing/leather_troll.ron b/assets/common/items/crafting_ing/leather_troll.ron new file mode 100644 index 0000000000..aed22f2733 --- /dev/null +++ b/assets/common/items/crafting_ing/leather_troll.ron @@ -0,0 +1,8 @@ +ItemDef( + name: "Troll Hide", + description: "Looted from cave trolls.", + kind: Ingredient( + kind: "TrollLeather", + ), + quality: High, +) diff --git a/assets/common/items/crafting_ing/mindflayer_bag_damaged.ron b/assets/common/items/crafting_ing/mindflayer_bag_damaged.ron new file mode 100644 index 0000000000..530bc6233f --- /dev/null +++ b/assets/common/items/crafting_ing/mindflayer_bag_damaged.ron @@ -0,0 +1,8 @@ +ItemDef( + name: "Glowing Remains", + description: "Looted from an evil being.\n\nWith some additional work it can surely be\nbrought back to it's former glory...", + kind: Ingredient( + kind: "FlayerBagDamaged", + ), + quality: Epic, +) diff --git a/assets/common/items/debug/admin_black_hole.ron b/assets/common/items/debug/admin_black_hole.ron new file mode 100644 index 0000000000..f2ec6911d7 --- /dev/null +++ b/assets/common/items/debug/admin_black_hole.ron @@ -0,0 +1,12 @@ +ItemDef( + name: "Admin's Black Hole", + description: "It just works.", + kind: Armor( + ( + kind: Bag("BrownFace"), + stats: (protection: Normal(0.0)), + ) + ), + quality: Artifact, + slots: 900, +) diff --git a/assets/common/items/flowers/red.ron b/assets/common/items/flowers/red.ron index 39167814ac..463b5b5100 100644 --- a/assets/common/items/flowers/red.ron +++ b/assets/common/items/flowers/red.ron @@ -1,8 +1,8 @@ ItemDef( name: "Red Flower", - description: "Roses are red...", + description: "Can be used as a dying ingredient.", kind: Ingredient( - kind: "Flower", + kind: "FlowerRed", ), quality: Common, ) diff --git a/assets/common/items/food/coconut.ron b/assets/common/items/food/coconut.ron index 75992f3335..7c728d55e2 100644 --- a/assets/common/items/food/coconut.ron +++ b/assets/common/items/food/coconut.ron @@ -1,6 +1,6 @@ ItemDef( name: "Coconut", - description: "Restores 20 health over 10 seconds\n\nReliable source of water and fat", + description: "Restores 20 health over 10 seconds\n\nReliable source of water and fat.\n\nNaturally growing at the top of palm trees.", kind: Consumable( kind: "Coconut", effect: [ diff --git a/assets/common/items/testing/test_bag_18_slot.ron b/assets/common/items/testing/test_bag_18_slot.ron new file mode 100644 index 0000000000..4c708efeab --- /dev/null +++ b/assets/common/items/testing/test_bag_18_slot.ron @@ -0,0 +1,12 @@ +ItemDef( + name: "Test 18 slot bag", + description: "Used for unit tests do not delete", + kind: Armor( + ( + kind: Bag("TestBag18Slot"), + stats: (protection: Normal(0.0)), + ) + ), + quality: High, + slots: 18, +) diff --git a/assets/common/items/testing/test_bag_9_slot.ron b/assets/common/items/testing/test_bag_9_slot.ron new file mode 100644 index 0000000000..454d51d06e --- /dev/null +++ b/assets/common/items/testing/test_bag_9_slot.ron @@ -0,0 +1,12 @@ +ItemDef( + name: "Test 9 slot bag", + description: "Used for unit tests do not delete", + kind: Armor( + ( + kind: Bag("TestBag9Slot"), + stats: (protection: Normal(0.0)), + ) + ), + quality: High, + slots: 9, +) diff --git a/assets/common/items/weapons/dagger/basic_0.ron b/assets/common/items/weapons/dagger/basic_0.ron index 7040efb5c3..4ee3f82ff6 100644 --- a/assets/common/items/weapons/dagger/basic_0.ron +++ b/assets/common/items/weapons/dagger/basic_0.ron @@ -6,7 +6,8 @@ ItemDef( kind: Dagger, stats: ( equip_time_millis: 0, - power: 1.80 + power: 1.80, + speed: 1.0 ), ) ), diff --git a/assets/common/items/weapons/dagger/cultist_0.ron b/assets/common/items/weapons/dagger/cultist_0.ron index 70c9a034d3..d581e75484 100644 --- a/assets/common/items/weapons/dagger/cultist_0.ron +++ b/assets/common/items/weapons/dagger/cultist_0.ron @@ -6,7 +6,8 @@ ItemDef( kind: Dagger, stats: ( equip_time_millis: 0, - power: 2.00 + power: 2.00, + speed: 1.5 ), ) ), diff --git a/assets/common/loot_tables/loot_table_boss_cultist-leader.ron b/assets/common/loot_tables/loot_table_boss_cultist-leader.ron index 9abc2fdc06..2ff4fc93ff 100644 --- a/assets/common/loot_tables/loot_table_boss_cultist-leader.ron +++ b/assets/common/loot_tables/loot_table_boss_cultist-leader.ron @@ -11,6 +11,7 @@ (1, "common.items.weapons.staff.cultist_staff"), (1, "common.items.weapons.hammer.cultist_purp_2h-0"), (1, "common.items.weapons.sword.cultist_purp_2h-0"), + (0.25, "common.items.weapons.crafting_ing.mindflayer_bag_damaged"), // misc (1, "common.items.boss_drops.lantern"), (0.1, "common.items.glider.glider_purp"), diff --git a/assets/common/loot_tables/loot_table_cave_large.ron b/assets/common/loot_tables/loot_table_cave_large.ron index 6f2696d083..0f0a97eee4 100644 --- a/assets/common/loot_tables/loot_table_cave_large.ron +++ b/assets/common/loot_tables/loot_table_cave_large.ron @@ -1,7 +1,7 @@ [ // Misc (0.25, "common.items.armor.neck.neck_1"), - (0.2, "common.items.crafting_ing.cloth_scraps"), + (0.5, "common.items.crafting_ing.cloth_scraps"), (1.0, "common.items.crafting_ing.empty_vial"), (0.1, "common.items.glider.glider_blue"), (0.1, "common.items.glider.glider_morpho"), @@ -41,7 +41,7 @@ // staves (1.00, "common.items.weapons.staff.bone_staff"), (1.00, "common.items.weapons.staff.amethyst_staff"), - (0.1, "common.items.weapons.sceptre.sceptre_velorite_0"), + (0.05, "common.items.weapons.sceptre.sceptre_velorite_0"), // hammers (0.30, "common.items.weapons.hammer.cobalt_hammer-0"), (0.30, "common.items.weapons.hammer.cobalt_hammer-1"), @@ -56,7 +56,7 @@ (0.05, "common.items.weapons.hammer.steel_hammer-4"), (0.05, "common.items.weapons.hammer.steel_hammer-5"), // bows - (0.1, "common.items.weapons.bow.nature_ore_longbow-0"), + (0.05, "common.items.weapons.bow.nature_ore_longbow-0"), ] diff --git a/assets/common/loot_tables/loot_table_crafting.ron b/assets/common/loot_tables/loot_table_crafting.ron index 0e407f813c..fe9f61e862 100644 --- a/assets/common/loot_tables/loot_table_crafting.ron +++ b/assets/common/loot_tables/loot_table_crafting.ron @@ -1,7 +1,7 @@ [ // crafting ingredients (2, "common.items.crafting_ing.leather_scraps"), - (2, "common.items.crafting_ing.cloth_scraps"), + (4, "common.items.crafting_ing.cloth_scraps"), (1, "common.items.crafting_ing.empty_vial"), (0.10, "common.items.crafting_ing.shiny_gem"), diff --git a/assets/common/loot_tables/loot_table_cultists.ron b/assets/common/loot_tables/loot_table_cultists.ron index c02ebbfaac..b046ac92da 100644 --- a/assets/common/loot_tables/loot_table_cultists.ron +++ b/assets/common/loot_tables/loot_table_cultists.ron @@ -5,7 +5,7 @@ (3, "common.items.food.apple"), (3, "common.items.food.mushroom"), (3, "common.items.food.coconut"), - (3, "common.items.crafting_ing.cloth_scraps"), + (5, "common.items.crafting_ing.cloth_scraps"), // crafted (0.5, "common.items.food.apple_mushroom_curry"), (0.5, "common.items.food.apple_stick"), @@ -65,6 +65,9 @@ //gloves (0.50, "common.items.armor.hand.leather_0"), (0.50, "common.items.armor.hand.leather_2"), + //backpack + (0.001, "common.items.armor.back.backpack_0"), + (0.1, "common.items.armor.bag.heavy_seabag"), // Common Weapons // swords (0.4, "common.items.weapons.sword.wood_sword"), diff --git a/assets/common/loot_tables/loot_table_humanoids.ron b/assets/common/loot_tables/loot_table_humanoids.ron index 122a3c2294..e7872e3107 100644 --- a/assets/common/loot_tables/loot_table_humanoids.ron +++ b/assets/common/loot_tables/loot_table_humanoids.ron @@ -2,7 +2,7 @@ // Crafting Ingredients (2, "common.items.crafting_ing.empty_vial"), (0.10, "common.items.crafting_ing.shiny_gem"), - (2, "common.items.crafting_ing.cloth_scraps"), + (4, "common.items.crafting_ing.cloth_scraps"), // Consumables (0.2, "common.items.consumable.potion_minor"), // Ring diff --git a/assets/common/loot_tables/loot_table_maneater.ron b/assets/common/loot_tables/loot_table_maneater.ron new file mode 100644 index 0000000000..524a6b2030 --- /dev/null +++ b/assets/common/loot_tables/loot_table_maneater.ron @@ -0,0 +1,5 @@ +[ + (1, "common.items.flowers.red"), + (1, "common.items.crafting_ing.twigs"), + (0.5, "common.items.food.coconut"), +] diff --git a/assets/common/loot_tables/loot_table_saurok.ron b/assets/common/loot_tables/loot_table_saurok.ron new file mode 100644 index 0000000000..0fe75b1c74 --- /dev/null +++ b/assets/common/loot_tables/loot_table_saurok.ron @@ -0,0 +1,19 @@ +[ + (2, "common.items.crafting_ing.empty_vial"), + (0.01, "common.items.crafting_ing.shiny_gem"), + (3, "common.items.crafting_ing.cloth_scraps"), + (2, "common.items.crafting_ing.leather_scraps"), + // Consumables + (0.5, "common.items.consumable.potion_minor"), + // Ring + (0.2, "common.items.armor.ring.ring_gold_0"), + // Utility + (0.1, "common.items.utility.collar"), + // Bag + (0.1, "common.items.armor.bag.liana_kit"), + // Food + (2.0, "common.items.food.coconut"), + (0.3, "common.items.food.apple_mushroom_curry"), + (0.6, "common.items.food.apple_stick"), + (0.8, "common.items.food.mushroom_stick"), +] \ No newline at end of file diff --git a/assets/common/loot_tables/loot_table_troll.ron b/assets/common/loot_tables/loot_table_troll.ron new file mode 100644 index 0000000000..8707fc9cf8 --- /dev/null +++ b/assets/common/loot_tables/loot_table_troll.ron @@ -0,0 +1,4 @@ +[ + (1, "common.items.crafting_ing.leather_troll"), + (0.5, "common.items.crafting_ing.leather_scraps"), +] \ No newline at end of file diff --git a/assets/common/loot_tables/loot_table_weapon_rare.ron b/assets/common/loot_tables/loot_table_weapon_rare.ron index a831bb2620..5755a38271 100644 --- a/assets/common/loot_tables/loot_table_weapon_rare.ron +++ b/assets/common/loot_tables/loot_table_weapon_rare.ron @@ -25,9 +25,9 @@ (0.30, "common.items.weapons.hammer.cobalt_hammer-1"), (0.15, "common.items.weapons.hammer.runic_hammer"), (0.15, "common.items.weapons.hammer.ramshead_hammer"), - (0.10, "common.items.weapons.hammer.mjolnir"), + (0.01, "common.items.weapons.hammer.mjolnir"), // bows (0.60, "common.items.weapons.bow.horn_longbow-0"), (0.30, "common.items.weapons.bow.iron_longbow-0"), - (0.10, "common.items.weapons.bow.rare_longbow"), + (0.05, "common.items.weapons.bow.rare_longbow"), ] \ No newline at end of file diff --git a/assets/common/recipe_book.ron b/assets/common/recipe_book.ron index 5fb252b86b..e6db59bb0e 100644 --- a/assets/common/recipe_book.ron +++ b/assets/common/recipe_book.ron @@ -1,42 +1,321 @@ { - // Tools - "crafting_hammer": (("common.items.crafting_tools.craftsman_hammer", 1),[("common.items.crafting_ing.twigs", 6), ("common.items.crafting_ing.stones", 6)]), - "mortar_pestle": (("common.items.crafting_tools.mortar_pestle", 1), [("common.items.crafting_ing.stones", 6), ("common.items.food.coconut", 2), ("common.items.crafting_tools.craftsman_hammer", 0)]), - "sewing_set": (("common.items.crafting_tools.sewing_set", 1),[("common.items.crafting_ing.leather_scraps", 2), ("common.items.crafting_ing.twigs", 4), ("common.items.crafting_ing.stones", 2), ("common.items.crafting_ing.shiny_gem", 1)]), - // Ore and more - "velorite_frag": (("common.items.ore.veloritefrag", 2), [("common.items.ore.velorite", 1), ("common.items.crafting_tools.craftsman_hammer", 0)]), - //Potions - "potion_s": (("common.items.consumable.potion_minor", 1), [("common.items.crafting_ing.empty_vial", 1), ("common.items.food.apple", 4), ("common.items.crafting_ing.honey", 1)]), - "potion_m": (("common.items.consumable.potion_med", 1), [("common.items.consumable.potion_minor", 2), ("common.items.ore.veloritefrag", 4)]), - "collar_basic": (("common.items.utility.collar", 1), [("common.items.crafting_ing.leather_scraps", 5), ("common.items.crafting_ing.shiny_gem", 1)]), - "bomb_coconut": (("common.items.utility.bomb", 1), [("common.items.crafting_ing.stones", 10), ("common.items.food.coconut", 2), ("common.items.ore.veloritefrag", 2), ("common.items.crafting_tools.mortar_pestle", 0)]), - // Firework - "firework_blue": (("common.items.utility.firework_blue", 1), [("common.items.crafting_ing.twigs", 1), ("common.items.crafting_ing.stones", 1), ("common.items.food.coconut", 1), ("common.items.ore.veloritefrag", 1), ("common.items.crafting_tools.mortar_pestle", 0)]), - "firework_green": (("common.items.utility.firework_green", 1), [("common.items.crafting_ing.twigs", 1), ("common.items.crafting_ing.stones", 1), ("common.items.food.coconut", 1), ("common.items.ore.veloritefrag", 1), ("common.items.crafting_tools.mortar_pestle", 0)]), - "firework_purple": (("common.items.utility.firework_purple", 1), [("common.items.crafting_ing.twigs", 1), ("common.items.crafting_ing.stones", 1), ("common.items.food.coconut", 1), ("common.items.ore.veloritefrag", 1), ("common.items.crafting_tools.mortar_pestle", 0)]), - "firework_red": (("common.items.utility.firework_red", 1), [("common.items.crafting_ing.twigs", 1), ("common.items.crafting_ing.stones", 1), ("common.items.food.coconut", 1), ("common.items.ore.veloritefrag", 1), ("common.items.crafting_tools.mortar_pestle", 0)]), - "firework_yellow": (("common.items.utility.firework_yellow", 1), [("common.items.crafting_ing.twigs", 1), ("common.items.crafting_ing.stones", 1), ("common.items.food.coconut", 1), ("common.items.ore.veloritefrag", 1), ("common.items.crafting_tools.mortar_pestle", 0)]), - // Food - "apple_shroom_curry": (("common.items.food.apple_mushroom_curry", 1), [("common.items.food.mushroom", 8), ("common.items.food.coconut", 1), ("common.items.food.apple", 4), ("common.items.crafting_tools.mortar_pestle", 0)]), - "apples_stick": (("common.items.food.apple_stick", 1),[("common.items.crafting_ing.twigs", 2), ("common.items.food.apple", 2)]), - "mushroom_stick": (("common.items.food.mushroom_stick", 1),[("common.items.crafting_ing.twigs", 2), ("common.items.food.mushroom", 3)]), - "sunflower_icetea": (("common.items.food.sunflower_icetea", 4),[("common.items.crafting_ing.empty_vial", 1), ("common.items.crafting_ing.icy_fang", 1),("common.items.flowers.sunflower", 4), ("common.items.crafting_ing.honey", 1)]), - // Gliders - "Leaves Glider": (("common.items.glider.glider_leaves", 1),[("common.items.crafting_ing.twigs", 5), ("common.items.crafting_ing.leather_scraps", 5), ("common.items.crafting_ing.cloth_scraps", 5), ("common.items.crafting_ing.shiny_gem", 1), ("common.items.crafting_tools.craftsman_hammer", 0),("common.items.crafting_tools.sewing_set", 0)]), - "Sand Raptor Wings": (("common.items.glider.glider_sandraptor", 1),[("common.items.crafting_ing.raptor_feather", 6), ("common.items.crafting_ing.twigs", 5), ("common.items.crafting_ing.leather_scraps", 5), ("common.items.crafting_ing.cloth_scraps", 5), ("common.items.crafting_ing.shiny_gem", 1), ("common.items.crafting_tools.craftsman_hammer", 0),("common.items.crafting_tools.sewing_set", 0)]), - "Snow Raptor Wings": (("common.items.glider.glider_snowraptor", 1),[("common.items.crafting_ing.raptor_feather", 6), ("common.items.crafting_ing.twigs", 5), ("common.items.crafting_ing.leather_scraps", 5), ("common.items.crafting_ing.cloth_scraps", 5), ("common.items.crafting_ing.icy_fang", 1), ("common.items.crafting_ing.shiny_gem", 1), ("common.items.crafting_tools.craftsman_hammer", 0),("common.items.crafting_tools.sewing_set", 0)]), - "Wood Raptor Wings": (("common.items.glider.glider_woodraptor", 1),[("common.items.crafting_ing.raptor_feather", 6), ("common.items.crafting_ing.twigs", 15), ("common.items.crafting_ing.leather_scraps", 5), ("common.items.crafting_ing.cloth_scraps", 5), ("common.items.crafting_ing.shiny_gem", 1), ("common.items.crafting_tools.craftsman_hammer", 0),("common.items.crafting_tools.sewing_set", 0)]), - // Weapons - "velorite_sceptre": (("common.items.weapons.sceptre.sceptre_velorite_0", 1),[("common.items.crafting_ing.twigs", 20), ("common.items.ore.veloritefrag", 10), ("common.items.crafting_ing.shiny_gem", 4), ("common.items.crafting_tools.craftsman_hammer", 0)]), - // Enhanced starting weapons - "better bow": (("common.items.weapons.bow.wood_shortbow-0", 1), [("common.items.crafting_ing.leather_scraps", 8),("common.items.crafting_ing.twigs", 6), ("common.items.crafting_ing.stones", 0)]), - "better sword": (("common.items.weapons.sword.wood_sword", 1), [("common.items.crafting_ing.leather_scraps", 4),("common.items.crafting_ing.twigs", 10), ("common.items.ore.veloritefrag", 1), ("common.items.crafting_ing.stones", 0)]), - // Adventurer/Beginner Leather Set - "adventure back": (("common.items.armor.back.leather_adventurer", 1),[("common.items.crafting_ing.leather_scraps", 4)]), - "adventure belt": (("common.items.armor.belt.leather_adventurer", 1),[("common.items.crafting_ing.leather_scraps", 2)]), - "adventure chest": (("common.items.armor.chest.leather_adventurer", 1),[("common.items.crafting_ing.leather_scraps", 12)]), - "adventure feet": (("common.items.armor.foot.leather_adventurer", 1),[("common.items.crafting_ing.leather_scraps", 3)]), - "adventure hands": (("common.items.armor.hand.leather_adventurer", 1),[("common.items.crafting_ing.leather_scraps", 4)]), - "adventure pants": (("common.items.armor.pants.leather_adventurer", 1),[("common.items.crafting_ing.leather_scraps", 8)]), - "adventure shoulder": (("common.items.armor.shoulder.leather_adventurer", 1),[("common.items.crafting_ing.leather_scraps", 12)]), + "crafting_hammer": ( + ("common.items.crafting_tools.craftsman_hammer", 1), + [ + ("common.items.crafting_ing.twigs", 6), + ("common.items.crafting_ing.stones", 6), + ], + ), + "mortar_pestle": ( + ("common.items.crafting_tools.mortar_pestle", 1), + [ + ("common.items.crafting_ing.stones", 6), + ("common.items.food.coconut", 1), + ("common.items.crafting_tools.craftsman_hammer", 0), + ], + ), + "sewing_set": ( + ("common.items.crafting_tools.sewing_set", 1), + [ + ("common.items.crafting_ing.leather_scraps", 2), + ("common.items.crafting_ing.twigs", 4), + ("common.items.crafting_ing.stones", 2), + ], + ), + "velorite_frag": ( + ("common.items.ore.veloritefrag", 2), + [ + ("common.items.ore.velorite", 1), + ("common.items.crafting_tools.craftsman_hammer", 0), + ], + ), + "potion_s": ( + ("common.items.consumable.potion_minor", 1), + [ + ("common.items.crafting_ing.empty_vial", 1), + ("common.items.food.apple", 4), + ("common.items.crafting_ing.honey", 1), + ], + ), + "potion_m": ( + ("common.items.consumable.potion_med", 1), + [ + ("common.items.consumable.potion_minor", 2), + ("common.items.ore.veloritefrag", 4), + ], + ), + "collar_basic": ( + ("common.items.utility.collar", 1), + [ + ("common.items.crafting_ing.leather_scraps", 5), + ("common.items.crafting_ing.shiny_gem", 1), + ], + ), + "bomb_coconut": ( + ("common.items.utility.bomb", 1), + [ + ("common.items.crafting_ing.stones", 10), + ("common.items.food.coconut", 2), + ("common.items.ore.veloritefrag", 2), + ("common.items.crafting_tools.mortar_pestle", 0), + ], + ), + "firework_blue": ( + ("common.items.utility.firework_blue", 1), + [ + ("common.items.crafting_ing.twigs", 1), + ("common.items.crafting_ing.stones", 1), + ("common.items.food.coconut", 1), + ("common.items.ore.veloritefrag", 1), + ("common.items.crafting_tools.mortar_pestle", 0), + ], + ), + "firework_green": ( + ("common.items.utility.firework_green", 1), + [ + ("common.items.crafting_ing.twigs", 1), + ("common.items.crafting_ing.stones", 1), + ("common.items.food.coconut", 1), + ("common.items.ore.veloritefrag", 1), + ("common.items.crafting_tools.mortar_pestle", 0), + ], + ), + "firework_purple": ( + ("common.items.utility.firework_purple", 1), + [ + ("common.items.crafting_ing.twigs", 1), + ("common.items.crafting_ing.stones", 1), + ("common.items.food.coconut", 1), + ("common.items.ore.veloritefrag", 1), + ("common.items.crafting_tools.mortar_pestle", 0), + ], + ), + "firework_red": ( + ("common.items.utility.firework_red", 1), + [ + ("common.items.crafting_ing.twigs", 1), + ("common.items.crafting_ing.stones", 1), + ("common.items.food.coconut", 1), + ("common.items.ore.veloritefrag", 1), + ("common.items.crafting_tools.mortar_pestle", 0), + ], + ), + "firework_yellow": ( + ("common.items.utility.firework_yellow", 1), + [ + ("common.items.crafting_ing.twigs", 1), + ("common.items.crafting_ing.stones", 1), + ("common.items.food.coconut", 1), + ("common.items.ore.veloritefrag", 1), + ("common.items.crafting_tools.mortar_pestle", 0), + ], + ), + "apple_shroom_curry": ( + ("common.items.food.apple_mushroom_curry", 1), + [ + ("common.items.food.mushroom", 8), + ("common.items.food.coconut", 1), + ("common.items.food.apple", 4), + ("common.items.crafting_tools.mortar_pestle", 0), + ], + ), + "apples_stick": ( + ("common.items.food.apple_stick", 1), + [("common.items.crafting_ing.twigs", 2), ("common.items.food.apple", 2)], + ), + "mushroom_stick": ( + ("common.items.food.mushroom_stick", 1), + [ + ("common.items.crafting_ing.twigs", 2), + ("common.items.food.mushroom", 3), + ], + ), + "sunflower_icetea": ( + ("common.items.food.sunflower_icetea", 4), + [ + ("common.items.crafting_ing.empty_vial", 1), + ("common.items.crafting_ing.icy_fang", 1), + ("common.items.flowers.sunflower", 4), + ("common.items.crafting_ing.honey", 1), + ], + ), + "Leaves Glider": ( + ("common.items.glider.glider_leaves", 1), + [ + ("common.items.crafting_ing.twigs", 5), + ("common.items.crafting_ing.leather_scraps", 5), + ("common.items.crafting_ing.cloth_scraps", 5), + ("common.items.crafting_ing.shiny_gem", 1), + ("common.items.crafting_tools.craftsman_hammer", 0), + ("common.items.crafting_tools.sewing_set", 0), + ], + ), + "Sand Raptor Wings": ( + ("common.items.glider.glider_sandraptor", 1), + [ + ("common.items.crafting_ing.raptor_feather", 6), + ("common.items.crafting_ing.twigs", 5), + ("common.items.crafting_ing.leather_scraps", 5), + ("common.items.crafting_ing.cloth_scraps", 5), + ("common.items.crafting_ing.shiny_gem", 1), + ("common.items.crafting_tools.craftsman_hammer", 0), + ("common.items.crafting_tools.sewing_set", 0), + ], + ), + "Snow Raptor Wings": ( + ("common.items.glider.glider_snowraptor", 1), + [ + ("common.items.crafting_ing.raptor_feather", 6), + ("common.items.crafting_ing.twigs", 5), + ("common.items.crafting_ing.leather_scraps", 5), + ("common.items.crafting_ing.cloth_scraps", 5), + ("common.items.crafting_ing.icy_fang", 1), + ("common.items.crafting_ing.shiny_gem", 1), + ("common.items.crafting_tools.craftsman_hammer", 0), + ("common.items.crafting_tools.sewing_set", 0), + ], + ), + "Wood Raptor Wings": ( + ("common.items.glider.glider_woodraptor", 1), + [ + ("common.items.crafting_ing.raptor_feather", 6), + ("common.items.crafting_ing.twigs", 15), + ("common.items.crafting_ing.leather_scraps", 5), + ("common.items.crafting_ing.cloth_scraps", 5), + ("common.items.crafting_ing.shiny_gem", 1), + ("common.items.crafting_tools.craftsman_hammer", 0), + ("common.items.crafting_tools.sewing_set", 0), + ], + ), + "velorite_sceptre": ( + ("common.items.weapons.sceptre.sceptre_velorite_0", 1), + [ + ("common.items.crafting_ing.twigs", 20), + ("common.items.ore.veloritefrag", 10), + ("common.items.crafting_ing.shiny_gem", 4), + ("common.items.crafting_tools.craftsman_hammer", 0), + ], + ), + "better bow": ( + ("common.items.weapons.bow.wood_shortbow-0", 1), + [ + ("common.items.crafting_ing.leather_scraps", 8), + ("common.items.crafting_ing.twigs", 6), + ("common.items.crafting_ing.stones", 0), + ], + ), + "better sword": ( + ("common.items.weapons.sword.wood_sword", 1), + [ + ("common.items.crafting_ing.leather_scraps", 4), + ("common.items.crafting_ing.twigs", 10), + ("common.items.ore.veloritefrag", 1), + ("common.items.crafting_ing.stones", 0), + ], + ), + "adventure back": ( + ("common.items.armor.back.leather_adventurer", 1), + [("common.items.crafting_ing.leather_scraps", 4)], + ), + "adventure belt": ( + ("common.items.armor.belt.leather_adventurer", 1), + [("common.items.crafting_ing.leather_scraps", 2)], + ), + "adventure chest": ( + ("common.items.armor.chest.leather_adventurer", 1), + [("common.items.crafting_ing.leather_scraps", 12)], + ), + "adventure feet": ( + ("common.items.armor.foot.leather_adventurer", 1), + [("common.items.crafting_ing.leather_scraps", 3)], + ), + "adventure hands": ( + ("common.items.armor.hand.leather_adventurer", 1), + [("common.items.crafting_ing.leather_scraps", 4)], + ), + "adventure pants": ( + ("common.items.armor.pants.leather_adventurer", 1), + [("common.items.crafting_ing.leather_scraps", 8)], + ), + "adventure shoulder": ( + ("common.items.armor.shoulder.leather_adventurer", 1), + [("common.items.crafting_ing.leather_scraps", 12)], + ), + "red cloth": ( + ("common.items.crafting_ing.cloth_scraps_red", 1), + [ + ("common.items.crafting_ing.cloth_scraps", 1), + ("common.items.flowers.red", 1), + ("common.items.crafting_tools.mortar_pestle", 0), + ], + ), + "tiny red pouch": ( + ("common.items.armor.bag.tiny_red_pouch", 1), + [ + ("common.items.crafting_ing.cloth_scraps_red", 3), + ("common.items.crafting_tools.sewing_set", 0), + ], + ), + "tiny leather pouch": ( + ("common.items.armor.bag.tiny_leather_pouch", 1), + [ + ("common.items.crafting_ing.leather_scraps", 6), + ("common.items.crafting_tools.sewing_set", 0), + ], + ), + "knitted red pouch": ( + ("common.items.armor.bag.knitted_red_pouch", 1), + [ + ("common.items.crafting_ing.cloth_scraps_red", 3), + ("common.items.armor.bag.tiny_red_pouch", 2), + ("common.items.crafting_tools.sewing_set", 0), + ], + ), + "woven red bag": ( + ("common.items.armor.bag.woven_red_bag", 1), + [ + ("common.items.crafting_ing.cloth_scraps_red", 6), + ("common.items.armor.bag.knitted_red_pouch", 1), + ("common.items.crafting_tools.sewing_set", 0), + ], + ), + "traveler backpack": ( + ("common.items.armor.back.backpack_0", 1), + [ + ("common.items.crafting_ing.shiny_gem", 2), + ("common.items.crafting_ing.twigs", 2), + ("common.items.crafting_ing.cloth_scraps", 3), + ("common.items.crafting_ing.leather_scraps", 3), + ("common.items.armor.bag.tiny_leather_pouch", 2), + ("common.items.crafting_tools.sewing_set", 0), + ], + ), + "sturdy red backpack": ( + ("common.items.armor.bag.sturdy_red_backpack", 1), + [ + ("common.items.crafting_ing.shiny_gem", 2), + ("common.items.crafting_ing.cloth_scraps_red", 3), + ("common.items.armor.bag.woven_red_bag", 1), + ("common.items.crafting_tools.sewing_set", 0), + ], + ), + "troll hide pack": ( + ("common.items.armor.bag.troll_hide_pack", 1), + [ + ("common.items.crafting_ing.leather_troll", 10), + ("common.items.crafting_ing.leather_scraps", 10), + ("common.items.crafting_ing.shiny_gem", 1), + ("common.items.crafting_tools.sewing_set", 0), + ], + ), + "Mindflayer Spellbag": ( + ("common.items.armor.bag.mindflayer_spellbag", 1), + [ + ("common.items.crafting_ing.mindflayer_bag_damaged", 1), + ("common.items.crafting_ing.leather_scraps", 10), + ("common.items.crafting_ing.shiny_gem", 4), + ("common.items.ore.veloritefrag", 10), + ("common.items.crafting_tools.sewing_set", 0), + ], + ), } diff --git a/assets/voxygen/element/bag/bot.vox b/assets/voxygen/element/bag/bot.vox deleted file mode 100644 index 4ebd7c9df7..0000000000 Binary files a/assets/voxygen/element/bag/bot.vox and /dev/null differ diff --git a/assets/voxygen/element/bag/mid.vox b/assets/voxygen/element/bag/mid.vox deleted file mode 100644 index b3d7bb8bdc..0000000000 Binary files a/assets/voxygen/element/bag/mid.vox and /dev/null differ diff --git a/assets/voxygen/element/bag/slot.vox b/assets/voxygen/element/bag/slot.vox deleted file mode 100644 index f4644fd960..0000000000 Binary files a/assets/voxygen/element/bag/slot.vox and /dev/null differ diff --git a/assets/voxygen/element/bag/top.vox b/assets/voxygen/element/bag/top.vox deleted file mode 100644 index 4c139b3757..0000000000 Binary files a/assets/voxygen/element/bag/top.vox and /dev/null differ diff --git a/assets/voxygen/element/buttons/inv_collapse.png b/assets/voxygen/element/buttons/inv_collapse.png new file mode 100644 index 0000000000..ab2be9777c Binary files /dev/null and b/assets/voxygen/element/buttons/inv_collapse.png differ diff --git a/assets/voxygen/element/buttons/inv_collapse_hover.png b/assets/voxygen/element/buttons/inv_collapse_hover.png new file mode 100644 index 0000000000..6ba5c25ccd Binary files /dev/null and b/assets/voxygen/element/buttons/inv_collapse_hover.png differ diff --git a/assets/voxygen/element/buttons/inv_collapse_press.png b/assets/voxygen/element/buttons/inv_collapse_press.png new file mode 100644 index 0000000000..5ff731f60f Binary files /dev/null and b/assets/voxygen/element/buttons/inv_collapse_press.png differ diff --git a/assets/voxygen/element/buttons/inv_expand.png b/assets/voxygen/element/buttons/inv_expand.png new file mode 100644 index 0000000000..d4dcdf4c3d Binary files /dev/null and b/assets/voxygen/element/buttons/inv_expand.png differ diff --git a/assets/voxygen/element/buttons/inv_expand_hover.png b/assets/voxygen/element/buttons/inv_expand_hover.png new file mode 100644 index 0000000000..7fca1eca8f Binary files /dev/null and b/assets/voxygen/element/buttons/inv_expand_hover.png differ diff --git a/assets/voxygen/element/buttons/inv_expand_press.png b/assets/voxygen/element/buttons/inv_expand_press.png new file mode 100644 index 0000000000..40e3da0a5f Binary files /dev/null and b/assets/voxygen/element/buttons/inv_expand_press.png differ diff --git a/assets/voxygen/element/buttons/key_button.png b/assets/voxygen/element/buttons/key_button.png new file mode 100644 index 0000000000..88c2fe2a28 Binary files /dev/null and b/assets/voxygen/element/buttons/key_button.png differ diff --git a/assets/voxygen/element/buttons/key_button_press.png b/assets/voxygen/element/buttons/key_button_press.png new file mode 100644 index 0000000000..b9825f915b Binary files /dev/null and b/assets/voxygen/element/buttons/key_button_press.png differ diff --git a/assets/voxygen/element/frames/prompt_dialog_bot.png b/assets/voxygen/element/frames/prompt_dialog_bot.png new file mode 100644 index 0000000000..63ebcd40cf Binary files /dev/null and b/assets/voxygen/element/frames/prompt_dialog_bot.png differ diff --git a/assets/voxygen/element/frames/prompt_dialog_mid.png b/assets/voxygen/element/frames/prompt_dialog_mid.png new file mode 100644 index 0000000000..3cae737d58 Binary files /dev/null and b/assets/voxygen/element/frames/prompt_dialog_mid.png differ diff --git a/assets/voxygen/element/frames/prompt_dialog_top.png b/assets/voxygen/element/frames/prompt_dialog_top.png new file mode 100644 index 0000000000..1fea2ca924 Binary files /dev/null and b/assets/voxygen/element/frames/prompt_dialog_top.png differ diff --git a/assets/voxygen/element/icons/bag.png b/assets/voxygen/element/icons/bag.png new file mode 100644 index 0000000000..7fcc427d6d Binary files /dev/null and b/assets/voxygen/element/icons/bag.png differ diff --git a/assets/voxygen/element/icons/fire_bolt_1.png b/assets/voxygen/element/icons/fire_bolt_1.png deleted file mode 100644 index 5f91556712..0000000000 Binary files a/assets/voxygen/element/icons/fire_bolt_1.png and /dev/null differ diff --git a/assets/voxygen/element/icons/fire_spell_0.png b/assets/voxygen/element/icons/fire_spell_0.png deleted file mode 100644 index d309dc6bc4..0000000000 Binary files a/assets/voxygen/element/icons/fire_spell_0.png and /dev/null differ diff --git a/assets/voxygen/element/icons/item_bag_blue.png b/assets/voxygen/element/icons/item_bag_blue.png new file mode 100644 index 0000000000..120b47ffe2 Binary files /dev/null and b/assets/voxygen/element/icons/item_bag_blue.png differ diff --git a/assets/voxygen/element/icons/item_bag_blue_face.png b/assets/voxygen/element/icons/item_bag_blue_face.png new file mode 100644 index 0000000000..c23cc39d23 Binary files /dev/null and b/assets/voxygen/element/icons/item_bag_blue_face.png differ diff --git a/assets/voxygen/element/icons/item_bag_brown_face.png b/assets/voxygen/element/icons/item_bag_brown_face.png new file mode 100644 index 0000000000..289bd92a4f Binary files /dev/null and b/assets/voxygen/element/icons/item_bag_brown_face.png differ diff --git a/assets/voxygen/element/icons/item_bag_green_large.png b/assets/voxygen/element/icons/item_bag_green_large.png new file mode 100644 index 0000000000..4c580970b8 Binary files /dev/null and b/assets/voxygen/element/icons/item_bag_green_large.png differ diff --git a/assets/voxygen/element/icons/item_bag_green_mid.png b/assets/voxygen/element/icons/item_bag_green_mid.png new file mode 100644 index 0000000000..2fd35db721 Binary files /dev/null and b/assets/voxygen/element/icons/item_bag_green_mid.png differ diff --git a/assets/voxygen/element/icons/item_bag_large.png b/assets/voxygen/element/icons/item_bag_large.png new file mode 100644 index 0000000000..84b38c35d8 Binary files /dev/null and b/assets/voxygen/element/icons/item_bag_large.png differ diff --git a/assets/voxygen/element/icons/item_bag_leather_large.png b/assets/voxygen/element/icons/item_bag_leather_large.png new file mode 100644 index 0000000000..2ce43a5558 Binary files /dev/null and b/assets/voxygen/element/icons/item_bag_leather_large.png differ diff --git a/assets/voxygen/element/icons/item_bag_leather_small.png b/assets/voxygen/element/icons/item_bag_leather_small.png new file mode 100644 index 0000000000..a5317a744e Binary files /dev/null and b/assets/voxygen/element/icons/item_bag_leather_small.png differ diff --git a/assets/voxygen/element/icons/item_bag_med.png b/assets/voxygen/element/icons/item_bag_med.png new file mode 100644 index 0000000000..d6045563dd Binary files /dev/null and b/assets/voxygen/element/icons/item_bag_med.png differ diff --git a/assets/voxygen/element/icons/item_bag_red_face.png b/assets/voxygen/element/icons/item_bag_red_face.png new file mode 100644 index 0000000000..865bc61954 Binary files /dev/null and b/assets/voxygen/element/icons/item_bag_red_face.png differ diff --git a/assets/voxygen/element/icons/item_bag_skull.png b/assets/voxygen/element/icons/item_bag_skull.png new file mode 100644 index 0000000000..c383c20f3c Binary files /dev/null and b/assets/voxygen/element/icons/item_bag_skull.png differ diff --git a/assets/voxygen/element/icons/item_bag_small.png b/assets/voxygen/element/icons/item_bag_small.png new file mode 100644 index 0000000000..c0d570e5fd Binary files /dev/null and b/assets/voxygen/element/icons/item_bag_small.png differ diff --git a/assets/voxygen/element/icons/item_bag_tiny.png b/assets/voxygen/element/icons/item_bag_tiny.png new file mode 100755 index 0000000000..ccd022a966 Binary files /dev/null and b/assets/voxygen/element/icons/item_bag_tiny.png differ diff --git a/assets/voxygen/element/icons/item_cloth_red.png b/assets/voxygen/element/icons/item_cloth_red.png new file mode 100644 index 0000000000..54760ba5a5 Binary files /dev/null and b/assets/voxygen/element/icons/item_cloth_red.png differ diff --git a/assets/voxygen/element/icons/item_flayer_soul.png b/assets/voxygen/element/icons/item_flayer_soul.png new file mode 100644 index 0000000000..5567b650a1 Binary files /dev/null and b/assets/voxygen/element/icons/item_flayer_soul.png differ diff --git a/assets/voxygen/element/icons/item_leather_green.png b/assets/voxygen/element/icons/item_leather_green.png new file mode 100644 index 0000000000..38c8e16e20 Binary files /dev/null and b/assets/voxygen/element/icons/item_leather_green.png differ diff --git a/assets/voxygen/element/icons/skill_charge_2.png b/assets/voxygen/element/icons/skill_charge_2.png deleted file mode 100644 index a0c7c21829..0000000000 Binary files a/assets/voxygen/element/icons/skill_charge_2.png and /dev/null differ diff --git a/assets/voxygen/element/misc_bg/inv_bg.png b/assets/voxygen/element/misc_bg/inv_bg.png deleted file mode 100644 index 04f2dd25d4..0000000000 Binary files a/assets/voxygen/element/misc_bg/inv_bg.png and /dev/null differ diff --git a/assets/voxygen/element/misc_bg/inv_bg_0.png b/assets/voxygen/element/misc_bg/inv_bg_0.png index 9046f54f73..a1b04217bb 100644 Binary files a/assets/voxygen/element/misc_bg/inv_bg_0.png and b/assets/voxygen/element/misc_bg/inv_bg_0.png differ diff --git a/assets/voxygen/element/misc_bg/inv_bg_bag.png b/assets/voxygen/element/misc_bg/inv_bg_bag.png new file mode 100644 index 0000000000..f7776edade Binary files /dev/null and b/assets/voxygen/element/misc_bg/inv_bg_bag.png differ diff --git a/assets/voxygen/element/misc_bg/inv_frame_bag.png b/assets/voxygen/element/misc_bg/inv_frame_bag.png new file mode 100644 index 0000000000..09f19eeb2a Binary files /dev/null and b/assets/voxygen/element/misc_bg/inv_frame_bag.png differ diff --git a/assets/voxygen/element/misc_bg/inv_runes.png b/assets/voxygen/element/misc_bg/inv_runes.png deleted file mode 100644 index acff8fa0a8..0000000000 Binary files a/assets/voxygen/element/misc_bg/inv_runes.png and /dev/null differ diff --git a/assets/voxygen/element/misc_bg/inv_slots.png b/assets/voxygen/element/misc_bg/inv_slots.png deleted file mode 100644 index c4f89f7b80..0000000000 Binary files a/assets/voxygen/element/misc_bg/inv_slots.png and /dev/null differ diff --git a/assets/voxygen/element/slider/scrollbar_1.png b/assets/voxygen/element/slider/scrollbar_1.png new file mode 100644 index 0000000000..af1720778c Binary files /dev/null and b/assets/voxygen/element/slider/scrollbar_1.png differ diff --git a/assets/voxygen/i18n/en.ron b/assets/voxygen/i18n/en.ron new file mode 100644 index 0000000000..e69de29bb2 diff --git a/assets/voxygen/i18n/en/_manifest.ron b/assets/voxygen/i18n/en/_manifest.ron index f365afa14e..91752e67e4 100644 --- a/assets/voxygen/i18n/en/_manifest.ron +++ b/assets/voxygen/i18n/en/_manifest.ron @@ -59,6 +59,7 @@ "You can toggle showing your amount of health on the healthbar in the settings.", "In order to see your stats click the 'Stats' button in the inventory.", "Sit near a campfire (with the 'K' key) to rest - receiving a slow heal-over-time.", + "Need more bags or better armor to continue your journey? Press 'C' to open the crafting menu!", ], "npc.speech.villager_under_attack": [ "Help, I'm under attack!", diff --git a/assets/voxygen/i18n/en/hud/bag.ron b/assets/voxygen/i18n/en/hud/bag.ron index e19e086eb5..49c65489c6 100644 --- a/assets/voxygen/i18n/en/hud/bag.ron +++ b/assets/voxygen/i18n/en/hud/bag.ron @@ -24,6 +24,7 @@ "hud.bag.feet": "Feet", "hud.bag.mainhand": "Mainhand", "hud.bag.offhand": "Offhand", + "hud.bag.bag": "Bag", }, diff --git a/assets/voxygen/item_image_manifest.ron b/assets/voxygen/item_image_manifest.ron index 7249245ea2..dbc3566387 100644 --- a/assets/voxygen/item_image_manifest.ron +++ b/assets/voxygen/item_image_manifest.ron @@ -1324,6 +1324,46 @@ "voxel.armor.head.assa_mask-0", (0.0, 0.0, 0.0), (-90.0, 180.0, 0.0), 1.0, ), + // Bags + Armor(Bag("RedFace")): Png ( + "element.icons.item_bag_red_face", + ), + Armor(Bag("BrownFace")): Png ( + "element.icons.item_bag_brown_face", + ), + Armor(Bag("BlueFace")): Png ( + "element.icons.item_bag_blue_face", + ), + Armor(Bag("PurpleSkull")): Png ( + "element.icons.item_bag_skull", + ), + Armor(Bag("GreenLarge")): Png ( + "element.icons.item_bag_green_large", + ), + Armor(Bag("LeatherLarge")): Png ( + "element.icons.item_bag_leather_large", + ), + Armor(Bag("GreenMid")): Png ( + "element.icons.item_bag_green_mid", + ), + Armor(Bag("LeatherSmall")): Png ( + "element.icons.item_bag_leather_small", + ), + Armor(Bag("RedLarge")): Png ( + "element.icons.item_bag_large", + ), + Armor(Bag("RedMed")): Png ( + "element.icons.item_bag_med", + ), + Armor(Bag("RedSmall")): Png ( + "element.icons.item_bag_small", + ), + Armor(Bag("RedTiny")): Png ( + "element.icons.item_bag_tiny", + ), + Armor(Bag("BluePouch")): Png ( + "element.icons.item_bag_blue", + ), // Consumables Consumable("Apple"): Png( "element.icons.item_apple", @@ -1419,6 +1459,10 @@ "voxel.sprite.flowers.sunflower_1", (-2.0, -0.5, -1.0), (-60.0, 40.0, 20.0), 1.1, ), + Ingredient("FlowerRed"): VoxTrans( + "voxel.sprite.flowers.flower_red-4", + (0.0, 0.5, 0.0), (-70.0, 10.0, 0.0), 0.8, + ), Ingredient("Sunflower"): VoxTrans( "voxel.sprite.flowers.sunflower_1", (0.0, -1.0, 0.0), (-50.0, 40.0, 20.0), 0.8, @@ -1434,6 +1478,9 @@ Ingredient("IcyShard"): Png( "element.icons.item_ice_shard", ), + Ingredient("FlayerBagDamaged"): Png( + "element.icons.item_flayer_soul", + ), Ingredient("RaptorFeather"): Png( "element.icons.item_raptor_feather", ), @@ -1447,9 +1494,15 @@ Ingredient("LeatherScraps"): Png( "element.icons.item_leather0", ), + Ingredient("TrollLeather"): Png( + "element.icons.item_leather_green", + ), Ingredient("ClothScraps"): Png( "element.icons.item_cloth0", ), + Ingredient("ClothScrapsRed"): Png( + "element.icons.item_cloth_red", + ), Ingredient("ShinyGem"): Png( "element.icons.gem", ), @@ -1479,15 +1532,15 @@ ), Glider("SandRaptor"): VoxTrans( "voxel.glider.glider_sandraptor", - (6.0, 0.0, 0.0), (-50.0, 30.0, 20.0), 1.1, + (6.0, 0.0, 0.0), (-50.0, 30.0, 20.0), 1.1, ), Glider("SnowRaptor"): VoxTrans( "voxel.glider.glider_snowraptor", - (6.0, 0.0, 0.0), (-50.0, 30.0, 20.0), 1.1, + (6.0, 0.0, 0.0), (-50.0, 30.0, 20.0), 1.1, ), Glider("WoodRaptor"): VoxTrans( "voxel.glider.glider_woodraptor", - (6.0, 0.0, 0.0), (-50.0, 30.0, 20.0), 1.1, + (6.0, 0.0, 0.0), (-50.0, 30.0, 20.0), 1.1, ), Glider("Purple0"): VoxTrans( "voxel.glider.glider_cultists", diff --git a/client/src/lib.rs b/client/src/lib.rs index 3f611de31d..34580ab608 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -933,8 +933,6 @@ impl Client { pub fn inventories(&self) -> ReadStorage { self.state.read_storage() } - pub fn loadouts(&self) -> ReadStorage { self.state.read_storage() } - /// Send a chat message to the server. pub fn send_chat(&mut self, message: String) { match validate_chat_msg(&message) { @@ -1455,11 +1453,10 @@ impl Client { self.presence = None; self.clean_state(); }, - ServerGeneral::InventoryUpdate(mut inventory, event) => { + ServerGeneral::InventoryUpdate(inventory, event) => { match event { InventoryUpdateEvent::CollectFailed => {}, _ => { - inventory.recount_items(); // Push the updated inventory component to the client self.state.write_component(self.entity, inventory); }, diff --git a/common/net/src/msg/ecs_packet.rs b/common/net/src/msg/ecs_packet.rs index 7b28f98496..25860655e7 100644 --- a/common/net/src/msg/ecs_packet.rs +++ b/common/net/src/msg/ecs_packet.rs @@ -19,6 +19,7 @@ sum_type! { Energy(comp::Energy), Health(comp::Health), LightEmitter(comp::LightEmitter), + Inventory(comp::Inventory), Item(comp::Item), Scale(comp::Scale), Group(comp::Group), @@ -28,7 +29,6 @@ sum_type! { Collider(comp::Collider), Gravity(comp::Gravity), Sticky(comp::Sticky), - Loadout(comp::Loadout), CharacterState(comp::CharacterState), Pos(comp::Pos), Vel(comp::Vel), @@ -51,6 +51,7 @@ sum_type! { Energy(PhantomData), Health(PhantomData), LightEmitter(PhantomData), + Inventory(PhantomData), Item(PhantomData), Scale(PhantomData), Group(PhantomData), @@ -60,7 +61,6 @@ sum_type! { Collider(PhantomData), Gravity(PhantomData), Sticky(PhantomData), - Loadout(PhantomData), CharacterState(PhantomData), Pos(PhantomData), Vel(PhantomData), @@ -83,6 +83,7 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPacket::Energy(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Health(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::LightEmitter(comp) => sync::handle_insert(comp, entity, world), + EcsCompPacket::Inventory(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Item(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Scale(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Group(comp) => sync::handle_insert(comp, entity, world), @@ -92,7 +93,6 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPacket::Collider(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Gravity(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Sticky(comp) => sync::handle_insert(comp, entity, world), - EcsCompPacket::Loadout(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::CharacterState(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Pos(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Vel(comp) => sync::handle_insert(comp, entity, world), @@ -113,6 +113,7 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPacket::Energy(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Health(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::LightEmitter(comp) => sync::handle_modify(comp, entity, world), + EcsCompPacket::Inventory(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Item(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Scale(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Group(comp) => sync::handle_modify(comp, entity, world), @@ -122,7 +123,6 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPacket::Collider(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Gravity(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Sticky(comp) => sync::handle_modify(comp, entity, world), - EcsCompPacket::Loadout(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::CharacterState(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Pos(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Vel(comp) => sync::handle_modify(comp, entity, world), @@ -145,6 +145,7 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPhantom::LightEmitter(_) => { sync::handle_remove::(entity, world) }, + EcsCompPhantom::Inventory(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Item(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Scale(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Group(_) => sync::handle_remove::(entity, world), @@ -154,7 +155,6 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPhantom::Collider(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Gravity(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Sticky(_) => sync::handle_remove::(entity, world), - EcsCompPhantom::Loadout(_) => sync::handle_remove::(entity, world), EcsCompPhantom::CharacterState(_) => { sync::handle_remove::(entity, world) }, diff --git a/common/src/bin/csv_export/main.rs b/common/src/bin/csv_export/main.rs index 5e408e78a9..cdfa9bc220 100644 --- a/common/src/bin/csv_export/main.rs +++ b/common/src/bin/csv_export/main.rs @@ -102,6 +102,7 @@ fn get_armor_kind(kind: &ArmorKind) -> String { ArmorKind::Neck(_) => "Neck".to_string(), ArmorKind::Head(_) => "Head".to_string(), ArmorKind::Tabard(_) => "Tabard".to_string(), + ArmorKind::Bag(_) => "Bag".to_string(), } } @@ -118,6 +119,7 @@ fn get_armor_kind_kind(kind: &ArmorKind) -> String { ArmorKind::Neck(x) => x.clone(), ArmorKind::Head(x) => x.clone(), ArmorKind::Tabard(x) => x.clone(), + ArmorKind::Bag(x) => x.clone(), } } diff --git a/common/src/character.rs b/common/src/character.rs index dbf5f9713c..fc596b432d 100644 --- a/common/src/character.rs +++ b/common/src/character.rs @@ -1,6 +1,6 @@ //! Structs representing a playable Character -use crate::comp; +use crate::{comp, comp::inventory::Inventory}; use serde::{Deserialize, Serialize}; /// The limit on how many characters that a player can have @@ -21,5 +21,5 @@ pub struct CharacterItem { pub character: Character, pub body: comp::Body, pub level: usize, - pub loadout: comp::Loadout, + pub inventory: Inventory, } diff --git a/common/src/cmd.rs b/common/src/cmd.rs index c2a8fc63d6..b3c3c3e4d4 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -42,6 +42,7 @@ pub enum ChatCommand { Campfire, Debug, DebugColumn, + DropAll, Dummy, Explosion, Faction, @@ -94,6 +95,7 @@ pub static CHAT_COMMANDS: &[ChatCommand] = &[ ChatCommand::Campfire, ChatCommand::Debug, ChatCommand::DebugColumn, + ChatCommand::DropAll, ChatCommand::Dummy, ChatCommand::Explosion, ChatCommand::Faction, @@ -230,6 +232,7 @@ impl ChatCommand { "Prints some debug information about a column", NoAdmin, ), + ChatCommand::DropAll => cmd(vec![], "Drops all your items on the ground", Admin), ChatCommand::Dummy => cmd(vec![], "Spawns a training dummy", Admin), ChatCommand::Explosion => cmd( vec![Float("radius", 5.0, Required)], @@ -445,6 +448,7 @@ impl ChatCommand { ChatCommand::Campfire => "campfire", ChatCommand::Debug => "debug", ChatCommand::DebugColumn => "debug_column", + ChatCommand::DropAll => "dropall", ChatCommand::Dummy => "dummy", ChatCommand::Explosion => "explosion", ChatCommand::Faction => "faction", @@ -533,12 +537,11 @@ impl Display for ChatCommand { impl FromStr for ChatCommand { type Err = (); - #[allow(clippy::manual_strip)] fn from_str(keyword: &str) -> Result { - let kwd = if keyword.starts_with('/') { - &keyword[1..] + let kwd = if let Some(stripped) = keyword.strip_prefix('/') { + stripped } else { - &keyword[..] + &keyword }; if keyword.len() == 1 { if let Some(c) = keyword diff --git a/common/src/combat.rs b/common/src/combat.rs index a414735cd6..7d3cc401c1 100644 --- a/common/src/combat.rs +++ b/common/src/combat.rs @@ -1,5 +1,8 @@ use crate::{ - comp::{HealthChange, HealthSource, Loadout}, + comp::{ + inventory::item::{armor::Protection, ItemKind}, + HealthChange, HealthSource, Inventory, + }, uid::Uid, util::Dir, }; @@ -31,8 +34,32 @@ pub struct Damage { } impl Damage { - pub fn modify_damage(self, loadout: Option<&Loadout>, uid: Option) -> HealthChange { + /// Returns the total damage reduction provided by all equipped items + pub fn compute_damage_reduction(inventory: &Inventory) -> f32 { + let protection = inventory + .equipped_items() + .filter_map(|item| { + if let ItemKind::Armor(armor) = &item.kind() { + Some(armor.get_protection()) + } else { + None + } + }) + .map(|protection| match protection { + Protection::Normal(protection) => Some(protection), + Protection::Invincible => None, + }) + .sum::>(); + match protection { + Some(dr) => dr / (60.0 + dr.abs()), + None => 1.0, + } + } + + pub fn modify_damage(self, inventory: Option<&Inventory>, uid: Option) -> HealthChange { let mut damage = self.value; + let damage_reduction = inventory.map_or(0.0, |inv| Damage::compute_damage_reduction(inv)); + match self.source { DamageSource::Melee => { // Critical hit @@ -41,7 +68,6 @@ impl Damage { critdamage = damage * 0.3; } // Armor - let damage_reduction = loadout.map_or(0.0, |l| l.get_damage_reduction()); damage *= 1.0 - damage_reduction; // Critical damage applies after armor for melee @@ -63,7 +89,6 @@ impl Damage { damage *= 1.2; } // Armor - let damage_reduction = loadout.map_or(0.0, |l| l.get_damage_reduction()); damage *= 1.0 - damage_reduction; HealthChange { @@ -76,7 +101,6 @@ impl Damage { }, DamageSource::Explosion => { // Armor - let damage_reduction = loadout.map_or(0.0, |l| l.get_damage_reduction()); damage *= 1.0 - damage_reduction; HealthChange { @@ -89,7 +113,6 @@ impl Damage { }, DamageSource::Shockwave => { // Armor - let damage_reduction = loadout.map_or(0.0, |l| l.get_damage_reduction()); damage *= 1.0 - damage_reduction; HealthChange { @@ -102,7 +125,6 @@ impl Damage { }, DamageSource::Energy => { // Armor - let damage_reduction = loadout.map_or(0.0, |l| l.get_damage_reduction()); damage *= 1.0 - damage_reduction; HealthChange { @@ -119,7 +141,6 @@ impl Damage { }, DamageSource::Falling => { // Armor - let damage_reduction = loadout.map_or(0.0, |l| l.get_damage_reduction()); if (damage_reduction - 1.0).abs() < f32::EPSILON { damage = 0.0; } diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index 0afd6067c0..f6584a385f 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -1,9 +1,8 @@ use crate::{ assets::{self, Asset}, comp::{ - item::{armor::Protection, tool::AbilityMap, Item, ItemKind}, - projectile::ProjectileConstructor, - Body, CharacterState, EnergySource, Gravity, LightEmitter, StateUpdate, + projectile::ProjectileConstructor, Body, CharacterState, EnergySource, Gravity, + LightEmitter, StateUpdate, }, states::{ behavior::JoinData, @@ -12,10 +11,7 @@ use crate::{ }, Knockback, }; -use arraygen::Arraygen; use serde::{Deserialize, Serialize}; -use specs::{Component, DerefFlaggedStorage}; -use specs_idvs::IdvStorage; use std::time::Duration; use vek::Vec3; @@ -304,7 +300,7 @@ impl CharacterAbility { } } - fn default_roll() -> CharacterAbility { + pub fn default_roll() -> CharacterAbility { CharacterAbility::Roll { energy_cost: 100, buildup_duration: 100, @@ -499,93 +495,6 @@ impl CharacterAbility { } } -#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] -pub struct ItemConfig { - pub item: Item, - pub ability1: Option, - pub ability2: Option, - pub ability3: Option, - pub block_ability: Option, - pub dodge_ability: Option, -} - -impl From<(Item, &AbilityMap)> for ItemConfig { - fn from((item, map): (Item, &AbilityMap)) -> Self { - if let ItemKind::Tool(tool) = &item.kind() { - let abilities = tool.get_abilities(map); - - return ItemConfig { - item, - ability1: Some(abilities.primary), - ability2: Some(abilities.secondary), - ability3: abilities.skills.get(0).cloned(), - block_ability: None, - dodge_ability: Some(CharacterAbility::default_roll()), - }; - } - - unimplemented!("ItemConfig is currently only supported for Tools") - } -} - -#[derive(Arraygen, Clone, PartialEq, Default, Debug, Serialize, Deserialize)] -#[gen_array(pub fn get_armor: &Option)] -pub struct Loadout { - pub active_item: Option, - pub second_item: Option, - - pub lantern: Option, - pub glider: Option, - - #[in_array(get_armor)] - pub shoulder: Option, - #[in_array(get_armor)] - pub chest: Option, - #[in_array(get_armor)] - pub belt: Option, - #[in_array(get_armor)] - pub hand: Option, - #[in_array(get_armor)] - pub pants: Option, - #[in_array(get_armor)] - pub foot: Option, - #[in_array(get_armor)] - pub back: Option, - #[in_array(get_armor)] - pub ring: Option, - #[in_array(get_armor)] - pub neck: Option, - #[in_array(get_armor)] - pub head: Option, - #[in_array(get_armor)] - pub tabard: Option, -} - -impl Loadout { - pub fn get_damage_reduction(&self) -> f32 { - let protection = self - .get_armor() - .iter() - .flat_map(|armor| armor.as_ref()) - .filter_map(|item| { - if let ItemKind::Armor(armor) = &item.kind() { - Some(armor.get_protection()) - } else { - None - } - }) - .map(|protection| match protection { - Protection::Normal(protection) => Some(protection), - Protection::Invincible => None, - }) - .sum::>(); - match protection { - Some(dr) => dr / (60.0 + dr.abs()), - None => 1.0, - } - } -} - impl From<(&CharacterAbility, AbilityKey)> for CharacterState { fn from((ability, key): (&CharacterAbility, AbilityKey)) -> Self { match ability { @@ -975,7 +884,3 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState { } } } - -impl Component for Loadout { - type Storage = DerefFlaggedStorage>; -} diff --git a/common/src/comp/inventory/item/armor.rs b/common/src/comp/inventory/item/armor.rs index a3beb9e5af..aba06d5873 100644 --- a/common/src/comp/inventory/item/armor.rs +++ b/common/src/comp/inventory/item/armor.rs @@ -13,6 +13,7 @@ pub enum ArmorKind { Neck(String), Head(String), Tabard(String), + Bag(String), } impl Armor { @@ -43,4 +44,12 @@ pub struct Armor { impl Armor { pub fn get_protection(&self) -> Protection { self.stats.protection } + + #[cfg(test)] + pub fn test_armor(kind: ArmorKind, protection: Protection) -> Armor { + Armor { + kind, + stats: Stats { protection }, + } + } } diff --git a/common/src/comp/inventory/item/mod.rs b/common/src/comp/inventory/item/mod.rs index 60e87e784f..063ee47561 100644 --- a/common/src/comp/inventory/item/mod.rs +++ b/common/src/comp/inventory/item/mod.rs @@ -6,10 +6,15 @@ pub use tool::{AbilitySet, Hands, Tool, ToolKind, UniqueKind}; use crate::{ assets::{self, AssetExt, Error}, + comp::{ + inventory::{item::tool::AbilityMap, InvSlot}, + Body, CharacterAbility, + }, effect::Effect, lottery::Lottery, terrain::{Block, SpriteKind}, }; +use core::mem; use crossbeam_utils::atomic::AtomicCell; use rand::prelude::*; use serde::{Deserialize, Serialize}; @@ -94,6 +99,15 @@ pub enum ItemKind { }, } +impl ItemKind { + pub fn is_equippable(&self) -> bool { + matches!( + self, + ItemKind::Tool(_) | ItemKind::Armor { .. } | ItemKind::Glider(_) | ItemKind::Lantern(_) + ) + } +} + pub type ItemId = AtomicCell>; /* /// The only way to access an item id outside this module is to mutably, atomically update it using @@ -101,11 +115,7 @@ pub type ItemId = AtomicCell>; /// only if it's not already set. pub struct CreateDatabaseItemId { item_id: Arc, -} - -pub struct CreateDatabaseItemId { - item_id: Arc, -} */ +}*/ #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Item { @@ -124,22 +134,54 @@ pub struct Item { /// amount is hidden because it needs to maintain the invariant that only /// stackable items can have > 1 amounts. amount: NonZeroU32, + /// The slots for items that this item has + slots: Vec, } #[derive(Debug, Serialize, Deserialize)] pub struct ItemDef { #[serde(default)] item_definition_id: String, + pub item_config: Option, pub name: String, pub description: String, pub kind: ItemKind, pub quality: Quality, + #[serde(default)] + pub slots: u16, } impl PartialEq for ItemDef { fn eq(&self, other: &Self) -> bool { self.item_definition_id == other.item_definition_id } } +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct ItemConfig { + pub ability1: Option, + pub ability2: Option, + pub ability3: Option, + pub block_ability: Option, + pub dodge_ability: Option, +} + +impl From<(&ItemKind, &AbilityMap)> for ItemConfig { + fn from((item_kind, map): (&ItemKind, &AbilityMap)) -> Self { + if let ItemKind::Tool(tool) = item_kind { + let abilities = tool.get_abilities(map); + + return ItemConfig { + ability1: Some(abilities.primary), + ability2: Some(abilities.secondary), + ability3: abilities.skills.get(0).cloned(), + block_ability: None, + dodge_ability: Some(CharacterAbility::default_roll()), + }; + } + + unimplemented!("ItemConfig is currently only supported for Tools") + } +} + impl ItemDef { pub fn is_stackable(&self) -> bool { matches!( @@ -150,6 +192,25 @@ impl ItemDef { | ItemKind::Utility { .. } ) } + + #[cfg(test)] + pub fn new_test( + item_definition_id: String, + item_config: Option, + kind: ItemKind, + quality: Quality, + slots: u16, + ) -> Self { + Self { + item_definition_id, + item_config, + name: "test item name".to_owned(), + description: "test item description".to_owned(), + kind, + quality, + slots, + } + } } impl PartialEq for Item { @@ -170,8 +231,19 @@ impl assets::Compound for ItemDef { description, kind, quality, + slots, } = raw; + let item_config = if let ItemKind::Tool(_) = kind { + let ability_map_handle = + cache.load::("common.abilities.weapon_ability_manifest")?; + let ability_map = &*ability_map_handle.read(); + + Some(ItemConfig::from((&kind, ability_map))) + } else { + None + }; + // Some commands like /give_item provide the asset specifier separated with \ // instead of . // @@ -180,10 +252,12 @@ impl assets::Compound for ItemDef { Ok(ItemDef { item_definition_id, + item_config, name, description, kind, quality, + slots, }) } } @@ -195,6 +269,8 @@ struct RawItemDef { description: String, kind: ItemKind, quality: Quality, + #[serde(default)] + slots: u16, } impl assets::Asset for RawItemDef { @@ -229,11 +305,12 @@ impl Item { // loadout when no weapon is present pub fn empty() -> Self { Item::new_from_asset_expect("common.items.weapons.empty.empty") } - pub fn new(inner_item: Arc) -> Self { + pub fn new_from_item_def(inner_item: Arc) -> Self { Item { item_id: Arc::new(AtomicCell::new(None)), - item_def: inner_item, amount: NonZeroU32::new(1).unwrap(), + slots: vec![None; inner_item.slots as usize], + item_def: inner_item, } } @@ -241,7 +318,7 @@ impl Item { /// Panics if the asset does not exist. pub fn new_from_asset_expect(asset_specifier: &str) -> Self { let inner_item = Arc::::load_expect_cloned(asset_specifier); - Item::new(inner_item) + Item::new_from_item_def(inner_item) } /// Creates a Vec containing one of each item that matches the provided @@ -254,11 +331,43 @@ impl Item { /// it exists pub fn new_from_asset(asset: &str) -> Result { let inner_item = Arc::::load_cloned(asset)?; - Ok(Item::new(inner_item)) + Ok(Item::new_from_item_def(inner_item)) + } + + pub fn new_default_for_body(body: &Body) -> Self { + let mut item = Item::new_from_asset_expect("common.items.weapons.empty.empty"); + + let empty_def = &*item.item_def; + item.item_def = Arc::new(ItemDef { + slots: empty_def.slots, + name: empty_def.name.clone(), + kind: empty_def.kind.clone(), + description: empty_def.description.clone(), + item_definition_id: empty_def.item_definition_id.clone(), + quality: empty_def.quality, + item_config: Some(ItemConfig { + ability1: Some(CharacterAbility::BasicMelee { + energy_cost: 10, + buildup_duration: 500, + swing_duration: 100, + recover_duration: 100, + base_damage: body.base_dmg(), + knockback: 0.0, + range: body.base_range(), + max_angle: 20.0, + }), + ability2: None, + ability3: None, + block_ability: None, + dodge_ability: None, + }), + }); + + item } /// Duplicates an item, creating an exact copy but with a new item ID - pub fn duplicate(&self) -> Self { Item::new(Arc::clone(&self.item_def)) } + pub fn duplicate(&self) -> Self { Item::new_from_item_def(Arc::clone(&self.item_def)) } /// FIXME: HACK: In order to set the entity ID asynchronously, we currently /// start it at None, and then atomically set it when it's saved for the @@ -320,6 +429,11 @@ impl Item { } } + /// Returns an iterator that drains items contained within the item's slots + pub fn drain(&mut self) -> impl Iterator + '_ { + self.slots.iter_mut().filter_map(|x| mem::take(x)) + } + pub fn item_definition_id(&self) -> &str { &self.item_def.item_definition_id } pub fn is_same_item_def(&self, item_def: &ItemDef) -> bool { @@ -338,6 +452,26 @@ impl Item { pub fn quality(&self) -> Quality { self.item_def.quality } + pub fn slots(&self) -> &[InvSlot] { &self.slots } + + pub fn slots_mut(&mut self) -> &mut [InvSlot] { &mut self.slots } + + pub fn item_config_expect(&self) -> &ItemConfig { + &self + .item_def + .item_config + .as_ref() + .expect("Item was expected to have an ItemConfig") + } + + pub fn free_slots(&self) -> usize { self.slots.iter().filter(|x| x.is_none()).count() } + + pub fn populated_slots(&self) -> usize { self.slots().len().saturating_sub(self.free_slots()) } + + pub fn slot(&self, slot: usize) -> Option<&InvSlot> { self.slots.get(slot) } + + pub fn slot_mut(&mut self, slot: usize) -> Option<&mut InvSlot> { self.slots.get_mut(slot) } + pub fn try_reclaim_from_block(block: Block) -> Option { let chosen; let mut rng = rand::thread_rng(); @@ -416,6 +550,7 @@ pub trait ItemDesc { fn name(&self) -> &str; fn kind(&self) -> &ItemKind; fn quality(&self) -> &Quality; + fn num_slots(&self) -> u16; fn item_definition_id(&self) -> &str; } @@ -428,6 +563,8 @@ impl ItemDesc for Item { fn quality(&self) -> &Quality { &self.item_def.quality } + fn num_slots(&self) -> u16 { self.item_def.slots } + fn item_definition_id(&self) -> &str { &self.item_def.item_definition_id } } @@ -440,6 +577,8 @@ impl ItemDesc for ItemDef { fn quality(&self) -> &Quality { &self.quality } + fn num_slots(&self) -> u16 { self.slots } + fn item_definition_id(&self) -> &str { &self.item_definition_id } } diff --git a/common/src/comp/inventory/loadout.rs b/common/src/comp/inventory/loadout.rs new file mode 100644 index 0000000000..4de423af8e --- /dev/null +++ b/common/src/comp/inventory/loadout.rs @@ -0,0 +1,363 @@ +use crate::comp::{ + inventory::{ + item::ItemKind, + slot::{ArmorSlot, EquipSlot}, + InvSlot, + }, + Item, +}; +use serde::{Deserialize, Serialize}; +use std::ops::Range; +use tracing::warn; + +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct Loadout { + slots: Vec, +} + +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct LoadoutSlot { + /// The EquipSlot that this slot represents + pub(super) equip_slot: EquipSlot, + /// The contents of the slot + slot: InvSlot, + /// The unique string that represents this loadout slot in the database (not + /// synced to clients) + #[serde(skip)] + persistence_key: String, +} + +impl LoadoutSlot { + fn new(equip_slot: EquipSlot, persistence_key: String) -> LoadoutSlot { + LoadoutSlot { + equip_slot, + slot: None, + persistence_key, + } + } +} + +pub(super) struct LoadoutSlotId { + // The index of the loadout item that provides this inventory slot. + pub loadout_idx: usize, + // The index of the slot within its container + pub slot_idx: usize, +} + +pub enum LoadoutError { + InvalidPersistenceKey, +} + +impl Loadout { + pub(super) fn new_empty() -> Self { + Self { + slots: vec![ + (EquipSlot::Lantern, "lantern".to_string()), + (EquipSlot::Glider, "glider".to_string()), + ( + EquipSlot::Armor(ArmorSlot::Shoulders), + "shoulder".to_string(), + ), + (EquipSlot::Armor(ArmorSlot::Chest), "chest".to_string()), + (EquipSlot::Armor(ArmorSlot::Belt), "belt".to_string()), + (EquipSlot::Armor(ArmorSlot::Hands), "hand".to_string()), + (EquipSlot::Armor(ArmorSlot::Legs), "pants".to_string()), + (EquipSlot::Armor(ArmorSlot::Feet), "foot".to_string()), + (EquipSlot::Armor(ArmorSlot::Back), "back".to_string()), + (EquipSlot::Armor(ArmorSlot::Ring1), "ring1".to_string()), + (EquipSlot::Armor(ArmorSlot::Ring2), "ring2".to_string()), + (EquipSlot::Armor(ArmorSlot::Neck), "neck".to_string()), + (EquipSlot::Armor(ArmorSlot::Head), "head".to_string()), + (EquipSlot::Armor(ArmorSlot::Tabard), "tabard".to_string()), + (EquipSlot::Armor(ArmorSlot::Bag1), "bag1".to_string()), + (EquipSlot::Armor(ArmorSlot::Bag2), "bag2".to_string()), + (EquipSlot::Armor(ArmorSlot::Bag3), "bag3".to_string()), + (EquipSlot::Armor(ArmorSlot::Bag4), "bag4".to_string()), + (EquipSlot::Mainhand, "active_item".to_string()), + (EquipSlot::Offhand, "second_item".to_string()), + ] + .into_iter() + .map(|(equip_slot, persistence_key)| LoadoutSlot::new(equip_slot, persistence_key)) + .collect(), + } + } + + /// Replaces the item in the Loadout slot that corresponds to the given + /// EquipSlot and returns the previous item if any + pub(super) fn swap(&mut self, equip_slot: EquipSlot, item: Option) -> Option { + self.slots + .iter_mut() + .find(|x| x.equip_slot == equip_slot) + .and_then(|x| core::mem::replace(&mut x.slot, item)) + } + + /// Returns a reference to the item (if any) equipped in the given EquipSlot + pub(super) fn equipped(&self, equip_slot: EquipSlot) -> Option<&Item> { + self.slot(equip_slot).and_then(|x| x.slot.as_ref()) + } + + fn slot(&self, equip_slot: EquipSlot) -> Option<&LoadoutSlot> { + self.slots + .iter() + .find(|loadout_slot| loadout_slot.equip_slot == equip_slot) + } + + pub(super) fn loadout_idx_for_equip_slot(&self, equip_slot: EquipSlot) -> Option { + self.slots + .iter() + .position(|loadout_slot| loadout_slot.equip_slot == equip_slot) + } + + /// Returns all loadout items paired with their persistence key + pub(super) fn items_with_persistence_key(&self) -> impl Iterator)> { + self.slots + .iter() + .map(|x| (x.persistence_key.as_str(), x.slot.as_ref())) + } + + /// Sets a loadout item in the correct slot using its persistence key. Any + /// item that already exists in the slot is lost. + pub fn set_item_at_slot_using_persistence_key( + &mut self, + persistence_key: &str, + item: Item, + ) -> Result<(), LoadoutError> { + if let Some(slot) = self + .slots + .iter_mut() + .find(|x| x.persistence_key == persistence_key) + { + slot.slot = Some(item); + Ok(()) + } else { + Err(LoadoutError::InvalidPersistenceKey) + } + } + + /// Swaps the contents of two loadout slots + pub(super) fn swap_slots(&mut self, equip_slot_a: EquipSlot, equip_slot_b: EquipSlot) { + if self.slot(equip_slot_b).is_none() || self.slot(equip_slot_b).is_none() { + // Currently all loadouts contain slots for all EquipSlots so this can never + // happen, but if loadouts with alternate slot combinations are + // introduced then it could. + warn!("Cannot swap slots for non-existent equip slot"); + return; + } + + let item_a = self.swap(equip_slot_a, None); + let item_b = self.swap(equip_slot_b, None); + + // Check if items can go in the other slots + if item_a + .as_ref() + .map_or(true, |i| equip_slot_b.can_hold(&i.kind())) + && item_b + .as_ref() + .map_or(true, |i| equip_slot_a.can_hold(&i.kind())) + { + // Swap + self.swap(equip_slot_b, item_a).unwrap_none(); + self.swap(equip_slot_a, item_b).unwrap_none(); + } else { + // Otherwise put the items back + self.swap(equip_slot_a, item_a).unwrap_none(); + self.swap(equip_slot_b, item_b).unwrap_none(); + } + } + + /// Gets a slot that an item of a particular `ItemKind` can be equipped + /// into. The first empty slot compatible with the item will be + /// returned, or if there are no free slots then the first occupied slot + /// will be returned. The bool part of the tuple indicates whether an item + /// is already equipped in the slot. + pub(super) fn get_slot_to_equip_into(&self, item_kind: &ItemKind) -> Option { + let mut suitable_slots = self + .slots + .iter() + .filter(|s| s.equip_slot.can_hold(item_kind)); + + let first = suitable_slots.next(); + + first + .into_iter() + .chain(suitable_slots) + .find(|loadout_slot| loadout_slot.slot.is_none()) + .map(|x| x.equip_slot) + .or_else(|| first.map(|x| x.equip_slot)) + } + + /// Returns the `InvSlot` for a given `LoadoutSlotId` + pub(super) fn inv_slot(&self, loadout_slot_id: LoadoutSlotId) -> Option<&InvSlot> { + self.slots + .get(loadout_slot_id.loadout_idx) + .and_then(|loadout_slot| loadout_slot.slot.as_ref()) + .and_then(|item| item.slot(loadout_slot_id.slot_idx)) + } + + /// Returns the `InvSlot` for a given `LoadoutSlotId` + pub(super) fn inv_slot_mut(&mut self, loadout_slot_id: LoadoutSlotId) -> Option<&mut InvSlot> { + self.slots + .get_mut(loadout_slot_id.loadout_idx) + .and_then(|loadout_slot| loadout_slot.slot.as_mut()) + .and_then(|item| item.slot_mut(loadout_slot_id.slot_idx)) + } + + /// Returns all inventory slots provided by equipped loadout items, along + /// with their `LoadoutSlotId` + pub(super) fn inv_slots_with_id(&self) -> impl Iterator { + self.slots + .iter() + .enumerate() + .filter_map(|(i, loadout_slot)| { + loadout_slot.slot.as_ref().map(|item| (i, item.slots())) + }) + .flat_map(|(loadout_slot_index, loadout_slots)| { + loadout_slots + .iter() + .enumerate() + .map(move |(item_slot_index, inv_slot)| { + ( + LoadoutSlotId { + loadout_idx: loadout_slot_index, + slot_idx: item_slot_index, + }, + inv_slot, + ) + }) + }) + } + + /// Returns all inventory slots provided by equipped loadout items + pub(super) fn inv_slots_mut(&mut self) -> impl Iterator { + self.slots.iter_mut() + .filter_map(|x| x.slot.as_mut().map(|item| item.slots_mut())) // Discard loadout items that have no slots of their own + .flat_map(|loadout_slots| loadout_slots.iter_mut()) //Collapse iter of Vec to iter of InvSlot + } + + /// Gets the range of loadout-provided inventory slot indexes that are + /// provided by the item in the given `EquipSlot` + pub(super) fn slot_range_for_equip_slot(&self, equip_slot: EquipSlot) -> Option> { + self.slots + .iter() + .map(|loadout_slot| { + ( + loadout_slot.equip_slot, + loadout_slot + .slot + .as_ref() + .map_or(0, |item| item.slots().len()), + ) + }) + .scan(0, |acc_len, (equip_slot, len)| { + let res = Some((equip_slot, len, *acc_len)); + *acc_len += len; + res + }) + .find(|(e, len, _)| *e == equip_slot && len > &0) + .map(|(_, slot_len, start)| start..start + slot_len) + } + + /// Attempts to equip the item into a compatible, unpopulated loadout slot. + /// If no slot is available the item is returned. + #[must_use = "Returned item will be lost if not used"] + pub(super) fn try_equip(&mut self, item: Item) -> Result<(), Item> { + if let Some(loadout_slot) = self + .slots + .iter_mut() + .find(|s| s.slot.is_none() && s.equip_slot.can_hold(item.kind())) + { + loadout_slot.slot = Some(item); + Ok(()) + } else { + Err(item) + } + } + + pub(super) fn items(&self) -> impl Iterator { + self.slots.iter().filter_map(|x| x.slot.as_ref()) + } +} + +#[cfg(test)] +mod tests { + use crate::comp::{ + inventory::{ + item::{ + armor::{Armor, ArmorKind, Protection}, + ItemKind, + }, + loadout::Loadout, + slot::{ArmorSlot, EquipSlot}, + test_helpers::get_test_bag, + }, + Item, + }; + + #[test] + fn test_slot_range_for_equip_slot() { + let mut loadout = Loadout::new_empty(); + + let bag1_slot = EquipSlot::Armor(ArmorSlot::Bag1); + let bag = get_test_bag(18); + loadout.swap(bag1_slot, Some(bag)); + + let result = loadout.slot_range_for_equip_slot(bag1_slot).unwrap(); + + assert_eq!(0..18, result); + } + + #[test] + fn test_slot_range_for_equip_slot_no_item() { + let loadout = Loadout::new_empty(); + let result = loadout.slot_range_for_equip_slot(EquipSlot::Armor(ArmorSlot::Bag1)); + + assert_eq!(None, result); + } + + #[test] + fn test_slot_range_for_equip_slot_item_without_slots() { + let mut loadout = Loadout::new_empty(); + + let feet_slot = EquipSlot::Armor(ArmorSlot::Feet); + let boots = Item::new_from_asset_expect("common.items.testing.test_boots"); + loadout.swap(feet_slot, Some(boots)); + let result = loadout.slot_range_for_equip_slot(feet_slot); + + assert_eq!(None, result); + } + + #[test] + fn test_get_slot_to_equip_into_second_bag_slot_free() { + let mut loadout = Loadout::new_empty(); + + loadout.swap(EquipSlot::Armor(ArmorSlot::Bag1), Some(get_test_bag(1))); + + let result = loadout + .get_slot_to_equip_into(&ItemKind::Armor(Armor::test_armor( + ArmorKind::Bag("test".to_string()), + Protection::Normal(0.0), + ))) + .unwrap(); + + assert_eq!(EquipSlot::Armor(ArmorSlot::Bag2), result); + } + + #[test] + fn test_get_slot_to_equip_into_no_bag_slots_free() { + let mut loadout = Loadout::new_empty(); + + loadout.swap(EquipSlot::Armor(ArmorSlot::Bag1), Some(get_test_bag(1))); + loadout.swap(EquipSlot::Armor(ArmorSlot::Bag2), Some(get_test_bag(1))); + loadout.swap(EquipSlot::Armor(ArmorSlot::Bag3), Some(get_test_bag(1))); + loadout.swap(EquipSlot::Armor(ArmorSlot::Bag4), Some(get_test_bag(1))); + + let result = loadout + .get_slot_to_equip_into(&ItemKind::Armor(Armor::test_armor( + ArmorKind::Bag("test".to_string()), + Protection::Normal(0.0), + ))) + .unwrap(); + + assert_eq!(EquipSlot::Armor(ArmorSlot::Bag1), result); + } +} diff --git a/common/src/loadout_builder.rs b/common/src/comp/inventory/loadout_builder.rs similarity index 57% rename from common/src/loadout_builder.rs rename to common/src/comp/inventory/loadout_builder.rs index a6b8c75813..82a36418cc 100644 --- a/common/src/loadout_builder.rs +++ b/common/src/comp/inventory/loadout_builder.rs @@ -1,7 +1,11 @@ use crate::comp::{ biped_large, golem, - item::{tool::AbilityMap, Item, ItemKind}, - quadruped_low, quadruped_medium, theropod, Body, CharacterAbility, ItemConfig, Loadout, + inventory::{ + loadout::Loadout, + slot::{ArmorSlot, EquipSlot}, + }, + item::{Item, ItemKind}, + quadruped_low, quadruped_medium, theropod, Body, }; use rand::Rng; @@ -13,19 +17,18 @@ use rand::Rng; /// use veloren_common::{ /// assets::AssetExt, /// comp::item::tool::AbilityMap, +/// comp::Item, /// LoadoutBuilder, /// }; /// -/// let map = AbilityMap::load_expect_cloned("common.abilities.weapon_ability_manifest"); -/// /// // Build a loadout with character starter defaults and a specific sword with default sword abilities /// let loadout = LoadoutBuilder::new() /// .defaults() -/// .active_item(Some(LoadoutBuilder::default_item_config_from_str( -/// "common.items.weapons.sword.zweihander_sword_0", &map -/// ))) +/// .active_item(Some(Item::new_from_asset_expect("common.items.weapons.sword.zweihander_sword_0"))) /// .build(); /// ``` +#[derive(Clone)] +pub struct LoadoutBuilder(Loadout); #[derive(Copy, Clone)] pub enum LoadoutConfig { @@ -40,29 +43,9 @@ pub enum LoadoutConfig { Warlock, } -pub struct LoadoutBuilder(Loadout); - impl LoadoutBuilder { #[allow(clippy::new_without_default)] // TODO: Pending review in #587 - pub fn new() -> Self { - Self(Loadout { - active_item: None, - second_item: None, - shoulder: None, - chest: None, - belt: None, - hand: None, - pants: None, - foot: None, - back: None, - ring: None, - neck: None, - lantern: None, - glider: None, - head: None, - tabard: None, - }) - } + pub fn new() -> Self { Self(Loadout::new_empty()) } /// Set default armor items for the loadout. This may vary with game /// updates, but should be safe defaults for a new character. @@ -73,7 +56,7 @@ impl LoadoutBuilder { .pants(Some(Item::new_from_asset_expect( "common.items.armor.starter.rugged_pants", ))) - .foot(Some(Item::new_from_asset_expect( + .feet(Some(Item::new_from_asset_expect( "common.items.armor.starter.sandals_0", ))) .lantern(Some(Item::new_from_asset_expect( @@ -89,7 +72,6 @@ impl LoadoutBuilder { pub fn build_loadout( body: Body, mut main_tool: Option, - map: &AbilityMap, config: Option, ) -> Self { // If no main tool is passed in, checks if species has a default main tool @@ -251,284 +233,230 @@ impl LoadoutBuilder { // Constructs ItemConfig from Item let active_item = if let Some(ItemKind::Tool(_)) = main_tool.as_ref().map(|i| i.kind()) { - main_tool.map(|item| ItemConfig::from((item, map))) + main_tool } else { - Some(LoadoutBuilder::animal(body)) + Some(Item::new_default_for_body(&body)) }; // Creates rest of loadout let loadout = if let Some(config) = config { use LoadoutConfig::*; match config { - Guard => Loadout { - active_item, - second_item: None, - shoulder: Some(Item::new_from_asset_expect( + Guard => LoadoutBuilder::new() + .active_item(active_item) + .shoulder(Some(Item::new_from_asset_expect( "common.items.armor.shoulder.steel_0", - )), - chest: Some(Item::new_from_asset_expect( + ))) + .chest(Some(Item::new_from_asset_expect( "common.items.armor.chest.steel_0", - )), - belt: Some(Item::new_from_asset_expect( + ))) + .belt(Some(Item::new_from_asset_expect( "common.items.armor.belt.steel_0", - )), - hand: Some(Item::new_from_asset_expect( + ))) + .hands(Some(Item::new_from_asset_expect( "common.items.armor.hand.steel_0", - )), - pants: Some(Item::new_from_asset_expect( + ))) + .pants(Some(Item::new_from_asset_expect( "common.items.armor.pants.steel_0", - )), - foot: Some(Item::new_from_asset_expect( + ))) + .feet(Some(Item::new_from_asset_expect( "common.items.armor.foot.steel_0", - )), - back: None, - ring: None, - neck: None, - lantern: match rand::thread_rng().gen_range(0, 3) { + ))) + .lantern(match rand::thread_rng().gen_range(0, 3) { 0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")), _ => None, - }, - glider: None, - head: None, - tabard: None, - }, - Outcast => Loadout { - active_item, - second_item: None, - shoulder: Some(Item::new_from_asset_expect( + }) + .build(), + Outcast => LoadoutBuilder::new() + .active_item(active_item) + .shoulder(Some(Item::new_from_asset_expect( "common.items.armor.shoulder.cloth_purple_0", - )), - chest: Some(Item::new_from_asset_expect( + ))) + .chest(Some(Item::new_from_asset_expect( "common.items.armor.chest.cloth_purple_0", - )), - belt: Some(Item::new_from_asset_expect( + ))) + .belt(Some(Item::new_from_asset_expect( "common.items.armor.belt.cloth_purple_0", - )), - hand: Some(Item::new_from_asset_expect( + ))) + .hands(Some(Item::new_from_asset_expect( "common.items.armor.hand.cloth_purple_0", - )), - pants: Some(Item::new_from_asset_expect( + ))) + .pants(Some(Item::new_from_asset_expect( "common.items.armor.pants.cloth_purple_0", - )), - foot: Some(Item::new_from_asset_expect( + ))) + .feet(Some(Item::new_from_asset_expect( "common.items.armor.foot.cloth_purple_0", - )), - back: None, - ring: None, - neck: None, - lantern: match rand::thread_rng().gen_range(0, 3) { + ))) + .lantern(match rand::thread_rng().gen_range(0, 3) { 0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")), _ => None, - }, - glider: None, - head: None, - tabard: None, - }, - Highwayman => Loadout { - active_item, - second_item: None, - shoulder: Some(Item::new_from_asset_expect( + }) + .build(), + Highwayman => LoadoutBuilder::new() + .active_item(active_item) + .shoulder(Some(Item::new_from_asset_expect( "common.items.armor.shoulder.leather_0", - )), - chest: Some(Item::new_from_asset_expect( + ))) + .chest(Some(Item::new_from_asset_expect( "common.items.armor.chest.leather_0", - )), - belt: Some(Item::new_from_asset_expect( + ))) + .belt(Some(Item::new_from_asset_expect( "common.items.armor.belt.leather_0", - )), - hand: Some(Item::new_from_asset_expect( + ))) + .hands(Some(Item::new_from_asset_expect( "common.items.armor.hand.leather_0", - )), - pants: Some(Item::new_from_asset_expect( + ))) + .pants(Some(Item::new_from_asset_expect( "common.items.armor.pants.leather_0", - )), - foot: Some(Item::new_from_asset_expect( + ))) + .feet(Some(Item::new_from_asset_expect( "common.items.armor.foot.leather_0", - )), - back: None, - ring: None, - neck: None, - lantern: match rand::thread_rng().gen_range(0, 3) { + ))) + .lantern(match rand::thread_rng().gen_range(0, 3) { 0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")), _ => None, - }, - glider: None, - head: None, - tabard: None, - }, - Bandit => Loadout { - active_item, - second_item: None, - shoulder: Some(Item::new_from_asset_expect( + }) + .build(), + Bandit => LoadoutBuilder::new() + .active_item(active_item) + .shoulder(Some(Item::new_from_asset_expect( "common.items.armor.shoulder.assassin", - )), - chest: Some(Item::new_from_asset_expect( + ))) + .chest(Some(Item::new_from_asset_expect( "common.items.armor.chest.assassin", - )), - belt: Some(Item::new_from_asset_expect( + ))) + .belt(Some(Item::new_from_asset_expect( "common.items.armor.belt.assassin", - )), - hand: Some(Item::new_from_asset_expect( + ))) + .hands(Some(Item::new_from_asset_expect( "common.items.armor.hand.assassin", - )), - pants: Some(Item::new_from_asset_expect( + ))) + .pants(Some(Item::new_from_asset_expect( "common.items.armor.pants.assassin", - )), - foot: Some(Item::new_from_asset_expect( + ))) + .feet(Some(Item::new_from_asset_expect( "common.items.armor.foot.assassin", - )), - back: None, - ring: None, - neck: None, - lantern: match rand::thread_rng().gen_range(0, 3) { + ))) + .lantern(match rand::thread_rng().gen_range(0, 3) { 0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")), _ => None, - }, - glider: None, - head: None, - tabard: None, - }, - CultistNovice => Loadout { - active_item, - second_item: None, - shoulder: Some(Item::new_from_asset_expect( + }) + .build(), + CultistNovice => LoadoutBuilder::new() + .active_item(active_item) + .shoulder(Some(Item::new_from_asset_expect( "common.items.armor.shoulder.steel_0", - )), - chest: Some(Item::new_from_asset_expect( + ))) + .chest(Some(Item::new_from_asset_expect( "common.items.armor.chest.steel_0", - )), - belt: Some(Item::new_from_asset_expect( + ))) + .belt(Some(Item::new_from_asset_expect( "common.items.armor.belt.steel_0", - )), - hand: Some(Item::new_from_asset_expect( + ))) + .hands(Some(Item::new_from_asset_expect( "common.items.armor.hand.steel_0", - )), - pants: Some(Item::new_from_asset_expect( + ))) + .pants(Some(Item::new_from_asset_expect( "common.items.armor.pants.steel_0", - )), - foot: Some(Item::new_from_asset_expect( + ))) + .feet(Some(Item::new_from_asset_expect( "common.items.armor.foot.steel_0", - )), - back: Some(Item::new_from_asset_expect( + ))) + .back(Some(Item::new_from_asset_expect( "common.items.armor.back.dungeon_purple-0", - )), - ring: None, - neck: None, - lantern: match rand::thread_rng().gen_range(0, 3) { + ))) + .lantern(match rand::thread_rng().gen_range(0, 3) { 0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")), _ => None, - }, - glider: None, - head: None, - tabard: None, - }, - CultistAcolyte => Loadout { - active_item, - second_item: None, - shoulder: Some(Item::new_from_asset_expect( + }) + .build(), + CultistAcolyte => LoadoutBuilder::new() + .active_item(active_item) + .shoulder(Some(Item::new_from_asset_expect( "common.items.armor.shoulder.cultist_shoulder_purple", - )), - chest: Some(Item::new_from_asset_expect( + ))) + .chest(Some(Item::new_from_asset_expect( "common.items.armor.chest.cultist_chest_purple", - )), - belt: Some(Item::new_from_asset_expect( + ))) + .belt(Some(Item::new_from_asset_expect( "common.items.armor.belt.cultist_belt", - )), - hand: Some(Item::new_from_asset_expect( + ))) + .hands(Some(Item::new_from_asset_expect( "common.items.armor.hand.cultist_hands_purple", - )), - pants: Some(Item::new_from_asset_expect( + ))) + .pants(Some(Item::new_from_asset_expect( "common.items.armor.pants.cultist_legs_purple", - )), - foot: Some(Item::new_from_asset_expect( + ))) + .feet(Some(Item::new_from_asset_expect( "common.items.armor.foot.cultist_boots", - )), - back: Some(Item::new_from_asset_expect( + ))) + .back(Some(Item::new_from_asset_expect( "common.items.armor.back.dungeon_purple-0", - )), - ring: None, - neck: None, - lantern: match rand::thread_rng().gen_range(0, 3) { + ))) + .lantern(match rand::thread_rng().gen_range(0, 3) { 0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")), _ => None, - }, - glider: None, - head: None, - tabard: None, - }, - Warlord => Loadout { - active_item, - second_item: None, - shoulder: Some(Item::new_from_asset_expect( + }) + .build(), + Warlord => LoadoutBuilder::new() + .active_item(active_item) + .shoulder(Some(Item::new_from_asset_expect( "common.items.armor.shoulder.warlord", - )), - chest: Some(Item::new_from_asset_expect( + ))) + .chest(Some(Item::new_from_asset_expect( "common.items.armor.chest.warlord", - )), - belt: Some(Item::new_from_asset_expect( + ))) + .belt(Some(Item::new_from_asset_expect( "common.items.armor.belt.warlord", - )), - hand: Some(Item::new_from_asset_expect( + ))) + .hands(Some(Item::new_from_asset_expect( "common.items.armor.hand.warlord", - )), - pants: Some(Item::new_from_asset_expect( + ))) + .pants(Some(Item::new_from_asset_expect( "common.items.armor.pants.warlord", - )), - foot: Some(Item::new_from_asset_expect( + ))) + .feet(Some(Item::new_from_asset_expect( "common.items.armor.foot.warlord", - )), - back: Some(Item::new_from_asset_expect( + ))) + .back(Some(Item::new_from_asset_expect( "common.items.armor.back.warlord", - )), - ring: None, - neck: None, - lantern: match rand::thread_rng().gen_range(0, 3) { + ))) + .lantern(match rand::thread_rng().gen_range(0, 3) { 0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")), _ => None, - }, - glider: None, - head: None, - tabard: None, - }, - Warlock => Loadout { - active_item, - second_item: None, - shoulder: Some(Item::new_from_asset_expect( + }) + .build(), + Warlock => LoadoutBuilder::new() + .active_item(active_item) + .shoulder(Some(Item::new_from_asset_expect( "common.items.armor.shoulder.warlock", - )), - chest: Some(Item::new_from_asset_expect( + ))) + .chest(Some(Item::new_from_asset_expect( "common.items.armor.chest.warlock", - )), - belt: Some(Item::new_from_asset_expect( + ))) + .belt(Some(Item::new_from_asset_expect( "common.items.armor.belt.warlock", - )), - hand: Some(Item::new_from_asset_expect( + ))) + .hands(Some(Item::new_from_asset_expect( "common.items.armor.hand.warlock", - )), - pants: Some(Item::new_from_asset_expect( + ))) + .pants(Some(Item::new_from_asset_expect( "common.items.armor.pants.warlock", - )), - foot: Some(Item::new_from_asset_expect( + ))) + .feet(Some(Item::new_from_asset_expect( "common.items.armor.foot.warlock", - )), - back: Some(Item::new_from_asset_expect( + ))) + .back(Some(Item::new_from_asset_expect( "common.items.armor.back.warlock", - )), - ring: None, - neck: None, - lantern: match rand::thread_rng().gen_range(0, 3) { + ))) + .lantern(match rand::thread_rng().gen_range(0, 3) { 0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")), _ => None, - }, - glider: None, - head: None, - tabard: None, - }, - Villager => Loadout { - active_item, - second_item: None, - shoulder: None, - chest: Some(Item::new_from_asset_expect( + }) + .build(), + Villager => LoadoutBuilder::new() + .active_item(active_item) + .chest(Some(Item::new_from_asset_expect( match rand::thread_rng().gen_range(0, 10) { 0 => "common.items.armor.chest.worker_green_0", 1 => "common.items.armor.chest.worker_green_1", @@ -541,167 +469,105 @@ impl LoadoutBuilder { 8 => "common.items.armor.chest.worker_orange_0", _ => "common.items.armor.chest.worker_orange_1", }, - )), - belt: Some(Item::new_from_asset_expect( + ))) + .belt(Some(Item::new_from_asset_expect( "common.items.armor.belt.leather_0", - )), - hand: None, - pants: Some(Item::new_from_asset_expect( + ))) + .pants(Some(Item::new_from_asset_expect( "common.items.armor.pants.worker_blue_0", - )), - foot: Some(Item::new_from_asset_expect( + ))) + .feet(Some(Item::new_from_asset_expect( match rand::thread_rng().gen_range(0, 2) { 0 => "common.items.armor.foot.leather_0", _ => "common.items.armor.starter.sandals_0", }, - )), - back: None, - ring: None, - neck: None, - lantern: None, - glider: None, - head: None, - tabard: None, - }, + ))) + .build(), } } else { - Loadout { - active_item, - second_item: None, - shoulder: None, - chest: None, - belt: None, - hand: None, - pants: None, - foot: None, - back: None, - ring: None, - neck: None, - lantern: None, - glider: None, - head: None, - tabard: None, - } + LoadoutBuilder::new().active_item(active_item).build() }; Self(loadout) } - /// Default animal configuration - pub fn animal(body: Body) -> ItemConfig { - ItemConfig { - item: Item::new_from_asset_expect("common.items.weapons.empty.empty"), - ability1: Some(CharacterAbility::BasicMelee { - energy_cost: 10, - buildup_duration: 500, - swing_duration: 100, - recover_duration: 100, - base_damage: body.base_dmg(), - knockback: 0.0, - range: body.base_range(), - max_angle: 20.0, - }), - ability2: None, - ability3: None, - block_ability: None, - dodge_ability: None, - } - } - - /// Get the default [ItemConfig](../comp/struct.ItemConfig.html) for a tool - /// (weapon). This information is required for the `active` and `second` - /// weapon items in a loadout. If some customisation to the item's - /// abilities or their timings is desired, you should create and provide - /// the item config directly to the [active_item](#method.active_item) - /// method - pub fn default_item_config_from_item(item: Item, map: &AbilityMap) -> ItemConfig { - ItemConfig::from((item, map)) - } - - /// Get an item's (weapon's) default - /// [ItemConfig](../comp/struct.ItemConfig.html) - /// by string reference. This will first attempt to load the Item, then - /// the default abilities for that item via the - /// [default_item_config_from_item](#method.default_item_config_from_item) - /// function - pub fn default_item_config_from_str(item_ref: &str, map: &AbilityMap) -> ItemConfig { - Self::default_item_config_from_item(Item::new_from_asset_expect(item_ref), map) - } - - pub fn active_item(mut self, item: Option) -> Self { - self.0.active_item = item; - + pub fn active_item(mut self, item: Option) -> Self { + self.0.swap(EquipSlot::Mainhand, item); self } - pub fn second_item(mut self, item: Option) -> Self { - self.0.second_item = item; - + pub fn second_item(mut self, item: Option) -> Self { + self.0.swap(EquipSlot::Offhand, item); self } pub fn shoulder(mut self, item: Option) -> Self { - self.0.shoulder = item; + self.0.swap(EquipSlot::Armor(ArmorSlot::Shoulders), item); self } pub fn chest(mut self, item: Option) -> Self { - self.0.chest = item; + self.0.swap(EquipSlot::Armor(ArmorSlot::Chest), item); self } pub fn belt(mut self, item: Option) -> Self { - self.0.belt = item; + self.0.swap(EquipSlot::Armor(ArmorSlot::Belt), item); self } - pub fn hand(mut self, item: Option) -> Self { - self.0.hand = item; + pub fn hands(mut self, item: Option) -> Self { + self.0.swap(EquipSlot::Armor(ArmorSlot::Hands), item); self } pub fn pants(mut self, item: Option) -> Self { - self.0.pants = item; + self.0.swap(EquipSlot::Armor(ArmorSlot::Legs), item); self } - pub fn foot(mut self, item: Option) -> Self { - self.0.foot = item; + pub fn feet(mut self, item: Option) -> Self { + self.0.swap(EquipSlot::Armor(ArmorSlot::Feet), item); self } pub fn back(mut self, item: Option) -> Self { - self.0.back = item; + self.0.swap(EquipSlot::Armor(ArmorSlot::Back), item); self } - pub fn ring(mut self, item: Option) -> Self { - self.0.ring = item; + pub fn ring1(mut self, item: Option) -> Self { + self.0.swap(EquipSlot::Armor(ArmorSlot::Ring1), item); + self + } + + pub fn ring2(mut self, item: Option) -> Self { + self.0.swap(EquipSlot::Armor(ArmorSlot::Ring2), item); self } pub fn neck(mut self, item: Option) -> Self { - self.0.neck = item; + self.0.swap(EquipSlot::Armor(ArmorSlot::Neck), item); self } pub fn lantern(mut self, item: Option) -> Self { - self.0.lantern = item; + self.0.swap(EquipSlot::Lantern, item); self } pub fn glider(mut self, item: Option) -> Self { - self.0.glider = item; + self.0.swap(EquipSlot::Glider, item); self } pub fn head(mut self, item: Option) -> Self { - self.0.head = item; + self.0.swap(EquipSlot::Armor(ArmorSlot::Head), item); self } pub fn tabard(mut self, item: Option) -> Self { - self.0.tabard = item; + self.0.swap(EquipSlot::Armor(ArmorSlot::Tabard), item); self } diff --git a/common/src/comp/inventory/mod.rs b/common/src/comp/inventory/mod.rs index 5425667dd0..ff3b285c68 100644 --- a/common/src/comp/inventory/mod.rs +++ b/common/src/comp/inventory/mod.rs @@ -1,17 +1,41 @@ -pub mod item; -pub mod slot; - -use crate::{comp::inventory::item::ItemDef, recipe::Recipe}; use core::ops::Not; -use item::Item; +use std::{collections::HashMap, convert::TryFrom, iter::once, mem, ops::Range}; + use serde::{Deserialize, Serialize}; -use specs::{Component, HashMapStorage}; +use specs::{Component, DerefFlaggedStorage}; use specs_idvs::IdvStorage; +use tracing::{debug, trace, warn}; + +use crate::{ + comp::{ + inventory::{ + item::ItemDef, + loadout::Loadout, + slot::{EquipSlot, Slot, SlotError}, + }, + slot::{InvSlotId, SlotId}, + Item, + }, + recipe::Recipe, + LoadoutBuilder, +}; + +pub mod item; +pub mod loadout; +pub mod loadout_builder; +pub mod slot; +#[cfg(test)] mod test; +#[cfg(test)] mod test_helpers; + +pub type InvSlot = Option; +const DEFAULT_INVENTORY_SLOTS: usize = 18; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Inventory { - slots: Vec>, - amount: u32, + loadout: Loadout, + /// The "built-in" slots belonging to the inventory itself, all other slots + /// are provided by equipped items + slots: Vec, } /// Errors which the methods on `Inventory` produce @@ -22,24 +46,55 @@ pub enum Error { Full(Vec), } -#[allow(clippy::len_without_is_empty)] // TODO: Pending review in #587 +/// Represents the Inventory of an entity. The inventory has 18 "built-in" +/// slots, with further slots being provided by items equipped in the Loadout +/// sub-struct. Inventory slots are indexed by `InvSlotId` which is +/// comprised of `loadout_idx` - the index of the loadout item that provides the +/// slot, 0 being the built-in inventory slots, and `slot_idx` - the index of +/// the slot within that loadout item. +/// +/// Currently, it is not supported for inventories to contain items that have +/// items inside them. This is due to both game balance purposes, and the lack +/// of a UI to show such items. Because of this, any action that would result in +/// such an item being put into the inventory (item pickup, unequipping an item +/// that contains items etc) must first ensure items are unloaded from the item. +/// This is handled in `inventory\slot.rs` impl Inventory { - pub fn new_empty() -> Inventory { + pub fn new_empty() -> Inventory { Self::new_with_loadout(LoadoutBuilder::new().build()) } + + pub fn new_with_loadout(loadout: Loadout) -> Inventory { Inventory { - slots: vec![None; 36], - amount: 0, + loadout, + slots: vec![None; DEFAULT_INVENTORY_SLOTS], } } - pub fn slots(&self) -> &[Option] { &self.slots } + /// Total number of slots in in the inventory. + pub fn capacity(&self) -> usize { self.slots().count() } - pub fn len(&self) -> usize { self.slots.len() } + /// An iterator of all inventory slots + pub fn slots(&self) -> impl Iterator { + self.slots + .iter() + .chain(self.loadout.inv_slots_with_id().map(|(_, slot)| slot)) + } - /// Total number of occupied slots in the inventory. - pub fn amount(&self) -> u32 { self.amount } + /// A mutable iterator of all inventory slots + fn slots_mut(&mut self) -> impl Iterator { + self.slots.iter_mut().chain(self.loadout.inv_slots_mut()) + } - pub fn recount_items(&mut self) { - self.amount = self.slots.iter().filter(|i| i.is_some()).count() as u32; + /// An iterator of all inventory slots and their position + pub fn slots_with_id(&self) -> impl Iterator { + self.slots + .iter() + .enumerate() + .map(|(i, slot)| ((InvSlotId::new(0, u16::try_from(i).unwrap())), slot)) + .chain( + self.loadout + .inv_slots_with_id() + .map(|(loadout_slot_id, inv_slot)| (loadout_slot_id.into(), inv_slot)), + ) } /// Adds a new item to the first fitting group of the inventory or starts a @@ -47,8 +102,7 @@ impl Inventory { pub fn push(&mut self, item: Item) -> Option { if item.is_stackable() { if let Some(slot_item) = self - .slots - .iter_mut() + .slots_mut() .filter_map(Option::as_mut) .find(|s| *s == &item) { @@ -61,21 +115,7 @@ impl Inventory { // No existing item to stack with or item not stackable, put the item in a new // slot - self.add_to_first_empty(item) - } - - /// Adds a new item to the first empty slot of the inventory. Returns the - /// item again if no free slot was found. - fn add_to_first_empty(&mut self, item: Item) -> Option { - let item = match self.slots.iter_mut().find(|slot| slot.is_none()) { - Some(slot) => { - *slot = Some(item); - None - }, - None => Some(item), - }; - self.recount_items(); - item + self.insert(item) } /// Add a series of items to inventory, returning any which do not fit as an @@ -120,24 +160,22 @@ impl Inventory { /// Replaces an item in a specific slot of the inventory. Returns the old /// item or the same item again if that slot was not found. - pub fn insert(&mut self, cell: usize, item: Item) -> Result, Item> { - match self.slots.get_mut(cell) { - Some(slot) => { - let old = core::mem::replace(slot, Some(item)); - if old.is_none() { - self.recount_items(); - } - Ok(old) - }, + pub fn insert_at(&mut self, inv_slot_id: InvSlotId, item: Item) -> Result, Item> { + match self.slot_mut(inv_slot_id) { + Some(slot) => Ok(core::mem::replace(slot, Some(item))), None => Err(item), } } /// Checks if inserting item exists in given cell. Inserts an item if it /// exists. - pub fn insert_or_stack(&mut self, cell: usize, item: Item) -> Result, Item> { + pub fn insert_or_stack_at( + &mut self, + inv_slot_id: InvSlotId, + item: Item, + ) -> Result, Item> { if item.is_stackable() { - match self.slots.get_mut(cell) { + match self.slot_mut(inv_slot_id) { Some(Some(slot_item)) => { Ok(if slot_item == &item { slot_item @@ -150,46 +188,77 @@ impl Inventory { Some(old_item) }) }, - Some(None) => self.insert(cell, item), + Some(None) => self.insert_at(inv_slot_id, item), None => Err(item), } } else { - self.insert(cell, item) + self.insert_at(inv_slot_id, item) } } - pub fn is_full(&self) -> bool { self.slots.iter().all(|slot| slot.is_some()) } + /// Attempts to equip the item into a compatible, unpopulated loadout slot. + /// If no slot is available the item is returned. + #[must_use = "Returned item will be lost if not used"] + pub fn try_equip(&mut self, item: Item) -> Result<(), Item> { self.loadout.try_equip(item) } - /// O(n) count the number of items in this inventory. - pub fn count(&self) -> usize { self.slots.iter().filter_map(|slot| slot.as_ref()).count() } + pub fn populated_slots(&self) -> usize { self.slots().filter_map(|slot| slot.as_ref()).count() } - /// O(n) check if an item is in this inventory. + fn free_slots(&self) -> usize { self.slots().filter(|slot| slot.is_none()).count() } + + /// Check if an item is in this inventory. pub fn contains(&self, item: &Item) -> bool { - self.slots.iter().any(|slot| slot.as_ref() == Some(item)) + self.slots().any(|slot| slot.as_ref() == Some(item)) } /// Get content of a slot - pub fn get(&self, cell: usize) -> Option<&Item> { - self.slots.get(cell).and_then(Option::as_ref) + pub fn get(&self, inv_slot_id: InvSlotId) -> Option<&Item> { + self.slot(inv_slot_id).and_then(Option::as_ref) + } + + /// Returns a reference to the item (if any) equipped in the given EquipSlot + pub fn equipped(&self, equip_slot: EquipSlot) -> Option<&Item> { + self.loadout.equipped(equip_slot) + } + + pub fn loadout_items_with_persistence_key( + &self, + ) -> impl Iterator)> { + self.loadout.items_with_persistence_key() + } + + /// Returns the range of inventory slot indexes that a particular equipped + /// item provides (used for UI highlighting of inventory slots when hovering + /// over a loadout item) + pub fn get_slot_range_for_equip_slot(&self, equip_slot: EquipSlot) -> Option> { + // The slot range returned from `Loadout` must be offset by the number of slots + // that the inventory itself provides. + let offset = self.slots.len(); + self.loadout + .slot_range_for_equip_slot(equip_slot) + .map(|loadout_range| (loadout_range.start + offset)..(loadout_range.end + offset)) } /// Swap the items inside of two slots - pub fn swap_slots(&mut self, a: usize, b: usize) { - if a.max(b) < self.slots.len() { - self.slots.swap(a, b); + pub fn swap_slots(&mut self, a: InvSlotId, b: InvSlotId) { + if self.slot(a).is_none() || self.slot(b).is_none() { + warn!("swap_slots called with non-existent inventory slot(s)"); + return; } + + let slot_a = mem::take(self.slot_mut(a).unwrap()); + let slot_b = mem::take(self.slot_mut(b).unwrap()); + *self.slot_mut(a).unwrap() = slot_b; + *self.slot_mut(b).unwrap() = slot_a; } /// Remove an item from the slot - pub fn remove(&mut self, cell: usize) -> Option { - let item = self.slots.get_mut(cell).and_then(|item| item.take()); - self.recount_items(); - item + pub fn remove(&mut self, inv_slot_id: InvSlotId) -> Option { + self.slot_mut(inv_slot_id).and_then(|item| item.take()) } /// Remove just one item from the slot - pub fn take(&mut self, cell: usize) -> Option { - if let Some(Some(item)) = self.slots.get_mut(cell) { + pub fn take(&mut self, inv_slot_id: InvSlotId) -> Option { + if let Some(Some(item)) = self.slot_mut(inv_slot_id) { let mut return_item = item.duplicate(); if item.is_stackable() && item.amount() > 1 { @@ -197,20 +266,25 @@ impl Inventory { return_item .set_amount(1) .expect("Items duplicated from a stackable item must be stackable."); - self.recount_items(); Some(return_item) } else { - self.remove(cell) + self.remove(inv_slot_id) } } else { None } } + /// Takes all items from the inventory + pub fn drain(&mut self) -> impl Iterator + '_ { + self.slots_mut() + .filter(|x| x.is_some()) + .filter_map(mem::take) + } + /// Determine how many of a particular item there is in the inventory. pub fn item_count(&self, item_def: &ItemDef) -> u64 { self.slots() - .iter() .flatten() .filter(|it| it.is_same_item_def(item_def)) .map(|it| u64::from(it.amount())) @@ -225,17 +299,18 @@ impl Inventory { pub fn contains_ingredients<'a>( &self, recipe: &'a Recipe, - ) -> Result, Vec<(&'a ItemDef, u32)>> { - let mut slot_claims = vec![0; self.slots.len()]; + ) -> Result, Vec<(&'a ItemDef, u32)>> { + let mut slot_claims = HashMap::::new(); let mut missing = Vec::<(&ItemDef, u32)>::new(); for (input, mut needed) in recipe.inputs() { let mut contains_any = false; - for (i, slot) in self.slots().iter().enumerate() { + for (inv_slot_id, slot) in self.slots_with_id() { if let Some(item) = slot.as_ref().filter(|item| item.is_same_item_def(&*input)) { - let can_claim = (item.amount() - slot_claims[i]).min(needed); - slot_claims[i] += can_claim; + let claim = slot_claims.entry(inv_slot_id).or_insert(0); + let can_claim = (item.amount() - *claim).min(needed); + *claim += can_claim; needed -= can_claim; contains_any = true; } @@ -252,24 +327,312 @@ impl Inventory { Err(missing) } } -} -impl Default for Inventory { - fn default() -> Inventory { - let mut inventory = Inventory { - slots: vec![None; 36], - amount: 0, - }; - inventory.push(Item::new_from_asset_expect( - "common.items.consumable.potion_minor", - )); - inventory.push(Item::new_from_asset_expect("common.items.food.cheese")); - inventory + /// Adds a new item to the first empty slot of the inventory. Returns the + /// item again if no free slot was found. + fn insert(&mut self, item: Item) -> Option { + match self.slots_mut().find(|slot| slot.is_none()) { + Some(slot) => { + *slot = Some(item); + None + }, + None => Some(item), + } + } + + fn slot(&self, inv_slot_id: InvSlotId) -> Option<&InvSlot> { + match SlotId::from(inv_slot_id) { + SlotId::Inventory(slot_idx) => self.slots.get(slot_idx), + SlotId::Loadout(loadout_slot_id) => self.loadout.inv_slot(loadout_slot_id), + } + } + + fn slot_mut(&mut self, inv_slot_id: InvSlotId) -> Option<&mut InvSlot> { + match SlotId::from(inv_slot_id) { + SlotId::Inventory(slot_idx) => self.slots.get_mut(slot_idx), + SlotId::Loadout(loadout_slot_id) => self.loadout.inv_slot_mut(loadout_slot_id), + } + } + + /// Returns the number of free slots in the inventory ignoring any slots + /// granted by the item (if any) equipped in the provided EquipSlot. + pub fn free_slots_minus_equipped_item(&self, equip_slot: EquipSlot) -> usize { + if let Some(mut equip_slot_idx) = self.loadout.loadout_idx_for_equip_slot(equip_slot) { + // Offset due to index 0 representing built-in inventory slots + equip_slot_idx += 1; + + self.slots_with_id() + .filter(|(inv_slot_id, slot)| { + inv_slot_id.loadout_idx() != equip_slot_idx && slot.is_none() + }) + .count() + } else { + // TODO: return Option and evaluate to None here + warn!( + "Attempted to fetch loadout index for non-existent EquipSlot: {:?}", + equip_slot + ); + 0 + } + } + + pub fn equipped_items(&self) -> impl Iterator { self.loadout.items() } + + /// Replaces the loadout item (if any) in the given EquipSlot with the + /// provided item, returning the item that was previously in the slot. + pub fn replace_loadout_item( + &mut self, + equip_slot: EquipSlot, + replacement_item: Option, + ) -> Option { + self.loadout.swap(equip_slot, replacement_item) + } + + /// Equip an item from a slot in inventory. The currently equipped item will + /// go into inventory. If the item is going to mainhand, put mainhand in + /// offhand and place offhand into inventory. + #[must_use = "Returned items will be lost if not used"] + pub fn equip(&mut self, inv_slot: InvSlotId) -> Option> { + let mut leftover_items = None; + self.get(inv_slot) + .map(|x| x.kind().clone()) + .map(|item_kind| { + self.loadout + .get_slot_to_equip_into(&item_kind) + .map(|equip_slot| { + // Special case when equipping into main hand - swap with offhand first + if equip_slot == EquipSlot::Mainhand { + self.loadout + .swap_slots(EquipSlot::Mainhand, EquipSlot::Offhand); + } + + leftover_items = self.swap_inventory_loadout(inv_slot, equip_slot); + }) + }); + + leftover_items + } + + /// Determines how many free inventory slots will be left after equipping an + /// item (because it could be swapped with an already equipped item that + /// provides more inventory slots than the item being equipped) + pub fn free_after_equip(&self, inv_slot: InvSlotId) -> i32 { + let (inv_slot_for_equipped, slots_from_equipped) = self + .get(inv_slot) + .map(|x| x.kind().clone()) + .and_then(|item_kind| self.loadout.get_slot_to_equip_into(&item_kind)) + .and_then(|equip_slot| self.equipped(equip_slot)) + .map_or((1, 0), |item| (0, item.slots().len())); + + let slots_from_inv = self + .get(inv_slot) + .map(|item| item.slots().len()) + .unwrap_or(0); + + i32::try_from(self.capacity()).expect("Inventory with more than i32::MAX slots") + - i32::try_from(slots_from_equipped) + .expect("Equipped item with more than i32::MAX slots") + + i32::try_from(slots_from_inv).expect("Inventory item with more than i32::MAX slots") + - i32::try_from(self.populated_slots()) + .expect("Inventory item with more than i32::MAX used slots") + + inv_slot_for_equipped // If there is no item already in the equip slot we gain 1 slot + } + + /// Handles picking up an item, unloading any items inside the item being + /// picked up and pushing them to the inventory to ensure that items + /// containing items aren't inserted into the inventory as this is not + /// currently supported. + pub fn pickup_item(&mut self, mut item: Item) -> Result<(), Item> { + if self.free_slots() < item.populated_slots() + 1 { + return Err(item); + } + + // Unload any items contained within the item, and push those items and the item + // itself into the inventory. We already know that there are enough free slots + // so push will never give us an item back. + item.drain() + .collect::>() + .into_iter() + .chain(once(item)) + .for_each(|item| { + self.push(item).unwrap_none(); + }); + + Ok(()) + } + + /// Unequip an item from slot and place into inventory. Will leave the item + /// equipped if inventory has no slots available. + #[must_use = "Returned items will be lost if not used"] + pub fn unequip(&mut self, equip_slot: EquipSlot) -> Result>, SlotError> { + // Ensure there is enough space in the inventory to place the unequipped item + if self.free_slots_minus_equipped_item(equip_slot) == 0 { + return Err(SlotError::InventoryFull); + } + + Ok(self + .loadout + .swap(equip_slot, None) + .and_then(|mut unequipped_item| { + let unloaded_items: Vec = unequipped_item.drain().collect(); + self.push(unequipped_item) + .expect_none("Failed to push item to inventory, precondition failed?"); + + // Unload any items that were inside the equipped item into the inventory, with + // any that don't fit to be to be dropped on the floor by the caller + match self.push_all(unloaded_items.into_iter()) { + Err(Error::Full(leftovers)) => Some(leftovers), + Ok(_) => None, + } + })) + } + + /// Determines how many free inventory slots will be left after unequipping + /// an item + pub fn free_after_unequip(&self, equip_slot: EquipSlot) -> i32 { + let (inv_slot_for_unequipped, slots_from_equipped) = self + .equipped(equip_slot) + .map_or((0, 0), |item| (1, item.slots().len())); + + i32::try_from(self.capacity()).expect("Inventory with more than i32::MAX slots") + - i32::try_from(slots_from_equipped) + .expect("Equipped item with more than i32::MAX slots") + - i32::try_from(self.populated_slots()) + .expect("Inventory item with more than i32::MAX used slots") + - inv_slot_for_unequipped // If there is an item being unequipped we lose 1 slot + } + + /// Swaps items from two slots, regardless of if either is inventory or + /// loadout. + #[must_use = "Returned items will be lost if not used"] + pub fn swap(&mut self, slot_a: Slot, slot_b: Slot) -> Option> { + match (slot_a, slot_b) { + (Slot::Inventory(slot_a), Slot::Inventory(slot_b)) => { + self.swap_slots(slot_a, slot_b); + None + }, + (Slot::Inventory(inv_slot), Slot::Equip(equip_slot)) + | (Slot::Equip(equip_slot), Slot::Inventory(inv_slot)) => { + self.swap_inventory_loadout(inv_slot, equip_slot) + }, + (Slot::Equip(slot_a), Slot::Equip(slot_b)) => { + self.loadout.swap_slots(slot_a, slot_b); + None + }, + } + } + + /// Determines how many free inventory slots will be left after swapping two + /// item slots + pub fn free_after_swap(&self, equip_slot: EquipSlot, inv_slot: InvSlotId) -> i32 { + let (inv_slot_for_equipped, slots_from_equipped) = self + .equipped(equip_slot) + .map_or((0, 0), |item| (1, item.slots().len())); + let (inv_slot_for_inv_item, slots_from_inv_item) = self + .get(inv_slot) + .map_or((0, 0), |item| (1, item.slots().len())); + + // Return the number of inventory slots that will be free once this slot swap is + // performed + i32::try_from(self.capacity()) + .expect("inventory with more than i32::MAX slots") + - i32::try_from(slots_from_equipped) + .expect("equipped item with more than i32::MAX slots") + + i32::try_from(slots_from_inv_item) + .expect("inventory item with more than i32::MAX slots") + - i32::try_from(self.populated_slots()) + .expect("inventory with more than i32::MAX used slots") + - inv_slot_for_equipped // +1 inventory slot required if an item was unequipped + + inv_slot_for_inv_item // -1 inventory slot required if an item was equipped + } + + /// Swap item in an inventory slot with one in a loadout slot. + #[must_use = "Returned items will be lost if not used"] + pub fn swap_inventory_loadout( + &mut self, + inv_slot_id: InvSlotId, + equip_slot: EquipSlot, + ) -> Option> { + if !self.can_swap(inv_slot_id, equip_slot) { + return None; + } + + let mut unloaded_items = None; + + // Take the item from the inventory + let from_inv = self.remove(inv_slot_id); + + // Swap the equipped item for the item from the inventory + let from_equip = self.loadout.swap(equip_slot, from_inv); + if let Some(mut from_equip) = from_equip { + // Unload any items held inside the previously equipped item + let items: Vec = from_equip.drain().collect(); + if items.iter().len() > 0 { + unloaded_items = Some(items); + } + + // Attempt to put the unequipped item in the same slot that the inventory item + // was in - if that slot no longer exists (because a large container was + // swapped for a smaller one) then push the item to the first free + // inventory slot instead. + if let Err(returned) = self.insert_at(inv_slot_id, from_equip) { + self.push(returned) + .expect_none("Unable to push to inventory, no slots (bug in can_swap()?)"); + } + } + + // Attempt to put any items unloaded from the unequipped item into empty + // inventory slots and return any that don't fit to the caller where they + // will be dropped on the ground + if let Some(unloaded_items) = unloaded_items { + let leftovers = match self.push_all(unloaded_items.into_iter()) { + Err(Error::Full(leftovers)) => leftovers, + Ok(_) => vec![], + }; + return Some(leftovers); + } + + None + } + + /// Determines if an inventory and loadout slot can be swapped, taking into + /// account whether there will be free space in the inventory for the + /// loadout item once any slots that were provided by it have been + /// removed. + pub fn can_swap(&self, inv_slot_id: InvSlotId, equip_slot: EquipSlot) -> bool { + // Check if loadout slot can hold item + if !self + .get(inv_slot_id) + .map_or(true, |item| equip_slot.can_hold(&item.kind())) + { + trace!("can_swap = false, equip slot can't hold item"); + return false; + } + + // If we're swapping an equipped item with an empty inventory slot, make + // sure that there will be enough space in the inventory after any + // slots granted by the item being unequipped have been removed. + if let Some(inv_slot) = self.slot(inv_slot_id) { + if inv_slot.is_none() && self.free_slots_minus_equipped_item(equip_slot) == 0 { + // No free inventory slots after slots provided by the equipped + //item are discounted + trace!("can_swap = false, no free slots minus item"); + return false; + } + } else { + debug!( + "can_swap = false, tried to swap into non-existent inventory slot: {:?}", + inv_slot_id + ); + return false; + } + + true } } impl Component for Inventory { - type Storage = HashMapStorage; + type Storage = DerefFlaggedStorage>; } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -306,5 +669,3 @@ impl InventoryUpdate { impl Component for InventoryUpdate { type Storage = IdvStorage; } - -#[cfg(test)] mod test; diff --git a/common/src/comp/inventory/slot.rs b/common/src/comp/inventory/slot.rs index 1d55dff848..4a5ad3b97a 100644 --- a/common/src/comp/inventory/slot.rs +++ b/common/src/comp/inventory/slot.rs @@ -1,21 +1,84 @@ -use crate::{ - comp, - comp::{ - item::{self, armor, tool::AbilityMap}, - ItemConfig, - }, -}; -use comp::{Inventory, Loadout}; use serde::{Deserialize, Serialize}; -use tracing::warn; +use std::{cmp::Ordering, convert::TryFrom}; -#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] -pub enum Slot { - Inventory(usize), - Equip(EquipSlot), +use crate::comp::{ + inventory::{ + item::{armor, armor::ArmorKind, ItemKind}, + loadout::LoadoutSlotId, + }, + item, +}; + +#[derive(Debug, PartialEq)] +pub enum SlotError { + InventoryFull, } #[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] +pub enum Slot { + Inventory(InvSlotId), + Equip(EquipSlot), +} + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct InvSlotId { + // The index of the loadout item that provides this inventory slot. 0 represents + // built-in inventory slots + loadout_idx: u16, + // The index of the slot within its container + slot_idx: u16, +} + +impl InvSlotId { + pub const fn new(loadout_idx: u16, slot_idx: u16) -> Self { + Self { + loadout_idx, + slot_idx, + } + } + + pub fn idx(&self) -> u32 { (u32::from(self.loadout_idx) << 16) | u32::from(self.slot_idx) } + + pub fn loadout_idx(&self) -> usize { usize::from(self.loadout_idx) } + + pub fn slot_idx(&self) -> usize { usize::from(self.slot_idx) } +} + +impl From for InvSlotId { + fn from(loadout_slot_id: LoadoutSlotId) -> Self { + Self { + loadout_idx: u16::try_from(loadout_slot_id.loadout_idx + 1).unwrap(), + slot_idx: u16::try_from(loadout_slot_id.slot_idx).unwrap(), + } + } +} + +impl PartialOrd for InvSlotId { + fn partial_cmp(&self, other: &InvSlotId) -> Option { Some(self.cmp(other)) } +} + +impl Ord for InvSlotId { + fn cmp(&self, other: &InvSlotId) -> Ordering { self.idx().cmp(&other.idx()) } +} + +pub(super) enum SlotId { + Inventory(usize), + Loadout(LoadoutSlotId), +} + +impl From for SlotId { + fn from(inv_slot_id: InvSlotId) -> Self { + match inv_slot_id.loadout_idx { + 0 => SlotId::Inventory(inv_slot_id.slot_idx()), + _ => SlotId::Loadout(LoadoutSlotId { + loadout_idx: inv_slot_id.loadout_idx() - 1, + slot_idx: inv_slot_id.slot_idx(), + }), + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, Serialize, Deserialize)] pub enum EquipSlot { Armor(ArmorSlot), Mainhand, @@ -24,25 +87,26 @@ pub enum EquipSlot { Glider, } -#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, Serialize, Deserialize)] pub enum ArmorSlot { Head, Neck, Shoulders, Chest, Hands, - Ring, + Ring1, + Ring2, Back, Belt, Legs, Feet, Tabard, + Bag1, + Bag2, + Bag3, + Bag4, } -//const ALL_ARMOR_SLOTS: [ArmorSlot; 11] = [ -// Head, Neck, Shoulders, Chest, Hands, Ring, Back, Belt, Legs, Feet, Tabard, -//]; - impl Slot { pub fn can_hold(self, item_kind: &item::ItemKind) -> bool { match (self, item_kind) { @@ -53,11 +117,9 @@ impl Slot { } impl EquipSlot { - fn can_hold(self, item_kind: &item::ItemKind) -> bool { - use armor::Armor; - use item::ItemKind; + pub fn can_hold(self, item_kind: &item::ItemKind) -> bool { match (self, item_kind) { - (Self::Armor(slot), ItemKind::Armor(Armor { kind, .. })) => slot.can_hold(kind), + (Self::Armor(slot), ItemKind::Armor(armor::Armor { kind, .. })) => slot.can_hold(kind), (Self::Mainhand, ItemKind::Tool(_)) => true, (Self::Offhand, ItemKind::Tool(_)) => true, (Self::Lantern, ItemKind::Lantern(_)) => true, @@ -69,7 +131,6 @@ impl EquipSlot { impl ArmorSlot { fn can_hold(self, armor: &item::armor::ArmorKind) -> bool { - use item::armor::ArmorKind; matches!( (self, armor), (Self::Head, ArmorKind::Head(_)) @@ -77,414 +138,17 @@ impl ArmorSlot { | (Self::Shoulders, ArmorKind::Shoulder(_)) | (Self::Chest, ArmorKind::Chest(_)) | (Self::Hands, ArmorKind::Hand(_)) - | (Self::Ring, ArmorKind::Ring(_)) + | (Self::Ring1, ArmorKind::Ring(_)) + | (Self::Ring2, ArmorKind::Ring(_)) | (Self::Back, ArmorKind::Back(_)) | (Self::Belt, ArmorKind::Belt(_)) | (Self::Legs, ArmorKind::Pants(_)) | (Self::Feet, ArmorKind::Foot(_)) | (Self::Tabard, ArmorKind::Tabard(_)) + | (Self::Bag1, ArmorKind::Bag(_)) + | (Self::Bag2, ArmorKind::Bag(_)) + | (Self::Bag3, ArmorKind::Bag(_)) + | (Self::Bag4, ArmorKind::Bag(_)) ) } } - -/// Replace an equipment slot with an item. Return the item that was in the -/// slot, if any. Doesn't update the inventory. -fn loadout_replace( - equip_slot: EquipSlot, - item: Option, - loadout: &mut Loadout, - map: &AbilityMap, -) -> Option { - use std::mem::replace; - match equip_slot { - EquipSlot::Armor(ArmorSlot::Head) => replace(&mut loadout.head, item), - EquipSlot::Armor(ArmorSlot::Neck) => replace(&mut loadout.neck, item), - EquipSlot::Armor(ArmorSlot::Shoulders) => replace(&mut loadout.shoulder, item), - EquipSlot::Armor(ArmorSlot::Chest) => replace(&mut loadout.chest, item), - EquipSlot::Armor(ArmorSlot::Hands) => replace(&mut loadout.hand, item), - EquipSlot::Armor(ArmorSlot::Ring) => replace(&mut loadout.ring, item), - EquipSlot::Armor(ArmorSlot::Back) => replace(&mut loadout.back, item), - EquipSlot::Armor(ArmorSlot::Belt) => replace(&mut loadout.belt, item), - EquipSlot::Armor(ArmorSlot::Legs) => replace(&mut loadout.pants, item), - EquipSlot::Armor(ArmorSlot::Feet) => replace(&mut loadout.foot, item), - EquipSlot::Armor(ArmorSlot::Tabard) => replace(&mut loadout.tabard, item), - EquipSlot::Lantern => replace(&mut loadout.lantern, item), - EquipSlot::Glider => replace(&mut loadout.glider, item), - EquipSlot::Mainhand => replace( - &mut loadout.active_item, - item.map(|item| ItemConfig::from((item, map))), - ) - .map(|i| i.item), - EquipSlot::Offhand => replace( - &mut loadout.second_item, - item.map(|item| ItemConfig::from((item, map))), - ) - .map(|i| i.item), - } -} - -/// Insert an item into a loadout. If the specified slot is already occupied -/// the old item is returned. -#[must_use] -fn loadout_insert( - equip_slot: EquipSlot, - item: item::Item, - loadout: &mut Loadout, - map: &AbilityMap, -) -> Option { - loadout_replace(equip_slot, Some(item), loadout, map) -} - -/// Remove an item from a loadout. -/// -/// ``` -/// use veloren_common::{ -/// assets::AssetExt, -/// comp::{ -/// item::tool::AbilityMap, -/// slot::{loadout_remove, EquipSlot}, -/// Inventory, -/// }, -/// LoadoutBuilder, -/// }; -/// -/// let mut inv = Inventory::new_empty(); -/// -/// let map = AbilityMap::load_expect_cloned("common.abilities.weapon_ability_manifest"); -/// -/// let mut loadout = LoadoutBuilder::new() -/// .defaults() -/// .active_item(Some(LoadoutBuilder::default_item_config_from_str( -/// "common.items.weapons.sword.zweihander_sword_0", -/// &map, -/// ))) -/// .build(); -/// -/// let slot = EquipSlot::Mainhand; -/// -/// loadout_remove(slot, &mut loadout, &map); -/// assert_eq!(None, loadout.active_item); -/// ``` -pub fn loadout_remove( - equip_slot: EquipSlot, - loadout: &mut Loadout, - map: &AbilityMap, -) -> Option { - loadout_replace(equip_slot, None, loadout, map) -} - -/// Swap item in an inventory slot with one in a loadout slot. -fn swap_inventory_loadout( - inventory_slot: usize, - equip_slot: EquipSlot, - inventory: &mut Inventory, - loadout: &mut Loadout, - map: &AbilityMap, -) { - // Check if loadout slot can hold item - if inventory - .get(inventory_slot) - .map_or(true, |item| equip_slot.can_hold(&item.kind())) - { - // Take item from loadout - let from_equip = loadout_remove(equip_slot, loadout, map); - // Swap with item in the inventory - let from_inv = if let Some(item) = from_equip { - // If this fails and we get item back as an err it will just be put back in the - // loadout - inventory.insert(inventory_slot, item).unwrap_or_else(Some) - } else { - inventory.remove(inventory_slot) - }; - // Put item from the inventory in loadout - if let Some(item) = from_inv { - loadout_insert(equip_slot, item, loadout, map).unwrap_none(); // Can never fail - } - } -} - -/// Swap items in loadout. Does nothing if items are not compatible with their -/// new slots. -fn swap_loadout(slot_a: EquipSlot, slot_b: EquipSlot, loadout: &mut Loadout, map: &AbilityMap) { - // Ensure that the slots are not the same - if slot_a == slot_b { - warn!("Tried to swap equip slot with itself"); - return; - } - - // Get items from the slots - let item_a = loadout_remove(slot_a, loadout, map); - let item_b = loadout_remove(slot_b, loadout, map); - // Check if items can go in the other slots - if item_a.as_ref().map_or(true, |i| slot_b.can_hold(&i.kind())) - && item_b.as_ref().map_or(true, |i| slot_a.can_hold(&i.kind())) - { - // Swap - loadout_replace(slot_b, item_a, loadout, map).unwrap_none(); - loadout_replace(slot_a, item_b, loadout, map).unwrap_none(); - } else { - // Otherwise put the items back - loadout_replace(slot_a, item_a, loadout, map).unwrap_none(); - loadout_replace(slot_b, item_b, loadout, map).unwrap_none(); - } -} - -// TODO: Should this report if a change actually occurred? (might be useful when -// minimizing network use) - -/// Swap items from two slots, regardless of if either is inventory or loadout. -pub fn swap( - slot_a: Slot, - slot_b: Slot, - inventory: Option<&mut Inventory>, - loadout: Option<&mut Loadout>, - map: &AbilityMap, -) { - match (slot_a, slot_b) { - (Slot::Inventory(slot_a), Slot::Inventory(slot_b)) => { - inventory.map(|i| i.swap_slots(slot_a, slot_b)); - }, - (Slot::Inventory(inv_slot), Slot::Equip(equip_slot)) - | (Slot::Equip(equip_slot), Slot::Inventory(inv_slot)) => { - if let Some((inventory, loadout)) = loadout.and_then(|l| inventory.map(|i| (i, l))) { - swap_inventory_loadout(inv_slot, equip_slot, inventory, loadout, map); - } - }, - - (Slot::Equip(slot_a), Slot::Equip(slot_b)) => { - loadout.map(|l| swap_loadout(slot_a, slot_b, l, map)); - }, - } -} - -/// Equip an item from a slot in inventory. The currently equipped item will go -/// into inventory. If the item is going to mainhand, put mainhand in -/// offhand and place offhand into inventory. -/// -/// ``` -/// use veloren_common::{ -/// assets::AssetExt, -/// comp::{ -/// item::tool::AbilityMap, -/// slot::{equip, EquipSlot}, -/// Inventory, Item, -/// }, -/// LoadoutBuilder, -/// }; -/// -/// let boots = Item::new_from_asset_expect("common.items.testing.test_boots"); -/// -/// let mut inv = Inventory::new_empty(); -/// inv.push(boots.duplicate()); -/// -/// let mut loadout = LoadoutBuilder::new().defaults().build(); -/// -/// let map = AbilityMap::load_expect_cloned("common.abilities.weapon_ability_manifest"); -/// -/// equip(0, &mut inv, &mut loadout, &map); -/// assert_eq!(Some(boots), loadout.foot); -/// ``` -pub fn equip(slot: usize, inventory: &mut Inventory, loadout: &mut Loadout, map: &AbilityMap) { - use armor::Armor; - use item::{armor::ArmorKind, ItemKind}; - - let equip_slot = inventory.get(slot).and_then(|i| match &i.kind() { - ItemKind::Tool(_) => Some(EquipSlot::Mainhand), - ItemKind::Armor(Armor { kind, .. }) => Some(EquipSlot::Armor(match kind { - ArmorKind::Head(_) => ArmorSlot::Head, - ArmorKind::Neck(_) => ArmorSlot::Neck, - ArmorKind::Shoulder(_) => ArmorSlot::Shoulders, - ArmorKind::Chest(_) => ArmorSlot::Chest, - ArmorKind::Hand(_) => ArmorSlot::Hands, - ArmorKind::Ring(_) => ArmorSlot::Ring, - ArmorKind::Back(_) => ArmorSlot::Back, - ArmorKind::Belt(_) => ArmorSlot::Belt, - ArmorKind::Pants(_) => ArmorSlot::Legs, - ArmorKind::Foot(_) => ArmorSlot::Feet, - ArmorKind::Tabard(_) => ArmorSlot::Tabard, - })), - ItemKind::Lantern(_) => Some(EquipSlot::Lantern), - ItemKind::Glider(_) => Some(EquipSlot::Glider), - _ => None, - }); - - if let Some(equip_slot) = equip_slot { - // If item is going to mainhand, put mainhand in offhand and place offhand in - // inventory - if let EquipSlot::Mainhand = equip_slot { - swap_loadout(EquipSlot::Mainhand, EquipSlot::Offhand, loadout, map); - } - - swap_inventory_loadout(slot, equip_slot, inventory, loadout, map); - } -} - -/// Unequip an item from slot and place into inventory. Will leave the item -/// equipped if inventory has no slots available. -/// -/// ``` -/// use veloren_common::{ -/// assets::AssetExt, -/// comp::{ -/// item::tool::AbilityMap, -/// slot::{unequip, EquipSlot}, -/// Inventory, -/// }, -/// LoadoutBuilder, -/// }; -/// -/// let mut inv = Inventory::new_empty(); -/// -/// let map = AbilityMap::load_expect_cloned("common.abilities.weapon_ability_manifest"); -/// -/// let mut loadout = LoadoutBuilder::new() -/// .defaults() -/// .active_item(Some(LoadoutBuilder::default_item_config_from_str( -/// "common.items.weapons.sword.zweihander_sword_0", -/// &map, -/// ))) -/// .build(); -/// -/// let slot = EquipSlot::Mainhand; -/// -/// unequip(slot, &mut inv, &mut loadout, &map); -/// assert_eq!(None, loadout.active_item); -/// ``` -pub fn unequip( - slot: EquipSlot, - inventory: &mut Inventory, - loadout: &mut Loadout, - map: &AbilityMap, -) { - loadout_remove(slot, loadout, map) // Remove item from loadout - .and_then(|i| inventory.push(i)) // Insert into inventory - .and_then(|i| loadout_insert(slot, i, loadout, map)) // If that fails put back in loadout - .unwrap_none(); // Never fails -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{assets::AssetExt, comp::Item, LoadoutBuilder}; - - #[test] - fn test_unequip_items_both_hands() { - let mut inv = Inventory { - slots: vec![None], - amount: 0, - }; - - let map = AbilityMap::load_expect_cloned("common.abilities.weapon_ability_manifest"); - - let sword = LoadoutBuilder::default_item_config_from_str( - "common.items.weapons.sword.zweihander_sword_0", - &map, - ); - - let mut loadout = LoadoutBuilder::new() - .defaults() - .active_item(Some(sword.clone())) - .second_item(Some(sword.clone())) - .build(); - - assert_eq!(Some(sword.clone()), loadout.active_item); - unequip(EquipSlot::Mainhand, &mut inv, &mut loadout, &map); - // We have space in the inventory, so this should have unequipped - assert_eq!(None, loadout.active_item); - - unequip(EquipSlot::Offhand, &mut inv, &mut loadout, &map); - // There is no more space in the inventory, so this should still be equipped - assert_eq!(Some(sword.clone()), loadout.second_item); - - // Verify inventory - assert_eq!(inv.slots[0], Some(sword.item)); - assert_eq!(inv.slots.len(), 1); - } - - #[test] - fn test_equip_item() { - let boots: Option = Some(Item::new_from_asset_expect( - "common.items.testing.test_boots", - )); - - let starting_sandles: Option = Some(Item::new_from_asset_expect( - "common.items.armor.starter.sandals_0", - )); - - let mut inv = Inventory { - slots: vec![boots.clone()], - amount: 1, - }; - - let mut loadout = LoadoutBuilder::new().defaults().build(); - - let map = AbilityMap::load_expect_cloned("common.abilities.weapon_ability_manifest"); - - // We should start with the starting sandles - assert_eq!(starting_sandles, loadout.foot); - equip(0, &mut inv, &mut loadout, &map); - - // We should now have the testing boots equiped - assert_eq!(boots, loadout.foot); - - // Verify inventory - assert_eq!(inv.slots[0], starting_sandles); - assert_eq!(inv.slots.len(), 1); - } - - #[test] - fn test_loadout_replace() { - let boots: Option = Some(Item::new_from_asset_expect( - "common.items.testing.test_boots", - )); - - let starting_sandles: Option = Some(Item::new_from_asset_expect( - "common.items.armor.starter.sandals_0", - )); - - let map = AbilityMap::load_expect_cloned("common.abilities.weapon_ability_manifest"); - - let mut loadout = LoadoutBuilder::new().defaults().build(); - - // We should start with the starting sandles - assert_eq!(starting_sandles, loadout.foot); - - // The swap should return the sandles - assert_eq!( - starting_sandles, - loadout_replace( - EquipSlot::Armor(ArmorSlot::Feet), - boots.clone(), - &mut loadout, - &map, - ) - ); - - // We should now have the testing boots equiped - assert_eq!(boots, loadout.foot); - } - - #[test] - fn test_loadout_remove() { - let map = AbilityMap::load_expect_cloned("common.abilities.weapon_ability_manifest"); - - let sword = LoadoutBuilder::default_item_config_from_str( - "common.items.weapons.sword.zweihander_sword_0", - &map, - ); - - let mut loadout = LoadoutBuilder::new() - .defaults() - .active_item(Some(sword.clone())) - .build(); - - // The swap should return the sword - assert_eq!( - Some(sword.item), - loadout_remove(EquipSlot::Mainhand, &mut loadout, &map) - ); - - // We should now have nothing equiped - assert_eq!(None, loadout.active_item); - } -} diff --git a/common/src/comp/inventory/test.rs b/common/src/comp/inventory/test.rs index 0b6aae5464..26f9d9abce 100644 --- a/common/src/comp/inventory/test.rs +++ b/common/src/comp/inventory/test.rs @@ -1,4 +1,8 @@ use super::*; +use crate::comp::{ + inventory::{slot::ArmorSlot, test_helpers::get_test_bag}, + Item, +}; use lazy_static::lazy_static; lazy_static! { static ref TEST_ITEMS: Vec = vec![ @@ -12,7 +16,7 @@ lazy_static! { fn push_full() { let mut inv = Inventory { slots: TEST_ITEMS.iter().map(|a| Some(a.clone())).collect(), - amount: 0, + loadout: LoadoutBuilder::new().build(), }; assert_eq!( inv.push(TEST_ITEMS[0].clone()).unwrap(), @@ -25,7 +29,7 @@ fn push_full() { fn push_all_full() { let mut inv = Inventory { slots: TEST_ITEMS.iter().map(|a| Some(a.clone())).collect(), - amount: 0, + loadout: LoadoutBuilder::new().build(), }; let Error::Full(leftovers) = inv .push_all(TEST_ITEMS.iter().cloned()) @@ -39,7 +43,7 @@ fn push_all_full() { fn push_unique_all_full() { let mut inv = Inventory { slots: TEST_ITEMS.iter().map(|a| Some(a.clone())).collect(), - amount: 0, + loadout: LoadoutBuilder::new().build(), }; inv.push_all_unique(TEST_ITEMS.iter().cloned()) .expect("Pushing unique items into an inventory that already contains them didn't work!"); @@ -51,7 +55,7 @@ fn push_unique_all_full() { fn push_all_empty() { let mut inv = Inventory { slots: vec![None, None], - amount: 0, + loadout: LoadoutBuilder::new().build(), }; inv.push_all(TEST_ITEMS.iter().cloned()) .expect("Pushing items into an empty inventory didn't work!"); @@ -63,9 +67,324 @@ fn push_all_empty() { fn push_all_unique_empty() { let mut inv = Inventory { slots: vec![None, None], - amount: 0, + loadout: LoadoutBuilder::new().build(), }; inv.push_all_unique(TEST_ITEMS.iter().cloned()).expect( "Pushing unique items into an empty inventory that didn't contain them didn't work!", ); } + +#[test] +fn free_slots_minus_equipped_item_items_only_present_in_equipped_bag_slots() { + let mut inv = Inventory::new_empty(); + + let bag = get_test_bag(18); + let bag1_slot = EquipSlot::Armor(ArmorSlot::Bag1); + inv.loadout.swap(bag1_slot, Some(bag.clone())); + + inv.insert_at(InvSlotId::new(15, 0), bag) + .unwrap() + .unwrap_none(); + + let result = inv.free_slots_minus_equipped_item(bag1_slot); + + // All of the base inventory slots are empty and the equipped bag slots are + // ignored + assert_eq!(18, result); +} + +#[test] +fn free_slots_minus_equipped_item() { + let mut inv = Inventory::new_empty(); + + let bag = get_test_bag(18); + let bag1_slot = EquipSlot::Armor(ArmorSlot::Bag1); + inv.loadout.swap(bag1_slot, Some(bag.clone())); + inv.loadout + .swap(EquipSlot::Armor(ArmorSlot::Bag2), Some(bag.clone())); + + inv.insert_at(InvSlotId::new(16, 0), bag) + .unwrap() + .unwrap_none(); + + let result = inv.free_slots_minus_equipped_item(bag1_slot); + + // All of the base 18 inventory slots are empty, the first equipped bag is + // ignored, and the second equipped bag has 17 free slots + assert_eq!(35, result); +} + +#[test] +fn get_slot_range_for_equip_slot() { + let mut inv = Inventory::new_empty(); + let bag = get_test_bag(18); + let bag1_slot = EquipSlot::Armor(ArmorSlot::Bag1); + inv.loadout.swap(bag1_slot, Some(bag)); + + let result = inv.get_slot_range_for_equip_slot(bag1_slot).unwrap(); + + assert_eq!(18..36, result); +} + +#[test] +fn can_swap_equipped_bag_into_empty_inv_slot_1_free_slot() { + can_swap_equipped_bag_into_empty_inv_slot(1, InvSlotId::new(0, 17), true); +} + +#[test] +fn can_swap_equipped_bag_into_empty_inv_slot_0_free_slots() { + can_swap_equipped_bag_into_empty_inv_slot(0, InvSlotId::new(0, 17), false); +} + +#[test] +fn can_swap_equipped_bag_into_empty_inv_slot_provided_by_equipped_bag() { + can_swap_equipped_bag_into_empty_inv_slot(1, InvSlotId::new(15, 0), true); +} + +fn can_swap_equipped_bag_into_empty_inv_slot( + free_slots: u16, + inv_slot_id: InvSlotId, + expected_result: bool, +) { + let mut inv = Inventory::new_empty(); + + inv.replace_loadout_item(EquipSlot::Armor(ArmorSlot::Bag1), Some(get_test_bag(18))); + + fill_inv_slots(&mut inv, 18 - free_slots); + + let result = inv.can_swap(inv_slot_id, EquipSlot::Armor(ArmorSlot::Bag1)); + + assert_eq!(expected_result, result); +} + +#[test] +fn can_swap_equipped_bag_into_only_empty_slot_provided_by_itself_should_return_false() { + let mut inv = Inventory::new_empty(); + + inv.replace_loadout_item(EquipSlot::Armor(ArmorSlot::Bag1), Some(get_test_bag(18))); + + fill_inv_slots(&mut inv, 35); + + let result = inv.can_swap(InvSlotId::new(15, 17), EquipSlot::Armor(ArmorSlot::Bag1)); + + assert_eq!(false, result); +} + +#[test] +fn unequip_items_both_hands() { + let mut inv = Inventory::new_empty(); + + let sword = Item::new_from_asset_expect("common.items.weapons.sword.zweihander_sword_0"); + + inv.replace_loadout_item(EquipSlot::Mainhand, Some(sword.clone())); + inv.replace_loadout_item(EquipSlot::Offhand, Some(sword.clone())); + + // Fill all inventory slots except one + fill_inv_slots(&mut inv, 17); + + let result = inv.unequip(EquipSlot::Mainhand); + // We have space in the inventory, so this should have unequipped + assert_eq!(None, inv.equipped(EquipSlot::Mainhand)); + assert_eq!(18, inv.populated_slots()); + assert_eq!(true, result.is_ok()); + + let result = inv.unequip(EquipSlot::Offhand).unwrap_err(); + assert_eq!(SlotError::InventoryFull, result); + + // There is no more space in the inventory, so this should still be equipped + assert_eq!(&sword, inv.equipped(EquipSlot::Offhand).unwrap()); + + // Verify inventory + assert_eq!(inv.slots[17], Some(sword)); + assert_eq!(inv.free_slots(), 0); +} + +#[test] +fn equip_replace_already_equipped_item() { + let boots = Item::new_from_asset_expect("common.items.testing.test_boots"); + + let starting_sandles = Some(Item::new_from_asset_expect( + "common.items.armor.starter.sandals_0", + )); + + let mut inv = Inventory::new_empty(); + inv.push(boots.clone()); + inv.replace_loadout_item(EquipSlot::Armor(ArmorSlot::Feet), starting_sandles.clone()); + + inv.equip(InvSlotId::new(0, 0)).unwrap_none(); + + // We should now have the testing boots equipped + assert_eq!( + &boots, + inv.equipped(EquipSlot::Armor(ArmorSlot::Feet)).unwrap() + ); + + // Verify inventory + assert_eq!( + inv.slots[0].as_ref().unwrap().item_definition_id(), + starting_sandles.unwrap().item_definition_id() + ); + assert_eq!(inv.populated_slots(), 1); +} + +/// Regression test for a panic that occurred when swapping an equipped bag +/// for a bag that exists in an inventory slot that will no longer exist +/// after equipping it (because the equipped bag is larger) +#[test] +fn equip_equipping_smaller_bag_from_last_slot_of_big_bag() { + let mut inv = Inventory::new_empty(); + + const LARGE_BAG_ID: &str = "common.items.testing.test_bag_18_slot"; + let small_bag = get_test_bag(9); + let large_bag = Item::new_from_asset_expect(LARGE_BAG_ID); + + inv.loadout + .swap(EquipSlot::Armor(ArmorSlot::Bag1), Some(large_bag)) + .unwrap_none(); + + inv.insert_at(InvSlotId::new(15, 15), small_bag).unwrap(); + + let result = inv.swap( + Slot::Equip(EquipSlot::Armor(ArmorSlot::Bag1)), + Slot::Inventory(InvSlotId::new(15, 15)), + ); + + assert_eq!( + inv.get(InvSlotId::new(0, 0)).unwrap().item_definition_id(), + LARGE_BAG_ID + ); + assert_eq!(result, None); +} + +#[test] +fn unequip_unequipping_bag_into_its_own_slot_with_no_other_free_slots() { + let mut inv = Inventory::new_empty(); + let bag = get_test_bag(9); + + inv.loadout + .swap(EquipSlot::Armor(ArmorSlot::Bag1), Some(bag)) + .unwrap_none(); + + // Fill all inventory built-in slots + fill_inv_slots(&mut inv, 18); + + inv.swap_inventory_loadout(InvSlotId::new(15, 0), EquipSlot::Armor(ArmorSlot::Bag1)) + .unwrap_none(); +} + +#[test] +fn equip_one_bag_equipped_equip_second_bag() { + let mut inv = Inventory::new_empty(); + + let bag = get_test_bag(9); + inv.loadout + .swap(EquipSlot::Armor(ArmorSlot::Bag1), Some(bag.clone())) + .unwrap_none(); + + inv.push(bag); + + inv.equip(InvSlotId::new(0, 0)).unwrap_none(); + + assert_eq!( + true, + inv.equipped(EquipSlot::Armor(ArmorSlot::Bag2)).is_some() + ); +} + +#[test] +fn free_after_swap_equipped_item_has_more_slots() { + let mut inv = Inventory::new_empty(); + + let bag = get_test_bag(18); + inv.loadout + .swap(EquipSlot::Armor(ArmorSlot::Bag1), Some(bag)) + .unwrap_none(); + + let small_bag = get_test_bag(9); + inv.push(small_bag); + + // Fill all remaining slots + fill_inv_slots(&mut inv, 35); + + let result = inv.free_after_swap(EquipSlot::Armor(ArmorSlot::Bag1), InvSlotId::new(0, 0)); + + // 18 inv slots + 9 bag slots - 36 used slots - + assert_eq!(-9, result); +} + +#[test] +fn free_after_swap_equipped_item_has_less_slots() { + let mut inv = Inventory::new_empty(); + + let bag = get_test_bag(9); + inv.loadout + .swap(EquipSlot::Armor(ArmorSlot::Bag1), Some(bag)) + .unwrap_none(); + + let small_bag = get_test_bag(18); + inv.push(small_bag); + + // Fill all slots except the last one + fill_inv_slots(&mut inv, 27); + + let result = inv.free_after_swap(EquipSlot::Armor(ArmorSlot::Bag1), InvSlotId::new(0, 0)); + + // 18 inv slots + 18 bag slots - 27 used slots + assert_eq!(9, result); +} + +#[test] +fn free_after_swap_equipped_item_with_slots_swapped_with_empty_inv_slot() { + let mut inv = Inventory::new_empty(); + + let bag = get_test_bag(9); + inv.loadout + .swap(EquipSlot::Armor(ArmorSlot::Bag1), Some(bag)) + .unwrap_none(); + + // Add 5 items to the inventory + fill_inv_slots(&mut inv, 5); + + let result = inv.free_after_swap(EquipSlot::Armor(ArmorSlot::Bag1), InvSlotId::new(0, 10)); + + // 18 inv slots - 5 used slots - 1 slot for unequipped item + assert_eq!(12, result); +} + +#[test] +fn free_after_swap_inv_item_with_slots_swapped_with_empty_equip_slot() { + let mut inv = Inventory::new_empty(); + + inv.push(get_test_bag(9)); + + // Add 5 items to the inventory + fill_inv_slots(&mut inv, 5); + + let result = inv.free_after_swap(EquipSlot::Armor(ArmorSlot::Bag1), InvSlotId::new(0, 0)); + + // 18 inv slots + 9 bag slots - 5 used slots + assert_eq!(22, result); +} + +#[test] +fn free_after_swap_inv_item_without_slots_swapped_with_empty_equip_slot() { + let mut inv = Inventory::new_empty(); + + let boots = Item::new_from_asset_expect("common.items.testing.test_boots"); + inv.push(boots); + + // Add 5 items to the inventory + fill_inv_slots(&mut inv, 5); + + let result = inv.free_after_swap(EquipSlot::Armor(ArmorSlot::Feet), InvSlotId::new(0, 0)); + + // 18 inv slots - 5 used slots + assert_eq!(13, result); +} + +fn fill_inv_slots(inv: &mut Inventory, items: u16) { + let boots = Item::new_from_asset_expect("common.items.testing.test_boots"); + for _ in 0..items { + inv.push(boots.clone()); + } +} diff --git a/common/src/comp/inventory/test_helpers.rs b/common/src/comp/inventory/test_helpers.rs new file mode 100644 index 0000000000..651d763081 --- /dev/null +++ b/common/src/comp/inventory/test_helpers.rs @@ -0,0 +1,24 @@ +use crate::comp::{ + inventory::item::{ + armor, + armor::{ArmorKind, Protection}, + ItemDef, ItemKind, Quality, + }, + Item, +}; +use std::sync::Arc; + +pub(super) fn get_test_bag(slots: u16) -> Item { + let item_def = ItemDef::new_test( + "common.items.testing.test_bag".to_string(), + None, + ItemKind::Armor(armor::Armor::test_armor( + ArmorKind::Bag("Test Bag".to_string()), + Protection::Normal(0.0), + )), + Quality::Common, + slots, + ); + + Item::new_from_item_def(Arc::new(item_def)) +} diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index 22ee92c660..a16b104c79 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -13,7 +13,7 @@ pub mod group; mod health; pub mod home_chunk; mod inputs; -mod inventory; +pub mod inventory; mod last; mod location; mod misc; @@ -26,7 +26,7 @@ mod stats; pub mod visual; // Reexports -pub use ability::{CharacterAbility, CharacterAbilityType, ItemConfig, Loadout}; +pub use ability::{CharacterAbility, CharacterAbilityType}; pub use admin::Admin; pub use agent::{Agent, Alignment}; pub use aura::{Aura, AuraChange, AuraKind, Auras}; @@ -54,7 +54,7 @@ pub use home_chunk::HomeChunk; pub use inputs::CanBuild; pub use inventory::{ item, - item::{Item, ItemDrop}, + item::{Item, ItemConfig, ItemDrop}, slot, Inventory, InventoryUpdate, InventoryUpdateEvent, }; pub use last::Last; diff --git a/common/src/event.rs b/common/src/event.rs index 85b29aac5a..a4f13a97b8 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -88,7 +88,6 @@ pub enum ServerEvent { comp::Body, comp::Stats, comp::Inventory, - comp::Loadout, Option, ), }, @@ -100,7 +99,7 @@ pub enum ServerEvent { pos: comp::Pos, stats: comp::Stats, health: comp::Health, - loadout: comp::Loadout, + loadout: comp::inventory::loadout::Loadout, body: comp::Body, agent: Option, alignment: comp::Alignment, diff --git a/common/src/generation.rs b/common/src/generation.rs index 7f163f8224..43d22b02fb 100644 --- a/common/src/generation.rs +++ b/common/src/generation.rs @@ -1,6 +1,5 @@ use crate::{ - comp::{self, humanoid, Alignment, Body, Item}, - loadout_builder::LoadoutConfig, + comp::{self, humanoid, inventory::loadout_builder::LoadoutConfig, Alignment, Body, Item}, npc::{self, NPC_NAMES}, }; use vek::*; diff --git a/common/src/lib.rs b/common/src/lib.rs index 3550085ab8..f762badc07 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -6,15 +6,17 @@ #![feature( arbitrary_enum_discriminant, associated_type_defaults, + bool_to_option, const_checked_int_methods, const_generics, fundamental, - option_unwrap_none, - bool_to_option, + iter_map_while, label_break_value, + option_expect_none, + option_unwrap_none, + option_zip, trait_alias, - type_alias_impl_trait, - option_zip + type_alias_impl_trait )] pub mod assets; @@ -31,7 +33,6 @@ pub mod explosion; pub mod figure; pub mod generation; pub mod grid; -pub mod loadout_builder; pub mod lottery; pub mod metrics; pub mod npc; @@ -54,5 +55,5 @@ pub mod vol; pub mod volumes; pub use combat::{Damage, DamageSource, GroupTarget, Knockback}; +pub use comp::inventory::loadout_builder::LoadoutBuilder; pub use explosion::{Explosion, RadiusEffect}; -pub use loadout_builder::LoadoutBuilder; diff --git a/common/src/recipe.rs b/common/src/recipe.rs index 24ae29396f..de712ab227 100644 --- a/common/src/recipe.rs +++ b/common/src/recipe.rs @@ -22,15 +22,14 @@ impl Recipe { // Get ingredient cells from inventory, inv.contains_ingredients(self)? .into_iter() - .enumerate() - .for_each(|(i, n)| { + .for_each(|(pos, n)| { (0..n).for_each(|_| { - inv.take(i).expect("Expected item to exist in inventory"); + inv.take(pos).expect("Expected item to exist in inventory"); }) }); for i in 0..self.output.1 { - let crafted_item = Item::new(Arc::clone(&self.output.0)); + let crafted_item = Item::new_from_item_def(Arc::clone(&self.output.0)); if let Some(item) = inv.push(crafted_item) { return Ok(Some((item, self.output.1 - i))); } diff --git a/common/src/states/behavior.rs b/common/src/states/behavior.rs index 0af73ec436..8ec1837d32 100644 --- a/common/src/states/behavior.rs +++ b/common/src/states/behavior.rs @@ -1,7 +1,7 @@ use crate::{ comp::{ Attacking, Beam, Body, CharacterState, ControlAction, Controller, ControllerInputs, Energy, - Health, Loadout, Ori, PhysicsState, Pos, StateUpdate, Vel, + Health, Inventory, Ori, PhysicsState, Pos, StateUpdate, Vel, }, resources::DeltaTime, uid::Uid, @@ -52,7 +52,7 @@ pub struct JoinData<'a> { pub inputs: &'a ControllerInputs, pub health: &'a Health, pub energy: &'a Energy, - pub loadout: &'a Loadout, + pub inventory: &'a Inventory, pub body: &'a Body, pub physics: &'a PhysicsState, pub attacking: Option<&'a Attacking>, @@ -76,7 +76,7 @@ pub type JoinTuple<'a> = ( &'a mut Vel, &'a mut Ori, RestrictedMut<'a, Energy>, - RestrictedMut<'a, Loadout>, + RestrictedMut<'a, Inventory>, &'a mut Controller, &'a Health, &'a Body, @@ -95,7 +95,7 @@ impl<'a> JoinData<'a> { vel: j.4, ori: j.5, energy: j.6.get_unchecked(), - loadout: j.7.get_unchecked(), + inventory: j.7.get_unchecked(), controller: j.8, inputs: &j.8.inputs, health: j.9, diff --git a/common/src/states/glide.rs b/common/src/states/glide.rs index 5fa8b267e6..2a7b729722 100644 --- a/common/src/states/glide.rs +++ b/common/src/states/glide.rs @@ -1,11 +1,12 @@ use super::utils::handle_climb; use crate::{ - comp::{CharacterState, EnergySource, StateUpdate}, + comp::{inventory::slot::EquipSlot, CharacterState, EnergySource, StateUpdate}, states::behavior::{CharacterBehavior, JoinData}, util::Dir, }; use serde::{Deserialize, Serialize}; use vek::Vec2; + // Gravity is 9.81 * 4, so this makes gravity equal to .15 const GLIDE_ANTIGRAV: f32 = crate::consts::GRAVITY * 0.90; const GLIDE_ACCEL: f32 = 12.0; @@ -31,7 +32,7 @@ impl CharacterBehavior for Data { { update.character = CharacterState::Idle; } - if data.loadout.glider.is_none() { + if data.inventory.equipped(EquipSlot::Glider).is_none() { update.character = CharacterState::Idle }; // If there is a wall in front of character and they are trying to climb go to diff --git a/common/src/states/glide_wield.rs b/common/src/states/glide_wield.rs index 170bb6f1fb..1535239e28 100644 --- a/common/src/states/glide_wield.rs +++ b/common/src/states/glide_wield.rs @@ -1,6 +1,6 @@ use super::utils::*; use crate::{ - comp::{CharacterState, StateUpdate}, + comp::{slot::EquipSlot, CharacterState, StateUpdate}, states::behavior::{CharacterBehavior, JoinData}, }; @@ -27,7 +27,7 @@ impl CharacterBehavior for Data { { update.character = CharacterState::Idle; } - if data.loadout.glider.is_none() { + if data.inventory.equipped(EquipSlot::Glider).is_none() { update.character = CharacterState::Idle }; diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index ea4f812fbd..ed89a63059 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -1,5 +1,6 @@ use crate::{ comp::{ + inventory::slot::EquipSlot, item::{Hands, ItemKind, Tool}, quadruped_low, quadruped_medium, theropod, Body, CharacterState, StateUpdate, }, @@ -290,7 +291,11 @@ pub fn handle_wield(data: &JoinData, update: &mut StateUpdate) { /// If a tool is equipped, goes into Equipping state, otherwise goes to Idle pub fn attempt_wield(data: &JoinData, update: &mut StateUpdate) { - if let Some(ItemKind::Tool(tool)) = data.loadout.active_item.as_ref().map(|i| i.item.kind()) { + if let Some(ItemKind::Tool(tool)) = data + .inventory + .equipped(EquipSlot::Mainhand) + .map(|i| i.kind()) + { update.character = CharacterState::Equipping(equipping::Data { static_data: equipping::StaticData { buildup_duration: tool.equip_time(), @@ -341,14 +346,14 @@ pub fn handle_climb(data: &JoinData, update: &mut StateUpdate) { /// Checks that player can Swap Weapons and updates `Loadout` if so pub fn attempt_swap_loadout(data: &JoinData, update: &mut StateUpdate) { - if data.loadout.second_item.is_some() { + if data.inventory.equipped(EquipSlot::Offhand).is_some() { update.swap_loadout = true; } } /// Checks that player can wield the glider and updates `CharacterState` if so pub fn attempt_glide_wield(data: &JoinData, update: &mut StateUpdate) { - if data.loadout.glider.is_some() + if data.inventory.equipped(EquipSlot::Glider).is_some() && !data .physics .in_liquid @@ -376,14 +381,12 @@ pub fn handle_jump(data: &JoinData, update: &mut StateUpdate) { } } -/// Will attempt to go into `loadout.active_item.ability1` pub fn handle_ability1_input(data: &JoinData, update: &mut StateUpdate) { if data.inputs.primary.is_pressed() { if let Some(ability) = data - .loadout - .active_item - .as_ref() - .and_then(|i| i.ability1.as_ref()) + .inventory + .equipped(EquipSlot::Mainhand) + .and_then(|i| i.item_config_expect().ability1.as_ref()) .filter(|ability| ability.requirements_paid(data, update)) { update.character = (ability, AbilityKey::Mouse1).into(); @@ -391,15 +394,22 @@ pub fn handle_ability1_input(data: &JoinData, update: &mut StateUpdate) { } } -/// Will attempt to go into `loadout.active_item.ability2` pub fn handle_ability2_input(data: &JoinData, update: &mut StateUpdate) { if data.inputs.secondary.is_pressed() { - let active_tool_kind = match data.loadout.active_item.as_ref().map(|i| i.item.kind()) { + let active_tool_kind = match data + .inventory + .equipped(EquipSlot::Mainhand) + .map(|i| i.kind()) + { Some(ItemKind::Tool(Tool { kind, .. })) => Some(kind), _ => None, }; - let second_tool_kind = match data.loadout.second_item.as_ref().map(|i| i.item.kind()) { + let second_tool_kind = match data + .inventory + .equipped(EquipSlot::Offhand) + .map(|i| i.kind()) + { Some(ItemKind::Tool(Tool { kind, .. })) => Some(kind), _ => None, }; @@ -410,10 +420,9 @@ pub fn handle_ability2_input(data: &JoinData, update: &mut StateUpdate) { ) { (Some(Hands::TwoHand), _) => { if let Some(ability) = data - .loadout - .active_item - .as_ref() - .and_then(|i| i.ability2.as_ref()) + .inventory + .equipped(EquipSlot::Mainhand) + .and_then(|i| i.item_config_expect().ability2.as_ref()) .filter(|ability| ability.requirements_paid(data, update)) { update.character = (ability, AbilityKey::Mouse2).into(); @@ -421,10 +430,9 @@ pub fn handle_ability2_input(data: &JoinData, update: &mut StateUpdate) { }, (_, Some(Hands::OneHand)) => { if let Some(ability) = data - .loadout - .second_item - .as_ref() - .and_then(|i| i.ability2.as_ref()) + .inventory + .equipped(EquipSlot::Offhand) + .and_then(|i| i.item_config_expect().ability2.as_ref()) .filter(|ability| ability.requirements_paid(data, update)) { update.character = (ability, AbilityKey::Mouse2).into(); @@ -435,14 +443,12 @@ pub fn handle_ability2_input(data: &JoinData, update: &mut StateUpdate) { } } -/// Will attempt to go into `loadout.active_item.ability3` pub fn handle_ability3_input(data: &JoinData, update: &mut StateUpdate) { if data.inputs.ability3.is_pressed() { if let Some(ability) = data - .loadout - .active_item - .as_ref() - .and_then(|i| i.ability3.as_ref()) + .inventory + .equipped(EquipSlot::Mainhand) + .and_then(|i| i.item_config_expect().ability3.as_ref()) .filter(|ability| ability.requirements_paid(data, update)) { update.character = (ability, AbilityKey::Skill1).into(); @@ -451,14 +457,13 @@ pub fn handle_ability3_input(data: &JoinData, update: &mut StateUpdate) { } /// Checks that player can perform a dodge, then -/// attempts to go into `loadout.active_item.dodge_ability` +/// attempts to perform their dodge ability pub fn handle_dodge_input(data: &JoinData, update: &mut StateUpdate) { if data.inputs.roll.is_pressed() && data.body.is_humanoid() { if let Some(ability) = data - .loadout - .active_item - .as_ref() - .and_then(|i| i.dodge_ability.as_ref()) + .inventory + .equipped(EquipSlot::Mainhand) + .and_then(|i| i.item_config_expect().dodge_ability.as_ref()) .filter(|ability| ability.requirements_paid(data, update)) { if data.character.is_wield() { @@ -479,8 +484,12 @@ pub fn handle_dodge_input(data: &JoinData, update: &mut StateUpdate) { } pub fn unwrap_tool_data<'a>(data: &'a JoinData) -> Option<&'a Tool> { - if let Some(ItemKind::Tool(tool)) = data.loadout.active_item.as_ref().map(|i| i.item.kind()) { - Some(tool) + if let Some(ItemKind::Tool(tool)) = data + .inventory + .equipped(EquipSlot::Mainhand) + .map(|i| i.kind()) + { + Some(&tool) } else { None } diff --git a/common/src/terrain/sprite.rs b/common/src/terrain/sprite.rs index 2f91924b28..aa8ed81ab9 100644 --- a/common/src/terrain/sprite.rs +++ b/common/src/terrain/sprite.rs @@ -177,7 +177,7 @@ impl SpriteKind { SpriteKind::BlueFlower => false, SpriteKind::PinkFlower => false, SpriteKind::PurpleFlower => false, - SpriteKind::RedFlower => false, + SpriteKind::RedFlower => true, SpriteKind::WhiteFlower => false, SpriteKind::YellowFlower => false, SpriteKind::Sunflower => true, diff --git a/common/sys/src/agent.rs b/common/sys/src/agent.rs index 76bc5319fd..e5a456febc 100644 --- a/common/sys/src/agent.rs +++ b/common/sys/src/agent.rs @@ -4,12 +4,13 @@ use common::{ agent::Activity, group, group::Invite, + inventory::slot::EquipSlot, item::{ tool::{ToolKind, UniqueKind}, ItemKind, }, Agent, Alignment, Body, CharacterState, ControlAction, ControlEvent, Controller, Energy, - GroupManip, Health, LightEmitter, Loadout, MountState, Ori, PhysicsState, Pos, Scale, + GroupManip, Health, Inventory, LightEmitter, MountState, Ori, PhysicsState, Pos, Scale, UnresolvedChatMsg, Vel, }, event::{EventBus, ServerEvent}, @@ -52,7 +53,7 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Ori>, ReadStorage<'a, Scale>, ReadStorage<'a, Health>, - ReadStorage<'a, Loadout>, + ReadStorage<'a, Inventory>, ReadStorage<'a, PhysicsState>, ReadStorage<'a, Uid>, ReadStorage<'a, group::Group>, @@ -82,7 +83,7 @@ impl<'a> System<'a> for Sys { orientations, scales, healths, - loadouts, + inventories, physics_states, uids, groups, @@ -108,7 +109,7 @@ impl<'a> System<'a> for Sys { &velocities, &orientations, alignments.maybe(), - &loadouts, + &inventories, &physics_states, bodies.maybe(), &uids, @@ -130,7 +131,7 @@ impl<'a> System<'a> for Sys { vel, ori, alignment, - loadout, + inventory, physics_state, body, uid, @@ -157,7 +158,7 @@ impl<'a> System<'a> for Sys { let mut event_emitter = event_bus.emitter(); // Light lanterns at night // TODO Add a method to turn on NPC lanterns underground - let lantern_equipped = loadout.lantern.as_ref().map_or(false, |item| { + let lantern_equipped = inventory.equipped(EquipSlot::Lantern).as_ref().map_or(false, |item| { matches!(item.kind(), comp::item::ItemKind::Lantern(_)) }); let lantern_turned_on = light_emitter.is_some(); @@ -370,8 +371,8 @@ impl<'a> System<'a> for Sys { Theropod, } - let tactic = match loadout.active_item.as_ref().and_then(|ic| { - if let ItemKind::Tool(tool) = &ic.item.kind() { + let tactic = match inventory.equipped(EquipSlot::Mainhand).as_ref().and_then(|item| { + if let ItemKind::Tool(tool) = &item.kind() { Some(&tool.kind) } else { None diff --git a/common/sys/src/beam.rs b/common/sys/src/beam.rs index 49844c75b7..3ab6cc6612 100644 --- a/common/sys/src/beam.rs +++ b/common/sys/src/beam.rs @@ -1,7 +1,7 @@ use common::{ comp::{ group, Beam, BeamSegment, Body, Energy, EnergyChange, EnergySource, Health, HealthChange, - HealthSource, Last, Loadout, Ori, Pos, Scale, + HealthSource, Inventory, Last, Ori, Pos, Scale, }, event::{EventBus, ServerEvent}, resources::{DeltaTime, Time}, @@ -29,7 +29,7 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Scale>, ReadStorage<'a, Body>, ReadStorage<'a, Health>, - ReadStorage<'a, Loadout>, + ReadStorage<'a, Inventory>, ReadStorage<'a, group::Group>, ReadStorage<'a, Energy>, WriteStorage<'a, BeamSegment>, @@ -51,7 +51,7 @@ impl<'a> System<'a> for Sys { scales, bodies, healths, - loadouts, + inventories, groups, energies, mut beam_segments, @@ -166,7 +166,7 @@ impl<'a> System<'a> for Sys { } // Modify damage - let change = damage.modify_damage(loadouts.get(b), beam_segment.owner); + let change = damage.modify_damage(inventories.get(b), beam_segment.owner); match target { Some(GroupTarget::OutOfGroup) => { diff --git a/common/sys/src/buff.rs b/common/sys/src/buff.rs index 3570493e98..39d66689e2 100644 --- a/common/sys/src/buff.rs +++ b/common/sys/src/buff.rs @@ -1,11 +1,11 @@ use common::{ comp::{ BuffCategory, BuffChange, BuffEffect, BuffId, BuffSource, Buffs, Health, HealthChange, - HealthSource, Loadout, ModifierKind, + HealthSource, Inventory, ModifierKind, }, event::{EventBus, ServerEvent}, resources::DeltaTime, - DamageSource, + Damage, DamageSource, }; use specs::{Entities, Join, Read, ReadStorage, System, WriteStorage}; use std::time::Duration; @@ -17,14 +17,14 @@ impl<'a> System<'a> for Sys { Entities<'a>, Read<'a, DeltaTime>, Read<'a, EventBus>, - ReadStorage<'a, Loadout>, + ReadStorage<'a, Inventory>, WriteStorage<'a, Health>, WriteStorage<'a, Buffs>, ); fn run( &mut self, - (entities, dt, server_bus, loadouts, mut healths, mut buffs): Self::SystemData, + (entities, dt, server_bus, inventories, mut healths, mut buffs): Self::SystemData, ) { let mut server_emitter = server_bus.emitter(); // Set to false to avoid spamming server @@ -51,8 +51,8 @@ impl<'a> System<'a> for Sys { } } - if let Some(loadout) = loadouts.get(entity) { - let damage_reduction = loadout.get_damage_reduction(); + if let Some(inventory) = inventories.get(entity) { + let damage_reduction = Damage::compute_damage_reduction(inventory); if (damage_reduction - 1.0).abs() < f32::EPSILON { for (id, buff) in buff_comp.buffs.iter() { if !buff.kind.is_buff() { diff --git a/common/sys/src/character_behavior.rs b/common/sys/src/character_behavior.rs index 337b49bd57..60eae9dde0 100644 --- a/common/sys/src/character_behavior.rs +++ b/common/sys/src/character_behavior.rs @@ -1,7 +1,10 @@ +use specs::{Entities, Join, LazyUpdate, Read, ReadExpect, ReadStorage, System, WriteStorage}; + use common::{ comp::{ - Attacking, Beam, Body, CharacterState, Controller, Energy, Health, Loadout, Mounting, Ori, - PhysicsState, Pos, StateUpdate, Vel, + inventory::slot::{EquipSlot, Slot}, + Attacking, Beam, Body, CharacterState, Controller, Energy, Health, Inventory, Mounting, + Ori, PhysicsState, Pos, StateUpdate, Vel, }, event::{EventBus, LocalEvent, ServerEvent}, metrics::SysMetrics, @@ -14,8 +17,6 @@ use common::{ uid::{Uid, UidAllocator}, }; -use specs::{Entities, Join, LazyUpdate, Read, ReadExpect, ReadStorage, System, WriteStorage}; - fn incorporate_update(tuple: &mut JoinTuple, state_update: StateUpdate) { // TODO: if checking equality is expensive use optional field in StateUpdate if tuple.2.get_unchecked() != &state_update.character { @@ -29,9 +30,14 @@ fn incorporate_update(tuple: &mut JoinTuple, state_update: StateUpdate) { *tuple.6.get_mut_unchecked() = state_update.energy }; if state_update.swap_loadout { - let mut loadout = tuple.7.get_mut_unchecked(); - let loadout = &mut *loadout; - std::mem::swap(&mut loadout.active_item, &mut loadout.second_item); + let mut inventory = tuple.7.get_mut_unchecked(); + let inventory = &mut *inventory; + inventory + .swap( + Slot::Equip(EquipSlot::Mainhand), + Slot::Equip(EquipSlot::Offhand), + ) + .unwrap_none(); // Swapping main and offhand never results in leftover items } } @@ -55,7 +61,7 @@ impl<'a> System<'a> for Sys { WriteStorage<'a, Vel>, WriteStorage<'a, Ori>, WriteStorage<'a, Energy>, - WriteStorage<'a, Loadout>, + WriteStorage<'a, Inventory>, WriteStorage<'a, Controller>, ReadStorage<'a, Health>, ReadStorage<'a, Body>, @@ -82,7 +88,7 @@ impl<'a> System<'a> for Sys { mut velocities, mut orientations, mut energies, - mut loadouts, + mut inventories, mut controllers, healths, bodies, @@ -106,7 +112,7 @@ impl<'a> System<'a> for Sys { &mut velocities, &mut orientations, &mut energies.restrict_mut(), - &mut loadouts.restrict_mut(), + &mut inventories.restrict_mut(), &mut controllers, &healths, &bodies, diff --git a/common/sys/src/lib.rs b/common/sys/src/lib.rs index f58c67e279..88daef5fa5 100644 --- a/common/sys/src/lib.rs +++ b/common/sys/src/lib.rs @@ -1,4 +1,4 @@ -#![feature(label_break_value, bool_to_option)] +#![feature(label_break_value, bool_to_option, option_unwrap_none)] pub mod agent; mod aura; diff --git a/common/sys/src/melee.rs b/common/sys/src/melee.rs index accc6ab276..ebf116965d 100644 --- a/common/sys/src/melee.rs +++ b/common/sys/src/melee.rs @@ -1,5 +1,5 @@ use common::{ - comp::{buff, group, Attacking, Body, CharacterState, Health, Loadout, Ori, Pos, Scale}, + comp::{buff, group, Attacking, Body, CharacterState, Health, Inventory, Ori, Pos, Scale}, event::{EventBus, LocalEvent, ServerEvent}, metrics::SysMetrics, span, @@ -28,7 +28,7 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Scale>, ReadStorage<'a, Body>, ReadStorage<'a, Health>, - ReadStorage<'a, Loadout>, + ReadStorage<'a, Inventory>, ReadStorage<'a, group::Group>, WriteStorage<'a, Attacking>, ReadStorage<'a, CharacterState>, @@ -47,7 +47,7 @@ impl<'a> System<'a> for Sys { scales, bodies, healths, - loadouts, + inventories, groups, mut attacking_storage, char_states, @@ -125,7 +125,7 @@ impl<'a> System<'a> for Sys { } } - let change = damage.modify_damage(loadouts.get(b), Some(*uid)); + let change = damage.modify_damage(inventories.get(b), Some(*uid)); server_emitter.emit(ServerEvent::Damage { entity: b, change }); // Apply bleeding buff on melee hits with 10% chance diff --git a/common/sys/src/projectile.rs b/common/sys/src/projectile.rs index 4fe3d4f63c..7ff5a2e670 100644 --- a/common/sys/src/projectile.rs +++ b/common/sys/src/projectile.rs @@ -1,7 +1,7 @@ use common::{ comp::{ buff::{Buff, BuffChange, BuffSource}, - projectile, EnergyChange, EnergySource, Group, HealthSource, Loadout, Ori, PhysicsState, + projectile, EnergyChange, EnergySource, Group, HealthSource, Inventory, Ori, PhysicsState, Pos, Projectile, Vel, }, event::{EventBus, ServerEvent}, @@ -32,7 +32,7 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Vel>, WriteStorage<'a, Ori>, WriteStorage<'a, Projectile>, - ReadStorage<'a, Loadout>, + ReadStorage<'a, Inventory>, ReadStorage<'a, Group>, ); @@ -49,7 +49,7 @@ impl<'a> System<'a> for Sys { velocities, mut orientations, mut projectiles, - loadouts, + inventories, groups, ): Self::SystemData, ) { @@ -116,9 +116,9 @@ impl<'a> System<'a> for Sys { if let Some(other_entity) = uid_allocator.retrieve_entity_internal(other.into()) { - let other_entity_loadout = loadouts.get(other_entity); + let other_entity_inventory = inventories.get(other_entity); let change = - damage.modify_damage(other_entity_loadout, projectile.owner); + damage.modify_damage(other_entity_inventory, projectile.owner); server_emitter.emit(ServerEvent::Damage { entity: other_entity, change, diff --git a/common/sys/src/shockwave.rs b/common/sys/src/shockwave.rs index 04ace531ba..8db1047a44 100644 --- a/common/sys/src/shockwave.rs +++ b/common/sys/src/shockwave.rs @@ -1,7 +1,7 @@ use common::{ comp::{ - group, Body, Health, HealthSource, Last, Loadout, Ori, PhysicsState, Pos, Scale, Shockwave, - ShockwaveHitEntities, + group, Body, Health, HealthSource, Inventory, Last, Ori, PhysicsState, Pos, Scale, + Shockwave, ShockwaveHitEntities, }, event::{EventBus, LocalEvent, ServerEvent}, resources::{DeltaTime, Time}, @@ -31,7 +31,7 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Scale>, ReadStorage<'a, Body>, ReadStorage<'a, Health>, - ReadStorage<'a, Loadout>, + ReadStorage<'a, Inventory>, ReadStorage<'a, group::Group>, ReadStorage<'a, PhysicsState>, WriteStorage<'a, Shockwave>, @@ -54,7 +54,7 @@ impl<'a> System<'a> for Sys { scales, bodies, healths, - loadouts, + inventories, groups, physics_states, mut shockwaves, @@ -199,7 +199,7 @@ impl<'a> System<'a> for Sys { } let owner_uid = shockwave.owner.unwrap_or(*uid); - let change = damage.modify_damage(loadouts.get(b), Some(owner_uid)); + let change = damage.modify_damage(inventories.get(b), Some(owner_uid)); server_emitter.emit(ServerEvent::Damage { entity: b, change }); shockwave_hit_list.hit_entities.push(*uid_b); diff --git a/common/sys/src/state.rs b/common/sys/src/state.rs index 1559b64c06..5d95686747 100644 --- a/common/sys/src/state.rs +++ b/common/sys/src/state.rs @@ -109,7 +109,6 @@ impl State { // Uids for sync ecs.register_sync_marker(); // Register server -> all clients synced components. - ecs.register::(); ecs.register::(); ecs.register::(); ecs.register::(); @@ -280,9 +279,6 @@ impl State { /// Get the current delta time. pub fn get_delta_time(&self) -> f32 { self.ecs.read_resource::().0 } - /// Get a reference to this state's ability map. - pub fn ability_map(&self) -> Fetch { self.ecs.read_resource() } - /// Get a reference to this state's terrain. pub fn terrain(&self) -> Fetch { self.ecs.read_resource() } diff --git a/server-cli/Cargo.toml b/server-cli/Cargo.toml index 6269f04932..66fd1cf960 100644 --- a/server-cli/Cargo.toml +++ b/server-cli/Cargo.toml @@ -9,6 +9,7 @@ worldgen = ["server/worldgen"] default = ["worldgen"] tracy = ["common/tracy", "tracing-tracy"] plugins = ["server/plugins"] +tweak = ["server/tweak"] [dependencies] server = { package = "veloren-server", path = "../server", default-features = false } diff --git a/server-cli/src/logging.rs b/server-cli/src/logging.rs index 6a738ed2c5..72909db2dd 100644 --- a/server-cli/src/logging.rs +++ b/server-cli/src/logging.rs @@ -16,6 +16,11 @@ pub fn init(basic: bool) { let base_exceptions = |env: EnvFilter| { env.add_directive("veloren_world::sim=info".parse().unwrap()) .add_directive("veloren_world::civ=info".parse().unwrap()) + .add_directive( + "veloren_common::comp::inventory::slot=info" + .parse() + .unwrap(), + ) .add_directive("uvth=warn".parse().unwrap()) .add_directive("tiny_http=warn".parse().unwrap()) .add_directive("mio::sys::windows=debug".parse().unwrap()) diff --git a/server/Cargo.toml b/server/Cargo.toml index 0b97156e82..31af7389b8 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -8,6 +8,7 @@ edition = "2018" worldgen = [] simd = ["vek/platform_intrinsics"] plugins = ["common-sys/plugins"] +tweak = ["const-tweaker"] default = ["worldgen", "plugins", "simd"] @@ -47,6 +48,7 @@ diesel = { version = "1.4.3", features = ["sqlite"] } diesel_migrations = "1.4.0" dotenv = "0.15.0" slab = "0.4" +const-tweaker = {version = "0.3.1", optional = true} # Plugins plugin-api = { package = "veloren-plugin-api", path = "../plugin/api"} diff --git a/server/src/character_creator.rs b/server/src/character_creator.rs index 6fdeadc53a..32199d2c6a 100644 --- a/server/src/character_creator.rs +++ b/server/src/character_creator.rs @@ -1,8 +1,5 @@ use crate::persistence::character_loader::CharacterLoader; -use common::{ - comp::{item::tool::AbilityMap, Body, Inventory, Stats}, - loadout_builder::LoadoutBuilder, -}; +use common::comp::{inventory::loadout_builder::LoadoutBuilder, Body, Inventory, Item, Stats}; use specs::{Entity, ReadExpect}; pub fn create_character( @@ -12,25 +9,30 @@ pub fn create_character( character_tool: Option, body: Body, character_loader: &ReadExpect<'_, CharacterLoader>, - map: &AbilityMap, ) { let stats = Stats::new(character_alias.to_string(), body); let loadout = LoadoutBuilder::new() .defaults() - .active_item(Some(LoadoutBuilder::default_item_config_from_str( + .active_item(Some(Item::new_from_asset_expect( character_tool.as_deref().unwrap(), - map, ))) .build(); - let inventory = Inventory::default(); + let mut inventory = Inventory::new_with_loadout(loadout); + + // Default items for new characters + inventory.push(Item::new_from_asset_expect( + "common.items.consumable.potion_minor", + )); + inventory.push(Item::new_from_asset_expect("common.items.food.cheese")); + let waypoint = None; character_loader.create_character( entity, player_uuid, character_alias, - (body, stats, inventory, loadout, waypoint), + (body, stats, inventory, waypoint), ); } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index fc91fc18b3..579fac55a9 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -13,7 +13,7 @@ use common::{ self, aura::{Aura, AuraKind}, buff::{BuffCategory, BuffData, BuffKind, BuffSource}, - ChatType, Item, LightEmitter, WaypointArea, + ChatType, Inventory, Item, LightEmitter, WaypointArea, }, effect::Effect, event::{EventBus, ServerEvent}, @@ -82,6 +82,7 @@ fn get_handler(cmd: &ChatCommand) -> CommandHandler { ChatCommand::Campfire => handle_spawn_campfire, ChatCommand::Debug => handle_debug, ChatCommand::DebugColumn => handle_debug_column, + ChatCommand::DropAll => handle_drop_all, ChatCommand::Dummy => handle_spawn_training_dummy, ChatCommand::Explosion => handle_explosion, ChatCommand::Faction => handle_faction, @@ -126,6 +127,50 @@ fn get_handler(cmd: &ChatCommand) -> CommandHandler { } } +fn handle_drop_all( + server: &mut Server, + client: EcsEntity, + _target: EcsEntity, + _args: String, + _action: &ChatCommand, +) { + let pos = server + .state + .ecs() + .read_storage::() + .get(client) + .cloned(); + + let mut items = Vec::new(); + if let Some(mut inventory) = server + .state + .ecs() + .write_storage::() + .get_mut(client) + { + items = inventory.drain().collect(); + } + + let mut rng = rand::thread_rng(); + + let pos = pos.expect("expected pos for entity using dropall command"); + for item in items { + let vel = Vec3::new(rng.gen_range(-0.1, 0.1), rng.gen_range(-0.1, 0.1), 0.5); + + server + .state + .create_object(Default::default(), comp::object::Body::Pouch) + .with(comp::Pos(Vec3::new( + pos.0.x + rng.gen_range(5.0, 10.0), + pos.0.y + rng.gen_range(5.0, 10.0), + pos.0.z + 5.0, + ))) + .with(item) + .with(comp::Vel(vel)) + .build(); + } +} + #[allow(clippy::useless_conversion)] // TODO: Pending review in #587 fn handle_give_item( server: &mut Server, @@ -146,7 +191,7 @@ fn handle_give_item( .ecs() .write_storage::() .get_mut(target) - .map(|inv| { + .map(|mut inv| { if inv.push(item).is_some() { server.notify_client( client, @@ -167,7 +212,7 @@ fn handle_give_item( .ecs() .write_storage::() .get_mut(target) - .map(|inv| { + .map(|mut inv| { for i in 0..give_amount { if inv.push(item.duplicate()).is_some() { server.notify_client( @@ -768,10 +813,9 @@ fn handle_spawn( let body = body(); - let map = server.state().ability_map(); - let loadout = - LoadoutBuilder::build_loadout(body, None, &map, None).build(); - drop(map); + let loadout = LoadoutBuilder::build_loadout(body, None, None).build(); + + let inventory = Inventory::new_with_loadout(loadout); let mut entity_base = server .state @@ -779,7 +823,7 @@ fn handle_spawn( pos, comp::Stats::new(get_npc_name(id), body), comp::Health::new(body, 1), - loadout, + inventory, body, ) .with(comp::Vel(vel)) @@ -892,7 +936,7 @@ fn handle_spawn_training_dummy( server .state - .create_npc(pos, stats, health, comp::Loadout::default(), body) + .create_npc(pos, stats, health, Inventory::new_empty(), body) .with(comp::Vel(vel)) .with(comp::MountState::Unmounted) .build(); @@ -2093,7 +2137,7 @@ fn handle_debug( .ecs() .write_storage::() .get_mut(target) - .map(|inv| inv.push_all_unique(items.into_iter())); + .map(|mut inv| inv.push_all_unique(items.into_iter())); let _ = server .state .ecs() diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index 7af70eccd4..e2c64e92b4 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -6,8 +6,10 @@ use common::{ aura::{Aura, AuraKind}, beam, buff::{BuffCategory, BuffData, BuffKind, BuffSource}, - group, shockwave, Agent, Alignment, Body, Gravity, Health, HomeChunk, Item, ItemDrop, - LightEmitter, Loadout, Ori, Pos, Projectile, Scale, Stats, Vel, WaypointArea, + group, + inventory::loadout::Loadout, + shockwave, Agent, Alignment, Body, Gravity, Health, HomeChunk, Inventory, Item, ItemDrop, + LightEmitter, Ori, Pos, Projectile, Scale, Stats, Vel, WaypointArea, }, outcome::Outcome, rtsim::RtSimEntity, @@ -32,7 +34,6 @@ pub fn handle_loaded_character_data( comp::Body, comp::Stats, comp::Inventory, - comp::Loadout, Option, ), ) { @@ -66,9 +67,11 @@ pub fn handle_create_npc( Alignment::Owned(_) => None, }; + let inventory = Inventory::new_with_loadout(loadout); + let entity = server .state - .create_npc(pos, stats, health, loadout, body) + .create_npc(pos, stats, health, inventory, body) .with(scale) .with(alignment); diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index f0a382f31c..d0c13d4479 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -1,6 +1,6 @@ use crate::{ client::Client, - comp::{biped_large, quadruped_medium, quadruped_small, theropod, PhysicsState}, + comp::{biped_large, quadruped_low, quadruped_medium, quadruped_small, theropod, PhysicsState}, rtsim::RtSim, Server, SpawnPoint, StateExt, }; @@ -10,7 +10,7 @@ use common::{ self, aura, buff, chat::{KillSource, KillType}, object, Alignment, Body, Energy, EnergyChange, Group, Health, HealthChange, HealthSource, - Item, Player, Pos, Stats, + Inventory, Item, Player, Pos, Stats, }, effect::Effect, lottery::Lottery, @@ -323,7 +323,18 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc 6 => "common.loot_tables.loot_table_weapon_rare", _ => "common.loot_tables.loot_table_cave_large", }, - _ => match rng.gen_range(0, 8) { + biped_large::Species::Troll => match rng.gen_range(0, 10) { + 0 => "common.loot_tables.loot_table_food", + 1 => "common.loot_tables.loot_table_cave_large", + 3 => "common.loot_tables.loot_table_armor_heavy", + 5 => "common.loot_tables.loot_table_weapon_uncommon", + 6 => "common.loot_tables.loot_table_weapon_rare", + _ => "common.loot_tables.loot_table_troll", + }, + biped_large::Species::Occultsaurok + | biped_large::Species::Mightysaurok + | biped_large::Species::Slysaurok => "common.loot_tables.loot_table_saurok", + _ => match rng.gen_range(0, 10) { 0 => "common.loot_tables.loot_table_food", 1 => "common.loot_tables.loot_table_armor_nature", 3 => "common.loot_tables.loot_table_armor_heavy", @@ -351,10 +362,17 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc _ => "common.loot_tables.loot_table_animal_parts", }, Some(common::comp::Body::Dragon(_)) => "common.loot_tables.loot_table_weapon_rare", - Some(common::comp::Body::QuadrupedLow(_)) => match rng.gen_range(0, 3) { - 0 => "common.loot_tables.loot_table_food", - 1 => "common.loot_tables.loot_table_animal_parts", - _ => "common.loot_tables.loot_table", + Some(common::comp::Body::QuadrupedLow(quadruped_low)) => { + match quadruped_low.species { + quadruped_low::Species::Maneater => { + "common.loot_tables.loot_table_maneater" + }, + _ => match rng.gen_range(0, 3) { + 0 => "common.loot_tables.loot_table_food", + 1 => "common.loot_tables.loot_table_animal_parts", + _ => "common.loot_tables.loot_table", + }, + } }, _ => "common.loot_tables.loot_table", }) @@ -438,8 +456,8 @@ pub fn handle_land_on_ground(server: &Server, entity: EcsEntity, vel: Vec3) source: DamageSource::Falling, value: falldmg, }; - let loadouts = state.ecs().read_storage::(); - let change = damage.modify_damage(loadouts.get(entity), None); + let inventories = state.ecs().read_storage::(); + let change = damage.modify_damage(inventories.get(entity), None); health.change_by(change); } } diff --git a/server/src/events/interaction.rs b/server/src/events/interaction.rs index d52ae57eee..8d2e85f337 100644 --- a/server/src/events/interaction.rs +++ b/server/src/events/interaction.rs @@ -1,20 +1,18 @@ +use specs::{world::WorldExt, Entity as EcsEntity}; +use tracing::error; + +use common::{ + comp::{self, inventory::slot::EquipSlot, item, slot::Slot, Inventory, Pos}, + consts::MAX_MOUNT_RANGE, + uid::Uid, +}; +use common_net::{msg::ServerGeneral, sync::WorldSyncExt}; + use crate::{ client::Client, presence::{Presence, RegionSubscription}, Server, }; -use common::{ - comp::{ - self, - item::{self, tool::AbilityMap}, - Pos, - }, - consts::MAX_MOUNT_RANGE, - uid::Uid, -}; -use common_net::{msg::ServerGeneral, sync::WorldSyncExt}; -use specs::{world::WorldExt, Entity as EcsEntity}; -use tracing::error; pub fn handle_lantern(server: &mut Server, entity: EcsEntity, enable: bool) { let ecs = server.state_mut().ecs(); @@ -32,10 +30,10 @@ pub fn handle_lantern(server: &mut Server, entity: EcsEntity, enable: bool) { .write_storage::() .remove(entity); } else { - let loadout_storage = ecs.read_storage::(); - let lantern_opt = loadout_storage + let inventory_storage = ecs.read_storage::(); + let lantern_opt = inventory_storage .get(entity) - .and_then(|loadout| loadout.lantern.as_ref()) + .and_then(|inventory| inventory.equipped(EquipSlot::Lantern)) .and_then(|item| { if let comp::item::ItemKind::Lantern(l) = item.kind() { Some(l) @@ -108,7 +106,6 @@ pub fn handle_unmount(server: &mut Server, mounter: EcsEntity) { #[allow(clippy::nonminimal_bool)] // TODO: Pending review in #587 pub fn handle_possess(server: &Server, possessor_uid: Uid, possesse_uid: Uid) { let ecs = &server.state.ecs(); - let ability_map = ecs.fetch::(); if let (Some(possessor), Some(possesse)) = ( ecs.entity_from_uid(possessor_uid.into()), ecs.entity_from_uid(possesse_uid.into()), @@ -173,18 +170,22 @@ pub fn handle_possess(server: &Server, possessor_uid: Uid, possesse_uid: Uid) { .map(|c| c.send_fallible(ServerGeneral::SetPlayerEntity(possesse_uid))); // Put possess item into loadout - let mut loadouts = ecs.write_storage::(); - let mut loadout = loadouts + let mut inventories = ecs.write_storage::(); + let mut inventory = inventories .entry(possesse) - .expect("Could not read loadouts component while possessing") - .or_insert(comp::Loadout::default()); + .expect("Could not read inventory component while possessing") + .or_insert(Inventory::new_empty()); - let item = comp::Item::new_from_asset_expect("common.items.debug.possess"); - if let item::ItemKind::Tool(_) = item.kind() { - let debug_item = comp::ItemConfig::from((item, &*ability_map)); - let loadout = &mut *loadout; - std::mem::swap(&mut loadout.active_item, &mut loadout.second_item); - loadout.active_item = Some(debug_item); + let debug_item = comp::Item::new_from_asset_expect("common.items.debug.possess"); + if let item::ItemKind::Tool(_) = debug_item.kind() { + inventory + .swap( + Slot::Equip(EquipSlot::Mainhand), + Slot::Equip(EquipSlot::Offhand), + ) + .unwrap_none(); // Swapping main and offhand never results in leftover items + + inventory.replace_loadout_item(EquipSlot::Mainhand, Some(debug_item)); } // Remove will of the entity diff --git a/server/src/events/inventory_manip.rs b/server/src/events/inventory_manip.rs index 6efd49f8d5..95a521b478 100644 --- a/server/src/events/inventory_manip.rs +++ b/server/src/events/inventory_manip.rs @@ -1,4 +1,8 @@ -use crate::{client::Client, Server, StateExt}; +use rand::Rng; +use specs::{join::Join, world::WorldExt, Builder, Entity as EcsEntity, WriteStorage}; +use tracing::{debug, error}; +use vek::{Rgb, Vec3}; + use common::{ comp::{ self, item, @@ -13,10 +17,8 @@ use common::{ use common_net::{msg::ServerGeneral, sync::WorldSyncExt}; use common_sys::state::State; use comp::LightEmitter; -use rand::Rng; -use specs::{join::Join, world::WorldExt, Builder, Entity as EcsEntity, WriteStorage}; -use tracing::{debug, error}; -use vek::{Rgb, Vec3}; + +use crate::{client::Client, Server, StateExt}; pub fn swap_lantern( storage: &mut WriteStorage, @@ -60,7 +62,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv match manip { comp::InventoryManip::Pickup(uid) => { let picked_up_item: Option; - let item_entity = if let (Some((item, item_entity)), Some(inv)) = ( + let item_entity = if let (Some((item, item_entity)), Some(mut inv)) = ( state .ecs() .entity_from_uid(uid.into()) @@ -96,10 +98,16 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv } } - // Attempt to add the item to the entity's inventory - match inv.push(item) { - None => Some(item_entity), - Some(_) => None, // Inventory was full + // First try to equip the picked up item + if let Err(returned_item) = inv.try_equip(item) { + // If we couldn't equip it (no empty slot for it or unequippable) then attempt + // to add the item to the entity's inventory + match inv.pickup_item(returned_item) { + Ok(_) => Some(item_entity), + Err(_) => None, // Inventory was full + } + } else { + Some(item_entity) } } else { // Item entity/component could not be found - most likely because the entity @@ -146,7 +154,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv }; if let Some(item) = comp::Item::try_reclaim_from_block(block) { - let (event, item_was_added) = if let Some(inv) = state + let (event, item_was_added) = if let Some(mut inv) = state .ecs() .write_storage::() .get_mut(entity) @@ -198,7 +206,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv comp::InventoryManip::Use(slot) => { let mut inventories = state.ecs().write_storage::(); - let inventory = if let Some(inventory) = inventories.get_mut(entity) { + let mut inventory = if let Some(inventory) = inventories.get_mut(entity) { inventory } else { error!( @@ -213,29 +221,32 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv let event = match slot { Slot::Inventory(slot) => { use item::ItemKind; + let (is_equippable, lantern_opt) = - inventory - .get(slot) - .map_or((false, None), |i| match i.kind() { - ItemKind::Tool(_) - | ItemKind::Armor { .. } - | ItemKind::Glider(_) => (true, None), - ItemKind::Lantern(lantern) => (true, Some(lantern)), - _ => (false, None), - }); + inventory.get(slot).map_or((false, None), |i| { + (i.kind().is_equippable(), match i.kind() { + ItemKind::Lantern(lantern) => Some(lantern), + _ => None, + }) + }); if is_equippable { - if let Some(mut loadout) = - state.ecs().write_storage::().get_mut(entity) - { - if let Some(lantern) = lantern_opt { - swap_lantern(&mut state.ecs().write_storage(), entity, &lantern); - } - let ability_map = state.ability_map(); - slot::equip(slot, inventory, &mut loadout, &ability_map); - Some(comp::InventoryUpdateEvent::Used) - } else { - None + if let Some(lantern) = lantern_opt { + swap_lantern(&mut state.ecs().write_storage(), entity, &lantern); } + if let Some(pos) = state.ecs().read_storage::().get(entity) { + if let Some(leftover_items) = inventory.equip(slot) { + dropped_items.extend(leftover_items.into_iter().map(|x| { + ( + *pos, + state + .read_component_copied::(entity) + .unwrap_or_default(), + x, + ) + })); + } + } + Some(comp::InventoryUpdateEvent::Used) } else if let Some(item) = inventory.take(slot) { match item.kind() { ItemKind::Consumable { kind, effect, .. } => { @@ -346,13 +357,13 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv }; if reinsert { - let _ = inventory.insert_or_stack(slot, item); + let _ = inventory.insert_or_stack_at(slot, item); } Some(comp::InventoryUpdateEvent::Used) }, _ => { - inventory.insert_or_stack(slot, item).unwrap(); + inventory.insert_or_stack_at(slot, item).unwrap(); None }, } @@ -361,23 +372,31 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv } }, Slot::Equip(slot) => { - if let Some(mut loadout) = - state.ecs().write_storage::().get_mut(entity) - { - if slot == slot::EquipSlot::Lantern { - snuff_lantern(&mut state.ecs().write_storage(), entity); - } - let ability_map = state.ability_map(); - slot::unequip(slot, inventory, &mut loadout, &ability_map); - Some(comp::InventoryUpdateEvent::Used) - } else { - error!(?entity, "Entity doesn't have a loadout, can't unequip..."); - None + if slot == slot::EquipSlot::Lantern { + snuff_lantern(&mut state.ecs().write_storage(), entity); } + + if let Some(pos) = state.ecs().read_storage::().get(entity) { + // Unequip the item, any items that no longer fit within the inventory (due + // to unequipping a bag for example) will be dropped on the floor + if let Ok(Some(leftover_items)) = inventory.unequip(slot) { + dropped_items.extend(leftover_items.into_iter().map(|x| { + ( + *pos, + state + .read_component_copied::(entity) + .unwrap_or_default(), + x, + ) + })); + } + } + Some(comp::InventoryUpdateEvent::Used) }, }; drop(inventories); + if let Some(effects) = maybe_effect { for effect in effects { state.apply_effect(entity, effect, None); @@ -390,20 +409,23 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv comp::InventoryManip::Swap(a, b) => { let ecs = state.ecs(); - let mut inventories = ecs.write_storage::(); - let mut loadouts = ecs.write_storage::(); - let inventory = inventories.get_mut(entity); - let mut loadout = loadouts.get_mut(entity); - let ability_map = state.ability_map(); - slot::swap(a, b, inventory, loadout.as_deref_mut(), &ability_map); - // slot::swap(a, b, inventory, loadout.as_mut().map(|x| &mut **x), - // &ability_map); - - // :/ - drop(loadouts); - drop(inventories); - drop(ability_map); + if let Some(pos) = ecs.read_storage::().get(entity) { + if let Some(mut inventory) = ecs.write_storage::().get_mut(entity) + { + if let Some(leftover_items) = inventory.swap(a, b) { + dropped_items.extend(leftover_items.into_iter().map(|x| { + ( + *pos, + state + .read_component_copied::(entity) + .unwrap_or_default(), + x, + ) + })); + } + } + } state.write_component( entity, @@ -412,20 +434,18 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv }, comp::InventoryManip::Drop(slot) => { - let ability_map = state.ability_map(); let item = match slot { Slot::Inventory(slot) => state .ecs() .write_storage::() .get_mut(entity) - .and_then(|inv| inv.remove(slot)), + .and_then(|mut inv| inv.remove(slot)), Slot::Equip(slot) => state .ecs() - .write_storage::() + .write_storage::() .get_mut(entity) - .and_then(|mut ldt| slot::loadout_remove(slot, &mut ldt, &ability_map)), + .and_then(|mut inv| inv.replace_loadout_item(slot, None)), }; - drop(ability_map); // FIXME: We should really require the drop and write to be atomic! if let (Some(mut item), Some(pos)) = @@ -447,13 +467,15 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv }, comp::InventoryManip::CraftRecipe(recipe) => { - if let Some(inv) = state + if let Some(mut inv) = state .ecs() .write_storage::() .get_mut(entity) { let recipe_book = default_recipe_book().read(); - let craft_result = recipe_book.get(&recipe).and_then(|r| r.perform(inv).ok()); + let craft_result = recipe_book + .get(&recipe) + .and_then(|r| r.perform(&mut inv).ok()); // FIXME: We should really require the drop and write to be atomic! if craft_result.is_some() { @@ -483,15 +505,11 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv // Drop items for (pos, ori, item) in dropped_items { - let vel = *ori.0 * 5.0 - + Vec3::unit_z() * 10.0 - + Vec3::::zero().map(|_| rand::thread_rng().gen::() - 0.5) * 4.0; - state .create_object(Default::default(), comp::object::Body::Pouch) - .with(comp::Pos(pos.0 + Vec3::unit_z() * 0.25)) + .with(comp::Pos(pos.0 + *ori.0 + Vec3::unit_z())) .with(item) - .with(comp::Vel(vel)) + .with(comp::Vel(Vec3::zero())) .build(); } @@ -572,10 +590,12 @@ fn within_pickup_range>( #[cfg(test)] mod tests { - use super::*; + use vek::Vec3; + use common::comp::Pos; use find_dist::*; - use vek::Vec3; + + use super::*; // Helper function #[allow(clippy::unnecessary_wraps)] diff --git a/server/src/events/player.rs b/server/src/events/player.rs index fa9cbc1b8b..03ac7dde10 100644 --- a/server/src/events/player.rs +++ b/server/src/events/player.rs @@ -162,11 +162,10 @@ pub fn handle_client_disconnect(server: &mut Server, entity: EcsEntity) -> Event } fn persist_entity(state: &mut State, entity: EcsEntity) -> EcsEntity { - if let (Some(presences), Some(stats), Some(inventory), Some(loadout), updater) = ( + if let (Some(presences), Some(stats), Some(inventory), updater) = ( state.read_storage::().get(entity), state.read_storage::().get(entity), state.read_storage::().get(entity), - state.read_storage::().get(entity), state .ecs() .read_resource::(), @@ -174,7 +173,7 @@ fn persist_entity(state: &mut State, entity: EcsEntity) -> EcsEntity { if let PresenceKind::Character(character_id) = presences.kind { let waypoint_read = state.read_storage::(); let waypoint = waypoint_read.get(entity); - updater.update(character_id, stats, inventory, loadout, waypoint); + updater.update(character_id, stats, inventory, waypoint); } } diff --git a/server/src/lib.rs b/server/src/lib.rs index 9eb1f84565..13cf84f689 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -1,7 +1,7 @@ #![deny(unsafe_code)] #![allow(clippy::option_map_unit_fn)] #![deny(clippy::clone_on_ref_ptr)] -#![feature(bool_to_option, drain_filter, option_zip)] +#![feature(bool_to_option, drain_filter, option_unwrap_none, option_zip)] #![cfg_attr(not(feature = "worldgen"), feature(const_panic))] pub mod alias_validator; @@ -46,10 +46,13 @@ use crate::{ state_ext::StateExt, sys::sentinel::{DeletedEntities, TrackedComps}, }; +#[cfg(not(feature = "worldgen"))] +use common::grid::Grid; use common::{ assets::AssetExt, cmd::ChatCommand, comp, + comp::CharacterAbility, event::{EventBus, ServerEvent}, outcome::Outcome, recipe::default_recipe_book, @@ -86,6 +89,7 @@ use test_world::{IndexOwned, World}; use tracing::{debug, error, info, trace}; use uvth::{ThreadPool, ThreadPoolBuilder}; use vek::*; + #[cfg(feature = "worldgen")] use world::{ sim::{FileOpts, WorldOpts, DEFAULT_WORLD_MAP}, @@ -168,13 +172,13 @@ impl Server { .ecs_mut() .insert(CharacterUpdater::new(&persistence_db_dir)?); - let ability_map = comp::item::tool::AbilityMap::load_expect_cloned( + let ability_map = comp::item::tool::AbilityMap::::load_expect_cloned( "common.abilities.weapon_ability_manifest", ); + state.ecs_mut().insert(ability_map); state .ecs_mut() - .insert(CharacterLoader::new(&persistence_db_dir, &ability_map)?); - state.ecs_mut().insert(ability_map); + .insert(CharacterLoader::new(&persistence_db_dir)?); state.ecs_mut().insert(Vec::::new()); // System timers for performance monitoring @@ -256,10 +260,10 @@ impl Server { let map = WorldMapMsg { dimensions_lg: Vec2::zero(), max_height: 1.0, - rgba: vec![0], + rgba: Grid::new(Vec2::new(1, 1), 1), horizons: [(vec![0], vec![0]), (vec![0], vec![0])], sea_level: 0.0, - alt: vec![30], + alt: Grid::new(Vec2::new(1, 1), 1), sites: Vec::new(), }; diff --git a/server/src/migrations/2020-11-28-205542_item-storage/down.sql b/server/src/migrations/2020-11-28-205542_item-storage/down.sql new file mode 100644 index 0000000000..291a97c5ce --- /dev/null +++ b/server/src/migrations/2020-11-28-205542_item-storage/down.sql @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` \ No newline at end of file diff --git a/server/src/migrations/2020-11-28-205542_item-storage/up.sql b/server/src/migrations/2020-11-28-205542_item-storage/up.sql new file mode 100644 index 0000000000..559d2c2a51 --- /dev/null +++ b/server/src/migrations/2020-11-28-205542_item-storage/up.sql @@ -0,0 +1,94 @@ +-- +-- Step 1 - Update renamed ring loadout position +-- +UPDATE item +SET position = 'ring1' +WHERE position = 'ring'; + +-- +-- Step 2 - Give every existing player 3x 6 slot bags in their bag1-3 loadout slots +-- + +CREATE TEMP TABLE _temp_loadout_containers +AS +SELECT item_id +FROM item +WHERE item_definition_id = 'veloren.core.pseudo_containers.loadout'; + +-- Insert an entity ID for each new bag item (3 per existing loadout) +WITH loadout_containers AS ( + SELECT 1 + FROM item + WHERE item_definition_id = 'veloren.core.pseudo_containers.loadout') +INSERT +INTO entity +SELECT NULL FROM loadout_containers +UNION ALL +SELECT NULL FROM loadout_containers +UNION ALL +SELECT NULL FROM loadout_containers; + +CREATE TEMP TABLE _temp_new_bag_item_ids AS +SELECT item_id AS loadout_container_item_id, + ROW_NUMBER() OVER(ORDER BY item_id) AS temp_id_bag1, + ROW_NUMBER() OVER(ORDER BY item_id) + (SELECT COUNT(1) FROM _temp_loadout_containers) AS temp_id_bag2, + ROW_NUMBER() OVER(ORDER BY item_id) + (SELECT COUNT(1) * 2 FROM _temp_loadout_containers) AS temp_id_bag3 +FROM item +WHERE item_definition_id = 'veloren.core.pseudo_containers.loadout'; + +INSERT INTO item +SELECT (SELECT MAX(entity_id) - temp_id_bag1 + 1 from entity) as new_item_id, + loadout_container_item_id, + 'common.items.armor.bag.tiny_leather_pouch', + 1, + 'bag1' +FROM _temp_new_bag_item_ids; + +INSERT INTO item +SELECT (SELECT MAX(entity_id) - temp_id_bag2 + 1 from entity) as new_item_id, + loadout_container_item_id, + 'common.items.armor.bag.tiny_leather_pouch', + 1, + 'bag2' +FROM _temp_new_bag_item_ids; + +INSERT INTO item +SELECT (SELECT MAX(entity_id) - temp_id_bag3 + 1 from entity) as new_item_id, + loadout_container_item_id, + 'common.items.armor.bag.tiny_leather_pouch', + 1, + 'bag3' +FROM _temp_new_bag_item_ids; + +-- +-- Step 3 - Update the position column for all existing inventory items, putting the first 18 +-- items in the inventory's built in slots, and the next 18 items inside the 3 new bags +-- + +WITH inventory_items AS ( + SELECT i2.item_id, + i2.parent_container_item_id, + CAST(i2.position AS NUMBER) AS position + FROM item i + JOIN item i2 ON (i2.parent_container_item_id = i.item_id) + WHERE i.item_definition_id = 'veloren.core.pseudo_containers.inventory' +), + new_positions AS ( + SELECT item_id, + parent_container_item_id, + position, + -- Slots 0 - 17 have loadout_idx 0 (built-in inventory slots) + -- Slots 18 - 23 have loadout_idx 15 (bag1 loadout slot) + -- Slots 24 - 29 have loadout_idx 16 (bag2 loadout slot) + -- Slots 30 - 35 have loadout_idx 17 (bag3 loadout slot) + (position / 18) * ((position / 6) + 12) as loadout_idx, + -- Slots 0-17 have their existing position as their slot_idx + -- Slots 18-35 go into slots 0-5 of the 3 new bags + CASE WHEN position < 18 THEN position ELSE (position % 18) % 6 END as slot_idx + FROM inventory_items + ) +UPDATE item +SET position = ( SELECT '{"loadout_idx":' || CAST(loadout_idx as VARCHAR) || ',"slot_idx":' || CAST(slot_idx as VARCHAR) || '}' + FROM new_positions + WHERE item_id = item.item_id) +WHERE item_id IN (SELECT item_id FROM new_positions); \ No newline at end of file diff --git a/server/src/persistence/character.rs b/server/src/persistence/character.rs index eb4c4fc52e..b9281ab569 100644 --- a/server/src/persistence/character.rs +++ b/server/src/persistence/character.rs @@ -9,6 +9,7 @@ extern crate diesel; use super::{error::Error, models::*, schema, VelorenTransaction}; use crate::{ comp, + comp::Inventory, persistence::{ character::conversions::{ convert_body_from_database, convert_body_to_database_json, @@ -22,10 +23,7 @@ use crate::{ PersistedComponents, }, }; -use common::{ - character::{CharacterId, CharacterItem, MAX_CHARACTERS_PER_PLAYER}, - comp::item::tool::AbilityMap, -}; +use common::character::{CharacterId, CharacterItem, MAX_CHARACTERS_PER_PLAYER}; use core::ops::Range; use diesel::{prelude::*, sql_query, sql_types::BigInt}; use std::sync::Arc; @@ -59,7 +57,6 @@ pub fn load_character_data( requesting_player_uuid: String, char_id: CharacterId, connection: VelorenTransaction, - map: &AbilityMap, ) -> CharacterDataResult { use schema::{body::dsl::*, character::dsl::*, item::dsl::*}; @@ -106,8 +103,7 @@ pub fn load_character_data( Ok(( convert_body_from_database(&char_body)?, convert_stats_from_database(&stats_data, character_data.alias), - convert_inventory_from_database_items(&inventory_items)?, - convert_loadout_from_database_items(&loadout_items, map)?, + convert_inventory_from_database_items(&inventory_items, &loadout_items)?, char_waypoint, )) } @@ -122,7 +118,6 @@ pub fn load_character_data( pub fn load_character_list( player_uuid_: &str, connection: VelorenTransaction, - map: &AbilityMap, ) -> CharacterListResult { use schema::{body::dsl::*, character::dsl::*, item::dsl::*, stats::dsl::*}; @@ -155,13 +150,13 @@ pub fn load_character_list( .filter(parent_container_item_id.eq(loadout_container_id)) .load::(&*connection)?; - let loadout = convert_loadout_from_database_items(&loadout_items, map)?; + let loadout = convert_loadout_from_database_items(&loadout_items)?; Ok(CharacterItem { character: char, body: char_body, level: char_stats.level as usize, - loadout, + inventory: Inventory::new_with_loadout(loadout), }) }) .collect() @@ -172,7 +167,6 @@ pub fn create_character( character_alias: &str, persisted_components: PersistedComponents, connection: VelorenTransaction, - map: &AbilityMap, ) -> CharacterCreationResult { use schema::item::dsl::*; @@ -180,7 +174,7 @@ pub fn create_character( use schema::{body, character}; - let (body, stats, inventory, loadout, waypoint) = persisted_components; + let (body, stats, inventory, waypoint) = persisted_components; // Fetch new entity IDs for character, inventory and loadout let mut new_entity_ids = get_new_entity_ids(connection, |next_id| next_id + 3)?; @@ -277,7 +271,6 @@ pub fn create_character( get_new_entity_ids(connection, |mut next_id| { let (inserts_, _deletes) = convert_items_to_database_items( - &loadout, loadout_container_id, &inventory, inventory_container_id, @@ -303,7 +296,7 @@ pub fn create_character( ))); } - load_character_list(uuid, connection, map).map(|list| (character_id, list)) + load_character_list(uuid, connection).map(|list| (character_id, list)) } /// Delete a character. Returns the updated character list. @@ -311,7 +304,6 @@ pub fn delete_character( requesting_player_uuid: &str, char_id: CharacterId, connection: VelorenTransaction, - map: &AbilityMap, ) -> CharacterListResult { use schema::{body::dsl::*, character::dsl::*, stats::dsl::*}; @@ -392,7 +384,7 @@ pub fn delete_character( ))); } - load_character_list(requesting_player_uuid, connection, map) + load_character_list(requesting_player_uuid, connection) } /// Before creating a character, we ensure that the limit on the number of @@ -534,7 +526,6 @@ pub fn update( char_id: CharacterId, char_stats: comp::Stats, inventory: comp::Inventory, - loadout: comp::Loadout, char_waypoint: Option, connection: VelorenTransaction, ) -> Result>, Error> { @@ -548,7 +539,6 @@ pub fn update( // upsert and which ones to delete. get_new_entity_ids(connection, |mut next_id| { let (upserts_, _deletes) = convert_items_to_database_items( - &loadout, pseudo_containers.loadout_container_id, &inventory, pseudo_containers.inventory_container_id, diff --git a/server/src/persistence/character/conversions.rs b/server/src/persistence/character/conversions.rs index 0ffb0c6910..f0e7416ae3 100644 --- a/server/src/persistence/character/conversions.rs +++ b/server/src/persistence/character/conversions.rs @@ -9,8 +9,14 @@ use crate::persistence::{ }; use common::{ character::CharacterId, - comp::{item::tool::AbilityMap, Body as CompBody, Waypoint, *}, - loadout_builder, + comp::{ + inventory::{ + loadout::{Loadout, LoadoutError}, + loadout_builder::LoadoutBuilder, + slot::InvSlotId, + }, + Body as CompBody, Waypoint, *, + }, resources::Time, }; use core::{convert::TryFrom, num::NonZeroU64}; @@ -24,42 +30,29 @@ pub struct ItemModelPair { /// The left vector contains all item rows to upsert; the right-hand vector /// contains all item rows to delete (by parent ID and position). +/// +/// NOTE: This method does not yet handle persisting nested items within +/// inventories. Although loadout items do store items inside them this does +/// not currently utilise `parent_container_id` - all loadout items have the +/// loadout pseudo-container as their parent. pub fn convert_items_to_database_items( - loadout: &Loadout, loadout_container_id: EntityId, inventory: &Inventory, inventory_container_id: EntityId, next_id: &mut i64, ) -> (Vec, Vec<(EntityId, String)>) { - // Loadout slots. - let loadout = [ - ("active_item", loadout.active_item.as_ref().map(|x| &x.item)), - ("second_item", loadout.second_item.as_ref().map(|x| &x.item)), - ("lantern", loadout.lantern.as_ref()), - ("shoulder", loadout.shoulder.as_ref()), - ("chest", loadout.chest.as_ref()), - ("belt", loadout.belt.as_ref()), - ("hand", loadout.hand.as_ref()), - ("pants", loadout.pants.as_ref()), - ("foot", loadout.foot.as_ref()), - ("back", loadout.back.as_ref()), - ("ring", loadout.ring.as_ref()), - ("neck", loadout.neck.as_ref()), - ("head", loadout.head.as_ref()), - ("tabard", loadout.tabard.as_ref()), - ("glider", loadout.glider.as_ref()), - ]; - - let loadout = loadout - .iter() - .map(|&(slot, item)| (slot.to_string(), item, loadout_container_id)); + let loadout = inventory + .loadout_items_with_persistence_key() + .map(|(slot, item)| (slot.to_string(), item, loadout_container_id)); // Inventory slots. - let inventory = inventory - .slots() - .iter() - .enumerate() - .map(|(slot, item)| (slot.to_string(), item.as_ref(), inventory_container_id)); + let inventory = inventory.slots_with_id().map(|(pos, item)| { + ( + serde_json::to_string(&pos).expect("failed to serialize InventorySlotPos"), + item.as_ref(), + inventory_container_id, + ) + }); // Construct new items. inventory.chain(loadout) @@ -209,9 +202,23 @@ pub fn convert_stats_to_database( }) } -pub fn convert_inventory_from_database_items(database_items: &[Item]) -> Result { - let mut inventory = Inventory::new_empty(); - for db_item in database_items.iter() { +pub fn convert_inventory_from_database_items( + inventory_items: &[Item], + loadout_items: &[Item], +) -> Result { + // Loadout items must be loaded before inventory items since loadout items + // provide inventory slots. Since items stored inside loadout items actually + // have their parent_container_item_id as the loadout pseudo-container we rely + // on populating the loadout items first, and then inserting the items into the + // inventory at the correct position. When we want to support items inside the + // player's inventory containing other items (such as "right click to + // unwrap" gifts perhaps) then we will need to refactor inventory/loadout + // persistence to traverse the tree of items and load them from the root + // down. + let loadout = convert_loadout_from_database_items(loadout_items)?; + let mut inventory = Inventory::new_with_loadout(loadout); + + for db_item in inventory_items.iter() { let mut item = get_item_from_asset(db_item.item_definition_id.as_str())?; // NOTE: Since this is freshly loaded, the atomic is *unique.* @@ -237,17 +244,20 @@ pub fn convert_inventory_from_database_items(database_items: &[Item]) -> Result< // Insert item into inventory // Slot position - let slot = &db_item.position.parse::().map_err(|_| { + let slot: InvSlotId = serde_json::from_str(&db_item.position).map_err(|_| { Error::ConversionError(format!( - "Failed to parse item position: {}", + "Failed to parse item position: {:?}", &db_item.position )) })?; - let insert_res = inventory.insert(*slot, item).map_err(|_| { + let insert_res = inventory.insert_at(slot, item).map_err(|_| { // If this happens there were too many items in the database for the current // inventory size - Error::ConversionError("Error inserting item into inventory".to_string()) + Error::ConversionError(format!( + "Error inserting item into inventory, position: {:?}", + slot + )) })?; if insert_res.is_some() { @@ -263,11 +273,10 @@ pub fn convert_inventory_from_database_items(database_items: &[Item]) -> Result< Ok(inventory) } -pub fn convert_loadout_from_database_items( - database_items: &[Item], - map: &AbilityMap, -) -> Result { - let mut loadout = loadout_builder::LoadoutBuilder::new(); +pub fn convert_loadout_from_database_items(database_items: &[Item]) -> Result { + let loadout_builder = LoadoutBuilder::new(); + let mut loadout = loadout_builder.build(); + for db_item in database_items.iter() { let item = get_item_from_asset(db_item.item_definition_id.as_str())?; // NOTE: item id is currently *unique*, so we can store the ID safely. @@ -276,32 +285,17 @@ pub fn convert_loadout_from_database_items( |_| Error::ConversionError("Item with zero item_id".to_owned()), )?)); - match db_item.position.as_str() { - "active_item" => loadout = loadout.active_item(Some(ItemConfig::from((item, map)))), - "second_item" => loadout = loadout.second_item(Some(ItemConfig::from((item, map)))), - "lantern" => loadout = loadout.lantern(Some(item)), - "shoulder" => loadout = loadout.shoulder(Some(item)), - "chest" => loadout = loadout.chest(Some(item)), - "belt" => loadout = loadout.belt(Some(item)), - "hand" => loadout = loadout.hand(Some(item)), - "pants" => loadout = loadout.pants(Some(item)), - "foot" => loadout = loadout.foot(Some(item)), - "back" => loadout = loadout.back(Some(item)), - "ring" => loadout = loadout.ring(Some(item)), - "neck" => loadout = loadout.neck(Some(item)), - "head" => loadout = loadout.head(Some(item)), - "tabard" => loadout = loadout.tabard(Some(item)), - "glider" => loadout = loadout.glider(Some(item)), - _ => { - return Err(Error::ConversionError(format!( - "Unknown loadout position on item: {}", - db_item.position.as_str() - ))); - }, - } + loadout + .set_item_at_slot_using_persistence_key(&db_item.position, item) + .map_err(|err| match err { + LoadoutError::InvalidPersistenceKey => Error::ConversionError(format!( + "Invalid persistence key: {}", + &db_item.position + )), + })?; } - Ok(loadout.build()) + Ok(loadout) } pub fn convert_body_from_database(body: &Body) -> Result { diff --git a/server/src/persistence/character_loader.rs b/server/src/persistence/character_loader.rs index c73a8425af..c1b4d8ffcf 100644 --- a/server/src/persistence/character_loader.rs +++ b/server/src/persistence/character_loader.rs @@ -3,10 +3,7 @@ use crate::persistence::{ error::Error, establish_connection, PersistedComponents, }; -use common::{ - character::{CharacterId, CharacterItem}, - comp::item::tool::AbilityMap, -}; +use common::character::{CharacterId, CharacterItem}; use crossbeam_channel::{self, TryIter}; use std::path::Path; use tracing::error; @@ -70,14 +67,12 @@ pub struct CharacterLoader { } impl CharacterLoader { - pub fn new(db_dir: &Path, map: &AbilityMap) -> diesel::QueryResult { + pub fn new(db_dir: &Path) -> diesel::QueryResult { let (update_tx, internal_rx) = crossbeam_channel::unbounded::(); let (internal_tx, update_rx) = crossbeam_channel::unbounded::(); let mut conn = establish_connection(db_dir)?; - let map = map.clone(); - std::thread::spawn(move || { for request in internal_rx { let (entity, kind) = request; @@ -97,7 +92,6 @@ impl CharacterLoader { &character_alias, persisted_components, txn, - &map, ) }, )), @@ -105,19 +99,19 @@ impl CharacterLoader { player_uuid, character_id, } => CharacterLoaderResponseKind::CharacterList(conn.transaction( - |txn| delete_character(&player_uuid, character_id, txn, &map), + |txn| delete_character(&player_uuid, character_id, txn), )), CharacterLoaderRequestKind::LoadCharacterList { player_uuid } => { - CharacterLoaderResponseKind::CharacterList(conn.transaction( - |txn| load_character_list(&player_uuid, txn, &map), - )) + CharacterLoaderResponseKind::CharacterList( + conn.transaction(|txn| load_character_list(&player_uuid, txn)), + ) }, CharacterLoaderRequestKind::LoadCharacterData { player_uuid, character_id, } => { let result = conn.transaction(|txn| { - load_character_data(player_uuid, character_id, txn, &map) + load_character_data(player_uuid, character_id, txn) }); if result.is_err() { error!( diff --git a/server/src/persistence/character_updater.rs b/server/src/persistence/character_updater.rs index ca58c0ea96..902b207d42 100644 --- a/server/src/persistence/character_updater.rs +++ b/server/src/persistence/character_updater.rs @@ -5,12 +5,7 @@ use crate::persistence::{establish_connection, VelorenConnection}; use std::{path::Path, sync::Arc}; use tracing::{error, trace}; -pub type CharacterUpdateData = ( - comp::Stats, - comp::Inventory, - comp::Loadout, - Option, -); +pub type CharacterUpdateData = (comp::Stats, comp::Inventory, Option); /// A unidirectional messaging resource for saving characters in a /// background thread. @@ -51,21 +46,15 @@ impl CharacterUpdater { CharacterId, &'a comp::Stats, &'a comp::Inventory, - &'a comp::Loadout, Option<&'a comp::Waypoint>, ), >, ) { let updates = updates - .map(|(character_id, stats, inventory, loadout, waypoint)| { + .map(|(character_id, stats, inventory, waypoint)| { ( character_id, - ( - stats.clone(), - inventory.clone(), - loadout.clone(), - waypoint.cloned(), - ), + (stats.clone(), inventory.clone(), waypoint.cloned()), ) }) .collect::>(); @@ -81,16 +70,9 @@ impl CharacterUpdater { character_id: CharacterId, stats: &comp::Stats, inventory: &comp::Inventory, - loadout: &comp::Loadout, waypoint: Option<&comp::Waypoint>, ) { - self.batch_update(std::iter::once(( - character_id, - stats, - inventory, - loadout, - waypoint, - ))); + self.batch_update(std::iter::once((character_id, stats, inventory, waypoint))); } } @@ -101,12 +83,11 @@ fn execute_batch_update( let mut inserted_items = Vec::>::new(); if let Err(e) = connection.transaction::<_, super::error::Error, _>(|txn| { - for (character_id, (stats, inventory, loadout, waypoint)) in updates { + for (character_id, (stats, inventory, waypoint)) in updates { inserted_items.append(&mut super::character::update( character_id, stats, inventory, - loadout, waypoint, txn, )?); diff --git a/server/src/persistence/mod.rs b/server/src/persistence/mod.rs index 89feb78ace..17f59c89c5 100644 --- a/server/src/persistence/mod.rs +++ b/server/src/persistence/mod.rs @@ -25,7 +25,6 @@ pub type PersistedComponents = ( comp::Body, comp::Stats, comp::Inventory, - comp::Loadout, Option, ); diff --git a/server/src/rtsim/entity.rs b/server/src/rtsim/entity.rs index eb517d9b48..e5737de144 100644 --- a/server/src/rtsim/entity.rs +++ b/server/src/rtsim/entity.rs @@ -1,5 +1,5 @@ use super::*; -use common::{comp::item::tool::AbilityMap, store::Id, terrain::TerrainGrid, LoadoutBuilder}; +use common::{comp::inventory::loadout_builder::LoadoutBuilder, store::Id, terrain::TerrainGrid}; use world::{ civ::{Site, Track}, util::RandomPerm, @@ -62,7 +62,7 @@ impl Entity { (self.rng(PERM_LEVEL).gen::().powi(2) * 15.0).ceil() as u32 } - pub fn get_loadout(&self, ability_map: &AbilityMap) -> comp::Loadout { + pub fn get_loadout(&self) -> comp::inventory::loadout::Loadout { let mut rng = self.rng(PERM_LOADOUT); let main_tool = comp::Item::new_from_asset_expect( (&[ @@ -114,7 +114,7 @@ impl Entity { "common.items.armor.shoulder.leather_0", )); - LoadoutBuilder::build_loadout(self.get_body(), Some(main_tool), ability_map, None) + LoadoutBuilder::build_loadout(self.get_body(), Some(main_tool), None) .back(back) .lantern(lantern) .chest(chest) diff --git a/server/src/rtsim/tick.rs b/server/src/rtsim/tick.rs index 583c036874..5302ecdd6c 100644 --- a/server/src/rtsim/tick.rs +++ b/server/src/rtsim/tick.rs @@ -3,6 +3,7 @@ use super::*; use common::{ comp, + comp::inventory::loadout_builder::LoadoutBuilder, event::{EventBus, ServerEvent}, resources::DeltaTime, terrain::TerrainGrid, @@ -25,7 +26,6 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, comp::Pos>, ReadStorage<'a, RtSimEntity>, WriteStorage<'a, comp::Agent>, - ReadExpect<'a, comp::item::tool::AbilityMap>, ); fn run( @@ -40,7 +40,6 @@ impl<'a> System<'a> for Sys { positions, rtsim_entities, mut agents, - ability_map, ): Self::SystemData, ) { let rtsim = &mut *rtsim; @@ -101,8 +100,8 @@ impl<'a> System<'a> for Sys { stats: comp::Stats::new(entity.get_name(), body).with_level(entity.get_level()), health: comp::Health::new(body, 10), loadout: match body { - comp::Body::Humanoid(_) => entity.get_loadout(&ability_map), - _ => comp::Loadout::default(), + comp::Body::Humanoid(_) => entity.get_loadout(), + _ => LoadoutBuilder::new().build(), }, body, agent: Some(comp::Agent::new( diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 318b94f7d6..21d99a02d1 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -5,6 +5,7 @@ use crate::{ use common::{ character::CharacterId, comp, + comp::Inventory, effect::Effect, uid::{Uid, UidAllocator}, util::Dir, @@ -31,7 +32,7 @@ pub trait StateExt { pos: comp::Pos, stats: comp::Stats, health: comp::Health, - loadout: comp::Loadout, + inventory: comp::Inventory, body: comp::Body, ) -> EcsEntityBuilder; /// Build a static object entity @@ -90,8 +91,8 @@ impl StateExt for State { .map(|mut stats| stats.exp.change_by(xp)); }, Effect::Damage(damage) => { - let loadouts = self.ecs().read_storage::(); - let change = damage.modify_damage(loadouts.get(entity), source); + let inventories = self.ecs().read_storage::(); + let change = damage.modify_damage(inventories.get(entity), source); self.ecs() .write_storage::() .get_mut(entity) @@ -118,7 +119,7 @@ impl StateExt for State { pos: comp::Pos, stats: comp::Stats, health: comp::Health, - loadout: comp::Loadout, + inventory: comp::Inventory, body: comp::Body, ) -> EcsEntityBuilder { self.ecs_mut() @@ -147,7 +148,7 @@ impl StateExt for State { .with(comp::Energy::new(body.base_energy())) .with(comp::Gravity(1.0)) .with(comp::CharacterState::default()) - .with(loadout) + .with(inventory) .with(comp::Buffs::default()) } @@ -258,7 +259,7 @@ impl StateExt for State { } fn update_character_data(&mut self, entity: EcsEntity, components: PersistedComponents) { - let (body, stats, inventory, loadout, waypoint) = components; + let (body, stats, inventory, waypoint) = components; if let Some(player_uid) = self.read_component_copied::(entity) { // Notify clients of a player list update @@ -281,7 +282,6 @@ impl StateExt for State { ); self.write_component(entity, stats); self.write_component(entity, inventory); - self.write_component(entity, loadout); self.write_component( entity, comp::InventoryUpdate::new(comp::InventoryUpdateEvent::default()), diff --git a/server/src/sys/msg/character_screen.rs b/server/src/sys/msg/character_screen.rs index ab901c8bf0..b3dd3ca57a 100644 --- a/server/src/sys/msg/character_screen.rs +++ b/server/src/sys/msg/character_screen.rs @@ -4,7 +4,7 @@ use crate::{ persistence::character_loader::CharacterLoader, presence::Presence, EditableSettings, }; use common::{ - comp::{item::tool::AbilityMap, ChatType, Player, UnresolvedChatMsg}, + comp::{ChatType, Player, UnresolvedChatMsg}, event::{EventBus, ServerEvent}, span, uid::Uid, @@ -28,7 +28,6 @@ impl Sys { editable_settings: &ReadExpect<'_, EditableSettings>, alias_validator: &ReadExpect<'_, AliasValidator>, msg: ClientGeneral, - map: &AbilityMap, ) -> Result<(), crate::error::Error> { match msg { // Request spectator state @@ -103,7 +102,6 @@ impl Sys { tool, body, character_loader, - map, ); } }, @@ -137,7 +135,6 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Presence>, ReadExpect<'a, EditableSettings>, ReadExpect<'a, AliasValidator>, - ReadExpect<'a, AbilityMap>, ); fn run( @@ -153,7 +150,6 @@ impl<'a> System<'a> for Sys { presences, editable_settings, alias_validator, - map, ): Self::SystemData, ) { span!(_guard, "run", "msg::character_screen::Sys::run"); @@ -176,7 +172,6 @@ impl<'a> System<'a> for Sys { &editable_settings, &alias_validator, msg, - &map, ) }); } diff --git a/server/src/sys/persistence.rs b/server/src/sys/persistence.rs index ac7053e061..2aecd80f75 100644 --- a/server/src/sys/persistence.rs +++ b/server/src/sys/persistence.rs @@ -4,7 +4,7 @@ use crate::{ sys::{SysScheduler, SysTimer}, }; use common::{ - comp::{Inventory, Loadout, Stats, Waypoint}, + comp::{Inventory, Stats, Waypoint}, span, }; use common_net::msg::PresenceKind; @@ -18,7 +18,6 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Presence>, ReadStorage<'a, Stats>, ReadStorage<'a, Inventory>, - ReadStorage<'a, Loadout>, ReadStorage<'a, Waypoint>, ReadExpect<'a, character_updater::CharacterUpdater>, Write<'a, SysScheduler>, @@ -31,7 +30,6 @@ impl<'a> System<'a> for Sys { presences, player_stats, player_inventories, - player_loadouts, player_waypoint, updater, mut scheduler, @@ -46,15 +44,12 @@ impl<'a> System<'a> for Sys { &presences, &player_stats, &player_inventories, - &player_loadouts, player_waypoint.maybe(), ) .join() .filter_map( - |(presence, stats, inventory, loadout, waypoint)| match presence.kind { - PresenceKind::Character(id) => { - Some((id, stats, inventory, loadout, waypoint)) - }, + |(presence, stats, inventory, waypoint)| match presence.kind { + PresenceKind::Character(id) => Some((id, stats, inventory, waypoint)), PresenceKind::Spectator => None, }, ), diff --git a/server/src/sys/sentinel.rs b/server/src/sys/sentinel.rs index 98a8c096a2..f3c5daee58 100644 --- a/server/src/sys/sentinel.rs +++ b/server/src/sys/sentinel.rs @@ -2,7 +2,7 @@ use super::SysTimer; use common::{ comp::{ Auras, BeamSegment, Body, Buffs, CanBuild, CharacterState, Collider, Energy, Gravity, - Group, Health, Item, LightEmitter, Loadout, Mass, MountState, Mounting, Ori, Player, Pos, + Group, Health, Inventory, Item, LightEmitter, Mass, MountState, Mounting, Ori, Player, Pos, Scale, Shockwave, Stats, Sticky, Vel, }, span, @@ -62,7 +62,7 @@ pub struct TrackedComps<'a> { pub collider: ReadStorage<'a, Collider>, pub sticky: ReadStorage<'a, Sticky>, pub gravity: ReadStorage<'a, Gravity>, - pub loadout: ReadStorage<'a, Loadout>, + pub inventory: ReadStorage<'a, Inventory>, pub character_state: ReadStorage<'a, CharacterState>, pub shockwave: ReadStorage<'a, Shockwave>, pub beam_segment: ReadStorage<'a, BeamSegment>, @@ -145,7 +145,7 @@ impl<'a> TrackedComps<'a> { .get(entity) .copied() .map(|c| comps.push(c.into())); - self.loadout + self.inventory .get(entity) .cloned() .map(|c| comps.push(c.into())); @@ -181,6 +181,7 @@ pub struct ReadTrackers<'a> { pub health: ReadExpect<'a, UpdateTracker>, pub can_build: ReadExpect<'a, UpdateTracker>, pub light_emitter: ReadExpect<'a, UpdateTracker>, + pub inventory: ReadExpect<'a, UpdateTracker>, pub item: ReadExpect<'a, UpdateTracker>, pub scale: ReadExpect<'a, UpdateTracker>, pub mounting: ReadExpect<'a, UpdateTracker>, @@ -190,7 +191,6 @@ pub struct ReadTrackers<'a> { pub collider: ReadExpect<'a, UpdateTracker>, pub sticky: ReadExpect<'a, UpdateTracker>, pub gravity: ReadExpect<'a, UpdateTracker>, - pub loadout: ReadExpect<'a, UpdateTracker>, pub character_state: ReadExpect<'a, UpdateTracker>, pub shockwave: ReadExpect<'a, UpdateTracker>, pub beam_segment: ReadExpect<'a, UpdateTracker>, @@ -228,7 +228,7 @@ impl<'a> ReadTrackers<'a> { .with_component(&comps.uid, &*self.collider, &comps.collider, filter) .with_component(&comps.uid, &*self.sticky, &comps.sticky, filter) .with_component(&comps.uid, &*self.gravity, &comps.gravity, filter) - .with_component(&comps.uid, &*self.loadout, &comps.loadout, filter) + .with_component(&comps.uid, &*self.inventory, &comps.inventory, filter) .with_component( &comps.uid, &*self.character_state, @@ -263,7 +263,7 @@ pub struct WriteTrackers<'a> { collider: WriteExpect<'a, UpdateTracker>, sticky: WriteExpect<'a, UpdateTracker>, gravity: WriteExpect<'a, UpdateTracker>, - loadout: WriteExpect<'a, UpdateTracker>, + inventory: WriteExpect<'a, UpdateTracker>, character_state: WriteExpect<'a, UpdateTracker>, shockwave: WriteExpect<'a, UpdateTracker>, beam: WriteExpect<'a, UpdateTracker>, @@ -290,7 +290,7 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) { trackers.collider.record_changes(&comps.collider); trackers.sticky.record_changes(&comps.sticky); trackers.gravity.record_changes(&comps.gravity); - trackers.loadout.record_changes(&comps.loadout); + trackers.inventory.record_changes(&comps.inventory); trackers .character_state .record_changes(&comps.character_state); @@ -355,7 +355,7 @@ pub fn register_trackers(world: &mut World) { world.register_tracker::(); world.register_tracker::(); world.register_tracker::(); - world.register_tracker::(); + world.register_tracker::(); world.register_tracker::(); world.register_tracker::(); world.register_tracker::(); diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index 8e1ed95b41..5371387c9e 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -3,7 +3,7 @@ use crate::{ chunk_generator::ChunkGenerator, client::Client, presence::Presence, rtsim::RtSim, Tick, }; use common::{ - comp::{self, bird_medium, item::tool::AbilityMap, Alignment, Pos}, + comp::{self, bird_medium, Alignment, Pos}, event::{EventBus, ServerEvent}, generation::get_npc_name, npc::NPC_NAMES, @@ -14,7 +14,7 @@ use common::{ use common_net::msg::ServerGeneral; use common_sys::state::TerrainChanges; use rand::Rng; -use specs::{Join, Read, ReadExpect, ReadStorage, System, Write, WriteExpect}; +use specs::{Join, Read, ReadStorage, System, Write, WriteExpect}; use std::sync::Arc; use vek::*; @@ -38,7 +38,6 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Pos>, ReadStorage<'a, Presence>, ReadStorage<'a, Client>, - ReadExpect<'a, AbilityMap>, ); fn run( @@ -54,7 +53,6 @@ impl<'a> System<'a> for Sys { positions, presences, clients, - map, ): Self::SystemData, ) { span!(_guard, "run", "terrain::Sys::run"); @@ -162,7 +160,7 @@ impl<'a> System<'a> for Sys { let config = entity.config; - let loadout = LoadoutBuilder::build_loadout(body, main_tool, &map, config).build(); + let loadout = LoadoutBuilder::build_loadout(body, main_tool, config).build(); let health = comp::Health::new(stats.body_type, stats.level.level()); @@ -195,7 +193,10 @@ impl<'a> System<'a> for Sys { Some(entity.pos), can_speak, &body, - matches!(config, Some(common::loadout_builder::LoadoutConfig::Guard)), + matches!( + config, + Some(comp::inventory::loadout_builder::LoadoutConfig::Guard) + ), )) } else { None diff --git a/voxygen/Cargo.toml b/voxygen/Cargo.toml index 34f846f99c..463d1554c7 100644 --- a/voxygen/Cargo.toml +++ b/voxygen/Cargo.toml @@ -73,6 +73,7 @@ glsl-include = "0.3.1" guillotiere = "0.6" hashbrown = {version = "0.9", features = ["rayon", "serde", "nightly"]} image = {version = "0.23.12", default-features = false, features = ["ico", "png"]} +lazy_static = "1.4.0" native-dialog = { version = "0.4.2", default-features = false, optional = true } num = "0.3.1" ordered-float = { version = "2.0.1", default-features = false } diff --git a/voxygen/src/audio/sfx/event_mapper/combat/mod.rs b/voxygen/src/audio/sfx/event_mapper/combat/mod.rs index cfdf3e8175..2f3a16cc7d 100644 --- a/voxygen/src/audio/sfx/event_mapper/combat/mod.rs +++ b/voxygen/src/audio/sfx/event_mapper/combat/mod.rs @@ -10,7 +10,10 @@ use super::EventMapper; use client::Client; use common::{ - comp::{item::ItemKind, CharacterAbilityType, CharacterState, Loadout, Pos}, + comp::{ + inventory::slot::EquipSlot, item::ItemKind, CharacterAbilityType, CharacterState, + Inventory, Pos, + }, terrain::TerrainChunk, vol::ReadVol, }; @@ -56,10 +59,10 @@ impl EventMapper for CombatEventMapper { let focus_off = camera.get_focus_pos().map(f32::trunc); let cam_pos = camera.dependents().cam_pos + focus_off; - for (entity, pos, loadout, character) in ( + for (entity, pos, inventory, character) in ( &ecs.entities(), &ecs.read_storage::(), - ecs.read_storage::().maybe(), + ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), ) .join() @@ -68,7 +71,9 @@ impl EventMapper for CombatEventMapper { if let Some(character) = character { let sfx_state = self.event_history.entry(entity).or_default(); - let mapped_event = Self::map_event(character, sfx_state, loadout); + let mapped_event = inventory.map_or(SfxEvent::Idle, |inv| { + Self::map_event(character, sfx_state, &inv) + }); // Check for SFX config entry for this movement if Self::should_emit(sfx_state, triggers.get_key_value(&mapped_event)) { @@ -138,30 +143,28 @@ impl CombatEventMapper { fn map_event( character_state: &CharacterState, previous_state: &PreviousEntityState, - loadout: Option<&Loadout>, + inventory: &Inventory, ) -> SfxEvent { - if let Some(active_loadout) = loadout { - if let Some(item_config) = &active_loadout.active_item { - if let ItemKind::Tool(data) = item_config.item.kind() { - if character_state.is_attack() { - return SfxEvent::Attack( - CharacterAbilityType::from(character_state), - data.kind, - ); - } else if let Some(wield_event) = match ( - previous_state.weapon_drawn, - character_state.is_dodge(), - Self::weapon_drawn(character_state), - ) { - (false, false, true) => Some(SfxEvent::Wield(data.kind)), - (true, false, false) => Some(SfxEvent::Unwield(data.kind)), - _ => None, - } { - return wield_event; - } + if let Some(item) = inventory.equipped(EquipSlot::Mainhand) { + if let ItemKind::Tool(data) = item.kind() { + if character_state.is_attack() { + return SfxEvent::Attack( + CharacterAbilityType::from(character_state), + data.kind, + ); + } else if let Some(wield_event) = match ( + previous_state.weapon_drawn, + character_state.is_dodge(), + Self::weapon_drawn(character_state), + ) { + (false, false, true) => Some(SfxEvent::Wield(data.kind)), + (true, false, false) => Some(SfxEvent::Unwield(data.kind)), + _ => None, + } { + return wield_event; } - // Check for attacking states } + // Check for attacking states } SfxEvent::Idle diff --git a/voxygen/src/audio/sfx/event_mapper/combat/tests.rs b/voxygen/src/audio/sfx/event_mapper/combat/tests.rs index 677d942164..f71a1745bf 100644 --- a/voxygen/src/audio/sfx/event_mapper/combat/tests.rs +++ b/voxygen/src/audio/sfx/event_mapper/combat/tests.rs @@ -1,24 +1,22 @@ use super::*; use crate::audio::sfx::SfxEvent; use common::{ - comp::{item::tool::ToolKind, CharacterAbilityType, CharacterState, Item, ItemConfig, Loadout}, + comp::{ + inventory::loadout_builder::LoadoutBuilder, item::tool::ToolKind, CharacterAbilityType, + CharacterState, Item, + }, states, }; use std::time::{Duration, Instant}; #[test] fn maps_wield_while_equipping() { - let loadout = Loadout { - active_item: Some(ItemConfig { - item: Item::new_from_asset_expect("common.items.weapons.axe.starter_axe"), - ability1: None, - ability2: None, - ability3: None, - block_ability: None, - dodge_ability: None, - }), - ..Default::default() - }; + let loadout = LoadoutBuilder::new() + .active_item(Some(Item::new_from_asset_expect( + "common.items.weapons.axe.starter_axe", + ))) + .build(); + let inventory = Inventory::new_with_loadout(loadout); let result = CombatEventMapper::map_event( &CharacterState::Equipping(states::equipping::Data { @@ -32,7 +30,7 @@ fn maps_wield_while_equipping() { time: Instant::now(), weapon_drawn: false, }, - Some(&loadout), + &inventory, ); assert_eq!(result, SfxEvent::Wield(ToolKind::Axe)); @@ -40,17 +38,12 @@ fn maps_wield_while_equipping() { #[test] fn maps_unwield() { - let loadout = Loadout { - active_item: Some(ItemConfig { - item: Item::new_from_asset_expect("common.items.weapons.bow.starter_bow"), - ability1: None, - ability2: None, - ability3: None, - block_ability: None, - dodge_ability: None, - }), - ..Default::default() - }; + let loadout = LoadoutBuilder::new() + .active_item(Some(Item::new_from_asset_expect( + "common.items.weapons.bow.starter_bow", + ))) + .build(); + let inventory = Inventory::new_with_loadout(loadout); let result = CombatEventMapper::map_event( &CharacterState::default(), @@ -59,7 +52,7 @@ fn maps_unwield() { time: Instant::now(), weapon_drawn: true, }, - Some(&loadout), + &inventory, ); assert_eq!(result, SfxEvent::Unwield(ToolKind::Bow)); @@ -67,17 +60,12 @@ fn maps_unwield() { #[test] fn maps_basic_melee() { - let loadout = Loadout { - active_item: Some(ItemConfig { - item: Item::new_from_asset_expect("common.items.weapons.axe.starter_axe"), - ability1: None, - ability2: None, - ability3: None, - block_ability: None, - dodge_ability: None, - }), - ..Default::default() - }; + let loadout = LoadoutBuilder::new() + .active_item(Some(Item::new_from_asset_expect( + "common.items.weapons.axe.starter_axe", + ))) + .build(); + let inventory = Inventory::new_with_loadout(loadout); let result = CombatEventMapper::map_event( &CharacterState::BasicMelee(states::basic_melee::Data { @@ -100,7 +88,7 @@ fn maps_basic_melee() { time: Instant::now(), weapon_drawn: true, }, - Some(&loadout), + &inventory, ); assert_eq!( @@ -111,17 +99,12 @@ fn maps_basic_melee() { #[test] fn matches_ability_stage() { - let loadout = Loadout { - active_item: Some(ItemConfig { - item: Item::new_from_asset_expect("common.items.weapons.sword.starter_sword"), - ability1: None, - ability2: None, - ability3: None, - block_ability: None, - dodge_ability: None, - }), - ..Default::default() - }; + let loadout = LoadoutBuilder::new() + .active_item(Some(Item::new_from_asset_expect( + "common.items.weapons.sword.starter_sword", + ))) + .build(); + let inventory = Inventory::new_with_loadout(loadout); let result = CombatEventMapper::map_event( &CharacterState::ComboMelee(states::combo_melee::Data { @@ -159,7 +142,7 @@ fn matches_ability_stage() { time: Instant::now(), weapon_drawn: true, }, - Some(&loadout), + &inventory, ); assert_eq!( @@ -173,17 +156,12 @@ fn matches_ability_stage() { #[test] fn ignores_different_ability_stage() { - let loadout = Loadout { - active_item: Some(ItemConfig { - item: Item::new_from_asset_expect("common.items.weapons.sword.starter_sword"), - ability1: None, - ability2: None, - ability3: None, - block_ability: None, - dodge_ability: None, - }), - ..Default::default() - }; + let loadout = LoadoutBuilder::new() + .active_item(Some(Item::new_from_asset_expect( + "common.items.weapons.axe.starter_axe", + ))) + .build(); + let inventory = Inventory::new_with_loadout(loadout); let result = CombatEventMapper::map_event( &CharacterState::ComboMelee(states::combo_melee::Data { @@ -221,7 +199,7 @@ fn ignores_different_ability_stage() { time: Instant::now(), weapon_drawn: true, }, - Some(&loadout), + &inventory, ); assert_ne!( diff --git a/voxygen/src/hud/bag.rs b/voxygen/src/hud/bag.rs index 0e535cca69..311017dfd5 100644 --- a/voxygen/src/hud/bag.rs +++ b/voxygen/src/hud/bag.rs @@ -4,7 +4,6 @@ use super::{ slots::{ArmorSlot, EquipSlot, InventorySlot, SlotManager}, util::loadout_slot_text, Show, CRITICAL_HP_COLOR, LOW_HP_COLOR, QUALITY_COMMON, TEXT_COLOR, UI_HIGHLIGHT_0, UI_MAIN, - XP_COLOR, }; use crate::{ hud::get_quality_col, @@ -16,13 +15,17 @@ use crate::{ }, }; use client::Client; -use common::comp::{item::Quality, Stats}; +use common::{ + combat::Damage, + comp::{item::Quality, Stats}, +}; use conrod_core::{ color, - widget::{self, Button, Image, Rectangle, Text}, + widget::{self, Button, Image, Rectangle, Scrollbar, Text}, widget_ids, Color, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, }; +use crate::hud::slots::SlotKind; use vek::Vec2; widget_ids! { @@ -45,11 +48,13 @@ widget_ids! { inventory_title, inventory_title_bg, scrollbar_bg, + scrollbar_slots, stats_button, tab_1, tab_2, tab_3, tab_4, + bag_expand_btn, // Stats stats_alignment, level, @@ -70,13 +75,18 @@ widget_ids! { legs_slot, belt_slot, lantern_slot, - ring_slot, + ring1_slot, + ring2_slot, feet_slot, back_slot, tabard_slot, glider_slot, mainhand_slot, offhand_slot, + bag1_slot, + bag2_slot, + bag3_slot, + bag4_slot, // ??? end_ico, fit_ico, @@ -140,7 +150,7 @@ pub struct State { } pub enum Event { - Stats, + BagExpand, Close, } @@ -163,30 +173,34 @@ impl<'a> Widget for Bag<'a> { let widget::UpdateArgs { state, ui, .. } = args; let mut event = None; - - let invs = self.client.inventories(); - let inventory = match invs.get(self.client.entity()) { - Some(i) => i, - None => return None, - }; - let loadouts = self.client.loadouts(); - let loadout = match loadouts.get(self.client.entity()) { + let bag_tooltip = Tooltip::new({ + // Edge images [t, b, r, l] + // Corner images [tr, tl, br, bl] + let edge = &self.rot_imgs.tt_side; + let corner = &self.rot_imgs.tt_corner; + ImageFrame::new( + [edge.cw180, edge.none, edge.cw270, edge.cw90], + [corner.none, corner.cw270, corner.cw90, corner.cw180], + Color::Rgba(0.08, 0.07, 0.04, 1.0), + 5.0, + ) + }) + .title_font_size(self.fonts.cyri.scale(15)) + .parent(ui.window) + .desc_font_size(self.fonts.cyri.scale(12)) + .font_id(self.fonts.cyri.conrod_id) + .desc_text_color(TEXT_COLOR); + let inventories = self.client.inventories(); + let inventory = match inventories.get(self.client.entity()) { Some(l) => l, None => return None, }; - let exp_percentage = (self.stats.exp.current() as f64) / (self.stats.exp.maximum() as f64); - let exp_threshold = format!( - "{}/{} {}", - self.stats.exp.current(), - self.stats.exp.maximum(), - &self.localized_strings.get("hud.bag.exp") - ); - let space_used = inventory.amount(); - let space_max = inventory.slots().len(); + + let space_used = inventory.populated_slots(); + let space_max = inventory.slots().count(); let bag_space = format!("{}/{}", space_used, space_max); let bag_space_percentage = space_used as f32 / space_max as f32; - let level = (self.stats.level.level()).to_string(); - let currency = 0; // TODO: Add as a Stat + let currency = 0; // TODO: Add as a Stat // Tooltips let item_tooltip = Tooltip::new({ @@ -209,6 +223,8 @@ impl<'a> Widget for Bag<'a> { // BG Image::new(if self.show.stats { self.imgs.inv_bg_stats + } else if self.show.bag_inv { + self.imgs.inv_bg_bag } else { self.imgs.inv_bg_armor }) @@ -216,11 +232,15 @@ impl<'a> Widget for Bag<'a> { .bottom_right_with_margins_on(ui.window, 60.0, 5.0) .color(Some(UI_MAIN)) .set(state.ids.bg, ui); - Image::new(self.imgs.inv_frame) - .w_h(424.0, 708.0) - .middle_of(state.ids.bg) - .color(Some(UI_HIGHLIGHT_0)) - .set(state.ids.bg_frame, ui); + Image::new(if self.show.bag_inv { + self.imgs.inv_frame_bag + } else { + self.imgs.inv_frame + }) + .w_h(424.0, 708.0) + .middle_of(state.ids.bg) + .color(Some(UI_HIGHLIGHT_0)) + .set(state.ids.bg_frame, ui); // Title Text::new( &self @@ -244,12 +264,36 @@ impl<'a> Widget for Bag<'a> { .font_size(self.fonts.cyri.scale(20)) .color(TEXT_COLOR) .set(state.ids.inventory_title, ui); - // Scrollbar-BG - Image::new(self.imgs.scrollbar_bg) - .w_h(9.0, 173.0) - .bottom_right_with_margins_on(state.ids.bg_frame, 42.0, 3.0) - .color(Some(UI_HIGHLIGHT_0)) - .set(state.ids.scrollbar_bg, ui); + // Slots Scrollbar + if space_max > 45 && !self.show.bag_inv { + // Scrollbar-BG + Image::new(self.imgs.scrollbar_bg) + .w_h(9.0, 173.0) + .bottom_right_with_margins_on(state.ids.bg_frame, 42.0, 3.0) + .color(Some(UI_HIGHLIGHT_0)) + .set(state.ids.scrollbar_bg, ui); + // Scrollbar + Scrollbar::y_axis(state.ids.inv_alignment) + .thickness(5.0) + .h(123.0) + .color(UI_MAIN) + .middle_of(state.ids.scrollbar_bg) + .set(state.ids.scrollbar_slots, ui); + } else if space_max > 135 { + // Scrollbar-BG + Image::new(self.imgs.scrollbar_bg_big) + .w_h(9.0, 592.0) + .bottom_right_with_margins_on(state.ids.bg_frame, 42.0, 3.0) + .color(Some(UI_HIGHLIGHT_0)) + .set(state.ids.scrollbar_bg, ui); + // Scrollbar + Scrollbar::y_axis(state.ids.inv_alignment) + .thickness(5.0) + .h(542.0) + .color(UI_MAIN) + .middle_of(state.ids.scrollbar_bg) + .set(state.ids.scrollbar_slots, ui); + }; // Char Pixel-Art Image::new(self.imgs.char_art) .w_h(40.0, 37.0) @@ -280,71 +324,136 @@ impl<'a> Widget for Bag<'a> { }) .set(state.ids.space_txt, ui); // Alignment for Grid - Rectangle::fill_with([362.0, 200.0], color::TRANSPARENT) - .bottom_left_with_margins_on(state.ids.bg_frame, 29.0, 44.0) - .scroll_kids_vertically() - .set(state.ids.inv_alignment, ui); + Rectangle::fill_with( + [362.0, if self.show.bag_inv { 600.0 } else { 200.0 }], + color::TRANSPARENT, + ) + .bottom_left_with_margins_on(state.ids.bg_frame, 29.0, 46.5) + .scroll_kids_vertically() + .set(state.ids.inv_alignment, ui); + // Button to expand bag + let txt = if self.show.bag_inv { + "Show Loadout" + } else { + "Expand Bag" + }; + let expand_btn = Button::image(if self.show.bag_inv { + self.imgs.collapse_btn + } else { + self.imgs.expand_btn + }) + .w_h(30.0, 17.0) + .hover_image(if self.show.bag_inv { + self.imgs.collapse_btn_hover + } else { + self.imgs.expand_btn_hover + }) + .press_image(if self.show.bag_inv { + self.imgs.collapse_btn_press + } else { + self.imgs.expand_btn_press + }); + // Only show expand button when it's needed... + if space_max > 45 && !self.show.bag_inv { + if expand_btn + .top_left_with_margins_on(state.ids.bg_frame, 460.0, 211.5) + .with_tooltip(self.tooltip_manager, &txt, "", &bag_tooltip, TEXT_COLOR) + .set(state.ids.bag_expand_btn, ui) + .was_clicked() + { + event = Some(Event::BagExpand); + } + } else if self.show.bag_inv { + //... but always show it when the bag is expanded + if expand_btn + .top_left_with_margins_on(state.ids.bg_frame, 53.0, 211.5) + .with_tooltip(self.tooltip_manager, &txt, "", &bag_tooltip, TEXT_COLOR) + .set(state.ids.bag_expand_btn, ui) + .was_clicked() + { + event = Some(Event::BagExpand); + } + } - if !self.show.stats { - // Title - Text::new( - &self - .localized_strings - .get("hud.bag.inventory") - .replace("{playername}", &self.stats.name.to_string().as_str()), - ) - .mid_top_with_margin_on(state.ids.bg_frame, 9.0) - .font_id(self.fonts.cyri.conrod_id) - .font_size(self.fonts.cyri.scale(22)) - .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) - .set(state.ids.inventory_title_bg, ui); - Text::new( - &self - .localized_strings - .get("hud.bag.inventory") - .replace("{playername}", &self.stats.name.to_string().as_str()), - ) - .top_left_with_margins_on(state.ids.inventory_title_bg, 2.0, 2.0) - .font_id(self.fonts.cyri.conrod_id) - .font_size(self.fonts.cyri.scale(22)) - .color(TEXT_COLOR) - .set(state.ids.inventory_title, ui); - // Armor Slots - let mut slot_maker = SlotMaker { - empty_slot: self.imgs.armor_slot_empty, - filled_slot: self.imgs.armor_slot, - selected_slot: self.imgs.armor_slot_sel, - background_color: Some(UI_HIGHLIGHT_0), - content_size: ContentSize { - width_height_ratio: 1.0, - max_fraction: 0.75, /* Changes the item image size by setting a maximum - * fraction - * of either the width or height */ - }, - selected_content_scale: 1.067, - amount_font: self.fonts.cyri.conrod_id, - amount_margins: Vec2::new(-4.0, 0.0), - amount_font_size: self.fonts.cyri.scale(12), - amount_text_color: TEXT_COLOR, - content_source: loadout, - image_source: self.item_imgs, - slot_manager: Some(self.slot_manager), - }; - let i18n = &self.localized_strings; - let filled_slot = self.imgs.armor_slot; + // Title + Text::new( + &self + .localized_strings + .get("hud.bag.inventory") + .replace("{playername}", &self.stats.name.to_string().as_str()), + ) + .mid_top_with_margin_on(state.ids.bg_frame, 9.0) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(22)) + .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) + .set(state.ids.inventory_title_bg, ui); + Text::new( + &self + .localized_strings + .get("hud.bag.inventory") + .replace("{playername}", &self.stats.name.to_string().as_str()), + ) + .top_left_with_margins_on(state.ids.inventory_title_bg, 2.0, 2.0) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(22)) + .color(TEXT_COLOR) + .set(state.ids.inventory_title, ui); + // Armor Slots + let mut slot_maker = SlotMaker { + empty_slot: self.imgs.armor_slot_empty, + filled_slot: self.imgs.armor_slot, + selected_slot: self.imgs.armor_slot_sel, + background_color: Some(UI_HIGHLIGHT_0), + content_size: ContentSize { + width_height_ratio: 1.0, + max_fraction: 0.75, /* Changes the item image size by setting a maximum + * fraction + * of either the width or height */ + }, + selected_content_scale: 1.067, + amount_font: self.fonts.cyri.conrod_id, + amount_margins: Vec2::new(-4.0, 0.0), + amount_font_size: self.fonts.cyri.scale(12), + amount_text_color: TEXT_COLOR, + content_source: inventory, + image_source: self.item_imgs, + slot_manager: Some(self.slot_manager), + }; + let i18n = &self.localized_strings; + let filled_slot = self.imgs.armor_slot; + if !self.show.bag_inv { + let damage_reduction = (100.0 * Damage::compute_damage_reduction(inventory)) as i32; + Button::image(self.imgs.protection_ico) + .w_h(20.0, 20.0) + .top_left_with_margins_on(state.ids.bg_frame, 51.0, 5.0) + .color(UI_HIGHLIGHT_0) + .label(&format!("{}%", damage_reduction)) + .label_y(conrod_core::position::Relative::Scalar(2.0)) + .label_x(conrod_core::position::Relative::Scalar(25.0)) + .label_color(TEXT_COLOR) + .label_font_size(self.fonts.cyri.scale(12)) + .label_font_id(self.fonts.cyri.conrod_id) + .with_tooltip( + self.tooltip_manager, + "Protection", + "Damage reduction through armor", + &bag_tooltip, + TEXT_COLOR, + ) + .set(state.ids.prot_ico, ui); // Head - let (title, desc) = - loadout_slot_text(loadout.head.as_ref(), || (i18n.get("hud.bag.head"), "")); - let head_q_col = loadout - .head - .as_ref() + let (title, desc) = loadout_slot_text( + inventory.equipped(EquipSlot::Armor(ArmorSlot::Head)), + || (i18n.get("hud.bag.head"), ""), + ); + let head_q_col = inventory + .equipped(EquipSlot::Armor(ArmorSlot::Head)) .map(|item| get_quality_col(item)) .unwrap_or(QUALITY_COMMON); slot_maker .fabricate(EquipSlot::Armor(ArmorSlot::Head), [45.0; 2]) .mid_top_with_margin_on(state.ids.bg_frame, 60.0) .with_icon(self.imgs.head_bg, Vec2::new(32.0, 40.0), Some(UI_MAIN)) - .with_background_color(TEXT_COLOR) .filled_slot(filled_slot) .with_tooltip( self.tooltip_manager, @@ -355,11 +464,12 @@ impl<'a> Widget for Bag<'a> { ) .set(state.ids.head_slot, ui); // Necklace - let (title, desc) = - loadout_slot_text(loadout.neck.as_ref(), || (i18n.get("hud.bag.neck"), "")); - let neck_q_col = loadout - .neck - .as_ref() + let (title, desc) = loadout_slot_text( + inventory.equipped(EquipSlot::Armor(ArmorSlot::Neck)), + || (i18n.get("hud.bag.neck"), ""), + ); + let neck_q_col = inventory + .equipped(EquipSlot::Armor(ArmorSlot::Neck)) .map(|item| get_quality_col(item)) .unwrap_or(QUALITY_COMMON); slot_maker @@ -377,11 +487,12 @@ impl<'a> Widget for Bag<'a> { .set(state.ids.neck_slot, ui); // Chest //Image::new(self.imgs.armor_slot) // different graphics for empty/non empty - let (title, desc) = - loadout_slot_text(loadout.chest.as_ref(), || (i18n.get("hud.bag.chest"), "")); - let chest_q_col = loadout - .chest - .as_ref() + let (title, desc) = loadout_slot_text( + inventory.equipped(EquipSlot::Armor(ArmorSlot::Chest)), + || (i18n.get("hud.bag.chest"), ""), + ); + let chest_q_col = inventory + .equipped(EquipSlot::Armor(ArmorSlot::Chest)) .map(|item| get_quality_col(item)) .unwrap_or(QUALITY_COMMON); slot_maker @@ -398,12 +509,12 @@ impl<'a> Widget for Bag<'a> { ) .set(state.ids.chest_slot, ui); // Shoulders - let (title, desc) = loadout_slot_text(loadout.shoulder.as_ref(), || { - (i18n.get("hud.bag.shoulders"), "") - }); - let shoulder_q_col = loadout - .shoulder - .as_ref() + let (title, desc) = loadout_slot_text( + inventory.equipped(EquipSlot::Armor(ArmorSlot::Shoulders)), + || (i18n.get("hud.bag.shoulders"), ""), + ); + let shoulder_q_col = inventory + .equipped(EquipSlot::Armor(ArmorSlot::Shoulders)) .map(|item| get_quality_col(item)) .unwrap_or(QUALITY_COMMON); slot_maker @@ -420,11 +531,12 @@ impl<'a> Widget for Bag<'a> { ) .set(state.ids.shoulders_slot, ui); // Hands - let (title, desc) = - loadout_slot_text(loadout.hand.as_ref(), || (i18n.get("hud.bag.hands"), "")); - let chest_q_col = loadout - .hand - .as_ref() + let (title, desc) = loadout_slot_text( + inventory.equipped(EquipSlot::Armor(ArmorSlot::Hands)), + || (i18n.get("hud.bag.hands"), ""), + ); + let chest_q_col = inventory + .equipped(EquipSlot::Armor(ArmorSlot::Hands)) .map(|item| get_quality_col(item)) .unwrap_or(QUALITY_COMMON); slot_maker @@ -441,11 +553,12 @@ impl<'a> Widget for Bag<'a> { ) .set(state.ids.hands_slot, ui); // Belt - let (title, desc) = - loadout_slot_text(loadout.belt.as_ref(), || (i18n.get("hud.bag.belt"), "")); - let belt_q_col = loadout - .belt - .as_ref() + let (title, desc) = loadout_slot_text( + inventory.equipped(EquipSlot::Armor(ArmorSlot::Belt)), + || (i18n.get("hud.bag.belt"), ""), + ); + let belt_q_col = inventory + .equipped(EquipSlot::Armor(ArmorSlot::Belt)) .map(|item| get_quality_col(item)) .unwrap_or(QUALITY_COMMON); slot_maker @@ -462,11 +575,12 @@ impl<'a> Widget for Bag<'a> { ) .set(state.ids.belt_slot, ui); // Legs - let (title, desc) = - loadout_slot_text(loadout.pants.as_ref(), || (i18n.get("hud.bag.legs"), "")); - let legs_q_col = loadout - .pants - .as_ref() + let (title, desc) = loadout_slot_text( + inventory.equipped(EquipSlot::Armor(ArmorSlot::Legs)), + || (i18n.get("hud.bag.legs"), ""), + ); + let legs_q_col = inventory + .equipped(EquipSlot::Armor(ArmorSlot::Legs)) .map(|item| get_quality_col(item)) .unwrap_or(QUALITY_COMMON); slot_maker @@ -482,38 +596,17 @@ impl<'a> Widget for Bag<'a> { legs_q_col, ) .set(state.ids.legs_slot, ui); - // Lantern - let (title, desc) = loadout_slot_text(loadout.lantern.as_ref(), || { - (i18n.get("hud.bag.lantern"), "") - }); - let lantern_q_col = loadout - .lantern - .as_ref() - .map(|item| get_quality_col(item)) - .unwrap_or(QUALITY_COMMON); - slot_maker - .fabricate(EquipSlot::Lantern, [45.0; 2]) - .bottom_right_with_margins_on(state.ids.shoulders_slot, -55.0, 0.0) - .with_icon(self.imgs.lantern_bg, Vec2::new(24.0, 38.0), Some(UI_MAIN)) - .filled_slot(filled_slot) - .with_tooltip( - self.tooltip_manager, - title, - &*desc, - &item_tooltip, - lantern_q_col, - ) - .set(state.ids.lantern_slot, ui); // Ring - let (title, desc) = - loadout_slot_text(loadout.ring.as_ref(), || (i18n.get("hud.bag.ring"), "")); - let ring_q_col = loadout - .ring - .as_ref() + let (title, desc) = loadout_slot_text( + inventory.equipped(EquipSlot::Armor(ArmorSlot::Ring1)), + || (i18n.get("hud.bag.ring"), ""), + ); + let ring_q_col = inventory + .equipped(EquipSlot::Armor(ArmorSlot::Ring1)) .map(|item| get_quality_col(item)) .unwrap_or(QUALITY_COMMON); slot_maker - .fabricate(EquipSlot::Armor(ArmorSlot::Ring), [45.0; 2]) + .fabricate(EquipSlot::Armor(ArmorSlot::Ring1), [45.0; 2]) .bottom_left_with_margins_on(state.ids.hands_slot, -55.0, 0.0) .with_icon(self.imgs.ring_bg, Vec2::new(36.0, 40.0), Some(UI_MAIN)) .filled_slot(filled_slot) @@ -524,18 +617,41 @@ impl<'a> Widget for Bag<'a> { &item_tooltip, ring_q_col, ) - .set(state.ids.ring_slot, ui); + .set(state.ids.ring1_slot, ui); + // Ring 2 + let (title, desc) = loadout_slot_text( + inventory.equipped(EquipSlot::Armor(ArmorSlot::Ring2)), + || (i18n.get("hud.bag.ring"), ""), + ); + let ring2_q_col = inventory + .equipped(EquipSlot::Armor(ArmorSlot::Ring2)) + .map(|item| get_quality_col(item)) + .unwrap_or(QUALITY_COMMON); + slot_maker + .fabricate(EquipSlot::Armor(ArmorSlot::Ring2), [45.0; 2]) + .bottom_right_with_margins_on(state.ids.shoulders_slot, -55.0, 0.0) + .with_icon(self.imgs.ring_bg, Vec2::new(36.0, 40.0), Some(UI_MAIN)) + .filled_slot(filled_slot) + .with_tooltip( + self.tooltip_manager, + title, + &*desc, + &item_tooltip, + ring2_q_col, + ) + .set(state.ids.ring2_slot, ui); // Back - let (title, desc) = - loadout_slot_text(loadout.back.as_ref(), || (i18n.get("hud.bag.back"), "")); - let back_q_col = loadout - .back - .as_ref() + let (title, desc) = loadout_slot_text( + inventory.equipped(EquipSlot::Armor(ArmorSlot::Back)), + || (i18n.get("hud.bag.back"), ""), + ); + let back_q_col = inventory + .equipped(EquipSlot::Armor(ArmorSlot::Back)) .map(|item| get_quality_col(item)) .unwrap_or(QUALITY_COMMON); slot_maker .fabricate(EquipSlot::Armor(ArmorSlot::Back), [45.0; 2]) - .down_from(state.ids.lantern_slot, 10.0) + .down_from(state.ids.ring2_slot, 10.0) .with_icon(self.imgs.back_bg, Vec2::new(33.0, 40.0), Some(UI_MAIN)) .filled_slot(filled_slot) .with_tooltip( @@ -547,16 +663,17 @@ impl<'a> Widget for Bag<'a> { ) .set(state.ids.back_slot, ui); // Foot - let (title, desc) = - loadout_slot_text(loadout.foot.as_ref(), || (i18n.get("hud.bag.feet"), "")); - let foot_q_col = loadout - .foot - .as_ref() + let (title, desc) = loadout_slot_text( + inventory.equipped(EquipSlot::Armor(ArmorSlot::Feet)), + || (i18n.get("hud.bag.feet"), ""), + ); + let foot_q_col = inventory + .equipped(EquipSlot::Armor(ArmorSlot::Feet)) .map(|item| get_quality_col(item)) .unwrap_or(QUALITY_COMMON); slot_maker .fabricate(EquipSlot::Armor(ArmorSlot::Feet), [45.0; 2]) - .down_from(state.ids.ring_slot, 10.0) + .down_from(state.ids.ring1_slot, 10.0) .with_icon(self.imgs.feet_bg, Vec2::new(32.0, 40.0), Some(UI_MAIN)) .filled_slot(filled_slot) .with_tooltip( @@ -567,39 +684,39 @@ impl<'a> Widget for Bag<'a> { foot_q_col, ) .set(state.ids.feet_slot, ui); - // Tabard - let (title, desc) = - loadout_slot_text(loadout.tabard.as_ref(), || (i18n.get("hud.bag.tabard"), "")); - let tabard_q_col = loadout - .tabard - .as_ref() + // Lantern + let (title, desc) = loadout_slot_text(inventory.equipped(EquipSlot::Lantern), || { + (i18n.get("hud.bag.lantern"), "") + }); + let lantern_q_col = inventory + .equipped(EquipSlot::Lantern) .map(|item| get_quality_col(item)) .unwrap_or(QUALITY_COMMON); slot_maker - .fabricate(EquipSlot::Armor(ArmorSlot::Tabard), [70.0; 2]) - .top_right_with_margins_on(state.ids.bg_frame, 80.5, 53.0) - .with_icon(self.imgs.tabard_bg, Vec2::new(60.0, 60.0), Some(UI_MAIN)) + .fabricate(EquipSlot::Lantern, [45.0; 2]) + .top_right_with_margins_on(state.ids.bg_frame, 60.0, 5.0) + .with_icon(self.imgs.lantern_bg, Vec2::new(24.0, 38.0), Some(UI_MAIN)) .filled_slot(filled_slot) .with_tooltip( self.tooltip_manager, title, &*desc, &item_tooltip, - tabard_q_col, + lantern_q_col, ) - .set(state.ids.tabard_slot, ui); + .set(state.ids.lantern_slot, ui); // Glider - let (title, desc) = - loadout_slot_text(loadout.glider.as_ref(), || (i18n.get("hud.bag.glider"), "")); - let glider_q_col = loadout - .glider - .as_ref() + let (title, desc) = loadout_slot_text(inventory.equipped(EquipSlot::Glider), || { + (i18n.get("hud.bag.glider"), "") + }); + let glider_q_col = inventory + .equipped(EquipSlot::Glider) .map(|item| get_quality_col(item)) .unwrap_or(QUALITY_COMMON); slot_maker - .fabricate(EquipSlot::Glider, [70.0; 2]) - .top_left_with_margins_on(state.ids.bg_frame, 80.5, 53.0) - .with_icon(self.imgs.glider_bg, Vec2::new(60.0, 60.0), Some(UI_MAIN)) + .fabricate(EquipSlot::Glider, [45.0; 2]) + .down_from(state.ids.lantern_slot, 5.0) + .with_icon(self.imgs.glider_bg, Vec2::new(38.0, 38.0), Some(UI_MAIN)) .filled_slot(filled_slot) .with_tooltip( self.tooltip_manager, @@ -609,15 +726,35 @@ impl<'a> Widget for Bag<'a> { glider_q_col, ) .set(state.ids.glider_slot, ui); + // Tabard + let (title, desc) = loadout_slot_text( + inventory.equipped(EquipSlot::Armor(ArmorSlot::Tabard)), + || (i18n.get("hud.bag.tabard"), ""), + ); + let tabard_q_col = inventory + .equipped(EquipSlot::Armor(ArmorSlot::Tabard)) + .map(|item| get_quality_col(item)) + .unwrap_or(QUALITY_COMMON); + slot_maker + .fabricate(EquipSlot::Armor(ArmorSlot::Tabard), [45.0; 2]) + .down_from(state.ids.glider_slot, 5.0) + .with_icon(self.imgs.tabard_bg, Vec2::new(38.0, 38.0), Some(UI_MAIN)) + .filled_slot(filled_slot) + .with_tooltip( + self.tooltip_manager, + title, + &*desc, + &item_tooltip, + tabard_q_col, + ) + .set(state.ids.tabard_slot, ui); // Mainhand/Left-Slot - let (title, desc) = - loadout_slot_text(loadout.active_item.as_ref().map(|i| &i.item), || { - (i18n.get("hud.bag.mainhand"), "") - }); - let mainhand_q_col = loadout - .active_item - .as_ref() - .map(|item| get_quality_col(&item.item)) + let (title, desc) = loadout_slot_text(inventory.equipped(EquipSlot::Mainhand), || { + (i18n.get("hud.bag.mainhand"), "") + }); + let mainhand_q_col = inventory + .equipped(EquipSlot::Mainhand) + .map(|item| get_quality_col(item)) .unwrap_or(QUALITY_COMMON); slot_maker .fabricate(EquipSlot::Mainhand, [85.0; 2]) @@ -633,14 +770,12 @@ impl<'a> Widget for Bag<'a> { ) .set(state.ids.mainhand_slot, ui); // Offhand/Right-Slot - let (title, desc) = - loadout_slot_text(loadout.second_item.as_ref().map(|i| &i.item), || { - (i18n.get("hud.bag.offhand"), "") - }); - let offhand_q_col = loadout - .second_item - .as_ref() - .map(|item| get_quality_col(&item.item)) + let (title, desc) = loadout_slot_text(inventory.equipped(EquipSlot::Offhand), || { + (i18n.get("hud.bag.offhand"), "") + }); + let offhand_q_col = inventory + .equipped(EquipSlot::Offhand) + .map(|item| get_quality_col(item)) .unwrap_or(QUALITY_COMMON); slot_maker .fabricate(EquipSlot::Offhand, [85.0; 2]) @@ -655,130 +790,123 @@ impl<'a> Widget for Bag<'a> { offhand_q_col, ) .set(state.ids.offhand_slot, ui); - } else { - // Stats - // Title - Text::new( - &self - .localized_strings - .get("hud.bag.stats_title") - .replace("{playername}", &self.stats.name.to_string().as_str()), - ) - .mid_top_with_margin_on(state.ids.bg_frame, 9.0) - .font_id(self.fonts.cyri.conrod_id) - .font_size(self.fonts.cyri.scale(22)) - .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) - .set(state.ids.inventory_title_bg, ui); - Text::new( - &self - .localized_strings - .get("hud.bag.stats_title") - .replace("{playername}", &self.stats.name.to_string().as_str()), - ) - .top_left_with_margins_on(state.ids.inventory_title_bg, 2.0, 2.0) - .font_id(self.fonts.cyri.conrod_id) - .font_size(self.fonts.cyri.scale(22)) - .color(TEXT_COLOR) - .set(state.ids.inventory_title, ui); - // Alignment for Stats - Rectangle::fill_with([418.0, 384.0], color::TRANSPARENT) - .mid_top_with_margin_on(state.ids.bg_frame, 48.0) - .scroll_kids_vertically() - .set(state.ids.stats_alignment, ui); - // Level - Text::new(&level) - .mid_top_with_margin_on(state.ids.stats_alignment, 10.0) - .font_id(self.fonts.cyri.conrod_id) - .font_size(self.fonts.cyri.scale(30)) - .color(TEXT_COLOR) - .set(state.ids.level, ui); - - // Exp-Bar Background - Rectangle::fill_with([170.0, 10.0], color::BLACK) - .mid_top_with_margin_on(state.ids.stats_alignment, 50.0) - .set(state.ids.exp_rectangle, ui); - - // Exp-Bar Progress - Rectangle::fill_with([170.0 * (exp_percentage), 6.0], XP_COLOR) // 0.8 = Experience percentage - .mid_left_with_margin_on(state.ids.expbar, 1.0) - .set(state.ids.exp_progress_rectangle, ui); - - // Exp-Bar Foreground Frame - Image::new(self.imgs.progress_frame) - .w_h(170.0, 10.0) - .middle_of(state.ids.exp_rectangle) - .color(Some(UI_HIGHLIGHT_0)) - .set(state.ids.expbar, ui); - - // Exp-Text - Text::new(&exp_threshold) - .mid_top_with_margin_on(state.ids.expbar, 10.0) - .font_id(self.fonts.cyri.conrod_id) - .font_size(self.fonts.cyri.scale(15)) - .color(TEXT_COLOR) - .set(state.ids.exp, ui); - - // Divider - /*Image::new(self.imgs.divider) - .w_h(50.0, 5.0) - .mid_top_with_margin_on(state.ids.exp, 20.0) - .color(Some(UI_HIGHLIGHT_0)) - .set(state.ids.divider, ui);*/ - - // Stats - // Defense - let damage_reduction = (100.0 * loadout.get_damage_reduction()) as i32; - - Text::new( - &self - .localized_strings - .get("character_window.character_stats"), - ) - .top_left_with_margins_on(state.ids.stats_alignment, 120.0, 150.0) - .font_id(self.fonts.cyri.conrod_id) - .font_size(self.fonts.cyri.scale(16)) - .color(TEXT_COLOR) - .set(state.ids.statnames, ui); - Image::new(self.imgs.endurance_ico) - .w_h(20.0, 20.0) - .top_left_with_margins_on(state.ids.statnames, 0.0, -40.0) - .color(Some(UI_HIGHLIGHT_0)) - .set(state.ids.end_ico, ui); - Image::new(self.imgs.fitness_ico) - .w_h(20.0, 20.0) - .down_from(state.ids.end_ico, 15.0) - .color(Some(UI_HIGHLIGHT_0)) - .set(state.ids.fit_ico, ui); - Image::new(self.imgs.willpower_ico) - .w_h(20.0, 20.0) - .down_from(state.ids.fit_ico, 15.0) - .color(Some(UI_HIGHLIGHT_0)) - .set(state.ids.wp_ico, ui); - Image::new(self.imgs.protection_ico) - .w_h(20.0, 20.0) - .down_from(state.ids.wp_ico, 15.0) - .color(Some(UI_HIGHLIGHT_0)) - .set(state.ids.prot_ico, ui); - - Text::new(&format!( - "{}\n\n{}\n\n{}\n\n{}%", - self.stats.endurance, self.stats.fitness, self.stats.willpower, damage_reduction - )) - .top_right_with_margins_on(state.ids.stats_alignment, 120.0, 130.0) - .font_id(self.fonts.cyri.conrod_id) - .font_size(self.fonts.cyri.scale(16)) - .color(TEXT_COLOR) - .set(state.ids.stats, ui); } + // Bag 1 + let (title, desc) = loadout_slot_text( + inventory.equipped(EquipSlot::Armor(ArmorSlot::Bag1)), + || (i18n.get("hud.bag.bag"), ""), + ); + let bag1_q_col = inventory + .equipped(EquipSlot::Armor(ArmorSlot::Bag1)) + .map(|item| get_quality_col(item)) + .unwrap_or(QUALITY_COMMON); + slot_maker + .fabricate(EquipSlot::Armor(ArmorSlot::Bag1), [35.0; 2]) + .bottom_left_with_margins_on( + state.ids.bg_frame, + if self.show.bag_inv { 600.0 } else { 167.0 }, + 3.0, + ) + .with_icon(self.imgs.bag_bg, Vec2::new(28.0, 24.0), Some(UI_MAIN)) + .filled_slot(filled_slot) + .with_tooltip( + self.tooltip_manager, + title, + &*desc, + &item_tooltip, + bag1_q_col, + ) + .set(state.ids.bag1_slot, ui); + // Bag 2 + let (title, desc) = loadout_slot_text( + inventory.equipped(EquipSlot::Armor(ArmorSlot::Bag2)), + || (i18n.get("hud.bag.bag"), ""), + ); + let bag2_q_col = inventory + .equipped(EquipSlot::Armor(ArmorSlot::Bag2)) + .map(|item| get_quality_col(item)) + .unwrap_or(QUALITY_COMMON); + slot_maker + .fabricate(EquipSlot::Armor(ArmorSlot::Bag2), [35.0; 2]) + .down_from(state.ids.bag1_slot, 2.0) + .with_icon(self.imgs.bag_bg, Vec2::new(28.0, 24.0), Some(UI_MAIN)) + .filled_slot(filled_slot) + .with_tooltip( + self.tooltip_manager, + title, + &*desc, + &item_tooltip, + bag2_q_col, + ) + .set(state.ids.bag2_slot, ui); + // Bag 3 + let (title, desc) = loadout_slot_text( + inventory.equipped(EquipSlot::Armor(ArmorSlot::Bag3)), + || (i18n.get("hud.bag.bag"), ""), + ); + let bag3_q_col = inventory + .equipped(EquipSlot::Armor(ArmorSlot::Bag3)) + .map(|item| get_quality_col(item)) + .unwrap_or(QUALITY_COMMON); + slot_maker + .fabricate(EquipSlot::Armor(ArmorSlot::Bag3), [35.0; 2]) + .down_from(state.ids.bag2_slot, 2.0) + .with_icon(self.imgs.bag_bg, Vec2::new(28.0, 24.0), Some(UI_MAIN)) + .filled_slot(filled_slot) + .with_tooltip( + self.tooltip_manager, + title, + &*desc, + &item_tooltip, + bag3_q_col, + ) + .set(state.ids.bag3_slot, ui); + // Bag 4 + let (title, desc) = loadout_slot_text( + inventory.equipped(EquipSlot::Armor(ArmorSlot::Bag4)), + || (i18n.get("hud.bag.bag"), ""), + ); + let bag4_q_col = inventory + .equipped(EquipSlot::Armor(ArmorSlot::Bag4)) + .map(|item| get_quality_col(item)) + .unwrap_or(QUALITY_COMMON); + slot_maker + .fabricate(EquipSlot::Armor(ArmorSlot::Bag4), [35.0; 2]) + .down_from(state.ids.bag3_slot, 2.0) + .with_icon(self.imgs.bag_bg, Vec2::new(28.0, 24.0), Some(UI_MAIN)) + .filled_slot(filled_slot) + .with_tooltip( + self.tooltip_manager, + title, + &*desc, + &item_tooltip, + bag4_q_col, + ) + .set(state.ids.bag4_slot, ui); // Bag Slots // Create available inventory slot widgets - if state.ids.inv_slots.len() < inventory.len() { + if state.ids.inv_slots.len() < inventory.capacity() { state.update(|s| { s.ids .inv_slots - .resize(inventory.len(), &mut ui.widget_id_generator()); + .resize(inventory.capacity(), &mut ui.widget_id_generator()); }); } + + // Determine the range of inventory slots that are provided by the loadout item + // that the mouse is over + let mouseover_loadout_slots = self + .slot_manager + .mouse_over_slot + .and_then(|x| { + if let SlotKind::Equip(e) = x { + inventory.get_slot_range_for_equip_slot(e) + } else { + None + } + }) + .unwrap_or(0usize..0usize); + // Display inventory contents let mut slot_maker = SlotMaker { empty_slot: self.imgs.inv_slot, @@ -798,18 +926,25 @@ impl<'a> Widget for Bag<'a> { image_source: self.item_imgs, slot_manager: Some(self.slot_manager), }; - for (i, item) in inventory.slots().iter().enumerate() { + + for (i, (pos, item)) in inventory.slots_with_id().enumerate() { let x = i % 9; let y = i / 9; // Slot - let slot_widget = slot_maker - .fabricate(InventorySlot(i), [40.0; 2]) + let mut slot_widget = slot_maker + .fabricate(InventorySlot(pos), [40.0; 2]) .top_left_with_margins_on( state.ids.inv_alignment, 0.0 + y as f64 * (40.0), 0.0 + x as f64 * (40.0), ); + + // Highlight slots are provided by the loadout item that the mouse is over + if mouseover_loadout_slots.contains(&i) { + slot_widget = slot_widget.with_background_color(Color::Rgba(1.0, 1.0, 1.0, 1.0)); + } + if let Some(item) = item { let (title, desc) = super::util::item_text(item); let quality_col = get_quality_col(item); @@ -837,83 +972,6 @@ impl<'a> Widget for Bag<'a> { slot_widget.set(state.ids.inv_slots[i], ui); } } - - // Stats Button - if Button::image(self.imgs.button) - .w_h(92.0, 22.0) - .mid_top_with_margin_on(state.ids.bg, 435.0) - .hover_image(self.imgs.button_hover) - .press_image(self.imgs.button_press) - .label(if self.show.stats { - &self.localized_strings.get("hud.bag.armor") - } else { - &self.localized_strings.get("hud.bag.stats") - }) - .label_y(conrod_core::position::Relative::Scalar(1.0)) - .label_color(TEXT_COLOR) - .label_font_size(self.fonts.cyri.scale(12)) - .label_font_id(self.fonts.cyri.conrod_id) - .set(state.ids.stats_button, ui) - .was_clicked() - { - return Some(Event::Stats); - }; - // Tabs - if Button::image(self.imgs.inv_tab_active) - .w_h(28.0, 44.0) - .bottom_left_with_margins_on(state.ids.bg, 172.0, 13.0) - .image_color(UI_MAIN) - .set(state.ids.tab_1, ui) - .was_clicked() - {} - if Button::image(self.imgs.inv_tab_inactive) - .w_h(28.0, 44.0) - .hover_image(self.imgs.inv_tab_inactive_hover) - .press_image(self.imgs.inv_tab_inactive_press) - .image_color(UI_HIGHLIGHT_0) - .down_from(state.ids.tab_1, 0.0) - .with_tooltip( - self.tooltip_manager, - "Not yet Available", - "", - &item_tooltip, - TEXT_COLOR, - ) - .set(state.ids.tab_2, ui) - .was_clicked() - {} - if Button::image(self.imgs.inv_tab_inactive) - .w_h(28.0, 44.0) - .hover_image(self.imgs.inv_tab_inactive_hover) - .press_image(self.imgs.inv_tab_inactive_press) - .down_from(state.ids.tab_2, 0.0) - .image_color(UI_HIGHLIGHT_0) - .with_tooltip( - self.tooltip_manager, - "Not yet Available", - "", - &item_tooltip, - TEXT_COLOR, - ) - .set(state.ids.tab_3, ui) - .was_clicked() - {} - if Button::image(self.imgs.inv_tab_inactive) - .w_h(28.0, 44.0) - .hover_image(self.imgs.inv_tab_inactive_hover) - .press_image(self.imgs.inv_tab_inactive_press) - .down_from(state.ids.tab_3, 0.0) - .image_color(UI_HIGHLIGHT_0) - .with_tooltip( - self.tooltip_manager, - "Not yet Available", - "", - &item_tooltip, - TEXT_COLOR, - ) - .set(state.ids.tab_4, ui) - .was_clicked() - {} // Close button if Button::image(self.imgs.close_btn) .w_h(24.0, 25.0) diff --git a/voxygen/src/hud/buttons.rs b/voxygen/src/hud/buttons.rs index 874a83f192..fb0ae3bac2 100644 --- a/voxygen/src/hud/buttons.rs +++ b/voxygen/src/hud/buttons.rs @@ -191,8 +191,8 @@ impl<'a> Widget for Buttons<'a> { .set(state.ids.bag_text, ui); } if !self.show_bag { - let space_used = inventory.amount(); - let space_max = inventory.slots().len(); + let space_used = inventory.populated_slots(); + let space_max = inventory.slots().count(); let bag_space = format!("{}/{}", space_used, space_max); let bag_space_percentage = space_used as f32 / space_max as f32; Text::new(&bag_space) diff --git a/voxygen/src/hud/hotbar.rs b/voxygen/src/hud/hotbar.rs index 1fbe84b101..1e29e25559 100644 --- a/voxygen/src/hud/hotbar.rs +++ b/voxygen/src/hud/hotbar.rs @@ -1,6 +1,8 @@ +use crate::hud::slots::EquipSlot; +use common::comp::{slot::InvSlotId, Inventory}; use serde::{Deserialize, Serialize}; -#[derive(Clone, Copy, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum Slot { One = 0, Two = 1, @@ -16,7 +18,7 @@ pub enum Slot { #[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] pub enum SlotContents { - Inventory(usize), + Inventory(InvSlotId), Ability3, } @@ -57,8 +59,8 @@ impl State { pub fn clear_slot(&mut self, slot: Slot) { self.slots[slot as usize] = None; } - pub fn add_inventory_link(&mut self, slot: Slot, inventory_index: usize) { - self.slots[slot as usize] = Some(SlotContents::Inventory(inventory_index)); + pub fn add_inventory_link(&mut self, slot: Slot, inventory_pos: InvSlotId) { + self.slots[slot as usize] = Some(SlotContents::Inventory(inventory_pos)); } // TODO: remove @@ -67,13 +69,12 @@ impl State { #[allow(clippy::unnested_or_patterns)] // TODO: Pending review in #587 pub fn maintain_ability3(&mut self, client: &client::Client) { use specs::WorldExt; - let loadouts = client.state().ecs().read_storage::(); - let loadout = loadouts.get(client.entity()); - let should_be_present = if let Some(loadout) = loadout { - loadout - .active_item - .as_ref() - .map(|i| i.item.kind()) + let inventories = client.state().ecs().read_storage::(); + let inventory = inventories.get(client.entity()); + let should_be_present = if let Some(inventory) = inventory { + inventory + .equipped(EquipSlot::Mainhand) + .map(|i| i.kind()) .filter(|kind| { use common::comp::item::{ tool::{ToolKind, UniqueKind}, diff --git a/voxygen/src/hud/img_ids.rs b/voxygen/src/hud/img_ids.rs index 5a7b391c70..cabe4f0d60 100644 --- a/voxygen/src/hud/img_ids.rs +++ b/voxygen/src/hud/img_ids.rs @@ -26,10 +26,6 @@ image_ids! { flower: "voxygen.element.icons.item_flower", grass: "voxygen.element.icons.item_grass", - // Charwindow - xp_charwindow: "voxygen.element.frames.xp_charwindow", - divider: "voxygen.element.frames.divider_charwindow", - // Items potion_red: "voxygen.voxel.object.potion_red", potion_green: "voxygen.voxel.object.potion_green", @@ -59,6 +55,13 @@ image_ids! { selection_hover: "voxygen.element.frames.selection_hover", selection_press: "voxygen.element.frames.selection_press", + // Prompt Dialog + prompt_top: "voxygen.element.frames.prompt_dialog_top", + prompt_mid: "voxygen.element.frames.prompt_dialog_mid", + prompt_bot: "voxygen.element.frames.prompt_dialog_bot", + key_button: "voxygen.element.buttons.key_button", + key_button_press: "voxygen.element.buttons.key_button_press", + // Social Window social_frame_on: "voxygen.element.misc_bg.social_frame", social_bg_on: "voxygen.element.misc_bg.social_bg", @@ -235,10 +238,18 @@ image_ids! { close_button_press: "voxygen.element.buttons.close_btn_press", // Inventory + collapse_btn: "voxygen.element.buttons.inv_collapse", + collapse_btn_hover: "voxygen.element.buttons.inv_collapse_hover", + collapse_btn_press: "voxygen.element.buttons.inv_collapse_press", + expand_btn: "voxygen.element.buttons.inv_expand", + expand_btn_hover: "voxygen.element.buttons.inv_expand_hover", + expand_btn_press: "voxygen.element.buttons.inv_expand_press", coin_ico: "voxygen.element.icons.coin", inv_bg_armor: "voxygen.element.misc_bg.inv_bg_0", inv_bg_stats: "voxygen.element.misc_bg.inv_bg_1", inv_frame: "voxygen.element.misc_bg.inv_frame", + inv_frame_bag: "voxygen.element.misc_bg.inv_frame_bag", + inv_bg_bag: "voxygen.element.misc_bg.inv_bg_bag", char_art: "voxygen.element.icons.character", inv_slot: "voxygen.element.buttons.inv_slot", inv_slot_grey: "voxygen.element.buttons.inv_slot_grey", @@ -250,12 +261,11 @@ image_ids! { inv_slot_red: "voxygen.element.buttons.inv_slot_red", inv_slot_sel: "voxygen.element.buttons.inv_slot_sel", scrollbar_bg: "voxygen.element.slider.scrollbar", + scrollbar_bg_big: "voxygen.element.slider.scrollbar_1", inv_tab_active: "voxygen.element.buttons.inv_tab_active", inv_tab_inactive: "voxygen.element.buttons.inv_tab_inactive", inv_tab_inactive_hover: "voxygen.element.buttons.inv_tab_inactive", inv_tab_inactive_press: "voxygen.element.buttons.inv_tab_inactive", - inv_slots: "voxygen.element.misc_bg.inv_slots", - inv_runes: "voxygen.element.misc_bg.inv_runes", armor_slot: "voxygen.element.buttons.armor_slot", armor_slot_sel: "voxygen.element.buttons.armor_slot_selected", armor_slot_empty: "voxygen.element.buttons.armor_slot_empty", @@ -273,6 +283,7 @@ image_ids! { lantern_bg: "voxygen.element.icons.lantern", necklace_bg: "voxygen.element.icons.necklace", mainhand_bg: "voxygen.element.icons.mainhand", + bag_bg: "voxygen.element.icons.bag", offhand_bg: "voxygen.element.icons.offhand", willpower_ico: "voxygen.element.icons.willpower", endurance_ico: "voxygen.element.icons.endurance", @@ -289,7 +300,6 @@ image_ids! { banner_top: "voxygen.element.frames.banner_top", // Icons - fire_spell_1: "voxygen.element.icons.fire_spell_0", snake_arrow_0: "voxygen.element.icons.snake", heal_0: "voxygen.element.icons.heal_0", sword_whirlwind: "voxygen.element.icons.sword_whirlwind", diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 3662b0695d..6a293661e1 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -13,6 +13,7 @@ mod minimap; mod overhead; mod overitem; mod popup; +mod prompt_dialog; mod settings_window; mod skillbar; mod slots; @@ -37,6 +38,7 @@ use item_imgs::ItemImgs; use map::Map; use minimap::MiniMap; use popup::Popup; +use prompt_dialog::PromptDialog; use serde::{Deserialize, Serialize}; use settings_window::{SettingsTab, SettingsWindow}; use skillbar::Skillbar; @@ -45,7 +47,7 @@ use spell::Spell; use crate::{ ecs::{comp as vcomp, comp::HpFloaterList}, - hud::img_ids::ImgsRot, + hud::{img_ids::ImgsRot, prompt_dialog::DialogOutcomeEvent}, i18n::{LanguageMetadata, Localization}, render::{Consts, Globals, RenderMode, Renderer}, scene::camera::{self, Camera}, @@ -239,6 +241,7 @@ widget_ids! { character_window, popup, minimap, + prompt_dialog, bag, social, quest, @@ -303,6 +306,7 @@ pub struct HudInfo { pub selected_entity: Option<(specs::Entity, std::time::Instant)>, } +#[derive(Clone)] pub enum Event { ToggleTips(bool), SendMessage(String), @@ -350,8 +354,15 @@ pub enum Event { ToggleDebug(bool), UiScale(ScaleChange), CharacterSelection, - UseSlot(comp::slot::Slot), - SwapSlots(comp::slot::Slot, comp::slot::Slot), + UseSlot { + slot: comp::slot::Slot, + bypass_dialog: bool, + }, + SwapSlots { + slot_a: comp::slot::Slot, + slot_b: comp::slot::Slot, + bypass_dialog: bool, + }, DropSlot(comp::slot::Slot), ChangeHotbarState(Box), Ability3(bool), @@ -439,6 +450,7 @@ pub struct Show { crafting: bool, debug: bool, bag: bool, + bag_inv: bool, social: bool, spell: bool, group: bool, @@ -454,6 +466,7 @@ pub struct Show { stats: bool, free_look: bool, auto_walk: bool, + prompt_dialog: Option, } impl Show { fn bag(&mut self, open: bool) { @@ -604,6 +617,28 @@ impl Show { } } +pub struct PromptDialogSettings { + message: String, + affirmative_event: Event, + negative_event: Option, + outcome_via_keypress: Option, +} + +impl PromptDialogSettings { + pub fn new(message: String, affirmative_event: Event, negative_event: Option) -> Self { + Self { + message, + affirmative_event, + negative_event, + outcome_via_keypress: None, + } + } + + pub fn set_outcome_via_keypress(&mut self, outcome: bool) { + self.outcome_via_keypress = Some(outcome); + } +} + pub struct Hud { ui: Ui, ids: Ids, @@ -695,6 +730,7 @@ impl Hud { intro: true, debug: false, bag: false, + bag_inv: false, esc_menu: false, open_windows: Windows::None, map: false, @@ -712,6 +748,7 @@ impl Hud { stats: false, free_look: false, auto_walk: false, + prompt_dialog: None, }, to_focus: None, //never_show: false, @@ -728,6 +765,10 @@ impl Hud { } } + pub fn set_prompt_dialog(&mut self, prompt_dialog: PromptDialogSettings) { + self.show.prompt_dialog = Some(prompt_dialog); + } + pub fn update_fonts(&mut self, i18n: &Localization) { self.fonts = Fonts::load(&i18n.fonts, &mut self.ui).expect("Impossible to load fonts!"); } @@ -1871,6 +1912,34 @@ impl Hud { None => {}, } + if let Some(prompt_dialog_settings) = &self.show.prompt_dialog { + // Prompt Dialog + match PromptDialog::new( + &self.imgs, + &self.fonts, + &global_state.i18n, + &global_state.settings, + &prompt_dialog_settings, + ) + .set(self.ids.prompt_dialog, ui_widgets) + { + Some(dialog_outcome_event) => { + match dialog_outcome_event { + DialogOutcomeEvent::Affirmative(event) => events.push(event), + DialogOutcomeEvent::Negative(event) => { + if let Some(event) = event { + events.push(event); + }; + }, + }; + + // Close the prompt dialog once an option has been chosen + self.show.prompt_dialog = None; + }, + None => {}, + } + } + // Bag contents if self.show.bag { if let Some(player_stats) = stats.get(client.entity()) { @@ -1889,7 +1958,7 @@ impl Hud { ) .set(self.ids.bag, ui_widgets) { - Some(bag::Event::Stats) => self.show.stats = !self.show.stats, + Some(bag::Event::BagExpand) => self.show.bag_inv = !self.show.bag_inv, Some(bag::Event::Close) => { self.show.stats = false; self.show.bag(false); @@ -1911,28 +1980,26 @@ impl Hud { let entity = client.entity(); let stats = ecs.read_storage::(); let healths = ecs.read_storage::(); - let loadouts = ecs.read_storage::(); + let inventories = ecs.read_storage::(); let energies = ecs.read_storage::(); let character_states = ecs.read_storage::(); let controllers = ecs.read_storage::(); - let inventories = ecs.read_storage::(); let ability_map = ecs.fetch::(); + if let ( Some(stats), Some(health), - Some(loadout), + Some(inventory), Some(energy), Some(_character_state), Some(_controller), - Some(inventory), ) = ( stats.get(entity), healths.get(entity), - loadouts.get(entity), + inventories.get(entity), energies.get(entity), character_states.get(entity), controllers.get(entity).map(|c| &c.inputs), - inventories.get(entity), ) { Skillbar::new( global_state, @@ -1942,12 +2009,11 @@ impl Hud { &self.rot_imgs, &stats, &health, - &loadout, + &inventory, &energy, //&character_state, self.pulse, //&controller, - &inventory, &self.hotbar, tooltip_manager, &mut self.slot_manager, @@ -2381,7 +2447,11 @@ impl Hud { slot::Event::Dragged(a, b) => { // Swap between slots if let (Some(a), Some(b)) = (to_slot(a), to_slot(b)) { - events.push(Event::SwapSlots(a, b)); + events.push(Event::SwapSlots { + slot_a: a, + slot_b: b, + bypass_dialog: false, + }); } else if let (Inventory(i), Hotbar(h)) = (a, b) { self.hotbar.add_inventory_link(h, i.0); events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned()))); @@ -2402,12 +2472,19 @@ impl Hud { slot::Event::Used(from) => { // Item used (selected and then clicked again) if let Some(from) = to_slot(from) { - events.push(Event::UseSlot(from)); + events.push(Event::UseSlot { + slot: from, + bypass_dialog: false, + }); } else if let Hotbar(h) = from { + // Used from hotbar self.hotbar.get(h).map(|s| { match s { hotbar::SlotContents::Inventory(i) => { - events.push(Event::UseSlot(comp::slot::Slot::Inventory(i))); + events.push(Event::UseSlot { + slot: comp::slot::Slot::Inventory(i), + bypass_dialog: false, + }); }, hotbar::SlotContents::Ability3 => {}, /* Event::Ability3(true), * sticks */ @@ -2469,7 +2546,10 @@ impl Hud { hotbar.get(slot).map(|s| match s { hotbar::SlotContents::Inventory(i) => { if just_pressed { - events.push(Event::UseSlot(comp::slot::Slot::Inventory(i))); + events.push(Event::UseSlot { + slot: comp::slot::Slot::Inventory(i), + bypass_dialog: false, + }); } }, hotbar::SlotContents::Ability3 => events.push(Event::Ability3(state)), @@ -2499,6 +2579,22 @@ impl Hud { self.force_ungrab = !self.force_ungrab; true }, + WinEvent::InputUpdate(GameInput::AcceptGroupInvite, true) if !self.typing() => { + if let Some(prompt_dialog) = &mut self.show.prompt_dialog { + prompt_dialog.set_outcome_via_keypress(true); + true + } else { + false + } + }, + WinEvent::InputUpdate(GameInput::DeclineGroupInvite, true) if !self.typing() => { + if let Some(prompt_dialog) = &mut self.show.prompt_dialog { + prompt_dialog.set_outcome_via_keypress(false); + true + } else { + false + } + }, // If not showing the ui don't allow keys that change the ui state but do listen for // hotbar keys diff --git a/voxygen/src/hud/prompt_dialog.rs b/voxygen/src/hud/prompt_dialog.rs new file mode 100644 index 0000000000..3c358f08fd --- /dev/null +++ b/voxygen/src/hud/prompt_dialog.rs @@ -0,0 +1,192 @@ +use super::{img_ids::Imgs, TEXT_COLOR, UI_HIGHLIGHT_0}; +use crate::{ + hud::{Event, PromptDialogSettings}, + i18n::Localization, + settings::Settings, + ui::fonts::Fonts, + window::GameInput, + AssetHandle, +}; +use conrod_core::{ + widget::{self, Button, Image, Text}, + widget_ids, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, +}; + +widget_ids! { + struct Ids { + top, + mid, + bot, + text, + accept_txt, // optional timer + accept_key, //button with label + decline_txt, + decline_key, + prompt_txt, + } +} +#[derive(WidgetCommon)] +pub struct PromptDialog<'a> { + imgs: &'a Imgs, + fonts: &'a Fonts, + #[conrod(common_builder)] + common: widget::CommonBuilder, + localized_strings: &'a AssetHandle, + settings: &'a Settings, + prompt_dialog_settings: &'a PromptDialogSettings, +} + +impl<'a> PromptDialog<'a> { + #[allow(clippy::too_many_arguments)] // TODO: Pending review in #587 + pub fn new( + imgs: &'a Imgs, + fonts: &'a Fonts, + localized_strings: &'a AssetHandle, + settings: &'a Settings, + prompt_dialog_settings: &'a PromptDialogSettings, + ) -> Self { + Self { + imgs, + fonts, + localized_strings, + common: widget::CommonBuilder::default(), + settings, + prompt_dialog_settings, + } + } +} + +pub struct State { + ids: Ids, +} + +pub enum DialogOutcomeEvent { + Affirmative(Event), + Negative(Option), +} + +impl<'a> Widget for PromptDialog<'a> { + type Event = Option; + type State = State; + type Style = (); + + fn init_state(&self, id_gen: widget::id::Generator) -> Self::State { + State { + ids: Ids::new(id_gen), + } + } + + #[allow(clippy::unused_unit)] // TODO: Pending review in #587 + fn style(&self) -> Self::Style { () } + + fn update(self, args: widget::UpdateArgs) -> Self::Event { + let widget::UpdateArgs { state, ui, .. } = args; + let _localized_strings = self.localized_strings; + let mut event: Option = None; + + let accept_key = self + .settings + .controls + .get_binding(GameInput::AcceptGroupInvite) + .map_or_else(|| "".into(), |key| key.to_string()); + let decline_key = self + .settings + .controls + .get_binding(GameInput::DeclineGroupInvite) + .map_or_else(|| "".into(), |key| key.to_string()); + + // Window + Image::new(self.imgs.prompt_top) + .w_h(276.0, 24.0) + .mid_top_with_margin_on(ui.window, 100.0) + .color(Some(UI_HIGHLIGHT_0)) + .set(state.ids.top, ui); + if !self.prompt_dialog_settings.message.is_empty() { + Image::new(self.imgs.prompt_mid) + .w(276.0) + .h_of(state.ids.prompt_txt) // height relative to content, max height 150 + .down_from(state.ids.top, 0.0) + .color(Some(UI_HIGHLIGHT_0)) + .scroll_kids_vertically() + .set(state.ids.mid, ui); + } + Image::new(self.imgs.prompt_bot) + .w_h(276.0, 35.0) + .down_from(state.ids.mid, 0.0) + .color(Some(UI_HIGHLIGHT_0)) + .set(state.ids.bot, ui); + + // Accept/Decline Buttons + if Button::image(self.imgs.key_button) + .w_h(20.0, 20.0) + .hover_image(self.imgs.close_btn_hover) + .press_image(self.imgs.close_btn_press) + .label(&accept_key) + .image_color(UI_HIGHLIGHT_0) + .label_color(TEXT_COLOR) + .label_font_size(self.fonts.cyri.scale(16)) + .label_font_id(self.fonts.cyri.conrod_id) + .label_y(conrod_core::position::Relative::Scalar(2.5)) + .label_x(conrod_core::position::Relative::Scalar(0.5)) + .bottom_left_with_margins_on(state.ids.bot, 4.0, 6.0) + .set(state.ids.accept_key, ui) + .was_clicked() + || self + .prompt_dialog_settings + .outcome_via_keypress + .map_or(false, |outcome| outcome) + { + // Primary use should be through pressing the key instead of clicking this + event = Some(DialogOutcomeEvent::Affirmative( + self.prompt_dialog_settings.affirmative_event.clone(), + )); + } + Text::new("Accept") + .bottom_right_with_margins_on(state.ids.accept_key, 5.0, -65.0) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(18)) + .color(TEXT_COLOR) + .set(state.ids.accept_txt, ui); + + if Button::image(self.imgs.key_button) + .w_h(20.0, 20.0) + .hover_image(self.imgs.close_btn_hover) + .press_image(self.imgs.close_btn_press) + .label(&decline_key) + .image_color(UI_HIGHLIGHT_0) + .label_color(TEXT_COLOR) + .label_font_size(self.fonts.cyri.scale(16)) + .label_font_id(self.fonts.cyri.conrod_id) + .label_y(conrod_core::position::Relative::Scalar(2.5)) + .label_x(conrod_core::position::Relative::Scalar(0.5)) + .bottom_right_with_margins_on(state.ids.bot, 4.0, 6.0) + .set(state.ids.decline_key, ui) + .was_clicked() + || self + .prompt_dialog_settings + .outcome_via_keypress + .map_or(false, |outcome| !outcome) + { + event = Some(DialogOutcomeEvent::Negative( + self.prompt_dialog_settings.negative_event.as_ref().cloned(), + )); + } + Text::new("Decline") + .bottom_left_with_margins_on(state.ids.decline_key, 4.0, -65.0) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(18)) + .color(TEXT_COLOR) + .set(state.ids.decline_txt, ui); + + // Prompt Description + Text::new(&self.prompt_dialog_settings.message) + .mid_top_with_margin_on(state.ids.mid,0.0) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(18)) + .color(TEXT_COLOR) + .w(260.0) // Text stays within frame + .set(state.ids.prompt_txt, ui); + + event + } +} diff --git a/voxygen/src/hud/settings_window.rs b/voxygen/src/hud/settings_window.rs index 9e34396478..ecb267623c 100644 --- a/voxygen/src/hud/settings_window.rs +++ b/voxygen/src/hud/settings_window.rs @@ -317,6 +317,7 @@ pub enum Event { ChangeStopAutoWalkOnInput(bool), } +#[derive(Clone)] pub enum ScaleChange { ToAbsolute, ToRelative, diff --git a/voxygen/src/hud/skillbar.rs b/voxygen/src/hud/skillbar.rs index 60a5697764..fa0498df28 100644 --- a/voxygen/src/hud/skillbar.rs +++ b/voxygen/src/hud/skillbar.rs @@ -16,11 +16,12 @@ use crate::{ GlobalState, }; use common::comp::{ + inventory::slot::EquipSlot, item::{ tool::{AbilityMap, Tool, ToolKind}, Hands, ItemKind, }, - Energy, Health, Inventory, Loadout, Stats, + Energy, Health, Inventory, Stats, }; use conrod_core::{ color, @@ -125,11 +126,10 @@ pub struct Skillbar<'a> { rot_imgs: &'a ImgsRot, stats: &'a Stats, health: &'a Health, - loadout: &'a Loadout, + inventory: &'a Inventory, energy: &'a Energy, // character_state: &'a CharacterState, // controller: &'a ControllerInputs, - inventory: &'a Inventory, hotbar: &'a hotbar::State, tooltip_manager: &'a mut TooltipManager, slot_manager: &'a mut slots::SlotManager, @@ -151,12 +151,11 @@ impl<'a> Skillbar<'a> { rot_imgs: &'a ImgsRot, stats: &'a Stats, health: &'a Health, - loadout: &'a Loadout, + inventory: &'a Inventory, energy: &'a Energy, // character_state: &'a CharacterState, pulse: f32, // controller: &'a ControllerInputs, - inventory: &'a Inventory, hotbar: &'a hotbar::State, tooltip_manager: &'a mut TooltipManager, slot_manager: &'a mut slots::SlotManager, @@ -172,13 +171,12 @@ impl<'a> Skillbar<'a> { rot_imgs, stats, health, - loadout, + inventory, energy, common: widget::CommonBuilder::default(), // character_state, pulse, // controller, - inventory, hotbar, tooltip_manager, slot_manager, @@ -473,13 +471,7 @@ impl<'a> Widget for Skillbar<'a> { .set(state.ids.stamina_txt, ui); } // Slots - let content_source = ( - self.hotbar, - self.inventory, - self.loadout, - self.energy, - self.ability_map, - ); // TODO: avoid this + let content_source = (self.hotbar, self.inventory, self.energy, self.ability_map); // TODO: avoid this let image_source = (self.item_imgs, self.imgs); let mut slot_maker = SlotMaker { // TODO: is a separate image needed for the frame? @@ -529,10 +521,9 @@ impl<'a> Widget for Skillbar<'a> { .get(i) .map(|item| (item.name(), item.description())), hotbar::SlotContents::Ability3 => content_source - .2 - .active_item - .as_ref() - .map(|i| i.item.kind()) + .1 + .equipped(EquipSlot::Mainhand) + .map(|i| i.kind()) .and_then(|kind| match kind { ItemKind::Tool(Tool { kind, .. }) => match kind { ToolKind::Hammer => Some(( @@ -630,7 +621,11 @@ impl<'a> Widget for Skillbar<'a> { .right_from(state.ids.slot5, 0.0) .set(state.ids.m1_slot_bg, ui); Button::image( - match self.loadout.active_item.as_ref().map(|i| i.item.kind()) { + match self + .inventory + .equipped(EquipSlot::Mainhand) + .map(|i| i.kind()) + { Some(ItemKind::Tool(Tool { kind, .. })) => match kind { ToolKind::Sword => self.imgs.twohsword_m1, ToolKind::Dagger => self.imgs.onehdagger_m1, @@ -655,19 +650,19 @@ impl<'a> Widget for Skillbar<'a> { .right_from(state.ids.m1_slot_bg, 0.0) .set(state.ids.m2_slot, ui); - let active_tool = match self.loadout.active_item.as_ref().map(|i| i.item.kind()) { - Some(ItemKind::Tool(tool)) => Some(tool), - _ => None, - }; + fn get_tool(inventory: &Inventory, equip_slot: EquipSlot) -> Option<&Tool> { + match inventory.equipped(equip_slot).map(|i| i.kind()) { + Some(ItemKind::Tool(tool)) => Some(tool), + _ => None, + } + } - let second_tool = match self.loadout.second_item.as_ref().map(|i| i.item.kind()) { - Some(ItemKind::Tool(tool)) => Some(tool), - _ => None, - }; + let active_tool = get_tool(self.inventory, EquipSlot::Mainhand); + let second_tool = get_tool(self.inventory, EquipSlot::Offhand); let tool = match ( - active_tool.map(|t| t.kind.hands()), - second_tool.map(|t| t.kind.hands()), + active_tool.map(|x| x.kind.hands()), + second_tool.map(|x| x.kind.hands()), ) { (Some(Hands::TwoHand), _) => active_tool, (_, Some(Hands::OneHand)) => second_tool, diff --git a/voxygen/src/hud/slots.rs b/voxygen/src/hud/slots.rs index f14f633921..5acfd790c6 100644 --- a/voxygen/src/hud/slots.rs +++ b/voxygen/src/hud/slots.rs @@ -9,13 +9,14 @@ use common::comp::{ tool::{AbilityMap, ToolKind}, ItemKind, }, - Energy, Inventory, Loadout, + slot::InvSlotId, + Energy, Inventory, }; use conrod_core::{image, Color}; pub use common::comp::slot::{ArmorSlot, EquipSlot}; -#[derive(Clone, Copy, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum SlotKind { Inventory(InventorySlot), Equip(EquipSlot), @@ -25,8 +26,8 @@ pub enum SlotKind { pub type SlotManager = slot::SlotManager; -#[derive(Clone, Copy, PartialEq)] -pub struct InventorySlot(pub usize); +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct InventorySlot(pub InvSlotId); impl SlotKey for InventorySlot { type ImageKey = ItemKey; @@ -47,32 +48,15 @@ impl SlotKey for InventorySlot { } } -impl SlotKey for EquipSlot { +impl SlotKey for EquipSlot { type ImageKey = ItemKey; - fn image_key(&self, source: &Loadout) -> Option<(Self::ImageKey, Option)> { - let item = match self { - EquipSlot::Armor(ArmorSlot::Shoulders) => source.shoulder.as_ref(), - EquipSlot::Armor(ArmorSlot::Chest) => source.chest.as_ref(), - EquipSlot::Armor(ArmorSlot::Belt) => source.belt.as_ref(), - EquipSlot::Armor(ArmorSlot::Hands) => source.hand.as_ref(), - EquipSlot::Armor(ArmorSlot::Legs) => source.pants.as_ref(), - EquipSlot::Armor(ArmorSlot::Feet) => source.foot.as_ref(), - EquipSlot::Armor(ArmorSlot::Back) => source.back.as_ref(), - EquipSlot::Armor(ArmorSlot::Ring) => source.ring.as_ref(), - EquipSlot::Armor(ArmorSlot::Neck) => source.neck.as_ref(), - EquipSlot::Armor(ArmorSlot::Head) => source.head.as_ref(), - EquipSlot::Armor(ArmorSlot::Tabard) => source.tabard.as_ref(), - EquipSlot::Mainhand => source.active_item.as_ref().map(|i| &i.item), - EquipSlot::Offhand => source.second_item.as_ref().map(|i| &i.item), - EquipSlot::Lantern => source.lantern.as_ref(), - EquipSlot::Glider => source.glider.as_ref(), - }; - + fn image_key(&self, source: &Inventory) -> Option<(Self::ImageKey, Option)> { + let item = source.equipped(*self); item.map(|i| (i.into(), None)) } - fn amount(&self, _: &Loadout) -> Option { None } + fn amount(&self, _: &Inventory) -> Option { None } fn image_id(key: &Self::ImageKey, source: &ItemImgs) -> image::Id { source.img_id_or_not_found_img(key.clone()) @@ -90,13 +74,7 @@ pub enum HotbarImage { BowJumpBurst, } -type HotbarSource<'a> = ( - &'a hotbar::State, - &'a Inventory, - &'a Loadout, - &'a Energy, - &'a AbilityMap, -); +type HotbarSource<'a> = (&'a hotbar::State, &'a Inventory, &'a Energy, &'a AbilityMap); type HotbarImageSource<'a> = (&'a ItemImgs, &'a img_ids::Imgs); impl<'a> SlotKey, HotbarImageSource<'a>> for HotbarSlot { @@ -104,7 +82,7 @@ impl<'a> SlotKey, HotbarImageSource<'a>> for HotbarSlot { fn image_key( &self, - (hotbar, inventory, loadout, energy, ability_map): &HotbarSource<'a>, + (hotbar, inventory, energy, ability_map): &HotbarSource<'a>, ) -> Option<(Self::ImageKey, Option)> { hotbar.get(*self).and_then(|contents| match contents { hotbar::SlotContents::Inventory(idx) => inventory @@ -112,7 +90,7 @@ impl<'a> SlotKey, HotbarImageSource<'a>> for HotbarSlot { .map(|item| HotbarImage::Item(item.into())) .map(|i| (i, None)), hotbar::SlotContents::Ability3 => { - let tool = match loadout.active_item.as_ref().map(|i| i.item.kind()) { + let tool = match inventory.equipped(EquipSlot::Mainhand).map(|i| i.kind()) { Some(ItemKind::Tool(tool)) => Some(tool), _ => None, }; @@ -146,7 +124,7 @@ impl<'a> SlotKey, HotbarImageSource<'a>> for HotbarSlot { }) } - fn amount(&self, (hotbar, inventory, _, _, _): &HotbarSource<'a>) -> Option { + fn amount(&self, (hotbar, inventory, _, _): &HotbarSource<'a>) -> Option { hotbar .get(*self) .and_then(|content| match content { diff --git a/voxygen/src/hud/util.rs b/voxygen/src/hud/util.rs index 2464663704..c427b70f69 100644 --- a/voxygen/src/hud/util.rs +++ b/voxygen/src/hud/util.rs @@ -3,7 +3,7 @@ use common::comp::item::{ tool::{Tool, ToolKind}, ItemDesc, ItemKind, }; -use std::borrow::Cow; +use std::{borrow::Cow, fmt::Write}; pub fn loadout_slot_text<'a>( item: Option<&'a impl ItemDesc>, @@ -20,7 +20,9 @@ pub fn loadout_slot_text<'a>( pub fn item_text<'a>(item: &'a impl ItemDesc) -> (&'_ str, Cow<'a, str>) { let desc: Cow = match item.kind() { - ItemKind::Armor(armor) => Cow::Owned(armor_desc(&armor, item.description())), + ItemKind::Armor(armor) => { + Cow::Owned(armor_desc(armor, item.description(), item.num_slots())) + }, ItemKind::Tool(tool) => Cow::Owned(tool_desc(&tool, item.description())), ItemKind::Glider(_glider) => Cow::Owned(glider_desc(item.description())), ItemKind::Consumable { .. } => Cow::Owned(consumable_desc(item.description())), @@ -51,7 +53,7 @@ fn ingredient_desc(desc: &str) -> String { format!("Crafting Ingredient\n\n{}", fn lantern_desc(desc: &str) -> String { format!("Lantern\n\n{}\n\n", desc) } -fn armor_desc(armor: &Armor, desc: &str) -> String { +fn armor_desc(armor: &Armor, desc: &str, slots: u16) -> String { // TODO: localization let kind = match armor.kind { ArmorKind::Shoulder(_) => "Shoulders", @@ -65,20 +67,26 @@ fn armor_desc(armor: &Armor, desc: &str) -> String { ArmorKind::Neck(_) => "Neck", ArmorKind::Head(_) => "Head", ArmorKind::Tabard(_) => "Tabard", + ArmorKind::Bag(_) => "Bag", }; - let armor = match armor.get_protection() { + + let protection = match armor.get_protection() { Protection::Normal(a) => a.to_string(), Protection::Invincible => "Inf".to_string(), }; + let mut description = format!("{}\n\nArmor: {}", kind, protection); + if !desc.is_empty() { - format!( - "{}\n\nArmor: {}\n\n{}\n\n", - kind, armor, desc - ) - } else { - format!("{}\n\nArmor: {}\n\n", kind, armor) + write!(&mut description, "\n\n{}", desc).unwrap(); } + + if slots > 0 { + write!(&mut description, "\n\nSlots: {}", slots).unwrap(); + } + + write!(&mut description, "\n\n").unwrap(); + description } fn tool_desc(tool: &Tool, desc: &str) -> String { diff --git a/voxygen/src/logging.rs b/voxygen/src/logging.rs index e0b4a962df..e7ee26dac7 100644 --- a/voxygen/src/logging.rs +++ b/voxygen/src/logging.rs @@ -45,6 +45,11 @@ pub fn init(settings: &Settings) -> Vec { .add_directive("uvth=warn".parse().unwrap()) .add_directive("tiny_http=warn".parse().unwrap()) .add_directive("mio::sys::windows=debug".parse().unwrap()) + .add_directive( + "veloren_common::comp::inventory::slot=info" + .parse() + .unwrap(), + ) .add_directive( "veloren_server::persistence::character=info" .parse() diff --git a/voxygen/src/menu/char_selection/mod.rs b/voxygen/src/menu/char_selection/mod.rs index cc836594a5..7219fc1d87 100644 --- a/voxygen/src/menu/char_selection/mod.rs +++ b/voxygen/src/menu/char_selection/mod.rs @@ -38,19 +38,22 @@ impl CharSelectionState { } } - fn get_humanoid_body_loadout<'a>( + fn get_humanoid_body_inventory<'a>( char_selection_ui: &'a CharSelectionUi, client: &'a Client, - ) -> (Option, Option<&'a comp::Loadout>) { + ) -> ( + Option, + Option<&'a comp::inventory::Inventory>, + ) { char_selection_ui - .display_body_loadout(&client.character_list().characters) - .map(|(body, loadout)| { + .display_body_inventory(&client.character_list().characters) + .map(|(body, inventory)| { ( match body { comp::Body::Humanoid(body) => Some(body), _ => None, }, - Some(loadout), + Some(inventory), ) }) .unwrap_or_default() @@ -137,7 +140,7 @@ impl PlayState for CharSelectionState { { let client = self.client.borrow(); let (humanoid_body, loadout) = - Self::get_humanoid_body_loadout(&self.char_selection_ui, &client); + Self::get_humanoid_body_inventory(&self.char_selection_ui, &client); // Maintain the scene. let scene_data = scene::SceneData { @@ -222,7 +225,7 @@ impl PlayState for CharSelectionState { fn render(&mut self, renderer: &mut Renderer, _: &Settings) { let client = self.client.borrow(); let (humanoid_body, loadout) = - Self::get_humanoid_body_loadout(&self.char_selection_ui, &client); + Self::get_humanoid_body_inventory(&self.char_selection_ui, &client); // Render the scene. self.scene diff --git a/voxygen/src/menu/char_selection/ui/mod.rs b/voxygen/src/menu/char_selection/ui/mod.rs index f94d071af1..0345961721 100644 --- a/voxygen/src/menu/char_selection/ui/mod.rs +++ b/voxygen/src/menu/char_selection/ui/mod.rs @@ -24,7 +24,7 @@ use client::Client; use common::{ assets::AssetHandle, character::{CharacterId, CharacterItem, MAX_CHARACTERS_PER_PLAYER}, - comp::{self, humanoid, item::tool::AbilityMap}, + comp::{self, humanoid, inventory::slot::EquipSlot, Inventory, Item}, LoadoutBuilder, }; //ImageFrame, Tooltip, @@ -148,7 +148,7 @@ enum Mode { Create { name: String, // TODO: default to username body: humanoid::Body, - loadout: Box, + inventory: Box, tool: &'static str, body_type_buttons: [button::State; 2], @@ -177,24 +177,21 @@ impl Mode { } } - pub fn create(name: String, map: &AbilityMap) -> Self { + pub fn create(name: String) -> Self { let tool = STARTER_SWORD; let loadout = LoadoutBuilder::new() .defaults() - .active_item(Some(LoadoutBuilder::default_item_config_from_str( - tool, map, - ))) + .active_item(Some(Item::new_from_asset_expect(tool))) .build(); - let loadout = Box::new(loadout); + let inventory = Box::new(Inventory::new_with_loadout(loadout)); Self::Create { name, body: humanoid::Body::random(), - loadout, + inventory, tool, - body_type_buttons: Default::default(), species_buttons: Default::default(), tool_buttons: Default::default(), @@ -700,7 +697,7 @@ impl Controls { Mode::Create { name, body, - loadout: _, + inventory: _, tool, ref mut scroll, ref mut body_type_buttons, @@ -1203,13 +1200,7 @@ impl Controls { .into() } - fn update( - &mut self, - message: Message, - events: &mut Vec, - characters: &[CharacterItem], - map: &AbilityMap, - ) { + fn update(&mut self, message: Message, events: &mut Vec, characters: &[CharacterItem]) { match message { Message::Back => { if matches!(&self.mode, Mode::Create { .. }) { @@ -1237,7 +1228,7 @@ impl Controls { }, Message::NewCharacter => { if matches!(&self.mode, Mode::Select { .. }) { - self.mode = Mode::create(String::new(), map); + self.mode = Mode::create(String::new()); } }, Message::CreateCharacter => { @@ -1271,10 +1262,15 @@ impl Controls { } }, Message::Tool(value) => { - if let Mode::Create { tool, loadout, .. } = &mut self.mode { + if let Mode::Create { + tool, inventory, .. + } = &mut self.mode + { *tool = value; - loadout.active_item = - Some(LoadoutBuilder::default_item_config_from_str(*tool, map)); + inventory.replace_loadout_item( + EquipSlot::Mainhand, + Some(Item::new_from_asset_expect(*tool)), + ); } }, Message::RandomizeCharacter => { @@ -1366,16 +1362,18 @@ impl Controls { } /// Get the character to display - pub fn display_body_loadout<'a>( + pub fn display_body_inventory<'a>( &'a self, characters: &'a [CharacterItem], - ) -> Option<(comp::Body, &'a comp::Loadout)> { + ) -> Option<(comp::Body, &'a comp::inventory::Inventory)> { match &self.mode { Mode::Select { .. } => self .selected .and_then(|id| characters.iter().find(|i| i.character.id == Some(id))) - .map(|i| (i.body, &i.loadout)), - Mode::Create { loadout, body, .. } => Some((comp::Body::Humanoid(*body), loadout)), + .map(|i| (i.body, &i.inventory)), + Mode::Create { + inventory, body, .. + } => Some((comp::Body::Humanoid(*body), inventory)), } } } @@ -1424,11 +1422,11 @@ impl CharSelectionUi { } } - pub fn display_body_loadout<'a>( + pub fn display_body_inventory<'a>( &'a self, characters: &'a [CharacterItem], - ) -> Option<(comp::Body, &'a comp::Loadout)> { - self.controls.display_body_loadout(characters) + ) -> Option<(comp::Body, &'a comp::inventory::Inventory)> { + self.controls.display_body_inventory(characters) } pub fn handle_event(&mut self, event: window::Event) -> bool { @@ -1497,12 +1495,8 @@ impl CharSelectionUi { } messages.into_iter().for_each(|message| { - self.controls.update( - message, - &mut events, - &client.character_list().characters, - &*client.state().ability_map(), - ) + self.controls + .update(message, &mut events, &client.character_list().characters) }); events diff --git a/voxygen/src/profile.rs b/voxygen/src/profile.rs index e1285031f9..81ea9b96af 100644 --- a/voxygen/src/profile.rs +++ b/voxygen/src/profile.rs @@ -1,5 +1,5 @@ use crate::{hud, settings}; -use common::character::CharacterId; +use common::{character::CharacterId, comp::slot::InvSlotId}; use hashbrown::HashMap; use serde::{Deserialize, Serialize}; use std::{fs, io::Write, path::PathBuf}; @@ -13,23 +13,25 @@ pub struct CharacterProfile { pub hotbar_slots: [Option; 10], } -const DEFAULT_SLOTS: [Option; 10] = [ - None, - None, - None, - None, - None, - Some(hud::HotbarSlotContents::Inventory(0)), - Some(hud::HotbarSlotContents::Inventory(1)), - None, - None, - None, -]; +const fn default_slots() -> [Option; 10] { + [ + None, + None, + None, + None, + None, + Some(hud::HotbarSlotContents::Inventory(InvSlotId::new(0, 0))), + Some(hud::HotbarSlotContents::Inventory(InvSlotId::new(0, 1))), + None, + None, + None, + ] +} impl Default for CharacterProfile { fn default() -> Self { CharacterProfile { - hotbar_slots: DEFAULT_SLOTS, + hotbar_slots: default_slots(), } } } @@ -127,7 +129,7 @@ impl Profile { .get(server) .and_then(|s| s.characters.get(&character_id)) .map(|c| c.hotbar_slots) - .unwrap_or(DEFAULT_SLOTS) + .unwrap_or_else(default_slots) } /// Set the hotbar_slots for the requested character_id. @@ -222,6 +224,7 @@ impl Profile { #[cfg(test)] mod tests { use super::*; + use common::comp::inventory::slot::InvSlotId; #[test] fn test_get_slots_with_empty_profile() { @@ -233,8 +236,8 @@ mod tests { None, None, None, - Some(hud::HotbarSlotContents::Inventory(0)), - Some(hud::HotbarSlotContents::Inventory(1)), + Some(hud::HotbarSlotContents::Inventory(InvSlotId::new(0, 0))), + Some(hud::HotbarSlotContents::Inventory(InvSlotId::new(0, 1))), None, None, None, @@ -250,8 +253,8 @@ mod tests { None, None, None, - Some(hud::HotbarSlotContents::Inventory(0)), - Some(hud::HotbarSlotContents::Inventory(1)), + Some(hud::HotbarSlotContents::Inventory(InvSlotId::new(0, 0))), + Some(hud::HotbarSlotContents::Inventory(InvSlotId::new(0, 1))), None, None, None, diff --git a/voxygen/src/scene/figure/cache.rs b/voxygen/src/scene/figure/cache.rs index 9b1cd2fd08..38c90a4eaf 100644 --- a/voxygen/src/scene/figure/cache.rs +++ b/voxygen/src/scene/figure/cache.rs @@ -10,11 +10,15 @@ use anim::Skeleton; use common::{ assets::AssetHandle, comp::{ + inventory::{ + slot::{ArmorSlot, EquipSlot}, + Inventory, + }, item::{ armor::{Armor, ArmorKind}, ItemKind, }, - CharacterState, Loadout, + CharacterState, }, figure::Segment, vol::BaseVol, @@ -106,7 +110,7 @@ pub(super) struct CharacterCacheKey { } impl CharacterCacheKey { - fn from(cs: Option<&CharacterState>, camera_mode: CameraMode, loadout: &Loadout) -> Self { + fn from(cs: Option<&CharacterState>, camera_mode: CameraMode, inventory: &Inventory) -> Self { let is_first_person = match camera_mode { CameraMode::FirstPerson => true, CameraMode::ThirdPerson | CameraMode::Freefly => false, @@ -133,7 +137,9 @@ impl CharacterCacheKey { shoulder: if let Some(ItemKind::Armor(Armor { kind: ArmorKind::Shoulder(armor), .. - })) = loadout.shoulder.as_ref().map(|i| i.kind()) + })) = inventory + .equipped(EquipSlot::Armor(ArmorSlot::Shoulders)) + .map(|i| i.kind()) { Some(armor.clone()) } else { @@ -142,7 +148,9 @@ impl CharacterCacheKey { chest: if let Some(ItemKind::Armor(Armor { kind: ArmorKind::Chest(armor), .. - })) = loadout.chest.as_ref().map(|i| i.kind()) + })) = inventory + .equipped(EquipSlot::Armor(ArmorSlot::Chest)) + .map(|i| i.kind()) { Some(armor.clone()) } else { @@ -151,7 +159,9 @@ impl CharacterCacheKey { belt: if let Some(ItemKind::Armor(Armor { kind: ArmorKind::Belt(armor), .. - })) = loadout.belt.as_ref().map(|i| i.kind()) + })) = inventory + .equipped(EquipSlot::Armor(ArmorSlot::Belt)) + .map(|i| i.kind()) { Some(armor.clone()) } else { @@ -160,7 +170,9 @@ impl CharacterCacheKey { back: if let Some(ItemKind::Armor(Armor { kind: ArmorKind::Back(armor), .. - })) = loadout.back.as_ref().map(|i| i.kind()) + })) = inventory + .equipped(EquipSlot::Armor(ArmorSlot::Back)) + .map(|i| i.kind()) { Some(armor.clone()) } else { @@ -169,7 +181,9 @@ impl CharacterCacheKey { pants: if let Some(ItemKind::Armor(Armor { kind: ArmorKind::Pants(armor), .. - })) = loadout.pants.as_ref().map(|i| i.kind()) + })) = inventory + .equipped(EquipSlot::Armor(ArmorSlot::Legs)) + .map(|i| i.kind()) { Some(armor.clone()) } else { @@ -179,27 +193,25 @@ impl CharacterCacheKey { }, tool: if are_tools_visible { Some(CharacterToolKey { - active: loadout - .active_item - .as_ref() - .map(|i| i.item.item_definition_id().to_owned()), - second: loadout - .second_item - .as_ref() - .map(|i| i.item.item_definition_id().to_owned()), + active: inventory + .equipped(EquipSlot::Mainhand) + .map(|i| i.item_definition_id().to_owned()), + second: inventory + .equipped(EquipSlot::Offhand) + .map(|i| i.item_definition_id().to_owned()), }) } else { None }, lantern: if let Some(ItemKind::Lantern(lantern)) = - loadout.lantern.as_ref().map(|i| i.kind()) + inventory.equipped(EquipSlot::Lantern).map(|i| i.kind()) { Some(lantern.kind.clone()) } else { None }, glider: if let Some(ItemKind::Glider(glider)) = - loadout.glider.as_ref().map(|i| i.kind()) + inventory.equipped(EquipSlot::Glider).map(|i| i.kind()) { Some(glider.kind.clone()) } else { @@ -208,7 +220,9 @@ impl CharacterCacheKey { hand: if let Some(ItemKind::Armor(Armor { kind: ArmorKind::Hand(armor), .. - })) = loadout.hand.as_ref().map(|i| i.kind()) + })) = inventory + .equipped(EquipSlot::Armor(ArmorSlot::Hands)) + .map(|i| i.kind()) { Some(armor.clone()) } else { @@ -217,7 +231,9 @@ impl CharacterCacheKey { foot: if let Some(ItemKind::Armor(Armor { kind: ArmorKind::Foot(armor), .. - })) = loadout.foot.as_ref().map(|i| i.kind()) + })) = inventory + .equipped(EquipSlot::Armor(ArmorSlot::Feet)) + .map(|i| i.kind()) { Some(armor.clone()) } else { @@ -261,7 +277,7 @@ where // TODO: If we ever convert to using an atlas here, use this. _col_lights: &super::FigureColLights, body: Skel::Body, - loadout: Option<&Loadout>, + inventory: Option<&Inventory>, // TODO: Consider updating the tick by putting it in a Cell. _tick: u64, camera_mode: CameraMode, @@ -270,11 +286,11 @@ where // TODO: Use raw entries to avoid lots of allocation (among other things). let key = FigureKey { body, - extra: loadout.map(|loadout| { + extra: inventory.map(|inventory| { Arc::new(CharacterCacheKey::from( character_state, camera_mode, - loadout, + inventory, )) }), }; @@ -291,7 +307,7 @@ where renderer: &mut Renderer, col_lights: &mut super::FigureColLights, body: Skel::Body, - loadout: Option<&Loadout>, + inventory: Option<&Inventory>, tick: u64, camera_mode: CameraMode, character_state: Option<&CharacterState>, @@ -305,11 +321,11 @@ where let skeleton_attr = (&body).into(); let key = FigureKey { body, - extra: loadout.map(|loadout| { + extra: inventory.map(|inventory| { Arc::new(CharacterCacheKey::from( character_state, camera_mode, - loadout, + inventory, )) }), }; diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index d25306bdf6..4edd0c761e 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -27,8 +27,9 @@ use anim::{ }; use common::{ comp::{ + inventory::slot::EquipSlot, item::{ItemKind, ToolKind}, - Body, CharacterState, Health, Item, Last, LightAnimation, LightEmitter, Loadout, Ori, + Body, CharacterState, Health, Inventory, Item, Last, LightAnimation, LightEmitter, Ori, PhysicsState, Pos, Scale, Vel, }, resources::DeltaTime, @@ -550,7 +551,7 @@ impl FigureMgr { last_character, physics, health, - loadout, + inventory, item, ), ) in ( @@ -564,7 +565,7 @@ impl FigureMgr { ecs.read_storage::>().maybe(), &ecs.read_storage::(), ecs.read_storage::().maybe(), - ecs.read_storage::().maybe(), + ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), ) .join() @@ -685,18 +686,18 @@ impl FigureMgr { let mut state_animation_rate = 1.0; - let active_item_kind = loadout - .and_then(|l| l.active_item.as_ref()) - .map(|i| i.item.kind()); + let active_item_kind = inventory + .and_then(|i| i.equipped(EquipSlot::Mainhand)) + .map(|i| i.kind()); let active_tool_kind = if let Some(ItemKind::Tool(tool)) = active_item_kind { Some(tool.kind) } else { None }; - let second_item_kind = loadout - .and_then(|l| l.second_item.as_ref()) - .map(|i| i.item.kind()); + let second_item_kind = inventory + .and_then(|i| i.equipped(EquipSlot::Offhand)) + .map(|i| i.kind()); let second_tool_kind = if let Some(ItemKind::Tool(tool)) = second_item_kind { Some(tool.kind) @@ -710,7 +711,7 @@ impl FigureMgr { renderer, &mut self.col_lights, *body, - loadout, + inventory, tick, player_camera_mode, player_character_state, @@ -1338,7 +1339,7 @@ impl FigureMgr { renderer, &mut self.col_lights, *body, - loadout, + inventory, tick, player_camera_mode, player_character_state, @@ -1488,7 +1489,7 @@ impl FigureMgr { renderer, &mut self.col_lights, *body, - loadout, + inventory, tick, player_camera_mode, player_character_state, @@ -1765,7 +1766,7 @@ impl FigureMgr { renderer, &mut self.col_lights, *body, - loadout, + inventory, tick, player_camera_mode, player_character_state, @@ -2059,7 +2060,7 @@ impl FigureMgr { renderer, &mut self.col_lights, *body, - loadout, + inventory, tick, player_camera_mode, player_character_state, @@ -2165,7 +2166,7 @@ impl FigureMgr { renderer, &mut self.col_lights, *body, - loadout, + inventory, tick, player_camera_mode, player_character_state, @@ -2242,7 +2243,7 @@ impl FigureMgr { renderer, &mut self.col_lights, *body, - loadout, + inventory, tick, player_camera_mode, player_character_state, @@ -2325,7 +2326,7 @@ impl FigureMgr { renderer, &mut self.col_lights, *body, - loadout, + inventory, tick, player_camera_mode, player_character_state, @@ -2463,7 +2464,7 @@ impl FigureMgr { renderer, &mut self.col_lights, *body, - loadout, + inventory, tick, player_camera_mode, player_character_state, @@ -2550,7 +2551,7 @@ impl FigureMgr { renderer, &mut self.col_lights, *body, - loadout, + inventory, tick, player_camera_mode, player_character_state, @@ -2627,7 +2628,7 @@ impl FigureMgr { renderer, &mut self.col_lights, *body, - loadout, + inventory, tick, player_camera_mode, player_character_state, @@ -3034,7 +3035,7 @@ impl FigureMgr { renderer, &mut self.col_lights, *body, - loadout, + inventory, tick, player_camera_mode, player_character_state, @@ -3139,7 +3140,7 @@ impl FigureMgr { renderer, &mut self.col_lights, *body, - loadout, + inventory, tick, player_camera_mode, player_character_state, @@ -3200,20 +3201,20 @@ impl FigureMgr { ecs.read_storage::().maybe(), &ecs.read_storage::(), ecs.read_storage::().maybe(), - ecs.read_storage::().maybe(), + ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), ) .join() // Don't render dead entities .filter(|(_, _, _, _, health, _, _)| health.map_or(true, |h| !h.is_dead)) - .for_each(|(entity, pos, _, body, _, loadout, _)| { + .for_each(|(entity, pos, _, body, _, inventory, _)| { if let Some((locals, bone_consts, model, _)) = self.get_model_for_render( tick, camera, None, entity, body, - loadout, + inventory, false, pos.0, figure_lod_render_distance, @@ -3248,13 +3249,13 @@ impl FigureMgr { let character_state_storage = state.read_storage::(); let character_state = character_state_storage.get(player_entity); - for (entity, pos, _, body, _, loadout, _) in ( + for (entity, pos, _, body, _, inventory, _) in ( &ecs.entities(), &ecs.read_storage::(), ecs.read_storage::().maybe(), &ecs.read_storage::(), ecs.read_storage::().maybe(), - ecs.read_storage::().maybe(), + ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), ) .join() @@ -3270,7 +3271,7 @@ impl FigureMgr { character_state, entity, body, - loadout, + inventory, false, pos.0, figure_lod_render_distance, @@ -3309,8 +3310,8 @@ impl FigureMgr { return; } - let loadout_storage = ecs.read_storage::(); - let loadout = loadout_storage.get(player_entity); + let inventory_storage = ecs.read_storage::(); + let inventory = inventory_storage.get(player_entity); if let Some((locals, bone_consts, model, col_lights)) = self.get_model_for_render( tick, @@ -3318,7 +3319,7 @@ impl FigureMgr { character_state, player_entity, body, - loadout, + inventory, true, pos.0, figure_lod_render_distance, @@ -3345,7 +3346,7 @@ impl FigureMgr { character_state: Option<&CharacterState>, entity: EcsEntity, body: &Body, - loadout: Option<&Loadout>, + inventory: Option<&Inventory>, is_player: bool, pos: vek::Vec3, figure_lod_render_distance: f32, @@ -3404,7 +3405,7 @@ impl FigureMgr { model_cache.get_model( col_lights, *body, - loadout, + inventory, tick, player_camera_mode, character_state, @@ -3421,7 +3422,7 @@ impl FigureMgr { quadruped_small_model_cache.get_model( col_lights, *body, - loadout, + inventory, tick, player_camera_mode, character_state, @@ -3438,7 +3439,7 @@ impl FigureMgr { quadruped_medium_model_cache.get_model( col_lights, *body, - loadout, + inventory, tick, player_camera_mode, character_state, @@ -3455,7 +3456,7 @@ impl FigureMgr { quadruped_low_model_cache.get_model( col_lights, *body, - loadout, + inventory, tick, player_camera_mode, character_state, @@ -3472,7 +3473,7 @@ impl FigureMgr { bird_medium_model_cache.get_model( col_lights, *body, - loadout, + inventory, tick, player_camera_mode, character_state, @@ -3489,7 +3490,7 @@ impl FigureMgr { fish_medium_model_cache.get_model( col_lights, *body, - loadout, + inventory, tick, player_camera_mode, character_state, @@ -3506,7 +3507,7 @@ impl FigureMgr { theropod_model_cache.get_model( col_lights, *body, - loadout, + inventory, tick, player_camera_mode, character_state, @@ -3523,7 +3524,7 @@ impl FigureMgr { dragon_model_cache.get_model( col_lights, *body, - loadout, + inventory, tick, player_camera_mode, character_state, @@ -3540,7 +3541,7 @@ impl FigureMgr { bird_small_model_cache.get_model( col_lights, *body, - loadout, + inventory, tick, player_camera_mode, character_state, @@ -3557,7 +3558,7 @@ impl FigureMgr { fish_small_model_cache.get_model( col_lights, *body, - loadout, + inventory, tick, player_camera_mode, character_state, @@ -3574,7 +3575,7 @@ impl FigureMgr { biped_large_model_cache.get_model( col_lights, *body, - loadout, + inventory, tick, player_camera_mode, character_state, @@ -3591,7 +3592,7 @@ impl FigureMgr { golem_model_cache.get_model( col_lights, *body, - loadout, + inventory, tick, player_camera_mode, character_state, @@ -3608,7 +3609,7 @@ impl FigureMgr { object_model_cache.get_model( col_lights, *body, - loadout, + inventory, tick, player_camera_mode, character_state, diff --git a/voxygen/src/scene/simple.rs b/voxygen/src/scene/simple.rs index a4825c36a3..3075d747e7 100644 --- a/voxygen/src/scene/simple.rs +++ b/voxygen/src/scene/simple.rs @@ -20,7 +20,11 @@ use anim::{ }; use client::Client; use common::{ - comp::{humanoid, item::ItemKind, Loadout}, + comp::{ + humanoid, + inventory::{slot::EquipSlot, Inventory}, + item::ItemKind, + }, figure::Segment, terrain::BlockKind, vol::{BaseVol, ReadVol}, @@ -247,7 +251,7 @@ impl Scene { &mut self, renderer: &mut Renderer, scene_data: SceneData, - loadout: Option<&Loadout>, + inventory: Option<&Inventory>, ) { self.camera.update( scene_data.time, @@ -297,9 +301,9 @@ impl Scene { self.figure_model_cache .clean(&mut self.col_lights, scene_data.tick); - let active_item_kind = loadout - .and_then(|l| l.active_item.as_ref()) - .map(|i| i.item.kind()); + let active_item_kind = inventory + .and_then(|inv| inv.equipped(EquipSlot::Mainhand)) + .map(|i| i.kind()); let active_tool_kind = if let Some(ItemKind::Tool(tool)) = active_item_kind { Some(tool.kind) @@ -307,9 +311,9 @@ impl Scene { None }; - let second_item_kind = loadout - .and_then(|l| l.second_item.as_ref()) - .map(|i| i.item.kind()); + let second_item_kind = inventory + .and_then(|inv| inv.equipped(EquipSlot::Offhand)) + .map(|i| i.kind()); let second_tool_kind = if let Some(ItemKind::Tool(tool)) = second_item_kind { Some(tool.kind) @@ -335,7 +339,7 @@ impl Scene { renderer, &mut self.col_lights, body, - loadout, + inventory, scene_data.tick, CameraMode::default(), None, @@ -367,7 +371,7 @@ impl Scene { renderer: &mut Renderer, tick: u64, body: Option, - loadout: Option<&Loadout>, + inventory: Option<&Inventory>, ) { renderer.render_skybox( &self.skybox.model, @@ -380,7 +384,7 @@ impl Scene { let model = &self.figure_model_cache.get_model( &self.col_lights, body, - loadout, + inventory, tick, CameraMode::default(), None, diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index bc9c464650..e97bd1e594 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -1,21 +1,15 @@ -use crate::{ - audio::sfx::SfxEvent, - ecs::MyEntity, - hud::{DebugInfo, Event as HudEvent, Hud, HudInfo, PressBehavior}, - i18n::{i18n_asset_key, Localization}, - key_state::KeyState, - menu::char_selection::CharSelectionState, - render::Renderer, - scene::{camera, CameraMode, Scene, SceneData}, - settings::{ControlSettings, Settings}, - window::{AnalogGameInput, Event, GameInput}, - Direction, Error, GlobalState, PlayState, PlayStateResult, -}; +use std::{cell::RefCell, rc::Rc, time::Duration}; + +use ordered_float::OrderedFloat; +use specs::{Join, WorldExt}; +use tracing::{error, info}; +use vek::*; + use client::{self, Client}; use common::{ assets::AssetExt, comp, - comp::{ChatMsg, ChatType, InventoryUpdateEvent, Pos, Vel}, + comp::{inventory::slot::Slot, ChatMsg, ChatType, InventoryUpdateEvent, Pos, Vel}, consts::{MAX_MOUNT_RANGE, MAX_PICKUP_RANGE}, outcome::Outcome, span, @@ -27,11 +21,20 @@ use common::{ vol::ReadVol, }; use common_net::msg::PresenceKind; -use ordered_float::OrderedFloat; -use specs::{Join, WorldExt}; -use std::{cell::RefCell, rc::Rc, time::Duration}; -use tracing::{error, info}; -use vek::*; + +use crate::{ + audio::sfx::SfxEvent, + ecs::MyEntity, + hud::{DebugInfo, Event as HudEvent, Hud, HudInfo, PressBehavior, PromptDialogSettings}, + i18n::{i18n_asset_key, Localization}, + key_state::KeyState, + menu::char_selection::CharSelectionState, + render::Renderer, + scene::{camera, CameraMode, Scene, SceneData}, + settings::{ControlSettings, Settings}, + window::{AnalogGameInput, Event, GameInput}, + Direction, Error, GlobalState, PlayState, PlayStateResult, +}; /// The action to perform after a tick enum TickAction { @@ -923,8 +926,133 @@ impl PlayState for SessionState { let mut client = self.client.borrow_mut(); client.remove_buff(buff_id); }, - HudEvent::UseSlot(x) => self.client.borrow_mut().use_slot(x), - HudEvent::SwapSlots(a, b) => self.client.borrow_mut().swap_slots(a, b), + HudEvent::UseSlot { + slot, + bypass_dialog, + } => { + let mut move_allowed = true; + + if !bypass_dialog { + if let Some(inventory) = self + .client + .borrow() + .state() + .ecs() + .read_storage::() + .get(self.client.borrow().entity()) + { + match slot { + comp::slot::Slot::Inventory(inv_slot) => { + let slot_deficit = inventory.free_after_equip(inv_slot); + if slot_deficit < 0 { + self.hud.set_prompt_dialog(PromptDialogSettings::new( + format!( + "Equipping this item will result in \ + insufficient inventory space to hold the \ + items in your inventory and {} items will \ + drop on the floor. Do you wish to continue?", + slot_deficit.abs() + ), + HudEvent::UseSlot { + slot, + bypass_dialog: true, + }, + None, + )); + move_allowed = false; + } + }, + comp::slot::Slot::Equip(equip_slot) => { + // Ensure there is a free slot that is not provided by the + // item being unequipped + let free_slots = + inventory.free_slots_minus_equipped_item(equip_slot); + if free_slots > 0 { + let slot_deficit = + inventory.free_after_unequip(equip_slot); + if slot_deficit < 0 { + self.hud.set_prompt_dialog( + PromptDialogSettings::new( + format!( + "Unequipping this item will result \ + in insufficient inventory space to \ + hold the items in your inventory and \ + {} items will drop on the floor. Do \ + you wish to continue?", + slot_deficit.abs() + ), + HudEvent::UseSlot { + slot, + bypass_dialog: true, + }, + None, + ), + ); + move_allowed = false; + } + } else { + move_allowed = false; + } + }, + } + }; + } + + if move_allowed { + self.client.borrow_mut().use_slot(slot); + } + }, + HudEvent::SwapSlots { + slot_a, + slot_b, + bypass_dialog, + } => { + let mut move_allowed = true; + if !bypass_dialog { + if let Some(inventory) = self + .client + .borrow() + .state() + .ecs() + .read_storage::() + .get(self.client.borrow().entity()) + { + match (slot_a, slot_b) { + (Slot::Inventory(inv_slot), Slot::Equip(equip_slot)) + | (Slot::Equip(equip_slot), Slot::Inventory(inv_slot)) => { + if !inventory.can_swap(inv_slot, equip_slot) { + move_allowed = false; + } else { + let slot_deficit = + inventory.free_after_swap(equip_slot, inv_slot); + if slot_deficit < 0 { + self.hud.set_prompt_dialog( + PromptDialogSettings::new( + format!( + "This will result in dropping {} \ + item(s) on the ground. Are you sure?", + slot_deficit.abs() + ), + HudEvent::SwapSlots { + slot_a, + slot_b, + bypass_dialog: true, + }, + None, + ), + ); + move_allowed = false; + } + } + }, + _ => {}, + } + } + } + if move_allowed { + self.client.borrow_mut().swap_slots(slot_a, slot_b); + } + }, HudEvent::DropSlot(x) => { let mut client = self.client.borrow_mut(); client.drop_slot(x); diff --git a/voxygen/src/ui/widgets/slot.rs b/voxygen/src/ui/widgets/slot.rs index 74c3b2e1c9..f5b99db232 100644 --- a/voxygen/src/ui/widgets/slot.rs +++ b/voxygen/src/ui/widgets/slot.rs @@ -123,6 +123,7 @@ pub struct SlotManager { // Size to display dragged content // Note: could potentially be specialized for each slot if needed drag_img_size: Vec2, + pub mouse_over_slot: Option, } impl SlotManager @@ -137,6 +138,7 @@ where events: Vec::new(), drag_id: gen.next(), drag_img_size, + mouse_over_slot: None, } } @@ -153,11 +155,16 @@ where } } + let input = &ui.global_input().current; + self.mouse_over_slot = input + .widget_under_mouse + .and_then(|x| slot_ids.iter().position(|slot_id| *slot_id == x)) + .map(|x| slots[x]); + // If dragging and mouse is released check if there is a slot widget under the // mouse if let ManagerState::Dragging(_, slot, content_img) = &self.state { let content_img = *content_img; - let input = &ui.global_input().current; if let mouse::ButtonPosition::Up = input.mouse.buttons.left() { // Get widget under the mouse if let Some(id) = input.widget_under_mouse { @@ -430,7 +437,7 @@ where #[allow(clippy::unused_unit)] // TODO: Pending review in #587 fn style(&self) -> Self::Style { () } - /// Update the state of the Slider. + /// Update the state of the Slot. fn update(self, args: widget::UpdateArgs) -> Self::Event { let widget::UpdateArgs { id, @@ -439,6 +446,7 @@ where ui, .. } = args; + let Slot { slot_key, empty_slot, diff --git a/world/src/layer/scatter.rs b/world/src/layer/scatter.rs index 6634c02302..ceca99913f 100644 --- a/world/src/layer/scatter.rs +++ b/world/src/layer/scatter.rs @@ -57,14 +57,14 @@ pub fn apply_scatter_to(canvas: &mut Canvas, rng: &mut impl Rng) { }), (RedFlower, false, |c, col| { ( - close(c.temp, CONFIG.tropical_temp, 0.6).min(close( + close(c.temp, CONFIG.tropical_temp, 0.7).min(close( c.humidity, CONFIG.jungle_hum, - 0.3, + 0.4, )) * col.tree_density * MUSH_FACT * 350.0, - Some((100.0, 0.05)), + Some((100.0, 0.1)), ) }), (WhiteFlower, false, |c, col| { diff --git a/world/src/site/dungeon/mod.rs b/world/src/site/dungeon/mod.rs index 9f4d550d1d..46c42b2c58 100644 --- a/world/src/site/dungeon/mod.rs +++ b/world/src/site/dungeon/mod.rs @@ -10,7 +10,10 @@ use crate::{ use common::{ assets::{AssetExt, AssetHandle}, astar::Astar, - comp::{self}, + comp::{ + inventory::loadout_builder, + {self}, + }, generation::{ChunkSupplement, EntityInfo}, lottery::Lottery, store::{Id, Store}, @@ -596,7 +599,7 @@ impl Floor { //.do_if(is_giant, |e| e.into_giant()) .with_body(comp::Body::Humanoid(comp::humanoid::Body::random())) .with_alignment(comp::Alignment::Enemy) - .with_config(common::loadout_builder::LoadoutConfig::CultistAcolyte) + .with_config(loadout_builder::LoadoutConfig::CultistAcolyte) .with_loot_drop(comp::Item::new_from_asset_expect(chosen)) .with_level(dynamic_rng.gen_range( (room.difficulty as f32).powf(1.25) + 3.0, @@ -605,7 +608,7 @@ impl Floor { let entity = match room.difficulty { 0 => entity .with_name("Outcast") - .with_config(common::loadout_builder::LoadoutConfig::Outcast) + .with_config(loadout_builder::LoadoutConfig::Outcast) .with_loot_drop(comp::Item::new_from_asset_expect(chosen)) .with_main_tool(comp::Item::new_from_asset_expect( match dynamic_rng.gen_range(0, 6) { @@ -619,7 +622,7 @@ impl Floor { )), 1 => entity .with_name("Highwayman") - .with_config(common::loadout_builder::LoadoutConfig::Highwayman) + .with_config(loadout_builder::LoadoutConfig::Highwayman) .with_loot_drop(comp::Item::new_from_asset_expect(chosen)) .with_main_tool(comp::Item::new_from_asset_expect( match dynamic_rng.gen_range(0, 6) { @@ -633,7 +636,7 @@ impl Floor { )), 2 => entity .with_name("Bandit") - .with_config(common::loadout_builder::LoadoutConfig::Bandit) + .with_config(loadout_builder::LoadoutConfig::Bandit) .with_loot_drop(comp::Item::new_from_asset_expect(chosen)) .with_main_tool(comp::Item::new_from_asset_expect( match dynamic_rng.gen_range(0, 6) { @@ -647,7 +650,7 @@ impl Floor { )), 3 => entity .with_name("Cultist Novice") - .with_config(common::loadout_builder::LoadoutConfig::CultistNovice) + .with_config(loadout_builder::LoadoutConfig::CultistNovice) .with_loot_drop(comp::Item::new_from_asset_expect(chosen)) .with_main_tool(comp::Item::new_from_asset_expect( match dynamic_rng.gen_range(0, 6) { @@ -661,7 +664,7 @@ impl Floor { )), 4 => entity .with_name("Cultist Acolyte") - .with_config(common::loadout_builder::LoadoutConfig::CultistAcolyte) + .with_config(loadout_builder::LoadoutConfig::CultistAcolyte) .with_loot_drop(comp::Item::new_from_asset_expect(chosen)) .with_main_tool(comp::Item::new_from_asset_expect( match dynamic_rng.gen_range(0, 6) { @@ -676,14 +679,14 @@ impl Floor { 5 => match dynamic_rng.gen_range(0, 6) { 0 => entity .with_name("Cultist Warlock") - .with_config(common::loadout_builder::LoadoutConfig::Warlock) + .with_config(loadout_builder::LoadoutConfig::Warlock) .with_loot_drop(comp::Item::new_from_asset_expect(chosen)) .with_main_tool(comp::Item::new_from_asset_expect( "common.items.npc_weapons.staff.cultist_staff", )), _ => entity .with_name("Cultist Warlord") - .with_config(common::loadout_builder::LoadoutConfig::Warlord) + .with_config(loadout_builder::LoadoutConfig::Warlord) .with_loot_drop(comp::Item::new_from_asset_expect(chosen)) .with_main_tool(comp::Item::new_from_asset_expect( match dynamic_rng.gen_range(0, 5) { @@ -752,9 +755,7 @@ impl Floor { )) .with_name("Outcast Leader".to_string()) .with_loot_drop(comp::Item::new_from_asset_expect(chosen)) - .with_config( - common::loadout_builder::LoadoutConfig::Outcast, - ) + .with_config(loadout_builder::LoadoutConfig::Outcast) .with_scale(2.0) .with_main_tool(comp::Item::new_from_asset_expect( match dynamic_rng.gen_range(0, 6) { @@ -800,7 +801,7 @@ impl Floor { )) .with_name("Bandit Captain".to_string()) .with_loot_drop(comp::Item::new_from_asset_expect(chosen)) - .with_config(common::loadout_builder::LoadoutConfig::Bandit) + .with_config(loadout_builder::LoadoutConfig::Bandit) .with_scale(2.0) .with_main_tool(comp::Item::new_from_asset_expect( match dynamic_rng.gen_range(0, 6) { @@ -814,27 +815,28 @@ impl Floor { ),); 2 ], - 3 => { - vec![ EntityInfo::at(tile_wcenter.map(|e| e as f32)) - .with_body(comp::Body::Humanoid(comp::humanoid::Body::random())) - .with_name("Cultist Acolyte".to_string()) - .with_loot_drop(comp::Item::new_from_asset_expect(chosen)) - .with_config(common::loadout_builder::LoadoutConfig::CultistAcolyte) - .with_scale(2.0) - .with_main_tool( - comp::Item::new_from_asset_expect( - match dynamic_rng.gen_range(0, 6) { - 0 => "common.items.weapons.axe.malachite_axe-0", - 1 => "common.items.weapons.sword.cultist_purp_2h-0", - 2 => "common.items.weapons.sword.cultist_purp_2h-0", - 3 => "common.items.weapons.hammer.cultist_purp_2h-0", - 4 => "common.items.weapons.staff.cultist_staff", - _ => "common.items.weapons.bow.horn_longbow-0", - }, - ), - ) - ; 2] - }, + 3 => vec![ + EntityInfo::at(tile_wcenter.map(|e| e as f32)) + .with_body(comp::Body::Humanoid( + comp::humanoid::Body::random() + )) + .with_name("Cultist Acolyte".to_string()) + .with_loot_drop(comp::Item::new_from_asset_expect(chosen)) + .with_config(loadout_builder::LoadoutConfig::CultistAcolyte) + .with_scale(2.0) + .with_main_tool(comp::Item::new_from_asset_expect( + match dynamic_rng.gen_range(0, 6) { + 0 => "common.items.weapons.axe.malachite_axe-0", + 1 => "common.items.weapons.sword.cultist_purp_2h-0", + 2 => "common.items.weapons.sword.cultist_purp_2h-0", + 3 => + "common.items.weapons.hammer.cultist_purp_2h-0", + 4 => "common.items.weapons.staff.cultist_staff", + _ => "common.items.weapons.bow.horn_longbow-0", + }, + ),); + 2 + ], 4 => vec![ EntityInfo::at(tile_wcenter.map(|e| e as f32)) .with_body(comp::Body::Golem( @@ -971,9 +973,7 @@ impl Floor { )) .with_name("Animal Trainer".to_string()) .with_loot_drop(comp::Item::new_from_asset_expect(chosen)) - .with_config( - common::loadout_builder::LoadoutConfig::CultistAcolyte, - ) + .with_config(loadout_builder::LoadoutConfig::CultistAcolyte) .with_scale(2.0) .with_main_tool(comp::Item::new_from_asset_expect( match dynamic_rng.gen_range(0, 6) { diff --git a/world/src/site/settlement/mod.rs b/world/src/site/settlement/mod.rs index b583383db3..cd0a8906a4 100644 --- a/world/src/site/settlement/mod.rs +++ b/world/src/site/settlement/mod.rs @@ -15,7 +15,9 @@ use crate::{ }; use common::{ astar::Astar, - comp::{self, bird_medium, humanoid, object, quadruped_small, Item}, + comp::{ + self, bird_medium, humanoid, inventory::loadout_builder, object, quadruped_small, Item, + }, generation::{ChunkSupplement, EntityInfo}, path::Path, spiral::Spiral2d, @@ -932,7 +934,7 @@ impl Settlement { )) .with_name("Guard") .with_level(dynamic_rng.gen_range(10, 15)) - .with_config(common::loadout_builder::LoadoutConfig::Guard), + .with_config(loadout_builder::LoadoutConfig::Guard), _ => entity .with_main_tool(Item::new_from_asset_expect( match dynamic_rng.gen_range(0, 7) { @@ -946,7 +948,7 @@ impl Settlement { //_ => "common.items.npc_weapons.bow.starter_bow", TODO: Re-Add this when we have a better way of distributing npc_weapons here }, )) - .with_config(common::loadout_builder::LoadoutConfig::Villager), + .with_config(loadout_builder::LoadoutConfig::Villager), } });