Compare commits

..

16 Commits

Author SHA1 Message Date
Elijah Voigt 9c4b4a79b9 Move to avian physics, stub out flappy bird (with rewind) 3 months ago
Elijah Voigt df069d0337 basic test suite for monologue parsing 3 months ago
Elijah Voigt dba46a99da Move trees debug code to separate module 3 months ago
Elijah Voigt c72ed47caa include build type in version string 3 months ago
Elijah Voigt 0df33ee4a6 yeah that works 3 months ago
Elijah Voigt 6433f7bb6a Very basic notification, need to close it when clicked 3 months ago
Elijah Voigt 5e246d21dc Spawning 10 trees at startup works 3 months ago
Elijah Voigt 4f1c634ed2 Add debug mode toggle & move some debug elements around 3 months ago
Elijah Voigt 2a1d7fe8c2 make trees debug menu look good enough 3 months ago
Elijah Voigt 145008a52d Make clippy happy 3 months ago
Elijah Voigt c24dabde16 Made the debug UI look a little less awful 3 months ago
Elijah Voigt d33460a884 fixed font color thing 3 months ago
Elijah Voigt 35cc3d1f91 Almost there with basic UI -- just need to fix a text bug 3 months ago
Elijah Voigt d17f5e70d1 Handle assigning monologues
Unfortunatly we don't start the dialog properly any more
We also panic if no trees are avaliable...
3 months ago
Elijah Voigt e54503b1b0 Fix tooltip ish 3 months ago
Elijah Voigt 0f14ed6e70 Fix special characters in mono files 3 months ago

219
Cargo.lock generated

@ -322,6 +322,38 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "avian3d"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0feaf6ed522e3e55130d820bfa2a3f3395092cbee2c986cfc1a464b60b50b4ca"
dependencies = [
"avian_derive",
"bevy",
"bevy_heavy",
"bevy_math",
"bevy_transform_interpolation",
"bitflags 2.9.1",
"derive_more",
"itertools 0.13.0",
"nalgebra",
"parry3d",
"parry3d-f64",
"thread_local",
]
[[package]]
name = "avian_derive"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12b257f601a1535e0d4a7a7796f535e3a13de62fd422b16dff7c14d27f0d4048"
dependencies = [
"proc-macro-error2",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.21.7" version = "0.21.7"
@ -707,6 +739,16 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "bevy_heavy"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34ccc861fea2ff58c67f4df119512e204050bd7631a3a9c65e1a5e9d162cce28"
dependencies = [
"bevy_math",
"bevy_reflect",
]
[[package]] [[package]]
name = "bevy_image" name = "bevy_image"
version = "0.16.1" version = "0.16.1"
@ -979,19 +1021,6 @@ version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df7370d0e46b60e071917711d0860721f5347bc958bf325975ae6913a5dfcf01" checksum = "df7370d0e46b60e071917711d0860721f5347bc958bf325975ae6913a5dfcf01"
[[package]]
name = "bevy_rapier3d"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdf74109573c2c82b05b217cb6101f7e71e6c53ad622aed6c370cc5783c59eb8"
dependencies = [
"bevy",
"bitflags 2.9.1",
"log",
"nalgebra",
"rapier3d",
]
[[package]] [[package]]
name = "bevy_reflect" name = "bevy_reflect"
version = "0.16.1" version = "0.16.1"
@ -1260,6 +1289,15 @@ dependencies = [
"thiserror 2.0.12", "thiserror 2.0.12",
] ]
[[package]]
name = "bevy_transform_interpolation"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72c7c6c4e6a3d5415b3a29a17bd20c17cd0e2f068b96b24e263316d58d5346ea"
dependencies = [
"bevy",
]
[[package]] [[package]]
name = "bevy_ui" name = "bevy_ui"
version = "0.16.1" version = "0.16.1"
@ -1821,19 +1859,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
[[package]]
name = "crossbeam"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-epoch",
"crossbeam-queue",
"crossbeam-utils",
]
[[package]] [[package]]
name = "crossbeam-channel" name = "crossbeam-channel"
version = "0.5.15" version = "0.5.15"
@ -2223,10 +2248,11 @@ dependencies = [
name = "games" name = "games"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"avian3d",
"bevy", "bevy",
"bevy_rapier3d",
"chrono", "chrono",
"itertools 0.13.0", "indoc",
"itertools 0.14.0",
"lipsum", "lipsum",
"rand", "rand",
"serde", "serde",
@ -2583,6 +2609,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "indoc"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
[[package]] [[package]]
name = "inflections" name = "inflections"
version = "1.1.1" version = "1.1.1"
@ -3450,15 +3482,6 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "ordered-float"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2c1f9f56e534ac6a9b8a4600bdf0f530fb393b5f393e7b4d03489c3cf0c3f01"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "overload" name = "overload"
version = "0.1.1" version = "0.1.1"
@ -3505,27 +3528,56 @@ dependencies = [
[[package]] [[package]]
name = "parry3d" name = "parry3d"
version = "0.20.2" version = "0.17.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec55ce6f725367f8149f750575e79a8879d71b7257c02273259f9375822821f" checksum = "6aeb9659a05b1783fb2e9bc94f48225ae5b40817eb45b62569c0e4dd767a6e51"
dependencies = [ dependencies = [
"approx", "approx",
"arrayvec", "arrayvec",
"bitflags 2.9.1", "bitflags 2.9.1",
"downcast-rs 2.0.1", "downcast-rs 1.2.1",
"either", "either",
"ena", "ena",
"hashbrown",
"log", "log",
"nalgebra", "nalgebra",
"num-derive", "num-derive",
"num-traits", "num-traits",
"ordered-float 5.0.0", "ordered-float",
"rayon",
"rstar", "rstar",
"rustc-hash 2.1.1",
"simba", "simba",
"slab", "slab",
"smallvec",
"spade", "spade",
"thiserror 2.0.12", "thiserror 1.0.69",
]
[[package]]
name = "parry3d-f64"
version = "0.17.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4484c8ad93ff03c0e57aa1a4f3ff5406ab6301a1eb838ef6dea90e94f00a6c7"
dependencies = [
"approx",
"arrayvec",
"bitflags 2.9.1",
"downcast-rs 1.2.1",
"either",
"ena",
"log",
"nalgebra",
"num-derive",
"num-traits",
"ordered-float",
"rayon",
"rstar",
"rustc-hash 2.1.1",
"simba",
"slab",
"smallvec",
"spade",
"thiserror 1.0.69",
] ]
[[package]] [[package]]
@ -3682,33 +3734,42 @@ dependencies = [
] ]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro-error-attr2"
version = "1.0.95" version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
dependencies = [ dependencies = [
"unicode-ident", "proc-macro2",
"quote",
] ]
[[package]] [[package]]
name = "profiling" name = "proc-macro-error2"
version = "1.0.17" version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
dependencies = [ dependencies = [
"profiling-procmacros", "proc-macro-error-attr2",
"proc-macro2",
"quote",
"syn",
] ]
[[package]] [[package]]
name = "profiling-procmacros" name = "proc-macro2"
version = "1.0.17" version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [ dependencies = [
"quote", "unicode-ident",
"syn",
] ]
[[package]]
name = "profiling"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
[[package]] [[package]]
name = "quick-xml" name = "quick-xml"
version = "0.37.5" version = "0.37.5"
@ -3791,30 +3852,6 @@ 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 = "f60fcc7d6849342eff22c4350c8b9a989ee8ceabc4b481253e8946b9fe83d684" checksum = "f60fcc7d6849342eff22c4350c8b9a989ee8ceabc4b481253e8946b9fe83d684"
[[package]]
name = "rapier3d"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a35ec3d01c4f918675411442024a1fbfb7eafdd878a6e82479ff6e461a9092fc"
dependencies = [
"approx",
"arrayvec",
"bit-vec 0.8.0",
"bitflags 2.9.1",
"crossbeam",
"downcast-rs 2.0.1",
"log",
"nalgebra",
"num-derive",
"num-traits",
"ordered-float 5.0.0",
"parry3d",
"profiling",
"rustc-hash 2.1.1",
"simba",
"thiserror 2.0.12",
]
[[package]] [[package]]
name = "raw-window-handle" name = "raw-window-handle"
version = "0.6.2" version = "0.6.2"
@ -3827,6 +3864,26 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
[[package]]
name = "rayon"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]] [[package]]
name = "read-fonts" name = "read-fonts"
version = "0.29.3" version = "0.29.3"
@ -5000,7 +5057,7 @@ dependencies = [
"ndk-sys 0.5.0+25.2.9519653", "ndk-sys 0.5.0+25.2.9519653",
"objc", "objc",
"once_cell", "once_cell",
"ordered-float 4.6.0", "ordered-float",
"parking_lot", "parking_lot",
"profiling", "profiling",
"range-alloc", "range-alloc",

@ -10,8 +10,8 @@ thiserror = "2.0.12"
version = "1.0.219" version = "1.0.219"
features = ["derive"] features = ["derive"]
[dependencies.bevy_rapier3d] [dependencies.avian3d]
version = "0.30.0" version = "0.3.1"
[dependencies.bevy] [dependencies.bevy]
version = "0.16.1" version = "0.16.1"
@ -21,6 +21,7 @@ features = ["wayland", "dynamic_linking"]
lipsum = "*" lipsum = "*"
rand = "*" rand = "*"
itertools = "*" itertools = "*"
indoc = "*"
[build-dependencies] [build-dependencies]
walkdir = "*" walkdir = "*"

@ -39,3 +39,6 @@ web/trees/serve: web/trees
itch.io/trees: web/trees itch.io/trees: web/trees
butler push dist/trees popgame/trees:html5 --userversion=$(VERSION) butler push dist/trees popgame/trees:html5 --userversion=$(VERSION)
clean:
rm -rf dist/trees

BIN
assets/flappy/bevy.png (Stored with Git LFS)

Binary file not shown.

@ -1 +1 @@
Oooh thats brisk! Oooh that's brisk!

@ -1,3 +1,3 @@
Everyone seems to be slowing down a bit. Everyone seems to be slowing down a bit.
--- ---
Good thing Ill stay green forever! Good thing I'll stay green forever!

@ -1,5 +1,6 @@
A: Im getting sleepy... is it time? A: I'm getting sleepy... is it time?
--- ---
B: Well since youre doing it, I suppose Ill follow suit. Who would I talk to through the winter with you hibernating, anyway? B: Well since you're doing it, I suppose I'll follow suit.
Who would I talk to through the winter with you hibernating, anyway?
--- ---
A: Wwwwwwwwww (like Zzzzz but for trees) A: Wwwwwwwwww (like 'Zzzzz' but for trees)

@ -2,7 +2,7 @@ A: Hey, are you awake yet?
--- ---
B: Wwwwwwwww B: Wwwwwwwww
--- ---
A: Wake up! Its warmer and there is food! A: Wake up! It's warmer and there is food!
--- ---
B: Www... B: Www...
--- ---

@ -1 +1 @@
Livin the dream. Livin' the dream.

@ -4,4 +4,4 @@ No, just me?
--- ---
Ok... Ok...
--- ---
Ill just stay put. I'll just stay put.

@ -1,10 +1,10 @@
A: I dont feel so good. A: I don't feel so good.
--- ---
Its that infested sort of feeling. It's that infested sort of feeling.
B: Oh no, they dont feel so good. B: Oh no, they don't feel so good.
--- ---
Its that infested sort of feeling. It's that infested sort of feeling.
C: Hm... sounds like... C: Hm... sounds like...
--- ---

@ -1 +1 @@
The heat is different this time... something doesnt feel right. The heat is different this time... something doesn't feel right.

@ -1,3 +1,4 @@
ugh... I am so lazy. I should probably try to just make one leaf or something, right? I mean, other trees keep their leaves in the winter. ugh... I am so lazy. I should probably try to just make one leaf or something, right?
I mean, other trees keep their leaves in the winter.
--- ---
Ill do it tomorrow. I'll do it tomorrow.

@ -25,6 +25,13 @@ fn write_version_file() {
.stdout, .stdout,
) )
.expect("Read stdout from git sha command"); .expect("Read stdout from git sha command");
let git_sha = git_sha.trim();
// Determine the build type based on cfg flags
#[cfg(debug_assertions)]
let build_type = "debug";
#[cfg(not(debug_assertions))]
let build_type = "release";
// If the workspace is dirty or clean // If the workspace is dirty or clean
let clean = Command::new("git") let clean = Command::new("git")
@ -37,9 +44,9 @@ fn write_version_file() {
let mut file = File::create("VERSION").expect("Create VERSION file"); let mut file = File::create("VERSION").expect("Create VERSION file");
if clean { if clean {
write!(file, "0.0.0-{now}+{}", git_sha.trim()).expect("Write version to VERSION file"); write!(file, "0.0.0-{now}+{git_sha}-{build_type}").expect("Write version to VERSION file");
} else { } else {
write!(file, "0.0.0-{now}+{}-dirty", git_sha.trim()) write!(file, "0.0.0-{now}+{git_sha}-{build_type}-dirty")
.expect("Write version to VERSION file"); .expect("Write version to VERSION file");
} }
} }

