add tower selection UI, cancellable actions, and pause UI

This commit is contained in:
Jim 2025-08-31 12:08:44 -04:00
parent 138a524abd
commit 918b4884b2
Signed by: jim
GPG key ID: 3D7D94BA53088BF4
18 changed files with 468 additions and 23 deletions

293
Cargo.lock generated
View file

@ -710,12 +710,14 @@ dependencies = [
"bevy_input",
"bevy_log",
"bevy_math",
"bevy_picking",
"bevy_platform",
"bevy_reflect",
"bevy_render",
"bevy_tasks",
"bevy_time",
"bevy_transform",
"bevy_ui",
"bevy_window",
"bevy_winit",
"bytemuck",
@ -728,6 +730,7 @@ dependencies = [
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webbrowser",
"wgpu-types",
"winit",
]
@ -2172,6 +2175,17 @@ dependencies = [
"objc2 0.6.2",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "disqualified"
version = "1.0.0"
@ -2494,6 +2508,15 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
name = "form_urlencoded"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
"percent-encoding",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
@ -2830,6 +2853,113 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]]
name = "icu_collections"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
dependencies = [
"displaydoc",
"potential_utf",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locale_core"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_normalizer"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
dependencies = [
"displaydoc",
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
[[package]]
name = "icu_properties"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
dependencies = [
"displaydoc",
"icu_collections",
"icu_locale_core",
"icu_properties_data",
"icu_provider",
"potential_utf",
"zerotrie",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
[[package]]
name = "icu_provider"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
dependencies = [
"displaydoc",
"icu_locale_core",
"stable_deref_trait",
"tinystr",
"writeable",
"yoke",
"zerofrom",
"zerotrie",
"zerovec",
]
[[package]]
name = "idna"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [
"idna_adapter",
"smallvec",
"utf8_iter",
]
[[package]]
name = "idna_adapter"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
dependencies = [
"icu_normalizer",
"icu_properties",
]
[[package]]
name = "image"
version = "0.25.6"
@ -3041,6 +3171,7 @@ version = "0.1.0"
dependencies = [
"avian3d",
"bevy",
"bevy_egui",
"building",
"camera",
"combat",
@ -3115,6 +3246,12 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]]
name = "litemap"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
[[package]]
name = "litrs"
version = "0.4.2"
@ -4052,6 +4189,15 @@ dependencies = [
"portable-atomic",
]
[[package]]
name = "potential_utf"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
dependencies = [
"zerovec",
]
[[package]]
name = "pp-rs"
version = "0.2.1"
@ -4781,6 +4927,17 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "synstructure"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "sys-locale"
version = "0.3.2"
@ -4909,6 +5066,16 @@ dependencies = [
"strict-num",
]
[[package]]
name = "tinystr"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
dependencies = [
"displaydoc",
"zerovec",
]
[[package]]
name = "tinyvec"
version = "1.10.0"
@ -5075,7 +5242,7 @@ name = "ui"
version = "0.1.0"
dependencies = [
"bevy",
"bevy-inspector-egui",
"bevy_egui",
]
[[package]]
@ -5094,6 +5261,11 @@ name = "ui_ingame"
version = "0.1.0"
dependencies = [
"bevy",
"bevy_egui",
"building",
"building_placement",
"resources",
"ui_state",
]
[[package]]
@ -5101,6 +5273,7 @@ name = "ui_pause"
version = "0.1.0"
dependencies = [
"bevy",
"bevy_egui",
"input",
"state",
"ui_state",
@ -5174,6 +5347,24 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "url"
version = "2.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
"serde",
]
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
version = "1.18.0"
@ -5440,6 +5631,22 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webbrowser"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aaf4f3c0ba838e82b4e5ccc4157003fb8c324ee24c058470ffb82820becbde98"
dependencies = [
"core-foundation 0.10.1",
"jni",
"log",
"ndk-context",
"objc2 0.6.2",
"objc2-foundation 0.3.1",
"url",
"web-sys",
]
[[package]]
name = "weezl"
version = "0.1.10"
@ -6194,6 +6401,12 @@ dependencies = [
"bitflags 2.9.3",
]
[[package]]
name = "writeable"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
[[package]]
name = "x11-dl"
version = "2.21.0"
@ -6263,6 +6476,30 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5"
[[package]]
name = "yoke"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
dependencies = [
"serde",
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zeno"
version = "0.3.3"
@ -6288,3 +6525,57 @@ dependencies = [
"quote",
"syn",
]
[[package]]
name = "zerofrom"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zerotrie"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
]
[[package]]
name = "zerovec"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View file

@ -25,6 +25,7 @@ itertools = "0.13.0"
avian3d = "0.3.1"
bevy-inspector-egui = "0.33.1"
ron = "0.10.1"
bevy_egui = "0.36.0"
[dependencies]
bevy = { workspace = true }

View file

@ -1 +1,4 @@
test
linux deps:
egui: `sudo apt install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev`

View file

@ -1,7 +1,7 @@
(
map: {
MouseButton(Middle): [
CameraOrbitEnable,
KeyCode(ShiftLeft): [
MultiPlaceTower,
],
MouseButton(Right): [
PlaceTower,
@ -9,18 +9,22 @@
MouseButton(Left): [
CameraPanEnable,
],
KeyCode(F1): [
ToggleDebugUI,
MouseButton(Middle): [
CameraOrbitEnable,
],
MouseScroll: [
CameraZoom,
],
KeyCode(F1): [
ToggleDebugUI,
],
MouseMovementX: [
CameraYaw,
CameraPanX,
],
KeyCode(Escape): [
TogglePause,
Cancel,
],
MouseMovementY: [
CameraPanY,

View file

@ -62,6 +62,7 @@ fn on_building_removed(mut world: DeferredWorld, context: HookContext) {
pub struct BuildingInfo {
pub preview: Entity,
pub size: UVec3,
pub unlocked: bool,
}
#[derive(Component)]
@ -69,9 +70,6 @@ pub struct BuildingPreview {
pub info: Entity,
}
#[derive(Component, Default)]
pub struct ActiveBuildingPreview(pub IVec3);
pub const PREVIEW_ALPHA: f32 = 0.5;
fn register_building_info(mut world: DeferredWorld, context: HookContext) {

View file

@ -59,6 +59,7 @@ fn register(
BuildingInfo {
preview,
size: SIZE,
unlocked: false,
},
Name::new("Core"),
));

View file

@ -4,9 +4,7 @@
use avian3d::prelude::*;
use bevy::prelude::*;
use building::{
ActiveBuildingPreview, BuildingInfo, BuildingPreview, PREVIEW_ALPHA, PopulatePlacedBuilding,
};
use building::{BuildingInfo, BuildingPreview, PREVIEW_ALPHA, PopulatePlacedBuilding};
use combat::{Defense, Health};
use combat_damagers::ContactDamage;
use resources::Cost;
@ -54,7 +52,7 @@ fn register(
let preview = commands
.spawn((
BuildingPreview { info },
ActiveBuildingPreview::default(), // Visibility::Hidden,
Visibility::Hidden,
Mesh3d(mesh.clone()),
MeshMaterial3d(preview_material),
))
@ -63,6 +61,7 @@ fn register(
BuildingInfo {
preview,
size: SIZE,
unlocked: true,
},
Cost { souls: 5, bones: 0 },
Name::new("Impaler"),

View file

@ -3,20 +3,25 @@
#![allow(clippy::too_many_arguments)]
use bevy::prelude::*;
use building::{
ActiveBuildingPreview, BuildingInfo, BuildingPreview, PlaceBuilding, PlacedBuildings,
};
use input::Inputs;
use building::{BuildingInfo, BuildingPreview, PlaceBuilding, PlacedBuildings};
use input::{Cancellable, Inputs, actions::InputAction};
use physics::Ground;
pub struct BuildingPlacementPlugin;
impl Plugin for BuildingPlacementPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, init);
app.add_systems(Update, (init, cancel_preview, select_preview).chain())
.add_event::<SelectBuildingPreview>();
}
}
#[derive(Component, Default)]
pub struct ActiveBuildingPreview(pub IVec3);
#[derive(Event)]
pub struct SelectBuildingPreview(pub Option<Entity>);
fn init(mut commands: Commands, query: Single<Entity, With<Ground>>, mut setup: Local<bool>) {
if *setup {
return;
@ -29,6 +34,16 @@ fn init(mut commands: Commands, query: Single<Entity, With<Ground>>, mut setup:
info!("added building placement observers!");
}
fn cancel_preview(
mut writer: EventWriter<SelectBuildingPreview>,
query: Query<(), (With<ActiveBuildingPreview>, With<Cancellable>)>,
inputs: Res<Inputs<Update>>,
) {
if !query.is_empty() && inputs.just_pressed(InputAction::Cancel) {
writer.write(SelectBuildingPreview(None));
}
}
fn move_preview(
trigger: Trigger<Pointer<Move>>,
preview: Single<(&mut Transform, &mut ActiveBuildingPreview, &BuildingPreview)>,
@ -60,8 +75,9 @@ fn place_preview(
info_query: Query<&BuildingInfo>,
placed_buildings: Res<PlacedBuildings>,
mut place_writer: EventWriter<PlaceBuilding>,
mut select_writer: EventWriter<SelectBuildingPreview>,
) {
if !inputs.pressed(input::actions::InputAction::PlaceTower) {
if !inputs.pressed(InputAction::PlaceTower) {
return;
}
let (preview, active) = query.into_inner();
@ -73,8 +89,42 @@ fn place_preview(
info!("invalid placement!!!!!");
return;
}
if !inputs.pressed(InputAction::MultiPlaceTower) {
select_writer.write(SelectBuildingPreview(None));
}
place_writer.write(PlaceBuilding {
info: preview.info,
location: active.0,
});
}
fn select_preview(
mut reader: EventReader<SelectBuildingPreview>,
mut old_query: Query<(Entity, &mut Visibility), With<ActiveBuildingPreview>>,
new_query: Query<(), With<BuildingPreview>>,
mut commands: Commands,
) {
let Some(SelectBuildingPreview(opt_entity)) = reader.read().last() else {
return;
};
for (entity, mut vis) in old_query.iter_mut() {
if *opt_entity != Some(entity) {
commands
.entity(entity)
.remove::<(ActiveBuildingPreview, Cancellable)>();
*vis = Visibility::Hidden;
}
}
let Some(new_entity) = opt_entity else {
return;
};
if !new_query.contains(*new_entity) {
error!("tried to select a building without a preview!");
return;
}
commands.entity(*new_entity).insert((
Visibility::Visible,
ActiveBuildingPreview::default(),
Cancellable,
));
}

View file

@ -14,6 +14,8 @@ pub enum InputAction {
CameraPanEnable,
CameraOrbitEnable,
PlaceTower,
MultiPlaceTower,
Cancel,
}
impl Default for InputMap {
@ -43,6 +45,11 @@ impl Default for InputMap {
InputMethod::MouseButton(MouseButton::Right),
InputAction::PlaceTower,
),
(
InputMethod::KeyCode(KeyCode::ShiftLeft),
InputAction::MultiPlaceTower,
),
(InputMethod::KeyCode(KeyCode::Escape), InputAction::Cancel),
])
}
}

View file

@ -34,6 +34,9 @@ impl Plugin for InputPlugin {
}
}
#[derive(Component)]
pub struct Cancellable;
#[derive(Hash, Clone, Copy, Debug, Reflect, PartialEq, Eq, Serialize, Deserialize)]
pub enum InputMethod {
Unbound,

View file

@ -6,6 +6,7 @@ edition = "2024"
[dependencies]
bevy = { workspace = true }
avian3d = { workspace = true }
bevy_egui = { workspace = true }
state = { path = "../state" }
camera = { path = "../camera" }

View file

@ -11,6 +11,7 @@ use bevy::{
prelude::*,
render::camera::Exposure,
};
use bevy_egui::PrimaryEguiContext;
use building::{BuildingTypes, PlaceBuilding};
use camera::ControlledCamera;
use inhabitant_coranth::SpawnCoranth;
@ -86,6 +87,7 @@ fn setup(
Exposure::SUNLIGHT,
Bloom::NATURAL,
PhysicsPickable,
PrimaryEguiContext,
Transform::from_xyz(-2.5, 9.5, 9.0).looking_at(Vec3::ZERO, Dir3::Y),
));
}

View file

@ -5,7 +5,7 @@ edition = "2024"
[dependencies]
bevy = { workspace = true }
bevy-inspector-egui = { workspace = true }
bevy_egui = { workspace = true }
[lints]
workspace = true

View file

@ -3,7 +3,7 @@
#![allow(clippy::too_many_arguments)]
use bevy::prelude::*;
use bevy_inspector_egui::bevy_egui::EguiPlugin;
use bevy_egui::EguiPlugin;
pub struct UiPlugin;

View file

@ -5,6 +5,12 @@ edition = "2024"
[dependencies]
bevy = { workspace = true }
bevy_egui = { workspace = true }
ui_state = { path = "../ui_state" }
resources = { path = "../resources" }
building = { path = "../building" }
building_placement = { path = "../building_placement" }
[lints]
workspace = true

View file

@ -3,9 +3,59 @@
#![allow(clippy::too_many_arguments)]
use bevy::prelude::*;
use bevy_egui::{
EguiContexts, EguiPrimaryContextPass,
egui::{self, Align2, RichText},
};
use building::BuildingInfo;
use building_placement::SelectBuildingPreview;
use resources::{Cost, Resources};
use ui_state::GameUIState;
pub struct UiIngamePlugin;
impl Plugin for UiIngamePlugin {
fn build(&self, app: &mut App) {}
fn build(&self, app: &mut App) {
app.add_systems(
EguiPrimaryContextPass,
resource_ui.run_if(in_state(GameUIState::Default)),
);
}
}
fn resource_ui(
mut contexts: EguiContexts,
resources: Res<Resources>,
buildings: Query<(&BuildingInfo, &Cost, &Name)>,
mut select_writer: EventWriter<SelectBuildingPreview>,
) {
let ctx = contexts.ctx_mut().unwrap();
let large_font_size = 24.;
let font_size = 16.;
egui::Window::new("Resources")
.resizable(false)
.collapsible(false)
.title_bar(false)
.anchor(Align2::RIGHT_TOP, [0., 0.])
.show(ctx, |ui| {
ui.label(RichText::new(format!("Souls: {}", resources.souls)).size(large_font_size));
ui.heading(RichText::new("Buildings").underline());
for (info, cost, name) in buildings.iter() {
if !info.unlocked {
continue;
}
if ui
.button(
RichText::new(format!(
"{} - {} souls, {} bones",
name, cost.souls, cost.bones
))
.size(font_size),
)
.clicked()
{
select_writer.write(SelectBuildingPreview(Some(info.preview)));
}
}
});
}

View file

@ -5,6 +5,7 @@ edition = "2024"
[dependencies]
bevy = { workspace = true }
bevy_egui = { workspace = true }
state = { path = "../state" }
input = { path = "../input" }

View file

@ -3,7 +3,11 @@
#![allow(clippy::too_many_arguments)]
use bevy::prelude::*;
use input::{actions::InputAction, conditions::action_just_pressed};
use bevy_egui::{
EguiContexts, EguiPrimaryContextPass,
egui::{self, Align2, RichText},
};
use input::{Cancellable, actions::InputAction, conditions::action_just_pressed};
use state::GameState;
pub struct UiPausePlugin;
@ -13,11 +17,35 @@ impl Plugin for UiPausePlugin {
app.add_systems(
Update,
toggle_pause.run_if(action_just_pressed::<Update>(InputAction::TogglePause)),
)
.add_systems(
EguiPrimaryContextPass,
pause_screen.run_if(in_state(GameState::Paused)),
);
}
}
fn toggle_pause(state: Res<State<GameState>>, mut next_state: ResMut<NextState<GameState>>) {
fn pause_screen(mut contexts: EguiContexts) {
let ctx = contexts.ctx_mut().unwrap();
egui::Window::new("Pause")
.resizable(false)
.collapsible(false)
.title_bar(false)
.anchor(Align2::CENTER_CENTER, [0., 0.])
.show(ctx, |ui| {
ui.label(RichText::new(format!("Paused")).size(64.));
});
}
fn toggle_pause(
state: Res<State<GameState>>,
mut next_state: ResMut<NextState<GameState>>,
query: Query<(), With<Cancellable>>,
) {
if !query.is_empty() {
// cancelling takes priority over pausing
return;
}
let next = match state.get() {
GameState::Loading => GameState::Loading,
GameState::Running => GameState::Paused,