37 Commits

Author SHA1 Message Date
ea9055e46c (autoformat) 2025-08-30 09:24:38 -05:00
be9cb3fe69 Fix: pass the machine ID through, not PLACEHOLDER
Oops. Yay for rushed work.
2025-08-30 09:23:59 -05:00
6fb3539329 Use hook on FuelGauge to show text on first spawn
There is an on-add hook that can be used to maintain invariants. I'll
use it to ensure the label text always gets the current fuel value upon
spawning the widget, regardless of whether or not the caller remembers
to set it up themselves.
2025-08-30 09:21:57 -05:00
fb8a27325e Add machine components to the other dummies 2025-08-29 10:08:13 -05:00
4a80b4f4ad Make Fuel a required component on machines
It will be pulled in automatically by Bevy, now. I've also set a default
fuel level of 5 units.
2025-08-29 09:55:16 -05:00
f470687494 Fix Fuel's field visibility, add it to the cutter
*Now* the fuel change events will work (and compile).
2025-08-28 15:35:46 -05:00
ed2e1e75ef Impl the FuelGauge text update logic
This does a linear search through all existing FuelGauges, but we're not
expecting that to be a problem. There should only be zero or one of them
on screen at any moment.

It also doesn't work because the rest of the game state isn't correct...
2025-08-28 15:33:21 -05:00
21c00d4a02 Pass target ID through the cutter's button handler
Use the machine entity ID in the button handler prototype. Now to see
about getting the FuelGauge to show the value after a button press.
2025-08-28 12:27:53 -05:00
5f1f283500 Change trait SpawnUi to require a target entity
The UIs aren't *any* UIs. They are specifically *machine* UIs. They need
to be able to fetch data from components on those machine entities,
which means they need some way to find the machine. Passing an entity ID
is the easiest way, IMO.
2025-08-28 12:27:45 -05:00
8f6dfc3e49 Proof-of-concept for fuel change event handling
Added a `FuelChanged` event which is to be used to trigger Observers
watching for a machine's fuel level change.

I've written a slapdash observer to emit these events when the cutting
machine's big red button is pressed. That observer *must* be replaced
because of how it feeds the machine ID into the UI (it only works when
there is exactly 1 CuttingMachine, which won't be the case).

The dummy machines are missing their machine components. I've added the
`CuttingMachine` component to the cutting machine just to test the event
passing.
2025-08-28 12:01:30 -05:00
e169750923 Fill out UI parts for the remaining machines 2025-08-27 15:27:09 -05:00
c7fd053fd1 Add other machine placeholder sprites 2025-08-27 13:48:55 -05:00
0c254b2ed0 Expand the dummy_machines spawner
Spawn a machine entity for each kind of machine. This is where the trait
impl becomes important -- now the compiler will make copies of that
function for each concrete machine struct so I don't have to.
2025-08-27 13:37:36 -05:00
3b0dad7063 Impl SpawnUi for the machine structs
Just add the `SpawnUi for` text to the impl blocks on the machine
widgets.
2025-08-27 13:35:31 -05:00
04fb8519f6 Trait SpawnUi for things that can spawn a UI
I don't want to copy-paste a bunch of code, so I'm going to get the
compiler to do it for me.
2025-08-27 13:34:31 -05: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
7 changed files with 723 additions and 212 deletions

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 {
@@ -47,7 +49,7 @@ 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], cells: [Cell; 9],
nw: bool, nw: bool,
@@ -616,7 +618,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]

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

@@ -0,0 +1,51 @@
use bevy::prelude::*;
/// 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::*;
use crate::game::consumables::Fuel;
#[derive(Component)]
#[require(Fuel)]
pub struct CuttingMachine;
#[derive(Component)]
#[require(Fuel)]
pub struct RotatingMachine;
#[derive(Component)]
#[require(Fuel)]
pub struct FlippingMachine;
#[derive(Component)]
#[require(Fuel)]
pub struct TransposingMachine;
}
pub mod consumables {
use bevy::prelude::*;
#[derive(Component)]
pub struct Fuel(pub u32);
impl Default for Fuel {
fn default() -> Self {
Self(5)
}
}
#[derive(Event)]
pub struct FuelChanged;
}

View File

