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![SignBinding {
143                id: 1,
144                talk: "signLab".into(),
145            }],
146            coord_events: vec![
147                CoordEventBinding {
148                    name: "northExit".into(),
149                    position: (4, 1),
150                    trigger: "enterRoute1".into(),
151                    one_shot: true,
152                },
153                CoordEventBinding {
154                    name: "southExit".into(),
155                    position: (4, 11),
156                    trigger: "enterStartTown".into(),
157                    one_shot: true,
158                },
159            ],
160        }
161    }
162
163    #[test]
164    fn test_on_load() {
165        let config = sample_config();
166        assert_eq!(config.on_load(), Some("onEnter"));
167    }
168
169    #[test]
170    fn test_on_load_none() {
171        let config = MapScriptConfig::default();
172        assert_eq!(config.on_load(), None);
173    }
174
175    #[test]
176    fn test_npc_talk_fn_found() {
177        let config = sample_config();
178        assert_eq!(config.npc_talk_fn(1), Some("talkProf"));
179        assert_eq!(config.npc_talk_fn(2), Some("talkRival"));
180    }
181
182    #[test]
183    fn test_npc_talk_fn_not_found() {
184        let config = sample_config();
185        assert_eq!(config.npc_talk_fn(99), None);
186    }
187
188    #[test]
189    fn test_npc_talk_fn_no_talk_field() {
190        let config = sample_config();
191        assert_eq!(config.npc_talk_fn(3), None);
192    }
193
194    #[test]
195    fn test_sign_talk_fn_found() {
196        let config = sample_config();
197        assert_eq!(config.sign_talk_fn(1), Some("signLab"));
198    }
199
200    #[test]
201    fn test_sign_talk_fn_not_found() {
202        let config = sample_config();
203        assert_eq!(config.sign_talk_fn(99), None);
204    }
205
206    #[test]
207    fn test_coord_event_fn_found() {
208        let config = sample_config();
209        assert_eq!(config.coord_event_fn(4, 1), Some("enterRoute1"));
210        assert_eq!(config.coord_event_fn(4, 11), Some("enterStartTown"));
211    }
212
213    #[test]
214    fn test_coord_event_fn_not_found() {
215        let config = sample_config();
216        assert_eq!(config.coord_event_fn(0, 0), None);
217    }
218
219    #[test]
220    fn test_coord_event_by_name_found() {
221        let config = sample_config();
222        assert_eq!(config.coord_event_by_name("northExit"), Some("enterRoute1"));
223        assert_eq!(
224            config.coord_event_by_name("southExit"),
225            Some("enterStartTown")
226        );
227    }
228
229    #[test]
230    fn test_coord_event_by_name_not_found() {
231        let config = sample_config();
232        assert_eq!(config.coord_event_by_name("nonexistent"), None);
233    }
234
235    #[test]
236    fn test_hidden_npc_ids() {
237        let config = sample_config();
238        let hidden = config.hidden_npc_ids();
239        assert_eq!(hidden, vec![2]);
240    }
241
242    #[test]
243    fn test_hidden_npc_ids_empty() {
244        let config = MapScriptConfig::default();
245        let hidden = config.hidden_npc_ids();
246        assert!(hidden.is_empty());
247    }
248
249    #[test]
250    fn test_npc_id_by_toggle_found() {
251        let config = sample_config();
252        assert_eq!(config.npc_id_by_toggle("START_TOWN_PROF"), Some(1));
253    }
254
255    #[test]
256    fn test_npc_id_by_toggle_not_found() {
257        let config = sample_config();
258        assert_eq!(config.npc_id_by_toggle("NONEXISTENT"), None);
259    }
260
261    #[test]
262    fn test_npc_id_by_script_id_found() {
263        let config = sample_config();
264        assert_eq!(config.npc_id_by_script_id("STARTTOWN_PROF"), Some(1));
265    }
266
267    #[test]
268    fn test_npc_id_by_script_id_not_found() {
269        let config = sample_config();
270        assert_eq!(config.npc_id_by_script_id("NONEXISTENT"), None);
271    }
272
273    #[test]
274    fn test_empty_config() {
275        let config = MapScriptConfig::default();
276        assert!(config.on_load.is_none());
277        assert!(config.npcs.is_empty());
278        assert!(config.signs.is_empty());
279        assert!(config.coord_events.is_empty());
280    }
281
282    #[test]
283    fn test_deserialize_full_config() {
284        let json = r#"{
285            "onLoad": "onEnter",
286            "npcs": [
287                {"id": 1, "talk": "talkProf", "toggleId": "T1", "scriptId": "S1", "defaultHidden": true}
288            ],
289            "signs": [
290                {"id": 2, "talk": "signLab"}
291            ],
292            "coordEvents": [
293                {"name": "exit", "position": [5, 3], "trigger": "onExit"}
294            ]
295        }"#;
296        let config: MapScriptConfig = serde_json::from_str(json).unwrap();
297        assert_eq!(config.on_load(), Some("onEnter"));
298        assert_eq!(config.npc_talk_fn(1), Some("talkProf"));
299        assert_eq!(config.npc_id_by_toggle("T1"), Some(1));
300        assert_eq!(config.npc_id_by_script_id("S1"), Some(1));
301        assert!(config.hidden_npc_ids().contains(&1));
302        assert_eq!(config.sign_talk_fn(2), Some("signLab"));
303        assert_eq!(config.coord_event_fn(5, 3), Some("onExit"));
304        assert_eq!(config.coord_event_by_name("exit"), Some("onExit"));
305    }
306}