35 Commits

Author SHA1 Message Date
Patrick Gelvin
37cb394bf5 Initial card => tilemap 2025-08-27 07:51:13 -07:00
Patrick Gelvin
c81d17ce31 Initial fuckery with ecs tilemap 2025-08-27 07:50:23 -07:00
4d8c8b5302 Demo the UI with a dummy machine
This finally shows the basic operating principle of the machine UIs.
There will be some object out in the world that, when clicked, will open
a new UI screen to operate the machine.

Next up is to attach various bits of info, like a `Fuel` component on
the machine which the UI widget locates and displays as text. This gives
me the infrastructure necessary to begin that work.
2025-08-26 16:34:18 -05:00
112b22875a Fix doc on CloseButton::bundle() 2025-08-26 13:38:12 -05:00
9411e57759 Make CloseButton observer functions private
Now that there's a UI plugin, these systems don't need to be public. So
they won't be.
2025-08-26 13:35:40 -05:00
a489cdc5e8 Add some theme TODOs on the button widgets 2025-08-26 13:35:20 -05:00
5f617a67f6 Fix a couple of lints 2025-08-26 13:28:39 -05:00
9fb374059a Fix: Delete trailing semicolon in BRB::bundle()
Normally this would be a compilation error, but Bevy has an
`impl Bundle for ()` somewhere.

The intended bundle content gets discarded and a unit is implicitly
returned. That unit is implicitly converted into a `Bundle`, satisfying
the compiler.
2025-08-26 13:17:38 -05:00
cf9b415bb7 Update BigRedButton usage sites, add callback note 2025-08-26 13:17:03 -05:00
9bb15b7511 Convert BigRedButton ctor into just a Bundle
The BigRedButton isn't doing anything that requires the `Commands`, so
it'll be turned back into just a Bundle.
2025-08-26 13:01:49 -05:00
c567bc3706 Fix: Filter for BigRedButton in it's observers
There was no BRB component so the Observer systems would only filter for
`Button`, which matched against the close button by accident.
2025-08-26 12:54:46 -05:00
85499f4156 Move BigRedButton's input handling observers
But there's a bug... the `CloseButton`s now get colored red and pink.
2025-08-26 12:49:37 -05:00
1c0681e67e Create a BigRedButton struct, like CloseButton
The `BigRedButton` should be a real struct and component so that I can
query for it later.

I'm following the same API pattern as the `CloseButton` struct, in that
there is a spawner using the `Commands` (or `ChildSpawnerCommands`) and
the observer functions are static methods.
2025-08-26 12:46:48 -05:00
da7f9b3152 Create a custom plugin to centralize UI setup
The UI elements need some things set up for them to work properly.
Namely, the `UiTheme` resource and the Observers which handle button
presses.
2025-08-26 12:34:35 -05:00
fb1b954262 Replace base-ui bundle with a spawning function 2025-08-26 12:25:49 -05:00
f1ce30bde5 Rename machine UI spawners 2025-08-26 11:47:32 -05:00
c833572f69 Reattach the close button to the rotator machine
That was much easier than I expected it to be. I'm still going to alter
how the `machine_ui_base` function works so that I don't have to
remember to attach a button all the time.
2025-08-26 09:49:47 -05:00
f89a0d2e66 Record target entity in CloseButton
I was originally planning to search up the entity hierarchy to find the
top-most entity and despawn that. This will become a problem if I ever
want to have panels in panels.

Instead, just record the target entity and despawn it.

This means the current usage doesn't work, so I've removed it from the
base machine UI bundle.
2025-08-26 09:10:34 -05:00
bda0bb7de3 Add most of a "CloseButton" widget
I'm trying out a new scope strategy. The CloseButton exists as a real
struct with the `Component` trait. There's a bundle spawning utility
function and several observer systems. The observer systems have been
added directly to the app, which I *believe* means they won't be
duplicated the way the Big Red Button's observers will.
2025-08-26 09:02:08 -05:00
f2f1674451 Rename machine button spawner, "big red button"
It's the big red button. Don't press it.

Or do.
2025-08-25 19:45:35 -05:00
d235d6af5e (autoformat) 2025-08-25 19:18:06 -05:00
e2b2d5b5d9 Move the machine UI spawners to a new submodule 2025-08-25 19:17:54 -05:00
655bc5d3e2 Add new module for game bits 2025-08-25 18:55:38 -05:00
Patrick Gelvin
d98b28cb36 Cut triangle test 2025-08-25 18:25:59 -05:00
4bf3c44b5e Move widgets to a folder module 2025-08-25 18:17:39 -05:00
45e44ef8b1 Begin work on a rotator UI 2025-08-25 18:07:53 -05:00
e362df8682 Plug in the new UiTheme resource
Fixed struct field visibility, initialize the resource, and register it
with the EGUI Debug overlay. The button observers have also been updated
to use the resource.
2025-08-25 13:10:09 -05:00
4d8a178b74 Remove print-debug observer system
This was just so I could see two Observers executing from the same
trigger.
2025-08-25 13:04:40 -05:00
74302afab1 Replace "constants" module with "resources"
These aren't constants and they were never meant to be.

I've also started collecting the UI theme values. For now, just button
colors.
2025-08-25 12:54:29 -05:00
aa0c8b421b Handle machine button UI events with observers
Observers are "just" systems with a Trigger as their first parameter.
I've made 4 systems to handle the start and stop of the press and hover
actions.

