20 Commits

Author SHA1 Message Date
7c385b1557 Make dynamic linking feature a default
All checks were successful
Basic checks / Basic build-and-test supertask (push) Successful in 7m17s
Now I don't need to keep setting the option on the command line. I'll
take it out (or use `--no-default-features`) when making the release
build.
2025-08-29 13:35:50 -05:00
746de2bd80 Add "Game Over" text to the game over screen
All checks were successful
Basic checks / Basic build-and-test supertask (push) Successful in 7m18s
2025-08-15 15:26:11 -05:00
73ee5e554b Reorder ship-collision steps, rm frozen debris
When the player gets a game-over, the debris is spawned and then frozen
in place as the physics system gets turned off. I can make sure it never
appears by writing the state change and returning early from the
function.

This means the asteroid despawning needs to happen before that point,
otherwise the asteroids would be left on screen.
2025-08-15 15:02:28 -05:00
0967795d51 Remove unnecessary mut on system parameter
All checks were successful
Basic checks / Basic build-and-test supertask (push) Successful in 7m4s
2025-08-14 22:33:53 -05:00
20c71658c3 Implement a basic ship debris effect
I'm stealing the thruster mesh, which itself is actually the ship's
mesh, to draw the debris particles. I'll come back and make the scatter
a bit better at some point, but for now it's passable.
2025-08-14 22:33:42 -05:00
960861af79 Add a sparkler component to flash entities
I'll be using this to make a sparkling effect on the debris field left
behind from a destroyed ship.

It can also be used to do the temporary invincibility effect when
(re)spawning the player.
2025-08-14 21:57:47 -05:00
4b70be7048 Fix and improve ButtonMenuAction doc comment 2025-08-14 20:54:29 -05:00
8bf21e74f0 Remove Rapier debug plugin 2025-08-14 18:25:33 -05:00
c0cf6d7f30 Fix: reset "get-ready" timer upon showing widget
All checks were successful
Basic checks / Basic build-and-test supertask (push) Successful in 7m21s
Closes #25: Reset "get ready" timer upon entering the get-ready state.
2025-08-14 18:20:50 -05:00
cc23e9e08e Despawn the player when exiting the Playing state
Closes #21: Despawn player ship when exiting `GameState::Playing`

I've also moved the `fn despawn<T>` utility to the lib.rs module.
2025-08-14 14:50:12 -05:00
cf9825fcc3 Fix: Starting game from GUI runs "get ready" timer
All checks were successful
Basic checks / Basic build-and-test supertask (push) Successful in 7m14s
Closes #23: Main menu's start button doesn't run the get-ready timer

I made the start button go directly to `GameState::Playing`, which is
not actually the right state for beginning a new game. Swap the states
and everything works again.
2025-08-14 14:39:24 -05:00
708f514582 Implement a proper gun fire-rate mechanism
There is now a `Weapon` component which is just a timer for the gun's
fire rate. It is ticked every frame, clamping at the "ready" state (0
time remaining).

The ship spawns with this thing, and the `input_ship_shoot` system has
been updated to use it.
2025-08-14 14:34:22 -05:00
0a7ffcfa0a Add reflection & debug inspector stuff to Score 2025-08-14 13:14:32 -05:00
40ee042b53 Implement the HUD for real
Now it works!... but it runs every single frame, probably causing a
bunch of unnecessary text rendering and UI layout operations. I'll have
to come back and make it event-based at some point.
2025-08-14 13:07:25 -05:00
9d9b25d1df Name the profiles to not suggest they're WASM-only
All checks were successful
Basic checks / Basic build-and-test supertask (push) Successful in 7m13s
2025-08-14 11:04:20 -05:00
89ee328807 A couple extra build profiles 2025-08-14 11:04:20 -05:00
90d8142855 Use wasm-server-runner as the WASM runner
This way I can `cargo run --target ...` and Cargo will spawn a dummy
webserver instead of attempting to execute the WASM file on my x86/arm
CPU.
2025-08-14 11:03:37 -05:00
10366b642c Add wasm-specific feature for RNG child dependency
The `rand` crate eventually depends on `getrandom` which requires a
different feature set when running in the browser. WASM has no OS, and
so no RNG provider... but the browser, specifically, has one that the
JavaScript runtime can touch. Enabling the "wasm_js" feature on
`getrandom` allowes it to use this backend.