@@ -1,8 +1,15 @@
use bevy::{prelude::*, window::WindowResolution}; use bevy::{color::palettes::css::*, prelude::*, window::WindowResolution};
use bevy_inspector_egui::{bevy_egui::EguiPlugin, quick::WorldInspectorPlugin}; use bevy_inspector_egui::{bevy_egui::EguiPlugin, quick::WorldInspectorPlugin};
use crate::{
game::machines::{CuttingMachine, FlippingMachine, RotatingMachine, TransposingMachine},
resources::UiTheme,
widgets::SpawnUi,
};
mod assets; mod assets;
mod card; mod card;
mod game;
mod resources; mod resources;
mod widgets; mod widgets;
@@ -17,16 +24,8 @@ fn main() {
})) }))
.add_plugins(EguiPlugin::default()) .add_plugins(EguiPlugin::default())
.add_plugins(WorldInspectorPlugin::new()) .add_plugins(WorldInspectorPlugin::new())
.init_resource::<resources::UiTheme>() .add_plugins(widgets::GameUiPlugin)
.register_type::<resources::UiTheme>() .add_systems(Startup, (setup, assets::load_assets, dummy_machines))
.add_systems(
Startup,
(
setup,
assets::load_assets,
widgets::spawn_rotator_machine_ui,
),
)
.run(); .run();
} }
@@ -40,3 +39,55 @@ fn despawn<T: Component>(mut commands: Commands, to_despawn: Query<Entity, With<
commands.entity(entity).despawn(); commands.entity(entity).despawn();
} }
} }
/// Dev tool for spawning dummy entities. I'm developing their UIs by hooking
/// up to these while the TileMap gets ready.
fn dummy_machines(mut commands: Commands) {
// Spawn a dummy Cutting Machine
commands
.spawn((
CuttingMachine,
Sprite::from_color(RED, Vec2::splat(40.0)),
Transform::from_translation(Vec3::new(-40.0, 40.0, 0.0)),
Pickable::default(),
))
.observe(spawn_machine_ui::<CuttingMachine>);
commands
.spawn((
RotatingMachine,
Sprite::from_color(GREEN, Vec2::splat(40.0)),
Transform::from_translation(Vec3::new(40.0, 40.0, 0.0)),
Pickable::default(),
))
.observe(spawn_machine_ui::<RotatingMachine>);
// TODO: The other observers, once they have a spawner struct & impl.
commands
.spawn((
FlippingMachine,
Sprite::from_color(PURPLE, Vec2::splat(40.)),
Transform::from_translation(Vec3::new(-40.0, -40.0, 0.0)),
Pickable::default(),
))
.observe(spawn_machine_ui::<FlippingMachine>);
commands
.spawn((
TransposingMachine,
Sprite::from_color(DARK_ORANGE, Vec2::splat(40.)),
Transform::from_translation(Vec3::new(40.0, -40.0, 0.0)),
Pickable::default(),
))
.observe(spawn_machine_ui::<TransposingMachine>);
}
fn spawn_machine_ui<M: SpawnUi>(
event: Trigger<Pointer<Click>>,
commands: Commands,
theme: Res<UiTheme>,
) {
let _entity = event.target;
// TODO: Hook up the Fuel component (and spawn one so that I can)
M::spawn_ui(commands, theme, event.target());
}

View File