Having the button *actually* do something (not just change colors) will
be achieved by attaching another button to the on-press trigger. I'm
expecting that the machine-ui-spawning system will do that, but in
principle it could be done by anything with access to the UI's button
entity ID.
2025-08-25 11:36:26 -05:00
875d157a21 Check-in the progress
Lots of nested components are making my head hurt. I'm not sure how to
solve the button call-back problem, yet.
2025-08-25 08:22:08 -05:00
a0cfc86f59 Center & Span the red rectangle to be a banner
Figured out how to place nodes in the grid. This will eventually become
the banner that says what the machine does.
2025-08-25 08:22:08 -05:00
79f66b2868 Colored squares on a grid layout
I'm still kinda figuring out how to do more complex UIs, so I'll be
making several commits that aren't very interesting to review... such as
this one.
2025-08-25 08:22:08 -05:00
Patrick Gelvin
7fb798edbe Initial asset library impl + tile images 2025-08-24 11:33:31 -07:00
Patrick Gelvin
b0ddc9236b Fix my busted implementations 2025-08-23 12:56:10 -07:00
56 changed files with 1282 additions and 52 deletions

View File

@@ -4,8 +4,9 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
bevy = { version = "0.16.1", features = ["dynamic_linking"]} bevy = { version = "0.16.1", features = ["dynamic_linking"] }
bevy-inspector-egui = "0.33.1" bevy-inspector-egui = "0.33.1"
bevy_ecs_tilemap = "0.16.0"
[profile.dev] [profile.dev]
opt-level = 1 opt-level = 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

92
src/assets.rs Normal file
View File

@@ -0,0 +1,92 @@
use bevy::prelude::*;
use std::path::Path;
pub struct TileImage {
pub name: &'static str,
pub thumbnail_handle: Handle<Image>,
pub tile_handle: Handle<Image>,
}
#[derive(Resource)]
pub struct AssetLibrary {
pub tile_images: Vec<TileImage>,
}
impl AssetLibrary {
fn new(asset_server: &AssetServer) -> Self {
let tile_names = vec![
"corner-ne-1-wall",
"corner-ne-2-walls-south",
"corner-ne-2-walls-west",
"corner-ne-3-walls",
"corner-nw-1-wall",
"corner-nw-2-walls-south",
"corner-nw-2-walls-east",
"corner-nw-3-walls",
"corner-se-1-wall",
"corner-se-2-walls-north",
"corner-se-2-walls-west",
"corner-se-3-walls",
"corner-sw-1-wall",
"corner-sw-2-walls-north",
"corner-sw-2-walls-east",
"corner-sw-3-walls",
"full-0-walls",
"full-1-wall",
"full-2-walls-corner",
"full-2-walls-hallway",
"full-3-walls",
"full-4-walls",
];
let tile_images = tile_names
.iter()
.cloned()
.map(|name| {
let thumbnail_path = Path::new("thumbnails").join(name).with_extension("png");
let tile_path = Path::new("tiles").join(name).with_extension("png");
let thumbnail_handle = asset_server.load::<Image>(thumbnail_path);
let tile_handle = asset_server.load::<Image>(tile_path);
TileImage {
name,
thumbnail_handle,
tile_handle,
}
})
.collect();
Self { tile_images }
}
pub fn get_index(&self, name: &str) -> Option<u32> {
self
.tile_images
.iter()
.enumerate()
.find(|(_, tile)| tile.name == name)
.map(|(index, _)| index as u32)
}
pub fn get_thumbnail(&self, name: &str) -> Option<Handle<Image>> {
self
.tile_images
.iter()
.find(|&tile| tile.name == name)
.map(|tile| tile.thumbnail_handle.clone())
}
pub fn get_tile(&self, name: &str) -> Option<Handle<Image>> {
self
.tile_images
.iter()
.find(|&tile| tile.name == name)
.map(|tile| tile.tile_handle.clone())
}
}
pub fn load_assets(mut commands: Commands, asset_server: Res<AssetServer>) {
let asset_library = AssetLibrary::new(&asset_server);
commands.insert_resource(asset_library);
}

View File

