1use super::CommandType;
2use crate::app::{
3 AppCommand, AppMeta, Autocomplete, AutocompleteSuggestion, Command, CommandAlias,
4 CommandMatches, ContextAwareParse, Runnable,
5};
6use crate::reference::{ItemCategory, ReferenceCommand, Spell};
7use crate::storage::{Change, Record, StorageCommand};
8use crate::time::TimeCommand;
9use crate::utils::CaseInsensitiveStr;
10use crate::world::npc::{Age, Ethnicity, Gender, NpcData, Species};
11use crate::world::thing::ThingData;
12use crate::world::{ParsedThing, WorldCommand};
13use async_trait::async_trait;
14use std::fmt;
15use uuid::Uuid;
16
17#[derive(Clone, Debug, Eq, PartialEq)]
33pub enum TutorialCommand {
34 Introduction,
35 GeneratingLocations,
36 SavingLocations,
37 GeneratingCharacters {
38 inn: ThingRef,
39 },
40 GeneratingAlternatives {
41 inn: ThingRef,
42 },
43 ViewingAlternatives {
44 inn: ThingRef,
45 npc_name: String,
46 },
47 EditingCharacters {
48 inn: ThingRef,
49 npc: ThingRef,
50 },
51 TheJournal {
52 inn: ThingRef,
53 npc: ThingRef,
54 },
55 LoadingFromJournal {
56 inn: ThingRef,
57 npc: ThingRef,
58 },
59 SrdReference {
60 inn: ThingRef,
61 npc: ThingRef,
62 },
63 SrdReferenceLists {
64 inn: ThingRef,
65 npc: ThingRef,
66 },
67 RollingDice {
68 inn: ThingRef,
69 npc: ThingRef,
70 },
71 DeletingThings {
72 inn: ThingRef,
73 npc: ThingRef,
74 },
75 AdvancingTime {
76 inn: ThingRef,
77 npc: ThingRef,
78 },
79 CheckingTheTime {
80 inn: ThingRef,
81 npc: ThingRef,
82 },
83 Conclusion {
84 inn: ThingRef,
85 npc: ThingRef,
86 },
87
88 Cancel {
89 inn: Option<ThingRef>,
90 npc: Option<ThingRef>,
91 },
92 Resume,
93 Restart {
94 inn: Option<ThingRef>,
95 npc: Option<ThingRef>,
96 },
97}
98
99#[derive(Clone, Debug, Eq, PartialEq)]
100pub struct ThingRef {
101 uuid: Uuid,
102 name: String,
103}
104
105impl TutorialCommand {
106 fn output(
121 &self,
122 command_output: Option<Result<String, String>>,
123 app_meta: &mut AppMeta,
124 ) -> Result<String, String> {
125 let is_ok = if let Some(r) = &command_output {
126 r.is_ok()
127 } else {
128 true
129 };
130
131 let mut output = command_output
132 .unwrap_or_else(|| Ok(String::new()))
133 .unwrap_or_else(|e| e);
134 if !output.is_empty() {
135 output.push_str("\n\n#");
136 }
137
138 match self {
139 Self::Introduction | Self::Cancel { .. } | Self::Resume | Self::Restart { .. } => {}
140 Self::GeneratingLocations => {
141 app_meta.command_aliases.insert(CommandAlias::literal(
142 "next",
143 "continue the tutorial",
144 Self::GeneratingLocations.into(),
145 ));
146
147 output.push_str(include_str!("../../../../data/tutorial/00-introduction.md"));
148 }
149 Self::SavingLocations => output.push_str(include_str!(
150 "../../../../data/tutorial/01-generating-locations.md"
151 )),
152 Self::GeneratingCharacters { inn } => {
153 app_meta.command_aliases.insert(CommandAlias::literal(
154 "save",
155 format!("save {}", inn),
156 StorageCommand::Save {
157 name: inn.to_string(),
158 }
159 .into(),
160 ));
161
162 output.push_str(&format!(
163 include_str!("../../../../data/tutorial/02-saving-locations.md"),
164 inn_name = inn,
165 ));
166 }
167 Self::GeneratingAlternatives { .. } => output.push_str(include_str!(
168 "../../../../data/tutorial/03-generating-characters.md"
169 )),
170 Self::ViewingAlternatives { npc_name, .. } => {
171 let thing_data: ThingData = NpcData {
172 species: Species::Human.into(),
173 ethnicity: Ethnicity::Human.into(),
174 age: Age::Adult.into(),
175 gender: Gender::Feminine.into(),
176 ..Default::default()
177 }
178 .into();
179
180 app_meta.command_aliases.insert(CommandAlias::literal(
181 "more",
182 format!("create {}", thing_data.display_description()),
183 WorldCommand::CreateMultiple { thing_data }.into(),
184 ));
185
186 output.push_str(&format!(
187 include_str!("../../../../data/tutorial/04-generating-alternatives.md"),
188 npc_name = npc_name,
189 ));
190 }
191 Self::EditingCharacters { npc, .. } => {
192 app_meta.command_aliases.insert(CommandAlias::literal(
193 "2",
194 format!("load {}", npc),
195 StorageCommand::Load {
196 name: npc.to_string(),
197 }
198 .into(),
199 ));
200
201 output.push_str(&format!(
202 include_str!("../../../../data/tutorial/05-viewing-alternatives.md"),
203 npc_name = npc,
204 ));
205 }
206 Self::TheJournal { npc, .. } => {
207 app_meta.command_aliases.insert(CommandAlias::literal(
208 "save",
209 format!("save {}", npc),
210 StorageCommand::Save {
211 name: npc.to_string(),
212 }
213 .into(),
214 ));
215
216 output.push_str(&format!(
217 include_str!("../../../../data/tutorial/06-editing-characters.md"),
218 npc_name = npc,
219 ));
220 }
221 Self::LoadingFromJournal { inn, .. } => output.push_str(&format!(
222 include_str!("../../../../data/tutorial/07-the-journal.md"),
223 inn_name = inn,
224 )),
225 Self::SrdReference { npc, .. } => output.push_str(&format!(
226 include_str!("../../../../data/tutorial/08-loading-from-journal.md"),
227 npc_name = npc,
228 )),
229 Self::SrdReferenceLists { .. } => output.push_str(include_str!(
230 "../../../../data/tutorial/09-srd-reference.md"
231 )),
232 Self::RollingDice { inn, npc } => output.push_str(&format!(
233 include_str!("../../../../data/tutorial/10-srd-reference-lists.md"),
234 inn_name = inn,
235 npc_name = npc,
236 )),
237 Self::DeletingThings { npc, .. } => output.push_str(&format!(
238 include_str!("../../../../data/tutorial/11-rolling-dice.md"),
239 npc_name = npc,
240 )),
241 Self::AdvancingTime { inn, npc } => output.push_str(&format!(
242 include_str!("../../../../data/tutorial/12-deleting-things.md"),
243 inn_name = inn,
244 npc_name = npc,
245 )),
246 Self::CheckingTheTime { .. } => output.push_str(include_str!(
247 "../../../../data/tutorial/13-advancing-time.md"
248 )),
249 Self::Conclusion { .. } => output.push_str(include_str!(
250 "../../../../data/tutorial/14-checking-the-time.md"
251 )),
252 }
253
254 if is_ok {
255 Ok(output)
256 } else {
257 Err(output)
258 }
259 }
260
261 fn inn(&self) -> Option<ThingRef> {
263 match self {
264 Self::Introduction
265 | Self::GeneratingLocations
266 | Self::SavingLocations
267 | Self::Resume => None,
268
269 Self::GeneratingCharacters { inn }
270 | Self::GeneratingAlternatives { inn }
271 | Self::ViewingAlternatives { inn, .. }
272 | Self::EditingCharacters { inn, .. }
273 | Self::TheJournal { inn, .. }
274 | Self::LoadingFromJournal { inn, .. }
275 | Self::SrdReference { inn, .. }
276 | Self::SrdReferenceLists { inn, .. }
277 | Self::RollingDice { inn, .. }
278 | Self::DeletingThings { inn, .. }
279 | Self::AdvancingTime { inn, .. }
280 | Self::CheckingTheTime { inn, .. }
281 | Self::Conclusion { inn, .. }
282 | Self::Cancel { inn: Some(inn), .. }
283 | Self::Restart { inn: Some(inn), .. } => Some(inn.clone()),
284
285 Self::Cancel { inn: None, .. } | Self::Restart { inn: None, .. } => None,
286 }
287 }
288
289 fn npc(&self) -> Option<ThingRef> {
291 match self {
292 Self::Introduction
293 | Self::GeneratingLocations
294 | Self::SavingLocations
295 | Self::Resume
296 | Self::GeneratingCharacters { .. }
297 | Self::GeneratingAlternatives { .. }
298 | Self::ViewingAlternatives { .. } => None,
299
300 Self::EditingCharacters { npc, .. }
301 | Self::TheJournal { npc, .. }
302 | Self::LoadingFromJournal { npc, .. }
303 | Self::SrdReference { npc, .. }
304 | Self::SrdReferenceLists { npc, .. }
305 | Self::RollingDice { npc, .. }
306 | Self::DeletingThings { npc, .. }
307 | Self::AdvancingTime { npc, .. }
308 | Self::CheckingTheTime { npc, .. }
309 | Self::Conclusion { npc, .. }
310 | Self::Cancel { npc: Some(npc), .. }
311 | Self::Restart { npc: Some(npc), .. } => Some(npc.clone()),
312
313 Self::Cancel { npc: None, .. } | Self::Restart { npc: None, .. } => None,
314 }
315 }
316
317 fn is_correct_command(&self, command: Option<&CommandType>) -> bool {
322 match self {
323 Self::Cancel { .. } | Self::Resume => false,
324 Self::Introduction | Self::Restart { .. } => true,
325 Self::GeneratingLocations => matches!(
326 command,
327 Some(CommandType::Tutorial(Self::GeneratingLocations))
328 ),
329 Self::SavingLocations => {
330 if let Some(CommandType::World(WorldCommand::Create { parsed_thing_data })) =
331 command
332 {
333 parsed_thing_data.thing_data
334 == "inn".parse::<ParsedThing<ThingData>>().unwrap().thing_data
335 } else {
336 false
337 }
338 }
339 Self::GeneratingCharacters { inn } => {
340 if let Some(CommandType::Storage(StorageCommand::Save { name })) = command {
341 name.eq_ci(&inn.name)
342 } else {
343 false
344 }
345 }
346 Self::GeneratingAlternatives { .. } => {
347 if let Some(CommandType::World(WorldCommand::Create {
348 parsed_thing_data:
349 ParsedThing {
350 thing_data,
351 unknown_words: _,
352 word_count: _,
353 },
354 })) = command
355 {
356 thing_data.npc_data()
357 == Some(&NpcData {
358 species: Species::Human.into(),
359 ethnicity: Ethnicity::Human.into(),
360 age: Age::Adult.into(),
361 gender: Gender::Feminine.into(),
362 ..Default::default()
363 })
364 } else {
365 false
366 }
367 }
368 Self::ViewingAlternatives { .. } => {
369 if let Some(CommandType::World(WorldCommand::CreateMultiple { thing_data })) =
370 command
371 {
372 thing_data.npc_data()
373 == Some(&NpcData {
374 species: Species::Human.into(),
375 ethnicity: Ethnicity::Human.into(),
376 age: Age::Adult.into(),
377 gender: Gender::Feminine.into(),
378 ..Default::default()
379 })
380 } else {
381 false
382 }
383 }
384 Self::EditingCharacters { npc, .. } => {
385 if let Some(CommandType::Storage(StorageCommand::Load { name })) = command {
386 name.eq_ci(&npc.name)
387 } else {
388 false
389 }
390 }
391 Self::TheJournal { npc, .. } => {
392 if let Some(CommandType::World(WorldCommand::Edit {
393 name,
394 parsed_diff:
395 ParsedThing {
396 thing_data,
397 unknown_words: _,
398 word_count: _,
399 },
400 })) = command
401 {
402 name.eq_ci(&npc.name)
403 && thing_data.npc_data()
404 == Some(&NpcData {
405 species: Species::HalfElf.into(),
406 ..Default::default()
407 })
408 } else {
409 false
410 }
411 }
412 Self::LoadingFromJournal { .. } => {
413 matches!(command, Some(CommandType::Storage(StorageCommand::Journal)))
414 }
415 Self::SrdReference { npc, .. } => {
416 if let Some(CommandType::Storage(StorageCommand::Load { name })) = command {
417 name.eq_ci(&npc.name)
418 } else {
419 false
420 }
421 }
422 Self::SrdReferenceLists { .. } => {
423 matches!(
424 command,
425 Some(CommandType::Reference(ReferenceCommand::Spell(
426 Spell::Fireball
427 ))),
428 )
429 }
430 Self::RollingDice { .. } => {
431 matches!(
432 command,
433 Some(CommandType::Reference(ReferenceCommand::ItemCategory(
434 ItemCategory::Weapon
435 ))),
436 )
437 }
438 Self::DeletingThings { .. } => {
439 matches!(command, Some(CommandType::App(AppCommand::Roll(_))))
440 }
441 Self::AdvancingTime { inn, .. } => {
442 if let Some(CommandType::Storage(StorageCommand::Delete { name })) = command {
443 name.eq_ci(&inn.name)
444 } else {
445 false
446 }
447 }
448 Self::CheckingTheTime { .. } => {
449 matches!(command, Some(CommandType::Time(TimeCommand::Add { .. })))
450 }
451 Self::Conclusion { .. } => matches!(command, Some(CommandType::Time(TimeCommand::Now))),
452 }
453 }
454}
455
456#[async_trait(?Send)]
457impl Runnable for TutorialCommand {
458 async fn run(self, input: &str, app_meta: &mut AppMeta) -> Result<String, String> {
459 let input_command = Command::parse_input_irrefutable(input, app_meta).await;
460
461 if let Some(CommandType::Tutorial(
462 TutorialCommand::Cancel { inn, npc } | TutorialCommand::Restart { inn, npc },
463 )) = input_command.get_type()
464 {
465 if let Some(inn) = inn {
466 app_meta
467 .repository
468 .modify(Change::Delete {
469 uuid: inn.uuid,
470 name: inn.name.clone(),
471 })
472 .await
473 .ok();
474 }
475
476 if let Some(npc) = npc {
477 app_meta
478 .repository
479 .modify(Change::Delete {
480 uuid: npc.uuid,
481 name: npc.name.clone(),
482 })
483 .await
484 .ok();
485 }
486 }
487
488 app_meta.command_aliases.clear();
489
490 let (result, next_command) = if self.is_correct_command(input_command.get_type()) {
491 match self {
492 Self::Cancel { .. } | Self::Resume => unreachable!(),
493 Self::Introduction | Self::Restart { .. } => {
494 let next = Self::GeneratingLocations;
495 (next.output(None, app_meta), Some(next))
496 }
497 Self::GeneratingLocations => {
498 let next = Self::SavingLocations;
499 (next.output(None, app_meta), Some(next))
500 }
501 Self::SavingLocations => {
502 let command_output = match input_command.run(input, app_meta).await {
503 Ok(output) => {
504 let inn_name = output
505 .lines()
506 .find(|s| s.starts_with('#'))
507 .unwrap()
508 .trim_start_matches(&[' ', '#'][..]);
509
510 let Record { thing: inn, .. } =
511 app_meta.repository.get_by_name(inn_name).await.unwrap();
512
513 Ok((
514 ThingRef {
515 name: inn.name().to_string(),
516 uuid: inn.uuid,
517 },
518 output,
519 ))
520 }
521 Err(e) => Err(e),
522 };
523
524 match command_output {
525 Ok((inn, output)) => {
526 let next = Self::GeneratingCharacters { inn };
527 (next.output(Some(Ok(output)), app_meta), Some(next))
528 }
529 Err(e) => (Err(e), Some(self)),
530 }
531 }
532 Self::GeneratingCharacters { inn } => {
533 let next = Self::GeneratingAlternatives { inn };
534
535 (
536 next.output(Some(input_command.run(input, app_meta).await), app_meta),
537 Some(next),
538 )
539 }
540 Self::GeneratingAlternatives { inn } => {
541 let command_output = match input_command.run(input, app_meta).await {
542 Ok(output) => {
543 let npc_name = output
544 .lines()
545 .find(|s| s.starts_with('#'))
546 .unwrap()
547 .trim_start_matches(&[' ', '#'][..])
548 .to_string();
549
550 Ok((npc_name, output))
551 }
552 Err(e) => Err(e),
553 };
554
555 match command_output {
556 Ok((npc_name, output)) => {
557 let next = Self::ViewingAlternatives { inn, npc_name };
558 (next.output(Some(Ok(output)), app_meta), Some(next))
559 }
560 Err(e) => (Err(e), Some(Self::GeneratingAlternatives { inn })),
561 }
562 }
563 Self::ViewingAlternatives { inn, npc_name } => {
564 let command_output = input_command.run(input, app_meta).await;
565
566 if let Ok(output) = command_output {
567 if let Some(new_npc_name) = output
568 .lines()
569 .find(|s| s.starts_with("~2~"))
570 .and_then(|s| s.find('(').map(|i| (i, s)))
571 .map(|(i, s)| s[10..i - 2].to_string())
572 {
573 let Record { thing: new_npc, .. } = app_meta
574 .repository
575 .get_by_name(&new_npc_name)
576 .await
577 .unwrap();
578
579 let new_npc = ThingRef {
580 name: new_npc_name,
581 uuid: new_npc.uuid,
582 };
583 let next = Self::EditingCharacters { npc: new_npc, inn };
584
585 (next.output(Some(Ok(output)), app_meta), Some(next))
586 } else {
587 (
588 Ok(output),
589 Some(Self::ViewingAlternatives { inn, npc_name }),
590 )
591 }
592 } else {
593 (
594 command_output,
595 Some(Self::ViewingAlternatives { inn, npc_name }),
596 )
597 }
598 }
599 Self::EditingCharacters { inn, npc } => {
600 let command_output = input_command.run(input, app_meta).await;
601
602 if let Ok(output) = command_output {
603 let next = Self::TheJournal { inn, npc };
604
605 (next.output(Some(Ok(output)), app_meta), Some(next))
606 } else {
607 (command_output, Some(Self::EditingCharacters { inn, npc }))
608 }
609 }
610 Self::TheJournal { inn, npc } => {
611 let next = Self::LoadingFromJournal { inn, npc };
612
613 (
614 next.output(Some(input_command.run(input, app_meta).await), app_meta),
615 Some(next),
616 )
617 }
618 Self::LoadingFromJournal { inn, npc } => {
619 let next = Self::SrdReference { inn, npc };
620
621 (
622 next.output(Some(input_command.run(input, app_meta).await), app_meta),
623 Some(next),
624 )
625 }
626 Self::SrdReference { inn, npc } => {
627 let next = Self::SrdReferenceLists { inn, npc };
628
629 (
630 next.output(Some(input_command.run(input, app_meta).await), app_meta),
631 Some(next),
632 )
633 }
634 Self::SrdReferenceLists { inn, npc } => {
635 let next = Self::RollingDice { inn, npc };
636
637 (
638 next.output(Some(input_command.run(input, app_meta).await), app_meta),
639 Some(next),
640 )
641 }
642 Self::RollingDice { inn, npc } => {
643 let next = Self::DeletingThings { inn, npc };
644
645 (
646 next.output(Some(input_command.run(input, app_meta).await), app_meta),
647 Some(next),
648 )
649 }
650 Self::DeletingThings { inn, npc } => {
651 let next = Self::AdvancingTime { inn, npc };
652
653 (
654 next.output(Some(input_command.run(input, app_meta).await), app_meta),
655 Some(next),
656 )
657 }
658 Self::AdvancingTime { inn, npc, .. } => {
659 let next = Self::CheckingTheTime { inn, npc };
660
661 (
662 next.output(Some(input_command.run(input, app_meta).await), app_meta),
663 Some(next),
664 )
665 }
666 Self::CheckingTheTime { inn, npc } => {
667 let next = Self::Conclusion { inn, npc };
668
669 (
670 next.output(Some(input_command.run(input, app_meta).await), app_meta),
671 Some(next),
672 )
673 }
674 Self::Conclusion { inn, npc } => {
675 app_meta
676 .repository
677 .modify(Change::Delete {
678 name: inn.name,
679 uuid: inn.uuid,
680 })
681 .await
682 .ok();
683 app_meta
684 .repository
685 .modify(Change::Delete {
686 name: npc.name,
687 uuid: npc.uuid,
688 })
689 .await
690 .ok();
691
692 (
693 input_command.run(input, app_meta).await.map(|mut output| {
694 output.push_str("\n\n#");
695 output.push_str(include_str!(
696 "../../../../data/tutorial/99-conclusion.md"
697 ));
698 output
699 }),
700 None,
701 )
702 }
703 }
704 } else if let Some(CommandType::Tutorial(TutorialCommand::Cancel { .. })) =
705 input_command.get_type()
706 {
707 (
708 Ok(include_str!("../../../../data/tutorial/xx-cancelled.md").to_string()),
709 None,
710 )
711 } else if let Some(CommandType::Tutorial(TutorialCommand::Resume)) =
712 input_command.get_type()
713 {
714 (self.output(None, app_meta), Some(self))
715 } else {
716 let result = {
717 let f = |mut s: String| {
718 if !s.is_empty() {
719 s.push_str("\n\n#");
720 }
721 s.push_str(include_str!("../../../../data/tutorial/xx-still-active.md"));
722 s
723 };
724
725 if !matches!(
726 input_command.get_type(),
727 Some(CommandType::Tutorial(TutorialCommand::Introduction))
728 ) {
729 input_command.run(input, app_meta).await.map(f).map_err(f)
730 } else {
731 Ok(f(String::new()))
732 }
733 };
734
735 app_meta.command_aliases.insert(CommandAlias::literal(
736 "resume",
737 "return to the tutorial",
738 Self::Resume.into(),
739 ));
740
741 app_meta.command_aliases.insert(CommandAlias::literal(
742 "restart",
743 "restart the tutorial",
744 Self::Restart {
745 inn: self.inn(),
746 npc: self.npc(),
747 }
748 .into(),
749 ));
750
751 (result, Some(self))
752 };
753
754 if let Some(command) = next_command {
755 app_meta.command_aliases.insert(CommandAlias::literal(
756 "cancel",
757 "cancel the tutorial",
758 Self::Cancel {
759 inn: command.inn(),
760 npc: command.npc(),
761 }
762 .into(),
763 ));
764
765 app_meta
766 .command_aliases
767 .insert(CommandAlias::strict_wildcard(command.into()));
768 }
769
770 result
771 }
772}
773
774#[async_trait(?Send)]
775impl ContextAwareParse for TutorialCommand {
776 async fn parse_input(input: &str, _app_meta: &AppMeta) -> CommandMatches<Self> {
777 if input.eq_ci("tutorial") {
778 CommandMatches::new_canonical(TutorialCommand::Introduction)
779 } else {
780 CommandMatches::default()
781 }
782 }
783}
784
785#[async_trait(?Send)]
786impl Autocomplete for TutorialCommand {
787 async fn autocomplete(input: &str, _app_meta: &AppMeta) -> Vec<AutocompleteSuggestion> {
788 if "tutorial".starts_with_ci(input) {
789 vec![AutocompleteSuggestion::new(
790 "tutorial",
791 "feature walkthrough",
792 )]
793 } else {
794 Vec::new()
795 }
796 }
797}
798
799impl fmt::Display for TutorialCommand {
800 fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
801 match self {
802 Self::Introduction => write!(f, "tutorial"),
803 _ => Ok(()),
804 }
805 }
806}
807
808impl fmt::Display for ThingRef {
809 fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
810 write!(f, "{}", self.name)
811 }
812}