1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12pub enum NarrativeFunction {
13 Revelation,
15 Escalation,
17 Confrontation,
19 Betrayal,
21 Alliance,
23 Discovery,
25 Loss,
27 ComicRelief,
29 Foreshadowing,
31 StatusChange,
33 Custom(String),
35}
36
37impl NarrativeFunction {
38 pub fn pacing(&self) -> f32 {
40 match self {
41 Self::Revelation => 0.4,
42 Self::Escalation => 0.8,
43 Self::Confrontation => 0.7,
44 Self::Betrayal => 0.6,
45 Self::Alliance => 0.3,
46 Self::Discovery => 0.5,
47 Self::Loss => 0.4,
48 Self::ComicRelief => 0.6,
49 Self::Foreshadowing => 0.2,
50 Self::StatusChange => 0.5,
51 Self::Custom(_) => 0.5,
52 }
53 }
54
55 pub fn valence(&self) -> f32 {
57 match self {
58 Self::Revelation => 0.0,
59 Self::Escalation => -0.3,
60 Self::Confrontation => -0.5,
61 Self::Betrayal => -0.8,
62 Self::Alliance => 0.6,
63 Self::Discovery => 0.5,
64 Self::Loss => -0.7,
65 Self::ComicRelief => 0.7,
66 Self::Foreshadowing => -0.2,
67 Self::StatusChange => 0.0,
68 Self::Custom(_) => 0.0,
69 }
70 }
71
72 pub fn intensity(&self) -> f32 {
74 match self {
75 Self::Revelation => 0.7,
76 Self::Escalation => 0.8,
77 Self::Confrontation => 0.9,
78 Self::Betrayal => 0.9,
79 Self::Alliance => 0.4,
80 Self::Discovery => 0.6,
81 Self::Loss => 0.8,
82 Self::ComicRelief => 0.3,
83 Self::Foreshadowing => 0.3,
84 Self::StatusChange => 0.5,
85 Self::Custom(_) => 0.5,
86 }
87 }
88
89 pub fn name(&self) -> &str {
91 match self {
92 Self::Revelation => "revelation",
93 Self::Escalation => "escalation",
94 Self::Confrontation => "confrontation",
95 Self::Betrayal => "betrayal",
96 Self::Alliance => "alliance",
97 Self::Discovery => "discovery",
98 Self::Loss => "loss",
99 Self::ComicRelief => "comic_relief",
100 Self::Foreshadowing => "foreshadowing",
101 Self::StatusChange => "status_change",
102 Self::Custom(name) => name.as_str(),
103 }
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn narrative_fn_variants() {
113 let f = NarrativeFunction::Revelation;
114 assert!(matches!(f, NarrativeFunction::Revelation));
115
116 let custom = NarrativeFunction::Custom("trade".to_string());
117 assert!(matches!(custom, NarrativeFunction::Custom(_)));
118 }
119
120 #[test]
121 fn pacing_values_in_range() {
122 let variants = [
123 NarrativeFunction::Revelation,
124 NarrativeFunction::Escalation,
125 NarrativeFunction::Confrontation,
126 NarrativeFunction::Betrayal,
127 NarrativeFunction::Alliance,
128 NarrativeFunction::Discovery,
129 NarrativeFunction::Loss,
130 NarrativeFunction::ComicRelief,
131 NarrativeFunction::Foreshadowing,
132 NarrativeFunction::StatusChange,
133 NarrativeFunction::Custom("test".to_string()),
134 ];
135 for v in &variants {
136 let p = v.pacing();
137 assert!(
138 (0.0..=1.0).contains(&p),
139 "{:?} pacing {} out of range",
140 v,
141 p
142 );
143 }
144 }
145
146 #[test]
147 fn valence_values_in_range() {
148 let variants = [
149 NarrativeFunction::Revelation,
150 NarrativeFunction::Escalation,
151 NarrativeFunction::Confrontation,
152 NarrativeFunction::Betrayal,
153 NarrativeFunction::Alliance,
154 NarrativeFunction::Discovery,
155 NarrativeFunction::Loss,
156 NarrativeFunction::ComicRelief,
157 NarrativeFunction::Foreshadowing,
158 NarrativeFunction::StatusChange,
159 ];
160 for v in &variants {
161 let val = v.valence();
162 assert!(
163 (-1.0..=1.0).contains(&val),
164 "{:?} valence {} out of range",
165 v,
166 val
167 );
168 }
169 }
170
171 #[test]
172 fn intensity_values_in_range() {
173 let variants = [
174 NarrativeFunction::Revelation,
175 NarrativeFunction::Escalation,
176 NarrativeFunction::Confrontation,
177 NarrativeFunction::Betrayal,
178 NarrativeFunction::Alliance,
179 NarrativeFunction::Discovery,
180 NarrativeFunction::Loss,
181 NarrativeFunction::ComicRelief,
182 NarrativeFunction::Foreshadowing,
183 NarrativeFunction::StatusChange,
184 ];
185 for v in &variants {
186 let i = v.intensity();
187 assert!(
188 (0.0..=1.0).contains(&i),
189 "{:?} intensity {} out of range",
190 v,
191 i
192 );
193 }
194 }
195
196 #[test]
197 fn confrontation_is_high_intensity() {
198 let c = NarrativeFunction::Confrontation;
199 assert!(c.intensity() >= 0.8);
200 assert!(c.valence() < 0.0);
201 }
202
203 #[test]
204 fn alliance_is_positive_valence() {
205 let a = NarrativeFunction::Alliance;
206 assert!(a.valence() > 0.0);
207 assert!(a.intensity() < 0.5);
208 }
209
210 #[test]
211 fn foreshadowing_is_slow_paced() {
212 let f = NarrativeFunction::Foreshadowing;
213 assert!(f.pacing() <= 0.3);
214 }
215
216 #[test]
217 fn name_returns_snake_case() {
218 assert_eq!(NarrativeFunction::ComicRelief.name(), "comic_relief");
219 assert_eq!(NarrativeFunction::StatusChange.name(), "status_change");
220 assert_eq!(NarrativeFunction::Revelation.name(), "revelation");
221 assert_eq!(
222 NarrativeFunction::Custom("my_fn".to_string()).name(),
223 "my_fn"
224 );
225 }
226}