use std::time::SystemTime;
use ascii_agents_core::sprite::blit::blit_frame;
use ascii_agents_core::sprite::format::Pack;
use ascii_agents_core::sprite::{Rgb, RgbBuffer};
use ascii_agents_core::AgentSlot;
use super::effects::{
paint_coffee_steam, paint_pet_hearts, paint_screen_glow, paint_sleep_z, paint_thinking_dots,
paint_waiting_bubble, paint_walking_dust,
};
use super::furniture::{
paint_area_rug, paint_coffee_table, paint_pantry_chair, paint_pantry_table, paint_side_table,
};
use super::paint_character_at;
use crate::tui::frame_cache::FrameCache;
use crate::tui::layout::{Layout, Point, DESK_H, DESK_W};
pub(super) struct Drawable<'a> {
pub(super) anchor_y: u16,
pub(super) kind: DrawableKind<'a>,
}
pub(super) enum DrawableKind<'a> {
DeskCubicle {
desk: Point,
is_last_col: bool,
has_cabinet: bool,
screen_glow: Option<Rgb>,
session_age_secs: u64,
has_coffee: bool,
coffee_steam: bool,
},
Character {
agent: &'a AgentSlot,
anim_name: &'static str,
frame_idx: usize,
anchor: Point,
flip_x: bool,
glow_tint: Option<Rgb>,
sleep_z_seed: Option<u64>,
waiting_bubble: bool,
thinking_dots: bool,
walking_dust_frame: Option<usize>,
},
WaypointCouch {
pos: Point,
},
WaypointPantry {
pos: Point,
use_large: bool,
},
MeetingSofa {
pos: Point,
mirrored: bool,
},
MeetingTable {
pos: Point,
},
AreaRug {
pos: Point,
width: u16,
height: u16,
},
LoungeSideTable {
pos: Point,
},
PantryTable {
pos: Point,
},
PantryChair {
pos: Point,
},
Plant {
kind: crate::tui::layout::PlantKind,
pos: Point,
},
PodDecorItem {
kind: crate::tui::layout::PodDecor,
pos: Point,
},
FloorLamp {
pos: Point,
},
Door {
pos: Point,
frame_idx: usize,
},
WallDecor {
kind: crate::tui::layout::WallDecor,
pos: Point,
},
VendingMachine {
pos: Point,
},
Printer {
pos: Point,
},
Cat {
pos: Point,
flip: bool,
anim_name: &'static str,
frame_idx: usize,
pet_elapsed_ms: Option<u64>,
},
}
pub(super) fn cat_position(
layout: &Layout,
pack: &Pack,
now: SystemTime,
idle_desk_indices: &[usize],
all_idle: bool,
cat_seed: u64,
) -> Option<(Point, bool, &'static str, usize)> {
pack.animation("cat_walk")?;
layout.corridor?;
let elapsed_ms = now
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
const CYCLE_MS: u64 = 40_000;
let cycle_n = (elapsed_ms / CYCLE_MS).wrapping_add(cat_seed);
let frac = (elapsed_ms % CYCLE_MS) as f32 / CYCLE_MS as f32;
let mut spots: Vec<(Point, bool)> = Vec::new();
for (i, desk) in layout.home_desks.iter().enumerate() {
spots.push((
Point {
x: desk.x + DESK_W + 1,
y: desk.y + DESK_H + 2,
},
idle_desk_indices.contains(&i),
));
}
if let Some(wp) = layout
.waypoints
.iter()
.find(|w| matches!(w.kind, crate::tui::layout::WaypointKind::Pantry))
{
spots.push((
Point {
x: wp.pos.x + 4,
y: wp.pos.y + 6,
},
false,
));
}
for sofa in &layout.meeting_sofas {
spots.push((
Point {
x: sofa.x + 4,
y: sofa.y + 4,
},
false,
));
}
if let Some(wp) = layout
.waypoints
.iter()
.find(|w| matches!(w.kind, crate::tui::layout::WaypointKind::Couch))
{
spots.push((
Point {
x: wp.pos.x + 4,
y: wp.pos.y + 6,
},
false,
));
}
if let Some(corridor) = layout.corridor {
spots.push((
Point {
x: corridor.x + corridor.width / 2,
y: corridor.y + corridor.height / 2,
},
false,
));
}
if spots.is_empty() {
return None;
}
let pick = |n: u64| -> (Point, bool) {
let h = n.wrapping_mul(0x9e37_79b9_7f4a_7c15) as usize;
spots[h % spots.len()]
};
let (dest, is_idle_spot) = pick(cycle_n);
let (prev, _) = pick(cycle_n.wrapping_sub(1));
let frame_idx = (elapsed_ms / 220) as usize % 2;
if frac < 0.35 {
let t = frac / 0.35;
let x = prev.x as f32 + (dest.x as f32 - prev.x as f32) * t;
let y = prev.y as f32 + (dest.y as f32 - prev.y as f32) * t;
let flip = dest.x < prev.x;
return Some((
Point {
x: x as u16,
y: y as u16,
},
flip,
"cat_walk",
frame_idx,
));
}
let anim = if all_idle || is_idle_spot {
"cat_sleep"
} else {
"cat_sit"
};
Some((dest, false, anim, 0))
}
pub(super) fn paint_drawable(
d: &Drawable<'_>,
buf: &mut RgbBuffer,
pack: &Pack,
cache: &mut FrameCache,
now: SystemTime,
theme: &crate::tui::theme::Theme,
) {
match &d.kind {
DrawableKind::DeskCubicle {
desk,
is_last_col,
has_cabinet,
screen_glow,
session_age_secs,
has_coffee,
coffee_steam,
} => {
let divider = theme.office.cubicle_divider;
if !is_last_col {
let div_x = desk.x + DESK_W + 3;
for dy in 0..(DESK_H + 1) {
let py = desk.y.saturating_sub(1) + dy;
if div_x < buf.width && py < buf.height {
buf.put(div_x, py, divider);
}
}
}
if *has_cabinet {
if let Some(cab) = pack
.animation("filing_cabinet")
.and_then(|a| a.frames.first())
{
let cab_x = desk.x.saturating_sub(cab.width + 1);
let cab_y = desk.y;
if cab_y + cab.height <= buf.height {
blit_frame(cab, cab_x, cab_y, buf);
}
}
}
if let Some(frame) = pack.animation("desk").and_then(|a| a.frames.first()) {
blit_frame(frame, desk.x, desk.y, buf);
}
if let Some(bin) = pack.animation("trash_bin").and_then(|a| a.frames.first()) {
let bin_x = desk.x + DESK_W;
let bin_y = desk.y + 4;
if bin_x + bin.width <= buf.width && bin_y + bin.height <= buf.height {
blit_frame(bin, bin_x, bin_y, buf);
}
}
paint_desk_personalization(
buf,
*desk,
*session_age_secs,
*has_coffee,
*coffee_steam,
now,
theme,
);
if let Some(tint) = screen_glow {
paint_screen_glow(buf, desk.x, desk.y, now, *tint, theme);
}
}
DrawableKind::Character {
agent,
anim_name,
frame_idx,
anchor,
flip_x,
glow_tint,
sleep_z_seed,
waiting_bubble,
thinking_dots,
walking_dust_frame,
} => {
if let Some(dust_frame) = walking_dust_frame {
paint_walking_dust(buf, *anchor, *dust_frame, theme);
}
paint_character_at(
buf, anim_name, *frame_idx, *anchor, agent, pack, *flip_x, *glow_tint, cache,
);
if let Some(seed) = sleep_z_seed {
paint_sleep_z(buf, *anchor, now, *seed, theme);
}
if *waiting_bubble {
paint_waiting_bubble(buf, *anchor, theme);
}
if *thinking_dots {
paint_thinking_dots(buf, *anchor, now, theme);
}
}
DrawableKind::WaypointCouch { pos } => {
if let Some(f) = pack
.animation("meeting_sofa")
.and_then(|a| a.frames.first())
{
let cx = pos.x.saturating_sub(f.width / 2);
let cy = pos.y.saturating_sub(f.height / 2);
let flipped = f.mirror_vertical();
blit_frame(&flipped, cx, cy, buf);
}
}
DrawableKind::WaypointPantry { pos, use_large } => {
let anim_name = if *use_large { "pantry" } else { "pantry_small" };
if let Some(f) = pack.animation(anim_name).and_then(|a| a.frames.first()) {
let cx = pos.x.saturating_sub(f.width / 2);
let cy = pos.y.saturating_sub(f.height / 2);
blit_frame(f, cx, cy, buf);
}
let steam_dx: i16 = if *use_large { -2 } else { 1 };
let steam_x = (pos.x as i32 + steam_dx as i32).max(0) as u16;
paint_coffee_steam(
buf,
Point {
x: steam_x,
y: pos.y.saturating_sub(2),
},
now,
theme,
);
}
DrawableKind::MeetingSofa { pos, mirrored } => {
if let Some(f) = pack
.animation("meeting_sofa")
.and_then(|a| a.frames.first())
{
let sx = pos.x.saturating_sub(f.width / 2);
let sy = pos.y.saturating_sub(f.height / 2);
if *mirrored {
let flipped = f.mirror_vertical();
blit_frame(&flipped, sx, sy, buf);
} else {
blit_frame(f, sx, sy, buf);
}
}
}
DrawableKind::MeetingTable { pos } => {
paint_coffee_table(buf, pos.x, pos.y, 11, 5, theme);
}
DrawableKind::AreaRug { pos, width, height } => {
paint_area_rug(buf, pos.x, pos.y, *width, *height, theme);
}
DrawableKind::LoungeSideTable { pos } => {
paint_side_table(buf, pos.x, pos.y, theme);
}
DrawableKind::PantryTable { pos } => {
paint_pantry_table(buf, pos.x, pos.y, theme);
}
DrawableKind::PantryChair { pos } => {
paint_pantry_chair(buf, pos.x, pos.y, theme);
}
DrawableKind::Plant { kind, pos } => {
use crate::tui::layout::PlantKind;
let anim_name = match kind {
PlantKind::Ficus => "plant",
PlantKind::Tall => "plant_tall",
PlantKind::Flower => "plant_flower",
PlantKind::Succulent => "plant_succulent",
};
if let Some(f) = pack.animation(anim_name).and_then(|a| a.frames.first()) {
let px = pos.x.saturating_sub(f.width / 2);
let py = pos.y.saturating_sub(f.height / 2);
blit_frame(f, px, py, buf);
}
}
DrawableKind::PodDecorItem { kind, pos } => {
use crate::tui::layout::PodDecor;
let anim_name = match kind {
PodDecor::PlantTall => "plant_tall",
PodDecor::Whiteboard => "whiteboard",
PodDecor::Tv => "tv_stand",
PodDecor::PhoneBooth => "phone_booth",
PodDecor::StandingDesk => "standing_desk",
};
if let Some(f) = pack.animation(anim_name).and_then(|a| a.frames.first()) {
let px = pos.x.saturating_sub(f.width / 2);
let py = pos.y.saturating_sub(f.height / 2);
blit_frame(f, px, py, buf);
}
}
DrawableKind::FloorLamp { pos } => {
if let Some(f) = pack.animation("floor_lamp").and_then(|a| a.frames.first()) {
let px = pos.x.saturating_sub(f.width / 2);
let py = pos.y.saturating_sub(f.height / 2);
blit_frame(f, px, py, buf);
}
}
DrawableKind::Door { pos, frame_idx } => {
if let Some(f) = pack
.animation("door")
.and_then(|a| a.frames.get(*frame_idx).or_else(|| a.frames.first()))
{
blit_frame(f, pos.x, pos.y, buf);
}
}
DrawableKind::WallDecor { kind, pos } => {
use crate::tui::layout::WallDecor;
let anim_name = match kind {
WallDecor::Bookshelf => "bookshelf",
WallDecor::BulletinBoard => "bulletin_board",
WallDecor::ExitSign => "exit_sign",
WallDecor::Whiteboard => "whiteboard",
WallDecor::MeetingScreen => "meeting_screen",
};
if let Some(f) = pack.animation(anim_name).and_then(|a| a.frames.first()) {
blit_frame(f, pos.x, pos.y, buf);
}
}
DrawableKind::VendingMachine { pos } => {
let body = Rgb(50, 55, 65);
let panel = Rgb(180, 60, 60);
let drinks = [
Rgb(220, 50, 50),
Rgb(50, 160, 50),
Rgb(50, 80, 200),
Rgb(220, 180, 40),
];
let vx = pos.x.saturating_sub(2);
let vy = pos.y.saturating_sub(3);
for dy in 0..6u16 {
for dx in 0..4u16 {
let px = vx + dx;
let py = vy + dy;
if px < buf.width && py < buf.height {
let color = if dy == 0 {
panel
} else if (1..=3).contains(&dy) && (1..=2).contains(&dx) {
let idx = ((dy - 1) * 2 + (dx - 1)) as usize;
if idx < drinks.len() {
drinks[idx]
} else {
body
}
} else if dy == 4 && dx == 2 {
Rgb(180, 170, 100)
} else if dy == 5 {
Rgb(40, 42, 48)
} else {
body
};
buf.put(px, py, color);
}
}
}
}
DrawableKind::Printer { pos } => {
let body_white = Rgb(220, 220, 225);
let top_dark = Rgb(60, 60, 68);
let glass = Rgb(130, 180, 200);
let paper = Rgb(245, 245, 240);
let tray = Rgb(180, 180, 185);
let px0 = pos.x.saturating_sub(2);
let py0 = pos.y.saturating_sub(2);
for dy in 0..4u16 {
for dx in 0..5u16 {
let px = px0 + dx;
let py = py0 + dy;
if px < buf.width && py < buf.height {
let color = if dy == 0 {
if (1..=3).contains(&dx) {
glass
} else {
top_dark
}
} else if dy == 3 {
if (1..=3).contains(&dx) {
paper
} else {
tray
}
} else if dx == 0 || dx == 4 {
tray
} else {
body_white
};
buf.put(px, py, color);
}
}
}
}
DrawableKind::Cat {
pos,
flip,
anim_name,
frame_idx,
pet_elapsed_ms,
} => {
let Some(anim) = pack.animation(anim_name) else {
return;
};
let Some(frame) = anim.frames.get(*frame_idx).or(anim.frames.first()) else {
return;
};
let final_frame = if *flip {
frame.mirror_horizontal()
} else {
frame.clone()
};
let px = pos.x.saturating_sub(final_frame.width / 2);
let py = pos.y.saturating_sub(final_frame.height / 2);
blit_frame(&final_frame, px, py, buf);
if let Some(elapsed) = pet_elapsed_ms {
paint_pet_hearts(buf, *pos, *elapsed, theme);
} else if *anim_name == "cat_sleep" {
paint_sleep_z(buf, *pos, now, 0xCAFE, theme);
}
}
}
}
fn paint_desk_personalization(
buf: &mut RgbBuffer,
desk: Point,
age_secs: u64,
has_coffee: bool,
coffee_steam: bool,
now: SystemTime,
theme: &crate::tui::theme::Theme,
) {
if age_secs == 0 && !has_coffee {
return;
}
let put = |buf: &mut RgbBuffer, x: u16, y: u16, c: Rgb| {
if x < buf.width && y < buf.height {
buf.put(x, y, c);
}
};
if has_coffee {
let cx = desk.x + 2;
let cy = desk.y + 2;
put(buf, cx, cy, theme.furniture.coffee_cup);
put(buf, cx + 1, cy, theme.furniture.coffee_cup);
put(buf, cx, cy + 1, theme.furniture.coffee_cup_shadow);
put(buf, cx + 1, cy + 1, theme.furniture.coffee_cup_shadow);
if coffee_steam {
paint_coffee_steam(buf, Point { x: cx, y: cy }, now, theme);
}
}
if age_secs >= 1800 {
let px = desk.x + DESK_W - 2;
let py = desk.y + 1;
put(buf, px, py, theme.furniture.desk_plant_light);
put(buf, px + 1, py, theme.furniture.desk_plant_dark);
put(buf, px, py + 1, theme.furniture.desk_plant_light);
put(buf, px + 1, py + 1, theme.furniture.desk_plant_light);
put(buf, px, py + 2, theme.furniture.desk_plant_pot);
put(buf, px + 1, py + 2, theme.furniture.desk_plant_pot);
}
if age_secs >= 3600 {
let fx = desk.x + 1;
let fy = desk.y;
put(buf, fx, fy, theme.furniture.photo_frame);
put(buf, fx + 1, fy, theme.furniture.photo_frame);
put(buf, fx, fy + 1, theme.furniture.photo_bg);
put(buf, fx + 1, fy + 1, theme.furniture.photo_bg);
}
}