返回 CodeWhale
pet_cameo.rs
根目录 / crates / tui / src / tui / ambient_life / pet_cameo.rs
1 //! Replaces the `≈≈>` cameo with the shared dot-whale widget. The existing
2 //! ocean completion clock is the only trigger. This is an authored completion
3 //! gesture, not an event-v1 bucketer or an estimate of hidden agent activity.
4
5 use super::pet_sim::{ChannelId, PetSim, PetState};
6 use super::pet_widget::PetWidget;
7 use super::{AmbientActivity, AmbientFrameStats, WhaleCameo, is_open_water, ocean};
8 use ratatui::{buffer::Buffer, layout::Rect, style::Color, text::Line, widgets::Widget};
9 use std::sync::LazyLock;
10
11 const WIDTH: u16 = 18;
12 const HEIGHT: u16 = 6;
13 pub(super) const DURATION_MS: u128 = 2_400;
14 const FPS: u128 = 30;
15 const FRAMES: usize = (DURATION_MS * FPS / 1_000) as usize;
16 #[cfg(test)]
17 pub(super) const MAX_MARKS: u32 = 3 * WIDTH as u32 * HEIGHT as u32;
18
19 struct CameoFrame {
20 buffer: Buffer,
21 marks: u32,
22 }
23
24 // A bounded raster cache, not mutable simulation history. Sampling backwards,
25 // skipping draws, or rendering another session cannot change a frame. Two
26 // fixed 72-frame tapes; no RNG beyond the core's existing particle stream.
27 static SOLO: LazyLock<Vec<CameoFrame>> = LazyLock::new(|| frames(ChannelId::Other));
28 static POD: LazyLock<Vec<CameoFrame>> = LazyLock::new(|| frames(ChannelId::Agent));
29
30 fn frames(channel: ChannelId) -> Vec<CameoFrame> {
31 let mut sim = PetSim::whale();
32 // A completion is observed, but it does not assert a model/tool category.
33 // The pod has three persistent slots here; it is not three more swarms.
34 let state = PetState {
35 channel,
36 activity: 0.12,
37 coherence: 0.95,
38 ..PetState::rest()
39 };
40 (0..FRAMES)
41 .map(|_| {
42 sim.step(1.0 / FPS as f64, &state, true, 1.0);
43 let mut buffer = Buffer::empty(Rect::new(0, 0, WIDTH, HEIGHT));
44 PetWidget {
45 sim: &sim,
46 state: &state,
47 }
48 .render(buffer.area, &mut buffer);
49 let marks = buffer.content.iter().filter(|c| c.symbol() != " ").count() as u32;
50 CameoFrame { buffer, marks }
51 })
52 .collect()
53 }
54
55 #[allow(clippy::too_many_arguments)]
56 pub(super) fn paint(
57 area: Rect,
58 buf: &mut Buffer,
59 ink: Color,
60 lines: &[Line<'_>],
61 presence: f32,
62 whale: WhaleCameo,
63 activity: AmbientActivity,
64 stats: &mut AmbientFrameStats,
65 ) {
66 let Some(age) = whale.elapsed_ms.filter(|age| *age < DURATION_MS) else {
67 return;
68 };
69 if presence <= 0.0 {
70 return;
71 }
72 let pod = activity == AmbientActivity::Subagents;
73 let tape = if pod { &*POD } else { &*SOLO };
74 let slots: &[i32] = if pod { &[-1, 0, 1] } else { &[0] };
75 for (index, slot) in slots.iter().enumerate() {
76 let age = age + index as u128 * 240;
77 if age >= DURATION_MS {
78 continue;
79 }
80 let frame = &tape[(age * FPS / 1_000) as usize];
81 let progress = age as f64 / DURATION_MS as f64;
82 let x = i32::from(whale.anchor_x) - i32::from(area.x) - i32::from(WIDTH / 2)
83 + slot * i32::from(WIDTH)
84 + (progress * 3.0).round() as i32;
85 let y = i32::from(whale.anchor_y) - i32::from(area.y) - i32::from(HEIGHT / 2);
86 stats.marks_built += frame.marks;
87 // Do not clamp off-screen pod members into one overlapping creature.
88 if x < 0
89 || y < 0
90 || x + i32::from(WIDTH) > i32::from(area.width)
91 || y + i32::from(HEIGHT) > i32::from(area.height)
92 {
93 stats.marks_clipped += frame.marks;
94 continue;
95 }
96 let (x, y) = (x as u16, y as u16);
97 let marks = || {
98 frame
99 .buffer
100 .content
101 .iter()
102 .enumerate()
103 .filter(|(_, c)| c.symbol() != " ")
104 };
105 // Atomically withhold body and label. A word touching one fluke must
106 // never leave a severed animal or an unsupported label in the water.
107 if marks()
108 .any(|(i, _)| !is_open_water(lines, x + i as u16 % WIDTH, y + i as u16 / WIDTH, 1))
109 {
110 stats.marks_skipped_text += frame.marks;
111 continue;
112 }
113 let glow = (0.65 + 0.35 * (progress * std::f64::consts::PI).sin()) as f32;
114 for (i, source) in marks() {
115 let point = (area.x + x + i as u16 % WIDTH, area.y + y + i as u16 / WIDTH);
116 let Some(cell) = buf.cell_mut(point) else {
117 stats.marks_clipped += 1;
118 continue;
119 };
120 // The existing theme owns all habitat ink. No category colour,
121 // especially Failure red, is introduced into completion chrome.
122 // Ambient ink is already calibrated against the ocean. Applying
123 // the standalone canvas alpha again would dim it twice.
124 let alpha = (glow * presence).clamp(0.0, 1.0);
125 let fg = ocean::mix_colors(cell.bg, ink, alpha);
126 cell.set_symbol(source.symbol()).set_fg(fg);
127 stats.marks_painted += 1;
128 stats.cells_written += 1;
129 }
130 }
131 }
132
132 lines RUST