1use flatland_protocol::ResourceNodeState;
4
5use crate::npc_states::{
6 resolve_placed_npc_presentation_key, resolve_wildlife_presentation_key,
7 PlacedNpcPresentationInput, WildlifeNpcPresentationInput,
8};
9use crate::player_states::{resolve_player_presentation_key, PlayerPresentationInput};
10use crate::validate::mode_exists_on_sheet;
11use crate::SpriteModeMap;
12
13pub const RESOURCE_PRESENTATION_KEYS: &[&str] = &["available", "harvesting", "depleted"];
15
16pub const RESOURCE_PRESENTATION_KEY_COOLDOWN_LEGACY: &str = "cooldown";
18
19pub fn resource_presentation_key(state: ResourceNodeState) -> &'static str {
20 match state {
21 ResourceNodeState::Available => "available",
22 ResourceNodeState::Harvesting => "harvesting",
23 ResourceNodeState::Cooldown => "depleted",
24 }
25}
26
27pub fn resolve_resource_sprite_mode(
29 state: ResourceNodeState,
30 sprite_modes: &SpriteModeMap,
31 sheet_modes: &[String],
32) -> Option<String> {
33 let key = resource_presentation_key(state);
34 pick_mode(key, sprite_modes, sheet_modes)
35}
36
37pub struct NpcPresentationInput<'a> {
39 pub presentation_key: &'a str,
41 pub sprite_modes: &'a SpriteModeMap,
42 pub sheet_modes: &'a [String],
43}
44
45pub fn npc_presentation_key<'a>(input: &'a NpcPresentationInput<'a>) -> &'a str {
46 input.presentation_key
47}
48
49pub fn resolve_npc_sprite_mode(input: &NpcPresentationInput<'_>) -> Option<String> {
51 let key = npc_presentation_key(input);
52 pick_mode(key, input.sprite_modes, input.sheet_modes)
53}
54
55pub fn wildlife_npc_presentation_input<'a>(
57 ctx: WildlifeNpcPresentationInput<'a>,
58 sprite_modes: &'a SpriteModeMap,
59 sheet_modes: &'a [String],
60) -> NpcPresentationInput<'a> {
61 let key = resolve_wildlife_presentation_key(ctx);
62 NpcPresentationInput {
63 presentation_key: key,
64 sprite_modes,
65 sheet_modes,
66 }
67}
68
69pub fn placed_npc_presentation_input<'a>(
71 ctx: PlacedNpcPresentationInput<'a>,
72 sprite_modes: &'a SpriteModeMap,
73 sheet_modes: &'a [String],
74) -> NpcPresentationInput<'a> {
75 let key = resolve_placed_npc_presentation_key(ctx);
76 NpcPresentationInput {
77 presentation_key: key,
78 sprite_modes,
79 sheet_modes,
80 }
81}
82
83pub struct PlayerSpritePresentationInput<'a> {
85 pub presentation_key: &'a str,
86 pub sprite_modes: &'a SpriteModeMap,
87 pub sheet_modes: &'a [String],
88}
89
90pub fn player_presentation_input<'a>(
91 ctx: PlayerPresentationInput,
92 sprite_modes: &'a SpriteModeMap,
93 sheet_modes: &'a [String],
94) -> PlayerSpritePresentationInput<'a> {
95 let key = resolve_player_presentation_key(ctx);
96 PlayerSpritePresentationInput {
97 presentation_key: key,
98 sprite_modes,
99 sheet_modes,
100 }
101}
102
103pub fn resolve_player_sprite_mode(input: &PlayerSpritePresentationInput<'_>) -> Option<String> {
105 pick_mode(
106 input.presentation_key,
107 input.sprite_modes,
108 input.sheet_modes,
109 )
110}
111
112fn pick_mode(
113 presentation_key: &str,
114 sprite_modes: &SpriteModeMap,
115 sheet_modes: &[String],
116) -> Option<String> {
117 if let Some(mode) = sprite_modes.get(presentation_key) {
118 if sheet_modes.is_empty() || mode_exists_on_sheet(mode, sheet_modes) {
119 return Some(mode.clone());
120 }
121 }
122 if sheet_modes.iter().any(|m| m == presentation_key) {
123 return Some(presentation_key.to_string());
124 }
125 let resource_alias = match presentation_key {
127 "available" => sheet_modes
128 .iter()
129 .find(|m| **m == "idle" || m.starts_with("idle_"))
130 .map(|m| {
131 if m.starts_with("idle_") {
132 "idle".to_string()
133 } else {
134 m.clone()
135 }
136 }),
137 "harvesting" => sheet_modes.iter().find(|m| *m == "harvest").cloned(),
138 "depleted" | "cooldown" => sheet_modes
139 .iter()
140 .find(|m| *m == "depleted" || *m == "cooldown")
141 .cloned(),
142 _ => None,
143 };
144 if let Some(mode) = resource_alias {
145 return Some(mode);
146 }
147 if presentation_key == "cooldown" {
149 if let Some(mode) = sprite_modes.get("depleted") {
150 if sheet_modes.is_empty() || mode_exists_on_sheet(mode, sheet_modes) {
151 return Some(mode.clone());
152 }
153 }
154 }
155 if let Some(sheet_id) = npc_sheet_alias(presentation_key, sheet_modes) {
157 return Some(sheet_id);
158 }
159 sprite_modes
160 .get(presentation_key)
161 .filter(|m| mode_exists_on_sheet(m, sheet_modes))
162 .cloned()
163 .or_else(|| sheet_modes.first().filter(|m| !m.contains('_')).cloned())
164}
165
166fn npc_sheet_alias(presentation_key: &str, sheet_modes: &[String]) -> Option<String> {
167 let candidates: &[&str] = match presentation_key {
168 "pursue" => &["chase", "alert", "run"],
169 "running" => &["chase", "run"],
170 "walking" => &["walk", "idle"],
171 "combat" => &["combat", "attack"],
172 "rest" => &["idle", "sleep", "rest"],
173 "work" => &["work", "idle"],
174 "patrol" => &["walk", "patrol", "idle"],
175 "talking" => &["talk", "idle"],
176 "chatting" => &["chat", "talk"],
177 "idle" => &["idle"],
178 "telegraph" => &["telegraph"],
179 "harvesting" => &["harvest"],
180 "crafting" => &["craft", "work"],
181 "sprinting" => &["run", "sprint", "chase"],
182 _ => &[],
183 };
184 candidates
185 .iter()
186 .find(|c| mode_exists_on_sheet(c, sheet_modes))
187 .map(|c| c.to_string())
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 fn modes(ids: &[&str]) -> Vec<String> {
195 ids.iter().map(|s| s.to_string()).collect()
196 }
197
198 #[test]
199 fn resource_explicit_map() {
200 let mut map = SpriteModeMap::new();
201 map.insert("available".into(), "idle".into());
202 map.insert("harvesting".into(), "harvest".into());
203 map.insert("depleted".into(), "depleted".into());
204 let sheet = modes(&["idle", "harvest", "depleted"]);
205 assert_eq!(
206 resolve_resource_sprite_mode(ResourceNodeState::Harvesting, &map, &sheet).as_deref(),
207 Some("harvest")
208 );
209 }
210
211 #[test]
212 fn resource_alias_fallback() {
213 let map = SpriteModeMap::new();
214 let sheet = modes(&["idle", "harvest"]);
215 assert_eq!(
216 resolve_resource_sprite_mode(ResourceNodeState::Available, &map, &sheet).as_deref(),
217 Some("idle")
218 );
219 assert_eq!(
220 resolve_resource_sprite_mode(ResourceNodeState::Harvesting, &map, &sheet).as_deref(),
221 Some("harvest")
222 );
223 }
224
225 #[test]
226 fn npc_telegraph_presentation_key() {
227 let map = SpriteModeMap::new();
228 let sheet = modes(&["idle", "combat", "telegraph"]);
229 let input = NpcPresentationInput {
230 presentation_key: "telegraph",
231 sprite_modes: &map,
232 sheet_modes: &sheet,
233 };
234 assert_eq!(
235 resolve_npc_sprite_mode(&input).as_deref(),
236 Some("telegraph")
237 );
238 }
239
240 #[test]
241 fn npc_pursue_maps_to_chase_mode() {
242 let mut map = SpriteModeMap::new();
243 map.insert("pursue".into(), "chase".into());
244 let sheet = modes(&["idle", "chase", "combat"]);
245 let input = wildlife_npc_presentation_input(
246 WildlifeNpcPresentationInput {
247 fsm_state: "chase",
248 telegraph_active: false,
249 has_target: true,
250 in_attack_range: false,
251 is_moving: true,
252 fast_movement: true,
253 },
254 &map,
255 &sheet,
256 );
257 assert_eq!(resolve_npc_sprite_mode(&input).as_deref(), Some("chase"));
258 assert_eq!(input.presentation_key, "pursue");
259 }
260
261 #[test]
262 fn npc_legacy_behavior_alias() {
263 let map = SpriteModeMap::new();
264 let sheet = modes(&["idle", "chase", "combat"]);
265 let input = NpcPresentationInput {
266 presentation_key: "chase",
267 sprite_modes: &map,
268 sheet_modes: &sheet,
269 };
270 assert_eq!(resolve_npc_sprite_mode(&input).as_deref(), Some("chase"));
271 }
272
273 #[test]
274 fn player_directional_sheet_returns_base_modes() {
275 let mut map = SpriteModeMap::new();
276 map.insert("idle".into(), "idle".into());
277 map.insert("walking".into(), "walk".into());
278 map.insert("sprinting".into(), "run".into());
279 let sheet = modes(&[
280 "idle_north",
281 "idle_south",
282 "walk_north",
283 "walk_east",
284 "run_south",
285 "combat",
286 ]);
287
288 let idle = player_presentation_input(
289 PlayerPresentationInput {
290 harvesting: false,
291 crafting: false,
292 talking: false,
293 chatting: false,
294 in_combat: false,
295 pursue: false,
296 in_attack_range: false,
297 is_moving: false,
298 sprinting: false,
299 },
300 &map,
301 &sheet,
302 );
303 assert_eq!(resolve_player_sprite_mode(&idle).as_deref(), Some("idle"));
304
305 let walking = player_presentation_input(
306 PlayerPresentationInput {
307 harvesting: false,
308 crafting: false,
309 talking: false,
310 chatting: false,
311 in_combat: false,
312 pursue: false,
313 in_attack_range: false,
314 is_moving: true,
315 sprinting: false,
316 },
317 &map,
318 &sheet,
319 );
320 assert_eq!(
321 resolve_player_sprite_mode(&walking).as_deref(),
322 Some("walk")
323 );
324 }
325}