@ -0,0 +1,55 @@
use std::f32::consts::PI;
use games::*;
fn main() {
App::new()
.add_plugins(BaseGamePlugin::default())
.add_systems(Startup, init_image)
.run();
}
fn init_image(
mut commands: Commands,
server: Res<AssetServer>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
mut std_materials: ResMut<Assets<StandardMaterial>>,
) {
// 2d
{
let texture = MeshMaterial2d(materials.add(ColorMaterial {
color: WHITE.into(),
alpha_mode: AlphaMode2d::Opaque,
texture: Some(server.load("flappy/bevy.png")),
..default()
}));
let mesh = Mesh2d(meshes.add(Rectangle::from_size(Vec2::splat(10.0))));
// opaque
// Each sprite should be square with the transparent parts being completely black
// The blue sprite should be on top with the white and green one behind it
commands.spawn((Name::new("2D Example"),mesh, texture));
}
// 3d
{
let material = MeshMaterial3d(std_materials.add(StandardMaterial {
base_color_texture: Some(server.load("flappy/bevy.png")),
base_color: WHITE.with_alpha(0.9).into(),
alpha_mode: AlphaMode::Blend,
..default()
}));
let mesh = Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(1.0))));
let name = Name::new("3D example");
let t = Transform::from_xyz(0.0, 0.0, -10.0).with_rotation(Quat::from_rotation_x(PI / 2.0));
commands.spawn((name, mesh, material, t));
}
}

