Skip to main content

dotzuki_engine_script/
config.rs

1use serde::Deserialize;
2
3#[derive(Debug, Clone, Deserialize, Default)]
4#[serde(rename_all = "camelCase")]
5pub struct MapScriptConfig {
6    #[serde(default)]
7    pub on_load: Option<String>,
8    #[serde(default)]
9    pub npcs: Vec<NpcBinding>,
10    #[serde(default)]
11    pub signs: Vec<SignBinding>,
12    #[serde(default)]
13    pub coord_events: Vec<CoordEventBinding>,
14}
15
16#[derive(Debug, Clone, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub struct NpcBinding {
19    pub id: u8,
20    #[serde(default)]
21    pub talk: Option<String>,
22    /// Named toggle identifier for script showObject/hideObject (e.g. "START_TOWN_PROF").
23    #[serde(default)]
24    pub toggle_id: Option<String>,
25    /// Script-facing NPC identifier used by moveNpc/startNpcMove (e.g. "STARTTOWN_PROF").
26    #[serde(default)]
27    pub script_id: Option<String>,
28    /// If true, this NPC is hidden when the map first loads (until a script shows it).
29    #[serde(default)]
30    pub default_hidden: bool,
31}
32
33#[derive(Debug, Clone, Deserialize)]
34pub struct SignBinding {
35    pub id: u8,
36    pub talk: String,
37}
38
39#[derive(Debug, Clone, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub struct CoordEventBinding {
42    pub name: String,
43    pub position: (u16, u16),
44    pub trigger: String,
45    /// If false, the event re-fires every time the player steps onto the
46    /// tile (the storyline's own flag checks gate re-entry). Defaults to
47    /// true (fire once per map entry).
48    #[serde(default = "default_one_shot")]
49    pub one_shot: bool,
50}
51
52fn default_one_shot() -> bool {
53    true
54}
55
56impl MapScriptConfig {
57    pub fn on_load(&self) -> Option<&str> {
58        self.on_load.as_deref()
59    }
60
61    pub fn npc_talk_fn(&self, npc_text_id: u8) -> Option<&str> {
62        self.npcs
63            .iter()
64            .find(|n| n.id == npc_text_id)
65            .and_then(|n| n.talk.as_deref())
66    }
67
68    pub fn sign_talk_fn(&self, sign_text_id: u8) -> Option<&str> {
69        self.signs
70            .iter()
71            .find(|s| s.id == sign_text_id)
72            .map(|s| s.talk.as_str())
73    }
74
75    pub fn coord_event_fn(&self, x: u16, y: u16) -> Option<&str> {
76        self.coord_events
77            .iter()
78            .find(|c| c.position == (x, y))
79            .map(|c| c.trigger.as_str())
80    }
81
82    pub fn coord_event_by_name(&self, name: &str) -> Option<&str> {
83        self.coord_events
84            .iter()
85            .find(|c| c.name == name)
86            .map(|c| c.trigger.as_str())
87    }
88
89    pub fn hidden_npc_ids(&self) -> Vec<u8> {
90        self.npcs
91            .iter()
92            .filter(|n| n.default_hidden)
93            .map(|n| n.id)
94            .collect()
95    }
96
97    pub fn npc_id_by_toggle(&self, toggle_id: &str) -> Option<u8> {
98        self.npcs
99            .iter()
100            .find(|n| n.toggle_id.as_deref() == Some(toggle_id))
101            .map(|n| n.id)
102    }
103
104    pub fn npc_id_by_script_id(&self, script_id: &str) -> Option<u8> {
105        self.npcs
106            .iter()
107            .find(|n| n.script_id.as_deref() == Some(script_id))
108            .map(|n| n.id)
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    fn sample_config() -> MapScriptConfig {
117        MapScriptConfig {
118            on_load: Some("onEnter".into()),
119            npcs: vec![
120                NpcBinding {
121                    id: 1,
122                    talk: Some("talkProf".into()),
123                    toggle_id: Some("START_TOWN_PROF".into()),
124                    script_id: Some("STARTTOWN_PROF".into()),
125                    default_hidden: false,
126                },
127                NpcBinding {
128                    id: 2,
129                    talk: Some("talkRival".into()),
130                    toggle_id: None,
131                    script_id: None,
132                    default_hidden: true,
133                },
134                NpcBinding {
135                    id: 3,
136                    talk: None,
137                    toggle_id: None,
138                    script_id: None,
139                    default_hidden: false,
140                },
141            ],
142            signs: vec![
143                SignBinding {
144                    id: 1,
145                    talk: "signLab".into(),
146                },
147            ],
148            coord_events: vec![
149                CoordEventBinding {
150                    name: "northExit".into(),
151                    position: (4, 1),
152                    trigger: "enterRoute1".into(),
153                    one_shot: true,
154                },
155                CoordEventBinding {
156                    name: "southExit".into(),
157                    position: (4, 11),
158                    trigger: "enterStartTown".into(),
159                    one_shot: true,
160                },
161            ],
162        }
163    }
164
165    #[test]
166    fn test_on_load() {
167        let config = sample_config();
168        assert_eq!(config.on_load(), Some("onEnter"));
169    }
170
171    #[test]
172    fn test_on_load_none() {
173        let config = MapScriptConfig::default();
174        assert_eq!(config.on_load(), None);
175    }
176
177    #[test]
178    fn test_npc_talk_fn_found() {
179        let config = sample_config();
180        assert_eq!(config.npc_talk_fn(1), Some("talkProf"));
181        assert_eq!(config.npc_talk_fn(2), Some("talkRival"));
182    }
183
184    #[test]
185    fn test_npc_talk_fn_not_found() {
186        let config = sample_config();
187        assert_eq!(config.npc_talk_fn(99), None);
188    }
189
190    #[test]
191    fn test_npc_talk_fn_no_talk_field() {
192        let config = sample_config();
193        assert_eq!(config.npc_talk_fn(3), None);
194    }
195
196    #[test]
197    fn test_sign_talk_fn_found() {
198        let config = sample_config();
199        assert_eq!(config.sign_talk_fn(1), Some("signLab"));
200    }
201
202    #[test]
203    fn test_sign_talk_fn_not_found() {
204        let config = sample_config();
205        assert_eq!(config.sign_talk_fn(99), None);
206    }
207
208    #[test]
209    fn test_coord_event_fn_found() {
210        let config = sample_config();
211        assert_eq!(config.coord_event_fn(4, 1), Some("enterRoute1"));
212        assert_eq!(config.coord_event_fn(4, 11), Some("enterStartTown"));
213    }
214
215    #[test]
216    fn test_coord_event_fn_not_found() {
217        let config = sample_config();
218        assert_eq!(config.coord_event_fn(0, 0), None);
219    }
220
221    #[test]
222    fn test_coord_event_by_name_found() {
223        let config = sample_config();
224        assert_eq!(config.coord_event_by_name("northExit"), Some("enterRoute1"));
225        assert_eq!(config.coord_event_by_name("southExit"), Some("enterStartTown"));
226    }
227
228    #[test]
229    fn test_coord_event_by_name_not_found() {
230        let config = sample_config();
231        assert_eq!(config.coord_event_by_name("nonexistent"), None);
232    }
233
234    #[test]
235    fn test_hidden_npc_ids() {
236        let config = sample_config();
237        let hidden = config.hidden_npc_ids();
238        assert_eq!(hidden, vec![2]);
239    }
240
241    #[test]
242    fn test_hidden_npc_ids_empty() {
243        let config = MapScriptConfig::default();
244        let hidden = config.hidden_npc_ids();
245        assert!(hidden.is_empty());
246    }
247
248    #[test]
249    fn test_npc_id_by_toggle_found() {
250        let config = sample_config();
251        assert_eq!(config.npc_id_by_toggle("START_TOWN_PROF"), Some(1));
252    }
253
254    #[test]
255    fn test_npc_id_by_toggle_not_found() {
256        let config = sample_config();
257        assert_eq!(config.npc_id_by_toggle("NONEXISTENT"), None);
258    }
259
260    #[test]
261    fn test_npc_id_by_script_id_found() {
262        let config = sample_config();
263        assert_eq!(config.npc_id_by_script_id("STARTTOWN_PROF"), Some(1));
264    }
265
266    #[test]
267    fn test_npc_id_by_script_id_not_found() {
268        let config = sample_config();
269        assert_eq!(config.npc_id_by_script_id("NONEXISTENT"), None);
270    }
271
272    #[test]
273    fn test_empty_config() {
274        let config = MapScriptConfig::default();
275        assert!(config.on_load.is_none());
276        assert!(config.npcs.is_empty());
277        assert!(config.signs.is_empty());
278        assert!(config.coord_events.is_empty());
279    }
280
281    #[test]
282    fn test_deserialize_full_config() {
283        let json = r#"{
284            "onLoad": "onEnter",
285            "npcs": [
286                {"id": 1, "talk": "talkProf", "toggleId": "T1", "scriptId": "S1", "defaultHidden": true}
287            ],
288            "signs": [
289                {"id": 2, "talk": "signLab"}
290            ],
291            "coordEvents": [
292                {"name": "exit", "position": [5, 3], "trigger": "onExit"}
293            ]
294        }"#;
295        let config: MapScriptConfig = serde_json::from_str(json).unwrap();
296        assert_eq!(config.on_load(), Some("onEnter"));
297        assert_eq!(config.npc_talk_fn(1), Some("talkProf"));
298        assert_eq!(config.npc_id_by_toggle("T1"), Some(1));
299        assert_eq!(config.npc_id_by_script_id("S1"), Some(1));
300        assert!(config.hidden_npc_ids().contains(&1));
301        assert_eq!(config.sign_talk_fn(2), Some("signLab"));
302        assert_eq!(config.coord_event_fn(5, 3), Some("onExit"));
303        assert_eq!(config.coord_event_by_name("exit"), Some("onExit"));
304    }
305}