Replicate Sounds (#68)
This commit is contained in:
@@ -17,7 +17,6 @@ bevy = { version = "0.16.0", default-features = false, features = [
|
|||||||
"animation",
|
"animation",
|
||||||
"async_executor",
|
"async_executor",
|
||||||
"bevy_asset",
|
"bevy_asset",
|
||||||
"bevy_audio",
|
|
||||||
"bevy_color",
|
"bevy_color",
|
||||||
"bevy_core_pipeline",
|
"bevy_core_pipeline",
|
||||||
"bevy_gilrs",
|
"bevy_gilrs",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ dbg = ["avian3d/debug-plugin", "dep:bevy-inspector-egui", "shared/dbg"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
avian3d = { workspace = true }
|
avian3d = { workspace = true }
|
||||||
bevy = { workspace = true, default-features = false, features = [
|
bevy = { workspace = true, default-features = false, features = [
|
||||||
|
"bevy_audio",
|
||||||
"bevy_window",
|
"bevy_window",
|
||||||
"bevy_winit",
|
"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,
|
control::ControlState,
|
||||||
global_observer,
|
global_observer,
|
||||||
player::Player,
|
player::Player,
|
||||||
protocol::{DespawnTbMapEntity, TbMapEntityId, TbMapEntityMapping},
|
protocol::{
|
||||||
|
ClientEnteredPlaying, TbMapEntityId, TbMapEntityMapping,
|
||||||
|
channels::UnorderedReliableChannel, messages::DespawnTbMapEntity,
|
||||||
|
},
|
||||||
tb_entities::{Platform, PlatformTarget},
|
tb_entities::{Platform, PlatformTarget},
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
env::current_exe,
|
env::current_exe,
|
||||||
|
fs::File,
|
||||||
io::{BufRead, BufReader},
|
io::{BufRead, BufReader},
|
||||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||||
process::Stdio,
|
process::Stdio,
|
||||||
@@ -43,7 +47,7 @@ pub fn plugin(app: &mut App) {
|
|||||||
parse_local_server_stdout.run_if(resource_exists::<LocalServerStdout>),
|
parse_local_server_stdout.run_if(resource_exists::<LocalServerStdout>),
|
||||||
);
|
);
|
||||||
app.add_systems(Last, close_server_processes);
|
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_connecting);
|
||||||
global_observer!(app, on_connection_failed);
|
global_observer!(app, on_connection_failed);
|
||||||
@@ -124,9 +128,11 @@ fn on_connection_succeeded(
|
|||||||
_trigger: Trigger<OnAdd, Connected>,
|
_trigger: Trigger<OnAdd, Connected>,
|
||||||
state: Res<State<GameState>>,
|
state: Res<State<GameState>>,
|
||||||
mut change_state: ResMut<NextState<GameState>>,
|
mut change_state: ResMut<NextState<GameState>>,
|
||||||
|
mut sender: Single<&mut TriggerSender<ClientEnteredPlaying>>,
|
||||||
) {
|
) {
|
||||||
if *state == GameState::Connecting {
|
if *state == GameState::Connecting {
|
||||||
change_state.set(GameState::Playing);
|
change_state.set(GameState::Playing);
|
||||||
|
sender.trigger::<UnorderedReliableChannel>(ClientEnteredPlaying);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +154,7 @@ fn on_connection_failed(
|
|||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
client_active: Query<&ClientActive>,
|
client_active: Query<&ClientActive>,
|
||||||
mut opened_server: Local<bool>,
|
mut opened_server: Local<bool>,
|
||||||
) {
|
) -> Result {
|
||||||
let disconnected = disconnected.get(trigger.target()).unwrap();
|
let disconnected = disconnected.get(trigger.target()).unwrap();
|
||||||
if *opened_server {
|
if *opened_server {
|
||||||
panic!(
|
panic!(
|
||||||
@@ -164,11 +170,13 @@ fn on_connection_failed(
|
|||||||
// the server executable is assumed to be adjacent to the client executable
|
// 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");
|
let mut exe_path = current_exe().expect("failed to get path of client executable");
|
||||||
exe_path.set_file_name("server");
|
exe_path.set_file_name("server");
|
||||||
|
let server_log_file = File::create("server.log")?;
|
||||||
let mut server_process = std::process::Command::new(exe_path)
|
let mut server_process = std::process::Command::new(exe_path)
|
||||||
.args(["--timeout", "60", "--close-on-client-disconnect"])
|
.args(["--timeout", "60", "--close-on-client-disconnect"])
|
||||||
|
.env("NO_COLOR", "1")
|
||||||
.stdin(Stdio::null())
|
.stdin(Stdio::null())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::null())
|
.stderr(server_log_file)
|
||||||
.spawn()
|
.spawn()
|
||||||
.expect("failed to start server");
|
.expect("failed to start server");
|
||||||
let server_stdout = server_process.stdout.take().unwrap();
|
let server_stdout = server_process.stdout.take().unwrap();
|
||||||
@@ -195,6 +203,8 @@ fn on_connection_failed(
|
|||||||
|
|
||||||
*opened_server = true;
|
*opened_server = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Event)]
|
#[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() {
|
while let Ok(line) = stdout.0.get().try_recv() {
|
||||||
if let "hedz.server_started" = &line[..] {
|
if let "hedz.server_started" = &line[..] {
|
||||||
commands.trigger(LocalServerStarted);
|
commands.trigger(LocalServerStarted);
|
||||||
|
} else {
|
||||||
|
info!("SERVER: {line}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,19 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use rand::{Rng, thread_rng};
|
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)]
|
#[derive(Component, Default)]
|
||||||
#[require(Transform, InheritedVisibility)]
|
#[require(Transform, InheritedVisibility)]
|
||||||
@@ -25,20 +38,52 @@ pub fn plugin(app: &mut App) {
|
|||||||
Update,
|
Update,
|
||||||
(on_added, update_effects, update_particles).run_if(in_state(GameState::Playing)),
|
(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>>) {
|
fn on_added(
|
||||||
for healing in query.iter() {
|
mut commands: Commands,
|
||||||
cmds.entity(healing.0).insert((
|
query: Query<Entity, Added<Healing>>,
|
||||||
Name::new("heal-particle-effect"),
|
assets: Res<AudioAssets>,
|
||||||
HealParticleEffect::default(),
|
) {
|
||||||
));
|
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(
|
fn update_effects(
|
||||||
mut cmds: Commands,
|
mut commands: Commands,
|
||||||
mut query: Query<(&mut HealParticleEffect, Entity)>,
|
mut query: Query<(&mut HealParticleEffect, &HealingEffectsOf, Entity)>,
|
||||||
|
mut transforms: Query<&mut Transform>,
|
||||||
time: Res<Time>,
|
time: Res<Time>,
|
||||||
assets: Res<GameAssets>,
|
assets: Res<GameAssets>,
|
||||||
) {
|
) {
|
||||||
@@ -46,7 +91,14 @@ fn update_effects(
|
|||||||
let mut rng = thread_rng();
|
let mut rng = thread_rng();
|
||||||
|
|
||||||
let now = time.elapsed_secs();
|
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 {
|
if effect.next_spawn < now {
|
||||||
let start_pos = Vec3::new(
|
let start_pos = Vec3::new(
|
||||||
rng.gen_range(-DISTANCE..DISTANCE),
|
rng.gen_range(-DISTANCE..DISTANCE),
|
||||||
@@ -59,7 +111,7 @@ fn update_effects(
|
|||||||
let start_scale = rng.gen_range(0.7..1.0);
|
let start_scale = rng.gen_range(0.7..1.0);
|
||||||
let end_scale = rng.gen_range(0.1..start_scale);
|
let end_scale = rng.gen_range(0.1..start_scale);
|
||||||
|
|
||||||
cmds.entity(e).with_child((
|
commands.entity(e).with_child((
|
||||||
Name::new("heal-particle"),
|
Name::new("heal-particle"),
|
||||||
SceneRoot(assets.mesh_heal_particle.clone()),
|
SceneRoot(assets.mesh_heal_particle.clone()),
|
||||||
Billboard::All,
|
Billboard::All,
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
|
mod backpack;
|
||||||
mod client;
|
mod client;
|
||||||
mod debug;
|
mod debug;
|
||||||
mod enemy;
|
mod enemy;
|
||||||
|
mod heal_effect;
|
||||||
|
mod player;
|
||||||
|
mod sounds;
|
||||||
mod steam;
|
mod steam;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
use crate::utils::{auto_rotate, explosions};
|
|
||||||
use avian3d::prelude::*;
|
use avian3d::prelude::*;
|
||||||
use bevy::{
|
use bevy::{
|
||||||
audio::{PlaybackMode, Volume},
|
audio::{PlaybackMode, Volume},
|
||||||
@@ -22,7 +25,6 @@ use lightyear::prelude::client::ClientPlugins;
|
|||||||
use loading_assets::AudioAssets;
|
use loading_assets::AudioAssets;
|
||||||
use shared::*;
|
use shared::*;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use utils::{billboards, sprite_3d_animation, squish_animation, trail};
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
@@ -87,43 +89,46 @@ fn main() {
|
|||||||
// });
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
app.add_plugins(ai::plugin);
|
app.add_plugins(shared::ai::plugin);
|
||||||
app.add_plugins(animation::plugin);
|
app.add_plugins(shared::animation::plugin);
|
||||||
app.add_plugins(character::plugin);
|
app.add_plugins(shared::character::plugin);
|
||||||
app.add_plugins(cash::plugin);
|
app.add_plugins(shared::cash::plugin);
|
||||||
app.add_plugins(enemy::plugin);
|
app.add_plugins(shared::player::plugin);
|
||||||
app.add_plugins(player::plugin);
|
app.add_plugins(shared::gates::plugin);
|
||||||
app.add_plugins(gates::plugin);
|
app.add_plugins(shared::platforms::plugin);
|
||||||
app.add_plugins(platforms::plugin);
|
app.add_plugins(shared::protocol::plugin);
|
||||||
app.add_plugins(protocol::plugin);
|
app.add_plugins(shared::movables::plugin);
|
||||||
app.add_plugins(movables::plugin);
|
app.add_plugins(shared::utils::billboards::plugin);
|
||||||
app.add_plugins(billboards::plugin);
|
app.add_plugins(shared::aim::plugin);
|
||||||
app.add_plugins(aim::plugin);
|
app.add_plugins(shared::npc::plugin);
|
||||||
app.add_plugins(client::plugin);
|
app.add_plugins(shared::keys::plugin);
|
||||||
app.add_plugins(npc::plugin);
|
app.add_plugins(shared::utils::squish_animation::plugin);
|
||||||
app.add_plugins(keys::plugin);
|
app.add_plugins(shared::cutscene::plugin);
|
||||||
app.add_plugins(squish_animation::plugin);
|
app.add_plugins(shared::control::plugin);
|
||||||
app.add_plugins(cutscene::plugin);
|
app.add_plugins(shared::camera::plugin);
|
||||||
app.add_plugins(control::plugin);
|
app.add_plugins(shared::backpack::plugin);
|
||||||
app.add_plugins(sounds::plugin);
|
app.add_plugins(shared::loading_assets::LoadingPlugin);
|
||||||
app.add_plugins(camera::plugin);
|
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(backpack::plugin);
|
||||||
app.add_plugins(loading_assets::LoadingPlugin);
|
app.add_plugins(client::plugin);
|
||||||
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(debug::plugin);
|
app.add_plugins(debug::plugin);
|
||||||
app.add_plugins(utils::plugin);
|
app.add_plugins(enemy::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(heal_effect::plugin);
|
app.add_plugins(heal_effect::plugin);
|
||||||
app.add_plugins(tb_entities::plugin);
|
app.add_plugins(player::plugin);
|
||||||
app.add_plugins(explosions::plugin);
|
app.add_plugins(sounds::plugin);
|
||||||
app.add_plugins(ui::plugin);
|
app.add_plugins(ui::plugin);
|
||||||
|
|
||||||
app.init_state::<GameState>();
|
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 crate::{global_observer, loading_assets::AudioAssets};
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
use shared::protocol::PlaySound;
|
||||||
#[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),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn plugin(app: &mut App) {
|
pub fn plugin(app: &mut App) {
|
||||||
global_observer!(app, on_spawn_sounds);
|
global_observer!(app, on_spawn_sounds);
|
||||||
@@ -31,7 +9,6 @@ pub fn plugin(app: &mut App) {
|
|||||||
fn on_spawn_sounds(
|
fn on_spawn_sounds(
|
||||||
trigger: Trigger<PlaySound>,
|
trigger: Trigger<PlaySound>,
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
// sound_res: Res<AudioAssets>,
|
|
||||||
// settings: SettingsRead,
|
// settings: SettingsRead,
|
||||||
assets: Res<AudioAssets>,
|
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 avian3d::prelude::*;
|
||||||
use bevy::{
|
use bevy::{app::plugin_group, core_pipeline::tonemapping::Tonemapping, prelude::*};
|
||||||
app::plugin_group,
|
|
||||||
audio::Volume,
|
|
||||||
core_pipeline::tonemapping::Tonemapping,
|
|
||||||
log::{BoxedLayer, tracing_subscriber::Layer},
|
|
||||||
prelude::*,
|
|
||||||
};
|
|
||||||
use bevy_common_assets::ron::RonAssetPlugin;
|
use bevy_common_assets::ron::RonAssetPlugin;
|
||||||
use bevy_sprite3d::Sprite3dPlugin;
|
use bevy_sprite3d::Sprite3dPlugin;
|
||||||
use bevy_trenchbroom::prelude::*;
|
use bevy_trenchbroom::prelude::*;
|
||||||
@@ -14,10 +8,12 @@ use lightyear::prelude::server::ServerPlugins;
|
|||||||
use shared::{DebugVisuals, GameState, heads_database::HeadDatabaseAsset};
|
use shared::{DebugVisuals, GameState, heads_database::HeadDatabaseAsset};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
mod backpack;
|
||||||
mod config;
|
mod config;
|
||||||
mod player;
|
mod player;
|
||||||
mod server;
|
mod server;
|
||||||
mod tb_entities;
|
mod tb_entities;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
plugin_group! {
|
plugin_group! {
|
||||||
pub struct DefaultPlugins {
|
pub struct DefaultPlugins {
|
||||||
@@ -44,7 +40,6 @@ plugin_group! {
|
|||||||
bevy::ui:::UiPlugin,
|
bevy::ui:::UiPlugin,
|
||||||
bevy::pbr:::PbrPlugin,
|
bevy::pbr:::PbrPlugin,
|
||||||
bevy::gltf:::GltfPlugin,
|
bevy::gltf:::GltfPlugin,
|
||||||
bevy::audio:::AudioPlugin,
|
|
||||||
bevy::gilrs:::GilrsPlugin,
|
bevy::gilrs:::GilrsPlugin,
|
||||||
bevy::animation:::AnimationPlugin,
|
bevy::animation:::AnimationPlugin,
|
||||||
bevy::gizmos:::GizmoPlugin,
|
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() {
|
fn main() {
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
|
|
||||||
@@ -88,7 +66,7 @@ fn main() {
|
|||||||
filter: "info,lightyear_replication=off".into(),
|
filter: "info,lightyear_replication=off".into(),
|
||||||
level: bevy::log::Level::INFO,
|
level: bevy::log::Level::INFO,
|
||||||
// provide custom log layer to receive logging events
|
// provide custom log layer to receive logging events
|
||||||
custom_layer: log_to_file_layer,
|
..default()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
app.add_plugins(ServerPlugins {
|
app.add_plugins(ServerPlugins {
|
||||||
@@ -102,17 +80,6 @@ fn main() {
|
|||||||
app.add_plugins(UiGradientsPlugin);
|
app.add_plugins(UiGradientsPlugin);
|
||||||
app.add_plugins(RonAssetPlugin::<HeadDatabaseAsset>::new(&["headsdb.ron"]));
|
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::abilities::plugin);
|
||||||
app.add_plugins(shared::ai::plugin);
|
app.add_plugins(shared::ai::plugin);
|
||||||
app.add_plugins(shared::aim::plugin);
|
app.add_plugins(shared::aim::plugin);
|
||||||
@@ -127,7 +94,6 @@ fn main() {
|
|||||||
app.add_plugins(shared::gates::plugin);
|
app.add_plugins(shared::gates::plugin);
|
||||||
app.add_plugins(shared::head_drop::plugin);
|
app.add_plugins(shared::head_drop::plugin);
|
||||||
app.add_plugins(shared::heads::plugin);
|
app.add_plugins(shared::heads::plugin);
|
||||||
app.add_plugins(shared::heal_effect::plugin);
|
|
||||||
app.add_plugins(shared::hitpoints::plugin);
|
app.add_plugins(shared::hitpoints::plugin);
|
||||||
app.add_plugins(shared::keys::plugin);
|
app.add_plugins(shared::keys::plugin);
|
||||||
app.add_plugins(shared::loading_assets::LoadingPlugin);
|
app.add_plugins(shared::loading_assets::LoadingPlugin);
|
||||||
@@ -137,7 +103,6 @@ fn main() {
|
|||||||
app.add_plugins(shared::platforms::plugin);
|
app.add_plugins(shared::platforms::plugin);
|
||||||
app.add_plugins(shared::player::plugin);
|
app.add_plugins(shared::player::plugin);
|
||||||
app.add_plugins(shared::protocol::plugin);
|
app.add_plugins(shared::protocol::plugin);
|
||||||
app.add_plugins(shared::sounds::plugin);
|
|
||||||
app.add_plugins(shared::steam::plugin);
|
app.add_plugins(shared::steam::plugin);
|
||||||
app.add_plugins(shared::tb_entities::plugin);
|
app.add_plugins(shared::tb_entities::plugin);
|
||||||
app.add_plugins(shared::utils::auto_rotate::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::utils::plugin);
|
||||||
app.add_plugins(shared::water::plugin);
|
app.add_plugins(shared::water::plugin);
|
||||||
|
|
||||||
|
app.add_plugins(backpack::plugin);
|
||||||
app.add_plugins(config::plugin);
|
app.add_plugins(config::plugin);
|
||||||
|
app.add_plugins(player::plugin);
|
||||||
app.add_plugins(server::plugin);
|
app.add_plugins(server::plugin);
|
||||||
app.add_plugins(tb_entities::plugin);
|
app.add_plugins(tb_entities::plugin);
|
||||||
|
app.add_plugins(utils::plugin);
|
||||||
|
|
||||||
app.init_state::<GameState>();
|
app.init_state::<GameState>();
|
||||||
|
|
||||||
app.insert_resource(AmbientLight {
|
app.add_systems(PostStartup, setup_panic_handler);
|
||||||
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.run();
|
app.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn setup_panic_handler() {
|
||||||
|
_ = std::panic::take_hook();
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use shared::{
|
|||||||
cash::CashResource,
|
cash::CashResource,
|
||||||
character::AnimatedCharacter,
|
character::AnimatedCharacter,
|
||||||
control::{ControlState, controller_common::PlayerCharacterController},
|
control::{ControlState, controller_common::PlayerCharacterController},
|
||||||
|
global_observer,
|
||||||
head::ActiveHead,
|
head::ActiveHead,
|
||||||
head_drop::HeadDrops,
|
head_drop::HeadDrops,
|
||||||
heads::{ActiveHeads, HeadChanged, HeadState, heads_ui::UiActiveHeads},
|
heads::{ActiveHeads, HeadChanged, HeadState, heads_ui::UiActiveHeads},
|
||||||
@@ -13,19 +14,23 @@ use shared::{
|
|||||||
hitpoints::{Hitpoints, Kill},
|
hitpoints::{Hitpoints, Kill},
|
||||||
npc::SpawnCharacter,
|
npc::SpawnCharacter,
|
||||||
player::{Player, PlayerBodyMesh},
|
player::{Player, PlayerBodyMesh},
|
||||||
|
protocol::{
|
||||||
|
PlaySound, PlayerId, channels::UnorderedReliableChannel, events::ClientHeadChanged,
|
||||||
|
},
|
||||||
tb_entities::SpawnPoint,
|
tb_entities::SpawnPoint,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub fn plugin(app: &mut App) {
|
||||||
|
global_observer!(app, on_update_head_mesh);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn spawn(
|
pub fn spawn(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
owner: Entity,
|
owner: Entity,
|
||||||
query: Query<&Transform, With<SpawnPoint>>,
|
query: Query<&Transform, With<SpawnPoint>>,
|
||||||
asset_server: Res<AssetServer>,
|
|
||||||
heads_db: Res<HeadsDatabase>,
|
heads_db: Res<HeadsDatabase>,
|
||||||
) {
|
) -> Option<Entity> {
|
||||||
let Some(spawn) = query.iter().next() else {
|
let spawn = query.iter().next()?;
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let transform = Transform::from_translation(spawn.translation + Vec3::new(0., 3., 0.));
|
let transform = Transform::from_translation(spawn.translation + Vec3::new(0., 3., 0.));
|
||||||
|
|
||||||
@@ -47,6 +52,7 @@ pub fn spawn(
|
|||||||
transform,
|
transform,
|
||||||
Visibility::default(),
|
Visibility::default(),
|
||||||
PlayerCharacterController,
|
PlayerCharacterController,
|
||||||
|
PlayerId { id: 0 },
|
||||||
),
|
),
|
||||||
ActionState::<ControlState>::default(),
|
ActionState::<ControlState>::default(),
|
||||||
Backpack::default(),
|
Backpack::default(),
|
||||||
@@ -67,12 +73,12 @@ pub fn spawn(
|
|||||||
));
|
));
|
||||||
player.observe(on_kill);
|
player.observe(on_kill);
|
||||||
|
|
||||||
commands.spawn((
|
let id = player.id();
|
||||||
AudioPlayer::new(asset_server.load("sfx/heads/angry demonstrator.ogg")),
|
|
||||||
PlaybackSettings::DESPAWN,
|
|
||||||
));
|
|
||||||
|
|
||||||
|
commands.trigger(PlaySound::Head("angry demonstrator".to_string()));
|
||||||
commands.trigger(SpawnCharacter(transform.translation));
|
commands.trigger(SpawnCharacter(transform.translation));
|
||||||
|
|
||||||
|
Some(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_kill(
|
fn on_kill(
|
||||||
@@ -92,3 +98,27 @@ fn on_kill(
|
|||||||
commands.trigger(HeadChanged(new_head));
|
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 crate::config::ServerConfig;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use lightyear::{
|
use lightyear::{
|
||||||
|
connection::client::PeerMetadata,
|
||||||
link::LinkConditioner,
|
link::LinkConditioner,
|
||||||
prelude::{
|
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::{
|
use std::{
|
||||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn plugin(app: &mut App) {
|
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(
|
app.add_systems(
|
||||||
Update,
|
Update,
|
||||||
(
|
(notify_started, run_timeout).run_if(in_state(GameState::Playing)),
|
||||||
notify_started.run_if(in_state(GameState::Playing)),
|
|
||||||
run_timeout,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
global_observer!(app, handle_new_client);
|
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, close_on_disconnect);
|
||||||
global_observer!(app, cancel_timeout);
|
global_observer!(app, cancel_timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
struct ClientPlayerId(u8);
|
||||||
|
|
||||||
fn handle_new_client(
|
fn handle_new_client(
|
||||||
trigger: Trigger<OnAdd, Connected>,
|
trigger: Trigger<OnAdd, Linked>,
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
id: Query<&PeerAddr>,
|
id: Query<&PeerAddr>,
|
||||||
asset_server: Res<AssetServer>,
|
|
||||||
query: Query<&Transform, With<SpawnPoint>>,
|
|
||||||
heads_db: Res<HeadsDatabase>,
|
|
||||||
) -> Result {
|
) -> Result {
|
||||||
let id = id.get(trigger.target())?;
|
let Ok(id) = id.get(trigger.target()) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
info!("Client connected on IP: {}", id.ip());
|
info!("Client connected on IP: {}", id.ip());
|
||||||
|
|
||||||
@@ -46,11 +58,38 @@ fn handle_new_client(
|
|||||||
incoming_loss: 0.0,
|
incoming_loss: 0.0,
|
||||||
});
|
});
|
||||||
|
|
||||||
commands
|
commands.entity(trigger.target()).insert((
|
||||||
.entity(trigger.target())
|
ReplicationSender::default(),
|
||||||
.insert((ReplicationSender::default(), Link::new(Some(conditioner))));
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -61,6 +100,7 @@ fn close_on_disconnect(
|
|||||||
mut writer: EventWriter<AppExit>,
|
mut writer: EventWriter<AppExit>,
|
||||||
) {
|
) {
|
||||||
if config.close_on_client_disconnect {
|
if config.close_on_client_disconnect {
|
||||||
|
info!("client disconnected, exiting");
|
||||||
writer.write(AppExit::Success);
|
writer.write(AppExit::Success);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,10 +143,12 @@ fn run_timeout(mut timer: ResMut<TimeoutTimer>, mut writer: EventWriter<AppExit>
|
|||||||
timer.0 -= time.delta_secs();
|
timer.0 -= time.delta_secs();
|
||||||
|
|
||||||
if timer.0 <= 0.0 {
|
if timer.0 <= 0.0 {
|
||||||
|
info!("client timed out, exiting");
|
||||||
writer.write(AppExit::Success);
|
writer.write(AppExit::Success);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cancel_timeout(_trigger: Trigger<OnAdd, Connected>, mut timer: ResMut<TimeoutTimer>) {
|
fn cancel_timeout(_trigger: Trigger<OnAdd, Connected>, mut timer: ResMut<TimeoutTimer>) {
|
||||||
|
info!("client connected, cancelling timeout");
|
||||||
timer.0 = f32::INFINITY;
|
timer.0 = f32::INFINITY;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use lightyear::prelude::{ActionsChannel, Connected, MessageSender};
|
use lightyear::prelude::{Connected, MessageSender};
|
||||||
use shared::{
|
use shared::{
|
||||||
GameState, global_observer,
|
GameState, global_observer,
|
||||||
protocol::{DespawnTbMapEntity, TbMapEntityId},
|
protocol::{TbMapEntityId, channels::UnorderedReliableChannel, messages::DespawnTbMapEntity},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn plugin(app: &mut App) {
|
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();
|
let mut send = send.get_mut(trigger.target()).unwrap();
|
||||||
for &id in cache.0.iter() {
|
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 super::TriggerArrow;
|
||||||
use crate::{
|
use crate::{
|
||||||
GameState, billboards::Billboard, global_observer, heads_database::HeadsDatabase,
|
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,
|
utils::sprite_3d_animation::AnimationTimer,
|
||||||
};
|
};
|
||||||
use avian3d::prelude::*;
|
use avian3d::prelude::*;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::TriggerGun;
|
use super::TriggerGun;
|
||||||
use crate::{
|
use crate::{
|
||||||
GameState, billboards::Billboard, global_observer, heads_database::HeadsDatabase,
|
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,
|
tb_entities::EnemySpawn, utils::sprite_3d_animation::AnimationTimer,
|
||||||
};
|
};
|
||||||
use avian3d::prelude::*;
|
use avian3d::prelude::*;
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
GameState, global_observer, heads::ActiveHeads, heads_database::HeadsDatabase,
|
GameState, global_observer, heads::ActiveHeads, heads_database::HeadsDatabase,
|
||||||
hitpoints::Hitpoints, loading_assets::AudioAssets,
|
hitpoints::Hitpoints,
|
||||||
};
|
};
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct Healing(pub Entity);
|
pub struct Healing;
|
||||||
|
|
||||||
#[derive(Event, Debug)]
|
#[derive(Clone, Event, Debug, Serialize, Deserialize)]
|
||||||
pub enum HealingStateChanged {
|
pub enum HealingStateChanged {
|
||||||
Started,
|
Started,
|
||||||
Stopped,
|
Stopped,
|
||||||
@@ -22,27 +23,21 @@ pub fn plugin(app: &mut App) {
|
|||||||
fn on_heal_start_stop(
|
fn on_heal_start_stop(
|
||||||
trigger: Trigger<HealingStateChanged>,
|
trigger: Trigger<HealingStateChanged>,
|
||||||
mut cmds: Commands,
|
mut cmds: Commands,
|
||||||
assets: Res<AudioAssets>,
|
|
||||||
query: Query<&Healing>,
|
query: Query<&Healing>,
|
||||||
) {
|
) {
|
||||||
if matches!(trigger.event(), HealingStateChanged::Started) {
|
if matches!(trigger.event(), HealingStateChanged::Started) {
|
||||||
let e = cmds
|
if query.contains(trigger.target()) {
|
||||||
.spawn((
|
// already healing, just ignore
|
||||||
Name::new("sfx-heal"),
|
return;
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmds.entity(trigger.target()).insert(Healing);
|
||||||
|
} else {
|
||||||
|
if !query.contains(trigger.target()) {
|
||||||
|
// Not healing, just ignore
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
cmds.entity(trigger.target()).remove::<Healing>();
|
cmds.entity(trigger.target()).remove::<Healing>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ use crate::{
|
|||||||
abilities::BuildExplosionSprite,
|
abilities::BuildExplosionSprite,
|
||||||
heads_database::HeadsDatabase,
|
heads_database::HeadsDatabase,
|
||||||
physics_layers::GameLayer,
|
physics_layers::GameLayer,
|
||||||
protocol::GltfSceneRoot,
|
protocol::{GltfSceneRoot, PlaySound},
|
||||||
sounds::PlaySound,
|
|
||||||
utils::{commands::CommandExt, explosions::Explosion, global_observer, trail::Trail},
|
utils::{commands::CommandExt, explosions::Explosion, global_observer, trail::Trail},
|
||||||
};
|
};
|
||||||
use avian3d::prelude::*;
|
use avian3d::prelude::*;
|
||||||
|
|||||||
@@ -9,25 +9,23 @@ use crate::{
|
|||||||
GameState,
|
GameState,
|
||||||
aim::AimTarget,
|
aim::AimTarget,
|
||||||
character::CharacterHierarchy,
|
character::CharacterHierarchy,
|
||||||
control::ControlState,
|
|
||||||
global_observer,
|
global_observer,
|
||||||
head::ActiveHead,
|
|
||||||
heads::ActiveHeads,
|
heads::ActiveHeads,
|
||||||
heads_database::HeadsDatabase,
|
heads_database::HeadsDatabase,
|
||||||
loading_assets::GameAssets,
|
loading_assets::GameAssets,
|
||||||
physics_layers::GameLayer,
|
physics_layers::GameLayer,
|
||||||
player::{Player, PlayerBodyMesh},
|
player::{Player, PlayerBodyMesh},
|
||||||
sounds::PlaySound,
|
protocol::PlaySound,
|
||||||
utils::{billboards::Billboard, sprite_3d_animation::AnimationTimer},
|
utils::{billboards::Billboard, sprite_3d_animation::AnimationTimer},
|
||||||
};
|
};
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
use crate::{control::ControlState, head::ActiveHead};
|
||||||
use bevy::{pbr::NotShadowCaster, prelude::*};
|
use bevy::{pbr::NotShadowCaster, prelude::*};
|
||||||
use bevy_sprite3d::{Sprite3dBuilder, Sprite3dParams};
|
use bevy_sprite3d::{Sprite3dBuilder, Sprite3dParams};
|
||||||
pub use healing::Healing;
|
pub use healing::Healing;
|
||||||
use healing::HealingStateChanged;
|
use healing::HealingStateChanged;
|
||||||
use lightyear::{
|
#[cfg(feature = "server")]
|
||||||
connection::client::ClientState,
|
use lightyear::prelude::input::native::ActionState;
|
||||||
prelude::{Client, input::native::ActionState},
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Reflect, Default, Serialize, Deserialize)]
|
#[derive(Debug, Copy, Clone, PartialEq, Reflect, Default, Serialize, Deserialize)]
|
||||||
@@ -113,6 +111,7 @@ pub fn plugin(app: &mut App) {
|
|||||||
Update,
|
Update,
|
||||||
(update, update_heal_ability).run_if(in_state(GameState::Playing)),
|
(update, update_heal_ability).run_if(in_state(GameState::Playing)),
|
||||||
);
|
);
|
||||||
|
#[cfg(feature = "server")]
|
||||||
app.add_systems(
|
app.add_systems(
|
||||||
FixedUpdate,
|
FixedUpdate,
|
||||||
on_trigger_state.run_if(in_state(GameState::Playing)),
|
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);
|
global_observer!(app, build_explosion_sprite);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "server")]
|
||||||
fn on_trigger_state(
|
fn on_trigger_state(
|
||||||
mut res: ResMut<TriggerStateRes>,
|
mut res: ResMut<TriggerStateRes>,
|
||||||
player: Query<(&ActiveHead, &ActionState<ControlState>), With<Player>>,
|
player: Query<(&ActiveHead, &ActionState<ControlState>), With<Player>>,
|
||||||
headdb: Res<HeadsDatabase>,
|
headdb: Res<HeadsDatabase>,
|
||||||
time: Res<Time>,
|
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() {
|
for (player_head, controls) in player.iter() {
|
||||||
res.active = controls.trigger;
|
res.active = controls.trigger;
|
||||||
if controls.just_triggered {
|
if controls.just_triggered {
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ use crate::{
|
|||||||
abilities::BuildExplosionSprite,
|
abilities::BuildExplosionSprite,
|
||||||
heads_database::HeadsDatabase,
|
heads_database::HeadsDatabase,
|
||||||
physics_layers::GameLayer,
|
physics_layers::GameLayer,
|
||||||
protocol::GltfSceneRoot,
|
protocol::{GltfSceneRoot, PlaySound},
|
||||||
sounds::PlaySound,
|
|
||||||
utils::{
|
utils::{
|
||||||
auto_rotate::AutoRotation, commands::CommandExt, explosions::Explosion, global_observer,
|
auto_rotate::AutoRotation, commands::CommandExt, explosions::Explosion, global_observer,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,352 +1,40 @@
|
|||||||
#[cfg(feature = "server")]
|
|
||||||
use super::Backpack;
|
|
||||||
use super::UiHeadState;
|
use super::UiHeadState;
|
||||||
use crate::{
|
use bevy::prelude::*;
|
||||||
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 serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
static HEAD_SLOTS: usize = 5;
|
pub static HEAD_SLOTS: usize = 5;
|
||||||
|
|
||||||
#[derive(Component, Default)]
|
#[derive(Component, Default)]
|
||||||
struct BackpackMarker;
|
pub struct BackpackMarker;
|
||||||
|
|
||||||
#[derive(Component, Default)]
|
#[derive(Component, Default)]
|
||||||
struct BackpackCountText;
|
pub struct BackpackCountText;
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
#[derive(Component, Default)]
|
#[derive(Component, Default)]
|
||||||
struct HeadSelector(pub usize);
|
pub struct HeadSelector(pub usize);
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
#[derive(Component, Default)]
|
#[derive(Component, Default)]
|
||||||
struct HeadImage(pub usize);
|
pub struct HeadImage(pub usize);
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
#[derive(Component, Default)]
|
#[derive(Component, Default)]
|
||||||
struct HeadDamage(pub usize);
|
pub struct HeadDamage(pub usize);
|
||||||
|
|
||||||
#[derive(Component, Default, Debug, Reflect, Serialize, Deserialize, PartialEq)]
|
#[derive(Component, Default, Debug, Reflect, Serialize, Deserialize, PartialEq)]
|
||||||
#[reflect(Component, Default)]
|
#[reflect(Component, Default)]
|
||||||
pub struct BackpackUiState {
|
pub struct BackpackUiState {
|
||||||
heads: [Option<UiHeadState>; 5],
|
pub heads: [Option<UiHeadState>; 5],
|
||||||
scroll: usize,
|
pub scroll: usize,
|
||||||
count: usize,
|
pub count: usize,
|
||||||
current_slot: usize,
|
pub current_slot: usize,
|
||||||
open: bool,
|
pub open: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "client")]
|
|
||||||
impl BackpackUiState {
|
impl BackpackUiState {
|
||||||
fn relative_current_slot(&self) -> usize {
|
pub fn relative_current_slot(&self) -> usize {
|
||||||
self.current_slot.saturating_sub(self.scroll)
|
self.current_slot.saturating_sub(self.scroll)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn plugin(app: &mut App) {
|
pub fn plugin(app: &mut App) {
|
||||||
app.register_type::<BackpackUiState>();
|
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 crate::heads::HeadState;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -24,8 +23,7 @@ impl UiHeadState {
|
|||||||
self.reloading
|
self.reloading
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
pub fn new(value: HeadState, time: f32) -> Self {
|
||||||
pub(crate) fn new(value: HeadState, time: f32) -> Self {
|
|
||||||
let reloading = if value.has_ammo() {
|
let reloading = if value.has_ammo() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::{GameState, HEDZ_GREEN, loading_assets::UIAssets};
|
use crate::{GameState, HEDZ_GREEN, loading_assets::UIAssets};
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
use crate::{global_observer, sounds::PlaySound};
|
use crate::{global_observer, protocol::PlaySound};
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
cash::CashResource, control::ControlState, hitpoints::Hitpoints, player::Player,
|
cash::CashResource, control::ControlState, hitpoints::Hitpoints, player::Player,
|
||||||
sounds::PlaySound,
|
protocol::PlaySound,
|
||||||
};
|
};
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use lightyear::prelude::input::native::ActionState;
|
use lightyear::prelude::input::native::ActionState;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use bevy::{
|
|||||||
animation::RepeatAnimation, ecs::system::SystemParam, platform::collections::HashMap,
|
animation::RepeatAnimation, ecs::system::SystemParam, platform::collections::HashMap,
|
||||||
prelude::*, scene::SceneInstanceReady,
|
prelude::*, scene::SceneInstanceReady,
|
||||||
};
|
};
|
||||||
|
use lightyear::prelude::DisableReplicateHierarchy;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{f32::consts::PI, time::Duration};
|
use std::{f32::consts::PI, time::Duration};
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ use std::{f32::consts::PI, time::Duration};
|
|||||||
pub struct ProjectileOrigin;
|
pub struct ProjectileOrigin;
|
||||||
|
|
||||||
#[derive(Component, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Component, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[require(Visibility, GlobalTransform)]
|
||||||
pub struct AnimatedCharacter {
|
pub struct AnimatedCharacter {
|
||||||
head: usize,
|
head: usize,
|
||||||
}
|
}
|
||||||
@@ -107,14 +109,19 @@ fn spawn(
|
|||||||
|
|
||||||
transform.rotate_y(PI);
|
transform.rotate_y(PI);
|
||||||
|
|
||||||
|
commands.entity(entity).despawn_related::<Children>();
|
||||||
|
|
||||||
commands
|
commands
|
||||||
.entity(entity)
|
.spawn((
|
||||||
.insert((
|
|
||||||
transform,
|
|
||||||
SceneRoot(asset.scenes[0].clone()),
|
SceneRoot(asset.scenes[0].clone()),
|
||||||
AnimatedCharacterAsset(handle.clone()),
|
DisableReplicateHierarchy,
|
||||||
|
ChildOf(entity),
|
||||||
))
|
))
|
||||||
.observe(find_marker_bones);
|
.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)]
|
#[derive(Component, Default, Reflect, PartialEq, Serialize, Deserialize)]
|
||||||
#[reflect(Component)]
|
#[reflect(Component)]
|
||||||
pub struct Character;
|
pub struct HedzCharacter;
|
||||||
|
|
||||||
fn setup_once_loaded(
|
fn setup_once_loaded(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut query: Query<(Entity, &mut AnimationPlayer), Added<AnimationPlayer>>,
|
mut query: Query<(Entity, &mut AnimationPlayer), Added<AnimationPlayer>>,
|
||||||
parent: Query<&ChildOf>,
|
parent: Query<&ChildOf>,
|
||||||
animated_character: Query<(&AnimatedCharacter, &AnimatedCharacterAsset)>,
|
animated_character: Query<(&AnimatedCharacter, &AnimatedCharacterAsset)>,
|
||||||
characters: Query<Entity, With<Character>>,
|
characters: Query<Entity, With<HedzCharacter>>,
|
||||||
gltf_assets: Res<Assets<Gltf>>,
|
gltf_assets: Res<Assets<Gltf>>,
|
||||||
mut graphs: ResMut<Assets<AnimationGraph>>,
|
mut graphs: ResMut<Assets<AnimationGraph>>,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -55,14 +55,12 @@ fn rotate_rig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn apply_controls(
|
fn apply_controls(
|
||||||
mut character: Query<(&mut MoveInput, &MovementSpeedFactor)>,
|
character: Single<(&mut MoveInput, &MovementSpeedFactor)>,
|
||||||
rig_transform_q: Option<Single<&GlobalTransform, With<PlayerBodyMesh>>>,
|
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 {
|
if let Some(ref rig_transform) = rig_transform_q {
|
||||||
char_input.set(-*rig_transform.forward() * factor.0);
|
char_input.set(-*rig_transform.forward() * factor.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
use super::{ControlState, Controls};
|
use super::{ControlState, Controls};
|
||||||
#[cfg(feature = "client")]
|
|
||||||
use crate::player::Player;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
GameState,
|
GameState,
|
||||||
control::{CharacterInputEnabled, ControllerSet},
|
control::{CharacterInputEnabled, ControllerSet},
|
||||||
@@ -14,7 +12,7 @@ use bevy::{
|
|||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "client")]
|
#[cfg(feature = "client")]
|
||||||
use lightyear::prelude::input::native::ActionState;
|
use lightyear::prelude::input::native::{ActionState, InputMarker};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub fn plugin(app: &mut App) {
|
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
|
/// Write inputs from combined keyboard/gamepad state into the networked input buffer
|
||||||
/// for the local player.
|
/// for the local player.
|
||||||
fn buffer_inputs(
|
fn buffer_inputs(
|
||||||
mut player: Single<&mut ActionState<ControlState>, With<Player>>,
|
mut player: Single<&mut ActionState<ControlState>, With<InputMarker<ControlState>>>,
|
||||||
controls: Res<ControlState>,
|
controls: Res<ControlState>,
|
||||||
) {
|
) {
|
||||||
player.0 = *controls;
|
player.0 = *controls;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
cutscene::StartCutscene, global_observer, keys::KeyCollected, movables::TriggerMovableEvent,
|
cutscene::StartCutscene, global_observer, keys::KeyCollected, movables::TriggerMovableEvent,
|
||||||
sounds::PlaySound,
|
protocol::PlaySound,
|
||||||
};
|
};
|
||||||
use bevy::{platform::collections::HashSet, prelude::*};
|
use bevy::{platform::collections::HashSet, prelude::*};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
GameState, billboards::Billboard, global_observer, heads_database::HeadsDatabase,
|
GameState,
|
||||||
physics_layers::GameLayer, player::Player, protocol::GltfSceneRoot, sounds::PlaySound,
|
billboards::Billboard,
|
||||||
squish_animation::SquishAnimation, tb_entities::SecretHead,
|
global_observer,
|
||||||
|
heads_database::HeadsDatabase,
|
||||||
|
physics_layers::GameLayer,
|
||||||
|
player::Player,
|
||||||
|
protocol::{GltfSceneRoot, PlaySound},
|
||||||
|
squish_animation::SquishAnimation,
|
||||||
|
tb_entities::SecretHead,
|
||||||
};
|
};
|
||||||
use avian3d::prelude::*;
|
use avian3d::prelude::*;
|
||||||
use bevy::{
|
use bevy::{
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ use crate::animation::AnimationFlags;
|
|||||||
use crate::{
|
use crate::{
|
||||||
GameState,
|
GameState,
|
||||||
backpack::{BackbackSwapEvent, Backpack},
|
backpack::{BackbackSwapEvent, Backpack},
|
||||||
control::ControlState,
|
|
||||||
global_observer,
|
global_observer,
|
||||||
heads_database::HeadsDatabase,
|
heads_database::HeadsDatabase,
|
||||||
hitpoints::Hitpoints,
|
hitpoints::Hitpoints,
|
||||||
player::Player,
|
player::Player,
|
||||||
sounds::PlaySound,
|
|
||||||
};
|
};
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
use crate::{control::ControlState, protocol::PlaySound};
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
#[cfg(feature = "server")]
|
||||||
use lightyear::prelude::input::native::ActionState;
|
use lightyear::prelude::input::native::ActionState;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -188,6 +189,7 @@ pub fn plugin(app: &mut App) {
|
|||||||
FixedUpdate,
|
FixedUpdate,
|
||||||
(reload, sync_hp).run_if(in_state(GameState::Playing)),
|
(reload, sync_hp).run_if(in_state(GameState::Playing)),
|
||||||
);
|
);
|
||||||
|
#[cfg(feature = "server")]
|
||||||
app.add_systems(FixedUpdate, on_select_active_head);
|
app.add_systems(FixedUpdate, on_select_active_head);
|
||||||
|
|
||||||
global_observer!(app, on_swap_backpack);
|
global_observer!(app, on_swap_backpack);
|
||||||
@@ -238,6 +240,7 @@ fn reload(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "server")]
|
||||||
fn on_select_active_head(
|
fn on_select_active_head(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut query: Query<(&mut ActiveHeads, &mut Hitpoints, &ActionState<ControlState>), With<Player>>,
|
mut query: Query<(&mut ActiveHeads, &mut Hitpoints, &ActionState<ControlState>), With<Player>>,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::{
|
|||||||
GameState,
|
GameState,
|
||||||
animation::AnimationFlags,
|
animation::AnimationFlags,
|
||||||
character::{CharacterAnimations, HasCharacterAnimations},
|
character::{CharacterAnimations, HasCharacterAnimations},
|
||||||
sounds::PlaySound,
|
protocol::PlaySound,
|
||||||
};
|
};
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
billboards::Billboard, global_observer, physics_layers::GameLayer, player::Player,
|
billboards::Billboard,
|
||||||
protocol::GltfSceneRoot, sounds::PlaySound, squish_animation::SquishAnimation,
|
global_observer,
|
||||||
|
physics_layers::GameLayer,
|
||||||
|
player::Player,
|
||||||
|
protocol::{GltfSceneRoot, PlaySound},
|
||||||
|
squish_animation::SquishAnimation,
|
||||||
};
|
};
|
||||||
use avian3d::prelude::*;
|
use avian3d::prelude::*;
|
||||||
use bevy::{
|
use bevy::{
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ pub mod head;
|
|||||||
pub mod head_drop;
|
pub mod head_drop;
|
||||||
pub mod heads;
|
pub mod heads;
|
||||||
pub mod heads_database;
|
pub mod heads_database;
|
||||||
pub mod heal_effect;
|
|
||||||
pub mod hitpoints;
|
pub mod hitpoints;
|
||||||
pub mod keys;
|
pub mod keys;
|
||||||
pub mod loading_assets;
|
pub mod loading_assets;
|
||||||
@@ -25,7 +24,6 @@ pub mod physics_layers;
|
|||||||
pub mod platforms;
|
pub mod platforms;
|
||||||
pub mod player;
|
pub mod player;
|
||||||
pub mod protocol;
|
pub mod protocol;
|
||||||
pub mod sounds;
|
|
||||||
pub mod steam;
|
pub mod steam;
|
||||||
pub mod tb_entities;
|
pub mod tb_entities;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|||||||
@@ -124,15 +124,16 @@ pub struct LoadingPlugin;
|
|||||||
impl Plugin for LoadingPlugin {
|
impl Plugin for LoadingPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.add_systems(OnExit(GameState::AssetLoading), on_exit);
|
app.add_systems(OnExit(GameState::AssetLoading), on_exit);
|
||||||
app.add_loading_state(
|
let loading_state = LoadingState::new(GameState::AssetLoading);
|
||||||
LoadingState::new(GameState::AssetLoading)
|
let loading_state = loading_state
|
||||||
.continue_to_state(GameState::MapLoading)
|
.continue_to_state(GameState::MapLoading)
|
||||||
.load_collection::<AudioAssets>()
|
.load_collection::<GameAssets>()
|
||||||
.load_collection::<GameAssets>()
|
.load_collection::<HeadsAssets>()
|
||||||
.load_collection::<HeadsAssets>()
|
.load_collection::<HeadDropAssets>()
|
||||||
.load_collection::<HeadDropAssets>()
|
.load_collection::<UIAssets>();
|
||||||
.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::{
|
use crate::{
|
||||||
GameState, character::Character, global_observer, loading_assets::GameAssets,
|
GameState, character::HedzCharacter, global_observer, loading_assets::GameAssets,
|
||||||
utils::billboards::Billboard,
|
utils::billboards::Billboard,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
@@ -12,7 +12,7 @@ use crate::{
|
|||||||
heads_database::HeadsDatabase,
|
heads_database::HeadsDatabase,
|
||||||
hitpoints::{Hitpoints, Kill},
|
hitpoints::{Hitpoints, Kill},
|
||||||
keys::KeySpawn,
|
keys::KeySpawn,
|
||||||
sounds::PlaySound,
|
protocol::PlaySound,
|
||||||
tb_entities::EnemySpawn,
|
tb_entities::EnemySpawn,
|
||||||
};
|
};
|
||||||
use bevy::{pbr::NotShadowCaster, prelude::*};
|
use bevy::{pbr::NotShadowCaster, prelude::*};
|
||||||
@@ -24,7 +24,7 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
#[derive(Component, Reflect, PartialEq, Serialize, Deserialize)]
|
#[derive(Component, Reflect, PartialEq, Serialize, Deserialize)]
|
||||||
#[reflect(Component)]
|
#[reflect(Component)]
|
||||||
#[require(Character)]
|
#[require(HedzCharacter)]
|
||||||
pub struct Npc;
|
pub struct Npc;
|
||||||
|
|
||||||
#[derive(Resource, Reflect, Default)]
|
#[derive(Resource, Reflect, Default)]
|
||||||
@@ -102,7 +102,6 @@ fn on_spawn_check(
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
]),
|
]),
|
||||||
#[cfg(feature = "server")]
|
|
||||||
Replicate::to_clients(NetworkTarget::All),
|
Replicate::to_clients(NetworkTarget::All),
|
||||||
))
|
))
|
||||||
.insert_if(Ai, || !spawn.disable_ai)
|
.insert_if(Ai, || !spawn.disable_ai)
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
GameState,
|
GameState,
|
||||||
cash::{Cash, CashCollectEvent},
|
cash::{Cash, CashCollectEvent},
|
||||||
character::{AnimatedCharacter, Character},
|
character::HedzCharacter,
|
||||||
global_observer,
|
|
||||||
head::ActiveHead,
|
|
||||||
heads::HeadChanged,
|
|
||||||
heads_database::{HeadControls, HeadsDatabase},
|
|
||||||
loading_assets::AudioAssets,
|
|
||||||
sounds::PlaySound,
|
|
||||||
};
|
};
|
||||||
use avian3d::prelude::*;
|
use avian3d::prelude::*;
|
||||||
use bevy::{
|
use bevy::{
|
||||||
@@ -18,7 +12,7 @@ use bevy::{
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Component, Default, Serialize, Deserialize, PartialEq)]
|
#[derive(Component, Default, Serialize, Deserialize, PartialEq)]
|
||||||
#[require(Character)]
|
#[require(HedzCharacter)]
|
||||||
pub struct Player;
|
pub struct Player;
|
||||||
|
|
||||||
#[derive(Component, Default, Serialize, Deserialize, PartialEq)]
|
#[derive(Component, Default, Serialize, Deserialize, PartialEq)]
|
||||||
@@ -36,8 +30,6 @@ pub fn plugin(app: &mut App) {
|
|||||||
)
|
)
|
||||||
.run_if(in_state(GameState::Playing)),
|
.run_if(in_state(GameState::Playing)),
|
||||||
);
|
);
|
||||||
|
|
||||||
global_observer!(app, on_update_head_mesh);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cursor_recenter(q_windows: Single<&mut Window, With<PrimaryWindow>>) {
|
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::{
|
use crate::{
|
||||||
GameState,
|
GameState,
|
||||||
abilities::BuildExplosionSprite,
|
abilities::{BuildExplosionSprite, healing::Healing},
|
||||||
animation::AnimationFlags,
|
animation::AnimationFlags,
|
||||||
backpack::{Backpack, backpack_ui::BackpackUiState},
|
backpack::{Backpack, backpack_ui::BackpackUiState},
|
||||||
camera::{CameraArmRotation, CameraTarget},
|
camera::{CameraArmRotation, CameraTarget},
|
||||||
@@ -16,16 +21,15 @@ use crate::{
|
|||||||
head::ActiveHead,
|
head::ActiveHead,
|
||||||
heads::{ActiveHeads, heads_ui::UiActiveHeads},
|
heads::{ActiveHeads, heads_ui::UiActiveHeads},
|
||||||
hitpoints::Hitpoints,
|
hitpoints::Hitpoints,
|
||||||
loading_assets::{GameAssets, HeadDropAssets},
|
|
||||||
platforms::ActivePlatform,
|
platforms::ActivePlatform,
|
||||||
player::{Player, PlayerBodyMesh},
|
player::{Player, PlayerBodyMesh},
|
||||||
|
protocol::channels::UnorderedReliableChannel,
|
||||||
utils::triggers::TriggerAppExt,
|
utils::triggers::TriggerAppExt,
|
||||||
};
|
};
|
||||||
use avian3d::prelude::{AngularVelocity, CollisionLayers, LinearVelocity};
|
use avian3d::prelude::{AngularVelocity, CollisionLayers, LinearVelocity};
|
||||||
use bevy::{
|
use bevy::prelude::*;
|
||||||
ecs::{component::HookContext, world::DeferredWorld},
|
pub use components::*;
|
||||||
prelude::*,
|
pub use events::*;
|
||||||
};
|
|
||||||
use happy_feet::{
|
use happy_feet::{
|
||||||
grounding::GroundingState,
|
grounding::GroundingState,
|
||||||
prelude::{
|
prelude::{
|
||||||
@@ -34,18 +38,19 @@ use happy_feet::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use lightyear::prelude::{
|
use lightyear::prelude::{
|
||||||
ActionsChannel, AppComponentExt, AppMessageExt, NetworkDirection, PredictionMode,
|
AppChannelExt, AppComponentExt, AppMessageExt, AppTriggerExt, ChannelMode, ChannelSettings,
|
||||||
PredictionRegistrationExt, input::native::InputPlugin,
|
NetworkDirection, PredictionMode, PredictionRegistrationExt, ReliableSettings,
|
||||||
|
input::native::InputPlugin,
|
||||||
};
|
};
|
||||||
use lightyear_serde::{
|
use lightyear_serde::{
|
||||||
SerializationError, reader::ReadInteger, registry::SerializeFns, writer::WriteInteger,
|
SerializationError, reader::ReadInteger, registry::SerializeFns, writer::WriteInteger,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use std::{collections::HashMap, time::Duration};
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
pub fn plugin(app: &mut App) {
|
pub fn plugin(app: &mut App) {
|
||||||
app.add_plugins(InputPlugin::<ControlState>::default());
|
app.add_plugins(InputPlugin::<ControlState>::default());
|
||||||
|
|
||||||
|
app.register_type::<PlayerId>();
|
||||||
app.register_type::<TbMapEntityId>();
|
app.register_type::<TbMapEntityId>();
|
||||||
app.register_type::<TbMapIdCounter>();
|
app.register_type::<TbMapIdCounter>();
|
||||||
app.register_type::<TbMapEntityMapping>();
|
app.register_type::<TbMapEntityMapping>();
|
||||||
@@ -53,9 +58,21 @@ pub fn plugin(app: &mut App) {
|
|||||||
app.init_resource::<TbMapIdCounter>();
|
app.init_resource::<TbMapIdCounter>();
|
||||||
app.init_resource::<TbMapEntityMapping>();
|
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);
|
.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::<ActiveHead>();
|
||||||
app.register_component::<ActiveHeads>();
|
app.register_component::<ActiveHeads>();
|
||||||
app.register_component::<ActivePlatform>();
|
app.register_component::<ActivePlatform>();
|
||||||
@@ -68,17 +85,17 @@ pub fn plugin(app: &mut App) {
|
|||||||
app.register_component::<CameraTarget>();
|
app.register_component::<CameraTarget>();
|
||||||
app.register_component::<CashResource>();
|
app.register_component::<CashResource>();
|
||||||
app.register_component::<happy_feet::prelude::Character>();
|
app.register_component::<happy_feet::prelude::Character>();
|
||||||
app.register_component::<character::Character>();
|
app.register_component::<character::HedzCharacter>();
|
||||||
app.register_component::<CharacterDrag>();
|
app.register_component::<CharacterDrag>();
|
||||||
app.register_component::<CharacterGravity>();
|
app.register_component::<CharacterGravity>();
|
||||||
app.register_component::<CharacterMovement>();
|
app.register_component::<CharacterMovement>();
|
||||||
app.register_component::<CollisionLayers>();
|
app.register_component::<CollisionLayers>();
|
||||||
app.register_component::<ControllerSettings>();
|
app.register_component::<ControllerSettings>();
|
||||||
app.register_component::<GltfSceneRoot>();
|
|
||||||
app.register_component::<GroundFriction>();
|
app.register_component::<GroundFriction>();
|
||||||
app.register_component::<Grounding>();
|
app.register_component::<Grounding>();
|
||||||
app.register_component::<GroundingConfig>();
|
app.register_component::<GroundingConfig>();
|
||||||
app.register_component::<GroundingState>();
|
app.register_component::<GroundingState>();
|
||||||
|
app.register_component::<Healing>();
|
||||||
app.register_component::<Hitpoints>();
|
app.register_component::<Hitpoints>();
|
||||||
app.register_component::<KinematicVelocity>();
|
app.register_component::<KinematicVelocity>();
|
||||||
app.register_component::<LinearVelocity>();
|
app.register_component::<LinearVelocity>();
|
||||||
@@ -89,7 +106,6 @@ pub fn plugin(app: &mut App) {
|
|||||||
app.register_component::<PlayerBodyMesh>();
|
app.register_component::<PlayerBodyMesh>();
|
||||||
app.register_component::<PlayerCharacterController>();
|
app.register_component::<PlayerCharacterController>();
|
||||||
app.register_component::<SteppingConfig>();
|
app.register_component::<SteppingConfig>();
|
||||||
app.register_component::<TbMapEntityId>();
|
|
||||||
app.register_component::<Transform>()
|
app.register_component::<Transform>()
|
||||||
.add_prediction(PredictionMode::Full)
|
.add_prediction(PredictionMode::Full)
|
||||||
.add_should_rollback(transform_should_rollback);
|
.add_should_rollback(transform_should_rollback);
|
||||||
@@ -108,52 +124,28 @@ pub fn plugin(app: &mut App) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
app.replicate_trigger::<BuildExplosionSprite, ActionsChannel>();
|
app.replicate_trigger::<BuildExplosionSprite, UnorderedReliableChannel>();
|
||||||
app.replicate_trigger::<StartCutscene, ActionsChannel>();
|
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(
|
app.add_systems(
|
||||||
OnEnter(GameState::MapLoading),
|
OnEnter(GameState::MapLoading),
|
||||||
|mut counter: ResMut<TbMapIdCounter>| counter.reset(),
|
|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 {
|
fn transform_should_rollback(this: &Transform, that: &Transform) -> bool {
|
||||||
this.translation.distance_squared(that.translation) >= 0.01f32.powf(2.)
|
this.translation.distance_squared(that.translation) >= 0.01f32.powf(2.)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trenchbroom map entities (spawned during map loading) must be despawned manually if the server
|
/// A global allocator for `TbMapEntityId` values. Should be reset when a map begins loading.
|
||||||
/// 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.
|
|
||||||
#[derive(Resource, Reflect, Default)]
|
#[derive(Resource, Reflect, Default)]
|
||||||
#[reflect(Resource)]
|
#[reflect(Resource)]
|
||||||
pub struct TbMapIdCounter(u64);
|
pub struct TbMapIdCounter(u64);
|
||||||
@@ -176,52 +168,3 @@ impl TbMapIdCounter {
|
|||||||
#[derive(Resource, Reflect, Default, Deref, DerefMut)]
|
#[derive(Resource, Reflect, Default, Deref, DerefMut)]
|
||||||
#[reflect(Resource)]
|
#[reflect(Resource)]
|
||||||
pub struct TbMapEntityMapping(pub HashMap<u64, Entity>);
|
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::{
|
use crate::{
|
||||||
cash::Cash, loading_assets::GameAssets, physics_layers::GameLayer, protocol::TbMapIdCounter,
|
cash::Cash, loading_assets::GameAssets, physics_layers::GameLayer, protocol::TbMapIdCounter,
|
||||||
|
utils::global_observer,
|
||||||
};
|
};
|
||||||
use avian3d::prelude::*;
|
use avian3d::prelude::*;
|
||||||
use bevy::{
|
use bevy::{
|
||||||
@@ -206,10 +207,10 @@ pub fn plugin(app: &mut App) {
|
|||||||
app.register_type::<CashSpawn>();
|
app.register_type::<CashSpawn>();
|
||||||
app.register_type::<SecretHead>();
|
app.register_type::<SecretHead>();
|
||||||
|
|
||||||
app.add_observer(tb_component_setup::<CashSpawn>);
|
global_observer!(app, tb_component_setup::<CashSpawn>);
|
||||||
app.add_observer(tb_component_setup::<Platform>);
|
global_observer!(app, tb_component_setup::<Platform>);
|
||||||
app.add_observer(tb_component_setup::<PlatformTarget>);
|
global_observer!(app, tb_component_setup::<PlatformTarget>);
|
||||||
app.add_observer(tb_component_setup::<Movable>);
|
global_observer!(app, tb_component_setup::<Movable>);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tb_component_setup<C: Component>(trigger: Trigger<OnAdd, C>, world: &mut World) {
|
fn tb_component_setup<C: Component>(trigger: Trigger<OnAdd, C>, world: &mut World) {
|
||||||
|
|||||||
@@ -2,11 +2,26 @@ use bevy::prelude::*;
|
|||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! global_observer {
|
macro_rules! global_observer {
|
||||||
($app:expr,$system:expr) => {{
|
($app:expr, $($system:tt)*) => {{
|
||||||
$app.world_mut()
|
$app.world_mut()
|
||||||
.add_observer($system)
|
.add_observer($($system)*)
|
||||||
.insert(Name::new(stringify!($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;
|
pub use global_observer;
|
||||||
@@ -30,6 +45,6 @@ fn global_observers(
|
|||||||
};
|
};
|
||||||
|
|
||||||
for o in query.iter() {
|
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 bevy::{ecs::system::SystemParam, prelude::*};
|
||||||
use lightyear::prelude::{AppTriggerExt, Channel, NetworkDirection, RemoteTrigger, TriggerSender};
|
use lightyear::prelude::{AppTriggerExt, Channel, NetworkDirection, RemoteTrigger, TriggerSender};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -31,8 +32,8 @@ impl TriggerAppExt for App {
|
|||||||
) {
|
) {
|
||||||
self.add_trigger::<M>()
|
self.add_trigger::<M>()
|
||||||
.add_direction(NetworkDirection::ServerToClient);
|
.add_direction(NetworkDirection::ServerToClient);
|
||||||
self.add_observer(replicate_trigger_to_clients::<M, C>);
|
global_observer!(self, replicate_trigger_to_clients::<M, C>);
|
||||||
self.add_observer(remote_to_local_trigger::<M>);
|
global_observer!(self, remote_to_local_trigger::<M>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
6
justfile
6
justfile
@@ -12,6 +12,9 @@ run *args:
|
|||||||
cargo b {{server_args}}
|
cargo b {{server_args}}
|
||||||
RUST_BACKTRACE=1 cargo r {{client_args}} -- {{args}}
|
RUST_BACKTRACE=1 cargo r {{client_args}} -- {{args}}
|
||||||
|
|
||||||
|
client *args:
|
||||||
|
RUST_BACKTRACE=1 cargo r {{client_args}} -- {{args}}
|
||||||
|
|
||||||
server:
|
server:
|
||||||
RUST_BACKTRACE=1 cargo r {{server_args}}
|
RUST_BACKTRACE=1 cargo r {{server_args}}
|
||||||
|
|
||||||
@@ -19,6 +22,9 @@ dbg *args:
|
|||||||
cargo b {{server_args}},dbg
|
cargo b {{server_args}},dbg
|
||||||
RUST_BACKTRACE=1 cargo r {{client_args}},dbg -- {{args}}
|
RUST_BACKTRACE=1 cargo r {{client_args}},dbg -- {{args}}
|
||||||
|
|
||||||
|
dbg-client *args:
|
||||||
|
RUST_BACKTRACE=1 cargo r {{client_args}},dbg -- {{args}}
|
||||||
|
|
||||||
dbg-server:
|
dbg-server:
|
||||||
RUST_BACKTRACE=1 cargo r {{server_args}},dbg
|
RUST_BACKTRACE=1 cargo r {{server_args}},dbg
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user