use std::time::SystemTime;
use ascii_agents_core::walkable::OccupancyOverlay;
use ascii_agents_core::AgentSlot;
use crate::tui::layout::{Point, WaypointKind, DESK_W};
use crate::tui::pathfind::Router;
use crate::tui::pose::{self, Pose};
pub(super) fn seated_anchor(desk: Point) -> Point {
Point {
x: desk.x + DESK_W.saturating_sub(8) / 2,
y: desk.y.saturating_sub(8),
}
}
pub(super) fn standing_at_desk_anchor(desk: Point) -> Point {
Point {
x: desk.x + DESK_W.saturating_sub(8) / 2,
y: desk.y.saturating_sub(12),
}
}
pub(super) fn walking_anchor(p: Point) -> Point {
Point {
x: p.x.saturating_sub(4),
y: p.y.saturating_sub(12),
}
}
pub(super) fn waypoint_anchor(wp: Point) -> Point {
Point {
x: wp.x.saturating_sub(4),
y: wp.y.saturating_sub(12),
}
}
fn breath_offset_y(agent_id: ascii_agents_core::AgentId, now: SystemTime) -> u16 {
let elapsed_ms = now
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
const CYCLE_MS: u64 = 4500;
let offset_ms = agent_id.raw() % CYCLE_MS;
let phase = elapsed_ms.wrapping_add(offset_ms) % CYCLE_MS;
if phase < CYCLE_MS / 2 {
0
} else {
1
}
}
pub(super) fn with_breath(
anchor: Point,
agent_id: ascii_agents_core::AgentId,
now: SystemTime,
) -> Point {
Point {
x: anchor.x,
y: anchor.y.saturating_sub(breath_offset_y(agent_id, now)),
}
}
pub(super) fn back_couch_anchor(wp: Point) -> Point {
Point {
x: wp.x.saturating_sub(4),
y: wp.y.saturating_sub(7),
}
}
pub(super) fn waypoint_rank_offset_x(kind: WaypointKind, rank: usize) -> i16 {
match (kind, rank) {
(_, 0) => 0,
(WaypointKind::Couch, 1) => 6,
(WaypointKind::Couch, 2) => -6,
(WaypointKind::Couch, _) => 0,
(_, 1) => 9,
(_, 2) => -9,
(_, _) => 0,
}
}
pub(super) fn walking_position(from: Point, to: Point, t_x1000: u16) -> Point {
let t = t_x1000 as i32;
let dx = to.x as i32 - from.x as i32;
let dy = to.y as i32 - from.y as i32;
Point {
x: (from.x as i32 + dx * t / 1000).max(0).min(u16::MAX as i32) as u16,
y: (from.y as i32 + dy * t / 1000).max(0).min(u16::MAX as i32) as u16,
}
}
#[allow(clippy::too_many_arguments)]
pub(in crate::tui) fn character_anchor(
agent: &AgentSlot,
layout: &crate::tui::layout::Layout,
now: SystemTime,
router: &mut dyn Router,
overlay: &OccupancyOverlay,
history: &mut pose::PoseHistory,
) -> Option<Point> {
let desk = *layout.home_desks.get(agent.desk_index)?;
let pose = pose::derive_with_routing(agent, now, layout, router, overlay, history)?;
let anchor = match pose {
Pose::SeatedIdle | Pose::SeatedThinking | Pose::SeatedTyping { .. } => seated_anchor(desk),
Pose::StandingAtDesk => standing_at_desk_anchor(desk),
Pose::AtWaypoint { wp, kind } => {
let wp_obj = layout.waypoints.get(wp)?;
match kind {
WaypointKind::Couch => back_couch_anchor(wp_obj.pos),
_ => waypoint_anchor(wp_obj.pos),
}
}
Pose::AimlessAt { dest } => waypoint_anchor(dest),
Pose::Walking {
from, to, t_x1000, ..
} => walking_anchor(walking_position(from, to, t_x1000)),
};
Some(anchor)
}
const DOOR_TRANSITION_MS: u64 = 200;
pub(super) fn compute_door_frame_idx(agents: &[AgentSlot], now: SystemTime) -> usize {
fn frame_for_progress(elapsed_ms: u64, total_ms: u64) -> usize {
if elapsed_ms < DOOR_TRANSITION_MS {
if elapsed_ms < DOOR_TRANSITION_MS / 2 {
1
} else {
2
}
} else if elapsed_ms + DOOR_TRANSITION_MS > total_ms {
let remaining = total_ms.saturating_sub(elapsed_ms);
if remaining < DOOR_TRANSITION_MS / 2 {
0
} else {
1
}
} else {
2
}
}
let mut max_frame: usize = 0;
for a in agents {
if a.exiting_at.is_none() {
if let Ok(d) = now.duration_since(a.created_at) {
let ms = d.as_millis() as u64;
if ms < pose::ENTRY_ANIMATION_MS {
max_frame = max_frame.max(frame_for_progress(ms, pose::ENTRY_ANIMATION_MS));
}
}
}
if let Some(exit_at) = a.exiting_at {
if let Ok(d) = now.duration_since(exit_at) {
let ms = d.as_millis() as u64;
let exit_window_ms =
ascii_agents_core::state::reducer::EXIT_GRACE_WINDOW.as_millis() as u64;
if ms < exit_window_ms {
max_frame = max_frame.max(frame_for_progress(ms, exit_window_ms));
}
}
}
}
max_frame
}