Crate unification (#88)
* move client/server/config into shared * move platforms into shared * move head drops into shared * move tb_entities to shared * reduce server to just a call into shared * get solo play working * fix server opening window * fix fmt * extracted a few more modules from client * near completely migrated client * fixed duplicate CharacterInputEnabled definition * simplify a few things related to builds * more simplifications * fix warnings/check * ci update * address comments * try fixing macos steam build * address comments * address comments * CI tweaks with default client feature --------- Co-authored-by: PROMETHIA-27 <electriccobras@gmail.com>
This commit is contained in:
212
crates/hedz_reloaded/src/client/backpack/backpack_ui.rs
Normal file
212
crates/hedz_reloaded/src/client/backpack/backpack_ui.rs
Normal file
@@ -0,0 +1,212 @@
|
||||
use crate::{
|
||||
GameState, HEDZ_GREEN,
|
||||
backpack::backpack_ui::{
|
||||
BACKPACK_HEAD_SLOTS, BackpackCountText, BackpackMarker, BackpackUiState, HeadDamage,
|
||||
HeadImage, HeadSelector,
|
||||
},
|
||||
heads::HeadsImages,
|
||||
loading_assets::UIAssets,
|
||||
};
|
||||
use bevy::{ecs::spawn::SpawnIter, prelude::*};
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_systems(OnEnter(GameState::Playing), setup);
|
||||
app.add_systems(
|
||||
FixedUpdate,
|
||||
(update, update_visibility, update_count).run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
}
|
||||
|
||||
fn setup(mut commands: Commands, assets: Res<UIAssets>) {
|
||||
commands.spawn((
|
||||
Name::new("backpack-ui"),
|
||||
BackpackMarker,
|
||||
Visibility::Hidden,
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
top: Val::Px(20.0),
|
||||
right: Val::Px(20.0),
|
||||
height: Val::Px(74.0),
|
||||
..default()
|
||||
},
|
||||
Children::spawn(SpawnIter((0..BACKPACK_HEAD_SLOTS).map({
|
||||
let bg = assets.head_bg.clone();
|
||||
let regular = assets.head_regular.clone();
|
||||
let selector = assets.head_selector.clone();
|
||||
let damage = assets.head_damage.clone();
|
||||
|
||||
move |i| {
|
||||
spawn_head_ui(
|
||||
bg.clone(),
|
||||
regular.clone(),
|
||||
selector.clone(),
|
||||
damage.clone(),
|
||||
i,
|
||||
)
|
||||
}
|
||||
}))),
|
||||
));
|
||||
|
||||
commands.spawn((
|
||||
Name::new("backpack-head-count-ui"),
|
||||
Text::new("0"),
|
||||
TextShadow::default(),
|
||||
BackpackCountText,
|
||||
TextFont {
|
||||
font: assets.font.clone(),
|
||||
font_size: 34.0,
|
||||
..default()
|
||||
},
|
||||
TextColor(HEDZ_GREEN.into()),
|
||||
TextLayout::new_with_justify(Justify::Center),
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
top: Val::Px(20.0),
|
||||
right: Val::Px(20.0),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
fn spawn_head_ui(
|
||||
bg: Handle<Image>,
|
||||
regular: Handle<Image>,
|
||||
selector: Handle<Image>,
|
||||
damage: Handle<Image>,
|
||||
head_slot: usize,
|
||||
) -> impl Bundle {
|
||||
const SIZE: f32 = 90.0;
|
||||
const DAMAGE_SIZE: f32 = 74.0;
|
||||
|
||||
(
|
||||
Node {
|
||||
position_type: PositionType::Relative,
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
width: Val::Px(SIZE),
|
||||
..default()
|
||||
},
|
||||
children![
|
||||
(
|
||||
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),
|
||||
),
|
||||
(
|
||||
Name::new("bg"),
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
..default()
|
||||
},
|
||||
ImageNode::new(bg),
|
||||
),
|
||||
(
|
||||
Name::new("head"),
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
..default()
|
||||
},
|
||||
ImageNode::default(),
|
||||
Visibility::Hidden,
|
||||
HeadImage(head_slot),
|
||||
),
|
||||
(
|
||||
Name::new("rings"),
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
..default()
|
||||
},
|
||||
ImageNode::new(regular),
|
||||
),
|
||||
(
|
||||
Name::new("health"),
|
||||
Node {
|
||||
height: Val::Px(DAMAGE_SIZE),
|
||||
width: Val::Px(DAMAGE_SIZE),
|
||||
..default()
|
||||
},
|
||||
children![(
|
||||
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(0.),
|
||||
..default()
|
||||
},
|
||||
children![ImageNode::new(damage)]
|
||||
)]
|
||||
)
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
fn update_visibility(
|
||||
state: Single<&BackpackUiState, Changed<BackpackUiState>>,
|
||||
mut backpack: Single<&mut Visibility, (With<BackpackMarker>, Without<BackpackCountText>)>,
|
||||
mut count: Single<&mut Visibility, (Without<BackpackMarker>, With<BackpackCountText>)>,
|
||||
) {
|
||||
**backpack = if state.open {
|
||||
Visibility::Visible
|
||||
} else {
|
||||
Visibility::Hidden
|
||||
};
|
||||
|
||||
**count = if !state.open {
|
||||
Visibility::Visible
|
||||
} else {
|
||||
Visibility::Hidden
|
||||
};
|
||||
}
|
||||
|
||||
fn update_count(
|
||||
state: Single<&BackpackUiState, Changed<BackpackUiState>>,
|
||||
text: Option<Single<Entity, With<BackpackCountText>>>,
|
||||
mut writer: TextUiWriter,
|
||||
) {
|
||||
let Some(text) = text else {
|
||||
return;
|
||||
};
|
||||
|
||||
*writer.text(*text, 0) = state.count.to_string();
|
||||
}
|
||||
|
||||
fn update(
|
||||
state: Single<&BackpackUiState, Changed<BackpackUiState>>,
|
||||
heads_images: Res<HeadsImages>,
|
||||
mut head_image: Query<(&HeadImage, &mut Visibility, &mut ImageNode), Without<HeadSelector>>,
|
||||
mut head_damage: Query<(&HeadDamage, &mut Node), Without<HeadSelector>>,
|
||||
mut head_selector: Query<(&HeadSelector, &mut Visibility), Without<HeadImage>>,
|
||||
) {
|
||||
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(head.damage() * 100.0);
|
||||
}
|
||||
}
|
||||
|
||||
for (HeadSelector(head), mut vis) in head_selector.iter_mut() {
|
||||
*vis = if *head == state.relative_current_slot() {
|
||||
Visibility::Inherited
|
||||
} else {
|
||||
Visibility::Hidden
|
||||
};
|
||||
}
|
||||
}
|
||||
7
crates/hedz_reloaded/src/client/backpack/mod.rs
Normal file
7
crates/hedz_reloaded/src/client/backpack/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod backpack_ui;
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_plugins(backpack_ui::plugin);
|
||||
}
|
||||
58
crates/hedz_reloaded/src/client/control/controller_flying.rs
Normal file
58
crates/hedz_reloaded/src/client/control/controller_flying.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use crate::{
|
||||
GameState,
|
||||
control::{ControllerSet, Inputs, LookDirMovement},
|
||||
player::{LocalPlayer, PlayerBodyMesh},
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
use std::f32::consts::PI;
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_systems(
|
||||
FixedUpdate,
|
||||
rotate_rig
|
||||
.before(crate::control::controller_flying::apply_controls)
|
||||
.in_set(ControllerSet::ApplyControlsFly)
|
||||
.run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
}
|
||||
|
||||
fn rotate_rig(
|
||||
inputs: Single<&Inputs, With<LocalPlayer>>,
|
||||
look_dir: Res<LookDirMovement>,
|
||||
local_player: Single<&Children, With<LocalPlayer>>,
|
||||
mut player_mesh: Query<&mut Transform, With<PlayerBodyMesh>>,
|
||||
) {
|
||||
if inputs.view_mode {
|
||||
return;
|
||||
}
|
||||
|
||||
local_player.iter().find(|&child| {
|
||||
if let Ok(mut rig_transform) = player_mesh.get_mut(child) {
|
||||
let look_dir = look_dir.0;
|
||||
|
||||
// todo: Make consistent with the running controller
|
||||
let sensitivity = 0.001;
|
||||
let max_pitch = 35.0 * PI / 180.0;
|
||||
let min_pitch = -25.0 * PI / 180.0;
|
||||
|
||||
rig_transform.rotate_y(look_dir.x * -sensitivity);
|
||||
|
||||
let euler_rot = rig_transform.rotation.to_euler(EulerRot::YXZ);
|
||||
let yaw = euler_rot.0;
|
||||
let pitch = euler_rot.1 + look_dir.y * -sensitivity;
|
||||
|
||||
let pitch_clamped = pitch.clamp(min_pitch, max_pitch);
|
||||
rig_transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch_clamped, 0.0);
|
||||
|
||||
// The following can be used to limit the amount of rotation per frame
|
||||
// let target_rotation = rig_transform.rotation
|
||||
// * Quat::from_rotation_x(controls.keyboard_state.look_dir.y * sensitivity);
|
||||
// let clamped_rotation = rig_transform.rotation.rotate_towards(target_rotation, 0.01);
|
||||
// rig_transform.rotation = clamped_rotation;
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
}
|
||||
265
crates/hedz_reloaded/src/client/control/controls.rs
Normal file
265
crates/hedz_reloaded/src/client/control/controls.rs
Normal file
@@ -0,0 +1,265 @@
|
||||
use crate::{
|
||||
GameState,
|
||||
client::control::CharacterInputEnabled,
|
||||
control::{
|
||||
BackpackButtonPress, CashHealPressed, ClientInputs, ControllerSet, Inputs, LocalInputs,
|
||||
LookDirMovement, SelectLeftPressed, SelectRightPressed,
|
||||
},
|
||||
player::{LocalPlayer, PlayerBodyMesh},
|
||||
};
|
||||
use bevy::{
|
||||
input::{
|
||||
gamepad::{GamepadConnection, GamepadEvent},
|
||||
mouse::MouseMotion,
|
||||
},
|
||||
prelude::*,
|
||||
};
|
||||
use bevy_replicon::client::ClientSystems;
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_systems(
|
||||
PreUpdate,
|
||||
(
|
||||
gamepad_connections.run_if(on_message::<GamepadEvent>),
|
||||
reset_lookdir,
|
||||
keyboard_controls,
|
||||
gamepad_controls,
|
||||
mouse_rotate,
|
||||
get_lookdir,
|
||||
send_inputs,
|
||||
)
|
||||
.chain()
|
||||
.in_set(ControllerSet::CollectInputs)
|
||||
.before(ClientSystems::Receive)
|
||||
.run_if(
|
||||
in_state(GameState::Playing)
|
||||
.and(resource_exists_and_equals(CharacterInputEnabled::On)),
|
||||
),
|
||||
);
|
||||
|
||||
// run this deliberately after local input processing ended
|
||||
// TODO: can and should be ordered using a set to guarantee it gets send out ASAP but after local input processing
|
||||
app.add_systems(
|
||||
PreUpdate,
|
||||
overwrite_local_inputs.after(ClientSystems::Receive).run_if(
|
||||
in_state(GameState::Playing).and(resource_exists_and_equals(CharacterInputEnabled::On)),
|
||||
),
|
||||
);
|
||||
|
||||
app.add_systems(
|
||||
Update,
|
||||
reset_control_state_on_disable.run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Overwrite inputs for this client that were replicated from the server with the local inputs
|
||||
fn overwrite_local_inputs(
|
||||
mut inputs: Single<&mut Inputs, With<LocalPlayer>>,
|
||||
local_inputs: Single<&LocalInputs>,
|
||||
) {
|
||||
**inputs = local_inputs.0;
|
||||
}
|
||||
|
||||
/// Write inputs from combined keyboard/gamepad state into the networked input buffer
|
||||
/// for the local player.
|
||||
fn send_inputs(mut writer: MessageWriter<ClientInputs>, local_inputs: Single<&LocalInputs>) {
|
||||
writer.write(ClientInputs(local_inputs.0));
|
||||
}
|
||||
|
||||
fn reset_lookdir(mut look_dir: ResMut<LookDirMovement>) {
|
||||
look_dir.0 = Vec2::ZERO;
|
||||
}
|
||||
|
||||
/// Reset character inputs to default when character input is disabled.
|
||||
fn reset_control_state_on_disable(
|
||||
state: Res<CharacterInputEnabled>,
|
||||
mut inputs: Single<&mut LocalInputs>,
|
||||
) {
|
||||
if state.is_changed() && matches!(*state, CharacterInputEnabled::Off) {
|
||||
inputs.0 = Inputs {
|
||||
look_dir: inputs.0.look_dir,
|
||||
..default()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn get_lookdir(
|
||||
mut inputs: Single<&mut LocalInputs>,
|
||||
rig_transform: Option<Single<&GlobalTransform, With<PlayerBodyMesh>>>,
|
||||
) {
|
||||
inputs.0.look_dir = if let Some(ref rig_transform) = rig_transform {
|
||||
rig_transform.forward().as_vec3()
|
||||
} else {
|
||||
Vec3::NEG_Z
|
||||
};
|
||||
}
|
||||
|
||||
/// Applies a square deadzone to a Vec2
|
||||
fn deadzone_square(v: Vec2, min: f32) -> Vec2 {
|
||||
Vec2::new(
|
||||
if v.x.abs() < min { 0. } else { v.x },
|
||||
if v.y.abs() < min { 0. } else { v.y },
|
||||
)
|
||||
}
|
||||
|
||||
/// Collect gamepad inputs
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn gamepad_controls(
|
||||
gamepads: Query<&Gamepad>,
|
||||
mut inputs: Single<&mut LocalInputs>,
|
||||
mut look_dir: ResMut<LookDirMovement>,
|
||||
mut backpack_inputs: MessageWriter<BackpackButtonPress>,
|
||||
mut select_left_pressed: MessageWriter<SelectLeftPressed>,
|
||||
mut select_right_pressed: MessageWriter<SelectRightPressed>,
|
||||
mut cash_heal_pressed: MessageWriter<CashHealPressed>,
|
||||
) {
|
||||
let deadzone_left_stick = 0.15;
|
||||
let deadzone_right_stick = 0.15;
|
||||
|
||||
for gamepad in gamepads.iter() {
|
||||
let rotate = gamepad
|
||||
.get(GamepadButton::RightTrigger2)
|
||||
.unwrap_or_default();
|
||||
|
||||
// 8BitDo Ultimate wireless Controller for PC
|
||||
look_dir.0 = if gamepad.vendor_id() == Some(11720) && gamepad.product_id() == Some(12306) {
|
||||
const EPSILON: f32 = 0.015;
|
||||
Vec2::new(
|
||||
if rotate < 0.5 - EPSILON {
|
||||
40. * (rotate - 0.5)
|
||||
} else if rotate > 0.5 + EPSILON {
|
||||
-40. * (rotate - 0.5)
|
||||
} else {
|
||||
0.
|
||||
},
|
||||
0.,
|
||||
)
|
||||
} else {
|
||||
deadzone_square(gamepad.right_stick(), deadzone_right_stick) * 40.
|
||||
};
|
||||
|
||||
let move_dir = deadzone_square(gamepad.left_stick(), deadzone_left_stick);
|
||||
|
||||
inputs.0.move_dir += move_dir.clamp_length_max(1.0);
|
||||
inputs.0.jump |= gamepad.pressed(GamepadButton::South);
|
||||
inputs.0.view_mode |= gamepad.pressed(GamepadButton::LeftTrigger2);
|
||||
inputs.0.trigger |= gamepad.pressed(GamepadButton::RightTrigger2);
|
||||
|
||||
if gamepad.just_pressed(GamepadButton::DPadUp) {
|
||||
backpack_inputs.write(BackpackButtonPress::Toggle);
|
||||
}
|
||||
|
||||
if gamepad.just_pressed(GamepadButton::DPadDown) {
|
||||
backpack_inputs.write(BackpackButtonPress::Swap);
|
||||
}
|
||||
|
||||
if gamepad.just_pressed(GamepadButton::DPadLeft) {
|
||||
backpack_inputs.write(BackpackButtonPress::Left);
|
||||
}
|
||||
|
||||
if gamepad.just_pressed(GamepadButton::DPadRight) {
|
||||
backpack_inputs.write(BackpackButtonPress::Right);
|
||||
}
|
||||
|
||||
if gamepad.just_pressed(GamepadButton::LeftTrigger) {
|
||||
select_left_pressed.write(SelectLeftPressed);
|
||||
}
|
||||
|
||||
if gamepad.just_pressed(GamepadButton::RightTrigger) {
|
||||
select_right_pressed.write(SelectRightPressed);
|
||||
}
|
||||
|
||||
if gamepad.just_pressed(GamepadButton::East) {
|
||||
cash_heal_pressed.write(CashHealPressed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect mouse movement input
|
||||
fn mouse_rotate(mut mouse: MessageReader<MouseMotion>, mut look_dir: ResMut<LookDirMovement>) {
|
||||
for ev in mouse.read() {
|
||||
look_dir.0 += ev.delta;
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect keyboard input
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn keyboard_controls(
|
||||
keyboard: Res<ButtonInput<KeyCode>>,
|
||||
mouse: Res<ButtonInput<MouseButton>>,
|
||||
mut inputs: Single<&mut LocalInputs>,
|
||||
mut backpack_inputs: MessageWriter<BackpackButtonPress>,
|
||||
mut select_left_pressed: MessageWriter<SelectLeftPressed>,
|
||||
mut select_right_pressed: MessageWriter<SelectRightPressed>,
|
||||
mut cash_heal_pressed: MessageWriter<CashHealPressed>,
|
||||
) {
|
||||
let up_binds = [KeyCode::KeyW, KeyCode::ArrowUp];
|
||||
let down_binds = [KeyCode::KeyS, KeyCode::ArrowDown];
|
||||
let left_binds = [KeyCode::KeyA, KeyCode::ArrowLeft];
|
||||
let right_binds = [KeyCode::KeyD, KeyCode::ArrowRight];
|
||||
|
||||
let up = keyboard.any_pressed(up_binds);
|
||||
let down = keyboard.any_pressed(down_binds);
|
||||
let left = keyboard.any_pressed(left_binds);
|
||||
let right = keyboard.any_pressed(right_binds);
|
||||
|
||||
let horizontal = right as i8 - left as i8;
|
||||
let vertical = up as i8 - down as i8;
|
||||
let direction = Vec2::new(horizontal as f32, vertical as f32).clamp_length_max(1.0);
|
||||
|
||||
inputs.0.move_dir = direction;
|
||||
inputs.0.jump = keyboard.pressed(KeyCode::Space);
|
||||
inputs.0.view_mode = keyboard.pressed(KeyCode::Tab);
|
||||
inputs.0.trigger = mouse.pressed(MouseButton::Left);
|
||||
|
||||
if keyboard.just_pressed(KeyCode::KeyB) {
|
||||
backpack_inputs.write(BackpackButtonPress::Toggle);
|
||||
}
|
||||
|
||||
if keyboard.just_pressed(KeyCode::Enter) {
|
||||
backpack_inputs.write(BackpackButtonPress::Swap);
|
||||
}
|
||||
|
||||
if keyboard.just_pressed(KeyCode::Comma) {
|
||||
backpack_inputs.write(BackpackButtonPress::Left);
|
||||
}
|
||||
|
||||
if keyboard.just_pressed(KeyCode::Period) {
|
||||
backpack_inputs.write(BackpackButtonPress::Right);
|
||||
}
|
||||
|
||||
if keyboard.just_pressed(KeyCode::KeyQ) {
|
||||
select_left_pressed.write(SelectLeftPressed);
|
||||
}
|
||||
|
||||
if keyboard.just_pressed(KeyCode::KeyE) {
|
||||
select_right_pressed.write(SelectRightPressed);
|
||||
}
|
||||
|
||||
if keyboard.just_pressed(KeyCode::Enter) {
|
||||
cash_heal_pressed.write(CashHealPressed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Receive gamepad connections and disconnections
|
||||
fn gamepad_connections(mut evr_gamepad: MessageReader<GamepadEvent>) {
|
||||
for ev in evr_gamepad.read() {
|
||||
if let GamepadEvent::Connection(connection) = ev {
|
||||
match &connection.connection {
|
||||
GamepadConnection::Connected {
|
||||
name,
|
||||
vendor_id,
|
||||
product_id,
|
||||
} => {
|
||||
info!(
|
||||
"New gamepad connected: {:?}, name: {name}, vendor: {vendor_id:?}, product: {product_id:?}",
|
||||
connection.gamepad,
|
||||
);
|
||||
}
|
||||
GamepadConnection::Disconnected => {
|
||||
info!("Lost connection with gamepad: {:?}", connection.gamepad);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
25
crates/hedz_reloaded/src/client/control/mod.rs
Normal file
25
crates/hedz_reloaded/src/client/control/mod.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use crate::{GameState, control::ControllerSet};
|
||||
use bevy::prelude::*;
|
||||
use bevy_replicon::client::ClientSystems;
|
||||
|
||||
mod controller_flying;
|
||||
pub mod controls;
|
||||
|
||||
#[derive(Resource, Debug, PartialEq, Eq)]
|
||||
pub enum CharacterInputEnabled {
|
||||
On,
|
||||
Off,
|
||||
}
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.insert_resource(CharacterInputEnabled::On);
|
||||
|
||||
app.add_plugins((controller_flying::plugin, controls::plugin));
|
||||
|
||||
app.configure_sets(
|
||||
PreUpdate,
|
||||
ControllerSet::CollectInputs
|
||||
.before(ClientSystems::Receive)
|
||||
.run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
}
|
||||
40
crates/hedz_reloaded/src/client/debug.rs
Normal file
40
crates/hedz_reloaded/src/client/debug.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use bevy::prelude::*;
|
||||
use bevy_debug_log::LogViewerVisibility;
|
||||
|
||||
// Is supplied by a build script via vergen_gitcl
|
||||
pub const GIT_HASH: &str = env!("VERGEN_GIT_SHA");
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_systems(Update, update);
|
||||
app.add_systems(Startup, setup);
|
||||
}
|
||||
|
||||
fn update(mut commands: Commands, keyboard: Res<ButtonInput<KeyCode>>, gamepads: Query<&Gamepad>) {
|
||||
if keyboard.just_pressed(KeyCode::Backquote) {
|
||||
commands.trigger(LogViewerVisibility::Toggle);
|
||||
}
|
||||
|
||||
for g in gamepads.iter() {
|
||||
if g.just_pressed(GamepadButton::North) {
|
||||
commands.trigger(LogViewerVisibility::Toggle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup(mut commands: Commands) {
|
||||
commands.spawn((
|
||||
Name::new("githash-ui"),
|
||||
Text::new(GIT_HASH),
|
||||
TextFont {
|
||||
font_size: 12.0,
|
||||
..default()
|
||||
},
|
||||
TextLayout::new_with_justify(Justify::Left),
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
top: Val::Px(5.0),
|
||||
left: Val::Px(5.0),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
}
|
||||
14
crates/hedz_reloaded/src/client/enemy.rs
Normal file
14
crates/hedz_reloaded/src/client/enemy.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use crate::{GameState, tb_entities::EnemySpawn};
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_systems(OnEnter(GameState::Connecting), despawn_enemy_spawns);
|
||||
}
|
||||
|
||||
/// Despawn enemy spawners because only the server will ever spawn enemies with them, and they have a
|
||||
/// collider.
|
||||
fn despawn_enemy_spawns(mut commands: Commands, enemy_spawns: Query<Entity, With<EnemySpawn>>) {
|
||||
for spawner in enemy_spawns.iter() {
|
||||
commands.entity(spawner).despawn();
|
||||
}
|
||||
}
|
||||
153
crates/hedz_reloaded/src/client/heal_effect.rs
Normal file
153
crates/hedz_reloaded/src/client/heal_effect.rs
Normal file
@@ -0,0 +1,153 @@
|
||||
use crate::{
|
||||
GameState,
|
||||
abilities::Healing,
|
||||
loading_assets::{AudioAssets, GameAssets},
|
||||
utils::{billboards::Billboard, observers::global_observer},
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
use rand::{Rng, thread_rng};
|
||||
|
||||
// Should not be a relationship because lightyear will silently track state for all relationships
|
||||
// and break if one end of the relationship isn't replicated and is despawned
|
||||
#[derive(Component)]
|
||||
struct HasHealingEffects {
|
||||
effects: Entity,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct HealingEffectsOf {
|
||||
of: Entity,
|
||||
}
|
||||
|
||||
#[derive(Component, Default)]
|
||||
#[require(Transform, InheritedVisibility)]
|
||||
struct HealParticleEffect {
|
||||
next_spawn: f32,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct HealParticle {
|
||||
start_scale: f32,
|
||||
end_scale: f32,
|
||||
start_pos: Vec3,
|
||||
end_pos: Vec3,
|
||||
start_time: f32,
|
||||
life_time: f32,
|
||||
}
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_systems(
|
||||
Update,
|
||||
(on_added, update_effects, update_particles).run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
|
||||
global_observer!(app, on_removed);
|
||||
}
|
||||
|
||||
fn on_added(
|
||||
mut commands: Commands,
|
||||
query: Query<Entity, Added<Healing>>,
|
||||
assets: Res<AudioAssets>,
|
||||
) {
|
||||
for entity in query.iter() {
|
||||
let effects = commands
|
||||
.spawn((
|
||||
Name::new("heal-particle-effect"),
|
||||
HealParticleEffect::default(),
|
||||
AudioPlayer::new(assets.healing.clone()),
|
||||
PlaybackSettings {
|
||||
mode: bevy::audio::PlaybackMode::Loop,
|
||||
..Default::default()
|
||||
},
|
||||
HealingEffectsOf { of: entity },
|
||||
))
|
||||
.id();
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(HasHealingEffects { effects });
|
||||
}
|
||||
}
|
||||
|
||||
fn on_removed(
|
||||
trigger: On<Remove, Healing>,
|
||||
mut commands: Commands,
|
||||
effects: Query<&HasHealingEffects>,
|
||||
) {
|
||||
let Ok(has_effects) = effects.get(trigger.event().entity) else {
|
||||
return;
|
||||
};
|
||||
commands.entity(has_effects.effects).try_despawn();
|
||||
commands
|
||||
.entity(trigger.event().entity)
|
||||
.remove::<HasHealingEffects>();
|
||||
}
|
||||
|
||||
fn update_effects(
|
||||
mut commands: Commands,
|
||||
mut query: Query<(&mut HealParticleEffect, &HealingEffectsOf, Entity)>,
|
||||
mut transforms: Query<&mut Transform>,
|
||||
time: Res<Time>,
|
||||
assets: Res<GameAssets>,
|
||||
) {
|
||||
const DISTANCE: f32 = 4.;
|
||||
let mut rng = thread_rng();
|
||||
|
||||
let now = time.elapsed_secs();
|
||||
for (mut effect, effects_of, e) in query.iter_mut() {
|
||||
// We have to manually track the healer's position because lightyear will try to synchronize
|
||||
// children and there's no reason to synchronize the particle effect entity when we're already
|
||||
// synchronizing `Healing`
|
||||
// (trying to ignore/avoid it by excluding the child from replication just causes crashes)
|
||||
let healer_pos = transforms.get(effects_of.of).unwrap().translation;
|
||||
transforms.get_mut(e).unwrap().translation = healer_pos;
|
||||
|
||||
if effect.next_spawn < now {
|
||||
let start_pos = Vec3::new(
|
||||
rng.gen_range(-DISTANCE..DISTANCE),
|
||||
2.,
|
||||
rng.gen_range(-DISTANCE..DISTANCE),
|
||||
);
|
||||
let max_distance = start_pos.length().max(0.8);
|
||||
let end_pos =
|
||||
start_pos + (start_pos.normalize() * -1.) * rng.gen_range(0.5..max_distance);
|
||||
let start_scale = rng.gen_range(0.7..1.0);
|
||||
let end_scale = rng.gen_range(0.1..start_scale);
|
||||
|
||||
commands.entity(e).with_child((
|
||||
Name::new("heal-particle"),
|
||||
SceneRoot(assets.mesh_heal_particle.clone()),
|
||||
Billboard::All,
|
||||
Transform::from_translation(start_pos),
|
||||
HealParticle {
|
||||
start_scale,
|
||||
end_scale,
|
||||
start_pos,
|
||||
end_pos,
|
||||
start_time: now,
|
||||
life_time: rng.gen_range(0.3..1.0),
|
||||
},
|
||||
));
|
||||
|
||||
effect.next_spawn = now + rng.gen_range(0.1..0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_particles(
|
||||
mut cmds: Commands,
|
||||
mut query: Query<(&mut Transform, &HealParticle, Entity)>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
for (mut transform, particle, e) in query.iter_mut() {
|
||||
if particle.start_time + particle.life_time < time.elapsed_secs() {
|
||||
cmds.entity(e).despawn();
|
||||
continue;
|
||||
}
|
||||
|
||||
let t = (time.elapsed_secs() - particle.start_time) / particle.life_time;
|
||||
|
||||
// info!("particle[{e:?}] t: {t}");
|
||||
transform.translation = particle.start_pos.lerp(particle.end_pos, t);
|
||||
transform.scale = Vec3::splat(particle.start_scale.lerp(particle.end_scale, t));
|
||||
}
|
||||
}
|
||||
93
crates/hedz_reloaded/src/client/player.rs
Normal file
93
crates/hedz_reloaded/src/client/player.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use crate::{
|
||||
global_observer,
|
||||
heads_database::{HeadControls, HeadsDatabase},
|
||||
loading_assets::AudioAssets,
|
||||
player::{LocalPlayer, PlayerBodyMesh},
|
||||
protocol::{ClientHeadChanged, PlaySound, PlayerId, messages::AssignClientPlayer},
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.init_state::<PlayerAssignmentState>();
|
||||
|
||||
app.add_systems(
|
||||
Update,
|
||||
receive_player_id.run_if(not(in_state(PlayerAssignmentState::Confirmed))),
|
||||
);
|
||||
|
||||
global_observer!(app, on_client_update_head_mesh);
|
||||
}
|
||||
|
||||
pub fn receive_player_id(
|
||||
mut commands: Commands,
|
||||
mut client_assignments: MessageReader<AssignClientPlayer>,
|
||||
mut next: ResMut<NextState<PlayerAssignmentState>>,
|
||||
mut local_id: Local<Option<PlayerId>>,
|
||||
players: Query<(Entity, &PlayerId), Changed<PlayerId>>,
|
||||
) {
|
||||
for &AssignClientPlayer(id) in client_assignments.read() {
|
||||
info!("player id `{}` received", id.id);
|
||||
|
||||
*local_id = Some(id);
|
||||
}
|
||||
|
||||
if let Some(local_id) = *local_id {
|
||||
for (entity, player_id) in players.iter() {
|
||||
if *player_id == local_id {
|
||||
commands.entity(entity).insert(LocalPlayer);
|
||||
next.set(PlayerAssignmentState::Confirmed);
|
||||
info!(
|
||||
"player entity {entity:?} confirmed with id `{}`",
|
||||
player_id.id
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Various states while trying to assign and match an ID to the player character.
|
||||
/// Every client is given an ID (its player index in the match) and every character controller
|
||||
/// is given an ID matching the client controlling it. This way the client can easily see which
|
||||
/// controller it owns.
|
||||
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, States)]
|
||||
pub enum PlayerAssignmentState {
|
||||
/// Waiting for the server to send an [`AssignClientPlayer`] message and replicate a [`PlayerId`]
|
||||
#[default]
|
||||
Waiting,
|
||||
/// Matching controller confirmed; a [`LocalPlayer`] exists
|
||||
Confirmed,
|
||||
}
|
||||
|
||||
fn on_client_update_head_mesh(
|
||||
trigger: On<ClientHeadChanged>,
|
||||
mut commands: Commands,
|
||||
body_mesh: Single<(Entity, &Children), With<PlayerBodyMesh>>,
|
||||
head_db: Res<HeadsDatabase>,
|
||||
audio_assets: Res<AudioAssets>,
|
||||
sfx: Query<&AudioPlayer>,
|
||||
) -> Result {
|
||||
let head = trigger.0 as usize;
|
||||
let (body_mesh, mesh_children) = *body_mesh;
|
||||
|
||||
let head_str = head_db.head_key(head);
|
||||
|
||||
commands.trigger(PlaySound::Head(head_str.to_string()));
|
||||
|
||||
//TODO: make part of full character mesh later
|
||||
for child in mesh_children.iter().filter(|child| sfx.contains(*child)) {
|
||||
commands.entity(child).despawn();
|
||||
}
|
||||
if head_db.head_stats(head).controls == HeadControls::Plane {
|
||||
commands.entity(body_mesh).with_child((
|
||||
Name::new("sfx"),
|
||||
AudioPlayer::new(audio_assets.jet.clone()),
|
||||
PlaybackSettings {
|
||||
mode: bevy::audio::PlaybackMode::Loop,
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
90
crates/hedz_reloaded/src/client/setup.rs
Normal file
90
crates/hedz_reloaded/src/client/setup.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
use crate::{DebugVisuals, GameState, camera::MainCamera, loading_assets::AudioAssets};
|
||||
use bevy::{
|
||||
audio::{PlaybackMode, Volume},
|
||||
core_pipeline::tonemapping::Tonemapping,
|
||||
prelude::*,
|
||||
render::view::ColorGrading,
|
||||
};
|
||||
use bevy_trenchbroom::TrenchBroomServer;
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
#[cfg(feature = "dbg")]
|
||||
{
|
||||
app.add_plugins(bevy_inspector_egui::bevy_egui::EguiPlugin::default());
|
||||
app.add_plugins(bevy_inspector_egui::quick::WorldInspectorPlugin::new());
|
||||
app.add_plugins(avian3d::prelude::PhysicsDebugPlugin::default());
|
||||
}
|
||||
|
||||
app.insert_resource(AmbientLight {
|
||||
color: Color::WHITE,
|
||||
brightness: 400.,
|
||||
..Default::default()
|
||||
});
|
||||
app.insert_resource(ClearColor(Color::BLACK));
|
||||
//TODO: let user control this
|
||||
app.insert_resource(GlobalVolume::new(Volume::Linear(0.4)));
|
||||
|
||||
app.add_systems(Startup, write_trenchbroom_config);
|
||||
app.add_systems(OnEnter(GameState::Playing), music);
|
||||
app.add_systems(Update, (set_materials_unlit, set_tonemapping, set_shadows));
|
||||
}
|
||||
|
||||
fn music(assets: Res<AudioAssets>, mut commands: Commands) {
|
||||
commands.spawn((
|
||||
Name::new("sfx-music"),
|
||||
AudioPlayer::new(assets.music.clone()),
|
||||
PlaybackSettings {
|
||||
mode: PlaybackMode::Loop,
|
||||
volume: Volume::Linear(0.6),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
|
||||
commands.spawn((
|
||||
Name::new("sfx-ambient"),
|
||||
AudioPlayer::new(assets.ambient.clone()),
|
||||
PlaybackSettings {
|
||||
mode: PlaybackMode::Loop,
|
||||
volume: Volume::Linear(0.8),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
fn write_trenchbroom_config(server: Res<TrenchBroomServer>, type_registry: Res<AppTypeRegistry>) {
|
||||
if let Err(e) = server
|
||||
.config
|
||||
.write_game_config("trenchbroom/hedz", &type_registry.read())
|
||||
{
|
||||
warn!("Failed to write trenchbroom config: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_tonemapping(
|
||||
mut cams: Query<(&mut Tonemapping, &mut ColorGrading), With<MainCamera>>,
|
||||
visuals: Res<DebugVisuals>,
|
||||
) {
|
||||
for (mut tm, mut color) in cams.iter_mut() {
|
||||
*tm = visuals.tonemapping;
|
||||
color.global.exposure = visuals.exposure;
|
||||
}
|
||||
}
|
||||
|
||||
fn set_materials_unlit(
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
visuals: Res<DebugVisuals>,
|
||||
) {
|
||||
if !materials.is_changed() {
|
||||
return;
|
||||
}
|
||||
|
||||
for (_, material) in materials.iter_mut() {
|
||||
material.unlit = visuals.unlit;
|
||||
}
|
||||
}
|
||||
|
||||
fn set_shadows(mut lights: Query<&mut DirectionalLight>, visuals: Res<DebugVisuals>) {
|
||||
for mut l in lights.iter_mut() {
|
||||
l.shadows_enabled = visuals.shadows;
|
||||
}
|
||||
}
|
||||
66
crates/hedz_reloaded/src/client/sounds.rs
Normal file
66
crates/hedz_reloaded/src/client/sounds.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use crate::{global_observer, loading_assets::AudioAssets, protocol::PlaySound};
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
global_observer!(app, on_spawn_sounds);
|
||||
}
|
||||
|
||||
fn on_spawn_sounds(
|
||||
trigger: On<PlaySound>,
|
||||
mut commands: Commands,
|
||||
// settings: SettingsRead,
|
||||
assets: Res<AudioAssets>,
|
||||
) {
|
||||
let event = trigger.event();
|
||||
|
||||
// if !settings.is_sound_on() {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
let source = match event {
|
||||
PlaySound::Hit => {
|
||||
let version = rand::random::<u8>() % 3;
|
||||
assets.hit[version as usize].clone()
|
||||
}
|
||||
PlaySound::KeyCollect => assets.key_collect.clone(),
|
||||
PlaySound::Gun => assets.gun.clone(),
|
||||
PlaySound::Crossbow => assets.crossbow.clone(),
|
||||
PlaySound::Gate => assets.gate.clone(),
|
||||
PlaySound::CashCollect => assets.cash_collect.clone(),
|
||||
PlaySound::Selection => assets.selection.clone(),
|
||||
PlaySound::Throw => assets.throw.clone(),
|
||||
PlaySound::ThrowHit => assets.throw_explosion.clone(),
|
||||
PlaySound::Reloaded => assets.reloaded.clone(),
|
||||
PlaySound::Invalid => assets.invalid.clone(),
|
||||
PlaySound::CashHeal => assets.cash_heal.clone(),
|
||||
PlaySound::HeadDrop => assets.head_drop.clone(),
|
||||
PlaySound::HeadCollect => assets.head_collect.clone(),
|
||||
PlaySound::SecretHeadCollect => assets.secret_head_collect.clone(),
|
||||
PlaySound::MissileExplosion => assets.missile_explosion.clone(),
|
||||
PlaySound::Beaming => assets.beaming.clone(),
|
||||
PlaySound::Backpack { open } => {
|
||||
if *open {
|
||||
assets.backpack_open.clone()
|
||||
} else {
|
||||
assets.backpack_close.clone()
|
||||
}
|
||||
}
|
||||
PlaySound::Head(name) => {
|
||||
let filename = format!("{name}.ogg");
|
||||
assets
|
||||
.head
|
||||
.get(filename.as_str())
|
||||
.unwrap_or_else(|| panic!("invalid head '{filename}'"))
|
||||
.clone()
|
||||
}
|
||||
};
|
||||
|
||||
commands.spawn((
|
||||
Name::new("sfx"),
|
||||
AudioPlayer::new(source),
|
||||
PlaybackSettings {
|
||||
mode: bevy::audio::PlaybackMode::Despawn,
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
93
crates/hedz_reloaded/src/client/steam.rs
Normal file
93
crates/hedz_reloaded/src/client/steam.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use bevy::prelude::*;
|
||||
use bevy_steamworks::{Client, FriendFlags, SteamworksEvent, SteamworksPlugin};
|
||||
use std::io::{Read, Write};
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
let app_id = 1603000;
|
||||
|
||||
// should only be done in production builds
|
||||
#[cfg(not(debug_assertions))]
|
||||
if steamworks::restart_app_if_necessary(app_id.into()) {
|
||||
info!("Restarting app via steam");
|
||||
return;
|
||||
}
|
||||
|
||||
info!("steam app init: {app_id}");
|
||||
|
||||
match SteamworksPlugin::init_app(app_id) {
|
||||
Ok(plugin) => {
|
||||
info!("steam app init done");
|
||||
app.add_plugins(plugin);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("steam init error: {e:?}");
|
||||
}
|
||||
};
|
||||
|
||||
app.add_systems(
|
||||
Startup,
|
||||
(test_steam_system, log_steam_events)
|
||||
.chain()
|
||||
.run_if(resource_exists::<Client>),
|
||||
);
|
||||
}
|
||||
|
||||
fn log_steam_events(mut events: MessageReader<SteamworksEvent>) {
|
||||
for e in events.read() {
|
||||
info!("steam ev: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
fn test_steam_system(steam_client: Res<Client>) {
|
||||
steam_client.matchmaking().request_lobby_list(|list| {
|
||||
let Ok(list) = list else { return };
|
||||
|
||||
info!("lobby list: [{}]", list.len());
|
||||
for (i, l) in list.iter().enumerate() {
|
||||
info!("lobby [{i}]: {:?}", l);
|
||||
}
|
||||
});
|
||||
|
||||
steam_client
|
||||
.matchmaking()
|
||||
.create_lobby(
|
||||
steamworks::LobbyType::FriendsOnly,
|
||||
4,
|
||||
|result| match result {
|
||||
Ok(lobby_id) => {
|
||||
info!("Created lobby with ID: {:?}", lobby_id);
|
||||
}
|
||||
Err(e) => error!("Failed to create lobby: {}", e),
|
||||
},
|
||||
);
|
||||
|
||||
for friend in steam_client.friends().get_friends(FriendFlags::IMMEDIATE) {
|
||||
info!(
|
||||
"Steam Friend: {:?} - {}({:?})",
|
||||
friend.id(),
|
||||
friend.name(),
|
||||
friend.state()
|
||||
);
|
||||
}
|
||||
|
||||
steam_client
|
||||
.remote_storage()
|
||||
.set_cloud_enabled_for_app(true);
|
||||
let f = steam_client.remote_storage().file("hedz_data.dat");
|
||||
if f.exists() {
|
||||
let mut buf = String::new();
|
||||
if let Err(e) = f.read().read_to_string(&mut buf) {
|
||||
error!("File read error: {}", e);
|
||||
} else {
|
||||
info!("File content: {}", buf);
|
||||
}
|
||||
} else {
|
||||
info!("File does not exist");
|
||||
|
||||
if let Err(e) = f.write().write_all(String::from("hello world").as_bytes()) {
|
||||
error!("steam cloud error: {}", e);
|
||||
} else {
|
||||
info!("steam cloud saved");
|
||||
}
|
||||
}
|
||||
}
|
||||
7
crates/hedz_reloaded/src/client/ui/mod.rs
Normal file
7
crates/hedz_reloaded/src/client/ui/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod pause;
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_plugins(pause::plugin);
|
||||
}
|
||||
188
crates/hedz_reloaded/src/client/ui/pause.rs
Normal file
188
crates/hedz_reloaded/src/client/ui/pause.rs
Normal file
@@ -0,0 +1,188 @@
|
||||
use crate::{
|
||||
GameState, HEDZ_GREEN, HEDZ_PURPLE, client::control::CharacterInputEnabled,
|
||||
loading_assets::UIAssets,
|
||||
};
|
||||
use bevy::{color::palettes::css::BLACK, prelude::*};
|
||||
|
||||
#[derive(States, Default, Clone, Eq, PartialEq, Debug, Hash)]
|
||||
#[states(scoped_entities)]
|
||||
enum PauseMenuState {
|
||||
#[default]
|
||||
Closed,
|
||||
Open,
|
||||
}
|
||||
|
||||
#[derive(Component, PartialEq, Eq, Clone, Copy)]
|
||||
enum ProgressBar {
|
||||
Music,
|
||||
Sound,
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct PauseMenuSelection(ProgressBar);
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.init_state::<PauseMenuState>();
|
||||
|
||||
app.add_systems(Update, open_pause_menu.run_if(in_state(GameState::Playing)));
|
||||
app.add_systems(
|
||||
Update,
|
||||
(selection_input, selection_changed).run_if(in_state(PauseMenuState::Open)),
|
||||
);
|
||||
app.add_systems(OnEnter(PauseMenuState::Open), setup);
|
||||
}
|
||||
|
||||
fn open_pause_menu(
|
||||
state: Res<State<PauseMenuState>>,
|
||||
mut next_state: ResMut<NextState<PauseMenuState>>,
|
||||
mut char_controls: ResMut<CharacterInputEnabled>,
|
||||
keyboard: Res<ButtonInput<KeyCode>>,
|
||||
) {
|
||||
if keyboard.just_pressed(KeyCode::Escape) {
|
||||
let menu_open = match state.get() {
|
||||
PauseMenuState::Closed => {
|
||||
next_state.set(PauseMenuState::Open);
|
||||
true
|
||||
}
|
||||
PauseMenuState::Open => {
|
||||
next_state.set(PauseMenuState::Closed);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if menu_open {
|
||||
*char_controls = CharacterInputEnabled::Off;
|
||||
} else {
|
||||
*char_controls = CharacterInputEnabled::On;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup(mut commands: Commands, assets: Res<UIAssets>) {
|
||||
commands.spawn((
|
||||
Name::new("pause-menu"),
|
||||
Node {
|
||||
width: Val::Percent(100.0),
|
||||
height: Val::Percent(100.0),
|
||||
flex_direction: FlexDirection::Column,
|
||||
align_items: AlignItems::Center,
|
||||
justify_content: JustifyContent::Center,
|
||||
row_gap: Val::Px(10.),
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(Color::linear_rgba(0., 0., 0., 0.6)),
|
||||
DespawnOnExit(PauseMenuState::Open),
|
||||
children![
|
||||
spawn_progress(ProgressBar::Music, 100, assets.font.clone()),
|
||||
spawn_progress(ProgressBar::Sound, 80, assets.font.clone())
|
||||
],
|
||||
));
|
||||
|
||||
commands.insert_resource(PauseMenuSelection(ProgressBar::Music));
|
||||
}
|
||||
|
||||
fn spawn_progress(bar: ProgressBar, value: u8, font: Handle<Font>) -> impl Bundle {
|
||||
(
|
||||
Node {
|
||||
width: Val::Px(500.0),
|
||||
height: Val::Px(60.0),
|
||||
border: UiRect::all(Val::Px(8.)),
|
||||
align_items: AlignItems::Center,
|
||||
row_gap: Val::Px(10.),
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(BLACK.into()),
|
||||
BorderRadius::all(Val::Px(100.)),
|
||||
BorderColor::all(HEDZ_PURPLE),
|
||||
BoxShadow::new(
|
||||
BLACK.into(),
|
||||
Val::Px(2.),
|
||||
Val::Px(2.),
|
||||
Val::Px(4.),
|
||||
Val::Px(4.),
|
||||
),
|
||||
bar,
|
||||
children![
|
||||
(
|
||||
Node {
|
||||
width: Val::Percent(100.0),
|
||||
margin: UiRect::left(Val::Px(10.)),
|
||||
..default()
|
||||
},
|
||||
Text::new(match bar {
|
||||
ProgressBar::Music => "MUSIC".to_string(),
|
||||
ProgressBar::Sound => "SOUND".to_string(),
|
||||
}),
|
||||
TextFont {
|
||||
font: font.clone(),
|
||||
font_size: 16.0,
|
||||
..Default::default()
|
||||
},
|
||||
TextColor(HEDZ_GREEN.into()),
|
||||
TextLayout::new_with_justify(Justify::Left),
|
||||
),
|
||||
(
|
||||
Node {
|
||||
margin: UiRect::horizontal(Val::Px(5.)),
|
||||
..default()
|
||||
},
|
||||
Text::new("<".to_string()),
|
||||
TextFont {
|
||||
font: font.clone(),
|
||||
font_size: 16.0,
|
||||
..Default::default()
|
||||
},
|
||||
TextColor(HEDZ_GREEN.into()),
|
||||
TextLayout::new_with_justify(Justify::Left).with_no_wrap(),
|
||||
),
|
||||
(
|
||||
Text::new(format!("{value}",)),
|
||||
TextFont {
|
||||
font: font.clone(),
|
||||
font_size: 16.0,
|
||||
..Default::default()
|
||||
},
|
||||
TextColor(HEDZ_GREEN.into()),
|
||||
TextLayout::new_with_justify(Justify::Left).with_no_wrap(),
|
||||
),
|
||||
(
|
||||
Node {
|
||||
margin: UiRect::horizontal(Val::Px(5.)),
|
||||
..default()
|
||||
},
|
||||
Text::new(">".to_string()),
|
||||
TextFont {
|
||||
font,
|
||||
font_size: 16.0,
|
||||
..Default::default()
|
||||
},
|
||||
TextColor(HEDZ_GREEN.into()),
|
||||
TextLayout::new_with_justify(Justify::Left).with_no_wrap(),
|
||||
)
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
fn selection_input(mut state: ResMut<PauseMenuSelection>, keyboard: Res<ButtonInput<KeyCode>>) {
|
||||
if keyboard.just_pressed(KeyCode::ArrowUp) || keyboard.just_pressed(KeyCode::ArrowDown) {
|
||||
state.0 = match state.0 {
|
||||
ProgressBar::Music => ProgressBar::Sound,
|
||||
ProgressBar::Sound => ProgressBar::Music,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn selection_changed(
|
||||
state: Res<PauseMenuSelection>,
|
||||
mut query: Query<(&mut BorderColor, &ProgressBar)>,
|
||||
) {
|
||||
if state.is_changed() {
|
||||
for (mut border, bar) in query.iter_mut() {
|
||||
*border = BorderColor::all(if *bar == state.0 {
|
||||
HEDZ_GREEN
|
||||
} else {
|
||||
HEDZ_PURPLE
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user