Compare commits
8 Commits
570b4916bc
...
fd2c52b5ac
| Author | SHA1 | Date |
|---|---|---|
|
|
fd2c52b5ac | 5 days ago |
|
|
44a343ff71 | 5 days ago |
|
|
b122c5cf0f | 5 days ago |
|
|
e07ff07e23 | 5 days ago |
|
|
277260ecf6 | 6 days ago |
|
|
b576de94e4 | 2 weeks ago |
|
|
6b2370c6a5 | 2 weeks ago |
|
|
e1dcda4584 | 2 weeks ago |
@ -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()
|
||||
@ -1,4 +1,4 @@
|
||||
use games::*;
|
||||
use engine::*;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
@ -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"
|
||||
@ -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()
|
||||
@ -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(¢er).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 = "*"
|
||||
Loading…
Reference in New Issue