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:
2026-06-06 17:57:05 -04:00
parent ef07880ca3
commit 4240c2b2ef
3 changed files with 1910 additions and 43 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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(&params); let (systems, contents, connections) = generate_galaxy(&params);
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),
parent
.spawn((
Transform::from_translation(sys.position), Transform::from_translation(sys.position),
StarSystem { StarSystem {
id: sys.id.clone(), id: sys.id.clone(),
name: sys.name.clone(), name: sys.name.clone(),
faction: sys.faction, faction: sys.faction,
security: sys.security, 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(&params); let (systems, contents, connections) = generate_galaxy(&params);
spawn_galaxy_scene(&mut commands, &mut meshes, &mut materials, &systems); spawn_galaxy_scene(
&mut commands,
&mut meshes,
&mut materials,
&systems,
&contents,
&connections,
);
} }

View 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,
}