Add multiloot

This commit is contained in:
Isse 2023-04-23 19:17:39 +00:00
parent 1a287e4e89
commit ab4076518f
82 changed files with 1051 additions and 592 deletions

View File

@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- NPCs will now tell you about nearby towns and how to visit them - NPCs will now tell you about nearby towns and how to visit them
- NPCs will migrate to new towns if they are dissatisfied with their current town - NPCs will migrate to new towns if they are dissatisfied with their current town
- Female humanoids now have a greeting sound effect - Female humanoids now have a greeting sound effect
- Loot that drops multiple items is now distributed fairly between damage contributors.
### Changed ### Changed

View File

@ -3,7 +3,7 @@
name: Name("Frost Gigas"), name: Name("Frost Gigas"),
body: RandomWith("gigas_frost"), body: RandomWith("gigas_frost"),
alignment: Alignment(Enemy), alignment: Alignment(Enemy),
loot: LootTable("common.loot_tables.world.world_bosses.gigas_frost.boss"), loot: MultiDrop(LootTable("common.loot_tables.world.world_bosses.gigas_frost.boss"), 3, 4),
inventory: ( inventory: (
loadout: FromBody, loadout: FromBody,
), ),

View File

@ -1,5 +1,5 @@
[ [
(4.0, ItemQuantity("common.items.crafting_ing.animal_misc.icy_fang", 1, 20)), (4.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.icy_fang"), 1, 20)),
(2.0, Item("common.items.armor.misc.head.boreal_warhelm")), (2.0, Item("common.items.armor.misc.head.boreal_warhelm")),
(1.0, Item("common.items.lantern.polaris")), (1.0, Item("common.items.lantern.polaris")),
] ]

View File

@ -1,4 +1,4 @@
[ [
(3.0, ItemQuantity("common.items.food.blue_cheese", 1, 15)), (3.0, MultiDrop(Item("common.items.food.blue_cheese"), 1, 15)),
(1.0, Item("common.items.calendar.christmas.armor.misc.head.woolly_wintercap")), (1.0, Item("common.items.calendar.christmas.armor.misc.head.woolly_wintercap")),
] ]

View File

@ -1,14 +1,14 @@
[ [
// Fireworks // Fireworks
(1.0, ItemQuantity("common.items.utility.firework_blue", 8, 10)), (1.0, MultiDrop(Item("common.items.utility.firework_blue"), 8, 10)),
(1.0, ItemQuantity("common.items.utility.firework_green", 8, 10)), (1.0, MultiDrop(Item("common.items.utility.firework_green"), 8, 10)),
(1.0, ItemQuantity("common.items.utility.firework_purple", 8, 10)), (1.0, MultiDrop(Item("common.items.utility.firework_purple"), 8, 10)),
(1.0, ItemQuantity("common.items.utility.firework_red", 8, 10)), (1.0, MultiDrop(Item("common.items.utility.firework_red"), 8, 10)),
(1.0, ItemQuantity("common.items.utility.firework_white", 8, 10)), (1.0, MultiDrop(Item("common.items.utility.firework_white"), 8, 10)),
(1.0, ItemQuantity("common.items.utility.firework_yellow", 8, 10)), (1.0, MultiDrop(Item("common.items.utility.firework_yellow"), 8, 10)),
// Potions // Potions
(10.0, ItemQuantity("common.items.consumable.potion_big", 2, 5)), (10.0, MultiDrop(Item("common.items.consumable.potion_big"), 2, 5)),
// Misc // Misc
(5.0, ItemQuantity("common.items.utility.collar", 2, 3)), (5.0, MultiDrop(Item("common.items.utility.collar"), 2, 3)),
(5.0, ItemQuantity("common.items.utility.bomb", 8, 10)), (5.0, MultiDrop(Item("common.items.utility.bomb"), 8, 10)),
] ]

View File

@ -1,14 +1,14 @@
[ [
// Fireworks // Fireworks
(1.0, ItemQuantity("common.items.utility.firework_blue", 3, 5)), (1.0, MultiDrop(Item("common.items.utility.firework_blue"), 3, 5)),
(1.0, ItemQuantity("common.items.utility.firework_green", 3, 5)), (1.0, MultiDrop(Item("common.items.utility.firework_green"), 3, 5)),
(1.0, ItemQuantity("common.items.utility.firework_purple", 3, 5)), (1.0, MultiDrop(Item("common.items.utility.firework_purple"), 3, 5)),
(1.0, ItemQuantity("common.items.utility.firework_red", 3, 5)), (1.0, MultiDrop(Item("common.items.utility.firework_red"), 3, 5)),
(1.0, ItemQuantity("common.items.utility.firework_white", 3, 5)), (1.0, MultiDrop(Item("common.items.utility.firework_white"), 3, 5)),
(1.0, ItemQuantity("common.items.utility.firework_yellow", 3, 5)), (1.0, MultiDrop(Item("common.items.utility.firework_yellow"), 3, 5)),
// Potions // Potions
(10.0, ItemQuantity("common.items.consumable.potion_med", 2, 5)), (10.0, MultiDrop(Item("common.items.consumable.potion_med"), 2, 5)),
// Misc // Misc
(5.0, ItemQuantity("common.items.utility.collar", 1, 2)), (5.0, MultiDrop(Item("common.items.utility.collar"), 1, 2)),
(5.0, ItemQuantity("common.items.utility.bomb", 3, 5)), (5.0, MultiDrop(Item("common.items.utility.bomb"), 3, 5)),
] ]

View File

@ -7,7 +7,7 @@
(1.0, Item("common.items.utility.firework_white")), (1.0, Item("common.items.utility.firework_white")),
(1.0, Item("common.items.utility.firework_yellow")), (1.0, Item("common.items.utility.firework_yellow")),
// Potions // Potions
(10.0, ItemQuantity("common.items.consumable.potion_minor", 2, 5)), (10.0, MultiDrop(Item("common.items.consumable.potion_minor"), 2, 5)),
// Misc // Misc
(5.0, Item("common.items.utility.collar")), (5.0, Item("common.items.utility.collar")),
(5.0, Item("common.items.utility.bomb")), (5.0, Item("common.items.utility.bomb")),

View File

@ -1,4 +1,4 @@
[ [
(1.5, ItemQuantity("common.items.crafting_ing.hide.carapace", 2, 5)), (1.5, MultiDrop(Item("common.items.crafting_ing.hide.carapace"), 2, 5)),
(1.0, ItemQuantity("common.items.crafting_ing.animal_misc.strong_pincer", 1, 2)), (1.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.strong_pincer"), 1, 2)),
] ]

View File

@ -1,5 +1,5 @@
[ [
(2.0, ItemQuantity("common.items.crafting_ing.sticky_thread", 2, 5)), (2.0, MultiDrop(Item("common.items.crafting_ing.sticky_thread"), 2, 5)),
(1.0, Item("common.items.crafting_ing.animal_misc.venom_sac")), (1.0, Item("common.items.crafting_ing.animal_misc.venom_sac")),
(1.0, ItemQuantity("common.items.crafting_ing.animal_misc.strong_pincer", 1, 2)), (1.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.strong_pincer"), 1, 2)),
] ]

View File

@ -1,4 +1,4 @@
[ [
(1.0, ItemQuantity("common.items.crafting_ing.hide.carapace", 1, 3)), (1.0, MultiDrop(Item("common.items.crafting_ing.hide.carapace"), 1, 3)),
(1.0, Item("common.items.crafting_ing.animal_misc.strong_pincer")), (1.0, Item("common.items.crafting_ing.animal_misc.strong_pincer")),
] ]

View File

@ -1,4 +1,4 @@
[ [
(2.0, ItemQuantity("common.items.flowers.plant_fiber", 1, 3)), (2.0, MultiDrop(Item("common.items.flowers.plant_fiber"), 1, 3)),
(1.0, ItemQuantity("common.items.crafting_ing.animal_misc.viscous_ooze", 1, 1)), (1.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.viscous_ooze"), 1, 1)),
] ]

View File

@ -1,5 +1,5 @@
[ [
(1.0, ItemQuantity("common.items.flowers.plant_fiber", 1, 3)), (1.0, MultiDrop(Item("common.items.flowers.plant_fiber"), 1, 3)),
(1.0, Item("common.items.crafting_ing.animal_misc.strong_pincer")), (1.0, Item("common.items.crafting_ing.animal_misc.strong_pincer")),
(0.5, ItemQuantity("common.items.crafting_ing.animal_misc.viscous_ooze", 1, 1)), (0.5, MultiDrop(Item("common.items.crafting_ing.animal_misc.viscous_ooze"), 1, 1)),
] ]

View File

@ -1,4 +1,4 @@
[ [
(1.0, ItemQuantity("common.items.crafting_ing.sticky_thread", 1, 3)), (1.0, MultiDrop(Item("common.items.crafting_ing.sticky_thread"), 1, 3)),
(0.5, ItemQuantity("common.items.crafting_ing.animal_misc.viscous_ooze", 1, 1)), (0.5, MultiDrop(Item("common.items.crafting_ing.animal_misc.viscous_ooze"), 1, 1)),
] ]

View File

@ -1,4 +1,4 @@
[ [
(1.0, ItemQuantity("common.items.crafting_ing.sticky_thread", 2, 5)), (1.0, MultiDrop(Item("common.items.crafting_ing.sticky_thread"), 2, 5)),
(1.0, ItemQuantity("common.items.crafting_ing.animal_misc.strong_pincer", 1, 2)), (1.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.strong_pincer"), 1, 2)),
] ]

View File

@ -1,5 +1,5 @@
[ [
(2.0, ItemQuantity("common.items.crafting_ing.sticky_thread", 1, 3)), (2.0, MultiDrop(Item("common.items.crafting_ing.sticky_thread"), 1, 3)),
(1.0, Item("common.items.crafting_ing.animal_misc.venom_sac")), (1.0, Item("common.items.crafting_ing.animal_misc.venom_sac")),
(1.0, Item("common.items.crafting_ing.animal_misc.strong_pincer")), (1.0, Item("common.items.crafting_ing.animal_misc.strong_pincer")),
] ]

View File

@ -1,4 +1,4 @@
[ [
(1.0, ItemQuantity("common.items.crafting_ing.sticky_thread", 1, 3)), (1.0, MultiDrop(Item("common.items.crafting_ing.sticky_thread"), 1, 3)),
(1.0, Item("common.items.crafting_ing.animal_misc.strong_pincer")), (1.0, Item("common.items.crafting_ing.animal_misc.strong_pincer")),
] ]

View File

@ -1,4 +1,4 @@
[ [
(1.0, Item("common.items.crafting_ing.hide.rugged_hide")), (1.0, Item("common.items.crafting_ing.hide.rugged_hide")),
(1.0, ItemQuantity("common.items.crafting_ing.animal_misc.long_tusk", 1, 2)), (1.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.long_tusk"), 1, 2)),
] ]

View File

@ -1,6 +1,6 @@
[ [
(1.0, ItemQuantity("common.items.food.meat.bird_large_raw", 1, 2)), (1.0, MultiDrop(Item("common.items.food.meat.bird_large_raw"), 1, 2)),
(1.0, ItemQuantity("common.items.food.meat.beast_large_raw", 2, 2)), (1.0, MultiDrop(Item("common.items.food.meat.beast_large_raw"), 2, 2)),
(2.0, ItemQuantity("common.items.crafting_ing.animal_misc.elegant_crest", 1, 3)), (2.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.elegant_crest"), 1, 3)),
(1.0, ItemQuantity("common.items.crafting_ing.hide.scales", 2, 6)), (1.0, MultiDrop(Item("common.items.crafting_ing.hide.scales"), 2, 6)),
] ]

View File

@ -1,5 +1,5 @@
[ [
(1.0, ItemQuantity("common.items.food.meat.bird_large_raw", 1, 2)), (1.0, MultiDrop(Item("common.items.food.meat.bird_large_raw"), 1, 2)),
(1.0, ItemQuantity("common.items.food.meat.beast_large_raw", 2, 2)), (1.0, MultiDrop(Item("common.items.food.meat.beast_large_raw"), 2, 2)),
(2.0, ItemQuantity("common.items.crafting_ing.animal_misc.elegant_crest", 1, 3)), (2.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.elegant_crest"), 1, 3)),
] ]

View File

@ -1,6 +1,6 @@
[ [
(1.0, ItemQuantity("common.items.food.meat.bird_large_raw", 1, 2)), (1.0, MultiDrop(Item("common.items.food.meat.bird_large_raw"), 1, 2)),
(1.0, ItemQuantity("common.items.food.meat.beast_large_raw", 2, 2)), (1.0, MultiDrop(Item("common.items.food.meat.beast_large_raw"), 2, 2)),
(2.0, ItemQuantity("common.items.crafting_ing.animal_misc.elegant_crest", 1, 3)), (2.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.elegant_crest"), 1, 3)),
(1.0, ItemQuantity("common.items.crafting_ing.hide.scales", 2, 6)), (1.0, MultiDrop(Item("common.items.crafting_ing.hide.scales"), 2, 6)),
] ]

View File

@ -1,5 +1,5 @@
[ [
(1.0, Item("common.items.food.meat.beast_large_raw")), (1.0, Item("common.items.food.meat.beast_large_raw")),
(1.0, Item("common.items.crafting_ing.hide.animal_hide")), (1.0, Item("common.items.crafting_ing.hide.animal_hide")),
(1.0, ItemQuantity("common.items.crafting_ing.animal_misc.fur", 1, 2)), (1.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.fur"), 1, 2)),
] ]

View File

@ -1,4 +1,4 @@
[ [
(1.0, Item("common.items.crafting_ing.hide.animal_hide")), (1.0, Item("common.items.crafting_ing.hide.animal_hide")),
(1.0, ItemQuantity("common.items.crafting_ing.animal_misc.icy_fang", 1, 2)), (1.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.icy_fang"), 1, 2)),
] ]

View File

@ -1,4 +1,4 @@
[ [
(2.0, ItemQuantity("common.items.crafting_ing.hide.rugged_hide", 1, 2)), (2.0, MultiDrop(Item("common.items.crafting_ing.hide.rugged_hide"), 1, 2)),
(1.0, ItemQuantity("common.items.crafting_ing.animal_misc.long_tusk", 2, 2)), (1.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.long_tusk"), 2, 2)),
] ]

View File

@ -1,5 +1,5 @@
[ [
(2.0, Item("common.items.crafting_ing.hide.rugged_hide")), (2.0, Item("common.items.crafting_ing.hide.rugged_hide")),
(1.0, ItemQuantity("common.items.crafting_ing.animal_misc.large_horn", 1, 2)), (1.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.large_horn"), 1, 2)),
(1.0, ItemQuantity("common.items.crafting_ing.animal_misc.long_tusk", 2, 2)), (1.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.long_tusk"), 2, 2)),
] ]

View File

@ -1,5 +1,5 @@
[ [
(2.0, ItemQuantity("common.items.crafting_ing.hide.rugged_hide", 1, 3)), (2.0, MultiDrop(Item("common.items.crafting_ing.hide.rugged_hide"), 1, 3)),
(1.0, ItemQuantity("common.items.crafting_ing.animal_misc.icy_fang", 2, 4)), (1.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.icy_fang"), 2, 4)),
(1.0, ItemQuantity("common.items.crafting_ing.animal_misc.long_tusk", 2, 2)), (1.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.long_tusk"), 2, 2)),
] ]

View File

@ -2,5 +2,5 @@
(1.5, Item("common.items.food.meat.beast_small_raw")), (1.5, Item("common.items.food.meat.beast_small_raw")),
(0.5, Item("common.items.food.meat.beast_large_raw")), (0.5, Item("common.items.food.meat.beast_large_raw")),
(1.0, Item("common.items.crafting_ing.hide.animal_hide")), (1.0, Item("common.items.crafting_ing.hide.animal_hide")),
(5.0, ItemQuantity("common.items.crafting_ing.cloth.wool", 2, 5)), (5.0, MultiDrop(Item("common.items.crafting_ing.cloth.wool"), 2, 5)),
] ]

View File

@ -1,4 +1,4 @@
[ [
(1.0, ItemQuantity("common.items.crafting_ing.animal_misc.fur", 1, 3)), (1.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.fur"), 1, 3)),
(0.25, LootTable("common.loot_tables.creature.quad_small.generic")), (0.25, LootTable("common.loot_tables.creature.quad_small.generic")),
] ]

View File

@ -1,4 +1,4 @@
[ [
(1.0, ItemQuantity("common.items.crafting_ing.hide.animal_hide", 1, 2)), (1.0, MultiDrop(Item("common.items.crafting_ing.hide.animal_hide"), 1, 2)),
(0.25, Item("common.items.food.meat.beast_small_raw")), (0.25, Item("common.items.food.meat.beast_small_raw")),
] ]

View File

@ -1,4 +1,4 @@
[ [
(9.0, ItemQuantity("common.items.food.mushroom", 2, 4)), (9.0, MultiDrop(Item("common.items.food.mushroom"), 2, 4)),
(1.0, Item("common.items.crafting_ing.animal_misc.long_tusk")), (1.0, Item("common.items.crafting_ing.animal_misc.long_tusk")),
] ]

View File

@ -1,4 +1,4 @@
[ [
(1.0, ItemQuantity("common.items.crafting_ing.cloth.wool", 2, 5)), (1.0, MultiDrop(Item("common.items.crafting_ing.cloth.wool"), 2, 5)),
(0.25, Item("common.items.food.meat.beast_small_raw")), (0.25, Item("common.items.food.meat.beast_small_raw")),
] ]

View File

@ -1,5 +1,5 @@
[ [
(4.0, ItemQuantity("common.items.crafting_ing.animal_misc.elegant_crest", 3, 6)), (4.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.elegant_crest"), 3, 6)),
(1.0, Item("common.items.crafting_ing.animal_misc.raptor_feather")), (1.0, Item("common.items.crafting_ing.animal_misc.raptor_feather")),
(0.5, Item("common.items.weapons.hammer.burnt_drumstick")), (0.5, Item("common.items.weapons.hammer.burnt_drumstick")),
] ]

View File

@ -1,4 +1,4 @@
[ [
(1.0, Item("common.items.crafting_ing.hide.rugged_hide")), (1.0, Item("common.items.crafting_ing.hide.rugged_hide")),
(1.0, ItemQuantity("common.items.crafting_ing.animal_misc.long_tusk", 2, 2)), (1.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.long_tusk"), 2, 2)),
] ]

View File

@ -1,4 +1,4 @@
[ [
(1.0, ItemQuantity("common.items.crafting_ing.hide.plate", 1, 3)), (1.0, MultiDrop(Item("common.items.crafting_ing.hide.plate"), 1, 3)),
(1.0, Item("common.items.crafting_ing.animal_misc.large_horn")), (1.0, Item("common.items.crafting_ing.animal_misc.large_horn")),
] ]

View File

@ -1,5 +1,5 @@
[ [
(1.0, ItemQuantity("common.items.crafting_ing.animal_misc.elegant_crest", 1, 1)), (1.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.elegant_crest"), 1, 1)),
(2.0, ItemQuantity("common.items.crafting_ing.animal_misc.raptor_feather", 2, 6)), (2.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.raptor_feather"), 2, 6)),
(1.0, ItemQuantity("common.items.crafting_ing.hide.scales", 1, 2)), (1.0, MultiDrop(Item("common.items.crafting_ing.hide.scales"), 1, 2)),
] ]

View File

@ -1,4 +1,4 @@
[ [
(1.0, ItemQuantity("common.items.crafting_ing.animal_misc.raptor_feather", 1, 2)), (1.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.raptor_feather"), 1, 2)),
(2.0, ItemQuantity("common.items.crafting_ing.hide.scales", 2, 6)), (2.0, MultiDrop(Item("common.items.crafting_ing.hide.scales"), 2, 6)),
] ]

View File

@ -1,6 +1,6 @@
[ [
(0.5, Item("common.items.crafting_ing.abyssal_heart")), (0.5, Item("common.items.crafting_ing.abyssal_heart")),
(2.0, Item("common.items.crafting_ing.coral_branch")), (2.0, Item("common.items.crafting_ing.coral_branch")),
(2.5, ItemQuantity("common.items.utility.coins", 50, 100)), (2.5, MultiDrop(Item("common.items.utility.coins"), 50, 100)),
(0.2, Item("common.items.tool.instruments.glass_flute")), (0.2, Item("common.items.tool.instruments.glass_flute")),
] ]

View File

@ -3,12 +3,12 @@
(1.0, LootTable("common.loot_tables.weapons.components.tier-0")), (1.0, LootTable("common.loot_tables.weapons.components.tier-0")),
(1.0, LootTable("common.loot_tables.armor.tier-0")), (1.0, LootTable("common.loot_tables.armor.tier-0")),
// Currency // Currency
(3.0, ItemQuantity("common.items.utility.coins", 10, 20)), (3.0, MultiDrop(Item("common.items.utility.coins"), 10, 20)),
// Materials // Materials
(1.0, ItemQuantity("common.items.crafting_ing.cloth.linen", 3, 10)), (1.0, MultiDrop(Item("common.items.crafting_ing.cloth.linen"), 3, 10)),
(1.0, ItemQuantity("common.items.crafting_ing.leather.simple_leather", 3, 10)), (1.0, MultiDrop(Item("common.items.crafting_ing.leather.simple_leather"), 3, 10)),
(1.0, ItemQuantity("common.items.mineral.ingot.bronze", 3, 10)), (1.0, MultiDrop(Item("common.items.mineral.ingot.bronze"), 3, 10)),
(1.0, ItemQuantity("common.items.log.wood", 3, 10)), (1.0, MultiDrop(Item("common.items.log.wood"), 3, 10)),
// Consumables // Consumables
(2.0, LootTable("common.loot_tables.consumable.poor")), (2.0, LootTable("common.loot_tables.consumable.poor")),
] ]

View File

@ -9,7 +9,7 @@
// Chieftain Mask // Chieftain Mask
(1.0, Item("common.items.armor.misc.head.gnarling_mask")), (1.0, Item("common.items.armor.misc.head.gnarling_mask")),
// Indirect crafting materials for T2 gear // Indirect crafting materials for T2 gear
(1.0, ItemQuantity("common.items.crafting_ing.sticky_thread", 2, 5)), (1.0, MultiDrop(Item("common.items.crafting_ing.sticky_thread"), 2, 5)),
(1.0, ItemQuantity("common.items.mineral.ore.coal", 2, 5)), (1.0, MultiDrop(Item("common.items.mineral.ore.coal"), 2, 5)),
(1.0, ItemQuantity("common.items.crafting_ing.leather.leather_strips", 2, 5)), (1.0, MultiDrop(Item("common.items.crafting_ing.leather.leather_strips"), 2, 5)),
] ]

View File

@ -1,6 +1,6 @@
[ [
// Currency // Currency
(1.0, ItemQuantity("common.items.utility.coins", 2, 4)), (1.0, MultiDrop(Item("common.items.utility.coins"), 2, 4)),
// Food // Food
(1.0, LootTable("common.loot_tables.food.wild_ingredients")), (1.0, LootTable("common.loot_tables.food.wild_ingredients")),
// Nothing // Nothing

View File

@ -1,4 +1,4 @@
[ [
(1.0, LootTable("common.loot_tables.dungeon.tier-0.enemy")), (1.0, LootTable("common.loot_tables.dungeon.tier-0.enemy")),
(1.0, ItemQuantity("common.items.flowers.plant_fiber", 2, 4)), (1.0, MultiDrop(Item("common.items.flowers.plant_fiber"), 2, 4)),
] ]

View File

@ -1,11 +1,11 @@
[ [
// Currency // Currency
(1.0, ItemQuantity("common.items.utility.coins", 10, 20)), (1.0, MultiDrop(Item("common.items.utility.coins"), 10, 20)),
// Food // Food
(1.0, LootTable("common.loot_tables.food.wild_ingredients")), (1.0, LootTable("common.loot_tables.food.wild_ingredients")),
// Consumables // Consumables
(2.0, LootTable("common.loot_tables.consumable.poor")), (2.0, LootTable("common.loot_tables.consumable.poor")),
// Crafting ingredients // Crafting ingredients
(1.0, ItemQuantity("common.items.log.wood", 5, 10)), (1.0, MultiDrop(Item("common.items.log.wood"), 5, 10)),
(0.5, LootTable("common.loot_tables.weapons.components.secondary.sceptre")), (0.5, LootTable("common.loot_tables.weapons.components.secondary.sceptre")),
] ]

View File

@ -4,12 +4,12 @@
(1.0, LootTable("common.loot_tables.armor.tier-1")), (1.0, LootTable("common.loot_tables.armor.tier-1")),
(0.5, Item("common.items.armor.misc.head.hog_hood")), (0.5, Item("common.items.armor.misc.head.hog_hood")),
// Currency // Currency
(3.0, ItemQuantity("common.items.utility.coins", 20, 50)), (3.0, MultiDrop(Item("common.items.utility.coins"), 20, 50)),
// Materials // Materials
(1.0, ItemQuantity("common.items.crafting_ing.cloth.wool", 3, 10)), (1.0, MultiDrop(Item("common.items.crafting_ing.cloth.wool"), 3, 10)),
(1.0, ItemQuantity("common.items.crafting_ing.leather.thick_leather", 3, 10)), (1.0, MultiDrop(Item("common.items.crafting_ing.leather.thick_leather"), 3, 10)),
(1.0, ItemQuantity("common.items.mineral.ingot.iron", 3, 10)), (1.0, MultiDrop(Item("common.items.mineral.ingot.iron"), 3, 10)),
(1.0, ItemQuantity("common.items.log.bamboo", 3, 10)), (1.0, MultiDrop(Item("common.items.log.bamboo"), 3, 10)),
// Consumables // Consumables
(2.0, LootTable("common.loot_tables.consumable.poor")), (2.0, LootTable("common.loot_tables.consumable.poor")),
] ]

View File

@ -1,6 +1,6 @@
[ [
// Currency // Currency
(1.0, ItemQuantity("common.items.utility.coins", 4, 10)), (1.0, MultiDrop(Item("common.items.utility.coins"), 4, 10)),
// Food // Food
(1.0, LootTable("common.loot_tables.food.wild_ingredients")), (1.0, LootTable("common.loot_tables.food.wild_ingredients")),
// Nothing // Nothing

View File

@ -3,12 +3,12 @@
(1.0, LootTable("common.loot_tables.weapons.components.tier-2")), (1.0, LootTable("common.loot_tables.weapons.components.tier-2")),
(1.0, LootTable("common.loot_tables.armor.tier-2")), (1.0, LootTable("common.loot_tables.armor.tier-2")),
// Currency // Currency
(3.0, ItemQuantity("common.items.utility.coins", 50, 100)), (3.0, MultiDrop(Item("common.items.utility.coins"), 50, 100)),
// Materials // Materials
(1.0, ItemQuantity("common.items.crafting_ing.cloth.silk", 3, 10)), (1.0, MultiDrop(Item("common.items.crafting_ing.cloth.silk"), 3, 10)),
(1.0, ItemQuantity("common.items.crafting_ing.hide.scales", 3, 10)), (1.0, MultiDrop(Item("common.items.crafting_ing.hide.scales"), 3, 10)),
(1.0, ItemQuantity("common.items.mineral.ingot.steel", 3, 10)), (1.0, MultiDrop(Item("common.items.mineral.ingot.steel"), 3, 10)),
(1.0, ItemQuantity("common.items.log.hardwood", 3, 10)), (1.0, MultiDrop(Item("common.items.log.hardwood"), 3, 10)),
// Consumables // Consumables
(2.0, LootTable("common.loot_tables.consumable.moderate")), (2.0, LootTable("common.loot_tables.consumable.moderate")),
] ]

View File

@ -1,6 +1,6 @@
[ [
// Currency // Currency
(1.0, ItemQuantity("common.items.utility.coins", 10, 20)), (1.0, MultiDrop(Item("common.items.utility.coins"), 10, 20)),
// Food // Food
(1.0, LootTable("common.loot_tables.food.wild_ingredients")), (1.0, LootTable("common.loot_tables.food.wild_ingredients")),
// Nothing // Nothing

View File

@ -3,13 +3,13 @@
(1.0, LootTable("common.loot_tables.weapons.components.tier-3")), (1.0, LootTable("common.loot_tables.weapons.components.tier-3")),
(1.0, LootTable("common.loot_tables.armor.tier-3")), (1.0, LootTable("common.loot_tables.armor.tier-3")),
// Currency // Currency
(3.0, ItemQuantity("common.items.utility.coins", 100, 200)), (3.0, MultiDrop(Item("common.items.utility.coins"), 100, 200)),
// Materials // Materials
// Allow ironwood to have higher drops till entity droppers are implemented // Allow ironwood to have higher drops till entity droppers are implemented
(1.0, ItemQuantity("common.items.crafting_ing.cloth.lifecloth", 2, 6)), (1.0, MultiDrop(Item("common.items.crafting_ing.cloth.lifecloth"), 2, 6)),
(1.0, ItemQuantity("common.items.crafting_ing.hide.carapace", 2, 6)), (1.0, MultiDrop(Item("common.items.crafting_ing.hide.carapace"), 2, 6)),
(1.0, ItemQuantity("common.items.mineral.ingot.cobalt", 2, 6)), (1.0, MultiDrop(Item("common.items.mineral.ingot.cobalt"), 2, 6)),
(1.0, ItemQuantity("common.items.log.ironwood", 3, 7)), (1.0, MultiDrop(Item("common.items.log.ironwood"), 3, 7)),
// Consumables // Consumables
(2.0, LootTable("common.loot_tables.consumable.moderate")), (2.0, LootTable("common.loot_tables.consumable.moderate")),
] ]

View File

@ -1,6 +1,6 @@
[ [
// Currency // Currency
(1.0, ItemQuantity("common.items.utility.coins", 20, 40)), (1.0, MultiDrop(Item("common.items.utility.coins"), 20, 40)),
// Food // Food
(1.0, LootTable("common.loot_tables.food.prepared")), (1.0, LootTable("common.loot_tables.food.prepared")),
// Nothing // Nothing

View File

@ -8,8 +8,8 @@
(1.0, LootTable("common.loot_tables.weapons.legendary_melee")), (1.0, LootTable("common.loot_tables.weapons.legendary_melee")),
// Crafting material // Crafting material
// Allow for DS and Eldwood to have higher drops till entity droppers are implemented // Allow for DS and Eldwood to have higher drops till entity droppers are implemented
(1.0, ItemQuantity("common.items.crafting_ing.cloth.sunsilk", 1, 3)), (1.0, MultiDrop(Item("common.items.crafting_ing.cloth.sunsilk"), 1, 3)),
(1.0, ItemQuantity("common.items.crafting_ing.hide.dragon_scale", 3, 8)), (1.0, MultiDrop(Item("common.items.crafting_ing.hide.dragon_scale"), 3, 8)),
(1.0, ItemQuantity("common.items.mineral.ingot.orichalcum", 1, 3)), (1.0, MultiDrop(Item("common.items.mineral.ingot.orichalcum"), 1, 3)),
(1.0, ItemQuantity("common.items.log.eldwood", 2, 6)), (1.0, MultiDrop(Item("common.items.log.eldwood"), 2, 6)),
] ]

View File

@ -4,13 +4,13 @@
(1.0, LootTable("common.loot_tables.armor.tier-4")), (1.0, LootTable("common.loot_tables.armor.tier-4")),
(1.0, Item("common.items.armor.misc.head.spikeguard")), (1.0, Item("common.items.armor.misc.head.spikeguard")),
// Currency // Currency
(3.0, ItemQuantity("common.items.utility.coins", 200, 500)), (3.0, MultiDrop(Item("common.items.utility.coins"), 200, 500)),
// Materials // Materials
// Allow frostwood to have higher drops till entity droppers are implemented // Allow frostwood to have higher drops till entity droppers are implemented
(1.0, ItemQuantity("common.items.crafting_ing.cloth.moonweave", 1, 5)), (1.0, MultiDrop(Item("common.items.crafting_ing.cloth.moonweave"), 1, 5)),
(1.0, ItemQuantity("common.items.crafting_ing.hide.plate", 1, 5)), (1.0, MultiDrop(Item("common.items.crafting_ing.hide.plate"), 1, 5)),
(1.0, ItemQuantity("common.items.mineral.ingot.bloodsteel", 1, 5)), (1.0, MultiDrop(Item("common.items.mineral.ingot.bloodsteel"), 1, 5)),
(1.0, ItemQuantity("common.items.log.frostwood", 3, 6)), (1.0, MultiDrop(Item("common.items.log.frostwood"), 3, 6)),
// Consumables // Consumables
(2.0, LootTable("common.loot_tables.consumable.good")), (2.0, LootTable("common.loot_tables.consumable.good")),
] ]

View File

@ -1,6 +1,6 @@
[ [
// Currency // Currency
(1.0, ItemQuantity("common.items.utility.coins", 40, 100)), (1.0, MultiDrop(Item("common.items.utility.coins"), 40, 100)),
// Food // Food
(1.0, LootTable("common.loot_tables.food.prepared")), (1.0, LootTable("common.loot_tables.food.prepared")),
// Nothing // Nothing

View File

@ -1,6 +1,6 @@
[ [
// Currency // Currency
(10.0, ItemQuantity("common.items.utility.coins", 200, 500)), (10.0, MultiDrop(Item("common.items.utility.coins"), 200, 500)),
// Food // Food
(5.0, LootTable("common.loot_tables.food.prepared")), (5.0, LootTable("common.loot_tables.food.prepared")),
// Consumables // Consumables

View File

@ -13,8 +13,8 @@
// Crafting material // Crafting material
// Allow for DS and Eldwood to have higher drops till entity droppers are implemented // Allow for DS and Eldwood to have higher drops till entity droppers are implemented
(1.0, Item("common.items.crafting_ing.mindflayer_bag_damaged")), (1.0, Item("common.items.crafting_ing.mindflayer_bag_damaged")),
(1.0, ItemQuantity("common.items.crafting_ing.cloth.sunsilk", 1, 3)), (1.0, MultiDrop(Item("common.items.crafting_ing.cloth.sunsilk"), 1, 3)),
(1.0, ItemQuantity("common.items.crafting_ing.hide.dragon_scale", 3, 8)), (1.0, MultiDrop(Item("common.items.crafting_ing.hide.dragon_scale"), 3, 8)),
(1.0, ItemQuantity("common.items.mineral.ingot.orichalcum", 1, 3)), (1.0, MultiDrop(Item("common.items.mineral.ingot.orichalcum"), 1, 3)),
(1.0, ItemQuantity("common.items.log.eldwood", 2, 6)), (1.0, MultiDrop(Item("common.items.log.eldwood"), 2, 6)),
] ]

View File

@ -4,13 +4,13 @@
(1.0, LootTable("common.loot_tables.armor.tier-4")), (1.0, LootTable("common.loot_tables.armor.tier-4")),
(0.1, Item("common.items.armor.cultist.bandana")), (0.1, Item("common.items.armor.cultist.bandana")),
// Currency // Currency
(3.0, ItemQuantity("common.items.utility.coins", 200, 500)), (3.0, MultiDrop(Item("common.items.utility.coins"), 200, 500)),
// Materials // Materials
(1.0, ItemQuantity("common.items.crafting_ing.cloth.moonweave", 1, 5)), (1.0, MultiDrop(Item("common.items.crafting_ing.cloth.moonweave"), 1, 5)),
(1.0, ItemQuantity("common.items.crafting_ing.hide.plate", 1, 5)), (1.0, MultiDrop(Item("common.items.crafting_ing.hide.plate"), 1, 5)),
(1.0, ItemQuantity("common.items.mineral.ingot.bloodsteel", 1, 5)), (1.0, MultiDrop(Item("common.items.mineral.ingot.bloodsteel"), 1, 5)),
(1.0, ItemQuantity("common.items.log.frostwood", 1, 5)), (1.0, MultiDrop(Item("common.items.log.frostwood"), 1, 5)),
// Consumables // Consumables
(2.0, LootTable("common.loot_tables.consumable.good")), (2.0, LootTable("common.loot_tables.consumable.good")),
(0.1, ItemQuantity("common.items.food.spore_corruption", 1, 3)), (0.1, MultiDrop(Item("common.items.food.spore_corruption"), 1, 3)),
] ]

View File

@ -1,8 +1,8 @@
[ [
// Currency // Currency
(1.0, ItemQuantity("common.items.utility.coins", 100, 200)), (1.0, MultiDrop(Item("common.items.utility.coins"), 100, 200)),
// Food // Food
(1.0, LootTable("common.loot_tables.food.prepared")), (1.0, LootTable("common.loot_tables.food.prepared")),
// Cheese // Cheese
(2.0, ItemQuantity("common.items.food.cheese", 3, 5)), (2.0, MultiDrop(Item("common.items.food.cheese"), 3, 5)),
] ]

View File

@ -1,6 +1,6 @@
[ [
// Currency // Currency
(10.0, ItemQuantity("common.items.utility.coins", 500, 1000)), (10.0, MultiDrop(Item("common.items.utility.coins"), 500, 1000)),
// Food // Food
(5.0, LootTable("common.loot_tables.food.prepared")), (5.0, LootTable("common.loot_tables.food.prepared")),
// Consumables // Consumables

View File

@ -1,6 +1,6 @@
[ [
// Currency // Currency
(30.0, ItemQuantity("common.items.utility.coins", 50, 100)), (30.0, MultiDrop(Item("common.items.utility.coins"), 50, 100)),
// Nothing // Nothing
(30.0, Nothing), (30.0, Nothing),
// Special // Special

View File

@ -1,10 +1,10 @@
[ [
//Currency //Currency
(4.0, ItemQuantity("common.items.utility.coins", 50, 200)), (4.0, MultiDrop(Item("common.items.utility.coins"), 50, 200)),
//Food //Food
(4.0, LootTable("common.loot_tables.food.prepared")), (4.0, LootTable("common.loot_tables.food.prepared")),
//Flowers, pretty //Flowers, pretty
(2.0, ItemQuantity("common.items.flowers.red", 3, 6)), (2.0, MultiDrop(Item("common.items.flowers.red"), 3, 6)),
//Weapon components //Weapon components
(2.0, LootTable("common.loot_tables.weapons.components.tier-0")), (2.0, LootTable("common.loot_tables.weapons.components.tier-0")),
(1.0, LootTable("common.loot_tables.weapons.components.tier-1")), (1.0, LootTable("common.loot_tables.weapons.components.tier-1")),

View File

@ -1,15 +1,15 @@
[ [
//Currency //Currency
(3.0, ItemQuantity("common.items.utility.coins", 50, 200)), (3.0, MultiDrop(Item("common.items.utility.coins"), 50, 200)),
(2.0, ItemQuantity("common.items.utility.coins", 200, 500)), (2.0, MultiDrop(Item("common.items.utility.coins"), 200, 500)),
//Food //Food
(3.0, LootTable("common.loot_tables.food.prepared")), (3.0, LootTable("common.loot_tables.food.prepared")),
//Ores //Ores
(2.0, ItemQuantity("common.items.mineral.ore.iron", 2, 7)), (2.0, MultiDrop(Item("common.items.mineral.ore.iron"), 2, 7)),
//Hides //Hides
(2.0, ItemQuantity("common.items.crafting_ing.hide.animal_hide", 5, 15)), (2.0, MultiDrop(Item("common.items.crafting_ing.hide.animal_hide"), 5, 15)),
//Flowers, pretty //Flowers, pretty
(2.0, ItemQuantity("common.items.flowers.red", 3, 6)), (2.0, MultiDrop(Item("common.items.flowers.red"), 3, 6)),
//Weapon components //Weapon components
(1.0, LootTable("common.loot_tables.weapons.components.tier-2")), (1.0, LootTable("common.loot_tables.weapons.components.tier-2")),
(1.0, LootTable("common.loot_tables.weapons.components.tier-3")), (1.0, LootTable("common.loot_tables.weapons.components.tier-3")),

View File

@ -1,21 +1,21 @@
[ [
//Currency //Currency
(3.0, ItemQuantity("common.items.utility.coins", 200, 500)), (3.0, MultiDrop(Item("common.items.utility.coins"), 200, 500)),
(2.0, ItemQuantity("common.items.utility.coins", 2000, 5000)), (2.0, MultiDrop(Item("common.items.utility.coins"), 2000, 5000)),
//Food //Food
(3.0, LootTable("common.loot_tables.food.prepared")), (3.0, LootTable("common.loot_tables.food.prepared")),
//Ores //Ores
(2.0, ItemQuantity("common.items.mineral.ore.coal", 5, 15)), (2.0, MultiDrop(Item("common.items.mineral.ore.coal"), 5, 15)),
(2.0, ItemQuantity("common.items.mineral.ore.iron", 2, 7)), (2.0, MultiDrop(Item("common.items.mineral.ore.iron"), 2, 7)),
(1.0, ItemQuantity("common.items.mineral.ore.cobalt", 2, 5)), (1.0, MultiDrop(Item("common.items.mineral.ore.cobalt"), 2, 5)),
(0.1, ItemQuantity("common.items.mineral.gem.diamond", 2, 5)), (0.1, MultiDrop(Item("common.items.mineral.gem.diamond"), 2, 5)),
//Hides //Hides
(2.0, ItemQuantity("common.items.crafting_ing.hide.animal_hide", 5, 15)), (2.0, MultiDrop(Item("common.items.crafting_ing.hide.animal_hide"), 5, 15)),
(2.0, ItemQuantity("common.items.crafting_ing.hide.scales", 5, 15)), (2.0, MultiDrop(Item("common.items.crafting_ing.hide.scales"), 5, 15)),
(1.0, ItemQuantity("common.items.crafting_ing.hide.carapace", 3, 10)), (1.0, MultiDrop(Item("common.items.crafting_ing.hide.carapace"), 3, 10)),
(1.0, ItemQuantity("common.items.crafting_ing.hide.tough_hide", 3, 10)), (1.0, MultiDrop(Item("common.items.crafting_ing.hide.tough_hide"), 3, 10)),
//Flowers, very pretty //Flowers, very pretty
(2.0, ItemQuantity("common.items.flowers.red", 5, 10)), (2.0, MultiDrop(Item("common.items.flowers.red"), 5, 10)),
//Weapon components //Weapon components
(1.0, LootTable("common.loot_tables.weapons.components.tier-2")), (1.0, LootTable("common.loot_tables.weapons.components.tier-2")),
(1.0, LootTable("common.loot_tables.weapons.components.tier-3")), (1.0, LootTable("common.loot_tables.weapons.components.tier-3")),

View File

@ -1,25 +1,25 @@
[ [
//Currency //Currency
(2.0, ItemQuantity("common.items.utility.coins", 200, 500)), (2.0, MultiDrop(Item("common.items.utility.coins"), 200, 500)),
(1.0, ItemQuantity("common.items.utility.coins", 2000, 5000)), (1.0, MultiDrop(Item("common.items.utility.coins"), 2000, 5000)),
//Food //Food
(4.0, LootTable("common.loot_tables.food.prepared")), (4.0, LootTable("common.loot_tables.food.prepared")),
//Ores //Ores
(2.0, ItemQuantity("common.items.mineral.ore.coal", 10, 30)), (2.0, MultiDrop(Item("common.items.mineral.ore.coal"), 10, 30)),
(2.0, ItemQuantity("common.items.mineral.ore.iron", 4, 14)), (2.0, MultiDrop(Item("common.items.mineral.ore.iron"), 4, 14)),
(1.0, ItemQuantity("common.items.mineral.ore.cobalt", 4, 10)), (1.0, MultiDrop(Item("common.items.mineral.ore.cobalt"), 4, 10)),
(0.1, ItemQuantity("common.items.mineral.gem.diamond", 4, 10)), (0.1, MultiDrop(Item("common.items.mineral.gem.diamond"), 4, 10)),
//Hides //Hides
(2.0, ItemQuantity("common.items.crafting_ing.hide.animal_hide", 10, 30)), (2.0, MultiDrop(Item("common.items.crafting_ing.hide.animal_hide"), 10, 30)),
(2.0, ItemQuantity("common.items.crafting_ing.hide.scales", 10, 30)), (2.0, MultiDrop(Item("common.items.crafting_ing.hide.scales"), 10, 30)),
(1.0, ItemQuantity("common.items.crafting_ing.hide.carapace", 6, 20)), (1.0, MultiDrop(Item("common.items.crafting_ing.hide.carapace"), 6, 20)),
(1.0, ItemQuantity("common.items.crafting_ing.hide.tough_hide", 6, 20)), (1.0, MultiDrop(Item("common.items.crafting_ing.hide.tough_hide"), 6, 20)),
(0.3, ItemQuantity("common.items.crafting_ing.hide.plate", 1, 8)), (0.3, MultiDrop(Item("common.items.crafting_ing.hide.plate"), 1, 8)),
(0.1, ItemQuantity("common.items.crafting_ing.hide.rugged_hide", 1, 6)), (0.1, MultiDrop(Item("common.items.crafting_ing.hide.rugged_hide"), 1, 6)),
//Flowers, very pretty //Flowers, very pretty
(3.0, ItemQuantity("common.items.flowers.red", 10, 20)), (3.0, MultiDrop(Item("common.items.flowers.red"), 10, 20)),
(2.0, ItemQuantity("common.items.flowers.moonbell", 6, 12)), (2.0, MultiDrop(Item("common.items.flowers.moonbell"), 6, 12)),
(1.0, ItemQuantity("common.items.flowers.pyrebloom", 3, 6)), (1.0, MultiDrop(Item("common.items.flowers.pyrebloom"), 3, 6)),
//Weapon components //Weapon components
(1.5, LootTable("common.loot_tables.weapons.components.tier-4")), (1.5, LootTable("common.loot_tables.weapons.components.tier-4")),
(0.2, LootTable("common.loot_tables.weapons.components.tier-5")), (0.2, LootTable("common.loot_tables.weapons.components.tier-5")),

View File

@ -1,4 +1,4 @@
[ [
(1.0, LootTable("common.loot_tables.armor.boreal")), (1.0, LootTable("common.loot_tables.armor.boreal")),
(1.0, ItemQuantity("common.items.crafting_ing.glacial_crystal", 5, 15)), (1.0, MultiDrop(Item("common.items.crafting_ing.glacial_crystal"), 5, 15)),
] ]

View File

@ -1,6 +1,6 @@
[ [
// Currency // Currency
(1.0, ItemQuantity("common.items.utility.coins", 40, 100)), (1.0, MultiDrop(Item("common.items.utility.coins"), 40, 100)),
// Food // Food
(1.0, LootTable("common.loot_tables.food.prepared")), (1.0, LootTable("common.loot_tables.food.prepared")),
// Nothing // Nothing

View File

@ -102,7 +102,7 @@ pub enum Event {
}, },
Disconnect, Disconnect,
DisconnectionNotification(u64), DisconnectionNotification(u64),
InventoryUpdated(InventoryUpdateEvent), InventoryUpdated(Vec<InventoryUpdateEvent>),
Kicked(String), Kicked(String),
Notification(Notification), Notification(Notification),
SetViewDistance(u32), SetViewDistance(u32),
@ -2388,11 +2388,16 @@ impl Client {
self.presence = None; self.presence = None;
self.clean_state(); self.clean_state();
}, },
ServerGeneral::InventoryUpdate(inventory, event) => { ServerGeneral::InventoryUpdate(inventory, events) => {
let mut update_inventory = false;
for event in events.iter() {
match event { match event {
InventoryUpdateEvent::BlockCollectFailed { .. } => {}, InventoryUpdateEvent::BlockCollectFailed { .. } => {},
InventoryUpdateEvent::EntityCollectFailed { .. } => {}, InventoryUpdateEvent::EntityCollectFailed { .. } => {},
_ => { _ => update_inventory = true,
}
}
if update_inventory {
// Push the updated inventory component to the client // Push the updated inventory component to the client
// FIXME: Figure out whether this error can happen under normal gameplay, // FIXME: Figure out whether this error can happen under normal gameplay,
// if not find a better way to handle it, if so maybe consider kicking the // if not find a better way to handle it, if so maybe consider kicking the
@ -2410,12 +2415,11 @@ impl Client {
entity was not found... this may be a bug." entity was not found... this may be a bug."
); );
} }
},
} }
self.update_available_recipes(); self.update_available_recipes();
frontend_events.push(Event::InventoryUpdated(event)); frontend_events.push(Event::InventoryUpdated(events));
}, },
ServerGeneral::SetViewDistance(vd) => { ServerGeneral::SetViewDistance(vd) => {
self.view_distance = Some(vd); self.view_distance = Some(vd);

View File

@ -91,6 +91,7 @@ rand_chacha = "0.3"
tracing-subscriber = { version = "0.3.7", default-features = false, features = ["fmt", "time", "ansi", "smallvec", "env-filter"] } tracing-subscriber = { version = "0.3.7", default-features = false, features = ["fmt", "time", "ansi", "smallvec", "env-filter"] }
petgraph = "0.6.0" petgraph = "0.6.0"
[[bench]] [[bench]]
name = "chonk_benchmark" name = "chonk_benchmark"
harness = false harness = false
@ -99,6 +100,10 @@ harness = false
name = "color_benchmark" name = "color_benchmark"
harness = false harness = false
[[bench]]
name = "loot_benchmark"
harness = false
[[bin]] [[bin]]
name = "csv_export" name = "csv_export"
required-features = ["bin_csv"] required-features = ["bin_csv"]

View File

@ -0,0 +1,146 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use rand::thread_rng;
use veloren_common::lottery::distribute_many;
fn criterion_benchmark(c: &mut Criterion) {
let mut c = c.benchmark_group("loot");
c.bench_function("loot distribute 1000 among 10", |b| {
let mut rng = thread_rng();
let v = (1..=10).map(|i| (i as f32 * 10.0, i)).collect::<Vec<_>>();
let items = vec![1, 2, 997];
b.iter(|| {
distribute_many(
black_box(v.iter().copied()),
&mut rng,
black_box(&items),
|i| *i,
|a, b, c| {
black_box((a, b, c));
},
)
})
});
c.bench_function("loot distribute 1000 among 100", |b| {
let mut rng = thread_rng();
let v = (1..=100).map(|i| (i as f32 * 10.0, i)).collect::<Vec<_>>();
let items = vec![1, 2, 997];
b.iter(|| {
distribute_many(
black_box(v.iter().copied()),
&mut rng,
black_box(&items),
|i| *i,
|a, b, c| {
black_box((a, b, c));
},
)
})
});
c.bench_function("loot distribute 10000 among 10", |b| {
let mut rng = thread_rng();
let v = (1..=10).map(|i| (i as f32 * 10.0, i)).collect::<Vec<_>>();
let items = vec![1, 2, 3, 9994];
b.iter(|| {
distribute_many(
black_box(v.iter().copied()),
&mut rng,
black_box(&items),
|i| *i,
|a, b, c| {
black_box((a, b, c));
},
)
})
});
c.bench_function("loot distribute 10000 among 1", |b| {
let mut rng = thread_rng();
let v = (1..=1).map(|i| (i as f32 * 10.0, i)).collect::<Vec<_>>();
let items = vec![1, 2, 3, 9994];
b.iter(|| {
distribute_many(
black_box(v.iter().copied()),
&mut rng,
black_box(&items),
|i| *i,
|a, b, c| {
black_box((a, b, c));
},
)
})
});
c.bench_function("loot distribute 100000 among 20", |b| {
let mut rng = thread_rng();
let v = (1..=20).map(|i| (i as f32 * 10.0, i)).collect::<Vec<_>>();
let items = vec![1, 2, 3, 99994];
b.iter(|| {
distribute_many(
black_box(v.iter().copied()),
&mut rng,
black_box(&items),
|i| *i,
|a, b, c| {
black_box((a, b, c));
},
)
})
});
c.bench_function("loot distribute 1000 among 400", |b| {
let mut rng = thread_rng();
let v = (1..=400).map(|i| (i as f32 * 10.0, i)).collect::<Vec<_>>();
let items = vec![1, 2, 997];
b.iter(|| {
distribute_many(
v.iter().copied(),
&mut rng,
black_box(&items),
|i| *i,
|a, b, c| {
black_box((a, b, c));
},
)
})
});
c.bench_function("loot distribute 1000 among 1000", |b| {
let mut rng = thread_rng();
let v = (1..=1000).map(|i| (i as f32 * 10.0, i)).collect::<Vec<_>>();
let items = vec![1, 2, 997];
b.iter(|| {
distribute_many(
v.iter().copied(),
&mut rng,
black_box(&items),
|i| *i,
|a, b, c| {
black_box((a, b, c));
},
)
})
});
c.bench_function("loot distribute 10000 among 1000", |b| {
let mut rng = thread_rng();
let v = (1..=1000).map(|i| (i as f32 * 10.0, i)).collect::<Vec<_>>();
let items = vec![1, 2, 3, 9994];
b.iter(|| {
distribute_many(
v.iter().copied(),
&mut rng,
black_box(&items),
|i| *i,
|a, b, c| {
black_box((a, b, c));
},
)
})
});
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

View File

@ -170,7 +170,7 @@ pub enum ServerGeneral {
/// Trigger cleanup for when the client goes back to the `Registered` state /// Trigger cleanup for when the client goes back to the `Registered` state
/// from an ingame state /// from an ingame state
ExitInGameSuccess, ExitInGameSuccess,
InventoryUpdate(comp::Inventory, comp::InventoryUpdateEvent), InventoryUpdate(comp::Inventory, Vec<comp::InventoryUpdateEvent>),
/// NOTE: The client can infer that entity view distance will be at most the /// NOTE: The client can infer that entity view distance will be at most the
/// terrain view distance that we send here (and if lower it won't be /// terrain view distance that we send here (and if lower it won't be
/// modified). So we just need to send the terrain VD back to the client /// modified). So we just need to send the terrain VD back to the client

View File

@ -254,31 +254,28 @@ fn loot_table(loot_table: &str) -> Result<(), Box<dyn Error>> {
.div(10_f32.powi(5)) .div(10_f32.powi(5))
.to_string(); .to_string();
fn write_loot_spec<W: std::io::Write>(
wtr: &mut csv::Writer<W>,
loot_spec: &LootSpec<String>,
chance: &str,
) -> Result<(), Box<dyn Error>> {
let get_hands = |hands| match hands { let get_hands = |hands| match hands {
Some(Hands::One) => "One", Some(Hands::One) => "One",
Some(Hands::Two) => "Two", Some(Hands::Two) => "Two",
None => "", None => "",
}; };
match loot_spec {
match item { LootSpec::Item(item) => wtr.write_record([chance, "Item", item, "", ""])?,
LootSpec::Item(item) => wtr.write_record([&chance, "Item", item, "", ""])?,
LootSpec::ItemQuantity(item, lower, upper) => wtr.write_record([
&chance,
"Item",
item,
&lower.to_string(),
&upper.to_string(),
])?,
LootSpec::LootTable(table) => { LootSpec::LootTable(table) => {
wtr.write_record([&chance, "LootTable", table, "", ""])? wtr.write_record([chance, "LootTable", table, "", ""])?
}, },
LootSpec::Nothing => wtr.write_record([&chance, "Nothing", "", ""])?, LootSpec::Nothing => wtr.write_record([chance, "Nothing", "", ""])?,
LootSpec::ModularWeapon { LootSpec::ModularWeapon {
tool, tool,
material, material,
hands, hands,
} => wtr.write_record([ } => wtr.write_record([
&chance, chance,
"Modular Weapon", "Modular Weapon",
&get_tool_kind(tool), &get_tool_kind(tool),
material.into(), material.into(),
@ -289,13 +286,21 @@ fn loot_table(loot_table: &str) -> Result<(), Box<dyn Error>> {
material, material,
hands, hands,
} => wtr.write_record([ } => wtr.write_record([
&chance, chance,
"Modular Weapon Primary Component", "Modular Weapon Primary Component",
&get_tool_kind(tool), &get_tool_kind(tool),
material.into(), material.into(),
get_hands(*hands), get_hands(*hands),
])?, ])?,
LootSpec::MultiDrop(loot, _, _) => {
// TODO: Write amount gotten somewhere?
write_loot_spec(wtr, loot, chance)?;
},
} }
Ok(())
}
write_loot_spec(&mut wtr, item, &chance)?;
} }
wtr.flush()?; wtr.flush()?;
@ -406,16 +411,6 @@ fn entity_drops(entity_config: &str) -> Result<(), Box<dyn Error>> {
"1".to_owned(), "1".to_owned(),
])?; ])?;
}, },
LootSpec::ItemQuantity(item, lower, upper) => {
wtr.write_record(&[
name.clone(),
asset_path.to_owned(),
percent_chance,
item_name(item),
// Tab needed so excel doesn't think it is a date...
format!("{lower}-{upper}\t"),
])?;
},
LootSpec::Nothing => { LootSpec::Nothing => {
wtr.write_record(&[ wtr.write_record(&[
name.clone(), name.clone(),
@ -477,6 +472,7 @@ fn entity_drops(entity_config: &str) -> Result<(), Box<dyn Error>> {
} }
}, },
LootSpec::LootTable(_) => unreachable!(), LootSpec::LootTable(_) => unreachable!(),
LootSpec::MultiDrop(_, _, _) => todo!(),
} }
} }

View File

@ -921,6 +921,34 @@ impl Item {
new_item new_item
} }
pub fn stacked_duplicates<'a>(
&'a self,
ability_map: &'a AbilityMap,
msm: &'a MaterialStatManifest,
count: u32,
) -> impl Iterator<Item = Self> + 'a {
let max_stack_count = count / self.max_amount();
let rest = count % self.max_amount();
(0..max_stack_count)
.map(|_| {
let mut item = self.duplicate(ability_map, msm);
item.set_amount(item.max_amount())
.expect("max_amount() is always a valid amount.");
item
})
.chain((rest > 0).then(move || {
let mut item = self.duplicate(ability_map, msm);
item.set_amount(rest)
.expect("anything less than max_amount() is always a valid amount.");
item
}))
}
/// FIXME: HACK: In order to set the entity ID asynchronously, we currently /// FIXME: HACK: In order to set the entity ID asynchronously, we currently
/// start it at None, and then atomically set it when it's saved for the /// start it at None, and then atomically set it when it's saved for the
/// first time in the database. Because this requires shared mutable /// first time in the database. Because this requires shared mutable
@ -1167,8 +1195,8 @@ impl Item {
pub fn slot_mut(&mut self, slot: usize) -> Option<&mut InvSlot> { self.slots.get_mut(slot) } pub fn slot_mut(&mut self, slot: usize) -> Option<&mut InvSlot> { self.slots.get_mut(slot) }
pub fn try_reclaim_from_block(block: Block) -> Option<Self> { pub fn try_reclaim_from_block(block: Block) -> Option<Vec<(u32, Self)>> {
block.get_sprite()?.collectible_id()??.to_item() block.get_sprite()?.collectible_id()??.to_items()
} }
pub fn ability_spec(&self) -> Option<Cow<AbilitySpec>> { pub fn ability_spec(&self) -> Option<Cow<AbilitySpec>> {
@ -1282,6 +1310,16 @@ impl Item {
} }
} }
pub fn flatten_counted_items<'a>(
items: &'a [(u32, Item)],
ability_map: &'a AbilityMap,
msm: &'a MaterialStatManifest,
) -> impl Iterator<Item = Item> + 'a {
items
.iter()
.flat_map(|(count, item)| item.stacked_duplicates(ability_map, msm, *count))
}
/// Provides common methods providing details about an item definition /// Provides common methods providing details about an item definition
/// for either an `Item` containing the definition, or the actual `ItemDef` /// for either an `Item` containing the definition, or the actual `ItemDef`
pub trait ItemDesc { pub trait ItemDesc {
@ -1370,9 +1408,9 @@ impl Component for Item {
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ItemDrop(pub Item); pub struct ItemDrops(pub Vec<(u32, Item)>);
impl Component for ItemDrop { impl Component for ItemDrops {
type Storage = DenseVecStorage<Self>; type Storage = DenseVecStorage<Self>;
} }

View File

@ -995,13 +995,19 @@ impl Default for InventoryUpdateEvent {
#[derive(Clone, Debug, Default, Serialize, Deserialize)] #[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct InventoryUpdate { pub struct InventoryUpdate {
event: InventoryUpdateEvent, events: Vec<InventoryUpdateEvent>,
} }
impl InventoryUpdate { impl InventoryUpdate {
pub fn new(event: InventoryUpdateEvent) -> Self { Self { event } } pub fn new(event: InventoryUpdateEvent) -> Self {
Self {
events: vec![event],
}
}
pub fn event(&self) -> InventoryUpdateEvent { self.event.clone() } pub fn push(&mut self, event: InventoryUpdateEvent) { self.events.push(event); }
pub fn take_events(&mut self) -> Vec<InventoryUpdateEvent> { std::mem::take(&mut self.events) }
} }
impl Component for InventoryUpdate { impl Component for InventoryUpdate {

View File

@ -362,20 +362,17 @@ impl From<Vec<(f32, LootSpec<String>)>> for ProbabilityFile {
} else { } else {
1.0 / content.iter().map(|e| e.0).sum::<f32>() 1.0 / content.iter().map(|e| e.0).sum::<f32>()
}; };
Self { fn get_content(
content: content rescale: f32,
.into_iter() p0: f32,
.flat_map(|(p0, loot)| match loot { loot: LootSpec<String>,
) -> Vec<(f32, ItemDefinitionIdOwned, f32)> {
match loot {
LootSpec::Item(asset) => { LootSpec::Item(asset) => {
vec![(p0 * rescale, ItemDefinitionIdOwned::Simple(asset), 1.0)] vec![(p0 * rescale, ItemDefinitionIdOwned::Simple(asset), 1.0)]
}, },
LootSpec::ItemQuantity(asset, a, b) => vec![(
p0 * rescale,
ItemDefinitionIdOwned::Simple(asset),
(a + b) as f32 * 0.5,
)],
LootSpec::LootTable(table_asset) => { LootSpec::LootTable(table_asset) => {
let unscaled = &Self::load_expect(&table_asset).read().content; let unscaled = &ProbabilityFile::load_expect(&table_asset).read().content;
let scale = p0 * rescale; let scale = p0 * rescale;
unscaled unscaled
.iter() .iter()
@ -429,7 +426,20 @@ impl From<Vec<(f32, LootSpec<String>)>> for ProbabilityFile {
res res
}, },
LootSpec::Nothing => Vec::new(), LootSpec::Nothing => Vec::new(),
}) LootSpec::MultiDrop(loot, a, b) => {
let average_count = (a + b) as f32 * 0.5;
let mut content = get_content(rescale, p0, *loot);
for (_, _, count) in content.iter_mut() {
*count *= average_count;
}
content
},
}
}
Self {
content: content
.into_iter()
.flat_map(|(p0, loot)| get_content(rescale, p0, loot))
.collect(), .collect(),
} }
} }
@ -1231,12 +1241,4 @@ mod tests {
let probability: ProbabilityFile = loot_table.into(); let probability: ProbabilityFile = loot_table.into();
assert!(normalized(&probability)); assert!(normalized(&probability));
} }
#[test]
fn test_normalizing_table4() {
let quantity = |asset: &str, a, b| LootSpec::ItemQuantity(asset.to_owned(), a, b);
let loot_table = vec![(1.0, quantity("such", 3, 5)), (1.0, quantity("much", 5, 9))];
let probability: ProbabilityFile = loot_table.into();
assert!(normalized(&probability));
}
} }

View File

@ -74,7 +74,7 @@ impl Component for LootOwner {
type Storage = DerefFlaggedStorage<Self, specs::DenseVecStorage<Self>>; type Storage = DerefFlaggedStorage<Self, specs::DenseVecStorage<Self>>;
} }
#[derive(Debug, Copy, Clone, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum LootOwnerKind { pub enum LootOwnerKind {
Player(Uid), Player(Uid),
Group(Group), Group(Group),

View File

@ -92,7 +92,7 @@ pub use self::{
self, self,
item_key::ItemKey, item_key::ItemKey,
tool::{self, AbilityItem}, tool::{self, AbilityItem},
Item, ItemConfig, ItemDrop, Item, ItemConfig, ItemDrops,
}, },
slot, CollectFailedReason, Inventory, InventoryUpdate, InventoryUpdateEvent, slot, CollectFailedReason, Inventory, InventoryUpdate, InventoryUpdateEvent,
}, },

View File

@ -26,6 +26,8 @@
// Cheese drop rate = 3/X = 29.6% // Cheese drop rate = 3/X = 29.6%
// Coconut drop rate = 1/X = 9.85% // Coconut drop rate = 1/X = 9.85%
use std::hash::Hash;
use crate::{ use crate::{
assets::{self, AssetExt}, assets::{self, AssetExt},
comp::{inventory::item, Item}, comp::{inventory::item, Item},
@ -76,12 +78,136 @@ impl<T> Lottery<T> {
pub fn total(&self) -> f32 { self.total } pub fn total(&self) -> f32 { self.total }
} }
/// Try to distribute stacked items fairly between weighted participants.
pub fn distribute_many<T: Copy + Eq + Hash, I>(
participants: impl IntoIterator<Item = (f32, T)>,
rng: &mut impl Rng,
items: &[I],
mut get_amount: impl FnMut(&I) -> u32,
mut exec_item: impl FnMut(&I, T, u32),
) {
struct Participant<T> {
// weight / total
weight: f32,
sorted_weight: f32,
data: T,
recieved_count: u32,
current_recieved_count: u32,
}
impl<T> Participant<T> {
fn give(&mut self, amount: u32) {
self.current_recieved_count += amount;
self.recieved_count += amount;
}
}
// Nothing to distribute, we can return early.
if items.is_empty() {
return;
}
let mut total_weight = 0.0;
let mut participants = participants
.into_iter()
.map(|(weight, participant)| Participant {
weight,
sorted_weight: {
total_weight += weight;
total_weight - weight
},
data: participant,
recieved_count: 0,
current_recieved_count: 0,
})
.collect::<Vec<_>>();
let total_item_amount = items.iter().map(&mut get_amount).sum::<u32>();
let mut current_total_weight = total_weight;
for item in items.iter() {
let amount = get_amount(item);
let mut distributed = 0;
let Some(mut give) = participants
.iter()
.map(|participant| (total_item_amount as f32 * participant.weight / total_weight).ceil() as u32 - participant.recieved_count)
.min() else {
tracing::error!("Tried to distribute items to no participants.");
return;
};
while distributed < amount {
// Can't give more than amount, and don't give more than the average between all
// to keep things well distributed.
let max_give = (amount / participants.len() as u32).clamp(1, amount - distributed);
give = give.clamp(1, max_give);
let x = rng.gen_range(0.0..=current_total_weight);
let index = participants
.binary_search_by(|item| item.sorted_weight.partial_cmp(&x).unwrap())
.unwrap_or_else(|i| i.saturating_sub(1));
let participant_count = participants.len();
let Some(winner) = participants
.get_mut(index) else {
tracing::error!("Tried to distribute items to no participants.");
return;
};
winner.give(give);
distributed += give;
// If a participant has received enough, remove it.
if participant_count > 1
&& winner.recieved_count as f32 / total_item_amount as f32
>= winner.weight / total_weight
{
current_total_weight = index
.checked_sub(1)
.and_then(|i| Some(participants.get(i)?.sorted_weight))
.unwrap_or(0.0);
let winner = participants.swap_remove(index);
exec_item(item, winner.data, winner.current_recieved_count);
// Keep participant weights correct so that we can binary search it.
for participant in &mut participants[index..] {
current_total_weight += participant.weight;
participant.sorted_weight = current_total_weight - participant.weight;
}
// Update max item give amount.
give = participants
.iter()
.map(|participant| {
(total_item_amount as f32 * participant.weight / total_weight).ceil() as u32
- participant.recieved_count
})
.min()
.unwrap_or(0);
} else {
give = give.min(
(total_item_amount as f32 * winner.weight / total_weight).ceil() as u32
- winner.recieved_count,
);
}
}
for participant in participants.iter_mut() {
if participant.current_recieved_count != 0 {
exec_item(item, participant.data, participant.current_recieved_count);
participant.current_recieved_count = 0;
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub enum LootSpec<T: AsRef<str>> { pub enum LootSpec<T: AsRef<str>> {
/// Asset specifier /// Asset specifier
Item(T), Item(T),
/// Asset specifier, lower range, upper range
ItemQuantity(T, u32, u32),
/// Loot table /// Loot table
LootTable(T), LootTable(T),
/// No loot given /// No loot given
@ -97,66 +223,98 @@ pub enum LootSpec<T: AsRef<str>> {
material: item::Material, material: item::Material,
hands: Option<item::tool::Hands>, hands: Option<item::tool::Hands>,
}, },
/// LootSpec, lower range, upper range
MultiDrop(Box<LootSpec<T>>, u32, u32),
} }
impl<T: AsRef<str>> LootSpec<T> { impl<T: AsRef<str>> LootSpec<T> {
pub fn to_item(&self) -> Option<Item> { fn to_items_inner(
let mut rng = thread_rng(); &self,
match self { rng: &mut rand::rngs::ThreadRng,
Self::Item(item) => Item::new_from_asset(item.as_ref()).map_or_else( amount: u32,
items: &mut Vec<(u32, Item)>,
) {
let convert_item = |item: &T| {
Item::new_from_asset(item.as_ref()).map_or_else(
|e| { |e| {
warn!(?e, "error while loading item: {}", item.as_ref()); warn!(?e, "error while loading item: {}", item.as_ref());
None None
}, },
Some, Some,
), )
Self::ItemQuantity(item, lower, upper) => { };
let range = *lower..=*upper; let mut push_item = |mut item: Item, count: u32| {
let quantity = thread_rng().gen_range(range); let count = item.amount().saturating_mul(count);
match Item::new_from_asset(item.as_ref()) { item.set_amount(1).expect("1 is always a valid amount.");
Ok(mut item) => { let hash = item.item_hash();
// TODO: Handle multiple of an item that is unstackable match items.binary_search_by_key(&hash, |(_, item)| item.item_hash()) {
if item.set_amount(quantity).is_err() { Ok(i) => {
warn!("Tried to set quantity on non stackable item"); // Since item hash can collide with other items, we search nearby items with the
} // same hash.
Some(item) // NOTE: The `ParitalEq` implementation for `Item` doesn't compare some data
}, // like durability, or wether slots contain anything. Although since these are
Err(e) => { // Newly loaded items we don't care about comparing those for deduplication
warn!(?e, "error while loading item: {}", item.as_ref()); // here.
None let has_same_hash = |i: &usize| items[*i].1.item_hash() == hash;
}, if let Some(i) = (i..items.len())
.take_while(has_same_hash)
.chain((0..i).rev().take_while(has_same_hash))
.find(|i| items[*i].1 == item)
{
// We saturate at 4 billion items, could use u64 instead if this isn't
// desirable.
items[i].0 = items[i].0.saturating_add(count);
} else {
items.insert(i, (count, item));
} }
}, },
Self::LootTable(table) => Lottery::<LootSpec<String>>::load_expect(table.as_ref()) Err(i) => items.insert(i, (count, item)),
.read() }
.choose() };
.to_item(),
Self::Nothing => None, match self {
Self::Item(item) => {
if let Some(item) = convert_item(item) {
push_item(item, amount);
}
},
Self::LootTable(table) => {
let loot_spec = Lottery::<LootSpec<String>>::load_expect(table.as_ref()).read();
for _ in 0..amount {
loot_spec.choose().to_items_inner(rng, 1, items)
}
},
Self::Nothing => {},
Self::ModularWeapon { Self::ModularWeapon {
tool, tool,
material, material,
hands, hands,
} => item::modular::random_weapon(*tool, *material, *hands, &mut rng).map_or_else( } => {
|e| { for _ in 0..amount {
match item::modular::random_weapon(*tool, *material, *hands, rng) {
Ok(item) => push_item(item, 1),
Err(e) => {
warn!( warn!(
?e, ?e,
"error while creating modular weapon. Toolkind: {:?}, Material: {:?}, \ "error while creating modular weapon. Toolkind: {:?}, Material: \
Hands: {:?}", {:?}, Hands: {:?}",
tool, tool,
material, material,
hands, hands,
); );
None
}, },
Some, }
), }
},
Self::ModularWeaponPrimaryComponent { Self::ModularWeaponPrimaryComponent {
tool, tool,
material, material,
hands, hands,
} => item::modular::random_weapon_primary_component(*tool, *material, *hands, &mut rng) } => {
.map_or_else( for _ in 0..amount {
|e| { match item::modular::random_weapon(*tool, *material, *hands, rng) {
Ok(item) => push_item(item, 1),
Err(e) => {
warn!( warn!(
?e, ?e,
"error while creating modular weapon primary component. Toolkind: \ "error while creating modular weapon primary component. Toolkind: \
@ -165,10 +323,29 @@ impl<T: AsRef<str>> LootSpec<T> {
material, material,
hands, hands,
); );
None
}, },
|(comp, _)| Some(comp), }
), }
},
Self::MultiDrop(loot_spec, lower, upper) => {
let sub_amount = rng.gen_range(*lower..=*upper);
// We saturate at 4 billion items, could use u64 instead if this isn't
// desirable.
loot_spec.to_items_inner(rng, sub_amount.saturating_mul(amount), items);
},
}
}
pub fn to_items(&self) -> Option<Vec<(u32, Item)>> {
let mut items = Vec::new();
self.to_items_inner(&mut thread_rng(), 1, &mut items);
if !items.is_empty() {
items.sort_unstable_by_key(|(amount, _)| *amount);
Some(items)
} else {
None
} }
} }
} }
@ -189,21 +366,6 @@ pub mod tests {
LootSpec::Item(item) => { LootSpec::Item(item) => {
Item::new_from_asset_expect(item); Item::new_from_asset_expect(item);
}, },
LootSpec::ItemQuantity(item, lower, upper) => {
assert!(
*lower > 0,
"Lower quantity must be more than 0. It is {}.",
lower
);
assert!(
upper >= lower,
"Upper quantity must be at least the value of lower quantity. Upper value: \
{}, low value: {}.",
upper,
lower
);
Item::new_from_asset_expect(item);
},
LootSpec::LootTable(loot_table) => { LootSpec::LootTable(loot_table) => {
let loot_table = Lottery::<LootSpec<String>>::load_expect_cloned(loot_table); let loot_table = Lottery::<LootSpec<String>>::load_expect_cloned(loot_table);
validate_table_contents(loot_table); validate_table_contents(loot_table);
@ -236,6 +398,21 @@ pub mod tests {
) )
}); });
}, },
LootSpec::MultiDrop(loot_spec, lower, upper) => {
assert!(
*lower > 0,
"Lower quantity must be more than 0. It is {}.",
lower
);
assert!(
upper >= lower,
"Upper quantity must be at least the value of lower quantity. Upper value: \
{}, low value: {}.",
upper,
lower
);
validate_loot_spec(loot_spec);
},
} }
} }
@ -253,4 +430,24 @@ pub mod tests {
validate_table_contents(loot_table.clone()); validate_table_contents(loot_table.clone());
} }
} }
#[test]
fn test_distribute_many() {
let mut rng = thread_rng();
// Known successful case
for _ in 0..10 {
distribute_many(
vec![(0.4f32, "a"), (0.4, "b"), (0.2, "c")],
&mut rng,
&[("item", 10)],
|(_, m)| *m,
|_item, winner, count| match winner {
"a" | "b" => assert_eq!(count, 4),
"c" => assert_eq!(count, 2),
_ => unreachable!(),
},
);
}
}
} }

View File

@ -253,7 +253,7 @@ impl State {
ecs.register::<comp::MapMarker>(); ecs.register::<comp::MapMarker>();
ecs.register::<comp::Projectile>(); ecs.register::<comp::Projectile>();
ecs.register::<comp::Melee>(); ecs.register::<comp::Melee>();
ecs.register::<comp::ItemDrop>(); ecs.register::<comp::ItemDrops>();
ecs.register::<comp::ChatMode>(); ecs.register::<comp::ChatMode>();
ecs.register::<comp::Faction>(); ecs.register::<comp::Faction>();
ecs.register::<comp::invite::Invite>(); ecs.register::<comp::invite::Invite>();

View File

@ -577,12 +577,20 @@ fn handle_give_item(
}); });
} }
insert_or_replace_component( let mut inventory_update = server
server, .state
.ecs_mut()
.write_storage::<comp::InventoryUpdate>();
if let Some(update) = inventory_update.get_mut(target) {
update.push(comp::InventoryUpdateEvent::Given);
} else {
inventory_update
.insert(
target, target,
comp::InventoryUpdate::new(comp::InventoryUpdateEvent::Given), comp::InventoryUpdate::new(comp::InventoryUpdateEvent::Given),
"target", )
)?; .map_err(|_| "Entity target is dead!")?;
}
res res
} else { } else {
Err(format!("Invalid item: {}", item_name)) Err(format!("Invalid item: {}", item_name))
@ -686,8 +694,8 @@ fn handle_make_npc(
entity_builder = entity_builder.with(agent); entity_builder = entity_builder.with(agent);
} }
if let Some(drop_item) = loot.to_item() { if let Some(drop_items) = loot.to_items() {
entity_builder = entity_builder.with(comp::ItemDrop(drop_item)); entity_builder = entity_builder.with(comp::ItemDrops(drop_items));
} }
// Some would say it's a hack, some would say it's incomplete // Some would say it's a hack, some would say it's incomplete

