1use crate::flip::WorldSide;
24use crate::items::ItemType;
25use crate::prelude::*;
26
27use strum::IntoEnumIterator;
28use strum_macros::{Display, EnumCount, EnumIter, EnumString};
29
30use std::collections::HashMap;
31
32#[declare]
33pub type StoneTemplateId = Uuid;
34
35#[derive(
46 Clone,
47 Copy,
48 Debug,
49 Default,
50 Serialize,
51 Deserialize,
52 PartialEq,
53 Eq,
54 PartialOrd,
55 Ord,
56 Hash,
57 JsonSchema,
58 Tsify,
59 Display,
60 EnumString,
61 EnumIter,
62 EnumCount,
63)]
64#[tsify(from_wasm_abi, into_wasm_abi)]
65pub enum StoneTier {
66 #[default]
67 Common,
68 Rare,
69 Epic,
70 Legendary,
71}
72
73#[derive(
75 Clone,
76 Copy,
77 Debug,
78 Default,
79 Serialize,
80 Deserialize,
81 PartialEq,
82 Eq,
83 Hash,
84 JsonSchema,
85 Tsify,
86 Display,
87 EnumString,
88 EnumIter,
89)]
90#[tsify(from_wasm_abi, into_wasm_abi)]
91pub enum StoneKind {
92 #[default]
94 Trigger,
95 Effect,
97}
98
99#[derive(
112 Clone,
113 Copy,
114 Debug,
115 Serialize,
116 Deserialize,
117 PartialEq,
118 Eq,
119 Hash,
120 JsonSchema,
121 Tsify,
122 Display,
123 EnumString,
124 EnumIter,
125)]
126#[tsify(from_wasm_abi, into_wasm_abi)]
127pub enum StoneSocketSlot {
128 Trigger,
129 RealEffect,
130 FantasyEffect,
131}
132
133impl StoneSocketSlot {
134 pub const fn accepts(self) -> StoneKind {
137 match self {
138 Self::Trigger => StoneKind::Trigger,
139 Self::RealEffect | Self::FantasyEffect => StoneKind::Effect,
140 }
141 }
142
143 pub const fn world_side(self) -> Option<WorldSide> {
146 match self {
147 Self::Trigger => None,
148 Self::RealEffect => Some(WorldSide::Real),
149 Self::FantasyEffect => Some(WorldSide::Fantasy),
150 }
151 }
152
153 pub const fn effect_for(side: WorldSide) -> Self {
155 match side {
156 WorldSide::Real => Self::RealEffect,
157 WorldSide::Fantasy => Self::FantasyEffect,
158 }
159 }
160}
161
162#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, JsonSchema, Tsify)]
169#[tsify(from_wasm_abi, into_wasm_abi)]
170pub struct StoneSocketKey {
171 pub item_type: ItemType,
172 pub socket: StoneSocketSlot,
173}
174
175impl StoneSocketKey {
176 pub fn new(item_type: ItemType, socket: StoneSocketSlot) -> Option<Self> {
179 item_type
180 .supports_world_side()
181 .then_some(Self { item_type, socket })
182 }
183
184 pub fn all() -> Vec<Self> {
186 ItemType::iter()
187 .filter(|item_type| item_type.supports_world_side())
188 .flat_map(|item_type| {
189 StoneSocketSlot::iter().map(move |socket| Self { item_type, socket })
190 })
191 .collect()
192 }
193}
194
195#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
209#[tsify(from_wasm_abi, into_wasm_abi)]
210pub struct Stone {
211 pub template_id: StoneTemplateId,
213 pub tier: StoneTier,
214 pub level: i64,
215 pub copies: i64,
218 pub socket: Option<StoneSocketKey>,
222}
223
224impl Stone {
225 pub fn new(template_id: StoneTemplateId, tier: StoneTier) -> Self {
227 Self {
228 template_id,
229 tier,
230 level: 1,
231 copies: 0,
232 socket: None,
233 }
234 }
235
236 pub const fn is_socketed(&self) -> bool {
237 self.socket.is_some()
238 }
239
240 pub const fn quality(&self) -> (StoneTier, i64) {
247 (self.tier, self.level)
248 }
249}
250
251#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
258pub struct UpgradedStonesMap(pub HashMap<StoneTemplateId, (i64, i64)>);
259
260impl UpgradedStonesMap {
261 pub fn insert(&mut self, id: StoneTemplateId, levels: (i64, i64)) {
262 self.0.insert(id, levels);
263 }
264
265 pub fn is_empty(&self) -> bool {
266 self.0.is_empty()
267 }
268}
269
270#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
273#[tsify(from_wasm_abi, into_wasm_abi)]
274pub struct StoneInventory {
275 pub trigger_stones: Vec<Stone>,
276 pub effect_stones: Vec<Stone>,
277}
278
279#[derive(Debug, thiserror::Error, PartialEq, Eq)]
283pub enum StoneError {
284 #[error("the player owns no stone of template {0}")]
285 UnknownStone(StoneTemplateId),
286 #[error("{item_type:?} has no stone sockets — only the five two-sided slots do")]
287 SlotHasNoSockets { item_type: ItemType },
288 #[error("socket {socket} takes a {expected} stone, not a {actual} one")]
289 WrongCollection {
290 socket: StoneSocketSlot,
291 expected: StoneKind,
292 actual: StoneKind,
293 },
294 #[error("socket {socket:?} on {item_type:?} is not unlocked yet")]
295 SocketLocked {
296 item_type: ItemType,
297 socket: StoneSocketSlot,
298 },
299 #[error("stone {0} is already socketed — a template holds at most one socket")]
302 AlreadySocketed(StoneTemplateId),
303 #[error("stone {0} is not socketed")]
304 NotSocketed(StoneTemplateId),
305 #[error("upgrading stone {stone} to level {level} needs {required} copies, {held} banked")]
306 NotEnoughCopies {
307 stone: StoneTemplateId,
308 level: i64,
309 required: i64,
310 held: i64,
311 },
312 #[error("stone {0} is already at the maximum level {1}")]
313 AlreadyMaxLevel(StoneTemplateId, i64),
314 #[error("the upgrade ladder has no cost for level {0}")]
318 NoUpgradeStep(i64),
319}
320
321impl StoneInventory {
322 pub fn collection(&self, kind: StoneKind) -> &Vec<Stone> {
323 match kind {
324 StoneKind::Trigger => &self.trigger_stones,
325 StoneKind::Effect => &self.effect_stones,
326 }
327 }
328
329 pub fn collection_mut(&mut self, kind: StoneKind) -> &mut Vec<Stone> {
330 match kind {
331 StoneKind::Trigger => &mut self.trigger_stones,
332 StoneKind::Effect => &mut self.effect_stones,
333 }
334 }
335
336 pub fn all(&self) -> impl Iterator<Item = (StoneKind, &Stone)> {
337 self.trigger_stones
338 .iter()
339 .map(|s| (StoneKind::Trigger, s))
340 .chain(self.effect_stones.iter().map(|s| (StoneKind::Effect, s)))
341 }
342
343 pub fn kind_of(&self, template_id: StoneTemplateId) -> Option<StoneKind> {
348 self.all()
349 .find(|(_, stone)| stone.template_id == template_id)
350 .map(|(kind, _)| kind)
351 }
352
353 pub fn get(&self, template_id: StoneTemplateId) -> Option<&Stone> {
354 self.all()
355 .find(|(_, s)| s.template_id == template_id)
356 .map(|(_, s)| s)
357 }
358
359 fn get_mut(&mut self, template_id: StoneTemplateId) -> Option<&mut Stone> {
360 self.trigger_stones
361 .iter_mut()
362 .chain(self.effect_stones.iter_mut())
363 .find(|s| s.template_id == template_id)
364 }
365
366 pub fn grant(&mut self, kind: StoneKind, template_id: StoneTemplateId, tier: StoneTier) {
374 if let Some(stone) = self.get_mut(template_id) {
375 stone.copies += 1;
376 return;
377 }
378 self.collection_mut(kind)
379 .push(Stone::new(template_id, tier));
380 }
381
382 pub fn socketed(&self, key: StoneSocketKey) -> Option<&Stone> {
384 self.all()
385 .find(|(_, stone)| stone.socket == Some(key))
386 .map(|(_, stone)| stone)
387 }
388
389 pub fn insert(
400 &mut self,
401 template_id: StoneTemplateId,
402 key: StoneSocketKey,
403 unlocked: bool,
404 ) -> Result<(), StoneError> {
405 if !key.item_type.supports_world_side() {
406 return Err(StoneError::SlotHasNoSockets {
407 item_type: key.item_type,
408 });
409 }
410 if !unlocked {
411 return Err(StoneError::SocketLocked {
412 item_type: key.item_type,
413 socket: key.socket,
414 });
415 }
416 let kind = self
417 .kind_of(template_id)
418 .ok_or(StoneError::UnknownStone(template_id))?;
419 if kind != key.socket.accepts() {
420 return Err(StoneError::WrongCollection {
421 socket: key.socket,
422 expected: key.socket.accepts(),
423 actual: kind,
424 });
425 }
426 match self.get(template_id) {
430 None => return Err(StoneError::UnknownStone(template_id)),
431 Some(stone) if stone.is_socketed() => {
432 return Err(StoneError::AlreadySocketed(template_id));
433 }
434 Some(_) => {}
435 }
436
437 if let Some(displaced) = self.socketed(key).map(|stone| stone.template_id)
442 && let Some(stone) = self.get_mut(displaced)
443 {
444 stone.socket = None;
445 }
446
447 let stone = self
448 .get_mut(template_id)
449 .ok_or(StoneError::UnknownStone(template_id))?;
450 stone.socket = Some(key);
451 Ok(())
452 }
453
454 pub fn remove(&mut self, template_id: StoneTemplateId) -> Result<StoneSocketKey, StoneError> {
458 let stone = self
459 .get_mut(template_id)
460 .ok_or(StoneError::UnknownStone(template_id))?;
461 stone
462 .socket
463 .take()
464 .ok_or(StoneError::NotSocketed(template_id))
465 }
466
467 pub fn quick_equip(
487 &mut self,
488 item_type: ItemType,
489 is_unlocked: &dyn Fn(StoneSocketSlot) -> bool,
490 catalog_order: &dyn Fn(StoneKind) -> Vec<StoneTemplateId>,
491 ) -> Vec<(StoneSocketSlot, StoneTemplateId)> {
492 let mut filled = Vec::new();
493 if !item_type.supports_world_side() {
494 return filled;
495 }
496 for socket in StoneSocketSlot::iter() {
497 let Some(key) = StoneSocketKey::new(item_type, socket) else {
498 continue;
499 };
500 if !is_unlocked(socket) {
501 continue;
502 }
503 if self.socketed(key).is_some() {
504 continue;
505 }
506 let kind = socket.accepts();
507 let catalog = catalog_order(kind);
508 let Some(template_id) = self
511 .collection(kind)
512 .iter()
513 .filter(|stone| !stone.is_socketed())
514 .filter_map(|stone| {
515 let rank = catalog.iter().position(|id| *id == stone.template_id)?;
516 Some((stone.quality(), std::cmp::Reverse(rank), stone.template_id))
517 })
518 .max()
519 .map(|(_, _, template_id)| template_id)
520 else {
521 continue;
522 };
523 match self.insert(template_id, key, true) {
526 Ok(()) => filled.push((socket, template_id)),
527 Err(err) => tracing::error!("QuickEquipStones picked an illegal stone: {err}"),
528 }
529 }
530 filled
531 }
532
533 pub fn upgrade_all(
551 &mut self,
552 kind: StoneKind,
553 max_level: i64,
554 cost: &dyn Fn(i64) -> Option<i64>,
555 ) -> Vec<(StoneTemplateId, i64, i64)> {
556 let templates: Vec<StoneTemplateId> = self
557 .all()
558 .filter(|(stone_kind, _)| *stone_kind == kind)
559 .map(|(_, s)| s.template_id)
560 .collect();
561 let mut upgraded = Vec::new();
562 for template_id in templates {
563 let Some(started_at) = self.get(template_id).map(|stone| stone.level) else {
566 continue;
567 };
568 let mut reached = None;
569 while let Some(stone) = self.get(template_id) {
572 let Some(required) = cost(stone.level + 1) else {
573 break;
574 };
575 match self.upgrade(template_id, required, max_level) {
576 Ok(level) => reached = Some(level),
577 Err(_) => break,
578 }
579 }
580 if let Some(level) = reached {
581 upgraded.push((template_id, started_at, level));
582 }
583 }
584 upgraded
585 }
586
587 pub fn upgrade(
599 &mut self,
600 template_id: StoneTemplateId,
601 required: i64,
602 max_level: i64,
603 ) -> Result<i64, StoneError> {
604 let stone = self
605 .get_mut(template_id)
606 .ok_or(StoneError::UnknownStone(template_id))?;
607
608 if stone.level >= max_level {
609 return Err(StoneError::AlreadyMaxLevel(template_id, max_level));
610 }
611 let next_level = stone.level + 1;
612 if stone.copies < required {
613 return Err(StoneError::NotEnoughCopies {
614 stone: template_id,
615 level: next_level,
616 required,
617 held: stone.copies,
618 });
619 }
620
621 stone.copies -= required;
622 stone.level = next_level;
623 Ok(next_level)
624 }
625}
626
627#[cfg(test)]
628mod tests {
629 use super::*;
630
631 const TRIGGER: u128 = 100;
632 const OTHER_TRIGGER: u128 = 101;
633 const EFFECT: u128 = 200;
634
635 fn template(n: u128) -> StoneTemplateId {
636 Uuid::from_u128(n)
637 }
638
639 fn stone(template_id: u128, copies: i64) -> Stone {
640 let mut stone = Stone::new(template(template_id), StoneTier::Common);
641 stone.copies = copies;
642 stone
643 }
644
645 fn inventory() -> StoneInventory {
648 StoneInventory {
649 trigger_stones: vec![stone(TRIGGER, 2)],
650 effect_stones: vec![stone(EFFECT, 2)],
651 }
652 }
653
654 #[test]
655 fn only_the_five_two_sided_slots_have_sockets() {
656 let sockets = StoneSocketKey::all();
657 assert_eq!(sockets.len(), 15, "5 two-sided slots × 3 sockets");
658 for socket in &sockets {
659 assert!(socket.item_type.supports_world_side());
660 }
661 assert!(StoneSocketKey::new(ItemType::Ring, StoneSocketSlot::Trigger).is_none());
662 }
663
664 #[test]
665 fn the_trigger_is_shared_and_the_two_effect_sides_are_separate() {
666 assert_eq!(StoneSocketSlot::Trigger.world_side(), None);
667 assert_eq!(
668 StoneSocketSlot::effect_for(WorldSide::Real),
669 StoneSocketSlot::RealEffect
670 );
671 assert_eq!(
672 StoneSocketSlot::effect_for(WorldSide::Fantasy),
673 StoneSocketSlot::FantasyEffect
674 );
675 assert_ne!(
676 StoneSocketSlot::RealEffect,
677 StoneSocketSlot::FantasyEffect,
678 "each side keeps its own effect"
679 );
680 }
681
682 #[test]
683 fn a_trigger_stone_does_not_fit_an_effect_socket() {
684 let mut inv = inventory();
685 let key = StoneSocketKey::new(ItemType::Weapon, StoneSocketSlot::RealEffect).unwrap();
686 let err = inv.insert(template(TRIGGER), key, true).unwrap_err();
687 assert!(matches!(err, StoneError::WrongCollection { .. }));
688
689 let key = StoneSocketKey::new(ItemType::Weapon, StoneSocketSlot::Trigger).unwrap();
690 let err = inv.insert(template(EFFECT), key, true).unwrap_err();
691 assert!(matches!(err, StoneError::WrongCollection { .. }));
692 }
693
694 #[test]
695 fn a_locked_socket_refuses_the_stone() {
696 let mut inv = inventory();
697 let key = StoneSocketKey::new(ItemType::Weapon, StoneSocketSlot::Trigger).unwrap();
698 assert!(matches!(
699 inv.insert(template(TRIGGER), key, false),
700 Err(StoneError::SocketLocked { .. })
701 ));
702 assert!(inv.get(template(TRIGGER)).unwrap().socket.is_none());
703 }
704
705 #[test]
706 fn a_removed_stone_returns_to_inventory_intact() {
707 let mut inv = inventory();
708 let before = inv.get(template(TRIGGER)).unwrap().clone();
709 let key = StoneSocketKey::new(ItemType::Weapon, StoneSocketSlot::Trigger).unwrap();
710
711 inv.insert(template(TRIGGER), key, true).unwrap();
712 assert_eq!(
713 inv.socketed(key).map(|s| s.template_id),
714 Some(template(TRIGGER))
715 );
716
717 assert_eq!(inv.remove(template(TRIGGER)).unwrap(), key);
718 assert_eq!(inv.trigger_stones.len(), 1, "nothing was consumed");
719 assert_eq!(
720 inv.get(template(TRIGGER)).unwrap(),
721 &before,
722 "the stone comes back exactly as it went in, copies included"
723 );
724 assert!(inv.socketed(key).is_none());
725 }
726
727 #[test]
728 fn a_socket_holds_one_stone_at_a_time() {
729 let mut inv = inventory();
730 inv.grant(StoneKind::Trigger, template(OTHER_TRIGGER), StoneTier::Rare);
731 let key = StoneSocketKey::new(ItemType::Weapon, StoneSocketSlot::Trigger).unwrap();
732 inv.insert(template(TRIGGER), key, true).unwrap();
733
734 inv.insert(template(OTHER_TRIGGER), key, true).unwrap();
737
738 assert_eq!(
739 inv.socketed(key).unwrap().template_id,
740 template(OTHER_TRIGGER)
741 );
742 assert_eq!(inv.get(template(TRIGGER)).unwrap().socket, None);
743 assert_eq!(
744 inv.all().filter(|(_, s)| s.socket == Some(key)).count(),
745 1,
746 "a swap must not leave two stones claiming the same socket"
747 );
748 }
749
750 #[test]
754 fn one_template_cannot_be_socketed_in_two_slots() {
755 let mut inv = inventory();
756 let weapon = StoneSocketKey::new(ItemType::Weapon, StoneSocketSlot::Trigger).unwrap();
757 let gloves = StoneSocketKey::new(ItemType::Gloves, StoneSocketSlot::Trigger).unwrap();
758
759 inv.insert(template(TRIGGER), weapon, true).unwrap();
760 assert_eq!(
761 inv.insert(template(TRIGGER), gloves, true),
762 Err(StoneError::AlreadySocketed(template(TRIGGER))),
763 );
764 assert_eq!(
765 inv.get(template(TRIGGER)).unwrap().socket,
766 Some(weapon),
767 "the refused second insert left the first socket alone"
768 );
769 assert!(inv.socketed(gloves).is_none());
770 }
771
772 #[test]
775 fn a_repeat_drop_banks_a_copy_instead_of_a_second_instance() {
776 let mut inv = StoneInventory::default();
777
778 inv.grant(StoneKind::Trigger, template(TRIGGER), StoneTier::Common);
779 assert_eq!(inv.trigger_stones.len(), 1);
780 assert_eq!(inv.trigger_stones[0].level, 1);
781 assert_eq!(
782 inv.trigger_stones[0].copies, 0,
783 "the first copy IS the instance"
784 );
785
786 inv.grant(StoneKind::Trigger, template(TRIGGER), StoneTier::Common);
787 inv.grant(StoneKind::Trigger, template(TRIGGER), StoneTier::Common);
788 assert_eq!(inv.trigger_stones.len(), 1, "still one instance");
789 assert_eq!(inv.trigger_stones[0].copies, 2);
790
791 inv.grant(StoneKind::Effect, template(EFFECT), StoneTier::Rare);
793 assert_eq!(inv.trigger_stones.len(), 1);
794 assert_eq!(inv.effect_stones.len(), 1);
795 }
796
797 #[test]
799 fn a_repeat_drop_leaves_the_socket_alone() {
800 let mut inv = inventory();
801 let key = StoneSocketKey::new(ItemType::Weapon, StoneSocketSlot::Trigger).unwrap();
802 inv.insert(template(TRIGGER), key, true).unwrap();
803
804 inv.grant(StoneKind::Trigger, template(TRIGGER), StoneTier::Common);
805
806 let stone = inv.get(template(TRIGGER)).unwrap();
807 assert_eq!(stone.socket, Some(key));
808 assert_eq!(stone.copies, 3);
809 }
810
811 #[test]
814 fn an_upgrade_spends_copies_and_keeps_the_stone() {
815 let mut inv = inventory();
816 inv.trigger_stones[0].copies = 5;
817
818 assert_eq!(inv.upgrade(template(TRIGGER), 2, 10), Ok(2));
819 let stone = inv.get(template(TRIGGER)).unwrap();
820 assert_eq!(stone.level, 2);
821 assert_eq!(stone.copies, 3, "exactly the rung's cost was spent");
822 assert_eq!(stone.template_id, template(TRIGGER));
823 assert_eq!(stone.tier, StoneTier::Common);
824
825 assert_eq!(inv.upgrade(template(TRIGGER), 3, 10), Ok(3));
827 assert_eq!(inv.get(template(TRIGGER)).unwrap().copies, 0);
828 assert_eq!(
829 inv.effect_stones[0].copies, 2,
830 "the other collection is untouched"
831 );
832 }
833
834 #[test]
837 fn a_socketed_stone_upgrades_in_place() {
838 let mut inv = inventory();
839 let key = StoneSocketKey::new(ItemType::Weapon, StoneSocketSlot::Trigger).unwrap();
840 inv.insert(template(TRIGGER), key, true).unwrap();
841
842 assert_eq!(inv.upgrade(template(TRIGGER), 2, 10), Ok(2));
843
844 let stone = inv.get(template(TRIGGER)).unwrap();
845 assert_eq!(stone.level, 2);
846 assert_eq!(stone.socket, Some(key), "the socket survived the upgrade");
847 assert_eq!(inv.socketed(key).map(|s| s.level), Some(2));
848 }
849
850 #[test]
852 fn a_short_bank_is_refused_without_spending_anything() {
853 let mut inv = inventory();
854 assert_eq!(
855 inv.upgrade(template(TRIGGER), 3, 10),
856 Err(StoneError::NotEnoughCopies {
857 stone: template(TRIGGER),
858 level: 2,
859 required: 3,
860 held: 2,
861 })
862 );
863
864 let stone = inv.get(template(TRIGGER)).unwrap();
865 assert_eq!(stone.copies, 2, "nothing was spent");
866 assert_eq!(stone.level, 1);
867 }
868
869 #[test]
870 fn an_upgrade_stops_at_the_maximum_level() {
871 let mut inv = inventory();
872 inv.trigger_stones[0].level = 10;
873 inv.trigger_stones[0].copies = 99;
874
875 assert_eq!(
876 inv.upgrade(template(TRIGGER), 2, 10),
877 Err(StoneError::AlreadyMaxLevel(template(TRIGGER), 10))
878 );
879 assert_eq!(inv.trigger_stones[0].copies, 99, "nothing was spent");
880 }
881
882 #[test]
883 fn upgrading_a_stone_the_player_does_not_own_is_refused() {
884 let mut inv = inventory();
885 assert_eq!(
886 inv.upgrade(template(OTHER_TRIGGER), 2, 10),
887 Err(StoneError::UnknownStone(template(OTHER_TRIGGER)))
888 );
889 }
890
891 #[test]
894 fn copies_never_cross_between_the_two_collections() {
895 let mut inv = StoneInventory::default();
896 inv.grant(StoneKind::Trigger, template(TRIGGER), StoneTier::Common);
897 for _ in 0..5 {
898 inv.grant(StoneKind::Effect, template(EFFECT), StoneTier::Rare);
899 }
900
901 assert!(matches!(
903 inv.upgrade(template(TRIGGER), 2, 10),
904 Err(StoneError::NotEnoughCopies { held: 0, .. })
905 ));
906 assert_eq!(inv.effect_stones[0].copies, 4, "the effect bank is intact");
907
908 assert_eq!(inv.upgrade(template(EFFECT), 2, 10), Ok(2));
910 assert_eq!(inv.effect_stones[0].copies, 2);
911 assert_eq!(inv.trigger_stones[0].level, 1);
912 }
913
914 #[test]
917 fn quality_ranks_tier_above_level() {
918 let mut common = Stone::new(template(1), StoneTier::Common);
919 common.level = 10;
920 let legendary = Stone::new(template(2), StoneTier::Legendary);
921 assert!(legendary.quality() > common.quality());
922
923 let mut rare_low = Stone::new(template(3), StoneTier::Rare);
925 rare_low.level = 2;
926 let mut rare_high = Stone::new(template(4), StoneTier::Rare);
927 rare_high.level = 3;
928 assert!(rare_high.quality() > rare_low.quality());
929 }
930
931 fn tiered(template_id: u128, kind: StoneKind, tier: StoneTier) -> Stone {
932 let _ = kind;
933 Stone::new(template(template_id), tier)
934 }
935
936 #[test]
939 fn quick_equip_fills_open_sockets_of_one_item_only() {
940 let mut inv = StoneInventory {
941 trigger_stones: vec![
942 tiered(1, StoneKind::Trigger, StoneTier::Common),
943 tiered(2, StoneKind::Trigger, StoneTier::Legendary),
944 ],
945 effect_stones: vec![
946 tiered(10, StoneKind::Effect, StoneTier::Rare),
947 tiered(11, StoneKind::Effect, StoneTier::Epic),
948 tiered(12, StoneKind::Effect, StoneTier::Common),
949 ],
950 };
951 let gloves = StoneSocketKey::new(ItemType::Gloves, StoneSocketSlot::RealEffect).unwrap();
953 inv.insert(template(11), gloves, true).unwrap();
954
955 let order = |kind: StoneKind| -> Vec<StoneTemplateId> {
960 match kind {
961 StoneKind::Trigger => vec![template(1), template(2)],
962 StoneKind::Effect => vec![template(10), template(11), template(12)],
963 }
964 };
965 let filled = inv.quick_equip(
966 ItemType::Weapon,
967 &|socket| socket != StoneSocketSlot::FantasyEffect,
968 &order,
969 );
970
971 assert_eq!(
972 filled,
973 vec![
974 (StoneSocketSlot::Trigger, template(2)),
975 (StoneSocketSlot::RealEffect, template(10)),
976 ],
977 "highest-tier trigger, highest-tier FREE effect"
978 );
979 assert!(
980 inv.socketed(
981 StoneSocketKey::new(ItemType::Weapon, StoneSocketSlot::FantasyEffect).unwrap()
982 )
983 .is_none(),
984 "a locked socket is never filled"
985 );
986 assert_eq!(
987 inv.get(template(11)).unwrap().socket,
988 Some(gloves),
989 "a stone worn on other gear is not moved"
990 );
991 }
992
993 #[test]
996 fn quick_equip_leaves_a_socket_empty_when_nothing_fits() {
997 let mut inv = StoneInventory {
998 trigger_stones: vec![tiered(1, StoneKind::Trigger, StoneTier::Common)],
999 effect_stones: Vec::new(),
1000 };
1001 let trigger_key = StoneSocketKey::new(ItemType::Weapon, StoneSocketSlot::Trigger).unwrap();
1002 inv.insert(template(1), trigger_key, true).unwrap();
1003
1004 let order = |kind: StoneKind| -> Vec<StoneTemplateId> {
1007 match kind {
1008 StoneKind::Trigger => vec![template(1)],
1009 StoneKind::Effect => Vec::new(),
1010 }
1011 };
1012 let filled = inv.quick_equip(ItemType::Weapon, &|_| true, &order);
1013
1014 assert!(filled.is_empty(), "nothing to do, and nothing broke");
1015 assert_eq!(inv.get(template(1)).unwrap().socket, Some(trigger_key));
1016
1017 assert!(
1019 inv.quick_equip(ItemType::Ring, &|_| true, &order)
1020 .is_empty()
1021 );
1022 }
1023
1024 #[test]
1029 fn two_socketed_stones_can_trade_places() {
1030 let mut inv = StoneInventory {
1031 trigger_stones: Vec::new(),
1032 effect_stones: vec![
1033 tiered(1, StoneKind::Effect, StoneTier::Rare),
1034 tiered(2, StoneKind::Effect, StoneTier::Epic),
1035 ],
1036 };
1037 let left = StoneSocketKey::new(ItemType::Weapon, StoneSocketSlot::RealEffect).unwrap();
1038 let right = StoneSocketKey::new(ItemType::Weapon, StoneSocketSlot::FantasyEffect).unwrap();
1039 inv.insert(template(1), left, true).unwrap();
1040 inv.insert(template(2), right, true).unwrap();
1041
1042 inv.remove(template(1)).unwrap();
1044 inv.remove(template(2)).unwrap();
1045 inv.insert(template(1), right, true).unwrap();
1046 inv.insert(template(2), left, true).unwrap();
1047
1048 assert_eq!(inv.socketed(right).unwrap().template_id, template(1));
1049 assert_eq!(
1050 inv.socketed(left).unwrap().template_id,
1051 template(2),
1052 "the displaced stone takes the vacated socket, it does not fall out"
1053 );
1054 }
1055
1056 #[test]
1059 fn quick_equip_leaves_an_occupied_socket_alone() {
1060 let mut inv = StoneInventory {
1061 trigger_stones: vec![
1062 tiered(1, StoneKind::Trigger, StoneTier::Common),
1063 tiered(2, StoneKind::Trigger, StoneTier::Legendary),
1064 ],
1065 effect_stones: Vec::new(),
1066 };
1067 let key = StoneSocketKey::new(ItemType::Weapon, StoneSocketSlot::Trigger).unwrap();
1068 inv.insert(template(1), key, true).unwrap();
1069
1070 let order = |kind: StoneKind| -> Vec<StoneTemplateId> {
1071 match kind {
1072 StoneKind::Trigger => vec![template(2), template(1)],
1073 StoneKind::Effect => Vec::new(),
1074 }
1075 };
1076 let filled = inv.quick_equip(
1077 ItemType::Weapon,
1078 &|socket| socket == StoneSocketSlot::Trigger,
1079 &order,
1080 );
1081
1082 assert!(
1083 filled.is_empty(),
1084 "an occupied socket is left as the player arranged it"
1085 );
1086 assert_eq!(inv.socketed(key).unwrap().template_id, template(1));
1087 assert_eq!(
1088 inv.get(template(2)).unwrap().socket,
1089 None,
1090 "the Legendary spare stays in the collection"
1091 );
1092 }
1093
1094 #[test]
1097 fn upgrade_all_climbs_several_levels_in_one_call() {
1098 let ladder = |level: i64| match level {
1099 2 => Some(2),
1100 3 => Some(3),
1101 4 => Some(5),
1102 _ => None,
1103 };
1104 let mut inv = StoneInventory {
1105 trigger_stones: vec![stone(TRIGGER, 5)],
1107 effect_stones: vec![stone(EFFECT, 1)],
1109 };
1110
1111 let upgraded = inv.upgrade_all(StoneKind::Trigger, 4, &ladder);
1112
1113 assert_eq!(
1114 upgraded,
1115 vec![(template(TRIGGER), 1, 3)],
1116 "reported as from-level 1 to level 3, not just where it landed"
1117 );
1118 let trigger = inv.get(template(TRIGGER)).unwrap();
1119 assert_eq!(trigger.level, 3);
1120 assert_eq!(trigger.copies, 0);
1121 let effect = inv.get(template(EFFECT)).unwrap();
1122 assert_eq!(effect.level, 1, "a short bank buys nothing");
1123 assert_eq!(effect.copies, 1, "and spends nothing");
1124 }
1125
1126 #[test]
1129 fn upgrade_all_touches_only_the_requested_kind() {
1130 let mut inv = inventory();
1131 inv.trigger_stones[0].copies = 1_000;
1132 inv.effect_stones[0].copies = 1_000;
1133
1134 let upgraded = inv.upgrade_all(StoneKind::Trigger, 3, &|_| Some(1));
1135
1136 assert_eq!(
1137 upgraded,
1138 vec![(template(TRIGGER), 1, 3)],
1139 "only the Trigger catalog is reported"
1140 );
1141 let effect = inv.get(template(EFFECT)).unwrap();
1142 assert_eq!(effect.level, 1, "the Effect stone did not move");
1143 assert_eq!(effect.copies, 1_000, "and its bank is untouched");
1144
1145 let upgraded = inv.upgrade_all(StoneKind::Effect, 3, &|_| Some(1));
1146
1147 assert_eq!(upgraded, vec![(template(EFFECT), 1, 3)]);
1148 assert_eq!(
1149 inv.get(template(TRIGGER)).unwrap().level,
1150 3,
1151 "the Trigger stone kept the level the first call gave it"
1152 );
1153 }
1154
1155 #[test]
1157 fn upgrade_all_respects_the_cap_and_keeps_sockets() {
1158 let mut inv = inventory();
1159 inv.trigger_stones[0].copies = 1_000;
1160 let key = StoneSocketKey::new(ItemType::Weapon, StoneSocketSlot::Trigger).unwrap();
1161 inv.insert(template(TRIGGER), key, true).unwrap();
1162
1163 let upgraded = inv.upgrade_all(StoneKind::Trigger, 3, &|_| Some(1));
1164
1165 assert_eq!(upgraded, vec![(template(TRIGGER), 1, 3)]);
1166 let trigger = inv.get(template(TRIGGER)).unwrap();
1167 assert_eq!(trigger.level, 3, "stops at the cap, not at the bank");
1168 assert_eq!(trigger.socket, Some(key), "the socket survived");
1169 }
1170}