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`].
|
||||
|
||||
mod axes;
|
||||
mod contents;
|
||||
mod params;
|
||||
mod poi;
|
||||
mod selection;
|
||||
mod ui;
|
||||
|
||||
pub use poi::*;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use rand::rngs::StdRng;
|
||||
use rand::{Rng, SeedableRng};
|
||||
@@ -17,6 +21,7 @@ use rand::{Rng, SeedableRng};
|
||||
use crate::camera::apply_orbit_reset;
|
||||
use crate::state::AppState;
|
||||
|
||||
pub use contents::{SystemContents, SystemContext, SystemSummary};
|
||||
pub use params::{GalaxyParams, SelectedStar};
|
||||
use params::{CORE_COUNT, NEAREST_NEIGHBOR_CONNECTIONS, SPACING_ATTEMPTS};
|
||||
|
||||
@@ -26,8 +31,14 @@ impl Plugin for GalaxyCreationPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<GalaxyParams>()
|
||||
.init_resource::<SelectedStar>()
|
||||
.add_systems(OnEnter(AppState::GalaxyCreation), (setup_galaxy_scene, ui::setup_galaxy_ui))
|
||||
.add_systems(OnExit(AppState::GalaxyCreation), (despawn_galaxy_creation, reset_selection))
|
||||
.add_systems(
|
||||
OnEnter(AppState::GalaxyCreation),
|
||||
(setup_galaxy_scene, ui::setup_galaxy_ui),
|
||||
)
|
||||
.add_systems(
|
||||
OnExit(AppState::GalaxyCreation),
|
||||
(despawn_galaxy_creation, reset_selection),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
@@ -59,15 +70,35 @@ pub struct GalaxyCreationSpawned;
|
||||
#[derive(Component)]
|
||||
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)]
|
||||
pub struct StarSystem {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub faction: &'static str,
|
||||
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) ──────────────────────────────
|
||||
|
||||
const FACTIONS: &[(&str, [f32; 3])] = &[
|
||||
@@ -86,16 +117,70 @@ fn setup_galaxy_scene(
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
params: Res<GalaxyParams>,
|
||||
) {
|
||||
let systems = generate_galaxy(¶ms);
|
||||
spawn_galaxy_scene(&mut commands, &mut meshes, &mut materials, &systems);
|
||||
let (systems, contents, connections) = generate_galaxy(¶ms);
|
||||
spawn_galaxy_scene(
|
||||
&mut commands,
|
||||
&mut meshes,
|
||||
&mut materials,
|
||||
&systems,
|
||||
&contents,
|
||||
&connections,
|
||||
);
|
||||
}
|
||||
|
||||
/// Generate the full galaxy layout (pure data — no Bevy types).
|
||||
///
|
||||
/// Faithful port of the TS reference in
|
||||
/// `apps/docs/src/prototypes/r3f/galaxy/GalaxyScene.tsx::generateGalaxy`.
|
||||
fn generate_galaxy(params: &GalaxyParams) -> Vec<GeneratedSystem> {
|
||||
/// `apps/docs/src/prototypes/r3f/galaxy/GalaxyScene.tsx::generateGalaxy`,
|
||||
/// 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 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);
|
||||
|
||||
// Spacing scales with density: smaller for tight cores, larger spread overall.
|
||||
@@ -129,8 +214,16 @@ fn generate_galaxy(params: &GalaxyParams) -> Vec<GeneratedSystem> {
|
||||
} else {
|
||||
rng.gen::<f32>().powf(0.62) * params.size
|
||||
};
|
||||
let arm_count = if vertical { vertical_arms.max(1) } else { horizontal_arms };
|
||||
let arm_twist = if vertical { params.vertical_twist } else { params.twist };
|
||||
let arm_count = if vertical {
|
||||
vertical_arms.max(1)
|
||||
} else {
|
||||
horizontal_arms
|
||||
};
|
||||
let arm_twist = if vertical {
|
||||
params.vertical_twist
|
||||
} else {
|
||||
params.twist
|
||||
};
|
||||
let angle = if core {
|
||||
rng.gen::<f32>() * std::f32::consts::TAU
|
||||
} 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;
|
||||
(vx, vy, vz)
|
||||
} 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);
|
||||
final_radius = r;
|
||||
@@ -170,8 +267,7 @@ fn generate_galaxy(params: &GalaxyParams) -> Vec<GeneratedSystem> {
|
||||
}
|
||||
}
|
||||
|
||||
let security =
|
||||
((1.0 - (final_radius / params.size) * 2.0) * 100.0).round() / 100.0;
|
||||
let security = ((1.0 - (final_radius / params.size) * 2.0) * 100.0).round() / 100.0;
|
||||
let name = if core {
|
||||
format!("COR-{}", 100 + index)
|
||||
} else {
|
||||
@@ -186,6 +282,7 @@ fn generate_galaxy(params: &GalaxyParams) -> Vec<GeneratedSystem> {
|
||||
faction_index,
|
||||
color,
|
||||
security,
|
||||
is_core: core,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -200,9 +297,12 @@ struct GeneratedSystem {
|
||||
faction_index: usize,
|
||||
/// Per-system tint; currently the renderer uses per-faction materials indexed
|
||||
/// by `faction_index`, but this is kept for future per-system variation.
|
||||
#[allow(dead_code)]
|
||||
color: [f32; 3],
|
||||
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(
|
||||
@@ -210,10 +310,19 @@ fn spawn_galaxy_scene(
|
||||
meshes: &mut Assets<Mesh>,
|
||||
materials: &mut Assets<StandardMaterial>,
|
||||
systems: &[GeneratedSystem],
|
||||
contents: &[SystemContents],
|
||||
connections: &[(usize, usize)],
|
||||
) {
|
||||
// Shared meshes (Bevy 0.16 required-components model — no bundle needed).
|
||||
let star_mesh = meshes.add(Sphere::new(1.5).mesh().ico(3).unwrap());
|
||||
let core_star_mesh = meshes.add(Sphere::new(3.0).mesh().ico(4).unwrap());
|
||||
debug_assert_eq!(
|
||||
systems.len(),
|
||||
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
|
||||
.iter()
|
||||
@@ -233,45 +342,89 @@ fn spawn_galaxy_scene(
|
||||
..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.
|
||||
commands
|
||||
.spawn((
|
||||
Transform::default(),
|
||||
GalaxyScene,
|
||||
GalaxyCreationSpawned,
|
||||
))
|
||||
.spawn((Transform::default(), GalaxyScene, GalaxyCreationSpawned))
|
||||
.with_children(|parent| {
|
||||
// XYZ reference axes through the origin.
|
||||
axes::spawn_axes(parent, meshes, materials);
|
||||
|
||||
// Star systems.
|
||||
for sys in systems {
|
||||
let (mesh, material) = if sys.faction_index == 0 && sys.position.length() < 40.0
|
||||
{
|
||||
// Star systems — each is a parent entity that owns its Star + POI children.
|
||||
for (sys, sys_contents) in systems.iter().zip(contents.iter()) {
|
||||
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.
|
||||
(core_star_mesh.clone(), faction_materials[0].clone())
|
||||
(core_star_mesh.clone(), faction_materials[0].clone(), 1.1)
|
||||
} 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),
|
||||
MeshMaterial3d(material),
|
||||
Transform::from_translation(sys.position),
|
||||
StarSystem {
|
||||
id: sys.id.clone(),
|
||||
name: sys.name.clone(),
|
||||
faction: sys.faction,
|
||||
security: sys.security,
|
||||
},
|
||||
));
|
||||
|
||||
let poi_count = sys_contents.total();
|
||||
|
||||
parent
|
||||
.spawn((
|
||||
Transform::from_translation(sys.position),
|
||||
StarSystem {
|
||||
id: sys.id.clone(),
|
||||
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.
|
||||
for (a, b) in connections {
|
||||
let from = systems[a].position;
|
||||
let to = systems[b].position;
|
||||
let from = systems[*a].position;
|
||||
let to = systems[*b].position;
|
||||
let delta = to - from;
|
||||
let length = delta.length();
|
||||
if length < 0.01 {
|
||||
@@ -371,6 +524,13 @@ fn regenerate_galaxy_on_param_change(
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
|
||||
let systems = generate_galaxy(¶ms);
|
||||
spawn_galaxy_scene(&mut commands, &mut meshes, &mut materials, &systems);
|
||||
let (systems, contents, connections) = generate_galaxy(¶ms);
|
||||
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