Replicate Sounds (#68)
This commit is contained in:
@@ -17,7 +17,6 @@ bevy = { version = "0.16.0", default-features = false, features = [
|
||||
"animation",
|
||||
"async_executor",
|
||||
"bevy_asset",
|
||||
"bevy_audio",
|
||||
"bevy_color",
|
||||
"bevy_core_pipeline",
|
||||
"bevy_gilrs",
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,19 @@ use crate::{
|
||||
};
|
||||
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)]
|
||||
@@ -25,20 +38,52 @@ pub fn plugin(app: &mut App) {
|
||||
Update,
|
||||
(on_added, update_effects, update_particles).run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
|
||||
global_observer!(app, on_removed);
|
||||
}
|
||||
|
||||
fn on_added(mut cmds: Commands, query: Query<&Healing, Added<Healing>>) {
|
||||
for healing in query.iter() {
|
||||
cmds.entity(healing.0).insert((
|
||||
Name::new("heal-particle-effect"),
|
||||
HealParticleEffect::default(),
|
||||
));
|
||||
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 cmds: Commands,
|
||||
mut query: Query<(&mut HealParticleEffect, Entity)>,
|
||||
mut commands: Commands,
|
||||
mut query: Query<(&mut HealParticleEffect, &HealingEffectsOf, Entity)>,
|
||||
mut transforms: Query<&mut Transform>,
|
||||
time: Res<Time>,
|
||||
assets: Res<GameAssets>,
|
||||
) {
|
||||
@@ -46,7 +91,14 @@ fn update_effects(
|
||||
let mut rng = thread_rng();
|
||||
|
||||
let now = time.elapsed_secs();
|
||||
for (mut effect, e) in query.iter_mut() {
|
||||
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),
|
||||
@@ -59,7 +111,7 @@ fn update_effects(
|
||||
let start_scale = rng.gen_range(0.7..1.0);
|
||||
let end_scale = rng.gen_range(0.1..start_scale);
|
||||
|
||||
cmds.entity(e).with_child((
|
||||
commands.entity(e).with_child((
|
||||
Name::new("heal-particle"),
|
||||
SceneRoot(assets.mesh_heal_particle.clone()),
|
||||
Billboard::All,
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -1,28 +1,6 @@
|
||||
use crate::{global_observer, loading_assets::AudioAssets};
|
||||
use bevy::prelude::*;
|
||||
|
||||
#[derive(Event, Clone, Debug)]
|
||||
pub enum PlaySound {
|
||||
Hit,
|
||||
KeyCollect,
|
||||
Gun,
|
||||
Throw,
|
||||
ThrowHit,
|
||||
Gate,
|
||||
CashCollect,
|
||||
HeadCollect,
|
||||
SecretHeadCollect,
|
||||
HeadDrop,
|
||||
Selection,
|
||||
Invalid,
|
||||
MissileExplosion,
|
||||
Reloaded,
|
||||
CashHeal,
|
||||
Crossbow,
|
||||
Beaming,
|
||||
Backpack { open: bool },
|
||||
Head(String),
|
||||
}
|
||||
use shared::protocol::PlaySound;
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
global_observer!(app, on_spawn_sounds);
|
||||
@@ -31,7 +9,6 @@ pub fn plugin(app: &mut App) {
|
||||
fn on_spawn_sounds(
|
||||
trigger: Trigger<PlaySound>,
|
||||
mut commands: Commands,
|
||||
// sound_res: Res<AudioAssets>,
|
||||
// settings: SettingsRead,
|
||||
assets: Res<AudioAssets>,
|
||||
) {
|
||||
92
crates/server/src/backpack/backpack_ui.rs
Normal file
92
crates/server/src/backpack/backpack_ui.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use bevy::prelude::*;
|
||||
use lightyear::prelude::input::native::ActionState;
|
||||
use shared::{
|
||||
GameState,
|
||||
backpack::{
|
||||
BackbackSwapEvent, Backpack, UiHeadState,
|
||||
backpack_ui::{BackpackUiState, HEAD_SLOTS},
|
||||
},
|
||||
control::ControlState,
|
||||
protocol::PlaySound,
|
||||
};
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_systems(
|
||||
FixedUpdate,
|
||||
sync_on_change.run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
app.add_systems(FixedUpdate, swap_head_inputs);
|
||||
}
|
||||
|
||||
fn swap_head_inputs(
|
||||
player: Query<(&ActionState<ControlState>, Ref<Backpack>)>,
|
||||
mut commands: Commands,
|
||||
mut state: Single<&mut BackpackUiState>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
for (controls, backpack) in player.iter() {
|
||||
if state.count == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
if controls.backpack_toggle {
|
||||
state.open = !state.open;
|
||||
commands.trigger(PlaySound::Backpack { open: state.open });
|
||||
}
|
||||
|
||||
if !state.open {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut changed = false;
|
||||
if controls.backpack_left && state.current_slot > 0 {
|
||||
state.current_slot -= 1;
|
||||
changed = true;
|
||||
}
|
||||
if controls.backpack_right && state.current_slot < state.count.saturating_sub(1) {
|
||||
state.current_slot += 1;
|
||||
changed = true;
|
||||
}
|
||||
if controls.backpack_swap {
|
||||
commands.trigger(BackbackSwapEvent(state.current_slot));
|
||||
}
|
||||
|
||||
if changed {
|
||||
commands.trigger(PlaySound::Selection);
|
||||
sync(&backpack, &mut state, time.elapsed_secs());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_on_change(
|
||||
backpack: Query<Ref<Backpack>>,
|
||||
mut state: Single<&mut BackpackUiState>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
for backpack in backpack.iter() {
|
||||
if backpack.is_changed() || backpack.reloading() {
|
||||
sync(&backpack, &mut state, time.elapsed_secs());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sync(backpack: &Backpack, state: &mut Single<&mut BackpackUiState>, time: f32) {
|
||||
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(UiHeadState::new(*head, time));
|
||||
} else {
|
||||
state.heads[i] = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
7
crates/server/src/backpack/mod.rs
Normal file
7
crates/server/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);
|
||||
}
|
||||
@@ -1,11 +1,5 @@
|
||||
use avian3d::prelude::*;
|
||||
use bevy::{
|
||||
app::plugin_group,
|
||||
audio::Volume,
|
||||
core_pipeline::tonemapping::Tonemapping,
|
||||
log::{BoxedLayer, tracing_subscriber::Layer},
|
||||
prelude::*,
|
||||
};
|
||||
use bevy::{app::plugin_group, core_pipeline::tonemapping::Tonemapping, prelude::*};
|
||||
use bevy_common_assets::ron::RonAssetPlugin;
|
||||
use bevy_sprite3d::Sprite3dPlugin;
|
||||
use bevy_trenchbroom::prelude::*;
|
||||
@@ -14,10 +8,12 @@ use lightyear::prelude::server::ServerPlugins;
|
||||
use shared::{DebugVisuals, GameState, heads_database::HeadDatabaseAsset};
|
||||
use std::time::Duration;
|
||||
|
||||
mod backpack;
|
||||
mod config;
|
||||
mod player;
|
||||
mod server;
|
||||
mod tb_entities;
|
||||
mod utils;
|
||||
|
||||
plugin_group! {
|
||||
pub struct DefaultPlugins {
|
||||
@@ -44,7 +40,6 @@ plugin_group! {
|
||||
bevy::ui:::UiPlugin,
|
||||
bevy::pbr:::PbrPlugin,
|
||||
bevy::gltf:::GltfPlugin,
|
||||
bevy::audio:::AudioPlugin,
|
||||
bevy::gilrs:::GilrsPlugin,
|
||||
bevy::animation:::AnimationPlugin,
|
||||
bevy::gizmos:::GizmoPlugin,
|
||||
@@ -54,23 +49,6 @@ plugin_group! {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_to_file_layer(_app: &mut App) -> Option<BoxedLayer> {
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open("server.log")
|
||||
.ok()?;
|
||||
Some(
|
||||
bevy::log::tracing_subscriber::fmt::layer()
|
||||
.with_writer(std::sync::Mutex::new(file))
|
||||
.with_ansi(false)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.boxed(),
|
||||
)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut app = App::new();
|
||||
|
||||
@@ -88,7 +66,7 @@ fn main() {
|
||||
filter: "info,lightyear_replication=off".into(),
|
||||
level: bevy::log::Level::INFO,
|
||||
// provide custom log layer to receive logging events
|
||||
custom_layer: log_to_file_layer,
|
||||
..default()
|
||||
}));
|
||||
|
||||
app.add_plugins(ServerPlugins {
|
||||
@@ -102,17 +80,6 @@ fn main() {
|
||||
app.add_plugins(UiGradientsPlugin);
|
||||
app.add_plugins(RonAssetPlugin::<HeadDatabaseAsset>::new(&["headsdb.ron"]));
|
||||
|
||||
#[cfg(feature = "dbg")]
|
||||
{
|
||||
app.add_plugins(PhysicsDebugPlugin::default());
|
||||
|
||||
// app.add_plugins(bevy::pbr::wireframe::WireframePlugin)
|
||||
// .insert_resource(bevy::pbr::wireframe::WireframeConfig {
|
||||
// global: true,
|
||||
// default_color: bevy::color::palettes::css::WHITE.into(),
|
||||
// });
|
||||
}
|
||||
|
||||
app.add_plugins(shared::abilities::plugin);
|
||||
app.add_plugins(shared::ai::plugin);
|
||||
app.add_plugins(shared::aim::plugin);
|
||||
@@ -127,7 +94,6 @@ fn main() {
|
||||
app.add_plugins(shared::gates::plugin);
|
||||
app.add_plugins(shared::head_drop::plugin);
|
||||
app.add_plugins(shared::heads::plugin);
|
||||
app.add_plugins(shared::heal_effect::plugin);
|
||||
app.add_plugins(shared::hitpoints::plugin);
|
||||
app.add_plugins(shared::keys::plugin);
|
||||
app.add_plugins(shared::loading_assets::LoadingPlugin);
|
||||
@@ -137,7 +103,6 @@ fn main() {
|
||||
app.add_plugins(shared::platforms::plugin);
|
||||
app.add_plugins(shared::player::plugin);
|
||||
app.add_plugins(shared::protocol::plugin);
|
||||
app.add_plugins(shared::sounds::plugin);
|
||||
app.add_plugins(shared::steam::plugin);
|
||||
app.add_plugins(shared::tb_entities::plugin);
|
||||
app.add_plugins(shared::utils::auto_rotate::plugin);
|
||||
@@ -149,20 +114,20 @@ fn main() {
|
||||
app.add_plugins(shared::utils::plugin);
|
||||
app.add_plugins(shared::water::plugin);
|
||||
|
||||
app.add_plugins(backpack::plugin);
|
||||
app.add_plugins(config::plugin);
|
||||
app.add_plugins(player::plugin);
|
||||
app.add_plugins(server::plugin);
|
||||
app.add_plugins(tb_entities::plugin);
|
||||
app.add_plugins(utils::plugin);
|
||||
|
||||
app.init_state::<GameState>();
|
||||
|
||||
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(PostStartup, setup_panic_handler);
|
||||
|
||||
app.run();
|
||||
}
|
||||
|
||||
fn setup_panic_handler() {
|
||||
_ = std::panic::take_hook();
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use shared::{
|
||||
cash::CashResource,
|
||||
character::AnimatedCharacter,
|
||||
control::{ControlState, controller_common::PlayerCharacterController},
|
||||
global_observer,
|
||||
head::ActiveHead,
|
||||
head_drop::HeadDrops,
|
||||
heads::{ActiveHeads, HeadChanged, HeadState, heads_ui::UiActiveHeads},
|
||||
@@ -13,19 +14,23 @@ use shared::{
|
||||
hitpoints::{Hitpoints, Kill},
|
||||
npc::SpawnCharacter,
|
||||
player::{Player, PlayerBodyMesh},
|
||||
protocol::{
|
||||
PlaySound, PlayerId, channels::UnorderedReliableChannel, events::ClientHeadChanged,
|
||||
},
|
||||
tb_entities::SpawnPoint,
|
||||
};
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
global_observer!(app, on_update_head_mesh);
|
||||
}
|
||||
|
||||
pub fn spawn(
|
||||
mut commands: Commands,
|
||||
owner: Entity,
|
||||
query: Query<&Transform, With<SpawnPoint>>,
|
||||
asset_server: Res<AssetServer>,
|
||||
heads_db: Res<HeadsDatabase>,
|
||||
) {
|
||||
let Some(spawn) = query.iter().next() else {
|
||||
return;
|
||||
};
|
||||
) -> Option<Entity> {
|
||||
let spawn = query.iter().next()?;
|
||||
|
||||
let transform = Transform::from_translation(spawn.translation + Vec3::new(0., 3., 0.));
|
||||
|
||||
@@ -47,6 +52,7 @@ pub fn spawn(
|
||||
transform,
|
||||
Visibility::default(),
|
||||
PlayerCharacterController,
|
||||
PlayerId { id: 0 },
|
||||
),
|
||||
ActionState::<ControlState>::default(),
|
||||
Backpack::default(),
|
||||
@@ -67,12 +73,12 @@ pub fn spawn(
|
||||
));
|
||||
player.observe(on_kill);
|
||||
|
||||
commands.spawn((
|
||||
AudioPlayer::new(asset_server.load("sfx/heads/angry demonstrator.ogg")),
|
||||
PlaybackSettings::DESPAWN,
|
||||
));
|
||||
let id = player.id();
|
||||
|
||||
commands.trigger(PlaySound::Head("angry demonstrator".to_string()));
|
||||
commands.trigger(SpawnCharacter(transform.translation));
|
||||
|
||||
Some(id)
|
||||
}
|
||||
|
||||
fn on_kill(
|
||||
@@ -92,3 +98,27 @@ fn on_kill(
|
||||
commands.trigger(HeadChanged(new_head));
|
||||
}
|
||||
}
|
||||
|
||||
fn on_update_head_mesh(
|
||||
trigger: Trigger<HeadChanged>,
|
||||
mut commands: Commands,
|
||||
mesh_children: Single<&Children, With<PlayerBodyMesh>>,
|
||||
mut sender: Single<&mut TriggerSender<ClientHeadChanged>>,
|
||||
animated_characters: Query<&AnimatedCharacter>,
|
||||
mut player: Single<&mut ActiveHead, With<Player>>,
|
||||
) -> Result {
|
||||
let animated_char = mesh_children
|
||||
.iter()
|
||||
.find(|child| animated_characters.contains(*child))
|
||||
.ok_or("tried to update head mesh before AnimatedCharacter was readded")?;
|
||||
|
||||
player.0 = trigger.0;
|
||||
|
||||
commands
|
||||
.entity(animated_char)
|
||||
.insert(AnimatedCharacter::new(trigger.0));
|
||||
|
||||
sender.trigger::<UnorderedReliableChannel>(ClientHeadChanged(trigger.0 as u64));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,42 +1,54 @@
|
||||
use crate::config::ServerConfig;
|
||||
use bevy::prelude::*;
|
||||
use lightyear::{
|
||||
connection::client::PeerMetadata,
|
||||
link::LinkConditioner,
|
||||
prelude::{
|
||||
server::{NetcodeConfig, NetcodeServer, ServerUdpIo, Started},
|
||||
server::{ClientOf, NetcodeConfig, NetcodeServer, ServerUdpIo, Started},
|
||||
*,
|
||||
},
|
||||
};
|
||||
use shared::{GameState, global_observer, heads_database::HeadsDatabase, tb_entities::SpawnPoint};
|
||||
use shared::{
|
||||
GameState, global_observer,
|
||||
heads_database::HeadsDatabase,
|
||||
protocol::{
|
||||
ClientEnteredPlaying, channels::UnorderedReliableChannel, messages::AssignClientPlayer,
|
||||
},
|
||||
tb_entities::SpawnPoint,
|
||||
};
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_systems(Startup, (start_server, setup_timeout_timer));
|
||||
app.add_systems(
|
||||
OnEnter(GameState::Playing),
|
||||
(start_server, setup_timeout_timer),
|
||||
);
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
notify_started.run_if(in_state(GameState::Playing)),
|
||||
run_timeout,
|
||||
),
|
||||
(notify_started, run_timeout).run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
|
||||
global_observer!(app, handle_new_client);
|
||||
global_observer!(app, on_client_connected);
|
||||
global_observer!(app, on_client_playing);
|
||||
global_observer!(app, close_on_disconnect);
|
||||
global_observer!(app, cancel_timeout);
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct ClientPlayerId(u8);
|
||||
|
||||
fn handle_new_client(
|
||||
trigger: Trigger<OnAdd, Connected>,
|
||||
trigger: Trigger<OnAdd, Linked>,
|
||||
mut commands: Commands,
|
||||
id: Query<&PeerAddr>,
|
||||
asset_server: Res<AssetServer>,
|
||||
query: Query<&Transform, With<SpawnPoint>>,
|
||||
heads_db: Res<HeadsDatabase>,
|
||||
) -> Result {
|
||||
let id = id.get(trigger.target())?;
|
||||
let Ok(id) = id.get(trigger.target()) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
info!("Client connected on IP: {}", id.ip());
|
||||
|
||||
@@ -46,11 +58,38 @@ fn handle_new_client(
|
||||
incoming_loss: 0.0,
|
||||
});
|
||||
|
||||
commands
|
||||
.entity(trigger.target())
|
||||
.insert((ReplicationSender::default(), Link::new(Some(conditioner))));
|
||||
commands.entity(trigger.target()).insert((
|
||||
ReplicationSender::default(),
|
||||
Link::new(Some(conditioner)),
|
||||
ClientPlayerId(0),
|
||||
));
|
||||
|
||||
crate::player::spawn(commands, trigger.target(), query, asset_server, heads_db);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_client_connected(
|
||||
trigger: Trigger<OnAdd, ClientOf>,
|
||||
mut assign_player: Query<(&ClientPlayerId, &mut MessageSender<AssignClientPlayer>)>,
|
||||
) -> Result {
|
||||
// `Linked` happens before the `ClientOf` and `MessageSender` components are added, so the server can't
|
||||
// send the client player id until now.
|
||||
let (id, mut sender) = assign_player.get_mut(trigger.target())?;
|
||||
sender.send::<UnorderedReliableChannel>(AssignClientPlayer(id.0));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_client_playing(
|
||||
trigger: Trigger<RemoteTrigger<ClientEnteredPlaying>>,
|
||||
commands: Commands,
|
||||
query: Query<&Transform, With<SpawnPoint>>,
|
||||
heads_db: Res<HeadsDatabase>,
|
||||
peers: Res<PeerMetadata>,
|
||||
) -> Result {
|
||||
let Some(&client) = peers.mapping.get(&trigger.from) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
crate::player::spawn(commands, client, query, heads_db).ok_or("failed to spawn player")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -61,6 +100,7 @@ fn close_on_disconnect(
|
||||
mut writer: EventWriter<AppExit>,
|
||||
) {
|
||||
if config.close_on_client_disconnect {
|
||||
info!("client disconnected, exiting");
|
||||
writer.write(AppExit::Success);
|
||||
}
|
||||
}
|
||||
@@ -103,10 +143,12 @@ fn run_timeout(mut timer: ResMut<TimeoutTimer>, mut writer: EventWriter<AppExit>
|
||||
timer.0 -= time.delta_secs();
|
||||
|
||||
if timer.0 <= 0.0 {
|
||||
info!("client timed out, exiting");
|
||||
writer.write(AppExit::Success);
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel_timeout(_trigger: Trigger<OnAdd, Connected>, mut timer: ResMut<TimeoutTimer>) {
|
||||
info!("client connected, cancelling timeout");
|
||||
timer.0 = f32::INFINITY;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use bevy::prelude::*;
|
||||
use lightyear::prelude::{ActionsChannel, Connected, MessageSender};
|
||||
use lightyear::prelude::{Connected, MessageSender};
|
||||
use shared::{
|
||||
GameState, global_observer,
|
||||
protocol::{DespawnTbMapEntity, TbMapEntityId},
|
||||
protocol::{TbMapEntityId, channels::UnorderedReliableChannel, messages::DespawnTbMapEntity},
|
||||
};
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
@@ -38,6 +38,6 @@ fn send_new_client_despawned_cache(
|
||||
) {
|
||||
let mut send = send.get_mut(trigger.target()).unwrap();
|
||||
for &id in cache.0.iter() {
|
||||
send.send::<ActionsChannel>(DespawnTbMapEntity(id));
|
||||
send.send::<UnorderedReliableChannel>(DespawnTbMapEntity(id));
|
||||
}
|
||||
}
|
||||
|
||||
37
crates/server/src/utils.rs
Normal file
37
crates/server/src/utils.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use bevy::{
|
||||
ecs::{archetype::Archetypes, component::Components, entity::Entities},
|
||||
prelude::*,
|
||||
};
|
||||
use shared::global_observer;
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
global_observer!(app, report_entity_components);
|
||||
}
|
||||
|
||||
#[derive(Event)]
|
||||
pub struct ReportEntityComponents(pub Entity);
|
||||
|
||||
fn report_entity_components(
|
||||
trigger: Trigger<ReportEntityComponents>,
|
||||
entities: &Entities,
|
||||
components: &Components,
|
||||
archetypes: &Archetypes,
|
||||
) {
|
||||
let Some(location) = entities.get(trigger.event().0) else {
|
||||
warn!("failed to report entity components; had no location");
|
||||
return;
|
||||
};
|
||||
let Some(archetype) = archetypes.get(location.archetype_id) else {
|
||||
warn!("failed to report entity components; had no archetype");
|
||||
return;
|
||||
};
|
||||
|
||||
let mut output = format!("Entity {:?} Components: ", trigger.event().0);
|
||||
for component in archetype.components() {
|
||||
if let Some(name) = components.get_name(component) {
|
||||
output.push_str(&format!("{name}, "));
|
||||
}
|
||||
}
|
||||
|
||||
info!("{}; Caller: {}", output, trigger.caller());
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::TriggerArrow;
|
||||
use crate::{
|
||||
GameState, billboards::Billboard, global_observer, heads_database::HeadsDatabase,
|
||||
hitpoints::Hit, loading_assets::GameAssets, physics_layers::GameLayer, sounds::PlaySound,
|
||||
hitpoints::Hit, loading_assets::GameAssets, physics_layers::GameLayer, protocol::PlaySound,
|
||||
utils::sprite_3d_animation::AnimationTimer,
|
||||
};
|
||||
use avian3d::prelude::*;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::TriggerGun;
|
||||
use crate::{
|
||||
GameState, billboards::Billboard, global_observer, heads_database::HeadsDatabase,
|
||||
hitpoints::Hit, loading_assets::GameAssets, physics_layers::GameLayer, sounds::PlaySound,
|
||||
hitpoints::Hit, loading_assets::GameAssets, physics_layers::GameLayer, protocol::PlaySound,
|
||||
tb_entities::EnemySpawn, utils::sprite_3d_animation::AnimationTimer,
|
||||
};
|
||||
use avian3d::prelude::*;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use crate::{
|
||||
GameState, global_observer, heads::ActiveHeads, heads_database::HeadsDatabase,
|
||||
hitpoints::Hitpoints, loading_assets::AudioAssets,
|
||||
hitpoints::Hitpoints,
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Healing(pub Entity);
|
||||
#[derive(Component, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Healing;
|
||||
|
||||
#[derive(Event, Debug)]
|
||||
#[derive(Clone, Event, Debug, Serialize, Deserialize)]
|
||||
pub enum HealingStateChanged {
|
||||
Started,
|
||||
Stopped,
|
||||
@@ -22,27 +23,21 @@ pub fn plugin(app: &mut App) {
|
||||
fn on_heal_start_stop(
|
||||
trigger: Trigger<HealingStateChanged>,
|
||||
mut cmds: Commands,
|
||||
assets: Res<AudioAssets>,
|
||||
query: Query<&Healing>,
|
||||
) {
|
||||
if matches!(trigger.event(), HealingStateChanged::Started) {
|
||||
let e = cmds
|
||||
.spawn((
|
||||
Name::new("sfx-heal"),
|
||||
AudioPlayer::new(assets.healing.clone()),
|
||||
PlaybackSettings {
|
||||
mode: bevy::audio::PlaybackMode::Loop,
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
.id();
|
||||
cmds.entity(trigger.target())
|
||||
.add_child(e)
|
||||
.insert(Healing(e));
|
||||
} else {
|
||||
if let Ok(healing) = query.single() {
|
||||
cmds.entity(healing.0).despawn();
|
||||
if query.contains(trigger.target()) {
|
||||
// already healing, just ignore
|
||||
return;
|
||||
}
|
||||
|
||||
cmds.entity(trigger.target()).insert(Healing);
|
||||
} else {
|
||||
if !query.contains(trigger.target()) {
|
||||
// Not healing, just ignore
|
||||
return;
|
||||
}
|
||||
|
||||
cmds.entity(trigger.target()).remove::<Healing>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@ use crate::{
|
||||
abilities::BuildExplosionSprite,
|
||||
heads_database::HeadsDatabase,
|
||||
physics_layers::GameLayer,
|
||||
protocol::GltfSceneRoot,
|
||||
sounds::PlaySound,
|
||||
protocol::{GltfSceneRoot, PlaySound},
|
||||
utils::{commands::CommandExt, explosions::Explosion, global_observer, trail::Trail},
|
||||
};
|
||||
use avian3d::prelude::*;
|
||||
|
||||
@@ -9,25 +9,23 @@ use crate::{
|
||||
GameState,
|
||||
aim::AimTarget,
|
||||
character::CharacterHierarchy,
|
||||
control::ControlState,
|
||||
global_observer,
|
||||
head::ActiveHead,
|
||||
heads::ActiveHeads,
|
||||
heads_database::HeadsDatabase,
|
||||
loading_assets::GameAssets,
|
||||
physics_layers::GameLayer,
|
||||
player::{Player, PlayerBodyMesh},
|
||||
sounds::PlaySound,
|
||||
protocol::PlaySound,
|
||||
utils::{billboards::Billboard, sprite_3d_animation::AnimationTimer},
|
||||
};
|
||||
#[cfg(feature = "server")]
|
||||
use crate::{control::ControlState, head::ActiveHead};
|
||||
use bevy::{pbr::NotShadowCaster, prelude::*};
|
||||
use bevy_sprite3d::{Sprite3dBuilder, Sprite3dParams};
|
||||
pub use healing::Healing;
|
||||
use healing::HealingStateChanged;
|
||||
use lightyear::{
|
||||
connection::client::ClientState,
|
||||
prelude::{Client, input::native::ActionState},
|
||||
};
|
||||
#[cfg(feature = "server")]
|
||||
use lightyear::prelude::input::native::ActionState;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Reflect, Default, Serialize, Deserialize)]
|
||||
@@ -113,6 +111,7 @@ pub fn plugin(app: &mut App) {
|
||||
Update,
|
||||
(update, update_heal_ability).run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
#[cfg(feature = "server")]
|
||||
app.add_systems(
|
||||
FixedUpdate,
|
||||
on_trigger_state.run_if(in_state(GameState::Playing)),
|
||||
@@ -121,19 +120,13 @@ pub fn plugin(app: &mut App) {
|
||||
global_observer!(app, build_explosion_sprite);
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
fn on_trigger_state(
|
||||
mut res: ResMut<TriggerStateRes>,
|
||||
player: Query<(&ActiveHead, &ActionState<ControlState>), With<Player>>,
|
||||
headdb: Res<HeadsDatabase>,
|
||||
time: Res<Time>,
|
||||
client: Query<&Client>,
|
||||
) {
|
||||
if let Ok(client) = client.single()
|
||||
&& (client.state == ClientState::Connected && cfg!(not(feature = "server")))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (player_head, controls) in player.iter() {
|
||||
res.active = controls.trigger;
|
||||
if controls.just_triggered {
|
||||
|
||||
@@ -4,8 +4,7 @@ use crate::{
|
||||
abilities::BuildExplosionSprite,
|
||||
heads_database::HeadsDatabase,
|
||||
physics_layers::GameLayer,
|
||||
protocol::GltfSceneRoot,
|
||||
sounds::PlaySound,
|
||||
protocol::{GltfSceneRoot, PlaySound},
|
||||
utils::{
|
||||
auto_rotate::AutoRotation, commands::CommandExt, explosions::Explosion, global_observer,
|
||||
},
|
||||
|
||||
@@ -1,352 +1,40 @@
|
||||
#[cfg(feature = "server")]
|
||||
use super::Backpack;
|
||||
use super::UiHeadState;
|
||||
use crate::{
|
||||
GameState, HEDZ_GREEN, loading_assets::UIAssets, protocol::PlayBackpackSound, sounds::PlaySound,
|
||||
};
|
||||
#[cfg(feature = "server")]
|
||||
use crate::{backpack::BackbackSwapEvent, control::ControlState};
|
||||
#[cfg(feature = "client")]
|
||||
use crate::{global_observer, heads::HeadsImages};
|
||||
use bevy::{ecs::spawn::SpawnIter, prelude::*};
|
||||
#[cfg(feature = "server")]
|
||||
use lightyear::prelude::{ActionsChannel, TriggerSender, input::native::ActionState};
|
||||
use bevy::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
static HEAD_SLOTS: usize = 5;
|
||||
pub static HEAD_SLOTS: usize = 5;
|
||||
|
||||
#[derive(Component, Default)]
|
||||
struct BackpackMarker;
|
||||
pub struct BackpackMarker;
|
||||
|
||||
#[derive(Component, Default)]
|
||||
struct BackpackCountText;
|
||||
pub struct BackpackCountText;
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Component, Default)]
|
||||
struct HeadSelector(pub usize);
|
||||
pub struct HeadSelector(pub usize);
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Component, Default)]
|
||||
struct HeadImage(pub usize);
|
||||
pub struct HeadImage(pub usize);
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Component, Default)]
|
||||
struct HeadDamage(pub usize);
|
||||
pub struct HeadDamage(pub usize);
|
||||
|
||||
#[derive(Component, Default, Debug, Reflect, Serialize, Deserialize, PartialEq)]
|
||||
#[reflect(Component, Default)]
|
||||
pub struct BackpackUiState {
|
||||
heads: [Option<UiHeadState>; 5],
|
||||
scroll: usize,
|
||||
count: usize,
|
||||
current_slot: usize,
|
||||
open: bool,
|
||||
pub heads: [Option<UiHeadState>; 5],
|
||||
pub scroll: usize,
|
||||
pub count: usize,
|
||||
pub current_slot: usize,
|
||||
pub open: bool,
|
||||
}
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
impl BackpackUiState {
|
||||
fn relative_current_slot(&self) -> usize {
|
||||
pub fn relative_current_slot(&self) -> usize {
|
||||
self.current_slot.saturating_sub(self.scroll)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.register_type::<BackpackUiState>();
|
||||
app.add_systems(OnEnter(GameState::Playing), setup);
|
||||
#[cfg(feature = "server")]
|
||||
app.add_systems(
|
||||
FixedUpdate,
|
||||
sync_on_change.run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
#[cfg(feature = "client")]
|
||||
app.add_systems(
|
||||
FixedUpdate,
|
||||
(update, update_visibility, update_count).run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
#[cfg(feature = "server")]
|
||||
app.add_systems(FixedUpdate, swap_head_inputs);
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
global_observer!(app, play_backpack_sound);
|
||||
}
|
||||
|
||||
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)]
|
||||
)]
|
||||
)
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
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();
|
||||
}
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
fn swap_head_inputs(
|
||||
player: Query<(&ActionState<ControlState>, Ref<Backpack>)>,
|
||||
mut trigger: Single<&mut TriggerSender<PlayBackpackSound>>,
|
||||
mut commands: Commands,
|
||||
mut state: Single<&mut BackpackUiState>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
for (controls, backpack) in player.iter() {
|
||||
if state.count == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
if controls.backpack_toggle {
|
||||
state.open = !state.open;
|
||||
trigger.trigger::<ActionsChannel>(PlayBackpackSound { open: state.open });
|
||||
}
|
||||
|
||||
if !state.open {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut changed = false;
|
||||
if controls.backpack_left && state.current_slot > 0 {
|
||||
state.current_slot -= 1;
|
||||
changed = true;
|
||||
}
|
||||
if controls.backpack_right && state.current_slot < state.count.saturating_sub(1) {
|
||||
state.current_slot += 1;
|
||||
changed = true;
|
||||
}
|
||||
if controls.backpack_swap {
|
||||
commands.trigger(BackbackSwapEvent(state.current_slot));
|
||||
}
|
||||
|
||||
if changed {
|
||||
commands.trigger(PlaySound::Selection);
|
||||
sync(&backpack, &mut state, time.elapsed_secs());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
fn play_backpack_sound(trigger: Trigger<PlayBackpackSound>, mut commands: Commands) {
|
||||
commands.trigger(PlaySound::Backpack {
|
||||
open: trigger.event().open,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
fn sync_on_change(
|
||||
backpack: Query<Ref<Backpack>>,
|
||||
mut state: Single<&mut BackpackUiState>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
for backpack in backpack.iter() {
|
||||
if backpack.is_changed() || backpack.reloading() {
|
||||
sync(&backpack, &mut state, time.elapsed_secs());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
fn sync(backpack: &Backpack, state: &mut Single<&mut BackpackUiState>, time: f32) {
|
||||
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(UiHeadState::new(*head, time));
|
||||
} else {
|
||||
state.heads[i] = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#[cfg(feature = "server")]
|
||||
use crate::heads::HeadState;
|
||||
use bevy::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -24,8 +23,7 @@ impl UiHeadState {
|
||||
self.reloading
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
pub(crate) fn new(value: HeadState, time: f32) -> Self {
|
||||
pub fn new(value: HeadState, time: f32) -> Self {
|
||||
let reloading = if value.has_ammo() {
|
||||
None
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{GameState, HEDZ_GREEN, loading_assets::UIAssets};
|
||||
#[cfg(feature = "server")]
|
||||
use crate::{global_observer, sounds::PlaySound};
|
||||
use crate::{global_observer, protocol::PlaySound};
|
||||
use bevy::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
cash::CashResource, control::ControlState, hitpoints::Hitpoints, player::Player,
|
||||
sounds::PlaySound,
|
||||
protocol::PlaySound,
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
use lightyear::prelude::input::native::ActionState;
|
||||
|
||||
@@ -9,6 +9,7 @@ use bevy::{
|
||||
animation::RepeatAnimation, ecs::system::SystemParam, platform::collections::HashMap,
|
||||
prelude::*, scene::SceneInstanceReady,
|
||||
};
|
||||
use lightyear::prelude::DisableReplicateHierarchy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{f32::consts::PI, time::Duration};
|
||||
|
||||
@@ -16,6 +17,7 @@ use std::{f32::consts::PI, time::Duration};
|
||||
pub struct ProjectileOrigin;
|
||||
|
||||
#[derive(Component, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[require(Visibility, GlobalTransform)]
|
||||
pub struct AnimatedCharacter {
|
||||
head: usize,
|
||||
}
|
||||
@@ -107,14 +109,19 @@ fn spawn(
|
||||
|
||||
transform.rotate_y(PI);
|
||||
|
||||
commands.entity(entity).despawn_related::<Children>();
|
||||
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert((
|
||||
transform,
|
||||
.spawn((
|
||||
SceneRoot(asset.scenes[0].clone()),
|
||||
AnimatedCharacterAsset(handle.clone()),
|
||||
DisableReplicateHierarchy,
|
||||
ChildOf(entity),
|
||||
))
|
||||
.observe(find_marker_bones);
|
||||
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert((transform, AnimatedCharacterAsset(handle.clone())));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,14 +169,14 @@ fn find_marker_bones(
|
||||
|
||||
#[derive(Component, Default, Reflect, PartialEq, Serialize, Deserialize)]
|
||||
#[reflect(Component)]
|
||||
pub struct Character;
|
||||
pub struct HedzCharacter;
|
||||
|
||||
fn setup_once_loaded(
|
||||
mut commands: Commands,
|
||||
mut query: Query<(Entity, &mut AnimationPlayer), Added<AnimationPlayer>>,
|
||||
parent: Query<&ChildOf>,
|
||||
animated_character: Query<(&AnimatedCharacter, &AnimatedCharacterAsset)>,
|
||||
characters: Query<Entity, With<Character>>,
|
||||
characters: Query<Entity, With<HedzCharacter>>,
|
||||
gltf_assets: Res<Assets<Gltf>>,
|
||||
mut graphs: ResMut<Assets<AnimationGraph>>,
|
||||
) {
|
||||
|
||||
@@ -55,14 +55,12 @@ fn rotate_rig(
|
||||
}
|
||||
|
||||
fn apply_controls(
|
||||
mut character: Query<(&mut MoveInput, &MovementSpeedFactor)>,
|
||||
character: Single<(&mut MoveInput, &MovementSpeedFactor)>,
|
||||
rig_transform_q: Option<Single<&GlobalTransform, With<PlayerBodyMesh>>>,
|
||||
) -> Result {
|
||||
let (mut char_input, factor) = character.single_mut()?;
|
||||
) {
|
||||
let (mut char_input, factor) = character.into_inner();
|
||||
|
||||
if let Some(ref rig_transform) = rig_transform_q {
|
||||
char_input.set(-*rig_transform.forward() * factor.0);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use super::{ControlState, Controls};
|
||||
#[cfg(feature = "client")]
|
||||
use crate::player::Player;
|
||||
use crate::{
|
||||
GameState,
|
||||
control::{CharacterInputEnabled, ControllerSet},
|
||||
@@ -14,7 +12,7 @@ use bevy::{
|
||||
prelude::*,
|
||||
};
|
||||
#[cfg(feature = "client")]
|
||||
use lightyear::prelude::input::native::ActionState;
|
||||
use lightyear::prelude::input::native::{ActionState, InputMarker};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
@@ -66,7 +64,7 @@ pub struct ControllerSettings {
|
||||
/// Write inputs from combined keyboard/gamepad state into the networked input buffer
|
||||
/// for the local player.
|
||||
fn buffer_inputs(
|
||||
mut player: Single<&mut ActionState<ControlState>, With<Player>>,
|
||||
mut player: Single<&mut ActionState<ControlState>, With<InputMarker<ControlState>>>,
|
||||
controls: Res<ControlState>,
|
||||
) {
|
||||
player.0 = *controls;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
cutscene::StartCutscene, global_observer, keys::KeyCollected, movables::TriggerMovableEvent,
|
||||
sounds::PlaySound,
|
||||
protocol::PlaySound,
|
||||
};
|
||||
use bevy::{platform::collections::HashSet, prelude::*};
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
use crate::{
|
||||
GameState, billboards::Billboard, global_observer, heads_database::HeadsDatabase,
|
||||
physics_layers::GameLayer, player::Player, protocol::GltfSceneRoot, sounds::PlaySound,
|
||||
squish_animation::SquishAnimation, tb_entities::SecretHead,
|
||||
GameState,
|
||||
billboards::Billboard,
|
||||
global_observer,
|
||||
heads_database::HeadsDatabase,
|
||||
physics_layers::GameLayer,
|
||||
player::Player,
|
||||
protocol::{GltfSceneRoot, PlaySound},
|
||||
squish_animation::SquishAnimation,
|
||||
tb_entities::SecretHead,
|
||||
};
|
||||
use avian3d::prelude::*;
|
||||
use bevy::{
|
||||
|
||||
@@ -5,14 +5,15 @@ use crate::animation::AnimationFlags;
|
||||
use crate::{
|
||||
GameState,
|
||||
backpack::{BackbackSwapEvent, Backpack},
|
||||
control::ControlState,
|
||||
global_observer,
|
||||
heads_database::HeadsDatabase,
|
||||
hitpoints::Hitpoints,
|
||||
player::Player,
|
||||
sounds::PlaySound,
|
||||
};
|
||||
#[cfg(feature = "server")]
|
||||
use crate::{control::ControlState, protocol::PlaySound};
|
||||
use bevy::prelude::*;
|
||||
#[cfg(feature = "server")]
|
||||
use lightyear::prelude::input::native::ActionState;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -188,6 +189,7 @@ pub fn plugin(app: &mut App) {
|
||||
FixedUpdate,
|
||||
(reload, sync_hp).run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
#[cfg(feature = "server")]
|
||||
app.add_systems(FixedUpdate, on_select_active_head);
|
||||
|
||||
global_observer!(app, on_swap_backpack);
|
||||
@@ -238,6 +240,7 @@ fn reload(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
fn on_select_active_head(
|
||||
mut commands: Commands,
|
||||
mut query: Query<(&mut ActiveHeads, &mut Hitpoints, &ActionState<ControlState>), With<Player>>,
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{
|
||||
GameState,
|
||||
animation::AnimationFlags,
|
||||
character::{CharacterAnimations, HasCharacterAnimations},
|
||||
sounds::PlaySound,
|
||||
protocol::PlaySound,
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use crate::{
|
||||
billboards::Billboard, global_observer, physics_layers::GameLayer, player::Player,
|
||||
protocol::GltfSceneRoot, sounds::PlaySound, squish_animation::SquishAnimation,
|
||||
billboards::Billboard,
|
||||
global_observer,
|
||||
physics_layers::GameLayer,
|
||||
player::Player,
|
||||
protocol::{GltfSceneRoot, PlaySound},
|
||||
squish_animation::SquishAnimation,
|
||||
};
|
||||
use avian3d::prelude::*;
|
||||
use bevy::{
|
||||
|
||||
@@ -14,7 +14,6 @@ pub mod head;
|
||||
pub mod head_drop;
|
||||
pub mod heads;
|
||||
pub mod heads_database;
|
||||
pub mod heal_effect;
|
||||
pub mod hitpoints;
|
||||
pub mod keys;
|
||||
pub mod loading_assets;
|
||||
@@ -25,7 +24,6 @@ pub mod physics_layers;
|
||||
pub mod platforms;
|
||||
pub mod player;
|
||||
pub mod protocol;
|
||||
pub mod sounds;
|
||||
pub mod steam;
|
||||
pub mod tb_entities;
|
||||
pub mod utils;
|
||||
|
||||
@@ -124,15 +124,16 @@ pub struct LoadingPlugin;
|
||||
impl Plugin for LoadingPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(OnExit(GameState::AssetLoading), on_exit);
|
||||
app.add_loading_state(
|
||||
LoadingState::new(GameState::AssetLoading)
|
||||
.continue_to_state(GameState::MapLoading)
|
||||
.load_collection::<AudioAssets>()
|
||||
.load_collection::<GameAssets>()
|
||||
.load_collection::<HeadsAssets>()
|
||||
.load_collection::<HeadDropAssets>()
|
||||
.load_collection::<UIAssets>(),
|
||||
);
|
||||
let loading_state = LoadingState::new(GameState::AssetLoading);
|
||||
let loading_state = loading_state
|
||||
.continue_to_state(GameState::MapLoading)
|
||||
.load_collection::<GameAssets>()
|
||||
.load_collection::<HeadsAssets>()
|
||||
.load_collection::<HeadDropAssets>()
|
||||
.load_collection::<UIAssets>();
|
||||
#[cfg(feature = "client")]
|
||||
let loading_state = loading_state.load_collection::<AudioAssets>();
|
||||
app.add_loading_state(loading_state);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
GameState, character::Character, global_observer, loading_assets::GameAssets,
|
||||
GameState, character::HedzCharacter, global_observer, loading_assets::GameAssets,
|
||||
utils::billboards::Billboard,
|
||||
};
|
||||
#[cfg(feature = "server")]
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
heads_database::HeadsDatabase,
|
||||
hitpoints::{Hitpoints, Kill},
|
||||
keys::KeySpawn,
|
||||
sounds::PlaySound,
|
||||
protocol::PlaySound,
|
||||
tb_entities::EnemySpawn,
|
||||
};
|
||||
use bevy::{pbr::NotShadowCaster, prelude::*};
|
||||
@@ -24,7 +24,7 @@ use std::collections::HashMap;
|
||||
|
||||
#[derive(Component, Reflect, PartialEq, Serialize, Deserialize)]
|
||||
#[reflect(Component)]
|
||||
#[require(Character)]
|
||||
#[require(HedzCharacter)]
|
||||
pub struct Npc;
|
||||
|
||||
#[derive(Resource, Reflect, Default)]
|
||||
@@ -102,7 +102,6 @@ fn on_spawn_check(
|
||||
None,
|
||||
None,
|
||||
]),
|
||||
#[cfg(feature = "server")]
|
||||
Replicate::to_clients(NetworkTarget::All),
|
||||
))
|
||||
.insert_if(Ai, || !spawn.disable_ai)
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
use crate::{
|
||||
GameState,
|
||||
cash::{Cash, CashCollectEvent},
|
||||
character::{AnimatedCharacter, Character},
|
||||
global_observer,
|
||||
head::ActiveHead,
|
||||
heads::HeadChanged,
|
||||
heads_database::{HeadControls, HeadsDatabase},
|
||||
loading_assets::AudioAssets,
|
||||
sounds::PlaySound,
|
||||
character::HedzCharacter,
|
||||
};
|
||||
use avian3d::prelude::*;
|
||||
use bevy::{
|
||||
@@ -18,7 +12,7 @@ use bevy::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Component, Default, Serialize, Deserialize, PartialEq)]
|
||||
#[require(Character)]
|
||||
#[require(HedzCharacter)]
|
||||
pub struct Player;
|
||||
|
||||
#[derive(Component, Default, Serialize, Deserialize, PartialEq)]
|
||||
@@ -36,8 +30,6 @@ pub fn plugin(app: &mut App) {
|
||||
)
|
||||
.run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
|
||||
global_observer!(app, on_update_head_mesh);
|
||||
}
|
||||
|
||||
fn cursor_recenter(q_windows: Single<&mut Window, With<PrimaryWindow>>) {
|
||||
@@ -103,48 +95,3 @@ fn setup_animations_marker_for_player(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_update_head_mesh(
|
||||
trigger: Trigger<HeadChanged>,
|
||||
mut commands: Commands,
|
||||
body_mesh: Single<(Entity, &Children), With<PlayerBodyMesh>>,
|
||||
animated_characters: Query<&AnimatedCharacter>,
|
||||
mut player: Single<&mut ActiveHead, With<Player>>,
|
||||
head_db: Res<HeadsDatabase>,
|
||||
audio_assets: Res<AudioAssets>,
|
||||
sfx: Query<&AudioPlayer>,
|
||||
) -> Result {
|
||||
let (body_mesh, mesh_children) = *body_mesh;
|
||||
|
||||
let animated_char = mesh_children
|
||||
.iter()
|
||||
.find(|child| animated_characters.contains(*child))
|
||||
.ok_or("tried to update head mesh before AnimatedCharacter was readded")?;
|
||||
|
||||
player.0 = trigger.0;
|
||||
|
||||
let head_str = head_db.head_key(trigger.0);
|
||||
|
||||
commands.trigger(PlaySound::Head(head_str.to_string()));
|
||||
|
||||
commands
|
||||
.entity(animated_char)
|
||||
.insert(AnimatedCharacter::new(trigger.0));
|
||||
|
||||
//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(trigger.0).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(())
|
||||
}
|
||||
|
||||
1
crates/shared/src/protocol/channels.rs
Normal file
1
crates/shared/src/protocol/channels.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub struct UnorderedReliableChannel;
|
||||
82
crates/shared/src/protocol/components.rs
Normal file
82
crates/shared/src/protocol/components.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use crate::{
|
||||
loading_assets::{GameAssets, HeadDropAssets},
|
||||
protocol::TbMapEntityMapping,
|
||||
};
|
||||
use bevy::{
|
||||
ecs::{component::HookContext, world::DeferredWorld},
|
||||
prelude::*,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Copy, Component, Reflect, Serialize, Deserialize, PartialEq)]
|
||||
#[reflect(Component)]
|
||||
pub struct PlayerId {
|
||||
pub id: u8,
|
||||
}
|
||||
|
||||
/// A unique ID assigned to every entity spawned by the trenchbroom map loader. This allows synchronizing
|
||||
/// them across the network even when they are spawned initially by both sides.
|
||||
#[derive(Clone, Copy, Component, Debug, Reflect, Serialize, Deserialize, PartialEq)]
|
||||
#[reflect(Component)]
|
||||
#[component(on_insert = TbMapEntityId::insert_id, on_remove = TbMapEntityId::remove_id)]
|
||||
pub struct TbMapEntityId {
|
||||
pub id: u64,
|
||||
}
|
||||
|
||||
impl TbMapEntityId {
|
||||
fn insert_id(mut world: DeferredWorld, ctx: HookContext) {
|
||||
let id = world.get::<TbMapEntityId>(ctx.entity).unwrap().id;
|
||||
world
|
||||
.resource_mut::<TbMapEntityMapping>()
|
||||
.insert(id, ctx.entity);
|
||||
}
|
||||
|
||||
fn remove_id(mut world: DeferredWorld, ctx: HookContext) {
|
||||
let id = world.get::<TbMapEntityId>(ctx.entity).unwrap().id;
|
||||
world.resource_mut::<TbMapEntityMapping>().remove(&id);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Reflect, Serialize, Deserialize, PartialEq)]
|
||||
#[reflect(Component)]
|
||||
pub enum GltfSceneRoot {
|
||||
Projectile(String),
|
||||
HeadDrop(String),
|
||||
Key,
|
||||
}
|
||||
|
||||
pub fn spawn_gltf_scene_roots(
|
||||
trigger: Trigger<OnAdd, GltfSceneRoot>,
|
||||
mut commands: Commands,
|
||||
gltf_roots: Query<&GltfSceneRoot>,
|
||||
head_drop_assets: Res<HeadDropAssets>,
|
||||
assets: Res<GameAssets>,
|
||||
gltfs: Res<Assets<Gltf>>,
|
||||
) -> Result {
|
||||
let root = gltf_roots.get(trigger.target())?;
|
||||
|
||||
let get_scene = |gltf: Handle<Gltf>, index: usize| {
|
||||
let gltf = gltfs.get(&gltf).unwrap();
|
||||
gltf.scenes[index].clone()
|
||||
};
|
||||
|
||||
let scene = match root {
|
||||
GltfSceneRoot::Projectile(addr) => get_scene(
|
||||
assets.projectiles[format!("{addr}.glb").as_str()].clone(),
|
||||
0,
|
||||
),
|
||||
GltfSceneRoot::HeadDrop(addr) => {
|
||||
let gltf = head_drop_assets
|
||||
.meshes
|
||||
.get(format!("{addr}.glb").as_str())
|
||||
.cloned();
|
||||
let gltf = gltf.unwrap_or_else(|| head_drop_assets.meshes["none.glb"].clone());
|
||||
get_scene(gltf, 0)
|
||||
}
|
||||
GltfSceneRoot::Key => assets.mesh_key.clone(),
|
||||
};
|
||||
|
||||
commands.entity(trigger.target()).insert(SceneRoot(scene));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
31
crates/shared/src/protocol/events.rs
Normal file
31
crates/shared/src/protocol/events.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use bevy::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Event, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ClientHeadChanged(pub u64);
|
||||
|
||||
#[derive(Event, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum PlaySound {
|
||||
Hit,
|
||||
KeyCollect,
|
||||
Gun,
|
||||
Throw,
|
||||
ThrowHit,
|
||||
Gate,
|
||||
CashCollect,
|
||||
HeadCollect,
|
||||
SecretHeadCollect,
|
||||
HeadDrop,
|
||||
Selection,
|
||||
Invalid,
|
||||
MissileExplosion,
|
||||
Reloaded,
|
||||
CashHeal,
|
||||
Crossbow,
|
||||
Beaming,
|
||||
Backpack { open: bool },
|
||||
Head(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Event, Serialize, Deserialize)]
|
||||
pub struct ClientEnteredPlaying;
|
||||
9
crates/shared/src/protocol/messages.rs
Normal file
9
crates/shared/src/protocol/messages.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Trenchbroom map entities (spawned during map loading) must be despawned manually if the server
|
||||
/// has already despawned it but the client has just loaded the map and connected
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct DespawnTbMapEntity(pub u64);
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct AssignClientPlayer(pub u8);
|
||||
@@ -1,6 +1,11 @@
|
||||
pub mod channels;
|
||||
pub mod components;
|
||||
pub mod events;
|
||||
pub mod messages;
|
||||
|
||||
use crate::{
|
||||
GameState,
|
||||
abilities::BuildExplosionSprite,
|
||||
abilities::{BuildExplosionSprite, healing::Healing},
|
||||
animation::AnimationFlags,
|
||||
backpack::{Backpack, backpack_ui::BackpackUiState},
|
||||
camera::{CameraArmRotation, CameraTarget},
|
||||
@@ -16,16 +21,15 @@ use crate::{
|
||||
head::ActiveHead,
|
||||
heads::{ActiveHeads, heads_ui::UiActiveHeads},
|
||||
hitpoints::Hitpoints,
|
||||
loading_assets::{GameAssets, HeadDropAssets},
|
||||
platforms::ActivePlatform,
|
||||
player::{Player, PlayerBodyMesh},
|
||||
protocol::channels::UnorderedReliableChannel,
|
||||
utils::triggers::TriggerAppExt,
|
||||
};
|
||||
use avian3d::prelude::{AngularVelocity, CollisionLayers, LinearVelocity};
|
||||
use bevy::{
|
||||
ecs::{component::HookContext, world::DeferredWorld},
|
||||
prelude::*,
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
pub use components::*;
|
||||
pub use events::*;
|
||||
use happy_feet::{
|
||||
grounding::GroundingState,
|
||||
prelude::{
|
||||
@@ -34,18 +38,19 @@ use happy_feet::{
|
||||
},
|
||||
};
|
||||
use lightyear::prelude::{
|
||||
ActionsChannel, AppComponentExt, AppMessageExt, NetworkDirection, PredictionMode,
|
||||
PredictionRegistrationExt, input::native::InputPlugin,
|
||||
AppChannelExt, AppComponentExt, AppMessageExt, AppTriggerExt, ChannelMode, ChannelSettings,
|
||||
NetworkDirection, PredictionMode, PredictionRegistrationExt, ReliableSettings,
|
||||
input::native::InputPlugin,
|
||||
};
|
||||
use lightyear_serde::{
|
||||
SerializationError, reader::ReadInteger, registry::SerializeFns, writer::WriteInteger,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_plugins(InputPlugin::<ControlState>::default());
|
||||
|
||||
app.register_type::<PlayerId>();
|
||||
app.register_type::<TbMapEntityId>();
|
||||
app.register_type::<TbMapIdCounter>();
|
||||
app.register_type::<TbMapEntityMapping>();
|
||||
@@ -53,9 +58,21 @@ pub fn plugin(app: &mut App) {
|
||||
app.init_resource::<TbMapIdCounter>();
|
||||
app.init_resource::<TbMapEntityMapping>();
|
||||
|
||||
app.add_message::<DespawnTbMapEntity>()
|
||||
app.add_channel::<UnorderedReliableChannel>(ChannelSettings {
|
||||
mode: ChannelMode::UnorderedReliable(ReliableSettings::default()),
|
||||
send_frequency: Duration::from_millis(100),
|
||||
priority: 1.0,
|
||||
})
|
||||
.add_direction(NetworkDirection::Bidirectional);
|
||||
|
||||
app.add_message::<messages::DespawnTbMapEntity>()
|
||||
.add_direction(NetworkDirection::ServerToClient);
|
||||
app.add_message::<messages::AssignClientPlayer>()
|
||||
.add_direction(NetworkDirection::ServerToClient);
|
||||
|
||||
app.register_component::<components::GltfSceneRoot>();
|
||||
app.register_component::<components::PlayerId>();
|
||||
app.register_component::<components::TbMapEntityId>();
|
||||
app.register_component::<ActiveHead>();
|
||||
app.register_component::<ActiveHeads>();
|
||||
app.register_component::<ActivePlatform>();
|
||||
@@ -68,17 +85,17 @@ pub fn plugin(app: &mut App) {
|
||||
app.register_component::<CameraTarget>();
|
||||
app.register_component::<CashResource>();
|
||||
app.register_component::<happy_feet::prelude::Character>();
|
||||
app.register_component::<character::Character>();
|
||||
app.register_component::<character::HedzCharacter>();
|
||||
app.register_component::<CharacterDrag>();
|
||||
app.register_component::<CharacterGravity>();
|
||||
app.register_component::<CharacterMovement>();
|
||||
app.register_component::<CollisionLayers>();
|
||||
app.register_component::<ControllerSettings>();
|
||||
app.register_component::<GltfSceneRoot>();
|
||||
app.register_component::<GroundFriction>();
|
||||
app.register_component::<Grounding>();
|
||||
app.register_component::<GroundingConfig>();
|
||||
app.register_component::<GroundingState>();
|
||||
app.register_component::<Healing>();
|
||||
app.register_component::<Hitpoints>();
|
||||
app.register_component::<KinematicVelocity>();
|
||||
app.register_component::<LinearVelocity>();
|
||||
@@ -89,7 +106,6 @@ pub fn plugin(app: &mut App) {
|
||||
app.register_component::<PlayerBodyMesh>();
|
||||
app.register_component::<PlayerCharacterController>();
|
||||
app.register_component::<SteppingConfig>();
|
||||
app.register_component::<TbMapEntityId>();
|
||||
app.register_component::<Transform>()
|
||||
.add_prediction(PredictionMode::Full)
|
||||
.add_should_rollback(transform_should_rollback);
|
||||
@@ -108,52 +124,28 @@ pub fn plugin(app: &mut App) {
|
||||
},
|
||||
});
|
||||
|
||||
app.replicate_trigger::<BuildExplosionSprite, ActionsChannel>();
|
||||
app.replicate_trigger::<StartCutscene, ActionsChannel>();
|
||||
app.replicate_trigger::<BuildExplosionSprite, UnorderedReliableChannel>();
|
||||
app.replicate_trigger::<StartCutscene, UnorderedReliableChannel>();
|
||||
|
||||
app.replicate_trigger::<PlayBackpackSound, ActionsChannel>();
|
||||
app.replicate_trigger::<events::ClientHeadChanged, UnorderedReliableChannel>();
|
||||
app.replicate_trigger::<events::PlaySound, UnorderedReliableChannel>();
|
||||
|
||||
app.add_trigger::<events::ClientEnteredPlaying>()
|
||||
.add_direction(NetworkDirection::ClientToServer);
|
||||
|
||||
app.add_systems(
|
||||
OnEnter(GameState::MapLoading),
|
||||
|mut counter: ResMut<TbMapIdCounter>| counter.reset(),
|
||||
);
|
||||
|
||||
global_observer!(app, spawn_gltf_scene_roots);
|
||||
global_observer!(app, components::spawn_gltf_scene_roots);
|
||||
}
|
||||
|
||||
fn transform_should_rollback(this: &Transform, that: &Transform) -> bool {
|
||||
this.translation.distance_squared(that.translation) >= 0.01f32.powf(2.)
|
||||
}
|
||||
|
||||
/// Trenchbroom map entities (spawned during map loading) must be despawned manually if the server
|
||||
/// has already despawned it but the client has just loaded the map and connected
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct DespawnTbMapEntity(pub u64);
|
||||
|
||||
/// A unique ID assigned to every entity spawned by the trenchbroom map loader. This allows synchronizing
|
||||
/// them across the network even when they are spawned initially by both sides.
|
||||
#[derive(Clone, Copy, Component, Debug, Reflect, Serialize, Deserialize, PartialEq)]
|
||||
#[reflect(Component)]
|
||||
#[component(on_insert = TbMapEntityId::insert_id, on_remove = TbMapEntityId::remove_id)]
|
||||
pub struct TbMapEntityId {
|
||||
pub id: u64,
|
||||
}
|
||||
|
||||
impl TbMapEntityId {
|
||||
fn insert_id(mut world: DeferredWorld, ctx: HookContext) {
|
||||
let id = world.get::<TbMapEntityId>(ctx.entity).unwrap().id;
|
||||
world
|
||||
.resource_mut::<TbMapEntityMapping>()
|
||||
.insert(id, ctx.entity);
|
||||
}
|
||||
|
||||
fn remove_id(mut world: DeferredWorld, ctx: HookContext) {
|
||||
let id = world.get::<TbMapEntityId>(ctx.entity).unwrap().id;
|
||||
world.resource_mut::<TbMapEntityMapping>().remove(&id);
|
||||
}
|
||||
}
|
||||
|
||||
/// A global allocator for `TBMapEntityId` values. Should be reset when a map begins loading.
|
||||
/// A global allocator for `TbMapEntityId` values. Should be reset when a map begins loading.
|
||||
#[derive(Resource, Reflect, Default)]
|
||||
#[reflect(Resource)]
|
||||
pub struct TbMapIdCounter(u64);
|
||||
@@ -176,52 +168,3 @@ impl TbMapIdCounter {
|
||||
#[derive(Resource, Reflect, Default, Deref, DerefMut)]
|
||||
#[reflect(Resource)]
|
||||
pub struct TbMapEntityMapping(pub HashMap<u64, Entity>);
|
||||
|
||||
#[derive(Clone, Event, Serialize, Deserialize)]
|
||||
pub struct PlayBackpackSound {
|
||||
pub open: bool,
|
||||
}
|
||||
|
||||
#[derive(Component, Reflect, Serialize, Deserialize, PartialEq)]
|
||||
#[reflect(Component)]
|
||||
pub enum GltfSceneRoot {
|
||||
Projectile(String),
|
||||
HeadDrop(String),
|
||||
Key,
|
||||
}
|
||||
|
||||
fn spawn_gltf_scene_roots(
|
||||
trigger: Trigger<OnAdd, GltfSceneRoot>,
|
||||
mut commands: Commands,
|
||||
gltf_roots: Query<&GltfSceneRoot>,
|
||||
head_drop_assets: Res<HeadDropAssets>,
|
||||
assets: Res<GameAssets>,
|
||||
gltfs: Res<Assets<Gltf>>,
|
||||
) -> Result {
|
||||
let root = gltf_roots.get(trigger.target())?;
|
||||
|
||||
let get_scene = |gltf: Handle<Gltf>, index: usize| {
|
||||
let gltf = gltfs.get(&gltf).unwrap();
|
||||
gltf.scenes[index].clone()
|
||||
};
|
||||
|
||||
let scene = match root {
|
||||
GltfSceneRoot::Projectile(addr) => get_scene(
|
||||
assets.projectiles[format!("{addr}.glb").as_str()].clone(),
|
||||
0,
|
||||
),
|
||||
GltfSceneRoot::HeadDrop(addr) => {
|
||||
let gltf = head_drop_assets
|
||||
.meshes
|
||||
.get(format!("{addr}.glb").as_str())
|
||||
.cloned();
|
||||
let gltf = gltf.unwrap_or_else(|| head_drop_assets.meshes["none.glb"].clone());
|
||||
get_scene(gltf, 0)
|
||||
}
|
||||
GltfSceneRoot::Key => assets.mesh_key.clone(),
|
||||
};
|
||||
|
||||
commands.entity(trigger.target()).insert(SceneRoot(scene));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
cash::Cash, loading_assets::GameAssets, physics_layers::GameLayer, protocol::TbMapIdCounter,
|
||||
utils::global_observer,
|
||||
};
|
||||
use avian3d::prelude::*;
|
||||
use bevy::{
|
||||
@@ -206,10 +207,10 @@ pub fn plugin(app: &mut App) {
|
||||
app.register_type::<CashSpawn>();
|
||||
app.register_type::<SecretHead>();
|
||||
|
||||
app.add_observer(tb_component_setup::<CashSpawn>);
|
||||
app.add_observer(tb_component_setup::<Platform>);
|
||||
app.add_observer(tb_component_setup::<PlatformTarget>);
|
||||
app.add_observer(tb_component_setup::<Movable>);
|
||||
global_observer!(app, tb_component_setup::<CashSpawn>);
|
||||
global_observer!(app, tb_component_setup::<Platform>);
|
||||
global_observer!(app, tb_component_setup::<PlatformTarget>);
|
||||
global_observer!(app, tb_component_setup::<Movable>);
|
||||
}
|
||||
|
||||
fn tb_component_setup<C: Component>(trigger: Trigger<OnAdd, C>, world: &mut World) {
|
||||
|
||||
@@ -2,11 +2,26 @@ use bevy::prelude::*;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! global_observer {
|
||||
($app:expr,$system:expr) => {{
|
||||
($app:expr, $($system:tt)*) => {{
|
||||
$app.world_mut()
|
||||
.add_observer($system)
|
||||
.insert(Name::new(stringify!($system)))
|
||||
.add_observer($($system)*)
|
||||
.insert(global_observer!(@name $($system)*))
|
||||
}};
|
||||
|
||||
(@name $system:ident ::< $($param:ident),+ $(,)? >) => {{
|
||||
let mut name = String::new();
|
||||
name.push_str(stringify!($system));
|
||||
name.push_str("::<");
|
||||
$(
|
||||
name.push_str(std::any::type_name::<$param>());
|
||||
)+
|
||||
name.push_str(">");
|
||||
Name::new(name)
|
||||
}};
|
||||
|
||||
(@name $system:expr) => {
|
||||
Name::new(stringify!($system))
|
||||
};
|
||||
}
|
||||
|
||||
pub use global_observer;
|
||||
@@ -30,6 +45,6 @@ fn global_observers(
|
||||
};
|
||||
|
||||
for o in query.iter() {
|
||||
cmds.entity(root).add_child(o);
|
||||
cmds.entity(o).try_insert(ChildOf(root));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::utils::global_observer;
|
||||
use bevy::{ecs::system::SystemParam, prelude::*};
|
||||
use lightyear::prelude::{AppTriggerExt, Channel, NetworkDirection, RemoteTrigger, TriggerSender};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -31,8 +32,8 @@ impl TriggerAppExt for App {
|
||||
) {
|
||||
self.add_trigger::<M>()
|
||||
.add_direction(NetworkDirection::ServerToClient);
|
||||
self.add_observer(replicate_trigger_to_clients::<M, C>);
|
||||
self.add_observer(remote_to_local_trigger::<M>);
|
||||
global_observer!(self, replicate_trigger_to_clients::<M, C>);
|
||||
global_observer!(self, remote_to_local_trigger::<M>);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
6
justfile
6
justfile
@@ -12,6 +12,9 @@ run *args:
|
||||
cargo b {{server_args}}
|
||||
RUST_BACKTRACE=1 cargo r {{client_args}} -- {{args}}
|
||||
|
||||
client *args:
|
||||
RUST_BACKTRACE=1 cargo r {{client_args}} -- {{args}}
|
||||
|
||||
server:
|
||||
RUST_BACKTRACE=1 cargo r {{server_args}}
|
||||
|
||||
@@ -19,6 +22,9 @@ dbg *args:
|
||||
cargo b {{server_args}},dbg
|
||||
RUST_BACKTRACE=1 cargo r {{client_args}},dbg -- {{args}}
|
||||
|
||||
dbg-client *args:
|
||||
RUST_BACKTRACE=1 cargo r {{client_args}},dbg -- {{args}}
|
||||
|
||||
dbg-server:
|
||||
RUST_BACKTRACE=1 cargo r {{server_args}},dbg
|
||||
|
||||
|
||||
Reference in New Issue
Block a user