@@ -1,5 +1,7 @@
//! TODO: module doc :v //! TODO: module doc :v
use bevy::ecs::component::Component;
/// Value for the "sub tiles" inside a room tile /// Value for the "sub tiles" inside a room tile
#[derive(Clone, Copy, Debug, Default, PartialEq)] #[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum Cell { pub enum Cell {
@@ -12,6 +14,25 @@ pub enum Cell {
Filled, Filled,
} }
#[derive(Clone, Copy, Debug)]
pub struct Walls {
pub north: bool,
pub east: bool,
pub south: bool,
pub west: bool,
}
impl Walls {
pub fn new(north: bool, east: bool, south: bool, west: bool) -> Self {
Self {
north,
east,
south,
west,
}
}
}
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub enum CutLine { pub enum CutLine {
VertLeft, VertLeft,
@@ -47,17 +68,17 @@ pub enum RotationDir {
/// An invidiual room, or "card" in the player's hand. The room may /// An invidiual room, or "card" in the player's hand. The room may
/// *or may not* be valid, yet. /// *or may not* be valid, yet.
#[derive(Clone, Copy, Debug, Default, PartialEq)] #[derive(Clone, Copy, Component, Debug, Default, PartialEq)]
pub struct Card { pub struct Card {
cells: [Cell; 9], pub cells: [Cell; 9],
nw: bool, pub nw: bool,
n: bool, pub n: bool,
ne: bool, pub ne: bool,
w: bool, pub w: bool,
e: bool, pub e: bool,
sw: bool, pub sw: bool,
s: bool, pub s: bool,
se: bool, pub se: bool,
} }
impl Card { impl Card {
@@ -160,13 +181,30 @@ impl Card {
pub fn flip(self, flip: FlipDir) -> Self { pub fn flip(self, flip: FlipDir) -> Self {
let mut new_card = Self::default(); let mut new_card = Self::default();
let transform = match flip {
FlipDir::Horizontal => |cell: Cell| match cell {
Cell::NW => Cell::NE,
Cell::NE => Cell::NW,
Cell::SW => Cell::SE,
Cell::SE => Cell::SW,
other => other,
},
FlipDir::Vertical => |cell: Cell| match cell {
Cell::NW => Cell::SW,
Cell::NE => Cell::SE,
Cell::SW => Cell::NW,
Cell::SE => Cell::NE,
other => other,
},
};
// Check for doors // Check for doors
macro_rules! assign_cell { macro_rules! assign_cell {
( ($source_dir:ident, $source_index:literal), ($dest_dir:ident, $dest_index:literal), $repeat:expr ) => { ( ($source_dir:ident, $source_index:literal), ($dest_dir:ident, $dest_index:literal), $repeat:expr ) => {
new_card.cells[$dest_index] = self.cells[$source_index]; new_card.cells[$dest_index] = transform(self.cells[$source_index]);
new_card.$dest_dir = self.$source_dir; new_card.$dest_dir = self.$source_dir;
if $repeat == true { if $repeat == true {
new_card.cells[$source_index] = self.cells[$dest_index]; new_card.cells[$source_index] = transform(self.cells[$dest_index]);
new_card.$source_dir = self.$dest_dir; new_card.$source_dir = self.$dest_dir;
} }
}; };
@@ -189,7 +227,7 @@ impl Card {
} }
} }
new_card.cells[4] = self.cells[4]; new_card.cells[4] = transform(self.cells[4]);
new_card new_card
} }
@@ -251,38 +289,55 @@ impl Card {
pub fn rotate(self, dir: RotationDir) -> Self { pub fn rotate(self, dir: RotationDir) -> Self {
let mut new_card = Self::default(); let mut new_card = Self::default();
let transform = match dir {
RotationDir::Clockwise => |cell: Cell| match cell {
Cell::NW => Cell::NE,
Cell::NE => Cell::SE,
Cell::SE => Cell::SW,
Cell::SW => Cell::NW,
other => other,
},
RotationDir::CounterClockwise => |cell: Cell| match cell {
Cell::NW => Cell::SW,
Cell::SW => Cell::SE,
Cell::SE => Cell::NW,
Cell::NE => Cell::NW,
other => other,
},
};
// Check for doors // Check for doors
macro_rules! assign_cell { macro_rules! assign_cell {
( ($source_dir:ident, $source_index:literal), ($dest_dir:ident, $dest_index:literal) ) => { ( ($source_dir:ident, $source_index:literal), ($dest_dir:ident, $dest_index:literal) ) => {
new_card.cells[$dest_index] = self.cells[$source_index]; new_card.cells[$dest_index] = transform(self.cells[$source_index]);
new_card.$dest_dir = self.$source_dir; new_card.$dest_dir = self.$source_dir;
}; };
} }
match dir { match dir {
RotationDir::Clockwise => { RotationDir::Clockwise => {
assign_cell!((nw, 0), (n, 1)); assign_cell!((nw, 0), (ne, 2));
assign_cell!((n, 1), (ne, 2)); assign_cell!((n, 1), (e, 5));
assign_cell!((ne, 2), (e, 5)); assign_cell!((ne, 2), (se, 8));
assign_cell!((e, 5), (se, 8)); assign_cell!((e, 5), (s, 7));
assign_cell!((se, 8), (s, 7)); assign_cell!((se, 8), (sw, 6));
assign_cell!((s, 7), (sw, 6)); assign_cell!((s, 7), (w, 3));
assign_cell!((sw, 6), (w, 3)); assign_cell!((sw, 6), (nw, 0));
assign_cell!((w, 3), (nw, 0)); assign_cell!((w, 3), (n, 1));
} }
RotationDir::CounterClockwise => { RotationDir::CounterClockwise => {
assign_cell!((nw, 0), (w, 3)); assign_cell!((nw, 0), (sw, 6));
assign_cell!((w, 3), (sw, 6)); assign_cell!((w, 3), (s, 7));
assign_cell!((sw, 6), (s, 7)); assign_cell!((sw, 6), (se, 8));
assign_cell!((s, 7), (se, 8)); assign_cell!((s, 7), (e, 5));
assign_cell!((se, 8), (e, 5)); assign_cell!((se, 8), (ne, 2));
assign_cell!((e, 5), (ne, 2)); assign_cell!((e, 5), (n, 1));
assign_cell!((ne, 2), (n, 1)); assign_cell!((ne, 2), (nw, 0));
assign_cell!((n, 1), (nw, 0)); assign_cell!((n, 1), (w, 3));
} }
} }
new_card.cells[4] = self.cells[4]; new_card.cells[4] = transform(self.cells[4]);
new_card new_card
} }
@@ -582,7 +637,108 @@ mod test {
#[test] #[test]
fn cut_triangle() { fn cut_triangle() {
todo!(); let tri = Card {
cells: NW_TRIANGLE,
..Default::default()
};
// Pairs of each different slice option
let vert_left_pair = (
Card {
#[rustfmt::skip]
cells: [
Cell::Empty, Cell::Empty, Cell::Empty,
Cell::Empty, Cell::Empty, Cell::Empty,
Cell::NW, Cell::Empty, Cell::Empty,
],
..Default::default()
},
Card {
#[rustfmt::skip]
cells: [
Cell::Empty, Cell::Empty, Cell::NW,
Cell::Empty, Cell::NW, Cell::Filled,
Cell::Empty, Cell::Filled, Cell::Filled,
],
..Default::default()
},
);
let vert_right_pair = (
Card {
#[rustfmt::skip]
cells: [
Cell::Empty, Cell::Empty, Cell::Empty,
Cell::Empty, Cell::NW, Cell::Empty,
Cell::NW, Cell::Filled, Cell::Empty,
],
..Default::default()
},
Card {
#[rustfmt::skip]
cells: [
Cell::Empty, Cell::Empty, Cell::NW,
Cell::Empty, Cell::Empty, Cell::Filled,
Cell::Empty, Cell::Empty, Cell::Filled,
],
..Default::default()
},
);
let horiz_top_pair = (
Card {
#[rustfmt::skip]
cells: [
Cell::Empty, Cell::Empty, Cell::NW,
Cell::Empty, Cell::Empty, Cell::Empty,
Cell::Empty, Cell::Empty, Cell::Empty,
],
..Default::default()
},
Card {
#[rustfmt::skip]
cells: [
Cell::Empty, Cell::Empty, Cell::Empty,
Cell::Empty, Cell::NW, Cell::Filled,
Cell::NW, Cell::Filled, Cell::Filled,
],
..Default::default()
},
);
let horiz_bottom_pair = (
Card {
#[rustfmt::skip]
cells: [
Cell::Empty, Cell::Empty, Cell::NW,
Cell::Empty, Cell::NW, Cell::Filled,
Cell::Empty, Cell::Empty, Cell::Empty,
],
..Default::default()
},
Card {
#[rustfmt::skip]
cells: [
Cell::Empty, Cell::Empty, Cell::Empty,
Cell::Empty, Cell::Empty, Cell::Empty,
Cell::NW, Cell::Filled, Cell::Filled,
],
..Default::default()
},
);
// Run tests
let result_vleft = tri.clone().cut(CutLine::VertLeft);
assert_eq!(result_vleft, vert_left_pair);
let result_vright = tri.clone().cut(CutLine::VertRight);
assert_eq!(result_vright, vert_right_pair);
let result_hupper = tri.clone().cut(CutLine::HorizUpper);
assert_eq!(result_hupper, horiz_top_pair);
let result_hlower = tri.cut(CutLine::HorizLower);
assert_eq!(result_hlower, horiz_bottom_pair);
} }
#[test] #[test]
@@ -704,10 +860,8 @@ mod test {
..Default::default() ..Default::default()
}; };
let (res_left, res_right) = left_shape.transpose( let (res_left, res_right) =
right_shape, left_shape.transpose(right_shape, TransposeSelection::Row(TransposeIndex::Third));
TransposeSelection::Row(TransposeIndex::Third),
);
assert_eq!(res_left, expected_left); assert_eq!(res_left, expected_left);
assert_eq!(res_right, expected_right); assert_eq!(res_right, expected_right);
} }

View File

@@ -1,12 +0,0 @@
//! Program constants & defaults
use bevy::prelude::*;
#[derive(Debug, Reflect, Resource)]
struct ConfigDefaults {}
impl FromWorld for ConfigDefaults {
fn from_world(world: &mut World) -> Self {
todo!()
}
}

439
src/game/map.rs Normal file
View File

@@ -0,0 +1,439 @@
use bevy::prelude::*;
use bevy_ecs_tilemap::prelude::*;
use crate::assets::AssetLibrary;
use crate::card::{Card, Cell, Walls};
pub fn setup_test_tilemap(mut commands: Commands, assets: Res<AssetLibrary>) {
let tilemap_entity = commands.spawn_empty().id();
let tilemap_id = TilemapId(tilemap_entity);
let map_size = TilemapSize { x: 9, y: 9 };
let mut tile_storage = TileStorage::empty(map_size);
let card = Card {
cells: [
Cell::NW,
Cell::Filled,
Cell::NE,
Cell::Filled,
Cell::Filled,
Cell::Filled,
Cell::SW,
Cell::Filled,
Cell::SE,
],
..Default::default()
};
add_card_to_tilemap(
&card,
&assets,
TilePos { x: 0, y: 2 },
tilemap_id,
&mut commands,
&mut tile_storage,
);
let tile_size = TilemapTileSize { x: 48.0, y: 48.0 };
let grid_size = tile_size.into();
let map_type = TilemapType::default();
let texture_handles = assets
.tile_images
.iter()
.map(|image| image.tile_handle.clone())
.collect();
commands.entity(tilemap_entity).insert(TilemapBundle {
grid_size,
map_type,
size: map_size,
storage: tile_storage,
texture: TilemapTexture::Vector(texture_handles),
tile_size,
anchor: TilemapAnchor::BottomLeft,
..Default::default()
});
}
/// Returns a cell description for a tilemap, including the texture index and a flip configuration
fn get_cell_description(
cell: &Cell,
walls: &Walls,
assets: &AssetLibrary,
) -> Option<(u32, TileFlip)> {
// Filter allowed walls
let walls = match cell {
Cell::NW => Walls {
north: false,
east: walls.east,
south: walls.south,
west: false,
},
Cell::NE => Walls {
north: false,
east: false,
south: walls.south,
west: walls.west,
},
Cell::SE => Walls {
north: walls.north,
east: false,
south: false,
west: walls.west,
},
Cell::SW => Walls {
north: walls.north,
east: walls.east,
south: false,
west: false,
},
_ => *walls,
};
let wall_count = (if walls.north { 1 } else { 0 })
+ (if walls.east { 1 } else { 0 })
+ (if walls.south { 1 } else { 0 })
+ (if walls.west { 1 } else { 0 });
match (cell, wall_count) {
(Cell::Empty, _) => None,
(Cell::NW, 0) => Some((
assets.get_index("corner-nw-1-wall").unwrap(),
TileFlip::default(),
)),
(Cell::NW, 1) => {
if walls.east {
Some((
assets.get_index("corner-nw-2-walls-east").unwrap(),
TileFlip::default(),
))
} else if walls.south {
Some((
assets.get_index("corner-nw-2-walls-south").unwrap(),
TileFlip::default(),
))
} else {
unreachable!()
}
}
(Cell::NW, 2) => Some((
assets.get_index("corner-nw-3-walls").unwrap(),
TileFlip::default(),
)),
(Cell::NE, 0) => Some((
assets.get_index("corner-ne-1-wall").unwrap(),
TileFlip::default(),
)),
(Cell::NE, 1) => {
if walls.west {
Some((
assets.get_index("corner-ne-2-walls-west").unwrap(),
TileFlip::default(),
))
} else if walls.south {
Some((
assets.get_index("corner-ne-2-walls-south").unwrap(),
TileFlip::default(),
))
} else {
unreachable!()
}
}
(Cell::NE, 2) => Some((
assets.get_index("corner-ne-3-walls").unwrap(),
TileFlip::default(),
)),
(Cell::SE, 0) => Some((
assets.get_index("corner-se-1-wall").unwrap(),
TileFlip::default(),
)),
(Cell::SE, 1) => {
if walls.west {
Some((
assets.get_index("corner-se-2-walls-west").unwrap(),
TileFlip::default(),
))
} else if walls.north {
Some((
assets.get_index("corner-se-2-walls-north").unwrap(),
TileFlip::default(),
))
} else {
unreachable!()
}
}
(Cell::SE, 2) => Some((
assets.get_index("corner-se-3-walls").unwrap(),
TileFlip::default(),
)),
(Cell::SW, 0) => Some((
assets.get_index("corner-sw-1-wall").unwrap(),
TileFlip::default(),
)),
(Cell::SW, 1) => {
if walls.east {
Some((
assets.get_index("corner-sw-2-walls-east").unwrap(),
TileFlip::default(),
))
} else if walls.north {
Some((
assets.get_index("corner-sw-2-walls-north").unwrap(),
TileFlip::default(),
))
} else {
unreachable!()
}
}
(Cell::SW, 2) => Some((
assets.get_index("corner-sw-3-walls").unwrap(),
TileFlip::default(),
)),
(Cell::Filled, 0) => Some((
assets.get_index("full-0-walls").unwrap(),
TileFlip::default(),
)),
(Cell::Filled, 1) => {
if walls.north {
Some((
assets.get_index("full-1-wall").unwrap(),
TileFlip::default(),
))
} else if walls.east {
Some((
assets.get_index("full-1-wall").unwrap(),
TileFlip {
x: true,
y: false,
d: true,
},
))
} else if walls.south {
Some((
assets.get_index("full-1-wall").unwrap(),
TileFlip {
x: false,
y: true,
d: false,
},
))
} else {
// walls.west
Some((
assets.get_index("full-1-wall").unwrap(),
TileFlip {
x: false,
y: false,
d: true,
},
))
}
}
(Cell::Filled, 2) => {
if walls.west && walls.north {
Some((
assets.get_index("full-2-walls-corner").unwrap(),
TileFlip::default(),
))
} else if walls.north && walls.east {
Some((
assets.get_index("full-2-walls-corner").unwrap(),
TileFlip {
x: true,
y: false,
d: false,
},
))
} else if walls.east && walls.south {
Some((
assets.get_index("full-2-walls-corner").unwrap(),
TileFlip {
x: true,
y: true,
d: false,
},
))
} else if walls.south && walls.west {
Some((
assets.get_index("full-2-walls-corner").unwrap(),
TileFlip {
x: false,
y: true,
d: false,
},
))
} else if walls.north && walls.south {
Some((
assets.get_index("full-2-walls-hallway").unwrap(),
TileFlip {
x: false,
y: false,
d: false,
},
))
} else if walls.east && walls.west {
Some((
assets.get_index("full-2-walls-hallway").unwrap(),
TileFlip {
x: false,
y: false,
d: true,
},
))
} else {
unreachable!()
}
}
(Cell::Filled, 3) => {
if !walls.north {
Some((
assets.get_index("full-3-walls").unwrap(),
TileFlip {
x: false,
y: true,
d: true,
},
))
} else if !walls.east {
Some((
assets.get_index("full-3-walls").unwrap(),
TileFlip::default(),
))
} else if !walls.south {
Some((
assets.get_index("full-3-walls").unwrap(),
TileFlip {
x: false,
y: false,
d: true,
},
))
} else if walls.west {
Some((
assets.get_index("full-3-walls").unwrap(),
TileFlip {
x: true,
y: false,
d: false,
},
))
} else {
unreachable!()
}
}
(Cell::Filled, 4) => Some((
assets.get_index("full-4-walls").unwrap(),
TileFlip::default(),
)),
_ => unreachable!(),
}
}
/// Returns an array of cell descriptions, starting from the top right tile and moving right to left, top to bottom
fn get_card_description(card: &Card, assets: &AssetLibrary) -> [Option<(u32, TileFlip)>; 9] {
let mut walls = [
Walls::new(true, false, false, true),
Walls::new(true, false, false, false),
Walls::new(true, true, false, false),
Walls::new(false, false, false, true),
Walls::new(false, false, false, false),
Walls::new(false, true, false, false),
Walls::new(false, false, true, true),
Walls::new(false, false, true, false),
Walls::new(false, true, true, false),
];
if card.cells[0] == Cell::Empty {
walls[1].west = true;
walls[3].north = true;
}
if card.cells[1] == Cell::Empty {
walls[0].east = true;
walls[2].west = true;
walls[4].north = true;
}
if card.cells[2] == Cell::Empty {
walls[1].east = true;
walls[5].north = true;
}
if card.cells[3] == Cell::Empty {
walls[0].south = true;
walls[4].west = true;
walls[6].north = true;
}
if card.cells[4] == Cell::Empty {
walls[1].south = true;
walls[3].east = true;
walls[5].west = true;
walls[7].north = true;
}
if card.cells[5] == Cell::Empty {
walls[2].south = true;
walls[4].east = true;
walls[8].north = true;
}
if card.cells[6] == Cell::Empty {
walls[7].west = true;
walls[3].south = true;
}
if card.cells[7] == Cell::Empty {
walls[6].east = true;
walls[8].west = true;
walls[4].south = true;
}
if card.cells[8] == Cell::Empty {
walls[7].east = true;
walls[5].south = true;
}
card
.cells
.iter()
.zip(walls.iter())
.map(|(cell, walls)| get_cell_description(cell, walls, assets))
.collect::<Vec<_>>()
.try_into()
.unwrap()
}
fn add_card_to_tilemap(
card: &Card,
assets: &AssetLibrary,
top_left: TilePos,
tilemap_id: TilemapId,
commands: &mut Commands,
tile_storage: &mut TileStorage,
) {
get_card_description(card, assets)
.iter()
.enumerate()
.for_each(|(index, desc)| {
let dx = (index as u32) % 3;
let dy = (index as u32) / 3;
let position = TilePos {
x: top_left.x + dx,
y: top_left.y - dy,
};
match desc {
Some((texture_index, flip)) => {
let tile_entity = commands
.spawn(TileBundle {
position,
tilemap_id,
texture_index: TileTextureIndex(*texture_index),
flip: *flip,
..Default::default()
})
.id();
tile_storage.set(&position, tile_entity);
}
None => {
tile_storage.remove(&position);
}
};
});
}

40
src/game/mod.rs Normal file
View File

@@ -0,0 +1,40 @@
use bevy::prelude::*;
pub mod map;
pub mod player;
/// Data component for info about the player's current set of cards.
///
/// [`Self::capacity`] is the maximum hand size.
///
/// [`Self::low_water_mark`] is the threshold for drawing new cards on-room-enter.
#[derive(Component)]
pub struct PlayerHand {
cards: Vec<Entity>,
capacity: u8,
low_water_mark: u8,
}
pub mod machines {
use bevy::prelude::*;
#[derive(Component)]
pub struct CuttingMachine;
#[derive(Component)]
pub struct RotatingMachine;
#[derive(Component)]
pub struct FlippingMachine;
#[derive(Component)]
pub struct TransposingMachine;
}
pub mod consumables {
use bevy::prelude::*;
#[derive(Component)]
pub struct Fuel(u32);
}

28
src/game/player.rs Normal file
View File

@@ -0,0 +1,28 @@
use bevy::prelude::*;
#[derive(Component)]
#[require(Transform)]
pub struct Player;
pub fn spawn_player(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
let shape = Circle::new(12.0);
let mesh = meshes.add(shape);
let color = Color::srgb(1., 0., 0.);
let material = materials.add(color);
commands.spawn((
Player,
Mesh2d(mesh),
MeshMaterial2d(material),
Transform::from_translation(Vec3 {
x: 72.,
y: 72.,
z: 1.,
}),
));
}

View File

@@ -1,8 +1,16 @@
use bevy::{prelude::*, window::WindowResolution}; use bevy::{color::palettes::css::GREEN, prelude::*, window::WindowResolution};
use bevy_ecs_tilemap::prelude::*;
use bevy_inspector_egui::{bevy_egui::EguiPlugin, quick::WorldInspectorPlugin}; use bevy_inspector_egui::{bevy_egui::EguiPlugin, quick::WorldInspectorPlugin};
use crate::{
game::machines::{CuttingMachine, RotatingMachine},
resources::UiTheme,
};
mod assets;
mod card; mod card;
mod constants; mod game;
mod resources;
mod widgets; mod widgets;
fn main() { fn main() {
@@ -16,5 +24,68 @@ fn main() {
})) }))
.add_plugins(EguiPlugin::default()) .add_plugins(EguiPlugin::default())
.add_plugins(WorldInspectorPlugin::new()) .add_plugins(WorldInspectorPlugin::new())
.add_plugins(widgets::GameUiPlugin)
.add_plugins(TilemapPlugin)
.init_resource::<resources::UiTheme>()
.register_type::<resources::UiTheme>()
.add_systems(
Startup,
(
setup,
assets::load_assets,
game::map::setup_test_tilemap,
dummy_machine,
)
.chain(),
)
.run(); .run();
} }
fn setup(mut commands: Commands) {
commands.spawn((
Camera2d,
Projection::Orthographic(OrthographicProjection {
scaling_mode: bevy::render::camera::ScalingMode::AutoMin {
min_width: 360.0,
min_height: 240.0,
},
..OrthographicProjection::default_2d()
}),
Transform::from_translation(Vec3 {
x: 72.,
y: 72.,
z: 0.,
}),
));
}
/// Generic utility for despawning entities that have a given component.
fn despawn<T: Component>(mut commands: Commands, to_despawn: Query<Entity, With<T>>) {
for entity in to_despawn {
commands.entity(entity).despawn();
}
}
fn dummy_machine(
mut commands: Commands,
// mut meshes: ResMut<Assets<Mesh>>,
// mut materials: ResMut<Assets<ColorMaterial>>,
) {
commands
.spawn((
Sprite::from_color(GREEN, Vec2::splat(40.0)),
// WARN: Mesh picking is not part of the `DefaultPlugins` plugin collection!
// (but sprite picking is!)
// Mesh2d(meshes.add(Rectangle::new(25., 25.))),
// MeshMaterial2d(materials.add(ColorMaterial::from_color(GREEN))),
Transform::default(),
Pickable::default(),
))
.observe(
|event: Trigger<Pointer<Click>>, commands: Commands, theme: Res<UiTheme>| {
let entity = event.target;
CuttingMachine::spawn_ui(commands, theme);
},
);
}

