1use chrono::TimeDelta;
2use leptos::{SignalGetUntracked, SignalUpdateUntracked};
3use serde::{Deserialize, Serialize};
4use std::cell::RefCell;
5use std::collections::HashMap;
6use std::sync::{Arc, Mutex};
7
8use super::*;
9
10#[derive(Debug, Default, Clone, Serialize, Deserialize)]
11pub struct CountableStore {
12 owner: uuid::Uuid,
13 store: HashMap<CountableId, Countable>,
14 selection: Vec<CountableId>,
15 is_changed: RefCell<bool>,
16}
17
18impl CountableStore {
19 pub fn new(owner: uuid::Uuid, store: HashMap<CountableId, Countable>) -> Self {
20 Self {
21 owner,
22 store,
23 ..Default::default()
24 }
25 }
26
27 pub fn owner(&self) -> uuid::Uuid {
28 self.owner
29 }
30
31 pub fn merge_checked(&mut self, other: Self) -> Result<(), AppError> {
32 for (id, other_c) in other.store {
33 if other_c.is_archived() {
34 self.store.insert(id, other_c);
35 } else if let Some(c) = self.get(&id)
36 && (c.last_edit_checked()? > other_c.last_edit_checked()? || c.is_archived())
37 {
38 continue;
39 } else {
40 self.store.insert(id, other_c);
41 }
42 }
43
44 self.is_changed.replace(true);
45
46 Ok(())
47 }
48
49 pub fn merge(&mut self, other: Self) {
50 self.merge_checked(other).unwrap()
51 }
52
53 pub fn contains(&self, countable: &CountableId) -> bool {
54 self.store.contains_key(countable)
55 }
56
57 pub fn get(&self, countable: &CountableId) -> Option<Countable> {
58 self.store.get(countable).cloned()
59 }
60
61 pub fn len(&self) -> usize {
62 self.store.len()
63 }
64
65 pub fn new_countable_checked(
66 &mut self,
67 name: &str,
68 kind: CountableKind,
69 parent: Option<CountableId>,
70 ) -> Result<CountableId, AppError> {
71 let countable = Countable::new(name, kind, self.owner, parent);
72 let key = countable.clone().into();
73 self.store.insert(key, countable);
74 if let Some(parent) = parent {
75 self.get(&parent)
76 .ok_or(AppError::CountableNotFound)?
77 .add_child_checked(key)?
78 }
79
80 self.is_changed.replace(true);
81
82 Ok(key)
83 }
84
85 pub fn new_countable(
86 &mut self,
87 name: &str,
88 kind: CountableKind,
89 parent: Option<CountableId>,
90 ) -> CountableId {
91 self.new_countable_checked(name, kind, parent).unwrap()
92 }
93
94 pub fn filter(&self, filter: impl Fn(&Countable) -> bool) -> Self {
95 let mut store = self.raw_filter(filter);
96 for i in store.store.clone().into_keys() {
98 let mut id = i;
99 while let Some(p) = self.parent(&id) {
100 id = p.uuid().into();
101 store.store.insert(id, p);
102 }
103 }
104
105 store
106 }
107
108 pub fn raw_filter(&self, filter: impl Fn(&Countable) -> bool) -> Self {
109 let store: HashMap<CountableId, Countable> = self
110 .store
111 .iter()
112 .filter(|(_, b)| filter(b))
113 .map(|(a, b)| (*a, b.clone()))
114 .collect();
115
116 Self {
117 owner: self.owner,
118 store,
119 selection: self.selection.clone(),
120 ..Default::default()
121 }
122 }
123
124 pub fn archive(&mut self, countable: &CountableId) -> Option<Countable> {
125 for child in self.children_checked(countable).ok()? {
126 self.archive(&child.uuid_checked().ok()?.into())?;
127 }
128
129 match self.get(countable)? {
130 Countable::Counter(c) => c.lock().ok()?.is_deleted = true,
131 Countable::Phase(p) => p.lock().ok()?.is_deleted = true,
132 Countable::Chain(_) => todo!(),
133 }
134
135 self.get(countable)
136 }
137
138 pub fn root_nodes(&self) -> Vec<Countable> {
139 self.store
140 .values()
141 .filter(|v| self.parent(&v.uuid().into()).is_none())
142 .cloned()
143 .collect()
144 }
145
146 pub fn nodes(&self) -> Vec<Countable> {
147 self.store.values().cloned().collect()
148 }
149
150 pub fn has_child(&self, countable: &CountableId, child: &CountableId) -> bool {
151 self.children(countable)
152 .into_iter()
153 .map(CountableId::from)
154 .collect::<Vec<_>>()
155 .contains(child)
156 }
157
158 pub fn children_checked(&self, countable: &CountableId) -> Result<Vec<Countable>, AppError> {
159 Ok(
160 match self
161 .store
162 .get(countable)
163 .ok_or(AppError::CountableNotFound)?
164 {
165 Countable::Counter(c) => {
166 let children = c.lock()?.children.clone();
167 children
168 .into_iter()
169 .filter_map(|id| self.store.get(&id).cloned())
170 .collect()
171 }
172 _ => Vec::new(),
173 },
174 )
175 }
176
177 pub fn children(&self, countable: &CountableId) -> Vec<Countable> {
178 self.children_checked(countable).unwrap()
179 }
180
181 pub fn parent_checked(&self, countable: &CountableId) -> Result<Option<Countable>, AppError> {
182 Ok(match self.store.get(countable) {
183 Some(Countable::Counter(c)) => {
184 c.lock()?.parent.and_then(|id| self.store.get(&id)).cloned()
185 }
186 Some(Countable::Phase(p)) => self.store.get(&p.lock()?.parent).cloned(),
187 Some(Countable::Chain(_)) => todo!(),
188 None => None,
189 })
190 }
191
192 pub fn parent(&self, countable: &CountableId) -> Option<Countable> {
193 self.parent_checked(countable).unwrap()
194 }
195
196 pub fn last_child_checked(&self, countable: &CountableId) -> Result<Countable, AppError> {
197 let children = self.children_checked(countable)?;
198 if children.is_empty() {
199 return self
200 .store
201 .get(countable)
202 .ok_or(AppError::CountableNotFound)
203 .cloned();
204 } else {
205 self.last_child_checked(&children.last().unwrap().uuid_checked()?.into())
206 }
207 }
208
209 pub fn last_child(&self, countable: &CountableId) -> Countable {
210 self.last_child_checked(countable).unwrap()
211 }
212
213 pub fn kind_checked(&self, countable: &CountableId) -> Result<CountableKind, AppError> {
214 Ok(
215 match self
216 .store
217 .get(countable)
218 .ok_or(AppError::CountableNotFound)?
219 {
220 Countable::Counter(_) => CountableKind::Counter,
221 Countable::Phase(_) => CountableKind::Phase,
222 Countable::Chain(_) => CountableKind::Chain,
223 },
224 )
225 }
226
227 pub fn kind(&self, countable: &CountableId) -> CountableKind {
228 self.kind_checked(countable).unwrap()
229 }
230
231 pub fn name_checked(&self, countable: &CountableId) -> Result<String, AppError> {
232 self.store
233 .get(countable)
234 .ok_or(AppError::CountableNotFound)?
235 .name_checked()
236 }
237
238 pub fn name(&self, countable: &CountableId) -> String {
239 self.name_checked(countable).unwrap()
240 }
241
242 pub fn set_name_checked(&self, countable: &CountableId, name: &str) -> Result<(), AppError> {
243 match self
244 .store
245 .get(countable)
246 .ok_or(AppError::CountableNotFound)?
247 {
248 Countable::Counter(c) => c.lock()?.name = name.into(),
249 Countable::Phase(p) => p.lock()?.name = name.into(),
250 Countable::Chain(_) => todo!(),
251 };
252
253 self.is_changed.replace(true);
254
255 Ok(())
256 }
257
258 pub fn set_name(&self, countable: &CountableId, name: &str) {
259 self.set_name_checked(countable, name).unwrap()
260 }
261
262 pub fn count_checked(&self, countable: &CountableId) -> Result<i32, AppError> {
263 Ok(
264 match self
265 .store
266 .get(countable)
267 .ok_or(AppError::CountableNotFound)?
268 {
269 Countable::Counter(c) => {
270 let mut sum = 0;
271 for child in c.lock()?.children.iter() {
272 sum += self.count_checked(child)?;
273 }
274 sum
275 }
276 Countable::Phase(p) => p.lock()?.count,
277 Countable::Chain(_) => todo!(),
278 },
279 )
280 }
281
282 pub fn count(&self, countable: &CountableId) -> i32 {
283 self.count_checked(countable).unwrap()
284 }
285
286 pub fn set_count_checked(&self, countable: &CountableId, count: i32) -> Result<(), AppError> {
287 let diff = count - self.count_checked(countable)?;
288 self.add_count_checked(countable, diff)?;
289 self.is_changed.replace(true);
290 Ok(())
291 }
292
293 pub fn set_count(&self, countable: &CountableId, count: i32) {
294 self.set_count_checked(countable, count).unwrap()
295 }
296
297 pub fn add_count_checked(&self, countable: &CountableId, mut add: i32) -> Result<(), AppError> {
298 match self
299 .store
300 .get(countable)
301 .ok_or(AppError::CountableNotFound)?
302 {
303 Countable::Counter(c) => {
304 for child in c.lock()?.children.iter().rev() {
305 let child_count = self.count_checked(child)?;
306 if child_count + add <= 0 {
307 self.set_count_checked(child, 0)?;
308 add += self.count_checked(child)?;
309 } else {
310 self.set_count_checked(child, child_count + add)?;
311 return Ok(());
312 }
313 }
314 }
315 Countable::Phase(p) => {
316 p.lock()?.count += add;
317 }
318 Countable::Chain(_) => todo!(),
319 }
320
321 self.is_changed.replace(true);
322
323 Ok(())
324 }
325
326 pub fn add_count(&self, countable: &CountableId, add: i32) {
327 self.add_count_checked(countable, add).unwrap();
328 }
329
330 pub fn time_checked(&self, countable: &CountableId) -> Result<TimeDelta, AppError> {
331 match self
332 .store
333 .get(countable)
334 .ok_or(AppError::CountableNotFound)?
335 {
336 Countable::Counter(c) => {
337 let mut time = TimeDelta::zero();
338 for child in c.lock()?.children.iter() {
339 time += self.time_checked(child)?;
340 }
341 Ok(time)
342 }
343 Countable::Phase(p) => Ok(p.lock()?.time),
344 Countable::Chain(_) => todo!(),
345 }
346 }
347
348 pub fn time(&self, countable: &CountableId) -> TimeDelta {
349 self.time_checked(countable).unwrap()
350 }
351
352 pub fn set_time_checked(
353 &self,
354 countable: &CountableId,
355 time: TimeDelta,
356 ) -> Result<(), AppError> {
357 let diff = time - self.time_checked(countable)?;
358 self.add_time_checked(countable, diff)?;
359 self.is_changed.replace(true);
360 Ok(())
361 }
362
363 pub fn set_time(&self, countable: &CountableId, time: TimeDelta) {
364 self.set_time_checked(countable, time).unwrap()
365 }
366
367 pub fn add_time_checked(
368 &self,
369 countable: &CountableId,
370 mut add: TimeDelta,
371 ) -> Result<(), AppError> {
372 match self
373 .store
374 .get(countable)
375 .ok_or(AppError::CountableNotFound)?
376 {
377 Countable::Counter(c) => {
378 for child in c.lock()?.children.iter().rev() {
379 let child_time = self.time_checked(child)?;
380 if child_time + add <= TimeDelta::zero() {
381 self.set_time_checked(child, TimeDelta::zero())?;
382 add += self.time_checked(child)?;
383 } else {
384 self.set_time_checked(child, child_time + add)?;
385 }
386 }
387 }
388 Countable::Phase(p) => {
389 p.lock()?.time += add;
390 }
391 Countable::Chain(_) => todo!(),
392 }
393
394 self.is_changed.replace(true);
395
396 Ok(())
397 }
398
399 pub fn add_time(&self, countable: &CountableId, add: TimeDelta) {
400 self.add_time_checked(countable, add).unwrap();
401 }
402
403 pub fn hunttype_checked(&self, countable: &CountableId) -> Result<Hunttype, AppError> {
404 match self
405 .store
406 .get(countable)
407 .ok_or(AppError::CountableNotFound)?
408 {
409 Countable::Counter(c) => {
410 let children = c.lock()?.children.clone();
411
412 let ht = children
413 .first()
414 .and_then(|child| self.hunttype_checked(child).ok())
415 .unwrap_or_default();
416
417 for child in children.iter() {
418 if self.hunttype_checked(child)? != ht {
419 return Ok(Hunttype::Mixed);
420 }
421 }
422 Ok(ht)
423 }
424 Countable::Phase(p) => Ok(p.lock()?.hunt_type),
425 Countable::Chain(_) => todo!(),
426 }
427 }
428
429 pub fn hunttype(&self, countable: &CountableId) -> Hunttype {
430 self.hunttype_checked(countable).unwrap()
431 }
432
433 pub fn rolls_checked(&self, countable: &CountableId) -> Result<usize, AppError> {
434 Ok(
435 match self
436 .store
437 .get(countable)
438 .ok_or(AppError::CountableNotFound)?
439 {
440 Countable::Counter(c) => c
441 .lock()?
442 .children
443 .iter()
444 .map(|child| self.rolls_checked(child))
445 .collect::<Result<Vec<_>, AppError>>()?
446 .into_iter()
447 .sum(),
448 Countable::Phase(_) => self.hunttype_checked(countable)?.rolls()(
449 self.count_checked(countable)?,
450 self.has_charm_checked(countable)?,
451 ),
452 Countable::Chain(_) => todo!(),
453 },
454 )
455 }
456
457 pub fn rolls(&self, countable: &CountableId) -> usize {
458 self.rolls_checked(countable).unwrap()
459 }
460
461 pub fn odds_checked(&self, countable: &CountableId) -> Result<f64, AppError> {
462 Ok(
463 match self
464 .store
465 .get(countable)
466 .ok_or(AppError::CountableNotFound)?
467 {
468 Countable::Counter(c) => {
469 let sum = c
470 .lock()?
471 .children
472 .iter()
473 .map(|child| {
474 let odds = self.odds_checked(child)?;
475 Ok(odds * self.count_checked(child)? as f64)
476 })
477 .collect::<Result<Vec<_>, AppError>>()?
478 .into_iter()
479 .sum::<f64>();
480 sum / (self.count_checked(countable)? as f64).max(1.0)
481 }
482 Countable::Phase(p) => p.lock()?.hunt_type.odds(),
483 Countable::Chain(_) => todo!(),
484 },
485 )
486 }
487
488 pub fn odds(&self, countable: &CountableId) -> f64 {
489 self.odds_checked(countable).unwrap()
490 }
491
492 pub fn progress_checked(&self, countable: &CountableId) -> Result<f64, AppError> {
493 let prob = 1.0 / self.odds_checked(countable)?;
494 let rolls = self.rolls_checked(countable)?;
495 Ok(
496 match self
497 .store
498 .get(countable)
499 .ok_or(AppError::CountableNotFound)?
500 {
501 Countable::Counter(c) => {
502 let children_len = c.lock()?.children.len();
503 let mut chance = 0.0;
504 for k in 0..((self.completed_checked(countable)? + 1).min(children_len)) {
505 let combs = n_choose_k(rolls, k);
506 chance +=
507 combs * prob.powi(k as i32) * (1.0 - prob).powi((rolls - k) as i32)
508 }
509
510 1.0 - chance
511 }
512 Countable::Phase(_) => 1.0 - (1.0 - prob).powi(rolls as i32),
513 Countable::Chain(_) => todo!(),
514 },
515 )
516 }
517
518 pub fn progress(&self, countable: &CountableId) -> f64 {
519 self.progress_checked(countable).unwrap()
520 }
521
522 pub fn completed_checked(&self, countable: &CountableId) -> Result<usize, AppError> {
523 Ok(
524 match self
525 .store
526 .get(countable)
527 .ok_or(AppError::CountableNotFound)?
528 {
529 Countable::Counter(c) => c
530 .lock()?
531 .children
532 .iter()
533 .map(|child| self.completed_checked(child))
534 .collect::<Result<Vec<_>, AppError>>()?
535 .into_iter()
536 .sum(),
537 Countable::Phase(p) => p.lock()?.success.into(),
538 Countable::Chain(_) => todo!(),
539 },
540 )
541 }
542
543 pub fn has_charm_checked(&self, countable: &CountableId) -> Result<bool, AppError> {
544 Ok(
545 match self
546 .store
547 .get(countable)
548 .ok_or(AppError::CountableNotFound)?
549 {
550 Countable::Counter(c) => {
551 let mut has = true;
552 for child in c.lock()?.children.iter() {
553 has &= self.has_charm_checked(child)?;
554 }
555 has
556 }
557 Countable::Phase(p) => p.lock()?.has_charm,
558 Countable::Chain(_) => todo!(),
559 },
560 )
561 }
562
563 pub fn has_charm(&self, countable: &CountableId) -> bool {
564 self.has_charm_checked(countable).unwrap()
565 }
566
567 pub fn is_success_checked(&self, countable: &CountableId) -> Result<bool, AppError> {
568 Ok(
569 match self
570 .store
571 .get(countable)
572 .ok_or(AppError::CountableNotFound)?
573 {
574 Countable::Counter(c) => c
575 .lock()?
576 .children
577 .last()
578 .and_then(|child| self.is_success_checked(child).ok())
579 .unwrap_or_default(),
580 Countable::Phase(p) => p.lock()?.success,
581 Countable::Chain(_) => todo!(),
582 },
583 )
584 }
585
586 pub fn is_success(&self, countable: &CountableId) -> bool {
587 self.is_success_checked(countable).unwrap()
588 }
589
590 pub fn toggle_success_checked(&self, countable: &CountableId) -> Result<(), AppError> {
591 match self
592 .store
593 .get(countable)
594 .ok_or(AppError::CountableNotFound)?
595 {
596 Countable::Counter(_) => {}
597 Countable::Phase(p) => {
598 let success = p.lock()?.success;
599 p.lock()?.success = !success;
600 }
601 Countable::Chain(_) => todo!(),
602 };
603
604 self.is_changed.replace(true);
605
606 Ok(())
607 }
608
609 pub fn toggle_success(&self, countable: &CountableId) {
610 self.toggle_success_checked(countable).unwrap()
611 }
612
613 pub fn created_at_checked(
614 &self,
615 countable: &CountableId,
616 ) -> Result<chrono::NaiveDateTime, AppError> {
617 self.store
618 .get(countable)
619 .ok_or(AppError::CountableNotFound)?
620 .created_at_checked()
621 }
622
623 pub fn created_at(&self, countable: &CountableId) -> chrono::NaiveDateTime {
624 self.created_at_checked(countable).unwrap()
625 }
626}
627
628#[typetag::serde]
629impl Savable for leptos::RwSignal<CountableStore> {
630 fn indexed_db_name(&self) -> String {
631 "Countable".into()
632 }
633
634 fn save_indexed<'a>(
635 &'a self,
636 obj: indexed_db::ObjectStore<AppError>,
637 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), AppError>> + 'a>> {
638 use wasm_bindgen::JsValue;
639
640 self.update_untracked(|s| {
641 s.is_changed.replace(false);
642 });
643
644 Box::pin(async move {
645 obj.clear().await?;
646 for c in self.get_untracked().store.values() {
647 let key = JsValue::from_str(&c.uuid().to_string());
648 let value = c.as_js();
649
650 obj.put_kv(&key, &value?).await?;
651 }
652 Ok(())
653 })
654 }
655
656 fn save_endpoint(
657 &self,
658 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), leptos::ServerFnError>>>>
659 {
660 self.update_untracked(|s| {
661 s.is_changed.replace(false);
662 });
663
664 Box::pin(api::update_countable_many(
665 self.get_untracked().store.into_values().collect(),
666 ))
667 }
668
669 fn message(&self) -> Option<leptos::View> {
670 None
671 }
672
673 fn clone_box(&self) -> Box<dyn Savable> {
674 Box::new(*self)
675 }
676
677 fn has_change(&self) -> bool {
678 *self.get_untracked().is_changed.borrow()
679 }
680}
681
682#[typetag::serde]
683impl Savable for CountableStore {
684 fn indexed_db_name(&self) -> String {
685 "Countable".into()
686 }
687
688 fn save_indexed<'a>(
689 &'a self,
690 obj: indexed_db::ObjectStore<AppError>,
691 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), AppError>> + 'a>> {
692 use wasm_bindgen::JsValue;
693
694 self.is_changed.replace(false);
695
696 Box::pin(async move {
697 obj.clear().await?;
698 for c in self.store.values() {
699 let key = JsValue::from_str(&c.uuid().to_string());
700 let value = c.as_js();
701
702 obj.put_kv(&key, &value?).await?;
703 }
704 Ok(())
705 })
706 }
707
708 fn save_endpoint(
709 &self,
710 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), leptos::ServerFnError>>>>
711 {
712 self.is_changed.replace(false);
713 let cloned = self.clone();
714 Box::pin(api::update_countable_many(
715 cloned.store.into_values().collect(),
716 ))
717 }
718
719 fn message(&self) -> Option<leptos::View> {
720 None
721 }
722
723 fn clone_box(&self) -> Box<dyn Savable> {
724 Box::new(self.clone())
725 }
726
727 fn has_change(&self) -> bool {
728 *self.is_changed.borrow()
729 }
730}
731
732#[typetag::serde]
733impl Savable for Vec<Countable> {
734 fn indexed_db_name(&self) -> String {
735 "Countable".into()
736 }
737
738 fn save_indexed<'a>(
739 &'a self,
740 obj: indexed_db::ObjectStore<AppError>,
741 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), AppError>> + 'a>> {
742 use wasm_bindgen::JsValue;
743
744 Box::pin(async move {
745 for c in self {
746 let key = JsValue::from_str(&c.uuid().to_string());
747 let value = c.as_js();
748
749 obj.put_kv(&key, &value?).await?;
750 }
751 Ok(())
752 })
753 }
754
755 fn save_endpoint(
756 &self,
757 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), leptos::ServerFnError>>>>
758 {
759 Box::pin(api::update_countable_many(self.clone()))
760 }
761
762 fn message(&self) -> Option<leptos::View> {
763 None
764 }
765
766 fn clone_box(&self) -> Box<dyn Savable> {
767 Box::new(self.clone())
768 }
769 fn has_change(&self) -> bool {
770 true
771 }
772}
773
774#[typetag::serde]
775impl Savable for Countable {
776 fn indexed_db_name(&self) -> String {
777 "Countable".into()
778 }
779
780 fn save_indexed<'a>(
781 &'a self,
782 obj: indexed_db::ObjectStore<AppError>,
783 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), AppError>> + 'a>> {
784 use wasm_bindgen::JsValue;
785 let key = JsValue::from_str(&self.uuid().to_string());
786 let value = self.as_js();
787 Box::pin(async move {
788 obj.put_kv(&key, &value?).await?;
789 Ok(())
790 })
791 }
792
793 fn save_endpoint(
794 &self,
795 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), leptos::ServerFnError>>>>
796 {
797 Box::pin(api::update_countable_many(vec![self.clone()]))
798 }
799
800 fn message(&self) -> Option<leptos::View> {
801 None
802 }
803
804 fn clone_box(&self) -> Box<dyn Savable> {
805 Box::new(self.clone())
806 }
807
808 fn has_change(&self) -> bool {
809 true
810 }
811}
812
813#[derive(
814 Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord,
815)]
816pub struct CountableId(uuid::Uuid);
817
818impl From<uuid::Uuid> for CountableId {
819 fn from(value: uuid::Uuid) -> Self {
820 Self(value)
821 }
822}
823
824#[derive(Debug, Clone, Serialize, Deserialize)]
825pub enum Countable {
826 Counter(Arc<Mutex<Counter>>),
827 Phase(Arc<Mutex<Phase>>),
828 Chain(Arc<Mutex<Chain>>),
829}
830
831impl From<Countable> for CountableId {
832 fn from(value: Countable) -> Self {
833 value.uuid().into()
834 }
835}
836
837impl From<&Countable> for CountableId {
838 fn from(value: &Countable) -> Self {
839 value.uuid().into()
840 }
841}
842
843impl Countable {
844 pub fn new(
845 name: &str,
846 kind: CountableKind,
847 owner_uuid: uuid::Uuid,
848 parent: Option<CountableId>,
849 ) -> Self {
850 match kind {
851 CountableKind::Counter => Self::Counter(Arc::new(Mutex::new(Counter::new(
852 name.into(),
853 owner_uuid,
854 parent,
855 )))),
856 CountableKind::Phase => Self::Phase(Arc::new(Mutex::new(Phase::new(
857 name.into(),
858 owner_uuid,
859 parent.expect("Phase has to have a parent"),
860 )))),
861 CountableKind::Chain => todo!(),
862 }
863 }
864
865 pub fn add_child_checked(&self, child: CountableId) -> Result<(), AppError> {
866 match self {
867 Countable::Counter(c) => {
868 c.lock()?.children.push(child);
869 Ok(())
870 }
871 Countable::Phase(_) => Err(AppError::CannotContainChildren("Phase".into())),
872 Countable::Chain(_) => Err(AppError::CannotContainChildren("Chain".into())),
873 }
874 }
875
876 pub fn add_child(&self, child: CountableId) {
877 self.add_child_checked(child).unwrap()
878 }
879
880 pub fn uuid_checked(&self) -> Result<uuid::Uuid, AppError> {
881 Ok(match self {
882 Countable::Counter(c) => c.lock()?.uuid,
883 Countable::Phase(p) => p.lock()?.uuid,
884 Countable::Chain(_) => todo!(),
885 })
886 }
887
888 pub fn uuid(&self) -> uuid::Uuid {
889 self.uuid_checked().unwrap()
890 }
891
892 pub fn name_checked(&self) -> Result<String, AppError> {
893 Ok(match self {
894 Countable::Counter(c) => c.lock()?.name.clone(),
895 Countable::Phase(p) => p.lock()?.name.clone(),
896 Countable::Chain(_) => todo!(),
897 })
898 }
899
900 pub fn name(&self) -> String {
901 self.name_checked().unwrap()
902 }
903
904 pub fn set_name_checked(&self, name: &str) -> Result<(), AppError> {
905 match self {
906 Countable::Counter(c) => c.lock()?.name = name.into(),
907 Countable::Phase(p) => p.lock()?.name = name.into(),
908 Countable::Chain(_) => todo!(),
909 }
910
911 Ok(())
912 }
913
914 pub fn set_name(&self, name: &str) {
915 self.set_name_checked(name).unwrap()
916 }
917
918 pub fn created_at_checked(&self) -> Result<chrono::NaiveDateTime, AppError> {
919 Ok(match self {
920 Countable::Counter(c) => c.lock()?.created_at,
921 Countable::Phase(p) => p.lock()?.created_at,
922 Countable::Chain(_) => todo!(),
923 })
924 }
925
926 pub fn created_at(&self) -> chrono::NaiveDateTime {
927 self.created_at_checked().unwrap()
928 }
929
930 pub fn last_edit_checked(&self) -> Result<chrono::NaiveDateTime, AppError> {
931 Ok(match self {
932 Countable::Counter(c) => c.lock()?.last_edit,
933 Countable::Phase(p) => p.lock()?.last_edit,
934 Countable::Chain(_) => todo!(),
935 })
936 }
937
938 pub fn last_edit(&self) -> chrono::NaiveDateTime {
939 self.last_edit_checked().unwrap()
940 }
941
942 pub fn is_archived_checked(&self) -> Result<bool, AppError> {
943 Ok(match self {
944 Countable::Counter(c) => c.lock()?.is_deleted,
945 Countable::Phase(p) => p.lock()?.is_deleted,
946 Countable::Chain(_) => todo!(),
947 })
948 }
949
950 pub fn is_archived(&self) -> bool {
951 self.is_archived_checked().unwrap()
952 }
953
954 pub fn as_js(&self) -> Result<wasm_bindgen::JsValue, AppError> {
955 Ok(js_sys::JSON::parse(&serde_json::to_string(&self)?)?)
956 }
957
958 pub fn from_js(val: wasm_bindgen::JsValue) -> Result<Self, AppError> {
959 let this = serde_json::from_str(
960 &js_sys::JSON::stringify(&val)?
961 .as_string()
962 .unwrap_or_default(),
963 )?;
964 Ok(this)
965 }
966}
967
968impl PartialEq for Countable {
969 fn eq(&self, other: &Self) -> bool {
970 match (self, other) {
971 (Countable::Counter(a), Countable::Counter(b)) => match (a.try_lock(), b.try_lock()) {
972 (Ok(a), Ok(b)) => a.eq(&*b),
973 _ => false,
974 },
975 (Countable::Phase(a), Countable::Phase(b)) => match (a.try_lock(), b.try_lock()) {
976 (Ok(a), Ok(b)) => a.eq(&*b),
977 _ => false,
978 },
979 (Countable::Chain(a), Countable::Chain(b)) => match (a.try_lock(), b.try_lock()) {
980 (Ok(a), Ok(b)) => a.eq(&*b),
981 _ => false,
982 },
983 _ => false,
984 }
985 }
986}
987
988impl Eq for Countable {}
989
990#[cfg(feature = "ssr")]
991impl From<backend::DbCounter> for Countable {
992 fn from(value: backend::DbCounter) -> Self {
993 Self::Counter(Arc::new(Mutex::new(Counter {
994 uuid: value.uuid,
995 owner_uuid: value.owner_uuid,
996 parent: None,
997 children: Vec::new(),
998 name: value.name,
999 last_edit: value.last_edit,
1000 created_at: value.created_at,
1001 is_deleted: value.is_deleted,
1002 })))
1003 }
1004}
1005
1006#[cfg(feature = "ssr")]
1007impl From<backend::DbPhase> for Countable {
1008 fn from(value: backend::DbPhase) -> Self {
1009 Self::Phase(Arc::new(Mutex::new(Phase {
1010 uuid: value.uuid,
1011 owner_uuid: value.owner_uuid,
1012 parent: value.parent_uuid.into(),
1013 name: value.name,
1014 count: value.count,
1015 time: chrono::Duration::milliseconds(value.time),
1016 hunt_type: value.hunt_type.into(),
1017 has_charm: value.has_charm,
1018 success: value.success,
1019 last_edit: value.last_edit,
1020 created_at: value.created_at,
1021 is_deleted: value.is_deleted,
1022 })))
1023 }
1024}
1025
1026#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
1027pub enum CountableKind {
1028 #[default]
1029 Counter,
1030 Phase,
1031 Chain,
1032}
1033
1034impl std::fmt::Display for CountableKind {
1035 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1036 match self {
1037 Self::Counter => write!(f, "Counter"),
1038 Self::Phase => write!(f, "Phase"),
1039 Self::Chain => write!(f, "Chain"),
1040 }
1041 }
1042}
1043
1044#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1045pub struct Counter {
1046 pub uuid: uuid::Uuid,
1047 pub owner_uuid: uuid::Uuid,
1048 pub parent: Option<CountableId>,
1049 #[serde(default)]
1050 pub children: Vec<CountableId>,
1051 pub name: String,
1052 pub last_edit: chrono::NaiveDateTime,
1053 pub created_at: chrono::NaiveDateTime,
1054 pub is_deleted: bool,
1055}
1056
1057impl Counter {
1058 fn new(name: String, owner_uuid: uuid::Uuid, parent: Option<CountableId>) -> Self {
1059 Self {
1060 uuid: uuid::Uuid::new_v4(),
1061 owner_uuid,
1062 parent,
1063 children: Vec::new(),
1064 name,
1065 last_edit: chrono::Utc::now().naive_utc(),
1066 created_at: chrono::Utc::now().naive_utc(),
1067 is_deleted: false,
1068 }
1069 }
1070}
1071
1072#[cfg(feature = "ssr")]
1073impl Into<backend::DbCounter> for Counter {
1074 fn into(self) -> backend::DbCounter {
1075 backend::DbCounter {
1076 uuid: self.uuid,
1077 owner_uuid: self.owner_uuid,
1078 name: self.name,
1079 last_edit: self.last_edit,
1080 created_at: self.created_at,
1081 is_deleted: self.is_deleted,
1082 }
1083 }
1084}
1085
1086#[serde_with::serde_as]
1087#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
1088pub struct Phase {
1089 pub uuid: uuid::Uuid,
1090 pub owner_uuid: uuid::Uuid,
1091 pub parent: CountableId,
1092 pub name: String,
1093 pub count: i32,
1094 #[serde_as(as = "serde_with::DurationMilliSeconds<i64>")]
1095 pub time: chrono::Duration,
1096 pub hunt_type: Hunttype,
1097 pub has_charm: bool,
1098 pub success: bool,
1099 pub last_edit: chrono::NaiveDateTime,
1100 pub created_at: chrono::NaiveDateTime,
1101 pub is_deleted: bool,
1102}
1103
1104impl Phase {
1105 fn new(name: String, owner_uuid: uuid::Uuid, parent: CountableId) -> Self {
1106 Self {
1107 uuid: uuid::Uuid::new_v4(),
1108 owner_uuid,
1109 parent,
1110 name,
1111 last_edit: chrono::Utc::now().naive_utc(),
1112 created_at: chrono::Utc::now().naive_utc(),
1113 ..Default::default()
1114 }
1115 }
1116}
1117
1118#[cfg(feature = "ssr")]
1119impl Into<backend::DbPhase> for Phase {
1120 fn into(self) -> backend::DbPhase {
1121 backend::DbPhase {
1122 uuid: self.uuid,
1123 owner_uuid: self.owner_uuid,
1124 parent_uuid: self.parent.0,
1125 name: self.name,
1126 count: self.count,
1127 time: self.time.num_milliseconds(),
1128 hunt_type: self.hunt_type.into(),
1129 has_charm: self.has_charm,
1130 dexnav_encounters: None,
1131 success: self.success,
1132 last_edit: self.last_edit,
1133 created_at: self.created_at,
1134 is_deleted: self.is_deleted,
1135 }
1136 }
1137}
1138
1139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1140pub struct Chain {}
1141
1142#[allow(clippy::upper_case_acronyms)]
1143#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1144pub enum Hunttype {
1145 #[default]
1146 OldOdds,
1147 NewOdds,
1148 SOS,
1149 Masuda(Masuda),
1151 Mixed,
1152}
1153
1154#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1155pub enum Masuda {
1156 GenIV,
1157 GenV,
1158 #[default]
1159 GenVI,
1160 }
1163
1164impl Hunttype {
1165 fn rolls(&self) -> impl Fn(i32, bool) -> usize {
1166 match self {
1167 Hunttype::OldOdds => {
1168 |count, has_charm: bool| (count * if has_charm { 3 } else { 1 }) as usize
1169 }
1170 Hunttype::NewOdds => {
1171 |count, has_charm: bool| (count * if has_charm { 3 } else { 1 }) as usize
1172 }
1173 Hunttype::SOS => |count, has_charm: bool| match count {
1174 c if c < 10 => (count * if has_charm { 3 } else { 1 }) as usize,
1175 c if c < 20 => (10 + (count - 10) * if has_charm { 3 + 4 } else { 1 + 4 }) as usize,
1176 c if c < 30 => (60 + (count - 20) * if has_charm { 3 + 8 } else { 1 + 8 }) as usize,
1177 _ => (50 + (count - 30) * if has_charm { 3 + 13 } else { 1 + 12 }) as usize,
1178 },
1179 Hunttype::Masuda(Masuda::GenIV) => {
1180 |count, has_charm: bool| (count * if has_charm { 3 + 4 } else { 1 + 4 }) as usize
1181 }
1182 Hunttype::Masuda(_) => {
1183 |count, has_charm: bool| (count * if has_charm { 3 + 5 } else { 1 + 5 }) as usize
1184 }
1185 Hunttype::Mixed => unreachable!(),
1186 }
1187 }
1188
1189 fn odds(&self) -> f64 {
1190 match self {
1191 Hunttype::OldOdds | Hunttype::Masuda(Masuda::GenIV) => 8192.0,
1192 _ => 4096.0,
1193 }
1194 }
1195
1196 pub fn repr(&self) -> &'static str {
1197 match self {
1198 Self::OldOdds => "Old Odds",
1199 Self::NewOdds => "New Odds",
1200 Self::SOS => "SOS",
1201 Self::Masuda(Masuda::GenIV) => "Masuda (gen IV)",
1202 Self::Masuda(Masuda::GenV) => "Masuda (gen V)",
1203 Self::Masuda(Masuda::GenVI) => "Masuda (gen VI+)",
1204 Self::Mixed => todo!(),
1205 }
1206 }
1207}
1208
1209impl From<Hunttype> for &'static str {
1210 fn from(val: Hunttype) -> Self {
1211 match val {
1212 Hunttype::OldOdds => "OldOdds",
1213 Hunttype::NewOdds => "NewOdds",
1214 Hunttype::SOS => "SOS",
1215 Hunttype::Masuda(Masuda::GenIV) => "MasudaGenIV",
1216 Hunttype::Masuda(Masuda::GenV) => "MasudaGenV",
1217 Hunttype::Masuda(Masuda::GenVI) => "MasudaGenVI",
1218 Hunttype::Mixed => todo!(),
1219 }
1220 }
1221}
1222
1223impl TryFrom<String> for Hunttype {
1224 type Error = String;
1225
1226 fn try_from(value: String) -> Result<Self, Self::Error> {
1227 return match value.as_str() {
1228 "OldOdds" => Ok(Self::OldOdds),
1229 "NewOdds" => Ok(Self::NewOdds),
1230 "SOS" => Ok(Self::SOS),
1231 "MasudaGenIV" => Ok(Self::Masuda(Masuda::GenIV)),
1232 "MasudaGenV" => Ok(Self::Masuda(Masuda::GenV)),
1233 "MasudaGenVI" => Ok(Self::Masuda(Masuda::GenVI)),
1234 _ => Err(String::from(
1235 "Hunttype should be one of the following: OldOdds, NewOdds, SOS, Masuda",
1236 )),
1237 };
1238 }
1239}
1240
1241impl From<Hunttype> for components::SelectOption {
1242 fn from(val: Hunttype) -> Self {
1243 (val.repr(), val.into()).into()
1244 }
1245}
1246
1247impl From<Hunttype> for leptos::Attribute {
1248 fn from(val: Hunttype) -> Self {
1249 let str: &'static str = val.into();
1250 leptos::Attribute::String(str.into())
1251 }
1252}
1253
1254impl leptos::IntoAttribute for Hunttype {
1255 fn into_attribute(self) -> leptos::Attribute {
1256 self.into()
1257 }
1258
1259 fn into_attribute_boxed(self: Box<Self>) -> leptos::Attribute {
1260 (*self).into()
1261 }
1262}
1263
1264#[cfg(feature = "ssr")]
1265impl From<backend::Hunttype> for Hunttype {
1266 fn from(value: backend::Hunttype) -> Self {
1267 match value {
1268 backend::Hunttype::OldOdds => Self::OldOdds,
1269 backend::Hunttype::NewOdds => Self::NewOdds,
1270 backend::Hunttype::SOS => Self::SOS,
1271 backend::Hunttype::DexNav => todo!(),
1272 backend::Hunttype::MasudaGenIV => Self::Masuda(Masuda::GenIV),
1273 backend::Hunttype::MasudaGenV => Self::Masuda(Masuda::GenV),
1274 backend::Hunttype::MasudaGenVI => Self::Masuda(Masuda::GenVI),
1275 }
1276 }
1277}
1278
1279#[cfg(feature = "ssr")]
1280impl Into<backend::Hunttype> for Hunttype {
1281 fn into(self) -> backend::Hunttype {
1282 match self {
1283 Self::OldOdds => backend::Hunttype::OldOdds,
1284 Self::NewOdds => backend::Hunttype::NewOdds,
1285 Self::SOS => backend::Hunttype::SOS,
1286 Self::Masuda(Masuda::GenIV) => backend::Hunttype::MasudaGenIV,
1287 Self::Masuda(Masuda::GenV) => backend::Hunttype::MasudaGenV,
1288 Self::Masuda(Masuda::GenVI) => backend::Hunttype::MasudaGenVI,
1289 Self::Mixed => unreachable!(),
1290 }
1291 }
1292}
1293
1294fn n_choose_k(n: usize, k: usize) -> f64 {
1295 match (n, k) {
1296 (n, k) if k > n => 0.0,
1297 (_, 0) => 1.0,
1298 (n, k) if k > n / 2 => n_choose_k(n, n - k),
1299 (n, k) => n as f64 / k as f64 * n_choose_k(n - 1, k - 1),
1300 }
1301}