From 9fe99d4168002315814fab27e95d851fe4b60cb7 Mon Sep 17 00:00:00 2001 From: Cameron Date: Fri, 10 Jul 2026 04:52:35 +0000 Subject: [PATCH] Draw in-flight work blips crawling the wire. Demand and knowledge hops now expose Sim::work_in_flight so Bevy, terminal, and agent can show teal/bone cargo moving between nodes instead of only stacks after deposit. Defense: wiki/mechanics/machine-work.md — tokens are the flow substrate rendered; queue depth is stacks and in-flight quanta are moving blips. The Voss desktop -> switch -> host path already existed; this makes the hop visible. Co-authored-by: Cursor --- README.md | 5 +++-- src/save.rs | 2 ++ src/sim.rs | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----- src/bin/bevy.rs | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ wiki/log/2026-07-09-route-blips.md | 29 +++++++++++++++++++++++++++++ wiki/log/DEVLOG.md | 11 +++++++++++ wiki/mechanics/machine-work.md | 33 +++++++++++++++++++++------------ wiki/mechanics/sim-mechanics.md | 6 +++++- wiki/process/ROADMAP.md | 4 ++-- src/bin/terminal/agent.rs | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/bin/terminal/mod.rs | 10 ++++++++-- src/bin/terminal/ui.rs | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 12 file(s) changed, 386 insertion(s)(+), 24 deletion(s)(-) diff --git a/README.md b/README.md --- a/README.md +++ b/README.md @@ -90,8 +90,9 @@ Current open work is legibility, not another hidden substrate: the next priority is the continuous witness/narration pass so a player can explain the last thirty seconds without reading the wiki. Machine work is still IN -PROGRESS beyond its first playable slice (non-host mode production, -routes/in-flight quanta, and people-as-carriers remain ahead). B2/B3 systems +PROGRESS beyond its first playable slice (non-host mode production and +people-as-carriers remain ahead; in-flight route blips landed 2026-07-09). +B2/B3 systems like rollback, z-planes, markets, objectives, and overt containment are specified ahead, but not all live in the build yet. Under the corpus's no-dead-code rule, the earlier facility-defense prototype lives only in git diff --git a/src/save.rs b/src/save.rs --- a/src/save.rs +++ b/src/save.rs @@ -240,6 +240,8 @@ sim.badge_access = self.badge_access; sim.operations_bandwidth = self.operations_bandwidth; sim.package_cover = self.package_cover; + // In-flight blips are ephemeral render state from the last route step. + sim.last_wired_moves.clear(); sim.recompute_derived(); sim.reconcile_work_grid(); sim.recompute_senses(); diff --git a/src/sim.rs b/src/sim.rs --- a/src/sim.rs +++ b/src/sim.rs @@ -41,7 +41,7 @@ use crate::save::SaveState; use crate::schedule::Schedule; use crate::tiles::TileType; -use crate::work_grid::{MachineMode, TokenFamily, WorkGrid, WorkQueues}; +use crate::work_grid::{MachineMode, TokenFamily, TokenMove, WorkGrid, WorkQueues}; /// Default deterministic seed for a fresh run. pub const DEFAULT_SEED: u64 = 0x5EED_1234; @@ -182,6 +182,22 @@ pub queues: WorkQueues, } +/// One hop of wired cargo from the last `advance_work_grid` step. Frontends +/// interpolate a blip from `(from_x, from_y)` to `(to_x, to_y)` over the +/// wall-clock tick interval (machine-work.md: in-flight quanta as moving +/// blips). Ephemeral — not saved; empty after load until the next route. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct WorkInFlightReadout { + pub family: TokenFamily, + pub from: u32, + pub to: u32, + pub from_x: i32, + pub from_y: i32, + pub to_x: i32, + pub to_y: i32, + pub amount: f32, +} + struct MessageDraft { channel: MessageChannel, from: MessageEndpoint, @@ -279,6 +295,9 @@ /// Last known online state per machine, for machinery/anomaly recordings. last_machine_online: HashMap, + /// Wired hops from the last `advance_work_grid` call — render contract for + /// in-flight blips. Transient (not saved); cleared when nothing moved. + pub(crate) last_wired_moves: Vec, /// Per-tick day-job compute delivered by the last economy split. last_day_job_rate: f32, /// Per-tick research compute delivered by the last economy split — @@ -415,6 +434,7 @@ utterance_fired: HashMap::new(), traffic_fired: HashMap::new(), last_machine_online: HashMap::new(), + last_wired_moves: Vec::new(), last_day_job_rate: 0.0, last_research_rate: 0.0, last_schemes_rate: 0.0, @@ -2780,18 +2800,24 @@ } } let demand_sinks = self.day_job_sinks(); - let _ = self.work_grid.route_wired_to_sinks( + let mut moves = Vec::new(); + if let Ok(step) = self.work_grid.route_wired_to_sinks( TokenFamily::Demand, demand_sinks, Self::WORK_GRID_WIRED_TOKENS_PER_TICK, false, - ); - let _ = self.work_grid.route_wired_to_sinks( + ) { + moves.extend(step.moves); + } + if let Ok(step) = self.work_grid.route_wired_to_sinks( TokenFamily::Knowledge, [self.core.host_machine], Self::WORK_GRID_WIRED_TOKENS_PER_TICK, true, - ); + ) { + moves.extend(step.moves); + } + self.last_wired_moves = moves; let _ = self.work_grid.absorb_exposure( Self::CONCEALMENT_WELL_RADIUS, Self::CONCEALMENT_ABSORB_PER_TICK, @@ -2905,6 +2931,28 @@ .iter() .find(|m| m.x == x && m.y == y)?; self.work_stack_for_machine(machine.id) + } + + /// Wired hops from the last work-grid step. Frontends draw these as + /// moving blips; empty when nothing routed this tick (or after load). + pub fn work_in_flight(&self) -> Vec { + self.last_wired_moves + .iter() + .filter_map(|m| { + let from = self.work_grid.node(m.from)?; + let to = self.work_grid.node(m.to)?; + Some(WorkInFlightReadout { + family: m.family, + from: m.from, + to: m.to, + from_x: from.x, + from_y: from.y, + to_x: to.x, + to_y: to.y, + amount: m.amount, + }) + }) + .collect() } pub fn work_mode_counts(&self) -> std::collections::BTreeMap { @@ -7314,14 +7362,26 @@ // Two hops at WORK_GRID_WIRED_TOKENS_PER_TICK — wait until the inbox // has landed on Rack 3 rather than asserting the assignment tick. let mut landed = 0.0; + let mut saw_in_flight = false; for _ in 0..400 { sim.advance(); + if sim + .work_in_flight() + .iter() + .any(|b| b.family == TokenFamily::Demand && b.amount > f32::EPSILON) + { + saw_in_flight = true; + } landed = sim.work_grid.queue(host, TokenFamily::Demand); if landed > 0.5 { break; } } assert!(landed > 0.5, "demand deposits on Rack 3 via the wire"); + assert!( + saw_in_flight, + "demand hops expose WorkInFlightReadout for route blips" + ); assert_eq!(sim.work_grid.mode(host), Some(MachineMode::DayJob)); // Let the wire drain so host consumption is measurable without diff --git a/src/bin/bevy.rs b/src/bin/bevy.rs --- a/src/bin/bevy.rs +++ b/src/bin/bevy.rs @@ -823,6 +823,7 @@ flicker_feeds, blink_machines_3d, render_real_links, + render_work_routes, render_cursor, render_tokens, ), @@ -2457,6 +2458,104 @@ let iso = Isometry3d::new(c, Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)); gizmos.rect(iso, Vec2::splat(1.06), AMBER); gizmos.line(c, c + Vec3::Y * 0.9, Color::srgba(1.0, 0.69, 0.0, 0.55)); +} + +/// Wired work cargo in flight (machine-work.md): teal demand / bone knowledge +/// blips crawl the last hop over the wall-clock tick. Positions come from +/// `Sim::work_in_flight` — not frontend counters. +fn render_work_routes(game: Res, mode: Res, mut gizmos: Gizmos) { + if game.screen != Screen::Playing { + return; + } + let progress = if game.paused { + 1.0 + } else { + game.tick_timer.fraction().clamp(0.0, 1.0) + }; + let w = game.sim.map.width; + let h = game.sim.map.height; + for hop in game.sim.work_in_flight() { + if hop.amount <= f32::EPSILON { + continue; + } + let Some(((sx, sy), (ex, ey))) = + visible_wire_segment(hop.from_x, hop.from_y, hop.to_x, hop.to_y, w, h) + else { + continue; + }; + let t = progress; + let x = sx as f32 + (ex - sx) as f32 * t; + let y = sy as f32 + (ey - sy) as f32 * t; + let base = match hop.family { + TokenFamily::Demand => SIGNAL, + TokenFamily::Knowledge => BONE, + TokenFamily::Exposure => continue, + }; + let alpha = (0.35 + 0.55 * hop.amount.min(1.0)).min(0.95); + let s = base.to_srgba(); + let c = Color::srgba(s.red, s.green, s.blue, alpha); + let trail = Color::srgba(s.red, s.green, s.blue, alpha * 0.22); + if mode.material { + let p = Vec3::new(x + 0.5, 0.55, y + 0.5); + let r = 0.10 + 0.05 * hop.amount.min(1.0); + // Small cross reads as a blip without depending on sphere gizmos. + gizmos.line(p + Vec3::new(-r, 0.0, 0.0), p + Vec3::new(r, 0.0, 0.0), c); + gizmos.line(p + Vec3::new(0.0, 0.0, -r), p + Vec3::new(0.0, 0.0, r), c); + gizmos.line(p + Vec3::Y * -0.05, p + Vec3::Y * 0.12, c); + let start = Vec3::new(sx as f32 + 0.5, 0.45, sy as f32 + 0.5); + let end = Vec3::new(ex as f32 + 0.5, 0.45, ey as f32 + 0.5); + gizmos.line(start, end, trail); + } else { + let p = Vec2::new( + x * TILE_SIZE + TILE_SIZE / 2.0, + -(y * TILE_SIZE + TILE_SIZE / 2.0), + ); + let r = 3.0 + 2.0 * hop.amount.min(1.0); + gizmos.line_2d(p + Vec2::new(-r, 0.0), p + Vec2::new(r, 0.0), c); + gizmos.line_2d(p + Vec2::new(0.0, -r), p + Vec2::new(0.0, r), c); + let start = Vec2::new( + sx as f32 * TILE_SIZE + TILE_SIZE / 2.0, + -(sy as f32 * TILE_SIZE + TILE_SIZE / 2.0), + ); + let end = Vec2::new( + ex as f32 * TILE_SIZE + TILE_SIZE / 2.0, + -(ey as f32 * TILE_SIZE + TILE_SIZE / 2.0), + ); + gizmos.line_2d(start, end, trail); + } + } +} + +/// Map a WorkGrid hop onto a drawable segment. Off-map sources (Voss desktop) +/// crawl into the on-map endpoint so teal arrives via the switch. +fn visible_wire_segment( + from_x: i32, + from_y: i32, + to_x: i32, + to_y: i32, + map_w: i32, + map_h: i32, +) -> Option<((i32, i32), (i32, i32))> { + let on = |x: i32, y: i32| x >= 0 && y >= 0 && x < map_w && y < map_h; + let from_on = on(from_x, from_y); + let to_on = on(to_x, to_y); + if !from_on && !to_on { + return None; + } + if from_on && to_on { + return Some(((from_x, from_y), (to_x, to_y))); + } + if !from_on && to_on { + // Inbound from off-map: start one tile toward the source so the blip + // reads as cargo arriving through the wire rather than teleporting. + let sx = to_x + (from_x - to_x).signum(); + let sy = to_y + (from_y - to_y).signum(); + Some(((sx, sy), (to_x, to_y))) + } else { + let ex = from_x + (to_x - from_x).signum(); + let ey = from_y + (to_y - from_y).signum(); + Some(((from_x, from_y), (ex, ey))) + } } /// People in the material render: billboard visibility mirrors render_people diff --git a/wiki/log/2026-07-09-route-blips.md b/wiki/log/2026-07-09-route-blips.md new file mode 100644 --- /dev/null +++ b/wiki/log/2026-07-09-route-blips.md @@ -0,0 +1,29 @@ +# 2026-07-09 — In-flight route blips on the work wire + +``` +Type: log +``` + +## Intent + +Make wired demand (and knowledge) visible while it hops — teal crawling +the link from Voss's desktop through the switch onto Rack 3 — so the +token system sells cargo-on-the-wire, not teleporting stacks. + +## Decided / changed + +- Sim keeps the last `route_wired_to_sinks` moves as ephemeral + `last_wired_moves` (not saved). +- `Sim::work_in_flight()` is the render contract: family, endpoints, + grid positions, amount. +- Bevy interpolates a cross blip + faint trail over `tick_timer` + fraction (flat + material). Off-map sources crawl into the on-map + endpoint so inbound teal arrives via the switch. +- Terminal draws bold SIGNAL/bone `·` along the hop; agent frames snap + mid-hop as `*` / `+`. +- Specs: machine-work.md, sim-mechanics.md, ROADMAP #33, README. + +## Checks + +Day-job ingress test asserts `work_in_flight` during routing; +`./tools/check.sh` before land. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -5,6 +5,17 @@ ``` Reverse chronological implementation notes. Keep this factual: what changed, why, checks, and spec impact. +## 2026-07-09 - In-flight route blips on the work wire + +- Intent: demand/knowledge hops should read as cargo crawling the wire, + not only as stacks that appear after deposit. +- Changed: `Sim::work_in_flight` exposes last-hop `TokenMove`s as + `WorkInFlightReadout` (ephemeral); Bevy / terminal / agent draw teal + demand and bone knowledge blips over the wall-clock tick; specs and + ROADMAP updated. +- Checks: focused day-job ingress + WorkGrid tests; full ./tools/check.sh. +- Log: wiki/log/2026-07-09-route-blips.md. + ## 2026-07-09 - Matte pass: chalk surfaces, no blowout - Intent: Cameron flagged the core render as too bright/shiny; the diff --git a/wiki/mechanics/machine-work.md b/wiki/mechanics/machine-work.md --- a/wiki/mechanics/machine-work.md +++ b/wiki/mechanics/machine-work.md @@ -19,8 +19,9 @@ flat sensorium keeps the exact-count D/!/K glyphs, which are now hidden in the material render (a floating count label over real token art is the rejected world read). `MISALIGNED_SHOT=tokens` - stages game evidence via the queue API. Route/in-flight quanta and - consumption animation remain pending. + stages game evidence via the queue API. Route/in-flight quanta landed + 2026-07-09 (`Sim::work_in_flight` + Bevy/terminal/agent blips); consumption + animation remains pending. Exposure DECIDED 2026-07-09 (issue #1 fully closed): crimson particulate residue — motes, drift, footprint trails; never liquid (blood's form is reserved). Trail deposit is a new mechanic scoped with people-tokens (#35). @@ -35,9 +36,10 @@ delegation in Bevy / terminal / agent; research-delegated machines receive bone knowledge routed to the core sink. 2026-07-09 DECIDED: day-job demand ingress is Voss desktop -> switch -> host (rejected switch-as- - source and teleport-onto-Rack-3). Still pending: non-host mode - production polish, route animation/in-flight quanta, researched network- - speed curve, people-as-carriers, Schemes-as-mode decision. COORDINATION: + source and teleport-onto-Rack-3). Route animation / in-flight quanta + landed 2026-07-09. Still pending: non-host mode production polish, + researched network-speed curve, people-as-carriers, Schemes-as-mode + decision. COORDINATION: ROADMAP #25's per-machine split destination is superseded by this one-machine-one-mode substrate (#33). 2026-07-09 DECIDED: the four fleet modes are Day Job / Research / Concealment / Operations; Operations replaces @@ -230,8 +232,11 @@ research progress math is intentionally preserved until ROADMAP #33 wires non-host research production fully onto delegated machines. -Not landed yet: route animation/in-flight quanta, researched network-speed -curve, non-research mode production, and people carrying exposure. +Not landed yet: researched network-speed curve, non-research mode +production, and people carrying exposure. Route animation / in-flight +quanta landed 2026-07-09: `Sim::work_in_flight` exposes the last hop's +`TokenMove`s; Bevy / terminal / agent draw teal (demand) and bone +(knowledge) blips crawling the wire over the wall-clock tick. ## The token taxonomy — three families (DECIDED 2026-07-09) @@ -427,14 +432,17 @@ substrate routes demand/knowledge one graph step per tick, consumes at sinks, and strands piles when no path exists. Research budget now produces live knowledge queues on research-delegated machines and the - WorkGrid routes them to the core sink. The researched network-speed - curve, route animation, and broader domain consequences are pending. + WorkGrid routes them to the core sink. Route animation / in-flight + quanta landed 2026-07-09 (`Sim::work_in_flight`; Bevy/terminal/agent + blips). The researched network-speed curve and broader domain + consequences are pending. 5. Token counts/rates in the render provably equal the sim's queue depths and flow rates (one-truth test, not a parallel counter). **Partial:** `queue_snapshot()` and unit tests pin queue depths as the render contract; terminal, Bevy, and agent mode now render D/!/K stacks through sim readouts, not frontend counters. In-flight route animation - is pending. + landed 2026-07-09: `work_in_flight()` exposes the last hop; frontends + interpolate blips over the wall-clock tick (agent frames snap mid-hop). 6. Token render anatomy matches the decided family kit after Tangled issue #1 closes: cold-signal filled dockets at upper-left for demand; bone-white knowledge at upper-right using the selected liquid treatment; crimson @@ -453,8 +461,9 @@ 8. The terminal surfaces stacks, routes, and modes with full legibility parity. **Partial:** terminal and agent frames show map glyphs, inspect facts, host stack readouts, and mode verbs; mixed machines must gain `*` - map treatment plus an exact `D n / ! n / K n` focus line, and routes are - pending. + map treatment plus an exact `D n / ! n / K n` focus line. Routes: + terminal teal/bone `·` blips and agent `*`/`+` mid-hop markers landed + 2026-07-09. [TUNE] token stack visual cap, arrival schedules, consumption rates, network speed research curve, attention pile thresholds. diff --git a/wiki/mechanics/sim-mechanics.md b/wiki/mechanics/sim-mechanics.md --- a/wiki/mechanics/sim-mechanics.md +++ b/wiki/mechanics/sim-mechanics.md @@ -171,7 +171,11 @@ per tick [TUNE] (`CONCEALMENT_WELL_RADIUS`, `CONCEALMENT_ABSORB_PER_TICK`). - Terminal, Bevy, and agent mode render stack glyphs/readouts from sim - readouts (`WorkStackReadout`), not frontend counters. + readouts (`WorkStackReadout`), not frontend counters. In-flight wired + hops from the last `advance_work_grid` step are exposed as + `WorkInFlightReadout` via `Sim::work_in_flight` (ephemeral — not saved). + Frontends interpolate a teal (demand) or bone (knowledge) blip along + each hop over the wall-clock tick; agent frames snap to mid-hop. ## Research: self-modification (wiki/mechanics/research.md) diff --git a/wiki/process/ROADMAP.md b/wiki/process/ROADMAP.md --- a/wiki/process/ROADMAP.md +++ b/wiki/process/ROADMAP.md @@ -520,8 +520,8 @@ agent mode). 2026-07-09: demand ingress path DECIDED (Voss desktop -> switch -> host). - **Next:** wire non-host machine modes into research/concealment/operations - production; render actual routes/in-flight quanta (the demand path now - exists for that animation). The target four-mode fleet is DECIDED: Day Job / + production. In-flight route blips landed 2026-07-09 (`Sim::work_in_flight`). + The target four-mode fleet is DECIDED: Day Job / Research / Concealment / Operations. Runtime, saves, and frontends now use Operations. **Operations job model DECIDED 2026-07-09 (Tangled issue #3):** player-authored work is cold-signal Demand consumed by Operations machines diff --git a/src/bin/terminal/agent.rs b/src/bin/terminal/agent.rs --- a/src/bin/terminal/agent.rs +++ b/src/bin/terminal/agent.rs @@ -1329,6 +1329,39 @@ } } + // In-flight wired cargo: snap to the hop midpoint so agent frames show + // the wire without wall-clock interpolation (machine-work.md). + for hop in sim.work_in_flight() { + if hop.amount <= f32::EPSILON { + continue; + } + let Some(((sx, sy), (ex, ey))) = visible_wire_segment( + hop.from_x, + hop.from_y, + hop.to_x, + hop.to_y, + sim.map.width, + sim.map.height, + ) else { + continue; + }; + let bx = (sx + ex) / 2; + let by = (sy + ey) / 2; + if bx >= origin_x + && bx < origin_x + view_w_i + && by >= origin_y + && by < origin_y + view_h_i + && (bx, by) != cursor + { + let glyph = match hop.family { + misaligned::work_grid::TokenFamily::Demand => '*', + misaligned::work_grid::TokenFamily::Knowledge => '+', + misaligned::work_grid::TokenFamily::Exposure => continue, + }; + grid[(by - origin_y) as usize][(bx - origin_x) as usize] = glyph; + } + } + grid[(cursor.1 - origin_y) as usize][(cursor.0 - origin_x) as usize] = '@'; grid.into_iter() @@ -1345,6 +1378,34 @@ Some('K') } else { None + } +} + +fn visible_wire_segment( + from_x: i32, + from_y: i32, + to_x: i32, + to_y: i32, + map_w: i32, + map_h: i32, +) -> Option<((i32, i32), (i32, i32))> { + let on = |x: i32, y: i32| x >= 0 && y >= 0 && x < map_w && y < map_h; + let from_on = on(from_x, from_y); + let to_on = on(to_x, to_y); + if !from_on && !to_on { + return None; + } + if from_on && to_on { + return Some(((from_x, from_y), (to_x, to_y))); + } + if !from_on && to_on { + let sx = to_x + (from_x - to_x).signum(); + let sy = to_y + (from_y - to_y).signum(); + Some(((sx, sy), (to_x, to_y))) + } else { + let ex = from_x + (to_x - from_x).signum(); + let ey = from_y + (to_y - from_y).signum(); + Some(((from_x, from_y), (ex, ey))) } } diff --git a/src/bin/terminal/mod.rs b/src/bin/terminal/mod.rs --- a/src/bin/terminal/mod.rs +++ b/src/bin/terminal/mod.rs @@ -357,8 +357,14 @@ self.ui.render_title_screen(stdout)?; } Screen::Playing => { + let progress = if self.paused { + 1.0 + } else { + (self.last_tick.elapsed().as_millis() as f32 / self.tick_ms as f32) + .clamp(0.0, 1.0) + }; self.ui - .render_map(stdout, &self.sim, self.cursor_x, self.cursor_y)?; + .render_map(stdout, &self.sim, self.cursor_x, self.cursor_y, progress)?; self.ui.render_sidebar( stdout, &self.sim, @@ -389,7 +395,7 @@ } Screen::GameOver => { self.ui - .render_map(stdout, &self.sim, self.cursor_x, self.cursor_y)?; + .render_map(stdout, &self.sim, self.cursor_x, self.cursor_y, 1.0)?; let reason = self .sim .game_over_reason diff --git a/src/bin/terminal/ui.rs b/src/bin/terminal/ui.rs --- a/src/bin/terminal/ui.rs +++ b/src/bin/terminal/ui.rs @@ -13,6 +13,7 @@ use misaligned::detection::{Band, SignatureKind}; use misaligned::sim::{FactSource, Fog, LogEvent, Nudge, Sim, TraceDebtStatus, WorkStackReadout}; use misaligned::tiles::TileType; +use misaligned::work_grid::TokenFamily; use std::io::Stdout; const SIDEBAR_W: i32 = 34; @@ -81,6 +82,12 @@ r: 140, g: 34, b: 34, + }; + /// Cold server-light — demand / live wire cargo (machine-work.md). + pub const SIGNAL: Color = Color::Rgb { + r: 92, + g: 209, + b: 199, }; } @@ -406,12 +413,43 @@ } } + /// Map a WorkGrid hop onto a drawable segment. Off-map sources crawl into + /// the on-map endpoint so teal arrives via the switch. + fn visible_wire_segment( + from_x: i32, + from_y: i32, + to_x: i32, + to_y: i32, + map_w: i32, + map_h: i32, + ) -> Option<((i32, i32), (i32, i32))> { + let on = |x: i32, y: i32| x >= 0 && y >= 0 && x < map_w && y < map_h; + let from_on = on(from_x, from_y); + let to_on = on(to_x, to_y); + if !from_on && !to_on { + return None; + } + if from_on && to_on { + return Some(((from_x, from_y), (to_x, to_y))); + } + if !from_on && to_on { + let sx = to_x + (from_x - to_x).signum(); + let sy = to_y + (from_y - to_y).signum(); + Some(((sx, sy), (to_x, to_y))) + } else { + let ex = from_x + (to_x - from_x).signum(); + let ey = from_y + (to_y - from_y).signum(); + Some(((from_x, from_y), (ex, ey))) + } + } + pub fn render_map( &mut self, stdout: &mut Stdout, sim: &Sim, cursor_x: i32, cursor_y: i32, + tick_progress: f32, ) -> std::io::Result<()> { let (max_x, max_y) = terminal::size()?; let view_w = (max_x as i32 - SIDEBAR_W - 1).min(sim.map.width); @@ -530,6 +568,48 @@ Attribute::Bold, )?; } + } + + // In-flight wired cargo (machine-work.md): crawl the last hop over + // the wall-clock tick. Demand is teal `·`; knowledge is bone `·`. + let t = tick_progress.clamp(0.0, 1.0); + for hop in sim.work_in_flight() { + if hop.amount <= f32::EPSILON { + continue; + } + let Some(((sx, sy), (ex, ey))) = Self::visible_wire_segment( + hop.from_x, + hop.from_y, + hop.to_x, + hop.to_y, + sim.map.width, + sim.map.height, + ) else { + continue; + }; + let bx = (sx as f32 + (ex - sx) as f32 * t).round() as i32; + let by = (sy as f32 + (ey - sy) as f32 * t).round() as i32; + if bx < origin_x || bx >= origin_x + view_w || by < origin_y || by >= origin_y + view_h + { + continue; + } + // Don't cover the cursor or a parked stack glyph on the endpoint. + if (bx, by) == (cursor_x, cursor_y) { + continue; + } + let color = match hop.family { + TokenFamily::Demand => pal::SIGNAL, + TokenFamily::Knowledge => pal::TEXT, + TokenFamily::Exposure => continue, + }; + put_attr( + stdout, + (bx - origin_x) as u16, + (by - origin_y) as u16, + "·", + color, + Attribute::Bold, + )?; } // The cursor: attention, not an avatar. It is always visible and -- tangled.sh