58
src/resources.rs Normal file
View File

@@ -0,0 +1,58 @@
//! Program constants & defaults
use bevy::{
color::palettes::{css::*, tailwind::SLATE_100},
prelude::*,
};
#[derive(Debug, Reflect, Resource)]
#[reflect(Resource)]
pub struct UiTheme {
// Colors for the machine UI panes
// (and others, but we're not there yet)
pub pane_bg: Color,
// Colors for the "Big Red Buttons" (the main actions of the machines)
// normal
pub brb_bg: Color,
pub brb_border: Color,
// hover
pub brb_hover_bg: Color,
pub brb_hover_border: Color,
// pressed
pub brb_pressed_bg: Color,
pub brb_pressed_border: Color,
// Colors for low-priority buttons
// normal
pub quiet_bg: Color,
pub quiet_border: Color,
// hover
pub quiet_hover_bg: Color,
pub quiet_hover_border: Color,
// pressed
pub quiet_pressed_bg: Color,
pub quiet_pressed_border: Color,
}
impl FromWorld for UiTheme {
fn from_world(_world: &mut World) -> Self {
Self {
pane_bg: SLATE_100.into(),
brb_bg: RED.into(),
brb_border: DARK_RED.into(),
brb_hover_bg: PINK.into(),
brb_hover_border: RED.into(),
brb_pressed_bg: GREEN.into(),
brb_pressed_border: DARK_GREEN.into(),
quiet_bg: GRAY.into(),
quiet_border: DARK_GRAY.into(),
quiet_hover_bg: DARK_GRAY.into(),
quiet_hover_border: LIGHT_GRAY.into(),
quiet_pressed_bg: LIGHT_GRAY.into(),
quiet_pressed_border: GRAY.into(),
}
}
}