@@ -1,11 +1,16 @@
//! Program constants & defaults //! Program constants & defaults
use bevy::{color::palettes::css::*, prelude::*}; use bevy::{
color::palettes::{css::*, tailwind::SLATE_100},
prelude::*,
};
#[derive(Debug, Reflect, Resource)] #[derive(Debug, Reflect, Resource)]
#[reflect(Resource)] #[reflect(Resource)]
pub struct UiTheme { pub struct UiTheme {
// TODO: Panes // 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) // Colors for the "Big Red Buttons" (the main actions of the machines)
// normal // normal
@@ -17,17 +22,37 @@ pub struct UiTheme {
// pressed // pressed
pub brb_pressed_bg: Color, pub brb_pressed_bg: Color,
pub brb_pressed_border: 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 { impl FromWorld for UiTheme {
fn from_world(world: &mut World) -> Self { fn from_world(_world: &mut World) -> Self {
Self { Self {
pane_bg: SLATE_100.into(),
brb_bg: RED.into(), brb_bg: RED.into(),
brb_border: DARK_RED.into(), brb_border: DARK_RED.into(),
brb_hover_bg: PINK.into(), brb_hover_bg: PINK.into(),
brb_hover_border: RED.into(), brb_hover_border: RED.into(),
brb_pressed_bg: GREEN.into(), brb_pressed_bg: GREEN.into(),
brb_pressed_border: DARK_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(),
} }
} }
} }

69
src/widgets/fuel_gauge.rs Normal file
View File

@@ -0,0 +1,69 @@
use bevy::{
color::palettes::css::*,
ecs::{component::HookContext, world::DeferredWorld},
prelude::*,
ui::Val::*,
};
use crate::game::consumables::{Fuel, FuelChanged};
#[derive(Component)]
#[require(Label)]
#[component(on_add = FuelGauge::refresh_label)]
pub struct FuelGauge(Entity);
impl FuelGauge {
/// Default bundle for a machine's fuel gauge widget. Give it the ID of the
/// machine whose info it is displaying.
pub fn bundle(target: Entity) -> impl Bundle {
(
FuelGauge(target),
Node {
min_width: Px(20.0),
min_height: Px(10.0),
..default()
},
BackgroundColor(GREEN.into()),
Text::new("Fuel: "),
)
}
/// Observer system to update the [`FuelGauge`] widget when a machine emits a
/// [`FuelChanged`] event.
pub fn detect_fuel_change(
event: Trigger<FuelChanged>,
fuel_levels: Query<&Fuel>,
mut gauges: Query<(&FuelGauge, &mut Text)>,
) {
// Find the FuelGauge that references the same Entity as the event target.
// That gauge's `Text` is the one to update.
let (_, mut label) = gauges
.iter_mut()
.find(|(gauge, _label)| gauge.0 == event.target())
.expect("Couldn't find any fuel gauges");
let fuel = fuel_levels.get(event.target()).expect(
"Logic error: a `FuelChanged` event was targetted at an entity with no `Fuel` component.",
);
label.0 = format!("Fuel: {}", fuel.0);
}
fn refresh_label(mut world: DeferredWorld, ctx: HookContext) {
// Get the machine ID from the FuelGauge component.
let Some(machine_id) = world.get::<FuelGauge>(ctx.entity).map(|fg| fg.0) else {
panic!("Couldn't get a FuelGauge during it's on-add hook run!");
};
// and fuel level
let Some(fuel) = world.get::<Fuel>(machine_id).map(|lvl| lvl.0) else {
// TODO: Maybe don't panic and just emit a warning.
panic!("Couldn't get a Fuel for a machine during a FuelGauge on-add hook.");
};
// Get an excl ref to the Text component so we can redraw its text
let Some(mut label) = world.get_mut::<Text>(ctx.entity) else {
panic!()
};
label.0 = format!("Fuel: {}", fuel);
}
}

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

@@ -0,0 +1,154 @@
use bevy::{color::palettes::css::*, prelude::*};
use crate::{
game::{consumables::FuelChanged, machines::*},
resources::UiTheme,
widgets::{BigRedButton, SpawnUi, fuel_gauge::FuelGauge, machine_ui_base},
};
impl SpawnUi for CuttingMachine {
fn spawn_ui(mut commands: Commands, theme: Res<UiTheme>, machine_id: Entity) {
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.
// TODO: Pass along target machine, not the UI's root entity.
commands.spawn(FuelGauge::bundle(machine_id));
// 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 mut button_cmds = cmds.spawn(BigRedButton::bundle("CUT"));
button_cmds.observe(move |trigger: Trigger<Pointer<Click>>, mut com: Commands| {
dbg!("Cut button pressed. Triggering a FuelChanged event");
com.trigger_targets(FuelChanged, machine_id);
});
// TODO: Attach on-press observer so this machine can do something
// in response to that button being pressed
});
});
}
}
impl SpawnUi for RotatingMachine {
fn spawn_ui(mut commands: Commands, theme: Res<UiTheme>, machine_id: Entity) {
let base_entity = machine_ui_base(&mut commands, "Rotating Machine", &theme);
commands.entity(base_entity).with_children(|commands| {
commands.spawn(FuelGauge::bundle(machine_id));
// 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.
});
});
}
}
impl SpawnUi for FlippingMachine {
fn spawn_ui(mut commands: Commands, theme: Res<UiTheme>, machine_id: Entity) {
let base_entity = machine_ui_base(&mut commands, "Flipping Machine", &theme);
commands.entity(base_entity).with_children(|commands| {
commands.spawn(FuelGauge::bundle(machine_id));
// Center panel (placeholder)
commands.spawn((
Node::default(),
BackgroundColor(BLUE.into()),
Pickable::default(),
children![
Text::new("Card Flipping placeholder"),
TextColor(MAGENTA.into()),
],
));
// Right panel go 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("FLIP"));
// TODO: Attach on-press observer to the button.
});
});
}
}
impl SpawnUi for TransposingMachine {
fn spawn_ui(mut commands: Commands, theme: Res<UiTheme>, machine_id: Entity) {
let base_entity = machine_ui_base(&mut commands, "Transposing Machine", &theme);
commands.entity(base_entity).with_children(|commands| {
commands.spawn(FuelGauge::bundle(machine_id));
// Center panel (placeholder)
commands.spawn((
Node::default(),
BackgroundColor(BLUE.into()),
Pickable::default(),
children![
Text::new("Card Transposition placeholder"),
TextColor(MAGENTA.into()),
],
));
// Right panel go 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("SWAP"));
// TODO: Attach on-press observer to the button.
});
});
}
}

