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::{self, Asset, AssetExt, AssetHandle},
8 recipe,
9};
10use common_base::dev_panic;
11use hashbrown::HashMap;
12use lazy_static::lazy_static;
13use rand::{Rng, prelude::SliceRandom};
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 Asset for MaterialStatManifest {
53 type Loader = assets::RonLoader;
54
55 const EXTENSION: &'static str = "ron";
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 pub fn generate_name(&self, components: &[Item]) -> Cow<str> {
144 match self {
145 ModularBase::Tool => {
146 let name = components
147 .iter()
148 .find_map(|comp| match &*comp.kind() {
149 ItemKind::ModularComponent(ModularComponent::ToolPrimaryComponent {
150 weapon_name,
151 ..
152 }) => {
153 let material_name = comp
154 .components()
155 .iter()
156 .find_map(|mat| match mat.kind() {
157 #[expect(deprecated)]
158 Cow::Owned(ItemKind::Ingredient { descriptor, .. }) => {
159 Some(Cow::Owned(descriptor))
160 },
161 #[expect(deprecated)]
162 Cow::Borrowed(ItemKind::Ingredient { descriptor, .. }) => {
163 Some(Cow::Borrowed(descriptor.as_str()))
164 },
165 _ => None,
166 })
167 .unwrap_or_else(|| "Modular".into());
168 Some(format!(
169 "{} {}",
170 material_name,
171 weapon_name.resolve_name(Self::resolve_hands(components))
172 ))
173 },
174 _ => None,
175 })
176 .unwrap_or_else(|| "Modular Weapon".to_owned());
177 Cow::Owned(name)
178 },
179 }
180 }
181
182 pub fn compute_quality(&self, components: &[Item]) -> Quality {
183 components
184 .iter()
185 .fold(Quality::MIN, |a, b| a.max(b.quality()))
186 }
187
188 pub fn ability_spec(&self, components: &[Item]) -> Option<Cow<AbilitySpec>> {
189 match self {
190 ModularBase::Tool => components.iter().find_map(|comp| match &*comp.kind() {
191 ItemKind::ModularComponent(ModularComponent::ToolPrimaryComponent {
192 toolkind,
193 ..
194 }) => Some(Cow::Owned(AbilitySpec::Tool(*toolkind))),
195 _ => None,
196 }),
197 }
198 }
199
200 pub fn generate_tags(&self, components: &[Item]) -> Vec<ItemTag> {
201 match self {
202 ModularBase::Tool => {
203 if let Some(comp) = components.iter().find(|comp| {
204 matches!(
205 &*comp.kind(),
206 ItemKind::ModularComponent(ModularComponent::ToolPrimaryComponent { .. })
207 )
208 }) {
209 if let Some(material) =
210 comp.components()
211 .iter()
212 .find_map(|comp| match &*comp.kind() {
213 ItemKind::Ingredient { .. } => {
214 comp.tags().into_iter().find_map(|tag| match tag {
215 ItemTag::Material(material) => Some(material),
216 _ => None,
217 })
218 },
219 _ => None,
220 })
221 {
222 vec![
223 ItemTag::Material(material),
224 ItemTag::SalvageInto(material, 1),
225 ]
226 } else {
227 Vec::new()
228 }
229 } else {
230 Vec::new()
231 }
232 },
233 }
234 }
235}
236
237#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
238#[serde(deny_unknown_fields)]
239pub enum ModularComponent {
240 ToolPrimaryComponent {
241 toolkind: ToolKind,
242 stats: tool::Stats,
243 hand_restriction: Option<Hands>,
244 weapon_name: WeaponName,
245 },
246 ToolSecondaryComponent {
247 toolkind: ToolKind,
248 stats: tool::Stats,
249 hand_restriction: Option<Hands>,
250 },
251}
252
253#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
254pub enum WeaponName {
255 Universal(String),
256 HandednessDependent {
257 one_handed: String,
258 two_handed: String,
259 },
260}
261
262impl WeaponName {
263 fn resolve_name(&self, handedness: Hands) -> &str {
264 match self {
265 Self::Universal(name) => name,
266 Self::HandednessDependent {
267 one_handed: name1,
268 two_handed: name2,
269 } => match handedness {
270 Hands::One => name1,
271 Hands::Two => name2,
272 },
273 }
274 }
275}
276
277impl ModularComponent {
278 pub fn tool_stats(
279 &self,
280 components: &[Item],
281 msm: &MaterialStatManifest,
282 ) -> Option<tool::Stats> {
283 match self {
284 Self::ToolPrimaryComponent { stats, .. } => {
285 let average_material_mult = components
286 .iter()
287 .filter_map(|comp| {
288 comp.item_definition_id()
289 .itemdef_id()
290 .and_then(|id| msm.tool_stats.get(id))
291 .copied()
292 .zip(Some(1))
293 })
294 .reduce(|(stats_a, count_a), (stats_b, count_b)| {
295 (stats_a + stats_b, count_a + count_b)
296 })
297 .map_or_else(tool::Stats::one, |(stats_sum, count)| {
298 stats_sum / (count as f32)
299 });
300
301 Some(*stats * average_material_mult)
302 },
303 Self::ToolSecondaryComponent { stats, .. } => Some(*stats),
304 }
305 }
306
307 pub fn toolkind(&self) -> Option<ToolKind> {
308 match self {
309 Self::ToolPrimaryComponent { toolkind, .. }
310 | Self::ToolSecondaryComponent { toolkind, .. } => Some(*toolkind),
311 }
312 }
313}
314
315const SUPPORTED_TOOLKINDS: [ToolKind; 6] = [
316 ToolKind::Sword,
317 ToolKind::Axe,
318 ToolKind::Hammer,
319 ToolKind::Bow,
320 ToolKind::Staff,
321 ToolKind::Sceptre,
322];
323
324type PrimaryComponentPool = HashMap<(ToolKind, String), Vec<(Item, Option<Hands>)>>;
325type SecondaryComponentPool = HashMap<ToolKind, Vec<(Arc<ItemDef>, Option<Hands>)>>;
326
327lazy_static! {
328 pub static ref PRIMARY_COMPONENT_POOL: PrimaryComponentPool = {
329 let mut component_pool = HashMap::new();
330
331 use crate::recipe::ComponentKey;
334 let recipes = recipe::default_component_recipe_book().read();
335 let ability_map = &AbilityMap::load().read();
336 let msm = &MaterialStatManifest::load().read();
337
338 recipes.iter().for_each(
339 |(
340 ComponentKey {
341 toolkind, material, ..
342 },
343 recipe,
344 )| {
345 let component = recipe.item_output(ability_map, msm);
346 let hand_restriction =
347 if let ItemKind::ModularComponent(ModularComponent::ToolPrimaryComponent {
348 hand_restriction,
349 ..
350 }) = &*component.kind()
351 {
352 *hand_restriction
353 } else {
354 return;
355 };
356 let entry: &mut Vec<_> = component_pool
357 .entry((*toolkind, String::from(material)))
358 .or_default();
359 entry.push((component, hand_restriction));
360 },
361 );
362
363 component_pool
364 };
365
366 static ref SECONDARY_COMPONENT_POOL: SecondaryComponentPool = {
367 let mut component_pool = HashMap::new();
368
369 const ASSET_PREFIX: &str = "common.items.modular.weapon.secondary";
370
371 for toolkind in SUPPORTED_TOOLKINDS {
372 let directory = format!("{}.{}", ASSET_PREFIX, toolkind.identifier_name());
373 if let Ok(items) = Item::new_from_asset_glob(&directory) {
374 items
375 .into_iter()
376 .filter_map(|comp| Some(comp.item_definition_id().itemdef_id()?.to_owned()))
377 .filter_map(|id| Arc::<ItemDef>::load_cloned(&id).ok())
378 .for_each(|comp_def| {
379 if let ItemKind::ModularComponent(
380 ModularComponent::ToolSecondaryComponent {
381 hand_restriction, ..
382 },
383 ) = comp_def.kind
384 {
385 let entry: &mut Vec<_> = component_pool.entry(toolkind).or_default();
386 entry.push((Arc::clone(&comp_def), hand_restriction));
387 }
388 });
389 }
390 }
391
392 component_pool
393 };
394}
395
396#[derive(Debug)]
397pub enum ModularWeaponCreationError {
398 MaterialNotFound,
399 PrimaryComponentNotFound,
400 SecondaryComponentNotFound,
401 WeaponHandednessNotFound,
402}
403
404pub fn compatible_handedness(a: Option<Hands>, b: Option<Hands>) -> bool {
408 match (a, b) {
409 (Some(a), Some(b)) => a == b,
410 _ => true,
411 }
412}
413
414pub fn matching_handedness(
418 primary: Option<Hands>,
419 secondary: Option<Hands>,
420 restriction: Option<Hands>,
421) -> bool {
422 match (primary, secondary, restriction) {
423 (Some(a), Some(b), Some(c)) => (a == b) && (b == c),
424
425 (None, None, Some(Hands::Two)) => false,
426 (None, None, Some(Hands::One)) => true,
427
428 (Some(a), None, Some(c)) => a == c,
429 (None, Some(b), Some(c)) => b == c,
430 (Some(a), Some(b), None) => a == b,
431
432 (_, _, None) => true,
433 }
434}
435
436pub fn generate_weapon_primary_components(
440 tool: ToolKind,
441 material: Material,
442 hand_restriction: Option<Hands>,
443) -> Result<Vec<(Item, Option<Hands>)>, ModularWeaponCreationError> {
444 if let Some(material_id) = material.asset_identifier() {
445 let ability_map = &AbilityMap::load().read();
447 let msm = &MaterialStatManifest::load().read();
448
449 Ok(PRIMARY_COMPONENT_POOL
450 .get(&(tool, material_id.to_owned()))
451 .into_iter()
452 .flatten()
453 .filter(|(_comp, hand)| compatible_handedness(hand_restriction, *hand))
454 .map(|(c, h)| (c.duplicate(ability_map, msm), *h))
455 .collect())
456 } else {
457 Err(ModularWeaponCreationError::MaterialNotFound)
458 }
459}
460
461pub fn random_weapon_primary_component(
472 tool: ToolKind,
473 material: Material,
474 hand_restriction: Option<Hands>,
475 mut rng: &mut impl Rng,
476) -> Result<(Item, Option<Hands>), ModularWeaponCreationError> {
477 let result = {
478 if let Some(material_id) = material.asset_identifier() {
479 let ability_map = &AbilityMap::load().read();
481 let msm = &MaterialStatManifest::load().read();
482
483 let primary_components = PRIMARY_COMPONENT_POOL
484 .get(&(tool, material_id.to_owned()))
485 .into_iter()
486 .flatten()
487 .filter(|(_comp, hand)| compatible_handedness(hand_restriction, *hand))
488 .collect::<Vec<_>>();
489
490 let (comp, hand) = primary_components
491 .choose(&mut rng)
492 .ok_or(ModularWeaponCreationError::PrimaryComponentNotFound)?;
493 let comp = comp.duplicate(ability_map, msm);
494 Ok((comp, (*hand)))
495 } else {
496 Err(ModularWeaponCreationError::MaterialNotFound)
497 }
498 };
499
500 if let Err(err) = &result {
501 let error_str = format!(
502 "Failed to synthesize a primary component for a modular {tool:?} made of {material:?} \
503 that had a hand restriction of {hand_restriction:?}. Error: {err:?}"
504 );
505 dev_panic!(error_str)
506 }
507 result
508}
509
510pub fn generate_weapons(
511 tool: ToolKind,
512 material: Material,
513 hand_restriction: Option<Hands>,
514) -> Result<Vec<Item>, ModularWeaponCreationError> {
515 let ability_map = &AbilityMap::load().read();
517 let msm = &MaterialStatManifest::load().read();
518
519 let primaries = generate_weapon_primary_components(tool, material, hand_restriction)?;
520 let mut weapons = Vec::new();
521
522 for (comp, comp_hand) in primaries {
523 let secondaries = SECONDARY_COMPONENT_POOL
524 .get(&tool)
525 .into_iter()
526 .flatten()
527 .filter(|(_def, hand)| matching_handedness(comp_hand, *hand, hand_restriction));
528
529 for (def, _hand) in secondaries {
530 let secondary = Item::new_from_item_base(
531 ItemBase::Simple(Arc::clone(def)),
532 Vec::new(),
533 ability_map,
534 msm,
535 );
536 weapons.push(Item::new_from_item_base(
537 ItemBase::Modular(ModularBase::Tool),
538 vec![comp.duplicate(ability_map, msm), secondary],
539 ability_map,
540 msm,
541 ));
542 }
543 }
544
545 Ok(weapons)
546}
547
548pub fn random_weapon(
551 tool: ToolKind,
552 material: Material,
553 hand_restriction: Option<Hands>,
554 mut rng: &mut impl Rng,
555) -> Result<Item, ModularWeaponCreationError> {
556 let result = {
557 let ability_map = &AbilityMap::load().read();
559 let msm = &MaterialStatManifest::load().read();
560
561 let (primary_component, primary_hands) =
562 random_weapon_primary_component(tool, material, hand_restriction, rng)?;
563
564 let secondary_components = SECONDARY_COMPONENT_POOL
565 .get(&tool)
566 .into_iter()
567 .flatten()
568 .filter(|(_def, hand)| matching_handedness(primary_hands, *hand, hand_restriction))
569 .collect::<Vec<_>>();
570
571 let secondary_component = {
572 let def = &secondary_components
573 .choose(&mut rng)
574 .ok_or(ModularWeaponCreationError::SecondaryComponentNotFound)?
575 .0;
576
577 Item::new_from_item_base(
578 ItemBase::Simple(Arc::clone(def)),
579 Vec::new(),
580 ability_map,
581 msm,
582 )
583 };
584
585 Ok(Item::new_from_item_base(
587 ItemBase::Modular(ModularBase::Tool),
588 vec![primary_component, secondary_component],
589 ability_map,
590 msm,
591 ))
592 };
593 if let Err(err) = &result {
594 let error_str = format!(
595 "Failed to synthesize a modular {tool:?} made of {material:?} that had a hand \
596 restriction of {hand_restriction:?}. Error: {err:?}"
597 );
598 dev_panic!(error_str)
599 }
600 result
601}
602
603pub fn modify_name<'a>(item_name: &'a str, item: &'a Item) -> Cow<'a, str> {
604 if let ItemKind::ModularComponent(_) = &*item.kind() {
605 if let Some(material_name) = item
606 .components()
607 .iter()
608 .find_map(|comp| match &*comp.kind() {
609 #[expect(deprecated)]
610 ItemKind::Ingredient { descriptor, .. } => Some(descriptor.to_owned()),
611 _ => None,
612 })
613 {
614 Cow::Owned(format!("{} {}", material_name, item_name))
615 } else {
616 Cow::Borrowed(item_name)
617 }
618 } else {
619 Cow::Borrowed(item_name)
620 }
621}
622
623pub type ModularWeaponKey = (String, String, Hands);
626
627pub fn weapon_to_key(mod_weap: impl ItemDesc) -> ModularWeaponKey {
628 let hands = if let ItemKind::Tool(tool) = &*mod_weap.kind() {
629 tool.hands
630 } else {
631 Hands::One
632 };
633
634 match mod_weap
635 .components()
636 .iter()
637 .find_map(|comp| match &*comp.kind() {
638 ItemKind::ModularComponent(ModularComponent::ToolPrimaryComponent { .. }) => {
639 let component_id = comp.item_definition_id().itemdef_id()?.to_owned();
640 let material_id = comp.components().iter().find_map(|mat| match &*mat.kind() {
641 ItemKind::Ingredient { .. } => {
642 Some(mat.item_definition_id().itemdef_id()?.to_owned())
643 },
644 _ => None,
645 });
646 Some((component_id, material_id))
647 },
648 _ => None,
649 }) {
650 Some((component_id, Some(material_id))) => (component_id, material_id, hands),
651 Some((component_id, None)) => (component_id, String::new(), hands),
652 None => (String::new(), String::new(), hands),
653 }
654}
655
656pub type ModularWeaponComponentKey = (String, String);
659
660pub enum ModularWeaponComponentKeyError {
661 MaterialNotFound,
662}
663
664pub fn weapon_component_to_key(
665 item_def_id: &str,
666 components: &[Item],
667) -> Result<ModularWeaponComponentKey, ModularWeaponComponentKeyError> {
668 match components.iter().find_map(|mat| match &*mat.kind() {
669 ItemKind::Ingredient { .. } => Some(mat.item_definition_id().itemdef_id()?.to_owned()),
670 _ => None,
671 }) {
672 Some(material_id) => Ok((item_def_id.to_owned(), material_id)),
673 None => Err(ModularWeaponComponentKeyError::MaterialNotFound),
674 }
675}