Shape layout stuff, test driven developmen to the rescue

main
Elijah Voigt 13 hours ago
parent 125f303ec4
commit c9d962df8d

127
Cargo.lock generated

@ -540,6 +540,7 @@ dependencies = [
"futures-io",
"futures-lite",
"js-sys",
"notify-debouncer-full",
"parking_lot",
"ron",
"serde",
@ -1876,7 +1877,7 @@ dependencies = [
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
"windows-link 0.1.3",
]
[[package]]
@ -2384,6 +2385,15 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "file-id"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1fc6a637b6dc58414714eddd9170ff187ecb0933d4c7024d1abbd23a3cc26e9"
dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "fixedbitset"
version = "0.5.7"
@ -2485,6 +2495,15 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
name = "fsevent-sys"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
dependencies = [
"libc",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
@ -2539,7 +2558,7 @@ dependencies = [
"js-sys",
"libc",
"r-efi",
"wasi",
"wasi 0.14.2+wasi-0.2.4",
"wasm-bindgen",
]
@ -3096,6 +3115,26 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
name = "kqueue"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
dependencies = [
"kqueue-sys",
"libc",
]
[[package]]
name = "kqueue-sys"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
dependencies = [
"bitflags 1.3.2",
"libc",
]
[[package]]
name = "ktx2"
version = "0.4.0"
@ -3292,6 +3331,18 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "mio"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
dependencies = [
"libc",
"log",
"wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.61.2",
]
[[package]]
name = "naga"
version = "26.0.0"
@ -3472,6 +3523,43 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51"
[[package]]
name = "notify"
version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
dependencies = [
"bitflags 2.9.1",
"fsevent-sys",
"inotify",
"kqueue",
"libc",
"log",
"mio",
"notify-types",
"walkdir",
"windows-sys 0.60.2",
]
[[package]]
name = "notify-debouncer-full"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d88b1a7538054351c8258338df7c931a590513fb3745e8c15eb9ff4199b8d1"
dependencies = [
"file-id",
"log",
"notify",
"notify-types",
"walkdir",
]
[[package]]
name = "notify-types"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
[[package]]
name = "ntapi"
version = "0.4.1"
@ -5284,6 +5372,12 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
@ -5710,7 +5804,7 @@ dependencies = [
"windows-collections",
"windows-core 0.61.2",
"windows-future",
"windows-link",
"windows-link 0.1.3",
"windows-numerics",
]
@ -5754,7 +5848,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement 0.60.0",
"windows-interface 0.59.1",
"windows-link",
"windows-link 0.1.3",
"windows-result 0.3.4",
"windows-strings 0.4.2",
]
@ -5766,7 +5860,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [
"windows-core 0.61.2",
"windows-link",
"windows-link 0.1.3",
"windows-threading",
]
@ -5820,6 +5914,12 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.2.0"
@ -5827,7 +5927,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core 0.61.2",
"windows-link",
"windows-link 0.1.3",
]
[[package]]
@ -5854,7 +5954,7 @@ version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link",
"windows-link 0.1.3",
]
[[package]]
@ -5873,7 +5973,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link",
"windows-link 0.1.3",
]
[[package]]
@ -5912,6 +6012,15 @@ dependencies = [
"windows-targets 0.53.2",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
@ -5980,7 +6089,7 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
dependencies = [
"windows-link",
"windows-link 0.1.3",
]
[[package]]

@ -23,6 +23,7 @@ pub use bevy::{
camera::{primitives::*, visibility::*, *},
color::palettes::css::*,
feathers::controls::*,
feathers::*,
gizmos::{aabb::AabbGizmoPlugin, light::LightGizmoPlugin},
input::{
ButtonState,
@ -39,7 +40,6 @@ pub use bevy::{
render::render_resource::{Extent3d, TextureDimension, TextureFormat, TextureUsages},
sprite_render::*,
time::common_conditions::*,
feathers::*,
ui::*,
ui_widgets::{Button, *},
window::{WindowResized, WindowResolution},

@ -6,6 +6,9 @@ check:
run:
cargo run -p tetris
test:
cargo test -p tetris
bindgen profile name:
mkdir -p ./dist/{{name}}

@ -16,4 +16,4 @@ thiserror = "*"
[dependencies.bevy]
version = "0.17.2"
features = ["wayland", "dynamic_linking", "track_location", "experimental_bevy_feathers", "experimental_bevy_ui_widgets"]
features = ["wayland", "dynamic_linking", "track_location", "experimental_bevy_feathers", "experimental_bevy_ui_widgets", "file_watcher"]

@ -2,3 +2,4 @@ layout = [
[0,1,0],
[1,1,1],
]
tint = "#AA4500"

@ -1,5 +1,8 @@
use super::*;
// TODO:
// - When shape asset is updated, shape should update in real time
/// Create tetris game with camera that renders to subset of viewport
///
/// Focus on a single piece and making it really tight mechanically
@ -14,6 +17,7 @@ impl Plugin for BlocksPlugin {
.init_asset_loader::<ShapeAssetLoader>()
.add_systems(OnEnter(Loading(true)), load_assets.run_if(run_once))
.add_systems(OnEnter(GameState::Setup), (setup_camera, setup_blocks))
.add_systems(Update, updated_shape_asset.run_if(on_message::<AssetEvent<ShapeAsset>>))
.add_observer(add_shape);
}
}
@ -22,16 +26,64 @@ impl Plugin for BlocksPlugin {
/// Stores shape data in an asset file, likely toml
#[derive(Asset, TypePath, Debug, Deserialize)]
struct ShapeAsset {
layout: Vec<Vec<u8>>,
layout: ShapeLayout,
#[serde(default = "default_tint")]
tint: String,
#[serde(skip)]
mesh: Mesh2d,
#[serde(skip)]
material: MeshMaterial2d<ColorMaterial>,
}
fn default_tint() -> String {
"#aa00aa".into()
}
impl ShapeAsset {
fn into_bundle(&self) -> impl Bundle {
(self.mesh.clone(), self.material.clone())
fn as_bundle(&self) -> impl Bundle {
let [a, b, c, d] = self.layout.positions();
related!(ShapeBlocks[
(self.mesh.clone(), self.material.clone(), a),
(self.mesh.clone(), self.material.clone(), b),
(self.mesh.clone(), self.material.clone(), c),
(self.mesh.clone(), self.material.clone(), d),
])
}
}
/// Block positions relative to the shape's center
#[derive(Component, PartialEq, Debug)]
pub(crate) struct RelativePosition {
pub x: isize,
pub y: isize,
}
impl From<(isize, isize)> for RelativePosition {
fn from((x, y): (isize, isize)) -> Self {
RelativePosition { x, y }
}
}
/// Layout for a given shape
#[derive(Debug, Deserialize)]
pub(crate) struct ShapeLayout(pub Vec<Vec<u8>>);
impl ShapeLayout {
pub(crate) fn positions(&self) -> [RelativePosition;4] {
let mut c: Vec<RelativePosition> = Vec::with_capacity(4);
let center = {
};
for (y, nested) in self.0.iter().enumerate() {
for (x, val) in nested.iter().enumerate() {
if *val == 1 {
println!("{x}{y}");
}
}
}
todo!()
}
}
@ -43,7 +95,9 @@ enum ShapeAssetError {
#[error("Failed to read file {0}")]
Io(#[from] std::io::Error),
#[error("Failed to parse file {0}")]
Parse(#[from] toml::de::Error),
ParseFile(#[from] toml::de::Error),
#[error("Failed to parse tint {0}")]
ParseTint(#[from] HexColorError),
}
impl AssetLoader for ShapeAssetLoader {
@ -58,29 +112,26 @@ impl AssetLoader for ShapeAssetLoader {
) -> Result<Self::Asset, Self::Error> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
// TODO: Create sub-assets for mesh and material
let parsed = toml::from_slice::<ShapeAsset>(bytes.as_slice())?;
let mesh = {
// https://docs.rs/bevy/latest/bevy/asset/struct.LoadContext.html#method.add_labeled_asset
let m: Mesh = Rectangle::new(100.0, 100.0).into();
let h: Handle<Mesh> = load_context.add_labeled_asset(format!("{}#mesh", load_context.asset_path()), m);
let h: Handle<Mesh> =
load_context.add_labeled_asset(format!("{}#mesh", load_context.asset_path()), m);
Mesh2d(h)
};
let material = {
let m = ColorMaterial {
color: PURPLE.into(),
..default()
};
let h: Handle<ColorMaterial> = load_context.add_labeled_asset(format!("{}#material", load_context.asset_path()), m);
let color = Srgba::hex(parsed.tint.clone())?.into();
let m = ColorMaterial { color, ..default() };
let h: Handle<ColorMaterial> = load_context
.add_labeled_asset(format!("{}#material", load_context.asset_path()), m);
MeshMaterial2d(h)
};
let parsed = toml::from_slice::<ShapeAsset>(bytes.as_slice())?;
Ok(
ShapeAsset {
mesh,
material,
..parsed
}
)
Ok(ShapeAsset {
mesh,
material,
..parsed
})
}
fn extensions(&self) -> &[&str] {
@ -117,6 +168,16 @@ fn setup_blocks(
checklist.spawn_shape = true;
}
/// Blocks <- Shape Relationship
#[derive(Component)]
#[relationship(relationship_target = ShapeBlocks)]
struct ShapeBlock(Entity);
/// Shape -> Blocks Relationship
#[derive(Component)]
#[relationship_target(relationship = ShapeBlock)]
struct ShapeBlocks(Vec<Entity>);
/// Event handler for transforming a handle component into a thing
fn add_shape(
event: On<Add, AssetComponent<ShapeAsset>>,
@ -126,5 +187,25 @@ fn add_shape(
) {
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());
commands.entity(event.entity).insert(shape.as_bundle());
}
fn updated_shape_asset(
mut messages: MessageReader<AssetEvent<ShapeAsset>>,
query: Query<&AssetComponent<ShapeAsset>>,
) {
messages.read().for_each(|asset_event| {
match asset_event {
AssetEvent::Added { id } => debug!("Asset added: {id:?}"),
AssetEvent::Modified { id } => query.iter().filter(|AssetComponent { handle }| {
handle.id() == *id
}).for_each(|ac| {
warn!("TODO: Update {ac:?}");
}),
AssetEvent::Removed { id } => warn!("Asset removed: {id:?}"),
AssetEvent::Unused { id } => warn!("Asset is unused: {id:?}"),
AssetEvent::LoadedWithDependencies { id } => debug!("Asset lodaed: {id:?}"),
}
})
}

@ -16,22 +16,28 @@ pub struct DebugPlugin;
// - Show current state(s) in UI
impl Plugin for DebugPlugin {
fn build(&self, app: &mut App) {
app
.init_state::<DebugState>()
app.init_state::<DebugState>()
.add_systems(Startup, setup_ui)
.add_systems(Update,
// Logging state transitions
(
log_transition::<DebugState>.run_if(state_changed::<DebugState>),
log_transition::<Loading>.run_if(state_changed::<Loading>),
log_transition::<GameState>.run_if(state_changed::<GameState>),
))
.add_systems(Update, toggle_debug.run_if(input_just_pressed(KeyCode::F12)))
.add_systems(Update,
.add_systems(
Update,
// Logging state transitions
(
log_transition::<DebugState>.run_if(state_changed::<DebugState>),
log_transition::<Loading>.run_if(state_changed::<Loading>),
log_transition::<GameState>.run_if(state_changed::<GameState>),
),
)
.add_systems(
Update,
toggle_debug.run_if(input_just_pressed(KeyCode::F12)),
)
.add_systems(
Update,
(
toggle_state_visibility::<DebugState>,
sync_state_to_ui::<DebugState>,
).run_if(state_changed::<DebugState>)
)
.run_if(state_changed::<DebugState>),
);
}
}
@ -53,9 +59,7 @@ impl fmt::Display for DebugState {
}
/// Setup the debugger UI
fn setup_ui(
mut commands: Commands,
) {
fn setup_ui(mut commands: Commands) {
commands.spawn((
Node {
display: Display::Flex,
@ -67,7 +71,11 @@ fn setup_ui(
},
children![
(Text::new("debugger:"), ThemedText),
(toggle_switch((),), observe(checkbox_self_update), observe(debug_toggle)),
(
toggle_switch((),),
observe(checkbox_self_update),
observe(debug_toggle)
),
],
));
@ -81,18 +89,18 @@ fn setup_ui(
},
DebugState(true),
children![
(Text::new("DebugState State"), SyncState::<DebugState>::default()),
(
Text::new("DebugState State"),
SyncState::<DebugState>::default()
),
(Text::new("Loading State"), SyncState::<Loading>::default()),
(Text::new("Game State"), SyncState::<GameState>::default()),
]
],
));
}
/// Logs all state transitions for state T
fn log_transition<T: States + PartialEq + Clone>(
curr: Res<State<T>>,
mut prev: Local<Option<T>>,
) {
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);
@ -101,10 +109,7 @@ fn log_transition<T: States + PartialEq + Clone>(
}
/// Toggle the debug state when a key is pressed
fn toggle_debug(
curr: Res<State<DebugState>>,
mut next: ResMut<NextState<DebugState>>,
) {
fn toggle_debug(curr: Res<State<DebugState>>, mut next: ResMut<NextState<DebugState>>) {
next.set(DebugState(!curr.get().0));
}

@ -1,6 +1,7 @@
mod blocks;
mod debug;
mod fighter;
mod test;
use engine::*;
use serde::Deserialize;
@ -19,10 +20,10 @@ fn main() {
.init_state::<GameState>()
.init_resource::<AllAssets>()
.init_resource::<SetupChecklist>()
// Check if assets were added to loading queue
// Move game back to Loading(true) state
.add_systems(
Update,
loading_check
set_loading
.run_if(in_state(Loading(true)))
.run_if(resource_changed::<AllAssets>),
)
@ -33,16 +34,21 @@ fn main() {
// Check if the game is ready to progress
.add_systems(Update, setup_wait.run_if(in_state(GameState::Setup)))
// State toggles
.add_systems(Update, (
(
toggle_state_visibility::<Loading>,
sync_state_to_ui::<Loading>,
).run_if(state_changed::<Loading>),
.add_systems(
Update,
(
toggle_state_visibility::<GameState>,
sync_state_to_ui::<GameState>,
).run_if(state_changed::<GameState>)
))
(
toggle_state_visibility::<Loading>,
sync_state_to_ui::<Loading>,
)
.run_if(state_changed::<Loading>),
(
toggle_state_visibility::<GameState>,
sync_state_to_ui::<GameState>,
)
.run_if(state_changed::<GameState>),
),
)
.run();
}
@ -100,7 +106,7 @@ impl SetupChecklist {
}
/// 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>) {
fn set_loading(mut next: ResMut<NextState<Loading>>, all_assets: Res<AllAssets>) {
debug_assert!(all_assets.is_changed());
next.set(Loading(true));

@ -0,0 +1,3 @@
use super::*;
mod shape_layout;

@ -0,0 +1,70 @@
use super::*;
#[test]
fn test_shape_layout_01() {
let actual = ShapeLayout(vec![
vec![0, 1, 0],
vec![1, 1, 1],
]).positions();
let expected: [RelativePosition;4] = [
(1, 1).into(),
(-1, 0).into(),
(0, 0).into(),
(1, 0).into()
];
debug_assert_eq!(expected, actual);
}
#[test]
fn test_shape_layout_02a() {
let actual = ShapeLayout(vec![
vec![1],
vec![1],
vec![1],
vec![1],
]).positions();
let expected: [RelativePosition;4] = [
(0, 2).into(),
(0, 1).into(),
(0, 0).into(),
(0, -1).into()
];
debug_assert_eq!(expected, actual);
}
#[test]
fn test_shape_layout_02b() {
let actual = ShapeLayout(vec![
vec![1, 1, 1, 1],
]).positions();
let expected: [RelativePosition;4] = [
(-1, 0).into(),
(0, 0).into(),
(0, 1).into(),
(0, 2).into()
];
debug_assert_eq!(expected, actual);
}
#[test]
fn test_shape_layout_03() {
let actual = ShapeLayout(vec![
vec![1, 1, 0],
vec![0, 1, 1],
]).positions();
let expected: [RelativePosition;4] = [
(-1, 1).into(),
(0, 1).into(),
(0, 0).into(),
(1, 0).into()
];
debug_assert_eq!(expected, actual);
}
Loading…
Cancel
Save