View File

@@ -1 +0,0 @@
//! Catch-all location for UI bits

95
src/widgets/machines.rs Normal file
View File

@@ -0,0 +1,95 @@
use bevy::{color::palettes::css::*, prelude::*, ui::Val::*};
use crate::{
game::machines::*,
resources::UiTheme,
widgets::{BigRedButton, machine_ui_base},
};
impl CuttingMachine {
pub fn spawn_ui(mut commands: Commands, theme: Res<UiTheme>) {
let base_entity = machine_ui_base(&mut commands, "Cutting Machine", &theme);
commands.entity(base_entity).with_children(|commands| {
// Left panel. For fuel or machine stats or whatever.
commands.spawn((
Node {
padding: UiRect::all(Px(10.0)),
..default()
},
BackgroundColor(GREEN.into()),
Pickable::default(),
children![(Text::new("Uses: <n>"), TextColor(BLACK.into()),)],
));
// Center panel (placeholder for the Card view)
commands.spawn((
Node::default(),
BackgroundColor(BLUE.into()),
Pickable::default(),
children![(
Text::new("Card cut view placeholder"),
TextColor(MAGENTA.into()),
TextShadow::default(),
),],
));
// Right panel for the "CUT" button
commands
.spawn((
Node {
align_items: AlignItems::End,
..Default::default()
},
BackgroundColor(DARK_GRAY.into()),
Pickable::default(),
))
.with_children(|cmds| {
let _button_cmds = cmds.spawn(BigRedButton::bundle("CUT"));
// TODO: Attach on-press observer so this machine can do something
// in response to that button being pressed
});
});
}
}
impl RotatingMachine {
pub fn spawn_ui(mut commands: Commands, theme: Res<UiTheme>) {
let base_entity = machine_ui_base(&mut commands, "Rotating Machine", &theme);
commands.entity(base_entity).with_children(|commands| {
commands.spawn((
Node {
padding: UiRect::all(Px(10.0)),
..Default::default()
},
BackgroundColor(GREEN.into()),
Pickable::default(),
children![(Text::new("Uses: <n>"), TextColor(BLACK.into()))],
));
// Center panel (placeholder for input-output rotation)
commands.spawn((
Node::default(),
BackgroundColor(BLUE.into()),
Pickable::default(),
children![
Text::new("Card rotation side-by-side placeholder"),
TextColor(MAGENTA.into()),
],
));
// Right panel for the rotation controls
commands
.spawn((
Node {
align_items: AlignItems::End,
..Default::default()
},
BackgroundColor(DARK_GRAY.into()),
Pickable::default(),
))
.with_children(|cmds| {
let _button_cmds = cmds.spawn(BigRedButton::bundle("TURN"));
// TODO: Attach on-press observer to the button.
});
});
}
}

