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]
generate = "run --package tools --"
test-server = "-Zpackage-features run --bin veloren-server-cli --no-default-features"
test-server = "-Zpackage-features run --bin veloren-server-cli --no-default-features -- -b"
tracy-server = "-Zunstable-options -Zpackage-features run --bin veloren-server-cli --no-default-features --features tracy,simd --profile no_overflow"
test-voxygen = "-Zpackage-features run --bin veloren-voxygen --no-default-features --features gl,simd"
tracy-voxygen = "-Zunstable-options -Zpackage-features run --bin veloren-voxygen --no-default-features --features tracy,gl,simd --profile no_overflow"

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
- Flying birds travel the world
- Plugin system now based on Wasmer 1.0.0
- Added 4x Bag loadout slots, used for upgrading inventory space
- Added an additional Ring loadout slot
- The inventory can now be expanded to fill the whole window
- Added /dropall admin command (drops all inventory items on the ground)
### Changed
@ -26,10 +30,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Glider can now be deployed even when not on the ground
- Gliding now has an energy cost for strenuous maneuvers based on lift
- Translations are now folders with multiple files instead of a huge single file
- Default inventory slots reduced to 18 - existing characters given 3x 6-slot bags as compensation
- Protection rating was moved to the top left of the loadout view
### Removed
- SSAAx4 option
- The Stats button and associated screen were removed
### Fixed

2
Cargo.lock generated
View File

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

View File

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

View File

@ -9,4 +9,5 @@ ItemDef(
)
),
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(
name: "Red Flower",
description: "Roses are red...",
description: "Can be used as a dying ingredient.",
kind: Ingredient(
kind: "Flower",
kind: "FlowerRed",
),
quality: Common,
)

View File

