Render asteroid belts as instanced jittered icospheres
Replace the flat-grey torus asteroid belt with a swarm of 60 individual rocks per belt — one shared jittered-icosphere mesh asset scaled, rotated, and palette-tinted per instance (5-entry greyscale/brown material set). - New GeneratedAsteroid data (position/scale/rotation/material_index) generated deterministically alongside the belt, preserving the same-seed-same-galaxy invariant - New build_asteroid_mesh + asteroid_material_palette helpers - ContentAssets drops belt_material, adds asteroid_mesh + asteroid_materials - spawn_system_contents signature simplified (no more &mut Assets<Mesh>) - Belt entity is now a no-mesh parent (Mineable/Identifiable/...) with rock meshes as children — sets the stage for per-asteroid mining - Bundles the POI component/marker scaffolding (poi.rs) and per-system contents generator (contents.rs: planets, stations, anomalies, stargates, gas clouds) that the asteroid rendering sits on top of - Tests: asteroid_generation_is_deterministic, asteroid_positions_stay_inside_annulus
This commit is contained in:
1219
apps/game/src/gameplay/galaxy_creation/contents.rs
Normal file
1219
apps/game/src/gameplay/galaxy_creation/contents.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,10 +6,14 @@
|
|||||||
//! live in [`super::ui`].
|
//! live in [`super::ui`].
|
||||||
|
|
||||||
mod axes;
|
mod axes;
|
||||||
|
mod contents;
|
||||||
mod params;
|
mod params;
|
||||||
|
mod poi;
|
||||||
mod selection;
|
mod selection;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
|
pub use poi::*;
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use rand::rngs::StdRng;
|
use rand::rngs::StdRng;
|
||||||
use rand::{Rng, SeedableRng};
|
use rand::{Rng, SeedableRng};
|
||||||
@@ -17,6 +21,7 @@ use rand::{Rng, SeedableRng};
|
|||||||
use crate::camera::apply_orbit_reset;
|
use crate::camera::apply_orbit_reset;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
pub use contents::{SystemContents, SystemContext, SystemSummary};
|
||||||
pub use params::{GalaxyParams, SelectedStar};
|
pub use params::{GalaxyParams, SelectedStar};
|
||||||
use params::{CORE_COUNT, NEAREST_NEIGHBOR_CONNECTIONS, SPACING_ATTEMPTS};
|
use params::{CORE_COUNT, NEAREST_NEIGHBOR_CONNECTIONS, SPACING_ATTEMPTS};
|
||||||
|
|
||||||
@@ -26,8 +31,14 @@ impl Plugin for GalaxyCreationPlugin {
|
|||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.init_resource::<GalaxyParams>()
|
app.init_resource::<GalaxyParams>()
|
||||||
.init_resource::<SelectedStar>()
|
.init_resource::<SelectedStar>()
|
||||||
.add_systems(OnEnter(AppState::GalaxyCreation), (setup_galaxy_scene, ui::setup_galaxy_ui))
|
.add_systems(
|
||||||
.add_systems(OnExit(AppState::GalaxyCreation), (despawn_galaxy_creation, reset_selection))
|
OnEnter(AppState::GalaxyCreation),
|
||||||
|
(setup_galaxy_scene, ui::setup_galaxy_ui),
|
||||||
|
)
|
||||||
|
.add_systems(
|
||||||
|
OnExit(AppState::GalaxyCreation),
|
||||||
|
(despawn_galaxy_creation, reset_selection),
|
||||||
|
)
|
||||||
.add_systems(
|
.add_systems(
|
||||||
Update,
|
Update,
|
||||||
(
|
(
|
||||||
@@ -59,15 +70,35 @@ pub struct GalaxyCreationSpawned;
|
|||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct GalaxyScene;
|
pub struct GalaxyScene;
|
||||||
|
|
||||||
/// A star system in the procedural galaxy.
|
/// A star system in the procedural galaxy. Acts as a parent entity for the
|
||||||
|
/// system's [`Star`] (visual marker) and all POI children (planets, belts,
|
||||||
|
/// stations, anomalies, stargates, gas clouds).
|
||||||
#[derive(Component, Debug)]
|
#[derive(Component, Debug)]
|
||||||
pub struct StarSystem {
|
pub struct StarSystem {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub faction: &'static str,
|
pub faction: &'static str,
|
||||||
pub security: f32,
|
pub security: f32,
|
||||||
|
/// Cached POI count for quick display in the info panel without walking
|
||||||
|
/// the child hierarchy. Updated whenever [`contents::SystemContents`] is
|
||||||
|
/// regenerated.
|
||||||
|
pub poi_count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The anchor celestial of a star system — its primary star. Distinct from
|
||||||
|
/// POIs (which are destinations within a system): a star is the body the
|
||||||
|
/// system is named after and the parent that planets/moons/belts orbit.
|
||||||
|
///
|
||||||
|
/// Requires components that describe its physical nature; mass-locks nearby
|
||||||
|
/// warp drives, emits light (used to compute habitable zones for orbiting
|
||||||
|
/// planets).
|
||||||
|
#[derive(Component, Debug, Clone, Copy, Reflect)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
#[require(Massive, Luminosity, MassLock, BoundingVolume, Identifiable)]
|
||||||
|
pub struct Star;
|
||||||
|
|
||||||
|
// POI components, markers, and events live in [`poi`] (re-exported as `pub use`).
|
||||||
|
|
||||||
// ── Faction palette (matches docs GalaxyScene) ──────────────────────────────
|
// ── Faction palette (matches docs GalaxyScene) ──────────────────────────────
|
||||||
|
|
||||||
const FACTIONS: &[(&str, [f32; 3])] = &[
|
const FACTIONS: &[(&str, [f32; 3])] = &[
|
||||||
@@ -86,16 +117,70 @@ fn setup_galaxy_scene(
|
|||||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||||
params: Res<GalaxyParams>,
|
params: Res<GalaxyParams>,
|
||||||
) {
|
) {
|
||||||
let systems = generate_galaxy(¶ms);
|
let (systems, contents, connections) = generate_galaxy(¶ms);
|
||||||
spawn_galaxy_scene(&mut commands, &mut meshes, &mut materials, &systems);
|
spawn_galaxy_scene(
|
||||||
|
&mut commands,
|
||||||
|
&mut meshes,
|
||||||
|
&mut materials,
|
||||||
|
&systems,
|
||||||
|
&contents,
|
||||||
|
&connections,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate the full galaxy layout (pure data — no Bevy types).
|
/// Generate the full galaxy layout (pure data — no Bevy types).
|
||||||
///
|
///
|
||||||
/// Faithful port of the TS reference in
|
/// Faithful port of the TS reference in
|
||||||
/// `apps/docs/src/prototypes/r3f/galaxy/GalaxyScene.tsx::generateGalaxy`.
|
/// `apps/docs/src/prototypes/r3f/galaxy/GalaxyScene.tsx::generateGalaxy`,
|
||||||
fn generate_galaxy(params: &GalaxyParams) -> Vec<GeneratedSystem> {
|
/// extended with per-system POI generation (planets, belts, stations,
|
||||||
|
/// anomalies, gas clouds, stargates).
|
||||||
|
///
|
||||||
|
/// Returns `(systems, contents_per_system, connections)`. The contents are
|
||||||
|
/// returned in parallel with the systems (same index) so the spawner can
|
||||||
|
/// attach them as children of each [`StarSystem`] entity.
|
||||||
|
fn generate_galaxy(
|
||||||
|
params: &GalaxyParams,
|
||||||
|
) -> (
|
||||||
|
Vec<GeneratedSystem>,
|
||||||
|
Vec<SystemContents>,
|
||||||
|
Vec<(usize, usize)>,
|
||||||
|
) {
|
||||||
let mut rng = StdRng::seed_from_u64(params.seed);
|
let mut rng = StdRng::seed_from_u64(params.seed);
|
||||||
|
let systems = generate_system_positions(params, &mut rng);
|
||||||
|
let connections = build_connections(&systems);
|
||||||
|
|
||||||
|
// Per-system contents (planets, belts, stations, anomalies, gas clouds).
|
||||||
|
let mut contents: Vec<SystemContents> = systems
|
||||||
|
.iter()
|
||||||
|
.map(|sys| {
|
||||||
|
let ctx = SystemContext {
|
||||||
|
id: &sys.id,
|
||||||
|
name: &sys.name,
|
||||||
|
faction: sys.faction,
|
||||||
|
security: sys.security,
|
||||||
|
is_core: sys.is_core,
|
||||||
|
};
|
||||||
|
contents::generate_system_contents(&mut rng, &ctx)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Stargates — paired across each connection.
|
||||||
|
let summaries: Vec<SystemSummary> = systems
|
||||||
|
.iter()
|
||||||
|
.map(|sys| SystemSummary {
|
||||||
|
id: sys.id.clone(),
|
||||||
|
name: sys.name.clone(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
contents::generate_stargates(&summaries, &mut contents, &connections);
|
||||||
|
|
||||||
|
(systems, contents, connections)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Position-only galaxy generation. Pure faithful port of the TS reference
|
||||||
|
/// in `apps/docs/src/prototypes/r3f/galaxy/GalaxyScene.tsx::generateGalaxy`.
|
||||||
|
/// Split out so [`generate_galaxy`] can compose it with POI generation.
|
||||||
|
fn generate_system_positions(params: &GalaxyParams, rng: &mut StdRng) -> Vec<GeneratedSystem> {
|
||||||
let mut systems: Vec<GeneratedSystem> = Vec::with_capacity(params.count);
|
let mut systems: Vec<GeneratedSystem> = Vec::with_capacity(params.count);
|
||||||
|
|
||||||
// Spacing scales with density: smaller for tight cores, larger spread overall.
|
// Spacing scales with density: smaller for tight cores, larger spread overall.
|
||||||
@@ -129,8 +214,16 @@ fn generate_galaxy(params: &GalaxyParams) -> Vec<GeneratedSystem> {
|
|||||||
} else {
|
} else {
|
||||||
rng.gen::<f32>().powf(0.62) * params.size
|
rng.gen::<f32>().powf(0.62) * params.size
|
||||||
};
|
};
|
||||||
let arm_count = if vertical { vertical_arms.max(1) } else { horizontal_arms };
|
let arm_count = if vertical {
|
||||||
let arm_twist = if vertical { params.vertical_twist } else { params.twist };
|
vertical_arms.max(1)
|
||||||
|
} else {
|
||||||
|
horizontal_arms
|
||||||
|
};
|
||||||
|
let arm_twist = if vertical {
|
||||||
|
params.vertical_twist
|
||||||
|
} else {
|
||||||
|
params.twist
|
||||||
|
};
|
||||||
let angle = if core {
|
let angle = if core {
|
||||||
rng.gen::<f32>() * std::f32::consts::TAU
|
rng.gen::<f32>() * std::f32::consts::TAU
|
||||||
} else {
|
} else {
|
||||||
@@ -147,7 +240,11 @@ fn generate_galaxy(params: &GalaxyParams) -> Vec<GeneratedSystem> {
|
|||||||
let vy = angle.sin() * r * 0.72 + (rng.gen::<f32>() - 0.5) * 12.0;
|
let vy = angle.sin() * r * 0.72 + (rng.gen::<f32>() - 0.5) * 12.0;
|
||||||
(vx, vy, vz)
|
(vx, vy, vz)
|
||||||
} else {
|
} else {
|
||||||
(angle.cos() * r, (rng.gen::<f32>() - 0.5) * 20.0, angle.sin() * r)
|
(
|
||||||
|
angle.cos() * r,
|
||||||
|
(rng.gen::<f32>() - 0.5) * 20.0,
|
||||||
|
angle.sin() * r,
|
||||||
|
)
|
||||||
};
|
};
|
||||||
let candidate = Vec3::new(x, y, z);
|
let candidate = Vec3::new(x, y, z);
|
||||||
final_radius = r;
|
final_radius = r;
|
||||||
@@ -170,8 +267,7 @@ fn generate_galaxy(params: &GalaxyParams) -> Vec<GeneratedSystem> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let security =
|
let security = ((1.0 - (final_radius / params.size) * 2.0) * 100.0).round() / 100.0;
|
||||||
((1.0 - (final_radius / params.size) * 2.0) * 100.0).round() / 100.0;
|
|
||||||
let name = if core {
|
let name = if core {
|
||||||
format!("COR-{}", 100 + index)
|
format!("COR-{}", 100 + index)
|
||||||
} else {
|
} else {
|
||||||
@@ -186,6 +282,7 @@ fn generate_galaxy(params: &GalaxyParams) -> Vec<GeneratedSystem> {
|
|||||||
faction_index,
|
faction_index,
|
||||||
color,
|
color,
|
||||||
security,
|
security,
|
||||||
|
is_core: core,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,9 +297,12 @@ struct GeneratedSystem {
|
|||||||
faction_index: usize,
|
faction_index: usize,
|
||||||
/// Per-system tint; currently the renderer uses per-faction materials indexed
|
/// Per-system tint; currently the renderer uses per-faction materials indexed
|
||||||
/// by `faction_index`, but this is kept for future per-system variation.
|
/// by `faction_index`, but this is kept for future per-system variation.
|
||||||
#[allow(dead_code)]
|
|
||||||
color: [f32; 3],
|
color: [f32; 3],
|
||||||
security: f32,
|
security: f32,
|
||||||
|
/// True for the [`CORE_COUNT`] Concord systems clustered near the origin.
|
||||||
|
/// Used by [`contents::generate_system_contents`] to bias planet / station
|
||||||
|
/// counts toward core systems.
|
||||||
|
is_core: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_galaxy_scene(
|
fn spawn_galaxy_scene(
|
||||||
@@ -210,10 +310,19 @@ fn spawn_galaxy_scene(
|
|||||||
meshes: &mut Assets<Mesh>,
|
meshes: &mut Assets<Mesh>,
|
||||||
materials: &mut Assets<StandardMaterial>,
|
materials: &mut Assets<StandardMaterial>,
|
||||||
systems: &[GeneratedSystem],
|
systems: &[GeneratedSystem],
|
||||||
|
contents: &[SystemContents],
|
||||||
|
connections: &[(usize, usize)],
|
||||||
) {
|
) {
|
||||||
// Shared meshes (Bevy 0.16 required-components model — no bundle needed).
|
debug_assert_eq!(
|
||||||
let star_mesh = meshes.add(Sphere::new(1.5).mesh().ico(3).unwrap());
|
systems.len(),
|
||||||
let core_star_mesh = meshes.add(Sphere::new(3.0).mesh().ico(4).unwrap());
|
contents.len(),
|
||||||
|
"systems and contents must be parallel arrays of the same length"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Shared star visuals (Bevy 0.16 required-components model — no bundle needed).
|
||||||
|
// Unit spheres, scaled per-faction at the spawn site.
|
||||||
|
let star_mesh = meshes.add(Sphere::new(1.0).mesh().ico(3).unwrap());
|
||||||
|
let core_star_mesh = meshes.add(Sphere::new(1.0).mesh().ico(4).unwrap());
|
||||||
|
|
||||||
let faction_materials: Vec<Handle<StandardMaterial>> = FACTIONS
|
let faction_materials: Vec<Handle<StandardMaterial>> = FACTIONS
|
||||||
.iter()
|
.iter()
|
||||||
@@ -233,45 +342,89 @@ fn spawn_galaxy_scene(
|
|||||||
..default()
|
..default()
|
||||||
});
|
});
|
||||||
|
|
||||||
let connections = build_connections(systems);
|
// Cached POI meshes/materials (one per type — entities scale via Transform).
|
||||||
|
let content_assets = contents::ContentAssets::new(meshes, materials);
|
||||||
|
|
||||||
// Parent group so all galaxy contents despawn together.
|
// Parent group so all galaxy contents despawn together.
|
||||||
commands
|
commands
|
||||||
.spawn((
|
.spawn((Transform::default(), GalaxyScene, GalaxyCreationSpawned))
|
||||||
Transform::default(),
|
|
||||||
GalaxyScene,
|
|
||||||
GalaxyCreationSpawned,
|
|
||||||
))
|
|
||||||
.with_children(|parent| {
|
.with_children(|parent| {
|
||||||
// XYZ reference axes through the origin.
|
// XYZ reference axes through the origin.
|
||||||
axes::spawn_axes(parent, meshes, materials);
|
axes::spawn_axes(parent, meshes, materials);
|
||||||
|
|
||||||
// Star systems.
|
// Star systems — each is a parent entity that owns its Star + POI children.
|
||||||
for sys in systems {
|
for (sys, sys_contents) in systems.iter().zip(contents.iter()) {
|
||||||
let (mesh, material) = if sys.faction_index == 0 && sys.position.length() < 40.0
|
let is_core = sys.faction_index == 0 && sys.is_core;
|
||||||
{
|
let (mesh, material, scale) = if is_core {
|
||||||
// Visual differentiation for the 7 Concord core systems.
|
// Visual differentiation for the 7 Concord core systems.
|
||||||
(core_star_mesh.clone(), faction_materials[0].clone())
|
(core_star_mesh.clone(), faction_materials[0].clone(), 1.1)
|
||||||
} else {
|
} else {
|
||||||
(star_mesh.clone(), faction_materials[sys.faction_index].clone())
|
(
|
||||||
|
star_mesh.clone(),
|
||||||
|
faction_materials[sys.faction_index].clone(),
|
||||||
|
1.0,
|
||||||
|
)
|
||||||
};
|
};
|
||||||
parent.spawn((
|
|
||||||
Mesh3d(mesh),
|
let poi_count = sys_contents.total();
|
||||||
MeshMaterial3d(material),
|
|
||||||
Transform::from_translation(sys.position),
|
parent
|
||||||
StarSystem {
|
.spawn((
|
||||||
id: sys.id.clone(),
|
Transform::from_translation(sys.position),
|
||||||
name: sys.name.clone(),
|
StarSystem {
|
||||||
faction: sys.faction,
|
id: sys.id.clone(),
|
||||||
security: sys.security,
|
name: sys.name.clone(),
|
||||||
},
|
faction: sys.faction,
|
||||||
));
|
security: sys.security,
|
||||||
|
poi_count,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.with_children(|sys_parent| {
|
||||||
|
// The anchor celestial — system's primary star.
|
||||||
|
let star_entity = sys_parent
|
||||||
|
.spawn((
|
||||||
|
Star,
|
||||||
|
Massive {
|
||||||
|
mass: if sys.is_core { 10000.0 } else { 5000.0 },
|
||||||
|
},
|
||||||
|
Luminosity {
|
||||||
|
value: if sys.is_core { 100.0 } else { 50.0 },
|
||||||
|
},
|
||||||
|
MassLock { radius: 5.0 },
|
||||||
|
BoundingVolume { radius: 1.5 },
|
||||||
|
Identifiable {
|
||||||
|
id: format!("{}-star", sys.id),
|
||||||
|
display_name: sys.name.clone(),
|
||||||
|
classification: Classification::Celestial,
|
||||||
|
},
|
||||||
|
Mesh3d(mesh),
|
||||||
|
MeshMaterial3d(material),
|
||||||
|
Transform::default().with_scale(Vec3::splat(scale)),
|
||||||
|
))
|
||||||
|
.id();
|
||||||
|
|
||||||
|
// POI children (planets, belts, stations, anomalies, stargates, gas).
|
||||||
|
let ctx = SystemContext {
|
||||||
|
id: &sys.id,
|
||||||
|
name: &sys.name,
|
||||||
|
faction: sys.faction,
|
||||||
|
security: sys.security,
|
||||||
|
is_core: sys.is_core,
|
||||||
|
};
|
||||||
|
contents::spawn_system_contents(
|
||||||
|
sys_parent,
|
||||||
|
&ctx,
|
||||||
|
sys_contents,
|
||||||
|
star_entity,
|
||||||
|
&content_assets,
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connections rendered as thin cylinders between linked systems.
|
// Connections rendered as thin cylinders between linked systems.
|
||||||
for (a, b) in connections {
|
for (a, b) in connections {
|
||||||
let from = systems[a].position;
|
let from = systems[*a].position;
|
||||||
let to = systems[b].position;
|
let to = systems[*b].position;
|
||||||
let delta = to - from;
|
let delta = to - from;
|
||||||
let length = delta.length();
|
let length = delta.length();
|
||||||
if length < 0.01 {
|
if length < 0.01 {
|
||||||
@@ -371,6 +524,13 @@ fn regenerate_galaxy_on_param_change(
|
|||||||
commands.entity(entity).despawn();
|
commands.entity(entity).despawn();
|
||||||
}
|
}
|
||||||
|
|
||||||
let systems = generate_galaxy(¶ms);
|
let (systems, contents, connections) = generate_galaxy(¶ms);
|
||||||
spawn_galaxy_scene(&mut commands, &mut meshes, &mut materials, &systems);
|
spawn_galaxy_scene(
|
||||||
|
&mut commands,
|
||||||
|
&mut meshes,
|
||||||
|
&mut materials,
|
||||||
|
&systems,
|
||||||
|
&contents,
|
||||||
|
&connections,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
488
apps/game/src/gameplay/galaxy_creation/poi.rs
Normal file
488
apps/game/src/gameplay/galaxy_creation/poi.rs
Normal file
@@ -0,0 +1,488 @@
|
|||||||
|
//! Points of interest (POIs): components, marker tags, and events.
|
||||||
|
//!
|
||||||
|
//! In Bevy, entity "capabilities" are expressed as **data components** that
|
||||||
|
//! systems query — not methods on objects. This file defines the data layout;
|
||||||
|
//! behavior lives in systems (`mining_system`, `scan_system`, …) that read and
|
||||||
|
//! write these components and react to events.
|
||||||
|
//!
|
||||||
|
//! ## Anatomy of a POI
|
||||||
|
//!
|
||||||
|
//! Every POI entity gets:
|
||||||
|
//!
|
||||||
|
//! 1. **One variant marker** (e.g. [`Planet`], [`AsteroidBelt`], [`Wormhole`]).
|
||||||
|
//! Markers use Bevy 0.16 `#[require(...)]` to declare the components they
|
||||||
|
//! are composed of. Bevy enforces this at spawn time — you can't make a
|
||||||
|
//! `Planet` without also providing `Orbital`, `Massive`, etc.
|
||||||
|
//! 2. **All required components** declared by the marker, either with explicit
|
||||||
|
//! values or `Default::default()`.
|
||||||
|
//! 3. **Optional capability components** added dynamically — e.g. a `Planet`
|
||||||
|
//! that gets colonized gains [`Inhabited`]; a [`Wormhole`] that destabilizes
|
||||||
|
//! loses [`Transit`].
|
||||||
|
//!
|
||||||
|
//! World-space position comes from Bevy's built-in `Transform`/`GlobalTransform`
|
||||||
|
//! — there is no separate `Positioned` component.
|
||||||
|
//!
|
||||||
|
//! ## Why components, not traits
|
||||||
|
//!
|
||||||
|
//! - **Queries are how Bevy dispatches**: `Query<&Mineable, With<AsteroidBelt>>`
|
||||||
|
//! is the equivalent of a downcast, but vectorized, cache-friendly, and
|
||||||
|
//! parallelizable across the schedule.
|
||||||
|
//! - **Composition over inheritance**: a POI's behavior is the sum of the
|
||||||
|
//! components attached to it. Adding/removing a capability at runtime is
|
||||||
|
//! `commands.insert(...)` / `commands.remove::<...>()`.
|
||||||
|
//! - **State changes flow through events** ([`Mined`], [`Scanned`], [`Depleted`],
|
||||||
|
//! …). Systems react rather than invoke. Multiple systems can observe the
|
||||||
|
//! same event (UI, audio, mission tracking, achievements) without coupling.
|
||||||
|
//! - **`Reflect` enables save/load, hot-reload, and editor inspectors** for
|
||||||
|
//! free. Trait objects can't do this without manual plumbing.
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy::reflect::Reflect;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
// ─── Supporting enums ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// EVE-style sensor quartet — drives which probe type can resolve a signal.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Reflect, Default)]
|
||||||
|
pub enum SignalKind {
|
||||||
|
#[default]
|
||||||
|
Infrared,
|
||||||
|
Gravimetric,
|
||||||
|
Radar,
|
||||||
|
Magnetometric,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Difficulty of resolving a scannable target with probes.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Reflect, Default)]
|
||||||
|
pub enum Difficulty {
|
||||||
|
#[default]
|
||||||
|
Trivial,
|
||||||
|
Easy,
|
||||||
|
Moderate,
|
||||||
|
Hard,
|
||||||
|
/// Requires multi-probe triangulation.
|
||||||
|
Expert,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// High-level classification shown on HUD / sensor panels.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Reflect, Default)]
|
||||||
|
pub enum Classification {
|
||||||
|
#[default]
|
||||||
|
Celestial,
|
||||||
|
Structure,
|
||||||
|
Anomaly,
|
||||||
|
Signature,
|
||||||
|
Ship,
|
||||||
|
Debris,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Damage kind for environmental hazards.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Reflect, Default)]
|
||||||
|
pub enum DamageKind {
|
||||||
|
#[default]
|
||||||
|
Radiation,
|
||||||
|
Emp,
|
||||||
|
Heat,
|
||||||
|
Kinetic,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Harvestable gas varieties.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Reflect, Default)]
|
||||||
|
pub enum GasKind {
|
||||||
|
#[default]
|
||||||
|
Hydrogen,
|
||||||
|
Helium,
|
||||||
|
Nitrogen,
|
||||||
|
/// Rare/anomalous gas — typically only found in anomalies.
|
||||||
|
Exotic,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What triggers an NPC spawn at a POI.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Reflect, Default)]
|
||||||
|
pub enum SpawnTrigger {
|
||||||
|
/// Player crosses the POI's "aggro bubble".
|
||||||
|
#[default]
|
||||||
|
OnPlayerEnter,
|
||||||
|
/// World tick — happens regardless of player presence.
|
||||||
|
WorldTick,
|
||||||
|
/// Triggered by an event or mission script.
|
||||||
|
EventDriven,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kind of interaction a player can initiate with an [`Interactable`] POI.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Reflect, Default)]
|
||||||
|
pub enum InteractionKind {
|
||||||
|
#[default]
|
||||||
|
Dialogue,
|
||||||
|
Lore,
|
||||||
|
Decode,
|
||||||
|
Investigate,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Placeholder types (promote to real modules when consumers arrive) ──────
|
||||||
|
|
||||||
|
pub type SystemId = String;
|
||||||
|
|
||||||
|
// ─── 1. Spatial / physical ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// A body that orbits a parent entity (star, planet, moon, belt center).
|
||||||
|
#[derive(Component, Debug, Clone, Reflect)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct Orbital {
|
||||||
|
pub semi_major_axis: f32,
|
||||||
|
pub period: f32,
|
||||||
|
pub phase: f32,
|
||||||
|
/// Parent entity — the body being orbited. Override
|
||||||
|
/// `Default::default()` (`Entity::PLACEHOLDER`) at spawn time.
|
||||||
|
pub parent: Entity,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Orbital {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
semi_major_axis: 0.0,
|
||||||
|
period: 0.0,
|
||||||
|
phase: 0.0,
|
||||||
|
parent: Entity::PLACEHOLDER,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spherical bound — used for collision, camera framing, sensor-blip scaling.
|
||||||
|
#[derive(Component, Debug, Clone, Reflect, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct BoundingVolume {
|
||||||
|
pub radius: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Massive body — drives gravity wells and (with [`MassLock`]) warp disruption.
|
||||||
|
#[derive(Component, Debug, Clone, Reflect, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct Massive {
|
||||||
|
pub mass: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 2. Detection / sensors ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Passive sensor profile of a POI.
|
||||||
|
#[derive(Component, Debug, Clone, Reflect, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct SensorSignature {
|
||||||
|
pub signature_radius: f32,
|
||||||
|
pub signal_type: SignalKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A POI that can be pinned down with scan probes.
|
||||||
|
#[derive(Component, Debug, Clone, Reflect, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct Scannable {
|
||||||
|
pub difficulty: Difficulty,
|
||||||
|
/// If true, shipboard sensors aren't enough — a probe must be launched.
|
||||||
|
pub requires_probe: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stable identity + display metadata for HUDs and target lists.
|
||||||
|
#[derive(Component, Debug, Clone, Reflect, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct Identifiable {
|
||||||
|
pub id: String,
|
||||||
|
pub display_name: String,
|
||||||
|
pub classification: Classification,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 3. Navigation / approach ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// A POI the ship can warp to.
|
||||||
|
#[derive(Component, Debug, Clone, Reflect, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct WarpTarget {
|
||||||
|
pub warp_distance: f32,
|
||||||
|
pub landing_radius: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A POI the ship can dock with.
|
||||||
|
#[derive(Component, Debug, Clone, Reflect, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct Dockable {
|
||||||
|
pub dock_range: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A POI that moves the ship to another system (wormhole, jump gate).
|
||||||
|
#[derive(Component, Debug, Clone, Reflect, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct Transit {
|
||||||
|
pub destination: Option<SystemId>,
|
||||||
|
/// `None` = permanent. `Some(d)` = expires after `d`.
|
||||||
|
pub lifetime: Option<Duration>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Massive enough to prevent warp within a radius. Stars, gas giants, etc.
|
||||||
|
#[derive(Component, Debug, Clone, Reflect, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct MassLock {
|
||||||
|
pub radius: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 4. Resource / harvesting ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Solid resource deposit — ore in asteroids, minerals in planetoids.
|
||||||
|
///
|
||||||
|
/// Depletion happens via [`Mined`] events in a system, not via a method.
|
||||||
|
#[derive(Component, Debug, Clone, Reflect, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct Mineable {
|
||||||
|
//Add loot table later
|
||||||
|
pub yield_remaining: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gaseous resource — scoopable by harvesters.
|
||||||
|
#[derive(Component, Debug, Clone, Reflect, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct Harvestable {
|
||||||
|
pub resource: GasKind,
|
||||||
|
/// Units per second a harvester can extract under ideal conditions.
|
||||||
|
pub flow_rate: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wreck or ruin that can be stripped for components.
|
||||||
|
#[derive(Component, Debug, Clone, Reflect, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct Salvageable {
|
||||||
|
// add loot table later
|
||||||
|
pub condition: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A POI whose resources can run out. Usually composed with one of the
|
||||||
|
/// harvesting components above.
|
||||||
|
#[derive(Component, Debug, Clone, Reflect, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct Exhaustible {
|
||||||
|
pub depleted: bool,
|
||||||
|
/// `None` = never respawns. `Some(d)` = replenished after `d`.
|
||||||
|
pub respawn_after: Option<Duration>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 5. Hazards ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Deals passive damage to anything within range.
|
||||||
|
#[derive(Component, Debug, Clone, Reflect, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct Hazardous {
|
||||||
|
pub damage_per_sec: f32,
|
||||||
|
pub damage_type: DamageKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reduces effective sensor / probe range for nearby ships.
|
||||||
|
#[derive(Component, Debug, Clone, Reflect, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct SensorJamming {
|
||||||
|
/// Strength of the jamming field. Higher = shorter effective range.
|
||||||
|
pub strength: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tag component — entity is currently cloaked. Pair with [`CloakField`] for
|
||||||
|
/// the decloak radius.
|
||||||
|
#[derive(Component, Debug, Clone, Copy, Reflect, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct Cloaked;
|
||||||
|
|
||||||
|
#[derive(Component, Debug, Clone, Reflect, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct CloakField {
|
||||||
|
/// Distance at which the cloak flickers / drops.
|
||||||
|
pub decloak_radius: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 6. Habitability / story ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Emits light — typically a star, used to compute habitable zones for
|
||||||
|
/// orbiting planets.
|
||||||
|
#[derive(Component, Debug, Clone, Reflect, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct Luminosity {
|
||||||
|
pub value: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tag — this body can support life.
|
||||||
|
#[derive(Component, Debug, Clone, Copy, Reflect, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct Habitable;
|
||||||
|
|
||||||
|
/// Tag — this body currently hosts a population.
|
||||||
|
#[derive(Component, Debug, Clone, Reflect, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct Inhabited {
|
||||||
|
pub population: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Player can trigger a non-combat interaction (dialogue, lore, decode, …).
|
||||||
|
#[derive(Component, Debug, Clone, Reflect, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct Interactable {
|
||||||
|
pub kind: InteractionKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 7. World simulation ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Spawns hostile NPCs under some condition.
|
||||||
|
#[derive(Component, Debug, Clone, Reflect, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct SpawnsHostiles; //implement later
|
||||||
|
|
||||||
|
/// A POI with a finite lifespan — anomaly, wormhole, world event.
|
||||||
|
#[derive(Component, Debug, Clone, Reflect, Default)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct EventTimer {
|
||||||
|
pub remaining: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── POI variant markers ────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Replace the old `PointOfInterest` enum. Each marker declares the components
|
||||||
|
// it requires via `#[require(...)]` — Bevy enforces composition at spawn time.
|
||||||
|
//
|
||||||
|
// Spawn example:
|
||||||
|
//
|
||||||
|
// commands.spawn((
|
||||||
|
// Planet,
|
||||||
|
// Orbital { semi_major_axis: 5.0, period: 365.0, phase: 0.0, parent: sun },
|
||||||
|
// BoundingVolume { radius: 6371.0 },
|
||||||
|
// Massive { mass: 5.97e24 },
|
||||||
|
// Identifiable { id: "sol-3".into(), display_name: "Earth".into(), ..default() },
|
||||||
|
// ));
|
||||||
|
//
|
||||||
|
// `WarpTarget`, `MassLock`, etc. are auto-inserted via `Default` since the
|
||||||
|
// marker requires them.
|
||||||
|
//
|
||||||
|
// Note: stars are NOT POIs — they are the anchor body of a star system and
|
||||||
|
// live next to [`super::StarSystem`] in `mod.rs`.
|
||||||
|
|
||||||
|
#[derive(Component, Debug, Clone, Copy, Reflect)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
#[require(Orbital, Massive, BoundingVolume, WarpTarget, MassLock, Identifiable)]
|
||||||
|
pub struct Planet;
|
||||||
|
|
||||||
|
#[derive(Component, Debug, Clone, Copy, Reflect)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
#[require(
|
||||||
|
Orbital,
|
||||||
|
BoundingVolume,
|
||||||
|
WarpTarget,
|
||||||
|
Mineable,
|
||||||
|
Exhaustible,
|
||||||
|
Identifiable
|
||||||
|
)]
|
||||||
|
pub struct AsteroidBelt;
|
||||||
|
|
||||||
|
#[derive(Component, Debug, Clone, Copy, Reflect)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
#[require(BoundingVolume, WarpTarget, Harvestable, Exhaustible, Identifiable)]
|
||||||
|
pub struct GasCloud;
|
||||||
|
|
||||||
|
#[derive(Component, Debug, Clone, Copy, Reflect)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
#[require(
|
||||||
|
BoundingVolume,
|
||||||
|
WarpTarget,
|
||||||
|
SensorSignature,
|
||||||
|
Scannable,
|
||||||
|
EventTimer,
|
||||||
|
Identifiable
|
||||||
|
)]
|
||||||
|
pub struct Anomaly;
|
||||||
|
|
||||||
|
#[derive(Component, Debug, Clone, Copy, Reflect)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
#[require(
|
||||||
|
BoundingVolume,
|
||||||
|
WarpTarget,
|
||||||
|
Transit,
|
||||||
|
SensorSignature,
|
||||||
|
EventTimer,
|
||||||
|
Identifiable
|
||||||
|
)]
|
||||||
|
pub struct Wormhole;
|
||||||
|
|
||||||
|
/// A constructed jump gate — paired with another stargate in the destination
|
||||||
|
/// system. Distinct from [`Wormhole`] (which is a natural, transient phenomenon).
|
||||||
|
#[derive(Component, Debug, Clone, Copy, Reflect)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
#[require(BoundingVolume, WarpTarget, Transit, Identifiable)]
|
||||||
|
pub struct Stargate;
|
||||||
|
|
||||||
|
/// A functioning, inhabited station (docking, market, missions).
|
||||||
|
/// Distinct from [`DerelictStation`] which is a wreck you can salvage.
|
||||||
|
#[derive(Component, Debug, Clone, Copy, Reflect)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
#[require(BoundingVolume, WarpTarget, Dockable, Identifiable)]
|
||||||
|
pub struct Station;
|
||||||
|
|
||||||
|
#[derive(Component, Debug, Clone, Copy, Reflect)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
#[require(
|
||||||
|
BoundingVolume,
|
||||||
|
WarpTarget,
|
||||||
|
SensorSignature,
|
||||||
|
Scannable,
|
||||||
|
Salvageable,
|
||||||
|
Identifiable
|
||||||
|
)]
|
||||||
|
pub struct DerelictStation;
|
||||||
|
|
||||||
|
// ─── Events ─────────────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// State changes that should be observed by other systems. Prefer events over
|
||||||
|
// direct mutation when:
|
||||||
|
// - Multiple systems might react (UI, audio, mission tracking, achievements).
|
||||||
|
// - You want to decouple the producer from the consumer.
|
||||||
|
|
||||||
|
/// A miner extracted `amount` from `deposit`. Mining systems read this and
|
||||||
|
/// mutate `&mut Mineable`; UI plays the yield popup; audio plays the laser hit.
|
||||||
|
#[derive(Event, Debug, Clone)]
|
||||||
|
pub struct Mined {
|
||||||
|
pub deposit: Entity,
|
||||||
|
pub miner: Entity,
|
||||||
|
pub amount: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Event, Debug, Clone)]
|
||||||
|
pub struct Harvested {
|
||||||
|
pub source: Entity,
|
||||||
|
pub harvester: Entity,
|
||||||
|
pub kind: GasKind,
|
||||||
|
pub amount: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Event, Debug, Clone)]
|
||||||
|
pub struct Salvaged {
|
||||||
|
pub target: Entity,
|
||||||
|
pub salvager: Entity,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A scan completed successfully — the target is now revealed to the scanner.
|
||||||
|
#[derive(Event, Debug, Clone)]
|
||||||
|
pub struct Scanned {
|
||||||
|
pub target: Entity,
|
||||||
|
pub scanner: Entity,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A resource deposit (or anomaly, or wormhole) has been fully consumed.
|
||||||
|
/// Systems react by clearing blips, queueing respawn timers, etc.
|
||||||
|
#[derive(Event, Debug, Clone)]
|
||||||
|
pub struct Depleted {
|
||||||
|
pub entity: Entity,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Event, Debug, Clone)]
|
||||||
|
pub struct Docked {
|
||||||
|
pub ship: Entity,
|
||||||
|
pub station: Entity,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A ship used a [`Transit`] POI (wormhole, jump gate) to change systems.
|
||||||
|
#[derive(Event, Debug, Clone)]
|
||||||
|
pub struct Transited {
|
||||||
|
pub traveler: Entity,
|
||||||
|
pub gate: Entity,
|
||||||
|
pub destination: SystemId,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user