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 use std::collections::HashMap;
194
195 fn modes(ids: &[&str]) -> Vec<String> {
196 ids.iter().map(|s| s.to_string()).collect()
197 }
198
199 #[test]
200 fn resource_explicit_map() {
201 let mut map = SpriteModeMap::new();
202 map.insert("available".into(), "idle".into());
203 map.insert("harvesting".into(), "harvest".into());
204 map.insert("depleted".into(), "depleted".into());
205 let sheet = modes(&["idle", "harvest", "depleted"]);
206 assert_eq!(
207 resolve_resource_sprite_mode(ResourceNodeState::Harvesting, &map, &sheet).as_deref(),
208 Some("harvest")
209 );
210 }
211
212 #[test]
213 fn resource_alias_fallback() {
214 let map = SpriteModeMap::new();
215 let sheet = modes(&["idle", "harvest"]);
216 assert_eq!(
217 resolve_resource_sprite_mode(ResourceNodeState::Available, &map, &sheet).as_deref(),
218 Some("idle")
219 );
220 assert_eq!(
221 resolve_resource_sprite_mode(ResourceNodeState::Harvesting, &map, &sheet).as_deref(),
222 Some("harvest")
223 );
224 }
225
226 #[test]
227 fn npc_telegraph_presentation_key() {
228 let map = SpriteModeMap::new();
229 let sheet = modes(&["idle", "combat", "telegraph"]);
230 let input = NpcPresentationInput {
231 presentation_key: "telegraph",
232 sprite_modes: &map,
233 sheet_modes: &sheet,
234 };
235 assert_eq!(
236 resolve_npc_sprite_mode(&input).as_deref(),
237 Some("telegraph")
238 );
239 }
240
241 #[test]
242 fn npc_pursue_maps_to_chase_mode() {
243 let mut map = SpriteModeMap::new();
244 map.insert("pursue".into(), "chase".into());
245 let sheet = modes(&["idle", "chase", "combat"]);
246 let input = wildlife_npc_presentation_input(
247 WildlifeNpcPresentationInput {
248 fsm_state: "chase",
249 telegraph_active: false,
250 has_target: true,
251 in_attack_range: false,
252 is_moving: true,
253 fast_movement: true,
254 },
255 &map,
256 &sheet,
257 );
258 assert_eq!(resolve_npc_sprite_mode(&input).as_deref(), Some("chase"));
259 assert_eq!(input.presentation_key, "pursue");
260 }
261
262 #[test]
263 fn npc_legacy_behavior_alias() {
264 let map = SpriteModeMap::new();
265 let sheet = modes(&["idle", "chase", "combat"]);
266 let input = NpcPresentationInput {
267 presentation_key: "chase",
268 sprite_modes: &map,
269 sheet_modes: &sheet,
270 };
271 assert_eq!(resolve_npc_sprite_mode(&input).as_deref(), Some("chase"));
272 }
273
274 #[test]
275 fn player_directional_sheet_returns_base_modes() {
276 let mut map = SpriteModeMap::new();
277 map.insert("idle".into(), "idle".into());
278 map.insert("walking".into(), "walk".into());
279 map.insert("sprinting".into(), "run".into());
280 let sheet = modes(&[
281 "idle_north",
282 "idle_south",
283 "walk_north",
284 "walk_east",
285 "run_south",
286 "combat",
287 ]);
288
289 let idle = player_presentation_input(
290 PlayerPresentationInput {
291 harvesting: false,
292 crafting: false,
293 talking: false,
294 chatting: false,
295 in_combat: false,
296 pursue: false,
297 in_attack_range: false,
298 is_moving: false,
299 sprinting: false,
300 },
301 &map,
302 &sheet,
303 );
304 assert_eq!(resolve_player_sprite_mode(&idle).as_deref(), Some("idle"));
305
306 let walking = player_presentation_input(
307 PlayerPresentationInput {
308 harvesting: false,
309 crafting: false,
310 talking: false,
311 chatting: false,
312 in_combat: false,
313 pursue: false,
314 in_attack_range: false,
315 is_moving: true,
316 sprinting: false,
317 },
318 &map,
319 &sheet,
320 );
321 assert_eq!(
322 resolve_player_sprite_mode(&walking).as_deref(),
323 Some("walk")
324 );
325 }
326}