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![
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}