Compare commits

...

8 Commits

188
Cargo.lock generated

@ -88,7 +88,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"getrandom 0.3.3",
"getrandom",
"once_cell",
"version_check",
"zerocopy",
@ -741,6 +741,35 @@ dependencies = [
"encase_derive_impl",
]
[[package]]
name = "bevy_feathers"
version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6a75a7f32e355d9232a92b3bca9f90af18f3c81232d26d1baed77f1326863ec"
dependencies = [
"accesskit",
"bevy_a11y",
"bevy_app",
"bevy_asset",
"bevy_camera",
"bevy_color",
"bevy_ecs",
"bevy_input_focus",
"bevy_log",
"bevy_math",
"bevy_picking",
"bevy_platform",
"bevy_reflect",
"bevy_render",
"bevy_shader",
"bevy_text",
"bevy_ui",
"bevy_ui_render",
"bevy_ui_widgets",
"bevy_window",
"smol_str",
]
[[package]]
name = "bevy_gilrs"
version = "0.17.2"
@ -925,6 +954,7 @@ dependencies = [
"bevy_derive",
"bevy_diagnostic",
"bevy_ecs",
"bevy_feathers",
"bevy_gilrs",
"bevy_gizmos",
"bevy_gltf",
@ -953,6 +983,7 @@ dependencies = [
"bevy_transform",
"bevy_ui",
"bevy_ui_render",
"bevy_ui_widgets",
"bevy_utils",
"bevy_window",
"bevy_winit",
@ -1130,7 +1161,7 @@ dependencies = [
"critical-section",
"foldhash 0.2.0",
"futures-channel",
"getrandom 0.3.3",
"getrandom",
"hashbrown 0.16.0",
"js-sys",
"portable-atomic",
@ -1554,6 +1585,25 @@ dependencies = [
"tracing",
]
[[package]]
name = "bevy_ui_widgets"
version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f131e13aa2ea9f8fa9af92aadfd6cf7f47d561d21510cf76854b46808c9b9bf"
dependencies = [
"accesskit",
"bevy_a11y",
"bevy_app",
"bevy_ecs",
"bevy_input",
"bevy_input_focus",
"bevy_log",
"bevy_math",
"bevy_picking",
"bevy_reflect",
"bevy_ui",
]
[[package]]
name = "bevy_utils"
version = "0.17.2"
@ -2249,6 +2299,20 @@ dependencies = [
"syn",
]
[[package]]
name = "engine"
version = "0.1.0"
dependencies = [
"bevy",
"chrono",
"itertools 0.14.0",
"lipsum",
"rand 0.9.2",
"serde",
"thiserror 2.0.12",
"walkdir",
]
[[package]]
name = "equivalent"
version = "1.0.2"
@ -2326,6 +2390,14 @@ version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]]
name = "flappy"
version = "0.1.0"
dependencies = [
"engine",
"physics",
]
[[package]]
name = "flate2"
version = "1.1.2"
@ -2447,23 +2519,6 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "games"
version = "0.1.0"
dependencies = [
"avian2d",
"avian3d",
"bevy",
"chrono",
"indoc",
"itertools 0.14.0",
"lipsum",
"rand 0.8.5",
"serde",
"thiserror 2.0.12",
"walkdir",
]
[[package]]
name = "gethostname"
version = "0.4.3"
@ -2474,17 +2529,6 @@ dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "getrandom"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
"wasi 0.11.1+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.3.3"
@ -2495,7 +2539,7 @@ dependencies = [
"js-sys",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
"wasi",
"wasm-bindgen",
]
@ -2863,6 +2907,13 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]]
name = "hum"
version = "0.1.0"
dependencies = [
"engine",
]
[[package]]
name = "iana-time-zone"
version = "0.1.63"
@ -3014,7 +3065,7 @@ version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
dependencies = [
"getrandom 0.3.3",
"getrandom",
"libc",
]
@ -4001,6 +4052,15 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "physics"
version = "0.1.0"
dependencies = [
"avian2d",
"avian3d",
"engine",
]
[[package]]
name = "pin-project"
version = "1.1.10"
@ -4193,8 +4253,6 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
@ -4233,9 +4291,6 @@ name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.16",
]
[[package]]
name = "rand_core"
@ -4243,7 +4298,7 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom 0.3.3",
"getrandom",
]
[[package]]
@ -4588,6 +4643,15 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392"
dependencies = [
"serde_core",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
@ -4813,6 +4877,17 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "tetris"
version = "0.1.0"
dependencies = [
"bevy",
"engine",
"serde",
"thiserror 1.0.69",
"toml",
]
[[package]]
name = "thiserror"
version = "1.0.69"
@ -4902,6 +4977,21 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "toml"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
dependencies = [
"indexmap",
"serde_core",
"serde_spanned",
"toml_datetime 0.7.3",
"toml_parser",
"toml_writer",
"winnow",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
@ -4949,6 +5039,12 @@ dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
[[package]]
name = "tracing"
version = "0.1.41"
@ -5033,6 +5129,14 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "trees"
version = "0.1.0"
dependencies = [
"engine",
"indoc",
]
[[package]]
name = "ttf-parser"
version = "0.20.0"
@ -5135,7 +5239,7 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
dependencies = [
"getrandom 0.3.3",
"getrandom",
"js-sys",
"serde",
"wasm-bindgen",
@ -5180,12 +5284,6 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"

@ -1,43 +1,3 @@
[package]
name = "games"
version = "0.1.0"
edition = "2024"
[[example]]
name = "demo_parallax2d"
path = "examples/demos/parallax2d.rs"
[[example]]
name = "demo_parallax3d"
path = "examples/demos/parallax3d.rs"
[features]
hide_debug = []
[dependencies]
itertools = "*"
thiserror = "2.0.12"
[dependencies.serde]
version = "1.0.219"
features = ["derive"]
[dependencies.avian3d]
version = "0.4.1"
[dependencies.avian2d]
version = "0.4.1"
[dependencies.bevy]
version = "0.17.2"
features = ["wayland", "dynamic_linking", "track_location"]
[dev-dependencies]
lipsum = "*"
rand = "*"
itertools = "*"
indoc = "*"
[build-dependencies]
walkdir = "*"
chrono = "*"
[workspace]
resolver = "3"
members = ["engine", "flappy", "hum", "physics", "tetris", "trees"]

@ -0,0 +1,28 @@
[package]
name = "engine"
version = "0.1.0"
edition = "2024"
[features]
hide_debug = []
[dependencies.bevy]
version = "0.17.2"
features = ["wayland", "dynamic_linking", "track_location", "experimental_bevy_feathers", "experimental_bevy_ui_widgets"]
[dependencies.serde]
version = "1.0.219"
features = ["derive"]
[dependencies]
itertools = "0.14.0"
thiserror = "2.0.12"
[dev-dependencies]
lipsum = "*"
rand = "*"
itertools = "*"
[build-dependencies]
walkdir = "*"
chrono = "*"

@ -0,0 +1,481 @@
//! This example shows off the various Bevy Feathers widgets.
use bevy::{
color::palettes,
feathers::{
FeathersPlugins,
controls::{
ButtonProps, ButtonVariant, ColorChannel, ColorSlider, ColorSliderProps, ColorSwatch,
SliderBaseColor, SliderProps, button, checkbox, color_slider, color_swatch, radio,
slider, toggle_switch,
},
dark_theme::create_dark_theme,
rounded_corners::RoundedCorners,
theme::{ThemeBackgroundColor, ThemedText, UiTheme},
tokens,
},
input_focus::tab_navigation::TabGroup,
prelude::*,
ui::{Checked, InteractionDisabled},
ui_widgets::{
Activate, RadioButton, RadioGroup, SliderPrecision, SliderStep, SliderValue, ValueChange,
checkbox_self_update, observe, slider_self_update,
},
};
/// A struct to hold the state of various widgets shown in the demo.
#[derive(Resource)]
struct DemoWidgetStates {
rgb_color: Srgba,
hsl_color: Hsla,
}
#[derive(Component, Clone, Copy, PartialEq)]
enum SwatchType {
Rgb,
Hsl,
}
#[derive(Component, Clone, Copy)]
struct DemoDisabledButton;
fn main() {
App::new()
.add_plugins((DefaultPlugins, FeathersPlugins))
.insert_resource(UiTheme(create_dark_theme()))
.insert_resource(DemoWidgetStates {
rgb_color: palettes::tailwind::EMERALD_800.with_alpha(0.7),
hsl_color: palettes::tailwind::AMBER_800.into(),
})
.add_systems(Startup, setup)
.add_systems(Update, update_colors)
.run();
}
fn setup(mut commands: Commands) {
// ui camera
commands.spawn(Camera2d);
commands.spawn(demo_root());
}
fn demo_root() -> impl Bundle {
(
Node {
width: percent(100),
height: percent(100),
align_items: AlignItems::Start,
justify_content: JustifyContent::Start,
display: Display::Flex,
flex_direction: FlexDirection::Column,
row_gap: px(10),
..default()
},
TabGroup::default(),
ThemeBackgroundColor(tokens::WINDOW_BG),
children![(
Node {
display: Display::Flex,
flex_direction: FlexDirection::Column,
align_items: AlignItems::Stretch,
justify_content: JustifyContent::Start,
padding: UiRect::all(px(8)),
row_gap: px(8),
width: percent(30),
min_width: px(200),
..default()
},
children![
(
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::Start,
column_gap: px(8),
..default()
},
children![
(
button(
ButtonProps::default(),
(),
Spawn((Text::new("Normal"), ThemedText))
),
observe(|_activate: On<Activate>| {
info!("Normal button clicked!");
})
),
(
button(
ButtonProps::default(),
(InteractionDisabled, DemoDisabledButton),
Spawn((Text::new("Disabled"), ThemedText))
),
observe(|_activate: On<Activate>| {
info!("Disabled button clicked!");
})
),
(
button(
ButtonProps {
variant: ButtonVariant::Primary,
..default()
},
(),
Spawn((Text::new("Primary"), ThemedText))
),
observe(|_activate: On<Activate>| {
info!("Disabled button clicked!");
})
),
]
),
(
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::Start,
column_gap: px(1),
..default()
},
children![
(
button(
ButtonProps {
corners: RoundedCorners::Left,
..default()
},
(),
Spawn((Text::new("Left"), ThemedText))
),
observe(|_activate: On<Activate>| {
info!("Left button clicked!");
})
),
(
button(
ButtonProps {
corners: RoundedCorners::None,
..default()
},
(),
Spawn((Text::new("Center"), ThemedText))
),
observe(|_activate: On<Activate>| {
info!("Center button clicked!");
})
),
(
button(
ButtonProps {
variant: ButtonVariant::Primary,
corners: RoundedCorners::Right,
},
(),
Spawn((Text::new("Right"), ThemedText))
),
observe(|_activate: On<Activate>| {
info!("Right button clicked!");
})
),
]
),
(
button(
ButtonProps::default(),
(),
Spawn((Text::new("Button"), ThemedText))
),
observe(|_activate: On<Activate>| {
info!("Wide button clicked!");
})
),
(
checkbox(Checked, Spawn((Text::new("Checkbox"), ThemedText))),
observe(
|change: On<ValueChange<bool>>,
query: Query<Entity, With<DemoDisabledButton>>,
mut commands: Commands| {
info!("Checkbox clicked!");
let mut button = commands.entity(query.single().unwrap());
if change.value {
button.insert(InteractionDisabled);
} else {
button.remove::<InteractionDisabled>();
}
let mut checkbox = commands.entity(change.source);
if change.value {
checkbox.insert(Checked);
} else {
checkbox.remove::<Checked>();
}
}
)
),
(
checkbox(
InteractionDisabled,
Spawn((Text::new("Disabled"), ThemedText))
),
observe(|_change: On<ValueChange<bool>>| {
warn!("Disabled checkbox clicked!");
})
),
(
checkbox(
(InteractionDisabled, Checked),
Spawn((Text::new("Disabled+Checked"), ThemedText))
),
observe(|_change: On<ValueChange<bool>>| {
warn!("Disabled checkbox clicked!");
})
),
(
Node {
display: Display::Flex,
flex_direction: FlexDirection::Column,
row_gap: px(4),
..default()
},
RadioGroup,
observe(
|value_change: On<ValueChange<Entity>>,
q_radio: Query<Entity, With<RadioButton>>,
mut commands: Commands| {
for radio in q_radio.iter() {
if radio == value_change.value {
commands.entity(radio).insert(Checked);
} else {
commands.entity(radio).remove::<Checked>();
}
}
}
),
children![
radio(Checked, Spawn((Text::new("One"), ThemedText))),
radio((), Spawn((Text::new("Two"), ThemedText))),
radio((), Spawn((Text::new("Three"), ThemedText))),
radio(
InteractionDisabled,
Spawn((Text::new("Disabled"), ThemedText))
),
]
),
(
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::Start,
column_gap: px(8),
..default()
},
children![
(toggle_switch((),), observe(checkbox_self_update)),
(
toggle_switch(InteractionDisabled,),
observe(checkbox_self_update)
),
(
toggle_switch((InteractionDisabled, Checked),),
observe(checkbox_self_update)
),
]
),
(
slider(
SliderProps {
max: 100.0,
value: 20.0,
..default()
},
(SliderStep(10.), SliderPrecision(2)),
),
observe(slider_self_update)
),
(
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
justify_content: JustifyContent::SpaceBetween,
..default()
},
children![Text("Srgba".to_owned()), color_swatch(SwatchType::Rgb),]
),
(
color_slider(
ColorSliderProps {
value: 0.5,
channel: ColorChannel::Red
},
()
),
observe(
|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
color.rgb_color.red = change.value;
}
)
),
(
color_slider(
ColorSliderProps {
value: 0.5,
channel: ColorChannel::Green
},
()
),
observe(
|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
color.rgb_color.green = change.value;
},
)
),
(
color_slider(
ColorSliderProps {
value: 0.5,
channel: ColorChannel::Blue
},
()
),
observe(
|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
color.rgb_color.blue = change.value;
},
)
),
(
color_slider(
ColorSliderProps {
value: 0.5,
channel: ColorChannel::Alpha
},
()
),
observe(
|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
color.rgb_color.alpha = change.value;
},
)
),
(
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
justify_content: JustifyContent::SpaceBetween,
..default()
},
children![Text("Hsl".to_owned()), color_swatch(SwatchType::Hsl),]
),
(
color_slider(
ColorSliderProps {
value: 0.5,
channel: ColorChannel::HslHue
},
()
),
observe(
|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
color.hsl_color.hue = change.value;
},
)
),
(
color_slider(
ColorSliderProps {
value: 0.5,
channel: ColorChannel::HslSaturation
},
()
),
observe(
|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
color.hsl_color.saturation = change.value;
},
)
),
(
color_slider(
ColorSliderProps {
value: 0.5,
channel: ColorChannel::HslLightness
},
()
),
observe(
|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
color.hsl_color.lightness = change.value;
},
)
)
]
),],
)
}
fn update_colors(
colors: Res<DemoWidgetStates>,
mut sliders: Query<(Entity, &ColorSlider, &mut SliderBaseColor)>,
swatches: Query<(&SwatchType, &Children), With<ColorSwatch>>,
mut commands: Commands,
) {
if colors.is_changed() {
for (slider_ent, slider, mut base) in sliders.iter_mut() {
match slider.channel {
ColorChannel::Red => {
base.0 = colors.rgb_color.into();
commands
.entity(slider_ent)
.insert(SliderValue(colors.rgb_color.red));
}
ColorChannel::Green => {
base.0 = colors.rgb_color.into();
commands
.entity(slider_ent)
.insert(SliderValue(colors.rgb_color.green));
}
ColorChannel::Blue => {
base.0 = colors.rgb_color.into();
commands
.entity(slider_ent)
.insert(SliderValue(colors.rgb_color.blue));
}
ColorChannel::HslHue => {
base.0 = colors.hsl_color.into();
commands
.entity(slider_ent)
.insert(SliderValue(colors.hsl_color.hue));
}
ColorChannel::HslSaturation => {
base.0 = colors.hsl_color.into();
commands
.entity(slider_ent)
.insert(SliderValue(colors.hsl_color.saturation));
}
ColorChannel::HslLightness => {
base.0 = colors.hsl_color.into();
commands
.entity(slider_ent)
.insert(SliderValue(colors.hsl_color.lightness));
}
ColorChannel::Alpha => {
base.0 = colors.rgb_color.into();
commands
.entity(slider_ent)
.insert(SliderValue(colors.rgb_color.alpha));
}
}
}
for (swatch_type, children) in swatches.iter() {
commands
.entity(children[0])
.insert(BackgroundColor(match swatch_type {
SwatchType::Rgb => colors.rgb_color.into(),
SwatchType::Hsl => colors.hsl_color.into(),
}));
}
}
}

