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}
402
403pub fn compatible_handedness(a: Option<Hands>, b: Option<Hands>) -> bool {
407 match (a, b) {
408 (Some(a), Some(b)) => a == b,
409 _ => true,
410 }
411}
412
413pub fn generate_weapon_primary_components(
417 tool: ToolKind,
418 material: Material,
419 hand_restriction: Option<Hands>,
420) -> Result<Vec<(Item, Option<Hands>)>, ModularWeaponCreationError> {
421 if let Some(material_id) = material.asset_identifier() {
422 let ability_map = &AbilityMap::load().read();
424 let msm = &MaterialStatManifest::load().read();
425
426 Ok(PRIMARY_COMPONENT_POOL
427 .get(&(tool, material_id.to_owned()))
428 .into_iter()
429 .flatten()
430 .filter(|(_comp, hand)| compatible_handedness(hand_restriction, *hand))
431 .map(|(c, h)| (c.duplicate(ability_map, msm), hand_restriction.or(*h)))
432 .collect())
433 } else {
434 Err(ModularWeaponCreationError::MaterialNotFound)
435 }
436}
437
438pub fn random_weapon_primary_component(
449 tool: ToolKind,
450 material: Material,
451 hand_restriction: Option<Hands>,
452 mut rng: &mut impl Rng,
453) -> Result<(Item, Option<Hands>), ModularWeaponCreationError> {
454 let result = {
455 if let Some(material_id) = material.asset_identifier() {
456 let ability_map = &AbilityMap::load().read();
458 let msm = &MaterialStatManifest::load().read();
459
460 let primary_components = PRIMARY_COMPONENT_POOL
461 .get(&(tool, material_id.to_owned()))
462 .into_iter()
463 .flatten()
464 .filter(|(_comp, hand)| compatible_handedness(hand_restriction, *hand))
465 .collect::<Vec<_>>();
466
467 let (comp, hand) = primary_components
468 .choose(&mut rng)
469 .ok_or(ModularWeaponCreationError::PrimaryComponentNotFound)?;
470 let comp = comp.duplicate(ability_map, msm);
471 Ok((comp, hand_restriction.or(*hand)))
472 } else {
473 Err(ModularWeaponCreationError::MaterialNotFound)
474 }
475 };
476
477 if let Err(err) = &result {
478 let error_str = format!(
479 "Failed to synthesize a primary component for a modular {tool:?} made of {material:?} \
480 that had a hand restriction of {hand_restriction:?}. Error: {err:?}"
481 );
482 dev_panic!(error_str)
483 }
484 result
485}
486
487pub fn generate_weapons(
488 tool: ToolKind,
489 material: Material,
490 hand_restriction: Option<Hands>,
491) -> Result<Vec<Item>, ModularWeaponCreationError> {
492 let ability_map = &AbilityMap::load().read();
494 let msm = &MaterialStatManifest::load().read();
495
496 let primaries = generate_weapon_primary_components(tool, material, hand_restriction)?;
497 let mut weapons = Vec::new();
498
499 for (comp, comp_hand) in primaries {
500 let secondaries = SECONDARY_COMPONENT_POOL
501 .get(&tool)
502 .into_iter()
503 .flatten()
504 .filter(|(_def, hand)| compatible_handedness(hand_restriction, *hand))
505 .filter(|(_def, hand)| compatible_handedness(comp_hand, *hand));
506
507 for (def, _hand) in secondaries {
508 let secondary = Item::new_from_item_base(
509 ItemBase::Simple(Arc::clone(def)),
510 Vec::new(),
511 ability_map,
512 msm,
513 );
514 let it = Item::new_from_item_base(
515 ItemBase::Modular(ModularBase::Tool),
516 vec![comp.duplicate(ability_map, msm), secondary],
517 ability_map,
518 msm,
519 );
520 weapons.push(it);
521 }
522 }
523
524 Ok(weapons)
525}
526
527pub fn random_weapon(
530 tool: ToolKind,
531 material: Material,
532 hand_restriction: Option<Hands>,
533 mut rng: &mut impl Rng,
534) -> Result<Item, ModularWeaponCreationError> {
535 let result = {
536 let ability_map = &AbilityMap::load().read();
538 let msm = &MaterialStatManifest::load().read();
539
540 let (primary_component, hand_restriction) =
541 random_weapon_primary_component(tool, material, hand_restriction, rng)?;
542
543 let secondary_components = SECONDARY_COMPONENT_POOL
544 .get(&tool)
545 .into_iter()
546 .flatten()
547 .filter(|(_def, hand)| compatible_handedness(hand_restriction, *hand))
548 .collect::<Vec<_>>();
549
550 let secondary_component = {
551 let def = &secondary_components
552 .choose(&mut rng)
553 .ok_or(ModularWeaponCreationError::SecondaryComponentNotFound)?
554 .0;
555
556 Item::new_from_item_base(
557 ItemBase::Simple(Arc::clone(def)),
558 Vec::new(),
559 ability_map,
560 msm,
561 )
562 };
563
564 Ok(Item::new_from_item_base(
566 ItemBase::Modular(ModularBase::Tool),
567 vec![primary_component, secondary_component],
568 ability_map,
569 msm,
570 ))
571 };
572 if let Err(err) = &result {
573 let error_str = format!(
574 "Failed to synthesize a modular {tool:?} made of {material:?} that had a hand \
575 restriction of {hand_restriction:?}. Error: {err:?}"
576 );
577 dev_panic!(error_str)
578 }
579 result
580}
581
582pub fn modify_name<'a>(item_name: &'a str, item: &'a Item) -> Cow<'a, str> {
583 if let ItemKind::ModularComponent(_) = &*item.kind() {
584 if let Some(material_name) = item
585 .components()
586 .iter()
587 .find_map(|comp| match &*comp.kind() {
588 #[expect(deprecated)]
589 ItemKind::Ingredient { descriptor, .. } => Some(descriptor.to_owned()),
590 _ => None,
591 })
592 {
593 Cow::Owned(format!("{} {}", material_name, item_name))
594 } else {
595 Cow::Borrowed(item_name)
596 }
597 } else {
598 Cow::Borrowed(item_name)
599 }
600}
601
602pub type ModularWeaponKey = (String, String, Hands);
605
606pub fn weapon_to_key(mod_weap: impl ItemDesc) -> ModularWeaponKey {
607 let hands = if let ItemKind::Tool(tool) = &*mod_weap.kind() {
608 tool.hands
609 } else {
610 Hands::One
611 };
612
613 match mod_weap
614 .components()
615 .iter()
616 .find_map(|comp| match &*comp.kind() {
617 ItemKind::ModularComponent(ModularComponent::ToolPrimaryComponent { .. }) => {
618 let component_id = comp.item_definition_id().itemdef_id()?.to_owned();
619 let material_id = comp.components().iter().find_map(|mat| match &*mat.kind() {
620 ItemKind::Ingredient { .. } => {
621 Some(mat.item_definition_id().itemdef_id()?.to_owned())
622 },
623 _ => None,
624 });
625 Some((component_id, material_id))
626 },
627 _ => None,
628 }) {
629 Some((component_id, Some(material_id))) => (component_id, material_id, hands),
630 Some((component_id, None)) => (component_id, String::new(), hands),
631 None => (String::new(), String::new(), hands),
632 }
633}
634
635pub type ModularWeaponComponentKey = (String, String);
638
639pub enum ModularWeaponComponentKeyError {
640 MaterialNotFound,
641}
642
643pub fn weapon_component_to_key(
644 item_def_id: &str,
645 components: &[Item],
646) -> Result<ModularWeaponComponentKey, ModularWeaponComponentKeyError> {
647 match components.iter().find_map(|mat| match &*mat.kind() {
648 ItemKind::Ingredient { .. } => Some(mat.item_definition_id().itemdef_id()?.to_owned()),
649 _ => None,
650 }) {
651 Some(material_id) => Ok((item_def_id.to_owned(), material_id)),
652 None => Err(ModularWeaponComponentKeyError::MaterialNotFound),
653 }
654}