View File

@ -9,7 +9,7 @@ use common::{
aura::{Aura, AuraKind, AuraTarget}, aura::{Aura, AuraKind, AuraTarget},
beam, beam,
buff::{BuffCategory, BuffData, BuffKind, BuffSource}, buff::{BuffCategory, BuffData, BuffKind, BuffSource},
shockwave, Alignment, BehaviorCapability, Body, ItemDrop, LightEmitter, Object, Ori, Pos, shockwave, Alignment, BehaviorCapability, Body, ItemDrops, LightEmitter, Object, Ori, Pos,
Projectile, TradingBehavior, Vel, WaypointArea, Projectile, TradingBehavior, Vel, WaypointArea,
}, },
event::{EventBus, NpcBuilder, UpdateCharacterMetadata}, event::{EventBus, NpcBuilder, UpdateCharacterMetadata},
@ -118,8 +118,8 @@ pub fn handle_create_npc(server: &mut Server, pos: Pos, mut npc: NpcBuilder) ->
entity entity
}; };
let entity = if let Some(drop_item) = npc.loot.to_item() { let entity = if let Some(drop_items) = npc.loot.to_items() {
entity.with(ItemDrop(drop_item)) entity.with(ItemDrops(drop_items))
} else { } else {
entity entity
}; };

