use std::time::SystemTime;
use crate::tui::layout::{Point, Size};
pub const PET_DURATION_MS: u64 = 2000;
pub struct PetState {
pub petted_at: SystemTime,
pub pet_pos: Point,
pub kind: PetKind,
pub floor_idx: usize,
}
impl PetState {
pub fn is_active(&self, now: SystemTime) -> bool {
now.duration_since(self.petted_at)
.map(|d| d.as_millis() as u64)
.unwrap_or(PET_DURATION_MS + 1)
< PET_DURATION_MS
}
pub fn elapsed_ms(&self, now: SystemTime) -> u64 {
now.duration_since(self.petted_at)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
}
#[derive(Clone, Copy)]
pub struct PetFrame {
pub pos: Point,
pub anim: &'static str,
pub kind: PetKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PetKind {
Cat,
Dog,
}
impl PetKind {
pub const ALL: &'static [PetKind] = &[PetKind::Cat, PetKind::Dog];
pub fn from_config_name(s: &str) -> Option<Self> {
match s {
"cat" => Some(PetKind::Cat),
"dog" => Some(PetKind::Dog),
_ => None,
}
}
pub fn walk_anim(self) -> &'static str {
match self {
PetKind::Cat => "cat_walk",
PetKind::Dog => "dog_walk",
}
}
pub fn sit_anim(self) -> &'static str {
match self {
PetKind::Cat => "cat_sit",
PetKind::Dog => "dog_sit",
}
}
pub fn sleep_anim(self) -> &'static str {
match self {
PetKind::Cat => "cat_sleep",
PetKind::Dog => "dog_sleep",
}
}
pub fn default_name(self) -> &'static str {
match self {
PetKind::Cat => "Office Cat",
PetKind::Dog => "Office Dog",
}
}
pub fn sleeps_near_idle(self) -> bool {
match self {
PetKind::Cat => true,
PetKind::Dog => false,
}
}
pub fn hitbox(self, anim_name: &str) -> Size {
if anim_name == self.walk_anim() {
Size { w: 8, h: 6 }
} else if anim_name == self.sleep_anim() {
Size { w: 6, h: 4 }
} else {
Size { w: 6, h: 6 }
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Pet {
pub kind: PetKind,
pub name: String,
}
impl Pet {
pub fn defaulted(kind: PetKind) -> Self {
Self {
kind,
name: kind.default_name().to_string(),
}
}
}
pub fn select_pet_for_floor(floor_seed: u64, pets: &[Pet]) -> Option<&Pet> {
if pets.is_empty() {
return None;
}
Some(&pets[(floor_seed as usize) % pets.len()])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_name_cat_and_dog() {
assert_eq!(PetKind::Cat.default_name(), "Office Cat");
assert_eq!(PetKind::Dog.default_name(), "Office Dog");
}
#[test]
fn default_name_all_nonempty() {
for &k in PetKind::ALL {
assert!(!k.default_name().is_empty(), "{k:?} default_name empty");
}
}
#[test]
fn every_pet_kind_is_reachable_from_config() {
for &kind in PetKind::ALL {
let name = match kind {
PetKind::Cat => "cat",
PetKind::Dog => "dog",
};
assert_eq!(PetKind::from_config_name(name), Some(kind));
}
}
#[test]
fn from_config_name_unknown_returns_none() {
assert_eq!(PetKind::from_config_name("hamster"), None);
}
#[test]
fn select_pet_empty_returns_none() {
assert_eq!(select_pet_for_floor(42, &[]), None);
}
#[test]
fn select_pet_single_always_returns_it() {
let pets = [Pet::defaulted(PetKind::Dog)];
assert_eq!(
select_pet_for_floor(0, &pets).map(|p| p.kind),
Some(PetKind::Dog)
);
assert_eq!(
select_pet_for_floor(99, &pets).map(|p| p.kind),
Some(PetKind::Dog)
);
}
#[test]
fn select_pet_two_pets_alternates_by_seed() {
let pets = vec![Pet::defaulted(PetKind::Cat), Pet::defaulted(PetKind::Dog)];
let floor0 = select_pet_for_floor(0, &pets).map(|p| p.kind);
let floor1 = select_pet_for_floor(1, &pets).map(|p| p.kind);
assert_ne!(floor0, floor1);
}
#[test]
fn defaulted_uses_default_name() {
assert_eq!(Pet::defaulted(PetKind::Cat).name, "Office Cat");
assert_eq!(Pet::defaulted(PetKind::Dog).kind, PetKind::Dog);
}
#[test]
fn anim_names_match_kind() {
assert!(PetKind::Cat.walk_anim().starts_with("cat_"));
assert!(PetKind::Dog.walk_anim().starts_with("dog_"));
}
#[test]
fn dog_anim_methods() {
assert_eq!(PetKind::Dog.walk_anim(), "dog_walk");
assert_eq!(PetKind::Dog.sit_anim(), "dog_sit");
assert_eq!(PetKind::Dog.sleep_anim(), "dog_sleep");
}
#[test]
fn dog_does_not_sleep_near_idle() {
assert!(!PetKind::Dog.sleeps_near_idle());
assert!(PetKind::Cat.sleeps_near_idle());
}
#[test]
fn hitbox_walk_larger_than_sit() {
for &kind in PetKind::ALL {
let ww = kind.hitbox(kind.walk_anim()).w;
let sw = kind.hitbox(kind.sit_anim()).w;
assert!(ww > sw, "{:?} walk should be wider than sit", kind);
}
}
#[test]
fn hitbox_sleep_shorter_than_sit() {
for &kind in PetKind::ALL {
let sh = kind.hitbox(kind.sit_anim()).h;
let slh = kind.hitbox(kind.sleep_anim()).h;
assert!(slh < sh, "{:?} sleep should be shorter than sit", kind);
}
}
#[test]
fn hitbox_unknown_anim_returns_default() {
assert_eq!(PetKind::Cat.hitbox("unknown"), Size { w: 6, h: 6 });
assert_eq!(PetKind::Dog.hitbox("unknown"), Size { w: 6, h: 6 });
}
}