@ -1,4 +1,4 @@
use games::*;
use engine::*;
fn main() {
App::new()

@ -1,6 +1,6 @@
use std::f32::consts::PI;
use games::*;
use engine::*;
fn main() {
App::new()

@ -1,4 +1,4 @@
use games::*;
use engine::*;
fn main() {
App::new()

@ -1,4 +1,4 @@
use games::*;
use engine::*;
fn main() {
App::new()

@ -3,7 +3,7 @@
//! This example illustrates scrolling in Bevy UI.
use bevy::picking::hover::HoverMap;
use games::*;
use engine::*;
use lipsum::*;
use rand::*;

@ -1,4 +1,4 @@
use games::*;
use engine::*;
fn main() {
App::new()

@ -6,29 +6,23 @@ mod base_game;
mod debug;
mod loading;
mod parallax;
pub mod physics2d;
pub mod physics3d;
mod scheduling;
mod ui;
mod version;
// Rust stdlib
pub use std::{
collections::VecDeque,
f32::consts::PI,
fmt::Display,
};
pub use core::time::Duration;
pub use std::{collections::VecDeque, f32::consts::PI, fmt::Display};
// Community libraries
pub use bevy::{
time::common_conditions::*,
asset::{
AssetLoader, AssetMetaCheck, LoadContext, LoadState, LoadedFolder, RenderAssetUsages,
io::Reader, uuid_handle,
},
camera::{primitives::*, visibility::*, *},
color::palettes::css::*,
feathers::controls::*,
gizmos::{aabb::AabbGizmoPlugin, light::LightGizmoPlugin},
input::{
ButtonState,
@ -40,10 +34,13 @@ pub use bevy::{
math::FloatOrd,
pbr::wireframe::{WireframeConfig, WireframePlugin},
platform::{collections::HashMap, hash::RandomState},
prelude::*,
prelude::{State, *},
reflect::TypePath,
render::render_resource::{Extent3d, TextureDimension, TextureFormat, TextureUsages},
sprite_render::*,
time::common_conditions::*,
ui::*,
ui_widgets::{Button, *},
window::{WindowResized, WindowResolution},
};
pub use itertools::Itertools;

@ -1,2 +1,2 @@
/// Include the version of this build from an auto-generated version file
pub const VERSION: &str = include_str!("../VERSION");
pub const VERSION: &str = include_str!("../../VERSION");

@ -0,0 +1,10 @@
[package]
name = "flappy"
version = "0.1.0"
edition = "2024"
[dependencies.engine]
path = "../engine"
[dependencies.physics]
path = "../physics"

@ -4,8 +4,8 @@
use bevy::image::{ImageLoaderSettings, ImageSampler};
use bevy::platform::time::Instant;
use bevy::render::view::ColorGrading;
use games::physics2d::*;
use games::*;
use engine::*;
use physics::physics2d::*;
use std::hash::BuildHasher;
fn main() {

@ -0,0 +1,7 @@
[package]
name = "hum"
version = "0.1.0"
edition = "2024"
[dependencies.engine]
path = "../engine"

@ -1,4 +1,4 @@
use games::*;
use engine::*;
fn main() {
App::new()

@ -1,5 +1,11 @@
VERSION := `git rev-parse --short HEAD`
check:
cargo check -p tetris
run:
cargo run -p tetris
bindgen profile name:
mkdir -p ./dist/{{name}}

@ -0,0 +1,14 @@
[package]
name = "physics"
version = "0.1.0"
edition = "2024"
[dependencies.engine]
path = "../engine"
[dependencies.avian3d]
version = "0.4.1"
[dependencies.avian2d]
version = "0.4.1"

@ -0,0 +1,2 @@
pub mod physics2d;
pub mod physics3d;

@ -1,4 +1,5 @@
use super::*;
pub use avian2d::prelude::*;
/// 2D Physics systems, resources, events, etc.

@ -1,45 +0,0 @@
# Design
## Tetris
Matrix Multiplication for rotating pieces.
Each piece (shape) contains a Mat4 containing a representation of it's shape.
For example:
```
0 1 0 0
0 1 0 0
0 1 0 0
0 1 0 0
```
This is the classic `line` piece.
And here it is on it's `up` side
```
0 0 0 0
1 1 1 1
0 0 0 0
0 0 0 0
```
And here is the `t` piece
```
0 1 0
1 1 1
0 0 0
```
A matrix multiplication is applied to this Mat6 to achieve a piece rotation.
When that matrix is updated, the 4 blocks parented to the shape are moved to reflect this new shape.
This matrix also allows us to do checks to see if any of the blocks in the shape would intersect with another piece on the board.
We can also check if a piece would go out of bounds during a move or rotation.
We can use this to "plan -> validate -> commit" changes based on user input.
Question: How the fuck do matrix multiplications work??

@ -1,7 +0,0 @@
# Falling Blocks RPG
This game is inspired by both Tetris and Peglin.
Your goal is to play Tetris (or a similar falling block game) but while that is happening you are carrying out a 2d real-time combat RPG battle.
Between battles/levels you choose a different level to go to in an overworld, choose upgrades, perks, and maybe some other stuff!

@ -1,15 +0,0 @@
# Tetris Battle
- Recognize finishing a level successfully
- Recognize failing a level
- Art pass
- Nicer preview for "Next" and "Swap" Shapes
- Use a timer resource for next step
## Bugs
- Two blocks in the same position
- Skipping doesn't work 100% of the time
- A little delay after skipping
## Nice to haves
- Fix tests after Shape -> ShapeLayout refactor

File diff suppressed because it is too large Load Diff

@ -1,220 +0,0 @@
use super::*;
#[test]
fn test_shape_t() {
let mut shape = Shape::new_t();
let expected_up = "010\n\
111\n\
000\n";
let expected_right = "010\n\
011\n\
010\n";
let expected_down = "000\n\
111\n\
010\n";
let expected_left = "010\n\
110\n\
010\n";
assert_eq!(shape.as_ascii(), expected_up);
shape = shape.rotated();
assert_eq!(shape.as_ascii(), expected_right);
shape = shape.rotated();
assert_eq!(shape.as_ascii(), expected_down);
shape = shape.rotated();
assert_eq!(shape.as_ascii(), expected_left);
shape = shape.rotated();
assert_eq!(shape.as_ascii(), expected_up);
}
#[test]
fn test_shape_i() {
let mut shape = Shape::new_i();
let expected_up = "1\n\
1\n\
1\n\
1\n";
let expected_right = "1111\n";
let expected_down = "1\n\
1\n\
1\n\
1\n";
let expected_left = "1111\n";
assert_eq!(shape.as_ascii(), expected_up);
shape = shape.rotated();
assert_eq!(shape.as_ascii(), expected_right);
shape = shape.rotated();
assert_eq!(shape.as_ascii(), expected_down);
shape = shape.rotated();
assert_eq!(shape.as_ascii(), expected_left);
shape = shape.rotated();
assert_eq!(shape.as_ascii(), expected_up);
}
#[test]
fn test_coordinates() {
let shape = Shape::new_t();
let center = GridPosition { x: 5, y: 5 };
let actual: Vec<Result<GridPosition, OutOfBoundsError>> = shape.coordinates(&center).collect();
let expected: Vec<Result<GridPosition, OutOfBoundsError>> = vec![
Ok((4, 5).into()),
Ok((5, 5).into()),
Ok((6, 5).into()),
Ok((5, 6).into()),
];
assert_eq!(shape.layout.center(), (1, 1));
assert_eq!(actual, expected);
}
#[test]
fn test_height() {
// todo: should be 2 for t piece
assert_eq!(Shape::new_t().height(), 3);
assert_eq!(Shape::new_i().height(), 4);
assert_eq!(Shape::new_l().height(), 3);
}
#[test]
fn test_shape_block_layout_rotation() {
{
let actual = ShapeBlockLayout {
inner: vec![vec![0, 0, 1]],
}
.rotated();
let expected = ShapeBlockLayout {
inner: vec![vec![0], vec![0], vec![1]],
};
assert_eq!(expected, actual);
}
{
let actual = ShapeBlockLayout {
inner: vec![vec![1, 2, 3], vec![4, 5, 6]],
}
.rotated();
let expected = ShapeBlockLayout {
inner: vec![vec![4, 1], vec![5, 2], vec![6, 3]],
};
assert_eq!(expected, actual);
}
{
let actual = ShapeBlockLayout {
inner: vec![vec![1, 2, 3], vec![4, 5, 6]],
}
.rotated();
let expected = ShapeBlockLayout {
inner: vec![vec![4, 1], vec![5, 2], vec![6, 3]],
};
assert_eq!(expected, actual);
}
{
let actual = ShapeBlockLayout {
inner: vec![
vec![1, 2, 3, 4],
vec![5, 6, 7, 8],
vec![9, 10, 11, 12],
vec![13, 14, 15, 16],
],
}
.rotated();
let expected = ShapeBlockLayout {
inner: vec![
vec![13, 9, 5, 1],
vec![14, 10, 6, 2],
vec![15, 11, 7, 3],
vec![16, 12, 8, 4],
],
};
assert_eq!(expected, actual);
}
}
#[test]
fn test_shape_block_center() {
{
let actual = ShapeBlockLayout {
inner: vec![
vec![0],
vec![0],
vec![0],
]
}.center();
let expected = (0, 1);
assert_eq!(actual, expected);
}
{
let actual = ShapeBlockLayout {
inner: vec![
vec![0],
vec![0],
vec![0],
vec![0],
]
}.center();
let expected = (0, 2);
assert_eq!(actual, expected);
}
{
let actual = ShapeBlockLayout {
inner: vec![
vec![0, 0],
vec![0, 0],
]
}.center();
let expected = (0, 1);
assert_eq!(actual, expected);
}
{
let actual = ShapeBlockLayout {
inner: vec![
vec![0, 0, 0],
vec![0, 0, 0],
vec![0, 0, 0],
]
}.center();
let expected = (1, 1);
assert_eq!(actual, expected);
}
{
let actual = ShapeBlockLayout {
inner: vec![
vec![0, 0, 0, 0],
vec![0, 0, 0, 0],
vec![0, 0, 0, 0],
vec![0, 0, 0, 0],
]
}.center();
let expected = (1, 2);
assert_eq!(actual, expected);
}
}

@ -0,0 +1,19 @@
[package]
name = "tetris"
version = "0.1.0"
edition = "2024"
[dependencies.serde]
version = "1.0.219"
features = ["derive"]
[dependencies.engine]
path = "../engine"
[dependencies]
toml = "0.9.8"
thiserror = "*"
[dependencies.bevy]
version = "0.17.2"
features = ["wayland", "dynamic_linking", "track_location", "experimental_bevy_feathers", "experimental_bevy_ui_widgets"]

@ -0,0 +1,4 @@
layout = [
[0,1,0],
[1,1,1],
]

@ -0,0 +1,105 @@
use super::*;
/// Create tetris game with camera that renders to subset of viewport
///
/// Focus on a single piece and making it really tight mechanically
/// A single piece with movement, rotation, jump-to-end, line clearing, etc.
///
/// Once done, make pieces a data input so we can add arbitrary metadata to them
pub struct BlocksPlugin;
impl Plugin for BlocksPlugin {
fn build(&self, app: &mut App) {
app.init_asset::<ShapeAsset>()
.init_asset_loader::<ShapeAssetLoader>()
.add_systems(OnEnter(Loading::Active), load_assets.run_if(run_once))
.add_systems(OnEnter(GameState::Setup), (setup_camera, setup_blocks))
.add_observer(add_shape);
}
}
/// Shape asset
/// Stores shape data in an asset file, likely toml
#[derive(Asset, TypePath, Debug, Deserialize)]
struct ShapeAsset {
layout: Vec<Vec<u8>>,
}
impl ShapeAsset {
fn into_bundle(&self) -> impl Bundle {
()
}
}
#[derive(Default)]
struct ShapeAssetLoader;
#[derive(Debug, Error)]
enum ShapeAssetError {
#[error("Failed to read file {0}")]
Io(#[from] std::io::Error),
#[error("Failed to parse file {0}")]
Parse(#[from] toml::de::Error),
}
impl AssetLoader for ShapeAssetLoader {
type Asset = ShapeAsset;
type Settings = ();
type Error = ShapeAssetError;
async fn load(
&self,
reader: &mut dyn Reader,
_settings: &(),
_load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let shape_asset = toml::from_slice::<ShapeAsset>(bytes.as_slice())?;
Ok(shape_asset)
}
fn extensions(&self) -> &[&str] {
&["shape.toml"]
}
}
/// Initialize camera and block game area
fn load_assets(server: Res<AssetServer>, mut all_assets: ResMut<AllAssets>) {
all_assets
.handles
.push(server.load::<ShapeAsset>("t.shape.toml").untyped());
}
/// Spawn the tetris 2d camera
fn setup_camera(mut commands: Commands) {
commands.spawn((Camera2d, Camera::default()));
}
/// Spawn a block
/// This system is temporary
fn setup_blocks(
mut commands: Commands,
all_assets: Res<AllAssets>,
server: Res<AssetServer>,
mut checklist: ResMut<SetupChecklist>,
) {
let h: Handle<ShapeAsset> = server
.get_handle(all_assets.handles[0].path().unwrap())
.unwrap();
commands.spawn(AssetComponent::new(h));
checklist.spawn_shape = true;
}
/// Event handler for transforming a handle component into a thing
fn add_shape(
event: On<Add, AssetComponent<ShapeAsset>>,
query: Query<&AssetComponent<ShapeAsset>>,
mut commands: Commands,
shapes: Res<Assets<ShapeAsset>>,
) {
let asset_component = query.get(event.entity).unwrap();
let shape = shapes.get(asset_component.handle.id()).unwrap();
commands.entity(event.entity).insert(shape.into_bundle());
}

@ -0,0 +1,40 @@
use super::*;
/// Debug UI for Tetris
/// Some overlap with more general purpose debug tools,
/// but built one-off because of the changse to UI in Bevy 0.17
pub struct DebugPlugin;
// Debugging wish-list:
// - Toggle debug on/off with f12 key
// - Bounding boxes around entities
// - Cursor at the center of the world
// - Show current state(s)
impl Plugin for DebugPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, (
log_transition::<Debugger>.run_if(state_changed::<Debugger>),
log_transition::<Loading>.run_if(state_changed::<Loading>),
log_transition::<GameState>.run_if(state_changed::<GameState>),
));
}
}
/// Tracks if the game is in debug mode
#[derive(States, Default, Clone, Eq, Debug, PartialEq, Hash)]
pub enum Debugger {
#[default]
Off,
On,
}
fn log_transition<T: States + PartialEq + Clone>(
curr: Res<State<T>>,
mut prev: Local<Option<T>>,
) {
debug_assert!(Some(curr.get().clone()) != *prev);
info!("State Change:: {:?} -> {:?}", *prev, *curr);
*prev = Some(curr.get().clone());
}

@ -0,0 +1,13 @@
use super::*;
/// Create figher game that renders to another subset of the viewport
///
/// Focus on a single fighter class with a single enemy (blob) to start with
/// Once damage both ways is tight, add multiple enemies as assets
pub struct FighterPlugin;
impl Plugin for FighterPlugin {
fn build(&self, app: &mut App) {
app;
}
}

@ -0,0 +1,127 @@
mod blocks;
mod debug;
mod fighter;
use engine::*;
use serde::Deserialize;
use thiserror::Error;
use blocks::*;
use debug::*;
use fighter::*;
fn main() {
App::new()
.add_plugins((DefaultPlugins, BlocksPlugin, FighterPlugin, DebugPlugin))
.init_state::<Loading>()
.init_state::<Debugger>()
.init_state::<GameState>()
.init_resource::<AllAssets>()
.init_resource::<SetupChecklist>()
// Check if assets were added to loading queue
.add_systems(
Update,
loading_check
.run_if(in_state(Loading::Active))
.run_if(resource_changed::<AllAssets>),
)
// Wait for pending assets to be loaded
.add_systems(Update, loading_wait.run_if(in_state(Loading::Active)))
// Once done loading, move to the setup state
.add_systems(OnEnter(Loading::Idle), setup_game)
// Check if the game is ready to progress
.add_systems(Update, setup_wait.run_if(in_state(GameState::Setup)))
.run();
}
/// Reports if the game is loading assets
#[derive(States, Default, Clone, Eq, Debug, PartialEq, Hash)]
enum Loading {
#[default]
Active,
Idle,
}
/// Tracks what state the main game loop is in
#[derive(States, Default, Clone, Eq, Debug, PartialEq, Hash)]
enum GameState {
#[default]
Boot,
Setup,
Run,
}
/// A list of all assets so we don't lose them
#[derive(Default, Resource, Debug)]
struct AllAssets {
handles: Vec<UntypedHandle>,
}
/// A "checklist" to know if we can progress from setup to the game
#[derive(Default, Resource)]
struct SetupChecklist {
spawn_shape: bool,
}
impl SetupChecklist {
fn done(&self) -> bool {
self.spawn_shape
}
}
/// Sends the game into Loading::Active if assets are added to the AllAssets list
fn loading_check(mut next: ResMut<NextState<Loading>>, all_assets: Res<AllAssets>) {
debug_assert!(all_assets.is_changed());
next.set(Loading::Active);
}
/// Waits in Loading::Active until all assets are loaded then move to Loading::Idle
fn loading_wait(
curr: Res<State<Loading>>,
mut next: ResMut<NextState<Loading>>,
server: Res<AssetServer>,
all_assets: Res<AllAssets>,
) {
debug_assert!(*curr.get() == Loading::Active);
if all_assets
.handles
.iter()
.all(|h| matches!(server.get_load_state(h.id()), Some(LoadState::Loaded)))
{
next.set(Loading::Idle);
}
}
/// Moves the game from Boot to Setup
fn setup_game(curr: Res<State<GameState>>, mut next: ResMut<NextState<GameState>>) {
debug_assert!(*curr.get() == GameState::Boot);
next.set(GameState::Setup);
}
/// Wait until all checklist items are complete
fn setup_wait(
curr: Res<State<GameState>>,
mut next: ResMut<NextState<GameState>>,
checklist: Res<SetupChecklist>,
) {
debug_assert!(*curr.get() == GameState::Setup);
// If all checks pass, move on to the run state
if checklist.done() {
next.set(GameState::Run);
}
}
/// A wrapper around a handle for assigning an arbitrary Handle<T> to an entity
#[derive(Debug, Component)]
struct AssetComponent<T: Asset> {
handle: Handle<T>,
}
impl<T: Asset> AssetComponent<T> {
fn new(handle: Handle<T>) -> Self {
Self { handle }
}
}

@ -0,0 +1,10 @@
[package]
name = "trees"
version = "0.1.0"
edition = "2024"
[dependencies.engine]
path = "../engine"
[dev-dependencies]
indoc = "*"

@ -8,7 +8,7 @@ mod mono;
use bevy::{picking::hover::HoverMap, platform::hash::RandomState};
use debug::*;
use games::*;
use engine::*;
use mono::*;
use std::hash::BuildHasher;
Loading…
Cancel
Save