flatland_presentation/
validate.rs1use crate::{SpriteCatalog, SpriteModeMap};
4
5pub fn presentation_validation_mode_ids(
7 catalog: &SpriteCatalog,
8 tile_id: Option<&str>,
9 paperdoll_ref: Option<&str>,
10) -> Vec<String> {
11 if let Some(skin) = paperdoll_ref.filter(|s| !s.is_empty()) {
12 let sheet = format!("paperdoll.{skin}");
13 let modes = catalog.mode_ids(&sheet);
14 if !modes.is_empty() {
15 return modes.to_vec();
16 }
17 }
18 if let Some(tid) = tile_id.filter(|s| !s.is_empty()) {
19 return crate::presentation_sheet_mode_ids(catalog, tid, paperdoll_ref);
20 }
21 Vec::new()
22}
23
24pub fn validate_sprite_modes_for_presentation(
26 owner_label: &str,
27 tile_id: Option<&str>,
28 paperdoll_ref: Option<&str>,
29 sprite_modes: &SpriteModeMap,
30 catalog: &SpriteCatalog,
31) -> Vec<String> {
32 let mut errors = Vec::new();
33 if sprite_modes.is_empty() {
34 return errors;
35 }
36 let sheet_modes = presentation_validation_mode_ids(catalog, tile_id, paperdoll_ref);
37 if sheet_modes.is_empty() {
38 errors.push(format!(
39 "{owner_label}: sprite_modes set but no modes found (tile_id={tile_id:?}, paperdoll_ref={paperdoll_ref:?})"
40 ));
41 return errors;
42 }
43 for (key, mode) in sprite_modes {
44 if mode_exists_on_sheet(mode, &sheet_modes) {
45 continue;
46 }
47 if npc_sheet_alias(key, &sheet_modes).is_some() {
48 continue;
49 }
50 errors.push(format!(
51 "{owner_label}: sprite_modes.{key} = '{mode}' not on presentation sheet (have: {})",
52 sheet_modes.join(", ")
53 ));
54 }
55 errors
56}
57
58pub fn validate_sprite_mode_maps(
60 owner_label: &str,
61 tile_id: Option<&str>,
62 sprite_modes: &SpriteModeMap,
63 catalog: &SpriteCatalog,
64) -> Vec<String> {
65 let mut errors = Vec::new();
66 let Some(tile_id) = tile_id.filter(|s| !s.is_empty()) else {
67 if !sprite_modes.is_empty() {
68 errors.push(format!(
69 "{owner_label}: sprite_modes set but tile_id is missing"
70 ));
71 }
72 return errors;
73 };
74 if sprite_modes.is_empty() {
75 return errors;
76 }
77 let sheet_modes = catalog.mode_ids(tile_id);
78 if sheet_modes.is_empty() {
79 errors.push(format!(
80 "{owner_label}: tile_id '{tile_id}' not found in sprite catalog or has no modes"
81 ));
82 return errors;
83 }
84 for (key, mode) in sprite_modes {
85 if !mode_exists_on_sheet(mode, &sheet_modes) {
86 errors.push(format!(
87 "{owner_label}: sprite_modes.{key} = '{mode}' not on sheet '{tile_id}' (have: {})",
88 sheet_modes.join(", ")
89 ));
90 }
91 }
92 errors
93}
94
95pub fn mode_exists_on_sheet(mode: &str, sheet_modes: &[String]) -> bool {
97 if sheet_modes.iter().any(|m| m == mode) {
98 return true;
99 }
100 let prefix = format!("{mode}_");
101 sheet_modes.iter().any(|m| m.starts_with(&prefix))
102}
103
104pub fn npc_sheet_alias(presentation_key: &str, sheet_modes: &[String]) -> Option<String> {
106 let candidates: &[&str] = match presentation_key {
107 "pursue" => &["chase", "alert", "run"],
108 "running" => &["chase", "run"],
109 "walking" => &["walk", "idle"],
110 "combat" => &["combat", "attack", "idle"],
111 "rest" => &["idle", "sleep", "rest"],
112 "work" => &["work", "idle"],
113 "patrol" => &["walk", "patrol", "idle"],
114 "talking" => &["talk", "idle"],
115 "chatting" => &["chat", "talk", "idle"],
116 "idle" => &["idle"],
117 "telegraph" => &["telegraph", "idle"],
118 "harvesting" => &["harvest", "work", "idle"],
119 "crafting" => &["craft", "work", "idle"],
120 "sprinting" => &["run", "sprint", "chase"],
121 _ => &[],
122 };
123 candidates
124 .iter()
125 .find(|c| mode_exists_on_sheet(c, sheet_modes))
126 .map(|c| c.to_string())
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::catalog::load_sprite_catalog;
133
134 #[test]
135 fn catches_unknown_mode() {
136 let dir = crate::catalog::default_sprites_dir().expect("sprites dir");
137 let catalog = load_sprite_catalog(&dir).expect("load");
138 let mut map = SpriteModeMap::new();
139 map.insert("available".into(), "bogus_mode_xyz".into());
140 let errs = validate_sprite_mode_maps("item.test", Some("resource.oak_log"), &map, &catalog);
141 assert_eq!(errs.len(), 1);
142 assert!(errs[0].contains("bogus_mode_xyz"));
143 }
144
145 #[test]
146 fn accepts_directional_prefix_for_base_mode() {
147 let sheet_modes = vec!["idle_north".into(), "walk_south".into(), "combat".into()];
148 assert!(mode_exists_on_sheet("idle", &sheet_modes));
149 assert!(mode_exists_on_sheet("walk", &sheet_modes));
150 assert!(mode_exists_on_sheet("combat", &sheet_modes));
151 assert!(!mode_exists_on_sheet("run", &sheet_modes));
152 }
153}