Add galaxy parameter UI, star selection, and movement/physics scaffolding
Galaxy creation scene (Bevy 0.16): - Split into module folder: params.rs, mod.rs (generation + spawn), ui.rs, selection.rs - GalaxyParams resource (seed, count, arms, vertical_arms, size, twist, vertical_twist) with generation counter for change detection - Control panel: 7 +/- slider rows + Regenerate button, no native Bevy slider widget so uses label + icon buttons - Info panel: live-refreshes on selection change via despawn_related::<Children> - Click-to-select: screen-space picking (project each star to viewport, closest within 18px threshold wins); selected star lerps scale to 2.2x - Generation faithfully ports the docs TS reference including vertical arms - Regeneration system: despawns GalaxyScene root only, preserves UI panels Camera: - Camera2d -> Camera3d + OrbitCamera (left-drag yaw/pitch, scroll zoom, pitch/distance clamped to avoid gimbal) - Orbit drag suppressed when cursor over UI New plugins (scaffolding): - movement/: Velocity, MaxSpeed, TurnRate, Drag, MoveTarget, Player components + click-to-move (no player spawned yet) - physics/: pure-data geometry (ray_vs_sphere, overlaps, separate, segment_vs_sphere) with 7 passing unit tests; no systems wired yet - star_map/: plugin skeleton gated on AppState::InGame Shared: - ui/util.rs: cursor_over_ui helper for UI-hover detection - docs: ARCH-9 decision record (custom movement & collision, no physics engine)
This commit is contained in:
@@ -1,5 +1,102 @@
|
||||
use bevy::input::mouse::MouseButton;
|
||||
use bevy::prelude::*;
|
||||
use bevy::window::PrimaryWindow;
|
||||
|
||||
pub fn spawn_camera(mut commands: Commands) {
|
||||
commands.spawn(Camera2d);
|
||||
use crate::ui::util::cursor_over_ui;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct MainCamera;
|
||||
|
||||
/// Orbit-style camera: rotates around `target` at `distance`, controlled by mouse.
|
||||
/// Used for inspection scenes like GalaxyCreation where there is no player to follow.
|
||||
#[derive(Component, Debug, Clone, Copy)]
|
||||
pub struct OrbitCamera {
|
||||
pub target: Vec3,
|
||||
pub distance: f32,
|
||||
/// Yaw around the Y axis (radians). 0 = camera at +Z looking toward origin.
|
||||
pub yaw: f32,
|
||||
/// Pitch above the horizontal plane (radians). 0 = horizontal, π/2 = straight down.
|
||||
pub pitch: f32,
|
||||
}
|
||||
|
||||
impl Default for OrbitCamera {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
target: Vec3::ZERO,
|
||||
distance: 420.0,
|
||||
yaw: 0.0,
|
||||
// ~36° above horizontal — roughly matches docs GalaxyScene camera position.
|
||||
pitch: 0.625,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Initial camera spawn. The same entity is reused across states; control systems
|
||||
/// decide how it moves depending on which state is active.
|
||||
pub fn spawn_camera(mut commands: Commands) {
|
||||
commands.spawn((
|
||||
Camera3d::default(),
|
||||
Transform::from_xyz(0.0, 260.0, 360.0).looking_at(Vec3::ZERO, Vec3::Y),
|
||||
MainCamera,
|
||||
OrbitCamera::default(),
|
||||
));
|
||||
}
|
||||
|
||||
const ORBIT_ROTATE_SENSITIVITY: f32 = 0.005;
|
||||
const ORBIT_ZOOM_SENSITIVITY: f32 = 0.1;
|
||||
const ORBIT_MIN_PITCH: f32 = 0.15; // ~9° above horizontal — never fully edge-on
|
||||
const ORBIT_MAX_PITCH: f32 = 1.4; // ~80° — never straight down (gimbal safeguard)
|
||||
const ORBIT_MIN_DISTANCE: f32 = 40.0;
|
||||
const ORBIT_MAX_DISTANCE: f32 = 1500.0;
|
||||
|
||||
/// Left-drag rotates around the orbit target; scroll wheel zooms.
|
||||
///
|
||||
/// Drag is suppressed when the cursor is over any UI node — otherwise clicking
|
||||
/// buttons or panels would also rotate the camera.
|
||||
pub fn orbit_camera_control(
|
||||
mouse_input: Res<ButtonInput<MouseButton>>,
|
||||
primary_window: Query<&Window, With<PrimaryWindow>>,
|
||||
mut mouse_motion: EventReader<bevy::input::mouse::MouseMotion>,
|
||||
mut scroll_events: EventReader<bevy::input::mouse::MouseWheel>,
|
||||
mut query: Query<(&mut Transform, &mut OrbitCamera), With<MainCamera>>,
|
||||
ui_nodes: Query<(&bevy::ui::ComputedNode, &GlobalTransform)>,
|
||||
) {
|
||||
let Ok((mut transform, mut orbit)) = query.single_mut() else {
|
||||
// Drain pending input to avoid stale buildup when there's no camera.
|
||||
mouse_motion.clear();
|
||||
scroll_events.clear();
|
||||
return;
|
||||
};
|
||||
|
||||
let cursor_over_ui = primary_window
|
||||
.single()
|
||||
.ok()
|
||||
.map(|w| cursor_over_ui(w, &ui_nodes))
|
||||
.unwrap_or(false);
|
||||
|
||||
if mouse_input.pressed(MouseButton::Left) && !cursor_over_ui {
|
||||
for event in mouse_motion.read() {
|
||||
orbit.yaw -= event.delta.x * ORBIT_ROTATE_SENSITIVITY;
|
||||
orbit.pitch -= event.delta.y * ORBIT_ROTATE_SENSITIVITY;
|
||||
}
|
||||
} else {
|
||||
mouse_motion.clear();
|
||||
}
|
||||
|
||||
for event in scroll_events.read() {
|
||||
// Scroll up (positive y) → decrease distance (zoom in).
|
||||
orbit.distance *= 1.0 - event.y * ORBIT_ZOOM_SENSITIVITY;
|
||||
}
|
||||
|
||||
orbit.pitch = orbit.pitch.clamp(ORBIT_MIN_PITCH, ORBIT_MAX_PITCH);
|
||||
orbit.distance = orbit.distance.clamp(ORBIT_MIN_DISTANCE, ORBIT_MAX_DISTANCE);
|
||||
|
||||
let cos_p = orbit.pitch.cos();
|
||||
let pos = orbit.target
|
||||
+ Vec3::new(
|
||||
orbit.distance * cos_p * orbit.yaw.sin(),
|
||||
orbit.distance * orbit.pitch.sin(),
|
||||
orbit.distance * cos_p * orbit.yaw.cos(),
|
||||
);
|
||||
*transform = Transform::from_translation(pos).looking_at(orbit.target, Vec3::Y);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user