Merge branch 'floppy_multiloot' into 'master'

Add multiloot

See merge request veloren/veloren!3839
This commit is contained in:
Isse 2023-04-23 19:17:40 +00:00
commit 4d79c89a0c
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 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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -102,7 +102,7 @@ pub enum Event {
},
Disconnect,
DisconnectionNotification(u64),
InventoryUpdated(InventoryUpdateEvent),
InventoryUpdated(Vec<InventoryUpdateEvent>),
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);

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"] }
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"]

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
/// from an ingame state
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
/// 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

View File

@ -254,48 +254,53 @@ fn loot_table(loot_table: &str) -> Result<(), Box<dyn Error>> {
.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<W: std::io::Write>(
wtr: &mut csv::Writer<W>,
loot_spec: &LootSpec<String>,
chance: &str,
) -> Result<(), Box<dyn Error>> {
let get_hands = |hands| match hands {
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<dyn Error>> {
"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<dyn Error>> {
}
},
LootSpec::LootTable(_) => unreachable!(),
LootSpec::MultiDrop(_, _, _) => todo!(),
}
}

View File

@ -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<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
/// 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<Self> {
block.get_sprite()?.collectible_id()??.to_item()
pub fn try_reclaim_from_block(block: Block) -> Option<Vec<(u32, Self)>> {
block.get_sprite()?.collectible_id()??.to_items()
}
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
/// 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<Self>;
}

View File

@ -995,13 +995,19 @@ impl Default for InventoryUpdateEvent {
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct InventoryUpdate {
event: InventoryUpdateEvent,
events: Vec<InventoryUpdateEvent>,
}
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 {

View File

@ -362,74 +362,84 @@ impl From<Vec<(f32, LootSpec<String>)>> for ProbabilityFile {
} else {
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 {
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::<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(),
})
.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));
}
}

View File

@ -74,7 +74,7 @@ impl Component for LootOwner {
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 {
Player(Uid),
Group(Group),

View File

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

View File

@ -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<T> Lottery<T> {
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)]
pub enum LootSpec<T: AsRef<str>> {
/// 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<T: AsRef<str>> {
material: item::Material,
hands: Option<item::tool::Hands>,
},
/// LootSpec, lower range, upper range
MultiDrop(Box<LootSpec<T>>, u32, u32),
}
impl<T: AsRef<str>> LootSpec<T> {
pub fn to_item(&self) -> Option<Item> {
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::<LootSpec<String>>::load_expect(table.as_ref())
.read()
.choose()
.to_item(),
Self::Nothing => None,
Self::LootTable(table) => {
let loot_spec = Lottery::<LootSpec<String>>::load_expect(table.as_ref()).read();
for _ in 0..amount {
loot_spec.choose().to_items_inner(rng, 1, items)
}
},
Self::Nothing => {},
Self::ModularWeapon {
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<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) => {
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::<LootSpec<String>>::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!(),
},
);
}
}
}

View File

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

View File

@ -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::<comp::InventoryUpdate>();
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

View File

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

View File

@ -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::<comp::ItemDrop>();
item_drop.remove(entity).map(|comp::ItemDrop(item)| item)
let items = {
let mut item_drops = state.ecs().write_storage::<comp::ItemDrops>();
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 vel = state.ecs().read_storage::<comp::Vel>().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::<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() {
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::<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)
*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::<LootOwner>()
.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_::<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 {
error!(
@ -1080,24 +1098,28 @@ pub fn handle_bonk(server: &mut Server, pos: Vec3<f32>, owner: Option<Uid>, 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();
}
}
}
}

View File

@ -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::<comp::SkillSet>()
.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::<EventBus<Outcome>>();
@ -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());

View File

@ -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::<common_state::BlockChange>();
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::<AbilityMap>(),
&ecs.read_resource::<MaterialStatManifest>(),
);
let event = match inventory.push(item) {
Ok(_) => {
if let Some(group_id) = ecs.read_storage::<Group>().get(entity) {
announce_loot_to_group(
group_id,
ecs,
entity,
item_msg.duplicate(
&ecs.read_resource::<AbilityMap>(),
&ecs.read_resource::<MaterialStatManifest>(),
),
);
}
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::<Group>().get(entity)
{
announce_loot_to_group(
group_id,
ecs,
entity,
item_msg.duplicate(
&ecs.read_resource::<AbilityMap>(),
&ecs.read_resource::<MaterialStatManifest>(),
),
);
}
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(

View File

@ -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(),
));
}

View File

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

View File

@ -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) => {