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!(cmd, ScriptCommand::ShowText { text: "Hello".to_string() });
201 }
202
203 #[test]
204 fn test_show_choice_command() {
205 let cmd = ScriptCommand::ShowChoice {
206 options: vec!["Yes".to_string(), "No".to_string()],
207 };
208 assert_eq!(
209 cmd,
210 ScriptCommand::ShowChoice {
211 options: vec!["Yes".to_string(), "No".to_string()]
212 }
213 );
214 }
215
216 #[test]
217 fn test_serialize_deserialize_roundtrip() {
218 let cmds: Vec<ScriptCommand> = vec![
219 ScriptCommand::ShowText { text: "Hello".into() },
220 ScriptCommand::ShowChoice { options: vec!["A".into(), "B".into()] },
221 ScriptCommand::GiveItem { item_id: "POTION".into(), quantity: 5 },
222 ScriptCommand::GiveMonster { species: "SPARKIT".into(), level: 5 },
223 ScriptCommand::Heal,
224 ScriptCommand::StopMusic,
225 ScriptCommand::ClearJoyIgnore,
226 ScriptCommand::WarpTo { map: "START_TOWN".into(), x: 5, y: 3 },
227 ScriptCommand::Delay { frames: 60 },
228 ScriptCommand::PlayMusic { music_id: "START_TOWN".into() },
229 ScriptCommand::PlaySound { sound_id: "SFX_BALL".into() },
230 ScriptCommand::FadeOutMusic,
231 ScriptCommand::FadeScreen { fade_type: "out".into() },
232 ScriptCommand::SetJoyIgnore { mask: 0xFF },
233 ScriptCommand::MoveNpc { npc_id: "prof".into(), path: vec![(2, 3), (4, 5)] },
234 ScriptCommand::FollowNpc { npc_id: "rival".into(), target_x: 10, target_y: 8 },
235 ScriptCommand::OpenShop { items: vec!["POTION".into()] },
236 ScriptCommand::GiveMoney { amount: 500 },
237 ScriptCommand::TakeMoney { amount: 100 },
238 ScriptCommand::GiveBadge { badge: 0 },
239 ScriptCommand::Custom {
240 name: "tradeMonster".into(),
241 args: vec![serde_json::json!("SPARKIT")],
242 },
243 ScriptCommand::ShowObject { object_index: 1 },
244 ScriptCommand::HideObjectByName { toggle_id: "HIDDEN_ITEM".into() },
245 ScriptCommand::SetWeather { weather: Some("sandstorm".into()) },
246 ScriptCommand::SetWeather { weather: None },
247 ];
248
249 for cmd in &cmds {
250 let json = serde_json::to_string(cmd).unwrap();
251 let deserialized: ScriptCommand = serde_json::from_str(&json).unwrap();
252 assert_eq!(*cmd, deserialized, "round-trip failed for {cmd:?}");
253 }
254 }
255
256 #[test]
257 fn test_command_result_equality() {
258 assert_eq!(CommandResult::Void, CommandResult::Void);
259 assert_eq!(CommandResult::Bool(true), CommandResult::Bool(true));
260 assert_eq!(CommandResult::Bool(false), CommandResult::Bool(false));
261 assert_ne!(CommandResult::Bool(true), CommandResult::Bool(false));
262 assert_eq!(CommandResult::Number(42.0), CommandResult::Number(42.0));
263 assert_ne!(CommandResult::Number(1.0), CommandResult::Number(2.0));
264 assert_eq!(
265 CommandResult::Text("hello".into()),
266 CommandResult::Text("hello".into())
267 );
268 assert_ne!(
269 CommandResult::Text("hello".into()),
270 CommandResult::Text("world".into())
271 );
272 }
273
274 #[test]
275 fn test_command_result_debug() {
276 let void = CommandResult::Void;
277 assert!(!format!("{void:?}").is_empty());
278
279 let b = CommandResult::Bool(true);
280 assert_eq!(format!("{b:?}"), "Bool(true)");
281
282 let n = CommandResult::Number(2.5);
283 assert!(format!("{n:?}").contains("2.5"));
284
285 let t = CommandResult::Text("result".into());
286 assert_eq!(format!("{t:?}"), "Text(\"result\")");
287 }
288
289 #[test]
290 fn test_give_take_money() {
291 let give = ScriptCommand::GiveMoney { amount: 999 };
292 let take = ScriptCommand::TakeMoney { amount: 50 };
293 assert_ne!(give, take);
294 if let ScriptCommand::GiveMoney { amount } = &give {
295 assert_eq!(*amount, 999);
296 } else {
297 panic!("expected GiveMoney");
298 }
299 }
300
301 #[test]
302 fn test_show_hide_scene() {
303 let show = ScriptCommand::ShowScene {
304 scene_name: "shop".into(),
305 layout_json: None,
306 };
307 let hide = ScriptCommand::HideScene {
308 scene_name: "shop".into(),
309 };
310 assert_ne!(show, hide);
311 assert_eq!(
312 show,
313 ScriptCommand::ShowScene {
314 scene_name: "shop".into(),
315 layout_json: None
316 }
317 );
318 }
319
320 #[test]
321 fn test_update_ui_command() {
322 let cmd = ScriptCommand::UpdateUI {
323 scene_name: "bag".into(),
324 data_json: r#"{"gold":500}"#.into(),
325 };
326 assert!(format!("{cmd:?}").contains("bag"));
327 assert!(format!("{cmd:?}").contains("gold"));
328 }
329}