Refactored loadout to have public functions for each slot instead of requiring callers to use the _INDEX consts

This commit is contained in:
Ben Wallis 2021-01-08 19:12:09 +00:00
parent 9deeefbd1e
commit aef2637288
167 changed files with 4233 additions and 2277 deletions

View File

@ -5,7 +5,7 @@ rustflags = [
[alias] [alias]
generate = "run --package tools --" 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" 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" 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" tracy-voxygen = "-Zunstable-options -Zpackage-features run --bin veloren-voxygen --no-default-features --features tracy,gl,simd --profile no_overflow"

View File

@ -19,6 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Initial support for alternate style keyboards - Initial support for alternate style keyboards
- Flying birds travel the world - Flying birds travel the world
- Plugin system now based on Wasmer 1.0.0 - 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 ### 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 - Glider can now be deployed even when not on the ground
- Gliding now has an energy cost for strenuous maneuvers based on lift - 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 - 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 ### Removed
- SSAAx4 option - SSAAx4 option
- The Stats button and associated screen were removed
### Fixed ### Fixed

2
Cargo.lock generated
View File

@ -6089,6 +6089,7 @@ version = "0.8.0"
dependencies = [ dependencies = [
"authc", "authc",
"chrono", "chrono",
"const-tweaker",
"crossbeam-channel 0.5.0", "crossbeam-channel 0.5.0",
"diesel", "diesel",
"diesel_migrations", "diesel_migrations",
@ -6179,6 +6180,7 @@ dependencies = [
"image", "image",
"inline_tweak", "inline_tweak",
"itertools", "itertools",
"lazy_static",
"native-dialog", "native-dialog",
"num 0.3.1", "num 0.3.1",
"old_school_gfx_glutin_ext", "old_school_gfx_glutin_ext",

View File

@ -4,7 +4,7 @@
(110, Stones), (110, Stones),
(150, ShortGrass), (150, ShortGrass),
(120, CaveMushroom), (120, CaveMushroom),
(2, ShinyGem), (4, ShinyGem),
(2, Chest), (5, Chest),
(15, Crate), (15, Crate),
] ]

View File

@ -9,4 +9,5 @@ ItemDef(
) )
), ),
quality: High, quality: High,
slots: 18,
) )

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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,
)

View File

@ -0,0 +1,12 @@
ItemDef(
name: "Cursed Soulkeeper",
description: "WIP",
kind: Armor(
(
kind: Bag("RedFace"),
stats: (protection: Normal(0.0)),
)
),
quality: Legendary,
slots: 36,
)

View File

@ -0,0 +1,12 @@
ItemDef(
name: "Purified Soulkeeper",
description: "WIP",
kind: Armor(
(
kind: Bag("BlueFace"),
stats: (protection: Normal(0.0)),
)
),
quality: Legendary,
slots: 36,
)

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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,
)

View File

@ -0,0 +1,8 @@
ItemDef(
name: "Red Cloth Scraps",
description: "Dyed red with flower pigments.",
kind: Ingredient(
kind: "ClothScrapsRed",
),
quality: Common,
)

View File

@ -0,0 +1,8 @@
ItemDef(
name: "Troll Hide",
description: "Looted from cave trolls.",
kind: Ingredient(
kind: "TrollLeather",
),
quality: High,
)

View File

@ -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,
)

View File

@ -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,
)

View File

@ -1,8 +1,8 @@
ItemDef( ItemDef(
name: "Red Flower", name: "Red Flower",
description: "Roses are red...", description: "Can be used as a dying ingredient.",
kind: Ingredient( kind: Ingredient(
kind: "Flower", kind: "FlowerRed",
), ),
quality: Common, quality: Common,
) )

View File

@ -1,6 +1,6 @@
ItemDef( ItemDef(
name: "Coconut", 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: Consumable(
kind: "Coconut", kind: "Coconut",
effect: [ effect: [

View File

@ -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,
)

View File

@ -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,
)

View File

@ -6,7 +6,8 @@ ItemDef(
kind: Dagger, kind: Dagger,
stats: ( stats: (
equip_time_millis: 0, equip_time_millis: 0,
power: 1.80 power: 1.80,
speed: 1.0
), ),
) )
), ),

View File

@ -6,7 +6,8 @@ ItemDef(
kind: Dagger, kind: Dagger,
stats: ( stats: (
equip_time_millis: 0, equip_time_millis: 0,
power: 2.00 power: 2.00,
speed: 1.5
), ),
) )
), ),

View File

@ -11,6 +11,7 @@
(1, "common.items.weapons.staff.cultist_staff"), (1, "common.items.weapons.staff.cultist_staff"),
(1, "common.items.weapons.hammer.cultist_purp_2h-0"), (1, "common.items.weapons.hammer.cultist_purp_2h-0"),
(1, "common.items.weapons.sword.cultist_purp_2h-0"), (1, "common.items.weapons.sword.cultist_purp_2h-0"),
(0.25, "common.items.weapons.crafting_ing.mindflayer_bag_damaged"),
// misc // misc
(1, "common.items.boss_drops.lantern"), (1, "common.items.boss_drops.lantern"),
(0.1, "common.items.glider.glider_purp"), (0.1, "common.items.glider.glider_purp"),

View File

