Replicate Sounds (#68)

This commit is contained in:
PROMETHIA-27
2025-09-29 14:46:38 -04:00
committed by GitHub
parent a07dfb3840
commit a16ee231cc
47 changed files with 992 additions and 721 deletions

View File

@@ -17,7 +17,6 @@ bevy = { version = "0.16.0", default-features = false, features = [
"animation",
"async_executor",
"bevy_asset",
"bevy_audio",
"bevy_color",
"bevy_core_pipeline",
"bevy_gilrs",

View File

@@ -11,6 +11,7 @@ dbg = ["avian3d/debug-plugin", "dep:bevy-inspector-egui", "shared/dbg"]
[dependencies]
avian3d = { workspace = true }
bevy = { workspace = true, default-features = false, features = [
"bevy_audio",
"bevy_window",
"bevy_winit",
] }

View 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
};
}
}

View File

@@ -0,0 +1,7 @@
pub mod backpack_ui;
use bevy::prelude::*;
pub fn plugin(app: &mut App) {
app.add_plugins(backpack_ui::plugin);
}

View File

@@ -21,11 +21,15 @@ use shared::{
control::ControlState,
global_observer,
player::Player,
protocol::{DespawnTbMapEntity, TbMapEntityId, TbMapEntityMapping},
protocol::{
ClientEnteredPlaying, TbMapEntityId, TbMapEntityMapping,
channels::UnorderedReliableChannel, messages::DespawnTbMapEntity,
},
tb_entities::{Platform, PlatformTarget},
};
use std::{
env::current_exe,
fs::File,
io::{BufRead, BufReader},
net::{IpAddr, Ipv4Addr, SocketAddr},
process::Stdio,
@@ -43,7 +47,7 @@ pub fn plugin(app: &mut App) {
parse_local_server_stdout.run_if(resource_exists::<LocalServerStdout>),
);
app.add_systems(Last, close_server_processes);
app.add_systems(FixedUpdate, despawn_absent_map_entities);
app.add_systems(Update, despawn_absent_map_entities);
global_observer!(app, on_connecting);
global_observer!(app, on_connection_failed);
@@ -124,9 +128,11 @@ fn on_connection_succeeded(
_trigger: Trigger<OnAdd, Connected>,
state: Res<State<GameState>>,
mut change_state: ResMut<NextState<GameState>>,
mut sender: Single<&mut TriggerSender<ClientEnteredPlaying>>,
) {
if *state == GameState::Connecting {
change_state.set(GameState::Playing);
sender.trigger::<UnorderedReliableChannel>(ClientEnteredPlaying);
}
}
@@ -148,7 +154,7 @@ fn on_connection_failed(
mut commands: Commands,
client_active: Query<&ClientActive>,
mut opened_server: Local<bool>,
) {
) -> Result {
let disconnected = disconnected.get(trigger.target()).unwrap();
if *opened_server {
panic!(
@@ -164,11 +170,13 @@ fn on_connection_failed(
// the server executable is assumed to be adjacent to the client executable
let mut exe_path = current_exe().expect("failed to get path of client executable");
exe_path.set_file_name("server");
let server_log_file = File::create("server.log")?;
let mut server_process = std::process::Command::new(exe_path)
.args(["--timeout", "60", "--close-on-client-disconnect"])
.env("NO_COLOR", "1")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.stderr(server_log_file)
.spawn()
.expect("failed to start server");
let server_stdout = server_process.stdout.take().unwrap();
@@ -195,6 +203,8 @@ fn on_connection_failed(
*opened_server = true;
}
Ok(())
}
#[derive(Event)]
@@ -206,6 +216,8 @@ fn parse_local_server_stdout(mut commands: Commands, mut stdout: ResMut<LocalSer
while let Ok(line) = stdout.0.get().try_recv() {
if let "hedz.server_started" = &line[..] {
commands.trigger(LocalServerStarted);
} else {
info!("SERVER: {line}");
}
}
}

View File

@@ -3,6 +3,19 @@ use crate::{
};
use bevy::prelude::*;
use rand::{Rng, thread_rng};
use shared::{loading_assets::AudioAssets, utils::observers::global_observer};
// Should not be a relationship because lightyear will silently track state for all relationships
// and break if one end of the relationship isn't replicated and is despawned
#[derive(Component)]
struct HasHealingEffects {
effects: Entity,
}
#[derive(Component)]
struct HealingEffectsOf {
of: Entity,
}
#[derive(Component, Default)]
#[require(Transform, InheritedVisibility)]
@@ -25,20 +38,52 @@ pub fn plugin(app: &mut App) {
Update,
(on_added, update_effects, update_particles).run_if(in_state(GameState::Playing)),
);
global_observer!(app, on_removed);
}
fn on_added(mut cmds: Commands, query: Query<&Healing, Added<Healing>>) {
for healing in query.iter() {
cmds.entity(healing.0).insert((
fn on_added(
mut commands: Commands,
query: Query<Entity, Added<Healing>>,
assets: Res<AudioAssets>,
) {
for entity in query.iter() {
let effects = commands
.spawn((
Name::new("heal-particle-effect"),
HealParticleEffect::default(),
));
AudioPlayer::new(assets.healing.clone()),
PlaybackSettings {
mode: bevy::audio::PlaybackMode::Loop,
..Default::default()
},
HealingEffectsOf { of: entity },
))
.id();
commands
.entity(entity)
.insert(HasHealingEffects { effects });
}
}
fn on_removed(
trigger: Trigger<OnRemove, Healing>,
mut commands: Commands,
effects: Query<&HasHealingEffects>,
) {
let Ok(has_effects) = effects.get(trigger.target()) else {
return;
};
commands.entity(has_effects.effects).try_despawn();
commands
.entity(trigger.target())
.remove::<HasHealingEffects>();
}
fn update_effects(
mut cmds: Commands,
mut query: Query<(&mut HealParticleEffect, Entity)>,
mut commands: Commands,
mut query: Query<(&mut HealParticleEffect, &HealingEffectsOf, Entity)>,
mut transforms: Query<&mut Transform>,
time: Res<Time>,
assets: Res<GameAssets>,
) {
@@ -46,7 +91,14 @@ fn update_effects(
let mut rng = thread_rng();
let now = time.elapsed_secs();
for (mut effect, e) in query.iter_mut() {
for (mut effect, effects_of, e) in query.iter_mut() {
// We have to manually track the healer's position because lightyear will try to synchronize
// children and there's no reason to synchronize the particle effect entity when we're already
// synchronizing `Healing`
// (trying to ignore/avoid it by excluding the child from replication just causes crashes)
let healer_pos = transforms.get(effects_of.of).unwrap().translation;
transforms.get_mut(e).unwrap().translation = healer_pos;
if effect.next_spawn < now {
let start_pos = Vec3::new(
rng.gen_range(-DISTANCE..DISTANCE),
@@ -59,7 +111,7 @@ fn update_effects(
let start_scale = rng.gen_range(0.7..1.0);
let end_scale = rng.gen_range(0.1..start_scale);
cmds.entity(e).with_child((
commands.entity(e).with_child((
Name::new("heal-particle"),
SceneRoot(assets.mesh_heal_particle.clone()),
Billboard::All,

View File

@@ -1,10 +1,13 @@
mod backpack;
mod client;
mod debug;
mod enemy;
mod heal_effect;
mod player;
mod sounds;
mod steam;
mod ui;
use crate::utils::{auto_rotate, explosions};
use avian3d::prelude::*;
use bevy::{
audio::{PlaybackMode, Volume},
@@ -22,7 +25,6 @@ use lightyear::prelude::client::ClientPlugins;
use loading_assets::AudioAssets;
use shared::*;
use std::time::Duration;
use utils::{billboards, sprite_3d_animation, squish_animation, trail};
fn main() {
let mut app = App::new();
@@ -87,43 +89,46 @@ fn main() {
// });
}
app.add_plugins(ai::plugin);
app.add_plugins(animation::plugin);
app.add_plugins(character::plugin);
app.add_plugins(cash::plugin);
app.add_plugins(enemy::plugin);
app.add_plugins(player::plugin);
app.add_plugins(gates::plugin);
app.add_plugins(platforms::plugin);
app.add_plugins(protocol::plugin);
app.add_plugins(movables::plugin);
app.add_plugins(billboards::plugin);
app.add_plugins(aim::plugin);
app.add_plugins(client::plugin);
app.add_plugins(npc::plugin);
app.add_plugins(keys::plugin);
app.add_plugins(squish_animation::plugin);
app.add_plugins(cutscene::plugin);
app.add_plugins(control::plugin);
app.add_plugins(sounds::plugin);
app.add_plugins(camera::plugin);
app.add_plugins(shared::ai::plugin);
app.add_plugins(shared::animation::plugin);
app.add_plugins(shared::character::plugin);
app.add_plugins(shared::cash::plugin);
app.add_plugins(shared::player::plugin);
app.add_plugins(shared::gates::plugin);
app.add_plugins(shared::platforms::plugin);
app.add_plugins(shared::protocol::plugin);
app.add_plugins(shared::movables::plugin);
app.add_plugins(shared::utils::billboards::plugin);
app.add_plugins(shared::aim::plugin);
app.add_plugins(shared::npc::plugin);
app.add_plugins(shared::keys::plugin);
app.add_plugins(shared::utils::squish_animation::plugin);
app.add_plugins(shared::cutscene::plugin);
app.add_plugins(shared::control::plugin);
app.add_plugins(shared::camera::plugin);
app.add_plugins(shared::backpack::plugin);
app.add_plugins(shared::loading_assets::LoadingPlugin);
app.add_plugins(shared::loading_map::plugin);
app.add_plugins(shared::utils::sprite_3d_animation::plugin);
app.add_plugins(shared::abilities::plugin);
app.add_plugins(shared::heads::plugin);
app.add_plugins(shared::hitpoints::plugin);
app.add_plugins(shared::cash_heal::plugin);
app.add_plugins(shared::utils::plugin);
app.add_plugins(shared::water::plugin);
app.add_plugins(shared::head_drop::plugin);
app.add_plugins(shared::utils::trail::plugin);
app.add_plugins(shared::utils::auto_rotate::plugin);
app.add_plugins(shared::tb_entities::plugin);
app.add_plugins(shared::utils::explosions::plugin);
app.add_plugins(backpack::plugin);
app.add_plugins(loading_assets::LoadingPlugin);
app.add_plugins(loading_map::plugin);
app.add_plugins(sprite_3d_animation::plugin);
app.add_plugins(abilities::plugin);
app.add_plugins(heads::plugin);
app.add_plugins(hitpoints::plugin);
app.add_plugins(cash_heal::plugin);
app.add_plugins(client::plugin);
app.add_plugins(debug::plugin);
app.add_plugins(utils::plugin);
app.add_plugins(water::plugin);
app.add_plugins(head_drop::plugin);
app.add_plugins(trail::plugin);
app.add_plugins(auto_rotate::plugin);
app.add_plugins(enemy::plugin);
app.add_plugins(heal_effect::plugin);
app.add_plugins(tb_entities::plugin);
app.add_plugins(explosions::plugin);
app.add_plugins(player::plugin);
app.add_plugins(sounds::plugin);
app.add_plugins(ui::plugin);
app.init_state::<GameState>();

115
crates/client/src/player.rs Normal file
View 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(())
}

View File

@@ -1,28 +1,6 @@
use crate::{global_observer, loading_assets::AudioAssets};
use bevy::prelude::*;
#[derive(Event, Clone, Debug)]
pub enum PlaySound {
Hit,
KeyCollect,
Gun,
Throw,
ThrowHit,
Gate,
CashCollect,
HeadCollect,
SecretHeadCollect,
HeadDrop,
Selection,
Invalid,
MissileExplosion,
Reloaded,
CashHeal,
Crossbow,
Beaming,
Backpack { open: bool },
Head(String),
}
use shared::protocol::PlaySound;
pub fn plugin(app: &mut App) {
global_observer!(app, on_spawn_sounds);
@@ -31,7 +9,6 @@ pub fn plugin(app: &mut App) {
fn on_spawn_sounds(
trigger: Trigger<PlaySound>,
mut commands: Commands,
// sound_res: Res<AudioAssets>,
// settings: SettingsRead,
assets: Res<AudioAssets>,
) {

View 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;
}
}
}

View File

@@ -0,0 +1,7 @@
pub mod backpack_ui;
use bevy::prelude::*;
pub fn plugin(app: &mut App) {
app.add_plugins(backpack_ui::plugin);
}

View File

@@ -1,11 +1,5 @@
use avian3d::prelude::*;
use bevy::{
app::plugin_group,
audio::Volume,
core_pipeline::tonemapping::Tonemapping,
log::{BoxedLayer, tracing_subscriber::Layer},
prelude::*,
};
use bevy::{app::plugin_group, core_pipeline::tonemapping::Tonemapping, prelude::*};
use bevy_common_assets::ron::RonAssetPlugin;
use bevy_sprite3d::Sprite3dPlugin;
use bevy_trenchbroom::prelude::*;
@@ -14,10 +8,12 @@ use lightyear::prelude::server::ServerPlugins;
use shared::{DebugVisuals, GameState, heads_database::HeadDatabaseAsset};
use std::time::Duration;
mod backpack;
mod config;
mod player;
mod server;
mod tb_entities;
mod utils;
plugin_group! {
pub struct DefaultPlugins {
@@ -44,7 +40,6 @@ plugin_group! {
bevy::ui:::UiPlugin,
bevy::pbr:::PbrPlugin,
bevy::gltf:::GltfPlugin,
bevy::audio:::AudioPlugin,
bevy::gilrs:::GilrsPlugin,
bevy::animation:::AnimationPlugin,
bevy::gizmos:::GizmoPlugin,
@@ -54,23 +49,6 @@ plugin_group! {
}
}
pub fn log_to_file_layer(_app: &mut App) -> Option<BoxedLayer> {
let file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open("server.log")
.ok()?;
Some(
bevy::log::tracing_subscriber::fmt::layer()
.with_writer(std::sync::Mutex::new(file))
.with_ansi(false)
.with_file(true)
.with_line_number(true)
.boxed(),
)
}
fn main() {
let mut app = App::new();
@@ -88,7 +66,7 @@ fn main() {
filter: "info,lightyear_replication=off".into(),
level: bevy::log::Level::INFO,
// provide custom log layer to receive logging events
custom_layer: log_to_file_layer,
..default()
}));
app.add_plugins(ServerPlugins {
@@ -102,17 +80,6 @@ fn main() {
app.add_plugins(UiGradientsPlugin);
app.add_plugins(RonAssetPlugin::<HeadDatabaseAsset>::new(&["headsdb.ron"]));
#[cfg(feature = "dbg")]
{
app.add_plugins(PhysicsDebugPlugin::default());
// app.add_plugins(bevy::pbr::wireframe::WireframePlugin)
// .insert_resource(bevy::pbr::wireframe::WireframeConfig {
// global: true,
// default_color: bevy::color::palettes::css::WHITE.into(),
// });
}
app.add_plugins(shared::abilities::plugin);
app.add_plugins(shared::ai::plugin);
app.add_plugins(shared::aim::plugin);
@@ -127,7 +94,6 @@ fn main() {
app.add_plugins(shared::gates::plugin);
app.add_plugins(shared::head_drop::plugin);
app.add_plugins(shared::heads::plugin);
app.add_plugins(shared::heal_effect::plugin);
app.add_plugins(shared::hitpoints::plugin);
app.add_plugins(shared::keys::plugin);
app.add_plugins(shared::loading_assets::LoadingPlugin);
@@ -137,7 +103,6 @@ fn main() {
app.add_plugins(shared::platforms::plugin);
app.add_plugins(shared::player::plugin);
app.add_plugins(shared::protocol::plugin);
app.add_plugins(shared::sounds::plugin);
app.add_plugins(shared::steam::plugin);
app.add_plugins(shared::tb_entities::plugin);
app.add_plugins(shared::utils::auto_rotate::plugin);
@@ -149,20 +114,20 @@ fn main() {
app.add_plugins(shared::utils::plugin);
app.add_plugins(shared::water::plugin);
app.add_plugins(backpack::plugin);
app.add_plugins(config::plugin);
app.add_plugins(player::plugin);
app.add_plugins(server::plugin);
app.add_plugins(tb_entities::plugin);
app.add_plugins(utils::plugin);
app.init_state::<GameState>();
app.insert_resource(AmbientLight {
color: Color::WHITE,
brightness: 400.,
..Default::default()
});
app.insert_resource(ClearColor(Color::BLACK));
//TODO: let user control this
app.insert_resource(GlobalVolume::new(Volume::Linear(0.4)));
app.add_systems(PostStartup, setup_panic_handler);
app.run();
}
fn setup_panic_handler() {
_ = std::panic::take_hook();
}

View File

@@ -6,6 +6,7 @@ use shared::{
cash::CashResource,
character::AnimatedCharacter,
control::{ControlState, controller_common::PlayerCharacterController},
global_observer,
head::ActiveHead,
head_drop::HeadDrops,
heads::{ActiveHeads, HeadChanged, HeadState, heads_ui::UiActiveHeads},
@@ -13,19 +14,23 @@ use shared::{
hitpoints::{Hitpoints, Kill},
npc::SpawnCharacter,
player::{Player, PlayerBodyMesh},
protocol::{
PlaySound, PlayerId, channels::UnorderedReliableChannel, events::ClientHeadChanged,
},
tb_entities::SpawnPoint,
};
pub fn plugin(app: &mut App) {
global_observer!(app, on_update_head_mesh);
}
pub fn spawn(
mut commands: Commands,
owner: Entity,
query: Query<&Transform, With<SpawnPoint>>,
asset_server: Res<AssetServer>,
heads_db: Res<HeadsDatabase>,
) {
let Some(spawn) = query.iter().next() else {
return;
};
) -> Option<Entity> {
let spawn = query.iter().next()?;
let transform = Transform::from_translation(spawn.translation + Vec3::new(0., 3., 0.));
@@ -47,6 +52,7 @@ pub fn spawn(
transform,
Visibility::default(),
PlayerCharacterController,
PlayerId { id: 0 },
),
ActionState::<ControlState>::default(),
Backpack::default(),
@@ -67,12 +73,12 @@ pub fn spawn(
));
player.observe(on_kill);
commands.spawn((
AudioPlayer::new(asset_server.load("sfx/heads/angry demonstrator.ogg")),
PlaybackSettings::DESPAWN,
));
let id = player.id();
commands.trigger(PlaySound::Head("angry demonstrator".to_string()));
commands.trigger(SpawnCharacter(transform.translation));
Some(id)
}
fn on_kill(
@@ -92,3 +98,27 @@ fn on_kill(
commands.trigger(HeadChanged(new_head));
}
}
fn on_update_head_mesh(
trigger: Trigger<HeadChanged>,
mut commands: Commands,
mesh_children: Single<&Children, With<PlayerBodyMesh>>,
mut sender: Single<&mut TriggerSender<ClientHeadChanged>>,
animated_characters: Query<&AnimatedCharacter>,
mut player: Single<&mut ActiveHead, With<Player>>,
) -> Result {
let animated_char = mesh_children
.iter()
.find(|child| animated_characters.contains(*child))
.ok_or("tried to update head mesh before AnimatedCharacter was readded")?;
player.0 = trigger.0;
commands
.entity(animated_char)
.insert(AnimatedCharacter::new(trigger.0));
sender.trigger::<UnorderedReliableChannel>(ClientHeadChanged(trigger.0 as u64));
Ok(())
}

View File

@@ -1,42 +1,54 @@
use crate::config::ServerConfig;
use bevy::prelude::*;
use lightyear::{
connection::client::PeerMetadata,
link::LinkConditioner,
prelude::{
server::{NetcodeConfig, NetcodeServer, ServerUdpIo, Started},
server::{ClientOf, NetcodeConfig, NetcodeServer, ServerUdpIo, Started},
*,
},
};
use shared::{GameState, global_observer, heads_database::HeadsDatabase, tb_entities::SpawnPoint};
use shared::{
GameState, global_observer,
heads_database::HeadsDatabase,
protocol::{
ClientEnteredPlaying, channels::UnorderedReliableChannel, messages::AssignClientPlayer,
},
tb_entities::SpawnPoint,
};
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
time::Duration,
};
pub fn plugin(app: &mut App) {
app.add_systems(Startup, (start_server, setup_timeout_timer));
app.add_systems(
OnEnter(GameState::Playing),
(start_server, setup_timeout_timer),
);
app.add_systems(
Update,
(
notify_started.run_if(in_state(GameState::Playing)),
run_timeout,
),
(notify_started, run_timeout).run_if(in_state(GameState::Playing)),
);
global_observer!(app, handle_new_client);
global_observer!(app, on_client_connected);
global_observer!(app, on_client_playing);
global_observer!(app, close_on_disconnect);
global_observer!(app, cancel_timeout);
}
#[derive(Component)]
struct ClientPlayerId(u8);
fn handle_new_client(
trigger: Trigger<OnAdd, Connected>,
trigger: Trigger<OnAdd, Linked>,
mut commands: Commands,
id: Query<&PeerAddr>,
asset_server: Res<AssetServer>,
query: Query<&Transform, With<SpawnPoint>>,
heads_db: Res<HeadsDatabase>,
) -> Result {
let id = id.get(trigger.target())?;
let Ok(id) = id.get(trigger.target()) else {
return Ok(());
};
info!("Client connected on IP: {}", id.ip());
@@ -46,11 +58,38 @@ fn handle_new_client(
incoming_loss: 0.0,
});
commands
.entity(trigger.target())
.insert((ReplicationSender::default(), Link::new(Some(conditioner))));
commands.entity(trigger.target()).insert((
ReplicationSender::default(),
Link::new(Some(conditioner)),
ClientPlayerId(0),
));
crate::player::spawn(commands, trigger.target(), query, asset_server, heads_db);
Ok(())
}
fn on_client_connected(
trigger: Trigger<OnAdd, ClientOf>,
mut assign_player: Query<(&ClientPlayerId, &mut MessageSender<AssignClientPlayer>)>,
) -> Result {
// `Linked` happens before the `ClientOf` and `MessageSender` components are added, so the server can't
// send the client player id until now.
let (id, mut sender) = assign_player.get_mut(trigger.target())?;
sender.send::<UnorderedReliableChannel>(AssignClientPlayer(id.0));
Ok(())
}
fn on_client_playing(
trigger: Trigger<RemoteTrigger<ClientEnteredPlaying>>,
commands: Commands,
query: Query<&Transform, With<SpawnPoint>>,
heads_db: Res<HeadsDatabase>,
peers: Res<PeerMetadata>,
) -> Result {
let Some(&client) = peers.mapping.get(&trigger.from) else {
return Ok(());
};
crate::player::spawn(commands, client, query, heads_db).ok_or("failed to spawn player")?;
Ok(())
}
@@ -61,6 +100,7 @@ fn close_on_disconnect(
mut writer: EventWriter<AppExit>,
) {
if config.close_on_client_disconnect {
info!("client disconnected, exiting");
writer.write(AppExit::Success);
}
}
@@ -103,10 +143,12 @@ fn run_timeout(mut timer: ResMut<TimeoutTimer>, mut writer: EventWriter<AppExit>
timer.0 -= time.delta_secs();
if timer.0 <= 0.0 {
info!("client timed out, exiting");
writer.write(AppExit::Success);
}
}
fn cancel_timeout(_trigger: Trigger<OnAdd, Connected>, mut timer: ResMut<TimeoutTimer>) {
info!("client connected, cancelling timeout");
timer.0 = f32::INFINITY;
}

View File

@@ -1,8 +1,8 @@
use bevy::prelude::*;
use lightyear::prelude::{ActionsChannel, Connected, MessageSender};
use lightyear::prelude::{Connected, MessageSender};
use shared::{
GameState, global_observer,
protocol::{DespawnTbMapEntity, TbMapEntityId},
protocol::{TbMapEntityId, channels::UnorderedReliableChannel, messages::DespawnTbMapEntity},
};
pub fn plugin(app: &mut App) {
@@ -38,6 +38,6 @@ fn send_new_client_despawned_cache(
) {
let mut send = send.get_mut(trigger.target()).unwrap();
for &id in cache.0.iter() {
send.send::<ActionsChannel>(DespawnTbMapEntity(id));
send.send::<UnorderedReliableChannel>(DespawnTbMapEntity(id));
}
}

View 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());
}

View File

@@ -1,7 +1,7 @@
use super::TriggerArrow;
use crate::{
GameState, billboards::Billboard, global_observer, heads_database::HeadsDatabase,
hitpoints::Hit, loading_assets::GameAssets, physics_layers::GameLayer, sounds::PlaySound,
hitpoints::Hit, loading_assets::GameAssets, physics_layers::GameLayer, protocol::PlaySound,
utils::sprite_3d_animation::AnimationTimer,
};
use avian3d::prelude::*;

View File

@@ -1,7 +1,7 @@
use super::TriggerGun;
use crate::{
GameState, billboards::Billboard, global_observer, heads_database::HeadsDatabase,
hitpoints::Hit, loading_assets::GameAssets, physics_layers::GameLayer, sounds::PlaySound,
hitpoints::Hit, loading_assets::GameAssets, physics_layers::GameLayer, protocol::PlaySound,
tb_entities::EnemySpawn, utils::sprite_3d_animation::AnimationTimer,
};
use avian3d::prelude::*;

View File

@@ -1,13 +1,14 @@
use crate::{
GameState, global_observer, heads::ActiveHeads, heads_database::HeadsDatabase,
hitpoints::Hitpoints, loading_assets::AudioAssets,
hitpoints::Hitpoints,
};
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Component)]
pub struct Healing(pub Entity);
#[derive(Component, Serialize, Deserialize, PartialEq)]
pub struct Healing;
#[derive(Event, Debug)]
#[derive(Clone, Event, Debug, Serialize, Deserialize)]
pub enum HealingStateChanged {
Started,
Stopped,
@@ -22,27 +23,21 @@ pub fn plugin(app: &mut App) {
fn on_heal_start_stop(
trigger: Trigger<HealingStateChanged>,
mut cmds: Commands,
assets: Res<AudioAssets>,
query: Query<&Healing>,
) {
if matches!(trigger.event(), HealingStateChanged::Started) {
let e = cmds
.spawn((
Name::new("sfx-heal"),
AudioPlayer::new(assets.healing.clone()),
PlaybackSettings {
mode: bevy::audio::PlaybackMode::Loop,
..Default::default()
},
))
.id();
cmds.entity(trigger.target())
.add_child(e)
.insert(Healing(e));
} else {
if let Ok(healing) = query.single() {
cmds.entity(healing.0).despawn();
if query.contains(trigger.target()) {
// already healing, just ignore
return;
}
cmds.entity(trigger.target()).insert(Healing);
} else {
if !query.contains(trigger.target()) {
// Not healing, just ignore
return;
}
cmds.entity(trigger.target()).remove::<Healing>();
}
}

View File

@@ -4,8 +4,7 @@ use crate::{
abilities::BuildExplosionSprite,
heads_database::HeadsDatabase,
physics_layers::GameLayer,
protocol::GltfSceneRoot,
sounds::PlaySound,
protocol::{GltfSceneRoot, PlaySound},
utils::{commands::CommandExt, explosions::Explosion, global_observer, trail::Trail},
};
use avian3d::prelude::*;

View File

@@ -9,25 +9,23 @@ use crate::{
GameState,
aim::AimTarget,
character::CharacterHierarchy,
control::ControlState,
global_observer,
head::ActiveHead,
heads::ActiveHeads,
heads_database::HeadsDatabase,
loading_assets::GameAssets,
physics_layers::GameLayer,
player::{Player, PlayerBodyMesh},
sounds::PlaySound,
protocol::PlaySound,
utils::{billboards::Billboard, sprite_3d_animation::AnimationTimer},
};
#[cfg(feature = "server")]
use crate::{control::ControlState, head::ActiveHead};
use bevy::{pbr::NotShadowCaster, prelude::*};
use bevy_sprite3d::{Sprite3dBuilder, Sprite3dParams};
pub use healing::Healing;
use healing::HealingStateChanged;
use lightyear::{
connection::client::ClientState,
prelude::{Client, input::native::ActionState},
};
#[cfg(feature = "server")]
use lightyear::prelude::input::native::ActionState;
use serde::{Deserialize, Serialize};
#[derive(Debug, Copy, Clone, PartialEq, Reflect, Default, Serialize, Deserialize)]
@@ -113,6 +111,7 @@ pub fn plugin(app: &mut App) {
Update,
(update, update_heal_ability).run_if(in_state(GameState::Playing)),
);
#[cfg(feature = "server")]
app.add_systems(
FixedUpdate,
on_trigger_state.run_if(in_state(GameState::Playing)),
@@ -121,19 +120,13 @@ pub fn plugin(app: &mut App) {
global_observer!(app, build_explosion_sprite);
}
#[cfg(feature = "server")]
fn on_trigger_state(
mut res: ResMut<TriggerStateRes>,
player: Query<(&ActiveHead, &ActionState<ControlState>), With<Player>>,
headdb: Res<HeadsDatabase>,
time: Res<Time>,
client: Query<&Client>,
) {
if let Ok(client) = client.single()
&& (client.state == ClientState::Connected && cfg!(not(feature = "server")))
{
return;
}
for (player_head, controls) in player.iter() {
res.active = controls.trigger;
if controls.just_triggered {

View File

@@ -4,8 +4,7 @@ use crate::{
abilities::BuildExplosionSprite,
heads_database::HeadsDatabase,
physics_layers::GameLayer,
protocol::GltfSceneRoot,
sounds::PlaySound,
protocol::{GltfSceneRoot, PlaySound},
utils::{
auto_rotate::AutoRotation, commands::CommandExt, explosions::Explosion, global_observer,
},

View File

@@ -1,352 +1,40 @@
#[cfg(feature = "server")]
use super::Backpack;
use super::UiHeadState;
use crate::{
GameState, HEDZ_GREEN, loading_assets::UIAssets, protocol::PlayBackpackSound, sounds::PlaySound,
};
#[cfg(feature = "server")]
use crate::{backpack::BackbackSwapEvent, control::ControlState};
#[cfg(feature = "client")]
use crate::{global_observer, heads::HeadsImages};
use bevy::{ecs::spawn::SpawnIter, prelude::*};
#[cfg(feature = "server")]
use lightyear::prelude::{ActionsChannel, TriggerSender, input::native::ActionState};
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
static HEAD_SLOTS: usize = 5;
pub static HEAD_SLOTS: usize = 5;
#[derive(Component, Default)]
struct BackpackMarker;
pub struct BackpackMarker;
#[derive(Component, Default)]
struct BackpackCountText;
pub struct BackpackCountText;
#[allow(unused)]
#[derive(Component, Default)]
struct HeadSelector(pub usize);
pub struct HeadSelector(pub usize);
#[allow(unused)]
#[derive(Component, Default)]
struct HeadImage(pub usize);
pub struct HeadImage(pub usize);
#[allow(unused)]
#[derive(Component, Default)]
struct HeadDamage(pub usize);
pub struct HeadDamage(pub usize);
#[derive(Component, Default, Debug, Reflect, Serialize, Deserialize, PartialEq)]
#[reflect(Component, Default)]
pub struct BackpackUiState {
heads: [Option<UiHeadState>; 5],
scroll: usize,
count: usize,
current_slot: usize,
open: bool,
pub heads: [Option<UiHeadState>; 5],
pub scroll: usize,
pub count: usize,
pub current_slot: usize,
pub open: bool,
}
#[cfg(feature = "client")]
impl BackpackUiState {
fn relative_current_slot(&self) -> usize {
pub fn relative_current_slot(&self) -> usize {
self.current_slot.saturating_sub(self.scroll)
}
}
pub fn plugin(app: &mut App) {
app.register_type::<BackpackUiState>();
app.add_systems(OnEnter(GameState::Playing), setup);
#[cfg(feature = "server")]
app.add_systems(
FixedUpdate,
sync_on_change.run_if(in_state(GameState::Playing)),
);
#[cfg(feature = "client")]
app.add_systems(
FixedUpdate,
(update, update_visibility, update_count).run_if(in_state(GameState::Playing)),
);
#[cfg(feature = "server")]
app.add_systems(FixedUpdate, swap_head_inputs);
#[cfg(feature = "client")]
global_observer!(app, play_backpack_sound);
}
fn setup(mut commands: Commands, assets: Res<UIAssets>) {
commands.spawn((
Name::new("backpack-ui"),
BackpackMarker,
Visibility::Hidden,
Node {
position_type: PositionType::Absolute,
top: Val::Px(20.0),
right: Val::Px(20.0),
height: Val::Px(74.0),
..default()
},
Children::spawn(SpawnIter((0..HEAD_SLOTS).map({
let bg = assets.head_bg.clone();
let regular = assets.head_regular.clone();
let selector = assets.head_selector.clone();
let damage = assets.head_damage.clone();
move |i| {
spawn_head_ui(
bg.clone(),
regular.clone(),
selector.clone(),
damage.clone(),
i,
)
}
}))),
));
commands.spawn((
Name::new("backpack-head-count-ui"),
Text::new("0"),
TextShadow::default(),
BackpackCountText,
TextFont {
font: assets.font.clone(),
font_size: 34.0,
..default()
},
TextColor(HEDZ_GREEN.into()),
TextLayout::new_with_justify(JustifyText::Center),
Node {
position_type: PositionType::Absolute,
top: Val::Px(20.0),
right: Val::Px(20.0),
..default()
},
));
}
fn spawn_head_ui(
bg: Handle<Image>,
regular: Handle<Image>,
selector: Handle<Image>,
damage: Handle<Image>,
head_slot: usize,
) -> impl Bundle {
const SIZE: f32 = 90.0;
const DAMAGE_SIZE: f32 = 74.0;
(
Node {
position_type: PositionType::Relative,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
width: Val::Px(SIZE),
..default()
},
children![
(
Name::new("selector"),
Node {
position_type: PositionType::Absolute,
bottom: Val::Px(-30.0),
..default()
},
Visibility::Hidden,
ImageNode::new(selector).with_flip_y(),
HeadSelector(head_slot),
),
(
Name::new("bg"),
Node {
position_type: PositionType::Absolute,
..default()
},
ImageNode::new(bg),
),
(
Name::new("head"),
Node {
position_type: PositionType::Absolute,
..default()
},
ImageNode::default(),
Visibility::Hidden,
HeadImage(head_slot),
),
(
Name::new("rings"),
Node {
position_type: PositionType::Absolute,
..default()
},
ImageNode::new(regular),
),
(
Name::new("health"),
Node {
height: Val::Px(DAMAGE_SIZE),
width: Val::Px(DAMAGE_SIZE),
..default()
},
children![(
Name::new("damage_ring"),
HeadDamage(head_slot),
Node {
position_type: PositionType::Absolute,
display: Display::Block,
overflow: Overflow::clip(),
top: Val::Px(0.),
left: Val::Px(0.),
right: Val::Px(0.),
height: Val::Percent(0.),
..default()
},
children![ImageNode::new(damage)]
)]
)
],
)
}
#[cfg(feature = "client")]
fn update_visibility(
state: Single<&BackpackUiState, Changed<BackpackUiState>>,
mut backpack: Single<&mut Visibility, (With<BackpackMarker>, Without<BackpackCountText>)>,
mut count: Single<&mut Visibility, (Without<BackpackMarker>, With<BackpackCountText>)>,
) {
**backpack = if state.open {
Visibility::Visible
} else {
Visibility::Hidden
};
**count = if !state.open {
Visibility::Visible
} else {
Visibility::Hidden
};
}
#[cfg(feature = "client")]
fn update_count(
state: Single<&BackpackUiState, Changed<BackpackUiState>>,
text: Option<Single<Entity, With<BackpackCountText>>>,
mut writer: TextUiWriter,
) {
let Some(text) = text else {
return;
};
*writer.text(*text, 0) = state.count.to_string();
}
#[cfg(feature = "client")]
fn update(
state: Single<&BackpackUiState, Changed<BackpackUiState>>,
heads_images: Res<HeadsImages>,
mut head_image: Query<(&HeadImage, &mut Visibility, &mut ImageNode), Without<HeadSelector>>,
mut head_damage: Query<(&HeadDamage, &mut Node), Without<HeadSelector>>,
mut head_selector: Query<(&HeadSelector, &mut Visibility), Without<HeadImage>>,
) {
for (HeadImage(head), mut vis, mut image) in head_image.iter_mut() {
if let Some(head) = &state.heads[*head] {
*vis = Visibility::Inherited;
image.image = heads_images.heads[head.head].clone();
} else {
*vis = Visibility::Hidden;
}
}
for (HeadDamage(head), mut node) in head_damage.iter_mut() {
if let Some(head) = &state.heads[*head] {
node.height = Val::Percent(head.damage() * 100.0);
}
}
for (HeadSelector(head), mut vis) in head_selector.iter_mut() {
*vis = if *head == state.relative_current_slot() {
Visibility::Inherited
} else {
Visibility::Hidden
};
}
}
#[cfg(feature = "server")]
fn swap_head_inputs(
player: Query<(&ActionState<ControlState>, Ref<Backpack>)>,
mut trigger: Single<&mut TriggerSender<PlayBackpackSound>>,
mut commands: Commands,
mut state: Single<&mut BackpackUiState>,
time: Res<Time>,
) {
for (controls, backpack) in player.iter() {
if state.count == 0 {
return;
}
if controls.backpack_toggle {
state.open = !state.open;
trigger.trigger::<ActionsChannel>(PlayBackpackSound { open: state.open });
}
if !state.open {
return;
}
let mut changed = false;
if controls.backpack_left && state.current_slot > 0 {
state.current_slot -= 1;
changed = true;
}
if controls.backpack_right && state.current_slot < state.count.saturating_sub(1) {
state.current_slot += 1;
changed = true;
}
if controls.backpack_swap {
commands.trigger(BackbackSwapEvent(state.current_slot));
}
if changed {
commands.trigger(PlaySound::Selection);
sync(&backpack, &mut state, time.elapsed_secs());
}
}
}
#[cfg(feature = "client")]
fn play_backpack_sound(trigger: Trigger<PlayBackpackSound>, mut commands: Commands) {
commands.trigger(PlaySound::Backpack {
open: trigger.event().open,
});
}
#[cfg(feature = "server")]
fn sync_on_change(
backpack: Query<Ref<Backpack>>,
mut state: Single<&mut BackpackUiState>,
time: Res<Time>,
) {
for backpack in backpack.iter() {
if backpack.is_changed() || backpack.reloading() {
sync(&backpack, &mut state, time.elapsed_secs());
}
}
}
#[cfg(feature = "server")]
fn sync(backpack: &Backpack, state: &mut Single<&mut BackpackUiState>, time: f32) {
state.count = backpack.heads.len();
state.scroll = state.scroll.min(state.count.saturating_sub(HEAD_SLOTS));
if state.current_slot >= state.scroll + HEAD_SLOTS {
state.scroll = state.current_slot.saturating_sub(HEAD_SLOTS - 1);
}
if state.current_slot < state.scroll {
state.scroll = state.current_slot;
}
for i in 0..HEAD_SLOTS {
if let Some(head) = backpack.heads.get(i + state.scroll) {
state.heads[i] = Some(UiHeadState::new(*head, time));
} else {
state.heads[i] = None;
}
}
}

View File

@@ -1,4 +1,3 @@
#[cfg(feature = "server")]
use crate::heads::HeadState;
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
@@ -24,8 +23,7 @@ impl UiHeadState {
self.reloading
}
#[cfg(feature = "server")]
pub(crate) fn new(value: HeadState, time: f32) -> Self {
pub fn new(value: HeadState, time: f32) -> Self {
let reloading = if value.has_ammo() {
None
} else {

View File

@@ -1,6 +1,6 @@
use crate::{GameState, HEDZ_GREEN, loading_assets::UIAssets};
#[cfg(feature = "server")]
use crate::{global_observer, sounds::PlaySound};
use crate::{global_observer, protocol::PlaySound};
use bevy::prelude::*;
use serde::{Deserialize, Serialize};

View File

@@ -1,6 +1,6 @@
use crate::{
cash::CashResource, control::ControlState, hitpoints::Hitpoints, player::Player,
sounds::PlaySound,
protocol::PlaySound,
};
use bevy::prelude::*;
use lightyear::prelude::input::native::ActionState;

View File

@@ -9,6 +9,7 @@ use bevy::{
animation::RepeatAnimation, ecs::system::SystemParam, platform::collections::HashMap,
prelude::*, scene::SceneInstanceReady,
};
use lightyear::prelude::DisableReplicateHierarchy;
use serde::{Deserialize, Serialize};
use std::{f32::consts::PI, time::Duration};
@@ -16,6 +17,7 @@ use std::{f32::consts::PI, time::Duration};
pub struct ProjectileOrigin;
#[derive(Component, Debug, Serialize, Deserialize, PartialEq)]
#[require(Visibility, GlobalTransform)]
pub struct AnimatedCharacter {
head: usize,
}
@@ -107,14 +109,19 @@ fn spawn(
transform.rotate_y(PI);
commands.entity(entity).despawn_related::<Children>();
commands
.entity(entity)
.insert((
transform,
.spawn((
SceneRoot(asset.scenes[0].clone()),
AnimatedCharacterAsset(handle.clone()),
DisableReplicateHierarchy,
ChildOf(entity),
))
.observe(find_marker_bones);
commands
.entity(entity)
.insert((transform, AnimatedCharacterAsset(handle.clone())));
}
}
@@ -162,14 +169,14 @@ fn find_marker_bones(
#[derive(Component, Default, Reflect, PartialEq, Serialize, Deserialize)]
#[reflect(Component)]
pub struct Character;
pub struct HedzCharacter;
fn setup_once_loaded(
mut commands: Commands,
mut query: Query<(Entity, &mut AnimationPlayer), Added<AnimationPlayer>>,
parent: Query<&ChildOf>,
animated_character: Query<(&AnimatedCharacter, &AnimatedCharacterAsset)>,
characters: Query<Entity, With<Character>>,
characters: Query<Entity, With<HedzCharacter>>,
gltf_assets: Res<Assets<Gltf>>,
mut graphs: ResMut<Assets<AnimationGraph>>,
) {

View File

@@ -55,14 +55,12 @@ fn rotate_rig(
}
fn apply_controls(
mut character: Query<(&mut MoveInput, &MovementSpeedFactor)>,
character: Single<(&mut MoveInput, &MovementSpeedFactor)>,
rig_transform_q: Option<Single<&GlobalTransform, With<PlayerBodyMesh>>>,
) -> Result {
let (mut char_input, factor) = character.single_mut()?;
) {
let (mut char_input, factor) = character.into_inner();
if let Some(ref rig_transform) = rig_transform_q {
char_input.set(-*rig_transform.forward() * factor.0);
}
Ok(())
}

View File

@@ -1,6 +1,4 @@
use super::{ControlState, Controls};
#[cfg(feature = "client")]
use crate::player::Player;
use crate::{
GameState,
control::{CharacterInputEnabled, ControllerSet},
@@ -14,7 +12,7 @@ use bevy::{
prelude::*,
};
#[cfg(feature = "client")]
use lightyear::prelude::input::native::ActionState;
use lightyear::prelude::input::native::{ActionState, InputMarker};
use serde::{Deserialize, Serialize};
pub fn plugin(app: &mut App) {
@@ -66,7 +64,7 @@ pub struct ControllerSettings {
/// Write inputs from combined keyboard/gamepad state into the networked input buffer
/// for the local player.
fn buffer_inputs(
mut player: Single<&mut ActionState<ControlState>, With<Player>>,
mut player: Single<&mut ActionState<ControlState>, With<InputMarker<ControlState>>>,
controls: Res<ControlState>,
) {
player.0 = *controls;

View File

@@ -1,6 +1,6 @@
use crate::{
cutscene::StartCutscene, global_observer, keys::KeyCollected, movables::TriggerMovableEvent,
sounds::PlaySound,
protocol::PlaySound,
};
use bevy::{platform::collections::HashSet, prelude::*};

View File

@@ -1,7 +1,13 @@
use crate::{
GameState, billboards::Billboard, global_observer, heads_database::HeadsDatabase,
physics_layers::GameLayer, player::Player, protocol::GltfSceneRoot, sounds::PlaySound,
squish_animation::SquishAnimation, tb_entities::SecretHead,
GameState,
billboards::Billboard,
global_observer,
heads_database::HeadsDatabase,
physics_layers::GameLayer,
player::Player,
protocol::{GltfSceneRoot, PlaySound},
squish_animation::SquishAnimation,
tb_entities::SecretHead,
};
use avian3d::prelude::*;
use bevy::{

View File

@@ -5,14 +5,15 @@ use crate::animation::AnimationFlags;
use crate::{
GameState,
backpack::{BackbackSwapEvent, Backpack},
control::ControlState,
global_observer,
heads_database::HeadsDatabase,
hitpoints::Hitpoints,
player::Player,
sounds::PlaySound,
};
#[cfg(feature = "server")]
use crate::{control::ControlState, protocol::PlaySound};
use bevy::prelude::*;
#[cfg(feature = "server")]
use lightyear::prelude::input::native::ActionState;
use serde::{Deserialize, Serialize};
@@ -188,6 +189,7 @@ pub fn plugin(app: &mut App) {
FixedUpdate,
(reload, sync_hp).run_if(in_state(GameState::Playing)),
);
#[cfg(feature = "server")]
app.add_systems(FixedUpdate, on_select_active_head);
global_observer!(app, on_swap_backpack);
@@ -238,6 +240,7 @@ fn reload(
}
}
#[cfg(feature = "server")]
fn on_select_active_head(
mut commands: Commands,
mut query: Query<(&mut ActiveHeads, &mut Hitpoints, &ActionState<ControlState>), With<Player>>,

View File

@@ -2,7 +2,7 @@ use crate::{
GameState,
animation::AnimationFlags,
character::{CharacterAnimations, HasCharacterAnimations},
sounds::PlaySound,
protocol::PlaySound,
};
use bevy::prelude::*;
use serde::{Deserialize, Serialize};

View File

@@ -1,6 +1,10 @@
use crate::{
billboards::Billboard, global_observer, physics_layers::GameLayer, player::Player,
protocol::GltfSceneRoot, sounds::PlaySound, squish_animation::SquishAnimation,
billboards::Billboard,
global_observer,
physics_layers::GameLayer,
player::Player,
protocol::{GltfSceneRoot, PlaySound},
squish_animation::SquishAnimation,
};
use avian3d::prelude::*;
use bevy::{

View File

@@ -14,7 +14,6 @@ pub mod head;
pub mod head_drop;
pub mod heads;
pub mod heads_database;
pub mod heal_effect;
pub mod hitpoints;
pub mod keys;
pub mod loading_assets;
@@ -25,7 +24,6 @@ pub mod physics_layers;
pub mod platforms;
pub mod player;
pub mod protocol;
pub mod sounds;
pub mod steam;
pub mod tb_entities;
pub mod utils;

View File

@@ -124,15 +124,16 @@ pub struct LoadingPlugin;
impl Plugin for LoadingPlugin {
fn build(&self, app: &mut App) {
app.add_systems(OnExit(GameState::AssetLoading), on_exit);
app.add_loading_state(
LoadingState::new(GameState::AssetLoading)
let loading_state = LoadingState::new(GameState::AssetLoading);
let loading_state = loading_state
.continue_to_state(GameState::MapLoading)
.load_collection::<AudioAssets>()
.load_collection::<GameAssets>()
.load_collection::<HeadsAssets>()
.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);
}
}

View File

@@ -1,5 +1,5 @@
use crate::{
GameState, character::Character, global_observer, loading_assets::GameAssets,
GameState, character::HedzCharacter, global_observer, loading_assets::GameAssets,
utils::billboards::Billboard,
};
#[cfg(feature = "server")]
@@ -12,7 +12,7 @@ use crate::{
heads_database::HeadsDatabase,
hitpoints::{Hitpoints, Kill},
keys::KeySpawn,
sounds::PlaySound,
protocol::PlaySound,
tb_entities::EnemySpawn,
};
use bevy::{pbr::NotShadowCaster, prelude::*};
@@ -24,7 +24,7 @@ use std::collections::HashMap;
#[derive(Component, Reflect, PartialEq, Serialize, Deserialize)]
#[reflect(Component)]
#[require(Character)]
#[require(HedzCharacter)]
pub struct Npc;
#[derive(Resource, Reflect, Default)]
@@ -102,7 +102,6 @@ fn on_spawn_check(
None,
None,
]),
#[cfg(feature = "server")]
Replicate::to_clients(NetworkTarget::All),
))
.insert_if(Ai, || !spawn.disable_ai)

View File

@@ -1,13 +1,7 @@
use crate::{
GameState,
cash::{Cash, CashCollectEvent},
character::{AnimatedCharacter, Character},
global_observer,
head::ActiveHead,
heads::HeadChanged,
heads_database::{HeadControls, HeadsDatabase},
loading_assets::AudioAssets,
sounds::PlaySound,
character::HedzCharacter,
};
use avian3d::prelude::*;
use bevy::{
@@ -18,7 +12,7 @@ use bevy::{
use serde::{Deserialize, Serialize};
#[derive(Component, Default, Serialize, Deserialize, PartialEq)]
#[require(Character)]
#[require(HedzCharacter)]
pub struct Player;
#[derive(Component, Default, Serialize, Deserialize, PartialEq)]
@@ -36,8 +30,6 @@ pub fn plugin(app: &mut App) {
)
.run_if(in_state(GameState::Playing)),
);
global_observer!(app, on_update_head_mesh);
}
fn cursor_recenter(q_windows: Single<&mut Window, With<PrimaryWindow>>) {
@@ -103,48 +95,3 @@ fn setup_animations_marker_for_player(
}
}
}
fn on_update_head_mesh(
trigger: Trigger<HeadChanged>,
mut commands: Commands,
body_mesh: Single<(Entity, &Children), With<PlayerBodyMesh>>,
animated_characters: Query<&AnimatedCharacter>,
mut player: Single<&mut ActiveHead, With<Player>>,
head_db: Res<HeadsDatabase>,
audio_assets: Res<AudioAssets>,
sfx: Query<&AudioPlayer>,
) -> Result {
let (body_mesh, mesh_children) = *body_mesh;
let animated_char = mesh_children
.iter()
.find(|child| animated_characters.contains(*child))
.ok_or("tried to update head mesh before AnimatedCharacter was readded")?;
player.0 = trigger.0;
let head_str = head_db.head_key(trigger.0);
commands.trigger(PlaySound::Head(head_str.to_string()));
commands
.entity(animated_char)
.insert(AnimatedCharacter::new(trigger.0));
//TODO: make part of full character mesh later
for child in mesh_children.iter().filter(|child| sfx.contains(*child)) {
commands.entity(child).despawn();
}
if head_db.head_stats(trigger.0).controls == HeadControls::Plane {
commands.entity(body_mesh).with_child((
Name::new("sfx"),
AudioPlayer::new(audio_assets.jet.clone()),
PlaybackSettings {
mode: bevy::audio::PlaybackMode::Loop,
..Default::default()
},
));
}
Ok(())
}

View File

@@ -0,0 +1 @@
pub struct UnorderedReliableChannel;

View 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(())
}

View 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;

View 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);

View File

@@ -1,6 +1,11 @@
pub mod channels;
pub mod components;
pub mod events;
pub mod messages;
use crate::{
GameState,
abilities::BuildExplosionSprite,
abilities::{BuildExplosionSprite, healing::Healing},
animation::AnimationFlags,
backpack::{Backpack, backpack_ui::BackpackUiState},
camera::{CameraArmRotation, CameraTarget},
@@ -16,16 +21,15 @@ use crate::{
head::ActiveHead,
heads::{ActiveHeads, heads_ui::UiActiveHeads},
hitpoints::Hitpoints,
loading_assets::{GameAssets, HeadDropAssets},
platforms::ActivePlatform,
player::{Player, PlayerBodyMesh},
protocol::channels::UnorderedReliableChannel,
utils::triggers::TriggerAppExt,
};
use avian3d::prelude::{AngularVelocity, CollisionLayers, LinearVelocity};
use bevy::{
ecs::{component::HookContext, world::DeferredWorld},
prelude::*,
};
use bevy::prelude::*;
pub use components::*;
pub use events::*;
use happy_feet::{
grounding::GroundingState,
prelude::{
@@ -34,18 +38,19 @@ use happy_feet::{
},
};
use lightyear::prelude::{
ActionsChannel, AppComponentExt, AppMessageExt, NetworkDirection, PredictionMode,
PredictionRegistrationExt, input::native::InputPlugin,
AppChannelExt, AppComponentExt, AppMessageExt, AppTriggerExt, ChannelMode, ChannelSettings,
NetworkDirection, PredictionMode, PredictionRegistrationExt, ReliableSettings,
input::native::InputPlugin,
};
use lightyear_serde::{
SerializationError, reader::ReadInteger, registry::SerializeFns, writer::WriteInteger,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::{collections::HashMap, time::Duration};
pub fn plugin(app: &mut App) {
app.add_plugins(InputPlugin::<ControlState>::default());
app.register_type::<PlayerId>();
app.register_type::<TbMapEntityId>();
app.register_type::<TbMapIdCounter>();
app.register_type::<TbMapEntityMapping>();
@@ -53,9 +58,21 @@ pub fn plugin(app: &mut App) {
app.init_resource::<TbMapIdCounter>();
app.init_resource::<TbMapEntityMapping>();
app.add_message::<DespawnTbMapEntity>()
app.add_channel::<UnorderedReliableChannel>(ChannelSettings {
mode: ChannelMode::UnorderedReliable(ReliableSettings::default()),
send_frequency: Duration::from_millis(100),
priority: 1.0,
})
.add_direction(NetworkDirection::Bidirectional);
app.add_message::<messages::DespawnTbMapEntity>()
.add_direction(NetworkDirection::ServerToClient);
app.add_message::<messages::AssignClientPlayer>()
.add_direction(NetworkDirection::ServerToClient);
app.register_component::<components::GltfSceneRoot>();
app.register_component::<components::PlayerId>();
app.register_component::<components::TbMapEntityId>();
app.register_component::<ActiveHead>();
app.register_component::<ActiveHeads>();
app.register_component::<ActivePlatform>();
@@ -68,17 +85,17 @@ pub fn plugin(app: &mut App) {
app.register_component::<CameraTarget>();
app.register_component::<CashResource>();
app.register_component::<happy_feet::prelude::Character>();
app.register_component::<character::Character>();
app.register_component::<character::HedzCharacter>();
app.register_component::<CharacterDrag>();
app.register_component::<CharacterGravity>();
app.register_component::<CharacterMovement>();
app.register_component::<CollisionLayers>();
app.register_component::<ControllerSettings>();
app.register_component::<GltfSceneRoot>();
app.register_component::<GroundFriction>();
app.register_component::<Grounding>();
app.register_component::<GroundingConfig>();
app.register_component::<GroundingState>();
app.register_component::<Healing>();
app.register_component::<Hitpoints>();
app.register_component::<KinematicVelocity>();
app.register_component::<LinearVelocity>();
@@ -89,7 +106,6 @@ pub fn plugin(app: &mut App) {
app.register_component::<PlayerBodyMesh>();
app.register_component::<PlayerCharacterController>();
app.register_component::<SteppingConfig>();
app.register_component::<TbMapEntityId>();
app.register_component::<Transform>()
.add_prediction(PredictionMode::Full)
.add_should_rollback(transform_should_rollback);
@@ -108,52 +124,28 @@ pub fn plugin(app: &mut App) {
},
});
app.replicate_trigger::<BuildExplosionSprite, ActionsChannel>();
app.replicate_trigger::<StartCutscene, ActionsChannel>();
app.replicate_trigger::<BuildExplosionSprite, UnorderedReliableChannel>();
app.replicate_trigger::<StartCutscene, UnorderedReliableChannel>();
app.replicate_trigger::<PlayBackpackSound, ActionsChannel>();
app.replicate_trigger::<events::ClientHeadChanged, UnorderedReliableChannel>();
app.replicate_trigger::<events::PlaySound, UnorderedReliableChannel>();
app.add_trigger::<events::ClientEnteredPlaying>()
.add_direction(NetworkDirection::ClientToServer);
app.add_systems(
OnEnter(GameState::MapLoading),
|mut counter: ResMut<TbMapIdCounter>| counter.reset(),
);
global_observer!(app, spawn_gltf_scene_roots);
global_observer!(app, components::spawn_gltf_scene_roots);
}
fn transform_should_rollback(this: &Transform, that: &Transform) -> bool {
this.translation.distance_squared(that.translation) >= 0.01f32.powf(2.)
}
/// Trenchbroom map entities (spawned during map loading) must be despawned manually if the server
/// has already despawned it but the client has just loaded the map and connected
#[derive(Clone, Copy, Serialize, Deserialize)]
pub struct DespawnTbMapEntity(pub u64);
/// A unique ID assigned to every entity spawned by the trenchbroom map loader. This allows synchronizing
/// them across the network even when they are spawned initially by both sides.
#[derive(Clone, Copy, Component, Debug, Reflect, Serialize, Deserialize, PartialEq)]
#[reflect(Component)]
#[component(on_insert = TbMapEntityId::insert_id, on_remove = TbMapEntityId::remove_id)]
pub struct TbMapEntityId {
pub id: u64,
}
impl TbMapEntityId {
fn insert_id(mut world: DeferredWorld, ctx: HookContext) {
let id = world.get::<TbMapEntityId>(ctx.entity).unwrap().id;
world
.resource_mut::<TbMapEntityMapping>()
.insert(id, ctx.entity);
}
fn remove_id(mut world: DeferredWorld, ctx: HookContext) {
let id = world.get::<TbMapEntityId>(ctx.entity).unwrap().id;
world.resource_mut::<TbMapEntityMapping>().remove(&id);
}
}
/// A global allocator for `TBMapEntityId` values. Should be reset when a map begins loading.
/// A global allocator for `TbMapEntityId` values. Should be reset when a map begins loading.
#[derive(Resource, Reflect, Default)]
#[reflect(Resource)]
pub struct TbMapIdCounter(u64);
@@ -176,52 +168,3 @@ impl TbMapIdCounter {
#[derive(Resource, Reflect, Default, Deref, DerefMut)]
#[reflect(Resource)]
pub struct TbMapEntityMapping(pub HashMap<u64, Entity>);
#[derive(Clone, Event, Serialize, Deserialize)]
pub struct PlayBackpackSound {
pub open: bool,
}
#[derive(Component, Reflect, Serialize, Deserialize, PartialEq)]
#[reflect(Component)]
pub enum GltfSceneRoot {
Projectile(String),
HeadDrop(String),
Key,
}
fn spawn_gltf_scene_roots(
trigger: Trigger<OnAdd, GltfSceneRoot>,
mut commands: Commands,
gltf_roots: Query<&GltfSceneRoot>,
head_drop_assets: Res<HeadDropAssets>,
assets: Res<GameAssets>,
gltfs: Res<Assets<Gltf>>,
) -> Result {
let root = gltf_roots.get(trigger.target())?;
let get_scene = |gltf: Handle<Gltf>, index: usize| {
let gltf = gltfs.get(&gltf).unwrap();
gltf.scenes[index].clone()
};
let scene = match root {
GltfSceneRoot::Projectile(addr) => get_scene(
assets.projectiles[format!("{addr}.glb").as_str()].clone(),
0,
),
GltfSceneRoot::HeadDrop(addr) => {
let gltf = head_drop_assets
.meshes
.get(format!("{addr}.glb").as_str())
.cloned();
let gltf = gltf.unwrap_or_else(|| head_drop_assets.meshes["none.glb"].clone());
get_scene(gltf, 0)
}
GltfSceneRoot::Key => assets.mesh_key.clone(),
};
commands.entity(trigger.target()).insert(SceneRoot(scene));
Ok(())
}

View File

@@ -1,5 +1,6 @@
use crate::{
cash::Cash, loading_assets::GameAssets, physics_layers::GameLayer, protocol::TbMapIdCounter,
utils::global_observer,
};
use avian3d::prelude::*;
use bevy::{
@@ -206,10 +207,10 @@ pub fn plugin(app: &mut App) {
app.register_type::<CashSpawn>();
app.register_type::<SecretHead>();
app.add_observer(tb_component_setup::<CashSpawn>);
app.add_observer(tb_component_setup::<Platform>);
app.add_observer(tb_component_setup::<PlatformTarget>);
app.add_observer(tb_component_setup::<Movable>);
global_observer!(app, tb_component_setup::<CashSpawn>);
global_observer!(app, tb_component_setup::<Platform>);
global_observer!(app, tb_component_setup::<PlatformTarget>);
global_observer!(app, tb_component_setup::<Movable>);
}
fn tb_component_setup<C: Component>(trigger: Trigger<OnAdd, C>, world: &mut World) {

View File

@@ -2,11 +2,26 @@ use bevy::prelude::*;
#[macro_export]
macro_rules! global_observer {
($app:expr,$system:expr) => {{
($app:expr, $($system:tt)*) => {{
$app.world_mut()
.add_observer($system)
.insert(Name::new(stringify!($system)))
.add_observer($($system)*)
.insert(global_observer!(@name $($system)*))
}};
(@name $system:ident ::< $($param:ident),+ $(,)? >) => {{
let mut name = String::new();
name.push_str(stringify!($system));
name.push_str("::<");
$(
name.push_str(std::any::type_name::<$param>());
)+
name.push_str(">");
Name::new(name)
}};
(@name $system:expr) => {
Name::new(stringify!($system))
};
}
pub use global_observer;
@@ -30,6 +45,6 @@ fn global_observers(
};
for o in query.iter() {
cmds.entity(root).add_child(o);
cmds.entity(o).try_insert(ChildOf(root));
}
}

View File

@@ -1,3 +1,4 @@
use crate::utils::global_observer;
use bevy::{ecs::system::SystemParam, prelude::*};
use lightyear::prelude::{AppTriggerExt, Channel, NetworkDirection, RemoteTrigger, TriggerSender};
use serde::{Deserialize, Serialize};
@@ -31,8 +32,8 @@ impl TriggerAppExt for App {
) {
self.add_trigger::<M>()
.add_direction(NetworkDirection::ServerToClient);
self.add_observer(replicate_trigger_to_clients::<M, C>);
self.add_observer(remote_to_local_trigger::<M>);
global_observer!(self, replicate_trigger_to_clients::<M, C>);
global_observer!(self, remote_to_local_trigger::<M>);
}
}

View File

@@ -12,6 +12,9 @@ run *args:
cargo b {{server_args}}
RUST_BACKTRACE=1 cargo r {{client_args}} -- {{args}}
client *args:
RUST_BACKTRACE=1 cargo r {{client_args}} -- {{args}}
server:
RUST_BACKTRACE=1 cargo r {{server_args}}
@@ -19,6 +22,9 @@ dbg *args:
cargo b {{server_args}},dbg
RUST_BACKTRACE=1 cargo r {{client_args}},dbg -- {{args}}
dbg-client *args:
RUST_BACKTRACE=1 cargo r {{client_args}},dbg -- {{args}}
dbg-server:
RUST_BACKTRACE=1 cargo r {{server_args}},dbg