Skip to main content

flatland_presentation/
npc_states.rs

1//! Canonical NPC presentation keys for gfx `sprite_modes` (designer-facing).
2
3/// Standard presentation keys shown in content-admin and used on the wire.
4pub const NPC_PRESENTATION_STATES: &[(&str, &str)] = &[
5    ("combat", "Combat — in melee / attacking"),
6    ("pursue", "Pursue — has target, out of attack range"),
7    ("running", "Running — fast movement"),
8    ("walking", "Walking — normal movement"),
9    ("harvesting", "Harvesting — gathering resources"),
10    ("talking", "Talking — in conversation with a player"),
11    ("rest", "Rest / sleep — off duty, low activity"),
12    ("work", "Work — at post / crafting / tending"),
13    ("patrol", "Patrol — on a scheduled route"),
14    ("idle", "Idle — stationary, available"),
15    ("telegraph", "Telegraph — attack windup (combat NPCs)"),
16];
17
18/// Legacy behavior FSM ids still accepted in `sprite_modes` when no canonical key matches.
19pub const NPC_LEGACY_BEHAVIOR_ALIASES: &[(&str, &str)] = &[
20    ("chase", "pursue"),
21    ("alert", "pursue"),
22    ("graze", "idle"),
23    ("flee", "running"),
24];
25
26/// Inputs for wildlife / combat-driven NPCs (keeps this crate free of sim types).
27#[derive(Debug, Clone, Copy)]
28pub struct WildlifeNpcPresentationInput<'a> {
29    pub fsm_state: &'a str,
30    pub telegraph_active: bool,
31    pub has_target: bool,
32    pub in_attack_range: bool,
33    pub is_moving: bool,
34    pub fast_movement: bool,
35}
36
37/// Town / building NPCs — schedule + conversation.
38#[derive(Debug, Clone, Copy)]
39pub struct PlacedNpcPresentationInput<'a> {
40    pub is_talking: bool,
41    /// `travel`, `work`, `idle`, `rest`, `sleep`, `patrol`, `hidden`
42    pub schedule_action: Option<&'a str>,
43    pub is_moving: bool,
44}
45
46pub fn resolve_wildlife_presentation_key(input: WildlifeNpcPresentationInput<'_>) -> &'static str {
47    if input.telegraph_active {
48        return "telegraph";
49    }
50    if input.fsm_state == "combat" && input.in_attack_range {
51        return "combat";
52    }
53    if input.has_target && !input.in_attack_range {
54        return "pursue";
55    }
56    if input.fsm_state == "combat" {
57        return "combat";
58    }
59    if input.is_moving && input.fast_movement {
60        return "running";
61    }
62    if input.is_moving {
63        return "walking";
64    }
65    match input.fsm_state {
66        "idle" | "graze" => "idle",
67        "flee" => "running",
68        _ => "idle",
69    }
70}
71
72pub fn resolve_placed_npc_presentation_key(input: PlacedNpcPresentationInput<'_>) -> &'static str {
73    if input.is_talking {
74        return "talking";
75    }
76    if input.is_moving {
77        return "walking";
78    }
79    match input.schedule_action {
80        Some("sleep") | Some("rest") => "rest",
81        Some("work") => "work",
82        Some("patrol") => "patrol",
83        Some("travel") => "walking",
84        Some("idle") => "idle",
85        _ => "idle",
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn wildlife_pursue_when_chasing() {
95        let key = resolve_wildlife_presentation_key(WildlifeNpcPresentationInput {
96            fsm_state: "chase",
97            telegraph_active: false,
98            has_target: true,
99            in_attack_range: false,
100            is_moving: true,
101            fast_movement: true,
102        });
103        assert_eq!(key, "pursue");
104    }
105
106    #[test]
107    fn wildlife_combat_in_range() {
108        let key = resolve_wildlife_presentation_key(WildlifeNpcPresentationInput {
109            fsm_state: "combat",
110            telegraph_active: false,
111            has_target: true,
112            in_attack_range: true,
113            is_moving: false,
114            fast_movement: false,
115        });
116        assert_eq!(key, "combat");
117    }
118
119    #[test]
120    fn placed_npc_talking_wins() {
121        let key = resolve_placed_npc_presentation_key(PlacedNpcPresentationInput {
122            is_talking: true,
123            schedule_action: Some("work"),
124            is_moving: false,
125        });
126        assert_eq!(key, "talking");
127    }
128}