use std::collections::BTreeSet;
use std::f32::consts::{PI, TAU};
use bevy_ecs::change_detection::DetectChangesMut;
use bevy_ecs::entity::Entity;
use bevy_ecs::prelude::{Commands, Component, Or, Query, Res, ResMut, With};
use serde::{Deserialize, Serialize};
use super::sim_math;
use super::{
EventQueue, FrameTime, GlobalTransform, MeshRenderer, PhysicsWorld,
RandomSeed, SceneId,
};
use crate::Transform;
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize,
)]
pub enum Easing {
#[default]
Linear,
QuadIn,
QuadOut,
QuadInOut,
CubicOut,
SineInOut,
BackOut,
BounceOut,
}
impl Easing {
#[must_use]
pub fn apply(self, t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
match self {
Self::Linear => t,
Self::QuadIn => t * t,
Self::QuadOut => 1.0 - (1.0 - t) * (1.0 - t),
Self::QuadInOut => {
if t < 0.5 {
2.0 * t * t
} else {
let u = -2.0 * t + 2.0;
1.0 - u * u / 2.0
}
}
Self::CubicOut => 1.0 - (1.0 - t) * (1.0 - t) * (1.0 - t),
Self::SineInOut => -(sim_math::sin_cos(PI * t).1 - 1.0) / 2.0,
Self::BackOut => {
let c1 = 1.70158;
let u = t - 1.0;
1.0 + (c1 + 1.0) * u * u * u + c1 * u * u
}
Self::BounceOut => {
let (n, d) = (7.5625, 2.75);
if t < 1.0 / d {
n * t * t
} else if t < 2.0 / d {
let t = t - 1.5 / d;
n * t * t + 0.75
} else if t < 2.5 / d {
let t = t - 2.25 / d;
n * t * t + 0.9375
} else {
let t = t - 2.625 / d;
n * t * t + 0.984_375
}
}
}
}
}
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize,
)]
pub enum TweenProperty {
#[default]
Position,
Rotation,
Scale,
}
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize,
)]
pub enum TweenRepeat {
#[default]
Once,
Loop,
PingPong,
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct Tween {
pub property: TweenProperty,
pub from: [f32; 3],
pub to: [f32; 3],
pub duration: f32,
pub delay: f32,
pub easing: Easing,
pub repeat: TweenRepeat,
#[serde(skip)]
pub elapsed: f32,
}
impl Default for Tween {
fn default() -> Self {
Self {
property: TweenProperty::Position,
from: [0.0; 3],
to: [0.0, 1.0, 0.0],
duration: 1.0,
delay: 0.0,
easing: Easing::QuadInOut,
repeat: TweenRepeat::PingPong,
elapsed: 0.0,
}
}
}
impl Tween {
#[must_use]
pub fn progress(&self) -> f32 {
if self.duration <= 0.0 {
return 1.0;
}
let played = (self.elapsed - self.delay).max(0.0) / self.duration;
match self.repeat {
TweenRepeat::Once => played.min(1.0),
TweenRepeat::Loop => played.fract(),
TweenRepeat::PingPong => {
let phase = played % 2.0;
if phase > 1.0 {
2.0 - phase
} else {
phase
}
}
}
}
#[must_use]
pub fn finished(&self) -> bool {
self.repeat == TweenRepeat::Once
&& self.elapsed >= self.delay + self.duration
}
#[must_use]
pub fn sample(&self) -> [f32; 3] {
let eased = self.easing.apply(self.progress());
std::array::from_fn(|axis| {
self.from[axis] + (self.to[axis] - self.from[axis]) * eased
})
}
pub fn restart(&mut self) {
self.elapsed = 0.0;
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct SoundEvent {
pub entity: Entity,
pub clip: String,
pub volume: f32,
}
#[derive(Component, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct SoundCue {
pub clip: String,
pub volume: f32,
pub on_collision: bool,
#[serde(skip)]
pub triggered: bool,
#[serde(skip)]
pub touching: bool,
}
impl Default for SoundCue {
fn default() -> Self {
Self {
clip: String::new(),
volume: 1.0,
on_collision: true,
triggered: false,
touching: false,
}
}
}
impl SoundCue {
pub fn trigger(&mut self) {
self.triggered = true;
}
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct BurstEmitter {
pub count: u32,
pub speed: f32,
pub lifetime: f32,
pub particle_scale: f32,
pub gravity: f32,
pub on_collision: bool,
pub rate: f32,
pub area: [f32; 3],
pub stretch: f32,
#[serde(skip)]
pub pending: f32,
#[serde(skip)]
pub triggered: bool,
#[serde(skip)]
pub touching: bool,
}
impl Default for BurstEmitter {
fn default() -> Self {
Self {
count: 12,
speed: 3.0,
lifetime: 0.6,
particle_scale: 0.15,
gravity: 9.81,
on_collision: true,
rate: 0.0,
area: [0.0; 3],
stretch: 1.0,
pending: 0.0,
triggered: false,
touching: false,
}
}
}
impl BurstEmitter {
pub fn trigger(&mut self) {
self.triggered = true;
}
}
#[derive(Component, Clone, Copy, Debug, PartialEq)]
pub struct BurstParticle {
pub velocity: [f32; 3],
pub gravity: f32,
pub remaining: f32,
pub lifetime: f32,
pub scale: [f32; 3],
}
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize,
)]
pub enum HudAnchor {
#[default]
TopLeft,
Top,
TopRight,
Center,
BottomLeft,
Bottom,
BottomRight,
}
#[derive(Component, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct HudElement {
pub text: String,
pub anchor: HudAnchor,
pub offset: [f32; 2],
pub font_size: f32,
pub color: [f32; 4],
pub button: bool,
pub requires: Option<String>,
pub camera: Option<String>,
}
impl Default for HudElement {
fn default() -> Self {
Self {
text: "Text".into(),
anchor: HudAnchor::TopLeft,
offset: [16.0, 16.0],
font_size: 18.0,
color: [1.0; 4],
button: false,
requires: None,
camera: None,
}
}
}
#[derive(Component, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct Counter {
pub name: String,
pub value: i32,
pub target: Option<i32>,
}
impl Default for Counter {
fn default() -> Self {
Self {
name: "score".into(),
value: 0,
target: None,
}
}
}
impl Counter {
#[must_use]
pub fn complete(&self) -> bool {
self.target.is_some_and(|target| self.value >= target)
}
}
#[derive(Component, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct Pickup {
pub counter: String,
pub value: i32,
pub requires: Option<String>,
pub collected: bool,
}
impl Default for Pickup {
fn default() -> Self {
Self {
counter: "score".into(),
value: 1,
requires: None,
collected: false,
}
}
}
pub(crate) fn find_counter<'a, C: std::ops::Deref<Target = Counter>>(
counters: impl Iterator<Item = (C, Option<&'a SceneId>)>,
name: &str,
) -> Option<C> {
counters
.filter(|(counter, _)| counter.name == name)
.min_by_key(|(_, id)| id.map(|id| id.0))
.map(|(counter, _)| counter)
}
#[must_use]
pub fn hud_text<'a>(
text: &str,
counters: impl Iterator<Item = (&'a Counter, Option<&'a SceneId>)> + Clone,
) -> String {
let mut out = String::with_capacity(text.len());
let mut rest = text;
while let Some(open) = rest.find('{') {
out.push_str(&rest[..open]);
let after = &rest[open + 1..];
let counter = after.find('}').and_then(|close| {
find_counter(counters.clone(), &after[..close])
.map(|counter| (counter.value, close))
});
match counter {
Some((value, close)) => {
out.push_str(&value.to_string());
rest = &after[close + 1..];
}
None => {
out.push('{');
rest = after;
}
}
}
out.push_str(rest);
out
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct SceneBackground {
pub color: [f32; 4],
}
impl Default for SceneBackground {
fn default() -> Self {
Self {
color: super::RenderSettings::default().background_color,
}
}
}
pub(super) fn apply_scene_background(
settings: Option<ResMut<super::RenderSettings>>,
backgrounds: Query<(&SceneBackground, Option<&SceneId>)>,
) {
let first = backgrounds
.iter()
.min_by_key(|(_, id)| id.map(|id| id.0))
.map(|(background, _)| background.color);
if let (Some(mut settings), Some(color)) = (settings, first) {
if settings.background_color != color {
settings.background_color = color;
}
}
}
#[derive(Component, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct EnvironmentMap {
pub texture: std::path::PathBuf,
pub intensity: f32,
#[serde(skip)]
pub handle: Option<crate::assets::Handle<crate::assets::TextureAsset>>,
}
impl Default for EnvironmentMap {
fn default() -> Self {
Self {
texture: std::path::PathBuf::new(),
intensity: 1.0,
handle: None,
}
}
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct ReflectionProbe {
pub extents: [f32; 3],
pub intensity: f32,
}
impl Default for ReflectionProbe {
fn default() -> Self {
Self {
extents: [5.0; 3],
intensity: 1.0,
}
}
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct Fog {
pub color: [f32; 3],
pub density: f32,
pub height: f32,
pub height_falloff: f32,
pub sun_scatter: f32,
pub sky_affect: f32,
}
impl Default for Fog {
fn default() -> Self {
Self {
color: [0.5, 0.6, 0.7],
density: 0.01,
height: 0.0,
height_falloff: 0.1,
sun_scatter: 0.3,
sky_affect: 1.0,
}
}
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct Bloom {
pub intensity: f32,
pub threshold: f32,
pub spread: f32,
}
impl Default for Bloom {
fn default() -> Self {
Self {
intensity: 0.5,
threshold: 1.0,
spread: 0.7,
}
}
}
#[derive(Component, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct CameraScreen {
pub camera: String,
pub size: [u32; 2],
}
impl Default for CameraScreen {
fn default() -> Self {
Self {
camera: String::new(),
size: [640, 360],
}
}
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct ColorGrading {
pub contrast: f32,
pub saturation: f32,
pub shadows: [f32; 3],
pub highlights: [f32; 3],
pub vignette: f32,
}
impl Default for ColorGrading {
fn default() -> Self {
Self {
contrast: 1.0,
saturation: 1.0,
shadows: [1.0; 3],
highlights: [1.0; 3],
vignette: 0.0,
}
}
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct AmbientOcclusion {
pub radius: f32,
pub intensity: f32,
}
impl Default for AmbientOcclusion {
fn default() -> Self {
Self {
radius: 1.0,
intensity: 1.0,
}
}
}
pub(super) fn load_environment_maps(
assets: Option<ResMut<crate::assets::AssetServer>>,
mut maps: Query<&mut EnvironmentMap>,
mut frame: bevy_ecs::prelude::Local<u32>,
) {
let Some(mut assets) = assets else {
return;
};
*frame = frame.wrapping_add(1);
for mut map in &mut maps {
let retry = map.handle.is_none()
&& !map.texture.as_os_str().is_empty()
&& (*frame).is_multiple_of(120);
if !bevy_ecs::change_detection::DetectChanges::is_changed(&map)
&& !retry
{
continue;
}
let map = map.bypass_change_detection();
map.handle = (!map.texture.as_os_str().is_empty())
.then(|| {
assets
.textures
.handle_for_path(&map.texture)
.map(Ok)
.unwrap_or_else(|| assets.load_texture(&map.texture))
.map_err(|error| eprintln!("environment map: {error}"))
.ok()
})
.flatten();
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct HudButtonPressed {
pub entity: Entity,
}
pub(super) fn advance_tweens(
time: Res<FrameTime>,
mut tweens: Query<(&mut Tween, &mut Transform)>,
) {
let dt = time.fixed_delta.as_secs_f32();
for (mut tween, mut transform) in &mut tweens {
if tween.finished() {
continue;
}
tween.elapsed += dt;
let value = tween.sample();
match tween.property {
TweenProperty::Position => transform.position = value,
TweenProperty::Rotation => transform.rotation = value,
TweenProperty::Scale => transform.scale = value,
}
}
}
pub(super) fn trigger_on_contact(
physics: Res<PhysicsWorld>,
mut cues: Query<(Entity, &mut SoundCue)>,
mut emitters: Query<(Entity, &mut BurstEmitter)>,
) {
if cues.is_empty() && emitters.is_empty() {
return;
}
let touching: BTreeSet<Entity> = physics
.contacts()
.iter()
.flat_map(|contact| [contact.a, contact.b])
.collect();
for (entity, mut cue) in &mut cues {
let now = cue.on_collision && touching.contains(&entity);
cue.triggered |= now && !cue.touching;
cue.touching = now;
}
for (entity, mut emitter) in &mut emitters {
let now = emitter.on_collision && touching.contains(&entity);
emitter.triggered |= now && !emitter.touching;
emitter.touching = now;
}
}
type IsPlayer = Or<(
With<super::PlatformerController>,
With<super::PlayerController>,
)>;
type PickupParts = (
Entity,
&'static mut Pickup,
Option<&'static SceneId>,
Option<&'static mut SoundCue>,
Option<&'static mut BurstEmitter>,
);
pub(super) fn collect_pickups(
mut commands: Commands,
physics: Res<PhysicsWorld>,
players: Query<(), IsPlayer>,
mut pickups: Query<PickupParts>,
mut counters: Query<(&mut Counter, Option<&SceneId>)>,
) {
let touched: BTreeSet<Entity> = physics
.contacts()
.iter()
.flat_map(|contact| [(contact.a, contact.b), (contact.b, contact.a)])
.filter(|(_, other)| players.contains(*other))
.map(|(entity, _)| entity)
.collect();
if touched.is_empty() {
return;
}
let mut ready: Vec<_> = pickups
.iter_mut()
.filter(|(entity, pickup, ..)| {
!pickup.collected && touched.contains(entity)
})
.collect();
ready.sort_by_key(|(entity, _, id, ..)| (id.map(|id| id.0), *entity));
for (entity, mut pickup, _, cue, emitter) in ready {
if let Some(required) = &pickup.requires {
let done = find_counter(counters.iter(), required)
.is_some_and(|counter| counter.complete());
if !done {
continue;
}
}
pickup.collected = true;
if let Some(mut counter) =
find_counter(counters.iter_mut(), &pickup.counter)
{
counter.value += pickup.value;
}
commands
.entity(entity)
.insert(super::Visibility { visible: false });
if let Some(mut cue) = cue {
cue.trigger();
}
if let Some(mut emitter) = emitter {
emitter.trigger();
}
}
}
pub(super) fn fire_sound_cues(
mut events: ResMut<EventQueue<SoundEvent>>,
mut cues: Query<(Entity, &mut SoundCue)>,
) {
for (entity, mut cue) in &mut cues {
if std::mem::take(&mut cue.triggered) {
events.send(SoundEvent {
entity,
clip: cue.clip.clone(),
volume: cue.volume,
});
}
}
}
type EmitterParts = (
&'static mut BurstEmitter,
&'static Transform,
Option<&'static GlobalTransform>,
Option<&'static SceneId>,
Option<&'static MeshRenderer>,
Entity,
);
pub(super) fn fire_bursts(
mut commands: Commands,
time: Res<FrameTime>,
seed: Res<RandomSeed>,
mut emitters: Query<EmitterParts>,
) {
let dt = time.fixed_delta.as_secs_f32();
for (mut emitter, transform, global, id, mesh, entity) in &mut emitters {
let mut count = 0;
if std::mem::take(&mut emitter.triggered) {
count += u64::from(emitter.count);
}
if emitter.rate > 0.0 {
emitter.pending += emitter.rate * dt;
let whole = emitter.pending.floor();
emitter.pending -= whole;
count += whole as u64;
}
if count == 0 {
continue;
}
let origin = global.map_or(transform.position, |global| {
let column = global.matrix[3];
[column[0], column[1], column[2]]
});
let key = id.map_or(entity.to_bits(), |id| {
let bits = id.0.as_u128();
(bits as u64) ^ ((bits >> 64) as u64)
});
let stream = RandomSeed::stream("bursts", key);
let area = RandomSeed::stream("burst_area", key);
let mut scale =
transform.scale.map(|axis| axis * emitter.particle_scale);
scale[1] *= emitter.stretch;
for index in 0..count {
let up = seed.unit(time.fixed_tick, stream ^ (index << 1));
let turn = seed.unit(time.fixed_tick, stream ^ (index << 1 | 1));
let side = (1.0 - up * up).sqrt();
let (sin, cos) = (turn * TAU).sin_cos();
let velocity =
[side * cos, up, side * sin].map(|axis| axis * emitter.speed);
let particle = BurstParticle {
velocity,
gravity: emitter.gravity,
remaining: emitter.lifetime,
lifetime: emitter.lifetime,
scale,
};
let position = std::array::from_fn(|axis| {
let unit = seed
.unit(time.fixed_tick, area ^ (index * 3 + axis as u64));
origin[axis] + (unit * 2.0 - 1.0) * emitter.area[axis]
});
let transform = Transform {
position,
scale,
..Transform::default()
};
let mut spawned = commands.spawn((transform, particle));
if let Some(mesh) = mesh {
spawned.insert(*mesh);
}
}
}
}
pub(super) fn update_burst_particles(
mut commands: Commands,
time: Res<FrameTime>,
mut particles: Query<(Entity, &mut BurstParticle, &mut Transform)>,
) {
let dt = time.fixed_delta.as_secs_f32();
for (entity, mut particle, mut transform) in &mut particles {
particle.remaining -= dt;
if particle.remaining <= 0.0 {
commands.entity(entity).despawn();
continue;
}
particle.velocity[1] -= particle.gravity * dt;
for axis in 0..3 {
transform.position[axis] += particle.velocity[axis] * dt;
}
let left = particle.remaining / particle.lifetime;
transform.scale = particle.scale.map(|axis| axis * left);
}
}
#[cfg(feature = "ui")]
pub(super) fn draw_hud(
ui: Res<super::RuntimeUi>,
mut pressed: ResMut<EventQueue<HudButtonPressed>>,
elements: Query<(Entity, &HudElement, Option<&SceneId>)>,
counters: Query<(&Counter, Option<&SceneId>)>,
visibility: Query<&super::Visibility>,
parents: Query<&super::Parent>,
cameras: Query<(&super::Name, &super::Camera)>,
) {
let visible = |entity: Entity| {
std::iter::successors(Some(entity), |&entity| {
parents.get(entity).ok().map(|parent| parent.0)
})
.take(1024)
.all(|entity| visibility.get(entity).map_or(true, |v| v.visible))
};
let mut elements: Vec<_> = elements.iter().collect();
elements.sort_by_key(|(entity, _, id)| (id.map(|id| id.0), *entity));
for (entity, element, _) in elements {
if !visible(entity) {
continue;
}
if let Some(required) = &element.requires {
if !find_counter(counters.iter(), required)
.is_some_and(|counter| counter.complete())
{
continue;
}
}
let screen = ui.context().screen_rect();
let area = match &element.camera {
None => screen,
Some(wanted) => {
let Some((_, camera)) = cameras
.iter()
.find(|(name, camera)| name.0 == *wanted && camera.active)
else {
continue;
};
match camera.viewport {
Some([x, y, w, h]) => egui::Rect::from_min_size(
screen.min + egui::vec2(x, y) * screen.size(),
egui::vec2(w, h) * screen.size(),
),
None => screen,
}
}
};
let (align, inward) = match element.anchor {
HudAnchor::TopLeft => (egui::Align2::LEFT_TOP, [1.0, 1.0]),
HudAnchor::Top => (egui::Align2::CENTER_TOP, [1.0, 1.0]),
HudAnchor::TopRight => (egui::Align2::RIGHT_TOP, [-1.0, 1.0]),
HudAnchor::Center => (egui::Align2::CENTER_CENTER, [1.0, 1.0]),
HudAnchor::BottomLeft => (egui::Align2::LEFT_BOTTOM, [1.0, -1.0]),
HudAnchor::Bottom => (egui::Align2::CENTER_BOTTOM, [1.0, -1.0]),
HudAnchor::BottomRight => {
(egui::Align2::RIGHT_BOTTOM, [-1.0, -1.0])
}
};
let offset = egui::vec2(
element.offset[0] * inward[0],
element.offset[1] * inward[1],
);
let [r, g, b, a] =
element.color.map(|c| (c.clamp(0.0, 1.0) * 255.0) as u8);
let text =
egui::RichText::new(hud_text(&element.text, counters.iter()))
.size(element.font_size)
.color(egui::Color32::from_rgba_unmultiplied(r, g, b, a));
let context = ui.context();
let galley = egui::WidgetText::from(text.clone()).into_galley_impl(
context,
&context.style(),
egui::text::TextWrapping::no_max_width(),
egui::FontSelection::Default,
egui::Align::LEFT,
);
let padding = if element.button {
2.0 * context.style().spacing.button_padding
} else {
egui::Vec2::ZERO
};
let point = align.pos_in_rect(&area) + offset;
let position = align.anchor_size(point, galley.size() + padding).min;
egui::Area::new(egui::Id::new(("rusting.hud", entity)))
.fixed_pos(position)
.fade_in(false)
.show(context, |ui| {
if element.button {
if ui.button(text).clicked() {
pressed.send(HudButtonPressed { entity });
}
} else {
ui.add(egui::Label::new(text).extend());
}
});
}
}