Compare commits
2 Commits
0e66b6222e
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c0ad6fcba | ||
|
|
efab67d027 |
6750
Cargo.lock
generated
36
Cargo.toml
@@ -1,16 +1,32 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "amethyst-starter-2d"
|
name = "flappy-bird-rust"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Hilmar Wiegand <me@hwgnd.de>"]
|
authors = ["Hilmar Wiegand <me@hwgnd.de>"]
|
||||||
edition = "2018"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
#amethyst = {path ="../amethyst", version = "0.15.0"}
|
bevy = "0.17.3"
|
||||||
amethyst = "0.15.0"
|
rand = "0.9"
|
||||||
log = { version = "0.4.8", features = ["serde"] }
|
|
||||||
|
|
||||||
[features]
|
[target.wasm32-unknown-unknown.dependencies]
|
||||||
default = ["vulkan"]
|
bevy = { version = "0.17.3", default-features = false, features = [
|
||||||
#empty = ["amethyst/empty"]
|
"bevy_asset",
|
||||||
#metal = ["amethyst/metal"]
|
"bevy_winit",
|
||||||
vulkan = ["amethyst/vulkan"]
|
"bevy_window",
|
||||||
|
"bevy_render",
|
||||||
|
"bevy_core_pipeline",
|
||||||
|
"bevy_sprite",
|
||||||
|
"bevy_text",
|
||||||
|
"bevy_ui",
|
||||||
|
"bevy_state",
|
||||||
|
"png",
|
||||||
|
"webgl2",
|
||||||
|
] }
|
||||||
|
getrandom = { version = "0.2", features = ["js"] }
|
||||||
|
|
||||||
|
# Enable dynamic linking for faster compile times during development
|
||||||
|
[profile.dev]
|
||||||
|
opt-level = 1
|
||||||
|
|
||||||
|
[profile.dev.package."*"]
|
||||||
|
opt-level = 3
|
||||||
|
|||||||
49
WEB_BUILD.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Building Flappy Bird for Web
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. Install the WebAssembly target:
|
||||||
|
```bash
|
||||||
|
rustup target add wasm32-unknown-unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install wasm-bindgen-cli (will be done automatically by build script):
|
||||||
|
```bash
|
||||||
|
cargo install wasm-bindgen-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Run the build script:
|
||||||
|
```bash
|
||||||
|
./build-web.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
- Build the WASM binary
|
||||||
|
- Generate JavaScript bindings
|
||||||
|
- Copy assets to the `web/` directory
|
||||||
|
- Copy the HTML file
|
||||||
|
|
||||||
|
## Running Locally
|
||||||
|
|
||||||
|
Serve the web directory with any HTTP server:
|
||||||
|
|
||||||
|
**Python:**
|
||||||
|
```bash
|
||||||
|
python3 -m http.server --directory web 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
**Alternative (if you have basic-http-server):**
|
||||||
|
```bash
|
||||||
|
cargo install basic-http-server
|
||||||
|
basic-http-server web
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open http://localhost:8080 in your browser!
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The game uses WebGL2 for rendering
|
||||||
|
- All assets are included in the build
|
||||||
|
- Image sampling is set to nearest neighbor for crisp pixel art
|
||||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
169
assets/sprites/flappy.ron
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
|
||||||
|
List((
|
||||||
|
texture_width: 512,
|
||||||
|
texture_height: 512,
|
||||||
|
sprites: [
|
||||||
|
( // Daytime background
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 144,
|
||||||
|
height: 256,
|
||||||
|
),
|
||||||
|
( // Nighttime background
|
||||||
|
x: 146,
|
||||||
|
y: 0,
|
||||||
|
width: 144,
|
||||||
|
height: 256,
|
||||||
|
),
|
||||||
|
( // Down Pipe
|
||||||
|
x: 56,
|
||||||
|
y: 323,
|
||||||
|
width: 26,
|
||||||
|
height: 160,
|
||||||
|
),
|
||||||
|
( // Up Pipe
|
||||||
|
x: 84,
|
||||||
|
y: 323,
|
||||||
|
width: 26,
|
||||||
|
height: 160,
|
||||||
|
),
|
||||||
|
( // Ground
|
||||||
|
x: 292,
|
||||||
|
y: 0,
|
||||||
|
width: 168,
|
||||||
|
height: 56,
|
||||||
|
),
|
||||||
|
( // Floppy
|
||||||
|
x: 3,
|
||||||
|
y: 490,
|
||||||
|
width: 17,
|
||||||
|
height: 13,
|
||||||
|
),
|
||||||
|
( // Tap Tap Dialogue
|
||||||
|
x: 292,
|
||||||
|
y: 91,
|
||||||
|
width: 56,
|
||||||
|
height: 48,
|
||||||
|
),
|
||||||
|
( // Play Button
|
||||||
|
x: 354,
|
||||||
|
y: 118,
|
||||||
|
width: 52,
|
||||||
|
height: 29,
|
||||||
|
),
|
||||||
|
( // Leaderboard button
|
||||||
|
x: 414,
|
||||||
|
y: 118,
|
||||||
|
width: 52,
|
||||||
|
height: 29,
|
||||||
|
),
|
||||||
|
( // Get Ready
|
||||||
|
x: 295,
|
||||||
|
y: 59,
|
||||||
|
width: 91,
|
||||||
|
height: 24,
|
||||||
|
),
|
||||||
|
( // Flappy Bird Text
|
||||||
|
x: 351,
|
||||||
|
y: 91,
|
||||||
|
width: 90,
|
||||||
|
height: 25,
|
||||||
|
),
|
||||||
|
( // Game Over
|
||||||
|
x: 395,
|
||||||
|
y: 58,
|
||||||
|
width: 100,
|
||||||
|
height: 22,
|
||||||
|
),
|
||||||
|
( // Number 0
|
||||||
|
x: 314,
|
||||||
|
y: 198,
|
||||||
|
width: 20,
|
||||||
|
height: 28,
|
||||||
|
),
|
||||||
|
( // Number 1
|
||||||
|
x: 135,
|
||||||
|
y: 455,
|
||||||
|
width: 20,
|
||||||
|
height: 28,
|
||||||
|
),
|
||||||
|
( // Number 2
|
||||||
|
x: 292,
|
||||||
|
y: 138,
|
||||||
|
width: 20,
|
||||||
|
height: 28,
|
||||||
|
),
|
||||||
|
( // Number 3
|
||||||
|
x: 314,
|
||||||
|
y: 138,
|
||||||
|
width: 20,
|
||||||
|
height: 28,
|
||||||
|
),
|
||||||
|
( // Number 4
|
||||||
|
x: 336,
|
||||||
|
y: 138,
|
||||||
|
width: 20,
|
||||||
|
height: 28,
|
||||||
|
),
|
||||||
|
( // Number 5
|
||||||
|
x: 358,
|
||||||
|
y: 138,
|
||||||
|
width: 20,
|
||||||
|
height: 28,
|
||||||
|
),
|
||||||
|
( // Number 6
|
||||||
|
x: 292,
|
||||||
|
y: 168,
|
||||||
|
width: 20,
|
||||||
|
height: 28,
|
||||||
|
),
|
||||||
|
( // Number 7
|
||||||
|
x: 314,
|
||||||
|
y: 168,
|
||||||
|
width: 20,
|
||||||
|
height: 28,
|
||||||
|
),
|
||||||
|
( // Number 8
|
||||||
|
x: 336,
|
||||||
|
y: 168,
|
||||||
|
width: 20,
|
||||||
|
height: 28,
|
||||||
|
),
|
||||||
|
( // Number 9
|
||||||
|
x: 358,
|
||||||
|
y: 168,
|
||||||
|
width: 20,
|
||||||
|
height: 28,
|
||||||
|
),
|
||||||
|
( // Menu Button (index 6)
|
||||||
|
x: 462,
|
||||||
|
y: 26,
|
||||||
|
width: 40,
|
||||||
|
height: 14,
|
||||||
|
),
|
||||||
|
( // OK Button (index 8)
|
||||||
|
x: 462,
|
||||||
|
y: 42,
|
||||||
|
width: 40,
|
||||||
|
height: 14,
|
||||||
|
),
|
||||||
|
( // Bird Animation 1 (index 67)
|
||||||
|
x: 3,
|
||||||
|
y: 491,
|
||||||
|
width: 17,
|
||||||
|
height: 12,
|
||||||
|
),
|
||||||
|
( // Bird Animation 2 (index 68)
|
||||||
|
x: 31,
|
||||||
|
y: 491,
|
||||||
|
width: 17,
|
||||||
|
height: 12,
|
||||||
|
),
|
||||||
|
( // Bird Animation 3 (index 69)
|
||||||
|
x: 59,
|
||||||
|
y: 491,
|
||||||
|
width: 17,
|
||||||
|
height: 12,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
))
|
||||||
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
25
build-web.sh
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Building for WebAssembly..."
|
||||||
|
|
||||||
|
# Install wasm-bindgen-cli if not present
|
||||||
|
if ! command -v wasm-bindgen &> /dev/null; then
|
||||||
|
echo "Installing wasm-bindgen-cli..."
|
||||||
|
cargo install wasm-bindgen-cli
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build the project
|
||||||
|
cargo build --release --target wasm32-unknown-unknown
|
||||||
|
|
||||||
|
# Generate JS bindings
|
||||||
|
wasm-bindgen --out-dir ./web --target web ./target/wasm32-unknown-unknown/release/flappy-bird-rust.wasm
|
||||||
|
|
||||||
|
# Copy assets and HTML
|
||||||
|
mkdir -p web/assets
|
||||||
|
cp -r assets/* web/assets/
|
||||||
|
cp index.html web/
|
||||||
|
|
||||||
|
echo "Build complete! Files are in ./web"
|
||||||
|
echo "To serve locally, run: python3 -m http.server --directory web 8080"
|
||||||
|
echo "Then open http://localhost:8080"
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
|
|
||||||
List((
|
|
||||||
texture_width: 512,
|
|
||||||
texture_height: 512,
|
|
||||||
sprites: [
|
|
||||||
( // Daytime background
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 144,
|
|
||||||
height: 256,
|
|
||||||
),
|
|
||||||
( // Nighttime background
|
|
||||||
x: 146,
|
|
||||||
y: 0,
|
|
||||||
width: 144,
|
|
||||||
height: 256,
|
|
||||||
),
|
|
||||||
( // Down Pipe
|
|
||||||
x: 56,
|
|
||||||
y: 323,
|
|
||||||
width: 26,
|
|
||||||
height: 160,
|
|
||||||
),
|
|
||||||
( // Up Pipe
|
|
||||||
x: 84,
|
|
||||||
y: 323,
|
|
||||||
width: 26,
|
|
||||||
height: 160,
|
|
||||||
),
|
|
||||||
( // Ground
|
|
||||||
x: 292,
|
|
||||||
y: 0,
|
|
||||||
width: 168,
|
|
||||||
height: 56,
|
|
||||||
),
|
|
||||||
( // Floppy
|
|
||||||
x: 3,
|
|
||||||
y: 490,
|
|
||||||
width: 17,
|
|
||||||
height: 13,
|
|
||||||
),
|
|
||||||
( // Tap Tap Dialogue
|
|
||||||
x: 292,
|
|
||||||
y: 91,
|
|
||||||
width: 56,
|
|
||||||
height: 48,
|
|
||||||
),
|
|
||||||
( // Play Button
|
|
||||||
x: 354,
|
|
||||||
y: 118,
|
|
||||||
width: 52,
|
|
||||||
height: 29,
|
|
||||||
),
|
|
||||||
( // Leaderboard button
|
|
||||||
x: 414,
|
|
||||||
y: 118,
|
|
||||||
width: 52,
|
|
||||||
height: 29,
|
|
||||||
),
|
|
||||||
( // Get Ready
|
|
||||||
x: 295,
|
|
||||||
y: 59,
|
|
||||||
width: 91,
|
|
||||||
height: 24,
|
|
||||||
),
|
|
||||||
( // Flappy Bird Text
|
|
||||||
x: 351,
|
|
||||||
y: 91,
|
|
||||||
width: 90,
|
|
||||||
height: 25,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
))
|
|
||||||
@@ -1,38 +1,80 @@
|
|||||||
use amethyst::{
|
use bevy::prelude::*;
|
||||||
core::transform::TransformBundle,
|
|
||||||
core::transform::Transform,
|
|
||||||
prelude::*,
|
|
||||||
renderer::{
|
|
||||||
plugins::{RenderFlat2D, RenderToWindow},
|
|
||||||
types::DefaultBackend,
|
|
||||||
RenderingBundle,
|
|
||||||
},
|
|
||||||
utils::application_root_dir,
|
|
||||||
core::SystemDesc,
|
|
||||||
derive::SystemDesc,
|
|
||||||
ecs::prelude::{Component, DenseVecStorage, Entity},
|
|
||||||
ecs::prelude::{Join, ReadStorage, System, SystemData, WriteStorage},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Scrolling background/ground component
|
||||||
// Falling object component to bucket us into something the system can manipulate
|
#[derive(Component, Clone)]
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct TiledScroller {
|
pub struct TiledScroller {
|
||||||
pub speed: f32,
|
pub speed: f32,
|
||||||
pub width: f32,
|
pub width: f32,
|
||||||
pub height: f32,
|
pub height: f32,
|
||||||
pub position: f32,
|
pub position: f32,
|
||||||
}
|
}
|
||||||
impl Component for TiledScroller {
|
|
||||||
type Storage = DenseVecStorage<Self>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Falling object component to bucket us into something the system can manipulate
|
// Bird component
|
||||||
|
#[derive(Component)]
|
||||||
pub struct Birb {
|
pub struct Birb {
|
||||||
pub vertical_speed: f32,
|
pub vertical_speed: f32,
|
||||||
pub starting_height: f32,
|
pub starting_height: f32,
|
||||||
pub position: f32,
|
pub position: f32,
|
||||||
}
|
}
|
||||||
impl Component for Birb {
|
|
||||||
type Storage = DenseVecStorage<Self>;
|
// Bird animation component
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct BirdAnimation {
|
||||||
|
pub timer: Timer,
|
||||||
|
pub current_frame: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pipe component
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct Pipe {
|
||||||
|
pub speed: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collider component for collision detection
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct Collider {
|
||||||
|
pub width: f32,
|
||||||
|
pub height: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ground marker component
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct Ground;
|
||||||
|
|
||||||
|
// Timer resource for pipe spawning
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct PipeSpawnTimer {
|
||||||
|
pub timer: Timer,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Score resource
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct Score {
|
||||||
|
pub value: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// High score resource
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct HighScore {
|
||||||
|
pub value: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timer resource for score incrementing
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct ScoreTimer {
|
||||||
|
pub timer: Timer,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Score display marker component
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct ScoreDisplay;
|
||||||
|
|
||||||
|
// Debug hitbox visualization resource
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct DebugHitboxes {
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marker for hitbox debug sprites
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct HitboxDebugSprite;
|
||||||
|
|||||||
412
src/gameover_state.rs
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
use crate::splash_state::*;
|
||||||
|
use crate::components::*;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct GameOverScreen;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct ScoreCard {
|
||||||
|
pub target_y: f32,
|
||||||
|
pub animation_speed: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct TinyScoreDisplay;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct TinyHighScoreDisplay;
|
||||||
|
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct DebugScoreCard {
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct ScoreCardDebugSprite;
|
||||||
|
|
||||||
|
pub fn setup_gameover(
|
||||||
|
mut commands: Commands,
|
||||||
|
sprite_handles: Res<SpriteHandles>,
|
||||||
|
window_query: Query<&Window>,
|
||||||
|
score: Res<Score>,
|
||||||
|
mut high_score: ResMut<HighScore>,
|
||||||
|
) {
|
||||||
|
let Ok(window) = window_query.single() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let width = window.width();
|
||||||
|
let height = window.height();
|
||||||
|
|
||||||
|
// Update high score if current score is higher
|
||||||
|
if score.value > high_score.value {
|
||||||
|
high_score.value = score.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Game Over sprite - positioned at 3/4 height
|
||||||
|
let target_y = height * 0.75;
|
||||||
|
|
||||||
|
commands.spawn((
|
||||||
|
Sprite {
|
||||||
|
image: sprite_handles.texture.clone(),
|
||||||
|
texture_atlas: Some(TextureAtlas {
|
||||||
|
layout: sprite_handles.layout.clone(),
|
||||||
|
index: SPRITE_GAME_OVER,
|
||||||
|
}),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Transform {
|
||||||
|
translation: Vec3::new(width * 0.5, target_y, 0.3),
|
||||||
|
scale: Vec3::splat(3.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
GameOverScreen,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Score card - starts below screen and animates up
|
||||||
|
let card_target_y = height * 0.4;
|
||||||
|
commands.spawn((
|
||||||
|
Sprite {
|
||||||
|
image: sprite_handles.texture.clone(),
|
||||||
|
texture_atlas: Some(TextureAtlas {
|
||||||
|
layout: sprite_handles.layout.clone(),
|
||||||
|
index: SPRITE_SCORE_CARD,
|
||||||
|
}),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Transform {
|
||||||
|
translation: Vec3::new(width * 0.5, -200.0, 0.2),
|
||||||
|
scale: Vec3::splat(3.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
ScoreCard {
|
||||||
|
target_y: card_target_y,
|
||||||
|
animation_speed: 400.0,
|
||||||
|
},
|
||||||
|
GameOverScreen,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Determine medal based on score
|
||||||
|
let medal_index = if score.value >= 40 {
|
||||||
|
SPRITE_MEDAL_PLATINUM
|
||||||
|
} else if score.value >= 30 {
|
||||||
|
SPRITE_MEDAL_GOLD
|
||||||
|
} else if score.value >= 20 {
|
||||||
|
SPRITE_MEDAL_SILVER
|
||||||
|
} else {
|
||||||
|
// Everyone gets at least bronze
|
||||||
|
SPRITE_MEDAL_BRONZE
|
||||||
|
};
|
||||||
|
|
||||||
|
// Spawn medal - attached to card animation
|
||||||
|
commands.spawn((
|
||||||
|
Sprite {
|
||||||
|
image: sprite_handles.texture.clone(),
|
||||||
|
texture_atlas: Some(TextureAtlas {
|
||||||
|
layout: sprite_handles.layout.clone(),
|
||||||
|
index: medal_index,
|
||||||
|
}),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Transform {
|
||||||
|
translation: Vec3::new(width * 0.35 - 33.0, -200.0, 0.3),
|
||||||
|
scale: Vec3::splat(3.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
ScoreCard {
|
||||||
|
target_y: card_target_y - 10.0,
|
||||||
|
animation_speed: 400.0,
|
||||||
|
},
|
||||||
|
GameOverScreen,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gameover_input(
|
||||||
|
keyboard: Res<ButtonInput<KeyCode>>,
|
||||||
|
mouse: Res<ButtonInput<MouseButton>>,
|
||||||
|
touches: Res<Touches>,
|
||||||
|
mut next_state: ResMut<NextState<GameState>>,
|
||||||
|
) {
|
||||||
|
if keyboard.just_pressed(KeyCode::Space)
|
||||||
|
|| mouse.just_pressed(MouseButton::Left)
|
||||||
|
|| touches.any_just_pressed() {
|
||||||
|
next_state.set(GameState::Ready);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn animate_score_card(
|
||||||
|
time: Res<Time>,
|
||||||
|
mut query: Query<(&mut Transform, &ScoreCard)>,
|
||||||
|
) {
|
||||||
|
for (mut transform, card) in query.iter_mut() {
|
||||||
|
if transform.translation.y < card.target_y {
|
||||||
|
transform.translation.y += card.animation_speed * time.delta_secs();
|
||||||
|
// Clamp to target
|
||||||
|
if transform.translation.y > card.target_y {
|
||||||
|
transform.translation.y = card.target_y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_tiny_score(
|
||||||
|
mut commands: Commands,
|
||||||
|
score: Res<Score>,
|
||||||
|
sprite_handles: Res<SpriteHandles>,
|
||||||
|
window_query: Query<&Window>,
|
||||||
|
existing_digits: Query<Entity, With<TinyScoreDisplay>>,
|
||||||
|
card_query: Query<&Transform, With<ScoreCard>>,
|
||||||
|
) {
|
||||||
|
// Wait for card to finish animating before showing score
|
||||||
|
// Use iter().next() instead of single() since we have multiple ScoreCard entities
|
||||||
|
let Some(card_transform) = card_query.iter().next() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(window) = window_query.single() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only render once card is near its target position
|
||||||
|
if card_transform.translation.y < window.height() * 0.35 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only update if score changed or digits don't exist
|
||||||
|
if !score.is_changed() && !existing_digits.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Despawn existing score digits
|
||||||
|
for entity in existing_digits.iter() {
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = window.width();
|
||||||
|
let height = window.height();
|
||||||
|
let card_y = height * 0.4;
|
||||||
|
|
||||||
|
// Convert score to digits
|
||||||
|
let score_string = score.value.to_string();
|
||||||
|
let num_digits = score_string.len() as f32;
|
||||||
|
let digit_spacing = 12.0;
|
||||||
|
let total_width = num_digits * digit_spacing;
|
||||||
|
let start_x = width * 0.65 - total_width / 2.0 + 30.0 + 20.0;
|
||||||
|
|
||||||
|
// Spawn tiny digit sprites on the score card
|
||||||
|
for (i, digit_char) in score_string.chars().enumerate() {
|
||||||
|
let digit = digit_char.to_digit(10).unwrap() as usize;
|
||||||
|
let sprite_index = SPRITE_TINY_NUMBER_0 + digit;
|
||||||
|
|
||||||
|
commands.spawn((
|
||||||
|
Sprite {
|
||||||
|
image: sprite_handles.texture.clone(),
|
||||||
|
texture_atlas: Some(TextureAtlas {
|
||||||
|
layout: sprite_handles.layout.clone(),
|
||||||
|
index: sprite_index,
|
||||||
|
}),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Transform {
|
||||||
|
translation: Vec3::new(start_x + (i as f32 * digit_spacing), card_y + 20.0, 0.35),
|
||||||
|
scale: Vec3::splat(3.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
TinyScoreDisplay,
|
||||||
|
GameOverScreen,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_tiny_high_score(
|
||||||
|
mut commands: Commands,
|
||||||
|
high_score: Res<HighScore>,
|
||||||
|
sprite_handles: Res<SpriteHandles>,
|
||||||
|
window_query: Query<&Window>,
|
||||||
|
existing_digits: Query<Entity, With<TinyHighScoreDisplay>>,
|
||||||
|
card_query: Query<&Transform, With<ScoreCard>>,
|
||||||
|
) {
|
||||||
|
// Wait for card to finish animating before showing high score
|
||||||
|
// Use iter().next() instead of single() since we have multiple ScoreCard entities
|
||||||
|
let Some(card_transform) = card_query.iter().next() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(window) = window_query.single() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only render once card is near its target position
|
||||||
|
if card_transform.translation.y < window.height() * 0.35 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only update if high score changed or digits don't exist
|
||||||
|
if !high_score.is_changed() && !existing_digits.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Despawn existing high score digits
|
||||||
|
for entity in existing_digits.iter() {
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = window.width();
|
||||||
|
let height = window.height();
|
||||||
|
let card_y = height * 0.4;
|
||||||
|
|
||||||
|
// Convert high score to digits
|
||||||
|
let high_score_string = high_score.value.to_string();
|
||||||
|
let num_digits = high_score_string.len() as f32;
|
||||||
|
let digit_spacing = 12.0;
|
||||||
|
let total_width = num_digits * digit_spacing;
|
||||||
|
let start_x = width * 0.65 - total_width / 2.0 + 30.0 + 20.0;
|
||||||
|
|
||||||
|
// Spawn tiny digit sprites on the score card below the current score
|
||||||
|
for (i, digit_char) in high_score_string.chars().enumerate() {
|
||||||
|
let digit = digit_char.to_digit(10).unwrap() as usize;
|
||||||
|
let sprite_index = SPRITE_TINY_NUMBER_0 + digit;
|
||||||
|
|
||||||
|
commands.spawn((
|
||||||
|
Sprite {
|
||||||
|
image: sprite_handles.texture.clone(),
|
||||||
|
texture_atlas: Some(TextureAtlas {
|
||||||
|
layout: sprite_handles.layout.clone(),
|
||||||
|
index: sprite_index,
|
||||||
|
}),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Transform {
|
||||||
|
translation: Vec3::new(start_x + (i as f32 * digit_spacing), card_y - 50.0, 0.35),
|
||||||
|
scale: Vec3::splat(3.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
TinyHighScoreDisplay,
|
||||||
|
GameOverScreen,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_scorecard_debug(
|
||||||
|
keyboard: Res<ButtonInput<KeyCode>>,
|
||||||
|
mut debug: ResMut<DebugScoreCard>,
|
||||||
|
) {
|
||||||
|
if keyboard.just_pressed(KeyCode::KeyD) {
|
||||||
|
debug.enabled = !debug.enabled;
|
||||||
|
println!("Score card debug: {}", if debug.enabled { "ON" } else { "OFF" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_scorecard_debug(
|
||||||
|
mut commands: Commands,
|
||||||
|
debug: Res<DebugScoreCard>,
|
||||||
|
card_query: Query<&Transform, With<ScoreCard>>,
|
||||||
|
existing_debug: Query<Entity, With<ScoreCardDebugSprite>>,
|
||||||
|
window_query: Query<&Window>,
|
||||||
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
|
mut materials: ResMut<Assets<ColorMaterial>>,
|
||||||
|
) {
|
||||||
|
// Clean up existing debug sprites
|
||||||
|
for entity in existing_debug.iter() {
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !debug.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok(window) = window_query.single() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(card_transform) = card_query.iter().next() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let width = window.width();
|
||||||
|
let height = window.height();
|
||||||
|
let card_y = card_transform.translation.y;
|
||||||
|
|
||||||
|
// Draw horizontal reference lines at different Y positions
|
||||||
|
let reference_ys = vec![
|
||||||
|
(card_y + 20.0, "Score line (current)"),
|
||||||
|
(card_y, "Card center"),
|
||||||
|
(card_y - 20.0, "Medal line (suggested)"),
|
||||||
|
(card_y - 50.0, "High score line (current)"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (y, label) in reference_ys {
|
||||||
|
// Horizontal line
|
||||||
|
let mesh = Mesh::from(Rectangle::new(width, 2.0));
|
||||||
|
commands.spawn((
|
||||||
|
Mesh2d::from(meshes.add(mesh)),
|
||||||
|
MeshMaterial2d(materials.add(ColorMaterial::from(Color::srgba(1.0, 0.0, 0.0, 0.5)))),
|
||||||
|
Transform {
|
||||||
|
translation: Vec3::new(width / 2.0, y, 0.9),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
ScoreCardDebugSprite,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Add tick marks every 10 pixels along the horizontal line
|
||||||
|
for tick_x in (0..(width as i32)).step_by(10) {
|
||||||
|
let tick_mesh = Mesh::from(Rectangle::new(1.0, 8.0));
|
||||||
|
commands.spawn((
|
||||||
|
Mesh2d::from(meshes.add(tick_mesh)),
|
||||||
|
MeshMaterial2d(materials.add(ColorMaterial::from(Color::srgba(1.0, 0.0, 0.0, 0.7)))),
|
||||||
|
Transform {
|
||||||
|
translation: Vec3::new(tick_x as f32, y, 0.91),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
ScoreCardDebugSprite,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{}: y = {}", label, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertical reference lines for X positions
|
||||||
|
let reference_xs = vec![
|
||||||
|
(width * 0.35, "Medal X (current)"),
|
||||||
|
(width * 0.5, "Center"),
|
||||||
|
(width * 0.65, "Score area center"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (x, label) in reference_xs {
|
||||||
|
let mesh = Mesh::from(Rectangle::new(2.0, height));
|
||||||
|
commands.spawn((
|
||||||
|
Mesh2d::from(meshes.add(mesh)),
|
||||||
|
MeshMaterial2d(materials.add(ColorMaterial::from(Color::srgba(0.0, 1.0, 0.0, 0.5)))),
|
||||||
|
Transform {
|
||||||
|
translation: Vec3::new(x, height / 2.0, 0.9),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
ScoreCardDebugSprite,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Add tick marks every 10 pixels along the vertical line
|
||||||
|
for tick_y in (0..(height as i32)).step_by(10) {
|
||||||
|
let tick_mesh = Mesh::from(Rectangle::new(8.0, 1.0));
|
||||||
|
commands.spawn((
|
||||||
|
Mesh2d::from(meshes.add(tick_mesh)),
|
||||||
|
MeshMaterial2d(materials.add(ColorMaterial::from(Color::srgba(0.0, 1.0, 0.0, 0.7)))),
|
||||||
|
Transform {
|
||||||
|
translation: Vec3::new(x, tick_y as f32, 0.91),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
ScoreCardDebugSprite,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{}: x = {}", label, x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cleanup_gameover(
|
||||||
|
mut commands: Commands,
|
||||||
|
query: Query<Entity, With<GameOverScreen>>,
|
||||||
|
) {
|
||||||
|
for entity in query.iter() {
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
116
src/main.rs
@@ -3,63 +3,67 @@ mod systems;
|
|||||||
mod ready_state;
|
mod ready_state;
|
||||||
mod play_state;
|
mod play_state;
|
||||||
mod splash_state;
|
mod splash_state;
|
||||||
|
mod gameover_state;
|
||||||
|
|
||||||
use amethyst::{
|
use bevy::prelude::*;
|
||||||
input::{InputBundle, StringBindings},
|
use bevy::window::WindowResolution;
|
||||||
core::transform::TransformBundle,
|
use systems::*;
|
||||||
core::transform::Transform,
|
use splash_state::*;
|
||||||
prelude::*,
|
use ready_state::*;
|
||||||
renderer::{
|
use play_state::*;
|
||||||
plugins::{RenderFlat2D, RenderToWindow},
|
use gameover_state::*;
|
||||||
types::DefaultBackend,
|
use components::*;
|
||||||
RenderingBundle,
|
|
||||||
},
|
|
||||||
utils::application_root_dir,
|
|
||||||
core::SystemDesc,
|
|
||||||
derive::SystemDesc,
|
|
||||||
ecs::prelude::{Component, DenseVecStorage, Entity},
|
|
||||||
ecs::prelude::{Join, ReadStorage, System, SystemData, WriteStorage},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::components::*;
|
fn main() {
|
||||||
use crate::systems::*;
|
App::new()
|
||||||
use std::path::PathBuf;
|
.add_plugins(DefaultPlugins
|
||||||
use std::str::FromStr;
|
.set(WindowPlugin {
|
||||||
use crate::splash_state::SplashState;
|
primary_window: Some(Window {
|
||||||
|
title: "Flappy Bird - Rust Edition".to_string(),
|
||||||
fn main() -> amethyst::Result<()> {
|
resolution: WindowResolution::new(429, 768),
|
||||||
|
..default()
|
||||||
amethyst::start_logger(Default::default());
|
}),
|
||||||
|
..default()
|
||||||
// Gets the root directory of the application
|
})
|
||||||
let mut app_root = PathBuf::from_str("/home/mrh/source/flappy-bird-rust/")?;
|
.set(ImagePlugin::default_nearest())
|
||||||
|
|
||||||
// join on the resources path, and the config.
|
|
||||||
let resources = app_root.join("resources");
|
|
||||||
let display_config = resources.join("display_config.ron");
|
|
||||||
let binding_path = resources.join("bindings.ron");
|
|
||||||
|
|
||||||
let input_bundle = InputBundle::<StringBindings>::new()
|
|
||||||
.with_bindings_from_file(binding_path)?;
|
|
||||||
|
|
||||||
let game_data = GameDataBuilder::default()
|
|
||||||
.with_bundle(TransformBundle::new())?
|
|
||||||
.with_bundle(input_bundle)?
|
|
||||||
// .with(System, "system", &["required_things"])
|
|
||||||
.with(ScrollScrollables, "scroll", &[])
|
|
||||||
.with_bundle(
|
|
||||||
RenderingBundle::<DefaultBackend>::new()
|
|
||||||
.with_plugin(
|
|
||||||
RenderToWindow::from_config_path(display_config).unwrap()
|
|
||||||
.with_clear([0.34, 0.36, 0.52, 1.0]),
|
|
||||||
)
|
)
|
||||||
.with_plugin(RenderFlat2D::default()),
|
.init_state::<GameState>()
|
||||||
)?;
|
.init_resource::<HighScore>()
|
||||||
|
.init_resource::<DebugHitboxes>()
|
||||||
|
.init_resource::<DebugScoreCard>()
|
||||||
// Creates the app with the startup state and bound game data
|
.add_systems(Startup, (load_sprites, setup_splash).chain())
|
||||||
let mut game = Application::new(resources, SplashState::default(), game_data)?;
|
.add_systems(Update, splash_input.run_if(in_state(GameState::Splash)))
|
||||||
game.run();
|
.add_systems(OnExit(GameState::Splash), cleanup_splash)
|
||||||
|
.add_systems(OnEnter(GameState::Ready), setup_ready)
|
||||||
Ok(())
|
.add_systems(Update, ready_input.run_if(in_state(GameState::Ready)))
|
||||||
|
.add_systems(OnExit(GameState::Ready), cleanup_ready)
|
||||||
|
.add_systems(OnEnter(GameState::Playing), setup_play)
|
||||||
|
.add_systems(Update, (
|
||||||
|
bird_gravity,
|
||||||
|
animate_bird,
|
||||||
|
rotate_bird,
|
||||||
|
spawn_pipes,
|
||||||
|
move_pipes,
|
||||||
|
check_collisions,
|
||||||
|
play_input,
|
||||||
|
update_score,
|
||||||
|
render_score,
|
||||||
|
).run_if(in_state(GameState::Playing)))
|
||||||
|
.add_systems(OnExit(GameState::Playing), cleanup_play)
|
||||||
|
.add_systems(OnEnter(GameState::GameOver), setup_gameover)
|
||||||
|
.add_systems(Update, (
|
||||||
|
gameover_input,
|
||||||
|
animate_score_card,
|
||||||
|
render_tiny_score,
|
||||||
|
render_tiny_high_score,
|
||||||
|
toggle_scorecard_debug,
|
||||||
|
render_scorecard_debug,
|
||||||
|
).run_if(in_state(GameState::GameOver)))
|
||||||
|
.add_systems(OnExit(GameState::GameOver), cleanup_gameover)
|
||||||
|
.add_systems(Update, (
|
||||||
|
scroll_scrollables,
|
||||||
|
toggle_hitbox_debug,
|
||||||
|
render_hitbox_debug,
|
||||||
|
))
|
||||||
|
.run();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,104 +1,143 @@
|
|||||||
use amethyst::{
|
use bevy::prelude::*;
|
||||||
assets::{AssetStorage, Loader},
|
|
||||||
core::transform::Transform,
|
|
||||||
core::math::Vector3,
|
|
||||||
input::{get_key, is_close_requested, is_key_down, VirtualKeyCode},
|
|
||||||
prelude::*,
|
|
||||||
renderer::{Camera, ImageFormat, SpriteRender, SpriteSheet, SpriteSheetFormat, Texture},
|
|
||||||
window::ScreenDimensions,
|
|
||||||
ecs::prelude::{Dispatcher, DispatcherBuilder, Component, DenseVecStorage, Entity},
|
|
||||||
};
|
|
||||||
|
|
||||||
use log::info;
|
|
||||||
use crate::components::*;
|
use crate::components::*;
|
||||||
use std::collections::HashMap;
|
use crate::splash_state::*;
|
||||||
use crate::systems::{BirbGravity, ScrollScrollables};
|
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Component)]
|
||||||
pub struct PlayState<'a, 'b> {
|
pub struct PlayScreen;
|
||||||
|
|
||||||
// Custom dispatch systems for this state
|
pub fn setup_play(
|
||||||
dispatcher: Option<Dispatcher<'a, 'b>>,
|
mut commands: Commands,
|
||||||
|
sprite_handles: Res<SpriteHandles>,
|
||||||
|
window_query: Query<&Window>,
|
||||||
|
) {
|
||||||
|
let Ok(window) = window_query.single() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let width = window.width();
|
||||||
|
let height = window.height();
|
||||||
|
|
||||||
sprites: Vec<Entity>,
|
// Initialize pipe spawn timer
|
||||||
}
|
commands.insert_resource(PipeSpawnTimer {
|
||||||
|
timer: Timer::from_seconds(2.0, TimerMode::Repeating),
|
||||||
|
});
|
||||||
|
|
||||||
impl<'a, 'b> PlayState<'a, 'b> {
|
// Initialize score and score timer
|
||||||
|
commands.insert_resource(Score::default());
|
||||||
|
commands.insert_resource(ScoreTimer {
|
||||||
|
timer: Timer::from_seconds(1.0, TimerMode::Repeating),
|
||||||
|
});
|
||||||
|
|
||||||
fn init_sprites(&mut self, world: &mut World) {
|
// Spawn the bird
|
||||||
|
commands.spawn((
|
||||||
let sprites = world.try_fetch_mut::<HashMap<String, SpriteRender>>().unwrap().clone();
|
Sprite {
|
||||||
let dimensions = (*world.read_resource::<ScreenDimensions>()).clone();
|
image: sprite_handles.texture.clone(),
|
||||||
|
texture_atlas: Some(TextureAtlas {
|
||||||
let birb_sprite = sprites
|
layout: sprite_handles.layout.clone(),
|
||||||
.get("floppy").unwrap();
|
index: SPRITE_BIRD_ANIM_1,
|
||||||
|
}),
|
||||||
let mut transform = Transform::default();
|
..default()
|
||||||
transform.set_scale(Vector3::new(3.0, 3.0, 3.0));
|
},
|
||||||
transform.set_translation_xyz(dimensions.width()/2.0, dimensions.height()/2.0, 0.2);
|
Transform {
|
||||||
|
translation: Vec3::new(width / 2.0, height / 2.0, 0.2),
|
||||||
self.sprites.push(world
|
scale: Vec3::splat(3.0),
|
||||||
.create_entity()
|
..default()
|
||||||
.with(birb_sprite.clone()) // Sprite Render
|
},
|
||||||
.with(Birb {
|
Birb {
|
||||||
vertical_speed: 0.0,
|
vertical_speed: 0.0,
|
||||||
position: 0.0,
|
position: 0.0,
|
||||||
starting_height: 0.0
|
starting_height: 0.0,
|
||||||
})
|
},
|
||||||
.with(transform)
|
BirdAnimation {
|
||||||
.build());
|
timer: Timer::from_seconds(0.1, TimerMode::Repeating),
|
||||||
|
current_frame: 0,
|
||||||
|
},
|
||||||
|
Collider {
|
||||||
|
width: BIRD_WIDTH * 0.6, // Reduce to 60% for more forgiving hitbox
|
||||||
|
height: BIRD_HEIGHT * 0.6,
|
||||||
|
},
|
||||||
|
PlayScreen,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn play_input(
|
||||||
|
keyboard: Res<ButtonInput<KeyCode>>,
|
||||||
|
mut next_state: ResMut<NextState<GameState>>,
|
||||||
|
) {
|
||||||
|
if keyboard.just_pressed(KeyCode::KeyP) {
|
||||||
|
next_state.set(GameState::Splash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b> SimpleState for PlayState<'a, 'b> {
|
pub fn update_score(
|
||||||
|
time: Res<Time>,
|
||||||
fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
|
mut score_timer: ResMut<ScoreTimer>,
|
||||||
|
mut score: ResMut<Score>,
|
||||||
let world = data.world;
|
) {
|
||||||
let dimensions = (*world.read_resource::<ScreenDimensions>()).clone();
|
if score_timer.timer.tick(time.delta()).just_finished() {
|
||||||
|
score.value += 1;
|
||||||
// Create the `DispatcherBuilder` and register some `System`s that should only run for this `State`.
|
|
||||||
let mut dispatcher_builder = DispatcherBuilder::new();
|
|
||||||
dispatcher_builder.add(BirbGravity { fired: false }, "gravity", &[]);
|
|
||||||
|
|
||||||
let mut dispatcher = dispatcher_builder.build();
|
|
||||||
dispatcher.setup(world);
|
|
||||||
|
|
||||||
self.dispatcher = Some(dispatcher);
|
|
||||||
|
|
||||||
PlayState::init_sprites(self, world);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_event(
|
|
||||||
&mut self,
|
|
||||||
mut data: StateData<'_, GameData<'_, '_>>,
|
|
||||||
event: StateEvent,
|
|
||||||
) -> SimpleTrans {
|
|
||||||
|
|
||||||
if let StateEvent::Window(event) = &event {
|
|
||||||
// Check if the window should be closed
|
|
||||||
if is_close_requested(&event) || is_key_down(&event, VirtualKeyCode::Escape) {
|
|
||||||
return Trans::Quit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_key_down(&event, VirtualKeyCode::P) {
|
|
||||||
let world = data.world;
|
|
||||||
for i in &self.sprites {
|
|
||||||
world.delete_entity(*i);
|
|
||||||
}
|
|
||||||
self.sprites.clear();
|
|
||||||
return Trans::Pop;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Trans::None
|
|
||||||
|
pub fn render_score(
|
||||||
|
mut commands: Commands,
|
||||||
|
score: Res<Score>,
|
||||||
|
sprite_handles: Res<SpriteHandles>,
|
||||||
|
window_query: Query<&Window>,
|
||||||
|
existing_digits: Query<Entity, With<ScoreDisplay>>,
|
||||||
|
) {
|
||||||
|
// Only update if score changed
|
||||||
|
if !score.is_changed() {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, data: &mut StateData<'_, GameData<'_, '_>>) -> SimpleTrans {
|
let Ok(window) = window_query.single() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let width = window.width();
|
||||||
|
let height = window.height();
|
||||||
|
|
||||||
if let Some(dispatcher) = self.dispatcher.as_mut() {
|
// Despawn existing score digits
|
||||||
dispatcher.dispatch(&data.world);
|
for entity in existing_digits.iter() {
|
||||||
|
commands.entity(entity).despawn();
|
||||||
}
|
}
|
||||||
Trans::None
|
|
||||||
|
// Convert score to digits
|
||||||
|
let score_string = score.value.to_string();
|
||||||
|
let num_digits = score_string.len() as f32;
|
||||||
|
let digit_spacing = 25.0;
|
||||||
|
let total_width = num_digits * digit_spacing;
|
||||||
|
let start_x = (width - total_width) / 2.0;
|
||||||
|
|
||||||
|
// Spawn digit sprites
|
||||||
|
for (i, digit_char) in score_string.chars().enumerate() {
|
||||||
|
let digit = digit_char.to_digit(10).unwrap() as usize;
|
||||||
|
let sprite_index = SPRITE_NUMBER_0 + digit;
|
||||||
|
|
||||||
|
commands.spawn((
|
||||||
|
Sprite {
|
||||||
|
image: sprite_handles.texture.clone(),
|
||||||
|
texture_atlas: Some(TextureAtlas {
|
||||||
|
layout: sprite_handles.layout.clone(),
|
||||||
|
index: sprite_index,
|
||||||
|
}),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Transform {
|
||||||
|
translation: Vec3::new(start_x + (i as f32 * digit_spacing), height - 50.0, 0.3),
|
||||||
|
scale: Vec3::splat(2.5),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
ScoreDisplay,
|
||||||
|
PlayScreen,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cleanup_play(
|
||||||
|
mut commands: Commands,
|
||||||
|
query: Query<Entity, With<PlayScreen>>,
|
||||||
|
) {
|
||||||
|
for entity in query.iter() {
|
||||||
|
commands.entity(entity).despawn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,104 +1,75 @@
|
|||||||
use amethyst::{
|
use bevy::prelude::*;
|
||||||
assets::{AssetStorage, Loader},
|
use crate::splash_state::*;
|
||||||
core::transform::Transform,
|
|
||||||
core::math::Vector3,
|
#[derive(Component)]
|
||||||
input::{get_mouse_button, get_key, is_close_requested, is_key_down, VirtualKeyCode},
|
pub struct ReadyScreen;
|
||||||
prelude::*,
|
|
||||||
renderer::{Camera, ImageFormat, SpriteRender, SpriteSheet, SpriteSheetFormat, Texture},
|
pub fn setup_ready(
|
||||||
window::ScreenDimensions,
|
mut commands: Commands,
|
||||||
ecs::prelude::{Dispatcher, DispatcherBuilder, Component, DenseVecStorage, Entity},
|
sprite_handles: Res<SpriteHandles>,
|
||||||
|
window_query: Query<&Window>,
|
||||||
|
) {
|
||||||
|
let Ok(window) = window_query.single() else {
|
||||||
|
return;
|
||||||
};
|
};
|
||||||
|
let width = window.width();
|
||||||
|
let height = window.height();
|
||||||
|
|
||||||
use log::info;
|
// Get ready text
|
||||||
use crate::components::*;
|
commands.spawn((
|
||||||
use std::collections::HashMap;
|
Sprite {
|
||||||
use crate::systems::{BirbGravity, ScrollScrollables};
|
image: sprite_handles.texture.clone(),
|
||||||
use crate::play_state::PlayState;
|
texture_atlas: Some(TextureAtlas {
|
||||||
|
layout: sprite_handles.layout.clone(),
|
||||||
|
index: SPRITE_GET_READY_TEXT,
|
||||||
|
}),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Transform {
|
||||||
|
translation: Vec3::new(width * 0.5, height * 0.8, 0.2),
|
||||||
|
scale: Vec3::splat(3.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
ReadyScreen,
|
||||||
|
));
|
||||||
|
|
||||||
#[derive(Default)]
|
// Tap tap dialogue
|
||||||
pub struct ReadyState {
|
commands.spawn((
|
||||||
sprites: Vec<Entity>,
|
Sprite {
|
||||||
|
image: sprite_handles.texture.clone(),
|
||||||
|
texture_atlas: Some(TextureAtlas {
|
||||||
|
layout: sprite_handles.layout.clone(),
|
||||||
|
index: SPRITE_TAP_TAP_DIALOGUE,
|
||||||
|
}),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Transform {
|
||||||
|
translation: Vec3::new(width * 0.5, height * 0.5, 0.2),
|
||||||
|
scale: Vec3::splat(3.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
ReadyScreen,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReadyState {
|
pub fn ready_input(
|
||||||
|
keyboard: Res<ButtonInput<KeyCode>>,
|
||||||
fn init_sprites(&mut self, world: &mut World) {
|
mouse: Res<ButtonInput<MouseButton>>,
|
||||||
|
touches: Res<Touches>,
|
||||||
let dimensions = (*world.read_resource::<ScreenDimensions>()).clone();
|
mut next_state: ResMut<NextState<GameState>>,
|
||||||
let sprites = world.try_fetch_mut::<HashMap<String, SpriteRender>>().unwrap().clone();
|
) {
|
||||||
|
if keyboard.just_pressed(KeyCode::Space)
|
||||||
let get_ready_text_sprite = sprites
|
|| mouse.just_pressed(MouseButton::Left)
|
||||||
.get("get-ready-text").unwrap().clone();
|
|| touches.any_just_pressed() {
|
||||||
|
next_state.set(GameState::Playing);
|
||||||
let tap_tap_dialogue_sprite = sprites
|
|
||||||
.get("tap-tap-dialogue").unwrap().clone();
|
|
||||||
|
|
||||||
let mut transform = Transform::default();
|
|
||||||
transform.set_scale(Vector3::new(3.0, 3.0, 3.0));
|
|
||||||
|
|
||||||
transform.set_translation_xyz(dimensions.width()*0.5, dimensions.height()*0.8, 0.2);
|
|
||||||
|
|
||||||
self.sprites.push(world
|
|
||||||
.create_entity()
|
|
||||||
.with(get_ready_text_sprite.clone()) // Sprite Render
|
|
||||||
.with(transform.clone())
|
|
||||||
.build());
|
|
||||||
|
|
||||||
transform.set_translation_xyz(dimensions.width()*0.5, dimensions.height()*0.5, 0.2);
|
|
||||||
|
|
||||||
self.sprites.push(world
|
|
||||||
.create_entity()
|
|
||||||
.with(tap_tap_dialogue_sprite.clone()) // Sprite Render
|
|
||||||
.with(transform.clone())
|
|
||||||
.build());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl SimpleState for ReadyState {
|
|
||||||
|
|
||||||
fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
|
pub fn cleanup_ready(
|
||||||
let world = data.world;
|
mut commands: Commands,
|
||||||
ReadyState::init_sprites(self, world);
|
query: Query<Entity, With<ReadyScreen>>,
|
||||||
}
|
) {
|
||||||
|
for entity in query.iter() {
|
||||||
fn on_resume(&mut self, data: StateData<'_, GameData<'_, '_>>) {
|
commands.entity(entity).despawn();
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_event(
|
|
||||||
&mut self,
|
|
||||||
mut data: StateData<'_, GameData<'_, '_>>,
|
|
||||||
event: StateEvent,
|
|
||||||
) -> SimpleTrans {
|
|
||||||
|
|
||||||
if let StateEvent::Ui(event) = &event {
|
|
||||||
|
|
||||||
// if event.event_type == UiEventType::Click {
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if let StateEvent::Window(event) = &event {
|
|
||||||
// Check if the window should be closed
|
|
||||||
if is_close_requested(&event) || is_key_down(&event, VirtualKeyCode::Escape) {
|
|
||||||
return Trans::Quit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the window should be closed
|
|
||||||
if is_key_down(&event, VirtualKeyCode::Space) {
|
|
||||||
let world = data.world;
|
|
||||||
for i in &self.sprites {
|
|
||||||
world.delete_entity(*i);
|
|
||||||
}
|
|
||||||
self.sprites.clear();
|
|
||||||
|
|
||||||
return Trans::Push(Box::new(PlayState::default()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Trans::None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&mut self, data: &mut StateData<'_, GameData<'_, '_>>) -> SimpleTrans {
|
|
||||||
Trans::None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,241 +1,332 @@
|
|||||||
use amethyst::{
|
use bevy::prelude::*;
|
||||||
assets::{AssetStorage, Loader},
|
|
||||||
core::transform::Transform,
|
|
||||||
core::math::Vector3,
|
|
||||||
input::{get_key, is_close_requested, is_key_down, VirtualKeyCode},
|
|
||||||
prelude::*,
|
|
||||||
renderer::{Camera, ImageFormat, SpriteRender, SpriteSheet, SpriteSheetFormat, Texture},
|
|
||||||
window::ScreenDimensions,
|
|
||||||
ecs::prelude::{Dispatcher, DispatcherBuilder, Component, DenseVecStorage, Entity},
|
|
||||||
};
|
|
||||||
|
|
||||||
use log::info;
|
|
||||||
use crate::components::*;
|
use crate::components::*;
|
||||||
use std::collections::HashMap;
|
|
||||||
use crate::systems::{BirbGravity, ScrollScrollables};
|
|
||||||
use crate::ready_state::ReadyState;
|
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
|
||||||
#[derive(Default)]
|
pub enum GameState {
|
||||||
pub struct SplashState {
|
#[default]
|
||||||
sprites: Vec<Entity>,
|
Splash,
|
||||||
persistent_sprites: Vec<Entity>,
|
Ready,
|
||||||
|
Playing,
|
||||||
|
GameOver,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SplashState {
|
#[derive(Component)]
|
||||||
|
pub struct SplashScreen;
|
||||||
|
|
||||||
fn load_sprites(world: &mut World) -> HashMap<String, SpriteRender> {
|
#[derive(Component)]
|
||||||
// Load the texture for our sprites. We'll later need to
|
pub struct PersistentSprite;
|
||||||
// add a handle to this texture to our `SpriteRender`s, so
|
|
||||||
// we need to keep a reference to it.
|
#[derive(Resource)]
|
||||||
let texture_handle = {
|
pub struct SpriteHandles {
|
||||||
let loader = world.read_resource::<Loader>();
|
pub texture: Handle<Image>,
|
||||||
let texture_storage = world.read_resource::<AssetStorage<Texture>>();
|
pub layout: Handle<TextureAtlasLayout>,
|
||||||
loader.load(
|
}
|
||||||
"sprites/flappy.png",
|
|
||||||
ImageFormat::default(),
|
// Sprite indices from the spritesheet
|
||||||
(),
|
pub const SPRITE_DAY_BACKGROUND: usize = 0;
|
||||||
&texture_storage,
|
pub const SPRITE_NIGHT_BACKGROUND: usize = 1;
|
||||||
)
|
pub const SPRITE_DOWN_PIPE: usize = 2;
|
||||||
|
pub const SPRITE_UP_PIPE: usize = 3;
|
||||||
|
pub const SPRITE_GROUND: usize = 4;
|
||||||
|
pub const SPRITE_FLOPPY: usize = 5;
|
||||||
|
|
||||||
|
// Sprite dimensions (from RON file)
|
||||||
|
pub const PIPE_WIDTH: f32 = 26.0;
|
||||||
|
pub const PIPE_HEIGHT: f32 = 160.0;
|
||||||
|
pub const BIRD_WIDTH: f32 = 17.0;
|
||||||
|
pub const BIRD_HEIGHT: f32 = 13.0;
|
||||||
|
pub const SPRITE_TAP_TAP_DIALOGUE: usize = 6;
|
||||||
|
pub const SPRITE_PLAY_BUTTON: usize = 7;
|
||||||
|
pub const SPRITE_LEADERBOARD_BUTTON: usize = 8;
|
||||||
|
pub const SPRITE_GET_READY_TEXT: usize = 9;
|
||||||
|
pub const SPRITE_FLAPPY_BIRD_TEXT: usize = 10;
|
||||||
|
pub const SPRITE_GAME_OVER: usize = 11;
|
||||||
|
pub const SPRITE_NUMBER_0: usize = 12;
|
||||||
|
pub const SPRITE_NUMBER_1: usize = 13;
|
||||||
|
pub const SPRITE_NUMBER_2: usize = 14;
|
||||||
|
pub const SPRITE_NUMBER_3: usize = 15;
|
||||||
|
pub const SPRITE_NUMBER_4: usize = 16;
|
||||||
|
pub const SPRITE_NUMBER_5: usize = 17;
|
||||||
|
pub const SPRITE_NUMBER_6: usize = 18;
|
||||||
|
pub const SPRITE_NUMBER_7: usize = 19;
|
||||||
|
pub const SPRITE_NUMBER_8: usize = 20;
|
||||||
|
pub const SPRITE_NUMBER_9: usize = 21;
|
||||||
|
|
||||||
|
// Game over screen sprites
|
||||||
|
pub const SPRITE_MEDAL_PLATINUM: usize = 22;
|
||||||
|
pub const SPRITE_SCORE_CARD: usize = 23;
|
||||||
|
pub const SPRITE_MEDAL_GOLD: usize = 24;
|
||||||
|
pub const SPRITE_MEDAL_SILVER: usize = 25;
|
||||||
|
pub const SPRITE_MEDAL_BRONZE: usize = 26;
|
||||||
|
|
||||||
|
// Tiny number sprites for score card
|
||||||
|
pub const SPRITE_TINY_NUMBER_0: usize = 27;
|
||||||
|
pub const SPRITE_TINY_NUMBER_1: usize = 28;
|
||||||
|
pub const SPRITE_TINY_NUMBER_2: usize = 29;
|
||||||
|
pub const SPRITE_TINY_NUMBER_3: usize = 30;
|
||||||
|
pub const SPRITE_TINY_NUMBER_4: usize = 31;
|
||||||
|
pub const SPRITE_TINY_NUMBER_5: usize = 32;
|
||||||
|
pub const SPRITE_TINY_NUMBER_6: usize = 33;
|
||||||
|
pub const SPRITE_TINY_NUMBER_7: usize = 34;
|
||||||
|
pub const SPRITE_TINY_NUMBER_8: usize = 35;
|
||||||
|
pub const SPRITE_TINY_NUMBER_9: usize = 36;
|
||||||
|
|
||||||
|
// Bird animation sprites
|
||||||
|
pub const SPRITE_BIRD_ANIM_1: usize = 37;
|
||||||
|
pub const SPRITE_BIRD_ANIM_2: usize = 38;
|
||||||
|
pub const SPRITE_BIRD_ANIM_3: usize = 39;
|
||||||
|
|
||||||
|
pub fn load_sprites(
|
||||||
|
mut commands: Commands,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
|
mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
|
||||||
|
) {
|
||||||
|
let texture = asset_server.load("sprites/flappy.png");
|
||||||
|
|
||||||
|
// Create texture atlas layout based on the spritesheet
|
||||||
|
// Coordinates from flappy.ron
|
||||||
|
let mut layout = TextureAtlasLayout::new_empty(UVec2::new(512, 512));
|
||||||
|
|
||||||
|
// Add sprite rectangles using correct coordinates from the RON file
|
||||||
|
layout.add_texture(URect::new(0, 0, 144, 256)); // 0: day-background
|
||||||
|
layout.add_texture(URect::new(146, 0, 290, 256)); // 1: night-background
|
||||||
|
layout.add_texture(URect::new(56, 323, 82, 483)); // 2: down-pipe
|
||||||
|
layout.add_texture(URect::new(84, 323, 110, 483)); // 3: up-pipe
|
||||||
|
layout.add_texture(URect::new(292, 0, 460, 56)); // 4: ground
|
||||||
|
layout.add_texture(URect::new(3, 490, 20, 503)); // 5: floppy
|
||||||
|
layout.add_texture(URect::new(292, 91, 348, 139)); // 6: tap-tap-dialogue
|
||||||
|
layout.add_texture(URect::new(354, 118, 406, 147)); // 7: play-button
|
||||||
|
layout.add_texture(URect::new(414, 118, 466, 147)); // 8: leaderboard-button
|
||||||
|
layout.add_texture(URect::new(295, 59, 386, 83)); // 9: get-ready-text
|
||||||
|
layout.add_texture(URect::new(351, 91, 441, 116)); // 10: flappy-bird-text
|
||||||
|
layout.add_texture(URect::new(395, 58, 495, 80)); // 11: game-over
|
||||||
|
layout.add_texture(URect::new(496, 60, 508, 78)); // 12: number 0
|
||||||
|
layout.add_texture(URect::new(136, 455, 144, 473)); // 13: number 1
|
||||||
|
layout.add_texture(URect::new(292, 160, 304, 178)); // 14: number 2
|
||||||
|
layout.add_texture(URect::new(306, 160, 318, 178)); // 15: number 3
|
||||||
|
layout.add_texture(URect::new(320, 160, 332, 178)); // 16: number 4
|
||||||
|
layout.add_texture(URect::new(334, 160, 346, 178)); // 17: number 5
|
||||||
|
layout.add_texture(URect::new(292, 184, 304, 202)); // 18: number 6
|
||||||
|
layout.add_texture(URect::new(306, 184, 318, 202)); // 19: number 7
|
||||||
|
layout.add_texture(URect::new(320, 184, 332, 202)); // 20: number 8
|
||||||
|
layout.add_texture(URect::new(334, 184, 346, 202)); // 21: number 9
|
||||||
|
|
||||||
|
// Game over screen sprites
|
||||||
|
layout.add_texture(URect::new(121, 258, 143, 280)); // 22: medal platinum
|
||||||
|
layout.add_texture(URect::new(3, 259, 116, 316)); // 23: score card
|
||||||
|
layout.add_texture(URect::new(121, 282, 143, 304)); // 24: medal gold
|
||||||
|
layout.add_texture(URect::new(112, 453, 134, 475)); // 25: medal silver
|
||||||
|
layout.add_texture(URect::new(112, 477, 134, 499)); // 26: medal bronze
|
||||||
|
|
||||||
|
// Tiny number sprites for score card
|
||||||
|
layout.add_texture(URect::new(138, 323, 144, 330)); // 27: tiny number 0
|
||||||
|
layout.add_texture(URect::new(140, 332, 144, 344)); // 28: tiny number 1
|
||||||
|
layout.add_texture(URect::new(138, 349, 144, 356)); // 29: tiny number 2
|
||||||
|
layout.add_texture(URect::new(138, 358, 144, 365)); // 30: tiny number 3
|
||||||
|
layout.add_texture(URect::new(138, 375, 144, 382)); // 31: tiny number 4
|
||||||
|
layout.add_texture(URect::new(138, 384, 144, 391)); // 32: tiny number 5
|
||||||
|
layout.add_texture(URect::new(138, 401, 144, 408)); // 33: tiny number 6
|
||||||
|
layout.add_texture(URect::new(138, 410, 144, 417)); // 34: tiny number 7
|
||||||
|
layout.add_texture(URect::new(138, 427, 144, 434)); // 35: tiny number 8
|
||||||
|
layout.add_texture(URect::new(138, 436, 144, 443)); // 36: tiny number 9
|
||||||
|
|
||||||
|
// Bird animation sprites
|
||||||
|
layout.add_texture(URect::new(3, 491, 20, 503)); // 37: bird animation 1
|
||||||
|
layout.add_texture(URect::new(31, 491, 48, 503)); // 38: bird animation 2
|
||||||
|
layout.add_texture(URect::new(59, 491, 76, 503)); // 39: bird animation 3
|
||||||
|
|
||||||
|
let layout_handle = texture_atlases.add(layout);
|
||||||
|
|
||||||
|
commands.insert_resource(SpriteHandles {
|
||||||
|
texture: texture.clone(),
|
||||||
|
layout: layout_handle,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup_splash(
|
||||||
|
mut commands: Commands,
|
||||||
|
sprite_handles: Res<SpriteHandles>,
|
||||||
|
window_query: Query<&Window>,
|
||||||
|
) {
|
||||||
|
let Ok(window) = window_query.single() else {
|
||||||
|
return;
|
||||||
};
|
};
|
||||||
|
let width = window.width();
|
||||||
|
let height = window.height();
|
||||||
|
|
||||||
// Load the spritesheet definition file, which contains metadata on our
|
// Setup camera
|
||||||
// spritesheet texture.
|
commands.spawn((Camera2d, Transform::from_xyz(width * 0.5, height * 0.5, 0.0)));
|
||||||
let sheet_handle = {
|
|
||||||
let loader = world.read_resource::<Loader>();
|
|
||||||
let sheet_storage = world.read_resource::<AssetStorage<SpriteSheet>>();
|
|
||||||
loader.load(
|
|
||||||
"sprites/flappy.ron",
|
|
||||||
SpriteSheetFormat(texture_handle),
|
|
||||||
(),
|
|
||||||
&sheet_storage,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
let sprite_map = vec![
|
// Background sprites (persistent across states)
|
||||||
("day-background".to_string(), 0),
|
commands.spawn((
|
||||||
("night-background".to_string(), 1),
|
Sprite {
|
||||||
("down-pipe".to_string(), 2),
|
image: sprite_handles.texture.clone(),
|
||||||
("up-pipe".to_string(), 3),
|
texture_atlas: Some(TextureAtlas {
|
||||||
("ground".to_string(), 4),
|
layout: sprite_handles.layout.clone(),
|
||||||
("floppy".to_string(), 5),
|
index: SPRITE_DAY_BACKGROUND,
|
||||||
("tap-tap-dialogue".to_string(), 6),
|
}),
|
||||||
("play-button".to_string(), 7),
|
..default()
|
||||||
("leaderboard-button".to_string(), 8),
|
},
|
||||||
("get-ready-text".to_string(), 9),
|
Transform {
|
||||||
("flappy-bird-text".to_string(), 10),
|
translation: Vec3::new(3.0 * 143.0 / 2.0, 3.0 * 256.0 / 2.0, 0.0),
|
||||||
];
|
scale: Vec3::splat(3.0),
|
||||||
|
..default()
|
||||||
sprite_map.iter()
|
},
|
||||||
.map(|i| (i.0.clone(), SpriteRender {
|
TiledScroller {
|
||||||
sprite_sheet: sheet_handle.clone(),
|
speed: -120.0,
|
||||||
sprite_number: i.1,
|
|
||||||
}))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_camera(world: &mut World) {
|
|
||||||
|
|
||||||
let dimensions = (*world.read_resource::<ScreenDimensions>()).clone();
|
|
||||||
|
|
||||||
// Center the camera in the middle of the screen, and let it cover
|
|
||||||
// the entire screen
|
|
||||||
let mut transform = Transform::default();
|
|
||||||
transform.set_translation_xyz(dimensions.width() * 0.5, dimensions.height() * 0.5, 1.);
|
|
||||||
|
|
||||||
world
|
|
||||||
.create_entity()
|
|
||||||
.with(Camera::standard_2d(dimensions.width(), dimensions.height()))
|
|
||||||
.with(transform)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_sprites(&mut self, world: &mut World) {
|
|
||||||
|
|
||||||
let sprites = world.try_fetch_mut::<HashMap<String, SpriteRender>>().unwrap().clone();
|
|
||||||
|
|
||||||
let dimensions = (*world.read_resource::<ScreenDimensions>()).clone();
|
|
||||||
|
|
||||||
let flappy_bird_text_sprite = sprites
|
|
||||||
.get("flappy-bird-text").unwrap().clone();
|
|
||||||
let play_button_sprite = sprites
|
|
||||||
.get("play-button").unwrap().clone();
|
|
||||||
let leaderboard_button_sprite = sprites
|
|
||||||
.get("leaderboard-button").unwrap().clone();
|
|
||||||
let background_sprite = sprites
|
|
||||||
.get("day-background").unwrap().clone();
|
|
||||||
let night_background_sprite = sprites
|
|
||||||
.get("night-background").unwrap().clone();
|
|
||||||
let ground_sprite = sprites
|
|
||||||
.get("ground").unwrap().clone();
|
|
||||||
|
|
||||||
|
|
||||||
let mut transform = Transform::default();
|
|
||||||
transform.set_scale(Vector3::new(3.0, 3.0, 3.0));
|
|
||||||
transform.set_translation_xyz(3.0*143.0/2.0, 3.0*256.0/2.0, 0.0);
|
|
||||||
|
|
||||||
self.persistent_sprites.push(world
|
|
||||||
.create_entity()
|
|
||||||
.with(background_sprite.clone()) // Sprite Render
|
|
||||||
.with(TiledScroller {
|
|
||||||
speed: -75.0,
|
|
||||||
position: 1.0,
|
position: 1.0,
|
||||||
width: 143.0 * 3.0,
|
width: 143.0 * 3.0,
|
||||||
height: 256.0 * 3.0,
|
height: 256.0 * 3.0,
|
||||||
})
|
},
|
||||||
.with(transform.clone())
|
PersistentSprite,
|
||||||
.build());
|
));
|
||||||
|
|
||||||
transform.set_translation_xyz(3.0*143.0/2.0*3.0, 3.0*256.0/2.0, 0.0);
|
commands.spawn((
|
||||||
|
Sprite {
|
||||||
self.persistent_sprites.push(world
|
image: sprite_handles.texture.clone(),
|
||||||
.create_entity()
|
texture_atlas: Some(TextureAtlas {
|
||||||
.with(background_sprite.clone()) // Sprite Render
|
layout: sprite_handles.layout.clone(),
|
||||||
.with(TiledScroller {
|
index: SPRITE_DAY_BACKGROUND,
|
||||||
speed: -75.0,
|
}),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Transform {
|
||||||
|
translation: Vec3::new(3.0 * 143.0 / 2.0 * 3.0, 3.0 * 256.0 / 2.0, 0.0),
|
||||||
|
scale: Vec3::splat(3.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
TiledScroller {
|
||||||
|
speed: -120.0,
|
||||||
position: 2.0,
|
position: 2.0,
|
||||||
width: 143.0 * 3.0,
|
width: 143.0 * 3.0,
|
||||||
height: 256.0 * 3.0,
|
height: 256.0 * 3.0,
|
||||||
})
|
},
|
||||||
.with(transform.clone())
|
PersistentSprite,
|
||||||
.build());
|
));
|
||||||
|
|
||||||
transform.set_translation_xyz(3.0*168.0/2.0, 3.0*56.0/2.0, 0.1);
|
// Ground sprites - positioned at bottom of screen
|
||||||
|
let ground_height = 56.0 * 3.0; // 168px
|
||||||
|
let ground_y = ground_height / 2.0; // Center the sprite vertically at half its height
|
||||||
|
|
||||||
self.persistent_sprites.push(world
|
commands.spawn((
|
||||||
.create_entity()
|
Sprite {
|
||||||
.with(ground_sprite.clone()) // Sprite Render
|
image: sprite_handles.texture.clone(),
|
||||||
.with(TiledScroller {
|
texture_atlas: Some(TextureAtlas {
|
||||||
speed: -100.0,
|
layout: sprite_handles.layout.clone(),
|
||||||
|
index: SPRITE_GROUND,
|
||||||
|
}),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Transform {
|
||||||
|
translation: Vec3::new(3.0 * 168.0 / 2.0, ground_y, 0.1),
|
||||||
|
scale: Vec3::splat(3.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
TiledScroller {
|
||||||
|
speed: -120.0,
|
||||||
position: 2.0,
|
position: 2.0,
|
||||||
width: 167.0 * 3.0,
|
width: 167.0 * 3.0,
|
||||||
height: 56.0 * 3.0,
|
height: ground_height,
|
||||||
})
|
},
|
||||||
.with(transform.clone())
|
Ground,
|
||||||
.build());
|
PersistentSprite,
|
||||||
|
));
|
||||||
|
|
||||||
transform.set_translation_xyz(3.0*168.0/2.0*3.0, 3.0*56.0/2.0, 0.1);
|
commands.spawn((
|
||||||
|
Sprite {
|
||||||
self.persistent_sprites.push(world
|
image: sprite_handles.texture.clone(),
|
||||||
.create_entity()
|
texture_atlas: Some(TextureAtlas {
|
||||||
.with(ground_sprite.clone()) // Sprite Render
|
layout: sprite_handles.layout.clone(),
|
||||||
.with(TiledScroller {
|
index: SPRITE_GROUND,
|
||||||
speed: -100.0,
|
}),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Transform {
|
||||||
|
translation: Vec3::new(3.0 * 168.0 / 2.0 * 3.0, ground_y, 0.1),
|
||||||
|
scale: Vec3::splat(3.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
TiledScroller {
|
||||||
|
speed: -120.0,
|
||||||
position: 2.0,
|
position: 2.0,
|
||||||
width: 167.0 * 3.0,
|
width: 167.0 * 3.0,
|
||||||
height: 56.0 * 3.0,
|
height: ground_height,
|
||||||
})
|
},
|
||||||
.with(transform.clone())
|
Ground,
|
||||||
.build());
|
PersistentSprite,
|
||||||
|
));
|
||||||
|
|
||||||
transform.set_translation_xyz(dimensions.width()*0.5, dimensions.height()*0.8, 0.2);
|
// Splash screen specific sprites
|
||||||
|
commands.spawn((
|
||||||
|
Sprite {
|
||||||
|
image: sprite_handles.texture.clone(),
|
||||||
|
texture_atlas: Some(TextureAtlas {
|
||||||
|
layout: sprite_handles.layout.clone(),
|
||||||
|
index: SPRITE_FLAPPY_BIRD_TEXT,
|
||||||
|
}),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Transform {
|
||||||
|
translation: Vec3::new(width * 0.5, height * 0.8, 0.2),
|
||||||
|
scale: Vec3::splat(3.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
SplashScreen,
|
||||||
|
));
|
||||||
|
|
||||||
self.sprites.push(world
|
commands.spawn((
|
||||||
.create_entity()
|
Sprite {
|
||||||
.with(flappy_bird_text_sprite.clone())
|
image: sprite_handles.texture.clone(),
|
||||||
.with(transform.clone())
|
texture_atlas: Some(TextureAtlas {
|
||||||
.build());
|
layout: sprite_handles.layout.clone(),
|
||||||
|
index: SPRITE_PLAY_BUTTON,
|
||||||
|
}),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Transform {
|
||||||
|
translation: Vec3::new(width * 0.25, height * 0.4, 0.2),
|
||||||
|
scale: Vec3::splat(3.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
SplashScreen,
|
||||||
|
));
|
||||||
|
|
||||||
transform.set_translation_xyz(dimensions.width()*0.25, dimensions.height()*0.4, 0.2);
|
commands.spawn((
|
||||||
|
Sprite {
|
||||||
|
image: sprite_handles.texture.clone(),
|
||||||
|
texture_atlas: Some(TextureAtlas {
|
||||||
|
layout: sprite_handles.layout.clone(),
|
||||||
|
index: SPRITE_LEADERBOARD_BUTTON,
|
||||||
|
}),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Transform {
|
||||||
|
translation: Vec3::new(width * 0.75, height * 0.4, 0.2),
|
||||||
|
scale: Vec3::splat(3.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
SplashScreen,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
self.sprites.push(world
|
pub fn splash_input(
|
||||||
.create_entity()
|
keyboard: Res<ButtonInput<KeyCode>>,
|
||||||
.with(play_button_sprite.clone())
|
mouse: Res<ButtonInput<MouseButton>>,
|
||||||
.with(transform.clone())
|
touches: Res<Touches>,
|
||||||
.build());
|
mut next_state: ResMut<NextState<GameState>>,
|
||||||
|
) {
|
||||||
transform.set_translation_xyz(dimensions.width()*0.75, dimensions.height()*0.4, 0.2);
|
if keyboard.just_pressed(KeyCode::Space)
|
||||||
|
|| mouse.just_pressed(MouseButton::Left)
|
||||||
self.sprites.push(world
|
|| touches.any_just_pressed() {
|
||||||
.create_entity()
|
next_state.set(GameState::Ready);
|
||||||
.with(leaderboard_button_sprite.clone())
|
|
||||||
.with(transform.clone())
|
|
||||||
.build());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SimpleState for SplashState {
|
pub fn cleanup_splash(
|
||||||
|
mut commands: Commands,
|
||||||
fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
|
query: Query<Entity, With<SplashScreen>>,
|
||||||
let world = data.world;
|
) {
|
||||||
|
for entity in query.iter() {
|
||||||
// Load the sprites. Insert them into the world as this is the first function to be called
|
commands.entity(entity).despawn();
|
||||||
let sprites = SplashState::load_sprites(world);
|
|
||||||
world.insert(sprites.clone());
|
|
||||||
|
|
||||||
SplashState::load_sprites(world);
|
|
||||||
SplashState::init_camera(world);
|
|
||||||
SplashState::init_sprites(self, world);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_event(
|
|
||||||
&mut self,
|
|
||||||
mut data: StateData<'_, GameData<'_, '_>>,
|
|
||||||
event: StateEvent,
|
|
||||||
) -> SimpleTrans {
|
|
||||||
|
|
||||||
if let StateEvent::Window(event) = &event {
|
|
||||||
// Check if the window should be closed
|
|
||||||
if is_close_requested(&event) || is_key_down(&event, VirtualKeyCode::Escape) {
|
|
||||||
return Trans::Quit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the window should be closed
|
|
||||||
if is_key_down(&event, VirtualKeyCode::Space) {
|
|
||||||
let world = data.world;
|
|
||||||
for i in &self.sprites {
|
|
||||||
world.delete_entity(*i);
|
|
||||||
}
|
|
||||||
self.sprites.clear();
|
|
||||||
|
|
||||||
return Trans::Push(Box::new(ReadyState::default()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Trans::None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&mut self, data: &mut StateData<'_, GameData<'_, '_>>) -> SimpleTrans {
|
|
||||||
Trans::None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
334
src/systems.rs
@@ -1,68 +1,296 @@
|
|||||||
use amethyst::{
|
use bevy::prelude::*;
|
||||||
core::SystemDesc,
|
use crate::components::*;
|
||||||
core::timing::Time,
|
use crate::splash_state::*;
|
||||||
core::transform::{Transform, TransformBundle},
|
use rand::Rng;
|
||||||
derive::SystemDesc,
|
|
||||||
ecs::prelude::{},
|
// System to scroll backgrounds and ground
|
||||||
ecs::prelude::{
|
pub fn scroll_scrollables(
|
||||||
Component, DenseVecStorage, Entity, Join, Read,
|
mut query: Query<(&mut Transform, &mut TiledScroller)>,
|
||||||
ReadStorage, System, SystemData, WriteStorage, Write
|
time: Res<Time>,
|
||||||
},
|
) {
|
||||||
input::{InputHandler, StringBindings},
|
for (mut transform, scroller) in query.iter_mut() {
|
||||||
|
transform.translation.x += scroller.speed * time.delta_secs();
|
||||||
|
|
||||||
|
// Reset position when off screen
|
||||||
|
if transform.translation.x + scroller.width / 2.0 < 0.0 {
|
||||||
|
transform.translation.x = scroller.width / 2.0 * 3.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// System to apply gravity to the bird
|
||||||
|
pub fn bird_gravity(
|
||||||
|
mut query: Query<(&mut Transform, &mut Birb)>,
|
||||||
|
time: Res<Time>,
|
||||||
|
keyboard: Res<ButtonInput<KeyCode>>,
|
||||||
|
mouse: Res<ButtonInput<MouseButton>>,
|
||||||
|
touches: Res<Touches>,
|
||||||
|
) {
|
||||||
|
for (mut transform, mut birb) in query.iter_mut() {
|
||||||
|
// Flap on space key, mouse click, or touch
|
||||||
|
if keyboard.just_pressed(KeyCode::Space)
|
||||||
|
|| mouse.just_pressed(MouseButton::Left)
|
||||||
|
|| touches.any_just_pressed() {
|
||||||
|
birb.vertical_speed = 600.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply gravity
|
||||||
|
birb.vertical_speed -= 1500.0 * time.delta_secs();
|
||||||
|
transform.translation.y += birb.vertical_speed * time.delta_secs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// System to animate the bird sprite
|
||||||
|
pub fn animate_bird(
|
||||||
|
time: Res<Time>,
|
||||||
|
mut query: Query<(&mut Sprite, &mut BirdAnimation)>,
|
||||||
|
) {
|
||||||
|
for (mut sprite, mut anim) in query.iter_mut() {
|
||||||
|
anim.timer.tick(time.delta());
|
||||||
|
|
||||||
|
if anim.timer.just_finished() {
|
||||||
|
anim.current_frame = (anim.current_frame + 1) % 3;
|
||||||
|
|
||||||
|
let frame_index = match anim.current_frame {
|
||||||
|
0 => SPRITE_BIRD_ANIM_1,
|
||||||
|
1 => SPRITE_BIRD_ANIM_2,
|
||||||
|
2 => SPRITE_BIRD_ANIM_3,
|
||||||
|
_ => SPRITE_BIRD_ANIM_1,
|
||||||
};
|
};
|
||||||
|
|
||||||
use log::info;
|
if let Some(ref mut atlas) = sprite.texture_atlas {
|
||||||
use crate::components::*;
|
atlas.index = frame_index;
|
||||||
|
|
||||||
pub struct ScrollScrollables;
|
|
||||||
|
|
||||||
/*
|
|
||||||
Pausable systems
|
|
||||||
https://book.amethyst.rs/stable/controlling_system_execution/pausable_systems.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
// This system iterates all the objects with transform (and falling object) component
|
|
||||||
impl<'a> System<'a> for ScrollScrollables {
|
|
||||||
type SystemData = (
|
|
||||||
WriteStorage<'a, Transform>,
|
|
||||||
WriteStorage<'a, TiledScroller>,
|
|
||||||
Read<'a, Time>,
|
|
||||||
);
|
|
||||||
|
|
||||||
fn run(&mut self, (mut transforms, mut scrolling, time): Self::SystemData) {
|
|
||||||
for (mut transform, mut object) in (&mut transforms, &mut scrolling).join() {
|
|
||||||
|
|
||||||
transform.prepend_translation_x(object.speed * time.delta_seconds());
|
|
||||||
if transform.translation().x+object.width/2.0 < 0.0 {
|
|
||||||
transform.set_translation_x(object.width/2.0*3.0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
// System to rotate bird based on velocity
|
||||||
pub struct BirbGravity {
|
pub fn rotate_bird(
|
||||||
pub fired: bool,
|
mut query: Query<(&mut Transform, &Birb)>,
|
||||||
|
) {
|
||||||
|
for (mut transform, birb) in query.iter_mut() {
|
||||||
|
// Map velocity to rotation angle
|
||||||
|
// Upward velocity (600) -> 45 degrees upward (-45° or -π/4 rad)
|
||||||
|
// Downward velocity -> 45 degrees downward (45° or π/4 rad)
|
||||||
|
// Clamp velocity between -600 and 600 for smooth rotation
|
||||||
|
let clamped_velocity = birb.vertical_speed.clamp(-600.0, 600.0);
|
||||||
|
|
||||||
|
// Map velocity to angle: -600 (falling) = -45°, +600 (rising) = 45°
|
||||||
|
let angle = (clamped_velocity / 600.0) * (std::f32::consts::PI / 4.0);
|
||||||
|
|
||||||
|
transform.rotation = Quat::from_rotation_z(angle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This system iterates all the objects with transform (and falling object) component
|
// System to spawn pipes
|
||||||
impl<'a> System<'a> for BirbGravity {
|
pub fn spawn_pipes(
|
||||||
type SystemData = (
|
mut commands: Commands,
|
||||||
WriteStorage<'a, Transform>,
|
mut timer: ResMut<PipeSpawnTimer>,
|
||||||
WriteStorage<'a, Birb>,
|
time: Res<Time>,
|
||||||
Read<'a, Time>,
|
sprite_handles: Res<SpriteHandles>,
|
||||||
Read<'a, InputHandler<StringBindings>>,
|
window_query: Query<&Window>,
|
||||||
);
|
) {
|
||||||
|
timer.timer.tick(time.delta());
|
||||||
|
|
||||||
fn run(&mut self, (mut transforms, mut scrolling, time, input): Self::SystemData) {
|
if timer.timer.just_finished() {
|
||||||
for (mut transform, mut object) in (&mut transforms, &mut scrolling).join() {
|
let Ok(window) = window_query.single() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
//match game.current_state
|
let mut rng = rand::rng();
|
||||||
if input.action_is_down("flap").expect("No action") {
|
let gap_size = 180.0;
|
||||||
object.vertical_speed = 600.0;
|
|
||||||
|
// Ground sprite is centered at y = 56.0 * 3.0 / 2.0 = 84px
|
||||||
|
// Top of ground is at y = 84 + (168/2) = 168px
|
||||||
|
let ground_height = 56.0 * 3.0; // 168px
|
||||||
|
let ground_y = ground_height / 2.0; // 84px (center of ground sprite)
|
||||||
|
let ground_top = ground_y + ground_height / 2.0; // 168px (top edge of ground)
|
||||||
|
|
||||||
|
// Calculate gap center position
|
||||||
|
// Ensure minimum clearance above ground and below ceiling
|
||||||
|
let min_gap_y = ground_top + gap_size / 2.0 + 50.0; // 50px clearance above ground
|
||||||
|
let max_gap_y = window.height() - gap_size / 2.0 - 50.0; // 50px clearance below ceiling
|
||||||
|
|
||||||
|
let gap_y = rng.random_range(min_gap_y..max_gap_y);
|
||||||
|
|
||||||
|
let pipe_speed = -120.0; // Match background/ground speed
|
||||||
|
|
||||||
|
// Calculate actual pipe positions for debugging
|
||||||
|
let top_pipe_y = gap_y + gap_size / 2.0 + PIPE_HEIGHT * 3.0 / 2.0;
|
||||||
|
let bottom_pipe_y = gap_y - gap_size / 2.0 - PIPE_HEIGHT * 3.0 / 2.0;
|
||||||
|
let bottom_pipe_bottom = bottom_pipe_y - PIPE_HEIGHT * 3.0 / 2.0;
|
||||||
|
|
||||||
|
println!("Ground top: {}, Bottom pipe bottom edge: {}, Overlap: {}",
|
||||||
|
ground_top, bottom_pipe_bottom, ground_top - bottom_pipe_bottom);
|
||||||
|
|
||||||
|
// Spawn top pipe (down-pipe)
|
||||||
|
commands.spawn((
|
||||||
|
Sprite {
|
||||||
|
image: sprite_handles.texture.clone(),
|
||||||
|
texture_atlas: Some(TextureAtlas {
|
||||||
|
layout: sprite_handles.layout.clone(),
|
||||||
|
index: SPRITE_DOWN_PIPE,
|
||||||
|
}),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Transform {
|
||||||
|
translation: Vec3::new(window.width() + 50.0, gap_y + gap_size / 2.0 + PIPE_HEIGHT * 3.0 / 2.0, 0.05),
|
||||||
|
scale: Vec3::splat(3.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Pipe { speed: pipe_speed },
|
||||||
|
Collider {
|
||||||
|
width: PIPE_WIDTH * 0.7, // Reduce to 70% for more forgiving hitbox
|
||||||
|
height: PIPE_HEIGHT * 0.9, // Slightly reduce height too
|
||||||
|
},
|
||||||
|
crate::play_state::PlayScreen,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Spawn bottom pipe (up-pipe)
|
||||||
|
commands.spawn((
|
||||||
|
Sprite {
|
||||||
|
image: sprite_handles.texture.clone(),
|
||||||
|
texture_atlas: Some(TextureAtlas {
|
||||||
|
layout: sprite_handles.layout.clone(),
|
||||||
|
index: SPRITE_UP_PIPE,
|
||||||
|
}),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Transform {
|
||||||
|
translation: Vec3::new(window.width() + 50.0, gap_y - gap_size / 2.0 - PIPE_HEIGHT * 3.0 / 2.0, 0.05),
|
||||||
|
scale: Vec3::splat(3.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Pipe { speed: pipe_speed },
|
||||||
|
Collider {
|
||||||
|
width: PIPE_WIDTH * 0.7, // Reduce to 70% for more forgiving hitbox
|
||||||
|
height: PIPE_HEIGHT * 0.9, // Slightly reduce height too
|
||||||
|
},
|
||||||
|
crate::play_state::PlayScreen,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
object.vertical_speed -= 1500.0 * time.delta_seconds();
|
}
|
||||||
transform.prepend_translation_y(object.vertical_speed * time.delta_seconds());
|
|
||||||
|
// System to move pipes
|
||||||
|
pub fn move_pipes(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut query: Query<(Entity, &mut Transform, &Pipe)>,
|
||||||
|
time: Res<Time>,
|
||||||
|
) {
|
||||||
|
for (entity, mut transform, pipe) in query.iter_mut() {
|
||||||
|
transform.translation.x += pipe.speed * time.delta_secs();
|
||||||
|
|
||||||
|
// Despawn pipes that are off screen
|
||||||
|
if transform.translation.x < -100.0 {
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// System to check collisions
|
||||||
|
pub fn check_collisions(
|
||||||
|
bird_query: Query<(&Transform, &Collider), With<Birb>>,
|
||||||
|
pipe_query: Query<(&Transform, &Collider), With<Pipe>>,
|
||||||
|
ground_query: Query<&Transform, With<Ground>>,
|
||||||
|
mut next_state: ResMut<NextState<GameState>>,
|
||||||
|
window_query: Query<&Window>,
|
||||||
|
) {
|
||||||
|
let Ok((bird_transform, bird_collider)) = bird_query.single() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let bird_pos = bird_transform.translation;
|
||||||
|
let bird_scale = bird_transform.scale;
|
||||||
|
let bird_half_width = (bird_collider.width * bird_scale.x) / 2.0;
|
||||||
|
let bird_half_height = (bird_collider.height * bird_scale.y) / 2.0;
|
||||||
|
|
||||||
|
// Check collision with pipes
|
||||||
|
for (pipe_transform, pipe_collider) in pipe_query.iter() {
|
||||||
|
let pipe_pos = pipe_transform.translation;
|
||||||
|
let pipe_scale = pipe_transform.scale;
|
||||||
|
let pipe_half_width = (pipe_collider.width * pipe_scale.x) / 2.0;
|
||||||
|
let pipe_half_height = (pipe_collider.height * pipe_scale.y) / 2.0;
|
||||||
|
|
||||||
|
// AABB collision detection
|
||||||
|
if (bird_pos.x - bird_half_width < pipe_pos.x + pipe_half_width)
|
||||||
|
&& (bird_pos.x + bird_half_width > pipe_pos.x - pipe_half_width)
|
||||||
|
&& (bird_pos.y - bird_half_height < pipe_pos.y + pipe_half_height)
|
||||||
|
&& (bird_pos.y + bird_half_height > pipe_pos.y - pipe_half_height)
|
||||||
|
{
|
||||||
|
next_state.set(GameState::GameOver);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check collision with ground
|
||||||
|
for ground_transform in ground_query.iter() {
|
||||||
|
let ground_height = 56.0 * 3.0; // 168px
|
||||||
|
let ground_y = ground_transform.translation.y + ground_height / 2.0; // Ground top edge
|
||||||
|
|
||||||
|
if bird_pos.y - bird_half_height <= ground_y {
|
||||||
|
next_state.set(GameState::GameOver);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if bird went above screen
|
||||||
|
if let Ok(window) = window_query.single() {
|
||||||
|
if bird_pos.y + bird_half_height >= window.height() {
|
||||||
|
next_state.set(GameState::GameOver);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// System to toggle hitbox debug visualization
|
||||||
|
pub fn toggle_hitbox_debug(
|
||||||
|
keyboard: Res<ButtonInput<KeyCode>>,
|
||||||
|
mut debug: ResMut<DebugHitboxes>,
|
||||||
|
) {
|
||||||
|
if keyboard.just_pressed(KeyCode::KeyH) {
|
||||||
|
debug.enabled = !debug.enabled;
|
||||||
|
println!("Hitbox debug: {}", if debug.enabled { "ON" } else { "OFF" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// System to render hitbox debug sprites
|
||||||
|
pub fn render_hitbox_debug(
|
||||||
|
mut commands: Commands,
|
||||||
|
debug: Res<DebugHitboxes>,
|
||||||
|
collider_query: Query<(&Transform, &Collider), (Without<HitboxDebugSprite>, Without<Ground>)>,
|
||||||
|
existing_debug: Query<Entity, With<HitboxDebugSprite>>,
|
||||||
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
|
mut materials: ResMut<Assets<ColorMaterial>>,
|
||||||
|
) {
|
||||||
|
// Clean up existing debug sprites
|
||||||
|
for entity in existing_debug.iter() {
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If debug is enabled, spawn new debug sprites
|
||||||
|
if debug.enabled {
|
||||||
|
for (transform, collider) in collider_query.iter() {
|
||||||
|
let scale = transform.scale;
|
||||||
|
let width = collider.width * scale.x;
|
||||||
|
let height = collider.height * scale.y;
|
||||||
|
|
||||||
|
// Create a rectangular mesh for the hitbox
|
||||||
|
let mesh = Mesh::from(Rectangle::new(width, height));
|
||||||
|
|
||||||
|
commands.spawn((
|
||||||
|
Mesh2d::from(meshes.add(mesh)),
|
||||||
|
MeshMaterial2d(materials.add(ColorMaterial::from(Color::srgba(1.0, 0.0, 0.0, 0.3)))),
|
||||||
|
Transform {
|
||||||
|
translation: Vec3::new(
|
||||||
|
transform.translation.x,
|
||||||
|
transform.translation.y,
|
||||||
|
0.9, // High z-order to render on top
|
||||||
|
),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
HitboxDebugSprite,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||