View File

@ -19,13 +19,16 @@ use common::{
self, aura, buff, self, aura, buff,
chat::{KillSource, KillType}, chat::{KillSource, KillType},
inventory::item::{AbilityMap, MaterialStatManifest}, inventory::item::{AbilityMap, MaterialStatManifest},
item::flatten_counted_items,
loot_owner::LootOwnerKind, loot_owner::LootOwnerKind,
Alignment, Auras, Body, CharacterState, Energy, Group, Health, HealthChange, Inventory, Alignment, Auras, Body, CharacterState, Energy, Group, Health, HealthChange, Inventory,
Player, Poise, Pos, SkillSet, Stats, Player, Poise, Pos, SkillSet, Stats,
}, },
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
lottery::distribute_many,
outcome::{HealthChangeInfo, Outcome}, outcome::{HealthChangeInfo, Outcome},
resources::{Secs, Time}, resources::{Secs, Time},
spiral::Spiral2d,
states::utils::StageSection, states::utils::StageSection,
terrain::{Block, BlockKind, TerrainGrid}, terrain::{Block, BlockKind, TerrainGrid},
trade::{TradeResult, Trades}, trade::{TradeResult, Trades},
@ -37,8 +40,7 @@ use common::{
use common_net::{msg::ServerGeneral, sync::WorldSyncExt}; use common_net::{msg::ServerGeneral, sync::WorldSyncExt};
use common_state::BlockChange; use common_state::BlockChange;
use hashbrown::HashSet; use hashbrown::HashSet;
use rand::{distributions::WeightedIndex, Rng}; use rand::Rng;
use rand_distr::Distribution;
use specs::{ use specs::{
join::Join, saveload::MarkerAllocator, Builder, Entity as EcsEntity, Entity, WorldExt, join::Join, saveload::MarkerAllocator, Builder, Entity as EcsEntity, Entity, WorldExt,
}; };
@ -427,46 +429,33 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
// Decide for a loot drop before turning into a lootbag // Decide for a loot drop before turning into a lootbag
let item = { let items = {
let mut item_drop = state.ecs().write_storage::<comp::ItemDrop>(); let mut item_drops = state.ecs().write_storage::<comp::ItemDrops>();
item_drop.remove(entity).map(|comp::ItemDrop(item)| item) item_drops.remove(entity).map(|comp::ItemDrops(item)| item)
}; };
if let Some(item) = item { if let Some(items) = items {
let pos = state.ecs().read_storage::<Pos>().get(entity).cloned(); let pos = state.ecs().read_storage::<Pos>().get(entity).cloned();
let vel = state.ecs().read_storage::<comp::Vel>().get(entity).cloned(); let vel = state.ecs().read_storage::<comp::Vel>().get(entity).cloned();
if let Some(pos) = pos { if let Some(pos) = pos {
// Remove entries where zero exp was awarded - this happens because some // Remove entries where zero exp was awarded - this happens because some
// entities like Object bodies don't give EXP. // entities like Object bodies don't give EXP.
let _ = exp_awards.drain_filter(|(_, exp, _)| *exp < f32::EPSILON); let mut item_receivers = HashMap::new();
for (entity, exp, group) in exp_awards {
let winner = if exp_awards.is_empty() { if exp >= f32::EPSILON {
None let loot_owner = if let Some(group) = group {
} else {
// Use the awarded exp per entity as the weight distribution for drop chance
// Creating the WeightedIndex can only fail if there are weights <= 0 or no
// weights, which shouldn't ever happen
let dist = WeightedIndex::new(exp_awards.iter().map(|x| x.1))
.expect("Failed to create WeightedIndex for loot drop chance");
let mut rng = rand::thread_rng();
let winner = exp_awards
.get(dist.sample(&mut rng))
.expect("Loot distribution failed to find a winner");
let (winner, group) = (winner.0, winner.2);
if let Some(group) = group {
Some(LootOwnerKind::Group(group)) Some(LootOwnerKind::Group(group))
} else { } else {
let uid = state let uid = state
.ecs() .ecs()
.read_storage::<Body>() .read_storage::<Body>()
.get(winner) .get(entity)
.and_then(|body| { .and_then(|body| {
// Only humanoids are awarded loot ownership - if the winner // Only humanoids are awarded loot ownership - if the winner
// was a // was a
// non-humanoid NPC the loot will be free-for-all // non-humanoid NPC the loot will be free-for-all
if matches!(body, Body::Humanoid(_)) { if matches!(body, Body::Humanoid(_)) {
Some(state.ecs().read_storage::<Uid>().get(winner).cloned()) Some(state.ecs().read_storage::<Uid>().get(entity).cloned())
} else { } else {
None None
} }
@ -474,25 +463,54 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
.flatten(); .flatten();
uid.map(LootOwnerKind::Player) uid.map(LootOwnerKind::Player)
}
}; };
*item_receivers.entry(loot_owner).or_insert(0.0) += exp;
}
}
if !item_receivers.is_empty() {
let msm = &MaterialStatManifest::load().read();
let ability_map = &AbilityMap::load().read();
let mut item_offset_spiral = Spiral2d::new();
let mut rng = rand::thread_rng();
distribute_many(
item_receivers
.iter()
.map(|(loot_owner, weight)| (*weight, *loot_owner)),
&mut rng,
&items,
|(amount, _)| *amount,
|(_, item), loot_owner, count| {
for item in item.stacked_duplicates(ability_map, msm, count) {
let offset = item_offset_spiral
.next()
.map(|offset| offset.as_::<f32>() * 0.25)
.unwrap_or_default();
let item_drop_entity = state let item_drop_entity = state
.create_item_drop(Pos(pos.0 + Vec3::unit_z() * 0.25), item) .create_item_drop(
Pos(pos.0 + Vec3::unit_z() * 0.25 + offset),
item,
)
.maybe_with(vel) .maybe_with(vel)
.build(); .build();
if let Some(loot_owner) = loot_owner {
// If there was a loot winner, assign them as the owner of the loot. There will debug!(
// not be a loot winner when an entity dies to environment damage and such so "Assigned UID {loot_owner:?} as the winner for the loot \
// the loot will be free-for-all. drop"
if let Some(uid) = winner { );
debug!("Assigned UID {:?} as the winner for the loot drop", uid); if let Err(err) = state
state
.ecs() .ecs()
.write_storage::<LootOwner>() .write_storage::<LootOwner>()
.insert(item_drop_entity, LootOwner::new(uid)) .insert(item_drop_entity, LootOwner::new(loot_owner))
.unwrap(); {
error!("Failed to set loot owner on item drop: {err}");
};
}
}
},
);
} }
} else { } else {
error!( error!(
@ -1080,7 +1098,10 @@ pub fn handle_bonk(server: &mut Server, pos: Vec3<f32>, owner: Option<Uid>, targ
{ {
drop(terrain); drop(terrain);
drop(block_change); drop(block_change);
if let Some(item) = comp::Item::try_reclaim_from_block(block) { if let Some(items) = comp::Item::try_reclaim_from_block(block) {
let msm = &MaterialStatManifest::load().read();
let ability_map = &AbilityMap::load().read();
for item in flatten_counted_items(&items, ability_map, msm) {
server server
.state .state
.create_object(Default::default(), match block.get_sprite() { .create_object(Default::default(), match block.get_sprite() {
@ -1102,6 +1123,7 @@ pub fn handle_bonk(server: &mut Server, pos: Vec3<f32>, owner: Option<Uid>, targ
} }
} }
} }
}
} }
pub fn handle_aura(server: &mut Server, entity: EcsEntity, aura_change: aura::AuraChange) { pub fn handle_aura(server: &mut Server, entity: EcsEntity, aura_change: aura::AuraChange) {

View File

@ -8,9 +8,10 @@ use common::{
agent::{AgentEvent, Sound, SoundKind}, agent::{AgentEvent, Sound, SoundKind},
dialogue::Subject, dialogue::Subject,
inventory::slot::EquipSlot, inventory::slot::EquipSlot,
item::{flatten_counted_items, MaterialStatManifest},
loot_owner::LootOwnerKind, loot_owner::LootOwnerKind,
pet::is_mountable, pet::is_mountable,
tool::ToolKind, tool::{AbilityMap, ToolKind},
Inventory, LootOwner, Pos, SkillGroupKind, Inventory, LootOwner, Pos, SkillGroupKind,
}, },
consts::{MAX_MOUNT_RANGE, SOUND_TRAVEL_DIST_PER_VOLUME}, consts::{MAX_MOUNT_RANGE, SOUND_TRAVEL_DIST_PER_VOLUME},
@ -178,7 +179,10 @@ pub fn handle_mine_block(
let block = state.terrain().get(pos).ok().copied(); let block = state.terrain().get(pos).ok().copied();
if let Some(block) = block.filter(|b| b.mine_tool().map_or(false, |t| Some(t) == tool)) { if let Some(block) = block.filter(|b| b.mine_tool().map_or(false, |t| Some(t) == tool)) {
// Drop item if one is recoverable from the block // Drop item if one is recoverable from the block
if let Some(mut item) = comp::Item::try_reclaim_from_block(block) { if let Some(items) = comp::Item::try_reclaim_from_block(block) {
let msm = &MaterialStatManifest::load().read();
let ability_map = &AbilityMap::load().read();
let mut items: Vec<_> = flatten_counted_items(&items, ability_map, msm).collect();
let maybe_uid = state.ecs().uid_from_entity(entity); let maybe_uid = state.ecs().uid_from_entity(entity);
if let Some(mut skillset) = state if let Some(mut skillset) = state
@ -186,12 +190,17 @@ pub fn handle_mine_block(
.write_storage::<comp::SkillSet>() .write_storage::<comp::SkillSet>()
.get_mut(entity) .get_mut(entity)
{ {
if let (Some(tool), Some(uid), Some(exp_reward)) = ( if let (Some(tool), Some(uid), exp_reward @ 1..) = (
tool, tool,
maybe_uid, maybe_uid,
item.item_definition_id() items
.itemdef_id() .iter()
.and_then(|id| RESOURCE_EXPERIENCE_MANIFEST.read().0.get(id).copied()), .filter_map(|item| {
item.item_definition_id().itemdef_id().and_then(|id| {
RESOURCE_EXPERIENCE_MANIFEST.read().0.get(id).copied()
})
})
.sum(),
) { ) {
let skill_group = SkillGroupKind::Weapon(tool); let skill_group = SkillGroupKind::Weapon(tool);
let outcome_bus = state.ecs().read_resource::<EventBus<Outcome>>(); let outcome_bus = state.ecs().read_resource::<EventBus<Outcome>>();
@ -230,8 +239,9 @@ pub fn handle_mine_block(
rng.gen_bool(chance_mod * f64::from(skill_level)) rng.gen_bool(chance_mod * f64::from(skill_level))
}; };
for item in items.iter_mut() {
let double_gain = item.item_definition_id().itemdef_id().map_or(false, |id| { let double_gain =
item.item_definition_id().itemdef_id().map_or(false, |id| {
(id.contains("mineral.ore.") && need_double_ore(&mut rng)) (id.contains("mineral.ore.") && need_double_ore(&mut rng))
|| (id.contains("mineral.gem.") && need_double_gem(&mut rng)) || (id.contains("mineral.gem.") && need_double_gem(&mut rng))
}); });
@ -241,6 +251,8 @@ pub fn handle_mine_block(
let _ = item.increase_amount(1); let _ = item.increase_amount(1);
} }
} }
}
for item in items {
let item_drop = state let item_drop = state
.create_item_drop(Default::default(), item) .create_item_drop(Default::default(), item)
.with(Pos(pos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0))); .with(Pos(pos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0)));
@ -251,6 +263,7 @@ pub fn handle_mine_block(
} }
.build(); .build();
} }
}
state.set_block(pos, block.into_vacant()); state.set_block(pos, block.into_vacant());
state state

View File

@ -8,8 +8,9 @@ use common::{
comp::{ comp::{
self, self,
group::members, group::members,
item::{self, tool::AbilityMap, MaterialStatManifest}, item::{self, flatten_counted_items, tool::AbilityMap, MaterialStatManifest},
slot::{self, Slot}, slot::{self, Slot},
InventoryUpdate,
}, },
consts::MAX_PICKUP_RANGE, consts::MAX_PICKUP_RANGE,
recipe::{ recipe::{
@ -262,7 +263,12 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
let mut block_change = ecs.write_resource::<common_state::BlockChange>(); let mut block_change = ecs.write_resource::<common_state::BlockChange>();
let block = terrain.get(sprite_pos).ok().copied(); let block = terrain.get(sprite_pos).ok().copied();
let mut drop_item = None; let mut drop_items = Vec::new();
let mut inventory_updates = ecs.write_storage();
let inventory_update = inventory_updates
.entry(entity)
.expect("We know entity exists since we got its inventory.")
.or_insert_with(InventoryUpdate::default);
if let Some(block) = block { if let Some(block) = block {
if block.is_collectible() && block_change.can_set_block(sprite_pos) { if block.is_collectible() && block_change.can_set_block(sprite_pos) {
@ -275,16 +281,17 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
); );
} }
// If there's an item to be reclaimed from the block, add it to the inventory // If there are items to be reclaimed from the block, add it to the inventory
if let Some(item) = comp::Item::try_reclaim_from_block(block) { if let Some(items) = comp::Item::try_reclaim_from_block(block) {
let msm = &MaterialStatManifest::load().read();
let ability_map = &AbilityMap::load().read();
for item in flatten_counted_items(&items, ability_map, msm) {
// NOTE: We dup the item for message purposes. // NOTE: We dup the item for message purposes.
let item_msg = item.duplicate( let item_msg = item.duplicate(ability_map, msm);
&ecs.read_resource::<AbilityMap>(), match inventory.push(item) {
&ecs.read_resource::<MaterialStatManifest>(),
);
let event = match inventory.push(item) {
Ok(_) => { Ok(_) => {
if let Some(group_id) = ecs.read_storage::<Group>().get(entity) { if let Some(group_id) = ecs.read_storage::<Group>().get(entity)
{
announce_loot_to_group( announce_loot_to_group(
group_id, group_id,
ecs, ecs,
@ -295,25 +302,16 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
), ),
); );
} }
comp::InventoryUpdate::new(InventoryUpdateEvent::Collected( inventory_update
item_msg, .push(InventoryUpdateEvent::Collected(item_msg));
))
}, },
// The item we created was in some sense "fake" so it's safe to // The item we created was in some sense "fake" so it's safe to
// drop it. // drop it.
Err(_) => { Err(_) => {
drop_item = Some(item_msg); drop_items.push(item_msg);
comp::InventoryUpdate::new(
InventoryUpdateEvent::BlockCollectFailed {
pos: sprite_pos,
reason: CollectFailedReason::InventoryFull,
}, },
) }
}, }
};
ecs.write_storage()
.insert(entity, event)
.expect("We know entity exists since we got its inventory.");
} }
// We made sure earlier the block was not already modified this tick // We made sure earlier the block was not already modified this tick
@ -367,10 +365,18 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
); );
} }
} }
if !drop_items.is_empty() {
inventory_update.push(InventoryUpdateEvent::BlockCollectFailed {
pos: sprite_pos,
reason: CollectFailedReason::InventoryFull,
})
}
drop(inventories); drop(inventories);
drop(terrain); drop(terrain);
drop(block_change); drop(block_change);
if let Some(item) = drop_item { drop(inventory_updates);
for item in drop_items {
state state
.create_item_drop(Default::default(), item) .create_item_drop(Default::default(), item)
.with(comp::Pos( .with(comp::Pos(

View File

@ -356,10 +356,10 @@ impl<'a> System<'a> for Sys {
// TODO: Sync clients that don't have a position? // TODO: Sync clients that don't have a position?
// Sync inventories // Sync inventories
for (inventory, update, client) in (inventories, &inventory_updates, &clients).join() { for (inventory, update, client) in (inventories, &mut inventory_updates, &clients).join() {
client.send_fallible(ServerGeneral::InventoryUpdate( client.send_fallible(ServerGeneral::InventoryUpdate(
inventory.clone(), inventory.clone(),
update.event(), update.take_events(),
)); ));
} }

View File

@ -2128,7 +2128,17 @@ impl Hud {
.x_y(0.0, 100.0) .x_y(0.0, 100.0)
.position_ingame(over_pos) .position_ingame(over_pos)
.set(overitem_id, ui_widgets); .set(overitem_id, ui_widgets);
} else if let Some(item) = Item::try_reclaim_from_block(*block) { }
// TODO: Handle this better. The items returned from `try_reclaim_from_block`
// are based on rng. We probably want some function to get only gauranteed items
// from `LootSpec`.
else if let Some((amount, mut item)) = Item::try_reclaim_from_block(*block)
.into_iter()
.flatten()
.next()
{
item.set_amount(amount.clamp(1, item.max_amount()))
.expect("amount >= 1 and <= max_amount is always a valid amount");
make_overitem( make_overitem(
&item, &item,
over_pos, over_pos,

View File

@ -321,10 +321,12 @@ impl SessionState {
TradeResult::NotEnoughSpace => "hud-trade-result-nospace", TradeResult::NotEnoughSpace => "hud-trade-result-nospace",
}))); })));
}, },
client::Event::InventoryUpdated(inv_event) => { client::Event::InventoryUpdated(inv_events) => {
let sfx_triggers = self.scene.sfx_mgr.triggers.read(); let sfx_triggers = self.scene.sfx_mgr.triggers.read();
let sfx_trigger_item = sfx_triggers.get_key_value(&SfxEvent::from(&inv_event)); for inv_event in inv_events {
let sfx_trigger_item =
sfx_triggers.get_key_value(&SfxEvent::from(&inv_event));
match inv_event { match inv_event {
InventoryUpdateEvent::Dropped InventoryUpdateEvent::Dropped
@ -358,7 +360,9 @@ impl SessionState {
entity: uid, entity: uid,
reason, reason,
} => { } => {
if let Some(entity) = client.state().ecs().entity_from_uid(uid.into()) { if let Some(entity) =
client.state().ecs().entity_from_uid(uid.into())
{
self.hud.add_failed_entity_pickup( self.hud.add_failed_entity_pickup(
entity, entity,
HudCollectFailedReason::from_server_reason( HudCollectFailedReason::from_server_reason(
@ -377,6 +381,7 @@ impl SessionState {
}, },
_ => {}, _ => {},
}; };
}
}, },
client::Event::Disconnect => return Ok(TickAction::Disconnect), client::Event::Disconnect => return Ok(TickAction::Disconnect),
client::Event::DisconnectionNotification(time) => { client::Event::DisconnectionNotification(time) => {