use std::collections::HashMap;
use std::time::SystemTime;
use pixtuoid_core::layout::WALKING_Y_OFF;
use pixtuoid_core::sprite::blit::blit_frame;
use pixtuoid_core::sprite::format::Pack;
use pixtuoid_core::sprite::{Rgb, RgbBuffer};
use pixtuoid_core::state::{ActivityState, FloorLocalDeskIndex};
use pixtuoid_core::walkable::OccupancyOverlay;
use pixtuoid_core::{AgentSlot, SceneState};
use crate::tui::chitchat::{self, ActiveChitchat, ChitchatBubble};
use crate::tui::floor::LightingState;
use crate::tui::frame_cache::FrameCache;
use crate::tui::layout::{
z_sort_row, Anchor, Layout, PlantItem, PodDecorItem, Point, Size, WallDecorItem, WallSegment,
DESK_H, DESK_W, ELEVATOR_H, ELEVATOR_W,
};
use crate::tui::motion::MotionState;
use crate::tui::pathfind::Router;
use crate::tui::pet::PetFrame;
use crate::tui::pose::{self, Pose};
pub(super) fn epoch_ms(now: SystemTime) -> u64 {
now.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
pub struct PixelPassResult {
pub pet_pos: Option<PetFrame>,
pub mascot_pos: Option<MascotFrame>,
pub chitchat_bubbles: Vec<ChitchatBubble>,
pub new_coffee_carriers: Vec<pixtuoid_core::AgentId>,
}
#[derive(Clone, Copy)]
pub struct MascotFrame {
pub pos: Point,
pub name: &'static str,
pub busy: bool,
pub degraded: bool,
pub active_sessions: u32,
}
mod ambient;
mod anchors;
mod background;
mod debug_overlay;
mod drawable;
mod effects;
mod furniture;
mod glass;
mod palette;
mod seat;
pub(in crate::tui) use anchors::character_anchor;
pub(in crate::tui) use anchors::walking_position;
use anchors::{
back_couch_anchor, compute_door_frame_idx, seated_anchor, standing_at_desk_anchor,
walking_anchor, waypoint_anchor, waypoint_rank_offset_x, with_breath, CHARACTER_SPRITE_W,
};
use background::{
daylight_floor_overlay, dim_floor_overlay, paint_ceiling_pool, paint_clock,
paint_corridor_runner, paint_floor_and_walls, paint_floor_lamp_halo, paint_neon_panel,
paint_shadow, time_of_day_look, Ellipse,
};
use drawable::{
gateway_mascot_def, mascot_position, paint_drawable, pet_position, Drawable, DrawableKind,
};
use glass::{paint_glass_wall_h, paint_glass_wall_v, stitch_vertical_wall, WALL_THICK_H_PX};
use palette::{agent_palette, recolor_frame};
use seat::{paint_character_at, seat_sprite, settle_seat_view, SeatView};
pub fn weather_names() -> Vec<&'static str> {
background::Weather::ALL.iter().map(|w| w.name()).collect()
}
pub fn force_weather(name: Option<&str>) -> Result<(), Vec<&'static str>> {
match name {
None => {
background::set_weather_override(None);
Ok(())
}
Some(s) => match background::Weather::from_name(s) {
Some(w) => {
background::set_weather_override(Some(w));
Ok(())
}
None => Err(weather_names()),
},
}
}
const COFFEE_STEAM_WINDOW_SECS: u64 = 120;
const DESK_FRONT_OVERHANG: u16 = 2;
fn center_pin_south_offset(h: u16) -> u16 {
h.saturating_sub(1) / 2
}
fn floor_lamp_south_offset() -> u16 {
center_pin_south_offset(
crate::tui::layout::furniture_def(crate::tui::layout::Furniture::FloorLamp)
.visual
.h,
)
}
pub struct PixelCtx<'a> {
pub scene: &'a SceneState,
pub layout: &'a Layout,
pub pack: &'a Pack,
pub now: SystemTime,
pub buf: &'a mut RgbBuffer,
pub cache: &'a mut FrameCache,
pub router: &'a mut dyn Router,
pub overlay: &'a mut OccupancyOverlay,
pub history: &'a mut pose::PoseHistory,
pub motion: &'a mut std::collections::HashMap<pixtuoid_core::AgentId, MotionState>,
pub door_anim_max_ms: u64,
pub theme: &'a crate::tui::theme::Theme,
pub floor: crate::tui::floor::FloorMeta,
pub active_pet: Option<&'a crate::tui::renderer::PetState>,
pub floor_pet: Option<&'a crate::tui::pet::Pet>,
pub chitchat_state: &'a mut HashMap<crate::tui::chitchat::VenueKey, ActiveChitchat>,
pub coffee_holders: &'a std::collections::HashSet<pixtuoid_core::AgentId>,
pub coffee_fetched_at: &'a HashMap<pixtuoid_core::AgentId, SystemTime>,
pub light: &'a mut crate::tui::floor::LightingState,
pub debug_walkable: bool,
}
pub fn render_to_rgb_buffer(ctx: &mut PixelCtx<'_>) -> PixelPassResult {
let agents: Vec<_> = ctx.scene.agents.values().cloned().collect();
let buf_w = ctx.layout.buf_w;
let buf_h = ctx.layout.buf_h;
let mut new_coffee_carriers: Vec<pixtuoid_core::AgentId> = Vec::new();
let look = time_of_day_look(ctx.now, ctx.theme);
let top_wall_h = ctx
.layout
.top_margin
.saturating_sub(pixtuoid_core::layout::WALL_BAND_TO_TOP_MARGIN);
let door_x_range = ctx.layout.door.map(|d| (d.x, d.x + ELEVATOR_W));
paint_floor_and_walls(
ctx.buf,
buf_w,
buf_h,
ctx.now,
&look,
top_wall_h,
door_x_range,
ctx.theme,
ctx.floor.altitude,
);
let indoor_scale = ctx.light.tick(ctx.scene.agents.is_empty(), ctx.now);
let min_level = LightingState::MIN_LEVEL;
let boost_ceiling = LightingState::EMPTY_FLOOR_DIM_BOOST;
let empty_floor_boost = 1.0 + (1.0 - indoor_scale) * (boost_ceiling - 1.0) / (1.0 - min_level);
let dim_strength = (0.45 - ctx.floor.sunlight_boost).max(0.1);
dim_floor_overlay(
ctx.buf,
top_wall_h,
buf_h,
look.darkness * dim_strength * empty_floor_boost,
ctx.theme,
);
const DAYLIGHT_FLOOR_LIFT: f32 = 0.22;
daylight_floor_overlay(
ctx.buf,
top_wall_h,
buf_h,
look.spill_strength * DAYLIGHT_FLOOR_LIFT,
);
let pool_strength = (0.15 + 0.30 * look.darkness) * indoor_scale;
for desk in &ctx.layout.home_desks {
paint_ceiling_pool(
ctx.buf,
Ellipse {
cx: desk.x + DESK_W / 2,
cy: desk.y.saturating_sub(2),
half_w: 10,
half_h: 5,
},
pool_strength,
ctx.theme,
);
}
if let Some(pr) = ctx.layout.pantry_room {
paint_ceiling_pool(
ctx.buf,
Ellipse {
cx: pr.x + pr.width / 2,
cy: pr.y + pr.height / 2,
half_w: 12,
half_h: 6,
},
pool_strength,
ctx.theme,
);
}
if let Some(corridor) = ctx.layout.corridor {
paint_ceiling_pool(
ctx.buf,
Ellipse {
cx: corridor.x + corridor.width / 2,
cy: corridor.y + corridor.height / 2,
half_w: 14,
half_h: 5,
},
pool_strength,
ctx.theme,
);
}
if let Some(lamp) = ctx.layout.floor_lamp {
paint_floor_lamp_halo(
ctx.buf,
lamp.x,
lamp.y + floor_lamp_south_offset(), look.darkness * 0.55 * indoor_scale,
ctx.theme,
);
}
let neon_w = 30u16;
let neon_h = 8u16;
paint_neon_panel(ctx.buf, 1, 1, neon_w, neon_h, ctx.now, ctx.theme);
let clock_x = (buf_w / 2).saturating_sub(3).max(neon_w + 2);
paint_clock(ctx.buf, clock_x, 1, ctx.now, ctx.theme);
if let Some(corridor) = ctx.layout.corridor {
paint_corridor_runner(ctx.buf, corridor, ctx.theme);
}
let h_rows: Vec<u16> = ctx
.layout
.room_walls
.iter()
.filter(|w| w.start.y == w.end.y)
.map(|w| w.start.y)
.collect();
for &WallSegment { start, end } in &ctx.layout.room_walls {
if start.x != end.x {
continue; }
let (y_top, y_bot) =
stitch_vertical_wall(start.y, end.y, ctx.layout.top_margin, top_wall_h, &h_rows);
paint_glass_wall_v(ctx.buf, ctx.theme, start.x, y_top, y_bot.min(buf_h - 1));
}
if let Some(mr) = ctx.layout.meeting_room {
furniture::paint_notice_board(ctx.buf, mr, ctx.theme);
furniture::paint_doormat(ctx.buf, mr, ctx.theme);
}
if let Some(pr) = ctx.layout.pantry_room {
furniture::paint_water_cooler(ctx.buf, pr, ctx.theme);
furniture::paint_trash_bin(ctx.buf, pr);
}
let shadow_strength = 0.5 - 0.3 * look.darkness;
for desk in &ctx.layout.home_desks {
paint_shadow(
ctx.buf,
Ellipse {
cx: desk.x + DESK_W / 2,
cy: desk.y + 7,
half_w: DESK_W / 2 + 1,
half_h: 3,
},
shadow_strength,
ctx.theme,
);
}
for wp in &ctx.layout.waypoints {
use crate::tui::layout::WaypointKind;
if matches!(wp.kind, WaypointKind::Couch | WaypointKind::Printer) {
continue;
}
paint_shadow(
ctx.buf,
Ellipse {
cx: wp.pos.x,
cy: wp.pos.y + 2,
half_w: 7,
half_h: 2,
},
shadow_strength,
ctx.theme,
);
}
for wp in ctx
.layout
.waypoints
.iter()
.filter(|w| w.kind == crate::tui::layout::WaypointKind::Printer)
{
paint_shadow(
ctx.buf,
Ellipse {
cx: wp.pos.x,
cy: wp.pos.y + 1,
half_w: 5,
half_h: 1,
},
shadow_strength,
ctx.theme,
);
}
if let Some(center) = ctx.layout.couch_sprite_center {
paint_shadow(
ctx.buf,
Ellipse {
cx: center.x,
cy: center.y + 2,
half_w: 7,
half_h: 2,
},
shadow_strength,
ctx.theme,
);
}
for &PlantItem { kind, pos } in &ctx.layout.plants {
let cy = pos.y
+ center_pin_south_offset(crate::tui::layout::furniture_def(kind.furniture()).visual.h);
paint_shadow(
ctx.buf,
Ellipse {
cx: pos.x,
cy,
half_w: 3,
half_h: 1,
},
shadow_strength,
ctx.theme,
);
}
if let Some(lamp) = ctx.layout.floor_lamp {
paint_shadow(
ctx.buf,
Ellipse {
cx: lamp.x,
cy: lamp.y + floor_lamp_south_offset(), half_w: 2,
half_h: 1,
},
shadow_strength,
ctx.theme,
);
}
let seated_agents: HashMap<FloorLocalDeskIndex, bool> = agents
.iter()
.filter(|a| {
ctx.layout
.home_desk(a.desk_index.single_floor_local())
.is_some()
&& a.exiting_at.is_none()
})
.map(|a| {
let p = pose::derive_with_routing(
a,
ctx.now,
ctx.layout,
&mut crate::tui::pose::RouteCtx {
router: &mut *ctx.router,
overlay: &*ctx.overlay,
history: &mut *ctx.history,
motion: &mut *ctx.motion,
},
);
let seated = matches!(p, Some(Pose::SeatedTyping { .. } | Pose::SeatedThinking));
(a.desk_index.single_floor_local(), seated)
})
.collect();
ambient::paint_ambient(ctx, &seated_agents);
ctx.overlay.clear();
for agent in &agents {
let Some(pose) = pose::derive(agent, ctx.now, ctx.layout) else {
continue;
};
if let Pose::AtWaypoint { wp, .. } = pose {
if let Some(w) = ctx.layout.waypoints.get(wp) {
let origin = ctx
.layout
.home_desk(agent.desk_index.single_floor_local())
.unwrap_or(w.pos);
let stand = pixtuoid_core::layout::stand_point(
w.kind,
w.pos,
ctx.layout.pantry_counter_size,
&ctx.layout.walkable,
origin,
w.facing,
);
ctx.overlay
.add(stand.x.saturating_sub(4), stand.y.saturating_sub(6), 8, 12);
}
}
}
let mut drawables: Vec<Drawable<'_>> = Vec::new();
enqueue_desk_cubicles(ctx, &agents, &seated_agents, &mut drawables);
enqueue_meeting_furniture(ctx.layout, &mut drawables);
enqueue_lounge_pantry_appliances(ctx.layout, &mut drawables);
enqueue_pod_decor_and_plants(ctx.layout, &mut drawables);
enqueue_floor_fixtures(ctx, &agents, &mut drawables);
enqueue_wall_decor(ctx.layout, &mut drawables);
let resolved_pet_pos = enqueue_pet(ctx, &agents, &mut drawables);
let resolved_mascot_pos = enqueue_gateway_mascot(ctx, &mut drawables);
let waypoint_visitors =
enqueue_characters(ctx, &agents, &mut drawables, &mut new_coffee_carriers);
enqueue_room_walls_h(ctx.layout, &mut drawables);
drawables.sort_by_key(|d| d.anchor_y);
for d in &drawables {
paint_drawable(d, ctx.buf, ctx.pack, ctx.cache, ctx.now, ctx.theme);
}
background::paint_lightning_flash(ctx.buf, ctx.now, background::weather_state(ctx.now));
if ctx.debug_walkable {
debug_overlay::paint(ctx.buf, ctx.layout, ctx.scene, ctx.motion);
}
let chitchat_bubbles = chitchat::update_and_collect(
ctx.chitchat_state,
ctx.floor.floor_idx,
&waypoint_visitors,
ctx.now,
);
PixelPassResult {
pet_pos: resolved_pet_pos,
mascot_pos: resolved_mascot_pos,
chitchat_bubbles,
new_coffee_carriers,
}
}
fn enqueue_characters<'a>(
ctx: &mut PixelCtx<'_>,
agents: &'a [AgentSlot],
drawables: &mut Vec<Drawable<'a>>,
new_coffee_carriers: &mut Vec<pixtuoid_core::AgentId>,
) -> Vec<chitchat::Visitor> {
let mut wp_rank: HashMap<usize, usize> = HashMap::new();
let mut waypoint_visitors: Vec<chitchat::Visitor> = Vec::new();
let couch_group_idx = ctx
.layout
.waypoints
.iter()
.position(|w| w.kind == crate::tui::layout::WaypointKind::Couch);
let char_w = ctx
.pack
.animation("standing")
.and_then(|a| a.frames.first())
.map_or(CHARACTER_SPRITE_W, |f| f.width);
for agent in agents {
let Some(desk) = ctx.layout.home_desk(agent.desk_index.single_floor_local()) else {
continue;
};
let Some(p) = pose::derive_with_routing(
agent,
ctx.now,
ctx.layout,
&mut crate::tui::pose::RouteCtx {
router: &mut *ctx.router,
overlay: &*ctx.overlay,
history: &mut *ctx.history,
motion: &mut *ctx.motion,
},
) else {
continue;
};
match p {
Pose::SeatedIdle => {
let anchor_no_breath = seated_anchor(desk, char_w);
let anchor = with_breath(anchor_no_breath, agent.agent_id, ctx.now);
let sleep_variant = if agent.agent_id.raw() % 2 == 0 {
"seated_sleeping"
} else {
"seated_sleeping_alt"
};
drawables.push(Drawable {
anchor_y: anchor_no_breath.y + WALKING_Y_OFF,
kind: DrawableKind::Character {
agent,
anim_name: sleep_variant,
frame_idx: 0,
anchor,
flip_x: false,
glow_tint: None,
sleep_z_seed: Some(agent.agent_id.raw()),
waiting_bubble: false,
walking_dust_frame: None,
},
});
}
Pose::SeatedThinking => {
let anchor_no_breath = seated_anchor(desk, char_w);
let anchor = with_breath(anchor_no_breath, agent.agent_id, ctx.now);
drawables.push(Drawable {
anchor_y: anchor_no_breath.y + WALKING_Y_OFF,
kind: DrawableKind::Character {
agent,
anim_name: "seated",
frame_idx: 0,
anchor,
flip_x: false,
glow_tint: Some(ctx.theme.tool_glow.default),
sleep_z_seed: None,
waiting_bubble: false,
walking_dust_frame: None,
},
});
}
Pose::SeatedTyping { frame } => {
let anchor_no_breath = seated_anchor(desk, char_w);
let anchor = with_breath(anchor_no_breath, agent.agent_id, ctx.now);
drawables.push(Drawable {
anchor_y: anchor_no_breath.y + WALKING_Y_OFF,
kind: DrawableKind::Character {
agent,
anim_name: "typing",
frame_idx: frame,
anchor,
flip_x: false,
glow_tint: palette::tool_glow_tint(agent, &ctx.theme.tool_glow),
sleep_z_seed: None,
waiting_bubble: false,
walking_dust_frame: None,
},
});
}
Pose::StandingAtDesk => {
let anchor_no_breath = standing_at_desk_anchor(desk, char_w);
let anchor = with_breath(anchor_no_breath, agent.agent_id, ctx.now);
let is_waiting = matches!(agent.state, ActivityState::Waiting { .. });
drawables.push(Drawable {
anchor_y: anchor_no_breath.y + WALKING_Y_OFF,
kind: DrawableKind::Character {
agent,
anim_name: "standing",
frame_idx: 0,
anchor,
flip_x: false,
glow_tint: None,
sleep_z_seed: None,
waiting_bubble: is_waiting,
walking_dust_frame: None,
},
});
}
Pose::AtWaypoint { wp, kind } => {
if let Some(wp_obj) = ctx.layout.waypoints.get(wp) {
let rank = *wp_rank.entry(wp).or_insert(0);
wp_rank.insert(wp, rank + 1);
let dx = waypoint_rank_offset_x(kind, rank);
use crate::tui::layout::WaypointKind;
let stand = pixtuoid_core::layout::stand_point(
wp_obj.kind,
wp_obj.pos,
ctx.layout.pantry_counter_size,
&ctx.layout.walkable,
desk,
wp_obj.facing,
);
let (anim_name, anchor_base, sprite_h, flip_x) = match kind {
WaypointKind::Pantry => (
"holding_coffee",
waypoint_anchor(stand, char_w),
12u16,
false,
),
WaypointKind::Couch | WaypointKind::MeetingSofa => {
let (anim, flip) = seat_sprite(kind, wp_obj.facing);
(anim, back_couch_anchor(stand, char_w), 9u16, flip)
}
WaypointKind::MeetingStand => {
let (anim, flip) = seat_sprite(kind, wp_obj.facing);
(anim, waypoint_anchor(stand, char_w), 12u16, flip)
}
WaypointKind::PhoneBooth
| WaypointKind::StandingDesk
| WaypointKind::VendingMachine
| WaypointKind::Printer => {
("standing", waypoint_anchor(stand, char_w), 12u16, false)
}
};
let anchor_no_breath = Point {
x: anchor_base.x.saturating_add_signed(dx),
y: anchor_base.y,
};
if chitchat::supports_chitchat(kind) {
waypoint_visitors.push(chitchat::Visitor {
wp_idx: chitchat::venue_wp_idx(kind, wp, couch_group_idx),
agent_id: agent.agent_id,
anchor: anchor_no_breath,
room_id: wp_obj.room_id,
});
}
let anchor = with_breath(anchor_no_breath, agent.agent_id, ctx.now);
drawables.push(Drawable {
anchor_y: match kind {
WaypointKind::Couch
| WaypointKind::MeetingSofa
| WaypointKind::MeetingStand => {
SeatView::of(kind, wp_obj.facing).z_key_for_seat(stand)
}
_ => anchor_no_breath.y + sprite_h,
},
kind: DrawableKind::Character {
agent,
anim_name,
frame_idx: 0,
anchor,
flip_x,
glow_tint: None,
sleep_z_seed: None,
waiting_bubble: false,
walking_dust_frame: None,
},
});
}
}
Pose::AimlessAt { dest } => {
let anchor_no_breath = waypoint_anchor(dest, char_w);
let anchor = with_breath(anchor_no_breath, agent.agent_id, ctx.now);
drawables.push(Drawable {
anchor_y: anchor_no_breath.y + WALKING_Y_OFF,
kind: DrawableKind::Character {
agent,
anim_name: "standing",
frame_idx: 0,
anchor,
flip_x: false,
glow_tint: None,
sleep_z_seed: None,
waiting_bubble: false,
walking_dust_frame: None,
},
});
}
Pose::Walking {
from,
to,
t_x1000,
frame,
mut carrying_coffee,
} => {
if agent.exiting_at.is_some() && ctx.coffee_holders.contains(&agent.agent_id) {
carrying_coffee = true;
}
if carrying_coffee {
new_coffee_carriers.push(agent.agent_id);
}
let pos = walking_position(from, to, t_x1000);
let walker_anchor = walking_anchor(pos, char_w);
let dx = to.x as i32 - from.x as i32;
let dy = to.y as i32 - from.y as i32;
let settle =
settle_seat_view(to, ctx.layout).or_else(|| settle_seat_view(from, ctx.layout));
let (going_back, flip) = match settle {
Some((view, _)) => view.settle_walk(),
None => (
dy.unsigned_abs() > dx.unsigned_abs() && dy < 0,
to.x < from.x,
),
};
let anim_name: &'static str = if going_back {
"walking_back"
} else if carrying_coffee && ctx.pack.animation("walking_coffee").is_some() {
"walking_coffee"
} else {
"walking"
};
drawables.push(Drawable {
anchor_y: match settle {
Some((_, z_key)) => z_key,
None => walker_anchor.y + WALKING_Y_OFF,
},
kind: DrawableKind::Character {
agent,
anim_name,
frame_idx: frame,
anchor: walker_anchor,
flip_x: flip,
glow_tint: None,
sleep_z_seed: None,
waiting_bubble: false,
walking_dust_frame: Some(frame),
},
});
}
}
}
waypoint_visitors
}
fn enqueue_room_walls_h<'a>(layout: &'a Layout, drawables: &mut Vec<Drawable<'a>>) {
for &WallSegment { start, end } in &layout.room_walls {
if start.y == end.y {
drawables.push(Drawable {
anchor_y: start.y + (WALL_THICK_H_PX - 1),
kind: DrawableKind::RoomWallH {
x0: start.x.min(end.x),
x1: start.x.max(end.x),
y_top: start.y,
},
});
}
}
}
fn enqueue_desk_cubicles<'a>(
ctx: &PixelCtx<'_>,
agents: &[AgentSlot],
seated_agents: &HashMap<FloorLocalDeskIndex, bool>,
drawables: &mut Vec<Drawable<'a>>,
) {
for (i, &desk) in ctx.layout.home_desks.iter().enumerate() {
let local = FloorLocalDeskIndex(i);
let Size {
w: desk_fp_w,
h: desk_fp_h,
} = crate::tui::layout::desk_furniture_def()
.footprint
.unwrap_or(Size {
w: DESK_W,
h: DESK_H,
});
let is_last_col = desk.x + desk_fp_w + DESK_W
>= ctx.layout.cubicle_band.x + ctx.layout.cubicle_band.width;
let occupant = agents
.iter()
.find(|a| a.desk_index.single_floor_local() == local && a.exiting_at.is_none());
let screen_glow = occupant
.filter(|_| seated_agents.get(&local).copied().unwrap_or(false))
.and_then(|a| palette::tool_glow_tint(a, &ctx.theme.tool_glow));
let has_coffee = occupant.is_some_and(|a| ctx.coffee_holders.contains(&a.agent_id));
let coffee_steam = has_coffee
&& occupant.is_some_and(|a| {
ctx.coffee_fetched_at
.get(&a.agent_id)
.and_then(|t| ctx.now.duration_since(*t).ok())
.is_some_and(|d| d.as_secs() < COFFEE_STEAM_WINDOW_SECS)
});
drawables.push(Drawable {
anchor_y: desk.y + desk_fp_h + DESK_FRONT_OVERHANG,
kind: DrawableKind::DeskCubicle {
desk,
is_last_col,
has_cabinet: i % 2 == 0,
screen_glow,
has_coffee,
coffee_steam,
},
});
}
}
fn enqueue_pet<'a>(
ctx: &PixelCtx<'_>,
agents: &[AgentSlot],
drawables: &mut Vec<Drawable<'a>>,
) -> Option<PetFrame> {
let kind = ctx.floor_pet.map(|p| p.kind)?;
let idle_desk_indices: Vec<FloorLocalDeskIndex> = agents
.iter()
.filter(|a| {
matches!(a.state, ActivityState::Idle)
&& ctx
.layout
.home_desk(a.desk_index.single_floor_local())
.is_some()
&& a.exiting_at.is_none()
})
.map(|a| a.desk_index.single_floor_local())
.collect();
let all_idle = agents
.iter()
.all(|a| matches!(a.state, ActivityState::Idle));
let active_pet = ctx
.active_pet
.filter(|p| p.is_active(ctx.now) && p.kind == kind && p.floor_idx == ctx.floor.floor_idx);
let pet_data = if let Some(pet) = active_pet {
Some((
pet.pet_pos,
false,
kind.sit_anim(),
0usize,
Some(pet.elapsed_ms(ctx.now)),
))
} else {
pet_position(
kind,
ctx.layout,
ctx.pack,
ctx.now,
&idle_desk_indices,
all_idle,
ctx.floor.floor_seed,
)
.map(|(pos, flip, anim, frame)| (pos, flip, anim, frame, None))
};
let (pos, flip, anim_name, frame_idx, pet_elapsed) = pet_data?;
let pet_h = ctx
.pack
.animation(anim_name)
.and_then(|a| a.frames.first())
.map_or(6, |f| f.height);
drawables.push(Drawable {
anchor_y: z_sort_row(Anchor::Center, pos, pet_h),
kind: DrawableKind::Pet {
kind,
pos,
flip,
anim_name,
frame_idx,
pet_elapsed_ms: pet_elapsed,
},
});
Some(PetFrame {
pos,
anim: anim_name,
kind,
})
}
fn enqueue_gateway_mascot<'a>(
ctx: &PixelCtx<'_>,
drawables: &mut Vec<Drawable<'a>>,
) -> Option<MascotFrame> {
let mut hover = None;
for (source, presence) in ctx.scene.daemons() {
let Some(def) = gateway_mascot_def(source) else {
continue;
};
let seed = source
.bytes()
.fold(0u64, |h, b| h.wrapping_mul(131).wrapping_add(b as u64));
let Some((pos, anim_name, frame_idx)) =
mascot_position(ctx.layout, presence, def.walk, def.rest, ctx.now, seed)
else {
continue;
};
let h = ctx
.pack
.animation(anim_name)
.and_then(|a| a.frames.first())
.map_or(12, |f| f.height);
let run_count = presence.in_flight_run_keys.len() as u32;
drawables.push(Drawable {
anchor_y: z_sort_row(Anchor::Center, pos, h),
kind: DrawableKind::GatewayMascot {
pos,
anim_name,
frame_idx,
run_count,
degraded: presence.state == pixtuoid_core::state::DaemonState::Degraded,
},
});
hover.get_or_insert(MascotFrame {
pos,
name: def.display_name,
busy: presence.state == pixtuoid_core::state::DaemonState::Busy,
degraded: presence.state == pixtuoid_core::state::DaemonState::Degraded,
active_sessions: presence.active_sessions,
});
}
hover
}
fn enqueue_meeting_furniture<'a>(layout: &'a Layout, drawables: &mut Vec<Drawable<'a>>) {
for room in &layout.meeting_furniture {
let table = room.table;
let [ts, bs] = room.sofas;
let rug_w = 18u16;
let rug_h =
bs.y.saturating_sub(ts.y)
.saturating_add(8)
.min(layout.buf_h.saturating_sub(table.y).saturating_add(8));
drawables.push(Drawable {
anchor_y: table.y.saturating_sub(rug_h / 2),
kind: DrawableKind::AreaRug {
pos: table,
width: rug_w,
height: rug_h,
},
});
}
for room in &layout.meeting_furniture {
for (i, sofa) in room.sofas.into_iter().enumerate() {
let mirrored = i % 2 != 0;
let faces_away = sofa.y >= room.table.y;
drawables.push(Drawable {
anchor_y: sofa.y + if faces_away { 3 } else { 2 },
kind: DrawableKind::MeetingSofa {
pos: sofa,
mirrored,
},
});
}
}
for room in &layout.meeting_furniture {
drawables.push(Drawable {
anchor_y: z_sort_row(
Anchor::Center,
room.table,
crate::tui::layout::furniture_def(crate::tui::layout::Furniture::MeetingTable)
.visual
.h,
),
kind: DrawableKind::MeetingTable { pos: room.table },
});
}
}
fn enqueue_lounge_pantry_appliances<'a>(layout: &'a Layout, drawables: &mut Vec<Drawable<'a>>) {
if let Some(table) = layout.pantry_table {
drawables.push(Drawable {
anchor_y: z_sort_row(
Anchor::Center,
table,
crate::tui::layout::furniture_def(crate::tui::layout::Furniture::PantryTable)
.visual
.h,
),
kind: DrawableKind::PantryTable { pos: table },
});
}
for chair in &layout.pantry_chairs {
drawables.push(Drawable {
anchor_y: z_sort_row(
Anchor::Center,
*chair,
crate::tui::layout::furniture_def(crate::tui::layout::Furniture::PantryChair)
.visual
.h,
),
kind: DrawableKind::PantryChair { pos: *chair },
});
}
if let Some(center) = layout.couch_sprite_center {
drawables.push(Drawable {
anchor_y: center.y.saturating_sub(2),
kind: DrawableKind::AreaRug {
pos: Point {
x: center.x,
y: center.y + 3,
},
width: 22,
height: 7,
},
});
drawables.push(Drawable {
anchor_y: z_sort_row(
Anchor::Center,
center,
crate::tui::layout::furniture_def(crate::tui::layout::Furniture::Couch)
.visual
.h,
),
kind: DrawableKind::WaypointCouch { pos: center },
});
if let Some(table) = layout.lounge_side_table {
drawables.push(Drawable {
anchor_y: z_sort_row(
Anchor::Center,
table,
crate::tui::layout::furniture_def(
crate::tui::layout::Furniture::LoungeSideTable,
)
.visual
.h,
),
kind: DrawableKind::LoungeSideTable { pos: table },
});
}
}
for wp in &layout.waypoints {
use crate::tui::layout::{furniture_def, WaypointKind};
let visual_h = furniture_def(wp.kind.furniture()).visual.h;
match wp.kind {
WaypointKind::Couch => {}
WaypointKind::Pantry => {
let Size { w: cw, h: ch } = layout.pantry_counter_size; drawables.push(Drawable {
anchor_y: z_sort_row(Anchor::Center, wp.pos, ch),
kind: DrawableKind::WaypointPantry {
pos: wp.pos,
use_large: cw >= 32,
},
});
}
WaypointKind::PhoneBooth | WaypointKind::StandingDesk => {}
WaypointKind::VendingMachine => {
drawables.push(Drawable {
anchor_y: z_sort_row(Anchor::Center, wp.pos, visual_h),
kind: DrawableKind::VendingMachine { pos: wp.pos },
});
}
WaypointKind::Printer => {
drawables.push(Drawable {
anchor_y: z_sort_row(Anchor::Center, wp.pos, visual_h),
kind: DrawableKind::Printer { pos: wp.pos },
});
}
WaypointKind::MeetingSofa | WaypointKind::MeetingStand => {}
}
}
}
fn enqueue_pod_decor_and_plants<'a>(layout: &'a Layout, drawables: &mut Vec<Drawable<'a>>) {
for &PodDecorItem { kind, pos } in &layout.pod_decor {
let Size { h, .. } = crate::tui::layout::furniture_def(kind.furniture()).visual;
drawables.push(Drawable {
anchor_y: z_sort_row(Anchor::Center, pos, h),
kind: DrawableKind::PodDecorItem { kind, pos },
});
}
for &PlantItem { kind, pos } in &layout.plants {
drawables.push(Drawable {
anchor_y: z_sort_row(
Anchor::Center,
pos,
crate::tui::layout::furniture_def(kind.furniture()).visual.h,
),
kind: DrawableKind::Plant { kind, pos },
});
}
}
fn enqueue_floor_fixtures<'a>(
ctx: &PixelCtx<'_>,
agents: &[AgentSlot],
drawables: &mut Vec<Drawable<'a>>,
) {
if let Some(lamp) = ctx.layout.floor_lamp {
drawables.push(Drawable {
anchor_y: lamp.y + floor_lamp_south_offset(),
kind: DrawableKind::FloorLamp { pos: lamp },
});
}
if let Some(mr) = ctx.layout.meeting_room {
if mr.width > 20 {
let cx = mr.x + mr.width - 5;
let cy = mr.y + mr.height / 2 - 4;
drawables.push(Drawable {
anchor_y: cy + 7,
kind: DrawableKind::CoatRack {
pos: Point { x: cx, y: cy },
},
});
}
}
if let Some(door_pos) = ctx.layout.door {
let frame_idx = compute_door_frame_idx(agents, ctx.now, ctx.door_anim_max_ms);
drawables.push(Drawable {
anchor_y: door_pos.y + ELEVATOR_H,
kind: DrawableKind::Door {
pos: door_pos,
frame_idx,
},
});
}
}
fn enqueue_wall_decor<'a>(layout: &'a Layout, drawables: &mut Vec<Drawable<'a>>) {
for &WallDecorItem { kind, pos } in &layout.wall_decor {
let Size { h, .. } = crate::tui::layout::furniture_def(kind.furniture()).visual;
drawables.push(Drawable {
anchor_y: z_sort_row(Anchor::TopLeft, pos, h),
kind: DrawableKind::WallDecor { kind, pos },
});
}
}
#[cfg(test)]
mod tests;