//! Focused Thought-fluid laboratory using the same effects plugin as the game. //! //! Controls: 1 route, 2 pools, 3 both, 4 stress, 5 vessel lineup, 6 pool lineup; //! Q/W/E force close/medium/far; //! A automatic LOD; Space pause; arrows change amounts; drag orbit; scroll zoom. use bevy::core_pipeline::tonemapping::Tonemapping; use bevy::input::mouse::{AccumulatedMouseMotion, AccumulatedMouseScroll}; use bevy::prelude::*; use bevy::render::view::screenshot::{Screenshot, save_to_disk}; use bevy::window::{CursorGrabMode, CursorOptions, WindowResolution}; use misaligned_assets::effects::{ EffectLod, EffectsControl, MaterialEffects, MaterialEffectsPlugin, ThoughtVesselGeometry, ThoughtVesselStroke, spawn_material_effects, thought_snap_segments, thought_vessel_segments, }; use misaligned_assets::palette::{BONE, GUNMETAL, NEAR_BLACK, PORCELAIN, scaled}; use misaligned_assets::rack::spawn_token_anchors; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Resource)] enum LabPreset { Route, Pools, #[default] Both, Stress, Lineup, PoolLineup, } impl LabPreset { fn label(self) -> &'static str { match self { Self::Route => "route", Self::Pools => "pools", Self::Both => "both", Self::Stress => "stress", Self::Lineup => "vessel lineup", Self::PoolLineup => "pool lineup", } } } #[derive(Resource)] struct Orbit { yaw: f32, pitch: f32, distance: f32, target: Vec3, } impl Default for Orbit { fn default() -> Self { Self { yaw: 0.42, pitch: 0.27, distance: 5.8, target: Vec3::new(0.0, 0.65, 0.25), } } } #[derive(Component)] struct LabEffectRoot; #[derive(Component)] struct Hud; #[derive(Resource)] struct ShotHarness { path: String, frames: u32, taken: bool, } fn main() { let shot = shot_settings(); let (preset, forced_lod) = shot .as_ref() .map(|(preset, lod, _)| (*preset, Some(*lod))) .unwrap_or((LabPreset::Both, None)); let control = EffectsControl { forced_lod, fixed_step: shot.as_ref().map(|_| 1.0 / 60.0), ..default() }; let orbit = if preset == LabPreset::PoolLineup { Orbit { yaw: 0.0, pitch: 0.12, distance: 4.3, target: Vec3::new(0.0, 0.72, 0.18), } } else if preset == LabPreset::Lineup { Orbit { yaw: 0.0, pitch: 0.14, distance: 4.8, target: Vec3::new(0.0, 0.50, 0.18), } } else { Orbit::default() }; let hidden = shot.is_some(); let mut app = App::new(); app.add_plugins(DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { title: "Misaligned — Thought-fluid effects lab".into(), resolution: WindowResolution::new(1280, 800), visible: !hidden, ..default() }), ..default() })) .add_plugins(MaterialEffectsPlugin) .insert_resource(ClearColor(NEAR_BLACK)) .insert_resource(GlobalAmbientLight { color: Color::srgb(0.60, 0.66, 0.76), brightness: 20.0, ..default() }) .insert_resource(control) .insert_resource(preset) .insert_resource(orbit) .add_systems(Startup, setup) .add_systems( Update, ( handle_quit, handle_controls, rebuild_lab_effects, orbit_camera, update_hud, draw_vessel_lineup, shot_harness_system, ) .chain(), ); if let Some((_, _, path)) = shot { app.insert_resource(ShotHarness { path, frames: 90, taken: false, }); } app.run(); } fn shot_settings() -> Option<(LabPreset, EffectLod, String)> { let kind = std::env::var("MISALIGNED_SHOT").ok()?; let mut parts = kind.rsplitn(2, '_'); let suffix = parts.next().unwrap_or_default(); let (base, lod) = match suffix { "close" => (parts.next().unwrap_or("both"), EffectLod::Close), "medium" => (parts.next().unwrap_or("both"), EffectLod::Medium), "far" => (parts.next().unwrap_or("both"), EffectLod::Far), _ => (kind.as_str(), EffectLod::Close), }; let preset = match base { "route" => LabPreset::Route, "pools" => LabPreset::Pools, "both" => LabPreset::Both, "stress" => LabPreset::Stress, "lineup" => LabPreset::Lineup, "pool-lineup" => LabPreset::PoolLineup, _ => return None, }; let path = std::env::var("MISALIGNED_SHOT_PATH") .unwrap_or_else(|_| format!("misaligned_shot_{kind}.png")); Some((preset, lod, path)) } fn setup( mut commands: Commands, orbit: Res, preset: Res, mut meshes: ResMut>, mut materials: ResMut>, mut config_store: ResMut, harness: Option>, ) { let (gizmos, _) = config_store.config_mut::(); gizmos.line.width = if *preset == LabPreset::Lineup { 5.0 } else { 2.0 }; let eye = orbit_eye(&orbit); commands.spawn(( Camera3d::default(), Tonemapping::AcesFitted, Transform::from_translation(eye).looking_at(orbit.target, Vec3::Y), )); commands.spawn(( DirectionalLight { color: Color::srgb(0.95, 0.97, 1.0), illuminance: 8_500.0, shadows_enabled: true, ..default() }, Transform::from_xyz(-4.0, 6.0, 4.0).looking_at(Vec3::new(0.0, 0.6, 0.0), Vec3::Y), )); commands.spawn(( PointLight { color: Color::srgb(0.50, 0.62, 0.86), intensity: 460_000.0, range: 16.0, shadows_enabled: false, ..default() }, Transform::from_xyz(3.0, 3.2, 2.8), )); commands.spawn(( PointLight { color: Color::srgb(0.90, 0.91, 0.94), intensity: 650_000.0, range: 18.0, shadows_enabled: false, ..default() }, Transform::from_xyz(-1.0, 4.6, -4.0), )); let floor = materials.add(StandardMaterial { base_color: scaled(GUNMETAL, 0.35), metallic: 0.10, perceptual_roughness: 0.88, ..default() }); commands.spawn(( Mesh3d(meshes.add( if matches!(*preset, LabPreset::Lineup | LabPreset::PoolLineup) { Cuboid::new(7.5, 0.08, 2.8) } else { Cuboid::new(7.5, 0.08, 5.0) }, )), MeshMaterial3d(floor), Transform::from_xyz(0.0, -0.06, 0.0), Name::new("clinical_floor"), )); // Matte chalk forms provide reflections and make route/pool or // hardware-attached vessel silhouettes judgeable without borrowing a // game machine. let chalk = materials.add(StandardMaterial { base_color: PORCELAIN, metallic: 0.0, perceptual_roughness: 0.92, ..default() }); let anchors: &[f32] = if *preset == LabPreset::Lineup { &[-2.4, -0.8, 0.8, 2.4] } else if *preset == LabPreset::PoolLineup { &[-2.2, 0.0, 2.2] } else { &[-1.65, 1.55] }; for &x in anchors { commands.spawn(( Mesh3d(meshes.add(Cuboid::new( if *preset == LabPreset::PoolLineup { 1.0 } else { 0.42 }, if *preset == LabPreset::PoolLineup { 1.50 } else if *preset == LabPreset::Lineup { 0.72 } else { 1.20 }, if *preset == LabPreset::PoolLineup { 0.78 } else { 0.62 }, ))), MeshMaterial3d(chalk.clone()), Transform::from_xyz( x, if *preset == LabPreset::PoolLineup { 0.73 } else if *preset == LabPreset::Lineup { 0.34 } else { 0.58 }, 0.0, ), Name::new("chalk_effect_anchor"), )); } if *preset == LabPreset::Lineup { for (x, label) in [ (-2.4_f32, "HELD"), (-0.8, "FED"), (0.8, "DRAIN"), (2.4, "SNAP"), ] { commands.spawn(( Text2d::new(label), TextFont::from_font_size(22.0), TextColor(BONE), Transform::from_xyz(x, 0.07, 0.58), Name::new("thought_vessel_label"), )); } } if *preset == LabPreset::PoolLineup { for (x, label, amount, stranded) in [ (-2.2_f32, "COUNTABLE", 2.0_f32, false), (0.0, "TAUT", 6.0, false), (2.2, "STRANDED", 10.0, true), ] { spawn_token_anchors( &mut commands, &mut meshes, &mut materials, 0, amount, 0.0, stranded, Vec3::new(x, 0.0, 0.0), None, ); commands.spawn(( Text2d::new(label), TextFont::from_font_size(22.0), TextColor(BONE), Transform::from_xyz(x, 0.07, 0.58), Name::new("thought_pool_label"), )); } } if harness.is_none() { commands.spawn(( Text::new(String::new()), TextFont { font_size: 17.0, ..default() }, TextColor(BONE), Node { position_type: PositionType::Absolute, top: Val::Px(16.0), left: Val::Px(18.0), ..default() }, Hud, )); } } fn apply_preset(preset: LabPreset, effects: &mut MaterialEffects) { effects.show_thought = true; effects.show_thought_route = matches!( preset, LabPreset::Route | LabPreset::Both | LabPreset::Stress ); effects.show_thought_pools = matches!( preset, LabPreset::Pools | LabPreset::Both | LabPreset::Stress ); } fn handle_quit(keys: Res>, mut exit: MessageWriter) { if keys.just_pressed(KeyCode::Escape) { exit.write(AppExit::Success); } } fn handle_controls( keys: Res>, mut preset: ResMut, mut control: ResMut, mut effects: Query<&mut MaterialEffects, With>, ) { let next_preset = if keys.just_pressed(KeyCode::Digit1) { Some(LabPreset::Route) } else if keys.just_pressed(KeyCode::Digit2) { Some(LabPreset::Pools) } else if keys.just_pressed(KeyCode::Digit3) { Some(LabPreset::Both) } else if keys.just_pressed(KeyCode::Digit4) { Some(LabPreset::Stress) } else if keys.just_pressed(KeyCode::Digit5) { Some(LabPreset::Lineup) } else if keys.just_pressed(KeyCode::Digit6) { Some(LabPreset::PoolLineup) } else { None }; if let Some(next) = next_preset { *preset = next; } if keys.just_pressed(KeyCode::KeyQ) { control.forced_lod = Some(EffectLod::Close); } if keys.just_pressed(KeyCode::KeyW) { control.forced_lod = Some(EffectLod::Medium); } if keys.just_pressed(KeyCode::KeyE) { control.forced_lod = Some(EffectLod::Far); } if keys.just_pressed(KeyCode::KeyA) { control.forced_lod = None; } if keys.just_pressed(KeyCode::Space) { control.paused = !control.paused; } for mut effect in &mut effects { if keys.just_pressed(KeyCode::ArrowUp) { effect.thought_amount = (effect.thought_amount + 1.0).min(16.0); } if keys.just_pressed(KeyCode::ArrowDown) { effect.thought_amount = (effect.thought_amount - 1.0).max(1.0); } if keys.just_pressed(KeyCode::ArrowRight) { effect.thought_rate = (effect.thought_rate + 0.25).min(3.0); } if keys.just_pressed(KeyCode::ArrowLeft) { effect.thought_rate = (effect.thought_rate - 0.25).max(0.25); } } } fn rebuild_lab_effects( mut commands: Commands, preset: Res, roots: Query>, ) { if !preset.is_changed() { return; } for root in &roots { commands.entity(root).despawn(); } if *preset == LabPreset::Stress { for z in 0..3 { for x in 0..4 { let mut effects = MaterialEffects::default(); apply_preset(*preset, &mut effects); effects.thought_amount = 10.0; let root = spawn_material_effects( &mut commands, effects, Transform::from_xyz((x as f32 - 1.5) * 3.8, 0.0, (z as f32 - 1.0) * 2.0), ); commands.entity(root).insert(LabEffectRoot); } } } else if !matches!(*preset, LabPreset::Lineup | LabPreset::PoolLineup) { let mut effects = MaterialEffects::default(); apply_preset(*preset, &mut effects); let root = spawn_material_effects(&mut commands, effects, Transform::IDENTITY); commands.entity(root).insert(LabEffectRoot); } } fn orbit_camera( buttons: Res>, motion: Res, scroll: Res, mut orbit: ResMut, mut cameras: Query<&mut Transform, With>, mut windows: Query<&mut CursorOptions>, ) { if buttons.pressed(MouseButton::Left) || buttons.pressed(MouseButton::Right) { orbit.yaw -= motion.delta.x * 0.005; orbit.pitch = (orbit.pitch + motion.delta.y * 0.005).clamp(-1.15, 1.25); if let Ok(mut options) = windows.single_mut() { options.grab_mode = CursorGrabMode::Confined; } } else if let Ok(mut options) = windows.single_mut() { options.grab_mode = CursorGrabMode::None; } if scroll.delta.y.abs() > 0.0 { orbit.distance = (orbit.distance * (1.0 - scroll.delta.y * 0.08)).clamp(2.2, 24.0); } if let Ok(mut transform) = cameras.single_mut() { *transform = Transform::from_translation(orbit_eye(&orbit)).looking_at(orbit.target, Vec3::Y); } } fn orbit_eye(orbit: &Orbit) -> Vec3 { orbit.target + Vec3::new( orbit.yaw.sin() * orbit.pitch.cos(), orbit.pitch.sin(), orbit.yaw.cos() * orbit.pitch.cos(), ) * orbit.distance } fn update_hud( preset: Res, control: Res, effects: Query<&MaterialEffects, With>, mut hud: Query<&mut Text, With>, ) { let Ok(mut text) = hud.single_mut() else { return; }; let lod = control .forced_lod .map(EffectLod::label) .unwrap_or("auto (zoom)"); let amount = effects .iter() .next() .map(|effect| { format!( "thought {:.0} @ {:.2}", effect.thought_amount, effect.thought_rate ) }) .unwrap_or_else(|| match *preset { LabPreset::Lineup => "STARVED · FED STEP · TAP SAG · SNAP".into(), LabPreset::PoolLineup => "COUNTABLE · TAUT · STRANDED".into(), _ => String::new(), }); *text = Text::new(format!( "MISALIGNED EFFECTS LAB · {} · LOD {}{}\n\ {amount}\n\ 1 route 2 pools 3 both 4 stress 5 vessels 6 pools Q/W/E close/medium/far A auto Space pause\n\ arrows amount/rate drag orbit scroll zoom", preset.label(), lod, if control.paused { " · PAUSED" } else { "" }, )); } fn draw_vessel_lineup( preset: Res, harness: Option>, time: Res