10 Commits

Author SHA1 Message Date
0c042f9937 Begin work on a reusable on-hover color component
The two buttons ("close" and the "Big Red" button) have identical hover
code, except for which color theme they select. I was about to start
work on the cutting board, particularly it's cut-line selectors, and
realized that would make a 3rd copy of this code.

This commit adds just the background color handler. It seems to work as
expected on the CloseButton. I'll finish removing the old observers once
I have the BorderColor version in place.
2025-08-31 11:35:42 -05:00
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
5 changed files with 125 additions and 35 deletions

View File

@@ -16,16 +16,22 @@ 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;
}
@@ -33,8 +39,13 @@ pub mod consumables {
use bevy::prelude::*;
#[derive(Component)]
pub struct Fuel(u32);
pub struct Fuel(pub u32);
impl Default for Fuel {
fn default() -> Self {
Self(5)
}
}
#[derive(Event)]
pub struct FuelChanged;
}

View File

@@ -55,6 +55,7 @@ fn dummy_machines(mut commands: Commands) {
commands
.spawn((
RotatingMachine,
Sprite::from_color(GREEN, Vec2::splat(40.0)),
Transform::from_translation(Vec3::new(40.0, 40.0, 0.0)),
Pickable::default(),
@@ -64,6 +65,7 @@ fn dummy_machines(mut commands: Commands) {
// 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(),
@@ -72,6 +74,7 @@ fn dummy_machines(mut commands: Commands) {
commands
.spawn((
TransposingMachine,
Sprite::from_color(DARK_ORANGE, Vec2::splat(40.)),
Transform::from_translation(Vec3::new(40.0, -40.0, 0.0)),
Pickable::default(),
@@ -86,5 +89,5 @@ fn spawn_machine_ui<M: SpawnUi>(
) {
let _entity = event.target;
// TODO: Hook up the Fuel component (and spawn one so that I can)
M::spawn_ui(commands, theme);
M::spawn_ui(commands, theme, event.target());
}

View File

@@ -1,9 +1,15 @@
use bevy::{color::palettes::css::*, prelude::*, ui::Val::*};
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 {
@@ -19,14 +25,45 @@ impl FuelGauge {
},
BackgroundColor(GREEN.into()),
Text::new("Fuel: "),
children![TextSpan::new("<n>"),],
)
}
pub fn detect_fuel_change(event: Trigger<FuelChanged>) {
dbg!(format!(
"Detected fuel change on entity ID: {}",
event.target()
));
/// 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);
}
}

View File

@@ -7,12 +7,12 @@ use crate::{
};
impl SpawnUi for CuttingMachine {
fn spawn_ui(mut commands: Commands, theme: Res<UiTheme>) {
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(Entity::PLACEHOLDER));
commands.spawn(FuelGauge::bundle(machine_id));
// Center panel (placeholder for the Card view)
commands.spawn((
@@ -37,19 +37,10 @@ impl SpawnUi for CuttingMachine {
))
.with_children(|cmds| {
let mut button_cmds = cmds.spawn(BigRedButton::bundle("CUT"));
button_cmds.observe(
|trigger: Trigger<Pointer<Click>>,
mut com: Commands,
q: Single<(Entity, &CuttingMachine)>| {
dbg!("Cut button pressed. Triggering a FuelChanged event");
// Feed through the target machine, once the SpawnUi trait is updated
// to require it gets here in the first place.
let cutting_machine = q.0;
com.trigger_targets(FuelChanged, cutting_machine);
},
);
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
});
@@ -58,10 +49,10 @@ impl SpawnUi for CuttingMachine {
}
impl SpawnUi for RotatingMachine {
fn spawn_ui(mut commands: Commands, theme: Res<UiTheme>) {
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(Entity::PLACEHOLDER));
commands.spawn(FuelGauge::bundle(machine_id));
// Center panel (placeholder for input-output rotation)
commands.spawn((
@@ -93,10 +84,10 @@ impl SpawnUi for RotatingMachine {
}
impl SpawnUi for FlippingMachine {
fn spawn_ui(mut commands: Commands, theme: Res<UiTheme>) {
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(Entity::PLACEHOLDER));
commands.spawn(FuelGauge::bundle(machine_id));
// Center panel (placeholder)
commands.spawn((
@@ -128,10 +119,10 @@ impl SpawnUi for FlippingMachine {
}
impl SpawnUi for TransposingMachine {
fn spawn_ui(mut commands: Commands, theme: Res<UiTheme>) {
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(Entity::PLACEHOLDER));
commands.spawn(FuelGauge::bundle(machine_id));
// Center panel (placeholder)
commands.spawn((

View File

@@ -18,6 +18,8 @@ impl Plugin for GameUiPlugin {
app
.init_resource::<UiTheme>()
.register_type::<UiTheme>()
.add_observer(HoverBackgroundcolor::hover_start)
.add_observer(HoverBackgroundcolor::hover_stop)
.add_observer(CloseButton::hover_start)
.add_observer(CloseButton::hover_stop)
.add_observer(CloseButton::press_start)
@@ -30,12 +32,12 @@ impl Plugin for GameUiPlugin {
}
}
/// Anything that can spawn a themed UI should impl this trait.
/// Machines spawn their UI through this trait.
///
/// This exists mainly so that I can write generic functions and have the *compiler*
/// This exists mainly so that I can write generic functions and have the compiler
/// expand the code for me, instead of doing it by hand.
pub trait SpawnUi {
fn spawn_ui(commands: Commands, theme: Res<UiTheme>);
fn spawn_ui(commands: Commands, theme: Res<UiTheme>, machine_id: Entity);
}
/// The base panel for the machines that manipulate the room cards.
@@ -216,6 +218,7 @@ impl CloseButton {
BackgroundColor(GRAY.into()),
BorderColor(DARK_GRAY.into()),
BorderRadius::all(Px(2.0)),
HoverBackgroundcolor::new(GREEN.into()),
children![(
// TODO: Replace with an icon/sprite
Text::new("X"),
@@ -233,7 +236,7 @@ impl CloseButton {
) {
// 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;
// bg.0 = ui_theme.quiet_hover_bg;
border.0 = ui_theme.quiet_hover_border;
}
}
@@ -244,7 +247,7 @@ impl CloseButton {
ui_theme: Res<UiTheme>,
) {
if let Ok((mut bg, mut border)) = button_colors.get_mut(event.target()) {
bg.0 = ui_theme.quiet_bg;
// bg.0 = ui_theme.quiet_bg;
border.0 = ui_theme.quiet_border;
}
}
@@ -273,3 +276,48 @@ impl CloseButton {
}
}
}
/// Background color to use for an item being hovered over.
///
/// Adding this to an entity both sets the color and implicitly enables the
/// on-hover behavior. Without it, the system(s) don't find the entity and so
/// no color changing is exhibited.
#[derive(Component)]
struct HoverBackgroundcolor {
pub color: Color,
// hold onto the original so it can be put back
orig_bg: Option<Color>,
}
impl HoverBackgroundcolor {
fn new(color: Color) -> Self {
Self {
color,
orig_bg: None,
}
}
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 colors: Query<(&mut HoverBackgroundcolor, &mut BackgroundColor)>,
) {
// Get the components for only the Trigger's target entity
if let Ok((mut hover, mut bg)) = colors.get_mut(event.target()) {
hover.orig_bg = Some(bg.0); // store the original
bg.0 = hover.color;
}
}
fn hover_stop(
event: Trigger<Pointer<Out>>,
mut colors: Query<(&HoverBackgroundcolor, &mut BackgroundColor)>,
) {
if let Ok((hover, mut bg)) = colors.get_mut(event.target()) {
bg.0 = hover
.orig_bg
.expect("Tried to restore an original color but there was none!");
}
}
}