use crate::simple_bt::composite::{Repeated, Sequence};
use crate::simple_bt::{BehaviorNode, BehaviorRunner, NodeResult};
use crate::utils::lerp_mix;
use crate::{camera, collision, movement, movement_pointer, GameState};
use crate::{fundsp_kira, player::PlayerAction, You};
use crate::{player::DashState, player::Player};
use bevy::prelude::*;
use bevy::sprite::MaterialMesh2dBundle;
use bevy_asset_loader::prelude::*;
use bevy_egui::{egui, EguiContexts};
use leafwing_input_manager::prelude::*;
use moonshine_spawn::spawn_children;
pub struct DebugPlugin;
impl Plugin for DebugPlugin {
fn build(&self, app: &mut App) {
app.add_event::<MakeABoxTrigger>()
.add_systems(Startup, setup_debug)
.add_systems(Update, make_a_box)
.add_systems(Update, make_polly_think.before(movement::Movement))
.add_systems(Update, debug_view_window.run_if(in_state(GameState::InRun)));
}
fn finish(&self, app: &mut App) {
app.configure_loading_state(
LoadingStateConfig::new(GameState::Booting)
.with_dynamic_assets_file::<StandardDynamicAssetCollection>("debug.assets.ron")
.load_collection::<DebugAssets>(),
);
}
}
#[derive(Component)]
struct Polly;
#[derive(Component)]
struct PollyBrain {
thinking: bool,
runner: BehaviorRunner<(Vec2, Vec2)>,
}
fn setup_debug(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
commands.spawn((
Polly,
PollyBrain {
thinking: false,
runner: BehaviorRunner::new(
Repeated::new(
[
MoveTo {
speed: 250.,
goal: Vec2::X * 300.0,
}
.arc(),
MoveTo {
speed: 250.,
goal: Vec2::NEG_X * 300.0,
}
.arc(),
]
.into_iter()
.collect::<Sequence<_>>()
.arc(),
)
.arc(),
),
},
movement_pointer::MovementDirection(Vec2::ZERO, 0.0),
MaterialMesh2dBundle {
mesh: meshes
.add(Rectangle {
half_size: (16.0, 24.0).into(),
})
.into(),
material: materials.add(Color::PURPLE),
transform: Transform::from_translation(Vec3::Z * 100.0),
..default()
},
collision::Collidable {
offset: Vec2::Y * 8.0,
extents: Vec2::splat(32.0),
},
spawn_children(|cb| {
cb.spawn(movement_pointer::MovementPointerBundle::new(
materials.add(Color::ORANGE_RED),
));
}),
));
}
fn make_polly_think(
mut query: Query<(&mut movement::Movable, &Transform, &mut PollyBrain), With<Polly>>,
) {
let Ok((mut movable, transform, mut thunk)) = query.get_single_mut() else {
return;
};
if thunk.thinking {
let mut context = (transform.translation.truncate(), movable.velocity);
if let Some(res) = thunk.runner.proceed(&mut context) {
info!("Thunking complete step: {context:?} {res}");
}
movable.velocity = context.1;
}
}
#[derive(Event)]
struct MakeABoxTrigger {
mass: f32,
}
const MIN_BOX_MASS: f32 = 0.25;
const MAX_BOX_MASS: f32 = 60.0;
fn make_a_box(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
mut reader: EventReader<MakeABoxTrigger>,
) {
const LIGHT_COLOR: Color = Color::TURQUOISE;
const HEAVY_COLOR: Color = Color::PURPLE;
for box_trigger in reader.read() {
commands.spawn((
movement_pointer::MovementDirection(Vec2::ZERO, 0.0),
MaterialMesh2dBundle {
mesh: meshes
.add(Rectangle {
half_size: (16.0, 24.0).into(),
})
.into(),
material: materials.add(lerp_mix(
LIGHT_COLOR,
HEAVY_COLOR,
(box_trigger.mass - MIN_BOX_MASS) / MAX_BOX_MASS,
)),
transform: Transform::from_translation(Vec3::Z * 100.0),
..default()
},
collision::Collidable {
offset: Vec2::Y * 8.0,
extents: Vec2::splat(32.0),
},
movement::Movable {
mass: dbg!(box_trigger.mass),
..default()
},
spawn_children(|cb| {
cb.spawn(movement_pointer::MovementPointerBundle::new(
materials.add(Color::ORANGE_RED),
));
}),
));
}
}
#[derive(Resource, AssetCollection)]
struct DebugAssets {
#[asset(key = "debug_sfxr", collection(typed))]
debug_sfxr: Vec<Handle<fundsp_kira::Machine>>,
}
#[derive(Debug, Clone)]
struct MoveTo {
speed: f32,
goal: Vec2,
}
impl BehaviorNode<(Vec2, Vec2)> for MoveTo {
fn tick(&self, (position, velocity): &mut (Vec2, Vec2)) -> NodeResult<(Vec2, Vec2)> {
let dist = (self.goal - *position).length();
const ERROR: f32 = 10.0;
if dist <= ERROR {
NodeResult::Success
} else {
let desired_vel = (self.goal - *position).normalize_or_zero() * self.speed;
debug!("{desired_vel:?} {velocity:?}");
let dvn = desired_vel.normalize_or_zero();
let vn = velocity.normalize_or_zero();
let dvl = desired_vel.length();
let vl = velocity.length();
const KEEP: f32 = 0.9;
if dvn.dot(vn) <= 0.0 || vl < dvl {
*velocity *= KEEP;
*velocity += (desired_vel - *velocity) * (1.0 - KEEP);
}
NodeResult::Running(self.clone().arc())
}
}
}
#[allow(clippy::too_many_arguments)]
fn debug_view_window(
query: Query<
(
Entity,
&ActionState<PlayerAction>,
&DashState,
Option<&camera::CameraTarget>,
),
With<Player>,
>,
mut polly: Query<
(
Entity,
Option<&camera::CameraTarget>,
Option<&mut movement::Movable>,
&mut PollyBrain,
),
With<Polly>,
>,
mut contexts: EguiContexts,
you: Option<Res<You>>,
mut track: ResMut<fundsp_kira::MainTrack>,
mut rig: ResMut<camera::CameraRig>,
assets: Res<DebugAssets>,
machines: Res<Assets<fundsp_kira::Machine>>,
mut commands: Commands,
mut writer: EventWriter<MakeABoxTrigger>,
mut last_debug_machine: Local<Option<Handle<fundsp_kira::Machine>>>,
mut box_mass: Local<f32>,
) {
egui::Window::new("Debug Window").show(contexts.ctx_mut(), |ui| {
for handle in assets.debug_sfxr.iter() {
if ui
.button(
handle
.path()
.and_then(|ap| ap.path().to_str())
.unwrap_or("<unlabeled>"),
)
.clicked()
{
*last_debug_machine = Some(handle.clone());
track.play(handle.clone());
}
}
ui.collapsing("Box Mania", |ui| {
ui.add(egui::Slider::new(&mut *box_mass, MIN_BOX_MASS..=MAX_BOX_MASS).text("Mass"));
*box_mass = (*box_mass).clamp(MIN_BOX_MASS, MAX_BOX_MASS);
if (*box_mass).is_nan() {
*box_mass = MIN_BOX_MASS;
}
if ui.button("Make A Box").clicked() {
writer.send(MakeABoxTrigger { mass: *box_mass });
}
});
ui.collapsing("Polly Control", |ui| {
let (polly_entity, cam_target, mut movable, mut thunk) = polly.single_mut();
if cam_target.is_some() {
if ui.button("Remove").clicked() {
commands
.entity(polly_entity)
.remove::<camera::CameraTarget>();
}
} else if ui.button("With You").clicked() {
commands
.entity(polly_entity)
.insert(camera::CameraTarget::default());
} else if ui.button("Without You").clicked() {
commands
.entity(polly_entity)
.insert(camera::CameraTarget(1));
}
if let Some(movable) = movable.as_mut() {
if ui.button("Static").clicked() {
commands.entity(polly_entity).remove::<movement::Movable>();
}
ui.add(
egui::Slider::new(&mut movable.mass, MIN_BOX_MASS..=MAX_BOX_MASS).text("Mass"),
);
movable.acceleration *= 0.1;
} else if ui.button("Dynamic").clicked() {
commands.entity(polly_entity).insert(movement::Movable {
mass: 20.0,
..default()
});
}
if ui
.toggle_value(&mut thunk.thinking, "Has Thought")
.changed()
{
if movable.is_none() && thunk.thinking {
commands.entity(polly_entity).insert(movement::Movable {
mass: 20.0,
..default()
});
}
}
});
ui.collapsing("Debug Sfxr", |ui| {
if let Some(sfxr) = last_debug_machine
.as_ref()
.and_then(|hnd| machines.get(hnd.clone()))
.and_then(|m| m.userdata())
.and_then(|ud| ud.downcast_ref::<crate::sfxr::Sfxr>())
{
ui.label(format!("{sfxr:#?}"));
} else {
ui.label("");
}
});
ui.collapsing("Camera Rig", |ui| {
let mut lag = rig.lag();
if ui
.add(egui::Slider::new(&mut lag, 0.0..=1.).text("Lag"))
.changed()
{
rig.set_lag(lag);
}
ui.add(egui::Slider::new(&mut rig.targetting, 0..=1).text("Target"));
let mut snap_dur_secs = rig.snap_duration.as_secs_f64();
if ui
.add(
egui::Slider::new(&mut snap_dur_secs, 60.0f64.recip()..=2.0).text("Snap Delta"),
)
.changed()
{
rig.snap_duration = std::time::Duration::from_secs_f64(snap_dur_secs);
}
});
ui.collapsing("Audio Details", |ui| {
use egui_plot::{Line, Plot, PlotPoints};
let left_wave: PlotPoints = track
.samples()
.iter()
.enumerate()
.map(|(t, (s, _))| [t as f64, *s as f64])
.collect();
let left_line = Line::new(left_wave).color(egui::Color32::BLUE);
let right_wave: PlotPoints = track
.samples()
.iter()
.enumerate()
.map(|(t, (_, s))| [t as f64, *s as f64])
.collect();
let right_line = Line::new(right_wave).color(egui::Color32::RED);
ui.label("Main Track Oscilloscope");
Plot::new("waveform")
.view_aspect(4.0)
.auto_bounds(egui::Vec2b::new(false, false))
.show_grid(egui::Vec2b::new(false, true))
.include_y(-std::f32::consts::SQRT_2.recip())
.include_y(std::f32::consts::SQRT_2.recip())
.include_x(0.0)
.include_x(track.buffer_length() as f64)
.show_axes(false)
.show(ui, |plot_ui| {
plot_ui.line(left_line);
plot_ui.line(right_line);
});
let current_rms = track.rms().last().copied().unwrap_or((0.0, 0.0));
ui.label(format!("Main Track RMS: {current_rms:?}"));
ui.label(format!("Main Track SR: {}", track.sample_rate()));
});
if let Some(you) = you.and_then(|you| you.0) {
ui.label(format!("You are {}.", you));
} else {
ui.label("There is no You.");
}
let Ok((player_entity, action_state, dash_state, maybe_cam_target)) = query.get_single()
else {
return;
};
if maybe_cam_target.is_some() {
if ui.button("Detach Camera").clicked() {
commands
.entity(player_entity)
.remove::<camera::CameraTarget>();
}
} else if ui.button("Attach Camera").clicked() {
commands
.entity(player_entity)
.insert(camera::CameraTarget::default());
}
ui.label(if dash_state.0 {
"Dashing"
} else {
"Not Dashing"
});
if let Some(dad) = action_state.clamped_axis_pair(&PlayerAction::Move) {
ui.label(format!("X movement: {}", dad.x()));
ui.label(format!("Y movement: {}", dad.y()));
} else {
ui.label("!! Move action not (properly) bound !!");
}
if let Some(hand_dad) = action_state.clamped_axis_pair(&PlayerAction::Hand) {
_ = hand_dad;
ui.label("Hand action connected!");
ui.label(format!(
"Hand actions: [{}]",
[
(hand_dad.x() > 0.5).then_some("HandMoveRight"),
(hand_dad.x() < -0.5).then_some("HandMoveLeft"),
(hand_dad.y() > 0.5).then_some("HandUse"),
]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join(", ")
));
} else {
ui.label("!! Hand action not (properly) bound !!");
}
});
}