mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'sam/durability' into 'master'
Durability See merge request veloren/veloren!3509
This commit is contained in:
commit
162509e1c9
@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Custom spots can be added without recompilation (only ron and vox files)
|
||||
- Setting in userdata/server/server_config/settings.ron that controls the length of each day/night cycle.
|
||||
- Starting site can now be chosen during character creation
|
||||
- Durability loss of equipped items on death
|
||||
|
||||
### Changed
|
||||
- Bats move slower and use a simple proportional controller to maintain altitude
|
||||
|
@ -25,7 +25,7 @@
|
||||
craft_sprite: Some(CraftingBench),
|
||||
),
|
||||
"velorite_frag": (
|
||||
output: ("common.items.mineral.ore.veloritefrag", 2),
|
||||
output: ("common.items.mineral.ore.veloritefrag", 3),
|
||||
inputs: [
|
||||
(Item("common.items.mineral.ore.velorite"), 1, false),
|
||||
(Item("common.items.tool.craftsman_hammer"), 0, false),
|
||||
|
1074
assets/common/repair_recipe_book.ron
Normal file
1074
assets/common/repair_recipe_book.ron
Normal file
File diff suppressed because it is too large
Load Diff
@ -11,10 +11,10 @@
|
||||
(0.08, Item("common.items.mineral.gem.ruby")),
|
||||
(0.1, Item("common.items.mineral.gem.emerald")),
|
||||
(0.2, Item("common.items.mineral.gem.sapphire")),
|
||||
(0.25, Item("common.items.mineral.ore.velorite")),
|
||||
(0.5, Item("common.items.mineral.ore.veloritefrag")),
|
||||
(0.5, Item("common.items.mineral.ore.velorite")),
|
||||
(0.75, Item("common.items.mineral.gem.topaz")),
|
||||
(1.0, Item("common.items.mineral.gem.amethyst")),
|
||||
(2.0, Item("common.items.mineral.ore.veloritefrag")),
|
||||
(4.0, Item("common.items.crafting_ing.stones")),
|
||||
|
||||
// Ores
|
||||
|
@ -103,6 +103,7 @@ common-stats-energy_reward = Energy Reward
|
||||
common-stats-crit_power = Crit Power
|
||||
common-stats-stealth = Stealth
|
||||
common-stats-slots = Slots
|
||||
common-stats-durability = Durability
|
||||
common-material-metal = Metal
|
||||
common-material-wood = Wood
|
||||
common-material-stone = Stone
|
||||
|
@ -3,6 +3,9 @@ hud-crafting-recipes = Recipes
|
||||
hud-crafting-ingredients = Ingredients:
|
||||
hud-crafting-craft = Craft
|
||||
hud-crafting-craft_all = Craft All
|
||||
hud-crafting-repair = Repair
|
||||
hud-crafting-repair_equipped = Repair Equipped
|
||||
hud-crafting-repair_all = Repair All
|
||||
hud-crafting-tool_cata = Requires:
|
||||
hud-crafting-req_crafting_station = Requires:
|
||||
hud-crafting-anvil = Anvil
|
||||
@ -14,6 +17,7 @@ hud-crafting-loom = Loom
|
||||
hud-crafting-spinning_wheel = Spinning Wheel
|
||||
hud-crafting-tanning_rack = Tanning Rack
|
||||
hud-crafting-salvaging_station = Salvaging Bench
|
||||
hud-crafting-repair_bench = Repair Bench
|
||||
hud-crafting-campfire = Campfire
|
||||
hud-crafting-tabs-all = All
|
||||
hud-crafting-tabs-armor = Armor
|
||||
@ -30,7 +34,7 @@ hud-crafting-dismantle_title = Dismantling
|
||||
hud-crafting-dismantle_explanation =
|
||||
Hover items in your bag to see what
|
||||
you can salvage.
|
||||
|
||||
|
||||
Double-Click them to start dismantling.
|
||||
hud-crafting-modular_desc = Drag Item-Parts here to craft a weapon
|
||||
hud-crafting-mod_weap_prim_slot_title = Primary Weapon Component
|
||||
@ -42,4 +46,6 @@ hud-crafting-mod_comp_metal_prim_slot_desc = Place a metal ingot here, only cert
|
||||
hud-crafting-mod_comp_wood_prim_slot_title = Wood
|
||||
hud-crafting-mod_comp_wood_prim_slot_desc = Place a kind of wood here, only certain woods can be used to make weapons.
|
||||
hud-crafting-mod_comp_sec_slot_title = Animal Material
|
||||
hud-crafting-mod_comp_sec_slot_desc = Optionally place an animal crafting ingredient, only certain ingredients can be used to augment weapons.
|
||||
hud-crafting-mod_comp_sec_slot_desc = Optionally place an animal crafting ingredient, only certain ingredients can be used to augment weapons.
|
||||
hud-crafting-repair_slot_title = Damaged Item
|
||||
hud-crafting-repair_slot_desc = Place an item here to see the cost of repairing it at its current durability level.
|
@ -13,6 +13,7 @@ hud-press_key_to_toggle_lantern_fmt = [{ $key }] Lantern
|
||||
hud-press_key_to_show_debug_info_fmt = Press { $key } to show debug info
|
||||
hud-press_key_to_toggle_keybindings_fmt = Press { $key } to toggle keybindings
|
||||
hud-press_key_to_toggle_debug_info_fmt = Press { $key } to toggle debug info
|
||||
hud_items_lost_dur = Your equipped items have lost Durability.
|
||||
hud-press_key_to_respawn = Press { $key } to respawn at the last campfire you visited.
|
||||
hud-tutorial_btn = Tutorial
|
||||
hud-tutorial_click_here = Press [ { $key } ] to free your cursor and click this button!
|
||||
|
@ -39,6 +39,10 @@
|
||||
"voxel.sprite.salvaging_station.salvaging_station-0",
|
||||
(0.0, 0.0, 0.0), (-50.0, 40.0, 30.0), 0.9,
|
||||
),
|
||||
Simple("RepairBench"): VoxTrans(
|
||||
"voxel.sprite.repair_bench.repair_bench-0",
|
||||
(0.0, 0.0, 0.0), (-50.0, 40.0, 30.0), 0.9,
|
||||
),
|
||||
// Weapons
|
||||
// Diary Example Images
|
||||
Simple("example_utility"): VoxTrans(
|
||||
@ -3155,7 +3159,7 @@
|
||||
(0.0, -1.0, 0.0), (-50.0, 40.0, 20.0), 0.8,
|
||||
),
|
||||
Simple("common.items.mineral.ore.veloritefrag"): VoxTrans(
|
||||
"voxel.sprite.velorite.velorite_1",
|
||||
"voxel.sprite.velorite.velorite",
|
||||
(0.0, 0.0, 0.0), (-50.0, 40.0, 20.0), 0.8,
|
||||
),
|
||||
Simple("common.items.food.apple_mushroom_curry"): VoxTrans(
|
||||
|
@ -447,7 +447,7 @@
|
||||
color: None
|
||||
),
|
||||
(Danari, Male, "common.items.armor.misc.head.straw"): (
|
||||
vox_spec: ("armor.misc.head.straw", (-2.0, -5.0, 7.0)),
|
||||
vox_spec: ("armor.misc.head.straw", (-2.0, -4.0, 7.0)),
|
||||
color: None
|
||||
),
|
||||
(Danari, Female, "common.items.armor.misc.head.straw"): (
|
||||
@ -463,7 +463,7 @@
|
||||
color: None
|
||||
),
|
||||
(Orc, Male, "common.items.armor.misc.head.straw"): (
|
||||
vox_spec: ("armor.misc.head.straw", (-3.0, -3.0, 8.0)),
|
||||
vox_spec: ("armor.misc.head.straw", (-3.0, -4.0, 7.0)),
|
||||
color: None
|
||||
),
|
||||
(Orc, Female, "common.items.armor.misc.head.straw"): (
|
||||
|
@ -800,7 +800,7 @@
|
||||
Simple("common.items.food.blue_cheese"): "voxel.object.blue_cheese",
|
||||
Simple("common.items.food.mushroom"): "voxel.sprite.mushrooms.mushroom-10",
|
||||
Simple("common.items.mineral.ore.velorite"): "voxel.sprite.velorite.velorite_ore",
|
||||
Simple("common.items.mineral.ore.veloritefrag"): "voxel.sprite.velorite.velorite_1",
|
||||
Simple("common.items.mineral.ore.veloritefrag"): "voxel.sprite.velorite.velorite",
|
||||
Simple("common.items.food.apple_mushroom_curry"): "voxel.object.mushroom_curry",
|
||||
Simple("common.items.food.spore_corruption"): "voxel.sprite.spore.corruption_spore",
|
||||
Simple("common.items.food.apple_stick"): "voxel.object.apple_stick",
|
||||
|
BIN
assets/voxygen/voxel/sprite/mineral/deposit/bloodstone.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/deposit/bloodstone.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/deposit/coal.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/deposit/coal.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/deposit/cobalt.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/deposit/cobalt.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/deposit/copper.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/deposit/copper.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/deposit/gold.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/deposit/gold.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/deposit/iron.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/deposit/iron.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/deposit/silver.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/deposit/silver.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/deposit/tin.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/deposit/tin.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/gem/amethyst-0.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/gem/amethyst-0.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/gem/amethyst_S-0.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/gem/amethyst_S-0.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/gem/diamond-0.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/gem/diamond-0.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/gem/diamond_S-0.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/gem/diamond_S-0.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/gem/emerald-0.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/gem/emerald-0.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/gem/emerald_S-0.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/gem/emerald_S-0.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/gem/ruby-0.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/gem/ruby-0.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/gem/ruby_S-0.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/gem/ruby_S-0.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/gem/sapphire-0.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/gem/sapphire-0.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/gem/sapphire_S-0.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/gem/sapphire_S-0.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/gem/topaz-0.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/gem/topaz-0.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/gem/topaz_S-0.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/gem/topaz_S-0.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/ore/bloodstone.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/ore/bloodstone.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/ore/coal.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/ore/coal.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/ore/cobalt.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/ore/cobalt.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/ore/copper.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/ore/copper.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/ore/gold.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/ore/gold.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/ore/iron.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/ore/iron.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/ore/silver.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/ore/silver.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/mineral/ore/tin.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/mineral/ore/tin.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/repair_bench/repair_bench-0.vox
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/voxel/sprite/repair_bench/repair_bench-0.vox
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/velorite/velorite.vox
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/voxel/sprite/velorite/velorite.vox
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/velorite/velorite_1.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/velorite/velorite_1.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/velorite/velorite_10.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/velorite/velorite_10.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/velorite/velorite_2.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/velorite/velorite_2.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/velorite/velorite_3.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/velorite/velorite_3.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/velorite/velorite_4.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/velorite/velorite_4.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/velorite/velorite_5.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/velorite/velorite_5.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/velorite/velorite_6.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/velorite/velorite_6.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/velorite/velorite_7.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/velorite/velorite_7.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/velorite/velorite_8.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/velorite/velorite_8.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/velorite/velorite_9.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/velorite/velorite_9.vox
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/voxel/sprite/velorite/velorite_ore.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/sprite/velorite/velorite_ore.vox
(Stored with Git LFS)
Binary file not shown.
@ -716,7 +716,7 @@ Velorite: Some((
|
||||
variations: [
|
||||
(
|
||||
model: "voxygen.voxel.sprite.velorite.velorite_ore",
|
||||
offset: (-5.0, -5.0, -5.0),
|
||||
offset: (-5.0, -5.0, 0.0),
|
||||
lod_axes: (1.0, 1.0, 1.0),
|
||||
),
|
||||
],
|
||||
@ -725,53 +725,8 @@ Velorite: Some((
|
||||
VeloriteFrag: Some((
|
||||
variations: [
|
||||
(
|
||||
model: "voxygen.voxel.sprite.velorite.velorite_1",
|
||||
offset: (-3.0, -5.0, 0.0),
|
||||
lod_axes: (1.0, 1.0, 1.0),
|
||||
),
|
||||
(
|
||||
model: "voxygen.voxel.sprite.velorite.velorite_2",
|
||||
offset: (-3.0, -5.0, 0.0),
|
||||
lod_axes: (1.0, 1.0, 1.0),
|
||||
),
|
||||
(
|
||||
model: "voxygen.voxel.sprite.velorite.velorite_3",
|
||||
offset: (-3.0, -5.0, 0.0),
|
||||
lod_axes: (1.0, 1.0, 1.0),
|
||||
),
|
||||
(
|
||||
model: "voxygen.voxel.sprite.velorite.velorite_4",
|
||||
offset: (-3.0, -5.0, 0.0),
|
||||
lod_axes: (1.0, 1.0, 1.0),
|
||||
),
|
||||
(
|
||||
model: "voxygen.voxel.sprite.velorite.velorite_5",
|
||||
offset: (-3.0, -5.0, 0.0),
|
||||
lod_axes: (1.0, 1.0, 1.0),
|
||||
),
|
||||
(
|
||||
model: "voxygen.voxel.sprite.velorite.velorite_6",
|
||||
offset: (-3.0, -5.0, 0.0),
|
||||
lod_axes: (1.0, 1.0, 1.0),
|
||||
),
|
||||
(
|
||||
model: "voxygen.voxel.sprite.velorite.velorite_7",
|
||||
offset: (-3.0, -5.0, 0.0),
|
||||
lod_axes: (1.0, 1.0, 1.0),
|
||||
),
|
||||
(
|
||||
model: "voxygen.voxel.sprite.velorite.velorite_8",
|
||||
offset: (-3.0, -5.0, 0.0),
|
||||
lod_axes: (1.0, 1.0, 1.0),
|
||||
),
|
||||
(
|
||||
model: "voxygen.voxel.sprite.velorite.velorite_9",
|
||||
offset: (-3.0, -5.0, 0.0),
|
||||
lod_axes: (1.0, 1.0, 1.0),
|
||||
),
|
||||
(
|
||||
model: "voxygen.voxel.sprite.velorite.velorite_10",
|
||||
offset: (-3.0, -5.0, 0.0),
|
||||
model: "voxygen.voxel.sprite.velorite.velorite",
|
||||
offset: (-4.0, -5.0, 0.0),
|
||||
lod_axes: (1.0, 1.0, 1.0),
|
||||
),
|
||||
],
|
||||
@ -784,7 +739,7 @@ Chest: Some((
|
||||
model: "voxygen.voxel.sprite.chests.chest",
|
||||
offset: (-7.0, -5.0, -0.0),
|
||||
lod_axes: (1.0, 1.0, 1.0),
|
||||
),
|
||||
),
|
||||
(
|
||||
model: "voxygen.voxel.sprite.chests.chest_dark",
|
||||
offset: (-7.0, -5.0, -0.0),
|
||||
@ -799,13 +754,13 @@ Chest: Some((
|
||||
wind_sway: 0.0,
|
||||
)),
|
||||
CommonLockedChest: Some((
|
||||
variations: [
|
||||
variations: [
|
||||
(
|
||||
model: "voxygen.voxel.sprite.chests.chest_gold",
|
||||
offset: (-7.0, -5.0, -0.0),
|
||||
lod_axes: (1.0, 1.0, 1.0),
|
||||
),
|
||||
|
||||
|
||||
],
|
||||
wind_sway: 0.0,
|
||||
)),
|
||||
@ -3788,6 +3743,17 @@ CookingPot: Some((
|
||||
],
|
||||
wind_sway: 0.0,
|
||||
)),
|
||||
// Repair Bench
|
||||
RepairBench: Some((
|
||||
variations: [
|
||||
(
|
||||
model: "voxygen.voxel.sprite.repair_bench.repair_bench-0",
|
||||
offset: (-7.0, -8.0, 0.0),
|
||||
lod_axes: (0.0, 0.0, 0.0),
|
||||
),
|
||||
],
|
||||
wind_sway: 0.0,
|
||||
)),
|
||||
// Ensnaring Vines
|
||||
EnsnaringVines: Some((
|
||||
variations: [
|
||||
|
@ -38,7 +38,7 @@ use common::{
|
||||
lod,
|
||||
mounting::Rider,
|
||||
outcome::Outcome,
|
||||
recipe::{ComponentRecipeBook, RecipeBook},
|
||||
recipe::{ComponentRecipeBook, RecipeBook, RepairRecipeBook},
|
||||
resources::{GameMode, PlayerEntity, Time, TimeOfDay},
|
||||
shared_server_config::ServerConstants,
|
||||
spiral::Spiral2d,
|
||||
@ -222,6 +222,7 @@ pub struct Client {
|
||||
pub chat_mode: ChatMode,
|
||||
recipe_book: RecipeBook,
|
||||
component_recipe_book: ComponentRecipeBook,
|
||||
repair_recipe_book: RepairRecipeBook,
|
||||
available_recipes: HashMap<String, Option<SpriteKind>>,
|
||||
lod_zones: HashMap<Vec2<i32>, lod::Zone>,
|
||||
lod_last_requested: Option<Instant>,
|
||||
@ -362,6 +363,7 @@ impl Client {
|
||||
material_stats,
|
||||
ability_map,
|
||||
server_constants,
|
||||
repair_recipe_book,
|
||||
} = loop {
|
||||
tokio::select! {
|
||||
// Spawn in a blocking thread (leaving the network thread free). This is mostly
|
||||
@ -655,6 +657,7 @@ impl Client {
|
||||
world_map.pois,
|
||||
recipe_book,
|
||||
component_recipe_book,
|
||||
repair_recipe_book,
|
||||
max_group_size,
|
||||
client_timeout,
|
||||
))
|
||||
@ -670,6 +673,7 @@ impl Client {
|
||||
pois,
|
||||
recipe_book,
|
||||
component_recipe_book,
|
||||
repair_recipe_book,
|
||||
max_group_size,
|
||||
client_timeout,
|
||||
) = loop {
|
||||
@ -708,6 +712,7 @@ impl Client {
|
||||
pois,
|
||||
recipe_book,
|
||||
component_recipe_book,
|
||||
repair_recipe_book,
|
||||
available_recipes: HashMap::default(),
|
||||
chat_mode: ChatMode::default(),
|
||||
|
||||
@ -1146,6 +1151,8 @@ impl Client {
|
||||
|
||||
pub fn component_recipe_book(&self) -> &ComponentRecipeBook { &self.component_recipe_book }
|
||||
|
||||
pub fn repair_recipe_book(&self) -> &RepairRecipeBook { &self.repair_recipe_book }
|
||||
|
||||
pub fn available_recipes(&self) -> &HashMap<String, Option<SpriteKind>> {
|
||||
&self.available_recipes
|
||||
}
|
||||
@ -1289,6 +1296,39 @@ impl Client {
|
||||
)));
|
||||
}
|
||||
|
||||
/// Repairs the item in the given inventory slot. `sprite_pos` should be
|
||||
/// the location of a relevant crafting station within range of the player.
|
||||
pub fn repair_item(
|
||||
&mut self,
|
||||
item: Slot,
|
||||
slots: Vec<(u32, InvSlotId)>,
|
||||
sprite_pos: Vec3<i32>,
|
||||
) -> bool {
|
||||
let is_repairable = {
|
||||
let inventories = self.inventories();
|
||||
let inventory = inventories.get(self.entity());
|
||||
inventory.map_or(false, |inv| {
|
||||
if let Some(item) = match item {
|
||||
Slot::Equip(equip_slot) => inv.equipped(equip_slot),
|
||||
Slot::Inventory(invslot) => inv.get(invslot),
|
||||
} {
|
||||
item.has_durability()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
};
|
||||
if is_repairable {
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
|
||||
InventoryEvent::CraftRecipe {
|
||||
craft_event: CraftEvent::Repair { item, slots },
|
||||
craft_sprite: Some(sprite_pos),
|
||||
},
|
||||
)));
|
||||
}
|
||||
is_repairable
|
||||
}
|
||||
|
||||
fn update_available_recipes(&mut self) {
|
||||
self.available_recipes = self
|
||||
.recipe_book
|
||||
|
@ -10,7 +10,7 @@ use common::{
|
||||
event::UpdateCharacterMetadata,
|
||||
lod,
|
||||
outcome::Outcome,
|
||||
recipe::{ComponentRecipeBook, RecipeBook},
|
||||
recipe::{ComponentRecipeBook, RecipeBook, RepairRecipeBook},
|
||||
resources::{Time, TimeOfDay},
|
||||
shared_server_config::ServerConstants,
|
||||
terrain::{Block, TerrainChunk, TerrainChunkMeta, TerrainChunkSize},
|
||||
@ -65,6 +65,7 @@ pub enum ServerInit {
|
||||
world_map: crate::msg::world_msg::WorldMapMsg,
|
||||
recipe_book: RecipeBook,
|
||||
component_recipe_book: ComponentRecipeBook,
|
||||
repair_recipe_book: RepairRecipeBook,
|
||||
material_stats: MaterialStatManifest,
|
||||
ability_map: comp::item::tool::AbilityMap,
|
||||
server_constants: ServerConstants,
|
||||
|
@ -15,7 +15,7 @@ use veloren_common::{
|
||||
armor::{ArmorKind, Protection},
|
||||
modular::{generate_weapon_primary_components, generate_weapons},
|
||||
tool::{Hands, Tool, ToolKind},
|
||||
Item, MaterialStatManifest,
|
||||
DurabilityMultiplier, Item, MaterialStatManifest,
|
||||
},
|
||||
},
|
||||
generation::{EntityConfig, EntityInfo},
|
||||
@ -58,21 +58,23 @@ fn armor_stats() -> Result<(), Box<dyn Error>> {
|
||||
}
|
||||
|
||||
let msm = &MaterialStatManifest::load().read();
|
||||
let dur_mult = DurabilityMultiplier(1.0);
|
||||
let armor_stats = armor.stats(msm, dur_mult);
|
||||
|
||||
let protection = match armor.stats(msm).protection {
|
||||
let protection = match armor_stats.protection {
|
||||
Some(Protection::Invincible) => "Invincible".to_string(),
|
||||
Some(Protection::Normal(value)) => value.to_string(),
|
||||
None => "0.0".to_string(),
|
||||
};
|
||||
let poise_resilience = match armor.stats(msm).poise_resilience {
|
||||
let poise_resilience = match armor_stats.poise_resilience {
|
||||
Some(Protection::Invincible) => "Invincible".to_string(),
|
||||
Some(Protection::Normal(value)) => value.to_string(),
|
||||
None => "0.0".to_string(),
|
||||
};
|
||||
let max_energy = armor.stats(msm).energy_max.unwrap_or(0.0).to_string();
|
||||
let energy_reward = armor.stats(msm).energy_reward.unwrap_or(0.0).to_string();
|
||||
let crit_power = armor.stats(msm).crit_power.unwrap_or(0.0).to_string();
|
||||
let stealth = armor.stats(msm).stealth.unwrap_or(0.0).to_string();
|
||||
let max_energy = armor_stats.energy_max.unwrap_or(0.0).to_string();
|
||||
let energy_reward = armor_stats.energy_reward.unwrap_or(0.0).to_string();
|
||||
let crit_power = armor_stats.crit_power.unwrap_or(0.0).to_string();
|
||||
let stealth = armor_stats.stealth.unwrap_or(0.0).to_string();
|
||||
|
||||
wtr.write_record([
|
||||
item.item_definition_id()
|
||||
@ -124,14 +126,17 @@ fn weapon_stats() -> Result<(), Box<dyn Error>> {
|
||||
|
||||
for item in items.iter() {
|
||||
if let comp::item::ItemKind::Tool(tool) = &*item.kind() {
|
||||
let power = tool.base_power().to_string();
|
||||
let effect_power = tool.base_effect_power().to_string();
|
||||
let speed = tool.base_speed().to_string();
|
||||
let crit_chance = tool.base_crit_chance().to_string();
|
||||
let range = tool.base_range().to_string();
|
||||
let energy_efficiency = tool.base_energy_efficiency().to_string();
|
||||
let buff_strength = tool.base_buff_strength().to_string();
|
||||
let equip_time = tool.equip_time().as_secs_f32().to_string();
|
||||
let dur_mult = DurabilityMultiplier(1.0);
|
||||
let tool_stats = tool.stats(dur_mult);
|
||||
|
||||
let power = tool_stats.power.to_string();
|
||||
let effect_power = tool_stats.effect_power.to_string();
|
||||
let speed = tool_stats.speed.to_string();
|
||||
let crit_chance = tool_stats.crit_chance.to_string();
|
||||
let range = tool_stats.range.to_string();
|
||||
let energy_efficiency = tool_stats.energy_efficiency.to_string();
|
||||
let buff_strength = tool_stats.buff_strength.to_string();
|
||||
let equip_time = tool_stats.equip_time_secs.to_string();
|
||||
let kind = get_tool_kind(&tool.kind);
|
||||
let hands = get_tool_hands(tool);
|
||||
|
||||
|
@ -1230,7 +1230,7 @@ fn weapon_rating<T: ItemDesc>(item: &T, _msm: &MaterialStatManifest) -> f32 {
|
||||
const BUFF_STRENGTH_WEIGHT: f32 = 1.5;
|
||||
|
||||
let rating = if let ItemKind::Tool(tool) = &*item.kind() {
|
||||
let stats = tool.stats;
|
||||
let stats = tool.stats(item.stats_durability_multiplier());
|
||||
|
||||
// TODO: Look into changing the 0.5 to reflect armor later maybe?
|
||||
// Since it is only for weapon though, it probably makes sense to leave
|
||||
@ -1361,7 +1361,9 @@ pub fn compute_crit_mult(inventory: Option<&Inventory>, msm: &MaterialStatManife
|
||||
inv.equipped_items()
|
||||
.filter_map(|item| {
|
||||
if let ItemKind::Armor(armor) = &*item.kind() {
|
||||
armor.stats(msm).crit_power
|
||||
armor
|
||||
.stats(msm, item.stats_durability_multiplier())
|
||||
.crit_power
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -1379,7 +1381,9 @@ pub fn compute_energy_reward_mod(inventory: Option<&Inventory>, msm: &MaterialSt
|
||||
inv.equipped_items()
|
||||
.filter_map(|item| {
|
||||
if let ItemKind::Armor(armor) = &*item.kind() {
|
||||
armor.stats(msm).energy_reward
|
||||
armor
|
||||
.stats(msm, item.stats_durability_multiplier())
|
||||
.energy_reward
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -1397,7 +1401,9 @@ pub fn compute_max_energy_mod(inventory: Option<&Inventory>, msm: &MaterialStatM
|
||||
inv.equipped_items()
|
||||
.filter_map(|item| {
|
||||
if let ItemKind::Armor(armor) = &*item.kind() {
|
||||
armor.stats(msm).energy_max
|
||||
armor
|
||||
.stats(msm, item.stats_durability_multiplier())
|
||||
.energy_max
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -1433,7 +1439,7 @@ pub fn stealth_multiplier_from_items(
|
||||
inv.equipped_items()
|
||||
.filter_map(|item| {
|
||||
if let ItemKind::Armor(armor) = &*item.kind() {
|
||||
armor.stats(msm).stealth
|
||||
armor.stats(msm, item.stats_durability_multiplier()).stealth
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -1456,7 +1462,9 @@ pub fn compute_protection(
|
||||
inv.equipped_items()
|
||||
.filter_map(|item| {
|
||||
if let ItemKind::Armor(armor) = &*item.kind() {
|
||||
armor.stats(msm).protection
|
||||
armor
|
||||
.stats(msm, item.stats_durability_multiplier())
|
||||
.protection
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -105,6 +105,10 @@ pub enum CraftEvent {
|
||||
modifier: Option<InvSlotId>,
|
||||
slots: Vec<(u32, InvSlotId)>,
|
||||
},
|
||||
Repair {
|
||||
item: Slot,
|
||||
slots: Vec<(u32, InvSlotId)>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
comp::item::{MaterialStatManifest, Rgb},
|
||||
comp::item::{DurabilityMultiplier, MaterialStatManifest, Rgb},
|
||||
terrain::{Block, BlockKind},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -25,6 +25,25 @@ pub enum ArmorKind {
|
||||
Bag,
|
||||
}
|
||||
|
||||
impl ArmorKind {
|
||||
pub fn has_durability(self) -> bool {
|
||||
match self {
|
||||
ArmorKind::Shoulder => true,
|
||||
ArmorKind::Chest => true,
|
||||
ArmorKind::Belt => true,
|
||||
ArmorKind::Hand => true,
|
||||
ArmorKind::Pants => true,
|
||||
ArmorKind::Foot => true,
|
||||
ArmorKind::Back => true,
|
||||
ArmorKind::Ring => false,
|
||||
ArmorKind::Neck => false,
|
||||
ArmorKind::Head => true,
|
||||
ArmorKind::Tabard => false,
|
||||
ArmorKind::Bag => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Armor {
|
||||
/// Determines whether two pieces of armour are superficially equivalent to
|
||||
/// one another (i.e: one may be substituted for the other in crafting
|
||||
@ -212,8 +231,12 @@ pub struct Armor {
|
||||
impl Armor {
|
||||
pub fn new(kind: ArmorKind, stats: StatsSource) -> Self { Self { kind, stats } }
|
||||
|
||||
pub fn stats(&self, msm: &MaterialStatManifest) -> Stats {
|
||||
match &self.stats {
|
||||
pub fn stats(
|
||||
&self,
|
||||
msm: &MaterialStatManifest,
|
||||
durability_multiplier: DurabilityMultiplier,
|
||||
) -> Stats {
|
||||
let base_stats = match &self.stats {
|
||||
StatsSource::Direct(stats) => *stats,
|
||||
StatsSource::FromSet(set) => {
|
||||
let set_stats = msm.armor_stats(set).unwrap_or_else(Stats::none);
|
||||
@ -237,7 +260,8 @@ impl Armor {
|
||||
|
||||
set_stats * multiplier
|
||||
},
|
||||
}
|
||||
};
|
||||
base_stats * durability_multiplier.0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -5,11 +5,11 @@ pub mod tool;
|
||||
|
||||
// Reexports
|
||||
pub use modular::{MaterialStatManifest, ModularBase, ModularComponent};
|
||||
pub use tool::{AbilitySet, AbilitySpec, Hands, Tool, ToolKind};
|
||||
pub use tool::{AbilityMap, AbilitySet, AbilitySpec, Hands, Tool, ToolKind};
|
||||
|
||||
use crate::{
|
||||
assets::{self, AssetExt, BoxedError, Error},
|
||||
comp::inventory::{item::tool::AbilityMap, InvSlot},
|
||||
comp::inventory::InvSlot,
|
||||
effect::Effect,
|
||||
recipe::RecipeInput,
|
||||
terrain::Block,
|
||||
@ -348,6 +348,21 @@ impl ItemKind {
|
||||
};
|
||||
result
|
||||
}
|
||||
|
||||
pub fn has_durability(&self) -> bool {
|
||||
match self {
|
||||
ItemKind::Tool(_) => true,
|
||||
ItemKind::Armor(armor) => armor.kind.has_durability(),
|
||||
ItemKind::ModularComponent(_)
|
||||
| ItemKind::Lantern(_)
|
||||
| ItemKind::Glider
|
||||
| ItemKind::Consumable { .. }
|
||||
| ItemKind::Throwable { .. }
|
||||
| ItemKind::Utility { .. }
|
||||
| ItemKind::Ingredient { .. }
|
||||
| ItemKind::TagExamples { .. } => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type ItemId = AtomicCell<Option<NonZeroU64>>;
|
||||
@ -396,6 +411,10 @@ pub struct Item {
|
||||
slots: Vec<InvSlot>,
|
||||
item_config: Option<Box<ItemConfig>>,
|
||||
hash: u64,
|
||||
/// Tracks how many deaths occurred while item was equipped, which is
|
||||
/// converted into the items durability. Only tracked for tools and armor
|
||||
/// currently.
|
||||
durability_lost: Option<u32>,
|
||||
}
|
||||
|
||||
use std::hash::{Hash, Hasher};
|
||||
@ -609,7 +628,8 @@ impl TryFrom<(&Item, &AbilityMap, &MaterialStatManifest)> for ItemConfig {
|
||||
};
|
||||
let abilities = if let Some(set_key) = item.ability_spec() {
|
||||
if let Some(set) = ability_map.get_ability_set(&set_key) {
|
||||
set.clone().modified_by_tool(tool)
|
||||
set.clone()
|
||||
.modified_by_tool(tool, item.stats_durability_multiplier())
|
||||
} else {
|
||||
error!(
|
||||
"Custom ability set: {:?} references non-existent set, falling back to \
|
||||
@ -619,7 +639,8 @@ impl TryFrom<(&Item, &AbilityMap, &MaterialStatManifest)> for ItemConfig {
|
||||
tool_default(tool.kind).cloned().unwrap_or_default()
|
||||
}
|
||||
} else if let Some(set) = tool_default(tool.kind) {
|
||||
set.clone().modified_by_tool(tool)
|
||||
set.clone()
|
||||
.modified_by_tool(tool, item.stats_durability_multiplier())
|
||||
} else {
|
||||
error!(
|
||||
"No ability set defined for tool: {:?}, falling back to default ability set.",
|
||||
@ -766,6 +787,8 @@ impl assets::Asset for RawItemDef {
|
||||
pub struct OperationFailure;
|
||||
|
||||
impl Item {
|
||||
pub const MAX_DURABILITY: u32 = 8;
|
||||
|
||||
// TODO: consider alternatives such as default abilities that can be added to a
|
||||
// loadout when no weapon is present
|
||||
pub fn empty() -> Self { Item::new_from_asset_expect("common.items.weapons.empty.empty") }
|
||||
@ -785,7 +808,9 @@ impl Item {
|
||||
// These fields are updated immediately below
|
||||
item_config: None,
|
||||
hash: 0,
|
||||
durability_lost: None,
|
||||
};
|
||||
item.durability_lost = item.has_durability().then_some(0);
|
||||
item.update_item_state(ability_map, msm);
|
||||
item
|
||||
}
|
||||
@ -1089,7 +1114,7 @@ impl Item {
|
||||
ItemBase::Modular(mod_base) => {
|
||||
// TODO: Try to move further upward
|
||||
let msm = MaterialStatManifest::load().read();
|
||||
mod_base.kind(self.components(), &msm)
|
||||
mod_base.kind(self.components(), &msm, self.stats_durability_multiplier())
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1188,6 +1213,59 @@ impl Item {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn durability(&self) -> Option<u32> {
|
||||
self.durability_lost.map(|x| x.min(Self::MAX_DURABILITY))
|
||||
}
|
||||
|
||||
pub fn stats_durability_multiplier(&self) -> DurabilityMultiplier {
|
||||
let durability_lost = self.durability_lost.unwrap_or(0);
|
||||
debug_assert!(durability_lost <= Self::MAX_DURABILITY);
|
||||
const DURABILITY_THRESHOLD: u32 = 4;
|
||||
const MIN_FRAC: f32 = 0.2;
|
||||
let mult = (1.0
|
||||
- durability_lost.saturating_sub(DURABILITY_THRESHOLD) as f32
|
||||
/ (Self::MAX_DURABILITY - DURABILITY_THRESHOLD) as f32)
|
||||
* (1.0 - MIN_FRAC)
|
||||
+ MIN_FRAC;
|
||||
DurabilityMultiplier(mult)
|
||||
}
|
||||
|
||||
pub fn has_durability(&self) -> bool { self.kind().has_durability() }
|
||||
|
||||
pub fn increment_damage(&mut self, ability_map: &AbilityMap, msm: &MaterialStatManifest) {
|
||||
if let Some(durability_lost) = &mut self.durability_lost {
|
||||
if *durability_lost < Self::MAX_DURABILITY {
|
||||
*durability_lost += 1;
|
||||
}
|
||||
}
|
||||
// Update item state after applying durability because stats have potential to
|
||||
// change from different durability
|
||||
self.update_item_state(ability_map, msm);
|
||||
}
|
||||
|
||||
pub fn persistence_durability(&self) -> Option<NonZeroU32> {
|
||||
self.durability_lost.and_then(NonZeroU32::new)
|
||||
}
|
||||
|
||||
pub fn persistence_set_durability(&mut self, value: Option<NonZeroU32>) {
|
||||
// If changes have been made so that item no longer needs to track durability,
|
||||
// set to None
|
||||
if !self.has_durability() {
|
||||
self.durability_lost = None;
|
||||
} else {
|
||||
// Set durability to persisted value, and if item previously had no durability,
|
||||
// set to Some(0) so that durability will be tracked
|
||||
self.durability_lost = Some(value.map_or(0, NonZeroU32::get));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_durability(&mut self, ability_map: &AbilityMap, msm: &MaterialStatManifest) {
|
||||
self.durability_lost = self.has_durability().then_some(0);
|
||||
// Update item state after applying durability because stats have potential to
|
||||
// change from different durability
|
||||
self.update_item_state(ability_map, msm);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn create_test_item_from_kind(kind: ItemKind) -> Self {
|
||||
let ability_map = &AbilityMap::load().read();
|
||||
@ -1211,10 +1289,11 @@ pub trait ItemDesc {
|
||||
fn num_slots(&self) -> u16;
|
||||
fn item_definition_id(&self) -> ItemDefinitionId<'_>;
|
||||
fn tags(&self) -> Vec<ItemTag>;
|
||||
|
||||
fn is_modular(&self) -> bool;
|
||||
|
||||
fn components(&self) -> &[Item];
|
||||
fn has_durability(&self) -> bool;
|
||||
fn durability(&self) -> Option<u32>;
|
||||
fn stats_durability_multiplier(&self) -> DurabilityMultiplier;
|
||||
|
||||
fn tool_info(&self) -> Option<ToolKind> {
|
||||
if let ItemKind::Tool(tool) = &*self.kind() {
|
||||
@ -1243,6 +1322,14 @@ impl ItemDesc for Item {
|
||||
fn is_modular(&self) -> bool { self.is_modular() }
|
||||
|
||||
fn components(&self) -> &[Item] { self.components() }
|
||||
|
||||
fn has_durability(&self) -> bool { self.has_durability() }
|
||||
|
||||
fn durability(&self) -> Option<u32> { self.durability() }
|
||||
|
||||
fn stats_durability_multiplier(&self) -> DurabilityMultiplier {
|
||||
self.stats_durability_multiplier()
|
||||
}
|
||||
}
|
||||
|
||||
impl ItemDesc for ItemDef {
|
||||
@ -1265,6 +1352,12 @@ impl ItemDesc for ItemDef {
|
||||
fn is_modular(&self) -> bool { false }
|
||||
|
||||
fn components(&self) -> &[Item] { &[] }
|
||||
|
||||
fn has_durability(&self) -> bool { self.kind().has_durability() }
|
||||
|
||||
fn durability(&self) -> Option<u32> { None }
|
||||
|
||||
fn stats_durability_multiplier(&self) -> DurabilityMultiplier { DurabilityMultiplier(1.0) }
|
||||
}
|
||||
|
||||
impl Component for Item {
|
||||
@ -1278,6 +1371,9 @@ impl Component for ItemDrop {
|
||||
type Storage = DenseVecStorage<Self>;
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct DurabilityMultiplier(pub f32);
|
||||
|
||||
impl<'a, T: ItemDesc + ?Sized> ItemDesc for &'a T {
|
||||
fn description(&self) -> &str { (*self).description() }
|
||||
|
||||
@ -1296,6 +1392,14 @@ impl<'a, T: ItemDesc + ?Sized> ItemDesc for &'a T {
|
||||
fn is_modular(&self) -> bool { (*self).is_modular() }
|
||||
|
||||
fn components(&self) -> &[Item] { (*self).components() }
|
||||
|
||||
fn has_durability(&self) -> bool { (*self).has_durability() }
|
||||
|
||||
fn durability(&self) -> Option<u32> { (*self).durability() }
|
||||
|
||||
fn stats_durability_multiplier(&self) -> DurabilityMultiplier {
|
||||
(*self).stats_durability_multiplier()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns all item asset specifiers
|
||||
|
@ -1,7 +1,8 @@
|
||||
use super::{
|
||||
armor,
|
||||
tool::{self, AbilityMap, AbilitySpec, Hands},
|
||||
Item, ItemBase, ItemDef, ItemDesc, ItemKind, ItemTag, Material, Quality, ToolKind,
|
||||
tool::{self, AbilityMap, AbilitySpec, Hands, Tool},
|
||||
DurabilityMultiplier, Item, ItemBase, ItemDef, ItemDesc, ItemKind, ItemTag, Material, Quality,
|
||||
ToolKind,
|
||||
};
|
||||
use crate::{
|
||||
assets::{self, Asset, AssetExt, AssetHandle},
|
||||
@ -98,8 +99,17 @@ impl ModularBase {
|
||||
hand_restriction.unwrap_or(Hands::One)
|
||||
}
|
||||
|
||||
pub fn kind(&self, components: &[Item], msm: &MaterialStatManifest) -> Cow<ItemKind> {
|
||||
fn resolve_stats(components: &[Item], msm: &MaterialStatManifest) -> tool::Stats {
|
||||
pub(super) fn kind(
|
||||
&self,
|
||||
components: &[Item],
|
||||
msm: &MaterialStatManifest,
|
||||
durability_multiplier: DurabilityMultiplier,
|
||||
) -> Cow<ItemKind> {
|
||||
fn resolve_stats(
|
||||
components: &[Item],
|
||||
msm: &MaterialStatManifest,
|
||||
durability_multiplier: DurabilityMultiplier,
|
||||
) -> tool::Stats {
|
||||
components
|
||||
.iter()
|
||||
.filter_map(|comp| {
|
||||
@ -110,6 +120,7 @@ impl ModularBase {
|
||||
}
|
||||
})
|
||||
.fold(tool::Stats::one(), |a, b| a * b)
|
||||
* durability_multiplier
|
||||
}
|
||||
|
||||
let toolkind = components
|
||||
@ -124,11 +135,11 @@ impl ModularBase {
|
||||
.unwrap_or(ToolKind::Empty);
|
||||
|
||||
match self {
|
||||
ModularBase::Tool => Cow::Owned(ItemKind::Tool(tool::Tool {
|
||||
kind: toolkind,
|
||||
hands: Self::resolve_hands(components),
|
||||
stats: resolve_stats(components, msm),
|
||||
})),
|
||||
ModularBase::Tool => Cow::Owned(ItemKind::Tool(Tool::new(
|
||||
toolkind,
|
||||
Self::resolve_hands(components),
|
||||
resolve_stats(components, msm, durability_multiplier),
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,14 +3,13 @@
|
||||
|
||||
use crate::{
|
||||
assets::{self, Asset, AssetExt, AssetHandle},
|
||||
comp::{ability::Stance, skills::Skill, CharacterAbility, SkillSet},
|
||||
comp::{
|
||||
ability::Stance, item::DurabilityMultiplier, skills::Skill, CharacterAbility, SkillSet,
|
||||
},
|
||||
};
|
||||
use hashbrown::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub},
|
||||
time::Duration,
|
||||
};
|
||||
use std::ops::{Add, AddAssign, Div, Mul, MulAssign, Sub};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Ord, PartialOrd)]
|
||||
pub enum ToolKind {
|
||||
@ -147,6 +146,20 @@ impl Stats {
|
||||
let diminished = (self.buff_strength - base + 1.0).log(5.0);
|
||||
base + diminished
|
||||
}
|
||||
|
||||
pub fn with_durability_mult(&self, dur_mult: DurabilityMultiplier) -> Self {
|
||||
let less_scaled = dur_mult.0 * 0.5 + 0.5;
|
||||
Self {
|
||||
equip_time_secs: self.equip_time_secs / less_scaled.max(0.01),
|
||||
power: self.power * dur_mult.0,
|
||||
effect_power: self.effect_power * dur_mult.0,
|
||||
speed: self.speed * less_scaled,
|
||||
crit_chance: self.crit_chance * dur_mult.0,
|
||||
range: self.range * dur_mult.0,
|
||||
energy_efficiency: self.energy_efficiency * dur_mult.0,
|
||||
buff_strength: self.buff_strength * dur_mult.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Asset for Stats {
|
||||
@ -171,9 +184,11 @@ impl Add<Stats> for Stats {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign<Stats> for Stats {
|
||||
fn add_assign(&mut self, other: Stats) { *self = *self + other; }
|
||||
}
|
||||
|
||||
impl Sub<Stats> for Stats {
|
||||
type Output = Self;
|
||||
|
||||
@ -190,6 +205,7 @@ impl Sub<Stats> for Stats {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Stats> for Stats {
|
||||
type Output = Self;
|
||||
|
||||
@ -206,9 +222,11 @@ impl Mul<Stats> for Stats {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MulAssign<Stats> for Stats {
|
||||
fn mul_assign(&mut self, other: Stats) { *self = *self * other; }
|
||||
}
|
||||
|
||||
impl Div<f32> for Stats {
|
||||
type Output = Self;
|
||||
|
||||
@ -225,15 +243,18 @@ impl Div<f32> for Stats {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl DivAssign<usize> for Stats {
|
||||
fn div_assign(&mut self, scalar: usize) { *self = *self / (scalar as f32); }
|
||||
|
||||
impl Mul<DurabilityMultiplier> for Stats {
|
||||
type Output = Self;
|
||||
|
||||
fn mul(self, value: DurabilityMultiplier) -> Self { self.with_durability_mult(value) }
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Tool {
|
||||
pub kind: ToolKind,
|
||||
pub hands: Hands,
|
||||
pub stats: Stats,
|
||||
stats: Stats,
|
||||
// TODO: item specific abilities
|
||||
}
|
||||
|
||||
@ -259,24 +280,9 @@ impl Tool {
|
||||
}
|
||||
}
|
||||
|
||||
// Keep power between 0.5 and 2.00
|
||||
pub fn base_power(&self) -> f32 { self.stats.power }
|
||||
|
||||
pub fn base_effect_power(&self) -> f32 { self.stats.effect_power }
|
||||
|
||||
/// Has floor to prevent infinite durations being created later down due to
|
||||
/// a divide by zero
|
||||
pub fn base_speed(&self) -> f32 { self.stats.speed.max(0.1) }
|
||||
|
||||
pub fn base_crit_chance(&self) -> f32 { self.stats.crit_chance }
|
||||
|
||||
pub fn base_range(&self) -> f32 { self.stats.range }
|
||||
|
||||
pub fn base_energy_efficiency(&self) -> f32 { self.stats.energy_efficiency }
|
||||
|
||||
pub fn base_buff_strength(&self) -> f32 { self.stats.buff_strength }
|
||||
|
||||
pub fn equip_time(&self) -> Duration { Duration::from_secs_f32(self.stats.equip_time_secs) }
|
||||
pub fn stats(&self, durability_multiplier: DurabilityMultiplier) -> Stats {
|
||||
self.stats * durability_multiplier
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
@ -376,10 +382,16 @@ impl AbilityContext {
|
||||
|
||||
impl AbilitySet<AbilityItem> {
|
||||
#[must_use]
|
||||
pub fn modified_by_tool(self, tool: &Tool) -> Self {
|
||||
pub fn modified_by_tool(
|
||||
self,
|
||||
tool: &Tool,
|
||||
durability_multiplier: DurabilityMultiplier,
|
||||
) -> Self {
|
||||
self.map(|a| AbilityItem {
|
||||
id: a.id,
|
||||
ability: a.ability.adjusted_by_stats(tool.stats),
|
||||
ability: a
|
||||
.ability
|
||||
.adjusted_by_stats(tool.stats(durability_multiplier)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -311,6 +311,12 @@ impl Loadout {
|
||||
self.slots.iter().filter_map(|x| x.slot.as_ref())
|
||||
}
|
||||
|
||||
pub(super) fn items_with_slot(&self) -> impl Iterator<Item = (EquipSlot, &Item)> {
|
||||
self.slots
|
||||
.iter()
|
||||
.filter_map(|x| x.slot.as_ref().map(|i| (x.equip_slot, i)))
|
||||
}
|
||||
|
||||
/// Checks that a slot can hold a given item
|
||||
pub(super) fn slot_can_hold(
|
||||
&self,
|
||||
@ -416,6 +422,36 @@ impl Loadout {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Increments durability by 1 of all valid items
|
||||
pub(super) fn damage_items(
|
||||
&mut self,
|
||||
ability_map: &item::tool::AbilityMap,
|
||||
msm: &item::MaterialStatManifest,
|
||||
) {
|
||||
self.slots
|
||||
.iter_mut()
|
||||
.filter_map(|slot| slot.slot.as_mut())
|
||||
.filter(|item| item.has_durability())
|
||||
.for_each(|item| item.increment_damage(ability_map, msm));
|
||||
}
|
||||
|
||||
/// Resets durability of item in specified slot
|
||||
pub(super) fn repair_item_at_slot(
|
||||
&mut self,
|
||||
equip_slot: EquipSlot,
|
||||
ability_map: &item::tool::AbilityMap,
|
||||
msm: &item::MaterialStatManifest,
|
||||
) {
|
||||
if let Some(item) = self
|
||||
.slots
|
||||
.iter_mut()
|
||||
.find(|slot| slot.equip_slot == equip_slot)
|
||||
.and_then(|slot| slot.slot.as_mut())
|
||||
{
|
||||
item.reset_durability(ability_map, msm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -605,6 +605,10 @@ impl Inventory {
|
||||
|
||||
pub fn equipped_items(&self) -> impl Iterator<Item = &Item> { self.loadout.items() }
|
||||
|
||||
pub fn equipped_items_with_slot(&self) -> impl Iterator<Item = (EquipSlot, &Item)> {
|
||||
self.loadout.items_with_slot()
|
||||
}
|
||||
|
||||
/// Replaces the loadout item (if any) in the given EquipSlot with the
|
||||
/// provided item, returning the item that was previously in the slot.
|
||||
pub fn replace_loadout_item(
|
||||
@ -878,6 +882,35 @@ impl Inventory {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Increments durability of all valid items equipped in loaodut by 1
|
||||
pub fn damage_items(
|
||||
&mut self,
|
||||
ability_map: &item::tool::AbilityMap,
|
||||
msm: &item::MaterialStatManifest,
|
||||
) {
|
||||
self.loadout.damage_items(ability_map, msm)
|
||||
}
|
||||
|
||||
/// Resets durability of item in specified slot
|
||||
pub fn repair_item_at_slot(
|
||||
&mut self,
|
||||
slot: Slot,
|
||||
ability_map: &item::tool::AbilityMap,
|
||||
msm: &item::MaterialStatManifest,
|
||||
) {
|
||||
match slot {
|
||||
Slot::Inventory(invslot) => {
|
||||
if let Some(Some(item)) = self.slot_mut(invslot) {
|
||||
item.reset_durability(ability_map, msm);
|
||||
}
|
||||
},
|
||||
Slot::Equip(equip_slot) => {
|
||||
self.loadout
|
||||
.repair_item_at_slot(equip_slot, ability_map, msm);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Inventory {
|
||||
|
@ -1022,16 +1022,17 @@ impl TradePricing {
|
||||
|
||||
#[cfg(test)]
|
||||
fn print_sorted(&self) {
|
||||
use crate::comp::item::armor; //, ItemKind, MaterialStatManifest};
|
||||
use crate::comp::item::{armor, DurabilityMultiplier}; //, ItemKind, MaterialStatManifest};
|
||||
|
||||
println!("Item, ForSale, Amount, Good, Quality, Deal, Unit,");
|
||||
|
||||
fn more_information(i: &Item, p: f32) -> (String, &'static str) {
|
||||
let msm = &MaterialStatManifest::load().read();
|
||||
let durability_multiplier = DurabilityMultiplier(1.0);
|
||||
|
||||
if let ItemKind::Armor(a) = &*i.kind() {
|
||||
(
|
||||
match a.stats(msm).protection {
|
||||
match a.stats(msm, durability_multiplier).protection {
|
||||
Some(armor::Protection::Invincible) => "Invincible".into(),
|
||||
Some(armor::Protection::Normal(x)) => format!("{:.4}", x * p),
|
||||
None => "0.0".into(),
|
||||
@ -1039,10 +1040,8 @@ impl TradePricing {
|
||||
"prot/val",
|
||||
)
|
||||
} else if let ItemKind::Tool(t) = &*i.kind() {
|
||||
(
|
||||
format!("{:.4}", t.stats.power * t.stats.speed * p),
|
||||
"dps/val",
|
||||
)
|
||||
let stats = t.stats(durability_multiplier);
|
||||
(format!("{:.4}", stats.power * stats.speed * p), "dps/val")
|
||||
} else if let ItemKind::Consumable { kind: _, effects } = &*i.kind() {
|
||||
(
|
||||
effects
|
||||
|
@ -270,7 +270,9 @@ impl Poise {
|
||||
inv.equipped_items()
|
||||
.filter_map(|item| {
|
||||
if let ItemKind::Armor(armor) = &*item.kind() {
|
||||
armor.stats(msm).poise_resilience
|
||||
armor
|
||||
.stats(msm, item.stats_durability_multiplier())
|
||||
.poise_resilience
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -11,7 +11,8 @@
|
||||
trait_alias,
|
||||
type_alias_impl_trait,
|
||||
extend_one,
|
||||
arbitrary_self_types
|
||||
arbitrary_self_types,
|
||||
int_roundings
|
||||
)]
|
||||
#![feature(hash_drain_filter)]
|
||||
|
||||
|
@ -1,11 +1,12 @@
|
||||
use crate::{
|
||||
assets::{self, AssetExt, AssetHandle},
|
||||
comp::{
|
||||
inventory::slot::InvSlotId,
|
||||
inventory::slot::{InvSlotId, Slot},
|
||||
item::{
|
||||
modular,
|
||||
tool::{AbilityMap, ToolKind},
|
||||
ItemBase, ItemDef, ItemDefinitionIdOwned, ItemKind, ItemTag, MaterialStatManifest,
|
||||
ItemBase, ItemDef, ItemDefinitionId, ItemDefinitionIdOwned, ItemKind, ItemTag,
|
||||
MaterialStatManifest,
|
||||
},
|
||||
Inventory, Item,
|
||||
},
|
||||
@ -39,6 +40,45 @@ pub enum RecipeInput {
|
||||
ListSameItem(Vec<Arc<ItemDef>>),
|
||||
}
|
||||
|
||||
impl RecipeInput {
|
||||
fn handle_requirement<'a, I: Iterator<Item = InvSlotId>>(
|
||||
&'a self,
|
||||
amount: u32,
|
||||
slot_claims: &mut HashMap<InvSlotId, u32>,
|
||||
unsatisfied_requirements: &mut Vec<(&'a RecipeInput, u32)>,
|
||||
inv: &Inventory,
|
||||
input_slots: I,
|
||||
) {
|
||||
let mut required = amount;
|
||||
// contains_any check used for recipes that have an input that is not consumed,
|
||||
// e.g. craftsman hammer
|
||||
// Goes through each slot and marks some amount from each slot as claimed
|
||||
let contains_any = input_slots.into_iter().all(|slot| {
|
||||
// Checks that the item in the slot can be used for the input
|
||||
if let Some(item) = inv
|
||||
.get(slot)
|
||||
.filter(|item| item.matches_recipe_input(self, amount))
|
||||
{
|
||||
// Gets the number of items claimed from the slot, or sets to 0 if slot has
|
||||
// not been claimed by another input yet
|
||||
let claimed = slot_claims.entry(slot).or_insert(0);
|
||||
let available = item.amount().saturating_sub(*claimed);
|
||||
let provided = available.min(required);
|
||||
required -= provided;
|
||||
*claimed += provided;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
// If there were not sufficient items to cover requirement between all provided
|
||||
// slots, or if non-consumed item was not present, mark input as not satisfied
|
||||
if required > 0 || !contains_any {
|
||||
unsatisfied_requirements.push((self, required));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Recipe {
|
||||
pub output: (Arc<ItemDef>, u32),
|
||||
@ -477,13 +517,11 @@ impl assets::Compound for RecipeBook {
|
||||
cache: assets::AnyCache,
|
||||
specifier: &assets::SharedString,
|
||||
) -> Result<Self, assets::BoxedError> {
|
||||
#[inline]
|
||||
fn load_item_def(spec: &(String, u32)) -> Result<(Arc<ItemDef>, u32), assets::Error> {
|
||||
let def = Arc::<ItemDef>::load_cloned(&spec.0)?;
|
||||
Ok((def, spec.1))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn load_recipe_input(
|
||||
(input, amount, is_mod_comp): &(RawRecipeInput, u32, bool),
|
||||
) -> Result<(RecipeInput, u32, bool), assets::Error> {
|
||||
@ -594,65 +632,26 @@ impl ComponentRecipe {
|
||||
let mut slot_claims = HashMap::new();
|
||||
let mut unsatisfied_requirements = Vec::new();
|
||||
|
||||
fn handle_requirement<'a, I: Iterator<Item = InvSlotId>>(
|
||||
slot_claims: &mut HashMap<InvSlotId, u32>,
|
||||
unsatisfied_requirements: &mut Vec<(&'a RecipeInput, u32)>,
|
||||
inv: &Inventory,
|
||||
input: &'a RecipeInput,
|
||||
amount: u32,
|
||||
input_slots: I,
|
||||
) {
|
||||
let mut required = amount;
|
||||
// contains_any check used for recipes that have an input that is not consumed,
|
||||
// e.g. craftsman hammer
|
||||
// Goes through each slot and marks some amount from each slot as claimed
|
||||
let contains_any = input_slots.into_iter().all(|slot| {
|
||||
// Checks that the item in the slot can be used for the input
|
||||
if let Some(item) = inv
|
||||
.get(slot)
|
||||
.filter(|item| item.matches_recipe_input(input, amount))
|
||||
{
|
||||
// Gets the number of items claimed from the slot, or sets to 0 if slot has
|
||||
// not been claimed by another input yet
|
||||
let claimed = slot_claims.entry(slot).or_insert(0);
|
||||
let available = item.amount().saturating_sub(*claimed);
|
||||
let provided = available.min(required);
|
||||
required -= provided;
|
||||
*claimed += provided;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
// If there were not sufficient items to cover requirement between all provided
|
||||
// slots, or if non-consumed item was not present, mark input as not satisfied
|
||||
if required > 0 || !contains_any {
|
||||
unsatisfied_requirements.push((input, required));
|
||||
}
|
||||
}
|
||||
|
||||
// Checks each input against slots in the inventory. If the slots contain an
|
||||
// item that fulfills the need of the input, marks some of the item as claimed
|
||||
// up to quantity needed for the crafting input. If the item either
|
||||
// cannot be used, or there is insufficient quantity, adds input and
|
||||
// number of materials needed to unsatisfied requirements.
|
||||
handle_requirement(
|
||||
self.material.0.handle_requirement(
|
||||
self.material.1,
|
||||
&mut slot_claims,
|
||||
&mut unsatisfied_requirements,
|
||||
inv,
|
||||
&self.material.0,
|
||||
self.material.1,
|
||||
core::iter::once(material_slot),
|
||||
);
|
||||
if let Some((modifier_input, modifier_amount)) = &self.modifier {
|
||||
// TODO: Better way to get slot to use that ensures this requirement fails if no
|
||||
// slot provided?
|
||||
handle_requirement(
|
||||
modifier_input.handle_requirement(
|
||||
*modifier_amount,
|
||||
&mut slot_claims,
|
||||
&mut unsatisfied_requirements,
|
||||
inv,
|
||||
modifier_input,
|
||||
*modifier_amount,
|
||||
core::iter::once(modifier_slot.unwrap_or(InvSlotId::new(0, 0))),
|
||||
);
|
||||
}
|
||||
@ -666,12 +665,11 @@ impl ComponentRecipe {
|
||||
.filter_map(|(j, slot)| if i as u32 == *j { Some(slot) } else { None })
|
||||
.copied();
|
||||
// Checks if requirement is met, and if not marks it as unsatisfied
|
||||
handle_requirement(
|
||||
input.handle_requirement(
|
||||
*amount,
|
||||
&mut slot_claims,
|
||||
&mut unsatisfied_requirements,
|
||||
inv,
|
||||
input,
|
||||
*amount,
|
||||
input_slots,
|
||||
);
|
||||
});
|
||||
@ -835,7 +833,6 @@ impl assets::Compound for ComponentRecipeBook {
|
||||
cache: assets::AnyCache,
|
||||
specifier: &assets::SharedString,
|
||||
) -> Result<Self, assets::BoxedError> {
|
||||
#[inline]
|
||||
fn create_recipe_key(raw_recipe: &RawComponentRecipe) -> ComponentKey {
|
||||
match &raw_recipe.output {
|
||||
RawComponentOutput::ToolPrimaryComponent { toolkind, item: _ } => {
|
||||
@ -853,7 +850,6 @@ impl assets::Compound for ComponentRecipeBook {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn load_recipe(raw_recipe: &RawComponentRecipe) -> Result<ComponentRecipe, assets::Error> {
|
||||
let output = match &raw_recipe.output {
|
||||
RawComponentOutput::ToolPrimaryComponent { toolkind: _, item } => {
|
||||
@ -900,6 +896,210 @@ impl assets::Compound for ComponentRecipeBook {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, Debug)]
|
||||
enum RepairKey {
|
||||
ItemDefId(String),
|
||||
ModularWeapon { material: String },
|
||||
}
|
||||
|
||||
impl RepairKey {
|
||||
fn from_item(item: &Item) -> Option<Self> {
|
||||
match item.item_definition_id() {
|
||||
ItemDefinitionId::Simple(item_id) => Some(Self::ItemDefId(String::from(item_id))),
|
||||
ItemDefinitionId::Compound { .. } => None,
|
||||
ItemDefinitionId::Modular { pseudo_base, .. } => match pseudo_base {
|
||||
"veloren.core.pseudo_items.modular.tool" => {
|
||||
if let Some(ItemDefinitionId::Simple(material)) = item
|
||||
.components()
|
||||
.iter()
|
||||
.find(|comp| {
|
||||
matches!(
|
||||
&*comp.kind(),
|
||||
ItemKind::ModularComponent(
|
||||
modular::ModularComponent::ToolPrimaryComponent { .. }
|
||||
)
|
||||
)
|
||||
})
|
||||
.and_then(|comp| {
|
||||
comp.components()
|
||||
.iter()
|
||||
.next()
|
||||
.map(|comp| comp.item_definition_id())
|
||||
})
|
||||
{
|
||||
let material = String::from(material);
|
||||
Some(Self::ModularWeapon { material })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
_ => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
struct RawRepairRecipe {
|
||||
inputs: Vec<(RawRecipeInput, u32)>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
struct RawRepairRecipeBook {
|
||||
recipes: HashMap<RepairKey, RawRepairRecipe>,
|
||||
fallback: RawRepairRecipe,
|
||||
}
|
||||
|
||||
impl assets::Asset for RawRepairRecipeBook {
|
||||
type Loader = assets::RonLoader;
|
||||
|
||||
const EXTENSION: &'static str = "ron";
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct RepairRecipe {
|
||||
inputs: Vec<(RecipeInput, u32)>,
|
||||
}
|
||||
|
||||
impl RepairRecipe {
|
||||
/// Determine whether the inventory contains the ingredients for a repair.
|
||||
/// If it does, return a vec of inventory slots that contain the
|
||||
/// ingredients needed, whose positions correspond to particular repair
|
||||
/// inputs. If items are missing, return the missing items, and how many
|
||||
/// are missing.
|
||||
pub fn inventory_contains_ingredients(
|
||||
&self,
|
||||
item: &Item,
|
||||
inv: &Inventory,
|
||||
) -> Result<Vec<(u32, InvSlotId)>, Vec<(&RecipeInput, u32)>> {
|
||||
inventory_contains_ingredients(self.inputs(item), inv, 1)
|
||||
}
|
||||
|
||||
pub fn inputs(&self, item: &Item) -> impl Iterator<Item = (&RecipeInput, u32)> {
|
||||
let item_durability = item.durability().unwrap_or(0);
|
||||
self.inputs
|
||||
.iter()
|
||||
.filter_map(move |(input, original_amount)| {
|
||||
let amount = (original_amount * item_durability) / Item::MAX_DURABILITY;
|
||||
// If original repair recipe consumed ingredients, but item not damaged enough
|
||||
// to actually need to consume item, remove item as requirement.
|
||||
if *original_amount > 0 && amount == 0 {
|
||||
None
|
||||
} else {
|
||||
Some((input, amount))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct RepairRecipeBook {
|
||||
recipes: HashMap<RepairKey, RepairRecipe>,
|
||||
fallback: RepairRecipe,
|
||||
}
|
||||
|
||||
impl RepairRecipeBook {
|
||||
pub fn repair_recipe(&self, item: &Item) -> Option<&RepairRecipe> {
|
||||
RepairKey::from_item(item)
|
||||
.as_ref()
|
||||
.and_then(|key| self.recipes.get(key))
|
||||
.or_else(|| item.has_durability().then_some(&self.fallback))
|
||||
}
|
||||
|
||||
pub fn repair_item(
|
||||
&self,
|
||||
inv: &mut Inventory,
|
||||
item: Slot,
|
||||
slots: Vec<(u32, InvSlotId)>,
|
||||
ability_map: &AbilityMap,
|
||||
msm: &MaterialStatManifest,
|
||||
) -> Result<(), Vec<(&RecipeInput, u32)>> {
|
||||
let mut slot_claims = HashMap::new();
|
||||
let mut unsatisfied_requirements = Vec::new();
|
||||
|
||||
if let Some(item) = match item {
|
||||
Slot::Equip(slot) => inv.equipped(slot),
|
||||
Slot::Inventory(slot) => inv.get(slot),
|
||||
} {
|
||||
if let Some(repair_recipe) = self.repair_recipe(item) {
|
||||
repair_recipe
|
||||
.inputs(item)
|
||||
.enumerate()
|
||||
.for_each(|(i, (input, amount))| {
|
||||
// Gets all slots provided for this input by the frontend
|
||||
let input_slots = slots
|
||||
.iter()
|
||||
.filter_map(|(j, slot)| if i as u32 == *j { Some(slot) } else { None })
|
||||
.copied();
|
||||
// Checks if requirement is met, and if not marks it as unsatisfied
|
||||
input.handle_requirement(
|
||||
amount,
|
||||
&mut slot_claims,
|
||||
&mut unsatisfied_requirements,
|
||||
inv,
|
||||
input_slots,
|
||||
);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if unsatisfied_requirements.is_empty() {
|
||||
for (slot, to_remove) in slot_claims.iter() {
|
||||
for _ in 0..*to_remove {
|
||||
let _ = inv
|
||||
.take(*slot, ability_map, msm)
|
||||
.expect("Expected item to exist in the inventory");
|
||||
}
|
||||
}
|
||||
|
||||
inv.repair_item_at_slot(item, ability_map, msm);
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(unsatisfied_requirements)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl assets::Compound for RepairRecipeBook {
|
||||
fn load(
|
||||
cache: assets::AnyCache,
|
||||
specifier: &assets::SharedString,
|
||||
) -> Result<Self, assets::BoxedError> {
|
||||
fn load_recipe_input(
|
||||
(input, amount): &(RawRecipeInput, u32),
|
||||
) -> Result<(RecipeInput, u32), assets::Error> {
|
||||
let input = input.load_recipe_input()?;
|
||||
Ok((input, *amount))
|
||||
}
|
||||
|
||||
let raw = cache.load::<RawRepairRecipeBook>(specifier)?.cloned();
|
||||
|
||||
let recipes = raw
|
||||
.recipes
|
||||
.iter()
|
||||
.map(|(key, RawRepairRecipe { inputs })| {
|
||||
let inputs = inputs
|
||||
.iter()
|
||||
.map(load_recipe_input)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok((key.clone(), RepairRecipe { inputs }))
|
||||
})
|
||||
.collect::<Result<_, assets::Error>>()?;
|
||||
|
||||
let fallback = RepairRecipe {
|
||||
inputs: raw
|
||||
.fallback
|
||||
.inputs
|
||||
.iter()
|
||||
.map(load_recipe_input)
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
};
|
||||
|
||||
Ok(RepairRecipeBook { recipes, fallback })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_recipe_book() -> AssetHandle<RecipeBook> {
|
||||
RecipeBook::load_expect("common.recipe_book")
|
||||
}
|
||||
@ -908,6 +1108,10 @@ pub fn default_component_recipe_book() -> AssetHandle<ComponentRecipeBook> {
|
||||
ComponentRecipeBook::load_expect("common.component_recipe_book")
|
||||
}
|
||||
|
||||
pub fn default_repair_recipe_book() -> AssetHandle<RepairRecipeBook> {
|
||||
RepairRecipeBook::load_expect("common.repair_recipe_book")
|
||||
}
|
||||
|
||||
impl assets::Compound for ReverseComponentRecipeBook {
|
||||
fn load(
|
||||
cache: assets::AnyCache,
|
||||
|
@ -335,7 +335,10 @@ pub fn handle_skating(data: &JoinData, update: &mut StateUpdate) {
|
||||
footwear = data.inventory.and_then(|inv| {
|
||||
inv.equipped(EquipSlot::Armor(ArmorSlot::Feet))
|
||||
.map(|armor| match armor.kind().as_ref() {
|
||||
ItemKind::Armor(a) => a.stats(data.msm).ground_contact,
|
||||
ItemKind::Armor(a) => {
|
||||
a.stats(data.msm, armor.stats_durability_multiplier())
|
||||
.ground_contact
|
||||
},
|
||||
_ => Friction::Normal,
|
||||
})
|
||||
});
|
||||
@ -719,7 +722,10 @@ pub fn attempt_wield(data: &JoinData<'_>, update: &mut StateUpdate) {
|
||||
data.inventory
|
||||
.and_then(|inv| inv.equipped(equip_slot))
|
||||
.and_then(|item| match &*item.kind() {
|
||||
ItemKind::Tool(tool) => Some(tool.equip_time()),
|
||||
ItemKind::Tool(tool) => Some(Duration::from_secs_f32(
|
||||
tool.stats(item.stats_durability_multiplier())
|
||||
.equip_time_secs,
|
||||
)),
|
||||
_ => None,
|
||||
})
|
||||
};
|
||||
@ -1261,7 +1267,7 @@ pub fn get_crit_data(data: &JoinData<'_>, ai: AbilityInfo) -> (f32, f32) {
|
||||
.and_then(|slot| data.inventory.and_then(|inv| inv.equipped(slot)))
|
||||
.and_then(|item| {
|
||||
if let ItemKind::Tool(tool) = &*item.kind() {
|
||||
Some(tool.base_crit_chance())
|
||||
Some(tool.stats(item.stats_durability_multiplier()).crit_chance)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -1282,7 +1288,7 @@ pub fn get_tool_stats(data: &JoinData<'_>, ai: AbilityInfo) -> tool::Stats {
|
||||
.and_then(|slot| data.inventory.and_then(|inv| inv.equipped(slot)))
|
||||
.and_then(|item| {
|
||||
if let ItemKind::Tool(tool) = &*item.kind() {
|
||||
Some(tool.stats)
|
||||
Some(tool.stats(item.stats_durability_multiplier()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -291,6 +291,7 @@ impl Block {
|
||||
| SpriteKind::Loom
|
||||
| SpriteKind::SpinningWheel
|
||||
| SpriteKind::DismantlingBench
|
||||
| SpriteKind::RepairBench
|
||||
| SpriteKind::TanningRack
|
||||
| SpriteKind::Chest
|
||||
| SpriteKind::DungeonChest0
|
||||
|
@ -240,6 +240,7 @@ make_case_elim!(
|
||||
Keyhole = 0xD7,
|
||||
KeyDoor = 0xD8,
|
||||
CommonLockedChest = 0xD9,
|
||||
RepairBench = 0xDA,
|
||||
}
|
||||
);
|
||||
|
||||
@ -308,6 +309,7 @@ impl SpriteKind {
|
||||
SpriteKind::CookingPot => 1.36,
|
||||
SpriteKind::DismantlingBench => 1.18,
|
||||
SpriteKind::IceSpike => 1.0,
|
||||
SpriteKind::RepairBench => 1.2,
|
||||
// TODO: Find suitable heights.
|
||||
SpriteKind::BarrelCactus
|
||||
| SpriteKind::RoundCactus
|
||||
@ -581,6 +583,7 @@ impl SpriteKind {
|
||||
| SpriteKind::TanningRack
|
||||
| SpriteKind::Loom
|
||||
| SpriteKind::DismantlingBench
|
||||
| SpriteKind::RepairBench
|
||||
| SpriteKind::ChristmasOrnament
|
||||
| SpriteKind::ChristmasWreath
|
||||
| SpriteKind::WindowArabic
|
||||
|
@ -18,7 +18,7 @@ use common::{
|
||||
comp::{
|
||||
self, aura, buff,
|
||||
chat::{KillSource, KillType},
|
||||
inventory::item::MaterialStatManifest,
|
||||
inventory::item::{AbilityMap, MaterialStatManifest},
|
||||
loot_owner::LootOwnerKind,
|
||||
Alignment, Auras, Body, CharacterState, Energy, Group, Health, HealthChange, Inventory,
|
||||
Player, Poise, Pos, SkillSet, Stats,
|
||||
@ -511,6 +511,13 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
|
||||
true
|
||||
};
|
||||
|
||||
// Modify durability on all equipped items
|
||||
if let Some(mut inventory) = state.ecs().write_storage::<Inventory>().get_mut(entity) {
|
||||
let ability_map = state.ecs().read_resource::<AbilityMap>();
|
||||
let msm = state.ecs().read_resource::<MaterialStatManifest>();
|
||||
inventory.damage_items(&ability_map, &msm);
|
||||
}
|
||||
|
||||
if should_delete {
|
||||
if let Some(rtsim_entity) = state
|
||||
.ecs()
|
||||
|
@ -12,7 +12,9 @@ use common::{
|
||||
slot::{self, Slot},
|
||||
},
|
||||
consts::MAX_PICKUP_RANGE,
|
||||
recipe::{self, default_component_recipe_book, default_recipe_book},
|
||||
recipe::{
|
||||
self, default_component_recipe_book, default_recipe_book, default_repair_recipe_book,
|
||||
},
|
||||
terrain::{Block, SpriteKind},
|
||||
trade::Trades,
|
||||
uid::Uid,
|
||||
@ -856,6 +858,20 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
||||
None
|
||||
}
|
||||
},
|
||||
CraftEvent::Repair { item, slots } => {
|
||||
let repair_recipes = default_repair_recipe_book().read();
|
||||
let sprite = get_craft_sprite(state, craft_sprite);
|
||||
if matches!(sprite, Some(SpriteKind::RepairBench)) {
|
||||
let _ = repair_recipes.repair_item(
|
||||
&mut inventory,
|
||||
item,
|
||||
slots,
|
||||
&state.ecs().read_resource::<AbilityMap>(),
|
||||
&state.ecs().read_resource::<item::MaterialStatManifest>(),
|
||||
);
|
||||
}
|
||||
None
|
||||
},
|
||||
};
|
||||
|
||||
// Attempt to insert items into inventory, dropping them if there is not enough
|
||||
|
1
server/src/migrations/V51__item_durability.sql
Normal file
1
server/src/migrations/V51__item_durability.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE item ADD properties TEXT NOT NULL DEFAULT '{}';
|
@ -70,13 +70,15 @@ pub fn load_items(connection: &Connection, root: i64) -> Result<Vec<Item>, Persi
|
||||
parent_container_item_id,
|
||||
item_definition_id,
|
||||
stack_size,
|
||||
position
|
||||
position,
|
||||
properties
|
||||
) AS (
|
||||
SELECT item_id,
|
||||
parent_container_item_id,
|
||||
item_definition_id,
|
||||
stack_size,
|
||||
position
|
||||
position,
|
||||
properties
|
||||
FROM item
|
||||
WHERE parent_container_item_id = ?1
|
||||
UNION ALL
|
||||
@ -84,7 +86,8 @@ pub fn load_items(connection: &Connection, root: i64) -> Result<Vec<Item>, Persi
|
||||
item.parent_container_item_id,
|
||||
item.item_definition_id,
|
||||
item.stack_size,
|
||||
item.position
|
||||
item.position,
|
||||
item.properties
|
||||
FROM item, items_tree
|
||||
WHERE item.parent_container_item_id = items_tree.item_id
|
||||
)
|
||||
@ -100,6 +103,7 @@ pub fn load_items(connection: &Connection, root: i64) -> Result<Vec<Item>, Persi
|
||||
item_definition_id: row.get(2)?,
|
||||
stack_size: row.get(3)?,
|
||||
position: row.get(4)?,
|
||||
properties: row.get(5)?,
|
||||
})
|
||||
})?
|
||||
.filter_map(Result::ok)
|
||||
@ -390,6 +394,7 @@ pub fn create_character(
|
||||
parent_container_item_id: WORLD_PSEUDO_CONTAINER_ID,
|
||||
item_definition_id: CHARACTER_PSEUDO_CONTAINER_DEF_ID.to_owned(),
|
||||
position: character_id.to_string(),
|
||||
properties: String::new(),
|
||||
},
|
||||
Item {
|
||||
stack_size: 1,
|
||||
@ -397,6 +402,7 @@ pub fn create_character(
|
||||
parent_container_item_id: character_id,
|
||||
item_definition_id: INVENTORY_PSEUDO_CONTAINER_DEF_ID.to_owned(),
|
||||
position: INVENTORY_PSEUDO_CONTAINER_POSITION.to_owned(),
|
||||
properties: String::new(),
|
||||
},
|
||||
Item {
|
||||
stack_size: 1,
|
||||
@ -404,6 +410,7 @@ pub fn create_character(
|
||||
parent_container_item_id: character_id,
|
||||
item_definition_id: LOADOUT_PSEUDO_CONTAINER_DEF_ID.to_owned(),
|
||||
position: LOADOUT_PSEUDO_CONTAINER_POSITION.to_owned(),
|
||||
properties: String::new(),
|
||||
},
|
||||
];
|
||||
|
||||
@ -413,8 +420,9 @@ pub fn create_character(
|
||||
parent_container_item_id,
|
||||
item_definition_id,
|
||||
stack_size,
|
||||
position)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
position,
|
||||
properties)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
)?;
|
||||
|
||||
for pseudo_container in pseudo_containers {
|
||||
@ -424,6 +432,7 @@ pub fn create_character(
|
||||
&pseudo_container.item_definition_id,
|
||||
&pseudo_container.stack_size,
|
||||
&pseudo_container.position,
|
||||
&pseudo_container.properties,
|
||||
])?;
|
||||
}
|
||||
drop(stmt);
|
||||
@ -521,8 +530,9 @@ pub fn create_character(
|
||||
parent_container_item_id,
|
||||
item_definition_id,
|
||||
stack_size,
|
||||
position)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
position,
|
||||
properties)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
)?;
|
||||
|
||||
for item in inserts {
|
||||
@ -532,6 +542,7 @@ pub fn create_character(
|
||||
&item.model.item_definition_id,
|
||||
&item.model.stack_size,
|
||||
&item.model.position,
|
||||
&item.model.properties,
|
||||
])?;
|
||||
}
|
||||
drop(stmt);
|
||||
@ -1048,8 +1059,9 @@ pub fn update(
|
||||
parent_container_item_id,
|
||||
item_definition_id,
|
||||
stack_size,
|
||||
position)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
position,
|
||||
properties)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
)?;
|
||||
|
||||
for item in upserted_items.iter() {
|
||||
@ -1059,6 +1071,7 @@ pub fn update(
|
||||
&item.item_definition_id,
|
||||
&item.stack_size,
|
||||
&item.position,
|
||||
&item.properties,
|
||||
])?;
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,10 @@ use crate::persistence::{
|
||||
|
||||
use crate::persistence::{
|
||||
error::PersistenceError,
|
||||
json_models::{self, CharacterPosition, DatabaseAbilitySet, GenericBody, HumanoidBody},
|
||||
json_models::{
|
||||
self, CharacterPosition, DatabaseAbilitySet, DatabaseItemProperties, GenericBody,
|
||||
HumanoidBody,
|
||||
},
|
||||
};
|
||||
use common::{
|
||||
character::CharacterId,
|
||||
@ -165,6 +168,8 @@ pub fn convert_items_to_database_items(
|
||||
bfs_queue.push_back((format!("component_{}", i), Some(component), item_id));
|
||||
}
|
||||
|
||||
let item_properties = json_models::item_properties_to_db_model(item);
|
||||
|
||||
let upsert = ItemModelPair {
|
||||
model: Item {
|
||||
item_definition_id: String::from(item.persistence_item_id()),
|
||||
@ -176,6 +181,8 @@ pub fn convert_items_to_database_items(
|
||||
} else {
|
||||
1
|
||||
},
|
||||
properties: serde_json::to_string(&item_properties)
|
||||
.expect("Failed to convert item properties to a json string."),
|
||||
},
|
||||
// Continue to remember the atomic, in case we detect an error later and want
|
||||
// to roll back to preserve liveness.
|
||||
@ -359,6 +366,9 @@ pub fn convert_inventory_from_database_items(
|
||||
item_indices.insert(db_item.item_id, i);
|
||||
|
||||
let mut item = get_item_from_asset(db_item.item_definition_id.as_str())?;
|
||||
let item_properties =
|
||||
serde_json::de::from_str::<DatabaseItemProperties>(&db_item.properties)?;
|
||||
json_models::apply_db_item_properties(&mut item, &item_properties);
|
||||
|
||||
// NOTE: Since this is freshly loaded, the atomic is *unique.*
|
||||
let comp = item.get_item_id_for_database();
|
||||
@ -459,7 +469,10 @@ pub fn convert_loadout_from_database_items(
|
||||
for (i, db_item) in database_items.iter().enumerate() {
|
||||
item_indices.insert(db_item.item_id, i);
|
||||
|
||||
let item = get_item_from_asset(db_item.item_definition_id.as_str())?;
|
||||
let mut item = get_item_from_asset(db_item.item_definition_id.as_str())?;
|
||||
let item_properties =
|
||||
serde_json::de::from_str::<DatabaseItemProperties>(&db_item.properties)?;
|
||||
json_models::apply_db_item_properties(&mut item, &item_properties);
|
||||
|
||||
// NOTE: item id is currently *unique*, so we can store the ID safely.
|
||||
let comp = item.get_item_id_for_database();
|
||||
|
@ -2,7 +2,7 @@ use common::comp;
|
||||
use common_base::dev_panic;
|
||||
use hashbrown::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::string::ToString;
|
||||
use std::{num::NonZeroU32, string::ToString};
|
||||
use vek::{Vec2, Vec3};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@ -285,3 +285,36 @@ pub fn active_abilities_from_db_model(
|
||||
.collect::<HashMap<_, _>>();
|
||||
comp::ability::ActiveAbilities::new(ability_sets)
|
||||
}
|
||||
|
||||
/// Struct containing item properties in the format that they get persisted to
|
||||
/// the database. Adding new fields is generally safe as long as they are
|
||||
/// optional. Renaming or removing old fields will require a migration.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct DatabaseItemProperties {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
durability: Option<NonZeroU32>,
|
||||
}
|
||||
|
||||
pub fn item_properties_to_db_model(item: &comp::Item) -> DatabaseItemProperties {
|
||||
DatabaseItemProperties {
|
||||
durability: item.persistence_durability(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_db_item_properties(item: &mut comp::Item, properties: &DatabaseItemProperties) {
|
||||
let DatabaseItemProperties { durability } = properties;
|
||||
item.persistence_set_durability(*durability);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
#[test]
|
||||
fn test_default_item_properties() {
|
||||
use super::DatabaseItemProperties;
|
||||
const DEFAULT_ITEM_PROPERTIES: &str = "{}";
|
||||
let _ = serde_json::de::from_str::<DatabaseItemProperties>(DEFAULT_ITEM_PROPERTIES).expect(
|
||||
"Default value should always load to ensure that changes to item properties is always \
|
||||
forward compatible with migration V50.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ pub struct Item {
|
||||
pub item_definition_id: String,
|
||||
pub stack_size: i32,
|
||||
pub position: String,
|
||||
pub properties: String,
|
||||
}
|
||||
|
||||
pub struct Body {
|
||||
|
@ -8,7 +8,7 @@ use crate::{
|
||||
use common::{
|
||||
comp::{self, Admin, Player, Stats},
|
||||
event::{EventBus, ServerEvent},
|
||||
recipe::{default_component_recipe_book, default_recipe_book},
|
||||
recipe::{default_component_recipe_book, default_recipe_book, default_repair_recipe_book},
|
||||
resources::TimeOfDay,
|
||||
shared_server_config::ServerConstants,
|
||||
uid::{Uid, UidAllocator},
|
||||
@ -348,6 +348,7 @@ impl<'a> System<'a> for Sys {
|
||||
world_map: (*read_data.map).clone(),
|
||||
recipe_book: default_recipe_book().cloned(),
|
||||
component_recipe_book: default_component_recipe_book().cloned(),
|
||||
repair_recipe_book: default_repair_recipe_book().cloned(),
|
||||
material_stats: (*read_data.material_stats).clone(),
|
||||
ability_map: (*read_data.ability_map).clone(),
|
||||
server_constants: ServerConstants {
|
||||
|
@ -22,7 +22,7 @@ use common::{
|
||||
Item, ItemBase, ItemDef, ItemDesc, ItemKind, ItemTag, MaterialStatManifest, Quality,
|
||||
TagExampleInfo,
|
||||
},
|
||||
slot::InvSlotId,
|
||||
slot::{InvSlotId, Slot},
|
||||
Inventory,
|
||||
},
|
||||
recipe::{ComponentKey, Recipe, RecipeInput},
|
||||
@ -86,8 +86,8 @@ widget_ids! {
|
||||
dismantle_title,
|
||||
dismantle_img,
|
||||
dismantle_txt,
|
||||
dismantle_highlight_txt,
|
||||
modular_inputs[],
|
||||
repair_buttons[],
|
||||
craft_slots[],
|
||||
modular_art,
|
||||
modular_desc_txt,
|
||||
modular_wep_empty_bg,
|
||||
@ -115,6 +115,9 @@ pub enum Event {
|
||||
Focus(widget::Id),
|
||||
SearchRecipe(Option<String>),
|
||||
ClearRecipeInputs,
|
||||
RepairItem {
|
||||
slot: Slot,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct CraftingShow {
|
||||
@ -122,8 +125,9 @@ pub struct CraftingShow {
|
||||
pub crafting_search_key: Option<String>,
|
||||
pub craft_sprite: Option<(Vec3<i32>, SpriteKind)>,
|
||||
pub salvage: bool,
|
||||
pub initialize_repair: bool,
|
||||
// TODO: Maybe try to do something that doesn't need to allocate?
|
||||
pub recipe_inputs: HashMap<u32, InvSlotId>,
|
||||
pub recipe_inputs: HashMap<u32, Slot>,
|
||||
}
|
||||
|
||||
impl Default for CraftingShow {
|
||||
@ -133,6 +137,7 @@ impl Default for CraftingShow {
|
||||
crafting_search_key: None,
|
||||
craft_sprite: None,
|
||||
salvage: false,
|
||||
initialize_repair: false,
|
||||
recipe_inputs: HashMap::new(),
|
||||
}
|
||||
}
|
||||
@ -207,7 +212,7 @@ pub enum CraftingTab {
|
||||
Bag,
|
||||
Utility,
|
||||
Glider,
|
||||
Dismantle, // Needs to be the last one or widget alignment will be messed up
|
||||
Dismantle,
|
||||
}
|
||||
|
||||
impl CraftingTab {
|
||||
@ -239,7 +244,8 @@ impl CraftingTab {
|
||||
CraftingTab::Weapon => imgs.icon_weapon,
|
||||
CraftingTab::Bag => imgs.icon_bag,
|
||||
CraftingTab::ProcessedMaterial => imgs.icon_processed_material,
|
||||
CraftingTab::Dismantle => imgs.icon_dismantle,
|
||||
// These tabs are never shown, so using not found is fine
|
||||
CraftingTab::Dismantle => imgs.not_found,
|
||||
}
|
||||
}
|
||||
|
||||
@ -271,6 +277,10 @@ impl CraftingTab {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Tells UI whether tab is an adhoc tab that should only sometimes be present
|
||||
// depending on what station is accessed
|
||||
fn is_adhoc(self) -> bool { matches!(self, CraftingTab::Dismantle) }
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
@ -313,6 +323,16 @@ impl<'a> Widget for Crafting<'a> {
|
||||
|
||||
let mut events = Vec::new();
|
||||
|
||||
// Handle any initialization
|
||||
// TODO: Replace with struct instead of making assorted booleans once there is
|
||||
// more than 1 field.
|
||||
if self.show.crafting_fields.initialize_repair {
|
||||
state.update(|s| {
|
||||
s.selected_recipe = Some(String::from("veloren.core.pseudo_recipe.repair"))
|
||||
});
|
||||
}
|
||||
self.show.crafting_fields.initialize_repair = false;
|
||||
|
||||
// Tooltips
|
||||
let item_tooltip = ItemTooltip::new(
|
||||
{
|
||||
@ -434,56 +454,57 @@ impl<'a> Widget for Crafting<'a> {
|
||||
})
|
||||
};
|
||||
let sel_crafting_tab = &self.show.crafting_fields.crafting_tab;
|
||||
for (i, crafting_tab) in CraftingTab::iter().enumerate() {
|
||||
if crafting_tab != CraftingTab::Dismantle {
|
||||
let tab_img = crafting_tab.img_id(self.imgs);
|
||||
// Button Background
|
||||
let mut bg = Image::new(self.imgs.pixel)
|
||||
.w_h(40.0, 30.0)
|
||||
.color(Some(UI_MAIN));
|
||||
if i == 0 {
|
||||
bg = bg.top_left_with_margins_on(state.ids.window_frame, 50.0, -40.0)
|
||||
} else {
|
||||
bg = bg.down_from(state.ids.category_bgs[i - 1], 0.0)
|
||||
};
|
||||
bg.set(state.ids.category_bgs[i], ui);
|
||||
// Category Button
|
||||
if Button::image(if crafting_tab == *sel_crafting_tab {
|
||||
self.imgs.wpn_icon_border_pressed
|
||||
} else {
|
||||
self.imgs.wpn_icon_border
|
||||
})
|
||||
.wh_of(state.ids.category_bgs[i])
|
||||
.middle_of(state.ids.category_bgs[i])
|
||||
.hover_image(if crafting_tab == *sel_crafting_tab {
|
||||
self.imgs.wpn_icon_border_pressed
|
||||
} else {
|
||||
self.imgs.wpn_icon_border_mo
|
||||
})
|
||||
.press_image(if crafting_tab == *sel_crafting_tab {
|
||||
self.imgs.wpn_icon_border_pressed
|
||||
} else {
|
||||
self.imgs.wpn_icon_border_press
|
||||
})
|
||||
.with_tooltip(
|
||||
self.tooltip_manager,
|
||||
&self.localized_strings.get_msg(crafting_tab.name_key()),
|
||||
"",
|
||||
&tabs_tooltip,
|
||||
TEXT_COLOR,
|
||||
)
|
||||
.set(state.ids.category_tabs[i], ui)
|
||||
.was_clicked()
|
||||
{
|
||||
events.push(Event::ChangeCraftingTab(crafting_tab))
|
||||
};
|
||||
// Tab images
|
||||
Image::new(tab_img)
|
||||
.middle_of(state.ids.category_tabs[i])
|
||||
.w_h(20.0, 20.0)
|
||||
.graphics_for(state.ids.category_tabs[i])
|
||||
.set(state.ids.category_imgs[i], ui);
|
||||
}
|
||||
for (i, crafting_tab) in CraftingTab::iter()
|
||||
.filter(|tab| !tab.is_adhoc())
|
||||
.enumerate()
|
||||
{
|
||||
let tab_img = crafting_tab.img_id(self.imgs);
|
||||
// Button Background
|
||||
let mut bg = Image::new(self.imgs.pixel)
|
||||
.w_h(40.0, 30.0)
|
||||
.color(Some(UI_MAIN));
|
||||
if i == 0 {
|
||||
bg = bg.top_left_with_margins_on(state.ids.window_frame, 50.0, -40.0)
|
||||
} else {
|
||||
bg = bg.down_from(state.ids.category_bgs[i - 1], 0.0)
|
||||
};
|
||||
bg.set(state.ids.category_bgs[i], ui);
|
||||
// Category Button
|
||||
if Button::image(if crafting_tab == *sel_crafting_tab {
|
||||
self.imgs.wpn_icon_border_pressed
|
||||
} else {
|
||||
self.imgs.wpn_icon_border
|
||||
})
|
||||
.wh_of(state.ids.category_bgs[i])
|
||||
.middle_of(state.ids.category_bgs[i])
|
||||
.hover_image(if crafting_tab == *sel_crafting_tab {
|
||||
self.imgs.wpn_icon_border_pressed
|
||||
} else {
|
||||
self.imgs.wpn_icon_border_mo
|
||||
})
|
||||
.press_image(if crafting_tab == *sel_crafting_tab {
|
||||
self.imgs.wpn_icon_border_pressed
|
||||
} else {
|
||||
self.imgs.wpn_icon_border_press
|
||||
})
|
||||
.with_tooltip(
|
||||
self.tooltip_manager,
|
||||
&self.localized_strings.get_msg(crafting_tab.name_key()),
|
||||
"",
|
||||
&tabs_tooltip,
|
||||
TEXT_COLOR,
|
||||
)
|
||||
.set(state.ids.category_tabs[i], ui)
|
||||
.was_clicked()
|
||||
{
|
||||
events.push(Event::ChangeCraftingTab(crafting_tab))
|
||||
};
|
||||
// Tab images
|
||||
Image::new(tab_img)
|
||||
.middle_of(state.ids.category_tabs[i])
|
||||
.w_h(20.0, 20.0)
|
||||
.graphics_for(state.ids.category_tabs[i])
|
||||
.set(state.ids.category_imgs[i], ui);
|
||||
}
|
||||
|
||||
// TODO: Consider UX for filtering searches, maybe a checkbox or a dropdown if
|
||||
@ -521,40 +542,45 @@ impl<'a> Widget for Crafting<'a> {
|
||||
let weapon_recipe = make_pseudo_recipe(SpriteKind::CraftingBench);
|
||||
let metal_comp_recipe = make_pseudo_recipe(SpriteKind::Anvil);
|
||||
let wood_comp_recipe = make_pseudo_recipe(SpriteKind::CraftingBench);
|
||||
let modular_entries = {
|
||||
let repair_recipe = make_pseudo_recipe(SpriteKind::RepairBench);
|
||||
let pseudo_entries = {
|
||||
// A BTreeMap is used over a HashMap as when a HashMap is used, the UI shuffles
|
||||
// the positions of these every tick, so a BTreeMap is necessary to keep it
|
||||
// ordered.
|
||||
let mut modular_entries = BTreeMap::new();
|
||||
modular_entries.insert(
|
||||
let mut pseudo_entries = BTreeMap::new();
|
||||
pseudo_entries.insert(
|
||||
String::from("veloren.core.pseudo_recipe.modular_weapon"),
|
||||
(&weapon_recipe, "Modular Weapon"),
|
||||
(&weapon_recipe, "Modular Weapon", CraftingTab::Weapon),
|
||||
);
|
||||
modular_entries.insert(
|
||||
pseudo_entries.insert(
|
||||
String::from("veloren.core.pseudo_recipe.modular_weapon_component.sword"),
|
||||
(&metal_comp_recipe, "Sword Blade"),
|
||||
(&metal_comp_recipe, "Sword Blade", CraftingTab::Weapon),
|
||||
);
|
||||
modular_entries.insert(
|
||||
pseudo_entries.insert(
|
||||
String::from("veloren.core.pseudo_recipe.modular_weapon_component.axe"),
|
||||
(&metal_comp_recipe, "Axe Head"),
|
||||
(&metal_comp_recipe, "Axe Head", CraftingTab::Weapon),
|
||||
);
|
||||
modular_entries.insert(
|
||||
pseudo_entries.insert(
|
||||
String::from("veloren.core.pseudo_recipe.modular_weapon_component.hammer"),
|
||||
(&metal_comp_recipe, "Hammer Head"),
|
||||
(&metal_comp_recipe, "Hammer Head", CraftingTab::Weapon),
|
||||
);
|
||||
modular_entries.insert(
|
||||
pseudo_entries.insert(
|
||||
String::from("veloren.core.pseudo_recipe.modular_weapon_component.bow"),
|
||||
(&wood_comp_recipe, "Bow Limbs"),
|
||||
(&wood_comp_recipe, "Bow Limbs", CraftingTab::Weapon),
|
||||
);
|
||||
modular_entries.insert(
|
||||
pseudo_entries.insert(
|
||||
String::from("veloren.core.pseudo_recipe.modular_weapon_component.staff"),
|
||||
(&wood_comp_recipe, "Staff Shaft"),
|
||||
(&wood_comp_recipe, "Staff Shaft", CraftingTab::Weapon),
|
||||
);
|
||||
modular_entries.insert(
|
||||
pseudo_entries.insert(
|
||||
String::from("veloren.core.pseudo_recipe.modular_weapon_component.sceptre"),
|
||||
(&wood_comp_recipe, "Sceptre Shaft"),
|
||||
(&wood_comp_recipe, "Sceptre Shaft", CraftingTab::Weapon),
|
||||
);
|
||||
modular_entries
|
||||
pseudo_entries.insert(
|
||||
String::from("veloren.core.pseudo_recipe.repair"),
|
||||
(&repair_recipe, "Repair Equipment", CraftingTab::All),
|
||||
);
|
||||
pseudo_entries
|
||||
};
|
||||
|
||||
// First available recipes, then ones with available materials,
|
||||
@ -605,36 +631,34 @@ impl<'a> Widget for Crafting<'a> {
|
||||
(name, recipe, is_craftable, has_materials)
|
||||
})
|
||||
.chain(
|
||||
matches!(sel_crafting_tab, CraftingTab::Weapon | CraftingTab::All)
|
||||
.then_some(
|
||||
modular_entries
|
||||
.iter()
|
||||
.filter(|(_, (_, output_name))| {
|
||||
match search_filter {
|
||||
SearchFilter::None => {
|
||||
let output_name = output_name.to_lowercase();
|
||||
search_keys
|
||||
.iter()
|
||||
.all(|&substring| output_name.contains(substring))
|
||||
},
|
||||
// TODO: Get input filtering to work here, probably requires
|
||||
// checking component recipe book?
|
||||
SearchFilter::Input => false,
|
||||
SearchFilter::Nonexistent => false,
|
||||
}
|
||||
})
|
||||
.map(|(recipe_name, (recipe, _))| {
|
||||
(
|
||||
recipe_name,
|
||||
*recipe,
|
||||
self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
|
||||
== recipe.craft_sprite,
|
||||
true,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
pseudo_entries
|
||||
.iter()
|
||||
// Filter by selected tab
|
||||
.filter(|(_, (_, _, tab))| *sel_crafting_tab == CraftingTab::All || sel_crafting_tab == tab)
|
||||
// Filter by search filter
|
||||
.filter(|(_, (_, output_name, _))| {
|
||||
match search_filter {
|
||||
SearchFilter::None => {
|
||||
let output_name = output_name.to_lowercase();
|
||||
search_keys
|
||||
.iter()
|
||||
.all(|&substring| output_name.contains(substring))
|
||||
},
|
||||
// TODO: Get input filtering to work here, probably requires
|
||||
// checking component recipe book?
|
||||
SearchFilter::Input => false,
|
||||
SearchFilter::Nonexistent => false,
|
||||
}
|
||||
})
|
||||
.map(|(recipe_name, (recipe, _, _))| {
|
||||
(
|
||||
recipe_name,
|
||||
*recipe,
|
||||
self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
|
||||
== recipe.craft_sprite,
|
||||
true,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.collect();
|
||||
ordered_recipes.sort_by_key(|(_, recipe, is_craftable, has_materials)| {
|
||||
@ -647,7 +671,7 @@ impl<'a> Widget for Crafting<'a> {
|
||||
});
|
||||
|
||||
// Recipe list
|
||||
let recipe_list_length = self.client.recipe_book().iter().len() + modular_entries.len();
|
||||
let recipe_list_length = self.client.recipe_book().iter().len() + pseudo_entries.len();
|
||||
if state.ids.recipe_list_btns.len() < recipe_list_length {
|
||||
state.update(|state| {
|
||||
state
|
||||
@ -702,11 +726,12 @@ impl<'a> Widget for Crafting<'a> {
|
||||
.press_image(self.imgs.selection_press)
|
||||
.image_color(color::rgba(1.0, 0.82, 0.27, 1.0));
|
||||
|
||||
let recipe_name = if let Some((_recipe, modular_name)) = modular_entries.get(name) {
|
||||
*modular_name
|
||||
} else {
|
||||
&recipe.output.0.name
|
||||
};
|
||||
let recipe_name =
|
||||
if let Some((_recipe, pseudo_name, _filter_tab)) = pseudo_entries.get(name) {
|
||||
*pseudo_name
|
||||
} else {
|
||||
&recipe.output.0.name
|
||||
};
|
||||
|
||||
let text = Text::new(recipe_name)
|
||||
.color(if is_craftable {
|
||||
@ -735,12 +760,9 @@ impl<'a> Widget for Crafting<'a> {
|
||||
if state.selected_recipe.as_ref() == Some(name) {
|
||||
state.update(|s| s.selected_recipe = None);
|
||||
} else {
|
||||
if matches!(
|
||||
self.show.crafting_fields.crafting_tab,
|
||||
CraftingTab::Dismantle
|
||||
) {
|
||||
// If current tab is dismantle, and recipe is selected, change to general
|
||||
// tab, as in dismantle tab recipe gets deselected
|
||||
if self.show.crafting_fields.crafting_tab.is_adhoc() {
|
||||
// If current tab is an adhoc tab, and recipe is selected, change to general
|
||||
// tab
|
||||
events.push(Event::ChangeCraftingTab(CraftingTab::All));
|
||||
}
|
||||
state.update(|s| s.selected_recipe = Some(name.clone()));
|
||||
@ -802,19 +824,17 @@ impl<'a> Widget for Crafting<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// Deselect recipe if current tab is dismantle, elsewhere if recipe selected
|
||||
// while dismantling, tab is changed to general
|
||||
if matches!(
|
||||
self.show.crafting_fields.crafting_tab,
|
||||
CraftingTab::Dismantle
|
||||
) {
|
||||
// Deselect recipe if current tab is an adhoc tab, elsewhere if recipe selected
|
||||
// while in an adhoc tab, tab is changed to general
|
||||
if self.show.crafting_fields.crafting_tab.is_adhoc() {
|
||||
state.update(|s| s.selected_recipe = None);
|
||||
}
|
||||
|
||||
// Selected Recipe
|
||||
if let Some((recipe_name, recipe)) = match state.selected_recipe.as_deref() {
|
||||
Some(selected_recipe) => {
|
||||
if let Some((modular_recipe, _modular_name)) = modular_entries.get(selected_recipe)
|
||||
if let Some((modular_recipe, _pseudo_name, _filter_tab)) =
|
||||
pseudo_entries.get(selected_recipe)
|
||||
{
|
||||
Some((selected_recipe, *modular_recipe))
|
||||
} else {
|
||||
@ -827,8 +847,10 @@ impl<'a> Widget for Crafting<'a> {
|
||||
None => None,
|
||||
} {
|
||||
let recipe_name = String::from(recipe_name);
|
||||
let title = if let Some((_recipe, modular_name)) = modular_entries.get(&recipe_name) {
|
||||
*modular_name
|
||||
let title = if let Some((_recipe, pseudo_name, _filter_tab)) =
|
||||
pseudo_entries.get(&recipe_name)
|
||||
{
|
||||
*pseudo_name
|
||||
} else {
|
||||
&recipe.output.0.name
|
||||
};
|
||||
@ -846,6 +868,7 @@ impl<'a> Widget for Crafting<'a> {
|
||||
ModularWeapon,
|
||||
Component(ToolKind),
|
||||
Simple,
|
||||
Repair,
|
||||
}
|
||||
|
||||
let recipe_kind = match recipe_name.as_str() {
|
||||
@ -868,37 +891,36 @@ impl<'a> Widget for Crafting<'a> {
|
||||
"veloren.core.pseudo_recipe.modular_weapon_component.sceptre" => {
|
||||
RecipeKind::Component(ToolKind::Sceptre)
|
||||
},
|
||||
"veloren.core.pseudo_recipe.repair" => RecipeKind::Repair,
|
||||
_ => RecipeKind::Simple,
|
||||
};
|
||||
|
||||
// Output slot, tags, and modular input slots
|
||||
let (modular_primary_slot, modular_secondary_slot, can_perform) = match recipe_kind {
|
||||
RecipeKind::ModularWeapon | RecipeKind::Component(_) => {
|
||||
let mut slot_maker = SlotMaker {
|
||||
empty_slot: self.imgs.inv_slot,
|
||||
filled_slot: self.imgs.inv_slot,
|
||||
selected_slot: self.imgs.inv_slot_sel,
|
||||
background_color: Some(UI_MAIN),
|
||||
content_size: ContentSize {
|
||||
width_height_ratio: 1.0,
|
||||
max_fraction: 0.75,
|
||||
},
|
||||
selected_content_scale: 1.067,
|
||||
amount_font: self.fonts.cyri.conrod_id,
|
||||
amount_margins: Vec2::new(-4.0, 0.0),
|
||||
amount_font_size: self.fonts.cyri.scale(12),
|
||||
amount_text_color: TEXT_COLOR,
|
||||
content_source: self.inventory,
|
||||
image_source: self.item_imgs,
|
||||
slot_manager: Some(self.slot_manager),
|
||||
pulse: self.pulse,
|
||||
};
|
||||
let mut slot_maker = SlotMaker {
|
||||
empty_slot: self.imgs.inv_slot,
|
||||
filled_slot: self.imgs.inv_slot,
|
||||
selected_slot: self.imgs.inv_slot_sel,
|
||||
background_color: Some(UI_MAIN),
|
||||
content_size: ContentSize {
|
||||
width_height_ratio: 1.0,
|
||||
max_fraction: 0.75,
|
||||
},
|
||||
selected_content_scale: 1.067,
|
||||
amount_font: self.fonts.cyri.conrod_id,
|
||||
amount_margins: Vec2::new(-4.0, 0.0),
|
||||
amount_font_size: self.fonts.cyri.scale(12),
|
||||
amount_text_color: TEXT_COLOR,
|
||||
content_source: self.inventory,
|
||||
image_source: self.item_imgs,
|
||||
slot_manager: Some(self.slot_manager),
|
||||
pulse: self.pulse,
|
||||
};
|
||||
|
||||
if state.ids.modular_inputs.len() < 2 {
|
||||
// Output slot, tags, and modular input slots
|
||||
let (craft_slot_1, craft_slot_2, can_perform) = match recipe_kind {
|
||||
RecipeKind::ModularWeapon | RecipeKind::Component(_) => {
|
||||
if state.ids.craft_slots.len() < 2 {
|
||||
state.update(|s| {
|
||||
s.ids
|
||||
.modular_inputs
|
||||
.resize(2, &mut ui.widget_id_generator());
|
||||
s.ids.craft_slots.resize(2, &mut ui.widget_id_generator());
|
||||
});
|
||||
}
|
||||
// Modular Weapon Crafting BG-Art
|
||||
@ -909,7 +931,7 @@ impl<'a> Widget for Crafting<'a> {
|
||||
|
||||
let primary_slot = CraftSlot {
|
||||
index: 0,
|
||||
invslot: self.show.crafting_fields.recipe_inputs.get(&0).copied(),
|
||||
slot: self.show.crafting_fields.recipe_inputs.get(&0).copied(),
|
||||
requirement: match recipe_kind {
|
||||
RecipeKind::ModularWeapon => |item, _, _| {
|
||||
matches!(
|
||||
@ -932,11 +954,13 @@ impl<'a> Widget for Crafting<'a> {
|
||||
false
|
||||
}
|
||||
},
|
||||
RecipeKind::Simple => |_, _, _| unreachable!(),
|
||||
RecipeKind::Simple | RecipeKind::Repair => |_, _, _| unreachable!(),
|
||||
},
|
||||
info: match recipe_kind {
|
||||
RecipeKind::Component(toolkind) => Some(CraftSlotInfo::Tool(toolkind)),
|
||||
RecipeKind::ModularWeapon | RecipeKind::Simple => None,
|
||||
RecipeKind::ModularWeapon | RecipeKind::Simple | RecipeKind::Repair => {
|
||||
None
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -945,10 +969,7 @@ impl<'a> Widget for Crafting<'a> {
|
||||
.top_left_with_margins_on(state.ids.modular_art, 4.0, 4.0)
|
||||
.parent(state.ids.align_ing);
|
||||
|
||||
if let Some(item) = primary_slot
|
||||
.invslot
|
||||
.and_then(|slot| self.inventory.get(slot))
|
||||
{
|
||||
if let Some(item) = primary_slot.item(self.inventory) {
|
||||
primary_slot_widget
|
||||
.with_item_tooltip(
|
||||
self.item_tooltip_manager,
|
||||
@ -956,7 +977,7 @@ impl<'a> Widget for Crafting<'a> {
|
||||
&None,
|
||||
&item_tooltip,
|
||||
)
|
||||
.set(state.ids.modular_inputs[0], ui);
|
||||
.set(state.ids.craft_slots[0], ui);
|
||||
} else {
|
||||
let (tooltip_title, tooltip_desc) = match recipe_kind {
|
||||
RecipeKind::ModularWeapon => (
|
||||
@ -981,7 +1002,7 @@ impl<'a> Widget for Crafting<'a> {
|
||||
self.localized_strings
|
||||
.get_msg("hud-crafting-mod_comp_wood_prim_slot_desc"),
|
||||
),
|
||||
RecipeKind::Component(_) | RecipeKind::Simple => {
|
||||
RecipeKind::Component(_) | RecipeKind::Simple | RecipeKind::Repair => {
|
||||
(Cow::Borrowed(""), Cow::Borrowed(""))
|
||||
},
|
||||
};
|
||||
@ -993,12 +1014,12 @@ impl<'a> Widget for Crafting<'a> {
|
||||
&tabs_tooltip,
|
||||
TEXT_COLOR,
|
||||
)
|
||||
.set(state.ids.modular_inputs[0], ui);
|
||||
.set(state.ids.craft_slots[0], ui);
|
||||
}
|
||||
|
||||
let secondary_slot = CraftSlot {
|
||||
index: 1,
|
||||
invslot: self.show.crafting_fields.recipe_inputs.get(&1).copied(),
|
||||
slot: self.show.crafting_fields.recipe_inputs.get(&1).copied(),
|
||||
requirement: match recipe_kind {
|
||||
RecipeKind::ModularWeapon => |item, _, _| {
|
||||
matches!(
|
||||
@ -1021,11 +1042,13 @@ impl<'a> Widget for Crafting<'a> {
|
||||
false
|
||||
}
|
||||
},
|
||||
RecipeKind::Simple => |_, _, _| unreachable!(),
|
||||
RecipeKind::Simple | RecipeKind::Repair => |_, _, _| unreachable!(),
|
||||
},
|
||||
info: match recipe_kind {
|
||||
RecipeKind::Component(toolkind) => Some(CraftSlotInfo::Tool(toolkind)),
|
||||
RecipeKind::ModularWeapon | RecipeKind::Simple => None,
|
||||
RecipeKind::ModularWeapon | RecipeKind::Simple | RecipeKind::Repair => {
|
||||
None
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -1034,10 +1057,7 @@ impl<'a> Widget for Crafting<'a> {
|
||||
.top_right_with_margins_on(state.ids.modular_art, 4.0, 4.0)
|
||||
.parent(state.ids.align_ing);
|
||||
|
||||
if let Some(item) = secondary_slot
|
||||
.invslot
|
||||
.and_then(|slot| self.inventory.get(slot))
|
||||
{
|
||||
if let Some(item) = secondary_slot.item(self.inventory) {
|
||||
secondary_slot_widget
|
||||
.with_item_tooltip(
|
||||
self.item_tooltip_manager,
|
||||
@ -1045,7 +1065,7 @@ impl<'a> Widget for Crafting<'a> {
|
||||
&None,
|
||||
&item_tooltip,
|
||||
)
|
||||
.set(state.ids.modular_inputs[1], ui);
|
||||
.set(state.ids.craft_slots[1], ui);
|
||||
} else {
|
||||
let (tooltip_title, tooltip_desc) = match recipe_kind {
|
||||
RecipeKind::ModularWeapon => (
|
||||
@ -1060,7 +1080,9 @@ impl<'a> Widget for Crafting<'a> {
|
||||
self.localized_strings
|
||||
.get_msg("hud-crafting-mod_comp_sec_slot_desc"),
|
||||
),
|
||||
RecipeKind::Simple => (Cow::Borrowed(""), Cow::Borrowed("")),
|
||||
RecipeKind::Simple | RecipeKind::Repair => {
|
||||
(Cow::Borrowed(""), Cow::Borrowed(""))
|
||||
},
|
||||
};
|
||||
secondary_slot_widget
|
||||
.with_tooltip(
|
||||
@ -1070,11 +1092,11 @@ impl<'a> Widget for Crafting<'a> {
|
||||
&tabs_tooltip,
|
||||
TEXT_COLOR,
|
||||
)
|
||||
.set(state.ids.modular_inputs[1], ui);
|
||||
.set(state.ids.craft_slots[1], ui);
|
||||
}
|
||||
|
||||
let prim_item_placed = primary_slot.invslot.is_some();
|
||||
let sec_item_placed = secondary_slot.invslot.is_some();
|
||||
let prim_item_placed = primary_slot.slot.is_some();
|
||||
let sec_item_placed = secondary_slot.slot.is_some();
|
||||
|
||||
let prim_icon = match recipe_kind {
|
||||
RecipeKind::ModularWeapon => self.imgs.icon_primary_comp,
|
||||
@ -1102,18 +1124,18 @@ impl<'a> Widget for Crafting<'a> {
|
||||
let bg_col = Color::Rgba(1.0, 1.0, 1.0, 0.4);
|
||||
if !prim_item_placed {
|
||||
Image::new(prim_icon)
|
||||
.middle_of(state.ids.modular_inputs[0])
|
||||
.middle_of(state.ids.craft_slots[0])
|
||||
.color(Some(bg_col))
|
||||
.w_h(34.0, 34.0)
|
||||
.graphics_for(state.ids.modular_inputs[0])
|
||||
.graphics_for(state.ids.craft_slots[0])
|
||||
.set(state.ids.modular_wep_ing_1_bg, ui);
|
||||
}
|
||||
if !sec_item_placed {
|
||||
Image::new(sec_icon)
|
||||
.middle_of(state.ids.modular_inputs[1])
|
||||
.middle_of(state.ids.craft_slots[1])
|
||||
.color(Some(bg_col))
|
||||
.w_h(50.0, 50.0)
|
||||
.graphics_for(state.ids.modular_inputs[1])
|
||||
.graphics_for(state.ids.craft_slots[1])
|
||||
.set(state.ids.modular_wep_ing_2_bg, ui);
|
||||
}
|
||||
|
||||
@ -1122,10 +1144,8 @@ impl<'a> Widget for Crafting<'a> {
|
||||
|
||||
let output_item = match recipe_kind {
|
||||
RecipeKind::ModularWeapon => {
|
||||
if let Some((primary_comp, toolkind, hand_restriction)) = primary_slot
|
||||
.invslot
|
||||
.and_then(|slot| self.inventory.get(slot))
|
||||
.and_then(|item| {
|
||||
if let Some((primary_comp, toolkind, hand_restriction)) =
|
||||
primary_slot.item(self.inventory).and_then(|item| {
|
||||
if let ItemKind::ModularComponent(
|
||||
ModularComponent::ToolPrimaryComponent {
|
||||
toolkind,
|
||||
@ -1141,8 +1161,7 @@ impl<'a> Widget for Crafting<'a> {
|
||||
})
|
||||
{
|
||||
secondary_slot
|
||||
.invslot
|
||||
.and_then(|slot| self.inventory.get(slot))
|
||||
.item(self.inventory)
|
||||
.filter(|item| {
|
||||
matches!(
|
||||
&*item.kind(),
|
||||
@ -1167,22 +1186,19 @@ impl<'a> Widget for Crafting<'a> {
|
||||
}
|
||||
},
|
||||
RecipeKind::Component(toolkind) => {
|
||||
if let Some(material) = primary_slot
|
||||
.invslot
|
||||
.and_then(|slot| self.inventory.get(slot))
|
||||
.and_then(|item| {
|
||||
if let Some(material) =
|
||||
primary_slot.item(self.inventory).and_then(|item| {
|
||||
item.item_definition_id().itemdef_id().map(String::from)
|
||||
})
|
||||
{
|
||||
let component_key = ComponentKey {
|
||||
toolkind,
|
||||
material,
|
||||
modifier: secondary_slot
|
||||
.invslot
|
||||
.and_then(|slot| self.inventory.get(slot))
|
||||
.and_then(|item| {
|
||||
modifier: secondary_slot.item(self.inventory).and_then(
|
||||
|item| {
|
||||
item.item_definition_id().itemdef_id().map(String::from)
|
||||
}),
|
||||
},
|
||||
),
|
||||
};
|
||||
self.client.component_recipe_book().get(&component_key).map(
|
||||
|component_recipe| {
|
||||
@ -1193,7 +1209,7 @@ impl<'a> Widget for Crafting<'a> {
|
||||
None
|
||||
}
|
||||
},
|
||||
RecipeKind::Simple => None,
|
||||
RecipeKind::Simple | RecipeKind::Repair => None,
|
||||
};
|
||||
|
||||
if let Some(output_item) = output_item {
|
||||
@ -1219,8 +1235,8 @@ impl<'a> Widget for Crafting<'a> {
|
||||
)
|
||||
.set(state.ids.output_img, ui);
|
||||
(
|
||||
primary_slot.invslot,
|
||||
secondary_slot.invslot,
|
||||
primary_slot.slot,
|
||||
secondary_slot.slot,
|
||||
self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
|
||||
== recipe.craft_sprite,
|
||||
)
|
||||
@ -1237,7 +1253,7 @@ impl<'a> Widget for Crafting<'a> {
|
||||
.w_h(70.0, 70.0)
|
||||
.graphics_for(state.ids.output_img)
|
||||
.set(state.ids.modular_wep_empty_bg, ui);
|
||||
(primary_slot.invslot, secondary_slot.invslot, false)
|
||||
(primary_slot.slot, secondary_slot.slot, false)
|
||||
}
|
||||
},
|
||||
RecipeKind::Simple => {
|
||||
@ -1342,6 +1358,140 @@ impl<'a> Widget for Crafting<'a> {
|
||||
}),
|
||||
)
|
||||
},
|
||||
RecipeKind::Repair => {
|
||||
if state.ids.craft_slots.len() < 1 {
|
||||
state.update(|s| {
|
||||
s.ids.craft_slots.resize(1, &mut ui.widget_id_generator());
|
||||
});
|
||||
}
|
||||
if state.ids.repair_buttons.len() < 2 {
|
||||
state.update(|s| {
|
||||
s.ids
|
||||
.repair_buttons
|
||||
.resize(2, &mut ui.widget_id_generator());
|
||||
});
|
||||
}
|
||||
|
||||
// Slot for item to be repaired
|
||||
let repair_slot = CraftSlot {
|
||||
index: 0,
|
||||
slot: self.show.crafting_fields.recipe_inputs.get(&0).copied(),
|
||||
requirement: |item, _, _| item.durability().map_or(false, |d| d > 0),
|
||||
info: None,
|
||||
};
|
||||
|
||||
let repair_slot_widget = slot_maker
|
||||
.fabricate(repair_slot, [40.0; 2])
|
||||
.top_left_with_margins_on(state.ids.align_ing, 20.0, 40.0)
|
||||
.parent(state.ids.align_ing);
|
||||
|
||||
if let Some(item) = repair_slot.item(self.inventory) {
|
||||
repair_slot_widget
|
||||
.with_item_tooltip(
|
||||
self.item_tooltip_manager,
|
||||
core::iter::once(item as &dyn ItemDesc),
|
||||
&None,
|
||||
&item_tooltip,
|
||||
)
|
||||
.set(state.ids.craft_slots[0], ui);
|
||||
} else {
|
||||
repair_slot_widget
|
||||
.with_tooltip(
|
||||
self.tooltip_manager,
|
||||
&self
|
||||
.localized_strings
|
||||
.get_msg("hud-crafting-repair_slot_title"),
|
||||
&self
|
||||
.localized_strings
|
||||
.get_msg("hud-crafting-repair_slot_desc"),
|
||||
&tabs_tooltip,
|
||||
TEXT_COLOR,
|
||||
)
|
||||
.set(state.ids.craft_slots[0], ui);
|
||||
}
|
||||
|
||||
let can_repair = |item: &Item| {
|
||||
// Check that item needs to be repaired, and that inventory has sufficient
|
||||
// materials to repair
|
||||
item.durability().map_or(false, |d| d > 0)
|
||||
&& self.client.repair_recipe_book().repair_recipe(item).map_or(
|
||||
false,
|
||||
|recipe| {
|
||||
recipe
|
||||
.inventory_contains_ingredients(item, self.inventory)
|
||||
.is_ok()
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
// Repair equipped button
|
||||
if Button::image(self.imgs.button)
|
||||
.w_h(105.0, 25.0)
|
||||
.hover_image(self.imgs.button_hover)
|
||||
.press_image(self.imgs.button_press)
|
||||
.label(
|
||||
&self
|
||||
.localized_strings
|
||||
.get_msg("hud-crafting-repair_equipped"),
|
||||
)
|
||||
.label_y(conrod_core::position::Relative::Scalar(1.0))
|
||||
.label_color(TEXT_COLOR)
|
||||
.label_font_size(self.fonts.cyri.scale(12))
|
||||
.label_font_id(self.fonts.cyri.conrod_id)
|
||||
.image_color(TEXT_COLOR)
|
||||
.top_right_with_margins_on(state.ids.align_ing, 20.0, 20.0)
|
||||
.set(state.ids.repair_buttons[0], ui)
|
||||
.was_clicked()
|
||||
{
|
||||
self.inventory
|
||||
.equipped_items_with_slot()
|
||||
.filter(|(_, item)| can_repair(item))
|
||||
.for_each(|(slot, _)| {
|
||||
events.push(Event::RepairItem {
|
||||
slot: Slot::Equip(slot),
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
// Repair all button
|
||||
if Button::image(self.imgs.button)
|
||||
.w_h(105.0, 25.0)
|
||||
.hover_image(self.imgs.button_hover)
|
||||
.press_image(self.imgs.button_press)
|
||||
.label(&self.localized_strings.get_msg("hud-crafting-repair_all"))
|
||||
.label_y(conrod_core::position::Relative::Scalar(1.0))
|
||||
.label_color(TEXT_COLOR)
|
||||
.label_font_size(self.fonts.cyri.scale(12))
|
||||
.label_font_id(self.fonts.cyri.conrod_id)
|
||||
.image_color(TEXT_COLOR)
|
||||
.mid_bottom_with_margin_on(state.ids.repair_buttons[0], -45.0)
|
||||
.set(state.ids.repair_buttons[1], ui)
|
||||
.was_clicked()
|
||||
{
|
||||
self.inventory
|
||||
.equipped_items_with_slot()
|
||||
.filter(|(_, item)| can_repair(item))
|
||||
.for_each(|(slot, _)| {
|
||||
events.push(Event::RepairItem {
|
||||
slot: Slot::Equip(slot),
|
||||
});
|
||||
});
|
||||
self.inventory
|
||||
.slots_with_id()
|
||||
.filter(|(_, item)| item.as_ref().map_or(false, |i| can_repair(i)))
|
||||
.for_each(|(slot, _)| {
|
||||
events.push(Event::RepairItem {
|
||||
slot: Slot::Inventory(slot),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let can_perform = repair_slot
|
||||
.item(self.inventory)
|
||||
.map_or(false, |item| can_repair(item));
|
||||
|
||||
(repair_slot.slot, None, can_perform)
|
||||
},
|
||||
};
|
||||
|
||||
// Craft button
|
||||
@ -1357,7 +1507,10 @@ impl<'a> Widget for Crafting<'a> {
|
||||
} else {
|
||||
self.imgs.button
|
||||
})
|
||||
.label(&self.localized_strings.get_msg("hud-crafting-craft"))
|
||||
.label(&match recipe_kind {
|
||||
RecipeKind::Repair => self.localized_strings.get_msg("hud-crafting-repair"),
|
||||
_ => self.localized_strings.get_msg("hud-crafting-craft"),
|
||||
})
|
||||
.label_y(conrod_core::position::Relative::Scalar(1.0))
|
||||
.label_color(if can_perform {
|
||||
TEXT_COLOR
|
||||
@ -1379,8 +1532,10 @@ impl<'a> Widget for Crafting<'a> {
|
||||
{
|
||||
match recipe_kind {
|
||||
RecipeKind::ModularWeapon => {
|
||||
if let (Some(primary_slot), Some(secondary_slot)) =
|
||||
(modular_primary_slot, modular_secondary_slot)
|
||||
if let (
|
||||
Some(Slot::Inventory(primary_slot)),
|
||||
Some(Slot::Inventory(secondary_slot)),
|
||||
) = (craft_slot_1, craft_slot_2)
|
||||
{
|
||||
events.push(Event::CraftModularWeapon {
|
||||
primary_slot,
|
||||
@ -1389,11 +1544,14 @@ impl<'a> Widget for Crafting<'a> {
|
||||
}
|
||||
},
|
||||
RecipeKind::Component(toolkind) => {
|
||||
if let Some(primary_slot) = modular_primary_slot {
|
||||
if let Some(Slot::Inventory(primary_slot)) = craft_slot_1 {
|
||||
events.push(Event::CraftModularWeaponComponent {
|
||||
toolkind,
|
||||
material: primary_slot,
|
||||
modifier: modular_secondary_slot,
|
||||
modifier: craft_slot_2.and_then(|slot| match slot {
|
||||
Slot::Inventory(slot) => Some(slot),
|
||||
Slot::Equip(_) => None,
|
||||
}),
|
||||
});
|
||||
}
|
||||
},
|
||||
@ -1401,6 +1559,11 @@ impl<'a> Widget for Crafting<'a> {
|
||||
recipe_name,
|
||||
amount: 1,
|
||||
}),
|
||||
RecipeKind::Repair => {
|
||||
if let Some(slot) = craft_slot_1 {
|
||||
events.push(Event::RepairItem { slot });
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -1470,6 +1633,9 @@ impl<'a> Widget for Crafting<'a> {
|
||||
RecipeKind::ModularWeapon | RecipeKind::Component(_) => {
|
||||
t.top_left_with_margins_on(state.ids.align_ing, 325.0, 5.0)
|
||||
},
|
||||
RecipeKind::Repair => {
|
||||
t.top_left_with_margins_on(state.ids.align_ing, 80.0, 5.0)
|
||||
},
|
||||
})
|
||||
.set(state.ids.req_station_title, ui);
|
||||
let station_img = match recipe.craft_sprite {
|
||||
@ -1482,6 +1648,7 @@ impl<'a> Widget for Crafting<'a> {
|
||||
Some(SpriteKind::SpinningWheel) => "SpinningWheel",
|
||||
Some(SpriteKind::TanningRack) => "TanningRack",
|
||||
Some(SpriteKind::DismantlingBench) => "DismantlingBench",
|
||||
Some(SpriteKind::RepairBench) => "RepairBench",
|
||||
None => "CraftsmanHammer",
|
||||
_ => "CraftsmanHammer",
|
||||
};
|
||||
@ -1506,6 +1673,7 @@ impl<'a> Widget for Crafting<'a> {
|
||||
Some(SpriteKind::SpinningWheel) => "hud-crafting-spinning_wheel",
|
||||
Some(SpriteKind::TanningRack) => "hud-crafting-tanning_rack",
|
||||
Some(SpriteKind::DismantlingBench) => "hud-crafting-salvaging_station",
|
||||
Some(SpriteKind::RepairBench) => "hud-crafting-repair_bench",
|
||||
_ => "",
|
||||
};
|
||||
Text::new(&self.localized_strings.get_msg(station_name))
|
||||
@ -1525,7 +1693,7 @@ impl<'a> Widget for Crafting<'a> {
|
||||
}
|
||||
// Ingredients Text
|
||||
// Hack from Sharp to account for iterators not having the same type
|
||||
let (mut iter_a, mut iter_b, mut iter_c);
|
||||
let (mut iter_a, mut iter_b, mut iter_c, mut iter_d);
|
||||
let ingredients = match recipe_kind {
|
||||
RecipeKind::Simple => {
|
||||
iter_a = recipe
|
||||
@ -1539,15 +1707,21 @@ impl<'a> Widget for Crafting<'a> {
|
||||
&mut iter_b
|
||||
},
|
||||
RecipeKind::Component(toolkind) => {
|
||||
if let Some(material) = modular_primary_slot
|
||||
.and_then(|slot| self.inventory.get(slot))
|
||||
if let Some(material) = craft_slot_1
|
||||
.and_then(|slot| match slot {
|
||||
Slot::Inventory(slot) => self.inventory.get(slot),
|
||||
Slot::Equip(_) => None,
|
||||
})
|
||||
.and_then(|item| item.item_definition_id().itemdef_id().map(String::from))
|
||||
{
|
||||
let component_key = ComponentKey {
|
||||
toolkind,
|
||||
material,
|
||||
modifier: modular_secondary_slot
|
||||
.and_then(|slot| self.inventory.get(slot))
|
||||
modifier: craft_slot_2
|
||||
.and_then(|slot| match slot {
|
||||
Slot::Inventory(slot) => self.inventory.get(slot),
|
||||
Slot::Equip(_) => None,
|
||||
})
|
||||
.and_then(|item| {
|
||||
item.item_definition_id().itemdef_id().map(String::from)
|
||||
}),
|
||||
@ -1566,6 +1740,24 @@ impl<'a> Widget for Crafting<'a> {
|
||||
&mut iter_b
|
||||
}
|
||||
},
|
||||
RecipeKind::Repair => {
|
||||
if let Some(item) = match craft_slot_1 {
|
||||
Some(Slot::Inventory(slot)) => self.inventory.get(slot),
|
||||
Some(Slot::Equip(slot)) => self.inventory.equipped(slot),
|
||||
None => None,
|
||||
} {
|
||||
if let Some(recipe) = self.client.repair_recipe_book().repair_recipe(item) {
|
||||
iter_d = recipe.inputs(item).collect::<Vec<_>>().into_iter();
|
||||
&mut iter_d as &mut dyn ExactSizeIterator<Item = _>
|
||||
} else {
|
||||
iter_b = core::iter::empty();
|
||||
&mut iter_b
|
||||
}
|
||||
} else {
|
||||
iter_b = core::iter::empty();
|
||||
&mut iter_b
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let num_ingredients = ingredients.len();
|
||||
|
@ -1211,7 +1211,9 @@ impl<'a> Widget for Diary<'a> {
|
||||
.inventory
|
||||
.equipped(EquipSlot::ActiveMainhand)
|
||||
.and_then(|item| match &*item.kind() {
|
||||
ItemKind::Tool(tool) => Some(tool.stats),
|
||||
ItemKind::Tool(tool) => {
|
||||
Some(tool.stats(item.stats_durability_multiplier()))
|
||||
},
|
||||
_ => None,
|
||||
});
|
||||
|
||||
@ -1219,7 +1221,9 @@ impl<'a> Widget for Diary<'a> {
|
||||
.inventory
|
||||
.equipped(EquipSlot::ActiveOffhand)
|
||||
.and_then(|item| match &*item.kind() {
|
||||
ItemKind::Tool(tool) => Some(tool.stats),
|
||||
ItemKind::Tool(tool) => {
|
||||
Some(tool.stats(item.stats_durability_multiplier()))
|
||||
},
|
||||
_ => None,
|
||||
});
|
||||
|
||||
|
@ -87,7 +87,11 @@ use common::{
|
||||
self,
|
||||
ability::{AuxiliaryAbility, Stance},
|
||||
fluid_dynamics,
|
||||
inventory::{slot::InvSlotId, trade_pricing::TradePricing, CollectFailedReason},
|
||||
inventory::{
|
||||
slot::{InvSlotId, Slot},
|
||||
trade_pricing::TradePricing,
|
||||
CollectFailedReason,
|
||||
},
|
||||
item::{
|
||||
tool::{AbilityContext, ToolKind},
|
||||
ItemDesc, MaterialStatManifest, Quality,
|
||||
@ -721,6 +725,10 @@ pub enum Event {
|
||||
modifier: Option<InvSlotId>,
|
||||
craft_sprite: Option<Vec3<i32>>,
|
||||
},
|
||||
RepairItem {
|
||||
item: Slot,
|
||||
sprite_pos: Vec3<i32>,
|
||||
},
|
||||
InviteMember(Uid),
|
||||
AcceptInvite,
|
||||
DeclineInvite,
|
||||
@ -914,6 +922,7 @@ impl Show {
|
||||
self.bag = open;
|
||||
self.map = false;
|
||||
self.crafting_fields.salvage = false;
|
||||
|
||||
if !open {
|
||||
self.crafting = false;
|
||||
}
|
||||
@ -992,6 +1001,10 @@ impl Show {
|
||||
self.crafting_fields.craft_sprite,
|
||||
Some((_, SpriteKind::DismantlingBench))
|
||||
) && matches!(tab, CraftingTab::Dismantle);
|
||||
self.crafting_fields.initialize_repair = matches!(
|
||||
self.crafting_fields.craft_sprite,
|
||||
Some((_, SpriteKind::RepairBench))
|
||||
);
|
||||
}
|
||||
|
||||
fn diary(&mut self, open: bool) {
|
||||
@ -3253,6 +3266,19 @@ impl Hud {
|
||||
crafting::Event::ClearRecipeInputs => {
|
||||
self.show.crafting_fields.recipe_inputs.clear();
|
||||
},
|
||||
crafting::Event::RepairItem { slot } => {
|
||||
if let Some(sprite_pos) = self
|
||||
.show
|
||||
.crafting_fields
|
||||
.craft_sprite
|
||||
.map(|(pos, _sprite)| pos)
|
||||
{
|
||||
events.push(Event::RepairItem {
|
||||
item: slot,
|
||||
sprite_pos,
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3688,7 +3714,6 @@ impl Hud {
|
||||
|
||||
// Maintain slot manager
|
||||
'slot_events: for event in self.slot_manager.maintain(ui_widgets) {
|
||||
use comp::slot::Slot;
|
||||
use slots::{AbilitySlot, InventorySlot, SlotKind::*};
|
||||
let to_slot = |slot_kind| match slot_kind {
|
||||
Inventory(InventorySlot {
|
||||
@ -3788,7 +3813,21 @@ impl Hud {
|
||||
self.show
|
||||
.crafting_fields
|
||||
.recipe_inputs
|
||||
.insert(c.index, i.slot);
|
||||
.insert(c.index, Slot::Inventory(i.slot));
|
||||
}
|
||||
} else if let (Equip(e), Crafting(c)) = (a, b) {
|
||||
// Add item to crafting input
|
||||
if inventories
|
||||
.get(client.entity())
|
||||
.and_then(|inv| inv.equipped(e))
|
||||
.map_or(false, |item| {
|
||||
(c.requirement)(item, client.component_recipe_book(), c.info)
|
||||
})
|
||||
{
|
||||
self.show
|
||||
.crafting_fields
|
||||
.recipe_inputs
|
||||
.insert(c.index, Slot::Equip(e));
|
||||
}
|
||||
} else if let (Crafting(c), Inventory(_)) = (a, b) {
|
||||
// Remove item from crafting input
|
||||
@ -5043,6 +5082,7 @@ pub fn get_sprite_desc(sprite: SpriteKind, localized_strings: &Localization) ->
|
||||
SpriteKind::Anvil => "hud-crafting-anvil",
|
||||
SpriteKind::Cauldron => "hud-crafting-cauldron",
|
||||
SpriteKind::CookingPot => "hud-crafting-cooking_pot",
|
||||
SpriteKind::RepairBench => "hud-crafting-repair_bench",
|
||||
SpriteKind::CraftingBench => "hud-crafting-crafting_bench",
|
||||
SpriteKind::Forge => "hud-crafting-forge",
|
||||
SpriteKind::Loom => "hud-crafting-loom",
|
||||
|
@ -47,6 +47,8 @@ widget_ids! {
|
||||
death_message_2,
|
||||
death_message_1_bg,
|
||||
death_message_2_bg,
|
||||
death_message_3,
|
||||
death_message_3_bg,
|
||||
death_bg,
|
||||
// Level up message
|
||||
level_up,
|
||||
@ -436,6 +438,12 @@ impl<'a> Skillbar<'a> {
|
||||
.font_id(self.fonts.cyri.conrod_id)
|
||||
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
|
||||
.set(state.ids.death_message_2_bg, ui);
|
||||
Text::new(&self.localized_strings.get_msg("hud_items_lost_dur"))
|
||||
.mid_bottom_with_margin_on(state.ids.death_message_2_bg, -50.0)
|
||||
.font_size(self.fonts.cyri.scale(30))
|
||||
.font_id(self.fonts.cyri.conrod_id)
|
||||
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
|
||||
.set(state.ids.death_message_3_bg, ui);
|
||||
Text::new(&self.localized_strings.get_msg("hud-you_died"))
|
||||
.bottom_left_with_margins_on(state.ids.death_message_1_bg, 2.0, 2.0)
|
||||
.font_size(self.fonts.cyri.scale(50))
|
||||
@ -448,6 +456,12 @@ impl<'a> Skillbar<'a> {
|
||||
.font_id(self.fonts.cyri.conrod_id)
|
||||
.color(CRITICAL_HP_COLOR)
|
||||
.set(state.ids.death_message_2, ui);
|
||||
Text::new(&self.localized_strings.get_msg("hud_items_lost_dur"))
|
||||
.bottom_left_with_margins_on(state.ids.death_message_3_bg, 2.0, 2.0)
|
||||
.font_size(self.fonts.cyri.scale(30))
|
||||
.font_id(self.fonts.cyri.conrod_id)
|
||||
.color(CRITICAL_HP_COLOR)
|
||||
.set(state.ids.death_message_3, ui);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ use common::{
|
||||
comp::{
|
||||
ability::{Ability, AbilityInput, AuxiliaryAbility},
|
||||
item::tool::{AbilityContext, ToolKind},
|
||||
slot::InvSlotId,
|
||||
slot::{InvSlotId, Slot},
|
||||
ActiveAbilities, Body, CharacterState, Combo, Energy, Inventory, Item, ItemKey, SkillSet,
|
||||
Stance,
|
||||
},
|
||||
@ -276,27 +276,35 @@ impl<'a> SlotKey<AbilitiesSource<'a>, img_ids::Imgs> for AbilitySlot {
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct CraftSlot {
|
||||
pub index: u32,
|
||||
pub invslot: Option<InvSlotId>,
|
||||
pub slot: Option<Slot>,
|
||||
pub requirement: fn(&Item, &ComponentRecipeBook, Option<CraftSlotInfo>) -> bool,
|
||||
pub info: Option<CraftSlotInfo>,
|
||||
}
|
||||
|
||||
impl CraftSlot {
|
||||
pub fn item<'a>(&'a self, inv: &'a Inventory) -> Option<&'a Item> {
|
||||
match self.slot {
|
||||
Some(Slot::Inventory(slot)) => inv.get(slot),
|
||||
Some(Slot::Equip(slot)) => inv.equipped(slot),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum CraftSlotInfo {
|
||||
Tool(ToolKind),
|
||||
}
|
||||
|
||||
impl PartialEq for CraftSlot {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
(self.index, self.invslot) == (other.index, other.invslot)
|
||||
}
|
||||
fn eq(&self, other: &Self) -> bool { (self.index, self.slot) == (other.index, other.slot) }
|
||||
}
|
||||
|
||||
impl Debug for CraftSlot {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
f.debug_struct("CraftSlot")
|
||||
.field("index", &self.index)
|
||||
.field("invslot", &self.invslot)
|
||||
.field("slot", &self.slot)
|
||||
.field("requirement", &"fn ptr")
|
||||
.finish()
|
||||
}
|
||||
@ -306,14 +314,11 @@ impl SlotKey<Inventory, ItemImgs> for CraftSlot {
|
||||
type ImageKey = ItemKey;
|
||||
|
||||
fn image_key(&self, source: &Inventory) -> Option<(Self::ImageKey, Option<Color>)> {
|
||||
self.invslot
|
||||
.and_then(|invslot| source.get(invslot))
|
||||
.map(|i| (i.into(), None))
|
||||
self.item(source).map(|i| (i.into(), None))
|
||||
}
|
||||
|
||||
fn amount(&self, source: &Inventory) -> Option<u32> {
|
||||
self.invslot
|
||||
.and_then(|invslot| source.get(invslot))
|
||||
self.item(source)
|
||||
.map(|item| item.amount())
|
||||
.filter(|amount| *amount > 1)
|
||||
}
|
||||
|
@ -5,7 +5,8 @@ use common::{
|
||||
item::{
|
||||
armor::{Armor, ArmorKind, Protection},
|
||||
tool::{Hands, Tool, ToolKind},
|
||||
Effects, ItemDefinitionId, ItemDesc, ItemKind, MaterialKind, MaterialStatManifest,
|
||||
Effects, Item, ItemDefinitionId, ItemDesc, ItemKind, MaterialKind,
|
||||
MaterialStatManifest,
|
||||
},
|
||||
BuffKind,
|
||||
},
|
||||
@ -100,9 +101,9 @@ pub fn material_kind_text<'a>(kind: &MaterialKind, i18n: &'a Localization) -> Co
|
||||
}
|
||||
|
||||
pub fn stats_count(item: &dyn ItemDesc, msm: &MaterialStatManifest) -> usize {
|
||||
match &*item.kind() {
|
||||
let mut count = match &*item.kind() {
|
||||
ItemKind::Armor(armor) => {
|
||||
let armor_stats = armor.stats(msm);
|
||||
let armor_stats = armor.stats(msm, item.stats_durability_multiplier());
|
||||
armor_stats.energy_reward.is_some() as usize
|
||||
+ armor_stats.energy_max.is_some() as usize
|
||||
+ armor_stats.stealth.is_some() as usize
|
||||
@ -118,7 +119,11 @@ pub fn stats_count(item: &dyn ItemDesc, msm: &MaterialStatManifest) -> usize {
|
||||
},
|
||||
ItemKind::ModularComponent { .. } => 7,
|
||||
_ => 0,
|
||||
};
|
||||
if item.has_durability() {
|
||||
count += 1;
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
pub fn line_count(item: &dyn ItemDesc, msm: &MaterialStatManifest, i18n: &Localization) -> usize {
|
||||
@ -351,6 +356,14 @@ pub fn protec2string(stat: Protection) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the durability of an item in a format more intuitive for UI
|
||||
pub fn item_durability(item: &dyn ItemDesc) -> Option<u32> {
|
||||
let durability = item
|
||||
.durability()
|
||||
.or_else(|| item.has_durability().then_some(0));
|
||||
durability.map(|d| Item::MAX_DURABILITY - d)
|
||||
}
|
||||
|
||||
pub fn ability_image(imgs: &img_ids::Imgs, ability_id: &str) -> image::Id {
|
||||
match ability_id {
|
||||
// Debug stick
|
||||
|
@ -165,6 +165,9 @@ impl BlocksOfInterest {
|
||||
fires.push(pos);
|
||||
interactables.push((pos, Interaction::Craft(CraftingTab::Dismantle)))
|
||||
},
|
||||
Some(SpriteKind::RepairBench) => {
|
||||
interactables.push((pos, Interaction::Craft(CraftingTab::All)))
|
||||
},
|
||||
_ => {},
|
||||
},
|
||||
}
|
||||
|
@ -1777,6 +1777,30 @@ impl PlayState for SessionState {
|
||||
HudEvent::SalvageItem { slot, salvage_pos } => {
|
||||
self.client.borrow_mut().salvage_item(slot, salvage_pos);
|
||||
},
|
||||
HudEvent::RepairItem { item, sprite_pos } => {
|
||||
let slots = {
|
||||
let client = self.client.borrow();
|
||||
let slots = (|| {
|
||||
if let Some(inventory) = client.inventories().get(client.entity()) {
|
||||
let item = match item {
|
||||
Slot::Equip(slot) => inventory.equipped(slot),
|
||||
Slot::Inventory(slot) => inventory.get(slot),
|
||||
}?;
|
||||
let repair_recipe =
|
||||
client.repair_recipe_book().repair_recipe(item)?;
|
||||
repair_recipe
|
||||
.inventory_contains_ingredients(item, inventory)
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})();
|
||||
slots.unwrap_or_default()
|
||||
};
|
||||
self.client
|
||||
.borrow_mut()
|
||||
.repair_item(item, slots, sprite_pos);
|
||||
},
|
||||
HudEvent::InviteMember(uid) => {
|
||||
self.client.borrow_mut().send_invite(uid, InviteKind::Group);
|
||||
},
|
||||
|
@ -583,7 +583,7 @@ impl<'a> Widget for ItemTooltip<'a> {
|
||||
// Stats
|
||||
match &*item.kind() {
|
||||
ItemKind::Tool(tool) => {
|
||||
let stats = tool.stats;
|
||||
let stats = tool.stats(item.stats_durability_multiplier());
|
||||
|
||||
// Power
|
||||
widget::Text::new(&format!(
|
||||
@ -669,10 +669,24 @@ impl<'a> Widget for ItemTooltip<'a> {
|
||||
6,
|
||||
);
|
||||
|
||||
if item.has_durability() {
|
||||
let durability = Item::MAX_DURABILITY - item.durability().unwrap_or(0);
|
||||
stat_text(
|
||||
format!(
|
||||
"{} : {}/{}",
|
||||
i18n.get_msg("common-stats-durability"),
|
||||
durability,
|
||||
Item::MAX_DURABILITY
|
||||
),
|
||||
7,
|
||||
)
|
||||
}
|
||||
|
||||
if let Some(equipped_item) = equipped_item {
|
||||
if let ItemKind::Tool(equipped_tool) = &*equipped_item.kind() {
|
||||
let tool_stats = tool.stats;
|
||||
let equipped_tool_stats = equipped_tool.stats;
|
||||
let tool_stats = tool.stats(item.stats_durability_multiplier());
|
||||
let equipped_tool_stats =
|
||||
equipped_tool.stats(equipped_item.stats_durability_multiplier());
|
||||
let diff = tool_stats - equipped_tool_stats;
|
||||
let power_diff =
|
||||
util::comparison(tool_stats.power, equipped_tool_stats.power);
|
||||
@ -697,6 +711,13 @@ impl<'a> Widget for ItemTooltip<'a> {
|
||||
equipped_tool_stats.buff_strength,
|
||||
);
|
||||
|
||||
let tool_durability =
|
||||
util::item_durability(item).unwrap_or(Item::MAX_DURABILITY);
|
||||
let equipped_durability =
|
||||
util::item_durability(equipped_item).unwrap_or(Item::MAX_DURABILITY);
|
||||
let durability_diff =
|
||||
util::comparison(tool_durability, equipped_durability);
|
||||
|
||||
let mut diff_text = |text: String, color, id_index| {
|
||||
widget::Text::new(&text)
|
||||
.align_middle_y_of(state.ids.stats[id_index])
|
||||
@ -752,11 +773,19 @@ impl<'a> Widget for ItemTooltip<'a> {
|
||||
);
|
||||
diff_text(text, buff_strength_diff.1, 6)
|
||||
}
|
||||
if tool_durability != equipped_durability {
|
||||
let text = format!(
|
||||
"{} {}",
|
||||
&durability_diff.0,
|
||||
tool_durability as i32 - equipped_durability as i32
|
||||
);
|
||||
diff_text(text, durability_diff.1, 7)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ItemKind::Armor(armor) => {
|
||||
let armor_stats = armor.stats(self.msm);
|
||||
let armor_stats = armor.stats(self.msm, item.stats_durability_multiplier());
|
||||
|
||||
let mut stat_text = |text: String, i: usize| {
|
||||
widget::Text::new(&text)
|
||||
@ -873,11 +902,25 @@ impl<'a> Widget for ItemTooltip<'a> {
|
||||
),
|
||||
index,
|
||||
);
|
||||
index += 1;
|
||||
}
|
||||
|
||||
if let Some(durability) = util::item_durability(item) {
|
||||
stat_text(
|
||||
format!(
|
||||
"{} : {}/{}",
|
||||
i18n.get_msg("common-stats-durability"),
|
||||
durability,
|
||||
Item::MAX_DURABILITY
|
||||
),
|
||||
index,
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(equipped_item) = equipped_item {
|
||||
if let ItemKind::Armor(equipped_armor) = &*equipped_item.kind() {
|
||||
let equipped_stats = equipped_armor.stats(self.msm);
|
||||
let equipped_stats = equipped_armor
|
||||
.stats(self.msm, equipped_item.stats_durability_multiplier());
|
||||
let diff = armor_stats - equipped_stats;
|
||||
let protection_diff = util::option_comparison(
|
||||
&armor_stats.protection,
|
||||
@ -902,6 +945,11 @@ impl<'a> Widget for ItemTooltip<'a> {
|
||||
let stealth_diff =
|
||||
util::option_comparison(&armor_stats.stealth, &equipped_stats.stealth);
|
||||
|
||||
let armor_durability = util::item_durability(item);
|
||||
let equipped_durability = util::item_durability(equipped_item);
|
||||
let durability_diff =
|
||||
util::option_comparison(&armor_durability, &equipped_durability);
|
||||
|
||||
let mut diff_text = |text: String, color, id_index| {
|
||||
widget::Text::new(&text)
|
||||
.align_middle_y_of(state.ids.stats[id_index])
|
||||
@ -970,6 +1018,14 @@ impl<'a> Widget for ItemTooltip<'a> {
|
||||
diff_text(text, stealth_diff.1, index);
|
||||
}
|
||||
}
|
||||
index += armor_stats.stealth.is_some() as usize;
|
||||
|
||||
if armor_durability != equipped_durability {
|
||||
let diff = armor_durability.unwrap_or(Item::MAX_DURABILITY) as i32
|
||||
- equipped_durability.unwrap_or(Item::MAX_DURABILITY) as i32;
|
||||
let text = format!("{} {}", &durability_diff.0, diff);
|
||||
diff_text(text, durability_diff.1, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -536,7 +536,7 @@ impl Archetype for House {
|
||||
center_offset.x,
|
||||
center_offset.y,
|
||||
z + 100,
|
||||
)) % 13
|
||||
)) % 14
|
||||
{
|
||||
0..=1 => SpriteKind::Crate,
|
||||
2 => SpriteKind::Bench,
|
||||
@ -550,6 +550,7 @@ impl Archetype for House {
|
||||
10 => SpriteKind::SpinningWheel,
|
||||
11 => SpriteKind::TanningRack,
|
||||
12 => SpriteKind::DismantlingBench,
|
||||
13 => SpriteKind::RepairBench,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
|
@ -632,6 +632,7 @@ impl Structure for CliffTower {
|
||||
SpriteKind::Forge,
|
||||
SpriteKind::DismantlingBench,
|
||||
SpriteKind::Anvil,
|
||||
SpriteKind::RepairBench,
|
||||
];
|
||||
for dir in LOCALITY {
|
||||
let pos = super_center + dir * (width / 3);
|
||||
|
@ -1150,6 +1150,7 @@ impl Structure for DesertCityMultiPlot {
|
||||
SpriteKind::Loom,
|
||||
SpriteKind::Anvil,
|
||||
SpriteKind::DismantlingBench,
|
||||
SpriteKind::RepairBench,
|
||||
];
|
||||
'outer: for d in 0..2 {
|
||||
for dir in NEIGHBORS {
|
||||
|
@ -888,6 +888,7 @@ impl Structure for SavannahPit {
|
||||
SpriteKind::Loom,
|
||||
SpriteKind::Anvil,
|
||||
SpriteKind::DismantlingBench,
|
||||
SpriteKind::RepairBench,
|
||||
];
|
||||
'outer: for d in 0..2 {
|
||||
for dir in NEIGHBORS {
|
||||
|
@ -128,6 +128,7 @@ impl Structure for Workshop {
|
||||
SpriteKind::Loom,
|
||||
SpriteKind::Anvil,
|
||||
SpriteKind::DismantlingBench,
|
||||
SpriteKind::RepairBench,
|
||||
];
|
||||
'outer: for d in 0..3 {
|
||||
for dir in CARDINALS {
|
||||
|
Loading…
Reference in New Issue
Block a user