use std::time::{Duration, SystemTime};
use ascii_agents_core::state::{AgentSlot, SceneState};
use ascii_agents_core::walkable::OccupancyOverlay;
use crate::tui::frame_cache::FrameCache;
use crate::tui::pathfind::{AStarRouter, Router};
use crate::tui::pose::PoseHistory;
pub use ascii_agents_core::state::MAX_FLOORS;
#[derive(Debug, Clone, Copy)]
pub struct FloorMeta {
pub floor_idx: usize,
pub altitude: f32,
pub floor_seed: u64,
pub sunlight_boost: f32,
}
impl FloorMeta {
pub fn for_floor(floor_idx: usize, total_floors: usize) -> Self {
let altitude = if total_floors <= 1 {
0.0
} else {
floor_idx as f32 / (total_floors - 1) as f32
};
Self {
floor_idx,
altitude,
floor_seed: (floor_idx as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15),
sunlight_boost: altitude * 0.3,
}
}
pub fn ground() -> Self {
Self::for_floor(0, 1)
}
}
pub struct FloorCtx {
pub router: AStarRouter,
pub overlay: OccupancyOverlay,
pub history: PoseHistory,
pub cache: FrameCache,
}
impl Default for FloorCtx {
fn default() -> Self {
Self::new()
}
}
impl FloorCtx {
pub fn new() -> Self {
Self {
router: AStarRouter::new(),
overlay: OccupancyOverlay::new(),
history: PoseHistory::new(),
cache: FrameCache::new(),
}
}
pub fn flush_caches(&mut self) {
self.router.invalidate();
self.overlay.clear();
self.history = PoseHistory::new();
self.cache = FrameCache::new();
}
}
pub struct FloorTransition {
pub from_floor: usize,
pub to_floor: usize,
pub started_at: SystemTime,
pub duration_ms: u64,
}
const TRANSITION_DURATION_MS: u64 = 900;
impl FloorTransition {
pub fn new(from: usize, to: usize, now: SystemTime) -> Self {
Self {
from_floor: from,
to_floor: to,
started_at: now,
duration_ms: TRANSITION_DURATION_MS,
}
}
pub fn t(&self, now: SystemTime) -> f32 {
let elapsed = now
.duration_since(self.started_at)
.unwrap_or(Duration::ZERO)
.as_millis() as f32;
let linear = (elapsed / self.duration_ms as f32).clamp(0.0, 1.0);
ease_out(linear)
}
pub fn is_done(&self, now: SystemTime) -> bool {
self.t(now) >= 1.0
}
}
fn ease_out(t: f32) -> f32 {
1.0 - (1.0 - t) * (1.0 - t)
}
pub fn floor_of(desk_index: usize, desks_per_floor: usize) -> usize {
if desks_per_floor == 0 {
return 0;
}
desk_index / desks_per_floor
}
pub fn floor_local_desk(desk_index: usize, desks_per_floor: usize) -> usize {
if desks_per_floor == 0 {
return 0;
}
desk_index % desks_per_floor
}
pub fn num_floors(scene: &SceneState) -> usize {
if scene.agents.is_empty() || scene.max_desks == 0 {
return 1;
}
let max_idx = scene
.agents
.values()
.map(|a| a.desk_index)
.max()
.unwrap_or(0);
max_idx / scene.max_desks + 1
}
pub fn build_floor_scene(scene: &SceneState, floor_idx: usize) -> (Vec<AgentSlot>, usize) {
let dpf = scene.max_desks;
let lo = floor_idx * dpf;
let hi = lo + dpf;
let agents: Vec<AgentSlot> = scene
.agents
.values()
.filter(|a| a.desk_index >= lo && a.desk_index < hi)
.map(|a| {
let mut slot = a.clone();
slot.desk_index = a.desk_index - lo;
slot
})
.collect();
(agents, dpf)
}
#[cfg(test)]
mod tests {
use super::*;
use ascii_agents_core::id::AgentId;
use ascii_agents_core::state::ActivityState;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
fn make_scene(n: usize, max_desks: usize) -> SceneState {
let mut s = SceneState::new(max_desks);
let now = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000);
for i in 0..n {
let id = AgentId::from_transcript_path(&format!("/p/{i}.jsonl"));
s.agents.insert(
id,
AgentSlot {
agent_id: id,
source: Arc::from("cc"),
session_id: Arc::from(format!("s{i}").as_str()),
cwd: Arc::from(Path::new("/repo")),
label: Arc::from(format!("a{i}").as_str()),
state: ActivityState::Idle,
state_started_at: now,
created_at: now,
last_event_at: now,
exiting_at: None,
pending_idle_at: None,
desk_index: i,
tool_call_count: 0,
active_ms: 0,
unknown_cwd: false,
parent_id: None,
},
);
}
s
}
#[test]
fn floor_of_maps_desk_to_floor() {
assert_eq!(floor_of(0, 16), 0);
assert_eq!(floor_of(15, 16), 0);
assert_eq!(floor_of(16, 16), 1);
assert_eq!(floor_of(31, 16), 1);
assert_eq!(floor_of(32, 16), 2);
}
#[test]
fn floor_local_desk_remaps_to_floor_range() {
assert_eq!(floor_local_desk(0, 16), 0);
assert_eq!(floor_local_desk(16, 16), 0);
assert_eq!(floor_local_desk(17, 16), 1);
assert_eq!(floor_local_desk(31, 16), 15);
}
#[test]
fn num_floors_with_overflow() {
let scene = make_scene(20, 16);
assert_eq!(num_floors(&scene), 2);
}
#[test]
fn num_floors_exact_fit() {
let scene = make_scene(16, 16);
assert_eq!(num_floors(&scene), 1);
}
#[test]
fn num_floors_empty() {
let scene = make_scene(0, 16);
assert_eq!(num_floors(&scene), 1);
}
#[test]
fn build_floor_scene_filters_and_remaps() {
let scene = make_scene(20, 16);
let (floor0, dpf0) = build_floor_scene(&scene, 0);
assert_eq!(dpf0, 16);
assert_eq!(floor0.len(), 16);
for a in &floor0 {
assert!(
a.desk_index < 16,
"desk_index {} out of range",
a.desk_index
);
}
let (floor1, dpf1) = build_floor_scene(&scene, 1);
assert_eq!(dpf1, 16);
assert_eq!(floor1.len(), 4);
let mut indices: Vec<usize> = floor1.iter().map(|a| a.desk_index).collect();
indices.sort();
assert_eq!(indices, vec![0, 1, 2, 3]);
}
#[test]
fn transition_t_progresses() {
let start = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000);
let tr = FloorTransition::new(0, 1, start);
assert!((tr.t(start) - 0.0).abs() < f32::EPSILON);
let mid = start + Duration::from_millis(450);
let t_mid = tr.t(mid);
assert!(
t_mid > 0.0 && t_mid < 1.0,
"mid should be between 0 and 1, got {t_mid}"
);
let end = start + Duration::from_millis(900);
assert!((tr.t(end) - 1.0).abs() < f32::EPSILON);
assert!(!tr.is_done(start + Duration::from_millis(450)));
assert!(tr.is_done(end));
}
#[test]
fn transition_t_clamps_past_duration() {
let start = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000);
let tr = FloorTransition::new(0, 1, start);
let past = start + Duration::from_millis(1000);
assert!((tr.t(past) - 1.0).abs() < f32::EPSILON);
assert!(tr.is_done(past));
}
}