You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1053 lines
33 KiB
Rust

// Bevy basically forces "complex types" with Querys
#![allow(clippy::type_complexity)]
use bevy::audio::PlaybackMode;
use bevy::image::{ImageLoaderSettings, ImageSampler};
use bevy::render::view::ColorGrading;
use games::physics2d::*;
use games::*;
use std::hash::BuildHasher;
fn main() {
App::new()
.add_plugins((
BaseGamePlugin {
title: "flappy bird (with rewind)".into(),
name: "flappy".into(),
game_type: GameType::Two,
},
Physics2dPlugin,
))
.insert_resource(Gravity(Vec2::NEG_Y * 9.8 * 100.0))
.init_resource::<BirdAssets>()
.init_resource::<PipeAssets>()
.init_resource::<GroundAssets>()
.init_resource::<CeilingAssets>()
.init_resource::<Score>()
.init_resource::<RewindFrames>()
.init_resource::<Flaps>()
.init_resource::<Deaths>()
.init_state::<PlayerState>()
.add_systems(
Startup,
(
// init_obstacles,
init_assets,
init_bird.after(init_assets),
init_first_batches.after(init_assets),
init_ui,
tweak_camera.after(create_camera_2d),
),
)
.add_systems(OnEnter(PlayerState::Alive), alive_bird)
.add_systems(OnEnter(PlayerState::Alive), reset_button::<FlapButton>)
.add_systems(OnEnter(PlayerState::Rewind), (start_rewinding, alive_bird))
.add_systems(OnEnter(PlayerState::Pause), pause_bird)
.add_systems(OnEnter(PlayerState::Stasis), pause_bird)
.add_systems(OnExit(PlayerState::Stasis), reset_button::<RewindButton>)
.add_systems(
Update,
(
// Systems to run when player state changes
(
// Print out when this state changes for debugging purposes
debug_state_changes::<PlayerState>,
// Toggle (UI) elements when the player dies/alives
toggle_state_visibility::<PlayerState>,
)
.run_if(state_changed::<PlayerState>),
// Detect if the bird is "dead" by checking if it is visible
// from the point of view of the camera
detect_dead.run_if(in_state(PlayerState::Alive)),
// Toggle rewinding state when "R" is pressed/released
toggle_rewind.run_if(
input_just_pressed(KeyCode::KeyR).or(input_just_released(KeyCode::KeyR)),
),
// Systems to run in the "play" state
(
// Only flap when we press the space key
flap_kb.run_if(input_just_pressed(KeyCode::Space)),
// Rewinding systems
record.run_if(any_component_changed::<Transform>),
)
.run_if(in_state(PlayerState::Alive)),
// Rewinding systems
rewind.run_if(in_state(PlayerState::Rewind)),
// Camera follows when bird moves regardless of player state
camera_follow_bird.run_if(any_component_changed::<Transform>),
// Pause when the player presses Escape
pause_game.run_if(input_just_pressed(KeyCode::Escape)),
// Transition out of the pause screen if the player presses space
un_pause_game
.run_if(input_just_pressed(KeyCode::Space))
.run_if(in_state(PlayerState::Pause)),
// Stats that get synced to the UI
(
sync_resource_to_ui::<Score>.run_if(resource_changed::<Score>),
sync_resource_to_ui::<Flaps>.run_if(resource_changed::<Flaps>),
sync_resource_to_ui::<Deaths>.run_if(resource_changed::<Deaths>),
sync_resource_to_ui::<RewindFrames>.run_if(resource_changed::<RewindFrames>),
),
(update_tooltip, debug_trail).run_if(in_state(DebuggingState::On)),
// TODO: Add run_if to this system
update_batch_position.run_if(any_component_changed::<Batch>),
move_batches.run_if(on_event::<CollisionStarted>.or(on_event::<CollisionEnded>)),
manage_score.run_if(on_event::<CollisionStarted>.or(on_event::<CollisionEnded>)),
shimmer_button::<RewindButton>.run_if(in_state(PlayerState::Stasis)),
shimmer_button::<FlapButton>.run_if(in_state(PlayerState::Pause)),
),
)
.add_observer(flap)
.add_observer(populate_batch)
.add_observer(populate_pipe)
.add_observer(populate_ground)
.add_observer(populate_ceiling)
.add_observer(populate_hitbox)
.run();
}
fn tweak_camera(camera: Single<(Entity, &mut Camera)>, mut commands: Commands) {
debug!("Tweaking camera");
let (e, mut c) = camera.into_inner();
c.clear_color = ClearColorConfig::Custom(WHITE.into());
commands.entity(e).insert(ColorGrading { ..default() });
}
#[derive(Component)]
struct Bird;
#[derive(States, Clone, Eq, PartialEq, Debug, Hash, Default, Component)]
enum PlayerState {
Alive,
Rewind,
Stasis,
// Kind of a hack, but we need a state for this
Credits,
#[default]
Pause,
}
// A tape tracking the bird's state every frame
#[derive(Component, Default)]
struct Tape {
linear_velocities: Vec<LinearVelocity>,
angular_velocities: Vec<AngularVelocity>,
external_impulses: Vec<ExternalImpulse>,
positions: Vec<Position>,
rotations: Vec<Rotation>,
}
fn init_bird(mut commands: Commands, bird_assets: Res<BirdAssets>) {
let name = Name::new("bird");
let t = Transform::from_xyz(0.0, 0.0, 0.0).with_scale(Vec3::splat(50.0));
let physics = (
RigidBody::Static,
Collider::rectangle(1.0, 1.2),
Mass(10.0),
ExternalImpulse::default().with_persistence(false),
MaxLinearSpeed(500.0),
);
let tape = Tape::default();
commands.spawn((
name,
bird_assets.mesh.clone(),
bird_assets.material.clone(),
physics,
t,
Bird,
tape,
CollisionEventsEnabled,
));
}
#[derive(Component, Clone)]
struct Ground(isize);
#[derive(Component, Clone)]
struct Ceiling(isize);
#[derive(Component, Clone)]
enum Pipe {
Top,
Bottom,
}
#[derive(Component, Clone)]
struct Hitbox;
#[derive(Component, Clone)]
struct Batch(usize);
fn init_first_batches(mut commands: Commands) {
commands.spawn(Batch(0));
commands.spawn(Batch(1));
commands.spawn(Batch(2));
commands.spawn(Batch(3));
commands.spawn(Batch(4));
}
/// We have the concept of an environment "batch" which contains:
/// * The ground obstacle
/// * The pipe obstacle
/// * The pipe scoring sensor
///
/// These are all populated via `OnAdd` observers for the respected components.
/// This makes it much more concise to spawn each part of the environment.
fn populate_batch(
trigger: Trigger<OnAdd, Batch>,
batches: Query<&Batch>,
children: Query<&ChildOf>,
mut commands: Commands,
) {
// Only run this for top level batch entities,
// not children containing a reference to their batch, like hitboxes
if !children.contains(trigger.target()) {
let Batch(batch_id) = batches.get(trigger.target()).unwrap();
commands
.entity(trigger.target())
.insert((
Transform::from_xyz(500.0 * (*batch_id) as f32, 0.0, 0.0),
Visibility::Inherited,
))
.with_children(|parent| {
parent.spawn(Ground(-2));
parent.spawn(Ground(-1));
parent.spawn(Ground(0));
parent.spawn(Ground(1));
parent.spawn(Ground(2));
parent.spawn(Ceiling(-2));
parent.spawn(Ceiling(-1));
parent.spawn(Ceiling(0));
parent.spawn(Ceiling(1));
parent.spawn(Ceiling(2));
if *batch_id > 0 {
parent.spawn((Pipe::Top, Batch(*batch_id)));
parent.spawn((Pipe::Bottom, Batch(*batch_id)));
parent.spawn((Hitbox, Batch(*batch_id)));
}
});
}
}
fn update_batch_position(
mut root_changes: Query<(&mut Transform, &Batch), (Changed<Batch>, Without<ChildOf>)>,
) {
root_changes.iter_mut().for_each(|(mut t, Batch(idx))| {
debug!("Updating batch {:?}", idx);
t.translation.x = 500.0 * (*idx) as f32;
});
}
fn populate_ground(
trigger: Trigger<OnAdd, Ground>,
grounds: Query<&Ground>,
ground_assets: Res<GroundAssets>,
mut commands: Commands,
) {
let Ground(idx) = grounds.get(trigger.target()).unwrap();
debug!("populating ground {:?}", idx);
commands.entity(trigger.target()).insert((
ground_assets.material.clone(),
ground_assets.mesh.clone(),
Name::new("ground"),
RigidBody::Static,
Collider::rectangle(1.0, 1.0),
Transform::from_xyz(100.0 * (*idx) as f32, -300.0, -1.0).with_scale(Vec3::splat(100.0)),
));
}
fn populate_ceiling(
trigger: Trigger<OnAdd, Ceiling>,
ceiling: Query<&Ceiling>,
ceiling_assets: Res<CeilingAssets>,
mut commands: Commands,
) {
let Ceiling(idx) = ceiling.get(trigger.target()).unwrap();
debug!("populating ceiling{:?}", idx);
commands.entity(trigger.target()).insert((
ceiling_assets.material.clone(),
ceiling_assets.mesh.clone(),
Name::new("ceiling"),
RigidBody::Static,
Collider::rectangle(1.0, 1.0),
Transform::from_xyz(100.0 * (*idx) as f32, 300.0, -3.0).with_scale(Vec3::splat(100.0)),
));
}
/// Based on if this is a Top or Bottom pipe the placement changes
/// Otherwise this just spawns in the center of the batch.
fn populate_pipe(
trigger: Trigger<OnAdd, Pipe>,
pipes: Query<(&Pipe, &Batch)>,
pipe_assets: Res<PipeAssets>,
mut commands: Commands,
rand: Res<Rand>,
) {
let pipe_t = {
let (pipe, Batch(id)) = pipes.get(trigger.target()).unwrap();
let offset = {
let val = rand.0.hash_one(id);
let option = val % 3;
match option {
0 => 100.0,
1 => 0.0,
2 => -100.0,
_ => panic!("Can only pick 1 of 3 pipe offsets"),
}
};
match pipe {
Pipe::Top => {
Transform::from_xyz(0.0, 300.0 + offset, -2.0).with_scale(Vec3::splat(100.0))
}
Pipe::Bottom => {
Transform::from_xyz(0.0, -300.0 + offset, -2.0).with_scale(Vec3::splat(100.0))
}
}
};
commands.entity(trigger.target()).insert((
pipe_t,
pipe_assets.material.clone(),
pipe_assets.mesh.clone(),
RigidBody::Static,
Collider::rectangle(1.0, 4.0),
Name::new("pipe"),
));
}
/// The hitbox should cover the entire height of the screen so if the player
/// passes between the pipes it registers a point
fn populate_hitbox(trigger: Trigger<OnAdd, Hitbox>, mut commands: Commands) {
commands.entity(trigger.target()).insert((
RigidBody::Static,
Collider::rectangle(1.0, 10.0),
Sensor,
CollisionEventsEnabled,
Name::new("hitbox"),
Transform::from_xyz(0.0, 0.0, 0.0).with_scale(Vec3::splat(100.0)),
));
}
#[derive(Resource, Default)]
struct BirdAssets {
material: MeshMaterial2d<ColorMaterial>,
mesh: Mesh2d,
}
#[derive(Resource, Default)]
struct GroundAssets {
material: MeshMaterial2d<ColorMaterial>,
mesh: Mesh2d,
}
#[derive(Resource, Default)]
struct CeilingAssets {
material: MeshMaterial2d<ColorMaterial>,
mesh: Mesh2d,
}
#[derive(Resource, Default)]
struct PipeAssets {
material: MeshMaterial2d<ColorMaterial>,
mesh: Mesh2d,
}
/// Initialize some materials and meshes used by the environment obstacles
fn init_assets(
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
mut bird_assets: ResMut<BirdAssets>,
mut pipe_assets: ResMut<PipeAssets>,
mut ground_assets: ResMut<GroundAssets>,
mut ceiling_assets: ResMut<CeilingAssets>,
server: Res<AssetServer>,
) {
pipe_assets.material = MeshMaterial2d(materials.add(ColorMaterial {
texture: Some(server.load("flappy/pipe.png")),
color: GREEN.into(),
..default()
}));
pipe_assets.mesh = Mesh2d(meshes.add(Rectangle::new(0.875, 4.0)));
ground_assets.material = MeshMaterial2d(materials.add(ColorMaterial {
texture: Some(server.load("flappy/ground.png")),
color: BLACK.into(),
..default()
}));
ground_assets.mesh = Mesh2d(meshes.add(Rectangle::new(1.0, 1.0)));
ceiling_assets.material = MeshMaterial2d(materials.add(ColorMaterial {
texture: Some(server.load("flappy/ceiling.png")),
color: BLACK.into(),
..default()
}));
ceiling_assets.mesh = Mesh2d(meshes.add(Rectangle::new(1.0, 0.777)));
bird_assets.material = MeshMaterial2d(materials.add(ColorMaterial {
texture: Some(server.load("flappy/bird.png")),
color: ORANGE.into(),
alpha_mode: AlphaMode2d::Blend,
..default()
}));
bird_assets.mesh = Mesh2d(meshes.add(Rectangle::new(1.0, 1.2)));
}
#[derive(Component)]
struct FlapSfx;
#[derive(Component)]
struct FlapButton;
#[derive(Component)]
struct BonkSfx;
#[derive(Component)]
struct RewindSfx;
#[derive(Component)]
struct RewindButton;
fn init_ui(mut commands: Commands, server: Res<AssetServer>) {
commands
.spawn((
Node {
align_self: AlignSelf::Center,
justify_self: JustifySelf::Center,
flex_direction: FlexDirection::Column,
..default()
},
PlayerState::Stasis,
))
.with_children(|parent| {
parent.spawn((
Text::new("Game Over...?"),
TextLayout::new_with_justify(JustifyText::Center),
));
parent.spawn((
SyncResource::<Score>::default(),
Text::default(),
TextLayout::new_with_justify(JustifyText::Center),
));
parent.spawn((
SyncResource::<Flaps>::default(),
Text::default(),
TextLayout::new_with_justify(JustifyText::Center),
));
parent.spawn((
SyncResource::<RewindFrames>::default(),
Text::default(),
TextLayout::new_with_justify(JustifyText::Center),
));
parent.spawn((
SyncResource::<Deaths>::default(),
Text::default(),
TextLayout::new_with_justify(JustifyText::Center),
));
parent.spawn((
Text::new("Press R to Rewind"),
TextLayout::new_with_justify(JustifyText::Center),
));
parent
.spawn((Node {
flex_direction: FlexDirection::Row,
justify_content: JustifyContent::SpaceEvenly,
..default()
},))
.with_children(|parent| {
fn quit_game(
_trigger: Trigger<Pointer<Click>>,
mut exit: EventWriter<AppExit>,
) {
warn!("Quitting game");
exit.write(AppExit::Success);
}
fn show_credits(
_trigger: Trigger<Pointer<Click>>,
mut state: ResMut<NextState<PlayerState>>,
) {
state.set(PlayerState::Credits);
}
parent
.spawn((
Button,
Node { ..default() },
children![Text::new("Credits")],
))
.observe(show_credits);
parent
.spawn((Button, Node { ..default() }, children![Text::new("Quit"),]))
.observe(quit_game);
});
});
fn hide_credits(_trigger: Trigger<Pointer<Click>>, mut state: ResMut<NextState<PlayerState>>) {
state.set(PlayerState::Stasis)
}
let credits_str = include_str!("../../../assets/flappy/CREDITS");
commands
.spawn((
Node {
align_self: AlignSelf::Center,
justify_self: JustifySelf::Center,
flex_direction: FlexDirection::Column,
..default()
},
PlayerState::Credits,
children![(
Text::new(credits_str),
TextLayout::new_with_justify(JustifyText::Center)
)],
))
.with_children(|parent| {
parent.spawn((
Node {
align_self: AlignSelf::Center,
..default()
},
Button,
children![Text::new("Close")],
));
})
.observe(hide_credits);
commands.spawn((
Node {
align_self: AlignSelf::Center,
justify_self: JustifySelf::Center,
flex_direction: FlexDirection::Column,
..default()
},
PlayerState::Pause,
Text::new("PAUSED"),
));
commands
.spawn(Node {
align_self: AlignSelf::End,
justify_self: JustifySelf::Center,
flex_direction: FlexDirection::Row,
justify_content: JustifyContent::SpaceEvenly,
width: Val::Percent(100.0),
min_height: Val::Percent(10.0),
..default()
})
.with_children(|parent| {
let rewind_image = server.load_with_settings(
"flappy/rewind.png",
|settings: &mut ImageLoaderSettings| {
// Need to use nearest filtering to avoid bleeding between the slices with tiling
settings.sampler = ImageSampler::nearest();
},
);
parent
.spawn((
Node {
width: Val::Percent(50.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
Button,
BackgroundColor::default(),
RewindButton,
children![
(
ImageNode { color: BLACK.into(), image: rewind_image, ..default() },
Node { height: Val::Px(50.0), ..default() },
),
(
Text::new("Rewind! (R)"),
TextFont::from_font_size(30.0),
TextLayout::new_with_justify(JustifyText::Center)
),
],
))
.observe(start_rewind)
.observe(end_rewind);
let play_image = server.load_with_settings(
"flappy/play.png",
|settings: &mut ImageLoaderSettings| {
// Need to use nearest filtering to avoid bleeding between the slices with tiling
settings.sampler = ImageSampler::nearest();
},
);
parent
.spawn((
Node {
width: Val::Percent(50.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
Button,
FlapButton,
children![
(
Text::new("Flap! (Spacebar)"),
TextFont::from_font_size(30.0),
TextLayout::new_with_justify(JustifyText::Center)
),
(
ImageNode { color: BLACK.into(), image: play_image, ..default() },
Node { height: Val::Px(50.0), ..default() },
),
],
))
.observe(flap_button);
});
commands.spawn((
Node {
align_self: AlignSelf::Start,
justify_self: JustifySelf::Center,
..default()
},
BackgroundColor(WHITE.into()),
children![(
SyncResource::<Score>::default(),
Text::default(),
TextLayout::new_with_justify(JustifyText::Center),
)]
));
}
fn start_rewind(_trigger: Trigger<Pointer<Pressed>>, mut next: ResMut<NextState<PlayerState>>) {
next.set(PlayerState::Rewind);
}
fn end_rewind(_trigger: Trigger<Pointer<Released>>, mut next: ResMut<NextState<PlayerState>>) {
next.set(PlayerState::Alive);
}
fn flap_button(
_trigger: Trigger<Pointer<Pressed>>,
mut commands: Commands,
bird: Single<Entity, With<Bird>>,
curr: Res<State<PlayerState>>,
mut next: ResMut<NextState<PlayerState>>,
) {
if !matches!(curr.get(), PlayerState::Alive) {
next.set(PlayerState::Alive);
}
let e = *bird;
debug!("Flapping {:?}", e);
commands.trigger_targets(Flap, e);
}
/// Pause the game when the player presses "Escape"
fn pause_game(mut next: ResMut<NextState<PlayerState>>) {
next.set(PlayerState::Pause);
}
fn un_pause_game(mut next: ResMut<NextState<PlayerState>>) {
next.set(PlayerState::Alive);
}
#[derive(Component, Clone, Event)]
struct Flap;
// Observer for flapping
fn flap(
trigger: Trigger<Flap>,
mut bird: Query<&mut ExternalImpulse, With<Bird>>,
mut flaps: ResMut<Flaps>,
server: ResMut<AssetServer>,
mut commands: Commands,
) {
debug!("real flap for {:?}", trigger.target());
// Increment flap stat
flaps.0 += 1;
// Flap birds wings
if let Ok(mut f) = bird.get_mut(trigger.target()) {
f.apply_impulse(Vec2::Y * 5000.0 + Vec2::X * 1000.0);
// Play flap sfx
commands.spawn((
AudioPlayer::new(server.load("flappy/bonk.ogg")),
PlaybackSettings {
mode: PlaybackMode::Despawn,
..default()
},
BonkSfx,
));
}
}
fn flap_kb(
#[cfg(debug_assertions)] state: Res<State<PlayerState>>,
#[cfg(debug_assertions)] keycode: Res<ButtonInput<KeyCode>>,
birds: Query<Entity, With<Bird>>,
mut commands: Commands,
) {
#[cfg(debug_assertions)]
{
debug_assert!(
matches!(state.get(), PlayerState::Alive),
"Only flap when playing"
);
debug_assert!(
keycode.just_pressed(KeyCode::Space),
"Only flap when space is just pressed"
);
}
birds.iter().for_each(|e| {
debug!("Flapping {:?}", e);
commands.trigger_targets(Flap, e);
});
}
fn toggle_rewind(keycode: Res<ButtonInput<KeyCode>>, mut next: ResMut<NextState<PlayerState>>) {
debug_assert!(
keycode.just_pressed(KeyCode::KeyR) || keycode.just_released(KeyCode::KeyR),
"Only toggle rewind when R is pressed"
);
if keycode.just_pressed(KeyCode::KeyR) {
debug!("Toggling rewind ON");
next.set(PlayerState::Rewind)
} else {
debug!("Toggling rewind OFF");
next.set(PlayerState::Alive)
}
}
fn record(
#[cfg(debug_assertions)] state: Res<State<PlayerState>>,
mut birds: Query<
(
&LinearVelocity,
&AngularVelocity,
&ExternalImpulse,
&Position,
&Rotation,
&mut Tape,
),
With<Bird>,
>,
) {
#[cfg(debug_assertions)]
debug_assert!(
matches!(state.get(), PlayerState::Alive),
"Only record in the alive state"
);
birds
.iter_mut()
.for_each(|(lv, av, ei, p, r, mut tape)| {
tape.linear_velocities.push(*lv);
tape.angular_velocities.push(*av);
tape.external_impulses.push(*ei);
tape.positions.push(*p);
tape.rotations.push(*r);
});
}
fn rewind(
#[cfg(debug_assertions)] state: Res<State<PlayerState>>,
mut next: ResMut<NextState<PlayerState>>,
mut birds: Query<
(
&mut LinearVelocity,
&mut AngularVelocity,
&mut ExternalImpulse,
&mut Position,
&mut Rotation,
&mut Tape,
),
With<Bird>,
>,
mut frames: ResMut<RewindFrames>,
) {
#[cfg(debug_assertions)]
debug_assert!(
matches!(state.get(), PlayerState::Rewind),
"Only rewind in the rewinding state"
);
birds.iter_mut().for_each(
|(mut lv, mut av, mut ei, mut p, mut r, mut tape)| {
if tape.positions.is_empty() {
next.set(PlayerState::Pause);
} else {
lv.0 = tape.linear_velocities.pop().unwrap().0;
av.0 = tape.angular_velocities.pop().unwrap().0;
ei.set_impulse(tape.external_impulses.pop().unwrap().impulse());
p.0 = tape.positions.pop().unwrap().0;
*r = tape.rotations.pop().unwrap();
frames.0 += 1;
}
},
);
}
// PERF: May run more than necessary, should be event-driven on aabb intersection
fn detect_dead(
#[cfg(debug_assertions)] state: Res<State<PlayerState>>,
bird: Single<&ColliderAabb, With<Bird>>,
obstacles: Query<&ColliderAabb, Or<(With<Ground>, With<Pipe>, With<Ceiling>)>>,
mut next: ResMut<NextState<PlayerState>>,
server: ResMut<AssetServer>,
mut commands: Commands,
) {
#[cfg(debug_assertions)]
debug_assert!(
matches!(state.get(), PlayerState::Alive),
"Only check if dead while alive"
);
if obstacles.iter().any(|obstacle| bird.intersects(obstacle)) {
next.set(PlayerState::Stasis);
// Play bonk sfx
commands.spawn((
AudioPlayer::new(server.load("flappy/flap.ogg")),
PlaybackSettings {
mode: PlaybackMode::Despawn,
..default()
},
FlapSfx,
));
}
}
fn alive_bird(mut bird: Single<&mut RigidBody, With<Bird>>) {
debug!("Setting bird to Dynamic");
**bird = RigidBody::Dynamic;
}
fn pause_bird(
state: Res<State<PlayerState>>,
mut bird: Single<&mut RigidBody, With<Bird>>,
mut deaths: ResMut<Deaths>,
) {
// Increment death count
if state.get() == &PlayerState::Stasis {
deaths.0 += 1
}
debug!("Setting bird to Static");
**bird = RigidBody::Static;
}
fn camera_follow_bird(
bird: Single<&Transform, (With<Bird>, Changed<Transform>)>,
mut camera: Single<&mut Transform, (With<Camera>, Without<Bird>)>,
) {
camera.translation.x = bird.translation.x;
}
/// How many pipes the player has passed
#[derive(Resource, Default)]
struct Score(usize);
impl Display for Score {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
writeln!(f, "Score: {}", self.0)
}
}
/// Number of frames that were rewound
#[derive(Resource, Default)]
struct RewindFrames(usize);
impl Display for RewindFrames {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
writeln!(f, "Frames Rewound: {}", self.0)
}
}
// Track number of times player flapped their wings
#[derive(Resource, Default)]
struct Flaps(usize);
impl Display for Flaps {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
writeln!(f, "Flaps: {}", self.0)
}
}
// Track number of times player died
#[derive(Resource, Default)]
struct Deaths(usize);
impl Display for Deaths {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
writeln!(f, "Deaths: {}", self.0)
}
}
fn manage_score(
mut start: EventReader<CollisionStarted>,
mut end: EventReader<CollisionEnded>,
state: Res<State<PlayerState>>,
hitboxes: Query<&Batch, With<Hitbox>>,
mut score: ResMut<Score>,
) {
match state.get() {
PlayerState::Rewind => {
start.read().for_each(|CollisionStarted(a, b)| {
// Set score to collided hitbox
if let Ok(Batch(this)) = hitboxes.get(*a) {
debug!("[Rewind] Setting score to {this}");
score.0 = this.saturating_sub(1);
} else if let Ok(Batch(this)) = hitboxes.get(*b) {
debug!("[Rewind] Setting score to {this}");
score.0 = this.saturating_sub(1);
}
})
}
_ => {
end.read().for_each(|CollisionEnded(a, b)| {
// Set score to collided hitbox
if let Ok(Batch(this)) = hitboxes.get(*b) {
debug!("[Alive] Setting score to {this}");
score.0 = *this;
} else if let Ok(Batch(this)) = hitboxes.get(*a) {
debug!("[Alive] Setting score to {this}");
score.0 = *this;
}
})
}
}
}
/// WARNING: This method is somewhat janky
///
/// We first figure out which collided entity was a hitbox
/// and bial out early if neither is a hitbox
/// Then we find the batch ID for the hitbox that was hit
/// Next we figure out the old -> new batch IDs based on offsets from the current batch
/// skipping the 0th batch intentionally as that is a special case
/// Finally we iterate over all entities with the old batch ID and upsert the new batch ID
/// This includes root batch entities as well as pipes and hitboxes
fn move_batches(
mut start: EventReader<CollisionStarted>,
mut end: EventReader<CollisionEnded>,
hitboxes: Query<Entity, With<Hitbox>>,
batches: Query<(Entity, &Batch)>,
state: Res<State<PlayerState>>,
mut commands: Commands,
) {
let s = start.read().map(|CollisionStarted(a, b)| (a, b));
let e = end.read().map(|CollisionEnded(a, b)| (a, b));
let c = s.chain(e);
c.for_each(|(a, b)| {
debug!("[batches] Collision {a} -> {b}");
let target = {
if hitboxes.contains(*b) {
*b
} else if hitboxes.contains(*a) {
*a
} else {
return;
}
};
let (_, Batch(curr)) = batches.get(target).unwrap();
debug!("[batches] Current: {curr}");
let (old_batch, new_batch) = match state.get() {
PlayerState::Alive => (curr.saturating_sub(2), curr.saturating_add(2)),
PlayerState::Rewind => (curr.saturating_add(2), curr.saturating_sub(2)),
_ => (*curr, *curr),
};
if old_batch > 0 && new_batch != 0 {
batches
.iter()
// Filter to just entities with this batch ID
.filter_map(|(e, Batch(id))| (*id == old_batch).then_some(e))
.for_each(|old| {
debug!("Moving batch {old_batch}({old}) -> {new_batch}");
commands.entity(old).insert(Batch(new_batch));
})
}
});
}
fn update_tooltip(
mut query: Query<(
&mut ToolTip,
Option<&LinearVelocity>,
Option<&Batch>,
Option<&RigidBody>,
)>,
) {
query.iter_mut().for_each(|(mut tt, lv, b, rb)| {
// Add Linear Velocity if present on entity
lv.iter().for_each(|it| {
tt.insert("Velocity", format!("{}", it.0));
});
// Add Batch ID to if present
b.iter().for_each(|it| {
tt.insert("Batch", format!("{}", it.0));
});
rb.iter().for_each(|it| {
tt.insert("RigidBody", format!("{it:?}"));
})
});
}
fn debug_trail(
mut physics_objects: Query<(Entity, &Transform), With<Mass>>,
mut gizmos: Gizmos,
mut positions: Local<HashMap<Entity, VecDeque<Vec3>>>,
) {
physics_objects.iter_mut().for_each(|(e, obj)| {
if let Some(list) = positions.get_mut(&e) {
list.push_front(obj.translation);
if list.len() > 60 {
list.pop_back();
}
} else {
positions.insert(e, vec![obj.translation].into());
}
positions.iter().for_each(|(_e, list)| {
list.iter().for_each(|v| {
gizmos.cross_2d(Vec2::new(v.x, v.y), 12., FUCHSIA);
});
});
})
}
fn start_rewinding(server: ResMut<AssetServer>, mut commands: Commands) {
commands.spawn((
AudioPlayer::new(server.load("flappy/rewind-start.ogg")),
PlaybackSettings {
mode: PlaybackMode::Despawn,
..default()
},
RewindSfx,
));
}
fn shimmer_button<T: Component>(mut bg: Single<&mut BackgroundColor, With<T>>, time: Res<Time>) {
let t = time.elapsed_secs();
let period = 3.0;
let r = (((t / period) % 1.0) * std::f32::consts::PI).cos();
let g = ((((t / period) + 0.3) % 1.0) * std::f32::consts::PI).cos();
let b = ((((t / period) + 0.6) % 1.0) * std::f32::consts::PI).cos();
bg.0 = Srgba::rgb(r, g, b).into();
}
fn reset_button<T: Component>(mut bg: Single<&mut BackgroundColor, With<T>>) {
bg.0 = WHITE.into();
}