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 #[serde(default)]
24 pub toggle_id: Option<String>,
25 #[serde(default)]
27 pub script_id: Option<String>,
28 #[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 #[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}