@ -10,7 +10,7 @@ use rand::*;
fn main() { fn main() {
let mut app = App::new(); let mut app = App::new();
app.add_plugins(BaseGamePlugin::default()) app.add_plugins(BaseGamePlugin::default())
.add_systems(Startup, (setup_list, setup_nav_tree)) .add_systems(Startup, (setup_list, setup_nav_tree, setup_button))
.add_systems( .add_systems(
Update, Update,
( (
@ -24,18 +24,16 @@ fn main() {
fn setup_list(mut commands: Commands) { fn setup_list(mut commands: Commands) {
// Scrolling list // Scrolling list
commands commands
.spawn(( .spawn((Node {
Node {
flex_direction: FlexDirection::Column, flex_direction: FlexDirection::Column,
// If height is not set, we need both align_self: Stetch and overflow: scroll() // If height is not set, we need both align_self: Stetch and overflow: scroll()
height: Val::Percent(50.0), height: Val::Percent(50.0),
// align_self: AlignSelf::Stretch, // align_self: AlignSelf::Stretch,
overflow: Overflow::scroll(), overflow: Overflow::scroll(),
..default() ..default()
}, },))
BackgroundColor(RED.into()),
))
.with_children(|parent| { .with_children(|parent| {
parent.spawn(Text("This scrolls...".into()));
// List items // List items
(0..250).for_each(|i| { (0..250).for_each(|i| {
parent.spawn(Text(format!("Item {i}"))); parent.spawn(Text(format!("Item {i}")));
@ -44,13 +42,25 @@ fn setup_list(mut commands: Commands) {
.observe(scroll); .observe(scroll);
} }
fn setup_button(mut commands: Commands) {
// Spawn a random button at the center of the screen for testing
commands.spawn((
Node {
align_self: AlignSelf::Center,
justify_self: JustifySelf::Center,
..default()
},
Button,
children![Text("Hello World".into()),],
));
}
fn setup_nav_tree(mut commands: Commands) { fn setup_nav_tree(mut commands: Commands) {
// Nav Tree // Nav Tree
commands commands
.spawn(( .spawn((
Text::new("+"),
Node { Node {
align_self: AlignSelf::Start, top: Val::Percent(25.0),
justify_self: JustifySelf::End, justify_self: JustifySelf::End,
min_width: Val::Px(25.0), min_width: Val::Px(25.0),
min_height: Val::Px(25.0), min_height: Val::Px(25.0),
@ -58,7 +68,8 @@ fn setup_nav_tree(mut commands: Commands) {
justify_content: JustifyContent::Center, justify_content: JustifyContent::Center,
..default() ..default()
}, },
BackgroundColor(RED.into()), Button,
children![Text::new("+Menu"),],
)) ))
.with_children(|parent| { .with_children(|parent| {
parent parent
@ -69,27 +80,22 @@ fn setup_nav_tree(mut commands: Commands) {
position_type: PositionType::Absolute, position_type: PositionType::Absolute,
flex_direction: FlexDirection::Column, flex_direction: FlexDirection::Column,
right: Val::Percent(100.0), right: Val::Percent(100.0),
width: Val::Auto,
..default() ..default()
}, },
Visibility::Hidden,
BackgroundColor(ORANGE.into()),
)) ))
.with_children(|parent| { .with_children(|parent| {
(0..10).for_each(|_| { (0..5).for_each(|_| {
let title: String = lipsum_title_with_rng(thread_rng()) let title: String = lipsum_title_with_rng(thread_rng())
.split_whitespace() .split_whitespace()
.take(2) .take(2)
.intersperse(" ") .intersperse(" ")
.collect(); .collect();
parent.spawn(( parent.spawn((
Text::new(title), children![Text::new(title),],
Node { Node {
width: Val::Auto, width: Val::Auto,
margin: UiRect::all(Val::Px(5.0)),
..default() ..default()
}, },
BackgroundColor(ORANGE_RED.into()),
Button, Button,
)); ));
}); });
@ -98,22 +104,22 @@ fn setup_nav_tree(mut commands: Commands) {
parent.spawn(( parent.spawn((
Name::new("Preview"), Name::new("Preview"),
NavState::default(), NavState::default(),
Text::new(lipsum_with_rng(thread_rng(), 50)), children![Text::new(lipsum_with_rng(thread_rng(), 50)),],
Node { Node {
position_type: PositionType::Absolute, position_type: PositionType::Absolute,
right: Val::Percent(100.0), right: Val::Percent(100.0),
width: Val::Percent(100.0),
max_height: Val::Percent(100.0),
..default() ..default()
}, },
BackgroundColor(YELLOW.into()),
Visibility::Hidden,
)); ));
}); });
}); });
} }
// When you pointer over the '+' make the entire menu visible // When you pointer over the '+' make the entire menu visible
fn hide_menu(mut nodes: Query<(Entity, &mut Visibility, &NavState), Changed<NavState>>) { fn hide_menu(mut nodes: Query<(&mut Visibility, &NavState), Changed<NavState>>) {
nodes.iter_mut().for_each(|(e, mut v, n)| { nodes.iter_mut().for_each(|(mut v, n)| {
*v = match n { *v = match n {
NavState::Open => Visibility::Inherited, NavState::Open => Visibility::Inherited,
NavState::Closed => Visibility::Hidden, NavState::Closed => Visibility::Hidden,

@ -25,7 +25,7 @@ impl Plugin for BaseGamePlugin {
})) }))
.add_plugins(DebuggingPlugin) .add_plugins(DebuggingPlugin)
.add_plugins(MeshPickingPlugin) .add_plugins(MeshPickingPlugin)
.add_plugins(RapierPhysicsPlugin::<NoUserData>::default()) .add_plugins(PhysicsPlugins::default())
.add_plugins(LoadingPlugin) .add_plugins(LoadingPlugin)
.add_plugins(BaseUiPlugin) .add_plugins(BaseUiPlugin)
.add_systems(Startup, setup_camera); .add_systems(Startup, setup_camera);

@ -0,0 +1,36 @@
use games::*;
fn main() {
App::new()
.add_plugins(BaseGamePlugin { name: "flappy bird (with rewind)".into() })
.add_systems(Startup, init_bird)
.run();
}
fn init_bird(
mut commands: Commands,
server: Res<AssetServer>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let material = MeshMaterial3d(materials.add(StandardMaterial {
base_color_texture: Some(server.load("flappy/bevy.png")),
base_color: WHITE.with_alpha(0.9).into(),
alpha_mode: AlphaMode::Blend,
..default()
}));
let mesh = Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(1.0))));
let name = Name::new("bird");
let t = Transform::from_xyz(0.0, 0.0, -10.0).with_rotation(Quat::from_rotation_x(PI / 2.0));
let mass = (
RigidBody::Dynamic,
Collider::capsule(1.0, 1.0),
Mass(5.0),
);
commands.spawn((name, mesh, material, mass, t));
}

@ -0,0 +1,415 @@
use super::*;
pub(crate) struct TreesDebugPlugin;
impl Plugin for TreesDebugPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
Startup,
init_debug_ui,
).add_systems(
Update,
(
(
spawn_debug_buttons.run_if(on_event::<AssetEvent<Monologue>>),
),
(
monologue_asset_tooltip
.run_if(on_event::<Pointer<Over>>.or(on_event::<Pointer<Out>>)),
hide_menu.run_if(any_component_changed::<NavState>),
clear_monologue.run_if(any_component_changed::<NavState>),
control_menu.run_if(on_event::<Pointer<Over>>.or(on_event::<Pointer<Out>>)),
delete_tree.run_if(on_event::<Pointer<Click>>),
drag_tree.run_if(on_event::<Pointer<Drag>>),
).run_if(in_state(DebuggingState::On)),
)
).add_observer(add_dialog_option);
}
}
#[derive(Component)]
struct MonologuesContainer;
#[derive(Component)]
struct MonologuesList;
#[derive(Component)]
struct MonologuePreview;
fn drag_tree(
mut events: EventReader<Pointer<Drag>>,
state: Res<State<DebuggingState>>,
mut query: Query<&mut Transform, With<Tree>>,
camera: Single<(&Camera, &GlobalTransform), With<Camera>>,
window: Single<&Window>,
) {
debug_assert_eq!(*state.get(), DebuggingState::On);
events.read().for_each(|event| {
if let Ok(mut t) = query.get_mut(event.target)
{
let world_position = window
.cursor_position()
.and_then(|cursor| camera.0.viewport_to_world(camera.1, cursor).ok())
.map(|ray| {
// Compute ray's distance to entity
let distance = ray
.intersect_plane(t.translation, InfinitePlane3d::new(t.up()))
.unwrap();
ray.get_point(distance)
});
t.translation = world_position.unwrap();
}
});
}
/// Panel for selecting which monologue tree to spawn
fn init_debug_ui(mut commands: Commands) {
let mut monologue_button = None;
commands
.spawn((
Node {
top: Val::Px(0.0),
left: Val::Px(0.0),
flex_direction: FlexDirection::Row,
..default()
},
DebuggingState::On,
))
.with_children(|parent| {
monologue_button = Some(
parent
.spawn((
Name::new("Monologue Assignment Menu"),
children![Text::new("+Monologue"),],
Button,
Node {
min_width: Val::Px(25.0),
min_height: Val::Px(25.0),
..default()
},
DebuggingState::On,
MonologuesContainer,
))
.id(),
);
parent
.spawn((
Name::new("Tree Planter"),
children![Text::new("+Tree"),],
Node {
min_width: Val::Px(25.0),
min_height: Val::Px(25.0),
..default()
},
DebuggingState::On,
MonologuesContainer,
Button,
))
.observe(spawn_tree);
});
commands
.spawn((
NavParent(monologue_button.unwrap()),
NavState::default(),
DebuggingState::On,
Name::new("Container"),
Node {
flex_direction: FlexDirection::Row,
top: Val::Px(25.0),
height: Val::Percent(90.0),
width: Val::Percent(80.0),
..default()
},
))
.with_children(|parent| {
parent
.spawn((
Name::new("Buttons"),
Node {
flex_direction: FlexDirection::Column,
height: Val::Percent(100.0),
width: Val::Percent(40.0),
overflow: Overflow::scroll_y(),
..default()
},
ScrollPosition::default(),
MonologuesList,
))
.observe(scroll);
parent.spawn((
Name::new("Preview"),
MonologuePreview,
NavParent(monologue_button.unwrap()),
NavState::default(),
Node {
flex_direction: FlexDirection::Column,
padding: UiRect::all(Val::Px(10.0)),
overflow: Overflow::scroll_y(),
width: Val::Percent(60.0),
..default()
},
));
});
}
// When you pointer goes off of the '+' or any of it's children make the entire menu invisible
fn control_menu(
mut over_events: EventReader<Pointer<Over>>,
mut out_events: EventReader<Pointer<Out>>,
nav_children: Query<&NavParent>,
children: Query<&ChildOf>,
nav_parents: Query<&NavChildren>,
parents: Query<&Children>,
mut nav: Query<&mut NavState>,
hover_map: Res<HoverMap>,
) {
over_events.read().for_each(|over| {
let root = nav_children.root_ancestor(over.target);
nav_parents.iter_descendants(root).for_each(|child| {
if let Ok(mut n) = nav.get_mut(child) {
*n = NavState::Open;
}
});
});
// Gives us the enities covered by the HoverMap
let is_hovered: Vec<&Entity> = hover_map
.iter()
.flat_map(|(_, submap)| {
let x: Vec<&Entity> = submap.iter().map(|(node, _)| node).collect();
x
})
.collect();
// For all pointer out events
out_events.read().for_each(|out| {
// If a relative of out.target is hovered, do nothing
// Otherwise set to closed
let root = children.root_ancestor(out.target);
let tree_still_hovered = parents
.iter_descendants(root)
.any(|child| is_hovered.contains(&&child));
if !tree_still_hovered {
if let Ok(mut n) = nav.get_mut(root) {
*n = NavState::Closed;
}
parents.iter_descendants(root).for_each(|child| {
if let Ok(mut n) = nav.get_mut(child) {
*n = NavState::Closed;
}
})
}
})
}
/// When a (Text) dialog option is added:
/// 1. Add the Button component
/// 2. Change the color to Orange
/// 3. Add observers for click (select) and hover (change color)
fn add_dialog_option(trigger: Trigger<OnAdd, DialogOption>, mut commands: Commands) {
commands
.entity(trigger.target())
.insert(Button)
.insert(Node {
width: Val::Percent(100.0),
..default()
})
.insert(TextLayout::new_with_justify(JustifyText::Center))
.insert(TextColor(ORANGE.into()))
.observe(choose_dialog_option)
.observe(hover_dialog_option_over)
.observe(hover_dialog_option_out);
}
fn assign_monologue_event(
trigger: Trigger<Pointer<Click>>,
mut events: EventWriter<AssignMonologue>,
monologues: Query<&TreeMonologue>,
) {
let TreeMonologue(handle) = monologues.get(trigger.target()).unwrap();
events.write(AssignMonologue(handle.clone()));
}
/// Observer for the "Plant a new tree" button in the debug UI
fn spawn_tree(_trigger: Trigger<Pointer<Click>>, mut events: EventWriter<PlantTree>) {
events.write(PlantTree(None));
}
fn clear_monologue(
mut nodes: Query<
(Entity, &NavState),
(Changed<NavState>, With<MonologuePreview>, Without<Text>),
>,
mut commands: Commands,
) {
nodes.iter_mut().for_each(|(e, n)| {
if matches!(n, NavState::Closed) {
commands.entity(e).despawn_related::<Children>();
}
});
}
// When you pointer over the '+' make the entire menu visible
fn hide_menu(mut nodes: Query<(&mut Visibility, &NavState), Changed<NavState>>) {
nodes.iter_mut().for_each(|(mut v, n)| {
*v = match n {
NavState::Open => Visibility::Inherited,
NavState::Closed => Visibility::Hidden,
};
});
}
fn delete_tree(mut events: EventReader<Pointer<Click>>, mut commands: Commands, query: Query<Entity, With<Tree>>) {
events.read().for_each(|event| {
if matches!(event.event.button, PointerButton::Middle) && query.contains(event.target) {
debug!("Middle Click -> Despawning {}", event.target);
commands.entity(event.target).despawn();
}
})
}
/// Add the "script: path/to/file.mono" tooltip info
fn monologue_asset_tooltip(
mut over_events: EventReader<Pointer<Over>>,
mut out_events: EventReader<Pointer<Out>>,
mut tooltip: ResMut<ToolTip>,
trees: Query<(&Tree, Option<&TreeMonologue>)>,
) {
out_events
.read()
.filter_map(|Pointer { target, .. }| trees.contains(*target).then_some(*target))
.for_each(|_| {
tooltip.remove("Script");
});
over_events
.read()
.filter_map(|Pointer { target, .. }| trees.contains(*target).then_some(*target))
.for_each(|e| match trees.get(e) {
Ok((_tree, Some(TreeMonologue(handle)))) => match handle.path() {
Some(p) => tooltip.insert("Script", format!("{p}")),
None => tooltip.insert("Script", "A".into()),
},
Ok((_tree, None)) => {
tooltip.insert("Script", "N/A".into());
}
_ => (),
});
}
/// When a dialog option is chosen (clicked on) we do the following:
fn choose_dialog_option(
trigger: Trigger<Pointer<Click>>,
mut dialog_events: EventWriter<DialogEvent>,
mut commands: Commands,
texts: Query<&Text>,
options: Query<Entity, With<DialogOption>>,
dialog_box: Single<Entity, With<DialogBox>>,
) {
debug!("Choosing dialog {:?}", trigger.target());
debug!("Despawning dialog options");
options.iter().for_each(|e| {
commands.entity(e).despawn();
});
debug!("Inserting dialog line");
if let Ok(t) = texts.get(trigger.target()) {
commands.entity(*dialog_box).with_children(|parent| {
parent.spawn((t.clone(), DialogLine));
});
}
// trigger the next dialog line
dialog_events.write(DialogEvent::NextBatch);
}
fn preview_monologue(
trigger: Trigger<Pointer<Over>>,
container: Single<Entity, (With<MonologuePreview>, Without<Button>, Without<Text>)>,
tree_monologue: Query<&TreeMonologue, With<Button>>,
monologues: Res<Assets<Monologue>>,
mut commands: Commands,
) {
// Get the handle for this button's monologuie
if let Ok(TreeMonologue(handle)) = tree_monologue.get(trigger.target()) {
// Get the monologue data
if let Some(monologue) = monologues.get(handle) {
commands.entity(*container).despawn_related::<Children>();
// Spawn the monologue
commands.entity(*container).with_children(|parent| {
let mut i = 0;
while let Some(batch) = monologue.get(i) {
parent.spawn((Text::new("---"), MonologuePreview));
debug!("---");
for (n, item) in batch.lines.iter().enumerate() {
parent.spawn((Text::new(format!("{i}.{n}: {item}")), MonologuePreview));
debug!("{i}.{n}: {item}");
}
// TODO: Just implement iter_batches or something
i += 1;
}
parent.spawn((Text::new("---"), MonologuePreview));
debug!("---");
});
}
}
}
fn spawn_debug_buttons(
mut events: EventReader<AssetEvent<Monologue>>,
mut commands: Commands,
container: Single<Entity, (With<MonologuesList>, Without<Button>)>,
server: Res<AssetServer>,
) {
debug_assert!(
!events.is_empty(),
"Should only spawn buttons when monologues are loaded"
);
events.read().for_each(|event| {
// If this is an "Asset was loaded" event
// These are just asset events for monologues so no need to check the asset type
if let AssetEvent::LoadedWithDependencies { id } = event {
// Get the handle from the asset ID
let handle = server.get_id_handle(*id).unwrap();
// Spawn a button in the box containing buttons
commands.entity(*container).with_children(|parent| {
parent
.spawn((
Button,
children![
Text::new(
handle
.path()
.unwrap()
.to_string()
.trim_prefix("trees/")
.trim_suffix(".mono"),
),
TextLayout::new(JustifyText::Left, LineBreak::WordBoundary),
],
TreeMonologue(handle.clone()),
MonologuesList,
))
.observe(assign_monologue_event)
.observe(preview_monologue);
});
}
});
}

@ -4,10 +4,12 @@
#![feature(trim_prefix_suffix)] #![feature(trim_prefix_suffix)]
mod mono; mod mono;
mod debug;
use bevy::{picking::hover::HoverMap, platform::hash::RandomState}; use bevy::{picking::hover::HoverMap, platform::hash::RandomState};
use games::*; use games::*;
use mono::*; use mono::*;
use debug::*;
use std::hash::BuildHasher; use std::hash::BuildHasher;
fn main() { fn main() {
@ -16,7 +18,10 @@ fn main() {
name: "trees".into(), name: "trees".into(),
}) })
.add_plugins(MonologueAssetsPlugin) .add_plugins(MonologueAssetsPlugin)
.add_plugins(TreesDebugPlugin)
.add_event::<DialogEvent>() .add_event::<DialogEvent>()
.add_event::<PlantTree>()
.add_event::<AssignMonologue>()
.init_state::<DialogState>() .init_state::<DialogState>()
.insert_resource(ClearColor(WHITE.into())) .insert_resource(ClearColor(WHITE.into()))
.add_systems( .add_systems(
@ -24,11 +29,12 @@ fn main() {
( (
init_trees, init_trees,
init_ui, init_ui,
init_debug_ui,
load_monologues, load_monologues,
position_camera.after(setup_camera), position_camera.after(setup_camera),
), ),
) )
// When we're done loading, plant forest
.add_systems(OnEnter(LoadingState::Idle), plant_forest.run_if(run_once))
.add_systems( .add_systems(
Update, Update,
( (
@ -42,24 +48,18 @@ fn main() {
.run_if(in_state(DebuggingState::Off)) .run_if(in_state(DebuggingState::Off))
.run_if(in_state(DialogState::Idle)) .run_if(in_state(DialogState::Idle))
.run_if(on_event::<Pointer<Click>>), .run_if(on_event::<Pointer<Click>>),
spawn_debug_buttons.run_if(on_event::<AssetEvent<Monologue>>), handle_plant_tree.run_if(on_event::<PlantTree>),
assign_monologue_to_tree
.run_if(on_event::<AssignMonologue>)
.after(handle_plant_tree),
dialog_engine.run_if(on_event::<DialogEvent>), dialog_engine.run_if(on_event::<DialogEvent>),
auto_scroll.run_if(any_component_added::<DialogOption>), auto_scroll.run_if(any_component_added::<DialogOption>),
dialog_box_visibility.run_if(state_changed::<DialogState>), dialog_box_visibility.run_if(state_changed::<DialogState>),
monologue_asset_tooltip
.run_if(on_event::<Pointer<Over>>.or(on_event::<Pointer<Out>>)),
scale_window.run_if(on_event::<WindowResized>), scale_window.run_if(on_event::<WindowResized>),
hide_menu.run_if(any_component_changed::<NavState>),
clear_monologue.run_if(any_component_changed::<NavState>),
control_menu.run_if(on_event::<Pointer<Over>>.or(on_event::<Pointer<Out>>)),
), ),
) )
.add_observer(add_dialog_option)
.add_observer(add_tree_monologue) .add_observer(add_tree_monologue)
.add_observer(remove_tree_monologue) .add_observer(remove_tree_monologue)
.add_observer(populate_tree)
.add_observer(show_monologue_list)
.add_observer(hide_monologue_list)
.run(); .run();
} }
@ -108,78 +108,6 @@ fn init_ui(mut commands: Commands) {
.observe(hover_dialog_box_out); .observe(hover_dialog_box_out);
} }
#[derive(Component)]
struct MonologuesContainer;
#[derive(Component)]
struct MonologuesList;
#[derive(Component)]
struct MonologuePreview;
/// Panel for selecting which monologue tree to spawn
fn init_debug_ui(mut commands: Commands) {
let button = commands
.spawn((
Name::new("Monologue Assignment Menu"),
Text::new("+ Monologue"),
Node {
align_self: AlignSelf::Start,
justify_self: JustifySelf::Start,
min_width: Val::Px(25.0),
min_height: Val::Px(25.0),
..default()
},
BackgroundColor(RED.into()),
DebuggingState::On,
MonologuesContainer,
))
.id();
commands
.spawn((
NavParent(button),
NavState::default(),
Name::new("Container"),
Node {
flex_direction: FlexDirection::Row,
top: Val::Px(25.0),
height: Val::Percent(90.0),
width: Val::Percent(80.0),
..default()
},
BackgroundColor(BLACK.into()),
)).with_children(|parent| {
parent.spawn((
Name::new("Buttons"),
Node {
flex_direction: FlexDirection::Column,
height: Val::Percent(100.0),
width: Val::Percent(40.0),
overflow: Overflow::scroll_y(),
..default()
},
ScrollPosition::default(),
BackgroundColor(ORANGE.into()),
MonologuesList,
)).observe(scroll);
parent.spawn((
Name::new("Preview"),
MonologuePreview,
NavParent(button),
NavState::default(),
Node {
flex_direction: FlexDirection::Column,
padding: UiRect::all(Val::Px(10.0)),
overflow: Overflow::scroll_y(),
width: Val::Percent(60.0),
..default()
},
BackgroundColor(ORANGE_RED.into()),
));
});
}
fn hover_dialog_box_over( fn hover_dialog_box_over(
trigger: Trigger<Pointer<Over>>, trigger: Trigger<Pointer<Over>>,
mut query: Query<&mut BackgroundColor, With<DialogBox>>, mut query: Query<&mut BackgroundColor, With<DialogBox>>,
@ -252,14 +180,14 @@ enum DialogState {
None, None,
} }
/// Start dialog, will expand later with handle to monologue asset /// Start dialog
fn start_dialog( fn start_dialog(
mut click_events: EventReader<Pointer<Click>>, mut click_events: EventReader<Pointer<Click>>,
mut dialog_events: EventWriter<DialogEvent>, mut dialog_events: EventWriter<DialogEvent>,
query: Query<&TreeMonologue, With<Tree>>, query: Query<&TreeMonologue, With<Tree>>,
) { ) {
click_events.read().for_each(|event| { click_events.read().for_each(|event| {
debug!("Click event detected"); debug!("Click event detected in start dialog systme");
if let Ok(TreeMonologue(handle)) = query.get(event.target) { if let Ok(TreeMonologue(handle)) = query.get(event.target) {
debug!("Tree Monologue received, sending start dialog event"); debug!("Tree Monologue received, sending start dialog event");
dialog_events.write(DialogEvent::Start(event.target, handle.clone())); dialog_events.write(DialogEvent::Start(event.target, handle.clone()));
@ -370,33 +298,6 @@ fn dialog_engine(
}); });
} }
/// When a dialog option is chosen (clicked on) we do the following:
fn choose_dialog_option(
trigger: Trigger<Pointer<Click>>,
mut dialog_events: EventWriter<DialogEvent>,
mut commands: Commands,
texts: Query<&Text>,
options: Query<Entity, With<DialogOption>>,
dialog_box: Single<Entity, With<DialogBox>>,
) {
debug!("Choosing dialog {:?}", trigger.target());
debug!("Despawning dialog options");
options.iter().for_each(|e| {
commands.entity(e).despawn();
});
debug!("Inserting dialog line");
if let Ok(t) = texts.get(trigger.target()) {
commands.entity(*dialog_box).with_children(|parent| {
parent.spawn((t.clone(), DialogLine));
});
}
// trigger the next dialog line
dialog_events.write(DialogEvent::NextBatch);
}
fn hover_dialog_option_over( fn hover_dialog_option_over(
trigger: Trigger<Pointer<Over>>, trigger: Trigger<Pointer<Over>>,
mut query: Query<(&mut TextColor, &mut BackgroundColor)>, mut query: Query<(&mut TextColor, &mut BackgroundColor)>,
@ -417,25 +318,6 @@ fn hover_dialog_option_out(
} }
} }
/// When a (Text) dialog option is added:
/// 1. Add the Button component
/// 2. Change the color to Orange
/// 3. Add observers for click (select) and hover (change color)
fn add_dialog_option(trigger: Trigger<OnAdd, DialogOption>, mut commands: Commands) {
commands
.entity(trigger.target())
.insert(Button)
.insert(Node {
width: Val::Percent(100.0),
..default()
})
.insert(TextLayout::new_with_justify(JustifyText::Center))
.insert(TextColor(ORANGE.into()))
.observe(choose_dialog_option)
.observe(hover_dialog_option_over)
.observe(hover_dialog_option_out);
}
fn dialog_box_visibility( fn dialog_box_visibility(
state: Res<State<DialogState>>, state: Res<State<DialogState>>,
mut dialog_box: Single<&mut Visibility, With<DialogBox>>, mut dialog_box: Single<&mut Visibility, With<DialogBox>>,
@ -447,33 +329,6 @@ fn dialog_box_visibility(
}; };
} }
/// Add the "script: path/to/file.mono" tooltip info
fn monologue_asset_tooltip(
mut over_events: EventReader<Pointer<Over>>,
mut out_events: EventReader<Pointer<Out>>,
mut tooltip: ResMut<ToolTip>,
scripts: Query<&TreeMonologue>,
) {
out_events
.read()
.filter_map(|Pointer { target, .. }| scripts.contains(*target).then_some(*target))
.for_each(|_| {
tooltip.remove("Script");
});
over_events
.read()
.filter_map(|Pointer { target, .. }| scripts.contains(*target).then_some(*target))
.for_each(|e| {
if let Ok(TreeMonologue(handle)) = scripts.get(e) {
match handle.path() {
Some(p) => tooltip.insert("Script", format!("{p}")),
None => tooltip.insert("Script", "???".into()),
}
}
});
}
fn add_tree_monologue( fn add_tree_monologue(
trigger: Trigger<OnAdd, TreeMonologue>, trigger: Trigger<OnAdd, TreeMonologue>,
query: Query<&MeshMaterial3d<StandardMaterial>>, query: Query<&MeshMaterial3d<StandardMaterial>>,
@ -532,13 +387,6 @@ fn scale_window(events: EventReader<WindowResized>, mut window: Single<&mut Wind
); );
} }
fn delete_tree(trigger: Trigger<Pointer<Click>>, mut commands: Commands) {
if matches!(trigger.event.button, PointerButton::Middle) {
info!("Middle Click -> Despawning {}", trigger.target());
commands.entity(trigger.target()).despawn();
}
}
/// Load all monologues so they are in the asset store and trigger on-load events /// Load all monologues so they are in the asset store and trigger on-load events
fn load_monologues(server: ResMut<AssetServer>, mut loaded_assets: Local<Vec<Handle<Monologue>>>) { fn load_monologues(server: ResMut<AssetServer>, mut loaded_assets: Local<Vec<Handle<Monologue>>>) {
*loaded_assets = include_str!("../../../assets/trees/MONOLOGUES") *loaded_assets = include_str!("../../../assets/trees/MONOLOGUES")
@ -547,148 +395,23 @@ fn load_monologues(server: ResMut<AssetServer>, mut loaded_assets: Local<Vec<Han
.collect(); .collect();
} }
fn spawn_debug_buttons( #[derive(Event)]
mut events: EventReader<AssetEvent<Monologue>>, struct PlantTree(Option<Handle<Monologue>>);
mut commands: Commands,
container: Single<Entity, (With<MonologuesList>, Without<Button>)>,
server: Res<AssetServer>,
) {
debug_assert!(
!events.is_empty(),
"Should only spawn buttons when monologues are loaded"
);
events.read().for_each(|event| {
// If this is an "Asset was loaded" event
// These are just asset events for monologues so no need to check the asset type
if let AssetEvent::LoadedWithDependencies { id } = event {
// Get the handle from the asset ID
let handle = server.get_id_handle(*id).unwrap();
// Spawn a button in the box containing buttons
commands.entity(*container).with_children(|parent| {
parent
.spawn((
Button,
Text::new(handle.path().unwrap().to_string().trim_prefix("trees/").trim_suffix(".mono")),
TextLayout::new(JustifyText::Left, LineBreak::WordBoundary),
TreeMonologue(handle.clone()),
MonologuesList,
BackgroundColor(PINK.into()),
))
.observe(preview_monologue)
.observe(spawn_monologue_tree)
.observe(toggle_debug_button_color_over)
.observe(toggle_debug_button_color_out);
});
}
});
}
fn toggle_debug_button_color_over(
trigger: Trigger<Pointer<Over>>,
mut buttons: Query<&mut BackgroundColor, With<Button>>,
) {
if let Ok(mut bg) = buttons.get_mut(trigger.target()) {
bg.0 = RED.into();
}
}
fn toggle_debug_button_color_out( /// Plan a tree in the world
trigger: Trigger<Pointer<Out>>, /// Handles random placement, 3d model, materials, and observers
mut buttons: Query<&mut BackgroundColor, With<Button>>, fn handle_plant_tree(
) { mut events: EventReader<PlantTree>,
if let Ok(mut bg) = buttons.get_mut(trigger.target()) { mut assignments: EventWriter<AssignMonologue>,
bg.0 = PINK.into();
}
}
fn preview_monologue(
trigger: Trigger<Pointer<Over>>,
container: Single<Entity, (With<MonologuePreview>, Without<Button>, Without<Text>)>,
tree_monologue: Query<&TreeMonologue, With<Button>>,
monologues: Res<Assets<Monologue>>,
mut commands: Commands,
) {
// Get the handle for this button's monologuie
if let Ok(TreeMonologue(handle)) = tree_monologue.get(trigger.target()) {
// Get the monologue data
if let Some(monologue) = monologues.get(handle) {
commands.entity(*container).despawn_related::<Children>();
// Spawn the monologue
commands.entity(*container).with_children(|parent| {
let mut i = 0;
while let Some(batch) = monologue.get(i) {
parent.spawn((Text::new("---"), MonologuePreview));
debug!("---");
for (n, item) in batch.lines.iter().enumerate() {
parent.spawn((Text::new(format!("{i}.{n}: {item}")), MonologuePreview));
debug!("{i}.{n}: {item}");
}
// TODO: Just implement iter_batches or something
i += 1;
}
parent.spawn((Text::new("---"), MonologuePreview));
debug!("---");
});
}
}
}
fn show_monologue_list(
trigger: Trigger<Pointer<Over>>,
container: Query<Entity, With<MonologuesContainer>>,
children: Query<&Children>,
mut visibility: Query<&mut Visibility>,
) {
if let Ok(root) = container.get(trigger.target()) {
children.iter_descendants(root).for_each(|e| {
*visibility.get_mut(e).unwrap() = Visibility::Inherited;
});
}
}
fn hide_monologue_list(
trigger: Trigger<Pointer<Out>>,
container: Query<Entity, With<MonologuesContainer>>,
children: Query<&Children>,
mut visibility: Query<&mut Visibility>,
) {
if let Ok(root) = container.get(trigger.target()) {
children.iter_descendants(root).for_each(|e| {
*visibility.get_mut(e).unwrap() = Visibility::Hidden;
});
}
}
fn spawn_monologue_tree(
trigger: Trigger<Pointer<Click>>,
tree_monologues: Query<&TreeMonologue, With<Button>>,
mut commands: Commands,
) {
let tree_monologue = tree_monologues.get(trigger.target()).unwrap();
info!("Spawning monologuing tree");
commands.spawn((Tree, tree_monologue.clone()));
}
/// When a monologuing tree is added, give it a mesh, a material, and a position in space
///
/// TODO: This can be an `on_add` hook intead, just a little more clunky
fn populate_tree(
trigger: Trigger<OnAdd, TreeMonologue>,
trees: Query<Entity, With<Tree>>, trees: Query<Entity, With<Tree>>,
server: Res<AssetServer>, server: Res<AssetServer>,
mut commands: Commands, mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>, mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>, mut materials: ResMut<Assets<StandardMaterial>>,
) { ) {
if !trees.contains(trigger.target()) { events.read().for_each(|PlantTree(assignment)| {
return; debug!("Planing tree");
} let mut tree = commands.spawn(Tree);
// Generate "random" X and Y Coordinates for this tree // Generate "random" X and Y Coordinates for this tree
// 1. Take the top 2 bytes // 1. Take the top 2 bytes
@ -696,7 +419,7 @@ fn populate_tree(
// 3. Re-interpret as i8s // 3. Re-interpret as i8s
// 4. Cast to f32 // 4. Cast to f32
let transform = { let transform = {
let n = RandomState::default().hash_one(trigger.target()); let n = RandomState::default().hash_one(tree.id());
let [a, b, ..] = n.to_be_bytes(); let [a, b, ..] = n.to_be_bytes();
let x: f32 = a.cast_signed().wrapping_div(4).into(); let x: f32 = a.cast_signed().wrapping_div(4).into();
@ -709,117 +432,65 @@ fn populate_tree(
let material = MeshMaterial3d(materials.add(StandardMaterial { let material = MeshMaterial3d(materials.add(StandardMaterial {
base_color_texture: Some(server.load("trees/placeholder/tree.png")), base_color_texture: Some(server.load("trees/placeholder/tree.png")),
base_color: WHITE.into(), base_color: WHITE.with_alpha(0.9).into(),
alpha_mode: AlphaMode::Blend, alpha_mode: AlphaMode::Blend,
..default() ..default()
})); }));
let mesh = Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(1.0)))); let mesh = Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(1.0))));
debug!("Fleshing out monologuing tree"); tree.insert((mesh, material, transform));
commands if let Some(handle) = assignment {
.entity(trigger.target()) assignments.write(AssignMonologue(handle.clone()));
.insert((mesh, material, transform))
.observe(delete_tree)
.observe(drag_tree);
}
fn drag_tree(
trigger: Trigger<Pointer<Drag>>,
state: Res<State<DebuggingState>>,
mut query: Query<&mut Transform, With<Tree>>,
camera: Single<(&Camera, &GlobalTransform), With<Camera>>,
window: Single<&Window>,
) {
if *state.get() == DebuggingState::On {
if let Ok(mut t) = query.get_mut(trigger.target()) {
let world_position = window
.cursor_position()
.and_then(|cursor| camera.0.viewport_to_world(camera.1, cursor).ok())
.map(|ray| {
// Compute ray's distance to entity
let distance = ray
.intersect_plane(t.translation, InfinitePlane3d::new(t.up()))
.unwrap();
ray.get_point(distance)
});
t.translation = world_position.unwrap();
}
} }
}
// When you pointer over the '+' make the entire menu visible
fn hide_menu(
mut nodes: Query<(Entity, &mut Visibility, &NavState), Changed<NavState>>
) {
nodes.iter_mut().for_each(|(e, mut v, n)| {
*v = match n {
NavState::Open => Visibility::Inherited,
NavState::Closed => Visibility::Hidden,
};
}); });
} }
fn clear_monologue( #[derive(Event, Debug)]
mut nodes: Query<(Entity, &NavState), (Changed<NavState>, With<MonologuePreview>, Without<Text>)>, struct AssignMonologue(Handle<Monologue>);
/// Assign the given monologue to a tree
fn assign_monologue_to_tree(
mut events: EventReader<AssignMonologue>,
query: Query<Entity, (With<Tree>, Without<TreeMonologue>)>,
mut notice: ResMut<Notice>,
mut commands: Commands, mut commands: Commands,
) { ) {
nodes.iter_mut().for_each(|(e, n)| { // Kinda a weird hack because query does not update
if matches!(n, NavState::Closed) { // If we do this inline we assign new monologues to the same first tree
commands.entity(e).despawn_related::<Children>(); let mut t = query.iter();
events.read().for_each(|event| {
debug!("Assigning monologue {:?}", event);
// Get a valid tree to assign an entity to
if let Some(tree) = t.next() {
// Create the TreeMonologue component
let monologue = TreeMonologue(event.0.clone());
// Insert the component to the entity
commands.entity(tree).insert(monologue);
} else if let Some(path) = event.0.path() {
error!("No trees avaliable for {path:?}");
notice.0 = format!("No trees avaliable for {path:?}");
} else {
error!("Monologue is not yet loaded!");
notice.0 = "Monologue is not yet loaded!".into();
} }
}); });
} }
// When you pointer goes off of the '+' or any of it's children make the entire menu invisible /// On startup, plant a forest (add a few trees to the game)
fn control_menu( fn plant_forest(monos: Res<Assets<Monologue>>, mut e_trees: EventWriter<PlantTree>) {
mut over_events: EventReader<Pointer<Over>>, let mut i = 10;
mut out_events: EventReader<Pointer<Out>>, for id in monos.ids() {
nav_children: Query<&NavParent>, debug!("Planting tree with monologue {:?}", id);
children: Query<&ChildOf>, if i > 5 {
nav_parents: Query<&NavChildren>, e_trees.write(PlantTree(Some(Handle::Weak(id))));
parents: Query<&Children>, } else if i > 0 {
mut nav: Query<&mut NavState>, e_trees.write(PlantTree(None));
hover_map: Res<HoverMap>, } else {
) { break;
over_events.read().for_each(|over| {
let root = nav_children.root_ancestor(over.target);
nav_parents.iter_descendants(root).for_each(|child| {
if let Ok(mut n) = nav.get_mut(child) {
*n = NavState::Open;
}
});
});
// Gives us the enities covered by the HoverMap
let is_hovered: Vec<&Entity> = hover_map
.iter()
.flat_map(|(_, submap)| {
let x: Vec<&Entity> = submap.iter().map(|(node, _)| node).collect();
x
})
.collect();
// For all pointer out events
out_events.read().for_each(|out| {
// If a relative of out.target is hovered, do nothing
// Otherwise set to closed
let root = children.root_ancestor(out.target);
let tree_still_hovered = parents
.iter_descendants(root)
.any(|child| is_hovered.contains(&&child));
if !tree_still_hovered {
if let Ok(mut n) = nav.get_mut(root) {
*n = NavState::Closed;
}
parents.iter_descendants(root).for_each(|child| {
if let Ok(mut n) = nav.get_mut(child) {
*n = NavState::Closed;
} }
}) i -= 1;
} }
})
} }

@ -1,7 +1,7 @@
use super::*; use super::*;
/// A monologue containing a list of optional lines /// A monologue containing a list of optional lines
#[derive(Asset, TypePath, Debug, Deserialize, Default, Clone)] #[derive(Asset, TypePath, Debug, Deserialize, Default, Clone, PartialEq)]
pub(crate) struct Monologue { pub(crate) struct Monologue {
pub batches: Vec<MonologueLineBatch>, pub batches: Vec<MonologueLineBatch>,
} }
@ -16,8 +16,49 @@ impl Monologue {
} }
} }
#[derive(Debug, Error)]
pub struct MonologueParseError;
impl std::fmt::Display for MonologueParseError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Could not parse monologue file")
}
}
impl TryFrom<&str> for Monologue {
type Error = MonologueParseError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
// Create a blank monologue to populate
let mut monologue = Monologue::default();
// Add an initial batch that may end up being empty
monologue.add_batch(MonologueLineBatch::default());
// Iterate over raw string lines in the .mono file
for line in value.lines() {
// Break up into batches by --- separator
if line.starts_with("---") {
monologue.add_batch(MonologueLineBatch::default());
// Skip any empty lines or comments
} else if line.starts_with("#") || line.is_empty() {
// Skip comments and blank lines
// everything else we read as a monologue line
} else {
monologue.batches.last_mut().unwrap().add_line(line.into());
}
}
// Clear empty batches
monologue.batches.retain(|batch| !batch.lines.is_empty());
Ok(monologue)
}
}
/// A set of possible lines in a monologue /// A set of possible lines in a monologue
#[derive(Debug, Deserialize, Default, Clone)] #[derive(Debug, Deserialize, Default, Clone, PartialEq)]
pub(crate) struct MonologueLineBatch { pub(crate) struct MonologueLineBatch {
pub lines: Vec<MonologueLine>, pub lines: Vec<MonologueLine>,
} }
@ -29,7 +70,7 @@ impl MonologueLineBatch {
} }
/// A single monologue line /// A single monologue line
#[derive(Debug, Deserialize, Default, Clone)] #[derive(Debug, Deserialize, Default, Clone, PartialEq)]
pub(crate) struct MonologueLine { pub(crate) struct MonologueLine {
pub value: String, pub value: String,
} }
@ -46,9 +87,9 @@ impl From<String> for MonologueLine {
} }
} }
impl Into<String> for MonologueLine { impl From<MonologueLine> for String {
fn into(self) -> String { fn from(val: MonologueLine) -> Self {
self.value val.value
} }
} }
@ -60,6 +101,156 @@ impl From<&str> for MonologueLine {
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use indoc::*;
#[test]
fn empty_monologue() {
const MONO: &str = "";
let parsed: Monologue = MONO.try_into().unwrap();
let expected = Monologue::default();
assert_eq!(parsed, expected);
}
#[test]
fn complicated_empty_monologue() {
const MONO: &str = indoc! {"
---
---
# a comment
---
--- # more stuff
# another comment
---
---
"};
let parsed: Monologue = MONO.try_into().unwrap();
let expected = Monologue::default();
assert_eq!(parsed, expected);
}
#[test]
fn basic_monologue() {
const MONO: &str = indoc! {"
---
hello
---
world
---
"};
let parsed: Monologue = MONO.try_into().unwrap();
let expected = Monologue {
batches: vec![
MonologueLineBatch {
lines: vec![
"hello".into()
],
},
MonologueLineBatch {
lines: vec![
"world".into()
]
}
]
};
assert_eq!(parsed, expected);
}
#[test]
fn basic_with_comments_monologue() {
const MONO: &str = indoc! {"
# Some stuff before we get started
---
# I really like this line
hello
---
# I'm skeptical about this one...
world
# This line needs work
---
# More notes after the lines
"};
let parsed: Monologue = MONO.try_into().unwrap();
let expected = Monologue {
batches: vec![
MonologueLineBatch {
lines: vec![
"hello".into()
],
},
MonologueLineBatch {
lines: vec![
"world".into()
]
}
]
};
assert_eq!(parsed, expected);
}
#[test]
fn batches_monologue() {
const MONO: &str = indoc! {"
a
b
c
---
d
e
---
# with comment before...
f
# ...and after
"};
let parsed: Monologue = MONO.try_into().unwrap();
let expected = Monologue {
batches: vec![
MonologueLineBatch {
lines: vec![
"a".into(), "b".into(), "c".into()
]
},
MonologueLineBatch {
lines: vec![
"d".into(), "e".into()
]
},
MonologueLineBatch {
lines: vec![
"f".into()
]
},
]
};
assert_eq!(parsed, expected);
}
}
#[derive(Default)] #[derive(Default)]
struct MonologueLoader; struct MonologueLoader;
@ -69,6 +260,9 @@ enum MonologueLoaderError {
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error("Could not parse utf8")] #[error("Could not parse utf8")]
Utf8(#[from] std::string::FromUtf8Error), Utf8(#[from] std::string::FromUtf8Error),
// TODO: Real errors
#[error("Could not as monologue")]
Parse(#[from] MonologueParseError),
} }
impl AssetLoader for MonologueLoader { impl AssetLoader for MonologueLoader {
@ -86,30 +280,7 @@ impl AssetLoader for MonologueLoader {
let raw_string = String::from_utf8(bytes)?; let raw_string = String::from_utf8(bytes)?;
// Create a blank monologue to populate Ok(Monologue::try_from(raw_string.as_str())?)
let mut monologue = Monologue::default();
// Add an initial batch that may end up being empty
monologue.add_batch(MonologueLineBatch::default());
// Iterate over raw string lines in the .mono file
for line in raw_string.lines() {
// Break up into batches by --- separator
if line.starts_with("---") {
monologue.add_batch(MonologueLineBatch::default());
// Skip any empty lines or comments
} else if line.starts_with("#") || line.is_empty() {
// Skip comments and blank lines
// everything else we read as a monologue line
} else {
monologue.batches.last_mut().unwrap().add_line(line.into());
}
}
// Clear empty batches
monologue.batches.retain(|batch| !batch.lines.is_empty());
Ok(monologue)
} }
fn extensions(&self) -> &[&str] { fn extensions(&self) -> &[&str] {

@ -11,10 +11,8 @@ impl Plugin for DebuggingPlugin {
.init_resource::<ToolTip>() .init_resource::<ToolTip>()
.init_resource::<Fps>() .init_resource::<Fps>()
.init_resource::<EntityCount>() .init_resource::<EntityCount>()
.add_plugins(RapierDebugRenderPlugin::default().disabled()) .init_resource::<Notice>()
// Added by Rapier .add_plugins(PhysicsDebugPlugin::default())
// .add_plugins(AabbGizmoPlugin)
// .add_plugins(LightGizmoPlugin)
.add_systems(Startup, init_debug_ui) .add_systems(Startup, init_debug_ui)
.add_systems( .add_systems(
Update, Update,
@ -32,6 +30,7 @@ impl Plugin for DebuggingPlugin {
sync_resource_to_ui::<Fps>.run_if(resource_changed::<Fps>), sync_resource_to_ui::<Fps>.run_if(resource_changed::<Fps>),
track_entity_count, track_entity_count,
sync_resource_to_ui::<EntityCount>.run_if(resource_changed::<EntityCount>), sync_resource_to_ui::<EntityCount>.run_if(resource_changed::<EntityCount>),
sync_resource_to_ui::<Notice>.run_if(resource_changed::<Notice>),
) )
.run_if(in_state(DebuggingState::On)), .run_if(in_state(DebuggingState::On)),
), ),
@ -47,7 +46,7 @@ impl Plugin for DebuggingPlugin {
}) })
.add_systems( .add_systems(
Update, Update,
toggle_rapier_debug_render.run_if(state_changed::<DebuggingState>), toggle_physics_debug_render.run_if(state_changed::<DebuggingState>),
) )
.add_systems(OnEnter(DebuggingState::On), enable_wireframe) .add_systems(OnEnter(DebuggingState::On), enable_wireframe)
.add_systems(OnExit(DebuggingState::On), disable_wireframe); .add_systems(OnExit(DebuggingState::On), disable_wireframe);
@ -67,75 +66,97 @@ pub enum DebuggingState {
/// Create the Debugging UI /// Create the Debugging UI
fn init_debug_ui(mut commands: Commands) { fn init_debug_ui(mut commands: Commands) {
// "Debugging On" Indicator // Version string for troubleshooting
commands.spawn(( commands.spawn((
DebuggingState::On, DebuggingState::On,
Name::new("Debug Indicator"), Name::new("Version #"),
Text(" Debug: ON ".into()), children![Text::new(VERSION),],
TextColor(WHITE.into()), GlobalZIndex(i32::MAX - 1),
BackgroundColor(RED.into()),
BorderRadius::MAX,
Node { Node {
align_self: AlignSelf::Center, max_width: Val::Percent(50.0),
align_self: AlignSelf::End,
justify_self: JustifySelf::End, justify_self: JustifySelf::End,
..default() ..default()
}, },
)); ));
// Version string for troubleshooting commands
commands.spawn(( .spawn((
DebuggingState::On, DebuggingState::On,
Name::new("Version #"), Name::new("Notice"),
Text::new(VERSION), children![(Text::new(""), SyncResource::<Notice>::default(),)],
TextColor(WHITE.into()), GlobalZIndex(i32::MAX - 1),
BackgroundColor(BLACK.into()),
Node { Node {
width: Val::Auto, max_width: Val::Percent(50.0),
align_self: AlignSelf::End, align_self: AlignSelf::End,
justify_self: JustifySelf::End, justify_self: JustifySelf::Start,
..default() ..default()
}, },
)); ))
.observe(close_on_click);
// Version string for troubleshooting // "Turn on Debugging" button
commands.spawn(( commands
.spawn((
Node {
align_self: AlignSelf::Start,
justify_self: JustifySelf::End,
flex_direction: FlexDirection::Column,
..default()
},
DebuggingState::Off,
Name::new("Debug Indicator"),
GlobalZIndex(i32::MAX - 1),
children![Text("Debug: OFF".into()),],
Button,
))
.observe(toggle_debug);
commands
.spawn((
DebuggingState::On, DebuggingState::On,
Name::new("FPS"),
Text::new("FPS: ##.#"),
TextColor(WHITE.into()),
BackgroundColor(BLACK.into()),
SyncResource::<Fps>::default(),
Node { Node {
width: Val::Auto,
align_self: AlignSelf::Start, align_self: AlignSelf::Start,
justify_self: JustifySelf::End, justify_self: JustifySelf::End,
flex_direction: FlexDirection::Column,
..default() ..default()
}, },
))
.with_children(|parent| {
parent
.spawn((
// Debug is active & button to toggle
DebuggingState::On,
Name::new("Debug Indicator"),
GlobalZIndex(i32::MAX - 1),
children![Text("Debug: ON".into()),],
Button,
))
.observe(toggle_debug);
parent.spawn((
// FPS Counter for troubleshooting
DebuggingState::On,
Name::new("FPS"),
GlobalZIndex(i32::MAX - 1),
Text::new("FPS: ##.#"),
SyncResource::<Fps>::default(),
)); ));
parent.spawn((
commands.spawn(( // Entity count
DebuggingState::On, DebuggingState::On,
Name::new("Entity Count"), Name::new("Entity Count"),
GlobalZIndex(i32::MAX - 1),
Text::new("Entities: ###"), Text::new("Entities: ###"),
TextColor(WHITE.into()),
BackgroundColor(BLACK.into()),
SyncResource::<EntityCount>::default(), SyncResource::<EntityCount>::default(),
Node {
width: Val::Auto,
align_self: AlignSelf::Start,
justify_self: JustifySelf::Center,
..default()
},
)); ));
});
// Tooltip // Tooltip
commands.spawn(( commands
.spawn((
DebuggingState::On, DebuggingState::On,
Text("Tooltip Placeholder".into()),
Pickable::IGNORE,
TextColor(WHITE.into()),
SyncResource::<ToolTip>::default(), SyncResource::<ToolTip>::default(),
BackgroundColor(BLACK.with_alpha(0.9).into()), Pickable::IGNORE,
GlobalZIndex(i32::MAX), GlobalZIndex(i32::MAX),
Node { Node {
position_type: PositionType::Absolute, position_type: PositionType::Absolute,
@ -147,7 +168,13 @@ fn init_debug_ui(mut commands: Commands) {
justify_content: JustifyContent::Center, justify_content: JustifyContent::Center,
..default() ..default()
}, },
))
.with_children(|parent| {
parent.spawn((
Text("Tooltip Placeholder".into()),
SyncResource::<ToolTip>::default(),
)); ));
});
} }
/// Toggles the debug state from off -> on // off -> on when triggered /// Toggles the debug state from off -> on // off -> on when triggered
@ -164,13 +191,13 @@ fn toggle_debug_state(
info!("Toggling debug state: {:?} -> {:?}", curr, next); info!("Toggling debug state: {:?} -> {:?}", curr, next);
} }
/// Simple system that enables/disables rapier debug visuals when the debugging state changes #[derive(Default, Resource)]
#[cfg(not(target_arch = "wasm32"))] pub struct Notice(pub String);
fn toggle_rapier_debug_render(
state: Res<State<DebuggingState>>, impl Display for Notice {
mut context: ResMut<DebugRenderContext>, fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
) { writeln!(f, "{}", self.0)
context.enabled = *state.get() == DebuggingState::On; }
} }
/// Add a generic Tooltip that follows the mouse in debug mode /// Add a generic Tooltip that follows the mouse in debug mode
@ -197,7 +224,7 @@ impl Display for ToolTip {
} }
fn tooltip_follow( fn tooltip_follow(
mut tooltip: Single<(&mut Node, &mut Visibility), With<SyncResource<ToolTip>>>, mut tooltip: Single<(&mut Node, &mut Visibility), (With<SyncResource<ToolTip>>, Without<Text>)>,
window: Single<&Window>, window: Single<&Window>,
) { ) {
if let Some(Vec2 { x, y }) = window.cursor_position() { if let Some(Vec2 { x, y }) = window.cursor_position() {
@ -338,3 +365,32 @@ impl Display for EntityCount {
fn track_entity_count(query: Query<Entity>, mut count: ResMut<EntityCount>) { fn track_entity_count(query: Query<Entity>, mut count: ResMut<EntityCount>) {
count.0 = query.iter().len(); count.0 = query.iter().len();
} }
/// Toggle the debug state when a button is clicked
fn toggle_debug(
_trigger: Trigger<Pointer<Click>>,
curr: Res<State<DebuggingState>>,
mut next: ResMut<NextState<DebuggingState>>,
) {
next.set(match curr.get() {
DebuggingState::On => DebuggingState::Off,
DebuggingState::Off => DebuggingState::On,
});
}
fn close_on_click(trigger: Trigger<Pointer<Click>>, mut query: Query<&mut Visibility>) {
if let Ok(mut v) = query.get_mut(trigger.target()) {
*v = Visibility::Hidden;
}
}
fn toggle_physics_debug_render(
state: Res<State<DebuggingState>>,
mut config_store: ResMut<GizmoConfigStore>,
) {
let (_, config) = config_store.config_mut::<PhysicsGizmos>();
*config = match state.get() {
DebuggingState::On => PhysicsGizmos::all(),
DebuggingState::Off => PhysicsGizmos::none(),
};
}

@ -8,9 +8,13 @@ mod scheduling;
mod ui; mod ui;
mod version; mod version;
// Rust stdlib
pub use std::fmt::Display; pub use std::fmt::Display;
pub use std::f32::consts::PI;
// Community libraries
pub use bevy::{ pub use bevy::{
sprite::AlphaMode2d,
asset::{AssetLoader, LoadContext, LoadState, LoadedFolder, io::Reader}, asset::{AssetLoader, LoadContext, LoadState, LoadedFolder, io::Reader},
color::palettes::css::*, color::palettes::css::*,
gizmos::{aabb::AabbGizmoPlugin, light::LightGizmoPlugin}, gizmos::{aabb::AabbGizmoPlugin, light::LightGizmoPlugin},
@ -27,10 +31,11 @@ pub use bevy::{
reflect::TypePath, reflect::TypePath,
window::WindowResized, window::WindowResized,
}; };
pub use bevy_rapier3d::prelude::*; pub use avian3d::prelude::*;
pub use serde::Deserialize; pub use serde::Deserialize;
pub use thiserror::Error; pub use thiserror::Error;
// Internal modules
pub use base_game::*; pub use base_game::*;
pub use debug::*; pub use debug::*;
pub use loading::*; pub use loading::*;

@ -4,10 +4,157 @@ pub(crate) struct BaseUiPlugin;
impl Plugin for BaseUiPlugin { impl Plugin for BaseUiPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
// TODO app.init_resource::<Style>().add_systems(
Update,
(
add_ui_node.run_if(any_component_added::<Node>),
add_ui_button
.run_if(any_component_added::<Button>)
.after(add_ui_node),
add_ui_text
.run_if(any_component_added::<Text>)
.after(add_ui_node),
navs_on_top.run_if(any_component_changed::<NavState>),
),
);
} }
} }
/// A resource for supplying style info, e.g., color pallette
#[derive(Resource)]
pub struct Style {
primary: Color,
secondary: Color,
accent: Color,
}
impl Default for Style {
fn default() -> Self {
Style {
primary: WHITE.into(),
secondary: BLACK.into(),
accent: ORANGE_RED.into(),
}
}
}
fn add_ui_button(added: Query<Entity, Added<Button>>, mut commands: Commands) {
fn over(
trigger: Trigger<Pointer<Over>>,
mut query: Query<&mut BorderColor>,
style: Res<Style>,
) {
if let Ok(mut bc) = query.get_mut(trigger.target()) {
debug!("pointer over {:?}", trigger.target());
bc.0 = style.accent;
}
}
fn out(trigger: Trigger<Pointer<Out>>, mut query: Query<&mut BorderColor>, style: Res<Style>) {
if let Ok(mut bc) = query.get_mut(trigger.target()) {
debug!("pointer out {:?}", trigger.target());
bc.0 = style.secondary;
}
}
fn pressed(
trigger: Trigger<Pointer<Pressed>>,
mut query: Query<&mut BackgroundColor>,
style: Res<Style>,
) {
if let Ok(mut bg) = query.get_mut(trigger.target()) {
debug!("pointer pressed {:?}", trigger.target());
bg.0 = style.accent;
}
}
fn released(
trigger: Trigger<Pointer<Released>>,
mut query: Query<&mut BackgroundColor>,
style: Res<Style>,
) {
if let Ok(mut bg) = query.get_mut(trigger.target()) {
debug!("pointer released {:?}", trigger.target());
bg.0 = style.primary;
}
}
added.iter().for_each(|e| {
debug!("Updating button: {:?}", e);
// Observe with hover over/out + click reactors
commands
.entity(e)
.observe(over)
.observe(out)
.observe(pressed)
.observe(released);
})
}
fn add_ui_text(added: Query<Entity, Added<Text>>, mut commands: Commands) {
fn pressed(
trigger: Trigger<Pointer<Pressed>>,
mut query: Query<(&mut TextColor, &ChildOf)>,
buttons: Query<Entity, With<Button>>,
style: Res<Style>,
) {
// ONLY DO THIS IF CHILD OF BUTTON
if let Ok((mut tc, ChildOf(p))) = query.get_mut(trigger.target())
&& buttons.contains(*p)
{
debug!("pointer pressed {:?}", trigger.target());
tc.0 = style.primary;
}
}
fn released(
trigger: Trigger<Pointer<Released>>,
mut query: Query<(&mut TextColor, &ChildOf)>,
buttons: Query<Entity, With<Button>>,
style: Res<Style>,
) {
// ONLY DO THIS IF CHILD OF BUTTON
if let Ok((mut tc, ChildOf(p))) = query.get_mut(trigger.target())
&& buttons.contains(*p)
{
debug!("pointer released {:?}", trigger.target());
tc.0 = style.secondary;
}
}
added.iter().for_each(|e| {
debug!("Updating text: {:?}", e);
// Observe with hover over/out + click reactors
commands.entity(e).observe(pressed).observe(released);
})
}
fn add_ui_node(
mut added: Query<(Entity, &mut Node), Added<Node>>,
style: Res<Style>,
text: Query<Entity, With<Text>>,
mut commands: Commands,
) {
added.iter_mut().for_each(|(e, mut n)| {
debug!("Updating node: {:?}", e);
// Update Node border
n.border = UiRect::all(Val::Px(2.0));
let mut this = commands.entity(e);
if text.contains(e) {
// Only change text color for text
this.insert(TextColor(style.secondary));
} else {
// Add extra stuff for non-text nodes
this.insert(BackgroundColor(style.primary));
this.insert(BorderColor(style.secondary));
this.insert(BorderRadius::all(Val::Px(5.0)));
}
})
}
/// Marker component for handling Resource -> Ui Sync /// Marker component for handling Resource -> Ui Sync
#[derive(Component, Default, Debug)] #[derive(Component, Default, Debug)]
pub struct SyncResource<R: Resource + Default + Display>(R); pub struct SyncResource<R: Resource + Default + Display>(R);
@ -16,11 +163,12 @@ pub struct SyncResource<R: Resource + Default + Display>(R);
/// ///
/// Mostly useful for quick n' dirty getting data to the user /// Mostly useful for quick n' dirty getting data to the user
pub fn sync_resource_to_ui<R: Resource + Default + Display>( pub fn sync_resource_to_ui<R: Resource + Default + Display>(
mut q: Query<&mut Text, With<SyncResource<R>>>, mut q: Query<(&mut Text, &mut Visibility), With<SyncResource<R>>>,
r: Res<R>, r: Res<R>,
) { ) {
q.iter_mut().for_each(|mut t| { q.iter_mut().for_each(|(mut t, mut v)| {
t.0 = format!("{}", *r); t.0 = format!("{}", *r);
*v = Visibility::Inherited;
}); });
} }
@ -60,3 +208,14 @@ pub enum NavState {
#[default] #[default]
Closed, Closed,
} }
fn navs_on_top(changed: Query<(Entity, &NavState), Changed<NavState>>, mut commands: Commands) {
changed.iter().for_each(|(e, ns)| match ns {
NavState::Open => {
commands.entity(e).insert(GlobalZIndex(i32::MAX / 2));
}
NavState::Closed => {
commands.entity(e).remove::<GlobalZIndex>();
}
})
}

Loading…
Cancel
Save