shared/domain/module/body/
find_answer.rs

1use crate::domain::module::{
2    body::{
3        Body, BodyConvert, BodyExt, ModeExt, StepExt, ThemeId,
4        _groups::design::{BaseContent, Trace},
5    },
6    ModuleKind,
7};
8use serde::{Deserialize, Serialize};
9use std::collections::HashSet;
10
11mod play_settings;
12pub use play_settings::*;
13
14use super::{
15    Audio, Transform,
16    _groups::design::{Sticker, Text},
17};
18
19/// The body for [`FindAnswer`](crate::domain::module::ModuleKind::FindAnswer) modules.
20#[derive(Default, Clone, Serialize, Deserialize, Debug)]
21pub struct ModuleData {
22    /// The content
23    pub content: Option<Content>,
24}
25
26impl BodyExt<Mode, Step> for ModuleData {
27    fn as_body(&self) -> Body {
28        Body::FindAnswer(self.clone())
29    }
30
31    fn is_complete(&self) -> bool {
32        self.content
33            .as_ref()
34            .map_or(false, |content| content.is_valid())
35    }
36
37    fn kind() -> ModuleKind {
38        ModuleKind::FindAnswer
39    }
40
41    fn new_with_mode_and_theme(mode: Mode, theme: ThemeId) -> Self {
42        let mut base = BaseContent::default();
43        base.theme = theme;
44
45        let mut sticker_content = Text::from_value(format!(
46            r#"{{"version":"0.1.0","content":[{{"children":[{{"text":"{}","element":"P2"}}]}}]}}"#,
47            "Questions appear here / שאלות מופיעות כאן"
48        ));
49        sticker_content.transform.translation.0[1] = -0.35; // Move Y coord up 35%
50        base.stickers.push(Sticker::Text(sticker_content));
51        let question_field_index = base.stickers.len() - 1;
52
53        Self {
54            content: Some(Content {
55                mode,
56                base,
57                question_field: QuestionField::Text(question_field_index),
58                ..Default::default()
59            }),
60        }
61    }
62
63    fn mode(&self) -> Option<Mode> {
64        self.content.as_ref().map(|c| c.mode.clone())
65    }
66
67    fn requires_choose_mode(&self) -> bool {
68        self.content.is_none()
69    }
70
71    fn set_editor_state_step(&mut self, step: Step) {
72        if let Some(content) = self.content.as_mut() {
73            content.editor_state.step = step;
74        }
75    }
76    fn set_editor_state_steps_completed(&mut self, steps_completed: HashSet<Step>) {
77        if let Some(content) = self.content.as_mut() {
78            content.editor_state.steps_completed = steps_completed;
79        }
80    }
81
82    fn get_editor_state_step(&self) -> Option<Step> {
83        self.content
84            .as_ref()
85            .map(|content| content.editor_state.step)
86    }
87
88    fn get_editor_state_steps_completed(&self) -> Option<HashSet<Step>> {
89        self.content
90            .as_ref()
91            .map(|content| content.editor_state.steps_completed.clone())
92    }
93
94    fn set_theme(&mut self, theme_id: ThemeId) {
95        if let Some(content) = self.content.as_mut() {
96            content.base.theme = theme_id;
97        }
98    }
99
100    fn get_theme(&self) -> Option<ThemeId> {
101        self.content.as_ref().map(|content| content.base.theme)
102    }
103}
104
105impl BodyConvert for ModuleData {}
106
107impl TryFrom<Body> for ModuleData {
108    type Error = &'static str;
109
110    fn try_from(body: Body) -> Result<Self, Self::Error> {
111        match body {
112            Body::FindAnswer(data) => Ok(data),
113            _ => Err("cannot convert body to Answer This!"),
114        }
115    }
116}
117
118/// The body for [`FindAnswer`](crate::domain::module::ModuleKind::FindAnswer) modules.
119#[derive(Default, Clone, Serialize, Deserialize, Debug)]
120pub struct Content {
121    /// The base content for all design modules
122    pub base: BaseContent,
123
124    /// The editor state
125    pub editor_state: EditorState,
126
127    /// The mode
128    pub mode: Mode,
129
130    /// Questions
131    pub questions: Vec<Question>,
132
133    /// Sticker index of the related question sticker
134    pub question_field: QuestionField,
135
136    /// play settings
137    pub play_settings: PlaySettings,
138}
139
140impl Content {
141    /// Convenience method to determine whether questions have configured correctly
142    pub fn is_valid(&self) -> bool {
143        !self.questions.is_empty()
144            && self
145                .questions
146                .iter()
147                .find(|question| !question.is_valid())
148                .is_none()
149    }
150}
151
152/// The type of field to be used for displaying question text.
153#[derive(Clone, Serialize, Deserialize, Debug)]
154pub enum QuestionField {
155    /// Index of the text sticker to be used as the question field.
156    Text(usize),
157    /// When the teacher hasn't added or selected a text sticker, a dynamic label will be added to
158    /// display the question. The teacher can move this around.
159    ///
160    /// Note (Ty): We won't make use of the scale field right now, but at some point we should add
161    /// the ability to scale the label text
162    Dynamic(Option<Transform>),
163}
164
165impl QuestionField {
166    /// Whether the current variant is `Dynamic`
167    pub fn is_dynamic(&self) -> bool {
168        matches!(self, Self::Dynamic(_))
169    }
170
171    /// Whether the current variant is `Text`
172    pub fn is_text(&self) -> bool {
173        matches!(self, Self::Text(_))
174    }
175}
176
177impl Default for QuestionField {
178    fn default() -> Self {
179        QuestionField::Dynamic(None)
180    }
181}
182
183/// Represents a single question
184#[derive(Default, Clone, Serialize, Deserialize, Debug)]
185pub struct Question {
186    /// Title of the question
187    pub title: String,
188
189    /// The question text
190    pub question_text: String,
191
192    /// Optional audio for the question
193    pub question_audio: Option<Audio>,
194
195    /// Optional audio for incorrect choices
196    pub incorrect_audio: Option<Audio>,
197
198    #[serde(default)]
199    /// Optional audio for correct choices
200    pub correct_audio: Option<Audio>,
201
202    /// Traces
203    pub traces: Vec<Trace>,
204}
205
206impl Question {
207    /// Convenience method to determine whether a question has been configured correctly
208    pub fn is_valid(&self) -> bool {
209        !self.traces.is_empty()
210    }
211}
212
213/// Editor state
214#[derive(Default, Clone, Serialize, Deserialize, Debug)]
215pub struct EditorState {
216    /// the current step
217    pub step: Step,
218
219    /// the completed steps
220    pub steps_completed: HashSet<Step>,
221}
222
223#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)]
224/// The mode
225pub enum Mode {
226    /// Family mode
227    Family,
228    /// Map mode
229    Map,
230    /// Multiple mode
231    MultipleChoice,
232    /// Scene mode
233    Scene,
234    /// Text mode
235    Text,
236    /// Find the differences mode
237    Differences,
238}
239
240impl Default for Mode {
241    fn default() -> Self {
242        Self::MultipleChoice
243    }
244}
245
246impl ModeExt for Mode {
247    fn get_list() -> Vec<Self> {
248        vec![
249            Self::Scene,
250            Self::MultipleChoice,
251            Self::Text,
252            Self::Differences,
253            Self::Map,
254            Self::Family,
255        ]
256    }
257
258    fn as_str_id(&self) -> &'static str {
259        match self {
260            Self::Family => "family",
261            Self::Map => "map",
262            Self::MultipleChoice => "multiple-choice",
263            Self::Scene => "scene",
264            Self::Text => "text",
265            Self::Differences => "differences",
266        }
267    }
268
269    fn label(&self) -> &'static str {
270        const STR_FAMILY_LABEL: &'static str = "Family Tree";
271        const STR_MAP_LABEL: &'static str = "Where on the Map?";
272        const STR_MULTIPLE_CHOICE_LABEL: &'static str = "Find the Answers";
273        const STR_SCENE_LABEL: &'static str = "Spot It";
274        const STR_TEXT_LABEL: &'static str = "Find the Text";
275        const STR_DIFFERENCES_LABEL: &'static str = "Find the Differences";
276
277        match self {
278            Self::Family => STR_FAMILY_LABEL,
279            Self::Map => STR_MAP_LABEL,
280            Self::MultipleChoice => STR_MULTIPLE_CHOICE_LABEL,
281            Self::Scene => STR_SCENE_LABEL,
282            Self::Text => STR_TEXT_LABEL,
283            Self::Differences => STR_DIFFERENCES_LABEL,
284        }
285    }
286}
287
288/// The Steps
289#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
290pub enum Step {
291    /// Step 1
292    One,
293    /// Step 2
294    Two,
295    /// Step 3
296    Three,
297    /// Step 4
298    Four,
299    /// Step 5
300    Five,
301}
302
303impl Default for Step {
304    fn default() -> Self {
305        Self::One
306    }
307}
308
309impl StepExt for Step {
310    fn next(&self) -> Option<Self> {
311        match self {
312            Self::One => Some(Self::Two),
313            Self::Two => Some(Self::Three),
314            Self::Three => Some(Self::Four),
315            Self::Four => Some(Self::Five),
316            Self::Five => None,
317        }
318    }
319
320    fn as_number(&self) -> usize {
321        match self {
322            Self::One => 1,
323            Self::Two => 2,
324            Self::Three => 3,
325            Self::Four => 4,
326            Self::Five => 5,
327        }
328    }
329
330    fn label(&self) -> &'static str {
331        const STR_BACKGROUND: &'static str = "Design";
332        const STR_CONTENT: &'static str = "Content";
333        const STR_INTERACTION: &'static str = "Interaction";
334        const STR_SETTINGS: &'static str = "Settings";
335        const STR_PREVIEW: &'static str = "Preview";
336        match self {
337            Self::One => STR_BACKGROUND,
338            Self::Two => STR_CONTENT,
339            Self::Three => STR_INTERACTION,
340            Self::Four => STR_SETTINGS,
341            Self::Five => STR_PREVIEW,
342        }
343    }
344
345    fn get_list() -> Vec<Self> {
346        vec![Self::One, Self::Two, Self::Three, Self::Four, Self::Five]
347    }
348    fn get_preview() -> Self {
349        Self::Five
350    }
351}