@ -1,7 +1,7 @@
[ [
// Misc // Misc
(0.25, "common.items.armor.neck.neck_1"), (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"), (1.0, "common.items.crafting_ing.empty_vial"),
(0.1, "common.items.glider.glider_blue"), (0.1, "common.items.glider.glider_blue"),
(0.1, "common.items.glider.glider_morpho"), (0.1, "common.items.glider.glider_morpho"),
@ -41,7 +41,7 @@
// staves // staves
(1.00, "common.items.weapons.staff.bone_staff"), (1.00, "common.items.weapons.staff.bone_staff"),
(1.00, "common.items.weapons.staff.amethyst_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 // hammers
(0.30, "common.items.weapons.hammer.cobalt_hammer-0"), (0.30, "common.items.weapons.hammer.cobalt_hammer-0"),
(0.30, "common.items.weapons.hammer.cobalt_hammer-1"), (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-4"),
(0.05, "common.items.weapons.hammer.steel_hammer-5"), (0.05, "common.items.weapons.hammer.steel_hammer-5"),
// bows // bows
(0.1, "common.items.weapons.bow.nature_ore_longbow-0"), (0.05, "common.items.weapons.bow.nature_ore_longbow-0"),
] ]

View File

@ -1,7 +1,7 @@
[ [
// crafting ingredients // crafting ingredients
(2, "common.items.crafting_ing.leather_scraps"), (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"), (1, "common.items.crafting_ing.empty_vial"),
(0.10, "common.items.crafting_ing.shiny_gem"), (0.10, "common.items.crafting_ing.shiny_gem"),

View File

@ -5,7 +5,7 @@
(3, "common.items.food.apple"), (3, "common.items.food.apple"),
(3, "common.items.food.mushroom"), (3, "common.items.food.mushroom"),
(3, "common.items.food.coconut"), (3, "common.items.food.coconut"),
(3, "common.items.crafting_ing.cloth_scraps"), (5, "common.items.crafting_ing.cloth_scraps"),
// crafted // crafted
(0.5, "common.items.food.apple_mushroom_curry"), (0.5, "common.items.food.apple_mushroom_curry"),
(0.5, "common.items.food.apple_stick"), (0.5, "common.items.food.apple_stick"),
@ -65,6 +65,9 @@
//gloves //gloves
(0.50, "common.items.armor.hand.leather_0"), (0.50, "common.items.armor.hand.leather_0"),
(0.50, "common.items.armor.hand.leather_2"), (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 // Common Weapons
// swords // swords
(0.4, "common.items.weapons.sword.wood_sword"), (0.4, "common.items.weapons.sword.wood_sword"),

View File

@ -2,7 +2,7 @@
// Crafting Ingredients // Crafting Ingredients
(2, "common.items.crafting_ing.empty_vial"), (2, "common.items.crafting_ing.empty_vial"),
(0.10, "common.items.crafting_ing.shiny_gem"), (0.10, "common.items.crafting_ing.shiny_gem"),
(2, "common.items.crafting_ing.cloth_scraps"), (4, "common.items.crafting_ing.cloth_scraps"),
// Consumables // Consumables
(0.2, "common.items.consumable.potion_minor"), (0.2, "common.items.consumable.potion_minor"),
// Ring // Ring

View File

@ -0,0 +1,5 @@
[
(1, "common.items.flowers.red"),
(1, "common.items.crafting_ing.twigs"),
(0.5, "common.items.food.coconut"),
]

View File

@ -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"),
]

View File

@ -0,0 +1,4 @@
[
(1, "common.items.crafting_ing.leather_troll"),
(0.5, "common.items.crafting_ing.leather_scraps"),
]

View File

@ -25,9 +25,9 @@
(0.30, "common.items.weapons.hammer.cobalt_hammer-1"), (0.30, "common.items.weapons.hammer.cobalt_hammer-1"),
(0.15, "common.items.weapons.hammer.runic_hammer"), (0.15, "common.items.weapons.hammer.runic_hammer"),
(0.15, "common.items.weapons.hammer.ramshead_hammer"), (0.15, "common.items.weapons.hammer.ramshead_hammer"),
(0.10, "common.items.weapons.hammer.mjolnir"), (0.01, "common.items.weapons.hammer.mjolnir"),
// bows // bows
(0.60, "common.items.weapons.bow.horn_longbow-0"), (0.60, "common.items.weapons.bow.horn_longbow-0"),
(0.30, "common.items.weapons.bow.iron_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"),
] ]

View File

@ -1,42 +1,321 @@
{ {
// Tools "crafting_hammer": (
"crafting_hammer": (("common.items.crafting_tools.craftsman_hammer", 1),[("common.items.crafting_ing.twigs", 6), ("common.items.crafting_ing.stones", 6)]), ("common.items.crafting_tools.craftsman_hammer", 1),
"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)]), ("common.items.crafting_ing.twigs", 6),
// Ore and more ("common.items.crafting_ing.stones", 6),
"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)]), "mortar_pestle": (
"potion_m": (("common.items.consumable.potion_med", 1), [("common.items.consumable.potion_minor", 2), ("common.items.ore.veloritefrag", 4)]), ("common.items.crafting_tools.mortar_pestle", 1),
"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)]), ("common.items.crafting_ing.stones", 6),
// Firework ("common.items.food.coconut", 1),
"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)]), ("common.items.crafting_tools.craftsman_hammer", 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)]), "sewing_set": (
"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)]), ("common.items.crafting_tools.sewing_set", 1),
// 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)]), ("common.items.crafting_ing.leather_scraps", 2),
"apples_stick": (("common.items.food.apple_stick", 1),[("common.items.crafting_ing.twigs", 2), ("common.items.food.apple", 2)]), ("common.items.crafting_ing.twigs", 4),
"mushroom_stick": (("common.items.food.mushroom_stick", 1),[("common.items.crafting_ing.twigs", 2), ("common.items.food.mushroom", 3)]), ("common.items.crafting_ing.stones", 2),
"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)]), "velorite_frag": (
"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)]), ("common.items.ore.veloritefrag", 2),
"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)]), ("common.items.ore.velorite", 1),
// Weapons ("common.items.crafting_tools.craftsman_hammer", 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)]), ],
// 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)]), "potion_s": (
"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)]), ("common.items.consumable.potion_minor", 1),
// Adventurer/Beginner Leather Set [
"adventure back": (("common.items.armor.back.leather_adventurer", 1),[("common.items.crafting_ing.leather_scraps", 4)]), ("common.items.crafting_ing.empty_vial", 1),
"adventure belt": (("common.items.armor.belt.leather_adventurer", 1),[("common.items.crafting_ing.leather_scraps", 2)]), ("common.items.food.apple", 4),
"adventure chest": (("common.items.armor.chest.leather_adventurer", 1),[("common.items.crafting_ing.leather_scraps", 12)]), ("common.items.crafting_ing.honey", 1),
"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)]), "potion_m": (
"adventure shoulder": (("common.items.armor.shoulder.leather_adventurer", 1),[("common.items.crafting_ing.leather_scraps", 12)]), ("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),
],
),
} }

BIN
assets/voxygen/element/bag/bot.vox (Stored with Git LFS)

Binary file not shown.

BIN
assets/voxygen/element/bag/mid.vox (Stored with Git LFS)

Binary file not shown.

BIN
assets/voxygen/element/bag/slot.vox (Stored with Git LFS)

Binary file not shown.

BIN
assets/voxygen/element/bag/top.vox (Stored with Git LFS)

Binary file not shown.

BIN
assets/voxygen/element/buttons/inv_collapse.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/buttons/inv_collapse_hover.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/buttons/inv_collapse_press.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/buttons/inv_expand.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/buttons/inv_expand_hover.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/buttons/inv_expand_press.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/buttons/key_button.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/buttons/key_button_press.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/prompt_dialog_bot.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/prompt_dialog_mid.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/frames/prompt_dialog_top.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/bag.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/voxygen/element/icons/item_bag_blue.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/item_bag_blue_face.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/item_bag_brown_face.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/item_bag_green_large.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/item_bag_green_mid.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/item_bag_large.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/item_bag_leather_large.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/item_bag_leather_small.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/item_bag_med.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/item_bag_red_face.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/item_bag_skull.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/item_bag_small.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/item_bag_tiny.png (Stored with Git LFS) Executable file

Binary file not shown.

BIN
assets/voxygen/element/icons/item_cloth_red.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/item_flayer_soul.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/item_leather_green.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

BIN
assets/voxygen/element/misc_bg/inv_bg.png (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

BIN
assets/voxygen/element/misc_bg/inv_bg_bag.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/misc_bg/inv_frame_bag.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/voxygen/element/slider/scrollbar_1.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

View File

@ -59,6 +59,7 @@
"You can toggle showing your amount of health on the healthbar in the settings.", "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.", "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.", "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": [ "npc.speech.villager_under_attack": [
"Help, I'm under attack!", "Help, I'm under attack!",

View File

@ -24,6 +24,7 @@
"hud.bag.feet": "Feet", "hud.bag.feet": "Feet",
"hud.bag.mainhand": "Mainhand", "hud.bag.mainhand": "Mainhand",
"hud.bag.offhand": "Offhand", "hud.bag.offhand": "Offhand",
"hud.bag.bag": "Bag",
}, },

View File

@ -1324,6 +1324,46 @@
"voxel.armor.head.assa_mask-0", "voxel.armor.head.assa_mask-0",
(0.0, 0.0, 0.0), (-90.0, 180.0, 0.0), 1.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 // Consumables
Consumable("Apple"): Png( Consumable("Apple"): Png(
"element.icons.item_apple", "element.icons.item_apple",
@ -1419,6 +1459,10 @@
"voxel.sprite.flowers.sunflower_1", "voxel.sprite.flowers.sunflower_1",
(-2.0, -0.5, -1.0), (-60.0, 40.0, 20.0), 1.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( Ingredient("Sunflower"): VoxTrans(
"voxel.sprite.flowers.sunflower_1", "voxel.sprite.flowers.sunflower_1",
(0.0, -1.0, 0.0), (-50.0, 40.0, 20.0), 0.8, (0.0, -1.0, 0.0), (-50.0, 40.0, 20.0), 0.8,
@ -1434,6 +1478,9 @@
Ingredient("IcyShard"): Png( Ingredient("IcyShard"): Png(
"element.icons.item_ice_shard", "element.icons.item_ice_shard",
), ),
Ingredient("FlayerBagDamaged"): Png(
"element.icons.item_flayer_soul",
),
Ingredient("RaptorFeather"): Png( Ingredient("RaptorFeather"): Png(
"element.icons.item_raptor_feather", "element.icons.item_raptor_feather",
), ),
@ -1447,9 +1494,15 @@
Ingredient("LeatherScraps"): Png( Ingredient("LeatherScraps"): Png(
"element.icons.item_leather0", "element.icons.item_leather0",
), ),
Ingredient("TrollLeather"): Png(
"element.icons.item_leather_green",
),
Ingredient("ClothScraps"): Png( Ingredient("ClothScraps"): Png(
"element.icons.item_cloth0", "element.icons.item_cloth0",
), ),
Ingredient("ClothScrapsRed"): Png(
"element.icons.item_cloth_red",
),
Ingredient("ShinyGem"): Png( Ingredient("ShinyGem"): Png(
"element.icons.gem", "element.icons.gem",
), ),
@ -1479,15 +1532,15 @@
), ),
Glider("SandRaptor"): VoxTrans( Glider("SandRaptor"): VoxTrans(
"voxel.glider.glider_sandraptor", "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( Glider("SnowRaptor"): VoxTrans(
"voxel.glider.glider_snowraptor", "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( Glider("WoodRaptor"): VoxTrans(
"voxel.glider.glider_woodraptor", "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( Glider("Purple0"): VoxTrans(
"voxel.glider.glider_cultists", "voxel.glider.glider_cultists",

View File

@ -933,8 +933,6 @@ impl Client {
pub fn inventories(&self) -> ReadStorage<comp::Inventory> { self.state.read_storage() } pub fn inventories(&self) -> ReadStorage<comp::Inventory> { self.state.read_storage() }
pub fn loadouts(&self) -> ReadStorage<comp::Loadout> { self.state.read_storage() }
/// Send a chat message to the server. /// Send a chat message to the server.
pub fn send_chat(&mut self, message: String) { pub fn send_chat(&mut self, message: String) {
match validate_chat_msg(&message) { match validate_chat_msg(&message) {
@ -1455,11 +1453,10 @@ impl Client {
self.presence = None; self.presence = None;
self.clean_state(); self.clean_state();
}, },
ServerGeneral::InventoryUpdate(mut inventory, event) => { ServerGeneral::InventoryUpdate(inventory, event) => {
match event { match event {
InventoryUpdateEvent::CollectFailed => {}, InventoryUpdateEvent::CollectFailed => {},
_ => { _ => {
inventory.recount_items();
// Push the updated inventory component to the client // Push the updated inventory component to the client
self.state.write_component(self.entity, inventory); self.state.write_component(self.entity, inventory);
}, },

View File

@ -19,6 +19,7 @@ sum_type! {
Energy(comp::Energy), Energy(comp::Energy),
Health(comp::Health), Health(comp::Health),
LightEmitter(comp::LightEmitter), LightEmitter(comp::LightEmitter),
Inventory(comp::Inventory),
Item(comp::Item), Item(comp::Item),
Scale(comp::Scale), Scale(comp::Scale),
Group(comp::Group), Group(comp::Group),
@ -28,7 +29,6 @@ sum_type! {
Collider(comp::Collider), Collider(comp::Collider),
Gravity(comp::Gravity), Gravity(comp::Gravity),
Sticky(comp::Sticky), Sticky(comp::Sticky),
Loadout(comp::Loadout),
CharacterState(comp::CharacterState), CharacterState(comp::CharacterState),
Pos(comp::Pos), Pos(comp::Pos),
Vel(comp::Vel), Vel(comp::Vel),
@ -51,6 +51,7 @@ sum_type! {
Energy(PhantomData<comp::Energy>), Energy(PhantomData<comp::Energy>),
Health(PhantomData<comp::Health>), Health(PhantomData<comp::Health>),
LightEmitter(PhantomData<comp::LightEmitter>), LightEmitter(PhantomData<comp::LightEmitter>),
Inventory(PhantomData<comp::Inventory>),
Item(PhantomData<comp::Item>), Item(PhantomData<comp::Item>),
Scale(PhantomData<comp::Scale>), Scale(PhantomData<comp::Scale>),
Group(PhantomData<comp::Group>), Group(PhantomData<comp::Group>),
@ -60,7 +61,6 @@ sum_type! {
Collider(PhantomData<comp::Collider>), Collider(PhantomData<comp::Collider>),
Gravity(PhantomData<comp::Gravity>), Gravity(PhantomData<comp::Gravity>),
Sticky(PhantomData<comp::Sticky>), Sticky(PhantomData<comp::Sticky>),
Loadout(PhantomData<comp::Loadout>),
CharacterState(PhantomData<comp::CharacterState>), CharacterState(PhantomData<comp::CharacterState>),
Pos(PhantomData<comp::Pos>), Pos(PhantomData<comp::Pos>),
Vel(PhantomData<comp::Vel>), Vel(PhantomData<comp::Vel>),
@ -83,6 +83,7 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPacket::Energy(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Energy(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Health(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::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::Item(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Scale(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), 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::Collider(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Gravity(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::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::CharacterState(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Pos(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), 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::Energy(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Health(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::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::Item(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Scale(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), 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::Collider(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Gravity(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::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::CharacterState(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Pos(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), EcsCompPacket::Vel(comp) => sync::handle_modify(comp, entity, world),
@ -145,6 +145,7 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPhantom::LightEmitter(_) => { EcsCompPhantom::LightEmitter(_) => {
sync::handle_remove::<comp::LightEmitter>(entity, world) sync::handle_remove::<comp::LightEmitter>(entity, world)
}, },
EcsCompPhantom::Inventory(_) => sync::handle_remove::<comp::Inventory>(entity, world),
EcsCompPhantom::Item(_) => sync::handle_remove::<comp::Item>(entity, world), EcsCompPhantom::Item(_) => sync::handle_remove::<comp::Item>(entity, world),
EcsCompPhantom::Scale(_) => sync::handle_remove::<comp::Scale>(entity, world), EcsCompPhantom::Scale(_) => sync::handle_remove::<comp::Scale>(entity, world),
EcsCompPhantom::Group(_) => sync::handle_remove::<comp::Group>(entity, world), EcsCompPhantom::Group(_) => sync::handle_remove::<comp::Group>(entity, world),
@ -154,7 +155,6 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPhantom::Collider(_) => sync::handle_remove::<comp::Collider>(entity, world), EcsCompPhantom::Collider(_) => sync::handle_remove::<comp::Collider>(entity, world),
EcsCompPhantom::Gravity(_) => sync::handle_remove::<comp::Gravity>(entity, world), EcsCompPhantom::Gravity(_) => sync::handle_remove::<comp::Gravity>(entity, world),
EcsCompPhantom::Sticky(_) => sync::handle_remove::<comp::Sticky>(entity, world), EcsCompPhantom::Sticky(_) => sync::handle_remove::<comp::Sticky>(entity, world),
EcsCompPhantom::Loadout(_) => sync::handle_remove::<comp::Loadout>(entity, world),
EcsCompPhantom::CharacterState(_) => { EcsCompPhantom::CharacterState(_) => {
sync::handle_remove::<comp::CharacterState>(entity, world) sync::handle_remove::<comp::CharacterState>(entity, world)
}, },

View File

@ -102,6 +102,7 @@ fn get_armor_kind(kind: &ArmorKind) -> String {
ArmorKind::Neck(_) => "Neck".to_string(), ArmorKind::Neck(_) => "Neck".to_string(),
ArmorKind::Head(_) => "Head".to_string(), ArmorKind::Head(_) => "Head".to_string(),
ArmorKind::Tabard(_) => "Tabard".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::Neck(x) => x.clone(),
ArmorKind::Head(x) => x.clone(), ArmorKind::Head(x) => x.clone(),
ArmorKind::Tabard(x) => x.clone(), ArmorKind::Tabard(x) => x.clone(),
ArmorKind::Bag(x) => x.clone(),
} }
} }

View File

@ -1,6 +1,6 @@
//! Structs representing a playable Character //! Structs representing a playable Character
use crate::comp; use crate::{comp, comp::inventory::Inventory};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// The limit on how many characters that a player can have /// The limit on how many characters that a player can have
@ -21,5 +21,5 @@ pub struct CharacterItem {
pub character: Character, pub character: Character,
pub body: comp::Body, pub body: comp::Body,
pub level: usize, pub level: usize,
pub loadout: comp::Loadout, pub inventory: Inventory,
} }

View File

@ -42,6 +42,7 @@ pub enum ChatCommand {
Campfire, Campfire,
Debug, Debug,
DebugColumn, DebugColumn,
DropAll,
Dummy, Dummy,
Explosion, Explosion,
Faction, Faction,
@ -94,6 +95,7 @@ pub static CHAT_COMMANDS: &[ChatCommand] = &[
ChatCommand::Campfire, ChatCommand::Campfire,
ChatCommand::Debug, ChatCommand::Debug,
ChatCommand::DebugColumn, ChatCommand::DebugColumn,
ChatCommand::DropAll,
ChatCommand::Dummy, ChatCommand::Dummy,
ChatCommand::Explosion, ChatCommand::Explosion,
ChatCommand::Faction, ChatCommand::Faction,
@ -230,6 +232,7 @@ impl ChatCommand {
"Prints some debug information about a column", "Prints some debug information about a column",
NoAdmin, NoAdmin,
), ),
ChatCommand::DropAll => cmd(vec![], "Drops all your items on the ground", Admin),
ChatCommand::Dummy => cmd(vec![], "Spawns a training dummy", Admin), ChatCommand::Dummy => cmd(vec![], "Spawns a training dummy", Admin),
ChatCommand::Explosion => cmd( ChatCommand::Explosion => cmd(
vec![Float("radius", 5.0, Required)], vec![Float("radius", 5.0, Required)],
@ -445,6 +448,7 @@ impl ChatCommand {
ChatCommand::Campfire => "campfire", ChatCommand::Campfire => "campfire",
ChatCommand::Debug => "debug", ChatCommand::Debug => "debug",
ChatCommand::DebugColumn => "debug_column", ChatCommand::DebugColumn => "debug_column",
ChatCommand::DropAll => "dropall",
ChatCommand::Dummy => "dummy", ChatCommand::Dummy => "dummy",
ChatCommand::Explosion => "explosion", ChatCommand::Explosion => "explosion",
ChatCommand::Faction => "faction", ChatCommand::Faction => "faction",
@ -533,12 +537,11 @@ impl Display for ChatCommand {
impl FromStr for ChatCommand { impl FromStr for ChatCommand {
type Err = (); type Err = ();
#[allow(clippy::manual_strip)]
fn from_str(keyword: &str) -> Result<ChatCommand, ()> { fn from_str(keyword: &str) -> Result<ChatCommand, ()> {
let kwd = if keyword.starts_with('/') { let kwd = if let Some(stripped) = keyword.strip_prefix('/') {
&keyword[1..] stripped
} else { } else {
&keyword[..] &keyword
}; };
if keyword.len() == 1 { if keyword.len() == 1 {
if let Some(c) = keyword if let Some(c) = keyword

View File

@ -1,5 +1,8 @@
use crate::{ use crate::{
comp::{HealthChange, HealthSource, Loadout}, comp::{
inventory::item::{armor::Protection, ItemKind},
HealthChange, HealthSource, Inventory,
},
uid::Uid, uid::Uid,
util::Dir, util::Dir,
}; };
@ -31,8 +34,32 @@ pub struct Damage {
} }
impl Damage { impl Damage {
pub fn modify_damage(self, loadout: Option<&Loadout>, uid: Option<Uid>) -> 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::<Option<f32>>();
match protection {
Some(dr) => dr / (60.0 + dr.abs()),
None => 1.0,
}
}
pub fn modify_damage(self, inventory: Option<&Inventory>, uid: Option<Uid>) -> HealthChange {
let mut damage = self.value; let mut damage = self.value;
let damage_reduction = inventory.map_or(0.0, |inv| Damage::compute_damage_reduction(inv));
match self.source { match self.source {
DamageSource::Melee => { DamageSource::Melee => {
// Critical hit // Critical hit
@ -41,7 +68,6 @@ impl Damage {
critdamage = damage * 0.3; critdamage = damage * 0.3;
} }
// Armor // Armor
let damage_reduction = loadout.map_or(0.0, |l| l.get_damage_reduction());
damage *= 1.0 - damage_reduction; damage *= 1.0 - damage_reduction;
// Critical damage applies after armor for melee // Critical damage applies after armor for melee
@ -63,7 +89,6 @@ impl Damage {
damage *= 1.2; damage *= 1.2;
} }
// Armor // Armor
let damage_reduction = loadout.map_or(0.0, |l| l.get_damage_reduction());
damage *= 1.0 - damage_reduction; damage *= 1.0 - damage_reduction;
HealthChange { HealthChange {
@ -76,7 +101,6 @@ impl Damage {
}, },
DamageSource::Explosion => { DamageSource::Explosion => {
// Armor // Armor
let damage_reduction = loadout.map_or(0.0, |l| l.get_damage_reduction());
damage *= 1.0 - damage_reduction; damage *= 1.0 - damage_reduction;
HealthChange { HealthChange {
@ -89,7 +113,6 @@ impl Damage {
}, },
DamageSource::Shockwave => { DamageSource::Shockwave => {
// Armor // Armor
let damage_reduction = loadout.map_or(0.0, |l| l.get_damage_reduction());
damage *= 1.0 - damage_reduction; damage *= 1.0 - damage_reduction;
HealthChange { HealthChange {
@ -102,7 +125,6 @@ impl Damage {
}, },
DamageSource::Energy => { DamageSource::Energy => {
// Armor // Armor
let damage_reduction = loadout.map_or(0.0, |l| l.get_damage_reduction());
damage *= 1.0 - damage_reduction; damage *= 1.0 - damage_reduction;
HealthChange { HealthChange {
@ -119,7 +141,6 @@ impl Damage {
}, },
DamageSource::Falling => { DamageSource::Falling => {
// Armor // Armor
let damage_reduction = loadout.map_or(0.0, |l| l.get_damage_reduction());
if (damage_reduction - 1.0).abs() < f32::EPSILON { if (damage_reduction - 1.0).abs() < f32::EPSILON {
damage = 0.0; damage = 0.0;
} }

View File

@ -1,9 +1,8 @@
use crate::{ use crate::{
assets::{self, Asset}, assets::{self, Asset},
comp::{ comp::{
item::{armor::Protection, tool::AbilityMap, Item, ItemKind}, projectile::ProjectileConstructor, Body, CharacterState, EnergySource, Gravity,
projectile::ProjectileConstructor, LightEmitter, StateUpdate,
Body, CharacterState, EnergySource, Gravity, LightEmitter, StateUpdate,
}, },
states::{ states::{
behavior::JoinData, behavior::JoinData,
@ -12,10 +11,7 @@ use crate::{
}, },
Knockback, Knockback,
}; };
use arraygen::Arraygen;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specs::{Component, DerefFlaggedStorage};
use specs_idvs::IdvStorage;
use std::time::Duration; use std::time::Duration;
use vek::Vec3; use vek::Vec3;
@ -304,7 +300,7 @@ impl CharacterAbility {
} }
} }
fn default_roll() -> CharacterAbility { pub fn default_roll() -> CharacterAbility {
CharacterAbility::Roll { CharacterAbility::Roll {
energy_cost: 100, energy_cost: 100,
buildup_duration: 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<CharacterAbility>,
pub ability2: Option<CharacterAbility>,
pub ability3: Option<CharacterAbility>,
pub block_ability: Option<CharacterAbility>,
pub dodge_ability: Option<CharacterAbility>,
}
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<Item>)]
pub struct Loadout {
pub active_item: Option<ItemConfig>,
pub second_item: Option<ItemConfig>,
pub lantern: Option<Item>,
pub glider: Option<Item>,
#[in_array(get_armor)]
pub shoulder: Option<Item>,
#[in_array(get_armor)]
pub chest: Option<Item>,
#[in_array(get_armor)]
pub belt: Option<Item>,
#[in_array(get_armor)]
pub hand: Option<Item>,
#[in_array(get_armor)]
pub pants: Option<Item>,
#[in_array(get_armor)]
pub foot: Option<Item>,
#[in_array(get_armor)]
pub back: Option<Item>,
#[in_array(get_armor)]
pub ring: Option<Item>,
#[in_array(get_armor)]
pub neck: Option<Item>,
#[in_array(get_armor)]
pub head: Option<Item>,
#[in_array(get_armor)]
pub tabard: Option<Item>,
}
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::<Option<f32>>();
match protection {
Some(dr) => dr / (60.0 + dr.abs()),
None => 1.0,
}
}
}
impl From<(&CharacterAbility, AbilityKey)> for CharacterState { impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
fn from((ability, key): (&CharacterAbility, AbilityKey)) -> Self { fn from((ability, key): (&CharacterAbility, AbilityKey)) -> Self {
match ability { match ability {
@ -975,7 +884,3 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
} }
} }
} }
impl Component for Loadout {
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
}

View File

@ -13,6 +13,7 @@ pub enum ArmorKind {
Neck(String), Neck(String),
Head(String), Head(String),
Tabard(String), Tabard(String),
Bag(String),
} }
impl Armor { impl Armor {
@ -43,4 +44,12 @@ pub struct Armor {
impl Armor { impl Armor {
pub fn get_protection(&self) -> Protection { self.stats.protection } 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 },
}
}
} }

View File

@ -6,10 +6,15 @@ pub use tool::{AbilitySet, Hands, Tool, ToolKind, UniqueKind};
use crate::{ use crate::{
assets::{self, AssetExt, Error}, assets::{self, AssetExt, Error},
comp::{
inventory::{item::tool::AbilityMap, InvSlot},
Body, CharacterAbility,
},
effect::Effect, effect::Effect,
lottery::Lottery, lottery::Lottery,
terrain::{Block, SpriteKind}, terrain::{Block, SpriteKind},
}; };
use core::mem;
use crossbeam_utils::atomic::AtomicCell; use crossbeam_utils::atomic::AtomicCell;
use rand::prelude::*; use rand::prelude::*;
use serde::{Deserialize, Serialize}; 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<Option<NonZeroU64>>; pub type ItemId = AtomicCell<Option<NonZeroU64>>;
/* /// The only way to access an item id outside this module is to mutably, atomically update it using /* /// 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<Option<NonZeroU64>>;
/// only if it's not already set. /// only if it's not already set.
pub struct CreateDatabaseItemId { pub struct CreateDatabaseItemId {
item_id: Arc<ItemId>, item_id: Arc<ItemId>,
} }*/
pub struct CreateDatabaseItemId {
item_id: Arc<ItemId>,
} */
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Item { pub struct Item {
@ -124,22 +134,54 @@ pub struct Item {
/// amount is hidden because it needs to maintain the invariant that only /// amount is hidden because it needs to maintain the invariant that only
/// stackable items can have > 1 amounts. /// stackable items can have > 1 amounts.
amount: NonZeroU32, amount: NonZeroU32,
/// The slots for items that this item has
slots: Vec<InvSlot>,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct ItemDef { pub struct ItemDef {
#[serde(default)] #[serde(default)]
item_definition_id: String, item_definition_id: String,
pub item_config: Option<ItemConfig>,
pub name: String, pub name: String,
pub description: String, pub description: String,
pub kind: ItemKind, pub kind: ItemKind,
pub quality: Quality, pub quality: Quality,
#[serde(default)]
pub slots: u16,
} }
impl PartialEq for ItemDef { impl PartialEq for ItemDef {
fn eq(&self, other: &Self) -> bool { self.item_definition_id == other.item_definition_id } 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<CharacterAbility>,
pub ability2: Option<CharacterAbility>,
pub ability3: Option<CharacterAbility>,
pub block_ability: Option<CharacterAbility>,
pub dodge_ability: Option<CharacterAbility>,
}
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 { impl ItemDef {
pub fn is_stackable(&self) -> bool { pub fn is_stackable(&self) -> bool {
matches!( matches!(
@ -150,6 +192,25 @@ impl ItemDef {
| ItemKind::Utility { .. } | ItemKind::Utility { .. }
) )
} }
#[cfg(test)]
pub fn new_test(
item_definition_id: String,
item_config: Option<ItemConfig>,
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 { impl PartialEq for Item {
@ -170,8 +231,19 @@ impl assets::Compound for ItemDef {
description, description,
kind, kind,
quality, quality,
slots,
} = raw; } = raw;
let item_config = if let ItemKind::Tool(_) = kind {
let ability_map_handle =
cache.load::<AbilityMap>("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 \ // Some commands like /give_item provide the asset specifier separated with \
// instead of . // instead of .
// //
@ -180,10 +252,12 @@ impl assets::Compound for ItemDef {
Ok(ItemDef { Ok(ItemDef {
item_definition_id, item_definition_id,
item_config,
name, name,
description, description,
kind, kind,
quality, quality,
slots,
}) })
} }
} }
@ -195,6 +269,8 @@ struct RawItemDef {
description: String, description: String,
kind: ItemKind, kind: ItemKind,
quality: Quality, quality: Quality,
#[serde(default)]
slots: u16,
} }
impl assets::Asset for RawItemDef { impl assets::Asset for RawItemDef {
@ -229,11 +305,12 @@ impl Item {
// loadout when no weapon is present // loadout when no weapon is present
pub fn empty() -> Self { Item::new_from_asset_expect("common.items.weapons.empty.empty") } pub fn empty() -> Self { Item::new_from_asset_expect("common.items.weapons.empty.empty") }
pub fn new(inner_item: Arc<ItemDef>) -> Self { pub fn new_from_item_def(inner_item: Arc<ItemDef>) -> Self {
Item { Item {
item_id: Arc::new(AtomicCell::new(None)), item_id: Arc::new(AtomicCell::new(None)),
item_def: inner_item,
amount: NonZeroU32::new(1).unwrap(), 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. /// Panics if the asset does not exist.
pub fn new_from_asset_expect(asset_specifier: &str) -> Self { pub fn new_from_asset_expect(asset_specifier: &str) -> Self {
let inner_item = Arc::<ItemDef>::load_expect_cloned(asset_specifier); let inner_item = Arc::<ItemDef>::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 /// Creates a Vec containing one of each item that matches the provided
@ -254,11 +331,43 @@ impl Item {
/// it exists /// it exists
pub fn new_from_asset(asset: &str) -> Result<Self, Error> { pub fn new_from_asset(asset: &str) -> Result<Self, Error> {
let inner_item = Arc::<ItemDef>::load_cloned(asset)?; let inner_item = Arc::<ItemDef>::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 /// 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 /// 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 /// 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<Item = Item> + '_ {
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 item_definition_id(&self) -> &str { &self.item_def.item_definition_id }
pub fn is_same_item_def(&self, item_def: &ItemDef) -> bool { 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 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<Self> { pub fn try_reclaim_from_block(block: Block) -> Option<Self> {
let chosen; let chosen;
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
@ -416,6 +550,7 @@ pub trait ItemDesc {
fn name(&self) -> &str; fn name(&self) -> &str;
fn kind(&self) -> &ItemKind; fn kind(&self) -> &ItemKind;
fn quality(&self) -> &Quality; fn quality(&self) -> &Quality;
fn num_slots(&self) -> u16;
fn item_definition_id(&self) -> &str; fn item_definition_id(&self) -> &str;
} }
@ -428,6 +563,8 @@ impl ItemDesc for Item {
fn quality(&self) -> &Quality { &self.item_def.quality } 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 } 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 quality(&self) -> &Quality { &self.quality }
fn num_slots(&self) -> u16 { self.slots }
fn item_definition_id(&self) -> &str { &self.item_definition_id } fn item_definition_id(&self) -> &str { &self.item_definition_id }
} }

View File

@ -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<LoadoutSlot>,
}
#[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<Item>) -> Option<Item> {
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<usize> {
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<Item = (&str, Option<&Item>)> {
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<EquipSlot> {
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<Item = (LoadoutSlotId, &InvSlot)> {
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<Item = &mut InvSlot> {
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<InvSlot> 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<Range<usize>> {
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<Item = &Item> {
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);
}
}

View File

@ -1,7 +1,11 @@
use crate::comp::{ use crate::comp::{
biped_large, golem, biped_large, golem,
item::{tool::AbilityMap, Item, ItemKind}, inventory::{
quadruped_low, quadruped_medium, theropod, Body, CharacterAbility, ItemConfig, Loadout, loadout::Loadout,
slot::{ArmorSlot, EquipSlot},
},
item::{Item, ItemKind},
quadruped_low, quadruped_medium, theropod, Body,
}; };
use rand::Rng; use rand::Rng;
@ -13,19 +17,18 @@ use rand::Rng;
/// use veloren_common::{ /// use veloren_common::{
/// assets::AssetExt, /// assets::AssetExt,
/// comp::item::tool::AbilityMap, /// comp::item::tool::AbilityMap,
/// comp::Item,
/// LoadoutBuilder, /// 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 /// // Build a loadout with character starter defaults and a specific sword with default sword abilities
/// let loadout = LoadoutBuilder::new() /// let loadout = LoadoutBuilder::new()
/// .defaults() /// .defaults()
/// .active_item(Some(LoadoutBuilder::default_item_config_from_str( /// .active_item(Some(Item::new_from_asset_expect("common.items.weapons.sword.zweihander_sword_0")))
/// "common.items.weapons.sword.zweihander_sword_0", &map
/// )))
/// .build(); /// .build();
/// ``` /// ```
#[derive(Clone)]
pub struct LoadoutBuilder(Loadout);
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub enum LoadoutConfig { pub enum LoadoutConfig {
@ -40,29 +43,9 @@ pub enum LoadoutConfig {
Warlock, Warlock,
} }
pub struct LoadoutBuilder(Loadout);
impl LoadoutBuilder { impl LoadoutBuilder {
#[allow(clippy::new_without_default)] // TODO: Pending review in #587 #[allow(clippy::new_without_default)] // TODO: Pending review in #587
pub fn new() -> Self { pub fn new() -> Self { Self(Loadout::new_empty()) }
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,
})
}
/// Set default armor items for the loadout. This may vary with game /// Set default armor items for the loadout. This may vary with game
/// updates, but should be safe defaults for a new character. /// updates, but should be safe defaults for a new character.
@ -73,7 +56,7 @@ impl LoadoutBuilder {
.pants(Some(Item::new_from_asset_expect( .pants(Some(Item::new_from_asset_expect(
"common.items.armor.starter.rugged_pants", "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", "common.items.armor.starter.sandals_0",
))) )))
.lantern(Some(Item::new_from_asset_expect( .lantern(Some(Item::new_from_asset_expect(
@ -89,7 +72,6 @@ impl LoadoutBuilder {
pub fn build_loadout( pub fn build_loadout(
body: Body, body: Body,
mut main_tool: Option<Item>, mut main_tool: Option<Item>,
map: &AbilityMap,
config: Option<LoadoutConfig>, config: Option<LoadoutConfig>,
) -> Self { ) -> Self {
// If no main tool is passed in, checks if species has a default main tool // 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 // Constructs ItemConfig from Item
let active_item = if let Some(ItemKind::Tool(_)) = main_tool.as_ref().map(|i| i.kind()) { 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 { } else {
Some(LoadoutBuilder::animal(body)) Some(Item::new_default_for_body(&body))
}; };
// Creates rest of loadout // Creates rest of loadout
let loadout = if let Some(config) = config { let loadout = if let Some(config) = config {
use LoadoutConfig::*; use LoadoutConfig::*;
match config { match config {
Guard => Loadout { Guard => LoadoutBuilder::new()
active_item, .active_item(active_item)
second_item: None, .shoulder(Some(Item::new_from_asset_expect(
shoulder: Some(Item::new_from_asset_expect(
"common.items.armor.shoulder.steel_0", "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", "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", "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", "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", "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", "common.items.armor.foot.steel_0",
)), )))
back: None, .lantern(match rand::thread_rng().gen_range(0, 3) {
ring: None,
neck: None,
lantern: match rand::thread_rng().gen_range(0, 3) {
0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")), 0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")),
_ => None, _ => None,
}, })
glider: None, .build(),
head: None, Outcast => LoadoutBuilder::new()
tabard: None, .active_item(active_item)
}, .shoulder(Some(Item::new_from_asset_expect(
Outcast => Loadout {
active_item,
second_item: None,
shoulder: Some(Item::new_from_asset_expect(
"common.items.armor.shoulder.cloth_purple_0", "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", "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", "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", "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", "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", "common.items.armor.foot.cloth_purple_0",
)), )))
back: None, .lantern(match rand::thread_rng().gen_range(0, 3) {
ring: None,
neck: None,
lantern: match rand::thread_rng().gen_range(0, 3) {
0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")), 0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")),
_ => None, _ => None,
}, })
glider: None, .build(),
head: None, Highwayman => LoadoutBuilder::new()
tabard: None, .active_item(active_item)
}, .shoulder(Some(Item::new_from_asset_expect(
Highwayman => Loadout {
active_item,
second_item: None,
shoulder: Some(Item::new_from_asset_expect(
"common.items.armor.shoulder.leather_0", "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", "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", "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", "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", "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", "common.items.armor.foot.leather_0",
)), )))
back: None, .lantern(match rand::thread_rng().gen_range(0, 3) {
ring: None,
neck: None,
lantern: match rand::thread_rng().gen_range(0, 3) {
0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")), 0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")),
_ => None, _ => None,
}, })
glider: None, .build(),
head: None, Bandit => LoadoutBuilder::new()
tabard: None, .active_item(active_item)
}, .shoulder(Some(Item::new_from_asset_expect(
Bandit => Loadout {
active_item,
second_item: None,
shoulder: Some(Item::new_from_asset_expect(
"common.items.armor.shoulder.assassin", "common.items.armor.shoulder.assassin",
)), )))
chest: Some(Item::new_from_asset_expect( .chest(Some(Item::new_from_asset_expect(
"common.items.armor.chest.assassin", "common.items.armor.chest.assassin",
)), )))
belt: Some(Item::new_from_asset_expect( .belt(Some(Item::new_from_asset_expect(
"common.items.armor.belt.assassin", "common.items.armor.belt.assassin",
)), )))
hand: Some(Item::new_from_asset_expect( .hands(Some(Item::new_from_asset_expect(
"common.items.armor.hand.assassin", "common.items.armor.hand.assassin",
)), )))
pants: Some(Item::new_from_asset_expect( .pants(Some(Item::new_from_asset_expect(
"common.items.armor.pants.assassin", "common.items.armor.pants.assassin",
)), )))
foot: Some(Item::new_from_asset_expect( .feet(Some(Item::new_from_asset_expect(
"common.items.armor.foot.assassin", "common.items.armor.foot.assassin",
)), )))
back: None, .lantern(match rand::thread_rng().gen_range(0, 3) {
ring: None,
neck: None,
lantern: match rand::thread_rng().gen_range(0, 3) {
0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")), 0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")),
_ => None, _ => None,
}, })
glider: None, .build(),
head: None, CultistNovice => LoadoutBuilder::new()
tabard: None, .active_item(active_item)
}, .shoulder(Some(Item::new_from_asset_expect(
CultistNovice => Loadout {
active_item,
second_item: None,
shoulder: Some(Item::new_from_asset_expect(
"common.items.armor.shoulder.steel_0", "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", "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", "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", "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", "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", "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", "common.items.armor.back.dungeon_purple-0",
)), )))
ring: None, .lantern(match rand::thread_rng().gen_range(0, 3) {
neck: None,
lantern: match rand::thread_rng().gen_range(0, 3) {
0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")), 0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")),
_ => None, _ => None,
}, })
glider: None, .build(),
head: None, CultistAcolyte => LoadoutBuilder::new()
tabard: None, .active_item(active_item)
}, .shoulder(Some(Item::new_from_asset_expect(
CultistAcolyte => Loadout {
active_item,
second_item: None,
shoulder: Some(Item::new_from_asset_expect(
"common.items.armor.shoulder.cultist_shoulder_purple", "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", "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", "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", "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", "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", "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", "common.items.armor.back.dungeon_purple-0",
)), )))
ring: None, .lantern(match rand::thread_rng().gen_range(0, 3) {
neck: None,
lantern: match rand::thread_rng().gen_range(0, 3) {
0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")), 0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")),
_ => None, _ => None,
}, })
glider: None, .build(),
head: None, Warlord => LoadoutBuilder::new()
tabard: None, .active_item(active_item)
}, .shoulder(Some(Item::new_from_asset_expect(
Warlord => Loadout {
active_item,
second_item: None,
shoulder: Some(Item::new_from_asset_expect(
"common.items.armor.shoulder.warlord", "common.items.armor.shoulder.warlord",
)), )))
chest: Some(Item::new_from_asset_expect( .chest(Some(Item::new_from_asset_expect(
"common.items.armor.chest.warlord", "common.items.armor.chest.warlord",
)), )))
belt: Some(Item::new_from_asset_expect( .belt(Some(Item::new_from_asset_expect(
"common.items.armor.belt.warlord", "common.items.armor.belt.warlord",
)), )))
hand: Some(Item::new_from_asset_expect( .hands(Some(Item::new_from_asset_expect(
"common.items.armor.hand.warlord", "common.items.armor.hand.warlord",
)), )))
pants: Some(Item::new_from_asset_expect( .pants(Some(Item::new_from_asset_expect(
"common.items.armor.pants.warlord", "common.items.armor.pants.warlord",
)), )))
foot: Some(Item::new_from_asset_expect( .feet(Some(Item::new_from_asset_expect(
"common.items.armor.foot.warlord", "common.items.armor.foot.warlord",
)), )))
back: Some(Item::new_from_asset_expect( .back(Some(Item::new_from_asset_expect(
"common.items.armor.back.warlord", "common.items.armor.back.warlord",
)), )))
ring: None, .lantern(match rand::thread_rng().gen_range(0, 3) {
neck: None,
lantern: match rand::thread_rng().gen_range(0, 3) {
0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")), 0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")),
_ => None, _ => None,
}, })
glider: None, .build(),
head: None, Warlock => LoadoutBuilder::new()
tabard: None, .active_item(active_item)
}, .shoulder(Some(Item::new_from_asset_expect(
Warlock => Loadout {
active_item,
second_item: None,
shoulder: Some(Item::new_from_asset_expect(
"common.items.armor.shoulder.warlock", "common.items.armor.shoulder.warlock",
)), )))
chest: Some(Item::new_from_asset_expect( .chest(Some(Item::new_from_asset_expect(
"common.items.armor.chest.warlock", "common.items.armor.chest.warlock",
)), )))
belt: Some(Item::new_from_asset_expect( .belt(Some(Item::new_from_asset_expect(
"common.items.armor.belt.warlock", "common.items.armor.belt.warlock",
)), )))
hand: Some(Item::new_from_asset_expect( .hands(Some(Item::new_from_asset_expect(
"common.items.armor.hand.warlock", "common.items.armor.hand.warlock",
)), )))
pants: Some(Item::new_from_asset_expect( .pants(Some(Item::new_from_asset_expect(
"common.items.armor.pants.warlock", "common.items.armor.pants.warlock",
)), )))
foot: Some(Item::new_from_asset_expect( .feet(Some(Item::new_from_asset_expect(
"common.items.armor.foot.warlock", "common.items.armor.foot.warlock",
)), )))
back: Some(Item::new_from_asset_expect( .back(Some(Item::new_from_asset_expect(
"common.items.armor.back.warlock", "common.items.armor.back.warlock",
)), )))
ring: None, .lantern(match rand::thread_rng().gen_range(0, 3) {
neck: None,
lantern: match rand::thread_rng().gen_range(0, 3) {
0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")), 0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")),
_ => None, _ => None,
}, })
glider: None, .build(),
head: None, Villager => LoadoutBuilder::new()
tabard: None, .active_item(active_item)
}, .chest(Some(Item::new_from_asset_expect(
Villager => Loadout {
active_item,
second_item: None,
shoulder: None,
chest: Some(Item::new_from_asset_expect(
match rand::thread_rng().gen_range(0, 10) { match rand::thread_rng().gen_range(0, 10) {
0 => "common.items.armor.chest.worker_green_0", 0 => "common.items.armor.chest.worker_green_0",
1 => "common.items.armor.chest.worker_green_1", 1 => "common.items.armor.chest.worker_green_1",
@ -541,167 +469,105 @@ impl LoadoutBuilder {
8 => "common.items.armor.chest.worker_orange_0", 8 => "common.items.armor.chest.worker_orange_0",
_ => "common.items.armor.chest.worker_orange_1", _ => "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", "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", "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) { match rand::thread_rng().gen_range(0, 2) {
0 => "common.items.armor.foot.leather_0", 0 => "common.items.armor.foot.leather_0",
_ => "common.items.armor.starter.sandals_0", _ => "common.items.armor.starter.sandals_0",
}, },
)), )))
back: None, .build(),
ring: None,
neck: None,
lantern: None,
glider: None,
head: None,
tabard: None,
},
} }
} else { } else {
Loadout { LoadoutBuilder::new().active_item(active_item).build()
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,
}
}; };
Self(loadout) Self(loadout)
} }
/// Default animal configuration pub fn active_item(mut self, item: Option<Item>) -> Self {
pub fn animal(body: Body) -> ItemConfig { self.0.swap(EquipSlot::Mainhand, item);
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<ItemConfig>) -> Self {
self.0.active_item = item;
self self
} }
pub fn second_item(mut self, item: Option<ItemConfig>) -> Self { pub fn second_item(mut self, item: Option<Item>) -> Self {
self.0.second_item = item; self.0.swap(EquipSlot::Offhand, item);
self self
} }
pub fn shoulder(mut self, item: Option<Item>) -> Self { pub fn shoulder(mut self, item: Option<Item>) -> Self {
self.0.shoulder = item; self.0.swap(EquipSlot::Armor(ArmorSlot::Shoulders), item);
self self
} }
pub fn chest(mut self, item: Option<Item>) -> Self { pub fn chest(mut self, item: Option<Item>) -> Self {
self.0.chest = item; self.0.swap(EquipSlot::Armor(ArmorSlot::Chest), item);
self self
} }
pub fn belt(mut self, item: Option<Item>) -> Self { pub fn belt(mut self, item: Option<Item>) -> Self {
self.0.belt = item; self.0.swap(EquipSlot::Armor(ArmorSlot::Belt), item);
self self
} }
pub fn hand(mut self, item: Option<Item>) -> Self { pub fn hands(mut self, item: Option<Item>) -> Self {
self.0.hand = item; self.0.swap(EquipSlot::Armor(ArmorSlot::Hands), item);
self self
} }
pub fn pants(mut self, item: Option<Item>) -> Self { pub fn pants(mut self, item: Option<Item>) -> Self {
self.0.pants = item; self.0.swap(EquipSlot::Armor(ArmorSlot::Legs), item);
self self
} }
pub fn foot(mut self, item: Option<Item>) -> Self { pub fn feet(mut self, item: Option<Item>) -> Self {
self.0.foot = item; self.0.swap(EquipSlot::Armor(ArmorSlot::Feet), item);
self self
} }
pub fn back(mut self, item: Option<Item>) -> Self { pub fn back(mut self, item: Option<Item>) -> Self {
self.0.back = item; self.0.swap(EquipSlot::Armor(ArmorSlot::Back), item);
self self
} }
pub fn ring(mut self, item: Option<Item>) -> Self { pub fn ring1(mut self, item: Option<Item>) -> Self {
self.0.ring = item; self.0.swap(EquipSlot::Armor(ArmorSlot::Ring1), item);
self
}
pub fn ring2(mut self, item: Option<Item>) -> Self {
self.0.swap(EquipSlot::Armor(ArmorSlot::Ring2), item);
self self
} }
pub fn neck(mut self, item: Option<Item>) -> Self { pub fn neck(mut self, item: Option<Item>) -> Self {
self.0.neck = item; self.0.swap(EquipSlot::Armor(ArmorSlot::Neck), item);
self self
} }
pub fn lantern(mut self, item: Option<Item>) -> Self { pub fn lantern(mut self, item: Option<Item>) -> Self {
self.0.lantern = item; self.0.swap(EquipSlot::Lantern, item);
self self
} }
pub fn glider(mut self, item: Option<Item>) -> Self { pub fn glider(mut self, item: Option<Item>) -> Self {
self.0.glider = item; self.0.swap(EquipSlot::Glider, item);
self self
} }
pub fn head(mut self, item: Option<Item>) -> Self { pub fn head(mut self, item: Option<Item>) -> Self {
self.0.head = item; self.0.swap(EquipSlot::Armor(ArmorSlot::Head), item);
self self
} }
pub fn tabard(mut self, item: Option<Item>) -> Self { pub fn tabard(mut self, item: Option<Item>) -> Self {
self.0.tabard = item; self.0.swap(EquipSlot::Armor(ArmorSlot::Tabard), item);
self self
} }

View File

@ -1,17 +1,41 @@
pub mod item;
pub mod slot;
use crate::{comp::inventory::item::ItemDef, recipe::Recipe};
use core::ops::Not; use core::ops::Not;
use item::Item; use std::{collections::HashMap, convert::TryFrom, iter::once, mem, ops::Range};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specs::{Component, HashMapStorage}; use specs::{Component, DerefFlaggedStorage};
use specs_idvs::IdvStorage; 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<Item>;
const DEFAULT_INVENTORY_SLOTS: usize = 18;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Inventory { pub struct Inventory {
slots: Vec<Option<Item>>, loadout: Loadout,
amount: u32, /// The "built-in" slots belonging to the inventory itself, all other slots
/// are provided by equipped items
slots: Vec<InvSlot>,
} }
/// Errors which the methods on `Inventory` produce /// Errors which the methods on `Inventory` produce
@ -22,24 +46,55 @@ pub enum Error {
Full(Vec<Item>), Full(Vec<Item>),
} }
#[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 { 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 { Inventory {
slots: vec![None; 36], loadout,
amount: 0, slots: vec![None; DEFAULT_INVENTORY_SLOTS],
} }
} }
pub fn slots(&self) -> &[Option<Item>] { &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<Item = &InvSlot> {
self.slots
.iter()
.chain(self.loadout.inv_slots_with_id().map(|(_, slot)| slot))
}
/// Total number of occupied slots in the inventory. /// A mutable iterator of all inventory slots
pub fn amount(&self) -> u32 { self.amount } fn slots_mut(&mut self) -> impl Iterator<Item = &mut InvSlot> {
self.slots.iter_mut().chain(self.loadout.inv_slots_mut())
}
pub fn recount_items(&mut self) { /// An iterator of all inventory slots and their position
self.amount = self.slots.iter().filter(|i| i.is_some()).count() as u32; pub fn slots_with_id(&self) -> impl Iterator<Item = (InvSlotId, &InvSlot)> {
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 /// 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<Item> { pub fn push(&mut self, item: Item) -> Option<Item> {
if item.is_stackable() { if item.is_stackable() {
if let Some(slot_item) = self if let Some(slot_item) = self
.slots .slots_mut()
.iter_mut()
.filter_map(Option::as_mut) .filter_map(Option::as_mut)
.find(|s| *s == &item) .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 // No existing item to stack with or item not stackable, put the item in a new
// slot // slot
self.add_to_first_empty(item) self.insert(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<Item> {
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
} }
/// Add a series of items to inventory, returning any which do not fit as an /// 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 /// 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. /// item or the same item again if that slot was not found.
pub fn insert(&mut self, cell: usize, item: Item) -> Result<Option<Item>, Item> { pub fn insert_at(&mut self, inv_slot_id: InvSlotId, item: Item) -> Result<Option<Item>, Item> {
match self.slots.get_mut(cell) { match self.slot_mut(inv_slot_id) {
Some(slot) => { Some(slot) => Ok(core::mem::replace(slot, Some(item))),
let old = core::mem::replace(slot, Some(item));
if old.is_none() {
self.recount_items();
}
Ok(old)
},
None => Err(item), None => Err(item),
} }
} }
/// Checks if inserting item exists in given cell. Inserts an item if it /// Checks if inserting item exists in given cell. Inserts an item if it
/// exists. /// exists.
pub fn insert_or_stack(&mut self, cell: usize, item: Item) -> Result<Option<Item>, Item> { pub fn insert_or_stack_at(
&mut self,
inv_slot_id: InvSlotId,
item: Item,
) -> Result<Option<Item>, Item> {
if item.is_stackable() { if item.is_stackable() {
match self.slots.get_mut(cell) { match self.slot_mut(inv_slot_id) {
Some(Some(slot_item)) => { Some(Some(slot_item)) => {
Ok(if slot_item == &item { Ok(if slot_item == &item {
slot_item slot_item
@ -150,46 +188,77 @@ impl Inventory {
Some(old_item) Some(old_item)
}) })
}, },
Some(None) => self.insert(cell, item), Some(None) => self.insert_at(inv_slot_id, item),
None => Err(item), None => Err(item),
} }
} else { } 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 populated_slots(&self) -> usize { self.slots().filter_map(|slot| slot.as_ref()).count() }
pub fn count(&self) -> usize { self.slots.iter().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 { 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 /// Get content of a slot
pub fn get(&self, cell: usize) -> Option<&Item> { pub fn get(&self, inv_slot_id: InvSlotId) -> Option<&Item> {
self.slots.get(cell).and_then(Option::as_ref) 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<Item = (&str, Option<&Item>)> {
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<Range<usize>> {
// 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 /// Swap the items inside of two slots
pub fn swap_slots(&mut self, a: usize, b: usize) { pub fn swap_slots(&mut self, a: InvSlotId, b: InvSlotId) {
if a.max(b) < self.slots.len() { if self.slot(a).is_none() || self.slot(b).is_none() {
self.slots.swap(a, b); 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 /// Remove an item from the slot
pub fn remove(&mut self, cell: usize) -> Option<Item> { pub fn remove(&mut self, inv_slot_id: InvSlotId) -> Option<Item> {
let item = self.slots.get_mut(cell).and_then(|item| item.take()); self.slot_mut(inv_slot_id).and_then(|item| item.take())
self.recount_items();
item
} }
/// Remove just one item from the slot /// Remove just one item from the slot
pub fn take(&mut self, cell: usize) -> Option<Item> { pub fn take(&mut self, inv_slot_id: InvSlotId) -> Option<Item> {
if let Some(Some(item)) = self.slots.get_mut(cell) { if let Some(Some(item)) = self.slot_mut(inv_slot_id) {
let mut return_item = item.duplicate(); let mut return_item = item.duplicate();
if item.is_stackable() && item.amount() > 1 { if item.is_stackable() && item.amount() > 1 {
@ -197,20 +266,25 @@ impl Inventory {
return_item return_item
.set_amount(1) .set_amount(1)
.expect("Items duplicated from a stackable item must be stackable."); .expect("Items duplicated from a stackable item must be stackable.");
self.recount_items();
Some(return_item) Some(return_item)
} else { } else {
self.remove(cell) self.remove(inv_slot_id)
} }
} else { } else {
None None
} }
} }
/// Takes all items from the inventory
pub fn drain(&mut self) -> impl Iterator<Item = Item> + '_ {
self.slots_mut()
.filter(|x| x.is_some())
.filter_map(mem::take)
}
/// Determine how many of a particular item there is in the inventory. /// Determine how many of a particular item there is in the inventory.
pub fn item_count(&self, item_def: &ItemDef) -> u64 { pub fn item_count(&self, item_def: &ItemDef) -> u64 {
self.slots() self.slots()
.iter()
.flatten() .flatten()
.filter(|it| it.is_same_item_def(item_def)) .filter(|it| it.is_same_item_def(item_def))
.map(|it| u64::from(it.amount())) .map(|it| u64::from(it.amount()))
@ -225,17 +299,18 @@ impl Inventory {
pub fn contains_ingredients<'a>( pub fn contains_ingredients<'a>(
&self, &self,
recipe: &'a Recipe, recipe: &'a Recipe,
) -> Result<Vec<u32>, Vec<(&'a ItemDef, u32)>> { ) -> Result<HashMap<InvSlotId, u32>, Vec<(&'a ItemDef, u32)>> {
let mut slot_claims = vec![0; self.slots.len()]; let mut slot_claims = HashMap::<InvSlotId, u32>::new();
let mut missing = Vec::<(&ItemDef, u32)>::new(); let mut missing = Vec::<(&ItemDef, u32)>::new();
for (input, mut needed) in recipe.inputs() { for (input, mut needed) in recipe.inputs() {
let mut contains_any = false; 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)) { 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); let claim = slot_claims.entry(inv_slot_id).or_insert(0);
slot_claims[i] += can_claim; let can_claim = (item.amount() - *claim).min(needed);
*claim += can_claim;
needed -= can_claim; needed -= can_claim;
contains_any = true; contains_any = true;
} }
@ -252,24 +327,312 @@ impl Inventory {
Err(missing) Err(missing)
} }
} }
}
impl Default for Inventory { /// Adds a new item to the first empty slot of the inventory. Returns the
fn default() -> Inventory { /// item again if no free slot was found.
let mut inventory = Inventory { fn insert(&mut self, item: Item) -> Option<Item> {
slots: vec![None; 36], match self.slots_mut().find(|slot| slot.is_none()) {
amount: 0, Some(slot) => {
}; *slot = Some(item);
inventory.push(Item::new_from_asset_expect( None
"common.items.consumable.potion_minor", },
)); None => Some(item),
inventory.push(Item::new_from_asset_expect("common.items.food.cheese")); }
inventory }
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<usize> 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<Item = &Item> { 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<Item>,
) -> Option<Item> {
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<Vec<Item>> {
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::<Vec<Item>>()
.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<Option<Vec<Item>>, 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<Item> = 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<Vec<Item>> {
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<Vec<Item>> {
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<Item> = 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 { impl Component for Inventory {
type Storage = HashMapStorage<Self>; type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
} }
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
@ -306,5 +669,3 @@ impl InventoryUpdate {
impl Component for InventoryUpdate { impl Component for InventoryUpdate {
type Storage = IdvStorage<Self>; type Storage = IdvStorage<Self>;
} }
#[cfg(test)] mod test;

View File

@ -1,21 +1,84 @@
use crate::{
comp,
comp::{
item::{self, armor, tool::AbilityMap},
ItemConfig,
},
};
use comp::{Inventory, Loadout};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::warn; use std::{cmp::Ordering, convert::TryFrom};
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] use crate::comp::{
pub enum Slot { inventory::{
Inventory(usize), item::{armor, armor::ArmorKind, ItemKind},
Equip(EquipSlot), loadout::LoadoutSlotId,
},
item,
};
#[derive(Debug, PartialEq)]
pub enum SlotError {
InventoryFull,
} }
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] #[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<LoadoutSlotId> 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<Ordering> { 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<InvSlotId> 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 { pub enum EquipSlot {
Armor(ArmorSlot), Armor(ArmorSlot),
Mainhand, Mainhand,
@ -24,25 +87,26 @@ pub enum EquipSlot {
Glider, Glider,
} }
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, Serialize, Deserialize)]
pub enum ArmorSlot { pub enum ArmorSlot {
Head, Head,
Neck, Neck,
Shoulders, Shoulders,
Chest, Chest,
Hands, Hands,
Ring, Ring1,
Ring2,
Back, Back,
Belt, Belt,
Legs, Legs,
Feet, Feet,
Tabard, Tabard,
Bag1,
Bag2,
Bag3,
Bag4,
} }
//const ALL_ARMOR_SLOTS: [ArmorSlot; 11] = [
// Head, Neck, Shoulders, Chest, Hands, Ring, Back, Belt, Legs, Feet, Tabard,
//];
impl Slot { impl Slot {
pub fn can_hold(self, item_kind: &item::ItemKind) -> bool { pub fn can_hold(self, item_kind: &item::ItemKind) -> bool {
match (self, item_kind) { match (self, item_kind) {
@ -53,11 +117,9 @@ impl Slot {
} }
impl EquipSlot { impl EquipSlot {
fn can_hold(self, item_kind: &item::ItemKind) -> bool { pub fn can_hold(self, item_kind: &item::ItemKind) -> bool {
use armor::Armor;
use item::ItemKind;
match (self, item_kind) { 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::Mainhand, ItemKind::Tool(_)) => true,
(Self::Offhand, ItemKind::Tool(_)) => true, (Self::Offhand, ItemKind::Tool(_)) => true,
(Self::Lantern, ItemKind::Lantern(_)) => true, (Self::Lantern, ItemKind::Lantern(_)) => true,
@ -69,7 +131,6 @@ impl EquipSlot {
impl ArmorSlot { impl ArmorSlot {
fn can_hold(self, armor: &item::armor::ArmorKind) -> bool { fn can_hold(self, armor: &item::armor::ArmorKind) -> bool {
use item::armor::ArmorKind;
matches!( matches!(
(self, armor), (self, armor),
(Self::Head, ArmorKind::Head(_)) (Self::Head, ArmorKind::Head(_))
@ -77,414 +138,17 @@ impl ArmorSlot {
| (Self::Shoulders, ArmorKind::Shoulder(_)) | (Self::Shoulders, ArmorKind::Shoulder(_))
| (Self::Chest, ArmorKind::Chest(_)) | (Self::Chest, ArmorKind::Chest(_))
| (Self::Hands, ArmorKind::Hand(_)) | (Self::Hands, ArmorKind::Hand(_))
| (Self::Ring, ArmorKind::Ring(_)) | (Self::Ring1, ArmorKind::Ring(_))
| (Self::Ring2, ArmorKind::Ring(_))
| (Self::Back, ArmorKind::Back(_)) | (Self::Back, ArmorKind::Back(_))
| (Self::Belt, ArmorKind::Belt(_)) | (Self::Belt, ArmorKind::Belt(_))
| (Self::Legs, ArmorKind::Pants(_)) | (Self::Legs, ArmorKind::Pants(_))
| (Self::Feet, ArmorKind::Foot(_)) | (Self::Feet, ArmorKind::Foot(_))
| (Self::Tabard, ArmorKind::Tabard(_)) | (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<item::Item>,
loadout: &mut Loadout,
map: &AbilityMap,
) -> Option<item::Item> {
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<item::Item> {
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<item::Item> {
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<comp::Item> = Some(Item::new_from_asset_expect(
"common.items.testing.test_boots",
));
let starting_sandles: Option<comp::Item> = 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<comp::Item> = Some(Item::new_from_asset_expect(
"common.items.testing.test_boots",
));
let starting_sandles: Option<comp::Item> = 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);
}
}

View File

@ -1,4 +1,8 @@
use super::*; use super::*;
use crate::comp::{
inventory::{slot::ArmorSlot, test_helpers::get_test_bag},
Item,
};
use lazy_static::lazy_static; use lazy_static::lazy_static;
lazy_static! { lazy_static! {
static ref TEST_ITEMS: Vec<Item> = vec![ static ref TEST_ITEMS: Vec<Item> = vec![
@ -12,7 +16,7 @@ lazy_static! {
fn push_full() { fn push_full() {
let mut inv = Inventory { let mut inv = Inventory {
slots: TEST_ITEMS.iter().map(|a| Some(a.clone())).collect(), slots: TEST_ITEMS.iter().map(|a| Some(a.clone())).collect(),
amount: 0, loadout: LoadoutBuilder::new().build(),
}; };
assert_eq!( assert_eq!(
inv.push(TEST_ITEMS[0].clone()).unwrap(), inv.push(TEST_ITEMS[0].clone()).unwrap(),
@ -25,7 +29,7 @@ fn push_full() {
fn push_all_full() { fn push_all_full() {
let mut inv = Inventory { let mut inv = Inventory {
slots: TEST_ITEMS.iter().map(|a| Some(a.clone())).collect(), slots: TEST_ITEMS.iter().map(|a| Some(a.clone())).collect(),
amount: 0, loadout: LoadoutBuilder::new().build(),
}; };
let Error::Full(leftovers) = inv let Error::Full(leftovers) = inv
.push_all(TEST_ITEMS.iter().cloned()) .push_all(TEST_ITEMS.iter().cloned())
@ -39,7 +43,7 @@ fn push_all_full() {
fn push_unique_all_full() { fn push_unique_all_full() {
let mut inv = Inventory { let mut inv = Inventory {
slots: TEST_ITEMS.iter().map(|a| Some(a.clone())).collect(), slots: TEST_ITEMS.iter().map(|a| Some(a.clone())).collect(),
amount: 0, loadout: LoadoutBuilder::new().build(),
}; };
inv.push_all_unique(TEST_ITEMS.iter().cloned()) inv.push_all_unique(TEST_ITEMS.iter().cloned())
.expect("Pushing unique items into an inventory that already contains them didn't work!"); .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() { fn push_all_empty() {
let mut inv = Inventory { let mut inv = Inventory {
slots: vec![None, None], slots: vec![None, None],
amount: 0, loadout: LoadoutBuilder::new().build(),
}; };
inv.push_all(TEST_ITEMS.iter().cloned()) inv.push_all(TEST_ITEMS.iter().cloned())
.expect("Pushing items into an empty inventory didn't work!"); .expect("Pushing items into an empty inventory didn't work!");
@ -63,9 +67,324 @@ fn push_all_empty() {
fn push_all_unique_empty() { fn push_all_unique_empty() {
let mut inv = Inventory { let mut inv = Inventory {
slots: vec![None, None], slots: vec![None, None],
amount: 0, loadout: LoadoutBuilder::new().build(),
}; };
inv.push_all_unique(TEST_ITEMS.iter().cloned()).expect( inv.push_all_unique(TEST_ITEMS.iter().cloned()).expect(
"Pushing unique items into an empty inventory that didn't contain them didn't work!", "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());
}
}

View File

@ -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))
}

View File

@ -13,7 +13,7 @@ pub mod group;
mod health; mod health;
pub mod home_chunk; pub mod home_chunk;
mod inputs; mod inputs;
mod inventory; pub mod inventory;
mod last; mod last;
mod location; mod location;
mod misc; mod misc;
@ -26,7 +26,7 @@ mod stats;
pub mod visual; pub mod visual;
// Reexports // Reexports
pub use ability::{CharacterAbility, CharacterAbilityType, ItemConfig, Loadout}; pub use ability::{CharacterAbility, CharacterAbilityType};
pub use admin::Admin; pub use admin::Admin;
pub use agent::{Agent, Alignment}; pub use agent::{Agent, Alignment};
pub use aura::{Aura, AuraChange, AuraKind, Auras}; pub use aura::{Aura, AuraChange, AuraKind, Auras};
@ -54,7 +54,7 @@ pub use home_chunk::HomeChunk;
pub use inputs::CanBuild; pub use inputs::CanBuild;
pub use inventory::{ pub use inventory::{
item, item,
item::{Item, ItemDrop}, item::{Item, ItemConfig, ItemDrop},
slot, Inventory, InventoryUpdate, InventoryUpdateEvent, slot, Inventory, InventoryUpdate, InventoryUpdateEvent,
}; };
pub use last::Last; pub use last::Last;

View File

@ -88,7 +88,6 @@ pub enum ServerEvent {
comp::Body, comp::Body,
comp::Stats, comp::Stats,
comp::Inventory, comp::Inventory,
comp::Loadout,
Option<comp::Waypoint>, Option<comp::Waypoint>,
), ),
}, },
@ -100,7 +99,7 @@ pub enum ServerEvent {
pos: comp::Pos, pos: comp::Pos,
stats: comp::Stats, stats: comp::Stats,
health: comp::Health, health: comp::Health,
loadout: comp::Loadout, loadout: comp::inventory::loadout::Loadout,
body: comp::Body, body: comp::Body,
agent: Option<comp::Agent>, agent: Option<comp::Agent>,
alignment: comp::Alignment, alignment: comp::Alignment,

Some files were not shown because too many files have changed in this diff Show More