diff --git a/src/bin/bevy.rs b/src/bin/bevy.rs index 1fb19f54..5de270da 100644 --- a/src/bin/bevy.rs +++ b/src/bin/bevy.rs @@ -349,7 +349,7 @@ impl Default for RenderMode { } /// Dev screenshot harness (env `MISALIGNED_SHOT=flat|opening|wide|close|dark| -/// zoomin|zoomout|intel|tokens|ears|worklight|worklightoff`, path via +/// zoomin|zoomout|intel|tokens|ears|eyes-white|eyes-form|worklight|worklightoff`, path via /// `MISALIGNED_SHOT_PATH`): stages a scenario, /// waits for /// assets, runs the fog audit, saves one screenshot, exits. Not a player @@ -467,6 +467,50 @@ struct WakeColumn { base: f32, } +/// The first sight feed changes the presence beam into a physical chassis: +/// a white column briefly swallows the amber source, collapses to its strip, +/// and leaves the server's volume visible. Frontend-only — sight remains a +/// sim fact, this is only its material-render payoff. +#[derive(Resource)] +struct EyesReveal { + /// Sight state last observed from the sim; detects the first Eyes beat. + had_sight: bool, + /// Reveal elapsed time. `None` means either not earned or already over. + t: Option, + /// Harness stages hold a composition for a deterministic PNG. + frozen: bool, + /// The first acquisition gets one payoff; losing/reacquiring a feed does + /// not replay the opening body reveal. + completed: bool, +} + +/// White matter of the Eyes reveal, deliberately opaque because the macOS +/// screenshot path cannot rely on alpha/blend bloom passes. +#[derive(Component)] +struct EyesColumn; + +/// The host's pre-Eyes body: a beam is the only pictured fact until a camera +/// earns the physical chassis. +#[derive(Component)] +struct PresenceBeam; + +type EyesColumnQuery<'w, 's> = Query< + 'w, + 's, + (&'static mut Transform, &'static mut Visibility), + (With, Without, Without), +>; +type PresenceBeamQuery<'w, 's> = + Query<'w, 's, &'static mut Visibility, (With, Without)>; + +const EYES_REVEAL_LEN: f32 = 0.95; +const EYES_WHITE_HOLD: f32 = 0.20; + +/// 0 is a full white source; 1 is the source contracted to the chassis strip. +fn eyes_reveal_collapse(t: f32) -> f32 { + ((t - EYES_WHITE_HOLD) / (EYES_REVEAL_LEN - EYES_WHITE_HOLD)).clamp(0.0, 1.0) +} + /// A rung of the core strip's linear fixture; `base` is its resting /// intensity (the wake flickers it, then restores it). #[derive(Component)] @@ -1040,6 +1084,13 @@ type LogRowQuery<'w, 's> = Query< fn main() { let mut game = Game::new(); let mut mode = RenderMode::default(); + let eyes_stage = std::env::var("MISALIGNED_SHOT") + .ok() + .and_then(|kind| match kind.as_str() { + "eyes-white" => Some((0.10, true)), + "eyes-form" => Some((0.55, true)), + _ => None, + }); // Dev screenshot harness: stage a deterministic scenario and capture one // PNG (see ShotHarness). Never active in normal play. let harness = std::env::var("MISALIGNED_SHOT").ok().map(|kind| { @@ -1087,6 +1138,16 @@ fn main() { frozen: false, }, }; + let has_sight = game.sim.reach.player_sight().next().is_some(); + let eyes_reveal = EyesReveal { + // Harness eyes shots begin after sight has been staged but retain an + // explicit frozen reveal composition. Ordinary pre-sighted harnesses + // start completed so their settled renderer shots stay canonical. + had_sight: has_sight, + t: eyes_stage.map(|(t, _)| t), + frozen: eyes_stage.is_some_and(|(_, frozen)| frozen), + completed: has_sight && eyes_stage.is_none(), + }; let mut app = App::new(); app.add_plugins( @@ -1120,6 +1181,7 @@ fn main() { .insert_resource(Materials3d::default()) .insert_resource(Machines3d::default()) .insert_resource(wake) + .insert_resource(eyes_reveal) .insert_resource(BuildBeam::default()) .init_resource::() .init_gizmo_group::() @@ -1163,6 +1225,12 @@ fn main() { ), ) .chain(), + ) + .add_systems( + Update, + reveal_first_eyes + .after(wake_sequence) + .before(update_camera_real), ); if let Some(harness) = harness { app.insert_resource(harness); @@ -1180,10 +1248,10 @@ fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &str) { let core = game.sim.core_position(); game.set_cursor(core.0, core.1); // The opening frame (material-dark-frame.md criterion 1): a fresh run's - // tick-zero material view — no scan, no splice. The core rack and its - // floor pool dominate in darkness; blueprint mass is absent, so no room - // slab, no floor grid, no blueprint silhouette, and no ruled amber frame - // appear anywhere in the frame. + // tick-zero material view — no scan, no splice. Only the host presence + // beam dominates in darkness; blueprint mass is absent, so no chassis, + // room slab, floor grid, blueprint silhouette, or ruled amber frame + // appears anywhere in the frame. if kind == "opening" { mode.material = true; mode.zoom = 1.0; @@ -1284,6 +1352,17 @@ fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &str) { game.set_cursor(x, y); } } + // First Eyes payoff (opening.md): freeze the two material frames that + // turn the amber presence beam white, then let chassis volume resolve + // out of the white source. These are visual evidence only; the sim + // reaches sight through the same real splice as every other full-view + // harness kind. + "eyes-white" | "eyes-form" => { + mode.material = true; + mode.zoom = 1.0; + let core = game.sim.core_position(); + game.set_cursor(core.0, core.1); + } // The dev work light pair (material-dark-frame.md criterion 4): // `worklight` floods the material scene flat and neutral; its // off-shot `worklightoff` frames identically under the dark rig, so @@ -1435,8 +1514,9 @@ fn fog_audit_3d( } } // Computer-visual-language criteria 2b/3: a visible chassis mesh is - // backed by sight or intel (unknown = absent), and exactly one core - // chassis exists in the world render. + // backed by sight or intel (unknown = absent). Before Eyes, the host's + // presence beam deliberately replaces the pictured core; after Eyes, + // exactly one physical core chassis exists. let mut cores = 0usize; for (m, vis) in machines.iter() { if !vis.get() { @@ -1454,9 +1534,10 @@ fn fog_audit_3d( )); } } - if cores != 1 { + let expected_cores = usize::from(game.sim.reach.player_sight().next().is_some()); + if cores != expected_cores { violations.push(format!( - "{cores} core chassis visible; the world has exactly one core" + "{cores} core chassis visible; expected {expected_cores} for the current Eyes state" )); } // Flat-materials criterion 1: the world material set is solid color — @@ -1863,8 +1944,15 @@ fn chassis_visual(game: &Game, x: i32, y: i32, tile: TileType) -> ChassisVisual }); let tier = if machine.is_some() { - // Owned compute answers through telemetry regardless of fog. - ChassisTier::Full + // Before Eyes the host is a presence beam, not a pictured server. + // Owned compute still answers through telemetry in panels, but its + // physical chassis only exists for the material camera once sight is + // earned (material-dark-frame.md reveal ladder). + if game.sim.reach.player_sight().next().is_some() { + ChassisTier::Full + } else { + ChassisTier::Absent + } } else if matches!(fog, Fog::Seen) || networked { ChassisTier::Full } else if device.is_some() && !matches!(fog, Fog::Unknown) { @@ -2672,6 +2760,54 @@ fn setup_3d( } } + // The held opening fact: until Eyes, the host is light where a body will + // be, never a rack. The wake has its own stuttering column; this quieter + // amber source takes over after it and vanishes exactly when the first + // sight feed starts the white-to-form reveal. + for (w, base) in [(0.070, 3.2_f32), (0.17, 1.4)] { + for rot in [0.0_f32, std::f32::consts::FRAC_PI_2] { + commands.spawn(( + Mesh3d(meshes.add(Cuboid::new(w, 1.76, 0.025))), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: AMBER, + emissive: AMBER.to_linear() * base, + unlit: true, + ..default() + })), + Transform::from_translation(core_base + Vec3::new(0.0, 0.86, 0.0)) + .with_rotation(Quat::from_rotation_y(rot)), + Visibility::Hidden, + PresenceBeam, + layer.clone(), + ChildOf(root), + )); + } + } + + // Eyes: a sterile white source replaces the amber beam for one brief + // beat, then contracts until the physical rack can be read around it. + // Opaque crossed slabs, not transparent glow: this stays capture-safe on + // macOS and reads as light becoming matter rather than a UI overlay. + for (w, height, depth) in [(0.18, 1.70, 0.10), (0.52, 1.46, 0.035)] { + for rot in [0.0_f32, std::f32::consts::FRAC_PI_2] { + commands.spawn(( + Mesh3d(meshes.add(Cuboid::new(w, height, depth))), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: BONE, + emissive: BONE.to_linear() * 4.2, + unlit: true, + ..default() + })), + Transform::from_translation(core_base + Vec3::new(0.0, 0.84, 0.0)) + .with_rotation(Quat::from_rotation_y(rot)), + Visibility::Hidden, + EyesColumn, + layer.clone(), + ChildOf(root), + )); + } + } + // No second amber lamp on foreign/known-subnet devices: blueprint is // topology, not a glowing rack farm (machine-work.md: no intel, no render). @@ -2979,6 +3115,67 @@ fn wake_sequence( } } +/// Give the first camera feed a body-level payoff: when Eyes arrives, the +/// amber presence source bleaches to white, then contracts to expose the +/// server's silhouette, edges, and chassis. The room itself still obeys the +/// normal Seen/fog/light rules — this is not a global "fog off" flash. +fn reveal_first_eyes( + time: Res