diff --git a/.gitignore b/.gitignore
index ea8c4bf..bf0a94c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,4 @@
+/build/*
+!/build/index.html
+
/target
diff --git a/Cargo.lock b/Cargo.lock
index e204520..77d4fa1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1968,6 +1968,17 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
+[[package]]
+name = "chacha20"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "rand_core",
+]
+
[[package]]
name = "codespan-reporting"
version = "0.12.0"
@@ -2627,8 +2638,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
+ "js-sys",
"libc",
"r-efi 6.0.0",
+ "rand_core",
+ "wasm-bindgen",
]
[[package]]
@@ -4098,6 +4112,9 @@ name = "pong"
version = "0.1.0"
dependencies = [
"bevy",
+ "chacha20",
+ "getrandom 0.4.3",
+ "rand",
]
[[package]]
@@ -4213,6 +4230,7 @@ version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
dependencies = [
+ "chacha20",
"getrandom 0.4.3",
"rand_core",
]
diff --git a/Cargo.toml b/Cargo.toml
index 7c66e6c..cc270b2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,6 +5,14 @@ edition = "2024"
[dependencies]
bevy = "0.19.0"
+chacha20 = { version = "0.10.1", default-features = false, features = ["rng"] }
+getrandom = "0.4.3"
+rand = "0.10.2"
+
+[features]
+default = ["debug"]
+debug = ["bevy/debug"]
+wasm = ["getrandom/wasm_js"]
# Enable some optimization in development so the game runs well.
# Enable more optimization for dependencies because they change less often.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..99a3eaa
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,16 @@
+.DEFAULT_GOAL := help
+
+.PHONY: help
+help: ## List targets in this Makefile
+ @awk 'BEGIN { FS = ":|:.*?## "; OFS="\t" }; /^[0-9a-zA-Z_-]+?:/ { print $$1, $$2 }' $(MAKEFILE_LIST) \
+ | column --separator $$'\t' --table --table-wrap 2 --output-separator ' ' \
+ | sort --dictionary-order
+
+.PHONY: build
+build:
+ cargo build --release --target wasm32-unknown-unknown --no-default-features --features wasm
+ wasm-bindgen --target web --out-dir build target/wasm32-unknown-unknown/release/pong.wasm
+
+.PHONY: publish
+publish:
+ rsync --verbose --checksum --recursive --safe-links --delete --progress build/ root@lab:/www/games/pong
diff --git a/build/index.html b/build/index.html
new file mode 100644
index 0000000..325f6d0
--- /dev/null
+++ b/build/index.html
@@ -0,0 +1,23 @@
+
+
+
+
+ Pong
+
+
+
diff --git a/clippy.toml b/clippy.toml
new file mode 100644
index 0000000..05bfba2
--- /dev/null
+++ b/clippy.toml
@@ -0,0 +1,5 @@
+# Bevy query types can get a bit more complicated than usual Rust code.
+type-complexity-threshold = 1000
+
+# Bevy systems sometimes rely on a lot of different entities.
+too-many-arguments-threshold = 10
diff --git a/src/ball.rs b/src/ball.rs
new file mode 100644
index 0000000..24da111
--- /dev/null
+++ b/src/ball.rs
@@ -0,0 +1,131 @@
+use bevy::math::bounding::{Aabb2d, Bounded2d, BoundingVolume, IntersectsVolume};
+use bevy::prelude::*;
+
+use crate::hud::Hud;
+use crate::{GameState, Player};
+
+pub struct BallPlugin;
+
+impl Plugin for BallPlugin {
+ fn build(&self, app: &mut App) {
+ app.init_resource::()
+ .add_systems(Startup, spawn_ball)
+ .add_systems(
+ FixedUpdate,
+ (collision, movement)
+ .chain()
+ .run_if(in_state(GameState::Game)),
+ );
+ }
+}
+
+#[derive(Component, Debug, Clone, Copy)]
+pub struct Ball;
+
+#[derive(Component, Debug, Clone, Copy, PartialEq, Default, Deref, DerefMut)]
+pub struct Velocity(pub Vec2);
+
+#[derive(Resource, Debug, Default, Deref, DerefMut)]
+pub struct ServeAngle(pub f32);
+
+#[derive(Event, Debug)]
+pub struct Serve;
+
+#[derive(Component)]
+pub enum Collision {
+ Wall(CollisionWall),
+ Paddle(CollisionPaddle),
+}
+
+#[derive(Debug)]
+pub struct CollisionWall {
+ pub plane: Plane2d,
+}
+
+#[derive(Debug)]
+pub struct CollisionPaddle {
+ pub normal: Vec2,
+ pub rect: Aabb2d,
+}
+
+pub const BALL_SIZE: f32 = 10.;
+
+const V_MULTIPLIER: f32 = 1.5;
+pub const V_MIN: f32 = 80.;
+pub const V_MAX: f32 = 500.;
+
+fn spawn_ball(
+ mut commands: Commands,
+ mut meshes: ResMut>,
+ mut materials: ResMut>,
+) {
+ commands.spawn((
+ Mesh2d(meshes.add(Rectangle::from_length(BALL_SIZE))),
+ MeshMaterial2d(materials.add(ColorMaterial::from_color(Color::WHITE))),
+ Transform::from_translation(Vec3::ZERO),
+ Ball,
+ Velocity(Vec2::ZERO),
+ ));
+
+ commands.add_observer(on_serve);
+}
+
+fn movement(time: Res