1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4pub enum ScriptCommand {
5 ShowText {
6 text: String,
7 },
8 ShowChoice {
9 options: Vec<String>,
10 },
11 GiveItem {
12 item_id: String,
13 quantity: u8,
14 },
15 GiveMonster {
16 species: String,
17 level: u8,
18 },
19 TakeItem {
20 item_id: String,
21 quantity: u8,
22 },
23 SetFlag {
24 flag: String,
25 },
26 ResetFlag {
27 flag: String,
28 },
29 CheckFlag {
30 flag: String,
31 },
32 ShowObject {
33 object_index: u8,
34 },
35 HideObject {
36 object_index: u8,
37 },
38 ShowObjectByName {
39 toggle_id: String,
40 },
41 HideObjectByName {
42 toggle_id: String,
43 },
44 MoveNpc {
45 npc_id: String,
46 path: Vec<(u8, u8)>,
47 },
48 StartNpcMove {
49 npc_id: String,
50 path: Vec<(u8, u8)>,
51 },
52 AwaitNpcMove {
53 npc_id: String,
54 },
55 MovePlayer {
56 path: Vec<(u8, u8)>,
57 },
58 MovePlayerRelative {
63 steps: Vec<(i16, i16)>,
64 },
65 MoveNpcTo {
66 npc_id: String,
67 x: u8,
68 y: u8,
69 },
70 StartNpcMoveTo {
71 npc_id: String,
72 x: u8,
73 y: u8,
74 },
75 MovePlayerTo {
76 x: u8,
77 y: u8,
78 },
79 FaceNpc {
80 npc_id: String,
81 direction: String,
82 },
83 FacePlayer {
84 direction: String,
85 },
86 PlayMusic {
87 music_id: String,
88 },
89 PlaySound {
90 sound_id: String,
91 },
92 StopMusic,
93 FadeOutMusic,
94 StartBattle {
95 trainer_id: String,
96 },
97 StartWildBattle {
101 species: String,
102 level: u8,
103 },
104 SetWeather {
110 weather: Option<String>,
111 },
112 Delay {
113 frames: u16,
114 },
115 WarpTo {
116 map: String,
117 x: u8,
118 y: u8,
119 },
120 Heal,
121 FadeScreen {
122 fade_type: String,
123 },
124 SetJoyIgnore {
125 mask: u8,
126 },
127 ClearJoyIgnore,
128 FollowNpc {
129 npc_id: String,
130 target_x: u8,
131 target_y: u8,
132 },
133 OpenShop {
134 items: Vec<String>,
135 },
136 ShowEmotionBubble {
137 npc_id: String,
138 emotion: String,
139 },
140 SetNpcPosition {
141 npc_id: String,
142 x: u8,
143 y: u8,
144 },
145 SetNpcFrame {
146 npc_id: String,
147 frame: u8,
148 },
149 ShowScene {
150 scene_name: String,
151 layout_json: Option<String>,
152 },
153 HideScene {
154 scene_name: String,
155 },
156 UpdateUI {
157 scene_name: String,
158 data_json: String,
159 },
160 GiveMoney {
161 amount: u32,
162 },
163 TakeMoney {
164 amount: u32,
165 },
166 PlayCry {
167 species: String,
168 },
169 GiveBadge {
170 badge: u8,
171 },
172 Custom {
178 name: String,
179 args: Vec<serde_json::Value>,
180 },
181}
182
183#[derive(Debug, Clone, PartialEq)]
184pub enum CommandResult {
185 Void,
186 Bool(bool),
187 Number(f64),
188 Text(String),
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn test_show_text_command() {
197 let cmd = ScriptCommand::ShowText {
198 text: "Hello".to_string(),
199 };
200 assert_eq!(
201 cmd,
202 ScriptCommand::ShowText {
203 text: "Hello".to_string()
204 }
205 );
206 }
207
208 #[test]
209 fn test_show_choice_command() {
210 let cmd = ScriptCommand::ShowChoice {
211 options: vec!["Yes".to_string(), "No".to_string()],
212 };
213 assert_eq!(
214 cmd,
215 ScriptCommand::ShowChoice {
216 options: vec!["Yes".to_string(), "No".to_string()]
217 }
218 );
219 }
220
221 #[test]
222 fn test_serialize_deserialize_roundtrip() {
223 let cmds: Vec<ScriptCommand> = vec![
224 ScriptCommand::ShowText {
225 text: "Hello".into(),
226 },
227 ScriptCommand::ShowChoice {
228 options: vec!["A".into(), "B".into()],
229 },
230 ScriptCommand::GiveItem {
231 item_id: "POTION".into(),
232 quantity: 5,
233 },
234 ScriptCommand::GiveMonster {
235 species: "SPARKIT".into(),
236 level: 5,
237 },
238 ScriptCommand::Heal,
239 ScriptCommand::StopMusic,
240 ScriptCommand::ClearJoyIgnore,
241 ScriptCommand::WarpTo {
242 map: "START_TOWN".into(),
243 x: 5,
244 y: 3,
245 },
246 ScriptCommand::Delay { frames: 60 },
247 ScriptCommand::PlayMusic {
248 music_id: "START_TOWN".into(),
249 },
250 ScriptCommand::PlaySound {
251 sound_id: "SFX_BALL".into(),
252 },
253 ScriptCommand::FadeOutMusic,
254 ScriptCommand::FadeScreen {
255 fade_type: "out".into(),
256 },
257 ScriptCommand::SetJoyIgnore { mask: 0xFF },
258 ScriptCommand::MoveNpc {
259 npc_id: "prof".into(),
260 path: vec![(2, 3), (4, 5)],
261 },
262 ScriptCommand::FollowNpc {
263 npc_id: "rival".into(),
264 target_x: 10,
265 target_y: 8,
266 },
267 ScriptCommand::OpenShop {
268 items: vec!["POTION".into()],
269 },
270 ScriptCommand::GiveMoney { amount: 500 },
271 ScriptCommand::TakeMoney { amount: 100 },
272 ScriptCommand::GiveBadge { badge: 0 },
273 ScriptCommand::Custom {
274 name: "tradeMonster".into(),
275 args: vec![serde_json::json!("SPARKIT")],
276 },
277 ScriptCommand::ShowObject { object_index: 1 },
278 ScriptCommand::HideObjectByName {
279 toggle_id: "HIDDEN_ITEM".into(),
280 },
281 ScriptCommand::SetWeather {
282 weather: Some("sandstorm".into()),
283 },
284 ScriptCommand::SetWeather { weather: None },
285 ];
286
287 for cmd in &cmds {
288 let json = serde_json::to_string(cmd).unwrap();
289 let deserialized: ScriptCommand = serde_json::from_str(&json).unwrap();
290 assert_eq!(*cmd, deserialized, "round-trip failed for {cmd:?}");
291 }
292 }
293
294 #[test]
295 fn test_command_result_equality() {
296 assert_eq!(CommandResult::Void, CommandResult::Void);
297 assert_eq!(CommandResult::Bool(true), CommandResult::Bool(true));
298 assert_eq!(CommandResult::Bool(false), CommandResult::Bool(false));
299 assert_ne!(CommandResult::Bool(true), CommandResult::Bool(false));
300 assert_eq!(CommandResult::Number(42.0), CommandResult::Number(42.0));
301 assert_ne!(CommandResult::Number(1.0), CommandResult::Number(2.0));
302 assert_eq!(
303 CommandResult::Text("hello".into()),
304 CommandResult::Text("hello".into())
305 );
306 assert_ne!(
307 CommandResult::Text("hello".into()),
308 CommandResult::Text("world".into())
309 );
310 }
311
312 #[test]
313 fn test_command_result_debug() {
314 let void = CommandResult::Void;
315 assert!(!format!("{void:?}").is_empty());
316
317 let b = CommandResult::Bool(true);
318 assert_eq!(format!("{b:?}"), "Bool(true)");
319
320 let n = CommandResult::Number(2.5);
321 assert!(format!("{n:?}").contains("2.5"));
322
323 let t = CommandResult::Text("result".into());
324 assert_eq!(format!("{t:?}"), "Text(\"result\")");
325 }
326
327 #[test]
328 fn test_give_take_money() {
329 let give = ScriptCommand::GiveMoney { amount: 999 };
330 let take = ScriptCommand::TakeMoney { amount: 50 };
331 assert_ne!(give, take);
332 if let ScriptCommand::GiveMoney { amount } = &give {
333 assert_eq!(*amount, 999);
334 } else {
335 panic!("expected GiveMoney");
336 }
337 }
338
339 #[test]
340 fn test_show_hide_scene() {
341 let show = ScriptCommand::ShowScene {
342 scene_name: "shop".into(),
343 layout_json: None,
344 };
345 let hide = ScriptCommand::HideScene {
346 scene_name: "shop".into(),
347 };
348 assert_ne!(show, hide);
349 assert_eq!(
350 show,
351 ScriptCommand::ShowScene {
352 scene_name: "shop".into(),
353 layout_json: None
354 }
355 );
356 }
357
358 #[test]
359 fn test_update_ui_command() {
360 let cmd = ScriptCommand::UpdateUI {
361 scene_name: "bag".into(),
362 data_json: r#"{"gold":500}"#.into(),
363 };
364 assert!(format!("{cmd:?}").contains("bag"));
365 assert!(format!("{cmd:?}").contains("gold"));
366 }
367}