View File

@@ -1,95 +1,52 @@
//! Catch-all location for UI bits //! Catch-all location for UI bits
use bevy::{ mod fuel_gauge;
color::palettes::{css::*, tailwind::*}, pub mod machines;
prelude::*,
ui::Val::*,
};
use crate::resources::UiTheme; use bevy::{color::palettes::css::*, prelude::*, ui::Val::*};
pub fn spawn_cutter_machine_ui(mut commands: Commands) { use crate::{resources::UiTheme, widgets::fuel_gauge::FuelGauge};
commands
.spawn((machine_ui_base("Cutting Machine"),))
.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) /// Plugin to set up systems & resources for the custom UI elements
commands.spawn(( ///
Node::default(), /// The UiTheme resource is initialized and the button press Observers
BackgroundColor(BLUE.into()), /// are registered here.
Pickable::default(), pub struct GameUiPlugin;
children![(
Text::new("Card cut view placeholder"), impl Plugin for GameUiPlugin {
TextColor(MAGENTA.into()), fn build(&self, app: &mut App) {
TextShadow::default(), app
),], .init_resource::<UiTheme>()
)); .register_type::<UiTheme>()
// Right panel for the "CUT" button .add_observer(CloseButton::hover_start)
commands .add_observer(CloseButton::hover_stop)
.spawn(( .add_observer(CloseButton::press_start)
Node { .add_observer(CloseButton::press_stop)
align_items: AlignItems::End, .add_observer(BigRedButton::button_hover_start)
..Default::default() .add_observer(BigRedButton::button_hover_stop)
}, .add_observer(BigRedButton::button_press_start)
BackgroundColor(DARK_GRAY.into()), .add_observer(BigRedButton::button_press_stop)
Pickable::default(), .add_observer(FuelGauge::detect_fuel_change);
)) }
.with_children(|cmds| spawn_machine_button(cmds, "CUT"));
});
} }
pub fn spawn_rotator_machine_ui(mut commands: Commands) { /// Machines spawn their UI through this trait.
commands ///
.spawn((machine_ui_base("Rotating Machine"),)) /// This exists mainly so that I can write generic functions and have the compiler
.with_children(|commands| { /// expand the code for me, instead of doing it by hand.
commands.spawn(( pub trait SpawnUi {
Node { fn spawn_ui(commands: Commands, theme: Res<UiTheme>, machine_id: Entity);
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| spawn_machine_button(cmds, "TURN"));
});
} }
/// The base panel for the machines that manipulate the room cards. /// The base panel for the machines that manipulate the room cards.
fn machine_ui_base(header: impl Into<String>) -> impl Bundle { ///
( /// 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 { Node {
// Position & size // Position & size
position_type: PositionType::Relative, position_type: PositionType::Relative,
@@ -113,13 +70,13 @@ fn machine_ui_base(header: impl Into<String>) -> impl Bundle {
..default() ..default()
}, },
BackgroundColor(SLATE_100.into()), BackgroundColor(theme.pane_bg),
BorderRadius::all(Percent(2.0)), BorderRadius::all(Percent(2.0)),
children![( children![(
// TODO: A real node with stuff in it (buttons, maybe?) // TODO: A real node with stuff in it (buttons, maybe?)
Node { Node {
justify_content: JustifyContent::Center, justify_content: JustifyContent::Center,
grid_column: GridPlacement::span(3), grid_column: GridPlacement::span(2),
..default() ..default()
}, },
BackgroundColor(RED.into()), BackgroundColor(RED.into()),
@@ -129,14 +86,32 @@ fn machine_ui_base(header: impl Into<String>) -> impl Bundle {
TextColor(BLACK.into()), TextColor(BLACK.into()),
// TODO: Text shadow, maybe. I couldn't make it look good. // 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
} }
// TODO: Hook up action handling (callback? Observer? Some other weird component?) /// The "Big Red Button" that makes a machine perform it's action.
fn spawn_machine_button(commands: &mut ChildSpawnerCommands, text: impl Into<String>) { #[derive(Component)]
let mut builder = commands.spawn(( 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, Button,
BigRedButton,
Node { Node {
width: Px(60.0), width: Px(60.0),
height: Px(60.0), height: Px(60.0),
@@ -158,60 +133,143 @@ fn spawn_machine_button(commands: &mut ChildSpawnerCommands, text: impl Into<Str
TextColor(WHITE.into()), TextColor(WHITE.into()),
TextShadow::default(), TextShadow::default(),
], ],
)); )
builder.observe(button_hover_start); }
builder.observe(button_hover_stop);
builder.observe(button_press_start);
builder.observe(button_press_stop);
}
/// Re-color the button when a pointer passes over it /// Re-color the button when a pointer passes over it
fn button_hover_start( fn button_hover_start(
event: Trigger<Pointer<Over>>, event: Trigger<Pointer<Over>>,
// Get button background and border colors so we can change them. // Get button background and border colors so we can change them.
// Filter for *changed* interactions, and only entities with a [`Button`] // Filter for *changed* interactions, and only entities with a [`Button`]
mut button_colors: Query<(&mut BackgroundColor, &mut BorderColor), With<Button>>, mut button_colors: Query<(&mut BackgroundColor, &mut BorderColor), With<BigRedButton>>,
ui_theme: Res<UiTheme>, ui_theme: Res<UiTheme>,
) { ) {
// Get the components for only the Trigger's target entity // Get the components for only the Trigger's target entity
if let Ok((mut bg, mut border)) = button_colors.get_mut(event.target()) { if let Ok((mut bg, mut border)) = button_colors.get_mut(event.target()) {
bg.0 = ui_theme.brb_hover_bg; bg.0 = ui_theme.brb_hover_bg;
border.0 = ui_theme.brb_hover_border; border.0 = ui_theme.brb_hover_border;
} }
} }
// TODO: Consolidate these with the help of a NewType enum and `trigger_map()` // TODO: Consolidate these with the help of a NewType enum and `trigger_map()`
// see: https://github.com/bevyengine/bevy/issues/14649 // see: https://github.com/bevyengine/bevy/issues/14649
fn button_hover_stop( fn button_hover_stop(
event: Trigger<Pointer<Out>>, event: Trigger<Pointer<Out>>,
mut button_colors: Query<(&mut BackgroundColor, &mut BorderColor), With<Button>>, mut button_colors: Query<(&mut BackgroundColor, &mut BorderColor), With<BigRedButton>>,
ui_theme: Res<UiTheme>, ui_theme: Res<UiTheme>,
) { ) {
if let Ok((mut bg, mut border)) = button_colors.get_mut(event.target()) { if let Ok((mut bg, mut border)) = button_colors.get_mut(event.target()) {
bg.0 = ui_theme.brb_bg; bg.0 = ui_theme.brb_bg;
border.0 = ui_theme.brb_border; border.0 = ui_theme.brb_border;
} }
} }
fn button_press_start( fn button_press_start(
event: Trigger<Pointer<Pressed>>, event: Trigger<Pointer<Pressed>>,
mut button_colors: Query<(&mut BackgroundColor, &mut BorderColor), With<Button>>, mut button_colors: Query<(&mut BackgroundColor, &mut BorderColor), With<BigRedButton>>,
ui_theme: Res<UiTheme>, ui_theme: Res<UiTheme>,
) { ) {
if let Ok((mut bg, mut border)) = button_colors.get_mut(event.target()) { if let Ok((mut bg, mut border)) = button_colors.get_mut(event.target()) {
bg.0 = ui_theme.brb_pressed_bg; bg.0 = ui_theme.brb_pressed_bg;
border.0 = ui_theme.brb_pressed_border; border.0 = ui_theme.brb_pressed_border;
} }
} }
fn button_press_stop( fn button_press_stop(
event: Trigger<Pointer<Released>>, event: Trigger<Pointer<Released>>,
mut button_colors: Query<(&mut BackgroundColor, &mut BorderColor), With<Button>>, mut button_colors: Query<(&mut BackgroundColor, &mut BorderColor), With<BigRedButton>>,
ui_theme: Res<UiTheme>, ui_theme: Res<UiTheme>,
) { ) {
if let Ok((mut bg, mut border)) = button_colors.get_mut(event.target()) { if let Ok((mut bg, mut border)) = button_colors.get_mut(event.target()) {
bg.0 = ui_theme.brb_bg; bg.0 = ui_theme.brb_bg;
border.0 = ui_theme.brb_border; 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();
}
}
} }