feat(galaxy): multi-disk galaxy generation with tab-based UI
- Add multi-disk support (up to 4 independent disk layers) with per-disk arms, twist, tilt, rotation, inner/outer radius - Add polar beams (top/bottom) with taper, gravity, thickness controls - Implement tab-based disk selector UI with add/remove disk - Add Randomize Settings button for full param randomization - Add Regenerate (reseed) and Center View buttons - Add per-system contents generation (planets, belts, stations, anomalies, gas clouds, stargates) with orbital mechanics - Refactor control panel into scrollable sections with value display
This commit is contained in:
@@ -45,6 +45,7 @@ impl Plugin for GalaxyPlugin {
|
||||
Update,
|
||||
(
|
||||
escape_to_main_menu,
|
||||
ui::rebuild_scroll_content,
|
||||
ui::param_button_handler,
|
||||
ui::refresh_control_panel_values,
|
||||
ui::scroll_control_panel,
|
||||
@@ -181,21 +182,22 @@ fn generate_galaxy(
|
||||
(systems, contents, connections)
|
||||
}
|
||||
|
||||
/// Position-only galaxy generation. Composes three structural layers —
|
||||
/// a [`CoreParams`] cluster, a [`DiskParams`] of horizontal spiral arms,
|
||||
/// and two [`BeamParams`] columns along ±Y — into a single flat list of
|
||||
/// systems ready for the connection graph and the spawner.
|
||||
/// Position-only galaxy generation. Composes multiple structural layers —
|
||||
/// a [`CoreParams`] cluster, up to [`MAX_DISKS`] independent [`DiskParams`]
|
||||
/// spiral disks, and two [`BeamParams`] columns along ±Y — into a single
|
||||
/// flat list of systems ready for the connection graph and the spawner.
|
||||
///
|
||||
/// Each layer owns its own system count; the total galaxy population is
|
||||
/// `core.count + disk.count + beam_top.count + beam_bottom.count`. System
|
||||
/// indices are global across all layers (used for `g-{n}` IDs and name
|
||||
/// suffixes) so the connection graph and POI generator see one consistent
|
||||
/// vector.
|
||||
/// `core.count + Σ disks[].count + beam_top.count + beam_bottom.count`.
|
||||
/// System indices are global across all layers (used for `g-{n}` IDs and
|
||||
/// name suffixes) so the connection graph and POI generator see one
|
||||
/// consistent vector.
|
||||
fn generate_system_positions(params: &GalaxyParams, rng: &mut StdRng) -> Vec<GeneratedSystem> {
|
||||
// Total budget — pre-allocation only; each pass appends as many systems
|
||||
// as it manages to place.
|
||||
let disk_total: usize = params.disks.iter().map(|d| d.count).sum();
|
||||
let total = params.core.count
|
||||
+ params.disk.count
|
||||
+ disk_total
|
||||
+ params.beam_top.count
|
||||
+ params.beam_bottom.count;
|
||||
let mut systems: Vec<GeneratedSystem> = Vec::with_capacity(total);
|
||||
@@ -209,14 +211,19 @@ fn generate_system_positions(params: &GalaxyParams, rng: &mut StdRng) -> Vec<Gen
|
||||
let mut next_index = 0usize;
|
||||
|
||||
generate_core(&mut systems, rng, ¶ms.core, params.size, base_spacing, &mut next_index);
|
||||
generate_disk(
|
||||
&mut systems,
|
||||
rng,
|
||||
¶ms.disk,
|
||||
params.size,
|
||||
base_spacing,
|
||||
&mut next_index,
|
||||
);
|
||||
|
||||
for (disk_index, disk) in params.disks.iter().enumerate() {
|
||||
generate_disk(
|
||||
&mut systems,
|
||||
rng,
|
||||
disk,
|
||||
params.size,
|
||||
base_spacing,
|
||||
&mut next_index,
|
||||
disk_index,
|
||||
);
|
||||
}
|
||||
|
||||
if params.beam_top.enabled {
|
||||
generate_beam(
|
||||
&mut systems,
|
||||
@@ -302,9 +309,10 @@ fn generate_core(
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the horizontal disk of spiral arms in the XZ plane. Faithful to
|
||||
/// the original generator's density bias (`pow(0.62)` → packed near origin)
|
||||
/// and arm twist.
|
||||
/// Generate one disk layer of spiral arms. Faithful to the original
|
||||
/// generator's density bias (`pow(0.62)` → packed near origin) and arm twist.
|
||||
/// Supports tilt (rotation around X, capped at 45°), Y-axis rotation offset,
|
||||
/// and independent inner/outer radii.
|
||||
fn generate_disk(
|
||||
systems: &mut Vec<GeneratedSystem>,
|
||||
rng: &mut StdRng,
|
||||
@@ -312,24 +320,53 @@ fn generate_disk(
|
||||
galaxy_size: f32,
|
||||
base_spacing: f32,
|
||||
next_index: &mut usize,
|
||||
disk_index: usize,
|
||||
) {
|
||||
let arm_count = disk.arms.max(1);
|
||||
// Effective outer radius: explicit value if set, else galaxy size.
|
||||
let outer = if disk.outer_radius > 0.0 {
|
||||
disk.outer_radius
|
||||
} else {
|
||||
galaxy_size
|
||||
};
|
||||
let inner = disk.inner_radius;
|
||||
let span = (outer - inner).max(1.0);
|
||||
|
||||
// Pre-compute the rotation quaternion for this disk's tilt + Y rotation.
|
||||
// Tilt is applied first (around X), then the Y rotation offset.
|
||||
let disk_rotation = if disk.tilt.abs() > 0.001 || disk.rotation_offset.abs() > 0.001 {
|
||||
let tilt = Quat::from_rotation_x(disk.tilt);
|
||||
let yaw = Quat::from_rotation_y(disk.rotation_offset);
|
||||
Some(yaw * tilt)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
for i in 0..disk.count {
|
||||
let arm = (i as u32) % arm_count;
|
||||
// Non-core disk factions cycle through Amarr/Minmatar/Gallente/Caldari.
|
||||
let faction_index = 1 + (arm as usize) % (FACTIONS.len() - 1);
|
||||
// Offset by disk_index so different disks don't all share the same
|
||||
// faction assignment pattern.
|
||||
let faction_index =
|
||||
1 + ((arm as usize + disk_index) % (FACTIONS.len() - 1));
|
||||
let (faction, color) = FACTIONS[faction_index];
|
||||
|
||||
let mut position = Option::<Vec3>::None;
|
||||
let mut final_radius = 0.0f32;
|
||||
for attempt in 0..SPACING_ATTEMPTS {
|
||||
let r = rng.gen::<f32>().powf(0.62) * galaxy_size;
|
||||
let r = inner + rng.gen::<f32>().powf(0.62) * span;
|
||||
let angle = std::f32::consts::TAU * arm as f32 / arm_count as f32
|
||||
+ (r / galaxy_size) * disk.twist
|
||||
+ ((r - inner) / span) * disk.twist
|
||||
+ (rng.gen::<f32>() - 0.5) * 0.72;
|
||||
let y = (rng.gen::<f32>() - 0.5) * 20.0;
|
||||
let candidate = Vec3::new(angle.cos() * r, y, angle.sin() * r);
|
||||
final_radius = r;
|
||||
let mut candidate = Vec3::new(angle.cos() * r, y, angle.sin() * r);
|
||||
|
||||
// Apply tilt + rotation for non-flat disks.
|
||||
if let Some(rot) = disk_rotation {
|
||||
candidate = rot * candidate;
|
||||
}
|
||||
|
||||
final_radius = candidate.length();
|
||||
let spacing = relax_spacing(base_spacing, attempt).max(MIN_SYSTEM_SPACING);
|
||||
if systems
|
||||
.iter()
|
||||
@@ -356,7 +393,10 @@ fn generate_disk(
|
||||
color,
|
||||
security,
|
||||
is_core: false,
|
||||
origin: SystemOrigin::Disk { arm },
|
||||
origin: SystemOrigin::Disk {
|
||||
disk: disk_index,
|
||||
arm,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -478,7 +518,12 @@ fn security_for_radius(radius: f32, galaxy_size: f32) -> f32 {
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum SystemOrigin {
|
||||
Core,
|
||||
Disk { arm: u32 },
|
||||
Disk {
|
||||
/// Which disk layer this system belongs to (index into [`GalaxyParams::disks`]).
|
||||
disk: usize,
|
||||
/// Which arm within the disk.
|
||||
arm: u32,
|
||||
},
|
||||
Beam { side: BeamTag },
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
//!
|
||||
//! ## Structure
|
||||
//!
|
||||
//! A galaxy is composed of three layers, each owning its own system count:
|
||||
//! A galaxy is composed of three structural layer types:
|
||||
//! - [`CoreParams`] — Concord systems clustered near the origin.
|
||||
//! - [`DiskParams`] — horizontal spiral arms in the XZ plane.
|
||||
//! - [`DiskParams`] (up to [`MAX_DISKS`]) — independent spiral disks, each
|
||||
//! with its own arm count, twist, tilt, rotation, and radial extent.
|
||||
//! - [`BeamParams`] (×2, top and bottom) — vertical columns of systems along
|
||||
//! ±Y, the "polar beams". Each beam is its own population of selectable
|
||||
//! star systems, fully equal citizens to disk arms.
|
||||
@@ -57,6 +58,9 @@ pub const CORE_RADIUS_MIN: f32 = 10.0;
|
||||
pub const CORE_RADIUS_MAX: f32 = 120.0;
|
||||
pub const CORE_RADIUS_STEP: f32 = 5.0;
|
||||
|
||||
/// Maximum number of independent disk layers.
|
||||
pub const MAX_DISKS: usize = 4;
|
||||
|
||||
pub const DISK_COUNT_MIN: usize = 20;
|
||||
pub const DISK_COUNT_MAX: usize = 220;
|
||||
pub const DISK_COUNT_STEP: usize = 4;
|
||||
@@ -66,6 +70,24 @@ pub const DISK_TWIST_MIN: f32 = 1.0;
|
||||
pub const DISK_TWIST_MAX: f32 = 6.0;
|
||||
pub const DISK_TWIST_STEP: f32 = 0.2;
|
||||
|
||||
/// Max tilt is 45° — disks should never be parallel to beams.
|
||||
pub const DISK_TILT_MIN: f32 = 0.0;
|
||||
pub const DISK_TILT_MAX: f32 = std::f32::consts::FRAC_PI_4;
|
||||
/// ~5° steps (45° / 9).
|
||||
pub const DISK_TILT_STEP: f32 = std::f32::consts::FRAC_PI_4 / 9.0;
|
||||
|
||||
pub const DISK_ROTATION_MIN: f32 = 0.0;
|
||||
pub const DISK_ROTATION_MAX: f32 = std::f32::consts::TAU;
|
||||
/// 30° steps.
|
||||
pub const DISK_ROTATION_STEP: f32 = std::f32::consts::TAU / 12.0;
|
||||
|
||||
pub const DISK_INNER_RADIUS_MIN: f32 = 0.0;
|
||||
pub const DISK_INNER_RADIUS_MAX: f32 = 400.0;
|
||||
pub const DISK_INNER_RADIUS_STEP: f32 = 10.0;
|
||||
pub const DISK_OUTER_RADIUS_MIN: f32 = 40.0;
|
||||
pub const DISK_OUTER_RADIUS_MAX: f32 = 420.0;
|
||||
pub const DISK_OUTER_RADIUS_STEP: f32 = 10.0;
|
||||
|
||||
pub const BEAM_COUNT_MIN: usize = 0;
|
||||
pub const BEAM_COUNT_MAX: usize = 120;
|
||||
pub const BEAM_COUNT_STEP: usize = 4;
|
||||
@@ -115,12 +137,31 @@ impl Default for CoreParams {
|
||||
}
|
||||
}
|
||||
|
||||
/// Horizontal disk of spiral arms in the XZ plane.
|
||||
/// One independent disk layer of spiral arms.
|
||||
///
|
||||
/// Disks are generated in the XZ plane and then tilted by [`Self::tilt`]
|
||||
/// (rotation around the X axis) and rotated by [`Self::rotation_offset`]
|
||||
/// (around the Y axis). Systems are placed between [`Self::inner_radius`]
|
||||
/// and [`Self::outer_radius`]; if `outer_radius` is 0 the galaxy-wide
|
||||
/// [`GalaxyParams::size`] is used instead.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DiskParams {
|
||||
pub count: usize,
|
||||
pub arms: u32,
|
||||
pub twist: f32,
|
||||
/// Tilt angle in radians (rotation around X axis).
|
||||
/// Capped at 45° so disks never become parallel to beams.
|
||||
/// 0 = flat in XZ plane, π/4 = maximum tilt.
|
||||
pub tilt: f32,
|
||||
/// Rotation offset around Y axis in radians.
|
||||
/// Rotates the disk in the horizontal plane, determining where the
|
||||
/// tilted disk's "high side" points.
|
||||
pub rotation_offset: f32,
|
||||
/// Minimum distance from the origin for systems in this disk.
|
||||
/// 0 = systems can start at the origin (overlapping the core).
|
||||
pub inner_radius: f32,
|
||||
/// Maximum distance from the origin. 0 = use [`GalaxyParams::size`].
|
||||
pub outer_radius: f32,
|
||||
}
|
||||
|
||||
impl Default for DiskParams {
|
||||
@@ -129,6 +170,10 @@ impl Default for DiskParams {
|
||||
count: DEFAULT_DISK_COUNT,
|
||||
arms: DEFAULT_DISK_ARMS,
|
||||
twist: DEFAULT_DISK_TWIST,
|
||||
tilt: 0.0,
|
||||
rotation_offset: 0.0,
|
||||
inner_radius: 0.0,
|
||||
outer_radius: 0.0, // means "use galaxy size"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,7 +234,8 @@ pub struct GalaxyParams {
|
||||
/// Overall galaxy extent — drives the disk radius.
|
||||
pub size: f32,
|
||||
pub core: CoreParams,
|
||||
pub disk: DiskParams,
|
||||
/// Independent disk layers. Always contains at least one entry.
|
||||
pub disks: Vec<DiskParams>,
|
||||
pub beam_top: BeamParams,
|
||||
pub beam_bottom: BeamParams,
|
||||
/// Monotonic counter — any mutation must bump this via [`Self::bump_generation`].
|
||||
@@ -204,7 +250,7 @@ impl Default for GalaxyParams {
|
||||
seed: DEFAULT_SEED,
|
||||
size: DEFAULT_SIZE,
|
||||
core: CoreParams::default(),
|
||||
disk: DiskParams::default(),
|
||||
disks: vec![DiskParams::default()],
|
||||
beam_top: BeamParams::default(),
|
||||
beam_bottom: BeamParams::default(),
|
||||
generation: 0,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user