use std::collections::BTreeSet;
use std::f32::consts::{PI, TAU};
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,
#[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,
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>,
}
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,
}
}
}
#[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,
}
}
}
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(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>,
) {
for (mut emitter, transform, global, id, mesh, entity) in &mut emitters {
if !std::mem::take(&mut emitter.triggered) {
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 scale = transform.scale.map(|axis| axis * emitter.particle_scale);
for index in 0..u64::from(emitter.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 transform = Transform {
position: origin,
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>)>,
) {
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 let Some(required) = &element.requires {
if !find_counter(counters.iter(), required)
.is_some_and(|counter| counter.complete())
{
continue;
}
}
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],
);
egui::Area::new(egui::Id::new(("rusting.hud", entity)))
.anchor(align, offset)
.show(ui.context(), |ui| {
let [r, g, b, a] =
element.color.map(|c| (c.clamp(0.0, 1.0) * 255.0) as u8);
let text = hud_text(&element.text, counters.iter());
let text = egui::RichText::new(text)
.size(element.font_size)
.color(egui::Color32::from_rgba_unmultiplied(r, g, b, a));
if element.button {
if ui.button(text).clicked() {
pressed.send(HudButtonPressed { entity });
}
} else {
ui.label(text);
}
});
}
}