9 Commits

Author SHA1 Message Date
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
14 changed files with 205 additions and 595 deletions

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 B

View File

Before

Width:  |  Height:  |  Size: 317 B

After

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 B

View File

Before

Width:  |  Height:  |  Size: 159 B

After

Width:  |  Height:  |  Size: 159 B

View File

@@ -2,14 +2,14 @@ use bevy::prelude::*;
use std::path::Path;
pub struct TileImage {
pub name: &'static str,
pub thumbnail_handle: Handle<Image>,
pub tile_handle: Handle<Image>,
name: &'static str,
thumbnail_handle: Handle<Image>,
tile_handle: Handle<Image>,
}
#[derive(Resource)]
pub struct AssetLibrary {
pub tile_images: Vec<TileImage>,
tile_images: Vec<TileImage>,
}
impl AssetLibrary {
@@ -33,8 +33,7 @@ impl AssetLibrary {
"corner-sw-3-walls",
"full-0-walls",
"full-1-wall",
"full-2-walls-corner",
"full-2-walls-hallway",
"full-2-walls",
"full-3-walls",
"full-4-walls",
];
@@ -60,15 +59,6 @@ impl AssetLibrary {
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

View File

@@ -14,25 +14,6 @@ pub enum Cell {
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)]
pub enum CutLine {
VertLeft,
@@ -70,36 +51,18 @@ pub enum RotationDir {
/// *or may not* be valid, yet.
#[derive(Clone, Copy, Component, Debug, Default, PartialEq)]
pub struct Card {
pub cells: [Cell; 9],
pub nw: bool,
pub n: bool,
pub ne: bool,
pub w: bool,
pub e: bool,
pub sw: bool,
pub s: bool,
pub se: bool,
cells: [Cell; 9],
nw: bool,
n: bool,
ne: bool,
w: bool,
e: bool,
sw: bool,
s: bool,
se: bool,
}
impl Card {
/// Create a new card from the given Cell array
pub const fn new(cells: [Cell; 9]) -> Self {
Self {
cells,
nw: false,
n: false,
ne: false,
w: false,
e: false,
sw: false,
s: false,
se: false,
}
}
// TODO: The other shapes
pub const OCTAGON: Self = Self::new(OCTAGON);
/// Produces a new card by stacking another on top of this one.
pub fn merge(self, top: Self) -> (Self, Option<Self>) {
let mut new_card = Self::default();

View File

@@ -1,426 +0,0 @@
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::OCTAGON;
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);
}
};
});
}

View File

@@ -1,8 +1,5 @@
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.
@@ -37,4 +34,7 @@ pub mod consumables {
#[derive(Component)]
pub struct Fuel(u32);
#[derive(Event)]
pub struct FuelChanged;
}

View File

@@ -1,28 +0,0 @@
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,10 +1,10 @@
use bevy::{color::palettes::css::GREEN, prelude::*, window::WindowResolution};
use bevy_ecs_tilemap::prelude::*;
use bevy::{color::palettes::css::*, prelude::*, window::WindowResolution};
use bevy_inspector_egui::{bevy_egui::EguiPlugin, quick::WorldInspectorPlugin};
use crate::{
game::machines::{CuttingMachine, RotatingMachine},
game::machines::{CuttingMachine, FlippingMachine, RotatingMachine, TransposingMachine},
resources::UiTheme,
widgets::SpawnUi,
};
mod assets;
@@ -25,38 +25,12 @@ fn main() {
.add_plugins(EguiPlugin::default())
.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(),
)
.add_systems(Startup, (setup, assets::load_assets, dummy_machines))
.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.,
}),
));
commands.spawn(Camera2d);
}
/// Generic utility for despawning entities that have a given component.
@@ -66,26 +40,51 @@ fn despawn<T: Component>(mut commands: Commands, to_despawn: Query<Entity, With<
}
}
fn dummy_machine(
mut commands: Commands,
// mut meshes: ResMut<Assets<Mesh>>,
// mut materials: ResMut<Assets<ColorMaterial>>,
) {
/// 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((
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(),
Transform::from_translation(Vec3::new(40.0, 40.0, 0.0)),
Pickable::default(),
))
.observe(
|event: Trigger<Pointer<Click>>, commands: Commands, theme: Res<UiTheme>| {
let entity = event.target;
CuttingMachine::spawn_ui(commands, theme);
},
);
.observe(spawn_machine_ui::<RotatingMachine>);
// TODO: The other observers, once they have a spawner struct & impl.
commands
.spawn((
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((
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());
}

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

@@ -0,0 +1,44 @@
use bevy::{color::palettes::css::*, prelude::*, ui::Val::*};
use crate::game::consumables::{Fuel, FuelChanged};
#[derive(Component)]
#[require(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);
}
}

View File

@@ -1,25 +1,18 @@
use bevy::{color::palettes::css::*, prelude::*, ui::Val::*};
use bevy::{color::palettes::css::*, prelude::*};
use crate::{
game::machines::*,
game::{consumables::FuelChanged, machines::*},
resources::UiTheme,
widgets::{BigRedButton, machine_ui_base},
widgets::{BigRedButton, SpawnUi, fuel_gauge::FuelGauge, machine_ui_base},
};
impl CuttingMachine {
pub fn spawn_ui(mut commands: Commands, theme: Res<UiTheme>) {
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.
commands.spawn((
Node {
padding: UiRect::all(Px(10.0)),
..default()
},
BackgroundColor(GREEN.into()),
Pickable::default(),
children![(Text::new("Uses: <n>"), TextColor(BLACK.into()),)],
));
// TODO: Pass along target machine, not the UI's root entity.
commands.spawn(FuelGauge::bundle(Entity::PLACEHOLDER));
// Center panel (placeholder for the Card view)
commands.spawn((
@@ -43,7 +36,11 @@ impl CuttingMachine {
Pickable::default(),
))
.with_children(|cmds| {
let _button_cmds = cmds.spawn(BigRedButton::bundle("CUT"));
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
});
@@ -51,19 +48,11 @@ impl CuttingMachine {
}
}
impl RotatingMachine {
pub fn spawn_ui(mut commands: Commands, theme: Res<UiTheme>) {
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((
Node {
padding: UiRect::all(Px(10.0)),
..Default::default()
},
BackgroundColor(GREEN.into()),
Pickable::default(),
children![(Text::new("Uses: <n>"), TextColor(BLACK.into()))],
));
commands.spawn(FuelGauge::bundle(Entity::PLACEHOLDER));
// Center panel (placeholder for input-output rotation)
commands.spawn((
@@ -93,3 +82,73 @@ impl RotatingMachine {
});
}
}
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(Entity::PLACEHOLDER));
// 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(Entity::PLACEHOLDER));
// 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,10 +1,11 @@
//! Catch-all location for UI bits
mod fuel_gauge;
pub mod machines;
use bevy::{color::palettes::css::*, prelude::*, ui::Val::*};
use crate::resources::UiTheme;
use crate::{resources::UiTheme, widgets::fuel_gauge::FuelGauge};
/// Plugin to set up systems & resources for the custom UI elements
///
@@ -24,10 +25,19 @@ impl Plugin for GameUiPlugin {
.add_observer(BigRedButton::button_hover_start)
.add_observer(BigRedButton::button_hover_stop)
.add_observer(BigRedButton::button_press_start)
.add_observer(BigRedButton::button_press_stop);
.add_observer(BigRedButton::button_press_stop)
.add_observer(FuelGauge::detect_fuel_change);
}
}
/// Machines spawn their UI through this trait.
///
/// 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>, machine_id: Entity);
}
/// 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