From f38177ed4b7d9b905228acf2e85adca07621829c Mon Sep 17 00:00:00 2001 From: Cameron Date: Thu, 9 Jul 2026 16:36:20 -0700 Subject: [PATCH] Prototype work-token anchor art in the asset tester MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit spawn_token_anchors in the shared chassis library: demand as rigid clipped cold-signal dockets stacked at the machine's upper-left (literal count to 8, then a compressed overflow block), knowledge as ivory mercury at the upper-right (countable viscous slugs while low, a taut suspended pool whose volume is the queue amount as it grows). Tester gains T/U load cycling and tokens/tokens_max shot kinds. Exposure art is deliberately absent (HOLD, Tangled issue #1). No sim change; game wiring waits on Cameron's look call. Defense: renders the token grammar decided 2026-07-09 in machine-work.md (docket anatomy, fixed anchors, ivory-mercury knowledge) exactly as the constitution's token-anatomy decision describes — paused geometry carries amount, families differ in form because their physics differ, amber is never cargo. Frontend-only: the tester slice explicitly sanctioned by ROADMAP #33 without the sim+save conflict slot. --- src/bin/assets/mod.rs | 120 ++++++++++++++-- src/bin/assets/rack.rs | 151 ++++++++++++++++++++ wiki/art/asset-tester.md | 24 +++- wiki/log/2026-07-09-token-anchors-tester.md | 45 ++++++ wiki/log/DEVLOG.md | 13 ++ wiki/mechanics/machine-work.md | 4 + wiki/process/ROADMAP.md | 6 +- 7 files changed, 345 insertions(+), 18 deletions(-) create mode 100644 wiki/log/2026-07-09-token-anchors-tester.md diff --git a/src/bin/assets/mod.rs b/src/bin/assets/mod.rs index 7c69f26e..30f742cb 100644 --- a/src/bin/assets/mod.rs +++ b/src/bin/assets/mod.rs @@ -19,7 +19,39 @@ use bevy::render::view::screenshot::{Screenshot, save_to_disk}; use bevy::window::{CursorGrabMode, CursorOptions, WindowResolution}; use palette::{GUNMETAL, NEAR_BLACK, PORCELAIN}; -use rack::{Blink, ClaimStyle, RackMode, RackRoot, RackState, spawn_server_rack}; +use rack::{ + Blink, ClaimStyle, RackMode, RackRoot, RackState, spawn_server_rack, spawn_token_anchors, +}; + +/// Work-token load staged on the single rack (machine-work.md anchors): +/// demand dockets upper-left, ivory-mercury knowledge upper-right. +/// Exposure is HOLD (Tangled issue #1) and has no tester art yet. +#[derive(Debug, Clone, Copy, PartialEq, Resource, Default)] +struct TokenLoad { + demand: usize, + knowledge: f32, +} + +impl TokenLoad { + const DEMAND_STEPS: [usize; 4] = [0, 2, 5, 9]; + const KNOWLEDGE_STEPS: [f32; 5] = [0.0, 1.0, 2.0, 4.0, 8.0]; + + fn next_demand(&mut self) { + let i = Self::DEMAND_STEPS + .iter() + .position(|&d| d == self.demand) + .unwrap_or(0); + self.demand = Self::DEMAND_STEPS[(i + 1) % Self::DEMAND_STEPS.len()]; + } + + fn next_knowledge(&mut self) { + let i = Self::KNOWLEDGE_STEPS + .iter() + .position(|&k| k == self.knowledge) + .unwrap_or(0); + self.knowledge = Self::KNOWLEDGE_STEPS[(i + 1) % Self::KNOWLEDGE_STEPS.len()]; + } +} /// Single asset on the pedestal, all five states side by side, or all /// four machine modes side by side (owned-busy racks). @@ -50,16 +82,40 @@ struct ShotHarness { splash: bool, } -fn scene_from_shot(kind: &str) -> (ViewMode, RackState, RackMode, ClaimStyle, bool) { +fn scene_from_shot(kind: &str) -> (ViewMode, RackState, RackMode, ClaimStyle, TokenLoad, bool) { if kind == "splash" { return ( ViewMode::Single, RackState::Idle, RackMode::DayJob, ClaimStyle::Ring, + TokenLoad::default(), true, ); } + // Token-anchor evidence: an owned-busy day-job rack under a mid or + // overflow work load (machine-work.md docket + ivory mercury). + if kind == "tokens" || kind == "tokens_max" { + let load = if kind == "tokens_max" { + TokenLoad { + demand: 9, + knowledge: 8.0, + } + } else { + TokenLoad { + demand: 5, + knowledge: 4.0, + } + }; + return ( + ViewMode::Single, + RackState::Busy, + RackMode::DayJob, + ClaimStyle::Ring, + load, + false, + ); + } let (head, style_tag) = match kind.split_once('_') { Some((h, s)) => (h, Some(s)), None => (kind, None), @@ -83,7 +139,7 @@ fn scene_from_shot(kind: &str) -> (ViewMode, RackState, RackMode, ClaimStyle, bo // `idle` plus legacy `rack`/`powered` and anything unrecognized. _ => (ViewMode::Single, RackState::Idle, RackMode::DayJob), }; - (view, state, mode, style, false) + (view, state, mode, style, TokenLoad::default(), false) } fn main() { @@ -92,9 +148,9 @@ fn main() { let harness = std::env::var("MISALIGNED_SHOT").ok().map(|kind| { let path = std::env::var("MISALIGNED_SHOT_PATH") .unwrap_or_else(|_| format!("misaligned_shot_{kind}.png")); - let (view, state, rack_mode, style, splash) = scene_from_shot(&kind); + let (view, state, rack_mode, style, tokens, splash) = scene_from_shot(&kind); ( - (view, state, rack_mode, style), + (view, state, rack_mode, style, tokens), ShotHarness { path, frames: 150, @@ -104,12 +160,13 @@ fn main() { ) }); let splash = harness.as_ref().map(|(_, h)| h.splash).unwrap_or(false); - let (initial_mode, initial_state, initial_rack_mode, initial_style) = + let (initial_mode, initial_state, initial_rack_mode, initial_style, initial_tokens) = harness.as_ref().map(|(scene, _)| *scene).unwrap_or(( ViewMode::Single, RackState::Idle, RackMode::DayJob, ClaimStyle::Ladder, + TokenLoad::default(), )); let clear = if splash { @@ -148,6 +205,7 @@ fn main() { .insert_resource(ambient) .insert_resource(initial_state) .insert_resource(initial_rack_mode) + .insert_resource(initial_tokens) .insert_resource(initial_style) .insert_resource(initial_mode) .insert_resource(if splash { @@ -323,12 +381,20 @@ fn setup(mut commands: Commands, orbit: Res, harness: Option String { +fn hud_string( + view: ViewMode, + state: RackState, + rack_mode: RackMode, + style: ClaimStyle, + tokens: TokenLoad, +) -> String { let line2 = match view { ViewMode::Single => format!( - "state: {} (1 dead 2 foreign 3 idle 4 busy 5 core) mode: {} (M cycles)", + "state: {} (1 dead 2 foreign 3 idle 4 busy 5 core) mode: {} (M cycles)\ntokens: D{} K{:.0} (T demand U knowledge)", state.label(), - rack_mode.label() + rack_mode.label(), + tokens.demand, + tokens.knowledge ), ViewMode::Lineup => { "lineup: dead | foreign | idle | busy | core (left to right)".to_string() @@ -367,6 +433,7 @@ fn handle_state_keys( kb: Res>, mut state: ResMut, mut rack_mode: ResMut, + mut tokens: ResMut, mut style: ResMut, mut mode: ResMut, ) { @@ -401,6 +468,12 @@ fn handle_state_keys( if kb.just_pressed(KeyCode::KeyM) { *rack_mode = rack_mode.next(); } + if kb.just_pressed(KeyCode::KeyT) { + tokens.next_demand(); + } + if kb.just_pressed(KeyCode::KeyU) { + tokens.next_knowledge(); + } if kb.just_pressed(KeyCode::KeyL) { *mode = match *mode { ViewMode::Lineup => ViewMode::Single, @@ -458,13 +531,19 @@ fn rebuild_scene( mut materials: ResMut>, state: Res, rack_mode: Res, + tokens: Res, style: Res, mode: Res, racks: Query>, pedestals: Query>, harness: Option>, ) { - if !(state.is_changed() || rack_mode.is_changed() || style.is_changed() || mode.is_changed()) { + if !(state.is_changed() + || rack_mode.is_changed() + || tokens.is_changed() + || style.is_changed() + || mode.is_changed()) + { return; } for e in racks.iter().chain(pedestals.iter()) { @@ -536,6 +615,17 @@ fn rebuild_scene( Vec3::ZERO, Some(pedestal), ); + if tokens.demand > 0 || tokens.knowledge > 0.0 { + spawn_token_anchors( + &mut commands, + &mut meshes, + &mut materials, + tokens.demand, + tokens.knowledge, + Vec3::ZERO, + Some(pedestal), + ); + } } ViewMode::Lineup => { let n = RackState::ALL.len(); @@ -592,15 +682,21 @@ fn blink_emissives( fn update_hud( state: Res, rack_mode: Res, + tokens: Res, style: Res, mode: Res, mut q: Query<&mut Text, With>, ) { - if !(state.is_changed() || rack_mode.is_changed() || style.is_changed() || mode.is_changed()) { + if !(state.is_changed() + || rack_mode.is_changed() + || tokens.is_changed() + || style.is_changed() + || mode.is_changed()) + { return; } if let Ok(mut text) = q.single_mut() { - *text = Text::new(hud_string(*mode, *state, *rack_mode, *style)); + *text = Text::new(hud_string(*mode, *state, *rack_mode, *style, *tokens)); } } diff --git a/src/bin/assets/rack.rs b/src/bin/assets/rack.rs index 59432be2..62e5a1d1 100644 --- a/src/bin/assets/rack.rs +++ b/src/bin/assets/rack.rs @@ -914,6 +914,157 @@ pub fn spawn_switch_chassis( root } +// ─── Work-token anchors (machine-work.md, DECIDED 2026-07-09) ──────────────── + +/// Demand docket footprint. +const DOCKET_W: f32 = 0.20; +const DOCKET_H: f32 = 0.125; +const DOCKET_T: f32 = 0.022; +/// Dockets drawn literally before the stack compresses (overflow). +const DOCKET_MAX: usize = 8; + +/// Spawn the work-token anchors beside a rack at `origin`: demand +/// dockets stacked at the machine's upper-left (rigid clipped +/// rectangles, the cold-signal family — imposed work stacks like work +/// orders), and ivory-mercury knowledge at the upper-right (opaque +/// bone liquid with a restrained self-glow; discrete viscous slugs +/// while low, a taut suspended pool as volume grows — queue amount is +/// pool volume). Exposure is deliberately absent: its surface +/// treatment is HOLD under Tangled issue #1. A paused frame carries +/// amount through geometry alone; animation, when it arrives, will +/// carry rate. Returns the anchor root (a `RackRoot`, so viewers +/// rebuild it with the chassis). +pub fn spawn_token_anchors( + commands: &mut Commands, + meshes: &mut Assets, + materials: &mut Assets, + demand: usize, + knowledge: f32, + origin: Vec3, + parent: Option, +) -> Entity { + let root = { + let mut e = commands.spawn(( + RackRoot, + Transform::from_translation(origin), + Visibility::default(), + Name::new(format!("token_anchors_d{demand}_k{knowledge:.1}")), + )); + if let Some(p) = parent { + e.insert(ChildOf(p)); + } + e.id() + }; + + let half_w = RACK_W * 0.5; + let front_z = RACK_D * 0.5; + + // Demand: the inbox at the upper-left. New demand snaps on top; + // consumption pulls the bottom docket down into the chassis. + if demand > 0 { + let docket = materials.add(StandardMaterial { + base_color: SIGNAL, + emissive: SIGNAL.to_linear() * 0.9, + perceptual_roughness: 0.45, + metallic: 0.05, + ..default() + }); + let x = -(half_w + 0.17); + let z = front_z - 0.08; + let y0 = RACK_H * 0.58; + let pitch = 0.16; + let shown = demand.min(DOCKET_MAX); + for i in 0..shown { + let y = y0 + pitch * i as f32; + // Clipped rectangle: a full-width lower slab plus a + // shorter, left-aligned upper slab — the top-right corner + // reads cut. Rigid, filled, axis-aligned. + spawn_box( + commands, + meshes, + docket.clone(), + root, + Vec3::new(DOCKET_W, DOCKET_H * 0.68, DOCKET_T), + Vec3::new(x, y, z), + ); + spawn_box( + commands, + meshes, + docket.clone(), + root, + Vec3::new(DOCKET_W * 0.72, DOCKET_H * 0.32, DOCKET_T), + Vec3::new(x - DOCKET_W * 0.14, y + DOCKET_H * 0.5, z), + ); + } + if demand > DOCKET_MAX { + // Overflow: the stack top compresses into one dense block — + // visibly distinct from any literal count without + // one-shape-per-unit rendering. + spawn_box( + commands, + meshes, + docket, + root, + Vec3::new(DOCKET_W * 1.08, DOCKET_H * 1.5, DOCKET_T * 1.6), + Vec3::new(x, y0 + pitch * shown as f32 + 0.10, z), + ); + } + } + + // Knowledge: ivory mercury at the upper-right. Opaque, softly + // luminous, and it casts no scene light. + if knowledge > 0.05 { + let mercury = materials.add(StandardMaterial { + base_color: scaled(BONE, 1.05), + emissive: BONE.to_linear() * 0.55, + perceptual_roughness: 0.22, + metallic: 0.0, + ..default() + }); + let x = half_w + 0.19; + let z = front_z - 0.08; + let y = RACK_H * 0.66; + if knowledge <= 2.5 { + // Discrete viscous slugs — quanta remain countable. + let n = (knowledge.ceil() as usize).max(1); + for i in 0..n { + let s = 0.052 + 0.008 * (i % 2) as f32; + commands.spawn(( + Mesh3d(meshes.add(Sphere::new(s))), + MeshMaterial3d(mercury.clone()), + Transform::from_translation(Vec3::new( + x + if i % 2 == 0 { -0.02 } else { 0.035 }, + y + 0.11 * i as f32, + z, + )) + .with_scale(Vec3::new(1.0, 0.55, 0.8)), + ChildOf(root), + )); + } + } else { + // The taut suspended pool: queue amount is pool volume. + let r = (0.055 + 0.048 * knowledge.sqrt()).min(0.26); + commands.spawn(( + Mesh3d(meshes.add(Sphere::new(r))), + MeshMaterial3d(mercury.clone()), + Transform::from_translation(Vec3::new(x, y, z)) + .with_scale(Vec3::new(1.0, 0.42, 0.78)), + ChildOf(root), + )); + // One trailing slug keeps the material read: it drips, it + // does not tile. + commands.spawn(( + Mesh3d(meshes.add(Sphere::new(0.045))), + MeshMaterial3d(mercury), + Transform::from_translation(Vec3::new(x - r * 0.9, y - 0.10, z)) + .with_scale(Vec3::new(1.0, 0.55, 0.8)), + ChildOf(root), + )); + } + } + root +} + // ─── The material law, pinned ──────────────────────────────────────────────── #[cfg(test)] diff --git a/wiki/art/asset-tester.md b/wiki/art/asset-tester.md index 813d6163..e6b27a9d 100644 --- a/wiki/art/asset-tester.md +++ b/wiki/art/asset-tester.md @@ -26,11 +26,13 @@ pattern as `misaligned-bevy`). Values: a state `dead` / `foreign` / `idle` `operations` (an owned-busy rack in that mode) — each optionally suffixed with a claim style `_ladder` / `_ring` / `_wash` (e.g. `core_ring`) — `lineup[_