diff --git a/CHANGELOG.md b/CHANGELOG.md index 1eb7c799f4..11959e20ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 migrate to new towns if they are dissatisfied with their current town - Female humanoids now have a greeting sound effect +- Loot that drops multiple items is now distributed fairly between damage contributors. ### Changed diff --git a/assets/common/entity/world/world_bosses/gigas_frost.ron b/assets/common/entity/world/world_bosses/gigas_frost.ron index a6b728cc73..6d049ff3b3 100644 --- a/assets/common/entity/world/world_bosses/gigas_frost.ron +++ b/assets/common/entity/world/world_bosses/gigas_frost.ron @@ -3,7 +3,7 @@ name: Name("Frost Gigas"), body: RandomWith("gigas_frost"), 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: ( loadout: FromBody, ), diff --git a/assets/common/loot_tables/calendar/christmas/boss.ron b/assets/common/loot_tables/calendar/christmas/boss.ron index 6d6c8807cb..7b32c4768c 100644 --- a/assets/common/loot_tables/calendar/christmas/boss.ron +++ b/assets/common/loot_tables/calendar/christmas/boss.ron @@ -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")), (1.0, Item("common.items.lantern.polaris")), ] diff --git a/assets/common/loot_tables/calendar/christmas/yeti.ron b/assets/common/loot_tables/calendar/christmas/yeti.ron index 35dc8ee132..d64c1699b9 100644 --- a/assets/common/loot_tables/calendar/christmas/yeti.ron +++ b/assets/common/loot_tables/calendar/christmas/yeti.ron @@ -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")), ] \ No newline at end of file diff --git a/assets/common/loot_tables/consumable/good.ron b/assets/common/loot_tables/consumable/good.ron index 4c1e8f8304..45c6d1525b 100644 --- a/assets/common/loot_tables/consumable/good.ron +++ b/assets/common/loot_tables/consumable/good.ron @@ -1,14 +1,14 @@ [ // Fireworks - (1.0, ItemQuantity("common.items.utility.firework_blue", 8, 10)), - (1.0, ItemQuantity("common.items.utility.firework_green", 8, 10)), - (1.0, ItemQuantity("common.items.utility.firework_purple", 8, 10)), - (1.0, ItemQuantity("common.items.utility.firework_red", 8, 10)), - (1.0, ItemQuantity("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_blue"), 8, 10)), + (1.0, MultiDrop(Item("common.items.utility.firework_green"), 8, 10)), + (1.0, MultiDrop(Item("common.items.utility.firework_purple"), 8, 10)), + (1.0, MultiDrop(Item("common.items.utility.firework_red"), 8, 10)), + (1.0, MultiDrop(Item("common.items.utility.firework_white"), 8, 10)), + (1.0, MultiDrop(Item("common.items.utility.firework_yellow"), 8, 10)), // Potions - (10.0, ItemQuantity("common.items.consumable.potion_big", 2, 5)), + (10.0, MultiDrop(Item("common.items.consumable.potion_big"), 2, 5)), // Misc - (5.0, ItemQuantity("common.items.utility.collar", 2, 3)), - (5.0, ItemQuantity("common.items.utility.bomb", 8, 10)), + (5.0, MultiDrop(Item("common.items.utility.collar"), 2, 3)), + (5.0, MultiDrop(Item("common.items.utility.bomb"), 8, 10)), ] \ No newline at end of file diff --git a/assets/common/loot_tables/consumable/moderate.ron b/assets/common/loot_tables/consumable/moderate.ron index 5756588c3e..e01ecdb198 100644 --- a/assets/common/loot_tables/consumable/moderate.ron +++ b/assets/common/loot_tables/consumable/moderate.ron @@ -1,14 +1,14 @@ [ // Fireworks - (1.0, ItemQuantity("common.items.utility.firework_blue", 3, 5)), - (1.0, ItemQuantity("common.items.utility.firework_green", 3, 5)), - (1.0, ItemQuantity("common.items.utility.firework_purple", 3, 5)), - (1.0, ItemQuantity("common.items.utility.firework_red", 3, 5)), - (1.0, ItemQuantity("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_blue"), 3, 5)), + (1.0, MultiDrop(Item("common.items.utility.firework_green"), 3, 5)), + (1.0, MultiDrop(Item("common.items.utility.firework_purple"), 3, 5)), + (1.0, MultiDrop(Item("common.items.utility.firework_red"), 3, 5)), + (1.0, MultiDrop(Item("common.items.utility.firework_white"), 3, 5)), + (1.0, MultiDrop(Item("common.items.utility.firework_yellow"), 3, 5)), // Potions - (10.0, ItemQuantity("common.items.consumable.potion_med", 2, 5)), + (10.0, MultiDrop(Item("common.items.consumable.potion_med"), 2, 5)), // Misc - (5.0, ItemQuantity("common.items.utility.collar", 1, 2)), - (5.0, ItemQuantity("common.items.utility.bomb", 3, 5)), + (5.0, MultiDrop(Item("common.items.utility.collar"), 1, 2)), + (5.0, MultiDrop(Item("common.items.utility.bomb"), 3, 5)), ] \ No newline at end of file diff --git a/assets/common/loot_tables/consumable/poor.ron b/assets/common/loot_tables/consumable/poor.ron index 674298958b..4d73801142 100644 --- a/assets/common/loot_tables/consumable/poor.ron +++ b/assets/common/loot_tables/consumable/poor.ron @@ -7,7 +7,7 @@ (1.0, Item("common.items.utility.firework_white")), (1.0, Item("common.items.utility.firework_yellow")), // Potions - (10.0, ItemQuantity("common.items.consumable.potion_minor", 2, 5)), + (10.0, MultiDrop(Item("common.items.consumable.potion_minor"), 2, 5)), // Misc (5.0, Item("common.items.utility.collar")), (5.0, Item("common.items.utility.bomb")), diff --git a/assets/common/loot_tables/creature/arthropod/antlion.ron b/assets/common/loot_tables/creature/arthropod/antlion.ron index 322ddfadb0..93dac22102 100644 --- a/assets/common/loot_tables/creature/arthropod/antlion.ron +++ b/assets/common/loot_tables/creature/arthropod/antlion.ron @@ -1,4 +1,4 @@ [ - (1.5, ItemQuantity("common.items.crafting_ing.hide.carapace", 2, 5)), - (1.0, ItemQuantity("common.items.crafting_ing.animal_misc.strong_pincer", 1, 2)), + (1.5, MultiDrop(Item("common.items.crafting_ing.hide.carapace"), 2, 5)), + (1.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.strong_pincer"), 1, 2)), ] \ No newline at end of file diff --git a/assets/common/loot_tables/creature/arthropod/black_widow.ron b/assets/common/loot_tables/creature/arthropod/black_widow.ron index b6cb7a93e8..87f9b99d0c 100644 --- a/assets/common/loot_tables/creature/arthropod/black_widow.ron +++ b/assets/common/loot_tables/creature/arthropod/black_widow.ron @@ -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, 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)), ] \ No newline at end of file diff --git a/assets/common/loot_tables/creature/arthropod/carapace.ron b/assets/common/loot_tables/creature/arthropod/carapace.ron index 2716e2eaf2..7c64974d1e 100644 --- a/assets/common/loot_tables/creature/arthropod/carapace.ron +++ b/assets/common/loot_tables/creature/arthropod/carapace.ron @@ -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")), ] \ No newline at end of file diff --git a/assets/common/loot_tables/creature/arthropod/leaf.ron b/assets/common/loot_tables/creature/arthropod/leaf.ron index 4db604535f..62c76a6a74 100644 --- a/assets/common/loot_tables/creature/arthropod/leaf.ron +++ b/assets/common/loot_tables/creature/arthropod/leaf.ron @@ -1,4 +1,4 @@ [ - (2.0, ItemQuantity("common.items.flowers.plant_fiber", 1, 3)), - (1.0, ItemQuantity("common.items.crafting_ing.animal_misc.viscous_ooze", 1, 1)), + (2.0, MultiDrop(Item("common.items.flowers.plant_fiber"), 1, 3)), + (1.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.viscous_ooze"), 1, 1)), ] \ No newline at end of file diff --git a/assets/common/loot_tables/creature/arthropod/leaf_beetle.ron b/assets/common/loot_tables/creature/arthropod/leaf_beetle.ron index ac97f0dc3e..607384d9fc 100644 --- a/assets/common/loot_tables/creature/arthropod/leaf_beetle.ron +++ b/assets/common/loot_tables/creature/arthropod/leaf_beetle.ron @@ -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")), - (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)), ] \ No newline at end of file diff --git a/assets/common/loot_tables/creature/arthropod/ooze.ron b/assets/common/loot_tables/creature/arthropod/ooze.ron index 041a94de30..f498a49dba 100644 --- a/assets/common/loot_tables/creature/arthropod/ooze.ron +++ b/assets/common/loot_tables/creature/arthropod/ooze.ron @@ -1,4 +1,4 @@ [ - (1.0, ItemQuantity("common.items.crafting_ing.sticky_thread", 1, 3)), - (0.5, ItemQuantity("common.items.crafting_ing.animal_misc.viscous_ooze", 1, 1)), + (1.0, MultiDrop(Item("common.items.crafting_ing.sticky_thread"), 1, 3)), + (0.5, MultiDrop(Item("common.items.crafting_ing.animal_misc.viscous_ooze"), 1, 1)), ] \ No newline at end of file diff --git a/assets/common/loot_tables/creature/arthropod/tarantula.ron b/assets/common/loot_tables/creature/arthropod/tarantula.ron index d1879e4c63..e5c9eac238 100644 --- a/assets/common/loot_tables/creature/arthropod/tarantula.ron +++ b/assets/common/loot_tables/creature/arthropod/tarantula.ron @@ -1,4 +1,4 @@ [ - (1.0, ItemQuantity("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.sticky_thread"), 2, 5)), + (1.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.strong_pincer"), 1, 2)), ] \ No newline at end of file diff --git a/assets/common/loot_tables/creature/arthropod/venom.ron b/assets/common/loot_tables/creature/arthropod/venom.ron index 1135844b80..b79348312e 100644 --- a/assets/common/loot_tables/creature/arthropod/venom.ron +++ b/assets/common/loot_tables/creature/arthropod/venom.ron @@ -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.strong_pincer")), ] \ No newline at end of file diff --git a/assets/common/loot_tables/creature/arthropod/web.ron b/assets/common/loot_tables/creature/arthropod/web.ron index 7b0fc127c0..6968d0ea44 100644 --- a/assets/common/loot_tables/creature/arthropod/web.ron +++ b/assets/common/loot_tables/creature/arthropod/web.ron @@ -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")), ] \ No newline at end of file diff --git a/assets/common/loot_tables/creature/biped_large/tursus.ron b/assets/common/loot_tables/creature/biped_large/tursus.ron index 364321aa8d..a6aeb3695f 100644 --- a/assets/common/loot_tables/creature/biped_large/tursus.ron +++ b/assets/common/loot_tables/creature/biped_large/tursus.ron @@ -1,4 +1,4 @@ [ (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)), ] \ No newline at end of file diff --git a/assets/common/loot_tables/creature/bird_large/cockatrice.ron b/assets/common/loot_tables/creature/bird_large/cockatrice.ron index 5ef047b785..636310cc1d 100644 --- a/assets/common/loot_tables/creature/bird_large/cockatrice.ron +++ b/assets/common/loot_tables/creature/bird_large/cockatrice.ron @@ -1,6 +1,6 @@ [ - (1.0, ItemQuantity("common.items.food.meat.bird_large_raw", 1, 2)), - (1.0, ItemQuantity("common.items.food.meat.beast_large_raw", 2, 2)), - (2.0, ItemQuantity("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.food.meat.bird_large_raw"), 1, 2)), + (1.0, MultiDrop(Item("common.items.food.meat.beast_large_raw"), 2, 2)), + (2.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.elegant_crest"), 1, 3)), + (1.0, MultiDrop(Item("common.items.crafting_ing.hide.scales"), 2, 6)), ] \ No newline at end of file diff --git a/assets/common/loot_tables/creature/bird_large/roc.ron b/assets/common/loot_tables/creature/bird_large/roc.ron index db51e28224..d5f649feef 100644 --- a/assets/common/loot_tables/creature/bird_large/roc.ron +++ b/assets/common/loot_tables/creature/bird_large/roc.ron @@ -1,5 +1,5 @@ [ - (1.0, ItemQuantity("common.items.food.meat.bird_large_raw", 1, 2)), - (1.0, ItemQuantity("common.items.food.meat.beast_large_raw", 2, 2)), - (2.0, ItemQuantity("common.items.crafting_ing.animal_misc.elegant_crest", 1, 3)), + (1.0, MultiDrop(Item("common.items.food.meat.bird_large_raw"), 1, 2)), + (1.0, MultiDrop(Item("common.items.food.meat.beast_large_raw"), 2, 2)), + (2.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.elegant_crest"), 1, 3)), ] \ No newline at end of file diff --git a/assets/common/loot_tables/creature/bird_large/wyvern.ron b/assets/common/loot_tables/creature/bird_large/wyvern.ron index 5ef047b785..636310cc1d 100644 --- a/assets/common/loot_tables/creature/bird_large/wyvern.ron +++ b/assets/common/loot_tables/creature/bird_large/wyvern.ron @@ -1,6 +1,6 @@ [ - (1.0, ItemQuantity("common.items.food.meat.bird_large_raw", 1, 2)), - (1.0, ItemQuantity("common.items.food.meat.beast_large_raw", 2, 2)), - (2.0, ItemQuantity("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.food.meat.bird_large_raw"), 1, 2)), + (1.0, MultiDrop(Item("common.items.food.meat.beast_large_raw"), 2, 2)), + (2.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.elegant_crest"), 1, 3)), + (1.0, MultiDrop(Item("common.items.crafting_ing.hide.scales"), 2, 6)), ] \ No newline at end of file diff --git a/assets/common/loot_tables/creature/quad_medium/highland.ron b/assets/common/loot_tables/creature/quad_medium/highland.ron index 35483d802e..40db52b79f 100644 --- a/assets/common/loot_tables/creature/quad_medium/highland.ron +++ b/assets/common/loot_tables/creature/quad_medium/highland.ron @@ -1,5 +1,5 @@ [ (1.0, Item("common.items.food.meat.beast_large_raw")), (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)), ] \ No newline at end of file diff --git a/assets/common/loot_tables/creature/quad_medium/ice.ron b/assets/common/loot_tables/creature/quad_medium/ice.ron index c9caf8ca82..c3ee3b92fc 100644 --- a/assets/common/loot_tables/creature/quad_medium/ice.ron +++ b/assets/common/loot_tables/creature/quad_medium/ice.ron @@ -1,4 +1,4 @@ [ (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)), ] \ No newline at end of file diff --git a/assets/common/loot_tables/creature/quad_medium/mammoth.ron b/assets/common/loot_tables/creature/quad_medium/mammoth.ron index 117251a95c..e7ff637620 100644 --- a/assets/common/loot_tables/creature/quad_medium/mammoth.ron +++ b/assets/common/loot_tables/creature/quad_medium/mammoth.ron @@ -1,4 +1,4 @@ [ - (2.0, ItemQuantity("common.items.crafting_ing.hide.rugged_hide", 1, 2)), - (1.0, ItemQuantity("common.items.crafting_ing.animal_misc.long_tusk", 2, 2)), + (2.0, MultiDrop(Item("common.items.crafting_ing.hide.rugged_hide"), 1, 2)), + (1.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.long_tusk"), 2, 2)), ] \ No newline at end of file diff --git a/assets/common/loot_tables/creature/quad_medium/ngoubou.ron b/assets/common/loot_tables/creature/quad_medium/ngoubou.ron index e4dc0a3f92..96a657bd90 100644 --- a/assets/common/loot_tables/creature/quad_medium/ngoubou.ron +++ b/assets/common/loot_tables/creature/quad_medium/ngoubou.ron @@ -1,5 +1,5 @@ [ (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, ItemQuantity("common.items.crafting_ing.animal_misc.long_tusk", 2, 2)), + (1.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.large_horn"), 1, 2)), + (1.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.long_tusk"), 2, 2)), ] \ No newline at end of file diff --git a/assets/common/loot_tables/creature/quad_medium/roshwalr.ron b/assets/common/loot_tables/creature/quad_medium/roshwalr.ron index e40ed0de23..baa9f83d7c 100644 --- a/assets/common/loot_tables/creature/quad_medium/roshwalr.ron +++ b/assets/common/loot_tables/creature/quad_medium/roshwalr.ron @@ -1,5 +1,5 @@ [ - (2.0, ItemQuantity("common.items.crafting_ing.hide.rugged_hide", 1, 3)), - (1.0, ItemQuantity("common.items.crafting_ing.animal_misc.icy_fang", 2, 4)), - (1.0, ItemQuantity("common.items.crafting_ing.animal_misc.long_tusk", 2, 2)), + (2.0, MultiDrop(Item("common.items.crafting_ing.hide.rugged_hide"), 1, 3)), + (1.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.icy_fang"), 2, 4)), + (1.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.long_tusk"), 2, 2)), ] \ No newline at end of file diff --git a/assets/common/loot_tables/creature/quad_medium/wool.ron b/assets/common/loot_tables/creature/quad_medium/wool.ron index 3b3fa3f62d..d59d4e067c 100644 --- a/assets/common/loot_tables/creature/quad_medium/wool.ron +++ b/assets/common/loot_tables/creature/quad_medium/wool.ron @@ -2,5 +2,5 @@ (1.5, Item("common.items.food.meat.beast_small_raw")), (0.5, Item("common.items.food.meat.beast_large_raw")), (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)), ] \ No newline at end of file diff --git a/assets/common/loot_tables/creature/quad_small/fur.ron b/assets/common/loot_tables/creature/quad_small/fur.ron index 5de9f43f02..15964e88d3 100644 --- a/assets/common/loot_tables/creature/quad_small/fur.ron +++ b/assets/common/loot_tables/creature/quad_small/fur.ron @@ -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")), ] diff --git a/assets/common/loot_tables/creature/quad_small/generic.ron b/assets/common/loot_tables/creature/quad_small/generic.ron index d7aeb6531a..134a045535 100644 --- a/assets/common/loot_tables/creature/quad_small/generic.ron +++ b/assets/common/loot_tables/creature/quad_small/generic.ron @@ -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")), ] \ No newline at end of file diff --git a/assets/common/loot_tables/creature/quad_small/truffler.ron b/assets/common/loot_tables/creature/quad_small/truffler.ron index de6adad911..fc105b2ee3 100644 --- a/assets/common/loot_tables/creature/quad_small/truffler.ron +++ b/assets/common/loot_tables/creature/quad_small/truffler.ron @@ -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")), ] \ No newline at end of file diff --git a/assets/common/loot_tables/creature/quad_small/wool.ron b/assets/common/loot_tables/creature/quad_small/wool.ron index 77cbaf3289..72b48af344 100644 --- a/assets/common/loot_tables/creature/quad_small/wool.ron +++ b/assets/common/loot_tables/creature/quad_small/wool.ron @@ -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")), ] \ No newline at end of file diff --git a/assets/common/loot_tables/creature/theropod/axebeak.ron b/assets/common/loot_tables/creature/theropod/axebeak.ron index e531d2db9f..5489ab9e20 100644 --- a/assets/common/loot_tables/creature/theropod/axebeak.ron +++ b/assets/common/loot_tables/creature/theropod/axebeak.ron @@ -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")), (0.5, Item("common.items.weapons.hammer.burnt_drumstick")), ] \ No newline at end of file diff --git a/assets/common/loot_tables/creature/theropod/odonto.ron b/assets/common/loot_tables/creature/theropod/odonto.ron index 81fa436249..b8675d794f 100644 --- a/assets/common/loot_tables/creature/theropod/odonto.ron +++ b/assets/common/loot_tables/creature/theropod/odonto.ron @@ -1,4 +1,4 @@ [ (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)), ] \ No newline at end of file diff --git a/assets/common/loot_tables/creature/theropod/plate.ron b/assets/common/loot_tables/creature/theropod/plate.ron index f4f8133003..226b6164ac 100644 --- a/assets/common/loot_tables/creature/theropod/plate.ron +++ b/assets/common/loot_tables/creature/theropod/plate.ron @@ -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")), ] diff --git a/assets/common/loot_tables/creature/theropod/raptor.ron b/assets/common/loot_tables/creature/theropod/raptor.ron index c0cdf2c156..961d48c4fb 100644 --- a/assets/common/loot_tables/creature/theropod/raptor.ron +++ b/assets/common/loot_tables/creature/theropod/raptor.ron @@ -1,5 +1,5 @@ [ - (1.0, ItemQuantity("common.items.crafting_ing.animal_misc.elegant_crest", 1, 1)), - (2.0, ItemQuantity("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.animal_misc.elegant_crest"), 1, 1)), + (2.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.raptor_feather"), 2, 6)), + (1.0, MultiDrop(Item("common.items.crafting_ing.hide.scales"), 1, 2)), ] \ No newline at end of file diff --git a/assets/common/loot_tables/creature/theropod/scale.ron b/assets/common/loot_tables/creature/theropod/scale.ron index bbb405719e..e101009b9c 100644 --- a/assets/common/loot_tables/creature/theropod/scale.ron +++ b/assets/common/loot_tables/creature/theropod/scale.ron @@ -1,4 +1,4 @@ [ - (1.0, ItemQuantity("common.items.crafting_ing.animal_misc.raptor_feather", 1, 2)), - (2.0, ItemQuantity("common.items.crafting_ing.hide.scales", 2, 6)), + (1.0, MultiDrop(Item("common.items.crafting_ing.animal_misc.raptor_feather"), 1, 2)), + (2.0, MultiDrop(Item("common.items.crafting_ing.hide.scales"), 2, 6)), ] \ No newline at end of file diff --git a/assets/common/loot_tables/dungeon/sea_chapel/cardinal.ron b/assets/common/loot_tables/dungeon/sea_chapel/cardinal.ron index 537ed1c913..e6f31655c0 100644 --- a/assets/common/loot_tables/dungeon/sea_chapel/cardinal.ron +++ b/assets/common/loot_tables/dungeon/sea_chapel/cardinal.ron @@ -1,6 +1,6 @@ [ (0.5, Item("common.items.crafting_ing.abyssal_heart")), (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")), ] \ No newline at end of file diff --git a/assets/common/loot_tables/dungeon/tier-0/chest.ron b/assets/common/loot_tables/dungeon/tier-0/chest.ron index e5819298be..83248d1713 100644 --- a/assets/common/loot_tables/dungeon/tier-0/chest.ron +++ b/assets/common/loot_tables/dungeon/tier-0/chest.ron @@ -3,12 +3,12 @@ (1.0, LootTable("common.loot_tables.weapons.components.tier-0")), (1.0, LootTable("common.loot_tables.armor.tier-0")), // Currency - (3.0, ItemQuantity("common.items.utility.coins", 10, 20)), + (3.0, MultiDrop(Item("common.items.utility.coins"), 10, 20)), // Materials - (1.0, ItemQuantity("common.items.crafting_ing.cloth.linen", 3, 10)), - (1.0, ItemQuantity("common.items.crafting_ing.leather.simple_leather", 3, 10)), - (1.0, ItemQuantity("common.items.mineral.ingot.bronze", 3, 10)), - (1.0, ItemQuantity("common.items.log.wood", 3, 10)), + (1.0, MultiDrop(Item("common.items.crafting_ing.cloth.linen"), 3, 10)), + (1.0, MultiDrop(Item("common.items.crafting_ing.leather.simple_leather"), 3, 10)), + (1.0, MultiDrop(Item("common.items.mineral.ingot.bronze"), 3, 10)), + (1.0, MultiDrop(Item("common.items.log.wood"), 3, 10)), // Consumables (2.0, LootTable("common.loot_tables.consumable.poor")), ] diff --git a/assets/common/loot_tables/dungeon/tier-0/chieftain.ron b/assets/common/loot_tables/dungeon/tier-0/chieftain.ron index 5f6ffffdfc..733891d6c5 100644 --- a/assets/common/loot_tables/dungeon/tier-0/chieftain.ron +++ b/assets/common/loot_tables/dungeon/tier-0/chieftain.ron @@ -9,7 +9,7 @@ // Chieftain Mask (1.0, Item("common.items.armor.misc.head.gnarling_mask")), // Indirect crafting materials for T2 gear - (1.0, ItemQuantity("common.items.crafting_ing.sticky_thread", 2, 5)), - (1.0, ItemQuantity("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.sticky_thread"), 2, 5)), + (1.0, MultiDrop(Item("common.items.mineral.ore.coal"), 2, 5)), + (1.0, MultiDrop(Item("common.items.crafting_ing.leather.leather_strips"), 2, 5)), ] \ No newline at end of file diff --git a/assets/common/loot_tables/dungeon/tier-0/enemy.ron b/assets/common/loot_tables/dungeon/tier-0/enemy.ron index 7bbde49d1c..7299067b56 100644 --- a/assets/common/loot_tables/dungeon/tier-0/enemy.ron +++ b/assets/common/loot_tables/dungeon/tier-0/enemy.ron @@ -1,6 +1,6 @@ [ // Currency - (1.0, ItemQuantity("common.items.utility.coins", 2, 4)), + (1.0, MultiDrop(Item("common.items.utility.coins"), 2, 4)), // Food (1.0, LootTable("common.loot_tables.food.wild_ingredients")), // Nothing diff --git a/assets/common/loot_tables/dungeon/tier-0/mandragora.ron b/assets/common/loot_tables/dungeon/tier-0/mandragora.ron index 1c84570b88..2f2754925c 100644 --- a/assets/common/loot_tables/dungeon/tier-0/mandragora.ron +++ b/assets/common/loot_tables/dungeon/tier-0/mandragora.ron @@ -1,4 +1,4 @@ [ (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)), ] \ No newline at end of file diff --git a/assets/common/loot_tables/dungeon/tier-0/woodgolem.ron b/assets/common/loot_tables/dungeon/tier-0/woodgolem.ron index 13a3693cbb..9d283b3e90 100644 --- a/assets/common/loot_tables/dungeon/tier-0/woodgolem.ron +++ b/assets/common/loot_tables/dungeon/tier-0/woodgolem.ron @@ -1,11 +1,11 @@ [ // Currency - (1.0, ItemQuantity("common.items.utility.coins", 10, 20)), + (1.0, MultiDrop(Item("common.items.utility.coins"), 10, 20)), // Food (1.0, LootTable("common.loot_tables.food.wild_ingredients")), // Consumables (2.0, LootTable("common.loot_tables.consumable.poor")), // 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")), ] diff --git a/assets/common/loot_tables/dungeon/tier-1/chest.ron b/assets/common/loot_tables/dungeon/tier-1/chest.ron index bfb943eb9b..a0e5d20283 100644 --- a/assets/common/loot_tables/dungeon/tier-1/chest.ron +++ b/assets/common/loot_tables/dungeon/tier-1/chest.ron @@ -4,12 +4,12 @@ (1.0, LootTable("common.loot_tables.armor.tier-1")), (0.5, Item("common.items.armor.misc.head.hog_hood")), // Currency - (3.0, ItemQuantity("common.items.utility.coins", 20, 50)), + (3.0, MultiDrop(Item("common.items.utility.coins"), 20, 50)), // Materials - (1.0, ItemQuantity("common.items.crafting_ing.cloth.wool", 3, 10)), - (1.0, ItemQuantity("common.items.crafting_ing.leather.thick_leather", 3, 10)), - (1.0, ItemQuantity("common.items.mineral.ingot.iron", 3, 10)), - (1.0, ItemQuantity("common.items.log.bamboo", 3, 10)), + (1.0, MultiDrop(Item("common.items.crafting_ing.cloth.wool"), 3, 10)), + (1.0, MultiDrop(Item("common.items.crafting_ing.leather.thick_leather"), 3, 10)), + (1.0, MultiDrop(Item("common.items.mineral.ingot.iron"), 3, 10)), + (1.0, MultiDrop(Item("common.items.log.bamboo"), 3, 10)), // Consumables (2.0, LootTable("common.loot_tables.consumable.poor")), ] diff --git a/assets/common/loot_tables/dungeon/tier-1/enemy.ron b/assets/common/loot_tables/dungeon/tier-1/enemy.ron index 7bda0c6cf5..e1526004b7 100644 --- a/assets/common/loot_tables/dungeon/tier-1/enemy.ron +++ b/assets/common/loot_tables/dungeon/tier-1/enemy.ron @@ -1,6 +1,6 @@ [ // Currency - (1.0, ItemQuantity("common.items.utility.coins", 4, 10)), + (1.0, MultiDrop(Item("common.items.utility.coins"), 4, 10)), // Food (1.0, LootTable("common.loot_tables.food.wild_ingredients")), // Nothing diff --git a/assets/common/loot_tables/dungeon/tier-2/chest.ron b/assets/common/loot_tables/dungeon/tier-2/chest.ron index d0215cd694..19ccf953d2 100644 --- a/assets/common/loot_tables/dungeon/tier-2/chest.ron +++ b/assets/common/loot_tables/dungeon/tier-2/chest.ron @@ -3,12 +3,12 @@ (1.0, LootTable("common.loot_tables.weapons.components.tier-2")), (1.0, LootTable("common.loot_tables.armor.tier-2")), // Currency - (3.0, ItemQuantity("common.items.utility.coins", 50, 100)), + (3.0, MultiDrop(Item("common.items.utility.coins"), 50, 100)), // Materials - (1.0, ItemQuantity("common.items.crafting_ing.cloth.silk", 3, 10)), - (1.0, ItemQuantity("common.items.crafting_ing.hide.scales", 3, 10)), - (1.0, ItemQuantity("common.items.mineral.ingot.steel", 3, 10)), - (1.0, ItemQuantity("common.items.log.hardwood", 3, 10)), + (1.0, MultiDrop(Item("common.items.crafting_ing.cloth.silk"), 3, 10)), + (1.0, MultiDrop(Item("common.items.crafting_ing.hide.scales"), 3, 10)), + (1.0, MultiDrop(Item("common.items.mineral.ingot.steel"), 3, 10)), + (1.0, MultiDrop(Item("common.items.log.hardwood"), 3, 10)), // Consumables (2.0, LootTable("common.loot_tables.consumable.moderate")), ] diff --git a/assets/common/loot_tables/dungeon/tier-2/enemy.ron b/assets/common/loot_tables/dungeon/tier-2/enemy.ron index 9df44a3959..c5e44a860c 100644 --- a/assets/common/loot_tables/dungeon/tier-2/enemy.ron +++ b/assets/common/loot_tables/dungeon/tier-2/enemy.ron @@ -1,6 +1,6 @@ [ // Currency - (1.0, ItemQuantity("common.items.utility.coins", 10, 20)), + (1.0, MultiDrop(Item("common.items.utility.coins"), 10, 20)), // Food (1.0, LootTable("common.loot_tables.food.wild_ingredients")), // Nothing diff --git a/assets/common/loot_tables/dungeon/tier-3/chest.ron b/assets/common/loot_tables/dungeon/tier-3/chest.ron index 0ceaf174a1..544f723012 100644 --- a/assets/common/loot_tables/dungeon/tier-3/chest.ron +++ b/assets/common/loot_tables/dungeon/tier-3/chest.ron @@ -3,13 +3,13 @@ (1.0, LootTable("common.loot_tables.weapons.components.tier-3")), (1.0, LootTable("common.loot_tables.armor.tier-3")), // Currency - (3.0, ItemQuantity("common.items.utility.coins", 100, 200)), + (3.0, MultiDrop(Item("common.items.utility.coins"), 100, 200)), // Materials // Allow ironwood to have higher drops till entity droppers are implemented - (1.0, ItemQuantity("common.items.crafting_ing.cloth.lifecloth", 2, 6)), - (1.0, ItemQuantity("common.items.crafting_ing.hide.carapace", 2, 6)), - (1.0, ItemQuantity("common.items.mineral.ingot.cobalt", 2, 6)), - (1.0, ItemQuantity("common.items.log.ironwood", 3, 7)), + (1.0, MultiDrop(Item("common.items.crafting_ing.cloth.lifecloth"), 2, 6)), + (1.0, MultiDrop(Item("common.items.crafting_ing.hide.carapace"), 2, 6)), + (1.0, MultiDrop(Item("common.items.mineral.ingot.cobalt"), 2, 6)), + (1.0, MultiDrop(Item("common.items.log.ironwood"), 3, 7)), // Consumables (2.0, LootTable("common.loot_tables.consumable.moderate")), ] diff --git a/assets/common/loot_tables/dungeon/tier-3/enemy.ron b/assets/common/loot_tables/dungeon/tier-3/enemy.ron index fb51a5f9be..84bdc54e23 100644 --- a/assets/common/loot_tables/dungeon/tier-3/enemy.ron +++ b/assets/common/loot_tables/dungeon/tier-3/enemy.ron @@ -1,6 +1,6 @@ [ // Currency - (1.0, ItemQuantity("common.items.utility.coins", 20, 40)), + (1.0, MultiDrop(Item("common.items.utility.coins"), 20, 40)), // Food (1.0, LootTable("common.loot_tables.food.prepared")), // Nothing diff --git a/assets/common/loot_tables/dungeon/tier-4/boss.ron b/assets/common/loot_tables/dungeon/tier-4/boss.ron index d8fa689f71..b77ff45d5a 100644 --- a/assets/common/loot_tables/dungeon/tier-4/boss.ron +++ b/assets/common/loot_tables/dungeon/tier-4/boss.ron @@ -8,8 +8,8 @@ (1.0, LootTable("common.loot_tables.weapons.legendary_melee")), // Crafting material // 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, ItemQuantity("common.items.crafting_ing.hide.dragon_scale", 3, 8)), - (1.0, ItemQuantity("common.items.mineral.ingot.orichalcum", 1, 3)), - (1.0, ItemQuantity("common.items.log.eldwood", 2, 6)), + (1.0, MultiDrop(Item("common.items.crafting_ing.cloth.sunsilk"), 1, 3)), + (1.0, MultiDrop(Item("common.items.crafting_ing.hide.dragon_scale"), 3, 8)), + (1.0, MultiDrop(Item("common.items.mineral.ingot.orichalcum"), 1, 3)), + (1.0, MultiDrop(Item("common.items.log.eldwood"), 2, 6)), ] diff --git a/assets/common/loot_tables/dungeon/tier-4/chest.ron b/assets/common/loot_tables/dungeon/tier-4/chest.ron index bd2322df3a..8f6b7e16e9 100644 --- a/assets/common/loot_tables/dungeon/tier-4/chest.ron +++ b/assets/common/loot_tables/dungeon/tier-4/chest.ron @@ -4,13 +4,13 @@ (1.0, LootTable("common.loot_tables.armor.tier-4")), (1.0, Item("common.items.armor.misc.head.spikeguard")), // Currency - (3.0, ItemQuantity("common.items.utility.coins", 200, 500)), + (3.0, MultiDrop(Item("common.items.utility.coins"), 200, 500)), // Materials // Allow frostwood to have higher drops till entity droppers are implemented - (1.0, ItemQuantity("common.items.crafting_ing.cloth.moonweave", 1, 5)), - (1.0, ItemQuantity("common.items.crafting_ing.hide.plate", 1, 5)), - (1.0, ItemQuantity("common.items.mineral.ingot.bloodsteel", 1, 5)), - (1.0, ItemQuantity("common.items.log.frostwood", 3, 6)), + (1.0, MultiDrop(Item("common.items.crafting_ing.cloth.moonweave"), 1, 5)), + (1.0, MultiDrop(Item("common.items.crafting_ing.hide.plate"), 1, 5)), + (1.0, MultiDrop(Item("common.items.mineral.ingot.bloodsteel"), 1, 5)), + (1.0, MultiDrop(Item("common.items.log.frostwood"), 3, 6)), // Consumables (2.0, LootTable("common.loot_tables.consumable.good")), ] diff --git a/assets/common/loot_tables/dungeon/tier-4/enemy.ron b/assets/common/loot_tables/dungeon/tier-4/enemy.ron index 19e8adf6aa..12a7efe81b 100644 --- a/assets/common/loot_tables/dungeon/tier-4/enemy.ron +++ b/assets/common/loot_tables/dungeon/tier-4/enemy.ron @@ -1,6 +1,6 @@ [ // Currency - (1.0, ItemQuantity("common.items.utility.coins", 40, 100)), + (1.0, MultiDrop(Item("common.items.utility.coins"), 40, 100)), // Food (1.0, LootTable("common.loot_tables.food.prepared")), // Nothing diff --git a/assets/common/loot_tables/dungeon/tier-4/miniboss.ron b/assets/common/loot_tables/dungeon/tier-4/miniboss.ron index e384bcaa07..e8572cfaf0 100644 --- a/assets/common/loot_tables/dungeon/tier-4/miniboss.ron +++ b/assets/common/loot_tables/dungeon/tier-4/miniboss.ron @@ -1,6 +1,6 @@ [ // Currency - (10.0, ItemQuantity("common.items.utility.coins", 200, 500)), + (10.0, MultiDrop(Item("common.items.utility.coins"), 200, 500)), // Food (5.0, LootTable("common.loot_tables.food.prepared")), // Consumables diff --git a/assets/common/loot_tables/dungeon/tier-5/boss.ron b/assets/common/loot_tables/dungeon/tier-5/boss.ron index 3b7572089d..1d7e2abdbb 100644 --- a/assets/common/loot_tables/dungeon/tier-5/boss.ron +++ b/assets/common/loot_tables/dungeon/tier-5/boss.ron @@ -13,8 +13,8 @@ // Crafting material // 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, ItemQuantity("common.items.crafting_ing.cloth.sunsilk", 1, 3)), - (1.0, ItemQuantity("common.items.crafting_ing.hide.dragon_scale", 3, 8)), - (1.0, ItemQuantity("common.items.mineral.ingot.orichalcum", 1, 3)), - (1.0, ItemQuantity("common.items.log.eldwood", 2, 6)), + (1.0, MultiDrop(Item("common.items.crafting_ing.cloth.sunsilk"), 1, 3)), + (1.0, MultiDrop(Item("common.items.crafting_ing.hide.dragon_scale"), 3, 8)), + (1.0, MultiDrop(Item("common.items.mineral.ingot.orichalcum"), 1, 3)), + (1.0, MultiDrop(Item("common.items.log.eldwood"), 2, 6)), ] diff --git a/assets/common/loot_tables/dungeon/tier-5/chest.ron b/assets/common/loot_tables/dungeon/tier-5/chest.ron index 08890c26b2..df1f89bd92 100644 --- a/assets/common/loot_tables/dungeon/tier-5/chest.ron +++ b/assets/common/loot_tables/dungeon/tier-5/chest.ron @@ -4,13 +4,13 @@ (1.0, LootTable("common.loot_tables.armor.tier-4")), (0.1, Item("common.items.armor.cultist.bandana")), // Currency - (3.0, ItemQuantity("common.items.utility.coins", 200, 500)), + (3.0, MultiDrop(Item("common.items.utility.coins"), 200, 500)), // Materials - (1.0, ItemQuantity("common.items.crafting_ing.cloth.moonweave", 1, 5)), - (1.0, ItemQuantity("common.items.crafting_ing.hide.plate", 1, 5)), - (1.0, ItemQuantity("common.items.mineral.ingot.bloodsteel", 1, 5)), - (1.0, ItemQuantity("common.items.log.frostwood", 1, 5)), + (1.0, MultiDrop(Item("common.items.crafting_ing.cloth.moonweave"), 1, 5)), + (1.0, MultiDrop(Item("common.items.crafting_ing.hide.plate"), 1, 5)), + (1.0, MultiDrop(Item("common.items.mineral.ingot.bloodsteel"), 1, 5)), + (1.0, MultiDrop(Item("common.items.log.frostwood"), 1, 5)), // Consumables (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)), ] diff --git a/assets/common/loot_tables/dungeon/tier-5/enemy.ron b/assets/common/loot_tables/dungeon/tier-5/enemy.ron index 106ef0b2ad..2389b3372d 100644 --- a/assets/common/loot_tables/dungeon/tier-5/enemy.ron +++ b/assets/common/loot_tables/dungeon/tier-5/enemy.ron @@ -1,8 +1,8 @@ [ // Currency - (1.0, ItemQuantity("common.items.utility.coins", 100, 200)), + (1.0, MultiDrop(Item("common.items.utility.coins"), 100, 200)), // Food (1.0, LootTable("common.loot_tables.food.prepared")), // Cheese - (2.0, ItemQuantity("common.items.food.cheese", 3, 5)), + (2.0, MultiDrop(Item("common.items.food.cheese"), 3, 5)), ] diff --git a/assets/common/loot_tables/dungeon/tier-5/miniboss.ron b/assets/common/loot_tables/dungeon/tier-5/miniboss.ron index 221f592fd6..706ff34223 100644 --- a/assets/common/loot_tables/dungeon/tier-5/miniboss.ron +++ b/assets/common/loot_tables/dungeon/tier-5/miniboss.ron @@ -1,6 +1,6 @@ [ // Currency - (10.0, ItemQuantity("common.items.utility.coins", 500, 1000)), + (10.0, MultiDrop(Item("common.items.utility.coins"), 500, 1000)), // Food (5.0, LootTable("common.loot_tables.food.prepared")), // Consumables diff --git a/assets/common/loot_tables/dungeon/tier-5/minion.ron b/assets/common/loot_tables/dungeon/tier-5/minion.ron index 0f01e36ee2..8afdbda546 100644 --- a/assets/common/loot_tables/dungeon/tier-5/minion.ron +++ b/assets/common/loot_tables/dungeon/tier-5/minion.ron @@ -1,6 +1,6 @@ [ // Currency - (30.0, ItemQuantity("common.items.utility.coins", 50, 100)), + (30.0, MultiDrop(Item("common.items.utility.coins"), 50, 100)), // Nothing (30.0, Nothing), // Special diff --git a/assets/common/loot_tables/world/traveler0.ron b/assets/common/loot_tables/world/traveler0.ron index 70802dd42b..16ef614570 100644 --- a/assets/common/loot_tables/world/traveler0.ron +++ b/assets/common/loot_tables/world/traveler0.ron @@ -1,10 +1,10 @@ [ //Currency - (4.0, ItemQuantity("common.items.utility.coins", 50, 200)), + (4.0, MultiDrop(Item("common.items.utility.coins"), 50, 200)), //Food (4.0, LootTable("common.loot_tables.food.prepared")), //Flowers, pretty - (2.0, ItemQuantity("common.items.flowers.red", 3, 6)), + (2.0, MultiDrop(Item("common.items.flowers.red"), 3, 6)), //Weapon components (2.0, LootTable("common.loot_tables.weapons.components.tier-0")), (1.0, LootTable("common.loot_tables.weapons.components.tier-1")), diff --git a/assets/common/loot_tables/world/traveler1.ron b/assets/common/loot_tables/world/traveler1.ron index f28ce37791..a2cee23e0f 100644 --- a/assets/common/loot_tables/world/traveler1.ron +++ b/assets/common/loot_tables/world/traveler1.ron @@ -1,15 +1,15 @@ [ //Currency - (3.0, ItemQuantity("common.items.utility.coins", 50, 200)), - (2.0, ItemQuantity("common.items.utility.coins", 200, 500)), + (3.0, MultiDrop(Item("common.items.utility.coins"), 50, 200)), + (2.0, MultiDrop(Item("common.items.utility.coins"), 200, 500)), //Food (3.0, LootTable("common.loot_tables.food.prepared")), //Ores - (2.0, ItemQuantity("common.items.mineral.ore.iron", 2, 7)), + (2.0, MultiDrop(Item("common.items.mineral.ore.iron"), 2, 7)), //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 - (2.0, ItemQuantity("common.items.flowers.red", 3, 6)), + (2.0, MultiDrop(Item("common.items.flowers.red"), 3, 6)), //Weapon components (1.0, LootTable("common.loot_tables.weapons.components.tier-2")), (1.0, LootTable("common.loot_tables.weapons.components.tier-3")), diff --git a/assets/common/loot_tables/world/traveler2.ron b/assets/common/loot_tables/world/traveler2.ron index 28b2684d0e..87a0f842c0 100644 --- a/assets/common/loot_tables/world/traveler2.ron +++ b/assets/common/loot_tables/world/traveler2.ron @@ -1,21 +1,21 @@ [ //Currency - (3.0, ItemQuantity("common.items.utility.coins", 200, 500)), - (2.0, ItemQuantity("common.items.utility.coins", 2000, 5000)), + (3.0, MultiDrop(Item("common.items.utility.coins"), 200, 500)), + (2.0, MultiDrop(Item("common.items.utility.coins"), 2000, 5000)), //Food (3.0, LootTable("common.loot_tables.food.prepared")), //Ores - (2.0, ItemQuantity("common.items.mineral.ore.coal", 5, 15)), - (2.0, ItemQuantity("common.items.mineral.ore.iron", 2, 7)), - (1.0, ItemQuantity("common.items.mineral.ore.cobalt", 2, 5)), - (0.1, ItemQuantity("common.items.mineral.gem.diamond", 2, 5)), + (2.0, MultiDrop(Item("common.items.mineral.ore.coal"), 5, 15)), + (2.0, MultiDrop(Item("common.items.mineral.ore.iron"), 2, 7)), + (1.0, MultiDrop(Item("common.items.mineral.ore.cobalt"), 2, 5)), + (0.1, MultiDrop(Item("common.items.mineral.gem.diamond"), 2, 5)), //Hides - (2.0, ItemQuantity("common.items.crafting_ing.hide.animal_hide", 5, 15)), - (2.0, ItemQuantity("common.items.crafting_ing.hide.scales", 5, 15)), - (1.0, ItemQuantity("common.items.crafting_ing.hide.carapace", 3, 10)), - (1.0, ItemQuantity("common.items.crafting_ing.hide.tough_hide", 3, 10)), + (2.0, MultiDrop(Item("common.items.crafting_ing.hide.animal_hide"), 5, 15)), + (2.0, MultiDrop(Item("common.items.crafting_ing.hide.scales"), 5, 15)), + (1.0, MultiDrop(Item("common.items.crafting_ing.hide.carapace"), 3, 10)), + (1.0, MultiDrop(Item("common.items.crafting_ing.hide.tough_hide"), 3, 10)), //Flowers, very pretty - (2.0, ItemQuantity("common.items.flowers.red", 5, 10)), + (2.0, MultiDrop(Item("common.items.flowers.red"), 5, 10)), //Weapon components (1.0, LootTable("common.loot_tables.weapons.components.tier-2")), (1.0, LootTable("common.loot_tables.weapons.components.tier-3")), diff --git a/assets/common/loot_tables/world/traveler3.ron b/assets/common/loot_tables/world/traveler3.ron index 0958dff409..0da21a3a25 100644 --- a/assets/common/loot_tables/world/traveler3.ron +++ b/assets/common/loot_tables/world/traveler3.ron @@ -1,25 +1,25 @@ [ //Currency - (2.0, ItemQuantity("common.items.utility.coins", 200, 500)), - (1.0, ItemQuantity("common.items.utility.coins", 2000, 5000)), + (2.0, MultiDrop(Item("common.items.utility.coins"), 200, 500)), + (1.0, MultiDrop(Item("common.items.utility.coins"), 2000, 5000)), //Food (4.0, LootTable("common.loot_tables.food.prepared")), //Ores - (2.0, ItemQuantity("common.items.mineral.ore.coal", 10, 30)), - (2.0, ItemQuantity("common.items.mineral.ore.iron", 4, 14)), - (1.0, ItemQuantity("common.items.mineral.ore.cobalt", 4, 10)), - (0.1, ItemQuantity("common.items.mineral.gem.diamond", 4, 10)), + (2.0, MultiDrop(Item("common.items.mineral.ore.coal"), 10, 30)), + (2.0, MultiDrop(Item("common.items.mineral.ore.iron"), 4, 14)), + (1.0, MultiDrop(Item("common.items.mineral.ore.cobalt"), 4, 10)), + (0.1, MultiDrop(Item("common.items.mineral.gem.diamond"), 4, 10)), //Hides - (2.0, ItemQuantity("common.items.crafting_ing.hide.animal_hide", 10, 30)), - (2.0, ItemQuantity("common.items.crafting_ing.hide.scales", 10, 30)), - (1.0, ItemQuantity("common.items.crafting_ing.hide.carapace", 6, 20)), - (1.0, ItemQuantity("common.items.crafting_ing.hide.tough_hide", 6, 20)), - (0.3, ItemQuantity("common.items.crafting_ing.hide.plate", 1, 8)), - (0.1, ItemQuantity("common.items.crafting_ing.hide.rugged_hide", 1, 6)), + (2.0, MultiDrop(Item("common.items.crafting_ing.hide.animal_hide"), 10, 30)), + (2.0, MultiDrop(Item("common.items.crafting_ing.hide.scales"), 10, 30)), + (1.0, MultiDrop(Item("common.items.crafting_ing.hide.carapace"), 6, 20)), + (1.0, MultiDrop(Item("common.items.crafting_ing.hide.tough_hide"), 6, 20)), + (0.3, MultiDrop(Item("common.items.crafting_ing.hide.plate"), 1, 8)), + (0.1, MultiDrop(Item("common.items.crafting_ing.hide.rugged_hide"), 1, 6)), //Flowers, very pretty - (3.0, ItemQuantity("common.items.flowers.red", 10, 20)), - (2.0, ItemQuantity("common.items.flowers.moonbell", 6, 12)), - (1.0, ItemQuantity("common.items.flowers.pyrebloom", 3, 6)), + (3.0, MultiDrop(Item("common.items.flowers.red"), 10, 20)), + (2.0, MultiDrop(Item("common.items.flowers.moonbell"), 6, 12)), + (1.0, MultiDrop(Item("common.items.flowers.pyrebloom"), 3, 6)), //Weapon components (1.5, LootTable("common.loot_tables.weapons.components.tier-4")), (0.2, LootTable("common.loot_tables.weapons.components.tier-5")), diff --git a/assets/common/loot_tables/world/world_bosses/gigas_frost/boss.ron b/assets/common/loot_tables/world/world_bosses/gigas_frost/boss.ron index 0e599748f5..001dea25f2 100644 --- a/assets/common/loot_tables/world/world_bosses/gigas_frost/boss.ron +++ b/assets/common/loot_tables/world/world_bosses/gigas_frost/boss.ron @@ -1,4 +1,4 @@ [ (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)), ] \ No newline at end of file diff --git a/assets/common/loot_tables/world/world_bosses/gigas_frost/summon.ron b/assets/common/loot_tables/world/world_bosses/gigas_frost/summon.ron index c1626bb6f6..49375c565a 100644 --- a/assets/common/loot_tables/world/world_bosses/gigas_frost/summon.ron +++ b/assets/common/loot_tables/world/world_bosses/gigas_frost/summon.ron @@ -1,6 +1,6 @@ [ // Currency - (1.0, ItemQuantity("common.items.utility.coins", 40, 100)), + (1.0, MultiDrop(Item("common.items.utility.coins"), 40, 100)), // Food (1.0, LootTable("common.loot_tables.food.prepared")), // Nothing diff --git a/client/src/lib.rs b/client/src/lib.rs index 5ae8d350b8..d52ea4292e 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -102,7 +102,7 @@ pub enum Event { }, Disconnect, DisconnectionNotification(u64), - InventoryUpdated(InventoryUpdateEvent), + InventoryUpdated(Vec), Kicked(String), Notification(Notification), SetViewDistance(u32), @@ -2388,34 +2388,38 @@ impl Client { self.presence = None; self.clean_state(); }, - ServerGeneral::InventoryUpdate(inventory, event) => { - match event { - InventoryUpdateEvent::BlockCollectFailed { .. } => {}, - InventoryUpdateEvent::EntityCollectFailed { .. } => {}, - _ => { - // Push the updated inventory component to the client - // 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 - // client back to login? - let entity = self.entity(); - if let Err(e) = self - .state - .ecs_mut() - .write_storage() - .insert(entity, inventory) - { - warn!( - ?e, - "Received an inventory update event for client entity, but this \ - entity was not found... this may be a bug." - ); - } - }, + ServerGeneral::InventoryUpdate(inventory, events) => { + let mut update_inventory = false; + for event in events.iter() { + match event { + InventoryUpdateEvent::BlockCollectFailed { .. } => {}, + InventoryUpdateEvent::EntityCollectFailed { .. } => {}, + _ => update_inventory = true, + } + } + if update_inventory { + // Push the updated inventory component to the client + // 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 + // client back to login? + let entity = self.entity(); + if let Err(e) = self + .state + .ecs_mut() + .write_storage() + .insert(entity, inventory) + { + warn!( + ?e, + "Received an inventory update event for client entity, but this \ + entity was not found... this may be a bug." + ); + } } self.update_available_recipes(); - frontend_events.push(Event::InventoryUpdated(event)); + frontend_events.push(Event::InventoryUpdated(events)); }, ServerGeneral::SetViewDistance(vd) => { self.view_distance = Some(vd); diff --git a/common/Cargo.toml b/common/Cargo.toml index e3ddc7d43b..5c1f61d6cb 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -91,6 +91,7 @@ rand_chacha = "0.3" tracing-subscriber = { version = "0.3.7", default-features = false, features = ["fmt", "time", "ansi", "smallvec", "env-filter"] } petgraph = "0.6.0" + [[bench]] name = "chonk_benchmark" harness = false @@ -99,6 +100,10 @@ harness = false name = "color_benchmark" harness = false +[[bench]] +name = "loot_benchmark" +harness = false + [[bin]] name = "csv_export" required-features = ["bin_csv"] diff --git a/common/benches/loot_benchmark.rs b/common/benches/loot_benchmark.rs new file mode 100644 index 0000000000..9bb53d9e21 --- /dev/null +++ b/common/benches/loot_benchmark.rs @@ -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::>(); + 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::>(); + 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::>(); + 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::>(); + 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::>(); + 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::>(); + 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::>(); + 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::>(); + 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); diff --git a/common/net/src/msg/server.rs b/common/net/src/msg/server.rs index a40a932bab..ca2d87c9e2 100644 --- a/common/net/src/msg/server.rs +++ b/common/net/src/msg/server.rs @@ -170,7 +170,7 @@ pub enum ServerGeneral { /// Trigger cleanup for when the client goes back to the `Registered` state /// from an ingame state ExitInGameSuccess, - InventoryUpdate(comp::Inventory, comp::InventoryUpdateEvent), + InventoryUpdate(comp::Inventory, Vec), /// 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 /// modified). So we just need to send the terrain VD back to the client diff --git a/common/src/bin/csv_export/main.rs b/common/src/bin/csv_export/main.rs index 5b41bd5d86..bc45460217 100644 --- a/common/src/bin/csv_export/main.rs +++ b/common/src/bin/csv_export/main.rs @@ -254,48 +254,53 @@ fn loot_table(loot_table: &str) -> Result<(), Box> { .div(10_f32.powi(5)) .to_string(); - let get_hands = |hands| match hands { - Some(Hands::One) => "One", - Some(Hands::Two) => "Two", - None => "", - }; - - match 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) => { - wtr.write_record([&chance, "LootTable", table, "", ""])? - }, - LootSpec::Nothing => wtr.write_record([&chance, "Nothing", "", ""])?, - LootSpec::ModularWeapon { - tool, - material, - hands, - } => wtr.write_record([ - &chance, - "Modular Weapon", - &get_tool_kind(tool), - material.into(), - get_hands(*hands), - ])?, - LootSpec::ModularWeaponPrimaryComponent { - tool, - material, - hands, - } => wtr.write_record([ - &chance, - "Modular Weapon Primary Component", - &get_tool_kind(tool), - material.into(), - get_hands(*hands), - ])?, + fn write_loot_spec( + wtr: &mut csv::Writer, + loot_spec: &LootSpec, + chance: &str, + ) -> Result<(), Box> { + let get_hands = |hands| match hands { + Some(Hands::One) => "One", + Some(Hands::Two) => "Two", + None => "", + }; + match loot_spec { + LootSpec::Item(item) => wtr.write_record([chance, "Item", item, "", ""])?, + LootSpec::LootTable(table) => { + wtr.write_record([chance, "LootTable", table, "", ""])? + }, + LootSpec::Nothing => wtr.write_record([chance, "Nothing", "", ""])?, + LootSpec::ModularWeapon { + tool, + material, + hands, + } => wtr.write_record([ + chance, + "Modular Weapon", + &get_tool_kind(tool), + material.into(), + get_hands(*hands), + ])?, + LootSpec::ModularWeaponPrimaryComponent { + tool, + material, + hands, + } => wtr.write_record([ + chance, + "Modular Weapon Primary Component", + &get_tool_kind(tool), + material.into(), + 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()?; @@ -406,16 +411,6 @@ fn entity_drops(entity_config: &str) -> Result<(), Box> { "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 => { wtr.write_record(&[ name.clone(), @@ -477,6 +472,7 @@ fn entity_drops(entity_config: &str) -> Result<(), Box> { } }, LootSpec::LootTable(_) => unreachable!(), + LootSpec::MultiDrop(_, _, _) => todo!(), } } diff --git a/common/src/comp/inventory/item/mod.rs b/common/src/comp/inventory/item/mod.rs index e6e6915696..a07575aad4 100644 --- a/common/src/comp/inventory/item/mod.rs +++ b/common/src/comp/inventory/item/mod.rs @@ -921,6 +921,34 @@ impl Item { new_item } + pub fn stacked_duplicates<'a>( + &'a self, + ability_map: &'a AbilityMap, + msm: &'a MaterialStatManifest, + count: u32, + ) -> impl Iterator + '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 /// 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 @@ -1167,8 +1195,8 @@ impl Item { 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 { - block.get_sprite()?.collectible_id()??.to_item() + pub fn try_reclaim_from_block(block: Block) -> Option> { + block.get_sprite()?.collectible_id()??.to_items() } pub fn ability_spec(&self) -> Option> { @@ -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 + 'a { + items + .iter() + .flat_map(|(count, item)| item.stacked_duplicates(ability_map, msm, *count)) +} + /// Provides common methods providing details about an item definition /// for either an `Item` containing the definition, or the actual `ItemDef` pub trait ItemDesc { @@ -1370,9 +1408,9 @@ impl Component for Item { } #[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; } diff --git a/common/src/comp/inventory/mod.rs b/common/src/comp/inventory/mod.rs index 4d15586854..53b3ddfffb 100644 --- a/common/src/comp/inventory/mod.rs +++ b/common/src/comp/inventory/mod.rs @@ -995,13 +995,19 @@ impl Default for InventoryUpdateEvent { #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct InventoryUpdate { - event: InventoryUpdateEvent, + events: Vec, } 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 { std::mem::take(&mut self.events) } } impl Component for InventoryUpdate { diff --git a/common/src/comp/inventory/trade_pricing.rs b/common/src/comp/inventory/trade_pricing.rs index a46a40bcdb..2c99f8a4b9 100644 --- a/common/src/comp/inventory/trade_pricing.rs +++ b/common/src/comp/inventory/trade_pricing.rs @@ -362,74 +362,84 @@ impl From)>> for ProbabilityFile { } else { 1.0 / content.iter().map(|e| e.0).sum::() }; + fn get_content( + rescale: f32, + p0: f32, + loot: LootSpec, + ) -> Vec<(f32, ItemDefinitionIdOwned, f32)> { + match loot { + LootSpec::Item(asset) => { + vec![(p0 * rescale, ItemDefinitionIdOwned::Simple(asset), 1.0)] + }, + LootSpec::LootTable(table_asset) => { + let unscaled = &ProbabilityFile::load_expect(&table_asset).read().content; + let scale = p0 * rescale; + unscaled + .iter() + .map(|(p1, asset, amount)| (*p1 * scale, asset.clone(), *amount)) + .collect::>() + }, + LootSpec::ModularWeapon { + tool, + material, + hands, + } => { + let mut primary = expand_primary_component(tool, material, hands); + let secondary: Vec = + expand_secondary_component(tool, material, hands).collect(); + let freq = if primary.is_empty() || secondary.is_empty() { + 0.0 + } else { + p0 * rescale / ((primary.len() * secondary.len()) as f32) + }; + let res: Vec<(f32, ItemDefinitionIdOwned, f32)> = primary + .drain(0..) + .flat_map(|p| { + secondary.iter().map(move |s| { + let components = vec![p.clone(), s.clone()]; + ( + freq, + ItemDefinitionIdOwned::Modular { + pseudo_base: ModularBase::Tool.pseudo_item_id().into(), + components, + }, + 1.0f32, + ) + }) + }) + .collect(); + res + }, + LootSpec::ModularWeaponPrimaryComponent { + tool, + material, + hands, + } => { + let mut res = expand_primary_component(tool, material, hands); + let freq = if res.is_empty() { + 0.0 + } else { + p0 * rescale / (res.len() as f32) + }; + let res: Vec<(f32, ItemDefinitionIdOwned, f32)> = + res.drain(0..).map(|e| (freq, e, 1.0f32)).collect(); + res + }, + 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)| match loot { - LootSpec::Item(asset) => { - 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) => { - let unscaled = &Self::load_expect(&table_asset).read().content; - let scale = p0 * rescale; - unscaled - .iter() - .map(|(p1, asset, amount)| (*p1 * scale, asset.clone(), *amount)) - .collect::>() - }, - LootSpec::ModularWeapon { - tool, - material, - hands, - } => { - let mut primary = expand_primary_component(tool, material, hands); - let secondary: Vec = - expand_secondary_component(tool, material, hands).collect(); - let freq = if primary.is_empty() || secondary.is_empty() { - 0.0 - } else { - p0 * rescale / ((primary.len() * secondary.len()) as f32) - }; - let res: Vec<(f32, ItemDefinitionIdOwned, f32)> = primary - .drain(0..) - .flat_map(|p| { - secondary.iter().map(move |s| { - let components = vec![p.clone(), s.clone()]; - ( - freq, - ItemDefinitionIdOwned::Modular { - pseudo_base: ModularBase::Tool.pseudo_item_id().into(), - components, - }, - 1.0f32, - ) - }) - }) - .collect(); - res - }, - LootSpec::ModularWeaponPrimaryComponent { - tool, - material, - hands, - } => { - let mut res = expand_primary_component(tool, material, hands); - let freq = if res.is_empty() { - 0.0 - } else { - p0 * rescale / (res.len() as f32) - }; - let res: Vec<(f32, ItemDefinitionIdOwned, f32)> = - res.drain(0..).map(|e| (freq, e, 1.0f32)).collect(); - res - }, - LootSpec::Nothing => Vec::new(), - }) + .flat_map(|(p0, loot)| get_content(rescale, p0, loot)) .collect(), } } @@ -1231,12 +1241,4 @@ mod tests { let probability: ProbabilityFile = loot_table.into(); 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)); - } } diff --git a/common/src/comp/loot_owner.rs b/common/src/comp/loot_owner.rs index 17f0fb7500..fdb0646927 100644 --- a/common/src/comp/loot_owner.rs +++ b/common/src/comp/loot_owner.rs @@ -74,7 +74,7 @@ impl Component for LootOwner { type Storage = DerefFlaggedStorage>; } -#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum LootOwnerKind { Player(Uid), Group(Group), diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index 12a8de0a06..2e4afeee61 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -92,7 +92,7 @@ pub use self::{ self, item_key::ItemKey, tool::{self, AbilityItem}, - Item, ItemConfig, ItemDrop, + Item, ItemConfig, ItemDrops, }, slot, CollectFailedReason, Inventory, InventoryUpdate, InventoryUpdateEvent, }, diff --git a/common/src/lottery.rs b/common/src/lottery.rs index d3c627a996..a30d8c12f1 100644 --- a/common/src/lottery.rs +++ b/common/src/lottery.rs @@ -26,6 +26,8 @@ // Cheese drop rate = 3/X = 29.6% // Coconut drop rate = 1/X = 9.85% +use std::hash::Hash; + use crate::{ assets::{self, AssetExt}, comp::{inventory::item, Item}, @@ -76,12 +78,136 @@ impl Lottery { pub fn total(&self) -> f32 { self.total } } +/// Try to distribute stacked items fairly between weighted participants. +pub fn distribute_many( + participants: impl IntoIterator, + rng: &mut impl Rng, + items: &[I], + mut get_amount: impl FnMut(&I) -> u32, + mut exec_item: impl FnMut(&I, T, u32), +) { + struct Participant { + // weight / total + weight: f32, + sorted_weight: f32, + data: T, + recieved_count: u32, + current_recieved_count: u32, + } + + impl Participant { + 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::>(); + + let total_item_amount = items.iter().map(&mut get_amount).sum::(); + + 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)] pub enum LootSpec> { /// Asset specifier Item(T), - /// Asset specifier, lower range, upper range - ItemQuantity(T, u32, u32), /// Loot table LootTable(T), /// No loot given @@ -97,78 +223,129 @@ pub enum LootSpec> { material: item::Material, hands: Option, }, + /// LootSpec, lower range, upper range + MultiDrop(Box>, u32, u32), } impl> LootSpec { - pub fn to_item(&self) -> Option { - let mut rng = thread_rng(); - match self { - Self::Item(item) => Item::new_from_asset(item.as_ref()).map_or_else( + fn to_items_inner( + &self, + rng: &mut rand::rngs::ThreadRng, + amount: u32, + items: &mut Vec<(u32, Item)>, + ) { + let convert_item = |item: &T| { + Item::new_from_asset(item.as_ref()).map_or_else( |e| { warn!(?e, "error while loading item: {}", item.as_ref()); None }, Some, - ), - Self::ItemQuantity(item, lower, upper) => { - let range = *lower..=*upper; - let quantity = thread_rng().gen_range(range); - match Item::new_from_asset(item.as_ref()) { - Ok(mut item) => { - // TODO: Handle multiple of an item that is unstackable - if item.set_amount(quantity).is_err() { - warn!("Tried to set quantity on non stackable item"); - } - Some(item) - }, - Err(e) => { - warn!(?e, "error while loading item: {}", item.as_ref()); - None - }, + ) + }; + let mut push_item = |mut item: Item, count: u32| { + let count = item.amount().saturating_mul(count); + item.set_amount(1).expect("1 is always a valid amount."); + let hash = item.item_hash(); + match items.binary_search_by_key(&hash, |(_, item)| item.item_hash()) { + Ok(i) => { + // Since item hash can collide with other items, we search nearby items with the + // same hash. + // NOTE: The `ParitalEq` implementation for `Item` doesn't compare some data + // like durability, or wether slots contain anything. Although since these are + // Newly loaded items we don't care about comparing those for deduplication + // here. + 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)); + } + }, + Err(i) => items.insert(i, (count, item)), + } + }; + + match self { + Self::Item(item) => { + if let Some(item) = convert_item(item) { + push_item(item, amount); } }, - Self::LootTable(table) => Lottery::>::load_expect(table.as_ref()) - .read() - .choose() - .to_item(), - Self::Nothing => None, + Self::LootTable(table) => { + let loot_spec = Lottery::>::load_expect(table.as_ref()).read(); + for _ in 0..amount { + loot_spec.choose().to_items_inner(rng, 1, items) + } + }, + Self::Nothing => {}, Self::ModularWeapon { tool, material, hands, - } => item::modular::random_weapon(*tool, *material, *hands, &mut rng).map_or_else( - |e| { - warn!( - ?e, - "error while creating modular weapon. Toolkind: {:?}, Material: {:?}, \ - Hands: {:?}", - tool, - material, - hands, - ); - None - }, - Some, - ), + } => { + for _ in 0..amount { + match item::modular::random_weapon(*tool, *material, *hands, rng) { + Ok(item) => push_item(item, 1), + Err(e) => { + warn!( + ?e, + "error while creating modular weapon. Toolkind: {:?}, Material: \ + {:?}, Hands: {:?}", + tool, + material, + hands, + ); + }, + } + } + }, Self::ModularWeaponPrimaryComponent { tool, material, hands, - } => item::modular::random_weapon_primary_component(*tool, *material, *hands, &mut rng) - .map_or_else( - |e| { - warn!( - ?e, - "error while creating modular weapon primary component. Toolkind: \ - {:?}, Material: {:?}, Hands: {:?}", - tool, - material, - hands, - ); - None - }, - |(comp, _)| Some(comp), - ), + } => { + for _ in 0..amount { + match item::modular::random_weapon(*tool, *material, *hands, rng) { + Ok(item) => push_item(item, 1), + Err(e) => { + warn!( + ?e, + "error while creating modular weapon primary component. Toolkind: \ + {:?}, Material: {:?}, Hands: {:?}", + tool, + material, + hands, + ); + }, + } + } + }, + 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> { + 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) => { 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) => { let loot_table = Lottery::>::load_expect_cloned(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()); } } + + #[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!(), + }, + ); + } + } } diff --git a/common/state/src/state.rs b/common/state/src/state.rs index 20224a5748..b188e7d30b 100644 --- a/common/state/src/state.rs +++ b/common/state/src/state.rs @@ -253,7 +253,7 @@ impl State { ecs.register::(); ecs.register::(); ecs.register::(); - ecs.register::(); + ecs.register::(); ecs.register::(); ecs.register::(); ecs.register::(); diff --git a/server/src/cmd.rs b/server/src/cmd.rs index a5b37f8e10..ecf79226bf 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -577,12 +577,20 @@ fn handle_give_item( }); } - insert_or_replace_component( - server, - target, - comp::InventoryUpdate::new(comp::InventoryUpdateEvent::Given), - "target", - )?; + let mut inventory_update = server + .state + .ecs_mut() + .write_storage::(); + if let Some(update) = inventory_update.get_mut(target) { + update.push(comp::InventoryUpdateEvent::Given); + } else { + inventory_update + .insert( + target, + comp::InventoryUpdate::new(comp::InventoryUpdateEvent::Given), + ) + .map_err(|_| "Entity target is dead!")?; + } res } else { Err(format!("Invalid item: {}", item_name)) @@ -686,8 +694,8 @@ fn handle_make_npc( entity_builder = entity_builder.with(agent); } - if let Some(drop_item) = loot.to_item() { - entity_builder = entity_builder.with(comp::ItemDrop(drop_item)); + if let Some(drop_items) = loot.to_items() { + entity_builder = entity_builder.with(comp::ItemDrops(drop_items)); } // Some would say it's a hack, some would say it's incomplete diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index 76067e4a78..87da49c5d2 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -9,7 +9,7 @@ use common::{ aura::{Aura, AuraKind, AuraTarget}, beam, 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, }, event::{EventBus, NpcBuilder, UpdateCharacterMetadata}, @@ -118,8 +118,8 @@ pub fn handle_create_npc(server: &mut Server, pos: Pos, mut npc: NpcBuilder) -> entity }; - let entity = if let Some(drop_item) = npc.loot.to_item() { - entity.with(ItemDrop(drop_item)) + let entity = if let Some(drop_items) = npc.loot.to_items() { + entity.with(ItemDrops(drop_items)) } else { entity }; diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index d7ecfcabce..dbe6f22955 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -19,13 +19,16 @@ use common::{ self, aura, buff, chat::{KillSource, KillType}, inventory::item::{AbilityMap, MaterialStatManifest}, + item::flatten_counted_items, loot_owner::LootOwnerKind, Alignment, Auras, Body, CharacterState, Energy, Group, Health, HealthChange, Inventory, Player, Poise, Pos, SkillSet, Stats, }, event::{EventBus, ServerEvent}, + lottery::distribute_many, outcome::{HealthChangeInfo, Outcome}, resources::{Secs, Time}, + spiral::Spiral2d, states::utils::StageSection, terrain::{Block, BlockKind, TerrainGrid}, trade::{TradeResult, Trades}, @@ -37,8 +40,7 @@ use common::{ use common_net::{msg::ServerGeneral, sync::WorldSyncExt}; use common_state::BlockChange; use hashbrown::HashSet; -use rand::{distributions::WeightedIndex, Rng}; -use rand_distr::Distribution; +use rand::Rng; use specs::{ join::Join, saveload::MarkerAllocator, Builder, Entity as EcsEntity, Entity, WorldExt, }; @@ -427,72 +429,88 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt // Decide for a loot drop before turning into a lootbag - let item = { - let mut item_drop = state.ecs().write_storage::(); - item_drop.remove(entity).map(|comp::ItemDrop(item)| item) + let items = { + let mut item_drops = state.ecs().write_storage::(); + 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::().get(entity).cloned(); let vel = state.ecs().read_storage::().get(entity).cloned(); if let Some(pos) = pos { // Remove entries where zero exp was awarded - this happens because some // 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 { + if exp >= f32::EPSILON { + let loot_owner = if let Some(group) = group { + Some(LootOwnerKind::Group(group)) + } else { + let uid = state + .ecs() + .read_storage::() + .get(entity) + .and_then(|body| { + // Only humanoids are awarded loot ownership - if the winner + // was a + // non-humanoid NPC the loot will be free-for-all + if matches!(body, Body::Humanoid(_)) { + Some(state.ecs().read_storage::().get(entity).cloned()) + } else { + None + } + }) + .flatten(); - let winner = if exp_awards.is_empty() { - None - } 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); + uid.map(LootOwnerKind::Player) + }; - if let Some(group) = group { - Some(LootOwnerKind::Group(group)) - } else { - let uid = state - .ecs() - .read_storage::() - .get(winner) - .and_then(|body| { - // Only humanoids are awarded loot ownership - if the winner - // was a - // non-humanoid NPC the loot will be free-for-all - if matches!(body, Body::Humanoid(_)) { - Some(state.ecs().read_storage::().get(winner).cloned()) - } else { - None - } - }) - .flatten(); - - uid.map(LootOwnerKind::Player) + *item_receivers.entry(loot_owner).or_insert(0.0) += exp; } - }; + } - let item_drop_entity = state - .create_item_drop(Pos(pos.0 + Vec3::unit_z() * 0.25), item) - .maybe_with(vel) - .build(); + if !item_receivers.is_empty() { + let msm = &MaterialStatManifest::load().read(); + let ability_map = &AbilityMap::load().read(); + let mut item_offset_spiral = Spiral2d::new(); - // If there was a loot winner, assign them as the owner of the loot. There will - // not be a loot winner when an entity dies to environment damage and such so - // the loot will be free-for-all. - if let Some(uid) = winner { - debug!("Assigned UID {:?} as the winner for the loot drop", uid); - - state - .ecs() - .write_storage::() - .insert(item_drop_entity, LootOwner::new(uid)) - .unwrap(); + 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_::() * 0.25) + .unwrap_or_default(); + let item_drop_entity = state + .create_item_drop( + Pos(pos.0 + Vec3::unit_z() * 0.25 + offset), + item, + ) + .maybe_with(vel) + .build(); + if let Some(loot_owner) = loot_owner { + debug!( + "Assigned UID {loot_owner:?} as the winner for the loot \ + drop" + ); + if let Err(err) = state + .ecs() + .write_storage::() + .insert(item_drop_entity, LootOwner::new(loot_owner)) + { + error!("Failed to set loot owner on item drop: {err}"); + }; + } + } + }, + ); } } else { error!( @@ -1080,24 +1098,28 @@ pub fn handle_bonk(server: &mut Server, pos: Vec3, owner: Option, targ { drop(terrain); drop(block_change); - if let Some(item) = comp::Item::try_reclaim_from_block(block) { - server - .state - .create_object(Default::default(), match block.get_sprite() { - // Create different containers depending on the original sprite - Some(SpriteKind::Apple) => comp::object::Body::Apple, - Some(SpriteKind::Beehive) => comp::object::Body::Hive, - Some(SpriteKind::Coconut) => comp::object::Body::Coconut, - Some(SpriteKind::Bomb) => comp::object::Body::Bomb, - _ => comp::object::Body::Pouch, - }) - .with(Pos(pos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0))) - .with(item) - .maybe_with(match block.get_sprite() { - Some(SpriteKind::Bomb) => Some(comp::Object::Bomb { owner }), - _ => None, - }) - .build(); + 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 + .state + .create_object(Default::default(), match block.get_sprite() { + // Create different containers depending on the original sprite + Some(SpriteKind::Apple) => comp::object::Body::Apple, + Some(SpriteKind::Beehive) => comp::object::Body::Hive, + Some(SpriteKind::Coconut) => comp::object::Body::Coconut, + Some(SpriteKind::Bomb) => comp::object::Body::Bomb, + _ => comp::object::Body::Pouch, + }) + .with(Pos(pos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0))) + .with(item) + .maybe_with(match block.get_sprite() { + Some(SpriteKind::Bomb) => Some(comp::Object::Bomb { owner }), + _ => None, + }) + .build(); + } } } } diff --git a/server/src/events/interaction.rs b/server/src/events/interaction.rs index 8a56355aad..0345d72c7e 100644 --- a/server/src/events/interaction.rs +++ b/server/src/events/interaction.rs @@ -8,9 +8,10 @@ use common::{ agent::{AgentEvent, Sound, SoundKind}, dialogue::Subject, inventory::slot::EquipSlot, + item::{flatten_counted_items, MaterialStatManifest}, loot_owner::LootOwnerKind, pet::is_mountable, - tool::ToolKind, + tool::{AbilityMap, ToolKind}, Inventory, LootOwner, Pos, SkillGroupKind, }, 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(); 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 - 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); if let Some(mut skillset) = state @@ -186,12 +190,17 @@ pub fn handle_mine_block( .write_storage::() .get_mut(entity) { - if let (Some(tool), Some(uid), Some(exp_reward)) = ( + if let (Some(tool), Some(uid), exp_reward @ 1..) = ( tool, maybe_uid, - item.item_definition_id() - .itemdef_id() - .and_then(|id| RESOURCE_EXPERIENCE_MANIFEST.read().0.get(id).copied()), + items + .iter() + .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 outcome_bus = state.ecs().read_resource::>(); @@ -230,26 +239,30 @@ pub fn handle_mine_block( 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| { + (id.contains("mineral.ore.") && need_double_ore(&mut rng)) + || (id.contains("mineral.gem.") && need_double_gem(&mut rng)) + }); - 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.gem.") && need_double_gem(&mut rng)) - }); - - if double_gain { - // Ignore non-stackable errors - let _ = item.increase_amount(1); + if double_gain { + // Ignore non-stackable errors + let _ = item.increase_amount(1); + } } } - let item_drop = state - .create_item_drop(Default::default(), item) - .with(Pos(pos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0))); - if let Some(uid) = maybe_uid { - item_drop.with(LootOwner::new(LootOwnerKind::Player(uid))) - } else { - item_drop + for item in items { + let item_drop = state + .create_item_drop(Default::default(), item) + .with(Pos(pos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0))); + if let Some(uid) = maybe_uid { + item_drop.with(LootOwner::new(LootOwnerKind::Player(uid))) + } else { + item_drop + } + .build(); } - .build(); } state.set_block(pos, block.into_vacant()); diff --git a/server/src/events/inventory_manip.rs b/server/src/events/inventory_manip.rs index 51e496e8d0..b61ff44b8c 100644 --- a/server/src/events/inventory_manip.rs +++ b/server/src/events/inventory_manip.rs @@ -8,8 +8,9 @@ use common::{ comp::{ self, group::members, - item::{self, tool::AbilityMap, MaterialStatManifest}, + item::{self, flatten_counted_items, tool::AbilityMap, MaterialStatManifest}, slot::{self, Slot}, + InventoryUpdate, }, consts::MAX_PICKUP_RANGE, recipe::{ @@ -262,7 +263,12 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv let mut block_change = ecs.write_resource::(); 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 block.is_collectible() && block_change.can_set_block(sprite_pos) { @@ -275,45 +281,37 @@ 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 let Some(item) = comp::Item::try_reclaim_from_block(block) { - // NOTE: We dup the item for message purposes. - let item_msg = item.duplicate( - &ecs.read_resource::(), - &ecs.read_resource::(), - ); - let event = match inventory.push(item) { - Ok(_) => { - if let Some(group_id) = ecs.read_storage::().get(entity) { - announce_loot_to_group( - group_id, - ecs, - entity, - item_msg.duplicate( - &ecs.read_resource::(), - &ecs.read_resource::(), - ), - ); - } - comp::InventoryUpdate::new(InventoryUpdateEvent::Collected( - item_msg, - )) - }, - // The item we created was in some sense "fake" so it's safe to - // drop it. - Err(_) => { - drop_item = Some(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."); + // If there are items to be reclaimed from the block, add it to the inventory + 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. + let item_msg = item.duplicate(ability_map, msm); + match inventory.push(item) { + Ok(_) => { + if let Some(group_id) = ecs.read_storage::().get(entity) + { + announce_loot_to_group( + group_id, + ecs, + entity, + item_msg.duplicate( + &ecs.read_resource::(), + &ecs.read_resource::(), + ), + ); + } + inventory_update + .push(InventoryUpdateEvent::Collected(item_msg)); + }, + // The item we created was in some sense "fake" so it's safe to + // drop it. + Err(_) => { + drop_items.push(item_msg); + }, + } + } } // 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(terrain); drop(block_change); - if let Some(item) = drop_item { + drop(inventory_updates); + + for item in drop_items { state .create_item_drop(Default::default(), item) .with(comp::Pos( diff --git a/server/src/sys/entity_sync.rs b/server/src/sys/entity_sync.rs index c6ac05e2d9..0dc785aa0a 100644 --- a/server/src/sys/entity_sync.rs +++ b/server/src/sys/entity_sync.rs @@ -356,10 +356,10 @@ impl<'a> System<'a> for Sys { // TODO: Sync clients that don't have a position? // 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( inventory.clone(), - update.event(), + update.take_events(), )); } diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 70a42b207f..d5560143dc 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -2128,7 +2128,17 @@ impl Hud { .x_y(0.0, 100.0) .position_ingame(over_pos) .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( &item, over_pos, diff --git a/voxygen/src/session/mod.rs b/voxygen/src/session/mod.rs index 7307b41d31..dab0b8e508 100644 --- a/voxygen/src/session/mod.rs +++ b/voxygen/src/session/mod.rs @@ -321,62 +321,67 @@ impl SessionState { 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_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 { - InventoryUpdateEvent::Dropped - | InventoryUpdateEvent::Swapped - | InventoryUpdateEvent::Given - | InventoryUpdateEvent::Collected(_) - | InventoryUpdateEvent::EntityCollectFailed { .. } - | InventoryUpdateEvent::BlockCollectFailed { .. } - | InventoryUpdateEvent::Craft => { - global_state.audio.emit_ui_sfx(sfx_trigger_item, Some(1.0)); - }, - _ => global_state.audio.emit_sfx( - sfx_trigger_item, - client.position().unwrap_or_default(), - Some(1.0), - underwater, - ), - } + match inv_event { + InventoryUpdateEvent::Dropped + | InventoryUpdateEvent::Swapped + | InventoryUpdateEvent::Given + | InventoryUpdateEvent::Collected(_) + | InventoryUpdateEvent::EntityCollectFailed { .. } + | InventoryUpdateEvent::BlockCollectFailed { .. } + | InventoryUpdateEvent::Craft => { + global_state.audio.emit_ui_sfx(sfx_trigger_item, Some(1.0)); + }, + _ => global_state.audio.emit_sfx( + sfx_trigger_item, + client.position().unwrap_or_default(), + Some(1.0), + underwater, + ), + } - match inv_event { - InventoryUpdateEvent::BlockCollectFailed { pos, reason } => { - self.hud.add_failed_block_pickup( - pos, - HudCollectFailedReason::from_server_reason( - &reason, - client.state().ecs(), - ), - ); - }, - InventoryUpdateEvent::EntityCollectFailed { - entity: uid, - reason, - } => { - if let Some(entity) = client.state().ecs().entity_from_uid(uid.into()) { - self.hud.add_failed_entity_pickup( - entity, + match inv_event { + InventoryUpdateEvent::BlockCollectFailed { pos, reason } => { + self.hud.add_failed_block_pickup( + pos, HudCollectFailedReason::from_server_reason( &reason, client.state().ecs(), ), ); - } - }, - InventoryUpdateEvent::Collected(item) => { - self.hud.new_loot_message(LootMessage { - amount: item.amount(), - item, - taken_by: "You".to_string(), - }); - }, - _ => {}, - }; + }, + InventoryUpdateEvent::EntityCollectFailed { + entity: uid, + reason, + } => { + if let Some(entity) = + client.state().ecs().entity_from_uid(uid.into()) + { + self.hud.add_failed_entity_pickup( + entity, + HudCollectFailedReason::from_server_reason( + &reason, + client.state().ecs(), + ), + ); + } + }, + InventoryUpdateEvent::Collected(item) => { + self.hud.new_loot_message(LootMessage { + amount: item.amount(), + item, + taken_by: "You".to_string(), + }); + }, + _ => {}, + }; + } }, client::Event::Disconnect => return Ok(TickAction::Disconnect), client::Event::DisconnectionNotification(time) => {