other files

This commit is contained in:
Mitchell Hansen
2025-11-24 22:36:53 -08:00
parent efab67d027
commit 9c0ad6fcba
11 changed files with 689 additions and 0 deletions

49
WEB_BUILD.md Normal file
View 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

6
assets/bindings.ron Normal file
View File

@@ -0,0 +1,6 @@
(
axes: {},
actions: {
"flap": [[Key(Space)]],
},
)

View File

@@ -0,0 +1,4 @@
(
title: "test",
dimensions: Some((432, 768)),
)

BIN
assets/screenshot1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
assets/screenshot2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
assets/sprites/flappy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

169
assets/sprites/flappy.ron Normal file
View 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,
)
]
))

BIN
assets/sprites/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

24
assets/sprites/logo.ron Normal file
View File

@@ -0,0 +1,24 @@
(
texture_width: 690,
texture_height: 230,
sprites: [
(
x: 0,
y: 0,
width: 230,
height: 230,
),
(
x: 230,
y: 0,
width: 230,
height: 230,
),
(
x: 460,
y: 0,
width: 230,
height: 230,
)
]
)

25
build-web.sh Executable file
View 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"

412
src/gameover_state.rs Normal file
View 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();
}
}