diff --git a/justfile b/justfile index 36ffc51..4cd64d1 100644 --- a/justfile +++ b/justfile @@ -4,3 +4,6 @@ tb_setup_mac: mkdir -p "$HOME/Library/Application Support/TrenchBroom/games/hedz" | true ln -s $(pwd)/trenchbroom/hedz/hedz.fgd "$HOME/Library/Application Support/TrenchBroom/games/hedz/hedz.fgd" | true ln -s $(pwd)/trenchbroom/hedz/GameConfig.cfg "$HOME/Library/Application Support/TrenchBroom/games/hedz/GameConfig.cfg" | true + +dbg: + cargo r --features dbg \ No newline at end of file diff --git a/src/backpack/backpack_ui.rs b/src/backpack/backpack_ui.rs new file mode 100644 index 0000000..2fa142f --- /dev/null +++ b/src/backpack/backpack_ui.rs @@ -0,0 +1,329 @@ +use super::{BackbackSwapEvent, Backpack, BackpackHead}; +use crate::heads_ui::HeadsImages; +use bevy::prelude::*; + +static HEAD_SLOTS: usize = 5; + +#[derive(Event, Clone, Copy, Reflect, PartialEq)] +pub enum BackpackAction { + Left, + Right, + Swap, + OpenClose, +} + +#[derive(Component, Default)] +struct BackpackMarker; + +#[derive(Component, Default)] +struct BackpackCountText; + +#[derive(Component, Default)] +struct HeadSelector(pub usize); + +#[derive(Component, Default)] +struct HeadImage(pub usize); + +#[derive(Component, Default)] +struct HeadDamage(pub usize); + +#[derive(Resource, Default, Debug)] +struct BackpackUiState { + heads: [Option; 5], + scroll: usize, + count: usize, + current_slot: usize, + open: bool, +} + +impl BackpackUiState { + fn relative_current_slot(&self) -> usize { + self.current_slot.saturating_sub(self.scroll) + } +} + +pub fn plugin(app: &mut App) { + app.init_resource::(); + app.add_systems(Startup, setup); + app.add_systems( + Update, + (update, sync_on_change, update_visibility, update_count), + ); + app.add_observer(swap_head_inputs); +} + +fn setup(mut commands: Commands, asset_server: Res) { + let bg = asset_server.load("ui/head_bg.png"); + let regular = asset_server.load("ui/head_regular.png"); + let damage = asset_server.load("ui/head_damage.png"); + let selector = asset_server.load("ui/selector.png"); + + commands + .spawn(( + Name::new("backpack"), + BackpackMarker, + Visibility::Hidden, + Node { + position_type: PositionType::Absolute, + top: Val::Px(20.0), + right: Val::Px(20.0), + height: Val::Px(74.0), + ..default() + }, + )) + .with_children(|parent| { + for i in 0..HEAD_SLOTS { + spawn_head_ui( + parent, + bg.clone(), + regular.clone(), + selector.clone(), + damage.clone(), + i, + ); + } + }); + + commands.spawn(( + Text::new("0"), + BackpackCountText, + TextFont { + font: asset_server.load("font.ttf"), + font_size: 34.0, + ..default() + }, + TextColor(Color::Srgba(Srgba::rgb(0., 1., 0.))), + TextLayout::new_with_justify(JustifyText::Center), + Node { + position_type: PositionType::Absolute, + top: Val::Px(20.0), + right: Val::Px(20.0), + ..default() + }, + )); +} + +fn spawn_head_ui( + parent: &mut ChildBuilder, + bg: Handle, + regular: Handle, + selector: Handle, + damage: Handle, + head_slot: usize, +) { + const SIZE: f32 = 90.0; + const DAMAGE_SIZE: f32 = 74.0; + + parent + .spawn((Node { + position_type: PositionType::Relative, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + width: Val::Px(SIZE), + ..default() + },)) + .with_children(|parent| { + parent.spawn(( + Name::new("selector"), + Node { + position_type: PositionType::Absolute, + bottom: Val::Px(-30.0), + ..default() + }, + Visibility::Hidden, + ImageNode::new(selector).with_flip_y(), + HeadSelector(head_slot), + )); + parent.spawn(( + Name::new("bg"), + Node { + position_type: PositionType::Absolute, + ..default() + }, + ImageNode::new(bg), + )); + parent.spawn(( + Name::new("head"), + Node { + position_type: PositionType::Absolute, + ..default() + }, + ImageNode::default(), + Visibility::Hidden, + HeadImage(head_slot), + )); + parent.spawn(( + Name::new("rings"), + Node { + position_type: PositionType::Absolute, + ..default() + }, + ImageNode::new(regular), + )); + parent + .spawn(( + Name::new("health"), + Node { + height: Val::Px(DAMAGE_SIZE), + width: Val::Px(DAMAGE_SIZE), + ..default() + }, + )) + .with_children(|parent| { + parent + .spawn(( + Name::new("damage_ring"), + HeadDamage(head_slot), + Node { + position_type: PositionType::Absolute, + display: Display::Block, + overflow: Overflow::clip(), + top: Val::Px(0.), + left: Val::Px(0.), + right: Val::Px(0.), + height: Val::Percent(25.), + ..default() + }, + )) + .with_child(ImageNode::new(damage)); + }); + }); +} + +fn update_visibility( + state: Res, + mut backpack: Query<&mut Visibility, (With, Without)>, + mut count: Query<&mut Visibility, (Without, With)>, +) { + if state.is_changed() { + for mut vis in backpack.iter_mut() { + *vis = if state.open { + Visibility::Visible + } else { + Visibility::Hidden + }; + } + + for mut vis in count.iter_mut() { + *vis = if !state.open { + Visibility::Visible + } else { + Visibility::Hidden + }; + } + } +} + +fn update_count( + state: Res, + text: Query>, + mut writer: TextUiWriter, +) { + if state.is_changed() { + let Some(text) = text.iter().next() else { + return; + }; + + *writer.text(text, 0) = state.count.to_string(); + } +} + +fn update( + state: Res, + heads_images: Res, + mut head_image: Query<(&HeadImage, &mut Visibility, &mut ImageNode), Without>, + mut head_damage: Query<(&HeadDamage, &mut Node), Without>, + mut head_selector: Query<(&HeadSelector, &mut Visibility), Without>, +) { + if state.is_changed() { + for (HeadImage(head), mut vis, mut image) in head_image.iter_mut() { + if let Some(head) = &state.heads[*head] { + *vis = Visibility::Inherited; + image.image = heads_images.heads[head.head].clone(); + } else { + *vis = Visibility::Hidden; + } + } + for (HeadDamage(head), mut node) in head_damage.iter_mut() { + if let Some(head) = &state.heads[*head] { + node.height = Val::Percent((1. - head.health) * 100.0); + } + } + + for (HeadSelector(head), mut vis) in head_selector.iter_mut() { + *vis = if *head == state.relative_current_slot() { + Visibility::Inherited + } else { + Visibility::Hidden + }; + } + } +} + +fn swap_head_inputs( + trigger: Trigger, + backpack: Res, + mut commands: Commands, + mut state: ResMut, +) { + if state.count == 0 { + return; + } + + let action = *trigger.event(); + if action == BackpackAction::OpenClose { + state.open = !state.open; + } + + if !state.open { + return; + } + + let mut changed = false; + if action == BackpackAction::Left { + if state.current_slot > 0 { + state.current_slot -= 1; + changed = true; + } + } + if action == BackpackAction::Right { + if state.current_slot < state.count.saturating_sub(1) { + state.current_slot += 1; + changed = true; + } + } + if action == BackpackAction::Swap { + commands.trigger(BackbackSwapEvent(state.current_slot)); + } + + if changed { + sync(&backpack, &mut state); + } +} + +fn sync_on_change(backpack: Res, mut state: ResMut) { + if backpack.is_changed() { + sync(&backpack, &mut state); + } +} + +fn sync(backpack: &Res, state: &mut ResMut) { + state.count = backpack.heads.len(); + + state.scroll = state.scroll.min(state.count.saturating_sub(HEAD_SLOTS)); + + if state.current_slot >= state.scroll + HEAD_SLOTS { + state.scroll = state.current_slot.saturating_sub(HEAD_SLOTS - 1); + } + if state.current_slot < state.scroll { + state.scroll = state.current_slot; + } + + for i in 0..HEAD_SLOTS { + if let Some(head) = backpack.heads.get(i + state.scroll) { + state.heads[i] = Some(*head); + } else { + state.heads[i] = None; + } + } +} diff --git a/src/backpack/mod.rs b/src/backpack/mod.rs new file mode 100644 index 0000000..56085c5 --- /dev/null +++ b/src/backpack/mod.rs @@ -0,0 +1,38 @@ +mod backpack_ui; + +use crate::heads_ui::HEAD_COUNT; +use bevy::prelude::*; + +pub use backpack_ui::BackpackAction; + +#[derive(Clone, Copy, Debug)] +pub struct BackpackHead { + pub head: usize, + pub health: f32, +} + +#[derive(Resource, Default)] +pub struct Backpack { + pub heads: Vec, +} + +#[derive(Event)] +pub struct BackbackSwapEvent(pub usize); + +pub fn plugin(app: &mut App) { + app.add_plugins(backpack_ui::plugin); + + app.add_systems(Startup, setup); +} + +fn setup(mut commands: Commands) { + commands.insert_resource(Backpack { + heads: (0usize..HEAD_COUNT) + .into_iter() + .map(|i| BackpackHead { + head: i, + health: 1., + }) + .collect(), + }); +} diff --git a/src/controls.rs b/src/controls.rs index 68f26e9..f8a744a 100644 --- a/src/controls.rs +++ b/src/controls.rs @@ -1,4 +1,4 @@ -use crate::{heads_ui::SwapHead, shooting::TriggerState}; +use crate::{backpack::BackpackAction, heads_ui::SelectActiveHead, shooting::TriggerState}; use bevy::{ input::{ ButtonState, @@ -86,10 +86,22 @@ fn gamepad_controls( commands.trigger(TriggerState::Inactive); } if gamepad.just_pressed(GamepadButton::LeftTrigger) { - commands.trigger(SwapHead::Left); + commands.trigger(SelectActiveHead::Left); } if gamepad.just_pressed(GamepadButton::RightTrigger) { - commands.trigger(SwapHead::Right); + commands.trigger(SelectActiveHead::Right); + } + if gamepad.just_pressed(GamepadButton::DPadLeft) { + commands.trigger(BackpackAction::Left); + } + if gamepad.just_pressed(GamepadButton::DPadRight) { + commands.trigger(BackpackAction::Right); + } + if gamepad.just_pressed(GamepadButton::DPadDown) { + commands.trigger(BackpackAction::Swap); + } + if gamepad.just_pressed(GamepadButton::DPadUp) { + commands.trigger(BackpackAction::OpenClose); } if controls @@ -129,11 +141,24 @@ fn keyboard_controls( direction += Vec2::X; } + if keyboard.just_pressed(KeyCode::KeyB) { + commands.trigger(BackpackAction::OpenClose); + } + if keyboard.just_pressed(KeyCode::Enter) { + commands.trigger(BackpackAction::Swap); + } + if keyboard.just_pressed(KeyCode::Comma) { + commands.trigger(BackpackAction::Left); + } + if keyboard.just_pressed(KeyCode::Period) { + commands.trigger(BackpackAction::Right); + } + if keyboard.just_pressed(KeyCode::KeyQ) { - commands.trigger(SwapHead::Left); + commands.trigger(SelectActiveHead::Left); } if keyboard.just_pressed(KeyCode::KeyE) { - commands.trigger(SwapHead::Right); + commands.trigger(SelectActiveHead::Right); } controls.keyboard_state.move_dir = direction; diff --git a/src/heads_ui.rs b/src/heads_ui.rs index 8d56daa..f13e41a 100644 --- a/src/heads_ui.rs +++ b/src/heads_ui.rs @@ -1,9 +1,14 @@ +use crate::{ + backpack::{BackbackSwapEvent, Backpack, BackpackHead}, + player::head_id_to_str, +}; use bevy::prelude::*; -use crate::player::head_id_to_str; +pub static HEAD_COUNT: usize = 18; +static HEAD_SLOTS: usize = 5; #[derive(Event, Reflect)] -pub enum SwapHead { +pub enum SelectActiveHead { Left, Right, } @@ -21,12 +26,12 @@ struct HeadImage(pub usize); struct HeadDamage(pub usize); #[derive(Resource, Default)] -struct HeadsImages { - heads: Vec>, +pub struct HeadsImages { + pub heads: Vec>, } #[derive(Resource, Default)] -struct ActiveHeads { +pub struct ActiveHeads { heads: [Option; 5], current_slot: usize, } @@ -37,8 +42,9 @@ pub struct HeadChanged(pub usize); pub fn plugin(app: &mut App) { app.register_type::(); app.add_systems(Startup, setup); - app.add_systems(Update, (update, swap_head_test)); - app.add_observer(on_swap_head); + app.add_systems(Update, update); + app.add_observer(on_select_active_head); + app.add_observer(on_swap_backpack); } fn setup(mut commands: Commands, asset_server: Res) { @@ -56,7 +62,7 @@ fn setup(mut commands: Commands, asset_server: Res) { ..default() }) .with_children(|parent| { - for i in 0..5 { + for i in 0..HEAD_SLOTS { spawn_head_ui( parent, bg.clone(), @@ -68,7 +74,7 @@ fn setup(mut commands: Commands, asset_server: Res) { } }); - let heads = (0usize..18) + let heads = (0usize..HEAD_COUNT) .into_iter() .map(|i| asset_server.load(format!("ui/heads/{}.png", head_id_to_str(i)))) .collect(); @@ -87,7 +93,7 @@ fn spawn_head_ui( regular: Handle, selector: Handle, damage: Handle, - head: usize, + head_slot: usize, ) { const SIZE: f32 = 90.0; const DAMAGE_SIZE: f32 = 74.0; @@ -109,7 +115,7 @@ fn spawn_head_ui( }, Visibility::Hidden, ImageNode::new(selector), - HeadSelector(head), + HeadSelector(head_slot), )); parent.spawn(( Node { @@ -127,7 +133,7 @@ fn spawn_head_ui( }, ImageNode::default(), Visibility::Hidden, - HeadImage(head), + HeadImage(head_slot), )); parent.spawn(( Node { @@ -145,7 +151,7 @@ fn spawn_head_ui( .with_children(|parent| { parent .spawn(( - HeadDamage(head), + HeadDamage(head_slot), Node { position_type: PositionType::Absolute, display: Display::Block, @@ -162,25 +168,6 @@ fn spawn_head_ui( }); } -fn swap_head_test( - mut commands: Commands, - mut active: ResMut, - keyboard: Res>, -) { - if keyboard.just_pressed(KeyCode::Comma) { - let slot = active.current_slot; - let current = active.heads[active.current_slot].unwrap(); - active.heads[slot] = Some((current + 17) % 18); - commands.trigger(HeadChanged(active.heads[slot].unwrap())); - } - if keyboard.just_pressed(KeyCode::Period) { - let slot = active.current_slot; - let current = active.heads[active.current_slot].unwrap(); - active.heads[slot] = Some((current + 1) % 18); - commands.trigger(HeadChanged(active.heads[slot].unwrap())); - } -} - fn update( res: Res, heads_images: Res, @@ -206,15 +193,46 @@ fn update( } } -fn on_swap_head(trigger: Trigger, mut commands: Commands, mut res: ResMut) { +fn on_select_active_head( + trigger: Trigger, + mut commands: Commands, + mut res: ResMut, +) { match trigger.event() { - SwapHead::Right => { - res.current_slot = (res.current_slot + 1) % 5; + SelectActiveHead::Right => { + res.current_slot = (res.current_slot + 1) % HEAD_SLOTS; } - SwapHead::Left => { - res.current_slot = (res.current_slot + 4) % 5; + SelectActiveHead::Left => { + res.current_slot = (res.current_slot + (HEAD_SLOTS - 1)) % HEAD_SLOTS; } } commands.trigger(HeadChanged(res.heads[res.current_slot].unwrap())); } + +fn on_swap_backpack( + trigger: Trigger, + mut commands: Commands, + mut res: ResMut, + mut backpack: ResMut, +) { + let backpack_slot = trigger.event().0; + + let head = backpack.heads.get(backpack_slot).unwrap(); + + let current_active_slot = res.current_slot; + + let current_active_head = res.heads[current_active_slot]; + res.heads[current_active_slot] = Some(head.head); + + if let Some(old_active) = current_active_head { + backpack.heads[backpack_slot] = BackpackHead { + head: old_active, + health: 1., + }; + } else { + backpack.heads.remove(backpack_slot); + } + + commands.trigger(HeadChanged(res.heads[res.current_slot].unwrap())); +} diff --git a/src/main.rs b/src/main.rs index f9f3381..7cc2389 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ mod aim; mod alien; +mod backpack; mod billboards; mod camera; mod cash; @@ -94,6 +95,7 @@ fn main() { app.add_plugins(controls::plugin); app.add_plugins(sounds::plugin); app.add_plugins(camera::plugin); + app.add_plugins(backpack::plugin); app.insert_resource(AmbientLight { color: Color::WHITE, @@ -104,10 +106,7 @@ fn main() { app.add_systems(Startup, (write_trenchbroom_config, music)); app.add_systems(PostStartup, setup_scene); - app.add_systems( - Update, - (set_materials_unlit, set_tonemapping, set_shadows, spawn_box), - ); + app.add_systems(Update, (set_materials_unlit, set_tonemapping, set_shadows)); app.run(); } @@ -137,24 +136,6 @@ fn setup_scene(mut commands: Commands, asset_server: Res) { )); } -fn spawn_box( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, - keys: Res>, -) { - if keys.just_pressed(KeyCode::Enter) { - commands.spawn(( - RigidBody::Dynamic, - Collider::cuboid(5.0, 5.0, 5.0), - AngularVelocity(Vec3::new(2.5, 3.5, 1.5)), - Mesh3d(meshes.add(Cuboid::from_length(5.0))), - MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))), - Transform::from_xyz(0.0, 50.0, 0.0), - )); - } -} - fn music(asset_server: Res, mut commands: Commands) { commands.spawn(( AudioPlayer::new(asset_server.load("sfx/music/02.ogg")),