@ -1,6 +1,6 @@
ItemDef(
name: "Coconut",
description: "Restores 20 health over 10 seconds\n\nReliable source of water and fat",
description: "Restores 20 health over 10 seconds\n\nReliable source of water and fat.\n\nNaturally growing at the top of palm trees.",
kind: Consumable(
kind: "Coconut",
effect: [

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,
stats: (
equip_time_millis: 0,
power: 1.80
power: 1.80,
speed: 1.0
),
)
),

View File

@ -6,7 +6,8 @@ ItemDef(
kind: Dagger,
stats: (
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.hammer.cultist_purp_2h-0"),
(1, "common.items.weapons.sword.cultist_purp_2h-0"),
(0.25, "common.items.weapons.crafting_ing.mindflayer_bag_damaged"),
// misc
(1, "common.items.boss_drops.lantern"),
(0.1, "common.items.glider.glider_purp"),

View File

@ -1,7 +1,7 @@
[
// Misc
(0.25, "common.items.armor.neck.neck_1"),
(0.2, "common.items.crafting_ing.cloth_scraps"),
(0.5, "common.items.crafting_ing.cloth_scraps"),
(1.0, "common.items.crafting_ing.empty_vial"),
(0.1, "common.items.glider.glider_blue"),
(0.1, "common.items.glider.glider_morpho"),
@ -41,7 +41,7 @@
// staves
(1.00, "common.items.weapons.staff.bone_staff"),
(1.00, "common.items.weapons.staff.amethyst_staff"),
(0.1, "common.items.weapons.sceptre.sceptre_velorite_0"),
(0.05, "common.items.weapons.sceptre.sceptre_velorite_0"),
// hammers
(0.30, "common.items.weapons.hammer.cobalt_hammer-0"),
(0.30, "common.items.weapons.hammer.cobalt_hammer-1"),
@ -56,7 +56,7 @@
(0.05, "common.items.weapons.hammer.steel_hammer-4"),
(0.05, "common.items.weapons.hammer.steel_hammer-5"),
// bows
(0.1, "common.items.weapons.bow.nature_ore_longbow-0"),
(0.05, "common.items.weapons.bow.nature_ore_longbow-0"),
]

View File

@ -1,7 +1,7 @@
[
// crafting ingredients
(2, "common.items.crafting_ing.leather_scraps"),
(2, "common.items.crafting_ing.cloth_scraps"),
(4, "common.items.crafting_ing.cloth_scraps"),
(1, "common.items.crafting_ing.empty_vial"),
(0.10, "common.items.crafting_ing.shiny_gem"),

View File

@ -5,7 +5,7 @@
(3, "common.items.food.apple"),
(3, "common.items.food.mushroom"),
(3, "common.items.food.coconut"),
(3, "common.items.crafting_ing.cloth_scraps"),
(5, "common.items.crafting_ing.cloth_scraps"),
// crafted
(0.5, "common.items.food.apple_mushroom_curry"),
(0.5, "common.items.food.apple_stick"),
@ -65,6 +65,9 @@
//gloves
(0.50, "common.items.armor.hand.leather_0"),
(0.50, "common.items.armor.hand.leather_2"),
//backpack
(0.001, "common.items.armor.back.backpack_0"),
(0.1, "common.items.armor.bag.heavy_seabag"),
// Common Weapons
// swords
(0.4, "common.items.weapons.sword.wood_sword"),

View File

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

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.15, "common.items.weapons.hammer.runic_hammer"),
(0.15, "common.items.weapons.hammer.ramshead_hammer"),
(0.10, "common.items.weapons.hammer.mjolnir"),
(0.01, "common.items.weapons.hammer.mjolnir"),
// bows
(0.60, "common.items.weapons.bow.horn_longbow-0"),
(0.30, "common.items.weapons.bow.iron_longbow-0"),
(0.10, "common.items.weapons.bow.rare_longbow"),
(0.05, "common.items.weapons.bow.rare_longbow"),
]

View File

@ -1,42 +1,321 @@
{
// Tools
"crafting_hammer": (("common.items.crafting_tools.craftsman_hammer", 1),[("common.items.crafting_ing.twigs", 6), ("common.items.crafting_ing.stones", 6)]),
"mortar_pestle": (("common.items.crafting_tools.mortar_pestle", 1), [("common.items.crafting_ing.stones", 6), ("common.items.food.coconut", 2), ("common.items.crafting_tools.craftsman_hammer", 0)]),
"sewing_set": (("common.items.crafting_tools.sewing_set", 1),[("common.items.crafting_ing.leather_scraps", 2), ("common.items.crafting_ing.twigs", 4), ("common.items.crafting_ing.stones", 2), ("common.items.crafting_ing.shiny_gem", 1)]),
// Ore and more
"velorite_frag": (("common.items.ore.veloritefrag", 2), [("common.items.ore.velorite", 1), ("common.items.crafting_tools.craftsman_hammer", 0)]),
//Potions
"potion_s": (("common.items.consumable.potion_minor", 1), [("common.items.crafting_ing.empty_vial", 1), ("common.items.food.apple", 4), ("common.items.crafting_ing.honey", 1)]),
"potion_m": (("common.items.consumable.potion_med", 1), [("common.items.consumable.potion_minor", 2), ("common.items.ore.veloritefrag", 4)]),
"collar_basic": (("common.items.utility.collar", 1), [("common.items.crafting_ing.leather_scraps", 5), ("common.items.crafting_ing.shiny_gem", 1)]),
"bomb_coconut": (("common.items.utility.bomb", 1), [("common.items.crafting_ing.stones", 10), ("common.items.food.coconut", 2), ("common.items.ore.veloritefrag", 2), ("common.items.crafting_tools.mortar_pestle", 0)]),
// Firework
"firework_blue": (("common.items.utility.firework_blue", 1), [("common.items.crafting_ing.twigs", 1), ("common.items.crafting_ing.stones", 1), ("common.items.food.coconut", 1), ("common.items.ore.veloritefrag", 1), ("common.items.crafting_tools.mortar_pestle", 0)]),
"firework_green": (("common.items.utility.firework_green", 1), [("common.items.crafting_ing.twigs", 1), ("common.items.crafting_ing.stones", 1), ("common.items.food.coconut", 1), ("common.items.ore.veloritefrag", 1), ("common.items.crafting_tools.mortar_pestle", 0)]),
"firework_purple": (("common.items.utility.firework_purple", 1), [("common.items.crafting_ing.twigs", 1), ("common.items.crafting_ing.stones", 1), ("common.items.food.coconut", 1), ("common.items.ore.veloritefrag", 1), ("common.items.crafting_tools.mortar_pestle", 0)]),
"firework_red": (("common.items.utility.firework_red", 1), [("common.items.crafting_ing.twigs", 1), ("common.items.crafting_ing.stones", 1), ("common.items.food.coconut", 1), ("common.items.ore.veloritefrag", 1), ("common.items.crafting_tools.mortar_pestle", 0)]),
"firework_yellow": (("common.items.utility.firework_yellow", 1), [("common.items.crafting_ing.twigs", 1), ("common.items.crafting_ing.stones", 1), ("common.items.food.coconut", 1), ("common.items.ore.veloritefrag", 1), ("common.items.crafting_tools.mortar_pestle", 0)]),
// Food
"apple_shroom_curry": (("common.items.food.apple_mushroom_curry", 1), [("common.items.food.mushroom", 8), ("common.items.food.coconut", 1), ("common.items.food.apple", 4), ("common.items.crafting_tools.mortar_pestle", 0)]),
"apples_stick": (("common.items.food.apple_stick", 1),[("common.items.crafting_ing.twigs", 2), ("common.items.food.apple", 2)]),
"mushroom_stick": (("common.items.food.mushroom_stick", 1),[("common.items.crafting_ing.twigs", 2), ("common.items.food.mushroom", 3)]),
"sunflower_icetea": (("common.items.food.sunflower_icetea", 4),[("common.items.crafting_ing.empty_vial", 1), ("common.items.crafting_ing.icy_fang", 1),("common.items.flowers.sunflower", 4), ("common.items.crafting_ing.honey", 1)]),
// Gliders
"Leaves Glider": (("common.items.glider.glider_leaves", 1),[("common.items.crafting_ing.twigs", 5), ("common.items.crafting_ing.leather_scraps", 5), ("common.items.crafting_ing.cloth_scraps", 5), ("common.items.crafting_ing.shiny_gem", 1), ("common.items.crafting_tools.craftsman_hammer", 0),("common.items.crafting_tools.sewing_set", 0)]),
"Sand Raptor Wings": (("common.items.glider.glider_sandraptor", 1),[("common.items.crafting_ing.raptor_feather", 6), ("common.items.crafting_ing.twigs", 5), ("common.items.crafting_ing.leather_scraps", 5), ("common.items.crafting_ing.cloth_scraps", 5), ("common.items.crafting_ing.shiny_gem", 1), ("common.items.crafting_tools.craftsman_hammer", 0),("common.items.crafting_tools.sewing_set", 0)]),
"Snow Raptor Wings": (("common.items.glider.glider_snowraptor", 1),[("common.items.crafting_ing.raptor_feather", 6), ("common.items.crafting_ing.twigs", 5), ("common.items.crafting_ing.leather_scraps", 5), ("common.items.crafting_ing.cloth_scraps", 5), ("common.items.crafting_ing.icy_fang", 1), ("common.items.crafting_ing.shiny_gem", 1), ("common.items.crafting_tools.craftsman_hammer", 0),("common.items.crafting_tools.sewing_set", 0)]),
"Wood Raptor Wings": (("common.items.glider.glider_woodraptor", 1),[("common.items.crafting_ing.raptor_feather", 6), ("common.items.crafting_ing.twigs", 15), ("common.items.crafting_ing.leather_scraps", 5), ("common.items.crafting_ing.cloth_scraps", 5), ("common.items.crafting_ing.shiny_gem", 1), ("common.items.crafting_tools.craftsman_hammer", 0),("common.items.crafting_tools.sewing_set", 0)]),
// Weapons
"velorite_sceptre": (("common.items.weapons.sceptre.sceptre_velorite_0", 1),[("common.items.crafting_ing.twigs", 20), ("common.items.ore.veloritefrag", 10), ("common.items.crafting_ing.shiny_gem", 4), ("common.items.crafting_tools.craftsman_hammer", 0)]),
// Enhanced starting weapons
"better bow": (("common.items.weapons.bow.wood_shortbow-0", 1), [("common.items.crafting_ing.leather_scraps", 8),("common.items.crafting_ing.twigs", 6), ("common.items.crafting_ing.stones", 0)]),
"better sword": (("common.items.weapons.sword.wood_sword", 1), [("common.items.crafting_ing.leather_scraps", 4),("common.items.crafting_ing.twigs", 10), ("common.items.ore.veloritefrag", 1), ("common.items.crafting_ing.stones", 0)]),
// Adventurer/Beginner Leather Set
"adventure back": (("common.items.armor.back.leather_adventurer", 1),[("common.items.crafting_ing.leather_scraps", 4)]),
"adventure belt": (("common.items.armor.belt.leather_adventurer", 1),[("common.items.crafting_ing.leather_scraps", 2)]),
"adventure chest": (("common.items.armor.chest.leather_adventurer", 1),[("common.items.crafting_ing.leather_scraps", 12)]),
"adventure feet": (("common.items.armor.foot.leather_adventurer", 1),[("common.items.crafting_ing.leather_scraps", 3)]),
"adventure hands": (("common.items.armor.hand.leather_adventurer", 1),[("common.items.crafting_ing.leather_scraps", 4)]),
"adventure pants": (("common.items.armor.pants.leather_adventurer", 1),[("common.items.crafting_ing.leather_scraps", 8)]),
"adventure shoulder": (("common.items.armor.shoulder.leather_adventurer", 1),[("common.items.crafting_ing.leather_scraps", 12)]),
"crafting_hammer": (
("common.items.crafting_tools.craftsman_hammer", 1),
[
("common.items.crafting_ing.twigs", 6),
("common.items.crafting_ing.stones", 6),
],
),
"mortar_pestle": (
("common.items.crafting_tools.mortar_pestle", 1),
[
("common.items.crafting_ing.stones", 6),
("common.items.food.coconut", 1),
("common.items.crafting_tools.craftsman_hammer", 0),
],
),
"sewing_set": (
("common.items.crafting_tools.sewing_set", 1),
[
("common.items.crafting_ing.leather_scraps", 2),
("common.items.crafting_ing.twigs", 4),
("common.items.crafting_ing.stones", 2),
],
),
"velorite_frag": (
("common.items.ore.veloritefrag", 2),
[
("common.items.ore.velorite", 1),
("common.items.crafting_tools.craftsman_hammer", 0),
],
),
"potion_s": (
("common.items.consumable.potion_minor", 1),
[
("common.items.crafting_ing.empty_vial", 1),
("common.items.food.apple", 4),
("common.items.crafting_ing.honey", 1),
],
),
"potion_m": (
("common.items.consumable.potion_med", 1),
[
("common.items.consumable.potion_minor", 2),
("common.items.ore.veloritefrag", 4),
],
),
"collar_basic": (
("common.items.utility.collar", 1),
[
("common.items.crafting_ing.leather_scraps", 5),
("common.items.crafting_ing.shiny_gem", 1),
],
),
"bomb_coconut": (
("common.items.utility.bomb", 1),
[
("common.items.crafting_ing.stones", 10),
("common.items.food.coconut", 2),
("common.items.ore.veloritefrag", 2),
("common.items.crafting_tools.mortar_pestle", 0),
],
),
"firework_blue": (
("common.items.utility.firework_blue", 1),
[
("common.items.crafting_ing.twigs", 1),
("common.items.crafting_ing.stones", 1),
("common.items.food.coconut", 1),
("common.items.ore.veloritefrag", 1),
("common.items.crafting_tools.mortar_pestle", 0),
],
),
"firework_green": (
("common.items.utility.firework_green", 1),
[
("common.items.crafting_ing.twigs", 1),
("common.items.crafting_ing.stones", 1),
("common.items.food.coconut", 1),
("common.items.ore.veloritefrag", 1),
("common.items.crafting_tools.mortar_pestle", 0),
],
),
"firework_purple": (
("common.items.utility.firework_purple", 1),
[
("common.items.crafting_ing.twigs", 1),
("common.items.crafting_ing.stones", 1),
("common.items.food.coconut", 1),
("common.items.ore.veloritefrag", 1),
("common.items.crafting_tools.mortar_pestle", 0),
],
),
"firework_red": (
("common.items.utility.firework_red", 1),
[
("common.items.crafting_ing.twigs", 1),
("common.items.crafting_ing.stones", 1),
("common.items.food.coconut", 1),
("common.items.ore.veloritefrag", 1),
("common.items.crafting_tools.mortar_pestle", 0),
],
),
"firework_yellow": (
("common.items.utility.firework_yellow", 1),
[
("common.items.crafting_ing.twigs", 1),
("common.items.crafting_ing.stones", 1),
("common.items.food.coconut", 1),
("common.items.ore.veloritefrag", 1),
("common.items.crafting_tools.mortar_pestle", 0),
],
),
"apple_shroom_curry": (
("common.items.food.apple_mushroom_curry", 1),
[
("common.items.food.mushroom", 8),
("common.items.food.coconut", 1),
("common.items.food.apple", 4),
("common.items.crafting_tools.mortar_pestle", 0),
],
),
"apples_stick": (
("common.items.food.apple_stick", 1),
[("common.items.crafting_ing.twigs", 2), ("common.items.food.apple", 2)],
),
"mushroom_stick": (
("common.items.food.mushroom_stick", 1),
[
("common.items.crafting_ing.twigs", 2),
("common.items.food.mushroom", 3),
],
),
"sunflower_icetea": (
("common.items.food.sunflower_icetea", 4),
[
("common.items.crafting_ing.empty_vial", 1),
("common.items.crafting_ing.icy_fang", 1),
("common.items.flowers.sunflower", 4),
("common.items.crafting_ing.honey", 1),
],
),
"Leaves Glider": (
("common.items.glider.glider_leaves", 1),
[
("common.items.crafting_ing.twigs", 5),
("common.items.crafting_ing.leather_scraps", 5),
("common.items.crafting_ing.cloth_scraps", 5),
("common.items.crafting_ing.shiny_gem", 1),
("common.items.crafting_tools.craftsman_hammer", 0),
("common.items.crafting_tools.sewing_set", 0),
],
),
"Sand Raptor Wings": (
("common.items.glider.glider_sandraptor", 1),
[
("common.items.crafting_ing.raptor_feather", 6),
("common.items.crafting_ing.twigs", 5),
("common.items.crafting_ing.leather_scraps", 5),
("common.items.crafting_ing.cloth_scraps", 5),
("common.items.crafting_ing.shiny_gem", 1),
("common.items.crafting_tools.craftsman_hammer", 0),
("common.items.crafting_tools.sewing_set", 0),
],
),
"Snow Raptor Wings": (
("common.items.glider.glider_snowraptor", 1),
[
("common.items.crafting_ing.raptor_feather", 6),
("common.items.crafting_ing.twigs", 5),
("common.items.crafting_ing.leather_scraps", 5),
("common.items.crafting_ing.cloth_scraps", 5),
("common.items.crafting_ing.icy_fang", 1),
("common.items.crafting_ing.shiny_gem", 1),
("common.items.crafting_tools.craftsman_hammer", 0),
("common.items.crafting_tools.sewing_set", 0),
],
),
"Wood Raptor Wings": (
("common.items.glider.glider_woodraptor", 1),
[
("common.items.crafting_ing.raptor_feather", 6),
("common.items.crafting_ing.twigs", 15),
("common.items.crafting_ing.leather_scraps", 5),
("common.items.crafting_ing.cloth_scraps", 5),
("common.items.crafting_ing.shiny_gem", 1),
("common.items.crafting_tools.craftsman_hammer", 0),
("common.items.crafting_tools.sewing_set", 0),
],
),
"velorite_sceptre": (
("common.items.weapons.sceptre.sceptre_velorite_0", 1),
[
("common.items.crafting_ing.twigs", 20),
("common.items.ore.veloritefrag", 10),
("common.items.crafting_ing.shiny_gem", 4),
("common.items.crafting_tools.craftsman_hammer", 0),
],
),
"better bow": (
("common.items.weapons.bow.wood_shortbow-0", 1),
[
("common.items.crafting_ing.leather_scraps", 8),
("common.items.crafting_ing.twigs", 6),
("common.items.crafting_ing.stones", 0),
],
),
"better sword": (
("common.items.weapons.sword.wood_sword", 1),
[
("common.items.crafting_ing.leather_scraps", 4),
("common.items.crafting_ing.twigs", 10),
("common.items.ore.veloritefrag", 1),
("common.items.crafting_ing.stones", 0),
],
),
"adventure back": (
("common.items.armor.back.leather_adventurer", 1),
[("common.items.crafting_ing.leather_scraps", 4)],
),
"adventure belt": (
("common.items.armor.belt.leather_adventurer", 1),
[("common.items.crafting_ing.leather_scraps", 2)],
),
"adventure chest": (
("common.items.armor.chest.leather_adventurer", 1),
[("common.items.crafting_ing.leather_scraps", 12)],
),
"adventure feet": (
("common.items.armor.foot.leather_adventurer", 1),
[("common.items.crafting_ing.leather_scraps", 3)],
),
"adventure hands": (
("common.items.armor.hand.leather_adventurer", 1),
[("common.items.crafting_ing.leather_scraps", 4)],
),
"adventure pants": (
("common.items.armor.pants.leather_adventurer", 1),
[("common.items.crafting_ing.leather_scraps", 8)],
),
"adventure shoulder": (
("common.items.armor.shoulder.leather_adventurer", 1),
[("common.items.crafting_ing.leather_scraps", 12)],
),
"red cloth": (
("common.items.crafting_ing.cloth_scraps_red", 1),
[
("common.items.crafting_ing.cloth_scraps", 1),
("common.items.flowers.red", 1),
("common.items.crafting_tools.mortar_pestle", 0),
],
),
"tiny red pouch": (
("common.items.armor.bag.tiny_red_pouch", 1),
[
("common.items.crafting_ing.cloth_scraps_red", 3),
("common.items.crafting_tools.sewing_set", 0),
],
),
"tiny leather pouch": (
("common.items.armor.bag.tiny_leather_pouch", 1),
[
("common.items.crafting_ing.leather_scraps", 6),
("common.items.crafting_tools.sewing_set", 0),
],
),
"knitted red pouch": (
("common.items.armor.bag.knitted_red_pouch", 1),
[
("common.items.crafting_ing.cloth_scraps_red", 3),
("common.items.armor.bag.tiny_red_pouch", 2),
("common.items.crafting_tools.sewing_set", 0),
],
),
"woven red bag": (
("common.items.armor.bag.woven_red_bag", 1),
[
("common.items.crafting_ing.cloth_scraps_red", 6),
("common.items.armor.bag.knitted_red_pouch", 1),
("common.items.crafting_tools.sewing_set", 0),
],
),
"traveler backpack": (
("common.items.armor.back.backpack_0", 1),
[
("common.items.crafting_ing.shiny_gem", 2),
("common.items.crafting_ing.twigs", 2),
("common.items.crafting_ing.cloth_scraps", 3),
("common.items.crafting_ing.leather_scraps", 3),
("common.items.armor.bag.tiny_leather_pouch", 2),
("common.items.crafting_tools.sewing_set", 0),
],
),
"sturdy red backpack": (
("common.items.armor.bag.sturdy_red_backpack", 1),
[
("common.items.crafting_ing.shiny_gem", 2),
("common.items.crafting_ing.cloth_scraps_red", 3),
("common.items.armor.bag.woven_red_bag", 1),
("common.items.crafting_tools.sewing_set", 0),
],
),
"troll hide pack": (
("common.items.armor.bag.troll_hide_pack", 1),
[
("common.items.crafting_ing.leather_troll", 10),
("common.items.crafting_ing.leather_scraps", 10),
("common.items.crafting_ing.shiny_gem", 1),
("common.items.crafting_tools.sewing_set", 0),
],
),
"Mindflayer Spellbag": (
("common.items.armor.bag.mindflayer_spellbag", 1),
[
("common.items.crafting_ing.mindflayer_bag_damaged", 1),
("common.items.crafting_ing.leather_scraps", 10),
("common.items.crafting_ing.shiny_gem", 4),
("common.items.ore.veloritefrag", 10),
("common.items.crafting_tools.sewing_set", 0),
],
),
}

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.",
"In order to see your stats click the 'Stats' button in the inventory.",
"Sit near a campfire (with the 'K' key) to rest - receiving a slow heal-over-time.",
"Need more bags or better armor to continue your journey? Press 'C' to open the crafting menu!",
],
"npc.speech.villager_under_attack": [
"Help, I'm under attack!",

View File

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

View File

@ -1324,6 +1324,46 @@
"voxel.armor.head.assa_mask-0",
(0.0, 0.0, 0.0), (-90.0, 180.0, 0.0), 1.0,
),
// Bags
Armor(Bag("RedFace")): Png (
"element.icons.item_bag_red_face",
),
Armor(Bag("BrownFace")): Png (
"element.icons.item_bag_brown_face",
),
Armor(Bag("BlueFace")): Png (
"element.icons.item_bag_blue_face",
),
Armor(Bag("PurpleSkull")): Png (
"element.icons.item_bag_skull",
),
Armor(Bag("GreenLarge")): Png (
"element.icons.item_bag_green_large",
),
Armor(Bag("LeatherLarge")): Png (
"element.icons.item_bag_leather_large",
),
Armor(Bag("GreenMid")): Png (
"element.icons.item_bag_green_mid",
),
Armor(Bag("LeatherSmall")): Png (
"element.icons.item_bag_leather_small",
),
Armor(Bag("RedLarge")): Png (
"element.icons.item_bag_large",
),
Armor(Bag("RedMed")): Png (
"element.icons.item_bag_med",
),
Armor(Bag("RedSmall")): Png (
"element.icons.item_bag_small",
),
Armor(Bag("RedTiny")): Png (
"element.icons.item_bag_tiny",
),
Armor(Bag("BluePouch")): Png (
"element.icons.item_bag_blue",
),
// Consumables
Consumable("Apple"): Png(
"element.icons.item_apple",
@ -1419,6 +1459,10 @@
"voxel.sprite.flowers.sunflower_1",
(-2.0, -0.5, -1.0), (-60.0, 40.0, 20.0), 1.1,
),
Ingredient("FlowerRed"): VoxTrans(
"voxel.sprite.flowers.flower_red-4",
(0.0, 0.5, 0.0), (-70.0, 10.0, 0.0), 0.8,
),
Ingredient("Sunflower"): VoxTrans(
"voxel.sprite.flowers.sunflower_1",
(0.0, -1.0, 0.0), (-50.0, 40.0, 20.0), 0.8,
@ -1434,6 +1478,9 @@
Ingredient("IcyShard"): Png(
"element.icons.item_ice_shard",
),
Ingredient("FlayerBagDamaged"): Png(
"element.icons.item_flayer_soul",
),
Ingredient("RaptorFeather"): Png(
"element.icons.item_raptor_feather",
),
@ -1447,9 +1494,15 @@
Ingredient("LeatherScraps"): Png(
"element.icons.item_leather0",
),
Ingredient("TrollLeather"): Png(
"element.icons.item_leather_green",
),
Ingredient("ClothScraps"): Png(
"element.icons.item_cloth0",
),
Ingredient("ClothScrapsRed"): Png(
"element.icons.item_cloth_red",
),
Ingredient("ShinyGem"): Png(
"element.icons.gem",
),
@ -1479,15 +1532,15 @@
),
Glider("SandRaptor"): VoxTrans(
"voxel.glider.glider_sandraptor",
(6.0, 0.0, 0.0), (-50.0, 30.0, 20.0), 1.1,
(6.0, 0.0, 0.0), (-50.0, 30.0, 20.0), 1.1,
),
Glider("SnowRaptor"): VoxTrans(
"voxel.glider.glider_snowraptor",
(6.0, 0.0, 0.0), (-50.0, 30.0, 20.0), 1.1,
(6.0, 0.0, 0.0), (-50.0, 30.0, 20.0), 1.1,
),
Glider("WoodRaptor"): VoxTrans(
"voxel.glider.glider_woodraptor",
(6.0, 0.0, 0.0), (-50.0, 30.0, 20.0), 1.1,
(6.0, 0.0, 0.0), (-50.0, 30.0, 20.0), 1.1,
),
Glider("Purple0"): VoxTrans(
"voxel.glider.glider_cultists",

View File

@ -933,8 +933,6 @@ impl Client {
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.
pub fn send_chat(&mut self, message: String) {
match validate_chat_msg(&message) {
@ -1455,11 +1453,10 @@ impl Client {
self.presence = None;
self.clean_state();
},
ServerGeneral::InventoryUpdate(mut inventory, event) => {
ServerGeneral::InventoryUpdate(inventory, event) => {
match event {
InventoryUpdateEvent::CollectFailed => {},
_ => {
inventory.recount_items();
// Push the updated inventory component to the client
self.state.write_component(self.entity, inventory);
},

View File

@ -19,6 +19,7 @@ sum_type! {
Energy(comp::Energy),
Health(comp::Health),
LightEmitter(comp::LightEmitter),
Inventory(comp::Inventory),
Item(comp::Item),
Scale(comp::Scale),
Group(comp::Group),
@ -28,7 +29,6 @@ sum_type! {
Collider(comp::Collider),
Gravity(comp::Gravity),
Sticky(comp::Sticky),
Loadout(comp::Loadout),
CharacterState(comp::CharacterState),
Pos(comp::Pos),
Vel(comp::Vel),
@ -51,6 +51,7 @@ sum_type! {
Energy(PhantomData<comp::Energy>),
Health(PhantomData<comp::Health>),
LightEmitter(PhantomData<comp::LightEmitter>),
Inventory(PhantomData<comp::Inventory>),
Item(PhantomData<comp::Item>),
Scale(PhantomData<comp::Scale>),
Group(PhantomData<comp::Group>),
@ -60,7 +61,6 @@ sum_type! {
Collider(PhantomData<comp::Collider>),
Gravity(PhantomData<comp::Gravity>),
Sticky(PhantomData<comp::Sticky>),
Loadout(PhantomData<comp::Loadout>),
CharacterState(PhantomData<comp::CharacterState>),
Pos(PhantomData<comp::Pos>),
Vel(PhantomData<comp::Vel>),
@ -83,6 +83,7 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPacket::Energy(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Health(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::LightEmitter(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Inventory(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Item(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Scale(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Group(comp) => sync::handle_insert(comp, entity, world),
@ -92,7 +93,6 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPacket::Collider(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Gravity(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Sticky(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Loadout(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::CharacterState(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Pos(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Vel(comp) => sync::handle_insert(comp, entity, world),
@ -113,6 +113,7 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPacket::Energy(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Health(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::LightEmitter(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Inventory(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Item(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Scale(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Group(comp) => sync::handle_modify(comp, entity, world),
@ -122,7 +123,6 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPacket::Collider(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Gravity(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Sticky(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Loadout(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::CharacterState(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Pos(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Vel(comp) => sync::handle_modify(comp, entity, world),
@ -145,6 +145,7 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPhantom::LightEmitter(_) => {
sync::handle_remove::<comp::LightEmitter>(entity, world)
},
EcsCompPhantom::Inventory(_) => sync::handle_remove::<comp::Inventory>(entity, world),
EcsCompPhantom::Item(_) => sync::handle_remove::<comp::Item>(entity, world),
EcsCompPhantom::Scale(_) => sync::handle_remove::<comp::Scale>(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::Gravity(_) => sync::handle_remove::<comp::Gravity>(entity, world),
EcsCompPhantom::Sticky(_) => sync::handle_remove::<comp::Sticky>(entity, world),
EcsCompPhantom::Loadout(_) => sync::handle_remove::<comp::Loadout>(entity, world),
EcsCompPhantom::CharacterState(_) => {
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::Head(_) => "Head".to_string(),
ArmorKind::Tabard(_) => "Tabard".to_string(),
ArmorKind::Bag(_) => "Bag".to_string(),
}
}
@ -118,6 +119,7 @@ fn get_armor_kind_kind(kind: &ArmorKind) -> String {
ArmorKind::Neck(x) => x.clone(),
ArmorKind::Head(x) => x.clone(),
ArmorKind::Tabard(x) => x.clone(),
ArmorKind::Bag(x) => x.clone(),
}
}

View File

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

View File

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

View File

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

View File

@ -1,9 +1,8 @@
use crate::{
assets::{self, Asset},
comp::{
item::{armor::Protection, tool::AbilityMap, Item, ItemKind},
projectile::ProjectileConstructor,
Body, CharacterState, EnergySource, Gravity, LightEmitter, StateUpdate,
projectile::ProjectileConstructor, Body, CharacterState, EnergySource, Gravity,
LightEmitter, StateUpdate,
},
states::{
behavior::JoinData,
@ -12,10 +11,7 @@ use crate::{
},
Knockback,
};
use arraygen::Arraygen;
use serde::{Deserialize, Serialize};
use specs::{Component, DerefFlaggedStorage};
use specs_idvs::IdvStorage;
use std::time::Duration;
use vek::Vec3;
@ -304,7 +300,7 @@ impl CharacterAbility {
}
}
fn default_roll() -> CharacterAbility {
pub fn default_roll() -> CharacterAbility {
CharacterAbility::Roll {
energy_cost: 100,
buildup_duration: 100,
@ -499,93 +495,6 @@ impl CharacterAbility {
}
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct ItemConfig {
pub item: Item,
pub ability1: Option<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 {
fn from((ability, key): (&CharacterAbility, AbilityKey)) -> Self {
match ability {
@ -975,7 +884,3 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
}
}
}
impl Component for Loadout {
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
}

View File

@ -13,6 +13,7 @@ pub enum ArmorKind {
Neck(String),
Head(String),
Tabard(String),
Bag(String),
}
impl Armor {
@ -43,4 +44,12 @@ pub struct Armor {
impl Armor {
pub fn get_protection(&self) -> Protection { self.stats.protection }
#[cfg(test)]
pub fn test_armor(kind: ArmorKind, protection: Protection) -> Armor {
Armor {
kind,
stats: Stats { protection },
}
}
}

View File

@ -6,10 +6,15 @@ pub use tool::{AbilitySet, Hands, Tool, ToolKind, UniqueKind};
use crate::{
assets::{self, AssetExt, Error},
comp::{
inventory::{item::tool::AbilityMap, InvSlot},
Body, CharacterAbility,
},
effect::Effect,
lottery::Lottery,
terrain::{Block, SpriteKind},
};
use core::mem;
use crossbeam_utils::atomic::AtomicCell;
use rand::prelude::*;
use serde::{Deserialize, Serialize};
@ -94,6 +99,15 @@ pub enum ItemKind {
},
}
impl ItemKind {
pub fn is_equippable(&self) -> bool {
matches!(
self,
ItemKind::Tool(_) | ItemKind::Armor { .. } | ItemKind::Glider(_) | ItemKind::Lantern(_)
)
}
}
pub type ItemId = AtomicCell<Option<NonZeroU64>>;
/* /// 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.
pub struct CreateDatabaseItemId {
item_id: Arc<ItemId>,
}
pub struct CreateDatabaseItemId {
item_id: Arc<ItemId>,
} */
}*/
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Item {
@ -124,22 +134,54 @@ pub struct Item {
/// amount is hidden because it needs to maintain the invariant that only
/// stackable items can have > 1 amounts.
amount: NonZeroU32,
/// The slots for items that this item has
slots: Vec<InvSlot>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ItemDef {
#[serde(default)]
item_definition_id: String,
pub item_config: Option<ItemConfig>,
pub name: String,
pub description: String,
pub kind: ItemKind,
pub quality: Quality,
#[serde(default)]
pub slots: u16,
}
impl PartialEq for ItemDef {
fn eq(&self, other: &Self) -> bool { self.item_definition_id == other.item_definition_id }
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct ItemConfig {
pub ability1: Option<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 {
pub fn is_stackable(&self) -> bool {
matches!(
@ -150,6 +192,25 @@ impl ItemDef {
| ItemKind::Utility { .. }
)
}
#[cfg(test)]
pub fn new_test(
item_definition_id: String,
item_config: Option<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 {
@ -170,8 +231,19 @@ impl assets::Compound for ItemDef {
description,
kind,
quality,
slots,
} = raw;
let item_config = if let ItemKind::Tool(_) = kind {
let ability_map_handle =
cache.load::<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 \
// instead of .
//
@ -180,10 +252,12 @@ impl assets::Compound for ItemDef {
Ok(ItemDef {
item_definition_id,
item_config,
name,
description,
kind,
quality,
slots,
})
}
}
@ -195,6 +269,8 @@ struct RawItemDef {
description: String,
kind: ItemKind,
quality: Quality,
#[serde(default)]
slots: u16,
}
impl assets::Asset for RawItemDef {
@ -229,11 +305,12 @@ impl Item {
// loadout when no weapon is present
pub fn empty() -> Self { Item::new_from_asset_expect("common.items.weapons.empty.empty") }
pub fn new(inner_item: Arc<ItemDef>) -> Self {
pub fn new_from_item_def(inner_item: Arc<ItemDef>) -> Self {
Item {
item_id: Arc::new(AtomicCell::new(None)),
item_def: inner_item,
amount: NonZeroU32::new(1).unwrap(),
slots: vec![None; inner_item.slots as usize],
item_def: inner_item,
}
}
@ -241,7 +318,7 @@ impl Item {
/// Panics if the asset does not exist.
pub fn new_from_asset_expect(asset_specifier: &str) -> Self {
let inner_item = Arc::<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
@ -254,11 +331,43 @@ impl Item {
/// it exists
pub fn new_from_asset(asset: &str) -> Result<Self, Error> {
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
pub fn duplicate(&self) -> Self { Item::new(Arc::clone(&self.item_def)) }
pub fn duplicate(&self) -> Self { Item::new_from_item_def(Arc::clone(&self.item_def)) }
/// FIXME: HACK: In order to set the entity ID asynchronously, we currently
/// start it at None, and then atomically set it when it's saved for the
@ -320,6 +429,11 @@ impl Item {
}
}
/// Returns an iterator that drains items contained within the item's slots
pub fn drain(&mut self) -> impl Iterator<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 is_same_item_def(&self, item_def: &ItemDef) -> bool {
@ -338,6 +452,26 @@ impl Item {
pub fn quality(&self) -> Quality { self.item_def.quality }
pub fn slots(&self) -> &[InvSlot] { &self.slots }
pub fn slots_mut(&mut self) -> &mut [InvSlot] { &mut self.slots }
pub fn item_config_expect(&self) -> &ItemConfig {
&self
.item_def
.item_config
.as_ref()
.expect("Item was expected to have an ItemConfig")
}
pub fn free_slots(&self) -> usize { self.slots.iter().filter(|x| x.is_none()).count() }
pub fn populated_slots(&self) -> usize { self.slots().len().saturating_sub(self.free_slots()) }
pub fn slot(&self, slot: usize) -> Option<&InvSlot> { self.slots.get(slot) }
pub fn slot_mut(&mut self, slot: usize) -> Option<&mut InvSlot> { self.slots.get_mut(slot) }
pub fn try_reclaim_from_block(block: Block) -> Option<Self> {
let chosen;
let mut rng = rand::thread_rng();
@ -416,6 +550,7 @@ pub trait ItemDesc {
fn name(&self) -> &str;
fn kind(&self) -> &ItemKind;
fn quality(&self) -> &Quality;
fn num_slots(&self) -> u16;
fn item_definition_id(&self) -> &str;
}
@ -428,6 +563,8 @@ impl ItemDesc for Item {
fn quality(&self) -> &Quality { &self.item_def.quality }
fn num_slots(&self) -> u16 { self.item_def.slots }
fn item_definition_id(&self) -> &str { &self.item_def.item_definition_id }
}
@ -440,6 +577,8 @@ impl ItemDesc for ItemDef {
fn quality(&self) -> &Quality { &self.quality }
fn num_slots(&self) -> u16 { self.slots }
fn item_definition_id(&self) -> &str { &self.item_definition_id }
}

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::{
biped_large, golem,
item::{tool::AbilityMap, Item, ItemKind},
quadruped_low, quadruped_medium, theropod, Body, CharacterAbility, ItemConfig, Loadout,
inventory::{
loadout::Loadout,
slot::{ArmorSlot, EquipSlot},
},
item::{Item, ItemKind},
quadruped_low, quadruped_medium, theropod, Body,
};
use rand::Rng;
@ -13,19 +17,18 @@ use rand::Rng;
/// use veloren_common::{
/// assets::AssetExt,
/// comp::item::tool::AbilityMap,
/// comp::Item,
/// LoadoutBuilder,
/// };
///
/// let map = AbilityMap::load_expect_cloned("common.abilities.weapon_ability_manifest");
///
/// // Build a loadout with character starter defaults and a specific sword with default sword abilities
/// let loadout = LoadoutBuilder::new()
/// .defaults()
/// .active_item(Some(LoadoutBuilder::default_item_config_from_str(
/// "common.items.weapons.sword.zweihander_sword_0", &map
/// )))
/// .active_item(Some(Item::new_from_asset_expect("common.items.weapons.sword.zweihander_sword_0")))
/// .build();
/// ```
#[derive(Clone)]
pub struct LoadoutBuilder(Loadout);
#[derive(Copy, Clone)]
pub enum LoadoutConfig {
@ -40,29 +43,9 @@ pub enum LoadoutConfig {
Warlock,
}
pub struct LoadoutBuilder(Loadout);
impl LoadoutBuilder {
#[allow(clippy::new_without_default)] // TODO: Pending review in #587
pub fn new() -> Self {
Self(Loadout {
active_item: None,
second_item: None,
shoulder: None,
chest: None,
belt: None,
hand: None,
pants: None,
foot: None,
back: None,
ring: None,
neck: None,
lantern: None,
glider: None,
head: None,
tabard: None,
})
}
pub fn new() -> Self { Self(Loadout::new_empty()) }
/// Set default armor items for the loadout. This may vary with game
/// updates, but should be safe defaults for a new character.
@ -73,7 +56,7 @@ impl LoadoutBuilder {
.pants(Some(Item::new_from_asset_expect(
"common.items.armor.starter.rugged_pants",
)))
.foot(Some(Item::new_from_asset_expect(
.feet(Some(Item::new_from_asset_expect(
"common.items.armor.starter.sandals_0",
)))
.lantern(Some(Item::new_from_asset_expect(
@ -89,7 +72,6 @@ impl LoadoutBuilder {
pub fn build_loadout(
body: Body,
mut main_tool: Option<Item>,
map: &AbilityMap,
config: Option<LoadoutConfig>,
) -> Self {
// If no main tool is passed in, checks if species has a default main tool
@ -251,284 +233,230 @@ impl LoadoutBuilder {
// Constructs ItemConfig from Item
let active_item = if let Some(ItemKind::Tool(_)) = main_tool.as_ref().map(|i| i.kind()) {
main_tool.map(|item| ItemConfig::from((item, map)))
main_tool
} else {
Some(LoadoutBuilder::animal(body))
Some(Item::new_default_for_body(&body))
};
// Creates rest of loadout
let loadout = if let Some(config) = config {
use LoadoutConfig::*;
match config {
Guard => Loadout {
active_item,
second_item: None,
shoulder: Some(Item::new_from_asset_expect(
Guard => LoadoutBuilder::new()
.active_item(active_item)
.shoulder(Some(Item::new_from_asset_expect(
"common.items.armor.shoulder.steel_0",
)),
chest: Some(Item::new_from_asset_expect(
)))
.chest(Some(Item::new_from_asset_expect(
"common.items.armor.chest.steel_0",
)),
belt: Some(Item::new_from_asset_expect(
)))
.belt(Some(Item::new_from_asset_expect(
"common.items.armor.belt.steel_0",
)),
hand: Some(Item::new_from_asset_expect(
)))
.hands(Some(Item::new_from_asset_expect(
"common.items.armor.hand.steel_0",
)),
pants: Some(Item::new_from_asset_expect(
)))
.pants(Some(Item::new_from_asset_expect(
"common.items.armor.pants.steel_0",
)),
foot: Some(Item::new_from_asset_expect(
)))
.feet(Some(Item::new_from_asset_expect(
"common.items.armor.foot.steel_0",
)),
back: None,
ring: None,
neck: None,
lantern: match rand::thread_rng().gen_range(0, 3) {
)))
.lantern(match rand::thread_rng().gen_range(0, 3) {
0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")),
_ => None,
},
glider: None,
head: None,
tabard: None,
},
Outcast => Loadout {
active_item,
second_item: None,
shoulder: Some(Item::new_from_asset_expect(
})
.build(),
Outcast => LoadoutBuilder::new()
.active_item(active_item)
.shoulder(Some(Item::new_from_asset_expect(
"common.items.armor.shoulder.cloth_purple_0",
)),
chest: Some(Item::new_from_asset_expect(
)))
.chest(Some(Item::new_from_asset_expect(
"common.items.armor.chest.cloth_purple_0",
)),
belt: Some(Item::new_from_asset_expect(
)))
.belt(Some(Item::new_from_asset_expect(
"common.items.armor.belt.cloth_purple_0",
)),
hand: Some(Item::new_from_asset_expect(
)))
.hands(Some(Item::new_from_asset_expect(
"common.items.armor.hand.cloth_purple_0",
)),
pants: Some(Item::new_from_asset_expect(
)))
.pants(Some(Item::new_from_asset_expect(
"common.items.armor.pants.cloth_purple_0",
)),
foot: Some(Item::new_from_asset_expect(
)))
.feet(Some(Item::new_from_asset_expect(
"common.items.armor.foot.cloth_purple_0",
)),
back: None,
ring: None,
neck: None,
lantern: match rand::thread_rng().gen_range(0, 3) {
)))
.lantern(match rand::thread_rng().gen_range(0, 3) {
0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")),
_ => None,
},
glider: None,
head: None,
tabard: None,
},
Highwayman => Loadout {
active_item,
second_item: None,
shoulder: Some(Item::new_from_asset_expect(
})
.build(),
Highwayman => LoadoutBuilder::new()
.active_item(active_item)
.shoulder(Some(Item::new_from_asset_expect(
"common.items.armor.shoulder.leather_0",
)),
chest: Some(Item::new_from_asset_expect(
)))
.chest(Some(Item::new_from_asset_expect(
"common.items.armor.chest.leather_0",
)),
belt: Some(Item::new_from_asset_expect(
)))
.belt(Some(Item::new_from_asset_expect(
"common.items.armor.belt.leather_0",
)),
hand: Some(Item::new_from_asset_expect(
)))
.hands(Some(Item::new_from_asset_expect(
"common.items.armor.hand.leather_0",
)),
pants: Some(Item::new_from_asset_expect(
)))
.pants(Some(Item::new_from_asset_expect(
"common.items.armor.pants.leather_0",
)),
foot: Some(Item::new_from_asset_expect(
)))
.feet(Some(Item::new_from_asset_expect(
"common.items.armor.foot.leather_0",
)),
back: None,
ring: None,
neck: None,
lantern: match rand::thread_rng().gen_range(0, 3) {
)))
.lantern(match rand::thread_rng().gen_range(0, 3) {
0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")),
_ => None,
},
glider: None,
head: None,
tabard: None,
},
Bandit => Loadout {
active_item,
second_item: None,
shoulder: Some(Item::new_from_asset_expect(
})
.build(),
Bandit => LoadoutBuilder::new()
.active_item(active_item)
.shoulder(Some(Item::new_from_asset_expect(
"common.items.armor.shoulder.assassin",
)),
chest: Some(Item::new_from_asset_expect(
)))
.chest(Some(Item::new_from_asset_expect(
"common.items.armor.chest.assassin",
)),
belt: Some(Item::new_from_asset_expect(
)))
.belt(Some(Item::new_from_asset_expect(
"common.items.armor.belt.assassin",
)),
hand: Some(Item::new_from_asset_expect(
)))
.hands(Some(Item::new_from_asset_expect(
"common.items.armor.hand.assassin",
)),
pants: Some(Item::new_from_asset_expect(
)))
.pants(Some(Item::new_from_asset_expect(
"common.items.armor.pants.assassin",
)),
foot: Some(Item::new_from_asset_expect(
)))
.feet(Some(Item::new_from_asset_expect(
"common.items.armor.foot.assassin",
)),
back: None,
ring: None,
neck: None,
lantern: match rand::thread_rng().gen_range(0, 3) {
)))
.lantern(match rand::thread_rng().gen_range(0, 3) {
0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")),
_ => None,
},
glider: None,
head: None,
tabard: None,
},
CultistNovice => Loadout {
active_item,
second_item: None,
shoulder: Some(Item::new_from_asset_expect(
})
.build(),
CultistNovice => LoadoutBuilder::new()
.active_item(active_item)
.shoulder(Some(Item::new_from_asset_expect(
"common.items.armor.shoulder.steel_0",
)),
chest: Some(Item::new_from_asset_expect(
)))
.chest(Some(Item::new_from_asset_expect(
"common.items.armor.chest.steel_0",
)),
belt: Some(Item::new_from_asset_expect(
)))
.belt(Some(Item::new_from_asset_expect(
"common.items.armor.belt.steel_0",
)),
hand: Some(Item::new_from_asset_expect(
)))
.hands(Some(Item::new_from_asset_expect(
"common.items.armor.hand.steel_0",
)),
pants: Some(Item::new_from_asset_expect(
)))
.pants(Some(Item::new_from_asset_expect(
"common.items.armor.pants.steel_0",
)),
foot: Some(Item::new_from_asset_expect(
)))
.feet(Some(Item::new_from_asset_expect(
"common.items.armor.foot.steel_0",
)),
back: Some(Item::new_from_asset_expect(
)))
.back(Some(Item::new_from_asset_expect(
"common.items.armor.back.dungeon_purple-0",
)),
ring: None,
neck: None,
lantern: match rand::thread_rng().gen_range(0, 3) {
)))
.lantern(match rand::thread_rng().gen_range(0, 3) {
0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")),
_ => None,
},
glider: None,
head: None,
tabard: None,
},
CultistAcolyte => Loadout {
active_item,
second_item: None,
shoulder: Some(Item::new_from_asset_expect(
})
.build(),
CultistAcolyte => LoadoutBuilder::new()
.active_item(active_item)
.shoulder(Some(Item::new_from_asset_expect(
"common.items.armor.shoulder.cultist_shoulder_purple",
)),
chest: Some(Item::new_from_asset_expect(
)))
.chest(Some(Item::new_from_asset_expect(
"common.items.armor.chest.cultist_chest_purple",
)),
belt: Some(Item::new_from_asset_expect(
)))
.belt(Some(Item::new_from_asset_expect(
"common.items.armor.belt.cultist_belt",
)),
hand: Some(Item::new_from_asset_expect(
)))
.hands(Some(Item::new_from_asset_expect(
"common.items.armor.hand.cultist_hands_purple",
)),
pants: Some(Item::new_from_asset_expect(
)))
.pants(Some(Item::new_from_asset_expect(
"common.items.armor.pants.cultist_legs_purple",
)),
foot: Some(Item::new_from_asset_expect(
)))
.feet(Some(Item::new_from_asset_expect(
"common.items.armor.foot.cultist_boots",
)),
back: Some(Item::new_from_asset_expect(
)))
.back(Some(Item::new_from_asset_expect(
"common.items.armor.back.dungeon_purple-0",
)),
ring: None,
neck: None,
lantern: match rand::thread_rng().gen_range(0, 3) {
)))
.lantern(match rand::thread_rng().gen_range(0, 3) {
0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")),
_ => None,
},
glider: None,
head: None,
tabard: None,
},
Warlord => Loadout {
active_item,
second_item: None,
shoulder: Some(Item::new_from_asset_expect(
})
.build(),
Warlord => LoadoutBuilder::new()
.active_item(active_item)
.shoulder(Some(Item::new_from_asset_expect(
"common.items.armor.shoulder.warlord",
)),
chest: Some(Item::new_from_asset_expect(
)))
.chest(Some(Item::new_from_asset_expect(
"common.items.armor.chest.warlord",
)),
belt: Some(Item::new_from_asset_expect(
)))
.belt(Some(Item::new_from_asset_expect(
"common.items.armor.belt.warlord",
)),
hand: Some(Item::new_from_asset_expect(
)))
.hands(Some(Item::new_from_asset_expect(
"common.items.armor.hand.warlord",
)),
pants: Some(Item::new_from_asset_expect(
)))
.pants(Some(Item::new_from_asset_expect(
"common.items.armor.pants.warlord",
)),
foot: Some(Item::new_from_asset_expect(
)))
.feet(Some(Item::new_from_asset_expect(
"common.items.armor.foot.warlord",
)),
back: Some(Item::new_from_asset_expect(
)))
.back(Some(Item::new_from_asset_expect(
"common.items.armor.back.warlord",
)),
ring: None,
neck: None,
lantern: match rand::thread_rng().gen_range(0, 3) {
)))
.lantern(match rand::thread_rng().gen_range(0, 3) {
0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")),
_ => None,
},
glider: None,
head: None,
tabard: None,
},
Warlock => Loadout {
active_item,
second_item: None,
shoulder: Some(Item::new_from_asset_expect(
})
.build(),
Warlock => LoadoutBuilder::new()
.active_item(active_item)
.shoulder(Some(Item::new_from_asset_expect(
"common.items.armor.shoulder.warlock",
)),
chest: Some(Item::new_from_asset_expect(
)))
.chest(Some(Item::new_from_asset_expect(
"common.items.armor.chest.warlock",
)),
belt: Some(Item::new_from_asset_expect(
)))
.belt(Some(Item::new_from_asset_expect(
"common.items.armor.belt.warlock",
)),
hand: Some(Item::new_from_asset_expect(
)))
.hands(Some(Item::new_from_asset_expect(
"common.items.armor.hand.warlock",
)),
pants: Some(Item::new_from_asset_expect(
)))
.pants(Some(Item::new_from_asset_expect(
"common.items.armor.pants.warlock",
)),
foot: Some(Item::new_from_asset_expect(
)))
.feet(Some(Item::new_from_asset_expect(
"common.items.armor.foot.warlock",
)),
back: Some(Item::new_from_asset_expect(
)))
.back(Some(Item::new_from_asset_expect(
"common.items.armor.back.warlock",
)),
ring: None,
neck: None,
lantern: match rand::thread_rng().gen_range(0, 3) {
)))
.lantern(match rand::thread_rng().gen_range(0, 3) {
0 => Some(Item::new_from_asset_expect("common.items.lantern.black_0")),
_ => None,
},
glider: None,
head: None,
tabard: None,
},
Villager => Loadout {
active_item,
second_item: None,
shoulder: None,
chest: Some(Item::new_from_asset_expect(
})
.build(),
Villager => LoadoutBuilder::new()
.active_item(active_item)
.chest(Some(Item::new_from_asset_expect(
match rand::thread_rng().gen_range(0, 10) {
0 => "common.items.armor.chest.worker_green_0",
1 => "common.items.armor.chest.worker_green_1",
@ -541,167 +469,105 @@ impl LoadoutBuilder {
8 => "common.items.armor.chest.worker_orange_0",
_ => "common.items.armor.chest.worker_orange_1",
},
)),
belt: Some(Item::new_from_asset_expect(
)))
.belt(Some(Item::new_from_asset_expect(
"common.items.armor.belt.leather_0",
)),
hand: None,
pants: Some(Item::new_from_asset_expect(
)))
.pants(Some(Item::new_from_asset_expect(
"common.items.armor.pants.worker_blue_0",
)),
foot: Some(Item::new_from_asset_expect(
)))
.feet(Some(Item::new_from_asset_expect(
match rand::thread_rng().gen_range(0, 2) {
0 => "common.items.armor.foot.leather_0",
_ => "common.items.armor.starter.sandals_0",
},
)),
back: None,
ring: None,
neck: None,
lantern: None,
glider: None,
head: None,
tabard: None,
},
)))
.build(),
}
} else {
Loadout {
active_item,
second_item: None,
shoulder: None,
chest: None,
belt: None,
hand: None,
pants: None,
foot: None,
back: None,
ring: None,
neck: None,
lantern: None,
glider: None,
head: None,
tabard: None,
}
LoadoutBuilder::new().active_item(active_item).build()
};
Self(loadout)
}
/// Default animal configuration
pub fn animal(body: Body) -> ItemConfig {
ItemConfig {
item: Item::new_from_asset_expect("common.items.weapons.empty.empty"),
ability1: Some(CharacterAbility::BasicMelee {
energy_cost: 10,
buildup_duration: 500,
swing_duration: 100,
recover_duration: 100,
base_damage: body.base_dmg(),
knockback: 0.0,
range: body.base_range(),
max_angle: 20.0,
}),
ability2: None,
ability3: None,
block_ability: None,
dodge_ability: None,
}
}
/// Get the default [ItemConfig](../comp/struct.ItemConfig.html) for a tool
/// (weapon). This information is required for the `active` and `second`
/// weapon items in a loadout. If some customisation to the item's
/// abilities or their timings is desired, you should create and provide
/// the item config directly to the [active_item](#method.active_item)
/// method
pub fn default_item_config_from_item(item: Item, map: &AbilityMap) -> ItemConfig {
ItemConfig::from((item, map))
}
/// Get an item's (weapon's) default
/// [ItemConfig](../comp/struct.ItemConfig.html)
/// by string reference. This will first attempt to load the Item, then
/// the default abilities for that item via the
/// [default_item_config_from_item](#method.default_item_config_from_item)
/// function
pub fn default_item_config_from_str(item_ref: &str, map: &AbilityMap) -> ItemConfig {
Self::default_item_config_from_item(Item::new_from_asset_expect(item_ref), map)
}
pub fn active_item(mut self, item: Option<ItemConfig>) -> Self {
self.0.active_item = item;
pub fn active_item(mut self, item: Option<Item>) -> Self {
self.0.swap(EquipSlot::Mainhand, item);
self
}
pub fn second_item(mut self, item: Option<ItemConfig>) -> Self {
self.0.second_item = item;
pub fn second_item(mut self, item: Option<Item>) -> Self {
self.0.swap(EquipSlot::Offhand, item);
self
}
pub fn shoulder(mut self, item: Option<Item>) -> Self {
self.0.shoulder = item;
self.0.swap(EquipSlot::Armor(ArmorSlot::Shoulders), item);
self
}
pub fn chest(mut self, item: Option<Item>) -> Self {
self.0.chest = item;
self.0.swap(EquipSlot::Armor(ArmorSlot::Chest), item);
self
}
pub fn belt(mut self, item: Option<Item>) -> Self {
self.0.belt = item;
self.0.swap(EquipSlot::Armor(ArmorSlot::Belt), item);
self
}
pub fn hand(mut self, item: Option<Item>) -> Self {
self.0.hand = item;
pub fn hands(mut self, item: Option<Item>) -> Self {
self.0.swap(EquipSlot::Armor(ArmorSlot::Hands), item);
self
}
pub fn pants(mut self, item: Option<Item>) -> Self {
self.0.pants = item;
self.0.swap(EquipSlot::Armor(ArmorSlot::Legs), item);
self
}
pub fn foot(mut self, item: Option<Item>) -> Self {
self.0.foot = item;
pub fn feet(mut self, item: Option<Item>) -> Self {
self.0.swap(EquipSlot::Armor(ArmorSlot::Feet), item);
self
}
pub fn back(mut self, item: Option<Item>) -> Self {
self.0.back = item;
self.0.swap(EquipSlot::Armor(ArmorSlot::Back), item);
self
}
pub fn ring(mut self, item: Option<Item>) -> Self {
self.0.ring = item;
pub fn ring1(mut self, item: Option<Item>) -> Self {
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
}
pub fn neck(mut self, item: Option<Item>) -> Self {
self.0.neck = item;
self.0.swap(EquipSlot::Armor(ArmorSlot::Neck), item);
self
}
pub fn lantern(mut self, item: Option<Item>) -> Self {
self.0.lantern = item;
self.0.swap(EquipSlot::Lantern, item);
self
}
pub fn glider(mut self, item: Option<Item>) -> Self {
self.0.glider = item;
self.0.swap(EquipSlot::Glider, item);
self
}
pub fn head(mut self, item: Option<Item>) -> Self {
self.0.head = item;
self.0.swap(EquipSlot::Armor(ArmorSlot::Head), item);
self
}
pub fn tabard(mut self, item: Option<Item>) -> Self {
self.0.tabard = item;
self.0.swap(EquipSlot::Armor(ArmorSlot::Tabard), item);
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 item::Item;
use std::{collections::HashMap, convert::TryFrom, iter::once, mem, ops::Range};
use serde::{Deserialize, Serialize};
use specs::{Component, HashMapStorage};
use specs::{Component, DerefFlaggedStorage};
use specs_idvs::IdvStorage;
use tracing::{debug, trace, warn};
use crate::{
comp::{
inventory::{
item::ItemDef,
loadout::Loadout,
slot::{EquipSlot, Slot, SlotError},
},
slot::{InvSlotId, SlotId},
Item,
},
recipe::Recipe,
LoadoutBuilder,
};
pub mod item;
pub mod loadout;
pub mod loadout_builder;
pub mod slot;
#[cfg(test)] mod test;
#[cfg(test)] mod test_helpers;
pub type InvSlot = Option<Item>;
const DEFAULT_INVENTORY_SLOTS: usize = 18;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Inventory {
slots: Vec<Option<Item>>,
amount: u32,
loadout: Loadout,
/// 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
@ -22,24 +46,55 @@ pub enum Error {
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 {
pub fn new_empty() -> Inventory {
pub fn new_empty() -> Inventory { Self::new_with_loadout(LoadoutBuilder::new().build()) }
pub fn new_with_loadout(loadout: Loadout) -> Inventory {
Inventory {
slots: vec![None; 36],
amount: 0,
loadout,
slots: vec![None; DEFAULT_INVENTORY_SLOTS],
}
}
pub fn slots(&self) -> &[Option<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.
pub fn amount(&self) -> u32 { self.amount }
/// A mutable iterator of all inventory slots
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) {
self.amount = self.slots.iter().filter(|i| i.is_some()).count() as u32;
/// An iterator of all inventory slots and their position
pub fn slots_with_id(&self) -> impl Iterator<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
@ -47,8 +102,7 @@ impl Inventory {
pub fn push(&mut self, item: Item) -> Option<Item> {
if item.is_stackable() {
if let Some(slot_item) = self
.slots
.iter_mut()
.slots_mut()
.filter_map(Option::as_mut)
.find(|s| *s == &item)
{
@ -61,21 +115,7 @@ impl Inventory {
// No existing item to stack with or item not stackable, put the item in a new
// slot
self.add_to_first_empty(item)
}
/// Adds a new item to the first empty slot of the inventory. Returns the
/// item again if no free slot was found.
fn add_to_first_empty(&mut self, item: Item) -> Option<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
self.insert(item)
}
/// Add a series of items to inventory, returning any which do not fit as an
@ -120,24 +160,22 @@ impl Inventory {
/// Replaces an item in a specific slot of the inventory. Returns the old
/// item or the same item again if that slot was not found.
pub fn insert(&mut self, cell: usize, item: Item) -> Result<Option<Item>, Item> {
match self.slots.get_mut(cell) {
Some(slot) => {
let old = core::mem::replace(slot, Some(item));
if old.is_none() {
self.recount_items();
}
Ok(old)
},
pub fn insert_at(&mut self, inv_slot_id: InvSlotId, item: Item) -> Result<Option<Item>, Item> {
match self.slot_mut(inv_slot_id) {
Some(slot) => Ok(core::mem::replace(slot, Some(item))),
None => Err(item),
}
}
/// Checks if inserting item exists in given cell. Inserts an item if it
/// exists.
pub fn insert_or_stack(&mut self, cell: usize, item: Item) -> Result<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() {
match self.slots.get_mut(cell) {
match self.slot_mut(inv_slot_id) {
Some(Some(slot_item)) => {
Ok(if slot_item == &item {
slot_item
@ -150,46 +188,77 @@ impl Inventory {
Some(old_item)
})
},
Some(None) => self.insert(cell, item),
Some(None) => self.insert_at(inv_slot_id, item),
None => Err(item),
}
} else {
self.insert(cell, item)
self.insert_at(inv_slot_id, item)
}
}
pub fn is_full(&self) -> bool { self.slots.iter().all(|slot| slot.is_some()) }
/// Attempts to equip the item into a compatible, unpopulated loadout slot.
/// If no slot is available the item is returned.
#[must_use = "Returned item will be lost if not used"]
pub fn try_equip(&mut self, item: Item) -> Result<(), Item> { self.loadout.try_equip(item) }
/// O(n) count the number of items in this inventory.
pub fn count(&self) -> usize { self.slots.iter().filter_map(|slot| slot.as_ref()).count() }
pub fn populated_slots(&self) -> usize { self.slots().filter_map(|slot| slot.as_ref()).count() }
/// O(n) check if an item is in this inventory.
fn free_slots(&self) -> usize { self.slots().filter(|slot| slot.is_none()).count() }
/// Check if an item is in this inventory.
pub fn contains(&self, item: &Item) -> bool {
self.slots.iter().any(|slot| slot.as_ref() == Some(item))
self.slots().any(|slot| slot.as_ref() == Some(item))
}
/// Get content of a slot
pub fn get(&self, cell: usize) -> Option<&Item> {
self.slots.get(cell).and_then(Option::as_ref)
pub fn get(&self, inv_slot_id: InvSlotId) -> Option<&Item> {
self.slot(inv_slot_id).and_then(Option::as_ref)
}
/// Returns a reference to the item (if any) equipped in the given EquipSlot
pub fn equipped(&self, equip_slot: EquipSlot) -> Option<&Item> {
self.loadout.equipped(equip_slot)
}
pub fn loadout_items_with_persistence_key(
&self,
) -> impl Iterator<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
pub fn swap_slots(&mut self, a: usize, b: usize) {
if a.max(b) < self.slots.len() {
self.slots.swap(a, b);
pub fn swap_slots(&mut self, a: InvSlotId, b: InvSlotId) {
if self.slot(a).is_none() || self.slot(b).is_none() {
warn!("swap_slots called with non-existent inventory slot(s)");
return;
}
let slot_a = mem::take(self.slot_mut(a).unwrap());
let slot_b = mem::take(self.slot_mut(b).unwrap());
*self.slot_mut(a).unwrap() = slot_b;
*self.slot_mut(b).unwrap() = slot_a;
}
/// Remove an item from the slot
pub fn remove(&mut self, cell: usize) -> Option<Item> {
let item = self.slots.get_mut(cell).and_then(|item| item.take());
self.recount_items();
item
pub fn remove(&mut self, inv_slot_id: InvSlotId) -> Option<Item> {
self.slot_mut(inv_slot_id).and_then(|item| item.take())
}
/// Remove just one item from the slot
pub fn take(&mut self, cell: usize) -> Option<Item> {
if let Some(Some(item)) = self.slots.get_mut(cell) {
pub fn take(&mut self, inv_slot_id: InvSlotId) -> Option<Item> {
if let Some(Some(item)) = self.slot_mut(inv_slot_id) {
let mut return_item = item.duplicate();
if item.is_stackable() && item.amount() > 1 {
@ -197,20 +266,25 @@ impl Inventory {
return_item
.set_amount(1)
.expect("Items duplicated from a stackable item must be stackable.");
self.recount_items();
Some(return_item)
} else {
self.remove(cell)
self.remove(inv_slot_id)
}
} else {
None
}
}
/// Takes all items from the inventory
pub fn drain(&mut self) -> impl Iterator<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.
pub fn item_count(&self, item_def: &ItemDef) -> u64 {
self.slots()
.iter()
.flatten()
.filter(|it| it.is_same_item_def(item_def))
.map(|it| u64::from(it.amount()))
@ -225,17 +299,18 @@ impl Inventory {
pub fn contains_ingredients<'a>(
&self,
recipe: &'a Recipe,
) -> Result<Vec<u32>, Vec<(&'a ItemDef, u32)>> {
let mut slot_claims = vec![0; self.slots.len()];
) -> Result<HashMap<InvSlotId, u32>, Vec<(&'a ItemDef, u32)>> {
let mut slot_claims = HashMap::<InvSlotId, u32>::new();
let mut missing = Vec::<(&ItemDef, u32)>::new();
for (input, mut needed) in recipe.inputs() {
let mut contains_any = false;
for (i, slot) in self.slots().iter().enumerate() {
for (inv_slot_id, slot) in self.slots_with_id() {
if let Some(item) = slot.as_ref().filter(|item| item.is_same_item_def(&*input)) {
let can_claim = (item.amount() - slot_claims[i]).min(needed);
slot_claims[i] += can_claim;
let claim = slot_claims.entry(inv_slot_id).or_insert(0);
let can_claim = (item.amount() - *claim).min(needed);
*claim += can_claim;
needed -= can_claim;
contains_any = true;
}
@ -252,24 +327,312 @@ impl Inventory {
Err(missing)
}
}
}
impl Default for Inventory {
fn default() -> Inventory {
let mut inventory = Inventory {
slots: vec![None; 36],
amount: 0,
};
inventory.push(Item::new_from_asset_expect(
"common.items.consumable.potion_minor",
));
inventory.push(Item::new_from_asset_expect("common.items.food.cheese"));
inventory
/// Adds a new item to the first empty slot of the inventory. Returns the
/// item again if no free slot was found.
fn insert(&mut self, item: Item) -> Option<Item> {
match self.slots_mut().find(|slot| slot.is_none()) {
Some(slot) => {
*slot = Some(item);
None
},
None => Some(item),
}
}
fn slot(&self, inv_slot_id: InvSlotId) -> Option<&InvSlot> {
match SlotId::from(inv_slot_id) {
SlotId::Inventory(slot_idx) => self.slots.get(slot_idx),
SlotId::Loadout(loadout_slot_id) => self.loadout.inv_slot(loadout_slot_id),
}
}
fn slot_mut(&mut self, inv_slot_id: InvSlotId) -> Option<&mut InvSlot> {
match SlotId::from(inv_slot_id) {
SlotId::Inventory(slot_idx) => self.slots.get_mut(slot_idx),
SlotId::Loadout(loadout_slot_id) => self.loadout.inv_slot_mut(loadout_slot_id),
}
}
/// Returns the number of free slots in the inventory ignoring any slots
/// granted by the item (if any) equipped in the provided EquipSlot.
pub fn free_slots_minus_equipped_item(&self, equip_slot: EquipSlot) -> usize {
if let Some(mut equip_slot_idx) = self.loadout.loadout_idx_for_equip_slot(equip_slot) {
// Offset due to index 0 representing built-in inventory slots
equip_slot_idx += 1;
self.slots_with_id()
.filter(|(inv_slot_id, slot)| {
inv_slot_id.loadout_idx() != equip_slot_idx && slot.is_none()
})
.count()
} else {
// TODO: return Option<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 {
type Storage = HashMapStorage<Self>;
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
@ -306,5 +669,3 @@ impl InventoryUpdate {
impl Component for InventoryUpdate {
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 tracing::warn;
use std::{cmp::Ordering, convert::TryFrom};
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
pub enum Slot {
Inventory(usize),
Equip(EquipSlot),
use crate::comp::{
inventory::{
item::{armor, armor::ArmorKind, ItemKind},
loadout::LoadoutSlotId,
},
item,
};
#[derive(Debug, PartialEq)]
pub enum SlotError {
InventoryFull,
}
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
pub enum Slot {
Inventory(InvSlotId),
Equip(EquipSlot),
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct InvSlotId {
// The index of the loadout item that provides this inventory slot. 0 represents
// built-in inventory slots
loadout_idx: u16,
// The index of the slot within its container
slot_idx: u16,
}
impl InvSlotId {
pub const fn new(loadout_idx: u16, slot_idx: u16) -> Self {
Self {
loadout_idx,
slot_idx,
}
}
pub fn idx(&self) -> u32 { (u32::from(self.loadout_idx) << 16) | u32::from(self.slot_idx) }
pub fn loadout_idx(&self) -> usize { usize::from(self.loadout_idx) }
pub fn slot_idx(&self) -> usize { usize::from(self.slot_idx) }
}
impl From<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 {
Armor(ArmorSlot),
Mainhand,
@ -24,25 +87,26 @@ pub enum EquipSlot {
Glider,
}
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, Serialize, Deserialize)]
pub enum ArmorSlot {
Head,
Neck,
Shoulders,
Chest,
Hands,
Ring,
Ring1,
Ring2,
Back,
Belt,
Legs,
Feet,
Tabard,
Bag1,
Bag2,
Bag3,
Bag4,
}
//const ALL_ARMOR_SLOTS: [ArmorSlot; 11] = [
// Head, Neck, Shoulders, Chest, Hands, Ring, Back, Belt, Legs, Feet, Tabard,
//];
impl Slot {
pub fn can_hold(self, item_kind: &item::ItemKind) -> bool {
match (self, item_kind) {
@ -53,11 +117,9 @@ impl Slot {
}
impl EquipSlot {
fn can_hold(self, item_kind: &item::ItemKind) -> bool {
use armor::Armor;
use item::ItemKind;
pub fn can_hold(self, item_kind: &item::ItemKind) -> bool {
match (self, item_kind) {
(Self::Armor(slot), ItemKind::Armor(Armor { kind, .. })) => slot.can_hold(kind),
(Self::Armor(slot), ItemKind::Armor(armor::Armor { kind, .. })) => slot.can_hold(kind),
(Self::Mainhand, ItemKind::Tool(_)) => true,
(Self::Offhand, ItemKind::Tool(_)) => true,
(Self::Lantern, ItemKind::Lantern(_)) => true,
@ -69,7 +131,6 @@ impl EquipSlot {
impl ArmorSlot {
fn can_hold(self, armor: &item::armor::ArmorKind) -> bool {
use item::armor::ArmorKind;
matches!(
(self, armor),
(Self::Head, ArmorKind::Head(_))
@ -77,414 +138,17 @@ impl ArmorSlot {
| (Self::Shoulders, ArmorKind::Shoulder(_))
| (Self::Chest, ArmorKind::Chest(_))
| (Self::Hands, ArmorKind::Hand(_))
| (Self::Ring, ArmorKind::Ring(_))
| (Self::Ring1, ArmorKind::Ring(_))
| (Self::Ring2, ArmorKind::Ring(_))
| (Self::Back, ArmorKind::Back(_))
| (Self::Belt, ArmorKind::Belt(_))
| (Self::Legs, ArmorKind::Pants(_))
| (Self::Feet, ArmorKind::Foot(_))
| (Self::Tabard, ArmorKind::Tabard(_))
| (Self::Bag1, ArmorKind::Bag(_))
| (Self::Bag2, ArmorKind::Bag(_))
| (Self::Bag3, ArmorKind::Bag(_))
| (Self::Bag4, ArmorKind::Bag(_))
)
}
}
/// Replace an equipment slot with an item. Return the item that was in the
/// slot, if any. Doesn't update the inventory.
fn loadout_replace(
equip_slot: EquipSlot,
item: Option<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 crate::comp::{
inventory::{slot::ArmorSlot, test_helpers::get_test_bag},
Item,
};
use lazy_static::lazy_static;
lazy_static! {
static ref TEST_ITEMS: Vec<Item> = vec![
@ -12,7 +16,7 @@ lazy_static! {
fn push_full() {
let mut inv = Inventory {
slots: TEST_ITEMS.iter().map(|a| Some(a.clone())).collect(),
amount: 0,
loadout: LoadoutBuilder::new().build(),
};
assert_eq!(
inv.push(TEST_ITEMS[0].clone()).unwrap(),
@ -25,7 +29,7 @@ fn push_full() {
fn push_all_full() {
let mut inv = Inventory {
slots: TEST_ITEMS.iter().map(|a| Some(a.clone())).collect(),
amount: 0,
loadout: LoadoutBuilder::new().build(),
};
let Error::Full(leftovers) = inv
.push_all(TEST_ITEMS.iter().cloned())
@ -39,7 +43,7 @@ fn push_all_full() {
fn push_unique_all_full() {
let mut inv = Inventory {
slots: TEST_ITEMS.iter().map(|a| Some(a.clone())).collect(),
amount: 0,
loadout: LoadoutBuilder::new().build(),
};
inv.push_all_unique(TEST_ITEMS.iter().cloned())
.expect("Pushing unique items into an inventory that already contains them didn't work!");
@ -51,7 +55,7 @@ fn push_unique_all_full() {
fn push_all_empty() {
let mut inv = Inventory {
slots: vec![None, None],
amount: 0,
loadout: LoadoutBuilder::new().build(),
};
inv.push_all(TEST_ITEMS.iter().cloned())
.expect("Pushing items into an empty inventory didn't work!");
@ -63,9 +67,324 @@ fn push_all_empty() {
fn push_all_unique_empty() {
let mut inv = Inventory {
slots: vec![None, None],
amount: 0,
loadout: LoadoutBuilder::new().build(),
};
inv.push_all_unique(TEST_ITEMS.iter().cloned()).expect(
"Pushing unique items into an empty inventory that didn't contain them didn't work!",
);
}
#[test]
fn free_slots_minus_equipped_item_items_only_present_in_equipped_bag_slots() {
let mut inv = Inventory::new_empty();
let bag = get_test_bag(18);
let bag1_slot = EquipSlot::Armor(ArmorSlot::Bag1);
inv.loadout.swap(bag1_slot, Some(bag.clone()));
inv.insert_at(InvSlotId::new(15, 0), bag)
.unwrap()
.unwrap_none();
let result = inv.free_slots_minus_equipped_item(bag1_slot);
// All of the base inventory slots are empty and the equipped bag slots are
// ignored
assert_eq!(18, result);
}
#[test]
fn free_slots_minus_equipped_item() {
let mut inv = Inventory::new_empty();
let bag = get_test_bag(18);
let bag1_slot = EquipSlot::Armor(ArmorSlot::Bag1);
inv.loadout.swap(bag1_slot, Some(bag.clone()));
inv.loadout
.swap(EquipSlot::Armor(ArmorSlot::Bag2), Some(bag.clone()));
inv.insert_at(InvSlotId::new(16, 0), bag)
.unwrap()
.unwrap_none();
let result = inv.free_slots_minus_equipped_item(bag1_slot);
// All of the base 18 inventory slots are empty, the first equipped bag is
// ignored, and the second equipped bag has 17 free slots
assert_eq!(35, result);
}
#[test]
fn get_slot_range_for_equip_slot() {
let mut inv = Inventory::new_empty();
let bag = get_test_bag(18);
let bag1_slot = EquipSlot::Armor(ArmorSlot::Bag1);
inv.loadout.swap(bag1_slot, Some(bag));
let result = inv.get_slot_range_for_equip_slot(bag1_slot).unwrap();
assert_eq!(18..36, result);
}
#[test]
fn can_swap_equipped_bag_into_empty_inv_slot_1_free_slot() {
can_swap_equipped_bag_into_empty_inv_slot(1, InvSlotId::new(0, 17), true);
}
#[test]
fn can_swap_equipped_bag_into_empty_inv_slot_0_free_slots() {
can_swap_equipped_bag_into_empty_inv_slot(0, InvSlotId::new(0, 17), false);
}
#[test]
fn can_swap_equipped_bag_into_empty_inv_slot_provided_by_equipped_bag() {
can_swap_equipped_bag_into_empty_inv_slot(1, InvSlotId::new(15, 0), true);
}
fn can_swap_equipped_bag_into_empty_inv_slot(
free_slots: u16,
inv_slot_id: InvSlotId,
expected_result: bool,
) {
let mut inv = Inventory::new_empty();
inv.replace_loadout_item(EquipSlot::Armor(ArmorSlot::Bag1), Some(get_test_bag(18)));
fill_inv_slots(&mut inv, 18 - free_slots);
let result = inv.can_swap(inv_slot_id, EquipSlot::Armor(ArmorSlot::Bag1));
assert_eq!(expected_result, result);
}
#[test]
fn can_swap_equipped_bag_into_only_empty_slot_provided_by_itself_should_return_false() {
let mut inv = Inventory::new_empty();
inv.replace_loadout_item(EquipSlot::Armor(ArmorSlot::Bag1), Some(get_test_bag(18)));
fill_inv_slots(&mut inv, 35);
let result = inv.can_swap(InvSlotId::new(15, 17), EquipSlot::Armor(ArmorSlot::Bag1));
assert_eq!(false, result);
}
#[test]
fn unequip_items_both_hands() {
let mut inv = Inventory::new_empty();
let sword = Item::new_from_asset_expect("common.items.weapons.sword.zweihander_sword_0");
inv.replace_loadout_item(EquipSlot::Mainhand, Some(sword.clone()));
inv.replace_loadout_item(EquipSlot::Offhand, Some(sword.clone()));
// Fill all inventory slots except one
fill_inv_slots(&mut inv, 17);
let result = inv.unequip(EquipSlot::Mainhand);
// We have space in the inventory, so this should have unequipped
assert_eq!(None, inv.equipped(EquipSlot::Mainhand));
assert_eq!(18, inv.populated_slots());
assert_eq!(true, result.is_ok());
let result = inv.unequip(EquipSlot::Offhand).unwrap_err();
assert_eq!(SlotError::InventoryFull, result);
// There is no more space in the inventory, so this should still be equipped
assert_eq!(&sword, inv.equipped(EquipSlot::Offhand).unwrap());
// Verify inventory
assert_eq!(inv.slots[17], Some(sword));
assert_eq!(inv.free_slots(), 0);
}
#[test]
fn equip_replace_already_equipped_item() {
let boots = Item::new_from_asset_expect("common.items.testing.test_boots");
let starting_sandles = Some(Item::new_from_asset_expect(
"common.items.armor.starter.sandals_0",
));
let mut inv = Inventory::new_empty();
inv.push(boots.clone());
inv.replace_loadout_item(EquipSlot::Armor(ArmorSlot::Feet), starting_sandles.clone());
inv.equip(InvSlotId::new(0, 0)).unwrap_none();
// We should now have the testing boots equipped
assert_eq!(
&boots,
inv.equipped(EquipSlot::Armor(ArmorSlot::Feet)).unwrap()
);
// Verify inventory
assert_eq!(
inv.slots[0].as_ref().unwrap().item_definition_id(),
starting_sandles.unwrap().item_definition_id()
);
assert_eq!(inv.populated_slots(), 1);
}
/// Regression test for a panic that occurred when swapping an equipped bag
/// for a bag that exists in an inventory slot that will no longer exist
/// after equipping it (because the equipped bag is larger)
#[test]
fn equip_equipping_smaller_bag_from_last_slot_of_big_bag() {
let mut inv = Inventory::new_empty();
const LARGE_BAG_ID: &str = "common.items.testing.test_bag_18_slot";
let small_bag = get_test_bag(9);
let large_bag = Item::new_from_asset_expect(LARGE_BAG_ID);
inv.loadout
.swap(EquipSlot::Armor(ArmorSlot::Bag1), Some(large_bag))
.unwrap_none();
inv.insert_at(InvSlotId::new(15, 15), small_bag).unwrap();
let result = inv.swap(
Slot::Equip(EquipSlot::Armor(ArmorSlot::Bag1)),
Slot::Inventory(InvSlotId::new(15, 15)),
);
assert_eq!(
inv.get(InvSlotId::new(0, 0)).unwrap().item_definition_id(),
LARGE_BAG_ID
);
assert_eq!(result, None);
}
#[test]
fn unequip_unequipping_bag_into_its_own_slot_with_no_other_free_slots() {
let mut inv = Inventory::new_empty();
let bag = get_test_bag(9);
inv.loadout
.swap(EquipSlot::Armor(ArmorSlot::Bag1), Some(bag))
.unwrap_none();
// Fill all inventory built-in slots
fill_inv_slots(&mut inv, 18);
inv.swap_inventory_loadout(InvSlotId::new(15, 0), EquipSlot::Armor(ArmorSlot::Bag1))
.unwrap_none();
}
#[test]
fn equip_one_bag_equipped_equip_second_bag() {
let mut inv = Inventory::new_empty();
let bag = get_test_bag(9);
inv.loadout
.swap(EquipSlot::Armor(ArmorSlot::Bag1), Some(bag.clone()))
.unwrap_none();
inv.push(bag);
inv.equip(InvSlotId::new(0, 0)).unwrap_none();
assert_eq!(
true,
inv.equipped(EquipSlot::Armor(ArmorSlot::Bag2)).is_some()
);
}
#[test]
fn free_after_swap_equipped_item_has_more_slots() {
let mut inv = Inventory::new_empty();
let bag = get_test_bag(18);
inv.loadout
.swap(EquipSlot::Armor(ArmorSlot::Bag1), Some(bag))
.unwrap_none();
let small_bag = get_test_bag(9);
inv.push(small_bag);
// Fill all remaining slots
fill_inv_slots(&mut inv, 35);
let result = inv.free_after_swap(EquipSlot::Armor(ArmorSlot::Bag1), InvSlotId::new(0, 0));
// 18 inv slots + 9 bag slots - 36 used slots -
assert_eq!(-9, result);
}
#[test]
fn free_after_swap_equipped_item_has_less_slots() {
let mut inv = Inventory::new_empty();
let bag = get_test_bag(9);
inv.loadout
.swap(EquipSlot::Armor(ArmorSlot::Bag1), Some(bag))
.unwrap_none();
let small_bag = get_test_bag(18);
inv.push(small_bag);
// Fill all slots except the last one
fill_inv_slots(&mut inv, 27);
let result = inv.free_after_swap(EquipSlot::Armor(ArmorSlot::Bag1), InvSlotId::new(0, 0));
// 18 inv slots + 18 bag slots - 27 used slots
assert_eq!(9, result);
}
#[test]
fn free_after_swap_equipped_item_with_slots_swapped_with_empty_inv_slot() {
let mut inv = Inventory::new_empty();
let bag = get_test_bag(9);
inv.loadout
.swap(EquipSlot::Armor(ArmorSlot::Bag1), Some(bag))
.unwrap_none();
// Add 5 items to the inventory
fill_inv_slots(&mut inv, 5);
let result = inv.free_after_swap(EquipSlot::Armor(ArmorSlot::Bag1), InvSlotId::new(0, 10));
// 18 inv slots - 5 used slots - 1 slot for unequipped item
assert_eq!(12, result);
}
#[test]
fn free_after_swap_inv_item_with_slots_swapped_with_empty_equip_slot() {
let mut inv = Inventory::new_empty();
inv.push(get_test_bag(9));
// Add 5 items to the inventory
fill_inv_slots(&mut inv, 5);
let result = inv.free_after_swap(EquipSlot::Armor(ArmorSlot::Bag1), InvSlotId::new(0, 0));
// 18 inv slots + 9 bag slots - 5 used slots
assert_eq!(22, result);
}
#[test]
fn free_after_swap_inv_item_without_slots_swapped_with_empty_equip_slot() {
let mut inv = Inventory::new_empty();
let boots = Item::new_from_asset_expect("common.items.testing.test_boots");
inv.push(boots);
// Add 5 items to the inventory
fill_inv_slots(&mut inv, 5);
let result = inv.free_after_swap(EquipSlot::Armor(ArmorSlot::Feet), InvSlotId::new(0, 0));
// 18 inv slots - 5 used slots
assert_eq!(13, result);
}
fn fill_inv_slots(inv: &mut Inventory, items: u16) {
let boots = Item::new_from_asset_expect("common.items.testing.test_boots");
for _ in 0..items {
inv.push(boots.clone());
}
}

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

View File

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

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