Replicate Sounds (#68)
This commit is contained in:
@@ -11,6 +11,7 @@ dbg = ["avian3d/debug-plugin", "dep:bevy-inspector-egui", "shared/dbg"]
|
||||
[dependencies]
|
||||
avian3d = { workspace = true }
|
||||
bevy = { workspace = true, default-features = false, features = [
|
||||
"bevy_audio",
|
||||
"bevy_window",
|
||||
"bevy_winit",
|
||||
] }
|
||||
|
||||
208
crates/client/src/backpack/backpack_ui.rs
Normal file
208
crates/client/src/backpack/backpack_ui.rs
Normal file
@@ -0,0 +1,208 @@
|
||||
use crate::{GameState, HEDZ_GREEN, heads::HeadsImages, loading_assets::UIAssets};
|
||||
use bevy::{ecs::spawn::SpawnIter, prelude::*};
|
||||
use shared::backpack::backpack_ui::{
|
||||
BackpackCountText, BackpackMarker, BackpackUiState, HEAD_SLOTS, HeadDamage, HeadImage,
|
||||
HeadSelector,
|
||||
};
|
||||
|
||||
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..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(JustifyText::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/client/src/backpack/mod.rs
Normal file
7
crates/client/src/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);
|
||||
}
|
||||
@@ -21,11 +21,15 @@ use shared::{
|
||||
control::ControlState,
|
||||
global_observer,
|
||||
player::Player,
|
||||
protocol::{DespawnTbMapEntity, TbMapEntityId, TbMapEntityMapping},
|
||||
protocol::{
|
||||
ClientEnteredPlaying, TbMapEntityId, TbMapEntityMapping,
|
||||
channels::UnorderedReliableChannel, messages::DespawnTbMapEntity,
|
||||
},
|
||||
tb_entities::{Platform, PlatformTarget},
|
||||
};
|
||||
use std::{
|
||||
env::current_exe,
|
||||
fs::File,
|
||||
io::{BufRead, BufReader},
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
process::Stdio,
|
||||
@@ -43,7 +47,7 @@ pub fn plugin(app: &mut App) {
|
||||
parse_local_server_stdout.run_if(resource_exists::<LocalServerStdout>),
|
||||
);
|
||||
app.add_systems(Last, close_server_processes);
|
||||
app.add_systems(FixedUpdate, despawn_absent_map_entities);
|
||||
app.add_systems(Update, despawn_absent_map_entities);
|
||||
|
||||
global_observer!(app, on_connecting);
|
||||
global_observer!(app, on_connection_failed);
|
||||
@@ -124,9 +128,11 @@ fn on_connection_succeeded(
|
||||
_trigger: Trigger<OnAdd, Connected>,
|
||||
state: Res<State<GameState>>,
|
||||
mut change_state: ResMut<NextState<GameState>>,
|
||||
mut sender: Single<&mut TriggerSender<ClientEnteredPlaying>>,
|
||||
) {
|
||||
if *state == GameState::Connecting {
|
||||
change_state.set(GameState::Playing);
|
||||
sender.trigger::<UnorderedReliableChannel>(ClientEnteredPlaying);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +154,7 @@ fn on_connection_failed(
|
||||
mut commands: Commands,
|
||||
client_active: Query<&ClientActive>,
|
||||
mut opened_server: Local<bool>,
|
||||
) {
|
||||
) -> Result {
|
||||
let disconnected = disconnected.get(trigger.target()).unwrap();
|
||||
if *opened_server {
|
||||
panic!(
|
||||
@@ -164,11 +170,13 @@ fn on_connection_failed(
|
||||
// the server executable is assumed to be adjacent to the client executable
|
||||
let mut exe_path = current_exe().expect("failed to get path of client executable");
|
||||
exe_path.set_file_name("server");
|
||||
let server_log_file = File::create("server.log")?;
|
||||
let mut server_process = std::process::Command::new(exe_path)
|
||||
.args(["--timeout", "60", "--close-on-client-disconnect"])
|
||||
.env("NO_COLOR", "1")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.stderr(server_log_file)
|
||||
.spawn()
|
||||
.expect("failed to start server");
|
||||
let server_stdout = server_process.stdout.take().unwrap();
|
||||
@@ -195,6 +203,8 @@ fn on_connection_failed(
|
||||
|
||||
*opened_server = true;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Event)]
|
||||
@@ -206,6 +216,8 @@ fn parse_local_server_stdout(mut commands: Commands, mut stdout: ResMut<LocalSer
|
||||
while let Ok(line) = stdout.0.get().try_recv() {
|
||||
if let "hedz.server_started" = &line[..] {
|
||||
commands.trigger(LocalServerStarted);
|
||||
} else {
|
||||
info!("SERVER: {line}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
151
crates/client/src/heal_effect.rs
Normal file
151
crates/client/src/heal_effect.rs
Normal file
@@ -0,0 +1,151 @@
|
||||
use crate::{
|
||||
GameState, abilities::Healing, loading_assets::GameAssets, utils::billboards::Billboard,
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
use rand::{Rng, thread_rng};
|
||||
use shared::{loading_assets::AudioAssets, utils::observers::global_observer};
|
||||
|
||||
// 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: Trigger<OnRemove, Healing>,
|
||||
mut commands: Commands,
|
||||
effects: Query<&HasHealingEffects>,
|
||||
) {
|
||||
let Ok(has_effects) = effects.get(trigger.target()) else {
|
||||
return;
|
||||
};
|
||||
commands.entity(has_effects.effects).try_despawn();
|
||||
commands
|
||||
.entity(trigger.target())
|
||||
.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));
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
mod backpack;
|
||||
mod client;
|
||||
mod debug;
|
||||
mod enemy;
|
||||
mod heal_effect;
|
||||
mod player;
|
||||
mod sounds;
|
||||
mod steam;
|
||||
mod ui;
|
||||
|
||||
use crate::utils::{auto_rotate, explosions};
|
||||
use avian3d::prelude::*;
|
||||
use bevy::{
|
||||
audio::{PlaybackMode, Volume},
|
||||
@@ -22,7 +25,6 @@ use lightyear::prelude::client::ClientPlugins;
|
||||
use loading_assets::AudioAssets;
|
||||
use shared::*;
|
||||
use std::time::Duration;
|
||||
use utils::{billboards, sprite_3d_animation, squish_animation, trail};
|
||||
|
||||
fn main() {
|
||||
let mut app = App::new();
|
||||
@@ -87,43 +89,46 @@ fn main() {
|
||||
// });
|
||||
}
|
||||
|
||||
app.add_plugins(ai::plugin);
|
||||
app.add_plugins(animation::plugin);
|
||||
app.add_plugins(character::plugin);
|
||||
app.add_plugins(cash::plugin);
|
||||
app.add_plugins(enemy::plugin);
|
||||
app.add_plugins(player::plugin);
|
||||
app.add_plugins(gates::plugin);
|
||||
app.add_plugins(platforms::plugin);
|
||||
app.add_plugins(protocol::plugin);
|
||||
app.add_plugins(movables::plugin);
|
||||
app.add_plugins(billboards::plugin);
|
||||
app.add_plugins(aim::plugin);
|
||||
app.add_plugins(client::plugin);
|
||||
app.add_plugins(npc::plugin);
|
||||
app.add_plugins(keys::plugin);
|
||||
app.add_plugins(squish_animation::plugin);
|
||||
app.add_plugins(cutscene::plugin);
|
||||
app.add_plugins(control::plugin);
|
||||
app.add_plugins(sounds::plugin);
|
||||
app.add_plugins(camera::plugin);
|
||||
app.add_plugins(shared::ai::plugin);
|
||||
app.add_plugins(shared::animation::plugin);
|
||||
app.add_plugins(shared::character::plugin);
|
||||
app.add_plugins(shared::cash::plugin);
|
||||
app.add_plugins(shared::player::plugin);
|
||||
app.add_plugins(shared::gates::plugin);
|
||||
app.add_plugins(shared::platforms::plugin);
|
||||
app.add_plugins(shared::protocol::plugin);
|
||||
app.add_plugins(shared::movables::plugin);
|
||||
app.add_plugins(shared::utils::billboards::plugin);
|
||||
app.add_plugins(shared::aim::plugin);
|
||||
app.add_plugins(shared::npc::plugin);
|
||||
app.add_plugins(shared::keys::plugin);
|
||||
app.add_plugins(shared::utils::squish_animation::plugin);
|
||||
app.add_plugins(shared::cutscene::plugin);
|
||||
app.add_plugins(shared::control::plugin);
|
||||
app.add_plugins(shared::camera::plugin);
|
||||
app.add_plugins(shared::backpack::plugin);
|
||||
app.add_plugins(shared::loading_assets::LoadingPlugin);
|
||||
app.add_plugins(shared::loading_map::plugin);
|
||||
app.add_plugins(shared::utils::sprite_3d_animation::plugin);
|
||||
app.add_plugins(shared::abilities::plugin);
|
||||
app.add_plugins(shared::heads::plugin);
|
||||
app.add_plugins(shared::hitpoints::plugin);
|
||||
app.add_plugins(shared::cash_heal::plugin);
|
||||
app.add_plugins(shared::utils::plugin);
|
||||
app.add_plugins(shared::water::plugin);
|
||||
app.add_plugins(shared::head_drop::plugin);
|
||||
app.add_plugins(shared::utils::trail::plugin);
|
||||
app.add_plugins(shared::utils::auto_rotate::plugin);
|
||||
app.add_plugins(shared::tb_entities::plugin);
|
||||
app.add_plugins(shared::utils::explosions::plugin);
|
||||
|
||||
app.add_plugins(backpack::plugin);
|
||||
app.add_plugins(loading_assets::LoadingPlugin);
|
||||
app.add_plugins(loading_map::plugin);
|
||||
app.add_plugins(sprite_3d_animation::plugin);
|
||||
app.add_plugins(abilities::plugin);
|
||||
app.add_plugins(heads::plugin);
|
||||
app.add_plugins(hitpoints::plugin);
|
||||
app.add_plugins(cash_heal::plugin);
|
||||
app.add_plugins(client::plugin);
|
||||
app.add_plugins(debug::plugin);
|
||||
app.add_plugins(utils::plugin);
|
||||
app.add_plugins(water::plugin);
|
||||
app.add_plugins(head_drop::plugin);
|
||||
app.add_plugins(trail::plugin);
|
||||
app.add_plugins(auto_rotate::plugin);
|
||||
app.add_plugins(enemy::plugin);
|
||||
app.add_plugins(heal_effect::plugin);
|
||||
app.add_plugins(tb_entities::plugin);
|
||||
app.add_plugins(explosions::plugin);
|
||||
app.add_plugins(player::plugin);
|
||||
app.add_plugins(sounds::plugin);
|
||||
app.add_plugins(ui::plugin);
|
||||
|
||||
app.init_state::<GameState>();
|
||||
|
||||
115
crates/client/src/player.rs
Normal file
115
crates/client/src/player.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use crate::{
|
||||
global_observer,
|
||||
heads_database::{HeadControls, HeadsDatabase},
|
||||
loading_assets::AudioAssets,
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
use lightyear::prelude::MessageReceiver;
|
||||
use shared::{
|
||||
player::PlayerBodyMesh,
|
||||
protocol::{PlaySound, PlayerId, events::ClientHeadChanged, messages::AssignClientPlayer},
|
||||
};
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.register_type::<ClientPlayerId>();
|
||||
app.register_type::<LocalPlayer>();
|
||||
|
||||
app.init_state::<PlayerAssignmentState>();
|
||||
|
||||
app.add_systems(
|
||||
Update,
|
||||
receive_player_id.run_if(in_state(PlayerAssignmentState::Waiting)),
|
||||
);
|
||||
app.add_systems(
|
||||
Update,
|
||||
match_player_id.run_if(in_state(PlayerAssignmentState::IdReceived)),
|
||||
);
|
||||
|
||||
global_observer!(app, on_update_head_mesh);
|
||||
}
|
||||
|
||||
#[derive(Resource, Reflect)]
|
||||
#[reflect(Resource)]
|
||||
pub struct ClientPlayerId {
|
||||
id: u8,
|
||||
}
|
||||
|
||||
fn receive_player_id(
|
||||
mut commands: Commands,
|
||||
mut recv: Single<&mut MessageReceiver<AssignClientPlayer>>,
|
||||
mut next: ResMut<NextState<PlayerAssignmentState>>,
|
||||
) {
|
||||
for AssignClientPlayer(id) in recv.receive() {
|
||||
commands.insert_resource(ClientPlayerId { id });
|
||||
next.set(PlayerAssignmentState::IdReceived);
|
||||
info!("player id `{id}` received");
|
||||
}
|
||||
}
|
||||
|
||||
fn match_player_id(
|
||||
mut commands: Commands,
|
||||
players: Query<(Entity, &PlayerId), Changed<PlayerId>>,
|
||||
client: Res<ClientPlayerId>,
|
||||
mut next: ResMut<NextState<PlayerAssignmentState>>,
|
||||
) {
|
||||
for (entity, player) in players.iter() {
|
||||
if player.id == client.id {
|
||||
commands.entity(entity).insert(LocalPlayer);
|
||||
next.set(PlayerAssignmentState::Confirmed);
|
||||
info!("player entity {entity:?} confirmed with id `{}`", player.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
|
||||
#[default]
|
||||
Waiting,
|
||||
/// Received an [`AssignClientPlayer`], querying for a matching controller
|
||||
IdReceived,
|
||||
/// Matching controller confirmed; a [`LocalPlayer`] exists
|
||||
Confirmed,
|
||||
}
|
||||
|
||||
#[derive(Component, Debug, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct LocalPlayer;
|
||||
|
||||
fn on_update_head_mesh(
|
||||
trigger: Trigger<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(())
|
||||
}
|
||||
67
crates/client/src/sounds.rs
Normal file
67
crates/client/src/sounds.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use crate::{global_observer, loading_assets::AudioAssets};
|
||||
use bevy::prelude::*;
|
||||
use shared::protocol::PlaySound;
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
global_observer!(app, on_spawn_sounds);
|
||||
}
|
||||
|
||||
fn on_spawn_sounds(
|
||||
trigger: Trigger<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()
|
||||
},
|
||||
));
|
||||
}
|
||||
Reference in New Issue
Block a user