Compare commits
No commits in common. 'fd2c52b5acd676d0b5851c410431ffff1ea81ee4' and '570b4916bc91c63c6a7b27d8fee3ea110104b96a' have entirely different histories.
fd2c52b5ac
...
570b4916bc
@ -1,3 +1,43 @@
|
|||||||
[workspace]
|
[package]
|
||||||
resolver = "3"
|
name = "games"
|
||||||
members = ["engine", "flappy", "hum", "physics", "tetris", "trees"]
|
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 = "*"
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
[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 = "*"
|
|
||||||
@ -1,481 +0,0 @@
|
|||||||
//! 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 engine::*;
|
use games::*;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
@ -1,6 +1,6 @@
|
|||||||
use std::f32::consts::PI;
|
use std::f32::consts::PI;
|
||||||
|
|
||||||
use engine::*;
|
use games::*;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
@ -1,4 +1,4 @@
|
|||||||
use engine::*;
|
use games::*;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
@ -1,4 +1,4 @@
|
|||||||
use engine::*;
|
use games::*;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
@ -1,4 +1,4 @@
|
|||||||
use engine::*;
|
use games::*;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
@ -1,10 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "flappy"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies.engine]
|
|
||||||
path = "../engine"
|
|
||||||
|
|
||||||
[dependencies.physics]
|
|
||||||
path = "../physics"
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "hum"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies.engine]
|
|
||||||
path = "../engine"
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
[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"
|
|
||||||
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
pub mod physics2d;
|
|
||||||
pub mod physics3d;
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
use engine::*;
|
use games::*;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
# 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??
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
# 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!
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
# 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
@ -0,0 +1,220 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,4 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub use avian2d::prelude::*;
|
pub use avian2d::prelude::*;
|
||||||
|
|
||||||
/// 2D Physics systems, resources, events, etc.
|
/// 2D Physics systems, resources, events, etc.
|
||||||
@ -1,2 +1,2 @@
|
|||||||
/// Include the version of this build from an auto-generated version file
|
/// 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");
|
||||||
@ -1,19 +0,0 @@
|
|||||||
[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"]
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
layout = [
|
|
||||||
[0,1,0],
|
|
||||||
[1,1,1],
|
|
||||||
]
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
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());
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
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());
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,127 +0,0 @@
|
|||||||
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 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "trees"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies.engine]
|
|
||||||
path = "../engine"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
indoc = "*"
|
|
||||||
Loading…
Reference in New Issue