265
src/widgets/mod.rs Normal file
View File

@@ -0,0 +1,265 @@
//! Catch-all location for UI bits
pub mod machines;
use bevy::{color::palettes::css::*, prelude::*, ui::Val::*};
use crate::resources::UiTheme;
/// Plugin to set up systems & resources for the custom UI elements
///
/// The UiTheme resource is initialized and the button press Observers
/// are registered here.
pub struct GameUiPlugin;
impl Plugin for GameUiPlugin {
fn build(&self, app: &mut App) {
app
.init_resource::<UiTheme>()
.register_type::<UiTheme>()
.add_observer(CloseButton::hover_start)
.add_observer(CloseButton::hover_stop)
.add_observer(CloseButton::press_start)
.add_observer(CloseButton::press_stop)
.add_observer(BigRedButton::button_hover_start)
.add_observer(BigRedButton::button_hover_stop)
.add_observer(BigRedButton::button_press_start)
.add_observer(BigRedButton::button_press_stop);
}
}
/// The base panel for the machines that manipulate the room cards.
///
/// This function is not a valid Bevy System because of how the Commands struct
/// is passed through. Users are meant to call this *from* a System to create a
/// base UI Node. That system then re-acquires an [`EntityCommands`] and adds
/// child nodes to fill out the panel.
fn machine_ui_base(commands: &mut Commands, header: impl Into<String>, theme: &UiTheme) -> Entity {
let root_pane = commands
.spawn((
Node {
// Position & size
position_type: PositionType::Relative,
width: Percent(60.0),
height: Percent(60.0),
top: Percent(20.0),
left: Percent(20.0),
// 5x5 grid, padding & gutters, etc
aspect_ratio: Some(1.0),
display: Display::Grid,
padding: UiRect::all(Val::Px(10.0)),
grid_template_columns: vec![
GridTrack::min_content(),
GridTrack::flex(1.0),
GridTrack::min_content(),
],
grid_template_rows: vec![GridTrack::min_content(), GridTrack::flex(1.0)],
row_gap: Val::Px(5.0),
column_gap: Val::Px(5.0),
..default()
},
BackgroundColor(theme.pane_bg),
BorderRadius::all(Percent(2.0)),
children![(
// TODO: A real node with stuff in it (buttons, maybe?)
Node {
justify_content: JustifyContent::Center,
grid_column: GridPlacement::span(2),
..default()
},
BackgroundColor(RED.into()),
Pickable::default(),
children![(
Text::new(header),
TextColor(BLACK.into()),
// TODO: Text shadow, maybe. I couldn't make it look good.
)],
),],
))
.id();
commands.entity(root_pane).with_children(|cmds| {
cmds.spawn(CloseButton::bundle(root_pane));
});
root_pane
}
/// The "Big Red Button" that makes a machine perform it's action.
#[derive(Component)]
pub struct BigRedButton;
impl BigRedButton {
/// Default bundle for a Big Red Button. Remember to attach on-press observers!
///
/// I haven't figure out what will receive the on-press events, so I'm moving
/// the problem. It will not be the button's job to hook up the event notice.
///
/// TODO: Pass in the UiTheme struct
fn bundle(text: impl Into<String>) -> impl Bundle {
(
// TODO: Remove `Button`? Add `Button` to the `CloseButton` bundle?
Button,
BigRedButton,
Node {
width: Px(60.0),
height: Px(60.0),
aspect_ratio: Some(1.0),
// Why is it "align_items" to center the text vertically,
// but "justify_*content*" to center it horizontally?
align_items: AlignItems::Center,
// align_content: AlignContent::Center,
justify_content: JustifyContent::Center,
border: UiRect::all(Px(2.0)),
..Default::default()
},
BackgroundColor(RED.into()),
BorderColor(DARK_RED.into()),
BorderRadius::MAX,
children![
Text::new(text),
TextColor(WHITE.into()),
TextShadow::default(),
],
)
}
/// Re-color the button when a pointer passes over it
fn button_hover_start(
event: Trigger<Pointer<Over>>,
// Get button background and border colors so we can change them.
// Filter for *changed* interactions, and only entities with a [`Button`]
mut button_colors: Query<(&mut BackgroundColor, &mut BorderColor), With<BigRedButton>>,
ui_theme: Res<UiTheme>,
) {
// Get the components for only the Trigger's target entity
if let Ok((mut bg, mut border)) = button_colors.get_mut(event.target()) {
bg.0 = ui_theme.brb_hover_bg;
border.0 = ui_theme.brb_hover_border;
}
}
// TODO: Consolidate these with the help of a NewType enum and `trigger_map()`
// see: https://github.com/bevyengine/bevy/issues/14649
fn button_hover_stop(
event: Trigger<Pointer<Out>>,
mut button_colors: Query<(&mut BackgroundColor, &mut BorderColor), With<BigRedButton>>,
ui_theme: Res<UiTheme>,
) {
if let Ok((mut bg, mut border)) = button_colors.get_mut(event.target()) {
bg.0 = ui_theme.brb_bg;
border.0 = ui_theme.brb_border;
}
}
fn button_press_start(
event: Trigger<Pointer<Pressed>>,
mut button_colors: Query<(&mut BackgroundColor, &mut BorderColor), With<BigRedButton>>,
ui_theme: Res<UiTheme>,
) {
if let Ok((mut bg, mut border)) = button_colors.get_mut(event.target()) {
bg.0 = ui_theme.brb_pressed_bg;
border.0 = ui_theme.brb_pressed_border;
}
}
fn button_press_stop(
event: Trigger<Pointer<Released>>,
mut button_colors: Query<(&mut BackgroundColor, &mut BorderColor), With<BigRedButton>>,
ui_theme: Res<UiTheme>,
) {
if let Ok((mut bg, mut border)) = button_colors.get_mut(event.target()) {
bg.0 = ui_theme.brb_bg;
border.0 = ui_theme.brb_border;
}
}
}
/// Button marker for closing (despawning) an in-game menu entity.
#[derive(Component)]
#[require(Button)]
pub struct CloseButton(Entity);
impl CloseButton {
/// Default bundle for a panel's close button. Pass in the entity to despawn.
///
/// TODO: Pass in the UiTheme struct
fn bundle(target: Entity) -> impl Bundle {
(
// TODO: Add `Button`? Remove `Button` from the BigRedButton bundle?
CloseButton(target),
Node {
width: Px(20.0),
height: Px(20.0),
aspect_ratio: Some(1.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
border: UiRect::all(Px(1.0)),
..Default::default()
},
// TODO: Get colors *somehow.*
// - Turn this function into a Bevy System, require `Res<...>`
// - Just pass in the colors, let the caller figure out sourcing them.
BackgroundColor(GRAY.into()),
BorderColor(DARK_GRAY.into()),
BorderRadius::all(Px(2.0)),
children![(
// TODO: Replace with an icon/sprite
Text::new("X"),
TextColor(BLACK.into()),
)],
)
}
fn hover_start(
event: Trigger<Pointer<Over>>,
// Get button background and border colors so we can change them.
// Filter for *changed* interactions, and only entities with a [`Button`]
mut button_colors: Query<(&mut BackgroundColor, &mut BorderColor), With<CloseButton>>,
ui_theme: Res<UiTheme>,
) {
// Get the components for only the Trigger's target entity
if let Ok((mut bg, mut border)) = button_colors.get_mut(event.target()) {
bg.0 = ui_theme.quiet_hover_bg;
border.0 = ui_theme.quiet_hover_border;
}
}
fn hover_stop(
event: Trigger<Pointer<Out>>,
mut button_colors: Query<(&mut BackgroundColor, &mut BorderColor), With<CloseButton>>,
ui_theme: Res<UiTheme>,
) {
if let Ok((mut bg, mut border)) = button_colors.get_mut(event.target()) {
bg.0 = ui_theme.quiet_bg;
border.0 = ui_theme.quiet_border;
}
}
fn press_start(
event: Trigger<Pointer<Pressed>>,
mut button_colors: Query<(&mut BackgroundColor, &mut BorderColor), With<CloseButton>>,
ui_theme: Res<UiTheme>,
) {
if let Ok((mut bg, mut border)) = button_colors.get_mut(event.target()) {
bg.0 = ui_theme.quiet_pressed_bg;
border.0 = ui_theme.quiet_pressed_border;
}
}
fn press_stop(
event: Trigger<Pointer<Released>>,
mut commands: Commands,
mut button_colors: Query<(&mut BackgroundColor, &mut BorderColor, &CloseButton)>,
ui_theme: Res<UiTheme>,
) {
if let Ok((mut bg, mut border, CloseButton(target))) = button_colors.get_mut(event.target()) {
bg.0 = ui_theme.quiet_bg;
border.0 = ui_theme.quiet_border;
commands.entity(*target).despawn();
}
}
}