Add multiloot

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -102,7 +102,7 @@ pub enum Event {
}, },
Disconnect, Disconnect,
DisconnectionNotification(u64), DisconnectionNotification(u64),
InventoryUpdated(InventoryUpdateEvent), InventoryUpdated(Vec<InventoryUpdateEvent>),
Kicked(String), Kicked(String),
Notification(Notification), Notification(Notification),
SetViewDistance(u32), SetViewDistance(u32),
@ -2388,34 +2388,38 @@ impl Client {
self.presence = None; self.presence = None;
self.clean_state(); self.clean_state();
}, },
ServerGeneral::InventoryUpdate(inventory, event) => { ServerGeneral::InventoryUpdate(inventory, events) => {
match event { let mut update_inventory = false;
InventoryUpdateEvent::BlockCollectFailed { .. } => {}, for event in events.iter() {
InventoryUpdateEvent::EntityCollectFailed { .. } => {}, match event {
_ => { InventoryUpdateEvent::BlockCollectFailed { .. } => {},
// Push the updated inventory component to the client InventoryUpdateEvent::EntityCollectFailed { .. } => {},
// FIXME: Figure out whether this error can happen under normal gameplay, _ => update_inventory = true,
// if not find a better way to handle it, if so maybe consider kicking the }
// client back to login? }
let entity = self.entity(); if update_inventory {
if let Err(e) = self // Push the updated inventory component to the client
.state // FIXME: Figure out whether this error can happen under normal gameplay,
.ecs_mut() // if not find a better way to handle it, if so maybe consider kicking the
.write_storage() // client back to login?
.insert(entity, inventory) let entity = self.entity();
{ if let Err(e) = self
warn!( .state
?e, .ecs_mut()
"Received an inventory update event for client entity, but this \ .write_storage()
entity was not found... this may be a bug." .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(); self.update_available_recipes();
frontend_events.push(Event::InventoryUpdated(event)); frontend_events.push(Event::InventoryUpdated(events));
}, },
ServerGeneral::SetViewDistance(vd) => { ServerGeneral::SetViewDistance(vd) => {
self.view_distance = Some(vd); self.view_distance = Some(vd);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -362,74 +362,84 @@ impl From<Vec<(f32, LootSpec<String>)>> for ProbabilityFile {
} else { } else {
1.0 / content.iter().map(|e| e.0).sum::<f32>() 1.0 / content.iter().map(|e| e.0).sum::<f32>()
}; };
fn get_content(
rescale: f32,
p0: f32,
loot: LootSpec<String>,
) -> 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::<Vec<_>>()
},
LootSpec::ModularWeapon {
tool,
material,
hands,
} => {
let mut primary = expand_primary_component(tool, material, hands);
let secondary: Vec<ItemDefinitionIdOwned> =
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 { Self {
content: content content: content
.into_iter() .into_iter()
.flat_map(|(p0, loot)| match loot { .flat_map(|(p0, loot)| get_content(rescale, p0, 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::<Vec<_>>()
},
LootSpec::ModularWeapon {
tool,
material,
hands,
} => {
let mut primary = expand_primary_component(tool, material, hands);
let secondary: Vec<ItemDefinitionIdOwned> =
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(),
})
.collect(), .collect(),
} }
} }
@ -1231,12 +1241,4 @@ mod tests {
let probability: ProbabilityFile = loot_table.into(); let probability: ProbabilityFile = loot_table.into();
assert!(normalized(&probability)); assert!(normalized(&probability));
} }
#[test]
fn test_normalizing_table4() {
let quantity = |asset: &str, a, b| LootSpec::ItemQuantity(asset.to_owned(), a, b);
let loot_table = vec![(1.0, quantity("such", 3, 5)), (1.0, quantity("much", 5, 9))];
let probability: ProbabilityFile = loot_table.into();
assert!(normalized(&probability));
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,13 +19,16 @@ use common::{
self, aura, buff, self, aura, buff,
chat::{KillSource, KillType}, chat::{KillSource, KillType},
inventory::item::{AbilityMap, MaterialStatManifest}, inventory::item::{AbilityMap, MaterialStatManifest},
item::flatten_counted_items,
loot_owner::LootOwnerKind, loot_owner::LootOwnerKind,
Alignment, Auras, Body, CharacterState, Energy, Group, Health, HealthChange, Inventory, Alignment, Auras, Body, CharacterState, Energy, Group, Health, HealthChange, Inventory,
Player, Poise, Pos, SkillSet, Stats, Player, Poise, Pos, SkillSet, Stats,
}, },
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
lottery::distribute_many,
outcome::{HealthChangeInfo, Outcome}, outcome::{HealthChangeInfo, Outcome},
resources::{Secs, Time}, resources::{Secs, Time},
spiral::Spiral2d,
states::utils::StageSection, states::utils::StageSection,
terrain::{Block, BlockKind, TerrainGrid}, terrain::{Block, BlockKind, TerrainGrid},
trade::{TradeResult, Trades}, trade::{TradeResult, Trades},
@ -37,8 +40,7 @@ use common::{
use common_net::{msg::ServerGeneral, sync::WorldSyncExt}; use common_net::{msg::ServerGeneral, sync::WorldSyncExt};
use common_state::BlockChange; use common_state::BlockChange;
use hashbrown::HashSet; use hashbrown::HashSet;
use rand::{distributions::WeightedIndex, Rng}; use rand::Rng;
use rand_distr::Distribution;
use specs::{ use specs::{
join::Join, saveload::MarkerAllocator, Builder, Entity as EcsEntity, Entity, WorldExt, join::Join, saveload::MarkerAllocator, Builder, Entity as EcsEntity, Entity, WorldExt,
}; };
@ -427,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 // Decide for a loot drop before turning into a lootbag
let item = { let items = {
let mut item_drop = state.ecs().write_storage::<comp::ItemDrop>(); let mut item_drops = state.ecs().write_storage::<comp::ItemDrops>();
item_drop.remove(entity).map(|comp::ItemDrop(item)| item) item_drops.remove(entity).map(|comp::ItemDrops(item)| item)
}; };
if let Some(item) = item { if let Some(items) = items {
let pos = state.ecs().read_storage::<Pos>().get(entity).cloned(); let pos = state.ecs().read_storage::<Pos>().get(entity).cloned();
let vel = state.ecs().read_storage::<comp::Vel>().get(entity).cloned(); let vel = state.ecs().read_storage::<comp::Vel>().get(entity).cloned();
if let Some(pos) = pos { if let Some(pos) = pos {
// Remove entries where zero exp was awarded - this happens because some // Remove entries where zero exp was awarded - this happens because some
// entities like Object bodies don't give EXP. // entities like Object bodies don't give EXP.
let _ = exp_awards.drain_filter(|(_, exp, _)| *exp < f32::EPSILON); let mut item_receivers = HashMap::new();
for (entity, exp, group) in exp_awards {
if exp >= f32::EPSILON {
let loot_owner = if let Some(group) = group {
Some(LootOwnerKind::Group(group))
} else {
let uid = state
.ecs()
.read_storage::<Body>()
.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::<Uid>().get(entity).cloned())
} else {
None
}
})
.flatten();
let winner = if exp_awards.is_empty() { uid.map(LootOwnerKind::Player)
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);
if let Some(group) = group { *item_receivers.entry(loot_owner).or_insert(0.0) += exp;
Some(LootOwnerKind::Group(group))
} else {
let uid = state
.ecs()
.read_storage::<Body>()
.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::<Uid>().get(winner).cloned())
} else {
None
}
})
.flatten();
uid.map(LootOwnerKind::Player)
} }
}; }
let item_drop_entity = state if !item_receivers.is_empty() {
.create_item_drop(Pos(pos.0 + Vec3::unit_z() * 0.25), item) let msm = &MaterialStatManifest::load().read();
.maybe_with(vel) let ability_map = &AbilityMap::load().read();
.build(); let mut item_offset_spiral = Spiral2d::new();
// If there was a loot winner, assign them as the owner of the loot. There will let mut rng = rand::thread_rng();
// not be a loot winner when an entity dies to environment damage and such so distribute_many(
// the loot will be free-for-all. item_receivers
if let Some(uid) = winner { .iter()
debug!("Assigned UID {:?} as the winner for the loot drop", uid); .map(|(loot_owner, weight)| (*weight, *loot_owner)),
&mut rng,
state &items,
.ecs() |(amount, _)| *amount,
.write_storage::<LootOwner>() |(_, item), loot_owner, count| {
.insert(item_drop_entity, LootOwner::new(uid)) for item in item.stacked_duplicates(ability_map, msm, count) {
.unwrap(); let offset = item_offset_spiral
.next()
.map(|offset| offset.as_::<f32>() * 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::<LootOwner>()
.insert(item_drop_entity, LootOwner::new(loot_owner))
{
error!("Failed to set loot owner on item drop: {err}");
};
}
}
},
);
} }
} else { } else {
error!( error!(
@ -1080,24 +1098,28 @@ pub fn handle_bonk(server: &mut Server, pos: Vec3<f32>, owner: Option<Uid>, targ
{ {
drop(terrain); drop(terrain);
drop(block_change); drop(block_change);
if let Some(item) = comp::Item::try_reclaim_from_block(block) { if let Some(items) = comp::Item::try_reclaim_from_block(block) {
server let msm = &MaterialStatManifest::load().read();
.state let ability_map = &AbilityMap::load().read();
.create_object(Default::default(), match block.get_sprite() { for item in flatten_counted_items(&items, ability_map, msm) {
// Create different containers depending on the original sprite server
Some(SpriteKind::Apple) => comp::object::Body::Apple, .state
Some(SpriteKind::Beehive) => comp::object::Body::Hive, .create_object(Default::default(), match block.get_sprite() {
Some(SpriteKind::Coconut) => comp::object::Body::Coconut, // Create different containers depending on the original sprite
Some(SpriteKind::Bomb) => comp::object::Body::Bomb, Some(SpriteKind::Apple) => comp::object::Body::Apple,
_ => comp::object::Body::Pouch, Some(SpriteKind::Beehive) => comp::object::Body::Hive,
}) Some(SpriteKind::Coconut) => comp::object::Body::Coconut,
.with(Pos(pos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0))) Some(SpriteKind::Bomb) => comp::object::Body::Bomb,
.with(item) _ => comp::object::Body::Pouch,
.maybe_with(match block.get_sprite() { })
Some(SpriteKind::Bomb) => Some(comp::Object::Bomb { owner }), .with(Pos(pos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0)))
_ => None, .with(item)
}) .maybe_with(match block.get_sprite() {
.build(); Some(SpriteKind::Bomb) => Some(comp::Object::Bomb { owner }),
_ => None,
})
.build();
}
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -321,62 +321,67 @@ impl SessionState {
TradeResult::NotEnoughSpace => "hud-trade-result-nospace", TradeResult::NotEnoughSpace => "hud-trade-result-nospace",
}))); })));
}, },
client::Event::InventoryUpdated(inv_event) => { client::Event::InventoryUpdated(inv_events) => {
let sfx_triggers = self.scene.sfx_mgr.triggers.read(); let sfx_triggers = self.scene.sfx_mgr.triggers.read();
let sfx_trigger_item = sfx_triggers.get_key_value(&SfxEvent::from(&inv_event)); for inv_event in inv_events {
let sfx_trigger_item =
sfx_triggers.get_key_value(&SfxEvent::from(&inv_event));
match inv_event { match inv_event {
InventoryUpdateEvent::Dropped InventoryUpdateEvent::Dropped
| InventoryUpdateEvent::Swapped | InventoryUpdateEvent::Swapped
| InventoryUpdateEvent::Given | InventoryUpdateEvent::Given
| InventoryUpdateEvent::Collected(_) | InventoryUpdateEvent::Collected(_)
| InventoryUpdateEvent::EntityCollectFailed { .. } | InventoryUpdateEvent::EntityCollectFailed { .. }
| InventoryUpdateEvent::BlockCollectFailed { .. } | InventoryUpdateEvent::BlockCollectFailed { .. }
| InventoryUpdateEvent::Craft => { | InventoryUpdateEvent::Craft => {
global_state.audio.emit_ui_sfx(sfx_trigger_item, Some(1.0)); global_state.audio.emit_ui_sfx(sfx_trigger_item, Some(1.0));
}, },
_ => global_state.audio.emit_sfx( _ => global_state.audio.emit_sfx(
sfx_trigger_item, sfx_trigger_item,
client.position().unwrap_or_default(), client.position().unwrap_or_default(),
Some(1.0), Some(1.0),
underwater, underwater,
), ),
} }
match inv_event { match inv_event {
InventoryUpdateEvent::BlockCollectFailed { pos, reason } => { InventoryUpdateEvent::BlockCollectFailed { pos, reason } => {
self.hud.add_failed_block_pickup( self.hud.add_failed_block_pickup(
pos, 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,
HudCollectFailedReason::from_server_reason( HudCollectFailedReason::from_server_reason(
&reason, &reason,
client.state().ecs(), client.state().ecs(),
), ),
); );
} },
}, InventoryUpdateEvent::EntityCollectFailed {
InventoryUpdateEvent::Collected(item) => { entity: uid,
self.hud.new_loot_message(LootMessage { reason,
amount: item.amount(), } => {
item, if let Some(entity) =
taken_by: "You".to_string(), 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::Disconnect => return Ok(TickAction::Disconnect),
client::Event::DisconnectionNotification(time) => { client::Event::DisconnectionNotification(time) => {