Compare commits
8 Commits
570b4916bc
...
fd2c52b5ac
| Author | SHA1 | Date |
|---|---|---|
|
|
fd2c52b5ac | 6 days ago |
|
|
44a343ff71 | 6 days ago |
|
|
b122c5cf0f | 6 days ago |
|
|
e07ff07e23 | 6 days ago |
|
|
277260ecf6 | 7 days ago |
|
|
b576de94e4 | 2 weeks ago |
|
|
6b2370c6a5 | 2 weeks ago |
|
|
e1dcda4584 | 2 weeks ago |
@ -1,43 +1,3 @@
|
|||||||
[package]
|
[workspace]
|
||||||
name = "games"
|
resolver = "3"
|
||||||
version = "0.1.0"
|
members = ["engine", "flappy", "hum", "physics", "tetris", "trees"]
|
||||||
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 = "*"
|
|
||||||
|
|||||||
@ -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() {
|
fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
@ -1,6 +1,6 @@
|
|||||||
use std::f32::consts::PI;
|
use std::f32::consts::PI;
|
||||||
|
|
||||||
use games::*;
|
use engine::*;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
@ -1,4 +1,4 @@
|
|||||||
use games::*;
|
use engine::*;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
@ -1,4 +1,4 @@
|
|||||||
use games::*;
|
use engine::*;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
@ -1,4 +1,4 @@
|
|||||||
use games::*;
|
use engine::*;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
@ -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");
|
||||||
@ -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() {
|
fn main() {
|
||||||
App::new()
|
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::*;
|
use super::*;
|
||||||
|
|
||||||
pub use avian2d::prelude::*;
|
pub use avian2d::prelude::*;
|
||||||
|
|
||||||
/// 2D Physics systems, resources, events, etc.
|
/// 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