303 lines
9.3 KiB
Rust
303 lines
9.3 KiB
Rust
mod appearance;
|
|
mod bulge;
|
|
mod direction;
|
|
mod movement;
|
|
|
|
use crate::{audio, fruit, grid, GameState};
|
|
use bevy::prelude::*;
|
|
use direction::Direction;
|
|
use itertools::Itertools;
|
|
use leafwing_input_manager::prelude::*;
|
|
|
|
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
|
enum SystemSet {
|
|
Movement,
|
|
DirectionFlush,
|
|
CollisionDetection,
|
|
}
|
|
|
|
pub struct SnakePlugin;
|
|
|
|
impl Plugin for SnakePlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.configure_set(SystemSet::Movement.after(SystemSet::CollisionDetection));
|
|
|
|
app.add_plugin(InputManagerPlugin::<direction::Direction>::default())
|
|
.add_event::<AddTailEvent>()
|
|
.insert_resource(bulge::PropagationTimer::default())
|
|
.add_startup_system(setup_snake_system)
|
|
.add_system(
|
|
segments_movement_system
|
|
.in_schedule(CoreSchedule::FixedUpdate)
|
|
.in_set(SystemSet::Movement)
|
|
.run_if(in_state(GameState::InGame))
|
|
.after(SystemSet::DirectionFlush),
|
|
)
|
|
.add_system(
|
|
direction::apply_direction_system
|
|
.in_schedule(CoreSchedule::FixedUpdate)
|
|
.run_if(in_state(GameState::InGame))
|
|
.before(SystemSet::DirectionFlush),
|
|
)
|
|
.add_system(
|
|
apply_system_buffers
|
|
.in_schedule(CoreSchedule::FixedUpdate)
|
|
.in_set(SystemSet::DirectionFlush)
|
|
.run_if(in_state(GameState::InGame)),
|
|
)
|
|
.add_system(
|
|
collision_system
|
|
.in_schedule(CoreSchedule::FixedUpdate)
|
|
.in_set(SystemSet::CollisionDetection)
|
|
.run_if(in_state(GameState::InGame))
|
|
.after(SystemSet::DirectionFlush),
|
|
)
|
|
.add_system(
|
|
game_over_system
|
|
.in_schedule(CoreSchedule::FixedUpdate)
|
|
.in_set(SystemSet::CollisionDetection)
|
|
.run_if(in_state(GameState::InGame))
|
|
.after(collision_system)
|
|
.run_if(about_to_collide),
|
|
)
|
|
.add_system(
|
|
collision_sound_system
|
|
.run_if(in_state(GameState::InGame))
|
|
.run_if(about_to_collide),
|
|
)
|
|
.add_system(
|
|
blip_sound_system
|
|
.run_if(fruit::eaten_event_sent),
|
|
)
|
|
.add_system(direction::start_game_system.run_if(in_state(GameState::Begin)))
|
|
.add_system(direction::change_direction_system)
|
|
.add_system(grid_transform_system.run_if(in_state(GameState::InGame)))
|
|
.add_system(add_tail_system)
|
|
.add_system(bulge::add_bulge_system)
|
|
.add_system(bulge::propagate_bulge_system)
|
|
.add_system(bulge::animate_bulge_system)
|
|
.add_system(eaten_event_system);
|
|
|
|
#[cfg(debug_assertions)]
|
|
app.add_system(debug::add_tail);
|
|
}
|
|
}
|
|
|
|
pub const Z_HEIGHT: f32 = 10.;
|
|
|
|
#[derive(Component, Debug)]
|
|
struct SnakeSegment;
|
|
|
|
#[derive(Component, Debug)]
|
|
pub struct SnakeHead;
|
|
|
|
#[derive(Component, Debug)]
|
|
struct Segments(Vec<Entity>);
|
|
|
|
#[derive(Component, Debug)]
|
|
struct Snake;
|
|
|
|
#[derive(Bundle)]
|
|
struct SnakeBundle {
|
|
snake: Snake,
|
|
collision: Collision,
|
|
segments: Segments,
|
|
|
|
#[bundle]
|
|
spatial_bundle: SpatialBundle,
|
|
}
|
|
|
|
#[derive(Component, Default, Debug)]
|
|
struct Collision {
|
|
about_to_collide: bool,
|
|
}
|
|
|
|
pub struct AddTailEvent;
|
|
|
|
fn create_snake_segment(
|
|
commands: &mut Commands,
|
|
grid_position: grid::Coordinate,
|
|
segment_number: u32,
|
|
) -> Entity {
|
|
let mut color = Color::RED;
|
|
color *= 0.99f32.powi(segment_number as _);
|
|
|
|
commands
|
|
.spawn(SpriteBundle {
|
|
sprite: Sprite {
|
|
color,
|
|
custom_size: Some(Vec2::splat(grid::SEGMENT_SIZE) * 0.9),
|
|
..Default::default()
|
|
},
|
|
transform: Transform::from_translation(Vec2::from(grid_position).extend(Z_HEIGHT)),
|
|
..Default::default()
|
|
})
|
|
.insert(SnakeSegment)
|
|
.insert(grid_position)
|
|
.insert(crate::canvas::EmissiveTile { intensity: 1.0 })
|
|
.id()
|
|
}
|
|
|
|
fn setup_snake_system(mut commands: Commands) {
|
|
let snake_head =
|
|
create_snake_segment(&mut commands, grid::Coordinate::splat(grid::SIZE / 2), 0);
|
|
|
|
commands
|
|
.entity(snake_head)
|
|
.insert(Name::new("SnakeHead"))
|
|
.insert(SnakeHead);
|
|
|
|
commands
|
|
.spawn(SnakeBundle {
|
|
snake: Snake,
|
|
collision: Default::default(),
|
|
segments: Segments(vec![snake_head]),
|
|
spatial_bundle: Default::default(),
|
|
})
|
|
.insert(Name::new("Snake"))
|
|
.insert(InputManagerBundle::<Direction> {
|
|
input_map: InputMap::new([
|
|
(KeyCode::Up, Direction::Up),
|
|
(KeyCode::Down, Direction::Down),
|
|
(KeyCode::Left, Direction::Left),
|
|
(KeyCode::Right, Direction::Right),
|
|
]),
|
|
..Default::default()
|
|
})
|
|
.add_child(snake_head);
|
|
}
|
|
|
|
fn eaten_event_system(
|
|
mut eaten_event_reader: EventReader<fruit::EatenEvent>,
|
|
mut tail_event_writer: EventWriter<AddTailEvent>,
|
|
) {
|
|
eaten_event_reader
|
|
.iter()
|
|
.for_each(|_| tail_event_writer.send(AddTailEvent));
|
|
}
|
|
|
|
fn add_tail_system(
|
|
mut commands: Commands,
|
|
mut snake_query: Query<(Entity, &mut Segments), With<Snake>>,
|
|
mut tail_event_reader: EventReader<AddTailEvent>,
|
|
) {
|
|
for _ in tail_event_reader.iter() {
|
|
let (snake_entity, mut segments) = snake_query.single_mut();
|
|
|
|
let segment = create_snake_segment(
|
|
&mut commands,
|
|
grid::Coordinate::splat(grid::Index::MIN / 2),
|
|
segments.0.len() as _,
|
|
);
|
|
|
|
segments.0.push(segment);
|
|
|
|
commands
|
|
.entity(segment)
|
|
.insert(Name::new(format!("Segment {}", segments.0.len())));
|
|
|
|
commands.entity(snake_entity).add_child(segment);
|
|
}
|
|
}
|
|
|
|
fn grid_transform_system(
|
|
mut query: Query<(&mut Transform, &grid::Coordinate), With<SnakeSegment>>,
|
|
) {
|
|
for (mut transform, grid_coordinate) in query.iter_mut() {
|
|
*transform = Transform::from_translation(Vec2::from(grid_coordinate).extend(Z_HEIGHT));
|
|
}
|
|
}
|
|
|
|
fn segments_movement_system(
|
|
mut segment_query: Query<&mut grid::Coordinate, With<SnakeSegment>>,
|
|
snake_query: Query<(&Collision, &Direction, &Segments), With<Snake>>,
|
|
) {
|
|
for (collision, direction, segments) in snake_query.iter() {
|
|
if collision.about_to_collide {
|
|
continue;
|
|
}
|
|
|
|
// Move other elements
|
|
for (&segment, &previous_segment) in segments.0.iter().rev().tuple_windows() {
|
|
let previous_coordinate = *segment_query.get(previous_segment).unwrap();
|
|
let mut coordinate = segment_query.get_mut(segment).unwrap();
|
|
*coordinate = previous_coordinate;
|
|
}
|
|
|
|
// Move head
|
|
let head = *segments.0.first().expect("Snake has always a head");
|
|
let mut head_coordinate = segment_query.get_mut(head).unwrap();
|
|
*head_coordinate = next_grid_coordinate(&head_coordinate, direction);
|
|
}
|
|
}
|
|
|
|
fn collision_system(
|
|
segment_query: Query<&grid::Coordinate, (With<SnakeSegment>, Without<SnakeHead>)>,
|
|
head_query: Query<(&grid::Coordinate, &Parent), With<SnakeHead>>,
|
|
mut snake_query: Query<(&Direction, &mut Collision), With<Snake>>,
|
|
) {
|
|
for (head_coordinate, parent) in head_query.iter() {
|
|
let (direction, mut collision) = snake_query
|
|
.get_mut(parent.get())
|
|
.expect("Head must be child of Snake");
|
|
|
|
let next_head_coordinate = next_grid_coordinate(head_coordinate, direction);
|
|
|
|
let hit_border = !next_head_coordinate.in_bounds();
|
|
|
|
let hit_snake = !segment_query
|
|
.iter()
|
|
.all(|&coordinate| coordinate != next_head_coordinate);
|
|
|
|
collision.about_to_collide = hit_border || hit_snake;
|
|
}
|
|
}
|
|
|
|
fn about_to_collide(query: Query<&Collision, With<Snake>>) -> bool {
|
|
query.get_single().unwrap().about_to_collide
|
|
}
|
|
|
|
fn game_over_system(mut next_state: ResMut<NextState<GameState>>) {
|
|
next_state.set(GameState::End);
|
|
}
|
|
|
|
fn collision_sound_system(audio: Res<Audio>, audio_assets: Res<audio::Assets>) {
|
|
audio.play(audio_assets.collision.clone());
|
|
}
|
|
|
|
fn blip_sound_system(
|
|
audio: Res<Audio>,
|
|
audio_assets: Res<audio::Assets>,
|
|
mut eaten_event_reader: EventReader<fruit::EatenEvent>,
|
|
) {
|
|
for _ in eaten_event_reader.iter() {
|
|
audio.play(audio_assets.blip.clone());
|
|
}
|
|
}
|
|
|
|
fn next_grid_coordinate(
|
|
current_coordinate: &grid::Coordinate,
|
|
direction: &Direction,
|
|
) -> grid::Coordinate {
|
|
match direction {
|
|
Direction::Up => *current_coordinate + grid::Coordinate(0, 1),
|
|
Direction::Down => *current_coordinate + grid::Coordinate(0, -1),
|
|
Direction::Left => *current_coordinate + grid::Coordinate(-1, 0),
|
|
Direction::Right => *current_coordinate + grid::Coordinate(1, 0),
|
|
}
|
|
}
|
|
|
|
#[cfg(debug_assertions)]
|
|
mod debug {
|
|
use super::*;
|
|
|
|
pub(super) fn add_tail(
|
|
keypress: Res<Input<KeyCode>>,
|
|
mut tail_event_writer: EventWriter<AddTailEvent>,
|
|
) {
|
|
if keypress.just_pressed(KeyCode::Space) {
|
|
tail_event_writer.send(AddTailEvent);
|
|
}
|
|
}
|
|
}
|