flatland-presentation 0.2.11

Gfx sprite mode resolution — maps game states to sprite sheet mode ids
Documentation
//! Canonical NPC presentation keys for gfx `sprite_modes` (designer-facing).

/// Standard presentation keys shown in content-admin and used on the wire.
pub const NPC_PRESENTATION_STATES: &[(&str, &str)] = &[
    ("combat", "Combat — in melee / attacking"),
    ("pursue", "Pursue — has target, out of attack range"),
    ("running", "Running — fast movement"),
    ("walking", "Walking — normal movement"),
    ("harvesting", "Harvesting — gathering resources"),
    ("talking", "Talking — in conversation with a player"),
    ("rest", "Rest / sleep — off duty, low activity"),
    ("work", "Work — at post / crafting / tending"),
    ("patrol", "Patrol — on a scheduled route"),
    ("idle", "Idle — stationary, available"),
    ("telegraph", "Telegraph — attack windup (combat NPCs)"),
];

/// Legacy behavior FSM ids still accepted in `sprite_modes` when no canonical key matches.
pub const NPC_LEGACY_BEHAVIOR_ALIASES: &[(&str, &str)] = &[
    ("chase", "pursue"),
    ("alert", "pursue"),
    ("graze", "idle"),
    ("flee", "running"),
];

/// Inputs for wildlife / combat-driven NPCs (keeps this crate free of sim types).
#[derive(Debug, Clone, Copy)]
pub struct WildlifeNpcPresentationInput<'a> {
    pub fsm_state: &'a str,
    pub telegraph_active: bool,
    pub has_target: bool,
    pub in_attack_range: bool,
    pub is_moving: bool,
    pub fast_movement: bool,
}

/// Town / building NPCs — schedule + conversation.
#[derive(Debug, Clone, Copy)]
pub struct PlacedNpcPresentationInput<'a> {
    pub is_talking: bool,
    /// `travel`, `work`, `idle`, `rest`, `sleep`, `patrol`, `hidden`
    pub schedule_action: Option<&'a str>,
    pub is_moving: bool,
}

pub fn resolve_wildlife_presentation_key(input: WildlifeNpcPresentationInput<'_>) -> &'static str {
    if input.telegraph_active {
        return "telegraph";
    }
    if input.fsm_state == "combat" && input.in_attack_range {
        return "combat";
    }
    if input.has_target && !input.in_attack_range {
        return "pursue";
    }
    if input.fsm_state == "combat" {
        return "combat";
    }
    if input.is_moving && input.fast_movement {
        return "running";
    }
    if input.is_moving {
        return "walking";
    }
    match input.fsm_state {
        "idle" | "graze" => "idle",
        "flee" => "running",
        _ => "idle",
    }
}

pub fn resolve_placed_npc_presentation_key(input: PlacedNpcPresentationInput<'_>) -> &'static str {
    if input.is_talking {
        return "talking";
    }
    if input.is_moving {
        return "walking";
    }
    match input.schedule_action {
        Some("sleep") | Some("rest") => "rest",
        Some("work") => "work",
        Some("patrol") => "patrol",
        Some("travel") => "walking",
        Some("idle") => "idle",
        _ => "idle",
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn wildlife_pursue_when_chasing() {
        let key = resolve_wildlife_presentation_key(WildlifeNpcPresentationInput {
            fsm_state: "chase",
            telegraph_active: false,
            has_target: true,
            in_attack_range: false,
            is_moving: true,
            fast_movement: true,
        });
        assert_eq!(key, "pursue");
    }

    #[test]
    fn wildlife_combat_in_range() {
        let key = resolve_wildlife_presentation_key(WildlifeNpcPresentationInput {
            fsm_state: "combat",
            telegraph_active: false,
            has_target: true,
            in_attack_range: true,
            is_moving: false,
            fast_movement: false,
        });
        assert_eq!(key, "combat");
    }

    #[test]
    fn placed_npc_talking_wins() {
        let key = resolve_placed_npc_presentation_key(PlacedNpcPresentationInput {
            is_talking: true,
            schedule_action: Some("work"),
            is_moving: false,
        });
        assert_eq!(key, "talking");
    }
}