1use super::{
2 DurabilityMultiplier, Item, ItemBase, ItemDef, ItemDesc, ItemKind, ItemTag, Material, Quality,
3 ToolKind, armor,
4 tool::{self, AbilityMap, AbilitySpec, Hands, Tool},
5};
6use crate::{
7 assets::{AssetExt, AssetHandle, BoxedError, FileAsset, load_ron},
8 recipe,
9};
10use common_base::dev_panic;
11use hashbrown::HashMap;
12use lazy_static::lazy_static;
13use rand::{Rng, prelude::IndexedRandom};
14use serde::{Deserialize, Serialize};
15use std::{borrow::Cow, sync::Arc};
16
17#[macro_export]
21macro_rules! modular_item_id_prefix {
22 () => {
23 "veloren.core.pseudo_items.modular."
24 };
25}
26
27#[derive(Clone, Debug, Serialize, Deserialize)]
28pub struct MaterialStatManifest {
29 tool_stats: HashMap<String, tool::Stats>,
30 armor_stats: HashMap<String, armor::Stats>,
31}
32
33impl MaterialStatManifest {
34 pub fn load() -> AssetHandle<Self> { Self::load_expect("common.material_stats_manifest") }
35
36 pub fn armor_stats(&self, key: &str) -> Option<armor::Stats> {
37 self.armor_stats.get(key).copied()
38 }
39
40 #[doc(hidden)]
41 pub fn with_empty() -> Self {
43 Self {
44 tool_stats: HashMap::default(),
45 armor_stats: HashMap::default(),
46 }
47 }
48}
49
50impl FileAsset for MaterialStatManifest {
53 const EXTENSION: &'static str = "ron";
54
55 fn from_bytes(bytes: Cow<[u8]>) -> Result<Self, BoxedError> { load_ron(&bytes) }
56}
57
58#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
59pub enum ModularBase {
60 Tool,
61}
62
63impl ModularBase {
64 pub fn pseudo_item_id(&self) -> &str {
67 match self {
68 ModularBase::Tool => concat!(modular_item_id_prefix!(), "tool"),
69 }
70 }
71
72 pub fn load_from_pseudo_id(id: &str) -> Self {
75 match id {
76 concat!(modular_item_id_prefix!(), "tool") => ModularBase::Tool,
77 _ => panic!("Attempted to load a non existent pseudo item: {}", id),
78 }
79 }
80
81 fn resolve_hands(components: &[Item]) -> Hands {
82 let hand_restriction = components.iter().find_map(|comp| match &*comp.kind() {
87 ItemKind::ModularComponent(mc) => match mc {
88 ModularComponent::ToolPrimaryComponent {
89 hand_restriction, ..
90 }
91 | ModularComponent::ToolSecondaryComponent {
92 hand_restriction, ..
93 } => *hand_restriction,
94 },
95 _ => None,
96 });
97 hand_restriction.unwrap_or(Hands::One)
99 }
100
101 #[inline(never)]
102 pub(super) fn kind(
103 &self,
104 components: &[Item],
105 msm: &MaterialStatManifest,
106 durability_multiplier: DurabilityMultiplier,
107 ) -> Cow<'_, ItemKind> {
108 let toolkind = components
109 .iter()
110 .find_map(|comp| match &*comp.kind() {
111 ItemKind::ModularComponent(ModularComponent::ToolPrimaryComponent {
112 toolkind,
113 ..
114 }) => Some(*toolkind),
115 _ => None,
116 })
117 .unwrap_or(ToolKind::Empty);
118
119 let stats: tool::Stats = components
120 .iter()
121 .filter_map(|comp| {
122 if let ItemKind::ModularComponent(mod_comp) = &*comp.kind() {
123 mod_comp.tool_stats(comp.components(), msm)
124 } else {
125 None
126 }
127 })
128 .fold(tool::Stats::one(), |a, b| a * b)
129 * durability_multiplier;
130
131 match self {
132 ModularBase::Tool => Cow::Owned(ItemKind::Tool(Tool::new(
133 toolkind,
134 Self::resolve_hands(components),
135 stats,
136 ))),
137 }
138 }
139
140 #[deprecated = "this function doesn't localize"]
144 pub fn generate_name(&self, components: &[Item]) -> Cow<'_, str> {
145 match self {
146 ModularBase::Tool => {
147 let name = components
148 .iter()
149 .find_map(|comp| match &*comp.kind() {
150 ItemKind::ModularComponent(ModularComponent::ToolPrimaryComponent {
151 weapon_name,
152 ..
153 }) => {
154 let material_name = comp
158 .components()
159 .iter()
160 .find_map(|mat| match mat.kind() {
161 #[expect(deprecated)]
162 Cow::Owned(ItemKind::Ingredient { descriptor, .. }) => {
163 Some(Cow::Owned(descriptor))
164 },
165 #[expect(deprecated)]
166 Cow::Borrowed(ItemKind::Ingredient { descriptor, .. }) => {
167 Some(Cow::Borrowed(descriptor.as_str()))
168 },
169 _ => None,
170 })
171 .unwrap_or_else(|| "Modular".into());
172
173 #[expect(deprecated)]
175 let weapon_name = match weapon_name {
176 WeaponName::Universal(name) => name,
177 WeaponName::HandednessDependent {
178 one_handed: name1,
179 two_handed: name2,
180 } => match Self::resolve_hands(components) {
181 Hands::One => name1,
182 Hands::Two => name2,
183 },
184 };
185
186 Some(format!("{material_name} {weapon_name}"))
188 },
189 _ => None,
190 })
191 .unwrap_or_else(|| "Modular Weapon".to_owned());
192 Cow::Owned(name)
193 },
194 }
195 }
196
197 pub fn compute_quality(&self, components: &[Item]) -> Quality {
198 components
199 .iter()
200 .fold(Quality::MIN, |a, b| a.max(b.quality()))
201 }
202
203 pub fn ability_spec(&self, components: &[Item]) -> Option<Cow<'_, AbilitySpec>> {
204 match self {
205 ModularBase::Tool => components.iter().find_map(|comp| match &*comp.kind() {
206 ItemKind::ModularComponent(ModularComponent::ToolPrimaryComponent {
207 toolkind,
208 ..
209 }) => Some(Cow::Owned(AbilitySpec::Tool(*toolkind))),
210 _ => None,
211 }),
212 }
213 }
214
215 pub fn generate_tags(&self, components: &[Item]) -> Vec<ItemTag> {
216 match self {
217 ModularBase::Tool => {
218 if let Some(comp) = components.iter().find(|comp| {
219 matches!(
220 &*comp.kind(),
221 ItemKind::ModularComponent(ModularComponent::ToolPrimaryComponent { .. })
222 )
223 }) {
224 if let Some(material) =
225 comp.components()
226 .iter()
227 .find_map(|comp| match &*comp.kind() {
228 ItemKind::Ingredient { .. } => {
229 comp.tags().into_iter().find_map(|tag| match tag {
230 ItemTag::Material(material) => Some(material),
231 _ => None,
232 })
233 },
234 _ => None,
235 })
236 {
237 vec![
238 ItemTag::Material(material),
239 ItemTag::SalvageInto(material, 1),
240 ]
241 } else {
242 Vec::new()
243 }
244 } else {
245 Vec::new()
246 }
247 },
248 }
249 }
250}
251
252#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
253#[serde(deny_unknown_fields)]
254pub enum ModularComponent {
255 ToolPrimaryComponent {
256 toolkind: ToolKind,
257 stats: tool::Stats,
258 hand_restriction: Option<Hands>,
259 weapon_name: WeaponName,
260 },
261 ToolSecondaryComponent {
262 toolkind: ToolKind,
263 stats: tool::Stats,
264 hand_restriction: Option<Hands>,
265 },
266}
267
268#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
269pub enum WeaponName {
270 #[deprecated = "since item i18n"]
271 Universal(String),
272 HandednessDependent {
273 #[deprecated = "since item i18n"]
274 one_handed: String,
275 #[deprecated = "since item i18n"]
276 two_handed: String,
277 },
278}
279
280impl ModularComponent {
281 pub fn tool_stats(
282 &self,
283 components: &[Item],
284 msm: &MaterialStatManifest,
285 ) -> Option<tool::Stats> {
286 match self {
287 Self::ToolPrimaryComponent { stats, .. } => {
288 let average_material_mult = components
289 .iter()
290 .filter_map(|comp| {
291 comp.item_definition_id()
292 .itemdef_id()
293 .and_then(|id| msm.tool_stats.get(id))
294 .copied()
295 .zip(Some(1))
296 })
297 .reduce(|(stats_a, count_a), (stats_b, count_b)| {
298 (stats_a + stats_b, count_a + count_b)
299 })
300 .map_or_else(tool::Stats::one, |(stats_sum, count)| {
301 stats_sum / (count as f32)
302 });
303
304 Some(*stats * average_material_mult)
305 },
306 Self::ToolSecondaryComponent { stats, .. } => Some(*stats),
307 }
308 }
309
310 pub fn toolkind(&self) -> Option<ToolKind> {
311 match self {
312 Self::ToolPrimaryComponent { toolkind, .. }
313 | Self::ToolSecondaryComponent { toolkind, .. } => Some(*toolkind),
314 }
315 }
316}
317
318const SUPPORTED_TOOLKINDS: [ToolKind; 6] = [
319 ToolKind::Sword,
320 ToolKind::Axe,
321 ToolKind::Hammer,
322 ToolKind::Bow,
323 ToolKind::Staff,
324 ToolKind::Sceptre,
325];
326
327type PrimaryComponentPool = HashMap<(ToolKind, String), Vec<(Item, Option<Hands>)>>;
328type SecondaryComponentPool = HashMap<ToolKind, Vec<(Arc<ItemDef>, Option<Hands>)>>;
329
330lazy_static! {
331 pub static ref PRIMARY_COMPONENT_POOL: PrimaryComponentPool = {
332 let mut component_pool = HashMap::new();
333
334 use crate::recipe::ComponentKey;
337 let recipes = recipe::default_component_recipe_book().read();
338 let ability_map = &AbilityMap::load().read();
339 let msm = &MaterialStatManifest::load().read();
340
341 recipes.iter().for_each(
342 |(
343 ComponentKey {
344 toolkind, material, ..
345 },
346 recipe,
347 )| {
348 let component = recipe.item_output(ability_map, msm);
349 let hand_restriction =
350 if let ItemKind::ModularComponent(ModularComponent::ToolPrimaryComponent {
351 hand_restriction,
352 ..
353 }) = &*component.kind()
354 {
355 *hand_restriction
356 } else {
357 return;
358 };
359 let entry: &mut Vec<_> = component_pool
360 .entry((*toolkind, String::from(material)))
361 .or_default();
362 entry.push((component, hand_restriction));
363 },
364 );
365
366 component_pool
367 };
368
369 static ref SECONDARY_COMPONENT_POOL: SecondaryComponentPool = {
370 let mut component_pool = HashMap::new();
371
372 const ASSET_PREFIX: &str = "common.items.modular.weapon.secondary";
373
374 for toolkind in SUPPORTED_TOOLKINDS {
375 let directory = format!("{}.{}", ASSET_PREFIX, toolkind.identifier_name());
376 if let Ok(items) = Item::new_from_asset_glob(&directory) {
377 items
378 .into_iter()
379 .filter_map(|comp| Some(comp.item_definition_id().itemdef_id()?.to_owned()))
380 .filter_map(|id| Arc::<ItemDef>::load_cloned(&id).ok())
381 .for_each(|comp_def| {
382 if let ItemKind::ModularComponent(
383 ModularComponent::ToolSecondaryComponent {
384 hand_restriction, ..
385 },
386 ) = comp_def.kind
387 {
388 let entry: &mut Vec<_> = component_pool.entry(toolkind).or_default();
389 entry.push((Arc::clone(&comp_def), hand_restriction));
390 }
391 });
392 }
393 }
394
395 component_pool
396 };
397}
398
399#[derive(Debug)]
400pub enum ModularWeaponCreationError {
401 MaterialNotFound,
402 PrimaryComponentNotFound,
403 SecondaryComponentNotFound,
404 WeaponHandednessNotFound,
405}
406
407pub fn compatible_handedness(a: Option<Hands>, b: Option<Hands>) -> bool {
411 match (a, b) {
412 (Some(a), Some(b)) => a == b,
413 _ => true,
414 }
415}
416
417pub fn matching_handedness(
421 primary: Option<Hands>,
422 secondary: Option<Hands>,
423 restriction: Option<Hands>,
424) -> bool {
425 match (primary, secondary, restriction) {
426 (Some(a), Some(b), Some(c)) => (a == b) && (b == c),
427
428 (None, None, Some(Hands::Two)) => false,
429 (None, None, Some(Hands::One)) => true,
430
431 (Some(a), None, Some(c)) => a == c,
432 (None, Some(b), Some(c)) => b == c,
433 (Some(a), Some(b), None) => a == b,
434
435 (_, _, None) => true,
436 }
437}
438
439pub fn generate_weapon_primary_components(
443 tool: ToolKind,
444 material: Material,
445 hand_restriction: Option<Hands>,
446) -> Result<Vec<(Item, Option<Hands>)>, ModularWeaponCreationError> {
447 if let Some(material_id) = material.asset_identifier() {
448 let ability_map = &AbilityMap::load().read();
450 let msm = &MaterialStatManifest::load().read();
451
452 Ok(PRIMARY_COMPONENT_POOL
453 .get(&(tool, material_id.to_owned()))
454 .into_iter()
455 .flatten()
456 .filter(|(_comp, hand)| compatible_handedness(hand_restriction, *hand))
457 .map(|(c, h)| (c.duplicate(ability_map, msm), *h))
458 .collect())
459 } else {
460 Err(ModularWeaponCreationError::MaterialNotFound)
461 }
462}
463
464pub fn random_weapon_primary_component(
475 tool: ToolKind,
476 material: Material,
477 hand_restriction: Option<Hands>,
478 mut rng: &mut impl Rng,
479) -> Result<(Item, Option<Hands>), ModularWeaponCreationError> {
480 let result = {
481 if let Some(material_id) = material.asset_identifier() {
482 let ability_map = &AbilityMap::load().read();
484 let msm = &MaterialStatManifest::load().read();
485
486 let primary_components = PRIMARY_COMPONENT_POOL
487 .get(&(tool, material_id.to_owned()))
488 .into_iter()
489 .flatten()
490 .filter(|(_comp, hand)| compatible_handedness(hand_restriction, *hand))
491 .collect::<Vec<_>>();
492
493 let (comp, hand) = primary_components
494 .choose(&mut rng)
495 .ok_or(ModularWeaponCreationError::PrimaryComponentNotFound)?;
496 let comp = comp.duplicate(ability_map, msm);
497 Ok((comp, (*hand)))
498 } else {
499 Err(ModularWeaponCreationError::MaterialNotFound)
500 }
501 };
502
503 if let Err(err) = &result {
504 let error_str = format!(
505 "Failed to synthesize a primary component for a modular {tool:?} made of {material:?} \
506 that had a hand restriction of {hand_restriction:?}. Error: {err:?}"
507 );
508 dev_panic!(error_str)
509 }
510 result
511}
512
513pub fn generate_weapons(
514 tool: ToolKind,
515 material: Material,
516 hand_restriction: Option<Hands>,
517) -> Result<Vec<Item>, ModularWeaponCreationError> {
518 let ability_map = &AbilityMap::load().read();
520 let msm = &MaterialStatManifest::load().read();
521
522 let primaries = generate_weapon_primary_components(tool, material, hand_restriction)?;
523 let mut weapons = Vec::new();
524
525 for (comp, comp_hand) in primaries {
526 let secondaries = SECONDARY_COMPONENT_POOL
527 .get(&tool)
528 .into_iter()
529 .flatten()
530 .filter(|(_def, hand)| matching_handedness(comp_hand, *hand, hand_restriction));
531
532 for (def, _hand) in secondaries {
533 let secondary = Item::new_from_item_base(
534 ItemBase::Simple(Arc::clone(def)),
535 Vec::new(),
536 ability_map,
537 msm,
538 );
539 weapons.push(Item::new_from_item_base(
540 ItemBase::Modular(ModularBase::Tool),
541 vec![comp.duplicate(ability_map, msm), secondary],
542 ability_map,
543 msm,
544 ));
545 }
546 }
547
548 Ok(weapons)
549}
550
551pub fn random_weapon(
554 tool: ToolKind,
555 material: Material,
556 hand_restriction: Option<Hands>,
557 mut rng: &mut impl Rng,
558) -> Result<Item, ModularWeaponCreationError> {
559 let result = {
560 let ability_map = &AbilityMap::load().read();
562 let msm = &MaterialStatManifest::load().read();
563
564 let (primary_component, primary_hands) =
565 random_weapon_primary_component(tool, material, hand_restriction, rng)?;
566
567 let secondary_components = SECONDARY_COMPONENT_POOL
568 .get(&tool)
569 .into_iter()
570 .flatten()
571 .filter(|(_def, hand)| matching_handedness(primary_hands, *hand, hand_restriction))
572 .collect::<Vec<_>>();
573
574 let secondary_component = {
575 let def = &secondary_components
576 .choose(&mut rng)
577 .ok_or(ModularWeaponCreationError::SecondaryComponentNotFound)?
578 .0;
579
580 Item::new_from_item_base(
581 ItemBase::Simple(Arc::clone(def)),
582 Vec::new(),
583 ability_map,
584 msm,
585 )
586 };
587
588 Ok(Item::new_from_item_base(
590 ItemBase::Modular(ModularBase::Tool),
591 vec![primary_component, secondary_component],
592 ability_map,
593 msm,
594 ))
595 };
596 if let Err(err) = &result {
597 let error_str = format!(
598 "Failed to synthesize a modular {tool:?} made of {material:?} that had a hand \
599 restriction of {hand_restriction:?}. Error: {err:?}"
600 );
601 dev_panic!(error_str)
602 }
603 result
604}
605
606#[deprecated = "since item i18n"]
608pub fn modify_name<'a>(item_name: &'a str, item: &'a Item) -> Cow<'a, str> {
609 if let ItemKind::ModularComponent(_) = &*item.kind() {
610 if let Some(material_name) = item
611 .components()
612 .iter()
613 .find_map(|comp| match &*comp.kind() {
614 #[expect(deprecated)]
615 ItemKind::Ingredient { descriptor, .. } => Some(descriptor.to_owned()),
616 _ => None,
617 })
618 {
619 Cow::Owned(format!("{} {}", material_name, item_name))
623 } else {
624 Cow::Borrowed(item_name)
625 }
626 } else {
627 Cow::Borrowed(item_name)
628 }
629}
630
631pub type ModularWeaponKey = (String, String, Hands);
643
644pub fn weapon_to_key(mod_weap: impl ItemDesc) -> ModularWeaponKey {
645 let hands = if let ItemKind::Tool(tool) = &*mod_weap.kind() {
646 tool.hands
647 } else {
648 Hands::One
649 };
650
651 match mod_weap
652 .components()
653 .iter()
654 .find_map(|comp| match &*comp.kind() {
655 ItemKind::ModularComponent(ModularComponent::ToolPrimaryComponent { .. }) => {
656 let component_id = comp.item_definition_id().itemdef_id()?.to_owned();
657 let material_id = comp.components().iter().find_map(|mat| match &*mat.kind() {
658 ItemKind::Ingredient { .. } => {
659 Some(mat.item_definition_id().itemdef_id()?.to_owned())
660 },
661 _ => None,
662 });
663 Some((component_id, material_id))
664 },
665 _ => None,
666 }) {
667 Some((component_id, Some(material_id))) => (component_id, material_id, hands),
668 Some((component_id, None)) => (component_id, String::new(), hands),
669 None => (String::new(), String::new(), hands),
670 }
671}
672
673pub type ModularWeaponComponentKey = (String, String);
684
685pub enum ModularWeaponComponentKeyError {
686 MaterialNotFound,
687}
688
689pub fn weapon_component_to_key(
690 item_def_id: &str,
691 components: &[Item],
692) -> Result<ModularWeaponComponentKey, ModularWeaponComponentKeyError> {
693 match components.iter().find_map(|mat| match &*mat.kind() {
694 ItemKind::Ingredient { .. } => Some(mat.item_definition_id().itemdef_id()?.to_owned()),
695 _ => None,
696 }) {
697 Some(material_id) => Ok((item_def_id.to_owned(), material_id)),
698 None => Err(ModularWeaponComponentKeyError::MaterialNotFound),
699 }
700}