Compare commits

..

No commits in common. '209e5f60c024e7debb00c2188fb8a7c1a9e0b938' and 'e7f816a7d5d02c740865ebd0649d808054289c8d' have entirely different histories.

@ -20,10 +20,8 @@ codegen-backend = "llvm"
[profile.wasm-release] [profile.wasm-release]
inherits = "release" inherits = "release"
opt-level = "z" opt-level = "s"
strip = true strip = "debuginfo"
panic = "abort"
lto = "thin"
[target.x86_64-unknown-linux-gnu] [target.x86_64-unknown-linux-gnu]
linker = "clang" linker = "clang"

135
engine/Cargo.lock generated

@ -349,6 +349,12 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]] [[package]]
name = "bevy" name = "bevy"
version = "0.19.0" version = "0.19.0"
@ -394,6 +400,7 @@ dependencies = [
"bevy_derive", "bevy_derive",
"bevy_ecs", "bevy_ecs",
"bevy_math", "bevy_math",
"bevy_mesh",
"bevy_platform", "bevy_platform",
"bevy_reflect", "bevy_reflect",
"bevy_time", "bevy_time",
@ -877,6 +884,7 @@ dependencies = [
"bevy_material", "bevy_material",
"bevy_math", "bevy_math",
"bevy_mesh", "bevy_mesh",
"bevy_pbr",
"bevy_reflect", "bevy_reflect",
"bevy_render", "bevy_render",
"bevy_shader", "bevy_shader",
@ -887,6 +895,42 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "bevy_gltf"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84e21ad83b7dc8d456491ea3d50057685d8c4cca8bc52a1b4a7161c86c369842"
dependencies = [
"async-lock",
"base64",
"bevy_animation",
"bevy_app",
"bevy_asset",
"bevy_camera",
"bevy_color",
"bevy_ecs",
"bevy_image",
"bevy_light",
"bevy_material",
"bevy_math",
"bevy_mesh",
"bevy_platform",
"bevy_reflect",
"bevy_tasks",
"bevy_transform",
"bevy_world_serialization",
"fixedbitset",
"gltf",
"itertools",
"percent-encoding",
"serde",
"serde_json",
"smallvec",
"thiserror 2.0.18",
"tracing",
"wgpu-types",
]
[[package]] [[package]]
name = "bevy_image" name = "bevy_image"
version = "0.19.0" version = "0.19.0"
@ -907,7 +951,9 @@ dependencies = [
"guillotiere", "guillotiere",
"half", "half",
"image", "image",
"ktx2",
"rectangle-pack", "rectangle-pack",
"ruzstd",
"serde", "serde",
"thiserror 2.0.18", "thiserror 2.0.18",
"tracing", "tracing",
@ -974,6 +1020,7 @@ dependencies = [
"bevy_gilrs", "bevy_gilrs",
"bevy_gizmos", "bevy_gizmos",
"bevy_gizmos_render", "bevy_gizmos_render",
"bevy_gltf",
"bevy_image", "bevy_image",
"bevy_input", "bevy_input",
"bevy_input_focus", "bevy_input_focus",
@ -1166,6 +1213,7 @@ dependencies = [
"bevy_derive", "bevy_derive",
"bevy_diagnostic", "bevy_diagnostic",
"bevy_ecs", "bevy_ecs",
"bevy_gltf",
"bevy_image", "bevy_image",
"bevy_light", "bevy_light",
"bevy_log", "bevy_log",
@ -2727,6 +2775,42 @@ dependencies = [
"web-sys", "web-sys",
] ]
[[package]]
name = "gltf"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3ce1918195723ce6ac74e80542c5a96a40c2b26162c1957a5cd70799b8cacf7"
dependencies = [
"byteorder",
"gltf-json",
"lazy_static",
"serde_json",
]
[[package]]
name = "gltf-derive"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14070e711538afba5d6c807edb74bcb84e5dbb9211a3bf5dea0dfab5b24f4c51"
dependencies = [
"inflections",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "gltf-json"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6176f9d60a7eab0a877e8e96548605dedbde9190a7ae1e80bbcc1c9af03ab14"
dependencies = [
"gltf-derive",
"serde",
"serde_derive",
"serde_json",
]
[[package]] [[package]]
name = "glutin_wgl_sys" name = "glutin_wgl_sys"
version = "0.6.1" version = "0.6.1"
@ -3050,6 +3134,12 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "inflections"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a"
[[package]] [[package]]
name = "inotify" name = "inotify"
version = "0.11.2" version = "0.11.2"
@ -3227,6 +3317,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "ktx2"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d50be8a24c37debdf3170be6ca396d5b5b165f37f94837891adac735ca416b8"
dependencies = [
"bitflags 2.12.1",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@ -3239,6 +3338,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "lewton"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030"
dependencies = [
"byteorder",
"ogg",
"tinyvec",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.186" version = "0.2.186"
@ -3948,6 +4058,15 @@ dependencies = [
"nonmax", "nonmax",
] ]
[[package]]
name = "ogg"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e"
dependencies = [
"byteorder",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.4" version = "1.21.4"
@ -4402,6 +4521,7 @@ checksum = "d0a536bb79db59098ef71a4dd4246c02eb87b316deceb1b68e0cde7167ec01eb"
dependencies = [ dependencies = [
"cpal", "cpal",
"dasp_sample", "dasp_sample",
"lewton",
"num-rational", "num-rational",
"thiserror 2.0.18", "thiserror 2.0.18",
"tracing", "tracing",
@ -4468,6 +4588,15 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ruzstd"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7c1c839d570d835527c9a5e4db7cb2198683a988cb9d7293fc8674e6bd58fc8"
dependencies = [
"twox-hash",
]
[[package]] [[package]]
name = "same-file" name = "same-file"
version = "1.0.6" version = "1.0.6"
@ -5017,6 +5146,12 @@ version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
[[package]]
name = "twox-hash"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c"
[[package]] [[package]]
name = "typeid" name = "typeid"
version = "1.0.3" version = "1.0.3"

@ -4,12 +4,7 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
bevy = { version = "0.19.0", default-features = false, features = [ bevy = "0.19.0"
"2d",
"ui",
"bevy_audio",
"png",
] }
engine_macros = { path = "macros" } engine_macros = { path = "macros" }
[features] [features]

@ -2,7 +2,6 @@ pub mod actions;
pub use actions::*; pub use actions::*;
pub mod color; pub mod color;
use bevy::ecs::query::QueryFilter;
pub use color::*; pub use color::*;
pub use std::time::Duration; pub use std::time::Duration;
@ -37,15 +36,3 @@ pub use bevy::{
checkbox_self_update, slider_self_update, checkbox_self_update, slider_self_update,
}, },
}; };
pub fn any_component_added<C: Component>(q: Query<Entity, Added<C>>) -> bool {
!q.is_empty()
}
pub fn any_component_changed<C: Component>(q: Query<Entity, Changed<C>>) -> bool {
!q.is_empty()
}
pub fn query_condition_empty<F: QueryFilter>(q: Query<Entity, F>) -> bool {
q.is_empty()
}

@ -37,10 +37,6 @@ fmt:
webbuild target: webbuild target:
scripts/webbuild {{target}} scripts/webbuild {{target}}
# Zip the built web output from dist/<target>/ into dist/<name>.zip
webzip target:
scripts/webzip {{target}}
# Serve a built web target from dist/<target>/ via simple-http-server # Serve a built web target from dist/<target>/ via simple-http-server
serve target: serve target:
scripts/serve {{target}} scripts/serve {{target}}

151
life/Cargo.lock generated

@ -349,6 +349,12 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]] [[package]]
name = "bevy" name = "bevy"
version = "0.19.0" version = "0.19.0"
@ -393,6 +399,7 @@ dependencies = [
"bevy_derive", "bevy_derive",
"bevy_ecs", "bevy_ecs",
"bevy_math", "bevy_math",
"bevy_mesh",
"bevy_platform", "bevy_platform",
"bevy_reflect", "bevy_reflect",
"bevy_time", "bevy_time",
@ -848,6 +855,7 @@ dependencies = [
"bevy_material", "bevy_material",
"bevy_math", "bevy_math",
"bevy_mesh", "bevy_mesh",
"bevy_pbr",
"bevy_reflect", "bevy_reflect",
"bevy_render", "bevy_render",
"bevy_shader", "bevy_shader",
@ -858,6 +866,42 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "bevy_gltf"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84e21ad83b7dc8d456491ea3d50057685d8c4cca8bc52a1b4a7161c86c369842"
dependencies = [
"async-lock",
"base64",
"bevy_animation",
"bevy_app",
"bevy_asset",
"bevy_camera",
"bevy_color",
"bevy_ecs",
"bevy_image",
"bevy_light",
"bevy_material",
"bevy_math",
"bevy_mesh",
"bevy_platform",
"bevy_reflect",
"bevy_tasks",
"bevy_transform",
"bevy_world_serialization",
"fixedbitset",
"gltf",
"itertools",
"percent-encoding",
"serde",
"serde_json",
"smallvec",
"thiserror 2.0.18",
"tracing",
"wgpu-types",
]
[[package]] [[package]]
name = "bevy_image" name = "bevy_image"
version = "0.19.0" version = "0.19.0"
@ -878,7 +922,9 @@ dependencies = [
"guillotiere", "guillotiere",
"half", "half",
"image", "image",
"ktx2",
"rectangle-pack", "rectangle-pack",
"ruzstd",
"serde", "serde",
"thiserror 2.0.18", "thiserror 2.0.18",
"tracing", "tracing",
@ -944,6 +990,7 @@ dependencies = [
"bevy_gilrs", "bevy_gilrs",
"bevy_gizmos", "bevy_gizmos",
"bevy_gizmos_render", "bevy_gizmos_render",
"bevy_gltf",
"bevy_image", "bevy_image",
"bevy_input", "bevy_input",
"bevy_input_focus", "bevy_input_focus",
@ -1136,6 +1183,7 @@ dependencies = [
"bevy_derive", "bevy_derive",
"bevy_diagnostic", "bevy_diagnostic",
"bevy_ecs", "bevy_ecs",
"bevy_gltf",
"bevy_image", "bevy_image",
"bevy_light", "bevy_light",
"bevy_log", "bevy_log",
@ -2679,6 +2727,42 @@ dependencies = [
"web-sys", "web-sys",
] ]
[[package]]
name = "gltf"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3ce1918195723ce6ac74e80542c5a96a40c2b26162c1957a5cd70799b8cacf7"
dependencies = [
"byteorder",
"gltf-json",
"lazy_static",
"serde_json",
]
[[package]]
name = "gltf-derive"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14070e711538afba5d6c807edb74bcb84e5dbb9211a3bf5dea0dfab5b24f4c51"
dependencies = [
"inflections",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "gltf-json"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6176f9d60a7eab0a877e8e96548605dedbde9190a7ae1e80bbcc1c9af03ab14"
dependencies = [
"gltf-derive",
"serde",
"serde_derive",
"serde_json",
]
[[package]] [[package]]
name = "glutin_wgl_sys" name = "glutin_wgl_sys"
version = "0.6.1" version = "0.6.1"
@ -3002,6 +3086,12 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "inflections"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a"
[[package]] [[package]]
name = "inotify" name = "inotify"
version = "0.11.2" version = "0.11.2"
@ -3158,6 +3248,15 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
name = "ktx2"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d50be8a24c37debdf3170be6ca396d5b5b165f37f94837891adac735ca416b8"
dependencies = [
"bitflags 2.13.0",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@ -3170,6 +3269,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "lewton"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030"
dependencies = [
"byteorder",
"ogg",
"tinyvec",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.186" version = "0.2.186"
@ -3214,14 +3324,6 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "life"
version = "0.1.0"
dependencies = [
"bevy",
"engine",
]
[[package]] [[package]]
name = "linebender_resource_handle" name = "linebender_resource_handle"
version = "0.1.1" version = "0.1.1"
@ -3835,6 +3937,15 @@ dependencies = [
"nonmax", "nonmax",
] ]
[[package]]
name = "ogg"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e"
dependencies = [
"byteorder",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.4" version = "1.21.4"
@ -4101,6 +4212,14 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5"
[[package]]
name = "prototypes"
version = "0.1.0"
dependencies = [
"bevy",
"engine",
]
[[package]] [[package]]
name = "pxfm" name = "pxfm"
version = "0.1.29" version = "0.1.29"
@ -4279,6 +4398,7 @@ checksum = "d0a536bb79db59098ef71a4dd4246c02eb87b316deceb1b68e0cde7167ec01eb"
dependencies = [ dependencies = [
"cpal", "cpal",
"dasp_sample", "dasp_sample",
"lewton",
"num-rational", "num-rational",
"thiserror 2.0.18", "thiserror 2.0.18",
"tracing", "tracing",
@ -4345,6 +4465,15 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ruzstd"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7c1c839d570d835527c9a5e4db7cb2198683a988cb9d7293fc8674e6bd58fc8"
dependencies = [
"twox-hash",
]
[[package]] [[package]]
name = "same-file" name = "same-file"
version = "1.0.6" version = "1.0.6"
@ -4884,6 +5013,12 @@ version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
[[package]]
name = "twox-hash"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c"
[[package]] [[package]]
name = "typeid" name = "typeid"
version = "1.0.3" version = "1.0.3"

@ -1,15 +1,10 @@
[package] [package]
name = "life" name = "prototypes"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
bevy = { version = "0.19.0", default-features = false, features = [ bevy = "0.19.0"
"2d",
"ui",
"bevy_audio",
"png",
] }
[dependencies.engine] [dependencies.engine]
path = "../engine" path = "../engine"

@ -15,7 +15,8 @@ impl Plugin for GameActionsPlugin {
.bind(ZoomAction::Zoom, [Binding::Scroll]) .bind(ZoomAction::Zoom, [Binding::Scroll])
.bind(ZoomAction::ZoomIn, [Binding::Key(KeyCode::Equal)]) .bind(ZoomAction::ZoomIn, [Binding::Key(KeyCode::Equal)])
.bind(ZoomAction::ZoomOut, [Binding::Key(KeyCode::Minus)]), .bind(ZoomAction::ZoomOut, [Binding::Key(KeyCode::Minus)]),
ActionsPlugin::<Action>::new().bind(Action::Quit, [Binding::Key(KeyCode::Escape)]), ActionsPlugin::<Action>::new()
.bind(Action::Quit, [Binding::Key(KeyCode::Escape)]),
)); ));
} }
} }
@ -98,7 +99,7 @@ pub(crate) enum ZoomAction {
#[action_handler(ZoomAction::zoom_out)] #[action_handler(ZoomAction::zoom_out)]
ZoomOut, ZoomOut,
#[default] #[default]
None, None
} }
#[derive(Copy, Clone, Eq, PartialEq, Hash, GameAction, Default)] #[derive(Copy, Clone, Eq, PartialEq, Hash, GameAction, Default)]
@ -110,7 +111,7 @@ pub(crate) enum Action {
} }
impl ZoomAction { impl ZoomAction {
pub(crate) fn zoom_by(factor: f32, mut projection: Query<&mut Projection>) { fn zoom_by(factor: f32, mut projection: Query<&mut Projection>) {
projection.iter_mut().for_each(|mut p| { projection.iter_mut().for_each(|mut p| {
if let Projection::Orthographic(o) = &mut *p { if let Projection::Orthographic(o) = &mut *p {
o.scale = (o.scale * factor).clamp(0.05, 20.0); o.scale = (o.scale * factor).clamp(0.05, 20.0);

@ -1,17 +1,13 @@
// TODO: // TODO:
// * Lock down system scheduling/run_if conditions // * Fix randomly audio not working
// * Performance: have a limit on per-note+octave sounds // * Lock down system scheduling/run_if conditions
// * Use a finite set of N audio entities and scale volume instead of // * Fix tiling repeating
// spawning a bunch of audio entities which is causing issues at >100 // * Negative coordinates should not mirror
// cells // * UI widget explaining rules/hotkeys
// * Stop creating cells when I didn't mean to (clicking UI, click + dragging)
// * Keep some controls open when main ui panel is closed
// * Exclude power button from web build
// * Show shadow of piece (same tile, just with alpha transparency) // * Show shadow of piece (same tile, just with alpha transparency)
// * Dark mode // * Start with an interesting pattern
// * UI minimize widgets
// * Make click and drag feel better // * Make click and drag feel better
// * Add arrow keys to move around
// * "Follow" enhancement: Zoom in/out
#![allow(clippy::complexity)] #![allow(clippy::complexity)]
// Only because of FromTemplat macros unfortunately... // Only because of FromTemplat macros unfortunately...
#![allow(dead_code)] #![allow(dead_code)]
@ -41,8 +37,6 @@ fn main() {
.insert_resource(UiTheme(create_light_theme())) .insert_resource(UiTheme(create_light_theme()))
.init_state::<SimulationPlayback>() .init_state::<SimulationPlayback>()
.init_state::<AudioPlayback>() .init_state::<AudioPlayback>()
.init_state::<DebugUi>()
.init_state::<MainUi>()
.add_message::<Lifecycle>() .add_message::<Lifecycle>()
.add_systems( .add_systems(
Startup, Startup,
@ -54,52 +48,22 @@ fn main() {
load_meshes, load_meshes,
), ),
) )
.configure_sets(
Update,
(GameSet::Plan, GameSet::Apply, GameSet::React).chain(),
)
// Cell management/Simulation // Cell management/Simulation
.add_systems( .add_systems(
Update, Update,
( (
// Produce lifecycle messages cell_lifecycle.run_if(on_message::<Lifecycle>),
( update_grid_position.run_if(on_message::<CursorMoved>),
default_board.run_if(run_once), de_spawn_cells
update_grid_position.run_if(on_message::<CursorMoved>), .run_if(not(is_ui_hovered))
de_spawn_cells .run_if(not(is_panning))
.run_if(not(is_ui_hovered)) .run_if(input_just_released(MouseButton::Left)),
.run_if(not(is_panning)) simulation_step
.run_if(input_just_released(MouseButton::Left)), .run_if(in_state(SimulationPlayback::Run))
simulation_step.run_if( .run_if(cooldown_secs(SIM_FRAME_DURATION)),
in_state(SimulationPlayback::Run) simulation_step.run_if(state_changed::<SimulationPlayback>),
.and_then(cooldown_secs(SIM_FRAME_DURATION)) pause_simulation.run_if(in_state(SimulationPlayback::Step)),
.or_else(state_changed::<SimulationPlayback>), update_material,
),
pause_simulation.run_if(in_state(SimulationPlayback::Step)),
)
.in_set(GameSet::Plan),
// Apply lifecycle message
cell_lifecycle
.run_if(on_message::<Lifecycle>)
.in_set(GameSet::Apply),
// React to board changes
(
reassign_cell_pitch.run_if(resource_changed::<PitchMap>),
update_material.run_if(
any_component_added::<CellPitch>
.or_else(any_component_changed::<CellPitch>),
),
camera_follow
.run_if(in_state(SimulationPlayback::Run))
.run_if(any_component_changed::<Coordinates>),
update_camera_position
.run_if(in_state(SimulationPlayback::Run))
.run_if(not(query_condition_empty::<(
With<Camera>,
Changed<Coordinates>,
)>)),
)
.in_set(GameSet::React),
), ),
) )
// Audio Systems // Audio Systems
@ -108,41 +72,28 @@ fn main() {
( (
control_volume, control_volume,
update_pitch_map_resource_ui.run_if(resource_changed::<PitchMap>), update_pitch_map_resource_ui.run_if(resource_changed::<PitchMap>),
update_audio reassign_cell_pitch.run_if(resource_changed::<PitchMap>),
.run_if( update_audio,
any_component_added::<CellPitch>
.or_else(any_component_changed::<CellPitch>),
)
.in_set(GameSet::React),
), ),
) )
// Debugging // Debugging
.add_systems( .add_systems(
Update, Update,
( (
assert_cell_uniqueness.run_if(any_with_component::<Cell>),
grid_gizmo, grid_gizmo,
update_debug_info_cell_count,
update_debug_info_coordinates, update_debug_info_coordinates,
( update_debug_info_active,
assert_cell_uniqueness
.run_if(any_with_component::<Cell>)
.after(update_material)
.after(update_audio),
update_debug_info_cell_count,
update_debug_info_active,
)
.in_set(GameSet::React),
), ),
) )
// Ui Controllers // Ui Controllers
.add_systems( .add_systems(
Update, Update,
( (
toggle_state_visible::<SimulationPlayback> toggle_state_visible::<SimulationPlayback>.run_if(state_changed::<SimulationPlayback>),
.run_if(state_changed::<SimulationPlayback>),
manage_interactive::<SimulationPlayback>, manage_interactive::<SimulationPlayback>,
toggle_state_visible::<AudioPlayback>.run_if(state_changed::<AudioPlayback>), toggle_state_visible::<AudioPlayback>.run_if(state_changed::<AudioPlayback>),
toggle_state_visible::<DebugUi>.run_if(state_changed::<DebugUi>),
toggle_state_visible::<MainUi>.run_if(state_changed::<MainUi>),
manage_interactive::<AudioPlayback>, manage_interactive::<AudioPlayback>,
update_note_interactivity, update_note_interactivity,
), ),
@ -157,13 +108,6 @@ fn main() {
const SCALE: f32 = 100.0; const SCALE: f32 = 100.0;
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
enum GameSet {
Plan,
Apply,
React,
}
#[derive(Component, PartialEq, Clone, Default)] #[derive(Component, PartialEq, Clone, Default)]
enum DebugInfo { enum DebugInfo {
CellCount, CellCount,
@ -173,20 +117,6 @@ enum DebugInfo {
Empty, Empty,
} }
#[derive(States, Debug, Default, Hash, PartialEq, Eq, Clone, Component, FromTemplate)]
enum DebugUi {
#[default]
Visible,
Hidden,
}
#[derive(States, Debug, Default, Hash, PartialEq, Eq, Clone, Component, FromTemplate)]
enum MainUi {
#[default]
Visible,
Hidden,
}
#[derive(States, Debug, Default, Hash, PartialEq, Eq, Clone, Component, FromTemplate)] #[derive(States, Debug, Default, Hash, PartialEq, Eq, Clone, Component, FromTemplate)]
enum AudioPlayback { enum AudioPlayback {
#[default] #[default]
@ -205,6 +135,11 @@ enum UiButton {
ZoomIn, ZoomIn,
PlayAudio, PlayAudio,
MuteAudio, MuteAudio,
AddHighOctave,
SubHighOctave,
AddLowOctave,
SubLowOctave,
Note,
Reset, Reset,
Wipe, Wipe,
#[default] #[default]
@ -222,6 +157,11 @@ impl UiButton {
Self::ZoomIn => "zoomIn.png", Self::ZoomIn => "zoomIn.png",
Self::PlayAudio => "audioOn.png", Self::PlayAudio => "audioOn.png",
Self::MuteAudio => "audioOff.png", Self::MuteAudio => "audioOff.png",
Self::AddHighOctave => "todo.png",
Self::SubHighOctave => "todo.png",
Self::AddLowOctave => "todo.png",
Self::SubLowOctave => "todo.png",
Self::Note => "todo.png",
Self::Reset => "return.png", Self::Reset => "return.png",
Self::Wipe => "trashcan.png", Self::Wipe => "trashcan.png",
Self::None => todo!(), Self::None => todo!(),
@ -259,10 +199,7 @@ fn ui_button(button: UiButton) -> impl Scene {
} }
} }
fn ui_toggle_button<A: Scene, B: Scene>( fn ui_toggle_button<A: Scene, B: Scene>((button_a, state_a): (UiButton, A), (button_b, state_b): (UiButton, B)) -> impl Scene {
(button_a, state_a): (UiButton, A),
(button_b, state_b): (UiButton, B),
) -> impl Scene {
bsn! { bsn! {
@FeathersButton @FeathersButton
Hovered Hovered
@ -302,7 +239,6 @@ fn ui_toggle_button<A: Scene, B: Scene>(
fn the_camera() -> impl Scene { fn the_camera() -> impl Scene {
bsn! { bsn! {
Camera2d Camera2d
Coordinates
} }
} }
@ -352,33 +288,21 @@ fn the_ui() -> impl Scene {
} }
fn debug_ui() -> impl Scene { fn debug_ui() -> impl Scene {
fn toggle_debug_ui(
_: On<Pointer<Click>>,
mut next: ResMut<NextState<DebugUi>>,
curr: Res<State<DebugUi>>,
) {
next.set(match curr.get() {
DebugUi::Hidden => DebugUi::Visible,
DebugUi::Visible => DebugUi::Hidden,
});
}
bsn! { bsn! {
Node { Node {
justify_self: JustifySelf::End, justify_self: JustifySelf::End,
display: Display::Flex, display: Display::Flex,
flex_direction: FlexDirection::Column, flex_direction: FlexDirection::Column,
} }
// TODO: Toggle visibility when debugging disabled
pane() pane()
Transform Transform
Children [ Children [
pane_header() pane_header()
on(toggle_debug_ui)
Children [ Children [
Text("Debug Info") ThemedText, Text("Debug Info") ThemedText,
], ],
pane_body() pane_body()
DebugUi::Visible
ToggleStateVisible
Children [ Children [
subpane() subpane()
Node { Node {
@ -462,182 +386,6 @@ fn primary_ui() -> impl Scene {
} }
} }
fn toggle_main_ui(
_: On<Pointer<Click>>,
mut next: ResMut<NextState<MainUi>>,
curr: Res<State<MainUi>>,
) {
next.set(match curr.get() {
MainUi::Hidden => MainUi::Visible,
MainUi::Visible => MainUi::Hidden,
});
}
fn description_text() -> impl Scene {
bsn! {
subpane()
Node {
display: Display::Flex,
flex_direction: FlexDirection::Column,
align_items: AlignItems::Stretch,
justify_content: JustifyContent::Start,
}
text_setup()
Children [
Text("Welcome to a little experiment I made: adding sound to Conways Game of Life") ThemedText,
Text("---") ThemedText,
Text("The buttons above control (in order)") ThemedText,
Text("Quit the game") ThemedText,
Text("Start/Pause the simulation") ThemedText,
Text("Step the simulation") ThemedText,
Text("Reset the world state") ThemedText,
Text("Wipe the world state") ThemedText,
Text("Zoom in/out") ThemedText,
Text("Toggle sound on/off") ThemedText,
Text("---") ThemedText,
Text("Below that are the notes you can toggle on/off") ThemedText,
Text("And below that are the number of octaves for each note") ThemedText,
Text("Notes are color coded horizontally") ThemedText,
Text("Octaves are color coded vertically") ThemedText,
Text("---") ThemedText,
Text("The camera automatically follows the action") ThemedText,
Text("---") ThemedText,
Text("Hotkyes") ThemedText,
Text("* Space: Play/Pause Simulation") ThemedText,
Text("* m: Mute/Play Audio") ThemedText,
Text("* r: Reset Simulation") ThemedText,
Text("* n: Next Simulation Step") ThemedText,
Text("* -/=: Zoom In/Out") ThemedText,
]
}
}
fn controls() -> impl Scene {
bsn! {
subpane()
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
align_items: AlignItems::Stretch,
justify_content: JustifyContent::Start,
}
Children [
subpane_header() Children [
Text("Nav") ThemedText
]
subpane_body()
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
align_items: AlignItems::Stretch,
justify_content: JustifyContent::Start,
}
Children [
ui_button(UiButton::Power)
ActionSource<Action>(Action::Quit),
ui_toggle_button((UiButton::Pause, bsn! { SimulationPlayback::Pause }), (UiButton::Play, bsn! { SimulationPlayback::Run }))
ActionSource<SimAction>(SimAction::Toggle),
ui_button(UiButton::Step)
ActionSource<SimAction>(SimAction::Step),
ui_button(UiButton::Reset)
ActionSource<SimAction>(SimAction::Reset),
ui_button(UiButton::Wipe)
ActionSource<SimAction>(SimAction::Wipe),
ui_button(UiButton::ZoomOut)
ActionSource<ZoomAction>(ZoomAction::ZoomOut),
ui_button(UiButton::ZoomIn)
ActionSource<ZoomAction>(ZoomAction::ZoomIn),
ui_toggle_button((UiButton::MuteAudio, bsn! { AudioPlayback::Mute }), (UiButton::PlayAudio, bsn! { AudioPlayback::Play }))
ActionSource<AudioAction>(AudioAction::Toggle),
]
]
}
}
fn notes() -> impl Scene {
bsn! {
subpane()
Children [
subpane_header() Children [
Text("Notes") ThemedText
]
subpane_body()
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
align_items: AlignItems::Stretch,
justify_content: JustifyContent::Start,
}
Children [
ui_checkbox("A")
template_value(Note::A)
on(toggle_note),
ui_checkbox("A#")
template_value(Note::ASharp)
on(toggle_note),
ui_checkbox("B")
template_value(Note::B)
on(toggle_note),
ui_checkbox("C")
template_value(Note::C)
on(toggle_note),
ui_checkbox("C#")
template_value(Note::CSharp)
on(toggle_note),
ui_checkbox("D")
template_value(Note::D)
on(toggle_note),
ui_checkbox("D#")
template_value(Note::DSharp)
on(toggle_note),
ui_checkbox("E")
template_value(Note::E)
on(toggle_note),
ui_checkbox("F")
template_value(Note::F)
on(toggle_note),
ui_checkbox("F#")
template_value(Note::FSharp)
on(toggle_note),
ui_checkbox("G")
template_value(Note::G)
on(toggle_note),
ui_checkbox("G#")
template_value(Note::GSharp)
on(toggle_note),
]
]
}
}
fn octaves() -> impl Scene {
bsn! {
subpane()
subpane_header() Children [
Text("Octaves") ThemedText
]
subpane_body()
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
align_items: AlignItems::Stretch,
justify_content: JustifyContent::Start,
}
Children [
@FeathersSlider { @max: 11., @value: 5., @min: 1. }
SliderStep(2.)
SliderPrecision(0)
on(|value_change: On<ValueChange<f32>>, mut pitch_map: ResMut<PitchMap>| {
if value_change.is_final {
pitch_map.octaves = value_change.value as usize;
}
})
on(slider_self_update),
]
}
}
bsn! { bsn! {
Node { Node {
display: Display::Flex, display: Display::Flex,
@ -648,19 +396,133 @@ fn primary_ui() -> impl Scene {
pane() pane()
Children [ Children [
pane_header() pane_header()
on(toggle_main_ui)
// todo: toggle main ui
Children [ Children [
Text("Conways Game of Life With Sound (click here to open control panel)") ThemedText, Text("Conways Game of Life With Sound") ThemedText,
], ],
pane_body() pane_body()
MainUi::Visible
ToggleStateVisible
Children [ Children [
controls(), subpane()
notes(), Node {
octaves(), display: Display::Flex,
description_text(), flex_direction: FlexDirection::Row,
align_items: AlignItems::Stretch,
justify_content: JustifyContent::Start,
}
Children [
subpane_header() Children [
Text("Nav") ThemedText
]
subpane_body()
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
align_items: AlignItems::Stretch,
justify_content: JustifyContent::Start,
}
Children [
ui_button(UiButton::Power)
ActionSource<Action>(Action::Quit),
ui_toggle_button((UiButton::Pause, bsn! { SimulationPlayback::Pause }), (UiButton::Play, bsn! { SimulationPlayback::Run }))
ActionSource<SimAction>(SimAction::Toggle),
ui_button(UiButton::Step)
ActionSource<SimAction>(SimAction::Step),
ui_button(UiButton::Reset)
ActionSource<SimAction>(SimAction::Reset),
ui_button(UiButton::Wipe)
ActionSource<SimAction>(SimAction::Wipe),
ui_button(UiButton::ZoomOut)
ActionSource<ZoomAction>(ZoomAction::ZoomOut),
ui_button(UiButton::ZoomIn)
ActionSource<ZoomAction>(ZoomAction::ZoomIn),
ui_toggle_button((UiButton::MuteAudio, bsn! { AudioPlayback::Mute }), (UiButton::PlayAudio, bsn! { AudioPlayback::Play }))
ActionSource<AudioAction>(AudioAction::Toggle),
]
],
subpane()
Children [
subpane_header() Children [
Text("Notes") ThemedText
]
subpane_body()
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
align_items: AlignItems::Stretch,
justify_content: JustifyContent::Start,
}
Children [
ui_checkbox("A")
template_value(Note::A)
on(toggle_note),
ui_checkbox("A#")
template_value(Note::ASharp)
on(toggle_note),
ui_checkbox("B")
template_value(Note::B)
on(toggle_note),
ui_checkbox("C")
template_value(Note::C)
on(toggle_note),
ui_checkbox("C#")
template_value(Note::CSharp)
on(toggle_note),
ui_checkbox("D")
template_value(Note::D)
on(toggle_note),
ui_checkbox("D#")
template_value(Note::DSharp)
on(toggle_note),
ui_checkbox("E")
template_value(Note::E)
on(toggle_note),
ui_checkbox("F")
template_value(Note::F)
on(toggle_note),
ui_checkbox("F#")
template_value(Note::FSharp)
on(toggle_note),
ui_checkbox("G")
template_value(Note::G)
on(toggle_note),
ui_checkbox("G#")
template_value(Note::GSharp)
on(toggle_note),
]
],
subpane()
subpane_header() Children [
Text("Octaves") ThemedText
]
subpane_body()
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
align_items: AlignItems::Stretch,
justify_content: JustifyContent::Start,
}
Children [
@FeathersSlider { @max: 5., @value: 3., @min: 1. }
SliderStep(1.)
SliderPrecision(0)
on(|value_change: On<ValueChange<f32>>, mut pitch_map: ResMut<PitchMap>| {
if value_change.is_final {
pitch_map.octaves_up = value_change.value as usize;
debug!("(octave up) Updated pitch map: {:#?}", pitch_map);
}
})
on(slider_self_update),
@FeathersSlider { @max: 5., @value: 3., @min: 1. }
SliderStep(1.)
SliderPrecision(0)
on(|value_change: On<ValueChange<f32>>, mut pitch_map: ResMut<PitchMap>| {
if value_change.is_final {
pitch_map.octaves_down = value_change.value as usize;
debug!("(octave down) Updated pitch map: {:#?}", pitch_map);
}
})
on(slider_self_update),
],
] ]
] ]
} }
@ -725,19 +587,39 @@ impl Coordinates {
} }
/// For the purposes of creating assests, what does the given coordinate translate to CellPitch wise /// For the purposes of creating assests, what does the given coordinate translate to CellPitch wise
fn to_cell_pitch(&self, pm: &PitchMap) -> CellPitch { fn to_cell_pitch(&self) -> CellPitch {
let rows = pm.notes.len(); let Coordinates { x, y } = self.normalized();
let cols = pm.octaves; let note = match x {
0 => Note::A,
let this_x = self.x.rem_euclid(rows as isize); 1 => Note::ASharp,
let this_y = self.y.rem_euclid(cols as isize) - (pm.octaves as isize / 2); 2 => Note::B,
3 => Note::C,
debug_assert!(this_x >= 0); 4 => Note::CSharp,
5 => Note::D,
6 => Note::DSharp,
7 => Note::E,
8 => Note::F,
9 => Note::FSharp,
10 => Note::G,
11 => Note::GSharp,
_ => panic!("This shouldn't happen!"),
};
let octave = y;
CellPitch { note, octave }
}
let note = pm.notes.get(this_x as usize).unwrap().clone(); // Turn an arbitrary coordinate to a "normalized" coordinate centered on 0,0
let octave = this_y; fn normalized(&self) -> Self {
println!("normalizing {:?}", (self.x, self.y));
let x = self.x.rem_euclid(12);
let y = self.y.rem_euclid(11);
println!("normalized {:?}", (x,y));
debug_assert!(x >= 0);
debug_assert!(x <= 12);
debug_assert!(y >= 0);
debug_assert!(y <= 11);
CellPitch { note, octave } Coordinates { x, y }
} }
} }
@ -916,21 +798,24 @@ fn new_cell(
let playback_settings = PlaybackSettings::ONCE.with_volume(volume); let playback_settings = PlaybackSettings::ONCE.with_volume(volume);
let cell_pitch = c.to_cell_pitch(&(*pitch_map)); let cell_pitch = pitch_map.coordinates_cell_pitch(&c);
debug!("cell pitch: {:#?}", cell_pitch); #[derive(Clone, Default, Component)]
debug!("materials: {:#?}", cell_assets.materials); enum Foo {
#[default]
A,
}
bsn! { bsn! {
Cell Cell
Coordinates { x, y } Coordinates { x, y }
template_value(cell_pitch.clone()) template_value(cell_pitch)
template_value(playback_settings) template_value(playback_settings)
template_value(Transform::from_translation(c.as_translation() + Vec3::new(0.0, 0.0, 1.0))) template_value(Transform::from_translation(c.as_translation() + Vec3::new(0.0, 0.0, 1.0)))
template_value(Mesh2dTemplate(cell_assets.mesh.clone().into())) template_value(Mesh2dTemplate(cell_assets.mesh.clone().into()))
// AudioPlayer added by update_audio // AudioPlayer added by update_audio
// MeshMaterial2d added by update_material // MeshMaterial2d added by update_material
template_value(MeshMaterial2dTemplate(cell_assets.materials.get(&cell_pitch).unwrap().clone().into())) template_value(MeshMaterial2dTemplate(cell_assets.materials.get(&pitch_map.coordinates_cell_pitch(&c)).unwrap().clone().into()))
} }
} }
@ -1096,7 +981,7 @@ impl CellPitch {
] ]
.into_iter() .into_iter()
.flat_map(|note| { .flat_map(|note| {
(-5..=5).map(move |octave| CellPitch { (0..11).map(move |octave| CellPitch {
note: note.clone(), note: note.clone(),
octave, octave,
}) })
@ -1175,15 +1060,6 @@ fn load_meshes(mut cell_assets: ResMut<CellAssets>, mut meshes: ResMut<Assets<Me
cell_assets.mesh = meshes.add(Rectangle::new(SCALE, SCALE)); cell_assets.mesh = meshes.add(Rectangle::new(SCALE, SCALE));
} }
fn default_board(mut writer: MessageWriter<Lifecycle>) {
// https://playgameoflife.com/lexicon/R-pentomino
[(0, 0), (0, -1), (-1, 0), (0, 1), (1, 1)]
.into_iter()
.for_each(|(x, y)| {
writer.write(Lifecycle::Alive(Coordinates { x, y }));
});
}
// TODO: Do not place piece if panning // TODO: Do not place piece if panning
// TODO: Tie cursor to real world space, not pixels // TODO: Tie cursor to real world space, not pixels
fn mouse_pan( fn mouse_pan(
@ -1267,14 +1143,16 @@ fn manage_interactive<T: Component + States + PartialEq>(
#[derive(Resource, Debug)] #[derive(Resource, Debug)]
struct PitchMap { struct PitchMap {
notes: Vec<Note>, notes: Vec<Note>,
octaves: usize, octaves_up: usize,
octaves_down: usize,
} }
impl Default for PitchMap { impl Default for PitchMap {
fn default() -> Self { fn default() -> Self {
Self { Self {
notes: vec![Note::A, Note::B, Note::CSharp, Note::E, Note::FSharp], notes: vec![Note::A, Note::B, Note::CSharp, Note::E, Note::FSharp],
octaves: 5, octaves_up: 3,
octaves_down: 3,
} }
} }
} }
@ -1289,33 +1167,72 @@ impl PitchMap {
// always ensure the notes list is sorted // always ensure the notes list is sorted
self.notes.sort(); self.notes.sort();
} }
/// Given the currently selected notes and octave range selected in the PitchMap convert the given coordinates to a CellPitch
fn coordinates_cell_pitch(&self, c: &Coordinates) -> CellPitch {
let Coordinates { x, y } = c.normalized();
let note: Note = {
let num_notes = self.notes.len();
self.notes.get((x % num_notes as isize) as usize).unwrap().clone()
};
let octave: isize = {
let num_octaves = self.octaves_up + self.octaves_down + 1;
println!("num_octaves: {:?}", num_octaves);
(y % num_octaves as isize) as isize
};
let cp = CellPitch { note, octave };
println!("Cell Pitch: {:?}", cp);
cp
}
} }
#[test] #[test]
fn test_cell_pitch() { fn test_cell_pitch() {
let pm = PitchMap { let pm = PitchMap {
notes: vec![Note::A, Note::B, Note::C], notes: vec![Note::A, Note::B, Note::C],
octaves: 3, octaves_up: 1,
octaves_down: 1,
}; };
let xs = -6..=5; {
let ys = -6..=4; let xs = [-6, -3, 0, 3, 6];
xs.enumerate().for_each(|(i, x)| { let ys = [-5, -2, 0, 3, 6];
ys.clone().enumerate().for_each(|(j, y)| { xs.iter().for_each(|x| {
let c = Coordinates { x, y }; ys.iter().for_each(|y| {
let n = pm.notes.get(i % 3).unwrap(); let CellPitch { note, octave } = pm.coordinates_cell_pitch(&Coordinates { x: *x, y: *y });
let o = (j as isize % 3) - 1; debug_assert_eq!(note, Note::A);
let cp = c.to_cell_pitch(&pm); debug_assert_eq!(octave, 0);
debug_assert_eq!(cp.note, *n); });
debug_assert_eq!(cp.octave, o); });
}) }
}); {
let xs = [-5, -2, 1, 4, 7];
let ys = [-4, -1, 1, 4, 7];
xs.iter().for_each(|x| {
ys.iter().for_each(|y| {
let CellPitch { note, octave } = pm.coordinates_cell_pitch(&Coordinates { x: *x, y: *y });
debug_assert_eq!(note, Note::B);
debug_assert_eq!(octave, 1);
});
});
}
{
let xs = [-4, -1, 2, 5, 8];
let ys = [-3, 0, 2, 5, 8];
xs.iter().for_each(|x| {
ys.iter().for_each(|y| {
let CellPitch { note, octave } = pm.coordinates_cell_pitch(&Coordinates { x: *x, y: *y });
debug_assert_eq!(note, Note::C);
debug_assert_eq!(octave, 2);
});
});
}
} }
/// When the pitch map changes, update all cells to use the correct pitch (note + octave) /// When the pitch map changes, update all cells to use the correct pitch (note + octave)
fn reassign_cell_pitch(mut query: Query<(&mut CellPitch, &Coordinates)>, pitch_map: Res<PitchMap>) { fn reassign_cell_pitch(mut query: Query<(&mut CellPitch, &Coordinates)>, pitch_map: Res<PitchMap>) {
debug!("query size: {:?}", query.iter().len()); debug!("query size: {:?}", query.iter().len());
query.iter_mut().for_each(|(mut cell_pitch, c)| { query.iter_mut().for_each(|(mut cell_pitch, c)| {
*cell_pitch = c.to_cell_pitch(&(*pitch_map)); *cell_pitch = pitch_map.coordinates_cell_pitch(c);
debug!("Updating cell pitch for {:?}", c); debug!("Updating cell pitch for {:?}", c);
}); });
} }
@ -1404,7 +1321,7 @@ struct ToggleStateVisible;
fn toggle_state_visible<S: States + Component>( fn toggle_state_visible<S: States + Component>(
mut q: Query<(&S, &mut Node), With<ToggleStateVisible>>, mut q: Query<(&S, &mut Node), With<ToggleStateVisible>>,
c: Res<State<S>>, c: Res<State<S>>
) { ) {
q.iter_mut().for_each(|(s, mut node)| { q.iter_mut().for_each(|(s, mut node)| {
node.display = if s == c.get() { node.display = if s == c.get() {
@ -1414,41 +1331,3 @@ fn toggle_state_visible<S: States + Component>(
}; };
}) })
} }
fn camera_follow(
mut camera: Query<&mut Coordinates, With<Camera>>,
cells: Query<&Coordinates, (With<Cell>, Without<Camera>)>,
) {
camera.iter_mut().for_each(|mut c| {
let left = cells
.iter()
.map(|Coordinates { x, .. }| *x)
.min()
.unwrap_or(0);
let right = cells
.iter()
.map(|Coordinates { x, .. }| *x)
.max()
.unwrap_or(0);
let top = cells
.iter()
.map(|Coordinates { y, .. }| *y)
.max()
.unwrap_or(0);
let bottom = cells
.iter()
.map(|Coordinates { y, .. }| *y)
.min()
.unwrap_or(0);
c.x = (right + left) / 2;
c.y = (top + bottom) / 2;
});
}
fn update_camera_position(mut query: Query<(&Coordinates, &mut Transform), With<Camera>>) {
query.iter_mut().for_each(|(c, mut t)| {
t.translation = c.as_translation();
});
}

@ -35,17 +35,8 @@ wasm-bindgen \
--out-name "bin" \ --out-name "bin" \
"${WASM}" "${WASM}"
# wasm-opt size pass. # wasm-opt size pass
# Enable the wasm features rustc/wasm-bindgen emit (bulk-memory etc.) so
# validation passes, and strip debug/producers sections for size.
wasm-opt -Oz \ wasm-opt -Oz \
--strip-debug --strip-producers \
--enable-bulk-memory \
--enable-nontrapping-float-to-int \
--enable-sign-ext \
--enable-reference-types \
--enable-mutable-globals \
--enable-multivalue \
-o "${DIST}/bin_bg-tmp.wasm" \ -o "${DIST}/bin_bg-tmp.wasm" \
"${DIST}/bin_bg.wasm" "${DIST}/bin_bg.wasm"
mv "${DIST}/bin_bg-tmp.wasm" "${DIST}/bin_bg.wasm" mv "${DIST}/bin_bg-tmp.wasm" "${DIST}/bin_bg.wasm"

@ -1,23 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
TARGET=$1
NAME="$(basename "${TARGET}")"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
DIST="${REPO_ROOT}/dist/${TARGET}"
ARCHIVE="${REPO_ROOT}/dist/${NAME}.tar.zst"
if [[ ! -d "${DIST}" ]]; then
echo "error: nothing built at ${DIST}; run 'just webbuild ${TARGET}' first" >&2
exit 1
fi
# Archive the built web output (files at archive root). The host serves the
# unpacked files, so the archive only affects upload size -- use zstd at its
# max ratio (--ultra -22), which beats zip's DEFLATE substantially on the
# wasm-dominated payload while decompressing far faster than xz.
rm -f "${ARCHIVE}"
tar -C "${DIST}" -cf - . | zstd -q --ultra -22 -c > "${ARCHIVE}"
echo "wrote ${ARCHIVE}"

@ -2,41 +2,6 @@
<html lang="en"> <html lang="en">
<body style="margin: 0px;"> <body style="margin: 0px;">
<script>
// --- Web Audio autoplay unlock -----------------------------------------
// Browsers (Chrome, Safari, mobile Firefox) create every AudioContext in a
// "suspended" state and refuse to start it until the user interacts with
// the page. Bevy -> rodio -> cpal build their AudioContext at startup, long
// before any gesture, so it stays suspended and no sound is ever produced
// ("AudioContext was prevented from starting automatically" in the console).
//
// We wrap the AudioContext constructor to remember every context that gets
// created, then resume them all on the first real user gesture. This must
// run BEFORE the wasm module loads so the wrapper is in place when cpal
// constructs its context.
(function () {
const Native = window.AudioContext || window.webkitAudioContext;
if (!Native) return;
const live = new Set();
function Wrapped(...args) {
const ctx = new Native(...args);
live.add(ctx);
return ctx;
}
Wrapped.prototype = Native.prototype;
window.AudioContext = Wrapped;
window.webkitAudioContext = Wrapped;
function resumeAll() {
for (const ctx of live) {
if (ctx.state === 'suspended') ctx.resume().catch(() => {});
}
}
for (const ev of ['pointerdown', 'mousedown', 'keydown', 'touchstart']) {
window.addEventListener(ev, resumeAll, { capture: true, passive: true });
}
})();
</script>
<script type="module"> <script type="module">
import init from './bin.js' import init from './bin.js'

Loading…
Cancel
Save