An extra config option must *also* be passed to `rustc`. THis can be set
through the environment, or the config.toml. I've chosen the latter so I
don't need to think about it very often.
2025-08-14 11:03:37 -05:00
94e0cd0999 Add my own crate features to pass through to deps
All checks were successful
Basic checks / Basic build-and-test supertask (push) Successful in 7m25s
Closes #20

Now this crate can be told to build with or without certain behavior,
rather than needing to patch the Cargo.toml file.
2025-08-14 10:34:03 -05:00
18d026637b Bump the crate version
I marked 0.2 at some point, so I guess I should keep incrementing this
during development. The menus are finished, which is as good a
checkpoint as any.
2025-08-14 08:58:02 -05:00
8 changed files with 210 additions and 43 deletions

4
.cargo/config.toml Normal file
View File

@@ -0,0 +1,4 @@
[target.wasm32-unknown-unknown]
runner = "wasm-server-runner"
rustflags = ["--cfg", "getrandom_backend=\"wasm_js\""]

View File

@@ -1,10 +1,34 @@
[package] [package]
name = "asteroids" name = "asteroids"
version = "0.2.0" version = "0.3.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
bevy = { version = "0.16", features = ["dynamic_linking"] } bevy = "0.16"
bevy-inspector-egui = "0.32.0" bevy-inspector-egui = "0.32.0"
bevy_rapier2d = { version = "0.31.0", features = ["debug-render-2d"] } bevy_rapier2d = "0.31.0"
rand = "0.9.2" rand = "0.9.2"
[features]
default = ["dynamic_linking"]
dynamic_linking = ["bevy/dynamic_linking"]
debug_ui = ["bevy_rapier2d/debug-render-2d"]
[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.3.3", features = ["wasm_js"] }
[profile.speedy]
inherits = "release"
codegen-units = 1
lto = "fat"
opt-level = 3
strip = "symbols"
panic = "abort"
[profile.tiny]
inherits = "release"
codegen-units = 1
lto = "fat"
opt-level = "z"
strip = "symbols"
panic = "abort"

View File

@@ -13,6 +13,7 @@ pub(crate) const BACKGROUND_COLOR: Color = Color::srgb(0.3, 0.3, 0.3);
pub(crate) const PLAYER_SHIP_COLOR: Color = Color::srgb(1.0, 1.0, 1.0); pub(crate) const PLAYER_SHIP_COLOR: Color = Color::srgb(1.0, 1.0, 1.0);
pub(crate) const SHIP_THRUSTER_COLOR_ACTIVE: Color = Color::srgb(1.0, 0.2, 0.2); pub(crate) const SHIP_THRUSTER_COLOR_ACTIVE: Color = Color::srgb(1.0, 0.2, 0.2);
pub(crate) const SHIP_THRUSTER_COLOR_INACTIVE: Color = Color::srgb(0.5, 0.5, 0.5); pub(crate) const SHIP_THRUSTER_COLOR_INACTIVE: Color = Color::srgb(0.5, 0.5, 0.5);
pub(crate) const SHIP_FIRE_RATE: f32 = 3.0; // in bullets-per-second
pub(crate) const ASTEROID_SMALL_COLOR: Color = Color::srgb(1.0, 0., 0.); pub(crate) const ASTEROID_SMALL_COLOR: Color = Color::srgb(1.0, 0., 0.);
pub(crate) const BULLET_COLOR: Color = Color::srgb(0.0, 0.1, 0.9); pub(crate) const BULLET_COLOR: Color = Color::srgb(0.0, 0.1, 0.9);
// TODO: asteroid medium & large // TODO: asteroid medium & large
@@ -24,5 +25,6 @@ pub(crate) const BULLET_SPEED: f32 = 150.0;
pub(crate) const BULLET_LIFETIME: f32 = 2.0; pub(crate) const BULLET_LIFETIME: f32 = 2.0;
pub(crate) const ASTEROID_LIFETIME: f32 = 40.0; pub(crate) const ASTEROID_LIFETIME: f32 = 40.0;
pub(crate) const DEBRIS_LIFETIME: f32 = 3.0; // lifetime, in seconds
pub const RNG_SEED: [u8; 32] = *b"12345678909876543210123456789098"; pub const RNG_SEED: [u8; 32] = *b"12345678909876543210123456789098";

View File

@@ -16,14 +16,13 @@ use crate::config::{
SHIP_THRUSTER_COLOR_INACTIVE, SHIP_THRUSTER_COLOR_INACTIVE,
}; };
use crate::machinery::AsteroidSpawner; use crate::machinery::AsteroidSpawner;
use crate::objects::{Bullet, Ship}; use crate::objects::{Bullet, Ship, Weapon};
use crate::physics::AngularVelocity; use crate::physics::AngularVelocity;
use bevy::prelude::*; use bevy::prelude::*;
use bevy_rapier2d::{ use bevy_rapier2d::{
plugin::{NoUserData, RapierPhysicsPlugin}, plugin::{NoUserData, RapierPhysicsPlugin},
prelude::*, prelude::*,
render::RapierDebugRenderPlugin,
}; };
use machinery::Lifetime; use machinery::Lifetime;
use resources::{GameAssets, Lives, Score, WorldSize}; use resources::{GameAssets, Lives, Score, WorldSize};
@@ -37,21 +36,20 @@ impl Plugin for AsteroidPlugin {
widgets::PluginGameMenu, widgets::PluginGameMenu,
widgets::PluginGameOver, widgets::PluginGameOver,
widgets::PluginGetReady, widgets::PluginGetReady,
widgets::PluginGameHud,
RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(10.0), RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(10.0),
RapierDebugRenderPlugin::default(),
)) ))
.insert_resource(ClearColor(BACKGROUND_COLOR)) .insert_resource(ClearColor(BACKGROUND_COLOR))
.insert_resource(WorldSize::default()) .insert_resource(WorldSize::default())
.insert_resource(Lives(3)) .insert_resource(Lives(3))
.register_type::<Lives>() .register_type::<Lives>()
.insert_resource(Score(0)) .insert_resource(Score(0))
.register_type::<Score>()
.insert_resource(AsteroidSpawner::new()) .insert_resource(AsteroidSpawner::new())
.init_resource::<GameAssets>() .init_resource::<GameAssets>()
.add_systems(Startup, spawn_camera) .add_systems(Startup, spawn_camera)
.add_systems( .add_systems(OnEnter(GameState::Playing), objects::spawn_player)
OnEnter(GameState::Playing), .add_systems(OnExit(GameState::Playing), despawn::<Ship>)
(objects::spawn_player, widgets::spawn_ui),
)
.add_systems( .add_systems(
FixedUpdate, FixedUpdate,
( (
@@ -60,6 +58,7 @@ impl Plugin for AsteroidPlugin {
input_ship_shoot, input_ship_shoot,
physics::wrap_entities, physics::wrap_entities,
machinery::tick_asteroid_manager, machinery::tick_asteroid_manager,
machinery::operate_sparklers,
objects::spawn_asteroid.after(machinery::tick_asteroid_manager), objects::spawn_asteroid.after(machinery::tick_asteroid_manager),
objects::split_asteroids, objects::split_asteroids,
objects::bullet_impact_listener, objects::bullet_impact_listener,
@@ -85,6 +84,14 @@ impl Plugin for AsteroidPlugin {
} }
} }
/// Despawns entities matching the generic argument. Intended to remove UI
/// elements.
pub(crate) fn despawn<T: Component>(mut commands: Commands, to_despawn: Query<Entity, With<T>>) {
for entity in to_despawn {
commands.entity(entity).despawn();
}
}
/// The game's main state tracking mechanism, registered with Bevy as a [`State`](`bevy::prelude::State`). /// The game's main state tracking mechanism, registered with Bevy as a [`State`](`bevy::prelude::State`).
#[derive(Clone, Debug, Eq, Hash, PartialEq, States)] #[derive(Clone, Debug, Eq, Hash, PartialEq, States)]
pub enum GameState { pub enum GameState {
@@ -161,19 +168,25 @@ fn input_ship_rotation(
/// tick those timers. Maybe this system? /// tick those timers. Maybe this system?
fn input_ship_shoot( fn input_ship_shoot(
keyboard_input: Res<ButtonInput<KeyCode>>, keyboard_input: Res<ButtonInput<KeyCode>>,
ship: Single<(&Transform, &physics::Velocity), With<Ship>>, ship: Single<(&Transform, &physics::Velocity, &mut Weapon), With<Ship>>,
game_assets: Res<GameAssets>, game_assets: Res<GameAssets>,
mut commands: Commands, mut commands: Commands,
time: Res<Time>,
) { ) {
let (ship_pos, ship_vel) = *ship; let (ship_pos, ship_vel, mut weapon) = ship.into_inner();
// Derive bullet velocity, add to the ship's velocity // Tick the timer so the cooldown eventually finishes. Once it does, the
let bullet_vel = (ship_pos.rotation * Vec3::X).xy() * BULLET_SPEED; // value will clamp at 0. The weapon is now ready.
let bullet_vel = bullet_vel + ship_vel.0; weapon.tick(time.delta());
// If the weapon is ready and the player presses the trigger,
// spawn a bullet & reset the timer.
if weapon.finished() && keyboard_input.pressed(KeyCode::Space) {
weapon.reset();
// Derive bullet velocity, add to the ship's velocity
let bullet_vel = (ship_pos.rotation * Vec3::X).xy() * BULLET_SPEED;
let bullet_vel = bullet_vel + ship_vel.0;
// TODO: create a timer for the gun fire rate.
// For now, spawn one for each press of the spacebar.
if keyboard_input.just_pressed(KeyCode::Space) {
commands.spawn(( commands.spawn((
Bullet, Bullet,
Collider::ball(0.2), Collider::ball(0.2),

View File

@@ -103,3 +103,35 @@ pub fn tick_lifetimes(
} }
} }
} }
/// Entities marked with this will flash. Used to make the debris field sparkle.
#[derive(Component, Deref, DerefMut)]
pub struct Sparkler(Timer);
impl Sparkler {
pub fn at_interval(period: f32) -> Self {
Self(Timer::from_seconds(period, TimerMode::Repeating))
}
}
/// Advances the timer in a sparkler, swapping between visible and invisible
/// each time the timer expires.
pub fn operate_sparklers(sparklers: Query<(&mut Visibility, &mut Sparkler)>, time: Res<Time>) {
for (mut vis, mut timer) in sparklers {
if timer.tick(time.delta()).just_finished() {
// Cycle between visible and in-visible modes (and print warning for "Inherited")
*vis = match *vis {
Visibility::Inherited => {
// I don't know when entities have this mode, so I'm going
// print a warning for a while.
eprintln!(
"->> WARN: `machinery::operate_sparklers` found an entity with Visibility::Inherited"
);
Visibility::Inherited
}
Visibility::Hidden => Visibility::Visible,
Visibility::Visible => Visibility::Hidden,
};
}
}
}

View File

@@ -4,6 +4,7 @@
use bevy::{ use bevy::{
ecs::{ ecs::{
bundle::Bundle,
component::Component, component::Component,
entity::Entity, entity::Entity,
event::{EventReader, EventWriter}, event::{EventReader, EventWriter},
@@ -12,7 +13,7 @@ use bevy::{
}, },
math::{Vec2, Vec3, Vec3Swizzles}, math::{Vec2, Vec3, Vec3Swizzles},
prelude::{Deref, DerefMut}, prelude::{Deref, DerefMut},
render::mesh::Mesh2d, render::{mesh::Mesh2d, view::Visibility},
sprite::MeshMaterial2d, sprite::MeshMaterial2d,
state::state::NextState, state::state::NextState,
time::{Timer, TimerMode}, time::{Timer, TimerMode},
@@ -22,9 +23,9 @@ use bevy_rapier2d::prelude::{ActiveCollisionTypes, ActiveEvents, Collider, Senso
use crate::{ use crate::{
AngularVelocity, GameAssets, GameState, Lives, AngularVelocity, GameAssets, GameState, Lives,
config::ASTEROID_LIFETIME, config::{ASTEROID_LIFETIME, DEBRIS_LIFETIME, SHIP_FIRE_RATE},
events::{AsteroidDestroy, BulletDestroy, ShipDestroy, SpawnAsteroid}, events::{AsteroidDestroy, BulletDestroy, ShipDestroy, SpawnAsteroid},
machinery::Lifetime, machinery::{Lifetime, Sparkler},
physics::{Velocity, Wrapping}, physics::{Velocity, Wrapping},
}; };
@@ -55,10 +56,18 @@ impl AsteroidSize {
#[derive(Component)] #[derive(Component)]
pub struct Ship; pub struct Ship;
/// The ship's gun (is just a timer)
#[derive(Component, Deref, DerefMut)]
pub struct Weapon(Timer);
/// Marker component for bullets. /// Marker component for bullets.
#[derive(Component)] #[derive(Component)]
pub struct Bullet; pub struct Bullet;
/// Debris left behind after the ship is destroyed.
#[derive(Component)]
pub struct Debris;
/// Responds to [`SpawnAsteroid`] events, spawning as specified /// Responds to [`SpawnAsteroid`] events, spawning as specified
pub fn spawn_asteroid( pub fn spawn_asteroid(
mut events: EventReader<SpawnAsteroid>, mut events: EventReader<SpawnAsteroid>,
@@ -144,6 +153,7 @@ pub fn spawn_player(mut commands: Commands, game_assets: Res<GameAssets>) {
ActiveEvents::COLLISION_EVENTS, ActiveEvents::COLLISION_EVENTS,
ActiveCollisionTypes::STATIC_STATIC, ActiveCollisionTypes::STATIC_STATIC,
Ship, Ship,
Weapon(Timer::from_seconds(1.0 / SHIP_FIRE_RATE, TimerMode::Once)),
Wrapping, Wrapping,
Velocity(Vec2::ZERO), Velocity(Vec2::ZERO),
AngularVelocity(0.0), AngularVelocity(0.0),
@@ -184,23 +194,42 @@ pub fn ship_impact_listener(
rocks: Query<Entity, With<Asteroid>>, rocks: Query<Entity, With<Asteroid>>,
mut player: Single<(&mut Transform, &mut Velocity), With<Ship>>, mut player: Single<(&mut Transform, &mut Velocity), With<Ship>>,
mut next_state: ResMut<NextState<GameState>>, mut next_state: ResMut<NextState<GameState>>,
game_assets: Res<GameAssets>,
) { ) {
for _ in events.read() { for _ in events.read() {
// STEP 1: Decrement lives (and maybe go to game over) // STEP 1: Clear asteroids
for rock in rocks {
commands.entity(rock).despawn();
}
// STEP 2: Decrement lives
if lives.0 == 0 { if lives.0 == 0 {
// If already at 0, game is over. // If the player has run out, return early with a state change.
next_state.set(GameState::GameOver); next_state.set(GameState::GameOver);
return;
} else { } else {
// Decrease life count. // Decrease life count.
lives.0 -= 1; lives.0 -= 1;
} }
// STEP 2: Clear asteroids // STEP 3: spawn the debris field where the player used to be.
for rock in rocks { for i in 0..10 {
commands.entity(rock).despawn(); let angle_rads = (i as f32) / 10.0 * std::f32::consts::TAU;
let dir = Vec2::from_angle(angle_rads);
let vel = player.1.0 + dir * 100.0;
commands.spawn((
Debris,
Visibility::Visible, // make sure it's "visible" not "Inherited" so the cycle works right
Lifetime(Timer::from_seconds(DEBRIS_LIFETIME, TimerMode::Once)),
Sparkler::at_interval(0.15),
Mesh2d(game_assets.thruster_mesh()), // borrow the thruster mesh for now
MeshMaterial2d(game_assets.thruster_mat_active()), // ... and the active thruster material
player.0.clone(), // clone the player transform
Velocity(vel),
));
} }
// STEP 3: Respawn player (teleport them to the origin) // STEP 4: Respawn player (teleport them to the origin)
player.0.translation = Vec3::ZERO; player.0.translation = Vec3::ZERO;
player.1.0 = Vec2::ZERO; player.1.0 = Vec2::ZERO;
} }

View File

@@ -22,7 +22,8 @@ use crate::{
SHIP_THRUSTER_COLOR_INACTIVE, config::WINDOW_SIZE, SHIP_THRUSTER_COLOR_INACTIVE, config::WINDOW_SIZE,
}; };
#[derive(Resource, Debug, Deref, Clone, Copy)] #[derive(InspectorOptions, Reflect, Resource, Debug, Deref, Clone, Copy)]
#[reflect(Resource, InspectorOptions)]
pub struct Score(pub i32); pub struct Score(pub i32);
impl From<Score> for String { impl From<Score> for String {

View File

@@ -1,6 +1,9 @@
use std::ops::DerefMut;
use crate::{ use crate::{
GameState, GameState,
config::{UI_BUTTON_HOVERED, UI_BUTTON_NORMAL, UI_BUTTON_PRESSED}, config::{UI_BUTTON_HOVERED, UI_BUTTON_NORMAL, UI_BUTTON_PRESSED},
despawn,
resources::{Lives, Score}, resources::{Lives, Score},
}; };
@@ -38,6 +41,17 @@ impl Plugin for PluginGetReady {
} }
} }
/// Plugin for the in-game HUD
pub struct PluginGameHud;
impl Plugin for PluginGameHud {
fn build(&self, app: &mut App) {
app.add_systems(OnEnter(GameState::Playing), spawn_ui)
.add_systems(OnExit(GameState::Playing), despawn::<MarkerHUD>)
.add_systems(Update, (operate_ui).run_if(in_state(GameState::Playing)));
}
}
/// Plugin for the game-over screen /// Plugin for the game-over screen
pub struct PluginGameOver; pub struct PluginGameOver;
@@ -65,10 +79,18 @@ struct OnReadySetGo;
#[derive(Component)] #[derive(Component)]
struct MarkerGameOver; struct MarkerGameOver;
/// Action specifier for the game-over menu's buttons. /// Marker for things on the HUD (the in-game UI elements)
#[derive(Component)]
struct MarkerHUD;
/// Action specifier for the buttons on the menus.
/// ///
/// Attach this component to a button and [`PluginGameOver`] will use it to /// Instead of holding function pointers for use as callbacks, I'm doing enum-
/// decide what to do when that button is pressed. /// dispatch. Add a variant according to what action the button press should
/// trigger.
///
/// Only [`PluginGameMenu`] and [`PluginGameOver`] will use it to this
/// component. There is no always-on, global system.
#[derive(Component)] #[derive(Component)]
enum ButtonMenuAction { enum ButtonMenuAction {
ToMainMenu, ToMainMenu,
@@ -88,14 +110,6 @@ struct CountdownText;
#[derive(Component)] #[derive(Component)]
struct CountdownBar; struct CountdownBar;
/// Despawns entities matching the generic argument. Intended to remove UI
/// elements.
fn despawn<T: Component>(mut commands: Commands, to_despawn: Query<Entity, With<T>>) {
for entity in to_despawn {
commands.entity(entity).despawn();
}
}
/// Utility function for creating a standard button. /// Utility function for creating a standard button.
fn button_bundle(text: &str) -> impl Bundle { fn button_bundle(text: &str) -> impl Bundle {
( (
@@ -152,7 +166,8 @@ fn spawn_menu(mut commands: Commands) {
}); });
} }
fn spawn_get_ready(mut commands: Commands) { fn spawn_get_ready(mut commands: Commands, mut timer: ResMut<ReadySetGoTimer>) {
timer.reset();
commands.spawn(( commands.spawn((
OnReadySetGo, // marker, so this can be de-spawned properly OnReadySetGo, // marker, so this can be de-spawned properly
Node { Node {
@@ -201,6 +216,11 @@ fn spawn_gameover_ui(mut commands: Commands) {
..default() ..default()
}, },
children![ children![
(
Text::new("Game Over"),
TextFont::from_font_size(35.0),
TextShadow::default(),
),
(button_bundle("Main Menu"), ButtonMenuAction::ToMainMenu,), (button_bundle("Main Menu"), ButtonMenuAction::ToMainMenu,),
(button_bundle("Quit"), ButtonMenuAction::Quit), (button_bundle("Quit"), ButtonMenuAction::Quit),
], ],
@@ -267,7 +287,7 @@ fn operate_buttons(
game_state.set(GameState::TitleScreen); game_state.set(GameState::TitleScreen);
} }
ButtonMenuAction::StartGame => { ButtonMenuAction::StartGame => {
game_state.set(GameState::Playing); game_state.set(GameState::GetReady);
} }
ButtonMenuAction::Quit => { ButtonMenuAction::Quit => {
app_exit_events.write(AppExit::Success); app_exit_events.write(AppExit::Success);
@@ -294,8 +314,50 @@ fn handle_spacebar(input: Res<ButtonInput<KeyCode>>, mut game_state: ResMut<Next
} }
pub fn spawn_ui(mut commands: Commands, score: Res<Score>, lives: Res<Lives>) { pub fn spawn_ui(mut commands: Commands, score: Res<Score>, lives: Res<Lives>) {
let score = score.0;
let lives = lives.0;
commands.spawn(( commands.spawn((
Text::new(format!("Score: {score:?} | Lives: {lives:?}")), MarkerHUD,
TextFont::from_font_size(25.0), Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Start,
justify_content: JustifyContent::SpaceBetween,
padding: UiRect::all(Val::Px(5.0)),
..default()
},
children![
(
Text::new(format!("Score: {score}")),
TextFont::from_font_size(25.0),
TextShadow::default(),
),
(
Text::new(format!("Lives: {lives}")),
TextFont::from_font_size(25.0),
TextShadow::default(),
)
],
)); ));
} }
/// Updates the HUD with the current score & life count
///
/// TODO: some kind of event-based thing. Touching the text nodes every frame
/// seems expensive.
fn operate_ui(
mut query: Single<(&Node, &Children), With<MarkerHUD>>,
mut text_query: Query<&mut Text>,
lives: Res<Lives>,
score: Res<Score>,
) {
let (_node, children) = query.deref_mut();
let score = score.0;
let lives = lives.0;
// TODO: Something smarter than `unwrap()`
let mut score_text = text_query.get_mut(children[0]).unwrap();
**score_text = format!("Score: {score}");
let mut lives_text = text_query.get_mut(children[1]).unwrap();
**lives_text = format!("Lives: {lives}");
}